diff --git a/doc/ble.md b/doc/ble.md index 2a4ecf4cb8..da2c1ec527 100644 --- a/doc/ble.md +++ b/doc/ble.md @@ -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 an Immediate Alert Service client, which can be used to send alerts to the companion app. +This is useful for "Find my Phone" functionality. + +More documentation about this service can be found here. + +[Immediate Alert Service](https://www.bluetooth.com/specifications/specs/immediate-alert-service-1-0/) --- ### Getting Information diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd2e656a4..fe806d27bb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -386,6 +386,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 @@ -461,6 +462,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 @@ -530,6 +532,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 @@ -610,6 +613,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 @@ -649,6 +653,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 @@ -841,7 +846,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) diff --git a/src/components/ble/ImmediateAlertClient.cpp b/src/components/ble/ImmediateAlertClient.cpp new file mode 100644 index 0000000000..4b50f6c537 --- /dev/null +++ b/src/components/ble/ImmediateAlertClient.cpp @@ -0,0 +1,114 @@ +#include "components/ble/ImmediateAlertClient.h" +#include +#include +#include "systemtask/SystemTask.h" + +using namespace Pinetime::Controllers; + +constexpr ble_uuid16_t ImmediateAlertClient::immediateAlertClientUuid; +constexpr ble_uuid16_t ImmediateAlertClient::alertLevelCharacteristicUuid; + +ImmediateAlertClient::ImmediateAlertClient(Pinetime::System::SystemTask& systemTask) : systemTask {systemTask} { +} + +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 (iasHandles.has_value()) { + NRF_LOG_INFO("[IAS] service found, starting characteristics discovery"); + + ble_gattc_disc_all_chrs(connectionHandle, + iasHandles->startHandle, + iasHandles->endHandle, + OnImmediateAlertCharacteristicDiscoveredCallback, + this); + } else { + NRF_LOG_INFO("[IAS] service 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); + iasHandles.emplace(HandleRange { + .startHandle = service->start_handle, + .endHandle = 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 (!alertLevelHandle.has_value()) { + NRF_LOG_INFO("[IAS] Alert level characteristic not found."); + onServiceDiscovered(conn_handle); + } + + return 0; + } + + if (characteristic != nullptr && ble_uuid_cmp(&alertLevelCharacteristicUuid.u, &characteristic->uuid.u) == 0) { + NRF_LOG_INFO("[IAS] Characteristic discovered : 0x%x", characteristic->val_handle); + alertLevelHandle = characteristic->val_handle; + state = State::Connected; + } + return 0; +} + +void ImmediateAlertClient::Discover(uint16_t connectionHandle, std::function onServiceDiscovered) { + NRF_LOG_INFO("[IAS] Starting discovery"); + this->onServiceDiscovered = onServiceDiscovered; + state = State::NoIAS; + ble_gattc_disc_svc_by_uuid(connectionHandle, &immediateAlertClientUuid.u, OnDiscoveryEventCallback, this); +} + +void ImmediateAlertClient::Reset() { + state = State::NoConnection; + iasHandles = std::nullopt; + alertLevelHandle = std::nullopt; +} + +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; + } + if (!alertLevelHandle.has_value()) { + return false; + } + + return ble_gattc_write_no_rsp(connectionHandle, *alertLevelHandle, om) == 0; +} + +int ImmediateAlertClient::OnDiscoveryEventCallback(uint16_t conn_handle, + const struct ble_gatt_error* error, + const struct ble_gatt_svc* service, + void* arg) { + auto client = static_cast(arg); + return client->OnDiscoveryEvent(conn_handle, error, service); +} + +int ImmediateAlertClient::OnImmediateAlertCharacteristicDiscoveredCallback(uint16_t conn_handle, + const struct ble_gatt_error* error, + const struct ble_gatt_chr* chr, + void* arg) { + auto client = static_cast(arg); + return client->OnCharacteristicDiscoveryEvent(conn_handle, error, chr); +} diff --git a/src/components/ble/ImmediateAlertClient.h b/src/components/ble/ImmediateAlertClient.h new file mode 100644 index 0000000000..33a2bbf6e0 --- /dev/null +++ b/src/components/ble/ImmediateAlertClient.h @@ -0,0 +1,77 @@ +#pragma once +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include +#undef max +#undef min +#include +#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 }; + enum class State { + NoConnection, + NoIAS, + Connected, + }; + + explicit ImmediateAlertClient(Pinetime::System::SystemTask& systemTask); + void Init(); + + bool SendImmediateAlert(Levels level); + + State GetState() const { + return state; + } + + void Discover(uint16_t connectionHandle, std::function lambda) override; + void Reset(); + + private: + 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); + + static constexpr const ble_uuid16_t* Uuid() { + return &ImmediateAlertClient::immediateAlertClientUuid; + } + + static constexpr const ble_uuid16_t* AlertLevelCharacteristicUuid() { + return &ImmediateAlertClient::alertLevelCharacteristicUuid; + } + + static int + OnDiscoveryEventCallback(uint16_t conn_handle, const struct ble_gatt_error* error, const struct ble_gatt_svc* service, void* arg); + static int OnImmediateAlertCharacteristicDiscoveredCallback(uint16_t conn_handle, + const struct ble_gatt_error* error, + const struct ble_gatt_chr* chr, + void* arg); + + 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}; + + struct HandleRange { + uint16_t startHandle; + uint16_t endHandle; + }; + + std::optional iasHandles; + std::optional alertLevelHandle; + std::function onServiceDiscovered; + State state {State::NoConnection}; + }; + } +} diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 5059007ab9..db084990c3 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -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({¤tTimeClient, &alertNotificationClient}) { + serviceDiscovery({¤tTimeClient, &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(); @@ -200,6 +202,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { /* Connection failed; resume advertising. */ currentTimeClient.Reset(); alertNotificationClient.Reset(); + iaClient.Reset(); connectionHandle = BLE_HS_CONN_HANDLE_NONE; bleController.Disconnect(); fastAdvCount = 0; @@ -223,6 +226,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { currentTimeClient.Reset(); alertNotificationClient.Reset(); + iaClient.Reset(); connectionHandle = BLE_HS_CONN_HANDLE_NONE; if (bleController.IsConnected()) { bleController.Disconnect(); diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 597ef0cc34..46ec0dcffe 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -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; diff --git a/src/components/ble/ServiceDiscovery.cpp b/src/components/ble/ServiceDiscovery.cpp index 03bcfeb47b..554cf3e7b2 100644 --- a/src/components/ble/ServiceDiscovery.cpp +++ b/src/components/ble/ServiceDiscovery.cpp @@ -4,7 +4,7 @@ using namespace Pinetime::Controllers; -ServiceDiscovery::ServiceDiscovery(std::array&& clients) : clients {clients} { +ServiceDiscovery::ServiceDiscovery(std::array&& 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); -} \ No newline at end of file +} diff --git a/src/components/ble/ServiceDiscovery.h b/src/components/ble/ServiceDiscovery.h index fc3b38c0a6..039862a442 100644 --- a/src/components/ble/ServiceDiscovery.h +++ b/src/components/ble/ServiceDiscovery.h @@ -9,13 +9,13 @@ namespace Pinetime { class ServiceDiscovery { public: - ServiceDiscovery(std::array&& bleClients); + ServiceDiscovery(std::array&& bleClients); void StartDiscovery(uint16_t connectionHandle); private: BleClient** clientIterator; - std::array clients; + std::array clients; void OnServiceDiscovered(uint16_t connectionHandle); void DiscoverNextService(uint16_t connectionHandle); }; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index bfd7dbed6d..29549dc087 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -1,6 +1,7 @@ #include "displayapp/DisplayApp.h" #include #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" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index f6feeb7b6d..afeade4249 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -43,6 +43,7 @@ namespace Pinetime { SettingChimes, SettingShakeThreshold, SettingBluetooth, + FindMyPhone, Error }; diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..57887a2d80 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -14,6 +14,7 @@ else () 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::Calculator") + 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") diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index fea3160572..092beaadb5 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -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, 0xf1ec, 0xf55a" + "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, 0xf1ec, 0xf55a, 0xf002" } ], "bpp": 1, diff --git a/src/displayapp/screens/FindMyPhone.cpp b/src/displayapp/screens/FindMyPhone.cpp new file mode 100644 index 0000000000..d6fca72669 --- /dev/null +++ b/src/displayapp/screens/FindMyPhone.cpp @@ -0,0 +1,153 @@ +#include "displayapp/screens/FindMyPhone.h" +#include + +#include "displayapp/DisplayApp.h" +#include "displayapp/InfiniTimeTheme.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + static constexpr char alertSentLabelText[] = "Alerting"; + + static constexpr char stopLabelText[] = "Stop"; + static constexpr char ringLabelText[] = "Ring"; + + void btnImmediateAlertEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + screen->OnImmediateAlertEvent(obj, event); + } +} + +const FindMyPhone::LabelState FindMyPhone::stoppedLabelState { + .text = "Alert stopped", + .color = LV_COLOR_WHITE, +}; + +const FindMyPhone::LabelState FindMyPhone::noConnectionLabelState { + .text = "No connection", + .color = LV_COLOR_WHITE, +}; + +const FindMyPhone::LabelState FindMyPhone::noServiceLabelState { + .text = "No service", + .color = LV_COLOR_WHITE, +}; + +const FindMyPhone::LabelState FindMyPhone::defaultLabelState { + .text = "Ready", + .color = LV_COLOR_WHITE, +}; + +const FindMyPhone::LabelState FindMyPhone::alertingLabelState { + .text = "Alerting", + .color = LV_COLOR_RED, +}; + +const FindMyPhone::LabelState FindMyPhone::sendFailedLabelState { + .text = "Communication fail", + .color = LV_COLOR_WHITE, +}; + +FindMyPhone::FindMyPhone(Pinetime::Controllers::ImmediateAlertClient& immediateAlertClient) : immediateAlertClient {immediateAlertClient} { + 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); + + lblTitle = lv_label_create(lv_scr_act(), nullptr); + + btnStop = lv_btn_create(container, nullptr); + btnStop->user_data = this; + lv_obj_set_event_cb(btnStop, btnImmediateAlertEventHandler); + lv_obj_set_size(btnStop, 114, 76); + lv_obj_align(btnStop, nullptr, LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + lblStop = lv_label_create(btnStop, nullptr); + lv_label_set_text_static(lblStop, stopLabelText); + lv_obj_set_style_local_bg_color(btnStop, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt); + + btnRing = lv_btn_create(container, nullptr); + btnRing->user_data = this; + lv_obj_set_event_cb(btnRing, btnImmediateAlertEventHandler); + lv_obj_set_size(btnRing, 114, 76); + lv_obj_align(btnRing, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0); + lblRing = lv_label_create(btnRing, nullptr); + lv_label_set_text_static(lblRing, ringLabelText); + lv_obj_set_style_local_bg_color(btnRing, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); + refreshTask = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + // Refresh ASAP to properly set buttons state. + Refresh(); +} + +FindMyPhone::~FindMyPhone() { + lv_task_del(refreshTask); + lv_obj_clean(lv_scr_act()); +} + +void FindMyPhone::OnImmediateAlertEvent(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + if (obj == btnStop) { + lastUserInitiatedLevel = Pinetime::Controllers::ImmediateAlertClient::Levels::NoAlert; + } else if (obj == btnRing) { + lastUserInitiatedLevel = Pinetime::Controllers::ImmediateAlertClient::Levels::HighAlert; + } else { + // Unknown button? + ASSERT(false); + return; + } + if (immediateAlertClient.SendImmediateAlert(*lastUserInitiatedLevel)) { + lastSendFailed = false; + } else { + lastSendFailed = true; + } + } +} + +const FindMyPhone::LabelState& FindMyPhone::GetLabelState() const { + const auto service_state = immediateAlertClient.GetState(); + switch (service_state) { + case Pinetime::Controllers::ImmediateAlertClient::State::NoConnection: + return noConnectionLabelState; + case Pinetime::Controllers::ImmediateAlertClient::State::NoIAS: + return noServiceLabelState; + case Pinetime::Controllers::ImmediateAlertClient::State::Connected: + break; + } + if (lastSendFailed) { + return sendFailedLabelState; + } + // Conntected state handling. + if (!lastUserInitiatedLevel.has_value()) { + return defaultLabelState; + } + switch (*lastUserInitiatedLevel) { + case Pinetime::Controllers::ImmediateAlertClient::Levels::NoAlert: + return stoppedLabelState; + case Pinetime::Controllers::ImmediateAlertClient::Levels::HighAlert: + return alertingLabelState; + case Pinetime::Controllers::ImmediateAlertClient::Levels::MildAlert: + // Not supported + default: + ASSERT(false); + return alertingLabelState; + } +} + +void FindMyPhone::Refresh() { + const auto service_state = immediateAlertClient.GetState(); + if (service_state == Pinetime::Controllers::ImmediateAlertClient::State::Connected) { + lv_obj_clear_state(btnStop, LV_STATE_DISABLED); + lv_obj_clear_state(btnRing, LV_STATE_DISABLED); + } else { + lv_obj_add_state(btnStop, LV_STATE_DISABLED); + lv_obj_add_state(btnRing, LV_STATE_DISABLED); + lastUserInitiatedLevel = std::nullopt; + lastSendFailed = false; + } + const auto& label_state = GetLabelState(); + lv_obj_set_style_local_text_color(lblTitle, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, label_state.color); + lv_label_set_text_static(lblTitle, label_state.text); + lv_obj_align(lblTitle, nullptr, LV_ALIGN_CENTER, 0, -40); +} diff --git a/src/displayapp/screens/FindMyPhone.h b/src/displayapp/screens/FindMyPhone.h new file mode 100644 index 0000000000..586532aa35 --- /dev/null +++ b/src/displayapp/screens/FindMyPhone.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "Symbols.h" +#include "systemtask/SystemTask.h" +#include "components/ble/ImmediateAlertClient.h" + +#include +#include + +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 StopRestoreLabelTask(); + void RestoreLabelText(); + void Refresh() override; + + private: + Pinetime::Controllers::ImmediateAlertClient& immediateAlertClient; + + struct LabelState { + const char* text; + lv_color_t color; + }; + + static const LabelState stoppedLabelState; + static const LabelState noConnectionLabelState; + static const LabelState noServiceLabelState; + static const LabelState defaultLabelState; + static const LabelState alertingLabelState; + static const LabelState sendFailedLabelState; + const LabelState& GetLabelState() const; + + lv_obj_t* container; + lv_obj_t* lblTitle; + lv_obj_t* btnStop; + lv_obj_t* btnRing; + lv_obj_t* lblStop; + lv_obj_t* lblRing; + lv_task_t* refreshTask = nullptr; + + bool lastSendFailed = false; + std::optional lastUserInitiatedLevel; + }; + } + + template <> + struct AppTraits { + 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()); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + }; + }; + } +} diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 40699b3d65..66115ab92d 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -41,6 +41,7 @@ namespace Pinetime { static constexpr const char* sleep = "\xEE\xBD\x84"; static constexpr const char* calculator = "\xEF\x87\xAC"; static constexpr const char* backspace = "\xEF\x95\x9A"; + static constexpr const char* magnifyingGlass = "\xEF\x80\x82"; // f002 // fontawesome_weathericons.c // static constexpr const char* sun = "\xEF\x86\x85";