runplus - Refactor karvonen to be a library

master
Martin Boonk 2024-04-21 16:48:56 +02:00
parent fc373c7ab5
commit 656eeda4d1
2 changed files with 241 additions and 209 deletions

View File

@ -1,10 +1,7 @@
// Use widget utils to show/hide widgets
let wu = require("widget_utils");
let runInterval; let runInterval;
let karvonenActive = false; let karvonenActive = false;
// Run interface wrapped in a function // Run interface wrapped in a function
let ExStats = require("exstats"); const ExStats = require("exstats");
let B2 = process.env.HWVERSION===2; let B2 = process.env.HWVERSION===2;
let Layout = require("Layout"); let Layout = require("Layout");
let locale = require("locale"); let locale = require("locale");
@ -13,11 +10,10 @@ let fontValue = B2 ? "6x15:2" : "6x8:3";
let headingCol = "#888"; let headingCol = "#888";
let fixCount = 0; let fixCount = 0;
let isMenuDisplayed = false; let isMenuDisplayed = false;
const wu = require("widget_utils");
g.reset().clear(); g.reset().clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets();
wu.show();
// --------------------------- // ---------------------------
let settings = Object.assign({ let settings = Object.assign({
@ -139,7 +135,7 @@ lc.push({ type:"h", filly:1, c:[
// Now calculate the layout // Now calculate the layout
let layout = new Layout( { let layout = new Layout( {
type:"v", c: lc type:"v", c: lc
},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); },{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {run();} onStartStop();}), id:"button"}]});
delete lc; delete lc;
setStatus(exs.state.active); setStatus(exs.state.active);
layout.render(); layout.render();
@ -169,9 +165,13 @@ Bangle.on("GPS", function(fix) {
} }
}); });
// run() function used to switch between traditional run UI and karvonen UI // run() function used to start updating traditional run ui
function run() { function run() {
require("runplus_karvonen").stop();
karvonenActive = false;
wu.show(); wu.show();
Bangle.drawWidgets();
g.reset().clearRect(Bangle.appRect);
layout.lazy = false; layout.lazy = false;
layout.render(); layout.render();
layout.lazy = true; layout.lazy = true;
@ -179,7 +179,7 @@ function run() {
if (!runInterval){ if (!runInterval){
runInterval = setInterval(function() { runInterval = setInterval(function() {
layout.clock.label = locale.time(new Date(),1); layout.clock.label = locale.time(new Date(),1);
if (!isMenuDisplayed && !karvonenActive) layout.render(); if (!isMenuDisplayed) layout.render();
}, 1000); }, 1000);
} }
} }
@ -189,25 +189,23 @@ run();
// Karvonen // Karvonen
/////////////////////////////////////////////// ///////////////////////////////////////////////
function stopRunUI() { function karvonen(){
// stop updating and drawing the traditional run app UI // stop updating and drawing the traditional run app UI
clearInterval(runInterval); if (runInterval) clearInterval(runInterval);
runInterval = undefined; runInterval = undefined;
g.reset().clearRect(Bangle.appRect);
require("runplus_karvonen").start(settings.HRM, exs.stats.bpm);
karvonenActive = true; karvonenActive = true;
} }
function stopKarvonenUI() {
g.reset().clear();
clearInterval(karvonenInterval);
karvonenInterval = undefined;
karvonenActive = false;
}
let karvonenInterval;
// Define the function to go back and forth between the different UI's // Define the function to go back and forth between the different UI's
function swipeHandler(LR,_) { function swipeHandler(LR,_) {
if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} if (!isMenuDisplayed){
if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} if (LR==-1 && karvonenActive)
run();
if (LR==1 && !karvonenActive)
karvonen();
}
} }
// Listen for swipes with the swipeHandler // Listen for swipes with the swipeHandler
Bangle.on("swipe", swipeHandler); Bangle.on("swipe", swipeHandler);

View File

