diff --git a/.eslintrc.js b/.eslintrc.js index 5d15ec385..e79f87a5d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -245,4 +245,5 @@ module.exports = { })), ], ignorePatterns: findGeneratedJS(["apps/", "modules/"]), + reportUnusedDisableDirectives: true, } diff --git a/README.md b/README.md index 9b0458043..d595c7df1 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ in an iframe. - +
Loading...
- + + + + + \ No newline at end of file diff --git a/apps/reply/lib.js b/apps/reply/lib.js new file mode 100644 index 000000000..4a040c557 --- /dev/null +++ b/apps/reply/lib.js @@ -0,0 +1,69 @@ +exports.reply = function (options) { + var keyboard = "textinput"; + try { + keyboard = require(keyboard); + } catch (e) { + keyboard = null; + } + + function constructReply(msg, replyText, resolve) { + var responseMessage = {msg: replyText}; + if (msg.id) { + responseMessage = { t: "notify", id: msg.id, n: "REPLY", msg: replyText }; + } + E.showMenu(); + if (options.sendReply == null || options.sendReply) { + Bluetooth.println(JSON.stringify(responseMessage)); + } + resolve(responseMessage); + } + + return new Promise((resolve, reject) => { + var menu = { + "": { + title: options.title || /*LANG*/ "Reply with:", + back: function () { + E.showMenu(); + reject("User pressed back"); + }, + }, // options + /*LANG*/ "Compose": function () { + keyboard.input().then((result) => { + constructReply(options.msg ?? {}, result, resolve); + }); + }, + }; + var replies = + require("Storage").readJSON( + options.fileOverride || "replies.json", + true + ) || []; + replies.forEach((reply) => { + menu = Object.defineProperty(menu, reply.text, { + value: () => constructReply(options.msg ?? {}, reply.text, resolve), + }); + }); + if (!keyboard) delete menu[/*LANG*/ "Compose"]; + + if (replies.length == 0) { + if (!keyboard) { + E.showPrompt( + /*LANG*/ "Please install a keyboard app, or set a custom reply via the app loader!", + { + buttons: { Ok: true }, + remove: function () { + reject( + "Please install a keyboard app, or set a custom reply via the app loader!" + ); + }, + } + ); + } else { + keyboard.input().then((result) => { + constructReply(options.msg.id, result, resolve); + }); + } + } + E.showMenu(menu); + }); +}; diff --git a/apps/reply/metadata.json b/apps/reply/metadata.json new file mode 100644 index 000000000..34843edd4 --- /dev/null +++ b/apps/reply/metadata.json @@ -0,0 +1,16 @@ +{ "id": "reply", + "name": "Reply Library", + "version": "0.01", + "description": "A library for replying to text messages via predefined responses or keyboard", + "icon": "app.png", + "type": "module", + "provides_modules" : ["reply"], + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"reply","url":"lib.js"} + ], + "data": [{"name":"replies.json"}] +} \ No newline at end of file diff --git a/apps/schoolCalendar/fullcalendar/main.js b/apps/schoolCalendar/fullcalendar/main.js index 95650f472..a41e45c67 100644 --- a/apps/schoolCalendar/fullcalendar/main.js +++ b/apps/schoolCalendar/fullcalendar/main.js @@ -111,7 +111,7 @@ var FullCalendar = (function (exports) { ContextType.Provider = function () { var _this = this; var isNew = !this.getChildContext; - var children = origProvider.apply(this, arguments); // eslint-disable-line prefer-rest-params + var children = origProvider.apply(this, arguments); if (isNew) { var subs_1 = []; this.shouldComponentUpdate = function (_props) { @@ -4688,14 +4688,14 @@ var FullCalendar = (function (exports) { var wrappedSuccess = function () { if (!isResolved) { isResolved = true; - success.apply(this, arguments); // eslint-disable-line prefer-rest-params + success.apply(this, arguments); } }; var wrappedFailure = function () { if (!isResolved) { isResolved = true; if (failure) { - failure.apply(this, arguments); // eslint-disable-line prefer-rest-params + failure.apply(this, arguments); } } }; @@ -5008,7 +5008,7 @@ var FullCalendar = (function (exports) { var createPortal = FullCalendarVDom.createPortal; var flushToDom = FullCalendarVDom.flushToDom; var unmountComponentAtNode = FullCalendarVDom.unmountComponentAtNode; - /* eslint-enable */ + var ScrollResponder = /** @class */ (function () { function ScrollResponder(execFunc, emitter, scrollTime, scrollTimeReset) { @@ -5085,7 +5085,7 @@ var FullCalendar = (function (exports) { } PureComponent.prototype.shouldComponentUpdate = function (nextProps, nextState) { if (this.debug) { - // eslint-disable-next-line no-console + console.log(getUnequalProps(nextProps, this.props), getUnequalProps(nextState, this.state)); } return !compareObjs(this.props, nextProps, this.propEquality) || @@ -6613,7 +6613,7 @@ var FullCalendar = (function (exports) { var endMarker = framingRange.end; var instanceStarts = []; while (dayMarker < endMarker) { - var instanceStart + var instanceStart // if everyday, or this particular day-of-week = void 0; // if everyday, or this particular day-of-week @@ -11731,7 +11731,7 @@ var FullCalendar = (function (exports) { } dragging.emitter.on('pointerdown', this.handlePointerDown); dragging.emitter.on('dragstart', this.handleDragStart); - new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new + new ExternalElementDragging(dragging, settings.eventData); } ExternalDraggable.prototype.destroy = function () { this.dragging.destroy(); @@ -11833,7 +11833,7 @@ var FullCalendar = (function (exports) { if (typeof settings.mirrorSelector === 'string') { dragging.mirrorSelector = settings.mirrorSelector; } - new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new + new ExternalElementDragging(dragging, settings.eventData); } ThirdPartyDraggable.prototype.destroy = function () { this.dragging.destroy(); @@ -13605,7 +13605,7 @@ var FullCalendar = (function (exports) { if (!slatCoords) { return null; } - return segs.map(function (seg, i) { return (createElement(NowIndicatorRoot, { isAxis: false, date: date, + return segs.map(function (seg, i) { return (createElement(NowIndicatorRoot, { isAxis: false, date: date, // key doesn't matter. will only ever be one key: i }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-line'].concat(classNames).join(' '), style: { top: slatCoords.computeDateTop(seg.start, date) } }, innerContent)); })); }); }; diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index d042f7fa6..c74cfda30 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -85,4 +85,11 @@ of 'Select Clock' 0.69: Add option to wake on double tap 0.70: Fix load() typo 0.71: Minor code improvements +<<<<<<< HEAD >>>>>>> 7a3e0d2e7e1acef8ad643a9b7cc35f240aab9488 +||||||| a038233fa +======= +0.72: Add setting for configuring BLE privacy +0.73: Fix `const` bug / work with fastload +0.74: Add extra layer of checks before allowing a factory reset (fix #3476) +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index 43c7797a7..67ca847d9 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.71", + "version": "0.74", "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 99a8971fb..d9d77d052 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -1,3 +1,4 @@ +{ Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -159,6 +160,8 @@ function showAlertsMenu() { function showBLEMenu() { var hidV = [false, "kbmedia", "kb", "com", "joy"]; var hidN = [/*LANG*/"Off", /*LANG*/"Kbrd & Media", /*LANG*/"Kbrd", /*LANG*/"Kbrd & Mouse", /*LANG*/"Joystick"]; + var privacy = [/*LANG*/"Off", /*LANG*/"Show name", /*LANG*/"Hide name"]; + E.showMenu({ '': { 'title': /*LANG*/'Bluetooth' }, '< Back': ()=>showMainMenu(), @@ -177,6 +180,32 @@ function showBLEMenu() { updateSettings(); } }, + /*LANG*/'Privacy': { + min: 0, max: privacy.length-1, + format: v => privacy[v], + value: (() => { + // settings.bleprivacy may be some custom object, but we ignore that for now + if (settings.bleprivacy && settings.blename === false) return 2; + if (settings.bleprivacy) return 1; + return 0; + })(), + onchange: v => { + settings.bleprivacy = 0; + delete settings.blename; + switch (v) { + case 0: + break; + case 1: + settings.bleprivacy = 1; + break; + case 2: + settings.bleprivacy = 1; + settings.blename = false; + break; + } + updateSettings(); + } + }, /*LANG*/'HID': { value: Math.max(0,0 | hidV.indexOf(settings.HID)), min: 0, max: hidN.length-1, @@ -622,11 +651,11 @@ function showUtilMenu() { E.showMessage(/*LANG*/'Flattening battery - this can take hours.\nLong-press button to cancel.'); Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); + Bangle.setLCDBrightness(1); if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat"); if (Bangle.setHRMPower) Bangle.setHRMPower(1,"flat"); if (Bangle.setCompassPower) Bangle.setCompassPower(1,"flat"); if (Bangle.setBarometerPower) Bangle.setBarometerPower(1,"flat"); - if (Bangle.setHRMPower) Bangle.setGPSPower(1,"flat"); setInterval(function() { var i=1000;while (i--); }, 1); @@ -634,7 +663,7 @@ function showUtilMenu() { }; if (BANGLEJS2) menu[/*LANG*/'Calibrate Battery'] = () => { - E.showPrompt(/*LANG*/"Is the battery fully charged?",{title:/*LANG*/"Calibrate"}).then(ok => { + E.showPrompt(/*LANG*/"Is the battery fully charged?",{title:/*LANG*/"Calibrate",back:showUtilMenu}).then(ok => { if (ok) { var s=storage.readJSON("setting.json"); s.batFullVoltage = (analogRead(D3)+analogRead(D3)+analogRead(D3)+analogRead(D3))/4; @@ -646,7 +675,7 @@ function showUtilMenu() { }); }; menu[/*LANG*/'Reset Settings'] = () => { - E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => { + E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings",back:showUtilMenu}).then((v) => { if (v) { E.showMessage(/*LANG*/'Resetting'); resetSettings(); @@ -656,7 +685,7 @@ function showUtilMenu() { }; menu[/*LANG*/"Turn Off"] = () => { E.showPrompt(/*LANG*/"Are you sure? Alarms and timers won't fire", { - title:/*LANG*/"Turn Off" + title:/*LANG*/"Turn Off",back:showUtilMenu }).then((confirmed) => { if (confirmed) { E.showMessage(/*LANG*/"See you\nlater!", /*LANG*/"Goodbye"); @@ -673,14 +702,24 @@ function showUtilMenu() { } }); }; - if (Bangle.factoryReset) { menu[/*LANG*/'Factory Reset'] = ()=>{ - E.showPrompt(/*LANG*/'This will remove everything!',{title:/*LANG*/"Factory Reset"}).then((v) => { + E.showPrompt(/*LANG*/'This will remove everything!',{title:/*LANG*/"Factory Reset",back:showUtilMenu}).then((v) => { if (v) { - E.showMessage(); - Terminal.setConsole(); - Bangle.factoryReset(); + var n = ((Math.random()*4)&3) + 1; + E.showPrompt(/*LANG*/"To confirm, please press "+n,{ + title:/*LANG*/"Factory Reset", + buttons : {"1":1,"2":2,"3":3,"4":4}, + back:showUtilMenu + }).then(function(v) { + if (v==n) { + E.showMessage(); + Terminal.setConsole(); + Bangle.factoryReset(); + } else { + showUtilMenu(); + } + }); } else showUtilMenu(); }); } @@ -956,3 +995,4 @@ function showTouchscreenCalibration() { } showMainMenu(); +} diff --git a/apps/setuichange/ChangeLog b/apps/setuichange/ChangeLog new file mode 100644 index 000000000..397e4f509 --- /dev/null +++ b/apps/setuichange/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Fix case where we tried to push to Bangle.btnWatches but it wasn't + defined. diff --git a/apps/setuichange/README.md b/apps/setuichange/README.md new file mode 100644 index 000000000..024ec9971 --- /dev/null +++ b/apps/setuichange/README.md @@ -0,0 +1,24 @@ +# setUI Proposals Preview + +Try out changes to setUI that may or may not eventually en up in the Bangle.js firmware. + +## Usage + +Just install it and it modifies setUI at boot time. + +## Features + +- Add custom handlers on top of the standard modes as well. Previously this was only possible for mode == "custom". + - The goal here is to make it possible to move all input handling inside `setUI` where today some apps add on extra handlers outside of `setUI` calls. +- Change the default behaviour of the hardware button to act immediately on press down. Previously it has been acting on button release. + - This makes the interaction slightly snappier. + - In addition to the existing `btn` key a new `btnRelease` key can now be specified. `btnRelease` will let you listen to the rising edge of the hardware button. + +## Requests + +Please report your experience and thoughts on this issue: +[ Discussion: HW buttons should act on 'rising' edge #3435 ](https://github.com/espruino/BangleApps/issues/3435) or on the related forum conversation [Making Bangle.js more responsive](https://forum.espruino.com/conversations/397606/). + +## Creator + +The changes done here were done by thyttan with help from Gordon Williams. diff --git a/apps/setuichange/app.png b/apps/setuichange/app.png new file mode 100644 index 000000000..78bea6c3b Binary files /dev/null and b/apps/setuichange/app.png differ diff --git a/apps/setuichange/boot.js b/apps/setuichange/boot.js new file mode 100644 index 000000000..c9f7aa898 --- /dev/null +++ b/apps/setuichange/boot.js @@ -0,0 +1,152 @@ +Bangle.setUI = (function(mode, cb) { + var options = {}; + if ("object"==typeof mode) { + options = mode; + mode = options.mode; + if (!mode) throw new Error("Missing mode in setUI({...})"); + } + var redraw = true; + if (global.WIDGETS && WIDGETS.back) { + redraw = false; + WIDGETS.back.remove(mode && options.back); + } + if (Bangle.btnWatches) { + Bangle.btnWatches.forEach(clearWatch); + delete Bangle.btnWatches; + } + if (Bangle.swipeHandler) { + Bangle.removeListener("swipe", Bangle.swipeHandler); + delete Bangle.swipeHandler; + } + if (Bangle.dragHandler) { + Bangle.removeListener("drag", Bangle.dragHandler); + delete Bangle.dragHandler; + } + if (Bangle.touchHandler) { + Bangle.removeListener("touch", Bangle.touchHandler); + delete Bangle.touchHandler; + } + delete Bangle.uiRedraw; + delete Bangle.CLOCK; + if (Bangle.uiRemove) { + let r = Bangle.uiRemove; + delete Bangle.uiRemove; // stop recursion if setUI is called inside uiRemove + r(); + } + g.reset();// reset graphics state, just in case + if (!mode) return; + function b() { + try{Bangle.buzz(30);}catch(e){} + } + if (mode=="updown") { + var dy = 0; + Bangle.dragHandler = e=>{ + dy += e.dy; + if (!e.b) dy=0; + while (Math.abs(dy)>32) { + if (dy>0) { dy-=32; cb(1) } + else { dy+=32; cb(-1) } + Bangle.buzz(20); + } + }; + Bangle.on('drag',Bangle.dragHandler); + Bangle.touchHandler = d => {b();cb();}; + Bangle.btnWatches = [ + setWatch(function() { b();cb(); }, BTN1, {repeat:1, edge:"rising"}), + ]; + } else if (mode=="leftright") { + var dx = 0; + Bangle.dragHandler = e=>{ + dx += e.dx; + if (!e.b) dx=0; + while (Math.abs(dx)>32) { + if (dx>0) { dx-=32; cb(1) } + else { dx+=32; cb(-1) } + Bangle.buzz(20); + } + }; + Bangle.on('drag',Bangle.dragHandler); + Bangle.touchHandler = d => {b();cb();}; + Bangle.btnWatches = [ + setWatch(function() { b();cb(); }, BTN1, {repeat:1, edge:"rising"}), + ]; + } else if (mode=="clock") { + Bangle.CLOCK=1; + Bangle.btnWatches = [ + setWatch(Bangle.showLauncher, BTN1, {repeat:1,edge:"rising"}) + ]; + } else if (mode=="clockupdown") { + Bangle.CLOCK=1; + Bangle.touchHandler = (d,e) => { + if (e.x < 120) return; + b();cb((e.y > 88) ? 1 : -1); + }; + Bangle.btnWatches = [ + setWatch(Bangle.showLauncher, BTN1, {repeat:1,edge:"rising"}) + ]; + } else if (mode=="custom") { + if (options.clock) { + Bangle.btnWatches = [ + setWatch(Bangle.showLauncher, BTN1, {repeat:1,edge:"rising"}) + ]; + } + } else + throw new Error("Unknown UI mode "+E.toJS(mode)); + if (options.clock) Bangle.CLOCK=1; + if (options.touch) + Bangle.touchHandler = options.touch; + if (options.drag) { + Bangle.dragHandler = options.drag; + Bangle.on("drag", Bangle.dragHandler); + } + if (options.swipe) { + Bangle.swipeHandler = options.swipe; + Bangle.on("swipe", Bangle.swipeHandler); + } + if ((options.btn || options.btnRelease) && !Bangle.btnWatches) Bangle.btnWatches = []; + if (options.btn) Bangle.btnWatches.push(setWatch(options.btn.bind(options), BTN1, {repeat:1,edge:"rising"})) + if (options.btnRelease) Bangle.btnWatches.push(setWatch(options.btnRelease.bind(options), BTN1, {repeat:1,edge:"falling"})) + if (options.remove) // handler for removing the UI (intervals/etc) + Bangle.uiRemove = options.remove; + if (options.redraw) // handler for redrawing the UI + Bangle.uiRedraw = options.redraw; + if (options.back) { + var touchHandler = (_,e) => { + if (e.y<36 && e.x<48) { + e.handled = true; + E.stopEventPropagation(); + options.back(); + } + }; + Bangle.on("touch", touchHandler); + // If a touch handler was needed for setUI, add it - but ignore touches if they've already gone to the 'back' handler + if (Bangle.touchHandler) { + var uiTouchHandler = Bangle.touchHandler; + Bangle.touchHandler = (_,e) => { + if (!e.handled) uiTouchHandler(_,e); + }; + Bangle.on("touch", Bangle.touchHandler); + } + var btnWatch; + if (Bangle.btnWatches===undefined) // only add back button handler if there's no existing watch on BTN1 + btnWatch = setWatch(function() { + btnWatch = undefined; + options.back(); + }, BTN1, {edge:"rising"}); + WIDGETS = Object.assign({back:{ + area:"tl", width:24, + draw:e=>g.reset().setColor("#f00").drawImage(atob("GBiBAAAYAAH/gAf/4A//8B//+D///D///H/P/n+H/n8P/n4f/vwAP/wAP34f/n8P/n+H/n/P/j///D///B//+A//8Af/4AH/gAAYAA=="),e.x,e.y), + remove:(noclear)=>{ + if (btnWatch) clearWatch(btnWatch); + Bangle.removeListener("touch", touchHandler); + if (!noclear) g.reset().clearRect({x:WIDGETS.back.x, y:WIDGETS.back.y, w:24,h:24}); + delete WIDGETS.back; + if (!noclear) Bangle.drawWidgets(); + } + }},global.WIDGETS); + if (redraw) Bangle.drawWidgets(); + } else { // If a touch handler was needed for setUI, add it + if (Bangle.touchHandler) + Bangle.on("touch", Bangle.touchHandler); + } +}) diff --git a/apps/setuichange/metadata.json b/apps/setuichange/metadata.json new file mode 100644 index 000000000..2d6cafc81 --- /dev/null +++ b/apps/setuichange/metadata.json @@ -0,0 +1,13 @@ +{ "id": "setuichange", + "name": "SetUI Proposals preview", + "version":"0.02", + "description": "Try out potential future changes to `Bangle.setUI`. Makes hardware button interaction snappier. Makes it possible to set custom event handlers on any type/mode, not just `\"custom\"`. Please provide feedback - see `Read more...` below.", + "icon": "app.png", + "tags": "", + "type": "bootloader", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"setuichange.0.boot.js","url":"boot.js"} + ] +} diff --git a/apps/sixths/ChangeLog b/apps/sixths/ChangeLog index 08c4b83b0..510748fb3 100644 --- a/apps/sixths/ChangeLog +++ b/apps/sixths/ChangeLog @@ -2,3 +2,4 @@ 0.02: better GPS support, adding altitude and temperature support 0.03: minor code improvements 0.04: make height auto-calibration useful and slow ticks to save power +0.05: add ability to navigate to waypoints, better documentation diff --git a/apps/sixths/README.md b/apps/sixths/README.md index 17369c7a0..18aa85beb 100644 --- a/apps/sixths/README.md +++ b/apps/sixths/README.md @@ -24,17 +24,35 @@ minutes, real distance will be usually higher than approximation. Useful gestures: -F -- disable GPS. -G -- enable GPS for 4 hours in low power mode. -N -- take a note and write it to the log. -S -- enable GPS for 30 minutes in high power mode. + B -- "Battery", show/buzz battery info +D -- "Down", previous waypoint +F -- "oFf", disable GPS. +G -- "Gps", enable GPS for 4 hours in low power mode. +I -- "Info", toggle info display + L -- "aLtimeter", load altimeter app +M -- "Mark", create mark from current position +N -- "Note", take a note and write it to the log. + O -- "Orloj", run orloj app + R -- "Run", run "runplus" app +S -- "Speed", enable GPS for 30 minutes in high power mode. + T -- "Time", buzz current time +U -- "Up", next waypoint +Y -- "compass", reset compass When application detects watch is being worn, it will use vibrations to communicate back to the user. +B -- battery low. E -- acknowledge, gesture understood. T -- start of new hour. +Three colored dots may appear on display. North is on the 12 o'clock +position (top of the display). + +red: this is direction to the waypoint. +green: this is direction you are moving into, according to GPS. +blue: this is direction top of watch faces, according to the compass. + Written by: [Pavel Machek](https://github.com/pavelmachek) ## Future Development @@ -55,4 +73,27 @@ Todo: *) only turn on compass when needed -*) adjust draw timeouts to save power \ No newline at end of file +*) only warn about battery low when it crosses thresholds, update +battery low message + +*) rename "show" to something else -- it collides with built-in + +*) adjust clock according to GPS + +*) show something more reasonable than (NOTEHERE). + +*) hide messages after timeout. + +*) show route lengths after the fact + +*) implement longer recording than "G". + +*) Probably T should be G. + +*) sum gps distances for a day + +*) allow setting up home altitude, or at least disable auto-calibration + +*) show time-to-sunset / sunrise? + +*) one-second updates when gps is active \ No newline at end of file diff --git a/apps/sixths/metadata.json b/apps/sixths/metadata.json index 91492fe1c..585f23170 100644 --- a/apps/sixths/metadata.json +++ b/apps/sixths/metadata.json @@ -1,6 +1,6 @@ { "id": "sixths", "name": "Sixth sense", - "version": "0.04", + "version": "0.05", "description": "Clock for outdoor use with GPS support", "icon": "app.png", "readme": "README.md", diff --git a/apps/sixths/sixths.app.js b/apps/sixths/sixths.app.js index 00c83153f..c5fb3b9cf 100644 --- a/apps/sixths/sixths.app.js +++ b/apps/sixths/sixths.app.js @@ -3,18 +3,16 @@ // Options you'll want to edit const rest_altitude = 354; -const geoid_to_sea_level = 0; // Maybe BangleJS2 already compensates? const W = g.getWidth(); const H = g.getHeight(); -var cx = 100, cy = 105, sc = 70, -temp = 0, alt = 0, bpm = 0; +var cx = 100, cy = 105, sc = 70, temp = 0, alt = 0, bpm = 0; var buzz = "", /* Set this to transmit morse via vibrations */ inm = "", l = "", /* For incoming morse handling */ in_str = "", - note = "(NOTEHERE)", - debug = "v1119", debug2 = "(otherdb)", debug3 = "(short)"; + note = "", + debug = "v0.04.1", debug2 = "(otherdb)", debug3 = "(short)"; var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note, 2.. mark name var disp_mode = 0; // 0 .. normal, 1 .. small time @@ -45,15 +43,15 @@ var cur_mark = null; // Icons var icon_alt = "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; -var icon_m = "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; +//var icon_m = "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; var icon_km = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; var icon_kph = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3"; var icon_c = "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; function toMorse(x) { - var r = ""; + let r = ""; for (var i = 0; i < x.length; i++) { - var c = x[i]; + let c = x[i]; if (c == " ") { r += " "; continue; @@ -113,20 +111,15 @@ function gpsHandleFix(fix) { doBuzz(" ."); prev_fix = fix; } - if (0) { - /* GPS altitude fluctuates a lot, not really usable */ - alt_adjust = cur_altitude - (fix.alt + geoid_to_sea_level); - alt_adjust_mode = "g"; - } if (1) { let now1 = Date(); let now2 = fix.time; - var n1 = now1.getMinutes() * 60 + now1.getSeconds(); - var n2 = now2.getMinutes() * 60 + now2.getSeconds(); + let n1 = now1.getMinutes() * 60 + now1.getSeconds(); + let n2 = now2.getMinutes() * 60 + now2.getSeconds(); debug2 = "te "+(n2-n1)+"s"; } loggps(fix); - var d = calcDistance(fix, prev_fix); + let d = calcDistance(fix, prev_fix); if (d > 30) { prev_fix = fix; gps_dist += d/1000; @@ -135,7 +128,7 @@ function gpsHandleFix(fix) { function gpsHandle() { let msg = ""; if (!last_restart) { - var d = (getTime()-last_pause); + let d = (getTime()-last_pause); if (last_fix) msg = "PL"+ fmtTimeDiff(getTime()-last_fix); else @@ -146,7 +139,7 @@ function gpsHandle() { gpsRestart(); } } else { - var fix = Bangle.getGPSFix(); + let fix = Bangle.getGPSFix(); if (fix && fix.fix && fix.lat) { gpsHandleFix(fix); msg = fix.speed.toFixed(1) + icon_kph; @@ -167,8 +160,8 @@ function gpsHandle() { } } - var d = (getTime()-last_restart); - var d2 = (getTime()-last_fstart); + let d = (getTime()-last_restart); + let d2 = (getTime()-last_fstart); print("gps on, restarted ", d, gps_needed, d2, fix.lat); if (getTime() > gps_speed_limit && (d > gps_needed || (last_fstart && d2 > 10))) { @@ -192,8 +185,11 @@ function markNew() { } function markHandle() { let m = cur_mark; - var msg = m.name + ">" + fmtTimeDiff(getTime()- m.time); - if (m.fix && m.fix.fix) { + let msg = m.name + ">"; + if (m.time) { + msg += fmtTimeDiff(getTime()- m.time); + } + if (prev_fix && prev_fix.fix && m.fix && m.fix.fix) { let s = fmtDist(calcDistance(m.fix, prev_fix)/1000) + icon_km; msg += " " + s; debug = "wp>" + s; @@ -214,6 +210,34 @@ function entryDone() { in_str = 0; mode = 0; } +var waypoints = [], sel_wp = 0; +function loadWPs() { + waypoints = require("Storage").readJSON(`waypoints.json`)||[{}]; + print("Have waypoints", waypoints); +} +function saveWPs() { + require("Storage").writeJSON(`waypoints.json`,waypoints); +} +function selectWP(i) { + sel_wp += i; + if (sel_wp < 0) + sel_wp = 0; + if (sel_wp >= waypoints.length) + sel_wp = waypoints.length - 1; + if (sel_wp < 0) { + show("No WPs", 60); + } + let wp = waypoints[sel_wp]; + cur_mark = {}; + cur_mark.name = wp.name; + cur_mark.gps_dist = 0; /* HACK */ + cur_mark.fix = {}; + cur_mark.fix.fix = 1; + cur_mark.fix.lat = wp.lat; + cur_mark.fix.lon = wp.lon; + show("WP:"+wp.name, 60); + print("Select waypoint: ", cur_mark); +} function inputHandler(s) { print("Ascii: ", s, s[0], s[1]); if (s[0] == '^') { @@ -230,9 +254,9 @@ function inputHandler(s) { return; } switch(s) { - case 'B': + case 'B': { s = ' B'; - var bat = E.getBattery(); + let bat = E.getBattery(); if (bat > 45) s += 'E'; else @@ -240,6 +264,8 @@ function inputHandler(s) { doBuzz(toMorse(s)); show("Bat "+bat+"%", 60); break; + } + case 'D': selectWP(1); break; case 'F': gpsOff(); show("GPS off", 3); break; case 'G': gpsOn(); gps_limit = getTime() + 60*60*4; show("GPS on", 3); break; case 'I': @@ -252,15 +278,17 @@ function inputHandler(s) { case 'M': mode = 2; show("M>", 10); cur_mark = markNew(); mode_time = getTime(); break; case 'N': mode = 1; show(">", 10); mode_time = getTime(); break; case 'O': aload("orloj.app.js"); break; + case 'R': aload("runplus.app.js"); break; case 'S': gpsOn(); gps_limit = getTime() + 60*30; gps_speed_limit = gps_limit; show("GPS on", 3); break; - case 'T': + case 'T': { s = ' T'; - var d = new Date(); + let d = new Date(); s += d.getHours() % 10; s += add0(d.getMinutes()); doBuzz(toMorse(s)); break; - case 'R': aload("run.app.js"); break; + } + case 'U': selectWP(-1); break; case 'Y': doBuzz(buzz); Bangle.resetCompass(); break; } } @@ -378,23 +406,22 @@ function loggps(fix) { } function hourly() { print("hourly"); - var s = ' T'; + let s = ' T'; + let bat = E.getBattery(); + if (bat < 25) { + s = ' B'; + show("Bat "+bat+"%", 60); + } if (is_active) doBuzz(toMorse(s)); - logstamp(""); + //logstamp(""); } function show(msg, timeout) { note = msg; } function fivemin() { print("fivemin"); - var s = ' B'; - var bat = E.getBattery(); - if (bat < 25) { - if (is_active) - doBuzz(toMorse(s)); - show("Bat "+bat+"%", 60); - } + let s = ' B'; try { Bangle.getPressure().then((x) => { cur_altitude = x.altitude; cur_temperature = x.temperature; }, @@ -470,8 +497,9 @@ function drawDot(h, d, s) { g.fillCircle(x,y, 10); } function drawBackground() { - var acc = Bangle.getAccel(); + let acc = Bangle.getAccel(); is_level = (acc.z < -0.95); + Bangle.setCompassPower(!!is_level, "sixths"); if (is_level) { let obj = Bangle.getCompass(); if (obj) { @@ -502,13 +530,6 @@ function drawTime(now) { dot = "."; g.drawString(now.getHours() + dot + add0(now.getMinutes()), W, 90); } -function adjPressure(a) { - var o = Bangle.getOptions(); - print(o); - o.seaLevelPressure = o.seaLevelPressure * m + a; - Bangle.setOptions(o); - var avr = []; -} function draw() { if (disp_mode == 2) { draw_all(); @@ -525,7 +546,11 @@ function draw() { if (gps_on) { msg = gpsHandle(); } else { - msg = note; + let o = Bangle.getOptions(); + msg = o.seaLevelPressure.toFixed(1) + "hPa"; + if (note != "") { + msg = note; + } } drawBackground(); @@ -551,7 +576,7 @@ function draw() { let alt_adjust = cur_altitude - rest_altitude; let abs = Math.abs(alt_adjust); print("adj", alt_adjust); - var o = Bangle.getOptions(); + let o = Bangle.getOptions(); if (abs > 10 && abs < 150) { let a = 0.01; // FIXME: draw is called often compared to alt reading @@ -588,16 +613,16 @@ function draw_all() { let now = new Date(); g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40); - var acc = Bangle.getAccel(); + let acc = Bangle.getAccel(); let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y; let diff = ax * ax + ay * ay + az * az; diff = diff * 3; if (diff > 1) diff = 1; - var co = Bangle.getCompass(); - var step = Bangle.getStepCount(); - var bat = E.getBattery(); + let co = Bangle.getCompass(); + let step = Bangle.getStepCount(); + let bat = E.getBattery(); Bangle.getPressure().then((x) => { alt = x.altitude; temp = x.temperature; }, print); @@ -618,7 +643,7 @@ function draw_all() { g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dz / 400, 5); } if (1) { - h = co.heading / 360 * 2 * Math.PI; + let h = co.heading / 360 * 2 * Math.PI; g.setColor(0, 0, 0.5); g.fillCircle(cx + sc * Math.sin(h), cy + sc * Math.cos(h), 5); } @@ -635,10 +660,10 @@ function draw_all() { queueDraw(); } function accelTask() { - var tm = 100; - var acc = Bangle.getAccel(); - var en = !Bangle.isLocked(); - var msg; + let tm = 100; + let acc = Bangle.getAccel(); + let en = !Bangle.isLocked(); + let msg = ""; if (en && acc.z < -0.95) { msg = "Level"; doBuzz(".-.."); @@ -654,14 +679,14 @@ function accelTask() { doBuzz("..-"); tm = 3000; } - + print(msg); setTimeout(accelTask, tm); } function buzzTask() { if (buzz != "") { - var now = buzz[0]; + let now = buzz[0]; buzz = buzz.substring(1); - var dot = 100; + let dot = 100; if (now == " ") { setTimeout(buzzTask, 300); } else if (now == ".") { @@ -675,13 +700,14 @@ function buzzTask() { } else print("Unknown character -- ", now, buzz); } } +var last_acc; function aliveTask() { function cmp(s) { let d = acc[s] - last_acc[s]; return d < -0.03 || d > 0.03; } // HRM seems to detect hand quite nicely - acc = Bangle.getAccel(); + let acc = Bangle.getAccel(); is_active = false; if (cmp("x") || cmp("y") || cmp("z")) { print("active"); @@ -700,7 +726,8 @@ function lockHandler(locked) { } function queueDraw() { - if (getTime() - last_unlocked > 5*60) + let next; + if (getTime() - last_unlocked > 3*60) next = 60000; else next = 1000; @@ -716,8 +743,6 @@ function start() { Bangle.on("drag", touchHandler); Bangle.on("lock", lockHandler); - if (0) - Bangle.on("accel", accelHandler); if (0) { Bangle.setCompassPower(1, "sixths"); Bangle.setBarometerPower(1, "sixths"); @@ -729,8 +754,10 @@ function start() { } draw(); + loadWPs(); buzzTask(); - //accelTask(); + if (0) + accelTask(); if (1) { last_acc = Bangle.getAccel(); diff --git a/apps/skyspy/ChangeLog b/apps/skyspy/ChangeLog index 79da4daf2..15a83ceeb 100644 --- a/apps/skyspy/ChangeLog +++ b/apps/skyspy/ChangeLog @@ -1,2 +1,3 @@ 0.01: attempt to import 0.02: Minor code improvements +0.03: big rewrite, adding time-adjust and altitude-adjust functionality diff --git a/apps/skyspy/metadata.json b/apps/skyspy/metadata.json index 07bc9280a..d8e9d1356 100644 --- a/apps/skyspy/metadata.json +++ b/apps/skyspy/metadata.json @@ -1,6 +1,6 @@ { "id": "skyspy", "name": "Sky Spy", - "version": "0.02", + "version": "0.03", "description": "Application for debugging GPS problems", "icon": "app.png", "readme": "README.md", diff --git a/apps/skyspy/skyspy.app.js b/apps/skyspy/skyspy.app.js index ff6e29712..a3a0d8776 100644 --- a/apps/skyspy/skyspy.app.js +++ b/apps/skyspy/skyspy.app.js @@ -1,20 +1,121 @@ /* Sky spy */ -/* 0 .. DD.ddddd - 1 .. DD MM.mmm' - 2 .. DD MM'ss" -*/ -var mode = 1; + +/* fmt library v0.1 */ +let fmt = { + icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3", + icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + + /* 0 .. DD.ddddd + 1 .. DD MM.mmm' + 2 .. DD MM'ss" + */ + geo_mode : 1, + + init: function() {}, + fmtDist: function(km) { return km.toFixed(1) + this.icon_km; }, + fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); }, + fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; }, + fmtTimeDiff: function(d) { + if (d < 180) + return ""+d.toFixed(0); + d = d/60; + return ""+d.toFixed(0)+"m"; + }, + fmtAngle: function(x) { + switch (this.geo_mode) { + case 0: + return "" + x; + case 1: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + return "" + d + " " + m.toFixed(3) + "'"; + } + case 2: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + let mf = Math.floor(m); + let s = m - mf; + s = s*60; + return "" + d + " " + mf + "'" + s.toFixed(0) + '"'; + } + } + return "bad mode?"; + }, + fmtPos: function(pos) { + let x = pos.lat; + let c = "N"; + if (x<0) { + c = "S"; + x = -x; + } + let s = c+this.fmtAngle(pos.lat) + "\n"; + c = "E"; + if (x<0) { + c = "W"; + x = -x; + } + return s + c + this.fmtAngle(pos.lon); + }, +}; + +/* gps library v0.1 */ +let gps = { + emulator: -1, + init: function(x) { + this.emulator = (process.env.BOARD=="EMSCRIPTEN" + || process.env.BOARD=="EMSCRIPTEN2")?1:0; + }, + state: {}, + on_gps: function(f) { + let fix = this.getGPSFix(); + f(fix); + + /* + "lat": number, // Latitude in degrees + "lon": number, // Longitude in degrees + "alt": number, // altitude in M + "speed": number, // Speed in kph + "course": number, // Course in degrees + "time": Date, // Current Time (or undefined if not known) + "satellites": 7, // Number of satellites + "fix": 1 // NMEA Fix state - 0 is no fix + "hdop": number, // Horizontal Dilution of Precision + */ + this.state.timeout = setTimeout(this.on_gps, 1000, f); + }, + off_gps: function() { + clearTimeout(this.state.timeout); + }, + getGPSFix: function() { + if (!this.emulator) + return Bangle.getGPSFix(); + let fix = {}; + fix.fix = 1; + fix.lat = 50; + fix.lon = 14; + fix.alt = 200; + fix.speed = 5; + fix.course = 30; + fix.time = Date(); + fix.satellites = 5; + fix.hdop = 12; + return fix; + } +}; + var display = 0; - var debug = 0; - -var cancel_gps, gps_start; +var gps_start; var cur_altitude; - var wi = 24; var h = 176-wi, w = 176; - var fix; +var adj_time = 0, adj_alt = 0; function radA(p) { return p*(Math.PI*2); } function radD(d) { return d*(h/2); } @@ -27,26 +128,7 @@ function radY(p, d) { return h/2 - Math.cos(a)*radD(d) + wi; } -function format(x) { - switch (mode) { - case 0: - return "" + x; - case 1: - d = Math.floor(x); - m = x - d; - m = m*60; - return "" + d + " " + m.toFixed(3) + "'"; - case 2: - d = Math.floor(x); - m = x - d; - m = m*60; - mf = Math.floor(m); - s = m - mf; - s = s*60; - return "" + d + " " + mf + "'" + s.toFixed(0) + '"'; - } -} -var qalt = -1; +var qalt = -1, min_dalt, max_dalt, step; function resetAlt() { min_dalt = 9999; max_dalt = -9999; step = 0; } @@ -64,65 +146,96 @@ function calcAlt(alt, cur_altitude) { return ddalt; } function updateGps() { - let /*have = false,*/ lat = "lat", lon = "lon", alt = "alt", - speed = "speed", hdop = "hdop"; // balt = "balt"; + let lat = "lat ", alt = "?", + speed = "speed ", hdop = "?", adelta = "adelta ", + tdelta = "tdelta "; - if (cancel_gps) - return; - fix = Bangle.getGPSFix(); + fix = gps.getGPSFix(); + if (adj_time) { + print("Adjusting time"); + setTime(fix.time.getTime()/1000); + adj_time = 0; + } + if (adj_alt) { + print("Adjust altitude"); + if (qalt < 5) { + let rest_altitude = fix.alt; + let alt_adjust = cur_altitude - rest_altitude; + let abs = Math.abs(alt_adjust); + print("adj", alt_adjust); + let o = Bangle.getOptions(); + if (abs > 10 && abs < 150) { + let a = 0.01; + // FIXME: draw is called often compared to alt reading + if (cur_altitude > rest_altitude) + a = -a; + o.seaLevelPressure = o.seaLevelPressure + a; + Bangle.setOptions(o); + } + msg = o.seaLevelPressure.toFixed(1) + "hPa"; + print(msg); + } + } try { Bangle.getPressure().then((x) => { cur_altitude = x.altitude; }, print); } catch (e) { - print("Altimeter error", e); + //print("Altimeter error", e); } - speed = getTime() - gps_start; + //print(fix); + if (fix && fix.time) { + tdelta = "" + (getTime() - fix.time.getTime()/1000).toFixed(0); + } if (fix && fix.fix && fix.lat) { - lat = "" + format(fix.lat); - lon = "" + format(fix.lon); - alt = "" + fix.alt.toFixed(1); + lat = "" + fmt.fmtPos(fix); + alt = "" + fix.alt.toFixed(0); + adelta = "" + (cur_altitude - fix.alt).toFixed(0); speed = "" + fix.speed.toFixed(1); - hdop = "" + fix.hdop.toFixed(1); - //have = true; + hdop = "" + fix.hdop.toFixed(0); + } else { + lat = "NO FIX\n" + + "" + (getTime() - gps_start).toFixed(0) + "s " + + sats_used + "/" + snum; + if (cur_altitude) + adelta = "" + cur_altitude.toFixed(0); } let ddalt = calcAlt(alt, cur_altitude); - if (display == 1) - g.reset().setFont("Vector", 20) - .setColor(1,1,1) - .fillRect(0, wi, 176, 176) - .setColor(0,0,0) - .drawString("Acquiring GPS", 0, 30) - .drawString(lat, 0, 50) - .drawString(lon, 0, 70) - .drawString("alt "+alt, 0, 90) - .drawString("speed "+speed, 0, 110) - .drawString("hdop "+hdop, 0, 130) - .drawString("balt" + cur_altitude, 0, 150); - + let msg = ""; + if (display == 1) { + msg = lat + + "\ne" + hdop + "m "+tdelta+"s\n" + + speed + "km/h\n"+ alt + "m+" + adelta + "\nmsghere"; + } if (display == 2) { - g.reset().setFont("Vector", 20) - .setColor(1,1,1) - .fillRect(0, wi, 176, 176) - .setColor(0,0,0) - .drawString("GPS status", 0, 30) - .drawString("speed "+speed, 0, 50) - .drawString("hdop "+hdop, 0, 70) - .drawString("dd "+qalt.toFixed(0) + " (" + ddalt.toFixed(0) + ")", 0, 90) - .drawString("alt "+alt, 0, 110) - .drawString("balt " + cur_altitude, 0, 130) - .drawString(step, 0, 150); + /* qalt is altitude quality estimate -- over ten seconds, + computes differences between GPS and barometric altitude. + The lower the better. + + ddalt is just a debugging -- same estimate, but without + waiting 10 seconds, so will be always optimistic at start + of the cycle */ + msg = speed + "km/h\n" + + "e"+hdop + "m" + +"\ndd "+qalt.toFixed(0) + "\n(" + step + "/" + ddalt.toFixed(0) + ")" + + "\n"+alt + "m+" + adelta; + } step++; if (step == 10) { qalt = max_dalt - min_dalt; resetAlt(); } + if (display > 0) { + g.reset().setFont("Vector", 31) + .setColor(1,1,1) + .fillRect(0, wi, 176, 176) + .setColor(0,0,0) + .drawString(msg, 3, 25); } - if (debug > 0) print(fix); setTimeout(updateGps, 1000); @@ -184,7 +297,7 @@ function drawSats(sats) { var sats = []; var snum = 0; -//var sats_receiving = 0; +var sats_used = 0; function parseRaw(msg, lost) { if (lost) @@ -199,6 +312,7 @@ function parseRaw(msg, lost) { if (s[2] == "1") { snum = 0; sats = []; + sats_used = 0; } let view = 1 * s[3]; @@ -217,6 +331,8 @@ function parseRaw(msg, lost) { sat.ele = 1*s[i++]; sat.azi = 1*s[i++]; sat.snr = s[i++]; + if (sat.snr != "") + sats_used++; if (debug > 0) print(" ", sat); sats[snum++] = sat; @@ -231,30 +347,80 @@ function parseRaw(msg, lost) { } } -function stopGps() { - cancel_gps=true; - Bangle.setGPSPower(0, "skyspy"); -} - function markGps() { - cancel_gps = false; Bangle.setGPSPower(1, "skyspy"); Bangle.on('GPS-raw', parseRaw); gps_start = getTime(); updateGps(); } - -function onSwipe(dir) { - display = display + 1; - if (display == 3) - display = 0; +function drawMsg(msg) { + g.reset().setFont("Vector", 35) + .setColor(1,1,1) + .fillRect(0, wi, 176, 176) + .setColor(0,0,0) + .drawString(msg, 5, 30); +} +function drawBusy() { + drawMsg("\n.oO busy"); } +var numScreens = 3; + +function nextScreen() { + display = display + 1; + if (display == numScreens) + display = 0; + drawBusy(); +} + +function prevScreen() { + display = display - 1; + if (display < 0) + display = numScreens - 1; + drawBusy(); +} + +function onSwipe(dir) { + nextScreen(); +} + +var last_b = 0; +function touchHandler(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + if (d.b != 1 || last_b != 0) { + last_b = d.b; + return; + } + last_b = d.b; + + if ((xh/2) && (yw/2)) + prevScreen(); + if ((x>h/2) && (y>w/2)) + nextScreen(); +} + +gps.init(); +fmt.init(); + +Bangle.on("drag", touchHandler); Bangle.setUI({ mode : "custom", swipe : onSwipe, clock : 0 }); + Bangle.loadWidgets(); Bangle.drawWidgets(); +drawBusy(); markGps(); diff --git a/apps/slopeclockpp/ChangeLog b/apps/slopeclockpp/ChangeLog index 39f837386..43b457d4e 100644 --- a/apps/slopeclockpp/ChangeLog +++ b/apps/slopeclockpp/ChangeLog @@ -10,4 +10,5 @@ 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 -0.10: Option to hide widgets, tweak top widget width to avoid overlap with hour text at 9am \ No newline at end of file +0.10: Option to hide widgets, tweak top widget width to avoid overlap with hour text at 9am +0.11: Avoid rendering clkinfo in the same colour as the background diff --git a/apps/slopeclockpp/app.js b/apps/slopeclockpp/app.js index 5b1d898d1..df732c1db 100644 --- a/apps/slopeclockpp/app.js +++ b/apps/slopeclockpp/app.js @@ -86,6 +86,7 @@ let draw = function() { let isAnimIn = true; let animInterval; +let minuteX; // Draw *just* the minute image let drawMinute = function() { var yo = slopeBorder + offsy + y - 2*slope*minuteX/R.w; @@ -128,9 +129,9 @@ let clockInfoDraw = (itm, info, options) => { let texty = options.y+41; // set a cliprect to stop us drawing outside our box g.reset().setClipRect(options.x, options.y, options.x+options.w-1, options.y+options.h-1); - g.setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty-15, options.x+options.w-2, texty); + g.setFont("6x15").setBgColor(options.bg).clearRect(options.x, texty-15, options.x+options.w-2, texty); - if (options.focus) g.setColor(options.hl); + g.setColor(options.focus ? options.hl : options.fg); if (options.x < g.getWidth()/2) { // left align let x = options.x+2; if (info.img) g.clearRect(x, options.y, x+23, options.y+23).drawImage(info.img, x, options.y); @@ -150,7 +151,7 @@ let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { // t }); let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { // bottom left 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 + draw : clockInfoDraw, bg : bgColor, fg : g.theme.bg, hl : (g.theme.fg===g.toColor(bgColor))?"#f00"/*red*/:g.theme.fg }); // Show launcher when middle button pressed @@ -175,4 +176,4 @@ Bangle.loadWidgets(); if (settings.hideWidgets) require("widget_utils").swipeOn(); else setTimeout(Bangle.drawWidgets,0); draw(); -} \ No newline at end of file +} diff --git a/apps/slopeclockpp/metadata.json b/apps/slopeclockpp/metadata.json index 086b8148f..00e0b0a77 100644 --- a/apps/slopeclockpp/metadata.json +++ b/apps/slopeclockpp/metadata.json @@ -1,6 +1,6 @@ { "id": "slopeclockpp", "name": "Slope Clock ++", - "version":"0.10", + "version":"0.11", "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"}], diff --git a/apps/slpquiet/ChangeLog b/apps/slpquiet/ChangeLog index 5560f00bc..3a4fdc392 100644 --- a/apps/slpquiet/ChangeLog +++ b/apps/slpquiet/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: added check if settings file exists and create if missing diff --git a/apps/slpquiet/README.md b/apps/slpquiet/README.md index a1f2e94e1..673deef5d 100644 --- a/apps/slpquiet/README.md +++ b/apps/slpquiet/README.md @@ -25,3 +25,9 @@ It reuses the widget from [Quiet Mode Schedule and Widget](https://github.com/es -optimization of code (and check if needed) -feedback is always welcome + +#### Attributions +The app icon is downloaded from [https://icons8.com](https://icons8.com). + +#### License +[MIT License](LICENSE) \ No newline at end of file diff --git a/apps/slpquiet/app.js b/apps/slpquiet/app.js index 1de61498b..0b03ba4fd 100644 --- a/apps/slpquiet/app.js +++ b/apps/slpquiet/app.js @@ -1,5 +1,16 @@ const SETTINGS_FILE = "quietSwitch.json"; const storage = require("Storage"); + +//check if settings file exists and create if missing +if (storage.read(SETTINGS_FILE)=== undefined) { + print("data file not existing, using defaults"); + let saved = { + quietWhenSleep: 0, //off + quietMode: 1, //alerts + }; + storage.writeJSON(SETTINGS_FILE,saved); +} + let saved = storage.readJSON(SETTINGS_FILE, 1) || {}; // Main menu diff --git a/apps/slpquiet/metadata.json b/apps/slpquiet/metadata.json index 342dbc5d5..79fe857b1 100644 --- a/apps/slpquiet/metadata.json +++ b/apps/slpquiet/metadata.json @@ -1,7 +1,7 @@ { "id": "slpquiet", "name": "Sleep Quiet (activate quiet mode when asleep)", "shortName":"Sleep Quiet", - "version":"0.01", + "version":"0.02", "description": "Set Quiet mode (or alarms only mode), when the sleep tracking app detects sleep (each 10 min evaluated)", "icon": "app.png", "tags": "tool,widget", diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 2c073ff43..c3f069428 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -7,3 +7,4 @@ 0.07: Update clock_info to avoid a redraw 0.08: Timer ClockInfo now updates once a minute 0.09: Timer ClockInfo resets to timer menu when blurred +0.10: Timer ClockInfo now uses +- icons, and changes timer from 'T-5 min' to just '5 min' to aid readability \ No newline at end of file diff --git a/apps/smpltmr/clkinfo.js b/apps/smpltmr/clkinfo.js index a7a6bf71b..6fc2cd265 100644 --- a/apps/smpltmr/clkinfo.js +++ b/apps/smpltmr/clkinfo.js @@ -28,7 +28,7 @@ var min = getAlarmMinutes(); if(min < 0) return "OFF"; - return "T-" + String(min)+ " min"; + return min + " min"; } function increaseAlarm(t){ @@ -80,7 +80,7 @@ offsets.forEach((o, i) => { smpltmrItems.items = smpltmrItems.items.concat({ name: null, - get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: smpltmrItems.img }), + get: () => ({ text: (o > 0 ? "+" : "") + o + " min", img: (o>0)?atob("GBiBAAB+AAB+AAAYAAAYAAB+AA3/sA+B8A4AcAwAMBgYGBgYGDAYDDAYDDH/jDH/jDAYDDAYDBgYGBgYGAwAMA4AcAeB4AH/gAB+AA=="):atob("GBiBAAB+AAB+AAAYAAAYAAB+AA3/sA+B8A4AcAwAMBgAGBgAGDAADDAADDH/jDH/jDAADDAADBgAGBgAGAwAMA4AcAeB4AH/gAB+AA==") }), show: function() { }, hide: function() { }, blur: restoreMainItem, diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 98affcfe6..2f33f07b9 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,7 +2,7 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.09", + "version": "0.10", "description": "A very simple app to start a timer.", "icon": "app.png", "tags": "tool,alarm,timer,clkinfo", diff --git a/apps/splitsw/ChangeLog b/apps/splitsw/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/splitsw/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/splitsw/README.md b/apps/splitsw/README.md new file mode 100644 index 000000000..5f8fbd54b --- /dev/null +++ b/apps/splitsw/README.md @@ -0,0 +1,30 @@ +# Stopwatch with split times + +A basic stopwatch with support for split times. + +![](screenshot.png) + +## Features + +Implemented: + +- Start stopwatch +- Stop stopwatch +- Show split times +- Reset stopwatch +- Keep display unlocked + +Future: + +- Save state and restore running stopwatch when it reopens +- View all split times +- Duplicate Start/Stop and/or Reset/Split button on the physical button +- Settings, e.g. what the physical button does, and whether to keep the backlight on + +## Creator + +James Taylor ([jt-nti](https://github.com/jt-nti)) + +## Icons + +The same icons as apps/stopwatch! diff --git a/apps/splitsw/app-icon.js b/apps/splitsw/app-icon.js new file mode 100644 index 000000000..32281b7ab --- /dev/null +++ b/apps/splitsw/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///vvvvEF/muH+cDHgPABf4AElWoKhILClALH1WqAQIWHBYIABwAKEgQKD1WgBYkK1X1r4XHlWtqtVvQLG1XVBYNXHYsC1YJBBoPqC4kKEQILCvQ7EhW1BYdeBYkqytVqwCCQwkqCgILCq4LFIoILCqoLEIwIsBGQJIBBZ+pA4Na0oDBtQLGvSFCBaYjIHYR3CI5AADBYhrCAAaDHAASDGQASGCBYizCAASzFZYQACZYrjCIwb7QHgIkCvQ6EGAWq+tf1QuEGAWqAAQuFEgQKBEQw9DHIwAuA=")) diff --git a/apps/splitsw/app.js b/apps/splitsw/app.js new file mode 100644 index 000000000..5d70b471d --- /dev/null +++ b/apps/splitsw/app.js @@ -0,0 +1,167 @@ +Bangle.loadWidgets(); +g.clear(true); +Bangle.drawWidgets(); + +Bangle.setLCDTimeout(undefined); + +let renderIntervalId; +let startTime; +let stopTime; +let subtotal; +let currentSplitNumber; +let splitStartTime; +let splitSubtotal; + +var Layout = require("Layout"); +var layout = new Layout( { + type:"v", c: [ + {type:"txt", pad:4, font:"20%", label:"", id:"time", fillx:1}, + {type:"h", c: [ + {type:"btn", pad:4, font:"6x8:2", label:"Start", id:"startStop", cb: l=>startStop() }, + {type:"btn", pad:4, font:"6x8:2", label:"Reset", id:"resetSplit", cb: l=>resetSplit() } + ]}, + { + type:"v", pad:4, c: [ + {type:"txt", font:"6x8:2", label:"", id:"split", fillx:1}, + {type:"txt", font:"6x8:2", label:"", id:"prevSplit", fillx:1}, + ]}, + ] +}, { + lazy: true, + back: load, +}); + +// TODO The code in this function appears in various apps so it might be +// nice to add something to the time_utils module. (There is already a +// formatDuration function but that doesn't quite work the same way.) +const getTime = function(milliseconds) { + let hrs = Math.floor(milliseconds/3600000); + let mins = Math.floor(milliseconds/60000)%60; + let secs = Math.floor(milliseconds/1000)%60; + let tnth = Math.floor(milliseconds/100)%10; + let text; + + if (hrs === 0) { + text = ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2) + "." + tnth; + } else { + text = ("0"+hrs) + ":" + ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2); + } + + return text; +}; + +const renderIntervalCallback = function() { + if (startTime === undefined) { + return; + } + + updateStopwatch(); +}; + +const buzz = function() { + Bangle.buzz(50, 0.5); +}; + +const startStop = function() { + buzz(); + + if (layout.startStop.label === "Start") { + start(); + } else { + stop(); + } +}; + +const start = function() { + if (stopTime === undefined) { + startTime = Date.now(); + splitStartTime = startTime; + subtotal = 0; + splitSubtotal = 0; + currentSplitNumber = 1; + } else { + subtotal += stopTime - startTime; + splitSubtotal += stopTime - splitStartTime; + startTime = Date.now(); + splitStartTime = startTime; + stopTime = undefined; + } + + layout.startStop.label = "Stop"; + layout.resetSplit.label = "Split"; + updateStopwatch(); + + renderIntervalId = setInterval(renderIntervalCallback, 100); +}; + +const stop = function() { + stopTime = Date.now(); + + layout.startStop.label = "Start"; + layout.resetSplit.label = "Reset"; + updateStopwatch(); + + if (renderIntervalId !== undefined) { + clearInterval(renderIntervalId); + renderIntervalId = undefined; + } +}; + +const resetSplit = function() { + buzz(); + + if (layout.resetSplit.label === "Reset") { + reset(); + } else { + split(); + } +}; + +const reset = function() { + layout.startStop.label = "Start"; + layout.resetSplit.label = "Reset"; + layout.split.label = ""; + layout.prevSplit.label = ""; + + startTime = undefined; + stopTime = undefined; + subtotal = 0; + currentSplitNumber = 1; + splitStartTime = undefined; + splitSubtotal = 0; + + updateStopwatch(); +}; + +const split = function() { + const splitTime = Date.now() - splitStartTime + splitSubtotal; + layout.prevSplit.label = "#" + currentSplitNumber + " " + getTime(splitTime); + + splitStartTime = Date.now(); + splitSubtotal = 0; + currentSplitNumber++; + + updateStopwatch(); +}; + +const updateStopwatch = function() { + let elapsedTime; + + if (startTime === undefined) { + elapsedTime = 0; + } else { + elapsedTime = Date.now() - startTime + subtotal; + } + + layout.time.label = getTime(elapsedTime); + + if (splitStartTime !== undefined) { + const splitTime = Date.now() - splitStartTime + splitSubtotal; + layout.split.label = "#" + currentSplitNumber + " " + getTime(splitTime); + } + + layout.render(); + // layout.debug(); +}; + +updateStopwatch(); diff --git a/apps/splitsw/app.png b/apps/splitsw/app.png new file mode 100644 index 000000000..92ffe73b7 Binary files /dev/null and b/apps/splitsw/app.png differ diff --git a/apps/splitsw/metadata.json b/apps/splitsw/metadata.json new file mode 100644 index 000000000..d5ae19347 --- /dev/null +++ b/apps/splitsw/metadata.json @@ -0,0 +1,16 @@ +{ "id": "splitsw", + "name": "Stopwatch with split times", + "shortName":"Stopwatch", + "version":"0.01", + "description": "A basic stopwatch with support for split times", + "icon": "app.png", + "tags": "tool,outdoors,health", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{ "url": "screenshot.png" }], + "allow_emulator": true, + "storage": [ + {"name":"splitsw.app.js","url":"app.js"}, + {"name":"splitsw.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/splitsw/screenshot.png b/apps/splitsw/screenshot.png new file mode 100644 index 000000000..184bc4fbc Binary files /dev/null and b/apps/splitsw/screenshot.png differ diff --git a/apps/supaclk/ChangeLog b/apps/supaclk/ChangeLog index 5560f00bc..3ffa2a6cd 100644 --- a/apps/supaclk/ChangeLog +++ b/apps/supaclk/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Adjusted main font size to fit nicely even at 8pm, minor tweaks diff --git a/apps/supaclk/app.js b/apps/supaclk/app.js index f0bf32ae5..73d75fcd6 100644 --- a/apps/supaclk/app.js +++ b/apps/supaclk/app.js @@ -1,5 +1,5 @@ { // must be inside our own scope here so that when we are unloaded everything disappears - // we also define functions using 'let fn = function() {..}' for the same reason. function decls are globalj + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global let removeHasNotRun = true; let drawTimeout; @@ -37,19 +37,19 @@ let drawSplashScreen = function (frame, total) { // for fast startup-feeling, draw the splash screen once directly once g.clear() -drawSplashScreen(0, 20); +drawSplashScreen(0, 15); let splashScreen = function () { g.clearRect(R.x,R.y, R.x2, R.y2); return new Promise((resolve, reject) => { let frame = 0; function tick() { - if (removeHasNotRun) drawSplashScreen(frame, 20); + if (removeHasNotRun) drawSplashScreen(frame, 15); frame += 1; if (!removeHasNotRun) { reject(); - } else if (frame < 20) { - setTimeout(tick, 50); + } else if (frame < 15) { + setTimeout(tick, 30); } else { resolve(); } @@ -62,13 +62,13 @@ let splashScreen = function () { Graphics.prototype.setFontPlayfairDisplay = function() { // https://www.espruino.com/Font+Converter // - // 60pt, 2bpp, Numeric - // Actual height 62 (67 - 6) + // Actual height 58 (61 - 4) + // 1 BPP return this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('AD8/A40B/4IGh/8DI/4BA3/8AHFg//wAIFj/+ES8DEQ5FIj4ZGBAKdzhwIHNA8f4BoGUpBwGg/wRQ7HHPA0BFJA6HJY9/FI46GgLWHMiF/Mi8Dbo8MbuYALv6VGg//BA1/BAwQB/51Fn4IBNokBA4P/UAkPBASYEFQIABDI7DEDIY9EDIY9DDIY0EJoICDJoYNCFYgnDAYcDJQUBMAbKDBgcAuADCgw8DBgcYBg0AkAMGgAVDsADCgQiDLYYVDg4ZDCocOBgYiDnwDGNocHNAh/CRYy3Gj6uLcYgZHW4f8BAYrDDIjaDA4Y0DGYjJBbIoIDFQhGDCAoRBCAwAqcYcEBAbNDiDNHoCLDZoUDEQ8MBAccAYVwOAigCOQbaBToylDPYicC//waI4iVegYiDdYYiEdYYiEHgYiECAZFEDobbGRYoADRYgADVwgADVwYAjcYUDaoQAB8ACBjxcEAYX4BAc4DIQUCSgJtCj4iDhwDC/wZGg4iDgIeCn4iDGYS6BEQc8agQiEVQV/EQcDHgMDv4iDh4CBnIiEnwqB84iDgIeBj8fEQcH+AKBEQkf+E/8IiEv7qBg4iEfYQiFBAPgEQoIBNAs/DIIiEgAZCEQkDBAI3BRYn//AiFFYPAEQs/AoIiED4KVBEQg0BwAiFdASuFCYYiEJAYiEPwYHGAFsDAYUOBAcIAYVwBAdgCgRtDgIECjgQDgwDCMgkYVwSHEEQQZBCwQnDUgM4IIkD4AkDvACBh4WDgINBgE8gEMBoYLBEQMwBoY8B4AWCBoTfBwEPEQICCb4MAZ4V+EQX/h/cGwLSCg///98gE/HgUf//9/gMBNYU///nCYLsDAoOfAQJiCeIP8v4IBCAUP//wA4P8BAQrBwYZEJwIyC/6hDDIITBDIZXBwA/BDIZbCGYgRBCYRNDCYYqEDgoAXgYIHd4IAGXwQAEZgIIGYYIqGOAYADj4iH/4iGVAIQGuYiGgePEQ0cdQaVD4AiGhxFHuEPEQsDYAIiFjBOBFQwiGhxXBEQtwgAiFZ4IACCQbyBAAQSCdIIADQAgACGod/EQ0HDIgiCFQgiCFQoiCDIp7GaI5oGH4TRGFwIZHEQ9/EQwZBEQwAcggIHiAIHoA7EwBzBVwn+AgMMaAfAmAFBAQRlCDIMAVwfwgyiCEQQZBjAEBjgiDgHgAoNwEQcDHgQlCEQMOAgMeEQk4AgN4EQ0DEoQiCIQMfDIQiBh5rBXAYiBn7bEEQX/PgYiDfgJ8CEQYIBaQYiCBAJ5CEQYZEEQIpB//4EQkHDIjxCWAhaBEQIrBGYd/LYN/JoYAD/5nDbYiBCAAgQGAFReBHYp4CLwZ6CBAysCDQp6BRQghDAAJ5DXoQABDIaIBWwoqDRYgqDbIoADGgTFCGgoZBD4MHFYYeBKgMBcQQMBgZSCMAUf4EYBAQiCDoNgCwRNC8AZCgEMDIQEDgEgAQN4gE4EQkDKIIwChwCDEgIFBIoQXBh5uBj5RCDIK3CNAQ/CRYpUBSoaLCDgKEDHgSeEQQRUCcYpZCaIzbDTgbKEfog0DA4gICM4QIFFQgAih4pHn6ICAAn/VwReFEQ4ZHn5uFEQTCBESIMEEQd/TwYiCQgIVGYQIVCEQSwCLYQiCaYR1CEQLKFEQSFBEQzkCLYQiBaQRFFfwoiBAIPwgxoEj7iFEQJ7G//HV4ogBnzRHYA0/IIbRMCA8PZA8/A40ACA4AUvDlKAAl/BAcHAgKlBRgYNCcIiBBX4a+Cj4WBX4ThCv4WBDILhEQQICBGgQZBVwLQEDIP/z/gv6XBgLwCBwMfFYK1BAAIWBjySCFAk4AQPgIwQFBsA8BCQQoCEQMMBARdBgQTBkA+CBwMIBAINBGgYOBEQJHBMwQiDQgcGH4aBBBAMYBAJCBGgIDBH4MD/h7C/EPEQKZCMQQtCLwSFCRYnhPgS3CAgKoCVwi/DPgQFB8CXCDIQFBXQbrCj4VBSwiyDRoYAEv4zCBApnBAAp4CAExZCAApeECAgIGSwJWFSYTJBAAbADDIyXBMQYZCQQVgDIg0BgKRBDIY0BhwHBgIQCFYMwJogrBgKnCn4DBv+Ag48CIQU+gE8HgQuCnEAWAUcCgXgg4NCJARDBDIRIDg0B+D+CDIUwh48CEQfARod8DIUPQgaRCnL+DQQJrCDIZoDTwk/BAYZCRYYZERYf/JoSuE/5bCFYjSEW4aBCGgraHcZAqDBAYQFEYQHFAHh2Cj5XDg5UBS4JVETIMHUocARAU/NIcHO4SUEj4WBWILIECwKxBZAgiCW4YiIh4iDJwZFIv4NBh7rDNAiRkA=='))), + E.toString(require('heatshrink').decompress(atob('ADv8AwsB/4PG/+AA43gA4t/+AHFn/4A4sfGA0P/wHFg44GIBF/IA0fPL8OA40/PI5IGMA0HPA0f4BXNO40DR40PU40/Ew5FWEw5FNgJFGgAmGAEbpBJYr5BMYoHB/5UEh4HBDAgHCKogHCMogHCEAgnCEAgHDNwYHDIIcDA4QoDA4OALQK6GeggkCgYwDuADGmADCjAHGhgLGgQLGgIDCgxtDNIUDA4ZACgJEDsBAKngDCTQZdCfAkPOwMfQJZ+BWQ1/BAQHDn4HGYQYHDFAbKDFAbzEEAQGDEAYHEDAI/EDARXDADKiDhAvDIoUEVwzKDVwa+EnAbFgEeB4TGDj5xC8CJG+AHCg4HCYQaRDNQa6IA4SKEA4ZADZQZADZQZADA4ZADKAZADAAccA40GA40BUw7jEAC8gAQMMA4cwAQMOA40PA444DYQUPPIYHDPIc8A4R5DNoUPPIY0Ch66DGgUPXQcHGgLdBaQY0Bhy6DgI0Bj1/IAUBFgM+n5ADEgM/j5ADv/B/5AEZQUHIAbKCwZADHoP/wJAEfIRAEA4RADgAHB4BAEv4HBIAgwB4BAEGAPAIAgwB4EDIAYgB4AzBA4aSBA4L7FDQQHEB4JADACkCeYoiBdYoHBHIQPDgA5CB4cAsAHGCgQHEjACBjkAhgEDcALqBAgICCOAMHAgYFBwEDHoIKCgaIBA4QCBgfggJFBg4CBgPwAIIMCDAP8gK4BA4d/wfuC4LLCn//++Ag7LCaQP7/EfA4UH//5doLTCW4P8A4i3B/giBbYYEBEQIHDn/+eoIHDj/+EQLrDg/+EQIHDgIUBv77EnhLCbApLBA4oaCABqjCA4rhCA4iQCA4iQCHAiICA4iACA4hAGgxQGh1/IAsOn5AFh0fIAsMh5AFjhAGjkDIAscQI0YYoIHEnCqBIAgHBIArhBdYgHEFIbIBAAQHHFIQHEFIQGD/5qCA4hqCC4hqFQI0AQIzCIQIzCIg7CGgJXDACsBBA4hDgZXCoAGCMwdgPIX8YYMAmB5C4EIZwaxB8EMewR+C+EGAgMOPwX4g5jCAQX8gY9BA4UD/0BW4MHBQJuBgBIBgbCDwFwKYhABngXBA4RABj4HBWYRABh/gXYZACaQhACA4K7CIAQHEIAQHEIAU//7LDIAMfA4vgGAIHD//wGAIPEHgIHEAAU/eY43DAAcgeS4AMJ4KHCAARfFIwR4BXARZCTAhpCAAIYEA4SMBA4zJCU4QABHIQHEVIgoGA4YoDIwQbBJIX/+IEBfQbBBBgQwCv8AA4UYM4UAoAEBhgCBn0AsAEBgQCBj0AmD/CA4ccKoRABh0Ah4PCFYIFBHoQCDBgJxEEQJyDA4QiBA4SiBFQSyCVQQqBYQKJDJwLSBA4b+BFATTFA44oBA4ogBWIYYDcQj8CUAQARH4IWGdAYADv4PGn4HGj4XGh5GGg5WGgZmDBYRABBYX/UAJABAYMPJgd/Z4RzDIASsBEARACSYhACYgRAEA4QsBIAV/A4ZACA4Q0BIATsCJARABRYpABBoZADaIpABQQsH/qSFOoKiFIAI8CYQguEIATbGn4HGjz4TACsPdo0fOQYMCYIJTCBgQHBNYQEB4ACBQYQEB+DZFAgLpBaIQECAQT+D//PZIcHAgP38EHHgKgBCod4A4glBjioBaAIdCgwKBIwQdCA4NgDIJOCEQMgXQJvCoEAjCyBGAQNBhhKCN4MQFQQgBM4IVBgYUBIIQVBgPwdYIYBhzpC//vDALeCFwPzLYhPCNYg2B+ASBMYRXCRgQHBFAP4PgS6Cv5nBA4kPA4IgBA4UDP4JiDAAZSBA4ojBF4QASg6iCAAZjBL4IADH4I7BA4qaBA4qACA4pAEA4RQBmAvDbgS7BA4YoBXYJvCFAS7CA4eAg5XC/+DGAIHCgYtBg/gcIUBHoXwgYEBA4c4A4UAAQUMgKbCsACBgwkCKYcCW4UAjgzCA4cPGYMDZ4TLDge/A4TICKYKTDMAQHEv4HGQIQHEPIYHDgYHC/yqDB4yyDA4ggCA4ggCegpBBdYpXBbQgAon50CToIHCGwMfIIc/AgMfKIYECh55Dj5mBKQJwDBgJrBFAQMCXoJiCBgbFBTIYMBv44Dv4MBIAkfA4MPA4cHA4MBSQoAE'))), 46, - atob("ExouGyYjJiEoISgoFQ=="), - 70|65536 + atob("EBYpGCEfIh4lHyQjEQ=="), + 65|65536 ); } @@ -95,11 +95,10 @@ let draw = function() { g.setColor(g.theme.fg) // Time const yt = R.y + 92 - 20 - 30 + 6 + 10; - const xt = R.w/2 - 5; + const xt = R.w/2; let hours = date.getHours()+''; - g.setFontAlign(1, 0).setFontPlayfairDisplay().drawString(hours, xt - 8, yt); - g.setFontAlign(0, 0).setFontPlayfairDisplay().drawString(':', xt, yt); - g.setFontAlign(-1, 0).setFontPlayfairDisplay().drawString(minutes, xt + 8, yt); + + g.setFontAlign(0, 0).setFontPlayfairDisplay().drawString(hours + ':' + minutes, xt, yt); // logo g.drawImage(supaClockImg, R.x2 - supaClockImg.width - 2, R.y + 2); // dow + date @@ -174,16 +173,15 @@ splashScreen().then(() => { draw(); Bangle.drawWidgets(); // Allocate and draw clockinfos - g.setFontAlign(1, 1).setFont('6x8').drawString('Loading Clock Info Modules...', R.x + 10, upperCI); setTimeout(() => { // delay loading of clock info, so that the clock face appears quicker g.clearRect(R.x, upperCI, R.x2, upperCI+10); // clear loading text try { clockInfoItems = require("clock_info").load(); - clockInfoMenu1 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x+1, y:upperCI, w:midX-2, h:28, draw : clockInfoDraw}); - clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+1, y:upperCI, w:midX-2, h:28, draw : clockInfoDrawR}); - clockInfoMenu3 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x+1, y:lowerCI, w:midX-2, h:28, draw : clockInfoDraw}); - clockInfoMenu4 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+1, y:lowerCI, w:midX-2, h:28, draw : clockInfoDrawR}); + clockInfoMenu1 = require("clock_info").addInteractive(clockInfoItems, { app:"supaclk", x:R.x+1, y:upperCI, w:midX-2, h:28, draw : clockInfoDraw}); + clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"supaclk", x:midX+1, y:upperCI, w:midX-2, h:28, draw : clockInfoDrawR}); + clockInfoMenu3 = require("clock_info").addInteractive(clockInfoItems, { app:"supaclk", x:R.x+1, y:lowerCI, w:midX-2, h:28, draw : clockInfoDraw}); + clockInfoMenu4 = require("clock_info").addInteractive(clockInfoItems, { app:"supaclk", x:midX+1, y:lowerCI, w:midX-2, h:28, draw : clockInfoDrawR}); } catch(err) { if ((err + '').includes('Module "clock_info" not found' )) { g.setFont('6x8').drawString('Please install\nclockinfo module!', R.x + 10, upperCI); diff --git a/apps/supaclk/metadata.json b/apps/supaclk/metadata.json index 016182545..0b7ca8f4b 100644 --- a/apps/supaclk/metadata.json +++ b/apps/supaclk/metadata.json @@ -1,6 +1,6 @@ { "id": "supaclk", "name": "SUPACLOCK Pro ULTRA", - "version": "0.01", + "version": "0.02", "description": "SUPACLOCK Pro ULTRA, with four ClockInfo areas at the bottom. Tap them and swipe up/down and left/right to toggle between different information.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}], diff --git a/apps/swiperclocklaunch/ChangeLog b/apps/swiperclocklaunch/ChangeLog index a341ee512..06c7577bc 100644 --- a/apps/swiperclocklaunch/ChangeLog +++ b/apps/swiperclocklaunch/ChangeLog @@ -4,3 +4,4 @@ 0.04: Update to work with new 'fast switch' clock->launcher functionality 0.05: Keep track of event listeners we "overwrite", and remove them at the start of setUI 0.06: Handle apps that call setUI({}) to reset +0.07: Use a more reliable method of detecting a clock diff --git a/apps/swiperclocklaunch/boot.js b/apps/swiperclocklaunch/boot.js index bb033891e..cbb2a4f61 100644 --- a/apps/swiperclocklaunch/boot.js +++ b/apps/swiperclocklaunch/boot.js @@ -3,24 +3,21 @@ var oldSwipe; Bangle.setUI = function(mode, cb) { - if (oldSwipe && oldSwipe !== Bangle.swipeHandler) + if (oldSwipe) { Bangle.removeListener("swipe", oldSwipe); + oldSwipe = undefined; + } + sui(mode,cb); - oldSwipe = Bangle.swipeHandler; - if ("object"==typeof mode) mode = mode.mode; - if (!mode) return; - - if (mode.startsWith("clock")) { + if (Bangle.CLOCK) { // clock -> launcher - Bangle.swipeHandler = dir => { if (dir<0) Bangle.showLauncher(); }; - Bangle.on("swipe", Bangle.swipeHandler); - } else { - if (global.__FILE__ && __FILE__.endsWith(".app.js") && (require("Storage").readJSON(__FILE__.slice(0,-6)+"info",1)||{}).type=="launch") { - // launcher -> clock - Bangle.swipeHandler = dir => { if (dir>0) load(); }; - Bangle.on("swipe", Bangle.swipeHandler); - } + oldSwipe = dir => { if (dir<0) Bangle.showLauncher(); }; + Bangle.on("swipe", oldSwipe); + } else if (global.__FILE__ && __FILE__.endsWith(".app.js") && (require("Storage").readJSON(__FILE__.slice(0,-6)+"info",1)||{}).type==="launch") { + // launcher -> clock + oldSwipe = dir => { if (dir>0) load(); }; + Bangle.on("swipe", oldSwipe); } }; })(); diff --git a/apps/swiperclocklaunch/metadata.json b/apps/swiperclocklaunch/metadata.json index 436d36868..d474b38e3 100644 --- a/apps/swiperclocklaunch/metadata.json +++ b/apps/swiperclocklaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "swiperclocklaunch", "name": "Swiper Clock Launch", - "version": "0.06", + "version": "0.07", "description": "Navigate between clock and launcher with Swipe action", "icon": "swiperclocklaunch.png", "type": "bootloader", diff --git a/apps/thmswtch/README.md b/apps/thmswtch/README.md index 31e6aabe9..b30686e8f 100644 --- a/apps/thmswtch/README.md +++ b/apps/thmswtch/README.md @@ -33,3 +33,9 @@ The app functionality is inspired by the [Quiet Mode Schedule and Widget](https: -transfer of configuration into settings, so app can be used as a shortcut to switch theme. -feedback is always welcome + +#### Attributions +The app icon is downloaded from [https://icons8.com](https://icons8.com). + +#### License +[MIT License](LICENSE) diff --git a/apps/timestamplog/ChangeLog b/apps/timestamplog/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/timestamplog/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/timestamplog/README.md b/apps/timestamplog/README.md new file mode 100644 index 000000000..96565e2d7 --- /dev/null +++ b/apps/timestamplog/README.md @@ -0,0 +1,55 @@ +# Timestamp Log + +Timestamp Log provides a convenient way to record points in time for later reference. Each time a button is tapped a date/time-stamped marker is logged. By default up to 30 entries can be stored at once; this can be increased up to 100 in the settings menu. + +![Timestamp Log screenshot](screenshot.png) + +## Usage and controls + +When the app starts you will see the log display. Initially the log is empty. The large button on the bottom left displays the current time and will add a date- and time-stamp when tapped. Each tap of the button adds a new entry to the bottom of the log. The small button on the bottom right opens the app settings menu. + +If the log contains more entries than can be displayed at once, swiping up and down will move through the entries one screenfull at a time. + +To delete an individual entry, display it on the screen and then tap on it. The entry's position in the list will be shown along with a Delete button. Tap this button to remove the entry. The Up and Down arrows on the right side of the screen can be used to move between log entries. Further deletions can be made. Finally, click the Back button in the upper-left to finish and return to the main log screen. + +## Settings + +The settings menu provides the following settings: + +### Log + +**Max entries:** Select the maximum number of entries that the log can hold. + +**Auto-delete oldest:** If turned on, adding a log entry when the log is full will cause the oldest entry to automatically be deleted to make room. Otherwise, it is not possible to add another log entry until some entries are manually deleted or the “Max entries” setting is increased. + +**Clear log:** Remove all log entries, leaving the log empty. + +### Appearance + +**Log font:** Select the font used to display log entries. + +**Log font H size** and **Log font V size**: Select the horizontal and vertical sizes, respectively, of the font. Reasonable values for bitmapped fonts are 1 or 2 for either setting. For Vector, values around 15 to 25 work best. Setting both sizes the same will display the font with normal proportions; varying the values will change the relative height or width of the font. + +### Button + +You can choose the action that the physical button (Bangle.js v2) performs when the screen is unlocked. + +**Log time:** Add a date/time stamp to the log. Same as tapping the large button on the touch screen. + +**Open settings:** Open this app settings menu. + +**Quit app:** Return to the main clock app. + +**Do nothing:** Perform no action. + +## Web interface + +Currently the web interface displays the list of dates and times, which can be copied and pasted as desired. The log cannot currently be edited with this interface, only displayed. + +## Support + +Issues and questions may be posted at: https://github.com/espruino/BangleApps/issues + +## Creator + +Travis Evans diff --git a/apps/timestamplog/app-icon.js b/apps/timestamplog/app-icon.js new file mode 100644 index 000000000..b35f05e08 --- /dev/null +++ b/apps/timestamplog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cB/4ACBQX48AFDAAUkyVJAQoDCBZACDymSoEAgVJkQRJkskGAuJCJEpBwYDCyIRHpVICI0SogRGqQyEAgdIBwIUEyAODAQkJiVJxBoDwARIgJuBiIUBCIKzKCIOCQYWkCJUkpNQoiMBkARKgmJxUIxMlOgIQIQwOJyNBiKeBCJeRyUokUoGZPYAQMRyVFgiwDAAsGCIlI0GIBYfAAgUB2wECCINJikRCIfbAgVt2DaDCIMiwR9DjdggEDtg5DgTECSQIJDtuAEwYRFSQOSBIUN2xWCgANBgVSAYKSBR4k2AgYRCxQDBSQTGKgVRCISSBoARKxUpCIKSFAA0SqNFCIKSFAA0RlUo0iSHCI0losUSRARFkmo0SSEwAPFeoORkmViiSEiARHxJXB0SSFAgQCByEAggRCqiSEilChEgwUIXgMkBgVKSQmiqFBgkQoMUoArESQdE6Y1FxIRESQco0WIEYkRiQRDSQWRmnTpojEwRhFSQOKEYOJEYkQogRESQNNEZEIPoQUCEYeKkIjEoLFBCIdTEYc0EYsiCIlKpQjCkojCNIYREpMpEYwCCEYoCB0gjBkmEEYImCgQRGyWTNYJECbQQjHJQIDBygjNpSHCEZ0QAYIjODoJHPEAgjDA==")) diff --git a/apps/timestamplog/app.js b/apps/timestamplog/app.js new file mode 100644 index 000000000..c44ab719b --- /dev/null +++ b/apps/timestamplog/app.js @@ -0,0 +1,523 @@ +const Layout = require('Layout'); +const locale = require('locale'); + +const tsl = require('timestamplog'); + + +// Min number of pixels of movement to recognize a touchscreen drag/swipe +const DRAG_THRESHOLD = 30; + +// Width of scroll indicators +const SCROLL_BAR_WIDTH = 12; + + +// Fetch a stringified image +function getIcon(id) { + if (id == 'add') { +// Graphics.createImage(` +// XX X X X X +// XX X X X X +// XXXXXX X X X X +// XXXXXX X X X X +// XX X X X X +// XX X X X X +// X XX X X +// X X X X +// X XX X +// X X X +// X X +// X XX +// XXX XX +// XXXXX +// XXXX +// XX +// `); + return "\0\x17\x10\x81\x000\t\x12`$K\xF0\x91'\xE2D\x83\t\x12\x06$H\x00\xB1 \x01$\x80\x042\x00\b(\x00 \x00A\x80\x01\xCC\x00\x03\xE0\x00\x0F\x00\x00\x18\x00\x00"; + } else if (id == 'menu') { +// Graphics.createImage(` +// +// +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// `); + return "\0\x10\x10\x81\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0"; + } +} + + +// UI layout render callback for log entries +function renderLogItem(elem) { + if (elem.item) { + g.setColor(g.theme.bg) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1) + .setFont(tsl.fontSpec(tsl.SETTINGS.logFont, + tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize)) + .setFontAlign(-1, -1) + .setColor(g.theme.fg) + .drawLine(elem.x, elem.y, elem.x + elem.w - 1, elem.y) + .drawString(locale.date(elem.item.stamp, 1) + + '\n' + + locale.time(elem.item.stamp).trim(), + elem.x, elem.y + 1); + } else { + g.setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.25)) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1); + } +} + +// Render a scroll indicator +// `scroll` format: { +// pos: int, +// min: int, +// max: int, +// itemsPerPage: int, +// } +function renderScrollBar(elem, scroll) { + const border = 1; + const boxArea = elem.h - 2 * border; + const boxSize = E.clip( + Math.round( + scroll.itemsPerPage / (scroll.max - scroll.min + 1) * (elem.h - 2) + ), + 3, + boxArea + ); + const boxTop = (scroll.max - scroll.min) ? + Math.round( + (scroll.pos - scroll.min) / (scroll.max - scroll.min) + * (boxArea - boxSize) + elem.y + border + ) : elem.y + border; + + // Draw border + g.setColor(g.theme.fg) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1) + // Draw scroll box area + .setColor(g.theme.bg) + .fillRect(elem.x + border, elem.y + border, + elem.x + elem.w - border - 1, elem.y + elem.h - border - 1) + // Draw scroll box + .setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.5)) + .fillRect(elem.x + border, boxTop, + elem.x + elem.w - border - 1, boxTop + boxSize - 1); +} + +// Main app screen interface, launched by calling start() +class MainScreen { + + constructor() { + // Values set up by start() + this.itemsPerPage = null; + this.scrollPos = null; + this.layout = null; + + // Handlers/listeners + this.buttonTimeoutId = null; + this.listeners = {}; + } + + // Launch this UI and make it live + start() { + this._initLayout(); + this.layout.clear(); + this.scroll('b'); + this.render('buttons'); + + this._initTouch(); + } + + // Stop this UI, shut down all timers/listeners, and otherwise clean up + stop() { + if (this.buttonTimeoutId) { + clearTimeout(this.buttonTimeoutId); + this.buttonTimeoutId = null; + } + + // Kill layout handlers + Bangle.removeListener('drag', this.listeners.drag); + Bangle.removeListener('touch', this.listeners.touch); + clearWatch(this.listeners.btnWatch); + Bangle.setUI(); + } + + _initLayout() { + let layout = new Layout( + {type: 'v', + c: [ + // Placeholder to force bottom alignment when there is unused + // vertical screen space + {type: '', id: 'placeholder', fillx: 1, filly: 1}, + + {type: 'h', + c: [ + {type: 'v', + id: 'logItems', + + // To be filled in with log item elements once we + // determine how many will fit on screen + c: [], + }, + {type: 'custom', + id: 'logScroll', + render: elem => { renderScrollBar(elem, this.scrollBarInfo()); } + }, + ], + }, + {type: 'h', + id: 'buttons', + c: [ + {type: 'btn', font: '6x8:2', fillx: 1, label: '+ XX:XX', id: 'addBtn', + cb: this.addTimestamp.bind(this)}, + {type: 'btn', font: '6x8:2', label: getIcon('menu'), id: 'menuBtn', + cb: () => launchSettingsMenu(returnFromSettings)}, + ], + }, + ], + } + ); + + // Calculate how many log items per page we have space to display + layout.update(); + let availableHeight = layout.placeholder.h; + g.setFont(tsl.fontSpec(tsl.SETTINGS.logFont, + tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize)); + let logItemHeight = g.getFontHeight() * 2; + this.itemsPerPage = Math.max(1, + Math.floor(availableHeight / logItemHeight)); + + // Populate log items in layout + for (let i = 0; i < this.itemsPerPage; i++) { + layout.logItems.c.push( + {type: 'custom', render: renderLogItem, item: undefined, itemIdx: undefined, + fillx: 1, height: logItemHeight} + ); + } + layout.logScroll.height = logItemHeight * this.itemsPerPage; + layout.logScroll.width = SCROLL_BAR_WIDTH; + layout.update(); + + this.layout = layout; + } + + // Redraw a particular display `item`, or everything if `item` is falsey + render(item) { + if (!item || item == 'log') { + let layLogItems = this.layout.logItems; + let logIdx = this.scrollPos - this.itemsPerPage; + for (let elem of layLogItems.c) { + logIdx++; + elem.item = stampLog.log[logIdx]; + elem.itemIdx = logIdx; + } + this.layout.render(layLogItems); + this.layout.render(this.layout.logScroll); + } + + if (!item || item == 'buttons') { + let addBtn = this.layout.addBtn; + + if (!tsl.SETTINGS.rotateLog && stampLog.isFull()) { + // Dimmed appearance for unselectable button + addBtn.btnFaceCol = g.blendColor(g.theme.bg2, g.theme.bg, 0.5); + addBtn.btnBorderCol = g.blendColor(g.theme.fg2, g.theme.bg, 0.5); + + addBtn.label = 'Log full'; + } else { + addBtn.btnFaceCol = g.theme.bg2; + addBtn.btnBorderCol = g.theme.fg2; + + addBtn.label = getIcon('add') + ' ' + locale.time(new Date(), 1).trim(); + } + + this.layout.render(this.layout.buttons); + + // Auto-update time of day indication on log-add button upon + // next minute + if (!this.buttonTimeoutId) { + this.buttonTimeoutId = setTimeout( + () => { + this.buttonTimeoutId = null; + this.render('buttons'); + }, + 60000 - (Date.now() % 60000) + ); + } + } + + } + + _initTouch() { + let distanceY = null; + + function dragHandler(ev) { + // Handle up/down swipes for scrolling + if (ev.b) { + if (distanceY === null) { + // Drag started + distanceY = ev.dy; + } else { + // Drag in progress + distanceY += ev.dy; + } + } else { + // Drag released + distanceY = null; + } + if (Math.abs(distanceY) > DRAG_THRESHOLD) { + // Scroll threshold reached + Bangle.buzz(50, .5); + this.scroll(distanceY > 0 ? 'u' : 'd'); + distanceY = null; + } + } + + this.listeners.drag = dragHandler.bind(this); + Bangle.on('drag', this.listeners.drag); + + function touchHandler(button, xy) { + // Handle taps on log entries + let logUIItems = this.layout.logItems.c; + for (var logUIObj of logUIItems) { + if (!xy.type && + logUIObj.x <= xy.x && xy.x < logUIObj.x + logUIObj.w && + logUIObj.y <= xy.y && xy.y < logUIObj.y + logUIObj.h && + logUIObj.item) { + switchUI(new LogEntryScreen(stampLog, logUIObj.itemIdx)); + break; + } + } + } + + this.listeners.touch = touchHandler.bind(this); + Bangle.on('touch', this.listeners.touch); + + function buttonHandler() { + let act = tsl.SETTINGS.buttonAction; + if (act == 'Log time') { + if (currentUI != mainUI) { + switchUI(mainUI); + } + mainUI.addTimestamp(); + } else if (act == 'Open settings') { + launchSettingsMenu(returnFromSettings); + } else if (act == 'Quit app') { + Bangle.showClock(); + } + } + + this.listeners.btnWatch = setWatch(buttonHandler, BTN, + {edge: 'falling', debounce: 50, repeat: true}); + } + + // Add current timestamp to log if possible and update UI display + addTimestamp() { + if (tsl.SETTINGS.rotateLog || !stampLog.isFull()) { + stampLog.addEntry(); + this.scroll('b'); + this.render('buttons'); + } + } + + // Get scroll information for log display + scrollInfo() { + return { + pos: this.scrollPos, + min: (stampLog.log.length - 1) % this.itemsPerPage, + max: stampLog.log.length - 1, + itemsPerPage: this.itemsPerPage + }; + } + + // Like scrollInfo, but adjust the data so as to suggest scrollbar + // geometry that accurately reflects the nature of the scrolling + // (page by page rather than item by item) + scrollBarInfo() { + const info = this.scrollInfo(); + + function toPage(scrollPos) { + return Math.floor(scrollPos / info.itemsPerPage); + } + + return { + // Define 1 "screenfull" as the unit here + itemsPerPage: 1, + pos: toPage(info.pos), + min: toPage(info.min), + max: toPage(info.max), + }; + } + + // Scroll display in given direction or to given position: + // 'u': up, 'd': down, 't': to top, 'b': to bottom + scroll(how) { + let scroll = this.scrollInfo(); + + if (how == 'u') { + this.scrollPos -= scroll.itemsPerPage; + } else if (how == 'd') { + this.scrollPos += scroll.itemsPerPage; + } else if (how == 't') { + this.scrollPos = scroll.min; + } else if (how == 'b') { + this.scrollPos = scroll.max; + } + + this.scrollPos = E.clip(this.scrollPos, scroll.min, scroll.max); + + this.render('log'); + } +} + + +// Log entry screen interface, launched by calling start() +class LogEntryScreen { + + constructor(stampLog, logIdx) { + this.logIdx = logIdx; + this.logItem = stampLog.log[logIdx]; + + this.defaultFont = tsl.fontSpec( + tsl.SETTINGS.logFont, tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize); + } + + start() { + this._initLayout(); + this.layout.clear(); + this.refresh(); + } + + stop() { + Bangle.setUI(); + } + + back() { + this.stop(); + switchUI(mainUI); + } + + _initLayout() { + let layout = new Layout( + {type: 'v', + c: [ + {type: 'txt', font: this.defaultFont, id: 'entryno', label: 'Entry ?/?'}, + {type: 'txt', font: this.defaultFont, id: 'date', label: '?'}, + {type: 'txt', font: this.defaultFont, id: 'time', label: '?'}, + {type: '', id: 'placeholder', fillx: 1, filly: 1}, + {type: 'btn', font: '12x20', label: 'Delete', + cb: this.delLogItem.bind(this)}, + ], + }, + { + back: this.back.bind(this), + btns: [ + {label: '<', cb: this.prevLogItem.bind(this)}, + {label: '>', cb: this.nextLogItem.bind(this)}, + ], + } + ); + + layout.update(); + this.layout = layout; + } + + render(item) { + this.layout.clear(); + this.layout.render(); + } + + refresh() { + this.logItem = stampLog.log[this.logIdx]; + this.layout.entryno.label = 'Entry ' + (this.logIdx+1) + '/' + stampLog.log.length; + this.layout.date.label = locale.date(this.logItem.stamp, 1); + this.layout.time.label = locale.time(this.logItem.stamp).trim(); + this.layout.update(); + this.render(); + } + + prevLogItem() { + this.logIdx = this.logIdx ? this.logIdx-1 : stampLog.log.length-1; + this.refresh(); + } + + nextLogItem() { + this.logIdx = this.logIdx == stampLog.log.length-1 ? 0 : this.logIdx+1; + this.refresh(); + } + + delLogItem() { + stampLog.deleteEntries([this.logItem]); + if (!stampLog.log.length) { + this.back(); + return; + } else if (this.logIdx > stampLog.log.length - 1) { + this.logIdx = stampLog.log.length - 1; + } + + // Create a brief “blink” on the screen to provide user feedback + // that the deletion has been performed + this.layout.clear(); + setTimeout(this.refresh.bind(this), 250); + } + +} + + +function switchUI(newUI) { + currentUI.stop(); + currentUI = newUI; + currentUI.start(); +} + + +function saveErrorAlert() { + currentUI.stop(); + // Not `showAlert` because the icon plus message don't fit the + // screen well + E.showPrompt( + 'Trouble saving timestamp log; data may be lost!', + {title: "Can't save log", + buttons: {'Ok': true}} + ).then(currentUI.start.bind(currentUI)); +} + + +function launchSettingsMenu(backCb) { + currentUI.stop(); + stampLog.save(); + tsl.launchSettingsMenu(backCb); +} + +function returnFromSettings() { + // Reload stampLog to pick up any changes made from settings UI + stampLog = loadStampLog(); + currentUI.start(); +} + + +function loadStampLog() { + // Create a StampLog object with its data loaded from storage + return new tsl.StampLog(tsl.LOG_FILENAME, tsl.SETTINGS.maxLogLength); +} + + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var stampLog = loadStampLog(); +E.on('kill', stampLog.save.bind(stampLog)); +stampLog.on('saveError', saveErrorAlert); + +var currentUI = new MainScreen(stampLog); +var mainUI = currentUI; +currentUI.start(); diff --git a/apps/timestamplog/app.png b/apps/timestamplog/app.png new file mode 100644 index 000000000..ddb51fe40 Binary files /dev/null and b/apps/timestamplog/app.png differ diff --git a/apps/timestamplog/app.xcf b/apps/timestamplog/app.xcf new file mode 100644 index 000000000..27406b1ed Binary files /dev/null and b/apps/timestamplog/app.xcf differ diff --git a/apps/timestamplog/interface.html b/apps/timestamplog/interface.html new file mode 100644 index 000000000..b54dfb010 --- /dev/null +++ b/apps/timestamplog/interface.html @@ -0,0 +1,31 @@ + + + + + + +
Loading...
+ + + diff --git a/apps/timestamplog/lib.js b/apps/timestamplog/lib.js new file mode 100644 index 000000000..59590bfb7 --- /dev/null +++ b/apps/timestamplog/lib.js @@ -0,0 +1,248 @@ +const storage = require('Storage'); + +// Storage filenames + +const LOG_FILENAME = 'timestamplog.json'; +const SETTINGS_FILENAME = 'timestamplog.settings.json'; + + +// Settings + +const SETTINGS = Object.assign({ + logFont: '12x20', + logFontHSize: 1, + logFontVSize: 1, + maxLogLength: 30, + rotateLog: false, + buttonAction: 'Log time', +}, storage.readJSON(SETTINGS_FILENAME, true) || {}); + +const SETTINGS_BUTTON_ACTION = [ + 'Log time', + 'Open settings', + 'Quit app', + 'Do nothing', +]; + + +function fontSpec(name, hsize, vsize) { + return name + ':' + hsize + 'x' + vsize; +} + + +//// Data models //// + +// High-level timestamp log object that provides an interface to the +// UI for managing log entries and automatically loading/saving +// changes to flash storage. +class StampLog { + constructor(filename, maxLength) { + // Name of file to save log to + this.filename = filename; + // Maximum entries for log before old entries are overwritten with + // newer ones + this.maxLength = maxLength; + + // `true` when we have changes that need to be saved + this.isDirty = false; + // Wait at most this many msec upon first data change before + // saving (this is to avoid excessive writes to flash if several + // changes happen quickly; we wait a little bit so they can be + // rolled into a single write) + this.saveTimeout = 30000; + // setTimeout ID for scheduled save job + this.saveId = null; + // Underlying raw log data object. Outside this class it's + // recommended to use only the class methods to change it rather + // than modifying the object directly to ensure that changes are + // recognized and saved to storage. + this.log = []; + + this.load(); + } + + // Read in the log data that is currently in storage + load() { + let log = storage.readJSON(this.filename, true); + if (!log) log = []; + // Convert stringified datetimes back into Date objects + for (let logEntry of log) { + logEntry.stamp = new Date(logEntry.stamp); + } + this.log = log; + } + + // Write current log data to storage if anything needs to be saved + save() { + // Cancel any pending scheduled calls to save() + if (this.saveId) { + clearTimeout(this.saveId); + this.saveId = null; + } + + if (this.isDirty) { + let logToSave = []; + for (let logEntry of this.log) { + // Serialize each Date object into an ISO string before saving + let newEntry = Object.assign({}, logEntry); + newEntry.stamp = logEntry.stamp.toISOString(); + logToSave.push(newEntry); + } + + if (storage.writeJSON(this.filename, logToSave)) { + console.log('stamplog: save to storage completed'); + this.isDirty = false; + } else { + console.log('stamplog: save to storage FAILED'); + this.emit('saveError'); + } + } else { + console.log('stamplog: skipping save to storage because no changes made'); + } + } + + // Mark log as needing to be (re)written to storage + setDirty() { + this.isDirty = true; + if (!this.saveId) { + this.saveId = setTimeout(this.save.bind(this), this.saveTimeout); + } + } + + // Add a timestamp for the current time to the end of the log + addEntry() { + // If log full, purge an old entry to make room for new one + if (this.maxLength) { + while (this.log.length + 1 > this.maxLength) { + this.log.shift(); + } + } + // Add new entry + this.log.push({ + stamp: new Date() + }); + this.setDirty(); + } + + // Delete the log objects given in the array `entries` from the log + deleteEntries(entries) { + this.log = this.log.filter(entry => !entries.includes(entry)); + this.setDirty(); + } + + // Does the log currently contain the maximum possible number of entries? + isFull() { + return this.log.length >= this.maxLength; + } +} + +function launchSettingsMenu(backCb) { + const fonts = g.getFonts(); + const stampLog = new StampLog(LOG_FILENAME, SETTINGS.maxLogLength); + + function saveSettings() { + console.log('Saving timestamp log and settings'); + stampLog.save(); + if (!storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { + E.showAlert('Trouble saving settings'); + } + } + E.on('kill', saveSettings); + + function endMenu() { + saveSettings(); + E.removeListener('kill', saveSettings); + backCb(); + } + + function topMenu() { + E.showMenu({ + '': { + title: 'Stamplog', + back: endMenu, + }, + 'Log': logMenu, + 'Appearance': appearanceMenu, + 'Button': { + value: SETTINGS_BUTTON_ACTION.indexOf(SETTINGS.buttonAction), + min: 0, max: SETTINGS_BUTTON_ACTION.length - 1, + format: v => SETTINGS_BUTTON_ACTION[v], + onchange: v => { + SETTINGS.buttonAction = SETTINGS_BUTTON_ACTION[v]; + }, + }, + }); + } + + function logMenu() { + E.showMenu({ + '': { + title: 'Log', + back: topMenu, + }, + 'Max entries': { + value: SETTINGS.maxLogLength, + min: 5, max: 100, step: 5, + onchange: v => { + SETTINGS.maxLogLength = v; + stampLog.maxLength = v; + } + }, + 'Auto-delete oldest': { + value: SETTINGS.rotateLog, + onchange: v => { + SETTINGS.rotateLog = !SETTINGS.rotateLog; + } + }, + 'Clear log': doClearLog, + }); + } + + function appearanceMenu() { + E.showMenu({ + '': { + title: 'Appearance', + back: topMenu, + }, + 'Log font': { + value: fonts.indexOf(SETTINGS.logFont), + min: 0, max: fonts.length - 1, + format: v => fonts[v], + onchange: v => { + SETTINGS.logFont = fonts[v]; + }, + }, + 'Log font H size': { + value: SETTINGS.logFontHSize, + min: 1, max: 50, + onchange: v => { + SETTINGS.logFontHSize = v; + }, + }, + 'Log font V size': { + value: SETTINGS.logFontVSize, + min: 1, max: 50, + onchange: v => { + SETTINGS.logFontVSize = v; + }, + }, + }); + } + + function doClearLog() { + E.showPrompt('Erase ALL log entries?', { + title: 'Clear log', + buttons: {'Erase':1, "Don't":0} + }).then((yes) => { + if (yes) { + stampLog.deleteEntries(stampLog.log); + } + logMenu(); + }); + } + + topMenu(); +} + +exports = {LOG_FILENAME, SETTINGS_FILENAME, SETTINGS, SETTINGS_BUTTON_ACTION, fontSpec, StampLog, + launchSettingsMenu}; diff --git a/apps/timestamplog/metadata.json b/apps/timestamplog/metadata.json new file mode 100644 index 000000000..e1aa0eb23 --- /dev/null +++ b/apps/timestamplog/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "timestamplog", + "name": "Timestamp log", + "shortName":"Timestamp log", + "icon": "app.png", + "version": "0.01", + "description": "Conveniently record a series of date/time stamps", + "screenshots": [ {"url": "screenshot.png" } ], + "readme": "README.md", + "tags": "timestamp, log", + "supports": ["BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name": "timestamplog.app.js", "url": "app.js"}, + {"name": "timestamplog.img", "url": "app-icon.js", "evaluate": true}, + {"name": "timestamplog", "url": "lib.js"}, + {"name": "timestamplog.settings.js", "url": "settings.js"} + ], + "data": [ + {"name": "timestamplog.settings"}, + {"name": "timestamplog.json"} + ] +} diff --git a/apps/timestamplog/screenshot.png b/apps/timestamplog/screenshot.png new file mode 100644 index 000000000..27f36ac32 Binary files /dev/null and b/apps/timestamplog/screenshot.png differ diff --git a/apps/timestamplog/settings.js b/apps/timestamplog/settings.js new file mode 100644 index 000000000..137ed31db --- /dev/null +++ b/apps/timestamplog/settings.js @@ -0,0 +1,7 @@ +const tsl = require('timestamplog'); + +( + function(backCb) { + tsl.launchSettingsMenu(backCb); + } +); diff --git a/apps/twotwoclock/ChangeLog b/apps/twotwoclock/ChangeLog index 09953593e..d3275c0e8 100644 --- a/apps/twotwoclock/ChangeLog +++ b/apps/twotwoclock/ChangeLog @@ -1 +1,2 @@ 0.01: New Clock! +0.02: Clockinfos now save under correct name, and wrap correctly to >1 line \ No newline at end of file diff --git a/apps/twotwoclock/app.js b/apps/twotwoclock/app.js index 57be691e1..b2d5ea9fb 100644 --- a/apps/twotwoclock/app.js +++ b/apps/twotwoclock/app.js @@ -134,8 +134,8 @@ for (var i=0;i<10;i++) if (g.stringWidth(txt) > options.w) // if too big, smaller font g.setFont("LECO1976Regular14"); if (g.stringWidth(txt) > options.w) {// if still too big, split to 2 lines - var l = g.wrapString(txt, options.w); - txt = l.slice(0,2).join("\n") + (l.length>2)?"...":""; + var l = g.wrapString(txt, options.w-4); + txt = l.slice(0,2).join("\n") + ((l.length>2)?"...":""); } var x = options.x+options.w/2, y = options.y+54; g.setColor(g.theme.bg).drawString(txt, x-2, y). // draw the text background @@ -147,12 +147,12 @@ for (var i=0;i<10;i++) }; clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, { - app:"pebblepp", + app:"twotwoclock", x : g.getWidth()-clockInfoW, y: 0, w: clockInfoW, h:clockInfoH, draw : clockInfoDraw }); clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, { - app:"pebblepp", + app:"twotwoclock", x : g.getWidth()-clockInfoW, y: clockInfoH, w: clockInfoW, h:clockInfoH, draw : clockInfoDraw }); diff --git a/apps/twotwoclock/metadata.json b/apps/twotwoclock/metadata.json index ebcba539c..ae3b958ef 100644 --- a/apps/twotwoclock/metadata.json +++ b/apps/twotwoclock/metadata.json @@ -1,7 +1,7 @@ { "id": "twotwoclock", "name": "TwoTwo Clock", "shortName":"22 Clock", - "version":"0.01", + "version":"0.02", "description": "A clock with the time split over two lines, with custom backgrounds and two ClockInfos", "icon": "icon.png", "type": "clock", diff --git a/apps/walkersclock/app.js b/apps/walkersclock/app.js index 5b83bf583..f78be61ca 100644 --- a/apps/walkersclock/app.js +++ b/apps/walkersclock/app.js @@ -8,7 +8,7 @@ * - two function menus at present * GPS Power = On/Off * GPS Display = Grid | Speed Alt - * when the modeline in CYAN use button BTN1 to switch between options + * when the modeline in CYAN use button BTN1 to switch between options * - display the current steps if one of the steps widgets is installed * - ensures that BTN2 requires a 1.5 second press in order to switch to the launcher * this is so you dont accidently switch out of the GPS/watch display with you coat sleeve @@ -65,14 +65,14 @@ let last_fix = { satellites: 0 }; -function drawTime() { +function drawTime() { var d = new Date(); var da = d.toString().split(" "); var time = da[4].substr(0,5); g.reset(); g.clearRect(0,Y_TIME, 239, Y_ACTIVITY - 1); - + g.setColor(1,1,1); // white g.setFontAlign(0, -1); @@ -83,7 +83,7 @@ function drawTime() { } else { g.setFont("Vector", 80); } - + g.drawString(time, g.getWidth()/2, Y_TIME); } @@ -93,19 +93,19 @@ function drawActivity() { clearActivityArea = true; prevSteps = steps; - + if (clearActivityArea) { g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); clearActivityArea = false; } - + if (!gpsPowerState) { g.setColor(0,255,0); // green g.setFont("Vector", 60); g.drawString(getSteps(), g.getWidth()/2, Y_ACTIVITY); return; } - + g.setFont("6x8", 3); g.setColor(1,1,1); g.setFontAlign(0, -1); @@ -130,10 +130,10 @@ function drawActivity() { let ref = to_map_ref(6, os.easting, os.northing); let speed; let activityStr = ""; - + if (age < 0) age = 0; g.setFontVector(40); - g.setColor(0xFFC0); + g.setColor(0xFFC0); switch(gpsDisplay) { case GDISP_OS: @@ -146,7 +146,7 @@ function drawActivity() { case GDISP_SPEED: speed = last_fix.speed; speed = speed.toFixed(1); - activityStr = speed + "kph"; + activityStr = speed + "kph"; break; case GDISP_ALT: activityStr = last_fix.alt + "m"; @@ -159,7 +159,7 @@ function drawActivity() { g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); g.drawString(activityStr, 120, Y_ACTIVITY); g.setFont("6x8",2); - g.setColor(1,1,1); + g.setColor(1,1,1); g.drawString(age, 120, Y_ACTIVITY + 46); } } @@ -167,7 +167,7 @@ function drawActivity() { function onTick() { if (!Bangle.isLCDOn()) return; - + if (gpsPowerState) { drawAll(); return; @@ -226,7 +226,7 @@ function drawInfo() { drawModeLine(str,col); return; } - + switch(infoMode) { case INFO_NONE: col = 0x0000; @@ -239,7 +239,7 @@ function drawInfo() { default: str = "Battery: " + E.getBattery() + "%"; } - + drawModeLine(str,col); } @@ -283,7 +283,7 @@ function changeInfoMode() { infoMode = INFO_NONE; clearActivityArea = true; return; - + case FN_MODE_GDISP: switch (gpsDisplay) { case GDISP_OS: @@ -304,7 +304,7 @@ function changeInfoMode() { break; } } - + switch(infoMode) { case INFO_NONE: if (stepsWidget() !== undefined) @@ -319,7 +319,7 @@ function changeInfoMode() { default: infoMode = INFO_NONE; } - + clearActivityArea = true; } @@ -351,7 +351,7 @@ function changeFunctionMode() { break; } } - + infoMode = INFO_NONE; // function mode overrides info mode } @@ -374,7 +374,7 @@ function processFix(fix) { gpsState = GPS_SATS; clearActivityArea = true; } - + if (fix.fix) { if (!last_fix.fix) { if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { @@ -401,7 +401,7 @@ function stepsWidget() { } return undefined; } - + /************* GPS / OSREF Code **************************/ @@ -413,10 +413,10 @@ function formatTime(now) { function timeSince(t) { var hms = t.split(":"); var now = new Date(); - + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); - + return (sn - st); } @@ -483,7 +483,7 @@ OsGridRef.latLongToOsGrid = function(point) { * */ function to_map_ref(digits, easting, northing) { - if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing + if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); let e = easting; let n = northing; diff --git a/apps/widbattpwr/ChangeLog b/apps/widbattpwr/ChangeLog new file mode 100644 index 000000000..929eb99e9 --- /dev/null +++ b/apps/widbattpwr/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial fork from hwid_a_battery_widget +0.02: Show battery percentage (instead of power) if charging +0.03: Use `power_usage` module diff --git a/apps/widbattpwr/README.md b/apps/widbattpwr/README.md new file mode 100644 index 000000000..22a575166 --- /dev/null +++ b/apps/widbattpwr/README.md @@ -0,0 +1,15 @@ +# Battery Power Widget + +Show the time remaining at the current power consumption, and battery percentage via shading of the text and a percentage bar. + +Battery percentage can be seen: +- Temporarily by tapping the widget +- By charging the watch + +Requires firmware 2v23 or above. + +This is a copy of `hwid_a_battery_widget` (that being a copy of `wid_a_battery_widget`). + +## Creator + +[@bobrippling](https://github.com/bobrippling) diff --git a/apps/widbattpwr/metadata.json b/apps/widbattpwr/metadata.json new file mode 100644 index 000000000..2a41169ab --- /dev/null +++ b/apps/widbattpwr/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widbattpwr", + "name": "Battery power and percentage widget", + "shortName": "Batt Pwr", + "icon": "widget.png", + "version": "0.03", + "type": "widget", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "description": "A battery widget showing percentage (via shading) and time remaining at current power consumption", + "tags": "widget,battery", + "provides_widgets": ["battery"], + "storage": [ + { + "name": "widbattpwr.wid.js", + "url": "widget.js" + } + ] +} diff --git a/apps/widbattpwr/widget.js b/apps/widbattpwr/widget.js new file mode 100644 index 000000000..83a4917f5 --- /dev/null +++ b/apps/widbattpwr/widget.js @@ -0,0 +1,74 @@ +(function () { + var intervalLow = 60000; + var intervalHigh = 2000; + var width = 30; + var height = 24; + var showPct = false; + var powerColour = function (pwr) { + return pwr >= 23000 + ? "#f00" + : pwr > 2000 + ? "#fc0" + : "#0f0"; + }; + var drawBar = function (x, y, batt) { + return g.fillRect(x + 1, y + height - 3, x + 1 + (width - 2) * batt / 100, y + height - 1); + }; + var drawString = function (x, y, txt) { + return g.drawString(txt, x + 14, y + 10); + }; + function draw() { + var x = this.x; + var y = this.y; + var _a = require("power_usage").get(), usage = _a.usage, hrsLeft = _a.hrsLeft, batt = _a.batt; + var pwrColour = powerColour(usage); + g.reset() + .setBgColor(g.theme.bg) + .clearRect(x, y, x + width - 1, y + height - 1); + g.setColor(g.theme.fg); + drawBar(x, y, 100); + g.setColor(pwrColour); + drawBar(x, y, batt); + g.setFontAlign(0, 0); + g.setFont("Vector", 16); + { + var txt = void 0; + if (showPct || Bangle.isCharging()) { + txt = "".concat(batt, "%"); + } + else { + var days = hrsLeft / 24; + txt = days >= 1 ? "".concat(Math.round(Math.min(days, 99)), "d") : "".concat(Math.round(hrsLeft), "h"); + } + var txth = 14; + g.setColor(g.theme.fg); + g.setClipRect(x, y, x + width, y + txth); + drawString(x, y, txt); + g.setColor(pwrColour); + g.setClipRect(x, y + txth * (1 - batt / 100), x + width, y + txth); + drawString(x, y, txt); + } + } + var id = setInterval(function () { + var w = WIDGETS["battpwr"]; + w.draw(w); + }, intervalLow); + Bangle.on("charging", function (charging) { + changeInterval(id, charging ? intervalHigh : intervalLow); + }); + Bangle.on("touch", function (_btn, xy) { + if (WIDGETS["back"] || !xy) + return; + var oversize = 5; + var w = WIDGETS["battpwr"]; + var x = xy.x, y = xy.y; + if (w.x - oversize <= x && x < w.x + width + oversize + && w.y - oversize <= y && y < w.y + height + oversize) { + E.stopEventPropagation && E.stopEventPropagation(); + showPct = true; + setTimeout(function () { return (showPct = false, w.draw(w)); }, 1000); + w.draw(w); + } + }); + WIDGETS["battpwr"] = { area: "tr", width: width, draw: draw }; +})(); diff --git a/apps/widbattpwr/widget.png b/apps/widbattpwr/widget.png new file mode 100644 index 000000000..e5a8284a1 Binary files /dev/null and b/apps/widbattpwr/widget.png differ diff --git a/apps/widbattpwr/widget.ts b/apps/widbattpwr/widget.ts new file mode 100644 index 000000000..2b92398d2 --- /dev/null +++ b/apps/widbattpwr/widget.ts @@ -0,0 +1,88 @@ +(() => { + const intervalLow = 60000; + const intervalHigh = 2000; + const width = 30; + const height = 24; + let showPct = false; + + const powerColour = (pwr: number) => + pwr >= 23000 + ? "#f00" // red, e.g. GPS ~20k + : pwr > 2000 + ? "#fc0" // yellow, e.g. CPU ~1k, HRM ~700 + : "#0f0"; // green: ok + + const drawBar = (x: number, y: number, batt: number) => + g.fillRect(x+1, y+height-3, x+1+(width-2)*batt/100, y+height-1); + + const drawString = (x: number, y: number, txt: string) => + g.drawString(txt, x + 14, y + 10); + + function draw(this: Widget) { + let x = this.x!; + let y = this.y!; + + const { usage, hrsLeft, batt } = require("power_usage").get(); + const pwrColour = powerColour(usage); + + g.reset() + .setBgColor(g.theme.bg) + .clearRect(x, y, x + width - 1, y + height - 1); + + g.setColor(g.theme.fg); + drawBar(x, y, 100); + g.setColor(pwrColour); + drawBar(x, y, batt); + + g.setFontAlign(0, 0); + g.setFont("Vector", 16); + { + let txt; + if(showPct || Bangle.isCharging()){ + txt = `${batt}%`; + }else{ + const days = hrsLeft / 24; + txt = days >= 1 ? `${Math.round(Math.min(days, 99))}d` : `${Math.round(hrsLeft)}h`; + } + + // draw time remaining, then shade it based on batt % + const txth = 14; + g.setColor(g.theme.fg); + g.setClipRect(x, y, x + width, y + txth); + drawString(x, y, txt); + + g.setColor(pwrColour); + g.setClipRect(x, y + txth * (1 - batt / 100), x + width, y + txth); + drawString(x, y, txt); + } + } + + const id = setInterval(() => { + const w = WIDGETS["battpwr"]!; + w.draw(w); + }, intervalLow); + + Bangle.on("charging", charging => { + changeInterval(id, charging ? intervalHigh : intervalLow); + }); + + Bangle.on("touch", (_btn, xy) => { + if(WIDGETS["back"] || !xy) return; + + const oversize = 5; + const w = WIDGETS["battpwr"]!; + const { x, y } = xy; + + if(w.x! - oversize <= x && x < w.x! + width + oversize + && w.y! - oversize <= y && y < w.y! + height + oversize) + { + E.stopEventPropagation && E.stopEventPropagation(); + + showPct = true; + setTimeout(() => (showPct = false, w.draw(w)), 1000); + w.draw(w); + } + }); + + WIDGETS["battpwr"] = { area: "tr", width, draw }; +})(); diff --git a/apps/widbtstates/widget.js b/apps/widbtstates/widget.js index e80da4082..105e4111d 100644 --- a/apps/widbtstates/widget.js +++ b/apps/widbtstates/widget.js @@ -17,12 +17,12 @@ }; var colours = (_a = {}, _a[1] = { - false: "#fff", + false: "#000", true: "#fff", }, _a[2] = { - false: "#0ff", - true: "#00f", + false: "#00f", + true: "#0ff", }, _a); WIDGETS["bluetooth"] = { diff --git a/apps/widbtstates/widget.ts b/apps/widbtstates/widget.ts index 8f02c1b8c..40f50f627 100644 --- a/apps/widbtstates/widget.ts +++ b/apps/widbtstates/widget.ts @@ -30,12 +30,12 @@ } } = { [State.Active]: { - false: "#fff", + false: "#000", true: "#fff", }, [State.Connected]: { - false: "#0ff", - true: "#00f", + false: "#00f", + true: "#0ff", }, }; diff --git a/apps/widdst/ChangeLog b/apps/widdst/ChangeLog index d1ad50fe2..cedeaa5b4 100644 --- a/apps/widdst/ChangeLog +++ b/apps/widdst/ChangeLog @@ -2,4 +2,5 @@ 0.02: Checks for correct firmware; E.setDST(...) moved to boot.js 0.03: Convert Yes/No On/Off in settings to checkboxes 0.04: Give the boot file the highest priority to ensure it runs before sched (fix #2663) -0.05: Tweaks to ensure Gadgetbridge can't overwrite timezone on 2v19.106 and later \ No newline at end of file +0.05: Tweaks to ensure Gadgetbridge can't overwrite timezone on 2v19.106 and later +0.06: If fastload is present, ensure DST changes are still applied when leaving settings diff --git a/apps/widdst/metadata.json b/apps/widdst/metadata.json index 006e03416..495f06086 100644 --- a/apps/widdst/metadata.json +++ b/apps/widdst/metadata.json @@ -1,6 +1,6 @@ { "id": "widdst", "name": "Daylight Saving", - "version":"0.05", + "version":"0.06", "description": "Widget to set daylight saving rules. Requires Espruino 2v15 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 7363aa6bf..0017cc499 100644 --- a/apps/widdst/settings.js +++ b/apps/widdst/settings.js @@ -33,8 +33,11 @@ at: 0 }; + var writtenSettings = false; + function writeSettings() { require('Storage').writeJSON("widdst.json", settings); + writtenSettings = true; } function writeSubMenuSettings() { @@ -136,7 +139,15 @@ "": { "Title": /*LANG*/"Daylight Saving" }, - "< Back": () => back(), + "< Back": () => { + if(writtenSettings && global._load){ + // disable fastload to ensure settings are applied + // when we exit the settings app + global.load = global._load; + delete global._load; + } + back(); + }, /*LANG*/"Enabled": { value: !!settings.has_dst, onchange: v => { diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index e32ab36ba..728c934ea 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef */ + const Setter = { NONE: "none", UPPER: 'upper', @@ -31,7 +31,7 @@ const upperLshape = isB1 ? { left: 210, bottom: 40, top: 210, - rectWidth: 30, + rectWidth: 30, cornerRoundness: 5, orientation: -1, color: '#f00' @@ -62,7 +62,7 @@ const centerBar = { minY: (upperLshape.bottom + upperLshape.top - (upperLshape.rectWidth*1.5))/2, maxY: (upperLshape.bottom + upperLshape.top + (upperLshape.rectWidth*1.5))/2, confidenceWidth: isB1 ? 10 : 8, - minX: isB1 ? 55 : upperLshape.rectWidth + 14, + minX: isB1 ? 55 : upperLshape.rectWidth + 14, maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14 }; diff --git a/core b/core index dc682af21..4f07b72ce 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit dc682af2179512525474c43ed616c801a9570ab8 +Subproject commit 4f07b72ce2bdac4a8da6bfa3da3e2152370446fc diff --git a/loader.js b/loader.js index a8d6d10eb..1f3350656 100644 --- a/loader.js +++ b/loader.js @@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") { 'This is not the official Bangle.js App Loader - you can try the Official Version here.'; } -var RECOMMENDED_VERSION = "2v22"; +var RECOMMENDED_VERSION = "2v24"; // could check http://www.espruino.com/json/BANGLEJS.json for this // We're only interested in Bangles diff --git a/modules/more_pickers.js b/modules/more_pickers.js new file mode 100644 index 000000000..596b36fdf --- /dev/null +++ b/modules/more_pickers.js @@ -0,0 +1,198 @@ +/* see more_pickers.md for more information */ + +exports.doublePicker = function (options) { + var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF"; + + var R = Bangle.appRect; + g.reset().clearRect(R); + g.setFont("12x20").setFontAlign(0, 0).drawString(menuIcon + " " + options.title, R.x + R.w / 2, R.y + 12); + + var v_1 = options.value_1; + var v_2 = options.value_2; + + function draw() { + g.setColor(g.theme.bg2) + .fillRect(14, 60, 81, 166) + .fillRect(95, 60, 162, 166); + + g.setColor(g.theme.fg2) + .fillPoly([47.5, 68, 62.5, 83, 32.5, 83]) + .fillPoly([47.5, 158, 62.5, 143, 32.5, 143]) + .fillPoly([128.5, 68, 143.5, 83, 113.5, 83]) + .fillPoly([128.5, 158, 143.5, 143, 113.5, 143]); + + var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; + + g.setFontAlign(0, 0) + .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_1))) + .drawString(txt_1, 47.5, 113) + .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_2))) + .drawString(txt_2, 128.5, 113) + .setFontVector(30) + .drawString(options.separator ?? "", 88, 110); + } + function cb(dir, x_part) { + if (dir) { + if (x_part == -1) { + v_1 -= (dir || 1) * (options.step_1 || 1); + if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1; + if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1; + } else { + v_2 -= (dir || 1) * (options.step_2 || 1); + if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_2; + if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_2; + } + draw(); + } else { // actually selected + options.value_1 = v_1; + options.value_2 = v_2; + if (options.onchange) options.onchange(options.value_1, options.value_2); + options.back(); // redraw original menu + } + } + + draw(); + var dy = 0; + + Bangle.setUI({ + mode: "custom", + back: options.back, + remove: options.remove, + redraw: draw, + drag: e => { + dy += e.dy; // after a certain amount of dragging up/down fire cb + if (!e.b) dy = 0; + var x_part; + if (e.x <= 88) { + x_part = -1; + } else { + x_part = 1; + } + while (Math.abs(dy) > 32) { + if (dy > 0) { dy -= 32; cb(1, x_part); } + else { dy += 32; cb(-1, x_part); } + Bangle.buzz(20); + } + }, + touch: (_, e) => { + Bangle.buzz(20); + var x_part; + if (e.x <= 88) { + x_part = -1; + } else { + x_part = 1; + } + if (e.y < 82) cb(-1, x_part); // top third + else if (e.y > 142) cb(1, x_part); // bottom third + else cb(); // middle = accept + } + }); +} + +exports.triplePicker = function (options) { + var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF"; + + var R = Bangle.appRect; + g.reset().clearRect(R); + g.setFont("12x20").setFontAlign(0, 0).drawString(menuIcon + " " + options.title, R.x + R.w / 2, R.y + 12); + + var v_1 = options.value_1; + var v_2 = options.value_2; + var v_3 = options.value_3; + + function draw() { + g.setColor(g.theme.bg2) + .fillRect(8, 60, 56, 166) + .fillRect(64, 60, 112, 166) + .fillRect(120, 60, 168, 166); + + g.setColor(g.theme.fg2) + .fillPoly([32, 68, 47, 83, 17, 83]) + .fillPoly([32, 158, 47, 143, 17, 143]) + .fillPoly([88, 68, 103, 83, 73, 83]) + .fillPoly([88, 158, 103, 143, 73, 143]) + .fillPoly([144, 68, 159, 83, 129, 83]) + .fillPoly([144, 158, 159, 143, 129, 143]); + + var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; + var txt_3 = options.format_3 ? options.format_3(v_3) : v_3; + + g.setFontAlign(0, 0) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_1))) + .drawString(txt_1, 32, 113) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_2))) + .drawString(txt_2, 88, 113) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_3))) + .drawString(txt_3, 144, 113) + .setFontVector(30) + .drawString(options.separator_1 ?? "", 60, 113) + .drawString(options.separator_2 ?? "", 116, 113); + } + function cb(dir, x_part) { + if (dir) { + if (x_part == -1) { + v_1 -= (dir || 1) * (options.step_1 || 1); + if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1; + if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1; + } else if (x_part == 0) { + v_2 -= (dir || 1) * (options.step_2 || 1); + if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_3; + if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_3; + } else { + v_3 -= (dir || 1) * (options.step_3 || 1); + if (options.min_3 !== undefined && v_3 < options.min_3) v_3 = options.wrap_3 ? options.max_3 : options.min_3; + if (options.max_3 !== undefined && v_3 > options.max_3) v_3 = options.wrap_3 ? options.min_3 : options.max_3; + } + draw(); + } else { // actually selected + options.value_1 = v_1; + options.value_2 = v_2; + options.value_3 = v_3; + if (options.onchange) options.onchange(options.value_1, options.value_2, options.value_3); + options.back(); // redraw original menu + } + } + + draw(); + var dy = 0; + + Bangle.setUI({ + mode: "custom", + back: options.back, + remove: options.remove, + redraw: draw, + drag: e => { + dy += e.dy; // after a certain amount of dragging up/down fire cb + if (!e.b) dy = 0; + var x_part; + if (e.x <= 58) { + x_part = -1; + } else if (58 < e.x && e.x <= 116) { + x_part = 0; + } else { + x_part = 1; + } + while (Math.abs(dy) > 32) { + if (dy > 0) { dy -= 32; cb(1, x_part); } + else { dy += 32; cb(-1, x_part); } + Bangle.buzz(20); + } + }, + touch: (_, e) => { + Bangle.buzz(20); + var x_part; + if (e.x <= 58) { + x_part = -1; + } else if (58 < e.x && e.x <= 116) { + x_part = 0; + } else { + x_part = 1; + } + if (e.y < 82) cb(-1, x_part); // top third + else if (e.y > 142) cb(1, x_part); // bottom third + else cb(); // middle = accept + } + }); +} \ No newline at end of file diff --git a/modules/more_pickers.md b/modules/more_pickers.md new file mode 100644 index 000000000..1d37af44e --- /dev/null +++ b/modules/more_pickers.md @@ -0,0 +1,93 @@ +# More pickers + +This library provides a double picker and a triple picker, similar to the stock picker. + +# How to use +**Important:** you need to define a `back` handler that will be called to go back to the previous screen when the user confirms the input or clicks on the back button. + +It is possible to define an optionnal custom separator between the values. See examples below. + +## Double picker + +Example: + +```javascript +// example of a formatting function +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +var hours = 10; +var minutes = 32; + +function showMainMenu() { + E.showMenu({ + 'Time': function () { + require("more_pickers").doublePicker({ + back: showMainMenu, + title: "Time", + separator: ":", + + value_1: hours, + min_1: 0, max_1: 23, step_1: 1, wrap_1: true, + + value_2: minutes, + min_2: 0, max_2: 59, step_2: 1, wrap_2: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2) { hours = v_1; minutes = v_2; } + }); + } + }); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +showMainMenu(); +``` + + +## Triple picker + +Example: + +```javascript +// example of a formatting function +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +var day = 21; +var month = 5; +var year = 2021; + +function showMainMenu() { + E.showMenu({ + 'Date': function () { + require("more_pickers").triplePicker({ + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: day, + min_1: 1, max_1: 31, step_1: 1, wrap_1: true, + + value_2: month, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: year, + min_3: 2000, max_3: 2050, step_3: 1, wrap_3: false, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { day = v_1; month = v_2; year = v_3; } + }); + } + }); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +showMainMenu(); \ No newline at end of file diff --git a/modules/power_usage.js b/modules/power_usage.js new file mode 100644 index 000000000..2aadd0d71 --- /dev/null +++ b/modules/power_usage.js @@ -0,0 +1,15 @@ +exports.get = function () { + var pwr = E.getPowerUsage(); + var batt = E.getBattery(); + var usage = 0; + for (var key in pwr.device) { + if (!key.startsWith("LCD")) + usage += pwr.device[key]; + } + var hrsLeft = 175000 * batt / (100 * usage); + return { + usage: usage, + hrsLeft: hrsLeft, + batt: batt, + }; +}; diff --git a/modules/power_usage.ts b/modules/power_usage.ts new file mode 100644 index 000000000..ab6982466 --- /dev/null +++ b/modules/power_usage.ts @@ -0,0 +1,29 @@ +type Pwr = { + usage: number, + hrsLeft: number, + batt: number, // battery percentage +}; + +// eslint-disable-next-line no-unused-vars +type PowerUsageModule = { + get: () => Pwr, +}; + +exports.get = (): Pwr => { + const pwr = E.getPowerUsage(); + const batt = E.getBattery(); + let usage = 0; + for(const key in pwr.device){ + if(!key.startsWith("LCD")) + usage += pwr.device[key as keyof typeof pwr.device]!; + } + + // 175mAh, scaled based on battery (batt/100), scaled down based on usage + const hrsLeft = 175000 * batt / (100 * usage); + + return { + usage, + hrsLeft, + batt, + }; +}; diff --git a/modules/widget_utils.js b/modules/widget_utils.js index 4e2acd296..4f9b85835 100644 --- a/modules/widget_utils.js +++ b/modules/widget_utils.js @@ -90,19 +90,17 @@ exports.swipeOn = function(autohide) { function queueDraw() { const o = exports.offset; + Bangle.appRect.y = o+24; + Bangle.appRect.h = 1 + Bangle.appRect.y2 - Bangle.appRect.y; if (o>-24) { - Bangle.appRect.y = o+24; - Bangle.appRect.h = 1 + Bangle.appRect.y2 - Bangle.appRect.y; - if (o>-24) { - Bangle.setLCDOverlay(og, 0, o, { - id:"widget_utils", - remove:()=>{ - require("widget_utils").cleanupOverlay(); - } - }); - } else { - Bangle.setLCDOverlay(undefined, {id: "widget_utils"}); - } + Bangle.setLCDOverlay(og, 0, o, { + id:"widget_utils", + remove:()=>{ + require("widget_utils").cleanupOverlay(); + } + }); + } else { + Bangle.setLCDOverlay(undefined, {id: "widget_utils"}); } } diff --git a/package-lock.json b/package-lock.json index b7b5d97a7..fd242623b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -830,12 +830,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1661,9 +1661,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4478,12 +4478,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "call-bind": { @@ -5020,9 +5020,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/package.json b/package.json index 5378bc5bd..26320d5c2 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "npm-watch": "^0.11.0" }, "scripts": { - "lint-apps": "node bin/sync-lint-exemptions.mjs && eslint ./apps", - "lint-modules": "eslint ./modules", + "lint-apps": "node bin/sync-lint-exemptions.mjs && eslint --max-warnings 0 ./apps", + "lint-modules": "eslint --max-warnings 0 ./modules", "test": "node bin/sanitycheck.js && npm run lint-apps && npm run lint-modules", "fix": "eslint --fix ./apps ./modules", "update-local-apps": "./bin/create_apps_json.sh apps.local.json", diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts index c1b1433a5..7c3883f0f 100644 --- a/typescript/types/main.d.ts +++ b/typescript/types/main.d.ts @@ -127,6 +127,7 @@ type MenuBooleanItem = { onchange?: (value: boolean) => void; }; +<<<<<<< HEAD /** * Menu item that holds a numerical value. */ @@ -183,6 +184,48 @@ type MenuInstance = { move: (n: number) => void; select: () => void; }; +||||||| a038233fa +type NRFSecurityStatus = { + advertising: boolean, +} & ( + { + connected: true, + encrypted: boolean, + mitm_protected: boolean, + bonded: boolean, + connected_addr?: string, + } | { + connected: false, + encrypted: false, + mitm_protected: false, + bonded: false, + } +); +======= +type NRFSecurityStatus = { + advertising: boolean, + privacy?: ShortBoolean | { + mode: "off" + } | { + mode: "device_privacy" | "network_privacy", + addr_type: "random_private_resolvable" | "random_private_non_resolvable", + addr_cycle_s: number, + }, +} & ( + { + connected: true, + encrypted: boolean, + mitm_protected: boolean, + bonded: boolean, + connected_addr?: string, + } | { + connected: false, + encrypted: false, + mitm_protected: false, + bonded: false, + } +); +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 type ImageObject = { width: number; @@ -291,6 +334,7 @@ type VariableSizeInformation = { more?: VariableSizeInformation; }; +<<<<<<< HEAD type PinMode = | "analog" | "input" @@ -309,6 +353,51 @@ interface ArrayLike { type IntervalId = number & { _brand: "interval" }; type TimeoutId = number & { _brand: "timeout" }; +||||||| a038233fa +======= +type PowerUsage = { + total: number, + device: { + CPU?: number, + UART?: number, + PWM?: number, + LED1?: number, + LED2?: number, + LED3?: number, + + // bangle + LCD?: number, + LCD_backlight?: number, + LCD_touch?: number, + HRM?: number, + GPS?: number, + compass?: number, + baro?: number, + + // nrf + BLE_periph?: number, + BLE_central?: number, + BLE_advertise?: number, + BLE_scan?: number, + + // pixljs + //LCD?: number, // (see above) + + // puck + mag?: number, + accel?: number, + + // jolt + driver0?: number, + driver1?: number, + pin0_internal_resistance?: number, + pin2_internal_resistance?: number, + pin4_internal_resistance?: number, + pin6_internal_resistance?: number, + }, +}; + +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 type PipeOptions = { chunkSize?: number, end?: boolean, @@ -2517,13 +2606,15 @@ declare class NRF { static eraseBonds(callback?: any): void; /** - * Get this device's default Bluetooth MAC address. + * Get this device's default or current Bluetooth MAC address. * For Puck.js, the last 5 characters of this (e.g. `ee:ff`) are used in the * device's advertised Bluetooth name. + * + * @param {boolean} current - If true, return the current address rather than the default * @returns {any} MAC address - a string of the form 'aa:bb:cc:dd:ee:ff' * @url http://www.espruino.com/Reference#l_NRF_getAddress */ - static getAddress(): any; + static getAddress(current: boolean): any; /** * Set this device's default Bluetooth MAC address: @@ -3567,6 +3658,9 @@ declare class NRF { * encryptUart : bool // default false (unless oob or passkey specified) * // This sets the BLE UART service such that it * // is encrypted and can only be used from a paired connection + * privacy : // default false, true to enable with (ideally sensible) defaults, + * // or an object defining BLE privacy / random address options - see below for more info + * // only available if Espruino was compiled with private address support (like for example on Bangle.js 2) * }); * ``` * **NOTE:** Some combinations of arguments will cause an error. For example @@ -3631,6 +3725,42 @@ declare class NRF { * **Note:** If `passkey` or `oob` is specified, the Nordic UART service (if * enabled) will automatically be set to require encryption, but otherwise it is * open. + * On Bangle.js 2, the `privacy` parameter can be used to set this device's BLE privacy / random address settings. + * The privacy feature provides a way to avoid being tracked over a period of time. + * This works by replacing the real BLE address with a random private address, + * that automatically changes at a specified interval. + * If a `"random_private_resolvable"` address is used, that address is generated with the help + * of an identity resolving key (IRK), that is exchanged during bonding. + * This allows a bonded device to still identify another device that is using a random private resolvable address. + * Note that, while this can help against being tracked, there are other ways a Bluetooth device can reveal its identity. + * For example, the name or services it advertises may be unique enough. + * ``` + * NRF.setSecurity({ + * privacy: { + * mode : "off"/"device_privacy"/"network_privacy" // The privacy mode that should be used. + * addr_type : "random_private_resolvable"/"random_private_non_resolvable" // The type of address to use. + * addr_cycle_s : int // How often the address should change, in seconds. + * } + * }); + * // enabled with (ideally sensible) defaults of: + * // mode: device_privacy + * // addr_type: random_private_resolvable + * // addr_cycle_s: 0 (use default address change interval) + * NRF.setSecurity({ + * privacy: 1 + * }); + * ``` + * `mode` can be one of: + * * `"off"` - Use the real address. + * * `"device_privacy"` - Use a private address. + * * `"network_privacy"` - Use a private address, + * and reject a peer that uses its real address if we know that peer's IRK. + * If `mode` is `"off"`, all other fields are ignored and become optional. + * `addr_type` can be one of: + * * `"random_private_resolvable"` - Address that can be resolved by a bonded peer that knows our IRK. + * * `"random_private_non_resolvable"` - Address that cannot be resolved. + * `addr_cycle_s` must be an integer. Pass `0` to use the default address change interval. + * The default is usually to change the address every 15 minutes (or 900 seconds). * * @param {any} options - An object containing security-related options (see below) * @url http://www.espruino.com/Reference#l_NRF_setSecurity @@ -3648,6 +3778,8 @@ declare class NRF { * bonded // The peer is bonded with us * advertising // Are we currently advertising? * connected_addr // If connected=true, the MAC address of the currently connected device + * privacy // Current BLE privacy / random address settings. + * // Only present if Espruino was compiled with private address support (like for example on Bangle.js 2). * } * ``` * If there is no active connection, `{connected:false}` will be returned. @@ -6772,7 +6904,7 @@ declare class Bangle { /** * This function can be used to adjust the brightness of Bangle.js's display, and * hence prolong its battery life. - * Due to hardware design constraints, software PWM has to be used which means that + * Due to hardware design constraints on Bangle.js 1, software PWM has to be used which means that * the display may flicker slightly when Bluetooth is active and the display is not * at full power. * **Power consumption** @@ -6782,6 +6914,8 @@ declare class Bangle { * * 0.5 = 28mA * * 0.9 = 40mA (switching overhead) * * 1 = 40mA + * In 2v21 and earlier, this function would erroneously turn the LCD backlight on. 2v22 and later + * fix this, and if you want the backlight on your should use `Bangle.setLCDPowerBacklight()` * * @param {number} brightness - The brightness of Bangle.js's display - from 0(off) to 1(on full) * @url http://www.espruino.com/Reference#l_Bangle_setLCDBrightness @@ -6841,39 +6975,38 @@ declare class Bangle { * GwIKCngWC14sB7QKCh4CBCwN/64KDgfACwWn6vWGwYsBCwOputWJgYsCgGqytVBQYsCLYOlqtqwAsFEINVrR4BFgghBBQosDEINWIQ * YsDEIQ3DFgYhCG4msSYeVFgnrFhMvOAgsEkE/FhEggYWCFgIhDkEACwQKBEIYKBCwSGFBQJxCQwYhBBQTKDqohCBQhCCEIJlDXwrKE * BQoWHBQdaCwuqJoI4CCwgKECwJ9CJgIKDq+qBYUq1WtBQf+BYIAC3/VBQX/tQKDz/9BQY5BAAVV/4WCBQJcBKwVf+oHBv4wCAAYhB`)); - * Bangle.setLCDOverlay(img,66,66); + * Bangle.setLCDOverlay(img,66,66, {id: "myOverlay", remove: () => print("Removed")}); * ``` * Or use a `Graphics` instance: * ``` - * var ovr = Graphics.createArrayBuffer(100,100,1,{msb:true}); // 1bpp - * ovr.drawLine(0,0,100,100); - * ovr.drawRect(0,0,99,99); - * Bangle.setLCDOverlay(ovr,38,38); - * ``` - * Although `Graphics` can be specified directly, it can often make more sense to - * create an Image from the `Graphics` instance, as this gives you access - * to color palettes and transparent colors. For instance this will draw a colored - * overlay with rounded corners: - * ``` * var ovr = Graphics.createArrayBuffer(100,100,2,{msb:true}); + * ovr.transparent = 0; // (optional) set a transparent color + * ovr.palette = new Uint16Array([0,0,g.toColor("#F00"),g.toColor("#FFF")]); // (optional) set a color palette * ovr.setColor(1).fillRect({x:0,y:0,w:99,h:99,r:8}); * ovr.setColor(3).fillRect({x:2,y:2,w:95,h:95,r:7}); * ovr.setColor(2).setFont("Vector:30").setFontAlign(0,0).drawString("Hi",50,50); - * Bangle.setLCDOverlay({ - * width:ovr.getWidth(), height:ovr.getHeight(), - * bpp:2, transparent:0, - * palette:new Uint16Array([0,0,g.toColor("#F00"),g.toColor("#FFF")]), - * buffer:ovr.buffer - * },38,38); + * Bangle.setLCDOverlay(ovr,38,38, {id: "myOverlay", remove: () => print("Removed")}); * ``` + * To remove an overlay, simply call: + * ``` + * Bangle.setLCDOverlay(undefined, {id: "myOverlay"}); + * ``` + * Before 2v22 the `options` object isn't parsed, and as a result + * the remove callback won't be called, and `Bangle.setLCDOverlay(undefined)` will + * remove *any* active overlay. + * The `remove` callback is called when the current overlay is removed or replaced with + * another, but *not* if setLCDOverlay is called again with an image and the same ID. * * @param {any} img - An image, or undefined to clear - * @param {number} x - The X offset the graphics instance should be overlaid on the screen with + * @param {any} x - The X offset the graphics instance should be overlaid on the screen with * @param {number} y - The Y offset the graphics instance should be overlaid on the screen with + * @param {any} options - [Optional] object `{remove:fn, id:"str"}` * @url http://www.espruino.com/Reference#l_Bangle_setLCDOverlay */ static setLCDOverlay(img: any, x: number, y: number): void; static setLCDOverlay(): void; + static setLCDOverlay(img: any, x: number, y: number, options: { id : string, remove: () => void }): void; + static setLCDOverlay(img: any, options: { id : string }): void; /** * This function can be used to turn Bangle.js's LCD power saving on or off. @@ -6943,6 +7076,10 @@ declare class Bangle { * current value. If you desire a specific interval (e.g. the default 80ms) you * must set it manually with `Bangle.setPollInterval(80)` after setting * `powerSave:false`. + * * `lowResistanceFix` (Bangle.js 2, 2v22+) In the very rare case that your watch button + * gets damaged such that it has a low resistance and always stays on, putting the watch + * into a boot loop, setting this flag may improve matters (by forcing the input low + * before reading and disabling the hardware watch on BTN1). * * `lockTimeout` how many milliseconds before the screen locks * * `lcdPowerTimeout` how many milliseconds before the screen turns off * * `backlightTimeout` how many milliseconds before the screen's backlight turns @@ -6981,6 +7118,7 @@ declare class Bangle { /** * Also see the `Bangle.lcdPower` event + * You can use `Bangle.setLCDPower` to turn on the LCD (on Bangle.js 2 the LCD is normally on, and draws very little power so can be left on). * @returns {boolean} Is the display on or not? * @url http://www.espruino.com/Reference#l_Bangle_isLCDOn */ @@ -6988,6 +7126,7 @@ declare class Bangle { /** * Also see the `Bangle.backlight` event + * You can use `Bangle.setLCDPowerBacklight` to turn on the LCD backlight. * @returns {boolean} Is the backlight on or not? * @url http://www.espruino.com/Reference#l_Bangle_isBacklightOn */ @@ -7207,6 +7346,62 @@ declare class Bangle { static D38: any; /** +<<<<<<< HEAD +||||||| a038233fa + * Writes a register on the accelerometer + * + * @param {number} reg + * @param {number} data + * @url http://www.espruino.com/Reference#l_Bangle_accelWr + */ + static accelWr(reg: number, data: number): void; + + /** + * Reads a register from the accelerometer + * **Note:** On Espruino 2v06 and before this function only returns a number (`cnt` + * is ignored). + * + * @param {number} reg + * @param {number} cnt - If specified, returns an array of the given length (max 128). If not (or 0) it returns a number +======= + * Writes a register on the touch controller + * + * @param {number} reg + * @param {number} data + * @url http://www.espruino.com/Reference#l_Bangle_touchWr + */ + static touchWr(reg: number, data: number): void; + + /** + * Reads a register from the touch controller + * **Note:** On Espruino 2v06 and before this function only returns a number (`cnt` + * is ignored). + * + * @param {number} reg - Register number to read + * @param {number} cnt - If specified, returns an array of the given length (max 128). If not (or 0) it returns a number + * @returns {any} + * @url http://www.espruino.com/Reference#l_Bangle_touchRd + */ + static touchRd(reg: number, cnt?: 0): number; + static touchRd(reg: number, cnt: number): number[]; + + /** + * Writes a register on the accelerometer + * + * @param {number} reg - Register number to write + * @param {number} data - An integer value to write to the register + * @url http://www.espruino.com/Reference#l_Bangle_accelWr + */ + static accelWr(reg: number, data: number): void; + + /** + * Reads a register from the accelerometer + * **Note:** On Espruino 2v06 and before this function only returns a number (`cnt` + * is ignored). + * + * @param {number} reg + * @param {number} cnt - If specified, returns an array of the given length (max 128). If not (or 0) it returns a number +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 * @returns {any} * @url http://www.espruino.com/Reference#l_WioLTE_D20 */ @@ -9095,19 +9290,21 @@ interface String { * like Bangle.js. Maximum layer count right now is 4. * ``` * layers = [ { - * {x : int, // x start position - * y : int, // y start position - * image : string/object, + * {x : float, // x start position + * y : float, // y start position + * image : string/object/Graphics, * scale : float, // scale factor, default 1 * rotate : float, // angle in radians * center : bool // center on x,y? default is top left * repeat : should this image be repeated (tiled?) * nobounds : bool // if true, the bounds of the image are not used to work out the default area to draw + * palette : new Uint16Array(2/4/8/16/256) // (2v22+) a color palette to use with the image (overrides the image's palette) + * compose : ""/"add"/"or"/"xor" // (2v22+) if set, the operation used when combining with the previous layer * } * ] - * options = { // the area to render. Defaults to rendering just enough to cover what's requested - * x,y, - * width,height + * options = { + * x,y, : int // the area to render. Defaults to rendering just enough to cover what's requested + * width,height : int * } * ``` * @@ -9130,9 +9327,15 @@ interface String { * * Is 8 bpp *OR* the `{msb:true}` option was given * * No other format options (zigzag/etc) were given * Otherwise data will be copied, which takes up more space and may be quite slow. - * If the `Graphics` object contains `transparent` or `pelette` fields, + * If the `Graphics` object contains `transparent` or `palette` fields, * [as you might find in an image](http://www.espruino.com/Graphics#images-bitmaps), * those will be included in the generated image too. + * ``` + * var gfx = Graphics.createArrayBuffer(8,8,1); + * gfx.transparent = 0; + * gfx.drawString("X",0,0); + * var im = gfx.asImage("string"); + * ``` * * @param {any} type - The type of image to return. Either `object`/undefined to return an image object, or `string` to return an image string * @returns {any} An Image that can be used with `Graphics.drawImage` @@ -9282,6 +9485,7 @@ interface String { * @returns {any} A match array or `null` (see below): * @url http://www.espruino.com/Reference#l_String_match */ +<<<<<<< HEAD match(substr: any): any; /** @@ -9436,6 +9640,41 @@ interface String { * @url http://www.espruino.com/Reference#l_String_padEnd */ padEnd(targetLength: number, padString?: any): string; +||||||| a038233fa + setTheme(theme: { [key in keyof Theme]?: Theme[key] extends number ? ColorResolvable : Theme[key] }): Graphics; +======= + setTheme(theme: { [key in keyof Theme]?: Theme[key] extends number ? ColorResolvable : Theme[key] }): Graphics; + + /** + * Perform a filter on the current Graphics instance. Requires the Graphics + * instance to support readback (eg `getPixel` should work), and only uses + * 8 bit values for buffer and filter. + * ``` + * g.filter([ // a gaussian filter + * 1, 4, 7, 4, 1, + * 4,16,26,16, 4, + * 7,26,41,26, 7, + * 4,16,26,16, 4, + * 1, 4, 7, 4, 1 + * ], { w:5, h:5, div:273 }); + * ``` + * ``` + * { + * w,h, // filter width+height + * div, // divisor applied after filter + * offset, // DC offset applied to filter before division (default 0) + * max, // maximum output value (default=max allowed by bpp) + * filter, // undefined (replace), or "max" (use max(original,filtered)) + * } + * ``` + * + * @param {any} filter - An array of filter params between -128 and 127 (2D arrays should be unwrapped) + * @param {any} options - An object of options, see below + * @returns {any} The instance of Graphics this was called on, to allow call chaining + * @url http://www.espruino.com/Reference#l_Graphics_filter + */ + filter(filter: any, options: any): Graphics; +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 } /** @@ -10717,7 +10956,7 @@ declare class E { * E.showScroller({ * h : 40, c : 8, * draw : (idx, r) => { - * g.setBgColor((idx&1)?"#666":"#999").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1); + * g.setBgColor((idx&1)?"#666":"#CCC").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1); * g.setFont("6x8:2").drawString("Item Number\n"+idx,r.x+10,r.y+4); * }, * select : (idx) => console.log("You selected ", idx) @@ -12061,7 +12300,7 @@ declare class E { * @returns {any} An object detailing power usage in microamps * @url http://www.espruino.com/Reference#l_E_getPowerUsage */ - static getPowerUsage(): any; + static getPowerUsage(): PowerUsage; /** * Decode a UTF8 string. @@ -12965,7 +13204,13 @@ interface Object { toString(radix?: any): string; /** - * Copy this object completely + * Copy this object to a new object, but as a shallow copy. This has a similar effect to calling `Object.assign({}, obj)`. + * ``` + * orig = { a : 1, b : [ 2, 3 ] } + * copy = orig.clone(); + * // copy = { a : 1, b : [ 2, 3 ] } + * ``` + * **Note:** This is not a standard JavaScript function, but is unique to Espruino * @returns {any} A copy of this Object * @url http://www.espruino.com/Reference#l_Object_clone */ @@ -13203,12 +13448,12 @@ interface JSONConstructor { * not as if they were objects (since it is more compact) * * @param {any} data - The data to be converted to a JSON string - * @param {any} replacer - This value is ignored - * @param {any} space - The number of spaces to use for padding, a string, or null/undefined for no whitespace + * @param {any} [replacer] - [optional] This value is ignored + * @param {any} [space] - [optional] The number of spaces to use for padding, a string, or null/undefined for no whitespace * @returns {any} A JSON string * @url http://www.espruino.com/Reference#l_JSON_stringify */ - stringify(data: any, replacer: any, space: any): any; + stringify(data: any, replacer?: any, space?: any): any; /** * Parse the given JSON string into a JavaScript object @@ -15433,23 +15678,59 @@ interface StringConstructor { /** * Read a byte * +<<<<<<< HEAD * @param {any} [count] - [optional] The amount of bytes to read * @returns {any} The byte that was read, or a Uint8Array if count was specified and >=0 * @url http://www.espruino.com/Reference#l_OneWire_read +||||||| a038233fa + * @param {any} substring - The string to search for + * @param {any} fromIndex - Index to search from + * @returns {number} The index of the string, or -1 if not found + * @url http://www.espruino.com/Reference#l_String_indexOf +======= + * @param {any} substring - The string to search for + * @param {any} [fromIndex] - [optional] Index to search from + * @returns {number} The index of the string, or -1 if not found + * @url http://www.espruino.com/Reference#l_String_indexOf +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 */ +<<<<<<< HEAD read(count?: any): any; +||||||| a038233fa + indexOf(substring: any, fromIndex: any): number; +======= + indexOf(substring: any, fromIndex?: any): number; +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 /** * Search for devices * +<<<<<<< HEAD * @param {number} command - (Optional) command byte. If not specified (or zero), this defaults to 0xF0. This can could be set to 0xEC to perform a DS18B20 'Alarm Search Command' * @returns {any} An array of devices that were found * @url http://www.espruino.com/Reference#l_OneWire_search +||||||| a038233fa + * @param {any} substring - The string to search for + * @param {any} fromIndex - Index to search from + * @returns {number} The index of the string, or -1 if not found + * @url http://www.espruino.com/Reference#l_String_lastIndexOf +======= + * @param {any} substring - The string to search for + * @param {any} [fromIndex] - [optional] Index to search from + * @returns {number} The index of the string, or -1 if not found + * @url http://www.espruino.com/Reference#l_String_lastIndexOf +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 */ +<<<<<<< HEAD <<<<<<< HEAD search(command: number): any; ======= lastIndexOf(substring: any, fromIndex: any): number; +||||||| a038233fa + lastIndexOf(substring: any, fromIndex: any): number; +======= + lastIndexOf(substring: any, fromIndex?: any): number; +>>>>>>> e51f7bfbe795f5170372a863e76c82d382e66344 /** * Matches an occurrence `subStr` in the string. @@ -15504,20 +15785,20 @@ interface StringConstructor { /** * * @param {number} start - The start character index (inclusive) - * @param {any} end - The end character index (exclusive) + * @param {any} [end] - [optional] The end character index (exclusive) * @returns {any} The part of this string between start and end * @url http://www.espruino.com/Reference#l_String_substring */ - substring(start: number, end: any): any; + substring(start: number, end?: any): any; /** * * @param {number} start - The start character index - * @param {any} len - The number of characters + * @param {any} [len] - [optional] The number of characters * @returns {any} Part of this string from start for len characters * @url http://www.espruino.com/Reference#l_String_substr */ - substr(start: number, len: any): any; + substr(start: number, len?: any): any; /** * @@ -15584,29 +15865,29 @@ interface StringConstructor { /** * * @param {any} searchString - The string to search for - * @param {number} position - The start character index (or 0 if not defined) + * @param {number} [position] - [optional] The start character index (or 0 if not defined) * @returns {boolean} `true` if the given characters are found at the beginning of the string, otherwise, `false`. * @url http://www.espruino.com/Reference#l_String_startsWith */ - startsWith(searchString: any, position: number): boolean; + startsWith(searchString: any, position?: number): boolean; /** * * @param {any} searchString - The string to search for - * @param {any} length - The 'end' of the string - if left off the actual length of the string is used + * @param {any} [length] - [optional] The 'end' of the string - if left off the actual length of the string is used * @returns {boolean} `true` if the given characters are found at the end of the string, otherwise, `false`. * @url http://www.espruino.com/Reference#l_String_endsWith */ - endsWith(searchString: any, length: any): boolean; + endsWith(searchString: any, length?: any): boolean; /** * * @param {any} substring - The string to search for - * @param {any} fromIndex - The start character index (or 0 if not defined) + * @param {any} [fromIndex] - [optional] The start character index (or 0 if not defined) * @returns {boolean} `true` if the given characters are in the string, otherwise, `false`. * @url http://www.espruino.com/Reference#l_String_includes */ - includes(substring: any, fromIndex: any): boolean; + includes(substring: any, fromIndex?: any): boolean; /** * Repeat this string the given number of times. @@ -15709,8 +15990,10 @@ interface RegExp { /** * The built-in class for handling Regular Expressions * **Note:** Espruino's regular expression parser does not contain all the features - * present in a full ES6 JS engine. However it does contain support for the all the - * basics. + * present in a full ES6 JS engine. however some parts of the spec are not implemented: + * * [Assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Assertions) other than `^` and `$` + * * [Numeric quantifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Quantifiers) (eg `x{3}`) + * There's a GitHub issue [concerning RegExp features here](https://github.com/espruino/Espruino/issues/1257) * @url http://www.espruino.com/Reference#RegExp */ declare const RegExp: RegExpConstructor @@ -17520,7 +17803,7 @@ declare function shiftOut(pins: Pin | Pin[], options: { clk?: Pin, clkPol?: bool * * @param {any} function - A Function or String to be executed * @param {Pin} pin - The pin to watch - * @param {any} options - If a boolean or integer, it determines whether to call this once (false = default) or every time a change occurs (true). Can be an object of the form `{ repeat: true/false(default), edge:'rising'/'falling'/'both'(default), debounce:10}` - see below for more information. + * @param {any} options - If a boolean or integer, it determines whether to call this once (false = default) or every time a change occurs (true). Can be an object of the form `{ repeat: true/false(default), edge:'rising'/'falling'/'both', debounce:10}` - see below for more information. * @returns {any} An ID that can be passed to clearWatch * @url http://www.espruino.com/Reference#l__global_setWatch */ diff --git a/typescript/types/modules.d.ts b/typescript/types/modules.d.ts index ad3612117..e8aa15ac1 100644 --- a/typescript/types/modules.d.ts +++ b/typescript/types/modules.d.ts @@ -5,3 +5,4 @@ declare function require(moduleName: "sched"): typeof Sched; declare function require(moduleName: "ClockFace"): typeof ClockFace_.ClockFace; declare function require(moduleName: "clock_info"): typeof ClockInfo; declare function require(moduleName: "Layout"): typeof Layout.Layout; +declare function require(moduleName: "power_usage"): PowerUsageModule; diff --git a/typescript/types/settings.d.ts b/typescript/types/settings.d.ts index d7dd23aae..adeb7be85 100644 --- a/typescript/types/settings.d.ts +++ b/typescript/types/settings.d.ts @@ -5,6 +5,8 @@ type Settings = { ble: boolean, blerepl: boolean, + bleprivacy?: NRFSecurityStatus["privacy"], + blename?: boolean, HID?: false | "kbmedia" | "kb" | "com" | "joy", passkey?: string,