Merge branch 'master' of github.com:espruino/BangleApps
commit
2b5a6a85a2
19
apps.json
19
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",
|
||||
|
|
@ -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 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))
|
||||
|
|
@ -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 |
|
|
@ -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);
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue