diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.vscode/settings.json b/.vscode/settings.json index a7b04eea..ad8decfe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -66,5 +66,8 @@ "streambuf": "cpp", "cinttypes": "cpp", "typeinfo": "cpp" - } + }, + "cSpell.words": [ + "Pinetime" + ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b5669b0..84e3914e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose Debug or Release") -project(pinetime VERSION 1.14.0 LANGUAGES C CXX ASM) +project(pinetime VERSION 1.15.1 LANGUAGES C CXX ASM) set(CMAKE_C_STANDARD 99) set(CMAKE_CXX_STANDARD 20) diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..f620865d --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).defaultNix diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..742baac7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-compat": { + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "revCount": 57, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1725634671, + "narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..f9dd94c0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,114 @@ +{ + description = "A very basic flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; + }; + + outputs = { self, ... }@inputs: + let + forAllSystems = function: + inputs.nixpkgs.lib.genAttrs [ + "x86_64-linux" + "aarch64-linux" + ] + (system: function (import inputs.nixpkgs { + inherit system; + config.allowUnfree = true; + })); + in + { + packages = forAllSystems (pkgs: + let + infinitime-nrf5-sdk = pkgs.nrf5-sdk.overrideAttrs (old: { + version = "15.3.0"; + src = pkgs.fetchzip { + url = "https://nsscprodmedia.blob.core.windows.net/prod/software-and-other-downloads/sdks/nrf5/binaries/nrf5sdk153059ac345.zip"; + sha256 = "sha256-pfmhbpgVv5x2ju489XcivguwpnofHbgVA7bFUJRTj08="; + }; + }); + in + with pkgs; { + default = stdenv.mkDerivation rec { + name = "infinitime"; + + src = fetchFromGitea { + domain = "git.techwork.zone"; + owner = "scott"; + repo = "InfiniTime"; + rev = "1.15.1"; + hash = "sha256-9cy7qYRZrg7qZZwQzYeMIZivGLZ5uGUoTyPgHEvdnZ8="; + fetchSubmodules = true; + }; + + nativeBuildInputs = [ + cmake + nodePackages.lv_font_conv + python3 + python3.pkgs.cbor + python3.pkgs.click + python3.pkgs.cryptography + python3.pkgs.intelhex + python3.pkgs.pillow + adafruit-nrfutil + patch + git + ]; + + postPatch = '' + # /usr/bin/env is not available in the build sandbox + substituteInPlace src/displayapp/fonts/generate.py --replace "'/usr/bin/env', 'patch'" "'patch'" + substituteInPlace tools/mcuboot/imgtool.py --replace "/usr/bin/env python3" "${python3}/bin/python3" + ''; + + cmakeFlags = [ + ''-DARM_NONE_EABI_TOOLCHAIN_PATH=${gcc-arm-embedded-10}'' + ''-DNRF5_SDK_PATH=${infinitime-nrf5-sdk}/share/nRF5_SDK'' + ''-DBUILD_DFU=1'' + ''-DBUILD_RESOURCES=1'' + ''-DCMAKE_SOURCE_DIR=${src}'' + ]; + + installPhase = '' + SOURCES_DIR=${src} BUILD_DIR=. OUTPUT_DIR=$out ./post_build.sh + ''; + }; + }); + + devShells = forAllSystems (pkgs: + let + infinitime-nrf5-sdk = pkgs.nrf5-sdk.overrideAttrs (old: { + version = "15.3.0"; + src = pkgs.fetchzip { + url = "https://nsscprodmedia.blob.core.windows.net/prod/software-and-other-downloads/sdks/nrf5/binaries/nrf5sdk153059ac345.zip"; + sha256 = "sha256-pfmhbpgVv5x2ju489XcivguwpnofHbgVA7bFUJRTj08="; + }; + }); + in + { + default = + pkgs.buildFHSUserEnv { + name = "infinitime-devenv"; + # build tools + targetPkgs = pkgs: (with pkgs; [ + cmake + nodePackages.lv_font_conv + python3 + python3.pkgs.cbor + python3.pkgs.click + python3.pkgs.cryptography + python3.pkgs.intelhex + adafruit-nrfutil + + (pkgs.writeShellScriptBin "cmake_infinitime" '' + cmake -DARM_NONE_EABI_TOOLCHAIN_PATH="${pkgs.gcc-arm-embedded-10}" \ + -DNRF5_SDK_PATH="${infinitime-nrf5-sdk}/share/nRF5_SDK" \ + "$@" + '') + ]); + }; + }); + }; +} + diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..942ce016 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).shellNix 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..dce60d5c 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" @@ -333,7 +334,7 @@ void DisplayApp::Refresh() { } else { LoadNewScreen(Apps::Timer, DisplayApp::FullRefreshDirections::Up); } - motorController.RunForDuration(35); + motorController.StartRinging(); break; case Messages::AlarmTriggered: if (currentApp == Apps::Alarm) { @@ -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;