Finally merge two broken Bluetooth Cycle Speed sensor apps to use one library that 'just works' (I hope!) - added clockinfo and recoder functionality

pull/3428/head
Gordon Williams 2024-05-17 16:00:31 +01:00
parent 9841aeddeb
commit 5c89b45722
17 changed files with 494 additions and 406 deletions

3
apps/blecsc/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: Initial version
0.02: Minor code improvements
0.03: Moved from cycling app, fixed connection issues and cadence

32
apps/blecsc/README.md Normal file
View File

@ -0,0 +1,32 @@
# BLE Cycling Speed Sencor (CSC)
Displays data from a BLE Cycling Speed and Cadence sensor.
Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over.
## Settings
Accessible from `Settings -> Apps -> BLE CSC`
Here you can set the wheel diameter
## Development
```
var csc = require("blecsc").getInstance();
csc.on("status", txt => {
print("##", txt);
E.showMessage(txt);
});
csc.on("data", e => print(e));
csc.start();
```
The `data` event contains:
* cwr/ccr => wheel/crank cumulative revs
* lwet/lcet => wheel/crank last event time in 1/1024s
* wrps/crps => calculated wheel/crank revs per second
* wdt/cdt => time period in seconds between events
* wr => wheel revs
* kph => kilometers per hour

216
apps/blecsc/blecsc.js Normal file
View File

@ -0,0 +1,216 @@
/**
* This library communicates with a Bluetooth CSC peripherial using the Espruino NRF library.
*
* ## Usage:
* 1. Register event handlers using the \`on(eventName, handlerFunction)\` method
* You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can
* have raw characteristic values passed through using the \`value\` event.
* 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method
* 3. To tear down the connection, call the \`disconnect()\` method
*
* ## Events
* - \`status\` - string containing connection status
* - \`data\` - the peripheral sends a notification containing wheel/crank event data
* - \`disconnect\` - the peripheral ends the connection or the connection is lost
*
* cwr/ccr => wheel/crank cumulative revs
* lwet/lcet => wheel/crank last event time in 1/1024s
* wrps/crps => calculated wheel/crank revs per second
* wdt/cdt => time period in seconds between events
* wr => wheel revs
* kph => kilometers per hour
*/
class BLECSC {
constructor() {
this.reconnect = false; // set when start called
this.device = undefined; // set when device found
this.gatt = undefined; // set when connected
// .on("status", => string
// .on("data"
// .on("disconnect"
this.resetStats();
// Set default values and merge with stored values
this.settings = Object.assign({
circum: 2068 // circumference in mm
}, (require('Storage').readJSON('blecsc.json', true) || {}));
}
resetStats() {
this.cwr = undefined;
this.ccr = undefined;
this.lwet = undefined;
this.lcet = undefined;
this.lastCwr = undefined;
this.lastCcr = undefined;
this.lastLwet = undefined;
this.lastLcet = undefined;
this.kph = undefined;
this.wrps = 0; // wheel revs per second
this.crps = 0; // crank revs per second
//this.batteryLevel = undefined;
}
getDeviceAddress() {
if (!this.device || !this.device.id)
return '00:00:00:00:00:00';
return this.device.id.split(" ")[0];
}
status(txt) {
this.emit("status", txt);
}
/**
* Find and connect to a device which exposes the CSC service.
*
* @return {Promise}
*/
connect() {
this.status("Scanning");
// Find a device, then get the CSC Service and subscribe to
// notifications on the CSC Measurement characteristic.
// NRF.setLowPowerConnection(true);
var reconnect = this.reconnect; // auto-reconnect
return NRF.requestDevice({
timeout: 5000,
filters: [{
services: ["1816"]
}],
}).then(device => {
this.status("Connecting");
this.device = device;
this.device.on('gattserverdisconnected', event => {
this.device = undefined;
this.gatt = undefined;
this.resetStats();
this.status("Disconnected");
this.emit("disconnect", event);
if (reconnect) {// auto-reconnect
reconnect = false;
setTimeout(() => {
if (this.reconnect) this.connect().then(() => {}, () => {});
}, 500);
}
});
return new Promise(resolve => setTimeout(resolve, 150)); // On CooSpo we get a 'Connection Timeout' if we try and connect too soon
}).then(() => {
return this.device.gatt.connect();
}).then(gatt => {
this.status("Connected");
this.gatt = gatt;
return gatt.getPrimaryService("1816");
}).then(service => {
return service.getCharacteristic("2a5b"); // UUID of the CSC measurement characteristic
}).then(characteristic => {
// register for changes on 2a5b
characteristic.on('characteristicvaluechanged', event => {
const flags = event.target.value.getUint8(0);
var offs = 0;
var data = {};
if (flags & 1) { // FLAGS_WREV_BM
this.lastCwr = this.cwr;
this.lastLwet = this.lwet;
this.cwr = event.target.value.getUint32(1, true);
this.lwet = event.target.value.getUint16(5, true);
if (this.lastCwr === undefined) this.lastCwr = this.cwr;
if (this.lastLwet === undefined) this.lastLwet = this.lwet;
if (this.lwet < this.lastLwet) this.lastLwet -= 65536;
let secs = (this.lwet - this.lastLwet) / 1024;
this.wrps = (this.cwr - this.lastCwr) / (secs?secs:1);
this.kph = this.wrps * this.settings.circum / 3600;
Object.assign(data, { // Notify the 'wheelEvent' handler
cwr: this.cwr, // cumulative wheel revolutions
lwet: this.lwet, // last wheel event time
wrps: this.wrps, // wheel revs per second
wr: this.cwr - this.lastCwr, // wheel revs
wdt : secs, // time period
kph : this.kph
});
offs += 6;
}
if (flags & 2) { // FLAGS_CREV_BM
this.lastCcr = this.ccr;
this.lastLcet = this.lcet;
this.ccr = event.target.value.getUint16(offs + 1, true);
this.lcet = event.target.value.getUint16(offs + 3, true);
if (this.lastCcr === undefined) this.lastCcr = this.ccr;
if (this.lastLcet === undefined) this.lastLcet = this.lcet;
if (this.lcet < this.lastLcet) this.lastLcet -= 65536;
let secs = (this.lcet - this.lastLcet) / 1024;
this.crps = (this.ccr - this.lastCcr) / (secs?secs:1);
Object.assign(data, { // Notify the 'crankEvent' handler
ccr: this.ccr, // cumulative crank revolutions
lcet: this.lcet, // last crank event time
crps: this.crps, // crank revs per second
cdt : secs, // time period
});
}
this.emit("data",data);
});
return characteristic.startNotifications();
/* }).then(() => {
return this.gatt.getPrimaryService("180f");
}).then(service => {
return service.getCharacteristic("2a19");
}).then(characteristic => {
characteristic.on('characteristicvaluechanged', (event)=>{
this.batteryLevel = event.target.value.getUint8(0);
});
return characteristic.startNotifications();*/
}).then(() => {
this.status("Ready");
}, err => {
this.status("Error: " + err);
if (reconnect) { // auto-reconnect
reconnect = false;
setTimeout(() => {
if (this.reconnect) this.connect().then(() => {}, () => {});
}, 500);
}
throw err;
});
}
/**
* Disconnect the device.
*/
disconnect() {
if (!this.gatt) return;
this.gatt.disconnect();
this.gatt = undefined;
}
/* Start trying to connect - will keep searching and attempting to connect*/
start() {
this.reconnect = true;
if (!this.device)
this.connect().then(() => {}, () => {});
}
/* Stop trying to connect, and disconnect */
stop() {
this.reconnect = false;
this.disconnect();
}
}
// Get an instance of BLECSC or create one if it doesn't exist
BLECSC.getInstance = function() {
if (!BLECSC.instance) {
BLECSC.instance = new BLECSC();
}
return BLECSC.instance;
};
exports = BLECSC;
/*
var csc = require("blecsc").getInstance();
csc.on("status", txt => {
print("##", txt);
E.showMessage(txt);
});
csc.on("data", e => print(e));
csc.start();
*/

74
apps/blecsc/clkinfo.js Normal file
View File

