diff --git a/apps.json b/apps.json index b0ebce263..803534d77 100644 --- a/apps.json +++ b/apps.json @@ -2,7 +2,7 @@ { "id": "fwupdate", "name": "Firmware Update", - "version": "0.02", + "version": "0.03", "description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates", "icon": "app.png", "type": "RAM", @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.39", + "version": "0.40", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -768,7 +768,7 @@ "id": "recorder", "name": "Recorder (BETA)", "shortName": "Recorder", - "version": "0.05", + "version": "0.06", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -845,7 +845,7 @@ { "id": "weather", "name": "Weather", - "version": "0.14", + "version": "0.15", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], @@ -936,7 +936,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", - "version": "0.15", + "version": "0.16", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "icon": "widget.png", "type": "widget", @@ -1040,16 +1040,19 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.01", + "version": "0.02", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", - "type": "boot", + "type": "app", "tags": "health,bluetooth", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ + {"name":"bthrm.app.js","url":"bthrm.js"}, + {"name":"bthrm.recorder.js","url":"recorder.js"}, {"name":"bthrm.boot.js","url":"boot.js"}, - {"name":"bthrm.img","url":"app-icon.js","evaluate":true} + {"name":"bthrm.img","url":"app-icon.js","evaluate":true}, + {"name":"bthrm.settings.js","url":"settings.js"} ] }, { @@ -1324,7 +1327,7 @@ "icon": "gesture.png", "type": "app", "tags": "gesture,ai", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"gesture.app.js","url":"gesture.js"}, {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, @@ -1501,7 +1504,7 @@ { "id": "gpsinfo", "name": "GPS Info", - "version": "0.06", + "version": "0.08", "description": "An application that displays information about altitude, lat/lon, satellites and time", "icon": "gps-info.png", "type": "app", @@ -1590,7 +1593,7 @@ { "id": "widpedom", "name": "Pedometer widget", - "version": "0.20", + "version": "0.22", "description": "Daily pedometer widget", "icon": "widget.png", "type": "widget", @@ -2430,7 +2433,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.05", + "version": "0.06", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], @@ -3533,7 +3536,7 @@ "id": "mclockplus", "name": "Morph Clock+", "shortName": "Morph Clock+", - "version": "0.02", + "version": "0.03", "description": "Morphing Clock with more readable seconds and date and additional stopwatch", "icon": "mclockplus.png", "type": "clock", @@ -3987,11 +3990,12 @@ "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "storage": [ - {"name":"doztime.app.js","url":"app.js"}, + {"name":"doztime.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]}, + {"name":"doztime.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}, {"name":"doztime.img","url":"app-icon.js","evaluate":true} ] }, @@ -4238,8 +4242,9 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.03", - "description": "A simple clock using the bold Anton font.", + "version": "0.04", + "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", + "readme":"README.md", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", @@ -4248,8 +4253,10 @@ "allow_emulator": true, "storage": [ {"name":"antonclk.app.js","url":"app.js"}, + {"name":"antonclk.settings.js","url":"settings.js"}, {"name":"antonclk.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"antonclk.json"}] }, { "id": "waveclk", @@ -4473,7 +4480,7 @@ "name": "A Battery Widget (with percentage)", "shortName":"A Battery Widget", "icon": "widget.png", - "version":"1.02", + "version":"1.03", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", @@ -4488,7 +4495,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.08", + "version":"0.09", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", @@ -4983,7 +4990,7 @@ { "id": "pooqround", "name": "pooq Round watch face", "shortName":"pooq Round", - "version":"0.01", + "version":"0.02", "description": "A 24 hour analogue watchface with high legibility and a novel style.", "icon": "app.png", "type": "clock", @@ -5139,6 +5146,24 @@ {"name":"fourTwenty","url":"fourTwenty.js"}, {"name":"fourTwentyTz","url":"fourTwentyTz.js"}, {"name":"ftclock.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "presentor", + "name": "Presentor", + "version": "3.0", + "description": "Use your Bangle to present!", + "icon": "app.png", + "type": "app", + "tags": "tool,bluetooth", + "interface": "interface.html", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"presentor.app.js","url":"app.js"}, + {"name":"presentor.img","url":"app-icon.js","evaluate":true}, + {"name":"presentor.json","url":"settings.json"} ] }, { @@ -5162,15 +5187,16 @@ { "id": "promenu", "name": "Pro Menu", - "version": "0.01", - "description": "Replace Bangle.js 1's built in menu function.", + "version": "0.02", + "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "icon": "icon.png", "type": "boot", "tags": "system", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"pro-menu-screenshot.png"}], "storage": [ - {"name":"promenu.boot.js","url":"boot.js"}, + {"name":"promenu.boot.js","url":"boot.js","supports": ["BANGLEJS"]}, + {"name":"promenu.boot.js","url":"bootb2.js","supports": ["BANGLEJS2"]}, {"name":"promenu.img","url":"promenuIcon.js","evaluate":true} ] }, @@ -5408,5 +5434,55 @@ "storage": [ {"name":"touchmenu.boot.js","url":"touchmenu.boot.js"} ] + }, + { + "id": "puzzle15", + "name": "15 puzzle", + "version": "0.05", + "description": "A 15 puzzle game with drag gesture interface", + "readme":"README.md", + "icon": "puzzle15.app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"puzzle15.app.js","url":"puzzle15.app.js"}, + {"name":"puzzle15.settings.js","url":"puzzle15.settings.js"}, + {"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true} + ], + "data": [{"name":"puzzle15.json"}] + }, + { + "id": "flipper", + "name": "flipper", + "version": "0.01", + "description": "Switch between dark and light theme and vice versa, combine with pattern launcher and swipe to flip.", + "readme":"README.md", + "screenshots": [{"url":"flipper.png"}], + "icon": "flipper.png", + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"flipper.app.js","url":"flipper.app.js"}, + {"name":"flipper.img","url":"flipper.icon.js","evaluate":true} + ] + }, + { "id": "ruuviwatch", + "name": "Ruuvi Watch", + "shortName":"Ruuvi Watch", + "icon": "ruuviwatch.png", + "version":"1.01", + "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", + "readme":"README.md", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"}, + {"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true} + ] } ] diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index f88276a90..668047d7a 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Load widgets after setUI so widclk knows when to hide 0.03: Clock now shows day of week under date. +0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too. diff --git a/apps/antonclk/README.md b/apps/antonclk/README.md new file mode 100644 index 000000000..41d3e4559 --- /dev/null +++ b/apps/antonclk/README.md @@ -0,0 +1,77 @@ +# Anton Clock - Large font digital watch with seconds and date + +Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit. + +## Features + +The basic time representation only shows hours and minutes of the current time. However, Anton clock can show additional information: + +* Seconds can be shown, either always or only if the screen is unlocked. +* To help easy recognition, the seconds can be coloured in blue on the Bangle.js 2. +* Date can be shown in three different formats: + * ISO-8601: 2021-12-19 + * short local format: 19/12/2021, 19.12.2021 + * long local format: DEC 19 2021 +* Weekday can be shown (on seconds screen only instead of year) + +## Usage + +Install Anton clock through the Bangle.js app loader. +Configure it through the default Bangle.js configuration mechanism +(Settings app, "Apps" menu, "Anton clock" submenu). +If you like it, make it your default watch face +(Settings app, "System" menu, "Clock" submenu, select "Anton clock"). + +## Configuration + +Anton clock is configured by the standard settings mechanism of Bangle.js's operating system: +Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu. +You configure Anton clock through several "on/off" switches in two menus. + +### The main menu + +The main menu contains several settings covering Anton clock in general. + +* **Seconds...** - Opens the submenu for configuring the presentation of the current time's seconds. +* **Date** - Format of the date representation. Possible values are + * **Long** - "Long" date format in the current locale. Usually with the month as name, not number. + * **Short** - "Short" date format in the current locale. Usually with the month as number. + * **ISO8601** - Show the date in ISO-8601 format (YYYY-MM-DD), irrespective of the current locale. +* **Show Weekday** - Weekday is shown in the time presentation without seconds. +Weekday name depends on the current locale. +If seconds are shown, the weekday is never shown as there is not enough space on the watch face. +* **Uppercase** - Weekday name and month name in the long format are converted to upper case letters. +This can improve readability. +* **Vector font** - Use the built-in vector font for dates and weekday. +This can improve readability. +Otherwise, a scaled version of the built-in 6x8 pixels font is used. + +### The "Seconds" submenu + +The "Seconds" submenu configures how (and if) seconds are shown on the "Anton" watch face. + +* **Show** - Configure when the seconds should be shown at all: + * **Never** - Seconds are never shown. +In this case, hour and minute are a bit more centered on the screen and the clock will always only update every minute. +This saves battery power. + * **Unlocked** - Seconds are shown if the display is unlocked. +On locked displays, only hour, minutes, date and optionally the weekday are shown. +_This option is highly recommended on the Bangle.js 2!_ + * **Always** - Seconds are _always_ shown, irrespective of the display's unlock state. +_Enabling this option increases power consumption as the watch face will update once per second instead of once per minute._ +* **With ":"** - If enabled, a colon ":" is prepended to the seconds. +This resembles the usual time representation "hh:mm:ss", even though the seconds are printed on a separate line. +* **Color** - If enabled, seconds are shown in blue instead of black. +If the date is shown on the seconds screen, it is colored read instead of black. +This make the visual orientation much easier on the watch face. +* **Date** - It is possible to show the date together with the seconds: + * **No** - Date is _not_ shown in the seconds screen. +In this case, the seconds are centered below hour and minute. + * **Year** - Date is shown with day, month, and year. If "Date" in the main settings is configured to _ISO8601_, this is used here, too. Otherwise, the short local format is used. + * **Weekday** - Date is shown with day, month, and weekday. + +The date is coloured in red if the "Coloured" option is chosen. + +## Compatibility + +Anton clock makes use of core Bangle.js 2 features (coloured display, display lock state). It also runs on the Bangle.js 1 but these features are not available there due to hardware restrictions. diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js index 7912dfc0f..1f3e49792 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -1,61 +1,203 @@ +// Clock with large digits using the "Anton" bold font + +var SETTINGSFILE = "antonclk.json"; + Graphics.prototype.setFontAnton = function(scale) { -// Actual height 69 (68 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78+(scale<<8)+(1<<16)); + // Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16)); +}; + +Graphics.prototype.setFontAntonSmall = function(scale) { + // Actual height 53 (52 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16)); +}; + +// variables defined from settings +var secondsMode; +var secondsColoured; +var secondsWithColon; +var dateOnMain; +var dateOnSecs; +var weekDay; +var upperCase; +var vectorFont; + +// dynamic variables +var drawTimeout; +var queueMillis = 1000; +var secondsScreen = true; + +var isBangle1 = (g.getWidth() == 240); + +/* For development purposes +require('Storage').writeJSON(SETTINGSFILE, { + secondsMode: "Always", // "Never", "Unlocked", "Always" + secondsColoured: true, + secondsWithColon: true, + dateOnMain: "Long", // "Short", "Long", "ISO8601" + dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false + weekDay: true, + upperCase: true, + vectorFont: true, +}); +/* */ + +/* OR (also for development purposes) +require('Storage').erase(SETTINGSFILE); +/* */ + +// Helper method for loading the settings +function def(value, def) { + return (value !== undefined ? value : def); } -// timeout used to update every minute -var drawTimeout; +// Load settings +function loadSettings() { + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + secondsMode = def(settings.secondsMode, "Never"); + secondsColoured = def(settings.secondsColoured, true); + secondsWithColon = def(settings.secondsWithColon, true); + dateOnMain = def(settings.dateOnMain, "Long"); + dateOnSecs = def(settings.dateOnSecs, "Year"); + weekDay = def(settings.weekDay, true); + upperCase = def(settings.upperCase, true); + vectorFont = def(settings.vectorFont, false); -// schedule a draw for the next minute + // Legacy + if (dateOnSecs === true) + dateOnSecs = "Year"; + if (dateOnSecs === false) + dateOnSecs = "No"; +} + +// schedule a draw for the next second or minute function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); - }, 60000 - (Date.now() % 60000)); + }, queueMillis - (Date.now() % queueMillis)); } - -function draw() { - var x = g.getWidth()/2; - var y = g.getHeight()/2; - g.reset(); - var date = new Date(); - var timeStr = require("locale").time(date,1); - var dateStr = require("locale").date(date).toUpperCase(); - var dowStr = require("locale").dow(date).toUpperCase(); - // draw time - g.setFontAlign(0,0).setFont("Anton"); - g.clearRect(0,y-40,g.getWidth(),y+35); // clear the background - g.drawString(timeStr,x,y); - // draw date - y += 40; - g.setFontAlign(0,0).setFont("6x8",2); - g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background - g.drawString(dateStr,x,y); - //draw day of week - y += 16; - g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background - g.drawString(dowStr,x,y); - // queue draw in one minute - queueDraw(); -} - -// Clear the screen once, at startup -g.clear(); -// draw immediately at first, queue update -draw(); -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ - if (on) { +function updateState() { + if (Bangle.isLCDOn()) { + if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") { + secondsScreen = true; + queueMillis = 1000; + } else { + secondsScreen = false; + queueMillis = 60000; + } draw(); // draw immediately, queue redraw } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } +} + +function isoStr(date) { + return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).substr(-2) + "-" + ("0" + date.getDate()).substr(-2); +} + +function doColor() { + return !isBangle1 && !Bangle.isLocked() && secondsColoured; +} + +// Actually draw the watch face +function draw() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0)); + g.reset(); + /* This is to mark the widget areas during development. + g.setColor("#888") + .fillRect(0, 0, g.getWidth(), 23) + .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset(); + /* */ + g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets) + var date = new Date(); // Actually the current date, this one is shown + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time + if (secondsScreen) { + y += 65; + var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).substr(-2); + if (doColor()) + g.setColor(0, 0, 1); + g.setFont("AntonSmall"); + if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left + g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds + y -= (vectorFont ? 15 : 13); + x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2); + var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1)); + var year; + var md; + var yearfirst; + if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year + year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1)); + md = dateStr2.slice(0, -4); + if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12) + md = md.slice(0, -1); + yearfirst = false; + } else { // formatted date begins with year + if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected... + dateStr2 = isoStr(date); // ...use ISO date format instead + year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1)); + md = dateStr2.slice(5); // never keep separator directly after year + yearfirst = true; + } + if (dateOnSecs === "Weekday" && upperCase) + year = year.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + if (doColor()) + g.setColor(1, 0, 0); + g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y)); + g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16))); + } else { + g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered + } + } else { // No seconds screen: Show date and optionally day of week + y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40); + var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1))); + if (upperCase) + dateStr = dateStr.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + g.drawString(dateStr, x, y); + if (weekDay) { + var dowStr = require("locale").dow(date); + if (upperCase) + dowStr = dowStr.toUpperCase(); + g.drawString(dowStr, x, y + (vectorFont ? 26 : 16)); + } + } + + // queue next draw + queueDraw(); +} + +// Init the settings of the app +loadSettings(); +// Clear the screen once, at startup +g.clear(); +// Set dynamic state and perform initial drawing +updateState(); +// Register hooks for LCD on/off event and screen lock on/off event +Bangle.on('lcdPower', on => { + updateState(); +}); +Bangle.on('lock', on => { + updateState(); }); // Show launcher when middle button pressed Bangle.setUI("clock"); // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); + +// end of file \ No newline at end of file diff --git a/apps/antonclk/app.png b/apps/antonclk/app.png index d96f17758..a38093c5f 100644 Binary files a/apps/antonclk/app.png and b/apps/antonclk/app.png differ diff --git a/apps/antonclk/screenshot.png b/apps/antonclk/screenshot.png index c66f8bdd8..e949b8a24 100644 Binary files a/apps/antonclk/screenshot.png and b/apps/antonclk/screenshot.png differ diff --git a/apps/antonclk/settings.js b/apps/antonclk/settings.js new file mode 100644 index 000000000..08fde512e --- /dev/null +++ b/apps/antonclk/settings.js @@ -0,0 +1,99 @@ +// Settings menu for the enhanced Anton clock + +(function(back) { + var FILE = "antonclk.json"; + // Load settings + var settings = Object.assign({ + secondsOnUnlock: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "Anton clock" + }, + "< Back": () => back(), + "Seconds...": () => E.showMenu(secmenu), + "Date": stringInSettings("dateOnMain", ["Short", "Long", "ISO8601"]), + "Show Weekday": { + value: (settings.weekDay !== undefined ? settings.weekDay : true), + format: v => v ? "On" : "Off", + onchange: v => { + settings.weekDay = v; + writeSettings(); + } + }, + "Uppercase": { + value: (settings.upperCase !== undefined ? settings.upperCase : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.upperCase = v; + writeSettings(); + } + }, + "Vector font": { + value: (settings.vectorFont !== undefined ? settings.vectorFont : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.vectorFont = v; + writeSettings(); + } + }, + }; + + // Submenu + var secmenu = { + "": { + "title": "Show seconds..." + }, + "< Back": () => E.showMenu(mainmenu), + "Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]), + "With \":\"": { + value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.secondsWithColon = v; + writeSettings(); + } + }, + "Color": { + value: (settings.secondsColoured !== undefined ? settings.secondsColoured : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.secondsColoured = v; + writeSettings(); + } + }, + "Date": stringInSettings("dateOnSecs", ["No", "Year", "Weekday"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 5c929421b..d6619822b 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -43,3 +43,4 @@ 0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app 0.38: Option to log to file if settings.log==2 0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035) +0.40: Bootloader now rebuilds for new firmware versions diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index e338d9020..664d64ee7 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -6,11 +6,11 @@ var s = require('Storage').readJSON('setting.json',1)||{}; var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 var boot = ""; if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed - var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`; + var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } else { - var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/)); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`; + var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; boot += `E.setFlags({pretokenise:1});\n`; diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 5560f00bc..27a58dd78 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -1 +1,4 @@ 0.01: New App! +0.02: Make overriding the HRM event optional + Emit BTHRM event for external sensor + Add recorder app plugin diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index 88e574480..0aa8d5c96 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -2,24 +2,43 @@ var log = function() {};//print var gatt; var status; - - Bangle.isHRMOn = function() { + + var origIsHRMOn = Bangle.isHRMOn; + + Bangle.isBTHRMOn = function(){ return (status=="searching" || status=="connecting") || (gatt!==undefined); } - Bangle.setHRMPower = function(isOn, app) { + + Bangle.isHRMOn = function() { + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + + print(settings); + if (settings.enabled && !settings.replace){ + return origIsHRMOn(); + } else if (settings.enabled && settings.replace){ + return Bangle.isBTHRMOn(); + } + return origIsHRMOn() || Bangle.isBTHRMOn(); + } + + Bangle.setBTHRMPower = function(isOn, app) { + + + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + // Do app power handling if (!app) app="?"; - log("setHRMPower ->", isOn, app); + log("setBTHRMPower ->", isOn, app); if (Bangle._PWR===undefined) Bangle._PWR={}; - if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[]; - if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app); - if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app); - isOn = Bangle._PWR.HRM.length; + if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; + if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); + if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app); + isOn = Bangle._PWR.BTHRM.length; // so now we know if we're really on if (isOn) { - log("setHRMPower on", app); - if (!Bangle.isHRMOn()) { - log("HRM not already on"); + log("setBTHRMPower on", app); + if (!Bangle.isBTHRMOn()) { + log("BTHRM not already on"); status = "searching"; NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) { log("Found device "+device.id); @@ -49,7 +68,11 @@ if (flags&16) { var interval = dv.getUint16(idx,1); // in milliseconds }*/ - Bangle.emit('HRM',{ + + + var eventName = settings.replace ? "HRM" : "BTHRM"; + + Bangle.emit(eventName, { bpm:bpm, confidence:100 }); @@ -65,15 +88,27 @@ }); } } else { // not on - log("setHRMPower off", app); + log("setBTHRMPower off", app); if (gatt) { - log("HRM connected - disconnecting"); + log("BTHRM connected - disconnecting"); status = undefined; try {gatt.disconnect();}catch(e) { - log("HRM disconnect error", e); + log("BTHRM disconnect error", e); } gatt = undefined; } } }; + + var origSetHRMPower = Bangle.setHRMPower; + + Bangle.setHRMPower = function(isOn, app) { + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + if (settings.enabled || !isOn){ + Bangle.setBTHRMPower(isOn, app); + } + if (settings.enabled && !settings.replace || !isOn){ + origSetHRMPower(isOn, app); + } + } })(); diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js new file mode 100644 index 000000000..7c80c735f --- /dev/null +++ b/apps/bthrm/bthrm.js @@ -0,0 +1,61 @@ +var btm = g.getHeight()-1; +var eventInt = null; +var eventBt = null; +var counterInt = 0; +var counterBt = 0; + + +function draw(y, event, type, counter) { + var px = g.getWidth()/2; + g.reset(); + g.setFontAlign(0,0); + g.clearRect(0,y,g.getWidth(),y+80); + if (type == null || event == null || counter == 0) return; + var str = event.bpm + ""; + g.setFontVector(40).drawString(str,px,y+20); + str = "Confidence: " + event.confidence; + g.setFontVector(12).drawString(str,px,y+50); + str = "Event: " + type; + g.setFontVector(12).drawString(str,px,y+60); +} + +function onBtHrm(e) { + print("Event for BT " + JSON.stringify(e)); + counterBt += 5; + eventBt = e; +} + +function onHrm(e) { + print("Event for Int " + JSON.stringify(e)); + counterInt += 5; + eventInt = e; +} + +Bangle.on('BTHRM', onBtHrm); +Bangle.on('HRM', onHrm); + +Bangle.setHRMPower(1,'bthrm') +Bangle.setBTHRMPower(1,'bthrm') + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.reset().setFont("6x8",2).setFontAlign(0,0); +g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); + +function drawInt(){ + counterInt--; + if (counterInt < 0) counterInt = 0; + if (counterInt > 5) counterInt = 5; + draw(24, eventInt, "HRM", counterInt); +} +function drawBt(){ + counterBt--; + if (counterBt < 0) counterBt = 0; + if (counterBt > 5) counterBt = 5; + draw(100, eventBt, "BTHRM", counterBt); +} + +var interval = setInterval(drawInt, 1000); +var interval = setInterval(drawBt, 1000); diff --git a/apps/bthrm/recorder.js b/apps/bthrm/recorder.js new file mode 100644 index 000000000..40f64a676 --- /dev/null +++ b/apps/bthrm/recorder.js @@ -0,0 +1,27 @@ +(function(recorders) { + recorders.bthrm = function() { + var bpm = 0; + function onHRM(h) { + bpm = h.bpm; + } + return { + name : "BTHR", + fields : ["BT Heartrate"], + getValues : () => { + result = [bpm]; + bpm = 0; + return result; + }, + start : () => { + Bangle.on('BTHRM', onHRM); + Bangle.setBTHRMPower(1,"recorder"); + }, + stop : () => { + Bangle.removeListener('BTHRM', onHRM); + Bangle.setBTHRMPower(0,"recorder"); + }, + draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) + }; + } +}) + diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js new file mode 100644 index 000000000..8cb00614e --- /dev/null +++ b/apps/bthrm/settings.js @@ -0,0 +1,33 @@ +(function(back) { + var FILE = "bthrm.json"; + + var settings = Object.assign({ + enabled: true, + replace: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Bluetooth HRM' }, + '< Back': back, + 'Use BT HRM': { + value: !!settings.enabled, + format: v => settings.enabled ? "On" : "Off", + onchange: v => { + settings.enabled = v; + writeSettings(); + } + }, + 'Use HRM event': { + value: !!settings.replace, + format: v => settings.replace ? "On" : "Off", + onchange: v => { + settings.replace = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 3deec00e0..beba4ed95 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -3,3 +3,4 @@ 0.03: Add setting to start week on Sunday 0.04: Add setting to switch color schemes. On Bangle 2 non-dithering colors will be used by default. Use localized names for months and days of the week (Language app needed). 0.05: Update calendar weekend colors for start on Sunday +0.06: Use larger font for dates diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 00e7d54cc..62702e349 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -206,6 +206,8 @@ function drawCalendar(date) { y2 - 1 ); } + require("Font8x12").add(Graphics); + g.setFont("8x12", fontSize); g.setColor(day < 50 ? fgOtherMonth : fgSameMonth); g.drawString( (day > 50 ? day - 50 : day).toString(), diff --git a/apps/calendar/screenshot_calendar.png b/apps/calendar/screenshot_calendar.png index 8285932c4..7ef5986d4 100644 Binary files a/apps/calendar/screenshot_calendar.png and b/apps/calendar/screenshot_calendar.png differ diff --git a/apps/doztime/README.md b/apps/doztime/README.md index 075b2f66a..2f5b04780 100644 --- a/apps/doztime/README.md +++ b/apps/doztime/README.md @@ -11,4 +11,4 @@ The year itself begins on the December solstice. Because that always happens, th The epoch (year numbering) begins in the last year when the perihelion coincided with the June solstice, near the beginning of the Holocene era. That astronomical basis makes the calendar free from politics, religion, or geography. -While the year number remains cardinal, BTN5 toggles between cardinal and ordinal for the rest of the calendar segments. BTN4 adds or removes a quickly changing digit to or from the clock. +While the year number remains cardinal, tapping on the right side of the watch face toggles between cardinal and ordinal for the rest of the calendar segments. Tapping on the left adds or removes a quickly changing digit to or from the clock. diff --git a/apps/doztime/app.js b/apps/doztime/app-bangle1.js similarity index 100% rename from apps/doztime/app.js rename to apps/doztime/app-bangle1.js diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js new file mode 100644 index 000000000..b77e5201a --- /dev/null +++ b/apps/doztime/app-bangle2.js @@ -0,0 +1,244 @@ +// Positioning values for graphics buffers +const g_height = 80; // total graphics height +const g_x_off = 0; // position from left was 16, then 8 here +const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240 +const g_width = 240 - 2 * g_x_off; // total graphics width +const g_height_d = 28; // height of date region was 32 +const g_y_off_d = 0; // y position of date region within graphics region +const spacing = 0; // space between date and time in graphics region +const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region +const g_height_t = 44; // height of time region was 48 + +// Other vars +const A1 = [30,30,30,30,31,31,31,31,31,31,30,30]; +const B1 = [30,30,30,30,30,31,31,31,31,31,30,30]; +const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; +const timeColour = "#ffffff"; +const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff +const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20 +const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20 +const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30 +const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30 +const baseYear = 11584; +const baseDate = Date(2020,11,21); // month values run from 0 to 11 +let accum = new Date(baseDate.getTime()); +let sequence = []; +let timeActiveUntil; +let addTimeDigit = false; +let dateFormat = false; +let lastX = 999999999; +let res = {}; +//var last_time_log = 0; + +var drawtime_timeout; + +// Date and time graphics buffers +var dateColour = "#ffffff"; // override later +var timeColour2 = timeColour; +var g_d = Graphics.createArrayBuffer(g_width,g_height_d,1,{'msb':true}); +var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true}); +// Set screen mode and function to write graphics buffers +//Bangle.setLCDMode(); +g.clear(); // start with blank screen +g.flip = function() +{ + g.setBgColor(0,0,0); + g.setColor(dateColour); + g.drawImage( + { + width:g_width, + height:g_height_d, + buffer:g_d.buffer + }, g_x_off, g_y_off + g_y_off_d); + g.setColor(timeColour2); + g.drawImage( + { + width:g_width, + height:g_height_t, + buffer:g_t.buffer + }, g_x_off, g_y_off + g_y_off_t); +}; + +setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1 +setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2 +//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); +//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true}); +//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true}); + +Bangle.on('touch', function(button, xy) { //from Gordon Williams + if (button==1) toggleTimeDigits(); + if (button==2) toggleDateFormat(); +}); + +function buildSequence(targ){ + for(let i=0;i n > dt)-1; + let year = baseYear+parseInt(index/12); + let month = index % 12; + let day = parseInt((dt-sequence[index])/86400000); + let colour = dateColours[day % 6]; + if(day==30){ colour=dateColours[6]; } + return({"year":year,"month":month,"day":day,"colour":colour}); +} +function toggleTimeDigits(){ + addTimeDigit = !addTimeDigit; + modeTime(); +} +function toggleDateFormat(){ + dateFormat = !dateFormat; + modeTime(); +} +function formatDate(res,dateFormat){ + let yyyy = res.year.toString(12); + calenDef = calen10; + if(!dateFormat){ //ordinal format + let mm = ("0"+(res.month+1).toString(12)).substr(-2); + let dd = ("0"+(res.day+1).toString(12)).substr(-2); + if(res.day==30){ + calenDef = calen7; + let m = ((res.month+1).toString(12)).substr(-2); + return(yyyy+"-"+"S"+m); // ordinal format + } + return(yyyy+"-"+mm+"-"+dd); + } + let m = res.month.toString(12); // cardinal format + let w = parseInt(res.day/6); + let d = res.day%6; + //return(yyyy+"-"+res.month+"-"+w+"-"+d); + return(yyyy+"-"+m+"-"+w+"-"+d); +} + +function writeDozTime(text,def){ + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_t.clear(); + g_t.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new + else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new + else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} +function writeDozDate(text,def,colour){ + + dateColour = colour; + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_d.clear(); + g_d.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new + else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new + else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} + +// Functions for time mode +function drawTime() +{ + let dt = new Date(); + let date = ""; + let timeDef; + let x = 0; + dt.setDate(dt.getDate()); + if(addTimeDigit){ + x = + 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); + let msg = "00000"+Math.floor(x).toString(12); + let time = msg.substr(-5,3)+"."+msg.substr(-2); + let wait = 347*(1-(x%1)); + timeDef = time6; + } else { + x = + 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds(); + let msg = "0000"+Math.floor(x).toString(12); + let time = msg.substr(-4,3)+"."+msg.substr(-1); + let wait = 4167*(1-(x%1)); + timeDef = time5; + } + if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day + date = formatDate(res,dateFormat); + if(dt2200)) { + } else { + // We have a GPS time. Set time + setTime(g.time.getTime()/1000); + } + }); + Bangle.setGPSPower(1,"time"); + setTimeout(fixTime, 10*60*1000); // every 10 minutes +} +// Start time fixing with GPS on next 10 minute interval +setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000); diff --git a/apps/flipper/ChangeLog b/apps/flipper/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/flipper/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/flipper/README.md b/apps/flipper/README.md new file mode 100644 index 000000000..88025b8b2 --- /dev/null +++ b/apps/flipper/README.md @@ -0,0 +1,20 @@ +# Flipper + +![](flipper.png) + + *A utility to switch from the dark to the light theme and vice versa* + +* If the current theme is dark it will switch to the light theme +* If the current theme is light it will switch to the dark theme +* Combine with the awesome pattern launcher and it saves loads of time + + +## Demo Video + +There's no screenshot but there is a [demo video.](https://espruino.microco.sm/api/v1/files/9caa1afef7e4cce1d9b518af2dd271f1a57c5ecc.mp4) + +## Future Enhancements + +* Nothing left to add + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/flipper/flipper.app.js b/apps/flipper/flipper.app.js new file mode 100644 index 000000000..7171306b1 --- /dev/null +++ b/apps/flipper/flipper.app.js @@ -0,0 +1,39 @@ +const storage = require('Storage'); +let settings = storage.readJSON('setting.json', 1); + +function cl(x) { return g.setColor(x).getColor(); } + +function upd(th) { + g.theme = th; + settings.theme = th; + storage.write('setting.json', settings); + delete g.reset; + g._reset = g.reset; + g.reset = function(n) { return g._reset().setColor(th.fg).setBgColor(th.bg); }; + g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); }; + g.clear(1); +} + +function flipTheme() { + if (!g.theme.dark) { + upd({ + fg:cl("#fff"), bg:cl("#000"), + fg2:cl("#0ff"), bg2:cl("#000"), + fgH:cl("#fff"), bgH:cl("#00f"), + dark:true + }); + } else { + upd({ + fg:cl("#000"), bg:cl("#fff"), + fg2:cl("#000"), bg2:cl("#cff"), + fgH:cl("#000"), bgH:cl("#0ff"), + dark:false + }); + } +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +flipTheme(); +setTimeout(load, 20); diff --git a/apps/flipper/flipper.icon.js b/apps/flipper/flipper.icon.js new file mode 100644 index 000000000..494072c3c --- /dev/null +++ b/apps/flipper/flipper.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAO/mMUzs975K+ggLKysUAYNVqoLFitUoAKBqtQBYkJBIQABqwLEgQLEqtABggJDqkVBaoNCBZQwEgILWgoJENYsVBIcVBYpDEgpSIBYMBKQg6CuogCBY1UgoLCXAQLDqAsDBYhSBqEJHAoLDoEBcQ4LBEwILIMooLdIg4LaVoyaGERLcFao4LIdRAACYYUQBY5RKAH4Ar")) diff --git a/apps/flipper/flipper.png b/apps/flipper/flipper.png new file mode 100644 index 000000000..b91543070 Binary files /dev/null and b/apps/flipper/flipper.png differ diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog index 96e7e4e9b..458d695f0 100644 --- a/apps/fwupdate/ChangeLog +++ b/apps/fwupdate/ChangeLog @@ -2,3 +2,5 @@ 0.02: Add support for ZIPs Find and download ZIPs direct from the Espruino website Take 'beta' tag off +0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later + Add CRC checks for common bootloaders that we know don't work diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 8c2008e54..7a1e736e4 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -60,6 +60,7 @@ function onInit(device) { document.getElementById("fw-unknown").style = "display:none"; document.getElementById("fw-ok").style = ""; } + } function checkForFileOnServer() { @@ -264,6 +265,8 @@ function createJS_app(binary, startAddress, endAddress) { bin32[3] = VERSION; // VERSION! Use this to test ourselves console.log("CRC 0x"+bin32[2].toString(16)); hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`; + hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 2v10.219 needs update"); load();}\n`; + hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("BOOTLOADER 2v10.236 needs update"); load();}\n`; hexJS += '\x10var s = require("Storage");\n'; hexJS += '\x10s.erase(".firmware");\n'; var CHUNKSIZE = 2048; @@ -291,20 +294,14 @@ function createJS_bootloader(binary, startAddress, endAddress) { var chunk = btoa(new Uint8Array(binary.buffer, binary.byteOffset+i, l)); hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n'; } -// hexJS += `\x10(function() { -// if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC!"; -// var f = require("Flash"); -// for (var i=${startAddress};i<${endAddress};i+=4096) f.erasePage(i); -// f.write(_fw,${startAddress}); -// E.reboot(); -// })();\n`; - hexJS += `\x10if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`; - hexJS += '\x10var f = require("Flash");\n'; + hexJS += `\x10(function() { if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`; + hexJS += 'E.showMessage("Flashing Bootloader...")\n'; + hexJS += 'E.setFlags({unsafeFlash:1})\n'; + hexJS += 'var f = require("Flash");\n'; for (var i=startAddress;iexitApp(), BTN1); +if (global.BTN2) { + setWatch(_=>exitApp(), BTN2); + setWatch(_=>exitApp(), BTN3); +} diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index c171ec3d4..f5d8346da 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -5,4 +5,5 @@ 0.05: Additional icons for (1) charging and (2) bat < 30%. 0.06: Fix - Alarm disabled, if clock was closed. 0.07: Added settings to adjust data that is shown for each row. -0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. \ No newline at end of file +0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. +0.09: Tab anywhere to open the launcher. \ No newline at end of file diff --git a/apps/lcars/README.md b/apps/lcars/README.md index 97695a408..4bf5218f6 100644 --- a/apps/lcars/README.md +++ b/apps/lcars/README.md @@ -8,13 +8,15 @@ To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerda * LCARS Style watch face. * Full screen mode - widgets are still loaded. * Supports multiple screens with different data. + * Tab anywhere to open the launcher. * [Screen 1] Date + Time + Lock status. * [Screen 1] Shows randomly images of real planets. * [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.) * [Screen 1] Swipe up/down to activate an alarm. * [Screen 1] Shows 3 customizable datapoints on the first screen. * [Screen 1] The lower orange line indicates the battery level. - * [Screen 2] Display month graphs for steps + hrm on the second screen. + * [Screen 2] Display graphs for steps + hrm on the second screen. + * [Screen 2] Switch between day/month via swipe up/down. ## Multiple screens support diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index 74d0450c0..167adad2d 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -24,6 +24,7 @@ let cOrange = "#FF9900"; let cPurple = "#FF00DC"; let cWhite = "#FFFFFF"; let cBlack = "#000000"; +let cGrey = "#9E9E9E"; /* * Global lcars variables @@ -147,14 +148,17 @@ function printData(key, y, c){ } g.setColor(c); + g.fillRect(79, y-2, 87 ,y+18); + + g.setFontAlign(1,-1,0); + g.drawString(value, 131, y); + + g.setColor(c); + g.setFontAlign(-1,-1,0); g.fillRect(133, y-2, 165 ,y+18); g.fillCircle(161, y+8, 10); g.setColor(cBlack); g.drawString(text, 135, y); - - g.setColor(c); - g.setFontAlign(1,-1,0); - g.drawString(value, 130, y); } function drawHorizontalBgLine(color, x1, x2, y, h){ @@ -191,13 +195,14 @@ function drawState(){ return; } - g.clearRect(20, 93, 77, 170); - g.setColor(cWhite); - var bat = E.getBattery(); - var current = new Date(); - var hours = current.getHours(); + g.clearRect(20, 93, 75, 170); + g.setFontAlign(0, 0, 0); + g.setFontAntonioMedium(); if(!isAlarmEnabled()){ + var bat = E.getBattery(); + var current = new Date(); + var hours = current.getHours(); var iconImg = Bangle.isCharging() ? iconCharging : bat < 30 ? iconNoBattery : @@ -206,16 +211,16 @@ function drawState(){ hours % 4 == 1 ? iconMars : hours % 4 == 2 ? iconMoon : iconEarth; - g.drawImage(iconImg, 29, 104); + g.drawImage(iconImg, 24, 118); + g.setColor(cWhite); + g.drawString("STATUS", 24+25, 108); } else { // Alarm within symbol - g.setFontAntonioMedium(); - g.setFontAlign(0, 0, 0); g.setColor(cOrange); - g.drawString("ALARM", 29+25, 107); + g.drawString("ALARM", 24+25, 108); g.setColor(cWhite); g.setFontAntonioLarge(); - g.drawString(getAlarmMinutes(), 29+25, 107+35); + g.drawString(getAlarmMinutes(), 24+25, 108+35); } g.setFontAlign(-1, -1, 0); @@ -236,7 +241,7 @@ function drawPosition0(){ var bat = E.getBattery() / 100.0; var batX2 = parseInt((172 - 35) * bat + 35); drawHorizontalBgLine(cOrange, 35, batX2, 171, 5); - drawHorizontalBgLine(cPurple, batX2+10, 172, 171, 5); + drawHorizontalBgLine(cGrey, batX2+10, 172, 171, 5); // Draw logo drawLock(); @@ -247,7 +252,7 @@ function drawPosition0(){ var currentDate = new Date(); var timeStr = locale.time(currentDate,1); g.setFontAntonioLarge(); - g.drawString(timeStr, 28, 10); + g.drawString(timeStr, 29, 10); // Write date g.setColor(cWhite); @@ -255,7 +260,7 @@ function drawPosition0(){ var dayStr = locale.dow(currentDate, true).toUpperCase(); dayStr += " " + currentDate.getDate(); dayStr += " " + currentDate.getFullYear(); - g.drawString(dayStr, 29, 56); + g.drawString(dayStr, 32, 56); // Draw data g.setFontAlign(-1, -1, 0); @@ -401,7 +406,7 @@ function draw(){ * Step counter via widget */ function getSteps() { - var steps = 0 + var steps = 0; try { health = require("health"); } catch(ex) { @@ -553,6 +558,10 @@ Bangle.on("drag", e => { } }); +Bangle.on("touch", e => { + Bangle.showLauncher(); +}); + /* * Lets start widgets, listen for btn etc. diff --git a/apps/lcars/screenshot.png b/apps/lcars/screenshot.png index b3dfd4200..fba55a9f7 100644 Binary files a/apps/lcars/screenshot.png and b/apps/lcars/screenshot.png differ diff --git a/apps/mclockplus/ChangeLog b/apps/mclockplus/ChangeLog index a1cecc698..097545ba8 100644 --- a/apps/mclockplus/ChangeLog +++ b/apps/mclockplus/ChangeLog @@ -1,2 +1,3 @@ 0.01: Created app 0.02: Use Bangle.setUI for button/launcher handling +0.03: Allow widgets to detect this is a clock diff --git a/apps/mclockplus/mclockplus.app.js b/apps/mclockplus/mclockplus.app.js index 4c74ce1be..970397478 100644 --- a/apps/mclockplus/mclockplus.app.js +++ b/apps/mclockplus/mclockplus.app.js @@ -304,15 +304,14 @@ Bangle.on('lcdPower',function(on) { }); g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); // Update time once a second timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); - // Start stopwatch when BTN3 is pressed setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"}); B3 = 1; // BTN3 is bound to start the stopwatch diff --git a/apps/pooqround/ChangeLog b/apps/pooqround/ChangeLog index 8eb91cf97..bc35b69df 100644 --- a/apps/pooqround/ChangeLog +++ b/apps/pooqround/ChangeLog @@ -1,2 +1,3 @@ 0.00: Initial check-in. 0.01: Add tap-to-decorate feature. Bugfixes. +0.02: Add autorotate while charging. Remove autolight. Tweak fonts. Add some haptic feedback on touchscreen operations. diff --git a/apps/pooqround/README.md b/apps/pooqround/README.md index d413fd88e..6f6bafa69 100644 --- a/apps/pooqround/README.md +++ b/apps/pooqround/README.md @@ -20,8 +20,7 @@ you can quickly alter the number of ‘hands’ on the display. When the watch i or down to remove the distraction. There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky, in case you want the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails. -In some versions of the Bangle.js firmware, the backlight doesn't come on automatically when you twist your wrist. There's currently a workaround -for this integrated into the watchface; you can disable it in the menu, if you prefer. +While charging the main display automatically rotates so that noon is up. This can be disabled. ## Limitations diff --git a/apps/pooqround/app.js b/apps/pooqround/app.js index 22cf5ff79..06e208e61 100644 --- a/apps/pooqround/app.js +++ b/apps/pooqround/app.js @@ -25,8 +25,8 @@ // // This only works for Bangle 2. -const isString = x => typeof x === 'string'; -const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; +const isString = x => typeof x === 'string', + imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; ////////////////////////////////////////////////////////////////////////////// /* System integration */ @@ -115,9 +115,9 @@ class RoundOptions extends Options { onchange: x => this.calendric = x, format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x], }, - 'Auto-Illum.': { - init: _ => this.autolight, - onchange: x => this.autolight = x + 'Autorotate': { + init: _ => this.autorotate, + onchange: x => this.autorotate = x }, Defaults: _ => {this.reset(); this.interact();} }); @@ -133,7 +133,7 @@ RoundOptions.defaults = { calendric: 5, dayFg: '#fff', nightFg: '#000', - autolight: true, + autorotate: true, }; ////////////////////////////////////////////////////////////////////////////// @@ -144,29 +144,29 @@ const dec = x => E.toString(heatshrink.decompress(atob(x))); const y10F = [ dec( 'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' + - 'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' + - 'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' + - 'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' + - 'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' + - 'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' + - 'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' + - 'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' + - 'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' + - 'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA=' + 'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUcgECCIZlBAY' + + 'N4CoRUBIoMP8AZBge8MoMB8+B8B4B+E/gf4jw/B/kD4ADBEQMPSYXgoAfBnEwgeA' + + 'hw7BvEDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMG4ReBn4zBJYKcDH' + + '4IABv+AXoSGCv0eAYP/FIMB/4iBTAIJBGIJ6B/yQCb4RDBEQTlBHoIOBn51BwC+B' + + 'MoWDAwKYBRgKYBCYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+BAY' + + 'JyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwZgPwnhxBGQJxBGQK5BGQKWDOwUACALlBI' + + 'YRrB8H///gnI+COwJGBgaUBWgqVDhgDCZYIADFIKAB84eBIwImBXwP8MoPwviYCI' + + 'AKYBIAKYB4JlCPwJlBS4IdC/IeBFwJlCh6XCY4Q2BLYMIDQN8PIPwg+B4B2B8FwG' + + 'oN4TgPAnk+MoM+v6tBGQOAZQJ3CQwUAA' ), 48, dec('hgAI'), 34 ];const y1F = [ dec( - 'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' + - 'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' + - '+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' + - 'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' + - 'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' + - 'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' + - 'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' + - 'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' + - 'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' + - 'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' + - 'BCQQUAA=' + 'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnEAgQ' + + 'FCjkAgwFCh0Ahg1EBoIABgeAFIf/4A1DFQIED/5MDGAYADEQYwDRwgMDhAYEH4Nw' + + 'AoUeAok/QYl/wAFD/fAHgUD+PgvAFBj/g+E/4EBLAN4j5SCgE8h4EB/AwCAoOAVA' + + 'PgggeBFoPgQgRLB8E8I4fgXQS/B8KwBMgOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQ' + + 'JIB/DMDMoJSBboQVBKoIDBSYZOBAAQlCAATpEg/4Xwc/QIZyBwBcBgf//gxBa4Qb' + + 'Ba4LZDv/4LwRfCGAcBGAYABC4IYCD4QjCR4IFDR4R6BR4QFDMAIFDF4IFBC4IIBA' + + 'oLEBBYQlBI4IFDR4ZrBR4QFBTgJMDHoaaCdQSmCC4SyCYYJJB+CHBj+Aj8ASYJNB' + + 'BINwIIOAM4ILDAYN/wABBB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBA' + + 'gaNCYwgFEbAkAwAFEc4SPCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh' + + '4FDMoQ1DK4ZBBMQIDBJYbWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwR' + + 'CvEBF4IdB+E/OIp9CJgZBCQQUAA=' ), 48, dec('hgAI'), 48 ];const y10sF = [ dec( @@ -194,20 +194,20 @@ const y10F = [ ];const d1F = [ dec( 'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' + - 'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' + - 'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' + - 'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' + - 'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' + - '4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' + - 'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' + - '4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' + - 'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' + - 'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' + - 'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' + - 'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' + - 'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' + - 'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' + - 'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA==' + 'oUHAowRFDoopFGopBFJopZGBgIKCAB5BBgA1CAoMBAokDCIgTCAYRTDAoI6CHgU/' + + 'Aol/Aog1GAqgAIhgCBn4CBvjZCLIKMBPIJZBcIMB+4lBMoMD84rDg/HL4cPw4FDj' + + '5rEnwFEvgFE/AFBaYMB+CJCwED8AFC8EP4CbC/F/wCnC//+H4bbCAoQWBO594EAI' + + 'TBgBrCAQTtBPQUD4EQQwRHBuEAjwuBCQRdBjxOBLoU8j47DvF/Aofz/IFCgPv4IF' + + 'Cgf/EYUPg43BFIUPIYIBBjwnCH4N8ZgT9B/jPCj//+AUBE4P/MoQANnwFETgIACg' + + 'YFEh/uAod/xwQEagQ6B/AFDHIIFCg4hBAoV/JIIFBaAJaDFQgFKFIYFZABN/JAM4' + + 'KYSvBjhICAoLjBd4IPBEgLvBvDjCAIL1BboITBAoc8AogVBAoZ2BDoTnCFIQuBDI' + + 'UeFwIlCnguBGIV4BAIhB4PwCgJJB//gEwJbCwDvCM4LuPC4TjD/4cE//4Fwh/BcY' + + 'K9BIAX/96DCegIJBQYXwTIJxBn+AAoarBAoUBFYIFCgZ9BDoR3CE4LSDE4I1CCoJ' + + 'BDJARNCZoRZDHAQDBDQYABSIQACSIYABDQjACFobABHIaMDIoQADFwSFCFwIEBAo' + + 'N4g4EBAoPwUAIABgPnUonfQgIuC/41Dh5tDEwJGEn46EvhdJACCJDv6VDAYPDJIS' + + 'lB86tCg+B+7HDDALHEnzHEPILpBcILvDAooRFDoopFGopBFJopZFMowAFZgs/VAI' + + '7BgAbCAoMB/InBAoMD8CkBAoWALIIgBeoJ7DLYYHDPYZbCNoQLDEoYNBGIQjBgI9' + + 'BF4Q4BHYcPJ4JHC/5ZC///fwc/OwkPCAQA==' ), 48, dec('ikPigAGA'), 48 ];const dowF = [ dec( @@ -220,10 +220,10 @@ const y10F = [ 'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' + 'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' + 'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' + - 'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' + - '//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' + - 'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' + - 'p/zC4n5EYj1BAoc//4RDU4IFDA==' + 'BDAoJsDAo7vhABZuBQYoFDv4FEYgpjDZRgFYGYYpHGoqxDAAMEAokDdwQbBh//DY' + + 'cf/+ALoU+g/AbIV4gLfDDILrDIIIFD8ARCAoYdBAoodB8EDAoIvBAoI1BHYPwAoQ' + + 'nBuARBEoVwDoJDCnEB+E/KoMYXYP+Wgd//DGDh4WBgEZMoQABnPnAodz4YQCgHjw' + + 'IFP+YXE/IjEeoIFDn//CIanBAoY=' ), 48, dec('kElkMljsljw='), 48 ];const mF = [ dec( @@ -322,21 +322,20 @@ class Round { this.r = this.xc - this.minR; } - reset(clear) {this.state = {}; clear && this.g.clear(true);} + reset(clear) {this.state = {}; clear == null || this.g.clear(true).setRotation(clear);} doIcons(which) { this.state[which] = null; - this.render(new Date()); // Not quite right, I think. } enhanceUntil(t) {this.enhance = t;} pie(f, a0, a1, invert) { if (!invert) return this.pie(f, a1, a0 + 1, true); - let t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI); + const t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI); let i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5); - let x = f.getWidth() / 2, y = f.getHeight() / 2; - let poly = [ + const x = f.getWidth() / 2, y = f.getHeight() / 2; + const poly = [ x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1), y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1), x, @@ -348,16 +347,17 @@ class Round { for (i0++; i0 <= i1; i0++) poly.push( 3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0 ); - f.setColor(0).fillPoly(poly); + return f.setColor(0).fillPoly(poly); } hand(t, d, c0, r0, c1, r1) { + const g = this.g; t *= Math.PI / 30; - const r = this.r; - const z = 2 * r0 + 1; - const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t); - const x0 = x - r0, y0 = y - r0; - d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true}); + const r = this.r, + z = 2 * r0 + 1, + x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t), + x0 = x - r0, y0 = y - r0; + d = d ? d[0] : Graphics.createArrayBuffer(z, z, 4, {msb: true}); for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) { d.setPixel(i, j, g.getPixel(x0 + i, y0 + j)); } @@ -366,24 +366,20 @@ class Round { return [d, x0, y0]; } - render(d) { - const g = this.g; - const b = this.b, bI = this.bI; - const c = this.c, cI = this.cI; - const e = d < this.enhance; - const state = this.state; - const options = this.options; - const cal = options.calendric; - const res = options.resolution; - const dow = (e || cal == 1 || cal > 2) && d.getDay(); - const ts = res < 2 && d.getSeconds(); - const tm = (e || res < 3) && d.getMinutes() + ts / 60; - const th = d.getHours() + d.getMinutes() / 60; - const dd = (e || cal > 1) && d.getDate(); - const dm = (e || cal > 3) && d.getMonth(); - const dy = (e || cal > 4) && d.getFullYear(); - const xc = this.xc, yc = this.yc, r = this.r; - const dlr = xc * 3/4, dlw = 8, dlhw = 4; + render(d, rate) { + const g = this.g, b = this.b, bI = this.bI, c = this.c, cI = this.cI, + e = d < this.enhance, + state = this.state, options = this.options, + cal = options.calendric, res = options.resolution, + dow = (e || cal === 1 || cal > 2) && d.getDay(), + ts = res < 2 && d.getSeconds(), + tm = (e || res < 3) && d.getMinutes() + ts / 60, + th = d.getHours() + d.getMinutes() / 60, + dd = (e || cal > 1) && d.getDate(), + dm = (e || cal > 3) && d.getMonth(), + dy = (e || cal > 4) && d.getFullYear(); + const xc = this.xc, yc = this.yc, r = this.r, + dlr = xc * 3/4, dlw = 8, dlhw = 4; // Restore saveunders for fast-moving, overdrawing indicators. if (state.sd) g.drawImage.apply(g, state.sd); @@ -397,10 +393,10 @@ class Round { state.dow = dow; } - const locked = Bangle.isLocked(); - const charging = Bangle.isCharging(); - const battery = E.getBattery(); - const HRMOn = Bangle.isHRMOn(); + const locked = Bangle.isLocked(), + charging = Bangle.isCharging(), + battery = E.getBattery(), + HRMOn = Bangle.isHRMOn(); if (dy !== state.dy || locked !== state.locked || charging !== state.charging || @@ -463,6 +459,7 @@ class Round { this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) : null; state.sd = ts === +ts ? + rate > 1000 ? this.hand(ts, state.sd, g.theme.fg2, this.secR, g.theme.bg, 2) : this.hand(ts, state.sd, g.theme.fg2, this.secR) : null; } @@ -482,13 +479,23 @@ class Clock { this.listeners = { lcdPower: on => on ? this.active() : this.inactive(), - charging: () => {face.doIcons('charging'); this.active();}, + charging: on => { + face.doIcons('charging'); + if (on) { + this.listeners.accel = + a => this.orientation(a) === this.attitude || this.active(); + Bangle.on('accel', this.listeners.accel); + } else { + Bangle.removeListener('accel', this.listeners.accel); + delete this.listeners.accel; + } + this.active(); + }, lock: () => {face.doIcons('locked'); this.active();}, faceUp: up => { this.conservative = !up; this.active(); }, - twist: _ => this.options.autolight && Bangle.setLCDPower(true), drag: e => { if (this.t0) { if (e.b) { @@ -498,20 +505,23 @@ class Clock { if (e.y - this.e0.y < -50) { this.options.resolution > 0 && this.options.resolution--; this.rates.clock = this.timescales[this.options.resolution]; + this.ack(); this.active(); } else if (e.y - this.e0.y > 50) { this.options.resolution < this.timescales.length - 1 && this.options.resolution++; this.rates.clock = this.timescales[this.options.resolution]; + this.ack(); this.active(); } else if (this.yX - this.yN < 20) { const now = new Date(); if (now - this.t0 < 250) { + this.ack(); face.enhanceUntil(now + 30000); - face.render(now); + this.active(); } else if (now - this.t0 > 500) { this.stop(); - this.options.interact(); + this.ack().then(_ => this.options.interact()); } } this.t0 = null; @@ -524,9 +534,25 @@ class Clock { }; } + ack() { + return Bangle.buzz(33); + } + + orientation(a) { + return Math.abs(a.z) < 0.85 ? + Math.abs(a.y) > Math.abs(a.x) ? a.y < 0 ? 0 : 2 : a.x > 0 ? 1 : 3 : + 0; + } + + rotation() { + return this.options.autorotate && Bangle.isCharging() ? + this.orientation(Bangle.getAccel()) : + 0; + } + redraw(rate) { const now = this.updated = new Date(); - if (this.refresh) this.face.reset(true); + if (this.refresh) this.face.reset(this.attitude = this.rotation()); this.refresh = false; rate = this.face.render(now, rate); if (rate !== this.rates.face) { @@ -541,13 +567,13 @@ class Clock { this.exception && clearTimeout(this.exception); this.interval && clearInterval(this.interval); this.timeout = this.exception = this.interval = this.rate = null; - this.face.reset(false); // Cancel any ongoing background rendering + this.face.reset(); // Cancel any ongoing background rendering return this; } active() { - const prev = this.rate; - const now = Date.now(); + const prev = this.rate, + now = Date.now(); let rate = Infinity; for (const k in this.rates) { let r = this.rates[k]; diff --git a/apps/pooqround/resourcer.js b/apps/pooqround/resourcer.js index 44186e658..17c35a40d 100644 --- a/apps/pooqround/resourcer.js +++ b/apps/pooqround/resourcer.js @@ -1,6 +1,6 @@ // pooqRoman resource maker // -// Copyright (c) 2021 Stephen P Spackman +// Copyright (c) 2021, 2022 Stephen P Spackman // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -147,18 +147,18 @@ res += prepFont('y10', ` xxx xxx -2-------------------------------- - x xx + x xxx xx xxx xxxx xxx xxxxx xxx xxxxxxx xxx xxxx xxx xxx - xxxx xxxx xxx - xxxx xxxx xxx - xxxx xxxxxxxx xxxxxxx - xxxx xxxxxxxxxxxxxxxxxxx + xxxx xxxx xxxx + xxxx xxxxx xxxx + xxxx xxxxxxx xxxxxx + xxxx xxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxx -xxxx xxxxxxxxxx +xxxx xxxxxxxxx -3-------------------------------- xxx x xxx xxx xx xxx @@ -270,10 +270,10 @@ res += prepFont('y1', ` xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -1---------------------------------------------- xxx - xxx - xxx - xxx x - xxx x + xxx x + xxx xx + xxx xx + xxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -282,18 +282,18 @@ res += prepFont('y1', ` xxx xxx -2---------------------------------------------- - x xx + x xxx xx xxx xxxx xxx - xxxxx xxx - xxxxxxx xxx - xxxx xxxx xxx - xxxx xxxxx xxx - xxxx xxxxxxx xxxx - xxxx xxxxxxxxxxxxx xxxxxxxxxxx - xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - xxxx xxxxxxxxxxxxxxxxxxxxxxxxx -xxxx xxxxxxxxxxxxxx + xxxxxx xxx + xxxxxxxx xxx + xxxx xxxxx xxx + xxxx xxxxxx xxxx + xxxx xxxxxxxx xxxx + xxxx xxxxxxxxxxx xxxxxxxx + xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxx xxxxxxxxxxxxxxxxxxxxxxxxx +xxxx xxxxxxxxxxxxxxxxx -3---------------------------------------------- xxx x xxx xxx xx xxx @@ -645,12 +645,12 @@ xxxx xxxx -1---------------------------------------------- -xxx x -xxx xx -xxx xxx -xxx xxx -xxx xxxx -xxx xxxx +xxx +xxx x +xxx xx +xxx xx +xxx xxx +xxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -993,9 +993,9 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxx - xxxxxxxxxxxxxxxx - xxxxxxxxxxxxxxxx - xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx xxxx xxxx xxx diff --git a/apps/presentor/ChangeLog b/apps/presentor/ChangeLog new file mode 100644 index 000000000..00833cf91 --- /dev/null +++ b/apps/presentor/ChangeLog @@ -0,0 +1,8 @@ +0.1: Start of app. +0.5: BLE keyboard functionality. +1.0: BLE mouse functionality to scroll back/forward. +1.5: Added accelerator style mouse. +2.0: Added touchpad style mouse. +2.1: Initial internal git(hub) release. Added icon and such. +2.2: Begin work on presentation parts. +3.0: Presentation parts! \ No newline at end of file diff --git a/apps/presentor/README.md b/apps/presentor/README.md new file mode 100644 index 000000000..8b22eb228 --- /dev/null +++ b/apps/presentor/README.md @@ -0,0 +1,2 @@ +# Presentor +Use your Bangle to present! diff --git a/apps/presentor/app-icon.js b/apps/presentor/app-icon.js new file mode 100644 index 000000000..76939d548 --- /dev/null +++ b/apps/presentor/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4ASlgADGmIxwLV4wqGQowfWZQwjKw4wJF7ghBmVWmQlELYoweFwYABGAwrHF7IuFGAwrIF7AuHMJADGF0AwHAYovWFxaSHADQuDEgIADYZYucAQOB1fQ1eBmQwiFwlX6AAE1gqBGD6MEmQqBwIICwIGB0rDeWYksFAIuBz+fvQHC6D0dcQssEwIuB4fC4V0M4VXF7YuFDYLqBlnDF4WeHAYugL5N6L4I4BF0IvB0vQvdQGAJeBY4YucmQxEAgJgBvYOBdwYQDFzUzmZ/EllXFIKKBAYVXBwSMaller7fFAoKSBAAOBFQK7dPoJfFEoYADRjgmEeIzFFdUQAOdUIumdRLtDAAIufdRQKCr8zCQjqmE4NeF4YudWxpqCFyovBK5LqiF6DqdR6D0BdTYwJGg5sBdTQwKEAIwHdTQwMSpQueYaAufGBouiGBYukGBIumGA4uoGAouqGAYABF1QAl")) diff --git a/apps/presentor/app.js b/apps/presentor/app.js new file mode 100644 index 000000000..6b7450a0c --- /dev/null +++ b/apps/presentor/app.js @@ -0,0 +1,471 @@ +// Presentor by 7kasper (Kasper Müller) +// Version 3.0 + +const SpecialReport = new Uint8Array([ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x85, 0x01, // REPORT_ID (1) + 0x09, 0x01, // USAGE (Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x05, // USAGE_MAXIMUM (Button 5) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x03, // REPORT_SIZE (3) + 0x81, 0x03, // INPUT (Cnst,Var,Abs) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT (Data,Var,Rel) + 0x05, 0x0c, // USAGE_PAGE (Consumer Devices) + 0x0a, 0x38, 0x02, // USAGE (AC Pan) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x01, // REPORT_COUNT (1) + 0x81, 0x06, // INPUT (Data,Var,Rel) + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x85, 0x02, // REPORT_ID (2) + 0x05, 0x07, // USAGE_PAGE (Keyboard) + 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) + 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x01, // REPORT_COUNT (1) + 0x81, 0x01, // INPUT (Cnst,Ary,Abs) + 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) + 0x29, 0x73, // USAGE_MAXIMUM (Keyboard F24) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x73, // LOGICAL_MAXIMUM (115) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x00, // INPUT (Data,Ary,Abs) + 0xc0 // END_COLLECTION +]); + +const MouseButton = { + NONE : 0, + LEFT : 1, + RIGHT : 2, + MIDDLE : 4, + BACK : 8, + FORWARD: 16 +}; + +const kb = require("ble_hid_keyboard"); + +const Layout = require("Layout"); +const Locale = require("locale"); +let mainLayout = new Layout({ + 'type': 'v', + filly: 1, + c: [ + { + type: 'txt', + font: '6x8', + label: 'Presentor', + valign: -1, + halign: 0, + col: g.theme.fg1, + // bgCol: g.theme.bg2, + bgCol: '#00F', + fillx: 1, + }, { + type: 'h', + fillx: 1, + c: [ + { + type: 'txt', + font: '15%', + label: '00:00', + id: 'Time', + halign: -1, + pad: 3 + }, { + fillx: 1 + }, { + type: 'txt', + font: '15%', + label: '00:00', + id: 'Timer', + halign: 1, + pad: 3 + } + ] + }, { + type: 'txt', + font: '10%', + label: '+00:00', + id: 'RestTime', + col: '#fff' + }, { + type: 'txt', + font: '10%', + label: '--------------' + }, { + type: 'txt', + font: '15%', + label: 'Presenting', + id: 'Subject' + }, { + type: 'txt', + font: '6x8', + label: 'Swipe up to start the time.', + id: 'Notes', + col: '#ff0', + fillx: 1, + filly: 1, + valign: 1 + } + ] +}, {lazy:true}); + +let settings = {pparts: [], sversion: 0}; +let HIDenabled = true; + +// Application variables +let pparti = -1; +let ppartBuzzed = false; +let restBuzzed = false; + +let lastx = 0; +let lasty = 0; + +// Mouse states +let holding = false; +let trackPadMode = false; + +// Timeout IDs. +let timeoutId = -1; +let timeoutHolding = -1; +let timeoutDraw = -1; + + +let homeRoll = 0; +let homePitch = 0; +let mCal = 0; +let mttl = 0; +let cttl = 0; + +// BT helper. +let clearToSend = true; + +// Presentation Timers +let ptimers = []; + +function delay(t, v) { + return new Promise((resolve) => { + setTimeout(resolve, t) + }); +} + +function formatTimePart(time) { + time = Math.floor(Math.abs(time)); + return time < 10 ? `0${time}` : `${time}`; +} + +function formatTime(time, doPlus) { + if (time == Infinity) return ' --:-- '; + return `${time < 0 ? '-' : (doPlus ? '+' : '')}${formatTimePart(time/60)}:${formatTimePart(time%60)}`; +} + +function loadSettings() { + settings = require("Storage").readJSON('presentor.json'); + for (let i = 0; i < settings.pparts.length; i++) { + ptimers[i] = { + active: false, + tracked: -1, + left: settings.pparts[i].minutes * 60 + settings.pparts[i].seconds + }; + } +} + +function getCurrentTimer() { + if (pparti < 0) return Infinity; + if (!settings.pparts || pparti >= settings.pparts.length) return Infinity; + if (ptimers[pparti].tracked == -1) return Infinity; + ptimers[pparti].left -= (getTime() - ptimers[pparti].tracked); + ptimers[pparti].tracked = getTime(); + // if we haven't buzzed yet and timer became negative just buzz here. + // TODO better place? + if (ptimers[pparti].left <= 0 && !ppartBuzzed) { + Bangle.buzz(400) + .then(() => delay(400)) + .then(() => Bangle.buzz(400)); + ppartBuzzed = true; + } + return ptimers[pparti].left; +} + +function getRestTime() { + let rem = 0; + // Add all remaining time from previous presentation parts. + for (let i = 0; i < pparti; i++) { + rem += ptimers[i].left; + } + if (pparti >= 0 && pparti < ptimers.length && ptimers[pparti].left < 0) { + rem += ptimers[pparti].left; + } + // if we haven't buzzed yet and timer became negative just buzz here. + // TODO better place? + if (rem < 0 && !restBuzzed) { + Bangle.buzz(200) + .then(() => delay(400)) + .then(() => Bangle.buzz(200)) + .then(() => delay(400)) + .then(() => Bangle.buzz(200)); + restBuzzed = true; + } + return rem; +} + +function drawMainFrame() { + var d = new Date(); + // update time + mainLayout.Time.label = Locale.time(d,1); + // update timer + mainLayout.Timer.label = formatTime(getCurrentTimer()); + let restTime = getRestTime(); + mainLayout.RestTime.label = formatTime(restTime, true); + mainLayout.RestTime.col = restTime < 0 ? '#f00' : (restTime > 0 ? '#0f0' : '#fff'); + mainLayout.render(); + // schedule a draw for the next minute + if (timeoutDraw != -1) clearTimeout(timeoutDraw); + timeoutDraw = setTimeout(function() { + timeoutDraw = -1; + drawMainFrame(); + }, 1000 - (Date.now() % 1000)); +} + +function drawMain() { + g.clear(); + mainLayout.forgetLazyState(); + drawMainFrame(); + // mainLayout.render(); + // E.showMessage('Presentor'); +} + +function doPPart(r) { + pparti += r; + if (pparti < 0) { + pparti = -1; + mainLayout.Subject.label = 'PAUSED'; + mainLayout.Notes.label = 'Swipe up to start again.'; + return; + } + if (!settings.pparts || pparti >= settings.pparts.length) { + pparti = settings.pparts.length; + mainLayout.Subject.label = 'FINISHED'; + mainLayout.Notes.label = 'Good Job!'; + return; + } + let ppart = settings.pparts[pparti]; + mainLayout.Subject.label = ppart.subject; + mainLayout.Notes.label = ppart.notes; + ptimers[pparti].tracked = getTime(); + // We haven't buzzed if there was time left. + ppartBuzzed = ptimers[pparti].left <= 0; + // Always reset buzzstate for the rest timer. + restBuzzed = getRestTime() < 0; + drawMainFrame(); +} + +NRF.setServices(undefined, { hid : SpecialReport }); +// TODO: figure out how to detect HID. +NRF.on('HID', function() { + HIDenabled = true; +}); + +function moveMouse(x,y,b,wheel,hwheel,callback) { + if (!HIDenabled) return; + if (!b) b = 0; + if (!wheel) wheel = 0; + if (!hwheel) hwheel = 0; + NRF.sendHIDReport([1,b,x,y,wheel,hwheel,0,0], function() { + if (callback) callback(); + }); +} + +// function getSign(x) { +// return ((x > 0) - (x < 0)) || +x; +// } + +function scroll(wheel,hwheel,callback) { + moveMouse(0,0,0,wheel,hwheel,callback); +} + +// Single click a certain button (immidiatly release). +function clickMouse(b, callback) { + if (!HIDenabled) return; + NRF.sendHIDReport([1,b,0,0,0,0,0,0], function() { + NRF.sendHIDReport([1,0,0,0,0,0,0,0], function() { + if (callback) callback(); + }); + }); +} + +function pressKey(keyCode, modifiers, callback) { + if (!HIDenabled) return; + if (!modifiers) modifiers = 0; + NRF.sendHIDReport([2, modifiers,0,keyCode,0,0,0,0], function() { + NRF.sendHIDReport([2,0,0,0,0,0,0,0], function() { + if (callback) callback(); + }); + }); +} + +function handleAcc(acc) { + let rRoll = acc.y * -50; + let rPitch = acc.x * -100; + if (mCal > 10) { + //console.log("x: " + (rRoll - homeRoll) + " y:" + (rPitch - homePitch)); + moveMouse(acc.y * -50 - homeRoll, acc.x * -100 - homePitch); + } else { + //console.log("homeroll: " +homeRoll +"homepitch: " + homePitch); + homeRoll = rRoll * 0.7 + homeRoll * 0.3; + homePitch = rPitch * 0.7 + homePitch * 0.3; + mCal = mCal + 1; + } +} +Bangle.on('lock', function(on) { + if (on && holding) { + Bangle.setLocked(false); + Bangle.setLCDPower(1); + } +}); + +function startHolding() { + pressKey(kb.KEY.F10); + holding = true; + Bangle.buzz(); + E.showMessage('Holding'); + Bangle.on('accel', handleAcc); + Bangle.setLCDPower(1); +} +function stopHolding() { + clearTimeout(timeoutId); + if (holding) { + pressKey(kb.KEY.F10); + homePitch = 0; + homeRoll = 0; + holding = false; + mCal = 0; + Bangle.removeListener('accel', handleAcc); + Bangle.buzz(); + drawMain(); + } else { + timeoutId = setTimeout(drawMain, 1000); + } + clearTimeout(timeoutHolding); + timeoutHolding = -1; +} + +Bangle.on('drag', function(e) { + if (cttl == 0) { cttl = getTime(); } + if (trackPadMode) { + if (lastx + lasty == 0) { + lastx = e.x; + lasty = e.y; + mttl = getTime(); + } + if (clearToSend) { + clearToSend = false; + let difX = e.x - lastx, difY = e.y - lasty; + let dT = getTime() - mttl; + let vX = difX / dT, vY = difY / dT; + //let qX = getSign(difX) * Math.pow(Math.abs(difX), 1.2); + //let qY = getSign(difY) * Math.pow(Math.abs(difY), 1.2); + let qX = difX + 0.02 * vX, qY = difY + 0.02 * vY; + moveMouse(qX, qY, 0, 0, 0, function() { + setTimeout(function() {clearToSend = true;}, 50); + }); + lastx = e.x; + lasty = e.y; + mttl = getTime(); + console.log("Dx: " + (qX) + " Dy: " + (qY)); + } + if (!e.b) { + // short press + if (getTime() - cttl < 0.2) { + clickMouse(MouseButton.LEFT); + console.log("click left"); + } + // longer press in center + else if (getTime() - cttl < 0.6 && e.x > g.getWidth()/4 && e.x < 3 * g.getWidth()/4 && e.y > g.getHeight() / 4 && e.y < 3 * g.getHeight() / 4) { + clickMouse(MouseButton.RIGHT); + console.log("click right"); + } + cttl = 0; + lastx = 0; + lasty = 0; + } + } else { + if(!e.b){ + Bangle.buzz(100); + if(lasty > 40){ + doPPart(-1); + // E.showMessage('down'); + } else if(lasty < -40){ + doPPart(1); + // E.showMessage('up'); + } else if(lastx > 40){ + // E.showMessage('right'); + //kb.tap(kb.KEY.RIGHT, 0); + scroll(-1); + } else if(lastx < -40){ + // E.showMessage('left'); + //kb.tap(kb.KEY.LEFT, 0); + scroll(1); + } else if(lastx==0 && lasty==0 && holding == false){ + // E.showMessage('press'); + clickMouse(MouseButton.LEFT); + } + stopHolding(); + lastx = 0; + lasty = 0; + } else{ + lastx = lastx + e.dx; + lasty = lasty + e.dy; + if (timeoutHolding == -1) { + timeoutHolding = setTimeout(startHolding, 500); + } + } + } +}); + + +function onBtn() { + if (trackPadMode) { + trackPadMode = false; + stopHolding(); + drawMain(); + } else { + clearToSend = true; + trackPadMode = true; + E.showMessage('Mouse'); + } + Bangle.buzz(); +} +setWatch(onBtn, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat: true}); + +loadSettings(); +drawMain(); \ No newline at end of file diff --git a/apps/presentor/app.png b/apps/presentor/app.png new file mode 100644 index 000000000..d29cf7be6 Binary files /dev/null and b/apps/presentor/app.png differ diff --git a/apps/presentor/interface.html b/apps/presentor/interface.html new file mode 100644 index 000000000..e772d24da --- /dev/null +++ b/apps/presentor/interface.html @@ -0,0 +1,285 @@ + + + + + + + + + + + + +
+

Presentor

+
+
+
+
+
+
#
+
Subject
+
Time
+
Notes
+
+
Loading...
+
+
+
+
+ + +
+ + + + + + diff --git a/apps/presentor/settings.json b/apps/presentor/settings.json new file mode 100644 index 000000000..398bf1332 --- /dev/null +++ b/apps/presentor/settings.json @@ -0,0 +1 @@ +{"pparts":[{"subject":"#1","minutes":10,"seconds":0,"notes":"This is a note."},{"subject":"#2","minutes":2,"seconds":50,"notes":"Change in the app!"}],"sversion":2.2} \ No newline at end of file diff --git a/apps/promenu/ChangeLog b/apps/promenu/ChangeLog index 5560f00bc..b7287cc80 100644 --- a/apps/promenu/ChangeLog +++ b/apps/promenu/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Add Bangle.js 2 Support diff --git a/apps/promenu/boot.js b/apps/promenu/boot.js index 002734113..bd813a812 100644 --- a/apps/promenu/boot.js +++ b/apps/promenu/boot.js @@ -70,7 +70,7 @@ E.showMenu = function(items) { if(g.theme.dark){ fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+20); }else{ - fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-10); + fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); } g.setColor(hl ? g.theme.fgH : g.theme.fg); g.setFontAlign(-1,-1); diff --git a/apps/promenu/bootb2.js b/apps/promenu/bootb2.js new file mode 100644 index 000000000..c84e0d894 --- /dev/null +++ b/apps/promenu/bootb2.js @@ -0,0 +1,142 @@ +E.showMenu = function(items) { + function RectRnd(x1,y1,x2,y2,r) { + pp = []; + pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r])); + pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2])); + pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r])); + pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1])); + return pp; + } + function fillRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.fillPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + function drawRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.drawPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + g.reset().clearRect(Bangle.appRect); // clear if no menu supplied + Bangle.setLCDPower(1); // ensure screen is on + if (!items) { + Bangle.setUI(); + return; + } + var menuItems = Object.keys(items); + var options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + options.fontHeight = options.fontHeight||25; + if (options.selected === undefined) + options.selected = 0; + var ar = Bangle.appRect; + var x = ar.x; + var x2 = ar.x2; + var y = ar.y; + var y2 = ar.y2 - 12; // padding at end for arrow + if (options.title) + y += 22; + var loc = require("locale"); + var l = { + lastIdx : 0, + draw : function(rowmin,rowmax) { + var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length); + var idx = E.clip(options.selected-( rows>>1),0,menuItems.length-rows); + if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled + l.lastIdx = idx; + var iy = y; + g.reset().setFontAlign(0,-1,0).setFont('12x20'); + if (options.predraw) options.predraw(g); + if (rowmin===undefined && options.title) + g.drawString(options.title,(x+x2)/2,y-21).drawLine(x,y-2,x2,y-2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 4; + if (rowmin!==undefined) { + if (idxrowmax) { + rows = 1+rowmax-rowmin; + } + } + while (rows--) { + var name = menuItems[idx]; + var item = items[name]; + var hl = (idx==options.selected && !l.selectEdit); + if(g.theme.dark){ + fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+40); + }else{ + fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); + } + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign(-1,-1); + var v = item.value; + v = loc.translate(""+v); + if(loc.translate(name).length >= 17-v.length && "object" == typeof item){ + if (item.format) v=item.format(v); + g.drawString(loc.translate(name).substring(0, 12-v.length)+"...",x+3.7,iy+2.7); + }else{ + if(loc.translate(name).length >= 15){ + g.drawString(loc.translate(name).substring(0, 15)+"...",x+3.7,iy+2.7); + }else{ + g.drawString(loc.translate(name),x+3.7,iy+2.7); + } + } + if ("object" == typeof item) { + var xo = x2; + var v = item.value; + if (item.format) v=item.format(v); + v = loc.translate(""+v); + if (l.selectEdit && idx==options.selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2}); + } + g.setFontAlign(1,-1); + g.drawString(v,xo-2,iy+1); + } + g.setColor(g.theme.fg); + iy += options.fontHeight; + idx++; + } + g.setFontAlign(-1,-1); + g.setColor((idxitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) item.onchange(item.value); + l.draw(options.selected,options.selected); + } else { + var lastSelected=options.selected; + options.selected = (dir+options.selected+menuItems.length)%menuItems.length; + l.draw(Math.min(lastSelected,options.selected), Math.max(lastSelected,options.selected)); + } + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + return l; +}; diff --git a/apps/ptlaunch/README.md b/apps/ptlaunch/README.md index cf75315a9..12c205980 100644 --- a/apps/ptlaunch/README.md +++ b/apps/ptlaunch/README.md @@ -2,11 +2,19 @@ Directly launch apps from the clock screen with custom patterns. -## Usage +## Installation and Usage + +Install Pattern Launcher alongside your main laucher app. +_Do not delete that launcher!_ +Pattern Launcher is designed as an additional app launching utility, not as a replacement for the main launcher. + +In the main launcher, start Pattern Launcher in the app menu to assign the pattern configuration (see below). +Note that this actually among the applications, _not_ in the application settings! Create patterns and link them to apps in the Pattern Launcher app. Then launch the linked apps directly from the clock screen by simply drawing the desired pattern. +Note that this does only work in the clock screen, not if other applications run. ## Add Pattern Screenshots @@ -28,7 +36,8 @@ Then launch the linked apps directly from the clock screen by simply drawing the ## Detailed Steps -From the main menu you can: +The main menu of Pattern Launcher is accessible from the _application_ starter of the main launcher. +From there you can: - Add a new pattern and link it to an app (first entry) - To create a new pattern first select "Add Pattern" @@ -60,6 +69,16 @@ Make sure the watch is unlocked before you start drawing. If this bothers you, y Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry! +4. Where can I configure the patterns? + +You have to start the "Pattern Launcher" app from the main app launcher's app selection. + +5. Do I have to delete my former app launcher so that Pattern Launcher is the only installed launcher? + +No! Pattern Launcher works alongside another "main" launcher. +If you have deleted that one, you do not have a general purpose app launcher any more and cannot access Pattern Launcher's configuration. +If you already have deleted your main launcher accidentially, just reinstall it from the app loader. + ## Authors Initial creation: [crazysaem](https://github.com/crazysaem) @@ -67,3 +86,5 @@ Initial creation: [crazysaem](https://github.com/crazysaem) Improve pattern detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/) Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/) + +Doc additions: [dirkhillbrecht](http://forum.espruino.com/profiles/182498/) diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog new file mode 100644 index 000000000..0950b7ae0 --- /dev/null +++ b/apps/puzzle15/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version, UI mechanics ready, no real game play so far +0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... +0.03: Menu logic now generally functioning, splash screen added. The first really playable version! +0.04: Settings dialog, about screen +0.05: Central game end function diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md new file mode 100644 index 000000000..16c0c4593 --- /dev/null +++ b/apps/puzzle15/README.md @@ -0,0 +1,57 @@ +# Puzzle15 - A 15-puzzle for the Bangle.js 2 + +This is a Bangle.js 2 adoption of the famous 15 puzzle. + +## The game + +A board of _n_ by _n_ fields is filled with _n^2-1_ numbered stones. So, one field, the "gap", is left free. + +Bring them in the correct order so that the gap is finally at the bottom right of the playing field. +The less moves you need, the better you are. + +If _n_ is 4, the number of stones is _16-1=15_. Hence the name of the game. + +## How to play + +If you start the game, it shows a splash screen and then generates a shuffled 4x4 board with a 15 puzzle. +Move the stones with drag gestures on the screen. +If you want to move the stone below the gap upward, drag from the bottom of the screen upward. +The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved. + +If you managed to order the stones correctly, a success message appears. +You can continue with another game, go to the game's main menu, or quit the game entirely. + +There is a grey menu button right of the board containing the well-known three-bar menu symbol ("Hamburger menu"). +It opens the game's main menu directly from within the game. + +## The main menu + +Puzzle15 has a main menu which can be reached from the in-game menu button or the end-of-game message window. +It features the following options: + +* **Continue** - Continue the currently running game. _This option is only shown if the main menu is opened during an open game._ +* **Start 3x3**, **Start 4x4**, **Start 5x5** - Start a new game on a board with the respective dimension. Any currently open game is dropped. +* **About** Show a small "About" info box. +* **Exit** Exit Puzzle15 and return to the default watch face. + +## Game settings + +The game has some global settings which can be accessed on the usual way through the Bangle.js' app settings user interface. +Currently it has the following options: + +* **Splash** - Define whether the game should open with a splash screen. **long** shows the splash screen for five seconds, **short** shows it for two seconds. **off** starts the app _without_ a splash screen, it directly comes up with whatever the "Start with" option says. +* **Start with** - What should happen after the splash screen (or, if it is disabled, directly at app start): **3x3**, **4x4** and **5x5** start the game with a board of the respective dimension, **menu** shows the main menu which allows to select the board size. + +## Implementation notes + +The game engine always generates puzzles which can be solved. + +Solvability is detected by counting inversions, +i.e. pairs of stones where the stone at the earlier field (row-wise, left to right, top to bottom) has a number _greater than_ the stone on the later field, with all pairs of stones compared. +The algorithm is described at https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ . + +## The splash screen + +The Splash screen shows a part of the illustration "The 14-15-puzzle in puzzleland" from Sam Loyd. Other than Puzzle15, it depicts a 15 puzzle with the stones "14" and "15" swapped. This puzzle is indeed *not* solvable. + +Have fun! diff --git a/apps/puzzle15/introscreen.png b/apps/puzzle15/introscreen.png new file mode 100644 index 000000000..766cd61fa Binary files /dev/null and b/apps/puzzle15/introscreen.png differ diff --git a/apps/puzzle15/puzzle15.app-icon.js b/apps/puzzle15/puzzle15.app-icon.js new file mode 100644 index 000000000..04fb4a665 --- /dev/null +++ b/apps/puzzle15/puzzle15.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AC3+7oAD7e7AAW8BQndBQe79/9DomgHocH74KD/RJE34Xax4XDtvoC4fJ54XDluAC4f2z4XDzm/C4ett4XD34OBF4e/I4m+C4f8r4XChHuC5U98oXEF4cP7/AC5O9mYXC/2/F4cGtwvE/SsBC4Ws7gvD7YCBL4ULO4i/u1QAD7QED1e6AoetCAnf/YeE1wpD/lgBQcKIAgXG14LD/twC5kL3Z+BC4P+LgIXBg272wXD7wXEh7eCC4PWzIXChHtOoIXB/WX54XDh3KmAXC1oLBI4UD+AXC+/rdIIvD5wvD3O4C4cJ4AXC/dUI4kJhgMBC4Ov+AXDh9QC4X2/gvEhvvoAXC81dC4duR4f8wSncC6v8u4AD3ndAAXcy4KDtYKD7vf/oGE2wRDvPNBQfLFAnP/o2EVIIACg7yBAATZBAAe/C7P9g4XCx+wn/6C4Op//AC4MK+cI/+QC4X2/fPC4PM2HKh8H7vpewIXBhvThV5+AXC+/5C4UL2HHC4Pf/P/AIJHB6cAj2wC4X+3AXPhADBF4fX94XB1va1vOC4PXAIX6hfrxvb0CPD7p3C1e6hW2C4LOBAIIXB3eJ3YXEX78GM4IAC9QXG1QAD7QEDJYIFD14oE//7DwgME/twBQcPC70G6EG5dQ1/8VYPtC4ObgfM5IXHr/whvO4Gvy6LBtX9vfugnr3AXHkXggGOC4P97/43X9ukOgnv6BfIC4Oe2AXC6+nI4MOgfI9QXJhssF4f91AXCgnA9IXHr3u1HusGv3Ob//s/t693l3xHJX9v+3YAD7oAE5YKD34XFAC4=")) diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js new file mode 100644 index 000000000..eec21933c --- /dev/null +++ b/apps/puzzle15/puzzle15.app.js @@ -0,0 +1,771 @@ +// A 15-puzzle game for the Bangle.js 2 clock +// (C) Dirk Hillbrecht 2022 +// Released unter the terms of the MIT license + +// The intro screen as large base-64-encoded binary data +const introscreen = E.toArrayBuffer(atob("sLABwAAAAA5QAAACAAAAHAAMEDgA/F/nvoAAAAA+3AAAAAAAAB4AQBIkAPwv//4AAAAAP/wQAAAAAAAAAA3wBAD8P//+AAAAAD7tGQAAAAAAAIADAAMF2C///wAAACA4oRmAHx/wQAAAAAABOfgP//8AAAAAAGEYgA+/AMAAQAAAAAP8B///AAAGAABxMKgD/AAABGAAAAAD/Af//wAAYQAr6TOoAfwAIAWTgAAAA/4D//8SAeCA9W0/mAH8ABAEBAAAAAP+A///YAMRh/5of/ADwMAYOAAAAAAD/gH/f4AMYd/3dv+AAwUABSAAAAAAA/8A/XIAMDrPn///gAMQAADAABgAAAH3APBAAMAdX3/fvjACEAQAAA/mAAAB/wcAAAcAz1/+3/34AgAAgADuF+AAAP8YAA8MEJ+Bt29/GAIAACAADAgwAAD/4AACMAyv/+f/6HwGAABAAE2IoAAAfgAAB8ADf/////mOADh5gABNCCAAAHwB/x4AAX//5/L9D4AAAAYBCQjgAA3QAgBgAAE//7P//jzAAADPAGkAoADggABwAwAC3//F9/5n4AAB/wFpACAHAADAAAEAA///c/v/4jABwf+A8Xwh+AD0BAAAAIfvPm/G/+IYA8P3wPh+P4AHAAAB+ACH//+Hy/9iCAfD/8CYX/QAL9AAAAcAme//z/PrR6gB4P/guPgAAODAGBwAAI8+7gf0P6/oAeD/4bjh//wxA4AAAw2f//8f+f+PiAHwf/+53AA/+AAAAA4Gv/P/9/A/nwgB8D6AuWD8EAAAYAD4A+7/8X/ADr8IAfAwgZlO/AAAHwAH4L2/////mDz9CAD6MB65YAAAAf+A/4D23/n/v4j4fcgA/wDguWAAAB8AD7ngg1////+EAD/Pg8AGDZlmgAP8e4zCX4aP///9AZA/D/4AAAGYf//////4ZwG2j/8H/gwLPggAAAAfWH/////8AQRCpr/+E/9IBT4IAA/hj1h///wFgIqBBO9//33/m6i8CAB8A//YH/gDMfGO/WH/0/5//4gEfAgP4Af+AB/gAHFEBoaTH+/+w/8fQDgI+aP//iifYABRCJ9a9w///+P+H/M4Gw8//34o3yBKkMUW0e3Pt//i6AHxeDg///gouP/poNPAnjzp/3v+2PgDfngz/v3rvjhO6bTTqDWsY//v/nz4LD/4N6bff3YYHu7UU6yZcm////5+3Kgf/CtMP/b+GJ7hr9O41tNB///+XNhAb/hv3kv/7hg7MQn3o+4N5Y9//gx8ME+59P47//gYK6Pnbvc5bu6///48MFl+d93/yv++GAVi+X51IyKqA//+eC4AzP0P3y4//hgb4/83QXYfjAP//1MYbwn9D/77/14YCnf/gIHLX96D//5WGA8Z7Bnf7b/8GgKG94GXuU9y4P/+DwwCeeafvfm93hubX8dvF3E6Ar/N/54OATj2Du/Pv/qY10/zTBB1h+CAff/8AwH4dmb6t7/2mN+IuRyI4CBngD///AMx6Xsnve+09pkNvgsQwAAWAHwo/+hBAeh5Czt5/HcZxSQ6AceCAAiAK/9ggQHBNBuZ5eQvGOducgFgA1BxADx5MIEX/Jo2p8YR/xjt4wINYAfYAf/K4TDAV8Ye48DLBDXY46ADQmBIe+HoSOMgwj5hFqH+CAB78O6AAX4gACAAgMPjYEK4f//7p/2f/NjOuAH8MAACACb5YmCCsDSPnbADz7w49+lI8BAAAf8aICRgAjACB/O4Ag1VcMuD/eAQD/oHISSsYEMzhg/qtABft//7LACAG/gwAMg2MGBCJ4cPD25gBN95Ovn5UA4DgHRncFAxQijj//5KA/jvcL6gAJBEAAf7fBB4MUI+fv/szeEAaqjO/gObYAAAAz57GDpAO5x19BIA/9uYw7gEyMQHwAD/mAg8KBjwv9Giv/vrvKQHBNAEsAUAP9kuP+gZAff+dcEX/ZljjebYDAAAAnf8Pj/YKsH/2E1HEAUQGTB21gwAAAGS5NYv0C6jP1mJY5MEqV0f9fcAfPoHM7qWL/EDg39PGcex7iu+4Bz7B/AD/63qRg/kwML/+DJR9PvOUTAK64RwYAcjek5f/4Yv/aT20L7v0m6gCNvNiYgDI15iE//jH/+2zij+9HsMY8qrA98f+CL9SiP/8//8/gkJ/Ht99M/OogD+MAOifQJj//nH//Y5KPw7//FLBZACAH+Arn8GV//////zMVT36V5sdAecAgCkHqP5H3///4v/+cbUdvIc7wADDAId4AegXuX///////iaVfkvNu/+wYA5Aj/AJl6j////v3/uyOW91wcvqImAZIcQeCBeg/////f/v8MqV/7gZ2cDwICMYAcgXpf////v+/JmJv5/7+B9A+HAiKYA5Bcb//////nxMOI8f92d+APDgZF34SbeU7///72/naafn22WAD/DwwCigR+kHlO///9/9fmWFt/+GMiT8uMARV/F5B/XP///5BTmsGTV/3EADfxlAMsfqjZ/XT///X/l/9FJs//CADDuQgEUnw08vnW/3/38tvx7G3v/BHeOfm4DNT4zpJ7uj///5NI//Cef/gvBlf5uBig8Og/aN////YVe9iWuvnwAD7g/JoxYeF0k/tf///s0U7c8ozbz4BwEHztJoPyrIdrZ//////qz5DpZvcDgBw/7MVHxc+mc+/8P/jL//+PV358GAACPtyKD4SYt/PZ+B////8/+dk/gDgB+G5tNA8fvir/HfNfAG4f/+9q8AAAAABgayg+E0Ot3/flDwADAP/4W/AHAAAAUfJoHizE7/xzww5AMPAAAXoQGADAAH9M/wZF2Sb/b4B3KAQPiLSOMAAAOAA4if/wmz3m+/eAOBwAA/aljmEAAAwAALNz/7Z8v/7/BIgbLwA3jBjNAIAHwAGnD39kTKe//3gBxeAAH0oreAP/+Pok7YH9HgCv7/+gABPAAADcxoADff4h3olwHy/Cvet+YAeAACgO/b+AR//g7geWjwP+/Kf/+IDgAeQXeEt3A+53AH4dKMDH//wnu/BIwDAYIbi/VQa/2bol+knwPg99r7/+MIAAAgDQ33L2P+/rLeyTfiaA/Cf/9kgAADYAIN+7f/ef+dj5pzj54Bynf8kiAABM8Dh+/r//+/anc0/A+7wH/9+On8AAHhgsf/1+CAD31mbA4bcDgI//DyQAAwAxrP/f4AgAKfzNlMNlkGib7gABdAYYBrD/3wLwAD//m3DGSPwYn/wA8BZgBg5S//i5AAAD/xR5x5uOHpP4AAQewAAPVn/nwAAAAP4o89mvDjSXsYAfH5AIB9///4Pwfx7+0debX4R09+pgeQHA8Bn/78f/nwAH/5DfLvzgfp3gAAkI0AJxL//+fjAAAP8k/ly4YOgF4no4Z3AAYH/xAL/gAAB7/DyJcGPz9cSA2XgaAPB+2AH/4AAAZ/+BonFj8B3AYAfAtiLg//A5QAQAAOQ/5WZg5qSVwYAAMAxK4P//jcAZyAGbD/sIQctMlYsAD9wclSf//gEAAB/B84H9UEPdm5cgAAAwOKMP//8eAGs/9i/gfaBnmziXYIABwXsRN7f//Cb+D/7fmB1wfzoqvktAAATGQD/3/vwdgAG918YP3h5+Q/0fmAAJMRZb///ZEQAAcT5xV/+A7MCdgIMAABBqt///+WEAADp+MNo/4XkCj4AMAAbILUf///vEhAgkvDH6D/vzA/mAgQIAPBrDpf/7D7+eSThxt4P/pgbnhkYCAQYDs//v/3/7P5Nw8+3g/+gPvgDhDABhgZP3//v3/j828efJsB/YDP8BjhAPM8Hf/9f/9/7eaDPPkwYH/g9BxIAQBAhA9Pa3/v/+3Fi/yz8Fgv+ASUAQYAJuOKH/9///+7yrj7p/MH//4/5CM8AwEw4S+7df/+uxx59ejvh2D//0YA+sAADhDq+d9v+b++M+zR3IfYf9ibgASEcAgCX1br+8i+fz+Nk7memA/J5IADiYYC2XwbbGK0/f/OHydxpz4DxiJgAAICAAY8e/3vd/mP8C5G4y93gP8hOAHhOeAA86rOvEfzQ/hMjudE4GAwlLwAgUcI4wtr77vf9uH+rR3OZI5YCMRvgAQAABDhR73nv8jwf7o5nnmfBx/yV+AIAAP+IY3/lX/ZvB90sxwzTkHiMZn4AAGQAxhXtX33E+8HoPI+ZhzwSAgXfwARwQPlaSv4dyfhg9w2cMw4+PjCYr+AEewAkhdlfvRP5qH/B+/YeODnIiHe4OAYAHCeld7/m88f/4ONkOHh2hjM//gAAAA4AK/n8TOLFp/gIzHjwsyYBXf8AAAACBA153p3ByYH8GZRw4ZMYwpM3wAwAAwA7E3k7j5Ng/zEg4cO0xOKyz3AAgABALQf6M44++D/sYcOD7iMRZHP8AAAAAGOFpkcObPwf7kOHhO3SkGQYbwANgBCDfUyOjun/B+yPjwzPzHJYlf2AOMAAQEjLHd1T+cHxDw4dj+YvjKX/wOBgBwAr/4udN/Zg/4McJxv9xMMLD/GAcACk+o/hOur3Cv/ifD4ZfsMizgO/BHgAEQfC8/bZz8zruHh+KR//FIYA/n2fAAif4ZjrmcnLR/w4jEMH8kQkAAwAJmJEB/j8GSOcFKH/AZjCAz6pCAAAAAHwIho8bjZnNjegf4GxgwD/GUgAGAAB+BIqDh/k3gZKeD/jYRMAB4ZwABwAAIg55/8P3ZwMrM4P/KISAAHimAAOAACIHD//59s8nJDzB/zFMAAAMLwAAi4B7Aen/8P3+7lJpcH9gHAAABzgAACNs+gBs/fx5Pd3ExxgfxngAAAOcCAAMBf4AEB/+P42diY8MD8w4AAAB/wfAOAB4AD4H3w/DcFMOQwP/DAAAAH+B+4AAIAAD8v/X4femHBzl/4AAAAAHwAQAACAAAJ39x/Ct5Dh8Lf/gAAAAAP4AAEh4AADe+/D8HUgxvGf/8AAAAAAOwABf4AAAMT/8fxOQd3i9//8AAAAAAzAAAYAAAD5d/P+5IMxxH//8wAAAAAHYBgCCMAAOPP4fxkHw4Vv/3DAAAAAA5AGAhRwAA/D/D/bD4cMx//wOAAAAADAAf8BAAABwH4f9xzvE/f/4A4AAAAAYAB/gpgAAaf/v+QcPim3//AB4AAAABwAP8AAAABg/8fYeDxmT//wABwAAAAHwD6AADgAHP/h/Dg+1Zf/8=")); + +// *** Global constants from which several other settings are derived + +// Minimum number of pixels to interpret it as drag gesture +const dragThreshold = 10; + +// Maximum number of pixels to interpret a click from a drag event series +const clickThreshold = 3; + +// Number of steps in stone move animation +const animationSteps = 6; + +// Milliseconds to wait between move animation steps +const animationWaitMillis = 30; + +// Total width of the playing field (full screen width) +const fieldw = g.getWidth(); + +// Total height of the playing field (screen height minus widget zones) +const fieldh = g.getHeight() - 48; + + +// *** Global game characteristics + +// Size of the playing field +var stonesPerLine; + +// Size of one field +var stonesize; + +// Actual left start of the playing field (so that it is centered) +var leftstart; + +// Actual top start of the playing field (so that it is centered) +var topstart; + +// Number of stones on the board (needed at several occasions) +var stonesPerBoard; + +// Set the stones per line globally and all derived values, too +function setStonesPreLine(bPL) { + stonesPerLine = bPL; + stonesize = Math.floor(Math.min(fieldw / (stonesPerLine + 1), fieldh / stonesPerLine)) - 2; + leftstart = (fieldw - ((stonesPerLine + 1) * stonesize + 8)) / 2; + topstart = 24 + ((fieldh - (stonesPerLine * stonesize + 6)) / 2); + stonesPerBoard = (stonesPerLine * stonesPerLine); +} + + +// *** Global app settings + +var SETTINGSFILE = "puzzle15.json"; + +// variables defined from settings +var splashMode; +var startWith; + +/* For development purposes +require('Storage').writeJSON(SETTINGSFILE, { + splashMode: "off", + startWith: "5x5", +}); +/* */ + +/* OR (also for development purposes) +require('Storage').erase(SETTINGSFILE); +/* */ + +// Helper method for loading the settings +function def(value, def) { + return (value !== undefined ? value : def); +} + +// Load settings +function loadSettings() { + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + splashMode = def(settings.splashMode, "long"); + startWith = def(settings.startWith, "4x4"); +} + + +// *** Low level helper classes + +// One node of a first-in-first-out storage +class FifoNode { + constructor(payload) { + this.payload = payload; + this.next = null; + } +} + +// Simple first-in-first-out (fifo) storage +// Needed to keep the stone movements in order +class Fifo { + + // Initialize an empty Fifo + constructor() { + this.first = null; + this.last = null; + } + + // Add an element to the end of the internal fifo queue + add(payload) { + if (this.last === null) { // queue is empty + this.first = new FifoNode(payload); + this.last = this.first; + } else { + let newlast = new FifoNode(payload); + this.last.next = newlast; + this.last = newlast; + } + } + + // Returns the first element in the queue, null if it is empty + remove() { + if (this.first === null) + return null; + let oldfirst = this.first; + this.first = this.first.next; + if (this.first === null) + this.last = null; + return oldfirst.payload; + } + + // Returns if the fifo is empty, i.e. it does not hold any elements + isEmpty() { + return (this.first === null); + } + +} + +// Helper class to keep track of tasks +// Executes tasks given by addTask. +// Tasks must call Worker.endTask() when they are finished, for this they get the worker passed as parameter. +// If a task is given with addTask() while another task is still running, +// it is queued and executed once the currently running task and all +// previously scheduled tasks have finished. +// Tasks must be functions with the Worker as first and only parameter. +class Worker { + + // Create an empty worker + constructor() { + this.tasks = new Fifo(); + this.busy = false; + } + + // Add a task to the worker + addTask(task) { + if (this.busy) // other task is running: Queue this task + this.tasks.add(task); + else { // No other task is running: Execute directly + this.busy = true; + task(this); + } + } + + // Called by the task once it finished + endTask() { + if (this.tasks.isEmpty()) // No more tasks queued: Become idle + this.busy = false; + else // Call the next task immediately + this.tasks.remove()(this); + } + +} + +// Evaluate "drag" events from the UI and call handlers for drags or clicks +// The UI sends a drag as a series of events indicating partial movements +// of the finger. +// This class combines such parts to a long drag from start to end +// If the drag is short, it is interpreted as click, +// otherwise as drag. +// The approprate method is called with the data of the drag. +class Dragger { + + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { + this.clickHandler = clickHandler; + this.dragHandler = dragHandler; + this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold); + this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold); + this.dx = 0; + this.dy = 0; + this.enabled = true; + } + + // Enable or disable the Dragger + setEnabled(b) { + this.enabled = b; + } + + // Handle a raw drag event from the UI + handleRawDrag(e) { + if (!this.enabled) + return; + this.dx += e.dx; // Always accumulate + this.dy += e.dy; + if (e.b === 0) { // Drag event ended: Evaluate full drag + if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold) + this.clickHandler({ + x: e.x - this.dx, + y: e.y - this.dy + }); // take x and y from the drag start + else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold) + this.dragHandler({ + x: e.x - this.dx, + y: e.y - this.dy, + dx: this.dx, + dy: this.dy + }); + this.dx = 0; // Clear the drag accumulator + this.dy = 0; + } + } + + // Attach the drag evaluator to the UI + attach() { + Bangle.on("drag", e => this.handleRawDrag(e)); + } +} + +// *** Mid-level game mechanics + +// Representation of a position where a stone is set. +// Stones can be moved from field to field. +// The playing field consists of a fixed set of fields forming a square. +// During an animation, a series of interim field instances is generated +// which represents the locations of a stone during the animation. +class Field { + + // Generate a field with a left and a top coordinate. + // Note that these coordinates are "cooked", i.e. they contain all offsets + // needed place the elements globally correct on the screen + constructor(left, top) { + this.left = left; + this.top = top; + this.centerx = (left + stonesize / 2) + 1; + this.centery = (top + stonesize / 2) + 2; + } + + // Returns whether this field contains the given coordinate + contains(x, y) { + return (this.left < x && this.left + stonesize > x && + this.top < y && this.top + stonesize > y); + } + + // Generate a field for the given playing field index. + // Playing field indexes start at top left with "0" + // and go from left to right line by line from top to bottom. + static forIndex(index) { + return new Field(leftstart + (index % stonesPerLine) * (stonesize + 2), + topstart + (Math.floor(index / stonesPerLine)) * (stonesize + 2)); + + } + // Special field for the result "stone" + static forResult() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart + ((stonesPerLine - 1) * (stonesize + 2))); + } + + // Special field for the menu + static forMenu() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart); + } + +} + +// Representation of a moveable stone of the game. +// Stones are moved from field to field to solve the puzzle +// Stones are numbered from 0 to the maximum number ot stones. +// Stone "0" represents the gap on the playing field. +// The main knowledge of a Stone instance is how to draw itself. +class Stone { + + // Create stone with the given number + // The constructor creates the "draw()" function which is used to draw the stone + constructor(number, targetindex) { + this.number = number; + this.targetindex = targetindex; + // gap: Does not draw anything + if (number === 0) + this.draw = function(field) {}; + else if ((number + (stonesPerLine % 2 == 0 ? (Math.floor((number - 1) / stonesPerLine)) : 0)) % 2 == 0) { + // Black stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(1, 1, 1).drawString(number, field.centerx, field.centery); + }; + } else { + // White stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.drawString(number, field.centerx, field.centery); + }; + } + } + + // Returns whether this stone is on its target index + isOnTarget(index) { + return index === this.targetindex; + } +} + +// Helper class which knows how to clear the rectangle opened up by the two given fields +class Clearer { + + // Create a clearer for the area between the two given fields + constructor(startfield, endfield) { + this.minleft = Math.min(startfield.left, endfield.left); + this.mintop = Math.min(startfield.top, endfield.top); + this.maxleft = Math.max(startfield.left, endfield.left); + this.maxtop = Math.max(startfield.top, endfield.top); + } + + // Clear the area defined by this clearer + clearArea() { + g.setColor(1, 1, 1); + g.fillRect(this.minleft, this.mintop, + this.maxleft + stonesize, this.maxtop + stonesize); + } + +} + +// Helper class which moves a stone between two fields +class Mover extends Clearer { + + // Create a mover which moves the given stone from startfield to endfield + // and animate the move in the given number of steps + constructor(stone, startfield, endfield, steps) { + super(startfield, endfield); + this.stone = stone; + this.startfield = startfield; + this.endfield = endfield; + this.steps = steps; + } + + // Create the coordinate between start and end for the given step + // Computation uses sinus for a smooth movement + stepCoo(start, end, step) { + return start + ((end - start) * ((1 + Math.sin((step / this.steps) * Math.PI - (Math.PI / 2))) / 2)); + } + + // Compute the interim field for the stone to place during the animation + stepField(step) { + return new Field( + (this.minleft === this.maxleft ? this.minleft : + this.stepCoo(this.startfield.left, this.endfield.left, step)), + (this.mintop === this.maxtop ? this.mintop : + this.stepCoo(this.startfield.top, this.endfield.top, step))); + } + + // Perform one animation step + animateStep(step, worker) { + this.clearArea(); + this.stone.draw(this.stepField(step)); + if (step < this.steps) // still steps left: Issue next step + setTimeout(function(t) { + t.animateStep(step + 1, worker); + }, animationWaitMillis, this); + else // all steps done: Inform the worker + worker.endTask(); + } + + // Start the animation, this method is called by the worker + animate(worker) { + this.animateStep(1, worker); + } + +} + +// Representation of the playing field +// Knows to draw the field and to move a stone into a gap +class Board { + + // Generates the actual playing field with all fields and stones + constructor() { + this.fields = []; + this.resultField = Field.forResult(); + this.menuField = Field.forMenu(); + for (i = 0; i < stonesPerBoard; i++) + this.fields[i] = Field.forIndex(i); + this.setShuffled(); + //this.setAlmostSolved(); // to test the game end + } + + /* Set the board into the "solved" position. Useful for showcasing and development + setSolved() { + this.stones = []; + for (i = 0; i < stonesPerBoard; i++) + this.stones[i] = new Stone((i + 1) % stonesPerBoard, i); + this.moveCount = 0; + } + /* */ + + /* Initialize an almost solved playing field. Useful for tests and development + setAlmostSolved() { + this.setSolved(); + b = this.stones[this.stones.length - 1]; + this.stones[this.stones.length - 1] = this.stones[this.stones.length - 2]; + this.stones[this.stones.length - 2] = b; + } + /* */ + + // Initialize a shuffled field. The fields are always solvable. + setShuffled() { + let nrs = []; // numbers of the stones + for (i = 0; i < stonesPerBoard; i++) + nrs[i] = i; + this.stones = []; + let count = stonesPerBoard; + for (i = 0; i < stonesPerBoard; i++) { + // Take a random number of the (remaining) numbers + let curridx = Math.floor(Math.random() * count); + let currnr = nrs[curridx]; + // Initialize the next stone with that random number + this.stones[i] = new Stone(currnr, (currnr + (stonesPerBoard - 1)) % stonesPerBoard); + // Remove the number just taken from the list of numbers + for (j = curridx + 1; j < count; j++) + nrs[j - 1] = nrs[j]; + count -= 1; + } + // not solvable: Swap the first and second stone which are not the gap. + // This will always result in a solvable board. + if (!this.isSolvable()) { + let a = (this.stones[0].number === 0 ? 2 : 0); + let b = (this.stones[1].number === 0 ? 2 : 1); + let bx = this.stones[a]; + this.stones[a] = this.stones[b]; + this.stones[b] = bx; + } + this.moveCount = 0; + } + + // Draws the complete playing field + draw() { + new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); + for (i = 0; i < this.fields.length; i++) + this.stones[i].draw(this.fields[i]); + this.drawResult(null); + this.drawMenu(); + } + + // returns the index of the field left of the field with the given index, + // -1 if there is none (index indicates already a leftmost field on the board) + leftOf(index) { + return (index % stonesPerLine === 0 ? -1 : index - 1); + } + + // returns the index of the field right of the field with the given index, + // -1 if there is none (index indicates already a rightmost field on the board) + rightOf(index) { + return (index % stonesPerLine === (stonesPerLine - 1) ? -1 : index + 1); + } + + // returns the index of the field top of the field with the given index, + // -1 if there is none (index indicates already a topmost field on the board) + topOf(index) { + return (index >= stonesPerLine ? index - stonesPerLine : -1); + } + + // returns the index of the field bottom of the field with the given index, + // -1 if there is none (index indicates already a bottommost field on the board) + bottomOf(index) { + return (index < (stonesPerLine - 1) * stonesPerLine ? index + stonesPerLine : -1); + } + + // Return the index of the gap in the field, -1 if there is none (should never happel) + indexOf0() { + for (i = 0; i < this.stones.length; i++) + if (this.stones[i].number === 0) + return i; + return -1; + } + + // Returns the row in which the gap is, 0 is upmost + rowOf0() { + let idx = this.indexOf0(); + if (idx < 0) + return -1; + return Math.floor(idx / stonesPerLine); + } + + // Searches the gap on the field and then moves one of the adjacent stones into it. + // The stone is selected by the given startfunc which returns the index + // of the selected adjacent field. + // Startfunc is one of (left|right|top|bottom)Of. + moveTo0(startfunc, worker) { + let endidx = this.indexOf0(); // Target field (the gap) + if (endidx === -1) { + worker.endTask(); + return; + } + let startidx = startfunc(endidx); // Start field (relative to the gap) + if (startidx === -1) { + worker.endTask(); + return; + } + // Replace in the internal representation + let moved = this.stones[startidx]; + this.stones[startidx] = this.stones[endidx]; + this.stones[endidx] = moved; + this.moveCount += 1; + // Move on screen using an animation effect. + new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(worker); + } + + // Move the stone right from the gap into the gap + moveRight(worker) { + this.moveTo0(this.leftOf, worker); + } + + // Move the stone left from the gap into the gap + moveLeft(worker) { + this.moveTo0(this.rightOf, worker); + } + + // Move the stone above the gap into the gap + moveUp(worker) { + this.moveTo0(this.bottomOf, worker); + } + + // Move the stone below the gap into the gap + moveDown(worker) { + this.moveTo0(this.topOf, worker); + } + + // Check if the board is solved (all stones at the right position) + isSolved() { + for (i = 0; i < this.stones.length; i++) + if (!this.stones[i].isOnTarget(i)) + return false; + return true; + } + + // counts the inversions on the board + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + getInversionCount() { + let inversions = 0; + for (outer = 0; outer < stonesPerBoard - 1; outer++) { + let outernr = this.stones[outer].number; + if (outernr === 0) + continue; + for (inner = outer + 1; inner < stonesPerBoard; inner++) { + let innernr = this.stones[inner].number; + if (innernr > 0 && outernr > innernr) + inversions++; + } + } + return inversions; + } + + // return whether the puzzle is solvable + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + isSolvable() { + let invs = this.getInversionCount(); + if (stonesPerLine % 2 !== 0) // odd number of rows/columns + return (invs % 2 === 0); + else { + return ((invs + this.rowOf0()) % 2 !== 0); + } + } + + // draw the result field, pass null as argument if not called from worker + drawResult(worker) { + let field = this.resultField; + let solved = this.isSolved(); + if (solved) + g.setColor(0, 1, 0); + else + g.setColor(1, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery); + if (worker !== null) + worker.endTask(); + if (solved) + setTimeout(() => { + gameEnd(this.moveCount); + }, 500); + } + + // draws the menu button + drawMenu() { + let field = this.menuField; + g.setColor(0.5, 0.5, 0.5); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + let l = field.left + 8; + let r = field.left + stonesize - 8; + let t = field.top + 5; + for (i = 0; i < 3; i++) + g.fillRect(l, t + (i * 6), r, t + (i * 6) + 2); + } + +} + + +// *** Global helper methods + +// draw some text with some surrounding to increase contrast +// text is drawn at given (x,y) position with textcol. +// frame is drawn 2 pixels around (x,y) in each direction in framecol. +function framedText(text, x, y, textcol, framecol) { + g.setColor(framecol); + for (i = -2; i < 3; i++) + for (j = -2; j < 3; j++) { + if (i === 0 && j === 0) + continue; + g.drawString(text, x + i, y + j); + } + g.setColor(textcol).drawString(text, x, y); +} + +// Show the splash screen at program start, call afterSplash afterwards. +// If spash mode is "off", call afterSplash directly. +function showSplash(afterSplash) { + if (splashMode === "off") + afterSplash(); + else { + g.reset(); + g.drawImage(introscreen, 0, 0); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(afterSplash, (splashMode === "long" ? 2000 : 1000)); + }, (splashMode === "long" ? 1000 : 1)); + }, (splashMode === "long" ? 2000 : 1000)); + } +} + + +// *** Global flow control + +// Initialize the game with an explicit number of stones per line +function initGame(bpl) { + setStonesPreLine(bpl); + newGame(); +} + +// Start a new game with the same number of stones per line as before +function newGame() { + board = new Board(); + continueGame(); +} + +// Continue the currently running game +function continueGame() { + E.showMenu(); + board.draw(); + dragger.setEnabled(true); +} + +// Show message on game end, allows to restart new game +function gameEnd(moveCount) { + dragger.setEnabled(false); + E.showPrompt("You solved the\n" + stonesPerLine + "x" + stonesPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { + title: "Puzzle solved", + buttons: { + "Again": newGame, + "Menu": () => showMenu(false), + "Exit": exitGame + } + }).then(v => { + E.showPrompt(); + setTimeout(v, 10); + }); +} + +// A tiny about screen +function showAbout(doContinue) { + E.showAlert("Author: Dirk Hillbrecht\nLicense: MIT", "Puzzle15").then(() => { + if (doContinue) + continueGame(); + else + showMenu(false); + }); +} + +// Show the in-game menu allowing to start a new game +function showMenu(withContinue) { + var mainmenu = { + "": { + "title": "15 Puzzle" + } + }; + if (withContinue) + mainmenu.Continue = continueGame; + mainmenu["Start 3x3"] = () => initGame(3); + mainmenu["Start 4x4"] = () => initGame(4); + mainmenu["Start 5x5"] = () => initGame(5); + mainmenu.About = () => showAbout(withContinue); + mainmenu.Exit = exitGame; + dragger.setEnabled(false); + g.clear(true); + E.showMenu(mainmenu); +} + +// Handle a "click" event (only needed for menu button) +function handleclick(e) { + if (board.menuField.contains(e.x, e.y)) + setTimeout(() => showMenu(true), 10); +} + +// Handle a drag event (moving the stones around) +function handledrag(e) { + worker.addTask(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => board.moveRight(e) : e => board.moveLeft(e)) : + (e.dy > 0 ? e => board.moveDown(e) : e => board.moveUp(e))); + worker.addTask(e => board.drawResult(e)); +} + +// exit the game, clear screen first to prevent ghost images +function exitGame() { + g.clear(true); + setTimeout(load, 300); +} + + +// *** Main program + +g.clear(true); + +// Load global app settings +loadSettings(); + +// We need a worker... +var worker = new Worker(); + +// Board will be initialized after the splash screen has been shown +var board; + +// Dragger is needed for interaction during the game +var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +// Disable dragger as board is not yet initialized +dragger.setEnabled(false); + +// Nevertheless attach it so that it is ready once the game starts +dragger.attach(); + +// Start the game by handling the splash screen sequence +showSplash(() => { + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); + if (startWith === "3x3") + initGame(3); + else if (startWith === "4x4") + initGame(4); + else if (startWith === "5x5") + initGame(5); + else + showMenu(false); +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/puzzle15.app.png b/apps/puzzle15/puzzle15.app.png new file mode 100644 index 000000000..f95366a46 Binary files /dev/null and b/apps/puzzle15/puzzle15.app.png differ diff --git a/apps/puzzle15/puzzle15.settings.js b/apps/puzzle15/puzzle15.settings.js new file mode 100644 index 000000000..352ec4315 --- /dev/null +++ b/apps/puzzle15/puzzle15.settings.js @@ -0,0 +1,50 @@ +// Settings menu for the Puzzle15 app + +(function(back) { + var FILE = "puzzle15.json"; + // Load settings + var settings = Object.assign({ + splashMode: "long", + startWith: "4x4" + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "15 Puzzle" + }, + "< Back": () => back(), + "Splash": stringInSettings("splashMode", ["long", "short", "off"]), + "Start with": stringInSettings("startWith", ["3x3", "4x4", "5x5", "menu"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/screenshot.png b/apps/puzzle15/screenshot.png new file mode 100644 index 000000000..3a79a3ae6 Binary files /dev/null and b/apps/puzzle15/screenshot.png differ diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index bedc63141..dbf086f7d 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -4,3 +4,7 @@ 0.03: Fix theme and maps/graphing if no GPS 0.04: Multiple bugfixes 0.05: Add recording for coresensor +0.06: Add recording for battery stats + Fix execution of other recorders (*.recorder.js) + Modified icons and colors for better visibility + Only show plotting speed if Latitude is available diff --git a/apps/recorder/README.md b/apps/recorder/README.md index 4a4561f1c..87be34424 100644 --- a/apps/recorder/README.md +++ b/apps/recorder/README.md @@ -16,7 +16,8 @@ You can record * **Time** The current time * **GPS** GPS Latitude, Longitude and Altitude * **Steps** Steps counted by the step counter -* **HR** Heart rate +* **HR** Heart rate and confidence +* **BAT** Battery percentage and voltage * **Core** CoreTemp body temperature **Note:** It is possible for other apps to record information using this app @@ -25,4 +26,4 @@ function in `widget.js` for more information. ## Tips -When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. +When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a red satellite symbol, which you will see turn green when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. diff --git a/apps/recorder/app.js b/apps/recorder/app.js index fcd8d6031..5b1c63aef 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -199,9 +199,10 @@ function viewTrack(filename, info) { menu['Plot Alt.'] = function() { plotGraph(info, "Altitude"); }; - menu['Plot Speed'] = function() { - plotGraph(info, "Speed"); - }; + if (info.fields.includes("Latitude")) + menu['Plot Speed'] = function() { + plotGraph(info, "Speed"); + }; // TODO: steps, heart rate? menu['Erase'] = function() { E.showPrompt("Delete Track?").then(function(v) { diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 4d8cdddb1..8f82f1f37 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -48,41 +48,50 @@ Bangle.removeListener('GPS', onGPS); Bangle.setGPSPower(0,"recorder"); }, - draw : (x,y) => g.setColor(hasFix?"#0ff":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y) + draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y) }; }, hrm:function() { var bpm = 0, bpmConfidence = 0; - var hasBPM = false; function onHRM(h) { if (h.confidence >= bpmConfidence) { bpmConfidence = h.confidence; bpm = h.bpm; - if (bpmConfidence) hasBPM = true; } } return { name : "HR", - fields : ["Heartrate"], + fields : ["Heartrate", "Confidence"], getValues : () => { - var r = [bpmConfidence?bpm:""]; + var r = [bpm,bpmConfidence]; bpm = 0; bpmConfidence = 0; return r; }, start : () => { - hasBPM = false; Bangle.on('HRM', onHRM); Bangle.setHRMPower(1,"recorder"); }, stop : () => { - hasBPM = false; Bangle.removeListener('HRM', onHRM); Bangle.setHRMPower(0,"recorder"); }, - draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y) + draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) + }; + }, + bat:function() { + return { + name : "BAT", + fields : ["Battery Percentage", "Battery Voltage", "Charging"], + getValues : () => { + return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()]; + }, + start : () => { + }, + stop : () => { + }, + draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y) }; }, - temp:function() { var core = 0, skin = 0; var hasCore = false; @@ -106,7 +115,7 @@ hasCore = false; Bangle.removeListener('CoreTemp', onCore); }, - draw : (x,y) => g.setColor(hasCore?"#0f0":"#888").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y) + draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAwBAAAOAKPOfgZgZgZgZgfgPAAA"),x,y) }; }, steps:function() { @@ -121,7 +130,7 @@ }, start : () => { lastSteps = Bangle.getStepCount(); }, stop : () => {}, - draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y) + draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y) }; } // TODO: recAltitude from pressure sensor @@ -138,7 +147,7 @@ } }) */ - require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders)); + require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders)); return recorders; } diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog new file mode 100644 index 000000000..ebde871fa --- /dev/null +++ b/apps/ruuviwatch/ChangeLog @@ -0,0 +1,2 @@ +1.00: Hello Ruuvi Watch! +1.01: Clear gfx on startup. \ No newline at end of file diff --git a/apps/ruuviwatch/README.md b/apps/ruuviwatch/README.md new file mode 100644 index 000000000..bf4358267 --- /dev/null +++ b/apps/ruuviwatch/README.md @@ -0,0 +1,25 @@ +# Ruuvi Watch + +Watch the status of [RuuviTags](https://ruuvi.com) in range. + + - Id + - Temperature (°C) + - Humidity (%) + - Pressure (hPa) + - Battery voltage + + Also shows how "fresh" the data is (age of reading). + + ## Usage + + - Scans for devices when launched and every N seconds. + - Page trough devices with BTN1/BTN3. + - Trigger scan with BTN2. + +## Todo / ideas + + - Allow to "name" known devices + - Prevent flicker when updating + - Include more data + - Support older Ruuvi protocols + diff --git a/apps/ruuviwatch/ruuviwatch.app-icon.js b/apps/ruuviwatch/ruuviwatch.app-icon.js new file mode 100644 index 000000000..7ed27ef6c --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA")) \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js new file mode 100644 index 000000000..46218a323 --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -0,0 +1,151 @@ +require("Storage").write("ruuviwatch.info",{ + "id":"ruuviwatch", + "name":"Ruuvi Watch", + "src":"ruuviwatch.app.js", + "icon":"ruuviwatch.img" + }); + + const lookup = {}; + const ruuvis = []; + let current = 0; + + function int2Hex (str) { + return ('0' + str.toString(16).toUpperCase()).slice(-2); + } + + function p(data) { + const OFFSET = 7; // 0-4 header, 5-6 Ruuvi id + const robject = {}; + robject.version = data[OFFSET]; + + let temperature = (data[OFFSET+1] << 8) | (data[OFFSET+2] & 0xff); + if (temperature > 32767) { + temperature -= 65534; + } + robject.temperature = temperature / 200.0; + + robject.humidity = (((data[OFFSET+3] & 0xff) << 8) | (data[OFFSET+4] & 0xff)) / 400.0; + robject.pressure = ((((data[OFFSET+5] & 0xff) << 8) | (data[OFFSET+6] & 0xff)) + 50000) / 100.0; + + let accelerationX = (data[OFFSET+7] << 8) | (data[OFFSET+8] & 0xff); + if (accelerationX > 32767) accelerationX -= 65536; // two's complement + robject.accelerationX = accelerationX / 1000.0; + + let accelerationY = (data[OFFSET+9] << 8) | (data[OFFSET+10] & 0xff); + if (accelerationY > 32767) accelerationY -= 65536; // two's complement + robject.accelerationY = accelerationY / 1000.0; + + let accelerationZ = (data[OFFSET+11] << 8) | (data[OFFSET+12] & 0xff); + if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement + robject.accelerationZ = accelerationZ / 1000.0; + + const powerInfo = ((data[OFFSET+13] & 0xff) << 8) | (data[OFFSET+14] & 0xff); + robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0; + robject.txPower = (powerInfo & 0b11111) * 2 - 40; + robject.movementCounter = data[OFFSET+15] & 0xff; + robject.measurementSequenceNumber = ((data[OFFSET+16] & 0xff) << 8) | (data[OFFSET+17] & 0xff); + + robject.mac = [ + int2Hex(data[OFFSET+18]), + int2Hex(data[OFFSET+19]), + int2Hex(data[OFFSET+20]), + int2Hex(data[OFFSET+21]), + int2Hex(data[OFFSET+22]), + int2Hex(data[OFFSET+23]) + ].join(':'); + + robject.name = "Ruuvi " + int2Hex(data[OFFSET+22]) + int2Hex(data[OFFSET+23]); + return robject; + } + + function getAge(created) { + const now = new Date().getTime(); + const ago = ((now - created) / 1000).toFixed(0); + return ago > 0 ? ago + "s ago" : "now"; + } + + function redraw() { + if (ruuvis.length > 0 && ruuvis[current]) { + const ruuvi = ruuvis[current]; + g.clear(); + g.setFontAlign(0,0); + g.setFont("Vector",12); + g.drawString(" (" + (current+1) + "/" + ruuvis.length + ")", g.getWidth()/2, 10); + g.setFont("Vector",20); + g.drawString(ruuvi.name, g.getWidth()/2, 30); + g.setFont("Vector",12); + const age = getAge(ruuvi.time); + if(age > (5*60)) { + g.setColor("#ff0000"); + } else if (age > 60) { + g.setColor("#f39c12"); + } else { + g.setColor("#2ecc71"); + } + g.drawString(age, g.getWidth()/2, 50); + g.setColor("#ffffff"); + g.setFont("Vector",60); + g.drawString(ruuvi.temperature.toFixed(2) + "°c", g.getWidth()/2, g.getHeight()/2); + g.setFontAlign(0,1); + g.setFont("Vector",20); + g.drawString(ruuvi.humidity + "% " + ruuvi.pressure + "hPa ", g.getWidth()/2, g.getHeight()-30); + g.setFont("Vector",12); + g.drawString(ruuvi.battery + "v", g.getWidth()/2, g.getHeight()-10); + } else { + g.clear(); + g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); + g.setFontAlign(0,0); + g.setFont("Vector",16); + g.drawString("Looking for Ruuvi...", g.getWidth()/2, g.getHeight()/2 + 50); + } + } + + function scan() { + NRF.findDevices(function(devices) { + let foundNew = false; + devices.forEach(device => { + const data = p(device.data); + data.time = new Date().getTime(); + const idx = lookup[data.name]; + if (idx !== undefined) { + ruuvis[idx] = data; + } else { + lookup[data.name] = ruuvis.push(data)-1; + foundNew = true; + } + }); + redraw(); + if (foundNew) { + Bangle.buzz(); + g.flip(); + } + + }, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] }); + } + + g.clear(); + g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); + + var drawInterval = setInterval(redraw, 1000); + var scanInterval = setInterval(scan, 10000); + setWatch(() => { + current--; + if (current < 0) { + current = ruuvis.length-1; + } + redraw(); + }, BTN1, {repeat:true}); + + setWatch(() => { + scan(); + }, BTN2, {repeat:true}); + + setWatch(() => { + current++; + if (current >= ruuvis.length) { + current = 0; + } + redraw(); + }, BTN3, {repeat:true}); + + scan(); \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.png b/apps/ruuviwatch/ruuviwatch.png new file mode 100644 index 000000000..3737a7e8c Binary files /dev/null and b/apps/ruuviwatch/ruuviwatch.png differ diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 910cd4658..101da48e1 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -11,3 +11,4 @@ 0.12: Allow hiding the widget 0.13: Tweak Bangle.js 2 light theme colors 0.14: Use weather condition code for icon selection +0.15: Fix widget icon diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 8afdfe6df..1d48116e1 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -53,6 +53,16 @@ exports.get = function() { scheduleExpiry(storage.readJSON('weather.json')||{}); +/** + * + * @param cond Weather condition, as one of: + * {number} code: (Preferred form) https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + * {string} weather description (in English: breaks for other languages!) + * {object} use cond.code if present, or fall back to cond.txt + * @param x Left + * @param y Top + * @param r Icon Size + */ exports.drawIcon = function(cond, x, y, r) { var palette; @@ -249,32 +259,35 @@ exports.drawIcon = function(cond, x, y, r) { g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); } - function chooseIcon(condition) { - if (!condition) return () => {}; - condition = condition.toLowerCase(); - if (condition.includes("thunderstorm")) return drawThunderstorm; - if (condition.includes("freezing")||condition.includes("snow")|| - condition.includes("sleet")) { + /* + * Choose weather icon to display based on weather description + */ + function chooseIconByTxt(txt) { + if (!txt) return () => {}; + txt = txt.toLowerCase(); + if (txt.includes("thunderstorm")) return drawThunderstorm; + if (txt.includes("freezing")||txt.includes("snow")|| + txt.includes("sleet")) { return drawSnow; } - if (condition.includes("drizzle")|| - condition.includes("shower")) { + if (txt.includes("drizzle")|| + txt.includes("shower")) { return drawRain; } - if (condition.includes("rain")) return drawShowerRain; - if (condition.includes("clear")) return drawSun; - if (condition.includes("few clouds")) return drawFewClouds; - if (condition.includes("scattered clouds")) return drawCloud; - if (condition.includes("clouds")) return drawBrokenClouds; - if (condition.includes("mist") || - condition.includes("smoke") || - condition.includes("haze") || - condition.includes("sand") || - condition.includes("dust") || - condition.includes("fog") || - condition.includes("ash") || - condition.includes("squalls") || - condition.includes("tornado")) { + if (txt.includes("rain")) return drawShowerRain; + if (txt.includes("clear")) return drawSun; + if (txt.includes("few clouds")) return drawFewClouds; + if (txt.includes("scattered clouds")) return drawCloud; + if (txt.includes("clouds")) return drawBrokenClouds; + if (txt.includes("mist") || + txt.includes("smoke") || + txt.includes("haze") || + txt.includes("sand") || + txt.includes("dust") || + txt.includes("fog") || + txt.includes("ash") || + txt.includes("squalls") || + txt.includes("tornado")) { return drawMist; } return drawUnknown; @@ -298,7 +311,6 @@ exports.drawIcon = function(cond, x, y, r) { case 531: return drawShowerRain; default: return drawRain; } - break; case 6: return drawSnow; case 7: return drawMist; case 8: @@ -308,16 +320,21 @@ exports.drawIcon = function(cond, x, y, r) { case 802: return drawCloud; default: return drawBrokenClouds; } - break; default: return drawUnknown; } } - if (cond.code && cond.code > 0) { - chooseIconByCode(cond.code)(x, y, r); - } else { - chooseIcon(cond.txt)(x, y, r); + function chooseIcon(cond) { + if (typeof (cond)==="object") { + if ("code" in cond) return chooseIconByCode(cond.code); + if ("txt" in cond) return chooseIconByTxt(cond.txt); + } else if (typeof (cond)==="number") { + return chooseIconByCode(cond.code); + } else if (typeof (cond)==="string") { + return chooseIconByTxt(cond.txt); + } + return drawUnknown; } - + chooseIcon(cond)(x, y, r); }; diff --git a/apps/weather/widget.js b/apps/weather/widget.js index f2ddf0b5b..2905d776b 100644 --- a/apps/weather/widget.js +++ b/apps/weather/widget.js @@ -52,8 +52,8 @@ if (!w) return; g.reset(); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); - if (w.txt) { - weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5); + if (w.code||w.txt) { + weather.drawIcon(w, this.x+10, this.y+8, 7.5); } if (w.temp) { let t = require('locale').temp(w.temp-273.15); // applies conversion diff --git a/apps/wid_a_battery_widget/ChangeLog b/apps/wid_a_battery_widget/ChangeLog index b04824ae8..8a1538479 100644 --- a/apps/wid_a_battery_widget/ChangeLog +++ b/apps/wid_a_battery_widget/ChangeLog @@ -1,3 +1,4 @@ 1.00: Release for Bangle 2 (2021/11/18) 1.01: Internal id update to wid_* as per Gordon's request (2021/11/21) -1.02: Support dark themes \ No newline at end of file +1.02: Support dark themes +1.03: Increase screen update rate when charging diff --git a/apps/wid_a_battery_widget/widget.js b/apps/wid_a_battery_widget/widget.js index 8ab644ab3..74c76784d 100644 --- a/apps/wid_a_battery_widget/widget.js +++ b/apps/wid_a_battery_widget/widget.js @@ -1,4 +1,7 @@ (function(){ + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + let COLORS = { 'white': g.theme.dark ? "#000" : "#fff", 'black': g.theme.dark ? "#fff" : "#000", @@ -36,10 +39,14 @@ g.setFontAlign(0,0); g.setFont('6x8'); g.drawString(l, x + 14, y + 10); + + if (Bangle.isCharging()) changeInterval(id, intervalHigh); + else changeInterval(id, intervalLow); } + Bangle.on('charging',function(charging) { draw(); }); - setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), 60000); + var id = setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), intervalLow); WIDGETS["wid_a_battery_widget"]={area:"tr",width:30,draw:draw}; })(); diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 273e611a4..e70093659 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -12,3 +12,4 @@ 0.13: Fillbar setting added, see README 0.14: Fix drawing the bar when charging 0.15: Added option to always display the icon when charging (useful if 'hide if charge greater than' is enabled) +0.16: Increase screen update rate when charging diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 5386ffe22..529923386 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,6 +1,9 @@ (function(){ + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + let COLORS = {}; - + if (process.env.HWVERSION == 1) { COLORS = { 'white': -1, // White @@ -17,13 +20,13 @@ 'high': "#0f0", // Green 'ok': "#ff0", // Orange 'low': "#f00", // Red - }; + }; } - const SETTINGS_FILE = 'widbatpc.json' + const SETTINGS_FILE = 'widbatpc.json'; - let settings + let settings; function loadSettings() { - settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; const DEFAULTS = { 'color': 'By Level', 'percentage': true, @@ -32,17 +35,17 @@ 'alwaysoncharge': false, }; Object.keys(DEFAULTS).forEach(k=>{ - if (settings[k]===undefined) settings[k]=DEFAULTS[k] + if (settings[k]===undefined) settings[k]=DEFAULTS[k]; }); } function setting(key) { - if (!settings) { loadSettings() } + if (!settings) { loadSettings(); } return settings[key]; } const levelColor = (l) => { // "charging" is very bright -> percentage is hard to read, "high" is ok(ish) - const green = setting('percentage') ? COLORS.high : COLORS.charging + const green = setting('percentage') ? COLORS.high : COLORS.charging; switch (setting('color')) { case 'Monochrome': return COLORS.white; // no chance of reading the percentage here :-( case 'Green': return green; @@ -59,10 +62,11 @@ if (l >= 15) return COLORS.ok; return COLORS.low; } - } + }; const chargerColor = () => { - return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging - } + return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging; + }; + // sets width, returns true if it changed function setWidth() { var w = 40; @@ -77,6 +81,7 @@ WIDGETS["batpc"].width = w; return changed; } + function draw() { // if hidden, don't draw if (!WIDGETS["batpc"].width) return; @@ -106,11 +111,11 @@ if (!setting('percentage')) { return; } - let gfx = g + let gfx = g; if (setting('color') === 'Monochrome') { // draw text inverted on battery level gfx = Graphics.createCallback(g.getWidth(),g.getHeight(), 1, - (x,y) => {g.setPixel(x,y,x<=xl?0:-1)}) + (x,y) => {g.setPixel(x,y,x<=xl?0:-1);}); } gfx.setFontAlign(-1,-1); if (l >= 100) { @@ -122,19 +127,24 @@ gfx.drawString(l, x + 6, y + 4); } } + // reload widget, e.g. when settings have changed function reload() { - loadSettings() + loadSettings(); // need to redraw all widgets, because changing the "charger" setting // can affect the width and mess with the whole widget layout - setWidth() + setWidth(); g.clear(); Bangle.drawWidgets(); } + // update widget - redraw just widget, or all widgets if size changed function update() { if (setWidth()) Bangle.drawWidgets(); else WIDGETS["batpc"].draw(); + + if (Bangle.isCharging()) changeInterval(id, intervalHigh); + else changeInterval(id, intervalLow); } Bangle.on('charging',function(charging) { @@ -142,20 +152,13 @@ update(); g.flip(); }); - var batteryInterval; + Bangle.on('lcdPower', function(on) { - if (on) { - update(); - // refresh once a minute if LCD on - if (!batteryInterval) - batteryInterval = setInterval(update, 60000); - } else { - if (batteryInterval) { - clearInterval(batteryInterval); - batteryInterval = undefined; - } - } + if (on) update(); }); + + var id = setInterval(()=>WIDGETS["batpc"].draw(), intervalLow); + WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; setWidth(); -})() +})(); diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index c033ea505..54f6b203b 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -20,3 +20,5 @@ Fix issue with widget overwrite in large font mode Memory usage enhancements 0.20: Fix issue where step count would randomly reset +0.21: Memory usage improvements, fix widget initial width (fix #1170) +0.22: Fix 'stps' regression for 0.21 (fix #1233) diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 0ec0780c9..cc7fdb579 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -1,5 +1,4 @@ (() => { - const PEDOMFILE = "wpedom.json" // Last time Bangle.on('step' was called let lastUpdate = new Date(); // Last step count when Bangle.on('step' was called @@ -8,19 +7,14 @@ let settings; function loadSettings() { - const d = require('Storage').readJSON(PEDOMFILE, 1) || {}; - settings = d.settings || {}; - } - - function setting(key) { - if (!settings) { loadSettings() } - const DEFAULTS = { + const d = require('Storage').readJSON("wpedom.json", 1) || {}; + settings = Object.assign({ 'goal': 10000, 'progress': false, 'large': false, 'hide': false - } - return (key in settings) ? settings[key] : DEFAULTS[key]; + }, d.settings || {}); + return d; } Bangle.on('step', stepCount => { @@ -31,10 +25,10 @@ if (lastUpdate.getDate() == date.getDate()){ stp_today += steps; } else { - // TODO: could save this to PEDOMFILE for lastUpdate's day? + // TODO: could save this to "wpedom.json" for lastUpdate's day? stp_today = steps; } - if (stp_today === setting('goal') + if (stp_today === settings.goal && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { let b = 3, buzz = () => { if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) @@ -51,29 +45,31 @@ }); // When unloading, save state E.on('kill', () => { - if (!settings) { loadSettings() } - let d = { + require("Storage").writeJSON("wpedom.json",{ lastUpdate : lastUpdate.valueOf(), stepsToday : stp_today, settings : settings, - }; - require("Storage").write(PEDOMFILE,d); + }); }); // add your widget - WIDGETS["wpedom"]={area:"tl",width:26, - redraw:function() { // work out the width, and queue a full redraw if needed + WIDGETS["wpedom"]={area:"tl",width:0, + getWidth:function() { let stps = stp_today.toString(); let newWidth = 24; - if (setting('hide')) + if (settings.hide) newWidth = 0; else { - if (setting('large')) { + if (settings.large) { newWidth = 12 * stps.length + 3; - if (setting('progress')) + if (settings.progress) newWidth += 24; } } + return newWidth; + }, + redraw:function() { // work out the width, and queue a full redraw if needed + let newWidth = this.getWidth(); if (newWidth!=this.width) { // width has changed, re-layout all widgets this.width = newWidth; @@ -84,14 +80,14 @@ } }, draw:function() { - if (setting('hide')) return; + if (settings.hide) return; if (stp_today > 99999) stp_today = stp_today % 100000; // cap to five digits + comma = 6 characters let stps = stp_today.toString(); g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background - if (setting('progress')) { + if (settings.progress) { const width = 23, half = 11; - const goal = setting('goal'), left = Math.max(goal-stps,0); + const goal = settings.goal, left = Math.max(goal-stps,0); // blue or dark green g.setColor(left ? "#08f" : "#080").fillCircle(this.x + half, this.y + half, half); if (left) { @@ -113,10 +109,10 @@ } g.reset(); } - if (setting('large')) { + if (settings.large) { g.setFont("6x8",2); g.setFontAlign(-1, 0); - g.drawString(stps, this.x + (setting('progress')?28:4), this.y + 12); + g.drawString(stps, this.x + (settings.progress?28:4), this.y + 12); } else { let w = 24; if (stps.length > 3){ @@ -137,11 +133,12 @@ getSteps:()=>stp_today }; // Load data at startup - let pedomData = require("Storage").readJSON(PEDOMFILE,1); + let pedomData = loadSettings(); if (pedomData) { if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); stp_today = pedomData.stepsToday|0; delete pedomData; } + WIDGETS["wpedom"].width = WIDGETS["wpedom"].getWidth(); })() diff --git a/core b/core index ae9586977..b05af96b2 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit ae9586977948279d267f2749bf3a48d3aa753c11 +Subproject commit b05af96b2522a7a7225a56d804faf9383f8a8f97 diff --git a/modules/Layout.js b/modules/Layout.js index 6dc4b6368..65e9a8dc8 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -42,8 +42,8 @@ layoutObject has: and `fillx`/`filly` to be set. Not compatible with text rotation. * A `col` field, eg `#f00` for red * A `bgCol` field for background color (will automatically fill on render) -* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center -* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center +* A `halign` field to set horizontal alignment WITHIN a `v` container. `-1`=left, `1`=right, `0`=center +* A `valign` field to set vertical alignment WITHIN a `h` container. `-1`=top, `1`=bottom, `0`=center * A `pad` integer field to set pixels padding * A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space * A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space