diff --git a/apps.json b/apps.json index c63fec72a..14e12c164 100644 --- a/apps.json +++ b/apps.json @@ -942,12 +942,13 @@ { "id": "widlock", "name": "Lock Widget", - "version": "0.03", + "version": "0.04", "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", "icon": "widget.png", "type": "widget", "tags": "widget,lock", "supports": ["BANGLEJS","BANGLEJS2"], + "sortorder": -1, "storage": [ {"name":"widlock.wid.js","url":"widget.js"} ] @@ -5594,7 +5595,7 @@ { "id": "banglexercise", "name": "BanglExercise", "shortName":"BanglExercise", - "version":"0.01", + "version":"0.02", "description": "Can automatically track exercises while wearing the Bangle.js watch.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], @@ -5618,7 +5619,7 @@ "shortName":"Simple Pedometer", "icon": "screenshot_widpa.png", "screenshots": [{"url":"screenshot_widpa.png"}], - "version":"0.01", + "version":"0.02", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", @@ -5634,7 +5635,7 @@ "shortName":"Lato Pedometer", "icon": "screenshot_widpb.png", "screenshots": [{"url":"screenshot_widpb.png"}], - "version":"0.01", + "version":"0.02", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", @@ -5660,5 +5661,20 @@ {"name":"timeandlife.app.js","url":"app.js"}, {"name":"timeandlife.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "acmaze", + "name": "AccelaMaze", + "shortName":"AccelaMaze", + "version":"0.01", + "description": "Tilt the watch to roll a ball through a maze", + "icon": "app.png", + "tags": "game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"screenshot.png"}], + "storage": [ + {"name":"acmaze.app.js","url":"app.js"}, + {"name":"acmaze.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/acmaze/ChangeLog b/apps/acmaze/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/acmaze/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/acmaze/README.md b/apps/acmaze/README.md new file mode 100644 index 000000000..4724eea3e --- /dev/null +++ b/apps/acmaze/README.md @@ -0,0 +1,17 @@ +# AccelaMaze + +Tilt the watch to roll a ball through a maze. + +![Screenshot](screenshot.png) + +## Usage + +* Use the menu to select difficulty level (or exit). +* Wait until the maze gets generated and a red ball appears. +* Tilt the watch to get the ball into the green cell. + +At any time you can click the button to return to the menu. + +## Creator + +[Nimrod Kerrett](https://zzzen.com) diff --git a/apps/acmaze/app-icon.js b/apps/acmaze/app-icon.js new file mode 100644 index 000000000..8bd043b8b --- /dev/null +++ b/apps/acmaze/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwggaXh3M53/AA3yl4IHn//+EM5nMAoIX/C4RfCC4szmcxC4QFBAAUxC4UPAwIOB+YCCiMRkAFCkIGBAAQfBC4IUEAQhHIAAQX/C5EDmcyCgUTAoYXDR4kzC4UBPoKVB+YFFAQSPBiAKBiCnDGoZECABDUCa4YX/C5qPBQwoXGkczmC/FQYSSCVQSSCEwQOCC4hKFX4QXCd5YX/C4qMEmQXITAinDPoIADTwSPFkKMBX47RGI47XIC/4XCgZ9DQYYABmKYBmIXFkczmEBRIK/CQYQIBkECSoiSCA4MQa5pEFd6IX/RgMyC6H/QASVCRIS/EAQrXFJQoX/C6kDRQIXCiYFD+QFBmIUCkYFD+CJBiSPCRwIFFSoQFCiF3u9wI4gAO+wXW+IXygAAW")) diff --git a/apps/acmaze/app.js b/apps/acmaze/app.js new file mode 100644 index 000000000..53a851b5e --- /dev/null +++ b/apps/acmaze/app.js @@ -0,0 +1,276 @@ +const MARGIN = 25; +const WALL_RIGHT = 1, WALL_DOWN = 2; +const STATUS_GENERATING = 0, STATUS_PLAYING = 1, + STATUS_SOLVED = 2, STATUS_ABORTED = -1; + +function Maze(n) { + this.n = n; + this.status = STATUS_GENERATING; + this.wall_length = Math.floor((g.getHeight()-2*MARGIN)/n); + this.total_length = this.wall_length*n; + this.margin = Math.floor((g.getHeight()-this.total_length)/2); + this.ball_x = 0; + this.ball_y = 0; + this.clearScreen = function() { + g.clearRect( + 0, this.margin, + g.getWidth(), this.margin+this.total_length + ); + }; + this.clearScreen(); + g.setColor(g.theme.fg); + for (let i=0; i<=n; i++) { + g.drawRect( + this.margin, this.margin+i*this.wall_length, + g.getWidth()-this.margin, this.margin+i*this.wall_length + ); + g.drawRect( + this.margin+i*this.wall_length, this.margin, + this.margin+i*this.wall_length, g.getHeight() - this.margin + ); + } + this.walls = new Uint8Array(n*n); + this.groups = new Uint8Array(n*n); + for (let cell = 0; cell0 && !(this.walls[n*(ball_r-1)+ball_c]&WALL_DOWN)) { + next_y--; + } else if (dy>0 && ball_r<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_DOWN)) { + next_y++; + } else if (dx<0 && ball_c>0 && !(this.walls[n*ball_r+ball_c-1]&WALL_RIGHT)) { + next_x--; + } else if (dx>0 && ball_c<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_RIGHT)) { + next_x++; + } else { + return false; + } + } + this.clearCell(ball_r, ball_c); + if (this.ball_x%this.wall_length) { + this.clearCell(ball_r, ball_c+1); + } + if (this.ball_y%this.wall_length) { + this.clearCell(ball_r+1, ball_c); + } + this.ball_x = next_x; + this.ball_y = next_y; + this.drawBall(this.ball_x, this.ball_y); + if (this.ball_x==(n-1)*this.wall_length && this.ball_y==(n-1)*this.wall_length) { + this.status = STATUS_SOLVED; + } + return true; + }; + this.try_move_horizontally = function(accel_x) { + if (accel_x>0.15) { + return this.move(-1, 0); + } else if (accel_x<-0.15) { + return this.move(1, 0); + } + return false; + }; + this.try_move_vertically = function(accel_y) { + if (accel_y<-0.15) { + return this.move(0,1); + } else if (accel_y>0.15) { + return this.move(0,-1); + } + return false; + }; + this.tick = function() { + accel = Bangle.getAccel(); + if (this.ball_x%this.wall_length) { + this.try_move_horizontally(accel.x); + } else if (this.ball_y%this.wall_length) { + this.try_move_vertically(accel.y); + } else { + if (Math.abs(accel.x)>Math.abs(accel.y)) { // prefer horizontally + if (!this.try_move_horizontally(accel.x)) { + this.try_move_vertically(accel.y); + } + } else { // prefer vertically + if (!this.try_move_vertically(accel.y)) { + this.try_move_horizontally(accel.x); + } + } + } + }; + this.clearCell(0,0); + this.clearCell(n-1,n-1); + this.drawBall(0,0); + this.status = STATUS_PLAYING; +} + +function timeToText(t) { // Courtesy of stopwatch app + let hrs = Math.floor(t/3600000); + let mins = Math.floor(t/60000)%60; + let secs = Math.floor(t/1000)%60; + let tnth = Math.floor(t/100)%10; + let text; + + if (hrs === 0) + text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth; + else + text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + return text; +} + +let aborting = false; +let start_time = 0; +let duration = 0; +let maze=null; +let mazeMenu = { + "": { "title": "Maze size", "selected": 1 }, + "Easy (8x8)": function() { E.showMenu(); maze = new Maze(8); }, + "Medium (10x10)": function() { E.showMenu(); maze = new Maze(10); }, + "Hard (14x14)": function() { E.showMenu(); maze = new Maze(14); }, + "< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock +}; + +g.clear(true); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setLocked(false); +Bangle.setLCDTimeout(0); +E.showMenu(mazeMenu); +let maze_interval = setInterval( + function() { + if (maze) { + if (digitalRead(BTN1) || maze.status==STATUS_ABORTED) { + console.log(`aborting ${start_time}`); + maze = null; + start_time = duration = 0; + aborting = false; + setTimeout(function() {E.showMenu(mazeMenu); }, 100); + return; + } + if (!start_time) { + start_time = Date.now(); + } + if (maze.status==STATUS_PLAYING) { + maze.tick(); + } + if (maze.status==STATUS_SOLVED && !duration) { + duration = Date.now()-start_time; + g.setFontAlign(0,0).setColor(g.theme.fg); + g.setFont("Vector",18); + g.drawString(`Solved in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true); + } + } + }, 25); diff --git a/apps/acmaze/app.png b/apps/acmaze/app.png new file mode 100644 index 000000000..0d96448b1 Binary files /dev/null and b/apps/acmaze/app.png differ diff --git a/apps/acmaze/screenshot.png b/apps/acmaze/screenshot.png new file mode 100644 index 000000000..4b7217b97 Binary files /dev/null and b/apps/acmaze/screenshot.png differ diff --git a/apps/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog index 5560f00bc..5f1d3bd7d 100644 --- a/apps/banglexercise/ChangeLog +++ b/apps/banglexercise/ChangeLog @@ -1 +1,4 @@ 0.01: New App! +0.02: Add sit ups + Add more feedback to the user about the exercises + Clean up code diff --git a/apps/banglexercise/README.md b/apps/banglexercise/README.md index 28b276a59..c9f9ec38a 100644 --- a/apps/banglexercise/README.md +++ b/apps/banglexercise/README.md @@ -2,7 +2,7 @@ Can automatically track exercises while wearing the Bangle.js watch. -Currently only push ups and curls are supported. +Currently only push ups, curls and sit ups are supported. ## Disclaimer @@ -23,7 +23,7 @@ Press stop to end your exercise. ## TODO * Add other exercise types: * Rope jumps - * Sit ups + * Star jumps * ... * Save exercise summaries to file system * Configure daily goal for exercises diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js index 0d5c814bf..bc6e35f07 100644 --- a/apps/banglexercise/app.js +++ b/apps/banglexercise/app.js @@ -25,22 +25,32 @@ let exerciseType = { const exerciseTypes = [{ "id": "pushup", "name": "push ups", - "useYaxe": true, - "useZaxe": false, - "thresholdY": 2500, - "thresholdMinTime": 1400, // mininmal time between two push ups in ms + "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": 700, // mininmal duration of half a push ups in ms + "thresholdMinDurationTime": 600, // mininmal duration of half a push up in ms }, { "id": "curl", "name": "curls", - "useYaxe": true, - "useZaxe": false, - "thresholdY": 2500, - "thresholdMinTime": 1000, // mininmal time between two curls in ms + "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 push ups 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; @@ -66,7 +76,7 @@ function showMainMenu() { }; exerciseTypes.forEach(function(et) { - menu["Do " + et.name] = function() { + menu[et.name] = function() { exerciseType = et; E.showMenu(); startTraining(); @@ -81,8 +91,8 @@ function showMainMenu() { value: exerciseCounter + " " + exerciseType.name }; } - menu.Exit = function() { - load(); + menu.exit = function() { + load(); }; E.showMenu(menu); @@ -91,11 +101,11 @@ function showMainMenu() { function accelHandler(accel) { if (!exerciseType) return; const t = Math.round(new Date().getTime()); // time in ms - const y = exerciseType.useYaxe ? accel.y * 8192 : 0; - const z = exerciseType.useZaxe ? accel.z * 8192 : 0; + const y = exerciseType.useYaxis ? accel.y * 8192 : 0; + const z = exerciseType.useZaxis ? accel.z * 8192 : 0; //console.log(t, y, z); - if (exerciseType.useYaxe) { + if (exerciseType.useYaxis) { while (historyY.length > avgSize) historyY.shift(); @@ -109,7 +119,7 @@ function accelHandler(accel) { } } - if (exerciseType.useYaxe) { + if (exerciseType.useZaxis) { while (historyZ.length > avgSize) historyZ.shift(); @@ -124,72 +134,64 @@ function accelHandler(accel) { } // slope for Y - if (exerciseType.useYaxe) { + 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 - switch (exerciseType.id) { - case "pushup": - isValidYAxisExercise(slopeY, t); - break; - case "curl": - isValidYAxisExercise(slopeY, t); - break; - } - + isValidExercise(slopeY, t); } } // slope for Z - if (exerciseType.useZaxe) { + if (exerciseType.useZaxis) { l = historyAvgZ.length; if (l > 1) { const p1 = historyAvgZ[l - 2]; const p2 = historyAvgZ[l - 1]; - const slopeZ = (p2[1] - p1[1]) / (p2[0] - p1[0]); - historyAvgZ.shift(); - historySlopeZ.push([p2[0] - p1[0], slopeZ]); - - // TODO: we can use this data for some exercises which can be detected by using Z axis data + 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); } } } /* - * Check if slope value of Y-axis data looks like an exercise + * 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 Y threshold for the current 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 isValidYAxisExercise(slopeY, t) { +function isValidExercise(slope, t) { if (!exerciseType) return; - const thresholdY = exerciseType.thresholdY; + 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(slopeY) >= thresholdY) { - historyAvgY.shift(); - historySlopeY.push([t, slopeY]); - //console.log(t, Math.abs(slopeY)); - const lSlopeY = historySlopeY.length; - if (lSlopeY > 1) { - const p1 = historySlopeY[lSlopeY - 1][1]; - const p2 = historySlopeY[lSlopeY - 2][1]; + 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..."); + console.log(t, exerciseName + " half complete..."); layout.progress.label = "½"; + layout.recording.label = "TRAINING"; g.clear(); layout.render(); } @@ -201,7 +203,7 @@ function isValidYAxisExercise(slopeY, t) { if (lastZeroPassCameFromPositive == true) { const tDiffLastExercise = t - lastExerciseCompletionTime; const tDiffStart = t - tStart; - //console.log(t, exerciseName + " maybe complete?", Math.round(tDiffLastExercise), Math.round(tDiffStart)); + console.log(t, exerciseName + " maybe complete?", Math.round(tDiffLastExercise), Math.round(tDiffStart)); // check minimal time between exercises: if ((lastExerciseCompletionTime <= 0 && tDiffStart >= thresholdMinTime) || tDiffLastExercise >= thresholdMinTime) { @@ -219,22 +221,36 @@ function isValidYAxisExercise(slopeY, t) { layout.count.label = exerciseCounter; layout.progress.label = ""; + layout.recording.label = "Good!"; + g.clear(); layout.render(); if (settings.buzz) - Bangle.buzz(100, 0.4); + Bangle.buzz(200, 0.5); } else { - //console.log(t, exerciseName + " to quick for duration time threshold!"); + 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 + " to slow for time threshold!"); + 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 + " to quick for time threshold!"); + console.log(t, exerciseName + " too quick for time threshold!"); // thresholdMinTime lastExerciseCompletionTime = t; + + layout.recording.label = "Go slower!"; + g.clear(); + layout.render(); } } @@ -267,6 +283,7 @@ function startTraining() { if (recordActive) return; g.clear(1); reset(); + Bangle.setLCDTimeout(0); // force LCD on Bangle.setHRMPower(1, "banglexercise"); if (!hrtValue) hrtValue = "..."; @@ -285,7 +302,7 @@ function startTraining() { type: "txt", id: "count", font: exerciseCounter < 100 ? "6x8:9" : "6x8:8", - label: 10, + label: exerciseCounter, pad: 5 }, { @@ -337,11 +354,16 @@ function startTraining() { layout.render(); Bangle.setPollInterval(80); // 12.5 Hz - Bangle.on('accel', accelHandler); + tStart = new Date().getTime(); recordActive = true; if (settings.buzz) Bangle.buzz(200, 1); + + // delay start a little bit + setTimeout(() => { + Bangle.on('accel', accelHandler); + }, 1000); } function stopTraining() { diff --git a/apps/widlock/ChangeLog b/apps/widlock/ChangeLog index 3b1436feb..8aeb75429 100644 --- a/apps/widlock/ChangeLog +++ b/apps/widlock/ChangeLog @@ -1,3 +1,4 @@ 0.01: First commit 0.02: Handle new firmwares with 'lock' event 0.03: Don't try to be fancy - just bail out on firmwares without a lock event +0.04: Set sortorder to -1 so that widget always takes up the furthest left position diff --git a/apps/widpa/ChangeLog b/apps/widpa/ChangeLog index 7b83706bf..5197bb4bd 100644 --- a/apps/widpa/ChangeLog +++ b/apps/widpa/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Size widget after step count is reset diff --git a/apps/widpa/widpa.wid.js b/apps/widpa/widpa.wid.js index b8c439d2b..1c0f27394 100644 --- a/apps/widpa/widpa.wid.js +++ b/apps/widpa/widpa.wid.js @@ -6,7 +6,7 @@ WIDGETS["widpa"]={area:"tl",width:13,draw:function() { if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off var steps = Bangle.getHealthStatus("day").steps; var w = 1 + (steps.toString().length)*12; - if (w > this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} + if (w != this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} g.reset(); g.setColor(g.theme.bg); g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); diff --git a/apps/widpb/ChangeLog b/apps/widpb/ChangeLog index 7b83706bf..1409a81ff 100644 --- a/apps/widpb/ChangeLog +++ b/apps/widpb/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Fixed widget id to wibpb, Size widget after step count is reset diff --git a/apps/widpb/widpb.wid.js b/apps/widpb/widpb.wid.js index d65d7fe43..6129fac51 100644 --- a/apps/widpb/widpb.wid.js +++ b/apps/widpb/widpb.wid.js @@ -1,13 +1,13 @@ // on.step version Bangle.on('step', function(s) { WIDGETS["bata"].draw(); }); Bangle.on('lcdPower', function(on) { - if (on) WIDGETS["bata"].draw(); + if (on) WIDGETS["widpb"].draw(); }); -WIDGETS["bata"]={area:"tl",width:13,draw:function() { +WIDGETS["widpb"]={area:"tl",width:13,draw:function() { if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off var steps = Bangle.getHealthStatus("day").steps; var w = 1 + (steps.toString().length)*12; - if (w > this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} + if (w != this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} g.reset(); g.setColor(g.theme.bg); g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background