@ -0,0 +1,74 @@
(function() {
var csc = require("blecsc").getInstance();
//csc.on("status", txt => { print("CSC",txt); });
csc.on("data", e => {
ci.items.forEach(it => { if (it._visible) it.emit('redraw'); });
});
csc.on("disconnect", e => {
// redraw all with no info
ci.items.forEach(it => { if (it._visible) it.emit('redraw'); });
});
var uses = 0;
var ci = {
name: "CSC",
items: [
{ name : "Speed",
get : () => {
return {
text : (csc.kph === undefined) ? "--" : require("locale").speed(csc.kph),
img : atob("GBiBAAAAAAAAAAAAAAABwAABwAeBgAMBgAH/gAH/wAPDwA/DcD9m/Ge35sW9o8//M8/7E8CBA2GBhn8A/h4AeAAAAAAAAAAAAAAAAA==")
};
},
show : function() {
uses++;
if (uses==1) csc.start();
this._visible = true;
},
hide : function() {
this._visible = false;
uses--;
if (uses==0) csc.stop();
}
},
{ name : "Distance",
get : () => {
return {
text : (csc.kph === undefined) ? "--" : require("locale").distance(csc.cwr * csc.settings.circum / 1000),
img : atob("GBiBAAAAAB8AADuAAGDAAGTAAGRAAEBAAGBAAGDAADCAADGAIB8B+A/BjAfjBgAyJgAyIgAyAj/jBnADBmABjGAA2HAA8D//4AAAAA==")
};
},
show : function() {
uses++;
if (uses==1) csc.start();
this._visible = true;
},
hide : function() {
this._visible = false;
uses--;
if (uses==0) csc.stop();
}
},
{ name : "Cadence",
get : () => {
return {
text : (csc.crps === undefined) ? "--" : Math.round(csc.crps*60),
img : atob("GBiBAAAAAAAAAAB+EAH/sAeB8A4A8AwB8BgAABgAADAAADAAADAAADAADDAADDAAABgAABgAGAwAEA4AAAeAwAH8gAB8AAAAAAAAAA==")
};
},
show : function() {
uses++;
if (uses==1) csc.start();
this._visible = true;
},
hide : function() {
this._visible = false;
uses--;
if (uses==0) csc.stop();
}
}
]
};
return ci;
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

22
apps/blecsc/metadata.json Normal file
View File

@ -0,0 +1,22 @@
{
"id": "blecsc",
"name": "BLE Cycling Speed Sensor Library",
"shortName": "BLE CSC",
"version": "0.03",
"description": "Module to get live values from a BLE Cycle Speed (CSC) sensor. Includes recorder and clockinfo plugins",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth,clkinfo",
"type":"module",
"provides_modules" : ["blecsc"],
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"blecsc","url":"blecsc.js"},
{"name":"blecsc.settings.js","url":"settings.js"},
{"name":"blecsc.recorder.js","url":"recorder.js"},
{"name":"blecsc.clkinfo.js","url":"clkinfo.js"}
],
"data": [
{"name":"blecsc.json"}
]
}

28
apps/blecsc/recorder.js Normal file
View File

@ -0,0 +1,28 @@
(function(recorders) {
recorders.blecsc = function() {
var csc = require("blecsc").getInstance();
var speed, cadence;
csc.on("data", e => {
speed = e.kph; // speed in KPH
cadence = (e.crps===undefined)?"":Math.round(e.crps*60); // crank rotations per minute
});
return {
name : "CSC",
fields : ["Speed (kph)","Cadence (rpm)"],
getValues : () => {
var r = [speed,cadence];
speed = "";
cadence = "";
return r;
},
start : () => {
csc.start();
},
stop : () => {
csc.stop();
},
draw : (x,y) => g.setColor(csc.device?"#0f0":"#8f8").drawImage(atob("Dw+BAAAAAAABgOIA5gHcBxw9fpfTPqYRC8HgAAAAAAAA"),x,y)
};
}
})

85
apps/blecsc/settings.js Normal file
View File

