1
0
Fork 0

Merge pull request #1253 from adamschmalhofer/wohrm-bjs2

Workout HRM 0.09
master
Gordon Williams 2022-01-10 08:37:13 +00:00 committed by GitHub
commit e28117a2c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 446 additions and 331 deletions

View File

@ -1717,17 +1717,18 @@
{ {
"id": "wohrm", "id": "wohrm",
"name": "Workout HRM", "name": "Workout HRM",
"version": "0.08", "version": "0.09",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",
"tags": "hrm,workout", "tags": "hrm,workout",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"allow_emulator": true, "allow_emulator": true,
"screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}], "screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}],
"storage": [ "storage": [
{"name":"wohrm.app.js","url":"app.js"}, {"name":"wohrm.app.js","url":"app.js"},
{"name":"wohrm.settings.js","url":"settings.js"},
{"name":"wohrm.img","url":"app-icon.js","evaluate":true} {"name":"wohrm.img","url":"app-icon.js","evaluate":true}
] ]
}, },

View File

@ -5,4 +5,7 @@
0.05: Improved buzz timing and rendering 0.05: Improved buzz timing and rendering
0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed
0.07: Home button fixed and README added 0.07: Home button fixed and README added
0.08: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) 0.08: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799)
0.09: Ported to Bangle.js2
Home returns to clock, instead of menu
Add settings

View File

@ -8,6 +8,9 @@ and will notify you with a buzz whenever your heart rate falls below or jumps ab
[Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html). [Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html).
## Setting the limits ## Setting the limits
Use the settings menu to set the limits. On the Bangle.js1 these can in addition be set with the buttons:
For setting the lower limit press button 4 (left part of the watch's touch screen). For setting the lower limit press button 4 (left part of the watch's touch screen).
Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch. Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch.
@ -22,7 +25,7 @@ the received value: For 85% and above the bars are green, between 84% and 50% th
and below 50% they turn red. and below 50% they turn red.
## Closing the app ## Closing the app
Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher. Pressing middle button will switch off the HRM of the watch and return you to the launcher.
# HRM usage # HRM usage
The HRM is switched on when the app is started. It stays switch on while the app is running, even The HRM is switched on when the app is started. It stays switch on while the app is running, even

View File

