Merge branch 'master' of github.com:espruino/BangleApps

master
Gordon Williams 2020-09-18 11:51:25 +01:00
commit 2b5a6a85a2
11 changed files with 267 additions and 5 deletions

View File

@ -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",
@ -2172,6 +2172,20 @@
{"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 @@
]
}
]

View File

@ -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)

View File

@ -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) {}
});

5
apps/cscsensor/ChangeLog Normal file
View File

@ -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

16
apps/cscsensor/README.md Normal file
View File

@ -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.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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);
})

View File

@ -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

View File

@ -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();