@ -0,0 +1,85 @@
(function(back) {
const storage = require('Storage')
const SETTINGS_FILE = 'blecsc.json'
// Set default values and merge with stored values
let settings = Object.assign({
circum: 2068 // circumference in mm
}, (storage.readJSON(SETTINGS_FILE, true) || {}));
function saveSettings() {
storage.writeJSON(SETTINGS_FILE, settings);
}
function circumMenu() {
var v = 0|settings.circum;
var cm = 0|(v/10);
var mm = v-(cm*10);
E.showMenu({
'': { title: /*LANG*/"Circumference", back: mainMenu },
'cm': {
value: cm,
min: 80, max: 240, step: 1,
onchange: (v) => {
cm = v;
settings.circum = (cm*10)+mm;
saveSettings();
},
},
'+ mm': {
value: mm,
min: 0, max: 9, step: 1,
onchange: (v) => {
mm = v;
settings.circum = (cm*10)+mm;
saveSettings();
},
},
/*LANG*/'Std Wheels': function() {
// https://support.wahoofitness.com/hc/en-us/articles/115000738484-Tire-Size-Wheel-Circumference-Chart
E.showMenu({
'': { title: /*LANG*/'Std Wheels', back: circumMenu },
'650x38 wheel' : function() {
settings.circum = 1995;
saveSettings();
mainMenu();
},
'700x32c wheel' : function() {
settings.circum = 2152;
saveSettings();
mainMenu();
},
'24"x1.75 wheel' : function() {
settings.circum = 1890;
saveSettings();
mainMenu();
},
'26"x1.5 wheel' : function() {
settings.circum = 2010;
saveSettings();
mainMenu();
},
'27.5"x1.5 wheel' : function() {
settings.circum = 2079;
saveSettings();
mainMenu();
}
});
}
});
}
function mainMenu() {
E.showMenu({
'': { 'title': 'BLE CSC' },
'< Back': back,
/*LANG*/'Circumference': {
value: settings.circum+"mm",
onchange: circumMenu
},
});
}
mainMenu();
})

View File

@ -8,4 +8,5 @@
0.07: Make Bangle.js 2 compatible
0.08: Convert Yes/No On/Off in settings to checkboxes
0.09: Automatically reconnect on error
0.10: Fix cscsensor when using coospoo sensor that supports crank *and* wheel
0.10: Fix cscsensor when using coospoo sensor that supports crank *and* wheel
0.11: Update to use blecsc library

View File

