diff --git a/apps.json b/apps.json index 611a78923..c46fb7dc0 100644 --- a/apps.json +++ b/apps.json @@ -1,8 +1,8 @@ [ { "id": "fwupdate", - "name": "Firmware Update (BETA)", - "version": "0.01", + "name": "Firmware Update", + "version": "0.02", "description": "Uploads new Espruino firmwares to Bangle.js 2", "icon": "app.png", "type": "RAM", @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.37", + "version": "0.38", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -57,7 +57,7 @@ { "id": "messages", "name": "Messages", - "version": "0.10", + "version": "0.11", "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", "type": "app", @@ -79,7 +79,7 @@ "name": "Android Integration", "shortName": "Android", "version": "0.04", - "description": "(BETA) App to display notifications from Gadgetbridge on Android. This will eventually replace the Gadgetbridge widget.", + "description": "Display notifications/music/etc from Gadgetbridge on Android. This replaces the old Gadgetbridge widget.", "icon": "app.png", "tags": "tool,system,messages,notifications", "dependencies": {"messages":"app"}, @@ -96,7 +96,7 @@ "id": "ios", "name": "iOS Integration", "version": "0.06", - "description": "(BETA) App to display notifications from iOS devices", + "description": "Display notifications/music/etc from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", "dependencies": {"messages":"app"}, @@ -146,7 +146,7 @@ { "id": "setting", "name": "Settings", - "version": "0.35", + "version": "0.36", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -283,7 +283,7 @@ "id": "gbridge", "name": "Gadgetbridge", "version": "0.25", - "description": "The default notification handler for Gadgetbridge notifications from Android. This will eventually be replaced by the 'Android' app.", + "description": "(NOT RECOMMENDED) Handles Gadgetbridge notifications from Android. This is now replaced by the 'Android' app.", "icon": "app.png", "type": "widget", "tags": "tool,system,android,widget", @@ -727,7 +727,7 @@ { "id": "gpsrec", "name": "GPS Recorder", - "version": "0.26", + "version": "0.27", "description": "Application that allows you to record a GPS track. Can run in background", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -1755,7 +1755,7 @@ "id": "cliock", "name": "Commandline-Clock", "shortName": "CLI-Clock", - "version": "0.14", + "version": "0.15", "description": "Simple CLI-Styled Clock", "icon": "app.png", "screenshots": [{"url":"screenshot_cli.png"}], @@ -1937,6 +1937,19 @@ {"name":"widmp.wid.js","url":"widget.js"} ] }, + { + "id": "widmpsh", + "name": "Moon Phase Widget Southern Hemisphere", + "version": "0.01", + "description": "Display the current moon phase in blueish for the southern hemisphere in eight phases", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widmpsh.wid.js","url":"widget.js"} + ] + }, { "id": "minionclk", "name": "Minion clock", @@ -2086,12 +2099,12 @@ "id": "numerals", "name": "Numerals Clock", "shortName": "Numerals Clock", - "version": "0.09", + "version": "0.10", "description": "A simple big numerals clock", "icon": "numerals.png", "type": "clock", "tags": "numerals,clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, "screenshots": [{"url":"bangle1-numerals-screenshot.png"}], "storage": [ @@ -2393,7 +2406,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.02", + "version": "0.03", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], @@ -2403,8 +2416,10 @@ "allow_emulator": true, "storage": [ {"name":"calendar.app.js","url":"calendar.js"}, + {"name":"calendar.settings.js","url":"settings.js"}, {"name":"calendar.img","url":"calendar-icon.js","evaluate":true} - ] + ], + "data": [{"name":"calendar.json"}] }, { "id": "hidjoystick", @@ -2642,12 +2657,12 @@ "id": "widviz", "name": "Widget Visibility Widget", "shortName": "Viz Widget", - "version": "0.02", + "version": "0.03", "description": "Swipe left to hide top bar widgets, swipe right to redisplay.", "icon": "eye.png", "type": "widget", "tags": "widget", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widviz.wid.js","url":"widget.js"} ] @@ -3780,7 +3795,7 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.07", + "version": "0.08", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], @@ -4080,7 +4095,7 @@ {"name":"carcrazy.img","url":"app-icon.js","evaluate":true}, {"name":"carcrazy.settings.js","url":"settings.js"} ], - "data": [{"name":"app.json"}] + "data": [{"name":"CarCrazy.csv"}] }, { "id": "shortcuts", @@ -4387,7 +4402,7 @@ "id": "emojuino", "name": "Emojuino", "shortName": "Emojuino", - "version": "0.02", + "version": "0.03", "description": "Emojis & Espruino: broadcast Unicode emojis via Bluetooth Low Energy.", "icon": "emojuino.png", "screenshots": [ @@ -4409,7 +4424,7 @@ "id": "cliclockJS2Enhanced", "name": "Commandline-Clock JS2 Enhanced", "shortName": "CLI-Clock JS2", - "version": "0.02", + "version": "0.03", "description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design. Also added HID media controlls, just swipe on the clock face to controll the media! Gadgetbride support coming soon(hopefully) Thanks to t0m1o1 for media controls!", "icon": "app.png", "screenshots": [{"url":"screengrab.png"}], @@ -4522,7 +4537,7 @@ {"name":"schoolCalendar.img","url":"app-icon.js","evaluate":true} ], "data": [ - {"name":"app.json"} + {"name":"calendarItems.csv"} ] }, { "id": "timecal", @@ -4575,7 +4590,7 @@ "shortName":"93 Dub", "icon": "93dub.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.04", + "version":"0.05", "description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo", "tags": "clock", "type": "clock", @@ -4593,9 +4608,10 @@ "version":"0.01", "description": "Simple app to power off your Bangle.js", "icon": "app.png", - "tags": "poweroff, shutdown", + "tags": "tool, poweroff, shutdown", "supports" : ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", + "allow_emulator": true, "storage": [ {"name":"poweroff.app.js","url":"app.js"}, {"name":"poweroff.img","url":"app-icon.js","evaluate":true} @@ -4605,9 +4621,17 @@ "id": "sensible", "name": "SensiBLE", "shortName": "SensiBLE", - "version": "0.02", + "version": "0.03", "description": "Collect, display and advertise real-time sensor data.", "icon": "sensible.png", + "screenshots": [ + { "url": "screenshot-top.png" }, + { "url": "screenshot-acc.png" }, + { "url": "screenshot-bar.png" }, + { "url": "screenshot-gps.png" }, + { "url": "screenshot-hrm.png" }, + { "url": "screenshot-mag.png" } + ], "type": "app", "tags": "tool,sensors", "supports" : [ "BANGLEJS2" ], @@ -4670,7 +4694,7 @@ "id": "pebble", "name": "Pebble Clock", "shortName": "Pebble", - "version": "0.03", + "version": "0.04", "description": "A pebble style clock to keep the rebellion going", "readme": "README.md", "icon": "pebble.png", @@ -4791,7 +4815,7 @@ "icon": "app.png", "allow_emulator": true, "tags": "tools, keyboard, text, scribble", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"scribble.app.js","url":"app.js"}, @@ -4800,6 +4824,38 @@ "screenshots":[ { "url":"screenshot.png" } ] - } - + }, + { + "id": "ptlaunch", + "name": "Pattern Launcher", + "shortName": "Pattern Launcher", + "version": "0.02", + "description": "Directly launch apps from the clock screen with custom patterns.", + "icon": "app.png", + "tags": "tools", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "ptlaunch.app.js", "url": "app.js" }, + { "name": "ptlaunch.boot.js", "url": "boot.js" }, + { "name": "ptlaunch.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{"name":"ptlaunch.patterns.json"}] + }, + { "id": "clicompleteclk", + "name": "CLI complete clock", + "shortName":"CLI cmplt clock", + "version":"0.02", + "description": "Command line styled clock with lots of information", + "icon": "app.png", + "allow_emulator": true, + "type": "clock", + "tags": "clock,cli,command,bash,shell,weather,hrt", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"clicompleteclk.app.js","url":"app.js"}, + {"name":"clicompleteclk.img","url":"app-icon.js","evaluate":true} + ] +} ] diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog index 36859c060..c1b2588bb 100644 --- a/apps/93dub/ChangeLog +++ b/apps/93dub/ChangeLog @@ -2,3 +2,4 @@ 0.02: DiscoMinotaur's adjustments (removed battery and adjusted spacing) 0.03: Code style cleanup 0.04: Set 00:00 to 12:00 for 12 hour time +0.05: Display time, even on Thursday diff --git a/apps/93dub/README.md b/apps/93dub/README.md index 3830ee023..4d1ade582 100644 --- a/apps/93dub/README.md +++ b/apps/93dub/README.md @@ -5,8 +5,8 @@ Uses many portions from Espruino documentation, example watchfaces, and the waveclk app. It also sourced from Jon Barlow's 91 Dub v2.0 source code and resources and adapted for Bangle.js 2's screen. Time, date and the battery display works. It is not pixel perfect to the original. Contributors: -Leer10 -Orviwan (original watchface and assets) -Gordon Williams (Bangle.js, watchapps for reference code and documentation) -DiscoMinotaur (adjustments) -Ray Holder (minor 12 hour time rendering adjustment) +* Leer10 +* Orviwan (original watchface and assets) +* Gordon Williams (Bangle.js, watchapps for reference code and documentation) +* DiscoMinotaur (adjustments) +* Ray Holder (minor 12 hour time rendering adjustment, fix Thursdays) diff --git a/apps/93dub/app.js b/apps/93dub/app.js index 8f662a616..1b0f69a94 100644 --- a/apps/93dub/app.js +++ b/apps/93dub/app.js @@ -93,7 +93,7 @@ function draw(){ if (w == 1) {imgW = imgMon;} if (w == 2) {imgW = imgTue;} if (w == 3) {imgW = imgWed;} - if (w == 4) {imgW = imgThr;} + if (w == 4) {imgW = imgThu;} if (w == 5) {imgW = imgFri;} if (w == 6) {imgW = imgSat;} g.drawImage(imgW, 85, 63); diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index ffc2be495..b941a9937 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -41,3 +41,4 @@ Don't set beep vibration up on Bangle.js 2 (built in) 0.36: Add comments to .boot0 to make debugging a bit easier 0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app +0.38: Option to log to file if settings.log==2 diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index daf311fe6..3001bb5c1 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -23,8 +23,14 @@ if (s.ble!==false) { boot += `bleServiceOptions.hid=Bangle.HID;\n`; } } -if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth - if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal +if (s.log==2) { // logging to file + boot += `_DBGLOG=require("Storage").open("log.txt","a"); +`; +} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth + if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +LoopbackA.setConsole(true);\n`; + else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console /* If not programmable add our own handler for Bluetooth data to allow Gadgetbridge commands to be received*/ @@ -41,7 +47,10 @@ Bluetooth.on('line',function(l) { try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} });\n`; } else { - if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) + if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`; + else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth } // we just reset, so BLE should be on. diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 8bafff34a..de887bfa7 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -1,2 +1,3 @@ 0.01: Basic calendar 0.02: Make Bangle 2 compatible +0.03: Add setting to start week on Sunday diff --git a/apps/calendar/README.md b/apps/calendar/README.md index 19a60afc0..e22d06573 100644 --- a/apps/calendar/README.md +++ b/apps/calendar/README.md @@ -6,3 +6,8 @@ Basic calendar - Use `BTN4` (left screen tap) to go to the previous month - Use `BTN5` (right screen tap) to go to the next month + +## Settings + +- Starts on Sunday: whether the calendar should start on Sunday (default is Monday). + diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 6f3c33164..5707bd97a 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -18,6 +18,10 @@ const gray2 = "#888888"; const gray3 = "#bbbbbb"; const red = "#d41706"; +let settings = require('Storage').readJSON("calendar.json", true) || {}; +if (settings.startOnSun === undefined) + settings.startOnSun = false; + function drawCalendar(date) { g.setBgColor(color4); g.clearRect(0, 0, maxX, maxY); @@ -61,13 +65,18 @@ function drawCalendar(date) { ); g.setFont("6x8", fontSize); - const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + let dowLbls; + if (settings.startOnSun) { + dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; + } else { + dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + } dowLbls.forEach((lbl, i) => { g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); }); date.setDate(1); - const dow = date.getDay(); + const dow = date.getDay() + (settings.startOnSun ? 1 : 0); const dowNorm = dow === 0 ? 7 : dow; const monthMaxDayMap = { diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js new file mode 100644 index 000000000..f9c7783a3 --- /dev/null +++ b/apps/calendar/settings.js @@ -0,0 +1,24 @@ +(function(back) { + var FILE = "calendar.json"; + var settings = require('Storage').readJSON(FILE, true) || {}; + if (settings.startOnSun === undefined) + settings.startOnSun = true; + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Calendar" }, + "< Back" : () => back(), + 'Start on Sunday': { + value: settings.startOnSun, + format: v => v?"Yes":"No", + onchange: v => { + settings.startOnSun = v; + writeSettings(); + } + }, + }); +}) + diff --git a/apps/cliclockJS2Enhanced/ChangeLog b/apps/cliclockJS2Enhanced/ChangeLog index c7cb9e2c6..f4d146d5f 100644 --- a/apps/cliclockJS2Enhanced/ChangeLog +++ b/apps/cliclockJS2Enhanced/ChangeLog @@ -1,2 +1,3 @@ 0.01: Submitted to App Loader 0.02: Removed unneded code, added HID controlls thanks to t0m1o1 for his code :p +0.03: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) diff --git a/apps/cliclockJS2Enhanced/app.js b/apps/cliclockJS2Enhanced/app.js index 70e86f3d6..b6172b497 100644 --- a/apps/cliclockJS2Enhanced/app.js +++ b/apps/cliclockJS2Enhanced/app.js @@ -50,7 +50,7 @@ if (next) { setTimeout(drawApp, 1000); Bangle.setLocked(true); }, BTN1, { edge:"falling",repeat:true,debounce:50}); - Bangle.on('drag', function(e) { + Bangle.on('drag', function(e) { if(!e.b){ console.log(lasty); console.log(lastx); @@ -91,7 +91,7 @@ if (next) { lasty = lasty + e.dy; } }); - + } @@ -144,14 +144,15 @@ function writeLine(str,line){ } g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -drawAll(); + Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); var click = setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ - drawAll(); + drawAll(); // why do we redraw here?? }); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(); diff --git a/apps/clicompleteclk/ChangeLog b/apps/clicompleteclk/ChangeLog new file mode 100644 index 000000000..ee05bd582 --- /dev/null +++ b/apps/clicompleteclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New clock! +0.02: Load steps from Health Tracking app (if installed) diff --git a/apps/clicompleteclk/README.md b/apps/clicompleteclk/README.md new file mode 100644 index 000000000..62fdbbc61 --- /dev/null +++ b/apps/clicompleteclk/README.md @@ -0,0 +1,22 @@ +# Command line complete clock + +Command line styled clock with lots of information: + +It can show the following (depending on availability) information: +* Time +* Day of week +* Date +* Weather conditions and temperature (requires app [Weather](https://banglejs.com/apps/#weather)) +* Steps (requires app [Health Tracking](https://banglejs.com/apps/#health%20tracking) or a step widget) +* Heart rate (when screen is on and unlocked) + +## TODO +* Make time font bigger +* Show progress of steps (if any goal is set) +* Show trend of HRM out of history data + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icon +Icon taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/clicompleteclk/app-icon.js b/apps/clicompleteclk/app-icon.js new file mode 100644 index 000000000..b874bb6fa --- /dev/null +++ b/apps/clicompleteclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgI/8/4ACAqYv/F/PwAqgA6A==")) diff --git a/apps/clicompleteclk/app.js b/apps/clicompleteclk/app.js new file mode 100644 index 000000000..7fbdabcc1 --- /dev/null +++ b/apps/clicompleteclk/app.js @@ -0,0 +1,195 @@ +const storage = require('Storage'); +const locale = require("locale"); + +const font = "12x20"; +const fontsize = 1; +const fontheight = 19; + +const marginTop = 10; +const marginLeftTopic = 3; // margin of topics +const marginLeftData = 68; // margin of data values + +const topicColor = g.theme.dark ? "#fff" : "#000"; +const textColor = g.theme.dark ? "#0f0" : "#080"; + +let hrtValue; +let hrtValueIsOld = false; +let localTempValue; +let weatherTempString; +let lastHeartRateRowIndex; + +// 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; + drawAll(false); + }, 60000 - (Date.now() % 60000)); +} + +function drawAll(drawInfoToo){ + let now = new Date(); + updateTime(now); + if (drawInfoToo) { + drawInfo(now); + } + queueDraw(); +} + +function updateTime(now){ + if (!Bangle.isLCDOn()) return; + writeLineTopic("TIME", 1); + writeLine(locale.time(now,1),1); + if(now.getMinutes() == 0) + drawInfo(now); +} + +function drawInfo(now) { + if (now == undefined) + now = new Date(); + + let i = 2; + + writeLineTopic("DOWK", i); + writeLine(locale.dow(now),i); + i++; + + writeLineTopic("DATE", i); + writeLine(locale.date(now,1),i); + i++; + + /* + writeLineTopic("BAT", i); + const b = E.getBattery(); + writeLine(b + "%", i); // TODO make bars + i++; + */ + + // weather + const weatherJson = getWeather(); + if(weatherJson && weatherJson.weather){ + const currentWeather = weatherJson.weather; + + const weatherTempValue = locale.temp(currentWeather.temp-273.15); + weatherTempString = weatherTempValue; + writeLineTopic("WTHR", i); + writeLine(currentWeather.txt,i); + i++; + + writeLineTopic("TEMP", i); + writeLine(weatherTempValue,i); + i++; + } + + // steps + const steps = getSteps(); + if (steps != undefined) { + writeLineTopic("STEP", i); + writeLine(steps, i); + i++; + } + + drawHeartRate(i); +} + +function drawHeartRate(i) { + if (i == undefined) + i = lastHeartRateRowIndex; + writeLineTopic("HRTM", i); + if (hrtValue != undefined) { + if (!hrtValueIsOld) + writeLine(hrtValue,i); + else + writeLine(hrtValue,i, topicColor); + } + lastHeartRateRowIndex = i; +} + + +function writeLineTopic(str, line) { + var y = marginTop+line*fontheight; + g.setFont(font,fontsize); + g.setColor(topicColor).setFontAlign(-1,-1); + + g.clearRect(0,y,g.getWidth(),y+fontheight-1); + g.drawString("[" + str + "]",marginLeftTopic,y); +} + +function writeLine(str,line,pColor){ + if (pColor == undefined) + pColor = textColor; + var y = marginTop+line*fontheight; + g.setFont(font,fontsize); + g.setColor(pColor).setFontAlign(-1,-1); + g.drawString(str,marginLeftData,y); +} + + +function getSteps() { + var steps = 0; + let health; + try { + health = require("health"); + } catch (e) { + // Module health not found + } + if (health != undefined) { + health.readDay(new Date(), h=>steps+=h.steps); + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } else if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom.getSteps(); + } + return steps; +} + +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} + +// EVENTS: + +// turn on HRM when the LCD is unlocked +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + Bangle.setHRMPower(1,"clicompleteclk"); + if (hrtValue == undefined) + hrtValue = "..."; + else + hrtValueIsOld = true; + } else { + hrtValueIsOld = true; + Bangle.setHRMPower(0,"clicompleteclk"); + } + drawHeartRate(); +}); + +Bangle.on('lcdPower',function(on) { + if (on) { + drawAll(true); + } else { + hrtValueIsOld = true; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('HRM', function(hrm) { + //if(hrm.confidence > 90){ + hrtValueIsOld = false; + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + //} else { + // hrtValue = undefined; + //} +}); + +g.clear(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(true); diff --git a/apps/clicompleteclk/app.png b/apps/clicompleteclk/app.png new file mode 100644 index 000000000..104e6124a Binary files /dev/null and b/apps/clicompleteclk/app.png differ diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog index 2a93a0d5f..68249b622 100644 --- a/apps/cliock/ChangeLog +++ b/apps/cliock/ChangeLog @@ -7,3 +7,4 @@ 0.13: Use setUI, work with smaller screens and themes 0.14: Fix BTN1 (fix #853) Add light/dark theme support +0.15: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) diff --git a/apps/cliock/app.js b/apps/cliock/app.js index 0fd6ea580..d9271bf15 100644 --- a/apps/cliock/app.js +++ b/apps/cliock/app.js @@ -183,9 +183,6 @@ Bangle.on('HRM', function(hrm) { }); g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -drawAll(); Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); @@ -195,4 +192,7 @@ Bangle.setUI("clockupdown", btn=>{ if (btn<0) changeInfoMode(); if (btn>0) changeFunctionMode(); drawAll(); -}); \ No newline at end of file +}); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(); diff --git a/apps/emojuino/ChangeLog b/apps/emojuino/ChangeLog index 1c99f1970..04367183f 100644 --- a/apps/emojuino/ChangeLog +++ b/apps/emojuino/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Upgraded text to images, added welcome screen and subtitles. +0.03: Advertise app name as Espruino manufacturer data when idle. diff --git a/apps/emojuino/emojuino.js b/apps/emojuino/emojuino.js index 5b7670652..d241063e6 100644 --- a/apps/emojuino/emojuino.js +++ b/apps/emojuino/emojuino.js @@ -32,6 +32,7 @@ const CYCLE_BUZZ_MILLISECONDS = 50; const WELCOME_MESSAGE = 'Emojuino:\r\n\r\n< Swipe >\r\nto select\r\n\r\nTap\r\nto transmit'; // Non-user-configurable constants +const APP_ID = 'emojuino'; const IMAGE_INDEX = 0; const CODE_POINT_INDEX = 1; const EMOJI_PX = 96; @@ -40,12 +41,11 @@ const EMOJI_Y = (g.getHeight() - EMOJI_PX) / 2; const TX_X = 68; const TX_Y = 12; const FONT_SIZE = 24; -const BTN_WATCH_OPTIONS = { repeat: true, debounce: 20, edge: "falling" }; +const ESPRUINO_COMPANY_CODE = 0x0590; const UNICODE_CODE_POINT_ELIDED_UUID = [ 0x49, 0x6f, 0x49, 0x44, 0x55, 0x54, 0x46, 0x2d, 0x33, 0x32 ]; - // Global variables let emojiIndex = 0; let isToggleOn = false; @@ -100,9 +100,22 @@ function transmitEmoji(image, codePoint, duration) { } +// Transmit the app name under the Espruino company code to facilitate discovery +function transmitAppName() { + let options = { + showName: false, + manufacturer: ESPRUINO_COMPANY_CODE, + manufacturerData: JSON.stringify({ name: APP_ID }), + interval: 2000 + } + + NRF.setAdvertising({}, options); +} + + // Terminate the emoji transmission function terminateEmoji(displayIntervalId) { - NRF.setAdvertising({ }); + transmitAppName(); isTransmitting = false; clearInterval(displayIntervalId); drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], false); @@ -169,3 +182,4 @@ g.setFontAlign(0, 0); g.drawString(WELCOME_MESSAGE, g.getWidth() / 2, g.getHeight() / 2); Bangle.on('touch', handleTouch); Bangle.on('drag', handleDrag); +transmitAppName(); diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog index ec66c5568..96e7e4e9b 100644 --- a/apps/fwupdate/ChangeLog +++ b/apps/fwupdate/ChangeLog @@ -1 +1,4 @@ 0.01: Initial version +0.02: Add support for ZIPs + Find and download ZIPs direct from the Espruino website + Take 'beta' tag off diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 7230a77a8..b5c79a325 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -3,29 +3,44 @@ -

THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE - INSTRUCTIONS FOR BANGLE.JS 1 AND BANGLE.JS 2

-

Firmware updates using the App Loader are only possible on +

Firmware updates using the App Loader are only possible on Bangle.js 2. For firmware updates on Bangle.js 1 please - see the Bangle.js 1 instructions

+ see the Bangle.js 1 instructions

+

Your current firmware version is unknown

+

Firmware updates via this tool work differently to the NRF Connect method mentioned on + the Bangle.js page. Firmware + is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies + the new firmware into internal Storage.

+

 
     
+    
+    
 
     
   
diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog
index 9cebf0a31..316b98a84 100644
--- a/apps/gbmusic/ChangeLog
+++ b/apps/gbmusic/ChangeLog
@@ -5,3 +5,4 @@
 0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker
 0.06: Bangle.js 2 support
 0.07: Fix "previous" button image
+0.08: Fix scrolling title background color
diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js
index f514dfccd..1bddf70f7 100644
--- a/apps/gbmusic/app.js
+++ b/apps/gbmusic/app.js
@@ -91,7 +91,7 @@ function rScroller(l) {
     y = l.y+l.h/2;
   l.offset = l.offset%w;
   g.setClipRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
-    .setColor(l.col)
+    .setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
     .setFontAlign(-1, 0) // left center
     .clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
     .drawString(l.label, l.x-l.offset+40, y)
diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog
index cb22dd13f..365405846 100644
--- a/apps/gpsrec/ChangeLog
+++ b/apps/gpsrec/ChangeLog
@@ -28,3 +28,4 @@
 0.24: Better support for Bangle.js 2, avoid widget area for Graphs, smooth graphs more
 0.25: Fix issue where if Bangle.js 2 got a GPS fix but no reported time, errors could be caused by the widget (fix #935)
 0.26: Multiple bugfixes
+0.27: Map drawing with light theme (fix #1023) 
diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js
index df3353930..833a816ea 100644
--- a/apps/gpsrec/app.js
+++ b/apps/gpsrec/app.js
@@ -197,15 +197,14 @@ function plotTrack(info) {
   g.setColor(1,0.5,0.5);
   g.setFont("Vector",16);
   g.drawString("Track"+info.fn.toString()+" - Loading",10,220);
-  g.setColor(0,0,0);
+  g.setColor(g.theme.bg);
   g.fillRect(0,220,239,239);
   if (!info.qOSTM) {
     g.setColor(1, 0, 0);
     g.fillRect(9,80,11,120);
     g.fillPoly([9,60,19,80,0,80]);
-    g.setColor(1,1,1);
+    g.setColor(g.theme.fg);
     g.drawString("N",2,40);
-    g.setColor(1,1,1);
   } else {
     osm.lat = info.lat;
     osm.lon = info.lon;
@@ -228,7 +227,7 @@ function plotTrack(info) {
   g.setColor(0,1,0);
   g.fillCircle(mp.x,mp.y,5);
   if (info.qOSTM) g.setColor(1,0,0.55);
-  else g.setColor(1,1,1);
+  else g.setColor(g.theme.fg);
   l = f.readLine(f);
   while(l!==undefined) {
     c = l.split(",");
@@ -248,11 +247,11 @@ function plotTrack(info) {
   g.setColor(1,0,0);
   g.fillCircle(ox,oy,5);
   if (info.qOSTM) g.setColor(0, 0, 0);
-  else g.setColor(1,1,1);
+  else g.setColor(g.theme.fg);
   g.drawString(require("locale").distance(dist),g.getWidth() / 2, g.getHeight() - 20);
   g.setFont("6x8",2);
   g.setFontAlign(0,0,3);
-  g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40);
+  g.drawString("Back",g.getWidth() - 10, g.getHeight()/2);
   setWatch(function() {
     viewTrack(info.fn, info);
   }, global.BTN3||BTN1);
diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog
index 269a2cf62..a3a4f377c 100644
--- a/apps/messages/ChangeLog
+++ b/apps/messages/ChangeLog
@@ -12,4 +12,5 @@
       buzz on new message (fix #999)
 0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
       Fix phone icon (#1014)
-0.10: Respect the 'new' attribute if it was set from iOS integrations
\ No newline at end of file
+0.10: Respect the 'new' attribute if it was set from iOS integrations
+0.11: Open app when touching the widget (Bangle.js 2 only)
\ No newline at end of file
diff --git a/apps/messages/widget.js b/apps/messages/widget.js
index 245a303fc..6403c6b8d 100644
--- a/apps/messages/widget.js
+++ b/apps/messages/widget.js
@@ -1,4 +1,5 @@
 WIDGETS["messages"]={area:"tl",width:0,draw:function() {
+  Bangle.removeListener('touch', this.touch);
   if (!this.width) return;
   var c = (Date.now()-this.t)/1000;
   g.reset().setBgColor((c&1) ? "#0f0" : "#030").setColor((c&1) ? "#000" : "#fff");
@@ -12,6 +13,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() {
     WIDGETS["messages"].buzz(); // buzz every 4 seconds
   }
   setTimeout(()=>WIDGETS["messages"].draw(), 1000);
+  if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
 },show:function(quiet) {
   WIDGETS["messages"].t=Date.now(); // first time
   WIDGETS["messages"].l=Date.now()-10000; // last buzz
@@ -33,6 +35,10 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() {
     if (c=="-") Bangle.buzz(500).then(()=>setTimeout(b,100));
   }
   b();
+},touch:function(b,c) {
+  var w=WIDGETS["messages"];
+  if (!w||!w.width||c.xw.x+w.width||c.yw.y+23) return;
+  load("messages.app.js");
 }};
 /* We might have returned here if we were in the Messages app for a
 message but then the watch was never viewed. In that case we don't
diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog
index f94d719f4..57818c180 100644
--- a/apps/numerals/ChangeLog
+++ b/apps/numerals/ChangeLog
@@ -7,3 +7,4 @@
 0.07: Add date on touch and some improvements (see settings and readme)
 0.08: Add new draw styles, tidy up draw functionality
 0.09: Tweak for faster rendering
+0.10: Enhance for use with Bangle2, insert new draw mode 'thickfill'
\ No newline at end of file
diff --git a/apps/numerals/README.md b/apps/numerals/README.md
index ebf4c10fe..7a8c25212 100644
--- a/apps/numerals/README.md
+++ b/apps/numerals/README.md
@@ -7,14 +7,20 @@ Settings can be accessed through the app/widget settings menu of the Bangle.js
 
 ### Color:
 * rnd - shows numerals in different color combinations every time the watches wakes
-* r/g - red/green
-* y/w - yellow/white 
-* o/c - orange/cyan
-* b/y - blue/yellow'ish
+* r/g - red/green (Bangle1/Bangle2)
+* y/w - yellow/white (Bangle1 only)
+* o/c - orange/cyan (Bangle1 only)
+* b/y - blue/yellow'ish (Bangle1 only)
+* r/g - red/green (Bangle2 only)
+* g/b - green/blue (Bangle2 only)
+* r/c - red/cyan (Bangle2 only)
+* m/g - magenta/green (Bangle2 only)
 
 ### Draw mode
 * fill - fill numerals
 * frame - only shows outline of numerals
+* framefill - frame with lighter color fill
+* thickfill - thick frame in theme foreground color
 
 ### Menu button
 * choose button to start launcher menu with
diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js
index 3c7607eb1..baf859915 100644
--- a/apps/numerals/numerals.app.js
+++ b/apps/numerals/numerals.app.js
@@ -6,7 +6,7 @@
  * + see README.md for details
  */
 
-var numerals = {
+ var numerals = {
   0:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[30,25,61,25,69,33,69,67,61,75,30,75,22,67,22,33]],
   1:[[50,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,50,27,42,19,42,9]],
   2:[[9,1,82,1,90,9,90,53,82,61,21,61,21,74,82,74,90,82,90,92,82,100,9,100,1,92,1,48,9,40,70,40,70,27,9,27,1,19,1,9]],
@@ -19,8 +19,8 @@ var numerals = {
   9:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,69,74,69,61,9,61,1,53,1,9],[22,27,69,27,69,41,22,41]],
 };
 var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false;
-var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"];
-var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"];
+var _hCol = [];
+var _mCol = [];
 var _rCol = 0;
 var scale = g.getWidth()/240;
 var interval = 0;
@@ -42,15 +42,23 @@ var drawFuncs = {
   },
   thickframe : function(poly,isHole){
     g.drawPoly(poly,true);
-    g.drawPoly(translate(1,0,poly),true);
-    g.drawPoly(translate(1,1,poly),true);
-    g.drawPoly(translate(0,1,poly),true);
+    g.drawPoly(translate(1,0,poly,1),true);
+    g.drawPoly(translate(1,1,poly,1),true);
+    g.drawPoly(translate(0,1,poly,1),true);
+  },
+  thickfill : function(poly,isHole){
+    if (isHole) g.setColor(g.theme.bg);
+    g.fillPoly(poly,true);
+    g.setColor(g.theme.fg);
+    g.drawPoly(translate(1,0,poly,1),true);
+    g.drawPoly(translate(1,1,poly,1),true);
+    g.drawPoly(translate(0,1,poly,1),true);
   }
 };
 
-function translate(tx, ty, p){
+function translate(tx, ty, p, ascale){
   //return p.map((x, i)=> x+((i&1)?ty:tx));
-  return g.transformVertices(p, {x:tx,y:ty,scale:scale});
+  return g.transformVertices(p, {x:tx,y:ty,scale:ascale==undefined?scale:ascale});
 }
 
 
@@ -99,6 +107,18 @@ function setUpdateInt(set){
   if (set) interval=setInterval(draw, REFRESH_RATE);
 }
 
+function setUp(){
+  if (process.env.HWVERSION==1){
+    _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"];
+    _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"];
+  } else {
+    _hCol = ["#ff0000","#00ff00","#ff0000","#ff00ff"];
+    _mCol = ["#00ff00","#0000ff","#00ffff","#00ff00"];
+  }
+  if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length);
+}
+
+setUp();
 g.clear(1);
 // Show launcher when button pressed
 Bangle.setUI("clock");
@@ -111,11 +131,12 @@ if (settings.showDate) {
 }
 Bangle.on('lcdPower', function(on){
   if (on){
-    if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length);
+    setUp();
     draw();
     setUpdateInt(1);
   } else setUpdateInt(0);
 });
+Bangle.on('lock', () => setUp());
 
 Bangle.loadWidgets();
-Bangle.drawWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js
index 70f6e0d98..ae321322a 100644
--- a/apps/numerals/numerals.settings.js
+++ b/apps/numerals/numerals.settings.js
@@ -12,8 +12,8 @@
   }
   let numeralsSettings = storage.readJSON('numerals.json',1);
   if (!numeralsSettings) resetSettings();
-  let dm = ["fill","frame","framefill","thickframe"];
-  let col = ["rnd","r/g","y/w","o/c","b/y"];
+  let dm = ["fill","frame","framefill","thickframe","thickfill"];
+  let col = process.env.HWVERSION==1?["rnd","r/g","y/w","o/c","b/y"]:["rnd","r/g","g/b","r/c","m/g"];
   let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]];
   var menu={
     "" : { "title":"Numerals"},
diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog
index fc3ff3ba4..76f90de8b 100644
--- a/apps/pebble/ChangeLog
+++ b/apps/pebble/ChangeLog
@@ -1,3 +1,4 @@
 0.01: first release
 0.02: included deployment of pebble.settings.js in apps.json
 0.03: Changed time+calendar font to LECO1976Regular, changed to slanting boot
+0.04: Fix widget hiding code (fix #1046)
diff --git a/apps/pebble/pebble.app.js b/apps/pebble/pebble.app.js
index ce9ab3340..106e09b82 100644
--- a/apps/pebble/pebble.app.js
+++ b/apps/pebble/pebble.app.js
@@ -34,7 +34,7 @@ function draw() {
   // turn the warning on once we have dipped below 30%
   if (E.getBattery() < 30)
     batteryWarning = true;
-  
+
   // turn the warning off once we have dipped above 40%
   if (E.getBattery() > 40)
     batteryWarning = false;
@@ -57,7 +57,7 @@ function draw() {
   g.setFontAlign(0, -1);
   g.drawString(da[0].toUpperCase(), w/4, ha); // day of week
   g.drawString(getSteps(), 3*w/4, ha);
-  
+
   // time
   // white on red for battery warning
   g.setColor(!batteryWarning ? g.theme.bg : '#f00');
@@ -71,7 +71,7 @@ function draw() {
   // contrast bar
   g.setColor(g.theme.fg);
   g.fillRect(0, h3, w, h3 + t);
-  
+
   // the bottom
   g.setColor(settings.bg);
   g.fillRect(0, h3 + t, w, h);
@@ -111,9 +111,10 @@ g.clear();
 Bangle.loadWidgets();
 /*
  * we are not drawing the widgets as we are taking over the whole screen
- * so we will blank out the draw() functions of each widget
+ * so we will blank out the draw() functions of each widget and change the
+ * area to the top bar doesn't get cleared.
  */
-for (let wd of WIDGETS) {wd.draw=()=>{};}
+for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
 loadSettings();
 setInterval(draw, 15000); // refresh every 15s
 draw();
diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog
new file mode 100644
index 000000000..f50936885
--- /dev/null
+++ b/apps/ptlaunch/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Initial creation of the pattern launch app
+0.02: Turn on lcd when launching an app if the lock screen was disabled in the settings
diff --git a/apps/ptlaunch/README.md b/apps/ptlaunch/README.md
new file mode 100644
index 000000000..a69492782
--- /dev/null
+++ b/apps/ptlaunch/README.md
@@ -0,0 +1,46 @@
+# Pattern Launcher
+
+Directly launch apps from the clock screen with custom patterns.
+
+## Usage
+
+Create patterns and link them to apps in the Pattern Launcher app.
+
+Then launch the linked apps directly from the clock screen by simply drawing the desired pattern.
+
+## Screenshots and detailed steps
+
+![](main_menu.png)
+![](add_pattern.png)
+![](select_app.png)
+
+From the main menu you can:
+- Add a new pattern and link it to an app (first entry)
+    - To create a new pattern first select "Add Pattern"
+    - Now draw any pattern you like, this will later launch the linked app from the clock screen
+    - If you don't like the pattern, simply re-draw it. The previous pattern will be discarded.
+    - If you are happy with the pattern tap on screen or press the button to continue
+    - Now select the app you want to launch with the pattern.
+    - Note, you can bind multiple patterns to the same app.
+- Remove linked patterns (second entry)
+    - To remove a pattern first select "Remove Pattern"
+    - You will now see a list of apps that have patterns linked to them
+    - Simply select the app that you want to unlink. This will remove the saved pattern, but not the app itself!
+    - Note, that you can not actually preview the patterns. This makes removing patterns that are linked to the same app annoying. sorry!
+- Disable the lock screen on the clock screen from the settings (third entry)
+    - To launch the app from the pattern on the clock screen the watch must be unlocked.
+    - If this annoys you, you can disable the lock on the clock screen from the setting here
+
+## FAQ
+
+1) Nothing happens when I draw on the clock screen!
+
+Please double-check if you actually have a pattern linked to an app.
+
+2) I have a pattern linked to an app and still nothing happens when I draw on the clock screen!
+
+Make sure the watch is unlocked before you start drawing. If this bothers you, you can permanently disable the watch-lock from within the Pattern Launcher app (via the Settings).
+
+3) I have done all that and still nothing happens!
+
+Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry!
diff --git a/apps/ptlaunch/add_pattern.png b/apps/ptlaunch/add_pattern.png
new file mode 100644
index 000000000..c7cc38e82
Binary files /dev/null and b/apps/ptlaunch/add_pattern.png differ
diff --git a/apps/ptlaunch/app-icon.js b/apps/ptlaunch/app-icon.js
new file mode 100644
index 000000000..07f025d71
--- /dev/null
+++ b/apps/ptlaunch/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkE//ziEAiM//4ACmMAgMvA4fziIIBCAUgEAUCBwXxFIYYDCAvyHIgPCGwIgFCA0wAYIRCh49BLQoXB+AHEgYUBgQaCE4JGEHAZGDAAMBAQMjC4UBEw0Aj5PFDIchC4Q/BC5CtIgIXUGwIXDI6EBiCaCCYJ3RIganTa5AnEgbXIewwPGn4ICCA8hgESAoQABmUQgI2CCBQA/AAvzbIRuD/8xNwMTCBTfDPwbYEPAaPDf4LnFB4T/EEAS/Fj7vFZ4LvBgMiFIQXBCAwmEE4Q3BiUikTnHJAQFEJ4XwgERHgI/CJ4oAIC4QYBiYXDCxgXDgUzLQQXIGwpHDLoRHJgJmFO4arCO5MCK4QACh6nCJ4poDCAbGFe4QnEY4IgGG4oOCc4ofCbAj3C/8hiMSAoQYCiMRMQQQKAH4AGkMAJwsyiEBL4wQER4Z+DR5AQFX4ooCX44QGVobvOgMREAUQBwg3B+IXFc4cTmYUBgIXFgImCAAkf/59BkERIgMBBwo/BC5AkDCgwXOAAIMGI5xFBBgR3SJYinXa5A4EfAQQHewoABJAgfCCA/zFAMRn4OC/8xIAIWDCAJGBgIQBA=="))
\ No newline at end of file
diff --git a/apps/ptlaunch/app.js b/apps/ptlaunch/app.js
new file mode 100644
index 000000000..8ba1adf81
--- /dev/null
+++ b/apps/ptlaunch/app.js
@@ -0,0 +1,416 @@
+var storage = require("Storage");
+
+var DEBUG = false;
+var log = (message) => {
+  if (DEBUG) {
+    console.log(JSON.stringify(message));
+  }
+};
+
+var CIRCLE_RADIUS = 25;
+var CIRCLE_RADIUS_2 = CIRCLE_RADIUS * CIRCLE_RADIUS;
+
+var CIRCLES = [
+  { x: 25, y: 25, i: 0 },
+  { x: 87, y: 25, i: 1 },
+  { x: 150, y: 25, i: 2 },
+  { x: 25, y: 87, i: 3 },
+  { x: 87, y: 87, i: 4 },
+  { x: 150, y: 87, i: 5 },
+  { x: 25, y: 150, i: 6 },
+  { x: 87, y: 150, i: 7 },
+  { x: 150, y: 150, i: 8 },
+];
+
+var showMainMenu = () => {
+  log("loading patterns");
+  var storedPatterns = storage.readJSON("ptlaunch.patterns.json", 1) || {};
+
+  var mainmenu = {
+    "": {
+      title: "Pattern Launcher",
+    },
+    "< Back": () => {
+      log("cancel");
+      load();
+    },
+    "Add Pattern": () => {
+      log("creating pattern");
+      createPattern().then((pattern) => {
+        log("got pattern");
+        log(pattern);
+        log(pattern.length);
+
+        var confirmPromise = new Promise((resolve) => resolve(true));
+
+        if (storedPatterns[pattern]) {
+          log("pattern already exists. show confirmation prompt");
+          confirmPromise = E.showPrompt("Pattern already exists\nOverwrite?", {
+            title: "Confirm",
+            buttons: { Yes: true, No: false },
+          });
+        }
+
+        confirmPromise.then((confirm) => {
+          log("confirmPromise resolved: " + confirm);
+          if (!confirm) {
+            showMainMenu();
+            return;
+          }
+
+          log("selecting app");
+          getSelectedApp().then((app) => {
+            E.showMessage("Saving...");
+            log("got app");
+            log("saving pattern");
+
+            storedPatterns[pattern] = {
+              app: { name: app.name, src: app.src },
+            };
+            storage.writeJSON("ptlaunch.patterns.json", storedPatterns);
+            showMainMenu();
+          });
+        });
+      });
+    },
+    "Remove Pattern": () => {
+      log("selecting pattern through app");
+      getStoredPatternViaApp(storedPatterns).then((pattern) => {
+        E.showMessage("Deleting...");
+        delete storedPatterns[pattern];
+        storage.writeJSON("ptlaunch.patterns.json", storedPatterns);
+        showMainMenu();
+      });
+    },
+    Settings: () => {
+      var settings = storedPatterns["settings"] || {};
+
+      var settingsmenu = {
+        "": {
+          title: "Pattern Settings",
+        },
+        "< Back": () => {
+          log("cancel");
+          load();
+        },
+      };
+
+      if (settings.lockDisabled) {
+        settingsmenu["Enable lock"] = () => {
+          settings.lockDisabled = false;
+          storedPatterns["settings"] = settings;
+          Bangle.setOptions({ lockTimeout: 1000 * 30 });
+          storage.writeJSON("ptlaunch.patterns.json", storedPatterns);
+          showMainMenu();
+        };
+      } else {
+        settingsmenu["Disable lock"] = () => {
+          settings.lockDisabled = true;
+          storedPatterns["settings"] = settings;
+          storage.writeJSON("ptlaunch.patterns.json", storedPatterns);
+          Bangle.setOptions({ lockTimeout: 1000 * 60 * 60 * 24 * 365 });
+          showMainMenu();
+        };
+      }
+
+      E.showMenu(settingsmenu);
+    },
+  };
+  E.showMenu(mainmenu);
+};
+
+var drawCircle = (circle) => {
+  g.fillCircle(circle.x, circle.y, CIRCLE_RADIUS);
+};
+
+var positions = [];
+var createPattern = () => {
+  return new Promise((resolve) => {
+    E.showMenu();
+    g.clear();
+    g.setColor(0, 0, 0);
+    CIRCLES.forEach((circle) => drawCircle(circle));
+
+    var pattern = [];
+
+    var isFinished = false;
+    var finishHandler = () => {
+      if (pattern.length === 0 || isFinished) {
+        return;
+      }
+      log("Pattern is finished.");
+      isFinished = true;
+      Bangle.removeListener("drag", dragHandler);
+      Bangle.removeListener("tap", finishHandler);
+      resolve(pattern.join(""));
+    };
+    setWatch(() => finishHandler(), BTN);
+    setTimeout(() => Bangle.on("tap", finishHandler), 250);
+
+    var dragHandler = (position) => {
+      positions.push(position);
+
+      debounce().then(() => {
+        if (isFinished) {
+          return;
+        }
+        E.showMessage("Calculating...");
+        var t0 = Date.now();
+
+        log(positions.length);
+
+        var circlesClone = cloneCirclesArray();
+        pattern = [];
+
+        var step = Math.floor(positions.length / 100) + 1;
+
+        var p, a, b, circle;
+
+        for (var i = 0; i < positions.length; i += step) {
+          p = positions[i];
+
+          circle = circlesClone[0];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(0, 1);
+            }
+          }
+
+          circle = circlesClone[1];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(1, 1);
+            }
+          }
+
+          circle = circlesClone[2];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(2, 1);
+            }
+          }
+
+          circle = circlesClone[3];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(3, 1);
+            }
+          }
+
+          circle = circlesClone[4];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(4, 1);
+            }
+          }
+
+          circle = circlesClone[5];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(5, 1);
+            }
+          }
+
+          circle = circlesClone[6];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(6, 1);
+            }
+          }
+          circle = circlesClone[7];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(7, 1);
+            }
+          }
+
+          circle = circlesClone[8];
+          if (circle) {
+            a = p.x - circle.x;
+            b = p.y - circle.y;
+            if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+              pattern.push(circle.i);
+              circlesClone.splice(8, 1);
+            }
+          }
+        }
+        var tx = Date.now();
+        log(tx - t0);
+        positions = [];
+        var t1 = Date.now();
+        log(t1 - t0);
+
+        log("pattern:");
+        log(pattern);
+
+        log("redrawing");
+        g.clear();
+        g.setColor(0, 0, 0);
+        CIRCLES.forEach((circle) => drawCircle(circle));
+
+        g.setColor(1, 1, 1);
+        g.setFontAlign(0, 0);
+        g.setFont("6x8", 4);
+        pattern.forEach((circleIndex, patternIndex) => {
+          var circle = CIRCLES[circleIndex];
+          g.drawString(patternIndex + 1, circle.x, circle.y);
+        });
+        var t2 = Date.now();
+        log(t2 - t0);
+      });
+    };
+
+    Bangle.on("drag", dragHandler);
+  });
+};
+
+var getAppList = () => {
+  var appList = storage
+    .list(/\.info$/)
+    .map((appInfoFileName) => {
+      var appInfo = storage.readJSON(appInfoFileName, 1);
+      return (
+        appInfo && {
+          name: appInfo.name,
+          // type: appInfo.type,
+          // icon: appInfo.icon,
+          sortorder: appInfo.sortorder,
+          src: appInfo.src,
+        }
+      );
+    })
+    .filter((app) => app && !!app.src);
+  appList.sort((a, b) => {
+    var n = (0 | a.sortorder) - (0 | b.sortorder);
+    if (n) return n; // do sortorder first
+    if (a.name < b.name) return -1;
+    if (a.name > b.name) return 1;
+    return 0;
+  });
+
+  return appList;
+};
+
+var getSelectedApp = () => {
+  E.showMessage("Loading apps...");
+  return new Promise((resolve) => {
+    var selectAppMenu = {
+      "": {
+        title: "Select App",
+      },
+      "< Cancel": () => {
+        log("cancel");
+        showMainMenu();
+      },
+    };
+
+    var appList = getAppList();
+    appList.forEach((app) => {
+      selectAppMenu[app.name] = () => {
+        log("app selected");
+        log(app);
+        resolve(app);
+      };
+    });
+
+    E.showMenu(selectAppMenu);
+  });
+};
+
+var getStoredPatternViaApp = (storedPatterns) => {
+  E.showMessage("Loading patterns...");
+  log("getStoredPatternViaApp");
+  return new Promise((resolve) => {
+    var selectPatternMenu = {
+      "": {
+        title: "Select App",
+      },
+      "< Cancel": () => {
+        log("cancel");
+        showMainMenu();
+      },
+    };
+
+    log(storedPatterns);
+    var patterns = Object.keys(storedPatterns);
+    log(patterns);
+
+    patterns.forEach((pattern) => {
+      if (pattern) {
+        if (storedPatterns[pattern]) {
+          var app = storedPatterns[pattern].app;
+          if (!!app && !!app.name) {
+            var appName = app.name;
+            var i = 0;
+            while (appName in selectPatternMenu[app.name]) {
+              appName = app.name + i;
+              i++;
+            }
+            selectPatternMenu[appName] = () => {
+              log("pattern via app selected");
+              log(pattern);
+              log(app);
+              resolve(pattern);
+            };
+          }
+        }
+      }
+    });
+
+    E.showMenu(selectPatternMenu);
+  });
+};
+
+showMainMenu();
+
+//////
+// lib functions
+//////
+
+var debounceTimeoutId;
+var debounce = (delay) => {
+  if (debounceTimeoutId) {
+    clearTimeout(debounceTimeoutId);
+  }
+
+  return new Promise((resolve) => {
+    debounceTimeoutId = setTimeout(() => {
+      debounceTimeoutId = undefined;
+      resolve();
+    }, delay || 500);
+  });
+};
+
+var cloneCirclesArray = () => {
+  var circlesClone = Array(CIRCLES.length);
+
+  for (var i = 0; i < CIRCLES.length; i++) {
+    circlesClone[i] = CIRCLES[i];
+  }
+
+  return circlesClone;
+};
diff --git a/apps/ptlaunch/app.png b/apps/ptlaunch/app.png
new file mode 100644
index 000000000..14ed77f1d
Binary files /dev/null and b/apps/ptlaunch/app.png differ
diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js
new file mode 100644
index 000000000..14d390b13
--- /dev/null
+++ b/apps/ptlaunch/boot.js
@@ -0,0 +1,202 @@
+var DEBUG = true;
+var log = (message) => {
+  if (DEBUG) {
+    console.log(JSON.stringify(message));
+  }
+};
+
+var CIRCLE_RADIUS = 25;
+var CIRCLE_RADIUS_2 = CIRCLE_RADIUS * CIRCLE_RADIUS;
+
+var CIRCLES = [
+  { x: 25, y: 25, i: 0 },
+  { x: 87, y: 25, i: 1 },
+  { x: 150, y: 25, i: 2 },
+  { x: 25, y: 87, i: 3 },
+  { x: 87, y: 87, i: 4 },
+  { x: 150, y: 87, i: 5 },
+  { x: 25, y: 150, i: 6 },
+  { x: 87, y: 150, i: 7 },
+  { x: 150, y: 150, i: 8 },
+];
+
+var storedPatterns;
+var positions = [];
+var dragHandler = (position) => {
+  positions.push(position);
+
+  debounce().then(() => {
+    log(positions.length);
+
+    var circlesClone = cloneCirclesArray();
+    var pattern = [];
+
+    var step = Math.floor(positions.length / 100) + 1;
+
+    var p, a, b, circle;
+
+    for (var i = 0; i < positions.length; i += step) {
+      p = positions[i];
+
+      circle = circlesClone[0];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(0, 1);
+        }
+      }
+
+      circle = circlesClone[1];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(1, 1);
+        }
+      }
+
+      circle = circlesClone[2];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(2, 1);
+        }
+      }
+
+      circle = circlesClone[3];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(3, 1);
+        }
+      }
+
+      circle = circlesClone[4];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(4, 1);
+        }
+      }
+
+      circle = circlesClone[5];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(5, 1);
+        }
+      }
+
+      circle = circlesClone[6];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(6, 1);
+        }
+      }
+      circle = circlesClone[7];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(7, 1);
+        }
+      }
+
+      circle = circlesClone[8];
+      if (circle) {
+        a = p.x - circle.x;
+        b = p.y - circle.y;
+        if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
+          pattern.push(circle.i);
+          circlesClone.splice(8, 1);
+        }
+      }
+    }
+    positions = [];
+
+    pattern = pattern.join("");
+
+    if (pattern) {
+      if (storedPatterns[pattern]) {
+        var app = storedPatterns[pattern].app;
+        if (!!app && !!app.src) {
+          if (storedPatterns.settings) {
+            if (storedPatterns.settings.lockDisabled) {
+              Bangle.setLCDPower(true);
+            }
+          }
+
+          Bangle.removeListener("drag", dragHandler);
+          load(app.src);
+        }
+      }
+    }
+  });
+};
+
+var debounceTimeoutId;
+var debounce = (delay) => {
+  if (debounceTimeoutId) {
+    clearTimeout(debounceTimeoutId);
+  }
+
+  return new Promise((resolve) => {
+    debounceTimeoutId = setTimeout(() => {
+      debounceTimeoutId = undefined;
+      resolve();
+    }, delay || 500);
+  });
+};
+
+var cloneCirclesArray = () => {
+  var circlesClone = Array(CIRCLES.length);
+
+  for (var i = 0; i < CIRCLES.length; i++) {
+    circlesClone[i] = CIRCLES[i];
+  }
+
+  return circlesClone;
+};
+
+(function () {
+  var sui = Bangle.setUI;
+  Bangle.setUI = function (mode, cb) {
+    sui(mode, cb);
+    if (!mode) {
+      Bangle.removeListener("drag", dragHandler);
+      storedPatterns = {};
+      return;
+    }
+    if (!mode.startsWith("clock")) {
+      storedPatterns = {};
+      Bangle.removeListener("drag", dragHandler);
+      return;
+    }
+
+    var storage = require("Storage");
+    storedPatterns = storage.readJSON("ptlaunch.patterns.json", 1) || {};
+    if (Object.keys(storedPatterns).length > 0) {
+      Bangle.on("drag", dragHandler);
+      if (storedPatterns.settings) {
+        if (storedPatterns.settings.lockDisabled) {
+          Bangle.setOptions({ lockTimeout: 1000 * 60 * 60 * 24 * 365 });
+        }
+      }
+    }
+  };
+})();
diff --git a/apps/ptlaunch/main_menu.png b/apps/ptlaunch/main_menu.png
new file mode 100644
index 000000000..a4ecebb0f
Binary files /dev/null and b/apps/ptlaunch/main_menu.png differ
diff --git a/apps/ptlaunch/select_app.png b/apps/ptlaunch/select_app.png
new file mode 100644
index 000000000..56f0dfc83
Binary files /dev/null and b/apps/ptlaunch/select_app.png differ
diff --git a/apps/sensible/ChangeLog b/apps/sensible/ChangeLog
index ba597a22f..baa93f297 100644
--- a/apps/sensible/ChangeLog
+++ b/apps/sensible/ChangeLog
@@ -1,2 +1,3 @@
 0.01: New App!
 0.02: Corrected variable initialisation
