From 3e6683a0adea391ba781d3154319790892c3d6cd Mon Sep 17 00:00:00 2001 From: smulrine Date: Tue, 11 Feb 2025 21:42:10 +0000 Subject: [PATCH 1/8] Create app.js --- apps/pacer/app.js | 845 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 845 insertions(+) create mode 100644 apps/pacer/app.js diff --git a/apps/pacer/app.js b/apps/pacer/app.js new file mode 100644 index 000000000..1422f7810 --- /dev/null +++ b/apps/pacer/app.js @@ -0,0 +1,845 @@ +Bangle.loadWidgets(); +g.clear(); +Bangle.drawWidgets(); + +var cfg; +try { + cfg = require("Storage").readJSON("pacer.json",true)||{}; +} +catch(err) { + cfg = {}; +} +if (process.env.BOARD == 'BANGLEJS2') + var bangle2 = true; +else + var bangle2 = false; + +var laps = ["Off","0.25","0.5","1","2","5","10"]; +var fg = 1; +var fixed = false; +var started = false; +var startHidden = false; +var recording = false; +var invID = 0; +var intID = 0; +var startID = 0; +var cadID = 0; +var finID = 0; +var lapID = 0; +var steps = 0; +var sats = 0; +var ctr = 0; +var elapsed_ms = 0; +var finish_ms = 0; +var lap_start_ms = 0; +var lap_ms; +var gps = {fix:0,satellites:0}; +var fp; +var start_time; +var current_time; +var paused_time = 0; +var last_time = 0; +var begin_pause; +var next_lap = 0.0; +var skip_ctr = 0; +var skip_max = 0; +var force_write = true; +var show_lap = false; +var lcd_on = true; +var dist = 0.0; +var pdist = 0.0; +var oldDist = 0.0; +var oldLat = -1; +var oldLon = -1; +var oldTime = -1; +var cadence = 0; +var pace = 0; +var ppace = 0; +var R = 6371; +var stepTimes = []; +var dists = []; + +function pace_str(pval) { + var psecs = 295 + 5 * pval; + return ''+Math.floor(psecs/60)+':'+('0'+psecs%60).substr(-2); +} + +function defaults() { + if (typeof(cfg.record) != 'boolean') + cfg.record = true; + if (typeof(cfg.metric) != 'boolean') + cfg.metric = false; + if (typeof(cfg.lap_idx) != 'number') + cfg.lap_idx = 3; + if (typeof(cfg.dark) != 'boolean') + cfg.dark = true; + if (typeof(cfg.eco) != 'boolean') + cfg.eco = false; + if (typeof(cfg.storage) != 'boolean') + cfg.storage = false; + if (typeof(cfg.show_steps) != 'boolean') + cfg.show_steps = false; + if (typeof(cfg.pacer) != 'number') + cfg.pacer = 0; + fg = cfg.dark?1:0; +} + +function genFilename() { + var today=new Date(); + return ('.pacer'+today.getFullYear()+('0'+(today.getMonth()+1)).substr(-2)+('0'+today.getDate()).substr(-2)+('0'+today.getHours()).substr(-2)+('0'+today.getMinutes()).substr(-2)+('0'+today.getSeconds()).substr(-2)+'.csv'); +} + +function doCadence() { + if (steps > 0) + clearInterval(cadID); + cadID = setTimeout(function() { + cadence = 0; + }, 2000); + if (recording) { + steps++; + stepTimes.push(Date.now()); + stepTimes = stepTimes.slice(-20); + const elapsed = stepTimes[stepTimes.length - 1] - stepTimes[0]; + cadence = elapsed ? Math.round(60000 * (stepTimes.length - 1) / elapsed) : 0; + } else + stepTimes = []; +} + +function doPace(thistime,thisdist) { + dists.push([thistime,thisdist]); + dists = dists.slice(-30); + const thiselapsed = dists[dists.length - 1][0] - dists[0][0]; + const thisdistance = dists[dists.length - 1][1] - dists[0][1]; + pace = thisdistance ? ((thiselapsed) / thisdistance) / 1000 : 0; +} + +function countStep() { + if (recording) + steps++; +} + +function calcCrow(lat1, lon1, lat2, lon2) +{ + var dLat = toRad(lat2-lat1); + var dLon = toRad(lon2-lon1); + var lat1r = toRad(lat1); + var lat2r = toRad(lat2); + + var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1r) * Math.cos(lat2r); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + var d = R * c; + if (isNaN(d)) + return 0; + else + return d; +} + +function toRad(Value) +{ + return Value * Math.PI / 180; +} + +function saveGPS(fix) { + var newLat, newLon, newTime, newDist; + + try { + newTime = fix.time.getTime(); + } + catch(err) { + newTime = NaN; + } + newLat = fix.lat; + newLon = fix.lon; + gps = fix; + if (!cfg.storage) { + if (gps.satellites >= 8) + skip_max = 0; + else if (gps.satellites < 4) + skip_max = 5; + else + skip_max = 8 - gps.satellites; + } + if (recording && cfg.pacer > 0 && skip_ctr >= (cfg.storage ? 9 : skip_max)) + pdist = (elapsed_ms / 1000) / ppace; + if (isNaN(newLat) || isNaN(newLon) || isNaN(newTime)) { + skip_ctr = 0; + skip_max = 0; + force_write = true; + } else { + if (oldLat != -1 && recording) { + skip_ctr++; + if (skip_ctr > (cfg.storage ? 9 : skip_max)) { + skip_ctr = 0; + oldDist = dist; + newDist = calcCrow(oldLat, oldLon, newLat, newLon); + dist += newDist; + doPace(newTime,dist); + oldLat = newLat; + oldLon = newLon; + oldTime = newTime; + } + } else { + oldLat = newLat; + oldLon = newLon; + oldTime = newTime; + } + if (recording && cfg.record && (force_write || skip_ctr == 0)) { + fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n"); + last_time = gps.time; + if (force_write) { + skip_ctr = 0; + force_write = false; + } + } + } +} + +function drawInvert() { + // not applicable to bangle2 + g.drawImage(atob("DQ0BD4HjHwT4L8D+B/A/gfwL4J8EeMD4AA=="),225,26); +} + +function drawSatIcon() { + if (bangle2) + g.drawImage(atob("CQkBIDo7i+Pj6O4uAgA="),3,53); + else + g.drawImage(atob("DAwBEAOAcQ84T8D4HwPyHPCOAcAI"),4,66); +} + +function drawStepsIcon() { + if (bangle2) + g.drawimage(atob("CQkBBhudzudzgBzsMAA="),3,139); + else + g.drawImage(atob("DAwBAMAcMeOeeeeeeAecAcOYOAGA"),4,197); +} + +function drawCadenceIcon() { + if (bangle2) + g.drawImage(atob("CQkBCB4SEBgMBCQ8CAA="),3,139); + else + g.drawImage(atob("DAwBBAAwP4YxxDwDwDwjjGH8DAAg"),4,197); +} + +function hideStart() { + g.clearRect(bangle2?162:226,bangle2?81:113,bangle2?174:238,bangle2?93:125); +} + +function drawStart() { + hideStart(); + g.fillPoly([bangle2?162:226,bangle2?81:113,bangle2?162:226,bangle2?93:125,bangle2?174:238,bangle2?87:119,bangle2?162:226,bangle2?81:113]); +} + +function drawPause() { + hideStart(); + g.fillRect(bangle2?165:227,bangle2?82:113,bangle2?167:230,bangle2?92:125); + g.fillRect(bangle2?171:234,bangle2?82:113,bangle2?173:237,bangle2?92:125); +} + +function drawStop() { + // not applicable to bangle2 + g.fillRect(226,202,237,213); +} + +function drawExit() { + if (bangle2) + g.drawImage(atob("CQkBwfHdx8HB8d3HwQg="),165,82); + else + g.drawImage(atob("DAwBwD4HcOOcH4DwDwH4OccO4HwD"),226,202); +} + +function setColours() { + g.setBgColor(1-fg,1-fg,1-fg); + g.setColor(fg,fg,fg); +} + +function setScreenMode() { + g.reset(); + setColours(); + g.clearRect(0,24,bangle2?175:239,bangle2?151:215); +} + +function doLayout() { + setColours(); + if (!bangle2) + drawInvert(); + drawSatIcon(); + drawDist(); + drawTime(); + if (cfg.pacer == 0) + drawPace(); + else { + drawPacer(); + drawSmallPace(); + } + g.setFont("6x8",bangle2?2:3); + if (cfg.show_steps) { + drawStepsIcon(); + g.drawString(steps.toString(),bangle2?15:20,bangle2?134:190,true); + } else { + drawCadenceIcon(); + g.drawString(cadence.toString()+" ",bangle2?15:20,bangle2?134:190,true); + } + drawStart(); + if (!bangle2) + drawStop(); +} + +function drawDist() { + g.setFont("6x8",bangle2?4:5); // 3:5? + var dStr = dist.toString(); + if (dStr.indexOf('.') == -1) + dStr += '.0'; + g.drawString(((' '+(dStr.split('.'))[0])).substr(-2),bangle2?33:53,bangle2?26:35,true); + g.fillRect(bangle2?80:112,bangle2?51:66,bangle2?82:115,bangle2?53:69); + g.drawString(((dStr.split('.'))[1]+'0').substr(0,2),bangle2?86:120,bangle2?26:35,true); + g.setFont("6x8",2); + g.drawString(cfg.metric?"K":"M",bangle2?134:180,bangle2?40:56,true); +} + +function drawPacer() { + g.setFont("6x8",bangle2?3:5); + var pstr=(pdist>dist?'-':'+')+(Math.floor(Math.abs(dist-pdist)))%100; + g.drawString(pstr,bangle2?(49-(pstr.length>2?18:0)):(53-(pstr.length>2?30:0)),bangle2?107:145,true); + g.fillRect(bangle2?84:112,bangle2?126:176,bangle2?85:115,bangle2?127:179); + g.drawString(('0'+Math.floor(Math.abs(((dist-pdist)*100)%100))).substr(-2),bangle2?89:120,bangle2?107:145,true); + g.setFont("6x8",bangle2?1:2); + g.drawString(cfg.metric?"K":"M",bangle2?125:180,bangle2?121:166,true); +} + +function drawPace() { + g.setFont("6x8",bangle2?3:5); + if (pace > 0 && pace < 6000) + g.drawString((' '+Math.floor(pace/60)).substr(-2),bangle2?49:53,bangle2?107:145,true); + else + g.drawString("--",bangle2?49:53,bangle2?107:145,true); + g.fillRect(bangle2?84:112,bangle2?117:160,bangle2?85:115,bangle2?118:163); + g.fillRect(bangle2?84:112,bangle2?120:166,bangle2?85:115,bangle2?121:169); + if (pace > 0 && pace < 6000) + g.drawString(('0'+Math.floor(pace%60)).substr(-2),bangle2?89:120,bangle2?107:145,true); + else + g.drawString("--",bangle2?89:120,bangle2?107:145,true); + g.setFont("6x8",bangle2?1:2); + g.drawString(cfg.metric?"/K":"/M",bangle2?124:178,bangle2?121:166,true); +} + +function drawSmallPace() { + g.setFont("6x8",bangle2?2:3); + if (pace > 0 && pace < 6000) + g.drawString((' '+Math.floor(pace/60)).substr(-2),bangle2?113:136,bangle2?134:190,true); + else + g.drawString("--",bangle2?113:136,bangle2?134:190,true); + if (bangle2) { + g.setPixel(136,140); + g.setPixel(136,142); + } else { + g.fillRect(172,199,173,200); + g.fillRect(172,203,173,204); + } + if (pace > 0 && pace < 6000) + g.drawString(('0'+Math.floor(pace%60)).substr(-2),bangle2?138:176,bangle2?134:190,true); + else + g.drawString("--",bangle2?138:176,bangle2?134:190,true); + g.setFont("6x8",1); + g.drawString(cfg.metric?"/K":"/M",bangle2?164:212,bangle2?141:204,true); +} + +function drawTime() { + var seconds; + var minutes; + var hours; + + setColours(); + g.setFont("6x8",bangle2?5:7); + seconds = parseInt(elapsed_ms/1000) % 60; + minutes = parseInt(elapsed_ms/60000) % 60; + hours = parseInt(elapsed_ms/3600000) % 10; + g.drawString(hours.toString(),bangle2?6:5,bangle2?63:82,true); + g.fillRect(bangle2?34:44,bangle2?79:103,bangle2?36:48,bangle2?81:107); + g.fillRect(bangle2?34:44,bangle2?84:112,bangle2?36:48,bangle2?86:116); + g.drawString(('0'+minutes).substr(-2),bangle2?40:53,bangle2?63:82,true); + g.fillRect(bangle2?98:134,bangle2?79:103,bangle2?100:138,bangle2?81:107); + g.fillRect(bangle2?98:134,bangle2?84:112,bangle2?100:138,bangle2?86:116); + g.drawString(('0'+seconds).substr(-2),bangle2?104:143,bangle2?63:82,true); +} + +function drawGPSBox() { + g.drawRect(2,26,bangle2?12:17,bangle2?51:63); +} + +function drawGPS() { + g.clearRect(3,27,bangle2?11:16,bangle2?50:62); + if (gps.satellites > 0) { + if (!gps.fix) + g.setColor("#FF0000"); + else if (gps.satellites < 4) + g.setColor("#FF5500"); + else if (gps.satellites < 6) + g.setColor("#FF8800"); + else if (gps.satellites < 8) + g.setColor("#FFCC00"); + else + g.setColor("#00FF00"); + g.fillRect(3,bangle2?50:62,bangle2?11:16,(bangle2?50:62)-(gps.satellites>12?12:gps.satellites)*(bangle2?2:3)+1); + g.setColor(fg,fg,fg); + } +} + +function hideLap() { + show_lap = false; + g.reset(); + setColours(); + g.clearRect(bangle2?6:5,bangle2?63:82,bangle2?162:224,bangle2?129:182); + if (recording) { + current_time = Date.now(); + elapsed_ms = current_time - (start_time + paused_time); + } else + elapsed_ms = begin_pause - (start_time + paused_time); + drawTime(); + if (cfg.pacer == 0) + drawPace(); + else { + drawPacer(); + drawSmallPace(); + } +} + +function showLap() { + g.clearRect(bangle2?6:5,bangle2?63:82,bangle2?162:224,bangle2?129:182); + g.drawRect(bangle2?21:28,bangle2?68:90,bangle2?147:201,bangle2?124:174); + g.drawRect(bangle2?23:30,bangle2?70:92,bangle2?145:199,bangle2?122:172); + g.setFont("6x8",bangle2?1:2); + g.drawString("Last lap",bangle2?61:68,bangle2?77:102,true); + g.setFont("6x8",bangle2?3:5); + if (lap_ms < 600000) { + g.drawString((''+Math.floor(lap_ms/60000)),bangle2?57:69,bangle2?89:122,true); + g.fillRect(bangle2?74:98,bangle2?99:137,bangle2?75:101,bangle2?100:140); + g.fillRect(bangle2?74:98,bangle2?102:143,bangle2?75:101,bangle2?103:146); + g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?78:106,bangle2?89:122,true); + } else if (lap_ms < 3600000) { + g.drawString((''+Math.floor(lap_ms/60000)),bangle2?48:54,bangle2?89:122,true); + g.fillRect(bangle2?83:113,bangle2?99:137,bangle2?84:116,bangle2?100:140); + g.fillRect(bangle2?83:113,bangle2?102:143,bangle2?84:116,bangle2?103:146); + g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?87:121,bangle2?89:122,true); + } else { + g.drawString((''+Math.floor(lap_ms/3600000)).substr(-1),bangle2?37:35,bangle2?89:122,true); + g.fillRect(bangle2?54:64,bangle2?99:137,bangle2?55:67,bangle2?100:140); + g.fillRect(bangle2?54:64,bangle2?102:143,bangle2?55:67,bangle2?103:146); + g.drawString(('0'+Math.floor((lap_ms%3600000)/60000)).substr(-2),bangle2?58:72,bangle2?89:122,true); + g.fillRect(bangle2?93:131,bangle2?99:137,bangle2?94:134,bangle2?100:140); + g.fillRect(bangle2?93:131,bangle2?102:143,bangle2?94:134,bangle2?103:146); + g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?97:139,bangle2?89:122,true); + } + Bangle.setLCDPower(true); +} + +function mainLoop() { + g.reset(); + setColours(); + current_time = Date.now(); + if (started) { + elapsed_ms = current_time - (start_time + paused_time); + if (oldDist != dist) { + drawDist(); + if (cfg.lap_idx > 0 && dist >= next_lap ) { + show_lap = true; + next_lap += parseFloat(laps[cfg.lap_idx]); + lap_ms = elapsed_ms - lap_start_ms; + lap_start_ms = elapsed_ms; + Bangle.buzz(); + lapID = setTimeout(hideLap,5000); + showLap(); + } + } + } else + elapsed_ms = 0; + drawSats = false; + if (recording) { + if (!show_lap) + drawTime(); + g.setFont("6x8",bangle2?2:3); + if (cfg.show_steps) + g.drawString(steps.toString(),bangle2?15:20,bangle2?134:190,true); + else + g.drawString(cadence.toString()+" ",bangle2?15:20,bangle2?134:190,true); + } /* else + g.setFont("6x8",3); */ + if (!show_lap) + if (cfg.pacer == 0) + drawPace(); + else { + drawPacer(); + drawSmallPace(); + } + if (gps.fix) { + if (!started && startHidden) { + startHidden = false; + if (!bangle2) + startID = setWatch(start, BTN2); + else + startID = setWatch(start, BTN1, {edge: 'falling'}); + drawStart(); + } + if (!fixed) { + fixed = true; + drawSats = true; + } + } else { + if (!started && !startHidden) { + startHidden = true; + clearWatch(startID); + hideStart(); + } + if (fixed) { + fixed = false; + drawSats = true; + } + } + if (gps.satellites != sats) { + sats = gps.satellites; + drawSats = true; + } + if (drawSats) + drawGPS(); + if (ctr++%10 == 0) { + g.reset(); + Bangle.drawWidgets(); + } +} + +function restart(e) { + if (bangle2 && (e.time - e.lastTime > 0.5)) { + finish(); + } + g.reset(); + setColours(); + paused_time += (Date.now() - begin_pause); + pace = 0; + drawPause(); + oldDist = dist; + skip_ctr = 0; + force_write = true; + recording = true; + Bangle.buzz(); + if (!bangle2) + setWatch(pause, BTN2); + else + setWatch(pause, BTN1, { edge: 'falling' }); +} + +function pause(e) { + if (bangle2 && (e.time - e.lastTime > 0.5)) { + finish(); + } + g.reset(); + setColours(); + begin_pause = Date.now(); + elapsed_ms = begin_pause - (start_time + paused_time); + finish_ms = elapsed_ms; + drawDist(); + recording = false; + if (!show_lap) + drawTime(); + drawStart(); + oldTime = -1; + if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time)) + fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n"); + Bangle.buzz(); + dists = []; + if (!bangle2) + setWatch(restart, BTN2); + else + setWatch(restart, BTN1, { edge: 'falling' }); +} + +function start() { + g.reset(); + setColours(); + if (cfg.eco){ + Bangle.setLCDPower(true); + Bangle.setLCDTimeout(10); + } + if (cfg.record) + fp = require("Storage").open(genFilename(),"w"); + start_time = Date.now(); + drawPause(); + Bangle.buzz(); + started = true; + recording = true; + if (!bangle2) + setWatch(pause, BTN2); + else + setWatch(pause, BTN1, { edge: 'falling' }); + if (cfg.show_steps) + Bangle.on('step',countStep); + else + Bangle.on('step',doCadence); + clearInterval(intID); + intID = setInterval(mainLoop,200); + if (cfg.lap_idx > 0) + next_lap = parseFloat(laps[cfg.lap_idx]); +} + +function endScreen() { + fg = 1-fg; + setScreenMode(); + if (!bangle2) + drawInvert(); + drawExit(); + g.setFont("6x8",bangle2?1:2); + var dStr = dist.toString(); + if (dStr.indexOf('.') == -1) + dStr += '.00'; + dStr = dStr.slice(0, (dStr.indexOf('.'))+3); + if (bangle2) + g.drawString('Distance: '+dStr+(cfg.metric?'K':'M'),38,43); + else { + //g.drawString('Distance: '+dist.toFixed(2),19,53); + g.drawString('Distance: '+dStr,19,53); + g.setFont("6x8",1); + //g.drawString(cfg.metric?'K':'M',139+12*(dist.toFixed(2).length),60); + g.drawString(cfg.metric?'K':'M',139+12*(dStr.length),60); + g.setFont("6x8",2); + } + g.drawString('Time: '+parseInt(finish_ms/3600000)%10+':'+('0'+parseInt(finish_ms/60000)%60).substr(-2)+':'+('0'+parseInt(finish_ms/1000)%60).substr(-2),bangle2?62:67,bangle2?63:83); + var avgPace = dist?((finish_ms/dist)/1000):0; + var paceStr = 'Avg Pace: '+parseInt(avgPace/60)+':'+('0'+parseInt(avgPace%60)).substr(-2); + if (bangle2) + g.drawString(paceStr+(cfg.metric?'/K':'/M'),38,83); + else { + g.drawString(paceStr,19,113); + g.setFont("6x8",1); + g.drawString(cfg.metric?'/K':'/M',19+12*(paceStr.length),120); + g.setFont("6x8",2); + } + g.drawString("Steps: "+steps,bangle2?56:55,bangle2?103:143); + var avgCadence = steps?(60*steps/(finish_ms/1000)):0; + g.drawString("Cadence: "+parseInt(avgCadence),bangle2?44:31,bangle2?123:173); + g.reset(); + Bangle.drawWidgets(); +} + +function finish() { + if (recording) { + finish_ms = elapsed_ms; + if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time)) + fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n"); + } + recording = false; + Bangle.setGPSPower(0); + Bangle.on('step',function(){}); + Bangle.on('GPS',function(){}); + clearInterval(lapID); + clearInterval(intID); + if (!bangle2) { + clearWatch(finID); + clearWatch(invID); + } + if (!bangle2) { + setWatch(function() {if (lcd_on) endScreen();}, BTN1, {repeat:true}); + setWatch(function() {load();},BTN3); + } else + setWatch(function() {load();},BTN1); + fg = 1-fg; + endScreen(); +} + +function startScreen() { + clearInterval(intID); + if (!bangle2) { + clearWatch(invID); + } + clearWatch(finID); + setScreenMode(); + doLayout(); + drawGPSBox(); + Bangle.buzz(); + if (!bangle2) { + invID = setWatch(invertRunning, BTN1, {repeat:true}); + startID = setWatch(start, BTN2); + finID = setWatch(finish, BTN3); + } else + startID = setWatch(start, BTN1, {edge: 'falling'}); + fixed = false; + intID = setInterval(mainLoop,1000); +} + +function invertRunning() { + // not applicable to bangle2 + if (!lcd_on) + return; + fg = 1-fg; + setScreenMode(); + if (started) + if (recording) { + current_time = Date.now(); + elapsed_ms = current_time - (start_time + paused_time); + } else + elapsed_ms = begin_pause - (start_time + paused_time); + else + elapsed_ms = 0; + drawInvert(); + drawSatIcon(); + drawGPSBox(); + drawGPS(); + drawDist(); + if (show_lap) + showLap(); + else { + drawTime(); + if (cfg.pacer == 0) + drawPace(); + else { + drawPacer(); + drawSmallPace(); + } + } + g.setFont("6x8",3); + if (cfg.show_steps) { + drawStepsIcon(); + g.drawString(steps.toString(),20,190,true); + } else { + drawCadenceIcon(); + g.drawString(cadence.toString()+" ",20,190,true); + } + if (recording) + drawPause(); + else if (started || gps.fix) + drawStart(); + drawStop(); + g.reset(); + Bangle.drawWidgets(); +} + +function drawDots() { + if (ctr % 4 == 0) + g.drawString(" ",bangle2?116:176,bangle2?90:108,true); + else if (ctr % 4 == 1) + g.drawString(".",bangle2?116:176,bangle2?90:108); + else if (ctr % 4 == 2) + g.drawString("..",bangle2?116:176,bangle2?90:108); + else + g.drawString("...",bangle2?116:176,bangle2?90:108); +} + +function invertWaiting() { + /* not applicable to bangle2 */ + fg = 1-fg; + setScreenMode(); + drawInvert(); + drawExit(); + g.setFont("6x8",2); + g.drawString("Locating",68,88); + g.drawString("Satellites",56,108); + ctr--; + drawDots(); + g.reset(); + Bangle.drawWidgets(); + ctr++; +} + +function awaitGPSLoop() { + g.reset(); + setColours(); + g.setFont("6x8",bangle2?1:2); + drawDots(); + if (gps.fix) + startScreen(); + if (ctr % 10 == 0) { + g.reset(); + Bangle.drawWidgets(); + } + ctr++; +} + +function awaitGPS() { + Bangle.setOptions({wakeOnTwist:false}); + Bangle.setGPSPower(1); + Bangle.on('GPS', saveGPS); + Bangle.setLCDPower(true); + Bangle.setLCDTimeout(0); + g.reset(); + setColours(); + if (!bangle2) { + drawInvert(); + invID = setWatch(invertWaiting, BTN1, {repeat:true}); + } + drawExit(); + g.setFont("6x8",bangle2?1:2); + // g.drawString("Locating",bangle2?36:68,bangle2?72:88); + // g.drawString("Satellites",bangle2?24:56,bangle2?92:108); + g.drawString("Locating",bangle2?62:68,bangle2?78:88); + g.drawString("Satellites",56,bangle2?90:108); + intID = setInterval(awaitGPSLoop,1000); + if (bangle2) + finID = setWatch(function(){load();},BTN1); + else + finID = setWatch(function(){load();},BTN3); +} + +function main() { + require("Storage").write("pacer.json", cfg); + E.showMenu(); + if (cfg.eco) + Bangle.on('lcdPower', function(on) {setTimeout(function(){lcd_on = on;}, 500);}); + fg = cfg.dark?1:0; + R = cfg.metric?6371:3959; + ppace = 295 + 5 * cfg.pacer; + setScreenMode(); + awaitGPS(); +} + +defaults(); + +var main_menu = { + "" : { "title" : "Pacer"}, + "Start": function() { main(); }, + "Recording" : { + value : cfg.record, + format : v => v?"On":"Off", + onchange : v => { cfg.record = v; }, + }, + "Units" : { + value : cfg.metric, + format : v => v?"Metric":"Imperial", + onchange : v => { cfg.metric = v; }, + }, + "Lap" : { + value : cfg.lap_idx, + format : v => laps[v], + min : 0, max : 6, + onchange : v => { cfg.lap_idx = v; } + }, + "Dark mode" : { + value : cfg.dark, + format : v => v?"On":"Off", + onchange : v => { cfg.dark = v; }, + }, + "Eco battery" : { + value : cfg.eco, + format : v => v?"On":"Off", + onchange : v => { cfg.eco = v; }, + }, + "Eco storage" : { + value : cfg.storage, + format : v => v?"On":"Off", + onchange : v => { cfg.storage = v; }, + }, + "Steps" : { + value : cfg.show_steps, + format : v => v?"Count":"Cadence", + onchange : v => { cfg.show_steps = v; }, + }, + "Pacer" : { + value : cfg.pacer, + format : v => v==0?"Off":pace_str(v), + min : 0, max : 121, + onchange : v => { cfg.pacer = v; } + }, +}; + +if (!bangle2) { + Bangle.setLCDMode(); +} +Bangle.setLCDBrightness(1); +setScreenMode(); +E.showMenu(main_menu); From 9425a3eef4c7e0f8ca8e618d9db5b2331cb4f3b7 Mon Sep 17 00:00:00 2001 From: smulrine Date: Tue, 11 Feb 2025 21:44:04 +0000 Subject: [PATCH 2/8] Add files via upload --- apps/pacer/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/pacer/app-icon.js diff --git a/apps/pacer/app-icon.js b/apps/pacer/app-icon.js new file mode 100644 index 000000000..33eeed26b --- /dev/null +++ b/apps/pacer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///un/+2Eqee1nV+X26NjtfNGLsf+AQOg+kzAROnskyfACJs5CINACJshCKAjMgfJBoPwgpHLiGSpMk3kHyneO5OECIMl23Aj+AO5QRD7ZSJgPpknZkmNCIJFJhFJk1wgFtCIN4CJFCpMwAgIRCt5HIoVN8B/BCIVmEZFyt6RCCIb7Ih5SCmYRCkjoM7YRB78k6ARO++kzwQKhgRC4GEzARKswRD8mT///Lg8DtmbCILvEEw8DkmzCIUcCIWTN40bkmWUoUCCIWbCJGSrIpC5YoB5x7HCINLRZcAg3bsgwBsARNtgRBlgRLmyNBCIMlwAQJgLkBtuyMwIRHvJUBpP2GoMGCIOwCI1SBQOSr3bA4PJSQKtGSoWSpXYBAMZAwKcGoQRCpoLCZAOSoARFh5HCk4IDtmpnikMAAMOpO8CJ0KpKQKAAlyfoQANqVMCByGBt4ROgWSfhgACjmTvAROimTCB0A8mbYgwAIwmYEZ/07wRPj/wCJ4AI")) From d1b344dbf70ef725ec738a79c055eb09fd65a852 Mon Sep 17 00:00:00 2001 From: smulrine Date: Tue, 11 Feb 2025 21:45:29 +0000 Subject: [PATCH 3/8] Add files via upload --- apps/pacer/README.md | 56 +++++++++++ apps/pacer/interface.html | 198 ++++++++++++++++++++++++++++++++++++++ apps/pacer/metadata.json | 19 ++++ 3 files changed, 273 insertions(+) create mode 100644 apps/pacer/README.md create mode 100644 apps/pacer/interface.html create mode 100644 apps/pacer/metadata.json diff --git a/apps/pacer/README.md b/apps/pacer/README.md new file mode 100644 index 000000000..0824bc7f4 --- /dev/null +++ b/apps/pacer/README.md @@ -0,0 +1,56 @@ +## Pacer + +![icon](app.png) + +Run with a virtual partner at your chosen pace, and export the GPX data +from the Bangle.js App Store. + +## Usage + +Pacer starts up with a menu. + +* **Recording** - whether to record the run +* **Units** - imperial or metric +* **Lap** - the multiple of a mile or kilometer to use for splits +* **Dark mode** - use black or white background +* **Eco battery** - display will turn off after 10 seconds +* **Eco storage** - only record GPS position every 10 seconds +* **Steps** - display step count or cadence +* **Pacer** - pace of virtual partner + +On selecting **Start**, GPS position will be detected. A run cannot be +started without a GPS fix. The watch touchscreen is disabled while the +app is running. + +The app will run on Bangle.js 1 and 2, although use on Bangle.js 2 is not +recommended due to poor GPS accuracy. + +On a Bangle.js 1, the top button reverses the screen colours, the middle +button starts, pauses or resumes a run, and the bottom button ends the run. + +On a Bangle.js 2, a short press of the button starts, pauses or resumes a +run, and a long press (over 0.5 seconds, but under 2!) ends the run. Note +that holding the button for 2 seconds will exit back to the default clock +app. + +## Downloading + +GPX tracks can be downloaded using the +[App Loader](https://banglejs.com/apps/?id=pacer). Connect the +Bangle.js and click on the Pacer app's disk icon to see the tracks +available for downloading. + +## Tips + +For best results, only start a run when the satellite signal strength bar is +green. + +Use the [Assisted GPS Updater](https://banglejs.com/apps/#AGPS) to improve +the time taken to get a GPS fix. + +## Bugs + +The eco settings are unlikely to be useful. + +GPS track smoothing is accomplished simply by reducing the frequency with +which readings are taken, depending on signal strength. diff --git a/apps/pacer/interface.html b/apps/pacer/interface.html new file mode 100644 index 000000000..71e8842d4 --- /dev/null +++ b/apps/pacer/interface.html @@ -0,0 +1,198 @@ + + + + + +
+
+ + + + + + + diff --git a/apps/pacer/metadata.json b/apps/pacer/metadata.json new file mode 100644 index 000000000..1fb5fcdcd --- /dev/null +++ b/apps/pacer/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "pacer", + "name": "Pacer", + "version": "0.01", + "description": "Run with a virtual partner", + "icon": "app.png", + "tags": "run,running,fitness,outdoors,gps", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"pacer.app.js","url":"app.js"}, + {"name":"pacer.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"pacer.json"}, + {"wildcard":".pacer*.csv","storageFile":true} + ] +} From 0f766ab1b6706685418f1fe242ea6785bbfd0035 Mon Sep 17 00:00:00 2001 From: smulrine Date: Tue, 11 Feb 2025 22:22:09 +0000 Subject: [PATCH 4/8] Add files via upload --- apps/pacer/app.png | Bin 0 -> 2263 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/pacer/app.png diff --git a/apps/pacer/app.png b/apps/pacer/app.png new file mode 100644 index 0000000000000000000000000000000000000000..92148056878e20aa72bc542fb374db0203226a15 GIT binary patch literal 2263 zcmV;|2q^c7P)+aIooC0iiNvOPJ!q`T*Q=ljmRXYbt$@Etd7Fq2nSmQebjP)3rX1sG0Xp&)N& zJg%~*R-H1(i!o>dPVAW{RV(Q;Lr0Gvt<`5q0007hAKdu&g(kPh_1w&5pPr5f2k&2+ zu}!TM?-)7$$#G%w003fI3ge1qapTRId@#IhVHHr{mwUvwBOwZ2wW&RVRbdQ5NS>Fy{GdM_T30PX1f@%Kt z4I6&3YiISod6Q*Q*($(LN-z|xX4-uF=DzfKAYL8`3HWU9PZb(DeQfBc@kN@9OzGqN zU49 zhoxh)K%r7ks`_Y-w%-J#L3=}+)zTK^c>n-tZ8CeVT|8ffETJchSAW?Q?Z55g)f-5q zHd88&x#0ri^T8Mm4Jt7S#$h82Dq6m!w*q?A1E>EqH&?1s?#6La&2jAEX^W4&+mqj( zJteBUiUM2SkODD=#YlkwtgS8Z%2XqeM5oVx&Yix6J-9wyqtQqklnTn=>hgxv;tIg)OSQ44lAYFCBk?1I+7a+PHl0rvL!8 ze_3>u^ZShyO#=XcA%B>rwXxyoh04WuqGJ(4Z7k>AK5O}jD@hmd){YIYH@9V{id+Qn zIYH1CkkC^LSX(VYCEHOP{_pj9UTbQ&Ra7uyj1~YuL{U&Q`)5V~Fhh$1ilfmtp00g{3grA#XK+biX+x)`bht(}!S#{lR zwcJI5&O8>N zZTACwgZt^|c+SsNv^JaLIt#I0MgRdlytU}_5_;#&-%h{n)Xm25PLknegX1`)YBIpj z_@M5r0~AA8qzSpUmYMeZqcNs0J9f0Tk|_1KoKvK7r7HS)*tme7d2qk^j>FAvO^N`M z$%MBBiQ_>5$1oHGmhpm2sfh5QrBV>n5^$NVn2Jz#BM$)3`SDV@uzlsh?P>`N z*tzwk@%7%^OI#?DEhQF%CPNPd*?lVr1w#;Y)VoQ}mu51Vf{EttY6&;7J*26QCJz9> z1p@HEVgW9|c8eiM0tlr+NlD>CqItVvCK6O$UUsO-t(n-$hz(nM^n#CF*!dUV%uWj1Ykk0Bbk%TI}}Nb(`M5nG^vvpDcWF*ywTF zbeRS^p1?z=7^u1S?Mp=};)4d2b>;f?6$iWeZ7qNO5+ll)K+97jLhy7u2LLFYB?E)5bD~tV-=9OZ7DbZ>0N7mi+C&9!KJ9baBJO!`k_23p ztE6ODpZejUfKY%x(tM}R8E>G7z+mANE|96{Xd9}s8rQx0_N7?9o63G)oQlkcSy!uo z5C9So@T>>OkOvepYQz^G&7YL0fQV_WIa0DlOi{}TLKOQj|9?Y*z;T@atOY|`N=uK1 z;z?Ff@yd2vApPYmt(cfLhJw*~B=7-0TseQ{@RUVImLwt|qFJZ^{OE1~*qsPaJmz*& zzneee((K7IXO9RUIRXI|2`JQ(DCU6PPR}T~zC5pRxUuUo+gx|E(d+Eko+##k2|x%i zT0(C~HyCB%F`vf`UbpM)Q)^B=NECO#1k@aQc12G9@Vu_oufKWi9^-Uv>I?3G2 Date: Wed, 12 Feb 2025 00:11:22 +0000 Subject: [PATCH 5/8] minor code fixes --- apps/pacer/app.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/pacer/app.js b/apps/pacer/app.js index 1422f7810..d12a13b01 100644 --- a/apps/pacer/app.js +++ b/apps/pacer/app.js @@ -46,12 +46,12 @@ var skip_max = 0; var force_write = true; var show_lap = false; var lcd_on = true; +var drawSats = false; var dist = 0.0; var pdist = 0.0; var oldDist = 0.0; var oldLat = -1; var oldLon = -1; -var oldTime = -1; var cadence = 0; var pace = 0; var ppace = 0; @@ -176,12 +176,10 @@ function saveGPS(fix) { doPace(newTime,dist); oldLat = newLat; oldLon = newLon; - oldTime = newTime; } } else { oldLat = newLat; oldLon = newLon; - oldTime = newTime; } if (recording && cfg.record && (force_write || skip_ctr == 0)) { fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n"); @@ -540,7 +538,6 @@ function pause(e) { if (!show_lap) drawTime(); drawStart(); - oldTime = -1; if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time)) fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n"); Bangle.buzz(); From 15688b545b60fe7d8b2beb074352cea184889da9 Mon Sep 17 00:00:00 2001 From: smulrine Date: Wed, 12 Feb 2025 23:18:36 +0000 Subject: [PATCH 6/8] Add files via upload --- apps/pacer/screenshot.png | Bin 0 -> 2592 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/pacer/screenshot.png diff --git a/apps/pacer/screenshot.png b/apps/pacer/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d97f04d7d9c7619d8b802b15905a92869e412f23 GIT binary patch literal 2592 zcmb7``8(8o7so%}8Dq>COK6yssfc7ZNLjN?m@;T`yRB0~Su(QUwjm`?gu+imA39r`$@M}S6AXsz)^|B_alH1y=VR<8_*VMs_D$EpNkg|2> zE6!UqH#M)DX2iN92jJqb1ICn9H9xhDmYxm3(O~nnRnKGLeQBX7XO%b`$WfA<9sB?lBDt<15d)@H^<_AF;DFkPylIqCz5g&11p)9XX`95k6 zuHe(1x2KU5yx9(Wt!hK6ZIdB13ch*_uY|hmSM$EI^~gg;Dg+b$ZUU;P-L&Ofrb*M@ zpHF^y3n&C_=i18HauVaKY;zE?ULwnG&uZDIGRhqT!Qb9pm*Zm!^69Ud(NsSbPknMh zm%9km;wBm_#8IS&qINOet|!OH58h4-vKLw5R;0W^*9KM0V6aL2`_d|`;cmsazG85M zFL#hD+$eL!qKm?`8a~Fo2HL{4gTbJI|7*l)|7oOZ){4{R@`or07#(R#;s++WqEe$=gGy#iY z2NeMv+z>Dop=F8wrz#8Nxe9sxM!rHDHMvSxG&IKQ0?(afYnaGvNgJ+^L%>74CX+!i| z9Nwq%Nhcn(Zxrzr)2LRP#1~7GUjAHbeSD}OjOhe*S+cd#K#J*xz54LP_4KJA91L(U zsrOwq)vkHu4+HI8``oF`KAHDyx9^%Mvyad%Zhb8SRDUTjl5$%Ko}(UXBi@OOGJ%-d z;&q$F3x3lgoi87D!v9iH!{9X;grLldP8j4lw^>e49&xFS|K5uyNHK~0>e?KSv<7!f znsi_?-k@58kcErPjh!C97^6pBM>d_|zekFzDYp@?`SAJblRn!YH+2GfC_)qK^JAPox+0aIC71Ocw2v1@-TicM|$qY2_ zbgv*2lFBFNCMY^&y}O8~H*sAFzyE!U$a*z8{d~ieAmLyKUF3JYaYeWJ7SCwRvkLSq zK=gIrS}6kX-#0raEXFL59#Z@ho}{4t zkJc)$SGC-&?y8^Df8^krB0wn72<**=2z9`LUuoDiErRac)-!nHgk?xYtZCk>h@LYF z;JSWC)kAtCa)iE9&v{d2fb#FGB1cxw5^`NIp*&KjB4UEqbFB8QsvDmrhv1JNm7<>qct7F zcK#F4f&DOSpuiBOFJRCx&_4!?)m=mr47})_N3X{d2V~EWP4%ZEat??8N29@Q{Xw0C z+}N2{p!AgB<}Y!3uKY7!dP=IUn$*43XSnu8eyl?>(&9cQ@}lv2jiSJtMnxd`Sck&D z#w(s~6ou45`F=ouMcWi=j{m;MI-uKUYRj+lNrZHqUJ5|2W#SPL6c{lL-qyMRm_5a9 zsR;^pl&`F9KGerPIJhm?I#Dc2qbg4BPz%-zhQAxZO z=d5ty#{o&}%;Rfr>)-s9 z8tp@rp|#g2xJ=+XqOGek#?&%7p4< zU4NcOs)Nn4XYL{?z@hp{!F%;hy#>czqm}YdOB?SNS!(NfBxiw@m>y>=q@OoTN#Kod zS3)O@BfxYLmhaunl(8GU!VZk74^`Bn#@XuyB+}Mh&mu!TYY`03eZzah&FP|^fsYH0_$BA872-t~ z(wu#MbIQERV|=AOKidNs%2W*#HLD1hrA%UVP%J6I@mYC0iId&Db-Zho9a}<3MM^Lg^?}sNvaF&=y06d5L}v1;Q3MAN4^iJiuFi10P$%!R^?R{eWH4ny?OVNQx4G wDG4HTkQ9HWJz%ngiM>d5-2YVe|7pw-nGNqLH6zQY@J>EpX>M&+X+n?tFVEPW{r~^~ literal 0 HcmV?d00001 From faf65dd93af47662188cb4f7fe670e71e7dbdc73 Mon Sep 17 00:00:00 2001 From: smulrine Date: Wed, 12 Feb 2025 23:20:06 +0000 Subject: [PATCH 7/8] added screenshot --- apps/pacer/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/pacer/metadata.json b/apps/pacer/metadata.json index 1fb5fcdcd..87d5fb1ce 100644 --- a/apps/pacer/metadata.json +++ b/apps/pacer/metadata.json @@ -8,6 +8,7 @@ "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "interface": "interface.html", + "screenshots": [{"url":"screenshot.png"}], "storage": [ {"name":"pacer.app.js","url":"app.js"}, {"name":"pacer.img","url":"app-icon.js","evaluate":true} From 2aadfe9bc862bccac3feaea6299e4d2cbaaecec6 Mon Sep 17 00:00:00 2001 From: smulrine Date: Wed, 12 Feb 2025 23:21:51 +0000 Subject: [PATCH 8/8] fixed Bangle.js2 exit icon --- apps/pacer/app.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/pacer/app.js b/apps/pacer/app.js index d12a13b01..6b2b9efce 100644 --- a/apps/pacer/app.js +++ b/apps/pacer/app.js @@ -46,7 +46,7 @@ var skip_max = 0; var force_write = true; var show_lap = false; var lcd_on = true; -var drawSats = false; +var drawSats = true; var dist = 0.0; var pdist = 0.0; var oldDist = 0.0; @@ -507,21 +507,22 @@ function mainLoop() { function restart(e) { if (bangle2 && (e.time - e.lastTime > 0.5)) { finish(); + } else { + g.reset(); + setColours(); + paused_time += (Date.now() - begin_pause); + pace = 0; + drawPause(); + oldDist = dist; + skip_ctr = 0; + force_write = true; + recording = true; + Bangle.buzz(); + if (!bangle2) + setWatch(pause, BTN2); + else + setWatch(pause, BTN1, { edge: 'falling' }); } - g.reset(); - setColours(); - paused_time += (Date.now() - begin_pause); - pace = 0; - drawPause(); - oldDist = dist; - skip_ctr = 0; - force_write = true; - recording = true; - Bangle.buzz(); - if (!bangle2) - setWatch(pause, BTN2); - else - setWatch(pause, BTN1, { edge: 'falling' }); } function pause(e) {