@ -1,8 +1,3 @@
var device;
var gatt;
var service;
var characteristic;
const SETTINGS_FILE = 'cscsensor.json';
const storage = require('Storage');
const W = g.getWidth();
@ -17,12 +12,10 @@ 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;
@ -34,8 +27,6 @@ class CSCSensor {
this.distFactor = this.qMetric ? 1.609344 : 1;
this.screenInit = true;
this.batteryLevel = -1;
this.lastCrankTime = 0;
this.lastCrankRevs = 0;
this.showCadence = false;
this.cadence = 0;
}
@ -63,10 +54,6 @@ class CSCSensor {
}
}
updateBatteryLevel(event) {
if (event.target.uuid == "0x2a19") this.setBatteryLevel(event.target.value.getUint8(0));
}
drawBatteryIcon() {
g.setColor(1, 1, 1).drawRect(10*W/240, yStart+0.029167*H, 20*W/240, yStart+0.1125*H)
.fillRect(14*W/240, yStart+0.020833*H, 16*W/240, yStart+0.029167*H)
@ -81,7 +68,7 @@ class CSCSensor {
}
updateScreenRevs() {
var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0;
var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*csc.settings.circum/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;
@ -157,119 +144,38 @@ class CSCSensor {
}
}
updateSensor(event) {
var qChanged = false;
if (event.target.uuid == "0x2a5b") {
let flags = event.target.value.getUint8(0);
let offs = 0;
if (flags & 1) {
// wheel revolution
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)>3) 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 if (!this.showCadence) {
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.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
offs += 6;
}
if (flags & 2) {
// crank revolution - if enabled
const crankRevs = event.target.value.getUint16(offs + 1, true);
const crankTime = event.target.value.getUint16(offs + 3, true);
if (crankTime > this.lastCrankTime) {
this.cadence = (crankRevs-this.lastCrankRevs)/(crankTime-this.lastCrankTime)*(60*1024);
qChanged = true;
}
this.lastCrankRevs = crankRevs;
this.lastCrankTime = crankTime;
}
}
if (qChanged) this.updateScreen();
}
}
var mySensor = new CSCSensor();
function getSensorBatteryLevel(gatt) {
gatt.getPrimaryService("180f").then(function(s) {
return s.getCharacteristic("2a19");
}).then(function(c) {
c.on('characteristicvaluechanged', (event)=>mySensor.updateBatteryLevel(event));
return c.startNotifications();
});
}
var csc = require("blecsc").getInstance();
csc.on("data", e => {
mySensor.totaldist += e.wr * csc.settings.circum/*mm*/ / 1000000; // finally in km
mySensor.lastRevs = e.cwr;
if (mySensor.lastRevsStart<0) mySensor.lastRevsStart = e.cwr;
mySensor.speed = e.kph;
mySensor.movingTime += e.wdt;
if (mySensor.speed>mySensor.maxSpeed && (mySensor.movingTime>3 || mySensor.speed<20) && mySensor.speed<50)
mySensor.maxSpeed = mySensor.speed;
mySensor.cadence = e.crps;
mySensor.updateScreen();
mySensor.updateScreen();
});
function connection_setup() {
mySensor.screenInit = true;
E.showMessage("Scanning for CSC sensor...");
NRF.requestDevice({ filters: [{services:["1816"]}]}).then(function(d) {
device = d;
E.showMessage("Found device");
return device.gatt.connect();
}).then(function(ga) {
gatt = ga;
E.showMessage("Connected");
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.reset().clearRect(Bangle.appRect).flip();
getSensorBatteryLevel(gatt);
mySensor.updateScreen();
}).catch(function(e) {
E.showMessage(e.toString(), "ERROR");
console.log(e);
setTimeout(connection_setup, 1000);
});
}
connection_setup();
csc.on("status", txt => {
//print("->", txt);
E.showMessage(txt);
});
E.on('kill',()=>{
if (gatt!=undefined) gatt.disconnect();
csc.stop();
mySensor.settings.totaldist = mySensor.totaldist;
storage.writeJSON(SETTINGS_FILE, mySensor.settings);
});
NRF.on('disconnect', connection_setup); // restart if disconnected
Bangle.setUI("updown", d=>{
if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
else if (!d) { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
});
Bangle.loadWidgets();
Bangle.drawWidgets();
csc.start(); // start a connection

View File

@ -2,10 +2,11 @@
"id": "cscsensor",
"name": "Cycling speed sensor",
"shortName": "CSCSensor",
"version": "0.10",
"version": "0.11",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth,bike,cycle,bicycle",
"dependencies" : { "blecsc":"module" },
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -1,2 +1,3 @@
0.01: Initial version
0.02: Minor code improvements
0.03: Move blecsc library into its own app so it can be shared (and fix some issues)

View File

@ -1,4 +1,5 @@
# Cycling
> Displays data from a BLE Cycling Speed and Cadence sensor.
*This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.*
@ -27,8 +28,5 @@ Inside the Cycling app, use button / tap screen to:
## TODO
* Sensor battery status
* Implement crank events / show cadence
* Bangle.js 1 compatibility
* Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike)
## Development
There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage.

View File

