mirror of https://github.com/espruino/BangleApps
454 lines
13 KiB
JavaScript
454 lines
13 KiB
JavaScript
const Layout = require('Layout');
|
|
const storage = require('Storage');
|
|
|
|
const SETTINGS_FILE = 'cycling.json';
|
|
const SETTINGS_DEFAULT = {
|
|
sensors: {},
|
|
metric: true,
|
|
};
|
|
|
|
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, true) || SETTINGS_DEFAULT;
|
|
this.wheelCirc = undefined;
|
|
|
|
// 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.deviceAddress = undefined;
|
|
this.display.useMetricUnits((this.settings.metric));
|
|
}
|
|
|
|
onDisconnect(event) {
|
|
console.log("disconnected ", event);
|
|
|
|
this.connected = false;
|
|
this.wheelCirc = undefined;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
loadCircumference() {
|
|
if (!this.deviceAddress) return;
|
|
|
|
// Add sensor to settings if not present
|
|
if (!this.settings.sensors[this.deviceAddress]) {
|
|
this.settings.sensors[this.deviceAddress] = {
|
|
cm: 223,
|
|
mm: 0,
|
|
};
|
|
storage.writeJSON(SETTINGS_FILE, this.settings);
|
|
}
|
|
|
|
const high = this.settings.sensors[this.deviceAddress].cm || 223;
|
|
const low = this.settings.sensors[this.deviceAddress].mm || 0;
|
|
this.wheelCirc = (10*high + low) / 1000;
|
|
}
|
|
|
|
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;
|
|
this.deviceAddress = this.blecsc.getDeviceAddress();
|
|
console.log("Connected to " + this.deviceAddress);
|
|
|
|
this.display.setDeviceAddress(this.deviceAddress);
|
|
this.display.setStatus("Connected");
|
|
|
|
this.loadCircumference();
|
|
|
|
// 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.reset();
|
|
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;
|
|
this.wheelCirc = undefined;
|
|
}
|
|
|
|
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.fontSmall = "15%";
|
|
this.fontMed = "18%";
|
|
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: 36,
|
|
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: 36,
|
|
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.fontSmall, 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.fontSmall, 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",
|
|
bgCol: "#fff",
|
|
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: "#fff",
|
|
height: 32,
|
|
c: [
|
|
{type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36},
|
|
{type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118},
|
|
{type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", 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.fontSmall, 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: "ADDR", 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();
|
|
Bangle.drawWidgets();
|
|
}
|
|
|
|
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) {
|
|
distance = this.convertDistance(distance);
|
|
if (distance >= 1000) {
|
|
this.layouts.distance.totald.label = String(Math.round(distance));
|
|
} else {
|
|
this.layouts.distance.totald.label = 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);
|
|
});
|
|
|
|
Bangle.loadWidgets();
|
|
sensor.connect();
|