This commit is contained in:
Vyacheslav Chigrin 2024-08-05 19:43:27 -04:00 committed by GitHub
commit 8b99e6e4e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 400 additions and 8 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ cmake-build-*
cmake-*/
CMakeFiles
**/CMakeCache.txt
CMakeLists.txt.user*
cmake_install.cmake
Makefile
build

View file

@ -14,6 +14,8 @@ This page describes the BLE implementation and API built in this firmware.
- [BLE Services](#ble-services)
- [CTS](#cts)
- [ANS](#ans)
- [BLE Clients](#ble-clients)
- [IAC](#iac)
- [Getting Information](#getting-information)
- [Firmware Version](#firmware-version)
- [Battery Level](#battery-level)
@ -113,6 +115,16 @@ The following custom services are implemented in InfiniTime:
![ANS sequence diagram](./ble/ans_sequence.png "ANS sequence diagram")
## BLE clients
### IAC
InfiniTime implements Immediade Alert Service client, that can be used to send notifications to companion app.
This is useful for "Find my Phone" functionality.
More documentation about this service can be found here.
[Immediade Alert Service](https://www.bluetooth.com/specifications/specs/immediate-alert-service-1-0/)
---
### Getting Information

View file

@ -385,6 +385,7 @@ list(APPEND SOURCE_FILES
displayapp/screens/Notifications.cpp
displayapp/screens/Twos.cpp
displayapp/screens/HeartRate.cpp
displayapp/screens/FindMyPhone.cpp
displayapp/screens/FlashLight.cpp
displayapp/screens/List.cpp
displayapp/screens/CheckboxList.cpp
@ -459,6 +460,7 @@ list(APPEND SOURCE_FILES
components/ble/BatteryInformationService.cpp
components/ble/FSService.cpp
components/ble/ImmediateAlertService.cpp
components/ble/ImmediateAlertClient.cpp
components/ble/ServiceDiscovery.cpp
components/ble/HeartRateService.cpp
components/ble/MotionService.cpp
@ -527,6 +529,7 @@ list(APPEND RECOVERY_SOURCE_FILES
components/ble/BatteryInformationService.cpp
components/ble/FSService.cpp
components/ble/ImmediateAlertService.cpp
components/ble/ImmediateAlertClient.cpp
components/ble/ServiceDiscovery.cpp
components/ble/NavigationService.cpp
components/ble/HeartRateService.cpp
@ -606,6 +609,7 @@ set(INCLUDE_FILES
displayapp/Apps.h
displayapp/screens/Notifications.h
displayapp/screens/HeartRate.h
displayapp/screens/FindMyPhone.h
displayapp/screens/Metronome.h
displayapp/screens/Motion.h
displayapp/screens/Timer.h
@ -645,6 +649,7 @@ set(INCLUDE_FILES
components/ble/BatteryInformationService.h
components/ble/FSService.h
components/ble/ImmediateAlertService.h
components/ble/ImmediateAlertClient.h
components/ble/ServiceDiscovery.h
components/ble/BleClient.h
components/ble/HeartRateService.h
@ -832,7 +837,7 @@ if (${CMAKE_BUILD_TYPE} STREQUAL "Debug")
# add_definitions(-DCLOCK_CONFIG_LOG_LEVEL=4)
# add_definitions(-DRTC_CONFIG_LOG_ENABLED=1)
# add_definitions(-DRTC_CONFIG_LOG_LEVEL=4)
# Nimble Logging
add_definitions(-DMYNEWT_VAL_NEWT_FEATURE_LOGCFG=1)
# add_definitions(-DMYNEWT_VAL_LOG_LEVEL=0)

View file

@ -62,7 +62,7 @@
#define configTICK_RATE_HZ 1024
#define configMAX_PRIORITIES (3)
#define configMINIMAL_STACK_SIZE (120)
#define configTOTAL_HEAP_SIZE (1024 * 40)
#define configTOTAL_HEAP_SIZE (1024 * 39)
#define configMAX_TASK_NAME_LEN (4)
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1

View file

@ -0,0 +1,113 @@
#include "components/ble/ImmediateAlertClient.h"
#include <cstring>
#include <nrf_log.h>
#include "systemtask/SystemTask.h"
using namespace Pinetime::Controllers;
constexpr ble_uuid16_t ImmediateAlertClient::immediateAlertClientUuid;
constexpr ble_uuid16_t ImmediateAlertClient::alertLevelCharacteristicUuid;
namespace {
int OnDiscoveryEventCallback(uint16_t conn_handle, const struct ble_gatt_error* error, const struct ble_gatt_svc* service, void* arg) {
auto client = static_cast<ImmediateAlertClient*>(arg);
return client->OnDiscoveryEvent(conn_handle, error, service);
}
int OnImmediateAlertCharacteristicDiscoveredCallback(uint16_t conn_handle,
const struct ble_gatt_error* error,
const struct ble_gatt_chr* chr,
void* arg) {
auto client = static_cast<ImmediateAlertClient*>(arg);
return client->OnCharacteristicDiscoveryEvent(conn_handle, error, chr);
}
}
ImmediateAlertClient::ImmediateAlertClient(Pinetime::System::SystemTask& systemTask)
: systemTask {systemTask},
characteristicDefinition {{
.uuid = &alertLevelCharacteristicUuid.u,
.arg = this,
.flags = BLE_GATT_CHR_F_WRITE_NO_RSP,
},
{0}},
serviceDefinition {
{/* Device Information Service */
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &immediateAlertClientUuid.u,
.characteristics = characteristicDefinition},
{0},
} {
}
void ImmediateAlertClient::Init() {
}
bool ImmediateAlertClient::OnDiscoveryEvent(uint16_t connectionHandle, const ble_gatt_error* error, const ble_gatt_svc* service) {
if (service == nullptr && error->status == BLE_HS_EDONE) {
if (isDiscovered) {
NRF_LOG_INFO("IAS found, starting characteristics discovery");
ble_gattc_disc_all_chrs(connectionHandle, iasStartHandle, iasEndHandle, OnImmediateAlertCharacteristicDiscoveredCallback, this);
} else {
NRF_LOG_INFO("IAS not found");
onServiceDiscovered(connectionHandle);
}
return true;
}
if (service != nullptr && ble_uuid_cmp(&immediateAlertClientUuid.u, &service->uuid.u) == 0) {
NRF_LOG_INFO("IAS discovered : 0x%x - 0x%x", service->start_handle, service->end_handle);
isDiscovered = true;
iasStartHandle = service->start_handle;
iasEndHandle = service->end_handle;
}
return false;
}
int ImmediateAlertClient::OnCharacteristicDiscoveryEvent(uint16_t conn_handle,
const ble_gatt_error* error,
const ble_gatt_chr* characteristic) {
if (error->status != 0 && error->status != BLE_HS_EDONE) {
NRF_LOG_INFO("IAS Characteristic discovery ERROR");
onServiceDiscovered(conn_handle);
return 0;
}
if (characteristic == nullptr && error->status == BLE_HS_EDONE) {
if (!isCharacteristicDiscovered) {
NRF_LOG_INFO("IAS Characteristic discovery unsuccessful");
onServiceDiscovered(conn_handle);
}
return 0;
}
if (characteristic != nullptr && ble_uuid_cmp(&alertLevelCharacteristicUuid.u, &characteristic->uuid.u) == 0) {
NRF_LOG_INFO("AIS Characteristic discovered : 0x%x", characteristic->val_handle);
isCharacteristicDiscovered = true;
alertLevelHandle = characteristic->val_handle;
}
return 0;
}
void ImmediateAlertClient::Discover(uint16_t connectionHandle, std::function<void(uint16_t)> onServiceDiscovered) {
NRF_LOG_INFO("[IAS] Starting discovery");
this->onServiceDiscovered = onServiceDiscovered;
ble_gattc_disc_svc_by_uuid(connectionHandle, &immediateAlertClientUuid.u, OnDiscoveryEventCallback, this);
}
bool ImmediateAlertClient::sendImmediateAlert(ImmediateAlertClient::Levels level) {
auto* om = ble_hs_mbuf_from_flat(&level, 1);
uint16_t connectionHandle = systemTask.nimble().connHandle();
if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) {
return false;
}
ble_gattc_write_no_rsp(connectionHandle, alertLevelHandle, om);
return true;
}

View file

@ -0,0 +1,61 @@
#pragma once
#define min // workaround: nimble's min/max macros conflict with libstdc++
#define max
#include <host/ble_gap.h>
#undef max
#undef min
#include <cstdint>
#include "components/ble/BleClient.h"
namespace Pinetime {
namespace System {
class SystemTask;
}
namespace Controllers {
class NotificationManager;
class ImmediateAlertClient : public BleClient {
public:
enum class Levels : uint8_t { NoAlert = 0, MildAlert = 1, HighAlert = 2 };
ImmediateAlertClient(Pinetime::System::SystemTask& systemTask);
void Init();
bool OnDiscoveryEvent(uint16_t connectionHandle, const ble_gatt_error* error, const ble_gatt_svc* service);
int OnCharacteristicDiscoveryEvent(uint16_t conn_handle, const ble_gatt_error* error, const ble_gatt_chr* characteristic);
bool sendImmediateAlert(Levels level);
static constexpr const ble_uuid16_t* Uuid() {
return &ImmediateAlertClient::immediateAlertClientUuid;
}
static constexpr const ble_uuid16_t* AlertLevelCharacteristicUuid() {
return &ImmediateAlertClient::alertLevelCharacteristicUuid;
}
void Discover(uint16_t connectionHandle, std::function<void(uint16_t)> lambda) override;
private:
Pinetime::System::SystemTask& systemTask;
static constexpr uint16_t immediateAlertClientId {0x1802};
static constexpr uint16_t alertLevelId {0x2A06};
static constexpr ble_uuid16_t immediateAlertClientUuid {.u {.type = BLE_UUID_TYPE_16}, .value = immediateAlertClientId};
static constexpr ble_uuid16_t alertLevelCharacteristicUuid {.u {.type = BLE_UUID_TYPE_16}, .value = alertLevelId};
bool isDiscovered = false;
uint16_t iasStartHandle;
uint16_t iasEndHandle;
bool isCharacteristicDiscovered = false;
struct ble_gatt_chr_def characteristicDefinition[3];
struct ble_gatt_svc_def serviceDefinition[2];
uint16_t alertLevelHandle;
std::function<void(uint16_t)> onServiceDiscovered;
};
}
}

View file

@ -46,10 +46,11 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
weatherService {dateTimeController},
batteryInformationService {batteryController},
immediateAlertService {systemTask, notificationManager},
iaClient {systemTask},
heartRateService {*this, heartRateController},
motionService {*this, motionController},
fsService {systemTask, fs},
serviceDiscovery({&currentTimeClient, &alertNotificationClient}) {
serviceDiscovery({&currentTimeClient, &alertNotificationClient, &iaClient}) {
}
void nimble_on_reset(int reason) {
@ -95,6 +96,7 @@ void NimbleController::Init() {
dfuService.Init();
batteryInformationService.Init();
immediateAlertService.Init();
iaClient.Init();
heartRateService.Init();
motionService.Init();
fsService.Init();

View file

@ -17,6 +17,7 @@
#include "components/ble/FSService.h"
#include "components/ble/HeartRateService.h"
#include "components/ble/ImmediateAlertService.h"
#include "components/ble/ImmediateAlertClient.h"
#include "components/ble/MusicService.h"
#include "components/ble/NavigationService.h"
#include "components/ble/ServiceDiscovery.h"
@ -71,6 +72,10 @@ namespace Pinetime {
return weatherService;
};
Pinetime::Controllers::ImmediateAlertClient& immediateAlertClient() {
return iaClient;
}
uint16_t connHandle();
void NotifyBatteryLevel(uint8_t level);
@ -103,6 +108,7 @@ namespace Pinetime {
NavigationService navService;
BatteryInformationService batteryInformationService;
ImmediateAlertService immediateAlertService;
ImmediateAlertClient iaClient;
HeartRateService heartRateService;
MotionService motionService;
FSService fsService;

View file

@ -4,7 +4,7 @@
using namespace Pinetime::Controllers;
ServiceDiscovery::ServiceDiscovery(std::array<BleClient*, 2>&& clients) : clients {clients} {
ServiceDiscovery::ServiceDiscovery(std::array<BleClient*, 3>&& clients) : clients {clients} {
}
void ServiceDiscovery::StartDiscovery(uint16_t connectionHandle) {
@ -29,4 +29,4 @@ void ServiceDiscovery::DiscoverNextService(uint16_t connectionHandle) {
this->OnServiceDiscovered(connectionHandle);
};
(*clientIterator)->Discover(connectionHandle, discoverNextService);
}
}

View file

@ -9,13 +9,13 @@ namespace Pinetime {
class ServiceDiscovery {
public:
ServiceDiscovery(std::array<BleClient*, 2>&& bleClients);
ServiceDiscovery(std::array<BleClient*, 3>&& bleClients);
void StartDiscovery(uint16_t connectionHandle);
private:
BleClient** clientIterator;
std::array<BleClient*, 2> clients;
std::array<BleClient*, 3> clients;
void OnServiceDiscovered(uint16_t connectionHandle);
void DiscoverNextService(uint16_t connectionHandle);
};

View file

@ -1,6 +1,7 @@
#include "displayapp/DisplayApp.h"
#include <libraries/log/nrf_log.h>
#include "displayapp/screens/HeartRate.h"
#include "displayapp/screens/FindMyPhone.h"
#include "displayapp/screens/Motion.h"
#include "displayapp/screens/Timer.h"
#include "displayapp/screens/Alarm.h"

View file

@ -42,6 +42,7 @@ namespace Pinetime {
SettingChimes,
SettingShakeThreshold,
SettingBluetooth,
FindMyPhone,
Error
};

View file

@ -13,6 +13,7 @@ else ()
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::FindMyPhone")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather")
#set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion")
set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware")

View file

@ -7,7 +7,7 @@
},
{
"file": "FontAwesome5-Solid+Brands+Regular.woff",
"range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743"
"range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf002"
}
],
"bpp": 1,

View file

@ -0,0 +1,126 @@
#include "displayapp/screens/FindMyPhone.h"
#include <lvgl/lvgl.h>
#include "displayapp/DisplayApp.h"
#include "displayapp/InfiniTimeTheme.h"
using namespace Pinetime::Applications::Screens;
namespace {
static constexpr char defaultLabelText[] = "Find my phone";
static constexpr char alertSentLabelText[] = "Alert sent";
static constexpr char noConnectionLabelText[] = "No connection";
static constexpr auto restoreLabelTimeoutTicks = pdMS_TO_TICKS(2 * 1000);
void btnImmediateAlertEventHandler(lv_obj_t* obj, lv_event_t event) {
auto* screen = static_cast<FindMyPhone*>(obj->user_data);
screen->OnImmediateAlertEvent(obj, event);
}
void RestoreLabelTaskCallback(lv_task_t* task) {
auto* screen = static_cast<FindMyPhone*>(task->user_data);
screen->RestoreLabelText();
screen->StopRestoreLabelTask();
}
}
FindMyPhone::FindMyPhone(Pinetime::Controllers::ImmediateAlertClient& immediateAlertClient) : immediateAlertClient {immediateAlertClient} {
last_level = Pinetime::Controllers::ImmediateAlertClient::Levels::NoAlert;
container = lv_cont_create(lv_scr_act(), nullptr);
lv_obj_set_size(container, LV_HOR_RES, LV_VER_RES);
lv_obj_set_style_local_bg_color(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK);
lv_obj_set_style_local_pad_all(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 0);
lv_obj_set_style_local_pad_inner(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 0);
lv_obj_set_style_local_border_width(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 0);
label_title = lv_label_create(lv_scr_act(), nullptr);
lv_label_set_text_static(label_title, defaultLabelText);
lv_obj_set_style_local_text_color(label_title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray);
lv_obj_align(label_title, nullptr, LV_ALIGN_CENTER, 0, -40);
bt_none = lv_btn_create(container, nullptr);
bt_none->user_data = this;
lv_obj_set_event_cb(bt_none, btnImmediateAlertEventHandler);
lv_obj_set_size(bt_none, 76, 76);
lv_obj_align(bt_none, nullptr, LV_ALIGN_IN_BOTTOM_LEFT, 0, 0);
label_none = lv_label_create(bt_none, nullptr);
lv_label_set_text_static(label_none, "None");
lv_obj_set_style_local_bg_color(bt_none, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray);
bt_mild = lv_btn_create(container, nullptr);
bt_mild->user_data = this;
lv_obj_set_event_cb(bt_mild, btnImmediateAlertEventHandler);
lv_obj_set_size(bt_mild, 76, 76);
lv_obj_align(bt_mild, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
label_mild = lv_label_create(bt_mild, nullptr);
lv_label_set_text_static(label_mild, "Mild");
lv_obj_set_style_local_bg_color(bt_mild, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::highlight);
bt_high = lv_btn_create(container, nullptr);
bt_high->user_data = this;
lv_obj_set_event_cb(bt_high, btnImmediateAlertEventHandler);
lv_obj_set_size(bt_high, 76, 76);
lv_obj_align(bt_high, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);
label_high = lv_label_create(bt_high, nullptr);
lv_label_set_text_static(label_high, "High");
lv_obj_set_style_local_bg_color(bt_high, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED);
}
FindMyPhone::~FindMyPhone() {
lv_obj_clean(lv_scr_act());
}
void FindMyPhone::OnImmediateAlertEvent(lv_obj_t* obj, lv_event_t event) {
if (event == LV_EVENT_CLICKED) {
if (obj == bt_none) {
last_level = Pinetime::Controllers::ImmediateAlertClient::Levels::NoAlert;
} else if (obj == bt_mild) {
last_level = Pinetime::Controllers::ImmediateAlertClient::Levels::MildAlert;
} else if (obj == bt_high) {
last_level = Pinetime::Controllers::ImmediateAlertClient::Levels::HighAlert;
}
UpdateImmediateAlerts();
}
}
void FindMyPhone::UpdateImmediateAlerts() {
switch (last_level) {
case Pinetime::Controllers::ImmediateAlertClient::Levels::NoAlert:
lv_obj_set_style_local_text_color(label_title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray);
break;
case Pinetime::Controllers::ImmediateAlertClient::Levels::MildAlert:
lv_obj_set_style_local_text_color(label_title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::highlight);
break;
case Pinetime::Controllers::ImmediateAlertClient::Levels::HighAlert:
lv_obj_set_style_local_text_color(label_title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED);
break;
}
if (immediateAlertClient.sendImmediateAlert(last_level)) {
lv_label_set_text_static(label_title, alertSentLabelText);
} else {
lv_label_set_text_static(label_title, noConnectionLabelText);
}
ScheduleRestoreLabelTask();
}
void FindMyPhone::ScheduleRestoreLabelTask() {
if (taskRestoreLabelText) {
return;
}
taskRestoreLabelText = lv_task_create(RestoreLabelTaskCallback, restoreLabelTimeoutTicks, LV_TASK_PRIO_MID, this);
}
void FindMyPhone::StopRestoreLabelTask() {
if (taskRestoreLabelText) {
lv_task_del(taskRestoreLabelText);
taskRestoreLabelText = nullptr;
}
}
void FindMyPhone::RestoreLabelText() {
lv_label_set_text_static(label_title, defaultLabelText);
lv_obj_set_style_local_text_color(label_title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray);
}

View file

@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
#include <chrono>
#include "displayapp/screens/Screen.h"
#include "Symbols.h"
#include "systemtask/SystemTask.h"
#include "components/ble/ImmediateAlertClient.h"
#include <lvgl/src/lv_core/lv_style.h>
#include <lvgl/src/lv_core/lv_obj.h>
namespace Pinetime {
namespace Controllers {
class ImmediateAlertClient;
}
namespace Applications {
namespace Screens {
class FindMyPhone : public Screen {
public:
explicit FindMyPhone(Pinetime::Controllers::ImmediateAlertClient& immediateAlertClient);
~FindMyPhone() override;
void OnImmediateAlertEvent(lv_obj_t* obj, lv_event_t event);
void ScheduleRestoreLabelTask();
void StopRestoreLabelTask();
void RestoreLabelText();
private:
Pinetime::Controllers::ImmediateAlertClient& immediateAlertClient;
void UpdateImmediateAlerts();
lv_obj_t* container;
lv_obj_t* label_title;
lv_obj_t* bt_none;
lv_obj_t* bt_high;
lv_obj_t* bt_mild;
lv_obj_t* label_none;
lv_obj_t* label_high;
lv_obj_t* label_mild;
lv_task_t* taskRestoreLabelText = nullptr;
Pinetime::Controllers::ImmediateAlertClient::Levels last_level;
};
}
template <>
struct AppTraits<Apps::FindMyPhone> {
static constexpr Apps app = Apps::FindMyPhone;
static constexpr const char* icon = Screens::Symbols::magnifyingGlass;
static Screens::Screen* Create(AppControllers& controllers) {
return new Screens::FindMyPhone(controllers.systemTask->nimble().immediateAlertClient());
};
};
}
}

View file

@ -39,6 +39,7 @@ namespace Pinetime {
static constexpr const char* eye = "\xEF\x81\xAE";
static constexpr const char* home = "\xEF\x80\x95";
static constexpr const char* sleep = "\xEE\xBD\x84";
static constexpr const char* magnifyingGlass = "\xEF\x80\x82"; // f002
// fontawesome_weathericons.c
// static constexpr const char* sun = "\xEF\x86\x85";