@ -1,111 +0,0 @@
// UUID of the Bluetooth CSC Service
//const SERVICE_UUID = "1816";
// UUID of the CSC measurement characteristic
const MEASUREMENT_UUID = "2a5b";
// Wheel revolution present bit mask
const FLAGS_WREV_BM = 0x01;
// Crank revolution present bit mask
const FLAGS_CREV_BM = 0x02;
/**
* Fake BLECSC implementation for the emulator, where it's hard to test
* with actual hardware. Generates "random" wheel events (no crank).
*
* To upload as a module, paste the entire file in the console using this
* command: require("Storage").write("blecsc-emu",`<FILE CONTENT HERE>`);
*/
class BLECSCEmulator {
constructor() {
this.timeout = undefined;
this.interval = 500;
this.ccr = 0;
this.lwt = 0;
this.handlers = {
// value
// disconnect
// wheelEvent
// crankEvent
};
}
getDeviceAddress() {
return 'fa:ke:00:de:vi:ce';
}
/**
* Callback for the GATT characteristicvaluechanged event.
* Consumers must not call this method!
*/
onValue(event) {
// Not interested in non-CSC characteristics
if (event.target.uuid != "0x" + MEASUREMENT_UUID) return;
// Notify the generic 'value' handler
if (this.handlers.value) this.handlers.value(event);
const flags = event.target.value.getUint8(0, true);
// Notify the 'wheelEvent' handler
if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({
cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions
lwet: event.target.value.getUint16(5, true), // last wheel event time
});
// Notify the 'crankEvent' handler
if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({
ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions
lcet: event.target.value.getUint16(9, true), // last crank event time
});
}
/**
* Register an event handler.
*
* @param {string} event value|disconnect
* @param {function} handler handler function that receives the event as its first argument
*/
on(event, handler) {
this.handlers[event] = handler;
}
fakeEvent() {
this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20));
this.lwt = (this.lwt + this.interval) % 0x10000;
this.ccr++;
var buffer = new ArrayBuffer(8);
var view = new DataView(buffer);
view.setUint8(0, 0x01); // Wheel revolution data present bit
view.setUint32(1, this.ccr, true); // Cumulative crank revolutions
view.setUint16(5, this.lwt, true); // Last wheel event time
this.onValue({
target: {
uuid: "0x2a5b",
value: view,
},
});
this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval);
}
/**
* Find and connect to a device which exposes the CSC service.
*
* @return {Promise}
*/
connect() {
this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval);
return Promise.resolve(true);
}
/**
* Disconnect the device.
*/
disconnect() {
if (!this.timeout) return;
clearTimeout(this.timeout);
}
}
exports = BLECSCEmulator;

View File

@ -1,150 +0,0 @@
const SERVICE_UUID = "1816";
// UUID of the CSC measurement characteristic
const MEASUREMENT_UUID = "2a5b";
// Wheel revolution present bit mask
const FLAGS_WREV_BM = 0x01;
// Crank revolution present bit mask
const FLAGS_CREV_BM = 0x02;
/**
* This class communicates with a Bluetooth CSC peripherial using the Espruino NRF library.
*
* ## Usage:
* 1. Register event handlers using the \`on(eventName, handlerFunction)\` method
* You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can
* have raw characteristic values passed through using the \`value\` event.
* 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method
* 3. To tear down the connection, call the \`disconnect()\` method
*
* ## Events
* - \`wheelEvent\` - the peripharial sends a notification containing wheel event data
* - \`crankEvent\` - the peripharial sends a notification containing crank event data
* - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event)
* - \`disconnect\` - the peripherial ends the connection or the connection is lost
*
* Each event can only have one handler. Any call to \`on()\` will
* replace a previously registered handler for the same event.
*/
class BLECSC {
constructor() {
this.device = undefined;
this.ccInterval = undefined;
this.gatt = undefined;
this.handlers = {
// wheelEvent
// crankEvent
// value
// disconnect
};
}
getDeviceAddress() {
if (!this.device || !this.device.id)
return '00:00:00:00:00:00';
return this.device.id.split(" ")[0];
}
checkConnection() {
if (!this.device)
console.log("no device");
// else
// console.log("rssi: " + this.device.rssi);
}
/**
* Callback for the GATT characteristicvaluechanged event.
* Consumers must not call this method!
*/
onValue(event) {
// Not interested in non-CSC characteristics
if (event.target.uuid != "0x" + MEASUREMENT_UUID) return;
// Notify the generic 'value' handler
if (this.handlers.value) this.handlers.value(event);
const flags = event.target.value.getUint8(0, true);
// Notify the 'wheelEvent' handler
if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({
cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions
lwet: event.target.value.getUint16(5, true), // last wheel event time
});
// Notify the 'crankEvent' handler
if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({
ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions
lcet: event.target.value.getUint16(9, true), // last crank event time
});
}
/**
* Callback for the NRF disconnect event.
* Consumers must not call this method!
*/
onDisconnect(event) {
console.log("disconnected");
if (this.ccInterval)
clearInterval(this.ccInterval);
if (!this.handlers.disconnect) return;
this.handlers.disconnect(event);
}
/**
* Register an event handler.
*
* @param {string} event wheelEvent|crankEvent|value|disconnect
* @param {function} handler function that will receive the event as its first argument
*/
on(event, handler) {
this.handlers[event] = handler;
}
/**
* Find and connect to a device which exposes the CSC service.
*
* @return {Promise}
*/
connect() {
// Register handler for the disconnect event to be passed throug
NRF.on('disconnect', this.onDisconnect.bind(this));
// Find a device, then get the CSC Service and subscribe to
// notifications on the CSC Measurement characteristic.
// NRF.setLowPowerConnection(true);
return NRF.requestDevice({
timeout: 5000,
filters: [{ services: [SERVICE_UUID] }],
}).then(device => {
this.device = device;
this.device.on('gattserverdisconnected', this.onDisconnect.bind(this));
this.ccInterval = setInterval(this.checkConnection.bind(this), 2000);
return device.gatt.connect();
}).then(gatt => {
this.gatt = gatt;
return gatt.getPrimaryService(SERVICE_UUID);
}).then(service => {
return service.getCharacteristic(MEASUREMENT_UUID);
}).then(characteristic => {
characteristic.on('characteristicvaluechanged', this.onValue.bind(this));
return characteristic.startNotifications();
});
}
/**
* Disconnect the device.
*/
disconnect() {
if (this.ccInterval)
clearInterval(this.ccInterval);
if (!this.gatt) return;
try {
this.gatt.disconnect();
} catch {
//
}
}
}
exports = BLECSC;

