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

@ -4,14 +4,16 @@ const Setter = {
UPPER: 'upper', UPPER: 'upper',
LOWER: 'lower' LOWER: 'lower'
}; };
const SETTINGS_FILE = "wohrm.setting.json";
var settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {
upperLimit: 130,
lowerLimit: 100
};
const shortBuzzTimeInMs = 80; const shortBuzzTimeInMs = 80;
const longBuzzTimeInMs = 400; const longBuzzTimeInMs = 400;
let upperLimit = 130;
let upperLimitChanged = true; let upperLimitChanged = true;
let lowerLimit = 100;
let lowerLimitChanged = true; let lowerLimitChanged = true;
let limitSetter = Setter.NONE; let limitSetter = Setter.NONE;
@ -23,54 +25,115 @@ let confidenceChanged = true;
let setterHighlightTimeout; let setterHighlightTimeout;
function renderUpperLimitBackground() { const isB1 = process.env.HWVERSION==1;
g.setColor(1,0,0); const upperLshape = isB1 ? {
g.fillRect(125,40, 210, 70); right: 125,
g.fillRect(180,70, 210, 200); left: 210,
bottom: 40,
top: 210,
rectWidth: 30,
cornerRoundness: 5,
orientation: -1,
color: '#f00'
} : {
right: Bangle.appRect.x2-100,
left: Bangle.appRect.x2,
bottom: 24,
top: Bangle.appRect.y2,
rectWidth: 26,
cornerRoundness: 4,
orientation: -1, // rotated 180°
color: '#f00'
};
//Round top left corner const lowerLshape = {
g.fillEllipse(115,40,135,70); left: isB1 ? 10 : Bangle.appRect.x,
right: 100,
bottom: upperLshape.top,
top: upperLshape.bottom,
rectWidth: upperLshape.rectWidth,
cornerRoundness: upperLshape.cornerRoundness,
orientation: 1,
color: '#00f'
};
//Round top right corner const centerBar = {
g.setColor(0,0,0); minY: (upperLshape.bottom + upperLshape.top - upperLshape.rectWidth)/2,
g.fillRect(205,40, 210, 45); maxY: (upperLshape.bottom + upperLshape.top + upperLshape.rectWidth)/2,
g.setColor(1,0,0); confidenceWidth: isB1 ? 10 : 8,
g.fillEllipse(190,40,210,50); minX: isB1 ? 55 : upperLshape.rectWidth + 14,
maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14
};
//Round inner corner const fontSizes = isB1 ? {
g.fillRect(174,71, 179, 76); limits: 13,
g.setColor(0,0,0); heartRate: 24
g.fillEllipse(160,71,179,82); } : {
limits: 12,
heartRate: 20
};
//Round bottom function fillEllipse(x, y, x2, y2) {
g.setColor(1,0,0); g.fillEllipse(Math.min(x, x2),
g.fillEllipse(180,190, 210, 210); Math.min(y, y2),
Math.max(x, x2),
Math.max(y, y2));
} }
function renderLowerLimitBackground() { /**
g.setColor(0,0,1); * @param p.left: the X coordinate of the left side of the L in its orientation
g.fillRect(10, 180, 100, 210); * @param p.right: the X coordinate of the right side of the L in its orientation
g.fillRect(10, 50, 40, 180); * @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
* @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°
* @param p.color: the color to draw the shape
*/
function renderLshape(p) {
g.setColor(p.color);
//Rounded top g.fillRect(p.right, p.bottom, p.left, p.bottom-p.orientation*p.rectWidth);
g.setColor(0,0,1); g.fillRect(p.left+p.orientation*p.rectWidth,
g.fillEllipse(10,40, 40, 60); p.bottom-p.orientation*p.rectWidth,
p.left,
p.top+p.orientation*p.cornerRoundness*2);
//Round bottom right corner //Round end of small line
g.setColor(0,0,1); fillEllipse(p.right+p.orientation*p.cornerRoundness*2,
g.fillEllipse(90,180,110,210); p.bottom,
p.right-p.orientation*p.cornerRoundness*2,
p.bottom-p.orientation*p.rectWidth);
//Round outer corner
g.setColor(g.theme.bg);
g.fillRect(p.left+p.orientation*p.cornerRoundness,
p.bottom,
p.left,
p.bottom-p.orientation*p.cornerRoundness);
g.setColor(p.color);
fillEllipse(p.left+p.orientation*p.cornerRoundness*4,
p.bottom,
p.left,
p.bottom-p.orientation*p.cornerRoundness*2);
//Round inner corner //Round inner corner
g.setColor(0,0,1); g.fillRect(p.left+p.orientation*(p.rectWidth+p.cornerRoundness+1),
g.fillRect(40,175,45,180); p.bottom-p.orientation*(p.rectWidth+1),
g.setColor(0,0,0); p.left+p.orientation*(p.rectWidth+1),
g.fillEllipse(41,170,60,179); p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness-1));
g.setColor(g.theme.bg);
fillEllipse(p.left+p.orientation*(p.rectWidth+p.cornerRoundness*4),
p.bottom-p.orientation*(p.rectWidth+1),
p.left+p.orientation*(p.rectWidth+1),
p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness*3-1));
//Round bottom left corner //Round end of long line
g.setColor(0,0,0); g.setColor(p.color);
g.fillRect(10,205, 15, 210); fillEllipse(p.left+p.orientation*p.rectWidth,
g.setColor(0,0,1); p.top+p.orientation*p.cornerRoundness*4,
g.fillEllipse(10,200,30,210); p.left,
p.top);
} }
function drawTrainingHeartRate() { function drawTrainingHeartRate() {
@ -91,16 +154,17 @@ function drawTrainingHeartRate() {
function renderUpperLimit() { function renderUpperLimit() {
if(!upperLimitChanged) { return; } if(!upperLimitChanged) { return; }
g.setColor(1,0,0); renderLshape(upperLshape);
g.fillRect(125,40, 210, 70);
if(limitSetter === Setter.UPPER){ if(limitSetter === Setter.UPPER){
g.setColor(255,255, 0); g.setColor(1,1,0);
} else { } else {
g.setColor(255,255,255); g.setColor(g.theme.fg);
} }
g.setFontVector(13); g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0);
g.drawString("Upper: " + upperLimit, 125, 50); g.drawString("Upper: " + settings.upperLimit,
upperLshape.right,
upperLshape.bottom+upperLshape.rectWidth/2);
upperLimitChanged = false; upperLimitChanged = false;
} }
@ -108,13 +172,17 @@ function renderUpperLimit() {
function renderCurrentHeartRate() { function renderCurrentHeartRate() {
if(!hrChanged) { return; } if(!hrChanged) { return; }
g.setColor(255,255,255); g.setColor(g.theme.fg);
g.fillRect(55, 110, 165, 150); g.fillRect(centerBar.minX, centerBar.minY,
centerBar.maxX, centerBar.maxY);
g.setColor(0,0,0); g.setColor(g.theme.bg);
g.setFontVector(24); g.setFontVector(fontSizes.heartRate);
g.setFontAlign(1, -1, 0); g.setFontAlign(1, 0, 0);
g.drawString(currentHeartRate, 130, 117); g.drawString(currentHeartRate,
Math.max(upperLshape.right+upperLshape.cornerRoundness,
lowerLshape.right-lowerLshape.cornerRoundness),
(centerBar.minY+centerBar.maxY)/2);
//Reset alignment to defaults //Reset alignment to defaults
g.setFontAlign(-1, -1, 0); g.setFontAlign(-1, -1, 0);
@ -125,16 +193,17 @@ function renderCurrentHeartRate() {
function renderLowerLimit() { function renderLowerLimit() {
if(!lowerLimitChanged) { return; } if(!lowerLimitChanged) { return; }
g.setColor(0,0,1); renderLshape(lowerLshape);
g.fillRect(10, 180, 100, 210);
if(limitSetter === Setter.LOWER){ if(limitSetter === Setter.LOWER){
g.setColor(255,255, 0); g.setColor(1,1,0);
} else { } else {
g.setColor(255,255,255); g.setColor(g.theme.fg);
} }
g.setFontVector(13); g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0);
g.drawString("Lower: " + lowerLimit, 20,190); g.drawString("Lower: " + settings.lowerLimit,
lowerLshape.left + lowerLshape.rectWidth/2,
lowerLshape.bottom - lowerLshape.rectWidth/2);
lowerLimitChanged = false; lowerLimitChanged = false;
} }
@ -143,26 +212,26 @@ function renderConfidenceBars(){
if(!confidenceChanged) { return; } if(!confidenceChanged) { return; }
if(hrConfidence >= 85){ if(hrConfidence >= 85){
g.setColor(0, 255, 0); g.setColor(0, 1, 0);
} else if (hrConfidence >= 50) { } else if (hrConfidence >= 50) {
g.setColor(255, 255, 0); g.setColor(1, 1, 0);
} else if(hrConfidence >= 0){ } else if(hrConfidence >= 0){
g.setColor(255, 0, 0); g.setColor(1, 0, 0);
} else { } else {
g.setColor(255, 255, 255); g.setColor(g.theme.fg);
} }
g.fillRect(45, 110, 55, 150); g.fillRect(centerBar.minX-centerBar.confidenceWidth, centerBar.minY, centerBar.minX, centerBar.maxY);
g.fillRect(165, 110, 175, 150); g.fillRect(centerBar.maxX, centerBar.minY, centerBar.maxX+centerBar.confidenceWidth, centerBar.maxY);
confidenceChanged = false; confidenceChanged = false;
} }
function renderPlusMinusIcons() { function renderPlusMinusIcons() {
if (limitSetter === Setter.NONE) { if (limitSetter === Setter.NONE) {
g.setColor(0, 0, 0); g.setColor(g.theme.bg);
} else { } else {
g.setColor(1, 1, 1); g.setColor(g.theme.fg);
} }
g.setFontVector(14); g.setFontVector(14);
@ -190,13 +259,13 @@ function buzz() {
// Do not buzz if not confident // Do not buzz if not confident
if(hrConfidence < 85) { return; } if(hrConfidence < 85) { return; }
if(currentHeartRate > upperLimit) if(currentHeartRate > settings.upperLimit)
{ {
Bangle.buzz(shortBuzzTimeInMs); Bangle.buzz(shortBuzzTimeInMs);
setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2);
} }
if(currentHeartRate < lowerLimit) if(currentHeartRate < settings.lowerLimit)
{ {
Bangle.buzz(longBuzzTimeInMs); Bangle.buzz(longBuzzTimeInMs);
} }
@ -255,11 +324,11 @@ function incrementLimit() {
resetHighlightTimeout(); resetHighlightTimeout();
if (limitSetter === Setter.UPPER) { if (limitSetter === Setter.UPPER) {
upperLimit++; settings.upperLimit++;
renderUpperLimit(); renderUpperLimit();
upperLimitChanged = true; upperLimitChanged = true;
} else if(limitSetter === Setter.LOWER) { } else if(limitSetter === Setter.LOWER) {
lowerLimit++; settings.lowerLimit++;
renderLowerLimit(); renderLowerLimit();
lowerLimitChanged = true; lowerLimitChanged = true;
} }
@ -269,11 +338,11 @@ function decrementLimit(){
resetHighlightTimeout(); resetHighlightTimeout();
if (limitSetter === Setter.UPPER) { if (limitSetter === Setter.UPPER) {
upperLimit--; settings.upperLimit--;
renderUpperLimit(); renderUpperLimit();
upperLimitChanged = true; upperLimitChanged = true;
} else if(limitSetter === Setter.LOWER) { } else if(limitSetter === Setter.LOWER) {
lowerLimit--; settings.lowerLimit--;
renderLowerLimit(); renderLowerLimit();
lowerLimitChanged = true; lowerLimitChanged = true;
} }
@ -289,17 +358,18 @@ function resetHighlightTimeout() {
function switchOffApp(){ function switchOffApp(){
Bangle.setHRMPower(0,"wohrm"); Bangle.setHRMPower(0,"wohrm");
Bangle.showLauncher(); load();
} }
Bangle.on('lcdPower', (on) => { Bangle.on('lcdPower', (on) => {
g.clear();
if (on) { if (on) {
Bangle.drawWidgets(); Bangle.drawWidgets();
if (typeof(BTN5) !== typeof(undefined)) {
renderHomeIcon(); renderHomeIcon();
renderLowerLimitBackground(); }
renderUpperLimitBackground(); renderLshape(lowerLshape);
renderLshape(upperLshape);
lowerLimitChanged = true; lowerLimitChanged = true;
upperLimitChanged = true; upperLimitChanged = true;
drawTrainingHeartRate(); drawTrainingHeartRate();
@ -309,19 +379,22 @@ Bangle.on('lcdPower', (on) => {
Bangle.setHRMPower(1,"wohrm"); Bangle.setHRMPower(1,"wohrm");
Bangle.on('HRM', onHrm); Bangle.on('HRM', onHrm);
setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); g.setTheme({bg:"#000",fg:"#fff",dark:true});
setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); g.reset();
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});
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
renderHomeIcon(); if (typeof(BTN5) !== typeof(undefined)) {
renderLowerLimitBackground(); renderHomeIcon();
renderUpperLimitBackground(); 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); 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();
}
}
});
})