diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dc3b6176..1d4facf1 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 1ae00a2d..49073e1a 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 602de3a5..3597e7b6 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -50,6 +50,17 @@ namespace Pinetime { int colorIndex = 0; }; + enum class HeartRateBackgroundMeasurementInterval : uint8_t { + Off, + Continuous, + TenSeconds, + ThirtySeconds, + OneMinute, + FiveMinutes, + TenMinutes, + ThirtyMinutes, + }; + Settings(Pinetime::Controllers::FS& fs); Settings(const Settings&) = delete; @@ -298,10 +309,21 @@ namespace Pinetime { return bleRadioEnabled; }; + HeartRateBackgroundMeasurementInterval GetHeartRateBackgroundMeasurementInterval() const { + return settings.heartRateBackgroundMeasurementInterval; + } + + void SetHeartRateBackgroundMeasurementInterval(HeartRateBackgroundMeasurementInterval newHeartRateBackgroundMeasurementInterval) { + if (newHeartRateBackgroundMeasurementInterval != settings.heartRateBackgroundMeasurementInterval) { + settingsChanged = true; + } + settings.heartRateBackgroundMeasurementInterval = newHeartRateBackgroundMeasurementInterval; + } + 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 +347,8 @@ namespace Pinetime { uint16_t shakeWakeThreshold = 150; Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium; + + HeartRateBackgroundMeasurementInterval heartRateBackgroundMeasurementInterval = HeartRateBackgroundMeasurementInterval::Off; }; SettingsData settings; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 79519621..3f8abc55 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" @@ -564,6 +565,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(this, settingsController); break; diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 2104a267..a74ca7a8 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 00000000..fdba9af1 --- /dev/null +++ b/src/displayapp/screens/settings/SettingHeartRate.cpp @@ -0,0 +1,75 @@ +#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); + + 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 (settingsController.GetHeartRateBackgroundMeasurementInterval() == options[i].interval) { + 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); + settingsController.SetHeartRateBackgroundMeasurementInterval(options[i].interval); + } 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 00000000..3cb08907 --- /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 Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval interval; + 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 = {{ + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::Off, " Off"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::Continuous, "Cont"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::TenSeconds, " 10s"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::ThirtySeconds, " 30s"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::OneMinute, " 1m"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::FiveMinutes, " 5m"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::TenMinutes, " 10m"}, + {Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::ThirtyMinutes, " 30m"}, + }}; + + lv_obj_t* cbOption[options.size()]; + }; + } + } +} diff --git a/src/displayapp/screens/settings/Settings.h b/src/displayapp/screens/settings/Settings.h index a21b4ccd..4f1082ad 100644 --- a/src/displayapp/screens/settings/Settings.h +++ b/src/displayapp/screens/settings/Settings.h @@ -38,15 +38,16 @@ 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}, diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp index 9d82d11e..0c082a5d 100644 --- a/src/heartratetask/HeartRateTask.cpp +++ b/src/heartratetask/HeartRateTask.cpp @@ -5,8 +5,23 @@ 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,77 +40,40 @@ void HeartRateTask::Process(void* instance) { void HeartRateTask::Work() { int lastBpm = 0; - while (true) { - 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)) { + while (true) { + TickType_t delay = CurrentTaskDelay(state, ppg.deltaTms); + Messages msg; + + 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) { - int8_t ambient = ppg.Preprocess(heartRateSensor.ReadHrs(), heartRateSensor.ReadAls()); - 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; } } } @@ -110,6 +88,7 @@ void HeartRateTask::StartMeasurement() { heartRateSensor.Enable(); ppg.Reset(true); vTaskDelay(100); + measurementStart = xTaskGetTickCount(); } void HeartRateTask::StopMeasurement() { @@ -117,3 +96,165 @@ void HeartRateTask::StopMeasurement() { ppg.Reset(true); vTaskDelay(100); } + +void HeartRateTask::StartWaiting() { + StopMeasurement(); + backgroundWaitingStart = xTaskGetTickCount(); +} + +void HeartRateTask::HandleGoToSleep() { + switch (state) { + case States::ScreenOnAndStopped: + state = States::ScreenOffAndStopped; + break; + case States::ScreenOnAndMeasuring: + state = States::ScreenOffAndMeasuring; + 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::ScreenOffAndStopped: + case States::ScreenOnAndStopped: + state = States::ScreenOnAndMeasuring; + *lastBpm = 0; + StartMeasurement(); + break; + 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: + state = States::ScreenOffAndStopped; + StopMeasurement(); + break; + case States::ScreenOnAndStopped: + case States::ScreenOffAndStopped: + // shouldn't happen -> ignore + break; + } +} + +void HeartRateTask::HandleBackgroundWaiting() { + if (!IsBackgroundMeasurementActivated()) { + return; + } + + TickType_t ticksSinceWaitingStart = xTaskGetTickCount() - backgroundWaitingStart; + if (ticksSinceWaitingStart >= GetHeartRateBackgroundMeasurementIntervalInTicks()) { + state = States::ScreenOffAndMeasuring; + StartMeasurement(); + } +} + +void HeartRateTask::HandleSensorData(int* lastBpm) { + int8_t ambient = ppg.Preprocess(heartRateSensor.ReadHrs(), heartRateSensor.ReadAls()); + 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; + } + + if (*lastBpm == 0 && bpm == 0) { + 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; + } + if (state == States::ScreenOffAndMeasuring) { + state = States::ScreenOffAndWaiting; + StartWaiting(); + } + } + TickType_t ticksSinceMeasurementStart = xTaskGetTickCount() - measurementStart; + if (bpm == 0 && state == States::ScreenOffAndMeasuring && !IsContinuousModeActivated() && + ticksSinceMeasurementStart >= DURATION_UNTIL_BACKGROUND_MEASUREMENT_IS_STOPPED) { + state = States::ScreenOffAndWaiting; + StartWaiting(); + } +} + +TickType_t HeartRateTask::GetHeartRateBackgroundMeasurementIntervalInTicks() { + int ms; + switch (settings.GetHeartRateBackgroundMeasurementInterval()) { + case Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::TenSeconds: + ms = 10 * 1000; + break; + case Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::ThirtySeconds: + ms = 30 * 1000; + break; + case Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::OneMinute: + ms = 60 * 1000; + break; + case Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::FiveMinutes: + ms = 5 * 60 * 1000; + break; + case Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::TenMinutes: + ms = 10 * 60 * 1000; + break; + case Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::ThirtyMinutes: + ms = 30 * 60 * 1000; + break; + default: + ms = 0; + break; + } + return pdMS_TO_TICKS(ms); +} + +bool HeartRateTask::IsContinuousModeActivated() { + return settings.GetHeartRateBackgroundMeasurementInterval() == + Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::Continuous; +} + +bool HeartRateTask::IsBackgroundMeasurementActivated() { + return settings.GetHeartRateBackgroundMeasurementInterval() != + Pinetime::Controllers::Settings::HeartRateBackgroundMeasurementInterval::Off; +} diff --git a/src/heartratetask/HeartRateTask.h b/src/heartratetask/HeartRateTask.h index 5bbfb9fb..f7d7e38b 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 { @@ -16,10 +19,24 @@ namespace Pinetime { namespace Applications { class HeartRateTask { public: - enum class Messages : uint8_t { GoToSleep, WakeUp, StartMeasurement, StopMeasurement }; - enum class States { Idle, Running }; + enum class Messages : uint8_t { + GoToSleep, + WakeUp, + StartMeasurement, + StopMeasurement + }; - 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); @@ -28,14 +45,29 @@ namespace Pinetime { static void Process(void* instance); void StartMeasurement(); void StopMeasurement(); + void StartWaiting(); + + void HandleGoToSleep(); + void HandleWakeUp(); + void HandleStartMeasurement(int* lastBpm); + void HandleStopMeasurement(); + + void HandleBackgroundWaiting(); + void HandleSensorData(int* lastBpm); + + TickType_t GetHeartRateBackgroundMeasurementIntervalInTicks(); + bool IsContinuousModeActivated(); + bool IsBackgroundMeasurementActivated(); 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 backgroundWaitingStart = 0; + TickType_t measurementStart = 0; }; } diff --git a/src/main.cpp b/src/main.cpp index ab50fa74..9db1abf0 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;