Merge pull request #3373 from halemmerich/runplus

runplus - Add lock, gps, pulse indicators to karvonen screen
master
thyttan 2024-05-02 21:26:44 +02:00 committed by GitHub
commit e20b815da5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 349 additions and 224 deletions

View File

@ -275,12 +275,6 @@ module.exports = {
"no-unused-vars"
]
},
"runplus/app.js": {
"hash": "fabec449552d17ceb5d510aa058e420757f3caba11999efbe6ebf2ac1a81eb32",
"rules": [
"no-unused-vars"
]
},
"powermanager/boot.js": {
"hash": "662d9d29a80a4f2c2855097b4073a099604f4f6d444c13a33304346c788cc5cb",
"rules": [
@ -741,12 +735,6 @@ module.exports = {
"no-undef"
]
},
"runplus/karvonen.js": {
"hash": "3011bbc5afc3e17bb873f4544d680acc8765105dd825417eadb01047ecadbcb7",
"rules": [
"no-undef"
]
},
"rescalc/app.js": {
"hash": "925f00a439817fadf92f4e7a7fcd509eb9d9c7e1e4309e315ea92a6881e18b4b",
"rules": [

View File

@ -24,3 +24,4 @@ Write to correct settings file, fixing settings not working.
0.21: Rebase on "Run" app ver. 0.16.
0.22: Ensure screen redraws after "Resume run?" menu (#3044)
0.23: Minor code improvements
0.24: Add indicators for lock,gps and pulse to karvonen screen

View File

@ -25,6 +25,7 @@ so if you have no GPS lock you just need to wait.
Unlock the screen and navigate between displays by swiping left or right.
The upper number is the limit before next heart rate zone. The lower number is the limit before previous heart rate zone. The number in the middle is the heart rate. The Z1 to Z5 number indicates the heart rate zone where you are. The circle provides a quick visualisation of the hr zone in which you are.
Indicator icons for lock, heartrate and location are updated on arrival off internal system events. The heart icon shows if the exstats module decided that the heart rate value is usable. The location icon shows if there was an GPS event with a position fix in it.
## Recording Tracks

View File

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

View File

@ -1,19 +1,19 @@
(function karvonen(hrmSettings, exsHrmStats) {
//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.
//Other methods are even more approximative.
let wu = require("widget_utils");
wu.hide();
let R = Bangle.appRect;
//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.
//Other methods are even more approximative.
let wu = require("widget_utils");
let R;
g.reset().clearRect(R).setFontAlign(0,0,0);
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);
}
let linePoints = { //Not lists of points, but used to update points in the drawArrows function.
const ICON_LOCK = atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g==");
const ICON_HEART = atob("Dw4BODj4+fv3///////f/z/+P/g/4D+APgA4ACAA");
const ICON_LOCATION = atob("CxABP4/7x/B+D8H8e/5/x/D+D4HwHAEAIA==");
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);
}
let linePoints = { //Not lists of points, but used to update points in the drawArrows function.
x: [
175/40,
2,
@ -24,190 +24,324 @@
175/52,
175/110,
175/122,
],
};
function drawArrows() {
g.setColor(g.theme.fg);
// 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[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0]));
// 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[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)
// HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr.
//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.
let minhr = hrmSettings.min;
let maxhr = hrmSettings.max;
function calculatehrr(minhr, maxhr) {
return maxhr - minhr;}
//test input for hrr (it works).
let hrr = calculatehrr(minhr, maxhr);
console.log(hrr);
//Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input.
let hr = exsHrmStats.getValue();
// 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 = hrr * 0.6 + minhr;
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.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.setFont("Vector",50);
g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4);
function drawArrows() {
g.setColor(g.theme.fg);
// 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[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0]));
// 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[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)
// HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr.
//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.
let minhr;
let maxhr;
let hrr;
//Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input.
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;
function calculatehrr(minhr, maxhr) {
hrr = maxhr - minhr;
minzone2 = hrr * 0.6 + minhr;
maxzone2 = hrr * 0.7 + minhr;
maxzone3 = hrr * 0.8 + minhr;
maxzone4 = hrr * 0.9 + minhr;
maxzone5 = hrr * 0.99 + minhr;
}
// HR data: large, readable, in the middle of the screen
function drawHR() {
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.setColor(g.theme.fg);
g.setFont("Vector",50);
g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4);
drawArrows();
}
function drawWaitHR() {
g.setColor(g.theme.fg);
// Waiting for HRM
g.setFontAlign(0,0,0);
g.setFont("Vector",50);
g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4);
// Waiting for current Zone
g.setFont("Vector",24);
g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2);
// waiting for upper and lower limit of current zone
g.setFont("Vector",20);
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2));
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7));
drawArrows();
}
//These functions call arcs to show different HR zones.
//To shorten the code, I'll reference some letiables and reuse them.
let centreX;
let centreY;
let minRadius;
let maxRadius;
//draw background image (dithered green zones)(I should draw different zones in different dithered colors)
const HRzones= require("graphics_utils");
let minRadiusz;
let startAngle;
let endAngle;
function drawBgArc() {
g.setColor(g.theme.dark==false?0xC618:"#002200");
HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle);
}
const zones = require("graphics_utils");
//####### A function to simplify a bit the code ######
function simplify (sA, eA, Z, currentZone, lastZone) {
let startAngle = zones.degreesToRadians(sA);
let endAngle = zones.degreesToRadians(eA);
if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle);
else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle);
g.setFont("Vector",24);
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.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2);
}
function zoning (max, min) { // draw values of upper and lower limit of current zone
g.setFont("Vector",20);
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/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10);
g.setFontAlign(0,0,0);
g.drawString(max, Rdiv(x,2), Rdiv(y,9/2));
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
g.setColor(g.theme.bg);
HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle);
}
function drawZone(zone) {
clearCurrentZone();
if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);}
if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);}
if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);}
if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);}
if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);}
if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);}
if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);}
if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);}
if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);}
if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);}
if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);}
if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);}
if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);}
}
function drawWaitHR() {
g.setColor(g.theme.fg);
// Waiting for HRM
g.setFontAlign(0,0,0);
g.setFont("Vector",50);
g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4);
function drawIndicators(){
drawLockIndicator();
drawPulseIndicator();
drawGpsIndicator();
}
// Waiting for current Zone
g.setFont("Vector",24);
g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2);
function drawZoneAlert() {
const HRzonemax = require("graphics_utils");
drawIndicators();
g.clearRect(R);
drawIndicators();
let minRadius = 0.40 * R.h;
let startAngle1 = HRzonemax.degreesToRadians(-90);
let endAngle1 = HRzonemax.degreesToRadians(270);
g.setFont("Vector",38);g.setColor("#ff0000");
HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1);
g.setFontAlign(0,0).drawString("ALERT", centreX, centreY);
}
// waiting for upper and lower limit of current zone
g.setFont("Vector",20);
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2));
g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7));
function drawWaitUI(){
g.clearRect(R);
drawIndicators();
drawBgArc();
drawWaitHR();
drawArrows();
}
function drawBase() {
g.clearRect(R);
drawIndicators();
drawBgArc();
}
function drawZoneUI(full){
if (full) {
drawBase();
}
//These functions call arcs to show different HR zones.
//To shorten the code, I'll reference some letiables and reuse them.
let centreX = R.x + 0.5 * R.w;
let centreY = R.y + 0.5 * R.h;
let minRadius = 0.38 * R.h;
let maxRadius = 0.50 * R.h;
//draw background image (dithered green zones)(I should draw different zones in different dithered colors)
const HRzones= require("graphics_utils");
let minRadiusz = 0.44 * R.h;
let startAngle = HRzones.degreesToRadians(-88.5);
let endAngle = HRzones.degreesToRadians(268.5);
drawHR();
drawZones();
}
function drawBgArc() {
g.setColor(g.theme.dark==false?0xC618:"#002200");
HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle);
//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;
function drawZones() {
if ((hr < maxhr - 2) && subZoneLast==13) { drawZoneUI(true); } // Reset UI when coming down from zone alert.
if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; drawZone(subZoneLast);}} // Z1
else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; drawZone(subZoneLast);}} // Z2a
else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; drawZone(subZoneLast);}} // Z2b
else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; drawZone(subZoneLast);}} // Z2c
else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; drawZone(subZoneLast);}} // Z3a
else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; drawZone(subZoneLast);}} // Z3b
else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; drawZone(subZoneLast);}} // Z3c
else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; drawZone(subZoneLast);}} // Z4a
else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; drawZone(subZoneLast);}} // Z4b
else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; drawZone(subZoneLast);}} // Z4c
else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; drawZone(subZoneLast);}} // Z5a
else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; drawZone(subZoneLast);}} // Z5b
else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; drawZone(subZoneLast);}} // Z5c
else if (hr >= maxhr - 2) {subZoneLast=13; drawZoneAlert();} // Alert
}
let karvonenInterval;
let hrmstat;
function drawLockIndicator() {
if (Bangle.isLocked())
g.setColor(g.theme.fg).drawImage(ICON_LOCK, 6, 8);
else
g.setColor(g.theme.bg).drawImage(ICON_LOCK, 6, 8);
}
let gpsTimeout;
let lastGps;
function drawGpsIndicator(e) {
if (e && e.fix){
if (gpsTimeout)
clearTimeout(gpsTimeout);
g.setColor(g.theme.fg).drawImage(ICON_LOCATION, 8, R.y2 - 23);
lastGps = 0;
gpsTimeout = setTimeout(()=>{
g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_LOCATION, 8, R.y2 - 23);
lastGps = 1;
gpsTimeout = setTimeout(()=>{
g.setColor(g.theme.bg).drawImage(ICON_LOCATION, 8, R.y2 - 23);
lastGps = 2;
}, 3900);
}, 1100);
} else if (lastGps !== undefined){
switch (lastGps) {
case 0: g.setColor(g.theme.fg).drawImage(ICON_LOCATION, 8, R.y2 - 23); break;
case 1: g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_LOCATION, 8, R.y2 - 23); break;
case 2: g.setColor(g.theme.bg).drawImage(ICON_LOCATION, 8, R.y2 - 23); break;
}
}
const zones = require("graphics_utils");
//####### A function to simplify a bit the code ######
function simplify (sA, eA, Z, currentZone, lastZone) {
let startAngle = zones.degreesToRadians(sA);
let endAngle = zones.degreesToRadians(eA);
if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle);
else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle);
g.setFont("Vector",24);
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.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2);
}
let pulseTimeout;
function drawPulseIndicator() {
if (hr){
if (pulseTimeout)
clearTimeout(pulseTimeout);
g.setColor(g.theme.fg).drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21);
pulseTimeout = setTimeout(()=>{
g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21);
pulseTimeout = setTimeout(()=>{
g.setColor(g.theme.bg).drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21);
}, 3900);
}, 1100);
}
}
function init(hrmSettings, exsHrmStats) {
R = Bangle.appRect;
hrmstat = exsHrmStats;
hr = hrmstat.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);
Bangle.on("lock", drawLockIndicator);
Bangle.on("HRM", drawPulseIndicator);
Bangle.on("HRM", updateUI);
Bangle.on("GPS", drawGpsIndicator);
setTimeout(updateUI,0,true);
}
let waitTimeout;
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;
}
function zoning (max, min) { // draw values of upper and lower limit of current zone
g.setFont("Vector",20);
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/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10);
g.setFontAlign(0,0,0);
g.drawString(max, Rdiv(x,2), Rdiv(y,9/2));
g.drawString(min, Rdiv(x,2), Rdiv(y,9/7));
if (waitTimeout) clearTimeout(waitTimeout);
waitTimeout = setTimeout(() => {drawWaitUI(); waitTimeout = undefined;}, 2000);
hr = hrmstat.getValue();
//if (h!=0) hr = h;
if (hrLast != hr || resetHrLast){
if (hr && hrLast != hr){
drawZoneUI(true);
} else if (!hr)
drawWaitUI();
}
function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color
g.setColor(g.theme.bg);
HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle);
}
function getZone(zone) {
drawBgArc();
clearCurrentZone();
if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);}
if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);}
if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);}
if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);}
if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);}
if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);}
if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);}
if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);}
if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);}
if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);}
if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);}
if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);}
if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);}
}
hrLast = hr;
//g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements.
}
function getZoneAlert() {
const HRzonemax = require("graphics_utils");
let minRadius = 0.40 * R.h;
let startAngle1 = HRzonemax.degreesToRadians(-90);
let endAngle1 = HRzonemax.degreesToRadians(270);
g.setFont("Vector",38);g.setColor("#ff0000");
HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1);
g.drawString("ALERT", 26,66);
}
//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;
function drawZones() {
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
else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a
else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b
else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c
else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a
else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b
else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c
else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a
else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b
else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c
else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a
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 >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert
}
function initDraw() {
drawArrows();
if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();}
//drawZones();
}
function stop(){
if (karvonenInterval) clearInterval(karvonenInterval);
karvonenInterval = undefined;
if (pulseTimeout) clearTimeout(pulseTimeout);
pulseTimeout = undefined;
if (gpsTimeout) clearTimeout(gpsTimeout);
gpsTimeout = undefined;
if (waitTimeout) clearTimeout(waitTimeout);
waitTimeout = undefined;
Bangle.removeListener("lock", drawLockIndicator);
Bangle.removeListener("GPS", drawGpsIndicator);
Bangle.removeListener("HRM", drawPulseIndicator);
Bangle.removeListener("HRM", updateUI);
lastGps = undefined;
wu.show();
}
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.
hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not.
hr = exsHrmStats.getValue();
//if (h!=0) hr = h;
if (hr!=hrLast) {
drawHR();
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.
}
initDraw();
// check for updates every second.
karvonenInterval = setInterval(function() {
if (!isMenuDisplayed && karvonenActive) updateUI();
}, 1000);
return karvonenInterval;
})
exports.start = start;
exports.stop = stop;

BIN
apps/runplus/karvonen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,12 +1,15 @@
{
"id": "runplus",
"name": "Run+",
"version": "0.23",
"version": "0.24",
"description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",
"supports": ["BANGLEJS2"],
"screenshots": [{"url": "screenshot.png"}],
"screenshots": [
{"url": "screenshot.png"},
{"url": "karvonen.png"}
],
"readme": "README.md",
"storage": [
{"name": "runplus.app.js", "url": "app.js"},