Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heartrate measurements in background #1718

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f7b1111
add heart rate measurments in the background
patricgruber Mar 31, 2023
58c507e
increase task delay when waiting in the background to 10s
patricgruber Mar 31, 2023
0370e3c
remove background start timestamp reset on sleep
patricgruber Apr 3, 2023
a5db54a
rebase on main
patricgruber May 11, 2023
7ae790b
stop background after 30s of no data from the heart rate sensor
patricgruber May 25, 2023
5dbe1f7
properly format using clang-format
patricgruber Jul 7, 2023
27ee1eb
add settings screen to choose heartrate measurement background
patricgruber Aug 25, 2023
be1a519
use different style for the heartrate settings and fix issues with se…
patricgruber Aug 25, 2023
d376a85
use enum instead of uint32_t for heartrater interval setting
patricgruber Aug 26, 2023
a2edd93
add heart rate measurments in the background
patricgruber Mar 31, 2023
f94c074
rebase on main
patricgruber May 11, 2023
eeaf537
stop background after 30s of no data from the heart rate sensor
patricgruber May 25, 2023
69578a6
properly format using clang-format
patricgruber Jul 7, 2023
04ed068
add settings screen to choose heartrate measurement background
patricgruber Aug 25, 2023
520e509
fix rebase mistakes
patricgruber Dec 1, 2023
50d88bb
bump settings version, fix types
patricgruber Dec 1, 2023
4ed4d2c
use pdMS_TO_TICKS correctly, format using clang-format
patricgruber Dec 1, 2023
d78f262
fix DisplayApp.cpp
patricgruber Dec 11, 2023
3b432cd
fix issues after rebase on main
patricgruber Jan 2, 2024
ffc5f96
bump settings version
patricgruber Apr 10, 2024
7cf4f6e
fix bug where settings open pair pin screen
patricgruber Apr 10, 2024
6a0276f
fix settings screen
patricgruber Jul 11, 2024
6169263
refactor heartrate task (switch cases, comments with explanation)
patricgruber Jul 11, 2024
e6f0a89
reduce RAM size
patricgruber Jul 11, 2024
78af44e
keep measuring when transitioning to background
patricgruber Jul 17, 2024
cedca79
use switch case
patricgruber Aug 30, 2024
0978964
Merge branch 'heartrate-measurements-in-background' of github.com:pat…
patricgruber Aug 30, 2024
71b31c7
use switch case
patricgruber Aug 30, 2024
b846547
remove unnecessary file
patricgruber Aug 30, 2024
9501d36
use better state names
patricgruber Aug 30, 2024
7df3999
integrate code review
patricgruber Aug 31, 2024
462ea11
use interval as interval, instead of wait time
patricgruber Sep 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/components/settings/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ Settings::Settings(Pinetime::Controllers::FS& fs) : fs {fs} {
}

void Settings::Init() {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental whitespace removal?
Or if this is on purpose, also remove from below

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the other whitespace

// Load default settings from Flash
LoadSettingsFromFile();
}
Expand Down
26 changes: 25 additions & 1 deletion src/components/settings/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ namespace Pinetime {
int colorIndex = 0;
};

enum class HeartRateBackgroundMeasurementInterval : uint8_t {
Off,
Continuous,
TenSeconds,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ten seconds is not a good option to expose: the PPG takes at least 6.4s to get a value and there's honestly no point turning the sensor off for 3.6s, just leave it on and enjoy much more stable heart rate values as the spectral averaging won't get reset every cycle

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case then it would make sense to have a different interval instead. Which interval would you propose instead?
Would 15s make more sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having 30 as the minimum is OK. The sensor is already allowed to run for up to 30s in the background if it can't find a good signal, in which case 30s mode becomes the same as continuous

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting of the settings screen only really works with 2*n items
So it would be nice to have another value instead.
Maybe 15s ? that would be roughly 50% off time and 50% on time for the sensor.
And longer values than 30m doesn't really make sense anymore I think, but I'm also down to add one hour as an option

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm how does it look with an odd number of items? If there's a rendering bug it should be fixed, I could take a look

I misinterpreted the timings actually. It's not the measurement-measurement interval as I thought but actually the measurement finished (got value or given up) to measurement start waiting interval

Is that intended? If I enabled this feature at say 1 minute, I would expect a reading to arrive every 1 minute rather than every 1 minute 20 seconds (if it took 20s to find the HR)

If it's not intended then I think 15s is also a bit pointless, but if it is then I think it could just about make sense

There are two reasons I think short measurement times are bad:

