diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog index b67f29e94..f4a0c96f5 100644 --- a/apps/calclock/ChangeLog +++ b/apps/calclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial version 0.02: More compact rendering & app icon 0.03: Tell clock widgets to hide. +0.04: Improve current time readability in light theme. diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js index 343d95c04..a55dc05f9 100644 --- a/apps/calclock/calclock.js +++ b/apps/calclock/calclock.js @@ -68,7 +68,7 @@ function drawEvent(event, y) { var curEventHeight = 0; function drawCurrentEvents(y) { - g.setColor("#0ff"); + g.setColor(g.theme.dark ? "#0ff" : "#0000ff"); g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight); curEventHeight = y; diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json index 7bac5c721..3aab55186 100644 --- a/apps/calclock/metadata.json +++ b/apps/calclock/metadata.json @@ -2,7 +2,7 @@ "id": "calclock", "name": "Calendar Clock", "shortName": "CalClock", - "version": "0.03", + "version": "0.04", "description": "Show the current and upcoming events synchronized from Gadgetbridge", "icon": "calclock.png", "type": "clock", diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gpstrek/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gpstrek/README.md b/apps/gpstrek/README.md new file mode 100644 index 000000000..eecf4d087 --- /dev/null +++ b/apps/gpstrek/README.md @@ -0,0 +1,43 @@ +# GPS Trekking + +Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation! + +This app is inspired by and uses code from "GPS Navigation" and "Navigation compass". + +## Usage + +Tapping or button to switch to the next information display, swipe right for the menu. + +Choose either a route or a waypoint as basis for the display. + +After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way. +Waypoints are shown with name if available and distance to waypoint. + +### Route + +Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh) + +The resulting file needs to be uploaded to the watch and will be shown in the file selection menu. + +The route can be mirrored to switch start and destination. + +If the GPS position is closer than 30m to the next waypoint, the route is automatically advanced to the next waypoint. + +### Waypoints + +You can select a waypoint from the "Waypoints" app as destination. + +## Calibration + +### Altitude + +You can correct the barometric altitude display either by manually setting a known correct value or using the GPS fix elevation as reference. This will only affect the display of altitude values. + +### Compass + +If the compass fallback starts to show unreliable values, you can reset the calibration in the menu. It starts to show values again after turning 360°. + +## Widget + +The widget keeps the sensors alive and records some very basic statics when the app is not started. +This uses a lot of power so ensure to stop the app if you are not actively using it. diff --git a/apps/gpstrek/app-icon.js b/apps/gpstrek/app-icon.js new file mode 100644 index 000000000..6b2924353 --- /dev/null +++ b/apps/gpstrek/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIjggOAApMD4AFJg4FF8AFJh/wApMf/AFJn/8ApN//wFDvfeAof774FD+fPLwYFBMAUB8fHAoUDAoJaCgfD4YFIg+D4JgCAosPAoJgCh6DBAoUfAoJgCjwFBvAFBnwFBvgFBngFBngFBvh3BnwFBvH//8eMgQFBMwX//k//5eB//wh//wAFBAQcDRoU/4EDJQfAbYbfFACYA=")) diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js new file mode 100644 index 000000000..091c407fb --- /dev/null +++ b/apps/gpstrek/app.js @@ -0,0 +1,828 @@ +const STORAGE = require("Storage"); +const showWidgets = true; +let numberOfSlices=4; + +if (showWidgets){ + Bangle.loadWidgets(); +} + +let state = WIDGETS["gpstrek"].getState(); +WIDGETS["gpstrek"].start(); + +function parseNumber(toParse){ + if (toParse.includes(".")) return parseFloat(toParse); + return parseFloat("" + toParse + ".0"); +} + +function parseWaypoint(filename, offset, result){ + result.lat = parseNumber(STORAGE.read(filename, offset, 11)); + result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12)); + return offset + 12; +} + +function parseWaypointWithElevation(filename, offset, result){ + offset = parseWaypoint(filename, offset, result); + result.alt = parseNumber(STORAGE.read(filename, offset, 6)); + return offset + 6; +} + +function parseWaypointWithName(filename, offset, result){ + offset = parseWaypoint(filename, offset, result); + return parseName(filename, offset, result); +} + +function parseName(filename, offset, result){ + let nameLength = STORAGE.read(filename, offset, 2) - 0; + result.name = STORAGE.read(filename, offset += 2, nameLength); + return offset + nameLength; +} + +function parseWaypointWithElevationAndName(filename, offset, result){ + offset = parseWaypointWithElevation(filename, offset, result); + return parseName(filename, offset, result); +} + +function getEntry(filename, offset, result){ + result.fileOffset = offset; + let type = STORAGE.read(filename, offset++, 1); + if (type == "") return -1; + switch (type){ + case "A": + offset = parseWaypoint(filename, offset, result); + break; + case "B": + offset = parseWaypointWithName(filename, offset, result); + break; + case "C": + offset = parseWaypointWithElevation(filename, offset, result); + break; + case "D": + offset = parseWaypointWithElevationAndName(filename, offset, result); + break; + default: + print("Unknown entry type", type); + return -1; + } + offset++; + + result.fileLength = offset - result.fileOffset; + //print(result); + return offset; +} + +const labels = ["N","NE","E","SE","S","SW","W","NW"]; +const loc = require("locale"); + +function matchFontSize(graphics, text, height, width){ + graphics.setFontVector(height); + let metrics; + let size = 1; + while (graphics.stringMetrics(text).width > 0.90 * width){ + size -= 0.05; + graphics.setFont("Vector",Math.floor(height*size)); + } +} + +function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){ + let lastDrawn = Date.now() - Math.random()*refreshTime; + return { + refresh: function (){ + return Date.now() - lastDrawn > (Bangle.isLocked()?(refreshTime?refreshTime:5000):(refreshTime?refreshTime*2:10000)); + }, + draw: function (graphics, x, y, height, width){ + lastDrawn = Date.now(); + if (typeof title1 == "function") title1 = title1(); + if (typeof title2 == "function") title2 = title2(); + graphics.clearRect(x,y,x+width,y+height); + + let value = provider1(); + matchFontSize(graphics, title1 + value, Math.floor(height*0.5), width); + graphics.setFontAlign(-1,-1); + graphics.drawString(title1, x+2, y); + graphics.setFontAlign(1,-1); + graphics.drawString(value, x+width, y); + + value = provider2(); + matchFontSize(graphics, title2 + value, Math.floor(height*0.5), width); + graphics.setFontAlign(-1,-1); + graphics.drawString(title2, x+2, y+(height*0.5)); + graphics.setFontAlign(1,-1); + graphics.drawString(value, x+width, y+(height*0.5)); + } + }; +} + +function getTargetSlice(targetDataSource){ + let nameIndex = 0; + let lastDrawn = Date.now() - Math.random()*3000; + return { + refresh: function (){ + return Date.now() - lastDrawn > (Bangle.isLocked()?10000:3000); + }, + draw: function (graphics, x, y, height, width){ + lastDrawn = Date.now(); + graphics.clearRect(x,y,x+width,y+height); + if (targetDataSource.icon){ + graphics.drawImage(targetDataSource.icon,x,y + (height - 16)/2); + x += 16; + width -= 16; + } + + if (!targetDataSource.getTarget() || !targetDataSource.getStart()) return; + + let dist = distance(targetDataSource.getStart(),targetDataSource.getTarget()); + if (isNaN(dist)) dist = Infinity; + let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°"; + if (targetDataSource.getTarget().name) { + graphics.setFont("Vector",Math.floor(height*0.5)); + let scrolledName = (targetDataSource.getTarget().name || "").substring(nameIndex); + if (graphics.stringMetrics(scrolledName).width > width){ + nameIndex++; + } else { + nameIndex = 0; + } + graphics.drawString(scrolledName, x+2, y); + + let distanceString = loc.distance(dist,2); + matchFontSize(graphics, distanceString + bearingString, height*0.5, width); + graphics.drawString(bearingString, x+2, y+(height*0.5)); + graphics.setFontAlign(1,-1); + graphics.drawString(distanceString, x + width, y+(height*0.5)); + } else { + graphics.setFont("Vector",Math.floor(height*1)); + let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°"; + let formattedDist = loc.distance(dist,2); + let distNum = (formattedDist.match(/[0-9\.]+/) || [Infinity])[0]; + let size = 0.8; + let distNumMetrics; + while (graphics.stringMetrics(bearingString).width + (distNumMetrics = graphics.stringMetrics(distNum)).width > 0.90 * width){ + size -= 0.05; + graphics.setFont("Vector",Math.floor(height*size)); + } + graphics.drawString(bearingString, x+2, y + (height - distNumMetrics.height)/2); + graphics.setFontAlign(1,-1); + graphics.drawString(distNum, x + width, y + (height - distNumMetrics.height)/2); + graphics.setFont("Vector",Math.floor(height*0.25)); + + graphics.setFontAlign(-1,1); + if (targetDataSource.getProgress){ + graphics.drawString(targetDataSource.getProgress(), x + 2, y + height); + } + graphics.setFontAlign(1,1); + if (!isNaN(distNum) && distNum != Infinity) + graphics.drawString(formattedDist.match(/[a-zA-Z]+/), x + width, y + height); + } + } + }; +} + +function drawCompass(graphics, x, y, height, width, increment, start){ + graphics.setFont12x20(); + graphics.setFontAlign(0,-1); + graphics.setColor(graphics.theme.fg); + let frag = 0 - start%15; + if (frag>0) frag = 0; + let xpos = 0 + frag*increment; + for (let i=start;i<=720;i+=15){ + var res = i + frag; + if (res%90==0) { + graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2); + graphics.fillRect(xpos-2,Math.floor(y+height*0.6),xpos+2,Math.floor(y+height)); + } else if (res%45==0) { + graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2); + graphics.fillRect(xpos-2,Math.floor(y+height*0.75),xpos+2,Math.floor(y+height)); + } else if (res%15==0) { + graphics.fillRect(xpos,Math.floor(y+height*0.9),xpos+1,Math.floor(y+height)); + } + xpos+=increment*15; + if (xpos > width + 20) break; + } +} + +function getCompassSlice(compassDataSource){ + let lastDrawn = Date.now() - Math.random()*2000; + const buffers = 4; + let buf = []; + return { + refresh : function (){return Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true;}, + draw: function (graphics, x,y,height,width){ + lastDrawn = Date.now(); + const max = 180; + const increment=width/max; + + graphics.clearRect(x,y,x+width,y+height); + + var start = compassDataSource.getCourse() - 90; + if (isNaN(compassDataSource.getCourse())) start = -90; + if (start<0) start+=360; + start = start % 360; + + if (state.acc && compassDataSource.getCourseType() == "MAG"){ + drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start); + } else { + drawCompass(graphics,0,y,height,width,increment,start); + } + + + if (compassDataSource.getPoints){ + for (let p of compassDataSource.getPoints()){ + var bpos = p.bearing - compassDataSource.getCourse(); + if (bpos>180) bpos -=360; + if (bpos<-180) bpos +=360; + bpos+=120; + let min = 0; + let max = 180; + if (bpos<=min){ + bpos = Math.floor(width*0.05); + } else if (bpos>=max) { + bpos = Math.ceil(width*0.95); + } else { + bpos=Math.round(bpos*increment); + } + graphics.setColor(p.color); + graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03)); + } + } + if (compassDataSource.getMarkers){ + for (let m of compassDataSource.getMarkers()){ + g.setColor(m.fillcolor); + let mpos = m.xpos * width; + if (m.xpos < 0.05) mpos = Math.floor(width*0.05); + if (m.xpos > 0.95) mpos = Math.ceil(width*0.95); + g.fillPoly(triangle(mpos,y+height-m.height, m.height, m.width)); + g.setColor(m.linecolor); + g.drawPoly(triangle(mpos,y+height-m.height, m.height, m.width),true); + } + } + graphics.setColor(g.theme.fg); + graphics.fillRect(x,y,Math.floor(width*0.05),y+height); + graphics.fillRect(Math.ceil(width*0.95),y,width,y+height); + if (state.acc && compassDataSource.getCourseType() == "MAG") { + let xh = E.clip(width*0.5-height/2+(((state.acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2); + let yh = E.clip(y+(((state.acc.y+1)/2)*height),y,y+height); + + graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05)); + + graphics.setColor(g.theme.bg); + graphics.drawLine(width*0.5 - 5, y, width*0.5 - 5, y + Math.floor(width*0.05)); + graphics.drawLine(width*0.5 + 5, y, width*0.5 + 5, y + Math.floor(width*0.05)); + graphics.fillRect(xh-1,y,xh+1,y+Math.floor(width*0.05)); + + let left = Math.floor(width*0.05); + let right = Math.ceil(width*0.95); + graphics.drawLine(0,y+height/2-5,left,y+height/2-5); + graphics.drawLine(right,y+height/2-5,x+width,y+height/2-5); + graphics.drawLine(0,y+height/2+5,left,y+height/2+5); + graphics.drawLine(right,y+height/2+5,x+width,y+height/2+5); + graphics.fillRect(0,yh-1,left,yh+1); + graphics.fillRect(right,yh-1,x+width,yh+1); + } + graphics.setColor(g.theme.fg); + graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height); + } + }; +} + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function bearing(a,b){ + if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; + 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))); +} + +function distance(a,b){ + if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; + 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); +} + +function triangle (x, y, width, height){ + return [ + Math.round(x),Math.round(y), + Math.round(x+width * 0.5), Math.round(y+height), + Math.round(x-width * 0.5), Math.round(y+height) + ]; +} + +function setButtons(){ + Bangle.setUI("leftright", (dir)=>{ + if (dir < 0) { + nextScreen(); + } else if (dir > 0) { + switchMenu(); + } else { + nextScreen(); + } + }); +} + +function getApproxFileSize(name){ + let currentStart = STORAGE.getStats().totalBytes; + let currentSize = 0; + for (let i = currentStart; i > 500; i/=2){ + let currentDiff = i; + //print("Searching", currentDiff); + while (STORAGE.read(name, currentSize+currentDiff, 1) == ""){ + //print("Loop", currentDiff); + currentDiff = Math.ceil(currentDiff/2); + } + i = currentDiff*2; + currentSize += currentDiff; + } + return currentSize; +} + +function parseRouteData(filename, progressMonitor){ + let routeInfo = {}; + + routeInfo.filename = filename; + routeInfo.refs = []; + + let c = {}; + let scanOffset = 0; + routeInfo.length = 0; + routeInfo.count = 0; + routeInfo.mirror = false; + let lastSeenWaypoint; + let lastSeenAlt; + let waypoint = {}; + + routeInfo.up = 0; + routeInfo.down = 0; + + let size = getApproxFileSize(filename); + + while ((scanOffset = getEntry(filename, scanOffset, waypoint)) > 0) { + if (routeInfo.count % 5 == 0) progressMonitor(scanOffset, "Loading", size); + if (lastSeenWaypoint){ + routeInfo.length += distance(lastSeenWaypoint, waypoint); + + let diff = waypoint.alt - lastSeenAlt; + //print("Distance", routeInfo.length, "alt", lastSeenAlt, waypoint.alt, diff); + if (waypoint.alt && lastSeenAlt && diff > 3){ + if (lastSeenAlt < waypoint.alt){ + //print("Up", diff); + routeInfo.up += diff; + } else { + //print("Down", diff); + routeInfo.down += diff; + } + } + } + routeInfo.count++; + routeInfo.refs.push(waypoint.fileOffset); + lastSeenWaypoint = waypoint; + if (!isNaN(waypoint.alt)) lastSeenAlt = waypoint.alt; + waypoint = {}; + } + + set(routeInfo, 0); + return routeInfo; +} + +function hasPrev(route){ + if (route.mirror) return route.index < (route.count - 1); + return route.index > 0; +} + +function hasNext(route){ + if (route.mirror) return route.index > 0; + return route.index < (route.count - 1); +} + +function next(route){ + if (!hasNext(route)) return; + if (route.mirror) set(route, --route.index); + if (!route.mirror) set(route, ++route.index); +} + +function set(route, index){ + route.currentWaypoint = {}; + route.index = index; + getEntry(route.filename, route.refs[index], route.currentWaypoint); +} + +function prev(route){ + if (!hasPrev(route)) return; + if (route.mirror) set(route, ++route.index); + if (!route.mirror) set(route, --route.index); +} + +let lastMirror; +let cachedLast; + +function getLast(route){ + let wp = {}; + if (lastMirror != route.mirror){ + if (route.mirror) getEntry(route.filename, route.refs[0], wp); + if (!route.mirror) getEntry(route.filename, route.refs[route.count - 1], wp); + lastMirror = route.mirror; + cachedLast = wp; + } + return cachedLast; +} + +function removeMenu(){ + E.showMenu(); + switchNav(); +} + +function showProgress(progress, title, max){ + //print("Progress",progress,max) + let message = title? title: "Loading"; + if (max){ + message += " " + E.clip((progress/max*100),0,100).toFixed(0) +"%"; + } else { + let dots = progress % 4; + for (let i = 0; i < dots; i++) message += "."; + for (let i = dots; i < 4; i++) message += " "; + } + E.showMessage(message); +} + +function handleLoading(c){ + E.showMenu(); + state.route = parseRouteData(c, showProgress); + state.waypoint = null; + removeMenu(); + state.route.mirror = false; +} + +function showRouteSelector (){ + var menu = { + "" : { + back : showRouteMenu, + } + }; + + for (let c of STORAGE.list((/\.trf$/))){ + let file = c; + menu[file] = ()=>{handleLoading(file);}; + } + + E.showMenu(menu); +} + +function showRouteMenu(){ + var menu = { + "" : { + "title" : "Route", + back : showMenu, + }, + "Select file" : showRouteSelector + }; + + if (state.route){ + menu.Mirror = { + value: state && state.route && !!state.route.mirror || false, + onchange: v=>{ + state.route.mirror = v; + } + }; + menu['Select closest waypoint'] = function () { + if (state.currentPos && state.currentPos.lat){ + setClosestWaypoint(state.route, null, showProgress); removeMenu(); + } else { + E.showAlert("No position").then(()=>{E.showMenu(menu);}); + } + }; + menu['Select closest waypoint (not visited)'] = function () { + if (state.currentPos && state.currentPos.lat){ + setClosestWaypoint(state.route, state.route.index, showProgress); removeMenu(); + } else { + E.showAlert("No position").then(()=>{E.showMenu(menu);}); + } + }; + menu['Select waypoint'] = { + value : state.route.index, + min:1,max:state.route.count,step:1, + onchange : v => { set(state.route, v-1); } + }; + menu['Select waypoint as current position'] = function (){ + state.currentPos.lat = state.route.currentWaypoint.lat; + state.currentPos.lon = state.route.currentWaypoint.lon; + state.currentPos.alt = state.route.currentWaypoint.alt; + removeMenu(); + }; + } + + if (state.route && hasPrev(state.route)) + menu['Previous waypoint'] = function() { prev(state.route); removeMenu(); }; + if (state.route && hasNext(state.route)) + menu['Next waypoint'] = function() { next(state.route); removeMenu(); }; + E.showMenu(menu); +} + +function showWaypointSelector(){ + let waypoints = require("waypoints").load(); + var menu = { + "" : { + back : showWaypointMenu, + } + }; + + for (let c in waypoints){ + menu[waypoints[c].name] = function (){ + state.waypoint = waypoints[c]; + state.waypointIndex = c; + state.route = null; + removeMenu(); + }; + } + + E.showMenu(menu); +} + +function showCalibrationMenu(){ + let menu = { + "" : { + "title" : "Calibration", + back : showMenu, + }, + "Barometer (GPS)" : ()=>{ + if (!state.currentPos || isNaN(state.currentPos.alt)){ + E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);}); + } else { + state.calibAltDiff = state.altitude - state.currentPos.alt; + E.showAlert("Calibrated Altitude Difference: " + state.calibAltDiff.toFixed(0)).then(()=>{removeMenu();}); + } + }, + "Barometer (Manual)" : { + value : Math.round(state.currentPos && (state.currentPos.alt != undefined && !isNaN(state.currentPos.alt)) ? state.currentPos.alt: state.altitude), + min:-2000,max: 10000,step:1, + onchange : v => { state.calibAltDiff = state.altitude - v; } + }, + "Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();}, + }; + E.showMenu(menu); +} + +function showWaypointMenu(){ + let menu = { + "" : { + "title" : "Waypoint", + back : showMenu, + }, + "Select waypoint" : showWaypointSelector, + }; + E.showMenu(menu); +} + +function showMenu(){ + var mainmenu = { + "" : { + "title" : "Main", + back : removeMenu, + }, + "Route" : showRouteMenu, + "Waypoint" : showWaypointMenu, + "Calibration": showCalibrationMenu, + "Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {state.active = true; removeMenu();} else {E.showMenu(mainmenu);}});}, + "Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS["gpstrek"].stop(); removeMenu();} else {E.showMenu(mainmenu);}});}, + "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS["gpstrek"].resetState(); removeMenu();} else {E.showMenu(mainmenu);}});}, + "Slices" : { + value : numberOfSlices, + min:1,max:6,step:1, + onchange : v => { setNumberOfSlices(v); } + }, + }; + + E.showMenu(mainmenu); +} + +let scheduleDraw = true; + +function switchMenu(){ + screen = 0; + scheduleDraw = false; + showMenu(); +} + +function drawInTimeout(){ + setTimeout(()=>{ + draw(); + if (scheduleDraw) + setTimeout(drawInTimeout, 0); + },0); +} + +function switchNav(){ + if (!screen) screen = 1; + setButtons(); + scheduleDraw = true; + drawInTimeout(); +} + +function nextScreen(){ + screen++; + if (screen > maxScreens){ + screen = 1; + } +} + +function setClosestWaypoint(route, startindex, progress){ + if (startindex >= state.route.count) startindex = state.route.count - 1; + if (!state.currentPos.lat){ + set(route, startindex); + return; + } + let minDist = 100000000000000; + let minIndex = 0; + for (let i = startindex?startindex:0; i < route.count - 1; i++){ + if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count); + let wp = {}; + getEntry(route.filename, route.refs[i], wp); + let curDist = distance(state.currentPos, wp); + if (curDist < minDist){ + minDist = curDist; + minIndex = i; + } else { + if (startindex) break; + } + } + set(route, minIndex); +} + +let screen = 1; + +const compassSliceData = { + getCourseType: function(){ + return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG"; + }, + getCourse: function (){ + if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course; + return state.compassHeading?state.compassHeading:undefined; + }, + getPoints: function (){ + let points = []; + if (state.currentPos && state.currentPos.lon && state.route && state.route.currentWaypoint){ + points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"}); + } + if (state.currentPos && state.currentPos.lon && state.route){ + points.push({bearing:bearing(state.currentPos, getLast(state.route)), color:"#00f"}); + } + return points; + }, + getMarkers: function (){ + return [{xpos:0.5, width:10, height:10, linecolor:g.theme.fg, fillcolor:"#f00"}]; + } +}; + +const waypointData = { + icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"), + getProgress: function() { + return (state.route.index + 1) + "/" + state.route.count; + }, + getTarget: function (){ + if (distance(state.currentPos,state.route.currentWaypoint) < 30 && hasNext(state.route)){ + next(state.route); + Bangle.buzz(1000); + } + return state.route.currentWaypoint; + }, + getStart: function (){ + return state.currentPos; + } +}; + +const finishData = { + icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="), + getTarget: function (){ + if (state.route) return getLast(state.route); + if (state.waypoint) return state.waypoint; + }, + getStart: function (){ + return state.currentPos; + } +}; + +let sliceHeight; +function setNumberOfSlices(number){ + numberOfSlices = number; + sliceHeight = Math.floor((g.getHeight()-(showWidgets?24:0))/numberOfSlices); +} + +let slices = []; +let maxScreens = 1; +setNumberOfSlices(3); + +let compassSlice = getCompassSlice(compassSliceData); +let waypointSlice = getTargetSlice(waypointData); +let finishSlice = getTargetSlice(finishData); +let eleSlice = getDoubleLineSlice("Up","Down",()=>{ + return loc.distance(state.up,3) + "/" + (state.route ? loc.distance(state.route.up,3):"---"); +},()=>{ + return loc.distance(state.down,3) + "/" + (state.route ? loc.distance(state.route.down,3): "---"); +}); + +let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{ + let speed = 0; + if (state.currentPos && state.currentPos.speed) speed = state.currentPos.speed; + return loc.speed(speed,2); +},()=>{ + let alt = Infinity; + if (!isNaN(state.altitude)){ + alt = isNaN(state.calibAltDiff) ? state.altitude : (state.altitude - state.calibAltDiff); + } + if (state.currentPos && state.currentPos.alt) alt = state.currentPos.alt; + return loc.distance(alt,3); +}); + +let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{ + return (state.compassHeading?Math.round(state.compassHeading):"---") + "°"; +},()=>{ + let course = "---°"; + if (state.currentPos && state.currentPos.course) course = state.currentPos.course + "°"; + return course; +},200); + +let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{ + return state.bpm; +},()=>{ + return state.steps; +}); + +let system2Slice = getDoubleLineSlice("Bat","",()=>{ + return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + NRF.getBattery().toFixed(2) + "V"; +},()=>{ + return ""; +}); + +let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{ + let ram = process.memory(false); + return ((ram.blocksize * ram.free)/1024).toFixed(0)+"kB"; +},()=>{ + return (STORAGE.getFree()/1024).toFixed(0)+"kB"; +}); + +function updateSlices(){ + slices = []; + slices.push(compassSlice); + + if (state.currentPos && state.currentPos.lat && state.route && state.route.currentWaypoint && state.route.index < state.route.count - 1) { + slices.push(waypointSlice); + } + if (state.currentPos && state.currentPos.lat && (state.route || state.waypoint)) { + slices.push(finishSlice); + } + if ((state.route && state.route.down !== undefined) || state.down != undefined) { + slices.push(eleSlice); + } + slices.push(statusSlice); + slices.push(status2Slice); + slices.push(healthSlice); + slices.push(systemSlice); + slices.push(system2Slice); + maxScreens = Math.ceil(slices.length/numberOfSlices); +} + +function clear() { + g.clearRect(0,(showWidgets ? 24 : 0), g.getWidth(),g.getHeight()); +} +let lastDrawnScreen; +let firstDraw = true; + +function draw(){ + if (!screen) return; + let ypos = showWidgets ? 24 : 0; + + let firstSlice = (screen-1)*numberOfSlices; + + updateSlices(); + + let force = lastDrawnScreen != screen || firstDraw; + if (force){ + clear(); + if (showWidgets){ + Bangle.drawWidgets(); + } + } + lastDrawnScreen = screen; + + for (let slice of slices.slice(firstSlice,firstSlice + numberOfSlices)) { + g.reset(); + if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth()); + ypos += sliceHeight+1; + g.drawLine(0,ypos-1,g.getWidth(),ypos-1); + } + firstDraw = false; +} + + +switchNav(); + +g.clear(); diff --git a/apps/gpstrek/createRoute.sh b/apps/gpstrek/createRoute.sh new file mode 100755 index 000000000..729e6af00 --- /dev/null +++ b/apps/gpstrek/createRoute.sh @@ -0,0 +1,14 @@ +#!/bin/bash +[ -z "$1" ] && echo Give gpx file name + + +xmlstarlet select -t -m '//_:trkpt' \ + --if '_:name and _:ele' -o D \ + --elif '_:ele and not(_:name)' -o C \ + --elif 'not(_:ele) and _:name' -o B \ + --else -o A -b \ + -v 'format-number(@lat,"+00.0000000;-00.0000000")' \ + -v 'format-number(@lon,"+000.0000000;-000.0000000")' \ + --if '_:ele' -v 'format-number(_:ele,"+00000;-00000")' -b \ + --if _:name -v 'format-number(string-length(_:name),"00")' -v '_:name' -b \ + -n "$1" | iconv -f utf8 -t iso8859-1 > "$(basename "$1" | sed -e "s|.gpx||").trf" diff --git a/apps/gpstrek/icon.png b/apps/gpstrek/icon.png new file mode 100644 index 000000000..e1ff2b99d Binary files /dev/null and b/apps/gpstrek/icon.png differ diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json new file mode 100644 index 000000000..5168c870e --- /dev/null +++ b/apps/gpstrek/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "gpstrek", + "name": "GPS Trekking", + "version": "0.01", + "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!", + "icon": "icon.png", + "screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}], + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "dependencies" : { "waypoints":"type" }, + "storage": [ + {"name":"gpstrek.app.js","url":"app.js"}, + {"name":"gpstrek.wid.js","url":"widget.js"}, + {"name":"gpstrek.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/gpstrek/screen1.png b/apps/gpstrek/screen1.png new file mode 100644 index 000000000..3cfd7d31b Binary files /dev/null and b/apps/gpstrek/screen1.png differ diff --git a/apps/gpstrek/screen2.png b/apps/gpstrek/screen2.png new file mode 100644 index 000000000..12cd65975 Binary files /dev/null and b/apps/gpstrek/screen2.png differ diff --git a/apps/gpstrek/screen3.png b/apps/gpstrek/screen3.png new file mode 100644 index 000000000..a0c7fd8c3 Binary files /dev/null and b/apps/gpstrek/screen3.png differ diff --git a/apps/gpstrek/screen4.png b/apps/gpstrek/screen4.png new file mode 100644 index 000000000..7b6812077 Binary files /dev/null and b/apps/gpstrek/screen4.png differ diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js new file mode 100644 index 000000000..bba1298d0 --- /dev/null +++ b/apps/gpstrek/widget.js @@ -0,0 +1,129 @@ +(() => { +const STORAGE=require('Storage'); +let state = STORAGE.readJSON("gpstrek.state.json")||{}; + +function saveState(){ + state.saved = Date.now(); + STORAGE.writeJSON("gpstrek.state.json", state); +} + +E.on("kill",()=>{ + if (state.active){ + saveState(); + } +}); + + +function onPulse(e){ + state.bpm = e.bpm; +} + +function onGPS(fix) { + if(fix.fix) state.currentPos = fix; +} + +Bangle.on('accel', function(e) { + state.acc = e; +}); + +function onMag(e) { + if (!state.compassHeading) state.compassHeading = e.heading; + + //if (a+180)mod 360 == b then + //return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction) +//else + //return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) ) + + /* + let average; + let a = radians(compassHeading); + let b = radians(e.heading); + if ((a+180) % 360 == b){ + average = ((a+b)/2 % 360); //can add 180 depending on rotation + } else { + average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) ); + } + print("Angle",compassHeading,e.heading, average); + compassHeading = (compassHeading + degrees(average)) % 360; + */ + state.compassHeading = Math.round(e.heading); +} + +function onStep(e) { + state.steps++; +} + +function onPressure(e) { + state.pressure = e.pressure; + + if (!state.altitude){ + state.altitude = e.altitude; + state.up = 0; + state.down = 0; + } + let diff = state.altitude - e.altitude; + if (Math.abs(diff) > 3){ + if (diff > 0){ + state.up += diff; + } else { + state.down -= diff; + } + state.altitude = e.altitude; + } +} + +function start(){ + Bangle.on('GPS', onGPS); + Bangle.on("HRM", onPulse); + Bangle.on("mag", onMag); + Bangle.on("step", onStep); + Bangle.on("pressure", onPressure); + + Bangle.setGPSPower(1, "gpstrek"); + Bangle.setHRMPower(1, "gpstrek"); + Bangle.setCompassPower(1, "gpstrek"); + Bangle.setBarometerPower(1, "gpstrek"); + state.active = true; + Bangle.drawWidgets(); +} + +function stop(){ + state.active = false; + saveState(); + Bangle.drawWidgets(); +} + +function initState(){ + //cleanup volatile state here + state.currentPos={}; + state.steps = Bangle.getStepCount(); + state.calibAltDiff = 0; + state.up = 0; + state.down = 0; +} + +if (state.saved && state.saved < Date.now() - 60000){ + initState(); +} + +if (state.active){ + start(); +} + +WIDGETS["gpstrek"]={ + area:"tl", + width:state.active?24:0, + resetState: initState, + getState: function() { + return state; + }, + start:start, + stop:stop, + draw:function() { + if (state.active){ + g.reset(); + g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y); + } + } +}; +})(); diff --git a/apps/imageclock/ChangeLog b/apps/imageclock/ChangeLog index 262794250..f81bbf185 100644 --- a/apps/imageclock/ChangeLog +++ b/apps/imageclock/ChangeLog @@ -9,3 +9,6 @@ Show/Hide widgets with swipe up or down 0.08: Use default Bangle formatter for booleans 0.09: Support new fast app switching +0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on +0.11: Additional option in customizer to force drawing directly + Fix some problems in handling timeouts diff --git a/apps/imageclock/app.js b/apps/imageclock/app.js index 28de9d1df..ff3f5a62d 100644 --- a/apps/imageclock/app.js +++ b/apps/imageclock/app.js @@ -1,8 +1,21 @@ let unlockedDrawInterval = []; let lockedDrawInterval = []; let showWidgets = false; +let firstDraw = true; { + let x = g.getWidth()/2; + let y = g.getHeight()/2; + g.setColor(g.theme.bg); + g.fillRect(x-49, y-19, x+49, y+19); + g.setColor(g.theme.fg); + g.drawRect(x-50, y-20, x+50, y+20); + y -= 4; + x -= 4*6; + g.setFont("6x8"); + g.setFontAlign(-1,-1); + g.drawString("Loading...", x, y); + let watchface = require("Storage").readJSON("imageclock.face.json"); let watchfaceResources = require("Storage").readJSON("imageclock.resources.json"); let precompiledJs = eval(require("Storage").read("imageclock.draw.js")); @@ -12,37 +25,37 @@ let showWidgets = false; let startPerfLog = () => {}; let endPerfLog = () => {}; - let printPerfLog = () => print("Deactivated"); - let resetPerfLog = () => {performanceLog = {};}; + Bangle.printPerfLog = () => {print("Deactivated");}; + Bangle.resetPerfLog = () => {performanceLog = {};}; let colormap={ - "#000":0, - "#00f":1, - "#0f0":2, - "#0ff":3, - "#f00":4, - "#f0f":5, - "#ff0":6, - "#fff":7 + "#000":0, + "#00f":1, + "#0f0":2, + "#0ff":3, + "#f00":4, + "#f0f":5, + "#ff0":6, + "#fff":7 }; let palette = new Uint16Array([ - 0x0000, //black #000 - 0x001f, //blue #00f - 0x07e0, //green #0f0 - 0x07ff, //cyan #0ff - 0xf800, //red #f00 - 0xf81f, //magenta #f0f - 0xffe0, //yellow #ff0 - 0xffff, //white #fff - 0xffff, //white - 0xffff, //white - 0xffff, //white - 0xffff, //white - 0xffff, //white - 0xffff, //white - 0xffff, //white - 0xffff, //white + 0x0000, //black #000 + 0x001f, //blue #00f + 0x07e0, //green #0f0 + 0x07ff, //cyan #0ff + 0xf800, //red #f00 + 0xf81f, //magenta #f0f + 0xffe0, //yellow #ff0 + 0xffff, //white #fff + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white ]); let p0 = g; @@ -67,7 +80,7 @@ let showWidgets = false; performanceLog.count[name]++; }; - printPerfLog = function(){ + Bangle.printPerfLog = function(){ let result = ""; let keys = []; for (let c in performanceLog.cum){ @@ -79,23 +92,33 @@ let showWidgets = false; } }; } - - let delayTimeouts = []; - function delay(t) { + startPerfLog("loadFunctions"); + + let delayTimeouts = {}; + let timeoutCount = 0; + + let delay = function(t) { return new Promise(function (resolve) { - delayTimeouts.push(setTimeout(resolve, t)); + const i = timeoutCount++; + let timeout = setTimeout(()=>{ + resolve(); + delete delayTimeouts[i]; + }, t); + delayTimeouts[i] = timeout; + //print("Add delay timeout", delayTimeouts); }); - } + }; - function cleanupDelays(){ + let cleanupDelays = function(){ + //print("Cleanup delays", delayTimeouts); for (let t of delayTimeouts){ clearTimeout(t); } - delayTimeouts = []; - } + delayTimeouts = {}; + }; - function prepareImg(resource){ + let prepareImg = function(resource){ startPerfLog("prepareImg"); //print("prepareImg: ", resource); @@ -104,15 +127,15 @@ let showWidgets = false; delete resource.dataOffset; delete resource.dataLength; if (resource.paletteData){ - result.palette = new Uint16Array(resource.paletteData); + resource.palette = new Uint16Array(resource.paletteData); delete resource.paletteData; } } endPerfLog("prepareImg"); return resource; - } + }; - function getByPath(object, path, lastElem){ + let getByPath = function(object, path, lastElem){ startPerfLog("getByPath"); //print("getByPath", path,lastElem); let current = object; @@ -133,23 +156,24 @@ let showWidgets = false; return undefined; } return current; - } + }; - function splitNumberToDigits(num){ + let splitNumberToDigits = function(num){ return String(num).split('').map(item => Number(item)); - } + }; - function isChangedNumber(element){ + let isChangedNumber = function(element){ return element.lastDrawnValue != getValue(element.Value); - } + }; - function isChangedMultistate(element){ + let isChangedMultistate = function(element){ return element.lastDrawnValue != getMultistate(element.Value); - } + }; - function drawNumber(graphics, resources, element){ + let drawNumber = function(graphics, resources, element){ startPerfLog("drawNumber"); let number = getValue(element.Value); + //print("drawNumber: ", number, element); let spacing = element.Spacing ? element.Spacing : 0; let unit = element.Unit; @@ -158,7 +182,6 @@ let showWidgets = false; let numberOfDigits = element.Digits; - //print("drawNumber: ", number, element); if (number) number = number.toFixed(0); let isNegative; @@ -245,7 +268,7 @@ let showWidgets = false; } else { currentDigit = 0; } - //print("Digit " + currentDigit + " " + currentX); + //print("Digit", currentDigit, currentX); drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, currentDigit + imageIndex); currentX += firstImage.width + spacing; } @@ -258,9 +281,9 @@ let showWidgets = false; element.lastDrawnValue = number; endPerfLog("drawNumber"); - } + }; - function drawElement(graphics, resources, pos, element, lastElem){ + let drawElement = function(graphics, resources, pos, element, lastElem){ startPerfLog("drawElement"); let cacheKey = "_"+(lastElem?lastElem:"nole"); if (!element.cachedImage) element.cachedImage={}; @@ -282,11 +305,9 @@ let showWidgets = false; } } - //print("cache ",typeof element.cachedImage[cacheKey], element.ImagePath, lastElem); + //print("cache ", typeof element.cachedImage[cacheKey], element.ImagePath, lastElem); if(element.cachedImage[cacheKey]){ - //print("drawElement ",pos, path, lastElem); - //print("resource ", resource,pos, path, lastElem); - //print("drawImage from drawElement", image, pos); + //print("drawElement ", pos, element, lastElem); let options={}; if (element.RotationValue){ options.rotate = radians(element); @@ -304,35 +325,36 @@ let showWidgets = false; endPerfLog("drawElement_g.drawImage"); } endPerfLog("drawElement"); - } + }; - function getValue(value, defaultValue){ + let getValue = function(value, defaultValue){ + startPerfLog("getValue"); if (typeof value == "string"){ return numbers[value](); } if (value == undefined) return defaultValue; + endPerfLog("getValue"); return value; - } + }; - function getMultistate(name, defaultValue){ + let getMultistate = function(name, defaultValue){ + startPerfLog("getMultistate"); if (typeof name == "string"){ return multistates[name](); } else { if (name == undefined) return defaultValue; } + endPerfLog("getMultistate"); return undefined; - } + }; - function drawScale(graphics, resources, scale){ + let drawScale = function(graphics, resources, scale){ startPerfLog("drawScale"); //print("drawScale", scale); let segments = scale.Segments; let imageIndex = scale.ImageIndex !== undefined ? scale.ImageIndex : 0; let value = scaledown(scale.Value, scale.MinValue, scale.MaxValue); - - //print("Value is ", value, "(", maxValue, ",", minValue, ")"); - let segmentsToDraw = Math.ceil(value * segments.length); for (let i = 0; i < segmentsToDraw; i++){ @@ -341,9 +363,9 @@ let showWidgets = false; scale.lastDrawnValue = segmentsToDraw; endPerfLog("drawScale"); - } + }; - function drawImage(graphics, resources, image, name){ + let drawImage = function(graphics, resources, image, name){ startPerfLog("drawImage"); //print("drawImage", image.X, image.Y, name); if (image.Value && image.Steps){ @@ -357,9 +379,9 @@ let showWidgets = false; } endPerfLog("drawImage"); - } + }; - function drawCodedImage(graphics, resources, image){ + let drawCodedImage = function(graphics, resources, image){ startPerfLog("drawCodedImage"); let code = getValue(image.Value); //print("drawCodedImage", image, code); @@ -386,9 +408,9 @@ let showWidgets = false; image.lastDrawnValue = code; startPerfLog("drawCodedImage"); - } + }; - function getWeatherCode(){ + let getWeatherCode = function(){ let jsonWeather = require("Storage").readJSON('weather.json'); let weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; @@ -396,9 +418,9 @@ let showWidgets = false; return weather.code; } return undefined; - } + }; - function getWeatherTemperature(){ + let getWeatherTemperature = function(){ let jsonWeather = require("Storage").readJSON('weather.json'); let weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; @@ -415,25 +437,25 @@ let showWidgets = false; } } return result; - } + }; - function scaledown(value, min, max){ + let scaledown = function(value, min, max){ //print("scaledown", value, min, max); let scaled = E.clip(getValue(value),getValue(min,0),getValue(max,1)); scaled -= getValue(min,0); scaled /= getValue(max,1); return scaled; - } + }; - function radians(rotation){ + let radians = function(rotation){ let value = scaledown(rotation.RotationValue, rotation.MinRotationValue, rotation.MaxRotationValue); value -= rotation.RotationOffset ? rotation.RotationOffset : 0; value *= 360; value *= Math.PI / 180; return value; - } + }; - function drawPoly(graphics, resources, element){ + let drawPoly = function(graphics, resources, element){ startPerfLog("drawPoly"); let vertices = []; @@ -463,9 +485,9 @@ let showWidgets = false; } endPerfLog("drawPoly"); - } + }; - function drawRect(graphics, resources, element){ + let drawRect = function(graphics, resources, element){ startPerfLog("drawRect"); let vertices = []; @@ -479,9 +501,9 @@ let showWidgets = false; endPerfLog("drawRect_g.fillRect"); } endPerfLog("drawRect"); - } + }; - function drawCircle(graphics, resources, element){ + let drawCircle = function(graphics, resources, element){ startPerfLog("drawCircle"); if (element.Filled){ @@ -494,7 +516,7 @@ let showWidgets = false; endPerfLog("drawCircle_g.drawCircle"); } endPerfLog("drawCircle"); - } + }; let numbers = {}; numbers.Hour = () => { return new Date().getHours(); }; @@ -547,7 +569,7 @@ let showWidgets = false; multistates.WeatherTemperatureUnit = () => { return getWeatherTemperature().unit; }; multistates.StepsGoal = () => { return (numbers.Steps() >= (settings.stepsgoal || 10000)) ? "on": "off"; }; - function drawMultiState(graphics, resources, element){ + let drawMultiState = function(graphics, resources, element){ startPerfLog("drawMultiState"); //print("drawMultiState", element); let value = multistates[element.Value](); @@ -555,7 +577,7 @@ let showWidgets = false; drawImage(graphics, resources, element, value); element.lastDrawnValue = value; endPerfLog("drawMultiState"); - } + }; let pulse,alt,temp,press; @@ -563,25 +585,20 @@ let showWidgets = false; let requestedDraws = 0; let isDrawing = false; - let drawingTime; - let start; let deferredTimout; - function initialDraw(resources, face){ + let initialDraw = function(resources, face){ //print("Free memory", process.memory(false).free); requestedDraws++; if (!isDrawing){ cleanupDelays(); //print(new Date().toISOString(), "Can draw,", requestedDraws, "draws requested so far"); isDrawing = true; - resetPerfLog(); requestedDraws = 0; //print(new Date().toISOString(), "Drawing start"); startPerfLog("initialDraw"); - //start = Date.now(); - drawingTime = 0; //print("Precompiled"); let promise = precompiledJs(watchfaceResources, watchface); @@ -595,8 +612,6 @@ let showWidgets = false; g.drawLine(0,24,g.getWidth(),24); } lastDrawTime = Date.now() - start; - drawingTime += Date.now() - currentDrawingTime; - //print(new Date().toISOString(), "Drawing done in", lastDrawTime.toFixed(0), "active:", drawingTime.toFixed(0)); isDrawing=false; firstDraw=false; requestRefresh = false; @@ -608,14 +623,16 @@ let showWidgets = false; if (requestedDraws > 0){ //print(new Date().toISOString(), "Had deferred drawing left, drawing again"); requestedDraws = 0; + //print("Clear deferred timeout", deferredTimout); + clearTimeout(deferredTimeout); deferredTimout = setTimeout(()=>{initialDraw(resources, face);}, 10); } } //else { //print("queued draw"); //} - } + }; - function handleHrm(e){ + let handleHrm = function(e){ if (e.confidence > 70){ pulse = e.bpm; if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){ @@ -623,9 +640,9 @@ let showWidgets = false; initialDraw(watchfaceResources, watchface); } } - } + }; - function handlePressure(e){ + let handlePressure = function(e){ alt = e.altitude; temp = e.temperature; press = e.pressure; @@ -633,42 +650,46 @@ let showWidgets = false; //print("Redrawing on pressure"); initialDraw(watchfaceResources, watchface); } - } + }; - function handleCharging(e){ + let handleCharging = function(e){ if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){ //print("Redrawing on charging"); initialDraw(watchfaceResources, watchface); } - } + }; - function getMatchedWaitingTime(time){ + let getMatchedWaitingTime = function(time){ let result = time - (Date.now() % time); - //print("Matched timeout", time, result); + //print("Matched wating time", time, result); return result; - } + }; - function setMatchedInterval(callable, time, intervalHandler, delay){ + let setMatchedInterval = function(callable, time, intervalHandler, delay){ //print("Setting matched interval for", time, intervalHandler); + if (!delay) delay = 0; let matchedTime = getMatchedWaitingTime(time + delay); return setTimeout(()=>{ let interval = setInterval(callable, time); + //print("setMatchedInterval", interval); if (intervalHandler) intervalHandler(interval); callable(); }, matchedTime); - } + }; + endPerfLog("loadFunctions"); let lastDrawTime = 0; - let firstDraw = true; + startPerfLog("loadProperties"); let lockedRedraw = getByPath(watchface, ["Properties","Redraw","Locked"]) || 60000; let unlockedRedraw = getByPath(watchface, ["Properties","Redraw","Unlocked"]) || 1000; let defaultRedraw = getByPath(watchface, ["Properties","Redraw","Default"]) || "Always"; let redrawEvents = getByPath(watchface, ["Properties","Redraw","Events"]); let clearOnRedraw = getByPath(watchface, ["Properties","Redraw","Clear"]); let events = getByPath(watchface, ["Properties","Events"]); + endPerfLog("loadProperties"); //print("events", events); //print("redrawEvents", redrawEvents); @@ -676,7 +697,7 @@ let showWidgets = false; let initialDrawTimeoutUnlocked; let initialDrawTimeoutLocked; - function handleLock(isLocked, forceRedraw){ + let handleLock = function(isLocked, forceRedraw){ //print("isLocked", Bangle.isLocked()); for (let i of unlockedDrawInterval){ //print("Clearing unlocked", i); @@ -694,6 +715,10 @@ let showWidgets = false; //print("Redrawing on unlock", isLocked); initialDraw(watchfaceResources, watchface); } + if (initialDrawTimeoutUnlocked){ + //print("clear initialDrawTimeUnlocked timet", initialDrawTimeoutUnlocked); + clearTimeout(initialDrawTimeoutUnlocked); + } initialDrawTimeoutUnlocked = setMatchedInterval(()=>{ //print("Redrawing on unlocked interval"); initialDraw(watchfaceResources, watchface); @@ -708,6 +733,10 @@ let showWidgets = false; //print("Redrawing on lock", isLocked); initialDraw(watchfaceResources, watchface); } + if (initialDrawTimeoutLocked){ + clearTimeout(initialDrawTimeoutLocked); + //print("clear initialDrawTimeLocked timet", initialDrawTimeoutLocked); + } initialDrawTimeoutLocked = setMatchedInterval(()=>{ //print("Redrawing on locked interval"); initialDraw(watchfaceResources, watchface); @@ -718,13 +747,13 @@ let showWidgets = false; Bangle.setHRMPower(0, "imageclock"); Bangle.setBarometerPower(0, 'imageclock'); } - } + }; let showWidgetsChanged = false; let currentDragDistance = 0; - function restoreWidgetDraw(){ + let restoreWidgetDraw = function(){ if (global.WIDGETS) { for (let w in global.WIDGETS) { let wd = global.WIDGETS[w]; @@ -732,9 +761,9 @@ let showWidgets = false; wd.area = originalWidgetArea[w]; } } - } + }; - function handleDrag(e){ + let handleDrag = function(e){ //print("handleDrag"); currentDragDistance += e.dy; if (Math.abs(currentDragDistance) < 10) return; @@ -757,7 +786,7 @@ let showWidgets = false; showWidgets = dragDown; initialDraw(); } - } + }; Bangle.on('drag', handleDrag); @@ -766,7 +795,7 @@ let showWidgets = false; try{ Bangle.setBarometerPower(1, 'imageclock'); } catch (e){ - print("Error during barometer power up", e); + //print("Error during barometer power up", e); } } if (!events || events.includes("HRM")) { @@ -783,7 +812,7 @@ let showWidgets = false; let originalWidgetDraw = {}; let originalWidgetArea = {}; - function clearWidgetsDraw(){ + let clearWidgetsDraw = function(){ //print("Clear widget draw calls"); if (global.WIDGETS) { originalWidgetDraw = {}; @@ -798,10 +827,7 @@ let showWidgets = false; } } - if (!global.WIDGETS) Bangle.loadWidgets(); - clearWidgetsDraw(); - - handleLock(Bangle.isLocked()); + handleLock(Bangle.isLocked(), true); Bangle.setUI({ mode : "clock", @@ -832,9 +858,19 @@ let showWidgets = false; } delete lockedDrawInterval; delete showWidgets; + delete firstDraw; + + delete Bangle.printPerfLog; + if (settings.perflog){ + delete Bangle.resetPerfLog; + delete performanceLog; + } cleanupDelays(); restoreWidgetDraw(); } }); + + Bangle.loadWidgets(); + clearWidgetsDraw(); } diff --git a/apps/imageclock/custom.html b/apps/imageclock/custom.html index af7a1835f..e595b51ca 100644 --- a/apps/imageclock/custom.html +++ b/apps/imageclock/custom.html @@ -23,6 +23,8 @@ Options:

+ +

@@ -579,11 +581,8 @@ return result; } - function convertToCode(elements, properties, wrapInTimeouts){ + function convertToCode(elements, properties, wrapInTimeouts, forceUseOrigPlane){ var code = "(function (wr, wf) {\n"; - if (!wrapInTimeouts){ - code += "var ct=Date.now();\n"; - } code += "var lc;\n"; code += "var p = Promise.resolve();\n"; @@ -595,7 +594,7 @@ var c = elements[i].value; console.log("Check element", c); var name = c.Layer; - var plane = wrapInTimeouts ? 1 : 0; + var plane = (wrapInTimeouts && !forceUseOrigPlane) ? 1 : 0; if (typeof c.Plane == "number"){ plane = c.Plane; } @@ -610,8 +609,6 @@ console.log("Found planes", planes, "with numbers", planeNumbers) - if (wrapInTimeouts && planes == 0) planes = 1; - code += "p0 = g;\n"; for (var planeIndex = 0; planeIndex < planeNumbers.length; planeIndex++){ @@ -624,32 +621,25 @@ if (plane != 0) code += "if (!p" + plane + ") p" + plane + " = Graphics.createArrayBuffer(g.getWidth(),g.getHeight(),4,{msb:true});\n"; if (properties.Redraw && properties.Redraw.Clear){ - if (wrapInTimeouts && plane != 0){ + if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){ code += "p = p.then(()=>delay(0)).then(()=>{\n"; } else { code += "p = p.then(()=>{\n"; } - code += "var ct=Date.now();\n" if (addDebug()) code += 'print("Clear for redraw of plane ' + p + '");'+"\n"; code += 'startPerfLog("initialDraw_g.clear");'+"\n"; code += "p" + plane + ".clear(true);\n"; code += 'endPerfLog("initialDraw_g.clear");'+ "\n"; - - code += "drawingTime += Date.now() - ct;\n"; code += "});\n"; } var previousPlane = plane + 1; if (previousPlane < planeNumbers.length){ code += "p = p.then(()=>{\n"; - code += "var ct=Date.now();\n"; if (addDebug()) code += 'print("Copying of plane ' + previousPlane + ' to display");'+"\n"; //code += "g.drawImage(p" + i + ".asImage());"; code += "p0.drawImage({width: p" + previousPlane + ".getWidth(), height: p" + previousPlane + ".getHeight(), bpp: p" + previousPlane + ".getBPP(), buffer: p" + previousPlane + ".buffer, palette: palette});\n"; - - - code += "drawingTime += Date.now() - ct;\n"; code += "});\n"; } @@ -660,12 +650,6 @@ console.log("Layer elements", layername, layerElements); //code for whole layer - if (wrapInTimeouts && plane != 0){ - code += "p = p.then(()=>delay(0)).then(()=>{\n"; - } else { - code += "p = p.then(()=>{\n"; - } - code += "var ct=Date.now();\n"; if (addDebug()) code += 'print("Starting layer ' + layername + '");' + "\n"; var checkForLayerChange = false; @@ -732,14 +716,17 @@ if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n"; code += "" + colorsetting; code += (condition.length > 0 ? "if (" + condition + "){\n" : ""); + if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){ + code += "p = p.then(()=>delay(0)).then(()=>{\n"; + } else { + code += "p = p.then(()=>{\n"; + } if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n"; code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n"; + code += "});\n"; code += (condition.length > 0 ? "}\n" : ""); } - - code += "drawingTime += Date.now() - ct;\n"; - code += "});\n"; } console.log("Current plane is", plane); @@ -759,7 +746,7 @@ var properties = faceJson.Properties; faceJson = { Properties: properties, Collapsed: collapseTree(faceJson,{X:0,Y:0})}; console.log("After collapsing", faceJson); - precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked); + precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked); console.log("After precompiling", precompiledJs); } @@ -1011,6 +998,10 @@ } } + document.getElementById("timeoutwrap").addEventListener("click", function() { + document.getElementById("forceOrigPlane").disabled = !document.getElementById("timeoutwrap").checked; + }); + document.getElementById("btnSave").addEventListener("click", function() { var h = document.createElement('a'); h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson)); diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json index 8a3fd5939..e068b9fa7 100644 --- a/apps/imageclock/metadata.json +++ b/apps/imageclock/metadata.json @@ -2,7 +2,7 @@ "id": "imageclock", "name": "Imageclock", "shortName": "Imageclock", - "version": "0.09", + "version": "0.11", "type": "clock", "description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.", "icon": "app.png", diff --git a/apps/linuxclock/ChangeLog b/apps/linuxclock/ChangeLog index 3bc4ef732..3f1ef5c55 100644 --- a/apps/linuxclock/ChangeLog +++ b/apps/linuxclock/ChangeLog @@ -1 +1,2 @@ -0.01: New App. \ No newline at end of file +0.01: New App. +0.02: Performance improvements. \ No newline at end of file diff --git a/apps/linuxclock/app.js b/apps/linuxclock/app.js index c38443fe1..02676310e 100644 --- a/apps/linuxclock/app.js +++ b/apps/linuxclock/app.js @@ -55,18 +55,18 @@ var H = g.getHeight(); show: function() { dateMenu.items[0].emit("redraw"); }, hide: function () {} }, + { name: "day", + get: () => ({ text: getDay(), img: null}), + show: function() { dateMenu.items[2].emit("redraw"); }, + hide: function () {} + }, { name: "date", get: () => ({ text: getDate(), img: null}), show: function() { dateMenu.items[1].emit("redraw"); }, hide: function () {} }, - { name: "steps", - get: () => ({ text: Bangle.getHealthStatus("day").steps, img: null}), - show: function() { dateMenu.items[2].emit("redraw"); }, - hide: function () {} - }, - { name: "battery", - get: () => ({ text: E.getBattery() + (Bangle.isCharging() ? "%++" : "%"), img: null}), + { name: "week", + get: () => ({ text: weekOfYear(), img: null}), show: function() { dateMenu.items[3].emit("redraw"); }, hide: function () {} }, @@ -130,37 +130,53 @@ function getDate(){ return twoD(date.getDate()) + "." + twoD(date.getMonth()); } +function getDay(){ + var date = new Date(); + return locale.dow(date, true); +} - - /************************************************ - * Draw - */ - function draw() { - queueDraw(); - - g.clear(); - Bangle.drawWidgets(); - - drawMainScreen(); - } +function weekOfYear() { + var date = new Date(); + date.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + // January 4 is always in week 1. + var week1 = new Date(date.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 + - 3 + (week1.getDay() + 6) % 7) / 7); +} - function drawMainScreen(){ - g.setFontUbuntuMono(); - g.setFontAlign(-1, -1); +/************************************************ +* Draw +*/ +function draw() { + queueDraw(); - // Get menu item based on x - var menuItem = menu[settings.menuPosX]; - var cmd = menuItem.name.slice(0,5).toLowerCase(); - drawCmd(cmd); + g.setFontUbuntuMono(); + g.setFontAlign(-1, -1); - // Draw menu items depending on our y value - drawMenuItems(menuItem); + g.clearRect(0,24,W,H); - // And draw the cursor - drawCursor(); - } + drawMainScreen(); +} + + + +function drawMainScreen(){ + // Get menu item based on x + var menuItem = menu[settings.menuPosX]; + var cmd = menuItem.name.slice(0,5).toLowerCase(); + drawCmd(cmd); + + // Draw menu items depending on our y value + drawMenuItems(menuItem); + + // And draw the cursor + drawCursor(); +} function drawMenuItems(menuItem) { var start = parseInt(settings.menuPosY / 4) * 4; @@ -174,194 +190,197 @@ function drawMenuItems(menuItem) { } function drawCursor(){ + g.setFontUbuntuMono(); + g.setFontAlign(-1, -1); + g.setColor(g.theme.fg); + g.clearRect(0, 27 + 28, 15, H); if(!Bangle.isLocked()){ g.drawString(">", -2, ((settings.menuPosY % 4) + 1) * 27 + 28); } } - function drawText(key, value, line){ - g.setFontUbuntuMono(); - var x = 15; - var y = line * 27 + 28; - g.setColor(g.theme.fg); +function drawText(key, value, line){ + var x = 15; + var y = line * 27 + 28; - if(key){ - key = (key.toLowerCase() + " ").slice(0, 4) + "|"; - } else { - key = "" + g.setFontUbuntuMono(); + g.setFontAlign(-1, -1); + g.setColor(g.theme.fg); + + if(key){ + key = (key.toLowerCase() + " ").slice(0, 4) + "|"; + } else { + key = "" + } + + value = String(value).replace("\n", " "); + g.drawString(key + value, x, y); + + lock_input -= 1; +} + + +function drawCmd(cmd){ + var c = 0; + var x = 10; + var y = 28; + + g.setColor("#0f0"); + g.drawString("bjs", x+c, y); + c += g.stringWidth("bjs"); + + g.setColor(g.theme.fg); + g.drawString(":", x+c, y); + c += g.stringWidth(":"); + + g.setColor("#0ff"); + g.drawString("$ ", x+c, y); + c += g.stringWidth("$ "); + + g.setColor(g.theme.fg); + g.drawString(cmd, x+c, y); +} + +function twoD(str){ + return ("0" + str).slice(-2) +} + + +/************************************************ +* Listener +*/ +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + + +Bangle.on('lock', function(isLocked) { + drawCursor(); +}); + + +Bangle.on('charging',function(charging) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + settings.menuPosX=0; + settings.menuPosY=0; + + draw(); +}); + +var lock_input = 0; + +Bangle.on('touch', function(btn, e){ + if(lock_input > 0){ + return; + } + lock_input = 0; + + var left = parseInt(g.getWidth() * 0.22); + var right = g.getWidth() - left; + var upper = parseInt(g.getHeight() * 0.22) + 20; + var lower = g.getHeight() - upper; + + var is_upper = e.y < upper; + var is_lower = e.y > lower; + var is_left = e.x < left && !is_upper && !is_lower; + var is_right = e.x > right && !is_upper && !is_lower; + var is_center = !is_upper && !is_lower && !is_left && !is_right; + + var oldYScreen = parseInt(settings.menuPosY/4); + if(is_lower){ + if(settings.menuPosY >= menu[settings.menuPosX].items.length-1){ + return; } - value = String(value).replace("\n", " "); - g.drawString(key + value, x, y); + Bangle.buzz(40, 0.6); + settings.menuPosY++; + if(parseInt(settings.menuPosY/4) == oldYScreen){ + drawCursor(); + return; + } + } - lock_input -= 1; - } - - - function drawCmd(cmd){ - var c = 0; - var x = 10; - var y = 28; - - g.setColor("#0f0"); - g.drawString("bjs", x+c, y); - c += g.stringWidth("bjs"); - - g.setColor(g.theme.fg); - g.drawString(":", x+c, y); - c += g.stringWidth(":"); - - g.setColor("#0ff"); - g.drawString("~", x+c, y); - c += g.stringWidth("~"); - - g.setColor(g.theme.fg); - g.drawString("$", x+c, y); - c += g.stringWidth("$ "); - - g.drawString(cmd, x+c, y); - } - - function twoD(str){ - return ("0" + str).slice(-2) - } - - - /************************************************ - * Listener - */ - // timeout used to update every minute - var drawTimeout; - - // schedule a draw for the next minute - function queueDraw() { - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function() { - drawTimeout = undefined; - draw(); - }, 60000 - (Date.now() % 60000)); - } - - - // Stop updates when LCD is off, restart when on - Bangle.on('lcdPower',on=>{ - if (on) { - draw(); // draw immediately, queue redraw - } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - } - }); - - - Bangle.on('lock', function(isLocked) { - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - - draw(); - }); - - - Bangle.on('charging',function(charging) { - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - - draw(); - }); - - var lock_input = 0; - - Bangle.on('touch', function(btn, e){ - if(lock_input > 0){ - return; - } - lock_input = 0; - - var left = parseInt(g.getWidth() * 0.22); - var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.22) + 20; - var lower = g.getHeight() - upper; - - var is_upper = e.y < upper; - var is_lower = e.y > lower; - var is_left = e.x < left && !is_upper && !is_lower; - var is_right = e.x > right && !is_upper && !is_lower; - var is_center = !is_upper && !is_lower && !is_left && !is_right; - - var oldYScreen = parseInt(settings.menuPosY/4); - if(is_lower){ - if(settings.menuPosY >= menu[settings.menuPosX].items.length-1){ - return; - } - - Bangle.buzz(40, 0.6); - settings.menuPosY++; - if(parseInt(settings.menuPosY/4) == oldYScreen){ - drawCursor(); - return; - } - } - - if(is_upper){ - if(e.y < 20){ // Reserved for widget clicks - return; - } - - if(settings.menuPosY <= 0){ - return; - } - Bangle.buzz(40, 0.6); - settings.menuPosY--; - settings.menuPosY = settings.menuPosY < 0 ? 0 : settings.menuPosY; - - if(parseInt(settings.menuPosY/4) == oldYScreen){ - drawCursor(); - return; - } - } - - if(is_right){ - Bangle.buzz(40, 0.6); - settings.menuPosX = (settings.menuPosX+1) % menu.length; - settings.menuPosY = 0; - } - - if(is_left){ - Bangle.buzz(40, 0.6); - settings.menuPosY = 0; - settings.menuPosX = settings.menuPosX-1; - settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; - } - - if(is_center){ - if(!canRunMenuItem()){ + if(is_upper){ + if(e.y < 20){ // Reserved for widget clicks return; } - runMenuItem(); - } - draw(); - }); + if(settings.menuPosY <= 0){ + return; + } + Bangle.buzz(40, 0.6); + settings.menuPosY--; + settings.menuPosY = settings.menuPosY < 0 ? 0 : settings.menuPosY; - E.on("kill", function(){ - try{ - storage.write(SETTINGS_FILE, settings); - } catch(ex){ - // If this fails, we still kill the app... - } - }); + if(parseInt(settings.menuPosY/4) == oldYScreen){ + drawCursor(); + return; + } + } + + if(is_right){ + Bangle.buzz(40, 0.6); + settings.menuPosX = (settings.menuPosX+1) % menu.length; + settings.menuPosY = 0; + } + + if(is_left){ + Bangle.buzz(40, 0.6); + settings.menuPosY = 0; + settings.menuPosX = settings.menuPosX-1; + settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; + } + + if(is_center){ + if(!canRunMenuItem()){ + return; + } + runMenuItem(); + } + + draw(); +}); + +E.on("kill", function(){ + try{ + storage.write(SETTINGS_FILE, settings); + } catch(ex){ + // If this fails, we still kill the app... + } +}); - /************************************************ - * Startup Clock - */ +/************************************************ +* Startup Clock +*/ +// Show launcher when middle button pressed +Bangle.setUI("clock"); - // Show launcher when middle button pressed - Bangle.setUI("clock"); +// Load and draw widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); - // Load and draw widgets - Bangle.loadWidgets(); - - // Draw first time - draw(); +// Draw first time +draw(); diff --git a/apps/linuxclock/metadata.json b/apps/linuxclock/metadata.json index 73bf575ed..dfb17a315 100644 --- a/apps/linuxclock/metadata.json +++ b/apps/linuxclock/metadata.json @@ -1,7 +1,7 @@ { "id": "linuxclock", "name": "Linux Clock", - "version": "0.01", + "version": "0.02", "description": "A Linux inspired clock.", "readme": "README.md", "icon": "app.png", diff --git a/apps/linuxclock/screenshot.png b/apps/linuxclock/screenshot.png index 9e3b5f3b6..4bc7f9967 100644 Binary files a/apps/linuxclock/screenshot.png and b/apps/linuxclock/screenshot.png differ diff --git a/apps/linuxclock/screenshot_2.png b/apps/linuxclock/screenshot_2.png index d993f35a5..abeba7a92 100644 Binary files a/apps/linuxclock/screenshot_2.png and b/apps/linuxclock/screenshot_2.png differ diff --git a/apps/powersave/ChangeLog b/apps/powersave/ChangeLog index a0e6da646..28d913cc8 100644 --- a/apps/powersave/ChangeLog +++ b/apps/powersave/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial release -0.02: Removed accelerometer poll interval adjustment, fixed a few issues with detecting the current app \ No newline at end of file +0.02: Removed accelerometer poll interval adjustment, fixed a few issues with detecting the current app +0.03: Fix a couple of silly mistakes \ No newline at end of file diff --git a/apps/powersave/boot.js b/apps/powersave/boot.js index 5c37dcc56..f37fbc536 100644 --- a/apps/powersave/boot.js +++ b/apps/powersave/boot.js @@ -12,7 +12,7 @@ E.on("init", () => { Storage.write("powersave.json", { app: __FILE__ }); - }else{ + }else if(!("__FILE__" in global)){ Storage.write("powersave.json", { app: null }); diff --git a/apps/powersave/metadata.json b/apps/powersave/metadata.json index cb2ad9456..705384058 100644 --- a/apps/powersave/metadata.json +++ b/apps/powersave/metadata.json @@ -1,7 +1,7 @@ { "id": "powersave", "name": "Power Save", - "version": "0.02", + "version": "0.03", "description": "Halts foreground app execution while screen is off while still allowing background processes.", "readme": "README.md", "icon": "powersave.png", @@ -10,7 +10,7 @@ "supports": ["BANGLEJS2"], "storage": [ {"name":"powersave.boot.js","url":"boot.js"}, - {"name":"powersave.screen.js","url":"boot.js"} + {"name":"powersave.screen.js","url":"screen.js"} ], "data": [ {"name": "powersave.json"} diff --git a/apps/primetime/README.md b/apps/primetime/README.md new file mode 100644 index 000000000..a07c19f52 --- /dev/null +++ b/apps/primetime/README.md @@ -0,0 +1,10 @@ +# App Name + +Watchface that displays time and the prime factors of the "military time" (i.e. 21:05 => 2105, shows prime factors of 2105 which are 5 & 421). Displays "Prime Time!" if prime. + +![image](https://user-images.githubusercontent.com/115424919/194777279-7f5e4d2a-f475-4099-beaf-38db5b460714.png) + + +## Creator + +Adapted from simplestclock by [Eve Bury](https://www.github.com/eveeeon) diff --git a/apps/primetime/app.png b/apps/primetime/app.png new file mode 100644 index 000000000..5024727fb Binary files /dev/null and b/apps/primetime/app.png differ diff --git a/apps/primetime/metadata.json b/apps/primetime/metadata.json new file mode 100644 index 000000000..d796d290c --- /dev/null +++ b/apps/primetime/metadata.json @@ -0,0 +1,15 @@ +{ "id": "primetime", + "name": "Prime Time Clock", + "version": "0.01", + "type": "clock", + "description": "A clock that tells you the primes of the time", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"primetime.app.js","url":"primetime.js"}, + {"name":"primetime.img","url":"primetime-icon.js","evaluate":true} + ] +} diff --git a/apps/primetime/primetime-icon.js b/apps/primetime/primetime-icon.js new file mode 100644 index 000000000..57969a68b --- /dev/null +++ b/apps/primetime/primetime-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgVVABVADJMBBf4L/Bf4LMgtQgIHCitAqoHBoEv+EHwALBv/S//4BYO//svwELoP//X/+gLB2E93+Ah9B9f+//QBYMVvv3C4XvvwLDl/0q+AgsB998qt4F4XgHYIXB/1+6ALC//93/4F4I7CI4QLBAIMLoF/6ABBBYNVqgBBgprCAIKz0qkAooLHgP8gXvvALH/EL7e4BY+tz/+vovH3PR1++L9YL/BYdVABQ=")) diff --git a/apps/primetime/primetime.js b/apps/primetime/primetime.js new file mode 100644 index 000000000..bba63bc48 --- /dev/null +++ b/apps/primetime/primetime.js @@ -0,0 +1,89 @@ +const h = g.getHeight(); +const w = g.getWidth(); + + + +// creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!" +function primeFactors(n) { + const factors = []; + let divisor = 2; + + while (n >= 2) { + if (n % divisor == 0) { + factors.push(divisor); + n = n / divisor; + } else { + divisor++; + } + } + if (factors.length === 1) { + return "Prime Time!"; + } + else + return factors.toString(); +} + + +// converts time HR:MIN to integer HRMIN e.g. 15:35 => 1535 +function timeToInt(t) { + var arr = t.split(':'); + var intTime = parseInt(arr[0])*100+parseInt(arr[1]); + + return intTime; +} + + + +function draw() { + var date = new Date(); + var timeStr = require("locale").time(date,1); + var primeStr = primeFactors(timeToInt(timeStr)); + + g.reset(); + g.setColor(0,0,0); + g.fillRect(Bangle.appRect); + + g.setFont("6x8", w/30); + g.setFontAlign(0, 0); + g.setColor(100,100,100); + g.drawString(timeStr, w/2, h/2); + g.setFont("6x8", w/60); + g.drawString(primeStr, w/2, 3*h/4); + queueDraw(); +} + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +g.clear(); + +// Show launcher when middle button pressed +// Bangle.setUI("clock"); +// use clockupdown as it tests for issue #1249 +Bangle.setUI("clockupdown", btn=> { + draw(); +}); + +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/primetime/screenshot.png b/apps/primetime/screenshot.png new file mode 100644 index 000000000..cb625a9b6 Binary files /dev/null and b/apps/primetime/screenshot.png differ diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/sensortools/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/sensortools/README.md b/apps/sensortools/README.md new file mode 100644 index 000000000..8b89add7c --- /dev/null +++ b/apps/sensortools/README.md @@ -0,0 +1,44 @@ +# Sensor tools + +This allows to simulate sensor behaviour for development purposes + + +## Per Sensor settings: + +enabled: + true or false +mode: + emulate: Completely craft events for this sensor + modify: Take existing events from real sensor and modify their data +name: + name of the emulation or modification mode +power: + emulate: Simulate Bangle._PWR changes, but do not call real power function + nop: Do nothing, ignore all power calls for this sensor but return true + passthrough: Just pass all power calls unmodified + on: Do not allow switching the sensor off, all calls are switching the real sensor on + +### HRM + +Modes: modify, emulate +Modification: + bpmtrippled: Multiply the bpm value of the original HRM values with 3 +Emulation: + sin: Calculate bpm changes by using sin + +### GPS + +Modes: emulate +Emulation: + staticfix: static complete fix with all values + route: A square route starting in the SW corner and moving SW->NW->NO->SW... + routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course + nofix: All values NaN but time,sattelites,fix and fix == 0 + changingfix: A fix with randomly changing values + +### Compass + +Modes: emulate +Emulation: + static: All values but heading are 1, heading == 0 + rotate: All values but heading are 1, heading rotates 360° diff --git a/apps/sensortools/boot.js b/apps/sensortools/boot.js new file mode 100644 index 000000000..82c2036a9 --- /dev/null +++ b/apps/sensortools/boot.js @@ -0,0 +1,351 @@ +(function() { + var settings = Object.assign( + require('Storage').readJSON("sensortools.default.json", true) || {}, + require('Storage').readJSON("sensortools.json", true) || {} + ); + + var log = function(text, param) { + var logline = new Date().toISOString() + " - " + "Sensortools - " + text; + if (param) logline += ": " + JSON.stringify(param); + print(logline); + }; + + if (settings.enabled) { + + log("Enabled"); + const POWER_DELAY = 10000; + + var onEvents = []; + + Bangle.sensortoolsOrigOn = Bangle.on; + Bangle.sensortoolsOrigEmit = Bangle.emit; + Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener; + + Bangle.on = function(name, callback) { + if (onEvents[name]) { + log("Redirecting listener for", name, "to", name + "_mod"); + Bangle.sensortoolsOrigOn(name + "_mod", callback); + Bangle.sensortoolsOrigOn(name, (e) => { + log("Redirected event for", name, "to", name + "_mod"); + Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e)); + }); + } else { + log("Pass through on call for", name, callback); + Bangle.sensortoolsOrigOn(name, callback); + } + }; + + Bangle.removeListener = function(name, callback) { + if (onEvents[name]) { + log("Removing augmented listener for", name, onEvents[name]); + Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback); + } else { + log("Pass through remove listener for", name); + Bangle.sensortoolsOrigRemoveListener(name, callback); + } + }; + + Bangle.emit = function(name, event) { + if (onEvents[name]) { + log("Augmenting emit call for", name, onEvents[name]); + Bangle.sensortoolsOrigEmit(name + "_mod", event); + } else { + log("Pass through emit call for", name); + Bangle.sensortoolsOrigEmit(name, event); + } + }; + + var createPowerFunction = function(type, name, origPower) { + return function(isOn, app) { + if (type == "nop") { + return true; + }else if (type == "delay") { + setTimeout(() => { + origPower(isOn, app); + }, POWER_DELAY); + } else if (type == "on") { + origPower(1, "sensortools_force_on"); + } else if (type == "passthrough"){ + origPower(isOn, "app"); + } else if (type == "emulate"){ + if (!Bangle._PWR) Bangle._PWR={}; + if (!Bangle._PWR[name]) Bangle._PWR[name] = []; + if (!app) app="?"; + if (isOn) { + Bangle._PWR[name].push(app); + return true; + } else { + Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;}); + return false; + } + } + }; + }; + + if (settings.hrm && settings.hrm.enabled) { + log("HRM", settings.hrm); + if (settings.hrm.power) { + log("HRM power"); + Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower; + Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower); + } + if (settings.hrm.mode == "modify") { + if (settings.hrm.name == "bpmtrippled") { + onEvents.HRM = (e) => { + return { + bpm: e.bpm * 3 + }; + }; + } + } else if (settings.hrm.mode == "emulate") { + if (settings.hrm.name == "sin") { + setInterval(() => { + Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000)); + }, 1000); + } + } + } + if (settings.gps && settings.gps.enabled) { + log("GPS", settings.gps); + let modGps = function(dataProvider) { + Bangle.getGPSFix = dataProvider; + setInterval(() => { + Bangle.sensortoolsOrigEmit("GPS", dataProvider()); + }, 1000); + }; + if (settings.gps.power) { + Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower; + Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower); + } + if (settings.gps.mode == "emulate") { + function radians(a) { + return a*Math.PI/180; + } + + function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; + } + + function bearing(a,b){ + if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; + 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))); + } + + function interpolate(a,b,progress){ + return { + lat: a.lat * progress + b.lat * (1-progress), + lon: a.lon * progress + b.lon * (1-progress), + ele: a.ele * progress + b.ele * (1-progress) + } + } + + function getSquareRoute(){ + return [ + {lat:"47.2577411",lon:"11.9927442",ele:2273}, + {lat:"47.266761",lon:"11.9926673",ele:2166}, + {lat:"47.2667605",lon:"12.0059511",ele:2245}, + {lat:"47.2577516",lon:"12.0059925",ele:1994} + ]; + } + function getSquareRouteFuzzy(){ + return [ + {lat:"47.2578455",lon:"11.9929891",ele:2265}, + {lat:"47.258592",lon:"11.9923341",ele:2256}, + {lat:"47.2594506",lon:"11.9927412",ele:2230}, + {lat:"47.2603323",lon:"11.9924949",ele:2219}, + {lat:"47.2612056",lon:"11.9928175",ele:2199}, + {lat:"47.2621002",lon:"11.9929817",ele:2182}, + {lat:"47.2629025",lon:"11.9923915",ele:2189}, + {lat:"47.2637828",lon:"11.9926486",ele:2180}, + {lat:"47.2646733",lon:"11.9928167",ele:2191}, + {lat:"47.2655617",lon:"11.9930357",ele:2185}, + {lat:"47.2662862",lon:"11.992252",ele:2186}, + {lat:"47.2669305",lon:"11.993173",ele:2166}, + {lat:"47.266666",lon:"11.9944419",ele:2171}, + {lat:"47.2667579",lon:"11.99576",ele:2194}, + {lat:"47.2669409",lon:"11.9970579",ele:2207}, + {lat:"47.2666562",lon:"11.9983128",ele:2212}, + {lat:"47.2666027",lon:"11.9996335",ele:2262}, + {lat:"47.2667245",lon:"12.0009395",ele:2278}, + {lat:"47.2668457",lon:"12.002256",ele:2297}, + {lat:"47.2666126",lon:"12.0035373",ele:2303}, + {lat:"47.2664554",lon:"12.004841",ele:2251}, + {lat:"47.2669461",lon:"12.005948",ele:2245}, + {lat:"47.2660877",lon:"12.006323",ele:2195}, + {lat:"47.2652729",lon:"12.0057552",ele:2163}, + {lat:"47.2643926",lon:"12.0060123",ele:2131}, + {lat:"47.2634978",lon:"12.0058302",ele:2095}, + {lat:"47.2626129",lon:"12.0060759",ele:2066}, + {lat:"47.2617325",lon:"12.0058188",ele:2037}, + {lat:"47.2608668",lon:"12.0061784",ele:1993}, + {lat:"47.2600155",lon:"12.0057392",ele:1967}, + {lat:"47.2591203",lon:"12.0058233",ele:1949}, + {lat:"47.2582307",lon:"12.0059718",ele:1972}, + {lat:"47.2578014",lon:"12.004804",ele:2011}, + {lat:"47.2577232",lon:"12.0034834",ele:2044}, + {lat:"47.257745",lon:"12.0021656",ele:2061}, + {lat:"47.2578682",lon:"12.0008597",ele:2065}, + {lat:"47.2577082",lon:"11.9995526",ele:2071}, + {lat:"47.2575917",lon:"11.9982348",ele:2102}, + {lat:"47.2577401",lon:"11.996924",ele:2147}, + {lat:"47.257715",lon:"11.9956061",ele:2197}, + {lat:"47.2578996",lon:"11.9943081",ele:2228} + ]; + } + + if (settings.gps.name == "staticfix") { + modGps(() => { return { + "lat": 52, + "lon": 8, + "alt": 100, + "speed": 10, + "course": 12, + "time": Date.now(), + "satellites": 7, + "fix": 1, + "hdop": 1 + };}); + } else if (settings.gps.name.includes("route")) { + let route; + let interpSteps; + if (settings.gps.name == "routeFuzzy"){ + route = getSquareRouteFuzzy(); + interpSteps = 5; + } else { + route = getSquareRoute(); + interpSteps = 50; + } + + let step = 0; + let routeIndex = 0; + modGps(() => { + let newIndex = (routeIndex + 1)%route.length; + + let result = { + "speed": Math.random() * 3 + 2, + "time": Date.now(), + "satellites": Math.floor(Math.random()*5)+3, + "fix": 1, + "hdop": Math.floor(Math.random(30)+1) + }; + + let oldPos = route[routeIndex]; + if (step != 0){ + oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps)); + } + let newPos = route[newIndex]; + if (step < interpSteps - 1){ + newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps)); + } + + if (step == interpSteps - 1){ + let followingIndex = (routeIndex + 2)%route.length; + newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps)); + } + + result.lat = oldPos.lat; + result.lon = oldPos.lon; + result.alt = oldPos.ele; + + result.course = bearing(oldPos,newPos); + + step++; + if (step == interpSteps){ + routeIndex = (routeIndex + 1) % route.length; + step = 0; + } + return result; + }); + } else if (settings.gps.name == "nofix") { + modGps(() => { return { + "lat": NaN, + "lon": NaN, + "alt": NaN, + "speed": NaN, + "course": NaN, + "time": Date.now(), + "satellites": 2, + "fix": 0, + "hdop": NaN + };}); + } else if (settings.gps.name == "changingfix") { + let currentSpeed=1; + let currentLat=20; + let currentLon=10; + let currentCourse=10; + let currentAlt=-100; + let currentSats=5; + modGps(() => { + currentLat += 0.1; + if (currentLat > 50) currentLat = 20; + currentLon += 0.1; + if (currentLon > 20) currentLon = 10; + currentSpeed *= 10; + if (currentSpeed > 1000) currentSpeed = 1; + currentCourse += 12; + if (currentCourse > 360) currentCourse -= 360; + currentSats += 1; + if (currentSats > 10) currentSats = 5; + currentAlt *= 10; + if (currentAlt > 1000) currentAlt = -100; + return { + "lat": currentLat, + "lon": currentLon, + "alt": currentAlt, + "speed": currentSpeed, + "course": currentCourse, + "time": Date.now(), + "satellites": currentSats, + "fix": 1, + "hdop": 1 + };}); + } + } + } + + if (settings.mag && settings.mag.enabled) { + log("MAG", settings.mag); + let modMag = function(data) { + setInterval(() => { + Bangle.getCompass = data; + Bangle.sensortoolsOrigEmit("mag", data()); + }, 100); + }; + if (settings.mag.power) { + Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower; + Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower); + } + if (settings.mag.mode == "emulate") { + if (settings.mag.name == "static") { + modMag(()=>{return { + x: 1, + y: 1, + z: 1, + dx: 1, + dy: 1, + dz: 1, + heading: 0 + };}); + } else if (settings.mag.name == "rotate"){ + let last = 0; + modMag(()=>{return { + x: 1, + y: 1, + z: 1, + dx: 1, + dy: 1, + dz: 1, + heading: last = (last+1)%360, + };}); + } + } + } + } +})(); diff --git a/apps/sensortools/default.json b/apps/sensortools/default.json new file mode 100644 index 000000000..a85e1ddeb --- /dev/null +++ b/apps/sensortools/default.json @@ -0,0 +1,18 @@ +{ + "enabled": false, + "mag": { + "enabled": false, + "mode": "emulate", + "name": "static" + }, + "hrm": { + "enabled": false, + "mode": "modify", + "name": "bpmtrippled" + }, + "gps": { + "enabled": false, + "mode": "emulate", + "name": "changingfix" + } +} diff --git a/apps/sensortools/icon.png b/apps/sensortools/icon.png new file mode 100644 index 000000000..b7b5ec9ea Binary files /dev/null and b/apps/sensortools/icon.png differ diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json new file mode 100644 index 000000000..550fa5edc --- /dev/null +++ b/apps/sensortools/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "sensortools", + "name": "Sensor tools", + "shortName": "Sensor tools", + "version": "0.01", + "description": "Tools for testing and debugging apps that use sensor input", + "icon": "icon.png", + "type": "bootloader", + "tags": "tool,boot,debug", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"sensortools.0.boot.js","url":"boot.js"}, + {"name":"sensortools.settings.js","url":"settings.js"}, + {"name":"sensortools.default.json","url":"default.json"} + ] +} diff --git a/apps/sensortools/settings.js b/apps/sensortools/settings.js new file mode 100644 index 000000000..231ab8467 --- /dev/null +++ b/apps/sensortools/settings.js @@ -0,0 +1,99 @@ +(function(back) { + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function writeSettingsParent(parent, key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + if (!s[parent]) s[parent] = {}; + s[parent][key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = Object.assign( + require('Storage').readJSON("sensortools.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + var FILE="sensortools.json"; + var settings; + readSettings(); + + + let modes = ["nop", "emulate", "modify"]; + let modesPower = ["nop", "emulate", "passthrough", "delay", "on"]; + + function showSubMenu(name,key,typesEmulate,typesModify){ + var menu = { + '': { 'title': name, + back: ()=>{E.showMenu(buildMainMenu());}}, + 'Enabled': { + value: !!settings[key].enabled, + onchange: v => { + writeSettingsParent(key, "enabled",v); + } + }, + 'Mode': { + value: modes.indexOf(settings[key].mode||"nop"), + min: 0, max: modes.length-1, + format: v => { return modes[v]; }, + onchange: v => { + writeSettingsParent(key,"mode",modes[v]); + showSubMenu(name,key,typesEmulate,typesModify); + } + }, + 'Name': {}, + 'Power': { + value: modesPower.indexOf(settings[key].power||"nop"), + min: 0, max: modesPower.length-1, + format: v => { return modesPower[v]; }, + onchange: v => { + writeSettingsParent(key,"power",modesPower[v]); + } + }, + }; + + if (settings[key].mode != "nop"){ + let types = typesEmulate; + if (settings[key].mode == "modify") types = typesModify; + menu.Name = { + value: types.indexOf(settings[key].name||"static"), + min: 0, max: types.length-1, + format: v => { return types[v]; }, + onchange: v => { + writeSettingsParent(key,"name",types[v]); + } + }; + } else { + delete menu.Name; + } + + E.showMenu(menu); + } + + + function buildMainMenu(){ + var mainmenu = { + '': { 'title': 'Sensor tools' }, + '< Back': back, + 'Enabled': { + value: !!settings.enabled, + onchange: v => { + writeSettings("enabled",v); + }, + }, + 'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);}, + 'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);}, + 'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);} + }; + return mainmenu; + } + + E.showMenu(buildMainMenu()); +}); diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 572aaa91e..b09100f50 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1,3 +1,4 @@ 0.01: Release 0.02: Rewrite with new interface -0.03: Added clock infos to expose timer functionality to clocks. \ No newline at end of file +0.03: Added clock infos to expose timer functionality to clocks. +0.04: Improvements of clock infos. \ No newline at end of file diff --git a/apps/smpltmr/clkinfo.js b/apps/smpltmr/clkinfo.js index dfc70aab9..1a63a9b7e 100644 --- a/apps/smpltmr/clkinfo.js +++ b/apps/smpltmr/clkinfo.js @@ -69,7 +69,7 @@ img: img, items: [ { - name: "Timer", + name: null, get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: null}), show: function() { smpltmrItems.items[0].emit("redraw"); }, hide: function () {}, @@ -78,17 +78,18 @@ ] }; - var offsets = [+1,+5,-1,-5]; + var offsets = [+5,-5]; offsets.forEach((o, i) => { smpltmrItems.items = smpltmrItems.items.concat({ - name: String(o), - get: () => ({ text: getAlarmMinutesText() + " (" + (o > 0 ? "+" : "") + o + ")", img: null}), + name: null, + get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: null}), show: function() { smpltmrItems.items[i+1].emit("redraw"); }, hide: function () {}, run: function() { if(o > 0) increaseAlarm(o); else decreaseAlarm(Math.abs(o)); this.show(); + return true; } }); }); diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 4a219fad2..5f1329dfc 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,7 +2,7 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.03", + "version": "0.04", "description": "A very simple app to start a timer.", "icon": "app.png", "tags": "tool,alarm,timer", diff --git a/apps/widcloselaunch/ChangeLog b/apps/widcloselaunch/ChangeLog new file mode 100644 index 000000000..4be6afb16 --- /dev/null +++ b/apps/widcloselaunch/ChangeLog @@ -0,0 +1 @@ +0.01: New widget! \ No newline at end of file diff --git a/apps/widcloselaunch/README.md b/apps/widcloselaunch/README.md new file mode 100644 index 000000000..1eb384ce1 --- /dev/null +++ b/apps/widcloselaunch/README.md @@ -0,0 +1,9 @@ +# Close Button Launcher + +Adds a ![X](preview.png) button to close the current app and go back to the launcher. +(Widget is not visible on the clock screen) + +Copied from widclose by @rigrig and slightly modified. + +![Light theme screenshot](screenshot_light.png) +![Dark theme screenshot](screenshot_dark.png) diff --git a/apps/widcloselaunch/icon.png b/apps/widcloselaunch/icon.png new file mode 100644 index 000000000..1d95ba0ce Binary files /dev/null and b/apps/widcloselaunch/icon.png differ diff --git a/apps/widcloselaunch/metadata.json b/apps/widcloselaunch/metadata.json new file mode 100644 index 000000000..36d06bd91 --- /dev/null +++ b/apps/widcloselaunch/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widcloselaunch", + "name": "Close Button to launcher", + "version": "0.01", + "description": "A button to close the current app and go to launcher", + "readme": "README.md", + "icon": "icon.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}], + "storage": [ + {"name":"widcloselaunch.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widcloselaunch/preview.png b/apps/widcloselaunch/preview.png new file mode 100644 index 000000000..d90a3b4c5 Binary files /dev/null and b/apps/widcloselaunch/preview.png differ diff --git a/apps/widcloselaunch/screenshot_dark.png b/apps/widcloselaunch/screenshot_dark.png new file mode 100644 index 000000000..58067a3b9 Binary files /dev/null and b/apps/widcloselaunch/screenshot_dark.png differ diff --git a/apps/widcloselaunch/screenshot_light.png b/apps/widcloselaunch/screenshot_light.png new file mode 100644 index 000000000..32817ea8d Binary files /dev/null and b/apps/widcloselaunch/screenshot_light.png differ diff --git a/apps/widcloselaunch/widget.js b/apps/widcloselaunch/widget.js new file mode 100644 index 000000000..ee5a9a7e3 --- /dev/null +++ b/apps/widcloselaunch/widget.js @@ -0,0 +1,14 @@ +if (!Bangle.CLOCK) WIDGETS.close = { + area: "tr", width: 24, sortorder: 10, // we want the right-most spot please + draw: function() { + Bangle.removeListener("touch", this.touch); + Bangle.on("touch", this.touch); + g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button + // b/w version of preview.png, 24x24 + "GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA" + ), this.x, this.y); + }, touch: function(_, c) { + const w = WIDGETS.close; + if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) Bangle.showLauncher(); + } +}; diff --git a/modules/clock_info.js b/modules/clock_info.js index 6a810371a..76a93ba49 100644 --- a/modules/clock_info.js +++ b/modules/clock_info.js @@ -97,10 +97,14 @@ exports.load = function() { // In case there exists already a menu object b with the same name as the next // object a, we append the items. Otherwise we add the new object a to the list. require("Storage").list(/clkinfo.js$/).forEach(fn => { - var a = eval(require("Storage").read(fn))(); - var b = menu.find(x => x.name === a.name) - if(b) b.items = b.items.concat(a.items); - else menu = menu.concat(a); + try{ + var a = eval(require("Storage").read(fn))(); + var b = menu.find(x => x.name === a.name) + if(b) b.items = b.items.concat(a.items); + else menu = menu.concat(a); + } catch(e){ + console.log("Could not load clock info.") + } }); // return it all!