diff --git a/apps.json b/apps.json index 44a6c07f6..e49d8b8e8 100644 --- a/apps.json +++ b/apps.json @@ -3013,5 +3013,23 @@ "evaluate": true } ] -} +}, +{ "id": "kitchen", + "name": "Kitchen Combo", + "icon": "kitchen.png", + "version":"0.01", + "description": "Combination of the stepo, walkersclock, arrow and waypointer apps into a multiclock format.", + "tags": "tool,outdoors,gps", + "readme": "README.md", + "interface":"waypoints.html", + "storage": [ + {"name":"kitchen.app.js","url":"kitchen.app.js"}, + {"name":"stepo.kit.js","url":"stepo.kit.js"}, + {"name":"gps.kit.js","url":"gps.kit.js"}, + {"name":"digi.kit.js","url":"digi.kit.js"}, + {"name":"compass.kit.js","url":"compass.kit.js"}, + {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, + {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} + ] +} ] diff --git a/apps/kitchen/compass.kit.js b/apps/kitchen/compass.kit.js new file mode 100644 index 000000000..b9272291d --- /dev/null +++ b/apps/kitchen/compass.kit.js @@ -0,0 +1,306 @@ +(() => { + function getFace(){ + var intervalRefSec; + var pal_by; + var pal_bw; + var pal_bb; + var buf1; + var buf2; + var bearing; + var heading; + var oldHeading; + var CALIBDATA; + var previous; + var wp; + var wp_distance; + var wp_bearing; + var loc; + var gpsObject; + + function log_debug(o) { + //console.log(o); + } + + function init(gps) { + showMem("compass init() START"); + gpsObject = gps; + pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow + pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white + pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue + buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true}); + buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true}); + + intervalRefSec = undefined; + bearing = 0; // always point north if GPS is off + heading = 0; + oldHeading = 0; + previous = {bs:"-", dst:"-", wp_name:"-", course:999}; + loc = require("locale"); + CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; + getWaypoint(); + + /* + * compass should be powered on before startDraw is called + * otherwise compass power widget will not come on + */ + if (!Bangle.isCompassOn()) Bangle.setCompassPower(1); + gps.determineGPSState(); + + showMem("compass init() END"); + } + + function freeResources() { + showMem("compass freeResources() START"); + gpsObject = undefined; + pal_by = undefined; + pal_bw = undefined; + pal_bb = undefined; + buf1 = undefined; + buf2 = undefined; + intervalRefSec = undefined; + previous = undefined; + + bearing = 0; + heading = 0; + oldHeading = 0; + loc = undefined; + CALIBDATA = undefined; + wp = undefined; + if (Bangle.isCompassOn()) Bangle.setCompassPower(0); + showMem("compass freeResources() END"); + } + + function flip1(x,y) { + g.drawImage({width:128,height:128,bpp:1,buffer:buf1.buffer, palette:pal_by},x ,y); + buf1.clear(); + } + + function flip2_bw(x,y) { + g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x ,y); + buf2.clear(); + } + + function flip2_bb(x,y) { + g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x ,y); + buf2.clear(); + } + + function startTimer() { + log_debug("startTimer()"); + if (!Bangle.isCompassOn()) Bangle.setCompassPower(1); + resetPrevious(); + draw(); + intervalRefSec = setInterval(draw, 500); + } + + function stopTimer() { + log_debug("stopTimer()"); + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + if (Bangle.isCompassOn()) Bangle.setCompassPower(0); + } + + function showMem(msg) { + var val = process.memory(); + var str = msg + " " + Math.round(val.usage*100/val.total) + "%"; + log_debug(str); + } + + function onButtonShort(btn) { + switch(btn) { + case 1: + log_debug("prev waypoint"); + gpsObject.nextWaypoint(-1); + break; + case 2: + log_debug("next waypoint"); + gpsObject.nextWaypoint(1); + break; + case 3: + default: + break; + } + resetPrevious(); + getWaypoint(); + drawGPSData(); + } + + function onButtonLong(btn) { + log_debug("markWaypoint()"); + if (btn !== 1) return; + if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; + + log_debug("markWaypoint()"); + + gpsObject.markWaypoint(); + resetPrevious(); + getWaypoint(); + drawGPSData(); + } + + function getWaypoint() { + log_debug("getWaypoint()"); + wp = gpsObject.getCurrentWaypoint(); + wp_distance = gpsObject.getWPdistance(); + wp_bearing = gpsObject.getWPbearing(); + log_debug(wp); + log_debug(wp_distance); + log_debug(wp_bearing); + } + + // takes 32ms + function drawCompass(hd) { + if (Math.abs(hd - oldHeading) < 2) return 0; + hd=hd*Math.PI/180; + var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760]; + + var poly = [ + 64+60*Math.sin(hd+p[0]), 64-60*Math.cos(hd+p[0]), + 64+44.7214*Math.sin(hd+p[1]), 64-44.7214*Math.cos(hd+p[1]), + 64+28.2843*Math.sin(hd+p[2]), 64-28.2843*Math.cos(hd+p[2]), + 64+63.2455*Math.sin(hd+p[3]), 64-63.2455*Math.cos(hd+p[3]), + 64+63.2455*Math.sin(hd+p[4]), 64-63.2455*Math.cos(hd+p[4]), + 64+28.2843*Math.sin(hd+p[5]), 64-28.2843*Math.cos(hd+p[5]), + 64+44.7214*Math.sin(hd+p[6]), 64-44.7214*Math.cos(hd+p[6]) + ]; + + buf1.fillPoly(poly); + flip1(56, 56); + } + + // stops violent compass swings and wobbles, takes 3ms + function newHeading(m,h){ + //log_debug("newHeading()"); + var s = Math.abs(m - h); + var delta = (m>h)?1:-1; + if (s>=180){s=360-s; delta = -delta;} + if (s<2) return h; + if (s<3) return h; + var hd = h + delta*(1 + Math.round(s/5)); + if (hd<0) hd+=360; + if (hd>360)hd-= 360; + return hd; + } + + // takes approx 7ms + function tiltfixread(O,S){ + //log_debug("tiltfixread()"); + var m = Bangle.getCompass(); + var g = Bangle.getAccel(); + m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z; + var d = Math.atan2(-m.dx,m.dy)*180/Math.PI; + if (d<0) d+=360; + var phi = Math.atan(-g.x/-g.z); + var cosphi = Math.cos(phi), sinphi = Math.sin(phi); + var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi)); + var costheta = Math.cos(theta), sintheta = Math.sin(theta); + var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta; + var yh = m.dz*sinphi - m.dx*cosphi; + var psi = Math.atan2(yh,xh)*180/Math.PI; + if (psi<0) psi+=360; + return psi; + } + + function draw() { + //log_debug("draw()"); + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + heading = newHeading(d,heading); + + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { + wp_dist = gpsObject.getWPdistance(); + wp_bearing = gpsObject.getWPbearing(); + bearing = wp_bearing; + } else { + bearing = 0; + wp_distance = 0; + wp_bearing = 0; + } + + var dir = bearing - heading; + if (dir < 0) dir += 360; + if (dir > 360) dir -= 360; + var t = drawCompass(dir); // we want compass to show us where to go + oldHeading = dir; + + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { + drawGPSData(); + } else { + drawCompassHeading(); + } + } + + function drawCompassHeading() { + //log_debug("drawCompassHeading()"); + // draw the heading + buf2.setColor(1); + buf2.setFontAlign(-1,-1); + buf2.setFont("Vector",38); + var hding = Math.round(heading); + var hd = hding.toString(); + hd = hding < 10 ? "00"+hd : hding < 100 ? "0"+hd : hd; + buf2.drawString(hd,0,0); + flip2_bw(90, 200); + } + + function drawGPSData() { + log_debug("drawGPSData()"); + buf2.setFont("Vector",24); + var bs = wp_bearing.toString(); + bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs; + var dst = loc.distance(wp_distance); + + log_debug(bs); + log_debug(dst); + + // -1=left (default), 0=center, 1=right + + // show distance on the left + if (previous.dst !== dst) { + previous.dst = dst + buf2.setColor(1); + buf2.setFontAlign(-1,-1); + buf2.setFont("Vector", 20); + if (gpsObject.waypointHasLocation()) { + buf2.drawString(dst,0,0); + flip2_bb(0, 200); + } else { + buf2.drawString(" ",0,0); + flip2_bw(0, 200); + } + } + + // bearing, place in middle at bottom of compass + if (previous.bs !== bs) { + previous.bs = bs; + buf2.setColor(1); + buf2.setFontAlign(0, -1); + buf2.setFont("Vector",38); + buf2.drawString(bs,40,0); + flip2_bw(80, 200); + } + + // waypoint name on right + if (previous.wp_name !== wp.name) { + buf2.setColor(1); + buf2.setFontAlign(1,-1); // right, bottom + buf2.setFont("Vector", 20); + buf2.drawString(wp.name, 80, 0); + if (gpsObject.waypointHasLocation()) + flip2_bb(160, 200); + else + flip2_bw(160, 200); + } + } + + // clear the attributes that control the display refresh + function resetPrevious() { + log_debug("resetPrevious()"); + previous = {bs:"-", dst:"-", wp_name:"-", course:999}; + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; + +})(); diff --git a/apps/kitchen/digi.kit.js b/apps/kitchen/digi.kit.js new file mode 100644 index 000000000..1a64882a7 --- /dev/null +++ b/apps/kitchen/digi.kit.js @@ -0,0 +1,146 @@ +(() => { + function getFace(){ + var intervalRefSec; + var buf; + var days; + var prevInfo; + var prevDate; + var prevTime; + var infoMode; + + const INFO_NONE = 0; + const INFO_BATT = 1; + const INFO_MEM = 2; + const Y_TIME = 30; + const Y_ACTIVITY = 116; + const Y_MODELINE = 200; + + function init(gps) { + showMem("digi init 1"); + days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday","Friday", "Saturday"]; + prevInfo = ""; + prevTimeStr = ""; + prevDateStr = ""; + infoMode = INFO_NONE; + g.clear(); + showMem("digi init 2"); + } + + function freeResources() { + showMem("digi free 1"); + days = undefined; + prevInfo = undefined; + prevTime = undefined; + prevDate = undefined; + showMem("digi free 2"); + } + + function showMem(msg) { + var val = process.memory(); + var str = msg + " " + Math.round(val.usage*100/val.total) + "%"; + //console.log(str); + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function onButtonShort(btn) { + if (btn === 1) cycleInfoMode(); + } + + function onButtonLong(btn) {} + function getGPSfix() { return undefined; } + function setGPSfix(f) {} + + function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + if (time !== prevTime) { + prevTime = time; + g.setColor(0); + g.fillRect(0, Y_TIME, 239, Y_ACTIVITY -1); + g.setColor(1,1,1); + g.setFont("Vector",80); + g.setFontAlign(0,-1); + g.drawString(time, 120, Y_TIME); + } + + var day = days[d.getDay()]; + var dateStr = da[2] + " " + da[1] + " " + da[3]; + + if (dateStr !== prevDate) { + prevDate = dateStr; + g.setColor(0); + g.fillRect(0, Y_ACTIVITY, 239, Y_MODELINE - 3); + g.setColor(1,1,1); + g.setFont("Vector",26); + g.drawString(day, 120, Y_ACTIVITY); + g.drawString(dateStr, 120, Y_ACTIVITY + 40); + } + + drawInfo(); + } + + function cycleInfoMode() { + switch(infoMode) { + case INFO_NONE: + infoMode = INFO_BATT; + break; + case INFO_BATT: + infoMode = INFO_MEM + break; + case INFO_MEM: + default: + infoMode = INFO_NONE; + break; + } + drawInfo(); + } + + function drawInfo() { + let val; + let str = ""; + let col = 0x07FF; // cyan + + switch(infoMode) { + case INFO_NONE: + col = 0x0000; + str = ""; + break; + case INFO_MEM: + val = process.memory(); + str = "Memory: " + Math.round(val.usage*100/val.total) + "%"; + break; + case INFO_BATT: + default: + str = "Battery: " + E.getBattery() + "%"; + } + + // check if we need to draw, avoid flicker + if (str == prevInfo) + return; + + prevInfo = str; + //g.setFont("6x8", 3); + g.setFont("Vector",26); + g.setColor(col); + g.fillRect(0, Y_MODELINE - 3, 239, Y_MODELINE + 25); + g.setColor(0,0,0); + g.setFontAlign(0, -1); + g.drawString(str, 120, Y_MODELINE); + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; +})(); diff --git a/apps/kitchen/gps.kit.js b/apps/kitchen/gps.kit.js new file mode 100644 index 000000000..ad2dd0ee4 --- /dev/null +++ b/apps/kitchen/gps.kit.js @@ -0,0 +1,182 @@ +(() => { + function getFace(){ + var intervalRefSec; + + const GDISP_OS = 4; + const GDISP_LATLN = 5; + const GDISP_SPEED = 6; + const GDISP_ALT = 7; + const GDISP_COURSE = 8; + + const Y_TIME = 30; + const Y_ACTIVITY = 120; + const Y_MODELINE = 200; + + let gpsDisplay = GDISP_OS; + let clearActivityArea = true; + let gpsObject = undefined; + + function log_debug(o) { + //console.log(o); + } + + function init(gps) { + log_debug("gps init"); + //log_debug(gps); + gpsObject = gps; + gpsDisplay = GDISP_OS; + clearActivityArea = true; + gpsObject.determineGPSState(); + } + + function freeResources() {} + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function onButtonShort(btn) { + if (btn === 1) cycleGPSDisplay(); + } + + function onButtonLong(btn) { + if (btn === 1) toggleGPSPower(); + } + + function draw(){ + drawGPSTime(); + drawGPSData(); + } + + function drawGPSTime() { + var time = gpsObject.getGPSTime(); + + g.reset(); + g.clearRect(0,Y_TIME, 239, Y_ACTIVITY - 1); + g.setColor(1,1,1); + g.setFontAlign(0, -1); + + if (time.length > 5) + g.setFont("Vector", 56); + else + g.setFont("Vector", 80); + + g.drawString(time, 120, Y_TIME); + } + + function drawGPSData() { + if (clearActivityArea) { + g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); + clearActivityArea = false; + } + + g.setFontVector(26); + g.setColor(0xFFC0); + g.setFontAlign(0, -1); + + if (gpsObject.getState() === gpsObject.GPS_OFF) { + g.drawString("GPS off", 120, Y_ACTIVITY); + return; + } + + if (gpsObject.getState() === gpsObject.GPS_TIME) { + g.drawString("Waiting for", 120, Y_ACTIVITY); + g.drawString("GPS", 120, Y_ACTIVITY + 36); + return; + } + + let fx = gpsObject.getLastFix(); + + log_debug("gpsObject.getState()= " + gpsObject.getState()); + + if (gpsObject.getState() === gpsObject.GPS_SATS) { + g.drawString("Satellites", 120, Y_ACTIVITY); + g.drawString(fx.satellites, 120, Y_ACTIVITY + 36); + return; + } + + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { + let time = gpsObject.formatTime(fx.time); + let age = gpsObject.timeSince(time); + let os = gpsObject.getOsRef(); + //let ref = to_map_ref(6, os.easting, os.northing); + let speed; + let activityStr = ""; + + if (age < 0) age = 0; + g.setFontVector(40); + g.setColor(0xFFC0); + + switch(gpsDisplay) { + case GDISP_OS: + activityStr = os; + break; + case GDISP_LATLN: + g.setFontVector(26); + activityStr = fx.lat.toFixed(4) + ", " + fx.lon.toFixed(4); + break; + case GDISP_SPEED: + speed = fx.speed; + speed = speed.toFixed(1); + activityStr = speed + "kph"; + break; + case GDISP_ALT: + activityStr = fx.alt + "m"; + break; + case GDISP_COURSE: + activityStr = fx.course; + break; + } + + g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); + g.drawString(activityStr, 120, Y_ACTIVITY); + g.setFont("6x8",2); + g.setColor(1,1,1); + g.drawString(age, 120, Y_ACTIVITY + 46); + } + } + + function toggleGPSPower() { + gpsObject.toggleGPSPower(); + clearActivityArea = true; + draw(); + } + + function cycleGPSDisplay() { + if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; + + switch (gpsDisplay) { + case GDISP_OS: + gpsDisplay = GDISP_SPEED; + break; + case GDISP_SPEED: + gpsDisplay = GDISP_ALT; + break; + case GDISP_ALT: + gpsDisplay = GDISP_LATLN; + break; + case GDISP_LATLN: + gpsDisplay = GDISP_COURSE; + break; + case GDISP_COURSE: + default: + gpsDisplay = GDISP_OS; + break; + } + + clearActivityArea = true; + drawGPSData(); + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; + +})(); diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js new file mode 100644 index 000000000..8e2a5c560 --- /dev/null +++ b/apps/kitchen/kitchen.app.js @@ -0,0 +1,482 @@ +// read in the faces +var FACES = []; +var STOR = require("Storage"); +STOR.list(/\.kit\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); +var iface = STOR.list(/\.kit\.js$/).indexOf("stepo.kit.js"); +var face = FACES[iface](); +var firstPress +var pressTimer; + +function stopdraw() { + face.stopTimer(); +} + +function startdraw() { + Bangle.drawWidgets(); + face.startTimer(); +} + +function nextFace(){ + stopdraw(); + face.freeResources(); + + iface += 1 + iface = iface % FACES.length; + face = FACES[iface](); + + g.clear(); + g.reset(); + face.init(gpsObj); + startdraw(); +} + +// when you feel the buzzer you know you have done a long press +function longPressCheck() { + Bangle.buzz(); + if (pressTimer) { + clearInterval(pressTimer); + pressTimer = undefined; + } +} + +// start a timer and buzz when held long enough +function buttonPressed(btn) { + if (btn === 3) { + nextFace(); + } else { + firstPress = getTime(); + pressTimer = setInterval(longPressCheck, 1500); + } +} + +// if you release too soon there is no buzz as timer is cleared +function buttonReleased(btn) { + var dur = getTime() - firstPress; + if (pressTimer) { + clearInterval(pressTimer); + pressTimer = undefined; + } + + if ( dur >= 1.5 ) { + switch(btn) { + case 1: + face.onButtonLong(btn); + break; + case 2: + Bangle.showLauncher(); + break; + case 3: + // do nothing + break; + } + return; + } + + if (btn !== 3) face.onButtonShort(btn); +} + +function setButtons(){ + setWatch(buttonPressed.bind(null,1), BTN1, {repeat:true,edge:"rising"}); + setWatch(buttonPressed.bind(null,2), BTN2, {repeat:true,edge:"rising"}); + setWatch(nextFace, BTN3, {repeat:true,edge:"rising"}); + + setWatch(buttonReleased.bind(null,1), BTN1, {repeat:true,edge:"falling"}); + setWatch(buttonReleased.bind(null,2), BTN2, {repeat:true,edge:"falling"}); + // BTN 3 long press should always reset the bangle +} + +Bangle.on('kill',()=>{ + Bangle.setCompassPower(0); + Bangle.setGPSPower(0); +}); + +Bangle.on('lcdPower',function(on) { + if (on) { + startdraw(); + } else { + stopdraw(); + } +}); + +/***************************************************************************** + +Start of GPS object code so we can share it between faces + +******************************************************************************/ + + +function log_debug(o) { + //console.log(o); +} + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function GPS() { + this.wp; + this.wp_index = 0; + this.wp_current = undefined; + this.GPS_OFF = 0; + this.GPS_TIME = 1; + this.GPS_SATS = 2; + this.GPS_RUNNING = 3; + this.gpsState = this.GPS_OFF; + this.gpsPowerState = false; + this.last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + satellites: 0 + }; + this.listenerCount = 0; + this.loadFirstWaypoint(); +} + +GPS.prototype.log_debug = function(o) { + //console.log(o); +}; + +GPS.prototype.getState = function() { + return this.gpsState; +} + +GPS.prototype.getLastFix = function() { + return this.last_fix; +} + +GPS.prototype.determineGPSState = function() { + this.log_debug("determineGPSState"); + gpsPowerState = Bangle.isGPSOn(); + + //this.log_debug("last_fix.fix " + this.last_fix.fix); + //this.log_debug("gpsPowerState " + this.gpsPowerState); + //this.log_debug("last_fix.satellites " + this.last_fix.satellites); + + if (!gpsPowerState) { + this.gpsState = this.GPS_OFF; + this.resetLastFix(); + } else if (this.last_fix.fix && this.gpsPowerState && this.last_fix.satellites > 0) { + this.gpsState = this.GPS_RUNNING; + } else { + this.gpsState = this.GPS_SATS; + } + + this.log_debug("gpsState=" + this.gpsState); + + if (this.gpsState !== this.GPS_OFF) { + if (this.listenerCount === 0) { + Bangle.on('GPS', processFix); + this.listenerCount++; + this.log_debug("listener added " + this.listenerCount); + } + } else { + if (this.listenerCount > 0) { + Bangle.removeListener("GPS", this.processFix); + this.listenerCount--; + this.log_debug("listener removed " + this.listenerCount); + } + } +}; + +GPS.prototype.getGPSTime = function() { + var time; + + if (this.last_fix !== undefined && this.last_fix.time !== undefined && this.last_fix.time.toUTCString !== undefined && + (this.gpsState == this.GPS_SATS || this.gpsState == this.GPS_RUNNING)) { + time = this.last_fix.time.toUTCString().split(" "); + return time[4]; + } else { + var d = new Date(); + var da = d.toString().split(" "); + time = da[4].substr(0,5); + return time; + } +}; + +GPS.prototype.toggleGPSPower = function() { + this.log_debug("toggleGPSPower()"); + this.gpsPowerState = Bangle.isGPSOn(); + this.gpsPowerState = !this.gpsPowerState; + Bangle.setGPSPower(this.gpsPowerState ? 1 : 0); + + this.resetLastFix(); + this.determineGPSState(); + + // poke the gps widget indicator to change + if (WIDGETS.gps !== undefined) { + WIDGETS.gps.draw(); + } +}; + +GPS.prototype.resetLastFix = function() { + this.last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + satellites: 0 + }; +}; + +function processFix(fix) { + //log_debug("processFix()"); + gpsObj.processFix(fix); +} + +GPS.prototype.processFix = function(fix) { + //this.log_debug("GPS:processFix()"); + //this.log_debug(fix); + this.last_fix.time = fix.time; + + if (this.gpsState == this.GPS_TIME) { + this.gpsState = this.GPS_SATS; + } + + if (fix.fix) { + //this.log_debug("Got fix - setting state to GPS_RUNNING"); + this.gpsState = this.GPS_RUNNING; + if (!this.last_fix.fix) Bangle.buzz(); // buzz on first position + this.last_fix = fix; + } +}; + +GPS.prototype.formatTime = function(now) { + var fd = now.toUTCString().split(" "); + return fd[4]; +}; + +GPS.prototype.timeSince = function(t) { + var hms = t.split(":"); + var now = new Date(); + + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); + var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); + + return (sn - st); +}; + +GPS.prototype.getOsRef = function() { + let os = OsGridRef.latLongToOsGrid(this.last_fix); + let ref = to_map_ref(6, os.easting, os.northing); + return ref; +}; + +GPS.prototype.calcBearing = function(a,b) { + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); +} + +GPS.prototype.calcDistance = function(a,b) { + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + +GPS.prototype.getWPdistance = function() { + //log_debug(this.last_fix); + //log_debug(this.wp_current); + + if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + return 0; + else + return this.calcDistance(this.last_fix, this.wp_current); +} + +GPS.prototype.getWPbearing = function() { + //log_debug(this.last_fix); + //log_debug(this.wp_current); + + if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + return 0; + else + return this.calcBearing(this.last_fix, this.wp_current); +} + +GPS.prototype.loadFirstWaypoint = function() { + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + this.wp_index = 0; + this.wp_current = waypoints[this.wp_index]; + log_debug(this.wp_current); + return this.wp_current; +} + +GPS.prototype.getCurrentWaypoint = function() { + return this.wp_current; +} + +GPS.prototype.waypointHasLocation = function() { + if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + return false; + else + return true; +} + +GPS.prototype.markWaypoint = function() { + + if(this.wp_current.name === "NONE") + return; + + log_debug("GPS::markWaypoint()"); + + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + this.wp_current = waypoints[this.wp_index]; + + if (this.waypointHasLocation()) { + waypoints[this.wp_index] = {name:this.wp_current.name, lat:0, lon:0}; + } else { + waypoints[this.wp_index] = {name:this.wp_current.name, lat:this.last_fix.lat, lon:this.last_fix.lon}; + } + + this.wp_current = waypoints[this.wp_index]; + require("Storage").writeJSON("waypoints.json", waypoints); + log_debug("GPS::markWaypoint() written"); +} + +GPS.prototype.nextWaypoint = function(inc) { + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + this.wp_index+=inc; + if (this.wp_index>=waypoints.length) this.wp_index=0; + if (this.wp_index<0) this.wp_index = waypoints.length-1; + this.wp_current = waypoints[this.wp_index]; + log_debug(this.wp_current); + return this.wp_current; +} + +var gpsObj = new GPS(); + + +/***************************************************************************** + +Start of OS lat lon to grid ref code + +******************************************************************************/ + +Number.prototype.toRad = function() { return this*Math.PI/180; }; +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* Ordnance Survey Grid Reference functions (c) Chris Veness 2005-2014 */ +/* - www.movable-type.co.uk/scripts/gridref.js */ +/* - www.movable-type.co.uk/scripts/latlon-gridref.html */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +function OsGridRef(easting, northing) { + this.easting = 0|easting; + this.northing = 0|northing; +} + +OsGridRef.latLongToOsGrid = function(point) { + var lat = point.lat.toRad(); + var lon = point.lon.toRad(); + + var a = 6377563.396, b = 6356256.909; // Airy 1830 major & minor semi-axes + var F0 = 0.9996012717; // NatGrid scale factor on central meridian + var lat0 = (49).toRad(), lon0 = (-2).toRad(); // NatGrid true origin is 49�N,2�W + var N0 = -100000, E0 = 400000; // northing & easting of true origin, metres + var e2 = 1 - (b*b)/(a*a); // eccentricity squared + var n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n; + + var cosLat = Math.cos(lat), sinLat = Math.sin(lat); + var nu = a*F0/Math.sqrt(1-e2*sinLat*sinLat); // transverse radius of curvature + var rho = a*F0*(1-e2)/Math.pow(1-e2*sinLat*sinLat, 1.5); // meridional radius of curvature + var eta2 = nu/rho-1; + + var Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (lat-lat0); + var Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(lat-lat0) * Math.cos(lat+lat0); + var Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(lat-lat0)) * Math.cos(2*(lat+lat0)); + var Md = (35/24)*n3 * Math.sin(3*(lat-lat0)) * Math.cos(3*(lat+lat0)); + var M = b * F0 * (Ma - Mb + Mc - Md); // meridional arc + + var cos3lat = cosLat*cosLat*cosLat; + var cos5lat = cos3lat*cosLat*cosLat; + var tan2lat = Math.tan(lat)*Math.tan(lat); + var tan4lat = tan2lat*tan2lat; + + var I = M + N0; + var II = (nu/2)*sinLat*cosLat; + var III = (nu/24)*sinLat*cos3lat*(5-tan2lat+9*eta2); + var IIIA = (nu/720)*sinLat*cos5lat*(61-58*tan2lat+tan4lat); + var IV = nu*cosLat; + var V = (nu/6)*cos3lat*(nu/rho-tan2lat); + var VI = (nu/120) * cos5lat * (5 - 18*tan2lat + tan4lat + 14*eta2 - 58*tan2lat*eta2); + + var dLon = lon-lon0; + var dLon2 = dLon*dLon, dLon3 = dLon2*dLon, dLon4 = dLon3*dLon, dLon5 = dLon4*dLon, dLon6 = dLon5*dLon; + + var N = I + II*dLon2 + III*dLon4 + IIIA*dLon6; + var E = E0 + IV*dLon + V*dLon3 + VI*dLon5; + + return new OsGridRef(E, N); +}; + +/* + * converts northing, easting to standard OS grid reference. + * + * [digits=10] - precision (10 digits = metres) + * to_map_ref(8, 651409, 313177); => 'TG 5140 1317' + * to_map_ref(0, 651409, 313177); => '651409,313177' + * + */ +function to_map_ref(digits, easting, northing) { + if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing + + let e = easting; + let n = northing; + + // use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7 + if (digits == 0) { + const format = { useGrouping: false, minimumIntegerDigits: 6, maximumFractionDigits: 3 }; + const ePad = e.toLocaleString('en', format); + const nPad = n.toLocaleString('en', format); + return `${ePad},${nPad}`; + } + + // get the 100km-grid indices + const e100km = Math.floor(e / 100000), n100km = Math.floor(n / 100000); + + // translate those into numeric equivalents of the grid letters + let l1 = (19 - n100km) - (19 - n100km) % 5 + Math.floor((e100km + 10) / 5); + let l2 = (19 - n100km) * 5 % 25 + e100km % 5; + + // compensate for skipped 'I' and build grid letter-pairs + if (l1 > 7) l1++; + if (l2 > 7) l2++; + const letterPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0)); + + // strip 100km-grid indices from easting & northing, and reduce precision + e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2)); + n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2)); + + // pad eastings & northings with leading zeros + e = e.toString().padStart(digits/2, '0'); + n = n.toString().padStart(digits/2, '0'); + + return `${letterPair} ${e} ${n}`; +} + + +/***************************************************************************** + +End of GPS object + +******************************************************************************/ + + +g.clear(); +Bangle.loadWidgets(); +face.init(gpsObj); +startdraw(); +setButtons(); diff --git a/apps/kitchen/kitchen.icon.js b/apps/kitchen/kitchen.icon.js new file mode 100644 index 000000000..32b01e4bd --- /dev/null +++ b/apps/kitchen/kitchen.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4AVgAACF1crnNkF9QuBLoMHF9cyGIYuprovBX1cAsgABrovYJSBeBF4SPBFy8HmQbOLwdkFzAtBmYcNCIIuCYAIvXLgJ/BF5pecAAy9OdqgmCg8rmUznMyAAJkCEI5eEdqQgBU4YAJrsHEYheErwuSFpoxEEoa9WIwgwTgFeAwQvQLqQwFaYa7TLyYuBmQYBdKQuCIoUrlcyABcrKwJcWXgZJBEIQiBAAQlEEwI+BmU5CgINDF6ZgFGQI3BAAkzLw8OLygvBg8VhwaBLY5eEg8OisVjgABhwvVhwaCACgv/F78VF/4v/F8sHd9owBF68HFyhgYRyowDYQMVFqMHFy4xEAAMHg41BAAIrFiq6BAAIuZGhIAIFj4A/AH4A/AH4AWA==")) diff --git a/apps/kitchen/kitchen.info.js b/apps/kitchen/kitchen.info.js new file mode 100644 index 000000000..d800a60ea --- /dev/null +++ b/apps/kitchen/kitchen.info.js @@ -0,0 +1,8 @@ +require("Storage").write("kitchen.info",{ + "id":"kitchen", + "name":"Kitchen", + "src":"kitchen.app.js", + "icon":"kitchen.img", + "type":"clock", + "files":"kitchen.info, kitchen.app.js, kitchen.img, stepo.kit.js, digi.kit.js, compass.kit.js, gps.kit.js" +}); diff --git a/apps/kitchen/kitchen.png b/apps/kitchen/kitchen.png new file mode 100644 index 000000000..83de52285 Binary files /dev/null and b/apps/kitchen/kitchen.png differ diff --git a/apps/kitchen/stepo.kit.js b/apps/kitchen/stepo.kit.js new file mode 100644 index 000000000..9d7601e2a --- /dev/null +++ b/apps/kitchen/stepo.kit.js @@ -0,0 +1,136 @@ +(() => { + function getFace(){ + var pal4color; + var pal4red; + var buf; + var intervalRefSec; + + function init(g) { + showMem("stepo init 1"); + pal4color = new Uint16Array([0x0000,0xFFFF,0x7BEF,0xAFE5],0,2); // b,w,grey,greenyellow + pal4red = new Uint16Array([0x0000,0xFFFF,0xF800,0xAFE5],0,2); // b,w,red,greenyellow + buf = Graphics.createArrayBuffer(120,120,2,{msb:true}); + showMem("stepo init 2"); + } + + function freeResources() { + showMem("stepo free 1"); + pal4color = undefined; + pal4red = undefined; + buf = undefined; + showMem("stepo free 2"); + } + + function showMem(msg) { + var val = process.memory(); + var str = msg + " " + Math.round(val.usage*100/val.total) + "%"; + //console.log(str); + } + + function flip(x,y) { + g.drawImage({width:120,height:120,bpp:2,buffer:buf.buffer, palette:pal4color}, x, y); + buf.clear(); + } + + function flip_red(x,y) { + g.drawImage({width:120,height:120,bpp:2,buffer:buf.buffer, palette:pal4red}, x, y); + buf.clear(); + } + + function onButtonShort(btn) {} + function onButtonLong(btn) {} + + function radians(a) { + return a*Math.PI/180; + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function drawSteps() { + var i = 0; + var cx = 60; + var cy = 60; + var r = 56; + var steps = getSteps(); + var percent = steps / 10000; + + if (percent > 1) percent = 1; + + var startrot = 0 - 180; + var midrot = -180 - (360 * percent); + var endrot = -360 - 180; + + buf.setColor(3); // green-yellow + + // draw guauge + for (i = startrot; i > midrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + buf.fillCircle(x,y,4); + } + + buf.setColor(2); // grey + + // draw remainder of guage in grey + for (i = midrot; i > endrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + buf.fillCircle(x,y,4); + } + + // draw steps + buf.setColor(1); // white + buf.setFont("Vector", 24); + buf.setFontAlign(0,0); + buf.drawString(steps, cx, cy); + + // change the remaining color to RED if battery is below 25% + if (E.getBattery() > 25) + flip(60,115); + else + flip_red(60,115); + } + + function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + g.clearRect(0, 30, 239, 99); + g.setColor(1,1,1); + g.setFontAlign(0, -1); + g.setFont("Vector", 80); + g.drawString(time, 120, 30, true); + + drawSteps(); + } + + function getSteps() { + if (stepsWidget() !== undefined) + return stepsWidget().getSteps(); + return "-"; + } + + function stepsWidget() { + if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom; + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom; + } + return undefined; + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; + +})(); diff --git a/apps/kitchen/waypoints.html b/apps/kitchen/waypoints.html new file mode 100644 index 000000000..d02260732 --- /dev/null +++ b/apps/kitchen/waypoints.html @@ -0,0 +1,170 @@ + +
+ + + + + +| Name | +Lat. | +Long. | +Actions | +
|---|