diff --git a/.gitignore b/.gitignore index d6f917cfc4..13b939a3f9 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,10 @@ src/arm-none-eabi node_modules package.json package-lock.json + +# Toolchain and External dependencies +sdk-toolchain/ + +# Developer files +CMakeUserPresets.json +compile_commands.json diff --git a/doc/gettingStarted/Applications.md b/doc/gettingStarted/Applications.md index 8ca2b25287..1f48f1fd7a 100644 --- a/doc/gettingStarted/Applications.md +++ b/doc/gettingStarted/Applications.md @@ -16,6 +16,7 @@ InfiniTime has 13 apps on the `main` branch at the time of writing. - Metronome - Maps - Weather +- Sleeptracking ### Stopwatch ![Stopwatch UI](/doc/gettingStarted/AppsScreenshots/stopwatch.png) @@ -97,3 +98,10 @@ InfiniTime has 13 apps on the `main` branch at the time of writing. ![Weather UI](/doc/gettingStarted/AppsScreenshots/Weather.png) - This app shows weather info. - Please note that this app is not very useful without a device connected. + +### SleepTracking +![Sleep UI](/doc/gettingStarted/AppsScreenshots/Sleeptracking.png) +- This app records your body movement and heartrate and wakes you up at the specified time. +- Sleeptracking files can be accessed through the files API in the `/logs/sleep` directory. + - Session files are comma-separated data files of the format: `%YYYY-%MM-%ddT%hh:%mm:%ss,heartrate,motionx,motiony,motionz`. + - To save space session files are rotated, only max 10 sessions are saved at a time. diff --git a/doc/gettingStarted/AppsScreenshots/Sleeptracking.png b/doc/gettingStarted/AppsScreenshots/Sleeptracking.png new file mode 100644 index 0000000000..4d65bd0aa5 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Sleeptracking.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd2e656a4..e9f50aa227 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -396,6 +396,7 @@ list(APPEND SOURCE_FILES displayapp/screens/PassKey.cpp displayapp/screens/Error.cpp displayapp/screens/Alarm.cpp + displayapp/screens/Sleep.cpp displayapp/screens/Styles.cpp displayapp/screens/WeatherSymbols.cpp displayapp/Colors.cpp @@ -469,6 +470,7 @@ list(APPEND SOURCE_FILES components/settings/Settings.cpp components/timer/Timer.cpp components/alarm/AlarmController.cpp + components/sleeptracking/SleepTrackingController.cpp components/fs/FS.cpp drivers/Cst816s.cpp FreeRTOS/port.c @@ -538,6 +540,7 @@ list(APPEND RECOVERY_SOURCE_FILES components/settings/Settings.cpp components/timer/Timer.cpp components/alarm/AlarmController.cpp + components/sleeptracking/SleepTrackingController.cpp drivers/Cst816s.cpp FreeRTOS/port.c FreeRTOS/port_cmsis_systick.c @@ -615,6 +618,7 @@ set(INCLUDE_FILES displayapp/screens/Timer.h displayapp/screens/Dice.h displayapp/screens/Alarm.h + displayapp/screens/Sleep.h displayapp/Colors.h displayapp/widgets/Counter.h displayapp/widgets/PageIndicator.h @@ -657,6 +661,7 @@ set(INCLUDE_FILES components/settings/Settings.h components/timer/Timer.h components/alarm/AlarmController.h + components/sleeptracking/SleepTrackingController.h drivers/Cst816s.h FreeRTOS/portmacro.h FreeRTOS/portmacro_cmsis.h diff --git a/src/components/sleeptracking/SleepTrackingController.cpp b/src/components/sleeptracking/SleepTrackingController.cpp new file mode 100644 index 0000000000..b7c6900ce0 --- /dev/null +++ b/src/components/sleeptracking/SleepTrackingController.cpp @@ -0,0 +1,303 @@ +/* Copyright (C) 2025 Asger Gitz-Johansen + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#include "SleepTrackingController.h" + +#include +#include +#include +#include + +#define pdSEC_TO_TICKS(n) pdMS_TO_TICKS(n * 1000) +#define pdMIN_TO_TICKS(n) pdSEC_TO_TICKS(n * 60) +#ifndef MIN // Wrap in an ifndef becuase the simulator doesn't autoinclude it. + #define MIN(a, b) ((a < b) ? (a) : (b)) +#endif + +namespace { + void MotionTrackingTimerTrigger(TimerHandle_t xTimer) { + static_cast(pvTimerGetTimerID(xTimer))->OnMotionTrackingTimerTrigger(); + } + + void HeartRateTrackingTimerTrigger(TimerHandle_t xTimer) { + static_cast(pvTimerGetTimerID(xTimer))->OnHeartRateTrackingTimerTrigger(); + } + + void StoreDataTimerTrigger(TimerHandle_t xTimer) { + static_cast(pvTimerGetTimerID(xTimer))->OnStoreDataTimerTrigger(); + } + + void GentleWakeupTimerTrigger(TimerHandle_t xTimer) { + static_cast(pvTimerGetTimerID(xTimer))->OnGentleWakeupTimerTrigger(); + } + + void WakeAlarmTimerTrigger(TimerHandle_t xTimer) { + static_cast(pvTimerGetTimerID(xTimer))->OnWakeAlarmTrigger(); + } +} + +namespace Pinetime::Controllers { + SleepTrackingController::SleepTrackingController(FS& filesystem, + DateTime& datetimeController, + Pinetime::Drivers::Bma421& motionSensor, + HeartRateController& heartRateController, + MotorController& motorController) + : settings {}, + systemTask {nullptr}, + filesystem {filesystem}, + datetimeController {datetimeController}, + + // Sleep tracking related + motionSensor {motionSensor}, + heartRateController {heartRateController}, + motionTrackingTimer {}, + heartRateTrackingTimer {}, + storeDataTimer {}, + currentDataPoint {}, + previousValues {}, + hasPreviousValues {false}, + + // Wakeup related + wakeupAlarmTimer {}, + gentleWakeupTimer {}, + motorController {motorController}, + vibrationDurationMillis {wakeAlarmVibrationDurationStart}, + isAlerting {false} { + } + + void SleepTrackingController::Init(System::SystemTask* systemTask) { + this->systemTask = systemTask; + motionTrackingTimer = xTimerCreate("sampleMotion", pdSEC_TO_TICKS(2), pdFALSE, this, MotionTrackingTimerTrigger); + heartRateTrackingTimer = xTimerCreate("sampleHR", pdMIN_TO_TICKS(2), pdFALSE, this, HeartRateTrackingTimerTrigger); + storeDataTimer = xTimerCreate("storeData", pdMIN_TO_TICKS(3), pdFALSE, this, StoreDataTimerTrigger); + wakeupAlarmTimer = xTimerCreate("wakeupAlarm", 1, pdFALSE, this, WakeAlarmTimerTrigger); + gentleWakeupTimer = xTimerCreate("gentleWakeup", pdSEC_TO_TICKS(10), pdFALSE, this, GentleWakeupTimerTrigger); + LoadSettings(); + + if (settings.isTracking) { + StartTracking(); + NRF_LOG_INFO("[SleepTrackingController] Sleep tracking resumed"); + } + } + + void SleepTrackingController::StartTracking() { + // Reset tracking data. + previousValues = {}; + hasPreviousValues = false; + currentDataPoint = {}; + settings.isTracking = true; + SaveSettings(); + ScheduleWakeAlarm(); + + // Start tracking timers. + xTimerStart(motionTrackingTimer, 0); + xTimerStart(heartRateTrackingTimer, 0); + xTimerStart(storeDataTimer, 0); + NRF_LOG_INFO("[SleepTrackingController] Sleep tracking started"); + } + + void SleepTrackingController::StopTracking() { + // Stop tracking timers. + xTimerStop(motionTrackingTimer, 0); + xTimerStop(heartRateTrackingTimer, 0); + xTimerStop(storeDataTimer, 0); + settings.isTracking = false; + settings.currentSession = (settings.currentSession + 1) % 10; + SaveSettings(); + DismissWakeAlarm(); + ClearTrackingFile(); + NRF_LOG_INFO("[SleepTrackingController] Sleep tracking stopped"); + } + + void SleepTrackingController::OnMotionTrackingTimerTrigger() { + auto data = motionSensor.Process(); + if (hasPreviousValues) { + currentDataPoint.xDiffSum += ABS(previousValues.x) - ABS(data.x); + currentDataPoint.yDiffSum += ABS(previousValues.y) - ABS(data.y); + currentDataPoint.zDiffSum += ABS(previousValues.z) - ABS(data.z); + } + previousValues = data; + hasPreviousValues = true; + xTimerStart(motionTrackingTimer, 0); + } + + void SleepTrackingController::OnHeartRateTrackingTimerTrigger() { + currentDataPoint.heartRate = heartRateController.HeartRate(); + xTimerStart(heartRateTrackingTimer, 0); + } + + void SleepTrackingController::OnStoreDataTimerTrigger() { + systemTask->PushMessage(Pinetime::System::Messages::OnSleepTrackingDataPoint); + } + + void SleepTrackingController::SaveDatapoint() { + lfs_file_t sleepDataFile; + auto day = datetimeController.Day(); + auto month = datetimeController.Month(); + auto year = datetimeController.Year(); + auto hours = datetimeController.Hours(); + auto minutes = datetimeController.Minutes(); + auto seconds = datetimeController.Seconds(); + + char filename[32] {}; + snprintf(filename, 32, "logs/sleep/session-%d.csv", settings.currentSession); + + // Ensure that the subdirectory exists. + lfs_dir logdir {}; + if (filesystem.DirOpen("logs", &logdir) != LFS_ERR_OK) { + filesystem.DirCreate("logs"); + } + filesystem.DirClose(&logdir); + if (filesystem.DirOpen("logs/sleep", &logdir) != LFS_ERR_OK) { + filesystem.DirCreate("logs/sleep"); + } + filesystem.DirClose(&logdir); + + if (filesystem.FileOpen(&sleepDataFile, filename, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND) < 0) { + NRF_LOG_WARNING("[SleepTrackingController] Failed to open '%s' file", filename); + xTimerStart(storeDataTimer, 0); + return; + } + + char buffer[64]; + auto len = snprintf(buffer, + sizeof(buffer), + "%04d-%02d-%02dT%02d:%02d:%02d,%d,%d,%d,%d\n", + year, + static_cast(month), + day, + hours, + minutes, + seconds, + currentDataPoint.heartRate, + currentDataPoint.xDiffSum, + currentDataPoint.yDiffSum, + currentDataPoint.zDiffSum); + filesystem.FileWrite(&sleepDataFile, reinterpret_cast(buffer), len); + filesystem.FileClose(&sleepDataFile); + xTimerStart(storeDataTimer, 0); + } + + void SleepTrackingController::DismissWakeAlarm() { + isAlerting = false; + xTimerStop(wakeupAlarmTimer, 0); + xTimerStop(gentleWakeupTimer, 0); + vibrationDurationMillis = wakeAlarmVibrationDurationStart; + if (!isAlerting) { + return; + } + motorController.StopRinging(); + } + + void SleepTrackingController::LoadSettings() { + lfs_file_t settingsFile; + Settings settingsBuffer; + if (filesystem.FileOpen(&settingsFile, settingsFileName, LFS_O_RDONLY) < 0) { + NRF_LOG_WARNING("[SleepTrackingController] Failed to open settings file"); + return; + } + + filesystem.FileRead(&settingsFile, reinterpret_cast(&settingsBuffer), sizeof(settingsBuffer)); + filesystem.FileClose(&settingsFile); + if (settingsBuffer.version != sleeptrackingSettingsFormatVersion) { + NRF_LOG_WARNING("[SleepTrackingController] Loaded settings has version %u instead of %u, discarding", + settingsBuffer.version, + sleeptrackingSettingsFormatVersion); + return; + } + + settings = settingsBuffer; + NRF_LOG_INFO("[SleepTrackingController] Loaded settings from file"); + } + + void SleepTrackingController::SaveSettings() { + lfs_file_t settingsFile; + if (filesystem.FileOpen(&settingsFile, settingsFileName, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) != LFS_ERR_OK) { + NRF_LOG_WARNING("[SleepTrackingController] Failed to open settings file"); + return; + } + filesystem.FileWrite(&settingsFile, reinterpret_cast(&settings), sizeof(settings)); + filesystem.FileClose(&settingsFile); + NRF_LOG_INFO("[SleepTrackingController] Saved settings to file"); + } + + void SleepTrackingController::OnWakeAlarmTrigger() { + isAlerting = true; + // Notify the system that the wake alarm is triggered, so we can show the alarm dismissal screen. + systemTask->PushMessage(System::Messages::SetOffWakeAlarm); + } + + void SleepTrackingController::OnGentleWakeupTimerTrigger() { + // TODO: Also set intensity when motorcontroller supports it. (start low, end medium) + motorController.RunForDuration(vibrationDurationMillis); + vibrationDurationMillis = MIN(vibrationDurationMillis + 100, static_cast(1000)); + xTimerStart(gentleWakeupTimer, 0); + } + + void SleepTrackingController::ScheduleWakeAlarm() { + // Determine the next time the alarm needs to go off and set the timer + xTimerStop(wakeupAlarmTimer, 0); + auto now = datetimeController.CurrentDateTime(); + auto ttAlarmTime = std::chrono::system_clock::to_time_t(std::chrono::time_point_cast(now)); + auto* tmAlarmTime = std::localtime(&ttAlarmTime); + // If the time being set has already passed today,the alarm should be set for tomorrow + if (settings.alarm.hours < datetimeController.Hours() || + (settings.alarm.hours == datetimeController.Hours() && settings.alarm.minutes <= datetimeController.Minutes())) { + tmAlarmTime->tm_mday += 1; + // tm_wday doesn't update automatically + tmAlarmTime->tm_wday = (tmAlarmTime->tm_wday + 1) % 7; + } + tmAlarmTime->tm_hour = settings.alarm.hours; + tmAlarmTime->tm_min = settings.alarm.minutes; + tmAlarmTime->tm_sec = 0; + tmAlarmTime->tm_isdst = -1; // use system timezone setting to determine DST + // now can convert back to a time_point + auto alarmTime = std::chrono::system_clock::from_time_t(std::mktime(tmAlarmTime)); + auto secondsToAlarm = std::chrono::duration_cast(alarmTime - now).count(); + xTimerChangePeriod(wakeupAlarmTimer, secondsToAlarm * configTICK_RATE_HZ, 0); + xTimerStart(wakeupAlarmTimer, 0); + NRF_LOG_INFO("[SleepTrackingController] New alarm scheduled in %d seconds", secondsToAlarm); + } + + void SleepTrackingController::ClearTrackingFile() { + char filename[32] {}; + snprintf(filename, 32, "logs/sleep/session-%d.csv", settings.currentSession); + lfs_info info; + filesystem.Stat(filename, &info); + if (info.size > 0) { + lfs_file_t file; + filesystem.FileOpen(&file, filename, LFS_O_CREAT | LFS_O_WRONLY | LFS_O_TRUNC); // NOTE: TRUNC = truncate + filesystem.FileClose(&file); + } + } + + SleepTrackingController::Settings SleepTrackingController::GetSettings() { + return settings; + } + + void SleepTrackingController::SetSettings(const Settings& newSettings) { + settings = newSettings; + } + + bool SleepTrackingController::IsAlerting() const { + return isAlerting; + } + + bool SleepTrackingController::IsTracking() const { + return settings.isTracking; + } +} diff --git a/src/components/sleeptracking/SleepTrackingController.h b/src/components/sleeptracking/SleepTrackingController.h new file mode 100644 index 0000000000..3162155547 --- /dev/null +++ b/src/components/sleeptracking/SleepTrackingController.h @@ -0,0 +1,110 @@ +/* Copyright (C) 2025 Asger Gitz-Johansen + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Pinetime::Controllers { + class SleepTrackingController { + private: + using timepoint = std::chrono::time_point; + static constexpr uint8_t sleeptrackingSettingsFormatVersion = 1; + static constexpr uint8_t maxSavedSessions = 14; // A night is ~5.5KiB (pessimistic estimate) = max(ish) 77KiB usage. + static constexpr const char* settingsFileName = "sleeptracksettings.dat"; + static constexpr uint8_t wakeAlarmVibrationDurationStart = 50; + static constexpr uint8_t maxSessionNameLength = 36; + + struct Settings { + uint8_t version = sleeptrackingSettingsFormatVersion; + uint8_t currentSession = 0; + bool isTracking = false; + + struct Alarm { + uint8_t hours = 7; + uint8_t minutes = 0; + } alarm; + }; + + struct SleepDataPoint { + int16_t xDiffSum = 0; + int16_t yDiffSum = 0; + int16_t zDiffSum = 0; + uint8_t heartRate = 0; + }; + + public: + SleepTrackingController(FS& filesystem, + DateTime& datetimeController, + Pinetime::Drivers::Bma421& motionSensor, + HeartRateController& heartRateController, + MotorController& motorController); + + void Init(System::SystemTask* systemTask); + + void StartTracking(); + void StopTracking(); + void OnWakeAlarmTrigger(); + + void OnMotionTrackingTimerTrigger(); + void OnHeartRateTrackingTimerTrigger(); + void OnStoreDataTimerTrigger(); + void OnGentleWakeupTimerTrigger(); + + void ScheduleWakeAlarm(); + bool IsAlerting() const; + bool IsTracking() const; + void SaveDatapoint(); + + Settings GetSettings(); + void SetSettings(const Settings& newSettings); + void SaveSettings(); + + private: + void DismissWakeAlarm(); + void LoadSettings(); + void ClearTrackingFile(); + + // Dependencies + Settings settings; + System::SystemTask* systemTask; + FS& filesystem; + DateTime& datetimeController; + + // Sleep tracking + Pinetime::Drivers::Bma421& motionSensor; + HeartRateController& heartRateController; + TimerHandle_t motionTrackingTimer; + TimerHandle_t heartRateTrackingTimer; + TimerHandle_t storeDataTimer; + SleepDataPoint currentDataPoint; + Drivers::Bma421::Values previousValues; + bool hasPreviousValues; + + // Wakeup + TimerHandle_t wakeupAlarmTimer; + TimerHandle_t gentleWakeupTimer; + MotorController& motorController; + uint16_t vibrationDurationMillis; + bool isAlerting; + }; +} diff --git a/src/displayapp/Controllers.h b/src/displayapp/Controllers.h index 9992426c5d..6aeb6ffbf3 100644 --- a/src/displayapp/Controllers.h +++ b/src/displayapp/Controllers.h @@ -20,6 +20,7 @@ namespace Pinetime { class MotionController; class AlarmController; class BrightnessController; + class SleepTrackingController; class SimpleWeatherService; class FS; class Timer; @@ -43,6 +44,7 @@ namespace Pinetime { Pinetime::Controllers::MotionController& motionController; Pinetime::Controllers::AlarmController& alarmController; Pinetime::Controllers::BrightnessController& brightnessController; + Pinetime::Controllers::SleepTrackingController& sleeptrackingController; Pinetime::Controllers::SimpleWeatherService* weatherController; Pinetime::Controllers::FS& filesystem; Pinetime::Controllers::Timer& timer; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index bfd7dbed6d..c17113eec0 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -1,9 +1,11 @@ #include "displayapp/DisplayApp.h" +#include #include #include "displayapp/screens/HeartRate.h" #include "displayapp/screens/Motion.h" #include "displayapp/screens/Timer.h" #include "displayapp/screens/Alarm.h" +#include "displayapp/screens/Sleep.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/datetime/DateTimeController.h" @@ -83,6 +85,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Pinetime::Controllers::MotionController& motionController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, + Pinetime::Controllers::SleepTrackingController& sleeptrackingController, Pinetime::Controllers::TouchHandler& touchHandler, Pinetime::Controllers::FS& filesystem, Pinetime::Drivers::SpiNorFlash& spiNorFlash) @@ -99,6 +102,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, motionController {motionController}, alarmController {alarmController}, brightnessController {brightnessController}, + sleeptrackingController {sleeptrackingController}, touchHandler {touchHandler}, filesystem {filesystem}, spiNorFlash {spiNorFlash}, @@ -114,6 +118,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, motionController, alarmController, brightnessController, + sleeptrackingController, nullptr, filesystem, timer, @@ -384,6 +389,18 @@ void DisplayApp::Refresh() { LoadNewScreen(Apps::Alarm, DisplayApp::FullRefreshDirections::None); } break; + case Messages::WakeAlarmTriggered: + if (currentApp == Apps::Sleep) { + auto* sleep = static_cast(currentScreen.get()); + sleep->StartAlerting(); + } else { + LoadNewScreen(Apps::Sleep, DisplayApp::FullRefreshDirections::None); + } + break; + case Messages::SleepSaveDataPoint: + sleeptrackingController.SaveDatapoint(); + PushMessage(Messages::GoToSleep); + break; case Messages::ShowPairingKey: LoadNewScreen(Apps::PassKey, DisplayApp::FullRefreshDirections::Up); motorController.RunForDuration(35); diff --git a/src/displayapp/DisplayApp.h b/src/displayapp/DisplayApp.h index dabed99ea7..0e4b1317fc 100644 --- a/src/displayapp/DisplayApp.h +++ b/src/displayapp/DisplayApp.h @@ -65,6 +65,7 @@ namespace Pinetime { Pinetime::Controllers::MotionController& motionController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, + Pinetime::Controllers::SleepTrackingController& sleeptrackingController, Pinetime::Controllers::TouchHandler& touchHandler, Pinetime::Controllers::FS& filesystem, Pinetime::Drivers::SpiNorFlash& spiNorFlash); @@ -95,6 +96,7 @@ namespace Pinetime { Pinetime::Controllers::MotionController& motionController; Pinetime::Controllers::AlarmController& alarmController; Pinetime::Controllers::BrightnessController& brightnessController; + Pinetime::Controllers::SleepTrackingController& sleeptrackingController; Pinetime::Controllers::TouchHandler& touchHandler; Pinetime::Controllers::FS& filesystem; Pinetime::Drivers::SpiNorFlash& spiNorFlash; diff --git a/src/displayapp/DisplayAppRecovery.cpp b/src/displayapp/DisplayAppRecovery.cpp index bcb8db0e9d..03d0beca29 100644 --- a/src/displayapp/DisplayAppRecovery.cpp +++ b/src/displayapp/DisplayAppRecovery.cpp @@ -23,6 +23,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Pinetime::Controllers::MotionController& /*motionController*/, Pinetime::Controllers::AlarmController& /*alarmController*/, Pinetime::Controllers::BrightnessController& /*brightnessController*/, + Pinetime::Controllers::SleepTrackingController& /*sleeptrackingController*/, Pinetime::Controllers::TouchHandler& /*touchHandler*/, Pinetime::Controllers::FS& /*filesystem*/, Pinetime::Drivers::SpiNorFlash& /*spiNorFlash*/) diff --git a/src/displayapp/DisplayAppRecovery.h b/src/displayapp/DisplayAppRecovery.h index 162ff2575e..f5c49505b9 100644 --- a/src/displayapp/DisplayAppRecovery.h +++ b/src/displayapp/DisplayAppRecovery.h @@ -33,6 +33,7 @@ namespace Pinetime { class MotorController; class AlarmController; class BrightnessController; + class SleepTrackingController; class FS; class SimpleWeatherService; class MusicService; @@ -59,6 +60,7 @@ namespace Pinetime { Pinetime::Controllers::MotionController& motionController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, + Pinetime::Controllers::SleepTrackingController& sleeptrackingController, Pinetime::Controllers::TouchHandler& touchHandler, Pinetime::Controllers::FS& filesystem, Pinetime::Drivers::SpiNorFlash& spiNorFlash); diff --git a/src/displayapp/Messages.h b/src/displayapp/Messages.h index 1fcd72d278..d3206ca556 100644 --- a/src/displayapp/Messages.h +++ b/src/displayapp/Messages.h @@ -22,6 +22,8 @@ namespace Pinetime { NotifyDeviceActivity, ShowPairingKey, AlarmTriggered, + WakeAlarmTriggered, + SleepSaveDataPoint, Chime, BleRadioEnableToggle, }; diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index f6feeb7b6d..57b3f1dc0c 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -14,6 +14,7 @@ namespace Pinetime { NotificationsPreview, Notifications, Timer, + Sleep, Alarm, FlashLight, BatteryInfo, diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..477acdbf65 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::Sleep") #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/fonts/fonts.json b/src/displayapp/fonts/fonts.json index fea3160572..f742fbc8a5 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, 0xf236" } ], "bpp": 1, diff --git a/src/displayapp/screens/Sleep.cpp b/src/displayapp/screens/Sleep.cpp new file mode 100644 index 0000000000..ab4f4e8004 --- /dev/null +++ b/src/displayapp/screens/Sleep.cpp @@ -0,0 +1,237 @@ +/* Copyright (C) 2025 Asger Gitz-Johansen + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#include "Sleep.h" + +#include +#include + +namespace { + void OnValueChangedHandler(void* userData) { + static_cast(userData)->OnAlarmValueChanged(); + } + + void OnButtonPress(lv_obj_t* obj, lv_event_t event) { + static_cast(obj->user_data)->OnButtonEvent(obj, event); + } + + void StopAlarmTaskCallback(lv_task_t* task) { + static_cast(task->user_data)->StopAlerting(); + } +} + +namespace Pinetime::Applications::Screens { + Sleep::Sleep(Controllers::SleepTrackingController& sleeptrackingController, + Controllers::Settings::ClockType clockType, + Controllers::MotorController& motorController, + System::SystemTask& systemTask, + DisplayApp& displayApp) + : sleeptrackingController {sleeptrackingController}, + motorController {motorController}, + systemTask {systemTask}, + displayApp {displayApp}, + wakeLock {systemTask}, + state {}, + hourCounter {0, 23, jetbrains_mono_76}, + minuteCounter {0, 59, jetbrains_mono_76}, + lblampm {nullptr}, + startButton {nullptr}, + startText {nullptr}, + stopButton {nullptr}, + stopText {nullptr}, + dismissButton {nullptr}, + dismissText {nullptr}, + colonLabel {nullptr}, + wakeUpLabel {nullptr}, + sleepingLabel {nullptr}, + taskStopAlarm {nullptr} { + auto settings = sleeptrackingController.GetSettings(); + hourCounter.Create(); + lv_obj_align(hourCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); + if (clockType == Controllers::Settings::ClockType::H12) { + hourCounter.EnableTwelveHourMode(); + + lblampm = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblampm, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_label_set_text_static(lblampm, "AM"); + lv_label_set_align(lblampm, LV_LABEL_ALIGN_CENTER); + lv_obj_align(lblampm, lv_scr_act(), LV_ALIGN_CENTER, 0, 30); + } + hourCounter.SetValue(settings.alarm.hours); + hourCounter.SetValueChangedEventCallback(this, OnValueChangedHandler); + + minuteCounter.Create(); + lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0); + minuteCounter.SetValue(settings.alarm.minutes); + minuteCounter.SetValueChangedEventCallback(this, OnValueChangedHandler); + + lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_label_set_text_static(colonLabel, ":"); + lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29); + + stopButton = lv_btn_create(lv_scr_act(), nullptr); + stopButton->user_data = this; + lv_obj_set_event_cb(stopButton, OnButtonPress); + lv_obj_set_size(stopButton, 240, 50); + lv_obj_align(stopButton, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + lv_obj_set_style_local_bg_color(stopButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE); + stopText = lv_label_create(stopButton, nullptr); + lv_label_set_text_static(stopText, Symbols::pause); + lv_obj_set_hidden(stopButton, true); + + dismissButton = lv_btn_create(lv_scr_act(), nullptr); + dismissButton->user_data = this; + lv_obj_set_event_cb(dismissButton, OnButtonPress); + lv_obj_set_size(dismissButton, 240, 50); + lv_obj_align(dismissButton, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + lv_obj_set_style_local_bg_color(dismissButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); + dismissText = lv_label_create(dismissButton, nullptr); + lv_label_set_text_static(dismissText, Symbols::stop); + lv_obj_set_hidden(dismissButton, true); + + startButton = lv_btn_create(lv_scr_act(), nullptr); + startButton->user_data = this; + lv_obj_set_event_cb(startButton, OnButtonPress); + lv_obj_set_size(startButton, 240, 50); + lv_obj_align(startButton, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + lv_obj_set_style_local_bg_color(startButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); + startText = lv_label_create(startButton, nullptr); + lv_label_set_text_static(startText, Symbols::bed); + lv_obj_set_hidden(startButton, false); + + UpdateWakeAlarmTime(); + + if (sleeptrackingController.IsAlerting()) { + StartAlerting(); + } else if (sleeptrackingController.IsTracking()) { + SetState(State::Tracking); + } else { + SetState(State::NotTracking); + } + } + + Sleep::~Sleep() { + if (sleeptrackingController.IsAlerting()) { + StopAlerting(); + } + lv_obj_clean(lv_scr_act()); + sleeptrackingController.SaveSettings(); + } + + void Sleep::StartAlerting() { + SetState(State::Alerting); + sleeptrackingController.OnGentleWakeupTimerTrigger(); + } + + void Sleep::OnButtonEvent(lv_obj_t* obj, lv_event_t event) { + if (event != LV_EVENT_CLICKED) { + return; + } + if (obj == stopButton) { + sleeptrackingController.StopTracking(); + SetState(State::NotTracking); + return; + } + if (obj == dismissButton) { + sleeptrackingController.StopTracking(); + SetState(State::NotTracking); + return; + } + if (obj == startButton) { + sleeptrackingController.StartTracking(); + SetState(State::Tracking); + return; + } + } + + void Sleep::StopAlerting() { + sleeptrackingController.StopTracking(); + SetState(State::NotTracking); + } + + void Sleep::OnAlarmValueChanged() { + UpdateWakeAlarmTime(); + } + + void Sleep::UpdateWakeAlarmTime() { + if (lblampm != nullptr) { + if (hourCounter.GetValue() >= 12) { + lv_label_set_text_static(lblampm, "PM"); + } else { + lv_label_set_text_static(lblampm, "AM"); + } + } + auto settings = sleeptrackingController.GetSettings(); + settings.alarm.hours = hourCounter.GetValue(); + settings.alarm.minutes = minuteCounter.GetValue(); + sleeptrackingController.SetSettings(settings); + } + + bool Sleep::OnTouchEvent(Pinetime::Applications::TouchEvents event) { + // Don't allow closing the screen by swiping while the wake alarm is alerting + return sleeptrackingController.IsAlerting() && event == TouchEvents::SwipeDown; + } + + void Sleep::SetState(const State& uistate) { + state = uistate; + switch (uistate) { + case State::NotTracking: + wakeLock.Release(); + lv_obj_set_hidden(stopButton, true); + lv_obj_set_hidden(dismissButton, true); + hourCounter.ShowControls(); + minuteCounter.ShowControls(); + lv_obj_set_hidden(startButton, false); + if (taskStopAlarm != nullptr) { + lv_task_del(taskStopAlarm); + taskStopAlarm = nullptr; + } + break; + case State::Tracking: + wakeLock.Release(); + lv_obj_set_hidden(startButton, true); + lv_obj_set_hidden(dismissButton, true); + hourCounter.HideControls(); + minuteCounter.HideControls(); + lv_obj_set_hidden(stopButton, false); + break; + case State::Alerting: + wakeLock.Lock(); + lv_obj_set_hidden(startButton, true); + lv_obj_set_hidden(stopButton, true); + hourCounter.HideControls(); + minuteCounter.HideControls(); + lv_obj_set_hidden(dismissButton, false); + if (taskStopAlarm != nullptr) { + lv_task_del(taskStopAlarm); + taskStopAlarm = nullptr; + } + taskStopAlarm = lv_task_create(StopAlarmTaskCallback, pdMS_TO_TICKS(10 * 60 * 1000), LV_TASK_PRIO_MID, this); + break; + } + } + + bool Sleep::OnButtonPushed() { + if (sleeptrackingController.IsAlerting()) { + sleeptrackingController.StopTracking(); + SetState(State::NotTracking); + return true; + } + return false; + } +} diff --git a/src/displayapp/screens/Sleep.h b/src/displayapp/screens/Sleep.h new file mode 100644 index 0000000000..a5eef332ce --- /dev/null +++ b/src/displayapp/screens/Sleep.h @@ -0,0 +1,93 @@ +/* Copyright (C) 2025 Asger Gitz-Johansen + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#pragma once + +#include "Apps.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Pinetime::Applications::Screens { + class Sleep : public Screen { + public: + enum class State : uint8_t { NotTracking, Tracking, Alerting }; + + Sleep(Controllers::SleepTrackingController& sleeptrackingController, + Controllers::Settings::ClockType clockType, + Controllers::MotorController& motorController, + System::SystemTask& systemTask, + DisplayApp& displayApp); + ~Sleep() override; + + void StartAlerting(); + + void SetState(const State& uistate); + + void OnButtonEvent(lv_obj_t* obj, lv_event_t event); + void OnAlarmValueChanged(); + + bool OnTouchEvent(TouchEvents event) override; + bool OnButtonPushed() override; + void StopAlerting(); + + private: + void UpdateWakeAlarmTime(); + + // Dependencies + Controllers::SleepTrackingController& sleeptrackingController; + Controllers::MotorController& motorController; + System::SystemTask& systemTask; + DisplayApp& displayApp; + System::WakeLock wakeLock; + + // UI + State state; + Widgets::Counter hourCounter; + Widgets::Counter minuteCounter; + lv_obj_t* lblampm; + lv_obj_t *startButton, *startText; + lv_obj_t *stopButton, *stopText; + lv_obj_t *dismissButton, *dismissText; + lv_obj_t *colonLabel, *wakeUpLabel, *sleepingLabel; + lv_task_t* taskStopAlarm; + }; +} + +namespace Pinetime::Applications { + template <> + struct AppTraits { + static constexpr Apps app = Apps::Sleep; + static constexpr const char* icon = Screens::Symbols::bed; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Sleep(controllers.sleeptrackingController, + controllers.settingsController.GetClockType(), + controllers.motorController, + *controllers.systemTask, + *controllers.displayApp); + } + + 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..0395d63af2 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* bed = "\xEF\x88\xB6"; // fontawesome_weathericons.c // static constexpr const char* sun = "\xEF\x86\x85"; diff --git a/src/main.cpp b/src/main.cpp index 24f13caddd..7fa2a9c06c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" #include "components/brightness/BrightnessController.h" +#include "components/sleeptracking/SleepTrackingController.h" #include "components/motor/MotorController.h" #include "components/datetime/DateTimeController.h" #include "components/heartrate/HeartRateController.h" @@ -108,6 +109,11 @@ Pinetime::Controllers::AlarmController alarmController {dateTimeController, fs}; Pinetime::Controllers::TouchHandler touchHandler; Pinetime::Controllers::ButtonHandler buttonHandler; Pinetime::Controllers::BrightnessController brightnessController {}; +Pinetime::Controllers::SleepTrackingController sleeptrackingController {fs, + dateTimeController, + motionSensor, + heartRateController, + motorController}; Pinetime::Applications::DisplayApp displayApp(lcd, touchPanel, @@ -122,6 +128,7 @@ Pinetime::Applications::DisplayApp displayApp(lcd, motionController, alarmController, brightnessController, + sleeptrackingController, touchHandler, fs, spiNorFlash); @@ -134,6 +141,7 @@ Pinetime::System::SystemTask systemTask(spi, bleController, dateTimeController, alarmController, + sleeptrackingController, watchdog, notificationManager, heartRateSensor, diff --git a/src/systemtask/Messages.h b/src/systemtask/Messages.h index fee94bb747..d3cddd2bfa 100644 --- a/src/systemtask/Messages.h +++ b/src/systemtask/Messages.h @@ -24,7 +24,9 @@ namespace Pinetime { OnNewHalfHour, OnChargingEvent, OnPairing, + OnSleepTrackingDataPoint, SetOffAlarm, + SetOffWakeAlarm, MeasureBatteryTimerExpired, BatteryPercentageUpdated, StartFileTransfer, diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index 8e0435e372..97ff079793 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -40,6 +40,7 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, Controllers::Ble& bleController, Controllers::DateTime& dateTimeController, Controllers::AlarmController& alarmController, + Controllers::SleepTrackingController& sleeptrackingController, Drivers::Watchdog& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, Pinetime::Drivers::Hrs3300& heartRateSensor, @@ -60,6 +61,7 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, bleController {bleController}, dateTimeController {dateTimeController}, alarmController {alarmController}, + sleeptrackingController {sleeptrackingController}, watchdog {watchdog}, notificationManager {notificationManager}, heartRateSensor {heartRateSensor}, @@ -128,6 +130,7 @@ void SystemTask::Work() { batteryController.Register(this); motionSensor.SoftReset(); alarmController.Init(this); + sleeptrackingController.Init(this); // Reset the TWI device because the motion sensor chip most probably crashed it... twiMaster.Sleep(); @@ -218,6 +221,14 @@ void SystemTask::Work() { GoToRunning(); displayApp.PushMessage(Pinetime::Applications::Display::Messages::AlarmTriggered); break; + case Messages::SetOffWakeAlarm: + GoToRunning(); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::WakeAlarmTriggered); + break; + case Messages::OnSleepTrackingDataPoint: + GoToRunning(); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::SleepSaveDataPoint); + break; case Messages::BleConnected: displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); isBleDiscoveryTimerRunning = true; diff --git a/src/systemtask/SystemTask.h b/src/systemtask/SystemTask.h index 0060e36096..5f69204947 100644 --- a/src/systemtask/SystemTask.h +++ b/src/systemtask/SystemTask.h @@ -16,6 +16,7 @@ #include "components/ble/NimbleController.h" #include "components/ble/NotificationManager.h" #include "components/alarm/AlarmController.h" +#include "components/sleeptracking/SleepTrackingController.h" #include "components/fs/FS.h" #include "touchhandler/TouchHandler.h" #include "buttonhandler/ButtonHandler.h" @@ -61,6 +62,7 @@ namespace Pinetime { Controllers::Ble& bleController, Controllers::DateTime& dateTimeController, Controllers::AlarmController& alarmController, + Controllers::SleepTrackingController& sleeptrackingController, Drivers::Watchdog& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, Pinetime::Drivers::Hrs3300& heartRateSensor, @@ -101,6 +103,7 @@ namespace Pinetime { Pinetime::Controllers::Ble& bleController; Pinetime::Controllers::DateTime& dateTimeController; Pinetime::Controllers::AlarmController& alarmController; + Pinetime::Controllers::SleepTrackingController& sleeptrackingController; QueueHandle_t systemTasksMsgQueue; Pinetime::Drivers::Watchdog& watchdog; Pinetime::Controllers::NotificationManager& notificationManager;