2022-01-10 15:10:42 +00:00
|
|
|
const Layout = require("Layout");
|
2022-01-10 18:02:39 +00:00
|
|
|
const heatshrink = require('heatshrink');
|
2022-01-11 15:18:10 +00:00
|
|
|
const storage = require('Storage');
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
let tStart;
|
2022-01-10 15:10:42 +00:00
|
|
|
let historyY = [];
|
|
|
|
let historyZ = [];
|
|
|
|
let historyAvgY = [];
|
|
|
|
let historyAvgZ = [];
|
|
|
|
let historySlopeY = [];
|
|
|
|
let historySlopeZ = [];
|
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
let lastZeroPassCameFromPositive;
|
2022-01-10 15:10:42 +00:00
|
|
|
let lastZeroPassTime = 0;
|
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
let lastExerciseCompletionTime = 0;
|
|
|
|
let lastExerciseHalfCompletionTime = 0;
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
let exerciseType = {
|
2022-01-11 11:38:19 +00:00
|
|
|
"id": "",
|
2022-01-10 18:02:39 +00:00
|
|
|
"name": ""
|
|
|
|
};
|
2022-01-11 11:38:19 +00:00
|
|
|
|
|
|
|
// add new exercises here:
|
2022-01-10 18:02:39 +00:00
|
|
|
const exerciseTypes = [{
|
|
|
|
"id": "pushup",
|
2022-01-11 15:04:54 +00:00
|
|
|
"name": "push ups",
|
2022-01-10 18:02:39 +00:00
|
|
|
"useYaxe": true,
|
2022-01-11 11:38:19 +00:00
|
|
|
"useZaxe": false,
|
|
|
|
"thresholdY": 2500,
|
2022-01-11 15:04:54 +00:00
|
|
|
"thresholdMinTime": 1400, // mininmal time between two push ups in ms
|
|
|
|
"thresholdMaxTime": 5000, // maximal time between two push ups in ms
|
|
|
|
"thresholdMinDurationTime": 700, // mininmal duration of half a push ups in ms
|
2022-01-11 11:38:19 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": "curl",
|
2022-01-11 15:04:54 +00:00
|
|
|
"name": "curls",
|
2022-01-11 11:38:19 +00:00
|
|
|
"useYaxe": true,
|
|
|
|
"useZaxe": false,
|
|
|
|
"thresholdY": 2500,
|
2022-01-11 15:04:54 +00:00
|
|
|
"thresholdMinTime": 1000, // mininmal time between two curls in ms
|
|
|
|
"thresholdMaxTime": 5000, // maximal time between two curls in ms
|
|
|
|
"thresholdMinDurationTime": 500, // mininmal duration of half a push ups in ms
|
2022-01-11 11:38:19 +00:00
|
|
|
}
|
2022-01-10 18:02:39 +00:00
|
|
|
];
|
|
|
|
let exerciseCounter = 0;
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
let layout;
|
2022-01-10 15:10:42 +00:00
|
|
|
let recordActive = false;
|
|
|
|
|
2022-01-11 11:38:19 +00:00
|
|
|
// Size of average window for data analysis
|
2022-01-10 18:02:39 +00:00
|
|
|
const avgSize = 6;
|
|
|
|
|
|
|
|
let hrtValue;
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
let settings = storage.readJSON("banglexercise.json", 1) || {
|
|
|
|
'buzz': true
|
|
|
|
};
|
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
function showMainMenu() {
|
2022-01-10 15:10:42 +00:00
|
|
|
let menu;
|
2022-01-10 18:02:39 +00:00
|
|
|
menu = {
|
|
|
|
"": {
|
|
|
|
title: "BanglExercise"
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exerciseTypes.forEach(function(et) {
|
|
|
|
menu["Do " + et.name] = function() {
|
|
|
|
exerciseType = et;
|
|
|
|
E.showMenu();
|
|
|
|
startRecording();
|
2022-01-10 15:10:42 +00:00
|
|
|
};
|
2022-01-10 18:02:39 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (exerciseCounter > 0) {
|
2022-01-11 15:04:54 +00:00
|
|
|
menu["--------"] = {
|
2022-01-11 11:38:19 +00:00
|
|
|
value: ""
|
|
|
|
};
|
2022-01-10 18:02:39 +00:00
|
|
|
menu["Last:"] = {
|
|
|
|
value: exerciseCounter + " " + exerciseType.name
|
2022-01-10 15:10:42 +00:00
|
|
|
};
|
|
|
|
}
|
2022-01-10 18:02:39 +00:00
|
|
|
|
2022-01-10 15:10:42 +00:00
|
|
|
E.showMenu(menu);
|
|
|
|
}
|
|
|
|
|
|
|
|
function accelHandler(accel) {
|
2022-01-10 18:02:39 +00:00
|
|
|
if (!exerciseType) return;
|
2022-01-10 15:10:42 +00:00
|
|
|
const t = Math.round(new Date().getTime()); // time in ms
|
2022-01-10 18:02:39 +00:00
|
|
|
const y = exerciseType.useYaxe ? accel.y * 8192 : 0;
|
|
|
|
const z = exerciseType.useZaxe ? accel.z * 8192 : 0;
|
2022-01-10 15:10:42 +00:00
|
|
|
//console.log(t, y, z);
|
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
if (exerciseType.useYaxe) {
|
|
|
|
while (historyY.length > avgSize)
|
|
|
|
historyY.shift();
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
historyY.push(y);
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
if (historyY.length > avgSize / 2) {
|
|
|
|
const avgY = E.sum(historyY) / historyY.length;
|
|
|
|
historyAvgY.push([t, avgY]);
|
2022-01-11 15:04:54 +00:00
|
|
|
while (historyAvgY.length > avgSize)
|
|
|
|
historyAvgY.shift();
|
2022-01-10 18:02:39 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
if (exerciseType.useYaxe) {
|
|
|
|
while (historyZ.length > avgSize)
|
|
|
|
historyZ.shift();
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
historyZ.push(z);
|
|
|
|
|
|
|
|
if (historyZ.length > avgSize / 2) {
|
|
|
|
const avgZ = E.sum(historyZ) / historyZ.length;
|
|
|
|
historyAvgZ.push([t, avgZ]);
|
2022-01-11 15:04:54 +00:00
|
|
|
while (historyAvgZ.length > avgSize)
|
|
|
|
historyAvgZ.shift();
|
2022-01-10 18:02:39 +00:00
|
|
|
}
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// slope for Y
|
2022-01-10 18:02:39 +00:00
|
|
|
if (exerciseType.useYaxe) {
|
|
|
|
let l = historyAvgY.length;
|
|
|
|
if (l > 1) {
|
|
|
|
const p1 = historyAvgY[l - 2];
|
|
|
|
const p2 = historyAvgY[l - 1];
|
2022-01-11 11:38:19 +00:00
|
|
|
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
|
|
|
|
switch (exerciseType.id) {
|
|
|
|
case "pushup":
|
|
|
|
isValidYAxisExercise(slopeY, t);
|
|
|
|
break;
|
|
|
|
case "curl":
|
|
|
|
isValidYAxisExercise(slopeY, t);
|
|
|
|
break;
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
2022-01-11 11:38:19 +00:00
|
|
|
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// slope for Z
|
2022-01-10 18:02:39 +00:00
|
|
|
if (exerciseType.useZaxe) {
|
|
|
|
l = historyAvgZ.length;
|
|
|
|
if (l > 1) {
|
|
|
|
const p1 = historyAvgZ[l - 2];
|
|
|
|
const p2 = historyAvgZ[l - 1];
|
2022-01-11 11:38:19 +00:00
|
|
|
const slopeZ = (p2[1] - p1[1]) / (p2[0] - p1[0]);
|
2022-01-10 18:02:39 +00:00
|
|
|
historyAvgZ.shift();
|
2022-01-11 11:38:19 +00:00
|
|
|
historySlopeZ.push([p2[0] - p1[0], slopeZ]);
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-11 11:38:19 +00:00
|
|
|
// TODO: we can use this data for some exercises which can be detected by using Z axis data
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-11 11:38:19 +00:00
|
|
|
}
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-11 11:38:19 +00:00
|
|
|
/*
|
|
|
|
* Check if slope value of Y-axis data looks like an exercise
|
|
|
|
*
|
|
|
|
* In detail we look for slop values which are bigger than the configured Y threshold for the current exercise
|
|
|
|
* 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 isValidYAxisExercise(slopeY, t) {
|
|
|
|
if (!exerciseType) return;
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-11 11:38:19 +00:00
|
|
|
const thresholdY = exerciseType.thresholdY;
|
2022-01-11 15:04:54 +00:00
|
|
|
const thresholdMinTime = exerciseType.thresholdMinTime;
|
|
|
|
const thresholdMaxTime = exerciseType.thresholdMaxTime;
|
|
|
|
const thresholdMinDurationTime = exerciseType.thresholdMinDurationTime;
|
2022-01-11 11:38:19 +00:00
|
|
|
const exerciseName = exerciseType.name;
|
|
|
|
|
|
|
|
if (Math.abs(slopeY) >= thresholdY) {
|
|
|
|
historyAvgY.shift();
|
|
|
|
historySlopeY.push([t, slopeY]);
|
|
|
|
//console.log(t, Math.abs(slopeY));
|
|
|
|
|
|
|
|
const lSlopeY = historySlopeY.length;
|
|
|
|
if (lSlopeY > 1) {
|
2022-01-11 15:04:54 +00:00
|
|
|
const p1 = historySlopeY[lSlopeY - 1][1];
|
|
|
|
const p2 = historySlopeY[lSlopeY - 2][1];
|
2022-01-11 11:38:19 +00:00
|
|
|
if (p1 > 0 && p2 < 0) {
|
2022-01-11 15:04:54 +00:00
|
|
|
if (lastZeroPassCameFromPositive == false) {
|
|
|
|
lastExerciseHalfCompletionTime = t;
|
2022-01-11 11:38:19 +00:00
|
|
|
console.log(t, exerciseName + " half complete...");
|
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
layout.progress.label = "½";
|
2022-01-11 11:38:19 +00:00
|
|
|
layout.render();
|
|
|
|
}
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
lastZeroPassCameFromPositive = true;
|
2022-01-11 11:38:19 +00:00
|
|
|
lastZeroPassTime = t;
|
|
|
|
}
|
|
|
|
if (p2 > 0 && p1 < 0) {
|
2022-01-11 15:04:54 +00:00
|
|
|
if (lastZeroPassCameFromPositive == true) {
|
|
|
|
const tDiffLastExercise = t - lastExerciseCompletionTime;
|
2022-01-11 11:38:19 +00:00
|
|
|
const tDiffStart = t - tStart;
|
2022-01-11 15:04:54 +00:00
|
|
|
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) {
|
|
|
|
console.log(t, exerciseName + " complete!!!");
|
|
|
|
|
|
|
|
lastExerciseCompletionTime = t;
|
|
|
|
exerciseCounter++;
|
|
|
|
|
|
|
|
layout.count.label = exerciseCounter;
|
|
|
|
layout.progress.label = "";
|
|
|
|
layout.render();
|
|
|
|
|
|
|
|
if (settings.buzz)
|
|
|
|
Bangle.buzz(100, 0.4);
|
|
|
|
} else {
|
|
|
|
console.log(t, exerciseName + " to quick for duration time threshold!");
|
|
|
|
lastExerciseCompletionTime = t;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log(t, exerciseName + " to slow for time threshold!");
|
|
|
|
lastExerciseCompletionTime = t;
|
|
|
|
}
|
2022-01-11 11:38:19 +00:00
|
|
|
} else {
|
|
|
|
console.log(t, exerciseName + " to quick for time threshold!");
|
2022-01-11 15:04:54 +00:00
|
|
|
lastExerciseCompletionTime = t;
|
2022-01-11 11:38:19 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-10 15:10:42 +00:00
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
lastZeroPassCameFromPositive = false;
|
2022-01-11 11:38:19 +00:00
|
|
|
lastZeroPassTime = t;
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-11 11:38:19 +00:00
|
|
|
|
2022-01-10 15:10:42 +00:00
|
|
|
function reset() {
|
|
|
|
historyY = [];
|
|
|
|
historyZ = [];
|
|
|
|
historyAvgY = [];
|
|
|
|
historyAvgZ = [];
|
|
|
|
historySlopeY = [];
|
|
|
|
historySlopeZ = [];
|
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
lastZeroPassCameFromPositive = undefined;
|
2022-01-10 15:10:42 +00:00
|
|
|
lastZeroPassTime = 0;
|
2022-01-11 15:04:54 +00:00
|
|
|
lastExerciseHalfCompletionTime = 0;
|
|
|
|
lastExerciseCompletionTime = 0;
|
2022-01-10 18:02:39 +00:00
|
|
|
exerciseCounter = 0;
|
|
|
|
tStart = 0;
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startRecording() {
|
|
|
|
if (recordActive) return;
|
|
|
|
g.clear(1);
|
|
|
|
reset();
|
2022-01-10 18:02:39 +00:00
|
|
|
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",
|
2022-01-10 18:02:39 +00:00
|
|
|
label: exerciseType.name,
|
2022-01-10 15:10:42 +00:00
|
|
|
pad: 5
|
|
|
|
},
|
|
|
|
{
|
2022-01-10 18:02:39 +00:00
|
|
|
type: "h",
|
|
|
|
c: [{
|
|
|
|
type: "txt",
|
|
|
|
id: "count",
|
|
|
|
font: "6x8:10",
|
|
|
|
label: exerciseCounter,
|
|
|
|
pad: 5,
|
|
|
|
bgCol: g.theme.bg
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "txt",
|
|
|
|
id: "progress",
|
|
|
|
font: "6x8:2",
|
|
|
|
label: "",
|
|
|
|
pad: 5
|
|
|
|
},
|
|
|
|
]
|
2022-01-10 15:10:42 +00:00
|
|
|
},
|
|
|
|
{
|
2022-01-10 18:02:39 +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: "RECORDING",
|
|
|
|
bgCol: "#f00",
|
|
|
|
pad: 5,
|
|
|
|
fillx: 1
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}, {
|
2022-01-11 11:38:19 +00:00
|
|
|
btns: [{
|
|
|
|
label: "STOP",
|
|
|
|
cb: () => {
|
|
|
|
stopRecording();
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
2022-01-11 11:38:19 +00:00
|
|
|
}],
|
2022-01-10 18:02:39 +00:00
|
|
|
lazy: true
|
2022-01-10 15:10:42 +00:00
|
|
|
});
|
|
|
|
layout.render();
|
|
|
|
|
|
|
|
Bangle.setPollInterval(80); // 12.5 Hz
|
|
|
|
Bangle.on('accel', accelHandler);
|
|
|
|
tStart = new Date().getTime();
|
|
|
|
recordActive = true;
|
2022-01-11 15:04:54 +00:00
|
|
|
if (settings.buzz)
|
|
|
|
Bangle.buzz(200, 1);
|
2022-01-10 15:10:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function stopRecording() {
|
|
|
|
if (!recordActive) return;
|
2022-01-10 18:02:39 +00:00
|
|
|
|
2022-01-11 15:04:54 +00:00
|
|
|
g.clear(1);
|
2022-01-10 15:10:42 +00:00
|
|
|
Bangle.removeListener('accel', accelHandler);
|
2022-01-11 15:04:54 +00:00
|
|
|
Bangle.setHRMPower(0, "banglexercise");
|
2022-01-10 18:02:39 +00:00
|
|
|
showMainMenu();
|
2022-01-10 15:10:42 +00:00
|
|
|
recordActive = false;
|
|
|
|
}
|
|
|
|
|
2022-01-10 18:02:39 +00:00
|
|
|
Bangle.on('HRM', function(hrm) {
|
|
|
|
hrtValue = hrm.bpm;
|
|
|
|
});
|
|
|
|
|
2022-01-10 15:10:42 +00:00
|
|
|
g.clear(1);
|
2022-01-10 18:02:39 +00:00
|
|
|
showMainMenu();
|