mirror of https://github.com/espruino/BangleApps
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