@ -1,19 +1,15 @@
(function karvonen(hrmSettings, exsHrmStats) { //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+
//This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ //The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab.
//The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. //Other methods are even more approximative.
//Other methods are even more approximative. let wu = require("widget_utils");
let wu = require("widget_utils"); let R;
wu.hide();
let R = Bangle.appRect;
const x = "x"; const y = "y";
g.reset().clearRect(R).setFontAlign(0,0,0); function Rdiv(axis, divisor) { // Used when placing things on the screen
const x = "x"; const y = "y";
function Rdiv(axis, divisor) { // Used when placing things on the screen
return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor); return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor);
} }
let linePoints = { //Not lists of points, but used to update points in the drawArrows function.
let linePoints = { //Not lists of points, but used to update points in the drawArrows function.
x: [ x: [
175/40, 175/40,
2, 2,
@ -24,11 +20,11 @@
175/52, 175/52,
175/110, 175/110,
175/122, 175/122,
], ]
};
};
function drawArrows() { function drawArrows() {
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
// Upper // Upper
g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1])); g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1]));
@ -36,43 +32,45 @@
// Lower // Lower
g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3])); g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]));
g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2])); g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2]));
} }
//To calculate Heart rate zones, we need to know the heart rate reserve (HRR) //To calculate Heart rate zones, we need to know the heart rate reserve (HRR)
// HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr. // HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr.
//get the hrr (heart rate reserve). //get the hrr (heart rate reserve).
// I put random data here, but this has to come as a menu in the settings section so that users can change it. // I put random data here, but this has to come as a menu in the settings section so that users can change it.
let minhr = hrmSettings.min; let minhr;
let maxhr = hrmSettings.max; let maxhr;
let hrr;
function calculatehrr(minhr, maxhr) { //Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input.
return maxhr - minhr;} let hr;
// These letiables display next and previous HR zone.
//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method
//60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack
let minzone2;
let maxzone2;
let maxzone3;
let maxzone4;
let maxzone5;
//test input for hrr (it works). function calculatehrr(minhr, maxhr) {
let hrr = calculatehrr(minhr, maxhr); hrr = maxhr - minhr;
console.log(hrr); minzone2 = hrr * 0.6 + minhr;
maxzone2 = hrr * 0.7 + minhr;
//Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input. maxzone3 = hrr * 0.8 + minhr;
let hr = exsHrmStats.getValue(); maxzone4 = hrr * 0.9 + minhr;
// These letiables display next and previous HR zone. maxzone5 = hrr * 0.99 + minhr;
//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method }
//60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack // HR data: large, readable, in the middle of the screen
let minzone2 = hrr * 0.6 + minhr; function drawHR() {
let maxzone2 = hrr * 0.7 + minhr;
let maxzone3 = hrr * 0.8 + minhr;
let maxzone4 = hrr * 0.9 + minhr;
let maxzone5 = hrr * 0.99 + minhr;
// HR data: large, readable, in the middle of the screen
function drawHR() {
g.setFontAlign(-1,0,0); g.setFontAlign(-1,0,0);
g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.setFont("Vector",50); g.setFont("Vector",50);
g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4);
} }
function drawWaitHR() { function drawWaitHR() {
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
// Waiting for HRM // Waiting for HRM
g.setFontAlign(0,0,0); g.setFontAlign(0,0,0);
@ -87,30 +85,30 @@
g.setFont("Vector",20); g.setFont("Vector",20);
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2));
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7));
} }
//These functions call arcs to show different HR zones. //These functions call arcs to show different HR zones.
//To shorten the code, I'll reference some letiables and reuse them. //To shorten the code, I'll reference some letiables and reuse them.
let centreX = R.x + 0.5 * R.w; let centreX;
let centreY = R.y + 0.5 * R.h; let centreY;
let minRadius = 0.38 * R.h; let minRadius;
let maxRadius = 0.50 * R.h; let maxRadius;
//draw background image (dithered green zones)(I should draw different zones in different dithered colors) //draw background image (dithered green zones)(I should draw different zones in different dithered colors)
const HRzones= require("graphics_utils"); const HRzones= require("graphics_utils");
let minRadiusz = 0.44 * R.h; let minRadiusz;
let startAngle = HRzones.degreesToRadians(-88.5); let startAngle;
let endAngle = HRzones.degreesToRadians(268.5); let endAngle;
function drawBgArc() { function drawBgArc() {
g.setColor(g.theme.dark==false?0xC618:"#002200"); g.setColor(g.theme.dark==false?0xC618:"#002200");
HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle);
} }
const zones = require("graphics_utils"); const zones = require("graphics_utils");
//####### A function to simplify a bit the code ###### //####### A function to simplify a bit the code ######
function simplify (sA, eA, Z, currentZone, lastZone) { function simplify (sA, eA, Z, currentZone, lastZone) {
let startAngle = zones.degreesToRadians(sA); let startAngle = zones.degreesToRadians(sA);
let endAngle = zones.degreesToRadians(eA); let endAngle = zones.degreesToRadians(eA);
if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle);
@ -119,9 +117,9 @@
g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12);
g.setFontAlign(0,0,0); g.setFontAlign(0,0,0);
g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2);
} }
function zoning (max, min) { // draw values of upper and lower limit of current zone function zoning (max, min) { // draw values of upper and lower limit of current zone
g.setFont("Vector",20); g.setFont("Vector",20);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10);
@ -129,14 +127,14 @@
g.setFontAlign(0,0,0); g.setFontAlign(0,0,0);
g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); g.drawString(max, Rdiv(x,2), Rdiv(y,9/2));
g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); g.drawString(min, Rdiv(x,2), Rdiv(y,9/7));
} }
function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color
g.setColor(g.theme.bg); g.setColor(g.theme.bg);
HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle);
} }
function getZone(zone) { function getZone(zone) {
drawBgArc(); drawBgArc();
clearCurrentZone(); clearCurrentZone();
if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);}
@ -154,19 +152,19 @@
if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);}
} }
function getZoneAlert() { function getZoneAlert() {
const HRzonemax = require("graphics_utils"); const HRzonemax = require("graphics_utils");
let minRadius = 0.40 * R.h; let minRadius = 0.40 * R.h;
let startAngle1 = HRzonemax.degreesToRadians(-90); let startAngle1 = HRzonemax.degreesToRadians(-90);
let endAngle1 = HRzonemax.degreesToRadians(270); let endAngle1 = HRzonemax.degreesToRadians(270);
g.setFont("Vector",38);g.setColor("#ff0000"); g.setFont("Vector",38);g.setColor("#ff0000");
HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1);
g.drawString("ALERT", 26,66); g.setFontAlign(0,0).drawString("ALERT", centreX, centreY);
} }
//Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones.
let subZoneLast; let subZoneLast;
function drawZones() { function drawZones() {
if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert.
if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1
else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a
@ -182,32 +180,68 @@
else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b
else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c
else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert
} }
function initDraw() { let karvonenInterval;
drawArrows(); let hrmstat;
if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();}
//drawZones();
}
let hrLast; function init(hrmSettings, exsHrmStats) {
//h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. R = Bangle.appRect;
function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. hrmstat = exsHrmStats;
hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. hr = hrmstat.getValue();
hr = exsHrmStats.getValue(); minhr = hrmSettings.min;
maxhr = hrmSettings.max;
calculatehrr(minhr, maxhr);
centreX = R.x + 0.5 * R.w;
centreY = R.y + 0.5 * R.h;
minRadius = 0.38 * R.h;
maxRadius = 0.50 * R.h;
minRadiusz = 0.44 * R.h;
startAngle = HRzones.degreesToRadians(-88.5);
endAngle = HRzones.degreesToRadians(268.5);
}
function start(hrmSettings, exsHrmStats) {
wu.hide();
init(hrmSettings, exsHrmStats);
g.reset().clearRect(R).setFontAlign(0,0,0);
//draw every second
setTimeout(updateUI,0,true);
karvonenInterval = setInterval(function() {
updateUI(false);
}, 1000);
}
let hrLast;
//h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below.
function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR.
if (resetHrLast){
hrLast = 0; // Handles correct updating on init depending on if we've got HRM readings yet or not.
subZoneLast = undefined;
}
hr = hrmstat.getValue();
//if (h!=0) hr = h; //if (h!=0) hr = h;
if (hr!=hrLast) { drawArrows();
if (hr && hr!=hrLast) {
drawHR(); drawHR();
drawZones(); drawZones();
} //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. } else {
drawBgArc();
drawWaitHR();
} }
initDraw(); //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements.
}
// check for updates every second. function stop(){
karvonenInterval = setInterval(function() { if (karvonenInterval) clearInterval(karvonenInterval);
if (!isMenuDisplayed && karvonenActive) updateUI(); karvonenInterval = undefined;
}, 1000); wu.show();
}
return karvonenInterval; exports.start = start;
}) exports.stop = stop;