diff --git a/CHANGELOG.md b/CHANGELOG.md index 6368c2c46..95e973e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,5 +6,7 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong * Added optional `README.md` file for apps * Remove 2v04 version warning, add links in About to official/developer versions -* Fix issue removing an app that was just installed (Fix #253) -* Add `Favourite` functionality +* Fix issue removing an app that was just installed (fix #253) +* Add `Favourite` functionality +* Version number now clickable even when you're at the latest version (fix #291) +* Rewrite 'getInstalledApps' to minimize RAM usage diff --git a/apps.json b/apps.json index ca2517806..7449a233b 100644 --- a/apps.json +++ b/apps.json @@ -78,7 +78,7 @@ { "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.06", + "version":"0.07", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator":true, @@ -86,13 +86,14 @@ {"name":"welcome.boot.js","url":"boot.js"}, {"name":"welcome.app.js","url":"app.js"}, {"name":"welcome.settings.js","url":"settings.js"}, + {"name":"welcome.settings.json","url":"settings-default.json","evaluate":true}, {"name":"welcome.img","url":"app-icon.js","evaluate":true} ] }, { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.08", + "version":"0.09", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "type":"widget", @@ -119,7 +120,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.13", + "version":"0.15", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ @@ -279,7 +280,7 @@ { "id": "gpsrec", "name": "GPS Recorder", "icon": "app.png", - "version":"0.06", + "version":"0.07", "interface": "interface.html", "description": "Application that allows you to record a GPS track. Can run in background", "tags": "tool,outdoors,gps,widget", @@ -329,7 +330,7 @@ { "id": "widbat", "name": "Battery Level Widget", "icon": "widget.png", - "version":"0.04", + "version":"0.05", "description": "Show the current battery level and charging status in the top right of the clock", "tags": "widget,battery", "type":"widget", @@ -341,7 +342,7 @@ "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", "icon": "widget.png", - "version":"0.08", + "version":"0.09", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type":"widget", @@ -354,7 +355,7 @@ { "id": "widbt", "name": "Bluetooth Widget", "icon": "widget.png", - "version":"0.03", + "version":"0.04", "description": "Show the current Bluetooth connection status in the top right of the clock", "tags": "widget,bluetooth", "type":"widget", @@ -362,6 +363,18 @@ {"name":"widbt.wid.js","url":"widget.js"} ] }, + { "id": "widram", + "name": "RAM Widget", + "shortName":"RAM Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Display your Bangle's available RAM percentage in a widget", + "tags": "widget", + "type": "widget", + "storage": [ + {"name":"widram.wid.js","url":"widget.js"} + ] + }, { "id": "hrm", "name": "Heart Rate Monitor", "icon": "heartrate.png", @@ -504,13 +517,14 @@ "id": "ncstart", "name": "NCEU Startup", "icon": "start.png", - "version":"0.03", + "version":"0.04", "description": "NodeConfEU 2019 'First Start' Sequence", "tags": "start,welcome", "storage": [ {"name":"ncstart.app.js","url":"start.js"}, {"name":"ncstart.boot.js","url":"boot.js"}, {"name":"ncstart.settings.js","url":"settings.js"}, + {"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true}, {"name":"ncstart.img","url":"start-icon.js","evaluate":true}, {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, @@ -775,7 +789,7 @@ { "id": "widclk", "name": "Digital clock widget", "icon": "widget.png", - "version":"0.03", + "version":"0.04", "description": "A simple digital clock widget", "tags": "widget,clock", "type":"widget", @@ -915,7 +929,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.09", + "version":"0.12", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", @@ -1108,6 +1122,20 @@ {"name":"openstmap.app.js","url":"app.js"}, {"name":"openstmap.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "activepedom", + "name": "Active Pedometer", + "shortName":"Active Pedometer", + "icon": "app.png", + "version":"0.01", + "description": "Pedometer that filters out arm movement and displays a step goal progress.", + "tags": "outdoors,widget", + "type":"widget", + "storage": [ + {"name":"activepedom.wid.js","url":"widget.js"}, + {"name":"activepedom.settings.js","url":"settings.js"}, + {"name":"activepedom.img","url":"app-icon.js","evaluate":true} + ] }, { "id": "tabata", "name": "Tabata", @@ -1148,9 +1176,9 @@ }, { "id": "batchart", "name": "Battery Chart", - "shortName":"BatChart", + "shortName":"Battery Chart", "icon": "app.png", - "version":"0.03", + "version":"0.07", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ @@ -1159,11 +1187,25 @@ {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] }, + { "id": "nato", + "name": "NATO Alphabet", + "shortName" : "NATOAlphabet", + "icon": "nato.png", + "version":"0.01", + "type": "app", + "description": "Learn the NATO Phonetic alphabet plus some numbers.", + "tags": "app,learn,visual", + "allow_emulator":true, + "storage": [ + {"name":"nato.app.js","url":"nato.js"}, + {"name":"nato.img","url":"nato-icon.js","evaluate":true} + ] + }, { "id": "numerals", "name": "Numerals Clock", "shortName": "Numerals Clock", "icon": "numerals.png", - "version":"0.01", + "version":"0.02", "description": "A simple big numerals clock", "tags": "numerals,clock", "type":"clock", @@ -1181,10 +1223,35 @@ "version":"0.02", "description": "Detect BLE devices and show some informations.", "tags": "app,bluetooth,tool", - "readme": "README.md", + "readme": "README.md", "storage": [ {"name":"bledetect.app.js","url":"bledetect.js"}, {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} ] + }, + { "id": "snake", + "name": "Snake", + "shortName":"Snake", + "icon": "snake.png", + "version":"0.02", + "description": "The classic snake game. Eat apples and don't bite your tail.", + "tags": "game,fun", + "readme": "README.md", + "storage": [ + {"name":"snake.app.js","url":"snake.js"}, + {"name":"snake.img","url":"snake-icon.js","evaluate":true} + ] + }, + { "id": "calculator", + "name": "Calculator", + "shortName":"Calculator", + "icon": "calculator.png", + "version":"0.01", + "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.", + "tags": "app,tool", + "storage": [ + {"name":"calculator.app.js","url":"app.js"}, + {"name":"calculator.img","url":"calculator-icon.js","evaluate":true} + ] } ] diff --git a/apps/activepedom/10600.png b/apps/activepedom/10600.png new file mode 100644 index 000000000..36de436df Binary files /dev/null and b/apps/activepedom/10600.png differ diff --git a/apps/activepedom/1600.png b/apps/activepedom/1600.png new file mode 100644 index 000000000..fb11f999a Binary files /dev/null and b/apps/activepedom/1600.png differ diff --git a/apps/activepedom/600.png b/apps/activepedom/600.png new file mode 100644 index 000000000..4d2c625c7 Binary files /dev/null and b/apps/activepedom/600.png differ diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/activepedom/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md new file mode 100644 index 000000000..8a10727cd --- /dev/null +++ b/apps/activepedom/README.md @@ -0,0 +1,38 @@ +# Improved pedometer +Pedometer that filters out arm movement and displays a step goal progress. + +I changed the step counting algorithm completely. +Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long. +To get in 'active' mode, you have to reach the step threshold before the active timer runs out. +When you reach the step threshold, the steps needed to reach the threshold are counted as well. + +## Screenshots +* 600 steps +![](600.png) + +* 1600 steps +![](1600.png) + +* 10600 steps +![](10600.png) + +## Features + +* Two line display +* Large number for good readability +* Small number with the exact steps counted +* Large number is displayed in green when status is 'active' +* Progress bar for step goal +* Counts steps only if they are reached in a certain time +* Filters out steps where time between two steps is too long or too short +* Step detection sensitivity from firmware can be configured +* Steps are saved to a file and read-in at start (to not lose step progress) +* Settings can be changed in Settings - App/widget settings - Active Pedometer + +## Development version + +* https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer + +## Requests + +If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/ \ No newline at end of file diff --git a/apps/activepedom/app-icon.js b/apps/activepedom/app-icon.js new file mode 100644 index 000000000..82e786c7f --- /dev/null +++ b/apps/activepedom/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I=")) \ No newline at end of file diff --git a/apps/activepedom/app.png b/apps/activepedom/app.png new file mode 100644 index 000000000..6fccf4308 Binary files /dev/null and b/apps/activepedom/app.png differ diff --git a/apps/activepedom/settings.js b/apps/activepedom/settings.js new file mode 100644 index 000000000..43764a164 --- /dev/null +++ b/apps/activepedom/settings.js @@ -0,0 +1,81 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'activepedom.settings.json'; + + // initialize with default settings... + let s = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + }; + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + const saved = storage.readJSON(SETTINGS_FILE, 1) || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + WIDGETS["activepedom"].draw(); + }; + } + + const menu = { + '': { 'title': 'Active Pedometer' }, + '< Back': back, + 'Max time (ms)': { + value: s.cMaxTime, + min: 0, + max: 10000, + step: 100, + onchange: save('cMaxTime'), + }, + 'Min time (ms)': { + value: s.cMinTime, + min: 0, + max: 500, + step: 10, + onchange: save('cMinTime'), + }, + 'Step threshold': { + value: s.stepThreshold, + min: 0, + max: 100, + step: 1, + onchange: save('stepThreshold'), + }, + 'Act.Res. (ms)': { + value: s.intervalResetActive, + min: 100, + max: 100000, + step: 1000, + onchange: save('intervalResetActive'), + }, + 'Step sens.': { + value: s.stepSensitivity, + min: 0, + max: 1000, + step: 10, + onchange: save('stepSensitivity'), + }, + 'Step goal': { + value: s.stepGoal, + min: 1000, + max: 100000, + step: 1000, + onchange: save('stepGoal'), + }, + }; + E.showMenu(menu); +}); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js new file mode 100644 index 000000000..0c8b2438d --- /dev/null +++ b/apps/activepedom/widget.js @@ -0,0 +1,180 @@ +(() => { + var stepTimeDiff = 9999; //Time difference between two steps + var startTimeStep = new Date(); //set start time + var stopTimeStep = 0; //Time after one step + var timerResetActive = 0; //timer to reset active + var steps = 0; //steps taken + var stepsCounted = 0; //active steps counted + var active = 0; //x steps in y seconds achieved + var stepGoalPercent = 0; //percentage of step goal + var stepGoalBarLength = 0; //length og progress bar + var lastUpdate = new Date(); + var width = 45; + + var stepsTooShort = 0; + var stepsTooLong = 0; + var stepsOutsideTime = 0; + + //define default settings + const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + }; + const SETTINGS_FILE = 'activepedom.settings.json'; + const PEDOMFILE = "activepedom.steps.json"; + + let settings; + //load settings + function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; + } + //return setting + function setting(key) { + if (!settings) { loadSettings(); } + return (key in settings) ? settings[key] : DEFAULTS[key]; + } + + function setStepSensitivity(s) { + function sqr(x) { return x*x; } + var X=sqr(8192-s); + var Y=sqr(8192+s); + Bangle.setOptions({stepCounterThresholdLow:X,stepCounterThresholdHigh:Y}); + } + + //format number to make them shorter + function kFormatter(num) { + if (num <= 999) return num; //smaller 1.000, return 600 as 600 + if (num >= 1000 && num < 10000) { //between 1.000 and 10.000 + num = Math.floor(num/100)*100; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 1600 as 1.6k + } + if (num >= 10000) { //greater 10.000 + num = Math.floor(num/1000)*1000; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 10.600 as 10k + } + } + + //Set Active to 0 + function resetActive() { + active = 0; + steps = 0; + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + } + + function calcSteps() { + stopTimeStep = new Date(); //stop time after each step + stepTimeDiff = stopTimeStep - startTimeStep; //time between steps in milliseconds + startTimeStep = new Date(); //start time again + + //Remove step if time between first and second step is too long + if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds + stepsTooLong++; //count steps which are note counted, because time too long + steps--; + } + + //Remove step if time between first and second step is too short + if (stepTimeDiff <= setting('cMinTime')) { //milliseconds + stepsTooShort++; //count steps which are note counted, because time too short + steps--; + } + + if (steps >= setting('stepThreshold')) { + if (active == 0) { + stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 + stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status + } + active = 1; + clearInterval(timerResetActive); //stop timer which resets active + timerResetActive = setInterval(resetActive, setting('intervalResetActive')); //reset active after timer runs out + steps = 0; + } + + if (active == 1) { + stepsCounted++; //count steps + } + else { + stepsOutsideTime++; + } + } + + function draw() { + var height = 23; //width is deined globally + var stepsDisplayLarge = kFormatter(stepsCounted); + + //Check if same day + let date = new Date(); + if (lastUpdate.getDate() == date.getDate()){ //if same day + } + else { + stepsCounted = 1; //set stepcount to 1 + } + lastUpdate = date; + + g.reset(); + g.clearRect(this.x, this.y, this.x+width, this.y+height); + + //draw numbers + if (active == 1) g.setColor(0x07E0); //green + else g.setColor(0xFFFF); //white + g.setFont("6x8", 2); + g.drawString(stepsDisplayLarge,this.x+1,this.y); //first line, big number + g.setFont("6x8", 1); + g.setColor(0xFFFF); //white + g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number + + //draw step goal bar + stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100; + stepGoalBarLength = width / 100 * stepGoalPercent; + if (stepGoalBarLength > width) stepGoalBarLength = width; //do not draw across width of widget + g.setColor(0x7BEF); //grey + g.fillRect(this.x, this.y+height, this.x+width, this.y+height); // draw background bar + g.setColor(0xFFFF); //white + g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar + g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar + g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar + } + + //This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off() + E.on('kill', () => { + let d = { //define array to write to file + lastUpdate : lastUpdate.toISOString(), + stepsToday : stepsCounted, + stepsTooShort : stepsTooShort, + stepsTooLong : stepsTooLong, + stepsOutsideTime : stepsOutsideTime + }; + require("Storage").write(PEDOMFILE,d); //write array to file + }); + + //When Step is registered by firmware + Bangle.on('step', (up) => { + steps++; //increase step count + calcSteps(); + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + }); + + // redraw when the LCD turns on + Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["activepedom"].draw(); + }); + + //Read data from file and set variables + let pedomData = require("Storage").readJSON(PEDOMFILE,1); + if (pedomData) { + if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); + stepsCounted = pedomData.stepsToday|0; + stepsTooShort = pedomData.stepsTooShort; + stepsTooLong = pedomData.stepsTooLong; + stepsOutsideTime = pedomData.stepsOutsideTime; + } + + setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) + + //Add widget + WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; + +})(); \ No newline at end of file diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 1b77ff82f..ba9e4e847 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -1,3 +1,7 @@ 0.01: New app and widget 0.02: Widget stores data to file (1 dataset/10min) 0.03: Rotate log files once a week. +0.04: chart in the app is now active. +0.05: Display temperature and LCD state in chart +0.06: Fixes widget events and charting of component states +0.07: Improve logging and charting of component states and add widget icon \ No newline at end of file diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 684f9a88d..deb406d8b 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -1,20 +1,213 @@ -// place your const, vars, functions or classes here +const GraphXZero = 40; +const GraphYZero = 180; +const GraphY100 = 80; -function renderBatteryChart(){ - g.drawString("t", 215, 175); - g.drawLine(40,190,40,80); +const GraphMarkerOffset = 5; +const MaxValueCount = 144; +const GraphXMax = GraphXZero + MaxValueCount; + +const GraphLcdY = GraphYZero + 10; +const GraphCompassY = GraphYZero + 16; +// const GraphBluetoothY = GraphYZero + 22; +const GraphGpsY = GraphYZero + 28; +const GraphHrmY = GraphYZero + 34; + +const Storage = require("Storage"); + +function renderCoordinateSystem() { + g.setFont("6x8", 1); - g.drawString("%", 39, 70); - g.drawString("100", 15, 75); - g.drawLine(35,80,40,80); + // Left Y axis (Battery) + g.setColor(1, 1, 0); + g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); + g.drawString("%", 39, GraphY100 - 10); - g.drawString("50", 20,125); - g.drawLine(35,130,40,130); + g.setFontAlign(1, -1, 0); + g.drawString("100", 30, GraphY100 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100); - g.drawString("0", 25, 175); - g.drawLine(35,180,210,180); + g.drawString("50", 30, GraphYZero - 50 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130); - g.drawString("Chart not yet functional", 60, 125); + g.drawString("0", 30, GraphYZero - GraphMarkerOffset); + + g.setColor(1,1,1); + g.setFontAlign(1, -1, 0); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero); + + // Right Y axis (Temperature) + g.setColor(0.4, 0.4, 1); + g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100); + g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10); + g.setFontAlign(-1, -1, 0); + g.drawString("20", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 130, GraphXMax, 130); + g.drawString("30", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - 50 - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 80, GraphXMax, 80); + g.drawString("40", GraphXMax + 2 * GraphMarkerOffset, GraphY100 - GraphMarkerOffset); + + g.setColor(1,1,1); +} + +function decrementDay(dayToDecrement) { + return dayToDecrement === 0 ? 6 : dayToDecrement-1; +} + +function loadData() { + const startingDay = new Date().getDay(); + + // Load data for the current day + let logFileName = "bclog" + startingDay; + + let dataLines = loadLinesFromFile(MaxValueCount, logFileName); + + // Top up to MaxValueCount from previous days as required + let previousDay = decrementDay(startingDay); + while (dataLines.length < MaxValueCount + && previousDay !== startingDay) { + + let topUpLogFileName = "bclog" + previousDay; + let remainingLines = MaxValueCount - dataLines.length; + let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); + + if(topUpLines) { + dataLines = topUpLines.concat(dataLines); + } + + previousDay = decrementDay(previousDay); + } + + return dataLines; +} + +function loadLinesFromFile(requestedLineCount, fileName) { + let allLines = []; + let returnLines = []; + + var readFile = Storage.open(fileName, "r"); + + while ((nextLine = readFile.readLine())) { + if(nextLine) { + allLines.push(nextLine); + } + } + + readFile = null; + + if (allLines.length <= 0) return; + + let linesToReadCount = Math.min(requestedLineCount, allLines.length); + let startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1); + + for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) { + if(allLines[i]) { + returnLines.push(allLines[i]); + } + } + + allLines = null; + + return returnLines; +} + +function renderData(dataArray) { + const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 + }; + + //const timestampIndex = 0; + const batteryIndex = 1; + const temperatureIndex = 2; + const switchabelsIndex = 3; + + var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; + + for (let i = 0; i < dataArray.length; i++) { + const element = dataArray[i]; + + var dataInfo = element.split(","); + + // Battery percentage + g.setColor(1, 1, 0); + g.setPixel(GraphXZero + i, GraphYZero - parseInt(dataInfo[batteryIndex])); + + // Temperature + g.setColor(0.4, 0.4, 1); + let scaledTemp = Math.floor(((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000)/20) + ((((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000) % 100)/25); + + g.setPixel(GraphXZero + i, GraphYZero - scaledTemp); + + // LCD state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd) { + g.setColor(1, 1, 1); + g.setFontAlign(1, -1, 0); + g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true); + g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1); + } + + // Compass state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.compass) { + g.setColor(0, 1, 0); + g.setFontAlign(-1, -1, 0); + g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); + g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); + } + + // // Bluetooth state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0, 0, 1); + // g.setFontAlign(1, -1, 0); + // g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true); + // g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); + // } + + // Gps state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) { + g.setColor(0.8, 0.5, 0.24); + g.setFontAlign(-1, -1, 0); + g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); + g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1); + } + + // Hrm state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.hrm) { + g.setColor(1, 0, 0); + g.setFontAlign(1, -1, 0); + g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); + g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1); + } + } + + dataArray = null; +} + +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + +function renderBatteryChart() { + renderCoordinateSystem(); + let data = loadData(); + renderData(data); + data = null; +} + +// Show launcher when middle button pressed +function switchOffApp(){ + Bangle.showLauncher(); } // special function to handle display switch on @@ -22,13 +215,20 @@ Bangle.on('lcdPower', (on) => { if (on) { // call your app function here // If you clear the screen, do Bangle.drawWidgets(); + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); renderBatteryChart(); } }); +setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); // call your app function here +renderHomeIcon(); + renderBatteryChart(); diff --git a/apps/batchart/batchart.dat b/apps/batchart/batchart.dat deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 2e2f43cdf..53f8b3549 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -1,5 +1,7 @@ (() => { - var switchableConsumers = { + const Storage = require("Storage"); + + const switchableConsumers = { none: 0, lcd: 1, compass: 2, @@ -8,59 +10,106 @@ hrm: 16 }; - var settings = {}; var batChartFile; // file for battery percentage recording const recordingInterval10Min = 60 * 10 * 1000; - const recordingInterval1Min = 60*1000; //For testing - const recordingInterval10S = 10*1000; //For testing + const recordingInterval1Min = 60*1000; //For testing + const recordingInterval10S = 10*1000; //For testing var recordingInterval = null; + var compassEventReceived = false; + var gpsEventReceived = false; + var hrmEventReceived = false; + // draw your widget function draw() { + let x = this.x; + let y = this.y; + + g.setColor(0, 1, 0); + g.fillPoly([x+5, y, x+5, y+4, x+1, y+4, x+1, y+20, x+18, y+20, x+18, y+4, x+13, y+4, x+13, y], true); + + g.setColor(0,0,0); + g.drawPoly([x+5, y+6, x+8, y+12, x+13, y+12, x+16, y+18], false); + g.reset(); - g.drawString("BC", this.x, this.y); + } + + function onMag(){ + compassEventReceived = true; + // Stop handling events when no longer necessarry + Bangle.removeListener("mag", onMag); + } + + function onGps() { + gpsEventReceived = true; + Bangle.removeListener("GPS", onGps); + } + + function onHrm() { + hrmEventReceived = true; + Bangle.removeListener("HRM", onHrm); } function getEnabledConsumersValue() { var enabledConsumers = switchableConsumers.none; + Bangle.on('mag', onMag); + Bangle.on('GPS', onGps); + Bangle.on('HRM', onHrm); + + // Wait two seconds, that should be enough for each of the events to get raised once + setTimeout(() => { + Bangle.removeAllListeners(); + }, 2000); + if (Bangle.isLCDOn()) enabledConsumers = enabledConsumers | switchableConsumers.lcd; // Already added in the hope they will be available soon to get more details - // if (Bangle.isCompassOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.compass; - // if (Bangle.isBluetoothOn()) + if (compassEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.compass; + if (gpsEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.gps; + if (hrmEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.hrm; + //if (Bangle.isBluetoothOn()) // enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; - // if (Bangle.isGpsOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.gps; - // if (Bangle.isHrmOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.hrm; - return enabledConsumers; + // Reset the event registration vars + compassEventReceived = false; + gpsEventReceived = false; + hrmEventReceived = false; + + return enabledConsumers.toString(); } function logBatteryData() { const previousWriteLogName = "bcprvday"; - const previousWriteDay = require("Storage").read(previousWriteLogName); + const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine()); const currentWriteDay = new Date().getDay(); const logFileName = "bclog" + currentWriteDay; // Change log target on day change - if (previousWriteDay != currentWriteDay) { + if (!isNaN(previousWriteDay) + && previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago - require("Storage").erase(logFileName); - require("Storage").write(previousWriteLogName, currentWriteDay); + Storage.open(logFileName, "r").erase(); + Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay)); } - var bcLogFileA = require("Storage").open(logFileName, "a"); + var bcLogFileA = Storage.open(logFileName, "a"); if (bcLogFileA) { - console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")); - bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n"); + let logTime = getTime().toFixed(0); + let logPercent = E.getBattery(); + let logTemperature = E.getTemperature(); + let logConsumers = getEnabledConsumersValue(); + + let logString = [logTime, logPercent, logTemperature, logConsumers].join(","); + + bcLogFileA.write(logString + "\n"); } } - // Called by the heart app to reload settings and decide what's function reload() { WIDGETS["batchart"].width = 24; @@ -74,6 +123,7 @@ reload(); Bangle.drawWidgets(); // relayout all widgets }}; + // load settings, set correct widget width reload(); })() \ No newline at end of file diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/calculator/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/calculator/app.js b/apps/calculator/app.js new file mode 100644 index 000000000..91dd7c49d --- /dev/null +++ b/apps/calculator/app.js @@ -0,0 +1,352 @@ +/** + * BangleJS Calculator + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + */ + +g.clear(); +Graphics.prototype.setFont7x11Numeric7Seg = function() { + this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11); +}; + +var DEFAULT_SELECTION = '5'; +var BOTTOM_MARGIN = 10; +var RIGHT_MARGIN = 20; +var COLORS = { + // [normal, selected] + DEFAULT: ['#7F8183', '#A6A6A7'], + OPERATOR: ['#F99D1C', '#CA7F2A'], + SPECIAL: ['#65686C', '#7F8183'] +}; + +var keys = { + '0': { + xy: [0, 200, 120, 240], + trbl: '2.00' + }, + '.': { + xy: [120, 200, 180, 240], + trbl: '3=.0' + }, + '=': { + xy: [181, 200, 240, 240], + trbl: '+==.', + color: COLORS.OPERATOR + }, + '1': { + xy: [0, 160, 60, 200], + trbl: '4201' + }, + '2': { + xy: [60, 160, 120, 200], + trbl: '5301' + }, + '3': { + xy: [120, 160, 180, 200], + trbl: '6+.2' + }, + '+': { + xy: [181, 160, 240, 200], + trbl: '-+=3', + color: COLORS.OPERATOR + }, + '4': { + xy: [0, 120, 60, 160], + trbl: '7514' + }, + '5': { + xy: [60, 120, 120, 160], + trbl: '8624' + }, + '6': { + xy: [120, 120, 180, 160], + trbl: '9-35' + }, + '-': { + xy: [181, 120, 240, 160], + trbl: '*-+6', + color: COLORS.OPERATOR + }, + '7': { + xy: [0, 80, 60, 120], + trbl: 'R847' + }, + '8': { + xy: [60, 80, 120, 120], + trbl: 'N957' + }, + '9': { + xy: [120, 80, 180, 120], + trbl: '%*68' + }, + '*': { + xy: [181, 80, 240, 120], + trbl: '/*-9', + color: COLORS.OPERATOR + }, + 'R': { + xy: [0, 40, 60, 79], + trbl: 'RN7R', + color: COLORS.SPECIAL, + val: 'AC' + }, + 'N': { + xy: [60, 40, 120, 79], + trbl: 'N%8R', + color: COLORS.SPECIAL, + val: '+/-' + }, + '%': { + xy: [120, 40, 180, 79], + trbl: '%/9N', + color: COLORS.SPECIAL + }, + '/': { + xy: [181, 40, 240, 79], + trbl: '//*%', + color: COLORS.OPERATOR + } +}; + +var selected = DEFAULT_SELECTION; +var prevSelected = DEFAULT_SELECTION; +var prevNumber = null; +var currNumber = null; +var operator = null; +var results = null; +var isDecimal = false; +var hasPressedEquals = false; + +function drawKey(name, k, selected) { + var rMargin = 0; + var bMargin = 0; + var color = k.color || COLORS.DEFAULT; + g.setColor(color[selected ? 1 : 0]); + g.setFont('Vector', 20); + g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); + g.setColor(-1); + // correct margins to center the texts + if (name == '0') { + rMargin = (RIGHT_MARGIN * 2) - 7; + } else if (name === '/') { + rMargin = 5; + } else if (name === '*') { + bMargin = 5; + rMargin = 3; + } else if (name === '-') { + rMargin = 3; + } else if (name === 'R' || name === 'N') { + rMargin = k.val === 'C' ? 0 : -9; + } else if (name === '%') { + rMargin = -3; + } + g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); +} + +function doMath(x, y, operator) { + // might not be a number due to display of dot "." algo + x = Number(x); + y = Number(y); + switch (operator) { + case '/': + return x / y; + case '*': + return x * y; + case '+': + return x + y; + case '-': + return x - y; + } +} + +function displayOutput(num) { + var len; + var minusMarge = 0; + g.setColor(0); + g.fillRect(0, 0, 240, 39); + g.setColor(-1); + if (num === Infinity || num === -Infinity || isNaN(num)) { + // handle division by 0 + if (num === Infinity) { + num = 'INFINITY'; + } else if (num === -Infinity) { + num = '-INFINITY'; + } else { + num = 'NOT A NUMBER'; + minusMarge = -25; + } + len = (num + '').length; + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + prevNumber = null; + operator = null; + keys.R.val = 'AC'; + drawKey('R', keys.R); + g.setFont('Vector', 22); + } else { + // might not be a number due to display of dot "." + var numNumeric = Number(num); + + if (typeof num === 'string') { + if (num.indexOf('.') !== -1) { + // display a 0 before a lonely dot + if (numNumeric == 0) { + num = '0.'; + } + } else { + // remove preceding 0 + while (num.length > 1 && num[0] === '0') + num = num.substr(1); + } + } + + len = (num + '').length; + if (numNumeric < 0) { + // minus is not available in font 7x11Numeric7Seg, we use Vector + g.setFont('Vector', 20); + g.drawString('-', 220 - (len * 15), 10); + minusMarge = 15; + } + g.setFont('7x11Numeric7Seg', 2); + } + g.drawString(num, 220 - (len * 15) + minusMarge, 10); +} + +function calculatorLogic(x) { + if (hasPressedEquals) { + currNumber = results; + prevNumber = null; + operator = null; + results = null; + isDecimal = null; + displayOutput(currNumber); + hasPressedEquals = false; + } + if (prevNumber != null && currNumber != null && operator != null) { + // we execute the calculus only when there was a previous number entered before and an operator + results = doMath(prevNumber, currNumber, operator); + operator = x; + prevNumber = results; + currNumber = null; + displayOutput(results); + } else if (prevNumber == null && currNumber != null && operator == null) { + // no operator yet, save the current number for later use when an operator is pressed + operator = x; + prevNumber = currNumber; + currNumber = null; + displayOutput(prevNumber); + } else if (prevNumber == null && currNumber == null && operator == null) { + displayOutput(0); + } +} + +function buttonPress(val) { + switch (val) { + case 'R': + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + if (keys.R.val == 'AC') { + prevNumber = null; + operator = null; + } else { + keys.R.val = 'AC'; + drawKey('R', keys.R); + } + displayOutput(0); + break; + case '%': + if (results != null) { + displayOutput(results /= 100); + } else if (currNumber != null) { + displayOutput(currNumber /= 100); + } + break; + case 'N': + if (results != null) { + displayOutput(results *= -1); + } else if (currNumber != null) { + displayOutput(currNumber *= -1); + } + break; + case '/': + case '*': + case '-': + case '+': + calculatorLogic(val); + break; + case '.': + keys.R.val = 'C'; + drawKey('R', keys.R); + isDecimal = true; + displayOutput(currNumber == null ? 0 + '.' : currNumber + '.'); + break; + case '=': + if (prevNumber != null && currNumber != null && operator != null) { + results = doMath(prevNumber, currNumber, operator); + prevNumber = results; + displayOutput(results); + hasPressedEquals = true; + } + break; + default: + keys.R.val = 'C'; + drawKey('R', keys.R); + if (isDecimal) { + currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val; + isDecimal = false; + } else { + currNumber = currNumber == null ? val : currNumber + val; + } + displayOutput(currNumber); + break; + } +} + +for (var k in keys) { + if (keys.hasOwnProperty(k)) { + drawKey(k, keys[k], k == '5'); + } +} +g.setFont('7x11Numeric7Seg', 2.8); +g.drawString('0', 205, 10); + + +setWatch(function() { + drawKey(selected, keys[selected]); + // key 0 is 2 keys wide, go up to 1 if it was previously selected + if (selected == '0' && prevSelected === '1') { + prevSelected = selected; + selected = '1'; + } else { + prevSelected = selected; + selected = keys[selected].trbl[0]; + } + drawKey(selected, keys[selected], true); +}, BTN1, {repeat: true, debounce: 100}); + +setWatch(function() { + drawKey(selected, keys[selected]); + prevSelected = selected; + selected = keys[selected].trbl[2]; + drawKey(selected, keys[selected], true); +}, BTN3, {repeat: true, debounce: 100}); + +Bangle.on('touch', function(direction) { + drawKey(selected, keys[selected]); + prevSelected = selected; + if (direction == 1) { + selected = keys[selected].trbl[3]; + } else if (direction == 2) { + selected = keys[selected].trbl[1]; + } + drawKey(selected, keys[selected], true); +}); + +setWatch(function() { + buttonPress(selected); +}, BTN2, {repeat: true, debounce: 100}); diff --git a/apps/calculator/calculator-icon.js b/apps/calculator/calculator-icon.js new file mode 100644 index 000000000..94158e7d2 --- /dev/null +++ b/apps/calculator/calculator-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44=")) diff --git a/apps/calculator/calculator.png b/apps/calculator/calculator.png new file mode 100644 index 000000000..8362c9200 Binary files /dev/null and b/apps/calculator/calculator.png differ diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index d1f9c6a62..53f8a1b4c 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -7,3 +7,4 @@ 0.06: Gadgetbridge App 'Connected' state is no longer toggleable 0.07: Move configuration to settings menu 0.08: Don't turn on LCD at start of every song +0.09: Update Bluetooth connection state automatically diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index fa44757fc..03c622443 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -189,8 +189,8 @@ g.flip(); // turns screen on } - NRF.on("connected", changedConnectionState); - NRF.on("disconnected", changedConnectionState); + NRF.on("connect", changedConnectionState); + NRF.on("disconnect", changedConnectionState); WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw }; diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index 9a47bdd9a..8f1c575a1 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -4,3 +4,4 @@ 0.04: Properly Fix GPS time display in gpsrec app 0.05: Tweaks for variable size widget system 0.06: Ensure widget update itself (fix #118) and change to using icons +0.07: Added @jeffmer's awesome track viewer diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js index 58b4295a6..63f3840ff 100644 --- a/apps/gpsrec/app.js +++ b/apps/gpsrec/app.js @@ -70,27 +70,65 @@ function viewTracks() { return E.showMenu(menu); } +function getTrackInfo(fn) { + var filename = getFN(fn); + var minLat = 90; + var maxLat = -90; + var minLong = 180; + var maxLong = -180; + var starttime, duration=0; + var f = require("Storage").open(filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var nl = 0, c, n; + if (l!==undefined) { + c = l.split(","); + starttime = parseInt(c[0]); + } + // pushed this loop together to try and bump loading speed a little + while(l!==undefined) { + ++nl;c=l.split(","); + n = parseFloat(c[1]);if(n>maxLat)maxLat=n;if(nmaxLong)maxLong=n;if(nylen ? 200/xlen : 200/ylen; + return { + fn : fn, + filename : filename, + time : new Date(starttime), + records : nl, + minLat : minLat, maxLat : maxLat, + minLong : minLong, maxLong : maxLong, + lfactor : lfactor, + scale : scale, + duration : Math.round(duration/1000) + }; +} + +function asTime(v){ + var mins = Math.floor(v/60); + var secs = v-mins*60; + return ""+mins.toString()+"m "+secs.toString()+"s"; +} + function viewTrack(n) { + E.showMessage("Loading...","GPS Track "+n); + var info = getTrackInfo(n); const menu = { '': { 'title': 'GPS Track '+n } }; - var trackCount = 0; - var trackTime; - var f = require("Storage").open(getFN(n),"r"); - var l = f.readLine(); - if (l!==undefined) { - var c = l.split(","); - trackTime = new Date(parseInt(c[0])); - } - while (l!==undefined) { - trackCount++; - // TODO: min/max/length of track? - l = f.readLine(); - } - if (trackTime) - menu[" "+trackTime.toISOString().substr(0,16).replace("T"," ")] = function(){}; - menu[trackCount+" records"] = function(){}; - // TODO: option to draw it? Just scan through, project using min/max + if (info.time) + menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){}; + menu["Duration"] = { value : asTime(info.duration)}; + menu["Records"] = { value : ""+info.records }; + menu['Plot'] = function() { + plotTrack(info); + }; menu['Erase'] = function() { E.showPrompt("Delete Track?").then(function(v) { if (v) { @@ -107,4 +145,80 @@ function viewTrack(n) { return E.showMenu(menu); } +function plotTrack(info) { + function xcoord(long){ + return 30 + Math.round((long-info.minLong)*info.lfactor*info.scale); + } + + function ycoord(lat){ + return 210 - Math.round((lat - info.minLat)*info.scale); + } + + function radians(a) { + return a*Math.PI/180; + } + + function distance(lat1,long1,lat2,long2){ + var x = radians(long1-long2) * Math.cos(radians((lat1+lat2)/2)); + var y = radians(lat2-lat1); + return Math.sqrt(x*x + y*y) * 6371000; + } + + E.showMenu(); // remove menu + g.setColor(1,0.5,0.5); + g.setFont("Vector",16); + g.fillRect(9,80,11,120); + g.fillPoly([9,60,19,80,0,80]); + g.setColor(1,1,1); + g.drawString("N",2,40); + g.drawString("Track"+info.fn.toString()+" - Loading",10,220); + g.setColor(0,0,0); + g.fillRect(0,220,239,239); + g.setColor(1,1,1); + g.drawString(asTime(info.duration),10,220); + var f = require("Storage").open(info.filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var ox=0; + var oy=0; + var olat,olong,dist=0; + var first = true; + var i=0; + while(l!==undefined) { + var c = l.split(","); + var lat = parseFloat(c[1]); + var long = parseFloat(c[2]); + var x = xcoord(long); + var y = ycoord(lat); + if (first) { + g.moveTo(x,y); + g.setColor(0,1,0); + g.fillCircle(x,y,5); + g.setColor(1,1,1); + first = false; + } else if (x!=ox || y!=oy) { + g.lineTo(x,y); + } + if (!first) { + var d = distance(olat,olong,lat,long); + if (!isNaN(d)) dist+=d; + } + olat = lat; + olong = long; + ox = x; + oy = y; + l = f.readLine(f); + } + g.setColor(1,0,0); + g.fillCircle(ox,oy,5); + g.setColor(1,1,1); + g.drawString(require("locale").distance(dist),120,220); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + g.drawString("Back",230,200); + setWatch(function() { + viewTrack(info.fn); + }, BTN3); +} + showMainMenu(); diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index acce6a7ed..69a3ccc7b 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -6,4 +6,7 @@ 0.06: Performance refactor, and enhanced graphics! 0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode 0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy -0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel \ No newline at end of file +0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel +0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. +0.11: User settings persisted and read to file. +0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%) \ No newline at end of file diff --git a/apps/marioclock/README.md b/apps/marioclock/README.md index e6aeaa1bb..25276a351 100644 --- a/apps/marioclock/README.md +++ b/apps/marioclock/README.md @@ -8,7 +8,7 @@ Enjoy watching Mario, or one of the other game characters run through a level wh ## Features * Multiple characters - swipe the screen right to change the character between `Mario`, `Toad`, and `Daisy` -* Night and Day modes - swipe left to toggle mode +* Night and Day modes - swipe left to enter night mode, with 3 levels of darkness before returning to day mode. * Smooth animation * Awesome 8-bit style grey-scale graphics * Mario jumps to change the time, every minute diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 529f1c95b..7601b89ba 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -16,6 +16,8 @@ const is12Hour = settings["12hour"] || false; // Screen dimensions let W, H; +// Screen brightness +let brightness = 1; let intervalRef, displayTimeoutRef = null; @@ -79,6 +81,16 @@ const phone = { messageType: null, }; +const SETTINGS_FILE = "marioclock.json"; + +function readSettings() { + return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; +} + +function writeSettings(newSettings) { + require("Storage").writeJSON(SETTINGS_FILE, newSettings); +} + function phoneOutbound(msg) { Bluetooth.println(JSON.stringify(msg)); } @@ -164,7 +176,17 @@ function switchCharacter() { } function toggleNightMode() { - nightMode = !nightMode; + if (!nightMode) { + nightMode = true; + return; + } + + brightness -= 0.30; + if (brightness <= 0) { + brightness = 1; + nightMode = false; + } + Bangle.setLCDBrightness(brightness); } function incrementTimer() { @@ -324,16 +346,20 @@ function drawToadFrame(idx, x, y) { function drawNotice(x, y) { if (phone.message === null) return; + let img; switch (phone.messageType) { case "call": - const callImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); - g.drawImage(callImg, characterSprite.x, characterSprite.y - 16); + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); break; case "notify": - const msgImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); - g.drawImage(msgImg, characterSprite.x, characterSprite.y - 16); + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + break; + case "lowBatt": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); break; } + + if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); } function drawCharacter(date, character) { @@ -555,8 +581,39 @@ function startTimers(){ redraw(); } +function loadSettings() { + const settings = readSettings(); + if (!settings) return; + + if (settings.character) characterSprite.character = settings.character; + if (settings.nightMode) nightMode = settings.nightMode; + if (settings.brightness) { + brightness = settings.brightness; + Bangle.setLCDBrightness(brightness); + } +} + +function updateSettings() { + const newSettings = { + character: characterSprite.character, + nightMode: nightMode, + brightness: brightness, + }; + writeSettings(newSettings); +} + +function checkBatteryLevel() { + if (Bangle.isCharging()) return; + if (E.getBattery() > 10) return; + if (phone.message !== null) return; + + phoneNewMessage("lowBatt", "Warning, battery is low"); +} + // Main function init() { + loadSettings(); + clearInterval(); // Initialise display @@ -606,23 +663,31 @@ function init() { default: toggleNightMode(); } + + updateSettings(); }); // Phone connectivity try { NRF.wake(); } catch (e) {} - NRF.on('disconnect', () => Bangle.buzz()); + NRF.on('disconnect', () => { + phoneNewMessage(null, "Phone disconnected"); + }); + NRF.on('connect', () => { setTimeout(() => { phoneOutbound({ t: "status", bat: E.getBattery() }); }, ONE_SECOND * 2); - Bangle.buzz(); + phoneNewMessage(null, "Phone connected"); }); GB = (evt) => phoneInbound(evt); startTimers(); + + setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); + checkBatteryLevel(); } // Initialise! -init() \ No newline at end of file +init(); \ No newline at end of file diff --git a/apps/nato/changelog.txt b/apps/nato/changelog.txt new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/nato/changelog.txt @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/nato/nato-icon.js b/apps/nato/nato-icon.js new file mode 100644 index 000000000..ae38c0274 --- /dev/null +++ b/apps/nato/nato-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA==")) diff --git a/apps/nato/nato.js b/apps/nato/nato.js new file mode 100644 index 000000000..f4301b83f --- /dev/null +++ b/apps/nato/nato.js @@ -0,0 +1,106 @@ +// Teach a user the NATO Phonetic Alphabet + numbers +// Based on the Morse Code app + +const FONT_NAME = 'Vector12'; +const FONT_SIZE = 80; +const SCREEN_PIXELS = 240; +const UNIT = 100; +const NATO_MAP = { + A: 'ALFA', + B: 'BRAVO', + C: 'CHARLIE', + D: 'DELTA', + E: 'ECHO', + F: 'FOXTROT', + G: 'GOLF', + H: 'HOTEL', + I: 'INDIA', + J: 'JULIETT', + K: 'KILO', + L: 'LIMA', + M: 'MIKE', + N: 'NOVEMBER', + O: 'OSCAR', + P: 'PAPA', + Q: 'QUEBEC', + R: 'ROMEO', + S: 'SIERRA', + T: 'TANGO', + U: 'UNIFORM', + V: 'VICTOR', + W: 'WHISKEY', + X: 'X-RAY', + Y: 'YANKEE', + Z: 'ZULU', + '0': 'ZE-RO', + '1': 'WUN', + '2': 'TOO', + '3': 'TREE', + '4': 'FOW-ER', + '5': 'FIFE', + '6': 'SIX', + '7': 'SEV-EN', + '8': 'AIT', + '9': 'NIN-ER', +}; + +let INDEX = 0; +let showLetter = true; + +const writeText = (txt) => { + g.clear(); + g.setFont(FONT_NAME, FONT_SIZE); + + var width = g.stringWidth(txt); + + // Fit text to screen + var fontFix = FONT_SIZE; + while(width > SCREEN_PIXELS-10){ + fontFix--; + g.setFont(FONT_NAME, fontFix); + width = g.stringWidth(txt); + } + g.drawString(txt, (SCREEN_PIXELS / 2) - (width / 2), SCREEN_PIXELS / 2); +}; +const writeLetter = () => { + writeText(Object.keys(NATO_MAP)[INDEX]); +}; +const writeCode = () => { + writeText(NATO_MAP[Object.keys(NATO_MAP)[INDEX]]); +}; +const toggle = () => { + showLetter = !showLetter; + if(showLetter){ + writeLetter(); + }else { + writeCode(); + } +}; + +// Bootstrapping + +g.clear(); +g.setFont(FONT_NAME, FONT_SIZE); +g.setColor(0, 1, 0); +g.setFontAlign(-1, 0, 0); + + +const step = (positive) => () => { + if (positive) { + INDEX = INDEX + 1; + if (INDEX > Object.keys(NATO_MAP).length - 1) INDEX = 0; + } else { + INDEX = INDEX - 1; + if (INDEX < 0) INDEX = Object.keys(NATO_MAP).length - 1; + } + showLetter = true; // for toggle() + writeLetter(); +}; + +writeLetter(); + +// Press the middle button to see the NATO Phonetic wording +setWatch(toggle, BTN2, { repeat: true }); +// Allow user to switch between letters +setWatch(step(true), BTN1, { repeat: true }); +setWatch(step(false), BTN3, { repeat: true }); diff --git a/apps/nato/nato.png b/apps/nato/nato.png new file mode 100644 index 000000000..bd4678c11 Binary files /dev/null and b/apps/nato/nato.png differ diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog index 553f7388a..f4418827e 100644 --- a/apps/ncstart/ChangeLog +++ b/apps/ncstart/ChangeLog @@ -2,3 +2,6 @@ Renamed as nodeconf-specific 0.03: Move configuration into App/widget settings Move loader into welcome.boot.js +0.04: Run again when updated + Don't run again when settings app is updated (or absent) + Add "Run Now" option to settings diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js index dbb70d213..e3f514f5b 100644 --- a/apps/ncstart/boot.js +++ b/apps/ncstart/boot.js @@ -1,9 +1,11 @@ (function() { - let s = require('Storage').readJSON('setting.json', 1) || {} + let s = require('Storage').readJSON('ncstart.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) + || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('ncstart.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('setting.json', s) + require('Storage').write('ncstart.settings.json', s) load('ncstart.app.js') }) } diff --git a/apps/ncstart/settings-default.json b/apps/ncstart/settings-default.json new file mode 100644 index 000000000..d250efff5 --- /dev/null +++ b/apps/ncstart/settings-default.json @@ -0,0 +1,3 @@ +{ + "welcomed": false +} diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js index 284262634..2b24095cf 100644 --- a/apps/ncstart/settings.js +++ b/apps/ncstart/settings.js @@ -1,16 +1,15 @@ // The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('setting.json', 1) || {} + let settings = require('Storage').readJSON('ncstart.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'NCEU Startup' }, - 'Run again': { + 'Run on Next Boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', - onchange: v => { - settings.welcomed = v ? undefined : true - require('Storage').write('setting.json', settings) - }, + format: v => v ? 'OK' : 'No', + onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}), }, + 'Run Now': () => load('ncstart.app.js'), '< Back': back, }) }) diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index 5560f00bc..a8396e26b 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Use BTN2 for settings menu like other clocks diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js index 648a1005a..fbfe5b9ed 100644 --- a/apps/numerals/numerals.app.js +++ b/apps/numerals/numerals.app.js @@ -11,7 +11,7 @@ var numerals = { 1:[[59,1,82,1,90,9,90,82,82,90,73,90,65,82,65,27,59,27,51,19,51,9,59,1]], 2:[[9,1,82,1,90,9,90,47,82,55,21,55,21,64,82,64,90,72,90,82,82,90,9,90,1,82,1,43,9,35,70,35,70,25,9,25,1,17,1,9,9,1]], 3:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,74,9,66,70,66,70,57,9,57,1,49,1,41,9,33,70,33,70,25,9,25,1,17,1,9,9,1]], - 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], + 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], 5:[[9,1,82,1,90,9,90,17,82,25,21,25,21,35,82,35,90,43,90,82,82,90,9,90,1,82,1,72,9,64,71,64,71,55,9,55,1,47,1,9,9,1]], 6:[[9,1,82,1,90,9,90,14,82,22,22,22,22,36,82,36,90,44,90,82,82,90,9,90,1,82,1,9,9,1],[22,55,69,55,69,69,22,69,22,55]], 7:[[9,1,82,1,90,9,90,15,15,90,9,90,1,82,1,76,54,23,9,23,1,15,1,9,9,1]], @@ -47,7 +47,7 @@ if (!settings) { function drawNum(num,col,x,y,func){ g.setColor(col); let tx = x*100+35; - let ty = y*100+35; + let ty = y*100+35; for (let i=0;i0) g.setColor((func==fill)?"#000000":col); func(translate(tx, ty,numerals[num][i])); @@ -57,7 +57,7 @@ function drawNum(num,col,x,y,func){ function draw(drawMode){ let d = new Date(); let h1 = Math.floor(d.getHours()/10); - let h2 = d.getHours()%10; + let h2 = d.getHours()%10; let m1 = Math.floor(d.getMinutes()/10); let m2 = d.getMinutes()%10; g.clearRect(0,24,240,240); @@ -70,9 +70,9 @@ function draw(drawMode){ Bangle.setLCDMode(); clearWatch(); -setWatch(Bangle.showLauncher, BTN1, {repeat:false,edge:"falling"}); +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); -g.clear(); +g.clear(); clearInterval(); if (settings.color>0) _rCol=settings.color-1; interval=setInterval(draw, REFRESH_RATE, settings.drawMode); @@ -80,7 +80,7 @@ draw(settings.drawMode); Bangle.on('lcdPower', function(on) { if (on) { - if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); draw(settings.drawMode); interval=setInterval(draw, REFRESH_RATE, settings.drawMode); }else @@ -90,4 +90,4 @@ Bangle.on('lcdPower', function(on) { }); Bangle.loadWidgets(); -Bangle.drawWidgets(); \ No newline at end of file +Bangle.drawWidgets(); diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index c3109dda6..3d82be9c0 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -12,4 +12,8 @@ 0.12: Fix memory leak (#206) Bring App settings nearer the top Move LCD Timeout to wakeup menu -0.13: Move LCD Brightness menu into more general LCD menu +0.13: Fix memory leak for App settings + Make capitalization more consistent + Move LCD Brightness menu into more general LCD menu +0.14: Reduce memory usage when running app settings page +0.15: Reduce memory usage when running default clock chooser (#294) diff --git a/apps/setting/settings.js b/apps/setting/settings.js index c6be52191..9e343a68e 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -64,7 +64,7 @@ function showMainMenu() { const mainmenu = { '': { 'title': 'Settings' }, 'Make Connectable': ()=>makeConnectable(), - 'App/widget settings': ()=>showAppSettingsMenu(), + 'App/Widget Settings': ()=>showAppSettingsMenu(), 'BLE': { value: settings.ble, format: boolFormat, @@ -81,7 +81,7 @@ function showMainMenu() { updateSettings(); } }, - 'Debug info': { + 'Debug Info': { value: settings.log, format: v => v ? "Show" : "Hide", onchange: () => { @@ -296,10 +296,10 @@ function makeConnectable() { }); } function showClockMenu() { - var clockApps = require("Storage").list(/\.info$/).map(app => { - try { return require("Storage").readJSON(app); } - catch (e) { } - }).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); + var clockApps = require("Storage").list(/\.info$/) + .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined}) + .filter(app => app) // filter out any undefined apps + .sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { 'title': 'Select Clock', @@ -325,8 +325,6 @@ function showClockMenu() { return E.showMenu(clockMenu); } - - function showSetTimeMenu() { d = new Date(); const timemenu = { @@ -419,8 +417,8 @@ function showAppSettingsMenu() { '< Back': ()=>showMainMenu(), } const apps = storage.list(/\.info$/) - .map(app => storage.readJSON(app, 1)) - .filter(app => app && app.settings) + .map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?a:undefined}) + .filter(app => app) // filter out any undefined apps .sort((a, b) => a.sortorder - b.sortorder) if (apps.length === 0) { appmenu['No app has settings'] = () => { }; @@ -450,7 +448,7 @@ function showAppSettings(app) { } try { // pass showAppSettingsMenu as "back" argument - appSettings(showAppSettingsMenu); + appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) return showError('Error in settings'); diff --git a/apps/snake/ChangeLog b/apps/snake/ChangeLog new file mode 100644 index 000000000..428718739 --- /dev/null +++ b/apps/snake/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Performance and graphic improvements, game pause, beep and buzz \ No newline at end of file diff --git a/apps/snake/README.md b/apps/snake/README.md new file mode 100644 index 000000000..483eae7a9 --- /dev/null +++ b/apps/snake/README.md @@ -0,0 +1,14 @@ +# Snake + +![Screenshot](https://i.imgur.com/bXQjxhB.png) + +The legentary classic game is now available on Bangle.js! +Eat apples and don't bite your tail. + +## Controls + +- UP: BTN1 +- DOWN: BTN3 +- LEFT: BTN4 +- RIGHT: BTN5 +- PAUSE: BTN2 diff --git a/apps/snake/snake-icon.js b/apps/snake/snake-icon.js new file mode 100644 index 000000000..305061003 --- /dev/null +++ b/apps/snake/snake-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ADE3m9hsIusrdhGIM3LtU3g0GAgQxlEwIqBmEAgEGF4QwkF4c3F4MxF4dbF8qLDrYHDre74IABF8QwBLoaPDF8wPKF96/jF/4v/F/4vrrc3AIQsnsIAKF94wiFxgv/R//+m4ABrYALBwIpYFwwAQLC4v/F7gXGF91hACovWFqwwUF4VbF7IwUFzSRVF1gwCF9wwZFyoA/AH4A/AH4A/AGg=")) \ No newline at end of file diff --git a/apps/snake/snake.js b/apps/snake/snake.js new file mode 100644 index 000000000..4532e0113 --- /dev/null +++ b/apps/snake/snake.js @@ -0,0 +1,155 @@ +Bangle.setLCDMode("120x120"); + +const H = g.getWidth(); +const W = g.getHeight(); +let running = true; +let score = 0; +let d; +const gridSize = 20; +const tileSize = 6; +let nextX = 0; +let nextY = 0; +const defaultTailSize = 3; +let tailSize = defaultTailSize; +const snakeTrail = []; +const snake = { x: 10, y: 10 }; +const apple = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; + +function drawBackground(){ + g.setColor("#000000"); + g.fillRect(0, 0, H, W); +} + +function drawApple(){ + g.setColor("#FF0000"); + g.fillCircle((apple.x * tileSize) + tileSize/2, (apple.y * tileSize) + tileSize/2, tileSize/2); +} + +function drawSnake(){ + g.setColor("#008000"); + for (let i = 0; i < snakeTrail.length; i++) { + g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize); + + //snake bites it's tail + if (snakeTrail[i].x === snake.x && snakeTrail[i].y === snake.y && tailSize > defaultTailSize) { + Bangle.buzz(1000); + gameOver(); + } + } +} + +function drawScore(){ + g.setColor("#FFFFFF"); + g.setFont("6x8"); + g.setFontAlign(0, 0); + g.drawString("Score:" + score, W / 2, 10); +} + +function gameStart() { + running = true; + score = 0; +} + +function gameOver() { + g.clear(); + g.setColor("#FFFFFF"); + g.setFont("6x8"); + g.drawString("GAME OVER!", W / 2, H / 2 - 10); + g.drawString("Tap to Restart", W / 2, H / 2 + 10); + running = false; + tailSize = defaultTailSize; +} + +function draw() { + if (!running) { + return; + } + + g.clear(); + + // move snake in next pos + snake.x += nextX; + snake.y += nextY; + + // snake over game world + if (snake.x < 0) { + snake.x = gridSize - 1; + } + if (snake.x > gridSize - 1) { + snake.x = 0; + } + + if (snake.y < 0) { + snake.y = gridSize - 1; + } + if (snake.y > gridSize - 1) { + snake.y = 0; + } + + //snake bite apple + if (snake.x === apple.x && snake.y === apple.y) { + Bangle.beep(20); + tailSize++; + score++; + + apple.x = Math.floor(Math.random() * gridSize); + apple.y = Math.floor(Math.random() * gridSize); + drawApple(); + } + + drawBackground(); + drawApple(); + drawSnake(); + drawScore(); + + //set snake trail + snakeTrail.push({ x: snake.x, y: snake.y }); + while (snakeTrail.length > tailSize) { + snakeTrail.shift(); + } + + g.flip(); +} + +// input +setWatch(() => {// Up + if (d !== 'd') { + nextX = 0; + nextY = -1; + d = 'u'; + } +}, BTN1, { repeat: true }); +setWatch(() => {// Down + if (d !== 'u') { + nextX = 0; + nextY = 1; + d = 'd'; + } +}, BTN3, { repeat: true }); +setWatch(() => {// Left + if (d !== 'r') { + nextX = -1; + nextY = 0; + d = 'l'; + } +}, BTN4, { repeat: true }); +setWatch(() => {// Right + if (d !== 'l') { + nextX = 1; + nextY = 0; + d = 'r'; + } +}, BTN5, { repeat: true }); +setWatch(() => {// Pause + running = !running; +}, BTN2, { repeat: true }); + +Bangle.on('touch', button => { + if (!running) { + gameStart(); + } +}); + +// render X times per second +const x = 5; +setInterval(draw, 1000 / x); \ No newline at end of file diff --git a/apps/snake/snake.png b/apps/snake/snake.png new file mode 100644 index 000000000..04564a8f7 Binary files /dev/null and b/apps/snake/snake.png differ diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index 89f3ab2c9..b8786af6a 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -4,3 +4,6 @@ 0.04: Fix regression after tweaks to Storage.readJSON 0.05: Move configuration into App/widget settings 0.06: Move loader into welcome.boot.js +0.07: Run again when updated + Don't run again when settings app is updated (or absent) + Add "Run Now" option to settings diff --git a/apps/welcome/app.js b/apps/welcome/app.js index 93a4234d8..a32a6e56f 100644 --- a/apps/welcome/app.js +++ b/apps/welcome/app.js @@ -288,6 +288,13 @@ setWatch(()=>{ }, BTN2, {repeat:true,edge:"rising"}); setWatch(()=>move(-1), BTN1, {repeat:true}); +(function migrateSettings(){ + let global_settings = require('Storage').readJSON('setting.json', 1) + if (global_settings) { + delete global_settings.welcomed + require('Storage').write('setting.json', global_settings) + } +})() Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); diff --git a/apps/welcome/boot.js b/apps/welcome/boot.js index ecf98b555..bc5afcc66 100644 --- a/apps/welcome/boot.js +++ b/apps/welcome/boot.js @@ -1,9 +1,11 @@ (function() { - let s = require('Storage').readJSON('setting.json', 1) || {} + let s = require('Storage').readJSON('welcome.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) + || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('welcome.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('setting.json', s) + require('Storage').write('welcome.settings.json', {welcomed: "yes"}) load('welcome.app.js') }) } diff --git a/apps/welcome/settings-default.json b/apps/welcome/settings-default.json new file mode 100644 index 000000000..d250efff5 --- /dev/null +++ b/apps/welcome/settings-default.json @@ -0,0 +1,3 @@ +{ + "welcomed": false +} diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index 2fbd585c6..b11921646 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -1,16 +1,15 @@ // The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('setting.json', 1) || {} + let settings = require('Storage').readJSON('welcome.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'Welcome App' }, - 'Run again': { + 'Run on Next Boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', - onchange: v => { - settings.welcomed = v ? undefined : true - require('Storage').write('setting.json', settings) - }, + format: v => v ? 'OK' : 'No', + onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}), }, + 'Run Now': () => load('welcome.app.js'), '< Back': back, }) }) diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index cd9993c02..b9d50ab8b 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -1,3 +1,4 @@ 0.02: Now refresh battery monitor every minute if LCD on 0.03: Tweaks for variable size widget system 0.04: Ensure redrawing works with variable size widget system +0.05: Fix regression stopping correct widget updates diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index 2f1f29802..dd6774d4c 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -30,7 +30,7 @@ Bangle.on('lcdPower', function(on) { WIDGETS["bat"].draw(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(draw, 60000); + batteryInterval = setInterval(()=>WIDGETS["bat"].draw(), 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 3627a86d3..129707320 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -5,3 +5,4 @@ 0.06: Show battery percentage as text 0.07: Add settings: percentage/color/charger icon 0.08: Draw percentage as inverted on monochrome battery +0.09: Fix regression stopping correct widget updates diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 9f88b5c49..aca690ce0 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -110,7 +110,7 @@ Bangle.on('lcdPower', function(on) { WIDGETS["batpc"].draw(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(draw, 60000); + batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index c268d6df0..59dc603a9 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -1,2 +1,3 @@ 0.02: Tweaks for variable size widget system 0.03: Ensure redrawing works with variable size widget system +0.04: Fix automatic update of Bluetooth connection status diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index 8e96a395d..c3254c791 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -13,7 +13,7 @@ function changed() { WIDGETS["bluetooth"].draw(); g.flip();// turns screen on } -NRF.on('connected',changed); -NRF.on('disconnected',changed); +NRF.on('connect',changed); +NRF.on('disconnect',changed); WIDGETS["bluetooth"]={area:"tr",width:24,draw:draw}; })() diff --git a/apps/widclk/ChangeLog b/apps/widclk/ChangeLog index 5370129cc..6fda78a08 100644 --- a/apps/widclk/ChangeLog +++ b/apps/widclk/ChangeLog @@ -1,2 +1,3 @@ 0.02: Now refresh battery monitor every minute if LCD on 0.03: Ensure redrawing works with variable size widget system +0.04: Fix regression stopping correct widget updates diff --git a/apps/widclk/widget.js b/apps/widclk/widget.js index 1d5df36b2..ff22bb4d1 100644 --- a/apps/widclk/widget.js +++ b/apps/widclk/widget.js @@ -14,7 +14,7 @@ } } function startTimers(){ - intervalRef = setInterval(draw, 60*1000); + intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000); WIDGETS["wdclk"].draw(); } Bangle.on('lcdPower', (on) => { @@ -23,5 +23,5 @@ }); WIDGETS["wdclk"]={area:"tr",width:width,draw:draw}; - if (Bangle.isLCDOn) intervalRef = setInterval(draw, 60*1000); + if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000); })() diff --git a/apps/widram/ChangeLog b/apps/widram/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/widram/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/widram/widget.js b/apps/widram/widget.js new file mode 100644 index 000000000..08710b726 --- /dev/null +++ b/apps/widram/widget.js @@ -0,0 +1,23 @@ +(() => { + function draw() { + g.reset(); + var m = process.memory(); + var pc = Math.round(m.usage*100/m.total); + g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4); + g.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff")); + g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/); + } + var ramInterval; + Bangle.on('lcdPower', function(on) { + if (on) { + WIDGETS["ram"].draw(); + if (!ramInterval) ramInterval = setInterval(()=>WIDGETS["ram"].draw(), 10000); + } else { + if (ramInterval) { + clearInterval(ramInterval); + ramInterval = undefined; + } + } + }); + WIDGETS["ram"]={area:"tl",width: 24,draw:draw}; +})() diff --git a/apps/widram/widget.png b/apps/widram/widget.png new file mode 100644 index 000000000..c1cbf2e1a Binary files /dev/null and b/apps/widram/widget.png differ diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index a2c9dee9a..62b111ae0 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -37,7 +37,13 @@ try{ ERROR("apps.json not valid JSON"); } -apps.forEach((app,addIdx) => { +const APP_KEYS = [ + 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', + 'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator', +]; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; + +apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); //console.log(`Checking ${app.id}...`); var appDir = APPSDIR+app.id+"/"; @@ -105,9 +111,15 @@ apps.forEach((app,addIdx) => { ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`); } } + for (const key in file) { + if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); + } }); //console.log(fileNames); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); + for (const key in app) { + if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); + } }); diff --git a/js/comms.js b/js/comms.js index 12989e089..1f840ada7 100644 --- a/js/comms.js +++ b/js/comms.js @@ -40,7 +40,7 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s currentBytes += f.content.length; // Chould check CRC here if needed instead of returning 'OK'... // E.CRC32(require("Storage").read(${JSON.stringify(app.name)})) - Puck.write(`\x10${f.cmd};Bluetooth.println("OK")\n`,(result) => { + Puck.write(`${f.cmd};Bluetooth.println("OK")\n`,(result) => { if (!result || result.trim()!="OK") { Progress.hide({sticky:true}); return reject("Unexpected response "+(result||"")); @@ -75,12 +75,21 @@ getInstalledApps : () => { Progress.hide({sticky:true}); return reject(""); } - Puck.eval('require("Storage").list(/\.info$/).map(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);return j})', (appList,err) => { + Puck.write('\x10Bluetooth.print("[");require("Storage").list(/\.info$/).forEach(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);Bluetooth.print(JSON.stringify(j)+",")});Bluetooth.println("0]")\n', (appList,err) => { Progress.hide({sticky:true}); + try { + appList = JSON.parse(appList); + // remove last element since we added a final '0' + // to make things easy on the Bangle.js side + appList = appList.slice(0,-1); + } catch (e) { + appList = null; + err = e.toString(); + } if (appList===null) return reject(err || ""); console.log("getInstalledApps", appList); resolve(appList); - }); + }, true /* callback on newline */); }); }); }, diff --git a/js/utils.js b/js/utils.js index 4913c7129..d8c1b8063 100644 --- a/js/utils.js +++ b/js/utils.js @@ -56,7 +56,7 @@ function getVersionInfo(appListing, appInstalled) { if (appListing.version) versionText = clicky("v"+appListing.version); } else { - versionText = (appInstalled.version ? ("v"+appInstalled.version) : "Unknown version"); + versionText = (appInstalled.version ? (clicky("v"+appInstalled.version)) : "Unknown version"); if (appListing.version != appInstalled.version) { if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version); canUpdate = true;