Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -403,6 +403,7 @@ list(APPEND SOURCE_FILES
displayapp/widgets/PageIndicator.cpp
displayapp/widgets/DotIndicator.cpp
displayapp/widgets/StatusIcons.cpp
displayapp/screens/HeartRateZone.cpp

## Settings
displayapp/screens/settings/QuickSettings.cpp
Expand Down
109 changes: 109 additions & 0 deletions src/components/heartrate/HeartRateController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,123 @@

using namespace Pinetime::Controllers;

HeartRateZoneSettings::HeartRateZoneSettings() {
version = 0;
adjustMsDelay = 300000;
exerciseMsTarget = 1800000;
age = 25;
maxHeartRate = maxHeartRateEstimate(age);
percentTarget = {50, 60, 70, 80, 90};
bpmTarget = bpmZones(percentTarget, maxHeartRate);
allowCalibration = true;
};

HeartRateController::HeartRateController(Pinetime::Controllers::FS& fs) : fs {fs} {
LoadSettingsFromFile();
};

void HeartRateController::Update(HeartRateController::States newState, uint8_t heartRate) {
this->state = newState;
if (this->heartRate != heartRate) {
uint32_t ts = xTaskGetTickCount();
uint32_t z;
zone = 0;
auto adjustMax = pdMS_TO_TICKS(this->zSettings.adjustMsDelay);
for (z = this->zSettings.bpmTarget.size() - 1; z < this->zSettings.bpmTarget.size(); --z) {
if (this->heartRate >= this->zSettings.bpmTarget[z]) {
uint32_t dt = ts - lastActiveTime;
currentActivity.zoneTime[z] += dt;
zone = z + 1;
// don't make increases unless this is consistantly higher than normal (zone 5 is max)
if (this->zSettings.allowCalibration && zone >= 5 && dt > adjustMax) {
this->zSettings.maxHeartRate = this->zSettings.maxHeartRate >= this->heartRate ? this->zSettings.maxHeartRate : this->heartRate;
this->zSettings.bpmTarget = bpmZones(this->zSettings.percentTarget, this->zSettings.maxHeartRate);
}
break;
}
}
lastActiveTime = ts;

this->heartRate = heartRate;

service->OnNewHeartRateValue(heartRate);
}
}

void HeartRateController::AdvanceDay() {
HeartRateZones<uint16_t> convertedActivity {};
auto ticksPerUnit = pdMS_TO_TICKS(2000); // 2s

auto totalTime = currentActivity.totalTime();
for (uint32_t i = 0; i < convertedActivity.zoneTime.size(); i++) {
convertedActivity.zoneTime[i] = (uint16_t)Utility::RoundedDiv((uint32_t)totalTime, (uint32_t)ticksPerUnit);
}

activity[0] = convertedActivity;
activity++;
}

void HeartRateController::SaveSettingsToFile() const {
lfs_dir systemDir;
if (fs.DirOpen("/.system", &systemDir) != LFS_ERR_OK) {
fs.DirCreate("/.system");
}
fs.DirClose(&systemDir);
lfs_file_t heartRateZoneFile;
if (fs.FileOpen(&heartRateZoneFile, "/.system/hrzs.dat", LFS_O_WRONLY | LFS_O_CREAT) != LFS_ERR_OK) {
NRF_LOG_WARNING("[HeartRateController] Failed to open heart rate zone settings file for saving");
return;
}

fs.FileWrite(&heartRateZoneFile, reinterpret_cast<const uint8_t*>(&(this->zSettings)), sizeof(this->zSettings));
fs.FileClose(&heartRateZoneFile);
NRF_LOG_INFO("[HeartRateController] Saved heart rate zone settings with format version %u to file", this->zSettings.version);

lfs_file_t zoneDataFile;
if (fs.FileOpen(&zoneDataFile, "/.system/hrz.dat", LFS_O_WRONLY | LFS_O_CREAT) != LFS_ERR_OK) {
NRF_LOG_WARNING("[HeartRateController] Failed to open heart rate zone data file for saving");
return;
}

// Version 1: Raw Data Dump to File
const int version = 0x1;
fs.FileWrite(&zoneDataFile, reinterpret_cast<const uint8_t*>(&version), sizeof(int));
// Raw Data
fs.FileWrite(&zoneDataFile, reinterpret_cast<const uint8_t*>(&activity), sizeof(activity));
fs.FileClose(&zoneDataFile);
NRF_LOG_INFO("[HeartRateController] Saved heart rate zone data with format version %u to file", this->zSettings.version);
}

void HeartRateController::LoadSettingsFromFile() {
HeartRateZoneSettings HRZSettings;
lfs_file_t settingsFile;

if (fs.FileOpen(&settingsFile, "/.system/hrzs.dat", LFS_O_RDONLY) != LFS_ERR_OK) {

} else {
fs.FileRead(&settingsFile, reinterpret_cast<uint8_t*>(&HRZSettings), sizeof(HRZSettings));
fs.FileClose(&settingsFile);
if (HRZSettings.version == zSettings.version) {
zSettings = HRZSettings;
}
}

lfs_file_t dataFile;

if (fs.FileOpen(&dataFile, "/.system/hrz.dat", LFS_O_RDONLY) != LFS_ERR_OK) {

} else {
int version = 0;
decltype(activity) HRZdata;
fs.FileRead(&dataFile, reinterpret_cast<uint8_t*>(&version), sizeof(version));
fs.FileRead(&dataFile, reinterpret_cast<uint8_t*>(&HRZdata), sizeof(HRZdata));
fs.FileClose(&dataFile);
if (version == 0x1) {
activity = HRZdata;
}
}
}

void HeartRateController::Enable() {
if (task != nullptr) {
state = States::NotEnoughData;
Expand Down
75 changes: 74 additions & 1 deletion src/components/heartrate/HeartRateController.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

#include <cstdint>
#include <components/ble/HeartRateService.h>
#include "utility/CircularBuffer.h"
#include "utility/Math.h"
#include "components/fs/FS.h"
#include <array>

namespace Pinetime {
namespace Applications {
Expand All @@ -13,11 +17,55 @@ namespace Pinetime {
}

namespace Controllers {
template <typename T>
struct HeartRateZones {
// 1440 minutes in a day (11 bits), 86400 seconds (17 bits)
std::array<T, 5> zoneTime = {};

T totalTime() const {
return zoneTime[0] + zoneTime[1] + zoneTime[2] + zoneTime[3] + zoneTime[4];
}
};

constexpr uint8_t maxHeartRateEstimate(uint8_t age) {
return 220 - age;
};

constexpr int16_t fixed_rounding(int16_t value, int16_t divisor) {
// true evil: we use >>'s signed behavior to propagate the leading 1 across all bits, eg: we have 0xffff or 0x0000
int16_t signed_value = value >> 15u;
int16_t half_divisor = (divisor / 2);
return (value + half_divisor - (divisor & signed_value)) / divisor; // we replace the * by "1" with an &
}

template <size_t N>
constexpr std::array<uint8_t, N> bpmZones(std::array<uint8_t, N>& percentages, uint8_t maxBeatsPerMinute) {
std::array<uint8_t, N> targets {};
const uint16_t bpm = maxBeatsPerMinute;
for (uint32_t i = 0; i < N; i++) {
targets[i++] = Utility::RoundedDiv((uint16_t)(percentages[i] * bpm), (uint16_t)100);
}
return targets;
};

struct HeartRateZoneSettings {
uint32_t version = 0;
uint32_t adjustMsDelay = 300000; // 5 minutes
uint32_t exerciseMsTarget = 1800000; // hour 3600'000
uint8_t age = 25;
uint8_t maxHeartRate = 195;
std::array<uint8_t, 5> percentTarget = {50, 60, 70, 80, 90};
std::array<uint8_t, 5> bpmTarget = {98, 117, 137, 156, 176};
bool allowCalibration = true;

HeartRateZoneSettings();
};

class HeartRateController {
public:
enum class States : uint8_t { Stopped, NotEnoughData, NoTouch, Running };

HeartRateController() = default;
HeartRateController(Pinetime::Controllers::FS& fs);
void Enable();
void Disable();
void Update(States newState, uint8_t heartRate);
Expand All @@ -32,13 +80,38 @@ namespace Pinetime {
return heartRate;
}

uint8_t Zone() const {
return zone;
}

HeartRateZones<uint32_t> Activity() const {
return currentActivity;
}

HeartRateZoneSettings hrzSettings() const {
return zSettings;
}

void SetService(Pinetime::Controllers::HeartRateService* service);

void AdvanceDay();
void SaveSettingsToFile() const;
void LoadSettingsFromFile();

private:
Applications::HeartRateTask* task = nullptr;
States state = States::Stopped;
uint8_t heartRate = 0;
uint8_t restingHeartRate = 0;
Pinetime::Controllers::HeartRateService* service = nullptr;
Pinetime::Controllers::FS& fs;

HeartRateZoneSettings zSettings = {};
uint32_t lastActiveTime = 0;
// Heart Rate Zone Storage
HeartRateZones<uint32_t> currentActivity = {};
Utility::CircularBuffer<HeartRateZones<uint16_t>, 31> activity = {};
uint8_t zone = 0;
};
}
}
1 change: 1 addition & 0 deletions src/displayapp/DisplayApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "displayapp/screens/PassKey.h"
#include "displayapp/screens/Error.h"
#include "displayapp/screens/Calculator.h"
#include "displayapp/screens/HeartRateZone.h"

#include "drivers/Cst816s.h"
#include "drivers/St7789.h"
Expand Down
1 change: 1 addition & 0 deletions src/displayapp/InfiniTimeTheme.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Colors {
static constexpr lv_color_t green = LV_COLOR_MAKE(0x0, 0xb0, 0x0);
static constexpr lv_color_t blue = LV_COLOR_MAKE(0x0, 0x50, 0xff);
static constexpr lv_color_t lightGray = LV_COLOR_MAKE(0xb0, 0xb0, 0xb0);
static constexpr lv_color_t heartRed = LV_COLOR_MAKE(0xce,0x1b,0x1b);

static constexpr lv_color_t bg = LV_COLOR_MAKE(0x5d, 0x69, 0x7e);
static constexpr lv_color_t bgAlt = LV_COLOR_MAKE(0x38, 0x38, 0x38);
Expand Down
1 change: 1 addition & 0 deletions src/displayapp/UserApps.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "displayapp/screens/Timer.h"
#include "displayapp/screens/Twos.h"
#include "displayapp/screens/Tile.h"
#include "displayapp/screens/HeartRateZone.h"
#include "displayapp/screens/ApplicationList.h"
#include "displayapp/screens/WatchFaceDigital.h"
#include "displayapp/screens/WatchFaceAnalog.h"
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 @@ -22,6 +22,7 @@ namespace Pinetime {
Paddle,
Twos,
HeartRate,
HeartRateZone,
Navigation,
StopWatch,
Metronome,
Expand Down
1 change: 1 addition & 0 deletions src/displayapp/apps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ else ()
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Timer")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Steps")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRate")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRateZone")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Music")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paint")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle")
Expand Down
112 changes: 112 additions & 0 deletions src/displayapp/screens/HeartRateZone.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include "displayapp/screens/HeartRateZone.h"
#include <lvgl/lvgl.h>
#include <components/heartrate/HeartRateController.h>

#include "displayapp/DisplayApp.h"
#include "displayapp/InfiniTimeTheme.h"

using namespace Pinetime::Applications::Screens;

HeartRateZone::HeartRateZone(Controllers::HeartRateController& heartRateController, System::SystemTask& systemTask)
: heartRateController {heartRateController}, wakeLock(systemTask) {
auto activity = heartRateController.Activity();
auto settings = heartRateController.hrzSettings();
uint32_t total = 0;

auto hundreths_of_hour = pdMS_TO_TICKS(10 * 60 * 60);
auto exercise_target = pdMS_TO_TICKS(settings.exerciseMsTarget);
uint32_t offset = 25 * (zone_bar.size() + 2);

lv_obj_t* screen = lv_scr_act();

title = lv_label_create(screen, nullptr);
lv_label_set_text_static(title, "BPM Breakdown");
lv_obj_align(title, screen, LV_ALIGN_IN_TOP_MID, 0, 20);

total_bar = lv_bar_create(screen, nullptr);
lv_obj_set_style_local_bg_opa(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_0);
lv_obj_set_style_local_line_color(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt);
lv_obj_set_style_local_border_width(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, 2);
lv_obj_set_style_local_radius(total_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, 0);
lv_obj_set_style_local_line_color(total_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_NAVY);

lv_obj_align(total_bar, screen, LV_ALIGN_IN_TOP_MID, 0, offset - (zone_bar.size() * 25));

total_label = lv_label_create(total_bar, nullptr);
lv_obj_align(total_label, total_bar, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_local_text_color(total_label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN);

for (uint8_t i = 0; i < zone_bar.size(); i++) {
zone_bar[i] = lv_bar_create(screen, nullptr);

total += activity.zoneTime[i];

lv_obj_set_style_local_bg_opa(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_0);
lv_obj_set_style_local_line_color(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt);
lv_obj_set_style_local_border_width(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, 2);
lv_obj_set_style_local_radius(zone_bar[i], LV_BAR_PART_BG, LV_STATE_DEFAULT, 0);

lv_obj_set_size(zone_bar[i], 240, 20);
lv_obj_align(zone_bar[i], screen, LV_ALIGN_IN_TOP_MID, 0, offset - i * 25);

label_time[i] = lv_label_create(zone_bar[i], nullptr);
lv_obj_align(label_time[i], zone_bar[i], LV_ALIGN_CENTER, 0, 0);

lv_obj_set_style_local_text_color(label_time[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN);
}

lv_label_set_text_static(label_time[0], "Warm Up");
lv_label_set_text_static(label_time[1], "Recovery");
lv_label_set_text_static(label_time[2], "Aerobic");
lv_label_set_text_static(label_time[3], "Threshold");
lv_label_set_text_static(label_time[4], "Anaerobic");
lv_label_set_text_static(total_label, "Goal");

lv_obj_set_style_local_line_color(zone_bar[0], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::blue);
lv_obj_set_style_local_line_color(zone_bar[1], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::green);
lv_obj_set_style_local_line_color(zone_bar[2], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::orange);
lv_obj_set_style_local_line_color(zone_bar[3], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::deepOrange);
lv_obj_set_style_local_line_color(zone_bar[4], LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::heartRed);

auto bar_limit = total / hundreths_of_hour;

for (uint8_t i = 0; i < zone_bar.size(); i++) {
uint32_t percent = activity.zoneTime[i] / hundreths_of_hour;
lv_bar_set_range(zone_bar[i], 0, bar_limit);
lv_bar_set_value(zone_bar[i], percent, LV_ANIM_OFF);
}

lv_bar_set_range(total_bar, 0, exercise_target);
lv_bar_set_value(total_bar, total > exercise_target ? exercise_target : total, LV_ANIM_OFF);

taskRefresh = lv_task_create(RefreshTaskCallback, 5000, LV_TASK_PRIO_MID, this);
}

HeartRateZone::~HeartRateZone() {
lv_task_del(taskRefresh);
lv_obj_clean(lv_scr_act());
}

void HeartRateZone::Refresh() {
auto activity = heartRateController.Activity();
auto settings = heartRateController.hrzSettings();
uint32_t total = 0;

auto hundreths_of_hour = pdMS_TO_TICKS(10 * 60 * 60);
auto exercise_target = pdMS_TO_TICKS(settings.exerciseMsTarget);

for (uint8_t i = 0; i < zone_bar.size(); i++) {
total += activity.zoneTime[i];
}

auto bar_limit = total / hundreths_of_hour;

for (uint8_t i = 0; i < zone_bar.size(); i++) {
uint32_t percent = activity.zoneTime[i] / hundreths_of_hour;
lv_bar_set_range(zone_bar[i], 0, bar_limit);
lv_bar_set_value(zone_bar[i], percent, LV_ANIM_OFF);
}

lv_bar_set_range(total_bar, 0, exercise_target);
lv_bar_set_value(total_bar, total > exercise_target ? exercise_target : total, LV_ANIM_OFF);
}
Loading
Loading