diff --git a/.eslintignore b/.eslintignore index 38bc5b5bf..2561e1379 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,6 @@ apps/qrcode/qr-scanner.umd.min.js apps/gipy/pkg/gpconv.js apps/health/chart.min.js *.test.js + +# typescript/generated files +apps/btadv/*.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..e2fbf5609 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 + +updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "daily" diff --git a/apps/accellog/ChangeLog b/apps/accellog/ChangeLog index 94241c7a7..45469da5c 100644 --- a/apps/accellog/ChangeLog +++ b/apps/accellog/ChangeLog @@ -3,3 +3,4 @@ 0.03: Exit as first menu option, dont show decimal places for seconds 0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware 0.05: Add max G values during recording, record actual G values and magnitude to CSV +0.06: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/accellog/app.js b/apps/accellog/app.js index 147f7503f..ee82f435f 100644 --- a/apps/accellog/app.js +++ b/apps/accellog/app.js @@ -26,8 +26,7 @@ function showMenu() { viewLogs(); }, /*LANG*/"Log raw data" : { - value : logRawData, - format : v => v?/*LANG*/"Yes":/*LANG*/"No", + value : !!logRawData, onchange : v => { logRawData=v; } }, }; diff --git a/apps/accellog/metadata.json b/apps/accellog/metadata.json index 903c57903..907828c7f 100644 --- a/apps/accellog/metadata.json +++ b/apps/accellog/metadata.json @@ -2,7 +2,7 @@ "id": "accellog", "name": "Acceleration Logger", "shortName": "Accel Log", - "version": "0.05", + "version": "0.06", "description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC", "icon": "app.png", "tags": "outdoor", diff --git a/apps/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog index 3811425ac..76f0945c8 100644 --- a/apps/activityreminder/ChangeLog +++ b/apps/activityreminder/ChangeLog @@ -8,3 +8,4 @@ 0.08: Use default Bangle formatter for booleans 0.09: New app screen (instead of showing settings or the alert) and some optimisations 0.10: Add software back button via setUI +0.11: Add setting to unlock screen diff --git a/apps/activityreminder/alert.js b/apps/activityreminder/alert.js index 96a9b76c4..8b359a073 100644 --- a/apps/activityreminder/alert.js +++ b/apps/activityreminder/alert.js @@ -26,6 +26,12 @@ if (!(storage.readJSON('setting.json', 1) || {}).quiet) { Bangle.buzz(400); } + + if ((storage.readJSON('activityreminder.s.json', 1) || {}).unlock) { + Bangle.setLocked(false); + Bangle.setLCDPower(1); + } + setTimeout(load, 20000); } @@ -34,4 +40,4 @@ Bangle.drawWidgets(); run(); -})(); \ No newline at end of file +})(); diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json index a7fb0c487..a5df15a26 100644 --- a/apps/activityreminder/metadata.json +++ b/apps/activityreminder/metadata.json @@ -3,7 +3,7 @@ "name": "Activity Reminder", "shortName":"Activity Reminder", "description": "A reminder to take short walks for the ones with a sedentary lifestyle", - "version":"0.10", + "version":"0.11", "icon": "app.png", "type": "app", "tags": "tool,activity", diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js index 051c0dcd8..28082a8a0 100644 --- a/apps/activityreminder/settings.js +++ b/apps/activityreminder/settings.js @@ -75,7 +75,14 @@ settings.tempThreshold = v; activityreminder.writeSettings(settings); } - } + }, + 'Unlock on alarm': { + value: !!settings.unlock, + onchange: v => { + settings.unlock = v; + activityreminder.writeSettings(settings); + } + }, }; return mainMenu; diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index 99c44a2b1..9e7151e1e 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -12,3 +12,4 @@ 0.11: Setting to use "Today" and "Yesterday" instead of dates Added dynamic, short and range fields to clkinfo 0.12: Added color field and updating clkinfo periodically (running events) +0.13: Show day of the week in date diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js index 8afca95a9..6d2b783fd 100644 --- a/apps/agenda/agenda.js +++ b/apps/agenda/agenda.js @@ -34,8 +34,9 @@ function getDate(timestamp) { return new Date(timestamp*1000); } function formatDay(date) { + let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); if (!settings.useToday) { - return Locale.date(date); + return formattedDate; } const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd @@ -46,7 +47,7 @@ function formatDay(date) { if (dateformatted == tomorrow) { return /*LANG*/"Tomorrow "; } - return Locale.date(date); + return formattedDate; } } function formatDateLong(date, includeDay, allDay) { @@ -58,7 +59,7 @@ function formatDateLong(date, includeDay, allDay) { return shortTime; } function formatDateShort(date, allDay) { - return formatDay(date).replace(/\d\d\d\d/,"")+(allDay?"":Locale.time(date,1)+Locale.meridian(date)); + return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date)); } var lines = []; @@ -75,25 +76,29 @@ function showEvent(ev) { if (titleCnt) lines.push(""); // add blank line after title if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth()) includeDay = false; - if(includeDay || ev.allDay) { + if(includeDay && ev.allDay) { + //single day all day (average to avoid getting previous day) lines = lines.concat( - /*LANG*/"Start:", + g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10)); + } else if(includeDay || ev.allDay) { + lines = lines.concat( + /*LANG*/"Start"+":", g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), - /*LANG*/"End:", + /*LANG*/"End"+":", g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); } else { lines = lines.concat( - g.wrapString(Locale.date(start), g.getWidth()-10), + g.wrapString(formatDateShort(start,true), g.getWidth()-10), g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); } if(ev.location) - lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10)); - if(ev.description) + lines = lines.concat("",/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10)); + if(ev.description && ev.description.trim()) lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10)); if(ev.calName) - lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10)); - lines = lines.concat(["",/*LANG*/"< Back"]); + lines = lines.concat("",/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10)); + lines = lines.concat("",/*LANG*/"< Back"); E.showScroller({ h : g.getFontHeight(), // height of each menu item in pixels c : lines.length, // number of menu items @@ -120,7 +125,7 @@ function showList() { CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000); } if(CALENDAR.length == 0) { - E.showMessage("No events"); + E.showMessage(/*LANG*/"No events"); return; } E.showScroller({ diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index 88dd2c1bc..737568cb5 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.12", + "version": "0.13", "description": "Simple agenda", "icon": "agenda.png", "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog index 6d6eeb55e..43236015e 100644 --- a/apps/aiclock/ChangeLog +++ b/apps/aiclock/ChangeLog @@ -4,4 +4,5 @@ 0.04: Use widget_utils module. 0.05: Support for clkinfo. 0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. -0.07: Use clock_info.addInteractive instead of a custom implementation \ No newline at end of file +0.07: Use clock_info.addInteractive instead of a custom implementation +0.08: Use clock_info module as an app diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json index 4c01ecaa9..d8d1e9d68 100644 --- a/apps/aiclock/metadata.json +++ b/apps/aiclock/metadata.json @@ -3,9 +3,10 @@ "name": "AI Clock", "shortName":"AI Clock", "icon": "aiclock.png", - "version":"0.07", + "version":"0.08", "readme": "README.md", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", "type": "clock", "tags": "clock", diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 89fb08a6f..ab7db522c 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -38,4 +38,7 @@ 0.35: Add automatic translation of more strings 0.36: alarm widget moved out of app 0.37: add message input and dated Events -0.38: Dated event repeat option +0.38: Display date in locale + When switching 'repeat' from 'Workdays', 'Weekends' to 'Custom' preset Custom menu with previous selection + Display alarm label in delete prompt +0.39: Dated event repeat option diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 972080872..1807ce6a8 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -42,6 +42,14 @@ function handleFirstDayOfWeek(dow) { // Check the first day of week and update the dow field accordingly (alarms only!) alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); +function getLabel(e) { + const dateStr = e.date && require("locale").date(new Date(e.date), 1); + return (e.timer + ? require("time_utils").formatDuration(e.timer) + : (e.date ? `${e.date.substring(5,10)}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : "")) + ) + (e.msg ? ` ${e.msg}` : ""); +} + function showMainMenu() { const menu = { "": { "title": /*LANG*/"Alarms & Timers" }, @@ -50,12 +58,7 @@ function showMainMenu() { }; alarms.forEach((e, index) => { - var label = (e.timer - ? require("time_utils").formatDuration(e.timer) - : (e.date ? `${e.date.substring(5,10)}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : "")) - ) + (e.msg ? ` ${e.msg}` : ""); - - menu[label] = { + menu[getLabel(e)] = { value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff), onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index) }; @@ -185,7 +188,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) { if (!isNew) { menu[/*LANG*/"Delete"] = () => { - E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { + E.showPrompt(getLabel(alarm) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { if (confirm) { alarms.splice(alarmIndex, 1); saveAndReload(); @@ -272,7 +275,7 @@ function showEditRepeatMenu(repeat, day, dowChangeCallback) { }, /*LANG*/"Custom": { value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false, - onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow) + onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow) } }; } else { @@ -401,7 +404,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) { if (!keyboard) delete menu[/*LANG*/"Message"]; if (!isNew) { menu[/*LANG*/"Delete"] = () => { - E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => { + E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => { if (confirm) { alarms.splice(timerIndex, 1); saveAndReload(); diff --git a/apps/altimeter/ChangeLog b/apps/altimeter/ChangeLog index 29388520e..8d21cf797 100644 --- a/apps/altimeter/ChangeLog +++ b/apps/altimeter/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Actually upload correct code +0.03: Display sea-level pressure, too, and allow calibration diff --git a/apps/altimeter/app.js b/apps/altimeter/app.js index cac4e80fd..de664af39 100644 --- a/apps/altimeter/app.js +++ b/apps/altimeter/app.js @@ -1,4 +1,4 @@ -Bangle.setBarometerPower(true, "app"); +Bangle.setBarometerPower(true, "altimeter"); g.clear(1); Bangle.loadWidgets(); @@ -10,21 +10,62 @@ var MEDIANLENGTH = 20; var avr = [], median; var value = 0; +function getStandardPressure(altitude) { + const P0 = 1013.25; // standard pressure at sea level in hPa + const T0 = 288.15; // standard temperature at sea level in K + const g0 = 9.80665; // standard gravitational acceleration in m/s^2 + const R = 8.31432; // gas constant in J/(mol*K) + const M = 0.0289644; // molar mass of air in kg/mol + const L = -0.0065; // temperature lapse rate in K/m + + const temperature = T0 + L * altitude; // temperature at the given altitude + const pressure = P0 * Math.pow((temperature / T0), (-g0 * M) / (R * L)); // pressure at the given altitude + + return pressure; +} + +function convertToSeaLevelPressure(pressure, altitude) { + return 1013.25 * (pressure / getStandardPressure(altitude)); +} + Bangle.on('pressure', function(e) { while (avr.length>MEDIANLENGTH) avr.pop(); avr.unshift(e.altitude); median = avr.slice().sort(); - g.reset().clearRect(0,y-30,g.getWidth()-10,y+30); + g.reset().clearRect(0,y-30,g.getWidth()-10,R.h); if (median.length>10) { var mid = median.length>>1; value = E.sum(median.slice(mid-4,mid+5)) / 9; - g.setFont("Vector",50).setFontAlign(0,0).drawString((value-zero).toFixed(1), g.getWidth()/2, y); + t = value-zero; + if ((t > -100) && (t < 1000)) + t = t.toFixed(1); + else + t = t.toFixed(0); + g.setFont("Vector",50).setFontAlign(0,0).drawString(t, g.getWidth()/2, y); + sea = convertToSeaLevelPressure(e.pressure, value-zero); + t = sea.toFixed(1) + " " + e.temperature.toFixed(1); + if (0) { + print("alt raw:", value.toFixed(1)); + print("temperature:", e.temperature); + print("pressure:", e.pressure); + print("sea pressure:", sea); + print("std pressure:", getStandardPressure(value-zero)); + } + g.setFont("Vector",25).setFontAlign(-1,0).drawString(t, + 10, R.y+R.h - 35); } }); +print(g.getFonts()); g.reset(); -g.setFont("6x8").setFontAlign(0,0).drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40); +g.setFont("Vector:15"); +g.setFontAlign(0,0); +g.drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40); +g.drawString(/*LANG*/"SEA L (hPa) TEMP (C)", g.getWidth()/2, y+62); +g.flip(); g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"ZERO", g.getWidth()-5, g.getHeight()/2); -setWatch(function() { - zero = value; -}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true}); +Bangle.setUI("updown", btn=> { + if (!btn) zero=value; + if (btn<0) zero-=5; + if (btn>0) zero+=5; +}); diff --git a/apps/altimeter/metadata.json b/apps/altimeter/metadata.json index 8bdbf3022..8bf3772ed 100644 --- a/apps/altimeter/metadata.json +++ b/apps/altimeter/metadata.json @@ -1,6 +1,6 @@ { "id": "altimeter", "name": "Altimeter", - "version":"0.02", + "version":"0.03", "description": "Simple altimeter that can display height changed using Bangle.js 2's built in pressure sensor.", "icon": "app.png", "tags": "tool,outdoors", diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index db5c0b057..a651c1747 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -20,3 +20,5 @@ 0.19: Add automatic translation for a couple of strings. 0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly. 0.21: Fix broken 'Messages' button in menu +0.22: Handle connection events for GPS forwarding from phone +0.23: Handle 'act' Gadgetbridge messages for realtime activity monitoring diff --git a/apps/android/boot.js b/apps/android/boot.js index c5a9dd746..81975182d 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -3,8 +3,11 @@ Bluetooth.println(""); Bluetooth.println(JSON.stringify(message)); } - var lastMsg; + var lastMsg; // for music messages - may not be needed now... + var actInterval; // Realtime activity reporting interval when `act` is true + var actHRMHandler; // For Realtime activity reporting + // this settings var is deleted after this executes to save memory var settings = require("Storage").readJSON("android.settings.json",1)||{}; //default alarm settings if (settings.rp == undefined) settings.rp = true; @@ -60,6 +63,7 @@ title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number}); require("messages").pushMessage(event); }, + // {"t":"alarm", "d":[{h:int,m:int,rep:int},... } "alarm" : function() { //wipe existing GB alarms var sched; @@ -92,6 +96,7 @@ }, //TODO perhaps move those in a library (like messages), used also for viewing events? //add and remove events based on activity on phone (pebble-like) + // {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool "calendar" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); if (!cal || !Array.isArray(cal)) cal = []; @@ -102,6 +107,7 @@ cal[i] = event; require("Storage").writeJSON("android.calendar.json", cal); }, + // {t:"calendar-", id:int} "calendar-" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); //if any of those happen we are out of sync! @@ -110,11 +116,13 @@ require("Storage").writeJSON("android.calendar.json", cal); }, //triggered by GB, send all ids + // { t:"force_calendar_sync_start" } "force_calendar_sync_start" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); if (!cal || !Array.isArray(cal)) cal = []; gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)}); }, + // {t:"http",resp:"......",[id:"..."]} "http":function() { //get the promise and call the promise resolve if (Bangle.httpRequest === undefined) return; @@ -127,21 +135,44 @@ else request.r(event); //r = resolve function }, + // {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true } "gps": function() { const settings = require("Storage").readJSON("android.settings.json",1)||{}; if (!settings.overwriteGps) return; delete event.t; event.satellites = NaN; - event.course = NaN; + if (!isFinite(event.course)) event.course = NaN; event.fix = 1; - if (event.long!==undefined) { + if (event.long!==undefined) { // for earlier Gadgetbridge implementations event.lon = event.long; delete event.long; } Bangle.emit('GPS', event); }, + // {t:"is_gps_active"} "is_gps_active": function() { - gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 }); + gbSend({ t: "gps_power", status: Bangle.isGPSOn() }); + }, + // {t:"act", hrm:bool, stp:bool, int:int} + "act": function() { + if (actInterval) clearInterval(actInterval); + actInterval = undefined; + if (actHRMHandler) + actHRMHandler = undefined; + Bangle.setHRMPower(event.hrm,"androidact"); + if (!(event.hrm || event.stp)) return; + if (!isFinite(event.int)) event.int=1; + var lastSteps = Bangle.getStepCount(); + var lastBPM = 0; + actHRMHandler = function(e) { + lastBPM = e.bpm; + }; + Bangle.on('HRM',actHRMHandler); + actInterval = setInterval(function() { + var steps = Bangle.getStepCount(); + gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM }); + lastSteps = steps; + }, event.int*1000); } }; var h = HANDLERS[event.t]; @@ -178,21 +209,28 @@ },options.timeout||30000)}; }); return promise; - } + }; // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } + Bangle.on("charging", sendBattery); NRF.on("connect", () => setTimeout(function() { sendBattery(); GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process }, 2000)); - Bangle.on("charging", sendBattery); - if (!settings.keep) - NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect + NRF.on("disconnect", () => { + // disable HRM/activity monitoring ('act' message) + GB({t:"act",stp:0,hrm:0,int:0}); // just call the handler to save duplication + // remove all messages on disconnect (if enabled) + var settings = require("Storage").readJSON("android.settings.json",1)||{}; + if (!settings.keep) + require("messages").clearAll(); + }); setInterval(sendBattery, 10*60*1000); // Health tracking Bangle.on('health', health=>{ - gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); + if (actInterval===undefined) // if 'realtime' we do it differently + gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); }); // Music control Bangle.musicControl = cmd => { @@ -207,13 +245,39 @@ }; // GPS overwrite logic if (settings.overwriteGps) { // if the overwrite option is set../ - // Save current logic - const originalSetGpsPower = Bangle.setGPSPower; + const origSetGPSPower = Bangle.setGPSPower; + // migrate all GPS clients to the other variant on connection events + let handleConnection = (state) => { + if (Bangle.isGPSOn()){ + let orig = Bangle._PWR.GPS; + delete Bangle._PWR.GPS; + origSetGPSPower(state); + Bangle._PWR.GPS = orig; + } + }; + NRF.on('connect', ()=>{handleConnection(0);}); + NRF.on('disconnect', ()=>{handleConnection(1);}); + + // Work around Serial1 for GPS not working when connected to something + let serialTimeout; + let wrap = function(f){ + return (s)=>{ + if (serialTimeout) clearTimeout(serialTimeout); + handleConnection(1); + f(s); + serialTimeout = setTimeout(()=>{ + serialTimeout = undefined; + if (NRF.getSecurityStatus().connected) handleConnection(0); + }, 10000); + }; + }; + Serial1.println = wrap(Serial1.println); + Serial1.write = wrap(Serial1.write); + // Replace set GPS power logic to suppress activation of gps (and instead request it from the phone) Bangle.setGPSPower = (isOn, appID) => { - // if not connected, use old logic - if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID); - // Emulate old GPS power logic + // if not connected use internal GPS power function + if (!NRF.getSecurityStatus().connected) return origSetGPSPower(isOn, appID); if (!Bangle._PWR) Bangle._PWR={}; if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[]; if (!appID) appID="?"; @@ -222,11 +286,15 @@ let pwr = Bangle._PWR.GPS.length>0; gbSend({ t: "gps_power", status: pwr }); return pwr; - } - // Replace check if the GPS is on to check the _PWR variable + }; + // Allow checking for GPS via GadgetBridge Bangle.isGPSOn = () => { - return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0; - } + return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0); + }; + // stop GPS on boot if not activated + setTimeout(()=>{ + if (!Bangle.isGPSOn()) gbSend({ t: "gps_power", status: false }); + },3000); } // remove settings object so it's not taking up RAM diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 63fd7759a..f8ea3521a 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.21", + "version": "0.23", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", diff --git a/apps/android/settings.js b/apps/android/settings.js index 0abb32249..1cfc8927c 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -12,7 +12,7 @@ var mainmenu = { "" : { "title" : "Android" }, "< Back" : back, - /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, /*LANG*/"Find Phone" : () => E.showMenu({ "" : { "title" : /*LANG*/"Find Phone" }, "< Back" : ()=>E.showMenu(mainmenu), diff --git a/apps/android/test.js b/apps/android/test.js new file mode 100644 index 000000000..88a7c0566 --- /dev/null +++ b/apps/android/test.js @@ -0,0 +1,126 @@ +let result = true; + +function assertTrue(condition, text) { + if (!condition) { + result = false; + print("FAILURE: " + text); + } else print("OK: " + text); +} + +function assertFalse(condition, text) { + assertTrue(!condition, text); +} + +function assertUndefinedOrEmpty(array, text) { + assertTrue(!array || array.length == 0, text); +} + +function assertNotEmpty(array, text) { + assertTrue(array && array.length > 0, text); +} + +let internalOn = () => { + return getPinMode((process.env.HWVERSION==2)?D30:D26) == "input"; +}; + +let sec = { + connected: false +}; + +NRF.getSecurityStatus = () => sec; + +setTimeout(() => { + // add an empty starting point to make the asserts work + Bangle._PWR={}; + + print("Not connected, should use internal GPS"); + assertTrue(!NRF.getSecurityStatus().connected, "Not connected"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + + assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on"); + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertTrue(internalOn(), "Internal GPS on"); + + assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + print("Connected, should use GB GPS"); + sec.connected = true; + + assertTrue(NRF.getSecurityStatus().connected, "Connected"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on"); + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + print("Connected, then reconnect cycle"); + sec.connected = true; + + assertTrue(NRF.getSecurityStatus().connected, "Connected"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on"); + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + NRF.emit("disconnect", {}); + print("disconnect"); + sec.connected = false; + + setTimeout(() => { + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertTrue(internalOn(), "Internal GPS on"); + + print("connect"); + sec.connected = true; + NRF.emit("connect", {}); + + setTimeout(() => { + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + setTimeout(() => { + print("Test disconnect without gps on"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + print("Result Overall is " + (result ? "OK" : "FAIL")); + }, 0); + }, 0); + }, 0); +}, 5000); \ No newline at end of file diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog index ff2de6f67..92f04dc6d 100644 --- a/apps/assistedgps/ChangeLog +++ b/apps/assistedgps/ChangeLog @@ -2,3 +2,4 @@ 0.02: Update to work with Bangle.js 2 0.03: Select GNSS systems to use for Bangle.js 2 0.04: Now turns GPS off after upload +0.05: Fix regression in 0.04 that caused AGPS data not to get loaded diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index 75a4ecf32..716865983 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -158,7 +158,7 @@ var chunk = bin.substr(i,chunkSize); js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`; } - js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay + js += "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay return js; } diff --git a/apps/assistedgps/metadata.json b/apps/assistedgps/metadata.json index ac9fe5725..d2e7334c4 100644 --- a/apps/assistedgps/metadata.json +++ b/apps/assistedgps/metadata.json @@ -1,7 +1,7 @@ { "id": "assistedgps", "name": "Assisted GPS Updater (AGPS)", - "version": "0.04", + "version": "0.05", "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", "sortorder": -1, "icon": "app.png", diff --git a/apps/backswipe/ChangeLog b/apps/backswipe/ChangeLog index 5560f00bc..1e5479d6e 100644 --- a/apps/backswipe/ChangeLog +++ b/apps/backswipe/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Don't fire if the app uses swipes already. diff --git a/apps/backswipe/README.md b/apps/backswipe/README.md new file mode 100644 index 000000000..21aa357b3 --- /dev/null +++ b/apps/backswipe/README.md @@ -0,0 +1,23 @@ +Service that allows you to use an app's back button using left to right swipe gesture. + +## Settings + +Mode: Blacklist/Whitelist/Always On/Disabled +App List: Black-/whitelisted apps +Standard # of swipe handlers: 0-10 (Default: 0, must be changed for backswipe to work at all) +Standard # of drag handlers: 0-10 (Default: 0, must be changed for backswipe to work at all) + + +Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however. + +To get an indication for standard # of handlers `Bangle["#onswipe"]` and `Bangle["#ondrag"]` can be entered in the [Espruino Web IDE](https://www.espruino.com/ide) console field. They return `undefined` if no handler is active, a function if one is active, or a list of functions if multiple are active. Calling this on the clock app is a good start. + +## TODO + +- Possibly add option to tweak standard # of handlers on per app basis. + +## Creator +Kedlub + +## Contributors +thyttan diff --git a/apps/backswipe/boot.js b/apps/backswipe/boot.js index 523149e8c..e46f902eb 100644 --- a/apps/backswipe/boot.js +++ b/apps/backswipe/boot.js @@ -15,18 +15,28 @@ var currentFile = global.__FILE__ || ""; - if(global.BACK) delete global.BACK; + if (global.BACK) delete global.BACK; if (options && options.back && enabledForApp(currentFile)) { global.BACK = options.back; } setUI(mode, cb); }; - function goBack(lr, ud) { + function countHandlers(eventType) { + if (Bangle["#on"+eventType] === undefined) { + return 0; + } else if (Bangle["#on"+eventType] instanceof Array) { + return Bangle["#on"+eventType].length; + } else if (Bangle["#on"+eventType] !== undefined) { + return 1; + } + } + + function goBack(lr, _) { // if it is a left to right swipe if (lr === 1) { // if we're in an app that has a back button, run the callback for it - if (global.BACK) { + if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) { global.BACK(); } } diff --git a/apps/backswipe/metadata.json b/apps/backswipe/metadata.json index 7aa9f6247..0274ec8d7 100644 --- a/apps/backswipe/metadata.json +++ b/apps/backswipe/metadata.json @@ -1,7 +1,7 @@ { "id": "backswipe", "name": "Back Swipe", "shortName":"BackSwipe", - "version":"0.01", + "version":"0.02", "description": "Service that allows you to use an app's back button using left to right swipe gesture", "icon": "app.png", "tags": "back,gesture,swipe", diff --git a/apps/backswipe/settings.js b/apps/backswipe/settings.js index 2c29e86f8..42ca7ae7d 100644 --- a/apps/backswipe/settings.js +++ b/apps/backswipe/settings.js @@ -4,19 +4,21 @@ // Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode var DEFAULTS = { 'mode': 0, - 'apps': [] + 'apps': [], + 'standardNumSwipeHandlers': 0, + 'standardNumDragHandlers': 0 }; - + var settings = {}; - + var loadSettings = function() { settings = require('Storage').readJSON(FILE, 1) || DEFAULTS; - } - + }; + var saveSettings = function(settings) { require('Storage').write(FILE, settings); - } - + }; + // Get all app info files var getApps = function() { var apps = require('Storage').list(/\.info$/).map(appInfoFileName => { @@ -35,8 +37,8 @@ return 0; }); return apps; - } - + }; + var showMenu = function() { var menu = { '': { 'title': 'Backswipe' }, @@ -55,11 +57,31 @@ }, 'App List': () => { showAppSubMenu(); + }, + 'Standard # of swipe handlers' : { // If more than this many handlers are present backswipe will not go back + value: 0|settings.standardNumSwipeHandlers, + min: 0, + max: 10, + format: v=>v, + onchange: v => { + settings.standardNumSwipeHandlers = v; + saveSettings(settings); + }, + }, + 'Standard # of drag handlers' : { // If more than this many handlers are present backswipe will not go back + value: 0|settings.standardNumDragHandlers, + min: 0, + max: 10, + format: v=>v, + onchange: v => { + settings.standardNumDragHandlers = v; + saveSettings(settings); + }, } }; - + E.showMenu(menu); - } + }; var showAppSubMenu = function() { var menu = { @@ -101,4 +123,4 @@ loadSettings(); showMenu(); -}) \ No newline at end of file +}) diff --git a/apps/blescanner/ChangeLog b/apps/blescanner/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/blescanner/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/blescanner/app-icon.js b/apps/blescanner/app-icon.js new file mode 100644 index 000000000..a08a17ae4 --- /dev/null +++ b/apps/blescanner/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4ARkQAHBwsBiIACiAHBgQXIkAXJiIuKGAwWEC4cjmYABn//AAMyC63yC653FC6HwC5aQBC5ybIC44WChGAWxMgC44rCxGIZxYXFIoYXBGAQNCAAQXILYYXBGAUDBoK0EC5AsBC4QwEC5wAEC853BhAWDI6CPCFwp3OX4ouCC8xHXCAJ3VX94XCwBHVGIiPTU4oNCAAQWBX5gDBgQRCAAoXGGAUIFwQXHkAXHJIgABCw4IBC5sAiIAEiAgHAAQXLHBAYIC+6wJQYIADgIXGGBJ3FC4iOBAH4A/ACAA==")) diff --git a/apps/blescanner/app.js b/apps/blescanner/app.js new file mode 100644 index 000000000..7cbf80d7e --- /dev/null +++ b/apps/blescanner/app.js @@ -0,0 +1,41 @@ +E.showMessage("Scanning..."); +var devices = []; + +setInterval(function() { + NRF.findDevices(function(devs) { + devs.forEach(dev=>{ + var existing = devices.find(d=>d.id==dev.id); + if (existing) { + existing.timeout = 0; + existing.rssi = (existing.rssi*3 + dev.rssi)/4; + } else { + dev.timeout = 0; + dev.new = 0; + devices.push(dev); + } + }); + devices.forEach(d=>{d.timeout++;d.new++}); + devices = devices.filter(dev=>dev.timeout<8); + devices.sort((a,b)=>b.rssi - a.rssi); + g.clear(1).setFont("12x20"); + var wasNew = false; + devices.forEach((d,y)=>{ + y*=20; + var n = d.name; + if (!n) n=d.id.substr(0,22); + if (d.new<4) { + g.fillRect(0,y,g.getWidth(),y+19); + g.setColor(g.theme.bg); + if (d.rssi > -70) wasNew = true; + } else { + g.setColor(g.theme.fg); + } + g.setFontAlign(-1,-1); + g.drawString(n,0,y); + g.setFontAlign(1,-1); + g.drawString(0|d.rssi,g.getWidth()-1,y); + }); + g.flip(); + Bangle.setLCDBrightness(wasNew); + }, 1200); +}, 1500); diff --git a/apps/blescanner/app.png b/apps/blescanner/app.png new file mode 100644 index 000000000..8665f24ad Binary files /dev/null and b/apps/blescanner/app.png differ diff --git a/apps/blescanner/metadata.json b/apps/blescanner/metadata.json new file mode 100644 index 000000000..54cde3ede --- /dev/null +++ b/apps/blescanner/metadata.json @@ -0,0 +1,14 @@ +{ "id": "blescanner", + "name": "BLE Scanner", + "shortName":"BLE Scan", + "version":"0.01", + "description": "Scans for bluetooth devices nearby and shows their names on the screen ordered by signal strength. The most recently discovered items are highlighted.", + "icon": "app.png", + "screenshots" : [ { "url":"screenshot.png" } ], + "tags": "tool,bluetooth", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"blescanner.app.js","url":"app.js"}, + {"name":"blescanner.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/blescanner/screenshot.png b/apps/blescanner/screenshot.png new file mode 100644 index 000000000..55bd44a52 Binary files /dev/null and b/apps/blescanner/screenshot.png differ diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 780d9cc7d..82e55fa91 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -64,3 +64,5 @@ 0.55: Add toLocalISOString polyfill for pre-2v15 firmwares Only add boot info comments if settings.bootDebug was set If settings.bootDebug is set, output timing for each section of .boot0 +0.56: Settings.log = 0,1,2,3 for off,display, log, both +0.57: Handle the whitelist being disabled diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 112dfeba8..626171490 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -32,14 +32,12 @@ if (s.ble!==false) { boot += `bleServiceOptions.hid=Bangle.HID;\n`; } } -if (s.log==2) { // logging to file - boot += `_DBGLOG=require("Storage").open("log.txt","a"); -`; -} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth - if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); -LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +// settings.log 0-off, 1-display, 2-log, 3-both +if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth + if (s.log>=2) { boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}}); LoopbackA.setConsole(true);\n`; - else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal + } else if (s.log==1) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console /* If not programmable add our own handler for Bluetooth data to allow Gadgetbridge commands to be received*/ @@ -56,10 +54,10 @@ Bluetooth.on('line',function(l) { try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} });\n`; } else { - if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); -LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); + if (s.log>=2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}}); if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`; - else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) + else if (s.log==1) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth } // we just reset, so BLE should be on. @@ -81,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; -if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; +if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation // ================================================== FIXING OLDER FIRMWARES if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted. diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 455563a16..c652f6136 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.55", + "version": "0.57", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/btadv/ChangeLog b/apps/btadv/ChangeLog new file mode 100644 index 000000000..1a3bc1757 --- /dev/null +++ b/apps/btadv/ChangeLog @@ -0,0 +1 @@ +0.01: New app! diff --git a/apps/btadv/README.md b/apps/btadv/README.md new file mode 100644 index 000000000..7b1afcefe --- /dev/null +++ b/apps/btadv/README.md @@ -0,0 +1,16 @@ +# Bluetooth Advert + +This app advertises and exports (over Bluetooth) live data from the bangle's sensors: + +- Heart Rate +- Accelerometer readings +- Pressure +- GPS information +- Magnetic flux + +Swipe in any direction to access settings, and tap a setting to toggle it. +Hit back to return to the details screen, which shows sensor data being exported. + +# TypeScript + +This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info diff --git a/apps/btadv/app.js b/apps/btadv/app.js new file mode 100644 index 000000000..ced701d79 --- /dev/null +++ b/apps/btadv/app.js @@ -0,0 +1,412 @@ +"use strict"; +var __assign = Object.assign; +var Layout = require("Layout"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +var HRM_MIN_CONFIDENCE = 75; +var services = ["0x180d", "0x181a", "0x1819"]; +var acc; +var bar; +var gps; +var hrm; +var hrmAny; +var mag; +var btnsShown = false; +var prevBtnsShown = undefined; +var hrmAnyClear; +var settings = { + bar: false, + gps: false, + hrm: false, + mag: false, +}; +var idToName = { + acc: "Acceleration", + bar: "Barometer", + gps: "GPS", + hrm: "HRM", + mag: "Magnetometer", +}; +var infoFont = "6x8:2"; +var colour = { + on: "#0f0", + off: "#fff", +}; +var makeToggle = function (id) { return function () { + settings[id] = !settings[id]; + var entry = btnLayout[id]; + var col = settings[id] ? colour.on : colour.off; + entry.btnBorder = entry.col = col; + btnLayout.update(); + btnLayout.render(); + enableSensors(); +}; }; +var btnStyle = { + font: "Vector:14", + fillx: 1, + filly: 1, + col: g.theme.fg, + bgCol: g.theme.bg, + btnBorder: "#fff", +}; +var btnLayout = new Layout({ + type: "v", + c: [ + { + type: "h", + c: [ + __assign({ type: "btn", label: idToName.bar, id: "bar", cb: makeToggle('bar') }, btnStyle), + __assign({ type: "btn", label: idToName.gps, id: "gps", cb: makeToggle('gps') }, btnStyle), + ] + }, + { + type: "h", + c: [ + __assign({ type: "btn", label: idToName.hrm, id: "hrm", cb: makeToggle('hrm') }, btnStyle), + __assign({ type: "btn", label: idToName.mag, id: "mag", cb: makeToggle('mag') }, btnStyle), + ] + }, + { + type: "h", + c: [ + __assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour.on, btnBorder: colour.on }), + __assign({ type: "btn", label: "Back", cb: function () { + setBtnsShown(false); + } }, btnStyle), + ] + } + ] +}, { + lazy: true, + back: function () { + setBtnsShown(false); + }, +}); +var setBtnsShown = function (b) { + btnsShown = b; + hook(!btnsShown); + setIntervals(); + redraw(); +}; +var drawInfo = function (force) { + var _a = Bangle.appRect, y = _a.y, x = _a.x, w = _a.w; + var mid = x + w / 2; + var drawn = false; + if (!force && !bar && !gps && !hrm && !mag) + return; + g.reset() + .clearRect(Bangle.appRect) + .setFont(infoFont) + .setFontAlign(0, -1); + if (bar) { + g.drawString("".concat(bar.altitude.toFixed(1), "m"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(bar.pressure.toFixed(1), " hPa"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(bar.temperature.toFixed(1), "C"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (gps) { + g.drawString("".concat(gps.lat.toFixed(4), " lat, ").concat(gps.lon.toFixed(4), " lon"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(gps.alt, "m (").concat(gps.satellites, " sat)"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (hrm) { + g.drawString("".concat(hrm.bpm, " BPM (").concat(hrm.confidence, "%)"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + else if (hrmAny) { + g.drawString("~".concat(hrmAny.bpm, " BPM (").concat(hrmAny.confidence, "%)"), mid, y); + y += g.getFontHeight(); + drawn = true; + if (!settings.hrm && !hrmAnyClear) { + hrmAnyClear = setTimeout(function () { + hrmAny = undefined; + hrmAnyClear = undefined; + }, 10000); + } + } + if (mag) { + g.drawString("".concat(mag.x, " ").concat(mag.y, " ").concat(mag.z), mid, y); + y += g.getFontHeight(); + g.drawString("heading: ".concat(mag.heading.toFixed(1)), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (!drawn) { + if (!force || Object.values(settings).every(function (x) { return !x; })) { + g.drawString("swipe to enable", mid, y); + } + else { + g.drawString("events pending", mid, y); + } + y += g.getFontHeight(); + } +}; +var onTap = function () { + setBtnsShown(true); +}; +var redraw = function () { + if (btnsShown) { + if (!prevBtnsShown) { + prevBtnsShown = btnsShown; + Bangle.removeListener("swipe", onTap); + btnLayout.setUI(); + btnLayout.forgetLazyState(); + g.clearRect(Bangle.appRect); + } + btnLayout.render(); + } + else { + if (prevBtnsShown) { + prevBtnsShown = btnsShown; + Bangle.setUI(); + Bangle.on("swipe", onTap); + drawInfo(true); + } + else { + drawInfo(); + } + } +}; +var encodeHrm = function (hrm) { + return [0, hrm.bpm]; +}; +encodeHrm.maxLen = 2; +var encodePressure = function (data) { + return toByteArray(Math.round(data.pressure * 10), 4, false); +}; +encodePressure.maxLen = 4; +var encodeElevation = function (data) { + return toByteArray(Math.round(data.altitude * 100), 3, true); +}; +encodeElevation.maxLen = 3; +var encodeTemp = function (data) { + return toByteArray(Math.round(data.temperature * 10), 2, true); +}; +encodeTemp.maxLen = 2; +var encodeGps = function (data) { + var speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false); + var lat = toByteArray(Math.round(data.lat * 10000000), 4, true); + var lon = toByteArray(Math.round(data.lon * 10000000), 4, true); + var elevation = toByteArray(Math.round(data.alt * 100), 3, true); + var heading = toByteArray(Math.round(data.course * 100), 2, false); + return [ + 157, + 2, + speed[0], speed[1], + lat[0], lat[1], lat[2], lat[3], + lon[0], lon[1], lon[2], lon[3], + elevation[0], elevation[1], elevation[2], + heading[0], heading[1] + ]; +}; +encodeGps.maxLen = 17; +var encodeGpsHeadingOnly = function (data) { + var heading = toByteArray(Math.round(data.heading * 100), 2, false); + return [ + 16, + 16, + heading[0], heading[1] + ]; +}; +encodeGpsHeadingOnly.maxLen = 17; +var encodeMag = function (data) { + var x = toByteArray(data.x, 2, true); + var y = toByteArray(data.y, 2, true); + var z = toByteArray(data.z, 2, true); + return [x[0], x[1], y[0], y[1], z[0], z[1]]; +}; +encodeMag.maxLen = 6; +var toByteArray = function (value, numberOfBytes, isSigned) { + var byteArray = new Array(numberOfBytes); + if (isSigned && (value < 0)) { + value += 1 << (numberOfBytes * 8); + } + for (var index = 0; index < numberOfBytes; index++) { + byteArray[index] = (value >> (index * 8)) & 0xff; + } + return byteArray; +}; +var enableSensors = function () { + Bangle.setBarometerPower(settings.bar, "btadv"); + if (!settings.bar) + bar = undefined; + Bangle.setGPSPower(settings.gps, "btadv"); + if (!settings.gps) + gps = undefined; + Bangle.setHRMPower(settings.hrm, "btadv"); + if (!settings.hrm) + hrm = hrmAny = undefined; + Bangle.setCompassPower(settings.mag, "btadv"); + if (!settings.mag) + mag = undefined; +}; +var haveServiceData = function (serv) { + switch (serv) { + case "0x180d": return !!hrm; + case "0x181a": return !!(bar || mag); + case "0x1819": return !!(gps && gps.lat && gps.lon || mag); + } +}; +var serviceToAdvert = function (serv, initial) { + var _a, _b, _c; + if (initial === void 0) { initial = false; } + switch (serv) { + case "0x180d": + if (hrm || initial) { + var o = { + maxLen: encodeHrm.maxLen, + readable: true, + notify: true, + }; + if (hrm) { + o.value = encodeHrm(hrm); + hrm = undefined; + } + return _a = {}, _a["0x2a37"] = o, _a; + } + return {}; + case "0x1819": + if (gps || initial) { + var o = { + maxLen: encodeGps.maxLen, + readable: true, + notify: true, + }; + if (gps) { + o.value = encodeGps(gps); + gps = undefined; + } + return _b = {}, _b["0x2a67"] = o, _b; + } + else if (mag) { + var o = { + maxLen: encodeGpsHeadingOnly.maxLen, + readable: true, + notify: true, + value: encodeGpsHeadingOnly(mag), + }; + return _c = {}, _c["0x2a67"] = o, _c; + } + return {}; + case "0x181a": { + var o = {}; + if (bar || initial) { + o["0x2a6c"] = { + maxLen: encodeElevation.maxLen, + readable: true, + notify: true, + }; + o["0x2A1F"] = { + maxLen: encodeTemp.maxLen, + readable: true, + notify: true, + }; + o["0x2a6d"] = { + maxLen: encodePressure.maxLen, + readable: true, + notify: true, + }; + if (bar) { + o["0x2a6c"].value = encodeElevation(bar); + o["0x2A1F"].value = encodeTemp(bar); + o["0x2a6d"].value = encodePressure(bar); + bar = undefined; + } + } + if (mag || initial) { + o["0x2aa1"] = { + maxLen: encodeMag.maxLen, + readable: true, + notify: true, + }; + if (mag) { + o["0x2aa1"].value = encodeMag(mag); + } + } + return o; + } + } +}; +var getBleAdvert = function (map, all) { + if (all === void 0) { all = false; } + var advert = {}; + for (var _i = 0, services_1 = services; _i < services_1.length; _i++) { + var serv = services_1[_i]; + if (all || haveServiceData(serv)) { + advert[serv] = map(serv); + } + } + mag = undefined; + return advert; +}; +var updateServices = function () { + var newAdvert = getBleAdvert(serviceToAdvert); + NRF.updateServices(newAdvert); +}; +var onAccel = function (newAcc) { return acc = newAcc; }; +var onPressure = function (newBar) { return bar = newBar; }; +var onGPS = function (newGps) { return gps = newGps; }; +var onHRM = function (newHrm) { + if (newHrm.confidence >= HRM_MIN_CONFIDENCE) + hrm = newHrm; + hrmAny = newHrm; +}; +var onMag = function (newMag) { return mag = newMag; }; +var hook = function (enable) { + if (enable) { + Bangle.on("accel", onAccel); + Bangle.on("pressure", onPressure); + Bangle.on("GPS", onGPS); + Bangle.on("HRM", onHRM); + Bangle.on("mag", onMag); + } + else { + Bangle.removeListener("accel", onAccel); + Bangle.removeListener("pressure", onPressure); + Bangle.removeListener("GPS", onGPS); + Bangle.removeListener("HRM", onHRM); + Bangle.removeListener("mag", onMag); + } +}; +var setIntervals = function (locked, connected) { + if (locked === void 0) { locked = Bangle.isLocked(); } + if (connected === void 0) { connected = NRF.getSecurityStatus().connected; } + changeInterval(redrawInterval, locked ? 15000 : 5000); + if (connected) { + var interval = btnsShown ? 5000 : 1000; + if (bleInterval) { + changeInterval(bleInterval, interval); + } + else { + bleInterval = setInterval(updateServices, interval); + } + } + else if (bleInterval) { + clearInterval(bleInterval); + bleInterval = undefined; + } +}; +var redrawInterval = setInterval(redraw, 1000); +Bangle.on("lock", function (locked) { return setIntervals(locked); }); +var bleInterval; +NRF.on("connect", function () { return setIntervals(undefined, true); }); +NRF.on("disconnect", function () { return setIntervals(undefined, false); }); +setIntervals(); +setBtnsShown(true); +enableSensors(); +{ + var ad = getBleAdvert(function (serv) { return serviceToAdvert(serv, true); }, true); + var adServices = Object + .keys(ad) + .map(function (k) { return k.replace("0x", ""); }); + NRF.setServices(ad, { + advertise: adServices, + uart: false, + }); +} diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts new file mode 100644 index 000000000..15128e484 --- /dev/null +++ b/apps/btadv/app.ts @@ -0,0 +1,715 @@ +// ts helpers: +const __assign = Object.assign; + +const Layout = require("Layout") as Layout_.Layout; + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const enum Intervals { + // BLE_ADVERT = 60 * 1000, + BLE = 1000, // info screen + BLE_BACKGROUND = 5000, // button screen + UI_INFO = 5 * 1000, // info refresh, wake + UI_INFO_SLEEP = 15 * 1000, // info refresh, asleep +} + +type Hrm = { bpm: number, confidence: number }; + +const HRM_MIN_CONFIDENCE = 75; + +// https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/ +const enum BleServ { + // org.bluetooth.service.heart_rate + // contains: HRM + HRM = "0x180d", + + // org.bluetooth.service.environmental_sensing + // contains: Elevation, Temp(Celsius), Pressure, Mag + EnvSensing = "0x181a", + + // org.bluetooth.service.location_and_navigation + // contains: LocationAndSpeed + LocationAndNavigation = "0x1819", + + // Acc // none known for this +} + +const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation]; + +const enum BleChar { + // org.bluetooth.characteristic.heart_rate_measurement + // + HRM = "0x2a37", + + // org.bluetooth.characteristic.elevation + // s24, meters 0.01 + Elevation = "0x2a6c", + + // org.bluetooth.characteristic.temperature + // s16 *10^2 + Temp = "0x2a6e", + // org.bluetooth.characteristic.temperature_celsius + // s16 *10^2 + TempCelsius = "0x2A1F", + + // org.bluetooth.characteristic.pressure + // u32 *10 + Pressure = "0x2a6d", + + // org.bluetooth.characteristic.location_and_speed + // + LocationAndSpeed = "0x2a67", + + // org.bluetooth.characteristic.magnetic_flux_density_3d + // s16: x, y, z, tesla (10^-7) + MagneticFlux3D = "0x2aa1", +} + +type BleCharAdvert = { + value?: Array, + readable?: true, + notify?: true, + indicate?: true, // notify + ACK + maxLen?: number, +}; + +type BleServAdvert = { + [key in BleChar]?: BleCharAdvert; +}; + +type LenFunc = { + (_: T): Array, + maxLen: number, +} + +let acc: undefined | AccelData; +let bar: undefined | PressureData; +let gps: undefined | GPSFix; +let hrm: undefined | Hrm; +let hrmAny: undefined | Hrm; +let mag: undefined | CompassData; +let btnsShown = false; +let prevBtnsShown: boolean | undefined = undefined; +let hrmAnyClear: undefined | number; + +type BtAdvType = "bar" | "gps" | "hrm" | "mag" | (IncludeAcc extends true ? "acc" : never); +type BtAdvMap = { [key in BtAdvType]: T }; + +const settings: BtAdvMap = { + bar: false, + gps: false, + hrm: false, + mag: false, +}; + +const idToName: BtAdvMap = { + acc: "Acceleration", + bar: "Barometer", + gps: "GPS", + hrm: "HRM", + mag: "Magnetometer", +}; + +// 15 characters per line +const infoFont: FontNameWithScaleFactor = "6x8:2"; + +const colour = { + on: "#0f0", + off: "#fff", +} as const; + +const makeToggle = (id: BtAdvType) => () => { + settings[id] = !settings[id]; + + const entry = btnLayout[id]!; + const col = settings[id] ? colour.on : colour.off; + + entry.btnBorder = entry.col = col; + + btnLayout.update(); + btnLayout.render(); + + //require('Storage').writeJSON(SETTINGS_FILENAME, settings); + enableSensors(); +}; + +const btnStyle: { + font: FontNameWithScaleFactor, + fillx?: 1, + filly?: 1, + col: ColorResolvable, + bgCol: ColorResolvable, + btnBorder: ColorResolvable, +} = { + font: "Vector:14", + fillx: 1, + filly: 1, + col: g.theme.fg, + bgCol: g.theme.bg, + btnBorder: "#fff", +}; + +const btnLayout = new Layout( + { + type: "v", + c: [ + { + type: "h", + c: [ + { + type: "btn", + label: idToName.bar, + id: "bar", + cb: makeToggle('bar'), + ...btnStyle, + }, + { + type: "btn", + label: idToName.gps, + id: "gps", + cb: makeToggle('gps'), + ...btnStyle, + }, + ] + }, + { + type: "h", + c: [ + // hrm, mag + { + type: "btn", + label: idToName.hrm, + id: "hrm", + cb: makeToggle('hrm'), + ...btnStyle, + }, + { + type: "btn", + label: idToName.mag, + id: "mag", + cb: makeToggle('mag'), + ...btnStyle, + }, + ] + }, + { + type: "h", + c: [ + { + type: "btn", + label: idToName.acc, + id: "acc", + cb: () => {}, + ...btnStyle, + col: colour.on, + btnBorder: colour.on, + }, + { + type: "btn", + label: "Back", + cb: () => { + setBtnsShown(false); + }, + ...btnStyle, + }, + ] + } + ] + }, + { + lazy: true, + back: () => { + setBtnsShown(false); + }, + }, +); + +const setBtnsShown = (b: boolean) => { + btnsShown = b; + + hook(!btnsShown); + setIntervals(); + + redraw(); +}; + +const drawInfo = (force?: true) => { + let { y, x, w } = Bangle.appRect; + const mid = x + w / 2 + let drawn = false; + + if (!force && !bar && !gps && !hrm && !mag) + return; + + g.reset() + .clearRect(Bangle.appRect) + .setFont(infoFont) + .setFontAlign(0, -1); + + if (bar) { + g.drawString(`${bar.altitude.toFixed(1)}m`, mid, y); + y += g.getFontHeight(); + + g.drawString(`${bar.pressure.toFixed(1)} hPa`, mid, y); + y += g.getFontHeight(); + + g.drawString(`${bar.temperature.toFixed(1)}C`, mid, y); + y += g.getFontHeight(); + + drawn = true; + } + + if (gps) { + g.drawString( + `${gps.lat.toFixed(4)} lat, ${gps.lon.toFixed(4)} lon`, + mid, + y, + ); + y += g.getFontHeight(); + + g.drawString( + `${gps.alt}m (${gps.satellites} sat)`, + mid, + y, + ); + y += g.getFontHeight(); + + drawn = true; + } + + if (hrm) { + g.drawString(`${hrm.bpm} BPM (${hrm.confidence}%)`, mid, y); + y += g.getFontHeight(); + + drawn = true; + } else if (hrmAny) { + g.drawString(`~${hrmAny.bpm} BPM (${hrmAny.confidence}%)`, mid, y); + y += g.getFontHeight(); + + drawn = true; + + if (!settings.hrm && !hrmAnyClear) { + // hrm is erased, but hrmAny will remain until cleared (or reset) + // if it runs via health check, we reset it here + hrmAnyClear = setTimeout(() => { + hrmAny = undefined; + hrmAnyClear = undefined; + }, 10000); + } + } + + if (mag) { + g.drawString( + `${mag.x} ${mag.y} ${mag.z}`, + mid, + y + ); + y += g.getFontHeight(); + + g.drawString( + `heading: ${mag.heading.toFixed(1)}`, + mid, + y + ); + y += g.getFontHeight(); + + drawn = true; + } + + if (!drawn) { + if (!force || Object.values(settings).every((x: boolean) => !x)) { + g.drawString(`swipe to enable`, mid, y); + } else { + g.drawString(`events pending`, mid, y); + } + y += g.getFontHeight(); + } +}; + +const onTap = (/* _: { ... } */) => { + setBtnsShown(true); +}; + +const redraw = () => { + if (btnsShown) { + if (!prevBtnsShown) { + prevBtnsShown = btnsShown; + + Bangle.removeListener("swipe", onTap); + + btnLayout.setUI(); + btnLayout.forgetLazyState(); + g.clearRect(Bangle.appRect); // in case btnLayout isn't full screen + } + + btnLayout.render(); + } else { + if (prevBtnsShown) { + prevBtnsShown = btnsShown; + + Bangle.setUI(); // remove all existing input handlers + Bangle.on("swipe", onTap); + + drawInfo(true); + } else { + drawInfo(); + } + } +}; + +const encodeHrm: LenFunc = (hrm: Hrm) => + // { + // flags: u8, + // bytes: [u8...] + // } + // flags { + // 1 << 0: 16bit bpm + // 1 << 1: sensor contact available + // 1 << 2: sensor contact boolean + // 1 << 3: energy expended, next 16 bits + // 1 << 4: "rr" data available, u16s, intervals + // } + [0, hrm.bpm]; +encodeHrm.maxLen = 2; + +const encodePressure: LenFunc = (data: PressureData) => + toByteArray(Math.round(data.pressure * 10), 4, false); +encodePressure.maxLen = 4; + +const encodeElevation: LenFunc = (data: PressureData) => + toByteArray(Math.round(data.altitude * 100), 3, true); +encodeElevation.maxLen = 3; + +const encodeTemp: LenFunc = (data: PressureData) => + toByteArray(Math.round(data.temperature * 10), 2, true); +encodeTemp.maxLen = 2; + +const encodeGps: LenFunc = (data: GPSFix) => { + // flags: 16 bits + // bit 0: Instantaneous Speed Present + // bit 1: Total Distance Present + // bit 2: Location Present + // bit 3: Elevation Present + // bit 4: Heading Present + // bit 5: Rolling Time Present + // bit 6: UTC Time Present + // + // bit 7-8: position status + // 0 (0b00): no position + // 1 (0b01): position ok + // 2 (0b10): estimated position + // 3 (0b11): last known position + // + // bit 9: speed & distance format + // 0: 2d + // 1: 3d + // + // bit 10-11: elevation source + // 0: Positioning System + // 1: Barometric Air Pressure + // 2: Database Service (or similiar) + // 3: Other + // + // bit 12: Heading Source + // 0: Heading based on movement + // 1: Heading based on magnetic compass + // + // speed: u16 (m/s), 1/100 + // distance: u24, 1/10 + // lat: s32, 1/10^7 + // lon: s32, 1/10^7 + // elevation: s24, 1/100 + // heading: u16 (deg), 1/100 + // rolling time: u8 (s) + // utc time: org.bluetooth.characteristic.date_time + + const speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false); + const lat = toByteArray(Math.round(data.lat * 10000000), 4, true); + const lon = toByteArray(Math.round(data.lon * 10000000), 4, true); + const elevation = toByteArray(Math.round(data.alt * 100), 3, true); + const heading = toByteArray(Math.round(data.course * 100), 2, false); + + return [ + 0b10011101, // speed, location, elevation, heading [...] + 0b00000010, // position ok, 3d speed/distance + speed[0]!, speed[1]!, + lat[0]!, lat[1]!, lat[2]!, lat[3]!, + lon[0]!, lon[1]!, lon[2]!, lon[3]!, + elevation[0]!, elevation[1]!, elevation[2]!, + heading[0]!, heading[1]! + ]; +}; +encodeGps.maxLen = 17; + +const encodeGpsHeadingOnly: LenFunc = (data: CompassData) => { + // see encodeGps() + const heading = toByteArray(Math.round(data.heading * 100), 2, false); + + return [ + 0b00010000, // heading present + 0b00010000, // heading source: mag + heading[0]!, heading[1]! + ]; +}; +encodeGpsHeadingOnly.maxLen = 17; + +const encodeMag: LenFunc = (data: CompassData) => { + const x = toByteArray(data.x, 2, true); + const y = toByteArray(data.y, 2, true); + const z = toByteArray(data.z, 2, true); + + return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ]; +}; +encodeMag.maxLen = 6; + +const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => { + const byteArray: Array = new Array(numberOfBytes); + + if(isSigned && (value < 0)) { + value += 1 << (numberOfBytes * 8); + } + + for(let index = 0; index < numberOfBytes; index++) { + byteArray[index] = (value >> (index * 8)) & 0xff; + } + + return byteArray; +}; + +const enableSensors = () => { + Bangle.setBarometerPower(settings.bar, "btadv"); + if (!settings.bar) + bar = undefined; + + Bangle.setGPSPower(settings.gps, "btadv"); + if (!settings.gps) + gps = undefined; + + Bangle.setHRMPower(settings.hrm, "btadv"); + if (!settings.hrm) + hrm = hrmAny = undefined; + + Bangle.setCompassPower(settings.mag, "btadv"); + if (!settings.mag) + mag = undefined; +}; + +// ---------------------------- + +const haveServiceData = (serv: BleServ): boolean => { + switch (serv) { + case BleServ.HRM: return !!hrm; + case BleServ.EnvSensing: return !!(bar || mag); + case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag); + } +}; + +const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => { + switch (serv) { + case BleServ.HRM: + if (hrm || initial) { + const o: BleCharAdvert = { + maxLen: encodeHrm.maxLen, + readable: true, + notify: true, + }; + if (hrm) { + o.value = encodeHrm(hrm); + hrm = undefined; + } + + return { [BleChar.HRM]: o }; + } + return {}; + + case BleServ.LocationAndNavigation: + if (gps || initial) { + const o: BleCharAdvert = { + maxLen: encodeGps.maxLen, + readable: true, + notify: true, + }; + if (gps) { + o.value = encodeGps(gps); + gps = undefined; + } + + return { [BleChar.LocationAndSpeed]: o }; + } else if (mag) { + const o: BleCharAdvert = { + maxLen: encodeGpsHeadingOnly.maxLen, + readable: true, + notify: true, + value: encodeGpsHeadingOnly(mag), + }; + + return { [BleChar.LocationAndSpeed]: o }; + } + return {}; + + case BleServ.EnvSensing: { + const o: BleServAdvert = {}; + + if (bar || initial) { + o[BleChar.Elevation] = { + maxLen: encodeElevation.maxLen, + readable: true, + notify: true, + }; + o[BleChar.TempCelsius] = { + maxLen: encodeTemp.maxLen, + readable: true, + notify: true, + }; + o[BleChar.Pressure] = { + maxLen: encodePressure.maxLen, + readable: true, + notify: true, + }; + + if (bar) { + o[BleChar.Elevation]!.value = encodeElevation(bar); + o[BleChar.TempCelsius]!.value = encodeTemp(bar); + o[BleChar.Pressure]!.value = encodePressure(bar); + bar = undefined; + } + } + + if (mag || initial) { + o[BleChar.MagneticFlux3D] = { + maxLen: encodeMag.maxLen, + readable: true, + notify: true, + }; + + if (mag) { + o[BleChar.MagneticFlux3D]!.value = encodeMag(mag); + } + } + + return o; + } + } +}; + +const getBleAdvert = (map: (s: BleServ) => T, all = false) => { + const advert: { [key in BleServ]?: T } = {}; + + for (const serv of services) { + if (all || haveServiceData(serv)) { + advert[serv] = map(serv); + } + } + + // clear mag only after both EnvSensing and LocationAndNavigation have run + mag = undefined; + + return advert; +}; + +// done via advertise in setServices() +//const updateBleAdvert = () => { +// let bleAdvert: ReturnType>; +// +// if (!(bleAdvert = (Bangle as any).bleAdvert)) { +// bleAdvert = getBleAdvert(_ => undefined); +// +// (Bangle as any).bleAdvert = bleAdvert; +// } +// +// try { +// NRF.setAdvertising(bleAdvert); +// } catch (e) { +// console.log("couldn't setAdvertising():", e); +// } +//}; + +const updateServices = () => { + const newAdvert = getBleAdvert(serviceToAdvert); + + NRF.updateServices(newAdvert); +}; + +const onAccel = (newAcc: NonNull) => acc = newAcc; +const onPressure = (newBar: NonNull) => bar = newBar; +const onGPS = (newGps: NonNull) => gps = newGps; +const onHRM = (newHrm: NonNull) => { + if (newHrm.confidence >= HRM_MIN_CONFIDENCE) + hrm = newHrm; + hrmAny = newHrm; +}; +const onMag = (newMag: NonNull) => mag = newMag; + +const hook = (enable: boolean) => { + // need to disable for perf reasons, when buttons are shown + if (enable) { + Bangle.on("accel", onAccel); + Bangle.on("pressure", onPressure); + Bangle.on("GPS", onGPS); + Bangle.on("HRM", onHRM); + Bangle.on("mag", onMag); + } else { + Bangle.removeListener("accel", onAccel); + Bangle.removeListener("pressure", onPressure); + Bangle.removeListener("GPS", onGPS); + Bangle.removeListener("HRM", onHRM); + Bangle.removeListener("mag", onMag); + } +} + +// --- intervals --- + +const setIntervals = ( + locked: boolean = Bangle.isLocked(), + connected: boolean = NRF.getSecurityStatus().connected, +) => { + changeInterval( + redrawInterval, + locked ? Intervals.UI_INFO_SLEEP : Intervals.UI_INFO, + ); + + if (connected) { + const interval = btnsShown ? Intervals.BLE_BACKGROUND : Intervals.BLE; + + if (bleInterval) { + changeInterval(bleInterval, interval); + } else { + bleInterval = setInterval(updateServices, interval); + } + } else if (bleInterval) { + clearInterval(bleInterval); + bleInterval = undefined; + } +}; + +const redrawInterval = setInterval(redraw, /*replaced*/1000); +Bangle.on("lock", locked => setIntervals(locked)); + +let bleInterval: undefined | number; +NRF.on("connect", () => setIntervals(undefined, true)); +NRF.on("disconnect", () => setIntervals(undefined, false)); + +setIntervals(); + +// turn things on +setBtnsShown(true); +enableSensors(); + +// set services/advert once at startup: +{ + // must have fixed services from the start: + const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true); + + const adServices = Object + .keys(ad) + .map((k: string) => k.replace("0x", "")); + + NRF.setServices( + ad, + { + advertise: adServices, + uart: false, + }, + ); +} diff --git a/apps/btadv/icon.js b/apps/btadv/icon.js new file mode 100644 index 000000000..03de6f5fd --- /dev/null +++ b/apps/btadv/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/gECoARQpARQpIRRkARQkgRRwBrPkmQBpIvDCIMEyQQIgIvDR4WSSRIvDCIUSSRIvDCISSCJRAvCCIoaDCIgvCGooaCiRNEDoRZFBwQRFgDCBPooOCOI0JkihDhApBHARxDgiSCyTFCHYQRGUIQRDHYIRCHYIRBiChDBAJKBHYICBpIRDyQyCSQQgBCJNBCIbCDCIZNDF4R0DEYwRCIIa5BI5ARDyAdCNZIFCCIKYBR5QRBVoJ6BWZY7CTwTXJWYQRFfZYRFRgQRCAoT4DCIgICCIQpCHARlCfYRBDCIhlDCIZuDGor1BCIgCBLgZZEAAiABEYIGCPooALUIYRQVQYRLdIQRPKAQROCBzjELJ4RPAHoA==")) diff --git a/apps/btadv/icon.png b/apps/btadv/icon.png new file mode 100644 index 000000000..28867f31e Binary files /dev/null and b/apps/btadv/icon.png differ diff --git a/apps/btadv/metadata.json b/apps/btadv/metadata.json new file mode 100644 index 000000000..29ee65286 --- /dev/null +++ b/apps/btadv/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "btadv", + "name": "btadv", + "shortName": "btadv", + "version": "0.01", + "description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth", + "icon": "icon.png", + "tags": "health,tool,sensors,bluetooth", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"btadv.app.js","url":"app.js"}, + {"name":"btadv.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index b07e7bd37..246b539d4 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -96,7 +96,7 @@ function draw(){ if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%"; if (bt.rr) layout.btRR.label = bt.rr.join(","); if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location]; - if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No"; + if (bt.contact !== undefined) layout.btContact.label = bt.contact ? /*LANG*/"Yes":/*LANG*/"No"; if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ"; } else { layout.bt.label = "--"; diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 8b82f6843..06f94854e 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -29,4 +29,5 @@ clkinfo.addInteractive that would cause ReferenceError. 0.28: Option to show (1) time only and (2) week of year. 0.29: use setItem of clockInfoMenu to change the active item -0.30: Use widget_utils. +0.30: Use widget_utils +0.31: Use clock_info module as an app diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index 5e2a7b55f..882d525f6 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -11,6 +11,7 @@ sub-items simply swipe up/down. To run an action (e.g. trigger home assistant), ![](screenshot_3.png) +Note: Check out the settings to change different themes. ## Settings - Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index de7c7d510..770c053c2 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -132,6 +132,7 @@ clockInfoItems[0].items.unshift({ name : "nop", let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + app: "bwclk", x : 0, y: 135, w: W, diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index 39106c827..430f466b2 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,7 +1,7 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.30", + "version": "0.31", "description": "A very minimalistic clock.", "readme": "README.md", "icon": "app.png", @@ -9,6 +9,7 @@ "type": "clock", "tags": "clock,clkinfo", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "allow_emulator": true, "storage": [ {"name":"bwclk.app.js","url":"app.js"}, diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 05eca83c0..27e1e2517 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -11,3 +11,5 @@ 0.10: Use default Bangle formatter for booleans 0.11: Fix off-by-one-error on next year 0.12: Mark dated events on a day +0.13: Switch to swipe left/right for month and up/down for year selection + Display events for current month on touch diff --git a/apps/calendar/README.md b/apps/calendar/README.md index 4fc6962cf..ec1c0c55a 100644 --- a/apps/calendar/README.md +++ b/apps/calendar/README.md @@ -4,11 +4,13 @@ Basic calendar ## Usage -- Use `BTN4` (left screen tap) to go to the previous month -- Use `BTN5` (right screen tap) to go to the next month +- Swipe left to go to the previous month +- Swipe right to go to the next month +- Swipe up (Bangle.js 2 only) to go to the previous year +- Swipe down (Bangle.js 2 only) to go to the next year +- Touch to display events for current month +- Press the button (button 3 on Bangle.js 1) to exit ## Settings -- Starts Sunday: whether the calendar should start on Sunday (default is Monday). - B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme. - diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index b61f2089c..6aab1aecd 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -24,11 +24,21 @@ let fgOtherMonth = gray1; let fgSameMonth = white; let bgEvent = blue; const eventsPerDay=6; // how much different events per day we can display +const date = new Date(); const timeutils = require("time_utils"); let settings = require('Storage').readJSON("calendar.json", true) || {}; let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0; -const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date); // all alarms that run on a specific date + // all alarms that run on a specific date +const events = (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}; +}); +events.sort((a,b) => a.date - b.date); if (settings.ndColors === undefined) { settings.ndColors = !g.theme.dark; @@ -192,6 +202,12 @@ function drawCalendar(date) { } } + const weekBeforeMonth = new Date(date.getTime()); + weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7); + const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0); + week2AfterMonth.setDate(week2AfterMonth.getDate() + 14); + const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth); + let i = 0; for (y = 0; y < rowN - 1; y++) { for (x = 0; x < colN; x++) { @@ -215,18 +231,20 @@ function drawCalendar(date) { ); } - // Display events for this day - const eventsCurDay = events.filter(ev => ev.date === curDay.toLocalISOString().substr(0, 10)); - if (eventsCurDay.length > 0) { + if (eventsThisMonth.length > 0) { + // Display events for this day g.setColor(bgEvent); - eventsCurDay.forEach(ev => { - const time = timeutils.decodeTime(ev.t); - const hour = time.h + time.m/60.0; - const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59 - const height = (y2-2) - (y1+2); // height of a cell - const sliceHeight = height/eventsPerDay; - const ystart = (y1+2) + slice*sliceHeight; - g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight); + eventsThisMonth.forEach((ev, idx) => { + if (sameDay(ev.date, curDay)) { + const hour = ev.date.getHours() + ev.date.getMinutes()/60.0; + const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59 + const height = (y2-2) - (y1+2); // height of a cell + const sliceHeight = height/eventsPerDay; + const ystart = (y1+2) + slice*sliceHeight; + g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight); + + eventsThisMonth.splice(idx, 1); // this event is no longer needed + } }); } @@ -242,23 +260,51 @@ function drawCalendar(date) { } } -const date = new Date(); -drawCalendar(date); -clearWatch(); -Bangle.on("touch", area => { - const month = date.getMonth(); - if (area == 1) { - let prevMonth = month > 0 ? month - 1 : 11; - if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); - date.setMonth(prevMonth); - } else { - let nextMonth = month < 11 ? month + 1 : 0; - if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1); - date.setMonth(nextMonth); - } - drawCalendar(date); -}); +function setUI() { + Bangle.setUI({ + mode : "custom", + swipe: (dirLR, dirUD) => { + if (dirLR<0) { // left + const month = date.getMonth(); + let prevMonth = month > 0 ? month - 1 : 11; + if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); + date.setMonth(prevMonth); + drawCalendar(date); + } else if (dirLR>0) { // right + const month = date.getMonth(); + let nextMonth = month < 11 ? month + 1 : 0; + if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1); + date.setMonth(nextMonth); + drawCalendar(date); + } else if (dirUD<0) { // up + date.setFullYear(date.getFullYear() - 1); + drawCalendar(date); + } else if (dirUD>0) { // down + date.setFullYear(date.getFullYear() + 1); + drawCalendar(date); + } + }, + btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(), + touch: (n,e) => { + const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => { + const dateStr = require("locale").date(e.date, 1); + const timeStr = require("locale").time(e.date, 1); + return { title: `${dateStr} ${timeStr}` + (e.msg ? " " + e.msg : "") }; + }); + if (menu.length === 0) { + menu.push({title: /*LANG*/"No events"}); + } + menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() }; + menu["< Back"] = () => { + E.showMenu(); + drawCalendar(date); + setUI(); + }; + E.showMenu(menu); + } + }); +} -// Show launcher when button pressed -setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" }); +drawCalendar(date); +setUI(); // No space for widgets! diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index 5bfc422fa..87599e3f4 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,11 +1,11 @@ { "id": "calendar", "name": "Calendar", - "version": "0.12", + "version": "0.13", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], - "tags": "calendar", + "tags": "calendar,tool", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "allow_emulator": true, diff --git a/apps/chargent/ChangeLog b/apps/chargent/ChangeLog index 7f837e50e..3003cd07d 100644 --- a/apps/chargent/ChangeLog +++ b/apps/chargent/ChangeLog @@ -1 +1,3 @@ 0.01: First version +0.02: Support BangleJS2 +0.03: Added threshold diff --git a/apps/chargent/README.md b/apps/chargent/README.md index 56bc763b4..db93615a6 100644 --- a/apps/chargent/README.md +++ b/apps/chargent/README.md @@ -6,6 +6,8 @@ The first stage of charging Li-ion ends at ~80% capacity when the charge voltage This app has no UI and no configuration. To disable the app, you have to uninstall it. +New in v0.03: before the very first buzz, the average value after the peak is written to chargent.json and used as threshold for future charges. This reduces the time spent in the second charge stage. + Side notes - Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100. - This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise. diff --git a/apps/chargent/boot.js b/apps/chargent/boot.js index 802c3f55a..c62003a21 100644 --- a/apps/chargent/boot.js +++ b/apps/chargent/boot.js @@ -1,22 +1,32 @@ (() => { + const pin = process.env.HWVERSION === 2 ? D3 : D30; + var id; Bangle.on('charging', (charging) => { if (charging) { if (!id) { var max = 0; - var count = 0; + var cnt = 0; + var sum = 0; + var lim = (require('Storage').readJSON('chargent.json', true) || {}).limit || 0; id = setInterval(() => { - var d30 = analogRead(D30); - if (max < d30) { - max = d30; - count = 0; + var val = analogRead(pin); + if (max < val) { + max = val; + cnt = 1; + sum = val; } else { - count++; - if (10 <= count) { // 10 * 30s == 5 min // TODO ? customizable - // TODO ? customizable - Bangle.buzz(500); - setTimeout(() => Bangle.buzz(500), 1000); + cnt++; + sum += val; + } + if (10 < cnt || (lim && lim <= max)) { // 10 * 30s == 5 min // TODO ? customizable + if (!lim) { + lim = sum / cnt; + require('Storage').writeJSON('chargent.json', {limit: lim}); } + // TODO ? customizable + Bangle.buzz(500); + setTimeout(() => Bangle.buzz(500), 1000); } }, 30*1000); } diff --git a/apps/chargent/boot.min.js b/apps/chargent/boot.min.js deleted file mode 100644 index 700198146..000000000 --- a/apps/chargent/boot.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){var a;Bangle.on("charging",function(e){if(e){if(!a){var c=0,b=0;a=setInterval(function(){var d=analogRead(D30);c { + let durationOnPause = "---"; + let redrawInterval: number | undefined; + let startTime: number | undefined; + + const unqueueRedraw = () => { + if (redrawInterval) clearInterval(redrawInterval); + redrawInterval = undefined; + }; + + const queueRedraw = function(this: ClockInfo.MenuItem) { + unqueueRedraw(); + redrawInterval = setInterval(() => this.emit('redraw'), 100); + }; + + const pad2 = (s: number) => ('0' + s.toFixed(0)).slice(-2); + + const duration = (start: number) => { + let seconds = (Date.now() - start) / 1000; + + if (seconds < 60) + return seconds.toFixed(1); + + let mins = seconds / 60; + seconds %= 60; + + if (mins < 60) + return `${pad2(mins)}m${pad2(seconds)}s`; + + let hours = mins / 60; + mins %= 60; + + return `${Math.round(hours)}h${pad2(mins)}m${pad2(seconds)}s`; + }; + + const img = () => atob("GBiBAAAAAAB+AAB+AAAAAAB+AAH/sAOB8AcA4A4YcAwYMBgYGBgYGBg8GBg8GBgYGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="); + + return { + name: "timer", + img: img(), + items: [ + { + name: "stopw", + get: () => ({ + text: startTime + ? duration(startTime) + : durationOnPause, + img: img(), + }), + show: queueRedraw, + hide: unqueueRedraw, + run: function() { // tapped + if (startTime) { + durationOnPause = duration(startTime); + startTime = undefined; + unqueueRedraw(); + } else { + queueRedraw.call(this); + startTime = Date.now(); + } + } + } + ] + }; +}) diff --git a/apps/clkinfostopw/metadata.json b/apps/clkinfostopw/metadata.json new file mode 100644 index 000000000..c0821b8be --- /dev/null +++ b/apps/clkinfostopw/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "clkinfostopw", + "name": "Stop Watch Clockinfo", + "version":"0.01", + "description": "A simple stopwatch, shown via clockinfo", + "icon": "app.png", + "type": "clkinfo", + "tags": "clkinfo,timer", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"stopw.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog new file mode 100644 index 000000000..7d51e0da5 --- /dev/null +++ b/apps/clock_info/ChangeLog @@ -0,0 +1,2 @@ +0.01: Moved from modules/clock_info.js +0.02: Fix settings page diff --git a/apps/clock_info/README.md b/apps/clock_info/README.md new file mode 100644 index 000000000..37bac8f80 --- /dev/null +++ b/apps/clock_info/README.md @@ -0,0 +1,99 @@ +# Clock Info module + +Module that allows for loading of clock 'info' displays +that can be scrolled through on the clock face. + +## Usage + +In most clocks that use Clock Info, you can interact with it the following way: + +* Tap on an info menu to 'focus' it (this will highlight it in some way) +* Swipe up/down to change which info is displayed within the category +* Tap to activate (if supported), eg for a Stopwatch, Home Assistant, etc +* Swipe left/right to change between categories (Bangle.js/Agenda/etc) +* Tap outside the area of the Clock Info to 'defocus' it + +## Extensions + +By default Clock Info provides: + +* Battery +* Steps +* Heart Rate (HRM) +* Altitude + +But by installing other apps that are tagged with the type `clkinfo` you can +add extra features. For example [Sunrise Clockinfo](http://banglejs.com/apps/?id=clkinfosunrise) + +A full list is available at https://banglejs.com/apps/?q=clkinfo + +## Settings + +Available from `Settings -> Apps -> Clock Info` + +* `Defocus on Lock` - (default=on) when the watch screen auto-locks, defocus +and previously focussed Clock Infos +* `HRM` - (default=always) when does the HRM stay on? + * `Always` - When a HRM ClockInfo is shown, keep the HRM on + * `Tap` - When a HRM ClockInfo is shown, turn HRM on for 1 minute. Turn on again when tapped. +* `Max Altitude` - on clocks like [Circles Clock](https://banglejs.com/apps/?id=circlesclock) a + progress/percent indicator may be shown. The percentage for altitude will be how far towards + the Max Altitude you are. If you go higher than `Max Altitude` the correct altitude will still + be shown - the percent indicator will just read 100% + +## API (Software development) + +See http://www.espruino.com/Bangle.js+Clock+Info for details on using +this module inside your apps (or generating your own Clock Info +extensions). + +`load()` returns an array of menu objects, where each object contains a list of menu items: +* `name` : text to display and identify menu object (e.g. weather) +* `img` : a 24x24px image +* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date) +* `items` : menu items such as temperature, humidity, wind etc. + +Note that each item is an object with: + +* `item.name` : friendly name to identify an item (e.g. temperature) +* `item.hasRange` : if `true`, `.get` returns `v/min/max` values (for progress bar/guage) +* `item.get` : function that returns an object: + +```JS +{ + 'text' // the text to display for this item + 'short' // optional: a shorter text to display for this item (at most 6 characters) + 'img' // optional: a 24x24px image to display for this item + 'color' // optional: a color string (like "#f00") to color the icon in compatible clocks + 'v' // (if hasRange==true) a numerical value + 'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage) +} +``` + +* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get' +* `item.hide` : called when item should be hidden. Disables updates. +* `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show') +* `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user. + +See the bottom of `lib.js` for example usage... + +example.clkinfo.js : + +```JS +(function() { + return { + name: "Bangle", + img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }), + items: [ + { name : "Item1", + get : () => ({ text : "TextOfItem1", v : 10, min : 0, max : 100, + img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") + }), + show : () => {}, + hide : () => {} + // run : () => {} optional (called when tapped) + } + ] + }; +}) // must not have a semi-colon! +``` diff --git a/apps/clock_info/app-icon.js b/apps/clock_info/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/clock_info/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/clock_info/app.png b/apps/clock_info/app.png new file mode 100644 index 000000000..4013f353f Binary files /dev/null and b/apps/clock_info/app.png differ diff --git a/modules/clock_info.js b/apps/clock_info/lib.js similarity index 76% rename from modules/clock_info.js rename to apps/clock_info/lib.js index f185a494c..7251f1b00 100644 --- a/modules/clock_info.js +++ b/apps/clock_info/lib.js @@ -1,54 +1,4 @@ -/* Module that allows for loading of clock 'info' displays -that can be scrolled through on the clock face. - -`load()` returns an array of menu objects, where each object contains a list of menu items: -* `name` : text to display and identify menu object (e.g. weather) -* `img` : a 24x24px image -* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date) -* `items` : menu items such as temperature, humidity, wind etc. - -Note that each item is an object with: - -* `item.name` : friendly name to identify an item (e.g. temperature) -* `item.hasRange` : if `true`, `.get` returns `v/min/max` values (for progress bar/guage) -* `item.get` : function that returns an object: - -{ - 'text' // the text to display for this item - 'short' // optional: a shorter text to display for this item (at most 6 characters) - 'img' // optional: a 24x24px image to display for this item - 'color' // optional: a color string (like "#ffffff") to color the icon in compatible clocks - 'v' // (if hasRange==true) a numerical value - 'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage) -} - -* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get' -* `item.hide` : called when item should be hidden. Disables updates. -* `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show') -* `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user. - -See the bottom of this file for example usage... - -example.clkinfo.js : - -(function() { - return { - name: "Bangle", - img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }), - items: [ - { name : "Item1", - get : () => ({ text : "TextOfItem1", v : 10, min : 0, max : 100, - img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") - }), - show : () => {}, - hide : () => {} - // run : () => {} optional (called when tapped) - } - ] - }; -}) // must not have a semi-colon! - -*/ +/* See the README for more info... */ let storage = require("Storage"); let stepGoal = undefined; @@ -60,7 +10,21 @@ if (stepGoal == undefined) { stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; } +// Load the settings, with defaults +exports.loadSettings = function() { + return Object.assign({ + hrmOn : 0, // 0(Always), 1(Tap) + defocusOnLock : true, + maxAltitude : 3000, + apps : {} + }, + require("Storage").readJSON("clock_info.json",1)||{} + ); +}; + exports.load = function() { + var settings = exports.loadSettings(); + delete settings.apps; // keep just the basic settings in memory // info used for drawing... var hrm = 0; var alt = "--"; @@ -111,8 +75,31 @@ exports.load = function() { text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200, img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==") }}, - show : function() { Bangle.setHRMPower(1,"clkinfo"); Bangle.on("HRM", hrmUpdateHandler); hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); }, - hide : function() { Bangle.setHRMPower(0,"clkinfo"); Bangle.removeListener("HRM", hrmUpdateHandler); hrm = 0; }, + run : function() { + Bangle.setHRMPower(1,"clkinfo"); + if (settings.hrmOn==1/*Tap*/) { + /* turn off after 1 minute. If Health HRM monitoring is + enabled we will still get HRM events every so often */ + this.timeout = setTimeout(function() { + this.timeout = undefined; + Bangle.setHRMPower(0,"clkinfo"); + }, 60000); + } + }, + show : function() { + Bangle.on("HRM", hrmUpdateHandler); + hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); + this.run(); // start HRM + }, + hide : function() { + Bangle.setHRMPower(0,"clkinfo"); + Bangle.removeListener("HRM", hrmUpdateHandler); + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + hrm = 0; + }, } ], }]; @@ -123,7 +110,7 @@ exports.load = function() { hasRange : true, get : () => ({ text : alt, v : parseInt(alt), - min : 0, max : 3000, + min : 0, max : settings.maxAltitude, img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==") }), show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); }, @@ -148,9 +135,18 @@ exports.load = function() { return menu; }; + /** Adds an interactive menu that could be used on a clock face by swiping. Simply supply the menu data (from .load) and a function to draw the clock info. +options = { + app : "str", // optional: app ID used when saving clock_info positions + // if defined, your app will remember its own positions, + // otherwise all apps share the same ones + x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info + draw : (itm, info, options) // draw function +} + For example: let clockInfoItems = require("clock_info").load(); @@ -181,7 +177,7 @@ clockInfoMenu is the 'options' parameter, with the following added: * `redraw` : function - force a redraw * `focus` : function - bool to show if menu is focused or not -You can have more than one clock_info at once as well, sfor instance: +You can have more than one clock_info at once as well, for instance: let clockInfoDraw = (itm, info, options) => { g.reset().setBgColor(options.bg).setColor(options.fg).clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); @@ -201,19 +197,19 @@ exports.addInteractive = function(menu, options) { options.index = 0|exports.loadCount; exports.loadCount = options.index+1; options.focus = options.index==0 && options.x===undefined; // focus if we're the first one loaded and no position has been defined - const appName = "default:"+options.index; + const appName = (options.app||"default")+":"+options.index; - { // load the currently showing clock_infos - let settings = require("Storage").readJSON("clock_info.json",1)||{}; - if (settings[appName]) { - let a = settings[appName].a|0; - let b = settings[appName].b|0; - if (menu[a] && menu[a].items[b]) { // all ok - options.menuA = a; - options.menuB = b; - } + // load the currently showing clock_infos + let settings = exports.loadSettings() + if (settings.apps[appName]) { + let a = settings.apps[appName].a|0; + let b = settings.apps[appName].b|0; + if (menu[a] && menu[a].items[b]) { // all ok + options.menuA = a; + options.menuB = b; } } + if (options.menuA===undefined) options.menuA = 0; if (options.menuB===undefined) options.menuB = Math.min(exports.loadCount, menu[options.menuA].items.length)-1; function drawItem(itm) { @@ -262,41 +258,46 @@ exports.addInteractive = function(menu, options) { menuShowItem(menu[options.menuA].items[options.menuB]); } // save the currently showing clock_info - let settings = require("Storage").readJSON("clock_info.json",1)||{}; - settings[appName] = {a:options.menuA,b:options.menuB}; + let settings = exports.loadSettings(); + settings.apps[appName] = {a:options.menuA,b:options.menuB}; require("Storage").writeJSON("clock_info.json",settings); } Bangle.on("swipe",swipeHandler); - var touchHandler; - var lockHandler; + let touchHandler, lockHandler; if (options.x!==undefined && options.y!==undefined && options.w && options.h) { - lockHandler = function() { - if(options.focus) { - options.focus=false; - options.redraw(); - } - }; touchHandler = function(_,e) { if (e.x(options.x+options.w) || e.y>(options.y+options.h)) { if (options.focus) { options.focus=false; + delete Bangle.CLKINFO_FOCUS; options.redraw(); } return; // outside area } if (!options.focus) { options.focus=true; // if not focussed, set focus + Bangle.CLKINFO_FOCUS=true; options.redraw(); } else if (menu[options.menuA].items[options.menuB].run) { Bangle.buzz(100, 0.7); menu[options.menuA].items[options.menuB].run(); // allow tap on an item to run it (eg home assistant) } else { options.focus=true; + Bangle.CLKINFO_FOCUS=true; } }; Bangle.on("touch",touchHandler); - Bangle.on("lock", lockHandler); + if (settings.defocusOnLock) { + lockHandler = function() { + if(options.focus) { + options.focus=false; + delete Bangle.CLKINFO_FOCUS; + options.redraw(); + } + }; + Bangle.on("lock", lockHandler); + } } // draw the first item menuShowItem(menu[options.menuA].items[options.menuB]); @@ -305,6 +306,7 @@ exports.addInteractive = function(menu, options) { Bangle.removeListener("swipe",swipeHandler); if (touchHandler) Bangle.removeListener("touch",touchHandler); if (lockHandler) Bangle.removeListener("lock", lockHandler); + delete Bangle.CLKINFO_FOCUS; menuHideItem(menu[options.menuA].items[options.menuB]); exports.loadCount--; }; @@ -328,6 +330,8 @@ exports.addInteractive = function(menu, options) { return true; } + + delete settings; // don't keep settings in RAM - save space return options; }; diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json new file mode 100644 index 000000000..6cc3e1233 --- /dev/null +++ b/apps/clock_info/metadata.json @@ -0,0 +1,18 @@ +{ "id": "clock_info", + "name": "Clock Info Module", + "shortName": "Clock Info", + "version":"0.02", + "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", + "icon": "app.png", + "type": "module", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "provides_modules" : ["clock_info"], + "readme": "README.md", + "storage": [ + {"name":"clock_info","url":"lib.js"}, + {"name":"clock_info.settings.js","url":"settings.js"} + ], "data": [ + {"name":"clock_info.json","url":"lib.js"} + ] +} diff --git a/apps/clock_info/settings.js b/apps/clock_info/settings.js new file mode 100644 index 000000000..a86fae473 --- /dev/null +++ b/apps/clock_info/settings.js @@ -0,0 +1,30 @@ +(function(back) { + let settings = require("clock_info").loadSettings(); + + function save(key, value) { + settings[key] = value; + require('Storage').write("clock_info.json", settings); + } + + let menu ={ + '': { 'title': 'Clock Info' }, + /*LANG*/'< Back': back, + /*LANG*/'Defocus on Lock': { + value: !!settings.defocusOnLock, + onchange: x => save('defocusOnLock', x), + }, + /*LANG*/'HRM': { + value: settings.hrmOn, + min: 0, max: 1, step: 1, + format: v => ["Always","Tap"][v], + onchange: x => save('hrmOn', x), + }, + /*LANG*/'Max Altitude': { + value: settings.maxAltitude, + min: 500, max: 10000, step: 500, + format: v => v+"m", + onchange: x => save('maxAltitude', x), + } + }; + E.showMenu(menu); +}) diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 27d4fc7f4..5657bf26d 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -4,3 +4,5 @@ 0.04: Use default Bangle formatter for booleans 0.05: Improved colors (connected vs disconnected) 0.06: Tell clock widgets to hide. +0.07: Convert Yes/No On/Off in settings to checkboxes +0.08: Fixed typo in settings.js for DRAGDOWN to make option work \ No newline at end of file diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index 872211495..998115827 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.06", + "version": "0.08", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index d4cc4df68..a406f3cf7 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -16,7 +16,7 @@ actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"]; require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js",""))); - + function writeSettings() { require('Storage').writeJSON(FILE, settings); } @@ -93,7 +93,7 @@ value: actions.indexOf(settings.DRAGDOWN), format: v => actions[v], onchange: v => { - settings.DRGDOWN = actions[v]; + settings.DRAGDOWN = actions[v]; writeSettings(); } }, @@ -106,18 +106,11 @@ writeSettings(); } }, - 'Load deafauls?': { - value: 0, - min: 0, max: 1, - format: v => ["No", "Yes"][v], - onchange: v => { - if (v == 1) { - settings = defaults; - writeSettings(); - load(); - } - } - }, + 'Load defaults': () => { + settings = defaults; + writeSettings(); + load(); + } }; // Show the menu E.showMenu(menu); diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index a98be5c0f..5264e8d42 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -6,3 +6,4 @@ 0.06: Now read wheel rev as well as cadence sensor Improve connection code 0.07: Make Bangle.js 2 compatible +0.08: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json index 4006789ef..ba250c914 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -2,7 +2,7 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.07", + "version": "0.08", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth", diff --git a/apps/cscsensor/settings.js b/apps/cscsensor/settings.js index d7a7d565d..4fac5d0c1 100644 --- a/apps/cscsensor/settings.js +++ b/apps/cscsensor/settings.js @@ -23,17 +23,17 @@ } } const menu = { - '': { 'title': 'Cycle speed sensor' }, + '': { 'title': /*LANG*/'Cycle speed sensor' }, '< Back': back, - 'Wheel circ.(mm)': { + /*LANG*/'Wheel circ.(mm)': { value: s.wheelcirc, min: 800, max: 2400, step: 5, onchange: save('wheelcirc'), }, - 'Reset total distance': function() { - E.showPrompt("Zero total distance?", {buttons: {"No":false, "Yes":true}}).then(function(v) { + /*LANG*/'Reset total distance': function() { + E.showPrompt(/*LANG*/"Zero total distance?", {buttons: {/*LANG*/"No":false, /*LANG*/"Yes":true}}).then(function(v) { if (v) { s['totaldist'] = 0; storage.write(SETTINGS_FILE, s); diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog new file mode 100644 index 000000000..a228aab54 --- /dev/null +++ b/apps/draguboard/ChangeLog @@ -0,0 +1 @@ +0.01: New App based on dragboard, but with a U shaped drag area diff --git a/apps/draguboard/README.md b/apps/draguboard/README.md new file mode 100644 index 000000000..2386c7658 --- /dev/null +++ b/apps/draguboard/README.md @@ -0,0 +1,8 @@ +Swipe along the drag bars and release to select a letter, number or punctuation. + +Tap on left for backspace or right for space. + +Settings: +- ABC Color: color of the characters row +- Num Color: color of the digits and symbols row +- Highlight Color: color of the currently shown character diff --git a/apps/draguboard/app.png b/apps/draguboard/app.png new file mode 100644 index 000000000..ae7262b47 Binary files /dev/null and b/apps/draguboard/app.png differ diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js new file mode 100644 index 000000000..258f8b02d --- /dev/null +++ b/apps/draguboard/lib.js @@ -0,0 +1,156 @@ +exports.input = function(options) { + options = options||{}; + var text = options.text; + if ("string"!=typeof text) text=""; + let settings = require('Storage').readJSON('draguboard.json',1)||{}; + + var R; + const paramToColor = (param) => g.toColor(`#${settings[param].toString(16).padStart(3,0)}`); + var BGCOLOR = g.theme.bg; + var HLCOLOR = settings.Highlight ? paramToColor("Highlight") : g.theme.fg; + var ABCCOLOR = settings.ABC ? paramToColor("ABC") : g.toColor(1,0,0);//'#FF0000'; + var NUMCOLOR = settings.Num ? paramToColor("Num") : g.toColor(0,1,0);//'#00FF00'; + var BIGFONT = '6x8:3'; + var SMALLFONT = '6x8:1'; + + var LEFT = "IJKLMNOPQ"; + var MIDDLE = "ABCDEFGH"; + var RIGHT = "RSTUVWXYZ"; + + var NUM = ' 1234567890!?,.-@'; + var rectHeight = 40; + var vLength = LEFT.length; + var MIDPADDING; + var NUMPADDING; + var showCharY; + var middleWidth; + var middleStart; + var topStart; + + function drawAbcRow() { + g.clear(); + try { // Draw widgets if they are present in the current app. + if (WIDGETS) Bangle.drawWidgets(); + } catch (_) {} + g.setColor(ABCCOLOR); + g.setFont('6x8:2x1'); + g.setFontAlign(-1, -1, 0); + g.drawString(RIGHT.split("").join("\n\n"), R.x2-28, topStart); + g.drawString(LEFT.split("").join("\n\n"), R.x+22, topStart); + g.setFont('6x8:1x2'); + var spaced = MIDDLE.split("").join(" "); + middleWidth = g.stringWidth(spaced); + middleStart = (R.x2-middleWidth)/2; + g.drawString(spaced, (R.x2-middleWidth)/2, (R.y2)/2); + g.fillRect(MIDPADDING, (R.y2)-26, (R.x2-MIDPADDING), (R.y2)); + // Draw left and right drag rectangles + g.fillRect(R.x, R.y, 12, R.y2); + g.fillRect(R.x2, R.y, R.x2-12, R.y2); + } + + function drawNumRow() { + g.setFont('6x8:1x2'); + g.setColor(NUMCOLOR); + NUMPADDING = (R.x2-g.stringWidth(NUM))/2; + g.setFontAlign(-1, -1, 0); + g.drawString(NUM, NUMPADDING, (R.y2)/4); + g.drawString("<-", NUMPADDING+10, showCharY+5); + g.drawString("->", R.x2-(NUMPADDING+20), showCharY+5); + + g.fillRect(NUMPADDING, (R.y2)-rectHeight*4/3, (R.x2)-NUMPADDING, (R.y2)-rectHeight*2/3); + } + + function updateTopString() { + g.setFont(SMALLFONT); + g.setColor(BGCOLOR); + g.fillRect(R.x,R.y,R.x2,R.y+9); + var rectLen = text.length<27? text.length*6:27*6; + g.setColor(0.7,0,0); + //draw cursor at end of text + g.fillRect(R.x+rectLen+5,R.y,R.x+rectLen+10,R.y+9); + g.setColor(HLCOLOR); + g.setFontAlign(-1, -1, 0); + g.drawString(text.length<=27? text : '<- '+text.substr(-24,24), R.x+5, R.y+1); + } + + function showChars(chars) { + "ram"; + + // clear large character + g.setColor(BGCOLOR); + g.fillRect(R.x+65,showCharY,R.x2-65,showCharY+28); + + // show new large character + g.setColor(HLCOLOR); + g.setFont(BIGFONT); + g.setFontAlign(-1, -1, 0); + g.drawString(chars, (R.x2 - g.stringWidth(chars))/2, showCharY+4); + } + + var charPos; + var char; + var prevChar; + + function moveCharPos(list, select, posPixels) { + charPos = Math.min(list.length-1, Math.max(0, Math.floor(posPixels))); + char = list.charAt(charPos); + + if (char != prevChar) showChars(char); + prevChar = char; + + if (select) { + text += char; + updateTopString(); + } + } + + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + g.clearRect(Bangle.appRect); + resolve(text); + }, + drag: function(event) { + "ram"; + + // drag on middle bottom rectangle + if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) { + moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length)); + } + // drag on left or right rectangle + else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) { + moveCharPos(event.x ( (R.y2) - 52 ))) { + moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6); + } + // Make a space or backspace by tapping right or left on screen above green rectangle + else if (event.y > R.y && event.b == 0) { + if (event.x < (R.x2)/2) { + showChars('<-'); + text = text.slice(0, -1); + } else { + //show space sign + showChars('->'); + text += ' '; + } + prevChar = null; + updateTopString(); + } + } + }); + + R = Bangle.appRect; + MIDPADDING = R.x + 35; + showCharY = (R.y2)/3; + topStart = R.y+12; + + drawAbcRow(); + drawNumRow(); + updateTopString(); + }); +}; diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json new file mode 100644 index 000000000..926e36807 --- /dev/null +++ b/apps/draguboard/metadata.json @@ -0,0 +1,15 @@ +{ "id": "draguboard", + "name": "DragUboard", + "version":"0.01", + "description": "A library for text input via swiping U-shaped keyboard.", + "icon": "app.png", + "type":"textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"}, + {"name":"draguboard.settings.js","url":"settings.js"} + ] +} diff --git a/apps/draguboard/screenshot.png b/apps/draguboard/screenshot.png new file mode 100644 index 000000000..f2cb91717 Binary files /dev/null and b/apps/draguboard/screenshot.png differ diff --git a/apps/draguboard/settings.js b/apps/draguboard/settings.js new file mode 100644 index 000000000..c94ebee70 --- /dev/null +++ b/apps/draguboard/settings.js @@ -0,0 +1,44 @@ +(function(back) { + let settings = require('Storage').readJSON('draguboard.json',1)||{}; + const colors = { + 4095: /*LANG*/"White", + 4080: /*LANG*/"Yellow", + 3840: /*LANG*/"Red", + 3855: /*LANG*/"Magenta", + 255: /*LANG*/"Cyan", + 240: /*LANG*/"Green", + 15: /*LANG*/"Blue", + 0: /*LANG*/"Black", + '-1': /*LANG*/"Default" + }; + + const save = () => require('Storage').write('draguboard.json', settings); + function colorMenu(key) { + let menu = {'': {title: key}, '< Back': () => E.showMenu(appMenu)}; + Object.keys(colors).forEach(color => { + var label = colors[color]; + menu[label] = { + value: settings[key] == color, + onchange: () => { + if (color >= 0) { + settings[key] = color; + } else { + delete settings[key]; + } + save(); + setTimeout(E.showMenu, 10, appMenu); + } + }; + }); + return menu; + } + + const appMenu = { + '': {title: 'draguboard'}, '< Back': back, + /*LANG*/'ABC Color': () => E.showMenu(colorMenu("ABC")), + /*LANG*/'Num Color': () => E.showMenu(colorMenu("Num")), + /*LANG*/'Highlight Color': () => E.showMenu(colorMenu("Highlight")) + }; + + E.showMenu(appMenu); +}); \ No newline at end of file diff --git a/apps/f9lander/ChangeLog b/apps/f9lander/ChangeLog index a13f2a313..b5a33bd2e 100644 --- a/apps/f9lander/ChangeLog +++ b/apps/f9lander/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Add lightning +0.03: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json index 1db777099..e53805ee0 100644 --- a/apps/f9lander/metadata.json +++ b/apps/f9lander/metadata.json @@ -1,7 +1,7 @@ { "id": "f9lander", "name": "Falcon9 Lander", "shortName":"F9lander", - "version":"0.02", + "version":"0.03", "description": "Land a rocket booster", "icon": "f9lander.png", "screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }], diff --git a/apps/f9lander/settings.js b/apps/f9lander/settings.js index 0f9fba302..9d85da394 100644 --- a/apps/f9lander/settings.js +++ b/apps/f9lander/settings.js @@ -2,7 +2,6 @@ /** * @param {function} back Use back() to return to settings menu */ -const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; (function(back) { const SETTINGS_FILE = 'f9settings.json' // initialize with default settings... @@ -27,8 +26,7 @@ const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; '': { 'title': 'OpenWind' }, '< Back': back, 'Lightning': { - value: settings.lightning, - format: boolFormat, + value: !!settings.lightning, onchange: save('lightning'), } } diff --git a/apps/fileman/fileman.app.js b/apps/fileman/fileman.app.js index 6a3c5598d..5baae298b 100644 --- a/apps/fileman/fileman.app.js +++ b/apps/fileman/fileman.app.js @@ -7,7 +7,7 @@ var m; var files; function delete_file(fn) { - E.showPrompt("Delete\n"+fn+"?", {buttons: {"No":false, "Yes":true}}).then(function(v) { + E.showPrompt(/*LANG*/"Delete\n"+fn+"?", {buttons: {/*LANG*/"No":false, /*LANG*/"Yes":true}}).then(function(v) { if (v) { if (fn.charCodeAt(fn.length-1)==1) { var fh = STOR.open(fn.substr(0, fn.length-1), "r"); diff --git a/apps/fuzzyw/ChangeLog b/apps/fuzzyw/ChangeLog index 206efb10f..dd73475f9 100644 --- a/apps/fuzzyw/ChangeLog +++ b/apps/fuzzyw/ChangeLog @@ -1,3 +1,4 @@ 0.01: First release 0.02: Move translations to locale module (removed watch settings, now pick language in Bangle App Loader, More..., Settings) -0.03: Change for fast loading, use widget_utils to hide widgets \ No newline at end of file +0.03: Change for fast loading, use widget_utils to hide widgets +0.04: Add animation when display changes \ No newline at end of file diff --git a/apps/fuzzyw/README.md b/apps/fuzzyw/README.md index 49d0fe0d5..062c9ac25 100644 --- a/apps/fuzzyw/README.md +++ b/apps/fuzzyw/README.md @@ -16,7 +16,6 @@ Most translations are taken from the original Fuzzy Text International code. ## TODO * Bold hour word (as the pebble version has) -* Animation when changing time? ## References Based on Pebble app Fuzzy Text International: https://github.com/hallettj/Fuzzy-Text-International diff --git a/apps/fuzzyw/fuzzyw.app.js b/apps/fuzzyw/fuzzyw.app.js index e185e2ccf..8bc51710f 100644 --- a/apps/fuzzyw/fuzzyw.app.js +++ b/apps/fuzzyw/fuzzyw.app.js @@ -33,13 +33,17 @@ ] }; -let text_scale = 3.5; +let text_scale = 4; let timeout = 2.5*60; let drawTimeout; +let animInterval; +let time_string = ""; +let time_string_old = ""; +let time_string_old_wrapped = ""; let loadSettings = function() { settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'showWidgets': false}; -} +}; let queueDraw = function(seconds) { let millisecs = seconds * 1000; @@ -48,10 +52,7 @@ let queueDraw = function(seconds) { drawTimeout = undefined; draw(); }, millisecs - (Date.now() % millisecs)); -} - -const h = g.getHeight(); -const w = g.getWidth(); +}; let getTimeString = function(date) { let segment = Math.round((date.getMinutes()*60 + date.getSeconds() + 1)/300); @@ -63,18 +64,47 @@ let getTimeString = function(date) { f_string = f_string.replace('$2', fuzzy_string.hours[(hour + 1) % 12]); } return f_string; -} +}; let draw = function() { - let time_string = getTimeString(new Date()).replace('*', ''); - // print(time_string); - g.setFont('Vector', (h-24*2)/text_scale); - g.setFontAlign(0, 0); - g.clearRect(0, 24, w, h-24); - g.setColor(g.theme.fg); - g.drawString(g.wrapString(time_string, w).join("\n"), w/2, h/2); + time_string = getTimeString(new Date()).replace('*', ''); + //print(time_string); + if (time_string != time_string_old) { + g.setFont('Vector', R.h/text_scale).setFontAlign(0, 0); + animate(3); + } queueDraw(timeout); -} +}; + +let animate = function(step) { + if (animInterval) clearInterval(animInterval); + let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n"); + slideX = 0; + animInterval = setInterval(function() { + let time_start = getTime() + //blank old time + g.setColor(g.theme.bg); + g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2); + g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2); + g.setColor(g.theme.fg); + slideX += step; + let stop = false; + if (slideX>=R.w) { + slideX=R.w; + stop = true; + } + //draw shifted new time + g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2); + g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2); + if (stop) { + time_string_old = time_string; + clearInterval(animInterval); + animInterval=undefined; + time_string_old_wrapped = time_string_new_wrapped; + } + print(Math.round((getTime() - time_start)*1000)) + }, 30); +}; g.clear(); loadSettings(); @@ -95,6 +125,8 @@ Bangle.setUI({ // Called to unload all of the clock app if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; + if (animInterval) clearInterval(animInterval); + animInterval = undefined; require('widget_utils').show(); // re-show widgets } }); @@ -106,5 +138,6 @@ if (settings.showWidgets) { require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe } +R = Bangle.appRect; draw(); } \ No newline at end of file diff --git a/apps/fuzzyw/metadata.json b/apps/fuzzyw/metadata.json index d1040fda3..97f060866 100644 --- a/apps/fuzzyw/metadata.json +++ b/apps/fuzzyw/metadata.json @@ -2,7 +2,7 @@ "id":"fuzzyw", "name":"Fuzzy Text Clock", "shortName": "Fuzzy Text", - "version": "0.03", + "version": "0.04", "description": "An imprecise clock for when you're not in a rush", "readme": "README.md", "icon":"fuzzyw.png", diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog index ea0b48eb9..a9673f6de 100644 --- a/apps/fwupdate/ChangeLog +++ b/apps/fwupdate/ChangeLog @@ -6,3 +6,4 @@ Add CRC checks for common bootloaders that we know don't work 0.04: Include a precompiled bootloader for easy bootloader updates 0.05: Rename Bootloader->DFU and add explanation to avoid confusion with Bootloader app +0.06: Lower chunk size to 1024 (from 2048) to make firmware updates more reliable diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 31eb4a256..de972b0e8 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -98,6 +98,7 @@ function onInit(device) { if (crc==4056371285) version = "2v13"; if (crc==1038322422) version = "2v14"; if (crc==2560806221) version = "2v15"; + if (crc==2886730689) version = "2v16"; if (!ok) { version += `(⚠ update required)`; } @@ -317,7 +318,7 @@ function createJS_app(binary, startAddress, endAddress) { hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("DFU 2v10.236 needs update"); load();}\n`; hexJS += '\x10var s = require("Storage");\n'; hexJS += '\x10s.erase(".firmware");\n'; - var CHUNKSIZE = 2048; + var CHUNKSIZE = 1024; for (var i=0;iCHUNKSIZE) l=CHUNKSIZE; diff --git a/apps/fwupdate/metadata.json b/apps/fwupdate/metadata.json index 372f6850c..e3294f316 100644 --- a/apps/fwupdate/metadata.json +++ b/apps/fwupdate/metadata.json @@ -1,7 +1,7 @@ { "id": "fwupdate", "name": "Firmware Update", - "version": "0.05", + "version": "0.06", "description": "Uploads new Espruino firmwares to Bangle.js 2", "icon": "app.png", "type": "RAM", diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js index cf6c84c73..ae63bb0f9 100644 --- a/apps/gbridge/settings.js +++ b/apps/gbridge/settings.js @@ -24,22 +24,22 @@ var mainmenu = { "" : { "title" : "Gadgetbridge" }, "< Back" : back, - "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, - "Show Icon" : { + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, + /*LANG*/"Show Icon" : { value: settings().showIcon, onchange: setIcon }, - "Find Phone" : function() { E.showMenu(findPhone); }, - "Record HRM" : { + /*LANG*/"Find Phone" : function() { E.showMenu(findPhone); }, + /*LANG*/"Record HRM" : { value: !!settings().hrm, onchange: v => updateSetting('hrm', v) - } + } }; var findPhone = { "" : { "title" : "-- Find Phone --" }, - "On" : _=>gb({t:"findPhone",n:true}), - "Off" : _=>gb({t:"findPhone",n:false}), + /*LANG*/"On" : _=>gb({t:"findPhone",n:true}), + /*LANG*/"Off" : _=>gb({t:"findPhone",n:false}), "< Back" : function() { E.showMenu(mainmenu); }, }; diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index f913c9e58..bfb2f4282 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -73,3 +73,5 @@ * Display current and next segment in red so that you know where to go. * Avoid angles flickering at low speed at the cost of less refresh. * Splash screen while waiting for gps signal. + +0.17: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 97d18f5fe..f0581d578 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.16", + "version": "0.17", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index af9cbef22..d43a557f6 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -19,8 +19,7 @@ "< Back": () => back(), "keep gps alive": { value: !!settings.keep_gps_alive, // !! converts undefined to false - format: (v) => (v ? "Yes" : "No"), - onchange: (v) => { + onchange: v => { settings.keep_gps_alive = v; writeSettings(); }, diff --git a/apps/grocery/metadata.json b/apps/grocery/metadata.json index ef073a1b2..26f67f3b0 100644 --- a/apps/grocery/metadata.json +++ b/apps/grocery/metadata.json @@ -5,7 +5,7 @@ "description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.", "icon": "grocery.png", "type": "app", - "tags": "tool,outdoors,shopping,list", + "tags": "tool,shopping,list", "supports": ["BANGLEJS", "BANGLEJS2"], "custom": "grocery.html", "interface": "interface.html", diff --git a/apps/happyclk/ChangeLog b/apps/happyclk/ChangeLog new file mode 100644 index 000000000..e87114779 --- /dev/null +++ b/apps/happyclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Added settings to show/hide widgets and settings for different styles. \ No newline at end of file diff --git a/apps/happyclk/README.md b/apps/happyclk/README.md new file mode 100644 index 000000000..d5b8752fb --- /dev/null +++ b/apps/happyclk/README.md @@ -0,0 +1,25 @@ +# Happy Clock + +A happy clock. + +![](screenshot_1.png) + +## How to read this happy clock? +- The left eye shows the hour, the right hour the minutes. +- The happiness decreases as the battery level decreases. +- The left mouthline shows whether your bangle is locked or not +- The right mouthline whether you reached 10k steps or not. + +Here you can see an example of a locked bangle with a low battery: + +![](screenshot_3.png) + +## Settings +- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). +- Theme: Select your custom theme, independent of system settings. + +## Creator +- [David Peer](https://github.com/peerdavid). + +Thanks for this great idea: +http://apps.rebble.io/de_DE/application/55014a037ed24ae745000004?section=watchfaces diff --git a/apps/happyclk/happyclk.app.js b/apps/happyclk/happyclk.app.js new file mode 100644 index 000000000..8e22b6e1e --- /dev/null +++ b/apps/happyclk/happyclk.app.js @@ -0,0 +1,284 @@ +/************************************************ + * Happy Clock + */ + + +const storage = require('Storage'); +const widget_utils = require("widget_utils"); + + +/************************************************ + * Settings + */ +const SETTINGS_FILE = "happyclk.setting.json"; + +let settings = { + color: "Dark", + screen: "Dynamic" +}; + +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key]; +} + +var color_map = { + "Dark":{ + fg: "#fff", + bg: "#000", + eye: "#fff", + eyePupils: "#000" + }, + "Black":{ + fg: "#fff", + bg: "#000", + eye: "#000", + eyePupils: "#fff" + }, + "White":{ + fg: "#000", + bg: "#fff", + eye: "#fff", + eyePupils: "#000" + }, + "Blue":{ + fg: "#fff", + bg: "#00f", + eye: "#fff", + eyePupils: "#000" + }, + "Green":{ + fg: "#000", + bg: "#0f0", + eye: "#fff", + eyePupils: "#000" + }, + "Red":{ + fg: "#fff", + bg: "#f00", + eye: "#fff", + eyePupils: "#000" + }, + "Purple":{ + fg: "#fff", + bg: "#f0f", + eye: "#fff", + eyePupils: "#000" + }, + "Yellow":{ + fg: "#000", + bg: "#ff0", + eye: "#fff", + eyePupils: "#000" + } +}; +var colors = color_map[settings.color]; + +/************************************************ + * Globals + */ +var W = g.getWidth(),R=W/2; +var H = g.getHeight(); +var drawTimeout; + + +/* + * HELPER + */ + +let isFullscreen = function() { + var s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked(); + } else { + return s == "full"; + } + }; + + +// Based on the great multi clock from https://github.com/jeffmer/BangleApps/ +Graphics.prototype.drawPupils = function(cx, cy, r1, dx, dy, angle) { + angle = angle % 360; + var theta=angle*Math.PI/180; + var x = parseInt(cx+r1*Math.sin(theta)*1.2); + var y = parseInt(cy-r1*Math.cos(theta)*1.2); + + g.setColor(g.theme.fg); + g.fillCircle(cx, cy, 32); + + g.setColor(colors.eye); + g.fillCircle(cx, cy, 27); + g.fillCircle(cx+dx, cy+dy, 28); + + g.setColor(colors.eyePupils); + g.fillCircle(x, y, 8); + g.fillCircle(x+1, y, 8); +}; + +let quadraticCurve = function(t, p0x, p0y, p1x, p1y, p2x, p2y){ + var t2 = t * t; + var oneMinT = 1 - t; + var oneMinT2 = oneMinT * oneMinT; + return { + x: p0x * oneMinT2 + 2 * p1x * t * oneMinT + p2x *t2, + y: p0y * oneMinT2 + 2 * p1y * t * oneMinT + p2y *t2 + }; +} + +// Thanks to user stephaneAG from the Espruino forum! +// https://forum.espruino.com/conversations/330154/#comment14593349 +let drawCurve = function(x1, y1, x2, y2, x3, y3){ + var p0 = { x: x1, y: y1}; + var p1 = { x: x2, y: y2}; + var p2 = { x: x3, y: y3}; + var time = 0; + var stepping = 0.1; // Stepping defines the speed. + + for(var y = 0; y < 8; y++){ + var pathPts = []; + for(time = 0; time <= 1; time+= stepping){ + var pos = quadraticCurve(time, p0.x, p0.y, p1.x, p1.y, p2.x, p2.y); + pathPts.push(pos.x, pos.y+y); + } + g.drawPoly(pathPts, false); + } + g.flip(); +} + + +/* + * Draw the clock + */ +let drawEyes = function(){ + // And now the analog time + var drawHour = g.drawPupils.bind(g,55,70,12,1,0); + var drawMinute = g.drawPupils.bind(g,125,70,12,0,1); + + g.setFontAlign(0,0); + + // Compute angles + var date = new Date(); + var m = parseInt(date.getMinutes() * 360 / 60); + var h = date.getHours(); + h = h > 12 ? h-12 : h; + h += date.getMinutes()/60.0; + h = parseInt(h*360/12); + + // Draw minute and hour fg + g.setColor(g.theme.fg); + drawHour(h); + drawMinute(m); +} + + +let drawSmile = function(isLocked){ + g.setColor(colors.fg); + var y = 120; + var o = parseInt(E.getBattery()*0.8); + + // Draw smile + drawCurve(30, y, W/2+12, y+o, W-40, y); + + // And the two "mouth lines" + var reachedSteps = Bangle.getHealthStatus("day").steps >= 10000; + for(var i=0; i < 6; i++){ + if(isLocked) g.drawLine(25, y+6+i, 35, y-5+i); + if(reachedSteps) g.drawLine(W-35, y+5+i, W-45, y-5+i); + } +} + +let drawEyeBrow = function(){ + if(!isFullscreen()) return; + + g.setColor(colors.fg); + var w = 6; + for(var i = 0; i < w; i++){ + g.drawLine(25, 25+i, 70, 15+i%3); + g.drawLine(W-25, 28+i%3, W-68, 19+i); + } +} + + +let drawWidgets = function(){ + if (isFullscreen()) { + widget_utils.hide(); + } else { + Bangle.drawWidgets(); + } +} + + + +let draw = function(){ + // Queue draw in one minute + queueDraw(); + + var isLocked = Bangle.isLocked(); + drawHelper(isLocked); +} + +let drawHelper = function(isLocked){ + g.setColor(g.theme.bg); + + g.fillRect(0, isFullscreen() ? 0 : 24, W, H); + g.setColor(g.theme.fg); + + drawEyes(); + drawEyeBrow(); + drawSmile(isLocked); + + drawWidgets(); +} + + +/* + * Listeners + */ +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('lock', function(isLocked) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ + // If we have to show the widgets again, we load it from our + // cache and not through Bangle.loadWidgets as its much faster! + widget_utils.show(); + } + + draw(isLocked); +}); + + +/* + * Some helpers + */ +let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +/* + * Lets start widgets, listen for btn etc. + */ +// Show launcher when middle button pressed +Bangle.setUI("clock"); +Bangle.loadWidgets(); + +// Clear the screen once, at startup and draw clock +g.setTheme({bg:colors.bg,fg:colors.fg,dark:false}); +draw(); + +// After drawing the watch face, we can draw the widgets +// Bangle.drawWidgets(); diff --git a/apps/happyclk/happyclk.icon.js b/apps/happyclk/happyclk.icon.js new file mode 100644 index 000000000..d59fc0668 --- /dev/null +++ b/apps/happyclk/happyclk.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/ADEP/AEC+E//kH//+gYIB8F//1/B4U/ERgdB/wdB//AFIJGCx/n+P8EIM/+fnE4IBB/PAv4aBv/84E/z/8//8wAFDwwFB74FBgQFD/wFGyF/AoUAz//z/+AoPfAoV/gPP/+/IIP585lCj/z8ZvCw+H/HwPQUf/iACACIrBAAaRCGAP+AoXzAonxAoJRB//lAQJLBC4X/44IE8KeCVoQCBj4CB/iYBEwX+h6sCAAOB8BCD4C+CDwTKCACI=")) \ No newline at end of file diff --git a/apps/happyclk/happyclk.png b/apps/happyclk/happyclk.png new file mode 100644 index 000000000..53fbe152e Binary files /dev/null and b/apps/happyclk/happyclk.png differ diff --git a/apps/happyclk/happyclk.settings.js b/apps/happyclk/happyclk.settings.js new file mode 100644 index 000000000..dd9f2f675 --- /dev/null +++ b/apps/happyclk/happyclk.settings.js @@ -0,0 +1,43 @@ +(function(back) { + const SETTINGS_FILE = "happyclk.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + color: "Dark", + screen: "Dynamic" + }; + 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) + } + + var colorOptions = ["Dark", "Black", "White", "Blue", "Green", "Red", "Purple", "Yellow"]; + var screenOptions = ["Normal", "Dynamic", "Full"]; + E.showMenu({ + '': { 'title': 'Happy Clock' }, + '< Back': back, + 'Screen': { + value: 0 | screenOptions.indexOf(settings.screen), + min: 0, max: screenOptions.length-1, + format: v => screenOptions[v], + onchange: v => { + settings.screen = screenOptions[v]; + save(); + }, + }, + 'Theme': { + value: 0 | colorOptions.indexOf(settings.color), + min: 0, max: colorOptions.length-1, + format: v => colorOptions[v], + onchange: v => { + settings.color = colorOptions[v]; + save(); + }, + }, + }); +}) diff --git a/apps/happyclk/metadata.json b/apps/happyclk/metadata.json new file mode 100644 index 000000000..e5129b0f0 --- /dev/null +++ b/apps/happyclk/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "happyclk", + "name": "Happy Clock", + "shortName":"Happy Clock", + "icon": "happyclk.png", + "version":"0.02", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "description": "A happy clock :)", + "type": "clock", + "tags": "clock", + "screenshots": [ + {"url":"screenshot_1.png"}, + {"url":"screenshot_2.png"}, + {"url":"screenshot_3.png"}, + {"url":"screenshot_4.png"}, + {"url":"screenshot_5.png"} + ], + "storage": [ + {"name":"happyclk.app.js","url":"happyclk.app.js"}, + {"name":"happyclk.img","url":"happyclk.icon.js","evaluate":true}, + {"name":"happyclk.settings.js","url":"happyclk.settings.js"} + ] +} diff --git a/apps/happyclk/screenshot_1.png b/apps/happyclk/screenshot_1.png new file mode 100644 index 000000000..20bf2d294 Binary files /dev/null and b/apps/happyclk/screenshot_1.png differ diff --git a/apps/happyclk/screenshot_2.png b/apps/happyclk/screenshot_2.png new file mode 100644 index 000000000..49c09ee31 Binary files /dev/null and b/apps/happyclk/screenshot_2.png differ diff --git a/apps/happyclk/screenshot_3.png b/apps/happyclk/screenshot_3.png new file mode 100644 index 000000000..404bd7918 Binary files /dev/null and b/apps/happyclk/screenshot_3.png differ diff --git a/apps/happyclk/screenshot_4.png b/apps/happyclk/screenshot_4.png new file mode 100644 index 000000000..b84e41ab1 Binary files /dev/null and b/apps/happyclk/screenshot_4.png differ diff --git a/apps/happyclk/screenshot_5.png b/apps/happyclk/screenshot_5.png new file mode 100644 index 000000000..401bb0c51 Binary files /dev/null and b/apps/happyclk/screenshot_5.png differ diff --git a/apps/hasensors/ChangeLog b/apps/hasensors/ChangeLog new file mode 100644 index 000000000..759f68777 --- /dev/null +++ b/apps/hasensors/ChangeLog @@ -0,0 +1 @@ +0.01: New app! \ No newline at end of file diff --git a/apps/hasensors/README.md b/apps/hasensors/README.md new file mode 100644 index 000000000..e7f6ca98d --- /dev/null +++ b/apps/hasensors/README.md @@ -0,0 +1,24 @@ +# Home Assistant Sensors + +Sends sensor values to [Home Assistant](https://www.home-assistant.io/) using +the [Android Integration](/?id=android). +It doesn't use the Home Assistant app on your phone, but posts directly to +Home Assistant, so it only works if Home Assistant can be reached from your phone. + +## Setup + +You need to fill out these fields: + +* *Sensor ID*: This is prefixed to sensor IDs in Home Assistant. + If you set this to `banglejs`, the battery sensor will be named `sensor.banglejs_battery_level`. +* *Sensor Name*: This is prefixed to human-friendly sensor names. + If you set this to `Bangle.js`, the battery sensor will show as `Bangle.js Battery Level`. +* *Home Assistant URL*: The URL of your Home Assistant Installation. +* *Authentication Token*: You need to generate a Long-Lived Access Token in + Home Assistant, at the bottom of [your profile page](https://my.home-assistant.io/redirect/profile/). + +## Features + +Currently creates these sensors: +* `_battery_level`: Your watch battery level as percentage +* `_battery_state`: `charging` or `discharging` \ No newline at end of file diff --git a/apps/hasensors/boot.js b/apps/hasensors/boot.js new file mode 100644 index 000000000..a9122be5d --- /dev/null +++ b/apps/hasensors/boot.js @@ -0,0 +1,6 @@ +(function () { + const sb = () => require('hasensors').sendBattery(); + Bangle.on("charging", sb); + NRF.on("connect", () => setTimeout(sb, 2000)); + setInterval(sb, 10 * 60 * 1000); +})(); \ No newline at end of file diff --git a/apps/hasensors/custom.html b/apps/hasensors/custom.html new file mode 100644 index 000000000..805001701 --- /dev/null +++ b/apps/hasensors/custom.html @@ -0,0 +1,89 @@ + + + + Home Assistant Sensors configuration + + + +
+ + + + +
+

+ +

+ + + + \ No newline at end of file diff --git a/apps/hasensors/ha.png b/apps/hasensors/ha.png new file mode 100644 index 000000000..8fce958e4 Binary files /dev/null and b/apps/hasensors/ha.png differ diff --git a/apps/hasensors/lib.js b/apps/hasensors/lib.js new file mode 100644 index 000000000..60cfb6da4 --- /dev/null +++ b/apps/hasensors/lib.js @@ -0,0 +1,35 @@ +// split out into a separate file to keep bootcode short. +function s(key) { + return (require('Storage').readJSON('hasensors.settings.js', true) || {})[key]; +} + +function post(sensor, data) { + const url = s('url') + '/api/states/sensor.' + s('id') + '_' + sensor; + Bangle.http(url, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + s('token'), + } + }); +} + +exports.sendBattery = function () { + if (!NRF.getSecurityStatus().connected) return; + post('battery_level', { + state: E.getBattery(), + attributes: { + friendly_name: s('name') + " Battery Level", + unit_of_measurement: "%", + device_class: "battery", + state_class: "measurement", + } + }); + post('battery_state', { + state: Bangle.isCharging() ? 'charging' : 'discharging', + attributes: { + friendly_name: s('name') + " Battery State", + } + }); +} \ No newline at end of file diff --git a/apps/hasensors/metadata.json b/apps/hasensors/metadata.json new file mode 100644 index 000000000..7713fadc7 --- /dev/null +++ b/apps/hasensors/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "hasensors", + "name": "Home Assistant Sensors", + "shortName": "HA sensors", + "version": "0.01", + "description": "Send sensor values to Home Assistant using the Android Integration.", + "icon": "ha.png", + "type": "bootloader", + "tags": "tool,sensors", + "dependencies": {"android":"app"}, + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"hasensors","url":"lib.js"}, + {"name":"hasensors.boot.js","url":"boot.js"} + ], + "data": [ + {"name":"hasensors.settings.json"} + ] +} diff --git a/apps/homework/app.js b/apps/homework/app.js index 3d9be31c9..a6ad331f0 100644 --- a/apps/homework/app.js +++ b/apps/homework/app.js @@ -46,8 +46,8 @@ var mainMenu = { "Reset Homework": function() { E.showPrompt("Are you sure you want to delete homework.txt?", { buttons: { - "No": false, - "Yes": true + /*LANG*/"No": false, + /*LANG*/"Yes": true } }).then(function(v) { if (v) { diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 29b0540c2..981b81079 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -9,6 +9,7 @@ "readme": "README.md", "description": "Simple and slim battery widget with charge status and percentage", "tags": "widget,battery", + "provides_widgets" : ["battery"], "storage": [ {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"} ] diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog index 0c33a4871..8bad496bf 100644 --- a/apps/iconlaunch/ChangeLog +++ b/apps/iconlaunch/ChangeLog @@ -20,3 +20,5 @@ still be loaded when they weren't supposed to. 0.15: Ensure that we hide widgets if in fullscreen mode (So that widgets are still hidden if launcher is fast-loaded) +0.16: Use firmware provided E.showScroller method +0.17: fix fullscreen with oneClickExit diff --git a/apps/iconlaunch/README.md b/apps/iconlaunch/README.md index 0d36fdeb4..49d01d3fd 100644 --- a/apps/iconlaunch/README.md +++ b/apps/iconlaunch/README.md @@ -6,7 +6,3 @@ This launcher shows 9 apps per screen, making it much faster to navigate versus ![A screenshot](screenshot1.png) ![Another screenshot](screenshot2.png) - -## Technical note - -The app uses `E.showScroller`'s code in the app but not the function itself because `E.showScroller` doesn't report the position of a press to the select function. diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index acf695ddb..9f8cedb0f 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -9,6 +9,7 @@ timeOut:"Off" }, s.readJSON("iconlaunch.json", true) || {}); + if (!settings.fullscreen) { Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -32,13 +33,14 @@ }) }; s.writeJSON("iconlaunch.cache.json", launchCache); } - let scroll = 0; + let selectedItem = -1; const R = Bangle.appRect; const iconSize = 48; const appsN = Math.floor(R.w / iconSize); const whitespace = (R.w - appsN * iconSize) / (appsN + 1); const itemSize = iconSize + whitespace; + let drawItem = function(itemI, r) { g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); let x = 0; @@ -61,36 +63,26 @@ } x += iconSize; } - drawText(itemI); + drawText(itemI, r.y); }; - let drawItemAuto = function(i) { - let y = idxToY(i); - g.reset().setClipRect(R.x, y, R.x2, y + itemSize); - drawItem(i, { - x: R.x, - y: y, - w: R.w, - h: itemSize - }); - g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); - }; - let lastIsDown = false; - let drawText = function(i) { + + let drawText = function(i, appY) { const selectedApp = launchCache.apps[selectedItem]; const idy = (selectedItem - (selectedItem % 3)) / 3; if (!selectedApp || i != idy) return; - const appY = idxToY(idy) + iconSize / 2; + appY = appY + itemSize/2; g.setFontAlign(0, 0, 0); g.setFont("12x20"); const rect = g.stringMetrics(selectedApp.name); g.clearRect( - R.w / 2 - rect.width / 2, - appY - rect.height / 2, - R.w / 2 + rect.width / 2, - appY + rect.height / 2 + R.w / 2 - rect.width / 2 - 2, + appY - rect.height / 2 - 2, + R.w / 2 + rect.width / 2 + 1, + appY + rect.height / 2 + 1 ); g.drawString(selectedApp.name, R.w / 2, appY); }; + let selectItem = function(id, e) { const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1); const appId = id * appsN + iconN; @@ -108,95 +100,46 @@ } } selectedItem = appId; - drawItems(); + if (scroller) scroller.draw(); }; - let idxToY = function(i) { - return i * itemSize + R.y - (scroll & ~1); - }; - let YtoIdx = function(y) { - return Math.floor((y + (scroll & ~1) - R.y) / itemSize); - }; - let drawItems = function() { - g.reset().clearRect(R.x, R.y, R.x2, R.y2); - g.setClipRect(R.x, R.y, R.x2, R.y2); - let a = YtoIdx(R.y); - let b = Math.min(YtoIdx(R.y2), 99); - for (let i = a; i <= b; i++) - drawItem(i, { - x: R.x, - y: idxToY(i), - w: R.w, - h: itemSize, - }); - g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); - }; - drawItems(); - g.flip(); const itemsN = Math.ceil(launchCache.apps.length / appsN); - let onDrag = function(e) { - updateTimeout(); - g.setColor(g.theme.fg); - g.setBgColor(g.theme.bg); - let dy = e.dy; - if (scroll + R.h - dy > itemsN * itemSize) { - dy = scroll + R.h - itemsN * itemSize; - } - if (scroll - dy < 0) { - dy = scroll; - } - scroll -= dy; - scroll = E.clip(scroll, 0, itemSize * (itemsN - 1)); - g.setClipRect(R.x, R.y, R.x2, R.y2); - g.scroll(0, dy); - if (dy < 0) { - g.setClipRect(R.x, R.y2 - (1 - dy), R.x2, R.y2); - let i = YtoIdx(R.y2 - (1 - dy)); - let y = idxToY(i); - while (y < R.y2) { - drawItem(i, { - x: R.x, - y: y, - w: R.w, - h: itemSize, - }); - i++; - y += itemSize; - } - } else { - g.setClipRect(R.x, R.y, R.x2, R.y + dy); - let i = YtoIdx(R.y + dy); - let y = idxToY(i); - while (y > R.y - itemSize) { - drawItem(i, { - x: R.x, - y: y, - w: R.w, - h: itemSize, - }); - y -= itemSize; - i--; - } - } - g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); - }; - let mode = { - mode: "custom", - drag: onDrag, - touch: (_, e) => { - if (e.y < R.y - 4) return; - updateTimeout(); - let i = YtoIdx(e.y); - selectItem(i, e); - }, - swipe: (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } }, - btn: _=> { if (settings.oneClickExit) Bangle.showClock(); }, + + let idWatch = null; + let options = { + h: itemSize, + c: itemsN, + draw: drawItem, + select: selectItem, remove: function() { if (timeout) clearTimeout(timeout); + Bangle.removeListener("drag", updateTimeout); + Bangle.removeListener("touch", updateTimeout); + Bangle.removeListener("swipe", swipeHandler); if (settings.fullscreen) { // for fast-load, if we hid widgets then we should show them again require("widget_utils").show(); } - } + if(idWatch) clearWatch(idWatch); + }, + btn:Bangle.showClock }; + + //work both the fullscreen and the oneClickExit + if( settings.fullscreen && settings.oneClickExit) + { + idWatch=setWatch(function(e) { + Bangle.showClock(); + }, BTN, {repeat:false, edge:'rising' }); + + } + else if( settings.oneClickExit ) + { + options.back=Bangle.showClock; + } + + + + + let scroller = E.showScroller(options); let timeout; const updateTimeout = function(){ @@ -207,7 +150,11 @@ } }; - updateTimeout(); + let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } }; + + Bangle.on("swipe", swipeHandler) + Bangle.on("drag", updateTimeout); + Bangle.on("touch", updateTimeout); - Bangle.setUI(mode); -} + updateTimeout(); +} \ No newline at end of file diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index 27f6386d3..35a7907bd 100644 --- a/apps/iconlaunch/metadata.json +++ b/apps/iconlaunch/metadata.json @@ -2,7 +2,7 @@ "id": "iconlaunch", "name": "Icon Launcher", "shortName" : "Icon launcher", - "version": "0.15", + "version": "0.17", "icon": "app.png", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "tags": "tool,system,launcher", diff --git a/apps/info/info.app.js b/apps/info/info.app.js index ade3f3ebb..d97f780da 100644 --- a/apps/info/info.app.js +++ b/apps/info/info.app.js @@ -21,11 +21,11 @@ var screens = [ name: "Hardware", items: [ {name: "Battery", fun: () => E.getBattery() + "%"}, - {name: "Charge?", fun: () => Bangle.isCharging() ? "Yes" : "No"}, + {name: "Charge?", fun: () => Bangle.isCharging() ? /*LANG*/"Yes" : /*LANG*/"No"}, {name: "TempInt.", fun: () => locale.temp(parseInt(E.getTemperature()))}, - {name: "Bluetooth", fun: () => NRF.getSecurityStatus().connected ? "Conn" : "NoConn"}, - {name: "GPS", fun: () => Bangle.isGPSOn() ? "On" : "Off"}, - {name: "Compass", fun: () => Bangle.isCompassOn() ? "On" : "Off"}, + {name: "Bluetooth", fun: () => NRF.getSecurityStatus().connected ? /*LANG*/"Conn" : /*LANG*/"NoConn"}, + {name: "GPS", fun: () => Bangle.isGPSOn() ? /*LANG*/"On" : /*LANG*/"Off"}, + {name: "Compass", fun: () => Bangle.isCompassOn() ? /*LANG*/"On" : /*LANG*/"Off"}, ] }, { @@ -160,4 +160,4 @@ Bangle.on('lock', function(isLocked) { }); Bangle.loadWidgets(); -Bangle.drawWidgets(); \ No newline at end of file +Bangle.drawWidgets(); diff --git a/apps/inspire/ChangeLog b/apps/inspire/ChangeLog index 267ee22c2..e8e154f2f 100644 --- a/apps/inspire/ChangeLog +++ b/apps/inspire/ChangeLog @@ -1 +1,2 @@ 0.01: First public version +0.02: Disable screen lock when breathing diff --git a/apps/inspire/README.md b/apps/inspire/README.md index f5f8e3aa9..43df1b760 100644 --- a/apps/inspire/README.md +++ b/apps/inspire/README.md @@ -2,7 +2,7 @@ A minimalistic app that will help you practive breathing. -Author: Written by pancake in 2022, powered by insomnia +Author: Written by pancake in 2022, updated in 2023, powered by insomnia ## Features @@ -10,6 +10,7 @@ Author: Written by pancake in 2022, powered by insomnia * [x] Tap to start * [x] Subtle vibrations * [x] Drag to pause breathing +* [x] Dont lock screen while breathing * [ ] Automatic buzz every hour during day ## Screenshots diff --git a/apps/inspire/app.js b/apps/inspire/app.js index 92b2c4ef2..818a6af76 100644 --- a/apps/inspire/app.js +++ b/apps/inspire/app.js @@ -14,6 +14,7 @@ var mode = 0; var sin = 0; var dragged = 0; var lastTime = Date.now(); + function breath(t) { var r = Math.abs(Math.sin(t / 100)) * w2; g.fillCircle(w/2,h/2, r); @@ -26,7 +27,7 @@ setTimeout(()=>{Bangle.buzz(60);}, 500); function showTouchScreen() { g.setColor(1,1,1); - g.fillCircle (w2, h2, h2-5); + g.fillCircle(w2, h2, h2-5); g.setColor(0,0,0); g.setFont("Vector", 32); g.drawString("Tap to", w/6, h2-fs); @@ -40,7 +41,7 @@ g.clear(); function animateCircle() { g.clear(); g.setColor(1,1,1); - g.fillCircle (w2, h2, radius); + g.fillCircle(w2, h2, radius); radius-=2; if (radius < 40) { breathing = true; @@ -68,6 +69,9 @@ function main() { return; } started = true; + Bangle.setLCDPower(1); + Bangle.setLocked(0); + Bangle.setLCDTimeout(0); animateCircle(); Bangle.buzz(40); } @@ -78,48 +82,48 @@ function main() { main(); function startBreathing() { -var cicles = 3; -g.setFont("Vector", fs); - -var interval = setInterval(function() { -if (lastTime + 10 > Date.now()) { - return; -} - lastTime = Date.now(); - g.setColor(0, 0, 0); - g.clear(); - - g.setColor(0, 0.5, 1); - var b = breath(count); - g.setColor(0.5, 0.5, 1); - var c = breath(count + 50); - count++; - g.setColor(1, 1, 1); - if (b < c) { - g.drawString("inspire",8,ty); - if (mode) { - mode = 0; - Bangle.buzz(20); - if (!dragged ) { - cicles--; + var cicles = 3; + g.setFont("Vector", fs); + + function breathTime() { + if (lastTime + 10 > Date.now()) { + return; + } + lastTime = Date.now(); + g.setColor(0, 0, 0); + g.clear(); + + g.setColor(0, 0.5, 1); + var b = breath(count); + g.setColor(0.5, 0.5, 1); + var c = breath(count + 50); + count++; + g.setColor(1, 1, 1); + if (b < c) { + g.drawString("inspire",8,ty); + if (mode) { + mode = 0; + Bangle.buzz(20); + if (!dragged ) { + cicles--; + } + } + } else { + g.drawString("expire",8,ty); + if (!mode) { + mode = 1; + Bangle.buzz(20); } } - } else { - g.drawString("expire",8,ty); - if (!mode) { - mode = 1; - Bangle.buzz(20); + g.drawString(cicles, w-fs, ty); + if (cicles < 1) { + clearInterval(interval); + g.clear(); + g.drawString("Thanks for",20,h/3); + g.drawString(" breathing!",20,(h/3) + 16); + Bangle.showClock(); } - } - g.drawString(cicles, w-fs, ty); - if (cicles < 1) { - clearInterval(interval); - g.clear(); - g.drawString("Thanks for",20,h/3); - g.drawString(" breathing!",20,(h/3) + 16); - Bangle.showClock(); - } dragged = 0; - -}, 4); + } + var interval = setInterval(breathTime, 4); } diff --git a/apps/inspire/metadata.json b/apps/inspire/metadata.json index 85b7365be..3ffbc40e7 100644 --- a/apps/inspire/metadata.json +++ b/apps/inspire/metadata.json @@ -2,7 +2,7 @@ "id": "inspire", "name": "Inspire Breathing", "shortName": "Inspire", - "version": "0.01", + "version": "0.02", "description": "exercise breathing every now and then", "icon": "app-icon.png", "tags": "tools,health", diff --git a/apps/kbtouch/ChangeLog b/apps/kbtouch/ChangeLog index 17e824c00..5bd2159e6 100644 --- a/apps/kbtouch/ChangeLog +++ b/apps/kbtouch/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Introduced settings to customize the layout and functionality of the keyboard. +0.03: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json index 89d121d63..8d7434e44 100644 --- a/apps/kbtouch/metadata.json +++ b/apps/kbtouch/metadata.json @@ -1,6 +1,6 @@ { "id": "kbtouch", "name": "Touch keyboard", - "version":"0.02", + "version":"0.03", "description": "A library for text input via onscreen keyboard", "icon": "app.png", "type":"textinput", diff --git a/apps/kbtouch/settings.js b/apps/kbtouch/settings.js index 871cc5d32..227efaf2d 100644 --- a/apps/kbtouch/settings.js +++ b/apps/kbtouch/settings.js @@ -8,13 +8,13 @@ if (settings.speedScaling===undefined) settings.speedScaling=24; return settings; } - + function updateSetting(setting, value) { let settings = require('Storage').readJSON("kbtouch.settings.json", true) || {}; settings[setting] = value; require('Storage').writeJSON("kbtouch.settings.json", settings); } - + var mainmenu = { "" : { "title" : /*LANG*/"Touch Keyboard" }, "< Back" : back, @@ -25,22 +25,16 @@ onchange: v => updateSetting("textSize", v) }, /*LANG*/'Offset keyboard': { - value: settings().offsetKeyboard, - min: 0, max: 1, - format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], - onchange: v => updateSetting("offsetKeyboard", v) + value: !!settings().offsetKeyboard, + onchange: v => updateSetting("offsetKeyboard", v?1:0) }, /*LANG*/'Loop around': { - value: settings().loopAround, - min: 0, max: 1, - format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], - onchange: v => updateSetting("loopAround", v) + value: !!settings().loopAround, + onchange: v => updateSetting("loopAround", v?1:0) }, /*LANG*/'One-to-one input and release to select': { - value: settings().oneToOne, - min: 0, max: 1, - format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], - onchange: v => updateSetting("oneToOne", v) + value: !!settings().oneToOne, + onchange: v => updateSetting("oneToOne", v?1:0) }, /*LANG*/'Speed scaling': { value: settings().speedScaling, @@ -49,10 +43,8 @@ onchange: v => updateSetting("speedScaling", v) } ///*LANG*/'Release to select': { - // value: 1|settings().fontSize, - // min: 0, max: 1, - // format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], - // onchange: v => updateSetting("releaseToSelect", v) + // value: !!(1|settings().fontSize), + // onchange: v => updateSetting("releaseToSelect", v?1:0) //} }; E.showMenu(mainmenu); diff --git a/apps/lato/ChangeLog b/apps/lato/ChangeLog new file mode 100644 index 000000000..7e6c3b0d5 --- /dev/null +++ b/apps/lato/ChangeLog @@ -0,0 +1,2 @@ +0.01: first release +0.02: Use clock_info module as an app diff --git a/apps/lato/metadata.json b/apps/lato/metadata.json index 0b5e4a0f3..e4def2df9 100644 --- a/apps/lato/metadata.json +++ b/apps/lato/metadata.json @@ -1,7 +1,7 @@ { "id": "lato", "name": "Lato", - "version": "0.01", + "version": "0.02", "description": "A Lato Font clock with fast load and clock_info", "readme": "README.md", "icon": "app.png", @@ -9,6 +9,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "storage": [ {"name":"lato.app.js","url":"app.js"}, {"name":"lato.img","url":"icon.js","evaluate":true} diff --git a/apps/lcdclock/ChangeLog b/apps/lcdclock/ChangeLog index 5560f00bc..220369925 100644 --- a/apps/lcdclock/ChangeLog +++ b/apps/lcdclock/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Use clock_info module as an app diff --git a/apps/lcdclock/metadata.json b/apps/lcdclock/metadata.json index d7d09b106..e286dc017 100644 --- a/apps/lcdclock/metadata.json +++ b/apps/lcdclock/metadata.json @@ -1,12 +1,13 @@ { "id": "lcdclock", "name": "LCD Clock", - "version":"0.01", + "version":"0.02", "description": "A Casio-style clock, with ClockInfo areas at the top and bottom. Tap them and swipe up/down to toggle between different information", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", "tags": "clock,clkinfo", "supports" : ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "storage": [ {"name":"lcdclock.app.js","url":"app.js"}, {"name":"lcdclock.img","url":"app-icon.js","evaluate":true} diff --git a/apps/legoremote/ChangeLog b/apps/legoremote/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/legoremote/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/legoremote/README.md b/apps/legoremote/README.md new file mode 100644 index 000000000..a95b7b154 --- /dev/null +++ b/apps/legoremote/README.md @@ -0,0 +1,28 @@ +# LEGO Remote control + +This app allows you to control LEGO models from Bangle.js + +Right now the only supported control device is the Mould King M-0006 +Bluetooth remote for LEGO Power Functions: http://www.espruino.com/LEGO+Power+Functions+Clone + +LEGO Power Functions does not have an official Bluetooth remote controller. Hopefully +in the future this app will be able to support other types of remote (see below). + +## Usage + +Run the app, and ensure you're not connected to your watch via Bluetooth +(a warning will pop up if so). + +Now press the arrow keys on the screen to control the robot. + +It is expected that the robot is controlled by two motors, one on the left +side (connected to the `A` output) and one on the right (connected to the `B` output). + +## Future additions + +In the future it would be great to add: + +* Recording a series of movements and playing them back +* Support for official LEGO bluetooth remotes (via [Pybricks](https://pybricks.com/)) +* Support for different robot styles and configurations +* Using the Bangle's compass (or even GPS) to allow better robot control. diff --git a/apps/legoremote/app-icon.js b/apps/legoremote/app-icon.js new file mode 100644 index 000000000..850e0eda6 --- /dev/null +++ b/apps/legoremote/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/8H5A4NH/4ABJf4A/AFMC1Uq1QABhWqBYYJDAAegBYWqxWogWohQBCBYWpqsVqoABytVBYUC0u5rf5rf1q/xtQLBhWW/t//2/AYVKBYWX/u//z3B3//rRGCr/1/+H/v9/1/BYn2/lT+v6/oXDlF/4//9/78/5F4epv/X//f7/3+v9I4Wp38b/9v7//+4LD0P/HgN/7f/EgMoBYOlJ4IACDAP1O4QLH6ibCBYI7Br/+qf/iwLC1NIquhq2lquprWAWQVVoNVgtU0NVlQLCZQ7XDbgLWJEgOCdgLZBdwgA/AH4AaA")) diff --git a/apps/legoremote/app.js b/apps/legoremote/app.js new file mode 100644 index 000000000..1c76a54a8 --- /dev/null +++ b/apps/legoremote/app.js @@ -0,0 +1,70 @@ +var lego = require("mouldking"); +lego.start(); +E.on('kill', () => { + // return to normal Bluetooth advertising + NRF.setAdvertising({},{showName:true}); +}); +// You must leave one second after 'start' to allow the remote to be paired + +var arrowIcon = atob("IiiBAAAAwAAAAPwAAAB/gAAAP/AAAB/+AAAP/8AAB//4AAP//wAA///gAH///AA///8AH///4A////wH////gf////D////8f////5/////n////+f////4AP/8AAA//wAAD//AAAP/8AAA//wAAD//AAAH/8AAAf/wAAB//AAAH/8AAAf/gAAB/+AAAH/4AAAf/gAAB/+AAAH/4AAAP/gAAA/+AAAD/wAAAD8AA"); +var controlState = ""; + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +var R = Bangle.appRect; +// we'll divide up into 3x3 +function getBoxCoords(x,y) { + return { + x : R.x + R.w*x/3, + y : R.y + R.h*y/3 + }; +} + +function draw() { + g.reset().clearRect(R); + var c, ninety = Math.PI/2; + var colOn = "#f00", colOff = g.theme.fg; + c = getBoxCoords(1.5, 0.5); + g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0}); + c = getBoxCoords(2.5, 1.5); + g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety}); + c = getBoxCoords(0.5, 1.5); + g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety}); + c = getBoxCoords(1.5, 1.5); + g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2}); + if (NRF.getSecurityStatus().connected) { + c = getBoxCoords(1.5, 2.5); + g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y); + } +} +draw(); +NRF.on('connect', draw); +NRF.on('disconnect', draw); + +function setControlState(s) { + controlState = s; + var c = {}; + var speed = 3; + if (s=="up") c={a:-speed,b:-speed}; + if (s=="down") c={a:speed,b:speed}; + if (s=="left") c={a:speed,b:-speed}; + if (s=="right") c={a:-speed,b:speed}; + draw(); + lego.set(c); +} + +Bangle.on('drag',e => { + var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99)); + var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99)); + if (!e.b) { + setControlState(""); + return; + } + if (y==0) { // top row + if (x==1) setControlState("up"); + } else if (y==1) { + if (x==0) setControlState("left"); + if (x==1) setControlState("down"); + if (x==2) setControlState("right"); + } +}); diff --git a/apps/legoremote/app.png b/apps/legoremote/app.png new file mode 100644 index 000000000..727049e2b Binary files /dev/null and b/apps/legoremote/app.png differ diff --git a/apps/legoremote/metadata.json b/apps/legoremote/metadata.json new file mode 100644 index 000000000..c86251860 --- /dev/null +++ b/apps/legoremote/metadata.json @@ -0,0 +1,14 @@ +{ "id": "legoremote", + "name": "LEGO Remote control", + "shortName":"LEGO Remote", + "version":"0.01", + "description": "Use your Bangle.js to control LEGO models. See the README for compatibility", + "icon": "app.png", + "tags": "toy,lego,bluetooth", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"legoremote.app.js","url":"app.js"}, + {"name":"legoremote.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/lightswitch/README.md b/apps/lightswitch/README.md index 67d070f5c..c17a6a4ed 100644 --- a/apps/lightswitch/README.md +++ b/apps/lightswitch/README.md @@ -1,6 +1,6 @@ # Light Switch Widget -With this widget I wanted to create a solution to quickly en-/disable the LCD backlight and even change the brightness. +With this widget I wanted to create a solution to quickly en-/disable the LCD backlight and even change the brightness. In addition it shows the lock status with the option to personalize the lock icon with a tiny image. All touch and drag inputs related to this widget are cached/masked to prevent actions in the active app. @@ -9,32 +9,32 @@ All touch and drag inputs related to this widget are cached/masked to prevent ac --- ### Control --- -* __On / off__ +* __On / off__ Single touch the widget to en-/disable the backlight. -* __Change brightness__ _(can be disabled)_ +* __Change brightness__ _(can be disabled)_ First touch the widget, then quickly touch the screen again and drag up/down until you reach your wished brigthness. -* __Double tap to flash backlight__ _(can be disabled)_ - By defaut you can double tap on the right side of your bangle to flash the backlight for a short duration. +* __Double tap to flash backlight__ _(can be disabled)_ + By defaut you can double tap on the right side of your bangle to flash the backlight for a short duration. (While the backlight is active your bangle will be unlocked.) -* __Double tap to unlock__ _(disabled by default)_ +* __Double tap to unlock__ _(disabled by default)_ If a side is defined in the app settings, your bangle will be unlocked if you double tap on that side. --- ### Settings --- #### Widget - Change the apperance of the widget: -* __Bulb col__ - _red_ / _yellow_ / _green_ / __cyan__ / _blue_ / _magenta_ - Define the color used for the lightbulbs inner circle. +* __Bulb col__ + _red_ / _yellow_ / _green_ / __cyan__ / _blue_ / _magenta_ + Define the color used for the lightbulbs inner circle. The selected color will be dimmed depending on the actual brightness value. -* __Image__ - __default__ / _random_ / _..._ +* __Image__ + __default__ / _random_ / _..._ Set your favourite lock icon image. (If no image file is found _no image_ will be displayed.) * _random_ -> Select a random image on each time the widget is drawn. #### Control - Change when and how to use the widget: -* __Touch__ - _on def clk_ / _on all clk_ / _clk+setting_ / _clk+launch_ / _except apps_ / __always on__ +* __Touch__ + _on def clk_ / _on all clk_ / _clk+setting_ / _clk+launch_ / _except apps_ / __always on__ Select when touching the widget is active to en-/disable the backlight. * _on def clk_ -> only on your selected main clock face * _on all clk_ -> on all apps of the type _clock_ @@ -42,32 +42,34 @@ All touch and drag inputs related to this widget are cached/masked to prevent ac * _clk+launch_ -> on all apps of the types _clock_ and _launch_ * _except apps_ -> on all apps of the types _clock_ and _launch_ and in the settings * _always on_ -> always enabled when the widget is displayed -* __Oversize__ - _0px_ / _1px_ / _..._ / __20px__ / _..._ / _50px_ +* __Oversize__ + _0px_ / _1px_ / _..._ / __20px__ / _..._ / _50px_ To make it easier to hit the widget, this value extends the touch area of the widget in all directions. -* __Drag Delay__ - _off_ / _50ms_ / _100ms_ / _..._ / __500ms__ / _..._ / _1000ms_ +* __Drag Delay__ + _off_ / _50ms_ / _100ms_ / _..._ / __500ms__ / _..._ / _1000ms_ Change the maximum delay between first touch and re-touch/drag to change the brightness or disable changing the brightness completely. -* __Min Value__ - _1%_ / _2%_ / _..._ / __10%__ / _..._ / _100%_ - Set the minimal level of brightness you can change to. +* __Min Value__ + _1%_ / _2%_ / _..._ / __10%__ / _..._ / _100%_ + Set the minimal level of brightness you can change to. +* __Tap to lock__ + Tapping the widget locks the screen, rather than toggling brightness. #### Unlock - Set double tap side to unlock: -* __TapSide__ +* __TapSide__ __off__ / _left_ / _right_ / _top_ / _bottom_ / _front_ / _back_ #### Flash - Change if and how to flash the backlight: -* __TapSide__ - _off_ / _left_ / __right__ / _top_ / _bottom_ / _front_ / _back_ +* __TapSide__ + _off_ / _left_ / __right__ / _top_ / _bottom_ / _front_ / _back_ Set double tap side to flash the backlight or disable completely. -* __Tap__ - _on locked_ / _on unlocked_ / __always on__ +* __Tap__ + _on locked_ / _on unlocked_ / __always on__ Select when a double tap is recognised. -* __Timeout__ - _0.5s_ / _1s_ / _..._ / __2s__ / _..._ / _10s_ +* __Timeout__ + _0.5s_ / _1s_ / _..._ / __2s__ / _..._ / _10s_ Change how long the backlight will be activated on a flash. -* __Min Value__ - _1%_ / _2%_ / _..._ / __20%__ / _..._ / _100%_ +* __Min Value__ + _1%_ / _2%_ / _..._ / __20%__ / _..._ / _100%_ Set the minimal level of brightness for the backlight on a flash. --- @@ -81,7 +83,7 @@ All touch and drag inputs related to this widget are cached/masked to prevent ac Examples in default light and dark theme. -| Lock | Heart | Invader | JS | Smiley | Skull | Storm | +| Lock | Heart | Invader | JS | Smiley | Skull | Storm | |:----:|:-----:|:-------:|:--:|:------:|:-----:|:-----:| | ![](images/image_lock.png) | ![](images/image_heart.png) | ![](images/image_invader.png) | ![](images/image_js.png) | ![](images/image_smiley.png) | ![](images/image_skull.png) | ![](images/image_storm.png) | diff --git a/apps/lightswitch/settings.js b/apps/lightswitch/settings.js index 5ac70bc28..aff45a444 100644 --- a/apps/lightswitch/settings.js +++ b/apps/lightswitch/settings.js @@ -10,6 +10,7 @@ oversize: 20, dragDelay: 500, minValue: 0.1, + tapToLock: false, unlockSide: "", tapSide: "right", tapOn: "always", @@ -119,6 +120,11 @@ max: 100, step: 1 }, + tapToLock: { + title: ["on", "off"], + value: [true, false], + drawWidgets: false + }, unlockSide: { title: ["off", "left", "right", "top", "bottom", "front", "back"], value: ["", "left", "right", "top", "bottom", "front", "back"] @@ -154,6 +160,7 @@ "Oversize": getEntry("oversize"), "Drag Delay": getEntry("dragDelay"), "Min Value": getEntry("minValue"), + "Tap to lock": getEntry("tapToLock"), "-- Unlock": 0, "TapSide": getEntry("unlockSide"), "-- Flash": 0, diff --git a/apps/lightswitch/widget.js b/apps/lightswitch/widget.js index 9eb488aca..922875216 100644 --- a/apps/lightswitch/widget.js +++ b/apps/lightswitch/widget.js @@ -7,6 +7,7 @@ oversize: 20, dragDelay: 500, minValue: 0.1, + tapToLock: false, unlockSide: "", tapSide: "right", tapOn: "always", @@ -208,8 +209,12 @@ w.dragStatus = "off"; }, w.dragDelay, w); } - // switch backlight - w.changeValue(); + if (w.tapToLock) { + Bangle.setLocked(true); + } else { + // switch backlight + w.changeValue(); + } // masks this touch event by messing up the event handler // see https://github.com/espruino/Espruino/issues/2151 Bangle.removeListener("touch", w.touchListener); @@ -251,7 +256,7 @@ w = undefined; } }); - + Bangle.on("lock", locked => { var w = WIDGETS.lightswitch; // set lcd brightness on unlocking diff --git a/apps/linuxclock/ChangeLog b/apps/linuxclock/ChangeLog index 71ca3afbb..5529034be 100644 --- a/apps/linuxclock/ChangeLog +++ b/apps/linuxclock/ChangeLog @@ -1,4 +1,5 @@ 0.01: New App. 0.02: Performance improvements. 0.03: Update clock_info to avoid a redraw -0.04: Fix clkinfo -- use .get instead of .show \ No newline at end of file +0.04: Fix clkinfo -- use .get instead of .show +0.05: Use clock_info module as an app diff --git a/apps/linuxclock/metadata.json b/apps/linuxclock/metadata.json index 68915b832..412fd53b4 100644 --- a/apps/linuxclock/metadata.json +++ b/apps/linuxclock/metadata.json @@ -1,7 +1,7 @@ { "id": "linuxclock", "name": "Linux Clock", - "version": "0.04", + "version": "0.05", "description": "A Linux inspired clock.", "readme": "README.md", "icon": "app.png", @@ -9,7 +9,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], - "allow_emulator": true, + "dependencies" : { "clock_info":"module" }, "storage": [ {"name":"linuxclock.app.js","url":"app.js"}, {"name":"linuxclock.img","url":"app-icon.js","evaluate":true}, diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 6289a2568..ee6371f4e 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -73,7 +73,7 @@ let lastTemp = 0; const phone = { get status() { - return NRF.getSecurityStatus().connected ? "Yes" : "No"; + return NRF.getSecurityStatus().connected ? /*LANG*/"Yes" : /*LANG*/"No"; }, message: null, messageTimeout: null, diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index d061e6642..f314c72d3 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -87,3 +87,4 @@ 0.62: Remove '.show' field, tidyup and fix .open if fast load not enabled 0.63: Fix messages app loading on clock without fast load 0.64: Ensure we don't get 'undefined' as the message body +0.65: Make sure messages are saved if not in the clock app (fix #2460) diff --git a/apps/messagegui/lib.js b/apps/messagegui/lib.js index a9436a77b..54d79866a 100644 --- a/apps/messagegui/lib.js +++ b/apps/messagegui/lib.js @@ -29,7 +29,7 @@ exports.listener = function(type, msg) { } const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {}; - let loadMessages = (Bangle.CLOCK || event.important); // should we load the messages app? + let loadMessages = (Bangle.CLOCK || msg.important); // should we load the messages app? if (type==="music") { if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) loadMessages = true; else return; diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json index 3504122f9..1e22f7304 100644 --- a/apps/messagegui/metadata.json +++ b/apps/messagegui/metadata.json @@ -2,7 +2,7 @@ "id": "messagegui", "name": "Message UI", "shortName": "Messages", - "version": "0.64", + "version": "0.65", "description": "Default app to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", diff --git a/apps/messageicons/ChangeLog b/apps/messageicons/ChangeLog index 1dd1f3d9e..f420615cb 100644 --- a/apps/messageicons/ChangeLog +++ b/apps/messageicons/ChangeLog @@ -3,3 +3,4 @@ 0.03: Fix icons broken in 0v02 (#2386) Store all icons in a separate binary file (much faster lookup) 0.04: Add message icon for 'clock' +0.05: Add message icon for 'jira' diff --git a/apps/messageicons/icons.img b/apps/messageicons/icons.img index 104168357..66ecb53f8 100644 Binary files a/apps/messageicons/icons.img and b/apps/messageicons/icons.img differ diff --git a/apps/messageicons/icons/generate.js b/apps/messageicons/icons/generate.js index e857032af..b77cfc26e 100755 --- a/apps/messageicons/icons/generate.js +++ b/apps/messageicons/icons/generate.js @@ -87,6 +87,7 @@ exports.getColor = function(msg,options) { if (st.iconColorMode == 'mono') return options.default; const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); return { + // This file is generated by /icons/generate.js. If you need to modify its content, you should do it there instead. // generic colors, using B2-safe colors // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/ @@ -107,6 +108,7 @@ exports.getColor = function(msg,options) { "google home": "#fbbc05", // "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background "instagram": "#ff0069", // https://about.instagram.com/brand/gradient + "jira": "#0052cc", //https://atlassian.design/resources/logo-library "lieferando": "#ff8000", "linkedin": "#0a66c2", // https://brand.linkedin.com/ "messenger": "#0078ff", diff --git a/apps/messageicons/icons/icon_names.json b/apps/messageicons/icons/icon_names.json index f7a743e85..7c09cd397 100644 --- a/apps/messageicons/icons/icon_names.json +++ b/apps/messageicons/icons/icon_names.json @@ -42,6 +42,7 @@ { "app":"google play store", "icon":"google play store.png" }, { "app":"home assistant", "icon":"home assistant.png" }, { "app":"instagram", "icon":"instagram.png" }, + { "app":"jira", "icon":"jira.png" }, { "app":"kalender", "icon":"kalender.png" }, { "app":"keep notes", "icon":"google keep.png" }, { "app":"lieferando", "icon":"lieferando.png" }, diff --git a/apps/messageicons/icons/jira.png b/apps/messageicons/icons/jira.png new file mode 100644 index 000000000..fe3d83b6a Binary files /dev/null and b/apps/messageicons/icons/jira.png differ diff --git a/apps/messageicons/lib.js b/apps/messageicons/lib.js index 314840c13..f7efa2d16 100644 --- a/apps/messageicons/lib.js +++ b/apps/messageicons/lib.js @@ -2,7 +2,7 @@ exports.getImage = function(msg) { if (msg.img) return atob(msg.img); let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); if (msg.id=="music") s="music"; - let match = ",default|0,airbnb|1,alarm|2,alarmclockreceiver|2,amazon shopping|3,bibel|4,bitwarden|5,1password|5,lastpass|5,dashlane|5,bring|6,calendar|7,etar|7,chat|8,chrome|9,clock|2,corona-warn|10,bmo|11,desjardins|11,rbc mobile|11,nbc|11,rabobank|11,scotiabank|11,td (canada)|11,discord|12,drive|13,element|14,facebook|15,messenger|16,firefox|17,firefox beta|17,firefox nightly|17,f-droid|5,neo store|5,aurora droid|5,github|18,gitlab|19,gmx|20,google|21,google home|22,google play store|23,home assistant|24,instagram|25,kalender|26,keep notes|27,lieferando|28,linkedin|29,maps|30,organic maps|30,osmand|30,mastodon|31,fedilab|31,tooot|31,tusky|31,mattermost|32,n26|33,netflix|34,news|35,cbc news|35,rc info|35,reuters|35,ap news|35,la presse|35,nbc news|35,nextbike|36,nina|37,outlook mail|38,paypal|39,phone|40,plex|41,pocket|42,post & dhl|43,proton mail|44,reddit|45,sync pro|45,sync dev|45,boost|45,infinity|45,slide|45,signal|46,skype|47,slack|48,snapchat|49,starbucks|50,steam|51,teams|52,telegram|53,telegram foss|53,threema|54,tiktok|55,to do|56,opentasks|56,tasks|56,transit|57,twitch|58,twitter|59,uber|60,lyft|60,vlc|61,warnapp|62,whatsapp|63,wordfeud|64,youtube|65,newpipe|65,zoom|66,meet|66,music|67,sms message|0,mail|0,gmail|0,".match(new RegExp(`,${s}\\|(\\d+)`)) + let match = ",default|0,airbnb|1,alarm|2,alarmclockreceiver|2,amazon shopping|3,bibel|4,bitwarden|5,1password|5,lastpass|5,dashlane|5,bring|6,calendar|7,etar|7,chat|8,chrome|9,clock|2,corona-warn|10,bmo|11,desjardins|11,rbc mobile|11,nbc|11,rabobank|11,scotiabank|11,td (canada)|11,discord|12,drive|13,element|14,facebook|15,messenger|16,firefox|17,firefox beta|17,firefox nightly|17,f-droid|5,neo store|5,aurora droid|5,github|18,gitlab|19,gmx|20,google|21,google home|22,google play store|23,home assistant|24,instagram|25,jira|26,kalender|27,keep notes|28,lieferando|29,linkedin|30,maps|31,organic maps|31,osmand|31,mastodon|32,fedilab|32,tooot|32,tusky|32,mattermost|33,n26|34,netflix|35,news|36,cbc news|36,rc info|36,reuters|36,ap news|36,la presse|36,nbc news|36,nextbike|37,nina|38,outlook mail|39,paypal|40,phone|41,plex|42,pocket|43,post & dhl|44,proton mail|45,reddit|46,sync pro|46,sync dev|46,boost|46,infinity|46,slide|46,signal|47,skype|48,slack|49,snapchat|50,starbucks|51,steam|52,teams|53,telegram|54,telegram foss|54,threema|55,tiktok|56,to do|57,opentasks|57,tasks|57,transit|58,twitch|59,twitter|60,uber|61,lyft|61,vlc|62,warnapp|63,whatsapp|64,wordfeud|65,youtube|66,newpipe|66,zoom|67,meet|67,music|68,sms message|0,mail|0,gmail|0,".match(new RegExp(`,${s}\\|(\\d+)`)) return require("Storage").read("messageicons.img", (match===null)?0:match[1]*76, 76); }; @@ -13,6 +13,7 @@ exports.getColor = function(msg,options) { if (st.iconColorMode == 'mono') return options.default; const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); return { + // This file is generated by /icons/generate.js. If you need to modify its content, you should do it there instead. // generic colors, using B2-safe colors // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/ @@ -33,6 +34,7 @@ exports.getColor = function(msg,options) { "google home": "#fbbc05", // "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background "instagram": "#ff0069", // https://about.instagram.com/brand/gradient + "jira": "#0052cc", //https://atlassian.design/resources/logo-library "lieferando": "#ff8000", "linkedin": "#0a66c2", // https://brand.linkedin.com/ "messenger": "#0078ff", diff --git a/apps/messageicons/metadata.json b/apps/messageicons/metadata.json index 0d1db227b..13f218508 100644 --- a/apps/messageicons/metadata.json +++ b/apps/messageicons/metadata.json @@ -1,7 +1,7 @@ { "id": "messageicons", "name": "Message Icons", - "version": "0.04", + "version": "0.05", "description": "Library containing a list of icons and colors for apps", "icon": "app.png", "type": "module", diff --git a/apps/messages_light/ChangeLog b/apps/messages_light/ChangeLog index 328e2a120..9c3acf0f2 100644 --- a/apps/messages_light/ChangeLog +++ b/apps/messages_light/ChangeLog @@ -4,4 +4,5 @@ settings now points to message settings implemented use of the "messageicons" library removed lib no longer used -1.3: icon changed \ No newline at end of file +1.3: icon changed +1.4: new management of events implemented; removed code no longer used (from now the music will be managed by the Messagesgui app) \ No newline at end of file diff --git a/apps/messages_light/README.md b/apps/messages_light/README.md index 00fe39bd0..2bc162cb8 100644 --- a/apps/messages_light/README.md +++ b/apps/messages_light/README.md @@ -5,7 +5,17 @@ This app handles the display of messages and message notifications. It is a GUI replacement for the `messages` apps. +To work, you must install: +- Messages +- Messages UI +- Messages Light (obviously) + +The Messages UI is recalled for the management of the "Music" notification (up to implementing a dedicated app) + + ## Creator Rarder44 +Thanks to @halemmerich for having "reviewed" the code. I applied some of your changes. + diff --git a/apps/messages_light/messages_light.app.js b/apps/messages_light/messages_light.app.js index 5d5363d38..94f619259 100644 --- a/apps/messages_light/messages_light.app.js +++ b/apps/messages_light/messages_light.app.js @@ -10,35 +10,39 @@ } */ + + + let LOG=function(){ //print.apply(null, arguments); } - - -let settings= (()=>{ - let tmp={}; - tmp.NewEventFileName="messages_light.NewEvent.json"; - - tmp.fontSmall = "6x8"; - tmp.fontMedium = g.getFonts().includes("Vector")?"Vector:16":"6x8:2"; - tmp.fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; - tmp.fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; +let settings= { + NewEventFileName:"messages_light.NewEvent.json", + fontSmall : "6x8", + fontMedium : "Vector:16", + fontBig : "Vector:20", + fontLarge : "Vector:30", + colHeadBg : g.theme.dark ? "#141":"#4f4", - tmp.colHeadBg = g.theme.dark ? "#141":"#4f4"; - tmp.colBg = g.theme.dark ? "#000":"#fff"; - tmp.colLock = g.theme.dark ? "#ff0000":"#ff0000"; + colBg : g.theme.dark ? "#000":"#fff", + colLock : g.theme.dark ? "#ff0000":"#ff0000", + + quiet:!!((require('Storage').readJSON('setting.json', 1) || {}).quiet), + timeOut:(require('Storage').readJSON("messages_light.settings.json", true) || {}).timeOut || "Off", +}; + + - tmp.quiet=((require('Storage').readJSON('setting.json', 1) || {}).quiet) - return tmp; -})(); let EventQueue=[]; //in posizione 0, c'è quello attualmente visualizzato let callInProgress=false; +let justOpened=true; + //TODO: RICORDARSI DI FARE IL DELETE @@ -46,68 +50,73 @@ var manageEvent = function(event) { event.new=true; - LOG("manageEvent"); - if( event.id=="call") - { - showCall(event); - return; + LOG(event); + + if( event.id=="call"){ + showCall(event); } - switch(event.t) - { - case "add": - EventQueue.unshift(event); + else if( event.id=="music"){ + //la musica non la gestisco più ( uso l'app standard o un altra app) + } + else{ - if(!callInProgress) - showMessage(event); - break; - - case "modify": - //cerco l'evento nella lista, se lo trovo, lo modifico, altrimenti lo pusho - let find=false; - EventQueue.forEach(element => { - if(element.id == event.id) - { - find=true; - Object.assign(element,event); - } - }); - if(!find) //se non l'ho trovato, lo aggiungo in fondo + //----------------- + //notification + //----------------- + if(event.t=="add"){ EventQueue.unshift(event); - - if(!callInProgress) - showMessage(event); - break; - - case "remove": - - //se non c'è niente nella queue e non c'è una chiamata in corso - if( EventQueue.length==0 && !callInProgress) - next(); - - //se l'id è uguale a quello attualmente visualizzato ( e non siamo in chiamata ) - if(!callInProgress && EventQueue[0] !== undefined && EventQueue[0].id == event.id) - next(); //passo al messaggio successivo ( per la rimozione ci penserà la next ) - - else{ - //altrimenti rimuovo tutti gli elementi con quell'id( creando un nuovo array ) - let newEventQueue=[]; + + if(!callInProgress) + showMessage(event); + } + else if(event.t=="modify"){ + //cerco l'evento nella lista, se lo trovo, lo modifico, altrimenti lo pusho + let find=false; EventQueue.forEach(element => { - if(element.id != event.id) - newEventQueue.push(element); + if(element.id == event.id) + { + find=true; + Object.assign(element,event); + } }); - EventQueue=newEventQueue; - } - - + if(!find) //se non l'ho trovato, lo aggiungo in fondo + EventQueue.unshift(event); + + if(!callInProgress) + showMessage(event); + } + else if(event.t=="remove"){ + //se non c'è niente nella queue e non c'è una chiamata in corso + if( EventQueue.length==0 && !callInProgress) + next(); + + //se l'id è uguale a quello attualmente visualizzato ( e non siamo in chiamata ) + if(!callInProgress && EventQueue[0] !== undefined && EventQueue[0].id == event.id) + next(); //passo al messaggio successivo ( per la rimozione ci penserà la next ) + + else{ + //altrimenti rimuovo tutti gli elementi con quell'id( creando un nuovo array ) + let newEventQueue=[]; + EventQueue.forEach(element => { + if(element.id != event.id) + newEventQueue.push(element); + }); + + //non sovrascrivo, cosi uso lo stesso oggetto in memoria e dovrei avere meno problemi di memory leak + EventQueue.length=0; + newEventQueue.forEach(element => { + EventQueue.push(element); + }); + } + } + //----------------- + //notification + //----------------- + - - break; - case "musicstate": - case "musicinfo": - - break; } + }; @@ -118,6 +127,10 @@ var manageEvent = function(event) { let showMessage = function(msg){ LOG("showMessage"); LOG(msg); + + updateTimeout(); + + g.setBgColor(settings.colBg); @@ -208,7 +221,7 @@ let showCall = function(msg) } callInProgress=true; - + updateTimeout(); //se è una chiamata ( o una nuova chiamata, diversa dalla precedente ) @@ -262,7 +275,6 @@ let showCall = function(msg) - let next=function(){ LOG("next"); @@ -274,9 +286,10 @@ let next=function(){ EventQueue.shift(); //passa al messaggio successivo, se presente - tolgo il primo callInProgress=false; + LOG(EventQueue.length); if( EventQueue.length == 0) { - LOG("no element in queue - closing") + LOG("no element in queue - closing"); setTimeout(_ => load()); return; } @@ -288,31 +301,12 @@ let next=function(){ - - - - - - - - -let showMapMessage=function(msg) { - - g.clearRect(Bangle.appRect); - PrintMessageStrings({body:"Not implemented!"}); - -} - - - - - -let CallBuzzTimer=null; +let CallBuzzTimer=undefined; let StopBuzzCall=function() { if (CallBuzzTimer){ clearInterval(CallBuzzTimer); - CallBuzzTimer=null; + CallBuzzTimer=undefined; } } let DrawTriangleUp=function() @@ -443,13 +437,16 @@ let PrintMessageStrings=function(msg) let doubleTapUnlock=function(data) { + updateTimeout(); if( data.double) //solo se in double { Bangle.setLocked(false); Bangle.setLCDPower(1); } } -let toushScroll=function(button, xy) { +let toushScroll=function(_, xy) { + updateTimeout(); + let height=176; //g.getHeight(); -> 176 B2 height/=2; @@ -464,6 +461,24 @@ let toushScroll=function(button, xy) { } + +let timeout; +const updateTimeout = function(){ +if (settings.timeOut!="Off"){ + removeTimeout(); + if( callInProgress) return; //c'è una chiamata in corso -> no timeout + if( music!=undefined && EventQueue.length==0 ) return; //ho aperto l'interfaccia della musica e non ho messaggi davanti -> no timeout + + + let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt + timeout = setTimeout(next,time*1000); //next or Bangle.showClock/load()??? + } +}; +const removeTimeout=function(){ + if (timeout) clearTimeout(timeout); +} + + let main = function(){ LOG("Main"); @@ -485,9 +500,11 @@ let main = function(){ manageEvent(eventToShow); else { - LOG("file not found!"); - setTimeout(_ => load(), 0); + LOG("file event not found! -> ?? open debug text"); + setTimeout(_=>{ GB({"t":"notify","id":15754117198411,"src":"Hangouts","title":"A Name","body":"Debug notification \nmessage contents demo demo demo demo"}) },0); } + justOpened=false; + }; diff --git a/apps/messages_light/messages_light.boot.js b/apps/messages_light/messages_light.boot.js index 741d08b96..f28e50661 100644 --- a/apps/messages_light/messages_light.boot.js +++ b/apps/messages_light/messages_light.boot.js @@ -1,33 +1,13 @@ -/* -//OLD CODE -> backup purpose - -let messageBootManager=function(type,event){ - //se l'app non è aperta - if ("undefined"==typeof manageEvent) - { - if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app - - //la apro - require("Storage").writeJSON("messages_light.NewEvent.json",{"event":event,"type":type}); - load("messages_light.app.js"); - } - else - { - //altrimenti gli dico di gestire il messaggio - manageEvent(type,event); - } -} -Bangle.on("message", messageBootManager); -Bangle.on("call", messageBootManager);*/ - - - +//OLD Code //override require to filter require("message") -global.require_real=global.require; +/*global.require_real=global.require; global.require = (_require => file => { - if (file==="messages") file = "messagesProxy"; - //else if (file==="messages_REAL") file = "messages"; //backdoor to real message - + if (file==="messages") file = "messagesProxy"; return _require(file); -})(require); - +})(require);*/ + +//the file on the device is called "boot_messages_light.boot.js" +//it's NOT an error! +//it's for the boot order + +Bangle.on("message", (type, msg) => require("messages_light.listener.js").listener(type, msg)); diff --git a/apps/messages_light/messages_light.listener.js b/apps/messages_light/messages_light.listener.js new file mode 100644 index 000000000..9b1158b1c --- /dev/null +++ b/apps/messages_light/messages_light.listener.js @@ -0,0 +1,43 @@ +exports.listener = function(type, event) { + + /* + //salva gli eventi che arrivano su file + events=require("Storage").readJSON("events_log",true) || []; + events.push ( event) + require("Storage").writeJSON("events_log",events);*/ + + //if (event.handled) return; // already handled/app open + if( type=="music" || event.id=="music") return; //lo lascio gestire a qualcun altro + + //se arrivo qua gestisco io + //non mi preoccupo di salvare ( a meno di problemi a mantenere tanti messaggi in queue nella ram...) + event.handled=true; + + + + let callApp; + //se l'app non è aperta + if ("undefined"==typeof manageEvent) + { + if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app + + //chiamo la load dell'app + callApp=function(event){ + require("Storage").writeJSON("messages_light.NewEvent.json",event); + load("messages_light.app.js"); + } + } + else + { + //dico all'app di gestire l'evento + callApp=function(event){ + manageEvent(event); + } + } + + + callApp(event); + +} + + diff --git a/apps/messages_light/messages_light.messagesProxy.js b/apps/messages_light/messages_light.messagesProxy.js deleted file mode 100644 index 723397057..000000000 --- a/apps/messages_light/messages_light.messagesProxy.js +++ /dev/null @@ -1,30 +0,0 @@ - -//gestisco il messaggio a modo mio -exports.pushMessage = function(event) { - - //TODO: now i can't handle the music, so i call the real message app - if( event.id=="music") return require_real("messages").pushMessage(event); - - //se l'app non è aperta - if ("undefined"==typeof manageEvent) - { - if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app - - //la apro - require_real("Storage").writeJSON("messages_light.NewEvent.json",event); - load("messages_light.app.js"); - } - else - { - //altrimenti gli dico di gestire il messaggio - manageEvent(event); - } -} - - -//Call original message library -exports.clearAll = function() { return require_real("messages").clearAll()} -exports.getMessages = function() { return require_real("messages").getMessages()} -exports.status = function() { return require_real("messages").status()} -exports.buzz = function() { return require_real("messages").buzz(msgSrc)} -exports.stopBuzz = function() { return require_real("messages").stopBuzz()} \ No newline at end of file diff --git a/apps/messages_light/messages_light.settings.js b/apps/messages_light/messages_light.settings.js index b7197c70a..cd813d928 100644 --- a/apps/messages_light/messages_light.settings.js +++ b/apps/messages_light/messages_light.settings.js @@ -1 +1,27 @@ -eval(require("Storage").read("messages.settings.js")); +(function(back) { + const SETTINGS_FILE_NAME="messages_light.settings.json"; + let settings = function() { + let settings = require('Storage').readJSON(SETTINGS_FILE_NAME, true) || {}; + return settings; + } + function updateSetting(setting, value) { + let settings = require('Storage').readJSON(SETTINGS_FILE_NAME, true) || {}; + settings[setting] = value; + require('Storage').writeJSON(SETTINGS_FILE_NAME, settings); + } + const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; + var mainmenu = { + "" : { "title" : /*LANG*/"Messages Light" }, + "< Back" : back, + /*LANG*/'Time Out': { + value: timeOutChoices.indexOf(settings.timeOut), + min: 0, max: timeOutChoices.length-1, + format: v => timeOutChoices[v], + onchange: m => { + updateSetting("timeOut", timeOutChoices[m]); + } + }, + }; + E.showMenu(mainmenu); + }); + \ No newline at end of file diff --git a/apps/messages_light/metadata.json b/apps/messages_light/metadata.json index 3515a75c2..5ec1f5a37 100644 --- a/apps/messages_light/metadata.json +++ b/apps/messages_light/metadata.json @@ -1,7 +1,7 @@ { "id": "messages_light", "name": "Messages Light", - "version": "1.3", + "version": "1.4", "description": "A light implementation of messages App (display notifications from iOS and Gadgetbridge/Android)", "icon": "app.png", "type": "app", @@ -13,8 +13,8 @@ {"name":"messages_light.app.js","url":"messages_light.app.js"}, {"name":"messages_light.settings.js","url":"messages_light.settings.js"}, {"name":"messages_light.img","url":"app-icon.js","evaluate":true}, - {"name":"messagesProxy","url":"messages_light.messagesProxy.js"}, - {"name":"messages_light.boot.js","url":"messages_light.boot.js"} + {"name":"boot_messages_light.boot.js","url":"messages_light.boot.js"}, + {"name":"messages_light.listener.js","url":"messages_light.listener.js"} ], "data": [{"name":"messages_light.settings.json"},{"name":"messages_light.NewMessage.json"}], "screenshots": [{"url":"screenshot-notify.png"} ,{"url":"screenshot-long-text1.png"},{"url":"screenshot-long-text2.png"}, {"url":"screenshot-call.png"} ] diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog new file mode 100644 index 000000000..0a2cf27b0 --- /dev/null +++ b/apps/messagesoverlay/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial fork from messages_light +0.02: Fix touch/drag/swipe handlers not being restored correctly if a message is removed +0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible. diff --git a/apps/messagesoverlay/README.md b/apps/messagesoverlay/README.md new file mode 100644 index 000000000..8ce1cc64d --- /dev/null +++ b/apps/messagesoverlay/README.md @@ -0,0 +1,24 @@ +# Messages overlay app + +This app handles the display of messages and message notifications as an overlay pop up. + +It is a GUI replacement for the `messages` apps. + +Messages are ephemeral and not stored on the Bangle. + +## Usage + +Close app by tapping the X and scroll by swiping. The border of the pop up changes color if the Bangle is locked. The color depends on your currently active theme. + +## Firmware hint +Current stable firmware draws incorrect colors for emojis. Nightly firmware builds correct this. + +## Low memory mode + +If free memory is below 2000 blocks, the overlay automatically only uses 1 bit depth. Default uses roundabout 1300 blocks, while low memory mode uses about 600. + +## Creator + +[halemmerich](https://github.com/halemmerich) +Forked from messages_light by Rarder44 + diff --git a/apps/messagesoverlay/app.png b/apps/messagesoverlay/app.png new file mode 100644 index 000000000..1f738504d Binary files /dev/null and b/apps/messagesoverlay/app.png differ diff --git a/apps/messagesoverlay/boot.js b/apps/messagesoverlay/boot.js new file mode 100644 index 000000000..7731d608a --- /dev/null +++ b/apps/messagesoverlay/boot.js @@ -0,0 +1,7 @@ +//override require to filter require("message") +global.require_real=global.require; +global.require = (_require => file => { + if (file==="messages") file = "messagesoverlay"; + return _require(file); +})(require); + diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js new file mode 100644 index 000000000..5587fce19 --- /dev/null +++ b/apps/messagesoverlay/lib.js @@ -0,0 +1,488 @@ +/* MESSAGES is a list of: + {id:int, + src, + title, + subject, + body, + sender, + tel:string, + new:true // not read yet + } +*/ + +const ovrx = 10; +const ovry = 10; +const ovrw = g.getWidth()-2*ovrx; +const ovrh = g.getHeight()-2*ovry; +let _g = g; + +let lockListener; +let quiet; + +let LOG = function() { + //print.apply(null, arguments); +}; + +let isQuiet = function(){ + if (quiet == undefined) quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet; + return quiet; +}; + +let settings = { + fontSmall:"6x8", + fontMedium:"Vector:14", + fontBig:"Vector:20", + fontLarge:"Vector:30", +}; + +let eventQueue = []; +let callInProgress = false; + +let show = function(ovr){ + let img = ovr; + if (ovr.getBPP() == 1) { + img = ovr.asImage(); + img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]); + } + Bangle.setLCDOverlay(img, ovrx, ovry); +}; + +let manageEvent = function(ovr, event) { + event.new = true; + + LOG("manageEvent"); + if (event.id == "call") { + showCall(ovr, event); + return; + } + switch (event.t) { + case "add": + eventQueue.unshift(event); + + if (!callInProgress) + showMessage(ovr, event); + break; + + case "modify": + let find = false; + eventQueue.forEach(element => { + if (element.id == event.id) { + find = true; + Object.assign(element, event); + } + }); + if (!find) + eventQueue.unshift(event); + + if (!callInProgress) + showMessage(ovr, event); + break; + + case "remove": + if (eventQueue.length == 0 && !callInProgress) + next(ovr); + + if (!callInProgress && eventQueue[0] !== undefined && eventQueue[0].id == event.id) + next(ovr); + + else { + eventQueue.length = 0; // empty existing queue + eventQueue.forEach(element => { + if (element.id != event.id) + neweventQueue.push(element); + }); + } + + break; + case "musicstate": + case "musicinfo": + + break; + } +}; + +let roundedRect = function(ovr, x,y,w,h,filled){ + var poly = [ + x,y+4, + x+4,y, + x+w-5,y, + x+w-1,y+4, + x+w-1,y+h-5, + x+w-5,y+h-1, + x+4,y+h-1, + x,y+h-5, + x,y+4 + ]; + ovr.drawPoly(poly,true); + if (filled) ovr.fillPoly(poly,true); +}; + +let drawScreen = function(ovr, title, titleFont, src, iconcolor, icon){ + ovr.setBgColor(ovr.theme.bg2); + ovr.clearRect(2,2,ovr.getWidth()-3,37); + + ovr.setColor(ovr.theme.fg2); + ovr.setFont(settings.fontSmall); + ovr.setFontAlign(0,-1); + + let textCenter = (ovr.getWidth()+35-26)/2; + + if (src) { + let shortened = src; + while (ovr.stringWidth(shortened) > ovr.getWidth()-80) shortened = shortened.substring(0,shortened.length-2); + if (shortened.length != src.length) shortened += "..."; + ovr.drawString(shortened, textCenter, 2); + } + + ovr.setFontAlign(0,0); + ovr.setFont(titleFont); + if (title) ovr.drawString(title, textCenter, 38/2 + 5); + + ovr.setColor(ovr.theme.fg2); + + ovr.setFont(settings.fontMedium); + roundedRect(ovr, ovr.getWidth()-26,5,22,30,false); + ovr.setFont("Vector:16"); + ovr.drawString("X",ovr.getWidth()-14,21); + + ovr.setColor("#888"); + roundedRect(ovr, 5,5,30,30,true); + ovr.setColor(ovr.getBPP() != 1 ? iconcolor : ovr.theme.bg2); + ovr.drawImage(icon,8,8); +}; + +let showMessage = function(ovr, msg) { + LOG("showMessage"); + ovr.setBgColor(ovr.theme.bg); + + if (typeof msg.CanscrollDown === "undefined") + msg.CanscrollDown = false; + if (typeof msg.CanscrollUp === "undefined") + msg.CanscrollUp = false; + + // Normal text message display + let title = msg.title, + titleFont = settings.fontLarge, + lines; + if (title) { + let w = ovr.getWidth() - 35 - 26; + if (ovr.setFont(titleFont).stringWidth(title) > w) + titleFont = settings.fontMedium; + if (ovr.setFont(titleFont).stringWidth(title) > w) { + lines = ovr.wrapString(title, w); + title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n"); + } + } + + drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg)); + + if (!isQuiet() && msg.new) { + msg.new = false; + Bangle.buzz(); + } + + drawMessage(ovr, msg); +}; + +let drawBorder = function(ovr) { + if (Bangle.isLocked()) + ovr.setColor(ovr.theme.fgH); + else + ovr.setColor(ovr.theme.fg); + ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1); + ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2); + show(ovr); + if (!isQuiet()) Bangle.setLCDPower(1); +}; + +let showCall = function(ovr, msg) { + LOG("showCall"); + LOG(msg); + + if (msg.t == "remove") { + LOG("hide call screen"); + next(ovr); //dont shift + return; + } + + callInProgress = true; + + let title = msg.title, + titleFont = settings.fontLarge, + lines; + if (title) { + let w = ovr.getWidth() - 35 -26; + if (ovr.setFont(titleFont).stringWidth(title) > w) + titleFont = settings.fontMedium; + if (ovr.setFont(titleFont).stringWidth(title) > w) { + lines = ovr.wrapString(title, w); + title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n"); + } + } + + drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg)); + + stopCallBuzz(); + if (!isQuiet()) { + if (msg.new) { + msg.new = false; + if (callBuzzTimer) clearInterval(callBuzzTimer); + callBuzzTimer = setInterval(function() { + Bangle.buzz(500); + }, 1000); + + Bangle.buzz(500); + } + } + drawMessage(ovr, msg); +}; + +let next = function(ovr) { + LOG("next"); + stopCallBuzz(); + + if (!callInProgress) + eventQueue.shift(); + + callInProgress = false; + if (eventQueue.length == 0) { + LOG("no element in queue - closing"); + cleanup(); + return; + } + + showMessage(ovr, eventQueue[0]); +}; + +let showMapMessage = function(ovr, msg) { + ovr.clearRect(2,2,ovr.getWidth()-3,ovr.getHeight()-3); + drawMessage(ovr, { + body: "Not implemented!" + }); +}; + +let callBuzzTimer = null; +let stopCallBuzz = function() { + if (callBuzzTimer) { + clearInterval(callBuzzTimer); + callBuzzTimer = undefined; + } +}; + +let drawTriangleUp = function(ovr) { + ovr.reset(); + ovr.fillPoly([ovr.getWidth()-9, 46,ovr.getWidth()-14, 56,ovr.getWidth()-4, 56]); +}; + +let drawTriangleDown = function(ovr) { + ovr.reset(); + ovr.fillPoly([ovr.getWidth()-9, ovr.getHeight()-6, ovr.getWidth()-14, ovr.getHeight()-16, ovr.getWidth()-4, ovr.getHeight()-16]); +}; + +let linesScroll = 6; + +let scrollUp = function(ovr) { + msg = eventQueue[0]; + LOG("up", msg); + if (typeof msg.FirstLine === "undefined") + msg.FirstLine = 0; + if (typeof msg.CanscrollUp === "undefined") + msg.CanscrollUp = false; + + if (!msg.CanscrollUp) return; + + msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - linesScroll : 0; + + drawMessage(ovr, msg); +}; + +let scrollDown = function(ovr) { + msg = eventQueue[0]; + LOG("down", msg); + if (typeof msg.FirstLine === "undefined") + msg.FirstLine = 0; + if (typeof msg.CanscrollDown === "undefined") + msg.CanscrollDown = false; + + if (!msg.CanscrollDown) return; + + msg.FirstLine = msg.FirstLine + linesScroll; + drawMessage(ovr, msg); +}; + +let drawMessage = function(ovr, msg) { + let MyWrapString = function(str, maxWidth) { + str = str.replace("\r\n", "\n").replace("\r", "\n"); + return ovr.wrapString(str, maxWidth); + }; + + if (typeof msg.FirstLine === "undefined") msg.FirstLine = 0; + + let bodyFont = typeof msg.bodyFont === "undefined" ? settings.fontMedium : msg.bodyFont; + let Padding = 3; + if (typeof msg.lines === "undefined") { + ovr.setFont(bodyFont); + msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2)); + if (msg.lines.length <= 2) { + bodyFont = ovr.getFonts().includes("Vector") ? "Vector:20" : "6x8:3"; + ovr.setFont(bodyFont); + msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2)); + msg.bodyFont = bodyFont; + } + } + + let NumLines = 7; + + let linesToPrint = (msg.lines.length > NumLines) ? msg.lines.slice(msg.FirstLine, msg.FirstLine + NumLines) : msg.lines; + + let yText = 40; + + ovr.setBgColor(ovr.theme.bg); + ovr.setColor(ovr.theme.fg); + ovr.clearRect(2, yText, ovrw-3, ovrh-3); + let xText = Padding; + yText += Padding; + ovr.setFont(bodyFont); + let HText = ovr.getFontHeight(); + + yText = ((ovrh - yText) / 2) - (linesToPrint.length * HText / 2) + yText; + + if (linesToPrint.length <= 3) { + ovr.setFontAlign(0, -1); + xText = ovr.getWidth() / 2; + } else + ovr.setFontAlign(-1, -1); + + + linesToPrint.forEach((line, i) => { + ovr.drawString(line, xText, yText + HText * i); + }); + + if (msg.FirstLine != 0) { + msg.CanscrollUp = true; + drawTriangleUp(ovr); + } else + msg.CanscrollUp = false; + + if (msg.FirstLine + linesToPrint.length < msg.lines.length) { + msg.CanscrollDown = true; + drawTriangleDown(ovr); + } else + msg.CanscrollDown = false; + show(ovr); + if (!isQuiet()) Bangle.setLCDPower(1); +}; + +let getSwipeHandler = function(ovr){ + return (lr, ud) => { + if (ud == 1) { + scrollUp(ovr); + } else if (ud == -1){ + scrollDown(ovr); + } + }; +}; + +let getTouchHandler = function(ovr){ + return (_, xy) => { + if (xy.y < ovry + 40){ + next(ovr); + } + }; +}; + +let restoreHandler = function(event){ + LOG("Restore", event, backup[event]); + Bangle.removeAllListeners(event); + Bangle["#on" + event]=backup[event]; + backup[event] = undefined; +}; + +let backupHandler = function(event){ + if (backupDone) return; // do not backup, overlay is already up + backup[event] = Bangle["#on" + event]; + LOG("Backed up", backup[event]); + Bangle.removeAllListeners(event); +}; + +let cleanup = function(){ + if (lockListener) { + Bangle.removeListener("lock", lockListener); + lockListener = undefined; + } + restoreHandler("touch"); + restoreHandler("swipe"); + restoreHandler("drag"); + + Bangle.setLCDOverlay(); + backupDone = false; + ovr = undefined; + quiet = undefined; +}; + +let backup = {}; + +let backupDone = false; + +let main = function(ovr, event) { + LOG("Main", event, settings); + + if (!lockListener) { + lockListener = function (){ + drawBorder(ovr); + }; + Bangle.on('lock', lockListener); + } + backupHandler("touch"); + backupHandler("swipe"); + backupHandler("drag"); + if (!backupDone){ + Bangle.on('touch', getTouchHandler(ovr)); + Bangle.on('swipe', getSwipeHandler(ovr)); + } + backupDone=true; + + if (event !== undefined){ + drawBorder(ovr); + manageEvent(ovr, event); + } else { + LOG("No event given"); + cleanup(); + } +}; + +let ovr; + +exports.pushMessage = function(event) { + if( event.id=="music") return require_real("messages").pushMessage(event); + + bpp = 4; + if (process.memory().free < 2000) bpp = 1; + + if (!ovr) { + ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, { + msb: true + }); + } else { + ovr.clear(); + } + + g = ovr; + + if (bpp == 4) + ovr.theme = g.theme; + else + ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 }; + + main(ovr, event); + + g = _g; +}; + + +//Call original message library +exports.clearAll = function() { return require_real("messages").clearAll();}; +exports.getMessages = function() { return require_real("messages").getMessages();}; +exports.status = function() { return require_real("messages").status();}; +exports.buzz = function() { return require_real("messages").buzz(msgSrc);}; +exports.stopBuzz = function() { return require_real("messages").stopBuzz();}; diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json new file mode 100644 index 000000000..6a3f953d8 --- /dev/null +++ b/apps/messagesoverlay/metadata.json @@ -0,0 +1,35 @@ +{ + "id": "messagesoverlay", + "name": "Messages Overlay", + "version": "0.03", + "description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)", + "icon": "app.png", + "type": "bootloader", + "tags": "tool,system", + "supports": [ + "BANGLEJS2" + ], + "dependencies": { + "messageicons": "module", + "messages": "app" + }, + "readme": "README.md", + "storage": [ + { + "name": "messagesoverlay", + "url": "lib.js" + }, + { + "name": "messagesoverlay.boot.js", + "url": "boot.js" + } + ], + "screenshots": [ + { + "url": "screen_call.png" + }, + { + "url": "screen_message.png" + } + ] +} diff --git a/apps/messagesoverlay/screen_call.png b/apps/messagesoverlay/screen_call.png new file mode 100644 index 000000000..45326b37e Binary files /dev/null and b/apps/messagesoverlay/screen_call.png differ diff --git a/apps/messagesoverlay/screen_message.png b/apps/messagesoverlay/screen_message.png new file mode 100644 index 000000000..f3cc3b6e5 Binary files /dev/null and b/apps/messagesoverlay/screen_message.png differ diff --git a/apps/mitherm/README.md b/apps/mitherm/README.md index cdf3daa61..496c2f234 100644 --- a/apps/mitherm/README.md +++ b/apps/mitherm/README.md @@ -1,6 +1,15 @@ +# MiTherm + +![](screenshot.png) + Reads BLE advertisement data from Xiaomi temperature/humidity sensors running the `pvvx` custom firmware (https://github.com/pvvx/ATC_MiThermometer). +NOTE: If you flash your Mi Temperature device to the `pvvx` firmware, +please ensure you disconnect it from your bluetooth source before +attempting to run the app on the Bangle. + + ## Features * Display temperature @@ -20,3 +29,4 @@ Reads BLE advertisement data from Xiaomi temperature/humidity sensors running th * Configurable scan length (currently 30s) * Alerts when temperature outside defined limits (with a widget or bootcode to work when app is inactive) + diff --git a/apps/mitherm/metadata.json b/apps/mitherm/metadata.json index a8da6fd26..b1005c5ac 100644 --- a/apps/mitherm/metadata.json +++ b/apps/mitherm/metadata.json @@ -5,9 +5,11 @@ "version": "0.01", "description": "Reads and displays data from Xiaomi temperature/humidity sensors running custom firmware", "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], "tags": "xiaomi,mi,ble,bluetooth,thermometer,humidity", "readme": "README.md", "supports": ["BANGLEJS", "BANGLEJS2"], + "dependencies": {"textinput":"type"}, "storage": [ {"name":"mitherm.app.js","url":"app.js"}, {"name":"mitherm.img","url":"app-icon.js","evaluate":true} diff --git a/apps/mitherm/screenshot.png b/apps/mitherm/screenshot.png new file mode 100644 index 000000000..87e1c9e20 Binary files /dev/null and b/apps/mitherm/screenshot.png differ diff --git a/apps/mixdiganclock/ChangeLog b/apps/mixdiganclock/ChangeLog index 13f27c06c..8ef626dd7 100644 --- a/apps/mixdiganclock/ChangeLog +++ b/apps/mixdiganclock/ChangeLog @@ -1,4 +1,5 @@ 0.01: fork from miclock, Added compatib with b widgets, devices(dynamic x,y) and themes(dynamic colors) 0.02: Code refactored, change colors in real time 0.03: Hour point size can be modified on real time. -0.04: Background color can be changed on real time, it will change also the list of fg color \ No newline at end of file +0.04: Background color can be changed on real time, it will change also the list of fg color +0.05: Main button can access to Messages, small improvements \ No newline at end of file diff --git a/apps/mixdiganclock/README.md b/apps/mixdiganclock/README.md index d5efbc126..7b9fb2ff0 100644 --- a/apps/mixdiganclock/README.md +++ b/apps/mixdiganclock/README.md @@ -9,6 +9,13 @@ Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators Bangle.JS1 ![](photo_mixdigan_bjs1.jpg) + +UI for bangleJS1 +![](bangle.js_UI.png) + +UI for bangleJS2 +![](bangle.js2_UI.png) + Screenshot emulator (BJS2) ![](ss_mixdigan_ems2.png) @@ -36,19 +43,26 @@ Open and see ## Features -Compatibility with devices -Dynamic positions x,y for different devices -Dynamic Colours (FG and BG) and positions -Support for bottom widgets + - Compatibility with different smartwatch models + - Dynamic positions x,y for different devices + - Dynamic Colours (FG and BG) and positions + - Realtime configuration + - Support for bottom widgets + - Shortcut to messages app -## Controls +## Controls/UI -Exit : BTN2 (BJS1) -Exit/launcher : left area -Change FG Color : right area -Increase/Decrease Hour circle/Points : swipe right -Change BG Color : swipe left + - BTN1 (BJS2): "Launcher" / open "Messages" + - BTN2 (BJS1): "Launcher" / open "Messages" + - BTN1 (BJS1): Change FG Color + - BTN3 (BJS1): Change BG Color + - Left area: Back/Exit/launcher + - Right area: Change FG Color + - Swipe left: Change BG Color + - Swipe right: Increase/Decrease Hour circle/Points + + - BTN3 (long press)(BJS1): default Exit/kill app ## Coming soon diff --git a/apps/mixdiganclock/bangle.js2_UI.png b/apps/mixdiganclock/bangle.js2_UI.png new file mode 100644 index 000000000..0c0f37fb5 Binary files /dev/null and b/apps/mixdiganclock/bangle.js2_UI.png differ diff --git a/apps/mixdiganclock/bangle.js_UI.png b/apps/mixdiganclock/bangle.js_UI.png new file mode 100644 index 000000000..d4e5df6d2 Binary files /dev/null and b/apps/mixdiganclock/bangle.js_UI.png differ diff --git a/apps/mixdiganclock/metadata.json b/apps/mixdiganclock/metadata.json index daf0924ec..2f6d049eb 100644 --- a/apps/mixdiganclock/metadata.json +++ b/apps/mixdiganclock/metadata.json @@ -1,8 +1,8 @@ { "id": "mixdiganclock", "name": "Mix Dig&Anal Clock", - "version": "0.04", - "description": "A dual Analog and Digital Clock, that gives the user the power to improve the UI/UX on realtime. Too much sun and polarized sunglasses should not be a problem to read the time anymore if you change colors or aspect. Originally based in Mixed Clock ", + "version": "0.05", + "description": "A dual Analog, Digital Clock, that gives the user the power to improve the aspect on realtime. Too much sun and/or polarized sunglasses? Not be a problem anymore to see the time. Originally based in Mixed Clock ", "icon": "mixdiganclock.png", "type": "clock", "tags": "clock", diff --git a/apps/mixdiganclock/mixdiganclock.app.js b/apps/mixdiganclock/mixdiganclock.app.js index 940e1a7e2..da2c22f18 100644 --- a/apps/mixdiganclock/mixdiganclock.app.js +++ b/apps/mixdiganclock/mixdiganclock.app.js @@ -2,7 +2,7 @@ compatible with BJS1, BJS2 and bottom widgets */ var locale = require("locale"); -var v_mode_debug=0 //, 0=no, 1 min, 2 prone detail +var v_mode_debug=0; //, 0=no, 1 min, 2 prone detail var v_model=process.env.BOARD; var v_array4colors=0; // 0 undef, 1 forDark, 2 forLight var a_colors=[]; //new Array(), values will depend on b_isarray4dark @@ -23,8 +23,25 @@ var v_center_y; g.clear(); //ONLY 1ST TIME //show the exit button -Bangle.setUI("clock"); //implies center button for launcher +function setMainBtn() { +//if messages app installed shortcut otherwise default access to launcher +if (require("Storage").read("messagegui.app.js")===undefined) +{ + if (require("Storage").read("messagelist.app.js")===undefined) Bangle.setUI("clock"); // implies btn2(js1) btn(js2)- launcher + else if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') setWatch(function (){load("messagelist.app.js");}, BTN2, { repeat: true }); + else setWatch(function (){load("messagelist.app.js");}, BTN1, { repeat: true }); +} +else if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') setWatch(function (){load("messagegui.app.js");}, BTN2, { repeat: true }); + else setWatch(function (){load("messagegui.app.js");}, BTN1, { repeat: true }); +} + +function setBJS1Btns() { + //only for bjs1, btns complementary to touch + setWatch(changeFGcolor, BTN1, { repeat: true }); + setWatch(changeBGcolor, BTN3, { repeat: true }); + } + Bangle.loadWidgets(); function getColorArray4BG() { //1st=0 1st compatible color (dark/light theme) @@ -211,6 +228,42 @@ function drawMixedClock() { g.setColor(a_colors[v_color1]); g.fillCircle(v_center_x, v_center_y, Radius.center); } + +function changeFGcolor(){ + //change color but monocolor watchface + if (v_mode_debug>0) console.log("v_count_col/total: "+v_count_col+"/"+a_colors.length); + if (v_count_col0) console.log("paint on color: "+v_count_col); + drawStaticRing(a_colors[v_color1]); + drawDailyTxt(); +} +function changeBGcolor(){ + //pend to refactor + //if black bg + if (v_array4colors==1) v_array4colors=2; // then white + else if (v_array4colors==2) v_array4colors=1; //if white bg + getColorArray4BG(); //set new list of colors + g.setBgColor( v_color_erase);// 0 white, 1 black + ClearScreen(); + //g.clear();//impact on widgets + drawStaticRing(a_colors[v_color1]); + drawDailyTxt(); //1st time + drawMixedClock(); +} +function changeRadiusRing(){ + drawStaticRing(v_color_erase); + if (Radius.circleH<13) Radius.circleH++; + else Radius.circleH=2; + if (v_mode_debug>0) console.log("radio: "+Radius.circleH); + drawStaticRing(a_colors[v_color1]); +} + function UserInput(){ Bangle.on('touch', function(button){ switch(button){ @@ -218,55 +271,21 @@ function UserInput(){ Bangle.showLauncher(); break; case 2: - //change color but monocolor watchface - if (v_mode_debug>0) console.log("v_count_col/total: "+v_count_col+"/"+a_colors.length); - if (v_count_col0) console.log("paint on color: "+v_count_col); - drawStaticRing(a_colors[v_color1]); - drawDailyTxt(); + changeFGcolor(); break; case 3: //console.log("Touch 3 aka 1+2 not for BJS1 emul");//center 1+2 break; } }); - //changing dimensions + //changing dimensions right Bangle.on('swipe', dir => { if(dir == 1) { - drawStaticRing(v_color_erase); - if (Radius.circleH<13) Radius.circleH++ - else Radius.circleH=2; - if (v_mode_debug>0) console.log("radio: "+Radius.circleH); - drawStaticRing(a_colors[v_color1]); - } - else { //swipe left, pend to refactor - if (v_array4colors==1) { //if black bg - v_array4colors=2; // then white - getColorArray4BG(); //set new list of colors - g.setBgColor( v_color_erase);// 0 white, 1 black - ClearScreen(); - //g.clear();//impact on widgets - drawStaticRing(a_colors[v_color1]); - drawDailyTxt(); //1st time - drawMixedClock(); - } else if (v_array4colors==2) { //if white bg - v_array4colors=1; - getColorArray4BG(); - console.log(a_colors[1]); - g.setBgColor(v_color_erase);// 0 white, 1 black - //g.clear(); - ClearScreen(); - drawStaticRing(a_colors[v_color1]); - drawDailyTxt(); //1st time - drawMixedClock(); //or just wait? - } + changeRadiusRing(); } + else { //swipe left,bg color + changeBGcolor(); + } }); } Bangle.on('lcdPower', function(on) { @@ -274,9 +293,11 @@ Bangle.on('lcdPower', function(on) { drawMixedClock(); }); +setMainBtn(); //assign btn to messages when installed setVariables(); Bangle.drawWidgets(); UserInput(); +if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') setBJS1Btns(); //assign btn1 and btn3 setInterval(drawMixedClock, 30000);//not realtime update drawStaticRing(a_colors[v_color1]); drawDailyTxt(); diff --git a/apps/mixdiganclock/mixdiganclock.info b/apps/mixdiganclock/mixdiganclock.info deleted file mode 100644 index 9675614c1..000000000 --- a/apps/mixdiganclock/mixdiganclock.info +++ /dev/null @@ -1 +0,0 @@ -{"id":"mixdiganclock","name":"Mix Dig&Anal","type":"clock","src":"mixdiganclock.app.js","icon":"mixdiganclock.img","version":"0.04","tags":"clock","files":"mixdiganclock.info,mixdiganclock.app.js,mixdiganclock.img"} \ No newline at end of file diff --git a/apps/mixdiganclock/photo_mixdigan_bjs1.jpg b/apps/mixdiganclock/photo_mixdigan_bjs1.jpg index 023183a59..a3d590647 100644 Binary files a/apps/mixdiganclock/photo_mixdigan_bjs1.jpg and b/apps/mixdiganclock/photo_mixdigan_bjs1.jpg differ diff --git a/apps/mtnclock/ChangeLog b/apps/mtnclock/ChangeLog new file mode 100644 index 000000000..98cd0cc94 --- /dev/null +++ b/apps/mtnclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Get weather from weather.json +0.03: Address unexpected undefined when reading weather.json \ No newline at end of file diff --git a/apps/mtnclock/README.md b/apps/mtnclock/README.md index 58538509d..441754b83 100644 --- a/apps/mtnclock/README.md +++ b/apps/mtnclock/README.md @@ -4,7 +4,7 @@ Based on the Pebble watchface Weather Land. Mountain Pass Clock changes depending on time (day/night) and weather conditions. -This clock requires Gadgetbridge and an app that Gadgetbridge can use to get the current weather from OpenWeatherMap (e.g. Weather Notification). To set up Gadgetbridge and weather, see https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather. +This clock requires Gadgetbridge and an app that Gadgetbridge can use to get the current weather from OpenWeatherMap (e.g. Weather Notification), or a Bangle app that will update weather.json such as OWM Weather. To set up Gadgetbridge and weather, see https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather. The scene will change according to the following OpenWeatherMap conditions: clear, cloudy, overcast, lightning, drizzle, rain, fog and snow. Each weather condition has night/day scenes. diff --git a/apps/mtnclock/app.js b/apps/mtnclock/app.js index 28ba25882..a65382dec 100644 --- a/apps/mtnclock/app.js +++ b/apps/mtnclock/app.js @@ -323,11 +323,28 @@ function setWeather() { draw(a); } +function readWeather() { + var weatherJson = require("Storage").readJSON('weather.json', 1); + // save updated weather data if available and it has been an hour since last updated + if (weatherJson && weatherJson.weather && weatherJson.weather.time && (data.time === undefined || (data.time + 3600000) < weatherJson.weather.time)) { + data = { + time: weatherJson.weather.time, + temp: weatherJson.weather.temp, + code: weatherJson.weather.code + }; + require("Storage").writeJSON('mtnclock.json', data); + } +} + const _GB = global.GB; global.GB = (event) => { if (event.t==="weather") { - data = event; - require("Storage").write('mtnclock.json', event); + data = { + temp: event.temp, + code: event.code, + time: Date.now() + }; + require("Storage").writeJSON('mtnclock.json', data); setWeather(); } if (_GB) setTimeout(_GB, 0, event); @@ -340,11 +357,13 @@ function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; + readWeather(); setWeather(); queueDraw(); }, 60000 - (Date.now() % 60000)); } queueDraw(); +readWeather(); setWeather(); Bangle.setUI("clock"); diff --git a/apps/mtnclock/metadata.json b/apps/mtnclock/metadata.json index a3a173069..2d3d0a02f 100644 --- a/apps/mtnclock/metadata.json +++ b/apps/mtnclock/metadata.json @@ -2,7 +2,7 @@ "id": "mtnclock", "name": "Mountain Pass Clock", "shortName": "Mtn Clock", - "version": "0.01", + "version": "0.03", "description": "A clock that changes scenery based on time and weather.", "readme":"README.md", "icon": "app.png", diff --git a/apps/multidice/ChangeLog b/apps/multidice/ChangeLog new file mode 100644 index 000000000..cb0cce2aa --- /dev/null +++ b/apps/multidice/ChangeLog @@ -0,0 +1,32 @@ +0.90: got most of the features done, lacking some polish and real-hardware testing +1.00: overhauled the whole app, made some margins larger to be easier to tap on +1.01: fixed bug that caused rolled dice on the right of screen to be writ off-screen +1.02: added vibration when dice is rolled +1.03: vibration caused the accelerometer to never stop +1.04: decreased vibration strength +1.05: toggled the acceleration handler to prevent infinite buzz loop +1.06: increased vibration again +1.07: IDK how to use promises properly +1.08: still trying to fix the lack of vibrations +1.09: hopefully now it's fixed? +1.10: not having web bluetooth to debug is a PAIN +1.11: decreased vibration time, decreased accel requirement +1.12: issue with app calling roll function too many times at startup +1.13: added a delay after the buzzer stops to prevent multi-rolling +1.14: made the delay needlessly long to see if it even does anything +1.15: moved accel & vibration commands to the accelHandler function +1.16: enabled button usage & temporarily disabled acceleration +1.17: made changes to when accelHandler gets overwritten, temporarily disabled button usage +1.18: decided to keep around the button even while testing, disabled all safety round the accelHandler self-triggering +1.19: added longer delay before resetting accelHandler +1.20: removed all traces of accel b/c I've given up +1.21: added a drawWidgets command to see if I have the padding right +1.22: ok the buzzing *might* work now +1.23: forgot to resolve the promise +1.24: fixed dumb errors +1.25: god I hope this works +1.26: trying to add timeout after it's done buzzing... again +1.27: OH GOD IT FINALLY WORKS +1.28: increased vibration strength, added some comments, & some QOL +1.29: changed image +1.30: changed image, again diff --git a/apps/multidice/README.md b/apps/multidice/README.md new file mode 100644 index 000000000..72a2d8af5 --- /dev/null +++ b/apps/multidice/README.md @@ -0,0 +1,20 @@ +# multiple dice roller + +roll anywhere from 1-8 dice at the same time. + +## Usage + +![startup.png](startup.png) +On the menu screen: tap on the dice to change what variant is selected, & shake/or press BTN to roll the dice +![single_rolled.png](single_rolled.png) +On the dice screen: tap anywhere on the screen to go back to the menu, or shake/or press BTN to roll the dice + +## Features + +roll anywhere from 1-8 dice (d4, d6, d8, d10, d12, d20, & d percentile). You can select multiple different dice at the same time +![many_selected.png](many_selected.png) +![many_rolled.png](many_rolled.png) + +## Controls + +App uses touchscreen to cycle through different dice, and accelerometer/BTN to roll them diff --git a/apps/multidice/app-icon.js b/apps/multidice/app-icon.js new file mode 100644 index 000000000..88feb83cd --- /dev/null +++ b/apps/multidice/app-icon.js @@ -0,0 +1 @@ +atob("MDABAAAAA8AAAAAAB+AAf//+DDAAwAACGBgAwAADMAwAwAADYAYAgAAAwYMAgAABg+GAgAADBiDAgDwGBiBggH4MAmAwgMMYA8AYgIEwAAAMgIFg8AeGgIHBkAyDgMNBGAjDgH5BGAjDgDxh8A+DgAAwYAMGgAAYAYAMgAAMA+AYwAAGBiAwwAADBiBgwAADgmDAf//+w8Gwf///4AM8wAADMAYEzwADGA3En4ABDBvkmYABBjIkmYABI+IkH4ABPAPkjwABHAHEgDwBAHAEAH4BAPgEgGYBAIgEgGYBAIgEgH4BAPgEgDwBAHAEgADxHAHEgAH5PgPkgAGZIgIkgAGZIgIkgAH5PgPkwADzHAHEwAADAAAEcAAPwAAcP//8///w") diff --git a/apps/multidice/app.js b/apps/multidice/app.js new file mode 100644 index 000000000..53f67e21e --- /dev/null +++ b/apps/multidice/app.js @@ -0,0 +1,185 @@ +var menu = true; // default to have the selection menu open +const DICE_ARRAY = [0, 4, 6, 8, 10, 12, 20, 100]; // 0 means nothing selected +const SELECTION_ARRAY = [6, 0, 0, 0, 0, 0, 0, 0]; // default to selecting a single d20 + +// function to draw the selection menu +function drawMenu() { + + stringArr = new Array ("", "", "", "", "", "", "", ""); + for (i = 0; i < 8; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + stringArr [i] = "" + DICE_ARRAY [SELECTION_ARRAY [i]]; + } else { + + stringArr [i] = " . "; // more clearly defines where the user can tap + } + } + + g.clear(); + g.setFont ("Vector", 40); + + // " ".slice(-3) left-pads all numbers with spaces + g.drawString ((" " + stringArr [0]).slice (-3), 5, 10); + g.drawString ((" " + stringArr [1]).slice (-3), 5, 50); + g.drawString ((" " + stringArr [2]).slice (-3), 5, 90); + g.drawString ((" " + stringArr [3]).slice (-3), 5, 130); + g.drawString ((" " + stringArr [4]).slice (-3), 96, 10); + g.drawString ((" " + stringArr [5]).slice (-3), 96, 50); + g.drawString ((" " + stringArr [6]).slice (-3), 96, 90); + g.drawString ((" " + stringArr [7]).slice (-3), 96, 130); +} + +// function to change what dice is selected in the menu +function touchHandler (button, xy) { + + if (! menu) { // if menu isn't open, open it & return + + menu = true; + drawMenu(); + return; + } + + if (xy.x <= 87) { // left + + if (xy.y <= 43) { // first + + selection = 0; + } else if (xy.y <= 87) { // second + + selection = 1; + } else if (xy.y <= 131) { // third + + selection = 2; + } else { // fourth + + selection = 3; + } + } else { // right + + if (xy.y <= 43) { // first + + selection = 4; + } else if (xy.y <= 87) { // second + + selection = 5; + } else if (xy.y <= 131) { // third + + selection = 6; + } else { // fourth + + selection = 7; + } + } + + if (SELECTION_ARRAY [selection] == SELECTION_ARRAY.length - 1) { // if last dice is selected, go back to first + + SELECTION_ARRAY [selection] = 0; + } else { + + SELECTION_ARRAY [selection] += 1; + } + + drawMenu(); +} + +function accelHandler (xyz) { + + if (xyz.diff >= 0.3) { + + menu = false; + mutex (rollDice).catch (() => { + + return; // not necessary, but prevents spamming the logs + }); + } +} + +// returns a resolved promise if no other mutex call is active, all further ones return a rejected one +let lock = false; +function mutex (functionRef) { + + if (lock) { + + return Promise.reject (new Error ("mutex is busy")); + } + + lock = true; + return new Promise ((resolve, reject) => { + + functionRef().then ((result) => { + + lock = false; + resolve (result); + }).catch ((error) => { + + lock = false; + reject (error); + }); + }); +} + +// function to roll all selected dice, and display them +function rollDice() { + + resultsArr = new Uint8Array (8); + for (i = 0; i < 8; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + resultsArr [i] = random (DICE_ARRAY [SELECTION_ARRAY [i]]); + } + } + + g.clear(); + g.setFont ("Vector", 40); + + for (i = 0; i < 4; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + g.drawString ((" " + resultsArr [i]).slice (-3), 5, 10 + 40 * i); + } + } + + for (i = 4; i < 8; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + g.drawString ((" " + resultsArr [i]).slice (-3), 96, 10 + 40 * (i - 4)); + } + } + + return vibrate(); +} + +// triggers the vibration, then pauses before returning +function vibrate() { + + return new Promise ((resolve, reject) => { + + return Bangle.buzz (50, 1).then ((value) => { + + setTimeout (() => { + + resolve (value); + }, 200); + }); + }); +} + +// returns a integer [1, max] +function random (max) { + + return Math.round (Math.random() * (max - 1) + 1); +} + +drawMenu(); +Bangle.on ('touch', touchHandler); +Bangle.on ('accel', accelHandler); +setWatch (function() { + + menu = false; + mutex (rollDice); +}, BTN, {repeat: true, edge: "falling", debounce: 10}); diff --git a/apps/multidice/app.png b/apps/multidice/app.png new file mode 100644 index 000000000..75ee0514a Binary files /dev/null and b/apps/multidice/app.png differ diff --git a/apps/multidice/many_rolled.png b/apps/multidice/many_rolled.png new file mode 100644 index 000000000..4cf34e9c7 Binary files /dev/null and b/apps/multidice/many_rolled.png differ diff --git a/apps/multidice/many_selected.png b/apps/multidice/many_selected.png new file mode 100644 index 000000000..137e2c363 Binary files /dev/null and b/apps/multidice/many_selected.png differ diff --git a/apps/multidice/metadata.json b/apps/multidice/metadata.json new file mode 100644 index 000000000..304c789e4 --- /dev/null +++ b/apps/multidice/metadata.json @@ -0,0 +1,15 @@ +{ "id": "multidice", + "name": "multiple dice roller", + "shortName":"multidice", + "version":"1.30", + "description": "roll anywhere from 1-8 dice at the same time", + "icon": "app.png", + "tags": "tool,game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"multidice.app.js","url":"app.js"}, + {"name":"multidice.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/multidice/single_rolled.png b/apps/multidice/single_rolled.png new file mode 100644 index 000000000..3ce67ab74 Binary files /dev/null and b/apps/multidice/single_rolled.png differ diff --git a/apps/multidice/startup.png b/apps/multidice/startup.png new file mode 100644 index 000000000..587281e5c Binary files /dev/null and b/apps/multidice/startup.png differ diff --git a/apps/neonx/ChangeLog b/apps/neonx/ChangeLog index e78686a00..b055d6a15 100644 --- a/apps/neonx/ChangeLog +++ b/apps/neonx/ChangeLog @@ -3,4 +3,5 @@ 0.03: Optional show lock status via color 0.04: Ensure that widgets are always hidden in fullscreen mode 0.05: Better lock/unlock animation -0.06: Use widget_utils. +0.06: Use widget_utils +0.07: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/neonx/metadata.json b/apps/neonx/metadata.json index c273cb05a..edd0e76b8 100644 --- a/apps/neonx/metadata.json +++ b/apps/neonx/metadata.json @@ -2,7 +2,7 @@ "id": "neonx", "name": "Neon X & IO X Clock", "shortName": "Neon X Clock", - "version": "0.06", + "version": "0.07", "description": "Pebble Neon X & Neon IO X for Bangle.js", "icon": "neonx.png", "type": "clock", diff --git a/apps/neonx/neonx.settings.js b/apps/neonx/neonx.settings.js index 68e156dae..8edaf5c50 100644 --- a/apps/neonx/neonx.settings.js +++ b/apps/neonx/neonx.settings.js @@ -25,11 +25,9 @@ "" : { "title":"Neon X & IO"}, "< Back": back, "Neon IO X": { - value: 0 | neonXSettings.io, - min: 0, max: 1, - format: v => v ? "On" : "Off", + value: !!neonXSettings.io, onchange: v => { - neonXSettings.io = v; + neonXSettings.io = v?1:0; updateSettings(); } }, @@ -43,27 +41,23 @@ } }, "Date on touch": { - value: 0 | neonXSettings.showDate, - min: 0, max: 1, - format: v => v ? "On" : "Off", + value: !!neonXSettings.showDate, onchange: v => { - neonXSettings.showDate = v; + neonXSettings.showDate = v?1:0; updateSettings(); } }, 'Fullscreen': { - value: false | neonXSettings.fullscreen, - format: () => (neonXSettings.fullscreen ? 'Yes' : 'No'), - onchange: () => { - neonXSettings.fullscreen = !neonXSettings.fullscreen; + value: !!neonXSettings.fullscreen, + onchange: v => { + neonXSettings.fullscreen = v; updateSettings(); }, }, 'Show lock': { - value: false | neonXSettings.showLock, - format: () => (neonXSettings.showLock ? 'Yes' : 'No'), - onchange: () => { - neonXSettings.showLock = !neonXSettings.showLock; + value: !!neonXSettings.showLock, + onchange: v => { + neonXSettings.showLock = v; updateSettings(); }, }, diff --git a/apps/notanalog/ChangeLog b/apps/notanalog/ChangeLog index 094125f52..e011c4ae1 100644 --- a/apps/notanalog/ChangeLog +++ b/apps/notanalog/ChangeLog @@ -4,3 +4,4 @@ 0.04: Use alarm for timer instead of own alarm implementation. 0.05: Use internal step counter if no widget is available. 0.06: Use widget_utils. +0.07: Respect system setting for 12h or 24h time diff --git a/apps/notanalog/metadata.json b/apps/notanalog/metadata.json index 319d396a9..851e95ec8 100644 --- a/apps/notanalog/metadata.json +++ b/apps/notanalog/metadata.json @@ -3,7 +3,7 @@ "name": "Not Analog", "shortName":"Not Analog", "icon": "notanalog.png", - "version":"0.06", + "version":"0.07", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "An analog watch face for people that can not read analog watch faces.", diff --git a/apps/notanalog/notanalog.app.js b/apps/notanalog/notanalog.app.js index 29fb1730f..b37c34721 100644 --- a/apps/notanalog/notanalog.app.js +++ b/apps/notanalog/notanalog.app.js @@ -13,6 +13,9 @@ let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { settings[key] = saved_settings[key] } +const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})[ + "12hour" +]; /* * Set some important constants such as width, height and center @@ -199,6 +202,9 @@ function drawTime(){ // Hour var h = state.currentDate.getHours(); + if (is12Hour && h > 12) { + h = h - 12; + } var h1 = parseInt(h / 10); var h2 = h < 10 ? h : h - h1*10; drawTextCleared(h1, cx, posY+8); diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index 57818c180..d6703a96b 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -7,4 +7,5 @@ 0.07: Add date on touch and some improvements (see settings and readme) 0.08: Add new draw styles, tidy up draw functionality 0.09: Tweak for faster rendering -0.10: Enhance for use with Bangle2, insert new draw mode 'thickfill' \ No newline at end of file +0.10: Enhance for use with Bangle2, insert new draw mode 'thickfill' +0.11: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/numerals/metadata.json b/apps/numerals/metadata.json index 6ba850d86..6a1adc810 100644 --- a/apps/numerals/metadata.json +++ b/apps/numerals/metadata.json @@ -2,7 +2,7 @@ "id": "numerals", "name": "Numerals Clock", "shortName": "Numerals Clock", - "version": "0.10", + "version": "0.11", "description": "A simple big numerals clock", "icon": "numerals.png", "type": "clock", diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js index ae321322a..b4d5d4286 100644 --- a/apps/numerals/numerals.settings.js +++ b/apps/numerals/numerals.settings.js @@ -30,10 +30,8 @@ onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} }, "Date on touch": { - value: 0|numeralsSettings.showDate, - min:0,max:1, - format: v=>v?"On":"Off", - onchange: v=> { numeralsSettings.showDate=v; updateSettings();} + value: !!numeralsSettings.showDate, + onchange: v=> { numeralsSettings.showDate=v?1:0; updateSettings();} }, "< back": back }; diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 7f788c139..a256b459c 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -16,3 +16,4 @@ Support for zooming in on map Satellite count moved to widget bar to leave more room for the map 0.15: Make track drawing an option (default off) +0.16: Draw waypoints, too. diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md index f19b13bd1..bf247c7b7 100644 --- a/apps/openstmap/README.md +++ b/apps/openstmap/README.md @@ -17,20 +17,20 @@ To add a map: * Scroll and zoom to the area of interest or use the Search button in the top left * Now choose the size you want to upload (Small/Medium/etc) * On Bangle.js 1 you can choose if you want a 3 bits per pixel map (this is lower -quality but uploads faster and takes less space). On Bangle.js 2 you only have a 3bpp -display so can only use 3bpp. +quality, but uploads faster and takes less space). Bangle.js 2 is limited to 3bpp. * Click `Get Map`, and a preview will be displayed. If you need to adjust the area you can change settings, move the map around, and click `Get Map` again. * When you're ready, click `Upload` ## Bangle.js App -The Bangle.js app allows you to view a map - it also turns the GPS on and marks -the path that you've been travelling (if enabled). +The Bangle.js app allows you to view a map. It also turns the GPS on +and marks the path that you've been travelling (if enabled), and +displays waypoints in the watch (if dependencies exist). * Drag on the screen to move the map -* Press the button to bring up a menu, where you can zoom, go to GPS location -, put the map back in its default location, or choose whether to draw the currently +* Press the button to bring up a menu, where you can zoom, go to GPS location, +put the map back in its default location, or choose whether to draw the currently recording GPS track (from the `Recorder` app). **Note:** If enabled, drawing the currently recorded GPS track can take a second diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 89e2d2ddb..a5130d23e 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -10,6 +10,7 @@ var settings = require("Storage").readJSON("openstmap.json",1)||{}; function redraw() { g.setClipRect(R.x,R.y,R.x2,R.y2); m.draw(); + drawPOI(); drawMarker(); // if track drawing is enabled... if (settings.drawTrack) { @@ -25,6 +26,26 @@ function redraw() { g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); } +// Draw the POIs +function drawPOI() { + try { + var waypoints = require("waypoints").load(); + } catch (ex) { + // Waypoints module not available. + return; + } + g.setFont("Vector", 18); + waypoints.forEach((wp, idx) => { + var p = m.latLonToXY(wp.lat, wp.lon); + var sz = 2; + g.setColor(0,0,0); + g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); + g.setColor(0,0,0); + g.drawString(wp.name, p.x, p.y); + print(wp.name); + }) +} + // Draw the marker for where we are function drawMarker() { if (!fix.fix) return; diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 819dc4122..4419cd411 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.15", + "version": "0.16", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "readme": "README.md", "icon": "app.png", diff --git a/apps/pokeclk/screenshot (1).png b/apps/pokeclk/screenshot.png similarity index 100% rename from apps/pokeclk/screenshot (1).png rename to apps/pokeclk/screenshot.png diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog index a83e8c676..ee31195b2 100644 --- a/apps/powermanager/ChangeLog +++ b/apps/powermanager/ChangeLog @@ -3,3 +3,9 @@ 0.03: Use default Bangle formatter for booleans 0.04: Remove calibration with current voltage (Calibrate->Auto) as it is now handled by settings app Allow automatic calibration on every charge longer than 3 hours +0.05: Add back button to settings menu. +0.06: Allow logging of some things using power + Add widget for live monitoring of power use +0.07: Convert Yes/No On/Off in settings to checkboxes +0.08: Fix the wrapping of intervals/timeouts with parameters + Fix the widget drawing if widgets are hidden and Bangle.setLCDBrightness is called \ No newline at end of file diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md index 88b3c370a..804e986e7 100644 --- a/apps/powermanager/README.md +++ b/apps/powermanager/README.md @@ -7,6 +7,26 @@ Features: * Force monotonic battery percentage or voltage * Automatic calibration on charging uninterrupted longer than 3 hours (reloads of the watch reset the timer). + +## Widget + +The widget shows an approximate current power use. There is a power gauge showing the estimation of the currently used power and the currently active sensor with the biggest power draw. +G for GPS, H for pulse sensor and C for compass. + +## Logging + +You can switch on logging in the options to diagnose unexpected power use. Currently the logging can capture the code running from timeouts and intervals and the power up and down of some devices. The captured times are probably not perfect but should be good enough to indicate problems. + +Do not use trace logging for extended time, it uses a lot of storage and can fill up the flash quite quick. + +### TODO + +* Wrap functions given as strings to setTimeout/setInterval +* Handle eval in setTimeout/setInterval +* Track functions executed as event handlers +* Track buzzer +* Modify browser interface to estimate power use like widget does + ## Internals Battery calibration offset is set by writing `batFullVoltage` in setting.json @@ -14,6 +34,10 @@ Battery calibration offset is set by writing `batFullVoltage` in setting.json ## TODO * Optionally keep battery history and show as graph +* Capture some more stuff in logging + * Event driven code execution + * Buzzer + * Better tracking of display on time ## Creator diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js index 2bc2aaa35..f3e3f718f 100644 --- a/apps/powermanager/boot.js +++ b/apps/powermanager/boot.js @@ -3,11 +3,116 @@ require('Storage').readJSON("powermanager.default.json", true) || {}, require('Storage').readJSON("powermanager.json", true) || {} ); - + + if (settings.log) { + let logFile = require('Storage').open("powermanager.log","a"); + let def = require('Storage').readJSON("powermanager.def.json", true) || {}; + if (!def.start) def.start = Date.now(); + if (!def.deferred) def.deferred = {}; + let hw = require('Storage').readJSON("powermanager.hw.json", true) || {}; + if (!hw.start) hw.start = Date.now(); + if (!hw.power) hw.power = {}; + + const saveEvery = 1000 * 60 * 5; + const TO_WRAP = ["GPS","Compass","Barometer","HRM","LCD"]; + + let save = ()=>{ + let defExists = require("Storage").read("powermanager.def.json")!==undefined; + if (!(!defExists && def.saved)){ + def.saved = Date.now(); + require('Storage').writeJSON("powermanager.def.json", def); + } + let hwExists = require("Storage").read("powermanager.hw.json")!==undefined; + if (!(!hwExists && hw.saved)){ + hw.saved = Date.now(); + require('Storage').writeJSON("powermanager.hw.json", hw); + } + } + + setInterval(save, saveEvery); + + E.on("kill", ()=>{ + for (let c of TO_WRAP){ + if (lastPowerOn[c] && Bangle["is"+c+"On"]()){ + hw.power[c] += Date.now() - lastPowerOn[c]; + } + } + save(); + }); + + + let logPower = (type, oldstate, state, app) => { + logFile.write("p," + type + ',' + (oldstate?1:0) + ',' + (state?1:0) + ',' + app + "\n"); + }; + let logDeferred = (type, duration, source) => { + logFile.write(type + ',' + duration + ',' + source.replace(/\n/g, " ").replace(/,/g,"") + "\n"); + }; + + let lastPowerOn = {}; + + for (let c of TO_WRAP){ + let functionName = "set" + c + "Power"; + let checkName = "is" + c + "On"; + let type = c + ""; + lastPowerOn[type] = (!lastPowerOn[type] && Bangle[checkName]()) ? Date.now() : undefined; + + lastPowerOn[type] = Date.now(); + + Bangle[functionName] = ((o) => (a,b) => { + let oldstate = Bangle[checkName](); + let result = o(a,b); + if (!lastPowerOn[type] && result) { + //switched on, store time + lastPowerOn[type] = Date.now(); + } else if (lastPowerOn[type] && !result){ + //switched off + hw.power[type] += Date.now() - lastPowerOn[type]; + lastPowerOn[type] = undefined; + } + + if (settings.logDetails) logPower(type, oldstate, result, b); + return result; + })(Bangle[functionName]); + } + + let functions = {}; + let wrapDeferred = ((o,t) => (a) => { + if (a == eval || typeof a == "string") { + return o.apply(this, arguments); + } else { + let wrapped = a; + if (!a.__wrapped){ + wrapped = ()=>{ + let start = Date.now(); + let result = a.apply(undefined, arguments.slice(2)); // function arguments for deferred calls start at index 2, first is function, second is time + let end = Date.now()-start; + let f = a.toString().substring(0,100); + if (settings.logDetails) logDeferred(t, end, f); + if (!def.deferred[f]) def.deferred[f] = 0; + def.deferred[f] += end; + return result; + }; + //copy over properties of functions + for (let p in a){ + wrapped[p] = a[p]; + } + //mark function as wrapped + wrapped.__wrapped = true; + } + let newArgs = arguments.slice(); + newArgs[0] = wrapped; + return o.apply(this, newArgs); + } + }); + + global.setTimeout = wrapDeferred(global.setTimeout, "t"); + global.setInterval = wrapDeferred(global.setInterval, "i"); + } + if (settings.warnEnabled){ var chargingInterval; - function handleCharging(charging){ + let handleCharging = (charging) => { if (charging){ if (chargingInterval) clearInterval(chargingInterval); chargingInterval = setInterval(()=>{ @@ -20,12 +125,12 @@ clearInterval(chargingInterval); chargingInterval = undefined; } - } + }; Bangle.on("charging",handleCharging); handleCharging(Bangle.isCharging()); } - + if (settings.forceMonoPercentage){ var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4; var op = E.getBattery; @@ -56,4 +161,4 @@ if (!charging) chargeStart = undefined; }); } -})(); +})(); \ No newline at end of file diff --git a/apps/powermanager/default.json b/apps/powermanager/default.json index 6c929dc38..457f79610 100644 --- a/apps/powermanager/default.json +++ b/apps/powermanager/default.json @@ -2,5 +2,6 @@ "warnEnabled": false, "warn": 96, "forceMonoVoltage": false, - "forceMonoPercentage": false + "forceMonoPercentage": false, + "log": false } diff --git a/apps/powermanager/interface.html b/apps/powermanager/interface.html new file mode 100644 index 000000000..7a00af993 --- /dev/null +++ b/apps/powermanager/interface.html @@ -0,0 +1,268 @@ + + + + + +
+ + + + + diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json index 0777feee3..5487c2278 100644 --- a/apps/powermanager/metadata.json +++ b/apps/powermanager/metadata.json @@ -2,17 +2,25 @@ "id": "powermanager", "name": "Power Manager", "shortName": "Power Manager", - "version": "0.04", + "version": "0.08", "description": "Allow configuration of warnings and thresholds for battery charging and display.", "icon": "app.png", "type": "bootloader", "tags": "tool", "supports": ["BANGLEJS2"], "readme": "README.md", + "interface": "interface.html", "storage": [ {"name":"powermanager.boot.js","url":"boot.js"}, {"name":"powermanager.settings.js","url":"settings.js"}, + {"name":"powermanager.wid.js","url":"widget.js"}, {"name":"powermanager","url":"lib.js"}, {"name":"powermanager.default.json","url":"default.json"} + ], + "data": [ + {"name":"powermanager.hw.json"}, + {"name":"powermanager.def.json"}, + {"name":"powermanager.json"}, + {"name":"powermanager.log"} ] } diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js index 9eeb29e00..fa186bfac 100644 --- a/apps/powermanager/settings.js +++ b/apps/powermanager/settings.js @@ -23,6 +23,10 @@ '': { 'title': 'Power Manager' }, + "< Back" : back, + 'Widget': function() { + E.showMenu(submenu_widget); + }, 'Monotonic percentage': { value: !!settings.forceMonoPercentage, onchange: v => { @@ -40,6 +44,9 @@ }, 'Calibrate': function() { E.showMenu(submenu_calibrate); + }, + 'Logging': function() { + E.showMenu(submenu_logging); } }; @@ -99,7 +106,6 @@ }, 'Enabled': { value: !!settings.warnEnabled, - format: v => settings.warnEnabled ? "On" : "Off", onchange: v => { writeSettings("warnEnabled", v); } @@ -116,5 +122,61 @@ } }; + var submenu_logging = { + '': { + title: "Logging", + back: function() { + E.showMenu(mainmenu); + }, + }, + 'Enabled': { + value: !!settings.log, + onchange: v => { + writeSettings("log", v); + } + }, + 'Trace': { + value: !!settings.logDetails, + onchange: v => { + writeSettings("logDetails", v); + } + } + } + + var submenu_widget = { + '': { + title: "Widget", + back: function() { + E.showMenu(mainmenu); + }, + }, + 'Enabled': { + value: !!settings.widget, + onchange: v => { + writeSettings("widget", v); + } + }, + 'Refresh': { + min: 0.5, + max: 60, + step: 0.5, + value: settings.refreshUnlocked || 1, + format: v => v + "s", + onchange: v => { + writeSettings("refreshUnlocked", v); + } + }, + 'Refresh locked': { + min: 5, + max: 120, + step: 5, + value: settings.refreshLocked || 60, + format: v => v + "s", + onchange: v => { + writeSettings("refreshLocked", v); + } + } + } + E.showMenu(mainmenu); }) diff --git a/apps/powermanager/widget.js b/apps/powermanager/widget.js new file mode 100644 index 000000000..3147c40ac --- /dev/null +++ b/apps/powermanager/widget.js @@ -0,0 +1,116 @@ +/* run widgets in their own function scope so they don't interfere with +currently-running apps */ +(() => { + const s = require("Storage").readJSON("powermanager.json") || {}; + + if (!s.widget) return; + + const SYSTICKMAX = peek32(0xE000E014); + const SYSTICKWAIT = SYSTICKMAX/64000; // 64 MHz clock rate, Systick counting down on every non idle clock + + const GU = require("graphics_utils"); + const APPROX_IDLE = 0.3; + const APPROX_HIGH_BW_BLE = 0.5; + const APPROX_COMPASS = process.HWVERSION == 2 ? 5.5 : 2; + const APPROX_HRM = process.HWVERSION == 2 ? 1 : 2.5; + const APPROX_CPU = 3; + const APPROX_GPS = process.HWVERSION == 2 ? 25 : 30; + const APPROX_TOUCH = 2.5; + const APPROX_BACKLIGHT = process.HWVERSION == 2 ? 16 : 40; + const MAX = APPROX_IDLE + APPROX_HIGH_BW_BLE + APPROX_COMPASS + APPROX_HRM + APPROX_CPU + APPROX_GPS + APPROX_TOUCH + APPROX_BACKLIGHT; + + let settings = require("Storage").readJSON("setting.json") || {}; + + let brightnessSetting = settings.brightness || 1; + Bangle.setLCDBrightness = ((o) => (a) => { + brightnessSetting = a; + WIDGETS.powermanager.draw(WIDGETS.powermanager); + return o(a); + })(Bangle.setLCDBrightness); + + let brightness = () => { + return process.HWVERSION == 2 ? (brightnessSetting * APPROX_BACKLIGHT) : (brightnessSetting * 0.9 * 33 + 7); + }; + + function doDraw(w, cpu){ + g.reset(); + + let current = APPROX_IDLE + cpu * APPROX_CPU; + let mostExpensive = "P"; + + if (!Bangle.isLocked()) current += APPROX_TOUCH + brightness(); + if (Bangle.isCompassOn()) { + current += APPROX_COMPASS; + mostExpensive = "C"; + } + if (Bangle.isHRMOn()) { + current += APPROX_HRM; + mostExpensive = "H"; + } + if (Bangle.isGPSOn()) { + current += APPROX_GPS; + mostExpensive = "G"; + } + + current = current / MAX; + + g.clearRect(w.x, w.y, w.x + 23, w.y + 23); + + g.setColor(g.theme.fg); + + g.setFont6x15(); + g.setFontAlign(0, 0); + g.drawString(mostExpensive, w.x + 12, w.y + 15); + let end = 135 + (current * (405 - 135)); + g.setColor(current > 0.7 ? "#f00" : (current > 0.3 ? "#ff0" : "#0f0")); + GU.fillArc(g, w.x + 12, w.y + 12, 9, 12, GU.degreesToRadians(135), GU.degreesToRadians(end), GU.degreesToRadians(30)); + + g.setColor(g.theme.fg); + let endCpu = 135 + (cpu * (405 - 135)); + GU.fillArc(g, w.x + 12, w.y + 12, 5.5, 8, GU.degreesToRadians(135), GU.degreesToRadians(endCpu), GU.degreesToRadians(30)); + } + let sTimeout; + let s2Timeout; + let systickDiff; + function draw(w) { + let nextRefresh = Bangle.isLocked() ? ((s.refreshLocked || 60) * 1000 ): ((s.refreshUnlocked || 1) * 1000) + + if (s2Timeout) clearTimeout(s2Timeout); + if (sTimeout) clearTimeout(sTimeout); + + let t,systickNow; + sTimeout = setTimeout(()=>{ + systickNow = peek32(0xE000E018); + t = Date.now(); + }, nextRefresh - SYSTICKWAIT - 100); + + s2Timeout = setTimeout(() => { + let tLater = Date.now(); + let systickLater = peek32(0xE000E018); + systickDiff = systickLater - systickNow; + if (systickDiff < 0) systickDiff += SYSTICKMAX; + }, nextRefresh - 100); + + doDraw(w, systickDiff ? (1 - systickDiff/SYSTICKMAX) : 0); + + if (w.timeoutId !== undefined) { + clearTimeout(w.timeoutId); + } + w.timeoutId = setTimeout(() => { + w.timeoutId = undefined; + w.draw(w); + }, nextRefresh); + } + + // add your widget + WIDGETS.powermanager = { + area: "tl", + width: 24, + draw: draw + }; + + Bangle.on("lock", ()=>{WIDGETS.powermanager.draw(WIDGETS.powermanager);}); + + // conserve memory + delete settings; +})(); \ No newline at end of file diff --git a/apps/qcenter/ChangeLog b/apps/qcenter/ChangeLog index 900b9017c..436949dc9 100644 --- a/apps/qcenter/ChangeLog +++ b/apps/qcenter/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Fix fast loading on swipe to clock +0.03: Adds a setting for going back to clock on a timeout +0.04: Fix timeouts closing fast loaded apps \ No newline at end of file diff --git a/apps/qcenter/app.js b/apps/qcenter/app.js index be28db3b6..bd22c87e1 100644 --- a/apps/qcenter/app.js +++ b/apps/qcenter/app.js @@ -110,6 +110,8 @@ let layout = new Layout({ }, { remove: ()=>{ Bangle.removeListener("swipe", onSwipe); + Bangle.removeListener("touch", updateTimeout); + if (timeout) clearTimeout(timeout); delete Graphics.prototype.setFont8x12; } }); @@ -117,6 +119,16 @@ g.clear(); layout.render(); Bangle.drawWidgets(); +let timeout; +const updateTimeout = function(){ +if (settings.timeout){ + if (timeout) clearTimeout(timeout); + timeout = setTimeout(Bangle.showClock,settings.timeout*1000); + } +}; + +updateTimeout(); + // swipe event listener for exit gesture let onSwipe = function (lr, ud) { if(exitGesture == "swipeup" && ud == -1) Bangle.showClock(); @@ -126,4 +138,5 @@ let onSwipe = function (lr, ud) { } Bangle.on("swipe", onSwipe); +Bangle.on("touch", updateTimeout); } diff --git a/apps/qcenter/metadata.json b/apps/qcenter/metadata.json index a325de10f..cd3e350a4 100644 --- a/apps/qcenter/metadata.json +++ b/apps/qcenter/metadata.json @@ -2,7 +2,7 @@ "id": "qcenter", "name": "Quick Center", "shortName": "QCenter", - "version": "0.02", + "version": "0.04", "description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.", "icon": "app.png", "tags": "", diff --git a/apps/qcenter/settings.js b/apps/qcenter/settings.js index 2c97f8a5f..5d38b079e 100644 --- a/apps/qcenter/settings.js +++ b/apps/qcenter/settings.js @@ -49,6 +49,11 @@ E.showMenu(exitGestureMenu); }; + // Set Timeout + mainmenu["Timeout: " + (settings.timeout ? (settings.timeout+"s") : "Off")] = function () { + E.showMenu(timeoutMenu); + }; + //List all pinned apps, redirecting to menu with options to unpin and reorder pinnedApps.forEach((app, i) => { mainmenu[app.name] = function () { @@ -129,5 +134,22 @@ showMainMenu(); }; + // menu for setting timeout + var timeoutMenu = { + "": { title: "Timeout", back: showMainMenu } + }; + timeoutMenu["Off"] = function () { + save("timeout", 0); + showMainMenu(); + }; + let timeoutvalues = [10,20,30,60]; + for (c in timeoutvalues){ + let v = timeoutvalues[c]; + timeoutMenu[v+"s"] = function () { + save("timeout", v); + showMainMenu(); + }; + } + showMainMenu(); }); diff --git a/apps/qrcode/metadata.json b/apps/qrcode/metadata.json index 24af7b813..89c859a0c 100644 --- a/apps/qrcode/metadata.json +++ b/apps/qrcode/metadata.json @@ -1,6 +1,7 @@ { "id": "qrcode", "name": "Custom QR Code", + "shortName": "QR Code", "version": "0.06", "description": "Use this to upload a customised QR code to Bangle.js", "icon": "app.png", diff --git a/apps/quicklaunch/ChangeLog b/apps/quicklaunch/ChangeLog index 3dcb9a2d2..4b92be8cc 100644 --- a/apps/quicklaunch/ChangeLog +++ b/apps/quicklaunch/ChangeLog @@ -6,3 +6,6 @@ 0.06: Use Bangle.load() to allow 'fast switch' for apps where it's available. 0.07: Revert version 0.06. This version is the same as 0.05. 0.08: Respect appRect on touch events +0.09: Do not react if clkinfo is focused +0.10: Extend the functionality via a quicklaunch.app.js file that can be launched + with quicklaunch itself. diff --git a/apps/quicklaunch/README.md b/apps/quicklaunch/README.md new file mode 100644 index 000000000..dc4c50071 --- /dev/null +++ b/apps/quicklaunch/README.md @@ -0,0 +1,3 @@ +Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. The Quick Launch Extension (included) can be chosen as one of the apps, in turn providing fast access to up to five additional apps. Configurations can be accessed through Settings->Apps. + + diff --git a/apps/quicklaunch/app.js b/apps/quicklaunch/app.js new file mode 100644 index 000000000..7ae01ded6 --- /dev/null +++ b/apps/quicklaunch/app.js @@ -0,0 +1,42 @@ +{ + const storage = require("Storage"); + let settings = storage.readJSON("quicklaunch.json", true) || {}; + + let reset = function(name){ + if (!settings[name]) settings[name] = {"name":"(none)"}; + if (!storage.read(settings[name].src)) settings[name] = {"name":"(none)"}; + storage.write("quicklaunch.json", settings); + }; + + let touchHandler = (_,e) => { + let R = Bangle.appRect; + if (e.x < R.x || e.x > R.x2 || e.y < R.y || e.y > R.y2 ) return; + if (settings.exttapapp.src){ if (settings.exttapapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.exttapapp.src)) reset("exttapapp"); else load(settings.exttapapp.src); } + }; + + let swipeHandler = (lr,ud) => { + if (lr == -1 && settings.extleftapp && settings.extleftapp.src){ if (settings.extleftapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.extleftapp.src)) reset("extleftapp"); else load(settings.extleftapp.src); } + if (lr == 1 && settings.extrightapp && settings.extrightapp.src){ if (settings.extrightapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.extrightapp.src)) reset("extrightapp"); else load(settings.extrightapp.src); } + if (ud == -1 && settings.extupapp && settings.extupapp.src){ if (settings.extupapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.extupapp.src)) reset("extupapp"); else load(settings.extupapp.src); } + if (ud == 1 && settings.extdownapp && settings.extdownapp.src){ if (settings.extdownapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.extdownapp.src)) reset("extdownapp"); else load(settings.extdownapp.src); } + }; + + Bangle.setUI({ + mode: "custom", + touch: touchHandler, + swipe : swipeHandler, + remove: ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);} // Compatability with Fastload Utils. + }); + + g.clearRect(Bangle.appRect); + Bangle.loadWidgets(); // Compatability with Fastload Utils. + + // taken from Icon Launcher with some alterations + let timeoutToClock; + const updateTimeoutToClock = function(){ + let time = 1000; // milliseconds + if (timeoutToClock) clearTimeout(timeoutToClock); + timeoutToClock = setTimeout(load,time); + }; + updateTimeoutToClock(); +} diff --git a/apps/quicklaunch/boot.js b/apps/quicklaunch/boot.js index b00e76e23..946a14308 100644 --- a/apps/quicklaunch/boot.js +++ b/apps/quicklaunch/boot.js @@ -1,22 +1,24 @@ { - let settings = require("Storage").readJSON("quicklaunch.json", true) || {}; const storage = require("Storage"); + let settings = storage.readJSON("quicklaunch.json", true) || {}; let reset = function(name){ if (!settings[name]) settings[name] = {"name":"(none)"}; - if (!require("Storage").read(settings[name].src)) settings[name] = {"name":"(none)"}; + if (!storage.read(settings[name].src)) settings[name] = {"name":"(none)"}; storage.write("quicklaunch.json", settings); }; Bangle.on("touch", (_,e) => { if (!Bangle.CLOCK) return; + if (Bangle.CLKINFO_FOCUS) return; let R = Bangle.appRect; if (e.x < R.x || e.x > R.x2 || e.y < R.y || e.y > R.y2 ) return; - if (settings.tapapp.src){ if (!storage.read(settings.tapapp.src)) reset("tapapp"); else load(settings.tapapp.src); } + if (settings.tapapp.src){ if (settings.tapapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.tapapp.src)) reset("tapapp"); else load(settings.tapapp.src); } }); Bangle.on("swipe", (lr,ud) => { if (!Bangle.CLOCK) return; + if (Bangle.CLKINFO_FOCUS) return; if (lr == -1 && settings.leftapp && settings.leftapp.src){ if (settings.leftapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.leftapp.src)) reset("leftapp"); else load(settings.leftapp.src); } if (lr == 1 && settings.rightapp && settings.rightapp.src){ if (settings.rightapp.name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings.rightapp.src)) reset("rightapp"); else load(settings.rightapp.src); } diff --git a/apps/quicklaunch/metadata.json b/apps/quicklaunch/metadata.json index 033eaaf25..e38d25d09 100644 --- a/apps/quicklaunch/metadata.json +++ b/apps/quicklaunch/metadata.json @@ -2,14 +2,16 @@ "id": "quicklaunch", "name": "Quick Launch", "icon": "app.png", - "version":"0.08", + "version": "0.10", "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.", "type": "bootloader", "tags": "tools, system", + "readme": "README.md", "supports": ["BANGLEJS2"], "storage": [ - {"name":"quicklaunch.settings.js","url":"settings.js"}, - {"name":"quicklaunch.boot.js","url":"boot.js"} + {"name": "quicklaunch.settings.js", "url": "settings.js"}, + {"name": "quicklaunch.boot.js", "url": "boot.js"}, + {"name": "quicklaunch.app.js", "url": "app.js"} ], - "data": [{"name":"quicklaunch.json"}] + "data": [{"name": "quicklaunch.json"}] } diff --git a/apps/quicklaunch/settings.js b/apps/quicklaunch/settings.js index d8907553a..39c2a13d6 100644 --- a/apps/quicklaunch/settings.js +++ b/apps/quicklaunch/settings.js @@ -1,17 +1,27 @@ (function(back) { -var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {}); +var storage = require("Storage"); +var settings = Object.assign(storage.readJSON("quicklaunch.json", true) || {}); -for (let c of ["leftapp","rightapp","upapp","downapp","tapapp"]){ +for (let c of ["leftapp","rightapp","upapp","downapp","tapapp","extleftapp","extrightapp","extupapp","extdownapp","exttapapp"]){ if (!settings[c]) settings[c] = {"name":"(none)"}; } -var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type)); +var apps = storage.list(/\.info$/).map(app=>{var a=storage.readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type)); // Add psuedo app to trigger Bangle.showLauncher later apps.push({ "name": "Show Launcher", - "type": undefined, "sortorder": -10, - "src": "no sorce" + "type": undefined, + "sortorder": -12, + "src": "no source" + }); + +// Add the Quick Launch extension app +apps.push({ + "name": "Quick Launch Extension", + "type": "app", + "sortorder": -11, + "src": "quicklaunch.app.js" }); apps.sort((a,b)=>{ @@ -24,11 +34,11 @@ apps.sort((a,b)=>{ function save(key, value) { settings[key] = value; - require("Storage").write("quicklaunch.json",settings); + storage.write("quicklaunch.json",settings); } -// Quick Launch menu function showMainMenu() { + // Quick Launch menu var mainmenu = { "" : { "title" : "Quick Launch" }, "< Back" : ()=>{load();} @@ -40,6 +50,7 @@ function showMainMenu() { mainmenu["Up: "+settings.upapp.name] = function() { E.showMenu(upmenu); }; mainmenu["Down: "+settings.downapp.name] = function() { E.showMenu(downmenu); }; mainmenu["Tap: "+settings.tapapp.name] = function() { E.showMenu(tapmenu); }; + mainmenu["Extend Quick Launch"] = showExtMenu; return E.showMenu(mainmenu); } @@ -129,5 +140,107 @@ apps.forEach((a)=>{ }; }); +function showExtMenu() { + // Extend Quick Launch menu + var extmenu = { + "" : { "title" : "Extend Quick Launch" }, + "< Back" : ()=>{showMainMenu();} + }; + + //List all selected apps + extmenu["Left: "+settings.extleftapp.name] = function() { E.showMenu(extleftmenu); }; + extmenu["Right: "+settings.extrightapp.name] = function() { E.showMenu(extrightmenu); }; + extmenu["Up: "+settings.extupapp.name] = function() { E.showMenu(extupmenu); }; + extmenu["Down: "+settings.extdownapp.name] = function() { E.showMenu(extdownmenu); }; + extmenu["Tap: "+settings.exttapapp.name] = function() { E.showMenu(exttapmenu); }; + + return E.showMenu(extmenu); +} + +//Extension Left swipe menu +var extleftmenu = { + "" : { "title" : "Extension Left Swipe" }, + "< Back" : showExtMenu +}; + +extleftmenu["(none)"] = function() { + save("extleftapp", {"name":"(none)"}); + showExtMenu(); +}; +apps.forEach((a)=>{ + extleftmenu[a.name] = function() { + save("extleftapp", a); + showExtMenu(); + }; +}); + +//Extension Right swipe menu +var extrightmenu = { + "" : { "title" : "Extension Right Swipe" }, + "< Back" : showExtMenu +}; + +extrightmenu["(none)"] = function() { + save("extrightapp", {"name":"(none)"}); + showExtMenu(); +}; +apps.forEach((a)=>{ + extrightmenu[a.name] = function() { + save("extrightapp", a); + showExtMenu(); + }; +}); + +//Extension Up swipe menu +var extupmenu = { + "" : { "title" : "Extension Up Swipe" }, + "< Back" : showExtMenu +}; + +extupmenu["(none)"] = function() { + save("extupapp", {"name":"(none)"}); + showExtMenu(); +}; +apps.forEach((a)=>{ + extupmenu[a.name] = function() { + save("extupapp", a); + showExtMenu(); + }; +}); + +//Extension Down swipe menu +var extdownmenu = { + "" : { "title" : "Extension Down Swipe" }, + "< Back" : showExtMenu +}; + +downmenu["(none)"] = function() { + save("extdownapp", {"name":"(none)"}); + showExtMenu(); +}; +apps.forEach((a)=>{ + extdownmenu[a.name] = function() { + save("extdownapp", a); + showExtMenu(); + }; +}); + +//Extension Tap menu +var exttapmenu = { + "" : { "title" : "Extension Tap" }, + "< Back" : showExtMenu +}; + +exttapmenu["(none)"] = function() { + save("exttapapp", {"name":"(none)"}); + showExtMenu(); +}; +apps.forEach((a)=>{ + exttapmenu[a.name] = function() { + save("exttapapp", a); + showExtMenu(); + }; +}); + showMainMenu(); }) diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index c4d1fa8c1..c3e20ed34 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -25,3 +25,4 @@ 0.19: Fix track plotting code 0.20: Automatic translation of some more strings. 0.21: Speed report now uses speed units from locale +0.22: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/recorder/app.js b/apps/recorder/app.js index 8ac3ff627..972a9580d 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -56,7 +56,6 @@ function showMainMenu() { '< Back': ()=>{load();}, /*LANG*/'RECORD': { value: !!settings.recording, - format: v=>v?/*LANG*/"On":/*LANG*/"Off", onchange: v => { setTimeout(function() { E.showMenu(); diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index 45d588d99..60abeadef 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.21", + "version": "0.22", "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/rings/ChangeLog b/apps/rings/ChangeLog new file mode 100644 index 000000000..ba635f847 --- /dev/null +++ b/apps/rings/ChangeLog @@ -0,0 +1 @@ +0.01: First rev. Could use a little optimization love. diff --git a/apps/rings/README.md b/apps/rings/README.md new file mode 100644 index 000000000..1f689623f --- /dev/null +++ b/apps/rings/README.md @@ -0,0 +1,11 @@ +# Rings watchface + +Ring based watchface, read from the outside in. When the watch is unlocked the circles shrink to show the date ring. + +By Amos Blanton, inspired by and remixed from Rinkulainen by Jukio Kallio. + +![](screenshot1.png) +View when watch is locked. + +![](screenshot2.png) +Watch unlocked, showing the date. \ No newline at end of file diff --git a/apps/rings/app-icon.js b/apps/rings/app-icon.js new file mode 100644 index 000000000..3e70c5e3a --- /dev/null +++ b/apps/rings/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+CR0GAokMiUMAQQLFAooAYg0SjEYjwAHjA0BGLQXChgrEGIIGCG4wyBLbItFFQYnDFgYREGIRiRCISHIFQgEDMYgCCSoYuPhi1IXg4vFMYI5CSh4uBJwYAEj5bDAAjGCjIzDTwguWFoMShkOgIVFgMOLQMYHwYCDGBZdKiUHisckcOwNWgGBxEkikjkcWhCmBj4sBMJ67GDIMAiojBAANd1lWq2BAAMOo4LCjkBiQWBLwQ0CK4RfGFw0SgJbBAAc51mBles1lPhwMENoMMGAJgDj0MGAoEBBgYuDhAgFkeIFgOBAQN6F40jisGYgQADRpguJL4LtEgE4B48VMIRiDSIxeFjEBDo1HWQMAqgACGIKdFAAUPYYUSSIjsJgAdHh1O1HSAAmovEICY8Id4RTDMAK9GRoMVDIscgGo6YuFAAPT1FOitHCosGFoYpBdwZeFg4uRGIYwHh4sBYIsAS4ZeJFwItKGAiSFjjyBL4aQBdo0MCwlHhxdMYgkAYJApDF48OIotOFw2ov1+NA3TvyRFisYMAJXDgCOEjCOEo8VLwuoqjXCAAIxF1EIJQsSF4RbBhkMXxcO5giEFwMqAAQwBqhkB6ZABqkdYArsBdQUSF4q+FkdUWQwrCAAQFFvzxGg5eCeARfGCYkdF4t+FI0Aq1VleBwNWgAvFFAgvHL4ovOq2swIvB1msgFAF7yPHlesLYNWwIHBR5AvJiUWd4ovE6S6EAQJfB1gyDF4rvCAAcSgwvEjEVCYdHh+o6ZgFAAksq1WlcrGwMHJQkciUfF4kAGwsOCgt4F4jCCqgABv1+SwcBgBKEkcVjAvEhkAjA0DAAIUEkcI1AvFAA+oHAIZGh0YAAQqBFAMSFodWkhFFjlOMAwAH6d+DA0MjESGIYrD1kAwOswMyIwsVGBouBhAXGXwRfCLYYsBAQIDBvUcC4lHGALzFFojHBFw0jcwq+CL4eBgGs1lWgwYFo8cgF+5gpBAAQvB0SlBo4VFh5bBRwIuBRwMGL4QABleBrgHBhxKGPYMIql+AAVUg8VjouGi0Mj7uELwYAGhkZhjZFMQTeCAAYIDdgqNCFoMMLwoABlYFBmJsCj8GGA4yCAAQMIFwJaCdoZeIBwQACj8Mh4jIABUWFwS8Dj0SLwovIGAMShCEBAB8PQ4JbCLoSNBABIwHjEGh4xMjkVDQMZRggEBFxQwIDIUMhEVdQQrDFgMOg0SCIQYFRY4wPj4gBiUMg8MAQS0BCgINBLYguDGBxJCGI4kCA5S7GFxwABa4QAXiQrPAAQ/CSZAANjBaQGA8MGKUYhiLSGJEGcgIsMfQItYGIxlChg0BAAUSFYYtQ")) \ No newline at end of file diff --git a/apps/rings/app.js b/apps/rings/app.js new file mode 100644 index 000000000..0fb768ea8 --- /dev/null +++ b/apps/rings/app.js @@ -0,0 +1,210 @@ +// Rings watch face +// for Bangle.js 2 +// by Amos Blanton +// Remixed from / inspired by Rinkulainen watch face by Jukio Kallio + +// To Do: +// Make Month / year text buffer 1/2 size +// Optimize text positioning transforms + +const watch = { + x:0, y:0, w:0, h:0, + color:"#000000", + dateRing : { size:109, weight:20, color:"#00FF00", cursor:14, numbers: true }, + hourRing : { size:85, weight:20, color:"#00FFFF", cursor:14, numbers: true }, + minuteRing : { size:61, weight:20, color:"#FFFF00", cursor:14, numbers: true }, + screen : { width:g.getWidth(), height:g.getHeight(), centerX: g.getWidth() *0.5, centerY: g.getHeight() * 0.5, cursor: 14, font:"6x8:2" }, +}; + +const month= ["JANUARY","FEBRUARY","MARCH","APRIL","MAY","JUNE","JULY", + "AUGUST","SEPTEMBER","OCTOBER","NOVEMBER","DECEMBER"]; + +var wait = 60000; // wait time, normally a minute +// timeout used to update every minute +var drawTimeout; +// Global for use in shrink / unshrink animations +var counter = 1; + +// Buffer for month circle text, 1/2 screen size (will be scaled up) +var monthCircleTextBuffer= Graphics.createArrayBuffer(watch.screen.width,watch.screen.height,1,{msb:true}); +var monthCircleTextImg = monthCircleTextBuffer.asImage(); +monthCircleTextImg.transparent = 1; +var lastMonthCircleImageText = ""; + +// Calculate number of days in this month / year for date ring +const getDays = (year, thisMonth) => { + return new Date(year, thisMonth, 0).getDate(); // getMonth() Jan = 0. +}; + +// Schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, wait - (Date.now() % wait)); +} + +// Draws a time circle (date, hours, minutes) +function drawTimeCircle(color, size, weight, range, value ) { + // variables for vertex transformations and positioning time + var tver, tobj, tran; + var ttime = (value / range) * (Math.PI * 2); + + // draw circle and line + g.setColor(color).fillCircle(watch.screen.centerX, watch.screen.centerY, size); + g.setColor("#000000").fillCircle(watch.screen.centerX, watch.screen.centerY, size - weight); + + tver = [-watch.screen.cursor, 0, watch.screen.cursor, 0, watch.screen.cursor, -size*1.01, -watch.screen.cursor, -size*1.05]; + + tobj = { x:watch.screen.centerX, y:watch.screen.centerY, scale:1, rotate:ttime }; + tran = g.transformVertices(tver, tobj); + g.fillPoly(tran); + + // Draw numbers + g.setFontAlign(0,0).setFont(watch.screen.font, 2).setColor(1,1,1); + + // size - 21 is the right offset to get the numbers aligned in the circle. + tver = [-1, 0, 1, 0, 1, -size, -1, -(size -21)]; + tran = g.transformVertices(tver, tobj); + g.setColor(1,1,1); + g.drawString(value, (tran[4]+tran[6]) / 2 , (tran[5]+tran[7]) / 2 ); + +} + +// Draws text for month and year in date circle +function drawMonthCircleText( text, circleSize, range, value){ + + // If the text isn't the same as last time, write it into a graphic object. + if(text != lastMonthCircleImageText){ + + monthCircleTextBuffer.clear(); + monthCircleTextBuffer.fillRect(0,0,watch.screen.width,watch.screen.height); + + var tver, tobj, tran; + + // From here: https://forum.espruino.com/comments/16781795/ + var gr = Graphics.createArrayBuffer(24,16,1,{msb:true}); + var grimg = gr.asImage(); + grimg.transparent = 1; + monthCircleTextBuffer.setColor(0,0,0); + + for(z=0; z < text.length; z++){ + tobj = { x:watch.screen.centerX, y:watch.screen.centerY, scale:1, rotate: ((z + 1) / range) * (Math.PI * 2) }; + tver = [-1, 0, 1, 0, 1, -circleSize, -1, -(circleSize -21)]; + tran = monthCircleTextBuffer.transformVertices(tver, tobj); + gr.clear().setColor(1,1,1).fillRect(0,0,24,16).setColor(0,0,0).setFont(watch.screen.font).setFontAlign(0,0).drawString(text[z],12,8); + + monthCircleTextBuffer.drawImage(grimg, + (tran[4]+tran[6]) / 2, + (tran[5]+tran[7]) / 2, + {rotate:((z+1) / range) * (Math.PI * 2) }); + } + + lastMonthCircleImageText = text; + } + + // Determine correct rotation for text in ring ( opposite the date position ) + var offset = value + (range / 2) - (text.length / 2); + if(offset > range) + offset = offset - range; + var rotation = (offset / range) * (Math.PI * 2); + + // Draw the image of text to the screen at that rotation + g.drawImage(monthCircleTextImg, watch.screen.centerX, watch.screen.centerY, {scale:1, rotate:rotation }); + +} + +// Animate by shrinking or expanding circles +function shrinkCircles(toggle){ + // If there's a queued draw operation,removeit so animation isn't interrupted. + if (drawTimeout) clearTimeout(drawTimeout); + + var date = new Date(); + var delta = 1; + + if(counter > 12) + { + counter = 1; + // We're finished, so queue next draw. + queueDraw(); + return; + } + + if(toggle) // We are shrinking + delta = counter * 2 * -1; + else // We are expanding + delta = counter *2 - 24; + + // Clear space on screen. + g.setColor(watch.color); + g.fillRect(0, 0, watch.screen.width, watch.screen.height); + + // Draw the date ring (unless it's the last run of an expansion). + if(counter < 11 || toggle){ + drawTimeCircle(watch.dateRing.color, watch.dateRing.size + delta, watch.dateRing.weight, getDays(date.getFullYear(), date.getMonth()+1), date.getDate() ); + // Draw month and year in date ring + drawMonthCircleText( month[date.getMonth()]+" "+date.getFullYear(), watch.dateRing.size - 24, getDays(date.getFullYear(), date.getMonth()+1), date.getDate()) ; + } + + drawTimeCircle(watch.hourRing.color, watch.hourRing.size + delta, watch.hourRing.weight, 12, date.getHours() ); + + drawTimeCircle(watch.minuteRing.color, watch.minuteRing.size + delta, watch.minuteRing.weight, 60, date.getMinutes() ); + + counter += 1; + setTimeout(shrinkCircles, 10, toggle); +} + + +// main draw function +function draw() { + // make date object + var date = new Date(); + var unLockedOffset = 0; + + // Reset the state of the graphics library + g.reset(); + + // Clear the area where we want to draw the time + g.setColor(watch.color); + g.fillRect(0, 0, watch.screen.width, watch.screen.height); + + // If unlocked, draw date ring and text and make hour and minute rings smaller + if(!Bangle.isLocked()){ + unLockedOffset = 24; + drawTimeCircle(watch.dateRing.color, watch.dateRing.size - unLockedOffset, watch.dateRing.weight, getDays(date.getFullYear(), date.getMonth()+1), date.getDate() ); + drawMonthCircleText( month[date.getMonth()]+" "+date.getFullYear(), watch.dateRing.size - unLockedOffset, getDays(date.getFullYear(), date.getMonth()+1), date.getDate()) ; + } + + drawTimeCircle(watch.hourRing.color, watch.hourRing.size - unLockedOffset, watch.hourRing.weight, 12, date.getHours() ); + drawTimeCircle(watch.minuteRing.color, watch.minuteRing.size -unLockedOffset , watch.minuteRing.weight, 60, date.getMinutes() ); + + queueDraw(); +} + +// Trigger shrink / expand animation on unlock / lock events +Bangle.on('lock', on=>{ + if (on) { // locked, expand circles + counter = 1; + shrinkCircles(false); + } else + { // unlocked, shrink circles and show date ring + counter = 1; + shrinkCircles(true); + } +}); + + +// End function definitions / start of initial execution. + +// Clear the screen once, at startup +g.clear(); + +// draw immediately at first +draw(); + + +// console.log("Whatevs"); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); \ No newline at end of file diff --git a/apps/rings/app.png b/apps/rings/app.png new file mode 100644 index 000000000..e4bfd6bac Binary files /dev/null and b/apps/rings/app.png differ diff --git a/apps/rings/metadata.json b/apps/rings/metadata.json new file mode 100644 index 000000000..4409bd13a --- /dev/null +++ b/apps/rings/metadata.json @@ -0,0 +1,16 @@ +{ "id": "rings", + "name": "Rings - an animated watchface", + "shortName":"Rings", + "version":"0.01", + "description": "Ring based watchface that animates to show the date when unlocked. Inspired by / remixed from Rinkulainen.", + "icon": "app.png", + "screenshots": [{"url":"screenshot1.png"}, {"url":"screenshot2.png"}], + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"rings.app.js","url":"app.js"}, + {"name":"rings.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/rings/screenshot1.png b/apps/rings/screenshot1.png new file mode 100644 index 000000000..26f2521fc Binary files /dev/null and b/apps/rings/screenshot1.png differ diff --git a/apps/rings/screenshot2.png b/apps/rings/screenshot2.png new file mode 100644 index 000000000..8edbf79ac Binary files /dev/null and b/apps/rings/screenshot2.png differ diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index 95945be78..3638407ef 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -13,3 +13,4 @@ 0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11 0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) 0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working +0.15: Keep run state between runs (allowing you to exit and restart the app) diff --git a/apps/run/app.js b/apps/run/app.js index 4038b8c1a..a56fce31c 100644 --- a/apps/run/app.js +++ b/apps/run/app.js @@ -41,6 +41,13 @@ var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,setti var exs = ExStats.getStats(statIDs, settings); // --------------------------- +function setStatus(running) { + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + layout.render(); +} + // Called to start/stop running function onStartStop() { var running = !exs.state.active; @@ -77,12 +84,9 @@ function onStartStop() { } 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(); + setStatus(running); }); } @@ -105,13 +109,14 @@ for (var i=0;i `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 +* `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)", "Max 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 + +* Keep a log of each run's stats (distance/steps/etc) + +## Development + +This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the +app loader, the module is automatically included in the app's source. However +when developing via the IDE the module won't get pulled in by default. + +There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) +## Contributors (Run and Run+) +gfwilliams +hughbarney +GrandVizierOlaf +BartS23 +f-teacher +thyttan diff --git a/apps/runplus/app-icon.js b/apps/runplus/app-icon.js new file mode 100644 index 000000000..a97d1b8ce --- /dev/null +++ b/apps/runplus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/runplus/app.js b/apps/runplus/app.js new file mode 100644 index 000000000..7cb5d4381 --- /dev/null +++ b/apps/runplus/app.js @@ -0,0 +1,198 @@ +// Use widget utils to show/hide widgets +let wu = require("widget_utils"); + +let runInterval; +let karvonenActive = false; +// Run interface wrapped in a function +let ExStats = require("exstats"); +let B2 = process.env.HWVERSION===2; +let Layout = require("Layout"); +let locale = require("locale"); +let fontHeading = "6x8:2"; +let fontValue = B2 ? "6x15:2" : "6x8:3"; +let headingCol = "#888"; +let fixCount = 0; +let isMenuDisplayed = false; + +g.reset().clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +wu.show(); + +// --------------------------- +let settings = Object.assign({ + 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: [], + }, + }, + HRM: { + min: 55, + max: 185, + }, +}, require("Storage").readJSON("runplus.json", 1) || {}); +let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); +let exs = ExStats.getStats(statIDs, settings); +// --------------------------- + +function setStatus(running) { + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + layout.render(); +} + +// Called to start/stop running +function onStartStop() { + let running = !exs.state.active; + let 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; + prepPromises.push( + WIDGETS["recorder"].setRecording(true).then(() => { + isMenuDisplayed = false; + layout.setUI(); // grab our input handling again + layout.forgetLazyState(); + layout.render(); + }) + ); + } else { + prepPromises.push( + WIDGETS["recorder"].setRecording(false) + ); + } + } + + if (!prepPromises.length) // fix for Promise.all bug in 2v12 + prepPromises.push(Promise.resolve()); + + Promise.all(prepPromises) + .then(() => { + if (running) { + exs.start(); + } else { + exs.stop(); + } + // if stopping running, don't clear state + // so we can at least refer to what we've done + setStatus(running); + }); +} + +let lc = []; +// Load stats in pair by pair +for (let i=0;ilayout[e.id].label = e.getString()); + if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); +} +// At the bottom put time/GPS state/etc +lc.push({ type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, + {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, + {type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 } +]}); +// Now calculate the layout +let layout = new Layout( { + type:"v", c: lc +},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); +delete lc; +setStatus(exs.state.active); +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 && exs.stats[statType]) { + 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) { + Bangle.buzz(); // first fix, does not need to respect quiet mode + } +}); + +// run() function used to switch between traditional run UI and karvonen UI +function run() { + wu.show(); + layout.lazy = false; + layout.render(); + layout.lazy = true; + // We always call ourselves once a second to update + if (!runInterval){ + runInterval = setInterval(function() { + layout.clock.label = locale.time(new Date(),1); + if (!isMenuDisplayed && !karvonenActive) layout.render(); + }, 1000); + } +} +run(); + +/////////////////////////////////////////////// +// Karvonen +/////////////////////////////////////////////// + +function stopRunUI() { + // stop updating and drawing the traditional run app UI + clearInterval(runInterval); + runInterval = undefined; + karvonenActive = true; +} + +function stopKarvonenUI() { + g.reset().clear(); + clearInterval(karvonenInterval); + karvonenInterval = undefined; + karvonenActive = false; +} + +let karvonenInterval; +// Define the function to go back and forth between the different UI's +function swipeHandler(LR,_) { + if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} + if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} +} +// Listen for swipes with the swipeHandler +Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/app.png b/apps/runplus/app.png new file mode 100644 index 000000000..7059b8b01 Binary files /dev/null and b/apps/runplus/app.png differ diff --git a/apps/runplus/karvonen.js b/apps/runplus/karvonen.js new file mode 100644 index 000000000..de81494bb --- /dev/null +++ b/apps/runplus/karvonen.js @@ -0,0 +1,215 @@ +(function karvonen(hrmSettings, exsHrmStats) { + //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ + //The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. + //Other methods are even more approximative. + let wu = require("widget_utils"); + wu.hide(); + let R = Bangle.appRect; + + + g.reset().clearRect(R).setFontAlign(0,0,0); + + const x = "x"; const y = "y"; + function Rdiv(axis, divisor) { // Used when placing things on the screen + return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor); + } + let linePoints = { //Not lists of points, but used to update points in the drawArrows function. + x: [ + 175/40, + 2, + 175/135, + ], + y: [ + 175/64, + 175/52, + 175/110, + 175/122, + ], + + }; + + function drawArrows() { + g.setColor(g.theme.fg); + // Upper + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0])); + // Lower + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2])); + } + + //To calculate Heart rate zones, we need to know the heart rate reserve (HRR) + // HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr. + //get the hrr (heart rate reserve). + // I put random data here, but this has to come as a menu in the settings section so that users can change it. + let minhr = hrmSettings.min; + let maxhr = hrmSettings.max; + + function calculatehrr(minhr, maxhr) { + return maxhr - minhr;} + + //test input for hrr (it works). + let hrr = calculatehrr(minhr, maxhr); + console.log(hrr); + + //Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input. + let hr = exsHrmStats.getValue(); + let hr1 = hr; + // These letiables display next and previous HR zone. + //get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method + //60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack + let minzone2 = hrr * 0.6 + minhr; + let maxzone2 = hrr * 0.7 + minhr; + let maxzone3 = hrr * 0.8 + minhr; + let maxzone4 = hrr * 0.9 + minhr; + let maxzone5 = hrr * 0.99 + minhr; + + // HR data: large, readable, in the middle of the screen + function drawHR() { + g.setFontAlign(-1,0,0); + g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); + g.setColor(g.theme.fg); + g.setFont("Vector",50); + g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); + } + + function drawWaitHR() { + g.setColor(g.theme.fg); + // Waiting for HRM + g.setFontAlign(0,0,0); + g.setFont("Vector",50); + g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); + + // Waiting for current Zone + g.setFont("Vector",24); + g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); + + // waiting for upper and lower limit of current zone + g.setFont("Vector",20); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); + } + + //These functions call arcs to show different HR zones. + + //To shorten the code, I'll reference some letiables and reuse them. + let centreX = R.x + 0.5 * R.w; + let centreY = R.y + 0.5 * R.h; + let minRadius = 0.38 * R.h; + let maxRadius = 0.50 * R.h; + + //draw background image (dithered green zones)(I should draw different zones in different dithered colors) + const HRzones= require("graphics_utils"); + let minRadiusz = 0.44 * R.h; + let startAngle = HRzones.degreesToRadians(-88.5); + let endAngle = HRzones.degreesToRadians(268.5); + + function drawBgArc() { + g.setColor(g.theme.dark==false?0xC618:"#002200"); + HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + } + + const zones = require("graphics_utils"); + //####### A function to simplify a bit the code ###### + function simplify (sA, eA, Z, currentZone, lastZone) { + let startAngle = zones.degreesToRadians(sA); + let endAngle = zones.degreesToRadians(eA); + if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); + else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + g.setFont("Vector",24); + g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); + g.setFontAlign(0,0,0); + g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); + } + + function zoning (max, min) { // draw values of upper and lower limit of current zone + g.setFont("Vector",20); + g.setColor(g.theme.fg); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); + g.setFontAlign(0,0,0); + g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); + g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); + } + + function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color + g.setColor(g.theme.bg); + HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); + } + + function getZone(zone) { + drawBgArc(); + clearCurrentZone(); + if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} + if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} + if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} + if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} + if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} + if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} + if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} + if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} + if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} + if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} + if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} + if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} + if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} + } + + function getZoneAlert() { + const HRzonemax = require("graphics_utils"); + let centreX1,centreY1,maxRadius1 = 1; + let minRadius = 0.40 * R.h; + let startAngle1 = HRzonemax.degreesToRadians(-90); + let endAngle1 = HRzonemax.degreesToRadians(270); + g.setFont("Vector",38);g.setColor("#ff0000"); + HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); + g.drawString("ALERT", 26,66); + } + + //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. + let subZoneLast; + function drawZones() { + if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. + if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 + else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a + else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b + else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c + else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a + else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b + else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c + else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a + else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b + else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c + else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a + else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b + else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c + else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert + } + + function initDraw() { + drawArrows(); + if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();} + //drawZones(); + } + + let hrLast; + //h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. + function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. + hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. + hr = exsHrmStats.getValue(); + //if (h!=0) hr = h; + if (hr!=hrLast) { + drawHR(); + drawZones(); + } //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. + } + + initDraw(); + + // check for updates every second. + karvonenInterval = setInterval(function() { + if (!isMenuDisplayed && karvonenActive) updateUI(); + }, 1000); + + return karvonenInterval; +}) diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json new file mode 100644 index 000000000..c605c438d --- /dev/null +++ b/apps/runplus/metadata.json @@ -0,0 +1,41 @@ +{ + "id": "runplus", + "name": "Run+", + "version": "0.18", + "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", + "icon": "app.png", + "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", + "supports": [ + "BANGLEJS2" + ], + "screenshots": [ + { + "url": "screenshot.png" + } + ], + "readme": "README.md", + "storage": [ + { + "name": "runplus.app.js", + "url": "app.js" + }, + { + "name": "runplus.img", + "url": "app-icon.js", + "evaluate": true + }, + { + "name": "runplus.settings.js", + "url": "settings.js" + }, + { + "name": "runplus_karvonen", + "url": "karvonen.js" + } + ], + "data": [ + { + "name": "runplus.json" + } + ] +} diff --git a/apps/runplus/screenshot.png b/apps/runplus/screenshot.png new file mode 100644 index 000000000..1a813f19d Binary files /dev/null and b/apps/runplus/screenshot.png differ diff --git a/apps/runplus/settings.js b/apps/runplus/settings.js new file mode 100644 index 000000000..539391a27 --- /dev/null +++ b/apps/runplus/settings.js @@ -0,0 +1,157 @@ +(function(back) { + const SETTINGS_FILE = "runplus.json"; + var ExStats = require("exstats"); + var statsList = ExStats.getList(); + statsList.unshift({name:"-",id:""}); // add blank menu item + var statsIDs = statsList.map(s=>s.id); + + // ...and overwrite them with any saved values + // 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, // TODO: Default to either 1km or 1mi based on locale + notify: { + dist: { + increment: 0, + notifications: [], + }, + step: { + increment: 0, + notifications: [], + }, + time: { + increment: 0, + notifications: [], + }, + }, + HRM: { + min: 55, + max: 185, + }, + }, storage.readJSON(SETTINGS_FILE, 1) || {}); + function saveSettings() { + storage.write(SETTINGS_FILE, settings) + } + + function getBoxChooser(boxID) { + return { + min: 0, max: statsIDs.length-1, + value: Math.max(statsIDs.indexOf(settings[boxID]),0), + format: v => statsList[v].name, + onchange: v => { + settings[boxID] = statsIDs[v]; + saveSettings(); + }, + } + } + + 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, + }; + if (global.WIDGETS&&WIDGETS["recorder"]) + menu[/*LANG*/"Record Run"] = { + value : !!settings.record, + onchange : v => { + settings.record = v; + saveSettings(); + } + }; + var notificationsMenu = { + '< Back': function() { E.showMenu(menu) }, + } + menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; + ExStats.appendMenuItems(menu, settings, saveSettings); + 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,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.dist.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Step Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.step.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Time Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"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"), + 'Box 4': getBoxChooser("B4"), + 'Box 5': getBoxChooser("B5"), + 'Box 6': getBoxChooser("B6"), + }); + menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; + + var hrmMenu = { + '< Back': function() { E.showMenu(menu) }, + } + + menu[/*LANG*/"HRM min/max"] = function() { E.showMenu(hrmMenu)}; + hrmMenu[/*LANG*/"min"] = { + min: 1, max: 100, + value: settings.HRM.min, + format: w => w, + onchange: w => { + settings.HRM.min = w; + saveSettings(); + }, + } + hrmMenu[/*LANG*/"max"] = { + min: 101, max: 220, + value: settings.HRM.max, + format: v => v, + onchange: v => { + settings.HRM.max = v; + saveSettings(); + }, + } + E.showMenu(menu); +}) diff --git a/apps/sched/interface.html b/apps/sched/interface.html new file mode 100644 index 000000000..366e597a2 --- /dev/null +++ b/apps/sched/interface.html @@ -0,0 +1,223 @@ + + + + + + + + + +

Manage dated events

+ +
+ +
+ + + + + + + + + + + +
DateSummary
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + + + + diff --git a/apps/sched/metadata.json b/apps/sched/metadata.json index 98602318d..1a4a64994 100644 --- a/apps/sched/metadata.json +++ b/apps/sched/metadata.json @@ -10,6 +10,7 @@ "provides_modules" : ["sched"], "default" : true, "readme": "README.md", + "interface": "interface.html", "storage": [ {"name":"sched.boot.js","url":"boot.js"}, {"name":"sched.js","url":"sched.js"}, diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog index 518e5dab4..7d9bdd6a8 100644 --- a/apps/sensortools/ChangeLog +++ b/apps/sensortools/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Less time used during boot if disabled 0.03: Fixed some test data +0.04: Correct type of time attribute in gps to Date diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js index 595492251..7dfc6307d 100644 --- a/apps/sensortools/lib.js +++ b/apps/sensortools/lib.js @@ -205,7 +205,7 @@ exports.enable = () => { "alt": 100, "speed": 10, "course": 12, - "time": Date.now(), + "time": new Date(), "satellites": 7, "fix": 1, "hdop": 1 @@ -228,7 +228,7 @@ exports.enable = () => { let result = { "speed": Math.random() * 3 + 2, - "time": Date.now(), + "time": new Date(), "satellites": Math.floor(Math.random()*5)+3, "fix": 1, "hdop": Math.floor(Math.random(30)+1) @@ -268,7 +268,7 @@ exports.enable = () => { "alt": NaN, "speed": NaN, "course": NaN, - "time": Date.now(), + "time": new Date(), "satellites": 2, "fix": 0, "hdop": NaN @@ -301,7 +301,7 @@ exports.enable = () => { "alt": currentAlt, "speed": currentSpeed, "course": currentCourse, - "time": Date.now(), + "time": new Date(), "satellites": currentSats, "fix": 1, "hdop": 1 diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json index 9f7a36c5c..f5bace383 100644 --- a/apps/sensortools/metadata.json +++ b/apps/sensortools/metadata.json @@ -2,7 +2,7 @@ "id": "sensortools", "name": "Sensor tools", "shortName": "Sensor tools", - "version": "0.03", + "version": "0.04", "description": "Tools for testing and debugging apps that use sensor input", "icon": "icon.png", "type": "bootloader", diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 85ccfa1a7..971162691 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -62,3 +62,6 @@ 0.55: More strings tagged for automatic translation. 0.56: make System menu items shorter and more consistant, Eg 'Clock', intead of 'Select Clock' +0.57: Settings.log = 0,1,2,3 for off,display,log,both +0.58: On/Off settings items now use checkboxes +0.59: Preserve BLE whitelist even when disabled diff --git a/apps/setting/README.md b/apps/setting/README.md index e5ea2b43d..2a7f7ee9c 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -56,9 +56,10 @@ The exact effects depend on the app. In general the watch will not wake up by i * **Debug Info** should debug info be shown on the watch's screen or not? - * `Hide` (default) do not show debug information - * `Show` Show on the Bangle's screen (when not connected to Bluetooth or `Programmable:off`) + * `Off` (default) do not show debug information + * `Display` Show on the Bangle's screen (when not connected to Bluetooth or `Programmable:off`) * `Log` Show on the Bangle's screen **and** write to a file called `log.txt` on Storage (when not connected to Bluetooth or `Programmable:off`). Warning - this file is appended to so may grow to be large if this is left enabled. + * `Both` Log and display on Bangle's screen * **Compact Storage** Removes deleted/old files from Storage - this will speed up your Bangle.js * **Rewrite Settings** Should not normally be required, but if `.boot0` has been deleted/corrupted (and so no settings are being loaded) this will fix it. * **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours. diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index 92fc75915..e62479604 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.56", + "version": "0.59", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index a877ec79c..5d2a5f7c6 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -64,8 +64,6 @@ if (("object" != typeof settings) || ("object" != typeof settings.options)) resetSettings(); -const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; - function showMainMenu() { const mainmenu = { @@ -102,7 +100,6 @@ function showAlertsMenu() { if (BANGLEJS2) { beepMenuItem = { value: settings.beep!=false, - format: boolFormat, onchange: v => { settings.beep = v; updateSettings(); @@ -134,7 +131,6 @@ function showAlertsMenu() { /*LANG*/'Beep': beepMenuItem, /*LANG*/'Vibration': { value: settings.vibrate, - format: boolFormat, onchange: () => { settings.vibrate = !settings.vibrate; updateSettings(); @@ -169,7 +165,6 @@ function showBLEMenu() { /*LANG*/'Make Connectable': ()=>makeConnectable(), /*LANG*/'BLE': { value: settings.ble, - format: boolFormat, onchange: () => { settings.ble = !settings.ble; updateSettings(); @@ -177,7 +172,6 @@ function showBLEMenu() { }, /*LANG*/'Programmable': { value: settings.blerepl, - format: boolFormat, onchange: () => { settings.blerepl = !settings.blerepl; updateSettings(); @@ -197,7 +191,14 @@ function showBLEMenu() { onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, /*LANG*/'Whitelist': { - value: settings.whitelist?(settings.whitelist.length+/*LANG*/" devs"):/*LANG*/"off", + value: + ( + settings.whitelist_disabled ? /*LANG*/"off" : /*LANG*/"on" + ) + ( + settings.whitelist + ? " (" + settings.whitelist.length + ")" + : "" + ), onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call } }); @@ -347,12 +348,21 @@ function showPasskeyMenu() { function showWhitelistMenu() { var menu = { "< Back" : ()=>showBLEMenu(), - /*LANG*/"Disable" : () => { - settings.whitelist = undefined; + }; + if (settings.whitelist_disabled) { + menu[/*LANG*/"Enable"] = () => { + delete settings.whitelist_disabled; updateSettings(); showBLEMenu(); - } - }; + }; + } else { + menu[/*LANG*/"Disable"] = () => { + settings.whitelist_disabled = true; + updateSettings(); + showBLEMenu(); + }; + } + if (settings.whitelist) settings.whitelist.forEach(function(d){ menu[d.substr(0,17)] = function() { E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { @@ -372,6 +382,7 @@ function showWhitelistMenu() { NRF.removeAllListeners('connect'); NRF.on('connect', function(addr) { if (!settings.whitelist) settings.whitelist=[]; + delete settings.whitelist_disabled; settings.whitelist.push(addr); updateSettings(); NRF.removeAllListeners('connect'); @@ -428,7 +439,6 @@ function showLCDMenu() { }, /*LANG*/'Wake on BTN1': { value: settings.options.wakeOnBTN1, - format: boolFormat, onchange: () => { settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1; updateOptions(); @@ -439,7 +449,6 @@ function showLCDMenu() { Object.assign(lcdMenu, { /*LANG*/'Wake on BTN2': { value: settings.options.wakeOnBTN2, - format: boolFormat, onchange: () => { settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2; updateOptions(); @@ -447,7 +456,6 @@ function showLCDMenu() { }, /*LANG*/'Wake on BTN3': { value: settings.options.wakeOnBTN3, - format: boolFormat, onchange: () => { settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3; updateOptions(); @@ -456,7 +464,6 @@ function showLCDMenu() { Object.assign(lcdMenu, { /*LANG*/'Wake on FaceUp': { value: settings.options.wakeOnFaceUp, - format: boolFormat, onchange: () => { settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp; updateOptions(); @@ -464,7 +471,6 @@ function showLCDMenu() { }, /*LANG*/'Wake on Touch': { value: settings.options.wakeOnTouch, - format: boolFormat, onchange: () => { settings.options.wakeOnTouch = !settings.options.wakeOnTouch; updateOptions(); @@ -472,7 +478,6 @@ function showLCDMenu() { }, /*LANG*/'Wake on Twist': { value: settings.options.wakeOnTwist, - format: boolFormat, onchange: () => { settings.options.wakeOnTwist = !settings.options.wakeOnTwist; updateOptions(); @@ -557,11 +562,11 @@ function showUtilMenu() { var menu = { '': { 'title': /*LANG*/'Utilities' }, '< Back': ()=>showMainMenu(), - /*LANG*/'Debug Info': { - value: E.clip(0|settings.log,0,2), + /*LANG*/'Debug': { + value: E.clip(0|settings.log,0,3), min: 0, - max: 2, - format: v => [/*LANG*/"Hide",/*LANG*/"Show",/*LANG*/"Log"][E.clip(0|v,0,2)], + max: 3, + format: v => [/*LANG*/"Off",/*LANG*/"Display",/*LANG*/"Log", /*LANG*/"Both"][E.clip(0|v,0,3)], onchange: v => { settings.log = v; updateSettings(); diff --git a/apps/simplest/simplest.app.js b/apps/simplest/simplest.app.js index 4038212d0..b1e22743d 100644 --- a/apps/simplest/simplest.app.js +++ b/apps/simplest/simplest.app.js @@ -13,6 +13,7 @@ function draw() { g.setFontAlign(0, 0); g.setColor(g.theme.fg); g.drawString(timeStr, w/2, h/2); + console.log(timeStr + ", simplest"); queueDraw(); } diff --git a/apps/simplestpp/ChangeLog b/apps/simplestpp/ChangeLog index 4db8559c8..51186fbdb 100644 --- a/apps/simplestpp/ChangeLog +++ b/apps/simplestpp/ChangeLog @@ -1,2 +1,3 @@ 0.01: first release 0.02: removed fast load, minimalism is useful for narrowing down on issues +0.03: Use clock_info module as an app diff --git a/apps/simplestpp/metadata.json b/apps/simplestpp/metadata.json index 145bf7309..93ae72bbe 100644 --- a/apps/simplestpp/metadata.json +++ b/apps/simplestpp/metadata.json @@ -2,7 +2,7 @@ "id": "simplestpp", "name": "Simplest++ Clock", "shortName": "Simplest++", - "version": "0.02", + "version": "0.03", "description": "The simplest working clock, with clock_info, acts as a tutorial piece", "readme": "README.md", "icon": "app.png", @@ -10,6 +10,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "storage": [ {"name":"simplestpp.app.js","url":"app.js"}, {"name":"simplestpp.img","url":"icon.js","evaluate":true} diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog index 795c62fa2..d6e3cb30b 100644 --- a/apps/sleepphasealarm/ChangeLog +++ b/apps/sleepphasealarm/ChangeLog @@ -13,3 +13,9 @@ 0.10: Fix: Do not wake when falling asleep 0.11: Minor tweaks 0.12: Support javascript command to execute as defined in scheduler 'js' configuration +0.13: Fix dated events alarm on wrong date +0.14: Reduce update interval of current time when seconds are not shown + Limit logging on Bangle.js 1 to one day due to low memory + Add plot logged data to settings +0.15: Convert Yes/No On/Off in settings to checkboxes +0.16: Fix Keep alarm enabled inverted settings diff --git a/apps/sleepphasealarm/README.md b/apps/sleepphasealarm/README.md index 574e84e1e..5aded316e 100644 --- a/apps/sleepphasealarm/README.md +++ b/apps/sleepphasealarm/README.md @@ -23,6 +23,11 @@ Replacing the watch strap with a more comfortable one (e.g. made of nylon) is re ## Logging For each day of month (1..31) the ESS states are logged. An entry will be overwritten in the next month, e.g. an entry on the 4th May will overwrite an entry on the 4th April. -The logs can be viewed with the download button: +On Bangle.js 1 only one day is logged due to low memory. +The logs can be plotted from the settings menu: -![](screenshot.jpg) +![](screenshot_log.png) + +The logs can also be viewed with the download button in the App Loader: + +![](interface.jpg) diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index ba8bff9b2..25ca7e781 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -10,10 +10,11 @@ const config = Object.assign({ disableAlarm: false, } }, require("Storage").readJSON(CONFIGFILE,1) || {}); -const active = alarms.filter(a=>a.on); +const active = alarms.filter(alarm => require("sched").getTimeToAlarm(alarm)); const schedSettings = require("sched").getSettings(); let buzzCount = schedSettings.buzzCount; let logs = []; +let drawTimeTimeout; // Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS): // Marko Borazio, Eugen Berlin, Nagihan Kücükyildiz, Philipp M. Scholl and Kristof Van Laerhoven, "Towards a Benchmark for Wearable Sleep Analysis with Inertial Wrist-worn Sensing Units", ICHI 2014, Verona, Italy, IEEE Press, 2014. @@ -26,7 +27,7 @@ const nomothresh=0.023; // Original implementation: 6, resolution 11 bit, scale const sleepthresh=600; var ess_values = []; var slsnds = 0; -function calc_ess(acc_magn) { +function calc_ess(acc_magn) {"ram" ess_values.push(acc_magn); if (ess_values.length == winwidth) { @@ -90,10 +91,12 @@ function drawApp() { layout.alarm_date.label = `${LABEL_WAKEUP_TIME}: ${alarmHour}:${alarmMinute}`; layout.render(); - function drawTime() { + function drawTime() {"ram" + const drawSeconds = !Bangle.isLocked(); + if (Bangle.isLCDOn()) { const now = new Date(); - layout.date.label = locale.time(now, BANGLEJS2 && Bangle.isLocked() ? 1 : 0); // hide seconds on bangle 2 + layout.date.label = locale.time(now, !drawSeconds); // hide seconds on bangle 2 const diff = nextAlarmDate - now; const diffHour = Math.floor((diff % 86400000) / 3600000).toString(); const diffMinutes = Math.floor(((diff % 86400000) % 3600000) / 60000).toString(); @@ -101,11 +104,22 @@ function drawApp() { layout.render(); } - setTimeout(()=>{ + const period = drawSeconds ? 1000 : 60000; + if (this.drawTimeTimeout !== undefined) { + clearTimeout(this.drawTimeTimeout); + } + drawTimeTimeout = setTimeout(()=>{ + drawTimeTimeout = undefined; drawTime(); - }, 1000 - (Date.now() % 1000)); + }, period - (Date.now() % period)); } + Bangle.on('lock', function(on) { + if (on === false) { + drawTime(); + } + }); + drawTime(); } @@ -132,8 +146,9 @@ function addLog(time, type) { var minAlarm = new Date(); var measure = true; if (nextAlarmDate !== undefined) { - config.logs[nextAlarmDate.getDate()] = []; // overwrite log on each day of month - logs = config.logs[nextAlarmDate.getDate()]; + const logday = BANGLEJS2 ? nextAlarmDate.getDate() : 0; + config.logs[logday] = []; // overwrite log on each day of month + logs = config.logs[logday]; g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -146,7 +161,7 @@ if (nextAlarmDate !== undefined) { layout.render(); Bangle.setOptions({powerSave: false}); // do not dynamically change accelerometer poll interval Bangle.setPollInterval(80); // 12.5Hz - Bangle.on('accel', (accelData) => { + Bangle.on('accel', (accelData) => {"ram" const now = new Date(); const acc = accelData.mag; const swest = calc_ess(acc); diff --git a/apps/sleepphasealarm/screenshot.jpg b/apps/sleepphasealarm/interface.jpg similarity index 100% rename from apps/sleepphasealarm/screenshot.jpg rename to apps/sleepphasealarm/interface.jpg diff --git a/apps/sleepphasealarm/metadata.json b/apps/sleepphasealarm/metadata.json index ced99062f..aa69694ab 100644 --- a/apps/sleepphasealarm/metadata.json +++ b/apps/sleepphasealarm/metadata.json @@ -2,10 +2,10 @@ "id": "sleepphasealarm", "name": "SleepPhaseAlarm", "shortName": "SleepPhaseAlarm", - "version": "0.12", + "version": "0.16", "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", "icon": "app.png", - "tags": "alarm", + "tags": "tool,alarm", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "dependencies": {"scheduler":"type"}, @@ -14,6 +14,7 @@ {"name":"sleepphasealarm.settings.js","url":"settings.js"}, {"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true} ], - "data": [{"name":"sleepphasealarm.json","storageFile":true}], - "interface": "interface.html" + "data": [{"name":"sleepphasealarm.json"}], + "interface": "interface.html", + "screenshots": [ {"url":"screenshot.png"}, {"url":"screenshot_log.png"} ] } diff --git a/apps/sleepphasealarm/screenshot.png b/apps/sleepphasealarm/screenshot.png new file mode 100644 index 000000000..769ebbbbc Binary files /dev/null and b/apps/sleepphasealarm/screenshot.png differ diff --git a/apps/sleepphasealarm/screenshot_log.png b/apps/sleepphasealarm/screenshot_log.png new file mode 100644 index 000000000..3442e36ca Binary files /dev/null and b/apps/sleepphasealarm/screenshot_log.png differ diff --git a/apps/sleepphasealarm/settings.js b/apps/sleepphasealarm/settings.js index a79abb598..e72d95e7f 100644 --- a/apps/sleepphasealarm/settings.js +++ b/apps/sleepphasealarm/settings.js @@ -13,25 +13,101 @@ require('Storage').writeJSON(CONFIGFILE, config); } - // Show the menu - E.showMenu({ - "" : { "title" : "SleepPhaseAlarm" }, - 'Keep alarm enabled': { - value: !!config.settings.disableAlarm, - format: v => v?"No":"Yes", - onchange: v => { - config.settings.disableAlarm = v; - writeSettings(); + function draw(log) { + const step = 10*60*1000; // resolution 10min + const yTicks = ["sleep", "awake", "alarm"]; + const starttime = new Date(log[0].time); + const endtime = new Date(log[log.length-1].time); + + let logidx = 0; + let curtime = starttime; + const data = new Uint8Array(Math.ceil((endtime-curtime)/step) + 1); + let curval; + let logtime; + let i=0; + while(curtime < endtime) { + if (logtime === undefined || curtime > logtime) { + curval = yTicks.indexOf(log[logidx].type); + logidx++; + logtime = new Date(log[logidx].time); } - }, "< Back" : () => back(), - 'Run before alarm': { - format: v => v === 0 ? 'disabled' : v+'h', - value: config.settings.startBeforeAlarm, - min: 0, max: 23, - onchange: v => { - config.settings.startBeforeAlarm = v; - writeSettings(); - } - }, - }); + + data[i++] = curval; + curtime = new Date(curtime + step); + } + data[i] = 1; // always end with awake + + Bangle.setUI({ + mode: "custom", + back: () => selectday(), + }); + g.reset().setFont("6x8",1); + + require("graph").drawLine(g, data, { + axes: true, + x: 4, + y: Bangle.appRect.y+8, + height: Bangle.appRect.h-20, + gridx: 1, + gridy: 1, + miny: -1, + maxy: 2, + title: /*LANG*/"Wakeup " + require("locale").date(endtime, 1), + ylabel: y => y >= 0 && y <= 1 ? yTicks[y] : "", + xlabel: x => { + if (x === Math.round(data.length/10)) { + return require("locale").time(starttime, 1); + } else if (x === (data.length-2)-Math.round(data.length/10)) { + return require("locale").time(endtime, 1); + } + return ""; + }, + }); + } + + function selectday() { + E.showMessage(/*LANG*/"Loading..."); + + const logs = config.logs.filter(log => log != null && log.filter(entry => entry.type === "alarm").length > 0); + logs.sort(function(a, b) { // sort by alarm date desc + const adate = new Date(a.filter(entry => entry.type === "alarm")[0].time); + const bdate = new Date(b.filter(entry => entry.type === "alarm")[0].time); + return bdate - adate; + }); + + const menu = {}; + menu[""] = { title: /*LANG*/"Select day" }; + menu["< Back"] = () => settingsmenu(); + logs.forEach((log, i) => { + const date = new Date(log.filter(entry => entry.type === "alarm")[0].time); + menu[require("locale").date(date, 1)] = () => { E.showMenu(); draw(log); }; + }); + E.showMenu(menu); + } + + function settingsmenu() { + // Show the menu + E.showMenu({ + "" : { "title" : "SleepPhaseAlarm" }, + 'Keep alarm enabled': { + value: !config.settings.disableAlarm, + onchange: v => { + config.settings.disableAlarm = !v; + writeSettings(); + } + }, "< Back" : () => back(), + 'Run before alarm': { + format: v => v === 0 ? 'disabled' : v+'h', + value: config.settings.startBeforeAlarm, + min: 0, max: 23, + onchange: v => { + config.settings.startBeforeAlarm = v; + writeSettings(); + } + }, + /*LANG*/'Select day': () => selectday(), + }); + } + + settingsmenu(); }) diff --git a/apps/slidingtext/ChangeLog b/apps/slidingtext/ChangeLog index 5c4a9fa75..42d1b061c 100644 --- a/apps/slidingtext/ChangeLog +++ b/apps/slidingtext/ChangeLog @@ -9,3 +9,4 @@ 0.09: Added button control toggle and other live controls to new settings screen. 0.10: Tell clock widgets to hide. 0.11: Added new styling and watch faces +0.12: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/slidingtext/metadata.json b/apps/slidingtext/metadata.json index 098fdb747..4dc6f1f0c 100644 --- a/apps/slidingtext/metadata.json +++ b/apps/slidingtext/metadata.json @@ -1,7 +1,7 @@ { "id": "slidingtext", "name": "Sliding Clock", - "version": "0.11", + "version": "0.12", "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported", "icon": "slidingtext.png", "screenshots": [{"url":"slidingtext-screenshot.english.png"},{"url":"slidingtext-screenshot.english2.png"},{"url":"slidingtext-screenshot.hybrid.png"}], diff --git a/apps/slidingtext/slidingtext.settings.js b/apps/slidingtext/slidingtext.settings.js index e13c857fd..1eee39bf0 100644 --- a/apps/slidingtext/slidingtext.settings.js +++ b/apps/slidingtext/slidingtext.settings.js @@ -173,12 +173,11 @@ "Colour": stringInSettings("color_scheme", ["black","white", "red","grey","purple","blue"]), "Style": stringInSettings("date_format", locales, (l)=>locale_mappings[l] ), "Live Control": { - value: (settings.enable_live_controls !== undefined ? settings.enable_live_controls : true), - format: v => v ? "On" : "Off", + value: (settings.enable_live_controls !== undefined ? !!settings.enable_live_controls : true), onchange: v => { settings.enable_live_controls = v; writeSettings(); } }, }); -}) \ No newline at end of file +}) diff --git a/apps/slomoclock/settings.js b/apps/slomoclock/settings.js index af67069dc..dcaf0aff6 100644 --- a/apps/slomoclock/settings.js +++ b/apps/slomoclock/settings.js @@ -1,24 +1,24 @@ (function(back) { let settings = require('Storage').readJSON('slomoclock.json',1)||{}; - + function writeSettings() { require('Storage').write('slomoclock.json',settings); } - + function setColour(c) { settings.colour = c; writeSettings(); } - + const appMenu = { '': {'title': 'SloMo Clock'}, '< Back': back, 'Colours' : function() { E.showMenu(colMenu); } - //,'Widget Space Top' : {value : settings.widTop, format : v => v?"On":"Off",onchange : () => { settings.widTop = !settings.widTop; writeSettings(); } - //,'Widget Space Bottom' : {value : settings.widBot, format : v => v?"On":"Off",onchange : () => { settings.widBot = !settings.widBot; writeSettings(); } + //,'Widget Space Top' : {value : settings.widTop, onchange : () => { settings.widTop = !settings.widTop; writeSettings(); } + //,'Widget Space Bottom' : {value : settings.widBot, onchange : () => { settings.widBot = !settings.widBot; writeSettings(); } }; - + const colMenu = { '': {'title': 'Colours'}, '< Back': function() { E.showMenu(appMenu); }, @@ -32,7 +32,7 @@ 'Violet' : function() { setColour(7); }, 'White' : function() { setColour(8); } }; - + E.showMenu(appMenu); }); diff --git a/apps/slopeclockpp/ChangeLog b/apps/slopeclockpp/ChangeLog index 58299b236..c9fb0de7a 100644 --- a/apps/slopeclockpp/ChangeLog +++ b/apps/slopeclockpp/ChangeLog @@ -9,3 +9,4 @@ 0.07: README file update as UI interaction was not easy to understand 0.08: Stability improvements - ensure we continue even if a flat string can't be allocated Stop ClockInfo text drawing outside the allocated area +0.09: Use clock_info module as an app diff --git a/apps/slopeclockpp/app.js b/apps/slopeclockpp/app.js index dca4a84e4..b57650f9e 100644 --- a/apps/slopeclockpp/app.js +++ b/apps/slopeclockpp/app.js @@ -141,8 +141,8 @@ let clockInfoDraw = (itm, info, options) => { g.setClipRect(0,0,g.getWidth()-1, g.getHeight()-1); }; let clockInfoItems = require("clock_info").load(); -let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#f00"/*red*/ }); -let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { x:0, y:115, w:50, h:40, draw : clockInfoDraw, bg : bgColor, fg : g.theme.bg, hl : (bgColor=="#000")?"#f00"/*red*/:g.theme.fg }); +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app:"slopeclockpp",x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#f00"/*red*/ }); +let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"slopeclockpp",x:0, y:115, w:50, h:40, draw : clockInfoDraw, bg : bgColor, fg : g.theme.bg, hl : (bgColor=="#000")?"#f00"/*red*/:g.theme.fg }); // Show launcher when middle button pressed Bangle.setUI({ diff --git a/apps/slopeclockpp/metadata.json b/apps/slopeclockpp/metadata.json index fbab02fca..30bc2ea3e 100644 --- a/apps/slopeclockpp/metadata.json +++ b/apps/slopeclockpp/metadata.json @@ -1,12 +1,13 @@ { "id": "slopeclockpp", "name": "Slope Clock ++", - "version":"0.08", + "version":"0.09", "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows extra information and allows the colors to be selected.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", "tags": "clock", "supports" : ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "readme": "README.md", "storage": [ {"name":"slopeclockpp.app.js","url":"app.js"}, diff --git a/apps/taglaunch/ChangeLog b/apps/taglaunch/ChangeLog index 981f50386..55315bf6e 100644 --- a/apps/taglaunch/ChangeLog +++ b/apps/taglaunch/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Use Bangle.showClock for changing to clock (Backport from launch) +0.03: Remove app from 'tool' when it has at least one other known tag + Add tag 'health' for apps like Heart Rate Monitor diff --git a/apps/taglaunch/README.md b/apps/taglaunch/README.md index 71eebae9f..57674c7a7 100644 --- a/apps/taglaunch/README.md +++ b/apps/taglaunch/README.md @@ -4,7 +4,7 @@ Launcher Based on the default launcher but puts all applications in a submenu by their tag. With many applications installed this can result in a faster applications selection than the linear access of the default launcher. -Currently the following tags are supported: clock, game, tool, bluetooth, outdoors, misc. +Currently the following tags are supported: clock, game, tool, bluetooth, outdoors, health and misc. Settings -------- diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js index c940284c2..aad61e298 100644 --- a/apps/taglaunch/app.js +++ b/apps/taglaunch/app.js @@ -8,7 +8,8 @@ let tags = {"clock": {name: /*LANG*/"Clocks", icon: atob("MDCEBERERERERERERERERE "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, "bluetooth": {name: /*LANG*/"Bluetooth", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, "outdoors": {name: /*LANG*/"Outdoor", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, - "misc": {name: /*LANG*/"Misc", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")} + "misc": {name: /*LANG*/"Misc", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, + "health": {name: /*LANG*/"Health"}, }; // handle customised launcher @@ -54,6 +55,9 @@ if (launchCache.hash!=launchHash) { .filter(tag => Object.keys(tags).includes(tag)); if (appTags.length === 0) { appTags.push("misc"); + } else if (appTags.length > 1 && appTags.indexOf("tool") >= 0) { + // everything has tag 'tool', unregister when at least one other known tag + appTags.splice(appTags.indexOf("tool"), 1); } appTags.forEach(tag => appsByTag[tag].push(app)); }); @@ -103,8 +107,10 @@ let showMainMenu = () => { let tag = tagKeys[i]; g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); g.setFont(font).setFontAlign(-1,0).drawString(tags[tag].name,64*scaleval,r.y+(32*scaleval)); - if (tags[tag].icon) { - try {g.drawImage(tags[tag].icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} + + const img = tags[tag].icon ? tags[tag].icon : s.read("taglaunch." + tag + ".img"); + if (img) { + try {g.drawImage(img,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} } }, select : i => { diff --git a/apps/taglaunch/health-icon.js b/apps/taglaunch/health-icon.js new file mode 100644 index 000000000..11b513b72 --- /dev/null +++ b/apps/taglaunch/health-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA==")) diff --git a/apps/taglaunch/metadata.json b/apps/taglaunch/metadata.json index d7f1954b1..4f7c295e9 100644 --- a/apps/taglaunch/metadata.json +++ b/apps/taglaunch/metadata.json @@ -2,16 +2,18 @@ "id": "taglaunch", "name": "Tag Launcher", "shortName": "Taglauncher", - "version": "0.02", + "version": "0.03", "description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.", "readme": "README.md", "icon": "app.png", "type": "launch", "tags": "tool,system,launcher", "supports": ["BANGLEJS2"], + "screenshots": [ {"url":"screenshot.png"} ], "storage": [ {"name":"taglaunch.app.js","url":"app.js"}, - {"name":"taglaunch.settings.js","url":"settings.js"} + {"name":"taglaunch.settings.js","url":"settings.js"}, + {"name":"taglaunch.health.img","url":"health-icon.js","evaluate":true} ], "data": [{"name":"taglaunch.json"},{"name":"taglaunch.cache.json"}] } diff --git a/apps/taglaunch/screenshot.png b/apps/taglaunch/screenshot.png new file mode 100644 index 000000000..cc3e9b8cf Binary files /dev/null and b/apps/taglaunch/screenshot.png differ diff --git a/apps/taglaunch/settings.js b/apps/taglaunch/settings.js index 52fa07a7f..94ec34a36 100644 --- a/apps/taglaunch/settings.js +++ b/apps/taglaunch/settings.js @@ -12,7 +12,7 @@ } const appMenu = { "": { "title": /*LANG*/"Tag Launcher" }, - /*LANG*/"< Back": back, + "< Back": back, /*LANG*/"Font": { value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"), min:0, max:fonts.length-1, step:1,wrap:true, diff --git a/apps/tempgraph/ChangeLog b/apps/tempgraph/ChangeLog new file mode 100644 index 000000000..74f2d7255 --- /dev/null +++ b/apps/tempgraph/ChangeLog @@ -0,0 +1,3 @@ +0.01: 3/Feb/2023 Added 'Temperature Graph' app to depository. +0.02: 4/Feb/2023 Rewrote the widget handling after discovering there's a 'widget_utils' module to properly hide and show them. +0.03: 4/Feb/2023 Fixed number error in timesData array. diff --git a/apps/tempgraph/README.md b/apps/tempgraph/README.md new file mode 100644 index 000000000..919c20267 --- /dev/null +++ b/apps/tempgraph/README.md @@ -0,0 +1,36 @@ +# Temperature Graph + +**Temperature Graph** (tempgraph) is a Bangle.js 2 app for recording graphs of the temperature for various time periods from 10 minutes to 7 days long. It samples the watch's temperature sensor 150 times while creating a graph, regardless of the time period selected. + +### Menu Options +* **Widgets** Toggles the watch's widgets on and off. With them off gives you a bigger graph when viewing it. + +* **Duration** Select the time period for drawing the graph, from 10 minutes to 7 days long. + +* **Draw Graph** Draws the graph. + * Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue). + * Pressing the watch button takes you back to the menu. **Note:** While the graph can still be viewed after returning to the menu, you can't continue recording it if you had returned to the menu before the time period was up. The graph is saved in the watch though so it's still there the next time you start the app. + +* **Show Graph** Shows the last drawn graph. + * Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue). + * Pressing the watch button takes you back to the menu. + +* **Save Graph** Sends a screengrab of the graph to the Espruino Web IDE from where you can save it as you would any image on a webpage. + +* **Save Data** Sends a CSV file of the graph's temperature data to the Espruino Web IDE where you can save it for further use. I suggest you use the Espruino Web IDE's Terminal Logger (selected in the IDE's Settings/General) to record the data as it's sent. This is the easiest way to save it as a text file. + +* **Show Temp** Shows the current temperature. + +### Note +Using the watch in a normal fashion can raise the temperature it's sensing to quite a few degrees above the surrounding temperature and it may take half an hour or so to drop to close to the surrounding temperature. After that it seems to give quite accurate readings, assuming the thermometer I've been comparing it to is itself reasonably accurate. So best to load the app then not touch the watch for half an hour before starting a recording. This is assuming you're not wearing the app and are just using it to record the temperature where you've put the watch. You could of course wear it and it'll still draw a graph, which might also be useful. + +### Screenshots +![](screenshot_1.png) +![](screenshot_2.png) +![](screenshot_3.png) + +### Creator +Carl Read ([mail](mailto:cread98@orcon.net.nz), [github](https://github.com/CarlR9)) + +#### License +[MIT License](LICENSE) diff --git a/apps/tempgraph/app-icon.js b/apps/tempgraph/app-icon.js new file mode 100644 index 000000000..50aae4c01 --- /dev/null +++ b/apps/tempgraph/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+goA/AH4AgrHXABFYF0XXkYAK64utGENYFxoABSTxeHXYJglF+UAAIQvEBApfhE4UAF4IDBFwZf/X7hfsR4K/tL96/vRwpf/X/5fJGYK/tL9u02i/tF4KOFL/6/XF4ZftR4K/tL96/vRwpf/X/5fJGYK/tL96/vRwpf/X7gADF8ouBGA4v/F/6/urAmGABFYF7pgIL0owPF0KSC64AIRj4A/AH4ACA=")) \ No newline at end of file diff --git a/apps/tempgraph/app.js b/apps/tempgraph/app.js new file mode 100644 index 000000000..b8dd231ae --- /dev/null +++ b/apps/tempgraph/app.js @@ -0,0 +1,394 @@ +// Temperature Graph +// BangleJS Script + +Bangle.setBarometerPower(true,"tempgraph"); +Bangle.loadWidgets(); +var widsOn=true; +var rm=null; +var gt=null; +var dg=null; +var Layout=require("Layout"); +var C=true; +var temp,tempMode,readErrCnt,watchButton2; + +var graph=require("Storage").readJSON("tempgraph.json",true); +if(graph==undefined) { + graph=[]; +} + +var timesData=[ + // dur=duration, u=time units, d=divisions on graph, s=seconds per unit. + {dur:10,u:"Mins",d:5,s:60}, + {dur:20,u:"Mins",d:4,s:60}, + {dur:30,u:"Mins",d:3,s:60}, + {dur:40,u:"Mins",d:4,s:60}, + {dur:1,u:"Hr",d:4,s:3600}, + {dur:2,u:"Hrs",d:4,s:3600}, + {dur:3,u:"Hrs",d:3,s:3600}, + {dur:4,u:"Hrs",d:4,s:3600}, + {dur:6,u:"Hrs",d:6,s:3600}, + {dur:8,u:"Hrs",d:4,s:3600}, + {dur:12,u:"Hrs",d:6,s:3600}, + {dur:16,u:"Hrs",d:4,s:3600}, + {dur:20,u:"Hrs",d:5,s:3600}, + {dur:1,u:"Day",d:4,s:86400}, + {dur:2,u:"Days",d:4,s:86400}, + {dur:3,u:"Days",d:3,s:86400}, + {dur:4,u:"Days",d:4,s:86400}, + {dur:5,u:"Days",d:5,s:86400}, + {dur:6,u:"Days",d:6,s:86400}, + {dur:7,u:"Days",d:7,s:86400} +]; +var times=[]; +for(n=0;n{ + temp=p.temperature; + if(tempMode=="drawGraph"&&graph.length>0&&Math.abs(graph[graph.length-1].temp-temp)>10&&readErrCnt<2){ + // A large change in temperature may be a reading error. ie. A 0C or less reading after + // a 20C reading. So if this happens, the reading is repeated up to 2 times to hopefully + // skip such errors. + readErrCnt++; + print("readErrCnt "+readErrCnt); + return; + } + clearInterval(gt); + readErrCnt=0; + switch (tempMode){ + case "showTemp": + showT(); + break; + case "drawGraph": + var date=new Date(); + var dateStr=require("locale").date(date).trim(); + var hrs=date.getHours(); + var mins=date.getMinutes(); + var secs=date.getSeconds(); + graph.push({ + temp:temp, + date:dateStr, + hrs:hrs, + mins:mins, + secs:secs + }); + if(graph.length==1){ + graph[0].dur=durInd; + } + require("Storage").writeJSON("tempgraph.json", graph); + if(graph.length==150){ + clearInterval(dg); + } + drawG(); + } + }); +} + +function getTemp(){ + readErrCnt=0; + gt = setInterval(getT,800); +} + +function setButton(){ + var watchButton=setWatch(function(){ + clearInterval(gt); + clearInterval(dg); + clearWatch(watchButton); + Bangle.removeListener("touch",screenTouch); + openMenu(); + },BTN); + Bangle.on('touch',screenTouch); +} + +function setButton2(){ + watchButton2=setWatch(function(){ + clearWatch(watchButton2); + openMenu(); + },BTN); +} + +function zPad(n){ + return n.toString().padStart(2,0); +} + +function screenTouch(n,ev){ + if(ev.y>23&&ev.y<152){ + C=C==false; + drawG(false); + } +} + +function drawG(){ + function cf(t){ + if(C){ + return t; + } + return getF(t); + } + drawWids(); + var top=1; + var bar=21; + var barBot=175-22; + if(widsOn){ + top=25; + bar=bar+24; + barBot=barBot-24; + } + var low=graph[0].temp; + var hi=low; + for(n=0;nt){ + low=t; + } + if(hi10){ + div=5; + } + if(C){ + g.setColor(1,0,0); + }else{ + g.setColor(0,0,1); + } + var step=(barBot-bar)/((tempHi-tempLow)/div); + for(n=0;nexit()}, + ],lazy:true + }); + drawWids(); + messageLO.render(); +} + +function showT(){ + tempLO.lab1.label=tempLO.lab3.label; + tempLO.lab2.label=tempLO.lab4.label; + tempLO.lab3.label=tempLO.lab5.label; + tempLO.lab4.label=tempLO.lab6.label; + tempLO.lab5.label=temp.toFixed(2)+"C"; + tempLO.lab6.label=getF(temp).toFixed(2)+"F"; + tempLO.render(); +} + +function exit(){ + clearWatch(watchButton2); + openMenu(); +} + +function showTemp(){ + tempMode="showTemp"; + setButton2(); + tempLO=new Layout({ + type:"v",c:[ + {type:"h",c:[ + {type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab1"}, + {type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab2"} + ]}, + {type:"h",c:[ + {type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab3"}, + {type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab4"} + ]}, + {type:"h",c:[ + {type:"txt",pad:5,col:"#f00",font:"6x8:2",label:" ",id:"lab5"}, + {type:"txt",pad:5,col:"#00f",font:"6x8:2",label:" ",id:"lab6"} + ]}, + {type:"h",c:[ + {type:"btn",pad:2,font:"6x8:2",label:"Temp",cb:l=>getTemp()}, + {type:"btn",pad:2,font:"6x8:2",label:"Exit",cb:l=>exit()} + ]} + ] + },{lazy:true}); + tempLO.render(); + getTemp(); +} + +var menu={ + "":{ + "title":" Temp. Graph" + }, + + "Widgets":{ + value:widsOn, + format:vis=>vis?"Hide":"Show", + onchange:vis=>{ + widsOn=vis; + refreshMenu(); + } + }, + + "Duration":{ + value:times.indexOf(duration), + min:0,max:times.length-1,step:1,wrap:true, + format:tim=>times[tim], + onchange:(dur)=>{ + duration=times[dur]; + } + }, + + "Draw Graph":function(){ + E.showMenu(); + drawGraph(); + }, + + "Show Graph" : function(){ + E.showMenu(); + if(graph.length>0){ + showGraph(); + }else{ + message("No graph to\nshow as no\ngraph has been\ndrawn yet."); + } + }, + + "Save Graph" : function(){ + E.showMenu(); + if(graph.length>0){ + saveGraph(); + }else{ + message("No graph to\nsave as no\ngraph has been\ndrawn yet."); + } + }, + + "Save Data" : function(){ + E.showMenu(); + if(graph.length>0){ + saveData(); + }else{ + message("No data to\nsave as no\ngraph has been\ndrawn yet."); + } + }, + + "Show Temp":function(){ + E.showMenu(); + showTemp(); + } +}; + +openMenu(); diff --git a/apps/tempgraph/app.png b/apps/tempgraph/app.png new file mode 100644 index 000000000..1ae347ac8 Binary files /dev/null and b/apps/tempgraph/app.png differ diff --git a/apps/tempgraph/metadata.json b/apps/tempgraph/metadata.json new file mode 100644 index 000000000..63d4feddd --- /dev/null +++ b/apps/tempgraph/metadata.json @@ -0,0 +1,19 @@ +{ "id": "tempgraph", + "name": "Temperature Graph", + "shortName":"Temp Graph", + "version":"0.03", + "description": "An app for recording the temperature for time periods ranging from 10 minutes to 7 days.", + "icon": "app.png", + "type": "app", + "tags": "temperature,tempgraph,graph", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"tempgraph.app.js","url":"app.js"}, + {"name":"tempgraph.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"tempgraph.json"} + ], + "screenshots": [{"url":"screenshot_1.png"},{"url":"screenshot_2.png"},{"url":"screenshot_3.png"}] +} diff --git a/apps/tempgraph/screenshot_1.png b/apps/tempgraph/screenshot_1.png new file mode 100644 index 000000000..7395f2896 Binary files /dev/null and b/apps/tempgraph/screenshot_1.png differ diff --git a/apps/tempgraph/screenshot_2.png b/apps/tempgraph/screenshot_2.png new file mode 100644 index 000000000..68d0ea236 Binary files /dev/null and b/apps/tempgraph/screenshot_2.png differ diff --git a/apps/tempgraph/screenshot_3.png b/apps/tempgraph/screenshot_3.png new file mode 100644 index 000000000..33f3d612e Binary files /dev/null and b/apps/tempgraph/screenshot_3.png differ diff --git a/apps/tempmonitor/ChangeLog b/apps/tempmonitor/ChangeLog index 99fe6a77d..af69e6e0a 100644 --- a/apps/tempmonitor/ChangeLog +++ b/apps/tempmonitor/ChangeLog @@ -1,2 +1,3 @@ 0.01: 1st version: saves values to csv 0.02: added HTML interface +0.03: Added Stop/start recording, change BG color, filesize info diff --git a/apps/tempmonitor/README.md b/apps/tempmonitor/README.md index a956f0e0f..e50ac07cb 100644 --- a/apps/tempmonitor/README.md +++ b/apps/tempmonitor/README.md @@ -9,6 +9,12 @@ Bangle JS1 ![](photo_banglejs1.jpg) +UI for bangleJS1 +![](bangle.js_UI.png) + +UI for bangleJS2 +![](bangle.js2_UI.png) + Screenshot BJS2 ![](ss_emul_bjs2.png) @@ -30,18 +36,35 @@ Screenshot data file content Open and see a temperature in the screen Download the CSV file and process in your favourite spreadsheet software +if you have any problem enable the modedebug in code; v_mode_debug=1 or 2 ## Features -Colours, all inputs , graph, widgets loaded -Counter for Times Display +- Cross compatibility (JS1,JS2) and widgets compatibility +- BG/FG Colour, Export to file and counter of saved records per session +- File operations: Info, delete (no yet) +## Pending/future Features +- Buttons layout: btn txt(BJS1) , on screen button (BJS2) +- Long press touch to delete file (BJS1,BJS2) +- File operations: Delete -## Controls - -exit: left side +## Controls/UI + - Left area: Back/Exit/launcher + - BTN3 (long press)(BJS1): default Exit/kill app + - BTN1 (BJS2): "Launcher" / open "Messages" + - BTN2 (BJS1): "Launcher" / open "Messages" + - BTN1 (BJS1): Change FG Color + - BTN3 (BJS1): Change BG Color + - Right area: Change FG Color + - Swipe left: Change BG Color + - Swipe right: Increase/Decrease Hour circle/Points + + ## Creator -Daniel Perez \ No newline at end of file +Daniel Perez +For suggestions or feedback +https://github.com/dapgo/my_espruino_smartwatch_things \ No newline at end of file diff --git a/apps/tempmonitor/bangle.js2_UI.png b/apps/tempmonitor/bangle.js2_UI.png new file mode 100644 index 000000000..ecfc31da8 Binary files /dev/null and b/apps/tempmonitor/bangle.js2_UI.png differ diff --git a/apps/tempmonitor/bangle.js_UI.png b/apps/tempmonitor/bangle.js_UI.png new file mode 100644 index 000000000..4b114b72c Binary files /dev/null and b/apps/tempmonitor/bangle.js_UI.png differ diff --git a/apps/tempmonitor/metadata.json b/apps/tempmonitor/metadata.json index dafb70f27..45d4d2c35 100644 --- a/apps/tempmonitor/metadata.json +++ b/apps/tempmonitor/metadata.json @@ -1,7 +1,7 @@ { "id": "tempmonitor", "name": "Temperature monitor", - "version": "0.02", + "version": "0.03", "description": "Displays the current temperature and stores in a CSV file", "icon": "app.png", "tags": "tool", diff --git a/apps/tempmonitor/tempmonitor.app.js b/apps/tempmonitor/tempmonitor.app.js index 62a4fee67..9a6e85645 100644 --- a/apps/tempmonitor/tempmonitor.app.js +++ b/apps/tempmonitor/tempmonitor.app.js @@ -1,42 +1,69 @@ // Temperature monitor that saves a log of measures // standalone ver for developer, to remove testing lines // delimiter ; (excel) or , (oldscool) +/* REFACTOR and remove commented code related to +SetUI, Layout, and setWatch( function(b) { }, BTN1, { repeat: true, edge:'falling' }) +*/ { var v_mode_debug=0; //, 0=no, 1 min, 2 prone detail //var required for drawing with dynamic screen var rect = Bangle.appRect; var history = []; -var readFreq=5000; //ms //PEND add to settings -var saveFreq=60000; //ms 1min +var readFreq=4000; //ms //PEND add to settings +if (v_mode_debug>0) var saveFreq=6000; //ms for testin 6sec +else var saveFreq=60000; //ms 1min var v_saveToFile= new Boolean(true); //true save //false //with upload file º is not displayed properly //with upload RAM º is displayed var v_t_symbol="";//ºC var v_saved_entries=0; -var filename ="temphistory.csv"; +var v_filename ="temphistory.csv"; var lastMeasure = new String(); var v_model=process.env.BOARD; +var v_color_erase=g.getBgColor(); //original BG color overwritten on SetVariables +var v_color=g.getColor();//original FG color +var id_rec_intv; //var for the recording interval +if (readFreq>saveFreq) console.log("Read refresh freq should be higher than saving"); +if (v_mode_debug>0) console.log("original BG/FG color="+v_color_erase+" / "+v_color); + + + +function SetVariables(){ //EMSCRIPTEN,EMSCRIPTEN2 if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') { v_font_size1=16; - v_font_size2=60; - //g.setColor("#0ff"); //light color + v_font_size2=50; }else{ - v_font_size1=11; - v_font_size2=40; - //g.setColor("#000"); //black or dark + //Banglejs2 or others + v_font_size1=11; //too small? + v_font_size2=40; } + //overwriting default BG, is better detect? + if (g.theme.dark==1) v_color_erase=0x0000; //dynamic; //bg black + else if (g.theme.dark==0) v_color_erase=0xFFFF; //dynamic; //bg white +} -function onTemperature(v_temp) { - if (v_mode_debug>1) console.log("v_temp in "+v_temp); +//print result +function printTemperature(v_temp) { + if (v_mode_debug>1) console.log("v_temp in "+v_temp+" entries "+v_saved_entries); ClearBox(); //g.setFont("6x8",2).setFontAlign(0,0); g.setFontVector(v_font_size1).setFontAlign(0,0); - var x = (rect.x+rect.x2)/2; + var x = (rect.x+(rect.x2-60))/2;//-60 space for graph and layout buttons var y = (rect.y+rect.y2)/2 + 20; - g.drawString("Records: "+v_saved_entries, x, rect.y+35); - g.drawString("Temperature:", x, rect.y+37+v_font_size1); + + if (v_saveToFile==true) { + // if (v_mode_debug>0) console.log("prev color="+v_color); + printInfo("Recording : "+v_saved_entries, '#CC3333',x,rect.y+30); + //g.setColor('#CC3333'); //red + // g.drawString("Recording : "+v_saved_entries, x, rect.y+35); + //g.setColor(v_color);//restore default color + } + else printInfo("Rec paused : "+v_saved_entries, v_color,x,rect.y+30); + //else g.drawString("Rec paused : "+v_saved_entries, x, rect.y+35); + //space for printing info + g.drawString("Temperature:", x, rect.y+45+(v_font_size1*2)); //dynamic font (g.getWidth() > 200 ? 60 : 40) g.setFontVector(v_font_size2).setFontAlign(0,0); // Avg of temperature readings @@ -48,33 +75,68 @@ function onTemperature(v_temp) { lastMeasure=avrTemp.toString(); if (lastMeasure.length>4) lastMeasure=lastMeasure.substr(0,4); //DRAW temperature in the center - g.drawString(" ", x-20, y); - g.drawString(v_temp+v_t_symbol, x-20, y); + //remove g.drawString(" ", x-20, y); + g.drawString(v_temp+v_t_symbol, x, y); g.flip(); } // from: BJS2 pressure sensor, BJS1 inbuilt thermistor -function drawTemperature() { +function getTemperature() { if(v_model.substr(0,10)!='EMSCRIPTEN'){ if (Bangle.getPressure) { - Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); - } else onTemperature(E.getTemperature()); + Bangle.getPressure().then(p =>{if (p) printTemperature(p);}); + } else printTemperature(E.getTemperature()); } - else onTemperature(11);//fake temp for emulators + else printTemperature(11.25);//fake temperature medition for emulators } -function saveToFile() { +/* Note that it changes BG and also FG to an opposite*/ +function changeBGcolor(){ + //pend to refactor + if (v_mode_debug>1) console.log("before BG/FG "+v_color_erase+" /"+v_color); + v_color_erase=0xFFFF-v_color_erase; + v_color=0xFFFF-v_color; + if (v_mode_debug>1) console.log("after result BG/FG "+v_color_erase+" /"+v_color); + //g.setColor(color_result); + g.setBgColor(v_color_erase);// 0 white, 1 black + g.setColor(v_color); + //move to event? + ClearScreen(); + ClearBox(); + drawGraph(); + getTemperature(); + //setDrawLayout(); //uncomment if layout can work with setUI + //g.clear();//impact on widgets +} + +function saveToFile(){ //input global vars: lastMeasure var a=new Date(); var strlastSaveTime=new String(); strlastSaveTime=a.toISOString(); //strlastSaveTime=strlastSaveTime.concat(a.getFullYear(),a.getMonth()+1,a.getDate(),a.getHours(),a.getMinutes());; - if (v_mode_debug==1) console.log("saving="+strlastSaveTime+";"+a.getHours()+":"+a.getMinutes()+";"+lastMeasure); + if (v_mode_debug>1) console.log("saving="+strlastSaveTime+";"+a.getHours()+":"+a.getMinutes()+";"+lastMeasure); if (v_saveToFile==true){ - //write(strlastSaveTime+";"+ - require("Storage").open(filename,"a").write((a.getMonth()+1)+";"+a.getDate()+";"+a.getHours()+":"+a.getMinutes()+";"+lastMeasure+"\n"); - //(getTime()+","); - v_saved_entries=v_saved_entries+1; + //write(strlastSaveTime+";"+ + //var f = require("Storage").open(v_filename,"r"); + // f=require("Storage").read(v_filename+"\1");//suffix required load completely!! + //note that .read uses Storage Class .open uses StorageFile Class , difference in file chunks + // if (v_mode_debug>0) console.log("f "+f); + var f = require("Storage").open(v_filename,"r"); + if ((v_mode_debug>0) && (v_saved_entries==0)) console.log("file info:"+f); + if (f.len>0) { + if (!f) { + require("Storage").open(v_filename,"w").write("Month;Day;Time;Temp"+"\n"); + if (v_mode_debug>0) console.log("not exist but created "+f); + } + else{ + require("Storage").open(v_filename,"a").write((a.getMonth()+1)+";"+a.getDate()+";"+a.getHours()+":"+a.getMinutes()+";"+lastMeasure+"\n"); + //(getTime()+","); + v_saved_entries=v_saved_entries+1; + if (v_mode_debug>1) console.log("append to already exist "+f.name+" , "+v_saved_entries); + } + } } + else if (v_mode_debug>0) console.log("recording mode stopped"); } function drawGraph(){ @@ -83,17 +145,19 @@ function drawGraph(){ transparent : 0, buffer : require("heatshrink").decompress(atob("AEFt2AMKm3bsAMJjdt23ABhEB+/7tgaJ///DRUP//7tuADRP923YDRXbDRfymwaJhu/koaK7eyiwaK3cLDRlWDRY1NKBY1Ztu5kjmJg3cyVI7YMHgdu5Mkyu2fxHkyVJjdgDRFJkmRDRPsDQNbDQ5QBGoONKBJrBoxQIQwO2eRcbtu24AMIFIQLJAH4AMA==")) }; - g.drawImage(img_obj_thermo,rect.x2-50,rect.y2/2); + g.drawImage(img_obj_thermo,rect.x2-60,rect.y2/2); g.flip(); } function ClearScreen(){ //avoid widget areas - g.reset(1).clearRect(rect.x, rect.y+24, rect.x2, rect.y2-24); + g.setBgColor(v_color_erase); + g.clearRect(rect.x, rect.y+24, rect.x2, rect.y2-24); g.flip(); } function ClearBox(){ //custom boxarea , left space for static graph at right - g.reset(1).clearRect(rect.x, rect.y+24, rect.x2-50, rect.y2-24); + g.setBgColor(v_color_erase); + g.clearRect(rect.x, rect.y+24, rect.x2-60, rect.y2-24); g.flip(); } function introPage(){ @@ -109,30 +173,140 @@ function introPage(){ g.drawString("Read freq(ms): "+readFreq, x, y ); g.drawString("Save to file: "+v_saveToFile, x, y+ ((v_font_size1*1)+2) ); g.drawString("Save freq(ms):"+saveFreq, x, y+((v_font_size1*2)+2) ); - fr=require("Storage").read(filename+"\1");//suffix required - if (fr) g.drawString("Current filesize:"+fr.length.toString()+"kb", x, y+((v_font_size1*3)+2) ); + fr=require("Storage").read(v_filename+"\1");//suffix required + if (fr) g.drawString("Filesize:"+fr.length.toString()+"kb", x, y+((v_font_size1*3)+2) ); else g.drawString("File not exist", x, y+((v_font_size1*3)+2)); } +function printInfo(pmsg, pcolor,px,py){ + g.setColor(pcolor); + g.setFontVector(v_font_size1).setFontAlign(0,0); + g.drawString(pmsg, px,py+v_font_size1); + g.setColor(v_color);//restore default color +} +function toggleRecMode(duration, exectime){ + //bydefault float, standard epoch requires *1000 + if (v_mode_debug>0) console.log("duration"+duration); + if (duration>2) { //delete file + var x = (rect.x+(rect.x2-60))/2; + printInfo("Deleting file",'#CC3333',x, rect.y+32+v_font_size1); + // g.setColor('#CC3333'); //red + + //too long "Deleting file: "+v_filename, + // for StorageFiles created with require("Storage").open(filename, ...) + //require("Storage").erase(v_filename); + //TODO refactor in a new function + //var mifile = require("Storage").open(v_filename,"w"); + var mifile = require("Storage").open("temphistory.csv","w"); + var v_output=mifile.erase(); + //mifile.StorageFile.erase(); + if (v_mode_debug>0) console.log("output"+v_output); + setTimeout(function() { if (v_mode_debug>0) console.log("pause for 1 sec");},1000); + return; //leave this function + } + if (v_saveToFile) v_saveToFile=false; + else v_saveToFile=true; + if (v_mode_debug>0) console.log("recording? "+v_saveToFile); + setRecordingFreq(); +} + +function setRecordingFreq(){ + if (v_saveToFile==true) { //TODO now start on false btn will no enable + id_rec_intv=setInterval(function() { + saveToFile(); + }, saveFreq); //ms + if (v_mode_debug>0) console.log("interval id / frq"+id_rec_intv+" / "+saveFreq); + } + else if (id_rec_intv){ + clearInterval(id_rec_intv); + if (v_mode_debug>0) console.log("rec interval removed, id "+id_rec_intv); + id_rec_intv=0; // to reset var + } +} + +function UserInput(){ + //theoretically incompatible with Layout + Bangle.setUI({ + mode : "custom", + //adds a back icon on top widget area + back : function() {load();}, + //touch : function(n,e) {}, // optional - handler for 'touch' events + // righ/Left 1/-1 , updown + swipe : function(dir_rl,dir_ud) { + if(dir_rl == 1) { + if (v_mode_debug>0) console.log("swipe right: "); + getFileInfo(v_filename); + } + else if (dir_rl == -1){ + if (v_mode_debug>0) console.log("swipe left: "); + changeBGcolor(); + } + }, + touch : function(tzone,tobj){ + if ((process.env.HWVERSION == 2)&&(v_mode_debug>0)){ + console.log("tobj x,y,type : "+tobj.x+" "+tobj.y+" "+tobj.type); + } + switch(tzone){ + //case 1: //left , back managed by setUI + case 2: // right disable/enable recording + toggleRecMode(0); //toggleRecMode(duration, exectime) + break; + // case 3: console.log("Touch 3 aka 1+2 not for BJS1 emul");//center 1+2 + // break; + } + }, + //inferior to + btn : function(btn) { + if(btn == 1) { + if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') toggleRecMode(1); //console.log("btn1 BJS1"); + else mainBtnShortcut(); //console.log("btn1 BJS2"); + } + else if (btn == 2) mainBtnShortcut(); //console.log("btn2 BJS1"); + else if (btn == 3) changeBGcolor(); //console.log("btn3 BJS1"); + } + }); //endof setUI + +} + +function mainBtnShortcut() { + //if messages app installed shortcut otherwise default access to launcher + if (require("Storage").read("messagegui.app.js")===undefined) + { + if (require("Storage").read("messagelist.app.js")===undefined) Bangle.showLauncher(); // implies btn2(js1) btn(js2)- launcher + else if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') load("messagelist.app.js"); + else load("messagelist.app.js"); + } + else if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') load("messagegui.app.js"); + else load("messagegui.app.js"); + } + + +// Show file size +function getFileInfo(v_filename) { + var f = require("Storage").open(v_filename,"r"); + //todo refactor and reuse common code + g.setFontVector(v_font_size1).setFontAlign(0,0); + var x = (rect.x+(rect.x2-60))/2; + printInfo("file size:"+f.len,v_color,x, rect.y+32+v_font_size1); + // g.drawString("file size:"+f.len, x, rect.y+37+v_font_size1); + if (v_mode_debug>0) console.log("file "+v_filename+" size: "+f.len); +}// not used + + //MAIN +SetVariables(); Bangle.loadWidgets(); -Bangle.setUI({ - mode : "custom", - back : function() {load();} -}); ClearScreen(); introPage(); +//setDrawLayout(); //uncomment if layout can work with setUI + +UserInput(); //inc SetUI and back icon + setInterval(function() { - drawTemperature(); + getTemperature(); }, readFreq); //ms -if (v_saveToFile==true) { - setInterval(function() { - saveToFile(); - }, saveFreq); //ms -} -setTimeout(ClearScreen, 3500); -setTimeout(drawGraph,4000); -setTimeout(drawTemperature,4500); +setRecordingFreq(); + } \ No newline at end of file diff --git a/apps/tempmonitor/tempmonitor.info b/apps/tempmonitor/tempmonitor.info index 1824c5c86..f31704b57 100644 --- a/apps/tempmonitor/tempmonitor.info +++ b/apps/tempmonitor/tempmonitor.info @@ -1 +1 @@ -{"id":"tempmonitor","name":"tempmonitor","src":"tempmonitor.app.js","icon":"tempmonitor.img","version":"0.01","files":"tempmonitor.info,tempmonitor.app.js,tempmonitor.img"} \ No newline at end of file +{"id":"tempmonitor","name":"tempmonitor","src":"tempmonitor.app.js","icon":"tempmonitor.img","version":"0.03","files":"tempmonitor.info,tempmonitor.app.js,tempmonitor.img"} \ No newline at end of file diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog index 268e0427c..acdbf0f1e 100644 --- a/apps/terminalclock/ChangeLog +++ b/apps/terminalclock/ChangeLog @@ -7,3 +7,5 @@ 0.07: Use ClockFace module and rework the settings to be able to personnalize the order of the lines 0.08: Hide widgets instead of not loading them at all Use Clockface_menu for widgets and power saving settings +0.09: Add default HRM value, default altitude value +0.10: Add fastloading diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js index 515ad8f66..b6cea9d42 100644 --- a/apps/terminalclock/app.js +++ b/apps/terminalclock/app.js @@ -1,211 +1,238 @@ -const locale = require("locale"); -var heartRate = 0; -var altitude = -9001; +{ + const locale = require("locale"); + let heartRate = 0; + let altitude = -9001; -const fontColor = g.theme.dark ? "#0f0" : "#000"; -// handling the differents versions of the Banglejs smartwatch screen sizes -if (process.env.HWVERSION == 1){ - var paddingY = 3; - var font6x8At4Size = 48; - var font6x8At2Size = 27; - var font6x8FirstTextSize = 6; - var font6x8DefaultTextSize = 3; -} else{ - var paddingY = 2; - var font6x8At4Size = 32; - var font6x8At2Size = 18; - var font6x8FirstTextSize = 4; - var font6x8DefaultTextSize = 2; -} + const fontColor = g.theme.dark ? "#0f0" : "#000"; + // handling the differents versions of the Banglejs smartwatch screen sizes + // default BJS2 + let paddingY = 2; + let font6x8At4Size = 32; + let font6x8At2Size = 18; + let font6x8FirstTextSize = 4; + let font6x8DefaultTextSize = 2; + if (process.env.HWVERSION == 1){ + paddingY = 3; + font6x8At4Size = 48; + font6x8At2Size = 27; + font6x8FirstTextSize = 6; + font6x8DefaultTextSize = 3; + } + // initialising the clockface + const ClockFace = require("ClockFace"); + const clock = new ClockFace({ + precision: 60, + settingsFile: "terminalclock.json", -// initialising the clockface -const ClockFace = require("ClockFace"); -const clock = new ClockFace({ - precision: 60, - settingsFile: "terminalclock.json", + init: function () { + // check settings and set default if needed + this.showHRM = false; + this.showAltitude = false; + this.lock_precision = this.precision; + this.unlock_precision = 1; + if (this.HRMinConfidence === undefined) this.HRMinConfidence = 50; + if (this.PowerOnInterval === undefined) this.PowerOnInterval = 15; + if (this.powerSave===undefined) this.powerSave = this.powerSaving; // migrate old setting + if (this.powerSave===undefined) this.powerSave = true; - init: function () { - // check settings and set default if needed - this.showHRM = false; - this.showAltitude = false; - this.lock_precision = this.precision; - this.unlock_precision = 1; - if (this.HRMinConfidence === undefined) this.HRMinConfidence = 50; - if (this.PowerOnInterval === undefined) this.PowerOnInterval = 15; - if (this.powerSave===undefined) this.powerSave = this.powerSaving; // migrate old setting - if (this.powerSave===undefined) this.powerSave = true; - ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(k => { - if (this[k]===undefined){ - if(k == "L2") this[k] = "Date"; - else if(k == "L3") { - this[k] = "HR"; - this.showHRM = true; - }else if(k == "L4") this[k] = "Motion"; - else if(k == "L5") this[k] = "Steps"; - else if(k == "L6") this[k] = ">"; - else this[k] = "Empty"; - } - else if (this[k]==="HR") this.showHRM = true; - else if (this[k]==="Alt") this.showAltitude = true && process.env.HWVERSION == 2; - }); + ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(k => { + if (this[k]===undefined){ + if(k == "L2") this[k] = "Date"; + else if(k == "L3") { + this[k] = "HR"; + this.showHRM = true; + }else if(k == "L4") this[k] = "Motion"; + else if(k == "L5") this[k] = "Steps"; + else if(k == "L6") this[k] = ">"; + else this[k] = "Empty"; + } + else if (this[k]==="HR") this.showHRM = true; + else if (this[k]==="Alt") this.showAltitude = true && process.env.HWVERSION == 2; + }); - // set the lock and unlock actions - Bangle.on("lock", on => { - if (on) lock(); - else unlock(); - }); + // set the services (HRM, pressure sensor, etc....) + if(!this.powerSave){ + turnOnServices(); + } else{ + this.turnOnInterval = setInterval(turnOnServices, this.PowerOnInterval*60000); // every PowerOnInterval min + } + // start the clock unlocked + unlock(); + }, - // set the services (HRM, pressure sensor, etc....) - if(!this.powerSave){ - turnOnServices(); - } else{ - setInterval(turnOnServices, this.PowerOnInterval*60000); // every PowerOnInterval min - } - // start the clock unlocked - unlock(); - }, - - draw: function (date) { - var curPos = 1; - g.setFontAlign(-1, -1); - g.setColor(fontColor); - drawTime(date, curPos); - curPos++; - - ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(line => { - if (this[line]==='Date') drawDate(date, curPos); - else if (this[line]==='HR') drawHRM(curPos); - else if (this[line]==='Motion') drawMotion(curPos); - else if (this[line]==='Alt') drawAltitude(curPos); - else if (this[line]==='Steps') drawStepCount(curPos); - else if (this[line]==='>') drawInput(curPos); + draw: function (date) { + let curPos = 1; + g.setFontAlign(-1, -1); + g.setColor(fontColor); + drawTime(date, curPos); curPos++; - }); - }, -}); + ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(line => { + if (this[line]==='Date') drawDate(date, curPos); + else if (this[line]==='HR') drawHRM(curPos); + else if (this[line]==='Motion') drawMotion(curPos); + else if (this[line]==='Alt') drawAltitude(curPos); + else if (this[line]==='Steps') drawStepCount(curPos); + else if (this[line]==='>') drawInput(curPos); + curPos++; + }); + }, -/* ---------------------------- -Draw related of specific lines --------------------------------- */ - -function drawLine(line, pos){ - if(pos == 1) - g.setFont("6x8", font6x8FirstTextSize); - else - g.setFont("6x8", font6x8DefaultTextSize); - - var yPos = Bangle.appRect.y + - paddingY * (pos - 1) + - font6x8At4Size * Math.min(1, pos-1) + - font6x8At2Size * Math.max(0, pos-2); - g.drawString(line, 5, yPos, true); -} - -function drawTime(now, pos){ - var h = now.getHours(); - var m = now.getMinutes(); - var time = ">" + (""+h).substr(-2) + ":" + ("0"+m).substr(-2); - drawLine(time, pos); -} - -function drawDate(now, pos){ - var dow = locale.dow(now, 1); - var date = locale.date(now, 1).substr(0,6) + locale.date(now, 1).substr(-2); - var locale_date = ">" + dow + " " + date; - drawLine(locale_date, pos); -} - -function drawInput(pos){ - drawLine(">", pos); -} - -function drawStepCount(pos){ - var health = Bangle.getHealthStatus("day"); - var steps_formated = ">Steps: " + health.steps; - drawLine(steps_formated, pos); -} - -function drawHRM(pos){ - if(heartRate != 0) - drawLine(">HR: " + parseInt(heartRate), pos); - else - drawLine(">HR: unknown", pos); -} - -function drawAltitude(pos){ - if(altitude > 0) - drawLine(">Alt: " + altitude.toFixed(1) + "m", pos); - else - drawLine(">Alt: unknown", pos); -} - -function drawMotion(pos){ - var health = Bangle.getHealthStatus('last'); - var steps_formated = ">Motion: " + parseInt(health.movement); - drawLine(steps_formated, pos); -} - -/* ----------------------------------------------- -Services functions (HRM, pressure, etc...) --------------------------------------------------- */ - -function turnOnServices(){ - if(clock.showHRM){ - Bangle.setHRMPower(true, "terminalclock"); - } - if(clock.showAltitude){ - Bangle.setBarometerPower(true, "terminalclock"); - } - if(clock.powerSave){ - setTimeout(function () { + remove: function() { + if (this.turnOnInterval){ + clearInterval(this.turnOnInterval); + delete this.turnOnInterval; + } + if (this.turnOffServiceTimeout){ + clearTimeout(this.turnOffServiceTimeout) + delete this.turnOffServiceTimeout + } turnOffServices(); - }, 45000); - } -} + if (this.onLock) Bangle.removeListener('lock', this.onLock); + if (this.onHRM) Bangle.removeListener('HRM', this.onHRM); + if (this.onPressure) Bangle.removeListener('pressure', this.onPressure); + } -function turnOffServices(){ - if(clock.showHRM){ - Bangle.setHRMPower(false, "terminalclock"); - } - if(clock.showAltitude){ - Bangle.setBarometerPower(false, "terminalclock"); - } -} + }); -Bangle.on('HRM',function(hrmInfo) { - if(hrmInfo.confidence >= clock.HRMinConfidence) - heartRate = hrmInfo.bpm; -}); -const MEDIANLENGTH = 20; // technical -var avr = [], median; // technical -Bangle.on('pressure', function(e) { - while (avr.length>MEDIANLENGTH) avr.pop(); - avr.unshift(e.altitude); - median = avr.slice().sort(); - if (median.length>10) { - var mid = median.length>>1; - altitude = E.sum(median.slice(mid-4,mid+5)) / 9; - } -}); + /* ---------------------------- + Draw related of specific lines + -------------------------------- */ -/* ------------------------------------------------- -Clock related functions but not in the ClockFace module ----------------------------------------------------- */ + let drawLine = function(line, pos){ + if(pos == 1) + g.setFont("6x8", font6x8FirstTextSize); + else + g.setFont("6x8", font6x8DefaultTextSize); -function unlock(){ - if(clock.powerSave){ - turnOnServices(); - } - clock.precision = clock.unlock_precision; - clock.tick(); -} + let yPos = Bangle.appRect.y + + paddingY * (pos - 1) + + font6x8At4Size * Math.min(1, pos-1) + + font6x8At2Size * Math.max(0, pos-2); + g.drawString(line, 5, yPos, true); + }; -function lock(){ - clock.precision = clock.lock_precision; - clock.tick(); -} + let drawTime = function(now, pos){ + let h = now.getHours(); + let m = now.getMinutes(); + let time = ">" + (""+h).substr(-2) + ":" + ("0"+m).substr(-2); + drawLine(time, pos); + }; -// starting the clock -clock.start(); + let drawDate = function(now, pos){ + let dow = locale.dow(now, 1); + let date = locale.date(now, 1).substr(0,6) + locale.date(now, 1).substr(-2); + let locale_date = ">" + dow + " " + date; + drawLine(locale_date, pos); + }; + + let drawInput = function(pos){ + drawLine(">", pos); + }; + + let drawStepCount = function(pos){ + let health = Bangle.getHealthStatus("day"); + let steps_formated = ">Steps: " + health.steps; + drawLine(steps_formated, pos); + }; + + let drawHRM = function(pos){ + if(heartRate != 0) + drawLine(">HR: " + parseInt(heartRate), pos); + else + drawLine( + ">HR: " + parseInt(Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm)), + pos); + }; + + let drawAltitude = function(pos){ + if(altitude > 0) + drawLine(">Alt: " + altitude.toFixed(1) + "m", pos); + else + drawLine(">Alt: unknown", pos); + }; + + let drawMotion = function(pos){ + let health = Bangle.getHealthStatus('last'); + let steps_formated = ">Motion: " + parseInt(health.movement); + drawLine(steps_formated, pos); + }; + + /* ----------------------------------------------- + Services functions (HRM, pressure, etc...) + -------------------------------------------------- */ + + let turnOnServices = function(){ + if(clock.showHRM){ + Bangle.setHRMPower(true, "terminalclock"); + } + if(clock.showAltitude){ + Bangle.setBarometerPower(true, "terminalclock"); + } + if(clock.powerSave){ + if(clock.turnOffServiceTimeout) clearTimeout(clock.turnOffServiceTimeout); + clock.turnOffServiceTimeout = setTimeout(function () { + turnOffServices(); + }, 45000); + } + }; + + let turnOffServices = function(){ + if(clock.showHRM){ + Bangle.setHRMPower(false, "terminalclock"); + } + if(clock.showAltitude){ + Bangle.setBarometerPower(false, "terminalclock"); + } + }; + + // set the lock and unlock actions + clock.onLock = lock_event => { + if (lock_event) lock(); + else unlock(); + }; + Bangle.on("lock", clock.onLock); + + clock.onHRM = hrmInfo => { + if(hrmInfo.confidence >= clock.HRMinConfidence) + heartRate = hrmInfo.bpm; + }; + Bangle.on('HRM', clock.onHRM); + + const MEDIANLENGTH = 20; // technical + let avr = [], median; // technical + clock.onPressure = pressureInfo => { + while (avr.length>MEDIANLENGTH) avr.pop(); + avr.unshift(pressureInfo.altitude); + median = avr.slice().sort(); + if (median.length>10) { + let mid = median.length>>1; + altitude = E.sum(median.slice(mid-4,mid+5)) / 9; + } + else + altitude = pressureInfo.altitude; + }; + Bangle.on('pressure', clock.onPressure); + + + /* ------------------------------------------------- + Clock related functions but not in the ClockFace module + ---------------------------------------------------- */ + + let unlock = function(){ + if(clock.powerSave){ + turnOnServices(); + } + clock.precision = clock.unlock_precision; + clock.tick(); + }; + + let lock = function(){ + clock.precision = clock.lock_precision; + clock.tick(); + }; + + // starting the clock + clock.start(); +} \ No newline at end of file diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json index 8403a3b4d..12ca5ef03 100644 --- a/apps/terminalclock/metadata.json +++ b/apps/terminalclock/metadata.json @@ -3,12 +3,12 @@ "name": "Terminal Clock", "shortName":"Terminal Clock", "description": "A terminal cli like clock displaying multiple sensor data", - "version":"0.08", + "version":"0.10", "icon": "app.png", "type": "clock", "tags": "clock", "supports": ["BANGLEJS", "BANGLEJS2"], - "allow_emulator": true, + "allow_emulator": false, "readme": "README.md", "storage": [ {"name": "terminalclock.app.js","url": "app.js"}, diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog index 60e8bfab0..8c8a323de 100644 --- a/apps/waypointer/ChangeLog +++ b/apps/waypointer/ChangeLog @@ -6,3 +6,4 @@ 0.06: Added adjustment for Bangle.js magnetometer heading fix 0.07: Add settings file with the option to disable the slow direction updates 0.08: Use tilt compensation from new magnav library +0.09: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json index e21c5f10f..b8f28fbb9 100644 --- a/apps/waypointer/metadata.json +++ b/apps/waypointer/metadata.json @@ -1,7 +1,7 @@ { "id": "waypointer", "name": "Way Pointer", - "version": "0.08", + "version": "0.09", "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", "icon": "waypointer.png", "tags": "tool,outdoors,gps", diff --git a/apps/waypointer/settings.js b/apps/waypointer/settings.js index c8b06b9f9..cc521c637 100644 --- a/apps/waypointer/settings.js +++ b/apps/waypointer/settings.js @@ -15,7 +15,6 @@ "< Back" : () => back(), 'Smooth arrow rot': { value: !!settings.smoothDirection, - format: v => v?"Yes":"No", onchange: v => { settings.smoothDirection = v; writeSettings(); diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 0010a58fd..50c033600 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -21,3 +21,4 @@ 0.22: Automatic translation of strings, some left untranslated. 0.23: Update clock_info to avoid a redraw 0.24: Redraw clock_info on update and provide color field for condition +0.25: Added monochrome parameter to drawIcon in lib diff --git a/apps/weather/clkinfo.js b/apps/weather/clkinfo.js index ef3b7d139..4e526b977 100644 --- a/apps/weather/clkinfo.js +++ b/apps/weather/clkinfo.js @@ -22,7 +22,7 @@ function weatherIcon(code) { var ovr = Graphics.createArrayBuffer(24,24,1,{msb:true}); - weatherLib.drawIcon({code:code},12,12,12,ovr); + weatherLib.drawIcon({code:code},12,12,12,ovr,true); var img = ovr.asImage(); img.transparent = 0; return img; diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 14ca77ec6..af6aa4d7e 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -155,14 +155,11 @@ exports.getColor = function(code) { * @param y Top * @param r Icon Size * @param ovr Graphics instance (or undefined for g) + * @param monochrome If true, produce a monochromatic icon */ -exports.drawIcon = function(cond, x, y, r, ovr) { +exports.drawIcon = function(cond, x, y, r, ovr, monochrome) { var palette; - var monochrome=1; - if(!ovr) { - ovr = g; - monochrome=0; - } + if(!ovr) ovr = g; palette = getPalette(monochrome, ovr); diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json index bcb2fe109..55c2973b0 100644 --- a/apps/weather/metadata.json +++ b/apps/weather/metadata.json @@ -1,7 +1,7 @@ { "id": "weather", "name": "Weather", - "version": "0.24", + "version": "0.25", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/weatherClock/ChangeLog b/apps/weatherClock/ChangeLog index a6a12c297..f31e15729 100644 --- a/apps/weatherClock/ChangeLog +++ b/apps/weatherClock/ChangeLog @@ -1,5 +1,6 @@ 0.01: New App! 0.02: Minor layout format tweak so it uses less memory and draws ok on Bangle.js 1 (#1012) 0.03: Minor layout extra spaces. -0.04: Layout now compatible with Bangle.js 2 -0.05: Use weather condition code for icon selection +0.04: Layout now compatible with Bangle.js 2. +0.05: Use weather condition code for icon selection. +0.06: WeatherClock icons now reflect weather conditions better. Add settings menu to hide elements and to use weather icons of Weather app. Images placed into functions for performance. \ No newline at end of file diff --git a/apps/weatherClock/README.md b/apps/weatherClock/README.md index f1f146440..a7f44f7f7 100644 --- a/apps/weatherClock/README.md +++ b/apps/weatherClock/README.md @@ -1,12 +1,14 @@ # Weather Clock -A clock which displays the current weather conditions. Temperature, wind speed, and an icon indicating the weather conditions are displayed. +A clock which displays the current weather conditions. Time, day of week, date, temperature, wind speed, and an icon indicating the weather condition are displayed. + +As of Weather Clock v0.06 the date, day of week, temperature, weather icon and/or wind speed can be hidden in Settings. The icons can be changed to those of the Weather app. Standard widgets are displayed. ## Requirements -**This clock requires Gadgetbridge and the weather app in order to get weather data!** +**This clock requires Gadgetbridge and the Weather app in order to get weather data!** See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather. diff --git a/apps/weatherClock/app.js b/apps/weatherClock/app.js index 91d0ab36f..4896a9f49 100644 --- a/apps/weatherClock/app.js +++ b/apps/weatherClock/app.js @@ -1,22 +1,36 @@ const Layout = require("Layout"); -const storage = require('Storage'); +const storage = require("Storage"); const locale = require("locale"); +const SETTINGS_FILE = "weatherClock.json"; +let s; +const w = require("weather"); -// weather icons from https://icons8.com/icon/set/weather/color -var sunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQC6vd7ouVC4IwUCwIwUFwQwQCYgAHDZQXc9wACC6QWDDAgXN7wXF9oXPCwowDC5guGGAYXMCw4wCC5RGJJAZGTJBiNISIylQVJrLCC5owGF65fXR7AwBC5jvhC7JIILxapDFxAXOGAy9KC4owGBAQXODAgHDC54AHC8T0FAAQSOGg4qPGA4WUGAIuVC7AA/AH4AEA=")); - -var partSunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AY6AWVhvdC6vd7owUFwIABFiYAFGR4Xa93u9oXTCwIYDC6HeC4fuC56MBC4ySOIwpIQXYQXHmYABRpwXECwQYKF5HjC4kwL5gQCAYYwO7wqFAAowK7wWKJBgXLJBPd6YX/AAoVMAAM/Cw0DC5yRHCx5JGFyAwGCyIwFC/4XyR4inXa64wRFwowQCw4A/AH4AkA")); - -var cloudIcon = require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4AtgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAH4A/AH4A/ADg=")); - -var snowIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AhxGAC9YUBC4QZRhAVBAIWIC6QAEI6IYEI5cIBgwWOC64NCKohHPNox3RBgqnQEo7XPHpKONR5AXYAH4ASLa4XWXILiBC6r5LDBgWWDBRrKC5hsCEacIHawvMCIwvQC5QvQFAROEfZ5ADLJ4YGCywvVI7CPGC9IA/AH4AF")); - -var rainIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AFgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAGeIBJEIwAVJhGIC5AJBC5QMJEJQMEC44JBC6QSCC54FHLxgNBBgYSEDgKpPMhQXneSwuUAH4A/AA4=")); - -var stormIcon = require("heatshrink").decompress(atob("mEwwhC/AFEzmcwCyoYUgYXDmYuVGAY0OFwocHC6pNLCxYXYJBQXuCxhhJRpgYKCyBKFFyIXFCyJIFC/4XaO66nU3eza6k7C4IWFGBwXBCwwwO3ewC5AZMC6RaCIxZiI3e7AYYwRCQIIBC4QwPIQIpDC5owDhYREIxgAEFIouNC4orDFyBGBGAcLC6BaFhYWRLSRIFISQXcCyqhRAH4Az")); - +// Weather icons from https://icons8.com/icon/set/weather/color +function getSun() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQC6vd7ouVC4IwUCwIwUFwQwQCYgAHDZQXc9wACC6QWDDAgXN7wXF9oXPCwowDC5guGGAYXMCw4wCC5RGJJAZGTJBiNISIylQVJrLCC5owGF65fXR7AwBC5jvhC7JIILxapDFxAXOGAy9KC4owGBAQXODAgHDC54AHC8T0FAAQSOGg4qPGA4WUGAIuVC7AA/AH4AEA=")); +} +function getPartSun() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AY6AWVhvdC6vd7owUFwIABFiYAFGR4Xa93u9oXTCwIYDC6HeC4fuC56MBC4ySOIwpIQXYQXHmYABRpwXECwQYKF5HjC4kwL5gQCAYYwO7wqFAAowK7wWKJBgXLJBPd6YX/AAoVMAAM/Cw0DC5yRHCx5JGFyAwGCyIwFC/4XyR4inXa64wRFwowQCw4A/AH4AkA")); +} +function getCloud() { + return require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4AtgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAH4A/AH4A/ADg=")); +} +function getSnow() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AhxGAC9YUBC4QZRhAVBAIWIC6QAEI6IYEI5cIBgwWOC64NCKohHPNox3RBgqnQEo7XPHpKONR5AXYAH4ASLa4XWXILiBC6r5LDBgWWDBRrKC5hsCEacIHawvMCIwvQC5QvQFAROEfZ5ADLJ4YGCywvVI7CPGC9IA/AH4AF")); +} +function getRain() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AFgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAGeIBJEIwAVJhGIC5AJBC5QMJEJQMEC44JBC6QSCC54FHLxgNBBgYSEDgKpPMhQXneSwuUAH4A/AA4=")); +} +function getStorm() { + return require("heatshrink").decompress(atob("mEwwhC/AFEzmcwCyoYUgYXDmYuVGAY0OFwocHC6pNLCxYXYJBQXuCxhhJRpgYKCyBKFFyIXFCyJIFC/4XaO66nU3eza6k7C4IWFGBwXBCwwwO3ewC5AZMC6RaCIxZiI3e7AYYwRCQIIBC4QwPIQIpDC5owDhYREIxgAEFIouNC4orDFyBGBGAcLC6BaFhYWRLSRIFISQXcCyqhRAH4Az")); +} // err icon - https://icons8.com/icons/set/error -var errIcon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4AZUAIWUiAXBWqgXXdIYuVGCgXBgICCIyYXCJCQTDC6QrEMCQSEJCQRFC6ApGJCCiDDQSpQFAYXEJBqNGJCA/EC4ZIOEwgXFJBgNEAhKlNAgxIKBgoXEJBjsLC5TsIeRycMBhRrMMBKzQEozjOBxAgHGww+IA6wfSH4hnIC47OMSJqlRIJAXCACIXaGoQARPwwuTAH4A/ABw")); +function getErr() { + return require("heatshrink").decompress(atob("mEwwkBiIA/AH4AZUAIWUiAXBWqgXXdIYuVGCgXBgICCIyYXCJCQTDC6QrEMCQSEJCQRFC6ApGJCCiDDQSpQFAYXEJBqNGJCA/EC4ZIOEwgXFJBgNEAhKlNAgxIKBgoXEJBjsLC5TsIeRycMBhRrMMBKzQEozjOBxAgHGww+IA6wfSH4hnIC47OMSJqlRIJAXCACIXaGoQARPwwuTAH4A/ABw")); +} +function getDummy() { + return require("heatshrink").decompress(atob("gMBwMAwA")); +} /** Choose weather icon to display based on condition. @@ -25,32 +39,30 @@ sent from gadget bridge. */ function chooseIcon(condition) { condition = condition.toLowerCase(); - if (condition.includes("thunderstorm")) return stormIcon; + if (condition.includes("thunderstorm")|| + condition.includes("squalls")|| + condition.includes("tornado")) return getStorm; if (condition.includes("freezing")||condition.includes("snow")|| condition.includes("sleet")) { - return snowIcon; + return getSnow; } if (condition.includes("drizzle")|| - condition.includes("shower")) { - return rainIcon; + condition.includes("shower")|| + condition.includes("rain")) return getRain; + if (condition.includes("clear")) return getSun; + if (condition.includes("clouds")) return getCloud; + if (condition.includes("few clouds")|| + condition.includes("scattered clouds")|| + condition.includes("mist")|| + condition.includes("smoke")|| + condition.includes("haze")|| + condition.includes("sand")|| + condition.includes("dust")|| + condition.includes("fog")|| + condition.includes("ash")) { + return getPartSun; } - if (condition.includes("rain")) return rainIcon; - if (condition.includes("clear")) return sunIcon; - if (condition.includes("few clouds")) return partSunIcon; - if (condition.includes("scattered clouds")) return cloudIcon; - if (condition.includes("clouds")) return cloudIcon; - if (condition.includes("mist") || - condition.includes("smoke") || - condition.includes("haze") || - condition.includes("sand") || - condition.includes("dust") || - condition.includes("fog") || - condition.includes("ash") || - condition.includes("squalls") || - condition.includes("tornado")) { - return cloudIcon; - } - return cloudIcon; + return getCloud; } /* @@ -60,55 +72,29 @@ function chooseIcon(condition) { function chooseIconByCode(code) { const codeGroup = Math.round(code / 100); switch (codeGroup) { - case 2: return stormIcon; - case 3: return rainIcon; - case 5: return rainIcon; - case 6: return snowIcon; - case 7: return cloudIcon; + case 2: return getStorm; + case 3: return getRain; + case 5: + switch (code) { + case 511: return getSnow; + default: return getRain; + } + case 6: return getSnow; + case 7: return getPartSun; case 8: switch (code) { - case 800: return sunIcon; - case 801: return partSunIcon; - default: return cloudIcon; + case 800: return getSun; + case 804: return getCloud; + default: return getPartSun; } - default: return cloudIcon; + default: return getCloud; } } -/** -Get weather stored in json file by weather app. -*/ -function getWeather() { - let jsonWeather = storage.readJSON('weather.json'); - return jsonWeather; -} - -var clockLayout = new Layout( { - type:"v", c: [ - {type:"txt", font:"35%", halign: 0, fillx:1, pad: 8, label:"00:00", id:"time" }, - {type: "h", fillx: 1, c: [ - {type:"txt", font:"10%", label:"THU", id:"dow" }, - {type:"txt", font:"10%", label:"01/01/1970", id:"date" } - ] - }, - {type: "h", valign : 1, fillx:1, c: [ - {type: "img", filly: 1, id: "weatherIcon", src: sunIcon}, - {type: "v", fillx:1, c: [ - {type: "h", c: [ - {type: "txt", font: "10%", id: "temp", label: "000 °C"}, - ]}, - {type: "h", c: [ - {type: "txt", font: "10%", id: "wind", label: "00 km/h"}, - ]} - ] - }, - ]}] -}); - -// timeout used to update every minute +// Timeout used to update every minute var drawTimeout; -// schedule a draw for the next minute +// Schedule a draw for the next minute function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { @@ -119,37 +105,85 @@ function queueDraw() { function draw() { var date = new Date(); - clockLayout.time.label = locale.time(date, 1); - clockLayout.date.label = locale.date(date, 1).toUpperCase(); - clockLayout.dow.label = locale.dow(date, 1).toUpperCase() + " "; - var weatherJson = getWeather(); - if(weatherJson && weatherJson.weather){ - var currentWeather = weatherJson.weather; - const temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/); - clockLayout.temp.label = temp[1] + " " + temp[2]; - const code = currentWeather.code || -1; + cLayout.time.label = locale.time(date, 1); + cLayout.dow.label = s.day ? locale.dow(date, 1).toUpperCase() + " " : ""; + cLayout.date.label = s.date ? locale.date(date, 1).toUpperCase() : ""; + let curr = w.get(); // Get weather from weather app. + if(curr){ + const temp = locale.temp(curr.temp-273.15).match(/^(\D*\d*)(.*)$/); + cLayout.temp.label = temp[1] + " " + temp[2]; + const code = curr.code || -1; if (code > 0) { - clockLayout.weatherIcon.src = chooseIconByCode(code); + let showIconC = s.src ? wDrawIcon(curr.code) : chooseIconByCode(curr.code); + cLayout.wIcon.src = s.icon ? showIconC : getDummy; } else { - clockLayout.weatherIcon.src = chooseIcon(currentWeather.txt); + let showIconT = s.src ? wDrawIcon(curr.txt) : chooseIcon(curr.txt); + cLayout.wIcon.src = s.icon ? showIconT : getDummy; } - const wind = locale.speed(currentWeather.wind).match(/^(\D*\d*)(.*)$/); - clockLayout.wind.label = wind[1] + " " + wind[2] + " " + (currentWeather.wrose||'').toUpperCase(); + const wind = locale.speed(curr.wind).match(/^(\D*\d*)(.*)$/); + cLayout.wind.label = wind[1] + " " + wind[2] + " " + (curr.wrose||"").toUpperCase(); } else{ - clockLayout.temp.label = "Err"; - clockLayout.wind.label = "No Data"; - clockLayout.weatherIcon.src = errIcon; + cLayout.temp.label = "Err"; + cLayout.wind.label = "No Data"; + cLayout.wIcon.src = s.icon ? getErr : getDummy; } - clockLayout.clear(); - clockLayout.render(); - // queue draw in one minute + cLayout.clear(); + cLayout.render(); + // Queue draw in one minute queueDraw(); } +// Load settings from file +s = storage.readJSON(SETTINGS_FILE,1)||{}; +s.src = s.src === undefined ? false : s.src; +s.icon = s.icon === undefined ? true : s.icon; +s.day = s.day === undefined ? true : s.day; +s.date = s.date === undefined ? true : s.date; +s.wind = s.wind === undefined ? true : s.wind; + +function wDrawIcon(code) { + var ovr = Graphics.createArrayBuffer(50,50,8,{msb:true}); + if (typeof code == "number") w.drawIcon({code:code},24,24,24,ovr); + if (typeof code == "string") w.drawIcon({txt:code},24,24,24,ovr); + var img = ovr.asImage(); + img.transparent = 0; + return img; +} + +let srcIcons = s.src ? wDrawIcon(800) : getSun; +let srcWeather = s.icon ? srcIcons : getDummy; +let fontTemp = s.wind ? "10%" : "20%"; +let fontWind = s.wind ? "10%" : "0%"; +let labelDay = s.day ? "THU" : ""; +let labelDate = s.date ? "01/01/1970" : ""; +var cLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"35%", halign: 0, fillx:1, pad: 8, label:"00:00", id:"time" }, + {type: "h", fillx: 1, c: [ + {type: "h", c: [ + {type:"txt", font:"10%", label:labelDay, id:"dow" }, + {type:"txt", font:"10%", label:labelDate, id:"date" } + ]}, + ] + }, + {type: "h", valign : 1, fillx:1, c: [ + {type: "img", filly: 1, pad: 8, id: "wIcon", src: srcWeather}, + {type: "v", fillx:1, c: [ + {type: "h", c: [ + {type: "txt", font: fontTemp, id: "temp", label: "000 °C"}, + ]}, + {type: "h", c: [ + {type: "txt", font: fontWind, id: "wind", label: "00 km/h"}, + ]} + ] + }, + ]}] +}); + g.clear(); Bangle.setUI("clock"); // Show launcher when middle button pressed Bangle.loadWidgets(); Bangle.drawWidgets(); -clockLayout.render(); +cLayout.render(); draw(); diff --git a/apps/weatherClock/metadata.json b/apps/weatherClock/metadata.json index cf8bd899e..270591c74 100644 --- a/apps/weatherClock/metadata.json +++ b/apps/weatherClock/metadata.json @@ -1,9 +1,11 @@ { "id": "weatherClock", "name": "Weather Clock", - "version": "0.05", + "shortName": "Weather Clock", + "version": "0.06", "description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).", "icon": "app.png", + "dependencies": {"weather":"app"}, "screenshots": [{"url":"screens/screen1.png"}], "type": "clock", "tags": "clock, weather", @@ -12,6 +14,8 @@ "readme": "README.md", "storage": [ {"name":"weatherClock.app.js","url":"app.js"}, - {"name":"weatherClock.img","url":"app-icon.js","evaluate":true} - ] + {"name":"weatherClock.img","url":"app-icon.js","evaluate":true}, + {"name":"weatherClock.settings.js","url":"settings.js"} + ], + "data": [{"name":"weatherClock.json"}] } diff --git a/apps/weatherClock/settings.js b/apps/weatherClock/settings.js new file mode 100644 index 000000000..0aa7330c1 --- /dev/null +++ b/apps/weatherClock/settings.js @@ -0,0 +1,58 @@ +(function(back) { + const SETTINGS_FILE = "weatherClock.json"; + + // Load settings file + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + let s = {}; + s.date = (settings.date === undefined ? true : settings.date); + s.day = (settings.day === undefined ? true : settings.day); + s.icon = (settings.icon === undefined ? true : settings.icon); + s.wind = (settings.wind === undefined ? true : settings.wind); + s.src = (settings.src === undefined ? false : settings.src); + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + E.showMenu({ + '': { 'title': 'Weather Clock' }, + '< Back': back, + 'Show date': { + value: !!s.date, + onchange: v => { + s.date = v; + save(); + }, + }, + 'Show day Of Week': { + value: !!s.day, + onchange: v => { + s.day = v; + save(); + }, + }, + 'Show weather Icon': { + value: !!s.icon, + onchange: v => { + s.icon = v; + save(); + }, + }, + 'Show wind Speed': { + value: !!s.wind, + onchange: v => { + s.wind = v; + save(); + }, + }, + 'Use weather app icons': { + value: !!s.src, + onchange: v => { + s.src = v; + save(); + }, + } + }); +}); diff --git a/apps/widChargingStatus/README.md b/apps/widChargingStatus/README.md new file mode 100644 index 000000000..a5ac62811 --- /dev/null +++ b/apps/widChargingStatus/README.md @@ -0,0 +1,8 @@ +# Charging Status + +This widget shows a yellow lightning icon to indicate that the watch is charging. A short buzz is also emitted on initial charging connection. +Nothing is shown when not charging. + +# TypeScript + +This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info diff --git a/apps/widChargingStatus/widget.js b/apps/widChargingStatus/widget.js index 5d9ea3837..46119cc5c 100644 --- a/apps/widChargingStatus/widget.js +++ b/apps/widChargingStatus/widget.js @@ -1,33 +1,33 @@ -"use strict"; -(() => { - const icon = require('heatshrink').decompress(atob('ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA')); - const iconWidth = 18; - function draw() { - g.reset(); - if (Bangle.isCharging()) { - g.setColor('#FD0'); - g.drawImage(icon, this.x + 1, this.y + 1, { - scale: 0.6875, - }); - } - } - WIDGETS.chargingStatus = { - area: 'tr', - width: Bangle.isCharging() ? iconWidth : 0, - draw: draw, - }; - Bangle.on('charging', (charging) => { - const widget = WIDGETS.chargingStatus; - if (widget) { - if (charging) { - Bangle.buzz(); - widget.width = iconWidth; - } - else { - widget.width = 0; - } - Bangle.drawWidgets(); // re-layout widgets - g.flip(); - } - }); -})(); +"use strict"; +(function () { + var icon = require('heatshrink').decompress(atob('ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA')); + var iconWidth = 18; + function draw() { + g.reset(); + if (Bangle.isCharging()) { + g.setColor('#FD0'); + g.drawImage(icon, this.x + 1, this.y + 1, { + scale: 0.6875, + }); + } + } + WIDGETS.chargingStatus = { + area: 'tr', + width: Bangle.isCharging() ? iconWidth : 0, + draw: draw, + }; + Bangle.on('charging', function (charging) { + var widget = WIDGETS.chargingStatus; + if (widget) { + if (charging) { + Bangle.buzz(); + widget.width = iconWidth; + } + else { + widget.width = 0; + } + Bangle.drawWidgets(); + g.flip(); + } + }); +})(); diff --git a/apps/widChargingStatus/widget.ts b/apps/widChargingStatus/widget.ts index 14b4df4a4..a161d5408 100644 --- a/apps/widChargingStatus/widget.ts +++ b/apps/widChargingStatus/widget.ts @@ -6,16 +6,17 @@ ); const iconWidth = 18; - function draw(this: { x: number; y: number }) { + function draw(this: { x?: number; y?: number }) { g.reset(); if (Bangle.isCharging()) { g.setColor('#FD0'); - g.drawImage(icon, this.x + 1, this.y + 1, { + g.drawImage(icon, this.x! + 1, this.y! + 1, { scale: 0.6875, }); } } + // @ts-ignore WIDGETS.chargingStatus = { area: 'tr', width: Bangle.isCharging() ? iconWidth : 0, @@ -23,6 +24,7 @@ }; Bangle.on('charging', (charging) => { + // @ts-ignore const widget = WIDGETS.chargingStatus; if (widget) { if (charging) { diff --git a/apps/wid_a_battery_widget/metadata.json b/apps/wid_a_battery_widget/metadata.json index 6c507b7b3..017550b1e 100644 --- a/apps/wid_a_battery_widget/metadata.json +++ b/apps/wid_a_battery_widget/metadata.json @@ -9,6 +9,7 @@ "readme": "README.md", "description": "Simple and slim battery widget with charge status and percentage", "tags": "widget,battery", + "provides_widgets" : ["battery"], "storage": [ {"name":"wid_a_battery_widget.wid.js","url":"widget.js"} ] diff --git a/apps/widalarmeta/ChangeLog b/apps/widalarmeta/ChangeLog new file mode 100644 index 000000000..2b74766c8 --- /dev/null +++ b/apps/widalarmeta/ChangeLog @@ -0,0 +1,11 @@ +0.01: New App! +0.02: Change font to 5x9 7 segment-style + Add settings page + Add option to show seconds +0.03: Fix Bell not appearing on alarms > 24h and redrawing interval + Update to match the default alarm widget, and not show itself when an alarm is hidden. +0.04: Fix check for active alarm +0.05: Convert Yes/No On/Off in settings to checkboxes +0.06: Remember next alarm to reduce calculation amount + Redraw only every hour when no alarm in next 24h +0.07: Fix when no alarms are present diff --git a/apps/widalarmeta/metadata.json b/apps/widalarmeta/metadata.json index ef9f55ba8..6b3d8978b 100644 --- a/apps/widalarmeta/metadata.json +++ b/apps/widalarmeta/metadata.json @@ -2,8 +2,8 @@ "id": "widalarmeta", "name": "Alarm & Timer ETA", "shortName": "Alarm ETA", - "version": "0.01", - "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h", + "version": "0.07", + "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).", "icon": "widget.png", "type": "widget", "tags": "widget", @@ -11,6 +11,8 @@ "provides_widgets" : ["alarm"], "screenshots" : [ { "url":"screenshot.png" } ], "storage": [ - {"name":"widalarmeta.wid.js","url":"widget.js"} - ] + {"name":"widalarmeta.wid.js","url":"widget.js"}, + {"name":"widalarmeta.settings.js","url":"settings.js"} + ], + "data": [{"name":"widalarmeta.json"}] } diff --git a/apps/widalarmeta/screenshot.png b/apps/widalarmeta/screenshot.png index 41a109557..3a23b757a 100644 Binary files a/apps/widalarmeta/screenshot.png and b/apps/widalarmeta/screenshot.png differ diff --git a/apps/widalarmeta/settings.js b/apps/widalarmeta/settings.js new file mode 100644 index 000000000..db9243ae0 --- /dev/null +++ b/apps/widalarmeta/settings.js @@ -0,0 +1,44 @@ +(function(back) { + const CONFIGFILE = "widalarmeta.json"; + // Load settings + const settings = Object.assign({ + maxhours: 24, + drawBell: false, + showSeconds: 0, // 0=never, 1=only when display is unlocked, 2=for less than a minute + }, require("Storage").readJSON(CONFIGFILE,1) || {}); + + function writeSettings() { + require('Storage').writeJSON(CONFIGFILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Alarm & Timer ETA" }, + "< Back" : () => back(), + /*LANG*/'Maximum hours': { + format: v => v === 0 ? /*LANG*/'disabled' : v, + value: settings.maxhours, + min: 0, max: 24, + onchange: v => { + settings.maxhours = v; + writeSettings(); + } + }, + /*LANG*/'Draw bell': { + value: !!settings.drawBell, + onchange: v => { + settings.drawBell = v; + writeSettings(); + } + }, + /*LANG*/'Show seconds': { + value: settings.showSeconds, + min: 0, max: 2, + format: v => [/*LANG*/"Never", /*LANG*/"Unlocked", /*LANG*/"Last minute"][v || 0], + onchange: v => { + settings.showSeconds = v; + writeSettings(); + } + }, + }); +}); diff --git a/apps/widalarmeta/widget.js b/apps/widalarmeta/widget.js index 0cddf953a..0104eb3b1 100644 --- a/apps/widalarmeta/widget.js +++ b/apps/widalarmeta/widget.js @@ -1,35 +1,103 @@ (() => { - const alarms = require("Storage").readJSON("sched.json",1) || []; + require("Font5x9Numeric7Seg").add(Graphics); + const config = Object.assign({ + maxhours: 24, + drawBell: false, + showSeconds: 0, // 0=never, 1=only when display is unlocked, 2=for less than a minute + }, require("Storage").readJSON("widalarmeta.json",1) || {}); - function draw() { - const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm)).filter(a => a !== undefined); - const next = Math.min.apply(null, times); - if (next > 0 && next < 86400000) { - const hours = Math.floor((next % 86400000) / 3600000).toString(); - const minutes = Math.floor(((next % 86400000) % 3600000) / 60000).toString(); + function getNextAlarm(date) { + const alarms = (require("Storage").readJSON("sched.json",1) || []).filter(alarm => alarm.on && alarm.hidden !== true); + WIDGETS["widalarmeta"].numActiveAlarms = alarms.length; + if (alarms.length > 0) { + const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm, date) || Number.POSITIVE_INFINITY); + const eta = Math.min.apply(null, times); + if (eta !== Number.POSITIVE_INFINITY) { + const idx = times.indexOf(eta); + const alarm = alarms[idx]; + delete alarm.msg; delete alarm.id; delete alarm.data; // free some memory + return alarm; + } + } + } // getNextAlarm + + function draw(fromInterval) { + if (this.nextAlarm === undefined) { + let alarm = getNextAlarm(); + if (alarm === undefined) { + // try again with next hour + const nextHour = new Date(); + nextHour.setHours(nextHour.getHours()+1); + alarm = getNextAlarm(nextHour); + } + if (alarm !== undefined) { + this.nextAlarm = alarm; + } + } + const next = this.nextAlarm !== undefined ? require("sched").getTimeToAlarm(this.nextAlarm) : 0; + + let calcWidth = 0; + let drawSeconds = false; + + if (next > 0 && next <= config.maxhours*60*60*1000) { + const hours = Math.floor((next-1) / 3600000).toString(); + const minutes = Math.floor(((next-1) % 3600000) / 60000).toString(); + const seconds = Math.floor(((next-1) % 60000) / 1000).toString(); + drawSeconds = (config.showSeconds & 0b01 && !Bangle.isLocked()) || (config.showSeconds & 0b10 && next <= 1000*60); g.reset(); // reset the graphics context to defaults (color/font/etc) g.setFontAlign(0,0); // center fonts g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); var text = hours.padStart(2, '0') + ":" + minutes.padStart(2, '0'); - g.setFont("6x8:1x2"); + if (drawSeconds) { + text += ":" + seconds.padStart(2, '0'); + } + g.setFont("5x9Numeric7Seg:1x2"); g.drawString(text, this.x+this.width/2, this.y+12); - if (this.width === 0) { - this.width = 6*5+2; - Bangle.drawWidgets(); // width changed, re-layout + + calcWidth = 5*5+2; + if (drawSeconds) { + calcWidth += 3*5; + } + this.bellVisible = false; + } else if (config.drawBell && this.numActiveAlarms > 0) { + calcWidth = 24; + // next alarm too far in future, draw only widalarm bell + if (this.bellVisible !== true || fromInterval !== true) { + g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); + this.bellVisible = true; } } + + if (this.width !== calcWidth) { + // width changed, re-layout + this.width = calcWidth; + Bangle.drawWidgets(); + } + + // redraw next hour when no alarm else full minute or second + const period = next === 0 ? 3600000 : (drawSeconds ? 1000 : 60000); + let timeout = next > 0 ? next % period : period - (Date.now() % period); + if (timeout === 0) { + timeout += period; + } + + if (this.timeoutId !== undefined) { + clearTimeout(this.timeoutId); + } + this.timeoutId = setTimeout(()=>{ + WIDGETS["widalarmeta"].timeoutId = undefined; + WIDGETS["widalarmeta"].draw(true); + }, timeout); + } /* draw */ + + if (config.maxhours > 0) { + // add your widget + WIDGETS["widalarmeta"]={ + area:"tl", + width: 0, // hide by default = assume no timer + draw:draw + }; } - - setInterval(function() { - WIDGETS["widalarmeta"].draw(WIDGETS["widalarmeta"]); - }, 30000); // update every half minute - - // add your widget - WIDGETS["widalarmeta"]={ - area:"tl", - width: 0, // hide by default = assume no timer - draw:draw - }; })(); diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog index 2dfe8336d..3b2ae75c4 100644 --- a/apps/widbaroalarm/ChangeLog +++ b/apps/widbaroalarm/ChangeLog @@ -7,3 +7,4 @@ Show difference of last measurement to pressure average of the the last three hours in the widget Only use valid pressure values 0.06: Fix exception +0.07: Ensure barometer gets turned off after a few readings (isBarometerOn broken in 2v16) diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json index ba6c47b37..0a6ddd71e 100644 --- a/apps/widbaroalarm/metadata.json +++ b/apps/widbaroalarm/metadata.json @@ -2,7 +2,7 @@ "id": "widbaroalarm", "name": "Barometer Alarm Widget", "shortName": "Barometer Alarm", - "version": "0.06", + "version": "0.07", "description": "A widget that can alarm on when the pressure reaches defined thresholds.", "icon": "widget.png", "type": "widget", diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js index d877c4384..d65a1c09c 100644 --- a/apps/widbaroalarm/widget.js +++ b/apps/widbaroalarm/widget.js @@ -211,6 +211,28 @@ function calculcate3hAveragePressure() { } } +function barometerPressureHandler(e) { + const MEDIANLENGTH = 20; + while (currentPressures.length > MEDIANLENGTH) + currentPressures.pop(); + + const pressure = e.pressure; + if (isValidPressureValue(pressure)) { + currentPressures.unshift(pressure); + median = currentPressures.slice().sort(); + + if (median.length > 10) { + var mid = median.length >> 1; + medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); + if (medianPressure > 0) { + turnOff(); + draw(); + handlePressureValue(medianPressure); + } + } + } +} + /* turn on barometer power take multiple measurements @@ -219,37 +241,15 @@ function calculcate3hAveragePressure() { turn off barometer power */ function getPressureValue() { - if (stop) - return; - const MEDIANLENGTH = 20; + if (stop) return; Bangle.setBarometerPower(true, "widbaroalarm"); - Bangle.on('pressure', function(e) { - while (currentPressures.length > MEDIANLENGTH) - currentPressures.pop(); - - const pressure = e.pressure; - if (isValidPressureValue(pressure)) { - currentPressures.unshift(pressure); - median = currentPressures.slice().sort(); - - if (median.length > 10) { - var mid = median.length >> 1; - medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); - if (medianPressure > 0) { - turnOff(); - draw(); - handlePressureValue(medianPressure); - } - } - } - }); - - setTimeout(function() { turnOff(); }, 30000); + Bangle.on('pressure', barometerPressureHandler); + setTimeout(turnOff, 30000); } function turnOff() { - if (Bangle.isBarometerOn()) - Bangle.setBarometerPower(false, "widbaroalarm"); + Bangle.removeListener('pressure', barometerPressureHandler); + Bangle.setBarometerPower(false, "widbaroalarm"); } function draw() { diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index 5986ecf3f..cb11b1be9 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -6,3 +6,5 @@ 0.07: Move CHARGING variable to more readable string 0.08: Ensure battery updates every 60s even if LCD was on at boot and stays on 0.09: Misc speed/memory tweaks +0.10: Color changes due to the battery level +0.11: Change level for medium charge (50% -> 40%), and darken color on light themes as yellow was almost invisible diff --git a/apps/widbat/metadata.json b/apps/widbat/metadata.json index 993310eb2..0151fcbd7 100644 --- a/apps/widbat/metadata.json +++ b/apps/widbat/metadata.json @@ -1,7 +1,7 @@ { "id": "widbat", "name": "Battery Level Widget", - "version": "0.09", + "version": "0.11", "description": "Show the current battery level and charging status in the top right of the clock", "icon": "widget.png", "type": "widget", diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index a8a0c5382..98eb09227 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -31,7 +31,11 @@ x+=16; } g.setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14); - g.setColor("#0f0").fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17); + var battery = E.getBattery(); + if(battery < 20) {g.setColor("#f00");} + else if (battery < 40) {g.setColor(g.theme.dark ? "#ff0" : "#f80");} + else {g.setColor("#0f0");} + g.fillRect(x+4,y+6,x+4+battery*(s-12)/100,y+17); }}; setWidth(); })() diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index e70093659..3592656a9 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -13,3 +13,6 @@ 0.14: Fix drawing the bar when charging 0.15: Added option to always display the icon when charging (useful if 'hide if charge greater than' is enabled) 0.16: Increase screen update rate when charging +0.17: Add option 'Remove Jitter'='Drop only' to prevent percentage from getting up again when not charging + Add option to disable vibration when charger connects +0.18: Only redraw when values change diff --git a/apps/widbatpc/metadata.json b/apps/widbatpc/metadata.json index 953f8d345..d361da442 100644 --- a/apps/widbatpc/metadata.json +++ b/apps/widbatpc/metadata.json @@ -2,7 +2,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", - "version": "0.16", + "version": "0.18", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "icon": "widget.png", "type": "widget", diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index b45fc6749..c988d23bf 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -5,6 +5,7 @@ (function(back) { const SETTINGS_FILE = 'widbatpc.json' const COLORS = ['By Level', 'Green', 'Monochrome'] + const RM_JITTER_OPTIONS = [/*LANG*/'Off', /*LANG*/'Drop only']; // initialize with default settings... let s = { @@ -14,6 +15,8 @@ 'charger': true, 'hideifmorethan': 100, 'alwaysoncharge': false, + 'removejitter': 0, + 'buzzoncharge': true, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -28,7 +31,9 @@ return function (value) { s[key] = value; storage.write(SETTINGS_FILE, s); - WIDGETS["batpc"].reload(); + if ("WIDGETS" in global && WIDGETS["batpc"] !== undefined) { + WIDGETS["batpc"].reload(); + } } } @@ -36,17 +41,17 @@ const menu = { '': { 'title': 'Battery Widget' }, '< Back': back, - 'Percentage': { + /*LANG*/'Percentage': { value: s.percentage, format: onOffFormat, onchange: save('percentage'), }, - 'Charging Icon': { + /*LANG*/'Charging Icon': { value: s.charger, format: onOffFormat, onchange: save('charger'), }, - 'Color': { + /*LANG*/'Color': { format: () => s.color, onchange: function () { // cycles through options @@ -56,12 +61,12 @@ save('color')(s.color) } }, - 'Fill Bar': { + /*LANG*/'Fill Bar': { value: s.fillbar, format: onOffFormat, onchange: save('fillbar'), }, - 'Hide if >': { + /*LANG*/'Hide if >': { value: s.hideifmorethan||100, min: 10, max : 100, @@ -69,11 +74,22 @@ format: x => x+"%", onchange: save('hideifmorethan'), }, - 'Show on charge': { // Not sure if this is readable enough in the 'big' menu + /*LANG*/'Show on charge': { // Not sure if this is readable enough in the 'big' menu value: s.alwaysoncharge, format: onOffFormat, onchange: save('alwaysoncharge'), }, + /*LANG*/'Buzz on charge': { + value: s.buzzoncharge, + format: onOffFormat, + onchange: save('buzzoncharge'), + }, + /*LANG*/'Remove Jitter': { + value: s.removejitter, + min: 0, max: 1, + format: v => RM_JITTER_OPTIONS[v], + onchange: save('removejitter'), + }, } E.showMenu(menu) }) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 529923386..b508cce8b 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -2,6 +2,8 @@ const intervalLow = 60000; // update time when not charging const intervalHigh = 2000; // update time when charging + let prevMin = 100; + let COLORS = {}; if (process.env.HWVERSION == 1) { @@ -33,6 +35,8 @@ 'charger': true, 'hideifmorethan': 100, 'alwaysoncharge': false, + 'removejitter': 0, // 0 == off, 1 == downwards only + 'buzzoncharge': true, }; Object.keys(DEFAULTS).forEach(k=>{ if (settings[k]===undefined) settings[k]=DEFAULTS[k]; @@ -82,14 +86,32 @@ return changed; } - function draw() { + function draw(fromInterval) { // if hidden, don't draw if (!WIDGETS["batpc"].width) return; // else... var s = 39; var x = this.x, y = this.y; - const l = E.getBattery(), - c = levelColor(l); + let l = E.getBattery(); + if (setting('removejitter') === 1) { + // if we have seen a battery percentage that was lower than current, use lower + if (Bangle.isCharging()) { + prevMin = l; // charging is the only way to increase percentage + } else if (prevMin >= l) { + prevMin = l; + } else { + l = prevMin; + } + } + + if (fromInterval === true && this.prevLevel === l && this.prevCharging === Bangle.isCharging()) { + return; // unchanged, do nothing + } + + this.prevLevel = l; + this.prevCharging = Bangle.isCharging(); + + const c = levelColor(l); if (Bangle.isCharging() && setting('charger')) { g.setColor(chargerColor()).drawImage(atob( @@ -148,7 +170,9 @@ } Bangle.on('charging',function(charging) { - if(charging) Bangle.buzz(); + if (setting('buzzoncharge')) { + if(charging) Bangle.buzz(); + } update(); g.flip(); }); @@ -157,7 +181,7 @@ if (on) update(); }); - var id = setInterval(()=>WIDGETS["batpc"].draw(), intervalLow); + var id = setInterval(()=>WIDGETS["batpc"].draw(true), intervalLow); WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; setWidth(); diff --git a/apps/widbgjs/ChangeLog b/apps/widbgjs/ChangeLog index 7b83706bf..a88c5f063 100644 --- a/apps/widbgjs/ChangeLog +++ b/apps/widbgjs/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Fixed settings changes are actually reflected now and old values are strikethrough diff --git a/apps/widbgjs/README.md b/apps/widbgjs/README.md index dbb8cd9e4..23a37fa33 100644 --- a/apps/widbgjs/README.md +++ b/apps/widbgjs/README.md @@ -11,7 +11,7 @@ For this widget to work and to get data from the phone, you need: ## How to use it Make sure you have all the prerequisites from above. -The watch should automatically start displaying values, if there is an arrow visible behind the value, the value is within the not-expired-yet time range changeable in the settings standard is 15 minutes. (I will probably change this in the future, to strike through the text to make expired values clearer). +The watch should automatically start displaying values. Old values are now written with strikethrough. ## Settings In the settings, you can: @@ -26,4 +26,3 @@ Developed by Phil Roggenbuck ( 0 ? 15 : 0; }; + var update = function (newState) { + state = newState; + WIDGETS["bluetooth"].width = width(); + setTimeout(Bangle.drawWidgets, 50); + }; + var colours = (_a = {}, + _a[1] = { + false: "#fff", + true: "#fff", + }, + _a[2] = { + false: "#0ff", + true: "#00f", + }, + _a); + WIDGETS["bluetooth"] = { + area: "tl", + sortorder: -1, + draw: function () { + if (state == 0) + return; + g.reset(); + g.setColor(colours[state]["".concat(g.theme.dark)]); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), this.x + 2, this.y + 2); + }, + width: width(), + }; + NRF.on("connect", update.bind(null, 2)); + NRF.on("disconnect", update.bind(null, 1)); + var origWake = NRF.wake; + var origSleep = NRF.sleep; + NRF.wake = function () { + update(1); + return origWake.apply(this, arguments); + }; + NRF.sleep = function () { + update(0); + return origSleep.apply(this, arguments); + }; +})(); diff --git a/apps/widbtstates/widget.png b/apps/widbtstates/widget.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/widbtstates/widget.png differ diff --git a/apps/widbtstates/widget.ts b/apps/widbtstates/widget.ts new file mode 100644 index 000000000..8f02c1b8c --- /dev/null +++ b/apps/widbtstates/widget.ts @@ -0,0 +1,77 @@ +(() => { + "ram"; + + const enum State { + Asleep, + Active, + Connected + } + + let state: State = (() => { + const status = NRF.getSecurityStatus(); + + if (status.connected) return State.Connected; + if (status.advertising) return State.Active; + return State.Asleep; + })(); + + const width = () => state > State.Asleep ? 15 : 0; + + const update = (newState: State) => { + state = newState; + WIDGETS["bluetooth"]!.width = width(); + setTimeout(Bangle.drawWidgets, 50); // no need for .bind() + }; + + type DarkTheme = `${boolean}`; + const colours: { + [key in State.Active | State.Connected]: { + [key in DarkTheme]: ColorResolvable + } + } = { + [State.Active]: { + false: "#fff", + true: "#fff", + }, + [State.Connected]: { + false: "#0ff", + true: "#00f", + }, + }; + + WIDGETS["bluetooth"] = { + area: "tl", + sortorder: -1, + draw: function() { + if (state == State.Asleep) + return; + + g.reset(); + + g.setColor(colours[state][`${g.theme.dark}`]); + + g.drawImage( + atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), + this.x! + 2, + this.y! + 2 + ); + }, + width: width(), + }; + + NRF.on("connect", update.bind(null, State.Connected)); + NRF.on("disconnect", update.bind(null, State.Active)); + + const origWake = NRF.wake; + const origSleep = NRF.sleep; + + NRF.wake = function() { + update(State.Active); + return origWake.apply(this, arguments); + }; + + NRF.sleep = function() { + update(State.Asleep); + return origSleep.apply(this, arguments); + }; +})(); diff --git a/apps/widdevst/ChangeLog b/apps/widdevst/ChangeLog new file mode 100644 index 000000000..addaa5326 --- /dev/null +++ b/apps/widdevst/ChangeLog @@ -0,0 +1,2 @@ +0.01: First version +0.02: Support for Bangle.js 2 diff --git a/apps/widdevst/README.md b/apps/widdevst/README.md new file mode 100644 index 000000000..49affc78d --- /dev/null +++ b/apps/widdevst/README.md @@ -0,0 +1,15 @@ +# Device Status Widget + +This widget shows a rectangle containing + +- `B` if Bluetooth is on +- `C` if the compass is on +- `G` if GPS is on +- `H` if the heart rate monitor is on + +at fixed positions, and two bars + +- left to right: usage of Flash storage +- bottom to top: usage of RAM + +in green if below 50%, orange if between 50% and 80%, and red if above 80%. diff --git a/apps/widdevst/icon.png b/apps/widdevst/icon.png new file mode 100644 index 000000000..be242e1e1 Binary files /dev/null and b/apps/widdevst/icon.png differ diff --git a/apps/widdevst/metadata.json b/apps/widdevst/metadata.json new file mode 100644 index 000000000..91d6265fe --- /dev/null +++ b/apps/widdevst/metadata.json @@ -0,0 +1,13 @@ +{ "id": "widdevst", + "name": "Device Status Widget", + "version": "0.02", + "description": "Shows power status of Bluetooth, Compass, GPS and Heart Rate Monitor as well as storage and memory usage.", + "icon": "icon.png", + "type": "widget", + "tags": "widget,bluetooth,compass,gps,hrm", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name": "widdevst.wid.js", "url": "wid.js"} + ] +} diff --git a/apps/widdevst/wid.js b/apps/widdevst/wid.js new file mode 100644 index 000000000..06eb20cdd --- /dev/null +++ b/apps/widdevst/wid.js @@ -0,0 +1,41 @@ +(() => { + WIDGETS.devst = {area: "tr", width: 21, draw: function() { + if (WIDGETS.devst._draw) return; + var x = this.x; + var y = this.y; + g.reset(); + g.clearRect(x, y, x + 20, y + 23); + g.drawRect(x + 1, y + 1, x + 19, y + 22); + g.setFont('6x8', 1); + if (NRF.getSecurityStatus().connected) g.drawString('B', x + 4, y + 3); + if (Bangle.isCompassOn()) g.drawString('C', x + 12, y + 3); + if (Bangle.isGPSOn()) g.drawString('G', x + 4, y + 13); + if (Bangle.isHRMOn()) g.drawString('H', x + 12, y + 13); + var t = require('Storage').getStats(); + var u = t.fileBytes / t.totalBytes; + g.setColor(col(u)); g.drawRect(x + 1, y + 22, x + 1 + u * 18, y + 23); + t = process.memory(false); + u = t.usage / t.total; + g.setColor(col(u)); g.drawRect(x, y + 22 - u * 21, x + 1, y + 22); + }}; + + function col(p) { + return p < 0.5 ? '#0f0' : (p < 0.8 ? '#f80' : '#f00'); + } + + var draw = WIDGETS.devst.draw.bind(WIDGETS.devst); + var iid = setInterval(draw, Bangle.isLocked() ? 6e4 : 2e3); + + Bangle.on('lcdPower', (on) => { + if (on) { + draw(); + if (!iid) iid = setInterval(draw, Bangle.isLocked() ? 6e4 : 2e3); + } else if (iid) iid = clearInterval(iid); + }); + Bangle.on('lock', (on) => { + if (iid) { + clearInterval(iid); + iid = setInterval(draw, on ? 6e4 : 2e3); + } + }); +})(); diff --git a/apps/widdst/ChangeLog b/apps/widdst/ChangeLog index e350137ee..7cb1016f5 100644 --- a/apps/widdst/ChangeLog +++ b/apps/widdst/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial version 0.02: Checks for correct firmware; E.setDST(...) moved to boot.js +0.03: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/widdst/metadata.json b/apps/widdst/metadata.json index 144c02998..b97b61369 100644 --- a/apps/widdst/metadata.json +++ b/apps/widdst/metadata.json @@ -1,6 +1,6 @@ { "id": "widdst", "name": "Daylight Saving", - "version":"0.02", + "version":"0.03", "description": "Widget to set daylight saving rules. Requires Espruino 2v14.49 or later - see the instructions below for more information.", "icon": "icon.png", "type": "widget", diff --git a/apps/widdst/settings.js b/apps/widdst/settings.js index 9a7e579b7..7363aa6bf 100644 --- a/apps/widdst/settings.js +++ b/apps/widdst/settings.js @@ -23,7 +23,7 @@ at: 2 } }, require("Storage").readJSON("widdst.json", true) || {}); - + var dst_start_end = { is_start: true, day_offset: 0, @@ -32,11 +32,11 @@ month: 0, at: 0 }; - + function writeSettings() { require('Storage').writeJSON("widdst.json", settings); } - + function writeSubMenuSettings() { if (dst_start_end.is_start) { settings.dst_start.day_offset = dst_start_end.day_offset; @@ -53,11 +53,11 @@ } writeSettings(); } - + function hoursToString(h) { return (h|0) + ':' + (((6*h)%6)|0) + (((60*h)%10)|0); } - + function getDSTStartEndMenu(start) { dst_start_end.is_start = start; if (start) { @@ -131,23 +131,21 @@ } } } - + var dstMenu = { "": { "Title": /*LANG*/"Daylight Saving" }, "< Back": () => back(), /*LANG*/"Enabled": { - value: settings.has_dst, - format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", + value: !!settings.has_dst, onchange: v => { settings.has_dst = v; writeSettings(); } }, /*LANG*/"Icon": { - value: settings.show_icon, - format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", + value: !!settings.show_icon, onchange: v => { settings.show_icon = v; writeSettings(); @@ -178,7 +176,7 @@ /*LANG*/"DST Start": () => E.showMenu(getDSTStartEndMenu(true)), /*LANG*/"DST End": () => E.showMenu(getDSTStartEndMenu(false)) }; - + E.showMenu(dstMenu); }); diff --git a/apps/widlockunlock/ChangeLog b/apps/widlockunlock/ChangeLog index b4d1ae593..b5efcaa86 100644 --- a/apps/widlockunlock/ChangeLog +++ b/apps/widlockunlock/ChangeLog @@ -1 +1,2 @@ 0.01: First commit +0.02: Add tap-to-lock functionality diff --git a/apps/widlockunlock/metadata.json b/apps/widlockunlock/metadata.json index d701279b9..cc4fa76cd 100644 --- a/apps/widlockunlock/metadata.json +++ b/apps/widlockunlock/metadata.json @@ -1,8 +1,8 @@ { "id": "widlockunlock", "name": "Lock/Unlock Widget", - "version": "0.01", - "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise", + "version": "0.02", + "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise. Tap to lock the lcd", "icon": "widget.png", "type": "widget", "tags": "widget,lock", diff --git a/apps/widlockunlock/widget.js b/apps/widlockunlock/widget.js index 0716a9edf..cfbbc87a3 100644 --- a/apps/widlockunlock/widget.js +++ b/apps/widlockunlock/widget.js @@ -1,5 +1,28 @@ -Bangle.on("lockunlock", function() { - Bangle.drawWidgets(); +Bangle.on("lock", () => Bangle.drawWidgets()); + +Bangle.on('touch', (_btn, xy) => { + const oversize = 5; + + const w = WIDGETS.lockunlock; + + const x = xy.x; + const y = xy.y; + + if(w.x - oversize <= x && x < w.x + 14 + oversize + && w.y - oversize <= y && y < w.y + 24 + oversize) + { + Bangle.setLocked(true); + + const backlightTimeout = Bangle.getOptions().backlightTimeout; // ms + + // seems to be a race/if we don't give the firmware enough time, + // it won't timeout the backlight and we'll restore it in our setTimeout below + Bangle.setOptions({ backlightTimeout: 100 }); + + setTimeout(() => { + Bangle.setOptions({ backlightTimeout }); + }, 300); + } }); WIDGETS["lockunlock"]={area:"tl",sortorder:10,width:14,draw:function(w) { g.reset().drawImage(atob(Bangle.isLocked() ? "DBGBAAAA8DnDDCBCBP////////n/n/n//////z/A" : "DBGBAAAA8BnDDCBABP///8A8A8Y8Y8Y8A8A//z/A"), w.x+1, w.y+3); diff --git a/apps/wohrm/metadata.json b/apps/wohrm/metadata.json index b2b89336c..e41c65023 100644 --- a/apps/wohrm/metadata.json +++ b/apps/wohrm/metadata.json @@ -5,7 +5,7 @@ "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "icon": "app.png", "type": "app", - "tags": "hrm,workout", + "tags": "hrm,workout,health", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, diff --git a/bin/language_render.js b/bin/language_render.js new file mode 100755 index 000000000..07c23f65c --- /dev/null +++ b/bin/language_render.js @@ -0,0 +1,64 @@ +#!/bin/node +/* + Takes language files that have been written with unicode chars that Bangle.js cannot render + with its built-in fonts, and pre-render them. +*/ + +//const FONT_SIZE = 18; +//const FONT_NAME = 'Sans'; +const FONT_SIZE = 16; // 12pt +const FONT_NAME = '"Unifont Regular"'; // or just 'Sans' + +var createCanvas, registerFont; +try { + createCanvas = require("canvas").createCanvas; + registerFont = require("canvas").registerFont; +} catch(e) { + console.log("ERROR: needc canvas library"); + console.log("Try: npm install canvas"); + process.exit(1); +} +// Use font from https://unifoundry.com/unifont/ as it scales well at 16px high +registerFont(__dirname+'/unifont-15.0.01.ttf', { family: 'Unifont Regular' }) + +var imageconverter = require(__dirname+"/../webtools/imageconverter.js"); + +const canvas = createCanvas(200, 20) +const ctx = canvas.getContext('2d') + +function renderText(txt) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.font = FONT_SIZE+'px '+FONT_NAME; + ctx.fillStyle = "white"; + ctx.fillText(txt, 0, FONT_SIZE); + var str = imageconverter.canvastoString(canvas, { autoCrop:true, output:"raw", mode:"1bit", transparent:true } ); + // for testing: +// console.log(txt); +// console.log("g.drawImage(",imageconverter.canvastoString(canvas, { autoCrop:true, output:"string", mode:"1bit" } ),");"); +// process.exit(1); + return "\0"+str; +} + +function renderLangFile(file) { + var fileIn = __dirname + "/../lang/unicode-based/"+file; + var fileOut = __dirname + "/../lang/"+file; + console.log("Reading",fileIn); + var inJSON = JSON.parse(require("fs").readFileSync(fileIn)); + var outJSON = { "// created with bin/language_render.js" : ""}; + for (var categoryName in inJSON) { + if (categoryName.includes("//")) continue; + var category = inJSON[categoryName]; + outJSON[categoryName] = {}; + for (var english in category) { + if (english.includes("//")) continue; + var translated = category[english]; + //console.log(english,"=>",translated); + outJSON[categoryName][english] = renderText(translated); + } + } + require("fs").writeFileSync(fileOut, JSON.stringify(outJSON,null,2)); + console.log("Written",fileOut); +} + + +renderLangFile("ja_JA.json"); diff --git a/bin/language_scan.js b/bin/language_scan.js index 464d8f998..99d91b519 100755 --- a/bin/language_scan.js +++ b/bin/language_scan.js @@ -59,7 +59,7 @@ function handleCliParameters () console.log(' --turl URL In combination with --deepl, use URL as the API base URL'); process.exit(0); default: - die("Unknown parameter: "+param); + die("Unknown parameter: "+param+", use --help for options"); } } if((hadTURL !== false || hadDEEPL !== false) && hadTURL !== hadDEEPL) @@ -122,25 +122,11 @@ function log(s) { console.log(s); } -var appsFile, apps; -try { - appsFile = fs.readFileSync(BASEDIR+"apps.json").toString(); -} catch (e) { - ERROR("apps.json not found"); -} -if (appsFile.indexOf("---") === 0 && fs.existsSync(BASEDIR+"bin/create_apps_json.sh")) -{ - console.log("apps.json has not been generated, running bin/create_apps_json.sh to build it..."); - childProcess.execFileSync(BASEDIR+'bin/create_apps_json.sh',[],{ - stdio: 'inherit' - }); - appsFile = fs.readFileSync(BASEDIR+"apps.json").toString(); -} -try{ - apps = JSON.parse(appsFile); -} catch (e) { - ERROR("apps.json not valid JSON"); -} +var apploader = require("./lib/apploader.js"); +apploader.init({ + DEVICEID : "BANGLEJS2" +}); +var apps = apploader.apps; // Given a string value, work out if it's obviously not a text string function isNotString(s, wasFnCall, wasArrayAccess) { diff --git a/bin/unifont-15.0.01.ttf b/bin/unifont-15.0.01.ttf new file mode 100644 index 000000000..bc3428d96 Binary files /dev/null and b/bin/unifont-15.0.01.ttf differ diff --git a/core b/core index 893c2dbbe..96d7ec120 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 893c2dbbe5a93fbb80d035a695663b4f4cca8875 +Subproject commit 96d7ec120740726b2fdc63d8c9ce760b38b3edd3 diff --git a/index.html b/index.html index cc69781ee..1ed95107e 100644 --- a/index.html +++ b/index.html @@ -148,7 +148,7 @@