diff --git a/apps/90sclk/settings.js b/apps/90sclk/settings.js index 8f97cd317..74241d603 100644 --- a/apps/90sclk/settings.js +++ b/apps/90sclk/settings.js @@ -21,7 +21,6 @@ '< Back': back, 'Full Screen': { value: settings.fullscreen, - format: () => (settings.fullscreen ? 'Yes' : 'No'), onchange: () => { settings.fullscreen = !settings.fullscreen; save(); diff --git a/apps/banglexercise/settings.js b/apps/banglexercise/settings.js index 3208c6eca..0b52acd72 100644 --- a/apps/banglexercise/settings.js +++ b/apps/banglexercise/settings.js @@ -11,7 +11,6 @@ '< Back': back, 'Buzz': { value: "buzz" in settings ? settings.buzz : false, - format: () => (settings.buzz ? 'Yes' : 'No'), onchange: () => { settings.buzz = !settings.buzz; save('buzz', settings.buzz); diff --git a/apps/binaryclk/ChangeLog b/apps/binaryclk/ChangeLog index a78cbe479..7b6810faa 100644 --- a/apps/binaryclk/ChangeLog +++ b/apps/binaryclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: Added app 0.02: Removed unneeded squares +0.03: Added settings with fullscreen option diff --git a/apps/binaryclk/app.js b/apps/binaryclk/app.js index 8b030ccff..94c906104 100644 --- a/apps/binaryclk/app.js +++ b/apps/binaryclk/app.js @@ -1,3 +1,7 @@ +var settings = Object.assign({ + fullscreen: false, +}, require('Storage').readJSON("binaryclk.json", true) || {}); + function draw() { var dt = new Date(); var h = dt.getHours(), m = dt.getMinutes(); @@ -11,10 +15,14 @@ function draw() { g.clearRect(Bangle.appRect); let i = 0; + var gap = 8; + var mgn = 20; + if (settings.fullscreen) { + gap = 12; + mgn = 0; + } const sq = 29; - const gap = 8; - const mgn = 20; - const pos = sq + gap; + var pos = sq + gap; for (let r = 3; r >= 0; r--) { for (let c = 0; c < 4; c++) { @@ -26,14 +34,15 @@ function draw() { } i++; } - g.clearRect(mgn/2 + gap, mgn + gap, mgn/2 + gap + sq, mgn + 2 * gap + 2 * sq); - g.clearRect(mgn/2 + 3 * gap + 2 * sq, mgn + gap, mgn/2 + 3 * gap + 3 * sq, mgn + gap + sq); + g.clearRect(mgn/2 + gap, mgn + gap, mgn/2 + gap + sq, mgn + 2 * gap + 2 * sq); + g.clearRect(mgn/2 + 3 * gap + 2 * sq, mgn + gap, mgn/2 + 3 * gap + 3 * sq, mgn + gap + sq); } - g.clear(); draw(); var secondInterval = setInterval(draw, 60000); Bangle.setUI("clock"); -Bangle.loadWidgets(); -Bangle.drawWidgets(); +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} diff --git a/apps/binaryclk/metadata.json b/apps/binaryclk/metadata.json index a81a39b7a..b4ddc6544 100644 --- a/apps/binaryclk/metadata.json +++ b/apps/binaryclk/metadata.json @@ -1,7 +1,7 @@ { "id": "binaryclk", "name": "Bin Clock", - "version": "0.02", + "version": "0.03", "description": "Clock face to show binary time in 24 hr format", "icon": "app-icon.png", "screenshots": [{"url":"screenshot.png"}], @@ -11,6 +11,8 @@ "allow_emulator": true, "storage": [ {"name":"binaryclk.app.js","url":"app.js"}, + {"name":"binaryclk.settings.js","url":"settings.js"}, {"name":"binaryclk.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"binaryclk.json"}] } diff --git a/apps/binaryclk/settings.js b/apps/binaryclk/settings.js new file mode 100644 index 000000000..0ef30e19d --- /dev/null +++ b/apps/binaryclk/settings.js @@ -0,0 +1,22 @@ +(function(back) { + var FILE = "binaryclk.json"; + var settings = Object.assign({ + fullscreen: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Bin Clock" }, + "< Back" : () => back(), + 'Fullscreen': { + value: settings.fullscreen, + onchange: v => { + settings.fullscreen = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js index 116253fda..8bcf0ae0f 100644 --- a/apps/bwclk/settings.js +++ b/apps/bwclk/settings.js @@ -32,7 +32,6 @@ }, 'Show Lock': { value: settings.showLock, - format: () => (settings.showLock ? 'Yes' : 'No'), onchange: () => { settings.showLock = !settings.showLock; save(); @@ -40,7 +39,6 @@ }, 'Hide Colon': { value: settings.hideColon, - format: () => (settings.hideColon ? 'Yes' : 'No'), onchange: () => { settings.hideColon = !settings.hideColon; save(); diff --git a/apps/bwclklite/settings.js b/apps/bwclklite/settings.js index 2d3916a3d..4c59198c6 100644 --- a/apps/bwclklite/settings.js +++ b/apps/bwclklite/settings.js @@ -32,7 +32,6 @@ }, 'Show Lock': { value: settings.showLock, - format: () => (settings.showLock ? 'Yes' : 'No'), onchange: () => { settings.showLock = !settings.showLock; save(); @@ -40,7 +39,6 @@ }, 'Hide Colon': { value: settings.hideColon, - format: () => (settings.hideColon ? 'Yes' : 'No'), onchange: () => { settings.hideColon = !settings.hideColon; save(); diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 6edb54f65..bd8e6117b 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -17,3 +17,4 @@ 0.15: Edit holidays on device in settings 0.16: Add menu to fast open settings to edit holidays Display Widgets in menus +0.17: Load holidays before events so the latter is not overpainted diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 7477775ca..f9fd43de8 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -43,24 +43,24 @@ const dowLbls = function() { }(); const loadEvents = () => { + // add holidays & other events + events = (require("Storage").readJSON("calendar.days.json",1) || []).map(d => { + const date = new Date(d.date); + const o = {date: date, msg: d.name, type: d.type}; + if (d.repeat) { + o.repeat = d.repeat; + } + return o; + }); // all alarms that run on a specific date - events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => { + events = events.concat((require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => { const date = new Date(a.date); const time = timeutils.decodeTime(a.t); date.setHours(time.h); date.setMinutes(time.m); date.setSeconds(time.s); return {date: date, msg: a.msg, type: "e"}; - }); - // add holidays & other events - (require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => { - const date = new Date(d.date); - const o = {date: date, msg: d.name, type: d.type}; - if (d.repeat) { - o.repeat = d.repeat; - } - events.push(o); - }); + })); }; const loadSettings = () => { @@ -280,14 +280,12 @@ const showMenu = function() { setUI(); }, /*LANG*/"Exit": () => load(), - /*LANG*/"Settings": () => { - const appSettings = eval(require('Storage').read('calendar.settings.js')); - appSettings(() => { + /*LANG*/"Settings": () => + eval(require('Storage').read('calendar.settings.js'))(() => { loadSettings(); loadEvents(); showMenu(); - }); - }, + }), }; if (require("Storage").read("alarm.app.js")) { menu[/*LANG*/"Launch Alarms"] = () => { diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index e263efe35..895f8f7aa 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.16", + "version": "0.17", "description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 63a2b0f93..ae090c1d7 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -30,7 +30,6 @@ }, /*LANG*/'show widgets': { value: !!settings.showWidgets, - format: () => (settings.showWidgets ? 'Yes' : 'No'), onchange: x => save('showWidgets', x), }, /*LANG*/'update interval': { @@ -45,7 +44,6 @@ }, /*LANG*/'show big weather': { value: !!settings.showBigWeather, - format: () => (settings.showBigWeather ? 'Yes' : 'No'), onchange: x => save('showBigWeather', x), }, /*LANG*/'colorize icons': ()=>showCircleMenus() @@ -87,8 +85,7 @@ const colorizeIconKey = circleName + "colorizeIcon"; menu[/*LANG*/'circle ' + circleId] = { value: settings[colorizeIconKey] || false, - format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'), - onchange: x => save(colorizeIconKey, x), + onchange: x => save(colorizeIconKey, x), }; } E.showMenu(menu); diff --git a/apps/clicompleteclk/settings.js b/apps/clicompleteclk/settings.js index 2df20ed3e..0213ead6e 100644 --- a/apps/clicompleteclk/settings.js +++ b/apps/clicompleteclk/settings.js @@ -9,7 +9,6 @@ '': { 'title': 'CLI complete clk' }, 'Show battery': { value: "battery" in settings ? settings.battery : false, - format: () => (settings.battery ? 'Yes' : 'No'), onchange: () => { settings.battery = !settings.battery; save('battery', settings.battery); @@ -27,7 +26,6 @@ }, 'Show weather': { value: "weather" in settings ? settings.weather : false, - format: () => (settings.weather ? 'Yes' : 'No'), onchange: () => { settings.weather = !settings.weather; save('weather', settings.weather); @@ -35,7 +33,6 @@ }, 'Show steps': { value: "steps" in settings ? settings.steps : false, - format: () => (settings.steps ? 'Yes' : 'No'), onchange: () => { settings.steps = !settings.steps; save('steps', settings.steps); @@ -43,7 +40,6 @@ }, 'Show heartrate': { value: "heartrate" in settings ? settings.heartrate : false, - format: () => (settings.heartrate ? 'Yes' : 'No'), onchange: () => { settings.heartrate = !settings.heartrate; save('heartrate', settings.heartrate); diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index 68cd82cfa..77cc63c98 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -8,3 +8,4 @@ 0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting edge 2v18 ones), allowing compatability with the Back Swipe app. 0.09: Fix colors settings, where color was stored as string instead of the expected int. +0.10: Fix touch region for letters diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 78ef11bd4..2e40f3a77 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -107,7 +107,7 @@ exports.input = function(options) { "ram"; // ABCDEFGHIJKLMNOPQRSTUVWXYZ // Choose character by draging along red rectangle at bottom of screen - if (event.y >= ( (R.y+R.h) - 12 )) { + if (event.y >= ( (R.y+R.h) - 26 )) { // Translate x-position to character if (event.x < ABCPADDING) { abcHL = 0; } else if (event.x >= 176-ABCPADDING) { abcHL = 25; } @@ -139,7 +139,7 @@ exports.input = function(options) { // 12345678901234567890 // Choose number or puctuation by draging on green rectangle - else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) { + else if ((event.y < ( (R.y+R.h) - 26 )) && (event.y > ( (R.y+R.h) - 52 ))) { // Translate x-position to character if (event.x < NUMPADDING) { numHL = 0; } else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; } diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index 090c37a01..c4596d7bd 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -1,6 +1,6 @@ { "id": "dragboard", "name": "Dragboard", - "version":"0.09", + "version":"0.10", "description": "A library for text input via swiping keyboard", "icon": "app.png", "type":"textinput", diff --git a/apps/forge/ChangeLog b/apps/forge/ChangeLog index 263d4078d..0c651c90b 100644 --- a/apps/forge/ChangeLog +++ b/apps/forge/ChangeLog @@ -1 +1,2 @@ 0.01: attempt to import +0.02: Make it possible for Fastload Utils to fastload into this app. diff --git a/apps/forge/forge.app.js b/apps/forge/forge.app.js index b972e13bb..b179fb540 100644 --- a/apps/forge/forge.app.js +++ b/apps/forge/forge.app.js @@ -1,5 +1,7 @@ // App Forge +"Bangle.loadWidgets()"; // Facilitates fastloading to this app via Fastload Utils, while still not loading widgets on standard `load` calls. + st = require('Storage'); l = /^a\..*\.js$/; diff --git a/apps/forge/metadata.json b/apps/forge/metadata.json index 03671a647..6e13a4df3 100644 --- a/apps/forge/metadata.json +++ b/apps/forge/metadata.json @@ -1,6 +1,6 @@ { "id": "forge", "name": "App Forge", - "version":"0.01", + "version":"0.02", "description": "Easy way to run development versions of your apps", "icon": "app.png", "readme": "README.md", diff --git a/apps/fuzzyw/fuzzyw.settings.js b/apps/fuzzyw/fuzzyw.settings.js index 8feb30bfb..535f91d67 100644 --- a/apps/fuzzyw/fuzzyw.settings.js +++ b/apps/fuzzyw/fuzzyw.settings.js @@ -23,7 +23,6 @@ '< Back': back, 'Show Widgets': { value: settings.showWidgets, - format: () => (settings.showWidgets ? 'Yes' : 'No'), onchange: () => { settings.showWidgets = !settings.showWidgets; save(); diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index b1a356d4e..0b1387967 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -431,7 +431,7 @@ function handleUpload() { storage:[ {name:"RAM", content:hexJS}, ] - }); + }, { noFinish: true }); } document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false); diff --git a/apps/health/settings.js b/apps/health/settings.js index 88c8061c6..4d6e1a067 100644 --- a/apps/health/settings.js +++ b/apps/health/settings.js @@ -43,7 +43,6 @@ /*LANG*/"Step Goal Notification": { value: "stepGoalNotification" in settings ? settings.stepGoalNotification : false, - format: () => (settings.stepGoalNotification ? 'Yes' : 'No'), onchange: () => { settings.stepGoalNotification = !settings.stepGoalNotification; setSettings(); diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js index db583741f..2ce3e4fc9 100644 --- a/apps/lcars/lcars.settings.js +++ b/apps/lcars/lcars.settings.js @@ -76,7 +76,6 @@ var bg_code = [ }, 'Full Screen': { value: settings.fullscreen, - format: () => (settings.fullscreen ? 'Yes' : 'No'), onchange: () => { settings.fullscreen = !settings.fullscreen; save(); @@ -120,7 +119,6 @@ var bg_code = [ }, 'Disable alarm functionality': { value: settings.disableAlarms, - format: () => (settings.disableAlarms ? 'Yes' : 'No'), onchange: () => { settings.disableAlarms = !settings.disableAlarms; save(); @@ -128,7 +126,6 @@ var bg_code = [ }, 'Disable data pages functionality': { value: settings.disableData, - format: () => (settings.disableData ? 'Yes' : 'No'), onchange: () => { settings.disableData = !settings.disableData; save(); @@ -136,7 +133,6 @@ var bg_code = [ }, 'Random colors on open': { value: settings.randomColors, - format: () => (settings.randomColors ? 'Yes' : 'No'), onchange: () => { settings.randomColors = !settings.randomColors; save(); diff --git a/apps/limelight/limelight.settings.js b/apps/limelight/limelight.settings.js index aacea2f86..fa1d857d2 100644 --- a/apps/limelight/limelight.settings.js +++ b/apps/limelight/limelight.settings.js @@ -27,13 +27,12 @@ } var font_options = ["Limelight","GochiHand","Grenadier","Monoton"]; - + E.showMenu({ '': { 'title': 'Limelight Clock' }, '< Back': back, 'Full Screen': { value: s.fullscreen, - format: () => (s.fullscreen ? 'Yes' : 'No'), onchange: () => { s.fullscreen = !s.fullscreen; save(); @@ -50,7 +49,6 @@ }, 'Vector Font': { value: s.vector, - format: () => (s.vector ? 'Yes' : 'No'), onchange: () => { s.vector = !s.vector; save(); @@ -68,7 +66,6 @@ }, 'Second Hand': { value: s.secondhand, - format: () => (s.secondhand ? 'Yes' : 'No'), onchange: () => { s.secondhand = !s.secondhand; save(); diff --git a/apps/line_clock/ChangeLog b/apps/line_clock/ChangeLog new file mode 100644 index 000000000..504dc0efe --- /dev/null +++ b/apps/line_clock/ChangeLog @@ -0,0 +1 @@ +0.1: init app diff --git a/apps/line_clock/LICENSE b/apps/line_clock/LICENSE new file mode 100644 index 000000000..404cbc7a0 --- /dev/null +++ b/apps/line_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Paul Spenke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/line_clock/README.md b/apps/line_clock/README.md new file mode 100644 index 000000000..5789acbbc --- /dev/null +++ b/apps/line_clock/README.md @@ -0,0 +1,11 @@ +# Line Clock + +This app displays a simple, different looking, analog clock. It considers the +currently configured "theme" (and may therefore look different than shown in +the screenshot on your watch depending on which theme you prefer). + +![](app-screenshot.png) + +## License + +[MIT License](LICENSE) diff --git a/apps/line_clock/app-icon.js b/apps/line_clock/app-icon.js new file mode 100644 index 000000000..eaaf719b4 --- /dev/null +++ b/apps/line_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgYMJh/4AgUD+AeKgIRDj/+n41O/4RQABcfIJYAEKZgAkL4U/8ARNBwIRP/+AGx6YBPSH/4ASPh/A/hfDAAZAHg/8gP/LguSoARHEwIRFiVJkDCFjgRHgEJkg4CcwQjIAAMEHAUDCoIRB46kIHAkH//xLIw4I8eAnCNKHAYAO/xxEABg4ByASPHAkBKAbUE/5xGhP//wRFv4RDOIYIB//ACQr1FHAIRJAA0TCAP/ZwIALgYRJVowRCj/4BIkBLIgABgRHC/KqFaI4RC5MkJBlPR4UECJizJJwoAKCKImVQAwAJv0HL5S6CbwIjLCKMAn4RDh0/LMKMhWaYAKA=")) diff --git a/apps/line_clock/app-icon.png b/apps/line_clock/app-icon.png new file mode 100644 index 000000000..275353812 Binary files /dev/null and b/apps/line_clock/app-icon.png differ diff --git a/apps/line_clock/app-screenshot.png b/apps/line_clock/app-screenshot.png new file mode 100644 index 000000000..9d7413388 Binary files /dev/null and b/apps/line_clock/app-screenshot.png differ diff --git a/apps/line_clock/app.js b/apps/line_clock/app.js new file mode 100644 index 000000000..eadc46fad --- /dev/null +++ b/apps/line_clock/app.js @@ -0,0 +1,287 @@ +const handWidth = 6; +const hourRadius = 4; +const hourWidth = 8; +const hourLength = 40; +const hourSLength = 20; +const radius = 220; +const lineOffset = 115; +const hourOffset = 32; +const numberOffset = 85; +const numberSize = 22; + +const storage = require('Storage'); + +const SETTINGS_FILE = "line_clock.setting.json"; + +let initialSettings = { + showLock: true, + showMinute: true, +}; + +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || initialSettings; +for (const key in saved_settings) { + initialSettings[key] = saved_settings[key]; +} + +let gWidth = g.getWidth(), gCenterX = gWidth/2; +let gHeight = g.getHeight(), gCenterY = gHeight/2; + +let currentTime = new Date(); +let currentHour = currentTime.getHours(); +let currentMinute = currentTime.getMinutes(); + +let drawTimeout; + +function imgLock() { + return { + width : 16, height : 16, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) + }; +} + +/** + * Retrieves the angle of the hour hand for the current time. + * + * @returns {number} The angle of the hour hand in degrees. + */ +function getHourHandAngle() { + let hourHandAngle = 30 * currentHour; + hourHandAngle += 0.5 * currentMinute; + return hourHandAngle; +} + +let hourAngle = getHourHandAngle(); + +/** + * Converts degrees to radians. + * + * @param {number} degrees - The degrees to be converted to radians. + * @return {number} - The equivalent value in radians. + */ +function degreesToRadians(degrees) { + return degrees * (Math.PI / 180); +} + +/** + * Rotates an array of points around a given angle and radius. + * + * @param {Array} points - The array of points to be rotated. + * @param {number} angle - The angle in degrees to rotate the points. + * @param {number} rad - The radius to offset the rotation. + * @returns {Array} - The array of rotated points. + */ +function rotatePoints(points, angle, rad) { + const ang = degreesToRadians(angle); + const hAng = degreesToRadians(hourAngle); + const rotatedPoints = []; + points.map(function(point) { + return { + x: point.x * Math.cos(ang) - point.y * Math.sin(ang), + y: point.x * Math.sin(ang) + point.y * Math.cos(ang) + }; + }).forEach(function(point) { + rotatedPoints.push(point.x + gCenterX - (rad * Math.sin(hAng))); + rotatedPoints.push(point.y + gCenterY + (rad * Math.cos(hAng))); + }); + return rotatedPoints; +} + +/** + * Draws a hand on the canvas. + * + * @function drawHand + * + * @returns {void} + */ +function drawHand() { + g.setColor(0xF800); + const halfWidth = handWidth / 2; + + const points = [{ + x: -halfWidth, + y: -gHeight + }, { + x: halfWidth, + y: -gHeight + }, { + x: halfWidth, + y: gHeight + }, { + x: -halfWidth, + y: gHeight + }]; + + g.fillPolyAA(rotatePoints(points, hourAngle, 0)); +} + +/** + * Retrieves the hour coordinates for a given small flag. + * @param {boolean} small - Determines if the flag is small. + * @returns {Array} - An array of hour coordinates. + */ +function getHourCoordinates(small) { + const dist = small ? (hourSLength - hourLength) : 0; + const halfWidth = hourWidth / 2; + const gh = gHeight + lineOffset; + return [{ + x: -halfWidth, + y: -gh - dist + }, { + x: halfWidth, + y: -gh - dist + }, { + x: halfWidth, + y: -gh + hourLength + }, { + x: -halfWidth, + y: -gh + hourLength + }]; +} + +/** + * Assign the given time to the hour dot on the clock face. + * + * @param {number} a - The time value to assign to the hour dot. + * @return {void} + */ +function hourDot(a) { + const h = gHeight + lineOffset; + const rotatedPoints = rotatePoints( + [{ + x: 0, + y: -h + hourLength - (hourRadius / 2) + }], a, radius + ); + g.fillCircle(rotatedPoints[0], rotatedPoints[1], hourRadius); +} + +/** + * Convert an hour into a number and display it on the clock face. + * + * @param {number} a - The hour to be converted (between 0 and 360 degrees). + */ +function hourNumber(a) { + const h = gHeight + lineOffset; + const rotatedPoints = rotatePoints( + [{ + x: 0, + y: -h + hourLength + hourOffset + }], a, radius + ); + g.drawString(String(a / 30), rotatedPoints[0], rotatedPoints[1]); +} + +/** + * Draws a number on the display. + * + * @param {number} n - The number to be drawn. + * @return {void} + */ +function drawNumber(n) { + const h = gHeight + lineOffset; + const halfWidth = handWidth / 2; + const rotatedPoints = rotatePoints( + [{ + x: 0, + y: -h + hourLength + numberOffset + }], hourAngle, radius + ); + g.setColor(0xF800); + g.fillCircle(rotatedPoints[0], rotatedPoints[1], numberSize+ halfWidth); + g.setColor(g.theme.bg); + g.fillCircle(rotatedPoints[0], rotatedPoints[1], numberSize - halfWidth); + g.setColor(g.theme.fg); + g.setFont("Vector:"+numberSize); + g.drawString(String(n), rotatedPoints[0], rotatedPoints[1]); +} + +const hourPoints = getHourCoordinates(false); +const hourSPoints = getHourCoordinates(true); + +/** + * Draws an hour on a clock face. + * + * @param {number} h - The hour to be drawn on the clock face. + * @return {undefined} + */ +function drawHour(h) { + if (h === 0) { h= 12; } + if (h === 13) { h= 1; } + g.setColor(g.theme.fg); + g.setFont("Vector:32"); + const a = h * 30; + g.fillPolyAA(rotatePoints(hourPoints, a, radius)); + g.fillPolyAA(rotatePoints(hourSPoints, a + 15, radius)); + hourNumber(a); + hourDot(a + 5); + hourDot(a + 10); + hourDot(a + 20); + hourDot(a + 25); +} + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function lockListenerBw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + draw(); +} +Bangle.on('lock', lockListenerBw); + +Bangle.setUI({ + mode : "clock", + // TODO implement https://www.espruino.com/Bangle.js+Fast+Load + // remove : function() { + // Bangle.removeListener('lock', lockListenerBw); + // if (drawTimeout) clearTimeout(drawTimeout); + // drawTimeout = undefined; + // } +}); + +/** + * Draws a clock on the canvas using the current time. + * + * @return {undefined} + */ +function draw() { + queueDraw(); + currentTime = new Date(); + currentHour = currentTime.getHours(); + if (currentHour > 12) { + currentHour -= 12; + } + currentMinute = currentTime.getMinutes(); + + hourAngle = getHourHandAngle(); + + g.clear(); + g.setFontAlign(0, 0); + + g.setColor(g.theme.bg); + g.fillRect(0, 0, gWidth, gHeight); + + if(initialSettings.showLock && Bangle.isLocked()){ + g.setColor(g.theme.fg); + g.drawImage(imgLock(), gWidth-16, 2); + } + + drawHour(currentHour); + drawHour(currentHour-1); + drawHour(currentHour+1); + + + drawHand(); + + if(initialSettings.showMinute){ + drawNumber(currentMinute); + } +} + +draw(); diff --git a/apps/line_clock/metadata.json b/apps/line_clock/metadata.json new file mode 100644 index 000000000..01393efdf --- /dev/null +++ b/apps/line_clock/metadata.json @@ -0,0 +1,19 @@ +{ "id": "line_clock", + "name": "Line Clock", + "shortName":"Line Clock", + "version":"0.1", + "description": "a readable analog clock", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"app-screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"line_clock.app.js","url":"app.js"}, + {"name":"line_clock.img","url":"app-icon.js","evaluate":true}, + {"name":"line_clock.settings.js","url":"settings.js"} + ], + "data":[{"name":"line_clock.setting.json"}] +} diff --git a/apps/line_clock/settings.js b/apps/line_clock/settings.js new file mode 100644 index 000000000..5da04e959 --- /dev/null +++ b/apps/line_clock/settings.js @@ -0,0 +1,37 @@ +(function(back) { + const SETTINGS_FILE = "line_clock.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + showLock: true, + showMinute: true, + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + function save() { + storage.write(SETTINGS_FILE, settings) + } + + E.showMenu({ + '': { 'title': 'Line Clock' }, + '< Back': back, + 'Show Lock': { + value: settings.showLock, + onchange: () => { + settings.showLock = !settings.showLock; + save(); + }, + }, + 'Show Minute': { + value: settings.showMinute, + onchange: () => { + settings.showMinute = !settings.showMinute; + save(); + }, + } + }); + }) diff --git a/apps/linuxclock/settings.js b/apps/linuxclock/settings.js index 116253fda..8bcf0ae0f 100644 --- a/apps/linuxclock/settings.js +++ b/apps/linuxclock/settings.js @@ -32,7 +32,6 @@ }, 'Show Lock': { value: settings.showLock, - format: () => (settings.showLock ? 'Yes' : 'No'), onchange: () => { settings.showLock = !settings.showLock; save(); @@ -40,7 +39,6 @@ }, 'Hide Colon': { value: settings.hideColon, - format: () => (settings.hideColon ? 'Yes' : 'No'), onchange: () => { settings.hideColon = !settings.hideColon; save(); diff --git a/apps/mosaic/mosaic.settings.js b/apps/mosaic/mosaic.settings.js index dcf725b84..ee80cf950 100644 --- a/apps/mosaic/mosaic.settings.js +++ b/apps/mosaic/mosaic.settings.js @@ -25,7 +25,6 @@ '< Back': back, 'Show Widgets': { value: settings.showWidgets, - format: () => (settings.showWidgets ? 'Yes' : 'No'), onchange: () => { settings.showWidgets = !settings.showWidgets; save(); diff --git a/apps/pebble/pebble.settings.js b/apps/pebble/pebble.settings.js index f1c065db4..83032270a 100644 --- a/apps/pebble/pebble.settings.js +++ b/apps/pebble/pebble.settings.js @@ -23,7 +23,7 @@ var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; var theme_options = ['System', 'Light', 'Dark']; - + E.showMenu({ '': { 'title': 'Pebble Clock' }, /*LANG*/'< Back': back, @@ -48,7 +48,6 @@ }, /*LANG*/'Show Lock': { value: settings.showlock, - format: () => (settings.showlock ? /*LANG*/'Yes' : /*LANG*/'No'), onchange: () => { settings.showlock = !settings.showlock; save(); diff --git a/apps/slopeclockpp/settings.js b/apps/slopeclockpp/settings.js index 3c0e0a6e9..2c2d2c463 100644 --- a/apps/slopeclockpp/settings.js +++ b/apps/slopeclockpp/settings.js @@ -21,42 +21,34 @@ }, /*LANG*/'Red': { value: !!settings.colorRed, - format: () => (settings.colorRed ? 'Yes' : 'No'), onchange: x => save('colorRed', x), }, /*LANG*/'Green': { value: !!settings.colorGreen, - format: () => (settings.colorGreen ? 'Yes' : 'No'), onchange: x => save('colorGreen', x), }, /*LANG*/'Blue': { value: !!settings.colorBlue, - format: () => (settings.colorBlue ? 'Yes' : 'No'), onchange: x => save('colorBlue', x), }, /*LANG*/'Magenta': { value: !!settings.colorMagenta, - format: () => (settings.colorMagenta ? 'Yes' : 'No'), onchange: x => save('colorMagenta', x), }, /*LANG*/'Cyan': { value: !!settings.colorCyan, - format: () => (settings.colorCyan ? 'Yes' : 'No'), onchange: x => save('colorCyan', x), }, /*LANG*/'Yellow': { value: !!settings.colorYellow, - format: () => (settings.colorYellow ? 'Yes' : 'No'), onchange: x => save('colorYellow', x), }, /*LANG*/'Black': { value: !!settings.colorBlack, - format: () => (settings.colorBlack ? 'Yes' : 'No'), onchange: x => save('colorBlack', x), }, /*LANG*/'White': { value: !!settings.colorWhite, - format: () => (settings.colorWhite ? 'Yes' : 'No'), onchange: x => save('colorWhite', x), } }; diff --git a/apps/stressless/ChangeLog b/apps/stressless/ChangeLog new file mode 100644 index 000000000..55ebb3e4b --- /dev/null +++ b/apps/stressless/ChangeLog @@ -0,0 +1 @@ +0.01: New App diff --git a/apps/stressless/app-icon.js b/apps/stressless/app-icon.js new file mode 100644 index 000000000..0d9739661 --- /dev/null +++ b/apps/stressless/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AFW7AAgThDBQeNCaYaSDg4TTFygcFCaYuWDgYTTFyHB5AmRGCARK4XJF7bXR5PQF9vQ6/CF8COL6/XYDgwERxYvBYEKOM67AhLQwEDFwTAGYywvIXIIhCAgPQSILAE4RmWF5AnBMAQECA4gJDMCovDGAfBQ4PR4K+C4RiCZAr3UFwgvDJ4IAB5IrEYAgJBHwaoF6HQNRIvFEQItDXYaTEYAQ+EAogADZZIvFGAYTBAgIBBFQg0CRwQREF5wuGeQpNDQYSUDAYYyFR5guIF4jyCdQ3CMYY+DNwTtRMBSJCAwgyBNAI+HFygwEQoJ4EQwQpEHwwuVGAhPFGwIABFIY+GFywwDdoTAFe4ZgCFzjDFSAy4NFyowIF4PIF0gwHXAKYGFz4wHXBgubGAxeOFzT1KFsowQF0AwNF0QxKFsoxIEj/XGBgQDCJYrODQICHEgQDGAQoSECwhaNCoYJDAwgoHBxBfVJgpfJGIgOHL5haGPgoBDTxIBHAH4AkA==")) diff --git a/apps/stressless/app.js b/apps/stressless/app.js new file mode 100644 index 000000000..b27c79a0d --- /dev/null +++ b/apps/stressless/app.js @@ -0,0 +1,287 @@ +var option = null; + +//debugging or analysis files +//var logfile = require("Storage").open("HRV_log.csv", "w"); + +var logfile = require("Storage").open("HRV_logs.csv", "a"); + +var csv = [ + "time", + "sample count", + "HR", + "SDNN", + "RMSSD", + "Temp", + "movement" + ]; +logfile.write(csv.join(",")+"\n"); + +var debugging = true; +var samples = 0; // how many samples have we connected? +var collectData = false; // are we currently collecting data? + +var BPM_array = []; +var raw_HR_array = new Float32Array(1536); +var alternate_array = new Float32Array(3072); +var pulse_array = []; +var cutoff_threshold = 0.5; +var sample_frequency = 51.6; +var gap_threshold = 0.15; +var movement = 0; + +var px = g.getWidth()/2; +var py = g.getHeight()/2; +var accel; // interval for acceleration logging + +function storeMyData(data, file_type) { "ram" + log = raw_HR_array; + // shift elements backwards - note the 4, because a Float32 is 4 bytes + log.set(new Float32Array(log.buffer, 4 /*bytes*/)); + // add ad final element + log[log.length - 1] = data; +} + +function average(samples) { + return E.sum(samples) / samples.length; // faster builtin + /* var sum = 0; + for (var i = 0; i < samples.length; i++) { + sum += parseFloat(samples[i]); + } + var avg = sum / samples.length; + return avg;*/ +} + +function StandardDeviation (array) { + const n = array.length; + const mean = E.sum(array) / n; //array.reduce((a, b) => a + b) / n; + //return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n); + return Math.sqrt(E.variance(array, mean)); +} + +function turn_off() { + Bangle.setHRMPower(0); + + + g.clear(); + g.drawString("processing 1/5", px, py); + + rolling_average(raw_HR_array,5); + g.clear(); + g.drawString("processing 2/5", px, py); + + upscale(); + g.clear(); + g.drawString("processing 3/5", px, py); + + rolling_average(alternate_array,5); + g.clear(); + g.drawString("processing 4/5", px, py); + + apply_cutoff(); + find_peaks(); + + g.clear(); + g.drawString("processing 5/5", px, py); + + calculate_HRV(); +} + +function bernstein(A, B, C, D, E, t) { "ram" + s = 1 - t; + x = (A * Math.pow(s, 4)) + (B * 4 * Math.pow(s, 3) * t) + (C * 6 * s * s * t * t) + + (D * 4 * s * Math.pow(t, 3)) + (E * Math.pow(t, 4)); + return x; +} + +function upscale() { "ram" + var index = 0; + for (let i = raw_HR_array.length - 1; i > 5; i -= 5) { + p0 = raw_HR_array[i]; + p1 = raw_HR_array[i - 1]; + p2 = raw_HR_array[i - 2]; + p3 = raw_HR_array[i - 3]; + p4 = raw_HR_array[i - 4]; + for (let T = 0; T < 100; T += 10) { + x = T / 100; + D = bernstein(p0, p1, p2, p3, p4, x); + alternate_array[index] = D; + index++; + } + } +} + +function rolling_average(values, count) { "ram" + var temp_array = []; + + for (let i = 0; i < values.length; i++) { + temp_array = []; + for (let x = 0; x < count; x++) + temp_array.push(values[i + x]); + values[i] = average(temp_array); + } +} + +function apply_cutoff() { "ram" + var x; + for (let i = 0; i < alternate_array.length; i++) { + x = alternate_array[i]; + if (x < cutoff_threshold) + x = cutoff_threshold; + alternate_array[i] = x; + } +} + +function find_peaks() { "ram" + var previous; + var previous_slope = 0; + var slope; + var gap_size = 0; + var temp_array = []; + + for (let i = 0; i < alternate_array.length; i++) { + if (previous == null) + previous = alternate_array[i]; + slope = alternate_array[i] - previous; + if (slope * previous_slope < 0) { + if (gap_size > 30) { + pulse_array.push(gap_size); + gap_size = 0; + } + } + else { + gap_size++; + } + previous_slope = slope; + previous = alternate_array[i]; + } +} + +function RMSSD(samples){ "ram" + var sum = 0; + var square = 0; + var data = []; + var value = 0; + + for (let i = 0; i < samples.length-1; i++) { + value = Math.abs(samples[i]-samples[i+1])*((1 / (sample_frequency * 2)) * 1000); + data.push(value); + } + + for (let i = 0; i < data.length; i++) { + square = data[i] * data[i]; + Math.round(square); + sum += square; + } + + var meansquare = sum/data.length; + var RMS = Math.sqrt(meansquare); + RMS = parseInt(RMS); + return RMS; +} + +function calculate_HRV() { + var gap_average = average(pulse_array); + var temp_array = []; + var gap_max = (1 + gap_threshold) * gap_average; + var gap_min = (1 - gap_threshold) * gap_average; + for (let i = 0; i < pulse_array.length; i++) { + if (pulse_array[i] > gap_min && pulse_array[i] < gap_max) + temp_array.push(pulse_array[i]); + } + gap_average = average(temp_array); + var calculatedHR = (sample_frequency*60)/(gap_average/2); + if(option == 0) + Bangle.setLCDPower(1); + g.clear(); + //var display_stdv = StandardDeviation(pulse_array).toFixed(1); + var SDNN = (StandardDeviation(temp_array) * (1 / (sample_frequency * 2) * 1000)).toFixed(0); + var RMS_SD = RMSSD(temp_array); + g.drawString("SDNN:" + SDNN + +"\nRMSSD:" + RMS_SD + + "\nHR:" + calculatedHR.toFixed(0) + +"\nSample Count:" + temp_array.length, px, py); + Bangle.setLCDPower(1); + if(option == 0) { // single run + Bangle.buzz(500,1); + option = null; + drawButtons(); + } else { + var csv = [ + 0|getTime(), + temp_array.length, + calculatedHR.toFixed(0), + SDNN, + RMS_SD, + E.getTemperature(), + movement.toFixed(5) + ]; + logfile.write(csv.join(",")+"\n"); + + + turn_on(); + } +} + + +function btn3Pressed() { + if(option === null){ + logfile.write(""); //reset HRV log + g.clear(); + g.drawString("continuous mode", px, py); + option = 1; + + turn_on(); + } +} + +function turn_on() { + BPM_array = []; + pulse_array = []; + samples = 0; + if (accel) clearInterval(accel); + movement = 0; + accel = setInterval(function () { + movement = movement + Bangle.getAccel().diff; + }, 1000); + Bangle.setHRMPower(1); + collectData = true; +} + +function drawButtons() { + g.setColor("#00ff7f"); + g.setFont("6x8", 2); + g.setFontAlign(-1,1); + g.drawString("start recording HRV", 120, 210); + g.setColor("#ffffff"); + g.setFontAlign(0, 0); +} + +g.clear(); + +drawButtons(); + +g.setFont("6x8", 2); +g.setColor("#ffffff"); +g.setFontAlign(0, 0); // center font + +setWatch(btn3Pressed, BTN3, {repeat:true}); + + + +Bangle.on('HRM-raw', function (e) { + if (!collectData) return; + storeMyData(e.raw, 0); + if (!(samples & 7)) { + Bangle.setLCDPower(1); + g.clearRect(0, py-10, g.getWidth(), py+22); + if (samples < 100) + g.drawString("setting up...\nremain still " + samples + "%", px, py, true); + else + g.drawString("logging: " + (samples*100/raw_HR_array.length).toFixed(0) + "%", px, py, true); + } + if (samples > raw_HR_array.length) { + collectData = false; + turn_off(); + } + samples++; +}); diff --git a/apps/stressless/metadata.json b/apps/stressless/metadata.json new file mode 100644 index 000000000..8c27620d3 --- /dev/null +++ b/apps/stressless/metadata.json @@ -0,0 +1,13 @@ +{ "id": "stressless", + "name": "Stressless", + "shortName":"Stressless", + "icon": "stressless.png", + "version":"0.01", + "description": "This is a heart activity tracker for PIIS stressless project", + "tags": "health", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"stressless.app.js","url":"app.js"}, + {"name":"stressless.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/stressless/stressless.png b/apps/stressless/stressless.png new file mode 100644 index 000000000..6f79d7c83 Binary files /dev/null and b/apps/stressless/stressless.png differ diff --git a/apps/weather/settings.js b/apps/weather/settings.js index 7e2c043b9..7e0bb24c8 100644 --- a/apps/weather/settings.js +++ b/apps/weather/settings.js @@ -21,7 +21,6 @@ }, 'Hide Widget': { value: "hide" in settings ? settings.hide : false, - format: () => (settings.hide ? 'Yes' : 'No'), onchange: () => { settings.hide = !settings.hide save('hide', settings.hide); diff --git a/apps/widbgjs/settings.js b/apps/widbgjs/settings.js index e480c8cf9..c599183dc 100644 --- a/apps/widbgjs/settings.js +++ b/apps/widbgjs/settings.js @@ -43,7 +43,6 @@ }, 'Hide Widget': { value: s.hide, - format: () => (s.hide ? 'Yes' : 'No'), onchange: () => { s.hide = !s.hide; save(); diff --git a/apps/widpedom/settings.js b/apps/widpedom/settings.js index 4455ce7d7..24e8c9b35 100644 --- a/apps/widpedom/settings.js +++ b/apps/widpedom/settings.js @@ -37,7 +37,6 @@ }, 'Show Progress': { value: s.progress, - format: () => (s.progress ? 'Yes' : 'No'), onchange: () => { s.progress = !s.progress save(); @@ -45,7 +44,6 @@ }, 'Large Digits': { value: s.large, - format: () => (s.large ? 'Yes' : 'No'), onchange: () => { s.large = !s.large save(); @@ -53,7 +51,6 @@ }, 'Hide Widget': { value: s.hide, - format: () => (s.hide ? 'Yes' : 'No'), onchange: () => { s.hide = !s.hide save(); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index f1e795156..4e6662e4a 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -256,9 +256,17 @@ apps.forEach((app,appIdx) => { if (a>=0 && b>=0 && a !d.name || !d.name.endsWith(".json")))) { - WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url}); + // if settings + if (/\.settings?\.js$/.test(file.name)) { + // suggest adding to datafiles + if (!app.data || app.data.every(d => !d.name || !d.name.endsWith(".json"))) { + WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url}); + } + // check for manual boolean formatter + const m = fileContents.match(/format: *\(\) *=>.*["'](yes|on)["']/i); + if (m) { + WARN(`Settings for ${app.id} has a boolean formatter - this is handled automatically, the line can be removed`, {file:appDirRelative+file.url, line: fileContents.substr(0, m.index).split("\n").length}); + } } } for (const key in file) { diff --git a/core b/core index c97b7851f..e6a65a8cb 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit c97b7851f50cfff4e898c2264a337a17085ce463 +Subproject commit e6a65a8cb20a730f75bbbab549c602300e69e8c4