Merge pull request #557 from berkenbu/master

Cycling speed sensor app
pull/563/head
Gordon Williams 2020-09-14 07:45:29 +01:00 committed by GitHub
commit 1af5fab35f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 219 additions and 2 deletions

View File

@ -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.03",
"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",

4
apps/cscsensor/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: New app!
0.02: Add wheel circumference settings dialog
0.03: Save total distance traveled

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,137 @@
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;
}
reset() {
this.maxSpeed = 0;
this.movingTime = 0;
this.lastRevsStart = this.lastRevs;
this.maxSpeed = 0;
}
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;
g.setFontAlign(1, -1, 0).setFontVector(18).setColor(1, 1, 0);
g.drawString("Time:", 86, 60);
g.drawString("Speed:", 86, 92);
g.drawString("Ave spd:", 86, 124);
g.drawString("Max spd:", 86, 156);
g.drawString("Trip dist:", 86, 188);
g.drawString("Total:", 86, 220);
g.setFontAlign(-1, -1, 0).setFontVector(24).setColor(1, 1, 1).clearRect(92, 60, 239, 239);
g.drawString(dmins+":"+dsecs, 92, 60);
g.drawString(dspeed+" "+this.speedUnit, 92, 92);
g.drawString(avespeed + " " + this.speedUnit, 92, 124);
g.drawString(maxspeed + " " + this.speedUnit, 92, 156);
g.drawString(ddist + " " + this.distUnit, 92, 188);
g.drawString(tdist + " " + this.distUnit, 92, 220);
}
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;
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 += dBT;
}
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 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();
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();
})}
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);
setWatch(function() { mySensor.reset(); g.clearRect(0, 60, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20});
Bangle.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); });
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);
})