+0.03: Advertise app name, added screenshots
diff --git a/apps/sensible/screenshot-acc.png b/apps/sensible/screenshot-acc.png
new file mode 100644
index 000000000..b286d1ed5
Binary files /dev/null and b/apps/sensible/screenshot-acc.png differ
diff --git a/apps/sensible/screenshot-bar.png b/apps/sensible/screenshot-bar.png
new file mode 100644
index 000000000..781ddbaa6
Binary files /dev/null and b/apps/sensible/screenshot-bar.png differ
diff --git a/apps/sensible/screenshot-gps.png b/apps/sensible/screenshot-gps.png
new file mode 100644
index 000000000..3fd1229e3
Binary files /dev/null and b/apps/sensible/screenshot-gps.png differ
diff --git a/apps/sensible/screenshot-hrm.png b/apps/sensible/screenshot-hrm.png
new file mode 100644
index 000000000..aa6a0574f
Binary files /dev/null and b/apps/sensible/screenshot-hrm.png differ
diff --git a/apps/sensible/screenshot-mag.png b/apps/sensible/screenshot-mag.png
new file mode 100644
index 000000000..829ac6727
Binary files /dev/null and b/apps/sensible/screenshot-mag.png differ
diff --git a/apps/sensible/screenshot-top.png b/apps/sensible/screenshot-top.png
new file mode 100644
index 000000000..e485933f0
Binary files /dev/null and b/apps/sensible/screenshot-top.png differ
diff --git a/apps/sensible/sensible.js b/apps/sensible/sensible.js
index c569ff720..45852adab 100644
--- a/apps/sensible/sensible.js
+++ b/apps/sensible/sensible.js
@@ -6,6 +6,7 @@
 
 // Non-user-configurable constants
 const APP_ID = 'sensible';
