diff --git a/apps.json b/apps.json index 802451c46..c66a90fe0 100644 --- a/apps.json +++ b/apps.json @@ -1,4 +1,13 @@ [ + { "id": "boot", + "name": "Bootloader", + "icon": "bootloader.png", + "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", + "tags": "tool,system", + "storage": [ + {"name":".bootcde","url":"bootloader.js"} + ] + }, { "id": "trex", "name": "T-Rex", "icon": "trex.png", @@ -75,6 +84,19 @@ {"name":"*slevel","url":"spiritlevel-icon.js","evaluate":true} ] }, + { "id": "settings", + "name": "Settings", + "icon": "settings.png", + "description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat", + "tags": "tool,system", + "storage": [ + {"name":"+settings","url":"settings.json"}, + {"name":"-settings","url":"settings.js"}, + {"name":"=settings","url":"settings-init.js"}, + {"name":"@settings","url":"settings-default.json","evaluate":true}, + {"name":"*settings","url":"settings-icon.js","evaluate":true} + ] + }, { "id": "sbat", "name": "Battery Level Widget", "icon": "widget-battery.png", @@ -84,6 +106,28 @@ {"name":"=sbat","url":"widget-battery.js"} ] }, + { "id": "hrm", + "name": "Heart Rate Monitor", + "icon": "heartrate.png", + "description": "Measure your current heart rate", + "tags": "health", + "storage": [ + {"name":"+hrm","url":"heartrate.json"}, + {"name":"-hrm","url":"heartrate.js"}, + {"name":"*hrm","url":"heartrate-icon.js","evaluate":true} + ] + }, + { "id": "swatch", + "name": "Stopwatch", + "icon": "stopwatch.png", + "description": "Simple stopwatch with Lap Time recording", + "tags": "health", + "storage": [ + {"name":"+swatch","url":"stopwatch.json"}, + {"name":"-swatch","url":"stopwatch.js"}, + {"name":"*swatch","url":"stopwatch-icon.js","evaluate":true} + ] + }, { "id": "qrcode", "name": "Custom QR Code", "icon": "qrcode.png", diff --git a/apps/bootloader.js b/apps/bootloader.js new file mode 100644 index 000000000..a4aed10ef --- /dev/null +++ b/apps/bootloader.js @@ -0,0 +1,74 @@ +E.setTimeZone(1); +E.setFlags({pretokenise:1}); +setWatch(function() { + Bangle.setLCDMode("direct"); + g.clear(); + clearInterval(); + clearWatch(); + Bangle.removeAllListeners(); + + var s = require("Storage"); + var apps = s.list().filter(a=>a[0]=='+').map(app=>s.readJSON(app)); + var selected = 0; + var menuScroll = 0; + var menuShowing = false; + + function drawMenu() { + g.setFont("6x8",2); + g.setFontAlign(-1,0); + var n = 3; + if (selected>=n+menuScroll) menuScroll = 1+selected-n; + if (selectedn+menuScroll) g.fillPoly([120,239,100,219,140,219]); + else g.clearRect(100,219,140,239); + for (var i=0;i0) { + selected--; + drawMenu(); + } + }, BTN1, {repeat:true}); + setWatch(function() { + if (selected+1WIDGETS[k].draw()); +} +eval(require("Storage").read("-clock")); +require("Storage").list().filter(a=>a[0]=='=').forEach(widget=>eval(require("Storage").read(widget))); +setTimeout(drawWidgets,100); diff --git a/apps/bootloader.png b/apps/bootloader.png new file mode 100644 index 000000000..abbc8bf90 Binary files /dev/null and b/apps/bootloader.png differ diff --git a/apps/heartrate-icon.js b/apps/heartrate-icon.js new file mode 100644 index 000000000..cadbc7dfa --- /dev/null +++ b/apps/heartrate-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AH4AThnMAAXABJoMHBwgJJAAYMFAAIJLFxImCBJIuLABYuI4gXNNZFCC6AIFkZIQA4szC6vEmdMC60sC6nDmc8C6RDBC4irLC4gTBocymgGBoYXO4UyUwNEAYKrMC4ZEBUwNMVAR7LC4dDCoYBBSYJ7DoZQCC4kCmczkc0JIVM4UzmgaBAAQWD4AXBggJBJAIkBocs4c0BAQXJJARBD4c8oc8HAKZCI4gWCVAYXEJIJoCOovNC4cMUIQPB4RFBTAYAFIwapEC4JyCZAalHGAvCJYZYCVAYuIMIhjE5heGCwxhDMYTtIFw4wFoYsGFxIwF4YuRGAh7DFxxhGFyIYKCxqrGIpwwKFx4YGCyJJFCyQYDCygA/AH4AFA=")) diff --git a/apps/heartrate.js b/apps/heartrate.js new file mode 100644 index 000000000..c31314af1 --- /dev/null +++ b/apps/heartrate.js @@ -0,0 +1,69 @@ +Bangle.setLCDPower(1); +Bangle.setLCDTimeout(0); +Bangle.ioWr(0x80,0) +x=0; +var min=0,max=0; +var wasHigh = 0, wasLow = 0; +var lastHigh = getTime(); +var hrmList = []; +var hrm; + +function readHRM() { + var a = analogRead(D29); + var h = getTime(); + min=Math.min(min*0.97+a*0.03,a); + max=Math.max(max*0.97+a*0.03,a); + y = E.clip(170 - (a*960*4),100,230); + if (x==0) { + g.clearRect(0,100,239,239); + g.moveTo(-100,0); + } + /*g.setColor(0,1,0); + var z = 170 - (min*960*4); g.fillRect(x,z,x,z); + var z = 170 - (max*960*4); g.fillRect(x,z,x,z);*/ + g.setColor(1,1,1); + g.lineTo(x,y); + if ((max-min)>0.005) { + if (4*a > (min+3*max)) { // high + g.setColor(1,0,0); + g.fillRect(x,230,x,239); + g.setColor(1,1,1); + if (!wasHigh && wasLow) { + var currentHrm = 60/(h-lastHigh); + lastHigh = h; + if (currentHrm<250) { + while (hrmList.length>12) hrmList.shift(); + hrmList.push(currentHrm); + // median filter + var t = hrmList.slice(); // copy + t.sort(); + // average the middle 3 + var mid = t.length>>1; + hrm = (t[mid]+t[mid+1]+t[mid+2])/3; + g.setFontVector(40); + g.setFontAlign(0,0); + g.clearRect(0,0,239,100); + var str = Math.round(hrm); + var px = 120; + g.drawString(str,px,40); + px += g.stringWidth(str)/2; + g.setFont("6x8"); + g.drawString("BPM",px+20,60); + } + } + wasLow = 0; + wasHigh = 1; + } else if (4*a < (max+3*min)) { // low + wasLow = 1; + } else { // middle + g.setColor(0.5,0,0); + g.fillRect(x,230,x,239); + g.setColor(1,1,1); + wasHigh = 0; + } + } + x++; + if (x>239)x=0; +} + +setInterval(readHRM,50); diff --git a/apps/heartrate.json b/apps/heartrate.json new file mode 100644 index 000000000..ef69a8d5f --- /dev/null +++ b/apps/heartrate.json @@ -0,0 +1,5 @@ +{ + name:"Heart Rate", + icon:"*hrm", + src:"-hrm" +} diff --git a/apps/heartrate.png b/apps/heartrate.png new file mode 100644 index 000000000..5bc07d647 Binary files /dev/null and b/apps/heartrate.png differ diff --git a/apps/settings-default.json b/apps/settings-default.json new file mode 100644 index 000000000..26d07d76a --- /dev/null +++ b/apps/settings-default.json @@ -0,0 +1,8 @@ +{ + timeout: 10, // Default LCD timeout in seconds + vibrate: true, // Vibration enabled by default. App must support + beep: true, // Beep enabled by default. App must support + timezone: 0, // Set the timezone for the device + HID : false, // BLE HID mode, off by default + debug: false, // Debug mode disabled by default. App must support +} diff --git a/apps/settings-icon.js b/apps/settings-icon.js new file mode 100644 index 000000000..b60a6327d --- /dev/null +++ b/apps/settings-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABEIxGAAgwWO/+IAoIECDB0I////GIxGPAoIXR//854DCC54TCAAYXQCYXIDYYXOOIP4xnMAAJgBPoSMKOIP8xgXDGQJ9CGREIOIXMxmICwITBxh9CC5BEBBoIWBOweMMYZKIWwRdBAgQeDBYYXKFAKnFA4YXIKgJeBFwj0DI5UICgIXDOoIXC5gCBYRIXCN4I+CDgQXCYBJIBBwIXGBQIXX9AXJI4QXHI5Z3K/h3LTYanHX4TvLxhICAAaXCd5gnDd4gLDI5X4xj0CAAPIGwbvJIAeIDAQWBIwXPO5EIRIPP/mM5AFB5HIA4IFBC5DZECAPMDQJdB5AUKJQ3IxnvAgIsLC4ZYCAAgXnCIJxCXgQXPYYJxCAgKMMDAoRCAggA/AH4A/AAoA=")) diff --git a/apps/settings-init.js b/apps/settings-init.js new file mode 100644 index 000000000..c8e2e0c37 --- /dev/null +++ b/apps/settings-init.js @@ -0,0 +1,55 @@ +(function() { + var s = require('Storage').readJSON('@settings'); + if (s.HID) { + Bangle.HID = new Uint8Array([ + 0x05, 0x01, + 0x09, 0x06, + 0xA1, 0x01, + 0x05, 0x07, + 0x19, 0xe0, + 0x29, 0xe7, + 0x15, 0x00, + 0x25, 0x01, + 0x75, 0x01, + 0x95, 0x08, + 0x81, 0x02, + 0x95, 0x01, + 0x75, 0x08, + 0x81, 0x01, + 0x95, 0x05, + 0x75, 0x01, + 0x05, 0x08, + 0x19, 0x01, + 0x29, 0x05, + 0x91, 0x02, + 0x95, 0x01, + 0x75, 0x03, + 0x91, 0x01, + 0x95, 0x06, + 0x75, 0x08, + 0x15, 0x00, + 0x25, 0x73, + 0x05, 0x07, + 0x19, 0x00, + 0x29, 0x73, + 0x81, 0x00, + 0x09, 0x05, + 0x15, 0x00, + 0x26, 0xFF, 0x00, + 0x75, 0x08, + 0x95, 0x02, + 0xB1, 0x02, + 0xC0 + ]); + NRF.setServices(undefined, { + uart: true, hid: Bangle.HID, + }); + } + + if (!s.vibrate) Bangle.buzz=()=>Promise.resolve(); + if (!s.beep) Bangle.beep=()=>Promise.resolve(); + Bangle.setLCDTimeout(s.timeout); + if (!s.timeout) Bangle.setLCDPower(1); + E.setTimeZone(s.timezone); +})(); + diff --git a/apps/settings.js b/apps/settings.js new file mode 100644 index 000000000..602a0ab79 --- /dev/null +++ b/apps/settings.js @@ -0,0 +1,135 @@ +Bangle.setLCDPower(1); +Bangle.setLCDTimeout(0); + +g.clear(); +const storage = require('Storage'); +let settings; + +function debug(msg, arg) { + if (settings.debug) + console.log(msg, arg); +} + +function updateSettings() { + debug('updating settings', settings); + storage.erase('@settings'); + storage.write('@settings', settings); +} + +function resetSettings() { + settings = { + timeout: 10, + vibrate: true, + beep: true, + timezone: 0, + HID : false, + debug: false, + }; + setLCDTimeout(settings.timeout); + updateSettings(); +} + +try { + settings = storage.readJSON('@settings'); +} catch (e) {} +if (!settings) resetSettings(); + +const boolFormat = (v) => v ? "On" : "Off"; + +function showMainMenu() { + const mainmenu = { + '': { 'title': 'Settings' }, + 'LCD Timeout': { + value: settings.timeout, + min: 0, + max: 60, + step: 5, + onchange: v => { + settings.timeout = 0 | v; + updateSettings(); + Bangle.setLCDTimeout(settings.timeout); + } + }, + 'Beep': { + value: settings.beep, + format: boolFormat, + onchange: () => { + settings.beep = !settings.beep; + updateSettings(); + if (settings.beep) { + Bangle.beep(1); + } + } + }, + 'Vibration': { + value: settings.vibrate, + format: boolFormat, + onchange: () => { + settings.vibrate = !settings.vibrate; + updateSettings(); + if (settings.vibrate) { + VIBRATE.write(1); + setTimeout(()=>VIBRATE.write(0), 10); + } + } + }, + 'Time Zone': { + value: settings.timezone, + min: -11, + max: 12, + step: 1, + onchange: v => { + settings.timezone = 0 | v; + updateSettings(); + } + }, + 'HID': { + value: settings.HID, + format: boolFormat, + onchange: () => { + settings.HID = !settings.HID; + updateSettings(); + } + }, + 'Debug': { + value: settings.debug, + format: boolFormat, + onchange: () => { + settings.debug = !settings.debug; + updateSettings(); + } + }, + 'Reset': showResetMenu, + 'Turn Off': Bangle.off, + '< Back': load + }; + return Bangle.menu(mainmenu); +} + +function showResetMenu() { + const resetmenu = { + '': { 'title': 'Reset' }, + '< Back': showMainMenu, + 'Reset Settings': () => { + E.showPrompt('Reset Settings?').then((v) => { + if (v) { + E.showMessage('Resetting'); + resetSettings(); + } + setTimeout(showMainMenu, 50); + }); + }, + // this is include for debugging. remove for production + /*'Erase': () => { + storage.erase('=settings'); + storage.erase('-settings'); + storage.erase('@settings'); + storage.erase('*settings'); + storage.erase('+settings'); + E.reboot(); + }*/ + }; + return Bangle.menu(resetmenu); +} + +showMainMenu(); diff --git a/apps/settings.json b/apps/settings.json new file mode 100644 index 000000000..b0dee3cc6 --- /dev/null +++ b/apps/settings.json @@ -0,0 +1,5 @@ +{ + name: 'Settings', + icon: '*settings', + src: '-settings' +} diff --git a/apps/settings.png b/apps/settings.png new file mode 100644 index 000000000..71e753523 Binary files /dev/null and b/apps/settings.png differ diff --git a/apps/spiritlevel.js b/apps/spiritlevel.js index ec02321f9..492fc60e1 100644 --- a/apps/spiritlevel.js +++ b/apps/spiritlevel.js @@ -1,6 +1,13 @@ g.clear(); var old = {x:0,y:0}; Bangle.on('accel',function(v) { + var max = Math.max(Math.abs(v.x),Math.abs(v.y),Math.abs(v.z)); + if (Math.abs(v.y)==max) { + v = {x:v.x,y:v.z,z:v.y}; + } else if (Math.abs(v.x)==max) { + v = {x:v.z,y:v.y,z:v.x}; + } + var d = Math.sqrt(v.x*v.x+v.y*v.y); var ang = Math.atan2(d,Math.abs(v.z))*180/Math.PI; diff --git a/apps/stopwatch-icon.js b/apps/stopwatch-icon.js new file mode 100644 index 000000000..9c3f21c86 --- /dev/null +++ b/apps/stopwatch-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AFECkQACkAX/C/4AKgMRiMQVCYXFGYIWOxAAEgJLPCwuIwRkCCyQABC5sICIUzn/zmYYEFxs//4AC+ZJCL5QuCCwXzDAuAFxeDCYYyDnALBC5YSD+UvGApGKFwYXFGARIIC4OPCIfxj4FD/AXJRgwXFJBQJBCAYXBiYGEC5ReE/8xC4pgBC50hiQXPOwn/iMRMwgXP+QXBVAiQBC8pHCO6rvFC6IAGC5TXFAAzvLUAgAF+YXJhB4GAAiOBwAXJMBReBC5BILIxQXDGBAuBC5RIBGA4uCIxIwDDAoWCFxQwExEzn/zmYGCFxYwEAAwWMDBIWODA4WQAH4AXA==")) diff --git a/apps/stopwatch.js b/apps/stopwatch.js new file mode 100644 index 000000000..98d6cba83 --- /dev/null +++ b/apps/stopwatch.js @@ -0,0 +1,84 @@ +var tStart = Date.now(); +var tCurrent = Date.now(); +var started = false; +var timeY = 60; +var hsXPos = 0; +var lapTimes = []; +var displayInterval; + +function timeToText(t) { + var secs = Math.floor(t/1000)%60; + var mins = Math.floor(t/60000); + var hs = Math.floor(t/10)%100; + return mins+":"+("0"+secs).substr(-2)+"."+("0"+hs).substr(-2); +} +function updateLabels() { + g.clear(); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + g.drawString(started?"STOP":"GO",230,120); + if (!started) g.drawString("RESET",230,50); + g.drawString("LAP",230,190); + g.setFont("6x8",1); + g.setFontAlign(-1,-1); + for (var i in lapTimes) { + g.drawString(i+": "+timeToText(lapTimes[i]),10,timeY + 30 + i*8); + } + drawsecs(); +} +function drawsecs() { + var t = tCurrent-tStart; + g.setFont("Vector",48); + g.setFontAlign(0,0); + var secs = Math.floor(t/1000)%60; + var mins = Math.floor(t/60000); + var txt = mins+":"+("0"+secs).substr(-2); + var x = 100; + g.clearRect(0,timeY-26,200,timeY+26); + g.drawString(txt,x,timeY); + hsXPos = 5+x+g.stringWidth(txt)/2; + drawms(); +} +function drawms() { + var t = tCurrent-tStart; + var hs = Math.floor(t/10)%100; + g.setFontAlign(-1,0); + g.setFont("6x8",2); + g.clearRect(hsXPos,timeY,220,timeY+20); + g.drawString("."+("0"+hs).substr(-2),hsXPos,timeY+10); +} + +setWatch(function() { // Start/stop + started = !started; + if (started) + tStart = Date.now()+tStart-tCurrent; + tCurrent = Date.now(); + if (displayInterval) { + clearInterval(displayInterval); + displayInterval = undefined; + } + updateLabels(); + if (started) + displayInterval = setInterval(function() { + var last = tCurrent; + if (started) tCurrent = Date.now(); + if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) + drawsecs(); + else + drawms(); + }, 20); +}, BTN2, {repeat:true}); +setWatch(function() { // Reset + if (!started) { + tStart = tCurrent = Date.now(); + } + updateLabels(); +}, BTN1, {repeat:true}); +setWatch(function() { // Lap + if (started) tCurrent = Date.now(); + lapTimes.unshift(tCurrent-tStart); + tStart = tCurrent; + updateLabels(); +}, BTN3, {repeat:true}); + +updateLabels(); diff --git a/apps/stopwatch.json b/apps/stopwatch.json new file mode 100644 index 000000000..2b57a1cbc --- /dev/null +++ b/apps/stopwatch.json @@ -0,0 +1,5 @@ +{ + name:"Stopwatch", + icon:"*swatch", + src:"-swatch" +} diff --git a/apps/stopwatch.png b/apps/stopwatch.png new file mode 100644 index 000000000..92ffe73b7 Binary files /dev/null and b/apps/stopwatch.png differ