BangleApps/apps/speedalt2/app.js

756 lines
18 KiB
JavaScript

/*
Speed and Altitude [speedalt2]
Mike Bennett mike[at]kereru.com
1.10 : add inverted colours
1.14 : Add VMG screen
*/
var v = '1.15';
/*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(240,160,2,{msb:true});
let LED = // LED as minimal and only definition (as instance / singleton)
{ isOn: false // status on / off, not needed if you don't need to ask for it
, set: function(v) { // turn on w/ no arg or truey, else off
g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(40,10,10); }
, reset: function() { this.set(false); } // turn off
, write: function(v) { this.set(v); } // turn on w/ no arg or truey, else off
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
}, LED1 = LED; // LED1 as 'synonym' for LED
// Load fonts
//require("Font7x11Numeric7Seg").add(Graphics);
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 tmrLP; // Timer for delay in switching to low power after screen turns off
var maxSpd = 0;
var maxAlt = 0;
var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
// Previous values for calculating VMG.
var lastDist = -1;
var lastTime = -1;
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
var wp = {}; // Waypoint to use for distance from cur position.
function nxtWp(){
cfg.wp++;
loadWp();
lastDist = -1; // Reset VMG calcs
lastTime = -1;
}
function loadWp() {
var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}];
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 metres
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 drawScrn(dat) {
if (!canDraw) return;
buf.clear();
buf.setBgColor(0);
var n;
n = dat.val.toString();
var s=50; // Font size
var l=n.length;
if ( l <= 7 ) s=55;
if ( l <= 6 ) s=60;
if ( l <= 5 ) s=80;
if ( l <= 4 ) s=100;
if ( l <= 3 ) s=120;
buf.setFontAlign(0,0); //Centre
buf.setColor(1);
buf.setFontVector(s);
buf.drawString(n,126,52);
// Primary Units
buf.setFontAlign(-1,1); //left, bottom
buf.setColor(2);
buf.setFontVector(35);
buf.drawString(dat.unit,5,164);
drawMax(dat.max); // MAX display indicator
drawWP(dat.wp); // Waypoint name
drawSats(dat.sats);
g.reset();
g.drawImage(img,0,40);
LED1.write(!pwrSav);
}
function drawPosn(dat) {
if (!canDraw) return;
buf.clear();
buf.setBgColor(0);
var x, y;
x=210;
y=0;
buf.setFontAlign(1,-1);
buf.setFontVector(60);
buf.setColor(1);
buf.drawString(dat.lat,x,y);
buf.drawString(dat.lon,x,y+70);
x = 240;
buf.setColor(2);
buf.setFontVector(40);
buf.drawString(dat.ns,x,y);
buf.drawString(dat.ew,x,y+70);
drawSats(dat.sats);
g.reset();
g.drawImage(img,0,40);
LED1.write(!pwrSav);
}
function drawClock() {
if (!canDraw) return;
buf.clear();
buf.setBgColor(0);
var x, y;
x=185;
y=0;
buf.setFontAlign(1,-1);
buf.setFontVector(94);
time = require("locale").time(new Date(),1);
buf.setColor(1);
buf.drawString(time.substring(0,2),x,y);
buf.drawString(time.substring(3,5),x,y+80);
g.reset();
g.drawImage(img,0,40);
LED1.write(!pwrSav);
}
function drawWP(wp) {
buf.setColor(3);
buf.setFontAlign(0,1); //left, bottom
buf.setFontVector(40);
buf.drawString(wp,120,132);
}
function drawSats(sats) {
buf.setColor(3);
buf.setFont("6x8", 2);
buf.setFontAlign(1,1); //right, bottom
buf.drawString(sats,240,160);
}
function drawMax(max) {
buf.setFontVector(30);
buf.setColor(2);
buf.setFontAlign(0,1); //centre, bottom
buf.drawString(max,120,164);
}
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 = '---';
var lat = '---.--';
var ns = '';
var ew = '';
var lon = '---.--';
var sats = '---';
var vmg = '---';
// Waypoint name
var wpName = wp.name;
if ( wpName == undefined || wpName == 'NONE' ) wpName = '';
wpName = wpName.substring(0,8);
if (fix.fix) lf = fix;
if (lf.fix) {
// 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 ( maxN <= 15 ) maxN++;
}
// Speed
sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
if ( sp < 10 ) sp = sp.toFixed(1);
else sp = Math.round(sp);
if (isNaN(sp)) sp = '---';
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;
// Altitude
al = lf.alt;
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
if (parseFloat(al) > parseFloat(maxAlt) && maxN > 15 ) maxAlt = al;
if (isNaN(al)) al = '---';
// Distance to waypoint and vmg
di = distance(lf,wp);
//lastDist = 13640;
//lastTime = (getTime()/1000) - 10;
if ( lastDist != -1 && ! isNaN(lastDist)) {
//console.log(' Distance : '+di);
//console.log('last.Distance : '+lastDist);
//console.log('last.Time : '+lastTime);
// Have two WP distances and a time. Calc speed
vmg = ((lastDist-di)/1000)/((getTime()/1000-lastTime)/3600); // k/h
vmg = vmg/parseFloat(cfg.spd); // Calculate for selected units
//console.log('VMG : '+vmg);
}
lastDist = di;
lastTime = getTime()/1000; // secs
di = (di/parseFloat(cfg.dist)).toFixed(2);
if ( di >= 100 ) di = parseFloat(di).toFixed(1);
if ( di >= 1000 ) di = parseFloat(di).toFixed(0);
if ( Math.abs(vmg) < 10 ) vmg = vmg.toFixed(1);
else vmg = Math.round(vmg);
if (isNaN(vmg)) vmg = '---';
if (isNaN(di)) di = '------';
// Age of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
// Lat / Lon
ns = 'N';
if ( lf.lat < 0 ) ns = 'S';
lat = Math.abs(lf.lat.toFixed(2));
ew = 'E';
if ( lf.lon < 0 ) ew = 'W';
lon = Math.abs(lf.lon.toFixed(2));
// Sats
if ( age > 10 ) {
sats = 'Age:'+Math.round(age);
if ( age > 90 ) sats = 'Age:>90';
}
else sats = 'Sats:'+lf.satellites;
}
if ( cfg.modeA == 0 ) {
// Speed
if ( showMax )
drawScrn({
val:maxSpd,
unit:cfg.spd_unit,
sats:sats,
age:age,
max:'MAX',
wp:''
}); // Speed maximums
else
drawScrn({
val:sp,
unit:cfg.spd_unit,
sats:sats,
age:age,
max:'SPD',
wp:''
});
}
if ( cfg.modeA == 1 ) {
// Alt
if ( showMax )
drawScrn({
val:maxAlt,
unit:cfg.alt_unit,
sats:sats,
age:age,
max:'MAX',
wp:''
}); // Alt maximums
else
drawScrn({
val:al,
unit:cfg.alt_unit,
sats:sats,
age:age,
max:'ALT',
wp:''
});
}
if ( cfg.modeA == 2 ) {
// Dist
drawScrn({
val:di,
unit:cfg.dist_unit,
sats:sats,
age:age,
max:'DST',
wp:wpName
});
}
if ( cfg.modeA == 3 ) {
// VMG
drawScrn({
val:vmg,
unit:cfg.spd_unit,
sats:sats,
age:age,
max:'VMG',
wp:wpName
});
}
if ( cfg.modeA == 4 ) {
// Position
drawPosn({
sats:sats,
age:age,
lat:lat,
lon:lon,
ns:ns,
ew:ew
});
}
if ( cfg.modeA == 5 ) {
// Large clock
drawClock();
}
}
function prevScrn() {
cfg.modeA = cfg.modeA-1;
if ( cfg.modeA < 0 ) cfg.modeA = 5;
savSettings();
onGPS(lf);
}
function nextScrn() {
cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 5 ) cfg.modeA = 0;
savSettings();
onGPS(lf);
}
// Next function on a screen
function nextFunc(dur) {
if ( cfg.modeA == 0 || cfg.modeA == 1 ) {
// Spd+Alt mode - Switch between fix and MAX
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values.
}
else if ( cfg.modeA == 2 || cfg.modeA == 3) nxtWp(); // Dist or VMG mode - Select next waypoint
onGPS(lf);
}
function updateClock() {
if (!canDraw) return;
if ( cfg.modeA != 5 ) return;
drawClock();
if ( emulator ) {maxSpd++;maxAlt++;}
}
function startDraw(){
canDraw=true;
g.clear();
Bangle.drawWidgets();
setLpMode('SuperE'); // off
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('speedalt2.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});
}
// == Events
function setButtons(){
// BTN1 - Max speed/alt or next waypoint
setWatch(function(e) {
var dur = e.time - e.lastTime;
nextFunc(dur);
}, BTN1, { edge:"falling",repeat:true});
// Power saving on/off
setWatch(function(e){
pwrSav=!pwrSav;
if ( pwrSav ) {
var s = require('Storage').readJSON('setting.json',1)||{};
var t = s.timeout||10;
Bangle.setLCDTimeout(t);
}
else {
Bangle.setLCDTimeout(0);
// Bangle.setLCDPower(1);
}
LED1.write(!pwrSav);
}, BTN2, {repeat:true,edge:"falling"});
// BTN3 - next screen
setWatch(function(e){
nextScrn();
}, BTN3, {repeat:true,edge:"falling"});
/*
// Touch screen same as BTN1 short
setWatch(function(e){
nextFunc(1); // Same as BTN1 short
}, BTN4, {repeat:true,edge:"falling"});
setWatch(function(e){
nextFunc(1); // Same as BTN1 short
}, BTN5, {repeat:true,edge:"falling"});
*/
}
Bangle.on('lcdPower',function(on) {
if (!SCREENACCESS.withApp) return;
if (on) startDraw();
else stopDraw();
});
Bangle.on('swipe',function(dir) {
if ( ! cfg.touch ) return;
if(dir == 1) prevScrn();
else nextScrn();
});
Bangle.on('touch', function(button){
if ( ! cfg.touch ) return;
nextFunc(0); // Same function as short BTN1
/*
switch(button){
case 1: // BTN4
console.log('BTN4');
prevScrn();
break;
case 2: // BTN5
console.log('BTN5');
nextScrn();
break;
case 3:
console.log('MDL');
nextFunc(0); // Centre - same function as short BTN1
break;
}
*/
});
// == Main Prog
// Read settings.
let cfg = require('Storage').readJSON('speedalt2.json',1)||{};
cfg.spd = cfg.spd||1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = cfg.spd_unit||'kph'; // Displayed speed unit
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
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||0; // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock
cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt;
cfg.touch = cfg.touch==undefined?true:cfg.touch;
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
*/
var img = {
width:buf.getWidth(),
height:buf.getHeight(),
bpp:2,
buffer:buf.buffer,
palette:new Uint16Array([0,0x4FE0,0xEFE0,0x07DB])
};
if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]);
if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xF800,0xFAE0,0xF813]);
if ( cfg.colour == 3 ) img.palette = new Uint16Array([0xFFFF,0x007F,0x0054,0x0054]);
var SCREENACCESS = {
withApp:true,
request:function(){this.withApp=false;stopDraw();},
release:function(){this.withApp=true;startDraw();}
};
var gpssetup;
try {
gpssetup = require("gpssetup");
} catch(e) {
gpssetup = false;
}
// All set up. Lets go.
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
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);