aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormark9064 <30447455+mark9064@users.noreply.github.com>2025-06-18 14:09:57 +0100
committermark9064 <30447455+mark9064@users.noreply.github.com>2025-11-05 10:34:49 +0000
commit8daddf87782c1228a44528da6f67d8dfce3edb40 (patch)
treedead2a56c55f5eec74c70f42389c89e379aa4ce7
parent04afd22943cf4d6a826e09cf5fd246886ee7cacf (diff)
Background heartrate measurement
Co-Authored-By: Patric Gruber <me@patric-gruber.at>
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/components/heartrate/HeartRateController.cpp8
-rw-r--r--src/components/heartrate/HeartRateController.h6
-rw-r--r--src/components/heartrate/Ppg.cpp5
-rw-r--r--src/components/heartrate/Ppg.h1
-rw-r--r--src/components/settings/Settings.h20
-rw-r--r--src/displayapp/DisplayApp.cpp4
-rw-r--r--src/displayapp/apps/Apps.h.in1
-rw-r--r--src/displayapp/screens/HeartRate.cpp4
-rw-r--r--src/displayapp/screens/settings/SettingHeartRate.cpp71
-rw-r--r--src/displayapp/screens/settings/SettingHeartRate.h44
-rw-r--r--src/displayapp/screens/settings/Settings.h11
-rw-r--r--src/heartratetask/HeartRateTask.cpp224
-rw-r--r--src/heartratetask/HeartRateTask.h24
-rw-r--r--src/main.cpp6
15 files changed, 343 insertions, 87 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 43aa1d15..dbbdb010 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -412,6 +412,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/heartrate/HeartRateController.cpp b/src/components/heartrate/HeartRateController.cpp
index e0d69272..c365e865 100644
--- a/src/components/heartrate/HeartRateController.cpp
+++ b/src/components/heartrate/HeartRateController.cpp
@@ -12,17 +12,17 @@ void HeartRateController::Update(HeartRateController::States newState, uint8_t h
}
}
-void HeartRateController::Start() {
+void HeartRateController::Enable() {
if (task != nullptr) {
state = States::NotEnoughData;
- task->PushMessage(Pinetime::Applications::HeartRateTask::Messages::StartMeasurement);
+ task->PushMessage(Pinetime::Applications::HeartRateTask::Messages::Enable);
}
}
-void HeartRateController::Stop() {
+void HeartRateController::Disable() {
if (task != nullptr) {
state = States::Stopped;
- task->PushMessage(Pinetime::Applications::HeartRateTask::Messages::StopMeasurement);
+ task->PushMessage(Pinetime::Applications::HeartRateTask::Messages::Disable);
}
}
diff --git a/src/components/heartrate/HeartRateController.h b/src/components/heartrate/HeartRateController.h
index f66c79f8..5bd3a8ef 100644
--- a/src/components/heartrate/HeartRateController.h
+++ b/src/components/heartrate/HeartRateController.h
@@ -15,11 +15,11 @@ namespace Pinetime {
namespace Controllers {
class HeartRateController {
public:
- enum class States { Stopped, NotEnoughData, NoTouch, Running };
+ enum class States : uint8_t { Stopped, NotEnoughData, NoTouch, Running };
HeartRateController() = default;
- void Start();
- void Stop();
+ void Enable();
+ void Disable();
void Update(States newState, uint8_t heartRate);
void SetHeartRateTask(Applications::HeartRateTask* task);
diff --git a/src/components/heartrate/Ppg.cpp b/src/components/heartrate/Ppg.cpp
index efbed852..25be6237 100644
--- a/src/components/heartrate/Ppg.cpp
+++ b/src/components/heartrate/Ppg.cpp
@@ -155,8 +155,12 @@ int8_t Ppg::Preprocess(uint16_t hrs, uint16_t als) {
int Ppg::HeartRate() {
if (dataIndex < dataLength) {
+ if (!enoughData) {
+ return -2;
+ }
return 0;
}
+ enoughData = true;
int hr = 0;
hr = ProcessHeartRate(resetSpectralAvg);
resetSpectralAvg = false;
@@ -171,6 +175,7 @@ int Ppg::HeartRate() {
void Ppg::Reset(bool resetDaqBuffer) {
if (resetDaqBuffer) {
dataIndex = 0;
+ enoughData = false;
}
avgIndex = 0;
dataAverage.fill(0.0f);
diff --git a/src/components/heartrate/Ppg.h b/src/components/heartrate/Ppg.h
index 373e7985..78935382 100644
--- a/src/components/heartrate/Ppg.h
+++ b/src/components/heartrate/Ppg.h
@@ -71,6 +71,7 @@ namespace Pinetime {
uint16_t dataIndex = 0;
float peakLocation;
bool resetSpectralAvg = true;
+ bool enoughData = false;
int ProcessHeartRate(bool init);
float HeartRateAverage(float hr);
diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h
index 093a3ac6..9133d3fe 100644
--- a/src/components/settings/Settings.h
+++ b/src/components/settings/Settings.h
@@ -1,6 +1,8 @@
#pragma once
#include <cstdint>
#include <bitset>
+#include <limits>
+#include <optional>
#include "components/brightness/BrightnessController.h"
#include "components/fs/FS.h"
#include "displayapp/apps/Apps.h"
@@ -334,10 +336,25 @@ namespace Pinetime {
return (settings.dfuAndFsEnabledOnBoot ? DfuAndFsMode::Enabled : DfuAndFsMode::Disabled);
};
+ std::optional<uint16_t> GetHeartRateBackgroundMeasurementInterval() const {
+ if (settings.heartRateBackgroundPeriod == std::numeric_limits<uint16_t>::max()) {
+ return std::nullopt;
+ }
+ return settings.heartRateBackgroundPeriod;
+ }
+
+ void SetHeartRateBackgroundMeasurementInterval(std::optional<uint16_t> newIntervalInSeconds) {
+ newIntervalInSeconds = newIntervalInSeconds.value_or(std::numeric_limits<uint16_t>::max());
+ if (newIntervalInSeconds != settings.heartRateBackgroundPeriod) {
+ settingsChanged = true;
+ }
+ settings.heartRateBackgroundPeriod = newIntervalInSeconds.value();
+ }
+
private:
Pinetime::Controllers::FS& fs;
- static constexpr uint32_t settingsVersion = 0x0009;
+ static constexpr uint32_t settingsVersion = 0x000a;
struct SettingsData {
uint32_t version = settingsVersion;
@@ -365,6 +382,7 @@ namespace Pinetime {
Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium;
bool dfuAndFsEnabledOnBoot = false;
+ uint16_t heartRateBackgroundPeriod = std::numeric_limits<uint16_t>::max(); // Disabled by default
};
SettingsData settings;
diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp
index 7585c55d..45a41032 100644
--- a/src/displayapp/DisplayApp.cpp
+++ b/src/displayapp/DisplayApp.cpp
@@ -48,6 +48,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"
#include "displayapp/screens/settings/SettingOTA.h"
@@ -606,6 +607,9 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio
case Apps::SettingWakeUp:
currentScreen = std::make_unique<Screens::SettingWakeUp>(settingsController);
break;
+ case Apps::SettingHeartRate:
+ currentScreen = std::make_unique<Screens::SettingHeartRate>(settingsController);
+ break;
case Apps::SettingDisplay:
currentScreen = std::make_unique<Screens::SettingDisplay>(settingsController);
break;
diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in
index 0e94d32a..d440b598 100644
--- a/src/displayapp/apps/Apps.h.in
+++ b/src/displayapp/apps/Apps.h.in
@@ -36,6 +36,7 @@ namespace Pinetime {
SettingWatchFace,
SettingTimeFormat,
SettingWeatherFormat,
+ SettingHeartRate,
SettingDisplay,
SettingWakeUp,
SettingSteps,
diff --git a/src/displayapp/screens/HeartRate.cpp b/src/displayapp/screens/HeartRate.cpp
index 1a84d349..14c873e2 100644
--- a/src/displayapp/screens/HeartRate.cpp
+++ b/src/displayapp/screens/HeartRate.cpp
@@ -98,12 +98,12 @@ void HeartRate::Refresh() {
void HeartRate::OnStartStopEvent(lv_event_t event) {
if (event == LV_EVENT_CLICKED) {
if (heartRateController.State() == Controllers::HeartRateController::States::Stopped) {
- heartRateController.Start();
+ heartRateController.Enable();
UpdateStartStopButton(heartRateController.State() != Controllers::HeartRateController::States::Stopped);
wakeLock.Lock();
lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::highlight);
} else {
- heartRateController.Stop();
+ heartRateController.Disable();
UpdateStartStopButton(heartRateController.State() != Controllers::HeartRateController::States::Stopped);
wakeLock.Release();
lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray);
diff --git a/src/displayapp/screens/settings/SettingHeartRate.cpp b/src/displayapp/screens/settings/SettingHeartRate.cpp
new file mode 100644
index 00000000..a45dc835
--- /dev/null
+++ b/src/displayapp/screens/settings/SettingHeartRate.cpp
@@ -0,0 +1,71 @@
+#include "displayapp/screens/settings/SettingHeartRate.h"
+#include <lvgl/lvgl.h>
+#include "displayapp/screens/Styles.h"
+#include "displayapp/screens/Symbols.h"
+
+using namespace Pinetime::Applications::Screens;
+
+namespace {
+ void EventHandler(lv_obj_t* obj, lv_event_t event) {
+ auto* screen = static_cast<SettingHeartRate*>(obj->user_data);
+ screen->UpdateSelected(obj, event);
+ }
+}
+
+SettingHeartRate::SettingHeartRate(Pinetime::Controllers::Settings& settingsController) : settingsController {settingsController} {
+ lv_obj_t* container = lv_cont_create(lv_scr_act(), nullptr);
+
+ lv_obj_set_style_local_bg_opa(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
+ lv_obj_set_style_local_pad_all(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 5);
+ lv_obj_set_style_local_pad_inner(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 5);
+ lv_obj_set_style_local_border_width(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 0);
+
+ lv_obj_set_pos(container, 10, 60);
+ lv_obj_set_width(container, LV_HOR_RES - 20);
+ lv_obj_set_height(container, LV_VER_RES - 50);
+ lv_cont_set_layout(container, 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);
+
+ std::optional<uint16_t> currentInterval = settingsController.GetHeartRateBackgroundMeasurementInterval();
+
+ for (std::size_t i = 0; i < options.size(); i++) {
+ cbOption[i] = lv_checkbox_create(container, nullptr);
+ lv_checkbox_set_text(cbOption[i], options[i].name);
+ cbOption[i]->user_data = this;
+ lv_obj_set_event_cb(cbOption[i], EventHandler);
+ SetRadioButtonStyle(cbOption[i]);
+
+ if (options[i].intervalInSeconds == 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 (std::size_t i = 0; i < options.size(); i++) {
+ if (object == cbOption[i]) {
+ lv_checkbox_set_checked(cbOption[i], true);
+ settingsController.SetHeartRateBackgroundMeasurementInterval(options[i].intervalInSeconds);
+ } 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..736f2b10
--- /dev/null
+++ b/src/displayapp/screens/settings/SettingHeartRate.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <cstdint>
+#include <lvgl/lvgl.h>
+#include <optional>
+#include <array>
+
+#include "components/settings/Settings.h"
+#include "displayapp/screens/Screen.h"
+
+namespace Pinetime {
+
+ namespace Applications {
+ namespace Screens {
+ class SettingHeartRate : public Screen {
+ public:
+ explicit SettingHeartRate(Pinetime::Controllers::Settings& settings);
+ ~SettingHeartRate() override;
+
+ void UpdateSelected(lv_obj_t* object, lv_event_t event);
+
+ private:
+ struct Option {
+ std::optional<uint16_t> intervalInSeconds;
+ const char* name;
+ };
+
+ Pinetime::Controllers::Settings& settingsController;
+
+ static constexpr std::array<Option, 7> options = {{
+ {.intervalInSeconds = std::nullopt, .name = " Off"},
+ {.intervalInSeconds = 0, .name = "Cont"},
+ {.intervalInSeconds = 30, .name = " 30s"},
+ {.intervalInSeconds = 60, .name = " 1m"},
+ {.intervalInSeconds = 5 * 60, .name = " 5m"},
+ {.intervalInSeconds = 10 * 60, .name = " 10m"},
+ {.intervalInSeconds = 30 * 60, .name = " 30m"},
+ }};
+
+ lv_obj_t* cbOption[options.size()];
+ };
+ }
+ }
+}
diff --git a/src/displayapp/screens/settings/Settings.h b/src/displayapp/screens/settings/Settings.h
index 370e83f4..32ac3ca9 100644
--- a/src/displayapp/screens/settings/Settings.h
+++ b/src/displayapp/screens/settings/Settings.h
@@ -38,23 +38,18 @@ 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::shieldAlt, "Over-the-air", Apps::SettingOTA},
+ {Symbols::shieldAlt, "Over-the-air", Apps::SettingOTA},
{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},
-
}};
ScreenList<nScreens> screens;
};
diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp
index 8a5a871b..d23feeda 100644
--- a/src/heartratetask/HeartRateTask.cpp
+++ b/src/heartratetask/HeartRateTask.cpp
@@ -1,12 +1,56 @@
#include "heartratetask/HeartRateTask.h"
#include <drivers/Hrs3300.h>
#include <components/heartrate/HeartRateController.h>
-#include <nrf_log.h>
using namespace Pinetime::Applications;
-HeartRateTask::HeartRateTask(Drivers::Hrs3300& heartRateSensor, Controllers::HeartRateController& controller)
- : heartRateSensor {heartRateSensor}, controller {controller} {
+namespace {
+ constexpr TickType_t backgroundMeasurementTimeLimit = 30 * configTICK_RATE_HZ;
+}
+
+std::optional<TickType_t> HeartRateTask::BackgroundMeasurementInterval() const {
+ auto interval = settings.GetHeartRateBackgroundMeasurementInterval();
+ if (!interval.has_value()) {
+ return std::nullopt;
+ }
+ return interval.value() * configTICK_RATE_HZ;
+}
+
+bool HeartRateTask::BackgroundMeasurementNeeded() const {
+ auto backgroundPeriod = BackgroundMeasurementInterval();
+ if (!backgroundPeriod.has_value()) {
+ return false;
+ }
+ return xTaskGetTickCount() - lastMeasurementTime >= backgroundPeriod.value();
+};
+
+TickType_t HeartRateTask::CurrentTaskDelay() const {
+ auto backgroundPeriod = BackgroundMeasurementInterval();
+ TickType_t currentTime = xTaskGetTickCount();
+ switch (state) {
+ case States::Disabled:
+ return portMAX_DELAY;
+ case States::Waiting:
+ // Sleep until a new event if background measuring disabled
+ if (!backgroundPeriod.has_value()) {
+ return portMAX_DELAY;
+ }
+ // Sleep until the next background measurement
+ if (currentTime - lastMeasurementTime < backgroundPeriod.value()) {
+ return backgroundPeriod.value() - (currentTime - lastMeasurementTime);
+ }
+ // If one is due now, go straight away
+ return 0;
+ case States::BackgroundMeasuring:
+ case States::ForegroundMeasuring:
+ return Pinetime::Controllers::Ppg::deltaTms;
+ }
+}
+
+HeartRateTask::HeartRateTask(Drivers::Hrs3300& heartRateSensor,
+ Controllers::HeartRateController& controller,
+ Controllers::Settings& settings)
+ : heartRateSensor {heartRateSensor}, controller {controller}, settings {settings} {
}
void HeartRateTask::Start() {
@@ -24,79 +68,68 @@ void HeartRateTask::Process(void* instance) {
}
void HeartRateTask::Work() {
- int lastBpm = 0;
+ // measurementStartTime is always initialised before use by StartMeasurement
+ // Need to initialise lastMeasurementTime so that the first background measurement happens at a reasonable time
+ lastMeasurementTime = xTaskGetTickCount();
+ valueCurrentlyShown = false;
+
while (true) {
+ TickType_t delay = CurrentTaskDelay();
Messages msg;
- uint32_t delay;
- if (state == States::Running) {
- if (measurementStarted) {
- delay = ppg.deltaTms;
- } else {
- delay = 100;
- }
- } else {
- delay = portMAX_DELAY;
- }
+ States newState = state;
- if (xQueueReceive(messageQueue, &msg, delay)) {
+ if (xQueueReceive(messageQueue, &msg, delay) == pdTRUE) {
switch (msg) {
case Messages::GoToSleep:
- StopMeasurement();
- state = States::Idle;
- break;
- case Messages::WakeUp:
- state = States::Running;
- if (measurementStarted) {
- lastBpm = 0;
- StartMeasurement();
- }
- break;
- case Messages::StartMeasurement:
- if (measurementStarted) {
+ // Ignore power state changes when disabled
+ if (state == States::Disabled) {
break;
}
- lastBpm = 0;
- StartMeasurement();
- measurementStarted = true;
+ // State is necessarily ForegroundMeasuring
+ // As previously screen was on and measurement is enabled
+ if (BackgroundMeasurementNeeded()) {
+ newState = States::BackgroundMeasuring;
+ } else {
+ newState = States::Waiting;
+ }
break;
- case Messages::StopMeasurement:
- if (!measurementStarted) {
+ case Messages::WakeUp:
+ // Ignore power state changes when disabled
+ if (state == States::Disabled) {
break;
}
- StopMeasurement();
- measurementStarted = false;
+ newState = States::ForegroundMeasuring;
+ break;
+ case Messages::Enable:
+ // Can only be enabled when the screen is on
+ // If this constraint is somehow violated, the unexpected state
+ // will self-resolve at the next screen on event
+ newState = States::ForegroundMeasuring;
+ valueCurrentlyShown = false;
+ break;
+ case Messages::Disable:
+ newState = States::Disabled;
break;
}
}
+ if (newState == States::Waiting && BackgroundMeasurementNeeded()) {
+ newState = States::BackgroundMeasuring;
+ } else if (newState == States::BackgroundMeasuring && !BackgroundMeasurementNeeded()) {
+ newState = States::Waiting;
+ }
- 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);
- }
+ // Apply state transition (switch sensor on/off)
+ if ((newState == States::ForegroundMeasuring || newState == States::BackgroundMeasuring) &&
+ (state == States::Waiting || state == States::Disabled)) {
+ StartMeasurement();
+ } else if ((newState == States::Waiting || newState == States::Disabled) &&
+ (state == States::ForegroundMeasuring || state == States::BackgroundMeasuring)) {
+ StopMeasurement();
+ }
+ state = newState;
- if (bpm != 0) {
- lastBpm = bpm;
- controller.Update(Controllers::HeartRateController::States::Running, lastBpm);
- }
+ if (state == States::ForegroundMeasuring || state == States::BackgroundMeasuring) {
+ HandleSensorData();
}
}
}
@@ -111,6 +144,8 @@ void HeartRateTask::StartMeasurement() {
heartRateSensor.Enable();
ppg.Reset(true);
vTaskDelay(100);
+ measurementSucceeded = false;
+ measurementStartTime = xTaskGetTickCount();
}
void HeartRateTask::StopMeasurement() {
@@ -118,3 +153,70 @@ void HeartRateTask::StopMeasurement() {
ppg.Reset(true);
vTaskDelay(100);
}
+
+void HeartRateTask::HandleSensorData() {
+ auto sensorData = heartRateSensor.ReadHrsAls();
+ int8_t ambient = ppg.Preprocess(sensorData.hrs, sensorData.als);
+ int bpm = ppg.HeartRate();
+
+ // Ambient light detected
+ if (ambient > 0) {
+ // Reset all DAQ buffers
+ ppg.Reset(true);
+ controller.Update(Controllers::HeartRateController::States::NotEnoughData, bpm);
+ bpm = 0;
+ valueCurrentlyShown = false;
+ }
+
+ // Reset requested, or not enough data
+ if (bpm == -1) {
+ // 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);
+ valueCurrentlyShown = false;
+ } else if (bpm == -2) {
+ // Not enough data
+ bpm = 0;
+ if (!valueCurrentlyShown) {
+ controller.Update(Controllers::HeartRateController::States::NotEnoughData, bpm);
+ }
+ }
+
+ if (bpm != 0) {
+ // Maintain constant frequency acquisition in background mode
+ // If the last measurement time is set to the start time, then the next measurement
+ // will start exactly one background period after this one
+ // Avoid this if measurement exceeded the time limit (which happens with background intervals <= limit)
+ if (state == States::BackgroundMeasuring && xTaskGetTickCount() - measurementStartTime < backgroundMeasurementTimeLimit) {
+ lastMeasurementTime = measurementStartTime;
+ } else {
+ lastMeasurementTime = xTaskGetTickCount();
+ }
+ measurementSucceeded = true;
+ valueCurrentlyShown = true;
+ controller.Update(Controllers::HeartRateController::States::Running, bpm);
+ return;
+ }
+ // If been measuring for longer than the time limit, set the last measurement time
+ // This allows giving up on background measurement after a while
+ // and also means that background measurement won't begin immediately after
+ // an unsuccessful long foreground measurement
+ if (xTaskGetTickCount() - measurementStartTime > backgroundMeasurementTimeLimit) {
+ // When measuring, propagate failure if no value within the time limit
+ // Prevents stale heart rates from being displayed for >1 background period
+ // Or more than the time limit after switching to screen on (where the last background measurement was successful)
+ // Note: Once a successful measurement is recorded in screen on it will never be cleared
+ // without some other state change e.g. ambient light reset
+ if (!measurementSucceeded) {
+ controller.Update(Controllers::HeartRateController::States::Running, 0);
+ valueCurrentlyShown = false;
+ }
+ if (state == States::BackgroundMeasuring) {
+ lastMeasurementTime = xTaskGetTickCount() - backgroundMeasurementTimeLimit;
+ } else {
+ lastMeasurementTime = xTaskGetTickCount();
+ }
+ }
+}
diff --git a/src/heartratetask/HeartRateTask.h b/src/heartratetask/HeartRateTask.h
index 5bbfb9fb..9478d0d4 100644
--- a/src/heartratetask/HeartRateTask.h
+++ b/src/heartratetask/HeartRateTask.h
@@ -1,8 +1,11 @@
#pragma once
#include <FreeRTOS.h>
+#include <cstdint>
+#include <optional>
#include <task.h>
#include <queue.h>
#include <components/heartrate/Ppg.h>
+#include "components/settings/Settings.h"
namespace Pinetime {
namespace Drivers {
@@ -16,26 +19,37 @@ 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, Enable, Disable };
- 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:
+ enum class States : uint8_t { Disabled, Waiting, BackgroundMeasuring, ForegroundMeasuring };
static void Process(void* instance);
+ void HandleSensorData();
void StartMeasurement();
void StopMeasurement();
+ [[nodiscard]] bool BackgroundMeasurementNeeded() const;
+ [[nodiscard]] std::optional<TickType_t> BackgroundMeasurementInterval() const;
+ [[nodiscard]] TickType_t CurrentTaskDelay() const;
+
TaskHandle_t taskHandle;
QueueHandle_t messageQueue;
- States state = States::Running;
+ bool valueCurrentlyShown;
+ bool measurementSucceeded;
+ States state = States::Disabled;
Drivers::Hrs3300& heartRateSensor;
Controllers::HeartRateController& controller;
+ Controllers::Settings& settings;
Controllers::Ppg ppg;
- bool measurementStarted = false;
+ TickType_t lastMeasurementTime;
+ TickType_t measurementStartTime;
};
}
diff --git a/src/main.cpp b/src/main.cpp
index c573b482..d0ab3e48 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -94,13 +94,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;