diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2b69b8b02..2abb56a47a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -411,6 +411,7 @@ list(APPEND SOURCE_FILES displayapp/screens/settings/SettingWeatherFormat.cpp displayapp/screens/settings/SettingWakeUp.cpp displayapp/screens/settings/SettingDisplay.cpp + displayapp/screens/settings/SettingHeartRate.cpp displayapp/screens/settings/SettingSteps.cpp displayapp/screens/settings/SettingSetDateTime.cpp displayapp/screens/settings/SettingSetDate.cpp diff --git a/src/components/settings/Settings.cpp b/src/components/settings/Settings.cpp index 1ae00a2dbc..49073e1a1d 100644 --- a/src/components/settings/Settings.cpp +++ b/src/components/settings/Settings.cpp @@ -8,13 +8,11 @@ Settings::Settings(Pinetime::Controllers::FS& fs) : fs {fs} { } void Settings::Init() { - // Load default settings from Flash LoadSettingsFromFile(); } void Settings::SaveSettings() { - // verify if is necessary to save if (settingsChanged) { SaveSettingsToFile(); diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index 602de3a585..1d58d26cde 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -50,6 +50,11 @@ namespace Pinetime { int colorIndex = 0; }; + struct HeartRateBackgroundMeasurement { + bool activated = false; + unsigned int intervalInSeconds = 0; + }; + Settings(Pinetime::Controllers::FS& fs); Settings(const Settings&) = delete; @@ -298,10 +303,34 @@ namespace Pinetime { return bleRadioEnabled; }; + bool IsHeartRateBackgroundMeasurementActivated() const { + return settings.heartRateBackgroundMeasurement.activated; + } + + void DeactivateHeartRateBackgroundMeasurement() { + if (settings.heartRateBackgroundMeasurement.activated) { + settingsChanged = true; + } + settings.heartRateBackgroundMeasurement.activated = false; + } + + unsigned int GetHeartRateBackgroundMeasurementInterval() const { + return settings.heartRateBackgroundMeasurement.intervalInSeconds; + } + + void SetHeartRateBackgroundMeasurementInterval(unsigned int newIntervalInSeconds) { + if (!settings.heartRateBackgroundMeasurement.activated || + newIntervalInSeconds != settings.heartRateBackgroundMeasurement.intervalInSeconds) { + settingsChanged = true; + } + settings.heartRateBackgroundMeasurement.intervalInSeconds = newIntervalInSeconds; + settings.heartRateBackgroundMeasurement.activated = true; + } + private: Pinetime::Controllers::FS& fs; - static constexpr uint32_t settingsVersion = 0x0008; + static constexpr uint32_t settingsVersion = 0x0009; struct SettingsData { uint32_t version = settingsVersion; @@ -325,6 +354,8 @@ namespace Pinetime { uint16_t shakeWakeThreshold = 150; Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium; + + HeartRateBackgroundMeasurement heartRateBackgroundMeasurement; }; SettingsData settings; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 6671ac9e51..b79bd1e009 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -47,6 +47,7 @@ #include "displayapp/screens/settings/SettingSteps.h" #include "displayapp/screens/settings/SettingSetDateTime.h" #include "displayapp/screens/settings/SettingChimes.h" +#include "displayapp/screens/settings/SettingHeartRate.h" #include "displayapp/screens/settings/SettingShakeThreshold.h" #include "displayapp/screens/settings/SettingBluetooth.h" @@ -605,6 +606,9 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio case Apps::SettingWakeUp: currentScreen = std::make_unique(settingsController); break; + case Apps::SettingHeartRate: + currentScreen = std::make_unique(settingsController); + break; case Apps::SettingDisplay: currentScreen = std::make_unique(settingsController); break; diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 2104a267c0..a74ca7a805 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -35,6 +35,7 @@ namespace Pinetime { SettingWatchFace, SettingTimeFormat, SettingWeatherFormat, + SettingHeartRate, SettingDisplay, SettingWakeUp, SettingSteps, diff --git a/src/displayapp/screens/settings/SettingHeartRate.cpp b/src/displayapp/screens/settings/SettingHeartRate.cpp new file mode 100644 index 0000000000..b004f00e30 --- /dev/null +++ b/src/displayapp/screens/settings/SettingHeartRate.cpp @@ -0,0 +1,87 @@ +#include "displayapp/screens/settings/SettingHeartRate.h" +#include +#include "displayapp/screens/Styles.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/Symbols.h" +#include +#include + +using namespace Pinetime::Applications::Screens; + +namespace { + void event_handler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + screen->UpdateSelected(obj, event); + } +} + +constexpr std::array SettingHeartRate::options; + +SettingHeartRate::SettingHeartRate(Pinetime::Controllers::Settings& settingsController) : settingsController {settingsController} { + + lv_obj_t* container1 = lv_cont_create(lv_scr_act(), nullptr); + + lv_obj_set_style_local_bg_opa(container1, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_pad_all(container1, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 5); + lv_obj_set_style_local_pad_inner(container1, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 5); + lv_obj_set_style_local_border_width(container1, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 0); + + lv_obj_set_pos(container1, 10, 60); + lv_obj_set_width(container1, LV_HOR_RES - 20); + lv_obj_set_height(container1, LV_VER_RES - 50); + lv_cont_set_layout(container1, LV_LAYOUT_PRETTY_TOP); + + lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(title, "Backg. Interval"); + lv_label_set_text(title, "Backg. Interval"); + lv_label_set_align(title, LV_LABEL_ALIGN_CENTER); + lv_obj_align(title, lv_scr_act(), LV_ALIGN_IN_TOP_MID, 10, 15); + + lv_obj_t* icon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); + lv_label_set_text_static(icon, Symbols::heartBeat); + lv_label_set_align(icon, LV_LABEL_ALIGN_CENTER); + lv_obj_align(icon, title, LV_ALIGN_OUT_LEFT_MID, -10, 0); + + bool isActivated = settingsController.IsHeartRateBackgroundMeasurementActivated(); + unsigned int currentInterval = settingsController.GetHeartRateBackgroundMeasurementInterval(); + + for (unsigned int i = 0; i < options.size(); i++) { + cbOption[i] = lv_checkbox_create(container1, nullptr); + lv_checkbox_set_text(cbOption[i], options[i].name); + cbOption[i]->user_data = this; + lv_obj_set_event_cb(cbOption[i], event_handler); + SetRadioButtonStyle(cbOption[i]); + + if (!isActivated && options[i].intervalInSeconds == -1) { + lv_checkbox_set_checked(cbOption[i], true); + } else if (isActivated && options[i].intervalInSeconds == (int) currentInterval) { + lv_checkbox_set_checked(cbOption[i], true); + } + } +} + +SettingHeartRate::~SettingHeartRate() { + lv_obj_clean(lv_scr_act()); + settingsController.SaveSettings(); +} + +void SettingHeartRate::UpdateSelected(lv_obj_t* object, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + for (unsigned int i = 0; i < options.size(); i++) { + if (object == cbOption[i]) { + lv_checkbox_set_checked(cbOption[i], true); + + int optionInterval = options[i].intervalInSeconds; + + if (optionInterval == -1) { + settingsController.DeactivateHeartRateBackgroundMeasurement(); + } else { + settingsController.SetHeartRateBackgroundMeasurementInterval((unsigned int) optionInterval); + } + } else { + lv_checkbox_set_checked(cbOption[i], false); + } + } + } +} diff --git a/src/displayapp/screens/settings/SettingHeartRate.h b/src/displayapp/screens/settings/SettingHeartRate.h new file mode 100644 index 0000000000..5bb7462d66 --- /dev/null +++ b/src/displayapp/screens/settings/SettingHeartRate.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include "components/settings/Settings.h" +#include "displayapp/screens/ScreenList.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/CheckboxList.h" + +namespace Pinetime { + + namespace Applications { + namespace Screens { + + struct Option { + const int intervalInSeconds; + const char* name; + }; + + class SettingHeartRate : public Screen { + public: + SettingHeartRate(Pinetime::Controllers::Settings& settings); + ~SettingHeartRate() override; + + void UpdateSelected(lv_obj_t* object, lv_event_t event); + + private: + Pinetime::Controllers::Settings& settingsController; + + static constexpr std::array options = {{ + {-1, " Off"}, + {0, "Cont"}, + {15, " 15s"}, + {30, " 30s"}, + {60, " 1m"}, + {5 * 60, " 5m"}, + {10 * 60, " 10m"}, + {30 * 60, " 30m"}, + }}; + + lv_obj_t* cbOption[options.size()]; + }; + } + } +} diff --git a/src/displayapp/screens/settings/Settings.h b/src/displayapp/screens/settings/Settings.h index 3722c2be39..50d6465d56 100644 --- a/src/displayapp/screens/settings/Settings.h +++ b/src/displayapp/screens/settings/Settings.h @@ -38,22 +38,19 @@ namespace Pinetime { {Symbols::home, "Watch face", Apps::SettingWatchFace}, {Symbols::shoe, "Steps", Apps::SettingSteps}, + {Symbols::heartBeat, "Heartrate", Apps::SettingHeartRate}, {Symbols::clock, "Date & Time", Apps::SettingSetDateTime}, {Symbols::cloudSunRain, "Weather", Apps::SettingWeatherFormat}, - {Symbols::batteryHalf, "Battery", Apps::BatteryInfo}, + {Symbols::batteryHalf, "Battery", Apps::BatteryInfo}, {Symbols::clock, "Chimes", Apps::SettingChimes}, {Symbols::tachometer, "Shake Calib.", Apps::SettingShakeThreshold}, {Symbols::check, "Firmware", Apps::FirmwareValidation}, - {Symbols::bluetooth, "Bluetooth", Apps::SettingBluetooth}, + {Symbols::bluetooth, "Bluetooth", Apps::SettingBluetooth}, {Symbols::list, "About", Apps::SysInfo}, - - // {Symbols::none, "None", Apps::None}, - // {Symbols::none, "None", Apps::None}, - // {Symbols::none, "None", Apps::None}, - // {Symbols::none, "None", Apps::None}, - + {Symbols::none, "None", Apps::None}, + {Symbols::none, "None", Apps::None}, }}; ScreenList screens; }; diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp index 8a5a871b41..f518df3490 100644 --- a/src/heartratetask/HeartRateTask.cpp +++ b/src/heartratetask/HeartRateTask.cpp @@ -5,8 +5,22 @@ using namespace Pinetime::Applications; -HeartRateTask::HeartRateTask(Drivers::Hrs3300& heartRateSensor, Controllers::HeartRateController& controller) - : heartRateSensor {heartRateSensor}, controller {controller} { +TickType_t CurrentTaskDelay(bool isMeasurmentActivated, bool isScreenOn, bool isBackgroundMeasuring, TickType_t ppgDeltaTms) { + if (!isMeasurmentActivated) { + return portMAX_DELAY; + } + + if (isScreenOn || isBackgroundMeasuring) { + return ppgDeltaTms; + } + + return pdMS_TO_TICKS(100); +} + +HeartRateTask::HeartRateTask(Drivers::Hrs3300& heartRateSensor, + Controllers::HeartRateController& controller, + Controllers::Settings& settings) + : heartRateSensor {heartRateSensor}, controller {controller}, settings {settings} { } void HeartRateTask::Start() { @@ -25,78 +39,36 @@ void HeartRateTask::Process(void* instance) { void HeartRateTask::Work() { int lastBpm = 0; + while (true) { + TickType_t delay = CurrentTaskDelay(isMeasurementActivated, isScreenOn, isBackgroundMeasuring, ppg.deltaTms); Messages msg; - uint32_t delay; - if (state == States::Running) { - if (measurementStarted) { - delay = ppg.deltaTms; - } else { - delay = 100; - } - } else { - delay = portMAX_DELAY; - } - if (xQueueReceive(messageQueue, &msg, delay)) { + if (xQueueReceive(messageQueue, &msg, delay) == pdTRUE) { switch (msg) { case Messages::GoToSleep: - StopMeasurement(); - state = States::Idle; + HandleGoToSleep(); break; case Messages::WakeUp: - state = States::Running; - if (measurementStarted) { - lastBpm = 0; - StartMeasurement(); - } + HandleWakeUp(); break; case Messages::StartMeasurement: - if (measurementStarted) { - break; - } - lastBpm = 0; - StartMeasurement(); - measurementStarted = true; + HandleStartMeasurement(&lastBpm); break; case Messages::StopMeasurement: - if (!measurementStarted) { - break; - } - StopMeasurement(); - measurementStarted = false; + HandleStopMeasurement(); break; } } - if (measurementStarted) { - auto sensorData = heartRateSensor.ReadHrsAls(); - int8_t ambient = ppg.Preprocess(sensorData.hrs, sensorData.als); - int bpm = ppg.HeartRate(); - - // If ambient light detected or a reset requested (bpm < 0) - if (ambient > 0) { - // Reset all DAQ buffers - ppg.Reset(true); - // Force state to NotEnoughData (below) - lastBpm = 0; - bpm = 0; - } else if (bpm < 0) { - // Reset all DAQ buffers except HRS buffer - ppg.Reset(false); - // Set HR to zero and update - bpm = 0; - controller.Update(Controllers::HeartRateController::States::Running, bpm); - } - - if (lastBpm == 0 && bpm == 0) { - controller.Update(Controllers::HeartRateController::States::NotEnoughData, bpm); - } + if (!isMeasurementActivated) { + continue; + } - if (bpm != 0) { - lastBpm = bpm; - controller.Update(Controllers::HeartRateController::States::Running, lastBpm); - } + if (isScreenOn || isBackgroundMeasuring) { + HandleSensorData(&lastBpm); + } else if (!isBackgroundMeasuring) { + HandleWaiting(); } } } @@ -107,14 +79,118 @@ void HeartRateTask::PushMessage(HeartRateTask::Messages msg) { portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } -void HeartRateTask::StartMeasurement() { +void HeartRateTask::StartSensor() { heartRateSensor.Enable(); ppg.Reset(true); vTaskDelay(100); } -void HeartRateTask::StopMeasurement() { +void HeartRateTask::StopSensor() { heartRateSensor.Disable(); ppg.Reset(true); vTaskDelay(100); } + +void HeartRateTask::HandleGoToSleep() { + isScreenOn = false; +} + +void HeartRateTask::HandleWakeUp() { + if (isMeasurementActivated) { + StartSensor(); + } + isScreenOn = true; +} + +void HeartRateTask::HandleStartMeasurement(int* lastBpm) { + isMeasurementActivated = true; + *lastBpm = 0; + StartSensor(); +} + +void HeartRateTask::HandleStopMeasurement() { + isMeasurementActivated = false; + StopSensor(); +} + +void HeartRateTask::HandleWaiting() { + if (!IsBackgroundMeasurementActivated() || !isMeasurementActivated) { + StopSensor(); + return; + } + + if (ShouldStartBackgroundMeasuring()) { + isBackgroundMeasuring = true; + StartSensor(); + } +} + +void HeartRateTask::HandleSensorData(int* lastBpm) { + auto sensorData = heartRateSensor.ReadHrsAls(); + int8_t ambient = ppg.Preprocess(sensorData.hrs, sensorData.als); + int bpm = ppg.HeartRate(); + + // If ambient light detected or a reset requested (bpm < 0) + if (ambient > 0) { + // Reset all DAQ buffers + ppg.Reset(true); + } else if (bpm < 0) { + // Reset all DAQ buffers except HRS buffer + ppg.Reset(false); + // Set HR to zero and update + bpm = 0; + } + + bool notEnoughData = *lastBpm == 0 && bpm == 0; + if (notEnoughData) { + controller.Update(Controllers::HeartRateController::States::NotEnoughData, bpm); + } + + if (bpm != 0) { + *lastBpm = bpm; + controller.Update(Controllers::HeartRateController::States::Running, bpm); + } + + if (isScreenOn || IsContinuousModeActivated()) { + return; + } + + if (ShouldStartBackgroundMeasuring()) { + // This doesn't change the state but resets the measurment timer, which basically starts the next measurment without resetting the + // sensor. This is basically a fall back to continuous mode, when measurments take too long. + measurementStart = xTaskGetTickCount(); + return; + } + + bool noDataWithinTimeLimit = bpm == 0 && ShoudStopTryingToGetData(); + bool dataWithinTimeLimit = bpm != 0; + if (dataWithinTimeLimit || noDataWithinTimeLimit) { + isBackgroundMeasuring = false; + StopSensor(); + } +} + +TickType_t HeartRateTask::GetBackgroundIntervalInTicks() { + int ms = settings.GetHeartRateBackgroundMeasurementInterval() * 1000; + return pdMS_TO_TICKS(ms); +} + +bool HeartRateTask::IsContinuousModeActivated() { + return settings.GetHeartRateBackgroundMeasurementInterval() == 0; +} + +bool HeartRateTask::IsBackgroundMeasurementActivated() { + return settings.IsHeartRateBackgroundMeasurementActivated(); +} + +TickType_t HeartRateTask::GetTicksSinceLastMeasurementStarted() { + return xTaskGetTickCount() - measurementStart; +} + +bool HeartRateTask::ShoudStopTryingToGetData() { + return GetTicksSinceLastMeasurementStarted() >= DURATION_UNTIL_BACKGROUND_MEASUREMENT_IS_STOPPED; +} + +bool HeartRateTask::ShouldStartBackgroundMeasuring() { + return GetTicksSinceLastMeasurementStarted() >= GetBackgroundIntervalInTicks(); +} \ No newline at end of file diff --git a/src/heartratetask/HeartRateTask.h b/src/heartratetask/HeartRateTask.h index 5bbfb9fb3e..36f9b46ca3 100644 --- a/src/heartratetask/HeartRateTask.h +++ b/src/heartratetask/HeartRateTask.h @@ -3,6 +3,9 @@ #include #include #include +#include "components/settings/Settings.h" + +#define DURATION_UNTIL_BACKGROUND_MEASUREMENT_IS_STOPPED pdMS_TO_TICKS(30 * 1000) namespace Pinetime { namespace Drivers { @@ -17,25 +20,48 @@ namespace Pinetime { class HeartRateTask { public: enum class Messages : uint8_t { GoToSleep, WakeUp, StartMeasurement, StopMeasurement }; - enum class States { Idle, Running }; - explicit HeartRateTask(Drivers::Hrs3300& heartRateSensor, Controllers::HeartRateController& controller); + explicit HeartRateTask(Drivers::Hrs3300& heartRateSensor, + Controllers::HeartRateController& controller, + Controllers::Settings& settings); void Start(); void Work(); void PushMessage(Messages msg); private: static void Process(void* instance); - void StartMeasurement(); - void StopMeasurement(); + void StartSensor(); + void StopSensor(); + + void HandleGoToSleep(); + void HandleWakeUp(); + void HandleStartMeasurement(int* lastBpm); + void HandleStopMeasurement(); + + void HandleWaiting(); + void HandleSensorData(int* lastBpm); + + TickType_t GetBackgroundIntervalInTicks(); + bool IsContinuousModeActivated(); + bool IsBackgroundMeasurementActivated(); + + TickType_t GetTicksSinceLastMeasurementStarted(); + bool ShoudStopTryingToGetData(); + bool ShouldStartBackgroundMeasuring(); TaskHandle_t taskHandle; QueueHandle_t messageQueue; - States state = States::Running; + + bool isBackgroundMeasuring = false; + bool isScreenOn = true; + bool isMeasurementActivated = false; + Drivers::Hrs3300& heartRateSensor; Controllers::HeartRateController& controller; + Controllers::Settings& settings; Controllers::Ppg ppg; - bool measurementStarted = false; + + TickType_t measurementStart = 0; }; } diff --git a/src/main.cpp b/src/main.cpp index 24f13caddd..9f412c5b87 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -93,13 +93,13 @@ TimerHandle_t debounceChargeTimer; Pinetime::Controllers::Battery batteryController; Pinetime::Controllers::Ble bleController; -Pinetime::Controllers::HeartRateController heartRateController; -Pinetime::Applications::HeartRateTask heartRateApp(heartRateSensor, heartRateController); - Pinetime::Controllers::FS fs {spiNorFlash}; Pinetime::Controllers::Settings settingsController {fs}; Pinetime::Controllers::MotorController motorController {}; +Pinetime::Controllers::HeartRateController heartRateController; +Pinetime::Applications::HeartRateTask heartRateApp(heartRateSensor, heartRateController, settingsController); + Pinetime::Controllers::DateTime dateTimeController {settingsController}; Pinetime::Drivers::Watchdog watchdog; Pinetime::Controllers::NotificationManager notificationManager;