diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4a354df64..3a6c93ff4f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -398,6 +398,7 @@ list(APPEND SOURCE_FILES displayapp/screens/Alarm.cpp displayapp/screens/Styles.cpp displayapp/screens/WeatherSymbols.cpp + displayapp/screens/Home.cpp displayapp/Colors.cpp displayapp/widgets/Counter.cpp displayapp/widgets/PageIndicator.cpp @@ -466,6 +467,7 @@ list(APPEND SOURCE_FILES components/ble/ServiceDiscovery.cpp components/ble/HeartRateService.cpp components/ble/MotionService.cpp + components/ble/HomeService.cpp components/firmwarevalidator/FirmwareValidator.cpp components/motor/MotorController.cpp components/settings/Settings.cpp @@ -658,6 +660,7 @@ set(INCLUDE_FILES components/ble/HeartRateService.h components/ble/MotionService.h components/ble/SimpleWeatherService.h + components/ble/HomeService.h components/settings/Settings.h components/timer/Timer.h components/stopwatch/StopWatchController.h diff --git a/src/components/ble/HomeService.cpp b/src/components/ble/HomeService.cpp new file mode 100644 index 0000000000..4859facd32 --- /dev/null +++ b/src/components/ble/HomeService.cpp @@ -0,0 +1,144 @@ +#include "HomeService.h" + +#include +#include "components/ble/NimbleController.h" + +namespace { + // 0006yyxx-78fc-48fe-8e23-433b3a1942d0 + constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { + return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128}, + .value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, x, y, 0x06, 0x00}}; + } + + // 00060000-78fc-48fe-8e23-433b3a1942d0 + constexpr ble_uuid128_t BaseUuid() { + return CharUuid(0x00, 0x00); + } + + constexpr ble_uuid128_t homeUuid {BaseUuid()}; + + constexpr ble_uuid128_t homeOpenUuid {CharUuid(0x01, 0x00)}; + constexpr ble_uuid128_t homeLayoutUuid {CharUuid(0x02, 0x00)}; + constexpr ble_uuid128_t homePressUuid {CharUuid(0x03, 0x00)}; + + int HomeCallback(uint16_t /*conn_handle*/, uint16_t /*attr_handle*/, struct ble_gatt_access_ctxt* ctxt, void* arg) { + return static_cast(arg)->OnCommand(ctxt); + } +} // namespace + +Pinetime::Controllers::HomeService::HomeService(NimbleController& nimble) : nimble(nimble) { + characteristicDefinition[0] = {.uuid = &homeLayoutUuid.u, .access_cb = HomeCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE}; + characteristicDefinition[1] = {.uuid = &homeOpenUuid.u, + .access_cb = HomeCallback, + .arg = this, + .flags = BLE_GATT_CHR_F_NOTIFY, + .val_handle = &eventOpenedHandle}; + characteristicDefinition[2] = {.uuid = &homePressUuid.u, + .access_cb = HomeCallback, + .arg = this, + .flags = BLE_GATT_CHR_F_NOTIFY, + .val_handle = &eventPressedHandle}; + characteristicDefinition[3] = {0}; + + serviceDefinition[0] = {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &homeUuid.u, .characteristics = characteristicDefinition}; + serviceDefinition[1] = {0}; +} + +void Pinetime::Controllers::HomeService::Init() { + uint8_t res = 0; + res = ble_gatts_count_cfg(serviceDefinition); + ASSERT(res == 0); + + res = ble_gatts_add_svcs(serviceDefinition); + ASSERT(res == 0); +} + +int Pinetime::Controllers::HomeService::OnCommand(ble_gatt_access_ctxt* ctxt) { + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + size_t bufferSize = OS_MBUF_PKTLEN(ctxt->om); + + uint8_t data[bufferSize]; + os_mbuf_copydata(ctxt->om, 0, bufferSize, data); + + if (ble_uuid_cmp(ctxt->chr->uuid, &homeLayoutUuid.u) == 0) { + auto screen = std::make_unique(); + uint8_t* ptr = &data[0]; + + numScreens = *ptr++; + screen->index = *ptr++; + + screen->cols = *ptr >> 4; + screen->rows = *ptr & 0x0F; + ptr++; + uint8_t num_comps = *(ptr++); + + for (size_t j = 0; j < num_comps; j++) { + Component comp; + comp.type = (ComponentType) (*(ptr++)); + + comp.x = *ptr >> 4; + comp.y = *ptr & 0x0F; + ptr++; + comp.w = *ptr >> 4; + comp.h = *ptr & 0x0F; + ptr++; + + uint8_t label_len = *(ptr++); + auto label = std::make_unique(label_len + 1); + memcpy(label.get(), ptr, label_len); + label.get()[label_len] = 0; + ptr += label_len; + + comp.label = std::move(label); + + screen->components.emplace_back(std::move(comp)); + } + + currentScreen = std::move(screen); + dataUpdateTime = xTaskGetTickCount(); + } + } + + return 0; +} + +bool Pinetime::Controllers::HomeService::NotifyOpened(int8_t screenIndex) { + uint16_t connectionHandle = nimble.connHandle(); + + if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { + return false; + } + + auto* om = ble_hs_mbuf_from_flat(&screenIndex, sizeof(screenIndex)); + ble_gattc_notify_custom(connectionHandle, eventOpenedHandle, om); + + return true; +} + +bool Pinetime::Controllers::HomeService::OnOpened() { + return NotifyOpened(0); +} + +void Pinetime::Controllers::HomeService::OnViewScreen(uint8_t n) { + NotifyOpened(n); +} + +void Pinetime::Controllers::HomeService::OnClosed() { + dataUpdateTime = 0; + numScreens = 0; + currentScreen.reset(); + + NotifyOpened(-1); +} + +void Pinetime::Controllers::HomeService::OnPressed(uint8_t screen, uint8_t componentId) { + uint16_t connectionHandle = nimble.connHandle(); + + if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { + return; + } + + uint8_t v[] = {screen, componentId}; + auto* om = ble_hs_mbuf_from_flat(v, sizeof(v)); + ble_gattc_notify_custom(connectionHandle, eventPressedHandle, om); +} diff --git a/src/components/ble/HomeService.h b/src/components/ble/HomeService.h new file mode 100644 index 0000000000..2079a4395c --- /dev/null +++ b/src/components/ble/HomeService.h @@ -0,0 +1,70 @@ +#pragma once +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include +#undef max +#undef min +#include +#include +#include + +namespace Pinetime { + namespace Controllers { + class NimbleController; + + class HomeService { + public: + enum ComponentType : uint8_t { + Button, + Label, + }; + + struct Component { + ComponentType type; + uint8_t x, y, w, h; + std::unique_ptr label; + }; + + struct Screen { + uint8_t index, rows, cols; + std::vector components {}; + }; + + HomeService(NimbleController& nimble); + void Init(); + + int OnCommand(struct ble_gatt_access_ctxt* ctxt); + + bool OnOpened(); + void OnViewScreen(uint8_t n); + void OnClosed(); + void OnPressed(uint8_t screen, uint8_t componentId); + + TickType_t DataUpdateTime() { + return dataUpdateTime; + } + + const Screen& CurrentScreen() { + return *currentScreen.get(); + } + + uint8_t NumScreens() { + return numScreens; + } + + private: + NimbleController& nimble; + + uint16_t eventOpenedHandle {}, eventPressedHandle; + + std::unique_ptr currentScreen; + TickType_t dataUpdateTime = 0; + uint8_t numScreens; + + struct ble_gatt_chr_def characteristicDefinition[4]; + struct ble_gatt_svc_def serviceDefinition[2]; + + bool NotifyOpened(int8_t screenIndex); + }; + } +} diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 5059007ab9..c1f2402186 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -49,7 +49,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, heartRateService {*this, heartRateController}, motionService {*this, motionController}, fsService {systemTask, fs}, - serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { + serviceDiscovery({¤tTimeClient, &alertNotificationClient}), + homeService {*this} { } void nimble_on_reset(int reason) { @@ -98,6 +99,7 @@ void NimbleController::Init() { heartRateService.Init(); motionService.Init(); fsService.Init(); + homeService.Init(); int rc; rc = ble_hs_util_ensure_addr(0); diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 597ef0cc34..b59f5e8ef0 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -22,6 +22,7 @@ #include "components/ble/ServiceDiscovery.h" #include "components/ble/MotionService.h" #include "components/ble/SimpleWeatherService.h" +#include "components/ble/HomeService.h" #include "components/fs/FS.h" namespace Pinetime { @@ -71,6 +72,10 @@ namespace Pinetime { return weatherService; }; + Pinetime::Controllers::HomeService& home() { + return homeService; + }; + uint16_t connHandle(); void NotifyBatteryLevel(uint8_t level); @@ -107,6 +112,7 @@ namespace Pinetime { MotionService motionService; FSService fsService; ServiceDiscovery serviceDiscovery; + HomeService homeService; uint8_t addrType; uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; diff --git a/src/displayapp/Controllers.h b/src/displayapp/Controllers.h index 223c7c699e..130ca8c6d9 100644 --- a/src/displayapp/Controllers.h +++ b/src/displayapp/Controllers.h @@ -26,6 +26,7 @@ namespace Pinetime { class Timer; class MusicService; class NavigationService; + class HomeService; } namespace System { @@ -53,6 +54,7 @@ namespace Pinetime { Pinetime::Components::LittleVgl& lvgl; Pinetime::Controllers::MusicService* musicService; Pinetime::Controllers::NavigationService* navigationService; + Pinetime::Controllers::HomeService* homeService; }; } } diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 84fa603622..edf7806954 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -732,6 +732,10 @@ void DisplayApp::Register(Pinetime::Controllers::NavigationService* NavigationSe this->controllers.navigationService = NavigationService; } +void DisplayApp::Register(Pinetime::Controllers::HomeService* homeService) { + this->controllers.homeService = homeService; +} + void DisplayApp::ApplyBrightness() { auto brightness = settingsController.GetBrightness(); if (brightness != Controllers::BrightnessController::Levels::Low && brightness != Controllers::BrightnessController::Levels::Medium && diff --git a/src/displayapp/DisplayApp.h b/src/displayapp/DisplayApp.h index 016f91d3b6..bf78b542e2 100644 --- a/src/displayapp/DisplayApp.h +++ b/src/displayapp/DisplayApp.h @@ -81,6 +81,7 @@ namespace Pinetime { void Register(Pinetime::Controllers::SimpleWeatherService* weatherService); void Register(Pinetime::Controllers::MusicService* musicService); void Register(Pinetime::Controllers::NavigationService* NavigationService); + void Register(Pinetime::Controllers::HomeService* homeService); private: Pinetime::Drivers::St7789& lcd; diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 25926edc40..6e65815298 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -7,6 +7,7 @@ #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" #include "displayapp/screens/Tile.h" +#include "displayapp/screens/Home.h" #include "displayapp/screens/ApplicationList.h" #include "displayapp/screens/WatchFaceDigital.h" #include "displayapp/screens/WatchFaceAnalog.h" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index d440b598d1..a4f0fba588 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -31,6 +31,7 @@ namespace Pinetime { Dice, Weather, PassKey, + Home, QuickSettings, Settings, SettingWatchFace, diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..9b9fa67ce9 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -15,6 +15,7 @@ else () 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::Weather") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Home") #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") endif () diff --git a/src/displayapp/screens/Home.cpp b/src/displayapp/screens/Home.cpp new file mode 100644 index 0000000000..2ae2f1101c --- /dev/null +++ b/src/displayapp/screens/Home.cpp @@ -0,0 +1,135 @@ +#include "Home.h" +#include "components/ble/HomeService.h" +#include "displayapp/DisplayApp.h" +#include + +using namespace Pinetime::Applications::Screens; + +static void RequestUpdateTaskCallback(lv_task_t* task) { + static_cast(task->user_data)->RequestUpdate(); +} + +static void event_handler(lv_obj_t* obj, lv_event_t event) { + static_cast(obj->parent->user_data)->OnObjectEvent(obj, event); +} + +Home::Home(Pinetime::Controllers::HomeService& home, DisplayApp* app) : homeService(home), app(app) { + is_connected = home.OnOpened(); + + container = lv_obj_create(lv_scr_act(), lv_scr_act()); + container->user_data = this; + + label_status = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(label_status, is_connected ? "loading" : "not connected"); + lv_obj_align(label_status, nullptr, LV_ALIGN_CENTER, 0, 0); + + if (is_connected) { + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + taskRequestUpdate = lv_task_create(RequestUpdateTaskCallback, 1000, LV_TASK_PRIO_MID, this); + } +} + +Home::~Home() { + lv_task_del(taskRefresh); + lv_task_del(taskRequestUpdate); + lv_obj_clean(lv_scr_act()); + + if (is_connected) + homeService.OnClosed(); +} + +void Home::Refresh() { + if (updated_at >= homeService.DataUpdateTime()) + return; // No need to refresh, we don't have new data + + if (current_screen == homeService.CurrentScreen().index) { + updated_at = homeService.DataUpdateTime(); + + lv_obj_set_hidden(label_status, true); + + ShowScreen(homeService.CurrentScreen()); + } +} + +void Home::OnObjectEvent(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_SHORT_CLICKED) { + size_t componentId = (size_t) obj->user_data; + + homeService.OnPressed(current_screen, (uint8_t) componentId); + } +} + +bool Pinetime::Applications::Screens::Home::OnTouchEvent(TouchEvents event) { + switch (event) { + case TouchEvents::SwipeRight: + if (current_screen == 0) + return false; + + current_screen--; + app->SetFullRefresh(DisplayApp::FullRefreshDirections::RightAnim); + break; + + case TouchEvents::SwipeLeft: + if (current_screen == homeService.NumScreens() - 1) + return false; + + current_screen++; + app->SetFullRefresh(DisplayApp::FullRefreshDirections::LeftAnim); + break; + + default: + return false; + } + + lv_obj_set_hidden(label_status, false); + homeService.OnViewScreen(current_screen); + return true; +} + +void Pinetime::Applications::Screens::Home::ShowScreen(const Pinetime::Controllers::HomeService::Screen& screen) { + lv_obj_clean(container); + + const int margin = 5; + + auto width = lv_obj_get_width_grid(lv_scr_act(), screen.cols, 1); + auto height = lv_obj_get_height_grid(lv_scr_act(), screen.rows, 1); + + for (size_t i = 0; i < screen.components.size(); i++) { + auto& comp = screen.components[i]; + + switch (comp.type) { + case Pinetime::Controllers::HomeService::ComponentType::Button: { + lv_obj_t* btn = lv_btn_create(container, nullptr); + btn->user_data = (lv_obj_user_data_t) i; + lv_obj_set_event_cb(btn, event_handler); + lv_obj_set_size(btn, (width * comp.w) - margin * 2, (height * comp.h) - margin * 2); + lv_obj_set_style_local_bg_color(btn, LV_BTN_PART_MAIN, LV_STATE_PRESSED, lv_color_make(255, 0, 0)); + lv_obj_align(btn, nullptr, LV_ALIGN_IN_TOP_LEFT, comp.x * width + margin, comp.y * height + margin); + + lv_obj_t* lbl = lv_label_create(btn, nullptr); + lv_label_set_text(lbl, comp.label.get()); + + break; + } + + case Pinetime::Controllers::HomeService::ComponentType::Label: { + lv_obj_t* cont = lv_obj_create(container, lv_scr_act()); + lv_obj_set_size(cont, (width * comp.w) - margin * 2, (height * comp.h) - margin * 2); + lv_obj_align(cont, nullptr, LV_ALIGN_IN_TOP_LEFT, comp.x * width + margin, comp.y * height + margin); + + lv_obj_t* lbl = lv_label_create(cont, nullptr); + lv_label_set_text(lbl, comp.label.get()); + lv_obj_align(lbl, nullptr, LV_ALIGN_CENTER, 0, 0); + + break; + } + + default: + break; + } + } +} + +void Pinetime::Applications::Screens::Home::RequestUpdate() { + homeService.OnViewScreen(current_screen); +} diff --git a/src/displayapp/screens/Home.h b/src/displayapp/screens/Home.h new file mode 100644 index 0000000000..129cf7b9ba --- /dev/null +++ b/src/displayapp/screens/Home.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "displayapp/widgets/PageIndicator.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "components/ble/HomeService.h" +#include "Symbols.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + class Home : public Screen { + public: + Home(Pinetime::Controllers::HomeService& home, DisplayApp* app); + + ~Home() override; + + void Refresh() override; + void OnObjectEvent(lv_obj_t* obj, lv_event_t event); + bool OnTouchEvent(TouchEvents event) override; + + void RequestUpdate(); + + private: + Pinetime::Controllers::HomeService& homeService; + DisplayApp* app; + + lv_task_t *taskRefresh, *taskRequestUpdate; + + bool is_connected = false; + TickType_t updated_at = 0; + + lv_obj_t *label_status, *container; + + uint8_t current_screen = 0; + + void ShowScreen(const Pinetime::Controllers::HomeService::Screen& screen); + + /** Watchapp */ + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Home; + static constexpr const char* icon = "H"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Home(*controllers.homeService, controllers.displayApp); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + }; + }; + } +} diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index 9feeadedd9..097c1b08fa 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -143,6 +143,7 @@ void SystemTask::Work() { displayApp.Register(&nimbleController.weather()); displayApp.Register(&nimbleController.music()); displayApp.Register(&nimbleController.navigation()); + displayApp.Register(&nimbleController.home()); displayApp.Start(bootError); heartRateSensor.Init();