/* Speed and Altitude [speedalt] Mike Bennett mike[at]kereru.com 1.00 : Use new GPS settings module 1.01 : Third mode large clock display 1.02 : add smoothing with kalman filter */ //var v = '1.02g'; const BANGLEJS2 = process.env.HWVERSION==2; const screenH = g.getHeight(); const screenH_Half = screenH / 2; const screenH_Third = screenH / 3; const screenH_TwoThirds = screenH * 2 / 3; const screenW = g.getWidth(); const screenW_Half = screenW / 2; const fontFactorB2 = 2/3; /*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ var KalmanFilter = (function () { 'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } /** * KalmanFilter * @class * @author Wouter Bulten * @see {@link http://github.com/wouterbulten/kalmanjs} * @version Version: 1.0.0-beta * @copyright Copyright 2015-2018 Wouter Bulten * @license MIT License * @preserve */ var KalmanFilter = /*#__PURE__*/ function () { /** * Create 1-dimensional kalman filter * @param {Number} options.R Process noise * @param {Number} options.Q Measurement noise * @param {Number} options.A State vector * @param {Number} options.B Control vector * @param {Number} options.C Measurement vector * @return {KalmanFilter} */ function KalmanFilter() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$R = _ref.R, R = _ref$R === void 0 ? 1 : _ref$R, _ref$Q = _ref.Q, Q = _ref$Q === void 0 ? 1 : _ref$Q, _ref$A = _ref.A, A = _ref$A === void 0 ? 1 : _ref$A, _ref$B = _ref.B, B = _ref$B === void 0 ? 0 : _ref$B, _ref$C = _ref.C, C = _ref$C === void 0 ? 1 : _ref$C; _classCallCheck(this, KalmanFilter); this.R = R; // noise power desirable this.Q = Q; // noise power estimated this.A = A; this.C = C; this.B = B; this.cov = NaN; this.x = NaN; // estimated signal without noise } /** * Filter a new value * @param {Number} z Measurement * @param {Number} u Control * @return {Number} */ _createClass(KalmanFilter, [{ key: "filter", value: function filter(z) { var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if (isNaN(this.x)) { this.x = 1 / this.C * z; this.cov = 1 / this.C * this.Q * (1 / this.C); } else { // Compute prediction var predX = this.predict(u); var predCov = this.uncertainty(); // Kalman gain var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction this.x = predX + K * (z - this.C * predX); this.cov = predCov - K * this.C * predCov; } return this.x; } /** * Predict next value * @param {Number} [u] Control * @return {Number} */ }, { key: "predict", value: function predict() { var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; return this.A * this.x + this.B * u; } /** * Return uncertainty of filter * @return {Number} */ }, { key: "uncertainty", value: function uncertainty() { return this.A * this.cov * this.A + this.R; } /** * Return the last filtered measurement * @return {Number} */ }, { key: "lastMeasurement", value: function lastMeasurement() { return this.x; } /** * Set measurement noise Q * @param {Number} noise */ }, { key: "setMeasurementNoise", value: function setMeasurementNoise(noise) { this.Q = noise; } /** * Set the process noise R * @param {Number} noise */ }, { key: "setProcessNoise", value: function setProcessNoise(noise) { this.R = noise; } }]); return KalmanFilter; }(); return KalmanFilter; }()); var buf = Graphics.createArrayBuffer(screenW,screenH_TwoThirds,2,{msb:true}); if (!BANGLEJS2) require("Font7x11Numeric7Seg").add(Graphics); // Load fonts var lf = {fix:0,satellites:0}; var showMax = 0; // 1 = display the max values. 0 = display the cur fix var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on. var canDraw = 1; var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time. var tmrLP; // Timer for delay in switching to low power after screen turns off var max = {}; max.spd = 0; max.alt = 0; max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data. var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values; var wp = {}; // Waypoint to use for distance from cur position. //var SATinView = 0; function nxtWp(inc){ cfg.wp+=inc; loadWp(); } function loadWp() { var w = require("waypoints").load(); if (cfg.wp>=w.length) cfg.wp=0; if (cfg.wp<0) cfg.wp = w.length-1; savSettings(); wp = w[cfg.wp]; } function radians(a) { return a*Math.PI/180; } function distance(a,b){ var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); var y = radians(b.lat-a.lat); // Distance in selected units var d = Math.sqrt(x*x + y*y) * 6371000; d = (d/parseFloat(cfg.dist)).toFixed(2); if ( d >= 100 ) d = parseFloat(d).toFixed(1); if ( d >= 1000 ) d = parseFloat(d).toFixed(0); return d; } function drawFix(dat) { if (!canDraw) return; buf.clear(); var v = ''; var u=''; // Primary Display v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString(); // Primary Units u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units; drawPrimary(v,u); // Secondary Display v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString(); // Secondary Units u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit; drawSecondary(v,u); // Time drawTime(); // Waypoint name drawWP(); //Sats if ( dat.age > 10 ) { if ( dat.age > 90 ) dat.age = '>90'; drawSats('Age:'+dat.age); } else drawSats('Sats:'+dat.sats); /* else if (!BANGLEJS2) { drawSats('Sats:'+dat.sats); } else { if (lf.fix) { if(emulator)console.log("fix "+lf.fix); drawSats('Sats:'+dat.sats); } else { if(emulator)console.log("inView: "+SATinView); drawSats('View:' + SATinView); } } */ g.reset(); g.drawImage(img,0,40); } function drawClock() { if (!canDraw) return; buf.clear(); drawTime(); drawWP(); g.reset(); g.drawImage(img,0,40); } function drawPrimary(n,u) { if(emulator)console.log("drawPrimary: " + n +" "+ u); // Primary Display var s=40; // Font size var l=n.length; if ( l <= 7 ) s=48; if ( l <= 6 ) s=55; if ( l <= 5 ) s=66; if ( l <= 4 ) s=85; if ( l <= 3 ) s=110; buf.setFontAlign(0,-1); //Centre buf.setColor(1); if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); buf.drawString(n,screenW_Half-10,0); // Primary Units s = 35; // Font size buf.setFontAlign(1,-1,3); //right buf.setColor(2); if (BANGLEJS2) s = 20; buf.setFontVector(s); buf.drawString(u,screenW-30,0); } function drawSecondary(n,u) { if(emulator)console.log("drawSecondary: " + n +" "+ u); var xu = 180; // units X position var l=n.length; if ( l <= 5 ) xu = 155; if ( l <= 4 ) xu = 125; if ( l <= 3 ) xu = 100; if ( l <= 2 ) xu = 65; if ( l <= 1 ) xu = 35; buf.setFontAlign(-1,1); //left, bottom buf.setColor(1); var s = 45; // Font size if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); buf.drawString(n,5,screenH_TwoThirds-20); // Secondary Units buf.setFontAlign(-1,1); //left, bottom buf.setColor(2); s = 30; // Font size if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); buf.drawString(u,xu - (BANGLEJS2*xu/5),screenH_TwoThirds-25); } function drawTime() { var x, y; if ( cfg.modeA == 2 ) { x = screenW_Half; y = 0; buf.setFontAlign(0,-1); buf.setFontVector(screenH_Third); } else { x = 0; y = screenH_TwoThirds; buf.setFontAlign(-1,1); if (!BANGLEJS2) buf.setFont("7x11Numeric7Seg", 2); else buf.setFont("6x8", 2); } buf.setColor(0); buf.drawString(time,x,y); time = require("locale").time(new Date(),1); buf.setColor(3); buf.drawString(time,x,y); } function drawWP() { // from waypoints.json - see README.md var nm = wp.name; if ( nm == undefined || nm == 'NONE' || cfg.modeA ==1 ) nm = ''; if (emulator) nm="waypoint"; buf.setColor(2); var s = 20; // Font size if ( cfg.modeA == 0 ) { // dist mode if(emulator)console.log("drawWP() 0: "+nm); buf.setFontAlign(-1,1); //left, bottom if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 15)); } if ( cfg.modeA == 2 ) { // clock/large mode if(emulator)console.log("drawWP() 2: "+nm); s = 55; // Font size buf.setFontAlign(0,1); //left, bottom if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); buf.drawString(nm.substring(0,6),screenW_Half,screenH_TwoThirds-(BANGLEJS2 * 20)); } } function drawSats(sats) { buf.setColor(3); buf.setFont("6x8", 2); buf.setFontAlign(1,1); //right, bottom buf.drawString(sats,screenW,screenH_TwoThirds); s = 30; // Font size if (BANGLEJS2) s = 18; buf.setFontVector(s); buf.setColor(2); if ( cfg.modeA == 1 ) { buf.drawString('A',screenW,140-(BANGLEJS2 * 40)); if ( showMax ) { buf.setFontAlign(0,1); //centre, bottom buf.drawString('MAX',screenW_Half,screenH_TwoThirds + 4); } } if ( cfg.modeA == 0 ) buf.drawString('D',screenW,140-(BANGLEJS2 * 40)); } function onGPS(fix) { if ( emulator ) { fix.fix = 1; fix.speed = 10 + (Math.random()*5); fix.alt = 354 + (Math.random()*50); fix.lat = -38.92; fix.lon = 175.7613350; fix.course = 245; fix.satellites = 12; fix.time = new Date(); fix.smoothed = 0; } var m; var sp = '---'; var al = '---'; var di = '---'; var age = '---'; if (fix.fix) lf = fix; if (lf.fix) { // if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw); // Smooth data if ( lf.smoothed !== 1 ) { if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed); if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt); lf.smoothed = 1; if ( max.n <= 15 ) max.n++; } // Speed if ( cfg.spd == 0 ) { m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units sp = parseFloat(m[1]); cfg.spd_unit = m[2]; } else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units if ( sp < 10 ) sp = sp.toFixed(1); else sp = Math.round(sp); if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp); // Altitude al = lf.alt; al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al); // Distance to waypoint di = distance(lf,wp); if (isNaN(di)) di = 0; // Age of last fix (secs) age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); } if ( cfg.modeA == 1 ) { if ( showMax ) drawFix({ speed:max.spd, sats:lf.satellites, alt:max.alt, alt_units:cfg.alt_unit, age:age, fix:lf.fix }); // Speed and alt maximums else drawFix({ speed:sp, sats:lf.satellites, alt:al, alt_units:cfg.alt_unit, age:age, fix:lf.fix }); // Show speed/altitude } if ( cfg.modeA == 0 ) { // Show speed/distance if ( di <= 0 ) drawFix({ speed:sp, sats:lf.satellites, alt:'', alt_units:'', age:age, fix:lf.fix }); // No WP selected else drawFix({ speed:sp, sats:lf.satellites, alt:di, alt_units:cfg.dist_unit, age:age, fix:lf.fix }); } if ( cfg.modeA == 2 ) { // Large clock drawClock(); } } function btn1press(longpress) { if(emulator) console.log("Btn1, long="+longpress); if ( cfg.modeA == 1 ) { // Spd+Alt mode - Switch between fix and MAX if ( !longpress ) showMax = !showMax; // Short press toggle fix/max display else { max.spd = 0; max.alt = 0; } // Long press resets max values. } else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint onGPS(lf); } function btn2press(){ if(emulator) console.log("Btn2"); pwrSav=!pwrSav; if ( pwrSav ) { LED1.reset(); var s = require('Storage').readJSON('setting.json',1)||{}; var t = s.timeout||10; Bangle.setLCDTimeout(t); } else { Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); LED1.set(); } } function btn3press(){ if(emulator) console.log("Btn3"); cfg.modeA = cfg.modeA+1; if ( cfg.modeA > 2 ) cfg.modeA = 0; if(emulator)console.log("cfg.modeA="+cfg.modeA); savSettings(); onGPS(lf); } function btn4press(){ if(emulator) console.log("Btn4"); cfg.primSpd = !cfg.primSpd; savSettings(); onGPS(lf); // Update display } function setButtons(){ if (!BANGLEJS2) { // Buttons for Bangle.js 1 setWatch(function(e) { btn1press(( e.time - e.lastTime) > 2); // > 2 sec. is long press }, BTN1, { edge:"falling",repeat:true}); // Power saving on/off (red dot visible if off) setWatch(btn2press, BTN2, {repeat:true,edge:"falling"}); // Toggle between alt or dist setWatch(btn3press, BTN3, {repeat:true,edge:"falling"}); // Touch left screen to toggle display setWatch(btn4press, BTN4, {repeat:true,edge:"falling"}); } else { // Buttons for Bangle.js 2 setWatch(function(e) { btn1press(( e.time - e.lastTime) > 0.4); // > 0.4 sec. is long press }, BTN1, { edge:"falling",repeat:true}); Bangle.on('touch', function(btn_l_r, e) { if(e.x < screenW_Half) btn4press(); else if (e.y < screenH_Half) btn2press(); else btn3press(); }); } } function updateClock() { if (!canDraw) return; drawTime(); g.reset(); g.drawImage(img,0,40); if ( emulator ) {max.spd++;max.alt++;} } function startDraw(){ canDraw=true; setLpMode('SuperE'); // off g.clear(); Bangle.drawWidgets(); onGPS(lf); // draw app screen } function stopDraw() { canDraw=false; if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix. } function savSettings() { require("Storage").write('speedalt.json',cfg); } function setLpMode(m) { if (tmrLP) {clearInterval(tmrLP);tmrLP = false;} // Stop any scheduled drop to low power if ( !gpssetup ) return; gpssetup.setPowerMode({power_mode:m}); } // =Main Prog // Read settings. let cfg = require('Storage').readJSON('speedalt.json',1)||{}; cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed cfg.spd_unit = cfg.spd_unit||'km/h'; // Displayed speed unit cfg.alt = cfg.alt||1;// Multiplier for altitude unit conversions. (feet:'0.3048') cfg.alt_unit = cfg.alt_unit||'meter'; // Displayed altitude units ('feet') cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions. cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units cfg.colour = cfg.colour||0; // Colour scheme. cfg.wp = cfg.wp||0; // Last selected waypoint for dist cfg.modeA = cfg.modeA||1; // 0 = [D]ist, 1 = [A]ltitude, 2 = [C]lock cfg.primSpd = cfg.primSpd||1; // 1 = Spd in primary, 0 = Spd in secondary cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt; if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 }); if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 }); loadWp(); /* Colour Pallet Idx 0 : Background (black) 1 : Speed/Alt 2 : Units 3 : Sats */ const background = 0; // g.theme.bg = 0xFFFF = gelb!? var img = { width:buf.getWidth(), height:buf.getHeight(), bpp:2, buffer:buf.buffer, palette:new Uint16Array([background,0x4FE0,0xEFE0,0x07DB]) // "Default" }; if ( cfg.colour == 1 ) img.palette = new Uint16Array([background,0xFFFF,0xFFF6,0xDFFF]); // "Hi contrast" if ( cfg.colour == 2 ) img.palette = new Uint16Array([background,0xFF800,0xFAE0,0xF813]); // "Night" var SCREENACCESS = { withApp:true, request:function(){this.withApp=false;stopDraw();}, release:function(){this.withApp=true;startDraw();} }; Bangle.on('lcdPower',function(on) { if (!SCREENACCESS.withApp) return; if (on) startDraw(); else stopDraw(); }); var gpssetup; try { gpssetup = require("gpssetup"); } catch(e) { gpssetup = false; } // All set up. Lets go. g.clear(); onGPS(lf); Bangle.setGPSPower(1); if ( gpssetup ) { gpssetup.setPowerMode({power_mode:"SuperE"}).then(function() { Bangle.setGPSPower(1); }); } else { Bangle.setGPSPower(1); } Bangle.on('GPS', onGPS); setButtons(); setInterval(updateClock, 10000); Bangle.loadWidgets(); Bangle.drawWidgets();