Cycling: Initial commit
parent
bbd2cac110
commit
31d835dad5
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Initial version
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Cycling
|
||||||
|
> Displays data from a BLE Cycling Speed and Cadence sensor.
|
||||||
|
|
||||||
|
*Fork of the CSCSensor app using the layout library and separate module for CSC functionality*
|
||||||
|
|
||||||
|
The following data are displayed:
|
||||||
|
- curent speed
|
||||||
|
- moving time
|
||||||
|
- average speed
|
||||||
|
- maximum speed
|
||||||
|
- trip distance
|
||||||
|
- total distance
|
||||||
|
|
||||||
|
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, a absolute value that persists throughout the lifetime of the sensor and never rolls over.
|
||||||
|
|
||||||
|
**Cadence / Crank features are currently not implemented**
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
* Settings: imperial/metric
|
||||||
|
* Store circumference per device address
|
||||||
|
* Sensor battery status
|
||||||
|
* Implement crank events / show cadence
|
||||||
|
* Bangle.js 1 compatibility
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
// 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;
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,420 @@
|
||||||
|
const Layout = require('Layout');
|
||||||
|
|
||||||
|
const SETTINGS_FILE = 'cscsensor.json';
|
||||||
|
const storage = require('Storage');
|
||||||
|
|
||||||
|
const RECONNECT_TIMEOUT = 4000;
|
||||||
|
const MAX_CONN_ATTEMPTS = 2;
|
||||||
|
|
||||||
|
class CSCSensor {
|
||||||
|
constructor(blecsc, display) {
|
||||||
|
// Dependency injection
|
||||||
|
this.blecsc = blecsc;
|
||||||
|
this.display = display;
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
this.settings = storage.readJSON(SETTINGS_FILE, 1) || {};
|
||||||
|
this.wheelCirc = (this.settings.wheelcirc || 2230) / 1000; // unit: m
|
||||||
|
|
||||||
|
// 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
|
||||||
|
this.maxSpeed = 0; // unit: m/s
|
||||||
|
this.speedFailed = 0;
|
||||||
|
|
||||||
|
// Other runtime variables
|
||||||
|
this.connected = false;
|
||||||
|
this.failedAttempts = 0;
|
||||||
|
this.failed = false;
|
||||||
|
|
||||||
|
// Layout configuration
|
||||||
|
this.layout = 0;
|
||||||
|
this.display.useMetricUnits(true);
|
||||||
|
// this.display.useMetricUnits(!require("locale").speed(1).toString().endsWith("mph"));
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisconnect(event) {
|
||||||
|
console.log("disconnected ", event);
|
||||||
|
|
||||||
|
this.connected = false;
|
||||||
|
this.setLayout(0);
|
||||||
|
this.display.setDeviceAddress("unknown");
|
||||||
|
|
||||||
|
if (this.failedAttempts >= MAX_CONN_ATTEMPTS) {
|
||||||
|
this.failed = true;
|
||||||
|
this.display.setStatus("Connection failed after " + MAX_CONN_ATTEMPTS + " attempts.");
|
||||||
|
} else {
|
||||||
|
this.display.setStatus("Disconnected");
|
||||||
|
setTimeout(this.connect.bind(this), RECONNECT_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.connected = false;
|
||||||
|
this.setLayout(0);
|
||||||
|
this.display.setStatus("Connecting...");
|
||||||
|
console.log("Trying to connect to BLE CSC");
|
||||||
|
|
||||||
|
// Hook up events
|
||||||
|
this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this));
|
||||||
|
this.blecsc.on('disconnect', this.onDisconnect.bind(this));
|
||||||
|
|
||||||
|
// Scan for BLE device and connect
|
||||||
|
this.blecsc.connect()
|
||||||
|
.then(function() {
|
||||||
|
this.failedAttempts = 0;
|
||||||
|
this.failed = false;
|
||||||
|
this.connected = true;
|
||||||
|
var addr = this.blecsc.getDeviceAddress();
|
||||||
|
console.log("Connected to " + addr);
|
||||||
|
|
||||||
|
this.display.setDeviceAddress(addr);
|
||||||
|
this.display.setStatus("Connected");
|
||||||
|
|
||||||
|
// Switch to speed screen in 2s
|
||||||
|
setTimeout(function() {
|
||||||
|
this.setLayout(1);
|
||||||
|
this.updateScreen();
|
||||||
|
}.bind(this), 2000);
|
||||||
|
}.bind(this))
|
||||||
|
.catch(function(e) {
|
||||||
|
this.failedAttempts++;
|
||||||
|
this.onDisconnect(e);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.blecsc.disconnect();
|
||||||
|
this.connected = false;
|
||||||
|
this.setLayout(0);
|
||||||
|
this.display.setStatus("Disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout(num) {
|
||||||
|
this.layout = num;
|
||||||
|
if (this.layout == 0) {
|
||||||
|
this.display.updateLayout("status");
|
||||||
|
} else if (this.layout == 1) {
|
||||||
|
this.display.updateLayout("speed");
|
||||||
|
} else if (this.layout == 2) {
|
||||||
|
this.display.updateLayout("distance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.connected = false;
|
||||||
|
this.failed = false;
|
||||||
|
this.failedAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
interact(d) {
|
||||||
|
// Only interested in tap / center button
|
||||||
|
if (d) return;
|
||||||
|
|
||||||
|
// Reconnect in failed state
|
||||||
|
if (this.failed) {
|
||||||
|
this.reset();
|
||||||
|
this.connect();
|
||||||
|
} else if (this.connected) {
|
||||||
|
this.setLayout((this.layout + 1) % 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScreen() {
|
||||||
|
var tripDist = this.cwrTrip * this.wheelCirc;
|
||||||
|
var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0
|
||||||
|
|
||||||
|
this.display.setTotalDistance(this.cwr * this.wheelCirc);
|
||||||
|
this.display.setTripDistance(tripDist);
|
||||||
|
this.display.setSpeed(this.speed);
|
||||||
|
this.display.setAvg(avgSpeed);
|
||||||
|
this.display.setMax(this.maxSpeed);
|
||||||
|
this.display.setTime(Math.floor(this.movingTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent(event) {
|
||||||
|
// Calculate number of revolutions since last wheel event
|
||||||
|
var dRevs = (this.cwr > 0 ? event.cwr - this.cwr : 0);
|
||||||
|
this.cwr = event.cwr;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
this.speedFailed = 0;
|
||||||
|
this.movingTime += dT;
|
||||||
|
} else {
|
||||||
|
this.speedFailed++;
|
||||||
|
if (this.speedFailed>3) {
|
||||||
|
this.speed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update max speed
|
||||||
|
if (this.speed>this.maxSpeed
|
||||||
|
&& (this.movingTime>3 || this.speed<20)
|
||||||
|
&& this.speed<50
|
||||||
|
) this.maxSpeed = this.speed;
|
||||||
|
|
||||||
|
this.updateScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CSCDisplay {
|
||||||
|
constructor() {
|
||||||
|
this.metric = true;
|
||||||
|
this.fontLabel = "6x8";
|
||||||
|
this.fontMed = "15%";
|
||||||
|
this.fontLarge = "32%";
|
||||||
|
this.currentLayout = "status";
|
||||||
|
this.layouts = {};
|
||||||
|
this.layouts.speed = new Layout({
|
||||||
|
type: "v",
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "speed_g",
|
||||||
|
fillx: 1,
|
||||||
|
filly: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{type: undefined, width: 32, halign: -1},
|
||||||
|
{type: "txt", id: "speed", label: "00.0", font: this.fontLarge, bgCol: "#fff", col: "#000", width: 122},
|
||||||
|
{type: "txt", id: "speed_u", label: " km/h", font: this.fontLabel, col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "time_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#000",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{type: undefined, width: 32, halign: -1},
|
||||||
|
{type: "txt", id: "time", label: "00:00", font: this.fontMed, bgCol: "#000", col: "#fff", width: 122},
|
||||||
|
{type: "txt", id: "time_u", label: "mins", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "stats_g",
|
||||||
|
fillx: 1,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "v",
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "max_l", label: "MAX", font: this.fontLabel, col: "#000"},
|
||||||
|
{type: "txt", id: "max", label: "00.0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 69},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "v",
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "avg_l", label: "AVG", font: this.fontLabel, col: "#000"},
|
||||||
|
{type: "txt", id: "avg", label: "00.0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 69},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{type: "txt", id: "stats_u", label: " km/h", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
this.layouts.distance = new Layout({
|
||||||
|
type: "v",
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "tripd_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "tripd_l", label: "TRP", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36},
|
||||||
|
{type: "txt", id: "tripd", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118},
|
||||||
|
{type: "txt", id: "tripd_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "totald_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#000",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 36},
|
||||||
|
{type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#000", col: "#fff", width: 118},
|
||||||
|
{type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
this.layouts.status = new Layout({
|
||||||
|
type: "v",
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "status_g",
|
||||||
|
fillx: 1,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 100,
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "status", label: "Bangle Cycling", font: this.fontMed, bgCol: "#fff", col: "#000", width: 176, wrap: 1},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "addr_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{ type: "txt", id: "addr_l", label: "MAC", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 },
|
||||||
|
{ type: "txt", id: "addr", label: "unknown", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 140 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLayout(layout) {
|
||||||
|
this.currentLayout = layout;
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
this.layouts[layout].update();
|
||||||
|
this.layouts[layout].render();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIfLayoutActive(layout, node) {
|
||||||
|
if (layout != this.currentLayout) return;
|
||||||
|
this.layouts[layout].render(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
useMetricUnits(metric) {
|
||||||
|
this.metric = metric;
|
||||||
|
|
||||||
|
console.log("using " + (metric ? "metric" : "imperial") + " units");
|
||||||
|
|
||||||
|
var speedUnit = metric ? "km/h" : "mph";
|
||||||
|
this.layouts.speed.speed_u.label = speedUnit;
|
||||||
|
this.layouts.speed.stats_u.label = speedUnit;
|
||||||
|
|
||||||
|
var distanceUnit = metric ? "km" : "mi";
|
||||||
|
this.layouts.distance.tripd_u.label = distanceUnit;
|
||||||
|
this.layouts.distance.totald_u.label = distanceUnit;
|
||||||
|
|
||||||
|
this.updateLayout(this.currentLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertDistance(meters) {
|
||||||
|
if (this.metric) return meters / 1000;
|
||||||
|
return meters / 1609.344;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertSpeed(mps) {
|
||||||
|
if (this.metric) return mps * 3.6;
|
||||||
|
return mps * 2.23694;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpeed(speed) {
|
||||||
|
this.layouts.speed.speed.label = this.convertSpeed(speed).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.speed_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvg(speed) {
|
||||||
|
this.layouts.speed.avg.label = this.convertSpeed(speed).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.stats_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMax(speed) {
|
||||||
|
this.layouts.speed.max.label = this.convertSpeed(speed).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.stats_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTime(seconds) {
|
||||||
|
var time = '';
|
||||||
|
var hours = Math.floor(seconds/3600);
|
||||||
|
if (hours) {
|
||||||
|
time += hours + ":";
|
||||||
|
this.layouts.speed.time_u.label = " hrs";
|
||||||
|
} else {
|
||||||
|
this.layouts.speed.time_u.label = "mins";
|
||||||
|
}
|
||||||
|
|
||||||
|
time += String(Math.floor((seconds%3600)/60)).padStart(2, '0') + ":";
|
||||||
|
time += String(seconds % 60).padStart(2, '0');
|
||||||
|
|
||||||
|
this.layouts.speed.time.label = time;
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.time_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTripDistance(distance) {
|
||||||
|
this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1)
|
||||||
|
this.renderIfLayoutActive("distance", this.layouts.distance.tripd_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTotalDistance(distance) {
|
||||||
|
this.layouts.distance.totald.label = this.convertDistance(distance).toFixed(1)
|
||||||
|
this.renderIfLayoutActive("distance", this.layouts.distance.totald_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeviceAddress(address) {
|
||||||
|
this.layouts.status.addr.label = address
|
||||||
|
this.renderIfLayoutActive("status", this.layouts.status.addr_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(status) {
|
||||||
|
this.layouts.status.status.label = status
|
||||||
|
this.renderIfLayoutActive("status", this.layouts.status.status_g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 display = new CSCDisplay();
|
||||||
|
var sensor = new CSCSensor(blecsc, display);
|
||||||
|
|
||||||
|
E.on('kill',()=>{
|
||||||
|
sensor.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.setUI("updown", d => {
|
||||||
|
sensor.interact(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
sensor.connect();
|
||||||
|
// Bangle.loadWidgets();
|
||||||
|
// Bangle.drawWidgets();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"id": "cycling",
|
||||||
|
"name": "Bangle Cycling",
|
||||||
|
"shortName": "Cycling",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Display live values from a BLE CSC sensor",
|
||||||
|
"icon": "icons8-cycling-48.png",
|
||||||
|
"tags": "outdoors,exercise,ble,bluetooth",
|
||||||
|
"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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
E.showMenu(menu);
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue