From 36a50d1ceec0df236b7c874f61a43c150cb8e726 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 9 Sep 2024 21:43:56 +0100 Subject: [PATCH 01/26] Layout: fix differing numeric types With a slight hack --- typescript/types/layout.d.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/typescript/types/layout.d.ts b/typescript/types/layout.d.ts index 55ddd7135..4329d0831 100644 --- a/typescript/types/layout.d.ts +++ b/typescript/types/layout.d.ts @@ -7,7 +7,16 @@ type ExtractIds = [Depth] extends [never] ? never : (T extends { id?: infer Id extends string } - ? { [k in Id]: { -readonly [P in keyof T]: T[P] extends string ? string : T[P] } } + ? { + [k in Id]: { + -readonly [P in keyof T]: + T[P] extends string + ? string + : T[P] extends number + ? number | undefined + : T[P] + } + } : never) | ( From d447b5c2e69e2f11d0f432b3975a7f17c8651df6 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 9 Sep 2024 22:01:07 +0100 Subject: [PATCH 02/26] pace: initial app --- apps/pace/ChangeLog | 1 + apps/pace/README.md | 6 ++ apps/pace/app-icon.js | 1 + apps/pace/app.js | 179 +++++++++++++++++++++++++++++++++ apps/pace/app.png | Bin 0 -> 715 bytes apps/pace/app.ts | 217 ++++++++++++++++++++++++++++++++++++++++ apps/pace/metadata.json | 14 +++ 7 files changed, 418 insertions(+) create mode 100644 apps/pace/ChangeLog create mode 100644 apps/pace/README.md create mode 100644 apps/pace/app-icon.js create mode 100644 apps/pace/app.js create mode 100644 apps/pace/app.png create mode 100644 apps/pace/app.ts create mode 100644 apps/pace/metadata.json diff --git a/apps/pace/ChangeLog b/apps/pace/ChangeLog new file mode 100644 index 000000000..1a3bc1757 --- /dev/null +++ b/apps/pace/ChangeLog @@ -0,0 +1 @@ +0.01: New app! diff --git a/apps/pace/README.md b/apps/pace/README.md new file mode 100644 index 000000000..f6c0ce941 --- /dev/null +++ b/apps/pace/README.md @@ -0,0 +1,6 @@ +# Description + +A running pace app, useful for races. Will also record your splits and display them to you on the pause menu. + +Drag up/down on the pause menu to scroll through your splits. +Press the button to pause/resume - when resumed, pressing the button will pause instantly, regardless of whether the screen is locked. diff --git a/apps/pace/app-icon.js b/apps/pace/app-icon.js new file mode 100644 index 000000000..b2b8b4c17 --- /dev/null +++ b/apps/pace/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AH8QRYoX/AAcAZqwXuMIQYCiAcOO456OCwwACF5orEDghzOE4oZCLxw+GDAKsXeSIqEGBKKGBwIRFGBCfIC7p5RC7oGHC8RxFC453JAw4Xda5AIEC5IAPO6AXmO5BuHC67OIUA4aERyIXEIxAAJiAuFC5gTEcgpDNBwoWCIxp4CCwp1OCIYAEOiDFNDBwVQAH4AvA=")) diff --git a/apps/pace/app.js b/apps/pace/app.js new file mode 100644 index 000000000..5acad0cee --- /dev/null +++ b/apps/pace/app.js @@ -0,0 +1,179 @@ +{ + var Layout_1 = require("Layout"); + var state_1 = 1; + var drawTimeout_1; + var lastUnlazy_1 = 0; + var lastResumeTime_1 = Date.now(); + var splitTime_1 = 0; + var totalTime_1 = 0; + var splits_1 = []; + var splitDist_1 = 0; + var splitOffset_1 = 0, splitOffsetPx_1 = 0; + var lastGPS_1 = 0; + var GPS_TIMEOUT_MS_1 = 30000; + var layout_1 = new Layout_1({ + type: "v", + c: [ + { + type: "txt", + font: "6x8:2", + label: "Pace", + id: "paceLabel", + pad: 4 + }, + { + type: "txt", + font: "Vector:40", + label: "", + id: "pace", + halign: 0 + }, + { + type: "txt", + font: "6x8:2", + label: "Time", + id: "timeLabel", + pad: 4 + }, + { + type: "txt", + font: "Vector:40", + label: "", + id: "time", + halign: 0 + }, + ] + }, { + lazy: true + }); + var formatTime_1 = function (ms) { + var totalSeconds = Math.floor(ms / 1000); + var minutes = Math.floor(totalSeconds / 60); + var seconds = totalSeconds % 60; + return "".concat(minutes, ":").concat(seconds < 10 ? '0' : '').concat(seconds); + }; + var calculatePace_1 = function (time, dist) { + if (dist === 0) + return 0; + return time / dist / 1000 / 60; + }; + var draw_1 = function () { + if (state_1 === 1) { + drawSplits_1(); + return; + } + if (drawTimeout_1) + clearTimeout(drawTimeout_1); + drawTimeout_1 = setTimeout(draw_1, 1000); + var now = Date.now(); + var elapsedTime = formatTime_1(totalTime_1 + (state_1 === 0 ? now - lastResumeTime_1 : 0)); + var pace; + if (now - lastGPS_1 <= GPS_TIMEOUT_MS_1) { + pace = calculatePace_1(thisSplitTime_1(), splitDist_1).toFixed(2); + } + else { + pace = "No GPS"; + } + layout_1["time"].label = elapsedTime; + layout_1["pace"].label = pace; + layout_1.render(); + if (now - lastUnlazy_1 > 30000) + layout_1.forgetLazyState(), lastUnlazy_1 = now; + }; + var drawSplits_1 = function () { + g.clearRect(Bangle.appRect); + var barSize = 20; + var barSpacing = 10; + var w = g.getWidth(); + var h = g.getHeight(); + var max = splits_1.reduce(function (a, x) { return Math.max(a, x); }, 0); + g.setFont("6x8", 2).setFontAlign(-1, -1); + var i = 0; + for (;; i++) { + var split = splits_1[i + splitOffset_1]; + if (split == null) + break; + var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; + if (y > h) + break; + var size = w * split / max; + g.setColor("#00f").fillRect(0, y, size, y + barSize); + var splitPace = calculatePace_1(split, 1); + g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y); + } + var splitTime = thisSplitTime_1(); + var pace = calculatePace_1(splitTime, splitDist_1); + g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(pace, " (").concat((splitTime / 1000).toFixed(2), ")"), 0, Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2); + }; + var thisSplitTime_1 = function () { + if (state_1 === 1) + return splitTime_1; + return Date.now() - lastResumeTime_1 + splitTime_1; + }; + var pauseRun_1 = function () { + state_1 = 1; + var now = Date.now(); + totalTime_1 += now - lastResumeTime_1; + splitTime_1 += now - lastResumeTime_1; + Bangle.setGPSPower(0, "pace"); + Bangle.removeListener('GPS', onGPS_1); + draw_1(); + }; + var resumeRun_1 = function () { + state_1 = 0; + lastResumeTime_1 = Date.now(); + Bangle.setGPSPower(1, "pace"); + Bangle.on('GPS', onGPS_1); + g.clearRect(Bangle.appRect); + layout_1.forgetLazyState(); + draw_1(); + }; + var onGPS_1 = function (fix) { + if (fix && fix.speed && state_1 === 0) { + var now = Date.now(); + var elapsedTime = now - lastGPS_1; + splitDist_1 += fix.speed * elapsedTime / 3600000; + while (splitDist_1 >= 1) { + splits_1.push(thisSplitTime_1()); + splitDist_1 -= 1; + splitTime_1 = 0; + } + lastGPS_1 = now; + } + }; + var onButton_1 = function () { + switch (state_1) { + case 0: + pauseRun_1(); + break; + case 1: + resumeRun_1(); + break; + } + }; + Bangle.on('lock', function (locked) { + if (!locked && state_1 == 0) + onButton_1(); + }); + setWatch(function () { return onButton_1(); }, BTN1, { repeat: true }); + Bangle.on('drag', function (e) { + if (state_1 !== 1 || e.b === 0) + return; + splitOffsetPx_1 -= e.dy; + if (splitOffsetPx_1 > 20) { + if (splitOffset_1 < splits_1.length - 3) + splitOffset_1++, Bangle.buzz(30); + splitOffsetPx_1 = 0; + } + else if (splitOffsetPx_1 < -20) { + if (splitOffset_1 > 0) + splitOffset_1--, Bangle.buzz(30); + splitOffsetPx_1 = 0; + } + draw_1(); + }); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + g.clearRect(Bangle.appRect); + draw_1(); +} diff --git a/apps/pace/app.png b/apps/pace/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ff8f29cdca8941f3a4d051ddba2b1469ea81a7a0 GIT binary patch literal 715 zcmV;+0yO=JP)6>ziQ3~@QX)^IJls6$MFG`9s z#e;I2(lls7@-iMwn7p`@2T~q9;6V|2a4AYDmq$jU6b;j0rd{jQYIo*4-`QvEvrlt= zwO-D5);{aMzqR&dK}ku;c+w2Cq_m)T4veI(OU=pwnI1F5| z1l$8o1Dk;vmfsF}rw^E(N8k&9bHL{sm(O3o4Vm>O;Jf$7Xhm*zoisRKppT! z`E3s+g-#~;R+Qfp<##NUB2Kav<#$s(@5xiZH%9?i)$`swH?l3jW9e8VS+nwcpah(u zfV@=#_EIqB$bl{ru$F?+u7V$wV|Q*voR&J^nHukw>p@x*T6pB2Ec4?@FC!Giap}LGgUSs)1`YK xlU`9H_l_LUDg=&MN#+78fF(YQv!rCAFbdFK++ROgi)sJ>002ovPDHLkV1i~1KimKS literal 0 HcmV?d00001 diff --git a/apps/pace/app.ts b/apps/pace/app.ts new file mode 100644 index 000000000..d7928504a --- /dev/null +++ b/apps/pace/app.ts @@ -0,0 +1,217 @@ +{ +const Layout = require("Layout"); + +const enum RunState { + RUNNING, + PAUSED +} + +let state = RunState.PAUSED; +let drawTimeout: TimeoutId | undefined; +let lastUnlazy = 0; + +let lastResumeTime = Date.now(); +let splitTime = 0; +let totalTime = 0; + +const splits: number[] = []; +let splitDist = 0; +let splitOffset = 0, splitOffsetPx = 0; + +let lastGPS = 0; +const GPS_TIMEOUT_MS = 30000; + +const layout = new Layout({ + type: "v", + c: [ + { + type: "txt", + font: "6x8:2", + label: "Pace", + id: "paceLabel", + pad: 4 + }, + { + type: "txt", + font: "Vector:40", + label: "", + id: "pace", + halign: 0 + }, + { + type: "txt", + font: "6x8:2", + label: "Time", + id: "timeLabel", + pad: 4 + }, + { + type: "txt", + font: "Vector:40", + label: "", + id: "time", + halign: 0 + }, + ] +}, { + lazy: true +}); + +const formatTime = (ms: number) => { + let totalSeconds = Math.floor(ms / 1000); + let minutes = Math.floor(totalSeconds / 60); + let seconds = totalSeconds % 60; + return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; +}; + +const calculatePace = (time: number, dist: number) => { + if (dist === 0) return 0; + return time / dist / 1000 / 60; +}; + +const draw = () => { + if (state === RunState.PAUSED) { + // no draw-timeout here, only on user interaction + drawSplits(); + return; + } + + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(draw, 1000); + + const now = Date.now(); + + const elapsedTime = formatTime(totalTime + (state === RunState.RUNNING ? now - lastResumeTime : 0)); + + let pace: string; + if (now - lastGPS <= GPS_TIMEOUT_MS) { + pace = calculatePace(thisSplitTime(), splitDist).toFixed(2); + }else{ + pace = "No GPS"; + } + + layout["time"]!.label = elapsedTime; + layout["pace"]!.label = pace; + layout.render(); + + if (now - lastUnlazy > 30000) + layout.forgetLazyState(), lastUnlazy = now; +}; + +const drawSplits = () => { + g.clearRect(Bangle.appRect); + + const barSize = 20; + const barSpacing = 10; + const w = g.getWidth(); + const h = g.getHeight(); + + const max = splits.reduce((a, x) => Math.max(a, x), 0); + + g.setFont("6x8", 2).setFontAlign(-1, -1); + + let i = 0; + for(; ; i++) { + const split = splits[i + splitOffset]; + if (split == null) break; + + const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; + if (y > h) break; + + const size = w * split / max; // Scale bar height based on pace + g.setColor("#00f").fillRect(0, y, size, y + barSize); + + const splitPace = calculatePace(split, 1); // Pace per km + g.setColor("#fff").drawString(`${i + 1 + splitOffset} @ ${splitPace.toFixed(2)}`, 0, y); + } + + const splitTime = thisSplitTime(); + const pace = calculatePace(splitTime, splitDist); + g.setColor("#fff").drawString( + `${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`, + 0, + Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2, + ); +}; + +const thisSplitTime = () => { + if (state === RunState.PAUSED) return splitTime; + return Date.now() - lastResumeTime + splitTime; +}; + +const pauseRun = () => { + state = RunState.PAUSED; + const now = Date.now(); + totalTime += now - lastResumeTime; + splitTime += now - lastResumeTime; + Bangle.setGPSPower(0, "pace") + Bangle.removeListener('GPS', onGPS); + draw(); +}; + +const resumeRun = () => { + state = RunState.RUNNING; + lastResumeTime = Date.now(); + Bangle.setGPSPower(1, "pace"); + Bangle.on('GPS', onGPS); + + g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine + layout.forgetLazyState(); + draw(); +}; + +const onGPS = (fix: GPSFix) => { + if (fix && fix.speed && state === RunState.RUNNING) { + const now = Date.now(); + + const elapsedTime = now - lastGPS; // ms + splitDist += fix.speed * elapsedTime / 3600000; // ms in one hour (fix.speed is in km/h) + + while (splitDist >= 1) { + splits.push(thisSplitTime()); + splitDist -= 1; + splitTime = 0; + } + + lastGPS = now; + } +}; + +const onButton = () => { + switch (state) { + case RunState.RUNNING: + pauseRun(); + break; + case RunState.PAUSED: + resumeRun(); + break; + } +}; + +Bangle.on('lock', locked => { + // treat an unlock (while running) as a pause + if(!locked && state == RunState.RUNNING) onButton(); +}); + +setWatch(() => onButton(), BTN1, { repeat: true }); + +Bangle.on('drag', e => { + if (state !== RunState.PAUSED || e.b === 0) return; + + splitOffsetPx -= e.dy; + if (splitOffsetPx > 20) { + if (splitOffset < splits.length-3) splitOffset++, Bangle.buzz(30); + splitOffsetPx = 0; + } else if (splitOffsetPx < -20) { + if (splitOffset > 0) splitOffset--, Bangle.buzz(30); + splitOffsetPx = 0; + } + draw(); +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.clearRect(Bangle.appRect); +draw(); +} diff --git a/apps/pace/metadata.json b/apps/pace/metadata.json new file mode 100644 index 000000000..e7066c958 --- /dev/null +++ b/apps/pace/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "pace", + "name": "Pace", + "version": "0.01", + "description": "Show pace and time running splits", + "icon": "app.png", + "tags": "run,running,fitness,outdoors", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "pace.app.js","url": "app.js" }, + { "name": "pace.img","url": "app-icon.js","evaluate": true } + ] +} From 4b08b4b24cc49978162b7fbca5d1e95dc518fa87 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sat, 21 Sep 2024 12:51:49 +0100 Subject: [PATCH 03/26] typescript: add exstats --- typescript/types/exstats.d.ts | 75 +++++++++++++++++++++++++++++++++++ typescript/types/modules.d.ts | 1 + 2 files changed, 76 insertions(+) create mode 100644 typescript/types/exstats.d.ts diff --git a/typescript/types/exstats.d.ts b/typescript/types/exstats.d.ts new file mode 100644 index 000000000..7c68b6d59 --- /dev/null +++ b/typescript/types/exstats.d.ts @@ -0,0 +1,75 @@ +declare module ExStats { + type StatsId = "time" | "dist" | "step" | "bpm" | "maxbpm" | "pacea" | "pacec" | "speed" | "caden" | "altg" | "altb"; + + function getList(): { name: string, id: StatsId }[]; + + function getStats( + ids: Ids[], + options?: Options + ): StatsInst; + + type Options = { + paceLength?: number, + notify?: Notify, + }; + + type Notify = { + [key in Ids & ("dist" | "step" | "time")]?: { + // optional when passed in ^ + increment?: number, + next?: number, + } + }; + + type StatsInst = { + stats: Stats, + state: State, + start(): void, + stop(): void, + resume(): void, + }; + + type State = { + notify: Notify, + + active: boolean, + duration: number, + startTime: number, + lastTime: number, + + BPM: number, + BPMage: number, + maxBPM: number, + + alt: number | undefined, + alti: number, + + avrSpeed: number, + curSpeed: number, + distance: number, + + startSteps: number, + lastSteps: number, + stepHistory: Uint8Array, + stepsPerMin: number, + + thisGPS: GPSFix | {}, + lastGPS: GPSFix | {}, + }; + + type Stats = { + [key in Ids]: Stat + }; + + type Stat = { + title: string, + getValue(): number, + getString(): string, + id: StatsId, + + on(what: "changed", cb: (stat: Stat) => void): void; + + // emitted by dist|step|time + on(what: "notify", cb: (stat: Stat) => void): void; + }; +} diff --git a/typescript/types/modules.d.ts b/typescript/types/modules.d.ts index e8aa15ac1..1227250c3 100644 --- a/typescript/types/modules.d.ts +++ b/typescript/types/modules.d.ts @@ -6,3 +6,4 @@ declare function require(moduleName: "ClockFace"): typeof ClockFace_.ClockFace; declare function require(moduleName: "clock_info"): typeof ClockInfo; declare function require(moduleName: "Layout"): typeof Layout.Layout; declare function require(moduleName: "power_usage"): PowerUsageModule; +declare function require(moduleName: "exstats"): typeof ExStats; From 5220bf6f86f599ea57f65df2ceae38f47ade7ff4 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 22 Sep 2024 21:47:20 +0100 Subject: [PATCH 04/26] pace: use `exstats` for measurements --- apps/pace/app.js | 91 +++++++++++++++----------------------- apps/pace/app.ts | 113 +++++++++++++++++++---------------------------- 2 files changed, 81 insertions(+), 123 deletions(-) diff --git a/apps/pace/app.js b/apps/pace/app.js index 5acad0cee..e5719a9ac 100644 --- a/apps/pace/app.js +++ b/apps/pace/app.js @@ -1,15 +1,16 @@ { var Layout_1 = require("Layout"); - var state_1 = 1; + var exs_1 = require("exstats").getStats(["time", "dist", "pacec"], { + notify: { + dist: { + increment: 1000, + }, + }, + }); var drawTimeout_1; var lastUnlazy_1 = 0; - var lastResumeTime_1 = Date.now(); - var splitTime_1 = 0; - var totalTime_1 = 0; var splits_1 = []; - var splitDist_1 = 0; var splitOffset_1 = 0, splitOffsetPx_1 = 0; - var lastGPS_1 = 0; var GPS_TIMEOUT_MS_1 = 30000; var layout_1 = new Layout_1({ type: "v", @@ -46,19 +47,13 @@ }, { lazy: true }); - var formatTime_1 = function (ms) { - var totalSeconds = Math.floor(ms / 1000); - var minutes = Math.floor(totalSeconds / 60); - var seconds = totalSeconds % 60; - return "".concat(minutes, ":").concat(seconds < 10 ? '0' : '').concat(seconds); - }; var calculatePace_1 = function (time, dist) { if (dist === 0) return 0; return time / dist / 1000 / 60; }; var draw_1 = function () { - if (state_1 === 1) { + if (!exs_1.state.active) { drawSplits_1(); return; } @@ -66,15 +61,15 @@ clearTimeout(drawTimeout_1); drawTimeout_1 = setTimeout(draw_1, 1000); var now = Date.now(); - var elapsedTime = formatTime_1(totalTime_1 + (state_1 === 0 ? now - lastResumeTime_1 : 0)); var pace; - if (now - lastGPS_1 <= GPS_TIMEOUT_MS_1) { - pace = calculatePace_1(thisSplitTime_1(), splitDist_1).toFixed(2); + if ("time" in exs_1.state.thisGPS + && now - exs_1.state.thisGPS.time < GPS_TIMEOUT_MS_1) { + pace = exs_1.stats.pacec.getString(); } else { pace = "No GPS"; } - layout_1["time"].label = elapsedTime; + layout_1["time"].label = exs_1.stats.time.getString(); layout_1["pace"].label = pace; layout_1.render(); if (now - lastUnlazy_1 > 30000) @@ -89,10 +84,12 @@ var max = splits_1.reduce(function (a, x) { return Math.max(a, x); }, 0); g.setFont("6x8", 2).setFontAlign(-1, -1); var i = 0; + var totalTime = 0; for (;; i++) { var split = splits_1[i + splitOffset_1]; if (split == null) break; + totalTime += split; var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; if (y > h) break; @@ -101,63 +98,47 @@ var splitPace = calculatePace_1(split, 1); g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y); } - var splitTime = thisSplitTime_1(); - var pace = calculatePace_1(splitTime, splitDist_1); + var pace = exs_1.stats.pacec.getString(); + var splitTime = exs_1.stats.time.getValue() - totalTime; g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(pace, " (").concat((splitTime / 1000).toFixed(2), ")"), 0, Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2); }; - var thisSplitTime_1 = function () { - if (state_1 === 1) - return splitTime_1; - return Date.now() - lastResumeTime_1 + splitTime_1; - }; var pauseRun_1 = function () { - state_1 = 1; - var now = Date.now(); - totalTime_1 += now - lastResumeTime_1; - splitTime_1 += now - lastResumeTime_1; + exs_1.stop(); Bangle.setGPSPower(0, "pace"); - Bangle.removeListener('GPS', onGPS_1); draw_1(); }; var resumeRun_1 = function () { - state_1 = 0; - lastResumeTime_1 = Date.now(); + exs_1.start(); Bangle.setGPSPower(1, "pace"); - Bangle.on('GPS', onGPS_1); g.clearRect(Bangle.appRect); layout_1.forgetLazyState(); draw_1(); }; - var onGPS_1 = function (fix) { - if (fix && fix.speed && state_1 === 0) { - var now = Date.now(); - var elapsedTime = now - lastGPS_1; - splitDist_1 += fix.speed * elapsedTime / 3600000; - while (splitDist_1 >= 1) { - splits_1.push(thisSplitTime_1()); - splitDist_1 -= 1; - splitTime_1 = 0; - } - lastGPS_1 = now; - } - }; var onButton_1 = function () { - switch (state_1) { - case 0: - pauseRun_1(); - break; - case 1: - resumeRun_1(); - break; - } + if (exs_1.state.active) + pauseRun_1(); + else + resumeRun_1(); }; + exs_1.stats.dist.on("notify", function (dist) { + var prev = splits_1[splits_1.length - 1] || 0; + var totalDist = dist.getValue(); + var thisSplit = totalDist - prev; + var prevTime = splits_1.reduce(function (a, b) { return a + b; }, 0); + var time = exs_1.stats.time.getValue() - prevTime; + while (thisSplit > 0) { + splits_1.push(time); + time = 0; + thisSplit -= 1000; + } + }); Bangle.on('lock', function (locked) { - if (!locked && state_1 == 0) + if (!locked && exs_1.state.active) onButton_1(); }); setWatch(function () { return onButton_1(); }, BTN1, { repeat: true }); Bangle.on('drag', function (e) { - if (state_1 !== 1 || e.b === 0) + if (exs_1.state.active || e.b === 0) return; splitOffsetPx_1 -= e.dy; if (splitOffsetPx_1 > 20) { diff --git a/apps/pace/app.ts b/apps/pace/app.ts index d7928504a..195a8f32b 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -1,24 +1,22 @@ { const Layout = require("Layout"); +const exs = require("exstats").getStats( + ["time", "dist", "pacec"], + { + notify: { + dist: { + increment: 1000, + }, + }, + }, +); -const enum RunState { - RUNNING, - PAUSED -} - -let state = RunState.PAUSED; let drawTimeout: TimeoutId | undefined; let lastUnlazy = 0; -let lastResumeTime = Date.now(); -let splitTime = 0; -let totalTime = 0; - -const splits: number[] = []; -let splitDist = 0; +const splits: number[] = []; // times let splitOffset = 0, splitOffsetPx = 0; -let lastGPS = 0; const GPS_TIMEOUT_MS = 30000; const layout = new Layout({ @@ -57,20 +55,13 @@ const layout = new Layout({ lazy: true }); -const formatTime = (ms: number) => { - let totalSeconds = Math.floor(ms / 1000); - let minutes = Math.floor(totalSeconds / 60); - let seconds = totalSeconds % 60; - return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; -}; - const calculatePace = (time: number, dist: number) => { if (dist === 0) return 0; return time / dist / 1000 / 60; }; const draw = () => { - if (state === RunState.PAUSED) { + if (!exs.state.active) { // no draw-timeout here, only on user interaction drawSplits(); return; @@ -81,16 +72,16 @@ const draw = () => { const now = Date.now(); - const elapsedTime = formatTime(totalTime + (state === RunState.RUNNING ? now - lastResumeTime : 0)); - let pace: string; - if (now - lastGPS <= GPS_TIMEOUT_MS) { - pace = calculatePace(thisSplitTime(), splitDist).toFixed(2); + if ("time" in exs.state.thisGPS + && now - (exs.state.thisGPS.time as unknown as number) < GPS_TIMEOUT_MS) + { + pace = exs.stats.pacec.getString() }else{ pace = "No GPS"; } - layout["time"]!.label = elapsedTime; + layout["time"]!.label = exs.stats.time.getString(); layout["pace"]!.label = pace; layout.render(); @@ -111,10 +102,13 @@ const drawSplits = () => { g.setFont("6x8", 2).setFontAlign(-1, -1); let i = 0; + let totalTime = 0; for(; ; i++) { const split = splits[i + splitOffset]; if (split == null) break; + totalTime += split; + const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; if (y > h) break; @@ -125,8 +119,9 @@ const drawSplits = () => { g.setColor("#fff").drawString(`${i + 1 + splitOffset} @ ${splitPace.toFixed(2)}`, 0, y); } - const splitTime = thisSplitTime(); - const pace = calculatePace(splitTime, splitDist); + const pace = exs.stats.pacec.getString(); + const splitTime = exs.stats.time.getValue() - totalTime; + g.setColor("#fff").drawString( `${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`, 0, @@ -134,69 +129,51 @@ const drawSplits = () => { ); }; -const thisSplitTime = () => { - if (state === RunState.PAUSED) return splitTime; - return Date.now() - lastResumeTime + splitTime; -}; - const pauseRun = () => { - state = RunState.PAUSED; - const now = Date.now(); - totalTime += now - lastResumeTime; - splitTime += now - lastResumeTime; + exs.stop(); Bangle.setGPSPower(0, "pace") - Bangle.removeListener('GPS', onGPS); draw(); }; const resumeRun = () => { - state = RunState.RUNNING; - lastResumeTime = Date.now(); + exs.start(); Bangle.setGPSPower(1, "pace"); - Bangle.on('GPS', onGPS); g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine layout.forgetLazyState(); draw(); }; -const onGPS = (fix: GPSFix) => { - if (fix && fix.speed && state === RunState.RUNNING) { - const now = Date.now(); - - const elapsedTime = now - lastGPS; // ms - splitDist += fix.speed * elapsedTime / 3600000; // ms in one hour (fix.speed is in km/h) - - while (splitDist >= 1) { - splits.push(thisSplitTime()); - splitDist -= 1; - splitTime = 0; - } - - lastGPS = now; - } -}; - const onButton = () => { - switch (state) { - case RunState.RUNNING: - pauseRun(); - break; - case RunState.PAUSED: - resumeRun(); - break; - } + if (exs.state.active) + pauseRun(); + else + resumeRun(); }; +exs.stats.dist.on("notify", (dist) => { + const prev = splits[splits.length - 1] || 0; + const totalDist = dist.getValue(); + let thisSplit = totalDist - prev; + const prevTime = splits.reduce((a, b) => a + b, 0); + let time = exs.stats.time.getValue() - prevTime; + + while(thisSplit > 0) { + splits.push(time); + time = 0; // if we've jumped more than 1k, credit the time to the first split + thisSplit -= 1000; + } +}); + Bangle.on('lock', locked => { // treat an unlock (while running) as a pause - if(!locked && state == RunState.RUNNING) onButton(); + if(!locked && exs.state.active) onButton(); }); setWatch(() => onButton(), BTN1, { repeat: true }); Bangle.on('drag', e => { - if (state !== RunState.PAUSED || e.b === 0) return; + if (exs.state.active || e.b === 0) return; splitOffsetPx -= e.dy; if (splitOffsetPx > 20) { From 1c7649b62fc50b4088a755e3a81426050a32ceec Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 22 Sep 2024 22:13:36 +0100 Subject: [PATCH 05/26] pace: resume, instead of start anew --- apps/pace/app.js | 2 +- apps/pace/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pace/app.js b/apps/pace/app.js index e5719a9ac..c12ef3e6a 100644 --- a/apps/pace/app.js +++ b/apps/pace/app.js @@ -108,7 +108,7 @@ draw_1(); }; var resumeRun_1 = function () { - exs_1.start(); + exs_1.resume(); Bangle.setGPSPower(1, "pace"); g.clearRect(Bangle.appRect); layout_1.forgetLazyState(); diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 195a8f32b..a9321501f 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -136,7 +136,7 @@ const pauseRun = () => { }; const resumeRun = () => { - exs.start(); + exs.resume(); Bangle.setGPSPower(1, "pace"); g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine From 8d34e408e7e1cd4eb80c2c285f8e90a5710f0c76 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 22 Sep 2024 22:12:17 +0100 Subject: [PATCH 06/26] typescript: add time_utils --- typescript/types/modules.d.ts | 1 + typescript/types/time_utils.d.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 typescript/types/time_utils.d.ts diff --git a/typescript/types/modules.d.ts b/typescript/types/modules.d.ts index 1227250c3..3ba43d38b 100644 --- a/typescript/types/modules.d.ts +++ b/typescript/types/modules.d.ts @@ -7,3 +7,4 @@ declare function require(moduleName: "clock_info"): typeof ClockInfo; declare function require(moduleName: "Layout"): typeof Layout.Layout; declare function require(moduleName: "power_usage"): PowerUsageModule; declare function require(moduleName: "exstats"): typeof ExStats; +declare function require(moduleName: "time_utils"): typeof TimeUtils; diff --git a/typescript/types/time_utils.d.ts b/typescript/types/time_utils.d.ts new file mode 100644 index 000000000..c176ffac6 --- /dev/null +++ b/typescript/types/time_utils.d.ts @@ -0,0 +1,18 @@ +declare module TimeUtils { + type TimeObj = { + d: number, + h: number, + m: number, + s: number, + }; + + function encodeTime(time: TimeObj): number; + + function decodeTime(millis: number): TimeObj; + + function formatTime(value: number | TimeObj): string + + function formatDuration(value: number | TimeObj, compact?: boolean): string; + + function getCurrentTimeMillis(): number; +} From 25b16a17385d8570eed875964bc866bb636def85 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 22 Sep 2024 22:13:03 +0100 Subject: [PATCH 07/26] pace: use duration instead of time stat time is total (or elapsed) time, duration is active time --- apps/pace/app.js | 10 ++++++---- apps/pace/app.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/pace/app.js b/apps/pace/app.js index c12ef3e6a..40d215869 100644 --- a/apps/pace/app.js +++ b/apps/pace/app.js @@ -1,6 +1,7 @@ { var Layout_1 = require("Layout"); - var exs_1 = require("exstats").getStats(["time", "dist", "pacec"], { + var time_utils_1 = require("time_utils"); + var exs_1 = require("exstats").getStats(["dist", "pacec"], { notify: { dist: { increment: 1000, @@ -69,7 +70,8 @@ else { pace = "No GPS"; } - layout_1["time"].label = exs_1.stats.time.getString(); + var tm = time_utils_1.decodeTime(exs_1.state.duration); + layout_1["time"].label = tm.d ? time_utils_1.formatDuration(tm) : time_utils_1.formatTime(tm); layout_1["pace"].label = pace; layout_1.render(); if (now - lastUnlazy_1 > 30000) @@ -99,7 +101,7 @@ g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y); } var pace = exs_1.stats.pacec.getString(); - var splitTime = exs_1.stats.time.getValue() - totalTime; + var splitTime = exs_1.state.duration - totalTime; g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(pace, " (").concat((splitTime / 1000).toFixed(2), ")"), 0, Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2); }; var pauseRun_1 = function () { @@ -125,7 +127,7 @@ var totalDist = dist.getValue(); var thisSplit = totalDist - prev; var prevTime = splits_1.reduce(function (a, b) { return a + b; }, 0); - var time = exs_1.stats.time.getValue() - prevTime; + var time = exs_1.state.duration - prevTime; while (thisSplit > 0) { splits_1.push(time); time = 0; diff --git a/apps/pace/app.ts b/apps/pace/app.ts index a9321501f..0351c4b3e 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -1,7 +1,8 @@ { const Layout = require("Layout"); +const time_utils = require("time_utils"); const exs = require("exstats").getStats( - ["time", "dist", "pacec"], + ["dist", "pacec"], { notify: { dist: { @@ -81,7 +82,8 @@ const draw = () => { pace = "No GPS"; } - layout["time"]!.label = exs.stats.time.getString(); + const tm = time_utils.decodeTime(exs.state.duration); + layout["time"]!.label = tm.d ? time_utils.formatDuration(tm) : time_utils.formatTime(tm); // formatTime throws if tm.d > 0 layout["pace"]!.label = pace; layout.render(); @@ -120,7 +122,7 @@ const drawSplits = () => { } const pace = exs.stats.pacec.getString(); - const splitTime = exs.stats.time.getValue() - totalTime; + const splitTime = exs.state.duration - totalTime; g.setColor("#fff").drawString( `${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`, @@ -156,7 +158,7 @@ exs.stats.dist.on("notify", (dist) => { const totalDist = dist.getValue(); let thisSplit = totalDist - prev; const prevTime = splits.reduce((a, b) => a + b, 0); - let time = exs.stats.time.getValue() - prevTime; + let time = exs.state.duration - prevTime; while(thisSplit > 0) { splits.push(time); From 4493b343bd651acf5fe638ef3ff8b36fda20a0b2 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:08:53 +0100 Subject: [PATCH 08/26] pace: factor out `drawSplit()` --- apps/pace/app.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 0351c4b3e..a03941524 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -104,13 +104,10 @@ const drawSplits = () => { g.setFont("6x8", 2).setFontAlign(-1, -1); let i = 0; - let totalTime = 0; for(; ; i++) { const split = splits[i + splitOffset]; if (split == null) break; - totalTime += split; - const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; if (y > h) break; @@ -118,17 +115,23 @@ const drawSplits = () => { g.setColor("#00f").fillRect(0, y, size, y + barSize); const splitPace = calculatePace(split, 1); // Pace per km - g.setColor("#fff").drawString(`${i + 1 + splitOffset} @ ${splitPace.toFixed(2)}`, 0, y); + drawSplit(i, y, splitPace); } const pace = exs.stats.pacec.getString(); - const splitTime = exs.state.duration - totalTime; - g.setColor("#fff").drawString( - `${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`, - 0, - Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2, - ); + const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; + drawSplit(i, y, pace); +}; + +const drawSplit = (i: number, y: number, pace: number | string) => { + g + .setColor("#fff") + .drawString( + `${i + 1 + splitOffset} ${typeof pace === "number" ? pace.toFixed(2) : pace}`, + 0, + y + ); }; const pauseRun = () => { From eabb2a88289263c283510520748bd466952ca77d Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:09:10 +0100 Subject: [PATCH 09/26] pace: fix duration formatting --- apps/pace/app.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index a03941524..97b5b8cbe 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -82,8 +82,7 @@ const draw = () => { pace = "No GPS"; } - const tm = time_utils.decodeTime(exs.state.duration); - layout["time"]!.label = tm.d ? time_utils.formatDuration(tm) : time_utils.formatTime(tm); // formatTime throws if tm.d > 0 + layout["time"]!.label = formatDuration(exs.state.duration); layout["pace"]!.label = pace; layout.render(); @@ -91,6 +90,15 @@ const draw = () => { layout.forgetLazyState(), lastUnlazy = now; }; +const pad2 = (n: number) => `0${n}`.substr(-2); + +const formatDuration = (ms: number) => { + const tm = time_utils.decodeTime(ms); + if(tm.h) + return `${tm.h}:${pad2(tm.m)}:${pad2(tm.s)}`; + return `${pad2(tm.m)}:${pad2(tm.s)}`; +}; + const drawSplits = () => { g.clearRect(Bangle.appRect); From 5c9a4aed6291e0987f568d4479d365c61ab6ff38 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:09:24 +0100 Subject: [PATCH 10/26] pace: reset exstats on load --- apps/pace/app.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 97b5b8cbe..2bc52bebe 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -164,6 +164,8 @@ const onButton = () => { resumeRun(); }; +exs.start(); // aka reset + exs.stats.dist.on("notify", (dist) => { const prev = splits[splits.length - 1] || 0; const totalDist = dist.getValue(); From ca1cb2253a4278b8a719f55a7345eecbc9b11caa Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:10:36 +0100 Subject: [PATCH 11/26] pace: drop lazyUnlazy --- apps/pace/app.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 2bc52bebe..5a992eda5 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -13,7 +13,6 @@ const exs = require("exstats").getStats( ); let drawTimeout: TimeoutId | undefined; -let lastUnlazy = 0; const splits: number[] = []; // times let splitOffset = 0, splitOffsetPx = 0; @@ -85,9 +84,6 @@ const draw = () => { layout["time"]!.label = formatDuration(exs.state.duration); layout["pace"]!.label = pace; layout.render(); - - if (now - lastUnlazy > 30000) - layout.forgetLazyState(), lastUnlazy = now; }; const pad2 = (n: number) => `0${n}`.substr(-2); From f581e15320debf22e63b964990ede48588aa5abd Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 21:27:00 +0100 Subject: [PATCH 12/26] exstats: permit "notify" handlers to change `.next` --- modules/exstats.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/exstats.js b/modules/exstats.js index 5cd0374a7..6cb6a8773 100644 --- a/modules/exstats.js +++ b/modules/exstats.js @@ -158,8 +158,8 @@ Bangle.on("GPS", function(fix) { if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]); if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]); if (state.notify.dist.increment > 0 && state.notify.dist.next <= state.distance) { - stats["dist"].emit("notify",stats["dist"]); state.notify.dist.next = state.notify.dist.next + state.notify.dist.increment; + stats["dist"].emit("notify",stats["dist"]); } }); @@ -169,8 +169,8 @@ Bangle.on("step", function(steps) { state.stepHistory[0] += steps-state.lastSteps; state.lastSteps = steps; if (state.notify.step.increment > 0 && state.notify.step.next <= steps) { - stats["step"].emit("notify",stats["step"]); state.notify.step.next = state.notify.step.next + state.notify.step.increment; + stats["step"].emit("notify",stats["step"]); } }); Bangle.on("HRM", function(h) { @@ -336,8 +336,8 @@ exports.getStats = function(statIDs, options) { if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]); } if (state.notify.time.increment > 0 && state.notify.time.next <= now) { - stats["time"].emit("notify",stats["time"]); state.notify.time.next = state.notify.time.next + state.notify.time.increment; + stats["time"].emit("notify",stats["time"]); } }, 1000); function reset() { From b2e788683a34e3a2937a8b88545697f666297f60 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 20:02:27 +0100 Subject: [PATCH 13/26] main.d.ts: fix `[].reduce` types --- typescript/types/main.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts index 24633db5b..8627e1323 100644 --- a/typescript/types/main.d.ts +++ b/typescript/types/main.d.ts @@ -10229,7 +10229,7 @@ interface Array { * @returns {any} The value returned by the last function called * @url http://www.espruino.com/Reference#l_Array_reduce */ - reduce(callback: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T; + reduce(callback: (previousValue: O, currentValue: T, currentIndex: number, array: T[]) => O, initialValue?: O): O; /** * Both remove and add items to an array From 23a53f028449bb10e420443857dd8c973c5135c2 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 20:06:30 +0100 Subject: [PATCH 14/26] utility.d.ts: add --- typescript/types/utility.d.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 typescript/types/utility.d.ts diff --git a/typescript/types/utility.d.ts b/typescript/types/utility.d.ts new file mode 100644 index 000000000..9deabd118 --- /dev/null +++ b/typescript/types/utility.d.ts @@ -0,0 +1,5 @@ +type Omit = Pick>; + +type Pick = { + [P in K]: T[P]; +}; From 3ef013d002ccad4a2ebae9b2c51d59552d55ca15 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:01:12 +0100 Subject: [PATCH 15/26] exstats.d.ts: permit optional notify input, but non-optional on `ex.state.notify` --- typescript/types/exstats.d.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/typescript/types/exstats.d.ts b/typescript/types/exstats.d.ts index 7c68b6d59..7e980882d 100644 --- a/typescript/types/exstats.d.ts +++ b/typescript/types/exstats.d.ts @@ -10,17 +10,25 @@ declare module ExStats { type Options = { paceLength?: number, - notify?: Notify, + notify?: NotifyInput, }; type Notify = { - [key in Ids & ("dist" | "step" | "time")]?: { - // optional when passed in ^ - increment?: number, - next?: number, + [key in Ids & ("dist" | "step" | "time")]: { + increment: number, + next: number, } }; + type NotifyInput = { + [K in keyof Notify]?: + Omit< + Notify[K], "next" + > & { + next?: number, + }; + }; + type StatsInst = { stats: Stats, state: State, From 160093a07bdbca3b9c4ec237c853890cb702fe20 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:02:33 +0100 Subject: [PATCH 16/26] pace: fix split calcs i.e. time/distance mix-up --- apps/pace/app.ts | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 5a992eda5..f6f3dcd80 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -14,7 +14,15 @@ const exs = require("exstats").getStats( let drawTimeout: TimeoutId | undefined; -const splits: number[] = []; // times +type Dist = number & { brand: 'dist' }; +type Time = number & { brand: 'time' }; + +type Split = { + dist: Dist, + time: Time, +}; + +const splits: Split[] = []; // times let splitOffset = 0, splitOffsetPx = 0; const GPS_TIMEOUT_MS = 30000; @@ -55,9 +63,9 @@ const layout = new Layout({ lazy: true }); -const calculatePace = (time: number, dist: number) => { - if (dist === 0) return 0; - return time / dist / 1000 / 60; +const calculatePace = (split: Split) => { + if (split.dist === 0) return 0; + return split.time / split.dist / 1000 / 60; }; const draw = () => { @@ -103,7 +111,7 @@ const drawSplits = () => { const w = g.getWidth(); const h = g.getHeight(); - const max = splits.reduce((a, x) => Math.max(a, x), 0); + const max = splits.reduce((a, s) => Math.max(a, s.time), 0); g.setFont("6x8", 2).setFontAlign(-1, -1); @@ -115,10 +123,10 @@ const drawSplits = () => { const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; if (y > h) break; - const size = w * split / max; // Scale bar height based on pace + const size = w * split.time / max; // Scale bar height based on pace g.setColor("#00f").fillRect(0, y, size, y + barSize); - const splitPace = calculatePace(split, 1); // Pace per km + const splitPace = calculatePace(split); // Pace per km drawSplit(i, y, splitPace); } @@ -163,15 +171,15 @@ const onButton = () => { exs.start(); // aka reset exs.stats.dist.on("notify", (dist) => { - const prev = splits[splits.length - 1] || 0; + const prevDist = splits[splits.length - 1]?.dist ?? 0; const totalDist = dist.getValue(); - let thisSplit = totalDist - prev; - const prevTime = splits.reduce((a, b) => a + b, 0); - let time = exs.state.duration - prevTime; + let thisSplit = totalDist - prevDist; + const prevTime = splits.reduce((t, s) => t + s.time, 0); + let thisTime = exs.state.duration - prevTime; - while(thisSplit > 0) { - splits.push(time); - time = 0; // if we've jumped more than 1k, credit the time to the first split + while(thisSplit > 1000) { + splits.push({ dist: thisSplit as Dist, time: thisTime as Time }); + thisTime = 0; // if we've jumped more than 1k, credit the time to the first split thisSplit -= 1000; } }); From 09a46eccfd19736d50fadacde21df4a1cb2d27fa Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:02:59 +0100 Subject: [PATCH 17/26] pace: compensate for next split --- apps/pace/app.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index f6f3dcd80..ad95ff732 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -182,6 +182,9 @@ exs.stats.dist.on("notify", (dist) => { thisTime = 0; // if we've jumped more than 1k, credit the time to the first split thisSplit -= 1000; } + + // subtract off the next split notify + exs.state.notify.dist.next -= thisSplit; }); Bangle.on('lock', locked => { From fa0a6f2ab8105421b7ddf97d865df327b63b6251 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 20:30:44 +0100 Subject: [PATCH 18/26] pace: save splits --- apps/pace/app.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index ad95ff732..aa542c36b 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -11,6 +11,7 @@ const exs = require("exstats").getStats( }, }, ); +const S = require("Storage"); let drawTimeout: TimeoutId | undefined; @@ -185,6 +186,8 @@ exs.stats.dist.on("notify", (dist) => { // subtract off the next split notify exs.state.notify.dist.next -= thisSplit; + + S.writeJSON("pace.json", { splits }); }); Bangle.on('lock', locked => { From 625f065834542797c81a962305bc51035a5aae95 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 20:31:17 +0100 Subject: [PATCH 19/26] pace: fix distance totalling --- apps/pace/app.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index aa542c36b..79244865a 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -172,11 +172,15 @@ const onButton = () => { exs.start(); // aka reset exs.stats.dist.on("notify", (dist) => { - const prevDist = splits[splits.length - 1]?.dist ?? 0; + const prev = { time: 0, dist: 0 }; + for(const s of splits){ + prev.time += s.time; + prev.dist += s.dist; + } + const totalDist = dist.getValue(); - let thisSplit = totalDist - prevDist; - const prevTime = splits.reduce((t, s) => t + s.time, 0); - let thisTime = exs.state.duration - prevTime; + let thisSplit = totalDist - prev.dist; + let thisTime = exs.state.duration - prev.time; while(thisSplit > 1000) { splits.push({ dist: thisSplit as Dist, time: thisTime as Time }); From a19d56c860530ff4295b802527d2417617b4b7d0 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 20:36:00 +0100 Subject: [PATCH 20/26] pace: backlight-on, on twist --- apps/pace/app.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 79244865a..0ecb4b78f 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -215,6 +215,10 @@ Bangle.on('drag', e => { draw(); }); +Bangle.on('twist', () => { + Bangle.setBacklight(1); +}); + Bangle.loadWidgets(); Bangle.drawWidgets(); From 281b178613121a88ade20e63d422f0527f7e15e3 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 14 Oct 2024 18:38:06 +0100 Subject: [PATCH 21/26] pace: fix pace calculation --- apps/pace/app.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/pace/app.ts b/apps/pace/app.ts index 0ecb4b78f..d71442684 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -64,11 +64,6 @@ const layout = new Layout({ lazy: true }); -const calculatePace = (split: Split) => { - if (split.dist === 0) return 0; - return split.time / split.dist / 1000 / 60; -}; - const draw = () => { if (!exs.state.active) { // no draw-timeout here, only on user interaction @@ -104,6 +99,9 @@ const formatDuration = (ms: number) => { return `${pad2(tm.m)}:${pad2(tm.s)}`; }; +// divide by actual distance, scale to milliseconds +const calculatePace = (split: Split) => formatDuration(split.time / split.dist * 1000); + const drawSplits = () => { g.clearRect(Bangle.appRect); From 566042bdac70ff51a8918bfdd77853d1eb9ae75c Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 14 Oct 2024 18:38:31 +0100 Subject: [PATCH 22/26] typescript: generate types (boolean -> ShortBoolean) also patch btadv --- apps/btadv/app.ts | 2 +- typescript/types/main.d.ts | 312 +++++++++++++++++++++++++++++-------- 2 files changed, 246 insertions(+), 68 deletions(-) diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 4ae75fae3..0c1803fdb 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -720,7 +720,7 @@ const hook = (enable: boolean) => { // --- intervals --- const setIntervals = ( - locked: boolean = Bangle.isLocked(), + locked: ShortBoolean = Bangle.isLocked(), connected: boolean = NRF.getSecurityStatus().connected, ) => { changeInterval( diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts index 8627e1323..e00577afb 100644 --- a/typescript/types/main.d.ts +++ b/typescript/types/main.d.ts @@ -586,7 +586,7 @@ declare class ESP32 { * @param {boolean} enable - switches Bluetooth on or off * @url http://www.espruino.com/Reference#l_ESP32_enableBLE */ - static enableBLE(enable: boolean): void; + static enableBLE(enable: ShortBoolean): void; /** * Switches Wifi off/on, removes saved code from Flash, resets the board, and on @@ -595,7 +595,7 @@ declare class ESP32 { * @param {boolean} enable - switches Wifi on or off * @url http://www.espruino.com/Reference#l_ESP32_enableWifi */ - static enableWifi(enable: boolean): void; + static enableWifi(enable: ShortBoolean): void; /** * This function is useful for ESP32 [OTA Updates](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/ota.html) @@ -609,7 +609,7 @@ declare class ESP32 { * @param {boolean} isValid - Set whether this app is valid or not. If `isValid==false` the device will reboot. * @url http://www.espruino.com/Reference#l_ESP32_setOTAValid */ - static setOTAValid(isValid: boolean): void; + static setOTAValid(isValid: ShortBoolean): void; } @@ -934,11 +934,11 @@ declare class NRF { /** * Called when Bluetooth advertising starts or stops on Espruino * @param {string} event - The event to listen to. - * @param {(isAdvertising: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(isAdvertising: ShortBoolean) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `isAdvertising` Whether we are advertising or not * @url http://www.espruino.com/Reference#l_NRF_advertising */ - static on(event: "advertising", callback: (isAdvertising: boolean) => void): void; + static on(event: "advertising", callback: (isAdvertising: ShortBoolean) => void): void; /** * Called during the bonding process to update on status @@ -1064,7 +1064,7 @@ declare class NRF { * @returns {any} MAC address - a string of the form 'aa:bb:cc:dd:ee:ff' * @url http://www.espruino.com/Reference#l_NRF_getAddress */ - static getAddress(current: boolean): any; + static getAddress(current: ShortBoolean): any; /** * Set this device's default Bluetooth MAC address: @@ -1643,7 +1643,7 @@ declare class NRF { * @param {boolean} lowPower - Whether the connection is low power or not * @url http://www.espruino.com/Reference#l_NRF_setLowPowerConnection */ - static setLowPowerConnection(lowPower: boolean): void; + static setLowPowerConnection(lowPower: ShortBoolean): void; /** * Enables NFC and starts advertising the given URL. For example: @@ -1770,7 +1770,7 @@ declare class NRF { * @param {boolean} positive - `true` for positive action, `false` for negative * @url http://www.espruino.com/Reference#l_NRF_ancsAction */ - static ancsAction(uid: number, positive: boolean): void; + static ancsAction(uid: number, positive: ShortBoolean): void; /** * Get ANCS info for a notification event received via `E.ANCS`, e.g.: @@ -2059,7 +2059,7 @@ declare class NRF { * @param {boolean} whitelisting - Are we using a whitelist? (default false) * @url http://www.espruino.com/Reference#l_NRF_setWhitelist */ - static setWhitelist(whitelisting: boolean): void; + static setWhitelist(whitelisting: ShortBoolean): void; /** * When connected, Bluetooth LE devices communicate at a set interval. Lowering the @@ -2245,7 +2245,7 @@ declare class NRF { * @returns {any} A promise * @url http://www.espruino.com/Reference#l_NRF_startBonding */ - static startBonding(forceRepair: boolean): any; + static startBonding(forceRepair: ShortBoolean): any; } @@ -2350,7 +2350,7 @@ declare class Pixl { * @param {boolean} isOn - True if the LCD should be on, false if not * @url http://www.espruino.com/Reference#l_Pixl_setLCDPower */ - static setLCDPower(isOn: boolean): void; + static setLCDPower(isOn: ShortBoolean): void; /** * Writes a command directly to the ST7567 LCD controller @@ -2480,7 +2480,7 @@ declare class url { * @returns {any} An object containing options for ```http.request``` or ```http.get```. Contains `method`, `host`, `path`, `pathname`, `search`, `port` and `query` * @url http://www.espruino.com/Reference#l_url_parse */ - static parse(urlStr: any, parseQuery: boolean): any; + static parse(urlStr: any, parseQuery: ShortBoolean): any; } @@ -3730,7 +3730,7 @@ declare class Qwiic { * @returns {any} The same Qwiic object (for call chaining) * @url http://www.espruino.com/Reference#l_Qwiic_setPower */ - setPower(isOn: boolean): any; + setPower(isOn: ShortBoolean): any; /** * @returns {any} An I2C object using this Qwiic connector, already set up @@ -3800,11 +3800,11 @@ declare class Bangle { /** * Has the watch been moved so that it is face-up, or not face up? * @param {string} event - The event to listen to. - * @param {(up: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(up: ShortBoolean) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `up` `true` if face-up * @url http://www.espruino.com/Reference#l_Bangle_faceUp */ - static on(event: "faceUp", callback: (up: boolean) => void): void; + static on(event: "faceUp", callback: (up: ShortBoolean) => void): void; /** * This event happens when the watch has been twisted around it's axis - for @@ -3819,11 +3819,11 @@ declare class Bangle { /** * Is the battery charging or not? * @param {string} event - The event to listen to. - * @param {(charging: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(charging: ShortBoolean) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `charging` `true` if charging * @url http://www.espruino.com/Reference#l_Bangle_charging */ - static on(event: "charging", callback: (charging: boolean) => void): void; + static on(event: "charging", callback: (charging: ShortBoolean) => void): void; /** * Magnetometer/Compass data available with `{x,y,z,dx,dy,dz,heading}` object as a @@ -3848,7 +3848,7 @@ declare class Bangle { * Raw NMEA GPS / u-blox data messages received as a string * To get this event you must turn the GPS on with `Bangle.setGPSPower(1)`. * @param {string} event - The event to listen to. - * @param {(nmea: any, dataLoss: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(nmea: any, dataLoss: ShortBoolean) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `nmea` A string containing the raw NMEA data from the GPS * * `dataLoss` This is set to true if some lines of GPS data have previously been lost (eg because system was too busy to queue up a GPS-raw event) * @url http://www.espruino.com/Reference#l_Bangle_GPS-raw @@ -3942,31 +3942,31 @@ declare class Bangle { * Has the screen been turned on or off? Can be used to stop tasks that are no * longer useful if nothing is displayed. Also see `Bangle.isLCDOn()` * @param {string} event - The event to listen to. - * @param {(on: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(on: ShortBoolean) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `on` `true` if screen is on * @url http://www.espruino.com/Reference#l_Bangle_lcdPower */ - static on(event: "lcdPower", callback: (on: boolean) => void): void; + static on(event: "lcdPower", callback: (on: ShortBoolean) => void): void; /** * Has the backlight been turned on or off? Can be used to stop tasks that are no * longer useful if want to see in sun screen only. Also see `Bangle.isBacklightOn()` * @param {string} event - The event to listen to. - * @param {(on: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(on: ShortBoolean) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `on` `true` if backlight is on * @url http://www.espruino.com/Reference#l_Bangle_backlight */ - static on(event: "backlight", callback: (on: boolean) => void): void; + static on(event: "backlight", callback: (on: ShortBoolean) => void): void; /** * Has the screen been locked? Also see `Bangle.isLocked()` * @param {string} event - The event to listen to. - * @param {(on: boolean, reason: string) => void} callback - A function that is executed when the event occurs. Its arguments are: + * @param {(on: ShortBoolean, reason: string) => void} callback - A function that is executed when the event occurs. Its arguments are: * * `on` `true` if screen is locked, `false` if it is unlocked and touchscreen/buttons will work * * `reason` (2v20 onwards) If known, the reason for locking/unlocking - 'button','js','tap','doubleTap','faceUp','twist','timeout' * @url http://www.espruino.com/Reference#l_Bangle_lock */ - static on(event: "lock", callback: (on: boolean, reason: string) => void): void; + static on(event: "lock", callback: (on: ShortBoolean, reason: string) => void): void; /** * If the watch is tapped, this event contains information on the way it was @@ -4103,7 +4103,7 @@ declare class Bangle { * @param {boolean} isOn - True if the LCD backlight should be on, false if not * @url http://www.espruino.com/Reference#l_Bangle_setBacklight */ - static setBacklight(isOn: boolean): void; + static setBacklight(isOn: ShortBoolean): void; /** * This function can be used to turn Bangle.js's LCD off or on. @@ -4122,7 +4122,7 @@ declare class Bangle { * @param {boolean} isOn - True if the LCD should be on, false if not * @url http://www.espruino.com/Reference#l_Bangle_setLCDPower */ - static setLCDPower(isOn: boolean): void; + static setLCDPower(isOn: ShortBoolean): void; /** * This function can be used to adjust the brightness of Bangle.js's display, and @@ -4309,6 +4309,9 @@ declare class Bangle { * off * * `btnLoadTimeout` how many milliseconds does the home button have to be pressed * for before the clock is reloaded? 1500ms default, or 0 means never. + * * `manualWatchdog` if set, this disables automatic kicking of the watchdog timer + * from the interrupt (when the button isn't held). You will then have to manually + * call `E.kickWatchdog()` from your code or the watch will reset after ~5 seconds. * * `hrmPollInterval` set the requested poll interval (in milliseconds) for the * heart rate monitor. On Bangle.js 2 only 10,20,40,80,160,200 ms are supported, * and polling rate may not be exact. The algorithm's filtering is tuned for @@ -4362,7 +4365,7 @@ declare class Bangle { * @param {boolean} isLocked - `true` if the Bangle is locked (no user input allowed) * @url http://www.espruino.com/Reference#l_Bangle_setLocked */ - static setLocked(isLocked: boolean): void; + static setLocked(isLocked: ShortBoolean): void; /** * Also see the `Bangle.lock` event @@ -4925,7 +4928,7 @@ declare class Bangle { * @param {boolean} noReboot - Do not reboot the watch when done (default false, so will reboot) * @url http://www.espruino.com/Reference#l_Bangle_factoryReset */ - static factoryReset(noReboot: boolean): void; + static factoryReset(noReboot: ShortBoolean): void; /** * Returns the rectangle on the screen that is currently reserved for the app. @@ -5170,7 +5173,7 @@ declare class BluetoothRemoteGATTServer { * @returns {any} A `Promise` that is resolved (or rejected) when the bonding is complete * @url http://www.espruino.com/Reference#l_BluetoothRemoteGATTServer_startBonding */ - startBonding(forceRePair: boolean): Promise; + startBonding(forceRePair: ShortBoolean): Promise; /** * Return an object with information about the security state of the current @@ -5453,9 +5456,10 @@ declare class Graphics { * An object of other options. `{ zigzag : true/false(default), vertical_byte : true/false(default), msb : true/false(default), color_order: 'rgb'(default),'bgr',etc }` * `zigzag` = whether to alternate the direction of scanlines for rows * `vertical_byte` = whether to align bits in a byte vertically or not - * `msb` = when bits<8, store pixels most significant bit first, when bits>8, store most significant byte first + * `msb` = when bits<8, store pixels most significant bit first, when bits>8, store most significant byte first (as of 2v25, msb:true is default) * `interleavex` = Pixels 0,2,4,etc are from the top half of the image, 1,3,5,etc from the bottom half. Used for P3 LED panels. * `color_order` = re-orders the colour values that are supplied via setColor + * `buffer` = if specified, createArrayBuffer won't create a new buffer but will use the given one * @returns {any} The new Graphics object * @url http://www.espruino.com/Reference#l_Graphics_createArrayBuffer */ @@ -5586,7 +5590,7 @@ declare class Graphics { * @param {boolean} [all] - [optional] (only on some devices) If `true` then copy all pixels, not just those that have changed. * @url http://www.espruino.com/Reference#l_Graphics_flip */ - flip(all?: boolean): void; + flip(all?: ShortBoolean): void; /** * On Graphics instances with an offscreen buffer, this is an `ArrayBuffer` that @@ -5650,7 +5654,7 @@ declare class Graphics { * @returns {any} The instance of Graphics this was called on, to allow call chaining * @url http://www.espruino.com/Reference#l_Graphics_clear */ - clear(reset?: boolean): Graphics; + clear(reset?: ShortBoolean): Graphics; /** * Fill a rectangular area in the Foreground Color @@ -6584,7 +6588,7 @@ declare class WioLTE { * @param {boolean} onoff - Whether to turn the Grove connectors power on or off (D38/D39 are always powered) * @url http://www.espruino.com/Reference#l_WioLTE_setGrovePower */ - static setGrovePower(onoff: boolean): void; + static setGrovePower(onoff: ShortBoolean): void; /** * Turn power to the WIO's LED on or off. @@ -6594,7 +6598,7 @@ declare class WioLTE { * @param {boolean} onoff - true = on, false = off * @url http://www.espruino.com/Reference#l_WioLTE_setLEDPower */ - static setLEDPower(onoff: boolean): void; + static setLEDPower(onoff: ShortBoolean): void; /** * @returns {any} @@ -6641,33 +6645,83 @@ declare class WioLTE { * @url http://www.espruino.com/Reference#Waveform */ declare class Waveform { + /** + * Event emitted when playback has finished + * @param {string} event - The event to listen to. + * @param {(buffer: any) => void} callback - A function that is executed when the event occurs. Its arguments are: + * * `buffer` the last played buffer + * @url http://www.espruino.com/Reference#l_Waveform_finish + */ + static on(event: "finish", callback: (buffer: any) => void): void; + + /** + * When in double-buffered mode, this event is emitted when the `Waveform` class swaps to playing a new buffer - so you should then fill this current buffer up with new data. + * @param {string} event - The event to listen to. + * @param {(buffer: any) => void} callback - A function that is executed when the event occurs. Its arguments are: + * * `buffer` the last played buffer (which now needs to be filled ready for playback) + * @url http://www.espruino.com/Reference#l_Waveform_buffer + */ + static on(event: "buffer", callback: (buffer: any) => void): void; + /** * Create a waveform class. This allows high speed input and output of waveforms. * It has an internal variable called `buffer` (as well as `buffer2` when * double-buffered - see `options` below) which contains the data to input/output. + * Options can contain: + * ```JS + * { + * doubleBuffer : bool // whether to allocate two buffers or not (default false) + * bits : 8/16 // the amount of bits to use (default 8). + * } + * ``` * When double-buffered, a 'buffer' event will be emitted each time a buffer is * finished with (the argument is that buffer). When the recording stops, a * 'finish' event will be emitted (with the first argument as the buffer). + * ```JS + * // Output a sine wave + * var w = new Waveform(1000); + * for (var i=0;i<1000;i++) w.buffer[i]=128+120*Math.sin(i/2); + * analogWrite(H0, 0.5, {freq:80000}); // set up H0 to output an analog value by PWM + * w.on("finish", () => print("Done!")) + * w.startOutput(H0,8000); // start playback + * // On 2v25, from Storage + * var f = require("Storage").read("sound.pcm"); + * var w = new Waveform(E.toArrayBuffer(f)); + * w.on("finish", () => print("Done!")) + * w.startOutput(H0,8000); // start playback + * ``` + * See https://www.espruino.com/Waveform for more examples. * @constructor * - * @param {number} samples - The number of samples - * @param {any} options - Optional options struct `{doubleBuffer:bool, bits : 8/16}` where: `doubleBuffer` is whether to allocate two buffers or not (default false), and bits is the amount of bits to use (default 8). + * @param {any} samples - The number of samples to allocate as an integer, *or* an arraybuffer (2v25+) containing the samples + * @param {any} [options] - [optional] options struct `{ doubleBuffer:bool, bits : 8/16 }` (see below) * @returns {any} An Waveform object * @url http://www.espruino.com/Reference#l_Waveform_Waveform */ - static new(samples: number, options: any): any; + static new(samples: any, options?: any): any; /** * Will start outputting the waveform on the given pin - the pin must have * previously been initialised with analogWrite. If not repeating, it'll emit a * `finish` event when it is done. + * ``` + * { + * time : float, // the that the waveform with start output at, e.g. `getTime()+1` (otherwise it is immediate) + * repeat : bool, // whether to repeat the given sample + * npin : Pin, // If specified, the waveform is output across two pins (see below) + * } + * ``` + * Using `npin` allows you to split the Waveform output between two pins and hence avoid + * any DC bias (or need to capacitor), for instance you could attach a speaker to `H0` and + * `H1` on Jolt.js. When the value in the waveform was at 50% both outputs would be 0, + * below 50% the signal would be on `npin` with `pin` as 0, and above 50% it would be on `pin` with `npin` as 0. * * @param {Pin} output - The pin to output on * @param {number} freq - The frequency to output each sample at - * @param {any} options - Optional options struct `{time:float,repeat:bool}` where: `time` is the that the waveform with start output at, e.g. `getTime()+1` (otherwise it is immediate), `repeat` is a boolean specifying whether to repeat the give sample + * @param {any} [options] - [optional] options struct `{time:float, repeat:bool, npin:Pin}` (see below) * @url http://www.espruino.com/Reference#l_Waveform_startOutput */ - startOutput(output: Pin, freq: number, options: any): void; + startOutput(output: Pin, freq: number, options?: any): void; /** * Will start inputting the waveform on the given pin that supports analog. If not @@ -6675,10 +6729,10 @@ declare class Waveform { * * @param {Pin} output - The pin to output on * @param {number} freq - The frequency to output each sample at - * @param {any} options - Optional options struct `{time:float,repeat:bool}` where: `time` is the that the waveform with start output at, e.g. `getTime()+1` (otherwise it is immediate), `repeat` is a boolean specifying whether to repeat the give sample + * @param {any} [options] - [optional] options struct `{time:float,repeat:bool}` where: `time` is the that the waveform with start output at, e.g. `getTime()+1` (otherwise it is immediate), `repeat` is a boolean specifying whether to repeat the give sample * @url http://www.espruino.com/Reference#l_Waveform_startInput */ - startInput(output: Pin, freq: number, options: any): void; + startInput(output: Pin, freq: number, options?: any): void; /** * Stop a waveform that is currently outputting @@ -7869,11 +7923,24 @@ interface MathConstructor { pow(x: number, y: number): number; /** - * @returns {number} A random number between 0 and 1 + * @returns {number} A random number X, where `0 <= X < 1` * @url http://www.espruino.com/Reference#l_Math_random */ random(): number; + /** + * (Added in 2v25) Returns a random integer `X`, where `0 <= X < range`, or `-2147483648 <= X <= 2147483647` if `range <= 0` or `undefined` + * If `range` is supplied, this value is created using `modulo` of a 31 bit integer, so as `val` gets larger (24+ bits) + * the values produced will be less randomly distributed, and no values above `0x7FFFFFFF` will ever be returned. + * If `val==undefined` or `val<=0` a **32 bit** random number will be returned as an int (`-2147483648` .. `2147483647`). + * **Note:** this is not part of the JS spec, but is included in Espruino as it makes a lot of sense on embedded targets + * + * @param {number} range - How big a random number do we want + * @returns {number} A random integer + * @url http://www.espruino.com/Reference#l_Math_randInt + */ + randInt(range: number): number; + /** * * @param {number} x - The value to round @@ -8294,6 +8361,10 @@ declare class E { * menu is removed * * (Bangle.js 2) `scroll : int` - an integer specifying how much the initial * menu should be scrolled by + * * (Bangle.js 2) The mapped functions can consider the touch event that interacted with the entry: + * `"Entry" : function(touch) { ... }` + * * This is also true of `onchange` mapped functions in entry objects: + * `onchange : (value, touch) => { ... }` * * The object returned by `E.showMenu` contains: * * (Bangle.js 2) `scroller` - the object returned by `E.showScroller` - * `scroller.scroll` returns the amount the menu is currently scrolled by @@ -8397,6 +8468,7 @@ declare class E { * draw : function(idx, rect) { ... } * // a function to call when the item is selected, touch parameter is only relevant * // for Bangle.js 2 and contains the coordinates touched inside the selected item + * // as well as the type of the touch - see `Bangle.touch`. * select : function(idx, touch) { ... } * // optional function to be called when 'back' is tapped * back : function() { ...} @@ -9047,6 +9119,23 @@ declare class E { */ static setClock(options: number | { M: number, N: number, P: number, Q: number, latency?: number, PCLK?: number, PCLK2?: number }): number; + /** + * On boards other than STM32 this currently just returns `undefined` + * ### STM32 + * See `E.setClock` for more information. + * Returns: + * ``` + * { + * sysclk, hclk, pclk1, pclk2, // various clocks in Hz + * M, N, P, Q, PCLK1, PCLK2 // STM32F4: currently set divisors + * RTCCLKSource : "LSI/LSE/HSE_Div#" // STM32F4 source for RTC clock + * } + * ``` + * @returns {any} An object containing information about the current clock + * @url http://www.espruino.com/Reference#l_E_getClock + */ + static getClock(): any; + /** * Changes the device that the JS console (otherwise known as the REPL) is attached * to. If the console is on a device, that device can be used for programming @@ -9186,7 +9275,7 @@ declare class E { * @returns {number} The address of the given variable * @url http://www.espruino.com/Reference#l_E_getAddressOf */ - static getAddressOf(v: any, flatAddress: boolean): number; + static getAddressOf(v: any, flatAddress: ShortBoolean): number; /** * Take each element of the `from` array, look it up in `map` (or call @@ -9519,7 +9608,7 @@ declare class E { * @returns {number} The RTC prescaler's current value * @url http://www.espruino.com/Reference#l_E_getRTCPrescaler */ - static getRTCPrescaler(calibrate: boolean): number; + static getRTCPrescaler(calibrate: ShortBoolean): number; /** * This function returns an object detailing the current **estimated** power usage @@ -9646,7 +9735,7 @@ declare class OneWire { * @param {boolean} power - Whether to leave power on after write (default is false) * @url http://www.espruino.com/Reference#l_OneWire_write */ - write(data: any, power: boolean): void; + write(data: any, power: ShortBoolean): void; /** * Read a byte @@ -10140,6 +10229,8 @@ interface Array { /** * Return an array which is made from the following: ```A.map(function) = * [function(A[0]), function(A[1]), ...]``` + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.map(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function used to map one item to another * @param {any} [thisArg] - [optional] If specified, the function is called with 'this' set to thisArg @@ -10150,6 +10241,8 @@ interface Array { /** * Executes a provided function once per array element. + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.forEach(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function to be executed * @param {any} [thisArg] - [optional] If specified, the function is called with 'this' set to thisArg @@ -10160,6 +10253,8 @@ interface Array { /** * Return an array which contains only those elements for which the callback * function returns 'true' + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.filter(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function to be executed * @param {any} [thisArg] - [optional] If specified, the function is called with 'this' set to thisArg @@ -10176,6 +10271,8 @@ interface Array { * ["Hello","There","World"].find(a=>a[0]=="T") * // returns "There" * ``` + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.find(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function to be executed * @returns {any} The array element where `function` returns `true`, or `undefined` @@ -10191,6 +10288,8 @@ interface Array { * ["Hello","There","World"].findIndex(a=>a[0]=="T") * // returns 1 * ``` + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.findIndex(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function to be executed * @returns {any} The array element's index where `function` returns `true`, or `-1` @@ -10201,6 +10300,8 @@ interface Array { /** * Return 'true' if the callback returns 'true' for any of the elements in the * array + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.some(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function to be executed * @param {any} [thisArg] - [optional] If specified, the function is called with 'this' set to thisArg @@ -10211,6 +10312,8 @@ interface Array { /** * Return 'true' if the callback returns 'true' for every element in the array + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.every(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} function - Function to be executed * @param {any} [thisArg] - [optional] If specified, the function is called with 'this' set to thisArg @@ -10223,6 +10326,8 @@ interface Array { * Execute `previousValue=initialValue` and then `previousValue = * callback(previousValue, currentValue, index, array)` for each element in the * array, and finally return previousValue. + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.reduce(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} callback - Function used to reduce the array * @param {any} initialValue - if specified, the initial value to pass to the function @@ -10274,6 +10379,8 @@ interface Array { /** * Do an in-place quicksort of the array + * **Note:** Do not modify the array you're iterating over from inside the callback (`a.sort(()=>a.push(0))`). + * It will cause non-spec-compliant behaviour. * * @param {any} var - A function to use to compare array elements (or undefined) * @returns {any} This array object @@ -10339,8 +10446,6 @@ interface JSONConstructor { /** * Parse the given JSON string into a JavaScript object - * NOTE: This implementation uses eval() internally, and as such it is unsafe as it - * can allow arbitrary JS commands to be executed. * * @param {any} string - A JSON string * @returns {any} The JavaScript object created by parsing the data string @@ -10922,7 +11027,7 @@ declare class Serial { * @param {boolean} force - Whether to force the console to this port * @url http://www.espruino.com/Reference#l_Serial_setConsole */ - setConsole(force: boolean): void; + setConsole(force: ShortBoolean): void; /** * Setup this Serial port with the given baud rate and options. @@ -11595,7 +11700,7 @@ declare class Pin { * @param {boolean} value - Whether to set output high (true/1) or low (false/0) * @url http://www.espruino.com/Reference#l_Pin_write */ - write(value: boolean): void; + write(value: ShortBoolean): void; /** * Sets the output state of the pin to the parameter given at the specified time. @@ -11603,10 +11708,10 @@ declare class Pin { * you need to use `pin.write(0)` or `pinMode(pin, 'output')` first. * * @param {boolean} value - Whether to set output high (true/1) or low (false/0) - * @param {number} time - Time at which to write + * @param {number} time - Time at which to write (in seconds) * @url http://www.espruino.com/Reference#l_Pin_writeAtTime */ - writeAtTime(value: boolean, time: number): void; + writeAtTime(value: ShortBoolean, time: number): void; /** * Return the current mode of the given pin. See `pinMode` for more information. @@ -11646,7 +11751,7 @@ declare class Pin { * @param {any} time - A time in milliseconds, or an array of times (in which case a square wave will be output starting with a pulse of 'value') * @url http://www.espruino.com/Reference#l_Pin_pulse */ - pulse(value: boolean, time: any): void; + pulse(value: ShortBoolean, time: any): void; /** * (Added in 2v20) Get the analogue value of the given pin. See `analogRead` for more information. @@ -11676,12 +11781,14 @@ declare class Pin { * Get information about this pin and its capabilities. Of the form: * ``` * { - * "port" : "A", // the Pin's port on the chip - * "num" : 12, // the Pin's number - * "in_addr" : 0x..., // (if available) the address of the pin's input address in bit-banded memory (can be used with peek) - * "out_addr" : 0x..., // (if available) the address of the pin's output address in bit-banded memory (can be used with poke) - * "analog" : { ADCs : [1], channel : 12 }, // If analog input is available - * "functions" : { + * "port" : "A", // the Pin's port on the chip + * "num" : 12, // the Pin's number + * "mode" : (2v25+) // string: the pin's mode (same as Pin.getMode()) + * "output" : (2v25+) // 0/1: the state of the pin's output register + * "in_addr" : 0x..., // (if available) the address of the pin's input address in bit-banded memory (can be used with peek) + * "out_addr" : 0x..., // (if available) the address of the pin's output address in bit-banded memory (can be used with poke) + * "analog" : { ADCs : [1], channel : 12 }, // If analog input is available + * "functions" : { * "TIM1":{type:"CH1, af:0}, * "I2C3":{type:"SCL", af:1} * } @@ -11716,6 +11823,67 @@ declare const Boolean: BooleanConstructor // GLOBALS +/** + * @returns {any} An object containing the pins for the Qwiic connector on Curio `{sda,scl}` + * @url http://www.espruino.com/Reference#l__global_Q + */ +declare const Q: Qwiic; + +/** + * @returns {Pin} The pin for the servo motor + * @url http://www.espruino.com/Reference#l__global_SERVO + */ +declare const SERVO: Pin; + +/** + * @returns {Pin} The pin for the IR LED + * @url http://www.espruino.com/Reference#l__global_IRLED + */ +declare const IRLED: Pin; + +/** + * @returns {Pin} The pin for the left IR sensor + * @url http://www.espruino.com/Reference#l__global_IRL + */ +declare const IRL: Pin; + +/** + * @returns {Pin} The pin for the left motor + * @url http://www.espruino.com/Reference#l__global_ML1 + */ +declare const ML1: Pin; + +/** + * @returns {Pin} The pin for the left motor + * @url http://www.espruino.com/Reference#l__global_ML2 + */ +declare const ML2: Pin; + +/** + * @returns {Pin} The pin for the right IR sensor + * @url http://www.espruino.com/Reference#l__global_IRR + */ +declare const IRR: Pin; + +/** + * @returns {Pin} The pin for the right motor + * @url http://www.espruino.com/Reference#l__global_MR1 + */ +declare const MR1: Pin; + +/** + * @returns {Pin} The pin for the right motor + * @url http://www.espruino.com/Reference#l__global_MR2 + */ +declare const MR2: Pin; + +/** + * + * @param {any} col - The colours to use, a 24 element array (8 x RGB) + * @url http://www.espruino.com/Reference#l__global_led + */ +declare function led(col: any): void; + /** * The pin marked SDA on the Arduino pin footprint. This is connected directly to * pin A4. @@ -12042,7 +12210,7 @@ declare function setSleepIndicator(pin: any): void; * @param {boolean} sleep * @url http://www.espruino.com/Reference#l__global_setDeepSleep */ -declare function setDeepSleep(sleep: boolean): void; +declare function setDeepSleep(sleep: ShortBoolean): void; /** * Output current interpreter state in a text form such that it can be copied to a @@ -12120,7 +12288,7 @@ declare function save(): void; * @param {boolean} clearFlash - Remove saved code from flash as well * @url http://www.espruino.com/Reference#l__global_reset */ -declare function reset(clearFlash: boolean): void; +declare function reset(clearFlash: ShortBoolean): void; /** * Fill the console with the contents of the given function, so you can edit it. @@ -12140,7 +12308,7 @@ declare function edit(funcName: any): void; * @param {boolean} echoOn * @url http://www.espruino.com/Reference#l__global_echo */ -declare function echo(echoOn: boolean): void; +declare function echo(echoOn: ShortBoolean): void; /** * Return the current system time in Seconds (as a floating point number) @@ -12541,7 +12709,7 @@ declare function shiftOut(pins: Pin | Pin[], options: { clk?: Pin, clkPol?: bool * // setting irq:true will call that function in the interrupt itself * irq : false(default) * // Advanced: If specified, the given pin will be read whenever the watch is called - * // and the state will be included as a 'data' field in the callback + * // and the state will be included as a 'data' field in the callback (`debounce:0` is required) * data : pin * // Advanced: On Nordic devices, a watch may be 'high' or 'low' accuracy. By default low * // accuracy is used (which is better for power consumption), but this means that @@ -12558,7 +12726,7 @@ declare function shiftOut(pins: Pin | Pin[], options: { clk?: Pin, clkPol?: bool * When using `edge:'rising'` or `edge:'falling'`, this is not the same as when * the function was last called. * * `data` is included if `data:pin` was specified in the options, and can be - * used for reading in clocked data + * used for reading in clocked data. It will only work if `debounce:0` is used * For instance, if you want to measure the length of a positive pulse you could * use `setWatch(function(e) { console.log(e.time-e.lastTime); }, BTN, { * repeat:true, edge:'falling' });`. This will only be called on the falling edge @@ -12597,6 +12765,16 @@ declare function clearWatch(id: number): void; declare function clearWatch(): void; declare const global: { + Q: typeof Q; + SERVO: typeof SERVO; + IRLED: typeof IRLED; + IRL: typeof IRL; + ML1: typeof ML1; + ML2: typeof ML2; + IRR: typeof IRR; + MR1: typeof MR1; + MR2: typeof MR2; + led: typeof led; SDA: typeof SDA; SCL: typeof SCL; show: typeof show; @@ -13032,7 +13210,7 @@ declare module "ESP8266" { * @param {boolean} enable - Enable or disable the debug logging. * @url http://www.espruino.com/Reference#l_ESP8266_logDebug */ - function logDebug(enable: boolean): void; + function logDebug(enable: ShortBoolean): void; /** * Set the debug logging mode. It can be disabled (which frees ~1.2KB of heap), @@ -14590,7 +14768,7 @@ declare module "Storage" { * @param {boolean} [showMessage] - [optional] If true, an overlay message will be displayed on the screen while compaction is happening. Default is false. * @url http://www.espruino.com/Reference#l_Storage_compact */ - function compact(showMessage?: boolean): void; + function compact(showMessage?: ShortBoolean): void; /** * This writes information about all blocks in flash memory to the console - and is @@ -14609,7 +14787,7 @@ declare module "Storage" { * @returns {number} The amount of free bytes * @url http://www.espruino.com/Reference#l_Storage_getFree */ - function getFree(checkInternalFlash: boolean): number; + function getFree(checkInternalFlash: ShortBoolean): number; /** * Returns: @@ -14629,7 +14807,7 @@ declare module "Storage" { * @returns {any} An object containing info about the current Storage system * @url http://www.espruino.com/Reference#l_Storage_getStats */ - function getStats(checkInternalFlash: boolean): any; + function getStats(checkInternalFlash: ShortBoolean): any; /** * Writes a lookup table for files into Bangle.js's storage. This allows any file From 53a4f4b0026ce09e4f9c51af072eae8e94f8630e Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 10 Oct 2024 18:08:36 +0100 Subject: [PATCH 23/26] pace: note todo/feature ideas in README --- apps/pace/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/pace/README.md b/apps/pace/README.md index f6c0ce941..7a1ea7c06 100644 --- a/apps/pace/README.md +++ b/apps/pace/README.md @@ -4,3 +4,8 @@ A running pace app, useful for races. Will also record your splits and display t Drag up/down on the pause menu to scroll through your splits. Press the button to pause/resume - when resumed, pressing the button will pause instantly, regardless of whether the screen is locked. + +# Todo + +- Load splits on app start, button to reset (exs is always reset) +- Show total time on pause screen From 7718374e12a46acd1a46a98b88bff981c1823fbe Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 14 Oct 2024 18:39:48 +0100 Subject: [PATCH 24/26] pace: generate js --- apps/pace/app.js | 69 ++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/apps/pace/app.js b/apps/pace/app.js index 40d215869..d201bbab6 100644 --- a/apps/pace/app.js +++ b/apps/pace/app.js @@ -8,8 +8,8 @@ }, }, }); + var S_1 = require("Storage"); var drawTimeout_1; - var lastUnlazy_1 = 0; var splits_1 = []; var splitOffset_1 = 0, splitOffsetPx_1 = 0; var GPS_TIMEOUT_MS_1 = 30000; @@ -48,11 +48,6 @@ }, { lazy: true }); - var calculatePace_1 = function (time, dist) { - if (dist === 0) - return 0; - return time / dist / 1000 / 60; - }; var draw_1 = function () { if (!exs_1.state.active) { drawSplits_1(); @@ -70,39 +65,47 @@ else { pace = "No GPS"; } - var tm = time_utils_1.decodeTime(exs_1.state.duration); - layout_1["time"].label = tm.d ? time_utils_1.formatDuration(tm) : time_utils_1.formatTime(tm); + layout_1["time"].label = formatDuration_1(exs_1.state.duration); layout_1["pace"].label = pace; layout_1.render(); - if (now - lastUnlazy_1 > 30000) - layout_1.forgetLazyState(), lastUnlazy_1 = now; }; + var pad2_1 = function (n) { return "0".concat(n).substr(-2); }; + var formatDuration_1 = function (ms) { + var tm = time_utils_1.decodeTime(ms); + if (tm.h) + return "".concat(tm.h, ":").concat(pad2_1(tm.m), ":").concat(pad2_1(tm.s)); + return "".concat(pad2_1(tm.m), ":").concat(pad2_1(tm.s)); + }; + var calculatePace_1 = function (split) { return formatDuration_1(split.time / split.dist * 1000); }; var drawSplits_1 = function () { g.clearRect(Bangle.appRect); var barSize = 20; var barSpacing = 10; var w = g.getWidth(); var h = g.getHeight(); - var max = splits_1.reduce(function (a, x) { return Math.max(a, x); }, 0); + var max = splits_1.reduce(function (a, s) { return Math.max(a, s.time); }, 0); g.setFont("6x8", 2).setFontAlign(-1, -1); var i = 0; - var totalTime = 0; for (;; i++) { var split = splits_1[i + splitOffset_1]; if (split == null) break; - totalTime += split; - var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; - if (y > h) + var y_1 = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; + if (y_1 > h) break; - var size = w * split / max; - g.setColor("#00f").fillRect(0, y, size, y + barSize); - var splitPace = calculatePace_1(split, 1); - g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y); + var size = w * split.time / max; + g.setColor("#00f").fillRect(0, y_1, size, y_1 + barSize); + var splitPace = calculatePace_1(split); + drawSplit_1(i, y_1, splitPace); } var pace = exs_1.stats.pacec.getString(); - var splitTime = exs_1.state.duration - totalTime; - g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(pace, " (").concat((splitTime / 1000).toFixed(2), ")"), 0, Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2); + var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; + drawSplit_1(i, y, pace); + }; + var drawSplit_1 = function (i, y, pace) { + g + .setColor("#fff") + .drawString("".concat(i + 1 + splitOffset_1, " ").concat(typeof pace === "number" ? pace.toFixed(2) : pace), 0, y); }; var pauseRun_1 = function () { exs_1.stop(); @@ -122,17 +125,24 @@ else resumeRun_1(); }; + exs_1.start(); exs_1.stats.dist.on("notify", function (dist) { - var prev = splits_1[splits_1.length - 1] || 0; + var prev = { time: 0, dist: 0 }; + for (var _i = 0, splits_2 = splits_1; _i < splits_2.length; _i++) { + var s = splits_2[_i]; + prev.time += s.time; + prev.dist += s.dist; + } var totalDist = dist.getValue(); - var thisSplit = totalDist - prev; - var prevTime = splits_1.reduce(function (a, b) { return a + b; }, 0); - var time = exs_1.state.duration - prevTime; - while (thisSplit > 0) { - splits_1.push(time); - time = 0; + var thisSplit = totalDist - prev.dist; + var thisTime = exs_1.state.duration - prev.time; + while (thisSplit > 1000) { + splits_1.push({ dist: thisSplit, time: thisTime }); + thisTime = 0; thisSplit -= 1000; } + exs_1.state.notify.dist.next -= thisSplit; + S_1.writeJSON("pace.json", { splits: splits_1 }); }); Bangle.on('lock', function (locked) { if (!locked && exs_1.state.active) @@ -155,6 +165,9 @@ } draw_1(); }); + Bangle.on('twist', function () { + Bangle.setBacklight(1); + }); Bangle.loadWidgets(); Bangle.drawWidgets(); g.clearRect(Bangle.appRect); From dc84e2600f40e76955d59c5d27c74d447cb05425 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 16 Oct 2024 21:24:34 +0100 Subject: [PATCH 25/26] pace: make app icon white - compression - transparency - inverted --- apps/pace/app-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pace/app-icon.js b/apps/pace/app-icon.js index b2b8b4c17..9f7887cf4 100644 --- a/apps/pace/app-icon.js +++ b/apps/pace/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwkBiIA/AH8QRYoX/AAcAZqwXuMIQYCiAcOO456OCwwACF5orEDghzOE4oZCLxw+GDAKsXeSIqEGBKKGBwIRFGBCfIC7p5RC7oGHC8RxFC453JAw4Xda5AIEC5IAPO6AXmO5BuHC67OIUA4aERyIXEIxAAJiAuFC5gTEcgpDNBwoWCIxp4CCwp1OCIYAEOiDFNDBwVQAH4AvA=")) +require("heatshrink").decompress(atob("mEwwIjgj//Ap8A8AFPgP4jAdEngFCv///AFC/8f+AFC/EBAodggF4FQgdDF4MeFoIhB8EPgEDApEHwAXDAosDApPwAoIdC//j4AFFGoV/AoJCDDogFIDoUAHYIFEKYxlFAAVAnwEDgF8JgaiBAoX8g6PDUIJGDVoJADg/8f74A==")) From 47a8d264abdd80dc34056110758f195c162f9a47 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 16 Oct 2024 21:26:12 +0100 Subject: [PATCH 26/26] pace: draw split text in theme fg colour --- apps/pace/app.js | 2 +- apps/pace/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pace/app.js b/apps/pace/app.js index d201bbab6..c854df030 100644 --- a/apps/pace/app.js +++ b/apps/pace/app.js @@ -104,7 +104,7 @@ }; var drawSplit_1 = function (i, y, pace) { g - .setColor("#fff") + .setColor(g.theme.fg) .drawString("".concat(i + 1 + splitOffset_1, " ").concat(typeof pace === "number" ? pace.toFixed(2) : pace), 0, y); }; var pauseRun_1 = function () { diff --git a/apps/pace/app.ts b/apps/pace/app.ts index d71442684..cf70fe406 100644 --- a/apps/pace/app.ts +++ b/apps/pace/app.ts @@ -137,7 +137,7 @@ const drawSplits = () => { const drawSplit = (i: number, y: number, pace: number | string) => { g - .setColor("#fff") + .setColor(g.theme.fg) .drawString( `${i + 1 + splitOffset} ${typeof pace === "number" ? pace.toFixed(2) : pace}`, 0,