From b711b50f888ecfb96f5a5cf4e2340507a6087c87 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Mon, 10 Jan 2022 16:10:42 +0100 Subject: [PATCH] Initial version of BanglExercise --- apps.json | 19 ++- apps/banglexercise/ChangeLog | 1 + apps/banglexercise/README.md | 35 ++++ apps/banglexercise/app-icon.js | 1 + apps/banglexercise/app.js | 259 ++++++++++++++++++++++++++++++ apps/banglexercise/app.png | Bin 0 -> 690 bytes apps/banglexercise/screenshot.png | Bin 0 -> 2043 bytes 7 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 apps/banglexercise/ChangeLog create mode 100644 apps/banglexercise/README.md create mode 100644 apps/banglexercise/app-icon.js create mode 100644 apps/banglexercise/app.js create mode 100644 apps/banglexercise/app.png create mode 100644 apps/banglexercise/screenshot.png diff --git a/apps.json b/apps.json index 389603249..0adb4abc5 100644 --- a/apps.json +++ b/apps.json @@ -5149,7 +5149,7 @@ {"name":"mmind.app.js","url":"mmind.app.js"}, {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} ] - }, + }, { "id": "presentor", "name": "Presentor", @@ -5503,5 +5503,22 @@ {"name":"limelight.settings.js","url":"limelight.settings.js"}, {"name":"limelight.img","url":"limelight.icon.js","evaluate":true} ] + }, + { "id": "banglexercise", + "name": "BanglExercise", + "shortName":"BanglExercise", + "version":"0.01", + "description": "Can automatically track exercises while wearing the Bangle.js watch.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "sport", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"banglexercise.app.js","url":"app.js"}, + {"name":"banglexercise.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/banglexercise/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/banglexercise/README.md b/apps/banglexercise/README.md new file mode 100644 index 000000000..e7ac9573d --- /dev/null +++ b/apps/banglexercise/README.md @@ -0,0 +1,35 @@ +# Banglexercise + +Can automatically track exercises while wearing the Bangle.js watch. + +Currently only push ups are supported. + + +## Usage + +Select the exercise type you want to practive and go for it! +Press stop to end your exercise. + + +## Screenshots +![](screenshot.png) + +## TODO +* Add other exercise types: + * Rope jumps + * Curls + * Sit ups + * ... +* Save exercises to file system +* Add settings (vibration, beep, ...) +* Find a nicer icon + + +## Contribute +Feel free to send in improvements and remarks. + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icons +Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/banglexercise/app-icon.js b/apps/banglexercise/app-icon.js new file mode 100644 index 000000000..e1923bf54 --- /dev/null +++ b/apps/banglexercise/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIbYh/8AYM/+EP/wFBv4FB/4FB/4FHAwIEBAv4FPAgIGCAosHAofggYFD4EABgXgOgIFLDAQWBAo0BAoOAVIV/UYQABj/4AocDCwQFTg46CEY4vFAopBBApIAVA==")) diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js new file mode 100644 index 000000000..feb3a1127 --- /dev/null +++ b/apps/banglexercise/app.js @@ -0,0 +1,259 @@ +const Layout = require("Layout"); + +let historyY = []; +let historyZ = []; +const avgSize = 10; + +const thresholdY = 2500; +const thresholdPushUpTime = 1400; // mininmal time between two push ups +let tStart; + +let avgY; +let avgZ; +let historyAvgY = []; +let historyAvgZ = []; +let historySlopeY = []; +let historySlopeZ = []; + +let lastZeroPassType; +let lastZeroPassTime = 0; +let lastPushUpCmpltTime = 0; + +let exerciseType = ""; +let pushUpCounter = 0; + +let layout; + +let recordActive = false; + + +function showMenu() { + let menu; + if (pushUpCounter == 0) { + menu = { + "": { + title: "Banglexercise" + }, + "Start push ups": function() { + exerciseType = "push ups"; + E.showMenu(); + startRecording(); + } + }; + } else { + menu = { + "": { + title: "Banglexercise" + }, + "Last:": { + value: pushUpCounter + " push ups" + }, + "Start push ups": function() { + exerciseType = "push ups"; + E.showMenu(); + startRecording(); + } + }; + } + E.showMenu(menu); +} + +function accelHandler(accel) { + const t = Math.round(new Date().getTime()); // time in ms + const y = accel.y * 8192; + const z = accel.z * 8192; + //console.log(t, y, z); + + while (historyY.length > avgSize) + historyY.shift(); + historyY.push(y); + + if (historyY.length > avgSize / 2) + avgY = E.sum(historyY) / historyY.length; + + while (historyZ.length > avgSize) + historyZ.shift(); + historyZ.push(z); + + if (historyZ.length > avgSize / 2) + avgZ = E.sum(historyZ) / historyZ.length; + + if (avgY) { + //console.log(avgY, avgZ); + historyAvgY.push([t, avgY]); + historyAvgZ.push([t, avgZ]); + } + + let mY; + let mZ; + // slope for Y + let l = historyAvgY.length; + if (l > 1) { + const p1 = historyAvgY[l - 2]; + const p2 = historyAvgY[l - 1]; + mY = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000); + if (Math.abs(mY) >= thresholdY) { + historyAvgY.shift(); + historySlopeY.push([t, mY]); + //console.log(t, Math.abs(mY)); + + const lMY = historySlopeY.length; + if (lMY > 1) { + const pMY1 = historySlopeY[lMY - 2][1]; + const pMY2 = historySlopeY[lMY - 1][1]; + isValidPushUp(pMY1, pMY2, t); + } + } + } + + // slope for Z + l = historyAvgZ.length; + if (l > 1) { + const p1 = historyAvgZ[l - 2]; + const p2 = historyAvgZ[l - 1]; + mZ = (p2[1] - p1[1]) / (p2[0] - p1[0]); + historyAvgZ.shift(); + historySlopeZ.push([p2[0] - p1[0], mZ]); + } +} + +function isValidPushUp(p1, p2, t) { + if (p1 > 0 && p2 < 0) { + + if (lastZeroPassType == "-+") { + console.log(t, "Push up half complete..."); + + layout.progress.label = "..."; + layout.render(layout.progress); + } + + lastZeroPassType = "+-"; + lastZeroPassTime = t; + } + if (p2 > 0 && p1 < 0) { + + if (lastZeroPassType == "+-") { + // potential complete push up. Let's check the time difference... + const tDiffLastPushUp = t - lastPushUpCmpltTime; + const tDiffStart = t - tStart; + console.log(t, "Push up maybe complete?", Math.round(tDiffLastPushUp), Math.round(tDiffStart)); + + if ((lastPushUpCmpltTime <= 0 && tDiffStart >= thresholdPushUpTime) || tDiffLastPushUp >= thresholdPushUpTime) { + console.log(t, "Push up complete!!!"); + + lastPushUpCmpltTime = t; + pushUpCounter++; + + layout.count.label = pushUpCounter; + layout.render(layout.count); + layout.progress.label = ""; + layout.render(layout.progress); + + Bangle.buzz(100, 0.3); // TODO make configurable + } else { + console.log(t, "Push up to quick for threshold!"); + } + } + + lastZeroPassType = "-+"; + lastZeroPassTime = t; + } +} + +/* + +function calcPushUps() { + const l = historySlopeY.length; + for (let i = 1; i < l; i++) { + const p1 = historySlopeY[i - 1][1]; + const p2 = historySlopeY[i][1]; + const t = historySlopeY[i][0]; + isValidPushUp(p1, p2, t); + } +} +*/ + +function reset() { + historyY = []; + historyZ = []; + historyAvgY = []; + historyAvgZ = []; + historySlopeY = []; + historySlopeZ = []; + + lastZeroPassType = ""; + lastZeroPassTime = 0; + lastPushUpCmpltTime = 0; + pushUpCounter = 0; +} + + +function startRecording() { + if (recordActive) return; + g.clear(1); + reset(); + layout = new Layout({ + type: "v", + c: [{ + type: "txt", + id: "type", + font: "6x8:2", + label: exerciseType, + pad: 5 + }, + { + type: "txt", + id: "count", + font: "6x8:9", + label: pushUpCounter, + pad: 5, + bgCol: g.theme.bg + }, + { + type: "txt", + id: "progress", + font: "6x8:2", + label: "", + pad: 5 + }, + { + type: "txt", + id: "recording", + font: "6x8:2", + label: "RECORDING", + bgCol: "#f00", + pad: 5, + fillx: 1 + }, + ] + }, { + btns: [ + { + label: "STOP", + cb: () => { + stopRecording(); + } + } + ] + }); + layout.render(); + + Bangle.setPollInterval(80); // 12.5 Hz + Bangle.on('accel', accelHandler); + Bangle.buzz(200, 1); + tStart = new Date().getTime(); + recordActive = true; +} + +function stopRecording() { + if (!recordActive) return; + g.clear(1); + Bangle.removeListener('accel', accelHandler); + showMenu(); + console.log("Found " + pushUpCounter + " push ups!"); + recordActive = false; +} + +g.clear(1); +Bangle.drawWidgets(); +showMenu(); diff --git a/apps/banglexercise/app.png b/apps/banglexercise/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ee733206310029c15e866e4fc6bef39305d70668 GIT binary patch literal 690 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`z6LcLCBs@Y8vBJ&@uo z@Q5r1(g|SvA=~LZ0|Vn0PZ!6KjC*fqZuC0rAk)5ISX|cS=uz>4qEPnOCH@cIG_P3P zFGDWi zNGnrbNL|R7G5egyoLkxdBkfxbtgvmLFk!~jikVyTJQzLaunJ`CzH(+>eiw_(tM5ID zCxaKT>-gQeKjZ#PZ`Muf7F<@rVfw%ShRjWzrNDVa4Hc#BdJrKWAyK=c`uohKOXBRS z7i6&M&E|RIp%;De^8uL)tYJyZ&wgNABYF7x`R1LH8)UCF6mu=pVXB?2D!xxh=y%`w zvwFGbP5q~azSwr`;=Yw1ZzZe9Wl8+~?RF@-QMWVkzz65fv&UH090_!2JnVKjno+s% zjis=&wZJj^z|x7+T(&#?Q18sW|I6{rTaII}*{P}%1L#X5Imj9~9Jam|MPl%6IS=VU&;E=?<Hxi+V}Sty2#I1U-4_X;bU=*PM1S-^ErIH(Bc9_KZH+1;!FW?Xq@{?d#wat2 zqSPaenYJE9vqHzCE!82k>QzeX6>8#H&He5_^T(`p*S-JUv(`C#?X~wgd+)PNP;eAqEM1m z_SP0|asJCCgE3v@PMo1! z*f|v3#LSzFzS{I!!rV21lYAG;c$8rSY` zrtv;c`F-*otRXP}(DjxKjPZJTgeC>x=>7yoFGVWJH|o)xX+XB^NKHex&wPl`a~=~X za(P8aF({q6xR8)IKj56l)xcxv656}p+2QGn?xOha*4FfUv5E=1o8>8@ke5^X-Alra z{?c-FSdtCn#41uNW-umNx0VFrlWLa)#(@Ij$rUqQ#GSv%Zi@XPBg}gPCyKWp&p8}M z6HV+)2y3H)YaQ^Mom$c5vW71elfNdWFPDfG{GjOe_VwTz(o~)T+9W+c*{D*%tK9*e zoJ~|W+Yzo%+X)vnc0{$qEqCJk=RJ4*O9-72(B~Uzi@Es|RQEyjmgu4b^j7ydTi6Z= zTKjLeV4AP--+63`7du)}g~nr62CXHl@SB==)Cgk4oJWBwo1uoLt@$Jzoe zC8XG-fYql4>_h}+N$)&!8#8LB2x!D1B(J^4SIpD$WT!%~SBvItV#w}Bu^X$Pk*we;?6cI0@-D(Q=rGO_|R9q%_clHFEW>WP@3PJ2lX6b9WH_gJ#2oO{P>u;wA8BEkN?TPq z%^@Y+O-FLEsH-w~$0{or?;P%C_!V_W>sRIRoIl=dlz?e=`(Rq?PWbD0J3{ciJg-L; zxvZ9qEgR}wlM84&H3=%LDFm|q3jH11uV|iGtX+s^Gmi0ynq5i*$3#_ zg+J!w${3g@L`bGr#cC#`$MNK5&VPC>2@A?DFMX?x#pRoMyH9t@B^7G+?s{Oh_8X0G zdxIWdXd?sP7~p>UV*XhE!$E*z;+B&8<4Q^M4d>$>b`fhPoA)+AQJd17ewQEr$U-(Z zi?LMG(ytl$U}&R@dAxoWMG>q1h-<}t6VKQVm+sCMa7sP7#%G{990ok zZaza$U~;u;dV?Im+ikXvp0`kdvfQSWgVmHyrEgp=ZF(@0w^`k28dzQ*VFU;ceDdf(o`Cg>Bmg3o{v-Hn zd2^(E?-x3$-1VX;Tbvj|TjlUy#vZ*@U9b6V39?(t1J}IJ@v@K-((+>Q%!fY+P7m~> z)!xYJRn$*Dw4B`OQ+_C%Wykju9`})?N8H^aB$=bl;ZJN}EjlY>dhI#z)la1C1rXhwz1fgbiQh50C;I)_p2 z20-=scl5QzK*vM-t?q$nFKrt?G64N@O6e*^3d{M literal 0 HcmV?d00001