@ -1,327 +1,400 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
const Setter = { const Setter = {
NONE: "none", NONE: "none",
UPPER: 'upper', UPPER: 'upper',
LOWER: 'lower' LOWER: 'lower'
}; };
const SETTINGS_FILE = "wohrm.setting.json";
const shortBuzzTimeInMs = 80; var settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {
const longBuzzTimeInMs = 400; upperLimit: 130,
lowerLimit: 100
let upperLimit = 130; };
let upperLimitChanged = true;
const shortBuzzTimeInMs = 80;
let lowerLimit = 100; const longBuzzTimeInMs = 400;
let lowerLimitChanged = true;
let upperLimitChanged = true;
let limitSetter = Setter.NONE; let lowerLimitChanged = true;
let currentHeartRate = 0; let limitSetter = Setter.NONE;
let hrConfidence = -1;
let hrChanged = true; let currentHeartRate = 0;
let confidenceChanged = true; let hrConfidence = -1;
let hrChanged = true;
let setterHighlightTimeout; let confidenceChanged = true;
function renderUpperLimitBackground() { let setterHighlightTimeout;
g.setColor(1,0,0);
g.fillRect(125,40, 210, 70); const isB1 = process.env.HWVERSION==1;
g.fillRect(180,70, 210, 200); const upperLshape = isB1 ? {
right: 125,
//Round top left corner left: 210,
g.fillEllipse(115,40,135,70); bottom: 40,
top: 210,
//Round top right corner rectWidth: 30,
g.setColor(0,0,0); cornerRoundness: 5,
g.fillRect(205,40, 210, 45); orientation: -1,
g.setColor(1,0,0); color: '#f00'
g.fillEllipse(190,40,210,50); } : {
right: Bangle.appRect.x2-100,
//Round inner corner left: Bangle.appRect.x2,
g.fillRect(174,71, 179, 76); bottom: 24,
g.setColor(0,0,0); top: Bangle.appRect.y2,
g.fillEllipse(160,71,179,82); rectWidth: 26,
cornerRoundness: 4,
//Round bottom orientation: -1, // rotated 180°
g.setColor(1,0,0); color: '#f00'
g.fillEllipse(180,190, 210, 210); };
}
const lowerLshape = {
function renderLowerLimitBackground() { left: isB1 ? 10 : Bangle.appRect.x,
g.setColor(0,0,1); right: 100,
g.fillRect(10, 180, 100, 210); bottom: upperLshape.top,
g.fillRect(10, 50, 40, 180); top: upperLshape.bottom,
rectWidth: upperLshape.rectWidth,
//Rounded top cornerRoundness: upperLshape.cornerRoundness,
g.setColor(0,0,1); orientation: 1,
g.fillEllipse(10,40, 40, 60); color: '#00f'
};
//Round bottom right corner
g.setColor(0,0,1); const centerBar = {
g.fillEllipse(90,180,110,210); minY: (upperLshape.bottom + upperLshape.top - upperLshape.rectWidth)/2,
maxY: (upperLshape.bottom + upperLshape.top + upperLshape.rectWidth)/2,
//Round inner corner confidenceWidth: isB1 ? 10 : 8,
g.setColor(0,0,1); minX: isB1 ? 55 : upperLshape.rectWidth + 14,
g.fillRect(40,175,45,180); maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14
g.setColor(0,0,0); };
g.fillEllipse(41,170,60,179);
const fontSizes = isB1 ? {
//Round bottom left corner limits: 13,
g.setColor(0,0,0); heartRate: 24
g.fillRect(10,205, 15, 210); } : {
g.setColor(0,0,1); limits: 12,
g.fillEllipse(10,200,30,210); heartRate: 20
} };
function drawTrainingHeartRate() { function fillEllipse(x, y, x2, y2) {
//Only redraw if the display is on g.fillEllipse(Math.min(x, x2),
if (Bangle.isLCDOn()) { Math.min(y, y2),
renderUpperLimit(); Math.max(x, x2),
Math.max(y, y2));
renderCurrentHeartRate(); }
renderLowerLimit(); /**
* @param p.left: the X coordinate of the left side of the L in its orientation
renderConfidenceBars(); * @param p.right: the X coordinate of the right side of the L in its orientation
} * @param p.top: the Y coordinate of the top side of the L in its orientation
* @param p.bottom: the Y coordinate of the bottom side of the L in its orientation
buzz(); * @param p.strokeWidth: how thick we draw the letter.
} * @param p.cornerRoundness: how much the corners should be rounded
* @param p.orientation: 1 == turned 0°; -1 == turned 180°
function renderUpperLimit() { * @param p.color: the color to draw the shape
if(!upperLimitChanged) { return; } */
function renderLshape(p) {
g.setColor(1,0,0); g.setColor(p.color);
g.fillRect(125,40, 210, 70);
g.fillRect(p.right, p.bottom, p.left, p.bottom-p.orientation*p.rectWidth);
if(limitSetter === Setter.UPPER){ g.fillRect(p.left+p.orientation*p.rectWidth,
g.setColor(255,255, 0); p.bottom-p.orientation*p.rectWidth,
} else { p.left,
g.setColor(255,255,255); p.top+p.orientation*p.cornerRoundness*2);
}
g.setFontVector(13); //Round end of small line
g.drawString("Upper: " + upperLimit, 125, 50); fillEllipse(p.right+p.orientation*p.cornerRoundness*2,
p.bottom,
upperLimitChanged = false; p.right-p.orientation*p.cornerRoundness*2,
} p.bottom-p.orientation*p.rectWidth);
function renderCurrentHeartRate() { //Round outer corner
if(!hrChanged) { return; } g.setColor(g.theme.bg);
g.fillRect(p.left+p.orientation*p.cornerRoundness,
g.setColor(255,255,255); p.bottom,
g.fillRect(55, 110, 165, 150); p.left,
p.bottom-p.orientation*p.cornerRoundness);
g.setColor(0,0,0); g.setColor(p.color);
g.setFontVector(24); fillEllipse(p.left+p.orientation*p.cornerRoundness*4,
g.setFontAlign(1, -1, 0); p.bottom,
g.drawString(currentHeartRate, 130, 117); p.left,
p.bottom-p.orientation*p.cornerRoundness*2);
//Reset alignment to defaults
g.setFontAlign(-1, -1, 0); //Round inner corner
g.fillRect(p.left+p.orientation*(p.rectWidth+p.cornerRoundness+1),
hrChanged = false; p.bottom-p.orientation*(p.rectWidth+1),
} p.left+p.orientation*(p.rectWidth+1),
p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness-1));
function renderLowerLimit() { g.setColor(g.theme.bg);
if(!lowerLimitChanged) { return; } fillEllipse(p.left+p.orientation*(p.rectWidth+p.cornerRoundness*4),
p.bottom-p.orientation*(p.rectWidth+1),
g.setColor(0,0,1); p.left+p.orientation*(p.rectWidth+1),
g.fillRect(10, 180, 100, 210); p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness*3-1));
if(limitSetter === Setter.LOWER){ //Round end of long line
g.setColor(255,255, 0); g.setColor(p.color);
} else { fillEllipse(p.left+p.orientation*p.rectWidth,
g.setColor(255,255,255); p.top+p.orientation*p.cornerRoundness*4,
} p.left,
g.setFontVector(13); p.top);
g.drawString("Lower: " + lowerLimit, 20,190); }
lowerLimitChanged = false; function drawTrainingHeartRate() {
} //Only redraw if the display is on
if (Bangle.isLCDOn()) {
function renderConfidenceBars(){ renderUpperLimit();
if(!confidenceChanged) { return; }
renderCurrentHeartRate();
if(hrConfidence >= 85){
g.setColor(0, 255, 0); renderLowerLimit();
} else if (hrConfidence >= 50) {
g.setColor(255, 255, 0); renderConfidenceBars();
} else if(hrConfidence >= 0){ }
g.setColor(255, 0, 0);
} else { buzz();
g.setColor(255, 255, 255); }
}
function renderUpperLimit() {
g.fillRect(45, 110, 55, 150); if(!upperLimitChanged) { return; }
g.fillRect(165, 110, 175, 150);
renderLshape(upperLshape);
confidenceChanged = false;
} if(limitSetter === Setter.UPPER){
g.setColor(1,1,0);
function renderPlusMinusIcons() { } else {
if (limitSetter === Setter.NONE) { g.setColor(g.theme.fg);
g.setColor(0, 0, 0); }
} else { g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0);
g.setColor(1, 1, 1); g.drawString("Upper: " + settings.upperLimit,
} upperLshape.right,
upperLshape.bottom+upperLshape.rectWidth/2);
g.setFontVector(14);
upperLimitChanged = false;
//+ for Btn1 }
g.drawString("+", 222, 50);
function renderCurrentHeartRate() {
//- for Btn3 if(!hrChanged) { return; }
g.drawString("-", 222,165);
g.setColor(g.theme.fg);
return; g.fillRect(centerBar.minX, centerBar.minY,
} centerBar.maxX, centerBar.maxY);
function renderHomeIcon() { g.setColor(g.theme.bg);
//Home for Btn2 g.setFontVector(fontSizes.heartRate);
g.setColor(1, 1, 1); g.setFontAlign(1, 0, 0);
g.drawLine(220, 118, 227, 110); g.drawString(currentHeartRate,
g.drawLine(227, 110, 234, 118); Math.max(upperLshape.right+upperLshape.cornerRoundness,
lowerLshape.right-lowerLshape.cornerRoundness),
g.drawPoly([222,117,222,125,232,125,232,117], false); (centerBar.minY+centerBar.maxY)/2);
g.drawRect(226,120,229,125);
} //Reset alignment to defaults
g.setFontAlign(-1, -1, 0);
function buzz() {
// Do not buzz if not confident hrChanged = false;
if(hrConfidence < 85) { return; } }
if(currentHeartRate > upperLimit) function renderLowerLimit() {
{ if(!lowerLimitChanged) { return; }
Bangle.buzz(shortBuzzTimeInMs);
setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); renderLshape(lowerLshape);
}
if(limitSetter === Setter.LOWER){
if(currentHeartRate < lowerLimit) g.setColor(1,1,0);
{ } else {
Bangle.buzz(longBuzzTimeInMs); g.setColor(g.theme.fg);
} }
} g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0);
g.drawString("Lower: " + settings.lowerLimit,
function onHrm(hrm){ lowerLshape.left + lowerLshape.rectWidth/2,
if(currentHeartRate !== hrm.bpm){ lowerLshape.bottom - lowerLshape.rectWidth/2);
currentHeartRate = hrm.bpm;
hrChanged = true; lowerLimitChanged = false;
} }
if(hrConfidence !== hrm.confidence) { function renderConfidenceBars(){
hrConfidence = hrm.confidence; if(!confidenceChanged) { return; }
confidenceChanged = true;
} if(hrConfidence >= 85){
} g.setColor(0, 1, 0);
} else if (hrConfidence >= 50) {
function setLimitSetterToLower() { g.setColor(1, 1, 0);
resetHighlightTimeout(); } else if(hrConfidence >= 0){
g.setColor(1, 0, 0);
limitSetter = Setter.LOWER; } else {
g.setColor(g.theme.fg);
upperLimitChanged = true; }
lowerLimitChanged = true;
g.fillRect(centerBar.minX-centerBar.confidenceWidth, centerBar.minY, centerBar.minX, centerBar.maxY);
renderUpperLimit(); g.fillRect(centerBar.maxX, centerBar.minY, centerBar.maxX+centerBar.confidenceWidth, centerBar.maxY);
renderLowerLimit();
renderPlusMinusIcons(); confidenceChanged = false;
} }
function setLimitSetterToUpper() { function renderPlusMinusIcons() {
resetHighlightTimeout(); if (limitSetter === Setter.NONE) {
g.setColor(g.theme.bg);
limitSetter = Setter.UPPER; } else {
g.setColor(g.theme.fg);
upperLimitChanged = true; }
lowerLimitChanged = true;
g.setFontVector(14);
renderLowerLimit();
renderUpperLimit(); //+ for Btn1
renderPlusMinusIcons(); g.drawString("+", 222, 50);
}
//- for Btn3
function setLimitSetterToNone() { g.drawString("-", 222,165);
limitSetter = Setter.NONE;
return;
upperLimitChanged = true; }
lowerLimitChanged = true;
function renderHomeIcon() {
renderLowerLimit(); //Home for Btn2
renderUpperLimit(); g.setColor(1, 1, 1);
renderPlusMinusIcons(); g.drawLine(220, 118, 227, 110);
} g.drawLine(227, 110, 234, 118);
function incrementLimit() { g.drawPoly([222,117,222,125,232,125,232,117], false);
resetHighlightTimeout(); g.drawRect(226,120,229,125);
}
if (limitSetter === Setter.UPPER) {
upperLimit++; function buzz() {
renderUpperLimit(); // Do not buzz if not confident
upperLimitChanged = true; if(hrConfidence < 85) { return; }
} else if(limitSetter === Setter.LOWER) {
lowerLimit++; if(currentHeartRate > settings.upperLimit)
renderLowerLimit(); {
lowerLimitChanged = true; Bangle.buzz(shortBuzzTimeInMs);
} setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2);
} }
function decrementLimit(){ if(currentHeartRate < settings.lowerLimit)
resetHighlightTimeout(); {
Bangle.buzz(longBuzzTimeInMs);
if (limitSetter === Setter.UPPER) { }
upperLimit--; }
renderUpperLimit();
upperLimitChanged = true; function onHrm(hrm){
} else if(limitSetter === Setter.LOWER) { if(currentHeartRate !== hrm.bpm){
lowerLimit--; currentHeartRate = hrm.bpm;
renderLowerLimit(); hrChanged = true;
lowerLimitChanged = true; }
}
} if(hrConfidence !== hrm.confidence) {
hrConfidence = hrm.confidence;
function resetHighlightTimeout() { confidenceChanged = true;
if (setterHighlightTimeout) { }
clearTimeout(setterHighlightTimeout); }
}
function setLimitSetterToLower() {
setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); resetHighlightTimeout();
}
limitSetter = Setter.LOWER;
function switchOffApp(){
Bangle.setHRMPower(0,"wohrm"); upperLimitChanged = true;
Bangle.showLauncher(); lowerLimitChanged = true;
}
renderUpperLimit();
Bangle.on('lcdPower', (on) => { renderLowerLimit();
g.clear(); renderPlusMinusIcons();
if (on) { }
Bangle.drawWidgets();
function setLimitSetterToUpper() {
renderHomeIcon(); resetHighlightTimeout();
renderLowerLimitBackground();
renderUpperLimitBackground(); limitSetter = Setter.UPPER;
lowerLimitChanged = true;
upperLimitChanged = true; upperLimitChanged = true;
drawTrainingHeartRate(); lowerLimitChanged = true;
}
}); renderLowerLimit();
renderUpperLimit();
Bangle.setHRMPower(1,"wohrm"); renderPlusMinusIcons();
Bangle.on('HRM', onHrm); }
setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); function setLimitSetterToNone() {
setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); limitSetter = Setter.NONE;
setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true});
setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); upperLimitChanged = true;
lowerLimitChanged = true;
setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true});
renderLowerLimit();
g.clear(); renderUpperLimit();
Bangle.loadWidgets(); renderPlusMinusIcons();
Bangle.drawWidgets(); }
renderHomeIcon(); function incrementLimit() {
renderLowerLimitBackground(); resetHighlightTimeout();
renderUpperLimitBackground();
if (limitSetter === Setter.UPPER) {
setInterval(drawTrainingHeartRate, 1000); settings.upperLimit++;
renderUpperLimit();
upperLimitChanged = true;
} else if(limitSetter === Setter.LOWER) {
settings.lowerLimit++;
renderLowerLimit();
lowerLimitChanged = true;
}
}
function decrementLimit(){
resetHighlightTimeout();
if (limitSetter === Setter.UPPER) {
settings.upperLimit--;
renderUpperLimit();
upperLimitChanged = true;
} else if(limitSetter === Setter.LOWER) {
settings.lowerLimit--;
renderLowerLimit();
lowerLimitChanged = true;
}
}
function resetHighlightTimeout() {
if (setterHighlightTimeout) {
clearTimeout(setterHighlightTimeout);
}
setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000);
}
function switchOffApp(){
Bangle.setHRMPower(0,"wohrm");
load();
}
Bangle.on('lcdPower', (on) => {
if (on) {
Bangle.drawWidgets();
if (typeof(BTN5) !== typeof(undefined)) {
renderHomeIcon();
}
renderLshape(lowerLshape);
renderLshape(upperLshape);
lowerLimitChanged = true;
upperLimitChanged = true;
drawTrainingHeartRate();
}
});
Bangle.setHRMPower(1,"wohrm");
Bangle.on('HRM', onHrm);
g.setTheme({bg:"#000",fg:"#fff",dark:true});
g.reset();
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
if (typeof(BTN5) !== typeof(undefined)) {
renderHomeIcon();
setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true});
setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true});
setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true});
setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true });
setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true});
} else {
setWatch(switchOffApp, BTN1, {edge:"falling", debounce:50, repeat:true});
}
setInterval(drawTrainingHeartRate, 1000);

35
apps/wohrm/settings.js Normal file
View File

@ -0,0 +1,35 @@
(function menu(back) {
const SETTINGS_FILE = "wohrm.setting.json";
// initialize with default settings...
const storage = require('Storage');
var settings = storage.readJSON(SETTINGS_FILE, 1) || {
upperLimit: 130,
lowerLimit: 100
};
function save() {
storage.write(SETTINGS_FILE, settings);
}
E.showMenu({
'': { 'title': 'Workout HRM' },
'< Back': back,
'Upper limit': {
value: settings.upperLimit,
min: 100, max: 200,
onchange: v => {
settings.upperLimit = v;
save();
}
},
'Lower limit': {
value: settings.lowerLimit,
min: 50, max: 150,
onchange: v => {
settings.lowerLimit = v;
save();
}
}
});
})