diff options
Diffstat (limited to 'src/displayapp/DisplayApp.cpp')
| -rw-r--r-- | src/displayapp/DisplayApp.cpp | 363 |
1 files changed, 266 insertions, 97 deletions
diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index ccba7ee6..add00650 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -11,7 +11,6 @@ #include "components/motion/MotionController.h" #include "components/motor/MotorController.h" #include "displayapp/screens/ApplicationList.h" -#include "displayapp/screens/Clock.h" #include "displayapp/screens/FirmwareUpdate.h" #include "displayapp/screens/FirmwareValidation.h" #include "displayapp/screens/InfiniPaint.h" @@ -27,8 +26,11 @@ #include "displayapp/screens/FlashLight.h" #include "displayapp/screens/BatteryInfo.h" #include "displayapp/screens/Steps.h" +#include "displayapp/screens/Dice.h" +#include "displayapp/screens/Weather.h" #include "displayapp/screens/PassKey.h" #include "displayapp/screens/Error.h" +#include "displayapp/screens/Calculator.h" #include "drivers/Cst816s.h" #include "drivers/St7789.h" @@ -40,6 +42,7 @@ #include "displayapp/screens/settings/Settings.h" #include "displayapp/screens/settings/SettingWatchFace.h" #include "displayapp/screens/settings/SettingTimeFormat.h" +#include "displayapp/screens/settings/SettingWeatherFormat.h" #include "displayapp/screens/settings/SettingWakeUp.h" #include "displayapp/screens/settings/SettingDisplay.h" #include "displayapp/screens/settings/SettingSteps.h" @@ -49,6 +52,7 @@ #include "displayapp/screens/settings/SettingBluetooth.h" #include "libs/lv_conf.h" +#include "UserApps.h" using namespace Pinetime::Applications; using namespace Pinetime::Applications::Display; @@ -57,6 +61,11 @@ namespace { inline bool in_isr() { return (SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) != 0; } + + void TimerCallback(TimerHandle_t xTimer) { + auto* dispApp = static_cast<DisplayApp*>(pvTimerGetTimerID(xTimer)); + dispApp->PushMessage(Display::Messages::TimerDone); + } } DisplayApp::DisplayApp(Drivers::St7789& lcd, @@ -70,11 +79,11 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, - Pinetime::Controllers::TimerController& timerController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem) + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash) : lcd {lcd}, touchPanel {touchPanel}, batteryController {batteryController}, @@ -86,12 +95,31 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, settingsController {settingsController}, motorController {motorController}, motionController {motionController}, - timerController {timerController}, alarmController {alarmController}, brightnessController {brightnessController}, touchHandler {touchHandler}, filesystem {filesystem}, - lvgl {lcd, filesystem} { + spiNorFlash {spiNorFlash}, + lvgl {lcd, filesystem}, + timer(this, TimerCallback), + controllers {batteryController, + bleController, + dateTimeController, + notificationManager, + heartRateController, + settingsController, + motorController, + motionController, + alarmController, + brightnessController, + nullptr, + filesystem, + timer, + nullptr, + this, + lvgl, + nullptr, + nullptr} { } void DisplayApp::Start(System::BootErrors error) { @@ -100,6 +128,7 @@ void DisplayApp::Start(System::BootErrors error) { bootError = error; lvgl.Init(); + motorController.Init(); if (error == System::BootErrors::TouchController) { LoadNewScreen(Apps::Error, DisplayApp::FullRefreshDirections::None); @@ -117,9 +146,6 @@ void DisplayApp::Process(void* instance) { NRF_LOG_INFO("displayapp task started!"); app->InitHw(); - // Send a dummy notification to unlock the lvgl display driver for the first iteration - xTaskNotifyGive(xTaskGetCurrentTaskHandle()); - while (true) { app->Refresh(); } @@ -128,10 +154,47 @@ void DisplayApp::Process(void* instance) { void DisplayApp::InitHw() { brightnessController.Init(); ApplyBrightness(); - motorController.Init(); lcd.Init(); } +TickType_t DisplayApp::CalculateSleepTime() { + // Calculates how many system ticks DisplayApp should sleep before rendering the next AOD frame + // Next frame time is frame count * refresh period (ms) * tick rate + + auto RoundedDiv = [](uint32_t a, uint32_t b) { + return ((a + (b / 2)) / b); + }; + // RoundedDiv overflows when numerator + (denominator floordiv 2) > uint32 max + // in this case around 9 hours (=overflow frame count / always on refresh period) + constexpr TickType_t overflowFrameCount = (UINT32_MAX - (1000 / 16)) / ((configTICK_RATE_HZ / 8) * alwaysOnRefreshPeriod); + + TickType_t ticksElapsed = xTaskGetTickCount() - alwaysOnStartTime; + // Divide both the numerator and denominator by 8 (=GCD(1000,1024)) + // to increase the number of ticks (frames) before the overflow tick is reached + TickType_t targetRenderTick = RoundedDiv((configTICK_RATE_HZ / 8) * alwaysOnFrameCount * alwaysOnRefreshPeriod, 1000 / 8); + + // Assumptions + + // Tick rate is multiple of 8 + // Needed for division trick above + static_assert(configTICK_RATE_HZ % 8 == 0); + + // Frame count must always wraparound more often than the system tick count does + // Always on overflow time (ms) < system tick overflow time (ms) + // Using 64bit ints here to avoid overflow + static_assert((uint64_t) overflowFrameCount * (uint64_t) alwaysOnRefreshPeriod < (uint64_t) UINT32_MAX * 1000ULL / configTICK_RATE_HZ); + + if (alwaysOnFrameCount == overflowFrameCount) { + alwaysOnFrameCount = 0; + alwaysOnStartTime = xTaskGetTickCount(); + } + if (targetRenderTick > ticksElapsed) { + return targetRenderTick - ticksElapsed; + } else { + return 0; + } +} + void DisplayApp::Refresh() { auto LoadPreviousScreen = [this]() { FullRefreshDirections returnDirection; @@ -155,16 +218,70 @@ void DisplayApp::Refresh() { LoadScreen(returnAppStack.Pop(), returnDirection); }; + auto IsPastDimTime = [this]() -> bool { + return lv_disp_get_inactive_time(nullptr) >= pdMS_TO_TICKS(settingsController.GetScreenTimeOut() - 2000); + }; + + auto IsPastSleepTime = [this]() -> bool { + return lv_disp_get_inactive_time(nullptr) >= pdMS_TO_TICKS(settingsController.GetScreenTimeOut()); + }; + TickType_t queueTimeout; switch (state) { case States::Idle: queueTimeout = portMAX_DELAY; break; + case States::AOD: + if (!currentScreen->IsRunning()) { + LoadPreviousScreen(); + } + // Check we've slept long enough + // Might not be true if the loop received an event + // If not true, then wait that amount of time + queueTimeout = CalculateSleepTime(); + if (queueTimeout == 0) { + // Only advance the tick count when LVGL is done + // Otherwise keep running the task handler while it still has things to draw + // Note: under high graphics load, LVGL will always have more work to do + if (lv_task_handler() > 0) { + // Drop frames that we've missed if drawing/event handling took way longer than expected + while (queueTimeout == 0) { + alwaysOnFrameCount += 1; + queueTimeout = CalculateSleepTime(); + } + } + } + break; case States::Running: if (!currentScreen->IsRunning()) { LoadPreviousScreen(); } queueTimeout = lv_task_handler(); + + if (!systemTask->IsSleepDisabled() && IsPastDimTime()) { + if (!isDimmed) { + isDimmed = true; + brightnessController.Set(Controllers::BrightnessController::Levels::Low); + } + if (IsPastSleepTime() && uxQueueMessagesWaiting(msgQueue) == 0) { + PushMessageToSystemTask(System::Messages::GoToSleep); + // Can't set state to Idle here, something may send + // DisableSleeping before this GoToSleep arrives + // Instead we check we have no messages queued before sending GoToSleep + // This works as the SystemTask is higher priority than DisplayApp + // As soon as we send GoToSleep, SystemTask pre-empts DisplayApp + // Whenever DisplayApp is running again, it is guaranteed that + // SystemTask has handled the message + // If it responded, we will have a GoToSleep waiting in the queue + // By checking that there are no messages in the queue, we avoid + // resending GoToSleep when we already have a response + // SystemTask is resilient to duplicate messages, this is an + // optimisation to reduce pressure on the message queues + } + } else if (isDimmed) { + isDimmed = false; + ApplyBrightness(); + } break; default: queueTimeout = portMAX_DELAY; @@ -174,38 +291,83 @@ void DisplayApp::Refresh() { Messages msg; if (xQueueReceive(msgQueue, &msg, queueTimeout) == pdTRUE) { switch (msg) { - case Messages::DimScreen: - brightnessController.Set(Controllers::BrightnessController::Levels::Low); - break; - case Messages::RestoreBrightness: - ApplyBrightness(); - break; case Messages::GoToSleep: - while (brightnessController.Level() != Controllers::BrightnessController::Levels::Off) { + case Messages::GoToAOD: + // Checking if SystemTask is sleeping is purely an optimisation. + // If it's no longer sleeping since it sent GoToSleep, it has + // cancelled the sleep and transitioned directly from + // GoingToSleep->Running, so we are about to receive GoToRunning + // and can ignore this message. If it wasn't ignored, DisplayApp + // would go to sleep and then immediately re-wake + if (state != States::Running || !systemTask->IsSleeping()) { + break; + } + while (brightnessController.Level() != Controllers::BrightnessController::Levels::Low) { brightnessController.Lower(); vTaskDelay(100); } - lcd.Sleep(); - PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping); - state = States::Idle; + // Turn brightness down (or set to AlwaysOn mode) + if (msg == Messages::GoToAOD) { + brightnessController.Set(Controllers::BrightnessController::Levels::AlwaysOn); + } else { + brightnessController.Set(Controllers::BrightnessController::Levels::Off); + } + // Since the active screen is not really an app, go back to Clock. + if (currentApp == Apps::Launcher || currentApp == Apps::Notifications || currentApp == Apps::QuickSettings || + currentApp == Apps::Settings) { + LoadScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); + // Wait for the clock app to load before moving on. + while (!lv_task_handler()) { + }; + } + // Clear any ongoing touch pressed events + // Without this LVGL gets stuck in the pressed state and will keep refreshing the + // display activity timer causing the screen to never sleep after timeout + lvgl.ClearTouchState(); + if (msg == Messages::GoToAOD) { + lcd.LowPowerOn(); + // Record idle entry time + alwaysOnFrameCount = 0; + alwaysOnStartTime = xTaskGetTickCount(); + PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskAOD); + state = States::AOD; + } else { + lcd.Sleep(); + PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping); + state = States::Idle; + } + break; + case Messages::NotifyDeviceActivity: + lv_disp_trig_activity(nullptr); break; case Messages::GoToRunning: - lcd.Wakeup(); + // If SystemTask is sleeping, the GoToRunning message is old + // and must be ignored. Otherwise DisplayApp will use SPI + // that is powered down and cause bad behaviour + if (state == States::Running || systemTask->IsSleeping()) { + break; + } + if (state == States::AOD) { + lcd.LowPowerOff(); + } else { + lcd.Wakeup(); + } + lv_disp_trig_activity(nullptr); ApplyBrightness(); state = States::Running; break; - case Messages::UpdateTimeOut: - PushMessageToSystemTask(System::Messages::UpdateTimeOut); - break; case Messages::UpdateBleConnection: - // clockScreen.SetBleConnectionState(bleController.IsConnected() ? Screens::Clock::BleConnectionStates::Connected : - // Screens::Clock::BleConnectionStates::NotConnected); + // Only used for recovery firmware break; case Messages::NewNotification: LoadNewScreen(Apps::NotificationsPreview, DisplayApp::FullRefreshDirections::Down); break; case Messages::TimerDone: + if (state != States::Running) { + PushMessageToSystemTask(System::Messages::GoToRunning); + } if (currentApp == Apps::Timer) { + lv_disp_trig_activity(nullptr); auto* timer = static_cast<Screens::Timer*>(currentScreen.get()); timer->Reset(); } else { @@ -310,21 +472,14 @@ void DisplayApp::Refresh() { case Messages::BleRadioEnableToggle: PushMessageToSystemTask(System::Messages::BleRadioEnableToggle); break; - case Messages::UpdateDateTime: - // Added to remove warning - // What should happen here? - break; case Messages::Chime: LoadNewScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); motorController.RunForDuration(35); break; - case Messages::OnChargingEvent: - motorController.RunForDuration(15); - break; } } - if (touchHandler.IsTouching()) { + if (state == States::Running && touchHandler.IsTouching()) { currentScreen->OnTouchEvent(touchHandler.GetX(), touchHandler.GetY()); } @@ -352,32 +507,40 @@ void DisplayApp::LoadNewScreen(Apps app, DisplayApp::FullRefreshDirections direc void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections direction) { lvgl.CancelTap(); - ApplyBrightness(); + lv_disp_trig_activity(nullptr); motorController.StopRinging(); currentScreen.reset(nullptr); SetFullRefresh(direction); switch (app) { - case Apps::Launcher: - currentScreen = - std::make_unique<Screens::ApplicationList>(this, settingsController, batteryController, bleController, dateTimeController); - break; - case Apps::Motion: - // currentScreen = std::make_unique<Screens::Motion>(motionController); - // break; - case Apps::None: - case Apps::Clock: - currentScreen = std::make_unique<Screens::Clock>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - heartRateController, - motionController, - filesystem); - break; - + case Apps::Launcher: { + std::array<Screens::Tile::Applications, UserAppTypes::Count> apps; + int i = 0; + for (const auto& userApp : userApps) { + apps[i++] = Screens::Tile::Applications {userApp.icon, userApp.app, true}; + } + currentScreen = std::make_unique<Screens::ApplicationList>(this, + settingsController, + batteryController, + bleController, + alarmController, + dateTimeController, + filesystem, + std::move(apps)); + } break; + case Apps::Clock: { + const auto* watchFace = + std::find_if(userWatchFaces.begin(), userWatchFaces.end(), [this](const WatchFaceDescription& watchfaceDescription) { + return watchfaceDescription.watchFace == settingsController.GetWatchFace(); + }); + if (watchFace != userWatchFaces.end()) + currentScreen.reset(watchFace->create(controllers)); + else { + currentScreen.reset(userWatchFaces[0].create(controllers)); + } + settingsController.SetAppMenu(0); + } break; case Apps::Error: currentScreen = std::make_unique<Screens::Error>(bootError); break; @@ -409,14 +572,6 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio *systemTask, Screens::Notifications::Modes::Preview); break; - case Apps::Timer: - currentScreen = std::make_unique<Screens::Timer>(timerController); - break; - case Apps::Alarm: - currentScreen = std::make_unique<Screens::Alarm>(alarmController, settingsController.GetClockType(), *systemTask, motorController); - break; - - // Settings case Apps::QuickSettings: currentScreen = std::make_unique<Screens::QuickSettings>(this, batteryController, @@ -424,22 +579,32 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio brightnessController, motorController, settingsController, - bleController); + bleController, + alarmController); break; case Apps::Settings: currentScreen = std::make_unique<Screens::Settings>(this, settingsController); break; - case Apps::SettingWatchFace: - currentScreen = std::make_unique<Screens::SettingWatchFace>(this, settingsController, filesystem); - break; + case Apps::SettingWatchFace: { + std::array<Screens::SettingWatchFace::Item, UserWatchFaceTypes::Count> items; + int i = 0; + for (const auto& userWatchFace : userWatchFaces) { + items[i++] = + Screens::SettingWatchFace::Item {userWatchFace.name, userWatchFace.watchFace, userWatchFace.isAvailable(controllers.filesystem)}; + } + currentScreen = std::make_unique<Screens::SettingWatchFace>(this, std::move(items), settingsController, filesystem); + } break; case Apps::SettingTimeFormat: currentScreen = std::make_unique<Screens::SettingTimeFormat>(settingsController); break; + case Apps::SettingWeatherFormat: + currentScreen = std::make_unique<Screens::SettingWeatherFormat>(settingsController); + break; case Apps::SettingWakeUp: currentScreen = std::make_unique<Screens::SettingWakeUp>(settingsController); break; case Apps::SettingDisplay: - currentScreen = std::make_unique<Screens::SettingDisplay>(this, settingsController); + currentScreen = std::make_unique<Screens::SettingDisplay>(settingsController); break; case Apps::SettingSteps: currentScreen = std::make_unique<Screens::SettingSteps>(settingsController); @@ -467,38 +632,23 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio bleController, watchdog, motionController, - touchPanel); + touchPanel, + spiNorFlash); break; case Apps::FlashLight: currentScreen = std::make_unique<Screens::FlashLight>(*systemTask, brightnessController); break; - case Apps::StopWatch: - currentScreen = std::make_unique<Screens::StopWatch>(*systemTask); - break; - case Apps::Twos: - currentScreen = std::make_unique<Screens::Twos>(); - break; - case Apps::Paint: - currentScreen = std::make_unique<Screens::InfiniPaint>(lvgl, motorController); - break; - case Apps::Paddle: - currentScreen = std::make_unique<Screens::Paddle>(lvgl); - break; - case Apps::Music: - currentScreen = std::make_unique<Screens::Music>(systemTask->nimble().music()); - break; - case Apps::Navigation: - currentScreen = std::make_unique<Screens::Navigation>(systemTask->nimble().navigation()); - break; - case Apps::HeartRate: - currentScreen = std::make_unique<Screens::HeartRate>(heartRateController, *systemTask); - break; - case Apps::Metronome: - currentScreen = std::make_unique<Screens::Metronome>(motorController, *systemTask); - break; - case Apps::Steps: - currentScreen = std::make_unique<Screens::Steps>(motionController, settingsController); + default: { + const auto* d = std::find_if(userApps.begin(), userApps.end(), [app](const AppDescription& appDescription) { + return appDescription.app == app; + }); + if (d != userApps.end()) { + currentScreen.reset(d->create(controllers)); + } else { + currentScreen.reset(userWatchFaces[0].create(controllers)); + } break; + } } currentApp = app; } @@ -507,11 +657,17 @@ void DisplayApp::PushMessage(Messages msg) { if (in_isr()) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) { - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { - xQueueSend(msgQueue, &msg, portMAX_DELAY); + TickType_t timeout = portMAX_DELAY; + // Make xQueueSend() non-blocking if the message is a Notification message. We do this to avoid + // deadlock between SystemTask and DisplayApp when their respective message queues are getting full + // when a lot of notifications are received on a very short time span. + if (msg == Messages::NewNotification) { + timeout = static_cast<TickType_t>(0); + } + + xQueueSend(msgQueue, &msg, timeout); } } @@ -548,6 +704,19 @@ void DisplayApp::PushMessageToSystemTask(Pinetime::System::Messages message) { void DisplayApp::Register(Pinetime::System::SystemTask* systemTask) { this->systemTask = systemTask; + this->controllers.systemTask = systemTask; +} + +void DisplayApp::Register(Pinetime::Controllers::SimpleWeatherService* weatherService) { + this->controllers.weatherController = weatherService; +} + +void DisplayApp::Register(Pinetime::Controllers::MusicService* musicService) { + this->controllers.musicService = musicService; +} + +void DisplayApp::Register(Pinetime::Controllers::NavigationService* NavigationService) { + this->controllers.navigationService = NavigationService; } void DisplayApp::ApplyBrightness() { |