+const ESPRUINO_COMPANY_CODE = 0x0590;
 
 
 // Global variables
@@ -90,6 +91,19 @@ let magMenu = {
 };
 
 
+// Transmit the app name under the Espruino company code to facilitate discovery
+function transmitAppName() {
+  let options = {
+      showName: false,
+      manufacturer: ESPRUINO_COMPANY_CODE,
+      manufacturerData: JSON.stringify({ name: APP_ID }),
+      interval: 2000
+  }
+
+  NRF.setAdvertising({}, options);
+}
+
+
 // Update acceleration
 Bangle.on('accel', function(newAcc) {
   acc = newAcc;
@@ -155,6 +169,7 @@ Bangle.on('mag', function(newMag) {
 
 // On start: enable sensors and display main menu
 g.clear();
+transmitAppName();
 Bangle.setBarometerPower(isBarEnabled, APP_ID);
 Bangle.setGPSPower(isGpsEnabled, APP_ID);
 Bangle.setHRMPower(isHrmEnabled, APP_ID);
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index b393dda00..c676e3828 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -38,3 +38,4 @@
 0.33: Really fix 'beep' menu on Bangle.js 2 this time
 0.34: Remove Quiet Mode LCD settings: now handled by Quiet Mode Schedule app
 0.35: Change App/Widget settings to 'App Settings' so it fits on Bangle screen
+0.36: Added 'Utils' menu with helpful utilities for restoring Bangle.js
diff --git a/apps/setting/README.md b/apps/setting/README.md
index fb567030f..305c0b610 100644
--- a/apps/setting/README.md
+++ b/apps/setting/README.md
@@ -2,27 +2,26 @@
 
 This is Bangle.js's settings menu
 
-* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up)
 * **App/Widget Settings** settings specific to installed applications
 * **BLE** Bluetooth Settings menu - see below.
-* **Debug Info** should debug info be shown on the watch's screen or not?
 * **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected
 * **Vibration** enable/disable the vibration motor
 * **Quiet Mode** prevent notifications/alarms from vibrating/beeping/turning the screen on - see below
 * **Locale** set time zone/whether the clock is 12/24 hour (for supported clocks)
 * **Select Clock** if you have more than one clock face, select the default one
-* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device.
-  * **NOTE:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps.
 * **Set Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader
 * **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below.
 * **Theme** Adjust the colour scheme
-* **Reset Settings** Reset the settings to defaults
+* **Utils** Utilities - including resetting settings (see below)
 * **Turn Off** Turn Bangle.js off
 
 ## BLE - Bluetooth Settings
 
+* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up)
 * **BLE** is Bluetooth LE enabled and the watch connectable?
 * **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps? As long as your watch firmware is up to date, Gadgetbridge will work even with `Programmable` set to `Off`.
+* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device.
+  * **NOTE:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps.
 * **Passkey BETA** allows you to set a passkey that is required to connect and pair to Bangle.js. **Note:** This is Beta and you will almost certainly encounter issues connecting with Web Bluetooth using this option.
 * **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app.
   * **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again.
@@ -44,4 +43,16 @@ The exact effects depend on the app.  In general the watch will not wake up by i
   - Off: Normal operation
   - Alarms: Stops notifications, but "alarm" apps will still work
   - Silent: Blocks even alarms
-  
\ No newline at end of file
+
+## Utils
+
+
+* **Debug Info** should debug info be shown on the watch's screen or not?
+  * `Hide` (default) do not show debug information
+  * `Show` Show on the Bangle's screen (when not connected to Bluetooth or `Programmable:off`)
+  * `Log` Show on the Bangle's screen **and** write to a file called `log.txt` on Storage (when not connected to Bluetooth or `Programmable:off`). Warning - this file is appended to so may grow to be large if this is left enabled.
+* **Compact Storage** Removes deleted/old files from Storage - this will speed up your Bangle.js
+* **Rewrite Settings** Should not normally be required, but if `.boot0` has been deleted/corrupted (and so no settings are being loaded) this will fix it.
+* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours.  
+* **Reset Settings** Reset the settings (as set in this app) to defaults. Does not reset settings for other apps.
+* **Factory Reset** (not available on Bangle.js 1) - wipe **everything** and return to a factory state
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index e00c15462..8f95eb3bb 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -95,17 +95,8 @@ function showMainMenu() {
   const mainmenu = {
     '': { 'title': 'Settings' },
     '< Back': ()=>load(),
-    'Make Connectable': ()=>makeConnectable(),
     'App Settings': ()=>showAppSettingsMenu(),
     'BLE': ()=>showBLEMenu(),
-    'Debug Info': {
-      value: settings.log,
-      format: v => v ? "Show" : "Hide",
-      onchange: () => {
-        settings.log = !settings.log;
-        updateSettings();
-      }
-    },
     'Beep': beepMenuItem,
     'Vibration': {
       value: settings.vibrate,
@@ -134,7 +125,7 @@ function showMainMenu() {
     'Set Time': ()=>showSetTimeMenu(),
     'LCD': ()=>showLCDMenu(),
     'Theme': ()=>showThemeMenu(),
-    'Reset Settings': ()=>showResetMenu(),
+    'Utils': ()=>showUtilMenu(),
     'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() },
   };
 
@@ -146,6 +137,7 @@ function showBLEMenu() {
   var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"];
   E.showMenu({
     '< Back': ()=>showMainMenu(),
+    'Make Connectable': ()=>makeConnectable(),
     'BLE': {
       value: settings.ble,
       format: boolFormat,
@@ -476,21 +468,63 @@ function showLocaleMenu() {
   return E.showMenu(localemenu);
 }
 
-function showResetMenu() {
-  const resetmenu = {
-    '': { 'title': 'Reset' },
+function showUtilMenu() {
+  var menu = {
+    '': { 'title': 'Utilities' },
     '< Back': ()=>showMainMenu(),
+    'Debug Info': {
+      value: E.clip(0|settings.log,0,2),
+      format: v => ["Hide","Show","Log"][E.clip(0|v,0,2)],
+      onchange: v => {
+        settings.log = v;
+        updateSettings();
+      }
+    },
+    'Compact Storage': () => {
+      E.showMessage("Compacting...\nTakes approx\n1 minute",{title:"Storage"});
+      require("Storage").compact();
+      showUtilMenu();
+    },
+    'Rewrite Settings': () => {
+      require("Storage").write(".boot0","eval(require('Storage').read('bootupdate.js'));");
+      load("setting.app.js");
+    },
+    'Flatten Battery': () => {
+      E.showMessage('Flattening battery - this can take hours.\nLong-press button to cancel.');
+      Bangle.setLCDTimeout(0);
+      Bangle.setLCDPower(1);
+      if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat");
+      if (Bangle.setHRMPower) Bangle.setHRMPower(1,"flat");
+      if (Bangle.setCompassPower) Bangle.setCompassPower(1,"flat");
+      if (Bangle.setBarometerPower) Bangle.setBarometerPower(1,"flat");
+      if (Bangle.setHRMPower) Bangle.setGPSPower(1,"flat");
+      setInterval(function() {
+        var i=1000;while (i--);
+      }, 1);
+    },
     'Reset Settings': () => {
-      E.showPrompt('Reset Settings?').then((v) => {
+      E.showPrompt('Reset to Defaults?',{title:"Settings"}).then((v) => {
         if (v) {
           E.showMessage('Resetting');
           resetSettings();
-        }
-        setTimeout(showMainMenu, 50);
+          setTimeout(showMainMenu, 50);
+        } else showUtilMenu();
       });
     }
   };
-  return E.showMenu(resetmenu);
+  if (Bangle.factoryReset) {
+    menu['Factory Reset'] = ()=>{
+      E.showPrompt('This will remove everything!',{title:"Factory Reset"}).then((v) => {
+        if (v) {
+          E.showMessage();
+          Terminal.setConsole();
+          Bangle.factoryReset();
+        } else showUtilMenu();
+      });
+    }
+  }
+
+  return E.showMenu(menu);
 }
 
 function makeConnectable() {
diff --git a/apps/widmpsh/ChangeLog b/apps/widmpsh/ChangeLog
new file mode 100644
index 000000000..e432f82e5
--- /dev/null
+++ b/apps/widmpsh/ChangeLog
@@ -0,0 +1 @@
+0.01: Copied from widmp and flipped the phase directions!
diff --git a/apps/widmpsh/widget.js b/apps/widmpsh/widget.js
new file mode 100644
index 000000000..9115a4719
--- /dev/null
+++ b/apps/widmpsh/widget.js
@@ -0,0 +1,25 @@
+WIDGETS["widmoonsh"] = { area: "tr", width: 24, draw: function() {
+  const MC = 29.5305882, NM = 694039.09;
+  var r = 11, mx = this.x + 12; my = this.y + 12;
+
+  function moonPhase(d) {
+    var tmp, month = d.getMonth(), year = d.getFullYear(), day = d.getDate();
+    if (month < 3) {year--; month += 12;}
+    tmp = ((365.25 * year + 30.6 * ++month + day - NM) / MC);
+    return Math.round(((tmp - (tmp | 0)) * 7)+1);
+  }
+
+  const BLACK = g.theme.bg, MOON = 0x41f;
+  var moon = {
+    0: () => { g.reset().setColor(BLACK).fillRect(mx - r, my - r, mx + r, my + r);},
+    1: () => { moon[0](); g.setColor(MOON).drawCircle(mx, my, r);},
+    2: () => { moon[3](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
+    3: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx, my - r, mx + r, my + r);},
+    4: () => { moon[3](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
+    5: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r);},
+    6: () => { moon[7](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
+    7: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx - r, my - r, mx, my + r);},
+    8: () => { moon[7](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}
+  };
+  moon[moonPhase(Date())]();
+} };
diff --git a/apps/widmpsh/widget.png b/apps/widmpsh/widget.png
new file mode 100644
index 000000000..aeaadea4b
Binary files /dev/null and b/apps/widmpsh/widget.png differ
diff --git a/apps/widviz/ChangeLog b/apps/widviz/ChangeLog
index e1958b429..9785f4d84 100644
--- a/apps/widviz/ChangeLog
+++ b/apps/widviz/ChangeLog
@@ -1,3 +1,3 @@
- 0.01: New Widget
- 0.02: swipe left,right update
- 
+0.01: New Widget
+0.02: swipe left,right update
+0.03: Fix widget visibility code to the top bar isn't cleared by drawWidgets 
diff --git a/apps/widviz/widget.js b/apps/widviz/widget.js
index 241dabf61..1490cf11a 100644
--- a/apps/widviz/widget.js
+++ b/apps/widviz/widget.js
@@ -6,16 +6,21 @@
     if (!Bangle.isLCDOn() || saved) return;
     saved = [];
     for (var wd of WIDGETS) {
-      saved.push(wd.draw);
+      saved.push({d:wd.draw,a:wd.area});
       wd.draw=()=>{};
+      wd.area="";
     }
     g.setColor(0,0,0);
-    g.fillRect(0,0,239,23);
+    g.fillRect(0,0,g.getWidth(),23);
   }
 
   function reveal(){
     if (!Bangle.isLCDOn() || !saved) return;
-    for (var wd of WIDGETS) wd.draw = saved.shift();
+    for (var wd of WIDGETS) {
+      var o = saved.shift();
+      wd.draw = o.d;
+      wd.area = o.a;
+    }
     Bangle.drawWidgets();
     saved=null;
   }
diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js
index ea45dc19b..572364224 100755
--- a/bin/sanitycheck.js
+++ b/bin/sanitycheck.js
@@ -207,10 +207,10 @@ apps.forEach((app,appIdx) => {
     }
   });
   // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?)
-  if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json"))
+ /* if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json"))
     WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`)
   else if (dataNames.includes(app.id+".settings.json"))
-    WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`)
+    WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`)*/
   // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?)
   if (fileNames.includes(app.id+".settings.json"))
     WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`)