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",
"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.",
"icon": "app.png",
"type": "app",
"tags": "hrm,workout",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}],
"storage": [
{"name":"wohrm.app.js","url":"app.js"},
{"name":"wohrm.settings.js","url":"settings.js"},
{"name":"wohrm.img","url":"app-icon.js","evaluate":true}
]
},

View File

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