  • It takes a minimum of 6 seconds for the heart rate to be found, and often more if the signal isn't super clean
  • The heart rate algorithm becomes more accurate the longer it is running

So sensor duty cycles (% of time passing with sensor on) between say 50-100% are a bit pointless IMO, just run the sensor all the time and receive the benefits of always having a value, and that value being more stable and reliable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not a perfect interval setting (meaning setting it to 1 minute = one value every minute). It's just a quick and easy solution to get the feature of repetitive measurements in the background.

I think it is difficult to make it more accurate. Since it can't be determined beforehand how long the sensor needs to measure until enough data is captured right? I mean there is an upper limit of 30s (for which the heart rate task just skips the current measurement) and there is a lower limit of around 6s, but that's still a wide range of measuring time. I think the current implementation is easy and quite robust in the regards that you know "If there was a measurement, then after x time another measurement is started"
Otherwise you would need to store the timestamp of the last measurement, and predict how long the sensor needs to take for the next measurement and start preemptively like (predicted time: 10s, interval time: 15s -> actual wait time: 5s) but this just moves the possible wait time from [21s, 44s] to [11s, 34s]. So we could decide to have a hard coded "expected sensor time"
The best approach would be a running average of the last n "sensor times" which would probably be the best predictor, but this adds two additional variables and one additional magic number (window size | n), which increases the added size to the binary. It's not a lot of course, but I had to already move an integer from a instance variable to a local variable that is changed via pointer and do other things to keep the size under the limit.

I'm fine with having a more complicated algorithm, but it seems a lot of effort for probably not a small positive effect, since I assume most people don't need a very precise interval. Also it only really matters for smaller values. If you use a 5 minute interval, another 6-15s is not really that important.

I'm also assuming, that having the sensor running all the time wastes battery. If you just want a HR roughly every 15 seconds, then having 15s wait + 6s measurement means having the sensor 71% of the time off. Which is quite a lot better than having it run all the time. Even if it degrades to 15s wait + 30s measurement, it's still 33% of the time deactivated. Instead of always on.
Even if the output quality might be a bit less accurate. There is always the option of continuous mode if someone wants more and better data.
And the code would have to decide based on the chosen interval setting if the sensor should be running all the time or have breaks in between, which makes it more complicated again.

For longer interval times, this would mean that the sensor is restarted for every measure, but then you again just get the bad "warm-up" data, so if the imprecise data that is retrieved from the sensor that has just been started is a problem, then the longer interval times shouldn't exist anyway (or the the whole PR needs to be a lot more complicated). Because having the sensor running all the time for measurements every 10 minutes seems like a waste of energy.

This PR is open quite for a long time already and I think small improvements can be discussed much more thoroughly and implemented way better after more people had the chance to test it.

But again I'm open for suggestions for making the code better and if anything (like an inaccurate interval time) is a deal breaker, I will of course fix it!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, what I was thinking was actually something different. I agree that trying to predict measurement time would be a bad idea (too complex), and having cases where runtime degrades to continuous is not ideal (for longer periods).

What I meant was storing the start time of the measurement, and then starting the next measurement after start+period, regardless of whether the last measurement succeeded or failed. So if 1 minute measurement is set (and all measurements succeed), after 1 hour 60 measurements will have been made. Currently, if each measurement took 20s, only 45 measurements would be made in an hour. This does cause any measurement period <= MAX_INTERVAL (30s) to degrade to continuous when heart rate cannot be detected.

What do you think of this?

If you're hitting memory limits, shrinking the heap wouldn't be unreasonable BTW, there is currently very little static memory free and the heap will probably be shrunk by 1K or 512B soon anyway

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please start a measurement every x seconds. That is what users will expect to happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented as discussed

ThirtySeconds,
OneMinute,
FiveMinutes,
TenMinutes,
ThirtyMinutes,
};
Copy link
Contributor

@minacode minacode Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is the wrong data structure for what you want to do. Maybe just use a const array of integers representing seconds? Or at least set the enums values to that?
In another file there is a function that maps the enums values to times. That all seems too complicated to me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried an array of integers before, but there was a problem when storing something longer than a uint8_t in the settings file. Therefore I opted to do it like this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, but I guess it's alright then 😄


Settings(Pinetime::Controllers::FS& fs);

Settings(const Settings&) = delete;
Expand Down Expand Up @@ -283,10 +294,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 = 0x0007;
static constexpr uint32_t settingsVersion = 0x0008;
Copy link

@fallaciousreasoning fallaciousreasoning Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be bumped to 0x0009? Looks like 0x0008 is in main already:
https://github.com/InfiniTimeOrg/InfiniTime/blob/main/src/components/settings/Settings.h#L304

Suggested change
static constexpr uint32_t settingsVersion = 0x0008;
static constexpr uint32_t settingsVersion = 0x0009;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you are right. Thank you!
I will keep this suggested change open until the PR is otherwise ready to merge.

I bumped the settings version a few times already and I don't want to do it every time there is the settings file in main anymore.


struct SettingsData {
uint32_t version = settingsVersion;
Expand All @@ -308,6 +330,8 @@ namespace Pinetime {
uint16_t shakeWakeThreshold = 150;

Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium;

HeartRateBackgroundMeasurementInterval heartRateBackgroundMeasurementInterval = HeartRateBackgroundMeasurementInterval::Off;
FintasticMan marked this conversation as resolved.
Show resolved Hide resolved
};

SettingsData settings;
Expand Down
4 changes: 4 additions & 0 deletions src/displayapp/DisplayApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -506,6 +507,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>(this, settingsController);
break;
Expand Down
1 change: 1 addition & 0 deletions src/displayapp/apps/Apps.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace Pinetime {
SettingWatchFace,
SettingTimeFormat,
SettingWeatherFormat,
SettingHeartRate,
SettingDisplay,
SettingWakeUp,
SettingSteps,
Expand Down
75 changes: 75 additions & 0 deletions src/displayapp/screens/settings/SettingHeartRate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include "displayapp/screens/settings/SettingHeartRate.h"
#include <lvgl/lvgl.h>
#include "displayapp/screens/Styles.h"
#include "displayapp/screens/Screen.h"
#include "displayapp/screens/Symbols.h"
#include <array>
#include <algorithm>

using namespace Pinetime::Applications::Screens;

namespace {
void event_handler(lv_obj_t* obj, lv_event_t event) {
auto* screen = static_cast<SettingHeartRate*>(obj->user_data);
screen->UpdateSelected(obj, event);
}
}

constexpr std::array<Option, 8> 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);
}
}
}
}
47 changes: 47 additions & 0 deletions src/displayapp/screens/settings/SettingHeartRate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <cstdint>
#include <lvgl/lvgl.h>

#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<Option, 8> 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()];
};
}
}
}
5 changes: 3 additions & 2 deletions src/displayapp/screens/settings/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
Loading
Loading