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();