From 721df7cf286e798f99919524fd58b55f03a275b7 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 31 May 2023 21:47:16 +0200 Subject: [PATCH 1/3] Delete runplus (will readd soon) --- apps/runplus/ChangeLog | 22 ---- apps/runplus/README.md | 76 ------------- apps/runplus/app-icon.js | 1 - apps/runplus/app.js | 198 --------------------------------- apps/runplus/app.png | Bin 1479 -> 0 bytes apps/runplus/karvonen.js | 215 ------------------------------------ apps/runplus/metadata.json | 20 ---- apps/runplus/screenshot.png | Bin 3716 -> 0 bytes apps/runplus/settings.js | 157 -------------------------- 9 files changed, 689 deletions(-) delete mode 100644 apps/runplus/ChangeLog delete mode 100644 apps/runplus/README.md delete mode 100644 apps/runplus/app-icon.js delete mode 100644 apps/runplus/app.js delete mode 100644 apps/runplus/app.png delete mode 100644 apps/runplus/karvonen.js delete mode 100644 apps/runplus/metadata.json delete mode 100644 apps/runplus/screenshot.png delete mode 100644 apps/runplus/settings.js diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog deleted file mode 100644 index d920a3eca..000000000 --- a/apps/runplus/ChangeLog +++ /dev/null @@ -1,22 +0,0 @@ -0.01: New App! -0.02: Set pace format to mm:ss, time format to h:mm:ss, - added settings to opt out of GPS and HRM -0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2 -0.04: Use the exstats module, and make what is displayed configurable -0.05: exstats updated so update 'distance' label is updated, option for 'speed' -0.06: Add option to record a run using the recorder app automatically -0.07: Fix crash if an odd number of active boxes are configured (fix #1473) -0.08: Added support for notifications from exstats. Support all stats from exstats -0.09: Fix broken start/stop if recording not enabled (fix #1561) -0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578) -0.11: Notifications fixes -0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11 -0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) -0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working -0.15: Diverge from the standard "Run" app. Swipe to intensity interface a la Karvonen (curtesy of FTeacher at https://github.com/f-teacher) - Keep run state between runs (allowing you to exit and restart the app) -0.16: Don't clear zone 2b indicator segment when updating HRM reading. - Write to correct settings file, fixing settings not working. -0.17: Fix typo in variable name preventing starting a run. -0.18: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix - another typo. diff --git a/apps/runplus/README.md b/apps/runplus/README.md deleted file mode 100644 index 659cd964d..000000000 --- a/apps/runplus/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Run App - -This app allows you to display the status of your run, it -shows distance, time, steps, cadence, pace and more. - -To use it, start the app and press the middle button so that -the red `STOP` in the bottom right turns to a green `RUN`. - -## Display - -* `DIST` - the distance travelled based on the GPS (if you have a GPS lock). - * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As - the GPS updates your position as it gets more satellites your position changes and the distance - shown will increase, even if you are standing still. -* `TIME` - the elapsed time for your run -* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far** -* `HEART (BPM)` - Your current heart rate -* `Max BPM` - Your maximum heart rate reached during the run -* `STEPS` - Steps since you started exercising -* `CADENCE` - Steps per second based on your step rate *over the last minute* -* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically -so if you have no GPS lock you just need to wait. -* The current time is displayed right at the bottom of the screen -* `RUN/STOP` - whether the distance for your run is being displayed or not - -## Recording Tracks - -When the `Recorder` app is installed, `Run` will automatically start and stop tracks -as needed, prompting you to overwrite or begin a new track if necessary. - -## Settings - -Under `Settings` -> `App` -> `Run` you can change settings for this app. - -* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically -record GPS/HRM/etc data every time you start a run? -* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon -* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display. - Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Max BPM", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence". - Any box set to "-" will display no information. - * Box 1 is the top left (defaults to "Distance") - * Box 2 is the top right (defaults to "Time") - * Box 3 is the middle left (defaults to "Pace (avg)") - * Box 4 is the middle right (defaults to "Heart (BPM)") - * Box 5 is the bottom left (defaults to "Steps") - * Box 6 is the bottom right (defaults to "Cadence") -* `Notifications` leads to a submenu where you can configure if the app will notify you after -your distance, steps, or time repeatedly pass your configured thresholds - * `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options - * "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon" - * `Ntfy Steps`: The number of steps that must pass before you are notified. - * "Off" (default), 100, 500, 1000, 5000, 10000 - * `Ntfy Time`: The amount of time that must pass before you are notified. - * "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour" - * `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold - * `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold - * `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold - -## TODO - -* Keep a log of each run's stats (distance/steps/etc) - -## Development - -This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the -app loader, the module is automatically included in the app's source. However -when developing via the IDE the module won't get pulled in by default. - -There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) -## Contributors (Run and Run+) -gfwilliams -hughbarney -GrandVizierOlaf -BartS23 -f-teacher -thyttan diff --git a/apps/runplus/app-icon.js b/apps/runplus/app-icon.js deleted file mode 100644 index a97d1b8ce..000000000 --- a/apps/runplus/app-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/runplus/app.js b/apps/runplus/app.js deleted file mode 100644 index 7cb5d4381..000000000 --- a/apps/runplus/app.js +++ /dev/null @@ -1,198 +0,0 @@ -// Use widget utils to show/hide widgets -let wu = require("widget_utils"); - -let runInterval; -let karvonenActive = false; -// Run interface wrapped in a function -let ExStats = require("exstats"); -let B2 = process.env.HWVERSION===2; -let Layout = require("Layout"); -let locale = require("locale"); -let fontHeading = "6x8:2"; -let fontValue = B2 ? "6x15:2" : "6x8:3"; -let headingCol = "#888"; -let fixCount = 0; -let isMenuDisplayed = false; - -g.reset().clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -wu.show(); - -// --------------------------- -let settings = Object.assign({ - record: true, - B1: "dist", - B2: "time", - B3: "pacea", - B4: "bpm", - B5: "step", - B6: "caden", - paceLength: 1000, - notify: { - dist: { - value: 0, - notifications: [], - }, - step: { - value: 0, - notifications: [], - }, - time: { - value: 0, - notifications: [], - }, - }, - HRM: { - min: 55, - max: 185, - }, -}, require("Storage").readJSON("runplus.json", 1) || {}); -let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); -let exs = ExStats.getStats(statIDs, settings); -// --------------------------- - -function setStatus(running) { - layout.button.label = running ? "STOP" : "START"; - layout.status.label = running ? "RUN" : "STOP"; - layout.status.bgCol = running ? "#0f0" : "#f00"; - layout.render(); -} - -// Called to start/stop running -function onStartStop() { - let running = !exs.state.active; - let prepPromises = []; - // start/stop recording - // Do this first in case recorder needs to prompt for - // an overwrite before we start tracking exstats - if (settings.record && WIDGETS["recorder"]) { - if (running) { - isMenuDisplayed = true; - prepPromises.push( - WIDGETS["recorder"].setRecording(true).then(() => { - isMenuDisplayed = false; - layout.setUI(); // grab our input handling again - layout.forgetLazyState(); - layout.render(); - }) - ); - } else { - prepPromises.push( - WIDGETS["recorder"].setRecording(false) - ); - } - } - - if (!prepPromises.length) // fix for Promise.all bug in 2v12 - prepPromises.push(Promise.resolve()); - - Promise.all(prepPromises) - .then(() => { - if (running) { - exs.start(); - } else { - exs.stop(); - } - // if stopping running, don't clear state - // so we can at least refer to what we've done - setStatus(running); - }); -} - -let lc = []; -// Load stats in pair by pair -for (let i=0;ilayout[e.id].label = e.getString()); - if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); -} -// At the bottom put time/GPS state/etc -lc.push({ type:"h", filly:1, c:[ - {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, - {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, - {type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 } -]}); -// Now calculate the layout -let layout = new Layout( { - type:"v", c: lc -},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); -delete lc; -setStatus(exs.state.active); -layout.render(); - -function configureNotification(stat) { - stat.on('notify', (e)=>{ - settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { - return promise.then(function () { - return Bangle.buzz(buzzPattern[0], buzzPattern[1]); - }); - }, Promise.resolve()); - }); -} - -Object.keys(settings.notify).forEach((statType) => { - if (settings.notify[statType].increment > 0 && exs.stats[statType]) { - configureNotification(exs.stats[statType]); - } -}); - -// Handle GPS state change for icon -Bangle.on("GPS", function(fix) { - layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; - if (!fix.fix) return; // only process actual fixes - if (fixCount++ === 0) { - Bangle.buzz(); // first fix, does not need to respect quiet mode - } -}); - -// run() function used to switch between traditional run UI and karvonen UI -function run() { - wu.show(); - layout.lazy = false; - layout.render(); - layout.lazy = true; - // We always call ourselves once a second to update - if (!runInterval){ - runInterval = setInterval(function() { - layout.clock.label = locale.time(new Date(),1); - if (!isMenuDisplayed && !karvonenActive) layout.render(); - }, 1000); - } -} -run(); - -/////////////////////////////////////////////// -// Karvonen -/////////////////////////////////////////////// - -function stopRunUI() { - // stop updating and drawing the traditional run app UI - clearInterval(runInterval); - runInterval = undefined; - karvonenActive = true; -} - -function stopKarvonenUI() { - g.reset().clear(); - clearInterval(karvonenInterval); - karvonenInterval = undefined; - karvonenActive = false; -} - -let karvonenInterval; -// Define the function to go back and forth between the different UI's -function swipeHandler(LR,_) { - if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} - if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} -} -// Listen for swipes with the swipeHandler -Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/app.png b/apps/runplus/app.png deleted file mode 100644 index 7059b8b015e20039a96de8b65c8a6b68a5e51e18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1479 zcmV;&1vvVNP)b0~r9hI1hzN2C_Hz2bec46d zz32!1f7|Dr^Zft+eV+5aXV1bTJi@~b@gPX$zNip1Yz1>vN2mz2DMEkvlG@MN zqD@lE?a)(59(>u0=Kppec*EA5(*zQ(EG3+(+KI->CEFJ!ACP%5!I+F&=p{{UFEnH!o7WN}(7DfEF{6T=^qRTOGUUmDCINa`%K?dqCzyBw&^& zdx;n90#h)O*AZj`^*@L)UE%?ZC>&z+*w0Pj=Mr*u5X*l)p2LibbmVEnr6riETg&$lZ8)JSSrFFIvt>|1LfBuoykZR)&GJv4Nif!en;z zv+6Z;L-k`+==0YD>-(E?LK%{G3lE#^`JOB$P&RcWluLnM)#~Vk(_duC*`K_4L>V>S z)_4dF#+CBq(XN1$@$$f&f%ft_-N11%bdLd>txt@&{Fx`lWV(+H188fpdoMWEz{=c= zcS=M^psn$7;>Mk!^KP#d`utPCk6iBMM%oOs0tuJ37nQ16rohH)E z9ia`tY}y~?;W9hY+Ci=jL57GcSZ9mZX%$dBo+%nx{F~SG>u0QvN67NBtqYU0IzJ12 z!D%4xE@^d0`F6j&s+C&7&h>F)stg0CXcP_pe^xQKQS72jOY-(wh zZbX~G&?_#4KvUbwYbW!Gs64aVQ65`xr~;X3@&0)ixmMMvar5@_BseI*>bwB=Hsh4X zYQh$!?Cq~z?1DoTIA3l2sj5-q2GG>pLe-60r0n}JscF?|Pu=!AaWEs^-w{4*u-m|v z)cN}2e|5diF%6APc(zvE@zj|5xTt9M^e(*}a5ghB(al~?Yp}~eT3VX+_&;u68O^;r zFm#Zy>3N^}(KCW>&hyn7OqS~|H4F%<8>aciPOVnaXAj8ih*=;qfnl~5NeLTDd^m8~ zvnv)fz-)*f*%Y>B(}4>B-+uJwvah5;)T5WD&l4qTwmGbG5^Ni*$W&M4^}G>c@+r0 zm&L6n!K$N!Oe1_uD^fG^zOXju@RlWhrFCBC&eabb;-_mPZum831lFJkDuuov#6kU5 z4CF+`AmwiE&BPU?TrL(A+P>=nva%L?1T`4WzcBdbD0fZgQOG$4`W)s&T>c@z;lw?$ z*r#$R-lF#xBG~4P-1p~Z&LlR|e(BYTGQ71roRQk)24wckiNM_6L75_6I@Icn{4O=n ze($4X(0i|)Kwp^SEDqOJAUkp)FsXM?t`S20EwnAT>>nDL9jAl(3m`*#gmZ99% of HRR+minHR = serious risk of heart attack - let minzone2 = hrr * 0.6 + minhr; - let maxzone2 = hrr * 0.7 + minhr; - let maxzone3 = hrr * 0.8 + minhr; - let maxzone4 = hrr * 0.9 + minhr; - let maxzone5 = hrr * 0.99 + minhr; - - // HR data: large, readable, in the middle of the screen - function drawHR() { - g.setFontAlign(-1,0,0); - g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); - g.setColor(g.theme.fg); - g.setFont("Vector",50); - g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); - } - - function drawWaitHR() { - g.setColor(g.theme.fg); - // Waiting for HRM - g.setFontAlign(0,0,0); - g.setFont("Vector",50); - g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); - - // Waiting for current Zone - g.setFont("Vector",24); - g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); - - // waiting for upper and lower limit of current zone - g.setFont("Vector",20); - g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); - g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); - } - - //These functions call arcs to show different HR zones. - - //To shorten the code, I'll reference some letiables and reuse them. - let centreX = R.x + 0.5 * R.w; - let centreY = R.y + 0.5 * R.h; - let minRadius = 0.38 * R.h; - let maxRadius = 0.50 * R.h; - - //draw background image (dithered green zones)(I should draw different zones in different dithered colors) - const HRzones= require("graphics_utils"); - let minRadiusz = 0.44 * R.h; - let startAngle = HRzones.degreesToRadians(-88.5); - let endAngle = HRzones.degreesToRadians(268.5); - - function drawBgArc() { - g.setColor(g.theme.dark==false?0xC618:"#002200"); - HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); - } - - const zones = require("graphics_utils"); - //####### A function to simplify a bit the code ###### - function simplify (sA, eA, Z, currentZone, lastZone) { - let startAngle = zones.degreesToRadians(sA); - let endAngle = zones.degreesToRadians(eA); - if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); - else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); - g.setFont("Vector",24); - g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); - g.setFontAlign(0,0,0); - g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); - } - - function zoning (max, min) { // draw values of upper and lower limit of current zone - g.setFont("Vector",20); - g.setColor(g.theme.fg); - g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); - g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); - g.setFontAlign(0,0,0); - g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); - g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); - } - - function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color - g.setColor(g.theme.bg); - HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); - } - - function getZone(zone) { - drawBgArc(); - clearCurrentZone(); - if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} - if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} - if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} - if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} - if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} - if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} - if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} - if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} - if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} - if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} - if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} - if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} - if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} - } - - function getZoneAlert() { - const HRzonemax = require("graphics_utils"); - let centreX1,centreY1,maxRadius1 = 1; - let minRadius = 0.40 * R.h; - let startAngle1 = HRzonemax.degreesToRadians(-90); - let endAngle1 = HRzonemax.degreesToRadians(270); - g.setFont("Vector",38);g.setColor("#ff0000"); - HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); - g.drawString("ALERT", 26,66); - } - - //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. - let subZoneLast; - function drawZones() { - if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. - if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 - else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a - else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b - else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c - else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a - else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b - else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c - else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a - else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b - else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c - else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a - else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b - else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c - else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert - } - - function initDraw() { - drawArrows(); - if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();} - //drawZones(); - } - - let hrLast; - //h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. - function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. - hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. - hr = exsHrmStats.getValue(); - //if (h!=0) hr = h; - if (hr!=hrLast) { - drawHR(); - drawZones(); - } //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. - } - - initDraw(); - - // check for updates every second. - karvonenInterval = setInterval(function() { - if (!isMenuDisplayed && karvonenActive) updateUI(); - }, 1000); - - return karvonenInterval; -}) diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json deleted file mode 100644 index 60860dc07..000000000 --- a/apps/runplus/metadata.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "runplus", - "name": "Run+", - "version": "0.18", - "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", - "icon": "app.png", - "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", - "supports": ["BANGLEJS2"], - "screenshots": [{"url": "screenshot.png"}], - "readme": "README.md", - "storage": [ - {"name": "runplus.app.js", "url": "app.js"}, - {"name": "runplus.img", "url": "app-icon.js", "evaluate": true}, - {"name": "runplus.settings.js", "url": "settings.js"}, - {"name": "runplus_karvonen", "url": "karvonen.js"} - ], - "data": [ - {"name": "runplus.json"} - ] -} diff --git a/apps/runplus/screenshot.png b/apps/runplus/screenshot.png deleted file mode 100644 index 1a813f19dfe304e3d8bf711692fd2abf1013c391..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3716 zcma)9i8mD5AJ;TuD9kVjVXS4BC1Om*G9ocOV`TT9;vt4!cB3g0*-4fx8I{IZ$M(V) zq7Z|6WGORgB-^u&ERFfi?=SfM?m72dWt=rKSDd*uTr+%eqw4OIaxzdEo;5|cv`pDBSN47K|UJq=6VDzBkHWn*!;PlpF ze(^n}LDM%GMu;}M5I}j(GWAen6Un4xU5>-I1Dx#l>;Z{Cb7j|( zK<4L6>y{&Db9JC52kOi-}{Chnt6Ig+jZL(>VA`ls2Ey8Pr z5g^sQAZ*o1m(Vto)Z+Le8OKl6@3BpBwPY6bEyL(Q`O<}w+4^NdTV2%8y72;!hZNA~ z7e5!Cguz0u9-KA*OW;@CjDsB=R?KtX(W$m2Pvx*tiR%P@tJPcgjyRu_%KN-Z`7dnf9xz8B?mMSbYsWY$ z|DH%HG)k&{8R($3%9yHlny2>;l_#4B@At;kl=anmT@wvY{%ZH)iZ7^cMFZ6px|1Yg zb1ya^vEmFYaQ~Gc6CeBeow@CT2^;E8ZC`kZn$GO9|Vf ze?f%B`R>(U5;Fsxt@F(94D1X_aTMBG42no{a*+^|um0fr6#*HA0O~axgw%v{i~Gn5 z%02Wc*Kf85fv@=eY|eO2I1ODpIx{^8KPD41?X`$RNxNOt5&|Ct2}(i_R_5*k)J zrhRWRGxo0U8uqUHpH^(yB}QuJn>c3B)=f>Nyi6)) zOECeHxpTcTXV<(xwEb zL5K`^0C}h(kXu7@S)!O!?8U`7Pl_6cPWVjLk)lom+B7>(RjvIA_R~F%YcSB-v@x@@ zo{E$!+3*JZls|AoVBq3zHdp=$5cLS`VM7LhB9IgfSO>cieEb*>{Cw_r63#PFK>d_Geiezu=XmOLG9&hHfP!exIkoxY zu=Lu7Kvncq2B?F5C0 z{D1i+DS}UyX&d!9ejkpsod{l>J$TwvhFFasSSQPkFowWX5*(_FQP|JxzZK?LOcV5E zX~~8dj-F(O`rVy3P6gjS8u|#t7oqdOIXV&7Te5bH0+(BI62UQ)vg&QjqWVhPc^yY4 zrRUYvR)Bj}U7G9%6t!ZLxG>P|9aBt$Lkm?E08efy$6AtPvSxHz6D}Q9?={^jc7K*r z4&5>&fd5wdTS!ON-xx&ZXu$K@g$CP74GEv1ZmOa+S(1ZL&0gd z2wA44M^ee5V3>5YQCdwiorujC=%IR07&&Luc{Uk5p#o0>_qSu5UGM`mYg9plqT9k5Fm}m$@z|5W3Y&%BZz_t}4gunQ2dPQS0Ys-Jws1Af4#!lFjbGC3h$ZT>q z$>%JcvLGzd6(e&d1@u`l1kimf#+;zcH`&Ep;Z93=uAfnvj8rc(u{@y(Eh=D4!Y+C=>@{sS#;$nO zBe;o^+DZ0`Jmm7#Y?sguyYwCU(&r>&66>J>stxwllI=Wb(aSHh2K2S$LkQJ76TrDk zd5(p#Kg^YTp#FZFCdyO?ur#Lb?CBpzu3g%`;ty6`vlp+(o>f?e>y@Y*eIFYodI$mw zLF5;$P6`X(G=L(bmr&sLs1?6=sK+gE46(1}ZU#v!VP{1l1Lyqh=Jr1;`(IPq=V()# zY7Mrh#HE@25YZm`j9;9G>v|nruUC9Z#UGG}mNS=m z>8EXdN0Hn`mX}FkxkRd88+u!ECZ*!fsB}iJ!Lm&4MX^b5SJ4csi2S?Ltm$aVx805F zGs!LV4N2rF;F7ZGtN|)AJ3*y9qhf%e`PCm#-*o4BpCY96TsP$EONg{8A;IO91vsd_ zyC6~7Y*mX1Q6j5l>`cK`%%R_>z$oAc z{nAIY*HZF$;qcJvGESyoAuVcn`Tp&Yb-o;D7=08?eGkOjRj$jt0L6bA zwouU#--D%6gA3I*ybKPr+Z73ky6GjlyP=z@J2Q-W1hFDs|3Z_3MbJrVcISm|EgkSx z%#o7kjA(C2g5J$F2bF+t7|s&?%^Z=1zFK3c#L+lk?c20h*jLV|AJ0fDN;@p)mjX-I zx)`35$zn}cwL#_gUk&9jh^*@KP2q|Vwrhw4Vnw~CZ5p|Ak?>Y|A2aw~M?}KMH59^i zSZR_sak%ILj=h}sBL$=UkB(KRiw69X4 z+g3@Rmg66AH^Q{Pzowona#!&QJMkYAmviIum!RTbQzb{v9uCx#&24<4{*OP0sM_FN z^0IFQjCHDo7t|p|(+Bk3y=`Kx)u8r9|7s+-m`fGST>PpeB~wy5h&mC#cEsmGq)ngz zFO_6y=>(POfDggC#%KbX{i%y7Ssj(AoewgPKBvt`u_jGo*G@ek4#&$%9sHd1J7k;f zsxoe5O37SoPSzs`8~ofZ=D-1!$u$t_=L<9QWM2GXs=byV1e2twmJFzYG?>+jA*glEA|wtfm;ZnwmpG>`?G z6yps4eFN}1ZPCm#ivusQObV|16alTM(UWcQdJ1(ktbz3qO#0)|eNrMYGsVSM;-S#V z4;Kgw25E4<;+`jJ>M!AN;E?R?bhWA= zu8Q>QmqvsR*zjp*kg+>Ls(+t&g_#$jSDa1aQo@dQ(`geJV9WA z4_qe|J#kc_=XBxFT2AzD+uwLMxqrPl6+r{a16O}V46{AL6t%%z*i zU*q|ZKiWe83il}i`hXjPa_)rpovkqCjsL2m#{W{nUyJWPy*pfYy1Dn&JHpgTg=0a_ zYjGiKX*)6{I;Es}M569jLPI|f<{43aKv4^0%Rj(kC)AHEDuqeAxmHtte4qVDwV{}3 zqp%Q~Q*)rwkie6*LLvIRc`yT*`SBJY%*D64SP6-#b#1jWmy(1?ew+Jves z*3KaRLi6JQ_7gs$SFslk5g7rMHVp%Cslk@2RirS$?R4(!@hml*pD?%!4Yy&s=qBznd7RQ-OlBXj*JM}W|=r!{x0f1mfQs.id); - - // ...and overwrite them with any saved values - // This way saved values are preserved if a new version adds more settings - const storage = require('Storage') - let settings = Object.assign({ - record: true, - B1: "dist", - B2: "time", - B3: "pacea", - B4: "bpm", - B5: "step", - B6: "caden", - paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale - notify: { - dist: { - increment: 0, - notifications: [], - }, - step: { - increment: 0, - notifications: [], - }, - time: { - increment: 0, - notifications: [], - }, - }, - HRM: { - min: 55, - max: 185, - }, - }, storage.readJSON(SETTINGS_FILE, 1) || {}); - function saveSettings() { - storage.write(SETTINGS_FILE, settings) - } - - function getBoxChooser(boxID) { - return { - min: 0, max: statsIDs.length-1, - value: Math.max(statsIDs.indexOf(settings[boxID]),0), - format: v => statsList[v].name, - onchange: v => { - settings[boxID] = statsIDs[v]; - saveSettings(); - }, - } - } - - function sampleBuzz(buzzPatterns) { - return buzzPatterns.reduce(function (promise, buzzPattern) { - return promise.then(function () { - return Bangle.buzz(buzzPattern[0], buzzPattern[1]); - }); - }, Promise.resolve()); - } - - var menu = { - '': { 'title': 'Run' }, - '< Back': back, - }; - if (global.WIDGETS&&WIDGETS["recorder"]) - menu[/*LANG*/"Record Run"] = { - value : !!settings.record, - onchange : v => { - settings.record = v; - saveSettings(); - } - }; - var notificationsMenu = { - '< Back': function() { E.showMenu(menu) }, - } - menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; - ExStats.appendMenuItems(menu, settings, saveSettings); - ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings); - var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; - var vibTimes = [ - [], - [[100, 1]], - [[300, 1]], - [[300, 1], [300, 0], [300, 1]], - [[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]], - [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], - ]; - notificationsMenu[/*LANG*/"Dist Pattern"] = { - value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), - min: 0, max: vibTimes.length - 1, - format: v => vibPatterns[v]||/*LANG*/"Off", - onchange: v => { - settings.notify.dist.notifications = vibTimes[v]; - sampleBuzz(vibTimes[v]); - saveSettings(); - } - } - notificationsMenu[/*LANG*/"Step Pattern"] = { - value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), - min: 0, max: vibTimes.length - 1, - format: v => vibPatterns[v]||/*LANG*/"Off", - onchange: v => { - settings.notify.step.notifications = vibTimes[v]; - sampleBuzz(vibTimes[v]); - saveSettings(); - } - } - notificationsMenu[/*LANG*/"Time Pattern"] = { - value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), - min: 0, max: vibTimes.length - 1, - format: v => vibPatterns[v]||/*LANG*/"Off", - onchange: v => { - settings.notify.time.notifications = vibTimes[v]; - sampleBuzz(vibTimes[v]); - saveSettings(); - } - } - var boxMenu = { - '< Back': function() { E.showMenu(menu) }, - } - Object.assign(boxMenu,{ - 'Box 1': getBoxChooser("B1"), - 'Box 2': getBoxChooser("B2"), - 'Box 3': getBoxChooser("B3"), - 'Box 4': getBoxChooser("B4"), - 'Box 5': getBoxChooser("B5"), - 'Box 6': getBoxChooser("B6"), - }); - menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; - - var hrmMenu = { - '< Back': function() { E.showMenu(menu) }, - } - - menu[/*LANG*/"HRM min/max"] = function() { E.showMenu(hrmMenu)}; - hrmMenu[/*LANG*/"min"] = { - min: 1, max: 100, - value: settings.HRM.min, - format: w => w, - onchange: w => { - settings.HRM.min = w; - saveSettings(); - }, - } - hrmMenu[/*LANG*/"max"] = { - min: 101, max: 220, - value: settings.HRM.max, - format: v => v, - onchange: v => { - settings.HRM.max = v; - saveSettings(); - }, - } - E.showMenu(menu); -}) From 6822d8ed709b484c37118d28f31bd3da004f3df7 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 31 May 2023 21:58:19 +0200 Subject: [PATCH 2/3] Forking run to runplus (2nd time) --- apps/runplus/ChangeLog | 17 ++++ apps/runplus/README.md | 69 ++++++++++++++++ apps/runplus/app-icon.js | 1 + apps/runplus/app.js | 160 ++++++++++++++++++++++++++++++++++++ apps/runplus/app.png | Bin 0 -> 1479 bytes apps/runplus/metadata.json | 16 ++++ apps/runplus/screenshot.png | Bin 0 -> 3716 bytes apps/runplus/settings.js | 129 +++++++++++++++++++++++++++++ 8 files changed, 392 insertions(+) create mode 100644 apps/runplus/ChangeLog create mode 100644 apps/runplus/README.md create mode 100644 apps/runplus/app-icon.js create mode 100644 apps/runplus/app.js create mode 100644 apps/runplus/app.png create mode 100644 apps/runplus/metadata.json create mode 100644 apps/runplus/screenshot.png create mode 100644 apps/runplus/settings.js diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog new file mode 100644 index 000000000..e79696c78 --- /dev/null +++ b/apps/runplus/ChangeLog @@ -0,0 +1,17 @@ +0.01: New App! +0.02: Set pace format to mm:ss, time format to h:mm:ss, + added settings to opt out of GPS and HRM +0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2 +0.04: Use the exstats module, and make what is displayed configurable +0.05: exstats updated so update 'distance' label is updated, option for 'speed' +0.06: Add option to record a run using the recorder app automatically +0.07: Fix crash if an odd number of active boxes are configured (fix #1473) +0.08: Added support for notifications from exstats. Support all stats from exstats +0.09: Fix broken start/stop if recording not enabled (fix #1561) +0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578) +0.11: Notifications fixes +0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11 +0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) +0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working +0.15: Keep run state between runs (allowing you to exit and restart the app) +0.16: Added ability to resume a run that was stopped previously (fix #1907) \ No newline at end of file diff --git a/apps/runplus/README.md b/apps/runplus/README.md new file mode 100644 index 000000000..7f645b518 --- /dev/null +++ b/apps/runplus/README.md @@ -0,0 +1,69 @@ +# Run App + +This app allows you to display the status of your run, it +shows distance, time, steps, cadence, pace and more. + +To use it, start the app and press the middle button so that +the red `STOP` in the bottom right turns to a green `RUN`. + +## Display + +* `DIST` - the distance travelled based on the GPS (if you have a GPS lock). + * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As + the GPS updates your position as it gets more satellites your position changes and the distance + shown will increase, even if you are standing still. +* `TIME` - the elapsed time for your run +* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far** +* `HEART (BPM)` - Your current heart rate +* `Max BPM` - Your maximum heart rate reached during the run +* `STEPS` - Steps since you started exercising +* `CADENCE` - Steps per second based on your step rate *over the last minute* +* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically +so if you have no GPS lock you just need to wait. +* The current time is displayed right at the bottom of the screen +* `RUN/STOP` - whether the distance for your run is being displayed or not + +## Recording Tracks + +When the `Recorder` app is installed, `Run` will automatically start and stop tracks +as needed, prompting you to overwrite or begin a new track if necessary. + +## Settings + +Under `Settings` -> `App` -> `Run` you can change settings for this app. + +* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically +record GPS/HRM/etc data every time you start a run? +* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon +* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display. + Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Max BPM", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence". + Any box set to "-" will display no information. + * Box 1 is the top left (defaults to "Distance") + * Box 2 is the top right (defaults to "Time") + * Box 3 is the middle left (defaults to "Pace (avg)") + * Box 4 is the middle right (defaults to "Heart (BPM)") + * Box 5 is the bottom left (defaults to "Steps") + * Box 6 is the bottom right (defaults to "Cadence") +* `Notifications` leads to a submenu where you can configure if the app will notify you after +your distance, steps, or time repeatedly pass your configured thresholds + * `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options + * "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon" + * `Ntfy Steps`: The number of steps that must pass before you are notified. + * "Off" (default), 100, 500, 1000, 5000, 10000 + * `Ntfy Time`: The amount of time that must pass before you are notified. + * "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour" + * `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold + * `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold + * `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold + +## TODO + +* Keep a log of each run's stats (distance/steps/etc) + +## Development + +This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the +app loader, the module is automatically included in the app's source. However +when developing via the IDE the module won't get pulled in by default. + +There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) diff --git a/apps/runplus/app-icon.js b/apps/runplus/app-icon.js new file mode 100644 index 000000000..a97d1b8ce --- /dev/null +++ b/apps/runplus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/runplus/app.js b/apps/runplus/app.js new file mode 100644 index 000000000..507e8581a --- /dev/null +++ b/apps/runplus/app.js @@ -0,0 +1,160 @@ +var ExStats = require("exstats"); +var B2 = process.env.HWVERSION===2; +var Layout = require("Layout"); +var locale = require("locale"); +var fontHeading = "6x8:2"; +var fontValue = B2 ? "6x15:2" : "6x8:3"; +var headingCol = "#888"; +var fixCount = 0; +var isMenuDisplayed = false; + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// --------------------------- +let settings = Object.assign({ + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, + notify: { + dist: { + value: 0, + notifications: [], + }, + step: { + value: 0, + notifications: [], + }, + time: { + value: 0, + notifications: [], + }, + }, +}, require("Storage").readJSON("run.json", 1) || {}); +var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); +var exs = ExStats.getStats(statIDs, settings); +// --------------------------- + +function setStatus(running) { + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + layout.render(); +} + +// Called to start/stop running +function onStartStop() { + var running = !exs.state.active; + var shouldResume = false; + var promise = Promise.resolve(); + + if (running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume? + promise = promise. + then(() => { + isMenuDisplayed = true; + return E.showPrompt("Resume run?",{title:"Run"}); + }).then(r => { + isMenuDisplayed=false;shouldResume=r; + }); + } + + // start/stop recording + // Do this first in case recorder needs to prompt for + // an overwrite before we start tracking exstats + if (settings.record && WIDGETS["recorder"]) { + if (running) { + isMenuDisplayed = true; + promise = promise. + then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })). + then(() => { + isMenuDisplayed = false; + layout.setUI(); // grab our input handling again + layout.forgetLazyState(); + layout.render(); + }); + } else { + promise = promise.then( + () => WIDGETS["recorder"].setRecording(false) + ); + } + } + + promise = promise.then(() => { + if (running) { + if (shouldResume) + exs.resume() + else + exs.start(); + } else { + exs.stop(); + } + // if stopping running, don't clear state + // so we can at least refer to what we've done + setStatus(running); + }); +} + +var lc = []; +// Load stats in pair by pair +for (var i=0;ilayout[e.id].label = e.getString()); + if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); +} +// At the bottom put time/GPS state/etc +lc.push({ type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, + {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, + {type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 } +]}); +// Now calculate the layout +var layout = new Layout( { + type:"v", c: lc +},{lazy:true, btns:[{ label:"---", cb: onStartStop, id:"button"}]}); +delete lc; +setStatus(exs.state.active); +layout.render(); + +function configureNotification(stat) { + stat.on('notify', (e)=>{ + settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + }); +} + +Object.keys(settings.notify).forEach((statType) => { + if (settings.notify[statType].increment > 0 && exs.stats[statType]) { + configureNotification(exs.stats[statType]); + } +}); + +// Handle GPS state change for icon +Bangle.on("GPS", function(fix) { + layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; + if (!fix.fix) return; // only process actual fixes + if (fixCount++ === 0) { + Bangle.buzz(); // first fix, does not need to respect quiet mode + } +}); +// We always call ourselves once a second to update +setInterval(function() { + layout.clock.label = locale.time(new Date(),1); + if (!isMenuDisplayed) layout.render(); +}, 1000); diff --git a/apps/runplus/app.png b/apps/runplus/app.png new file mode 100644 index 0000000000000000000000000000000000000000..7059b8b015e20039a96de8b65c8a6b68a5e51e18 GIT binary patch literal 1479 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.id); + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = Object.assign({ + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale + notify: { + dist: { + increment: 0, + notifications: [], + }, + step: { + increment: 0, + notifications: [], + }, + time: { + increment: 0, + notifications: [], + }, + }, + }, storage.readJSON(SETTINGS_FILE, 1) || {}); + function saveSettings() { + storage.write(SETTINGS_FILE, settings) + } + + function getBoxChooser(boxID) { + return { + min: 0, max: statsIDs.length-1, + value: Math.max(statsIDs.indexOf(settings[boxID]),0), + format: v => statsList[v].name, + onchange: v => { + settings[boxID] = statsIDs[v]; + saveSettings(); + }, + } + } + + function sampleBuzz(buzzPatterns) { + return buzzPatterns.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + } + + var menu = { + '': { 'title': 'Run' }, + '< Back': back, + }; + if (global.WIDGETS&&WIDGETS["recorder"]) + menu[/*LANG*/"Record Run"] = { + value : !!settings.record, + onchange : v => { + settings.record = v; + saveSettings(); + } + }; + var notificationsMenu = { + '< Back': function() { E.showMenu(menu) }, + } + menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; + ExStats.appendMenuItems(menu, settings, saveSettings); + ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings); + var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; + var vibTimes = [ + [], + [[100, 1]], + [[300, 1]], + [[300, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], + ]; + notificationsMenu[/*LANG*/"Dist Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.dist.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Step Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.step.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Time Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.time.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + var boxMenu = { + '< Back': function() { E.showMenu(menu) }, + } + Object.assign(boxMenu,{ + 'Box 1': getBoxChooser("B1"), + 'Box 2': getBoxChooser("B2"), + 'Box 3': getBoxChooser("B3"), + 'Box 4': getBoxChooser("B4"), + 'Box 5': getBoxChooser("B5"), + 'Box 6': getBoxChooser("B6"), + }); + menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; + E.showMenu(menu); +}) From d5f114762f331237befbdedc39f4b079e8bd2dc3 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 22 Feb 2023 22:25:56 +0100 Subject: [PATCH 3/3] add changes to runplus changes to differentiate runplus from run app fix default settings fallback for HRM fixes to draw correctly and in a timely manner when swiping to the karvonnen ui depending on if we have hrm data or not Update ChangeLog small ui tweak run: Keep run state between runs (allowing you to exit and restart the app) tweak clearRect width to not clear the indicator segment bump version write to correct settings file add tag karvonen change spelling karvonnen -> karvonen in app.js, karvonen.js spelling karvonnen -> karvonen in metadata runplus - Fix typo in variable name preventing starting a run tweak and align HRM min/max defaults, change allowed min/max interval in settings bump version, fix typo follow the preferred metadata.json style Readd contributors to README.md Tweak ChangeLog --- apps/run/ChangeLog | 2 +- apps/runplus/ChangeLog | 9 +- apps/runplus/README.md | 7 ++ apps/runplus/app.js | 105 +++++++++++++----- apps/runplus/karvonen.js | 215 +++++++++++++++++++++++++++++++++++++ apps/runplus/metadata.json | 26 +++-- apps/runplus/settings.js | 30 +++++- 7 files changed, 352 insertions(+), 42 deletions(-) create mode 100644 apps/runplus/karvonen.js diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index e79696c78..ab2803ec6 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -14,4 +14,4 @@ 0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) 0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working 0.15: Keep run state between runs (allowing you to exit and restart the app) -0.16: Added ability to resume a run that was stopped previously (fix #1907) \ No newline at end of file +0.16: Added ability to resume a run that was stopped previously (fix #1907) diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog index e79696c78..05d24b96d 100644 --- a/apps/runplus/ChangeLog +++ b/apps/runplus/ChangeLog @@ -14,4 +14,11 @@ 0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) 0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working 0.15: Keep run state between runs (allowing you to exit and restart the app) -0.16: Added ability to resume a run that was stopped previously (fix #1907) \ No newline at end of file +0.16: Added ability to resume a run that was stopped previously (fix #1907) +0.17: Diverge from the standard "Run" app. Swipe to intensity interface a la Karvonen (curtesy of FTeacher at https://github.com/f-teacher) +0.18: Don't clear zone 2b indicator segment when updating HRM reading. +Write to correct settings file, fixing settings not working. +0.19: Fix typo in variable name preventing starting a run +0.20: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix + another typo. +0.21: Rebase on "Run" app ver. 0.16. diff --git a/apps/runplus/README.md b/apps/runplus/README.md index 7f645b518..659cd964d 100644 --- a/apps/runplus/README.md +++ b/apps/runplus/README.md @@ -67,3 +67,10 @@ app loader, the module is automatically included in the app's source. However when developing via the IDE the module won't get pulled in by default. There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) +## Contributors (Run and Run+) +gfwilliams +hughbarney +GrandVizierOlaf +BartS23 +f-teacher +thyttan diff --git a/apps/runplus/app.js b/apps/runplus/app.js index 507e8581a..41fab7ae2 100644 --- a/apps/runplus/app.js +++ b/apps/runplus/app.js @@ -1,16 +1,23 @@ -var ExStats = require("exstats"); -var B2 = process.env.HWVERSION===2; -var Layout = require("Layout"); -var locale = require("locale"); -var fontHeading = "6x8:2"; -var fontValue = B2 ? "6x15:2" : "6x8:3"; -var headingCol = "#888"; -var fixCount = 0; -var isMenuDisplayed = false; +// Use widget utils to show/hide widgets +let wu = require("widget_utils"); -g.clear(); +let runInterval; +let karvonenActive = false; +// Run interface wrapped in a function +let ExStats = require("exstats"); +let B2 = process.env.HWVERSION===2; +let Layout = require("Layout"); +let locale = require("locale"); +let fontHeading = "6x8:2"; +let fontValue = B2 ? "6x15:2" : "6x8:3"; +let headingCol = "#888"; +let fixCount = 0; +let isMenuDisplayed = false; + +g.reset().clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); +wu.show(); // --------------------------- let settings = Object.assign({ @@ -36,9 +43,13 @@ let settings = Object.assign({ notifications: [], }, }, -}, require("Storage").readJSON("run.json", 1) || {}); -var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); -var exs = ExStats.getStats(statIDs, settings); + HRM: { + min: 55, + max: 185, + }, +}, require("Storage").readJSON("runplus.json", 1) || {}); +let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); +let exs = ExStats.getStats(statIDs, settings); // --------------------------- function setStatus(running) { @@ -100,11 +111,11 @@ function onStartStop() { }); } -var lc = []; +let lc = []; // Load stats in pair by pair -for (var i=0;i{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); delete lc; setStatus(exs.state.active); layout.render(); @@ -132,16 +143,16 @@ layout.render(); function configureNotification(stat) { stat.on('notify', (e)=>{ settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { - return promise.then(function () { - return Bangle.buzz(buzzPattern[0], buzzPattern[1]); - }); + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); }, Promise.resolve()); }); } Object.keys(settings.notify).forEach((statType) => { if (settings.notify[statType].increment > 0 && exs.stats[statType]) { - configureNotification(exs.stats[statType]); + configureNotification(exs.stats[statType]); } }); @@ -153,8 +164,46 @@ Bangle.on("GPS", function(fix) { Bangle.buzz(); // first fix, does not need to respect quiet mode } }); -// We always call ourselves once a second to update -setInterval(function() { - layout.clock.label = locale.time(new Date(),1); - if (!isMenuDisplayed) layout.render(); -}, 1000); + +// run() function used to switch between traditional run UI and karvonen UI +function run() { + wu.show(); + layout.lazy = false; + layout.render(); + layout.lazy = true; + // We always call ourselves once a second to update + if (!runInterval){ + runInterval = setInterval(function() { + layout.clock.label = locale.time(new Date(),1); + if (!isMenuDisplayed && !karvonenActive) layout.render(); + }, 1000); + } +} +run(); + +/////////////////////////////////////////////// +// Karvonen +/////////////////////////////////////////////// + +function stopRunUI() { + // stop updating and drawing the traditional run app UI + clearInterval(runInterval); + runInterval = undefined; + karvonenActive = true; +} + +function stopKarvonenUI() { + g.reset().clear(); + clearInterval(karvonenInterval); + karvonenInterval = undefined; + karvonenActive = false; +} + +let karvonenInterval; +// Define the function to go back and forth between the different UI's +function swipeHandler(LR,_) { + if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} + if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} +} +// Listen for swipes with the swipeHandler +Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/karvonen.js b/apps/runplus/karvonen.js new file mode 100644 index 000000000..de81494bb --- /dev/null +++ b/apps/runplus/karvonen.js @@ -0,0 +1,215 @@ +(function karvonen(hrmSettings, exsHrmStats) { + //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ + //The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. + //Other methods are even more approximative. + let wu = require("widget_utils"); + wu.hide(); + let R = Bangle.appRect; + + + g.reset().clearRect(R).setFontAlign(0,0,0); + + const x = "x"; const y = "y"; + function Rdiv(axis, divisor) { // Used when placing things on the screen + return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor); + } + let linePoints = { //Not lists of points, but used to update points in the drawArrows function. + x: [ + 175/40, + 2, + 175/135, + ], + y: [ + 175/64, + 175/52, + 175/110, + 175/122, + ], + + }; + + function drawArrows() { + g.setColor(g.theme.fg); + // Upper + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0])); + // Lower + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2])); + } + + //To calculate Heart rate zones, we need to know the heart rate reserve (HRR) + // HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr. + //get the hrr (heart rate reserve). + // I put random data here, but this has to come as a menu in the settings section so that users can change it. + let minhr = hrmSettings.min; + let maxhr = hrmSettings.max; + + function calculatehrr(minhr, maxhr) { + return maxhr - minhr;} + + //test input for hrr (it works). + let hrr = calculatehrr(minhr, maxhr); + console.log(hrr); + + //Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input. + let hr = exsHrmStats.getValue(); + let hr1 = hr; + // These letiables display next and previous HR zone. + //get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method + //60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack + let minzone2 = hrr * 0.6 + minhr; + let maxzone2 = hrr * 0.7 + minhr; + let maxzone3 = hrr * 0.8 + minhr; + let maxzone4 = hrr * 0.9 + minhr; + let maxzone5 = hrr * 0.99 + minhr; + + // HR data: large, readable, in the middle of the screen + function drawHR() { + g.setFontAlign(-1,0,0); + g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); + g.setColor(g.theme.fg); + g.setFont("Vector",50); + g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); + } + + function drawWaitHR() { + g.setColor(g.theme.fg); + // Waiting for HRM + g.setFontAlign(0,0,0); + g.setFont("Vector",50); + g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); + + // Waiting for current Zone + g.setFont("Vector",24); + g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); + + // waiting for upper and lower limit of current zone + g.setFont("Vector",20); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); + } + + //These functions call arcs to show different HR zones. + + //To shorten the code, I'll reference some letiables and reuse them. + let centreX = R.x + 0.5 * R.w; + let centreY = R.y + 0.5 * R.h; + let minRadius = 0.38 * R.h; + let maxRadius = 0.50 * R.h; + + //draw background image (dithered green zones)(I should draw different zones in different dithered colors) + const HRzones= require("graphics_utils"); + let minRadiusz = 0.44 * R.h; + let startAngle = HRzones.degreesToRadians(-88.5); + let endAngle = HRzones.degreesToRadians(268.5); + + function drawBgArc() { + g.setColor(g.theme.dark==false?0xC618:"#002200"); + HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + } + + const zones = require("graphics_utils"); + //####### A function to simplify a bit the code ###### + function simplify (sA, eA, Z, currentZone, lastZone) { + let startAngle = zones.degreesToRadians(sA); + let endAngle = zones.degreesToRadians(eA); + if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); + else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + g.setFont("Vector",24); + g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); + g.setFontAlign(0,0,0); + g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); + } + + function zoning (max, min) { // draw values of upper and lower limit of current zone + g.setFont("Vector",20); + g.setColor(g.theme.fg); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); + g.setFontAlign(0,0,0); + g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); + g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); + } + + function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color + g.setColor(g.theme.bg); + HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); + } + + function getZone(zone) { + drawBgArc(); + clearCurrentZone(); + if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} + if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} + if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} + if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} + if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} + if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} + if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} + if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} + if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} + if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} + if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} + if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} + if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} + } + + function getZoneAlert() { + const HRzonemax = require("graphics_utils"); + let centreX1,centreY1,maxRadius1 = 1; + let minRadius = 0.40 * R.h; + let startAngle1 = HRzonemax.degreesToRadians(-90); + let endAngle1 = HRzonemax.degreesToRadians(270); + g.setFont("Vector",38);g.setColor("#ff0000"); + HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); + g.drawString("ALERT", 26,66); + } + + //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. + let subZoneLast; + function drawZones() { + if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. + if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 + else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a + else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b + else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c + else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a + else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b + else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c + else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a + else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b + else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c + else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a + else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b + else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c + else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert + } + + function initDraw() { + drawArrows(); + if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();} + //drawZones(); + } + + let hrLast; + //h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. + function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. + hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. + hr = exsHrmStats.getValue(); + //if (h!=0) hr = h; + if (hr!=hrLast) { + drawHR(); + drawZones(); + } //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. + } + + initDraw(); + + // check for updates every second. + karvonenInterval = setInterval(function() { + if (!isMenuDisplayed && karvonenActive) updateUI(); + }, 1000); + + return karvonenInterval; +}) diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json index ed253a319..40256e595 100644 --- a/apps/runplus/metadata.json +++ b/apps/runplus/metadata.json @@ -1,16 +1,20 @@ -{ "id": "run", - "name": "Run", - "version":"0.16", - "description": "Displays distance, time, steps, cadence, pace and more for runners.", +{ + "id": "runplus", + "name": "Run+", + "version": "0.21", + "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", "icon": "app.png", - "tags": "run,running,fitness,outdoors,gps", - "supports" : ["BANGLEJS","BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}], + "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", + "supports": ["BANGLEJS2"], + "screenshots": [{"url": "screenshot.png"}], "readme": "README.md", "storage": [ - {"name":"run.app.js","url":"app.js"}, - {"name":"run.img","url":"app-icon.js","evaluate":true}, - {"name":"run.settings.js","url":"settings.js"} + {"name": "runplus.app.js", "url": "app.js"}, + {"name": "runplus.img", "url": "app-icon.js", "evaluate": true}, + {"name": "runplus.settings.js", "url": "settings.js"}, + {"name": "runplus_karvonen", "url": "karvonen.js"} ], - "data": [{"name":"run.json"}] + "data": [ + {"name": "runplus.json"} + ] } diff --git a/apps/runplus/settings.js b/apps/runplus/settings.js index 0312200a3..539391a27 100644 --- a/apps/runplus/settings.js +++ b/apps/runplus/settings.js @@ -1,5 +1,5 @@ (function(back) { - const SETTINGS_FILE = "run.json"; + const SETTINGS_FILE = "runplus.json"; var ExStats = require("exstats"); var statsList = ExStats.getList(); statsList.unshift({name:"-",id:""}); // add blank menu item @@ -31,6 +31,10 @@ notifications: [], }, }, + HRM: { + min: 55, + max: 185, + }, }, storage.readJSON(SETTINGS_FILE, 1) || {}); function saveSettings() { storage.write(SETTINGS_FILE, settings) @@ -125,5 +129,29 @@ 'Box 6': getBoxChooser("B6"), }); menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; + + var hrmMenu = { + '< Back': function() { E.showMenu(menu) }, + } + + menu[/*LANG*/"HRM min/max"] = function() { E.showMenu(hrmMenu)}; + hrmMenu[/*LANG*/"min"] = { + min: 1, max: 100, + value: settings.HRM.min, + format: w => w, + onchange: w => { + settings.HRM.min = w; + saveSettings(); + }, + } + hrmMenu[/*LANG*/"max"] = { + min: 101, max: 220, + value: settings.HRM.max, + format: v => v, + onchange: v => { + settings.HRM.max = v; + saveSettings(); + }, + } E.showMenu(menu); })