Skip to content

Commit 53957c2

Browse files
committed
Categorizes time spent in five heart rate ranges. These are calculated relative to a set max heart rate estimated by age corresponding to 50% (zone 1), 60% (zone 2), 70% (zone 3), 80% (zone 4) and 90% (zone 5) and above respectively.
1 parent f2814dd commit 53957c2

File tree

14 files changed

+361
-7
lines changed

14 files changed

+361
-7
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ list(APPEND SOURCE_FILES
403403
displayapp/widgets/PageIndicator.cpp
404404
displayapp/widgets/DotIndicator.cpp
405405
displayapp/widgets/StatusIcons.cpp
406+
displayapp/screens/HeartRateZone.cpp
406407

407408
## Settings
408409
displayapp/screens/settings/QuickSettings.cpp

src/components/heartrate/HeartRateController.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,123 @@
44

55
using namespace Pinetime::Controllers;
66

7+
HeartRateZoneSettings::HeartRateZoneSettings() {
8+
version = 0;
9+
adjustMsDelay = 300000;
10+
exerciseMsTarget = 1800000;
11+
age = 25;
12+
maxHeartRate = maxHeartRateEstimate(age);
13+
percentTarget = {50, 60, 70, 80, 90};
14+
bpmTarget = bpmZones(percentTarget, maxHeartRate);
15+
allowCalibration = true;
16+
};
17+
18+
HeartRateController::HeartRateController(Pinetime::Controllers::FS& fs) : fs {fs} {
19+
LoadSettingsFromFile();
20+
};
21+
722
void HeartRateController::Update(HeartRateController::States newState, uint8_t heartRate) {
823
this->state = newState;
924
if (this->heartRate != heartRate) {
25+
uint32_t ts = xTaskGetTickCount();
26+
uint32_t z;
27+
zone = 0;
28+
auto adjustMax = pdMS_TO_TICKS(this->zSettings.adjustMsDelay);
29+
for (z = this->zSettings.bpmTarget.size() - 1; z < this->zSettings.bpmTarget.size(); --z) {
30+
if (this->heartRate >= this->zSettings.bpmTarget[z]) {
31+
uint32_t dt = ts - lastActiveTime;
32+
currentActivity.zoneTime[z] += dt;
33+
zone = z + 1;
34+
// don't make increases unless this is consistantly higher than normal (zone 5 is max)
35+
if (this->zSettings.allowCalibration && zone >= 5 && dt > adjustMax) {
36+
this->zSettings.maxHeartRate = this->zSettings.maxHeartRate >= this->heartRate ? this->zSettings.maxHeartRate : this->heartRate;
37+
this->zSettings.bpmTarget = bpmZones(this->zSettings.percentTarget, this->zSettings.maxHeartRate);
38+
}
39+
break;
40+
}
41+
}
42+
lastActiveTime = ts;
43+
1044
this->heartRate = heartRate;
45+
1146
service->OnNewHeartRateValue(heartRate);
1247
}
1348
}
1449

50+
void HeartRateController::AdvanceDay() {
51+
HeartRateZones<uint16_t> convertedActivity {};
52+
auto ticksPerUnit = pdMS_TO_TICKS(2000); // 2s
53+
54+
auto totalTime = currentActivity.totalTime();
55+
for (uint32_t i = 0; i < convertedActivity.zoneTime.size(); i++) {
56+
convertedActivity.zoneTime[i] = (uint16_t)Utility::RoundedDiv((uint32_t)totalTime, (uint32_t)ticksPerUnit);
57+
}
58+
59+
activity[0] = convertedActivity;
60+
activity++;
61+
}
62+
63+
void HeartRateController::SaveSettingsToFile() const {
64+
lfs_dir systemDir;
65+
if (fs.DirOpen("/.system", &systemDir) != LFS_ERR_OK) {
66+
fs.DirCreate("/.system");
67+
}
68+
fs.DirClose(&systemDir);
69+
lfs_file_t heartRateZoneFile;
70+
if (fs.FileOpen(&heartRateZoneFile, "/.system/hrzs.dat", LFS_O_WRONLY | LFS_O_CREAT) != LFS_ERR_OK) {
71+
NRF_LOG_WARNING("[HeartRateController] Failed to open heart rate zone settings file for saving");
72+
return;
73+
}
74+
75+
fs.FileWrite(&heartRateZoneFile, reinterpret_cast<const uint8_t*>(&(this->zSettings)), sizeof(this->zSettings));
76+
fs.FileClose(&heartRateZoneFile);
77+
NRF_LOG_INFO("[HeartRateController] Saved heart rate zone settings with format version %u to file", this->zSettings.version);
78+
79+
lfs_file_t zoneDataFile;
80+
if (fs.FileOpen(&zoneDataFile, "/.system/hrz.dat", LFS_O_WRONLY | LFS_O_CREAT) != LFS_ERR_OK) {
81+
NRF_LOG_WARNING("[HeartRateController] Failed to open heart rate zone data file for saving");
82+
return;
83+
}
84+
85+
// Version 1: Raw Data Dump to File
86+
const int version = 0x1;
87+
fs.FileWrite(&zoneDataFile, reinterpret_cast<const uint8_t*>(&version), sizeof(int));
88+
// Raw Data
89+
fs.FileWrite(&zoneDataFile, reinterpret_cast<const uint8_t*>(&activity), sizeof(activity));
90+
fs.FileClose(&zoneDataFile);
91+
NRF_LOG_INFO("[HeartRateController] Saved heart rate zone data with format version %u to file", this->zSettings.version);
92+
}
93+
94+
void HeartRateController::LoadSettingsFromFile() {
95+
HeartRateZoneSettings HRZSettings;
96+
lfs_file_t settingsFile;
97+
98+
if (fs.FileOpen(&settingsFile, "/.system/hrzs.dat", LFS_O_RDONLY) != LFS_ERR_OK) {
99+
100+
} else {
101+
fs.FileRead(&settingsFile, reinterpret_cast<uint8_t*>(&HRZSettings), sizeof(HRZSettings));
102+
fs.FileClose(&settingsFile);
103+
if (HRZSettings.version == zSettings.version) {
104+
zSettings = HRZSettings;
105+
}
106+
}
107+
108+
lfs_file_t dataFile;
109+
110+
if (fs.FileOpen(&dataFile, "/.system/hrz.dat", LFS_O_RDONLY) != LFS_ERR_OK) {
111+
112+
} else {
113+
int version = 0;
114+
decltype(activity) HRZdata;
115+
fs.FileRead(&dataFile, reinterpret_cast<uint8_t*>(&version), sizeof(version));
116+
fs.FileRead(&dataFile, reinterpret_cast<uint8_t*>(&HRZdata), sizeof(HRZdata));
117+
fs.FileClose(&dataFile);
118+
if (version == 0x1) {
119+
activity = HRZdata;
120+
}
121+
}
122+
}
123+
15124
void HeartRateController::Enable() {
16125
if (task != nullptr) {
17126
state = States::NotEnoughData;

src/components/heartrate/HeartRateController.h

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
#include <cstdint>
44
#include <components/ble/HeartRateService.h>
5+
#include "utility/CircularBuffer.h"
6+
#include "utility/Math.h"
7+
#include "components/fs/FS.h"
8+
#include <array>
59

610
namespace Pinetime {
711
namespace Applications {
@@ -13,11 +17,55 @@ namespace Pinetime {
1317
}
1418

1519
namespace Controllers {
20+
template <typename T>
21+
struct HeartRateZones {
22+
// 1440 minutes in a day (11 bits), 86400 seconds (17 bits)
23+
std::array<T, 5> zoneTime = {};
24+
25+
T totalTime() const {
26+
return zoneTime[0] + zoneTime[1] + zoneTime[2] + zoneTime[3] + zoneTime[4];
27+
}
28+
};
29+
30+
constexpr uint8_t maxHeartRateEstimate(uint8_t age) {
31+
return 220 - age;
32+
};
33+
34+
constexpr int16_t fixed_rounding(int16_t value, int16_t divisor) {
35+
// true evil: we use >>'s signed behavior to propagate the leading 1 across all bits, eg: we have 0xffff or 0x0000
36+
int16_t signed_value = value >> 15u;
37+
int16_t half_divisor = (divisor / 2);
38+
return (value + half_divisor - (divisor & signed_value)) / divisor; // we replace the * by "1" with an &
39+
}
40+
41+
template <size_t N>
42+
constexpr std::array<uint8_t, N> bpmZones(std::array<uint8_t, N>& percentages, uint8_t maxBeatsPerMinute) {
43+
std::array<uint8_t, N> targets {};
44+
const uint16_t bpm = maxBeatsPerMinute;
45+
for (uint32_t i = 0; i < N; i++) {
46+
targets[i++] = Utility::RoundedDiv((uint16_t)(percentages[i] * bpm), (uint16_t)100);
47+
}
48+
return targets;
49+
};
50+
51+
struct HeartRateZoneSettings {
52+
uint32_t version = 0;
53+
uint32_t adjustMsDelay = 300000; // 5 minutes
54+
uint32_t exerciseMsTarget = 1800000; // hour 3600'000
55+
uint8_t age = 25;
56+
uint8_t maxHeartRate = 195;
57+
std::array<uint8_t, 5> percentTarget = {50, 60, 70, 80, 90};
58+
std::array<uint8_t, 5> bpmTarget = {98, 117, 137, 156, 176};
59+
bool allowCalibration = true;
60+
61+
HeartRateZoneSettings();
62+
};
63+
1664
class HeartRateController {
1765
public:
1866
enum class States : uint8_t { Stopped, NotEnoughData, NoTouch, Running };
1967

20-
HeartRateController() = default;
68+
HeartRateController(Pinetime::Controllers::FS& fs);
2169
void Enable();
2270
void Disable();
2371
void Update(States newState, uint8_t heartRate);
@@ -32,13 +80,38 @@ namespace Pinetime {
3280
return heartRate;
3381
}
3482

83+
uint8_t Zone() const {
84+
return zone;
85+
}
86+
87+
HeartRateZones<uint32_t> Activity() const {
88+
return currentActivity;
89+
}
90+
91+
HeartRateZoneSettings hrzSettings() const {
92+
return zSettings;
93+
}
94+
3595
void SetService(Pinetime::Controllers::HeartRateService* service);
3696

97+
void AdvanceDay();
98+
void SaveSettingsToFile() const;
99+
void LoadSettingsFromFile();
100+
37101
private:
38102
Applications::HeartRateTask* task = nullptr;
39103
States state = States::Stopped;
40104
uint8_t heartRate = 0;
105+
uint8_t restingHeartRate = 0;
41106
Pinetime::Controllers::HeartRateService* service = nullptr;
107+
Pinetime::Controllers::FS& fs;
108+
109+
HeartRateZoneSettings zSettings = {};
110+
uint32_t lastActiveTime = 0;
111+
// Heart Rate Zone Storage
112+
HeartRateZones<uint32_t> currentActivity = {};
113+
Utility::CircularBuffer<HeartRateZones<uint16_t>, 31> activity = {};
114+
uint8_t zone = 0;
42115
};
43116
}
44117
}

src/displayapp/DisplayApp.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "displayapp/screens/PassKey.h"
3232
#include "displayapp/screens/Error.h"
3333
#include "displayapp/screens/Calculator.h"
34+
#include "displayapp/screens/HeartRateZone.h"
3435

3536
#include "drivers/Cst816s.h"
3637
#include "drivers/St7789.h"

src/displayapp/InfiniTimeTheme.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Colors {
88
static constexpr lv_color_t green = LV_COLOR_MAKE(0x0, 0xb0, 0x0);
99
static constexpr lv_color_t blue = LV_COLOR_MAKE(0x0, 0x50, 0xff);
1010
static constexpr lv_color_t lightGray = LV_COLOR_MAKE(0xb0, 0xb0, 0xb0);
11+
static constexpr lv_color_t heartRed = LV_COLOR_MAKE(0xce,0x1b,0x1b);
1112

1213
static constexpr lv_color_t bg = LV_COLOR_MAKE(0x5d, 0x69, 0x7e);
1314
static constexpr lv_color_t bgAlt = LV_COLOR_MAKE(0x38, 0x38, 0x38);

src/displayapp/UserApps.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "displayapp/screens/Timer.h"
88
#include "displayapp/screens/Twos.h"
99
#include "displayapp/screens/Tile.h"
10+
#include "displayapp/screens/HeartRateZone.h"
1011
#include "displayapp/screens/ApplicationList.h"
1112
#include "displayapp/screens/WatchFaceDigital.h"
1213
#include "displayapp/screens/WatchFaceAnalog.h"

src/displayapp/apps/Apps.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace Pinetime {
2222
Paddle,
2323
Twos,
2424
HeartRate,
25+
HeartRateZone,
2526
Navigation,
2627
StopWatch,
2728
Metronome,

src/displayapp/apps/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ else ()
66
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Timer")
77
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Steps")
88
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRate")
9+
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRateZone")
910
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Music")
1011
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paint")
1112
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle")
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#include "displayapp/screens/HeartRateZone.h"
2+
#include <lvgl/lvgl.h>
3+
#include <components/heartrate/HeartRateController.h>
4+
5+
#include "displayapp/DisplayApp.h"
6+
#include "displayapp/InfiniTimeTheme.h"
7+
8+
using namespace Pinetime::Applications::Screens;
9+
10+
HeartRateZone::HeartRateZone(Controllers::HeartRateController& heartRateController, System::SystemTask& systemTask)
11+
: heartRateController {heartRateController}, wakeLock(systemTask) {
12+
auto activity = heartRateController.Activity();
13+
auto settings = heartRateController.hrzSettings();
14+
uint32_t total = 0;
15+
16+
auto hundreths_of_hour = pdMS_TO_TICKS(10 * 60 * 60);
17+
auto exercise_target = pdMS_TO_TICKS(settings.exerciseMsTarget);
18+
uint32_t offset = 25 * (zone_bar.size() + 2);
19+
20+
lv_obj_t* screen = lv_scr_act();
21+
22+
title = lv_label_create(screen, nullptr);
23+
lv_label_set_text_static(title, "BPM Breakdown");
24+
lv_obj_align(title, screen, LV_ALIGN_IN_TOP_MID, 0, 20);
25+
26+
total_bar = lv_bar_create(screen, nullptr);
27+
lv_obj_set_style_local_bg_opa(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_0);
28+
lv_obj_set_style_local_line_color(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt);
29+
lv_obj_set_style_local_border_width(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, 2);
30+
lv_obj_set_style_local_radius(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, 0);
31+
lv_obj_set_style_local_line_color(total_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_NAVY);
32+
33+
lv_obj_align(total_bar, screen, LV_ALIGN_IN_TOP_MID, 0, offset - (zone_bar.size() * 25));
34+
35+
total_label = lv_label_create(total_bar, nullptr);
36+
lv_obj_align(total_label, total_bar, LV_ALIGN_CENTER, 0, 0);
37+
lv_obj_set_style_local_text_color(total_label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN);
38+
39+
for (uint8_t i = 0; i < zone_bar.size(); i++) {
40+
zone_bar[i] = lv_bar_create(screen, nullptr);
41+
42+
total += activity.zoneTime[i];
43+
44+
lv_obj_set_style_local_bg_opa(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_0);
45+
lv_obj_set_style_local_line_color(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt);
46+
lv_obj_set_style_local_border_width(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, 2);
47+
lv_obj_set_style_local_radius(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, 0);
48+
49+
lv_obj_set_size(zone_bar[i], 240, 20);
50+
lv_obj_align(zone_bar[i], screen, LV_ALIGN_IN_TOP_MID, 0, offset - i * 25);
51+
52+
label_time[i] = lv_label_create(zone_bar[i], nullptr);
53+
lv_obj_align(label_time[i], zone_bar[i], LV_ALIGN_CENTER, 0, 0);
54+
55+
lv_obj_set_style_local_text_color(label_time[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN);
56+
}
57+
58+
lv_label_set_text_static(label_time[0], "Warm Up");
59+
lv_label_set_text_static(label_time[1], "Recovery");
60+
lv_label_set_text_static(label_time[2], "Aerobic");
61+
lv_label_set_text_static(label_time[3], "Threshold");
62+
lv_label_set_text_static(label_time[4], "Anaerobic");
63+
lv_label_set_text_static(total_label, "Goal");
64+
65+
lv_obj_set_style_local_line_color(zone_bar[0], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::blue);
66+
lv_obj_set_style_local_line_color(zone_bar[1], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::green);
67+
lv_obj_set_style_local_line_color(zone_bar[2], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::orange);
68+
lv_obj_set_style_local_line_color(zone_bar[3], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::deepOrange);
69+
lv_obj_set_style_local_line_color(zone_bar[4], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::heartRed);
70+
71+
auto bar_limit = total / hundreths_of_hour;
72+
73+
for (uint8_t i = 0; i < zone_bar.size(); i++) {
74+
uint32_t percent = activity.zoneTime[i] / hundreths_of_hour;
75+
lv_bar_set_range(zone_bar[i], 0, bar_limit);
76+
lv_bar_set_value(zone_bar[i], percent, LV_ANIM_OFF);
77+
}
78+
79+
lv_bar_set_range(total_bar, 0, exercise_target);
80+
lv_bar_set_value(total_bar, total > exercise_target ? exercise_target : total, LV_ANIM_OFF);
81+
82+
taskRefresh = lv_task_create(RefreshTaskCallback, 5000, LV_TASK_PRIO_MID, this);
83+
}
84+
85+
HeartRateZone::~HeartRateZone() {
86+
lv_task_del(taskRefresh);
87+
lv_obj_clean(lv_scr_act());
88+
}
89+
90+
void HeartRateZone::Refresh() {
91+
auto activity = heartRateController.Activity();
92+
auto settings = heartRateController.hrzSettings();
93+
uint32_t total = 0;
94+
95+
auto hundreths_of_hour = pdMS_TO_TICKS(10 * 60 * 60);
96+
auto exercise_target = pdMS_TO_TICKS(settings.exerciseMsTarget);
97+
98+
for (uint8_t i = 0; i < zone_bar.size(); i++) {
99+
total += activity.zoneTime[i];
100+
}
101+
102+
auto bar_limit = total / hundreths_of_hour;
103+
104+
for (uint8_t i = 0; i < zone_bar.size(); i++) {
105+
uint32_t percent = activity.zoneTime[i] / hundreths_of_hour;
106+
lv_bar_set_range(zone_bar[i], 0, bar_limit);
107+
lv_bar_set_value(zone_bar[i], percent, LV_ANIM_OFF);
108+
}
109+
110+
lv_bar_set_range(total_bar, 0, exercise_target);
111+
lv_bar_set_value(total_bar, total > exercise_target ? exercise_target : total, LV_ANIM_OFF);
112+
}

0 commit comments

Comments
 (0)