diff --git a/apps.json b/apps.json index bb527533c..07aa644f6 100644 --- a/apps.json +++ b/apps.json @@ -2,7 +2,7 @@ { "id": "boot", "name": "Bootloader", "icon": "bootloader.png", - "version":"0.20", + "version":"0.21", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "tags": "tool,system", "type":"bootloader", @@ -1243,7 +1243,7 @@ { "id": "minionclk", "name": "Minion clock", "icon": "minionclk.png", - "version": "0.03", + "version": "0.04", "description": "Minion themed clock.", "tags": "clock,minion", "type": "clock", @@ -2170,8 +2170,22 @@ {"name":"tetra.stl","url":"tetra.stl"}, {"name":"cube.stl","url":"cube.stl"}, {"name":"icosa.stl","url":"icosa.stl"} - ] - }, + ] + }, + { "id": "cscsensor", + "name": "Cycling speed sensor", + "shortName":"CSCSensor", + "icon": "icons8-cycling-48.png", + "version":"0.04", + "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", + "tags": "outdoors,exercise,ble,bluetooth", + "readme": "README.md", + "storage": [ + {"name":"cscsensor.app.js","url":"cscsensor.app.js"}, + {"name":"cscsensor.settings.js","url":"settings.js"}, + {"name":"cscsensor.img","url":"cscsensor-icon.js","evaluate":true} + ] + }, { "id": "worldclock", "name": "World Clock - 4 time zones", "shortName":"World Clock", @@ -2189,4 +2203,3 @@ ] } ] - diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 5dde04f9c..d2f68fd0e 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -19,3 +19,4 @@ 0.18: Fix 'GPS time' checks for western hemisphere 0.19: Tweaks to simplify code and lower memory usage 0.20: Allow Gadgetbridge to work even with programmable:off +0.21: Handle echo off char from Gadgetbridge app when programmable:off (fix #558) diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js index b674d601f..630252dea 100644 --- a/apps/boot/boot0.js +++ b/apps/boot/boot0.js @@ -22,6 +22,7 @@ if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth l.forEach(n=>Bluetooth.emit("line",n)); }); Bluetooth.on('line',function(l) { + if (l.startsWith('\x10')) l=l.slice(1); if (l.startsWith('GB({') && l.endsWith('})') && global.GB) try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} }); diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog new file mode 100644 index 000000000..7be2ed3e2 --- /dev/null +++ b/apps/cscsensor/ChangeLog @@ -0,0 +1,5 @@ +0.01: New app! +0.02: Add wheel circumference settings dialog +0.03: Save total distance traveled +0.04: Add sensor battery level indicator + diff --git a/apps/cscsensor/README.md b/apps/cscsensor/README.md new file mode 100644 index 000000000..8ba862241 --- /dev/null +++ b/apps/cscsensor/README.md @@ -0,0 +1,16 @@ +# CSCSensor + +Simple app that can read a cycling speed and cadence (CSC) sensor and display the information on the watch. +Currently the app displays the following data: + +- moving time +- current speed +- average speed +- maximum speed +- trip distance traveled +- total distance traveled + +Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. + +I do not have access to a cadence sensor at the moment, so only the speed part is currently implemented. Values displayed are imperial or metric (depending on locale), +the wheel circumference can be adjusted in the global settings app. diff --git a/apps/cscsensor/cscsensor-icon.js b/apps/cscsensor/cscsensor-icon.js new file mode 100644 index 000000000..12c597956 --- /dev/null +++ b/apps/cscsensor/cscsensor-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA")) diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js new file mode 100644 index 000000000..65b50dfe7 --- /dev/null +++ b/apps/cscsensor/cscsensor.app.js @@ -0,0 +1,177 @@ +var device; +var gatt; +var service; +var characteristic; + +const SETTINGS_FILE = 'cscsensor.json'; +const storage = require('Storage'); + +class CSCSensor { + constructor() { + this.movingTime = 0; + this.lastTime = 0; + this.lastBangleTime = Date.now(); + this.lastRevs = -1; + this.settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + this.settings.totaldist = this.settings.totaldist || 0; + this.totaldist = this.settings.totaldist; + this.wheelCirc = (this.settings.wheelcirc || 2230)/25.4; + this.speedFailed = 0; + this.speed = 0; + this.maxSpeed = 0; + this.lastSpeed = 0; + this.qUpdateScreen = true; + this.lastRevsStart = -1; + this.qMetric = !require("locale").speed(1).toString().endsWith("mph"); + this.speedUnit = this.qMetric ? "km/h" : "mph"; + this.distUnit = this.qMetric ? "km" : "mi"; + this.distFactor = this.qMetric ? 1.609344 : 1; + this.batteryLevel = -1; + } + reset() { + this.settings.totaldist = this.totaldist; + storage.writeJSON(SETTINGS_FILE, this.settings); + this.maxSpeed = 0; + this.movingTime = 0; + this.lastRevsStart = this.lastRevs; + this.maxSpeed = 0; + } + setBatteryLevel(level) { + this.batteryLevel = level; + } + updateScreen() { + var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0; + var ddist = Math.round(100*dist)/100; + var tdist = Math.round(this.distFactor*this.totaldist*10)/10; + var dspeed = Math.round(10*this.distFactor*this.speed)/10; + var dmins = Math.floor(this.movingTime/60).toString(); + if (dmins.length<2) dmins = "0"+dmins; + var dsecs = (Math.floor(this.movingTime) % 60).toString(); + if (dsecs.length<2) dsecs = "0"+dsecs; + var avespeed = (this.movingTime>2 ? Math.round(10*dist/(this.movingTime/3600))/10 : 0); + var maxspeed = Math.round(10*this.distFactor*this.maxSpeed)/10; + for (var i=0; i<6; ++i) { + if ((i&1)==0) g.setColor(0, 0, 0); + else g.setColor(0.2, 0.1, 0.4); + g.fillRect(0, 48+i*32, 86, 48+(i+1)*32); + if ((i&1)==1) g.setColor(0, 0, 0); + else g.setColor(0.2, 0.1, 0.4); + g.fillRect(87, 48+i*32, 239, 48+(i+1)*32); + g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239).drawRect(0, 48, 87, 239); + } + g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0); + g.drawString("Time:", 87, 66); + g.drawString("Speed:", 87, 98); + g.drawString("Ave spd:", 87, 130); + g.drawString("Max spd:", 87, 162); + g.drawString("Trip:", 87, 194); + g.drawString("Total:", 87, 226); + g.setFontAlign(-1, 0, 0).setFontVector(26).setColor(1, 1, 1);//.clearRect(92, 60, 239, 239); + g.drawString(dmins+":"+dsecs, 92, 66); + g.drawString(dspeed+" "+this.speedUnit, 92, 98); + g.drawString(avespeed + " " + this.speedUnit, 92, 130); + g.drawString(maxspeed + " " + this.speedUnit, 92, 162); + g.drawString(ddist + " " + this.distUnit, 92, 194); + g.drawString(tdist + " " + this.distUnit, 92, 226); + if (this.batteryLevel!=-1) { + g.setColor(1, 1, 1).drawRect(10, 55, 20, 75).fillRect(14, 53, 16, 55); + if (this.batteryLevel<25) g.setColor(1, 0, 0); + else if (this.batteryLevel<50) g.setColor(1, 0.5, 0); + else g.setColor(0, 1, 0); + g.fillRect(11, 74-18*this.batteryLevel/100, 19, 74); + console.log(this.batteryLevel); + this.batteryLevel = -1; + } + } + updateSensor(event) { + var qChanged = false; + if (event.target.uuid == "0x2a5b") { + var wheelRevs = event.target.value.getUint32(1, true); + var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); + if (dRevs>0) { + qChanged = true; + this.totaldist += dRevs*this.wheelCirc/63360.0; + if ((this.totaldist-this.settings.totaldist)>0.1) { + this.settings.totaldist = this.totaldist; + storage.writeJSON(SETTINGS_FILE, this.settings); + } + } + this.lastRevs = wheelRevs; + if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; + var wheelTime = event.target.value.getUint16(5, true); + var dT = (wheelTime-this.lastTime)/1024; + var dBT = (Date.now()-this.lastBangleTime)/1000; + this.lastBangleTime = Date.now(); + if (dT<0) dT+=64; + if (Math.abs(dT-dBT)>2) dT = dBT; + this.lastTime = wheelTime; + this.speed = this.lastSpeed; + if (dRevs>0 && dT>0) { + this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; + this.speedFailed = 0; + this.movingTime += dT; + } + else { + this.speedFailed++; + qChanged = false; + if (this.speedFailed>3) { + this.speed = 0; + qChanged = (this.lastSpeed>0); + } + } + this.lastSpeed = this.speed; + if (this.speed > this.maxSpeed) this.maxSpeed = this.speed; + } + if (qChanged && this.qUpdateScreen) this.updateScreen(); + } +} + +var mySensor = new CSCSensor(); + +function getSensorBatteryLevel(gatt) { + gatt.getPrimaryService("180f").then(function(s) { + return s.getCharacteristic("2a19"); + }).then(function(c) { + return c.readValue(); + }).then(function(d) { + mySensor.setBatteryLevel(d.buffer[0]); + }); +} + +function parseDevice(d) { + device = d; + g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Found device", 120, 120).flip(); + device.gatt.connect().then(function(ga) { + gatt = ga; + g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Connected", 120, 120).flip(); + return gatt.getPrimaryService("1816"); +}).then(function(s) { + service = s; + return service.getCharacteristic("2a5b"); +}).then(function(c) { + characteristic = c; + characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); + return characteristic.startNotifications(); +}).then(function() { + console.log("Done!"); + g.clearRect(0, 60, 239, 239).setColor(1, 1, 1).flip(); + getSensorBatteryLevel(gatt); + mySensor.updateScreen(); +}).catch(function(e) { + g.clearRect(0, 60, 239, 239).setColor(1, 0, 0).setFontAlign(0, 0, 0).drawString("ERROR"+e, 120, 120).flip(); + console.log(e); +})} + +function connection_setup() { + NRF.setScan(parseDevice, { filters: [{services:["1816"]}], timeout: 2000}); + g.clearRect(0, 60, 239, 239).setFontVector(18).setFontAlign(0, 0, 0).setColor(0, 1, 0); + g.drawString("Scanning for CSC sensor...", 120, 120); +} + +connection_setup(); +setWatch(function() { mySensor.reset(); g.clearRect(0, 60, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20}); +E.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); }); +NRF.on('disconnect', connection_setup); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/cscsensor/icons8-cycling-48.png b/apps/cscsensor/icons8-cycling-48.png new file mode 100644 index 000000000..0bc83859f Binary files /dev/null and b/apps/cscsensor/icons8-cycling-48.png differ diff --git a/apps/cscsensor/settings.js b/apps/cscsensor/settings.js new file mode 100644 index 000000000..d7a7d565d --- /dev/null +++ b/apps/cscsensor/settings.js @@ -0,0 +1,45 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'cscsensor.json' + // initialize with default settings... + let s = { + 'wheelcirc': 2230, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + s[key] = saved[key]; + } + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + } + } + const menu = { + '': { 'title': 'Cycle speed sensor' }, + '< Back': back, + 'Wheel circ.(mm)': { + value: s.wheelcirc, + min: 800, + max: 2400, + step: 5, + onchange: save('wheelcirc'), + }, + 'Reset total distance': function() { + E.showPrompt("Zero total distance?", {buttons: {"No":false, "Yes":true}}).then(function(v) { + if (v) { + s['totaldist'] = 0; + storage.write(SETTINGS_FILE, s); + } + }).then(back); + } + } + E.showMenu(menu); +}) diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog index 3b6757d9a..27dab7259 100755 --- a/apps/minionclk/ChangeLog +++ b/apps/minionclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: First release 0.02: Improved date readability, fixed drawing of widgets 0.03: Fixed rendering for Espruino v2.06 +0.04: Fixed overlapped rendering of dates diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 2d6a38603..f0afbc45c 100755 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -56,6 +56,8 @@ function startDrawing() { hour = ''; minute = ''; date = ''; + g.setColor(0x0000); + g.fillRect(0, 216, 240, 240); g.drawImage(getBackground(), 0, 24, { scale: 2 }); Bangle.drawWidgets(); draw();