aboutsummaryrefslogtreecommitdiffstats
path: root/src/displayapp/DisplayApp.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/displayapp/DisplayApp.cpp')
-rw-r--r--src/displayapp/DisplayApp.cpp363
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() {