View File

@ -23,7 +23,6 @@ class CSCSensor {
// CSC runtime variables
this.movingTime = 0; // unit: s
this.lastBangleTime = Date.now(); // unit: ms
this.lwet = 0; // last wheel event time (unit: s/1024)
this.cwr = -1; // cumulative wheel revolutions
this.cwrTrip = 0; // wheel revolutions since trip start
this.speed = 0; // unit: m/s
@ -84,7 +83,7 @@ class CSCSensor {
console.log("Trying to connect to BLE CSC");
// Hook up events
this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this));
this.blecsc.on('data', this.onWheelEvent.bind(this));
this.blecsc.on('disconnect', this.onDisconnect.bind(this));
// Scan for BLE device and connect
@ -171,20 +170,11 @@ class CSCSensor {
// Increment the trip revolutions counter
this.cwrTrip += dRevs;
// Calculate time delta since last wheel event
var dT = (event.lwet - this.lwet)/1024;
var now = Date.now();
var dBT = (now-this.lastBangleTime)/1000;
this.lastBangleTime = now;
if (dT<0) dT+=64; // wheel event time wraps every 64s
if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this
this.lwet = event.lwet;
// Recalculate current speed
if (dRevs>0 && dT>0) {
this.speed = dRevs * this.wheelCirc / dT;
if (dRevs>0 ) {
this.speed = event.wrps * this.wheelCirc;
this.speedFailed = 0;
this.movingTime += dT;
this.movingTime += event.wdt;
} else {
this.speedFailed++;
if (this.speedFailed>3) {
@ -429,15 +419,7 @@ class CSCDisplay {
}
}
var BLECSC;
if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") {
// Emulator
BLECSC = require("blecsc-emu");
} else {
// Actual hardware
BLECSC = require("blecsc");
}
var blecsc = new BLECSC();
var blecsc = require("blecsc").getInstance();
var display = new CSCDisplay();
var sensor = new CSCSensor(blecsc, display);

View File

@ -2,16 +2,16 @@
"id": "cycling",
"name": "Bangle Cycling",
"shortName": "Cycling",
"version": "0.02",
"version": "0.03",
"description": "Display live values from a BLE CSC sensor",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth",
"dependencies" : { "blecsc":"module" },
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"cycling.app.js","url":"cycling.app.js"},
{"name":"cycling.settings.js","url":"settings.js"},
{"name":"blecsc","url":"blecsc.js"},
{"name":"cycling.img","url":"cycling.icon.js","evaluate": true}
],
"data": [