From c53abd4c8cdb4e581086fd6de6f19d8840ca03ca Mon Sep 17 00:00:00 2001 From: Patric Gruber Date: Fri, 31 Mar 2023 10:00:56 +0200 Subject: [PATCH 1/2] add heart rate measurments in the background --- src/CMakeLists.txt | 1 + src/components/settings/Settings.cpp | 2 - src/components/settings/Settings.h | 33 ++- src/displayapp/DisplayApp.cpp | 4 + src/displayapp/apps/Apps.h.in | 1 + .../screens/settings/SettingHeartRate.cpp | 87 ++++++ .../screens/settings/SettingHeartRate.h | 47 ++++ src/displayapp/screens/settings/Settings.h | 13 +- src/heartratetask/HeartRateTask.cpp | 251 ++++++++++++++---- src/heartratetask/HeartRateTask.h | 83 +++++- src/main.cpp | 6 +- 11 files changed, 451 insertions(+), 77 deletions(-) create mode 100644 src/displayapp/screens/settings/SettingHeartRate.cpp create mode 100644 src/displayapp/screens/settings/SettingHeartRate.h 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..b069f9d0c4 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(HeartRateTask::States state, TickType_t ppgDeltaTms) { + switch (state) { + case HeartRateTask::States::ScreenOnAndMeasuring: + case HeartRateTask::States::ScreenOffAndMeasuring: + return ppgDeltaTms; + case HeartRateTask::States::ScreenOffAndWaiting: + return pdMS_TO_TICKS(1000); + default: + return portMAX_DELAY; + } +} + +HeartRateTask::HeartRateTask(Drivers::Hrs3300& heartRateSensor, + Controllers::HeartRateController& controller, + Controllers::Settings& settings) + : heartRateSensor {heartRateSensor}, controller {controller}, settings {settings} { } void HeartRateTask::Start() { @@ -25,78 +39,40 @@ void HeartRateTask::Process(void* instance) { void HeartRateTask::Work() { int lastBpm = 0; + while (true) { + TickType_t delay = CurrentTaskDelay(state, 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 (bpm != 0) { - lastBpm = bpm; - controller.Update(Controllers::HeartRateController::States::Running, lastBpm); - } + switch (state) { + case States::ScreenOffAndWaiting: + HandleBackgroundWaiting(); + break; + case States::ScreenOffAndMeasuring: + case States::ScreenOnAndMeasuring: + HandleSensorData(&lastBpm); + break; + case States::ScreenOffAndStopped: + case States::ScreenOnAndStopped: + // nothing to do -> ignore + break; } } } @@ -111,6 +87,11 @@ void HeartRateTask::StartMeasurement() { heartRateSensor.Enable(); ppg.Reset(true); vTaskDelay(100); + + if (state == States::ScreenOffAndMeasuring) { + // only set the start timestamp when the screen is off + measurementStart = xTaskGetTickCount(); + } } void HeartRateTask::StopMeasurement() { @@ -118,3 +99,155 @@ void HeartRateTask::StopMeasurement() { ppg.Reset(true); vTaskDelay(100); } + +void HeartRateTask::HandleGoToSleep() { + switch (state) { + case States::ScreenOnAndStopped: + state = States::ScreenOffAndStopped; + break; + case States::ScreenOnAndMeasuring: + state = States::ScreenOffAndWaiting; + StopMeasurement(); + break; + case States::ScreenOffAndStopped: + case States::ScreenOffAndWaiting: + case States::ScreenOffAndMeasuring: + // shouldn't happen -> ignore + break; + } +} + +void HeartRateTask::HandleWakeUp() { + switch (state) { + case States::ScreenOffAndStopped: + state = States::ScreenOnAndStopped; + break; + case States::ScreenOffAndMeasuring: + state = States::ScreenOnAndMeasuring; + break; + case States::ScreenOffAndWaiting: + state = States::ScreenOnAndMeasuring; + StartMeasurement(); + break; + case States::ScreenOnAndStopped: + case States::ScreenOnAndMeasuring: + // shouldn't happen -> ignore + break; + } +} + +void HeartRateTask::HandleStartMeasurement(int* lastBpm) { + switch (state) { + case States::ScreenOnAndStopped: + state = States::ScreenOnAndMeasuring; + *lastBpm = 0; + StartMeasurement(); + break; + case States::ScreenOffAndStopped: + case States::ScreenOnAndMeasuring: + case States::ScreenOffAndMeasuring: + case States::ScreenOffAndWaiting: + // shouldn't happen -> ignore + break; + } +} + +void HeartRateTask::HandleStopMeasurement() { + switch (state) { + case States::ScreenOnAndMeasuring: + state = States::ScreenOnAndStopped; + StopMeasurement(); + break; + case States::ScreenOffAndMeasuring: + case States::ScreenOffAndWaiting: + case States::ScreenOnAndStopped: + case States::ScreenOffAndStopped: + // shouldn't happen -> ignore + break; + } +} + +void HeartRateTask::HandleBackgroundWaiting() { + if (!IsBackgroundMeasurementActivated()) { + return; + } + + if (ShouldStartBackgroundMeasuring()) { + state = States::ScreenOffAndMeasuring; + StartMeasurement(); + } +} + +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 (state == States::ScreenOnAndMeasuring || IsContinuousModeActivated()) { + return; + } + + // state == States::ScreenOffAndMeasuring + // (because state != ScreenOnAndMeasuring and the only state that enables measuring is ScreenOffAndMeasuring) + // !IsContinuousModeActivated() + + 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) { + state = States::ScreenOffAndWaiting; + StopMeasurement(); + } +} + +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..39a8b33358 100644 --- a/src/heartratetask/HeartRateTask.h +++ b/src/heartratetask/HeartRateTask.h @@ -3,6 +3,61 @@ #include #include #include +#include "components/settings/Settings.h" + +#define DURATION_UNTIL_BACKGROUND_MEASUREMENT_IS_STOPPED pdMS_TO_TICKS(30 * 1000) + +/* + *** Background Measurement deactivated *** + + + +┌─────────────────────────┐ ┌─────────────────────────┐ +│ ├───StartMeasurement───►│ │ +│ ScreenOnAndStopped │ │ ScreenOnAndMeasuring │ +│ │◄──StopMeasurement │ │ +└──▲────────────────┬─────┘ └──▲──────────────────┬───┘ + │ │ │ │ + WakeUp GoToSleep WakeUp GoToSleep + │ │ │ │ +┌──┴────────────────▼─────┐ ┌──┴──────────────────▼───┐ +│ │ │ │ +│ ScreenOffAndStopped │ │ ScreenOffAndWaiting │ +│ │ │ │ +└─────────────────────────┘ └─────────────────────────┘ + + + + + + *** Background Measurement activated *** + + + +┌─────────────────────────┐ ┌─────────────────────────┐ +│ ├───StartMeasurement───►│ │ +│ ScreenOnAndStopped │ │ ScreenOnAndMeasuring │ +│ │◄──StopMeasurement │ │ +└──▲────────────────┬─────┘ └──▲──────────────────┬───┘ + │ │ ┌───────┘ │ + WakeUp GoToSleep │ WakeUp GoToSleep + │ │ │ │ │ +┌──┴────────────────▼─────┐ │ ┌──┴──────────────────▼───┐ +│ │ │ │ │ +│ ScreenOffAndStopped │ │ │ ScreenOffAndWating │ +│ │ │ │ │ +└─────────────────────────┘ │ └───┬──────────────────▲──┘ + │ │ │ + │ Waited Got sensor + │ interval data + │ time │ + │ │ │ + WakeUp ┌───▼──────────────────┴──┐ + │ │ │ + └────┤ ScreenOffAndMeasuring │ + │ │ + └─────────────────────────┘ + */ namespace Pinetime { namespace Drivers { @@ -17,9 +72,12 @@ 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); + enum class States { ScreenOnAndStopped, ScreenOnAndMeasuring, ScreenOffAndStopped, ScreenOffAndWaiting, ScreenOffAndMeasuring }; + + explicit HeartRateTask(Drivers::Hrs3300& heartRateSensor, + Controllers::HeartRateController& controller, + Controllers::Settings& settings); void Start(); void Work(); void PushMessage(Messages msg); @@ -29,13 +87,30 @@ namespace Pinetime { void StartMeasurement(); void StopMeasurement(); + void HandleGoToSleep(); + void HandleWakeUp(); + void HandleStartMeasurement(int* lastBpm); + void HandleStopMeasurement(); + + void HandleBackgroundWaiting(); + 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; + States state = States::ScreenOnAndStopped; 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; From 5737b9581e2fa48ae944d8cbf9f6f4f58887b47c Mon Sep 17 00:00:00 2001 From: Patric Gruber Date: Fri, 7 Mar 2025 00:37:48 +0100 Subject: [PATCH 2/2] split state machine --- src/heartratetask/HeartRateTask.cpp | 131 ++++++++-------------------- src/heartratetask/HeartRateTask.h | 67 ++------------ 2 files changed, 46 insertions(+), 152 deletions(-) diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp index b069f9d0c4..f518df3490 100644 --- a/src/heartratetask/HeartRateTask.cpp +++ b/src/heartratetask/HeartRateTask.cpp @@ -5,16 +5,16 @@ using namespace Pinetime::Applications; -TickType_t CurrentTaskDelay(HeartRateTask::States state, TickType_t ppgDeltaTms) { - switch (state) { - case HeartRateTask::States::ScreenOnAndMeasuring: - case HeartRateTask::States::ScreenOffAndMeasuring: - return ppgDeltaTms; - case HeartRateTask::States::ScreenOffAndWaiting: - return pdMS_TO_TICKS(1000); - default: - return portMAX_DELAY; +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, @@ -41,7 +41,7 @@ void HeartRateTask::Work() { int lastBpm = 0; while (true) { - TickType_t delay = CurrentTaskDelay(state, ppg.deltaTms); + TickType_t delay = CurrentTaskDelay(isMeasurementActivated, isScreenOn, isBackgroundMeasuring, ppg.deltaTms); Messages msg; if (xQueueReceive(messageQueue, &msg, delay) == pdTRUE) { @@ -61,18 +61,14 @@ void HeartRateTask::Work() { } } - switch (state) { - case States::ScreenOffAndWaiting: - HandleBackgroundWaiting(); - break; - case States::ScreenOffAndMeasuring: - case States::ScreenOnAndMeasuring: - HandleSensorData(&lastBpm); - break; - case States::ScreenOffAndStopped: - case States::ScreenOnAndStopped: - // nothing to do -> ignore - break; + if (!isMeasurementActivated) { + continue; + } + + if (isScreenOn || isBackgroundMeasuring) { + HandleSensorData(&lastBpm); + } else if (!isBackgroundMeasuring) { + HandleWaiting(); } } } @@ -83,98 +79,49 @@ void HeartRateTask::PushMessage(HeartRateTask::Messages msg) { portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } -void HeartRateTask::StartMeasurement() { +void HeartRateTask::StartSensor() { heartRateSensor.Enable(); ppg.Reset(true); vTaskDelay(100); - - if (state == States::ScreenOffAndMeasuring) { - // only set the start timestamp when the screen is off - measurementStart = xTaskGetTickCount(); - } } -void HeartRateTask::StopMeasurement() { +void HeartRateTask::StopSensor() { heartRateSensor.Disable(); ppg.Reset(true); vTaskDelay(100); } void HeartRateTask::HandleGoToSleep() { - switch (state) { - case States::ScreenOnAndStopped: - state = States::ScreenOffAndStopped; - break; - case States::ScreenOnAndMeasuring: - state = States::ScreenOffAndWaiting; - StopMeasurement(); - break; - case States::ScreenOffAndStopped: - case States::ScreenOffAndWaiting: - case States::ScreenOffAndMeasuring: - // shouldn't happen -> ignore - break; - } + isScreenOn = false; } void HeartRateTask::HandleWakeUp() { - switch (state) { - case States::ScreenOffAndStopped: - state = States::ScreenOnAndStopped; - break; - case States::ScreenOffAndMeasuring: - state = States::ScreenOnAndMeasuring; - break; - case States::ScreenOffAndWaiting: - state = States::ScreenOnAndMeasuring; - StartMeasurement(); - break; - case States::ScreenOnAndStopped: - case States::ScreenOnAndMeasuring: - // shouldn't happen -> ignore - break; + if (isMeasurementActivated) { + StartSensor(); } + isScreenOn = true; } void HeartRateTask::HandleStartMeasurement(int* lastBpm) { - switch (state) { - case States::ScreenOnAndStopped: - state = States::ScreenOnAndMeasuring; - *lastBpm = 0; - StartMeasurement(); - break; - case States::ScreenOffAndStopped: - case States::ScreenOnAndMeasuring: - case States::ScreenOffAndMeasuring: - case States::ScreenOffAndWaiting: - // shouldn't happen -> ignore - break; - } + isMeasurementActivated = true; + *lastBpm = 0; + StartSensor(); } void HeartRateTask::HandleStopMeasurement() { - switch (state) { - case States::ScreenOnAndMeasuring: - state = States::ScreenOnAndStopped; - StopMeasurement(); - break; - case States::ScreenOffAndMeasuring: - case States::ScreenOffAndWaiting: - case States::ScreenOnAndStopped: - case States::ScreenOffAndStopped: - // shouldn't happen -> ignore - break; - } + isMeasurementActivated = false; + StopSensor(); } -void HeartRateTask::HandleBackgroundWaiting() { - if (!IsBackgroundMeasurementActivated()) { +void HeartRateTask::HandleWaiting() { + if (!IsBackgroundMeasurementActivated() || !isMeasurementActivated) { + StopSensor(); return; } if (ShouldStartBackgroundMeasuring()) { - state = States::ScreenOffAndMeasuring; - StartMeasurement(); + isBackgroundMeasuring = true; + StartSensor(); } } @@ -204,14 +151,10 @@ void HeartRateTask::HandleSensorData(int* lastBpm) { controller.Update(Controllers::HeartRateController::States::Running, bpm); } - if (state == States::ScreenOnAndMeasuring || IsContinuousModeActivated()) { + if (isScreenOn || IsContinuousModeActivated()) { return; } - // state == States::ScreenOffAndMeasuring - // (because state != ScreenOnAndMeasuring and the only state that enables measuring is ScreenOffAndMeasuring) - // !IsContinuousModeActivated() - 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. @@ -222,8 +165,8 @@ void HeartRateTask::HandleSensorData(int* lastBpm) { bool noDataWithinTimeLimit = bpm == 0 && ShoudStopTryingToGetData(); bool dataWithinTimeLimit = bpm != 0; if (dataWithinTimeLimit || noDataWithinTimeLimit) { - state = States::ScreenOffAndWaiting; - StopMeasurement(); + isBackgroundMeasuring = false; + StopSensor(); } } diff --git a/src/heartratetask/HeartRateTask.h b/src/heartratetask/HeartRateTask.h index 39a8b33358..36f9b46ca3 100644 --- a/src/heartratetask/HeartRateTask.h +++ b/src/heartratetask/HeartRateTask.h @@ -7,58 +7,6 @@ #define DURATION_UNTIL_BACKGROUND_MEASUREMENT_IS_STOPPED pdMS_TO_TICKS(30 * 1000) -/* - *** Background Measurement deactivated *** - - - -┌─────────────────────────┐ ┌─────────────────────────┐ -│ ├───StartMeasurement───►│ │ -│ ScreenOnAndStopped │ │ ScreenOnAndMeasuring │ -│ │◄──StopMeasurement │ │ -└──▲────────────────┬─────┘ └──▲──────────────────┬───┘ - │ │ │ │ - WakeUp GoToSleep WakeUp GoToSleep - │ │ │ │ -┌──┴────────────────▼─────┐ ┌──┴──────────────────▼───┐ -│ │ │ │ -│ ScreenOffAndStopped │ │ ScreenOffAndWaiting │ -│ │ │ │ -└─────────────────────────┘ └─────────────────────────┘ - - - - - - *** Background Measurement activated *** - - - -┌─────────────────────────┐ ┌─────────────────────────┐ -│ ├───StartMeasurement───►│ │ -│ ScreenOnAndStopped │ │ ScreenOnAndMeasuring │ -│ │◄──StopMeasurement │ │ -└──▲────────────────┬─────┘ └──▲──────────────────┬───┘ - │ │ ┌───────┘ │ - WakeUp GoToSleep │ WakeUp GoToSleep - │ │ │ │ │ -┌──┴────────────────▼─────┐ │ ┌──┴──────────────────▼───┐ -│ │ │ │ │ -│ ScreenOffAndStopped │ │ │ ScreenOffAndWating │ -│ │ │ │ │ -└─────────────────────────┘ │ └───┬──────────────────▲──┘ - │ │ │ - │ Waited Got sensor - │ interval data - │ time │ - │ │ │ - WakeUp ┌───▼──────────────────┴──┐ - │ │ │ - └────┤ ScreenOffAndMeasuring │ - │ │ - └─────────────────────────┘ - */ - namespace Pinetime { namespace Drivers { class Hrs3300; @@ -73,8 +21,6 @@ namespace Pinetime { public: enum class Messages : uint8_t { GoToSleep, WakeUp, StartMeasurement, StopMeasurement }; - enum class States { ScreenOnAndStopped, ScreenOnAndMeasuring, ScreenOffAndStopped, ScreenOffAndWaiting, ScreenOffAndMeasuring }; - explicit HeartRateTask(Drivers::Hrs3300& heartRateSensor, Controllers::HeartRateController& controller, Controllers::Settings& settings); @@ -84,15 +30,15 @@ namespace Pinetime { 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 HandleBackgroundWaiting(); + void HandleWaiting(); void HandleSensorData(int* lastBpm); TickType_t GetBackgroundIntervalInTicks(); @@ -105,11 +51,16 @@ namespace Pinetime { TaskHandle_t taskHandle; QueueHandle_t messageQueue; - States state = States::ScreenOnAndStopped; + + bool isBackgroundMeasuring = false; + bool isScreenOn = true; + bool isMeasurementActivated = false; + Drivers::Hrs3300& heartRateSensor; Controllers::HeartRateController& controller; Controllers::Settings& settings; Controllers::Ppg ppg; + TickType_t measurementStart = 0; };