BangleApps/apps/banglexercise/app.js

387 lines
10 KiB
JavaScript
Raw Normal View History

2022-01-10 15:10:42 +00:00
const Layout = require("Layout");
const heatshrink = require('heatshrink');
2022-01-11 15:18:10 +00:00
const storage = require('Storage');
2022-01-10 15:10:42 +00:00
let tStart;
2022-01-10 15:10:42 +00:00
let historyY = [];
let historyZ = [];
let historyAvgY = [];
let historyAvgZ = [];
let historySlopeY = [];
let historySlopeZ = [];
let lastZeroPassCameFromPositive;
2024-03-04 20:34:50 +00:00
//let lastZeroPassTime = 0;
2022-01-10 15:10:42 +00:00
let lastExerciseCompletionTime = 0;
let lastExerciseHalfCompletionTime = 0;
2022-01-10 15:10:42 +00:00
let exerciseType = {
"id": "",
"name": ""
};
// add new exercises here:
const exerciseTypes = [{
"id": "pushup",
"name": "push ups",
"useYaxis": true,
"useZaxis": false,
"threshold": 2500,
"thresholdMinTime": 800, // mininmal time between two push ups in ms
"thresholdMaxTime": 5000, // maximal time between two push ups in ms
"thresholdMinDurationTime": 600, // mininmal duration of half a push up in ms
},
{
"id": "curl",
"name": "curls",
"useYaxis": true,
"useZaxis": false,
"threshold": 2500,
"thresholdMinTime": 800, // mininmal time between two curls in ms
"thresholdMaxTime": 5000, // maximal time between two curls in ms
"thresholdMinDurationTime": 500, // mininmal duration of half a curl in ms
},
{
"id": "situp",
"name": "sit ups",
"useYaxis": false,
"useZaxis": true,
"threshold": 3500,
"thresholdMinTime": 800, // mininmal time between two sit ups in ms
"thresholdMaxTime": 5000, // maximal time between two sit ups in ms
"thresholdMinDurationTime": 500, // mininmal duration of half a sit up in ms
}
];
let exerciseCounter = 0;
2022-01-10 15:10:42 +00:00
let layout;
2022-01-10 15:10:42 +00:00
let recordActive = false;
// Size of average window for data analysis
const avgSize = 6;
let hrtValue;
2022-01-10 15:10:42 +00:00
let settings = storage.readJSON("banglexercise.json", 1) || {
'buzz': true
};
function showMainMenu() {
2022-01-10 15:10:42 +00:00
let menu;
menu = {
"": {
2022-09-22 22:39:27 +00:00
title: "BanglExercise",
back: load
}
};
exerciseTypes.forEach(function(et) {
menu[et.name] = function() {
exerciseType = et;
E.showMenu();
startTraining();
2022-01-10 15:10:42 +00:00
};
});
if (exerciseCounter > 0) {
menu["--------"] = {
value: ""
};
menu["Last:"] = {
value: exerciseCounter + " " + exerciseType.name
2022-01-10 15:10:42 +00:00
};
}
menu.exit = function() {
load();
2022-01-12 09:27:39 +00:00
};
2022-01-10 15:10:42 +00:00
E.showMenu(menu);
}
function accelHandler(accel) {
if (!exerciseType) return;
2022-01-10 15:10:42 +00:00
const t = Math.round(new Date().getTime()); // time in ms
const y = exerciseType.useYaxis ? accel.y * 8192 : 0;
const z = exerciseType.useZaxis ? accel.z * 8192 : 0;
2022-01-10 15:10:42 +00:00
//console.log(t, y, z);
if (exerciseType.useYaxis) {
while (historyY.length > avgSize)
historyY.shift();
2022-01-10 15:10:42 +00:00
historyY.push(y);
2022-01-10 15:10:42 +00:00
if (historyY.length > avgSize / 2) {
const avgY = E.sum(historyY) / historyY.length;
historyAvgY.push([t, avgY]);
while (historyAvgY.length > avgSize)
historyAvgY.shift();
}
}
2022-01-10 15:10:42 +00:00
if (exerciseType.useZaxis) {
while (historyZ.length > avgSize)
historyZ.shift();
2022-01-10 15:10:42 +00:00
historyZ.push(z);
if (historyZ.length > avgSize / 2) {
const avgZ = E.sum(historyZ) / historyZ.length;
historyAvgZ.push([t, avgZ]);
while (historyAvgZ.length > avgSize)
historyAvgZ.shift();
}
2022-01-10 15:10:42 +00:00
}
// slope for Y
if (exerciseType.useYaxis) {
let l = historyAvgY.length;
if (l > 1) {
const p1 = historyAvgY[l - 2];
const p2 = historyAvgY[l - 1];
const slopeY = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000);
// we use this data for exercises which can be detected by using Y axis data
isValidExercise(slopeY, t);
2022-01-10 15:10:42 +00:00
}
}
// slope for Z
if (exerciseType.useZaxis) {
2024-03-13 10:51:40 +00:00
let l = historyAvgZ.length;
if (l > 1) {
const p1 = historyAvgZ[l - 2];
const p2 = historyAvgZ[l - 1];
const slopeZ = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000);
// we use this data for some exercises which can be detected by using Z axis data
isValidExercise(slopeZ, t);
2022-01-10 15:10:42 +00:00
}
}
}
2022-01-10 15:10:42 +00:00
/*
* Check if slope value of Y-axis or Z-axis data (depending on exercise type) looks like an exercise
*
* In detail we look for slop values which are bigger than the configured threshold for the current exercise type
* Then we look for two consecutive slope values of which one is above 0 and the other is below zero.
* If we find one pair of these values this could be part of one exercise.
* Then we look for a pair of values which cross the zero from the otherwise direction
*/
function isValidExercise(slope, t) {
if (!exerciseType) return;
2022-01-10 15:10:42 +00:00
const threshold = exerciseType.threshold;
const historySlopeValues = exerciseType.useYaxis ? historySlopeY : historySlopeZ;
const thresholdMinTime = exerciseType.thresholdMinTime;
const thresholdMaxTime = exerciseType.thresholdMaxTime;
const thresholdMinDurationTime = exerciseType.thresholdMinDurationTime;
const exerciseName = exerciseType.name;
if (Math.abs(slope) >= threshold) {
historySlopeValues.push([t, slope]);
//console.log(t, Math.abs(slope));
const lSlopeHistory = historySlopeValues.length;
if (lSlopeHistory > 1) {
const p1 = historySlopeValues[lSlopeHistory - 1][1];
const p2 = historySlopeValues[lSlopeHistory - 2][1];
if (p1 > 0 && p2 < 0) {
if (lastZeroPassCameFromPositive == false) {
lastExerciseHalfCompletionTime = t;
console.log(t, exerciseName + " half complete...");
layout.progress.label = "½";
layout.recording.label = "TRAINING";
2022-01-12 09:27:39 +00:00
g.clear();
layout.render();
}
2022-01-10 15:10:42 +00:00
lastZeroPassCameFromPositive = true;
2024-03-04 20:34:50 +00:00
//lastZeroPassTime = t;
}
if (p2 > 0 && p1 < 0) {
if (lastZeroPassCameFromPositive == true) {
const tDiffLastExercise = t - lastExerciseCompletionTime;
const tDiffStart = t - tStart;
console.log(t, exerciseName + " maybe complete?", Math.round(tDiffLastExercise), Math.round(tDiffStart));
// check minimal time between exercises:
if ((lastExerciseCompletionTime <= 0 && tDiffStart >= thresholdMinTime) || tDiffLastExercise >= thresholdMinTime) {
// check maximal time between exercises:
if (lastExerciseCompletionTime <= 0 || tDiffLastExercise <= thresholdMaxTime) {
// check minimal duration of exercise:
const tDiffExerciseHalfCompletion = t - lastExerciseHalfCompletionTime;
if (tDiffExerciseHalfCompletion > thresholdMinDurationTime) {
2022-01-12 09:31:05 +00:00
//console.log(t, exerciseName + " complete!!!");
lastExerciseCompletionTime = t;
exerciseCounter++;
layout.count.label = exerciseCounter;
layout.progress.label = "";
layout.recording.label = "Good!";
2022-01-12 09:27:39 +00:00
g.clear();
layout.render();
if (settings.buzz)
Bangle.buzz(200, 0.5);
} else {
console.log(t, exerciseName + " too quick for duration time threshold!"); // thresholdMinDurationTime
lastExerciseCompletionTime = t;
layout.recording.label = "Go slower!";
g.clear();
layout.render();
}
} else {
console.log(t, exerciseName + " top slow for time threshold!"); // thresholdMaxTime
lastExerciseCompletionTime = t;
layout.recording.label = "Go faster!";
g.clear();
layout.render();
}
} else {
console.log(t, exerciseName + " too quick for time threshold!"); // thresholdMinTime
lastExerciseCompletionTime = t;
layout.recording.label = "Go slower!";
g.clear();
layout.render();
}
}
2022-01-10 15:10:42 +00:00
lastZeroPassCameFromPositive = false;
2024-03-04 20:34:50 +00:00
//lastZeroPassTime = t;
2022-01-10 15:10:42 +00:00
}
}
}
}
2022-01-10 15:10:42 +00:00
function reset() {
historyY = [];
historyZ = [];
historyAvgY = [];
historyAvgZ = [];
historySlopeY = [];
historySlopeZ = [];
lastZeroPassCameFromPositive = undefined;
2024-03-04 20:34:50 +00:00
//lastZeroPassTime = 0;
lastExerciseHalfCompletionTime = 0;
lastExerciseCompletionTime = 0;
exerciseCounter = 0;
tStart = 0;
2022-01-10 15:10:42 +00:00
}
function startTraining() {
2022-01-10 15:10:42 +00:00
if (recordActive) return;
g.clear(1);
reset();
Bangle.setLCDTimeout(0); // force LCD on
Bangle.setHRMPower(1, "banglexercise");
if (!hrtValue) hrtValue = "...";
2022-01-10 15:10:42 +00:00
layout = new Layout({
type: "v",
c: [{
type: "txt",
id: "type",
font: "6x8:2",
label: exerciseType.name,
2022-01-10 15:10:42 +00:00
pad: 5
},
{
type: "h",
c: [{
type: "txt",
id: "count",
2022-01-12 09:27:39 +00:00
font: exerciseCounter < 100 ? "6x8:9" : "6x8:8",
label: exerciseCounter,
2022-01-12 09:27:39 +00:00
pad: 5
},
{
type: "txt",
id: "progress",
font: "6x8:2",
label: "",
pad: 5
},
]
2022-01-10 15:10:42 +00:00
},
{
type: "h",
c: [{
type: "img",
pad: 4,
src: function() {
return heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
}
},
{
type: "txt",
id: "hrtRate",
font: "6x8:2",
label: hrtValue,
pad: 5
},
]
2022-01-10 15:10:42 +00:00
},
{
type: "txt",
id: "recording",
font: "6x8:2",
label: "TRAINING",
2022-01-10 15:10:42 +00:00
bgCol: "#f00",
pad: 5,
fillx: 1
},
]
}, {
btns: [{
label: "STOP",
cb: () => {
stopTraining();
2022-01-10 15:10:42 +00:00
}
}],
2022-01-12 09:27:39 +00:00
lazy: false
2022-01-10 15:10:42 +00:00
});
layout.render();
Bangle.setPollInterval(80); // 12.5 Hz
2022-01-10 15:10:42 +00:00
tStart = new Date().getTime();
recordActive = true;
if (settings.buzz)
Bangle.buzz(200, 1);
// delay start a little bit
setTimeout(() => {
Bangle.on('accel', accelHandler);
}, 1000);
2022-01-10 15:10:42 +00:00
}
function stopTraining() {
2022-01-10 15:10:42 +00:00
if (!recordActive) return;
g.clear(1);
2022-01-10 15:10:42 +00:00
Bangle.removeListener('accel', accelHandler);
Bangle.setHRMPower(0, "banglexercise");
showMainMenu();
2022-01-10 15:10:42 +00:00
recordActive = false;
}
Bangle.on('HRM', function(hrm) {
hrtValue = hrm.bpm;
});
2022-01-10 15:10:42 +00:00
g.clear(1);
Bangle.loadWidgets();
showMainMenu();