diff --git a/README.md b/README.md index 9cf30065a..38ce09f75 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ and which gives information about the app for the Launcher. "screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app "type":"...", // optional(if app) - // 'app' - an application + // 'clock' - a clock - required for clocks to automatically start // 'widget' - a widget // 'launch' - replacement launcher app // 'bootloader' - code that runs at startup only diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog new file mode 100644 index 000000000..e874c8c67 --- /dev/null +++ b/apps/clockcal/ChangeLog @@ -0,0 +1 @@ +0.01: Initial upload diff --git a/apps/clockcal/README.md b/apps/clockcal/README.md new file mode 100644 index 000000000..c19ee54a6 --- /dev/null +++ b/apps/clockcal/README.md @@ -0,0 +1,21 @@ +# Clock & Calendar by Michael + +This is my "Hello World". I first made this watchface almost 10 years ago for my original Pebble and Pebble Time and I missed this so much, that I had to write it for the BangleJS2. +I know that it seems redundant because there already **is** a *time&cal*-app, but it didn't fit my style. + +- locked screen with only one minimal update/minute +- ![locked screen](https://foostuff.github.io/BangleApps/apps/clockcal/screenshot.png) +- unlocked screen (twist?) with seconds +- ![unlocked screen](https://foostuff.github.io/BangleApps/apps/clockcal/screenshot2.png) + +## Configurable Features +- Number of calendar rows (weeks) +- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included) +- Clock Mode (24h/12h). Doesn't have an am/pm indicator. It's only there because it was easy. +- First day of the week +- Red Saturday +- Red Sunday + +## Feedback +The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings. +So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues diff --git a/apps/clockcal/app-icon.js b/apps/clockcal/app-icon.js new file mode 100644 index 000000000..5bab7853e --- /dev/null +++ b/apps/clockcal/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkECqMCkQACiEDkIXQuUnkUBkESiYXPgN/u8jgEx/8vC6E3k9xiH//8/C6BHCPQMSL6EDO4cgaf4A/ACEC+YFDl4FEAAM/+ISHbIIECh4FB+QWEA4PwCQsfC4gVBkYGDgP/mQ4CCQk/iAXEAQTiCgMiDQQSFiATDBgQXCgILBEQkQBwYrEC4sPLQRpCBwoXECgUCC4oSBAggXHNQRfDV4X/JgQXJBIIXFgYuDC5QKBiE/C4f/bwgXJmanGJgoSDiTQBmQMBE4JYBfwJ5BBYMiYQISEB4IAB+KdCAgfwAwTrCn4SDiczAAMwGwMTmR0CmECBgRSBCQwA/AGsBgEQAgYABAwcHu93s4GBqAXEmLrCiYICmICBj4XEgvABIMMqECiIXCgQXCegLYBC4NwF4VcAQNV4EPkEhF4REBgYXCiQvCu4UCAQMFJYRfKgxGBuxfGLgkjFgMCkMBmEjgEigZaBI4XFMYcRC4kBmRhBkMQgI5DF4MFgAXCLARfCFoIvDkZmBhnF4sA5gvDYghfEHIQJDAAhQBIAPwVQMTgQvCNIMhAwJfBR4MMU4JRB+RJBiUQgUDVwMgYwMBgcwX4amBqBQBiTqBgUQh8RmJhCL4IvC4HMR4ZaEAgIBBL4LBDL5EBmI5BkQvBXwIGBmMPMwMvkEFR4VcR4UgU4MSC4UQmIJBn7dBiQNBqoXBPYNQh8Q+MB+MvgEvG4JyBj8A+RkBhlQd4ZHBiBYCL4bBELxEAA==")) \ No newline at end of file diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js new file mode 100644 index 000000000..fc299912f --- /dev/null +++ b/apps/clockcal/app.js @@ -0,0 +1,119 @@ +Bangle.loadWidgets(); + +var s = Object.assign({ + CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually + MODE24: true, //24h mode vs 12h mode + FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon + REDSUN: true, // Use red color for sunday? + REDSAT: true, // Use red color for saturday? +}, require('Storage').readJSON("clockcal.json", true) || {}); + +const h = g.getHeight(); +const w = g.getWidth(); +const CELL_W = w / 7; +const CELL_H = 15; +const CAL_Y = h - s.CAL_ROWS * CELL_H; +const DEBUG = false; + +function drawMinutes() { + if (DEBUG) console.log("|-->minutes"); + var d = new Date(); + var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' '); + var minutes = d.getMinutes().toString().padStart(2, '0'); + var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00'; + var size = 50; + var clock_x = (w - 20) / 2; + if (dimSeconds) { + size = 65; + clock_x = 4 + (w / 2); + } + g.setBgColor(0); + g.setColor(textColor); + g.setFont("Vector", size); + g.setFontAlign(0, 1); + g.drawString(hours + ":" + minutes, clock_x, CAL_Y - 10, 1); + var nextminute = (61 - d.getSeconds()); + if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); + minuteInterval = setTimeout(drawMinutes, nextminute * 1000); +} + +function drawSeconds() { + if (DEBUG) console.log("|--->seconds"); + var d = new Date(); + g.setColor(); + g.fillRect(w - 31, CAL_Y - 36, w - 3, CAL_Y - 19); + g.setBgColor(0); + g.setColor('#fff'); + g.setFont("Vector", 24); + g.setFontAlign(1, 1); + g.drawString(" " + d.getSeconds().toString().padStart(2, '0'), w, CAL_Y - 13); + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000); +} + +function drawCalendar() { + if (DEBUG) console.log("CALENDAR"); + var d = new Date(); + g.reset(); + g.setBgColor(0); + g.clear(); + drawMinutes(); + if (!dimSeconds) drawSeconds(); + const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6 + const today = d.getDate(); + var rD = new Date(d.getTime()); + rD.setDate(rD.getDate() - dow); + var rDate = rD.getDate(); + g.setFontAlign(1, 1); + for (var y = 1; y <= s.CAL_ROWS; y++) { + for (var x = 1; x <= 7; x++) { + bottomrightX = x * CELL_W - 2; + bottomrightY = y * CELL_H + CAL_Y; + g.setFont("Vector", 16); + var fg = ((s.REDSUN && rD.getDay() == 0) || (s.REDSAT && rD.getDay() == 6)) ? '#f00' : '#fff'; + if (y == 1 && today == rDate) { + g.setColor('#0f0'); + g.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); + g.setColor('#000'); + g.drawString(rDate, bottomrightX, bottomrightY); + } + else { + g.setColor(fg); + g.drawString(rDate, bottomrightX, bottomrightY); + } + rD.setDate(rDate + 1); + rDate = rD.getDate(); + } + } + Bangle.drawWidgets(); + + var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1); + if (DEBUG) console.log("Next Day:" + (nextday / 3600)); + if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); + dayInterval = setTimeout(drawCalendar, nextday * 1000); +} + +function BTevent() { + drawMinutes(); + if (s.BUZZ_ON_BT) { + var interval = (NRF.getSecurityStatus().connected) ? 100 : 500; + Bangle.buzz(interval); + setTimeout(function () { Bangle.buzz(interval); }, interval * 3); + } +} + +//register events +Bangle.on('lock', locked => { + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + dimSeconds = locked; //dim seconds if lock=on + drawCalendar(); +}); +NRF.on('connect', BTevent); +NRF.on('disconnect', BTevent); + + +dimSeconds = Bangle.isLocked(); +drawCalendar(); + +Bangle.setUI("clock"); diff --git a/apps/clockcal/app.png b/apps/clockcal/app.png new file mode 100644 index 000000000..2e2e4461e Binary files /dev/null and b/apps/clockcal/app.png differ diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json new file mode 100644 index 000000000..ccc84a980 --- /dev/null +++ b/apps/clockcal/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "clockcal", + "name": "Clock & Calendar", + "version": "0.01", + "description": "Clock with Calendar", + "readme":"README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"clockcal.app.js","url":"app.js"}, + {"name":"clockcal.settings.js","url":"settings.js"}, + {"name":"clockcal.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"clockcal.json"}] +} diff --git a/apps/clockcal/screenshot.png b/apps/clockcal/screenshot.png new file mode 100644 index 000000000..fcfde0c4a Binary files /dev/null and b/apps/clockcal/screenshot.png differ diff --git a/apps/clockcal/screenshot2.png b/apps/clockcal/screenshot2.png new file mode 100644 index 000000000..98acfa9a0 Binary files /dev/null and b/apps/clockcal/screenshot2.png differ diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js new file mode 100644 index 000000000..cc2a78181 --- /dev/null +++ b/apps/clockcal/settings.js @@ -0,0 +1,92 @@ +(function (back) { + var FILE = "clockcal.json"; + + settings = Object.assign({ + CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually + MODE24: true, //24h mode vs 12h mode + FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su + REDSUN: true, // Use red color for sunday? + REDSAT: true, // Use red color for saturday? + }, require('Storage').readJSON(FILE, true) || {}); + + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + menu = { + "": { "title": "Clock & Calendar" }, + "< Back": () => back(), + 'Buzz(dis)conn.?': { + value: settings.BUZZ_ON_BT, + format: v => v ? "On" : "Off", + onchange: v => { + settings.BUZZ_ON_BT = v; + writeSettings(); + } + }, + '#Calendar Rows': { + value: settings.CAL_ROWS, + min: 0, max: 6, + onchange: v => { + settings.CAL_ROWS = v; + writeSettings(); + } + }, + 'Clock mode': { + value: settings.MODE24, + format: v => v ? "24h" : "12h", + onchange: v => { + settings.MODE24 = v; + writeSettings(); + } + }, + 'First Day': { + value: settings.FIRSTDAY, + min: 0, max: 6, + format: v => ["Sun", "Sat", "Fri", "Thu", "Wed", "Tue", "Mon"][v], + onchange: v => { + settings.FIRSTDAY = v; + writeSettings(); + } + }, + 'Red Saturday?': { + value: settings.REDSAT, + format: v => v ? "On" : "Off", + onchange: v => { + settings.REDSAT = v; + writeSettings(); + } + }, + 'Red Sunday?': { + value: settings.REDSUN, + format: v => v ? "On" : "Off", + onchange: v => { + settings.REDSUN = v; + writeSettings(); + } + }, + 'Load deafauls?': { + value: 0, + min: 0, max: 1, + format: v => ["No", "Yes"][v], + onchange: v => { + if (v == 1) { + settings = { + CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. + MODE24: true, //24h mode vs 12h mode + FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su + REDSUN: true, // Use red color for sunday? + REDSAT: true, // Use red color for saturday? + }; + writeSettings(); + load() + } + } + }, + } + // Show the menu + E.showMenu(menu); +}) diff --git a/apps/custom/custom.html b/apps/custom/custom.html index 684f813ae..307f2fd2f 100644 --- a/apps/custom/custom.html +++ b/apps/custom/custom.html @@ -16,12 +16,12 @@

Type your javascript code here

-

Then click

+

Then click  

diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 39b825e02..2dbb4febb 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -7,7 +7,7 @@ 0.06: Remove translations if not required Ensure 'on' is always supplied for translations 0.07: Improve handling of non-ASCII characters (fix #469) -0.08: Added Mavigation units and en_NAV +0.08: Added Navigation units and en_NAV 0.09: Added New Zealand en_NZ 0.10: Apply 12hour setting to time 0.11: Added translations for nl_NL and changes one formatting diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog index c868b6668..94fcffe1a 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -5,4 +5,5 @@ 0.05: Avoid immediately redrawing widgets on load 0.06: Fix: don't try to redraw widget when widgets not loaded 0.07: Option to switch theme - Changed time selection to 5-minute intervals \ No newline at end of file + Changed time selection to 5-minute intervals +0.08: Support new Bangle.js 2 menu \ No newline at end of file diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js index e05eff6a2..8cd0fa8d9 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -1,8 +1,8 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); -const modeNames = ["Off", "Alarms", "Silent"]; - +const modeNames = [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"]; +const B2 = process.env.HWVERSION===2; // load global settings let bSettings = require('Storage').readJSON('setting.json',true)||{}; let current = 0|bSettings.quiet; @@ -109,34 +109,26 @@ function setAppQuietMode(mode) { let m; function showMainMenu() { - let menu = { - "": {"title": "Quiet Mode"}, - "< Exit": () => load() - }; - // "Current Mode""Silent" won't fit on Bangle.js 2 - menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = { + let menu = {"": {"title": /*LANG*/"Quiet Mode"},}; + menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Exit"] = () => {load();}; + menu[/*LANG*/"Current Mode"] = { value: current, min:0, max:2, wrap: true, - format: () => modeNames[current], + format: v => modeNames[v], onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current` }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { - menu[formatTime(sched.hr)] = { - format: () => modeNames[sched.mode], // abuse format to right-align text - onchange: () => { - m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange) - showEditMenu(idx); - } - }; + menu[formatTime(sched.hr)] = () => { showEditMenu(idx); }; + menu[formatTime(sched.hr)].format = () => modeNames[sched.mode]+' >'; // this does nothing :-( }); - menu["Add Schedule"] = () => showEditMenu(-1); - menu["Switch Theme"] = { + menu[/*LANG*/"Add Schedule"] = () => showEditMenu(-1); + menu[/*LANG*/"Switch Theme"] = { value: !!get("switchTheme"), format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", onchange: v => v ? set("switchTheme", v) : unset("switchTheme"), }; - menu["LCD Settings"] = () => showOptionsMenu(); + menu[/*LANG*/"LCD Settings"] = () => showOptionsMenu(); m = E.showMenu(menu); } @@ -150,25 +142,23 @@ function showEditMenu(index) { mins = Math.round((s.hr-hrs)*60); mode = s.mode; } - const menu = { - "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, - "< Cancel": () => showMainMenu(), - "Hours": { - value: hrs, - min:0, max:23, wrap:true, - onchange: v => {hrs = v;}, - }, - "Minutes": { - value: mins, - min:0, max:55, step:5, wrap:true, - onchange: v => {mins = v;}, - }, - "Switch to": { - value: mode, - min:0, max:2, wrap:true, - format: v => modeNames[v], - onchange: v => {mode = v;}, - }, + let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}}; + menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Cancel"] = () => showMainMenu(); + menu[/*LANG*/"Hours"] = { + value: hrs, + min:0, max:23, wrap:true, + onchange: v => {hrs = v;}, + }; + menu[/*LANG*/"Minutes"] = { + value: mins, + min:0, max:55, step:5, wrap:true, + onchange: v => {mins = v;}, + }; + menu[/*LANG*/"Switch to"] = { + value: mode, + min:0, max:2, wrap:true, + format: v => modeNames[v], + onchange: v => {mode = v;}, }; function getSched() { return { @@ -176,7 +166,7 @@ function showEditMenu(index) { mode: mode, }; } - menu["> Save"] = function() { + menu[B2 ? /*LANG*/"Save" : /*LANG*/"> Save"] = function() { if (isNew) { scheds.push(getSched()); } else { @@ -186,7 +176,7 @@ function showEditMenu(index) { showMainMenu(); }; if (!isNew) { - menu["> Delete"] = function() { + menu[B2 ? /*LANG*/"Delete" : /*LANG*/"> Delete"] = function() { scheds.splice(index, 1); save(); showMainMenu(); @@ -196,7 +186,7 @@ function showEditMenu(index) { } function showOptionsMenu() { - const disabledFormat = v => v ? "Off" : "-"; + const disabledFormat = v => v ? /*LANG*/"Off" : "-"; function toggle(option) { // we disable wakeOn* events by setting them to `false` in options // not disabled = not present in options at all @@ -209,9 +199,9 @@ function showOptionsMenu() { } let resetTimeout; const oMenu = { - "": {"title": "LCD Settings"}, - "< Back": () => showMainMenu(), - "LCD Brightness": { + "": {"title": /*LANG*/"LCD Settings"}, + /*LANG*/"< Back": () => showMainMenu(), + /*LANG*/"LCD Brightness": { value: get("brightness", 0), min: 0, // 0 = use default max: 1, @@ -233,7 +223,7 @@ function showOptionsMenu() { } }, }, - "LCD Timeout": { + /*LANG*/"LCD Timeout": { value: get("timeout", 0), min: 0, // 0 = use default (no constant on for quiet mode) max: 60, @@ -246,17 +236,17 @@ function showOptionsMenu() { }, // we disable wakeOn* events by overwriting them as false in options // not disabled = not present in options at all - "Wake on FaceUp": { + /*LANG*/"Wake on FaceUp": { value: "wakeOnFaceUp" in options, format: disabledFormat, onchange: () => {toggle("wakeOnFaceUp");}, }, - "Wake on Touch": { + /*LANG*/"Wake on Touch": { value: "wakeOnTouch" in options, format: disabledFormat, onchange: () => {toggle("wakeOnTouch");}, }, - "Wake on Twist": { + /*LANG*/"Wake on Twist": { value: "wakeOnTwist" in options, format: disabledFormat, onchange: () => {toggle("wakeOnTwist");}, diff --git a/apps/qmsched/metadata.json b/apps/qmsched/metadata.json index daeaad624..326a8fc4f 100644 --- a/apps/qmsched/metadata.json +++ b/apps/qmsched/metadata.json @@ -2,7 +2,7 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.07", + "version": "0.08", "description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.", "icon": "app.png", "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 60477ae97..963944144 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -15,3 +15,4 @@ 0.09: Show correct number for log in overwrite prompt 0.10: Fix broken recorder settings (when launched from settings app) 0.11: Fix KML and GPX export when there is no GPS data +0.12: Fix 'Back' label positioning on track/graph display, make translateable diff --git a/apps/recorder/app.js b/apps/recorder/app.js index 7075563aa..d900c12c1 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -49,11 +49,11 @@ function showMainMenu() { }; } const mainmenu = { - '': { 'title': 'Recorder' }, + '': { 'title': /*LANG*/'Recorder' }, '< Back': ()=>{load();}, - 'RECORD': { + /*LANG*/'RECORD': { value: !!settings.recording, - format: v=>v?"On":"Off", + format: v=>v?/*LANG*/"On":/*LANG*/"Off", onchange: v => { setTimeout(function() { E.showMenu(); @@ -66,7 +66,7 @@ function showMainMenu() { }, 1); } }, - 'File #': { + /*LANG*/'File #': { value: getTrackNumber(settings.file), min: 0, max: 99, @@ -77,8 +77,8 @@ function showMainMenu() { updateSettings(); } }, - 'View Tracks': ()=>{viewTracks();}, - 'Time Period': { + /*LANG*/'View Tracks': ()=>{viewTracks();}, + /*LANG*/'Time Period': { value: settings.period||10, min: 1, max: 120, @@ -103,15 +103,15 @@ function showMainMenu() { function viewTracks() { const menu = { - '': { 'title': 'Tracks' } + '': { 'title': /*LANG*/'Tracks' } }; var found = false; require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{ found = true; - menu["Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false); + menu[/*LANG*/"Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false); }); if (!found) - menu["No Tracks found"] = function(){}; + menu[/*LANG*/"No Tracks found"] = function(){}; menu['< Back'] = () => { showMainMenu(); }; return E.showMenu(menu); } @@ -175,38 +175,38 @@ function asTime(v){ function viewTrack(filename, info) { if (!info) { - E.showMessage("Loading...","Track "+getTrackNumber(filename)); + E.showMessage(/*LANG*/"Loading...",/*LANG*/"Track "+getTrackNumber(filename)); info = getTrackInfo(filename); } //console.log(info); const menu = { - '': { 'title': 'Track '+info.fn } + '': { 'title': /*LANG*/'Track '+info.fn } }; if (info.time) menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){}; menu["Duration"] = { value : asTime(info.duration)}; menu["Records"] = { value : ""+info.records }; if (info.fields.includes("Latitude")) - menu['Plot Map'] = function() { + menu[/*LANG*/'Plot Map'] = function() { info.qOSTM = false; plotTrack(info); }; if (osm && info.fields.includes("Latitude")) - menu['Plot OpenStMap'] = function() { + menu[/*LANG*/'Plot OpenStMap'] = function() { info.qOSTM = true; plotTrack(info); } if (info.fields.includes("Altitude")) - menu['Plot Alt.'] = function() { + menu[/*LANG*/'Plot Alt.'] = function() { plotGraph(info, "Altitude"); }; if (info.fields.includes("Latitude")) - menu['Plot Speed'] = function() { + menu[/*LANG*/'Plot Speed'] = function() { plotGraph(info, "Speed"); }; // TODO: steps, heart rate? - menu['Erase'] = function() { - E.showPrompt("Delete Track?").then(function(v) { + menu[/*LANG*/'Erase'] = function() { + E.showPrompt(/*LANG*/"Delete Track?").then(function(v) { if (v) { settings.recording = false; updateSettings(); @@ -238,7 +238,7 @@ function viewTrack(filename, info) { } E.showMenu(); // remove menu - E.showMessage("Drawing...","Track "+info.fn); + E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn); g.flip(); // on buffered screens, draw a not saying we're busy g.clear(1); var s = require("Storage"); @@ -305,17 +305,18 @@ function viewTrack(filename, info) { 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); + var isBTN3 = "BTN3" in global; + g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); setWatch(function() { viewTrack(info.fn, info); - }, global.BTN3||BTN1); + }, isBTN3?BTN3:BTN1); Bangle.drawWidgets(); g.flip(); } function plotGraph(info, style) { "ram" E.showMenu(); // remove menu - E.showMessage("Calculating...","Track "+info.fn); + E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn); var filename = info.filename; var infn = new Float32Array(80); var infc = new Uint16Array(80); @@ -334,7 +335,7 @@ function viewTrack(filename, info) { strt = c[timeIdx]; } if (style=="Altitude") { - title = "Altitude (m)"; + title = /*LANG*/"Altitude (m)"; var altIdx = info.fields.indexOf("Altitude"); while(l!==undefined) { ++nl;c=l.split(",");l = f.readLine(f); @@ -344,7 +345,7 @@ function viewTrack(filename, info) { infc[i]++; } } else if (style=="Speed") { - title = "Speed (m/s)"; + title = /*LANG*/"Speed (m/s)"; var latIdx = info.fields.indexOf("Latitude"); var lonIdx = info.fields.indexOf("Longitude"); // skip until we find our first data @@ -404,10 +405,11 @@ function viewTrack(filename, info) { }); g.setFont("6x8",2); g.setFontAlign(0,0,3); - g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40); + var isBTN3 = "BTN3" in global; + g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); setWatch(function() { viewTrack(info.filename, info); - }, global.BTN3||BTN1); + }, isBTN3?BTN3:BTN1); g.flip(); } diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index 56865e885..09873dada 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.11", + "version": "0.12", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index 0d61aa789..0a697ecb9 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -6,3 +6,4 @@ 0.05: exstats updated so update 'distance' label is updated, option for 'speed' 0.06: Add option to record a run using the recorder app automatically 0.07: Fix crash if an odd number of active boxes are configured (fix #1473) +0.08: Added support for notifications from exstats. Support all stats from exstats \ No newline at end of file diff --git a/apps/run/README.md b/apps/run/README.md index 5b3bb635a..89750eb7d 100644 --- a/apps/run/README.md +++ b/apps/run/README.md @@ -13,7 +13,7 @@ the red `STOP` in the bottom right turns to a green `RUN`. the GPS updates your position as it gets more satellites your position changes and the distance shown will increase, even if you are standing still. * `TIME` - the elapsed time for your run -* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far** +* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far** * `HEART` - Your heart rate * `STEPS` - Steps since you started exercising * `CADENCE` - Steps per second based on your step rate *over the last minute* @@ -24,9 +24,8 @@ so if you have no GPS lock you just need to wait. ## Recording Tracks -`Run` doesn't directly allow you to record your tracks at the moment. -However you can just install the `Recorder` app, turn recording on in -that, and then start the `Run` app. +When the `Recorder` app is installed, `Run` will automatically start and stop tracks +as needed, prompting you to overwrite or begin a new track if necessary. ## Settings @@ -35,13 +34,29 @@ Under `Settings` -> `App` -> `Run` you can change settings for this app. * `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically record GPS/HRM/etc data every time you start a run? * `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon -* `Box 1/2/3/4/5/6` are what should be shown in each of the 6 boxes on the display. From the top left, down. - If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information - if you wish by setting the last 2 boxes to `-`. +* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display. + Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence". + Any box set to "-" will display no information. + * Box 1 is the top left (defaults to "Distance") + * Box 2 is the top right (defaults to "Time") + * Box 3 is the middle left (defaults to "Pace (avg)") + * Box 4 is the middle right (defaults to "Heart (BPM)") + * Box 5 is the bottom left (defaults to "Steps") + * Box 6 is the bottom right (defaults to "Cadence") +* `Notifications` leads to a submenu where you can configure if the app will notify you after +your distance, steps, or time repeatedly pass your configured thresholds + * `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options + * "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon" + * `Ntfy Steps`: The number of steps that must pass before you are notified. + * "Off" (default), 100, 500, 1000, 5000, 10000 + * `Ntfy Time`: The amount of time that must pass before you are notified. + * "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour" + * `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold + * `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold + * `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold ## TODO -* Allow this app to trigger the `Recorder` app on and off directly. * Keep a log of each run's stats (distance/steps/etc) ## Development diff --git a/apps/run/app.js b/apps/run/app.js index bc1d54de2..45daf878e 100644 --- a/apps/run/app.js +++ b/apps/run/app.js @@ -1,5 +1,5 @@ var ExStats = require("exstats"); -var B2 = process.env.HWVERSION==2; +var B2 = process.env.HWVERSION===2; var Layout = require("Layout"); var locale = require("locale"); var fontHeading = "6x8:2"; @@ -14,46 +14,72 @@ Bangle.drawWidgets(); // --------------------------- let settings = Object.assign({ - record : true, - B1 : "dist", - B2 : "time", - B3 : "pacea", - B4 : "bpm", - B5 : "step", - B6 : "caden", - paceLength : 1000 + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, + notify: { + dist: { + value: 0, + notifications: [], + }, + step: { + value: 0, + notifications: [], + }, + time: { + value: 0, + notifications: [], + }, + }, }, require("Storage").readJSON("run.json", 1) || {}); -var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!=""); +var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); var exs = ExStats.getStats(statIDs, settings); // --------------------------- // Called to start/stop running function onStartStop() { var running = !exs.state.active; - if (running) { - exs.start(); - } else { - exs.stop(); - } - layout.button.label = running ? "STOP" : "START"; - layout.status.label = running ? "RUN" : "STOP"; - layout.status.bgCol = running ? "#0f0" : "#f00"; - // if stopping running, don't clear state - // so we can at least refer to what we've done - layout.render(); + var prepPromises = []; + // start/stop recording + // Do this first in case recorder needs to prompt for + // an overwrite before we start tracking exstats if (settings.record && WIDGETS["recorder"]) { if (running) { isMenuDisplayed = true; - WIDGETS["recorder"].setRecording(true).then(() => { - isMenuDisplayed = false; - layout.forgetLazyState(); - layout.render(); - }); + prepPromises.push( + WIDGETS["recorder"].setRecording(true).then(() => { + isMenuDisplayed = false; + layout.forgetLazyState(); + layout.render(); + }) + ); } else { - WIDGETS["recorder"].setRecording(false); + prepPromises.push( + WIDGETS["recorder"].setRecording(false) + ); } } + + Promise.all(prepPromises) + .then(() => { + if (running) { + exs.start(); + } else { + exs.stop(); + } + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + // if stopping running, don't clear state + // so we can at least refer to what we've done + layout.render(); + }); } var lc = []; @@ -84,11 +110,27 @@ var layout = new Layout( { delete lc; layout.render(); +function configureNotification(stat) { + stat.on('notify', (e)=>{ + settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + }); +} + +Object.keys(settings.notify).forEach((statType) => { + if (settings.notify[statType].increment > 0) { + configureNotification(exs.stats[statType]); + } +}); + // Handle GPS state change for icon Bangle.on("GPS", function(fix) { layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; if (!fix.fix) return; // only process actual fixes - if (fixCount++ == 0) { + if (fixCount++ === 0) { Bangle.buzz(); // first fix, does not need to respect quiet mode } }); diff --git a/apps/run/metadata.json b/apps/run/metadata.json index 7aabf8b53..8f139c2d5 100644 --- a/apps/run/metadata.json +++ b/apps/run/metadata.json @@ -1,6 +1,6 @@ { "id": "run", "name": "Run", - "version":"0.07", + "version":"0.08", "description": "Displays distance, time, steps, cadence, pace and more for runners.", "icon": "app.png", "tags": "run,running,fitness,outdoors,gps", diff --git a/apps/run/settings.js b/apps/run/settings.js index 7eb8a8611..29a2f43cc 100644 --- a/apps/run/settings.js +++ b/apps/run/settings.js @@ -9,14 +9,28 @@ // This way saved values are preserved if a new version adds more settings const storage = require('Storage') let settings = Object.assign({ - record : true, - B1 : "dist", - B2 : "time", - B3 : "pacea", - B4 : "bpm", - B5 : "step", - B6 : "caden", - paceLength : 1000 + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale + notify: { + dist: { + increment: 0, + notifications: [], + }, + step: { + increment: 0, + notifications: [], + }, + time: { + increment: 0, + notifications: [], + }, + }, }, storage.readJSON(SETTINGS_FILE, 1) || {}); function saveSettings() { storage.write(SETTINGS_FILE, settings) @@ -24,7 +38,7 @@ function getBoxChooser(boxID) { return { - min :0, max: statsIDs.length-1, + min: 0, max: statsIDs.length-1, value: Math.max(statsIDs.indexOf(settings[boxID]),0), format: v => statsList[v].name, onchange: v => { @@ -34,6 +48,14 @@ } } + function sampleBuzz(buzzPatterns) { + return buzzPatterns.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + } + var menu = { '': { 'title': 'Run' }, '< Back': back, @@ -47,8 +69,55 @@ saveSettings(); } }; + var notificationsMenu = { + '< Back': function() { E.showMenu(menu) }, + } + menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; ExStats.appendMenuItems(menu, settings, saveSettings); - Object.assign(menu,{ + ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings); + var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; + var vibTimes = [ + [], + [[100, 1]], + [[300, 1]], + [[300, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], + ]; + notificationsMenu[/*LANG*/"Dist Pattern"] = { + value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + settings.notify.dist.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Step Pattern"] = { + value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + settings.notify.step.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Time Pattern"] = { + value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + settings.notify.time.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + var boxMenu = { + '< Back': function() { E.showMenu(menu) }, + } + Object.assign(boxMenu,{ 'Box 1': getBoxChooser("B1"), 'Box 2': getBoxChooser("B2"), 'Box 3': getBoxChooser("B3"), @@ -56,5 +125,6 @@ 'Box 5': getBoxChooser("B5"), 'Box 6': getBoxChooser("B6"), }); + menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; E.showMenu(menu); }) diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/widbaroalarm/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/widbaroalarm/README.md b/apps/widbaroalarm/README.md new file mode 100644 index 000000000..b92a51050 --- /dev/null +++ b/apps/widbaroalarm/README.md @@ -0,0 +1,26 @@ +# Barometer alarm widget + +Get a notification when the pressure reaches defined thresholds. + +![Screenshot](screenshot.png) + +## Settings + +* Interval: check interval of sensor data in minutes. 0 to disable automatic check. +* Low alarm: Toggle low alarm + * Low threshold: Warn when pressure drops below this value +* High alarm: Toggle high alarm + * High threshold: Warn when pressure exceeds above this value +* Drop alarm: Warn when pressure drops more than this value in the recent 3 hours (having at least 30 min of data) + 0 to disable this alarm. +* Raise alarm: Warn when pressure raises more than this value in the recent 3 hours (having at least 30 min of data) + 0 to disable this alarm. +* Show widget: Enable/disable widget visibility +* Buzz on alarm: Enable/disable buzzer on alarm + + +## Widget +The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours. + +## Creator +Marco ([myxor](https://github.com/myxor)) diff --git a/apps/widbaroalarm/default.json b/apps/widbaroalarm/default.json new file mode 100644 index 000000000..3d81baa81 --- /dev/null +++ b/apps/widbaroalarm/default.json @@ -0,0 +1,11 @@ +{ + "buzz": true, + "lowalarm": false, + "min": 950, + "highalarm": false, + "max": 1030, + "drop3halarm": 2, + "raise3halarm": 0, + "show": true, + "interval": 15 +} diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json new file mode 100644 index 000000000..0976df531 --- /dev/null +++ b/apps/widbaroalarm/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widbaroalarm", + "name": "Barometer alarm widget", + "shortName": "Barometer alarm", + "version": "0.01", + "description": "A widget that can alarm on when the pressure reaches defined thresholds.", + "icon": "widget.png", + "type": "widget", + "tags": "tool,barometer", + "supports": ["BANGLEJS2"], + "dependencies": {"notify":"type"}, + "readme": "README.md", + "storage": [ + {"name":"widbaroalarm.wid.js","url":"widget.js"}, + {"name":"widbaroalarm.settings.js","url":"settings.js"}, + {"name":"widbaroalarm.default.json","url":"default.json"} + ], + "data": [{"name":"widbaroalarm.json"}, {"name":"widbaroalarm.log"}] +} diff --git a/apps/widbaroalarm/settings.js b/apps/widbaroalarm/settings.js new file mode 100644 index 000000000..1455072e4 --- /dev/null +++ b/apps/widbaroalarm/settings.js @@ -0,0 +1,95 @@ +(function(back) { + const SETTINGS_FILE = "widbaroalarm.json"; + const storage = require('Storage'); + let settings = Object.assign( + storage.readJSON("widbaroalarm.default.json", true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function save(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + + function showMainMenu() { + let menu ={ + '': { 'title': 'Barometer alarm widget' }, + /*LANG*/'< Back': back, + "Interval": { + value: settings.interval, + min: 0, + max: 120, + step: 1, + format: x => { + return x != 0 ? x + ' min' : 'off'; + }, + onchange: x => save("interval", x) + }, + "Low alarm": { + value: settings.lowalarm, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save("lowalarm", x), + }, + "Low threshold": { + value: settings.min, + min: 600, + max: 1000, + step: 10, + onchange: x => save("min", x), + }, + "High alarm": { + value: settings.highalarm, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save("highalarm", x), + }, + "High threshold": { + value: settings.max, + min: 1000, + max: 1100, + step: 10, + onchange: x => save("max", x), + }, + "Drop alarm": { + value: settings.drop3halarm, + min: 0, + max: 10, + step: 1, + format: x => { + return x != 0 ? x + ' hPa/3h' : 'off'; + }, + onchange: x => save("drop3halarm", x) + }, + "Raise alarm": { + value: settings.raise3halarm, + min: 0, + max: 10, + step: 1, + format: x => { + return x != 0 ? x + ' hPa/3h' : 'off'; + }, + onchange: x => save("raise3halarm", x) + }, + "Show widget": { + value: settings.show, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save('show', x) + }, + "Buzz on alarm": { + value: settings.buzz, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save('buzz', x) + }, + }; + E.showMenu(menu); + } + + showMainMenu(); +}); diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js new file mode 100644 index 000000000..7279a963a --- /dev/null +++ b/apps/widbaroalarm/widget.js @@ -0,0 +1,185 @@ +(function() { + let medianPressure; + let threeHourAvrPressure; + let currentPressures = []; + + const LOG_FILE = "widbaroalarm.log.json"; + const SETTINGS_FILE = "widbaroalarm.json"; + const storage = require('Storage'); + let settings = Object.assign( + storage.readJSON("widbaroalarm.default.json", true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function setting(key) { + return settings[key]; + } + const interval = setting("interval"); + + let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours + + function showAlarm(body, title) { + if (body == undefined) return; + + require("notify").show({ + title: title || "Pressure", + body: body, + icon: require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA")) + }); + + if (setting("buzz") && + !(storage.readJSON('setting.json', 1) || {}).quiet) { + Bangle.buzz(); + } + } + + let alreadyWarned = false; + + function checkForAlarms(pressure) { + if (pressure == undefined || pressure <= 0) return; + + const ts = Math.round(Date.now() / 1000); // seconds + const d = { + "ts": ts, + "p": pressure + }; + + // delete entries older than 3h + for (let i = 0; i < history3.length; i++) { + if (history3[i]["ts"] < ts - (3 * 60 * 60)) { + history3.shift(); + } + } + // delete oldest entries until we have max 50 + while (history3.length > 50) { + history3.shift(); + } + + history3.push(d); + // write data to storage + storage.writeJSON(LOG_FILE, history3); + + if (setting("lowalarm") && pressure <= setting("min")) { + showAlarm("Pressure low: " + Math.round(pressure) + " hPa"); + alreadyWarned = true; + } + if (setting("highalarm") && pressure >= setting("max")) { + showAlarm("Pressure high: " + Math.round(pressure) + " hPa"); + alreadyWarned = true; + } + + if (!alreadyWarned) { + // 3h change detection + const drop3halarm = setting("drop3halarm"); + const raise3halarm = setting("raise3halarm"); + if (drop3halarm > 0 || raise3halarm > 0) { + // we need at least 30min of data for reliable detection + if (history3[0]["ts"] > ts - (30 * 60)) { + return; + } + + // Get oldest entry: + const oldestPressure = history3[0]["p"]; + if (oldestPressure != undefined && oldestPressure > 0) { + const diff = oldestPressure - pressure; + + // drop alarm + if (drop3halarm > 0 && oldestPressure > pressure) { + if (Math.abs(diff) > drop3halarm) { + showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " + + Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop"); + } + } + + // raise alarm + if (raise3halarm > 0 && oldestPressure < pressure) { + if (Math.abs(diff) > raise3halarm) { + showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " + + Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise"); + } + } + } + } + } + + // calculate 3h average for widget + let sum = 0; + for (let i = 0; i < history3.length; i++) { + sum += history3[i]["p"]; + } + threeHourAvrPressure = sum / history3.length; +} + + + +function baroHandler(data) { + if (data) { + const pressure = Math.round(data.pressure); + if (pressure == undefined || pressure <= 0) return; + currentPressures.push(pressure); + } +} + +/* + turn on barometer power + take 5 measurements + sort the results + take the middle one (median) + turn off barometer power +*/ +function check() { + Bangle.setBarometerPower(true, "widbaroalarm"); + setTimeout(function() { + currentPressures = []; + + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + + setTimeout(function() { + Bangle.setBarometerPower(false, "widbaroalarm"); + + currentPressures.sort(); + + // take median value + medianPressure = currentPressures[3]; + checkForAlarms(medianPressure); + }, 1000); + }, 500); +} + +function reload() { + check(); +} + +function draw() { + g.reset(); + if (setting("show") && medianPressure != undefined) { + g.setFont("6x8", 1).setFontAlign(1, 0); + g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6); + if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) { + g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10); + } + } +} + +if (global.WIDGETS != undefined && typeof WIDGETS === "object") { + WIDGETS["baroalarm"] = { + width: setting("show") ? 24 : 0, + reload: reload, + area: "tr", + draw: draw + }; +} + +// Let's delay the first check a bit +setTimeout(function() { +check(); +if (interval > 0) { + setInterval(check, interval * 60000); +} +}, 5000); + +})(); diff --git a/apps/widbaroalarm/widget.png b/apps/widbaroalarm/widget.png new file mode 100644 index 000000000..5be292143 Binary files /dev/null and b/apps/widbaroalarm/widget.png differ diff --git a/apps/widbaroalarm/widget24.png b/apps/widbaroalarm/widget24.png new file mode 100644 index 000000000..2f0d5e4ce Binary files /dev/null and b/apps/widbaroalarm/widget24.png differ diff --git a/modules/Layout.js b/modules/Layout.js index 4223867a4..20fa2be8b 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -1,4 +1,4 @@ -/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */ +/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */ /* Take a look at README.md for hints on developing with this library. diff --git a/modules/exstats.js b/modules/exstats.js index b72ee6584..b106622d0 100644 --- a/modules/exstats.js +++ b/modules/exstats.js @@ -1,4 +1,4 @@ -/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */ +/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */ /* Exercise Stats module Take a look at README.md for hints on developing with this library. @@ -48,6 +48,15 @@ var menu = { ... }; ExStats.appendMenuItems(menu, settings, saveSettingsFunction); E.showMenu(menu); +// Additionally, if your app makes use of the stat notifications, you can display additional menu +// settings for configuring when to notify (note the added line in the example below)W + +var menu = { ... }; +ExStats.appendMenuItems(menu, settings, saveSettingsFunction); +ExStats.appendNotifyMenuItems(menu, settings, saveSettingsFunction); +E.showMenu(menu); + + */ var state = { active : false, // are we working or not? @@ -63,15 +72,31 @@ var state = { // cadence // steps per minute adjusted if <1 minute // BPM // beats per minute // BPMage // how many seconds was BPM set? + // Notifies: 0 for disabled, otherwise how often to notify in meters, seconds, or steps + notify: { + dist: { + increment: 0, + next: 0, + }, + steps: { + increment: 0, + next: 0, + }, + time: { + increment: 0, + next: 0, + }, + }, }; // list of active stats (indexed by ID) var stats = {}; // distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km // https://www.movable-type.co.uk/scripts/latlong.html +// (Equirectangular approximation) function calcDistance(a,b) { function radians(a) { return a*Math.PI/180; } - var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2)); var y = radians(b.lat-a.lat); return Math.sqrt(x*x + y*y) * 6371000; } @@ -114,6 +139,10 @@ Bangle.on("GPS", function(fix) { if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]); if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]); if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]); + if (state.notify.dist.increment > 0 && state.notify.dist.next <= stats["dist"]) { + stats["dist"].emit("notify",stats["dist"]); + state.notify.dist.next = stats["dist"] + state.notify.dist.increment; + } }); Bangle.on("step", function(steps) { @@ -121,12 +150,16 @@ Bangle.on("step", function(steps) { if (stats["step"]) stats["step"].emit("changed",stats["step"]); state.stepHistory[0] += steps-state.lastStepCount; state.lastStepCount = steps; + if (state.notify.step.increment > 0 && state.notify.step.next <= steps) { + stats["step"].emit("notify",stats["step"]); + state.notify.step.next = steps + state.notify.step.increment; + } }); Bangle.on("HRM", function(h) { if (h.confidence>=60) { state.BPM = h.bpm; state.BPMage = 0; - stats["bpm"].emit("changed",stats["bpm"]); + if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]); } }); @@ -137,20 +170,34 @@ exports.getList = function() { {name: "Distance", id:"dist"}, {name: "Steps", id:"step"}, {name: "Heart (BPM)", id:"bpm"}, - {name: "Pace (avr)", id:"pacea"}, - {name: "Pace (current)", id:"pacec"}, + {name: "Pace (avg)", id:"pacea"}, + {name: "Pace (curr)", id:"pacec"}, {name: "Speed", id:"speed"}, {name: "Cadence", id:"caden"}, ]; }; -/** Instatiate the given list of statistic IDs (see comments at top) +/** Instantiate the given list of statistic IDs (see comments at top) options = { paceLength : meters to measure pace over + notify: { + dist: { + increment: 0 to not notify on distance milestones, otherwise the number of meters to notify after, repeating + }, + step: { + increment: 0 to not notify on step milestones, otherwise the number of steps to notify after, repeating + }, + time: { + increment: 0 to not notify on time milestones, otherwise the number of milliseconds to notify after, repeating + } + } } */ exports.getStats = function(statIDs, options) { options = options||{}; options.paceLength = options.paceLength||1000; + options.notify.dist.increment = (options.notify && options.notify.dist && options.notify.dist.increment)||0; + options.notify.step.increment = (options.notify && options.notify.step && options.notify.step.increment)||0; + options.notify.time.increment = (options.notify && options.notify.time && options.notify.time.increment)||0; var needGPS,needHRM; // ====================== if (statIDs.includes("time")) { @@ -159,7 +206,7 @@ exports.getStats = function(statIDs, options) { getValue : function() { return Date.now()-state.startTime; }, getString : function() { return formatTime(this.getValue()) }, }; - }; + } if (statIDs.includes("dist")) { needGPS = true; stats["dist"]={ @@ -221,7 +268,8 @@ exports.getStats = function(statIDs, options) { setInterval(function() { // run once a second.... if (!state.active) return; // called once a second - var duration = Date.now() - state.startTime; // in ms + var now = Date.now(); + var duration = now - state.startTime; // in ms // set cadence -> steps over last minute state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000)); if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]); @@ -235,6 +283,10 @@ exports.getStats = function(statIDs, options) { state.BPM = 0; if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]); } + if (state.notify.time.increment > 0 && state.notify.time.next <= now) { + stats["time"].emit("notify",stats["time"]); + state.notify.time.next = now + state.notify.time.increment; + } }, 1000); function reset() { state.startTime = Date.now(); @@ -247,6 +299,16 @@ exports.getStats = function(statIDs, options) { state.curSpeed = 0; state.BPM = 0; state.BPMage = 0; + state.notify = options.notify; + if (options.notify.dist.increment > 0) { + state.notify.dist.next = state.distance + options.notify.dist.increment; + } + if (options.notify.step.increment > 0) { + state.notify.step.next = state.startSteps + options.notify.step.increment; + } + if (options.notify.time.increment > 0) { + state.notify.time.next = state.startTime + options.notify.time.increment; + } } reset(); return { @@ -262,15 +324,50 @@ exports.getStats = function(statIDs, options) { }; exports.appendMenuItems = function(menu, settings, saveSettings) { - var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",]; - var paceAmts = [1000,1609,21098,42195]; + var paceNames = ["1000m", "1 mile", "1/2 Mthn", "Marathon",]; + var paceAmts = [1000, 1609, 21098, 42195]; menu['Pace'] = { - min :0, max: paceNames.length-1, - value: Math.max(paceAmts.indexOf(settings.paceLength),0), + min: 0, max: paceNames.length - 1, + value: Math.max(paceAmts.indexOf(settings.paceLength), 0), format: v => paceNames[v], onchange: v => { settings.paceLength = paceAmts[v]; saveSettings(); }, }; +} +exports.appendNotifyMenuItems = function(menu, settings, saveSettings) { + var distNames = ['Off', "1000m","1 mile","1/2 Mthn", "Marathon",]; + var distAmts = [0, 1000,1609,21098,42195]; + menu['Ntfy Dist'] = { + min: 0, max: distNames.length-1, + value: Math.max(distAmts.indexOf(settings.notify.dist.increment),0), + format: v => distNames[v], + onchange: v => { + settings.notify.dist.increment = distAmts[v]; + saveSettings(); + }, + }; + var stepNames = ['Off', '100', '500', '1000', '5000', '10000']; + var stepAmts = [0, 100, 500, 1000, 5000, 10000]; + menu['Ntfy Steps'] = { + min: 0, max: stepNames.length-1, + value: Math.max(stepAmts.indexOf(settings.notify.step.increment),0), + format: v => stepNames[v], + onchange: v => { + settings.notify.step.increment = stepAmts[v]; + saveSettings(); + }, + }; + var timeNames = ['Off', '30s', '1min', '2min', '5min', '10min', '30min', '1hr']; + var timeAmts = [0, 30000, 60000, 120000, 300000, 600000, 1800000, 3600000]; + menu['Ntfy Time'] = { + min: 0, max: timeNames.length-1, + value: Math.max(timeAmts.indexOf(settings.notify.time.increment),0), + format: v => timeNames[v], + onchange: v => { + settings.notify.time.increment = timeAmts[v]; + saveSettings(); + }, + }; };