diff --git a/.gitignore b/.gitignore index b83632eaa..be33fbc90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .htaccess node_modules package-lock.json +.DS_Store +*.js.bak +appdates.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfef69ac..e5cd6aef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,16 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong * Added optional `README.md` file for apps +* Remove 2v04 version warning, add links in About to official/developer versions +* Fix issue removing an app that was just installed (fix #253) +* Add `Favourite` functionality +* Version number now clickable even when you're at the latest version (fix #291) +* Rewrite 'getInstalledApps' to minimize RAM usage +* Added code to handle Settings +* Added espruinotools.js for pretokenisation +* Included image and compression tools in repo +* Added better upload of large files (incl. compression) +* URL fetch is now async +* Adding '#search' after the URL (when not the name of a 'filter' chip) will set up search for that term +* If `bin/pre-publish.sh` has been run and recent.csv created, add 'Sort By' chip +* New 'espruinotools' which fixes pretokenise issue when ID follows ID (fix #416) diff --git a/README.md b/README.md index 3f6c82c02..04854c99e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Bangle.js App Loader (and Apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) -**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By +**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By submitting code to this repository you confirm that you are happy with it being MIT licensed, and that it is not licensed in another way that would make this impossible. @@ -29,7 +29,7 @@ Check out: ## What filenames are used -Filenames in storage are limited to 8 characters. To +Filenames in storage are limited to 28 characters. To easily distinguish between file types, we use the following: * `stuff.info` is JSON that describes an app - this is auto-generated by the App Loader @@ -202,6 +202,11 @@ and which gives information about the app for the Launcher. "files:"file1,file2,file3", // added by BangleApps loader on upload - lists all files // that belong to the app so it can be deleted + "data":"appid.data.json,appid.data?.json;appidStorageFile,appidStorageFile*" + // added by BangleApps loader on upload - lists files that + // the app might write, so they can be deleted on uninstall + // typically these files are not uploaded, but created by the app + // these can include '*' or '?' wildcards } ``` @@ -240,16 +245,24 @@ and which gives information about the app for the Launcher. "evaluate":true // if supplied, data isn't quoted into a String before upload // (eg it's evaluated as JS) }, + ] + "data": [ // list of files the app writes to + {"name":"appid.data.json", // filename used in storage + "storageFile":true // if supplied, file is treated as storageFile + }, + {"wildcard":"appid.data.*" // wildcard of filenames used in storage + }, // this is mutually exclusive with using "name" + ], "sortorder" : 0, // optional - choose where in the list this goes. // this should only really be used to put system // stuff at the top - ] } ``` * name, icon and description present the app in the app loader. * tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. * storage is used to identify the app files and how to handle them +* data is used to clean up files when the app is uninstalled ### `apps.json`: `custom` element @@ -328,18 +341,21 @@ See [apps/gpsrec/interface.html](the GPS Recorder) for a full example. Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings". To do so, the app needs to include a `settings.js` file, containing a single function that handles configuring the app. -When the app settings are opened, this function is called with one +When the app settings are opened, this function is called with one argument, `back`: a callback to return to the settings menu. +Usually it will save any information in `app.json` where `app` is the name +of your app - so you should change the example accordingly. + Example `settings.js` ```js // make sure to enclose the function in parentheses (function(back) { - let settings = require('Storage').readJSON('app.settings.json',1)||{}; + let settings = require('Storage').readJSON('app.json',1)||{}; function save(key, value) { settings[key] = value; - require('Storage').write('app.settings.json',settings); - } + require('Storage').write('app.json',settings); + } const appMenu = { '': {'title': 'App Settings'}, '< Back': back, @@ -351,19 +367,20 @@ Example `settings.js` E.showMenu(appMenu) }) ``` -In this example the app needs to add both `app.settings.js` and -`app.settings.json` to `apps.json`: +In this example the app needs to add `app.settings.js` to `storage` in `apps.json`. +It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled. ```json { "id": "app", ... "storage": [ ... {"name":"app.settings.js","url":"settings.js"}, - {"name":"app.settings.json","content":"{}"} + ], + "data": [ + {"name":"app.json"} ] }, ``` -That way removing the app also cleans up `app.settings.json`. ## Coding hints diff --git a/apps.json b/apps.json index a82da04d2..7e7616b22 100644 --- a/apps.json +++ b/apps.json @@ -3,7 +3,7 @@ "id": "boot", "name": "Bootloader", "icon": "bootloader.png", - "version": "0.14", + "version":"0.17", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "tags": "tool,system", "type": "bootloader", @@ -68,7 +68,7 @@ "name": "Default Launcher", "shortName": "Launcher", "icon": "app.png", - "version": "0.01", + "version":"0.03", "description": "This is needed by Bangle.js to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "tags": "tool,system,launcher", "type": "launch", @@ -84,7 +84,7 @@ "id": "about", "name": "About", "icon": "app.png", - "version": "0.04", + "version":"0.05", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "tags": "tool,system", "allow_emulator": true, @@ -104,7 +104,7 @@ "id": "locale", "name": "Languages", "icon": "locale.png", - "version": "0.05", + "version":"0.06", "description": "Translations for different countries", "tags": "tool,system,locale,translate", "type": "locale", @@ -120,35 +120,25 @@ "id": "welcome", "name": "Welcome", "icon": "app.png", - "version": "0.06", + "version":"0.08", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator": true, "storage": [ - { - "name": "welcome.boot.js", - "url": "boot.js" - }, - { - "name": "welcome.app.js", - "url": "app.js" - }, - { - "name": "welcome.settings.js", - "url": "settings.js" - }, - { - "name": "welcome.img", - "url": "app-icon.js", - "evaluate": true - } + {"name":"welcome.boot.js","url":"boot.js"}, + {"name":"welcome.app.js","url":"app.js"}, + {"name":"welcome.settings.js","url":"settings.js"}, + {"name":"welcome.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"welcome.json"} ] }, { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version": "0.07", + "version":"0.10", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "type": "widget", @@ -172,7 +162,7 @@ "id": "mclock", "name": "Morphing Clock", "icon": "clock-morphing.png", - "version": "0.03", + "version":"0.06", "description": "7 segment clock that morphs between minutes and hours", "tags": "clock", "type": "clock", @@ -194,28 +184,14 @@ "id": "setting", "name": "Settings", "icon": "settings.png", - "version": "0.10", + "version":"0.19", "description": "A menu for setting up Bangle.js", "tags": "tool,system", + "readme": "README.md", "storage": [ - { - "name": "setting.app.js", - "url": "settings.js" - }, - { - "name": "setting.boot.js", - "url": "boot.js" - }, - { - "name": "setting.json", - "url": "settings-default.json", - "evaluate": true - }, - { - "name": "setting.img", - "url": "settings-icon.js", - "evaluate": true - } + {"name":"setting.app.js","url":"settings.js"}, + {"name":"setting.boot.js","url":"boot.js"}, + {"name":"setting.img","url":"settings-icon.js","evaluate":true} ], "sortorder": -2 }, @@ -224,35 +200,18 @@ "name": "Default Alarm", "shortName": "Alarms", "icon": "app.png", - "version": "0.05", + "version":"0.07", "description": "Set and respond to alarms", "tags": "tool,alarm,widget", "storage": [ - { - "name": "alarm.app.js", - "url": "app.js" - }, - { - "name": "alarm.boot.js", - "url": "boot.js" - }, - { - "name": "alarm.js", - "url": "alarm.js" - }, - { - "name": "alarm.json", - "content": "[]" - }, - { - "name": "alarm.img", - "url": "app-icon.js", - "evaluate": true - }, - { - "name": "alarm.wid.js", - "url": "widget.js" - } + {"name":"alarm.app.js","url":"app.js"}, + {"name":"alarm.boot.js","url":"boot.js"}, + {"name":"alarm.js","url":"alarm.js"}, + {"name":"alarm.img","url":"app-icon.js","evaluate":true}, + {"name":"alarm.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"alarm.json"} ] }, { @@ -276,11 +235,40 @@ } ] }, - { - "id": "aclock", + { "id": "imgclock", + "name": "Image background clock", + "shortName":"Image Clock", + "icon": "app.png", + "version":"0.06", + "description": "A clock with an image as a background", + "tags": "clock", + "type" : "clock", + "custom": "custom.html", + "storage": [ + {"name":"imgclock.app.js","url":"app.js"}, + {"name":"imgclock.img","url":"app-icon.js","evaluate":true}, + {"name":"imgclock.face.img"}, + {"name":"imgclock.face.json"}, + {"name":"imgclock.face.bg","content":""} + ] + }, + { "id": "impwclock", + "name": "Imprecise Word Clock", + "icon": "clock-impword.png", + "version":"0.02", + "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"impwclock.app.js","url":"clock-impword.js"}, + {"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true} + ] + }, + { "id": "aclock", "name": "Analog Clock", "icon": "clock-analog.png", - "version": "0.11", + "version": "0.13", "description": "An Analog Clock", "tags": "clock", "type": "clock", @@ -322,7 +310,7 @@ "id": "trex", "name": "T-Rex", "icon": "trex.png", - "version": "0.01", + "version":"0.02", "description": "T-Rex game in the style of Chrome's offline game", "tags": "game", "allow_emulator": true, @@ -342,7 +330,7 @@ "id": "astroid", "name": "Asteroids!", "icon": "asteroids.png", - "version": "0.01", + "version":"0.02", "description": "Retro asteroids game", "tags": "game", "allow_emulator": true, @@ -400,7 +388,7 @@ "id": "compass", "name": "Compass", "icon": "compass.png", - "version": "0.01", + "version":"0.02", "description": "Simple compass that points North", "tags": "tool,outdoors", "storage": [ @@ -458,7 +446,7 @@ "id": "speedo", "name": "Speedo", "icon": "speedo.png", - "version": "0.01", + "version":"0.03", "description": "Show the current speed according to the GPS", "tags": "tool,outdoors,gps", "storage": [ @@ -477,58 +465,49 @@ "id": "gpsrec", "name": "GPS Recorder", "icon": "app.png", - "version": "0.06", + "version":"0.09", "interface": "interface.html", "description": "Application that allows you to record a GPS track. Can run in background", "tags": "tool,outdoors,gps,widget", "storage": [ - { - "name": "gpsrec.app.js", - "url": "app.js" - }, - { - "name": "gpsrec.json", - "url": "app-settings.json", - "evaluate": true - }, - { - "name": "gpsrec.img", - "url": "app-icon.js", - "evaluate": true - }, - { - "name": "gpsrec.wid.js", - "url": "widget.js" - } + {"name":"gpsrec.app.js","url":"app.js"}, + {"name":"gpsrec.img","url":"app-icon.js","evaluate":true}, + {"name":"gpsrec.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"gpsrec.json"}, + {"wildcard":".gpsrc?","storageFile": true} + ] + }, + { "id": "gpsnav", + "name": "GPS Navigation", + "icon": "icon.png", + "version":"0.01", + "description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording", + "tags": "tool,outdoors,gps", + "readme": "README.md", + "storage": [ + {"name":"gpsnav.app.js","url":"app.js"}, + {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, + {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} ] }, { "id": "heart", "name": "Heart Rate Recorder", "icon": "app.png", - "version": "0.01", + "version":"0.02", "interface": "interface.html", "description": "Application that allows you to record your heart rate. Can run in background", "tags": "tool,health,widget", "storage": [ - { - "name": "heart.app.js", - "url": "app.js" - }, - { - "name": "heart.json", - "url": "app-settings.json", - "evaluate": true - }, - { - "name": "heart.img", - "url": "app-icon.js", - "evaluate": true - }, - { - "name": "heart.wid.js", - "url": "widget.js" - } + {"name":"heart.app.js","url":"app.js"}, + {"name":"heart.img","url":"app-icon.js","evaluate":true}, + {"name":"heart.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"heart.json"}, + {"wildcard":".heart?","storageFile": true} ] }, { @@ -554,9 +533,9 @@ "id": "files", "name": "App Manager", "icon": "files.png", - "version": "0.01", + "version":"0.05", "description": "Show currently installed apps, free space, and allow their deletion from the watch", - "tags": "tool,system", + "tags": "tool,system,files", "storage": [ { "name": "files.app.js", @@ -569,11 +548,27 @@ } ] }, - { - "id": "widbat", + { "id": "weather", + "name": "Weather", + "icon": "icon.png", + "version":"0.01", + "description": "Show Gadgetbridge weather report", + "readme": "readme.md", + "tags": "widget,outdoors", + "storage": [ + {"name":"weather.app.js","url":"app.js"}, + {"name":"weather.wid.js","url":"widget.js"}, + {"name":"weather","url":"lib.js"}, + {"name":"weather.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name": "weather.json"} + ] + }, + { "id": "widbat", "name": "Battery Level Widget", "icon": "widget.png", - "version": "0.04", + "version":"0.05", "description": "Show the current battery level and charging status in the top right of the clock", "tags": "widget,battery", "type": "widget", @@ -589,30 +584,23 @@ "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", "icon": "widget.png", - "version": "0.08", + "version":"0.11", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type": "widget", "storage": [ - { - "name": "widbatpc.wid.js", - "url": "widget.js" - }, - { - "name": "widbatpc.settings.js", - "url": "settings.js" - }, - { - "name": "widbatpc.settings.json", - "content": "{}" - } + {"name":"widbatpc.wid.js","url":"widget.js"}, + {"name":"widbatpc.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widbatpc.json"} ] }, { "id": "widbt", "name": "Bluetooth Widget", "icon": "widget.png", - "version": "0.03", + "version":"0.04", "description": "Show the current Bluetooth connection status in the top right of the clock", "tags": "widget,bluetooth", "type": "widget", @@ -623,8 +611,19 @@ } ] }, - { - "id": "hrm", + { "id": "widram", + "name": "RAM Widget", + "shortName":"RAM Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Display your Bangle's available RAM percentage in a widget", + "tags": "widget", + "type": "widget", + "storage": [ + {"name":"widram.wid.js","url":"widget.js"} + ] + }, + { "id": "hrm", "name": "Heart Rate Monitor", "icon": "heartrate.png", "version": "0.01", @@ -680,7 +679,7 @@ "id": "swatch", "name": "Stopwatch", "icon": "stopwatch.png", - "version": "0.05", + "version":"0.07", "interface": "interface.html", "description": "Simple stopwatch with Lap Time logging to a JSON file", "tags": "health", @@ -703,7 +702,7 @@ "name": "Bluetooth Music Controls", "shortName": "Music Control", "icon": "hid-music.png", - "version": "0.01", + "version": "0.02", "description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!", "tags": "bluetooth", "storage": [ @@ -723,7 +722,7 @@ "name": "Bluetooth Keyboard", "shortName": "Bluetooth Kbd", "icon": "hid-keyboard.png", - "version": "0.01", + "version":"0.02", "description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps", "tags": "bluetooth", "storage": [ @@ -743,7 +742,7 @@ "name": "Binary Bluetooth Keyboard", "shortName": "Binary BT Kbd", "icon": "hid-binary-keyboard.png", - "version": "0.01", + "version":"0.02", "description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want", "tags": "bluetooth", "storage": [ @@ -820,104 +819,62 @@ { "id": "qrcode", "name": "Custom QR Code", - "icon": "qrcode.png", - "version": "0.01", + "icon": "app.png", + "version":"0.02", "description": "Use this to upload a customised QR code to Bangle.js", - "tags": "", - "custom": "qrcode.html", + "tags": "qrcode", + "custom": "custom.html", "storage": [ - { - "name": "qrcode.app.js" - }, - { - "name": "qrcode.img" - } + {"name":"qrcode.app.js"}, + {"name":"qrcode.img","url":"app-icon.js","evaluate":true} ] }, { "id": "beer", "name": "Beer Compass", - "icon": "beercompass.png", - "version": "0.01", + "icon": "app.png", + "version":"0.01", "description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one", "tags": "", - "custom": "beercompass.html", + "custom": "custom.html", "storage": [ - { - "name": "beer.app.js" - }, - { - "name": "beer.img" - } + {"name":"beer.app.js"}, + {"name":"beer.img","url":"app-icon.js","evaluate":true} ] }, { "id": "route", "name": "Route Viewer", - "icon": "route.png", - "version": "0.01", + "icon": "app.png", + "version":"0.01", "description": "Upload a KML file of a route, and have your watch display a map with how far around it you are", "tags": "", - "custom": "route.html", + "custom": "custom.html", "storage": [ - { - "name": "route.app.js" - }, - { - "name": "route.img" - } + {"name":"route.app.js"}, + {"name":"route.img","url":"app-icon.js","evaluate":true} ] }, { "id": "ncstart", "name": "NCEU Startup", "icon": "start.png", - "version": "0.03", + "version":"0.05", "description": "NodeConfEU 2019 'First Start' Sequence", "tags": "start,welcome", "storage": [ - { - "name": "ncstart.app.js", - "url": "start.js" - }, - { - "name": "ncstart.boot.js", - "url": "boot.js" - }, - { - "name": "ncstart.settings.js", - "url": "settings.js" - }, - { - "name": "ncstart.img", - "url": "start-icon.js", - "evaluate": true - }, - { - "name": "nc-bangle.img", - "url": "start-bangle.js", - "evaluate": true - }, - { - "name": "nc-nceu.img", - "url": "start-nceu.js", - "evaluate": true - }, - { - "name": "nc-nfr.img", - "url": "start-nfr.js", - "evaluate": true - }, - { - "name": "nc-nodew.img", - "url": "start-nodew.js", - "evaluate": true - }, - { - "name": "nc-tf.img", - "url": "start-tf.js", - "evaluate": true - } + {"name":"ncstart.app.js","url":"start.js"}, + {"name":"ncstart.boot.js","url":"boot.js"}, + {"name":"ncstart.settings.js","url":"settings.js"}, + {"name":"ncstart.img","url":"start-icon.js","evaluate":true}, + {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, + {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, + {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true}, + {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true}, + {"name":"nc-tf.img","url":"start-tf.js","evaluate":true} + ], + "data": [ + {"name":"ncstart.json"} ] }, { @@ -1237,7 +1194,7 @@ "id": "flappy", "name": "Flappy Bird", "icon": "app.png", - "version": "0.03", + "version":"0.04", "description": "A Flappy Bird game clone", "tags": "game", "allow_emulator": true, @@ -1257,7 +1214,7 @@ "id": "gpsinfo", "name": "GPS Info", "icon": "gps-info.png", - "version": "0.02", + "version":"0.03", "description": "An application that displays information about altitude, lat/lon, satellites and time", "tags": "gps", "type": "app", @@ -1341,7 +1298,7 @@ "id": "widclk", "name": "Digital clock widget", "icon": "widget.png", - "version": "0.03", + "version":"0.04", "description": "A simple digital clock widget", "tags": "widget,clock", "type": "widget", @@ -1356,15 +1313,13 @@ "id": "widpedom", "name": "Pedometer widget", "icon": "widget.png", - "version": "0.08", + "version":"0.10", "description": "Daily pedometer widget", "tags": "widget", "type": "widget", "storage": [ - { - "name": "widpedom.wid.js", - "url": "widget.js" - } + {"name":"widpedom.wid.js","url":"widget.js"}, + {"name":"widpedom.settings.js","url":"settings.js"} ] }, { @@ -1467,7 +1422,7 @@ "id": "pipboy", "name": "Pipboy", "icon": "app.png", - "version": "0.02", + "version": "0.03", "description": "Pipboy themed clock", "tags": "clock", "type": "clock", @@ -1489,8 +1444,8 @@ "name": "Torch", "shortName": "Torch", "icon": "app.png", - "version": "0.01", - "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN3 four times in quick succession to start when in normal clock mode", + "version":"0.02", + "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets", "tags": "tool,torch", "storage": [ { @@ -1512,7 +1467,8 @@ "id": "wohrm", "name": "Workout HRM", "icon": "app.png", - "version": "0.06", + "version":"0.07", + "readme": "README.md", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "tags": "hrm,workout", "type": "app", @@ -1548,9 +1504,9 @@ "id": "grocery", "name": "Grocery", "icon": "grocery.png", - "version": "0.01", - "description": "Simple grocery list - Display a list of product and track if you already put them in your cart.", - "tags": "tool,outdoors", + "version":"0.01", + "description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.", + "tags": "tool,outdoors,shopping,list", "type": "app", "custom": "grocery.html", "storage": [ @@ -1571,7 +1527,7 @@ "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version": "0.07", + "version":"0.12", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", @@ -1630,7 +1586,7 @@ "id": "barclock", "name": "Bar Clock", "icon": "clock-bar.png", - "version": "0.04", + "version":"0.05", "description": "A simple digital clock showing seconds as a bar", "tags": "clock", "type": "clock", @@ -1707,7 +1663,7 @@ "id": "astrocalc", "name": "Astrocalc", "icon": "astrocalc.png", - "version": "0.01", + "version":"0.02", "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", "tags": "app,sun,moon,cycles,tool,outdoors", "allow_emulator": true, @@ -1785,17 +1741,18 @@ { "id": "toucher", "name": "Touch Launcher", - "shortName": "Menu", + "shortName":"Toucher", "icon": "app.png", - "version": "0.04", + "version":"0.06", "description": "Touch enable left to right launcher.", "tags": "tool,system,launcher", - "type": "launch", + "type":"launch", + "data": [ + {"name":"toucher.json"} + ], "storage": [ - { - "name": "toucher.app.js", - "url": "app.js" - } + {"name":"toucher.app.js","url":"app.js"}, + {"name":"toucher.settings.js","url":"settings.js"} ], "sortorder": -10 }, @@ -1859,7 +1816,7 @@ "id": "minionclk", "name": "Minion clock", "icon": "minionclk.png", - "version": "0.01", + "version": "0.02", "description": "Minion themed clock.", "tags": "clock,minion", "type": "clock", @@ -1875,5 +1832,549 @@ "evaluate": true } ] + }, + { "id": "openstmap", + "name": "OpenStreetMap", + "shortName":"OpenStMap", + "icon": "app.png", + "version":"0.03", + "description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are", + "tags": "outdoors,gps", + "custom": "custom.html", + "storage": [ + {"name":"openstmap.app.js","url":"app.js"}, + {"name":"openstmap.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "activepedom", + "name": "Active Pedometer", + "shortName":"Active Pedometer", + "icon": "app.png", + "version":"0.04", + "description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.", + "tags": "outdoors,widget", + "readme": "README.md", + "storage": [ + {"name":"activepedom.wid.js","url":"widget.js"}, + {"name":"activepedom.settings.js","url":"settings.js"}, + {"name":"activepedom.img","url":"app-icon.js","evaluate":true}, + {"name":"activepedom.app.js","url":"app.js"} + ] + }, + { "id": "chronowid", + "name": "Chrono Widget", + "shortName":"Chrono Widget", + "icon": "app.png", + "version":"0.03", + "description": "Chronometer (timer) which runs as widget.", + "tags": "tools,widget", + "readme": "README.md", + "storage": [ + {"name":"chronowid.wid.js","url":"widget.js"}, + {"name":"chronowid.app.js","url":"app.js"}, + {"name":"chronowid.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "tabata", + "name": "Tabata", + "shortName": "Tabata - Control High-Intensity Interval Training", + "icon": "tabata.png", + "version":"0.01", + "description": "Control high-intensity interval training (according to tabata: https://en.wikipedia.org/wiki/Tabata_method).", + "tags": "workout,health", + "storage": [ + {"name":"tabata.app.js","url":"tabata.js"}, + {"name":"tabata.img","url":"tabata-icon.js","evaluate":true} + ] + }, + { "id": "custom", + "name": "Custom Boot Code ", + "icon": "custom.png", + "version":"0.01", + "description": "Add code you want to run at boot time", + "tags": "tool,system", + "type": "bootloader", + "custom":"custom.html", + "storage": [ + {"name":"custom"} + ] + }, + { "id": "devstopwatch", + "name": "Dev Stopwatch", + "shortName":"Dev Stopwatch", + "icon": "app.png", + "version":"0.01", + "description": "Stopwatch with 5 laps supported (cyclically replaced)", + "tags": "stopwatch, chrono, timer, chronometer", + "allow_emulator":true, + "storage": [ + {"name":"devstopwatch.app.js","url":"app.js"}, + {"name":"devstopwatch.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "batchart", + "name": "Battery Chart", + "shortName":"Battery Chart", + "icon": "app.png", + "version":"0.10", + "readme": "README.md", + "description": "A widget and an app for recording and visualizing battery percentage over time.", + "tags": "app,widget,battery,time,record,chart,tool", + "storage": [ + {"name":"batchart.wid.js","url":"widget.js"}, + {"name":"batchart.app.js","url":"app.js"}, + {"name":"batchart.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "nato", + "name": "NATO Alphabet", + "shortName" : "NATOAlphabet", + "icon": "nato.png", + "version":"0.01", + "type": "app", + "description": "Learn the NATO Phonetic alphabet plus some numbers.", + "tags": "app,learn,visual", + "allow_emulator":true, + "storage": [ + {"name":"nato.app.js","url":"nato.js"}, + {"name":"nato.img","url":"nato-icon.js","evaluate":true} + ] + }, + { "id": "numerals", + "name": "Numerals Clock", + "shortName": "Numerals Clock", + "icon": "numerals.png", + "version":"0.05", + "description": "A simple big numerals clock", + "tags": "numerals,clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"numerals.app.js","url":"numerals.app.js"}, + {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, + {"name":"numerals.settings.js","url":"numerals.settings.js"} + ], + "data":[ + {"name":"numerals.json"} + ] + }, + { "id": "bledetect", + "name": "BLE Detector", + "shortName":"BLE Detector", + "icon": "bledetect.png", + "version":"0.02", + "description": "Detect BLE devices and show some informations.", + "tags": "app,bluetooth,tool", + "readme": "README.md", + "storage": [ + {"name":"bledetect.app.js","url":"bledetect.js"}, + {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} + ] + }, + { "id": "snake", + "name": "Snake", + "shortName":"Snake", + "icon": "snake.png", + "version":"0.02", + "description": "The classic snake game. Eat apples and don't bite your tail.", + "tags": "game,fun", + "readme": "README.md", + "storage": [ + {"name":"snake.app.js","url":"snake.js"}, + {"name":"snake.img","url":"snake-icon.js","evaluate":true} + ] + }, + { "id": "calculator", + "name": "Calculator", + "shortName":"Calculator", + "icon": "calculator.png", + "version":"0.02", + "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", + "tags": "app,tool", + "storage": [ + {"name":"calculator.app.js","url":"app.js"}, + {"name":"calculator.img","url":"calculator-icon.js","evaluate":true} + ] + }, + { + "id": "dane", + "name": "Digital Assistant, not EDITH", + "shortName": "DANE", + "icon": "app.png", + "version": "0.07", + "description": "A Watchface inspired by Tony Stark's EDITH", + "tags": "clock", + "type": "clock", + "allow_emulator": true, + "storage": [ + { + "name": "dane.app.js", + "url": "app.js" + }, + { + "name": "dane.img", + "url": "app-icon.js", + "evaluate": true + } + ] + }, + { + "id": "buffgym", + "name": "BuffGym", + "icon": "buffgym.png", + "version":"0.02", + "description": "BuffGym is the famous 5x5 workout program for the BangleJS", + "tags": "tool,outdoors,gym,exercise", + "type": "app", + "interface": "buffgym.html", + "allow_emulator": false, + "readme": "README.md", + "storage": [ + {"name":"buffgym.app.js", "url": "buffgym.app.js"}, + {"name":"buffgym-set.js","url":"buffgym-set.js"}, + {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, + {"name":"buffgym-workout.js","url":"buffgym-workout.js"}, + {"name":"buffgym-workout-a.json","url":"buffgym-workout-a.json"}, + {"name":"buffgym-workout-b.json","url":"buffgym-workout-b.json"}, + {"name":"buffgym-workout-index.json","url":"buffgym-workout-index.json"}, + {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} + ] + }, + { + "id": "banglerun", + "name": "BangleRun", + "shortName": "BangleRun", + "icon": "banglerun.png", + "version": "0.01", + "description": "An app for running sessions.", + "tags": "run,running,fitness,outdoors", + "allow_emulator": false, + "storage": [ + { + "name": "banglerun.app.js", + "url": "app.js" + }, + { + "name": "banglerun.img", + "url": "app-icon.js", + "evaluate": true + } + ] + }, + { + "id": "metronome", + "name": "Metronome", + "icon": "metronome_icon.png", + "version": "0.04", + "readme": "README.md", + "description": "Makes the watch blinking and vibrating with a given rate", + "tags": "tool", + "allow_emulator": true, + "storage": [ + { + "name": "metronome.app.js", + "url": "metronome.js" + }, + { + "name": "metronome.img", + "url": "metronome-icon.js", + "evaluate": true + } + ] + }, + { "id": "blackjack", + "name": "Black Jack game", + "shortName":"Black Jack game", + "icon": "blackjack.png", + "version":"0.01", + "description": "Simple implementation of card game Black Jack", + "tags": "game", + "allow_emulator":true, + "storage": [ + {"name":"blackjack.app.js","url":"blackjack.app.js"}, + {"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true} + ] + }, + { "id": "hidcam", + "name": "Camera shutter", + "shortName":"Cam shutter", + "icon": "app.png", + "version":"0.03", + "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", + "readme": "README.md", + "tags": "bluetooth,tool", + "storage": [ + {"name":"hidcam.app.js","url":"app.js"}, + {"name":"hidcam.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "rclock", + "name": "Round clock with seconds, minutes and date", + "shortName":"Round Clock", + "icon": "app.png", + "version":"0.03", + "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", + "tags": "clock", + "type": "clock", + "storage": [ + {"name":"rclock.app.js","url":"rclock.app.js"}, + {"name":"rclock.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "hamloc", + "name": "QTH Locator / Maidenhead Locator System", + "shortName": "QTH Locator", + "icon": "app.png", + "version":"0.01", + "description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators", + "tags": "tool,outdoors,gps", + "readme": "README.md", + "storage": [ + {"name":"hamloc.app.js","url":"app.js"}, + {"name":"hamloc.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "osmpoi", + "name": "POI Compass", + "icon": "app.png", + "version":"0.03", + "description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.", + "tags": "tool,outdoors,gps", + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"osmpoi.app.js"}, + {"name":"osmpoi.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "pong", + "name": "Pong", + "shortName": "Pong", + "icon": "pong.png", + "version": "0.02", + "description": "A clone of the Atari game Pong", + "tags": "game", + "type": "app", + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"pong.app.js","url":"app.js"}, + {"name":"pong.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "ballmaze", + "name": "Ball Maze", + "icon": "icon.png", + "version": "0.01", + "description": "Navigate a ball through a maze by tilting your watch.", + "readme": "README.md", + "tags": "game", + "type": "app", + "storage": [ + {"name": "ballmaze.app.js","url":"app.js"}, + {"name": "ballmaze.img","url":"icon.js","evaluate": true} + ], + "data": [ + {"name": "ballmaze.json"} + ] + }, + { + "id": "calendar", + "name": "Calendar", + "icon": "calendar.png", + "version": "0.01", + "description": "Simple calendar", + "tags": "calendar", + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { + "name": "calendar.app.js", + "url": "calendar.js" + }, + { + "name": "calendar.img", + "url": "calendar-icon.js", + "evaluate": true + } + ] + }, + { "id": "hidjoystick", + "name": "Bluetooth Joystick", + "shortName": "Joystick", + "icon": "app.png", + "version":"0.01", + "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.", + "tags": "bluetooth", + "storage": [ + {"name":"hidjoystick.app.js","url":"app.js"}, + {"name":"hidjoystick.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "largeclock", + "name": "Large Clock", + "icon": "largeclock.png", + "version": "0.03", + "description": "A readable and informational digital watch, with date, seconds and moon phase", + "readme": "README.md", + "tags": "clock", + "type": "clock", + "allow_emulator": true, + "storage": [ + { + "name": "largeclock.app.js", + "url": "largeclock.js" + }, + { + "name": "largeclock.img", + "url": "largeclock-icon.js", + "evaluate": true + }, + { + "name": "largeclock.settings.js", + "url": "settings.js" + } + ], + "data": [ + {"name":"largeclock.json"} + ] + }, + { "id": "smtswch", + "name": "Smart Switch", + "shortName":"Smart Switch", + "icon": "app.png", + "version":"0.01", + "description": "Using EspruinoHub, control your smart devices on and off via Bluetooth Low Energy!", + "tags": "bluetooth,btle,smart,switch", + "type": "app", + "readme": "README.md", + "storage": [ + {"name":"smtswch.app.js","url":"app.js"}, + {"name":"smtswch.img","url":"app-icon.js","evaluate":true}, + {"name":"light-on.img","url":"light-on.js","evaluate":true}, + {"name":"light-off.img","url":"light-off.js","evaluate":true}, + {"name":"switch-on.img","url":"switch-on.js","evaluate":true}, + {"name":"switch-off.img","url":"switch-off.js","evaluate":true} + ] + }, + { "id": "miplant", + "name": "Xiaomi Plant Sensor", + "shortName":"Mi Plant", + "icon": "app.png", + "version":"0.01", + "description": "Reads and displays data from Xiaomi bluetooth plant moisture sensors", + "tags": "xiaomi,mi,plant,ble,bluetooth", + "storage": [ + {"name":"miplant.app.js","url":"app.js"}, + {"name":"miplant.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "simpletimer", + "name": "Timer", + "icon": "app.png", + "version": "0.02", + "description": "Simple timer, useful when playing board games or cooking", + "tags": "timer", + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { + "name": "simpletimer.app.js", + "url": "app.js" + }, + { + "name": ".tfnames", + "url": "gesture-tfnames.js", + "evaluate": true + }, + { + "name": ".tfmodel", + "url": "gesture-tfmodel.js", + "evaluate": true + }, + { + "name": "simpletimer.img", + "url": "app-icon.js", + "evaluate": true + } + ] + }, + { + "id": "beebclock", + "name": "Beeb Clock", + "icon": "beebclock.png", + "version":"0.02", + "description": "Clock face that may be coincidentally familiar to BBC viewers", + "tags": "clock", + "type": "clock", + "allow_emulator": true, + "storage": [ + {"name":"beebclock.app.js","url":"beebclock.js"}, + {"name":"beebclock.img","url":"beebclock-icon.js","evaluate":true} + ] + }, + { "id": "findphone", + "name": "Find Phone", + "shortName":"Find Phone", + "icon": "app.png", + "version":"0.01", + "description": "Find your phone via Gadgetbridge. Click any button to let your phone ring. 📳", + "tags": "tool,android", + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"findphone.app.js","url":"app.js"}, + {"name":"findphone.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "getup", + "name": "Get Up", + "shortName":"Get Up", + "icon": "app.png", + "version":"0.01", + "description": "Reminds you to getup every x minutes. Sitting to long is dangerous!", + "tags": "tools,health", + "readme": "README.md", + "allow_emulator":true, + "storage": [ + {"name":"getup.app.js","url":"app.js"}, + {"name":"getup.settings.js","url":"settings.js"}, + {"name":"getup.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "gallifr", + "name": "Time Traveller's Chronometer", + "shortName": "Time Travel Clock", + "icon": "gallifr.png", + "version": "0.01", + "description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.", + "tags": "clock", + "readme": "README.md", + "type": "clock", + "allow_emulator":true, + "storage": [ + { "name": "gallifr.app.js", "url": "app.js" }, + { "name": "gallifr.img", "url": "app-icon.js", "evaluate": true }, + { "name": "gallifr.settings.js", "url": "settings.js" } + ], + "data": [ + {"name":"gallifr.json"} + ] + }, + { "id": "rndmclk", + "name": "Random Clock Loader", + "icon": "rndmclk.png", + "version":"0.01", + "description": "Load a different clock whenever the LCD is switched on.", + "readme": "README.md", + "tags": "widget,clock", + "type":"widget", + "storage": [ + {"name":"rndmclk.wid.js","url":"widget.js"} + ] } ] \ No newline at end of file diff --git a/apps/_example_app/README.md b/apps/_example_app/README.md new file mode 100644 index 000000000..dc139bc9a --- /dev/null +++ b/apps/_example_app/README.md @@ -0,0 +1,25 @@ +# App Name + +Describe the app... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_app/add_to_apps.json b/apps/_example_app/add_to_apps.json index dd66030b6..ee83db39e 100644 --- a/apps/_example_app/add_to_apps.json +++ b/apps/_example_app/add_to_apps.json @@ -5,6 +5,7 @@ "version":"0.01", "description": "A detailed description of my great app", "tags": "", + "readme": "README.md", "storage": [ {"name":"7chname.app.js","url":"app.js"}, {"name":"7chname.img","url":"app-icon.js","evaluate":true} diff --git a/apps/_example_widget/README.md b/apps/_example_widget/README.md new file mode 100644 index 000000000..a909e9e7e --- /dev/null +++ b/apps/_example_widget/README.md @@ -0,0 +1,25 @@ +# Widget Name + +Describe the app... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_widget/add_to_apps.json b/apps/_example_widget/add_to_apps.json index 5d0057960..527c698a0 100644 --- a/apps/_example_widget/add_to_apps.json +++ b/apps/_example_widget/add_to_apps.json @@ -7,6 +7,7 @@ "description": "A detailed description of my great widget", "tags": "widget", "type": "widget", + "readme": "README.md", "storage": [ {"name":"7chname.wid.js","url":"widget.js"} ] diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index 2c81c0537..16aea0610 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -2,3 +2,4 @@ 0.02: Update version checker for new filename type 0.03: Actual pixels as of 5 Mar 2020 0.04: Actual pixels as of 9 Mar 2020 +0.05: Actual pixels as of 27 Apr 2020 diff --git a/apps/about/app.js b/apps/about/app.js index dc7b0cad8..57c85563d 100644 --- a/apps/about/app.js +++ b/apps/about/app.js @@ -29,5 +29,5 @@ g.drawString(NRF.getAddress(),120,232); g.flip(); // Pixel chooser image -g.drawImage(require("heatshrink").decompress(atob("+FQgl+xnu8AIBwGQgHuAoN3gF/hcLgEHu943G3gHdhvdDwIBCAAV3uEAhoBBhsO90OgHgoACBh0IhP5AAQZD8Hw+GwAwXn4AECxGAh0MEAOeJAMP3+/Lw0GswGEHgMM9gCBAIX//5PBhvQ7gJBxAAB9ng8vs5nMDgOg8HnOwIBBgBHDAAfQNAJBBgBQDgF4HQfd7veKoKbBO4Pr30IEAhgBAIIAG3oJDx+AQwLBBYgR3JsABCzOQzOeO4cP4HPc4QCBPoPN4HNO4QoB9wAByDvBO4L2COwZ4Gd4UP/7vEf4LvGKoUAooDB9x3FgEQI4TwBgEIN4NpwEMXILvBO4bvD/Y3BO46eDgGdO4n8CoXw+cQh/w/kNd4fodoXJhLvCKYJ4Dhe7AYJXFwBHBUAgABewMPhvQd4bwB8FQqDvHO4YADhH4B4XM9nABQTsCAAf/awbXBO4Vmd4xED57vD+EwFgOIBoUNxv/1////5zOAy8AvPN6AQCbQIiCOIIKB7EILwZIEO4YACKYlFoB3CHIZ2CAIJHBEAToCMwLvBAArvCAAnAAALvDAIIPByA5BEQUM/n8O4TzCAAQtBhvd/X8d4YYBvwOBO4bBFO4b2D4ASELoP/d4IbGABMBiINLV4YAD9LyFO5bvCYYfPCARKBmAcDh3ud4Wt7vdDgONwF8O4Q8Bh5jCBAOPO4o0BgFAAoLcB/4UBLIgBDAAPI5DeKIQIDChcLL4IABGIOAJITvHAAkGs0HgG7AAO99p3Dhi2N43N7rLCxGHgF56AHCRwUwAYIlBhsNGoR3CqALCh54CFAXHAIg/CRAIDBIgtHGIR3D3ZhCWwXQwA1CAAMP5/M/nPMhp3BwAJGWIQ7Dgczt1pzIHCa4IABhpkBOgQACD4ZRCs1m4AyEO4IBBABUMXYYZDgEEvoRFd4TwBO5IAJ5nAFAMNTYZEBGgRiD7p0CO4nM43JmZABAIICBAAOA+HwgUgkEiGxFsAQOwGQLeBhPpz2QChEO8AoCd4R5CdwZpCNgdVqq0B7vQ7vdMQWIbYJkFAAIjBEoR3DCoOA8A3CYAOvh/wgH/d4hVBd4VAgn/eIYAGX4cAgw2DNQ2e9I0DBgxIBxGAWgS1DAAZrBLAi2DeAJwDOoLcFNQOA5jbCd4gACO4OgAgMHu4aBDokKgGIZ4LtBogABBgXw4HwhnL5lwEQRmJb4bvBO4/uIAfQKAJ3Gh7sC6/XcgR3NDwR3DA4K4CAQJ3GV4JrBCoZuBAIMK1Wg4eAhwRB91AdpENdwbwEAAkHP5D8DPoIrBQ4LvMNYICDO4z7Bd5HM5jvD4DxBd4PQGwIBCHIMAeAQAEhQIC4GIboTfGT4JcBO4TvINQV2sDvCAAw6DRZIcB+APEhoxDACJ3BBZPwAAIsDhTwDXwbvFO5LvQhnMu1wNQoABBAMOM4RqDuFwY4IUEGpKUCcYPwAQIXEAAnu9wbJBQPg+ArCcoIBBhkMMoqCBO4IVBEYfuNYsNLISHDZYkM/93CgmIOwJtBh3uAIPuNQZ3BLwsOSYuIAIOABYPex2P9+JxncZAJcCO5VgXYRPCWQQzF4AABDohHB5gACBYPeSAYAHdwcJQYfc/OQIAQZBwB2BABQMBhiBBcQcP///AoLkBgH4+DvI1GKxGoFRVmXYThFAAwNFh0PawUNxoDC95fBDAsP+AnFFox3B9vtO4LvBG47/CcofOPoYABWIJ3Cd4jYBB4NwgwFBd4LxCIoQuGdwJIBdAoAHBoixBAQMJhvdBALuBBAJ3Gh/ADQkNLwboBAQLvDZAMP54ACMoJcCsAYC5nOV4OXcgQADd4QADs8HsF2g1QSwQAE+AcGRILhD/5cHMAgEFg2AzuNV4bvFhp3C5igN73u6DQBMwIAC/4/BcgaQDhwtBy8A3ewEAjvBAAdQgoCEDYbHCLgRIBeAwMCQoKdDwEMg6XBBgIXDO4WJhuNHQyOF+DvFAAwLB9vdVg7vJAAeXhYjHhGAAIKpL6CoBd4UDgbvDO44gDAYMHW4bCECIWdOoI2FKA0A0AABAwfu9oOFOwPgPI4ABWAICBE4p3KAARaBJQQDCAgJ3DdYLsEdwm3FwP/dwRiCd4nwQoYfDxEN7uIVxh3B1R3Bh0ONo/u93gAIIfMbozvY7oFELoMwA4h3CAAMJzOQAgOIO4LvG6ENAQP4xCjDAAiBBh6aBgEKd4139xNFd4SEBAAY6BhgHExAuG3ewO4zxCTBgnBAAMAgZKCEoo9EO4QAEdAIBBO4mPx5eBuCTDCYWfh/P6AeFNgVwg53EfITvC4BIB4B3HMgv/Vw3d7p3CFIPgHAwAMG4IAROwR1BAIWI/GAhm3gHMLAUAg1md4Q/Fh3uRgN3d4o+CPQPAAAWQ/7GB5nMH48DO4xDCF4YFCP4OAwD4GJgQCBhkJJQquGAwvAAQZsBAALvChfLuAICTKGIwBSDhoEB9yEBNwMM4GfgH8hnPO4wuBmB3ChYfFTYivBhAwBfAQABuA/GVAKKCADH4xHwhm8RYSICAALNIO4vQfgZfB8Hgd5H//gqBeYIrB5fLF4gAC6ENzIQBd453FYoUPO4ZUBCQMP/5SLuHwSg5UBAoggBxCiEJoe8714zUQCYbvBO4pDFXwRPBd4UOfwIzB5e7O44ABzP/LYp3CPAIHCu4XGhgiBBwR3IRQcP54ECyEJzJ3DkYUDGIIABRQTvJhvcZghFCu4XBZgRKGbQQAEO4m7hewGIIAEEJJjIKASKDNwh3Id4cJhJ5BOoMOgE9mAQCxGAd4jBHDAMN3p2Dd4Z+FSYThHhYDCnm8AgWwPAIVB/nM9nDO5kP//wBZD+DF4kPOoIBBC4rtCLwMO8EAgchd4w6JzwYBhHdegYkBO4oMDJwxKEgcAQgZ3D5//53Onk8O4a+BAIO62DbJwEJKIMIZoa1D+AABR4X/O4jvDO4PHyEQu0GfoIADegIAB5vmwGrd4YADSYMGy2WO4jODd4j5EAA52BMwLvB53uO4MNTIUBgIRB1WgCwXuEZYABg4EDHYI9CXAK6FLQcOO4IFBsACBGoMRgGHO4mJO4IAChkKyENNoTvFKwLGHhh5BhnMPoQEDBAnM5jvB4YIBFQUQ+EQd4vgV4LuDAAI0F6DUDO5eZzIFDO4TvDGYIBBd4OHw53BxR3E4GqyHA2ArBgwJBhe7XRH/O4UAhzONAAp3Bh8B+KWBAAnu8CRCAAVVgtQAoULeAq3GABOOSwp3DBIMICg0LW4MJyEIBoTvC38vYgeQyGZBYI3BfAx/DO5wcBSoLsDEILuBhn8BQdA+FAeIw/DBAbuDuEHf4adDbgQBB4IiF2ELbwQBBAwIMDEAuy+R3DOgJ4BO4vQIwfMGQJdB5nM55rELYo4CAAXvO4cIxDdEbw5MDO4n/PAMHAAQJCg/ud4UMAAYMCzOIwB3CEwWwO4oABJQbvFAAg3BHAPgFIKpDO4TgB//5RYIABjUAhUQeAYABxAeC7qWDABJXDOwYABBAsHu7vEAwIbD5h3FhKCBd45qD7ACB1StDBwK4CXY7vGO4cJzOZznMKgoUBO4g/BLYp5MO4sNO4UODYbuCKITvB54TBd453Fd48NhADBZwSnD/7aBh7KBOYZNNhx9CAAQoCO4uIOCIbCAAaiBI4Xg8AUGaoLvB4HwO4bzB34MBhI3BhZxBd4YGBd4t3agRCI7sNAAJsDAQMMN4oKB5jvEAAUNSIhkBh7tDAIcADQuIAALMBd4YBCh0JeAZ3G93Ah7RDAAO7+EJd4QAKd4IOB9x3LOwoADOwxJB5wgBhZHEAYq3B+Hw/8AuAIBAQScBDQQBBd4RtBF4OQAALvOzJ2DRATvCzJ3McQh3BhIfCZghrH7Z3CPAZEC+P4ZwwAHh7vBh/wg4ABTgpRBAIPuEwXteAhlEAAkL3YEC/PwAgW5VoYAGFIYACJ4nMRYIxCc4vMNgUJm4MBIoR3DhxFC/8QDAYiBu7cBRIdwUwLvBAAp3DdwYlBNga3LAA7vHLIZmBBQYMEhGIAodVDwQfB7sNHAf/JgUJMIML7wGBMogACiMf/4VBhKZBuFwhgODuHQE4LwBgDvFCIO7hbNCYokNAgMLXYUPAAp4G+xPCd4vHvgSGPIbvEAAKVCGITwDUAcJ06uHEQSsFhZ3Cd4ZBCO4bqCuAJCO4ULhZ4Bd4Y7C4AqCCQQAK+B9B/9gIQ53FwBxEhAFB5ncDYIsMAA5CD8DCBAQQADd5AFB7ruCh7sBAIaQCAARMBhAzGd52ZzMAsx3CYAZFB5nMTQTMFBgOAJQPQBghYCAQJBBO5wAKIQNwg7vBO4buBABewAAK+DGime9L0DNoI2BeQXAWoZ2Ef4Z3ILAMJyG5IQKoD9wABgHN8F5f5wAGcgJ3GdocAgjuDABLvCdQcGAoh3Fh/vdIJ3CcQLbFPAgAD5ncgEKAIPdRoMJCoJCD/4CBEYIaB4HguGgKBYDGTAKBKfIYQBCQnwaoICCd49gsDKGzLvHKYQADxAIC8HuAQINDd4Wg0HQ5j4ByAaEHoTvFO4OwMouYmcwh//AIIKDhByGZgZ3Bg7dBgxoFCAWACYjoDh7uBgwGDBocN5YfFhz1Bg4GCxOAd5B3BOILwBd4PMZJQAOxEwRoJFCqACBxw3DAASEEd4I7BAwQ4Sd46OCLQIAHO4cIH4R2BPAwAHgYIHhpODO55qBMwMI9HoeYZBC5kM4DvEZ4XAxGAg93zLeC3ew2DwFdwIFEO4kJFoRxDFoQFDBwMA8B2ChjrBAAaAFyBeBAA3QzOZOxQrBUoLvDVYXdSIR3DhnMAALvC6Hgd4YQCIAXwgELfCMPqAcCuF3O4l3AwgAF4AABIQJ3HyYCB1MK7gOCYwOQB4cMNYP/WoYMByDtBBAQHBhv9/p3FOwXMeAK6ChKMCKYV5U4Z3Bd4bqDAAZ3F81wdA14KQggEd4ZlBhn8Qg7vCyGQ6EMgF3O4LvLhQEDxEIMAOgO4MPDQJ3G553DABC4EO4zvM8HgFoQAB+CiBHoIgCAQbwFPQcAgjvHSgPQCINwvvQgEJhe7AAIbBhIWCGARrCwACBKoPd+H9DQJ3DGgPMVwfHyBwEO4ziDWoLvJCgXw9wDBO4f/gHcSYcMDwT0CAAgJDolANAPpeQgfBDQNwuDvD2CaC4HACALuEd4iRB7vzO4MIhEHJITwCZIMMvLYIgf/+RwBaoLWBAYQAHhwLBd4YACqHwAILlFAILyHPAUEAAIkBTIQAGO4QXDO4wAJdQMN7vddwOIg93XIXMhxRBdwIcJ+Hw/7iChnsBgkNhsMHoUOCAJ3BegQABgtVNQwzBAYMLWYIADO4VAOwNAd4oAEKwR3GgEJWwaREVAS6EAA4PCOA7KEO4QDBAIIjBSIPMDYxyDhaCBb4zvJ9wAE2C4CO4IAGFQPgLoVt5nODoJ3B3YTGWQhnIBQkMQoSGMAAwXCh///5/BNgJtC7q9D2HQ2G9BAT/BhLDChgfCCYYADSwZ3I93gAIJ3FABMO7wECCoJmMhkN7o2ChOQzOQcgQAD3ewKYJVFg93u9wEgp3Dd4R6CVYXA2GQgyLCfhTvHyBZCO5vvvaVBD4QkE9wRE/5mDAQR3BhoWCOgIBBAA2q0D3Md4IOMABBPDO5DvGO47YIh8O+65GNAQRF/7dFgHMd4mIwABBQoISEBAMOAAUA8DjDAA/MAYRAF7rxCABsPd5oAN995Z4mAwHM4AQF/+IO4wAGyDvFepB3BgBhCNYNwg93hGIgHAGoUHCwibDoAeDagQXBAIIRCC4h3EgxRLXQQLIhDUBO4cIhZ3Bd44AFzJxDCIMM/IxEd4kNDIsHg8IAgJ3DeAt3AoJiBRIUO9zFDJwIAB2BIJ8C2JIogMJwBBEAAMwaQoAQHBYAChruBd4QHB5iBECgzaCN4MMCQTvF35mGQYR3Ex2wAYP8O4gvG9ns8GIwEMO4cLeAQlCO4hNHAAS4CHAQaBhgACd4sOuHnd4RdDdwYBBCwK+GRIOIJALuBSQUPIQV3DIIABhGZwB3EP4UGRAjXEhp9CdQruI9x4BDIPgEwUA3YABNwQAC4GQHIOwV4QAUUIRpBAwUGKwLvCxjvGVgVwTYIfDBgJvExx3Cd4gBCAAPdpxjCHwigBhLwCBQnuUoVQHARqBAARCDhn5DQIABDIUEYAbnFABDuCAAIJEDIUM5iPKO4tAgGQMIbvGhwACdwR/Dd4MHu48Bh5oCAAkOd4cwbogEBdwgABdwLvJIAJCCdxjvEP4NgB4mIDpF3AAJBCHoZ3EBQTvDc4TwDBIh1BO4X/O44FEfgLvEO4JuHQIQoBd4Z3Gh8Pdw4ABdwqWGS5LuEADp3CBQ/uCpLvH5n5eASQBSIuIaIsP+BCOMoUIDwcIhGIO6DFDABpLEuAhC/4ABDJpXBhe7gG7dw4AC8AABaAjPIAAmgdZoDCAoX8ShIJEzOZXAetFZTDFX4f/FZHP/ieQFQgrFO4g2HTQOqEBLpBeAPAPonAAwTNBKwnvd5Pb6ADB9wACFALDBIALEGAA71C4EMVBAAMFIcLO4o0EKgMPhcz9zEKOIMMHYI8DXAcHg8AxApCIwIHBAAzvEOIUAu9wO40IO5EJzIoBd4p3Fh3dAwg7Eh6TCuDFEhxRDd4uu3QFBokEoEA9RHCY4J1BhnMHYbvCuGAvAPBeoZlBH4V3GYOOXgsOFAJNBO4YSB+/3MgPMhJLBJoUJ/JvFgcAmAHE93QOoZtBAQSKDhcIeAKHIgHA53u93qeAVAAAJWB1wRDd4wAEsEIO4MGs1mu4ABHQQCBhHIO4wDB2GwG4Pu8BRBv9/CwMM/ON6ABBd4h3KhzvEOgMHAQKeBO4TvGIwQAD5nA8Hg92u1R3BAITwEd4Z3Hg0GgGIgB2BO4d2IITvJO4ZDEKQKRCd40P/+QGwsiAwsOd4hnCOAQbBKYLuLMoJFB9w=")),0,135); +g.drawImage(require("heatshrink").decompress(atob("+FQgl+xnu8AIBwGQgHuAoN3gF/hcLgEHu943G3gHdhvdDwIBCAAV3uEAhoBBhsO90OgHgoACBh0IhP5AAQZD8Hw+GwAwXn4AECxGAh0MEAOeJAMP3+/huIDocMg1mMog8BhnsAQIBC///J4MN6HcBIOIAAPs8Hl9nM5gcB0Hg852BAIMAI4YAD6BoBIIMAKAcAvA6D7vd7xVBTYJ3B9e+hAgEMAIBBAA29BIePwCGBYILECO4Y+BCIXMsEAAIOZyGZzx3Dh/A57nCRgUA5vA5p3CFAPuAAOQd4J3BewR2DPAzvCh//d4j/Bd4xVCgFFAYPuO4sAiBHCeAMAhBvBtOAhi5Bd4J3Dd4f7/7vDh4TBOoKeDgGdO4n8JoIvB+cQh/w/kNd4fodoXJhLvCKYJ4Dhe7AYJXFwBHBUAhBCAIMN6DvDeAPgqFQd453DAAcI/APC5ns4AKCdgQAD//wUwMMhhgBO4Nmd4xED57vD+EwFgKTCYoON/+v////OZwGXgF55vQCATaBEQRxB6Hw7EILwZIEO4YACKYlFoB3CHIZ2CAIJHBEAToCMwLvBAArvCAAnA4HP/8MOoIBBB4OQHIIiChn8/h3CeYQACFoMN7v6/jvDDAN+BwJ3DYIoKBh/YewfACQhdB/7vBDYwAJgMRBpavDAAfpeQp3D+B1CO4bvCYYfP4BKDmAcDh3ud4Wt7vdDgONwF8O4Q8Bh5jCEoOPgHf/53CGgMAoAFBbgP/CgJZEAIYAB5HIbxRCBAYULhZfBAAMA/GA/47Bd44ABh4CBg1mg8A3YAB3vtO4cMWxvG5vdZYWIw8AvPQA4SOCmADBEoMNho1CO4VQBYRABPAIoC44BEH4SIBAYJEFo4xCO4e7MITLC+GANYRwC5/M/nPMhp3BwAJGWIQ7Dgczt1pzIHCa4IABhpkBOgQACD4ZRCs1m4AyEJgJOEAA8MXYYZDgEEvoRFd4TwBO5IAJ5nAFAMNTYZEBGgRiD7p0CO4nM43JmZABAIICBAAOAHIMCkEgkQgD3cOAgVsAQOwGQLeBhPpz2QJZEO8AoCd4R5CdwcNAQkAqtVWgP/+H//5iCxDbBMgoABEYIlCO4YVBwHgG4TAB18P+AnBd4hVBd4VAgn/eIYAGX4Ww30GGwZqGz3pGgYMGJAOIwC0CWoYAD7vdLAnQNYK2COAZ1BbgpqBwHMbYTvEAAR3B0AEBg93DQIdEhUAxDPBdoNEAAIMC+HA+EM5fMuAiC8DvCu4IBb4zvBO4/uIAfQKAJ3Gh7sC6/X7ogBUIL0BCwJ3HDwR3DA4K4CAQJ3GKAJrBCoZuBAIMK1Wg4eAhwRB91AdpA/BdwQAB2BhCO4cHc5D8DPoIrBQ4LvM6BWBAQILCwB9BO4P//7vI5nMd4fAeILvB6A2BAIQ5BgDwCAAkKBAXAxDdCAAIPET4K3DLwQAB3wmBOQJqCu1gd4QAGHQYADRYocB+APEhoxChPJG4TlFAA53BzOZBY/wAAIsDhTwDXwbvFO5LvHxbvEdwUM5l2egZqCAAIIBhxnCNQdwuDHBCgg1JeAPgcYPwAQIXEhOQAgXu92QAAIdGJYPg+ArCcoIBBhgpBMoiCBO4IVBDAIcChYRFLISHDAwN3NIMM/93CgmIOwJtBh3uAIPuNQZ3BLwgiBSYuIAIOA5MO72Ox/vxOM7jIBLgMJhJ3EzJ3DsC7CJ4SyCGYvAAAKJEI4PMAAQLB7yQDgGJwADBAQTuBWgSDD7n5HQJrDwB2BABQMBhiBBA4Xgh///4FBcgMA/HwBgTvF1GKxGoO4gAByGZAYNmAQLhGAAwNFh0PboUNxoDC95fBB4UIzEAh/wE4otGO4Pt9p3Bd4I3Hf4TlD5x9DAAKxBGYTvDbAQPBuEGAoLvBAIMJGgMPXATuBA4LuBJALoFXYIkCeAYEDWIICBhMN7oIBdwIIBCAbwBh8P4AaBEQUNLwYIDd4bIBh/PAARlBLgVgDAXM5yvBy7kCAAbvCAAdng9gu0GqCWCAAnwDgyJBcIf/LgYnGSQYEDg2AzuNV4bvENoIRBh/MUAwAG73u6DQBMwIAC/4/BcgaQDhwtBy8A3ewEAjvBAAdQgoCEhfu9cOY4RcCJAIWDeAQMCQoJ1Bd4OAhkHS4IMBC4Z3CxMNxo6GRwvwd4QAJBYPt7qsCAAPgOQLvJAAeXhYdCZYIBBKYOAAIIwI3yMB6CoBd4UDgbvDO44gBPIQ+BW4YADD4TvBOoI2FKA0A0AABAwfu9oOFOwPgAQLgBDoqwBAQIJFO5QACJIP/JQIDC+AVCO4LrBdgjuE24uB/7uFd4nwQob0DxEN7uIVxJ3E1R3Bh0ONoZ+E93gAIIPCVQ7fDgENAwRhC8AWBE4LvNAAXdaQsAmAHEO4QABhOZyB6BxB3BIg3QH4PQ/GIEIIAGQIMPTQMAhTuB1DaE9xNCAQTvCLgQACyDcDAAWIFARbD3ew9ycEKILvCABkMAAMAgZKCAAYlBHog8BAArqDO4mPx5bBuCTDCYWfh/P6AeFNgVwg7FEaITvC4BIB4B3HMgXdEwP/VwyCBO4QpB8A4GABiUCACB2COoIBCxH4wEM28A5hYCgEGszvC6F3NojKBuF3O4g+DPQPAAAWQ/7GB5nMH48D+AsCAAZDBF4YFCP4OAwD4GJgQCBhkJBYg8BBQJeBCgoABBAQCBNgIABd4UL5dwBASZQxGAKQcNAgPuQgJuBhnAz8A/kM553GFwMwO4PPhYfFTYjvBhAwBfAQABuA/GVAKKCTgxdR/GI+EM3gXCSIZeBg8Au7vEO4vQJgIAB+BTB8DvI//8FQLzBFYPL5YDBKQvQd5Z3FYoUPO4ZUBCQOf/5YDVoIFDIwNw+CUHBgQADEAOIUQnHg9wg+8714zUQCYbvBO4pDFXwRPBd4UOfwIzB5e7U4gAMO4R4BA4S4HhgiBO452DRQcP54ECyEJzJ3DkYXDGIIABRQTvCVoI0EhvcZghFCu4QBhswJQ7rBBAp3E3cL2AxBCIr0EABJjCKASKDO4q7ChwTC8DvDhMJPIIJBh0AnpUDxGAd4kAdwJ3DzIYBhu9OwbvDAAXfEoKTCcI8LAYU83gEC2B4BCoP85ns4Z6BO5UP/5lCAAz+DF4kPOoIBBC4rtCLwMO8EAgchd4w6JzwYBhHdYoibBaoO72He7qbCJwxKEgcAQgZ3D5//53Onk8O4YiBAIO62DvIKQMJKIMIZoa8D+AABR4X/O4jvDO4PHyEQu0GcYT0EAAPN82A1bvDAAaTBg2WywID6ENJ4TvEIYYAIOwIWBd4PO9x3BhvQUwMBgIRB1WgCwXuEZYABg4EDHYI9CXAK6FLQcOO4IFBsACBGoMRgGHO4mJO4IAChkKyENYgTvCAAWN77GHhh5BhnMPoQEDBAnM5jvB4YIBFQUQ+EQd4vgV4LuDAAI0F6DUDO44aDzOZCwZ3Cd4YzBAILvBw+HO4OKO4nA1WQ4GwFYMGBIML3YDBJwYAC/53CgEOZxoAFO4MPgPxSwIAE93gSIQACqsFqEMF4MLeAqPDW4QAJxyWFO4YJBhAUGhZoBhOQhANCd4W/l51DyGQzILBG4LgBAAp/CO5wcBSoJcDEIJfBhn8gH5bgNA+FAQAo0DboMO/zwCAANwg7/DTobcCAIPBH4uwhbeCAIIGBBgYgDboOy+WwcQR0BPAJ3F6BGD5gyBLoPM5nPNYhbFHAQAC953DhGIgGZNAMPFwJ3FJgYOBC4X/PAMHAAQOCg/ud4UMAAYMCzOIwB3CEwWwO4oABJQbvFAAg3BHAPgFIKpDO4TgB//5RYIABjUAhUQeAYABxAeC7qWDAALvCAAfAK4Z2DAAIIFg93d4gGBAgSVBO4sJQQLvH2EIBwPYAQOqVoYOBXAICDbI5YDO4cJzOZzjPEKYXQO4PMCQI/BLYorIABGQhp3ChwbDdwRRCd4PPCYLvHO4rvHhp6CZwSnD/7aBh6/EZYoAIhx9CAAQoCO4UHgzvBOCIbCAAaiBI4Xg8AUG2DvC4HwO4bzB34MBhI3BhZxBd4YGBDoTvCu7UCIRHdhoABNgYCBhhvFBQPMd4gAChqRBg9gMgUPdoYBDfwIaExAABZgLvDAIUOhIBBQAMJAYJ3D93Ah7RDAAO7+ARBEQgADBAbvBAoPuO48OW4R2FAAZ2GCoPOEAMLX4gDCNYS3B+Hw/8AuAIBAQScBDQQBBG4SoBF4OQAALvDO4ZQCd4eZOwbDCd4WZwEPGwQAL7p3BhOQDALMBQQPgNY/bO4R4DCAXx/DOGAAZnBAAMPd4JCBg4ABTgo4BAIPuEwXteAhlDJgOQd4UL3YMC/PwAgW52EJ/grDh//O4IpDeQ0A5iLBGIOwc4ZBB5hsChM3eoJFCO4cOVYX/iAkDEQN3OgKJDuCmBd4IAFO4buDEoImCW4QARd4x3D5nMO4QKBFIcAhGIAodVDwQfB7sN6CLBwH/JgUJMIML7zaCMoYACiMfF4PwX4OQuFwdgZ3B6BgBeAMAd4oRB3cLVgLFFhoEBha7Ch8PhAABAgJ4G+xPCd4vHvjBBVIZ5Ed4gABSoQxChsICQKgDhOnVw4iCT4hQBO4TvDMYR3DdQVwBIR3ChcLPALvDHwXAFQQSCABXwPoP/sBCHO4SMCwBxEhAFB5ncDYIsMAA5CD8DCBAQOZ5nMRYTvHAoPdH4UPdgIBDSAQACJgMIGYzvDdoQADBweZzMAsx3CYAZIBIofAZgoMBwBKB6AMELAQCBIIJ3OAAmZ/6YDIQNwg7vBO4buBABewAAK+DGh4AEz3pegZtBGwLyC4C1DOwj/DO5BYBhOQ3JCBh7LBgHuAAMA5vgvI9HVAKpCABDkBO4ztDgEEdwYAJd4TqDgwFEO4sP95ABO4TiBbYp4EKoncgEKAIPdRoMJCoJCDbYQjBDQPA8Fw0BQLAYyYBQJT5DCAISE+DVBAQTvHsFgZQ2Zd45TCAAeIBAXg9wCBBobvC0Gg6HMfAOQDQg9Cd4p3B2BlFzEzmEP/4BBBQbEDAAcPO4kHboMGNAoQCwATEdAcIdwMGAwYWDhvLD4sOeoMHAwWJwDvIO4JxBeALvB5jJKABf4RAOImCNBKoVQAQOOG4YACQgjvBHYIGCHCTvFh8fRwRaBAA53DhA/COwJ4GAAULhy7BhkDBo8NJwYAHxAqBO4hqBMwMI9HoeYZBC5kM4DvEZ4XAEIMHu+Zh5iB3ew2HP5nAdAbwBAocP+J3ChItCOIYtCAoYOBgHgOwUMdYIADBIOw8Fw6GQLwIAG6GZzLvKFYJ6Bd4arC7qRCO4cM5gABd4XQ8DvDCARKC+C8BAgP//4GBABEBiJ3BqAcCuF3O4l3AwgAF4AABIQJ3Ch7wDyYIB1MK7gOCYwOQDgcMNYP/NwQMCyDtBBAQHBhv9/p3FOwTZBXQcJx3ugF3uEHvKnDO4LvDdQYADL4kP81wdA14KQmwcoq3CAQP8BYfweATvCyGQ6EMI4J3Bd5UAhQEDxEIdoOgO4MPDQJ3GMIZEF8BXCJQR3EGpIAFh/g8AtCLwQlBHoIgCAQbwFPQcAggLEd4SUB6ARBuF96EAhML3YABDYMJCwQwCNYWAAQJVB7vw/oaBO4Y0B5iuD4+Qhx3Kh4DCWoIGBh7tCAgIUE+HuAYJ3D/8A7iTDhgeCegQAEBIdEoBoB9IIDO4PcDQNwuDvD2CaC4HACALuEd4iRB7vzO4JTBg5JCeATJBhl5d4wEBgf/+RwBaoIMBAYQAHhwLBd4YACqHwAILlFAILyHPAUEAAIkBTISDEAAJ3CC4Z3GABLqBhvd7ruBxEHu65C5kOKILuBLgQ3CNoILB+Hw/7iChnsFIkNhsMHoUOCAJ3BegQABgtVNQwnBAYMLWYIADNgVAOwNAd4UN5pfFKwR3GgEJgBkBLIX/VoKoCXQgAHB4QAFOAPwLYIBBO4QDBAIIjBSIPMDYxyDhaCBb4zvJ9wAE2C4CO4KlEO4IqBXQUAtvM5wdBO4O7fggTBCgJJCM5ByEhjjEAA4KBBg4XCh//UoRsBNoXdJwWw2HQ2G9BAIYBhcJYYIFBD4TRCAAiWDO4sAyEA93gAIJ3FAA94vEO70AzOQCoLtMhkN7o2ChOQDALkCAAe72BTBKosHu93VYIAENwKOBd4R6CVYXA2GQgyLCfhTvHLYJ3P997SoNwhBgCEgXuCIn/MwYCCO4MNCwQvBAIIAG1WgSxbvCGggABCpjqCAwsIDojvGaYR3EbBEPh33uELg94cAoRF/7dFgHMd4mIwABBQoISEBAJkCCQPgcYIAJ5jvCfQvdeIQANh7vLGRbvEvOQW4KbBwGA5nACwv/xB3GAA2Qd4r1INAMAMIRrBuEHu8IxEA4HARAMHCwibDoAeDagQXBAIIRCC4h3EgxQKhi6CBIsIaIICCO4cIQYP/d44AFzJxDCIMM/IMDd4sNDIsHg6uBO4QJCeAl3AoJiBRIUO9wLBYoJOBAAOwJBPgWxA8BVIJEC7oPHwBBEAAMwaQoAQd5I+FdwLvCA4PMQIg2GbQRvBhgSCd4u/FQsOQYR3BhP8gGO2AIB/kN6HMOwR9B6AZC9ns8GIwEMO4cLeAQlCO4hNCAA64CO4QaBhgACd4sOuHnd4RdDdwYBBO4i+DRIOIJALuBSQUPIQV3DIIABhGZwB3EP4UGOIJ4BOwJfC6ENAwL6BMJA/E9x4BDIPgEwUA3YABNwQAC4GQPAOwV4QAUUI0HgxWBd4WMd4ysCuCbBDAYMBDALvDO4TvBOIJwBeAfdpxjCG4igBhLwCBQnuUoVQHARqBAARCDhn5DQIABDIUEYAZIBsABCABFwgcwmEzJ4IZFhnMR5R3FoEAyBhDd4gABhwACdwQICd4UHu9wO4JoCAAkOd4cwbogEBdwgABdwLvJIAOAs8HO5LuFhCxBuATFxBgCAASACu4ABIIQ9DO4gKCd4Pd6DnCh0NUobvCOoJ3C/53HAoj8Bd4h3BNw6BCFALvDO4d3MYMPh7uGAYUwYIPgJQgeDD4QHDZoKSGAAcKSwIAVO4QFCT4JFC9wVJd4/M/LwCSAKRFxDRBh95AwMP+AnJO4LvCMoRdDxAKBxB3R1AJHeILsBAQMNbotwEIX/AAIHBAAIdFs3M5kAK4ML3cA3buCVY/gAALQEAIMHUAIAI0AGFdwjrCAYQFC/g8BO4QAETwjvBRYetFYwADYYoACh//EIJ/BO4nP/lm9x3BABGAPYQqEFYp3CFAI2HTQOqFBLpBUQJuCO4XA4EMIAJLEh/vD5PbTgXuAATJC8BABYgwAHeoI1Bhh3DVAdAJocLeBBoDO4g0FKgMPhcz9zEKOIMMHYMMBAX8AYUHg8AxApCIwIHBAAzvEOIUAu9wO40IO5EJzIoBd4XMO4dAp8EcgPdgGwDgQ7Eh6TCuDFEhxRDd4uu3QFBokEUAPqI4SgBOoLoCNgT2CuGAvCwDF4JlBH4V3GYOOAwO7hewOIIoBJoJ3F+/3+CoByBLBJoUJ/LnFgcAmEAwmAO4Pu6BNCg5tBAQS7DfYLwBAAbDF4HO93u9TwCoAABKwOuCIbvGAAlghA5Bg1ms13AAI6CAQMI5AFB2AABd4YFBG4PuO4V/v4WB5+QxvQAILvEO49NJwMOd4RlCOwICBWIJ3Cd4xGCAAfM4Hg8Hu12qFwQBBeAjvDO48Gg0AxEAOwJ3Du1mHwLvE2ABBO4oiFSITvHh//yB3EgEiAoVEYwSKBboY2BOAQbBKYLuLMoMAOwIA=")),0,135); g.flip(); diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog index 98e3da8e7..9687bc58f 100644 --- a/apps/aclock/ChangeLog +++ b/apps/aclock/ChangeLog @@ -6,3 +6,5 @@ 0.09: center date, remove box around it, internal refactor to remove redundant code. 0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed 0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date +0.12: Fix regression after 0.11 +0.13: Fix broken date padding (fix #376) diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js index 7b60a728f..951145c4e 100644 --- a/apps/aclock/clock-analog.js +++ b/apps/aclock/clock-analog.js @@ -1,7 +1,3 @@ -// eliminate ide undefined errors -let g; -let Bangle; - // http://forum.espruino.com/conversations/345155/#comment15172813 const locale = require('locale'); const p = Math.PI / 2; @@ -88,7 +84,7 @@ const drawDate = () => { const dayString = locale.dow(currentDate, true); // pad left date - const dateString = (currentDate.getDate() < 10) ? '0' : '' + currentDate.getDate().toString(); + const dateString = ("0"+currentDate.getDate().toString()).substr(-2); const dateDisplay = `${dayString}-${dateString}`; // console.log(`${dayString}|${dateString}`); // center date diff --git a/apps/activepedom/10600.png b/apps/activepedom/10600.png new file mode 100644 index 000000000..36de436df Binary files /dev/null and b/apps/activepedom/10600.png differ diff --git a/apps/activepedom/1600.png b/apps/activepedom/1600.png new file mode 100644 index 000000000..fb11f999a Binary files /dev/null and b/apps/activepedom/1600.png differ diff --git a/apps/activepedom/600.png b/apps/activepedom/600.png new file mode 100644 index 000000000..4d2c625c7 Binary files /dev/null and b/apps/activepedom/600.png differ diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog new file mode 100644 index 000000000..ca26a648a --- /dev/null +++ b/apps/activepedom/ChangeLog @@ -0,0 +1,4 @@ +0.01: New Widget! +0.02: Distance calculation and display +0.03: Data logging and display +0.04: Steps are set to 0 in log on new day \ No newline at end of file diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md new file mode 100644 index 000000000..a2a351a12 --- /dev/null +++ b/apps/activepedom/README.md @@ -0,0 +1,72 @@ +# Active Pedometer +Pedometer that filters out arm movement and displays a step goal progress. + +I changed the step counting algorithm completely. +Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long. +To get in 'active' mode, you have to reach the step threshold before the active timer runs out. +When you reach the step threshold, the steps needed to reach the threshold are counted as well. + +Steps are saved to a datafile every 5 minutes. You can watch a graph using the app. + +## Screenshots +* 600 steps +![](600.png) + +* 1600 steps +![](1600.png) + +* 10600 steps +![](10600.png) + +## Features Widget + +* Two line display +* Can display distance (in km) or steps in each line +* Large number for good readability +* Small number with the exact steps counted or more exact distance +* Large number is displayed in green when status is 'active' +* Progress bar for step goal +* Counts steps only if they are reached in a certain time +* Filters out steps where time between two steps is too long or too short +* Step detection sensitivity from firmware can be configured +* Steps are saved to a file and read-in at start (to not lose step progress) +* Settings can be changed in Settings - App/widget settings - Active Pedometer + +## Features App + +* The app accesses the data stored for the current day +* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day + +## Data storage + +* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data) +* One file is created for each day +* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime +* 'now' is UNIX timestamp in ms +* You can use the app to watch a steps graph +* You can import the file into Excel +* The file does not include a header +* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400) +* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss + +## Settings + +* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100 +* Min time (ms): Minimum time between two steps in milliseconds, steps will not be counted if fallen below. Standard: 240 +* Step threshold: How many steps are needed to reach 'active' mode. If you do not reach the threshold in the 'Active Reset' time, the steps are not counted. Standard: 30 +* Act.Res. (ms): Active Reset. After how many miliseconds will the 'active mode' reset. You have to reach the step threshold in this time, otherwise the steps are not counted. Standard: 30000 +* Step sens.: Step Sensitivity. How sensitive should the sted detection be? This changes sensitivity in step detection in the firmware. Standard in firmware: 80 +* Step goal: This is your daily step goal. Standard: 10000 +* Step length: Length of one step in cm. Standard: 75 +* Line One: What to display in line one, steps or distance. Standard: steps +* Line Two: What to display in line two, steps or distance. Standard: distance + +## Releases + +* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/activepedom (https://banglejs.com/apps) +* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/activepedom (https://purple-tentacle.github.io/BangleApps/#widget) +* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer + +## Requests + +If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/ \ No newline at end of file diff --git a/apps/activepedom/app-icon.js b/apps/activepedom/app-icon.js new file mode 100644 index 000000000..82e786c7f --- /dev/null +++ b/apps/activepedom/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I=")) \ No newline at end of file diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js new file mode 100644 index 000000000..cc875f371 --- /dev/null +++ b/apps/activepedom/app.js @@ -0,0 +1,165 @@ +(() => { + +//Graph module, as long as modules are not added by the app loader +Modules.addCached("graph",function(){exports.drawAxes=function(b,c,a){function h(a){return e+m*(a-t)/x}function l(a){return f+g-g*(a-n)/u}var k=a.padx||0,d=a.pady||0,t=-k,w=c.length+k-1,n=(void 0!==a.miny?a.miny:a.miny=c.reduce(function(a,b){return Math.min(a,b)},c[0]))-d;c=(void 0!==a.maxy?a.maxy:a.maxy=c.reduce(function(a,b){return Math.max(a,b)},c[0]))+d;a.gridy&&(d=a.gridy,n=d*Math.floor(n/d),c=d*Math.ceil(c/d));var e=a.x||0,f=a.y||0,m=a.width||b.getWidth()-(e+1),g=a.height||b.getHeight()-(f+1);a.axes&&(null!==a.ylabel&& + (e+=6,m-=6),null!==a.xlabel&&(g-=6));a.title&&(f+=6,g-=6);a.axes&&(b.drawLine(e,f,e,f+g),b.drawLine(e,f+g,e+m,f+g));a.title&&(b.setFontAlign(0,-1),b.drawString(a.title,e+m/2,f-6));var x=w-t,u=c-n;u||(u=1);if(a.gridx){b.setFontAlign(0,-1,0);var v=a.gridx;for(d=Math.ceil((t+k)/v)*v;d<=w-k;d+=v){var r=h(d),p=a.xlabel?a.xlabel(d):d;b.setPixel(r,f+g-1);var q=b.stringWidth(p)/2;null!==a.xlabel&&r>q&&b.getWidth()>r+q&&b.drawString(p,r,f+g+2)}}if(a.gridy)for(b.setFontAlign(0,0,1),d=n;d<=c;d+=a.gridy)k=l(d), + p=a.ylabel?a.ylabel(d):d,b.setPixel(e+1,k),q=b.stringWidth(p)/2,null!==a.ylabel&&k>q&&b.getHeight()>k+q&&b.drawString(p,e-5,k+1);b.setFontAlign(-1,-1,0);return{x:e,y:f,w:m,h:g,getx:h,gety:l}};exports.drawLine=function(b,c,a){a=a||{};a=exports.drawAxes(b,c,a);var h=!0,l;for(l in c)h?b.moveTo(a.getx(l),a.gety(c[l])):b.lineTo(a.getx(l),a.gety(c[l])),h=!1;return a};exports.drawBar=function(b,c,a){a=a||{};a.padx=1;a=exports.drawAxes(b,c,a);for(var h in c)b.fillRect(a.getx(h-.5)+1,a.gety(c[h]),a.getx(h+ + .5)-1,a.gety(0));return a}}); + +const storage = require("Storage"); +const SETTINGS_FILE = 'activepedom.settings.json'; +var history = 86400000; // 28800000=8h 43200000=12h //86400000=24h + +//return setting +function setting(key) { +//define default settings +const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + 'stepLength' : 75, +}; +if (!settings) { loadSettings(); } +return (key in settings) ? settings[key] : DEFAULTS[key]; +} + +//Convert ms to time +function getTime(t) { + date = new Date(t); + offset = date.getTimezoneOffset() / 60; + //var milliseconds = parseInt((t % 1000) / 100), + seconds = Math.floor((t / 1000) % 60); + minutes = Math.floor((t / (1000 * 60)) % 60); + hours = Math.floor((t / (1000 * 60 * 60)) % 24); + hours = hours - offset; + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + return hours + ":" + minutes + ":" + seconds; +} + +function getDate(t) { + date = new Date(t*1); + year = date.getFullYear(); + month = date.getMonth()+1; //month is zero-based + day = date.getDate(); + month = (month < 10) ? "0" + month : month; + day = (day < 10) ? "0" + day : day; + return year + "-" + month + "-" + day; +} + +//columns: 0=time, 1=stepsCounted, 2=active, 3=stepsTooShort, 4=stepsTooLong, 5=stepsOutsideTime +function getArrayFromCSV(file, column) { + i = 0; + array = []; + now = new Date(); + while ((nextLine = file.readLine())) { //as long as there is a next line + if(nextLine) { + dataSplitted = nextLine.split(','); //split line, + diff = now - dataSplitted[0]; //calculate difference between now and stored time + if (diff <= history) { //only entries from the last x ms + array.push(dataSplitted[column]); + } + } + i++; + } + return array; +} + +function drawGraph() { + //times + // actives = getArrayFromCSV(csvFile, 2); + // shorts = getArrayFromCSV(csvFile, 3); + // longs = getArrayFromCSV(csvFile, 4); + // outsides = getArrayFromCSV(csvFile, 5); //array.push(dataSplitted[5].slice(0,-1)); + now = new Date(); + month = now.getMonth() + 1; + if (month < 10) month = "0" + month; + filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; + var csvFile = storage.open(filename, "r"); + times = getArrayFromCSV(csvFile, 0); + first = getDate(times[0]) + " " + getTime(times[0]); //first entry in datafile + last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); //last entry in datafile + //free memory + csvFile = undefined; + times = undefined; + + //steps + var csvFile = storage.open(filename, "r"); + steps = getArrayFromCSV(csvFile, 1); + first = first + " " + steps[0] + "/" + setting('stepGoal'); + last = last + " " + steps[steps.length-1] + "/" + setting('stepGoal'); + + //define y-axis grid labels + stepsLastEntry = steps[steps.length-1]; + if (stepsLastEntry < 1000) gridyValue = 100; + if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 1000; + if (stepsLastEntry > 10000) gridyValue = 5000; + + //draw + drawMenu(); + g.drawString("First: " + first, 10, 30); + g.drawString(" Last: " + last, 10, 40); + require("graph").drawLine(g, steps, { + //title: "Steps Counted", + axes : true, + gridy : gridyValue, + y : 60, //offset on screen + x : 5, //offset on screen + }); + //free memory from big variables + allData = undefined; + allDataFile = undefined; + csvFile = undefined; + times = undefined; +} + +function drawMenu () { + g.clear(); + g.setFont("6x8", 1); + g.drawString("BTN1:Timespan | BTN2:Draw", 20, 10); + g.drawString("Timespan: " + history/1000/60/60 + " hours", 20, 20); +} + +setWatch(function() { //BTN1 + switch(history) { + case 3600000 : //1h + history = 14400000; //4h + break; + case 86400000 : //24 + history = 3600000; //1h + break; + default : + history = history + 14400000; //4h + break; + } + drawMenu(); +}, BTN1, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN2 + g.setFont("6x8", 2); + g.drawString ("Drawing...",30,60); + drawGraph(); +}, BTN2, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN3 +}, BTN3, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN4 +}, BTN4, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN5 +}, BTN5, {edge:"rising", debounce:50, repeat:true}); + +//load settings +let settings; +function loadSettings() { +settings = storage.readJSON(SETTINGS_FILE, 1) || {}; +} + +drawMenu(); + +})(); \ No newline at end of file diff --git a/apps/activepedom/app.png b/apps/activepedom/app.png new file mode 100644 index 000000000..6fccf4308 Binary files /dev/null and b/apps/activepedom/app.png differ diff --git a/apps/activepedom/settings.js b/apps/activepedom/settings.js new file mode 100644 index 000000000..94ae435d2 --- /dev/null +++ b/apps/activepedom/settings.js @@ -0,0 +1,112 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'activepedom.settings.json'; + const LINES = ['Steps', 'Distance']; + + // initialize with default settings... + let s = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + 'stepLength' : 75, + 'lineOne': LINES[0], + 'lineTwo': LINES[1], + }; + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + const saved = storage.readJSON(SETTINGS_FILE, 1) || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + //WIDGETS["activepedom"].draw(); + }; + } + + const menu = { + '': { 'title': 'Active Pedometer' }, + '< Back': back, + 'Max time (ms)': { + value: s.cMaxTime, + min: 0, + max: 10000, + step: 100, + onchange: save('cMaxTime'), + }, + 'Min time (ms)': { + value: s.cMinTime, + min: 0, + max: 500, + step: 10, + onchange: save('cMinTime'), + }, + 'Step threshold': { + value: s.stepThreshold, + min: 0, + max: 100, + step: 1, + onchange: save('stepThreshold'), + }, + 'Act.Res. (ms)': { + value: s.intervalResetActive, + min: 100, + max: 100000, + step: 1000, + onchange: save('intervalResetActive'), + }, + 'Step sens.': { + value: s.stepSensitivity, + min: 0, + max: 1000, + step: 10, + onchange: save('stepSensitivity'), + }, + 'Step goal': { + value: s.stepGoal, + min: 1000, + max: 100000, + step: 1000, + onchange: save('stepGoal'), + }, + 'Step length (cm)': { + value: s.stepLength, + min: 1, + max: 150, + step: 1, + onchange: save('stepLength'), + }, + 'Line One': { + format: () => s.lineOne, + onchange: function () { + // cycles through options + const oldIndex = LINES.indexOf(s.lineOne) + const newIndex = (oldIndex + 1) % LINES.length + s.lineOne = LINES[newIndex] + save('lineOne')(s.lineOne) + }, + }, + 'Line Two': { + format: () => s.lineTwo, + onchange: function () { + // cycles through options + const oldIndex = LINES.indexOf(s.lineTwo) + const newIndex = (oldIndex + 1) % LINES.length + s.lineTwo = LINES[newIndex] + save('lineTwo')(s.lineTwo) + }, + }, + }; + E.showMenu(menu); +}); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js new file mode 100644 index 000000000..2ae1b9b62 --- /dev/null +++ b/apps/activepedom/widget.js @@ -0,0 +1,232 @@ +(() => { + var stepTimeDiff = 9999; //Time difference between two steps + var startTimeStep = new Date(); //set start time + var stopTimeStep = 0; //Time after one step + var timerResetActive = 0; //timer to reset active + var timerStoreData = 0; //timer to store data + var steps = 0; //steps taken + var stepsCounted = 0; //active steps counted + var active = 0; //x steps in y seconds achieved + var stepGoalPercent = 0; //percentage of step goal + var stepGoalBarLength = 0; //length og progress bar + var lastUpdate = new Date(); //used to reset counted steps on new day + var width = 46; //width of widget + + //used for statistics and debugging + var stepsTooShort = 0; + var stepsTooLong = 0; + var stepsOutsideTime = 0; + + var distance = 0; //distance travelled + + const s = require('Storage'); + const SETTINGS_FILE = 'activepedom.settings.json'; + const PEDOMFILE = "activepedom.steps.json"; + var dataFile; + var storeDataInterval = 5*60*1000; //ms + + let settings; + //load settings + function loadSettings() { + settings = s.readJSON(SETTINGS_FILE, 1) || {}; + } + + function storeData() { + now = new Date(); + month = now.getMonth() + 1; //month is 0-based + if (month < 10) month = "0" + month; //leading 0 + filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; //new file for each day + dataFile = s.open(filename,"a"); + if (dataFile) { //check if filen already exists + if (dataFile.getLength() == 0) { + //new day, set steps to 0 + stepsCounted = 0; + stepsTooShort = 0; + stepsTooLong = 0; + stepsOutsideTime = 0; + } + dataFile.write([ + now.getTime(), + stepsCounted, + active, + stepsTooShort, + stepsTooLong, + stepsOutsideTime, + ].join(",")+"\n"); + } + dataFile = undefined; //save memory + } + + //return setting + function setting(key) { + //define default settings + const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + 'stepLength' : 75, + }; + if (!settings) { loadSettings(); } + return (key in settings) ? settings[key] : DEFAULTS[key]; + } + + function setStepSensitivity(s) { + function sqr(x) { return x*x; } + var X=sqr(8192-s); + var Y=sqr(8192+s); + Bangle.setOptions({stepCounterThresholdLow:X,stepCounterThresholdHigh:Y}); + } + + //format number to make them shorter + function kFormatterSteps(num) { + if (num <= 999) return num; //smaller 1.000, return 600 as 600 + if (num >= 1000 && num < 10000) { //between 1.000 and 10.000 + num = Math.floor(num/100)*100; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 1600 as 1.6k + } + if (num >= 10000) { //greater 10.000 + num = Math.floor(num/1000)*1000; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 10.600 as 10k + } + } + + //Set Active to 0 + function resetActive() { + active = 0; + steps = 0; + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + } + + function calcSteps() { + stopTimeStep = new Date(); //stop time after each step + stepTimeDiff = stopTimeStep - startTimeStep; //time between steps in milliseconds + startTimeStep = new Date(); //start time again + + //Remove step if time between first and second step is too long + if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds + stepsTooLong++; //count steps which are not counted, because time too long + steps--; + } + //Remove step if time between first and second step is too short + if (stepTimeDiff <= setting('cMinTime')) { //milliseconds + stepsTooShort++; //count steps which are not counted, because time too short + steps--; + } + + //Step threshold reached + if (steps >= setting('stepThreshold')) { + if (active == 0) { + stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 + stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reach active status + } + active = 1; + clearInterval(timerResetActive); //stop timer which resets active + timerResetActive = setInterval(resetActive, setting('intervalResetActive')); //reset active after timer runs out + steps = 0; + } + + if (active == 1) { + stepsCounted++; //count steps + } + else { + stepsOutsideTime++; + } + settings = 0; //reset settings to save memory + } + + function draw() { + var height = 23; //width is deined globally + distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km + + //Check if same day + let date = new Date(); + if (lastUpdate.getDate() == date.getDate()){ //if same day + } + else { //different day, set all steps to 0 + stepsCounted = 0; + stepsTooShort = 0; + stepsTooLong = 0; + stepsOutsideTime = 0; + } + lastUpdate = date; + + g.reset(); + g.clearRect(this.x, this.y, this.x+width, this.y+height); + + //draw numbers + if (active == 1) g.setColor(0x07E0); //green + else g.setColor(0xFFFF); //white + g.setFont("6x8", 2); + + if (setting('lineOne') == 'Steps') { + g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps + } + if (setting('lineOne') == 'Distance') { + g.drawString(distance.toFixed(2),this.x+1,this.y); //first line, big number, distance + } + g.setFont("6x8", 1); + g.setColor(0xFFFF); //white + if (setting('lineTwo') == 'Steps') { + g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number, steps + } + if (setting('lineTwo') == 'Distance') { + g.drawString(distance.toFixed(3) + "km",this.x+1,this.y+14); //second line, small number, distance + } + + //draw step goal bar + stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100; + stepGoalBarLength = width / 100 * stepGoalPercent; + if (stepGoalBarLength > width) stepGoalBarLength = width; //do not draw across width of widget + g.setColor(0x7BEF); //grey + g.fillRect(this.x, this.y+height, this.x+width, this.y+height); // draw background bar + g.setColor(0xFFFF); //white + g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar + g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar + g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar + + settings = 0; //reset settings to save memory + } + + //This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off() + E.on('kill', () => { + let d = { //define array to write to file + lastUpdate : lastUpdate.toISOString(), + stepsToday : stepsCounted, + stepsTooShort : stepsTooShort, + stepsTooLong : stepsTooLong, + stepsOutsideTime : stepsOutsideTime + }; + s.write(PEDOMFILE,d); //write array to file + }); + + //When Step is registered by firmware + Bangle.on('step', (up) => { + steps++; //increase step count + calcSteps(); + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + }); + + // redraw when the LCD turns on + Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["activepedom"].draw(); + }); + + //Read data from file and set variables + let pedomData = s.readJSON(PEDOMFILE,1); + if (pedomData) { + if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); + stepsCounted = pedomData.stepsToday|0; + stepsTooShort = pedomData.stepsTooShort; + stepsTooLong = pedomData.stepsTooLong; + stepsOutsideTime = pedomData.stepsOutsideTime; + } + pedomdata = 0; //reset pedomdata to save memory + + setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) + timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly + //Add widget + WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; +})(); \ No newline at end of file diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index be3c1513c..ca92a0d97 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -3,3 +3,5 @@ 0.03: More alarm scheduling issues 0.04: Tweaks for variable size widget system 0.05: Add alarm.boot.js and move code from the bootloader +0.06: Change 'New Alarm' to 'Save', allow Deletion of Alarms +0.07: Don't overwrite existing settings on app update diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 6dd0debb1..745a7e797 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -84,15 +84,15 @@ function editAlarm(alarmIndex) { last : day, rp : repeat }; } - if (newAlarm) { - menu["> New Alarm"] = function() { - alarms.push(getAlarm()); - require("Storage").write("alarm.json",JSON.stringify(alarms)); - showMainMenu(); - }; - } else { - menu["> Save"] = function() { - alarms[alarmIndex] = getAlarm(); + menu["> Save"] = function() { + if (newAlarm) alarms.push(getAlarm()); + else alarms[alarmIndex] = getAlarm(); + require("Storage").write("alarm.json",JSON.stringify(alarms)); + showMainMenu(); + }; + if (!newAlarm) { + menu["> Delete"] = function() { + alarms.splice(alarmIndex,1); require("Storage").write("alarm.json",JSON.stringify(alarms)); showMainMenu(); }; diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 0c8adeb61..60ef5da0a 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -1 +1,2 @@ 0.01: Create astrocalc app +0.02: Store last GPS lock, can be used instead of waiting for new GPS on start diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 318147b13..6b848abda 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -1,8 +1,18 @@ /** + * BangleJS ASTROCALC + * * Inspired by: https://www.timeanddate.com + * + * Original Author: Paul Cockrell https://github.com/paulcockrell + * Created: April 2020 + * + * Calculate the Sun and Moon positions based on watch GPS and display graphically */ const SunCalc = require("suncalc.js"); +const storage = require("Storage"); +const LAST_GPS_FILE = "astrocalc.gps.json"; +let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null); function drawMoon(phase, x, y) { const moonImgFiles = [ @@ -296,22 +306,49 @@ function indexPageMenu(gps) { return E.showMenu(menu); } +function getCenterStringX(str) { + return (g.getWidth() - g.stringWidth(str)) / 2; +} + /** * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page */ function drawGPSWaitPage() { - const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")) - + const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")); + const str1 = "Astrocalc v0.02"; + const str2 = "Locating GPS"; + const str3 = "Please wait..."; + g.clear(); g.drawImage(img, 100, 50); g.setFont("6x8", 1); - g.drawString("Astrocalc v0.01", 80, 105); - g.drawString("Locating GPS", 85, 140); - g.drawString("Please wait...", 80, 155); + g.drawString(str1, getCenterStringX(str1), 105); + g.drawString(str2, getCenterStringX(str2), 140); + g.drawString(str3, getCenterStringX(str3), 155); + + if (lastGPS) { + lastGPS = JSON.parse(lastGPS); + lastGPS.time = new Date(); + + const str4 = "Press Button 3 to use last GPS"; + g.setColor("#d32e29"); + g.fillRect(0, 190, g.getWidth(), 215); + g.setColor("#ffffff"); + g.drawString(str4, getCenterStringX(str4), 200); + + setWatch(() => { + clearWatch(); + Bangle.setGPSPower(0); + m = indexPageMenu(lastGPS); + }, BTN3, {repeat: false}); + } + g.flip(); const DEBUG = false; if (DEBUG) { + clearWatch(); + const gps = { "lat": 56.45783133333, "lon": -3.02188583333, @@ -330,7 +367,10 @@ function drawGPSWaitPage() { Bangle.on('GPS', (gps) => { if (gps.fix === 0) return; + clearWatch(); + if (isNaN(gps.course)) gps.course = 0; + require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps)); Bangle.setGPSPower(0); Bangle.buzz(); Bangle.setLCDPower(true); diff --git a/apps/astroid/ChangeLog b/apps/astroid/ChangeLog new file mode 100644 index 000000000..42c1df403 --- /dev/null +++ b/apps/astroid/ChangeLog @@ -0,0 +1 @@ +0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast diff --git a/apps/astroid/asteroids.js b/apps/astroid/asteroids.js index cb44db904..da4dc017e 100644 --- a/apps/astroid/asteroids.js +++ b/apps/astroid/asteroids.js @@ -59,6 +59,7 @@ function gameStart() { function onFrame() { + "ram" var t = getTime(); var d = (lastFrame===undefined)?0:(t-lastFrame)*20; lastFrame = t; diff --git a/apps/ballmaze/README.md b/apps/ballmaze/README.md new file mode 100644 index 000000000..22a295686 --- /dev/null +++ b/apps/ballmaze/README.md @@ -0,0 +1,15 @@ +# Ball Maze + +Navigate a ball through a maze by tilting your watch. + +![Screenshot](size_select.png) +![Screenshot](maze.png) + +## Usage + +Select a maze size to begin the game. +Tilt your watch to steer the ball towards the target and advance to the next level. + +## Creator + +Richard de Boer diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js new file mode 100644 index 000000000..3e26277b7 --- /dev/null +++ b/apps/ballmaze/app.js @@ -0,0 +1,552 @@ +(() => { + let intervalID; + let settings = require("Storage").readJSON("ballmaze.json",true) || {}; + + // density, elasticity of bounces, "drag coefficient" + const rho = 100, e = 0.3, C = 0.01; + // screen width & height in pixels + const sW = 240, sH = 160; + // gravity constant (lowercase was already taken) + const G = 9.80665; + + // wall bit flags + const TOP = 1<<0, LEFT = 1<<1, BOTTOM = 1<<2, RIGHT = 1<<3, + LINKED = 1<<4; // used in maze generation + + // The play area is 240x160, sizes are the ball radius, so we can use common + // denominators of 120x80 to get square rooms + // Reverse the order to show the easiest on top of the menu + const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), + // even size 1 actually works, but larger mazes take forever to generate + minSize = 4, defaultSize = 10; + const sizeNames = { + 1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large", + 10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial", + }; + + /** + * Draw something to all screen buffers + * @param draw {function} Callback which performs the drawing + */ + function drawAll(draw) { + draw(); + g.flip(); + draw(); + g.flip(); + } + + /** + * Clear all buffers + */ + function clearAll() { + drawAll(() => g.clear()); + } + + // use unbuffered graphics for UI stuff + function showMessage(message, title) { + Bangle.setLCDMode(); + return E.showMessage(message, title); + } + + function showPrompt(prompt, options) { + Bangle.setLCDMode(); + return E.showPrompt(prompt, options); + } + + function showMenu(menu) { + Bangle.setLCDMode(); + return E.showMenu(menu); + } + + const sign = (n) => n<0?-1:1; // we don't really care about zero + + /** + * Play the game, using a ball with radius size + * @param size {number} + */ + function playMaze(size) { + const r = size; + // ball mass, weight, "drag" + // Yes, larger maze = larger ball = heavier ball + // (atm our physics is so oversimplified that mass cancels out though) + const m = rho*(r*r*r), w = G*m, d = C*w; + + // number of columns/rows + const cols = Math.round(sW/(r*2.5)), + rows = Math.round(sH/(r*2.5)); + // width & height of one column/row in pixels + const cW = sW/cols, rH = sH/rows; + + // list of rooms, every room can have one or more wall bits set + // actual layout: 0 1 2 + // 3 4 5 + // this means that for room with index "i": (except edge cases!) + // i-1 = room to the left + // i+1 = room to the right + // i-cols = room above + // i+cols = room below + let rooms = new Uint8Array(rows*cols); + // shortest route from start to finish + let route; + + let x, y, // current position + px, py, ppx, ppy, // previous positions (for erasing old image) + vx, vy; // velocity + + function start() { + // start in top left corner + x = cW/2; + y = rH/2; + vx = vy = 0; + ppx = px = x; + ppy = py = y; + + generateMaze(); // this shows unbuffered progress messages + if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-( + + Bangle.setLCDMode("doublebuffered"); + clearAll(); + drawAll(drawMaze); + intervalID = setInterval(tick, 100); + } + + // Position conversions + // index: index of room in rooms[] + // rowcol: position measured in roomsizes + // xy: position measured in pixels + /** + * Index from RowCol + * @param row {number} + * @param col {number} + * @returns {number} rooms[] index + */ + function iFromRC(row, col) { + return row*cols+col; + } + + /** + * RowCol from index + * @param index {number} + * @returns {(number)[]} [row,column] + */ + function rcFromI(index) { + return [ + Math.floor(index/cols), + index%cols, + ]; + } + + /** + * RowCol from Xy + * @param x {number} + * @param y {number} + * @returns {(number)[]} [row,column] + */ + function rcFromXy(x, y) { + return [ + Math.floor(y/sH*rows), + Math.floor(x/sW*cols), + ]; + } + + /** + * Link another room up + * @param index {number} Dig from already linked room with this index + * @param dir {number} in this direction + * @return {number} index of room we just linked up + */ + function dig(index, dir) { + rooms[index] &= ~dir; + let neighbour; + switch(dir) { + case LEFT: + neighbour = index-1; + rooms[neighbour] &= ~RIGHT; + break; + case RIGHT: + neighbour = index+1; + rooms[neighbour] &= ~LEFT; + break; + case TOP: + neighbour = index-cols; + rooms[neighbour] &= ~BOTTOM; + break; + case BOTTOM: + neighbour = index+cols; + rooms[neighbour] &= ~TOP; + break; + } + rooms[neighbour] |= LINKED; + return neighbour; + } + + /** + * Generate the maze + */ + function generateMaze() { + // Maze generation basically works like this: + // 1. Start with all rooms set to completely walled off and "unlinked" + // 2. Then mark a room as "linked", and add it to the "to do" list + // 3. When the "to do" list is empty, we're done + // 4. pick a random room from the list + // 5. if all adjacent rooms are linked -> remove room from list, goto 3 + // 6. pick a random unlinked adjacent room + // 7. remove the walls between the rooms + // 8. mark the adjacent room as linked and add it to the "to do" list + // 9. go to 4 + let pdotnum = 0; + const title = "Please wait", + message = "Generating maze\n", + showProgress = (done, total) => { + const dotnum = Math.floor(done/total*10); + if (dotnum>pdotnum) { + const dots = ".".repeat(dotnum)+" ".repeat(10-dotnum); + showMessage(message+dots, title); + pdotnum = dotnum; + } + }; + showProgress(0, 100); + // start with all rooms completely walled off + rooms.fill(TOP|LEFT|BOTTOM|RIGHT); + const + // is room at row,col already linked? + linked = (row, col) => !!(rooms[iFromRC(row, col)]&LINKED), + // pick random array element + pickRandom = (arr) => arr[Math.floor(Math.random()*arr.length)]; + // starting with top-right room seems to generate more interesting mazes + rooms[cols] |= LINKED; + let todo = [cols], done = 1; + while(todo.length) { + const index = pickRandom(todo); + const rc = rcFromI(index), + row = rc[0], col = rc[1]; + let sides = []; + if ((col>0) && !linked(row, col-1)) sides.push(LEFT); + if ((col0) && !linked(row-1, col)) sides.push(TOP); + if ((row0 && !(walls&LEFT) && dist[i-1]>d+1) { + dist[i-1] = d+1; + todo.push(i-1); + } + if (row>0 && !(walls&TOP) && dist[i-cols]>d+1) { + dist[i-cols] = d+1; + todo.push(i-cols); + } + if (cold+1) { + dist[i+1] = d+1; + todo.push(i+1); + } + if (rowd+1) { + dist[i+cols] = d+1; + todo.push(i+cols); + } + } + + route = [rooms.length-1]; + while(true) { + const i = route[0], d = dist[i], walls = rooms[i], + rc = rcFromI(i), + row = rc[0], col = rc[1]; + if (i===0) { break; } + if (col0 && !(walls&TOP) && dist[i-cols]0 && !(walls&LEFT) && dist[i-1] { + const rc = rcFromI(i), + row = rc[0], col = rc[1], + x = (col+0.5)*cW, y = (row+0.5)*rH; + g.lineTo(x, y); + }); + } + + /** + * Move the ball + */ + function move() { + const a = Bangle.getAccel(); + const fx = (-a.x*w)-(sign(vx)*d*a.z), fy = (-a.y*w)-(sign(vy)*d*a.z); + vx += fx/m; + vy += fy/m; + const s = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy))); + for(let n = s; n>0; n--) { + x += vx/s; + y += vy/s; + bounce(); + } + if (x>sW-cW && y>sH-rH) win(); + } + + /** + * Check whether we hit any walls, and if so: Bounce. + * + * Bounce = reverse velocity in bounce direction, multiply with elasticity + * Also apply drag in perpendicular direction ("friction with the wall") + */ + function bounce() { + const row = Math.floor(y/sH*rows), col = Math.floor(x/sW*cols), + i = row*cols+col, walls = rooms[i]; + const left = col*cW, + right = (col+1)*cW, + top = row*rH, + bottom = (row+1)*rH; + let bounced = false; + if (vx<0) { + if ((walls&LEFT) && x<=left+r) { + x += (1+e)*(left+r-x); + const fy = sign(vy)*d*Math.abs(vx); + vy -= fy/m; + vx = -vx*e; + bounced = true; + } + } else { + if ((walls&RIGHT) && x>=right-r) { + x -= (1+e)*(x+r-right); + const fy = sign(vy)*d*Math.abs(vx); + vy -= fy/m; + vx = -vx*e; + bounced = true; + } + } + if (vy<0) { + if ((walls&TOP) && y<=top+r) { + y += (1+e)*(top+r-y); + const fx = sign(vx)*d*Math.abs(vy); + vx -= fx/m; + vy = -vy*e; + bounced = true; + } + } else { + if ((walls&BOTTOM) && y>=bottom-r) { + y -= (1+e)*(y+r-bottom); + const fx = sign(vx)*d*Math.abs(vy); + vx -= fx/m; + vy = -vy*e; + bounced = true; + } + } + if (bounced) return; + let cx, cy; + if ((rooms[i-1]&TOP) || rooms[i-cols]&LEFT) { + if ((x-left)*(x-left)+(y-top)*(y-top)<=r*r) { + cx = left; + cy = top; + } + } + else if ((rooms[i-1]&BOTTOM) || rooms[i+cols]&LEFT) { + if ((x-left)*(x-left)+(bottom-y)*(bottom-y)<=r*r) { + cx = left; + cy = bottom; + } + } + else if ((rooms[i+1]&TOP) || rooms[i-cols]&RIGHT) { + if ((right-x)*(right-x)+(y-top)*(y-top)<=r*r) { + cx = right; + cy = top; + } + } + else if ((rooms[i+1]&BOTTOM) || rooms[i+cols]&RIGHT) { + if ((right-x)*(right-x)+(bottom-y)*(bottom-y)<=r*r) { + cx = right; + cy = bottom; + } + } + if (!cx) return; + let nx = x-cx, ny = y-cy; + const l = Math.sqrt(nx*nx+ny*ny); + nx /= l; + ny /= l; + const p = vx*nx+vy*ny; + vx -= 2*p*nx*e; + vy -= 2*p*ny*e; + } + + /** + * You reached the bottom-right corner, you win! + */ + function win() { + clearInterval(intervalID); + Bangle.buzz().then(askAgain); + } + + /** + * You solved the maze, try the next one? + */ + function askAgain() { + const nextLevel = (size>minSize)?"next level":"again"; + const nextSize = (size>minSize)?sizes[sizes.indexOf(size)+1]:size; + showPrompt(`Well done!\n\nPlay ${nextLevel}?`, + {"title": "Congratulations!"}) + .then(function(again) { + if (again) { + playMaze(nextSize); + } else { + startGame(); + } + }); + } + + function tick() { + ppx = px; + ppy = py; + px = x; + py = y; + move(); + drawUpdate(); + } + + start(); + } + + /** + * Ask player what size maze they would like to play + */ + function startGame() { + let menu = { + "": { + title: "Select Maze Size", + selected: sizes.indexOf(settings.size || defaultSize), + }, + }; + sizes.filter(s => s>=minSize).forEach(size => { + let name = sizeNames[size]; + if (size { + // remember chosen size + settings.size = size; + require("Storage").write("ballmaze.json", settings); + playMaze(size); + }; + }); + menu["< Exit"] = () => load(); + showMenu(menu); + } + + startGame(); +})(); diff --git a/apps/ballmaze/icon.js b/apps/ballmaze/icon.js new file mode 100644 index 000000000..10b5a502e --- /dev/null +++ b/apps/ballmaze/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA")) diff --git a/apps/ballmaze/icon.png b/apps/ballmaze/icon.png new file mode 100644 index 000000000..44697db4b Binary files /dev/null and b/apps/ballmaze/icon.png differ diff --git a/apps/ballmaze/maze.png b/apps/ballmaze/maze.png new file mode 100644 index 000000000..7bda56d9b Binary files /dev/null and b/apps/ballmaze/maze.png differ diff --git a/apps/ballmaze/size_select.png b/apps/ballmaze/size_select.png new file mode 100644 index 000000000..cac278820 Binary files /dev/null and b/apps/ballmaze/size_select.png differ diff --git a/apps/banglerun/ChangeLog b/apps/banglerun/ChangeLog new file mode 100755 index 000000000..7b83706bf --- /dev/null +++ b/apps/banglerun/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/banglerun/app-icon.js b/apps/banglerun/app-icon.js new file mode 100644 index 000000000..0ccbedab4 --- /dev/null +++ b/apps/banglerun/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwMB/4ACx4ED/0DApP8AqAXB84GDg/DAgXj/+DCAUABgIFB4EAv4FCwEAj0PAoJPBgwFEgEfDgMOAoM/AoMegFAAoP8jkA8F/AoM8gP4DgP4nBvD/F4KQfwuAFE+A/CAoPgAofx8A/CKYRwELIIFDLII6BAoZSBLIYeC/0BwAFDgfAGAQFBHgf8g4BBIIUH/wFBSYMPAoXwAog/Bj4FEv4FDDQQCBQoQFCZYYFi/6KE/+P/4A=")) diff --git a/apps/banglerun/app.js b/apps/banglerun/app.js new file mode 100644 index 000000000..fc21e3627 --- /dev/null +++ b/apps/banglerun/app.js @@ -0,0 +1,314 @@ +/** Global constants */ +const DEG_TO_RAD = Math.PI / 180; +const EARTH_RADIUS = 6371008.8; + +/** Utilities for handling vectors */ +class Vector { + static magnitude(a) { + let sum = 0; + for (const key of Object.keys(a)) { + sum += a[key] * a[key]; + } + return Math.sqrt(sum); + } + + static add(a, b) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] + b[key]; + } + return result; + } + + static sub(a, b) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] - b[key]; + } + return result; + } + + static multiplyScalar(a, x) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] * x; + } + return result; + } + + static divideScalar(a, x) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] / x; + } + return result; + } +} + +/** Interquartile range filter, to detect outliers */ +class IqrFilter { + constructor(size, threshold) { + const q = Math.floor(size / 4); + this._buffer = []; + this._size = 4 * q + 2; + this._i1 = q; + this._i3 = 3 * q + 1; + this._threshold = threshold; + } + + isReady() { + return this._buffer.length === this._size; + } + + isOutlier(point) { + let result = true; + if (this._buffer.length === this._size) { + result = false; + for (const key of Object.keys(point)) { + const data = this._buffer.map(item => item[key]); + data.sort((a, b) => (a - b) / Math.abs(a - b)); + const q1 = data[this._i1]; + const q3 = data[this._i3]; + const iqr = q3 - q1; + const lower = q1 - this._threshold * iqr; + const upper = q3 + this._threshold * iqr; + if (point[key] < lower || point[key] > upper) { + result = true; + break; + } + } + } + this._buffer.push(point); + this._buffer = this._buffer.slice(-this._size); + return result; + } +} + +/** Process GPS data */ +class Gps { + constructor() { + this._lastCall = Date.now(); + this._lastValid = 0; + this._coords = null; + this._filter = new IqrFilter(10, 1.5); + this._shift = { x: 0, y: 0, z: 0 }; + } + + isReady() { + return this._filter.isReady(); + } + + getDistance(gps) { + const time = Date.now(); + const interval = (time - this._lastCall) / 1000; + this._lastCall = time; + + if (!gps.fix) { + return { t: interval, d: 0 }; + } + + const p = gps.lat * DEG_TO_RAD; + const q = gps.lon * DEG_TO_RAD; + const coords = { + x: EARTH_RADIUS * Math.sin(p) * Math.cos(q), + y: EARTH_RADIUS * Math.sin(p) * Math.sin(q), + z: EARTH_RADIUS * Math.cos(p), + }; + + if (!this._coords) { + this._coords = coords; + this._lastValid = time; + return { t: interval, d: 0 }; + } + + const ds = Vector.sub(coords, this._coords); + const dt = (time - this._lastValid) / 1000; + const v = Vector.divideScalar(ds, dt); + + if (this._filter.isOutlier(v)) { + return { t: interval, d: 0 }; + } + + this._shift = Vector.add(this._shift, ds); + const length = Vector.magnitude(this._shift); + const remainder = length % 10; + const distance = length - remainder; + + this._coords = coords; + this._lastValid = time; + if (distance > 0) { + this._shift = Vector.multiplyScalar(this._shift, remainder / length); + } + + return { t: interval, d: distance }; + } +} + +/** Process step counter data */ +class Step { + constructor(size) { + this._buffer = []; + this._size = size; + } + + getCadence() { + this._buffer.push(Date.now() / 1000); + this._buffer = this._buffer.slice(-this._size); + const interval = this._buffer[this._buffer.length - 1] - this._buffer[0]; + return interval ? Math.round(60 * (this._buffer.length - 1) / interval) : 0; + } +} + +const gps = new Gps(); +const step = new Step(10); + +let totDist = 0; +let totTime = 0; +let totSteps = 0; + +let speed = 0; +let cadence = 0; +let heartRate = 0; + +let gpsReady = false; +let hrmReady = false; +let running = false; + +function formatClock(date) { + return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2); +} + +function formatDistance(m) { + return ('0' + (m / 1000).toFixed(2) + ' km').substr(-7); +} + +function formatTime(s) { + const hrs = Math.floor(s / 3600); + const min = Math.floor(s / 60); + const sec = Math.floor(s % 60); + return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2); +} + +function formatSpeed(kmh) { + if (kmh <= 0.6) { + return `__'__"`; + } + const skm = 3600 / kmh; + const min = Math.floor(skm / 60); + const sec = Math.floor(skm % 60); + return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`; +} + +function drawBackground() { + g.setColor(running ? 0x00E0 : 0x0000); + g.fillRect(0, 30, 240, 240); + + g.setColor(0xFFFF); + g.setFontAlign(0, -1, 0); + g.setFont('6x8', 2); + + g.drawString('DISTANCE', 120, 50); + g.drawString('TIME', 60, 100); + g.drawString('PACE', 180, 100); + g.drawString('STEPS', 60, 150); + g.drawString('STP/m', 180, 150); + g.drawString('SPEED', 40, 200); + g.drawString('HEART', 120, 200); + g.drawString('CADENCE', 200, 200); +} + +function draw() { + const totSpeed = totTime ? 3.6 * totDist / totTime : 0; + const totCadence = totTime ? Math.round(60 * totSteps / totTime) : 0; + + g.setColor(running ? 0x00E0 : 0x0000); + g.fillRect(0, 30, 240, 50); + g.fillRect(0, 70, 240, 100); + g.fillRect(0, 120, 240, 150); + g.fillRect(0, 170, 240, 200); + g.fillRect(0, 220, 240, 240); + + g.setFont('6x8', 2); + + g.setFontAlign(-1, -1, 0); + g.setColor(gpsReady ? 0x07E0 : 0xF800); + g.drawString(' GPS', 6, 30); + + g.setFontAlign(1, -1, 0); + g.setColor(0xFFFF); + g.drawString(formatClock(new Date()), 234, 30); + + g.setFontAlign(0, -1, 0); + g.setFontVector(20); + g.drawString(formatDistance(totDist), 120, 70); + g.drawString(formatTime(totTime), 60, 120); + g.drawString(formatSpeed(totSpeed), 180, 120); + g.drawString(totSteps, 60, 170); + g.drawString(totCadence, 180, 170); + + g.setFont('6x8', 2); + g.drawString(formatSpeed(speed), 40, 220); + + g.setColor(hrmReady ? 0x07E0 : 0xF800); + g.drawString(heartRate, 120, 220); + + g.setColor(0xFFFF); + g.drawString(cadence, 200, 220); +} + +function handleGps(coords) { + const step = gps.getDistance(coords); + gpsReady = coords.fix > 0 && gps.isReady(); + speed = isFinite(gps.speed) ? gps.speed : 0; + if (running) { + totDist += step.d; + totTime += step.t; + } +} + +function handleHrm(hrm) { + hrmReady = hrm.confidence > 50; + heartRate = hrm.bpm; +} + +function handleStep() { + cadence = step.getCadence(); + if (running) { + totSteps += 1; + } +} + +function start() { + running = true; + drawBackground(); + draw(); +} + +function stop() { + if (!running) { + totDist = 0; + totTime = 0; + totSteps = 0; + } + running = false; + drawBackground(); + draw(); +} + +Bangle.on('GPS', handleGps); +Bangle.on('HRM', handleHrm); +Bangle.on('step', handleStep); + +Bangle.setGPSPower(1); +Bangle.setHRMPower(1); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawBackground(); +draw(); + +setInterval(draw, 500); + +setWatch(start, BTN1, { repeat: true }); +setWatch(stop, BTN3, { repeat: true }); diff --git a/apps/banglerun/banglerun.png b/apps/banglerun/banglerun.png new file mode 100644 index 000000000..bf2cd8af3 Binary files /dev/null and b/apps/banglerun/banglerun.png differ diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index 2e0fd088c..616ee66e9 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -2,3 +2,4 @@ 0.02: Apply locale, 12-hour setting 0.03: Fix dates drawing over each other at midnight 0.04: Small bugfix +0.05: Clock does not start if app Languages is not installed \ No newline at end of file diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index da436daee..0f2609298 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -12,7 +12,12 @@ date.setMonth(1, 3) // februari: months are zero-indexed const localized = locale.date(date, true) locale.dayFirst = /3.*2/.test(localized) - locale.hasMeridian = (locale.meridian(date) !== '') + + locale.hasMeridian = false + if(typeof locale.meridian === 'function') { // function does not exists if languages app is not installed + locale.hasMeridian = (locale.meridian(date) !== '') + } + } const screen = { width: g.getWidth(), diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog new file mode 100644 index 000000000..31c386684 --- /dev/null +++ b/apps/batchart/ChangeLog @@ -0,0 +1,10 @@ +0.01: New app and widget +0.02: Widget stores data to file (1 dataset/10min) +0.03: Rotate log files once a week. +0.04: chart in the app is now active. +0.05: Display temperature and LCD state in chart +0.06: Fixes widget events and charting of component states +0.07: Improve logging and charting of component states and add widget icon +0.08: Fix for Home button in the app and README added. +0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state +0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381) \ No newline at end of file diff --git a/apps/batchart/README.md b/apps/batchart/README.md new file mode 100644 index 000000000..0ce2c646d --- /dev/null +++ b/apps/batchart/README.md @@ -0,0 +1,67 @@ +# Summary + +Battery Chart contains a widget that records the battery usage as well as information that might influence this usage. + +The app that comes with provides a graph that accumulates this information in a single screen. + +## How the widget works + +The widget records data in a fixed interval of ten minutes. + +When this timespan has passed, it saves the following information to a file called `bclogx` where `x` is + +the current day retrieved by `new Date().getDay()`: + +- Battery percentage +- Temperature (of the die) +- LCD state +- Compass state +- HRM state +- GPS state + +After seven days the logging rolls over and the previous data is overwritten. + +To properly handle the roll-over, the day of the previous logging operation is stored in `bcprvday`. + +The value is changed with the first recording operation of the new day. + +## How the App works + +### Events + +The app charts the last 144 (6/h * 24h) datapoints that have been recorded. + +If for the current day the 144 events have not been reached the list is padded with + +events from the previous `bclog` file(s). + +### Graph + +The graph then contains the battery percentage (left y-axis) and the temperature (right y-axis). + +In case the recorded temperature is outside the limits of the graph, the value is set to a minimum of 19 or a maximum of 41 and thus should be clearly visible outside of the graph's boundaries for the y-axis. + +The states of the various SoC devices are depicted below the graph. If at the time of recording the device was enabled a marker in the respective color is set, if not the pixels for this point in time stay black. + +If a device was not enabled during the 144 selected events, the name is not displayed. + +## File schema + +You can download the `bclog` files for your own analysis. They are `CSV` files without header rows and contain + +``` +timestamp,batteryPercentage,temperatureInDegreeC,deviceStates +``` + +with the `deviceStates` resembling a flag set consisting of + +``` +const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 +}; +``` diff --git a/apps/batchart/app-icon.js b/apps/batchart/app-icon.js new file mode 100644 index 000000000..b41629e01 --- /dev/null +++ b/apps/batchart/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AS64AIF/4pZABYuuGDIv/F/4v/F9+Gw0rAQIASF7YxTF7cxwAvtrdVF9qQTF/4vMYCQvcYCQvcSCQvdqpgQF7oEBYJ4veAoNbF9uGmMrrgvsw2AGILFKF8IACrYxJF8gxDSowvmBwWAF9oPGF9NbmIvtCAovqMAgvqCIgvrrdVF9oSDF9iPuF7crACxf/F++wFqmG2AvXGCouZAH4A/AGY")) diff --git a/apps/batchart/app.js b/apps/batchart/app.js new file mode 100644 index 000000000..472fb3a8a --- /dev/null +++ b/apps/batchart/app.js @@ -0,0 +1,246 @@ +const GraphXZero = 40; +const GraphYZero = 180; +const GraphY100 = 80; + +const GraphMarkerOffset = 5; +const MaxValueCount = 144; +const GraphXMax = GraphXZero + MaxValueCount; + +const GraphLcdY = GraphYZero + 10; +const GraphCompassY = GraphYZero + 16; +const GraphBluetoothY = GraphYZero + 22; +const GraphGpsY = GraphYZero + 28; +const GraphHrmY = GraphYZero + 34; + +const Storage = require("Storage"); + +function renderCoordinateSystem() { + g.setFont("6x8", 1); + + // Left Y axis (Battery) + g.setColor(1, 1, 0); + g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); + g.drawString("%", 39, GraphY100 - 10); + + g.setFontAlign(1, -1, 0); + g.drawString("100", 30, GraphY100 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100); + + g.drawString("50", 30, GraphYZero - 50 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130); + + g.drawString("0", 30, GraphYZero - GraphMarkerOffset); + + g.setColor(1,1,1); + g.setFontAlign(1, -1, 0); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero); + + // Right Y axis (Temperature) + g.setColor(0.4, 0.4, 1); + g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100); + g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10); + g.setFontAlign(-1, -1, 0); + g.drawString("20", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 130, GraphXMax, 130); + g.drawString("30", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - 50 - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 80, GraphXMax, 80); + g.drawString("40", GraphXMax + 2 * GraphMarkerOffset, GraphY100 - GraphMarkerOffset); + + g.setColor(1,1,1); +} + +function decrementDay(dayToDecrement) { + return dayToDecrement === 0 ? 6 : dayToDecrement-1; +} + +function loadData() { + const startingDay = new Date().getDay(); + + // Load data for the current day + let logFileName = "bclog" + startingDay; + + let dataLines = loadLinesFromFile(MaxValueCount, logFileName); + + // Top up to MaxValueCount from previous days as required + let previousDay = decrementDay(startingDay); + while (dataLines.length < MaxValueCount && previousDay !== startingDay) { + let topUpLogFileName = "bclog" + previousDay; + let remainingLines = MaxValueCount - dataLines.length; + let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); + + if(topUpLines) { + dataLines = topUpLines.concat(dataLines); + } + + previousDay = decrementDay(previousDay); + } + + return dataLines; +} + +function loadLinesFromFile(requestedLineCount, fileName) { + let allLines = []; + let returnLines = []; + + var readFile = Storage.open(fileName, "r"); + + while ((nextLine = readFile.readLine())) { + if(nextLine) { + allLines.push(nextLine); + } + } + + readFile = null; + + if (allLines.length <= 0) return; + + let linesToReadCount = Math.min(requestedLineCount, allLines.length); + let startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1); + + for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) { + if(allLines[i]) { + returnLines.push(allLines[i]); + } + } + + allLines = null; + + return returnLines; +} + +function renderData(dataArray) { + const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 + }; + + //const timestampIndex = 0; + const batteryIndex = 1; + const temperatureIndex = 2; + const switchabelsIndex = 3; + + const minTemperature = 20; + const maxTemparature = 40; + + const belowMinIndicatorValue = minTemperature - 1; + const aboveMaxIndicatorValue = maxTemparature + 1; + + var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; + + for (let i = 0; i < dataArray.length; i++) { + const element = dataArray[i]; + + var dataInfo = element.split(","); + + // Battery percentage + g.setColor(1, 1, 0); + g.setPixel(GraphXZero + i, GraphYZero - parseInt(dataInfo[batteryIndex])); + + // Temperature + g.setColor(0.4, 0.4, 1); + + let datapointTemp = parseFloat(dataInfo[temperatureIndex]); + + if (datapointTemp < minTemperature) { + datapointTemp = belowMinIndicatorValue; + } + if (datapointTemp > maxTemparature) { + datapointTemp = aboveMaxIndicatorValue; + } + + // Scale down the range of 20 - 40°C to a 100px y-axis, where 1px = .25° + let scaledTemp = Math.floor(((datapointTemp * 100) - 2000) / 20) + ((((datapointTemp * 100) - 2000) % 100) / 25); + + g.setPixel(GraphXZero + i, GraphYZero - scaledTemp); + + // LCD state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd) { + g.setColor(1, 1, 1); + g.setFontAlign(1, -1, 0); + g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true); + g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1); + } + + // Compass state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.compass) { + g.setColor(0, 1, 0); + g.setFontAlign(-1, -1, 0); + g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); + g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); + } + + // Bluetooth state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.bluetooth) { + g.setColor(0, 0, 1); + g.setFontAlign(1, -1, 0); + g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true); + g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); + } + + // Gps state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) { + g.setColor(0.8, 0.5, 0.24); + g.setFontAlign(-1, -1, 0); + g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); + g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1); + } + + // Hrm state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.hrm) { + g.setColor(1, 0, 0); + g.setFontAlign(1, -1, 0); + g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); + g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1); + } + } + + dataArray = null; +} + +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + +function renderBatteryChart() { + renderCoordinateSystem(); + let data = loadData(); + renderData(data); + data = null; +} + +// Show launcher when middle button pressed +function switchOffApp(){ + Bangle.showLauncher(); +} + +// special function to handle display switch on +Bangle.on('lcdPower', (on) => { + if (on) { + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + renderBatteryChart(); + } +}); + +setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +renderHomeIcon(); + +renderBatteryChart(); diff --git a/apps/batchart/app.png b/apps/batchart/app.png new file mode 100644 index 000000000..9a60d1004 Binary files /dev/null and b/apps/batchart/app.png differ diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js new file mode 100644 index 000000000..4a116c990 --- /dev/null +++ b/apps/batchart/widget.js @@ -0,0 +1,124 @@ +(() => { + let recordingInterval = null; + const Storage = require("Storage"); + + const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 + }; + + var batChartFile; // file for battery percentage recording + const recordingInterval10Min = 60 * 10 * 1000; + const recordingInterval1Min = 60 * 1000; //For testing + const recordingInterval10S = 10 * 1000; //For testing + + var compassEventReceived = false; + var gpsEventReceived = false; + var hrmEventReceived = false; + + function draw() { + // void + } + + function batteryChartOnMag() { + compassEventReceived = true; + // Stop handling events when no longer necessarry + Bangle.removeListener("mag", batteryChartOnMag); + } + + function batterChartOnGps() { + gpsEventReceived = true; + Bangle.removeListener("GPS", batterChartOnGps); + } + + function batteryChartOnHrm() { + hrmEventReceived = true; + Bangle.removeListener("HRM", batteryChartOnHrm); + } + + function getEnabledConsumersValue() { + // Wait for an event from each of the devices to see if they are switched on + var enabledConsumers = switchableConsumers.none; + + Bangle.on('mag', batteryChartOnMag); + Bangle.on('GPS', batterChartOnGps); + Bangle.on('HRM', batteryChartOnHrm); + + // Wait two seconds, that should be enough for each of the events to get raised once + setTimeout(() => { + Bangle.removeListener('mag', batteryChartOnMag); + Bangle.removeListener('GPS', batterChartOnGps); + Bangle.removeListener('HRM', batteryChartOnHrm); + }, 2000); + + if (Bangle.isLCDOn()) + enabledConsumers = enabledConsumers | switchableConsumers.lcd; + if (compassEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.compass; + if (gpsEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.gps; + if (hrmEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.hrm; + + // Very coarse first approach to check if the BLE device is on. + if (NRF.getSecurityStatus().connected) + enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; + + // Reset the event registration vars + compassEventReceived = false; + gpsEventReceived = false; + hrmEventReceived = false; + + return enabledConsumers.toString(); + } + + function logBatteryData() { + const previousWriteLogName = "bcprvday"; + const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine()); + const currentWriteDay = new Date().getDay(); + + const logFileName = "bclog" + currentWriteDay; + + // Change log target on day change + if (!isNaN(previousWriteDay) && previousWriteDay != currentWriteDay) { + //Remove a log file containing data from a week ago + Storage.open(logFileName, "r").erase(); + Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay)); + } + + var bcLogFileA = Storage.open(logFileName, "a"); + if (bcLogFileA) { + let logTime = getTime().toFixed(0); + let logPercent = E.getBattery(); + let logTemperature = E.getTemperature(); + let logConsumers = getEnabledConsumersValue(); + + let logString = [logTime, logPercent, logTemperature, logConsumers].join(","); + + bcLogFileA.write(logString + "\n"); + } + } + + function reload() { + console.log("Reloading BatteryChart widget"); + WIDGETS["batchart"].width = 0; + + if (recordingInterval) { + clearInterval(recordingInterval); + recordingInterval = null; + } + + recordingInterval = setInterval(logBatteryData, recordingInterval10Min); + } + + // add the widget + WIDGETS["batchart"] = { + area: "tl", width: 0, draw: draw, reload: reload + }; + + reload(); +})(); \ No newline at end of file diff --git a/apps/beebclock/ChangeLog b/apps/beebclock/ChangeLog new file mode 100644 index 000000000..14dd12220 --- /dev/null +++ b/apps/beebclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial commit. Not very efficient, and widgets not working for some reason. +0.02: Fixes; widget support diff --git a/apps/beebclock/beebclock-icon.js b/apps/beebclock/beebclock-icon.js new file mode 100644 index 000000000..b4d173068 --- /dev/null +++ b/apps/beebclock/beebclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkHA4dQAgcFgPyl8QDxgNE0EAggXGAgcFDQ0TBgQXBkcgBQURCw8GBYUj+AEBI430AgI7BBAVgCIU/if0DoMC+UfiwLBgUyEQRyGgEzmPzCIQvCBwMPj4rCAwJECAAUD+MvkEQgMhkRABgEvkaKIJAXzj49BBYMBBIOm+IIBgMVPQxiBn8xkAIDAYMBj6TBSIYyFhUPBoJRCF4RlBAoJRBBggSBIIgAI0qhCFgUB/4WFTIYDDFwJMCCAUSifzDoYsGBQJIBfoM0kIEBn81168CfAwACKwMS+UT+ovC/8gmRRCGQqQBRgUjocyB4YYBI4QrEDwRdCfAQ4EsD5DAA5dCDYbMDCoTPCBAsQaYprHRosR0ICBB4ZtDEYJZHM4X/kMKFAwSGAocBn8hkX/NBMFEAJXDAQMD+IcBkcwBIZ1EHgP/BgIzD17QBDYPwI4kCn8/mcjkUyCAQlCVocB+IqDC4IVBmYWBkVAVAkvaIboDqAGBCwMyIwM/I4IVBoYHBI4qzDI4egLYURiaiCO4UAl4bCMIJLEEAUj//zlVQgynBmNC/5LBcQsA0BXBCoNCeQkDKQX1X5Ef+clTQIkCT4URiJYBXwYlEirHGOAkAJYIvHEAUTNoadBegn/EYUCB4IjDiRtCCoWgEwj8BCQMCCAQkBAoMhkZJDC4kFh/yNAMyifzE4U/kMf+RRGM4beCp/xibLBqERj6EDboQjCT4beDmMhQwRNEQIoACiISCIYILCgKNCXgQXFGYoTBC4a/DgcmBoRLCEAMDCQQPBbwxVBmLDDGwUCj/wHY4ADn8TBwbYD+3xCY8AhQlB+M/JwS3BGIXzj5RENAS1Cj86YQUB+U/KIdvmB6FIw8Qg3yl5KCgcyMAgZFiNPOwYXDAoURL45LCiSLD+YXBAoTXDAAbTIL4oJCCIRdEDA1gI4ooFgAA==")) diff --git a/apps/beebclock/beebclock.js b/apps/beebclock/beebclock.js new file mode 100644 index 000000000..7b071c22d --- /dev/null +++ b/apps/beebclock/beebclock.js @@ -0,0 +1,397 @@ +/* jshint esversion: 6 */ +// Beebclock +// © 2020, Tom Gidden +// https://github.com/tomgidden + +const storage = require("Storage"); +const filename = 'beebjson'; + +require('FontTeletext10x18Ascii').add(Graphics); + +// Double height text +Graphics.prototype.drawStringDH = function (txt, px, py, align, gw) { + let g2 = Graphics.createArrayBuffer(gw,18,1,{msb:true}); + g2.setFontTeletext10x18Ascii(); + let w = g2.stringWidth(txt); + let c = (w+3)>>2; + g2.drawString(txt); + let img = {width:w,height:1,transparent:0,buffer:new ArrayBuffer(c)}; + let a = new Uint8Array(img.buffer); + + let x; + switch (align) { + case 'C': x = px + (gw - w)/2; break; + case 'R': x = gw - w + px; break; + default: x = px; + } + + for (var y=0;y<18;y++) { + a.set(new Uint8Array(g2.buffer,gw*y/8,c)); + this.drawImage(img,x,py+y*2); + this.drawImage(img,x,py+1+y*2); + } +}; + +// Fill rectangle rotated around the centre +Graphics.prototype.fillRotRect = function (sina, cosa, cx, cy, x0, x1, y0, y1) { + let fn = Math.ceil; + return this.fillPoly([ + fn(cx - x0*cosa + y0*sina), fn(cy - x0*sina - y0*cosa), + fn(cx - x1*cosa + y0*sina), fn(cy - x1*sina - y0*cosa), + fn(cx - x1*cosa + y1*sina), fn(cy - x1*sina - y1*cosa), + fn(cx - x0*cosa + y1*sina), fn(cy - x0*sina - y1*cosa) + ]); +}; + +// Draw a line from r1,a to r2,a relative to cx+cy +Graphics.prototype.drawRotLine = function (sina, cosa, cx, cy, r1, r2) { + return this.drawLine( + cx + r1*sina, cy - r1*cosa, + cx + r2*sina, cy - r2*cosa + ); +}; + + +(function(g) { + // Display modes + // + // 0: full-screen + // 1: with widgets + // 2: centred on Bangle (v.1), no widgets or time/date + // 3: centred with time above + // 4: centred with date above + // 5: centred with time and date above + let mode; + + // R1, R2: Outer and inner radii of hour marks + // RC1, RC2: Outer and inner radii of hub + // CX, CY: Centre location, relative to buffer (not screen, necessarily) + // HW2, MW2: Half-width of hour and minute hand + // HR, MR: Length of hour and minute hand, relative to CX,CY + // M: Half-width of gap in hour marks + // HSCALE: Half-width of hour mark as function(0 { + const fw = R1 * 2; + const fh = R1 * 2; + const fw2 = R1; + const fh2 = R1; + let hs = []; + + // Wipe the image and start with white + G.clear(); + G.setColor(1,1,1); + + // Draw the hour marks. + for (let h=1; h<=12; h++) { + hs[h] = HSCALE(h); + G.fillRotRect(ss[h], cs[h], CX, CY, -hs[h], hs[h], R2, R1); + + } + + // Draw the hub + G.fillCircle(CX, CY, RC1); + + // Black + G.setColor(0,0,0); + + // Clear the centre of the hub + G.fillCircle(CX, CY, RC2); + + // Draw the gap in the hour marks + for (let h=1; h<=12; h++) { + G.fillRotRect(ss[h], cs[h], CX, CY, -M, M, R2-1, R1+1); + } + + // Back to white for future draw operations + G.setColor(1,1,1); + + // While the buffer remains full-screen, we may trim out the + // bottom of the image so we can shift the whole thing down for + // widgets. + const img = {width:GW,height:GH-TM,buffer:G.buffer}; + return img; + }; + + let hours, minutes, seconds, date; + + // Schedule event for calling at the start of the next second + const inOneSecond = (cb) => { + let now = new Date(); + clearTimeout(); + setTimeout(cb, 1000 - now.getMilliseconds()); + }; + + // Schedule event for calling at the start of the next minute + const inOneMinute = (cb) => { + let now = new Date(); + clearTimeout(); + setTimeout(cb, 60000 - (now.getSeconds() * 1000 + now.getMilliseconds())); + }; + + // Draw a fat hour/minute hand + const drawHand = (G, a, w2, r1, r2) => + G.fillRotRect(Math.sin(a), Math.cos(a), CX, CY, -w2, w2, r1, r2); + + // Redraw function + const drawAll = (force) => { + let now = new Date(); + + if (!faceImg) force = true; + + let face_changed = force; + let date_changed = false; + + tmp = hours; + hours = now.getHours(); + if (tmp !== hours) + face_changed = true; + + tmp = minutes; + minutes = now.getMinutes(); + if (tmp !== minutes) + face_changed = true; + + // If the face has been updated and/or needs a redraw, + // face_changed is true. + + let time_changed = face_changed; + + // If the screen needs an update, regardless of whether the face + // needs a redraw, time_changed is true. + + if (with_seconds) { + // If we're going by second, we always need an update. + seconds = now.getSeconds(); + time_changed = true; + } + + if (with_digital_date) { + // See if the date has changed. If it has, then we need a + // full-blown redraw of the screen and the face, plus text. + tmp = date; + date = now.getDate(); + if (tmp !== date) { + date_changed = true; + face_changed = true; // Should have changed anyway with hour/minute rollover + } + } + + if (face_changed) { + // Redraw the face and hands onto the buffer G1. + faceImg = drawFace(G1); + drawHand(G1, Math.PI*hours/6, HW2, RC1, HR); + drawHand(G1, Math.PI*minutes/30, MW2, RC1, MR); + } + + // Has the time updated? If so, we'll need to draw something. + if (time_changed) { + + // Are we adding text? + if (with_digital_date || with_digital_time) { + + // Construct the date/time text to add above the face + let d = now.toString(); + let da = d.toString().split(" "); + let txt; + + if (with_digital_time) { + txt = da[4].substr(0, 5); + if (with_digital_date) + G1.drawStringDH(txt+',', 24, 0, 'L', GW); + else + G1.drawStringDH(txt, 0, 0, 'C', GW); + } + + if (with_digital_date) { + let txt = [da[0], da[1], da[2]].join(" "); + if (with_digital_time) + G1.drawStringDH(txt, -24, 0, 'R', GW); + else + G1.drawStringDH(txt, 0, 0, 'C', GW); + } + } + + // If the time has updated, we need to _at least_ draw the + // image to the screen. + g.setColor(1,1,1); + g.drawImage({width:GW, + height:GH-TM, + buffer:G1.buffer}, 0, TM); + + // and possibly add the second hand + if (with_seconds) { + let a = 2.0 * Math.PI * seconds / 60.0; + g.drawRotLine(Math.sin(a), Math.cos(a), CX, CY+TM, RC1, R1); + } + + // Clock chime on the hour. + if (hours >= 0 && minutes === 0) + try { + Bangle.buzz(); + } catch (e) { } + + // And draw widgets if we're in that mode + if (with_widgets) + Bangle.drawWidgets(); + } + + // Schedule to repeat this. A `setTimeout(1000)` isn't good + // enough, as all the above might've taken some milliseconds and + // we don't want to drift. + if (with_seconds) + inOneSecond(drawAll); + else + inOneMinute(drawAll); + }; + + const setButtons = () => { + const opts = { repeat: true, edge:'rising', debounce:30}; + + // BTN1: enable/disable second hand + setWatch(changeSeconds, BTN1, opts); + + // BTN2: return to launcher + setWatch(Bangle.showLauncher, BTN2, { repeat:false, edge:'falling' }); + + // BTN3: change display mode + setWatch(function () { ++mode; setMode(); drawAll(true); }, BTN3, opts); + }; + + // Load display parameters based on `mode` + const setMode = () => { + // Normalize mode to 0 <= mode <= 5 + mode = (6+mode) % 6; + + // [R1, R2, RC1, RC2, HW2, MW3, HR, MR, M, HSCALE] = + const scales = [ + [120, 84, 17, 12.4, 4.6, 2.2, 8, 2, 1, h => (3.0 + Math.ceil(h/1.5)) ], + [102, 70, 14.6, 10.7, 3.88, 1.8, 8, 2, 1, h => (2.4 + Math.ceil(h/1.6)) ], + ]; + + if (mode < 3) { + // Face without time/date text. Might have widgets though. + with_digital_time = with_digital_date = false; + with_widgets = (mode == 1); + } + else { + // Face with time/date text, but no widgets + with_digital_time = (mode-2)&1; + with_digital_date = (mode-2)&2; + with_widgets = false; + } + + // Destructure the array to the global display parameters + let arr = scales[mode > 0 ? 1 : 0]; + R1 = arr[0]; + R2 = arr[1]; + RC1 = arr[2]; + RC2 = arr[3]; + HW2 = arr[4]; + MW2 = arr[5]; + HR = R2 - arr[6]; + MR = R1 - arr[7]; + M = arr[8]; + HSCALE = arr[9]; + TM = with_widgets ? 36 : 0; + + CX = GW/2; + CY = R1; + + // If we're in the small-face + text regime, we're going to buffer + // the full screen but draw the clock face further down to give + // space for the text. + // + // Compare with modes 0 (full-screen) and 1 (with_widgets==true) + // where the face is drawn at the top of the buffer, but drawn + // lower down the screen (so CY doesn't move) + if (mode > 1) { + CY += 36; + } + + // We only don't bother redrawing the face from modes 2 to 5, as + // they're the same. + if (!faceImg || mode<3) { + faceImg = undefined; + } + + // Store the settings for next time + try { + storage.writeJSON(filename, [mode,with_seconds]); + } catch (e) { + console.log(e); + } + + // Clear the screen: we need to make sure all parts are cleaned off. + g.clear(); + }; + + const changeSeconds = () => { + with_seconds = !with_seconds; + drawAll(true); + }; + + Bangle.loadWidgets(); + + // Restore mode + try { + conf = storage.readJSON(filename); + mode = conf[0]; + with_seconds = conf[1]; + } catch (e) { + console.log(e); + mode = 1; + } + + setButtons(); + setMode(); + drawAll(); + + Bangle.on('lcdPower', (on) => { + if (on) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + drawAll(); + } else { + clearTimeout(); + } + }); + +})(g); diff --git a/apps/beebclock/beebclock.png b/apps/beebclock/beebclock.png new file mode 100644 index 000000000..447ec9a41 Binary files /dev/null and b/apps/beebclock/beebclock.png differ diff --git a/apps/beer/app-icon.js b/apps/beer/app-icon.js new file mode 100644 index 000000000..c700b3bd2 --- /dev/null +++ b/apps/beer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4")) diff --git a/apps/beer/beercompass.png b/apps/beer/app.png similarity index 100% rename from apps/beer/beercompass.png rename to apps/beer/app.png diff --git a/apps/beer/beercompass.html b/apps/beer/custom.html similarity index 94% rename from apps/beer/beercompass.html rename to apps/beer/custom.html index 434f0f6a9..ab3f80b50 100644 --- a/apps/beer/beercompass.html +++ b/apps/beer/custom.html @@ -196,12 +196,10 @@ Bangle.on('mag', function(m) { Bangle.setCompassPower(1); Bangle.setGPSPower(1); g.clear();`; -var icon = `require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))`; sendCustomizedApp({ storage:[ - {name:"beer.app.js", content:app}, - {name:"beer.img", content:icon, evaluate:true}, + {name:"beer.app.js", content:app} ] }); }); diff --git a/apps/blackjack/ChangeLog b/apps/blackjack/ChangeLog new file mode 100644 index 000000000..c941d90e5 --- /dev/null +++ b/apps/blackjack/ChangeLog @@ -0,0 +1 @@ +0.01: New game! BTN4- Hit card, BTN5- Stand \ No newline at end of file diff --git a/apps/blackjack/blackjack-icon.js b/apps/blackjack/blackjack-icon.js new file mode 100644 index 000000000..cb4d00cdd --- /dev/null +++ b/apps/blackjack/blackjack-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIQNgfAAgU///wAgMH/4dBAoMMAQMQAQMIAQMYAQ4RCApcPwAFDgIwBAoQ4BAoMP8EHwfghk//AXBuEMv38n+AjEMvl8/4FDvoFBmEMvF994FBg04vgdBAoMAAot4AoNgAoPwAoZFBAongAoPggyIBAoPAg0HwAFDh4BBAoUeh0PwOAg08AocDv/+Ao3DAod//a3BAorBDAohRBgf+AocBAokApgCBhzSCWIkHVYgYCWIngYwQrB/gFDgF//AFDD4QAD8AFEAAIA=")) \ No newline at end of file diff --git a/apps/blackjack/blackjack.app.js b/apps/blackjack/blackjack.app.js new file mode 100644 index 000000000..dc5d35494 --- /dev/null +++ b/apps/blackjack/blackjack.app.js @@ -0,0 +1,191 @@ +const Clubs = { width : 48, height : 48, bpp : 1, + buffer : require("heatshrink").decompress(atob("ACcP+AFDn/8Aod//wFD///AgUBAoOAApsDAoPAAr4vLI4pTEgP8L4M/wEH/5rB//gh//x/x//wj//9/3//4n4iBAAIZBAol/Aof+Apv5z4FP+OPAo41BAoX8I4Pj45HBAoPD4YFBLIOD4JZBRAMD4CKC/AFBj59Cg/gQYYFXAB4=")) +}; + +const Spades = { width : 48, height : 48, bpp : 1, + buffer : require("heatshrink").decompress(atob("ABsBwAFDgfAAocH8AFDh/wAocf/AFDn/8Aod//wFD///FwYFBGAUDAoIwCg4FBGAUPAoIwCj4FBGAU/AoIwCv4FBGAQEBGAQuCGAQuCGAQFLHQQ8CAupHLL4prB+fPTgU/8fHVwbLLApbXFbpYFLdIoADA==")) +}; + +const Hearts = { width : 48, height : 48, bpp : 4, + buffer : require("heatshrink").decompress(atob("ADlVqtQBQ8FBYIKIrnMAAINGqoKC4okGCwYAB4AKDhgKE4oWKAAILDBQwYEBYwwDFwojFgoLHEgQ6H5hhCBZAkCBRAjLEgI6IC4YLIC5Y7BBZXBjgjVABYX/C8CnKABbXLABTvMC8sMC6fAC4KQURwIABRypgULwRgULwRIUCwhIRIwiRSRoZITCwx5POoowRCxAwNFxIwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQwDFycAgoXBqAXTgFc4oWUJAJGUJARGVAEo")) + }; + +const Diamonds = { width : 48, height : 48, bpp : 4, + buffer : require("heatshrink").decompress(atob("AHUFC60M4AXV5nFIyvM5hGVC4JIUCwJIUIwRIUIwRIUCwZISIwgABqBGUJCQWFPKBGGJCFcC455OCw4wOOox5QIxB5NOpBIOFxZ5LCxYwKOpQwMIxh5KOxipLL6xgNR5QwMX5TvXPJZ1JJBpGLPJR1LJBZGNPJIWOJA5GOPJB1NJBIWQPIpGRJApGRPIoWSJAa8PJA5GTJAYWUJAJGVAAJGVAHo=")) + }; + + +var deck = []; +var player = {Hand:[]}; +var computer = {Hand:[]}; + +function createDeck() { + var suits = ["Spades", "Hearts", "Diamonds", "Clubs"]; + var values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]; + + var dck = []; + for (var i = 0 ; i < values.length; i++) { + for(var x = 0; x < suits.length; x++) { + dck.push({ Value: values[i], Suit: suits[x] }); + } + } + return dck; +} + +function shuffle(a) { + var j, x, i; + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = a[i]; + a[i] = a[j]; + a[j] = x; + } + return a; +} + +function EndGameMessdage(msg){ + g.drawString(msg, 155, 200); + setTimeout(function(){ + startGame(); + }, 2500); + +} + +function hitMe() { + player.Hand.push(deck.pop()); + renderOnScreen(1); + var playerWeight = calcWeight(player.Hand, 0); + + if(playerWeight == 21) + EndGameMessdage('WINNER'); + else if(playerWeight > 21) + EndGameMessdage('LOOSER'); +} + +function calcWeight(hand, hideCard) { + + if(hideCard === 1) { + if (hand[0].Value == "J" || hand[0].Value == "Q" || hand[0].Value == "K") + return "10 +"; + else if (hand[0].Value == "A") + return "11 +"; + else + return parseInt(hand[0].Value) +" +"; + } + else { + var weight = 0; + for(i=0; i 21 || bangleWeight < playerWeight) + EndGameMessdage('WINNER'); + else if(bangleWeight > playerWeight) + EndGameMessdage('LOOSER'); +} + +function renderOnScreen(HideCard) { + const fontName = "6x8"; + + g.clear(); // clear screen + g.reset(); // default draw styles + g.setFont(fontName, 1); + + g.drawString('RST', 220, 35); + g.drawString('Hit', 60, 230); + g.drawString('Stand', 165, 230); + + g.setFont(fontName, 3); + for(i=0; i scan() +}; + +function showMainMenu() { + menu["< Back"] = () => load(); + return E.showMenu(menu); +} + +function showDeviceInfo(device){ + const deviceMenu = { + "": { "title": "Device Info" }, + "name": { + value: device.name + }, + "rssi": { + value: device.rssi + }, + "manufacturer": { + value: device.manufacturer + } + }; + + deviceMenu[device.id] = () => {}; + deviceMenu["< Back"] = () => showMainMenu(); + + return E.showMenu(deviceMenu); +} + +function scan() { + menu = { + "": { "title": "BLE Detector" }, + "RE-SCAN": () => scan() + }; + + waitMessage(); + + NRF.findDevices(devices => { + devices.forEach(device =>{ + let deviceName = device.id.substring(0,17); + + if (device.name) { + deviceName = device.name; + } + + menu[deviceName] = () => showDeviceInfo(device); + }); + showMainMenu(menu); + }, { active: true }); +} + +function waitMessage() { + E.showMenu(); + E.showMessage("scanning"); +} + +scan(); +waitMessage(); \ No newline at end of file diff --git a/apps/bledetect/bledetect.png b/apps/bledetect/bledetect.png new file mode 100644 index 000000000..59d6a26ce Binary files /dev/null and b/apps/bledetect/bledetect.png differ diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 7ab79a5a5..c157c6705 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -13,3 +13,6 @@ 0.13: Now automatically load *.boot.js at startup Move alarm code into alarm.boot.js 0.14: Move welcome loaders to *.boot.js +0.15: Added BLE HID option for Joystick and bare Keyboard +0.16: Detect out of memory errors and draw them onto the bottom of the screen in red +0.17: Don't modify beep/buzz behaviour if firmware does it automatically diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js index dd3b3a9ba..38423362d 100644 --- a/apps/boot/boot0.js +++ b/apps/boot/boot0.js @@ -4,7 +4,9 @@ E.setFlags({pretokenise:1}); var s = require('Storage').readJSON('setting.json',1)||{}; if (s.ble!==false) { if (s.HID) { // Human interface device - Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA==")); + if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA=")); + else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA==")); + else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA==")); NRF.setServices({}, {uart:true, hid:Bangle.HID}); } } @@ -19,24 +21,31 @@ if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth // Don't disconnect if something is already connected to us if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep(); // Set time, vibrate, beep, etc -if (!s.vibrate) Bangle.buzz=Promise.resolve; -if (s.beep===false) Bangle.beep=Promise.resolve; -else if (s.beep=="vib") Bangle.beep = function (time, freq) { - return new Promise(function(resolve) { - if ((0|freq)<=0) freq=4000; - if ((0|time)<=0) time=200; - if (time>5000) time=5000; - analogWrite(D13,0.1,{freq:freq}); - setTimeout(function() { - digitalWrite(D13,0); - resolve(); - }, time); - }); -}; +if (!Bangle.F_BEEPSET) { + if (!s.vibrate) Bangle.buzz=Promise.resolve; + if (s.beep===false) Bangle.beep=Promise.resolve; + else if (s.beep=="vib") Bangle.beep = function (time, freq) { + return new Promise(function(resolve) { + if ((0|freq)<=0) freq=4000; + if ((0|time)<=0) time=200; + if (time>5000) time=5000; + analogWrite(D13,0.1,{freq:freq}); + setTimeout(function() { + digitalWrite(D13,0); + resolve(); + }, time); + }); + }; +} Bangle.setLCDTimeout(s.timeout); if (!s.timeout) Bangle.setLCDPower(1); E.setTimeZone(s.timezone); delete s; +// Draw out of memory errors onto the screen +E.on('errorFlag', function(errorFlags) { g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); + print("Interpreter error:",errorFlags); + E.getErrorFlags(); // clear flags so we get called next time +}); // stop users doing bad things! global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); } // Load *.boot.js files diff --git a/apps/boot/hid_info.txt b/apps/boot/hid_info.txt new file mode 100644 index 000000000..873b50f63 --- /dev/null +++ b/apps/boot/hid_info.txt @@ -0,0 +1,88 @@ + +## Joystick: + +https://github.com/espruino/BangleApps/issues/349#issuecomment-620231524 + +``` +0x05, 0x01, // Usage Page (Generic Desktop) +0x09, 0x04, // Usage (Joystick) +0xA1, 0x01, // Collection (Application) + 0x09, 0x01, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + // Buttons + 0x05, 0x09, // Usage Page (Buttons) + 0x19, 0x01, // Usage Minimum (1) + 0x29, 0x05, // Usage Maximum (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, Variable, Absolute) + + // padding bits + 0x95, 0x03, // Report Count (3) + 0x75, 0x01, // Report Size (1) + 0x81, 0x03, // Input (Constant) + + // Stick + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7f, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x02, // Report Count (2) + 0x81, 0x02, // Input (Data, Variable, Absolute) + 0xC0, // End Collection (Physical) +0xC0 // End Collection (Application) +``` + +## Keyboard + +http://www.espruino.com/BLE+Keyboard + +``` +0x05, 0x01, // Usage Page (Generic Desktop) +0x09, 0x06, // Usage (Keyboard) +0xA1, 0x01, // Collection (Application) +0x05, 0x07, // Usage Page (Key Codes) +0x19, 0xe0, // Usage Minimum (224) +0x29, 0xe7, // Usage Maximum (231) +0x15, 0x00, // Logical Minimum (0) +0x25, 0x01, // Logical Maximum (1) +0x75, 0x01, // Report Size (1) +0x95, 0x08, // Report Count (8) +0x81, 0x02, // Input (Data, Variable, Absolute) + +0x95, 0x01, // Report Count (1) +0x75, 0x08, // Report Size (8) +0x81, 0x01, // Input (Constant) reserved byte(1) + +0x95, 0x05, // Report Count (5) +0x75, 0x01, // Report Size (1) +0x05, 0x08, // Usage Page (Page# for LEDs) +0x19, 0x01, // Usage Minimum (1) +0x29, 0x05, // Usage Maximum (5) +0x91, 0x02, // Output (Data, Variable, Absolute), Led report +0x95, 0x01, // Report Count (1) +0x75, 0x03, // Report Size (3) +0x91, 0x01, // Output (Data, Variable, Absolute), Led report padding + +0x95, 0x06, // Report Count (6) +0x75, 0x08, // Report Size (8) +0x15, 0x00, // Logical Minimum (0) +0x25, 0x73, // Logical Maximum (115 - include F13, etc) +0x05, 0x07, // Usage Page (Key codes) +0x19, 0x00, // Usage Minimum (0) +0x29, 0x73, // Usage Maximum (115 - include F13, etc) +0x81, 0x00, // Input (Data, Array) Key array(6 bytes) + +0x09, 0x05, // Usage (Vendor Defined) +0x15, 0x00, // Logical Minimum (0) +0x26, 0xFF, 0x00, // Logical Maximum (255) +0x75, 0x08, // Report Count (2) +0x95, 0x02, // Report Size (8 bit) +0xB1, 0x02, // Feature (Data, Variable, Absolute) + +0xC0 // End Collection (Application) +``` diff --git a/apps/buffgym/.eslintrc.json b/apps/buffgym/.eslintrc.json new file mode 100644 index 000000000..c91a72544 --- /dev/null +++ b/apps/buffgym/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "windows" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/apps/buffgym/ChangeLog b/apps/buffgym/ChangeLog new file mode 100644 index 000000000..6efdd865a --- /dev/null +++ b/apps/buffgym/ChangeLog @@ -0,0 +1,2 @@ +0.01: Create BuffGym app +0.02: Add web interface for personalising workout diff --git a/apps/buffgym/README.md b/apps/buffgym/README.md new file mode 100644 index 000000000..bcadf22c4 --- /dev/null +++ b/apps/buffgym/README.md @@ -0,0 +1,60 @@ +# BuffGym + +This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program. + +## Configuration + +### Setting your start weight values + +You will want to set your own starting weight values for your 5x5 training program. To do this is easy! After installing this app, go to the BangleJS app store, connect to your watch, and navigate to the `My Apps` tab. In there you will find this app in the list, and an icon (a down arrow) to the right of the app title. Click that icon to reveal a configuration page. Enter your weights and other details, and click upload. That is it, you are now ready to train! + +## Usage + +### Start screen + +When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start + +![](buffgym-scrn1.png) + +### Workouts menu + +You are then presented with the workouts menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the workout. + +![](buffgym-scrn2.png) + +### Recording your training + +You will now begin moving through the exercises in the workout. You will see the exercise information on the display. + +![](buffgym-scrn3.png) + +1. At the top is the exercise name, e.g 'Squats' +2. Next is the weight you must train +3. In the center is where you record the number of *reps* you completed (more on that shortly) +4. Below the *reps* value, is the target reps you must try to reach. +5. Below the target reps is the current set you are training, out of the total sets for the exercise. +6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps + +### Rest timers + +You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise. + +![](buffgym-scrn4.png) +![](buffgym-scrn5.png) + +### Workout completed + +Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are. + +![](buffgym-scrn6.png) + +## Features + +* If you successfully complete all reps and sets for an exercise, it will automatically update your weights for next time +* Has a neat rest timer to make sure you are training optimally +* Doesn't require a mobile phone, most 'smart watches' are just a visual presentation of the mobile phone app, this runs purley on the watch. So why not leave your phone and its distractions out of the gym! +* Clear and simple user interface + +## Created by + +[Paul Cockrell](https://github.com/paulcockrell) April 2020. diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js new file mode 100644 index 000000000..ea20aa132 --- /dev/null +++ b/apps/buffgym/buffgym-exercise.js @@ -0,0 +1,153 @@ +exports = class Exercise { + constructor(params) { + this.completed = false; + this.sets = []; + this.title = params.title; + this.weight = params.weight; + this.weightIncrement = params.weightIncrement; + this.unit = params.unit; + this.restPeriod = params.restPeriod; + this._originalRestPeriod = params.restPeriod; + this._restTimeout = null; + this._restInterval = null; + this._state = null; + } + + get humanTitle() { + return `${this.title} ${this.weight}${this.unit}`; + } + + get subTitle() { + const totalSets = this.sets.length; + const uncompletedSets = this.sets.filter((set) => !set.isCompleted()).length; + const currentSet = (totalSets - uncompletedSets) + 1; + return `Set ${currentSet} of ${totalSets}`; + } + + decRestPeriod() { + this.restPeriod--; + } + + addSet(set) { + this.sets.push(set); + } + + currentSet() { + return this.sets.filter(set => !set.isCompleted())[0]; + } + + isLastSet() { + return this.sets.filter(set => !set.isCompleted()).length === 1; + } + + isCompleted() { + return !!this.completed; + } + + canSetCompleted() { + return this.sets.filter(set => set.isCompleted()).length === this.sets.length; + } + + setCompleted() { + if (!this.canSetCompleted()) throw "All sets must be completed"; + if (this.canProgress()) this.weight += this.weightIncrement; + this.completed = true; + } + + canProgress() { + let completedRepsTotalSum = 0; + let targetRepsTotalSum = 0; + this.sets.forEach(set => completedRepsTotalSum += set.reps); + this.sets.forEach(set => targetRepsTotalSum += set.maxReps); + + return (targetRepsTotalSum - completedRepsTotalSum) === 0; + } + + startRestTimer(workout) { + this._restTimeout = setTimeout(() => { + this.next(workout); + }, 1000 * this.restPeriod); + + this._restInterval = setInterval(() => { + this.decRestPeriod(); + + if (this.restPeriod < 0) { + this.resetRestTimer(); + this.next(); + + return; + } + + workout.emit("redraw"); + }, 1000 ); + } + + resetRestTimer() { + clearTimeout(this._restTimeout); + clearInterval(this._restInterval); + this._restTimeout = null; + this._restInterval = null; + this.restPeriod = this._originalRestPeriod; + } + + isRestTimerRunning() { + return this._restTimeout != null; + } + + setupStartedButtons(workout) { + clearWatch(); + + setWatch(() => { + this.currentSet().incReps(); + workout.emit("redraw"); + }, BTN1, {repeat: true}); + + setWatch(workout.next.bind(workout), BTN2, {repeat: false}); + + setWatch(() => { + this.currentSet().decReps(); + workout.emit("redraw"); + }, BTN3, {repeat: true}); + } + + setupRestingButtons(workout) { + clearWatch(); + setWatch(workout.next.bind(workout), BTN2, {repeat: false}); + } + + next(workout) { + const STARTED = 1; + const RESTING = 2; + const COMPLETED = 3; + + switch(this._state) { + case null: + this._state = STARTED; + this.setupStartedButtons(workout); + break; + case STARTED: + this._state = RESTING; + this.startRestTimer(workout); + this.setupRestingButtons(workout); + break; + case RESTING: + this.resetRestTimer(); + this.currentSet().setCompleted(); + + if (this.canSetCompleted()) { + this._state = COMPLETED; + this.setCompleted(); + } else { + this._state = null; + } + // As we are changing state and require it to be reprocessed + // invoke the next step of workout + workout.next(); + break; + default: + throw "Exercise: Attempting to move to an unknown state"; + } + + workout.emit("redraw"); + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-icon.js b/apps/buffgym/buffgym-icon.js new file mode 100644 index 000000000..523ed35b6 --- /dev/null +++ b/apps/buffgym/buffgym-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA=")) diff --git a/apps/buffgym/buffgym-scrn1.png b/apps/buffgym/buffgym-scrn1.png new file mode 100755 index 000000000..07b79386f Binary files /dev/null and b/apps/buffgym/buffgym-scrn1.png differ diff --git a/apps/buffgym/buffgym-scrn2.png b/apps/buffgym/buffgym-scrn2.png new file mode 100755 index 000000000..ec70fb791 Binary files /dev/null and b/apps/buffgym/buffgym-scrn2.png differ diff --git a/apps/buffgym/buffgym-scrn3.png b/apps/buffgym/buffgym-scrn3.png new file mode 100755 index 000000000..0888fc507 Binary files /dev/null and b/apps/buffgym/buffgym-scrn3.png differ diff --git a/apps/buffgym/buffgym-scrn4.png b/apps/buffgym/buffgym-scrn4.png new file mode 100755 index 000000000..3078d25dc Binary files /dev/null and b/apps/buffgym/buffgym-scrn4.png differ diff --git a/apps/buffgym/buffgym-scrn5.png b/apps/buffgym/buffgym-scrn5.png new file mode 100755 index 000000000..b34a5b124 Binary files /dev/null and b/apps/buffgym/buffgym-scrn5.png differ diff --git a/apps/buffgym/buffgym-scrn6.png b/apps/buffgym/buffgym-scrn6.png new file mode 100755 index 000000000..563426ad3 Binary files /dev/null and b/apps/buffgym/buffgym-scrn6.png differ diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js new file mode 100644 index 000000000..dc0c05671 --- /dev/null +++ b/apps/buffgym/buffgym-set.js @@ -0,0 +1,28 @@ +exports = class Set { + constructor(maxReps) { + this.completed = false; + this.minReps = 0; + this.reps = 0; + this.maxReps = maxReps; + } + + isCompleted() { + return !!this.completed; + } + + setCompleted() { + this.completed = true; + } + + incReps() { + if (this.completed) return; + if (this.reps >= this.maxReps) return; + this.reps++; + } + + decReps() { + if (this.completed) return; + if (this.reps <= this.minReps) return; + this.reps--; + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-workout-a.json b/apps/buffgym/buffgym-workout-a.json new file mode 100644 index 000000000..8eb8611d6 --- /dev/null +++ b/apps/buffgym/buffgym-workout-a.json @@ -0,0 +1,33 @@ +{ + "title": "Workout A", + "exercises": [ + { + "title": "Squats", + "weight": 40, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Overhead press", + "weight": 20, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Deadlift", + "weight": 20, + "unit": "Kg", + "sets": [5], + "restPeriod": 90 + }, + { + "title": "Pullups", + "weight": 0, + "unit": "Kg", + "sets": [10, 10, 10], + "restPeriod": 90 + } + ] +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-workout-b.json b/apps/buffgym/buffgym-workout-b.json new file mode 100644 index 000000000..43845a98b --- /dev/null +++ b/apps/buffgym/buffgym-workout-b.json @@ -0,0 +1,33 @@ +{ + "title": "Workout B", + "exercises": [ + { + "title": "Squats", + "weight": 40, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Bench press", + "weight": 20, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Row", + "weight": 20, + "unit":"Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Tricep extension", + "weight": 20, + "unit": "Kg", + "sets": [10, 10, 10], + "restPeriod": 90 + } + ] +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-workout-index.json b/apps/buffgym/buffgym-workout-index.json new file mode 100644 index 000000000..af74d5e3b --- /dev/null +++ b/apps/buffgym/buffgym-workout-index.json @@ -0,0 +1,10 @@ +[ + { + "title": "Workout A", + "file": "buffgym-workout-a.json" + }, + { + "title": "Workout B", + "file": "buffgym-workout-b.json" + } +] \ No newline at end of file diff --git a/apps/buffgym/buffgym-workout.js b/apps/buffgym/buffgym-workout.js new file mode 100644 index 000000000..124c27f4b --- /dev/null +++ b/apps/buffgym/buffgym-workout.js @@ -0,0 +1,83 @@ +exports = class Workout { + constructor(params) { + this.title = params.title; + this.exercises = []; + this.completed = false; + this.on("redraw", redraw.bind(null, this)); + } + + addExercises(exercises) { + exercises.forEach(exercise => this.exercises.push(exercise)); + } + + currentExercise() { + return this.exercises.filter(exercise => !exercise.isCompleted())[0]; + } + + canComplete() { + return this.exercises.filter(exercise => exercise.isCompleted()).length === this.exercises.length; + } + + setCompleted() { + if (!this.canComplete()) throw "All exercises must be completed"; + this.completed = true; + } + + isCompleted() { + return !!this.completed; + } + + static fromJSON(workoutJSON) { + const Set = require("buffgym-set.js"); + const Exercise = require("buffgym-exercise.js"); + const workout = new this({ + title: workoutJSON.title, + }); + const exercises = workoutJSON.exercises.map(exerciseJSON => { + const exercise = new Exercise({ + title: exerciseJSON.title, + weight: exerciseJSON.weight, + weightIncrement: exerciseJSON.weightIncrement, + unit: exerciseJSON.unit, + restPeriod: exerciseJSON.restPeriod, + }); + exerciseJSON.sets.forEach(setJSON => { + exercise.addSet(new Set(setJSON)); + }); + + return exercise; + }); + + workout.addExercises(exercises); + + return workout; + } + + toJSON() { + return { + title: this.title, + exercises: this.exercises.map(exercise => { + return { + title: exercise.title, + weight: exercise.weight, + weightIncrement: exercise.weightIncrement, + unit: exercise.unit, + sets: exercise.sets.map(set => set.maxReps), + restPeriod: exercise.restPeriod, + }; + }), + }; + } + + // State machine + next() { + if (this.canComplete()) { + this.setCompleted(); + this.emit("redraw"); + return; + } + + // Call current exercise state machine + this.currentExercise().next(this); + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js new file mode 100755 index 000000000..fc2be83f9 --- /dev/null +++ b/apps/buffgym/buffgym.app.js @@ -0,0 +1,261 @@ +/** + * BangleJS Stronglifts 5x5 training aid + * + * Original Author: Paul Cockrell https://github.com/paulcockrell + * Created: April 2020 + * + * Inspired by: + * - Stronglifts 5x5 training workout https://stronglifts.com/5x5/ + * - Stronglifts smart watch app + */ + +Bangle.setLCDMode("120x120"); + +const W = g.getWidth(); +const H = g.getHeight(); +const RED = "#d32e29"; +const PINK = "#f05a56"; +const WHITE = "#ffffff"; + +function drawMenu(params) { + const hs = require("heatshrink"); + const incImg = hs.decompress(atob("gsFwMAkM+oUA")); + const decImg = hs.decompress(atob("gsFwIEBnwCBA")); + const okImg = hs.decompress(atob("gsFwMAhGFo0A")); + const DEFAULT_PARAMS = { + showBTN1: false, + showBTN2: false, + showBTN3: false, + }; + const p = Object.assign({}, DEFAULT_PARAMS, params); + if (p.showBTN1) g.drawImage(incImg, W - 10, 10); + if (p.showBTN2) g.drawImage(okImg, W - 10, 60); + if (p.showBTN3) g.drawImage(decImg, W - 10, 110); +} + +function drawSet(exercise) { + const set = exercise.currentSet(); + if (set.isCompleted()) return; + + g.clear(); + + // Draw exercise title + g.setColor(PINK); + g.fillRect(15, 0, W - 15, 18); + g.setFontAlign(0, -1); + g.setFont("6x8", 1); + g.setColor(WHITE); + g.drawString(exercise.title, W / 2, 5); + g.setFont("6x8", 1); + g.drawString(exercise.weight + " " + exercise.unit, W / 2, 27); + // Draw completed reps counter + g.setFontAlign(0, 0); + g.setColor(PINK); + g.fillRect(15, 42, W - 15, 80); + g.setColor(WHITE); + g.setFont("6x8", 5); + g.drawString(set.reps, (W / 2) + 2, (H / 2) + 1); + g.setFont("6x8", 1); + const note = `Target reps: ${set.maxReps}`; + g.drawString(note, W / 2, H - 24); + // Draw sets monitor + g.drawString(exercise.subTitle, W / 2, H - 12); + + drawMenu({showBTN1: true, showBTN2: true, showBTN3: true}); + + g.flip(); +} + +function drawWorkoutDone() { + const title1 = "You did"; + const title2 = "GREAT!"; + const msg = "That's the workout\ncompleted. Now eat\nsome food and\nget plenty of rest."; + + clearWatch(); + setWatch(Bangle.showLauncher, BTN2, {repeat: false}); + drawMenu({showBTN2: true}); + + g.setFontAlign(0, -1); + g.setColor(WHITE); + g.setFont("6x8", 2); + g.drawString(title1, W / 2, 10); + g.drawString(title2, W / 2, 30); + g.setFont("6x8", 1); + g.drawString(msg, (W / 2) + 3, 70); + g.flip(); +} + +function drawSetComp(exercise) { + const title = "Good work"; + const msg1= "No need to rest\nmove straight on\nto the next\nexercise."; + const msg2 = exercise.canProgress()? + "Your\nweight has been\nincreased for\nnext time!": + "You'll\nsmash it next\ntime!"; + + g.clear(); + drawMenu({showBTN2: true}); + + g.setFontAlign(0, -1); + g.setColor(WHITE); + g.setFont("6x8", 2); + g.drawString(title, W / 2, 10); + g.setFont("6x8", 1); + g.drawString(msg1 + msg2, (W / 2) - 2, 45); + + g.flip(); +} + +function drawRestTimer(exercise) { + g.clear(); + drawMenu({showBTN2: true}); + g.setFontAlign(0, -1); + g.setColor(PINK); + g.fillRect(15, 42, W - 15, 80); + g.setColor(WHITE); + g.setFont("6x8", 1); + g.drawString("Have a short\nrest period.", W / 2, 10); + g.setFont("6x8", 5); + g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19); + g.flip(); +} + +function redraw(workout) { + const exercise = workout.currentExercise(); + g.clear(); + + if (workout.isCompleted()) { + saveWorkout(workout); + drawWorkoutDone(); + return; + } + + if (exercise.isRestTimerRunning()) { + if (exercise.isLastSet()) { + drawSetComp(exercise); + } else { + drawRestTimer(exercise); + } + + return; + } + + drawSet(exercise); +} + +function drawWorkoutMenu(workouts, selWorkoutIdx) { + g.clear(); + g.setFontAlign(0, -1); + g.setColor(WHITE); + g.setFont("6x8", 2); + g.drawString("BuffGym", W / 2, 10); + + g.setFont("6x8", 1); + g.setFontAlign(-1, -1); + let selectedWorkout = workouts[selWorkoutIdx].title; + let yPos = 50; + workouts.forEach(workout => { + g.setColor("#f05a56"); + g.fillRect(0, yPos, W, yPos + 11); + g.setColor("#ffffff"); + if (selectedWorkout === workout.title) { + g.drawRect(0, yPos, W - 1, yPos + 11); + } + g.drawString(workout.title, 10, yPos + 2); + yPos += 15; + }); + g.flip(); +} + +function setupMenu() { + clearWatch(); + const workouts = getWorkoutIndex(); + let selWorkoutIdx = 0; + drawWorkoutMenu(workouts, selWorkoutIdx); + + setWatch(()=>{ + selWorkoutIdx--; + if (selWorkoutIdx< 0) selWorkoutIdx = 0; + drawWorkoutMenu(workouts, selWorkoutIdx); + }, BTN1, {repeat: true}); + + setWatch(()=>{ + const workout = buildWorkout(workouts[selWorkoutIdx].file); + workout.next(); + }, BTN2, {repeat: false}); + + setWatch(()=>{ + selWorkoutIdx++; + if (selWorkoutIdx > workouts.length - 1) selWorkoutIdx = workouts.length - 1; + drawWorkoutMenu(workouts, selWorkoutIdx); + }, BTN3, {repeat: true}); +} + +function drawSplash() { + g.reset(); + g.setBgColor(RED); + g.clear(); + g.setColor(WHITE); + g.setFontAlign(0,-1); + g.setFont("6x8", 2); + g.drawString("BuffGym", W / 2, 10); + g.setFont("6x8", 1); + g.drawString("5x5", W / 2, 42); + g.drawString("training app", W / 2, 55); + g.drawRect(19, 38, 100, 99); + const img = require("heatshrink").decompress(atob("lkdxH+AB/I5ASQACwpB5vNFkwpBAIfNFdZZkFYwskFZAsiFZBZiVYawEFf6ETFUwsIFUYmB54ADAwIskFYoRKBoIroB4grV58kkgCDFRotWFZwqHFiwYMFZIsTC5wLDFjGlCoWlkgJDRQIABCRAsLCwodCFAIABCwIOCFQYABr4RCCQIrMC4gqEAAwpFFZosFC5ArHFQ4rFNYQrGEgosMBxIrFLQwrLAB4sFSw4rFFjYrQFi4rNbASeEFjIoJFQYsGMAgAPEQgAIGwosCRoorbA=")); + g.drawImage(img, 40, 70); + g.flip(); + + let flasher = false; + let bgCol, txtCol; + const i = setInterval(() => { + if (flasher) { + bgCol = WHITE; + txtCol = RED; + } else { + bgCol = RED; + txtCol = WHITE; + } + flasher = !flasher; + g.setColor(bgCol); + g.fillRect(0, 108, W, 120); + g.setColor(txtCol); + g.drawString("Press btn to begin", W / 2, 110); + g.flip(); + }, 250); + + setWatch(()=>{ + clearInterval(i); + setupMenu(); + }, BTN1, {repeat: false}); + + setWatch(()=>{ + clearInterval(i); + setupMenu(); + }, BTN2, {repeat: false}); + + setWatch(()=>{ + clearInterval(i); + setupMenu(); + }, BTN3, {repeat: false}); +} + +function getWorkoutIndex() { + const workoutIdx = require("Storage").readJSON("buffgym-workout-index.json"); + return workoutIdx; +} + +function buildWorkout(fName) { + const Workout = require("buffgym-workout.js"); + const workoutJSON = require("Storage").readJSON(fName); + const workout = Workout.fromJSON(workoutJSON); + + return workout; +} + +function saveWorkout(workout) { + const fName = getWorkoutIndex().find(w => w.title === workout.title).file; + require("Storage").writeJSON(fName, workout.toJSON()); +} + +drawSplash(); \ No newline at end of file diff --git a/apps/buffgym/buffgym.html b/apps/buffgym/buffgym.html new file mode 100644 index 000000000..3c18932e9 --- /dev/null +++ b/apps/buffgym/buffgym.html @@ -0,0 +1,250 @@ + + + + + +

BuffGym

+

+ Enter in your weights for each exercise, start light and keep consistent with your training. The weight increment field is how much the app will increase your weights for an exercise if you successfully complete all the reps and sets for an exercise. Make sure its a value that matches the weights in your gym. +

+

+ For more information on how to train this program refer the Stronglifts website +

+
+

Workout A

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExerciseSets / RepsWeightWeight increment
+ Squats + + 5x5 + + + + +
+ Overhead press + + 5x5 + + + + +
+ Deadlift + + 1x5 + + + + +
+ Pullups + + 3x10 + + + + +
+

Workout B

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExerciseSets / RepsWeightWeight increment
+ Squats + + 5x5 + + + + +
+ Bench press + + 5x5 + + + + +
+ Row + + 5x5 + + + + +
+ Tricep extension + + 3x10 + + + + +
+
+

+ + + + + + + diff --git a/apps/buffgym/buffgym.png b/apps/buffgym/buffgym.png new file mode 100755 index 000000000..9bde64cc4 Binary files /dev/null and b/apps/buffgym/buffgym.png differ diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog new file mode 100644 index 000000000..3b9b23270 --- /dev/null +++ b/apps/calculator/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: fix precision rounding issue + no reset when equals pressed diff --git a/apps/calculator/README.md b/apps/calculator/README.md new file mode 100644 index 000000000..b25d355bf --- /dev/null +++ b/apps/calculator/README.md @@ -0,0 +1,23 @@ +# Calculator + +Basic calculator reminiscent of MacOs's one. Handy for small calculus. + + + +## Features + +- add / substract / divide / multiply +- handles floats +- basic memory button + +## Controls + +- UP: BTN1 +- DOWN: BTN3 +- LEFT: BTN4 +- RIGHT: BTN5 +- SELECT: BTN2 + +## Creator + + diff --git a/apps/calculator/app.js b/apps/calculator/app.js new file mode 100644 index 000000000..ad26d2d22 --- /dev/null +++ b/apps/calculator/app.js @@ -0,0 +1,392 @@ +/** + * BangleJS Calculator + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + */ + +g.clear(); +Graphics.prototype.setFont7x11Numeric7Seg = function() { + this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11); +}; + +var DEFAULT_SELECTION = '5'; +var BOTTOM_MARGIN = 10; +var RIGHT_MARGIN = 20; +var COLORS = { + // [normal, selected] + DEFAULT: ['#7F8183', '#A6A6A7'], + OPERATOR: ['#F99D1C', '#CA7F2A'], + SPECIAL: ['#65686C', '#7F8183'] +}; + +var keys = { + '0': { + xy: [0, 200, 120, 240], + trbl: '2.00' + }, + '.': { + xy: [120, 200, 180, 240], + trbl: '3=.0' + }, + '=': { + xy: [181, 200, 240, 240], + trbl: '+==.', + color: COLORS.OPERATOR + }, + '1': { + xy: [0, 160, 60, 200], + trbl: '4201' + }, + '2': { + xy: [60, 160, 120, 200], + trbl: '5301' + }, + '3': { + xy: [120, 160, 180, 200], + trbl: '6+.2' + }, + '+': { + xy: [181, 160, 240, 200], + trbl: '-+=3', + color: COLORS.OPERATOR + }, + '4': { + xy: [0, 120, 60, 160], + trbl: '7514' + }, + '5': { + xy: [60, 120, 120, 160], + trbl: '8624' + }, + '6': { + xy: [120, 120, 180, 160], + trbl: '9-35' + }, + '-': { + xy: [181, 120, 240, 160], + trbl: '*-+6', + color: COLORS.OPERATOR + }, + '7': { + xy: [0, 80, 60, 120], + trbl: 'R847' + }, + '8': { + xy: [60, 80, 120, 120], + trbl: 'N957' + }, + '9': { + xy: [120, 80, 180, 120], + trbl: '%*68' + }, + '*': { + xy: [181, 80, 240, 120], + trbl: '/*-9', + color: COLORS.OPERATOR + }, + 'R': { + xy: [0, 40, 60, 79], + trbl: 'RN7R', + color: COLORS.SPECIAL, + val: 'AC' + }, + 'N': { + xy: [60, 40, 120, 79], + trbl: 'N%8R', + color: COLORS.SPECIAL, + val: '+/-' + }, + '%': { + xy: [120, 40, 180, 79], + trbl: '%/9N', + color: COLORS.SPECIAL + }, + '/': { + xy: [181, 40, 240, 79], + trbl: '//*%', + color: COLORS.OPERATOR + } +}; + +var selected = DEFAULT_SELECTION; +var prevSelected = DEFAULT_SELECTION; +var prevNumber = null; +var currNumber = null; +var operator = null; +var results = null; +var isDecimal = false; +var hasPressedEquals = false; + +function drawKey(name, k, selected) { + var rMargin = 0; + var bMargin = 0; + var color = k.color || COLORS.DEFAULT; + g.setColor(color[selected ? 1 : 0]); + g.setFont('Vector', 20); + g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); + g.setColor(-1); + // correct margins to center the texts + if (name == '0') { + rMargin = (RIGHT_MARGIN * 2) - 7; + } else if (name === '/') { + rMargin = 5; + } else if (name === '*') { + bMargin = 5; + rMargin = 3; + } else if (name === '-') { + rMargin = 3; + } else if (name === 'R' || name === 'N') { + rMargin = k.val === 'C' ? 0 : -9; + } else if (name === '%') { + rMargin = -3; + } + g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); +} + +function getIntWithPrecision(x) { + var xStr = x.toString(); + var xRadix = xStr.indexOf('.'); + var xPrecision = xRadix === -1 ? 0 : xStr.length - xRadix - 1; + return { + num: Number(xStr.replace('.', '')), + p: xPrecision + }; +} + +function multiply(x, y) { + var xNum = getIntWithPrecision(x); + var yNum = getIntWithPrecision(y); + return xNum.num * yNum.num / Math.pow(10, xNum.p + yNum.p); +} + +function divide(x, y) { + var xNum = getIntWithPrecision(x); + var yNum = getIntWithPrecision(y); + return xNum.num / yNum.num / Math.pow(10, xNum.p - yNum.p); +} + +function sum(x, y) { + let xNum = getIntWithPrecision(x); + let yNum = getIntWithPrecision(y); + + let diffPrecision = Math.abs(xNum.p - yNum.p); + if (diffPrecision > 0) { + if (xNum.p > yNum.p) { + yNum.num = yNum.num * Math.pow(10, diffPrecision); + } else { + xNum.num = xNum.num * Math.pow(10, diffPrecision); + } + } + return (xNum.num + yNum.num) / Math.pow(10, Math.max(xNum.p, yNum.p)); +} + +function subtract(x, y) { + return sum(x, -y); +} + +function doMath(x, y, operator) { + switch (operator) { + case '/': + return divide(x, y); + case '*': + return multiply(x, y); + case '+': + return sum(x, y); + case '-': + return subtract(x, y); + } +} + +function displayOutput(num) { + var len; + var minusMarge = 0; + g.setColor(0); + g.fillRect(0, 0, 240, 39); + g.setColor(-1); + if (num === Infinity || num === -Infinity || isNaN(num)) { + // handle division by 0 + if (num === Infinity) { + num = 'INFINITY'; + } else if (num === -Infinity) { + num = '-INFINITY'; + } else { + num = 'NOT A NUMBER'; + minusMarge = -25; + } + len = (num + '').length; + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + prevNumber = null; + operator = null; + keys.R.val = 'AC'; + drawKey('R', keys.R); + g.setFont('Vector', 22); + } else { + // might not be a number due to display of dot "." + var numNumeric = Number(num); + + if (typeof num === 'string') { + if (num.indexOf('.') !== -1) { + // display a 0 before a lonely dot + if (numNumeric == 0) { + num = '0.'; + } + } else { + // remove preceding 0 + while (num.length > 1 && num[0] === '0') + num = num.substr(1); + } + } + + len = (num + '').length; + if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) { + // minus is not available in font 7x11Numeric7Seg, we use Vector + g.setFont('Vector', 20); + g.drawString('-', 220 - (len * 15), 10); + minusMarge = 15; + } + g.setFont('7x11Numeric7Seg', 2); + } + g.drawString(num, 220 - (len * 15) + minusMarge, 10); +} +var wasPressedEquals = false; +var hasPressedNumber = false; +function calculatorLogic(x) { + if (wasPressedEquals && hasPressedNumber !== false) { + prevNumber = null; + currNumber = hasPressedNumber; + wasPressedEquals = false; + hasPressedNumber = false; + return; + } + if (hasPressedEquals) { + if (hasPressedNumber) { + prevNumber = null; + hasPressedNumber = false; + operator = null; + } else { + currNumber = null; + prevNumber = results; + } + hasPressedEquals = false; + wasPressedEquals = true; + } + + if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) { + operator = x; + displayOutput(prevNumber); + } else if (prevNumber != null && currNumber != null && operator != null) { + // we execute the calculus only when there was a previous number entered before and an operator + results = doMath(prevNumber, currNumber, operator); + operator = x; + prevNumber = results; + currNumber = null; + displayOutput(results); + } else if (prevNumber == null && currNumber != null && operator == null) { + // no operator yet, save the current number for later use when an operator is pressed + operator = x; + prevNumber = currNumber; + currNumber = null; + displayOutput(prevNumber); + } else if (prevNumber == null && currNumber == null && operator == null) { + displayOutput(0); + } +} + +function buttonPress(val) { + switch (val) { + case 'R': + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + if (keys.R.val == 'AC') { + prevNumber = null; + operator = null; + } else { + keys.R.val = 'AC'; + drawKey('R', keys.R, true); + } + wasPressedEquals = false; + hasPressedNumber = false; + displayOutput(0); + break; + case '%': + if (results != null) { + displayOutput(results /= 100); + } else if (currNumber != null) { + displayOutput(currNumber /= 100); + } + hasPressedNumber = false; + break; + case 'N': + if (results != null) { + displayOutput(results *= -1); + } else { + displayOutput(currNumber *= -1); + } + break; + case '/': + case '*': + case '-': + case '+': + calculatorLogic(val); + hasPressedNumber = false; + break; + case '.': + keys.R.val = 'C'; + drawKey('R', keys.R); + isDecimal = true; + displayOutput(currNumber == null ? 0 + '.' : currNumber + '.'); + break; + case '=': + if (prevNumber != null && currNumber != null && operator != null) { + results = doMath(prevNumber, currNumber, operator); + prevNumber = results; + displayOutput(results); + hasPressedEquals = 1; + } + hasPressedNumber = false; + break; + default: + keys.R.val = 'C'; + drawKey('R', keys.R); + const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity); + if (isDecimal) { + currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val; + isDecimal = false; + } else { + currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val); + } + if (hasPressedEquals === 1) { + hasPressedEquals = 2; + } + hasPressedNumber = currNumber; + displayOutput(currNumber); + break; + } +} + +for (var k in keys) { + if (keys.hasOwnProperty(k)) { + drawKey(k, keys[k], k == '5'); + } +} +g.setFont('7x11Numeric7Seg', 2.8); +g.drawString('0', 205, 10); + +function moveDirection(d) { + drawKey(selected, keys[selected]); + prevSelected = selected; + selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d]; + drawKey(selected, keys[selected], true); +} + +setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100}); +setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100}); diff --git a/apps/calculator/calculator-icon.js b/apps/calculator/calculator-icon.js new file mode 100644 index 000000000..94158e7d2 --- /dev/null +++ b/apps/calculator/calculator-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44=")) diff --git a/apps/calculator/calculator.png b/apps/calculator/calculator.png new file mode 100644 index 000000000..8362c9200 Binary files /dev/null and b/apps/calculator/calculator.png differ diff --git a/apps/calculator/tests.html b/apps/calculator/tests.html new file mode 100644 index 000000000..1cbfdf617 --- /dev/null +++ b/apps/calculator/tests.html @@ -0,0 +1,273 @@ + + + + + + Calculator tests + + + + + + +
+ + + + + + + diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog new file mode 100644 index 000000000..3cf79ffe8 --- /dev/null +++ b/apps/calendar/ChangeLog @@ -0,0 +1 @@ +0.01: Basic calendar diff --git a/apps/calendar/README.md b/apps/calendar/README.md new file mode 100644 index 000000000..19a60afc0 --- /dev/null +++ b/apps/calendar/README.md @@ -0,0 +1,8 @@ +# Calendar + +Basic calendar + +## Usage + +- Use `BTN4` (left screen tap) to go to the previous month +- Use `BTN5` (right screen tap) to go to the next month diff --git a/apps/calendar/calendar-icon.js b/apps/calendar/calendar-icon.js new file mode 100644 index 000000000..ed1bf3667 --- /dev/null +++ b/apps/calendar/calendar-icon.js @@ -0,0 +1,5 @@ +require("heatshrink").decompress( + atob( + "mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA==" + ) +) diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js new file mode 100644 index 000000000..720986162 --- /dev/null +++ b/apps/calendar/calendar.js @@ -0,0 +1,160 @@ +const maxX = 240; +const maxY = 240; +const rowN = 7; +const colN = 7; +const headerH = maxY / 7; +const rowH = (maxY - headerH) / rowN; +const colW = maxX / colN; +const color1 = "#035AA6"; +const color2 = "#4192D9"; +const color3 = "#026873"; +const color4 = "#038C8C"; +const color5 = "#03A696"; +const black = "#000000"; +const white = "#ffffff"; +const gray1 = "#444444"; +const gray2 = "#888888"; +const gray3 = "#bbbbbb"; +const red = "#d41706"; + +function drawCalendar(date) { + g.setBgColor(color4); + g.clearRect(0, 0, maxX, maxY); + g.setBgColor(color1); + g.clearRect(0, 0, maxX, headerH); + g.setBgColor(color2); + g.clearRect(0, headerH, maxX, headerH + rowH); + g.setBgColor(color3); + g.clearRect(colW * 5, headerH + rowH, maxX, maxY); + for (let y = headerH; y < maxY; y += rowH) { + g.drawLine(0, y, maxX, y); + } + for (let x = 0; x < maxX; x += colW) { + g.drawLine(x, headerH, x, maxY); + } + + const month = date.getMonth(); + const year = date.getFullYear(); + const monthMap = { + 0: "January", + 1: "February", + 2: "March", + 3: "April", + 4: "May", + 5: "June", + 6: "July", + 7: "August", + 8: "September", + 9: "October", + 10: "November", + 11: "December" + }; + g.setFontAlign(0, 0); + g.setFont("6x8", 2); + g.setColor(white); + g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2); + g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true); + g.drawPoly( + [maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10], + true + ); + + g.setFont("6x8", 2); + const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + dowLbls.forEach((lbl, i) => { + g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); + }); + + date.setDate(1); + const dow = date.getDay(); + const dowNorm = dow === 0 ? 7 : dow; + + const monthMaxDayMap = { + 0: 31, + 1: (2020 - year) % 4 === 0 ? 29 : 28, + 2: 31, + 3: 30, + 4: 31, + 5: 30, + 6: 31, + 7: 31, + 8: 30, + 9: 31, + 10: 30, + 11: 31 + }; + + let days = []; + let nextMonthDay = 1; + let thisMonthDay = 51; + let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm; + for (let i = 0; i < colN * (rowN - 1) + 1; i++) { + if (i < dowNorm) { + days.push(prevMonthDay); + prevMonthDay++; + } else if (thisMonthDay <= monthMaxDayMap[month] + 50) { + days.push(thisMonthDay); + thisMonthDay++; + } else { + days.push(nextMonthDay); + nextMonthDay++; + } + } + + let i = 0; + for (y = 0; y < rowN - 1; y++) { + for (x = 0; x < colN; x++) { + i++; + const day = days[i]; + const isToday = + today.year === year && today.month === month && today.day === day - 50; + if (isToday) { + g.setColor(red); + g.drawRect( + x * colW, + y * rowH + headerH + rowH, + x * colW + colW - 1, + y * rowH + headerH + rowH + rowH + ); + } + g.setColor(day < 50 ? gray3 : white); + g.drawString( + (day > 50 ? day - 50 : day).toString(), + x * colW + colW / 2, + headerH + rowH + y * rowH + rowH / 2 + ); + } + } +} + +const date = new Date(); +const today = { + day: date.getDate(), + month: date.getMonth(), + year: date.getFullYear() +}; +drawCalendar(date); +clearWatch(); +setWatch( + () => { + const month = date.getMonth(); + const prevMonth = month > 0 ? month - 1 : 11; + if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); + date.setMonth(prevMonth); + drawCalendar(date); + }, + BTN4, + { repeat: true } +); +setWatch( + () => { + const month = date.getMonth(); + const prevMonth = month < 11 ? month + 1 : 0; + if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1); + date.setMonth(month + 1); + drawCalendar(date); + }, + BTN5, + { repeat: true } +); +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); diff --git a/apps/calendar/calendar.png b/apps/calendar/calendar.png new file mode 100644 index 000000000..056cab3b7 Binary files /dev/null and b/apps/calendar/calendar.png differ diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog new file mode 100644 index 000000000..e173467a1 --- /dev/null +++ b/apps/chronowid/ChangeLog @@ -0,0 +1,3 @@ +0.01: New widget and app! +0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) +0.03: Display only minutes:seconds when less than 1 hour left \ No newline at end of file diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md new file mode 100644 index 000000000..f422dd956 --- /dev/null +++ b/apps/chronowid/README.md @@ -0,0 +1,38 @@ +# Chronometer Widget + +Chronometer (timer) that runs as a widget. +The advantage is, that you can still see your normal watchface and other widgets when the timer is running. +The widget is always active, but only shown when the timer is on. +Hours, minutes, seconds and timer status can be set with an app. + +Depending on when you start the timer, it may alert up to 0,999 seconds early. This is because it checks only for full seconds. When there is less than one seconds left, it buzzes. This cannot be avoided without checking more than every second, which I would like to avoid. + +## Screenshots + +TBD + +## Features + +* Using other apps does not interrupt the timer, no need to keep the widget open (BUT: there will be no buzz when the time is up, for that the widget has to be loaded) +* Target time is saved to a file and timer picks up again when widget is loaded again. + +## Settings + +There are no settings section in the settings app, timer can be set using an app. + +* Reset values: Reset hours, minutes, seconds to 0; set timer on to false; write to settings file +* Hours: Set the hours for the timer +* Minutes: Set the minutes for the timer +* Seconds: Set the seconds for the timer +* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. + + +## Releases + +* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) +* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) +* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid + +## Requests + +If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ \ No newline at end of file diff --git a/apps/chronowid/app-icon.js b/apps/chronowid/app-icon.js new file mode 100644 index 000000000..db2010218 --- /dev/null +++ b/apps/chronowid/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIFCn/8BYYFRABcD4AFFgIFCh/wgeAAoP//8HCYMDAoPD8EAg4FB8PwgEf+EP/H4HQOAgP8uEAvwfBv0ggBFCn4CB/EBwEfgEB+AFBh+AgfgAoI1BIoQJB4AHBAoXgg4uBAIIFCCYQFGh5rDJQJUBK4IFCNYIFVDoopDGoJiBHYYFKVYRZBWIYDBA4IFBNIQzBG4IbBToKkBAQKVFUIYICVoQUCXIQmCYoIsCaITqDAoLvDNYUAA=")) \ No newline at end of file diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js new file mode 100644 index 000000000..dd9531233 --- /dev/null +++ b/apps/chronowid/app.js @@ -0,0 +1,98 @@ +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const storage = require('Storage'); +const boolFormat = v => v ? "On" : "Off"; +let settingsChronowid; + +function updateSettings() { + var now = new Date(); + const goal = new Date(now.getFullYear(), now.getMonth(), now.getDate(), + now.getHours() + settingsChronowid.hours, now.getMinutes() + settingsChronowid.minutes, now.getSeconds() + settingsChronowid.seconds); + settingsChronowid.goal = goal.getTime(); + storage.writeJSON('chronowid.json', settingsChronowid); +} + +function resetSettings() { + settingsChronowid = { + hours : 0, + minutes : 0, + seconds : 0, + started : false, + counter : 0, + goal : 0, + }; + updateSettings(); +} + +settingsChronowid = storage.readJSON('chronowid.json',1); +if (!settingsChronowid) resetSettings(); + +E.on('kill', () => { + updateSettings(); +}); + +function showMenu() { + const timerMenu = { + '': { + 'title': 'Set timer', + 'predraw': function() { + timerMenu.hours.value = settingsChronowid.hours; + timerMenu.minutes.value = settingsChronowid.minutes; + timerMenu.seconds.value = settingsChronowid.seconds; + timerMenu.started.value = settingsChronowid.started; + } + }, + 'Reset values': function() { + settingsChronowid.hours = 0; + settingsChronowid.minutes = 0; + settingsChronowid.seconds = 0; + settingsChronowid.started = false; + updateSettings(); + showMenu(); + }, + 'Hours': { + value: settingsChronowid.hours, + min: 0, + max: 24, + step: 1, + onchange: v => { + settingsChronowid.hours = v; + updateSettings(); + } + }, + 'Minutes': { + value: settingsChronowid.minutes, + min: 0, + max: 59, + step: 1, + onchange: v => { + settingsChronowid.minutes = v; + updateSettings(); + } + }, + 'Seconds': { + value: settingsChronowid.seconds, + min: 0, + max: 59, + step: 1, + onchange: v => { + settingsChronowid.seconds = v; + updateSettings(); + } + }, + 'Timer on': { + value: settingsChronowid.started, + format: boolFormat, + onchange: v => { + settingsChronowid.started = v; + updateSettings(); + } + }, + }; + timerMenu['-Exit-'] = ()=>{load();}; + return E.showMenu(timerMenu); +} + +showMenu(); \ No newline at end of file diff --git a/apps/chronowid/app.png b/apps/chronowid/app.png new file mode 100644 index 000000000..5ac7a480c Binary files /dev/null and b/apps/chronowid/app.png differ diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js new file mode 100644 index 000000000..0c9366b86 --- /dev/null +++ b/apps/chronowid/widget.js @@ -0,0 +1,93 @@ +(() => { + const storage = require('Storage'); + settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file + var height = 23; + var width = 58; + var interval = 0; //used for the 1 second interval timer + var now = new Date(); + + var time = 0; + var diff = settingsChronowid.goal - now; + + //Convert ms to time + function getTime(t) { + var milliseconds = parseInt((t % 1000) / 100), + seconds = Math.floor((t / 1000) % 60), + minutes = Math.floor((t / (1000 * 60)) % 60), + hours = Math.floor((t / (1000 * 60 * 60)) % 24); + + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + + return hours + ":" + minutes + ":" + seconds; + } + + function printDebug() { + print ("Nowtime: " + getTime(now)); + print ("Now: " + now); + print ("Goaltime: " + getTime(settingsChronowid.goal)); + print ("Goal: " + settingsChronowid.goal); + print("Difftime: " + getTime(diff)); + print("Diff: " + diff); + print ("Started: " + settingsChronowid.started); + print ("----"); + } + + //counts down, calculates and displays + function countDown() { + now = new Date(); + diff = settingsChronowid.goal - now; //calculate difference + WIDGETS["chronowid"].draw(); + //time is up + if (settingsChronowid.started && diff < 1000) { + Bangle.buzz(1500); + //write timer off to file + settingsChronowid.started = false; + storage.writeJSON('chronowid.json', settingsChronowid); + clearInterval(interval); //stop interval + } + //printDebug(); + } + + // draw your widget + function draw() { + if (!settingsChronowid.started) { + width = 0; + return; //do not draw anything if timer is not started + } + g.reset(); + if (diff >= 0) { + if (diff < 3600000) { //less than 1 hour left + width = 58; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 2); + g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 + } + if (diff >= 3600000) { //one hour or more left + width = 48; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 1); + g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 + } + } + // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. + // else { + // width = 58; + // g.clearRect(this.x,this.y,this.x+width,this.y+height); + // g.setFont("6x8", 2); + // g.drawString("END", this.x+15, this.y+5); + // } + } + + if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second + + // add the widget + WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { + reload(); + Bangle.drawWidgets(); // relayout all widgets + }}; + + //printDebug(); + countDown(); +})(); \ No newline at end of file diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog new file mode 100644 index 000000000..efd778c72 --- /dev/null +++ b/apps/compass/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Show text if uncalibrated \ No newline at end of file diff --git a/apps/compass/compass.js b/apps/compass/compass.js index 10895e3cd..a014d79ff 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -1,34 +1,43 @@ -g.clear(); -g.setColor(0,0.5,1); -g.fillCircle(120,130,80,80); -g.setColor(0,0,0); -g.fillCircle(120,130,70,70); - -function arrow(r,c) { - r=r*Math.PI/180; - var p = Math.PI/2; - g.setColor(c); - g.fillPoly([ - 120+60*Math.sin(r), 130-60*Math.cos(r), - 120+10*Math.sin(r+p), 130-10*Math.cos(r+p), - 120+10*Math.sin(r+-p), 130-10*Math.cos(r-p), - ]); -} - -var oldHeading = 0; -Bangle.on('mag', function(m) { - if (!Bangle.isLCDOn()) return; - g.setFont("6x8",3); - g.setColor(0); - g.fillRect(70,0,170,24); - g.setColor(0xffff); - g.setFontAlign(0,0); - g.drawString(isNaN(m.heading)?"---":Math.round(m.heading),120,12); - g.setColor(0,0,0); - arrow(oldHeading,0); - arrow(oldHeading+180,0); - arrow(m.heading,0xF800); - arrow(m.heading+180,0x001F); - oldHeading = m.heading; -}); -Bangle.setCompassPower(1); +g.clear(); +g.setColor(0,0.5,1); +g.fillCircle(120,130,80,80); +g.setColor(0,0,0); +g.fillCircle(120,130,70,70); + +function arrow(r,c) { + r=r*Math.PI/180; + var p = Math.PI/2; + g.setColor(c); + g.fillPoly([ + 120+60*Math.sin(r), 130-60*Math.cos(r), + 120+10*Math.sin(r+p), 130-10*Math.cos(r+p), + 120+10*Math.sin(r+-p), 130-10*Math.cos(r-p), + ]); +} + +var oldHeading = 0; +Bangle.on('mag', function(m) { + if (!Bangle.isLCDOn()) return; + g.setFont("6x8",3); + g.setColor(0); + g.fillRect(0,0,230,40); + g.setColor(0xffff); + if (isNaN(m.heading)) { + g.setFontAlign(-1,-1); + g.setFont("6x8",2); + g.drawString("Uncalibrated",50,12); + g.drawString("turn 360° around",25,26); + } + else { + g.setFontAlign(0,0); + g.setFont("6x8",3); + g.drawString(Math.round(m.heading),120,12); + } + g.setColor(0,0,0); + arrow(oldHeading,0); + arrow(oldHeading+180,0); + arrow(m.heading,0xF800); + arrow(m.heading+180,0x001F); + oldHeading = m.heading; +}); +Bangle.setCompassPower(1); diff --git a/apps/custom/custom.html b/apps/custom/custom.html new file mode 100644 index 000000000..5a5dbbecd --- /dev/null +++ b/apps/custom/custom.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + +

Type your javascript code here

+

+

Then click

+ + + + diff --git a/apps/custom/custom.png b/apps/custom/custom.png new file mode 100644 index 000000000..722ceb9ee Binary files /dev/null and b/apps/custom/custom.png differ diff --git a/apps/dane/ChangeLog b/apps/dane/ChangeLog new file mode 100644 index 000000000..419109ec1 --- /dev/null +++ b/apps/dane/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.04: Added Icon to watchface +0.05: bugfix +0.06: moved and resized icon +0.07: Added Description \ No newline at end of file diff --git a/apps/dane/app-icon.js b/apps/dane/app-icon.js new file mode 100644 index 000000000..4deb12640 --- /dev/null +++ b/apps/dane/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("l8wxH+AH4A/AH4A/AFUvl8Cu4AEgUCBQIrfFQMRAAe/Aw4xbDYIlBiUS7AjCAAY5BBYMSiJkBGC4sCicTiRQJHoUSCAIwBF6sv30SikUiRMMMIISD7AvTl/YiYtPF40TF6R4BicVFqAWDF4MViaPRIwQWTF4O/IwiKRCoMRUiZHEDJ5cXJAxeOOQuQhQuShWQJIe/JJkviIuC74tTFwORRqKLD+3cmVLpsLFZtNAANKhXeDYKNOu4uEmdlDwVNBoNlsoDDmoKBhYQChcyFycVFwOTFwJcBpomBhYjCmouBAwYMCmZdBa4d3FyonBKoIoCAwIECLooucEIIjCRIYuFms1Lqq7CFwS7DLQQsDhYrBHIZdHXZkCdQpQDXoIQDFwIDBeoQQCpYuSl8RFwMT70KCRYAIhUSFwMTiMvFxm/CQUSFyp5Did3Fxi8DOBwuLDSEv7ETfoRCNDI13DIMT34ZPIYSgOaxJ3SIgZeTC7COBdgMCC58vOoakWiQvQFoQTBFqgvEiURF5gRDOKIdIDwMRiO/axMCBoMRLQItXF4Z9B7F3BxF37BZBAAQnRIYobDMAKqIl5aDAA5zJFwaCBAA6PBFxQQEAAYKBFxjSCU4IECA4YuJCAoAEFx0UikTAAIEBAwQuKCIoADFxsCI5RdiUAoAEVgIVJABRDHAH4A/AH4A/ADAA=")) \ No newline at end of file diff --git a/apps/dane/app.js b/apps/dane/app.js new file mode 100644 index 000000000..dc6262c58 --- /dev/null +++ b/apps/dane/app.js @@ -0,0 +1,163 @@ +const font = "6x8"; +const timeFontSize = 4; +const dateFontSize = 3; +const smallFontSize = 2; +const yOffset = 23; +const xyCenter = g.getWidth()/2; +const cornerSize = 14; +const cornerOffset = 3; +const borderWidth = 1; +const yposTime = 27+yOffset; +const yposDate = 65+yOffset; + +const mainColor = "#26dafd"; +const mainColorDark = "#029dbb"; +const mainColorLight = "#8bebfe"; + +const secondaryColor = "#df9527"; +const secondaryColorDark = "#8b5c15"; +const secondaryColorLight = "#ecc180"; + +const success = "#00ff00"; +const successDark = "#000900"; +const successLight = "#060f06"; + +const alert = "#ff0000"; +const alertDark = "#090000"; +const alertLight = "#0f0606"; + +var img = { + width : 120, height : 120, bpp : 8, + transparent : 254, + buffer : require("heatshrink").decompress(atob("/wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AAdCABhN/OFM8ABU2P35zkM4U2hkAABwSBCwJ6/OjZxBgxyPABAZBPgJ6/OqbnBOg8rAAJyNCBEGhk2PX51PmBhHOhEGmwACPRQXFCoL1DOP51HdIh0IhkwnhcDAAoKBm0wDwYdEDwp5/Oo8MKxjQEABwiEkp5/Oxs2OpBTDOgwjOEyEMPHrJFJwxPCmx0QPRM8PIQpJFQjs8JZLEDJa55EUYMGFpMwPG5ICgzsQUrimCkryKnh40OyYxfPAQxIGQMGPGZ2EIZJ2iPCLxyOwRBMO0Z4/IIp2yPH4/Dhg9JHwJ2nPAg5Mgx3sFgMwgEqHhMMO1B4EeBQ7EO1U8HZSzBni0rHh0AmyzqHPB4FmDwLgC1BHGsMB4J3uWxY/Ed2ivBO1h4DmxAOG00MV2jwYmBBld354DmB3LeEo0Bgzu9eCMGIcYzOm1DoZ3wPAUMeF4yNg8Bnp3zGYM3gEHO5U2eEIhBdxcHg52zO4U9gJ3JPAMMO8U2O5k3odEO+VEPAKxBO5UAnh3tHgM9oh30AAMNO4tWO4s2O79CoUGdxcHn1EotFO+NFO4M3O5R4BgxXBO708dxR3BhB2Co1AO+J4BnCzBO/U4OwdAoIACN8goDAAVAow2Bnx3FAApTBnh3fmx3FljuFO4NGsmzAAWPxOJstlLpGJx4LGBIWJSIgIBCIVBsuPFYYsCsjwCO+ApEO5NlJAJ0BAAllegwRCPAwJC2YVEOIJ/BAAOJT4YoDeAVEhB3roVCdwsrqx3IJgJSDZYNlcoTbGNo53EDop3GBglBoB3KJAhUBmx3mmR3Fn53ILYjlDA4LQCMwYKDO4SCCDYQkEFQILDO40yd5h3nAAkHhx3BoB3EN4ZWHOgIGBPQQKE2YLBOIh3SnEHPBJ37boZWEOYJnCO44LBxKGCO5AWBAAZ4BO/53GDYhcGOQp8DNwoPBQ4Z3GAAINBAANlO/53TB4J3EAogREsrwCd59FO/53FPAhlHLggVENw4QCSRQABoB3/O5ZWGMIIABNAJ8BAAIMEPomPCAJ3Nox3+hB3HAAZeCKwQOCdwTwDO5ATCRYR38PAJ3Pox3HNIOPNIZ8BQozjBBpB+BO44cFoFAO6E8O782PBR3GJoIADdohpCAoIoEPAQJBO4YKCeAZ3FB4IVBAAVkeAJ3vnh3Mnx3BZgZ6DJoLmFOwoABO4ZpBsoLFx53CRQQqEAAKbBO/0HnFFotAoBvDNo4AXD4opEAAIyBGwNEm53Lg1CO79Cgx3MohBBoxyeACZ2Boh2KO+M3H4NFO2R3OgEAmx2ePAU2EoJ4Jho/Boh3zGoNDO5k8O90HodDO2Z3Boc9O5cMoR3hoUMO5UBO4J40GoM3gJ3IZAM2O0DwNg8Anp33IoMkO5M8O8c8O5IyBmFCO+lCoRELgwOBGUcMGRUAGUZDSO5TuleBozDPGQzBmxDKd0jwPmB31IRLunGocGVhh4wGIM8dxUMIE4nBmw2IVoZ3ymDuyG4cMG5TwwdxYIBmw+qHBjwvU4S2Khg9rWJrwuFoM2HhMGHfSyCWdlCOxU8O9p4LA4M2PFQqCgx2IHIZ2sPBy1CH8x2/PGwlBnkMO3p4zEYU8dpMGO2q8EIoJGFAwMwPEIhCmx2HGAMGVMZIYmBABg54GeQQtiOw7sCO25KEnkMIYJMEYAJKdFQQpHAAMMUgR25PAlCmx5GAoR5BFLM8gx1IUIh27PAp5BJYRUCKIgoXEYZ0EToZ2/PA7MBeYZ5DmBPWoTtBOos2ngxFO/5FGPQUwPAcMO64cEOhB2xnh3XPITPDKCocBDYZ1JPCEwO78MO7JbEZKqTGABhBLnk2O78Amw1KJBp3bmwaCHIwASDoJ3ggw+aO4c8O+M8hgbBhg2UIB0wIKx3DDQI2YLYLZCACEMZIIADO8YAEhgAEGgoAHlZ3bDgQAWlYaCO8QmDH7B3WmAcCGyoXCO9AAZgEMICdCoUMGrh3DPDp3iICR3/d+42BO8J2cO/53/IDU8GykGO/88O+g1ggB2dIIgAdO64AeO/cwmwACGyoZDADU8VqhBPEoIADoQATG7IuUGsBCjHswA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A1")) +} + + +function drawTopLeftCorner(x,y) { + g.setColor(mainColor); + var x1 = x-cornerOffset; + var y1 = y-cornerOffset; + g.fillRect(x1,y1,x1+cornerSize,y1+cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x+cornerSize-cornerOffset,y+cornerSize-cornerOffset); +} +function drawTopRightCorner(x,y) { + g.setColor(mainColor); + var x1 = x+cornerOffset; + var y1 = y-cornerOffset; + g.fillRect(x1,y1,x1-cornerSize,y1+cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x-cornerSize-cornerOffset,y+cornerSize-cornerOffset); +} +function drawBottomLeftCorner(x,y) { + g.setColor(mainColor); + var x1 = x-cornerOffset; + var y1 = y+cornerOffset; + g.fillRect(x1,y1,x1+cornerSize,y1-cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x+cornerSize-cornerOffset,y-cornerSize+cornerOffset); +} +function drawBottomRightCorner(x,y) { + g.setColor(mainColor); + var x1 = x+cornerOffset; + var y1 = y+cornerOffset; + g.fillRect(x1,y1,x1-cornerSize,y1-cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x-cornerSize+cornerOffset,y-cornerSize+cornerOffset); +} + +function drawFrame(x1,y1,x2,y2) { + drawTopLeftCorner(x1,y1); + drawTopRightCorner(x2,y1); + drawBottomLeftCorner(x1,y2); + drawBottomRightCorner(x2,y2); + g.setColor(mainColorDark); + g.drawRect(x1,y1,x2,y2); + g.setColor("#000000"); + g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth); +} +function drawTopFrame(x1,y1,x2,y2) { + + drawBottomLeftCorner(x1,y2); + drawBottomRightCorner(x2,y2); + g.setColor(mainColorDark); + g.drawRect(x1,y1,x2,y2); + g.setColor("#000000"); + g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth); +} +function drawBottomFrame(x1,y1,x2,y2) { + drawTopLeftCorner(x1,y1); + drawTopRightCorner(x2,y1); + g.setColor(mainColorDark); + g.drawRect(x1,y1,x2,y2); + g.setColor("#000000"); + g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth); +} + +function getUTCTime(d) { + return d.toUTCString().split(' ')[4].split(':').map(function(d){return Number(d);}); +} + + + + + +function drawTimeText() { + g.setFontAlign(0, 0); + var d = new Date(); + var da = d.toString().split(" "); + var dutc = getUTCTime(d); + + var time = da[4].split(":"); + var hours = time[0], + minutes = time[1], + seconds = time[2]; + g.setColor(mainColor); + g.setFont(font, timeFontSize); + g.drawString(`${hours}:${minutes}:${seconds}`, xyCenter, yposTime, true); + g.setFont(font, smallFontSize); +} +function drawDateText() { + g.setFontAlign(0, 0); + var d = new Date(); + g.setFont(font, dateFontSize); + g.drawString(`${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`, xyCenter, yposDate, true); +} + + + +function drawClock() { + // main frame + drawFrame(3,10+yOffset,g.getWidth()-3,g.getHeight()-3); + // time frame + drawTopFrame(20,10+yOffset,220,46+yOffset); + // date frame + drawTopFrame(28,46+yOffset,212,46+yOffset+35); + + // texts + drawTimeText(); + drawDateText(); + g.drawImage(img,g.getWidth()/2-(img.width/2),g.getHeight()/2); +} +function updateClock() { + drawTimeText(); + drawDateText(); +} + + +Bangle.on('lcdPower', function(on) { + if (on) drawClock(); +}); +g.clear(); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + + +drawClock(); + + +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); + +// refesh every 100 milliseconds +setInterval(updateClock, 100); diff --git a/apps/dane/app.png b/apps/dane/app.png new file mode 100644 index 000000000..ee4f8403a Binary files /dev/null and b/apps/dane/app.png differ diff --git a/apps/devstopwatch/ChangeLog b/apps/devstopwatch/ChangeLog new file mode 100644 index 000000000..e7c9e714a --- /dev/null +++ b/apps/devstopwatch/ChangeLog @@ -0,0 +1 @@ +0.01: App created diff --git a/apps/devstopwatch/app-icon.js b/apps/devstopwatch/app-icon.js new file mode 100644 index 000000000..c31177034 --- /dev/null +++ b/apps/devstopwatch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AAc1AAwvpApIv/SDTCteV6MuSRY8bEowEGAxYuXKRYODBQYwWJ5KOINxTkVWxZaFMQoARrNZAg4SPCZgcJDSQQDFy4vEBhFlsoVRFzAvEGIowXPBoKBxIADDCJeMBxYvEGApgTqdTAQYOJF4wwDDRwgGC4gvKwetAAgvFDwYvRNhQACvunF4enDZ4vUAgmD04ADF64ACZZQFDvoAEDZwvNCwt3u4FFSwgvWEQQCFBgwLIDZAvbAAgvgAYIkIFpQLEF6IsFC6AaJCiBjGF6IWUMA55XMCgwSIiowMDpYuYPAwaDAoQhHBIYuWC4ocNCSQdMRwofGBIqeNABubAAIFFAAwSIGLwHDBxgwcKwoLJGMSQKAH4A/AH4A+")) diff --git a/apps/devstopwatch/app.js b/apps/devstopwatch/app.js new file mode 100644 index 000000000..cf7186c98 --- /dev/null +++ b/apps/devstopwatch/app.js @@ -0,0 +1,159 @@ +const EMPTY_LAP = '--:--:---'; +const EMPTY_H = '00:00:000'; +const MAX_LAPS = 6; +const XY_CENTER = g.getWidth() / 2; +const Y_CHRONO = 40; +const Y_HEADER = 80; +const Y_LAPS = 125; +const Y_BTN3 = 225; +const FONT = '6x8'; +const CHRONO = '/* C H R O N O */'; + +var laps = [EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP]; +var started = false; +var reset = false; +var whenStarted; +var whenStartedTotal; +var currentLapIndex = 1; +var currentLap = ''; +var chronoInterval; + +// Set laps. +setWatch(() => { + + reset = false; + + if (started) { + changeLap(); + } else { + if (!reset) { + chronoInterval = setInterval(chronometer, 10); + } + } +}, BTN1, { repeat: true, edge: 'rising' }); + +// Reset chronometre. +setWatch(() => { resetChrono(); }, BTN3, { repeat: true, edge: 'rising' }); + +// Show launcher when middle button pressed. +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); + +function resetChrono() { + laps = [EMPTY_H, EMPTY_H, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP]; + started = false; + reset = true; + currentLapIndex = 1; + currentLap = ''; + + if (chronoInterval !== undefined) { + clearInterval(chronoInterval); + } + + printChrono(); +} + +function chronometer() { + + if (!started) { + var rightNow = Date.now(); + whenStarted = rightNow; + whenStartedTotal = rightNow; + started = true; + reset = false; + } + + currentLap = calculateLap(whenStarted); + total = calculateLap(whenStartedTotal); + + laps[0] = total; + laps[1] = currentLap; + printChrono(); +} + +function changeLap() { + + currentLapIndex++; + + if ((currentLapIndex) > MAX_LAPS) { + currentLapIndex = 2; + } + + laps[currentLapIndex] = currentLap; + whenStarted = Date.now(); +} + +function calculateLap(whenStarted) { + + var now = Date.now(); + var diffTime = now - whenStarted; + var dateDiffTime = new Date(diffTime); + + var millis = padStart(dateDiffTime.getMilliseconds().toString(), 3); + var seconds = padStart(dateDiffTime.getSeconds().toString(), 2); + var minutes = padStart(dateDiffTime.getMinutes().toString(), 2); + + return `${minutes}:${seconds}:${millis}`; +} + +function printChrono() { + + g.reset(); + g.setFontAlign(0, 0); + + var print = ''; + + g.setFont(FONT, 2); + print = CHRONO; + g.drawString(print, XY_CENTER, Y_CHRONO, true); + + g.setColor(0, 220, 0); + g.setFont(FONT, 3); + print = ` T ${laps[0]}\n`; + print += ` C ${laps[1]}\n`; + g.drawString(print, XY_CENTER, Y_HEADER, true); + + g.setColor(255, 255, 255); + g.setFont(FONT, 2); + + for (var i = 2; i < MAX_LAPS + 1; i++) { + + g.setColor(255, 255, 255); + let suffix = ' '; + if (currentLapIndex === i) { + let suffix = '*'; + g.setColor(255, 200, 0); + } + + const lapLine = `L${i - 1} ${laps[i]} ${suffix}\n`; + g.drawString(lapLine, XY_CENTER, Y_LAPS + (15 * (i - 1)), true); + } + + g.setColor(255, 255, 255); + g.setFont(FONT, 1); + print = 'Press 3 to reset'; + g.drawString(print, XY_CENTER, Y_BTN3, true); + + g.flip(); +} + +function padStart(value, size) { + + var result = ''; + var pads = size - value.length; + + if (pads > 0) { + for (var i = 0; i < pads; i++) { + result += '0'; + } + } + + result += value; + return result; +} + +// Clean app screen. +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +resetChrono(); diff --git a/apps/devstopwatch/app.png b/apps/devstopwatch/app.png new file mode 100644 index 000000000..b0feeebc9 Binary files /dev/null and b/apps/devstopwatch/app.png differ diff --git a/apps/files/ChangeLog b/apps/files/ChangeLog new file mode 100644 index 000000000..f2e5f64f5 --- /dev/null +++ b/apps/files/ChangeLog @@ -0,0 +1,4 @@ +0.02: Fix deletion of apps - now use files list in app.info (fix #262) +0.03: Add support for data files +0.04: Add functionality to sort apps manually or alphabetically ascending/descending. +0.05: Tweaks to help with memory usage diff --git a/apps/files/files.js b/apps/files/files.js index 31353cf96..ab259d6df 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -1,9 +1,7 @@ -const storage = require('Storage'); +const store = require('Storage'); const boolFormat = (v) => v ? "On" : "Off"; -let m; - function showMainMenu() { const mainmenu = { '': { @@ -12,50 +10,104 @@ function showMainMenu() { 'Free': { value: undefined, format: (v) => { - return storage.getFree(); + return store.getFree(); }, onchange: () => {} }, 'Compact': () => { E.showMessage('Compacting...'); try { - storage.compact(); + store.compact(); } catch (e) { } - m = showMainMenu(); + showMainMenu(); }, - 'Apps': ()=> m = showApps(), + 'Apps': ()=> showApps(), + 'Sort Apps': () => showSortAppsMenu(), '< Back': ()=> {load();} }; - return E.showMenu(mainmenu); + E.showMenu(mainmenu); } -function eraseApp(app) { +function isGlob(f) { + return /[?*]/.test(f); +} + +function globToRegex(pattern) { + const ESCAPE = '.*+-?^${}()|[]\\'; + const regex = pattern.replace(/./g, c => { + switch (c) { + case '?': return '.'; + case '*': return '.*'; + default: return ESCAPE.includes(c) ? ('\\' + c) : c; + } + }); + return new RegExp('^'+regex+'$'); +} + +function eraseFiles(app) { + app.files.split(",").forEach(f=>store.erase(f)); +} + +function eraseData(app) { + if(!app.data) return; + const d=app.data.split(';'), + files=d[0].split(','), + sFiles=(d[1]||'').split(','); + let erase = f=>store.erase(f); + files.forEach(f=>{ + if (!isGlob(f)) erase(f); + else store.list(globToRegex(f)).forEach(erase); + }); + erase = sf=>store.open(sf,'r').erase(); + sFiles.forEach(sf=>{ + if (!isGlob(sf)) erase(sf); + else store.list(globToRegex(sf+'\u0001')) + .forEach(fs=>erase(fs.substring(0,fs.length-1))); + }); +} +function eraseApp(app, files,data) { E.showMessage('Erasing\n' + app.name + '...'); - storage.erase(app['']); - storage.erase(app.icon); - storage.erase(app.src); + if (files) eraseFiles(app); + if (data) eraseData(app); +} +function eraseOne(app, files,data){ + E.showPrompt('Erase\n'+app.name+'?').then((v) => { + if (v) { + Bangle.buzz(100, 1); + eraseApp(app, files, data); + showApps(); + } else { + showAppMenu(app); + } + }); +} +function eraseAll(apps, files,data) { + E.showPrompt('Erase all?').then((v) => { + if (v) { + Bangle.buzz(100, 1); + for(var n = 0; n m = showApps(), - 'Erase': () => { - E.showPrompt('Erase\n' + app.name + '?').then((v) => { - if (v) { - Bangle.buzz(100, 1); - eraseApp(app); - m = showApps(); - } else { - m = showAppMenu(app) - } - }); - } + '< Back': () => showApps(), }; - return E.showMenu(appmenu); + if (app.data) { + appmenu['Erase Completely'] = () => eraseOne(app, true, true); + appmenu['Erase App,Keep Data'] = () => eraseOne(app, true, false); + appmenu['Only Erase Data'] = () => eraseOne(app, false, true); + } else { + appmenu['Erase'] = () => eraseOne(app, true, false); + } + E.showMenu(appmenu); } function showApps() { @@ -63,30 +115,29 @@ function showApps() { '': { 'title': 'Apps', }, - '< Back': () => m = showMainMenu(), + '< Back': () => showMainMenu(), }; - var list = storage.list(/\.info$/).filter((a)=> { + var list = store.list(/\.info$/).filter((a)=> { return a !== 'setting.info'; }).sort().map((app) => { - var ret = storage.readJSON(app,1)||{}; + var ret = store.readJSON(app,1)||{}; ret[''] = app; return ret; }); if (list.length > 0) { list.reduce((menu, app) => { - menu[app.name] = () => m = showAppMenu(app); + menu[app.name] = () => showAppMenu(app); return menu; }, appsmenu); appsmenu['Erase All'] = () => { - E.showPrompt('Erase all?').then((v) => { - if (v) { - Bangle.buzz(100, 1); - for (var n = 0; n < list.length; n++) - eraseApp(list[n]); - } - m = showApps(); + E.showMenu({ + '': {'title': 'Erase All'}, + 'Erase Everything': () => eraseAll(list, true, true), + 'Erase Apps,Keep Data': () => eraseAll(list, true, false), + 'Only Erase Data': () => eraseAll(list, false, true), + '< Back': () => showApps(), }); }; } else { @@ -96,7 +147,82 @@ function showApps() { onchange: ()=> {} }; } - return E.showMenu(appsmenu); + E.showMenu(appsmenu); } -m = showMainMenu(); +function showSortAppsMenu() { + const sorterMenu = { + '': { + 'title': 'App Sorter', + }, + '< Back': () => showMainMenu(), + 'Sort: manually': ()=> showSortAppsManually(), + 'Sort: alph. ASC': () => { + E.showMessage('Sorting:\nAlphabetically\nascending ...'); + sortAlphabet(false); + }, + 'Sort: alph. DESC': () => { + E.showMessage('Sorting:\nAlphabetically\ndescending ...'); + sortAlphabet(true); + } + }; + E.showMenu(sorterMenu); +} + +function showSortAppsManually() { + const appsSorterMenu = { + '': { + 'title': 'Sort: manually', + }, + '< Back': () => showSortAppsMenu(), + }; + let appList = getAppsList(); + if (appList.length > 0) { + appList.reduce((menu, app) => { + menu[app.name] = { + value: app.sortorder || 0, + min: 0, + max: appList.length, + step: 1, + onchange: val => setSortorder(app, val) + }; + return menu; + }, appsSorterMenu); + } else { + appsSorterMenu['...No Apps...'] = { + value: undefined, + format: ()=> '', + onchange: ()=> {} + }; + } + E.showMenu(appsSorterMenu); +} + +function setSortorder(app, val) { + app = store.readJSON(app.id + '.info', 1); + app.sortorder = val; + store.write(app.id + '.info', JSON.stringify(app)); +} + +function getAppsList() { + return store.list('.info').map((a)=> { + let app = store.readJSON(a, 1) || {}; + if (app.type !== 'widget') { + return {id: app.id, name: app.name, sortorder: app.sortorder}; + } + }).filter((a) => a).sort(sortHelper()); +} + +function sortAlphabet(desc) { + let appsSorted = desc ? getAppsList().reverse() : getAppsList(); + appsSorted.forEach((a, i) => { + setSortorder(a, i); + }); + showSortAppsMenu(); +} + +function sortHelper() { + return (a, b) => (a.name > b.name) - (a.name < b.name); +} + +showMainMenu(); diff --git a/apps/findphone/ChangeLog b/apps/findphone/ChangeLog new file mode 100644 index 000000000..9297fc6c7 --- /dev/null +++ b/apps/findphone/ChangeLog @@ -0,0 +1 @@ +0.01: First Version \ No newline at end of file diff --git a/apps/findphone/README.md b/apps/findphone/README.md new file mode 100644 index 000000000..870847222 --- /dev/null +++ b/apps/findphone/README.md @@ -0,0 +1,9 @@ +# Find Phone + +Ring your phone via GadgetBridge if you lost it somewhere. + +1. Enable HID in settings +2. Connect GadgetBridge +3. Lose phone +4. Open app +5. Click any button or screen diff --git a/apps/findphone/app-icon.js b/apps/findphone/app-icon.js new file mode 100644 index 000000000..95a73755e --- /dev/null +++ b/apps/findphone/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkCkQA/AEp0JCxkgC5KJMUpYXMgf/AA0wC5sPC4/wC/4XhxAXXwQXtlBIJC5URC4QwIC5PxFgQXT/QUCC6fwC4ZgIC5E/+EYCgJ4JC4/zwfwhAXTnGIC9pHXO66nY//4a63xFYTvUiJeCC6cAOxQXNFxIXllAWIC5oAKC+EDC48wC5oAKC9EBiAXokBGJgQXLMBQWMAH4AZA=")) \ No newline at end of file diff --git a/apps/findphone/app.js b/apps/findphone/app.js new file mode 100644 index 000000000..bbabdd38a --- /dev/null +++ b/apps/findphone/app.js @@ -0,0 +1,33 @@ +var storage = require('Storage'); + +//notify your phone +function find(){ + Bluetooth.println(JSON.stringify({t:"findPhone", n:true})); +} + +//init graphics +g.clear(); +require("Font8x12").add(Graphics); +g.setFont("8x12",3); +g.setFontAlign(0,0); +g.flip(); + +//init settings +const settings = storage.readJSON('setting.json',1) || { HID: false }; + +//check if HID enabled and show message +if (settings.HID=="kb" || settings.HID=="kbmedia") { + g.setColor(0x03E0); + g.drawString("click to find", g.getWidth()/2, g.getHeight()/2); + + //register all buttons and screen to find phone + setWatch(find, BTN1); + setWatch(find, BTN2); + setWatch(find, BTN3); + setWatch(find, BTN4); + setWatch(find, BTN5); + +}else{ + g.setColor(0xf800); + g.drawString("enable HID!", g.getWidth()/2, g.getHeight()/2); +} \ No newline at end of file diff --git a/apps/findphone/app.png b/apps/findphone/app.png new file mode 100644 index 000000000..70d891396 Binary files /dev/null and b/apps/findphone/app.png differ diff --git a/apps/flappy/ChangeLog b/apps/flappy/ChangeLog index 25c3827bd..62f107d11 100644 --- a/apps/flappy/ChangeLog +++ b/apps/flappy/ChangeLog @@ -1,2 +1,3 @@ 0.02: Tweaks to make flappy bird run with less RAM available 0.03: A few tweaks to improve rendering speed +0.04: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast diff --git a/apps/flappy/app.js b/apps/flappy/app.js index 4895b8530..9276010c0 100644 --- a/apps/flappy/app.js +++ b/apps/flappy/app.js @@ -32,10 +32,11 @@ function gameStop() { } function draw() { + "ram" var H = g.getHeight()-24; g.setColor("#71c6cf"); g.fillRect(0,0,g.getWidth(),H-1); - floorpos++; + floorpos++; for (var x=-(floorpos&15);xbbot)) gameStop(); }); diff --git a/apps/gallifr/ChangeLog b/apps/gallifr/ChangeLog new file mode 100644 index 000000000..c785cbd67 --- /dev/null +++ b/apps/gallifr/ChangeLog @@ -0,0 +1 @@ +0.01: First released version diff --git a/apps/gallifr/README.md b/apps/gallifr/README.md new file mode 100644 index 000000000..b88a3cb53 --- /dev/null +++ b/apps/gallifr/README.md @@ -0,0 +1,20 @@ +# Time Traveller's Clock + +The time travelling wristwatch is for those who are so attuned to the ebb and flow of time that they no longer require antiquated numbers to read the time. + +For those that need some tuition in the ways of the time traveller, the light coloured segment of the pie chart provides a traditional readout of minute. The black sphere that revolves around the edge of the display provides an indication of the hour. + +## Features + +The following aspects are customisable using the App Loader Menu system: + +1. Colour; the dial has four colour schemes: +- shades of green +- shades of red +- shades of blue +- a 1980's scheme +2. Widgets; these can be turned on or off - when turned off, the dial uses the whole screen and is slightly larger +3. Decoration; for those attuned to the time streams, the dial itself reads 'time'. For those who don't need to be reminded what the dial is for, this can be optionally turned off. + +## Code description +The code includes some functions that others may find useful in creating their own applications. These are explained in my robot-building blog [here](https://k9-build.blogspot.com/). \ No newline at end of file diff --git a/apps/gallifr/app-icon.js b/apps/gallifr/app-icon.js new file mode 100644 index 000000000..f0b27e1c8 --- /dev/null +++ b/apps/gallifr/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDAIAAAAAAAAAAAAAAAABgYAAAAAAAYGDAwSGBgYGBISBgAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAwMEhISGBgYHh4eHh4eHhgSDAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABhgYEhIYGBgYEhIYHh4eHh4eHh4eHhgMAAAGAAAAAAAAAAAAAAAAAAAAAAAGAAASHh4eGBISEhISEhIYHh4eHh4eHh4eHh4eGAYABgAAAAAAAAAAAAAAAAAAAAYADBgeHh4eGBIYEhISEhIYHh4eHh4eHh4eHh4eHh4SAAYAAAAAAAAAAAAAAAAABgAMHh4eHh4eHhgSEhIYGBgYHh4eHh4eHh4eHh4eHh4eEgAGAAAAAAAAAAAAAAAGABIeHh4eHh4eHhgSGBgSEhIMEhgYHh4eHh4eHh4eHh4eHhIABgAAAAAAAAAAAAYAEh4eHh4eHh4eHh4SEgwAAAAAAAAAABIYHh4eHh4eHh4eHh4SAAYAAAAAAAAABgAMHh4eHh4eHh4eHhIGAAAMEhISGB4YEgAABhIeHh4eHh4eHh4eEgAGAAAAAAAAAAweHh4eHh4eHh4YDAAABhIYGBgYHh4eHhgGAAAMGB4eHh4eHh4eHhIABgAAAAAAABgeHh4eHh4eHhISGAAMGBgSEhIYHh4eHh4eDAASEgweHh4eHh4eHh4MAAAAAAYAEh4eHh4eHh4eBhIeDAAeHhISGBgYHh4eHh4eGAAGHhIAHh4eHh4eHh4YAAAAAAAGHh4eHh4eHh4GEh4YABgeHhgSEhIMEhgeHh4eHhgAEh4SBh4eHh4eHh4eEgAGBgAYHh4eHh4eHhISHh4MDB4eHhgSDAAAAAAMHh4eHh4AAB4eEgweHh4eHh4eHgAAAAYeHh4eHh4eGBIeHh4AEh4eHh4MABISGBgADB4eHh4SABgeHgwSHh4eHh4eHhIAABIeHh4eHh4eBh4eHhgAGB4eHhgADBgYHh4SABgeHh4YABgeHhgAHh4eHh4eHhgAABgeHh4eHh4SEh4eHhgAGB4eHhgAEhgYHh4YABgeHh4YABIeHh4MEh4eHh4eHh4MBh4eHh4eHh4MHh4eHhgAGB4eHhgAEhgYHh4YABgeHh4YABgeHh4YAB4eHh4eHh4SDB4eHh4eHhgSHh4eHhgAGB4eHh4ADBIYHh4MAB4eHh4YABgeHh4eDBIeHh4eHh4YEh4eHh4eHhIYHh4eHhgADB4eHh4YAAYSEgwAGB4eHh4SAAweHh4eEgYYHh4eHh4YGB4eHh4eHgwYHh4eHh4MAB4eHh4eGAYAAAYYHh4eHh4AEh4YGBgSAAAAABgeHh4eGB4eHh4eHgweHh4eHh4YABIeHh4eHh4YGB4eHh4eHhIAGB4eGAAAEgYSEgAYHh4eGB4eHh4eGBIeHh4eHh4eDAAYHh4eHh4eHh4eHh4eGAASHh4eGAAYHgwYHgYAHh4eGB4eHh4eGAweHh4eHh4MAAYAGB4eHh4eHh4eHh4YAAweHh4eEgAeHgwYHhIAGB4eHh4eHh4eGAweHh4eGAYMGB4MABIeHh4eHh4eHhIADB4eHh4eEgAeHgwYHhIAGB4eHh4eHh4eGAweHhgSEhgeHh4YAAAADBIYGBIMAAASHh4eHh4eGAAYHgweHgwGHh4eHh4eHh4eGAYeGBgeHh4eHh4MABgSDAAAAAAMEhgeHh4eHh4eHgwAEgwYEgAYHh4eGB4eHh4eGAAYHh4eHh4eHhgAGB4eHh4YGB4eHh4eHh4eHh4eHh4MAAAAABgeHh4YGB4eHh4YGAYSHh4eHh4eHgASHh4eHh4eHh4eHh4eHh4eHh4eHh4eEgwYHh4eHh4YEh4YEgwYHhIMHh4eHh4eDBIeHh4eHh4eHh4eHh4eHh4eHh4eHh4eEhgeHh4eHh4SDBIAEh4eHh4AGB4eHh4YEh4eHh4eHh4YGB4eHh4eHh4eHh4eHh4eDB4eHh4eHh4MAAwYHh4eHh4SDB4eHhgGEhgYHh4YEhgSEhgYGB4eHh4eHh4eHh4SEh4eHh4eHh4GABgeHh4eHh4eABgeDAAAAAAABgwYHh4eHh4eGBIYHh4eHh4eHh4GHh4eHh4eHhgAABIeHh4eHh4eGAwGAAYAAAYGAAASHh4eHh4eHh4SEh4eHh4eHhIYHh4eHh4eHhIAAAAYHh4eHh4eGAAABgAAAAAAAAYAEh4eHh4eHh4eGBIeHh4eEhIeHh4eHh4eGAAABgASHh4eHh4eDAAGAAAAAAAAAAAGABgeHh4eHh4eHhIYHh4SBh4eHh4eHh4eEgAGAAYAGB4eHh4YAAAAAAAAAAAAAAAGABIeHh4eHh4eHh4SHhIGHh4eHh4eHh4YAAAAAAAABh4eHh4YAAYAAAAAAAAAAAAGAAweHh4eHh4eHh4YDBIeHh4eHh4eHh4GAAAAAAAGABIeHh4MAAAAAAAAAAAAAAAGAAYeHh4eHh4eHh4SGB4eHh4eHh4eHhIABgAAAAAABgASHhIABgAAAAAAAAAAAAAGAAweHh4eHh4eHh4eHh4eHh4eHh4eEgAGAAAAAAAAAAAADAYSAAAAAAAAAAAAAAAGABIeHh4eHh4eHh4eHh4eHh4eHh4YAAAAAAAAAAAAAAAAABIeBgAGAAAAAAAAAAAGABgeHh4eHh4eHh4eHh4eHh4eHhgAAAAAAAAAAAAAAAAAAAAYGAAAAAAAAAAAAAYADB4eHh4eHh4eHh4eHh4eHh4eEgAAAAAAAAAAAAAAAAAAAAAAEgwAAAAABgYGBgAAGB4eHh4eHh4eHh4eHh4eHh4SAAAAAAAAAAAAAAAAAAAAAAAAAAYGAAAAAAAAAAweHh4eHh4eHh4eHh4eHh4eGAYABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAYMDAwSGB4eHh4eHh4eHh4eHh4eHhgSAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYMEhgeHh4eHh4eHh4eHh4eHh4YEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYMEhgYHh4eHh4eGBgSDAYAAAAGAAAAAAAAAAAAAAAA")) \ No newline at end of file diff --git a/apps/gallifr/app.js b/apps/gallifr/app.js new file mode 100644 index 000000000..8948393d5 --- /dev/null +++ b/apps/gallifr/app.js @@ -0,0 +1,247 @@ +// +// Time Travellers Watch +// Written May 2020 by Richard Hopkins +// based on a skeleton app by Gordon Williams +// +const locale = require('locale'); +let timer = null; +let currentDate = new Date(); +const cirRad = 2*Math.PI; +const proportion = 0.3; // relative size of hour hand +const thickness = 4; // thickness of decorative lines +// retrieve settings from menu +let settings = require('Storage').readJSON('gallifr.json',1)||{}; +const decoration = !settings.decoration; +const widgets = !settings.widgets; +if (widgets) { + widgetHeight = 24;} +else { + widgetHeight = 0;} +const colours = ["green","red","blue","80s"]; +const colour = colours[settings.colour]; +const centerX = Math.round(g.getWidth() / 2); +const centerY = widgetHeight + Math.round((g.getHeight()-widgetHeight) / 2); +const radius = Math.round(Math.min(g.getWidth()/2,(g.getHeight()-widgetHeight) / 2)); + +const drawSegment = (params) => { + angle1 = params.start/360*cirRad; + angle2 = (params.start + params.arc)/360*cirRad; + segRadius = Math.round(params.radius*radius); + x = centerX + (params.x * radius); + y = centerY - (params.y *radius); + g.setColor(0,0,0); + incr = cirRad/15; + for (i = angle1; i < angle2; i=i+incr) { + brush = thickness * (angle2-angle1) /angle2; + points = [ + x + Math.sin(i) * (segRadius+brush), + y - Math.cos(i) * (segRadius+brush), + x + Math.sin(i+incr) * (segRadius+brush), + y - Math.cos(i+incr) * (segRadius+brush), + x + Math.sin(i+incr) * (segRadius-brush), + y - Math.cos(i+incr) * (segRadius-brush), + x + Math.sin(i) * (segRadius-brush), + y - Math.cos(i) * (segRadius-brush) + ]; + g.fillPoly(points); + } +}; + +const drawThickLine = (params) => { + g.setColor(0,0,0); + from = { + x: centerX + (params.fromX * radius), + y: centerY - (params.fromY * radius) + }; + to = { + x: centerX + (params.toX * radius), + y: centerY - (params.toY * radius) + }; + vec = {}; + vec.x = to.x - from.x; + vec.y = to.y - from.y; + pVec = {}; + pVec.x = vec.y; + pVec.y = -vec.x; + length = Math.sqrt(pVec.x * pVec.x + pVec.y * pVec.y); + nVec = {}; + nVec.x = pVec.x / length; + nVec.y = pVec.y / length; + array = [ + from.x + nVec.x * thickness, + from.y + nVec.y * thickness, + from.x - nVec.x * thickness, + from.y - nVec.y * thickness, + to.x + nVec.x * thickness, + to.y + nVec.y * thickness, + to.x - nVec.x * thickness, + to.y - nVec.y * thickness + ]; + g.fillPoly(array); +}; + + + +const drawHands = () => { + drawMinuteHand(); + drawHourHand(); + if (decoration) { + drawDecoration(); + } +}; + +const drawDecoration = () => { + params = { + start: 210, + arc: 295, + radius: 0.7, + x: 0, + y: 0 + }; + drawSegment(params); + params = { + start: 290, + arc: 135, + radius: 0.4, + x: 0, + y: -0.7 + }; + drawSegment(params); + params = { + start: 0, + arc: 360, + radius: 0.4, + x: 0, + y: 0.3 + }; + drawSegment(params); + params = { + start: 0, + arc: 360, + radius: 0.15, + x: 0, + y: 0.3 + }; + drawSegment(params); + params = { + start: 0, + arc: 360, + radius: 0.15, + x: 0.7, + y: 0 + }; + drawSegment(params); + params = { + fromX: 0.4, + fromY: 0.2, + toX: 0.6, + toY: 0.1 + }; + drawThickLine(params); + params = { + fromX: -0.2, + fromY: -0.05, + toX: -0.7, + toY: -0.7 + }; + drawThickLine(params); + params = { + fromX: -0.3, + fromY: 0.05, + toX: -0.95, + toY: -0.3 + }; + drawThickLine(params); +}; + +const drawMinuteHand = () => { + angle = currentDate.getMinutes()/60 * cirRad; + //angle = currentDate.getSeconds()/60 * cirRad; + switch(colour) { + case "red": + g.setColor(1,0,0); + break; + case "green": + g.setColor(0,1,0); + break; + case "blue": + g.setColor(0,0,1); + break; + case "80s": + g.setColor(1,0,0); + break; + default: + g.setColor(0,1,0); + } + + var points = [centerX,centerY]; + for (i = 0; i < angle; i=i+cirRad/60) { + points.push(Math.round(centerX + Math.sin(i) * radius), + Math.round(centerY - Math.cos(i) * radius)); + } + g.fillPoly(points); +}; + +const drawHourHand = () => { + g.setColor(0,0,0); + //angle = currentDate.getMinutes()/60 * cirRad; + angle = currentDate.getHours()/12 * cirRad; + g.fillCircle( + Math.round(centerX + Math.sin(angle) * radius * (1-proportion)), + Math.round(centerY - Math.cos(angle) * radius * (1-proportion)), + radius * proportion + ); +}; + +const drawClockFace = () => { + switch(colour) { + case "red": + g.setColor(0.8,0.3,0); + break; + case "green": + g.setColor(0.1,0.7,0); + break; + case "blue": + g.setColor(0,0.3,0.8); + break; + case "80s": + g.setColor(1,1,1); + break; + default: + g.setColor(0.1,0.7,0); + } + g.fillCircle(centerX,centerY,radius*0.98); + }; + +const drawAll = () => { + currentDate = new Date(); + g.clear(); + if (widgets) {Bangle.drawWidgets();} + drawClockFace(); + drawHands(); +}; + + +const startTimers = () => { + //timer = setInterval(drawAll, 1000); + timer = setInterval(drawAll, 1000*20); +}; + +Bangle.on('lcdPower', (on) => { + if (on) { + startTimers(); + drawAll(); + } else { + if (timer) { + clearInterval(timer); + } + } +}); + +g.clear(); +startTimers(); +Bangle.loadWidgets(); +drawAll(); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); diff --git a/apps/gallifr/gallifr.png b/apps/gallifr/gallifr.png new file mode 100644 index 000000000..9bb50e3cd Binary files /dev/null and b/apps/gallifr/gallifr.png differ diff --git a/apps/gallifr/settings.js b/apps/gallifr/settings.js new file mode 100644 index 000000000..bf6aae846 --- /dev/null +++ b/apps/gallifr/settings.js @@ -0,0 +1,33 @@ +// make sure to enclose the function in parentheses +(function (back) { + let settings = require('Storage').readJSON('gallifr.json',1)||{}; + let colours = ["green","red","blue","80s"]; + let onoff = ["on","off"]; + function save(key, value) { + settings[key] = value; + require('Storage').writeJSON('gallifr.json',settings); + } + const appMenu = { + '': {'title': 'Clock Settings'}, + '< Back': back, + 'Colour': { + value: 0|settings['colour'], + min:0,max:3, + format: m => colours[m], + onchange: m => {save('colour', m)} + }, + 'Widgets': { + value: 0|settings['widgets'], + min:0,max:1, + format: m => onoff[m], + onchange: m => {save('widgets', m)} + }, + 'Decoration': { + value: 0|settings['decoration'], + min:0,max:1, + format: m => onoff[m], + onchange: m => {save('decoration', m)} + } + }; + E.showMenu(appMenu) + }) diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index e02ef176d..f23a4eb6d 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -6,3 +6,6 @@ Optimize animation, limit title length 0.06: Gadgetbridge App 'Connected' state is no longer toggleable 0.07: Move configuration to settings menu +0.08: Don't turn on LCD at start of every song +0.09: Update Bluetooth connection state automatically +0.10: Make widget play well with other Gadgetbridge widgets/apps diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index 3f9c7053f..a87b9d1ec 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -16,7 +16,8 @@ Bluetooth.println(JSON.stringify(message)); } - function showNotification(size, render) { + function showNotification(size, render, turnOn) { + if (turnOn === undefined) turnOn = true var oldMode = Bangle.getLCDMode(); Bangle.setLCDMode("direct"); @@ -31,7 +32,7 @@ g.fillRect(238, 240, 239, 319); g.fillRect(2, 318, 238, 319); - Bangle.setLCDPower(1); // light up + if (turnOn) Bangle.setLCDPower(1); // light up Bangle.setLCDMode(oldMode); // clears cliprect function anim() { @@ -97,6 +98,7 @@ } function handleMusicStateUpdate(event) { + const changed = state.music === event.state state.music = event.state if (state.music == "play") { @@ -113,7 +115,7 @@ g.setFont("6x8", 1); g.setColor("#ffffff"); g.drawString(state.musicInfo.track, x, y + 22); - }); + }, changed); } if (state.music == "pause") { @@ -143,6 +145,7 @@ } } + var _GB = global.GB; global.GB = (event) => { switch (event.t) { case "notify": @@ -158,6 +161,7 @@ handleCallEvent(event); break; } + if(_GB)setTimeout(_GB,0,event); }; // Touch control @@ -187,8 +191,8 @@ g.flip(); // turns screen on } - NRF.on("connected", changedConnectionState); - NRF.on("disconnected", changedConnectionState); + NRF.on("connect", changedConnectionState); + NRF.on("disconnect", changedConnectionState); WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw }; diff --git a/apps/getup/ChangeLog b/apps/getup/ChangeLog new file mode 100644 index 000000000..9297fc6c7 --- /dev/null +++ b/apps/getup/ChangeLog @@ -0,0 +1 @@ +0.01: First Version \ No newline at end of file diff --git a/apps/getup/README.md b/apps/getup/README.md new file mode 100644 index 000000000..b92bedb7c --- /dev/null +++ b/apps/getup/README.md @@ -0,0 +1,7 @@ +# Get Up + +Reminds you to getup every x minutes (default: 20). + +Sitting to long is dangerous! + +Sit and move time configurable in settings. diff --git a/apps/getup/app-icon.js b/apps/getup/app-icon.js new file mode 100644 index 000000000..09010684e --- /dev/null +++ b/apps/getup/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A75wACCJugAAguaGBouFGCwuF53NFxem6PX6/R0wwVF4xgJEwOsFoMrlYDB1gwUL55dCFQIvE65hUL54jBRgQvF6JgaRxQpCF4SUC67BV5ouLF40yGAOBF64ANR4vXwJhCR6oABq4ACF5TvDAAOsL4LvS4wuGGBi6DGIYuSAAQvGMJiSC6JdSGAovPGAQAFXSQvDrgrBqwvMGAzqTF4d/F4owLADKQGmQv/F7eAF4UySQQwn0ZcCq0ylkySFYyDMEgvDvQwFAAYvk0aLBqy/CAAaUhSAi+BX4QzCAwJkgF4eAX4gzDSsIvDeIzFlGAhhEF9QAHBwIwvF8IwSF7oxMF8gALSEQwRF/4v/YH4v/GD4usAH4A/AH4ARA=")) diff --git a/apps/getup/app.js b/apps/getup/app.js new file mode 100644 index 000000000..8e4a4a099 --- /dev/null +++ b/apps/getup/app.js @@ -0,0 +1,45 @@ +//init settings +const storage = require("Storage"); +const SETTINGS_FILE = 'getup.settings.json'; + +function setting(key) { + const DEFAULTS = { + 'sitTime' : 20, + 'moveTime' : 1 + } + if (!settings) { + loadSettings(); + } + return (key in settings) ? settings[key] : DEFAULTS[key]; +} + +let settings; + +function loadSettings() { + settings = storage.readJSON(SETTINGS_FILE, 1) || {}; +} + +//vibrate, draw move message and start timer for sitting message +function remind() { + Bangle.buzz(1000,1); + g.clear(); + g.setFont("8x12",4); + g.setColor(0x03E0); + g.drawString("MOVE!", g.getWidth()/2, g.getHeight()/2); + setTimeout(print_message,setting("moveTime") * 60000); +} +//draw sitting message and start timer for reminder +function print_message(){ + g.clear(); + g.setFont("8x12",2); + g.setColor(0xF800); + g.drawString("sitting is dangerous!", g.getWidth()/2, g.getHeight()/2); + setTimeout(remind,setting("sitTime") * 60000); +} + +//init graphics +require("Font8x12").add(Graphics); +g.setFontAlign(0,0); +g.flip(); + +print_message(); diff --git a/apps/getup/app.png b/apps/getup/app.png new file mode 100644 index 000000000..fec421183 Binary files /dev/null and b/apps/getup/app.png differ diff --git a/apps/getup/settings.js b/apps/getup/settings.js new file mode 100644 index 000000000..f34262f2a --- /dev/null +++ b/apps/getup/settings.js @@ -0,0 +1,48 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'getup.settings.json'; + + // initialize with default settings... + let s = { + 'sitTime' : 20, + 'moveTime' : 1 + }; + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + const saved = storage.readJSON(SETTINGS_FILE, 1) || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + }; + } + + const menu = { + '': { 'title': 'Get Up' }, + '< Back': back, + 'Sit time (min)': { + value: s.sitTime, + min: 0, + max: 10000, + step: 1, + onchange: save('sitTime'), + }, + 'Move time (min)': { + value: s.moveTime, + min: 0, + max: 5000, + step: 1, + onchange: save('moveTime'), + }, + }; + E.showMenu(menu); +}); diff --git a/apps/gpsinfo/ChangeLog b/apps/gpsinfo/ChangeLog index 50d79e72d..90ace259c 100644 --- a/apps/gpsinfo/ChangeLog +++ b/apps/gpsinfo/ChangeLog @@ -1 +1,2 @@ 0.02: Ensure screen doesn't display garbage at startup +0.03: Show number of satellites while waiting for fix \ No newline at end of file diff --git a/apps/gpsinfo/gps-info.js b/apps/gpsinfo/gps-info.js index f7daf245a..836e3a71b 100644 --- a/apps/gpsinfo/gps-info.js +++ b/apps/gpsinfo/gps-info.js @@ -52,7 +52,11 @@ function onGPS(fix) { g.setFont("6x8", 2); g.drawString("Waiting for GPS", 120, 80); nofix = (nofix+1) % 4; - g.drawString(".".repeat(nofix) + " ".repeat(4-nofix), 120, 120) + g.drawString(".".repeat(nofix) + " ".repeat(4-nofix), 120, 120); + // Show number of satellites: + g.setFontAlign(0,0); + g.setFont("6x8"); + g.drawString(fix.satellites+" satellites", 120, 100); } g.flip(); } diff --git a/apps/gpsnav/ChangeLog b/apps/gpsnav/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gpsnav/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gpsnav/README.md b/apps/gpsnav/README.md new file mode 100644 index 000000000..80c6c1d00 --- /dev/null +++ b/apps/gpsnav/README.md @@ -0,0 +1,66 @@ +## gpsnav - navigate to waypoints + +The app is aimed at small boat navigation although it can also be used to mark the location of your car, bicycle etc and then get directions back to it. Please note that it would be foolish in the extreme to rely on this as your only boat navigation aid! + +The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix. + +![](first_screen.jpg) + +The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn white. Then use BTN1 and BTN3 to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected. + +![](waypoint_screen.jpg) + +The display shows that Stone Henge is 108.75Km from the location where I made the screenshot and the direction is 255 degrees - approximately west. The display shows that I am currently moving approximately north - albeit slowly!. The position of the blue circle indicates that I need to turn left to get on course to Stone Henge. When the circle and red triangle line up you are on course and course will equal bearing. + +### Marking Waypoints + +The app lets you mark your current location as follows. There are vacant slots in the waypoint file which can be allocated a location. In the distributed waypoint file these are labelled WP0 to WP4. Select one of these - WP2 is shown below. + +![](select_screen.jpg) + +Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2. + +![](marked_screen.jpg) + +The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot as you have just marked your current location. + +### Waypoint JSON file + +When the app is loaded from the app loader, a file named waypoints.json is loaded along with the javascript etc. The file has the following contents: + +~~~ +[ + { + "mark":0, + "name":"NONE" + }, + { + "mark":1, + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "mark":1, + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] +~~~ + +The file contains the initial NONE waypoint which is useful if you just want to display course and speed. The next two entries are waypoints to No 10 Downing Street and to Stone Henge - obtained from Google Maps. The last five entries are entries which can be *marked*. + +You add and delete entries using the Web IDE to load and then save the file from and to watch storage. The app itself does not limit the number of entries although it does load the entire file into RAM which will obviously limit this. + +I plan to release an accompanying watch app to edit waypoint files in the near future and a way to download your own waypoint file using the app loader. + + + + + diff --git a/apps/gpsnav/app-icon.js b/apps/gpsnav/app-icon.js new file mode 100644 index 000000000..890981d5a --- /dev/null +++ b/apps/gpsnav/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFmACysDC9+IC6szC/8AgUgLwYXBPAgLDAA8kC5MyC5cyogXHmYiDURMkDAMzC4JgBmcyoAXMGANCC4YDBkgXMHwVEC4hQDC5kyF4kjJ4QAMOgMjC4eCohGNMARbCC4ODkilLAAQSBCYJ3EmYVLhAWCCgQaCAAUwCpowCFwYADIRAYHC4wZFRQIAGnAhJXgwAFxAYHwC9JFwiQCFhIZISAQwDX5sCoQTCDYUjUpAAFglElAXDmS9JAAtEoUyC4ckkbvMC4QQBC4YeBC5sEB4IXEkgfBJBkEH4QXCCYMkoQXMHwcIC4ZQCUpYMDC4oiBC5YEDC40AkCRNAAIXBCJ4X2URgAJhAXvCyoA/ACoA=")) \ No newline at end of file diff --git a/apps/gpsnav/app.js b/apps/gpsnav/app.js new file mode 100644 index 000000000..2a480410c --- /dev/null +++ b/apps/gpsnav/app.js @@ -0,0 +1,224 @@ +const Yoff = 40; +var pal2color = new Uint16Array([0x0000,0xffff,0x07ff,0xC618],0,2); +var buf = Graphics.createArrayBuffer(240,50,2,{msb:true}); + +function flip(b,y) { + g.drawImage({width:240,height:50,bpp:2,buffer:b.buffer, palette:pal2color},0,y); + b.clear(); +} + +var brg=0; +var wpindex=0; +const labels = ["N","NE","E","SE","S","SW","W","NW"]; + +function drawCompass(course) { + buf.setColor(1); + buf.setFont("Vector",16); + var start = course-90; + if (start<0) start+=360; + buf.fillRect(28,45,212,49); + var xpos = 30; + var frag = 15 - start%15; + if (frag<15) xpos+=frag; else frag = 0; + for (var i=frag;i<=180-frag;i+=15){ + var res = start + i; + if (res%90==0) { + buf.drawString(labels[Math.floor(res/45)%8],xpos-8,0); + buf.fillRect(xpos-2,25,xpos+2,45); + } else if (res%45==0) { + buf.drawString(labels[Math.floor(res/45)%8],xpos-12,0); + buf.fillRect(xpos-2,30,xpos+2,45); + } else if (res%15==0) { + buf.fillRect(xpos,35,xpos+1,45); + } + xpos+=15; + } + if (wpindex!=0) { + var bpos = brg - course; + if (bpos>180) bpos -=360; + if (bpos<-180) bpos +=360; + bpos+=120; + if (bpos<30) bpos = 14; + if (bpos>210) bpos = 226; + buf.setColor(2); + buf.fillCircle(bpos,40,8); + } + flip(buf,Yoff); +} + +//displayed heading +var heading = 0; +function newHeading(m,h){ + var s = Math.abs(m - h); + var delta = 1; + if (s<2) return h; + if (m > h){ + if (s >= 180) { delta = -1; s = 360 - s;} + } else if (m <= h){ + if (s < 180) delta = -1; + else s = 360 -s; + } + delta = delta * (1 + Math.round(s/15)); + heading+=delta; + if (heading<0) heading += 360; + if (heading>360) heading -= 360; + return heading; +} + +var course =0; +var speed = 0; +var satellites = 0; +var wp; +var dist=0; + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function bearing(a,b){ + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); +} + +function distance(a,b){ + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + +var selected = false; + +function drawN(){ + buf.setColor(1); + buf.setFont("6x8",2); + buf.drawString("o",100,0); + buf.setFont("6x8",1); + buf.drawString("kph",220,40); + buf.setFont("Vector",40); + var cs = course.toString(); + cs = course<10?"00"+cs : course<100 ?"0"+cs : cs; + buf.drawString(cs,10,0); + var txt = (speed<10) ? speed.toFixed(1) : Math.round(speed); + buf.drawString(txt,140,4); + flip(buf,Yoff+70); + buf.setColor(1); + buf.setFont("Vector",20); + var bs = brg.toString(); + bs = brg<10?"00"+bs : brg<100 ?"0"+bs : bs; + buf.setColor(3); + buf.drawString("Brg: ",0,0); + buf.drawString("Dist: ",0,30); + buf.setColor(selected?1:2); + buf.drawString(wp.name,140,0); + buf.setColor(1); + buf.drawString(bs,60,0); + if (dist<1000) + buf.drawString(dist.toString()+"m",60,30); + else + buf.drawString((dist/1000).toFixed(2)+"Km",60,30); + flip(buf,Yoff+130); + g.setFont("6x8",1); + g.setColor(0,0,0); + g.fillRect(10,230,60,239); + g.setColor(1,1,1); + g.drawString("Sats " + satellites.toString(),10,230); +} + +var savedfix; + +function onGPS(fix) { + savedfix = fix; + if (fix!==undefined){ + course = isNaN(fix.course) ? course : Math.round(fix.course); + speed = isNaN(fix.speed) ? speed : fix.speed; + satellites = fix.satellites; + } + if (Bangle.isLCDOn()) { + if (fix!==undefined && fix.fix==1){ + dist = distance(fix,wp); + if (isNaN(dist)) dist = 0; + brg = bearing(fix,wp); + if (isNaN(brg)) brg = 0; + } + drawN(); + } +} + +var intervalRef; + +function clearTimers() { + if(intervalRef) {clearInterval(intervalRef);} +} + +function startTimers() { + intervalRefSec = setInterval(function() { + newHeading(course,heading); + if (course!=heading) drawCompass(heading); + },200); +} + +Bangle.on('lcdPower',function(on) { + if (on) { + g.clear(); + Bangle.drawWidgets(); + startTimers(); + drawAll(); + }else { + clearTimers(); + } +}); + +function drawAll(){ + g.setColor(1,0.5,0.5); + g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]); + g.setColor(1,1,1); + drawN(); + drawCompass(heading); +} + +var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; +wp=waypoints[0]; + +function nextwp(inc){ + if (!selected) return; + wpindex+=inc; + if (wpindex>=waypoints.length) wpindex=0; + if (wpindex<0) wpindex = waypoints.length-1; + wp = waypoints[wpindex]; + drawN(); +} + +function doselect(){ + if (selected && waypoints[wpindex].mark===undefined && savedfix.fix) { + waypoints[wpindex] ={mark:1, name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon}; + wp = waypoints[wpindex]; + require("Storage").writeJSON("waypoints.json", waypoints); + } + selected=!selected; + drawN(); +} + +g.clear(); +Bangle.setLCDBrightness(1); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// load widgets can turn off GPS +Bangle.setGPSPower(1); +drawAll(); +startTimers(); +Bangle.on('GPS', onGPS); +// Toggle selected +setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"}); +setWatch(doselect, BTN2, {repeat:true,edge:"falling"}); +setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"}); + diff --git a/apps/gpsnav/first_screen.jpg b/apps/gpsnav/first_screen.jpg new file mode 100644 index 000000000..34fbe1b50 Binary files /dev/null and b/apps/gpsnav/first_screen.jpg differ diff --git a/apps/gpsnav/gpsnav.jpg b/apps/gpsnav/gpsnav.jpg new file mode 100644 index 000000000..975fe3903 Binary files /dev/null and b/apps/gpsnav/gpsnav.jpg differ diff --git a/apps/gpsnav/icon.png b/apps/gpsnav/icon.png new file mode 100644 index 000000000..f899683d1 Binary files /dev/null and b/apps/gpsnav/icon.png differ diff --git a/apps/gpsnav/marked_screen.jpg b/apps/gpsnav/marked_screen.jpg new file mode 100644 index 000000000..accd3b15f Binary files /dev/null and b/apps/gpsnav/marked_screen.jpg differ diff --git a/apps/gpsnav/select_screen.jpg b/apps/gpsnav/select_screen.jpg new file mode 100644 index 000000000..8e42411b0 Binary files /dev/null and b/apps/gpsnav/select_screen.jpg differ diff --git a/apps/gpsnav/waypoint_screen.jpg b/apps/gpsnav/waypoint_screen.jpg new file mode 100644 index 000000000..f4c946ee6 Binary files /dev/null and b/apps/gpsnav/waypoint_screen.jpg differ diff --git a/apps/gpsnav/waypoints.json b/apps/gpsnav/waypoints.json new file mode 100644 index 000000000..143316b19 --- /dev/null +++ b/apps/gpsnav/waypoints.json @@ -0,0 +1,23 @@ +[ + { + "mark":0, + "name":"NONE" + }, + { + "mark":1, + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "mark":1, + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] \ No newline at end of file diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index 9a47bdd9a..469671b38 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -4,3 +4,9 @@ 0.04: Properly Fix GPS time display in gpsrec app 0.05: Tweaks for variable size widget system 0.06: Ensure widget update itself (fix #118) and change to using icons +0.07: Added @jeffmer's awesome track viewer +0.08: Don't overwrite existing settings on app update + Clean up recorded tracks on app removal +0.09: Change default GPS period to 10 (1 is overkill for most uses and makes things slow) + Added RAM keyword to functions & other tweaks to speed up rendering + Going 'back' from track view now doesn't load again diff --git a/apps/gpsrec/app-settings.json b/apps/gpsrec/app-settings.json index 7e1c8ee72..4265e46ec 100644 --- a/apps/gpsrec/app-settings.json +++ b/apps/gpsrec/app-settings.json @@ -1,5 +1,5 @@ { "recording":false, "file":0, - "period":1 + "period":10 } diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js index 58b4295a6..aeea18bc2 100644 --- a/apps/gpsrec/app.js +++ b/apps/gpsrec/app.js @@ -60,7 +60,7 @@ function viewTracks() { for (var n=0;n<36;n++) { var f = require("Storage").open(getFN(n),"r"); if (f.readLine()!==undefined) { - menu["Track "+n] = viewTrack.bind(null,n); + menu["Track "+n] = viewTrack.bind(null,n,false); found = true; } } @@ -70,27 +70,68 @@ function viewTracks() { return E.showMenu(menu); } -function viewTrack(n) { +function getTrackInfo(fn) { + "ram" + var filename = getFN(fn); + var minLat = 90; + var maxLat = -90; + var minLong = 180; + var maxLong = -180; + var starttime, duration=0; + var f = require("Storage").open(filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var nl = 0, c, n; + if (l!==undefined) { + c = l.split(","); + starttime = parseInt(c[0]); + } + // pushed this loop together to try and bump loading speed a little + while(l!==undefined) { + ++nl;c=l.split(","); + n = +c[1];if(n>maxLat)maxLat=n;if(nmaxLong)maxLong=n;if(nylen ? 200/xlen : 200/ylen; + return { + fn : fn, + filename : filename, + time : new Date(starttime), + records : nl, + minLat : minLat, maxLat : maxLat, + minLong : minLong, maxLong : maxLong, + lfactor : lfactor, + scale : scale, + duration : Math.round(duration/1000) + }; +} + +function asTime(v){ + var mins = Math.floor(v/60); + var secs = v-mins*60; + return ""+mins.toString()+"m "+secs.toString()+"s"; +} + +function viewTrack(n, info) { + if (!info) { + E.showMessage("Loading...","GPS Track "+n); + info = getTrackInfo(n); + } const menu = { '': { 'title': 'GPS Track '+n } }; - var trackCount = 0; - var trackTime; - var f = require("Storage").open(getFN(n),"r"); - var l = f.readLine(); - if (l!==undefined) { - var c = l.split(","); - trackTime = new Date(parseInt(c[0])); - } - while (l!==undefined) { - trackCount++; - // TODO: min/max/length of track? - l = f.readLine(); - } - if (trackTime) - menu[" "+trackTime.toISOString().substr(0,16).replace("T"," ")] = function(){}; - menu[trackCount+" records"] = function(){}; - // TODO: option to draw it? Just scan through, project using min/max + if (info.time) + menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){}; + menu["Duration"] = { value : asTime(info.duration)}; + menu["Records"] = { value : ""+info.records }; + menu['Plot'] = function() { + plotTrack(info); + }; menu['Erase'] = function() { E.showPrompt("Delete Track?").then(function(v) { if (v) { @@ -100,11 +141,80 @@ function viewTrack(n) { f.erase(); viewTracks(); } else - viewTrack(n); + viewTrack(n, info); }); }; menu['< Back'] = viewTracks; return E.showMenu(menu); } +function plotTrack(info) { + "ram" + + function radians(a) { + return a*Math.PI/180; + } + + function distance(lat1,long1,lat2,long2){ + var x = radians(long1-long2) * Math.cos(radians((lat1+lat2)/2)); + var y = radians(lat2-lat1); + return Math.sqrt(x*x + y*y) * 6371000; + } + + E.showMenu(); // remove menu + g.setColor(1,0.5,0.5); + g.setFont("Vector",16); + g.fillRect(9,80,11,120); + g.fillPoly([9,60,19,80,0,80]); + g.setColor(1,1,1); + g.drawString("N",2,40); + g.drawString("Track"+info.fn.toString()+" - Loading",10,220); + g.setColor(0,0,0); + g.fillRect(0,220,239,239); + g.setColor(1,1,1); + g.drawString(asTime(info.duration),10,220); + var f = require("Storage").open(info.filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var ox=0; + var oy=0; + var olat,olong,dist=0; + var i=0; + var c = l.split(","); + var lat = +c[1]; + var long = +c[2]; + var x = 30 + Math.round((long-info.minLong)*info.lfactor*info.scale); + var y = 210 - Math.round((lat - info.minLat)*info.scale); + g.moveTo(x,y); + g.setColor(0,1,0); + g.fillCircle(x,y,5); + g.setColor(1,1,1); + l = f.readLine(f); + while(l!==undefined) { + c = l.split(","); + lat = +c[1]; + long = +c[2]; + x = 30 + Math.round((long-info.minLong)*info.lfactor*info.scale); + y = 210 - Math.round((lat - info.minLat)*info.scale); + g.lineTo(x,y); + var d = distance(olat,olong,lat,long); + if (!isNaN(d)) dist+=d; + olat = lat; + olong = long; + ox = x; + oy = y; + l = f.readLine(f); + } + g.setColor(1,0,0); + g.fillCircle(ox,oy,5); + g.setColor(1,1,1); + g.drawString(require("locale").distance(dist),120,220); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + g.drawString("Back",230,200); + setWatch(function() { + viewTrack(info.fn, info); + }, BTN3); +} + showMainMenu(); diff --git a/apps/gpsrec/widget.js b/apps/gpsrec/widget.js index 2ad0cfc8c..dae613cd2 100644 --- a/apps/gpsrec/widget.js +++ b/apps/gpsrec/widget.js @@ -41,7 +41,7 @@ // Called by the GPS app to reload settings and decide what to do function reload() { settings = require("Storage").readJSON("gpsrec.json",1)||{}; - settings.period = settings.period||1; + settings.period = settings.period||10; settings.file |= 0; Bangle.removeListener('GPS',onGPS); diff --git a/apps/hamloc/ChangeLog b/apps/hamloc/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/hamloc/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/hamloc/README.md b/apps/hamloc/README.md new file mode 100644 index 000000000..6a2a93a44 --- /dev/null +++ b/apps/hamloc/README.md @@ -0,0 +1,18 @@ +# QTH Locator + +Convert your current GPS location to the [Maidenhead](https://en.wikipedia.org/wiki/Maidenhead_Locator_System) locator system used by HAM amateur radio operators. + +## Description + +A Maidenhead locator compresses latitude and longitude into a short string of characters, which is similar in concept to the World Geographic Reference System or GEOREF. This position information is presented in a limited level of precision to limit the number of characters needed for its transmission using voice, Morse code, or any other operating mode. + +The chosen coding uses alternating pairs of letters and digits, like so: + +* BL11bh +## +* support Paul Brewer KI6CQ HamGridSquare.js +* support Chris Veness 2002-2012 LatLon library + +## Requests + +If you have any bug or feature request, please contact [Renaudgweb](https://github.com/renaudgweb/) diff --git a/apps/hamloc/app-icon.js b/apps/hamloc/app-icon.js new file mode 100644 index 000000000..175b492c7 --- /dev/null +++ b/apps/hamloc/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4ACwVeAAM6nQECJsYqDFYItCAgQ0DFrxWE2ZhBxOJA4ICBGwhbbxGtAYOz2etnWt2YwBnQCBGgWtxBjXJQQpBLgQABEQIDBMAgwE2YYBwRcUxOtDwgABGgYvGTQQLCMSRNGEQaODF4SQDHgYwUFIlfAgY2DYQQGDsouCGAQSEGBouEKANl1onEwQvGA4QACA4IwQaIISFFwIwDXwI2ExL2BB4YABJgwwKnQAkRpzYDAAbuBA4oABe4aaDD44uGxCNEQwStEdwgAFR4IHGF4iRBxJeLBwIdCcwgvJxLwFFIRgLBo4dBcwj2CF45xIKIxeLAww4FAAuINIYbKMAteso8FAwovPCQllQQtlF4gLFUYwvDsq/HfIShEDhCQDIgIAFnQHGBBITRnWIXwdfAAYGFSYblBNI5KBsobEDo4GCdxyFDr2tR4+tDQrwNF5YlEnQvJaZIvKr4RIEoovaSAIvyXArbBF6OJR6mCXAgvBDwIlFd5IfBd6tfSQQRGCgeIBJE6DIIAFDoIGGF4OCAgIAEnQHGBBITRnWIF4P+V4ZMCWxBoBRw5PBNI7IGnQuCSAIfE1oSB1oADF4WIXxAvHsoIFH4IvEdIJWEDg4vICRTuJSAwABxAcJF5A5C1ovKRwhgCwSQGF6CpEXxBeGMA59JZIIvGA4hhBLw+JF4xRFSBBNDFIhHFe4VfLxhgCAEguIMAJSB1oABSA4HEB4RwBAgQXHss6LxKRDPQTxBsovIRIdexCOGRpwwIr5gFAwbuEAoheBFyQwFRIrwDLwZuBdwgTBC4IuRYYowGRAq+BAoeCAoRABFyIABD4IsCGAgpFGoguBd4eIFyRiEFoIwCEoKJDRwYqCCAJcUGJICBK4JgDKgOtOIQtce4s6AAI2EBAgteAAizBGgYECwQsiAD4")) diff --git a/apps/hamloc/app.js b/apps/hamloc/app.js new file mode 100644 index 000000000..ac608a5f4 --- /dev/null +++ b/apps/hamloc/app.js @@ -0,0 +1,21 @@ +latLonToGridSquare=function(o,a){var t,e,n,s,l,i,r,h,M,f=-100,g=0,u="ABCDEFGHIJKLMNOPQRSTUVWX",d=u.toLowerCase();function N(o){return"number"==typeof o?o:"string"==typeof o?parseFloat(o):"function"==typeof o?parseFloat(o()):void E.showMessage("can't convert \ninput: "+o)}return"object"==typeof o?2===o.length?(f=N(o[0]),g=N(o[1])):"lat"in o&&"lon"in o?(f=N(o.lat),g=N(o.lon)):"latitude"in o&&"longitude"in o?(f=N(o.latitude),g=N(o.longitude)):E.showMessage("can't convert \nobject "+o):(f=N(o),g=N(a)),isNaN(f)&&E.showMessage("lat is NaN"),isNaN(g)&&E.showMessage("lon is NaN"),90===Math.abs(f)&&E.showMessage("grid invalid \nat N/S"),90{ - NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => { - NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], resolve); - }); - }); -}; +var sendHID; function showChars(x,chars) { var lines = Math.round(Math.sqrt(chars.length)*2); @@ -103,10 +97,24 @@ function startKeyboardHID() { }).then(startKeyboardHID); }; -if (!settings.HID) { - E.showMessage('HID disabled'); - setTimeout(load, 1000); -} else { +if (settings.HID=="kb" || settings.HID=="kbmedia") { + if (settings.HID=="kbmedia") { + sendHID = function(code) { + return new Promise(resolve=>{ + NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => { + NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], resolve); + }); + }); + }; + } else { + sendHID = function(code) { + return new Promise(resolve=>{ + NRF.sendHIDReport([0,0,code,0,0,0,0,0], () => { + NRF.sendHIDReport([0,0,0,0,0,0,0,0], resolve); + }); + }); + }; + } startKeyboardHID(); setWatch(() => { sendHID(44); // space @@ -114,4 +122,12 @@ if (!settings.HID) { setWatch(() => { sendHID(40); // enter }, BTN3, {repeat:true}); +} else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kb"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidbkbd.app.js"); + } else setTimeout(load, 1000); + }); } diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog new file mode 100644 index 000000000..480d7d448 --- /dev/null +++ b/apps/hidcam/ChangeLog @@ -0,0 +1,3 @@ +0.01: Core functionnality +0.02: Offer to enable HID if disabled +0.03: Adds Readme and tags to be used by App Loader diff --git a/apps/hidcam/README.md b/apps/hidcam/README.md new file mode 100644 index 000000000..5e8d40817 --- /dev/null +++ b/apps/hidcam/README.md @@ -0,0 +1,18 @@ +# Camera shutter + +Control the camera shutter from your phone using your watch + +## Usage + +1. In settings, enable HID for "Keyboard & Media". +2. Pair your watch to your phone. +3. Load your camera app on your phone. +4. There you go, launch the app on your watch and press button 2 to trigger the shutter ! + +## How does it work ? + +The app uses HID to send the key "Vol +", which is a shortcut for camera trigger on Android and iOS. + +## Creator + +Paul Charlet, using code from HID music app. \ No newline at end of file diff --git a/apps/hidcam/app-icon.js b/apps/hidcam/app-icon.js new file mode 100644 index 000000000..aa9d5e194 --- /dev/null +++ b/apps/hidcam/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDCEAzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMxERERERETMzMzMzMzMzMzMzMzMzMzMzMRERERERERMzMzMzMzMzMzMzMzMzMzMzMREREREREREzMzMzMzMzMzMzMzMAAAAzEREREREREREzMzMzMzMzMzMzMzMAAAAxERERERERERETMzMzMzMzMzMzMxERERERERERERERERERERERETMzMzMzMRERERERERERERERERERERERERMzMzMzEREREREREREAAAAAEREREREREREzMzMzEREREREREQAAAAAAABERESIiIREzMzMzEREREREREAAAAAAAAAERESIiIREzMzMzEREREREQAAAKqqqgAAABESIiIREzMzMzEREREREQAAqqqqqqoAABESIiIREzMzMzEREREREAAKqqqqqqqgAAEREREREzMzMzERERERAACqqqqqqqqqAAAREREREzMzMzERERERAAqqqiIiIqqqoAAREREREzMzMzqqqqqgAAqqoiIiIiKqoAAKqqqqozMzMzqqqqqgAKqqIiIiIiKqqgAKqqqqozMzMzqqqqqgAKqqIiqqqiKqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAAqqqqqqqqqqoAAKqqqqozMzMzqqqqqqAAqqqqqqqqqqoACqqqqqozMzMzqqqqqqAACqqqqqqqqqAACqqqqqozMzMzqqqqqqoAAKqqqqqqqgAAqqqqqqozMzMzqqqqqqoAAAqqqqqqoAAAqqqqqqozMzMzqqqqqqqgAAAKqqqgAAAKqqqqqqozMzMzqqqqqqqqAAAAAAAAAACqqqqqqqozMzMzqqqqqqqqqgAAAAAAAKqqqqqqqqozMzMzqqqqqqqqqqoAAAAAqqqqqqqqqqozMzMzOqqqqqqqqqqqqqqqqqqqqqqqqqMzMzMzM6qqqqqqqqqqqqqqqqqqqqqqqjMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMw==")) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js new file mode 100644 index 000000000..adb1a4b29 --- /dev/null +++ b/apps/hidcam/app.js @@ -0,0 +1,57 @@ +var storage = require('Storage'); + +const settings = storage.readJSON('setting.json',1) || { HID: false }; + +var sendHid, camShot, profile; + +if (settings.HID=="kbmedia") { + profile = 'camShutter'; + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([1,code], () => { + NRF.sendHIDReport([1,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; + camShot = function (cb) { sendHid(0x80, cb); }; +} else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kbmedia"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidcam.app.js"); + } else setTimeout(load, 1000); + }); +} +function drawApp() { + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + g.fillCircle(122,127,60); + g.drawImage(storage.read("hidcam.img"),100,105); + const d = g.getWidth() - 18; + + function c(a) { + return { + width: 8, + height: a.length, + bpp: 1, + buffer: (new Uint8Array(a)).buffer + }; + } + g.fillRect(180,130, 240, 124); +} + +if (camShot) { + setWatch(function(e) { + E.showMessage('camShot !'); + setTimeout(drawApp, 1000); + camShot(() => {}); + }, BTN2, { edge:"falling",repeat:true,debounce:50}); + + drawApp(); +} diff --git a/apps/hidcam/app.png b/apps/hidcam/app.png new file mode 100644 index 000000000..3f631a0d8 Binary files /dev/null and b/apps/hidcam/app.png differ diff --git a/apps/hidjoystick/app-icon.js b/apps/hidjoystick/app-icon.js new file mode 100644 index 000000000..21d10dd00 --- /dev/null +++ b/apps/hidjoystick/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4ADhvd6AWVAAIYTCwQABC9JGDJCYX/R+7XYgEE7tACycAgczmAX/C/4X/C6kBiMQCyoABDB0N7vdAgIWCAAIXjxAAQCwkIC6OAC/4X/C/4XbgAXRCwgA/AH4ANA")) diff --git a/apps/hidjoystick/app.js b/apps/hidjoystick/app.js new file mode 100644 index 000000000..0b3187a53 --- /dev/null +++ b/apps/hidjoystick/app.js @@ -0,0 +1,74 @@ +var storage = require('Storage'); +const settings = storage.readJSON('setting.json',1) || { HID: false }; + +var sendInProgress = false; // Only send one message at a time, do not flood + +const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) { + try { + const buttons = (btn5<<4) | (btn4<<3) | (btn3<<2) | (btn2<<1) | (btn1<<0); + if (!sendInProgress) { + sendInProgress = true; + NRF.sendHIDReport([buttons, x, y], () => { + sendInProgress = false; + if (cb) cb(); + }); + } + } catch(e) { + print(e); + } +}; + +function drawApp() { + g.clear(); + g.setFont("6x8",2); + g.setFontAlign(0,0); + g.drawString("Joystick", 120, 120); + const d = g.getWidth() - 18; + + function c(a) { + return { + width: 8, + height: a.length, + bpp: 1, + buffer: (new Uint8Array(a)).buffer + }; + } + + g.drawImage(c([16,56,124,254,16,16,16,16]),d,40); + g.drawImage(c([16,16,16,16,254,124,56,16]),d,194); + g.drawImage(c([0,8,12,14,255,14,12,8]),d,116); +} + +function update() { + const btn1 = BTN1.read(); + const btn2 = BTN2.read(); + const btn3 = BTN3.read(); + const btn4 = BTN4.read(); + const btn5 = BTN5.read(); + const acc = Bangle.getAccel(); + var x = acc.x*-127; + var y = acc.y*-127; + + // check limits + if (x > 127) x = 127; + else if (x < -127) x = -127; + if (y > 127) y = 127; + else if (y < -127) y = -127; + + sendHid(x & 0xff, y & 0xff, btn1, btn2, btn3, btn4, btn5); +} + +if (settings.HID === "joy") { + drawApp(); + setInterval(update, 100); // 10 Hz +} else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "joy"; + storage.write('setting.json', settings); + setTimeout(load, 1000, "hidjoystick.app.js"); + } else { + setTimeout(load, 1000); + } + }); +} diff --git a/apps/hidjoystick/app.png b/apps/hidjoystick/app.png new file mode 100644 index 000000000..aca42a818 Binary files /dev/null and b/apps/hidjoystick/app.png differ diff --git a/apps/hidkbd/ChangeLog b/apps/hidkbd/ChangeLog new file mode 100644 index 000000000..459bf40b9 --- /dev/null +++ b/apps/hidkbd/ChangeLog @@ -0,0 +1,2 @@ +0.01: Core functionnality +0.02: Offer to enable HID if disabled. Handle with/without media keys diff --git a/apps/hidkbd/hid-keyboard.js b/apps/hidkbd/hid-keyboard.js index ed406e093..0d489bc0d 100644 --- a/apps/hidkbd/hid-keyboard.js +++ b/apps/hidkbd/hid-keyboard.js @@ -4,27 +4,46 @@ const settings = storage.readJSON('setting.json',1) || { HID: false }; var sendHid, next, prev, toggle, up, down, profile; -if (settings.HID) { +if (settings.HID=="kb" || settings.HID=="kbmedia") { profile = 'Keyboard'; - sendHid = function (code, cb) { - try { - NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => { - NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], () => { - if (cb) cb(); + if (settings.HID=="kbmedia") { + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => { + NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], () => { + if (cb) cb(); + }); }); - }); - } catch(e) { - print(e); - } - }; + } catch(e) { + print(e); + } + }; + } else { + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([0,0,code,0,0,0,0,0], () => { + NRF.sendHIDReport([0,0,0,0,0,0,0,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; + } next = function (cb) { sendHid(0x4f, cb); }; prev = function (cb) { sendHid(0x50, cb); }; toggle = function (cb) { sendHid(0x2c, cb); }; up = function (cb) {sendHid(0x52, cb); }; down = function (cb) { sendHid(0x51, cb); }; } else { - E.showMessage('HID disabled'); - setTimeout(load, 1000); + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kb"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidkbd.app.js"); + } else setTimeout(load, 1000); + }); } function drawApp() { diff --git a/apps/hidmsic/ChangeLog b/apps/hidmsic/ChangeLog new file mode 100644 index 000000000..9e7c84e5d --- /dev/null +++ b/apps/hidmsic/ChangeLog @@ -0,0 +1,2 @@ +0.01: Core functionnality +0.02: Added BLE HID option for Joystick and bare Keyboard diff --git a/apps/hidmsic/hid-music.js b/apps/hidmsic/hid-music.js index 034bbd231..db81744f3 100644 --- a/apps/hidmsic/hid-music.js +++ b/apps/hidmsic/hid-music.js @@ -4,7 +4,7 @@ const settings = storage.readJSON('setting.json',1) || { HID: false }; var sendHid, next, prev, toggle, up, down, profile; -if (settings.HID) { +if (settings.HID=="kbmedia") { profile = 'Music'; sendHid = function (code, cb) { try { @@ -23,8 +23,13 @@ if (settings.HID) { up = function (cb) {sendHid(0x40, cb); }; down = function (cb) { sendHid(0x80, cb); }; } else { - E.showMessage('HID disabled'); - setTimeout(load, 1000); + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kbmedia"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidmsc.app.js"); + } else setTimeout(load, 1000); + }); } function drawApp() { diff --git a/apps/imgclock/122240.png b/apps/imgclock/122240.png new file mode 100644 index 000000000..14b3cf84b Binary files /dev/null and b/apps/imgclock/122240.png differ diff --git a/apps/imgclock/122271.png b/apps/imgclock/122271.png new file mode 100644 index 000000000..cd9b5e45f Binary files /dev/null and b/apps/imgclock/122271.png differ diff --git a/apps/imgclock/ChangeLog b/apps/imgclock/ChangeLog new file mode 100644 index 000000000..ae978f9f9 --- /dev/null +++ b/apps/imgclock/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App! +0.02: Add configurable color - and 'this is fine.' +0.03: Add {msb:true} so that on new builds, color is correct for 16 bit +0.04: Fix hour alignment for single digits + Scaling for background images <240px wide +0.05: Fix memory/interval leak when LCD turns on +0.06: Support 12 hour time \ No newline at end of file diff --git a/apps/imgclock/app-icon.js b/apps/imgclock/app-icon.js new file mode 100644 index 000000000..2189484d0 --- /dev/null +++ b/apps/imgclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AD1oAaF/4v2nAAUF/4v/F6WJAAQvbw/X69jF54xPF5YuBAAIvnrms64BBAAQGCrgvHnc7r4vYRYXX2QCE6+HF8/W5vN6wvNsqPb6/N5nM5qRCR5QvCsoACF9BeCF4YyKR7ovCYYIvYd6DuCss6nSVBSJSPKSISNCR5a+Cr4vWAAYvDBhAvisYuBsYvPnYvBYAQuIF5gAMF4buDLxovgRxwvcr4uBsqOOF7juELxovDADDuEF9TuERxovfLx4vedwIvtdwKOOAAR1CACIlBr4pBF4RePGDAAFnYvTAAIUCJgQvTRyIwIAAgvPLygyNGhVlF7QyNGgrzCFzQyQsouCF7wyQF8IyNF0YyLF84yHF9QyDF1oA/AH4AeA==")) diff --git a/apps/imgclock/app.js b/apps/imgclock/app.js new file mode 100644 index 000000000..dc961f58b --- /dev/null +++ b/apps/imgclock/app.js @@ -0,0 +1,81 @@ +/* +Draws a fullscreen image from flash memory +Saves a small image to flash which is just the area where the clock is +Keeps an offscreen buffer and draws the time to that +*/ +var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; +var inf = require("Storage").readJSON("imgclock.face.json"); +var img = require("Storage").read("imgclock.face.img"); +var IX = inf.x, IY = inf.y, IBPP = inf.bpp; +var IW = 110, IH = 45, OY = 24; +var bgwidth = img.charCodeAt(0); +var bgoptions; +if (bgwidth<240) + bgoptions = { scale : 240/bgwidth }; + +require("Font7x11Numeric7Seg").add(Graphics); +var cg = Graphics.createArrayBuffer(IW,IH,IBPP,{msb:true}); +var cgimg = {width:IW,height:IH,bpp:IBPP,buffer:cg.buffer}; +var locale = require("locale"); + +// store clock background image in bgimg (a file in flash memory) +var bgimg = require("Storage").read("imgclock.face.bg"); +// if it doesn't exist, make it +function createBgImg() { + cg.drawImage(img,-IX,-IY,bgoptions); + require("Storage").write("imgclock.face.bg", cg.buffer); + bgimg = require("Storage").read("imgclock.face.bg"); +} +if (!bgimg || !bgimg.length) createBgImg(); + +function draw() { + var t = new Date(); + var hours = t.getHours(); + var meridian = ""; + if (is12Hour) { + meridian = (hours < 12) ? "AM" : "PM"; + hours = ((hours + 11) % 12) + 1; + } + // quickly set background image + new Uint8Array(cg.buffer).set(bgimg); + // draw time + cg.setColor(inf.col); + var x = 40; + cg.setFont("7x11Numeric7Seg",3); + cg.setFontAlign(1,-1); + cg.drawString(hours, x, 0); + x+=2; + cg.fillRect(x, 10, x+2, 10+2).fillRect(x, 20, x+2, 20+2); + x+=6; + cg.setFontAlign(-1,-1); + cg.drawString(("0"+t.getMinutes()).substr(-2), x, 0); + x+=44; + cg.setFont("7x11Numeric7Seg",1); + cg.drawString(("0"+t.getSeconds()).substr(-2), x, 20); + cg.setFont("6x8",1); + cg.drawString(meridian, x+2, 0); + cg.setFontAlign(0,-1); + cg.drawString(locale.date(t),IW/2,IH-8); + // draw to screen + g.drawImage(cgimg,IX,IY+OY); +} + +// draw background +g.drawImage(img, 0,OY,bgoptions); +// draw clock itself and do it every second +draw(); +var secondInterval = setInterval(draw,1000); +// load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// Stop when LCD goes off +Bangle.on('lcdPower',on=>{ + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + if (on) { + secondInterval = setInterval(draw,1000); + draw(); + } +}); +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); diff --git a/apps/imgclock/app.png b/apps/imgclock/app.png new file mode 100644 index 000000000..237f3f82a Binary files /dev/null and b/apps/imgclock/app.png differ diff --git a/apps/imgclock/custom.html b/apps/imgclock/custom.html new file mode 100644 index 000000000..8c920571a --- /dev/null +++ b/apps/imgclock/custom.html @@ -0,0 +1,139 @@ + + + + + +
+
+
+
+ + + + + + + diff --git a/apps/imgclock/thisisfine.png b/apps/imgclock/thisisfine.png new file mode 100644 index 000000000..a7be57043 Binary files /dev/null and b/apps/imgclock/thisisfine.png differ diff --git a/apps/impwclock/ChangeLog b/apps/impwclock/ChangeLog new file mode 100644 index 000000000..c6974d37c --- /dev/null +++ b/apps/impwclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Stopped watchface from flashing every interval diff --git a/apps/impwclock/README.md b/apps/impwclock/README.md new file mode 100644 index 000000000..30e42c95e --- /dev/null +++ b/apps/impwclock/README.md @@ -0,0 +1,4 @@ +# Imprecise Word Clock + +This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Press button 1 to see the time in accurate, digital form. But do you really need to know the exact time? + diff --git a/apps/impwclock/clock-impword-icon.js b/apps/impwclock/clock-impword-icon.js new file mode 100644 index 000000000..f5ed47f1f --- /dev/null +++ b/apps/impwclock/clock-impword-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A3iIBEn8ggP//8wgX/+cQl8Agc/BQPyCokQgHzmEB+ET+EfmMj+AXCmABBF4MBiIABiEC+PxC4Uwn4NB+QXMBAMzI4UxmYOBC5sfCgIvBgPzF4cfC5BgCFAMPkPwiXzL4cPmMvkAXDPAnzEgMxR4wDCGITl/AH4ApgUQbIICBAgXwBYMD+UAYoP/l4CBiUhd4QXFgIXCh73BfQUfAgIPBC4cQiIACC4cvj4PBC5AuCC48zgcwC4ZHBC5sBCAIEBF5EAC4RgDCQItCPAIXLCoQBBFgM/IoZHER4QA/AH4Anj8wgXzgX/+cQWoPyYQK9Bn/zj/wb4MTCAMf+MDAYMxkfwj8BmYXBmEzCYMf+cDmPzkMvj8zAIM/eoPyC4fy+IXDl8TmfwI4UvmYABAwIXB//xgPwBIIXCgYFBmEP/8fh/yF4sDC4QjBC4RvBF4UPB4JUBL4kAn8ROIJbBC4IIBL4hDBmaPEgBuB+EB+aPCUQUjCALn/AH4A/A")) \ No newline at end of file diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js new file mode 100644 index 000000000..ff4e24e2c --- /dev/null +++ b/apps/impwclock/clock-impword.js @@ -0,0 +1,165 @@ +/* Imprecise Word Clock - A. Blanton +A remix of word clock +by Gordon Williams https://github.com/gfwilliams +- Changes the representation of time to be more general +- Shows accurate digital time when button 1 is pressed +*/ +/* jshint esversion: 6 */ + +const allWords = [ + "AEARLYDN", + "LATEYRZO", + "MORNINGO", + "KMIDDLEN", + "AFTERDAY", + "OFDZTHEC", + "EVENINGR", + "ORMNIGHT" +]; + + +const timeOfDay = { + 0: ["", 0, 0], + 1: ["EARLYMORNING", 10, 20, 30, 40, 50, 02, 12, 22, 32, 42, 52, 62], + 2: ["MORNING", 02, 12, 22, 32, 42, 52, 62], + 3: ["LATEMORNING", 01, 11, 21, 31, 02, 12, 22, 32, 42, 52, 62], + 4: ["MIDDAY", 13, 23, 33, 54, 64, 74], + 5: ["EARLYAFTERNOON", 10, 20, 30, 40, 50, 04, 14, 24, 34, 44, 70, 71, 72, 73], + 6: ["AFTERNOON", 04, 14, 24, 34, 44, 70, 71, 72, 73], + 7: ["LATEAFTERNOON", 01, 11, 21, 31, 04, 14, 24, 34, 44, 70, 71, 72, 73], + 8: ["EARLYEVENING", 10, 20, 30, 40, 50, 06, 16, 26, 36, 46, 56, 66], + 9: ["EVENING", 06, 16, 26, 36, 46, 56, 66], + 10: ["NIGHT", 37, 47, 57, 67, 77], + 11: ["MIDDLEOFTHENIGHT", 13, 23, 33, 43, 53, 63, 05, 15, 45, 55, 65, 37,47,57,67,77 ], +}; + + +// offsets and increments +const xs = 35; +const ys = 31; +const dy = 22; +const dx = 25; + +// font size and color +const fontSize = 3; // "6x8" +const passivColor = 0x3186 /*grey*/ ; +const activeColorNight = 0xF800 /*red*/ ; +const activeColorDay = 0xFFFF /* white */; + +var hidxPrev; + +function drawWordClock() { + + + // get time + var t = new Date(); + var h = t.getHours(); + var m = t.getMinutes(); + var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2); + var day = t.getDay(); + + var hidx; + + var activeColor = activeColorDay; + if(h < 7 || h > 19) {activeColor = activeColorNight;} + + g.setFont("6x8",fontSize); + g.setColor(passivColor); + g.setFontAlign(0, -1, 0); + + + // Switch case isn't good for this in Js apparently so... + if(h < 3){ + // Middle of the Night + hidx = 11; + } + else if (h < 7){ + // Early Morning + hidx = 1; + } + else if (h < 10){ + // Morning + hidx = 2; + } + else if (h < 12){ + // Late Morning + hidx = 3; + } + else if (h < 13){ + // Midday + hidx = 4; + } + else if (h < 14){ + // Early afternoon + hidx = 5; + } + else if (h < 16){ + // Afternoon + hidx = 6; + } + else if (h < 17){ + // Late Afternoon + hidx = 7; + } + else if (h < 19){ + // Early evening + hidx = 8; + } + else if (h < 21){ + // evening + hidx = 9; + } + else if (h < 24){ + // Night + hidx = 10; + } + + // check whether we need to redraw the watchface + if (hidx !== hidxPrev) { + // draw allWords + var c; + var y = ys; + var x = xs; + allWords.forEach((line) => { + x = xs; + for (c in line) { + g.drawString(line[c], x, y); + x += dx; + } + y += dy; + }); + + // write hour in active color + g.setColor(activeColor); + timeOfDay[hidx][0].split('').forEach((c, pos) => { + x = xs + (timeOfDay[hidx][pos + 1] / 10 | 0) * dx; + y = ys + (timeOfDay[hidx][pos + 1] % 10) * dy; + g.drawString(c, x, y); + }); + hidxPrev = hidx; + } + + // Display digital time while button 1 is pressed + g.clearRect(0, 215, 240, 240); + if (BTN1.read()){ + g.setColor(activeColor); + g.drawString(time, 120, 215); + } +} + + +Bangle.on('lcdPower', function(on) { + if (on) drawWordClock(); +}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setInterval(drawWordClock, 1E4); +drawWordClock(); + +// Show digital time while top button is pressed +setWatch(drawWordClock, BTN1, {repeat:true,edge:"both"}); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/impwclock/clock-impword.png b/apps/impwclock/clock-impword.png new file mode 100644 index 000000000..e7ed0e828 Binary files /dev/null and b/apps/impwclock/clock-impword.png differ diff --git a/apps/largeclock/ChangeLog b/apps/largeclock/ChangeLog new file mode 100644 index 000000000..fe44e5078 --- /dev/null +++ b/apps/largeclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: Init +0.02: fix 3/4 moon orientation +0.03: Change `largeclock.json` to 'data' file to allow settings to be preserved diff --git a/apps/largeclock/README.md b/apps/largeclock/README.md new file mode 100644 index 000000000..c9a325823 --- /dev/null +++ b/apps/largeclock/README.md @@ -0,0 +1,19 @@ +# Large clock + +A readable and informational digital watch, with date, seconds and moon phase and with programmable BTN1 & BTN3 + +## Features + +- Readable +- Informative: hours, minutes, secondsa, date, year and moon phase +- Pairs nicely with any other apps: in setting > large clock any installed app can be assigned to BTN1 and BTN3 in order to open it easily directly from the watch, without the hassle of passing trough the launcher. For example BTN1 can be assigned to alarm and BTN3 to chronometer. + +## How to use it + +- The clock can be used as any other one, if you like it just set it as the default clock app in settings > select clock +- In setting > large clock you can select which app is to be open by BTN1 and BTN3 + +## Credits + +- The clock face is heavily inspired by Big Clock byJeffmer https://jeffmer.github.io/JeffsBangleAppsDev/ +- The moon phase is basically the one from the widget https://github.com/espruino/BangleApps/tree/master/apps/widmp diff --git a/apps/largeclock/largeclock-icon.js b/apps/largeclock/largeclock-icon.js new file mode 100644 index 000000000..22aadc576 --- /dev/null +++ b/apps/largeclock/largeclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4ArmYAQCwkDC6MwFyowFC/4XKnGIAAIQFBAWDC5INCBwggEEIYXdxAODnAYCAYIgDDAQXECoIrDE4YrEBwYX/C/4X/C/4X8BwIKBAAM4DgQDBBAQDBBAIXFE4QOCA4QrCAAQHCC7wODCwYhEEAYXGACAX/C5cDCyMwC4YwSCwgA/AH4AlA=")) diff --git a/apps/largeclock/largeclock.js b/apps/largeclock/largeclock.js new file mode 100644 index 000000000..9975775fb --- /dev/null +++ b/apps/largeclock/largeclock.js @@ -0,0 +1,196 @@ +const REFRESH_RATE = 1000; + +let interval; +let lastMoonPhase; +let lastMinutes; + +const moonR = 12; +const moonX = 215; +const moonY = 50; + +const settings = require("Storage").readJSON("largeclock.json", 1); +const BTN1app = settings.BTN1 || ""; +const BTN3app = settings.BTN3 || ""; + +function drawMoon(d) { + const BLACK = 0, + MOON = 0x41f, + MC = 29.5305882, + NM = 694039.09; + + var moon = { + // reset + 0: () => { + g.setColor(BLACK).fillRect( + moonX - moonR, + moonY - moonR, + moonX + moonR, + moonY + moonR + ); + }, + // new moon + 1: () => { + moon[0](); + g.setColor(MOON).drawCircle(moonX, moonY, moonR); + }, + // 1/4 ascending + 2: () => { + moon[3](); + g.setColor(BLACK).fillEllipse( + moonX - moonR / 2, + moonY - moonR, + moonX + moonR / 2, + moonY + moonR + ); + }, + // 1/2 ascending + 3: () => { + moon[0](); + g.setColor(MOON) + .fillCircle(moonX, moonY, moonR) + .setColor(BLACK) + .fillRect(moonX, moonY - moonR, moonX + moonR + moonR, moonY + moonR); + }, + // 3/4 ascending + 4: () => { + moon[3](); + g.setColor(MOON).fillEllipse( + moonX - moonR / 2, + moonY - moonR, + moonX + moonR / 2, + moonY + moonR + ); + }, + // Full moon + 5: () => { + moon[0](); + g.setColor(MOON).fillCircle(moonX, moonY, moonR); + }, + // 3/4 descending + 6: () => { + moon[7](); + g.setColor(MOON).fillEllipse( + moonX - moonR / 2, + moonY - moonR, + moonX + moonR / 2, + moonY + moonR + ); + }, + // 1/2 descending + 7: () => { + moon[0](); + g.setColor(MOON) + .fillCircle(moonX, moonY, moonR) + .setColor(BLACK) + .fillRect(moonX - moonR, moonY - moonR, moonX, moonY + moonR); + }, + // 1/4 descending + 8: () => { + moon[7](); + g.setColor(BLACK).fillEllipse( + moonX - moonR / 2, + moonY - moonR, + moonX + moonR / 2, + moonY + moonR + ); + } + }; + + function moonPhase(d) { + var tmp, + month = d.getMonth(), + year = d.getFullYear(), + day = d.getDate(); + if (month < 3) { + year--; + month += 12; + } + tmp = (365.25 * year + 30.6 * ++month + day - NM) / MC; + return Math.round((tmp - (tmp | 0)) * 7 + 1); + } + + const currentMoonPhase = moonPhase(d); + if (currentMoonPhase != lastMoonPhase) { + moon[currentMoonPhase](); + lastMoonPhase = currentMoonPhase; + } +} + +function drawTime(d) { + const da = d.toString().split(" "); + const time = da[4].substr(0, 5).split(":"); + const dow = da[0]; + const month = da[1]; + const day = da[2]; + const year = da[3]; + const hours = time[0]; + const minutes = time[1]; + const seconds = d.getSeconds(); + if (minutes != lastMinutes) { + g.clearRect(0, 24, moonX - moonR - 10, 239); + g.setColor(1, 1, 1); + g.setFontAlign(-1, -1); + g.setFont("Vector", 100); + g.drawString(hours, 50, 24, true); + g.setColor(1, 50, 1); + g.drawString(minutes, 50, 135, true); + g.setFont("Vector", 20); + g.setRotation(3); + g.drawString(`${dow} ${day} ${month}`, 50, 15, true); + g.drawString(year, 75, 205, true); + lastMinutes = minutes; + } + g.setRotation(0); + g.setFont("Vector", 20); + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.clearRect(200, 210, 240, 240); + g.drawString(seconds, 215, 215); +} + +function drawClockFace() { + const d = new Date(); + drawTime(d); + drawMoon(d); +} + +Bangle.on("lcdPower", function(on) { + if (on) { + g.clear(); + Bangle.drawWidgets(); + drawClockFace(); + interval = setInterval(drawClockFace, REFRESH_RATE); + } else { + clearInterval(interval); + lastMinutes = undefined; + lastMoonPhase = undefined; + } +}); + +Bangle.setLCDMode(); + +// Show launcher when middle button pressed +clearWatch(); +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +if (BTN1app) setWatch( + function() { + load(BTN1app); + }, + BTN1, + { repeat: false, edge: "rising" } +); +if (BTN3app) setWatch( + function() { + load(BTN3app); + }, + BTN3, + { repeat: false, edge: "rising" } +); + +g.clear(); +clearInterval(); +drawClockFace(); +interval = setInterval(drawClockFace, REFRESH_RATE); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/largeclock/largeclock.json b/apps/largeclock/largeclock.json new file mode 100644 index 000000000..7c38d59de --- /dev/null +++ b/apps/largeclock/largeclock.json @@ -0,0 +1,4 @@ +{ + "BTN1": "timer.app.js", + "BTN3": "calendar.app.js" +} diff --git a/apps/largeclock/largeclock.png b/apps/largeclock/largeclock.png new file mode 100644 index 000000000..32e87e768 Binary files /dev/null and b/apps/largeclock/largeclock.png differ diff --git a/apps/largeclock/settings.js b/apps/largeclock/settings.js new file mode 100644 index 000000000..a33f3c438 --- /dev/null +++ b/apps/largeclock/settings.js @@ -0,0 +1,72 @@ +(function(back) { + const s = require("Storage"); + const apps = s + .list(/\.info$/) + .map(app => { + var a = s.readJSON(app, 1); + return ( + a && { + n: a.name, + t: a.type, + src: a.src + } + ); + }) + .filter(app => app && (app.t == "app" || app.t == "clock" || !app.t)) + .map(a => { + return { n: a.n, src: a.src }; + }); + apps.sort((a, b) => { + if (a.n < b.n) return -1; + if (a.n > b.n) return 1; + return 0; + }); + + const settings = s.readJSON("largeclock.json", 1) || { + BTN1: "", + BTN3: "" + }; + + function showApps(btn) { + function format(v) { + return v === settings[btn] ? "*" : ""; + } + + function onchange(v) { + settings[btn] = v; + s.writeJSON("largeclock.json", settings); + } + + const btnMenu = { + "": { + title: `Apps for ${btn}` + }, + "< Back": () => E.showMenu(mainMenu) + }; + + if (apps.length > 0) { + for (let i = 0; i < apps.length; i++) { + btnMenu[apps[i].n] = { + value: apps[i].src, + format: format, + onchange: onchange + }; + } + } else { + btnMenu["...No Apps..."] = { + value: undefined, + format: () => "", + onchange: () => {} + }; + } + return E.showMenu(btnMenu); + } + + const mainMenu = { + "": { title: "Large Clock Settings" }, + "< Back": back, + "BTN1 app": () => showApps("BTN1"), + "BTN3 app": () => showApps("BTN3") + }; + E.showMenu(mainMenu); +}); diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog new file mode 100644 index 000000000..fe5aa9d0e --- /dev/null +++ b/apps/launch/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Only store relevant app data (saves RAM when many apps) +0.03: Allow scrolling to wrap around (fix #382) diff --git a/apps/launch/app.js b/apps/launch/app.js index 682122f82..b20c808a1 100644 --- a/apps/launch/app.js +++ b/apps/launch/app.js @@ -1,5 +1,5 @@ var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>s.readJSON(app,1)||{name:"DEAD: "+app.substr(1)}).filter(app=>app.type=="app" || app.type=="clock" || !app.type); +var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src}}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first @@ -40,16 +40,14 @@ function drawMenu() { } drawMenu(); setWatch(function() { - if (selected>0) { - selected--; - drawMenu(); - } + selected--; + if (selected<0) selected = apps.length-1; + drawMenu(); }, BTN1, {repeat:true}); setWatch(function() { - if (selected+1=apps.length) selected = 0; + drawMenu(); }, BTN3, {repeat:true}); setWatch(function() { // run if (!apps[selected].src) return; diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index d46bbaea0..3d983150d 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -4,3 +4,5 @@ 0.04: Add function meridian 0.05: Inline locale details - faster, less memory overhead Add correct scaling for speed/distance/temperature +0.06: Remove translations if not required + Ensure 'on' is always supplied for translations diff --git a/apps/locale/locale.html b/apps/locale/locale.html index 5cb4b4598..21bf37f29 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -50,6 +50,7 @@ exports = { name : "en_GB", currencySym:"£", // do some sanity checks Object.keys(locales).forEach(function(localeName) { var locale = locales[localeName]; + if (locale.trans && !locale.trans.on) console.error(localeName+": If translations are provided, 'on' *must* be included"); if (distanceUnits[locale.distance[0]]===undefined) console.error(localeName+": Unknown distance unit "+locale.distance[0]); if (distanceUnits[locale.distance[1]]===undefined) console.error(localeName+": Unknown distance unit "+locale.distance[1]); if (speedUnits[locale.speed]===undefined) console.error(localeName+": Unknown speed unit "+locale.speed); @@ -131,7 +132,7 @@ exports = { distance: n => (n < ${distanceUnits[locale.distance[1]]}) ? Math.round(n/${distanceUnits[locale.distance[0]]}) + ${js(locale.distance[0])} : Math.round(n/${distanceUnits[locale.distance[1]]}) + ${js(locale.distance[1])}, speed: s => Math.round(s/${speedUnits[locale.speed]}) + ${js(locale.speed)}, temp: t => Math.round(${temperature}) + ${js(locale.temperature)}, - translate: s => {var t=${js(locale.trans)};s=""+s;return t[s]||t[s.toLowerCase()]||s;}, + translate: s => ${locale.trans?`{var t=${js(locale.trans)};s=""+s;return t[s]||t[s.toLowerCase()]||s;}`:`s`}, date: (d,short) => (short) ? \`${dateS}\`: \`${dateN}\`, time: (d,short) => (short) ? \`${timeS}\`: \`${timeN}\`, meridian: d => (d.getHours() <= 12) ? ${js(locale.ampm[0])}:${js(locale.ampm[1])}, diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 43e073cd1..cbc56b85c 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -1,15 +1,16 @@ /* jshint esversion: 6 */ const distanceUnits = { // how many meters per X? - "m" : 1, - "yd" : 0.9144, - "mi" : 1609.34, - "km" : 1000, - "kmi" : 1000 + "m": 1, + "yd": 0.9144, + "mi": 1609.34, + "km": 1000, + "kmi": 1000 }; const speedUnits = { // how many kph per X? - "kmh" : 1, - "kph" : 1, - "mph" : 1.60934 + "kmh": 1, + "kph": 1, + "km/h": 1, + "mph": 1.60934 }; /* @@ -33,23 +34,24 @@ timePattern / datePattern: */ var locales = { - "en_GB": { // this is default - lang: "en_GB", - decimal_point: ".", - thousands_sep: ",", - currency_symbol: "£", currency_first:true, - int_curr_symbol: "GBP", - speed: 'mph', - distance: { "0": "yd", "1": "mi" }, - temperature: '°C', - ampm: {0:"am",1:"pm"}, - timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, - datePattern: { 0: "%b %d %Y", 1: "%d/%m/%Y" }, // Feb 28 2020" // "01/03/2020"(short) - abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", - month: "January,February,March,April,May,June,July,August,September,October,November,December", - abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", - day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", - trans: { /*yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off"*/ }}, + "en_GB": { // this is default + lang: "en_GB", + decimal_point: ".", + thousands_sep: ",", + currency_symbol: "£", currency_first: true, + int_curr_symbol: "GBP", + speed: 'mph', + distance: { "0": "yd", "1": "mi" }, + temperature: '°C', + ampm: { 0: "am", 1: "pm" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%b %d %Y", 1: "%d/%m/%Y" }, // Feb 28 2020" // "01/03/2020"(short) + abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + month: "January,February,March,April,May,June,July,August,September,October,November,December", + abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", + day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", + // No translation for english... + }, "de_DE": { lang: "de_DE", decimal_point: ",", @@ -59,31 +61,33 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"",1:""}, + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%Y" }, // Sonntag, 1. März 2020 // 01.01.20 abmonth: "Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" }}, + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" } + }, "en_US": { lang: "en_US", decimal_point: ".", thousands_sep: ",", - currency_symbol: "$", currency_first:true, + currency_symbol: "$", currency_first: true, int_curr_symbol: "USD", speed: "mph", distance: { 0: "yd", 1: "mi" }, temperature: "°F", - ampm: {0:"am",1:"pm"}, + ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "", 1: "%m/%d/%y" }, abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", month: "January,February,March,April,May,June,July,August,September,October,November,December", abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", - trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + // No translation for english... + }, "en_JP": { // we do not have the font, so it is not ja_JP lang: "en_JP", decimal_point: ".", @@ -93,14 +97,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°F", - ampm: {0:"",1:""}, + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%y/%M/%d", 1: "%y/%m;/%d" }, abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", month: "January,February,March,April,May,June,July,August,September,October,November,December", abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", - trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + // No translation for english... + }, "nl_NL": { lang: "nl_NL", decimal_point: ",", @@ -110,31 +115,33 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"",1:""}, + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, datePattern: { 0: "%A %B %d %Y", 1: "%d.%m.%y" }, // zondag 1 maart 2020 // 01.01.20 abday: "zo,ma,di,wo,do,vr,za", day: "zondag,maandag,dinsdag,woensdag,donderdag,vrijdag,zaterdag", abmonth: "jan,feb,mrt,apr,mei,jun,jul,aug,sep,okt,nov,dec", month: "januari,februari,maart,april,mei,juni,juli,augustus,september,oktober,november,december", - trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + // No translation for english... + }, "en_CA": { lang: "en_CA", decimal_point: ".", thousands_sep: ",", currency_symbol: "$", int_curr_symbol: "CAD", - speed: "mph", - distance: { 0: "mi", 1: "kmi" }, - temperature: "°F", - ampm: {0:"am",1:"pm"}, + speed: "km/h", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %B %d, %Y", "1": "%Y-%m-%d" }, // Sunday, March 1, 2020 // 2012-12-20 abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", month: "January,February,March,April,May,June,July,August,September,October,November,December", abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", - trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + // No translation for english... + }, "fr_FR": { lang: "fr_FR", decimal_point: ",", @@ -144,14 +151,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"",1:""}, + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%Y" }, // dimanche 1 mars 2020 // 01/03/2020 abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc", month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre", abday: "dim,lun,mar,mer,jeu,ven,sam", day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi", - trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }}, + trans: { yes: "oui", Yes: "Oui", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, "sv_SE": { lang: "sv_SE", decimal_point: ",", @@ -161,14 +169,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"fm",1:"em"}, + ampm: { 0: "fm", 1: "em" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A %B %d %Y", "1": "%Y-%m-%d" }, // söndag 1 mars 2020 // 2020-03-01 - abmonth: "jan,feb,mars,apr,maj,juni,juli,aug,sep,okt,nov,dec", - month: "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december", - abday: "sön,mån,tis,ons,tors,fre,lör", - day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag", - trans : { yes : "ja", Yes: "Ja", no: "nej", No: "Nej", ok : "ok", on: "on", off: "off" }}, + abmonth: "jan,feb,mars,apr,maj,juni,juli,aug,sep,okt,nov,dec", + month: "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december", + abday: "sön,mån,tis,ons,tors,fre,lör", + day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag", + trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "on", off: "off" } + }, "en_AU": { lang: "en_AU", decimal_point: ".", @@ -178,14 +187,15 @@ var locales = { speed: "mph", distance: { 0: "mi", 1: "kmi" }, temperature: "°F", - ampm: {0:"am",1:"pm"}, + ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %B %d, %Y", "1": "%m/%d/%y" }, // Sunday, 1 March 2020 // 1/3/20 abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", month: "January,February,March,April,May,June,July,August,September,October,November,December", abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", - trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + // No translation for english... + }, "de_AT": { lang: "de_AT", decimal_point: ",", @@ -195,13 +205,14 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%y" }, // Sonntag, 1. März 2020 // 01.03.20 abmonth: "Jän,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Jänner,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" }}, + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" } + }, "en_IL": { lang: "en_IL", decimal_point: ",", @@ -210,15 +221,16 @@ var locales = { int_curr_symbol: "ILS", speed: "kmh", distance: { 0: "m", 1: "km" }, - temperature: "°F", - ampm: {0:"am",1:"pm"}, + temperature: "°C", + ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %B %d, %Y", "1": "%d/%m/%Y" }, // Sunday, 1 March 2020 // 01/03/2020 abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", month: "January,February,March,April,May,June,July,August,September,October,November,December", abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", - trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + // No translation for english... + }, "es_ES": { lang: "es_ES", decimal_point: ",", @@ -228,14 +240,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"",1:""}, + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %d de %B de %Y", "1": "%d/%m/%y" }, // domingo, 1 de marzo de 2020 // 01/03/20 abmonth: "ene,feb,mar,abr,may,jun,jul,ago,sept,oct,nov,dic", month: "enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre", abday: "dom,lun,mar,mié,jue,vie,sáb", day: "domingo,lunes,martes,miércoles,jueves,viernes,sábado", - trans: { yes : "sí", Yes: "Sí",no: "no", No: "No", ok : "ok", on: "on", off: "off" }}, + trans: { yes: "sí", Yes: "Sí", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, "fr_BE": { lang: "fr_BE", decimal_point: ",", @@ -245,14 +258,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0: "",1: ""}, + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20 abmonth: "anv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.", month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre", abday: "dim,lun,mar,mer,jeu,ven,sam", day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi", - trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }}, + trans: { yes: "oui", Yes: "Oui", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, "fi_FI": { lang: "fi_FI", decimal_point: ",", @@ -262,48 +276,51 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0: "ap",1: "ip"}, + ampm: { 0: "ap", 1: "ip" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, // 17.00.00 // 17.00 datePattern: { 0: "%A %d. %B %Y", "1": "%-d/%-m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020 abmonth: "tammik,helmik,maalisk,huhtik,toukok,kesäk,heinäk,elok,syysk,lokak,marrask,jouluk", month: "tammikuuta,helmikuuta,maaliskuuta,huhtikuuta,toukokuuta,kesäkuuta,heinäkuuta,elokuuta,syyskuuta,lokakuuta,marraskuuta,joulukuuta", abday: "su,ma,ti,ke,to,pe,la", day: "sunnuntaina,maanantaina,tiistaina,keskiviikkona,torstaina,perjantaina,lauantaina", - trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }}, + trans: { yes: "oui", Yes: "Oui", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, "de_CH": { lang: "de_CH", - decimal_point: ",", + decimal_point: ",", thousands_sep: ".", currency_symbol: "CHF", int_curr_symbol: "CHF", speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"vorm",1:" nachm"}, + ampm: { 0: "vorm", 1: " nachm" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%Y" }, // Sonntag, 1. März 2020 // 1.3.2020 abmonth: "Jan,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" }}, + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" } + }, "fr_CH": { lang: "fr_CH", - decimal_point: ",", + decimal_point: ",", thousands_sep: ".", currency_symbol: "CHF", int_curr_symbol: "CHF", speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"AM",1:"PM"}, + ampm: { 0: "AM", 1: "PM" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20 abmonth: "anv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.", month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre", abday: "dim,lun,mar,mer,jeu,ven,sam", day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi", - trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }}, + trans: { yes: "oui", Yes: "Oui", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, "it_CH": { lang: "it_CH", decimal_point: ",", @@ -313,29 +330,51 @@ var locales = { speed: 'kmh', distance: { "0": "m", "1": "km" }, temperature: '°C', + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00 datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020 abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic", month: "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre", - abday : "dom,lun,mar,mer,gio,ven,sab", + abday: "dom,lun,mar,mer,gio,ven,sab", day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì, sabato", - trans : { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, - "wae_CH" : { + trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, + "it_IT": { + lang: "it_IT", + decimal_point: ",", + thousands_sep: ".", + currency_symbol: "\x80", + int_curr_symbol: "EUR", + speed: 'kmh', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00 + datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020 + abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic", + month: "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre", + abday: "dom,lun,mar,mer,gio,ven,sab", + day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì, sabato", + trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, + "wae_CH": { lang: "wae_CH", - decimal_point: ",", + decimal_point: ",", thousands_sep: ".", currency_symbol: "CHF", int_curr_symbol: "CHF", speed: 'kmh', distance: { "0": "m", "1": "km" }, temperature: '°C', + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00 datePattern: { 0: "%A, %d. %B %Y", "1": "%Y-%m-%d" }, // Sunntag, 1. Märze 2020 // 2020-03-01 abmonth: "Jen,Hor,Mär,Abr,Mei,Brá,Hei,Öig,Her,Wím,Win,Chr", month: "Jenner,Hornig,Märze,Abrille,Meije,Bráčet,Heiwet,Öigšte,Herbštmánet,Wímánet,Wintermánet,Chrištmánet", abday: "Sun,Män,Ziš,Mit,Fró,Fri,Sam", day: "Sunntag,Mäntag,Zištag,Mittwuč,Fróntag,Fritag,Samštag", - trans : { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" }}, + trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + }, "tr_TR": { // this is default lang: "tr_TR", decimal_point: ",", @@ -345,49 +384,52 @@ var locales = { speed: 'kmh', distance: { "0": "m", "1": "km" }, temperature: '°C', - ampm: {0:"öö",1:"ös"}, + ampm: { 0: "öö", 1: "ös" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%d %w %Y %A", 1: "%d/%m/%Y" }, // 1 Mart 2020 Pazar // "01/03/2020" abmonth: "Oca,Sub,Mar,Nis,May,Haz,Tem,Agu,Eyl,Eki,Kas,Ara", month: "Ocak,Subat,Mart,Nisan,Mayis,Haziran,Temmuz,Agustos,Eylul,Ekim,Kasim,Aralik", abday: "Paz,Pzt,Sal,Car,Per,Cum,Cmt", day: "Pazar,Pazartesi,Sali,Carsamba,Persembe,Cuma,Cumartesi", - trans: { yes: "evet", Yes: "Evet", no: "hayir", No: "Hayir", ok: "tamam", on: "acik", off: "kapali" }}, - "hu_HU": { - lang: "hu_HU", - decimal_point: ",", - thousands_sep: " ", - currency_symbol: "Ft", - int_curr_symbol: "HUF", - speed: 'kph', - distance: { "0": "m", "1": "km" }, - temperature: '°C', - ampm: {0:"de",1:"du"}, - timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, - datePattern: { 0: "%Y %b %d, %A", 1: "%Y.%m.%d" }, // 2020 Feb 28, Péntek" // "2020.03.01."(short) - abmonth: "Jan,Feb,Már,Ápr,Máj,Jún,Júl,Aug,Szep,Okt,Nov,Dec", - month: "Január,Február,Március,Április,Május,Június,Július,Augusztus,Szeptember,Október,November,December", - abday: "Vas,Hét,Ke,Szer,Csüt,Pén,Szom", - day: "Vasárnap,Hétfő,Kedd,Szerda,Csütörtök,Péntek,Szombat", - trans: { yes: "igen", Yes: "Igen", no: "nem", No: "Nem", ok: "ok", on: "be", off: "ki" }}, + trans: { yes: "evet", Yes: "Evet", no: "hayir", No: "Hayir", ok: "tamam", on: "acik", off: "kapali" } + }, + "hu_HU": { + lang: "hu_HU", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "Ft", + int_curr_symbol: "HUF", + speed: 'kph', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: { 0: "de", 1: "du" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%Y %b %d, %A", 1: "%Y.%m.%d" }, // 2020 Feb 28, Péntek" // "2020.03.01."(short) + abmonth: "Jan,Feb,Már,Ápr,Máj,Jún,Júl,Aug,Szep,Okt,Nov,Dec", + month: "Január,Február,Március,Április,Május,Június,Július,Augusztus,Szeptember,Október,November,December", + abday: "Vas,Hét,Ke,Szer,Csüt,Pén,Szom", + day: "Vasárnap,Hétfő,Kedd,Szerda,Csütörtök,Péntek,Szombat", + trans: { yes: "igen", Yes: "Igen", no: "nem", No: "Nem", ok: "ok", on: "be", off: "ki" } + }, "pt_BR": { lang: "pt_BR", decimal_point: ",", thousands_sep: ".", - currency_symbol: "R$", currency_first:true, + currency_symbol: "R$", currency_first: true, int_curr_symbol: "BRL", speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", - ampm: {0:"am",1:"pm"}, + ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "", 1: "%d/%m/%y" }, abmonth: "Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez", month: "Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro", abday: "Dom,Seg,Ter,Qua,Qui,Sex,Sab", day: "Domingo,Segunda-feira,Terça-feira,Quarta-feira,Quinta-feira,Sexta-feira,Sábado", - trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "certo", on: "ligado", off: "desligado" }}, - "cs_CZ": { + trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "certo", on: "ligado", off: "desligado" } + }, + "cs_CZ": { lang: "cs_CZ", decimal_point: ",", thousands_sep: " ", @@ -396,12 +438,13 @@ var locales = { speed: 'kmh', distance: { "0": "m", "1": "km" }, temperature: '°C', - ampm: {0:"dop",1:"odp"}, + ampm: { 0: "dop", 1: "odp" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%d. %b %Y", 1: "%d.%m.%Y" }, // "30. led 2020" // "30.01.2020"(short) abmonth: "led,úno,bře,dub,kvě,čvn,čvc,srp,zář,říj,lis,pro", month: "leden,únor,březen,duben,květen,červen,červenec,srpen,září,říjen,listopad,prosinec", abday: "ne,po,út,st,čt,pá,so", day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota", - trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "na", off: "poza" }} + trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "na", off: "poza" } + } }; diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index c16d02fc8..69a3ccc7b 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -4,4 +4,9 @@ 0.04: modify date to display to be more at the original idea but still localized 0.05: use 12/24 hour clock from settings 0.06: Performance refactor, and enhanced graphics! -0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode \ No newline at end of file +0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode +0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy +0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel +0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. +0.11: User settings persisted and read to file. +0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%) \ No newline at end of file diff --git a/apps/marioclock/README.md b/apps/marioclock/README.md index a74c863f8..25276a351 100644 --- a/apps/marioclock/README.md +++ b/apps/marioclock/README.md @@ -7,12 +7,14 @@ Enjoy watching Mario, or one of the other game characters run through a level wh ## Features -* Multiple characters - swipe the screen right to change the character -* Night and Day modes - swipe left to toggle mode +* Multiple characters - swipe the screen right to change the character between `Mario`, `Toad`, and `Daisy` +* Night and Day modes - swipe left to enter night mode, with 3 levels of darkness before returning to day mode. * Smooth animation * Awesome 8-bit style grey-scale graphics * Mario jumps to change the time, every minute -* You can make Mario jump by pressing the top button (Button 1) on the watch +* You can make Mario jump by pressing the bottom button (Button 3) on the watch +* Toggle the info pannel bettween `Date`, `Battery level`, and `Temperature` by pressing the top button (Button 1) +* If you have [GadgetBridge](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) installed on your phone, Mario will let you know when you get a new call or notification. You can clear a message by pressing either Button 1 or Button 3 ## Requests diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index dabe7ad9e..7601b89ba 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -16,6 +16,8 @@ const is12Hour = settings["12hour"] || false; // Screen dimensions let W, H; +// Screen brightness +let brightness = 1; let intervalRef, displayTimeoutRef = null; @@ -27,12 +29,13 @@ const DARKEST = "#122d3e"; const NIGHT = "#001818"; // Character names +const DAISY = "daisy"; const TOAD = "toad"; const MARIO = "mario"; const characterSprite = { frameIdx: 0, - x: 35, + x: 33, y: 55, jumpCounter: 0, jumpIncrement: Math.PI / 6, @@ -54,10 +57,100 @@ const pyramidSprite = { }; const ONE_SECOND = 1000; +const DATE_MODE = "date"; +const BATT_MODE = "batt"; +const TEMP_MODE = "temp"; +const PHON_MODE = "gbri"; let timer = 0; let backgroundArr = []; let nightMode = false; +let infoMode = DATE_MODE; + +// Used to stop values flapping when displayed on screen +let lastBatt = 0; +let lastTemp = 0; + +const phone = { + get status() { + return NRF.getSecurityStatus().connected ? "Yes" : "No"; + }, + message: null, + messageTimeout: null, + messageScrollX: null, + messageType: null, +}; + +const SETTINGS_FILE = "marioclock.json"; + +function readSettings() { + return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; +} + +function writeSettings(newSettings) { + require("Storage").writeJSON(SETTINGS_FILE, newSettings); +} + +function phoneOutbound(msg) { + Bluetooth.println(JSON.stringify(msg)); +} + +function phoneClearMessage() { + if (phone.message === null) return; + + if (phone.messageTimeout) { + clearTimeout(phone.messageTimeout); + phone.messageTimeout = null; + } + phone.message = null; + phone.messageScrollX = null; + phone.messageType = null; +} + +function phoneNewMessage(type, msg) { + Bangle.buzz(); + + phoneClearMessage(); + phone.messageTimeout = setTimeout(() => phone.message = null, ONE_SECOND * 30); + phone.message = msg; + phone.messageType = type; + + // Notify user and active screen + if (!Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDPower(true); + } +} + +function truncStr(str, max) { + if (str.length > max) { + return str.substr(0, max) + '...'; + } + return str; +} + +function phoneInbound(evt) { + switch (evt.t) { + case 'notify': + const sender = truncStr(evt.sender, 10); + const subject = truncStr(evt.subject, 15); + phoneNewMessage("notify", `${sender} - '${subject}'`); + break; + case 'call': + if (evt.cmd === "accept") { + let nameOrNumber = "Unknown"; + if (evt.name !== null || evt.name !== "") { + nameOrNumber = evt.name; + } else if (evt.number !== null || evt.number !== "") { + nameOrNumber = evt.number; + } + phoneNewMessage("call", nameOrNumber); + } + break; + default: + return null; + } +} function genRanNum(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); @@ -65,18 +158,35 @@ function genRanNum(min, max) { function switchCharacter() { const curChar = characterSprite.character; + let newChar; - if (curChar === MARIO) { - newChar = TOAD; - } else { - newChar = MARIO; + switch(curChar) { + case DAISY: + newChar = MARIO; + break; + case TOAD: + newChar = DAISY; + break; + case MARIO: + default: + newChar = TOAD; } characterSprite.character = newChar; } function toggleNightMode() { - nightMode = !nightMode; + if (!nightMode) { + nightMode = true; + return; + } + + brightness -= 0.30; + if (brightness <= 0) { + brightness = 1; + nightMode = false; + } + Bangle.setLCDBrightness(brightness); } function incrementTimer() { @@ -193,6 +303,19 @@ function drawCoin() { drawCoinFrame(coinSprite.x, coinSprite.y); } +function drawDaisyFrame(idx, x, y) { + switch(idx) { + case 0: + const dFr1 = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgWHw6TIAQXWrlcWZAqBDQIeBBxQaBDxIcCHIQ8JDAIAFWJLPHA==")); + g.drawImage(dFr1, x, y); + break; + case 1: + default: + const dFr2 = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgQvBSZACCBwNcWZQcCAAIPIDgYACFw4YBDYIOCD4waEDYI+HaBQ=")); + g.drawImage(dFr2, x, y); + } +} + function drawMarioFrame(idx, x, y) { switch(idx) { case 0: @@ -200,10 +323,9 @@ function drawMarioFrame(idx, x, y) { g.drawImage(mFr1, x, y); break; case 1: + default: const mFr2 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIdCCgTOIFgiVHFwYCBUhA9FBwz8HAo73GACQA=")); // Mario frame 2 g.drawImage(mFr2, x, y); - break; - default: } } @@ -214,13 +336,32 @@ function drawToadFrame(idx, x, y) { g.drawImage(tFr1, x, y); break; case 1: + default: const tFr2 = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2 g.drawImage(tFr2, x, y); - break; - default: } } +// Mario speach bubble +function drawNotice(x, y) { + if (phone.message === null) return; + + let img; + switch (phone.messageType) { + case "call": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); + break; + case "notify": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + break; + case "lowBatt": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); + break; + } + + if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); +} + function drawCharacter(date, character) { // calculate jumping const seconds = date.getSeconds(), @@ -251,10 +392,13 @@ function drawCharacter(date, character) { } switch(characterSprite.character) { - case(TOAD): + case DAISY: + drawDaisyFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + break; + case TOAD: drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); break; - case(MARIO): + case MARIO: default: drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); } @@ -281,13 +425,109 @@ function drawTime(date) { g.drawString(mins, 47, 29); } -function drawDate(date) { - g.setFont("6x8"); - g.setColor(LIGHTEST); +function buildDateStr(date) { let dateStr = locale.date(date, true); dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,""); dateStr = locale.dow(date, true) + " " + dateStr; - g.drawString(dateStr, (W - g.stringWidth(dateStr))/2, 1); + + return dateStr; +} + +function buildBatStr() { + let batt = parseInt(E.getBattery()); + const battDiff = Math.abs(lastBatt - batt); + + // Suppress flapping values + // Only update batt if it moves greater than +-2 + if (battDiff > 2) { + lastBatt = batt; + } else { + batt = lastBatt; + } + + const battStr = `Bat: ${batt}%`; + + return battStr; +} + +function buildTempStr() { + let temp = parseInt(E.getTemperature()); + const tempDiff = Math.abs(lastTemp - temp); + + // Suppress flapping values + // Only update temp if it moves greater than +-2 + if (tempDiff > 2) { + lastTemp = temp; + } else { + temp = lastTemp; + } + const tempStr = `Temp: ${temp}'c`; + + return tempStr; +} + +function buildPhonStr() { + return `Phone: ${phone.status}`; +} + +function drawInfo(date) { + let xPos; + let str = ""; + + if (phone.message !== null) { + str = phone.message; + const strLen = g.stringWidth(str); + if (strLen > W) { + if (phone.messageScrollX === null || (phone.messageScrollX <= (strLen * -1))) { + phone.messageScrollX = W; + resetDisplayTimeout(); + } else { + phone.messageScrollX -= 2; + } + xPos = phone.messageScrollX; + } else { + xPos = (W - g.stringWidth(str)) / 2; + } + } else { + switch(infoMode) { + case PHON_MODE: + str = buildPhonStr(); + break; + case TEMP_MODE: + str = buildTempStr(); + break; + case BATT_MODE: + str = buildBatStr(); + break; + case DATE_MODE: + default: + str = buildDateStr(date); + } + xPos = (W - g.stringWidth(str)) / 2; + } + + g.setFont("6x8"); + g.setColor(LIGHTEST); + g.drawString(str, xPos, 1); +} + +function changeInfoMode() { + phoneClearMessage(); + + switch(infoMode) { + case BATT_MODE: + infoMode = TEMP_MODE; + break; + case TEMP_MODE: + infoMode = PHON_MODE; + break; + case PHON_MODE: + infoMode = DATE_MODE; + break; + case DATE_MODE: + default: + infoMode = BATT_MODE; + } } function redraw() { @@ -302,8 +542,9 @@ function redraw() { drawPyramid(); drawTrees(); drawTime(date); - drawDate(date); + drawInfo(date); drawCharacter(date); + drawNotice(); drawCoin(); // Render new frame @@ -340,8 +581,39 @@ function startTimers(){ redraw(); } +function loadSettings() { + const settings = readSettings(); + if (!settings) return; + + if (settings.character) characterSprite.character = settings.character; + if (settings.nightMode) nightMode = settings.nightMode; + if (settings.brightness) { + brightness = settings.brightness; + Bangle.setLCDBrightness(brightness); + } +} + +function updateSettings() { + const newSettings = { + character: characterSprite.character, + nightMode: nightMode, + brightness: brightness, + }; + writeSettings(newSettings); +} + +function checkBatteryLevel() { + if (Bangle.isCharging()) return; + if (E.getBattery() > 10) return; + if (phone.message !== null) return; + + phoneNewMessage("lowBatt", "Warning, battery is low"); +} + // Main function init() { + loadSettings(); + clearInterval(); // Initialise display @@ -355,20 +627,21 @@ function init() { setWatch(() => { if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true; resetDisplayTimeout(); - }, BTN1, {repeat:true}); + phoneClearMessage(); // Clear any phone messages and message timers + }, BTN3, {repeat: true}); + // Close watch and load launcher app setWatch(() => { Bangle.setLCDMode(); Bangle.showLauncher(); - }, BTN2, {repeat:false,edge:"falling"}); + }, BTN2, {repeat: false, edge: "falling"}); - Bangle.on('lcdPower', (on) => { - if (on) { - startTimers(); - } else { - clearTimers(); - } - }); + // Change info mode + setWatch(() => { + changeInfoMode(); + }, BTN1, {repeat: true}); + + Bangle.on('lcdPower', (on) => on ? startTimers() : clearTimers()); Bangle.on('faceUp', (up) => { if (up && !Bangle.isLCDOn()) { @@ -382,17 +655,38 @@ function init() { switch(sDir) { // Swipe right (1) - change character (on a loop) - case(1): + case 1: switchCharacter(); break; // Swipe left (-1) - change day/night mode (on a loop) - case(-1): + case -1: default: toggleNightMode(); } + + updateSettings(); }); + // Phone connectivity + try { NRF.wake(); } catch (e) {} + + NRF.on('disconnect', () => { + phoneNewMessage(null, "Phone disconnected"); + }); + + NRF.on('connect', () => { + setTimeout(() => { + phoneOutbound({ t: "status", bat: E.getBattery() }); + }, ONE_SECOND * 2); + phoneNewMessage(null, "Phone connected"); + }); + + GB = (evt) => phoneInbound(evt); + startTimers(); + + setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); + checkBatteryLevel(); } // Initialise! diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog index e6a689e9e..cca1b6e6b 100644 --- a/apps/mclock/ChangeLog +++ b/apps/mclock/ChangeLog @@ -1,2 +1,6 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Added Locale based date +0.04: Improve performance, attempt to remove occasional glitch when LCD on (fix #279) +0.05: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast + Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365) +0.06: Support 12 hour time diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js index ce30ad033..32048cd60 100644 --- a/apps/mclock/clock-morphing.js +++ b/apps/mclock/clock-morphing.js @@ -1,14 +1,16 @@ +var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; var locale = require("locale"); +var CHARW = 34; // how tall are digits? +var CHARP = 2; // how chunky are digits? +var Y = 50; // start height // Offscreen buffer -var buf = Graphics.createArrayBuffer(240,86,1,{msb:true}); -function flip() { - g.setColor(1,1,1); - g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,50); -} +var buf = Graphics.createArrayBuffer(CHARW+CHARP*2,CHARW*2 + CHARP*2,1,{msb:true}); +var bufimg = {width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer}; // The last time that we displayed -var lastTime = " "; +var lastTime = "-----"; // If animating, this is the interval's id var animInterval; +var timeInterval; /* Get array of lines from digit d to d+1. n is the amount (0..1) @@ -49,7 +51,7 @@ const DIGITS = { [0,1,1,1], [1,1,1,2], [1-n,2,1,2]], -"5": (n,maxFive)=>maxFive ? [ // 5 -> 0 +"5to0": n=>[ // 5 -> 0 [0,0,0,1], [0,0,1,0], [n,1,1,1], @@ -57,7 +59,8 @@ const DIGITS = { [0,2,1,2], [0,2,0,2], [1,1-n,1,1], -[0,1,0,1+n]] : [ // 5 -> 6 +[0,1,0,1+n]], +"5to6": n=>[ // 5 -> 6 [0,0,0,1], [0,0,1,0], [0,1,1,1], @@ -109,59 +112,71 @@ const DIGITS = { /* Draw a transition between lastText and thisText. 'n' is the amount - 0..1 */ -function draw(lastText,thisText,n) { - buf.clear(); - var x = 1; // x offset - const p = 2; // padding around digits - var y = p; // y offset - const s = 34; // character size +function drawDigits(lastText,thisText,n) { + "ram" + const p = CHARP; // padding around digits + const s = CHARW; // character size + var x = 0; // x offset + g.reset(); for (var i=0;i{ + if (c[0]!=c[2]) // horiz + buf.fillRect(p+c[0]*s,c[1]*s,p+c[2]*s,2*p+c[3]*s); + else if (c[1]!=c[3]) // vert + buf.fillRect(c[0]*s,p+c[1]*s,2*p+c[2]*s,p+c[3]*s); + }); + g.drawImage(bufimg,x,Y); } - var l = DIGITS[ch](chn,lastCh==5 && thisCh==0); - l.forEach(c=>{ - if (c[0]!=c[2]) // horiz - buf.fillRect(x+c[0]*s,y+c[1]*s-p,x+c[2]*s,y+c[3]*s+p); - else if (c[1]!=c[3]) // vert - buf.fillRect(x+c[0]*s-p,y+c[1]*s,x+c[2]*s+p,y+c[3]*s); - }); if (thisCh==":") x-=4; x+=s+p+7; } - y += 2*s; +} +function drawEverythingElse() { + var x = (CHARW + CHARP + 6)*5; + var y = Y + 2*CHARW + CHARP; var d = new Date(); - buf.setFont("6x8"); - buf.setFontAlign(-1,-1); - buf.drawString(("0"+d.getSeconds()).substr(-2), x, y-8); + g.reset(); + g.setFont("6x8"); + g.setFontAlign(-1,-1); + g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true); + // meridian + if (is12Hour) g.drawString((d.getHours() < 12) ? "AM" : "PM", x, Y + 4, true); // date - buf.setFontAlign(0,-1); + g.setFontAlign(0,-1); var date = locale.date(d,false); - buf.drawString(date, buf.getWidth()/2, y+8); - flip(); + g.drawString(date, g.getWidth()/2, y+8, true); } /* Show the current time, and animate if needed */ function showTime() { - if (!Bangle.isLCDOn()) return; if (animInterval) return; // in animation - quit var d = new Date(); - var t = (" "+d.getHours()).substr(-2)+":"+ + var hours = d.getHours(); + if (is12Hour) hours = ((hours + 11) % 12) + 1; + var t = (" "+hours).substr(-2)+":"+ ("0"+d.getMinutes()).substr(-2); var l = lastTime; // same - don't animate - if (t==l) { - draw(t,l,0); + if (t==l || l=="-----") { + drawDigits(l,t,0); + drawEverythingElse(); + lastTime = t; return; } var n = 0; @@ -170,23 +185,35 @@ function showTime() { if (n>=1) { n=1; clearInterval(animInterval); - animInterval=0; + animInterval = undefined; } - draw(l,t,n); + drawDigits(l,t,n); }, 20); lastTime = t; } Bangle.on('lcdPower',function(on) { - if (on) + if (animInterval) { + clearInterval(animInterval); + animInterval = undefined; + } + if (timeInterval) { + clearInterval(timeInterval); + timeInterval = undefined; + } + if (on) { showTime(); + timeInterval = setInterval(showTime, 1000); + } else { + lastTime = "-----"; + } }); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); // Update time once a second -setInterval(showTime, 1000); +timeInterval = setInterval(showTime, 1000); showTime(); // Show launcher when middle button pressed diff --git a/apps/metronome/ChangeLog b/apps/metronome/ChangeLog new file mode 100644 index 000000000..25628660e --- /dev/null +++ b/apps/metronome/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Watch vibrates with every beat +0.03: Uses mean of three time intervalls to calculate bmp +0.04: App shows instructions, Widgets remain visible, color changed diff --git a/apps/metronome/README.md b/apps/metronome/README.md new file mode 100644 index 000000000..1bb9a893c --- /dev/null +++ b/apps/metronome/README.md @@ -0,0 +1,14 @@ +# Metronome + +This metronome makes your watch blink and vibrate with a given rate. + +## Usage + +* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat. +* Use `BTN1` to increase the bmp value by one. +* Use `BTN3` to decrease the bmp value by one. +* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`. + +## Attributions + +Icon made by Roundicons from www.flaticon.com diff --git a/apps/metronome/metronome-icon.js b/apps/metronome/metronome-icon.js new file mode 100644 index 000000000..8b45f233b --- /dev/null +++ b/apps/metronome/metronome-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+ABt4AB4fOFyFOABtUGDotOAAYvcp4ARqovbq0rACAvbqwABF98yGCAvdGAcHgAAEF8tWmIuGGA6QaF4lWFw4vgFwovPmIvuYDIvd0ejF59cF6qQFFwIvnMAguSqxfaFyQvYvOi0QuTF64uCAAQuRXwIvUqouEF6guFF5+cAAiOZF6iOaF5sxv+iF6xfRmVWFwWjv8rp4tSL6YvBqwuDMgQvnFwovURwIvQRggAELygvPgwuIF8ouEBwIvnFwwwXF54uBvwuFq0yF6buCF5guClQuFGAgvfFwcAF49WmIvRFwQvKFwkAmQvHYQMxF7l+FwgvKGAIvalQuGF5dWFx1VABVUvF4p0qAAdPCZNPF51OAD4vOKQIACF/4waF9wuEqgv/F/gwMF97vvAAUqADYtQAAMAADYuRGDgmLA=")) diff --git a/apps/metronome/metronome.js b/apps/metronome/metronome.js new file mode 100644 index 000000000..c41305f77 --- /dev/null +++ b/apps/metronome/metronome.js @@ -0,0 +1,97 @@ +var tStart = Date.now(); +var cindex=0; // index to iterate through colous +var bpm=60; // ininital bpm value +var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm +var tindex=0; //index to iterate through time_diffs + +Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app + +function changecolor() { + const maxColors = 2; + const colors = { + 0: { value: 0xFFFF, name: "White" }, + // 1: { value: 0x000F, name: "Navy" }, + // 2: { value: 0x03E0, name: "DarkGreen" }, + // 3: { value: 0x03EF, name: "DarkCyan" }, + // 4: { value: 0x7800, name: "Maroon" }, + // 5: { value: 0x780F, name: "Purple" }, + // 6: { value: 0x7BE0, name: "Olive" }, + // 7: { value: 0xC618, name: "LightGray" }, + // 8: { value: 0x7BEF, name: "DarkGrey" }, + // 9: { value: 0x001F, name: "Blue" }, + // 10: { value: 0x07E0, name: "Green" }, + // 11: { value: 0x07FF, name: "Cyan" }, + 1: { value: 0xF800, name: "Red" }, + // 13: { value: 0xF81F, name: "Magenta" }, + // 14: { value: 0xFFE0, name: "Yellow" }, + // 15: { value: 0xFFFF, name: "White" }, + // 16: { value: 0xFD20, name: "Orange" }, + // 17: { value: 0xAFE5, name: "GreenYellow" }, + // 18: { value: 0xF81F, name: "Pink" }, + }; + g.setColor(colors[cindex].value); + if (cindex == maxColors-1) { + cindex = 0; + } + else { + cindex += 1; + } + return cindex; +} + +function updateScreen() { + g.clearRect(0, 50, 250, 150); + changecolor(); + Bangle.buzz(50, 0.75); + g.setFont("Vector",48); + g.drawString(Math.floor(bpm)+"bpm", 5, 60); +} + +Bangle.on('touch', function(button) { +// setting bpm by tapping the screen. Uses the mean time difference between several tappings. + if (tindex < time_diffs.length) { + if (Date.now()-tStart < 5000) { + time_diffs[tindex] = Date.now()-tStart; + } + } else { + tindex=0; + time_diffs[tindex] = Date.now()-tStart; + } + tindex += 1; + mean_time = 0.0; + for(count = 0; count < time_diffs.length; count++) { + mean_time += time_diffs[count]; + } + time_diff = mean_time/count; + + tStart = Date.now(); + clearInterval(time_diff); + bpm = (60 * 1000/(time_diff)); + updateScreen(); + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + return bpm; +}); + +// enable bpm finetuning via buttons. +setWatch(() => { + bpm += 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); +}, BTN1, {repeat:true}); + +setWatch(() => { + if (bpm > 1) { + bpm -= 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + } +}, BTN3, {repeat:true}); + +interval = setInterval(updateScreen, 60000 / bpm); + +g.clear(); +g.drawString('Touch the screen to set tempo.\nUse BTN1 to increase, and\nBTN3 to decrease bpm value by 1.', 15, 150); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/metronome/metronome_icon.png b/apps/metronome/metronome_icon.png new file mode 100644 index 000000000..4dac7117f Binary files /dev/null and b/apps/metronome/metronome_icon.png differ diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog index 7b83706bf..dbe920a80 100755 --- a/apps/minionclk/ChangeLog +++ b/apps/minionclk/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Improved date readability, fixed drawing of widgets diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 88fe446ae..3453f49e1 100755 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -1,5 +1,3 @@ -const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA")); - const locale = require("locale"); const black = 0x0000; @@ -9,6 +7,8 @@ let hour; let minute; let date; +let timer; + function draw() { const d = new Date(); @@ -37,7 +37,7 @@ function draw() { } if (newDate !== date) { - g.setFontVector(12); + g.setFont('6x8', 2); g.setColor(black); g.drawString(date, 120, 228); g.setColor(0xFFFF); @@ -46,23 +46,32 @@ function draw() { } } -function drawAll() { +function startDrawing() { hour = ''; minute = ''; date = ''; + var bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ultr7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBttJttPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNtAAIDGtwGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMOHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1tzw0BDYI6B0R3DAAJ1BvMOp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw8zgAAiFYivEPgoSEAYo9jGgY4EO5Q7kVwiyFGggBFhASBHkhsKAAcqqtOAgMzd8o6HO495vF5QgMzrw7lhBoBvMqG4g")); g.drawImage(bob, 0, 0, { scale: 4 }); + Bangle.drawWidgets(); draw(); + timer = setInterval(draw, 1000); +} + +function stopDrawing() { + if (timer) { + clearInterval(timer); + timer = undefined; + } } Bangle.on('lcdPower', function(on) { + stopDrawing(); if (on) { - drawAll(); + startDrawing(); } }); Bangle.loadWidgets(); -Bangle.drawWidgets(); -setInterval(draw, 1000); -drawAll(); +startDrawing(); -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); diff --git a/apps/miplant/ChangeLog b/apps/miplant/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/miplant/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/miplant/app-icon.js b/apps/miplant/app-icon.js new file mode 100644 index 000000000..bf800ea4c --- /dev/null +++ b/apps/miplant/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+4AA/AH4A/AAPQ64AQ44ZKBYwvc6/QF9wwFF9XXF73I4IAH44/F54vdCpYQESAYvm5Avm44ADA4Yvmc44v/F/4v/F/4v/F/4v+zmjv4ABF9GrwoACrYvn2WGFwgABSh4AV0QtDFwYvmFwlhF9wuDF9QuEF82jFw4vmMIQuFv4vuEDOi0d/WgV/0YNGBIN/LrSvCABAkECAI4GAChKBF5QABCIYEDADKpCsIvJLQQ0EGoShJMBwACSJYvEB5GiMCYxJF4LuCLorTLF6IxGQILuBMYgAGC4QwP0YwHYwQbCF4K1CL4lhCohfQMBBiCFQQvEAgIsFAATxWSQyPDAYYSIHgRgZE4IsBF4QGCCI6NRMBboFXgTTIFyYwIPYWcGAb2CCI2iF6qwBD4aqERYQABIQ+cFywALLwgAqXoYvrSAQROA==")) diff --git a/apps/miplant/app.js b/apps/miplant/app.js new file mode 100644 index 000000000..336fddc15 --- /dev/null +++ b/apps/miplant/app.js @@ -0,0 +1,74 @@ + +function getImgHum() { + return require("heatshrink").decompress(atob("jUoxH+AEtlsoYYDS4ZYDAYaVDLAYFDSQYHDSIZYDBIaPDLAYLDRoZYDBoaLDLAYPDRIZYDCIaHDLAYTDQoZYDCoaDDOQYXAA+JxIYX1utDSwYBAAIzYGiwZUTgpODQpzPGGgY3OdI4aRDIIaMDJIYCDIztDGRwaJP5oaWDAwaRDBAbOC5YcKB5I=")); +} +function getImgTemp() { + return require("heatshrink").decompress(atob("iUqxH+AA2sAAQLHCBASMCAoSLCPOBAAQRfI/5Hn3YACy4ACCL4ADCL5H/I/AQHCRAQJCQwQLCQgQNCQYRQCB4A/ADaPjYqTpSCRYQGCZALFA")); +} +function getImgFert() { + return require("heatshrink").decompress(atob("kklxH+AC+FwtbDbAfFAAVbEbgiGEbYiHEbQiEsIjiEQYjeEQiPdEQrXdEdKnTAAJsMD6QlJFZAAIGAIkPEaIkCrdhEaR9MT4gkLFAyjMYoojNUZ4jFEoxrGEBCJDEZSWEEZdhCwpsKJQiJFAgYgGEQwjLD4QjFCRD+KCAylGQ4gjXVhAiPEhAKDJIwiQEowIEEQo2GERgAKEYwAcEUQkDEL9VAAgHFETgAIDJwePEZwdTE5ggdMJt6AAQEEqwRMABYQDAAwkBF5AkKEBQAPEUR6ESAQicJIX+A==")); +} + +var deviceInfo = {}; + +function parseDevice(device) { + var d = new DataView(device.serviceData["fe95"]); + var frame = d.getUint16(0,true); + var offset = 5; + if (frame&16) offset+=6; // mac address + if (frame&32) offset+=1; // capabilitities + if (frame&64) { // event + var l = d.getUint8(offset+2); + var code = d.getUint16(offset,true); + if (!deviceInfo[device.id]) deviceInfo[device.id]={id:device.id}; + event = deviceInfo[device.id]; + switch (code) { + case 0x1004: event.temperature = d.getInt16(offset+3,true)/10; break; + case 0x1006: event.humidity = d.getInt16(offset+3)/10; break; + case 0x100D: + event.temperature = d.getInt16(offset+3,true)/10; + event.humidity = d.getInt16(offset+5)/10; break; + case 0x1008: event.moisture = d.getUint8(offset+3); break; + case 0x1009: event.fertility = d.getUint16(offset+3,true)/10; break; + // case 0x1007: break; // 3 bytes? got 84,0,0 or 68,0,0 + default: event.code = code; + event.raw = new Uint8Array(d.buffer, offset+3, l); + break; + } + //print(event); + show(event); + } +} + +/* +eg. { + "id": "c4:7c:8d:6a:ac:79 public", + "temperature": 16.6, "code": 4103, + "raw": new Uint8Array([246, 0, 0]), + "moisture": 46, "fertility": 20.8 } +*/ +function show(event) { + g.reset().setFont("6x8"); + var y = 45 + 50*Object.keys(deviceInfo).indexOf(event.id); + + g.drawString(event.id.substr(0,17),0,y); + g.drawImage(getImgHum(),0,y+15); + g.setFont("6x8",2); + var t = (event.moisture===undefined) ? "?" : event.moisture; + g.drawString((t+" ").substr(0,3),35,y+25,true); + g.drawImage(getImgFert(),80,y+15); + t = Math.round(event.fertility) || "?"; + g.drawString((t+" ").substr(0,3), 120, y+25, true); + g.drawImage(getImgTemp(),160,y+15); + t = Math.round(event.temperature) || "?"; + g.drawString((t+" ").substr(0,3), 180, y+25, true); + g.flip(); +} + +g.clear(); +g.setFont("6x8",2).setFontAlign(0,-1).drawString("Scanning...",120,24); + +Bangle.loadWidgets() +Bangle.drawWidgets() + +NRF.setScan(parseDevice, { filters: [{serviceData:{"fe95":{}}}], timeout: 2000 }); diff --git a/apps/miplant/app.png b/apps/miplant/app.png new file mode 100644 index 000000000..d73d3d79a Binary files /dev/null and b/apps/miplant/app.png differ diff --git a/apps/nato/changelog.txt b/apps/nato/changelog.txt new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/nato/changelog.txt @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/nato/nato-icon.js b/apps/nato/nato-icon.js new file mode 100644 index 000000000..ae38c0274 --- /dev/null +++ b/apps/nato/nato-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA==")) diff --git a/apps/nato/nato.js b/apps/nato/nato.js new file mode 100644 index 000000000..f4301b83f --- /dev/null +++ b/apps/nato/nato.js @@ -0,0 +1,106 @@ +// Teach a user the NATO Phonetic Alphabet + numbers +// Based on the Morse Code app + +const FONT_NAME = 'Vector12'; +const FONT_SIZE = 80; +const SCREEN_PIXELS = 240; +const UNIT = 100; +const NATO_MAP = { + A: 'ALFA', + B: 'BRAVO', + C: 'CHARLIE', + D: 'DELTA', + E: 'ECHO', + F: 'FOXTROT', + G: 'GOLF', + H: 'HOTEL', + I: 'INDIA', + J: 'JULIETT', + K: 'KILO', + L: 'LIMA', + M: 'MIKE', + N: 'NOVEMBER', + O: 'OSCAR', + P: 'PAPA', + Q: 'QUEBEC', + R: 'ROMEO', + S: 'SIERRA', + T: 'TANGO', + U: 'UNIFORM', + V: 'VICTOR', + W: 'WHISKEY', + X: 'X-RAY', + Y: 'YANKEE', + Z: 'ZULU', + '0': 'ZE-RO', + '1': 'WUN', + '2': 'TOO', + '3': 'TREE', + '4': 'FOW-ER', + '5': 'FIFE', + '6': 'SIX', + '7': 'SEV-EN', + '8': 'AIT', + '9': 'NIN-ER', +}; + +let INDEX = 0; +let showLetter = true; + +const writeText = (txt) => { + g.clear(); + g.setFont(FONT_NAME, FONT_SIZE); + + var width = g.stringWidth(txt); + + // Fit text to screen + var fontFix = FONT_SIZE; + while(width > SCREEN_PIXELS-10){ + fontFix--; + g.setFont(FONT_NAME, fontFix); + width = g.stringWidth(txt); + } + g.drawString(txt, (SCREEN_PIXELS / 2) - (width / 2), SCREEN_PIXELS / 2); +}; +const writeLetter = () => { + writeText(Object.keys(NATO_MAP)[INDEX]); +}; +const writeCode = () => { + writeText(NATO_MAP[Object.keys(NATO_MAP)[INDEX]]); +}; +const toggle = () => { + showLetter = !showLetter; + if(showLetter){ + writeLetter(); + }else { + writeCode(); + } +}; + +// Bootstrapping + +g.clear(); +g.setFont(FONT_NAME, FONT_SIZE); +g.setColor(0, 1, 0); +g.setFontAlign(-1, 0, 0); + + +const step = (positive) => () => { + if (positive) { + INDEX = INDEX + 1; + if (INDEX > Object.keys(NATO_MAP).length - 1) INDEX = 0; + } else { + INDEX = INDEX - 1; + if (INDEX < 0) INDEX = Object.keys(NATO_MAP).length - 1; + } + showLetter = true; // for toggle() + writeLetter(); +}; + +writeLetter(); + +// Press the middle button to see the NATO Phonetic wording +setWatch(toggle, BTN2, { repeat: true }); +// Allow user to switch between letters +setWatch(step(true), BTN1, { repeat: true }); +setWatch(step(false), BTN3, { repeat: true }); diff --git a/apps/nato/nato.png b/apps/nato/nato.png new file mode 100644 index 000000000..bd4678c11 Binary files /dev/null and b/apps/nato/nato.png differ diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog index 553f7388a..522633f7b 100644 --- a/apps/ncstart/ChangeLog +++ b/apps/ncstart/ChangeLog @@ -2,3 +2,7 @@ Renamed as nodeconf-specific 0.03: Move configuration into App/widget settings Move loader into welcome.boot.js +0.04: Run again when updated + Don't run again when settings app is updated (or absent) + Add "Run Now" option to settings +0.05: Don't overwrite existing settings on app update diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js index dbb70d213..094033094 100644 --- a/apps/ncstart/boot.js +++ b/apps/ncstart/boot.js @@ -1,9 +1,11 @@ (function() { - let s = require('Storage').readJSON('setting.json', 1) || {} + let s = require('Storage').readJSON('ncstart.json', 1) + || require('Storage').readJSON('setting.json', 1) + || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('ncstart.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('setting.json', s) + require('Storage').write('ncstart.json', s) load('ncstart.app.js') }) } diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js index 284262634..560fad8ba 100644 --- a/apps/ncstart/settings.js +++ b/apps/ncstart/settings.js @@ -1,16 +1,14 @@ -// The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('setting.json', 1) || {} + let settings = require('Storage').readJSON('ncstart.json', 1) + || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'NCEU Startup' }, - 'Run again': { + 'Run on Next Boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', - onchange: v => { - settings.welcomed = v ? undefined : true - require('Storage').write('setting.json', settings) - }, + format: v => v ? 'OK' : 'No', + onchange: v => require('Storage').write('ncstart.json', {welcomed: !v}), }, + 'Run Now': () => load('ncstart.app.js'), '< Back': back, }) }) diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog new file mode 100644 index 000000000..855442377 --- /dev/null +++ b/apps/numerals/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: Use BTN2 for settings menu like other clocks +0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting +0.04: Don't overwrite existing settings on app update +0.05: Fix settings issue diff --git a/apps/numerals/README.md b/apps/numerals/README.md new file mode 100644 index 000000000..52e84c76d --- /dev/null +++ b/apps/numerals/README.md @@ -0,0 +1,20 @@ +# Numerals Clock + +This is a simple big numerals clock. +Settings can be accessed through the app/widget settings menu of the Bangle.js + +## Settings available + +### Color: +* rnd - shows numerals in different color combinations every time the watches wakes +* r/g - red/green +* y/w - yellow/white +* o/c - orange/cyan +* b/y - blue/yellow'ish + +### Draw mode +* fill - fill numerals +* frame - only shows outline of numerals + +### Menu button +* choose button to start launcher menu with \ No newline at end of file diff --git a/apps/numerals/numerals-icon.js b/apps/numerals/numerals-icon.js new file mode 100644 index 000000000..7ef609874 --- /dev/null +++ b/apps/numerals/numerals-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/ABXdAAfQBJAAEBgUNBJ4mGBKAmFEhAuLEwQhSABoX/C6yPPYw61IB4r3DHxoIFCwQIHC5YuDCIo3HC4oWEBI4X/C/4X/C/4X/C7XQC4gOEC5gwEBA4XLGAYOFC5oPCA44XNAA4X/C8SAGC6q4CCxb4EG5guICAgfIFxQA/ADg")) \ No newline at end of file diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js new file mode 100644 index 000000000..b24e8bc5e --- /dev/null +++ b/apps/numerals/numerals.app.js @@ -0,0 +1,95 @@ +/** + * Bangle.js Numerals Clock + * + * + Original Author: Raik M. https://github.com/ps-igel + * + Created: April 2020 + * + see README.md for details + */ + +var numerals = { + 0:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[30,25,61,25,69,33,69,67,61,75,30,75,22,67,22,33]], + 1:[[59,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,59,27,51,19,51,9]], + 2:[[9,1,82,1,90,9,90,53,82,61,21,61,21,74,82,74,90,82,90,92,82,100,9,100,1,92,1,48,9,40,70,40,70,27,9,27,1,19,1,9]], + 3:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,70,74,70,61,9,61,1,53,1,48,9,40,70,40,70,27,9,27,1,19,1,9]], + 4:[[9,1,14,1,22,9,22,36,69,36,69,9,77,1,82,1,90,9,90,92,82,100,78,100,70,92,70,61,9,61,1,53,1,9]], + 5:[[9,1,82,1,90,9,90,19,82,27,21,27,21,40,82,40,90,48,90,92,82,100,9,100,1,92,1,82,9,74,71,74,71,61,9,61,1,53,1,9]], + 6:[[9,1,82,1,90,9,90,19,82,27,22,27,22,40,82,40,90,48,90,92,82,100,9,100,1,92,1,9],[22,60,69,60,69,74,22,74]], + 7:[[9,1,82,1,90,9,90,15,20,98,9,98,1,90,1,86,56,22,9,22,1,14,1,9]], + 8:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[22,27,69,27,69,43,22,43],[22,58,69,58,69,74,22,74]], + 9:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,69,74,69,61,9,61,1,53,1,9],[22,27,69,27,69,41,22,41]], +}; +var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; +var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; +var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; +var _rCol = 0; +var interval = 0; +const REFRESH_RATE = 10E3; + +function translate(tx, ty, p){ + return p.map((x, i)=> x+((i%2)?ty:tx)); +} + +function fill(poly){ + return g.fillPoly(poly,true); +} + +function frame(poly){ + return g.drawPoly(poly,true); +} + +let settings = require('Storage').readJSON('numerals.json',1); +if (!settings) { + settings = { + color:0, + drawMode:"fill", + menuButton:24 + }; +} + +function drawNum(num,col,x,y,func){ + g.setColor(col); + let tx = x*100+25; + let ty = y*104+32; + for (let i=0;i0) g.setColor((func==fill)?"#000000":col); + func(translate(tx,ty,numerals[num][i])); + } +} + +function draw(drawMode){ + let d = new Date(); + let h1 = Math.floor((_12hour?d.getHours()%12:d.getHours())/10); + let h2 = (_12hour?d.getHours()%12:d.getHours())%10; + let m1 = Math.floor(d.getMinutes()/10); + let m2 = d.getMinutes()%10; + g.clearRect(0,24,240,240); + drawNum(h1,_hCol[_rCol],0,0,eval(drawMode)); + drawNum(h2,_hCol[_rCol],1,0,eval(drawMode)); + drawNum(m1,_mCol[_rCol],0,1,eval(drawMode)); + drawNum(m2,_mCol[_rCol],1,1,eval(drawMode)); +} + +Bangle.setLCDMode(); + +clearWatch(); +setWatch(Bangle.showLauncher, settings.menuButton, {repeat:false,edge:"falling"}); + +g.clear(); +clearInterval(); +if (settings.color>0) _rCol=settings.color-1; +interval=setInterval(draw, REFRESH_RATE, settings.drawMode); +draw(settings.drawMode); + +Bangle.on('lcdPower', function(on){ + if (on){ + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + draw(settings.drawMode); + interval=setInterval(draw, REFRESH_RATE, settings.drawMode); + }else + { + clearInterval(interval); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/numerals/numerals.png b/apps/numerals/numerals.png new file mode 100644 index 000000000..c181e2e0d Binary files /dev/null and b/apps/numerals/numerals.png differ diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js new file mode 100644 index 000000000..1e97271b6 --- /dev/null +++ b/apps/numerals/numerals.settings.js @@ -0,0 +1,42 @@ +(function(back) { + function updateSettings() { + storage.write('numerals.json', numeralsSettings); + }; + function resetSettings() { + numeralsSettings = { + color:0, + drawMode:"fill", + menuButton:22 + }; + updateSettings(); + } + let numeralsSettings = storage.readJSON('numerals.json',1); + if (!numeralsSettings) resetSettings(); + if (numeralsSettings.menuButton===undefined) numeralsSettings.menuButton=22; + let dm = ["fill","frame"]; + let col = ["rnd","r/g","y/w","o/c","b/y"]; + let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]]; + var menu={ + "" : { "title":"Numerals"}, + "Colors": { + value: 0|numeralsSettings.color, + min:0,max:4, + format: v=>col[v], + onchange: v=> { numeralsSettings.color=v; updateSettings();} + }, + "Draw mode": { + value: 0|dm.indexOf(numeralsSettings.drawMode), + min:0,max:1, + format: v=>dm[v], + onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} + }, + "Menu button": { + value: btn.findIndex(e=>e[0]==numeralsSettings.menuButton), + min:0,max:4, + format: v=>btn[v][1], + onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();} + }, + "< back": back + }; + E.showMenu(menu); +}) \ No newline at end of file diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog new file mode 100644 index 000000000..a7b8065fa --- /dev/null +++ b/apps/openstmap/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Fix marker position, color, and map scaling +0.03: Show widgets (mainly so we can use the GPS recorder widget) diff --git a/apps/openstmap/app-48.png b/apps/openstmap/app-48.png new file mode 100644 index 000000000..b3e3c1047 Binary files /dev/null and b/apps/openstmap/app-48.png differ diff --git a/apps/openstmap/app-icon.js b/apps/openstmap/app-icon.js new file mode 100644 index 000000000..be06afe32 --- /dev/null +++ b/apps/openstmap/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AAOxAAIFCABmrwwRP/2yBJAvCAAYZJ2Wr1eHAAYSLAAQwP1YYHF4xEGCQovT2IYGFwIwDCIWAq1cwIABrtWwAaETZIuFCgIVDA4QvEBoNcq2s6/X2ezCQIDBwIKB2QBBF5ulAYQvI2IvBEQQAD1gACGQVXwKQPLwQwERwmrwFXLAJTDGAoACxFWF55VCGYSqCXYKOBrqHCAAXXAIQyExABBrjAOFQKSDMYQIBw9W2a7CFYiUGw4wBwTwPSQhmCGAJ7BAB4wDqwvIcwTnFYggFBrrjDRonX2ZnCSImHAQMrF5IAFLQQ3DrgtCRoYvDAQLJDF4ZgJOpAvBFgLtBXoIvHLgQCBF4QuCMAeIF5otCLwIuCw2B1mzEwQBB2etAAouEFoJgBSAwtE2IBCRYQvCwGIE4wACGQI0DGAwvGLopaBMIQvDwztBFQopBMQRkEF47AFRoy8Ga4KOBGAgpCSBoABF5mxRous1eBR4jmCSpAvHR4qJBAAYtCAYIvCAgWBEYZDDF4+yFwuyF4gmBGA4zD1gvCrovHE4JeFF4gNCLwgwBLQYvFAoOsw+rwQvDDwQwHBQ2y69dF4wnCGIelAga+B1bwBSAYkGLQQHF2fX2a+FSAoEB0oDCDwQvDlbxCKo4AGFwPXq4uFeAwuCF4RNBR4OswUslYvPFoPXxAuHF4ruEbAgvBq8rlgvN6wuB1iNGSAwrBMAitCcQOIq0rGBgtC2YTB1gvIQQLpDd4esAoIfCFwUrAYOBRZWzrtdVAIvISA+lFwIEBwGk1YuCF4IABEQIvG2eBq2I1eHF5RfCAAeIMIOxrgqBGIMrmNWrlcwAhBrmBAAdWwAWBxAMB1ZfLFoWsxBkCFwQABv9/qweBwAwDagQABAwIACwIgBLxLkCAAKSDFwiMCwWr2SACLYQ2BHIQACHIKRBF5CiBVIguIldcxGsJwIPCLAYsCwwABF4OswIvJc4QuLq2BfIIwDCYaRBRwiaCqwvILoIWB2IDBFxGAWoOIZgIUDa4YtEMQQuJCoYdBqwuIcoWrxJgBCwaVCFgQBCAALuJPQYuLAARDBSIJdBaoYuDAAdcF5QYB1guJLgOlFwKKCGAaUCXwxeJL4ZdJwLhCLoQAD1jDBF4ZeFF5mHFxGHRghdCGAg3C1YuGF5SbCFw1cFwexwFWCYJnBGAgvCFgmGXpIAD0mrFworB1YsDAAr4CGAQDBJoIsNAAfP1QuCg8AlcGC51cHxIAN1Wjg4wBDSoAUldVF1gA/AH4A5A")) diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js new file mode 100644 index 000000000..5be4be82a --- /dev/null +++ b/apps/openstmap/app.js @@ -0,0 +1,82 @@ +var s = require("Storage"); +var map = s.readJSON("openstmap.json"); +var HASWIDGETS = true; +var y1,y2; + +map.center = Bangle.project({lat:map.lat,lon:map.lon}); +var lat = map.lat, lon = map.lon; +var fix = {}; + +function redraw() { + var cx = g.getWidth()/2; + var cy = g.getHeight()/2; + var p = Bangle.project({lat:lat,lon:lon}); + var ix = (p.x-map.center.x)*4096/map.scale + (map.imgx/2) - cx; + var iy = (map.center.y-p.y)*4096/map.scale + (map.imgy/2) - cy; + //console.log(ix,iy); + var tx = 0|(ix/map.tilesize); + var ty = 0|(iy/map.tilesize); + var ox = (tx*map.tilesize)-ix; + var oy = (ty*map.tilesize)-iy; + g.setClipRect(0,y1,g.getWidth()-1,y2); + for (var x=ox,ttx=tx;xWIDGETS[w].area[0]=="b"); + y2 = g.getHeight() - (hasBottomRow ? 24 : 1); +} else { + y1=0; + y2=g.getHeight()-1; +} + +redraw(); + +setWatch(function() { + if (!fix.fix) return; + lat = fix.lat; + lon = fix.lon; + redraw(); +}, BTN2, {repeat:true}); diff --git a/apps/openstmap/app.png b/apps/openstmap/app.png new file mode 100644 index 000000000..9047fce33 Binary files /dev/null and b/apps/openstmap/app.png differ diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html new file mode 100644 index 000000000..3e74e3d2b --- /dev/null +++ b/apps/openstmap/custom.html @@ -0,0 +1,166 @@ + + + + + + + + +
+
+
+
+ + +
+ + + + + + + + + + diff --git a/apps/osmpoi/ChangeLog b/apps/osmpoi/ChangeLog new file mode 100644 index 000000000..1c066f451 --- /dev/null +++ b/apps/osmpoi/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Change img when no fix +0.03: Add HTML class for Spectre.CSS diff --git a/apps/osmpoi/README.md b/apps/osmpoi/README.md new file mode 100644 index 000000000..01e77dd86 --- /dev/null +++ b/apps/osmpoi/README.md @@ -0,0 +1,9 @@ +# Points Of Interest Compass + +## Description + +Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i. + +## Requests + +If you have any bug or feature request, please contact [Renaudgweb](https://github.com/renaudgweb/) diff --git a/apps/osmpoi/app-icon.js b/apps/osmpoi/app-icon.js new file mode 100644 index 000000000..a2baf9e9e --- /dev/null +++ b/apps/osmpoi/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A0PgYurg9kr0rGM4nBg9dsgADmUHGUYtHAAddGIJcgFpIxEMTsAlYtMAAZiaLh4AFmQwXLiSTaLiosBnMymUrGCYTBAAgvPCgjwaMh4raF/4v/F/4vUg4vulZgDgAAIF8EyEQUAh0cAAkVisHGA4+HF6gVBiwwFjkONo0HAAMOAAIvTnIhEiovMFQQADNgYvQroUDg4uGjj9EF448DF6a+HAAS+EF5CQCF6SMIeAQvFXYYwHF59eLpSOBF4hgKGAIvPskAFxKOFF80VM4KOFSBs5F6EWRY4xBF43+F5UyF6DuEizZBKwIuHSBQvXdIwvKMYsHlYvQW4IuPYAYRBMggvRgIvCFxwwCTYZlEg4vPlcHjkVFx6WJF6YuXMoTwCF58yVgIvXY4YvQrqqDGDAvvPYIvPsguaYQYv/F7owBF/4vfg4AGTIIAHF7gA/AH4AwA=")) diff --git a/apps/osmpoi/app.png b/apps/osmpoi/app.png new file mode 100644 index 000000000..31f09b531 Binary files /dev/null and b/apps/osmpoi/app.png differ diff --git a/apps/osmpoi/custom.html b/apps/osmpoi/custom.html new file mode 100644 index 000000000..82532f64e --- /dev/null +++ b/apps/osmpoi/custom.html @@ -0,0 +1,226 @@ + + + + + + + + +
+
+ +
+ +

Click

+

If ok, Click

+
+ + + + + + + + + diff --git a/apps/pipboy/ChangeLog b/apps/pipboy/ChangeLog index 134ba4e18..edbadd9b4 100644 --- a/apps/pipboy/ChangeLog +++ b/apps/pipboy/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Watch! 0.02: Changed colors for better readability and added current date +0.03: Added Info to HP (day in year) and LEVEL (day of week / progress bar show day progress) diff --git a/apps/pipboy/README.md b/apps/pipboy/README.md new file mode 100644 index 000000000..a8e03d638 --- /dev/null +++ b/apps/pipboy/README.md @@ -0,0 +1,19 @@ +# Pipboy themed clock +Have your own Pip-Boy (based on Java Script) + +![](pipboy-screenshot.png) + +## Features + +* High-end greenscreen design +* Shows all your stats +* Time +* Date +* STIMPAKS left +* RADAWAY left +* Health remaining (shows current day in year / days in year) +* Your Level (Shows day in week and progress bar for time passed in day) + +## Requests + +If you have any feature requests, please contact the original author https://twitter.com/simons_bird or co-author http://forum.espruino.com/profiles/155005/ \ No newline at end of file diff --git a/apps/pipboy/app.js b/apps/pipboy/app.js index 48a87fc5d..a8539c7db 100644 --- a/apps/pipboy/app.js +++ b/apps/pipboy/app.js @@ -6,6 +6,11 @@ const darkGreen = 0x0461; const darkerGreen = 0x0261; const pip = require("heatshrink").decompress(atob("klQyAlihNhgNhNP5FC0MjokboUJ8JC64ABBgNAjdDifiikhjfjjekjcDgOhImMKoUbscTsUbwUT4QBC8UMkMMgUT8MLwcBS8/hiNiIo2DhkBgkhhfhHoMT0MT0RLBjYJCCIMTocBUoIAiiIvBgcKsMSG4KLChY1CjckgUhgOAhJDBocL4RTBAYMT0ZJliS9BwUDQIchgWAhXCgVAgUghWhiXihWBgUAhdjhYTBkEB8ELkUTgcA8BHfgNAZ4MT8UTwcCJIOjikjihVB8QDBAIcToUSRYNDkdjjWDgOhjdjhVBI8EAgVBiXhQ4MTkcb4ZPCTILLBAYPBjZDBAIUL8QBBd4KRBWIMboZHfiPCOIMKsK5BR4XChkBAIUChkiheibYMT0RPBBoJZBgWgiRfD0RHfhMhhPhjcjhcBiQrBwSHBRocLRINCgUAhWBR4SZBsatCI4IRBsRHfAAMKkJBBhYBCgfBgZDBAYQFC0Q3BicCgehgbRBscKoMCkBlBjXiI8IxBifDgVAidDhfiIoMLkMUgUb4USQ4MiAoLlDiejgVhI4L3BjUjIr8BIILPBwUBwEa8YzBhQ/BoTLBjejgOgiTVEhkChdhiXCI4MB4MA8BHgsAxBjkDP4MjskTgZ3BTIMMkMb8cKLINCS4KPEcIMChWiK4LVhgJ5EAIJJBOoKTBbIQLB4I9BA4QJDCoOigZLBocJ8JHiwELGoPAgfghdCBIJ7BH4mhAYXAAYMDAIQNE0UKwJHioETwZ/C8cT0cS4UDgCBC4UTHIKjCiaNChfiB4SVDoUA4BJhicjhVgjeEjfDifiikCiZPBwUS4MB4ES0Ub4UUgMMgJDBAYJTBiXjIsIABiPiaIK5BgWggXhhXhgQJB4Mi8kakRJBHoJHDLIViiWCWYJHjhNBhWCHoMK0MbgkbkkTgYHBKYMTgUC4DVCcYUbscB8BDjAArFBOoKLCoECgADBaoMToUTsMLwML0MLBIUJ4JFpAAK3BidDhfiQIXCQIIBBRIcEgJFC0UKoJFrbYnBjeDibHBAIUMgIFC8QBC4UKsEA8EZ4cRJd0BwDPC8Y/BI4IBBR4IHBheijXkgOBjcCjcDVoJHriUiiWigVhiY3BS4PBI4JLCwcKkUK0UMgULwSrBiPBRtEhidDQII3BgMgPoMKgRRBhVhgPAiWBieCiehhQBBoKpBJYJFjiPihPhhdChfBieiGIMKwEKkI5BJIMTsUL4UL4afBgUgidjBIMbscJsBFfhOhjdDicigUAhWBYYMT8MT4QBHcIMCwIVBU4TnCMIMbgarBaLkgIoML8UT8Q1BiViYYI/DikCAoRXCgOAiWCCoIbBAIRHBEIPjBoJHbiR7D8UMgQ9BIoMKbIIJCAIMLkMSoSjCIYUMgIBECYIFCKYL/BIq8KgMTwRtBXIcL0UKsELRIIJBBYUCwEK4UL4UD4MDCoPgCIITEEIYJBgUBsLTUsMTka1DAIS3B0UCoETQYIvCT4MKoLXBH4QXCAYIBDEInhSIIZCsTTUsQZBNIXigkBGIVjgUhZIQ7DTIIPBoSHESoRDFAIYlBT4MToUBkCNQsETsRFBgaFBAoPhEIUiR4WiB4QxBwK+BhYTCZojPDAIYJCgfgAoRjBkKNQ0UTZoLVBgMKOYPihiLBoYhBToQJBXocALYbTEEIJHFE4IJCUYQFBkUA8CNMPoOCGIIBCoLPC4aPCsTNCBoIvD4MCkC/BHYrZEAoKTGdIQDB0UJ4KNMsR9CDYeCgR9BwZ9CsQHBGYhHB4RHGJIT1CNoTdGMIQnC0MSwSNKgEbobBBYYQXCgQrBkYpCJ4T9BI4sKkELoQzEMoKtB0BhBQYKnCT4Z5EjdCHoIAHTYIbBZYIBBDIWihWBhWhhYBBI4UDsJPCDIOhI4TjBD4PBAIJ9C8SdCkQdBBYIbCEoITDwUJ0JHHhTLBe4xPBhUigVBA4TNBoIhBA4KjC8RZBhcCDoKvDC4cTgUCkMbwgPDXoIfDikCiWBIw3ggOAidjI4ZFBiXCBoWgS4JHEoRZDBYMK4MKB4Q1BI4YDC4USsUKAoOjF4TrCAIcbkSNGoQZCeoODFYMboZRBB4LvBicjjeDCoMLoSBER4PhhWCV4StDJ4UbscSkQhBiXjjZJCLIsTsUBsBHDGoWChVhgUBhVBiPiLAnAhPgB4MJNoLJCQoJHBichTYWjAIJXBKIMCsMKkLHBjWDhOhOYJHFLocB8A0BhNhLYMLwJTBjUjiPChNBIwYFBjXjiXhiVha4RLB4ADBDYMCkEB4ABCLoQZBCoL9BjcjfoILBDIMD8AhCAISBBGoMR0USDIIbCEoMS0ZlBI4cBIINjhkCNIXiOIZzCZoODc4IBBSYQJBCYUT4cTgcA0DLBieCBYTXCAoMSwZHCwSJBV4JJBN4MST4Mga4ngY4MLoMbGYJrDW4QtC4RLBAIRDBE4INB0UKOYNggEhgFgiWiCYQPB8BPBhUCYoTRBokCwEbskJ4KzBhUia4j/B4cCsC3CgKlBAIXCKIQvCIIIvBjcidoJvCkMa8UBgMAdYPBjahCjbPB8MKGYQ3BjUDgPhjcEhUhiWhV4MJwTXCsDDCBYMCiVjhUBTIL7BKIIBBgVAhQ3B4BDCOoPjDoa5CB4MAhWCGYI3BEoMjkkSoQ3CoMSkZJBgPgNYITBhIPDoELwUDV4PBidjidDiXihXChWBF4IxCSYMCidihehhfAgfADIL7Ba4JBCwMCDYInBoTZCoDJDiNikXkiQpBoUZ8YNDM4MLsUD8EEgAvBGYI3CHIgNBAIYVBC4cL4T3BEYKZBjdjNYIBBCYYjBWII5DAAMJ0MawbjDAAnAjcje4MLSIPiGIJrBegMTOIPhB4IFEAIITBBYUCgMSAIKDBgKLC0ULsJFBWoMJ4I7GABgXBFYMMgIBBJILNBXYMT0JDBHoTpCLYQDD4USW4gnCI4NhhSzBkMSwcBR4wANhViI4aRDV4MKKYRDBBogBEJ4RdCf4sTgUTIYJpBoMRwRFTcocTsR7E0UCkELgQHCAJo5CI4lgI4MCoMasZPBADHgbIIxEwUCgELkQHC0BDHgYDB0IBBgPhNosa4cBkEA4BGZbIUiieiYYWjgPgTIwBKieCRIIjDZoMR4ZDbWYmAidCI41jA4RJBAI6nBBoNiV4I/fABMSwUb4RHDjejikCAJphBI9cBoETGINigUAhbfBapkL4UK4UJ4MKDAIAohOgjXjgUgheBhfAgY9B0IBBAoIHCAIOihVCiXCiXDI9JJCwMCgKPCI4YBIieCgPhicjjWEI9YABhUhY4I9C8JDEAoIBCdYJHCLwJGtI4NCikDikCAIsMAIUT8RDBCYMbwapBR+iRFRoeihVhhWhdYMS0RHtgUhgY1B0EDAIPAAoQDB4MToUCgELgMDA4MiR+sDR4Q9BBIUhgPgieCgYDBoRHwaYoBD8DPDa4IJBhkAiXjI91BifihkCifhAoMUgUMgMb4cKgMSsQNBhkhJ4JHugKNCIoIBCA4cbocBsEJ4MT0RVBgUBI9sJHoOhaIQDB4EDBIUSsITDjWjkcjgMgI9sB4BHEAIPgSoVihOhLYkBiNCItpHC0CLDAYMDI4OijXjHt5HKwET4cL8UTAIPjiciTYMI8BH4gES4ULkcS8USkUJ8AJBiTPwAA8KsUKsMCoECsMK0MTgUTsZH1hPhhdChaNB4MT0RBC4cKTINCgMgImGghPihdigfBgeggfAhehhXBgHAZ+qLCwULAYPCiaNBoTbBgNAIuoABiOiiQBB0LJBhUhgKLBAEgA==")); +function isLeapYear(year) +{ + return !((year % 4) && (year % 100) || !(year % 400)); +} + function topLine() { g.setColor(green); @@ -28,15 +33,29 @@ function topLine() { } function bottomLine() { + var today = new Date(); + var yy = today.getFullYear(); + var day = today.getDay(); //day of week as number + var h = today.getHours(); + + var startDate = new Date(yy, 0, 0); + var oneDay = 1000 * 60 * 60 * 24; + var daysInYear = 0; + var diff = today - startDate; + var currDayInYear = Math.floor(diff / oneDay); + + if (isLeapYear(yy)) daysInYear = 366; + else daysInYear = 365; + + g.setFont("6x8", tinyFont); //first line g.setColor(darkerGreen); g.fillRect(5, 175, 100, 185); //DATE g.fillRect(105, 175, 160, 185);//STIM g.fillRect(166, 175, 239, 185); // RADAWAY - g.setColor(green); - g.setFont("6x8", tinyFont); + g.drawString("DATE", 20, 177); g.drawString("STIM (3)", 135, 177); g.drawString("RADAWAY (8)", 205, 177); @@ -47,9 +66,10 @@ function bottomLine() { g.fillRect(75, 190, 239, 200); g.setColor(green); - g.drawString("HP 115/115", 38, 192); - g.drawString("LEVEL 6", 100, 192); - g.drawRect(127, 192, 235, 198); + g.drawString("HP "+ currDayInYear + "/"+ daysInYear, 38, 192); + g.drawString("LEVEL " + day, 100, 192); //show week day + g.drawRect(127, 192, 235, 198); //frame + g.fillRect(128, 193, 128 + ((107/24)*h), 197); //progress bar showing progress of day } function boy() { @@ -57,7 +77,6 @@ function boy() { } function drawClock() { - var t = new Date(); var h = t.getHours(); var m = t.getMinutes(); diff --git a/apps/pipboy/pipboy-screenshot.png b/apps/pipboy/pipboy-screenshot.png new file mode 100644 index 000000000..26d90ba3b Binary files /dev/null and b/apps/pipboy/pipboy-screenshot.png differ diff --git a/apps/pong/ChangeLog b/apps/pong/ChangeLog new file mode 100644 index 000000000..6433ebce4 --- /dev/null +++ b/apps/pong/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: 2 players local + improve ai diff --git a/apps/pong/README.md b/apps/pong/README.md new file mode 100644 index 000000000..ea4939539 --- /dev/null +++ b/apps/pong/README.md @@ -0,0 +1,28 @@ +# Pong + +A clone of the Atari game Pong + + + +## Features + +- Play against a dumb AI +- Play local Multiplayer against your friends + +## Controls + +Player's controls: +- UP: BTN1 +- DOWN: BTN2 +long press to move faster + +Restart a game: +- RESET: BTN3 + +Buttons for player 2: +- UP: BTN4 +- DOWN: BTN5 + +## Creator + + diff --git a/apps/pong/app-icon.js b/apps/pong/app-icon.js new file mode 100644 index 000000000..881e60ba9 --- /dev/null +++ b/apps/pong/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIEBgOABQYFD8AUEApoXFDqIXV4BYGKZIANsIRE+IFE/IFEvCIFGrgXLDqIAOgc/9/2hv+g8///3AoUwvE3xuABYP4m3NzwFB7E2tu/CIMYm09wYFDjoFCj4pB/8HkEP+EBFII7EAosDJxYA=")) diff --git a/apps/pong/app.js b/apps/pong/app.js new file mode 100644 index 000000000..ba34d60b5 --- /dev/null +++ b/apps/pong/app.js @@ -0,0 +1,424 @@ +/** + * BangleJS Pong game + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + * + * Inspired by: + * - Let's make pong, One Man Army Studios, Youtube + * - Pong.js, KanoComputing, Github + * - Coding Challenge #67: Pong!, The Coding Train, Youtube + * - Pixl.js Multiplayer Pong, espruino website + */ + +const SCREEN_WIDTH = 240; +const FPS = 16; +const MAX_SCORE = 11; +let scores = [0, 0]; +let aiSpeedRandom = 0; +let winnerMessage = ''; + +const sound = { + ping: () => Bangle.beep(8, 466), + pong: () => Bangle.beep(8, 220), + fall: () => Bangle.beep(16*3, 494).then(_ => Bangle.beep(32*3, 3322)) +}; + +function Vector(x, y) { + this.x = x; + this.y = y; +} +Vector.prototype.add = function (x) { + this.x += x.x || 0; + this.y += x.y || 0; + return this; +}; + +const constrain = (n, low, high) => Math.max(Math.min(n, high), low); +const random = (min, max) => Math.random() * (max - min) + min; +const intersects = (circ, rect, right) => { + var c = circ.pos; + var r = circ.r; + if (c.y - r < rect.pos.y + rect.height && c.y + r > rect.pos.y) { + if (right) { + return c.x + r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width + } else { + return c.x - r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width + } + } + return false; +} + +///////////////////////////// Ball ////////////////////////////////////////// + +function Ball() { + this.r = 4; + this.prevPos = null; + this.originalSpeed = 4; + this.maxSpeed = 6; + + this.reset(); +} +Ball.prototype.reset = function() { + this.speed = this.originalSpeed; + var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed; + var bounceAngle = Math.PI/6; + this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle)); + this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH)); + this.ballReturn = 0; +}; +Ball.prototype.restart = function() { + this.reset(); + ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2); + player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2); + this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2); +}; +Ball.prototype.show = function (invert) { + if (this.prevPos != null) { + g.setColor(invert ? -1 : 0); + g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r); + } + g.setColor(invert ? 0 : -1); + g.fillCircle(this.pos.x, this.pos.y, this.r); + this.prevPos = { + x: this.pos.x, + y: this.pos.y, + r: this.r + }; +}; +function bounceAngle(playerY, ballY, playerHeight, maxHangle) { + let relativeIntersectY = (playerY + (playerHeight/2)) - ballY; + let normalizedRelativeIntersectionY = relativeIntersectY / (playerHeight/2); + let bounceAngle = normalizedRelativeIntersectionY * maxHangle; + return { x: Math.cos(bounceAngle), y: -Math.sin(bounceAngle) }; +} +Ball.prototype.bouncePlayer = function (directionX, directionY, player) { + this.ballReturn++; + this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed); + var MAX_BOUNCE_ANGLE = 4 * Math.PI/12; + var angle = bounceAngle(player.pos.y, this.pos.y, player.height, MAX_BOUNCE_ANGLE) + this.velocity.x = this.speed * angle.x * directionX; + this.velocity.y = this.speed * angle.y * directionY; + this.ballReturn % 2 === 0 ? sound.ping() : sound.pong(); +}; +Ball.prototype.bounce = function (directionX, directionY, player) { + if (player) + return this.bouncePlayer(directionX, directionY, player); + + if (directionX) { + this.velocity.x = Math.abs(this.velocity.x) * directionX; + } + if (directionY) { + this.velocity.y = Math.abs(this.velocity.y) * directionY; + } +}; +Ball.prototype.fall = function (playerId) { + scores[playerId]++; + if (scores[playerId] >= MAX_SCORE) { + this.restart(); + state = 3; + if (playerId === 1) { + winnerMessage = startOption === 0 ? "AI Wins!" : "Player 2 Wins!"; + } else { + winnerMessage = startOption === 0 ? "You Win!" : "Player 1 Wins!"; + } + } else { + sound.fall(); + this.reset(); + } +}; +Ball.prototype.wallCollision = function () { + if (this.pos.y < 0) { + this.bounce(0, 1); + } else if (this.pos.y > SCREEN_WIDTH) { + this.bounce(0, -1); + } else if (this.pos.x < 0) { + this.fall(1); + } else if (this.pos.x > SCREEN_WIDTH) { + this.fall(0); + } else { + return false; + } + return true; +}; +Ball.prototype.playerCollision = function (player) { + if (intersects(this, player)) { + if (this.pos.x < SCREEN_WIDTH/2) { + this.bounce(1, 1, player); + this.pos.add(new Vector(this.width, 0)); + aiSpeedRandom = random(-1.6, 1.6); + } else { + this.bounce(-1, 1, player); + this.pos.add(new Vector(-(this.width / 2 + 1), 0)); + } + return true; + } + return false; +}; +Ball.prototype.collisions = function () { + return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai); +}; +Ball.prototype.updatePosition = function () { + var elapsed = new Date().getTime() - this.lastUpdate; + var x = (elapsed / 50) * this.velocity.x; + var y = (elapsed / 50) * this.velocity.y; + this.pos.add(new Vector(x, y)); +}; +Ball.prototype.update = function () { + this.updatePosition(); + this.lastUpdate = new Date().getTime(); + this.collisions(); +}; + +//////////////////////////// Player ///////////////////////////////////////// + +function Player(right) { + this.width = 4; + this.height = 30; + this.pos = new Vector(right ? SCREEN_WIDTH-this.width : this.width, SCREEN_WIDTH/2 - this.height/2); + this.acc = new Vector(0, 0); + this.speed = 15; + this.maxSpeed = 25; + this.prevPos = null; + this.right = right; +} +Player.prototype.show = function () { + if (this.prevPos != null) { + g.setColor(0); + g.fillRect(this.prevPos.x1, this.prevPos.y1, this.prevPos.x2, this.prevPos.y2); + } + g.setColor(-1); + g.fillRect(this.pos.x, this.pos.y, this.pos.x+this.width, this.pos.y+this.height); + this.prevPos = { + x1: this.pos.x, + y1: this.pos.y, + x2: this.pos.x+this.width, + y2: this.pos.y+this.height + }; +}; +Player.prototype.up = function () { + this.acc.y -= this.speed; +}; +Player.prototype.down = function () { + this.acc.y += this.speed; +}; +Player.prototype.stop = function () { + this.acc.y = 0; +}; +Player.prototype.update = function () { + this.acc.y = constrain(this.acc.y, -this.maxSpeed, this.maxSpeed); + this.pos.add(this.acc); + this.pos.y = constrain(this.pos.y, 0, SCREEN_WIDTH-this.height); +}; + +////////////////////////////// AI /////////////////////////////////////////// + +function AI() { + Player.call(this); + this.pos = new Vector(SCREEN_WIDTH-this.width*2, SCREEN_WIDTH/2 - this.height/2); +} +AI.prototype = Object.create(Player.prototype); +AI.prototype.constructor = Player; +AI.prototype.update = function () { + var y = ball.pos.y - this.height/2; + var randomizedY = ball.ballReturn < 3 ? y : y + (aiSpeedRandom * this.height/2); + var yConstrained = constrain(randomizedY, 0, SCREEN_WIDTH-this.height); + this.pos = new Vector(this.pos.x, yConstrained); +}; + +/////////////////////////////// Scenes //////////////////////////////////////// + +function net() { + var dashSize = 5; + for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) { + g.setColor(-1); + let halfScreen = SCREEN_WIDTH/2; + g.fillRect(halfScreen-dashSize/2, y, halfScreen+dashSize/2, y+dashSize); + } +} + +function drawScores() { + let x1 = SCREEN_WIDTH/4-5; + let x2 = SCREEN_WIDTH*3/4-5; + + g.setColor(0); + g.setFont('Vector', 20); + g.drawString(prevScores[0], x1, 7); + g.drawString(prevScores[1], x2, 7); + g.setColor(-1); + g.setFont('Vector', 20); + g.drawString(scores[0], x1, 7); + g.drawString(scores[1], x2, 7); + prevScores = scores.slice(); +} + +function drawGameOver() { + g.setFont("Vector", 20); + g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10); +} + +function showControls(hide) { + g.setColor(hide ? 0 : -1); + g.setFont("Vector", 8); + var topArrowString = ` + ######## + ## + ## ## + ### ## + ### ## + ### +## +`; + + var arrows = [Graphics.createImage(topArrowString), Graphics.createImage(` + ## + ## +#################### + ## + ## +`), Graphics.createImage(topArrowString.split('\n').reverse().join('\n')) + ]; + + g.drawString('UP', 170, 50); + g.drawImage(arrows[0], 200, 40); + g.drawString('DOWN', 156, 120); + g.drawImage(arrows[1], 200, 120); + g.drawString('START', 152, 190); + g.drawImage(arrows[2], 200, 200); +} + +function drawStartScreen(hide) { + g.setColor(hide ? 0 : -1); + g.setFont("Vector", 10); + g.drawString("1 PLAYER", 95, 80); + g.drawString("2 PLAYERS", 95, 110); + + const ball1 = new Ball(); + ball1.prevPos = null; + ball1.pos = new Vector(87, 86); + ball1.show(hide || !(startOption === 0)); + + const ball2 = new Ball(); + ball2.prevPos = null; + ball2.pos = new Vector(87, 116); + ball2.show(hide || !(startOption === 1)); +} + +function drawStartTimer(count, callback) { + setTimeout(_ => { + player.show(); + ai.show(); + net(); + g.setColor(0); + g.fillRect(117-7, 115-7, 117+14, 115+14); + if (count >= 0) { + g.setFont("Vector", 10); + g.drawString(count+1, 115, 115); + g.setColor(-1); + g.drawString(count === 0 ? 'Go!' : count, 115 - (count === 0 ? 4: 0), 115); + drawStartTimer(count - 1, callback); + } else { + g.setColor(0); + g.fillRect(117-7, 115-7, 117+14, 115+14); + callback(); + } + }, 800); +} + +//////////////////////////////// Main ///////////////////////////////////////// + +function onFrame() { + if (state === 1) { + ball.update(); + player.update(); + ai.update(); + ball.show(); + player.show(); + ai.show(); + net(); + ball.show(); + } else if (state === 3) { + g.clear(); + g.setColor(0); + g.fillRect(0,0,240,240); + state++; + } else if (state === 4) { + drawGameOver(); + } else { + player.show(); + ai.show(); + net(); + } + drawScores(); +} + +function startThatGame() { + player.show(); + ai.show(); + net(); + drawScores(); + drawStartTimer(3, () => setInterval(onFrame, 1000 / FPS)); +} + +var player = new Player(); +var ai; +var ball = new Ball(); +var state = 0; +var prevScores = [0, 0]; +var playerBle = null; +var startOption = 0; + +g.clear(); +g.setColor(0); +g.fillRect(0,0,240,240); +showControls(); +setTimeout(() => { + showControls(true); + drawStartScreen(); +}, 2000); + +////////////////////////////// Controls /////////////////////////////////////// + +setWatch(o => { + if (state === 0) { + if (o.state) { + startOption = startOption === 0 ? startOption : startOption - 1; + drawStartScreen(); + } + } else o.state ? player.up() : player.stop(); +}, BTN1, {repeat: true, edge: 'both'}); +setWatch(o => { + if (state === 0) { + if (o.state) { + startOption = startOption === 1 ? startOption : startOption + 1; + drawStartScreen(); + } + } else o.state ? player.down() : player.stop(); +}, BTN2, {repeat: true, edge: 'both'}); +setWatch(o => { + state++; + clearInterval(); + if (state >= 2) { + g.setColor(0); + g.fillRect(0, 0, 240, 240); + ball.show(true); + scores = [0, 0]; + playerBle = null; + ball = new Ball(); + state = 1; + startThatGame(); + } else { + drawStartScreen(true); + showControls(true); + if (startOption === 1) { + ai = new Player(true); + startThatGame(); + } else { + ai = new AI(); + startThatGame(); + } + } +}, BTN3, {repeat: true}); + +setWatch(o => startOption === 1 && (o.state ? ai.up() : ai.stop()), BTN4, {repeat: true, edge: 'both'}); +setWatch(o => startOption === 1 && (o.state ? ai.down() : ai.stop()), BTN5, {repeat: true, edge: 'both'}); diff --git a/apps/pong/pong.png b/apps/pong/pong.png new file mode 100644 index 000000000..cc97f58f7 Binary files /dev/null and b/apps/pong/pong.png differ diff --git a/apps/qrcode/ChangeLog b/apps/qrcode/ChangeLog new file mode 100644 index 000000000..e2ae6b02a --- /dev/null +++ b/apps/qrcode/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Add posibillity to generate Wifi code. diff --git a/apps/qrcode/app-icon.js b/apps/qrcode/app-icon.js new file mode 100644 index 000000000..5905fa7e9 --- /dev/null +++ b/apps/qrcode/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AEX8gE8nkAn4FSngCWF6xfYDgIABHAQFPDQXD4YgDApxNDMooFOAQIdDAqIvWfcYA=")) diff --git a/apps/qrcode/qrcode.png b/apps/qrcode/app.png similarity index 100% rename from apps/qrcode/qrcode.png rename to apps/qrcode/app.png diff --git a/apps/qrcode/custom.html b/apps/qrcode/custom.html new file mode 100644 index 000000000..b37038ff7 --- /dev/null +++ b/apps/qrcode/custom.html @@ -0,0 +1,115 @@ + + + + + + + + +
+ + + +

Wifi password:

+
+ +
+ +
+
+
+ + +
+

Try your QR Code:

+

Click

+ + + + + + + + + diff --git a/apps/qrcode/qrcode.html b/apps/qrcode/qrcode.html deleted file mode 100644 index 5d372aa59..000000000 --- a/apps/qrcode/qrcode.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - -

Enter a URL:

-

Try your QR Code:

-

Click

- - - - - - - - - diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog new file mode 100644 index 000000000..fa62e12fb --- /dev/null +++ b/apps/rclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: First published version of app +0.02: Added support for locale and 12H clock +0.03: Added HR indication to clock diff --git a/apps/rclock/app-icon.js b/apps/rclock/app-icon.js new file mode 100644 index 000000000..62f5310d5 --- /dev/null +++ b/apps/rclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+If4A/AH4AXqwBEF9VWlYxEAoIAllYuGGwIxnSxAwkR4InCFIbGmF4TCCGAYEBSgK/kXYQxFetDzCLYhgjeBQ3EGE69ESwgwoZYiSpMAgCEGFRfqYQrDblRfRMDdU0QFDp2iAAN4HIowBLYYwXvHG4w0D4wtB0QDBGApcCGYqLSEgIvEAwIqCHQNUYArdaKwIlBRwYpDGgIvEL4QxBYDIvEAAhpBpxZaF6BeBvAIFL4qVXF44uIF4pffFxI0GF7ouKlbrClaNXF4wEB0VUAAUqF4qTEF7heBAAhjDLQS+CL7MqqgECLgZfNGDIAORIaNZACCOBLIbvaFxy/ERtDpCAgYCDF1DsnFgS2ERk4sBF4hhBMYgAiE4bsDF0zAKMFABBXkxZEX4QunWwS4CFtCMEFsN4AAOiAYcAqgGB0UqgGip2iqgvcD4IuCAYgwBAoINBAIN4F7gkBAAplCGgVUNQhfcqlOAAIDCgEqAQIBBAoKXBAQIAL")) \ No newline at end of file diff --git a/apps/rclock/app.png b/apps/rclock/app.png new file mode 100644 index 000000000..7950d4bc3 Binary files /dev/null and b/apps/rclock/app.png differ diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js new file mode 100644 index 000000000..a22f6e2b7 --- /dev/null +++ b/apps/rclock/rclock.app.js @@ -0,0 +1,233 @@ +{ + var minutes; + var seconds; + var hours; + var date; + var first = true; + var locale = require('locale'); + var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; + + //HR variables + var id = 0; + var grow = true; + var size=10; + + //Screen dimensions + const screen = { + width: g.getWidth(), + height: g.getWidth(), + middle: g.getWidth() / 2, + center: g.getHeight() / 2, + }; + + // Ssettings + const settings = { + time: { + color: '#f0af00', + shadow: '#CF7500', + font: 'Vector', + size: 60, + middle: screen.middle - 30, + center: screen.center, + }, + date: { + color: '#f0af00', + shadow: '#CF7500', + font: 'Vector', + size: 15, + middle: screen.height - 20, // at bottom of screen + center: screen.center, + }, + circle: { + colormin: '#eeeeee', + colorsec: '#bbbbbb', + width: 10, + middle: screen.middle, + center: screen.center, + height: screen.height + }, + hr: { + color: '#333333', + size: 10, + x: screen.center, + y: screen.middle + 45 + } + }; + + const dateStr = function (date) { + return locale.date(new Date(), 1); + }; + + const getArcXY = function (centerX, centerY, radius, angle) { + var s, r = []; + s = 2 * Math.PI * angle / 360; + r.push(centerX + Math.round(Math.cos(s) * radius)); + r.push(centerY + Math.round(Math.sin(s) * radius)); + + return r; + }; + + const drawMinArc = function (sections, color) { + g.setColor(color); + rad = (settings.circle.height / 2) - 20; + r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + g.drawLine(r1[0], r1[1], r2[0], r2[1]); + g.setColor('#333333'); + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) + }; + + const drawSecArc = function (sections, color) { + g.setColor(color); + rad = (settings.circle.height / 2) - 40; + r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + g.drawLine(r1[0], r1[1], r2[0], r2[1]); + g.setColor('#333333'); + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) + }; + + const drawClock = function () { + + currentTime = new Date(); + + //Set to initial time when started + if (first == true) { + minutes = currentTime.getMinutes(); + seconds = currentTime.getSeconds(); + for (count = 0; count <= minutes; count++) { + drawMinArc(count, settings.circle.colormin); + } + + for (count = 0; count <= seconds; count++) { + drawSecArc(count, settings.circle.colorsec); + } + first = false; + } + + // Reset seconds + if (seconds == 59) { + g.setColor('#000000'); + g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 40); + } + // Reset minutes + if (minutes == 59 && seconds == 59) { + g.setColor('#000000'); + g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 20); + } + + //Get date as a string + date = dateStr(currentTime); + + // Update minutes when needed + if (minutes != currentTime.getMinutes()) { + minutes = currentTime.getMinutes(); + drawMinArc(minutes, settings.circle.colormin); + } + + //Update seconds when needed + if (seconds != currentTime.getSeconds()) { + seconds = currentTime.getSeconds(); + drawSecArc(seconds, settings.circle.colorsec); + } + + //Write the time as configured in the settings + hours = currentTime.getHours(); + if (_12hour && hours > 13) { + hours = hours - 12; + } + + var meridian; + + if (typeof locale.meridian === "function") { + meridian = locale.meridian(new Date()); + } else { + meridian = ""; + } + + var timestr; + + if (meridian.length > 0 && _12hour) { + timestr = hours + " " + meridian; + } else { + timestr = hours; + } + g.setFontAlign(0, 0, 0); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size); + g.drawString(timestr, settings.time.center, settings.time.middle); + + //Write the date as configured in the settings + g.setColor(settings.date.color); + g.setFont(settings.date.font, settings.date.size); + g.drawString(date, settings.date.center, settings.date.middle); + }; + + //setInterval for HR visualisation + const newBeats = function (hr) { + if (id != 0) { + changeInterval(id, 6e3 / hr.bpm); + } else { + id = setInterval(drawHR, 6e3 / hr.bpm); + } + }; + + //visualize HR with circles pulsating + const drawHR = function () { + if (grow && size < settings.hr.size) { + size++; + } + + if (!grow && size > 3) { + size--; + } + + if (size == settings.hr.size || size == 3) { + grow = !grow; + } + + if (grow) { + color = settings.hr.color; + g.setColor(color); + g.fillCircle(settings.hr.x, settings.hr.y, size); + } else { + color = "#000000"; + g.setColor(color); + g.drawCircle(settings.hr.x, settings.hr.y, size); + } + }; + + // clean app screen + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + +//manage when things should be enabled and not + Bangle.on('lcdPower', function (on) { + if (on) { + Bangle.setHRMPower(1); + } else { + Bangle.setHRMPower(0); + } + }); + + // refesh every second + setInterval(drawClock, 1E3); + + //start HR monitor and update frequency of update + Bangle.setHRMPower(1); + Bangle.on('HRM', function (d) { + newBeats(d); + }); + + // draw now + drawClock(); + + // Show launcher when middle button pressed + setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +} \ No newline at end of file diff --git a/apps/rndmclk/ChangeLog b/apps/rndmclk/ChangeLog new file mode 100644 index 000000000..55cda0f21 --- /dev/null +++ b/apps/rndmclk/ChangeLog @@ -0,0 +1 @@ +0.01: New widget diff --git a/apps/rndmclk/README.md b/apps/rndmclk/README.md new file mode 100644 index 000000000..3bfaf0e89 --- /dev/null +++ b/apps/rndmclk/README.md @@ -0,0 +1,6 @@ +# Summary + +Random Clock is a widget that will randomly show one of the installed watch faces each time the LCD is turned on. + +## Disclaimer +This is an early version and will load a clock each time the LCD is turned on no matter what app was running before the screen went to standby. Also the next watch face is only loaded after the last one is shown for a few tens of seconds. \ No newline at end of file diff --git a/apps/rndmclk/rndmclk.png b/apps/rndmclk/rndmclk.png new file mode 100644 index 000000000..9519b8d09 Binary files /dev/null and b/apps/rndmclk/rndmclk.png differ diff --git a/apps/rndmclk/widget.js b/apps/rndmclk/widget.js new file mode 100644 index 000000000..1c3b3d7bc --- /dev/null +++ b/apps/rndmclk/widget.js @@ -0,0 +1,29 @@ +(() => { + + /** + * Random value between zero (inclusive) and max (exclusive) + * @param {int} max + */ + function getRandomInt(max) { + return Math.floor(Math.random() * Math.floor(max)); + } + + function loadRandomClock() { + // Find available clock apps (same way as in the bootloader) + var clockApps = require("Storage").list(/\.info$/).map(app => require("Storage").readJSON(app, 1) || {}).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); + + if (clockApps && clockApps.length > 0) { + var clockIndex = getRandomInt(clockApps.length); + + load(clockApps[clockIndex].src); + } + } + + Bangle.on('lcdPower', (on) => { + if (on) { + // TODO: Only run if the current app is a clock as well + loadRandomClock(); + } + }); + +})(); \ No newline at end of file diff --git a/apps/route/app-icon.js b/apps/route/app-icon.js new file mode 100644 index 000000000..8410cad40 --- /dev/null +++ b/apps/route/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIkhvgFE/wEDgOHAocDgYFEgOAAp4XEEYsB4w1E5hBKnByFKw8/AQNAAQP/4EAAIMB4HggBABHoNwCwUGE4kOgEYBAMAhk+hgIBAoM/hkEAoMIv8MC4QFChARCAoIMCDoQXChkcjA1EAoJBBg5dCJoJHDKYWAsCGD4AJBAAXBDYIlCsYFBGwUzPok+AokcsOOmIUCAogAWA==")) diff --git a/apps/route/route.png b/apps/route/app.png similarity index 100% rename from apps/route/route.png rename to apps/route/app.png diff --git a/apps/route/route.html b/apps/route/custom.html similarity index 95% rename from apps/route/route.html rename to apps/route/custom.html index 2417aa232..52b6b635f 100644 --- a/apps/route/route.html +++ b/apps/route/custom.html @@ -240,12 +240,10 @@ Bangle.setGPSPower(1); Bangle.setCompassPower(1); g.clear(); `; -var icon = `require("heatshrink").decompress(atob("mEwgIkhvgFE/wEDgOHAocDgYFEgOAAp4XEEYsB4w1E5hBKnByFKw8/AQNAAQP/4EAAIMB4HggBABHoNwCwUGE4kOgEYBAMAhk+hgIBAoM/hkEAoMIv8MC4QFChARCAoIMCDoQXChkcjA1EAoJBBg5dCJoJHDKYWAsCGD4AJBAAXBDYIlCsYFBGwUzPok+AokcsOOmIUCAogAWA=="))`; sendCustomizedApp({ storage:[ - {name:"route.app.js", content:app}, - {name:"route.img", content:icon, evaluate:true}, + {name:"route.app.js", content:app} ] }); }); diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 73bbc7bd1..f168a1fe5 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -8,3 +8,16 @@ 0.09: Move Welcome into App/widget settings 0.10: Added LCD wake-up settings Adds LCD brightness setting +0.11: Make LCD brightness work after leaving settings +0.12: Fix memory leak (#206) + Bring App settings nearer the top + Move LCD Timeout to wakeup menu +0.13: Fix memory leak for App settings + Make capitalization more consistent + Move LCD Brightness menu into more general LCD menu +0.14: Reduce memory usage when running app settings page +0.15: Reduce memory usage when running default clock chooser (#294) +0.16: Reduce memory usage further when running app settings page +0.17: Remove need for "settings" in appid.info +0.18: Don't overwrite existing settings on app update +0.19: Allow BLE HID settings, add README.md diff --git a/apps/setting/README.md b/apps/setting/README.md new file mode 100644 index 000000000..4052da0ff --- /dev/null +++ b/apps/setting/README.md @@ -0,0 +1,18 @@ +# Settings + +This is Bangle.js's settings menu + +* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up) +* **App/Widget Settings** settings specific to installed applications +* **BLE** is Bluetooth LE enabled and the watch connectable? +* **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps? +* **Debug Info** should debug info be shown on the watch's screen or not? +* **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected +* **Vibration** enable/disable the vibration motor +* **Locale** set time zone/whether the clock is 12/24 hour (for supported clocks) +* **Select Clock** if you have more than one clock face, select the default one +* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device. **Note:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps. +* **Set Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader +* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on. +* **Reset Settings** Reset the settings to defaults +* **Turn Off** Turn Bangle.js off diff --git a/apps/setting/boot.js b/apps/setting/boot.js index 8bf9df50d..b437cf744 100644 --- a/apps/setting/boot.js +++ b/apps/setting/boot.js @@ -1,6 +1,6 @@ (() => { - var settings = require('Storage').readJSON('setting.json', true); - if (settings != undefined) { - Bangle.setOptions(settings.options); - } + var settings = require('Storage').readJSON('setting.json', true); + if (!settings) return; + if (settings.options) Bangle.setOptions(settings.options); + if (settings.brightness && settings.brightness!=1) Bangle.setLCDBrightness(settings.brightness); })() diff --git a/apps/setting/settings-default.json b/apps/setting/settings-default.json deleted file mode 100644 index c61fd6109..000000000 --- a/apps/setting/settings-default.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - ble: true, // Bluetooth enabled by default - blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used? - log: false, // Do log messages appear on screen? - timeout: 10, // Default LCD timeout in seconds - vibrate: true, // Vibration enabled by default. App must support - beep: "vib", // Beep enabled by default. App must support - timezone: 0, // Set the timezone for the device - HID : false, // BLE HID mode, off by default - clock: null, // a string for the default clock's name - "12hour" : false, // 12 or 24 hour clock? - // welcomed : undefined/true (whether welcome app should show) - brightness: 1, // LCD brightness from 0 to 1 - options: { - wakeOnBTN1: true, - wakeOnBTN2: true, - wakeOnBTN3: true, - wakeOnFaceUp: false, - wakeOnTouch: false, - wakeOnTwist: true, - twistThreshold: 819.2, - twistMaxY: -800, - twistTimeout: 1000 - } -} diff --git a/apps/setting/settings.js b/apps/setting/settings.js index cbd856ec5..8ed6f58c5 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -61,9 +61,12 @@ const boolFormat = v => v ? "On" : "Off"; function showMainMenu() { var beepV = [false, true, "vib"]; var beepN = ["Off", "Piezo", "Vibrate"]; + var hidV = [false, "kbmedia", "kb", "joy"]; + var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"]; const mainmenu = { '': { 'title': 'Settings' }, - 'Make Connectable': makeConnectable, + 'Make Connectable': ()=>makeConnectable(), + 'App/Widget Settings': ()=>showAppSettingsMenu(), 'BLE': { value: settings.ble, format: boolFormat, @@ -80,7 +83,7 @@ function showMainMenu() { updateSettings(); } }, - 'Debug info': { + 'Debug Info': { value: settings.log, format: v => v ? "Show" : "Hide", onchange: () => { @@ -88,28 +91,6 @@ function showMainMenu() { updateSettings(); } }, - 'LCD Timeout': { - value: settings.timeout, - min: 0, - max: 60, - step: 5, - onchange: v => { - settings.timeout = 0 | v; - updateSettings(); - Bangle.setLCDTimeout(settings.timeout); - } - }, - 'LCD Brightness': { - value: settings.brightness, - min: 0, - max: 1, - step: 0.1, - onchange: v => { - settings.brightness = v || 1; - updateSettings(); - Bangle.setLCDBrightness(settings.brightness); - } - }, 'Beep': { value: 0 | beepV.indexOf(settings.beep), min: 0, max: 2, @@ -133,31 +114,53 @@ function showMainMenu() { } } }, - 'Locale': showLocaleMenu, - 'Select Clock': showClockMenu, + 'Locale': ()=>showLocaleMenu(), + 'Select Clock': ()=>showClockMenu(), 'HID': { - value: settings.HID, - format: boolFormat, - onchange: () => { - settings.HID = !settings.HID; + value: 0 | hidV.indexOf(settings.HID), + min: 0, max: 3, + format: v => hidN[v], + onchange: v => { + settings.HID = hidV[v]; updateSettings(); } }, - 'Set Time': showSetTimeMenu, - 'LCD Wake-Up': showWakeUpMenu, - 'App/widget settings': showAppSettingsMenu, - 'Reset Settings': showResetMenu, - 'Turn Off': Bangle.off, - '< Back': () => { load(); } + 'Set Time': ()=>showSetTimeMenu(), + 'LCD': ()=>showLCDMenu(), + 'Reset Settings': ()=>showResetMenu(), + 'Turn Off': ()=>Bangle.off(), + '< Back': ()=>load() }; return E.showMenu(mainmenu); } -function showWakeUpMenu() { - const wakeUpMenu = { - '': { 'title': 'LCD Wake-Up' }, - '< Back': showMainMenu, - 'Wake On BTN1': { +function showLCDMenu() { + const lcdMenu = { + '': { 'title': 'LCD' }, + '< Back': ()=>showMainMenu(), + 'LCD Brightness': { + value: settings.brightness, + min: 0.1, + max: 1, + step: 0.1, + onchange: v => { + settings.brightness = v || 1; + updateSettings(); + Bangle.setLCDBrightness(settings.brightness); + } + }, + 'LCD Timeout': { + value: settings.timeout, + min: 0, + max: 60, + step: 5, + onchange: v => { + settings.timeout = 0 | v; + updateSettings(); + Bangle.setLCDTimeout(settings.timeout); + } + }, + 'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { @@ -165,7 +168,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN2': { + 'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { @@ -173,7 +176,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN3': { + 'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { @@ -197,7 +200,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On Twist': { + 'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { @@ -236,13 +239,13 @@ function showWakeUpMenu() { } } } - return E.showMenu(wakeUpMenu) + return E.showMenu(lcdMenu) } function showLocaleMenu() { const localemenu = { '': { 'title': 'Locale' }, - '< Back': showMainMenu, + '< Back': ()=>showMainMenu(), 'Time Zone': { value: settings.timezone, min: -11, @@ -268,7 +271,7 @@ function showLocaleMenu() { function showResetMenu() { const resetmenu = { '': { 'title': 'Reset' }, - '< Back': showMainMenu, + '< Back': ()=>showMainMenu(), 'Reset Settings': () => { E.showPrompt('Reset Settings?').then((v) => { if (v) { @@ -296,15 +299,15 @@ function makeConnectable() { }); } function showClockMenu() { - var clockApps = require("Storage").list(/\.info$/).map(app => { - try { return require("Storage").readJSON(app); } - catch (e) { } - }).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); + var clockApps = require("Storage").list(/\.info$/) + .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined}) + .filter(app => app) // filter out any undefined apps + .sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { 'title': 'Select Clock', }, - '< Back': showMainMenu, + '< Back': ()=>showMainMenu(), }; clockApps.forEach((app, index) => { var label = app.name; @@ -325,8 +328,6 @@ function showClockMenu() { return E.showMenu(clockMenu); } - - function showSetTimeMenu() { d = new Date(); const timemenu = { @@ -342,7 +343,7 @@ function showSetTimeMenu() { timemenu.Year.value = d.getFullYear(); } }, - '< Back': showMainMenu, + '< Back': ()=>showMainMenu(), 'Hour': { value: d.getHours(), min: 0, @@ -416,12 +417,21 @@ function showSetTimeMenu() { function showAppSettingsMenu() { let appmenu = { '': { 'title': 'App Settings' }, - '< Back': showMainMenu, + '< Back': ()=>showMainMenu(), } - const apps = storage.list(/\.info$/) - .map(app => storage.readJSON(app, 1)) - .filter(app => app && app.settings) - .sort((a, b) => a.sortorder - b.sortorder) + const apps = storage.list(/\.settings\.js$/) + .map(s => s.substr(0, s.length-12)) + .map(id => { + const a=storage.readJSON(id+'.info',1) || {name: id}; + return {id:id,name:a.name,sortorder:a.sortorder}; + }) + .sort((a, b) => { + const n = (0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }) if (apps.length === 0) { appmenu['No app has settings'] = () => { }; } @@ -435,10 +445,7 @@ function showAppSettings(app) { E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`); setWatch(showAppSettingsMenu, BTN1, { repeat: false }); } - let appSettings = storage.read(app.settings); - if (!appSettings) { - return showError('Missing settings'); - } + let appSettings = storage.read(app.id+'.settings.js'); try { appSettings = eval(appSettings); } catch (e) { @@ -450,7 +457,7 @@ function showAppSettings(app) { } try { // pass showAppSettingsMenu as "back" argument - appSettings(showAppSettingsMenu); + appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) return showError('Error in settings'); diff --git a/apps/simpletimer/ChangeLog b/apps/simpletimer/ChangeLog new file mode 100644 index 000000000..f9f79cd47 --- /dev/null +++ b/apps/simpletimer/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version +0.02: Reset with gesture diff --git a/apps/simpletimer/README.md b/apps/simpletimer/README.md new file mode 100644 index 000000000..ebe54dbe5 --- /dev/null +++ b/apps/simpletimer/README.md @@ -0,0 +1,18 @@ +# Timer + +Simple timer, useful when playing board games or cooking + +## Features + +- When the time is up the timer can be reset to starting time, this is useful e.g. for playing board games +- When the countdown is running the timer cannot be adjusted, this prevents accidental time variations +- When the time is up the starting time is shown, as a reminder of the time elapsed +- When the time is up the timer can be reset with a gesture, no need to use any button + +## How to use it + +- Tap on minutes to increase them one by one +- Tap on seconds to increase them one by one +- Press BTN3 to reset time to 0 +- Press BTN1 to start the timer or reset to the original time +- When the time is up use the [swipeleft](https://github.com/espruino/BangleApps/tree/master/apps/gesture) gesture to reset the timer diff --git a/apps/simpletimer/app-icon.js b/apps/simpletimer/app-icon.js new file mode 100644 index 000000000..b55486dd1 --- /dev/null +++ b/apps/simpletimer/app-icon.js @@ -0,0 +1,5 @@ +require("heatshrink").decompress( + atob( + "mEwxH+AH4A/AEsxAAQso1eyrgvDrmrw4skAAQuDAAIHBrYABFsQvMGLYtGAAOAFweA2WrF4gwYFxAwEFwIvBwowFsIub64AB6wJF6wJB1mGMTFbrmsEYoADHAwAC1dhGCoTCmJhBEYoAM2RiFF6VbleBF6QABGAguSw2sgAwnCAdhXYIwBqwvT2WFDwYvP1YZCwMAlYwT1ZgORogZEqwwB1iRhBoYmGlcAYiZgOBgWFDIzCBAALESYIYvMw4ZHGCuHF5aOKeYgABYiCQMBYeyDZLzBAAQwO2QvPDhbzCeqAvbGAQQBlYvqeYIvteYMreJ7vaACbvQJxwAP1YvLGAeHF7uHFxYvDwovdwovPSDusRxgvEwwvbwwvNGAmrds4vGsOyFy+ysIvPSLqNPGDwuT/xyEwySS2QuEF6BgEYYL0Q1ZIEFyIwGMQIxM1ZcFFyYwHreFw+rSwmy1eHwoSGFygxJABwtXeo4upMSQtdGZorjAH4A/AF4A==" + ) +) diff --git a/apps/simpletimer/app.js b/apps/simpletimer/app.js new file mode 100644 index 000000000..b8e07d107 --- /dev/null +++ b/apps/simpletimer/app.js @@ -0,0 +1,154 @@ +let counter = 0; +let setValue = 0; +let counterInterval; +let state; + +const DEBOUNCE = 50; + +function buzzAndBeep() { + return Bangle.buzz(1000, 1) + .then(() => Bangle.beep(200, 3000)) + .then(() => setTimeout(buzzAndBeep, 5000)); +} + +function outOfTime() { + g.clearRect(0, 0, 220, 70); + g.setFontAlign(0, 0); + g.setFont("6x8", 3); + g.drawString("Time UP!", 120, 50); + counter = setValue; + buzzAndBeep(); + setInterval(() => { + g.clearRect(0, 70, 220, 160); + setTimeout(draw, 200); + }, 400); + state = "stopped"; +} + +function draw() { + const minutes = Math.floor(counter / 60); + const seconds = Math.floor(counter % 60); + const seconds2Digits = seconds < 10 ? `0${seconds}` : seconds.toString(); + g.clearRect(0, 70, 220, 160); + g.setFontAlign(0, 0); + g.setFont("6x8", 7); + g.drawString( + `${minutes < 10 ? "0" : ""}${minutes}:${seconds2Digits}`, + 120, + 120 + ); +} + +function countDown() { + if (counter <= 0) { + if (counterInterval) { + clearInterval(counterInterval); + counterInterval = undefined; + } + outOfTime(); + return; + } + + counter--; + draw(); +} + +function clearIntervals() { + clearInterval(); + counterInterval = undefined; +} + +function set(delta) { + if (state === "started") return; + counter += delta; + if (state === "unset") { + state = "set"; + } + draw(); + g.flip(); +} + +function startTimer() { + setValue = counter; + countDown(); + counterInterval = setInterval(countDown, 1000); +} + +// unset -> set -> started -> -> stopped -> set +const stateMap = { + set: () => { + state = "started"; + startTimer(); + }, + started: () => { + reset(setValue); + }, + stopped: () => { + reset(setValue); + } +}; + +function changeState() { + if (stateMap[state]) stateMap[state](); +} + +function drawLabels() { + g.clear(); + g.setFontAlign(-1, 0); + g.setFont("6x8", 7); + g.drawString(`+ +`, 35, 180); + g.setFontAlign(0, 0, 3); + g.setFont("6x8", 1); + g.drawString(`reset (re)start`, 230, 120); +} + +function reset(value) { + clearIntervals(); + counter = value; + setValue = value; + drawLabels(); + draw(); + state = value === 0 ? "unset" : "set"; +} + +function addWatch() { + clearWatch(); + setWatch(changeState, BTN1, { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + }); + setWatch( + () => { + reset(0); + }, + BTN3, + { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + } + ); + setWatch( + () => { + set(60); + }, + BTN4, + { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + } + ); + setWatch(() => set(1), BTN5, { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + }); +} +Bangle.on("aiGesture", gesture => { + if (gesture === "swipeleft" && state === "stopped") reset(0); +}); + +reset(0); +addWatch(); diff --git a/apps/simpletimer/app.png b/apps/simpletimer/app.png new file mode 100644 index 000000000..f593a3a8b Binary files /dev/null and b/apps/simpletimer/app.png differ diff --git a/apps/simpletimer/gesture-tfmodel.js b/apps/simpletimer/gesture-tfmodel.js new file mode 100644 index 000000000..a29901ef5 --- /dev/null +++ b/apps/simpletimer/gesture-tfmodel.js @@ -0,0 +1 @@ +atob("HAAAAFRGTDMAABIAHAAEAAgADAAQABQAAAAYABIAAAADAAAAjA0AABAEAAD4AwAAPAAAAAQAAAABAAAADAAAAAgADAAEAAgACAAAAAgAAAAMAAAAEwAAAG1pbl9ydW50aW1lX3ZlcnNpb24ADQAAALADAACIAwAAWAMAAKQBAABcAQAAVAEAAEwBAABEAQAAEAEAAAgBAAAAAQAAHAAAAAQAAACu/P//BAAAAAUAAAAxLjUuMAAAAML8//8EAAAA0gAAAMJbJV3AgRwc/Nn1I0Qd5WDwPa0nY6nMbPyvfyOWOlqqf+64Juoa5kjpQVjoHTubf/NpJEH1Sqe0/PfJ5/o08zInA5f6fyg/vRaJEX9VQE5BPlRI0EP1Imq8NkNBLz3Q1hW5k9frf1Lfuc/rEwfGJqbG/txEf35Ey0jgICJ/B+vyu1FF8M+HA2ZcLhAX+QGB26MS3iLLDxnLNlvp9jbo8gM/5YEc0RoWA+W+Ih9T3AyBSNRX4Ew+739Y9R3p+cnS/dj283/BrQDWUu/y4Q8JAwj+5QAA4Pz//+T8//+q/f//BAAAACQAAABejWTlyU3qNn6j53/W2AR0lmjJ9d87YN1cNbBDobHjz5gdMFIY/f//HP3//yD9///m/f//BAAAADgAAADB////e////zwAAACt////5P////3///8yAAAAmP///53///9K////SP///2H///+J////c////yr+//8EAAAApAEAAA4mHQIO1TUu59UbG4FFDeDwNN3sEtUHD18IISYb5aXfCQ0g6/wWDMADFirm8M7t9f4H1eMQluL9Btze72b+5wXX08vbHvLxET4L+xHtFeHZ5NfHV/zUByIHzTf/rxUS28LqwaXnhn8izicXm7z7t+Ja6dHaVL8zE6oR6TwY3LUxFktGE/DcDQz71rkc9SrmL+zs6+3/yTse/xvcHcsIgenD5OHuJeT199TsxRCi6bVGzgb579Xj2vTt2g/RqTAbAsroB9oAtgbn7AkS8fEJ0O8x/nML+1Xf/cAqK8Yo9yvVIjztKSQT+NH09AdCIb/6Af/VD9+EI+vvKuvEEk9h5k8PtNrNIucw/xGBFVzOCQ3q/wH+BBtoCOf74smpLzb37xcvFlcQEPAMmP4o9+L5JU8QMgHG5wrINjRx8/UnPBc57cvo79oGAUzs5jTzFWznEvzP6s8Wf+gQHOn2QAYD/hE8Fuw45P8B0y0GCgr4AyrsAgjRH0XpGRM0+gD9YPoB++3wM/TcDn+fDf1lAAoaCtP4M/3kbQvrATrd6g/y7/rv6Kwj3Nr///8EAAAAGAAAAAYBAADiAQAA2P///5f+//+6////Mf///wAABgAIAAQABgAAAAQAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaP///w8AAABUT0NPIENvbnZlcnRlZC4AAQAAAAQAAAD09v//3AEAANABAADEAQAABAAAAAYAAACYAQAAQAEAANgAAACEAAAAOAAAAAQAAACK/v//BAAAABAAAAAEAAAAAQAAAAwAAAABAAAAAAAAAAAADgAYAAgADAAQAAcAFAAOAAAAAAAACAEAAAAcAAAAEAAAAAgAAAAEAAQABAAAAAEAAAAAAAAAAwAAAAoAAAAJAAAAAQAAAAAADgAaAAgADAAQAAcAFAAOAAAAAAAABQIAAAA8AAAAMAAAABQAAAAAAA4AGAAHAAgADAAQABQADgAAAAAAAAEBAAAALgAAAAEAAAAuAAAAAQAAAAoAAAABAAAACAAAAKr///8AAAABPAAAADAAAAAUAAAAEAAYAAAACAAMAAcAEAAUABAAAAAAAAABAQAAAAEAAAACAAAAAgAAAAEAAAAIAAAAAwAAAAUAAAAGAAAABwAAAAAADgAUAAAACAAMAAcAEAAOAAAAAAAAATAAAAAkAAAAEAAAAAwAEAAGAAgADAAHAAwAAAAAAAEBAQAAAAEAAAABAAAABQAAAAMAAAACAAAAAwAAAAQAAAAAAAoAEAAEAAgADAAKAAAAAwAAABAAAAAEAAAAAQAAAAIAAAABAAAACwAAAAEAAAAMAAAAAQAAAAsAAAANAAAAEAcAAJwGAAAcBgAAAAUAAPwDAAB4AwAAvAIAABgCAACMAQAACAEAAHQAAAA8AAAABAAAANj///8YAAAABAAAAAgAAABJZGVudGl0eQAAAAACAAAAAQAAAAYAAAAMAAwABAAAAAAACAAMAAAAHAAAAAQAAAAMAAAAYWNjZWxlcmF0aW9uAAAAAAQAAAABAAAAMgAAAAEAAAADAAAAmvn//wAAAAl0AAAABgAAAEQAAAAEAAAAjPn//zAAAAAkAAAAGAAAAAQAAAABAAAAgP////////8AAAAAAQAAAN6l9z4BAAAAOK72QgEAAAAAAAAAIAAAAHNlcXVlbnRpYWwvbWF4X3Bvb2xpbmcyZC9NYXhQb29sAAAAAAQAAAABAAAAAQAAAAEAAAAGAAAAKvr//wAAAAlsAAAACAAAACwAAAAEAAAAjPr//xgAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAEAAABVoaw7MAAAAHNlcXVlbnRpYWwvZGVuc2UvTWF0TXVsL1JlYWRWYXJpYWJsZU9wL3RyYW5zcG9zZQAAAAACAAAABgAAAAYAAACq+v//AAAACWwAAAAJAAAARAAAAAQAAACc+v//MAAAACQAAAAYAAAABAAAAAEAAACA/////////wAAAAABAAAA3qX3PgEAAAA4rvZCAQAAAAAAAAAYAAAAc2VxdWVudGlhbC9jb252MmRfMS9SZWx1AAAAAAQAAAABAAAALgAAAAEAAAAGAAAAMvv//wAAAAKQAAAAAgAAAGQAAAAEAAAAlPv//zwAAAAEAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAABUbvA6FaemOke74Do4TZA6gTLYOpTt+zofAAAAc2VxdWVudGlhbC9jb252MmRfMS9Db252MkRfYmlhcwABAAAABgAAANL7//8AAAAJnAAAAAMAAABkAAAABAAAADT8//88AAAABAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAQ3uTO3hzTDv12Yk71QcxO86dhDuqiJo7KQAAAHNlcXVlbnRpYWwvY29udjJkXzEvQ29udjJEL1JlYWRWYXJpYWJsZU9wAAAABAAAAAYAAAAFAAAAAQAAAA4AAACK/P//AAAACWQAAAAHAAAAQAAAAAQAAAB8/P//LAAAACAAAAAUAAAABAAAAAEAAACA/////////wEAAADfq9A+AQAAADPbz0IBAAAAAAAAABYAAABzZXF1ZW50aWFsL2NvbnYyZC9SZWx1AAAEAAAAAQAAAC4AAAABAAAADgAAAAr9//8AAAAC8AAAAAQAAADEAAAABAAAAGz9//98AAAABAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAIieCjtEoPM6zIP5OgV3xjosFgQ7+qsTOwl55zpbKvM6YZ8XO+0qyjoJ/CM7Eq0SO2aMIDuFGvM6HQAAAHNlcXVlbnRpYWwvY29udjJkL0NvbnYyRF9iaWFzAAAAAQAAAA4AAAAK/v//AAAACfwAAAALAAAAyAAAAAQAAABs/v//gAAAAAQAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAqzQ2O3QdIDs6/CM7Nm8CO3aeLTvLGkI7syAYO/bPHzs3TEc7Md4EO/GLVzu9y0A7uAdTO43FHzsnAAAAc2VxdWVudGlhbC9jb252MmQvQ29udjJEL1JlYWRWYXJpYWJsZU9wAAQAAAAOAAAABQAAAAEAAAADAAAAIv///wAAAAlgAAAACgAAAEAAAAAEAAAAFP///ywAAAAgAAAAFAAAAAQAAAABAAAABwAAAAAAAAABAAAAw8JCPwEAAAAAALZCAQAAAAAAzsIRAAAAYWNjZWxlcmF0aW9uX2ludDgAAAAEAAAAAQAAADIAAAABAAAAAwAAAJ7///8AAAACUAAAAAEAAAA0AAAAEAAAAAwADAAAAAAABAAIAAwAAAAUAAAABAAAAAEAAAAAAAAAAAAAAAEAAACD/yY7DQAAAElkZW50aXR5X2JpYXMAAAABAAAABgAAAAAADgAYAAgABwAMABAAFAAOAAAAAAAACWgAAAAFAAAATAAAABAAAAAMABQABAAIAAwAEAAMAAAALAAAACAAAAAUAAAABAAAAAEAAAAJAAAAAAAAAAEAAACacSU/AQAAALcXmUIBAAAAmoCwwg0AAABJZGVudGl0eV9pbnQ4AAAAAgAAAAEAAAAGAAAABQAAAGAAAABEAAAAKAAAABwAAAAEAAAA1v///wAAAAYCAAAAAAAGAAgABwAGAAAAAAAAcvL///8AAAARAgAAAAAACgAOAAcAAAAIAAoAAAAAAAAJBAAAAAAACgAMAAcAAAAIAAoAAAAAAAADAwAAAA==") diff --git a/apps/simpletimer/gesture-tfnames.js b/apps/simpletimer/gesture-tfnames.js new file mode 100644 index 000000000..ba0d58546 --- /dev/null +++ b/apps/simpletimer/gesture-tfnames.js @@ -0,0 +1 @@ +"swipeleft,swiperight,upup,waggle,clap2" diff --git a/apps/smtswch/ChangeLog b/apps/smtswch/ChangeLog new file mode 100644 index 000000000..6d3bcf353 --- /dev/null +++ b/apps/smtswch/ChangeLog @@ -0,0 +1 @@ +0.01: New App! See the README.MD for details on how to use it. \ No newline at end of file diff --git a/apps/smtswch/README.md b/apps/smtswch/README.md new file mode 100644 index 000000000..3ac6658c9 --- /dev/null +++ b/apps/smtswch/README.md @@ -0,0 +1,72 @@ +# Smart Switch app for BangleJS + +This app allows you to remotely control devices (or anything else you like!) with: + +* [Bangle.js](https://www.espruino.com/Bangle.js) (Hackable JavaScript Smartwatch) +* [EspruinoHub](https://github.com/espruino/EspruinoHub) (Bluetooth Low Energy -> MQTT bridge) +* [Node-RED](https://nodered.org) (Flow-based programming tool) + +![Demo of Smart Switch app in action](https://raw.githubusercontent.com/wdmtech/BangleApps/add-video/apps/smtswch/demo.gif) + +* Swipe right to turn a device ON +* Swipe left to turn a device OFF +* BTN1 (top-right) - Previous device (page) +* BTN3 (bottom-right) - Next device (page) + +> Currently, devices can only be added/removed/changed by editing them in the app's source code. + +# How to use + +First, you'll need a device that supports BLE. + +Install EspruinoHub following the directions at [https://github.com/espruino/EspruinoHub](https://github.com/espruino/EspruinoHub) +Install [Node-RED](https://nodered.org/docs/getting-started) + +## Example Node-RED flow + +Import the following JSON into Node-RED and configure the MQTT IN node to use your EspruinoHub's MQTT instance (default port is 1883): + +```JSON +[{"id":"87c6f73e.f22038","type":"mqtt in","z":"a256522.ca0b0b","name":"⌚️BangleJS data","topic":"/ble/advertise/ec:5a:c1:a7:fc:91/data","qos":"2","datatype":"auto","broker":"b961407a.91beb","x":860,"y":100,"wires":[["c37809de.3fc538"]]},{"id":"c37809de.3fc538","type":"function","z":"a256522.ca0b0b","name":"Set topic, remove quotes","func":"msg.topic = \"any_topic_here\";\nmsg.payload = msg.payload.replace(/['\"]+/g, \"\")\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":100,"wires":[["9019be89.5b6d5"]]},{"id":"9019be89.5b6d5","type":"debug","z":"a256522.ca0b0b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1250,"y":100,"wires":[]},{"id":"b961407a.91beb","type":"mqtt-broker","z":"","name":"","broker":"192.168.1.22","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"hello_there","birthQos":"0","birthPayload":"","closeTopic":"bye_now","closeQos":"0","closePayload":"true","willTopic":"bye_now","willQos":"0","willPayload":"true"}] +``` + +Replace the topic of the MQTT IN node to use the ID of your Bangle.js device, e.g: + +`/ble/advertise/ec:5a:c1:a7:fc:91/data` + +Once you see the MQTT IN node is configured correctly (it says `connected` below the node itself), try swiping in the Smart Switch app, and +you should see some data in the Debug node. + +The possibilities for switching things on and off via Bangle.js are now endless. Have fun! + +# How it works + +This is the code that does the actual [BLE advertising](https://www.espruino.com/BLE%20Advertising) on the watch itself: + +```JS +NRF.setAdvertising({ + 0xFFFF: [currentPage, page.state] +}); +``` + +# Not working? + +If you can't see any data in Node-RED after swiping, check to see if your device is advertising by visiting port 1888 of your EspruinoHub instance: + +You should see something like the following: + +``` +ec:5a:c1:a7:fc:91 - Bangle.js fc91 (RSSI -83) + ffff => {"data":"1,1"} +``` + +# Any comments? + +[Tweet me!](https://twitter.com/BillyWhizzkid) + +# Future + +PRs welcome! + +[ ] Add an HTML GUI for configuring devices inside the Bangle.js App Loader +[ ] Allow enable/disable of buzz/beep on change of device state diff --git a/apps/smtswch/app-icon.js b/apps/smtswch/app-icon.js new file mode 100644 index 000000000..9153bd3ca --- /dev/null +++ b/apps/smtswch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA")) \ No newline at end of file diff --git a/apps/smtswch/app.js b/apps/smtswch/app.js new file mode 100644 index 000000000..e8491a065 --- /dev/null +++ b/apps/smtswch/app.js @@ -0,0 +1,79 @@ + +// Learn more! +// https://www.espruino.com/Reference#l_NRF_setAdvertising +// https://www.espruino.com/Bangle.js#buttons + +// Initial graphics setup +g.clear(); +g.setFontAlign(0, 0); // center font +// g.setFont("6x8", 8); // bitmap font, 8x magnified +g.setFont("Vector", 40); // vector font, 80px + +// Let the app begin! +const storage = require("Storage"); + +let currentPage = 0; +let pages = [ + { + name: "Downstairs", + icon: "light", + state: false + }, + { + name: "Upstairs", + icon: "switch", + state: false + }]; + +function loadPage(page) { + const icon = page.state ? page.icon + "-on" : page.icon + "-off"; + Bangle.beep(); + g.clear(); + g.setFont("Vector", 10); + g.drawString("prev", g.getWidth() - 25, 20); + g.drawString("next", g.getWidth() - 25, 220); + g.setFont("Vector", 15); + g.drawString(page.name, g.getWidth() / 2, 200); + g.setFont("Vector", 40); + g.drawString(page.state ? "On" : "Off", g.getWidth() / 2, g.getHeight() / 2); + g.drawImage(storage.read(`${icon}.img`), g.getWidth() / 2 - 24, g.getHeight() / 2 - 24 - 50); +} + +function prevPage() { + if (currentPage > 0) { + currentPage--; + loadPage(pages[currentPage]); + } +} + +function nextPage() { + if (currentPage < pages.length - 1) { + currentPage++; + loadPage(pages[currentPage]); + } +} + +function swipe(dir) { + + const page = pages[currentPage]; + + page.state = dir == 1; + + NRF.setAdvertising({ + 0xFFFF: [currentPage, page.state] + }); + + loadPage(page); + + // optional - this keeps the watch LCD lit up + g.flip(); + + Bangle.buzz(); +} + +Bangle.on('swipe', swipe); + +setWatch(prevPage, BTN, {edge: "rising", debounce: 50, repeat: true}); +setWatch(nextPage, BTN3, {edge: "rising", debounce: 50, repeat: true}); + +loadPage(pages[currentPage]); \ No newline at end of file diff --git a/apps/smtswch/app.png b/apps/smtswch/app.png new file mode 100644 index 000000000..9ed00c6b6 Binary files /dev/null and b/apps/smtswch/app.png differ diff --git a/apps/smtswch/light-off.js b/apps/smtswch/light-off.js new file mode 100644 index 000000000..c6e6b7e77 --- /dev/null +++ b/apps/smtswch/light-off.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGeJAAwttGMotLGMQiD1uzAAWtGEgtE64ACF5IwbFwYtESUouGFpowaFywvXDIS7CFyIwXLwouSF6peF1ovrRqowWF4heEstlApIveDolfAAIEGF76OGFYQuMF6+zdo4uOF6+tF49lFwK9KF7AAJLxovUGBiOhF+IwLF5guWF+AwKF5YuYGBQvKFzQwJF5IucGBAvIFzwwHF44ugF+AwFF4wui/2CABQvrr1YAAIvjrwoDAAwvjFhFeR8onDX/4vcXxIvkYA73BR0gACYA4umMI4uoGAouqAH4AK")) \ No newline at end of file diff --git a/apps/smtswch/light-on.js b/apps/smtswch/light-on.js new file mode 100644 index 000000000..a3e7c322f --- /dev/null +++ b/apps/smtswch/light-on.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AT5gAGFtoxlFpYxhFp4xeFyYwaFyowZF9wuXGC4vuFzIwVF9wdK53OApIwYDRHN6gAC5oFFF8QoC5wyIMRAvZ5wkERgJbCBQqPfEoKGGL4S/j5i3GFwS/jK5BnIF6owMW4S8KFygvKSIQDFF85bBF8QwKF54uUF+AwJF5wuWF+AwIF5ouYGBAvMFzQwHF5YucGAwvKFzwwFF5IugAAOCAA1erAABF0X+rwoDAAwvjFhFeMYIvkE4QAHF8a/vwS+JF8jAHe4KOkGAaQFroumAAUrAAQtpGAgusAH4A/AFI=")) \ No newline at end of file diff --git a/apps/smtswch/switch-off.js b/apps/smtswch/switch-off.js new file mode 100644 index 000000000..58e6e94e6 --- /dev/null +++ b/apps/smtswch/switch-off.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AI1gAEFlgAEz2WAAm6ABwuPxGCAAgJC0wwVGJQtIAAWIGIWXF6gxIEAItIMAgABMCowGFyKSGGCulRhQvHegovVLySRGF6QwBLyjyaF4IuQBAaQX3WmF5wIG0ovXXxaZJYDLuMF8SPHRRCPed4mIcwaNJd7YvBAA4uKH4OXF63+/wuHLxi+YF4JgHLxiOXFwJgHLxmmFwYvXGAqNQFzAwELxKMBdjQwJMAwtCRgovRFpDDIAAjqEFyItLGRQWQAH4A/AH4A/AH4A/AH4A/AH4AP")) \ No newline at end of file diff --git a/apps/smtswch/switch-on.js b/apps/smtswch/switch-on.js new file mode 100644 index 000000000..9153bd3ca --- /dev/null +++ b/apps/smtswch/switch-on.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA")) \ No newline at end of file diff --git a/apps/snake/ChangeLog b/apps/snake/ChangeLog new file mode 100644 index 000000000..428718739 --- /dev/null +++ b/apps/snake/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Performance and graphic improvements, game pause, beep and buzz \ No newline at end of file diff --git a/apps/snake/README.md b/apps/snake/README.md new file mode 100644 index 000000000..483eae7a9 --- /dev/null +++ b/apps/snake/README.md @@ -0,0 +1,14 @@ +# Snake + +![Screenshot](https://i.imgur.com/bXQjxhB.png) + +The legentary classic game is now available on Bangle.js! +Eat apples and don't bite your tail. + +## Controls + +- UP: BTN1 +- DOWN: BTN3 +- LEFT: BTN4 +- RIGHT: BTN5 +- PAUSE: BTN2 diff --git a/apps/snake/snake-icon.js b/apps/snake/snake-icon.js new file mode 100644 index 000000000..305061003 --- /dev/null +++ b/apps/snake/snake-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ADE3m9hsIusrdhGIM3LtU3g0GAgQxlEwIqBmEAgEGF4QwkF4c3F4MxF4dbF8qLDrYHDre74IABF8QwBLoaPDF8wPKF96/jF/4v/F/4vrrc3AIQsnsIAKF94wiFxgv/R//+m4ABrYALBwIpYFwwAQLC4v/F7gXGF91hACovWFqwwUF4VbF7IwUFzSRVF1gwCF9wwZFyoA/AH4A/AH4A/AGg=")) \ No newline at end of file diff --git a/apps/snake/snake.js b/apps/snake/snake.js new file mode 100644 index 000000000..4532e0113 --- /dev/null +++ b/apps/snake/snake.js @@ -0,0 +1,155 @@ +Bangle.setLCDMode("120x120"); + +const H = g.getWidth(); +const W = g.getHeight(); +let running = true; +let score = 0; +let d; +const gridSize = 20; +const tileSize = 6; +let nextX = 0; +let nextY = 0; +const defaultTailSize = 3; +let tailSize = defaultTailSize; +const snakeTrail = []; +const snake = { x: 10, y: 10 }; +const apple = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; + +function drawBackground(){ + g.setColor("#000000"); + g.fillRect(0, 0, H, W); +} + +function drawApple(){ + g.setColor("#FF0000"); + g.fillCircle((apple.x * tileSize) + tileSize/2, (apple.y * tileSize) + tileSize/2, tileSize/2); +} + +function drawSnake(){ + g.setColor("#008000"); + for (let i = 0; i < snakeTrail.length; i++) { + g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize); + + //snake bites it's tail + if (snakeTrail[i].x === snake.x && snakeTrail[i].y === snake.y && tailSize > defaultTailSize) { + Bangle.buzz(1000); + gameOver(); + } + } +} + +function drawScore(){ + g.setColor("#FFFFFF"); + g.setFont("6x8"); + g.setFontAlign(0, 0); + g.drawString("Score:" + score, W / 2, 10); +} + +function gameStart() { + running = true; + score = 0; +} + +function gameOver() { + g.clear(); + g.setColor("#FFFFFF"); + g.setFont("6x8"); + g.drawString("GAME OVER!", W / 2, H / 2 - 10); + g.drawString("Tap to Restart", W / 2, H / 2 + 10); + running = false; + tailSize = defaultTailSize; +} + +function draw() { + if (!running) { + return; + } + + g.clear(); + + // move snake in next pos + snake.x += nextX; + snake.y += nextY; + + // snake over game world + if (snake.x < 0) { + snake.x = gridSize - 1; + } + if (snake.x > gridSize - 1) { + snake.x = 0; + } + + if (snake.y < 0) { + snake.y = gridSize - 1; + } + if (snake.y > gridSize - 1) { + snake.y = 0; + } + + //snake bite apple + if (snake.x === apple.x && snake.y === apple.y) { + Bangle.beep(20); + tailSize++; + score++; + + apple.x = Math.floor(Math.random() * gridSize); + apple.y = Math.floor(Math.random() * gridSize); + drawApple(); + } + + drawBackground(); + drawApple(); + drawSnake(); + drawScore(); + + //set snake trail + snakeTrail.push({ x: snake.x, y: snake.y }); + while (snakeTrail.length > tailSize) { + snakeTrail.shift(); + } + + g.flip(); +} + +// input +setWatch(() => {// Up + if (d !== 'd') { + nextX = 0; + nextY = -1; + d = 'u'; + } +}, BTN1, { repeat: true }); +setWatch(() => {// Down + if (d !== 'u') { + nextX = 0; + nextY = 1; + d = 'd'; + } +}, BTN3, { repeat: true }); +setWatch(() => {// Left + if (d !== 'r') { + nextX = -1; + nextY = 0; + d = 'l'; + } +}, BTN4, { repeat: true }); +setWatch(() => {// Right + if (d !== 'l') { + nextX = 1; + nextY = 0; + d = 'r'; + } +}, BTN5, { repeat: true }); +setWatch(() => {// Pause + running = !running; +}, BTN2, { repeat: true }); + +Bangle.on('touch', button => { + if (!running) { + gameStart(); + } +}); + +// render X times per second +const x = 5; +setInterval(draw, 1000 / x); \ No newline at end of file diff --git a/apps/snake/snake.png b/apps/snake/snake.png new file mode 100644 index 000000000..04564a8f7 Binary files /dev/null and b/apps/snake/snake.png differ diff --git a/apps/speedo/ChangeLog b/apps/speedo/ChangeLog new file mode 100644 index 000000000..4ea81e266 --- /dev/null +++ b/apps/speedo/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add widgets to app +0.03: Use offscreen buffer (not doublebuffer) + Use 'locale' to get internationalised speed diff --git a/apps/speedo/speedo.js b/apps/speedo/speedo.js index 2fada429a..9b8a1d44b 100644 --- a/apps/speedo/speedo.js +++ b/apps/speedo/speedo.js @@ -1,24 +1,33 @@ Bangle.setGPSPower(1); -Bangle.setLCDMode("doublebuffered"); +var buf = Graphics.createArrayBuffer(240,120,1,{msb:true}); var lastFix = {fix:0,satellites:0}; function onGPS(fix) { lastFix = fix; - g.clear(); - g.setFontAlign(0,0); - g.setFont("6x8"); - g.drawString(fix.satellites+" satellites",120,6); + buf.clear(); + buf.setFontAlign(0,0); + buf.setFont("6x8"); + buf.drawString(fix.satellites+" satellites",120,6); if (fix.fix) { + var speed = require("locale").speed(fix.speed); + var m = speed.match(/([0-9,\.]+)(.*)/); // regex splits numbers from units var txt = (fix.speed<20) ? fix.speed.toFixed(1) : Math.round(fix.speed); + var value = m[1], units = m[2]; var s = 80; - g.setFontVector(s); - g.drawString(txt,120,80); - g.setFont("6x8",2); - g.drawString("km/h",120,80+16+s/2); + buf.setFontVector(s); + buf.drawString(value,120,10+s/2); + buf.setFont("6x8",2); + buf.drawString(units,120,s+26); } else { - g.setFont("6x8",2); - g.drawString("Waiting for GPS",120,80); + buf.setFont("6x8",2); + buf.drawString("Waiting for GPS",120,56); } + g.reset(); + g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,70); g.flip(); } +g.clear(); onGPS(lastFix); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + Bangle.on('GPS', onGPS); diff --git a/apps/swatch/ChangeLog b/apps/swatch/ChangeLog index 3246eeced..caa74a1ba 100644 --- a/apps/swatch/ChangeLog +++ b/apps/swatch/ChangeLog @@ -5,3 +5,5 @@ Fixed bug from 0.01 where BN1 (reset) could clear the lap log when timer is running 0.04: Changed save file filename, add interface.html to allow laps to be loaded 0.05: Added widgets +0.06: Added total running time, moved lap time to smaller display, total run time now appends as first entry in array, saving now saves last lap as well +0.07: Ensure seconds counter is in sync with subseconds (fix #341) diff --git a/apps/swatch/interface.html b/apps/swatch/interface.html index 928c5fe39..45391fb6e 100644 --- a/apps/swatch/interface.html +++ b/apps/swatch/interface.html @@ -17,11 +17,12 @@ function getLapTimes() {
\n`; lapData.forEach((lap,lapIndex) => { lap.date = lap.n.substr(7,16).replace("_"," "); + lap.elapsed = lap.d.shift(); // remove first item html += `
${lap.date}
-
${lap.d.length} Laps
+
${lap.d.length} Laps, total time ${lap.elapsed}
diff --git a/apps/swatch/stopwatch.js b/apps/swatch/stopwatch.js index 6f8ad9e34..478de2712 100644 --- a/apps/swatch/stopwatch.js +++ b/apps/swatch/stopwatch.js @@ -1,8 +1,11 @@ +var tTotal = Date.now(); var tStart = Date.now(); var tCurrent = Date.now(); var started = false; -var timeY = 60; +var timeY = 45; var hsXPos = 0; +var TtimeY = 75; +var ThsXPos = 0; var lapTimes = []; var displayInterval; @@ -12,6 +15,7 @@ function timeToText(t) { var hs = Math.floor(t/10)%100; return mins+":"+("0"+secs).substr(-2)+"."+("0"+hs).substr(-2); } + function updateLabels() { g.reset(1); g.clearRect(0,23,g.getWidth()-1,g.getHeight()-24); @@ -23,36 +27,54 @@ function updateLabels() { g.setFont("6x8",1); g.setFontAlign(-1,-1); for (var i in lapTimes) { - if (i<16) - {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 30 + i*8);} - else if (i<32) - {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 30 + (i-16)*8);} + if (i<15) + {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 40 + i*8);} + else if (i<30) + {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 40 + (i-15)*8);} } drawsecs(); } + function drawsecs() { var t = tCurrent-tStart; - g.reset(1); - g.setFont("Vector",48); - g.setFontAlign(0,0); + var Tt = tCurrent-tTotal; var secs = Math.floor(t/1000)%60; var mins = Math.floor(t/60000); var txt = mins+":"+("0"+secs).substr(-2); + var Tsecs = Math.floor(Tt/1000)%60; + var Tmins = Math.floor(Tt/60000); + var Ttxt = Tmins+":"+("0"+Tsecs).substr(-2); var x = 100; - g.clearRect(0,timeY-26,200,timeY+26); - g.drawString(txt,x,timeY); + var Tx = 125; + g.reset(1); + g.setFont("Vector",38); + g.setFontAlign(0,0); + g.clearRect(0,timeY-21,200,timeY+21); + g.drawString(Ttxt,x,timeY); hsXPos = 5+x+g.stringWidth(txt)/2; + g.setFont("6x8",2); + g.clearRect(0,TtimeY-7,200,TtimeY+7); + g.drawString(txt,Tx,TtimeY); + ThsXPos = 5+Tx+g.stringWidth(Ttxt)/2; drawms(); } + function drawms() { var t = tCurrent-tStart; var hs = Math.floor(t/10)%100; + var Tt = tCurrent-tTotal; + var Ths = Math.floor(Tt/10)%100; g.setFontAlign(-1,0); g.setFont("6x8",2); g.clearRect(hsXPos,timeY,220,timeY+20); - g.drawString("."+("0"+hs).substr(-2),hsXPos,timeY+10); + g.drawString("."+("0"+Ths).substr(-2),hsXPos-5,timeY+14); + g.setFont("6x8",1); + g.clearRect(ThsXPos,TtimeY,220,TtimeY+5); + g.drawString("."+("0"+hs).substr(-2),ThsXPos-5,TtimeY+3); } + function getLapTimesArray() { + lapTimes.push(tCurrent-tTotal); return lapTimes.map(timeToText).reverse(); } @@ -61,7 +83,8 @@ setWatch(function() { // Start/stop Bangle.beep(); if (started) tStart = Date.now()+tStart-tCurrent; - tCurrent = Date.now(); + tTotal = Date.now()+tTotal-tCurrent; + tCurrent = Date.now(); if (displayInterval) { clearInterval(displayInterval); displayInterval = undefined; @@ -71,35 +94,40 @@ setWatch(function() { // Start/stop displayInterval = setInterval(function() { var last = tCurrent; if (started) tCurrent = Date.now(); - if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) + if (Math.floor((last-tStart)/1000)!=Math.floor((tCurrent-tStart)/1000) || + Math.floor((last-tTotal)/1000)!=Math.floor((tCurrent-tTotal)/1000)) drawsecs(); else drawms(); }, 20); }, BTN2, {repeat:true}); + setWatch(function() { // Lap Bangle.beep(); if (started) { tCurrent = Date.now(); lapTimes.unshift(tCurrent-tStart); } - tStart = tCurrent; if (!started) { // save - var timenow= Date(); var filename = "swatch-"+(new Date()).toISOString().substr(0,16).replace("T","_")+".json"; + if (tCurrent!=tStart) + lapTimes.unshift(tCurrent-tStart); // this maxes out the 28 char maximum require("Storage").writeJSON(filename, getLapTimesArray()); + tStart = tCurrent = tTotal = Date.now(); + lapTimes = []; E.showMessage("Laps Saved","Stopwatch"); setTimeout(updateLabels, 1000); } else { + tStart = tCurrent; updateLabels(); } }, BTN1, {repeat:true}); setWatch(function() { // Reset if (!started) { - Bangle.beep(); - tStart = tCurrent = Date.now(); - lapTimes = []; + Bangle.beep(); + tStart = tCurrent = tTotal = Date.now(); + lapTimes = []; } updateLabels(); }, BTN3, {repeat:true}); diff --git a/apps/tabata/tabata-icon.js b/apps/tabata/tabata-icon.js new file mode 100644 index 000000000..0a360fbbe --- /dev/null +++ b/apps/tabata/tabata-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg96iOZzORgMZAYMQCxsBCwIAGDBsZyIDCCYQyBBAQuLEwWZCYYJBFx8BL4ITDjJILBgcJDYIGEyBCHFYQoGC4Y2BRBAJBC4wrDC4w9BiC6BC6Q6DAYIXpCIcJC/4X/C/4X/C50BjITCjOZD4IXOhOZCYMBAYIwEC52QC7QMDC/4XCzBH/C/4X/C5OQa6INBC4WRyIvQC5IOBC5YTBAYILByAECBAIXLA4IwBAYITBgMZDYIXGHgZICEAWRAYQfEAgIXIFAIIBiERCwYKCCQQOBBIQAvA")) diff --git a/apps/tabata/tabata.js b/apps/tabata/tabata.js new file mode 100644 index 000000000..603cf96ee --- /dev/null +++ b/apps/tabata/tabata.js @@ -0,0 +1,130 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var settings = require("Storage").readJSON("tabata.json",1)||{}; +settings.pause = settings.pause || 10; +settings.training = settings.training || 20; +settings.rounds = settings.rounds || 8; + +const MAX_SECONDS = 100; + +function debounce(callback, ms) { + var timer; + return () => { + if (timer) clearTimeout(timer); + timer = setTimeout(callback, ms); + }; +} + +function saveSettings() { + require("Storage").write("tabata.json",JSON.stringify(settings)); +} + +var saveSettingsDebounce = debounce(saveSettings, 250); + +function showMainMenu() { + const menu = { + '': { 'title': 'Tabata Training' }, + '>> Start >>': ()=> { + startTabata(); + }, + 'Pause sec.': { + value: settings.pause, + onchange: function(v){ + if (v<0)v=MAX_SECONDS; + if (v>MAX_SECONDS)v=0; + settings.pause=v; + this.value=v; + saveSettingsDebounce(); + } + }, + 'Trainig sec.': { + value: settings.training, + onchange: function(v){if (v<0)v=MAX_SECONDS;if (v>MAX_SECONDS)v=0;settings.training=v; + this.value=v; + saveSettingsDebounce(); + } + }, + 'Rounds': { + value: settings.rounds, + onchange: function(v){if (v<0)v=MAX_SECONDS;if (v>MAX_SECONDS)v=0;settings.rounds=v;this.value=v; + saveSettingsDebounce(); + } + }, + '< Back': () => load() + }; + menu['< Back'] = ()=>{load();}; + return E.showMenu(menu); +} + +function startTabata() { + g.clear(); + Bangle.setLCDMode("doublebuffered"); + g.flip(); + var pause = settings.pause, + training = settings.training, + round = 1, + active = true, + clearBtn1, clearBtn2, clearBtn3, timer; + Bangle.buzz(1000, 1); + + function exitTraining() { + clearTimeout(timer); + clearWatch(clearBtn1); + clearWatch(clearBtn2); + clearWatch(clearBtn2); + showMainMenu(); + } + + clearBtn1 = setWatch(exitTraining, BTN1); + clearBtn2 = setWatch(exitTraining, BTN2); + clearBtn3 = setWatch(exitTraining, BTN3); + + + timer = setInterval(function() { + if (round > settings.rounds) { + exitTraining(); + return; + } + + if (active) { + drawCountDown(round, training, active); + training--; + } else { + drawCountDown(round, pause, active); + pause--; + if (pause !== 0) { + Bangle.buzz(50, 0.2); + } + } + + if (training === 0) { + training = settings.training; + active = false; + Bangle.buzz(500, 1); + } + if (pause === 0) { + round++; + pause = settings.pause; + active = true; + Bangle.buzz(1000, 1); + } + }, 1000); +} + +function drawCountDown(round, count, active) { + g.clear(); + + g.setFontAlign(0,0); + g.setFont("6x8", 2); + g.drawString("Round " + round + "/" + settings.rounds,120,6); + + g.setFont("6x8", 6); + g.drawString("" + count,120,80); + + g.setFont("6x8",2); + g.drawString(active ? "Training" : "Pause", 120,45); + g.flip(); +} + +showMainMenu(); diff --git a/apps/tabata/tabata.png b/apps/tabata/tabata.png new file mode 100644 index 000000000..f0aaadede Binary files /dev/null and b/apps/tabata/tabata.png differ diff --git a/apps/torch/ChangeLog b/apps/torch/ChangeLog index 5560f00bc..8e76b717a 100644 --- a/apps/torch/ChangeLog +++ b/apps/torch/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342) diff --git a/apps/torch/widget.js b/apps/torch/widget.js index e3d1ea22f..a5002ea71 100644 --- a/apps/torch/widget.js +++ b/apps/torch/widget.js @@ -1,18 +1,26 @@ +(function() { var clickTimes = []; -var CLICK_COUNT = 4; // number of taps -var CLICK_PERIOD = 1; // second +var clickPattern = ""; +var TAPS = 4; // number of taps +var PERIOD = 1; // seconds // we don't actually create/draw a widget here at all... - Bangle.on("lcdPower",function(on) { // First click (that turns LCD on) isn't given to // setWatch, so handle it here - if (on) clickTimes=[getTime()]; + if (!on) return; + clickTimes=[getTime()]; + clickPattern="x"; }); -setWatch(function(e) { - while (clickTimes.length>=CLICK_COUNT) clickTimes.shift(); +function tap(e,c) { + clickPattern = clickPattern.substr(-3)+c; + while (clickTimes.length>=TAPS) clickTimes.shift(); clickTimes.push(e.time); var clickPeriod = e.time-clickTimes[0]; - if (clickTimes.length==CLICK_COUNT && clickPeriod app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) }) - .filter(app=>app.type=="app" || app.type=="clock" || !app.type) - .sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; - }); -} +let icons = {}; const HEIGHT = g.getHeight(); const WIDTH = g.getWidth(); const HALF = WIDTH/2; -const ANIMATION_FRAME = 3; -const ANIMATION_STEP = HALF / ANIMATION_FRAME; +const ORIGINAL_ICON_SIZE = 48; + +const STATE = { + settings_open: false, + index: 0, + target: 240, + offset: 0 +}; function getPosition(index){ return (index*HALF); } -let current_app = 0; -let target = 0; -let slideOffset = 0; +function getApps(){ + const exit_app = { + name: 'Exit', + special: true + }; + const raw_apps = Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) }) + .filter(app=>app.type=="app" || app.type=="clock" || !app.type) + .sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }).map(raw => ({ + name: raw.name, + src: raw.src, + icon: raw.icon, + version: raw.version + })); -const back = { - name: 'BACK', - back: true -}; - -const apps = [back].concat(getApps()); -apps.push(back); - -function noIcon(x, y, size){ - const half = size/2; - g.setColor(1,1,1); - g.setFontAlign(-0,0); - const fontSize = Math.floor(size / 30 * 2); - g.setFont('6x8', fontSize); - if(fontSize) g.drawString('-?-', x+1.5, y); - g.drawRect(x-half, y-half, x+half, y+half); + const apps = [Object.assign({}, exit_app)].concat(raw_apps); + apps.push(exit_app); + return apps.map((app, i) => { + app.x = getPosition(i); + return app; + }); } -function drawIcons(offset){ - apps.forEach((app, i) => { - const x = getPosition(i) + HALF - offset; - const y = HALF - (HALF*0.3);//-(HALF*0.7); - let diff = (x - HALF); - if(diff < 0) diff *=-1; - let size = 30; - if((diff*0.5) < size) size -= (diff*0.5); - else size = 0; +const APPS = getApps(); - const scale = size / 30; - if(size){ - let c = size / 30 * 2; - c = c -1; - if(c < 0) c = 0; - - if(app.back){ - g.setFont('6x8', 1); - g.setFontAlign(0, -1); - g.setColor(c,c,c); - g.drawString('Back', HALF, HALF); - return; - } - // icon - const icon = app.icon ? Storage.read(app.icon) : null; - if(icon){ - try { - g.drawImage(icon, x-(scale*24), y-(scale*24), { scale: scale }); - } catch(e){ - noIcon(x, y, size); - } - }else{ - noIcon(x, y, size); - } - //text - g.setFont('6x8', 1); - g.setFontAlign(0, -1); - g.setColor(c,c,c); - g.drawString(app.name, HALF, HEIGHT - (HALF*0.7)); - - const type = app.type ? app.type : 'App'; - const version = app.version ? app.version : '0.00'; - const info = type+' v'+version; - g.setFontAlign(0,1); - g.setFont('4x6', 0.25); - g.setColor(c,c,c); - g.drawString(info, HALF, 110, { scale: scale }); - } - }); +function noIcon(x, y, scale){ + if(scale < 0.2) return; + g.setColor(scale, scale, scale); + g.setFontAlign(0,0); + g.setFont('6x8',settings.highres ? 6:3); + g.drawString('x_x', x+1.5, y); + const h = (ORIGINAL_ICON_SIZE/3); + g.drawRect(x-h, y-h, x+h, y+h); } -function draw(ignoreLoop){ +function render(){ + const start = Date.now(); + + const ANIMATION_FRAME = settings.frame; + const ANIMATION_STEP = Math.floor(HALF / ANIMATION_FRAME); + const THRESHOLD = ANIMATION_STEP - 1; + g.clear(); - drawIcons(slideOffset); + const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF ); + + visibleApps.forEach(app => { + + const x = app.x+HALF-STATE.offset; + const y = HALF - (HALF*0.3); + + let dist = HALF - x; + if(dist < 0) dist *= -1; + + const scale = 1 - (dist / HALF); + + if(!scale) return; + + if(app.special){ + const font = settings.highres ? '6x8' : '4x6'; + const fontSize = settings.highres ? 2 : 1; + g.setFont(font, fontSize); + g.setColor(scale,scale,scale); + g.setFontAlign(0,0); + g.drawString(app.name, HALF, HALF); + return; + } + + //draw icon + const icon = app.icon ? + icons[app.name] ? icons[app.name] : Storage.read(app.icon) + : null; + + if(icon){ + icons[app.name] = icon; + try { + const rescale = settings.highres ? scale*ORIGINAL_ICON_SIZE : (scale*(ORIGINAL_ICON_SIZE/2)); + const imageScale = settings.highres ? scale*2 : scale; + g.drawImage(icon, x-rescale, y-rescale, { scale: imageScale }); + } catch(e){ + noIcon(x, y, scale); + } + }else{ + noIcon(x, y, scale); + } + + //draw text + g.setColor(scale,scale,scale); + if(scale > 0.1){ + const font = settings.highres ? '6x8': '4x6'; + const fontSize = settings.highres ? 2 : 1; + g.setFont(font, fontSize); + g.setFontAlign(0,0); + g.drawString(app.name, HALF, HEIGHT/4*3); + } + + if(settings.highres){ + const type = app.type ? app.type : 'App'; + const version = app.version ? app.version : '0.00'; + const info = type+' v'+version; + g.setFontAlign(0,1); + g.setFont('6x8', 1.5); + g.setColor(scale,scale,scale); + g.drawString(info, HALF, 215, { scale: scale }); + } + + }); + + const duration = Math.floor(Date.now()-start); + if(settings.debug){ + g.setFontAlign(0,1); + g.setColor(0, 1, 0); + const fontSize = settings.highres ? 2 : 1; + g.setFont('4x6',fontSize); + g.drawString('Render: '+duration+'ms', HALF, HEIGHT); + } g.flip(); - if(slideOffset == target) return; - if(slideOffset < target) slideOffset+= ANIMATION_STEP; - else if(slideOffset > target) slideOffset -= ANIMATION_STEP; - if(!ignoreLoop) draw(); + if(STATE.offset == STATE.target) return; + + if(STATE.offset < STATE.target) STATE.offset += ANIMATION_STEP; + else if(STATE.offset > STATE.target) STATE.offset -= ANIMATION_STEP; + + if(STATE.offset >= STATE.target-THRESHOLD && STATE.offset < STATE.target) STATE.offset = STATE.target; + if(STATE.offset <= STATE.target+THRESHOLD && STATE.offset > STATE.target) STATE.offset = STATE.target; + setTimeout(render, 0); } function animateTo(index){ - target = getPosition(index); - draw(); -} -function goTo(index){ - current_app = index; - target = getPosition(index); - slideOffset = target; - draw(true); + STATE.index = index; + STATE.target = getPosition(index); + render(); } -goTo(1); +function jumpTo(index){ + STATE.index = index; + STATE.target = getPosition(index); + STATE.offset = STATE.target; + render(); +} function prev(){ - if(current_app == 0) goTo(apps.length-1); - current_app -= 1; - if(current_app < 0) current_app = 0; - animateTo(current_app); + if(STATE.settings_open) return; + if(STATE.index == 0) jumpTo(APPS.length-1); + setTimeout(() => { + if(!settings.animation) jumpTo(STATE.index-1); + else animateTo(STATE.index-1); + },1); } function next(){ - if(current_app == apps.length-1) goTo(0); - current_app += 1; - if(current_app > apps.length-1) current_app = apps.length-1; - animateTo(current_app); + if(STATE.settings_open) return; + if(STATE.index == APPS.length-1) jumpTo(0); + setTimeout(() => { + if(!settings.animation) jumpTo(STATE.index+1); + else animateTo(STATE.index+1); + },1); } -function run() { - const app = apps[current_app]; - if(app.back) return load(); +function run(){ + + const app = APPS[STATE.index]; + if(app.name == 'Exit') return load(); + if (Storage.read(app.src)===undefined) { E.showMessage("App Source\nNot found"); - setTimeout(draw, 2000); + setTimeout(render, 2000); } else { Bangle.setLCDMode(); g.clear(); @@ -149,15 +208,12 @@ function run() { E.showMessage("Loading..."); load(app.src); } + } - -setWatch(prev, BTN1, { repeat: true }); -setWatch(next, BTN3, { repeat: true }); -setWatch(run, BTN2, {repeat:true,edge:"falling"}); - // Screen event Bangle.on('touch', function(button){ + if(STATE.settings_open) return; switch(button){ case 1: prev(); @@ -172,6 +228,7 @@ Bangle.on('touch', function(button){ }); Bangle.on('swipe', dir => { + if(STATE.settings_open) return; if(dir == 1) prev(); else next(); }); @@ -179,4 +236,11 @@ Bangle.on('swipe', dir => { // close launcher when lcd is off Bangle.on('lcdPower', on => { if(!on) return load(); -}); \ No newline at end of file +}); + + +setWatch(prev, BTN1, { repeat: true }); +setWatch(next, BTN3, { repeat: true }); +setWatch(run, BTN2, { repeat:true }); + +jumpTo(1); \ No newline at end of file diff --git a/apps/toucher/settings.js b/apps/toucher/settings.js new file mode 100644 index 000000000..6f7320513 --- /dev/null +++ b/apps/toucher/settings.js @@ -0,0 +1,59 @@ +(function(back) { + + const Storage = require("Storage"); + const filename = 'toucher.json'; + let settings = Storage.readJSON(filename,1)|| null; + + function getSettings(){ + return { + highres: true, + animation : true, + frame : 3, + debug: true + }; + } + + function updateSettings() { + require("Storage").writeJSON(filename, settings); + Bangle.buzz(); + } + + if(!settings){ + settings = getSettings(); + updateSettings(); + } + + function saveChange(name){ + return function(v){ + settings[name] = v; + updateSettings(); + } + } + + E.showMenu({ + '': { 'title': 'Toucher settings' }, + "Resolution" : { + value : settings.highres, + format : v => v?"High":"Low", + onchange: v => { + saveChange('highres')(!settings.highres); + } + }, + "Animation" : { + value : settings.animation, + format : v => v?"On":"Off", + onchange : saveChange('animation') + }, + "Frame rate" : { + value : settings.frame, + min: 1, max: 10, step: 1, + onchange : saveChange('frame') + }, + "Debug" : { + value : settings.debug, + format : v => v?"On":"Off", + onchange : saveChange('debug') + }, + '< Back': back + }); +}); \ No newline at end of file diff --git a/apps/trex/ChangeLog b/apps/trex/ChangeLog new file mode 100644 index 000000000..42c1df403 --- /dev/null +++ b/apps/trex/ChangeLog @@ -0,0 +1 @@ +0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast diff --git a/apps/trex/trex.js b/apps/trex/trex.js index 92c5d049c..fe84cb31a 100644 --- a/apps/trex/trex.js +++ b/apps/trex/trex.js @@ -165,6 +165,7 @@ function gameStop() { } function onFrame() { + "ram" g.clear(); if (rex.alive) { frame++; diff --git a/apps/weather/app.js b/apps/weather/app.js new file mode 100644 index 000000000..8493144f7 --- /dev/null +++ b/apps/weather/app.js @@ -0,0 +1,54 @@ +(() => { + function draw(w) { + g.reset(); + g.setColor(0).fillRect(0, 24, 239, 239); + + require('weather').drawIcon(w.txt, 65, 90, 55); + const locale = require("locale"); + + g.setColor(-1); + + const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/); + let width = g.setFont("Vector", 40).stringWidth(temp[1]); + width += g.setFont("Vector", 20).stringWidth(temp[2]); + g.setFont("Vector", 40).setFontAlign(-1, -1, 0); + g.drawString(temp[1], 180-width/2, 70); + g.setFont("Vector", 20).setFontAlign(1, -1, 0); + g.drawString(temp[2], 180+width/2, 70); + + g.setFont("6x8", 1); + g.setFontAlign(-1, 0, 0); + g.drawString("Humidity", 135, 130); + g.drawString("Wind", 135, 142); + g.setFontAlign(1, 0, 0); + g.drawString(w.hum+"%", 225, 130); + g.drawString(locale.speed(w.wind), 225, 142); + + g.setFont("6x8", 2).setFontAlign(0, 0, 0); + g.drawString(w.loc, 120, 170); + + g.setFont("6x8", 1).setFontAlign(0, 0, 0); + g.drawString(w.txt.charAt(0).toUpperCase()+w.txt.slice(1), 120, 190); + + g.flip(); + } + + const _GB = global.GB; + global.GB = (event) => { + if (event.t==="weather") draw(event); + if (_GB) setTimeout(_GB, 0, event); + }; + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + const weather = require('weather').load(); + if (weather) { + draw(weather); + } else { + E.showMessage('Weather unknown\n\nIs Gadgetbridge\nconnected?'); + } + + // Show launcher when middle button pressed + setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'}) +})() diff --git a/apps/weather/icon.js b/apps/weather/icon.js new file mode 100644 index 000000000..18ca2b0c9 --- /dev/null +++ b/apps/weather/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AE8N6AXV7vdFyoXBGCQUBAAoXp73u93tC6YWBAAIXSFwQwDRiAWDGASSBmYABIx5IDgYXCmC7KCoYRBnvUCwQwKC4gSEAAgwKC5gwKCwPjC6inBC6owBC6wVKPBXd6YXMDBAuNJJQXWfwZITC/6QIBw073ezR6m73anOJAwuHeBAYFIw4WJAAsL3YQOAAxeBCiWIC4e72AJChGACpMIxAXBIwIwEBIIXKBgouDEIYuLC4ghEC6ELLoYXPU4YXFLxQNBBgcLEQqqQFwYA/AH4AYA==")) diff --git a/apps/weather/icon.png b/apps/weather/icon.png new file mode 100644 index 000000000..bbfc0ace0 Binary files /dev/null and b/apps/weather/icon.png differ diff --git a/apps/weather/lib.js b/apps/weather/lib.js new file mode 100644 index 000000000..f87984fe5 --- /dev/null +++ b/apps/weather/lib.js @@ -0,0 +1,176 @@ +exports = { + save: weather => { + let json = require('Storage').readJSON('weather.json')||{} + json.weather = Object.assign({}, weather) // don't mutate GB events + delete json.weather.t // don't save the event type (if present) + require('Storage').write('weather.json', json) + }, + load: () => { + let json = require('Storage').readJSON('weather.json')||{} + return json.weather + }, + drawIcon: (cond, x, y, r) => { + function drawSun(x, y, r) { + g.setColor("#FF7700"); + g.fillCircle(x, y, r); + } + + function drawCloud(x, y, r, c) { + const u = r/12; + if (c==null) c = "#EEEEEE"; + g.setColor(c); + g.fillCircle(x-8*u, y+3*u, 4*u); + g.fillCircle(x-4*u, y-2*u, 5*u); + g.fillCircle(x+4*u, y+0*u, 4*u); + g.fillCircle(x+9*u, y+4*u, 3*u); + g.fillPoly([ + x-8*u, y+7*u, + x-8*u, y+3*u, + x-4*u, y-2*u, + x+4*u, y+0*u, + x+9*u, y+4*u, + x+9*u, y+7*u, + ]); + } + + function drawBrokenClouds(x, y, r) { + drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777777"); + drawCloud(x-1/8*r, y+1/8*r, 7/8*r); + } + + function drawFewClouds(x, y, r) { + drawSun(x+3/8*r, y-1/8*r, 5/8*r); + drawCloud(x-1/8*r, y+1/8*r, 7/8*r); + } + + function drawRainLines(x, y, r) { + g.setColor("#FFFFFF"); + const y1 = y+1/2*r; + const y2 = y+1*r; + g.fillPoly([ + x-6/12*r+1, y1, + x-8/12*r+1, y2, + x-7/12*r, y2, + x-5/12*r, y1, + ]); + g.fillPoly([ + x-2/12*r+1, y1, + x-4/12*r+1, y2, + x-3/12*r, y2, + x-1/12*r, y1, + ]); + g.fillPoly([ + x+2/12*r+1, y1, + x+0/12*r+1, y2, + x+1/12*r, y2, + x+3/12*r, y1, + ]); + } + + function drawShowerRain(x, y, r) { + drawFewClouds(x, y-1/3*r, r); + drawRainLines(x, y, r); + } + + function drawRain(x, y, r) { + drawBrokenClouds(x, y-1/3*r, r); + drawRainLines(x, y, r); + } + + function drawThunderstorm(x, y, r) { + function drawLightning(x, y, r) { + g.setColor("#FF7700"); + g.fillPoly([ + x-2/6*r, y-r, + x-4/6*r, y+1/6*r, + x-1/6*r, y+1/6*r, + x-3/6*r, y+1*r, + x+3/6*r, y-1/6*r, + x+0/6*r, y-1/6*r, + x+3/6*r, y-r, + ]); + } + + drawBrokenClouds(x, y-1/3*r, r); + drawLightning(x-1/12*r, y+1/2*r, 1/2*r); + } + + function drawSnow(x, y, r) { + function rotatePoints(points, pivotX, pivotY, angle) { + for(let i = 0; i {}; + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm")) return drawThunderstorm; + if (condition.includes("freezing")||condition.includes("snow")|| + condition.includes("sleet")) { + return drawSnow; + } + if (condition.includes("drizzle")|| + condition.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; + return drawMist; + } + + chooseIcon(cond)(x, y, r) + }, +} diff --git a/apps/weather/readme.md b/apps/weather/readme.md new file mode 100644 index 000000000..a326b67dd --- /dev/null +++ b/apps/weather/readme.md @@ -0,0 +1,16 @@ +# Weather + +Shows Gadgetbridge weather reports. + +This adds a widget with a weather pictogram and the temperature. +You can view the full report through the app: +![Screenshot](screenshot.png) + +## Setup + +See [this guide](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather) +to setup Gadgetbridge weather reporting. + +## Controls + +BTN2: opens the launcher diff --git a/apps/weather/screenshot.png b/apps/weather/screenshot.png new file mode 100644 index 000000000..ce79b3b64 Binary files /dev/null and b/apps/weather/screenshot.png differ diff --git a/apps/weather/widget.js b/apps/weather/widget.js new file mode 100644 index 000000000..e02591543 --- /dev/null +++ b/apps/weather/widget.js @@ -0,0 +1,40 @@ +(() => { + function draw() { + const w = require('weather').load() + if (!w) return; + g.reset(); + g.setColor(0).fillRect(this.x, this.y, this.x+this.width, this.y+24) + if (w.txt) { + require('weather').drawIcon(w.txt, this.x+10, this.y+8, 8); + } + if (w.temp) { + let t = require('locale').temp(w.temp-273.15); // applies conversion + t = t.substr(0, t.length-2); // but we have no room for units + g.setFontAlign(0, 1); // center horizontally at bottom of widget + g.setFont('6x8', 1); + g.setColor(-1) + g.drawString(t, this.x+10, this.y+24) + } + } + + function update(weather) { + require('weather').save(weather); + if (!WIDGETS["weather"].width) { + WIDGETS["weather"].width = 20 + Bangle.drawWidgets() + } else if (Bangle.isLCDOn()) { + WIDGETS["weather"].draw() + } + } + + const _GB = global.GB; + global.GB = (event) => { + if (event.t==="weather") update(event); + if (_GB) setTimeout(_GB, 0, event); + }; + + WIDGETS["weather"] = {area: "tl", width: 20, draw: draw}; + if (!require('weather').load()) { + WIDGETS["weather"].width = 0 + } +})(); diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index 89f3ab2c9..a377fc81e 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -4,3 +4,7 @@ 0.04: Fix regression after tweaks to Storage.readJSON 0.05: Move configuration into App/widget settings 0.06: Move loader into welcome.boot.js +0.07: Run again when updated + Don't run again when settings app is updated (or absent) + Add "Run Now" option to settings +0.08: Don't overwrite existing settings on app update diff --git a/apps/welcome/app.js b/apps/welcome/app.js index 93a4234d8..a32a6e56f 100644 --- a/apps/welcome/app.js +++ b/apps/welcome/app.js @@ -288,6 +288,13 @@ setWatch(()=>{ }, BTN2, {repeat:true,edge:"rising"}); setWatch(()=>move(-1), BTN1, {repeat:true}); +(function migrateSettings(){ + let global_settings = require('Storage').readJSON('setting.json', 1) + if (global_settings) { + delete global_settings.welcomed + require('Storage').write('setting.json', global_settings) + } +})() Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); diff --git a/apps/welcome/boot.js b/apps/welcome/boot.js index ecf98b555..f6ba6d2d6 100644 --- a/apps/welcome/boot.js +++ b/apps/welcome/boot.js @@ -1,9 +1,11 @@ (function() { - let s = require('Storage').readJSON('setting.json', 1) || {} + let s = require('Storage').readJSON('welcome.json', 1) + || require('Storage').readJSON('setting.json', 1) + || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('welcome.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('setting.json', s) + require('Storage').write('welcome.json', {welcomed: "yes"}) load('welcome.app.js') }) } diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index 2fbd585c6..20c2e9b13 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -1,16 +1,14 @@ -// The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('setting.json', 1) || {} + let settings = require('Storage').readJSON('welcome.json', 1) + || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'Welcome App' }, - 'Run again': { + 'Run on Next Boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', - onchange: v => { - settings.welcomed = v ? undefined : true - require('Storage').write('setting.json', settings) - }, + format: v => v ? 'OK' : 'No', + onchange: v => require('Storage').write('welcome.json', {welcomed: !v}), }, + 'Run Now': () => load('welcome.app.js'), '< Back': back, }) }) diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index cd9993c02..b9d50ab8b 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -1,3 +1,4 @@ 0.02: Now refresh battery monitor every minute if LCD on 0.03: Tweaks for variable size widget system 0.04: Ensure redrawing works with variable size widget system +0.05: Fix regression stopping correct widget updates diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index 2f1f29802..dd6774d4c 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -30,7 +30,7 @@ Bangle.on('lcdPower', function(on) { WIDGETS["bat"].draw(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(draw, 60000); + batteryInterval = setInterval(()=>WIDGETS["bat"].draw(), 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 3627a86d3..a8851b1d8 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -5,3 +5,6 @@ 0.06: Show battery percentage as text 0.07: Add settings: percentage/color/charger icon 0.08: Draw percentage as inverted on monochrome battery +0.09: Fix regression stopping correct widget updates +0.10: Add 'hide if charge greater than' +0.11: Don't overwrite existing settings on app update diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index 5c0bdbcae..f38bb3a08 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -3,7 +3,7 @@ * @param {function} back Use back() to return to settings menu */ (function(back) { - const SETTINGS_FILE = 'widbatpc.settings.json' + const SETTINGS_FILE = 'widbatpc.json' const COLORS = ['By Level', 'Green', 'Monochrome'] // initialize with default settings... @@ -11,21 +11,22 @@ 'color': COLORS[0], 'percentage': true, 'charger': true, + 'hideifmorethan': 100, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings const storage = require('Storage') const saved = storage.readJSON(SETTINGS_FILE, 1) || {} for (const key in saved) { - s[key] = saved[key] + s[key] = saved[key]; } // creates a function to safe a specific setting, e.g. save('color')(1) function save(key) { return function (value) { - s[key] = value - storage.write(SETTINGS_FILE, s) - WIDGETS["batpc"].reload() + s[key] = value; + storage.write(SETTINGS_FILE, s); + WIDGETS["batpc"].reload(); } } @@ -51,8 +52,16 @@ const newIndex = (oldIndex + 1) % COLORS.length s.color = COLORS[newIndex] save('color')(s.color) - }, - }, - } + } + }, + 'Hide if >': { + value: s.hideifmorethan||100, + min: 10, + max : 100, + step: 10, + format: x => x+"%", + onchange: save('hideifmorethan'), + }, + } E.showMenu(menu) }) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 9f88b5c49..3fa4cb79a 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,9 +1,4 @@ (function(){ -const DEFAULTS = { - 'color': 'By Level', - 'percentage': true, - 'charger': true, -} const COLORS = { 'white': -1, 'charging': 0x07E0, // "Green" @@ -11,15 +6,24 @@ const COLORS = { 'ok': 0xFD20, // "Orange" 'low':0xF800, // "Red" } -const SETTINGS_FILE = 'widbatpc.settings.json' +const SETTINGS_FILE = 'widbatpc.json' let settings function loadSettings() { settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} + const DEFAULTS = { + 'color': 'By Level', + 'percentage': true, + 'charger': true, + 'hideifmorethan': 100, + }; + Object.keys(DEFAULTS).forEach(k=>{ + if (settings[k]===undefined) settings[k]=DEFAULTS[k] + }); } function setting(key) { if (!settings) { loadSettings() } - return (key in settings) ? settings[key] : DEFAULTS[key] + return settings[key]; } const levelColor = (l) => { @@ -45,16 +49,27 @@ const levelColor = (l) => { const chargerColor = () => { return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging } - +// sets width, returns true if it changed function setWidth() { - WIDGETS["batpc"].width = 40; - if (Bangle.isCharging() && setting('charger')) { - WIDGETS["batpc"].width += 16; - } + var w = 40; + if (Bangle.isCharging() && setting('charger')) + w += 16; + if (E.getBattery() > setting('hideifmorethan')) + w = 0; + var changed = WIDGETS["batpc"].width != w; + WIDGETS["batpc"].width = w; + return changed; } function draw() { + // if hidden, don't draw + if (!WIDGETS["batpc"].width) return; + // else... var s = 39; var x = this.x, y = this.y; + const l = E.getBattery(), + c = levelColor(l); + const xl = x+4+l*(s-12)/100 + if (Bangle.isCharging() && setting('charger')) { g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); @@ -64,9 +79,7 @@ function draw() { g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); g.fillRect(x+s-3,y+10,x+s,y+14); - const l = E.getBattery(), - c = levelColor(l); - const xl = x+4+l*(s-12)/100 + g.setColor(c).fillRect(x+4,y+6,xl,y+17); g.setColor(-1); if (!setting('percentage')) { @@ -97,20 +110,24 @@ function reload() { 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(); +} Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); - setWidth(); - Bangle.drawWidgets(); // relayout widgets + update(); g.flip(); }); var batteryInterval; Bangle.on('lcdPower', function(on) { if (on) { - WIDGETS["batpc"].draw(); + update(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(draw, 60000); + batteryInterval = setInterval(update, 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index c268d6df0..59dc603a9 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -1,2 +1,3 @@ 0.02: Tweaks for variable size widget system 0.03: Ensure redrawing works with variable size widget system +0.04: Fix automatic update of Bluetooth connection status diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index 8e96a395d..c3254c791 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -13,7 +13,7 @@ function changed() { WIDGETS["bluetooth"].draw(); g.flip();// turns screen on } -NRF.on('connected',changed); -NRF.on('disconnected',changed); +NRF.on('connect',changed); +NRF.on('disconnect',changed); WIDGETS["bluetooth"]={area:"tr",width:24,draw:draw}; })() diff --git a/apps/widclk/ChangeLog b/apps/widclk/ChangeLog index 5370129cc..6fda78a08 100644 --- a/apps/widclk/ChangeLog +++ b/apps/widclk/ChangeLog @@ -1,2 +1,3 @@ 0.02: Now refresh battery monitor every minute if LCD on 0.03: Ensure redrawing works with variable size widget system +0.04: Fix regression stopping correct widget updates diff --git a/apps/widclk/widget.js b/apps/widclk/widget.js index 1d5df36b2..ff22bb4d1 100644 --- a/apps/widclk/widget.js +++ b/apps/widclk/widget.js @@ -14,7 +14,7 @@ } } function startTimers(){ - intervalRef = setInterval(draw, 60*1000); + intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000); WIDGETS["wdclk"].draw(); } Bangle.on('lcdPower', (on) => { @@ -23,5 +23,5 @@ }); WIDGETS["wdclk"]={area:"tr",width:width,draw:draw}; - if (Bangle.isLCDOn) intervalRef = setInterval(draw, 60*1000); + if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000); })() diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index 980494acb..43fcc8dc9 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -5,3 +5,5 @@ 0.06: Fix widget position increment 0.07: Tweaks for variable size widget system 0.08: Ensure redrawing works with variable size widget system +0.09: Add daily goal +0.10: Fix daily goal, don't store settings in separate file diff --git a/apps/widpedom/settings.js b/apps/widpedom/settings.js new file mode 100644 index 000000000..d2db58593 --- /dev/null +++ b/apps/widpedom/settings.js @@ -0,0 +1,46 @@ +(function(back) { + const PEDOMFILE = "wpedom.json"; + + // initialize with default settings... + let s = { + 'goal': 10000, + 'progress': false, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const d = storage.readJSON(PEDOMFILE, 1) || {} + const saved = d.settings || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + d.settings = s + storage.write(PEDOMFILE, d) + WIDGETS['wpedom'].reload() + } + + E.showMenu({ + '': { 'title': 'Pedometer widget' }, + 'Daily Goal': { + value: s.goal, + min: 0, step: 1000, + format: s => (s ? s / 1000 + ',000' : '0'), + onchange: (g) => { + s.goal = g + s.progress = !!g + save() + }, + }, + 'Show Progress': { + value: s.progress, + format: () => (s.progress ? 'Yes' : 'No'), + onchange: () => { + s.progress = !s.progress + save() + }, + }, + '< Back': back, + }) +}) diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 1cc14fc2c..e7c3961b4 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -1,7 +1,55 @@ (() => { - const PEDOMFILE = "wpedom.json"; + const PEDOMFILE = "wpedom.json" + const DEFAULTS = { + 'goal': 10000, + 'progress': false, + } + const COLORS = { + 'white': -1, + 'progress': 0x001F, // Blue + 'done': 0x03E0, // DarkGreen + } + const TAU = Math.PI*2; let lastUpdate = new Date(); let stp_today = 0; + let settings; + + function loadSettings() { + const d = require('Storage').readJSON(PEDOMFILE, 1) || {}; + settings = d.settings || {}; + } + + function setting(key) { + if (!settings) { loadSettings() } + return (key in settings) ? settings[key] : DEFAULTS[key]; + } + + function drawProgress(stps) { + const width = 24, half = width/2; + const goal = setting('goal'), left = Math.max(goal-stps,0); + const c = left ? COLORS.progress : COLORS.done; + g.setColor(c).fillCircle(this.x + half, this.y + half, half); + if (left) { + const f = left/goal; // fraction to blank out + let p = []; + p.push(half,half); + p.push(half,0); + if(f>1/8) p.push(0,0); + if(f>2/8) p.push(0,half); + if(f>3/8) p.push(0,width); + if(f>4/8) p.push(half,width); + if(f>5/8) p.push(width,width); + if(f>6/8) p.push(width,half); + if(f>7/8) p.push(width,0); + p.push(half - Math.sin(f * TAU) * half); + p.push(half - Math.cos(f * TAU) * half); + for (let i = p.length; i; i -= 2) { + p[i - 2] += this.x; + p[i - 1] += this.y; + } + g.setColor(0).fillPoly(p); + } + } // draw your widget function draw() { @@ -11,6 +59,9 @@ } let stps = stp_today.toString(); g.reset(); + g.clearRect(this.x, this.y, this.x + width, this.y + 23); // erase background + if (setting('progress')){ drawProgress.call(this, stps); } + g.setColor(COLORS.white); if (stps.length > 3){ stps = stps.slice(0,-3) + "," + stps.slice(-3); g.setFont("4x6", 1); // if big, shrink text to fix @@ -18,11 +69,15 @@ g.setFont("6x8", 1); } g.setFontAlign(0, 0); // align to x: center, y: center - g.clearRect(this.x,this.y+15,this.x+width,this.y+23); // erase background g.drawString(stps, this.x+width/2, this.y+19); g.drawImage(atob("CgoCLguH9f2/7+v6/79f56CtAAAD9fw/n8Hx9A=="),this.x+(width-10)/2,this.y+2); } + function reload() { + loadSettings() + draw() + } + Bangle.on('step', (up) => { let date = new Date(); if (lastUpdate.getDate() == date.getDate()){ @@ -31,7 +86,13 @@ // TODO: could save this to PEDOMFILE for lastUpdate's day? stp_today = 1; } - lastUpdate = date; + if (stp_today === setting('goal')) { + let b = 3, buzz = () => { + if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) + } + buzz() + } + lastUpdate = date //console.log("up: " + up + " stp: " + stp_today + " " + date.toString()); if (Bangle.isLCDOn()) WIDGETS["wpedom"].draw(); }); @@ -41,15 +102,17 @@ }); // When unloading, save state E.on('kill', () => { + if (!settings) { loadSettings() } let d = { lastUpdate : lastUpdate.toISOString(), - stepsToday : stp_today + stepsToday : stp_today, + settings : settings, }; require("Storage").write(PEDOMFILE,d); }); // add your widget - WIDGETS["wpedom"]={area:"tl",width:26,draw:draw}; + WIDGETS["wpedom"]={area:"tl",width:26,draw:draw,reload:reload}; // Load data at startup let pedomData = require("Storage").readJSON(PEDOMFILE,1); if (pedomData) { diff --git a/apps/widram/ChangeLog b/apps/widram/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/widram/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/widram/widget.js b/apps/widram/widget.js new file mode 100644 index 000000000..08710b726 --- /dev/null +++ b/apps/widram/widget.js @@ -0,0 +1,23 @@ +(() => { + function draw() { + g.reset(); + var m = process.memory(); + var pc = Math.round(m.usage*100/m.total); + g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4); + g.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff")); + g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/); + } + var ramInterval; + Bangle.on('lcdPower', function(on) { + if (on) { + WIDGETS["ram"].draw(); + if (!ramInterval) ramInterval = setInterval(()=>WIDGETS["ram"].draw(), 10000); + } else { + if (ramInterval) { + clearInterval(ramInterval); + ramInterval = undefined; + } + } + }); + WIDGETS["ram"]={area:"tl",width: 24,draw:draw}; +})() diff --git a/apps/widram/widget.png b/apps/widram/widget.png new file mode 100644 index 000000000..c1cbf2e1a Binary files /dev/null and b/apps/widram/widget.png differ diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog index f5c64dbee..53c451bcd 100644 --- a/apps/wohrm/ChangeLog +++ b/apps/wohrm/ChangeLog @@ -4,3 +4,4 @@ 0.04: Only buzz on high confidence (>85%) 0.05: Improved buzz timing and rendering 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed +0.07: Home button fixed and README added \ No newline at end of file diff --git a/apps/wohrm/README.md b/apps/wohrm/README.md new file mode 100644 index 000000000..ad9e82525 --- /dev/null +++ b/apps/wohrm/README.md @@ -0,0 +1,29 @@ +# Summary +Workout heart rate monitor that buzzes when your heart rate hits the limits. + +This app is for the [Bangle.js watch](https://banglejs.com/). While active it monitors your heart rate +and will notify you with a buzz whenever your heart rate falls below or jumps above the set limits. + +# How it works +[Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html). + +## Setting the limits +For setting the lower limit press button 4 (left part of the watch's touch screen). +Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch. + +For setting the upper limit act accordingly after pressing button 5 (the right part of the watch's screen). + +## Reading Reliability +As per the specs of the watch the HR monitor is not 100% reliable all the time. +That's why the WOHRM displays a confidence value for each reading of the current heart rate. + +To the left and right of the "Current" value two colored bars indicate the confidence in +the received value: For 85% and above the bars are green, between 84% and 50% the bars are yellow +and below 50% they turn red. + +## Closing the app +Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher. + +# HRM usage +The HRM is switched on when the app is started. It stays switch on while the app is running, even +when the watch screen goes to stand-by. diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index 7e0af4219..b3ce8acc8 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -287,13 +287,11 @@ function resetHighlightTimeout() { setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); } -// Show launcher when middle button pressed function switchOffApp(){ Bangle.setHRMPower(0); Bangle.showLauncher(); } -// special function to handle display switch on Bangle.on('lcdPower', (on) => { g.clear(); if (on) { @@ -312,19 +310,18 @@ Bangle.setHRMPower(1); Bangle.on('HRM', onHrm); setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); -setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); +setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -//drawTrainingHeartRate(); renderHomeIcon(); renderLowerLimitBackground(); renderUpperLimitBackground(); -// refesh every sec setInterval(drawTrainingHeartRate, 1000); diff --git a/bin/apploader.js b/bin/apploader.js new file mode 100644 index 000000000..fb86540b8 --- /dev/null +++ b/bin/apploader.js @@ -0,0 +1,219 @@ +#!/bin/node +/* Simple Command-line app loader for Node.js +=============================================== + +NOTE: This needs the '@abandonware/noble' library to be installed. +However we don't want this in package.json (at least +as a normal dependency) because we want `sanitycheck.js` +to be able to run *quickly* in travis for every commit, +and we don't want NPM pulling in (and compiling native modules) +for Noble. +*/ + +var SETTINGS = { + pretokenise : true +}; +var Utils = require("../js/utils.js"); +var AppInfo = require("../js/appinfo.js"); +var apps; + +function ERROR(msg) { + console.error(msg); + process.exit(1); +} + +try { + apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json")); +} catch(e) { + ERROR("'apps.json' could not be loaded"); +} + +var args = process.argv; + +if (args.length==3 && args[2]=="list") cmdListApps(); +else if (args.length==4 && args[2]=="install") cmdInstallApp(args[3]); +else { + console.log(`apploader.js +------------- + +USAGE: + +apploader.js list +apploader.js install appname +`); +process.exit(0); +} + +function cmdListApps() { + console.log(apps.map(a=>a.id).join("\n")); +} +function cmdInstallApp(appId) { + var app = apps.find(a=>a.id==appId); + if (!app) ERROR(`App ${JSON.stringify(appId)} not found`); + if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); + return AppInfo.getFiles(app, { + fileGetter:function(url) { + return Promise.resolve(require("fs").readFileSync(url).toString()); + }, settings : SETTINGS}).then(files => { + //console.log(files); + var command = files.map(f=>f.cmd).join("\n")+"\n"; + bangleSend(command).then(() => process.exit(0)); + }); +} + +function bangleSend(command) { + var noble = require('noble'); + var log = function() { + var args = [].slice.call(arguments); + console.log("UART: "+args.join(" ")); + } + + var RESET = true; + var DEVICEADDRESS = ""; + + var complete = false; + var foundDevices = []; + var flowControlPaused = false; + var btDevice; + var txCharacteristic; + var rxCharacteristic; + + return new Promise((resolve,reject) => { + function foundDevice(dev) { + if (btDevice!==undefined) return; + log("Connecting to "+dev.address); + noble.stopScanning(); + connect(dev, function() { + // Connected! + function writeCode() { + log("Writing code..."); + write(command, function() { + complete = true; + btDevice.disconnect(); + }); + } + if (RESET) { + setTimeout(function() { + log("Resetting..."); + write("\x03\x10reset()\n", function() { + setTimeout(writeCode, 1000); + }); + }, 500); + } else + setTimeout(writeCode, 1000); + }); + } + + function connect(dev, callback) { + btDevice = dev; + log("BT> Connecting"); + btDevice.on('disconnect', function() { + log("Disconnected"); + setTimeout(function() { + if (complete) resolve(); + else reject("Disconnected but not complete"); + }, 500); + }); + btDevice.connect(function (error) { + if (error) { + log("BT> ERROR Connecting",error); + btDevice = undefined; + return; + } + log("BT> Connected"); + btDevice.discoverAllServicesAndCharacteristics(function(error, services, characteristics) { + function findByUUID(list, uuid) { + for (var i=0;i ERROR getting services/characteristics"); + log("Service "+btUARTService); + log("TX "+txCharacteristic); + log("RX "+rxCharacteristic); + btDevice.disconnect(); + txCharacteristic = undefined; + rxCharacteristic = undefined; + btDevice = undefined; + return openCallback(); + } + + rxCharacteristic.on('data', function (data) { + var s = ""; + for (var i=0;i=10) { + log("Writing "+amt+"/"+total); + progress=0; + } + //log("Writing ",JSON.stringify(d)); + amt += d.length; + for (var i = 0; i < buf.length; i++) + buf.writeUInt8(d.charCodeAt(i), i); + txCharacteristic.write(buf, false, writeAgain); + } + writeAgain(); + } + + function disconnect() { + btDevice.disconnect(); + } + + log("Discovering..."); + noble.on('discover', function(dev) { + if (!dev.advertisement) return; + if (!dev.advertisement.localName) return; + var a = dev.address.toString(); + if (foundDevices.indexOf(a)>=0) return; + foundDevices.push(a); + log("Found device: ",a,dev.advertisement.localName); + if (a == DEVICEADDRESS) + return foundDevice(dev); + else if (DEVICEADDRESS=="" && dev.advertisement.localName.indexOf("Bangle.js")==0) { + return foundDevice(dev); + } + }); + noble.startScanning([], true); + }); +} diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index e5db392dd..41290cf7e 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -3,6 +3,9 @@ Mashes together a bunch of different apps to make a single firmware JS file which can be uploaded. */ +var SETTINGS = { + pretokenise : true +}; var path = require('path'); var ROOTDIR = path.join(__dirname, '..'); @@ -16,7 +19,7 @@ var APPS = [ // IDs of apps to install var MINIFY = true; var fs = require("fs"); -var AppInfo = require(ROOTDIR+"/appinfo.js"); +var AppInfo = require(ROOTDIR+"/js/appinfo.js"); var appjson = JSON.parse(fs.readFileSync(APPJSON).toString()); var appfiles = []; @@ -49,7 +52,10 @@ function fileGetter(url) { Promise.all(APPS.map(appid => { var app = appjson.find(app=>app.id==appid); if (app===undefined) throw new Error(`App ${appid} not found`); - return AppInfo.getFiles(app, fileGetter).then(files => { + return AppInfo.getFiles(app, { + fileGetter : fileGetter, + settings : SETTINGS + }).then(files => { appfiles = appfiles.concat(files); }); })).then(() => { diff --git a/bin/pre-publish.sh b/bin/pre-publish.sh new file mode 100755 index 000000000..ee73968d7 --- /dev/null +++ b/bin/pre-publish.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +cd `dirname $0`/.. +nodejs bin/sanitycheck.js || exit 1 + +echo "Sanity check passed." + +echo "Finding app dates..." + +# Create list of: +# appid,created_time,modified_time +cd apps +for appfolder in *; do + echo "$appfolder,$(git log --follow --format=%ai -- $appfolder | tail -n 1),$(git log --follow --format=%ai -- $appfolder | head -n 1)" ; +done | grep -v _example_ | grep -v unknown.png > ../appdates.csv +cd .. + +echo "Ready to publish" diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index a2c9dee9a..bf1972303 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -27,17 +27,52 @@ function WARN(s) { var appsFile, apps; try { - appsFile = fs.readFileSync(BASEDIR+"apps.json"); + appsFile = fs.readFileSync(BASEDIR+"apps.json").toString(); } catch (e) { ERROR("apps.json not found"); } try{ apps = JSON.parse(appsFile); } catch (e) { + console.log(e); + var m = e.toString().match(/in JSON at position (\d+)/); + if (m) { + var char = parseInt(m[1]); + console.log("==============================================="); + console.log("LINE "+appsFile.substr(0,char).split("\n").length); + console.log("==============================================="); + console.log(appsFile.substr(char-10, 20)); + console.log("==============================================="); + } + console.log(m); ERROR("apps.json not valid JSON"); + } -apps.forEach((app,addIdx) => { +const APP_KEYS = [ + 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', + 'sortorder', 'readme', 'custom', 'interface', 'storage', 'data', 'allow_emulator', +]; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; +const DATA_KEYS = ['name', 'wildcard', 'storageFile']; +const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info +const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; + +function globToRegex(pattern) { + const ESCAPE = '.*+-?^${}()|[]\\'; + const regex = pattern.replace(/./g, c => { + switch (c) { + case '?': return '.'; + case '*': return '.*'; + default: return ESCAPE.includes(c) ? ('\\' + c) : c; + } + }); + return new RegExp('^'+regex+'$'); +} +const isGlob = f => /[?*]/.test(f) +// All storage+data files in all apps: {app:,[file: | data:]} +let allFiles = []; +apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); //console.log(`Checking ${app.id}...`); var appDir = APPSDIR+app.id+"/"; @@ -68,9 +103,13 @@ apps.forEach((app,addIdx) => { var fileNames = []; app.storage.forEach((file) => { if (!file.name) ERROR(`App ${app.id} has a file with no name`); + if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); + let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS) + if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`) if (fileNames.includes(file.name)) ERROR(`App ${app.id} file ${file.name} is a duplicate`); fileNames.push(file.name); + allFiles.push({app: app.id, file: file.name}); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`); var fileContents = ""; @@ -105,9 +144,82 @@ apps.forEach((app,addIdx) => { ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`); } } + for (const key in file) { + if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); + } }); + let dataNames = []; + (app.data||[]).forEach((data)=>{ + if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`); + if (dataNames.includes(data.name||data.wildcard)) + ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`); + dataNames.push(data.name||data.wildcard) + allFiles.push({app: app.id, data: (data.name||data.wildcard)}); + if ('name' in data && 'wildcard' in data) + ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`); + if (isGlob(data.name)) + ERROR(`App ${app.id} data file name ${data.name} contains wildcards`); + if (data.wildcard) { + if (!isGlob(data.wildcard)) + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`); + if (data.wildcard.replace(/\?|\*/g,'') === '') + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`); + else if (data.wildcard.replace(/\?|\*/g,'').length < 3) + WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`); + else if (!data.wildcard.includes(app.id)) + WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`); + } + let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS) + if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`) + if ('storageFile' in data && typeof data.storageFile !== 'boolean') + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`); + for (const key in data) { + if (!DATA_KEYS.includes(key)) + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`); + } + }); + // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?) + if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json")) + WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`) + // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?) + if (fileNames.includes(app.id+".settings.json")) + WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`) + if (fileNames.includes(app.id+".json")) + WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`) + // warn if storage file matches data file of same app + dataNames.forEach(dataName=>{ + const glob = globToRegex(dataName) + fileNames.forEach(fileName=>{ + if (glob.test(fileName)) { + if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`) + else WARN(`App ${app.id} storage file ${fileName} is also listed in data`) + } + }) + }) //console.log(fileNames); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); + for (const key in app) { + if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); + } }); +// Do not allow files from different apps to collide +let fileA +while(fileA=allFiles.pop()) { + if (VALID_DUPLICATES.includes(fileA.file)) + return; + const nameA = (fileA.file||fileA.data), + globA = globToRegex(nameA), + typeA = fileA.file?'storage':'data' + allFiles.forEach(fileB => { + const nameB = (fileB.file||fileB.data), + globB = globToRegex(nameB), + typeB = fileB.file?'storage':'data' + if (globA.test(nameB)||globB.test(nameA)) { + if (isGlob(nameA)||isGlob(nameB)) + ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) + else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + } + }) +} diff --git a/index.html b/index.html index 67f5a748b..f3ca261b5 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,12 @@ .chip { cursor: pointer; } + .filter-nav { + display: inline-block; + } + .sort-nav { + float: right; + } .tile-content { position: relative; } .link-github { position:absolute; @@ -69,9 +75,6 @@ -

Note: If you have a version of Bangle.js firmware before 2v04, please update to the latest firmware or - use the legacy app loader. -

@@ -91,16 +94,26 @@
-
- - - - - - - +
+
+ + + + + + + + +
+
-
+ +
@@ -115,6 +128,7 @@
+
@@ -129,6 +143,7 @@
+

Check out the Source on GitHub, or find out how to add your own app

Using Espruino, Icons from icons8.com

@@ -136,7 +151,16 @@

Utilities

-

+ +

+

Settings

+
+ + +
@@ -155,6 +179,8 @@ + + diff --git a/js/appinfo.js b/js/appinfo.js index f4ab498b1..ad2611d19 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -1,16 +1,56 @@ +if (typeof btoa==="undefined") + function btoa(d) { return Buffer.from(d).toString('base64'); } + +// Converts a string into most efficient way to send to Espruino (either json, base64, or compressed base64) function toJS(txt) { - return JSON.stringify(txt); + var json = JSON.stringify(txt); + var b64 = "atob("+JSON.stringify(btoa(json))+")"; + var js = b64.length < json.length ? b64 : json; + + if (typeof heatshrink !== "undefined") { + var ua = new Uint8Array(txt.length); + for (var i=0;i { + /* Get files needed for app. + options = { + fileGetter : callback for getting URL, + settings : global settings object + } + */ + getFiles : (app,options) => { return new Promise((resolve,reject) => { // Load all files Promise.all(app.storage.map(storageFile => { - if (storageFile.content) + if (storageFile.content!==undefined) return Promise.resolve(storageFile); else if (storageFile.url) - return fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => { + return options.fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => { + if (storageFile.url.endsWith(".js") && !storageFile.url.endsWith(".min.js")) { // if original file ends in '.js'... + return Espruino.transform(content, { + SET_TIME_ON_WRITE : false, + PRETOKENISE : options.settings.pretokenise, + //MINIFICATION_LEVEL : "ESPRIMA", // disable due to https://github.com/espruino/BangleApps/pull/355#issuecomment-620124162 + builtinModules : "Flash,Storage,heatshrink,tensorflow,locale" + }); + } else + return content; + }).then(content => { return { name : storageFile.name, content : content, @@ -31,14 +71,14 @@ var AppInfo = { let js = storageFile.content.trim(); if (js.endsWith(";")) js = js.slice(0,-1); - storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${js});`; + storageFile.cmd = `\x10require('Storage').write(${JSON.stringify(storageFile.name)},${js});`; } else { let code = storageFile.content; // write code in chunks, in case it is too big to fit in RAM (fix #157) var CHUNKSIZE = 4096; - storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${toJS(code.substr(0,CHUNKSIZE))},0,${code.length});`; + storageFile.cmd = `\x10require('Storage').write(${JSON.stringify(storageFile.name)},${toJS(code.substr(0,CHUNKSIZE))},0,${code.length});`; for (var i=CHUNKSIZE;if.name==app.id+".app.js")) json.src = app.id+".app.js"; - if (fileContents.find(f=>f.name==app.id+".settings.js")) - json.settings = app.id+".settings.js"; if (fileContents.find(f=>f.name==app.id+".img")) json.icon = app.id+".img"; if (app.sortorder) json.sortorder = app.sortorder; @@ -69,13 +107,48 @@ var AppInfo = { var fileList = fileContents.map(storageFile=>storageFile.name); fileList.unshift(appJSONName); // do we want this? makes life easier! json.files = fileList.join(","); + if ('data' in app) { + let data = {dataFiles: [], storageFiles: []}; + // add "data" files to appropriate list + app.data.forEach(d=>{ + if (d.storageFile) data.storageFiles.push(d.name||d.wildcard) + else data.dataFiles.push(d.name||d.wildcard) + }) + const dataString = AppInfo.makeDataString(data) + if (dataString) json.data = dataString + } fileContents.push({ name : appJSONName, content : JSON.stringify(json) }); resolve(fileContents); }); - } + }, + // (.info).data holds filenames of data: both regular and storageFiles + // These are stored as: (note comma vs semicolons) + // "fil1,file2", "file1,file2;storageFileA,storageFileB" or ";storageFileA" + /** + * Convert appid.info "data" to object with file names/patterns + * Passing in undefined works + * @param data "data" as stored in appid.info + * @returns {{storageFiles:[], dataFiles:[]}} + */ + parseDataString(data) { + data = data || ''; + let [files = [], storage = []] = data.split(';').map(d => d.split(',')) + return {dataFiles: files, storageFiles: storage} + }, + /** + * Convert object with file names/patterns to appid.info "data" string + * Passing in an incomplete object will not work + * @param data {{storageFiles:[], dataFiles:[]}} + * @returns {string} "data" to store in appid.info + */ + makeDataString(data) { + if (!data.dataFiles.length && !data.storageFiles.length) { return '' } + if (!data.storageFiles.length) { return data.dataFiles.join(',') } + return [data.dataFiles.join(','),data.storageFiles.join(',')].join(';') + }, }; if ("undefined"!=typeof module) diff --git a/js/comms.js b/js/comms.js index eb453871d..dad1b56fc 100644 --- a/js/comms.js +++ b/js/comms.js @@ -10,12 +10,20 @@ reset : (opt) => new Promise((resolve,reject) => { }), uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `storage`) Progress.show({title:`Uploading ${app.name}`,sticky:true}); - return AppInfo.getFiles(app, httpGet).then(fileContents => { + return AppInfo.getFiles(app, { + fileGetter : httpGet, + settings : SETTINGS + }).then(fileContents => { return new Promise((resolve,reject) => { console.log("uploadApp",fileContents.map(f=>f.name).join(", ")); - var maxBytes = fileContents.reduce((b,f)=>b+f.content.length, 0)||1; + var maxBytes = fileContents.reduce((b,f)=>b+f.cmd.length, 0)||1; var currentBytes = 0; + var appInfoFileName = app.id+".info"; + var appInfoFile = fileContents.find(f=>f.name==appInfoFileName); + if (!appInfoFile) reject(`${appInfoFileName} not found`); + var appInfo = JSON.parse(appInfoFile.content); + // Upload each file one at a time function doUploadFiles() { // No files left - print 'reboot' message @@ -23,25 +31,31 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s Puck.write(`\x10E.showMessage('Hold BTN3\\nto reload')\n`,(result) => { Progress.hide({sticky:true}); if (result===null) return reject(""); - resolve(app); + resolve(appInfo); }); return; } var f = fileContents.shift(); console.log(`Upload ${f.name} => ${JSON.stringify(f.content)}`); - Progress.show({ - min:currentBytes / maxBytes, - max:(currentBytes+f.content.length) / maxBytes}); - currentBytes += f.content.length; // Chould check CRC here if needed instead of returning 'OK'... // E.CRC32(require("Storage").read(${JSON.stringify(app.name)})) - Puck.write(`\x10${f.cmd};Bluetooth.println("OK")\n`,(result) => { - if (!result || result.trim()!="OK") { - Progress.hide({sticky:true}); - return reject("Unexpected response "+(result||"")); - } - doUploadFiles(); - }, true); // wait for a newline + var cmds = f.cmd.split("\n"); + function uploadCmd() { + if (!cmds.length) return doUploadFiles(); + var cmd = cmds.shift(); + Progress.show({ + min:currentBytes / maxBytes, + max:(currentBytes+cmd.length) / maxBytes}); + currentBytes += cmd.length; + Puck.write(`${cmd};Bluetooth.println("OK")\n`,(result) => { + if (!result || result.trim()!="OK") { + Progress.hide({sticky:true}); + return reject("Unexpected response "+(result||"")); + } + uploadCmd(); + }, true); // wait for a newline + } + uploadCmd(); } // Start the upload function doUpload() { @@ -70,20 +84,48 @@ getInstalledApps : () => { Progress.hide({sticky:true}); return reject(""); } - Puck.eval('require("Storage").list(/\.info$/).map(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);return j})', (appList,err) => { + Puck.write('\x10Bluetooth.print("[");require("Storage").list(/\.info$/).forEach(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);Bluetooth.print(JSON.stringify(j)+",")});Bluetooth.println("0]")\n', (appList,err) => { Progress.hide({sticky:true}); + try { + appList = JSON.parse(appList); + // remove last element since we added a final '0' + // to make things easy on the Bangle.js side + appList = appList.slice(0,-1); + } catch (e) { + appList = null; + err = e.toString(); + } if (appList===null) return reject(err || ""); console.log("getInstalledApps", appList); resolve(appList); - }); + }, true /* callback on newline */); }); }); }, removeApp : app => { // expects an appid.info structure (i.e. with `files`) - if (app.files === '') return Promise.resolve(); // nothing to erase + if (!app.files && !app.data) return Promise.resolve(); // nothing to erase Progress.show({title:`Removing ${app.name}`,sticky:true}); - var cmds = app.files.split(',').map(file=>{ - return `\x10require("Storage").erase(${toJS(file)});\n`; + let cmds = '\x10const s=require("Storage");\n'; + // remove App files: regular files, exact names only + cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join(""); + // remove app Data: (dataFiles and storageFiles) + const data = AppInfo.parseDataString(app.data) + const isGlob = f => /[?*]/.test(f) + // regular files, can use wildcards + cmds += data.dataFiles.map(file => { + if (!isGlob(file)) return `\x10s.erase(${toJS(file)});\n`; + const regex = new RegExp(globToRegex(file)) + return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`; + }).join(""); + // storageFiles, can use wildcards + cmds += data.storageFiles.map(file => { + if (!isGlob(file)) return `\x10s.open(${toJS(file)},'r').erase();\n`; + // storageFiles have a chunk number appended to their real name + const regex = globToRegex(file+'\u0001') + // open() doesn't want the chunk number though + let cmd = `\x10s.list(${regex}).forEach(f=>s.open(f.substring(0,f.length-1),'r').erase());\n` + // using a literal \u0001 char fails (not sure why), so escape it + return cmd.replace('\u0001', '\\x01') }).join(""); console.log("removeApp", cmds); return Comms.reset().then(new Promise((resolve,reject) => { diff --git a/js/espruinotools.js b/js/espruinotools.js new file mode 100644 index 000000000..8e266f267 --- /dev/null +++ b/js/espruinotools.js @@ -0,0 +1,6807 @@ +// EspruinoTools bundle (https://github.com/espruino/EspruinoTools) +// Created with https://github.com/espruino/EspruinoWebIDE/blob/gh-pages/extras/create_espruinotools_js.sh +// Based on EspruinoWebIDE 0.73.7 +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Initialisation code + ------------------------------------------------------------------ +**/ +"use strict"; + +var Espruino; + +(function() { + + /** List of processors. These are functions that are called one + * after the other with the data received from the last one. + * + * Common processors are: + * + * jsCodeChanged - called when the code in the editor changes with {code} + * sending - sending code to Espruino (no data) + * transformForEspruino - transform code ready to be sent to Espruino + * transformModuleForEspruino({code,name}) + * - transform module code before it's sent to Espruino with Modules.addCached (we only do this if we don't think it's been minified before) + * connected - connected to Espruino (no data) + * disconnected - disconnected from Espruino (no data) + * environmentVar - Board's process.env loaded (object to be saved into Espruino.Env.environmentData) + * boardJSONLoaded - Board's JSON was loaded into environmentVar + * getModule - Called with data={moduleName:"foo", moduleCode:undefined} - moduleCode should be filled in if the module can be found + * getURL - Called with data={url:"http://....", data:undefined) - data should be filled in if the URL is handled (See Espruino.Core.Utils.getURL to use this) + * terminalClear - terminal has been cleared + * terminalPrompt - we've received a '>' character (eg, `>` or `debug>`). The argument is the current line's contents. + * terminalNewLine - When we get a new line on the terminal, this gets called with the last line's contents + * debugMode - called with true or false when debug mode is entered or left + * editorHover - called with { node : htmlNode, showTooltip : function(htmlNode) } when something is hovered over + * notification - called with { mdg, type:"success","error"/"warning"/"info" } + **/ + var processors = {}; + + function init() { + + Espruino.Core.Config.loadConfiguration(function() { + // Initialise all modules + function initModule(modName, mod) { + console.log("Initialising "+modName); + if (mod.init !== undefined) + mod.init(); + } + + var module; + for (module in Espruino.Core) initModule(module, Espruino.Core[module]); + for (module in Espruino.Plugins) initModule(module, Espruino.Plugins[module]); + + callProcessor("initialised", undefined, function() { + // We need the delay because of background.js's url_handler... + setTimeout(function() { + Espruino.initialised = true; + }, 1000); + }); + }); + } + + // Automatically start up when all is loaded + if (typeof document!=="undefined") + document.addEventListener("DOMContentLoaded", init); + + /** Add a processor function of type function(data,callback) */ + function addProcessor(eventType, processor) { + if (processors[eventType]===undefined) + processors[eventType] = []; + processors[eventType].push(processor); + } + + /** Call a processor function */ + function callProcessor(eventType, data, callback) { + var p = processors[eventType]; + // no processors + if (p===undefined || p.length==0) { + if (callback!==undefined) callback(data); + return; + } + // now go through all processors + var n = 0; + var cbCalled = false; + var cb = function(inData) { + if (cbCalled) throw new Error("Internal error in "+eventType+" processor. Callback is called TWICE."); + cbCalled = true; + if (n < p.length) { + cbCalled = false; + p[n++](inData, cb); + } else { + if (callback!==undefined) callback(inData); + } + }; + cb(data); + } + + // ----------------------------------- + Espruino = { + Core : { }, + Plugins : { }, + addProcessor : addProcessor, + callProcessor : callProcessor, + initialised : false, + init : init, // just in case we need to initialise this by hand + }; + + return Espruino; +})(); +Espruino.Core.Notifications = { + success : function(e) { console.log(e); }, + error : function(e) { console.error(e); }, + warning : function(e) { console.warn(e); }, + info : function(e) { console.log(e); }, +}; +Espruino.Core.Status = { + setStatus : function(e,len) { console.log(e); }, + hasProgress : function() { return false; }, + incrementProgress : function(amt) {} +}; +var acorn = (function(){ var exports={}; +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.acorn = {}))); +}(this, (function (exports) { 'use strict'; + +// Reserved word lists for various dialects of the language + +var reservedWords = { + 3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile", + 5: "class enum extends super const export import", + 6: "enum", + strict: "implements interface let package private protected public static yield", + strictBind: "eval arguments" +}; + +// And the keywords + +var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this"; + +var keywords = { + 5: ecma5AndLessKeywords, + 6: ecma5AndLessKeywords + " const class extends export import super" +}; + +var keywordRelationalOperator = /^in(stanceof)?$/; + +// ## Character categories + +// Big ugly regular expressions that match characters in the +// whitespace, identifier, and identifier-start categories. These +// are only applied when a character is found to actually have a +// code point above 128. +// Generated by `bin/generate-identifier-regex.js`. + +var nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0-\u08b4\u08b6-\u08bd\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c88\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fd5\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7ae\ua7b0-\ua7b7\ua7f7-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab65\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc"; +var nonASCIIidentifierChars = "\u200c\u200d\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08d4-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0d01-\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2-\u1cf4\u1cf8\u1cf9\u1dc0-\u1df5\u1dfb-\u1dff\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua900-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f"; + +var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]"); +var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]"); + +nonASCIIidentifierStartChars = nonASCIIidentifierChars = null; + +// These are a run-length and offset encoded representation of the +// >0xffff code points that are a valid part of identifiers. The +// offset starts at 0x10000, and each pair of numbers represents an +// offset to the next range, and then a size of the range. They were +// generated by bin/generate-identifier-regex.js + +// eslint-disable-next-line comma-spacing +var astralIdentifierStartCodes = [0,11,2,25,2,18,2,1,2,14,3,13,35,122,70,52,268,28,4,48,48,31,17,26,6,37,11,29,3,35,5,7,2,4,43,157,19,35,5,35,5,39,9,51,157,310,10,21,11,7,153,5,3,0,2,43,2,1,4,0,3,22,11,22,10,30,66,18,2,1,11,21,11,25,71,55,7,1,65,0,16,3,2,2,2,26,45,28,4,28,36,7,2,27,28,53,11,21,11,18,14,17,111,72,56,50,14,50,785,52,76,44,33,24,27,35,42,34,4,0,13,47,15,3,22,0,2,0,36,17,2,24,85,6,2,0,2,3,2,14,2,9,8,46,39,7,3,1,3,21,2,6,2,1,2,4,4,0,19,0,13,4,159,52,19,3,54,47,21,1,2,0,185,46,42,3,37,47,21,0,60,42,86,25,391,63,32,0,449,56,264,8,2,36,18,0,50,29,881,921,103,110,18,195,2749,1070,4050,582,8634,568,8,30,114,29,19,47,17,3,32,20,6,18,881,68,12,0,67,12,65,0,32,6124,20,754,9486,1,3071,106,6,12,4,8,8,9,5991,84,2,70,2,1,3,0,3,1,3,3,2,11,2,0,2,6,2,64,2,3,3,7,2,6,2,27,2,3,2,4,2,0,4,6,2,339,3,24,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,7,4149,196,60,67,1213,3,2,26,2,1,2,0,3,0,2,9,2,3,2,0,2,0,7,0,5,0,2,0,2,0,2,2,2,1,2,0,3,0,2,0,2,0,2,0,2,0,2,1,2,0,3,3,2,6,2,3,2,3,2,0,2,9,2,16,6,2,2,4,2,16,4421,42710,42,4148,12,221,3,5761,10591,541]; + +// eslint-disable-next-line comma-spacing +var astralIdentifierCodes = [509,0,227,0,150,4,294,9,1368,2,2,1,6,3,41,2,5,0,166,1,1306,2,54,14,32,9,16,3,46,10,54,9,7,2,37,13,2,9,52,0,13,2,49,13,10,2,4,9,83,11,7,0,161,11,6,9,7,3,57,0,2,6,3,1,3,2,10,0,11,1,3,6,4,4,193,17,10,9,87,19,13,9,214,6,3,8,28,1,83,16,16,9,82,12,9,9,84,14,5,9,423,9,838,7,2,7,17,9,57,21,2,13,19882,9,135,4,60,6,26,9,1016,45,17,3,19723,1,5319,4,4,5,9,7,3,6,31,3,149,2,1418,49,513,54,5,49,9,0,15,0,23,4,2,14,1361,6,2,16,3,6,2,1,2,4,2214,6,110,6,6,9,792487,239]; + +// This has a complexity linear to the value of the code. The +// assumption is that looking up astral identifier characters is +// rare. +function isInAstralSet(code, set) { + var pos = 0x10000; + for (var i = 0; i < set.length; i += 2) { + pos += set[i]; + if (pos > code) { return false } + pos += set[i + 1]; + if (pos >= code) { return true } + } +} + +// Test whether a given character code starts an identifier. + +function isIdentifierStart(code, astral) { + if (code < 65) { return code === 36 } + if (code < 91) { return true } + if (code < 97) { return code === 95 } + if (code < 123) { return true } + if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code)) } + if (astral === false) { return false } + return isInAstralSet(code, astralIdentifierStartCodes) +} + +// Test whether a given character is part of an identifier. + +function isIdentifierChar(code, astral) { + if (code < 48) { return code === 36 } + if (code < 58) { return true } + if (code < 65) { return false } + if (code < 91) { return true } + if (code < 97) { return code === 95 } + if (code < 123) { return true } + if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code)) } + if (astral === false) { return false } + return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes) +} + +// ## Token types + +// The assignment of fine-grained, information-carrying type objects +// allows the tokenizer to store the information it has about a +// token in a way that is very cheap for the parser to look up. + +// All token type variables start with an underscore, to make them +// easy to recognize. + +// The `beforeExpr` property is used to disambiguate between regular +// expressions and divisions. It is set on all token types that can +// be followed by an expression (thus, a slash after them would be a +// regular expression). +// +// The `startsExpr` property is used to check if the token ends a +// `yield` expression. It is set on all token types that either can +// directly start an expression (like a quotation mark) or can +// continue an expression (like the body of a string). +// +// `isLoop` marks a keyword as starting a loop, which is important +// to know when parsing a label, in order to allow or disallow +// continue jumps to that label. + +var TokenType = function TokenType(label, conf) { + if ( conf === void 0 ) conf = {}; + + this.label = label; + this.keyword = conf.keyword; + this.beforeExpr = !!conf.beforeExpr; + this.startsExpr = !!conf.startsExpr; + this.isLoop = !!conf.isLoop; + this.isAssign = !!conf.isAssign; + this.prefix = !!conf.prefix; + this.postfix = !!conf.postfix; + this.binop = conf.binop || null; + this.updateContext = null; +}; + +function binop(name, prec) { + return new TokenType(name, {beforeExpr: true, binop: prec}) +} +var beforeExpr = {beforeExpr: true}; +var startsExpr = {startsExpr: true}; + +// Map keyword names to token types. + +var keywords$1 = {}; + +// Succinct definitions of keyword token types +function kw(name, options) { + if ( options === void 0 ) options = {}; + + options.keyword = name; + return keywords$1[name] = new TokenType(name, options) +} + +var types = { + num: new TokenType("num", startsExpr), + regexp: new TokenType("regexp", startsExpr), + string: new TokenType("string", startsExpr), + name: new TokenType("name", startsExpr), + eof: new TokenType("eof"), + + // Punctuation token types. + bracketL: new TokenType("[", {beforeExpr: true, startsExpr: true}), + bracketR: new TokenType("]"), + braceL: new TokenType("{", {beforeExpr: true, startsExpr: true}), + braceR: new TokenType("}"), + parenL: new TokenType("(", {beforeExpr: true, startsExpr: true}), + parenR: new TokenType(")"), + comma: new TokenType(",", beforeExpr), + semi: new TokenType(";", beforeExpr), + colon: new TokenType(":", beforeExpr), + dot: new TokenType("."), + question: new TokenType("?", beforeExpr), + arrow: new TokenType("=>", beforeExpr), + template: new TokenType("template"), + invalidTemplate: new TokenType("invalidTemplate"), + ellipsis: new TokenType("...", beforeExpr), + backQuote: new TokenType("`", startsExpr), + dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}), + + // Operators. These carry several kinds of properties to help the + // parser use them properly (the presence of these properties is + // what categorizes them as operators). + // + // `binop`, when present, specifies that this operator is a binary + // operator, and will refer to its precedence. + // + // `prefix` and `postfix` mark the operator as a prefix or postfix + // unary operator. + // + // `isAssign` marks all of `=`, `+=`, `-=` etcetera, which act as + // binary operators with a very low precedence, that should result + // in AssignmentExpression nodes. + + eq: new TokenType("=", {beforeExpr: true, isAssign: true}), + assign: new TokenType("_=", {beforeExpr: true, isAssign: true}), + incDec: new TokenType("++/--", {prefix: true, postfix: true, startsExpr: true}), + prefix: new TokenType("!/~", {beforeExpr: true, prefix: true, startsExpr: true}), + logicalOR: binop("||", 1), + logicalAND: binop("&&", 2), + bitwiseOR: binop("|", 3), + bitwiseXOR: binop("^", 4), + bitwiseAND: binop("&", 5), + equality: binop("==/!=/===/!==", 6), + relational: binop("/<=/>=", 7), + bitShift: binop("<>/>>>", 8), + plusMin: new TokenType("+/-", {beforeExpr: true, binop: 9, prefix: true, startsExpr: true}), + modulo: binop("%", 10), + star: binop("*", 10), + slash: binop("/", 10), + starstar: new TokenType("**", {beforeExpr: true}), + + // Keyword token types. + _break: kw("break"), + _case: kw("case", beforeExpr), + _catch: kw("catch"), + _continue: kw("continue"), + _debugger: kw("debugger"), + _default: kw("default", beforeExpr), + _do: kw("do", {isLoop: true, beforeExpr: true}), + _else: kw("else", beforeExpr), + _finally: kw("finally"), + _for: kw("for", {isLoop: true}), + _function: kw("function", startsExpr), + _if: kw("if"), + _return: kw("return", beforeExpr), + _switch: kw("switch"), + _throw: kw("throw", beforeExpr), + _try: kw("try"), + _var: kw("var"), + _const: kw("const"), + _while: kw("while", {isLoop: true}), + _with: kw("with"), + _new: kw("new", {beforeExpr: true, startsExpr: true}), + _this: kw("this", startsExpr), + _super: kw("super", startsExpr), + _class: kw("class", startsExpr), + _extends: kw("extends", beforeExpr), + _export: kw("export"), + _import: kw("import"), + _null: kw("null", startsExpr), + _true: kw("true", startsExpr), + _false: kw("false", startsExpr), + _in: kw("in", {beforeExpr: true, binop: 7}), + _instanceof: kw("instanceof", {beforeExpr: true, binop: 7}), + _typeof: kw("typeof", {beforeExpr: true, prefix: true, startsExpr: true}), + _void: kw("void", {beforeExpr: true, prefix: true, startsExpr: true}), + _delete: kw("delete", {beforeExpr: true, prefix: true, startsExpr: true}) +}; + +// Matches a whole line break (where CRLF is considered a single +// line break). Used to count lines. + +var lineBreak = /\r\n?|\n|\u2028|\u2029/; +var lineBreakG = new RegExp(lineBreak.source, "g"); + +function isNewLine(code) { + return code === 10 || code === 13 || code === 0x2028 || code === 0x2029 +} + +var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/; + +var skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g; + +var ref = Object.prototype; +var hasOwnProperty = ref.hasOwnProperty; +var toString = ref.toString; + +// Checks if an object has a property. + +function has(obj, propName) { + return hasOwnProperty.call(obj, propName) +} + +var isArray = Array.isArray || (function (obj) { return ( + toString.call(obj) === "[object Array]" +); }); + +// These are used when `options.locations` is on, for the +// `startLoc` and `endLoc` properties. + +var Position = function Position(line, col) { + this.line = line; + this.column = col; +}; + +Position.prototype.offset = function offset (n) { + return new Position(this.line, this.column + n) +}; + +var SourceLocation = function SourceLocation(p, start, end) { + this.start = start; + this.end = end; + if (p.sourceFile !== null) { this.source = p.sourceFile; } +}; + +// The `getLineInfo` function is mostly useful when the +// `locations` option is off (for performance reasons) and you +// want to find the line/column position for a given character +// offset. `input` should be the code string that the offset refers +// into. + +function getLineInfo(input, offset) { + for (var line = 1, cur = 0;;) { + lineBreakG.lastIndex = cur; + var match = lineBreakG.exec(input); + if (match && match.index < offset) { + ++line; + cur = match.index + match[0].length; + } else { + return new Position(line, offset - cur) + } + } +} + +// A second optional argument can be given to further configure +// the parser process. These options are recognized: + +var defaultOptions = { + // `ecmaVersion` indicates the ECMAScript version to parse. Must + // be either 3, 5, 6 (2015), 7 (2016), or 8 (2017). This influences support + // for strict mode, the set of reserved words, and support for + // new syntax features. The default is 7. + ecmaVersion: 7, + // `sourceType` indicates the mode the code should be parsed in. + // Can be either `"script"` or `"module"`. This influences global + // strict mode and parsing of `import` and `export` declarations. + sourceType: "script", + // `onInsertedSemicolon` can be a callback that will be called + // when a semicolon is automatically inserted. It will be passed + // th position of the comma as an offset, and if `locations` is + // enabled, it is given the location as a `{line, column}` object + // as second argument. + onInsertedSemicolon: null, + // `onTrailingComma` is similar to `onInsertedSemicolon`, but for + // trailing commas. + onTrailingComma: null, + // By default, reserved words are only enforced if ecmaVersion >= 5. + // Set `allowReserved` to a boolean value to explicitly turn this on + // an off. When this option has the value "never", reserved words + // and keywords can also not be used as property names. + allowReserved: null, + // When enabled, a return at the top level is not considered an + // error. + allowReturnOutsideFunction: false, + // When enabled, import/export statements are not constrained to + // appearing at the top of the program. + allowImportExportEverywhere: false, + // When enabled, hashbang directive in the beginning of file + // is allowed and treated as a line comment. + allowHashBang: false, + // When `locations` is on, `loc` properties holding objects with + // `start` and `end` properties in `{line, column}` form (with + // line being 1-based and column 0-based) will be attached to the + // nodes. + locations: false, + // A function can be passed as `onToken` option, which will + // cause Acorn to call that function with object in the same + // format as tokens returned from `tokenizer().getToken()`. Note + // that you are not allowed to call the parser from the + // callback—that will corrupt its internal state. + onToken: null, + // A function can be passed as `onComment` option, which will + // cause Acorn to call that function with `(block, text, start, + // end)` parameters whenever a comment is skipped. `block` is a + // boolean indicating whether this is a block (`/* */`) comment, + // `text` is the content of the comment, and `start` and `end` are + // character offsets that denote the start and end of the comment. + // When the `locations` option is on, two more parameters are + // passed, the full `{line, column}` locations of the start and + // end of the comments. Note that you are not allowed to call the + // parser from the callback—that will corrupt its internal state. + onComment: null, + // Nodes have their start and end characters offsets recorded in + // `start` and `end` properties (directly on the node, rather than + // the `loc` object, which holds line/column data. To also add a + // [semi-standardized][range] `range` property holding a `[start, + // end]` array with the same numbers, set the `ranges` option to + // `true`. + // + // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678 + ranges: false, + // It is possible to parse multiple files into a single AST by + // passing the tree produced by parsing the first file as + // `program` option in subsequent parses. This will add the + // toplevel forms of the parsed file to the `Program` (top) node + // of an existing parse tree. + program: null, + // When `locations` is on, you can pass this to record the source + // file in every node's `loc` object. + sourceFile: null, + // This value, if given, is stored in every node, whether + // `locations` is on or off. + directSourceFile: null, + // When enabled, parenthesized expressions are represented by + // (non-standard) ParenthesizedExpression nodes + preserveParens: false, + plugins: {} +}; + +// Interpret and default an options object + +function getOptions(opts) { + var options = {}; + + for (var opt in defaultOptions) + { options[opt] = opts && has(opts, opt) ? opts[opt] : defaultOptions[opt]; } + + if (options.ecmaVersion >= 2015) + { options.ecmaVersion -= 2009; } + + if (options.allowReserved == null) + { options.allowReserved = options.ecmaVersion < 5; } + + if (isArray(options.onToken)) { + var tokens = options.onToken; + options.onToken = function (token) { return tokens.push(token); }; + } + if (isArray(options.onComment)) + { options.onComment = pushComment(options, options.onComment); } + + return options +} + +function pushComment(options, array) { + return function(block, text, start, end, startLoc, endLoc) { + var comment = { + type: block ? "Block" : "Line", + value: text, + start: start, + end: end + }; + if (options.locations) + { comment.loc = new SourceLocation(this, startLoc, endLoc); } + if (options.ranges) + { comment.range = [start, end]; } + array.push(comment); + } +} + +// Registered plugins +var plugins = {}; + +function keywordRegexp(words) { + return new RegExp("^(?:" + words.replace(/ /g, "|") + ")$") +} + +var Parser = function Parser(options, input, startPos) { + this.options = options = getOptions(options); + this.sourceFile = options.sourceFile; + this.keywords = keywordRegexp(keywords[options.ecmaVersion >= 6 ? 6 : 5]); + var reserved = ""; + if (!options.allowReserved) { + for (var v = options.ecmaVersion;; v--) + { if (reserved = reservedWords[v]) { break } } + if (options.sourceType == "module") { reserved += " await"; } + } + this.reservedWords = keywordRegexp(reserved); + var reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict; + this.reservedWordsStrict = keywordRegexp(reservedStrict); + this.reservedWordsStrictBind = keywordRegexp(reservedStrict + " " + reservedWords.strictBind); + this.input = String(input); + + // Used to signal to callers of `readWord1` whether the word + // contained any escape sequences. This is needed because words with + // escape sequences must not be interpreted as keywords. + this.containsEsc = false; + + // Load plugins + this.loadPlugins(options.plugins); + + // Set up token state + + // The current position of the tokenizer in the input. + if (startPos) { + this.pos = startPos; + this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1; + this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length; + } else { + this.pos = this.lineStart = 0; + this.curLine = 1; + } + + // Properties of the current token: + // Its type + this.type = types.eof; + // For tokens that include more information than their type, the value + this.value = null; + // Its start and end offset + this.start = this.end = this.pos; + // And, if locations are used, the {line, column} object + // corresponding to those offsets + this.startLoc = this.endLoc = this.curPosition(); + + // Position information for the previous token + this.lastTokEndLoc = this.lastTokStartLoc = null; + this.lastTokStart = this.lastTokEnd = this.pos; + + // The context stack is used to superficially track syntactic + // context to predict whether a regular expression is allowed in a + // given position. + this.context = this.initialContext(); + this.exprAllowed = true; + + // Figure out if it's a module code. + this.inModule = options.sourceType === "module"; + this.strict = this.inModule || this.strictDirective(this.pos); + + // Used to signify the start of a potential arrow function + this.potentialArrowAt = -1; + + // Flags to track whether we are in a function, a generator, an async function. + this.inFunction = this.inGenerator = this.inAsync = false; + // Positions to delayed-check that yield/await does not exist in default parameters. + this.yieldPos = this.awaitPos = 0; + // Labels in scope. + this.labels = []; + + // If enabled, skip leading hashbang line. + if (this.pos === 0 && options.allowHashBang && this.input.slice(0, 2) === "#!") + { this.skipLineComment(2); } + + // Scope tracking for duplicate variable names (see scope.js) + this.scopeStack = []; + this.enterFunctionScope(); +}; + +// DEPRECATED Kept for backwards compatibility until 3.0 in case a plugin uses them +Parser.prototype.isKeyword = function isKeyword (word) { return this.keywords.test(word) }; +Parser.prototype.isReservedWord = function isReservedWord (word) { return this.reservedWords.test(word) }; + +Parser.prototype.extend = function extend (name, f) { + this[name] = f(this[name]); +}; + +Parser.prototype.loadPlugins = function loadPlugins (pluginConfigs) { + var this$1 = this; + + for (var name in pluginConfigs) { + var plugin = plugins[name]; + if (!plugin) { throw new Error("Plugin '" + name + "' not found") } + plugin(this$1, pluginConfigs[name]); + } +}; + +Parser.prototype.parse = function parse () { + var node = this.options.program || this.startNode(); + this.nextToken(); + return this.parseTopLevel(node) +}; + +var pp = Parser.prototype; + +// ## Parser utilities + +var literal = /^(?:'((?:\\.|[^'])*?)'|"((?:\\.|[^"])*?)"|;)/; +pp.strictDirective = function(start) { + var this$1 = this; + + for (;;) { + skipWhiteSpace.lastIndex = start; + start += skipWhiteSpace.exec(this$1.input)[0].length; + var match = literal.exec(this$1.input.slice(start)); + if (!match) { return false } + if ((match[1] || match[2]) == "use strict") { return true } + start += match[0].length; + } +}; + +// Predicate that tests whether the next token is of the given +// type, and if yes, consumes it as a side effect. + +pp.eat = function(type) { + if (this.type === type) { + this.next(); + return true + } else { + return false + } +}; + +// Tests whether parsed token is a contextual keyword. + +pp.isContextual = function(name) { + return this.type === types.name && this.value === name && !this.containsEsc +}; + +// Consumes contextual keyword if possible. + +pp.eatContextual = function(name) { + if (!this.isContextual(name)) { return false } + this.next(); + return true +}; + +// Asserts that following token is given contextual keyword. + +pp.expectContextual = function(name) { + if (!this.eatContextual(name)) { this.unexpected(); } +}; + +// Test whether a semicolon can be inserted at the current position. + +pp.canInsertSemicolon = function() { + return this.type === types.eof || + this.type === types.braceR || + lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) +}; + +pp.insertSemicolon = function() { + if (this.canInsertSemicolon()) { + if (this.options.onInsertedSemicolon) + { this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc); } + return true + } +}; + +// Consume a semicolon, or, failing that, see if we are allowed to +// pretend that there is a semicolon at this position. + +pp.semicolon = function() { + if (!this.eat(types.semi) && !this.insertSemicolon()) { this.unexpected(); } +}; + +pp.afterTrailingComma = function(tokType, notNext) { + if (this.type == tokType) { + if (this.options.onTrailingComma) + { this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc); } + if (!notNext) + { this.next(); } + return true + } +}; + +// Expect a token of a given type. If found, consume it, otherwise, +// raise an unexpected token error. + +pp.expect = function(type) { + this.eat(type) || this.unexpected(); +}; + +// Raise an unexpected token error. + +pp.unexpected = function(pos) { + this.raise(pos != null ? pos : this.start, "Unexpected token"); +}; + +function DestructuringErrors() { + this.shorthandAssign = + this.trailingComma = + this.parenthesizedAssign = + this.parenthesizedBind = + this.doubleProto = + -1; +} + +pp.checkPatternErrors = function(refDestructuringErrors, isAssign) { + if (!refDestructuringErrors) { return } + if (refDestructuringErrors.trailingComma > -1) + { this.raiseRecoverable(refDestructuringErrors.trailingComma, "Comma is not permitted after the rest element"); } + var parens = isAssign ? refDestructuringErrors.parenthesizedAssign : refDestructuringErrors.parenthesizedBind; + if (parens > -1) { this.raiseRecoverable(parens, "Parenthesized pattern"); } +}; + +pp.checkExpressionErrors = function(refDestructuringErrors, andThrow) { + if (!refDestructuringErrors) { return false } + var shorthandAssign = refDestructuringErrors.shorthandAssign; + var doubleProto = refDestructuringErrors.doubleProto; + if (!andThrow) { return shorthandAssign >= 0 || doubleProto >= 0 } + if (shorthandAssign >= 0) + { this.raise(shorthandAssign, "Shorthand property assignments are valid only in destructuring patterns"); } + if (doubleProto >= 0) + { this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property"); } +}; + +pp.checkYieldAwaitInDefaultParams = function() { + if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos)) + { this.raise(this.yieldPos, "Yield expression cannot be a default value"); } + if (this.awaitPos) + { this.raise(this.awaitPos, "Await expression cannot be a default value"); } +}; + +pp.isSimpleAssignTarget = function(expr) { + if (expr.type === "ParenthesizedExpression") + { return this.isSimpleAssignTarget(expr.expression) } + return expr.type === "Identifier" || expr.type === "MemberExpression" +}; + +var pp$1 = Parser.prototype; + +// ### Statement parsing + +// Parse a program. Initializes the parser, reads any number of +// statements, and wraps them in a Program node. Optionally takes a +// `program` argument. If present, the statements will be appended +// to its body instead of creating a new node. + +pp$1.parseTopLevel = function(node) { + var this$1 = this; + + var exports = {}; + if (!node.body) { node.body = []; } + while (this.type !== types.eof) { + var stmt = this$1.parseStatement(true, true, exports); + node.body.push(stmt); + } + this.adaptDirectivePrologue(node.body); + this.next(); + if (this.options.ecmaVersion >= 6) { + node.sourceType = this.options.sourceType; + } + return this.finishNode(node, "Program") +}; + +var loopLabel = {kind: "loop"}; +var switchLabel = {kind: "switch"}; + +pp$1.isLet = function() { + if (this.options.ecmaVersion < 6 || !this.isContextual("let")) { return false } + skipWhiteSpace.lastIndex = this.pos; + var skip = skipWhiteSpace.exec(this.input); + var next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next); + if (nextCh === 91 || nextCh == 123) { return true } // '{' and '[' + if (isIdentifierStart(nextCh, true)) { + var pos = next + 1; + while (isIdentifierChar(this.input.charCodeAt(pos), true)) { ++pos; } + var ident = this.input.slice(next, pos); + if (!keywordRelationalOperator.test(ident)) { return true } + } + return false +}; + +// check 'async [no LineTerminator here] function' +// - 'async /*foo*/ function' is OK. +// - 'async /*\n*/ function' is invalid. +pp$1.isAsyncFunction = function() { + if (this.options.ecmaVersion < 8 || !this.isContextual("async")) + { return false } + + skipWhiteSpace.lastIndex = this.pos; + var skip = skipWhiteSpace.exec(this.input); + var next = this.pos + skip[0].length; + return !lineBreak.test(this.input.slice(this.pos, next)) && + this.input.slice(next, next + 8) === "function" && + (next + 8 == this.input.length || !isIdentifierChar(this.input.charAt(next + 8))) +}; + +// Parse a single statement. +// +// If expecting a statement and finding a slash operator, parse a +// regular expression literal. This is to handle cases like +// `if (foo) /blah/.exec(foo)`, where looking at the previous token +// does not help. + +pp$1.parseStatement = function(declaration, topLevel, exports) { + var starttype = this.type, node = this.startNode(), kind; + + if (this.isLet()) { + starttype = types._var; + kind = "let"; + } + + // Most types of statements are recognized by the keyword they + // start with. Many are trivial to parse, some require a bit of + // complexity. + + switch (starttype) { + case types._break: case types._continue: return this.parseBreakContinueStatement(node, starttype.keyword) + case types._debugger: return this.parseDebuggerStatement(node) + case types._do: return this.parseDoStatement(node) + case types._for: return this.parseForStatement(node) + case types._function: + if (!declaration && this.options.ecmaVersion >= 6) { this.unexpected(); } + return this.parseFunctionStatement(node, false) + case types._class: + if (!declaration) { this.unexpected(); } + return this.parseClass(node, true) + case types._if: return this.parseIfStatement(node) + case types._return: return this.parseReturnStatement(node) + case types._switch: return this.parseSwitchStatement(node) + case types._throw: return this.parseThrowStatement(node) + case types._try: return this.parseTryStatement(node) + case types._const: case types._var: + kind = kind || this.value; + if (!declaration && kind != "var") { this.unexpected(); } + return this.parseVarStatement(node, kind) + case types._while: return this.parseWhileStatement(node) + case types._with: return this.parseWithStatement(node) + case types.braceL: return this.parseBlock() + case types.semi: return this.parseEmptyStatement(node) + case types._export: + case types._import: + if (!this.options.allowImportExportEverywhere) { + if (!topLevel) + { this.raise(this.start, "'import' and 'export' may only appear at the top level"); } + if (!this.inModule) + { this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'"); } + } + return starttype === types._import ? this.parseImport(node) : this.parseExport(node, exports) + + // If the statement does not start with a statement keyword or a + // brace, it's an ExpressionStatement or LabeledStatement. We + // simply start parsing an expression, and afterwards, if the + // next token is a colon and the expression was a simple + // Identifier node, we switch to interpreting it as a label. + default: + if (this.isAsyncFunction()) { + if (!declaration) { this.unexpected(); } + this.next(); + return this.parseFunctionStatement(node, true) + } + + var maybeName = this.value, expr = this.parseExpression(); + if (starttype === types.name && expr.type === "Identifier" && this.eat(types.colon)) + { return this.parseLabeledStatement(node, maybeName, expr) } + else { return this.parseExpressionStatement(node, expr) } + } +}; + +pp$1.parseBreakContinueStatement = function(node, keyword) { + var this$1 = this; + + var isBreak = keyword == "break"; + this.next(); + if (this.eat(types.semi) || this.insertSemicolon()) { node.label = null; } + else if (this.type !== types.name) { this.unexpected(); } + else { + node.label = this.parseIdent(); + this.semicolon(); + } + + // Verify that there is an actual destination to break or + // continue to. + var i = 0; + for (; i < this.labels.length; ++i) { + var lab = this$1.labels[i]; + if (node.label == null || lab.name === node.label.name) { + if (lab.kind != null && (isBreak || lab.kind === "loop")) { break } + if (node.label && isBreak) { break } + } + } + if (i === this.labels.length) { this.raise(node.start, "Unsyntactic " + keyword); } + return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement") +}; + +pp$1.parseDebuggerStatement = function(node) { + this.next(); + this.semicolon(); + return this.finishNode(node, "DebuggerStatement") +}; + +pp$1.parseDoStatement = function(node) { + this.next(); + this.labels.push(loopLabel); + node.body = this.parseStatement(false); + this.labels.pop(); + this.expect(types._while); + node.test = this.parseParenExpression(); + if (this.options.ecmaVersion >= 6) + { this.eat(types.semi); } + else + { this.semicolon(); } + return this.finishNode(node, "DoWhileStatement") +}; + +// Disambiguating between a `for` and a `for`/`in` or `for`/`of` +// loop is non-trivial. Basically, we have to parse the init `var` +// statement or expression, disallowing the `in` operator (see +// the second parameter to `parseExpression`), and then check +// whether the next token is `in` or `of`. When there is no init +// part (semicolon immediately after the opening parenthesis), it +// is a regular `for` loop. + +pp$1.parseForStatement = function(node) { + this.next(); + var awaitAt = (this.options.ecmaVersion >= 9 && this.inAsync && this.eatContextual("await")) ? this.lastTokStart : -1; + this.labels.push(loopLabel); + this.enterLexicalScope(); + this.expect(types.parenL); + if (this.type === types.semi) { + if (awaitAt > -1) { this.unexpected(awaitAt); } + return this.parseFor(node, null) + } + var isLet = this.isLet(); + if (this.type === types._var || this.type === types._const || isLet) { + var init$1 = this.startNode(), kind = isLet ? "let" : this.value; + this.next(); + this.parseVar(init$1, true, kind); + this.finishNode(init$1, "VariableDeclaration"); + if ((this.type === types._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init$1.declarations.length === 1 && + !(kind !== "var" && init$1.declarations[0].init)) { + if (this.options.ecmaVersion >= 9) { + if (this.type === types._in) { + if (awaitAt > -1) { this.unexpected(awaitAt); } + } else { node.await = awaitAt > -1; } + } + return this.parseForIn(node, init$1) + } + if (awaitAt > -1) { this.unexpected(awaitAt); } + return this.parseFor(node, init$1) + } + var refDestructuringErrors = new DestructuringErrors; + var init = this.parseExpression(true, refDestructuringErrors); + if (this.type === types._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) { + if (this.options.ecmaVersion >= 9) { + if (this.type === types._in) { + if (awaitAt > -1) { this.unexpected(awaitAt); } + } else { node.await = awaitAt > -1; } + } + this.toAssignable(init, false, refDestructuringErrors); + this.checkLVal(init); + return this.parseForIn(node, init) + } else { + this.checkExpressionErrors(refDestructuringErrors, true); + } + if (awaitAt > -1) { this.unexpected(awaitAt); } + return this.parseFor(node, init) +}; + +pp$1.parseFunctionStatement = function(node, isAsync) { + this.next(); + return this.parseFunction(node, true, false, isAsync) +}; + +pp$1.parseIfStatement = function(node) { + this.next(); + node.test = this.parseParenExpression(); + // allow function declarations in branches, but only in non-strict mode + node.consequent = this.parseStatement(!this.strict && this.type == types._function); + node.alternate = this.eat(types._else) ? this.parseStatement(!this.strict && this.type == types._function) : null; + return this.finishNode(node, "IfStatement") +}; + +pp$1.parseReturnStatement = function(node) { + if (!this.inFunction && !this.options.allowReturnOutsideFunction) + { this.raise(this.start, "'return' outside of function"); } + this.next(); + + // In `return` (and `break`/`continue`), the keywords with + // optional arguments, we eagerly look for a semicolon or the + // possibility to insert one. + + if (this.eat(types.semi) || this.insertSemicolon()) { node.argument = null; } + else { node.argument = this.parseExpression(); this.semicolon(); } + return this.finishNode(node, "ReturnStatement") +}; + +pp$1.parseSwitchStatement = function(node) { + var this$1 = this; + + this.next(); + node.discriminant = this.parseParenExpression(); + node.cases = []; + this.expect(types.braceL); + this.labels.push(switchLabel); + this.enterLexicalScope(); + + // Statements under must be grouped (by label) in SwitchCase + // nodes. `cur` is used to keep the node that we are currently + // adding statements to. + + var cur; + for (var sawDefault = false; this.type != types.braceR;) { + if (this$1.type === types._case || this$1.type === types._default) { + var isCase = this$1.type === types._case; + if (cur) { this$1.finishNode(cur, "SwitchCase"); } + node.cases.push(cur = this$1.startNode()); + cur.consequent = []; + this$1.next(); + if (isCase) { + cur.test = this$1.parseExpression(); + } else { + if (sawDefault) { this$1.raiseRecoverable(this$1.lastTokStart, "Multiple default clauses"); } + sawDefault = true; + cur.test = null; + } + this$1.expect(types.colon); + } else { + if (!cur) { this$1.unexpected(); } + cur.consequent.push(this$1.parseStatement(true)); + } + } + this.exitLexicalScope(); + if (cur) { this.finishNode(cur, "SwitchCase"); } + this.next(); // Closing brace + this.labels.pop(); + return this.finishNode(node, "SwitchStatement") +}; + +pp$1.parseThrowStatement = function(node) { + this.next(); + if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) + { this.raise(this.lastTokEnd, "Illegal newline after throw"); } + node.argument = this.parseExpression(); + this.semicolon(); + return this.finishNode(node, "ThrowStatement") +}; + +// Reused empty array added for node fields that are always empty. + +var empty = []; + +pp$1.parseTryStatement = function(node) { + this.next(); + node.block = this.parseBlock(); + node.handler = null; + if (this.type === types._catch) { + var clause = this.startNode(); + this.next(); + this.expect(types.parenL); + clause.param = this.parseBindingAtom(); + this.enterLexicalScope(); + this.checkLVal(clause.param, "let"); + this.expect(types.parenR); + clause.body = this.parseBlock(false); + this.exitLexicalScope(); + node.handler = this.finishNode(clause, "CatchClause"); + } + node.finalizer = this.eat(types._finally) ? this.parseBlock() : null; + if (!node.handler && !node.finalizer) + { this.raise(node.start, "Missing catch or finally clause"); } + return this.finishNode(node, "TryStatement") +}; + +pp$1.parseVarStatement = function(node, kind) { + this.next(); + this.parseVar(node, false, kind); + this.semicolon(); + return this.finishNode(node, "VariableDeclaration") +}; + +pp$1.parseWhileStatement = function(node) { + this.next(); + node.test = this.parseParenExpression(); + this.labels.push(loopLabel); + node.body = this.parseStatement(false); + this.labels.pop(); + return this.finishNode(node, "WhileStatement") +}; + +pp$1.parseWithStatement = function(node) { + if (this.strict) { this.raise(this.start, "'with' in strict mode"); } + this.next(); + node.object = this.parseParenExpression(); + node.body = this.parseStatement(false); + return this.finishNode(node, "WithStatement") +}; + +pp$1.parseEmptyStatement = function(node) { + this.next(); + return this.finishNode(node, "EmptyStatement") +}; + +pp$1.parseLabeledStatement = function(node, maybeName, expr) { + var this$1 = this; + + for (var i$1 = 0, list = this$1.labels; i$1 < list.length; i$1 += 1) + { + var label = list[i$1]; + + if (label.name === maybeName) + { this$1.raise(expr.start, "Label '" + maybeName + "' is already declared"); + } } + var kind = this.type.isLoop ? "loop" : this.type === types._switch ? "switch" : null; + for (var i = this.labels.length - 1; i >= 0; i--) { + var label$1 = this$1.labels[i]; + if (label$1.statementStart == node.start) { + // Update information about previous labels on this node + label$1.statementStart = this$1.start; + label$1.kind = kind; + } else { break } + } + this.labels.push({name: maybeName, kind: kind, statementStart: this.start}); + node.body = this.parseStatement(true); + if (node.body.type == "ClassDeclaration" || + node.body.type == "VariableDeclaration" && node.body.kind != "var" || + node.body.type == "FunctionDeclaration" && (this.strict || node.body.generator)) + { this.raiseRecoverable(node.body.start, "Invalid labeled declaration"); } + this.labels.pop(); + node.label = expr; + return this.finishNode(node, "LabeledStatement") +}; + +pp$1.parseExpressionStatement = function(node, expr) { + node.expression = expr; + this.semicolon(); + return this.finishNode(node, "ExpressionStatement") +}; + +// Parse a semicolon-enclosed block of statements, handling `"use +// strict"` declarations when `allowStrict` is true (used for +// function bodies). + +pp$1.parseBlock = function(createNewLexicalScope) { + var this$1 = this; + if ( createNewLexicalScope === void 0 ) createNewLexicalScope = true; + + var node = this.startNode(); + node.body = []; + this.expect(types.braceL); + if (createNewLexicalScope) { + this.enterLexicalScope(); + } + while (!this.eat(types.braceR)) { + var stmt = this$1.parseStatement(true); + node.body.push(stmt); + } + if (createNewLexicalScope) { + this.exitLexicalScope(); + } + return this.finishNode(node, "BlockStatement") +}; + +// Parse a regular `for` loop. The disambiguation code in +// `parseStatement` will already have parsed the init statement or +// expression. + +pp$1.parseFor = function(node, init) { + node.init = init; + this.expect(types.semi); + node.test = this.type === types.semi ? null : this.parseExpression(); + this.expect(types.semi); + node.update = this.type === types.parenR ? null : this.parseExpression(); + this.expect(types.parenR); + this.exitLexicalScope(); + node.body = this.parseStatement(false); + this.labels.pop(); + return this.finishNode(node, "ForStatement") +}; + +// Parse a `for`/`in` and `for`/`of` loop, which are almost +// same from parser's perspective. + +pp$1.parseForIn = function(node, init) { + var type = this.type === types._in ? "ForInStatement" : "ForOfStatement"; + this.next(); + if (type == "ForInStatement") { + if (init.type === "AssignmentPattern" || + (init.type === "VariableDeclaration" && init.declarations[0].init != null && + (this.strict || init.declarations[0].id.type !== "Identifier"))) + { this.raise(init.start, "Invalid assignment in for-in loop head"); } + } + node.left = init; + node.right = type == "ForInStatement" ? this.parseExpression() : this.parseMaybeAssign(); + this.expect(types.parenR); + this.exitLexicalScope(); + node.body = this.parseStatement(false); + this.labels.pop(); + return this.finishNode(node, type) +}; + +// Parse a list of variable declarations. + +pp$1.parseVar = function(node, isFor, kind) { + var this$1 = this; + + node.declarations = []; + node.kind = kind; + for (;;) { + var decl = this$1.startNode(); + this$1.parseVarId(decl, kind); + if (this$1.eat(types.eq)) { + decl.init = this$1.parseMaybeAssign(isFor); + } else if (kind === "const" && !(this$1.type === types._in || (this$1.options.ecmaVersion >= 6 && this$1.isContextual("of")))) { + this$1.unexpected(); + } else if (decl.id.type != "Identifier" && !(isFor && (this$1.type === types._in || this$1.isContextual("of")))) { + this$1.raise(this$1.lastTokEnd, "Complex binding patterns require an initialization value"); + } else { + decl.init = null; + } + node.declarations.push(this$1.finishNode(decl, "VariableDeclarator")); + if (!this$1.eat(types.comma)) { break } + } + return node +}; + +pp$1.parseVarId = function(decl, kind) { + decl.id = this.parseBindingAtom(kind); + this.checkLVal(decl.id, kind, false); +}; + +// Parse a function declaration or literal (depending on the +// `isStatement` parameter). + +pp$1.parseFunction = function(node, isStatement, allowExpressionBody, isAsync) { + this.initFunction(node); + if (this.options.ecmaVersion >= 9 || this.options.ecmaVersion >= 6 && !isAsync) + { node.generator = this.eat(types.star); } + if (this.options.ecmaVersion >= 8) + { node.async = !!isAsync; } + + if (isStatement) { + node.id = isStatement === "nullableID" && this.type != types.name ? null : this.parseIdent(); + if (node.id) { + this.checkLVal(node.id, "var"); + } + } + + var oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction; + this.inGenerator = node.generator; + this.inAsync = node.async; + this.yieldPos = 0; + this.awaitPos = 0; + this.inFunction = true; + this.enterFunctionScope(); + + if (!isStatement) + { node.id = this.type == types.name ? this.parseIdent() : null; } + + this.parseFunctionParams(node); + this.parseFunctionBody(node, allowExpressionBody); + + this.inGenerator = oldInGen; + this.inAsync = oldInAsync; + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + this.inFunction = oldInFunc; + return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression") +}; + +pp$1.parseFunctionParams = function(node) { + this.expect(types.parenL); + node.params = this.parseBindingList(types.parenR, false, this.options.ecmaVersion >= 8); + this.checkYieldAwaitInDefaultParams(); +}; + +// Parse a class declaration or literal (depending on the +// `isStatement` parameter). + +pp$1.parseClass = function(node, isStatement) { + var this$1 = this; + + this.next(); + + this.parseClassId(node, isStatement); + this.parseClassSuper(node); + var classBody = this.startNode(); + var hadConstructor = false; + classBody.body = []; + this.expect(types.braceL); + while (!this.eat(types.braceR)) { + var member = this$1.parseClassMember(classBody); + if (member && member.type === "MethodDefinition" && member.kind === "constructor") { + if (hadConstructor) { this$1.raise(member.start, "Duplicate constructor in the same class"); } + hadConstructor = true; + } + } + node.body = this.finishNode(classBody, "ClassBody"); + return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") +}; + +pp$1.parseClassMember = function(classBody) { + var this$1 = this; + + if (this.eat(types.semi)) { return null } + + var method = this.startNode(); + var tryContextual = function (k, noLineBreak) { + if ( noLineBreak === void 0 ) noLineBreak = false; + + var start = this$1.start, startLoc = this$1.startLoc; + if (!this$1.eatContextual(k)) { return false } + if (this$1.type !== types.parenL && (!noLineBreak || !this$1.canInsertSemicolon())) { return true } + if (method.key) { this$1.unexpected(); } + method.computed = false; + method.key = this$1.startNodeAt(start, startLoc); + method.key.name = k; + this$1.finishNode(method.key, "Identifier"); + return false + }; + + method.kind = "method"; + method.static = tryContextual("static"); + var isGenerator = this.eat(types.star); + var isAsync = false; + if (!isGenerator) { + if (this.options.ecmaVersion >= 8 && tryContextual("async", true)) { + isAsync = true; + isGenerator = this.options.ecmaVersion >= 9 && this.eat(types.star); + } else if (tryContextual("get")) { + method.kind = "get"; + } else if (tryContextual("set")) { + method.kind = "set"; + } + } + if (!method.key) { this.parsePropertyName(method); } + var key = method.key; + if (!method.computed && !method.static && (key.type === "Identifier" && key.name === "constructor" || + key.type === "Literal" && key.value === "constructor")) { + if (method.kind !== "method") { this.raise(key.start, "Constructor can't have get/set modifier"); } + if (isGenerator) { this.raise(key.start, "Constructor can't be a generator"); } + if (isAsync) { this.raise(key.start, "Constructor can't be an async method"); } + method.kind = "constructor"; + } else if (method.static && key.type === "Identifier" && key.name === "prototype") { + this.raise(key.start, "Classes may not have a static property named prototype"); + } + this.parseClassMethod(classBody, method, isGenerator, isAsync); + if (method.kind === "get" && method.value.params.length !== 0) + { this.raiseRecoverable(method.value.start, "getter should have no params"); } + if (method.kind === "set" && method.value.params.length !== 1) + { this.raiseRecoverable(method.value.start, "setter should have exactly one param"); } + if (method.kind === "set" && method.value.params[0].type === "RestElement") + { this.raiseRecoverable(method.value.params[0].start, "Setter cannot use rest params"); } + return method +}; + +pp$1.parseClassMethod = function(classBody, method, isGenerator, isAsync) { + method.value = this.parseMethod(isGenerator, isAsync); + classBody.body.push(this.finishNode(method, "MethodDefinition")); +}; + +pp$1.parseClassId = function(node, isStatement) { + node.id = this.type === types.name ? this.parseIdent() : isStatement === true ? this.unexpected() : null; +}; + +pp$1.parseClassSuper = function(node) { + node.superClass = this.eat(types._extends) ? this.parseExprSubscripts() : null; +}; + +// Parses module export declaration. + +pp$1.parseExport = function(node, exports) { + var this$1 = this; + + this.next(); + // export * from '...' + if (this.eat(types.star)) { + this.expectContextual("from"); + if (this.type !== types.string) { this.unexpected(); } + node.source = this.parseExprAtom(); + this.semicolon(); + return this.finishNode(node, "ExportAllDeclaration") + } + if (this.eat(types._default)) { // export default ... + this.checkExport(exports, "default", this.lastTokStart); + var isAsync; + if (this.type === types._function || (isAsync = this.isAsyncFunction())) { + var fNode = this.startNode(); + this.next(); + if (isAsync) { this.next(); } + node.declaration = this.parseFunction(fNode, "nullableID", false, isAsync); + } else if (this.type === types._class) { + var cNode = this.startNode(); + node.declaration = this.parseClass(cNode, "nullableID"); + } else { + node.declaration = this.parseMaybeAssign(); + this.semicolon(); + } + return this.finishNode(node, "ExportDefaultDeclaration") + } + // export var|const|let|function|class ... + if (this.shouldParseExportStatement()) { + node.declaration = this.parseStatement(true); + if (node.declaration.type === "VariableDeclaration") + { this.checkVariableExport(exports, node.declaration.declarations); } + else + { this.checkExport(exports, node.declaration.id.name, node.declaration.id.start); } + node.specifiers = []; + node.source = null; + } else { // export { x, y as z } [from '...'] + node.declaration = null; + node.specifiers = this.parseExportSpecifiers(exports); + if (this.eatContextual("from")) { + if (this.type !== types.string) { this.unexpected(); } + node.source = this.parseExprAtom(); + } else { + // check for keywords used as local names + for (var i = 0, list = node.specifiers; i < list.length; i += 1) { + var spec = list[i]; + + this$1.checkUnreserved(spec.local); + } + + node.source = null; + } + this.semicolon(); + } + return this.finishNode(node, "ExportNamedDeclaration") +}; + +pp$1.checkExport = function(exports, name, pos) { + if (!exports) { return } + if (has(exports, name)) + { this.raiseRecoverable(pos, "Duplicate export '" + name + "'"); } + exports[name] = true; +}; + +pp$1.checkPatternExport = function(exports, pat) { + var this$1 = this; + + var type = pat.type; + if (type == "Identifier") + { this.checkExport(exports, pat.name, pat.start); } + else if (type == "ObjectPattern") + { for (var i = 0, list = pat.properties; i < list.length; i += 1) + { + var prop = list[i]; + + this$1.checkPatternExport(exports, prop); + } } + else if (type == "ArrayPattern") + { for (var i$1 = 0, list$1 = pat.elements; i$1 < list$1.length; i$1 += 1) { + var elt = list$1[i$1]; + + if (elt) { this$1.checkPatternExport(exports, elt); } + } } + else if (type == "Property") + { this.checkPatternExport(exports, pat.value); } + else if (type == "AssignmentPattern") + { this.checkPatternExport(exports, pat.left); } + else if (type == "RestElement") + { this.checkPatternExport(exports, pat.argument); } + else if (type == "ParenthesizedExpression") + { this.checkPatternExport(exports, pat.expression); } +}; + +pp$1.checkVariableExport = function(exports, decls) { + var this$1 = this; + + if (!exports) { return } + for (var i = 0, list = decls; i < list.length; i += 1) + { + var decl = list[i]; + + this$1.checkPatternExport(exports, decl.id); + } +}; + +pp$1.shouldParseExportStatement = function() { + return this.type.keyword === "var" || + this.type.keyword === "const" || + this.type.keyword === "class" || + this.type.keyword === "function" || + this.isLet() || + this.isAsyncFunction() +}; + +// Parses a comma-separated list of module exports. + +pp$1.parseExportSpecifiers = function(exports) { + var this$1 = this; + + var nodes = [], first = true; + // export { x, y as z } [from '...'] + this.expect(types.braceL); + while (!this.eat(types.braceR)) { + if (!first) { + this$1.expect(types.comma); + if (this$1.afterTrailingComma(types.braceR)) { break } + } else { first = false; } + + var node = this$1.startNode(); + node.local = this$1.parseIdent(true); + node.exported = this$1.eatContextual("as") ? this$1.parseIdent(true) : node.local; + this$1.checkExport(exports, node.exported.name, node.exported.start); + nodes.push(this$1.finishNode(node, "ExportSpecifier")); + } + return nodes +}; + +// Parses import declaration. + +pp$1.parseImport = function(node) { + this.next(); + // import '...' + if (this.type === types.string) { + node.specifiers = empty; + node.source = this.parseExprAtom(); + } else { + node.specifiers = this.parseImportSpecifiers(); + this.expectContextual("from"); + node.source = this.type === types.string ? this.parseExprAtom() : this.unexpected(); + } + this.semicolon(); + return this.finishNode(node, "ImportDeclaration") +}; + +// Parses a comma-separated list of module imports. + +pp$1.parseImportSpecifiers = function() { + var this$1 = this; + + var nodes = [], first = true; + if (this.type === types.name) { + // import defaultObj, { x, y as z } from '...' + var node = this.startNode(); + node.local = this.parseIdent(); + this.checkLVal(node.local, "let"); + nodes.push(this.finishNode(node, "ImportDefaultSpecifier")); + if (!this.eat(types.comma)) { return nodes } + } + if (this.type === types.star) { + var node$1 = this.startNode(); + this.next(); + this.expectContextual("as"); + node$1.local = this.parseIdent(); + this.checkLVal(node$1.local, "let"); + nodes.push(this.finishNode(node$1, "ImportNamespaceSpecifier")); + return nodes + } + this.expect(types.braceL); + while (!this.eat(types.braceR)) { + if (!first) { + this$1.expect(types.comma); + if (this$1.afterTrailingComma(types.braceR)) { break } + } else { first = false; } + + var node$2 = this$1.startNode(); + node$2.imported = this$1.parseIdent(true); + if (this$1.eatContextual("as")) { + node$2.local = this$1.parseIdent(); + } else { + this$1.checkUnreserved(node$2.imported); + node$2.local = node$2.imported; + } + this$1.checkLVal(node$2.local, "let"); + nodes.push(this$1.finishNode(node$2, "ImportSpecifier")); + } + return nodes +}; + +// Set `ExpressionStatement#directive` property for directive prologues. +pp$1.adaptDirectivePrologue = function(statements) { + for (var i = 0; i < statements.length && this.isDirectiveCandidate(statements[i]); ++i) { + statements[i].directive = statements[i].expression.raw.slice(1, -1); + } +}; +pp$1.isDirectiveCandidate = function(statement) { + return ( + statement.type === "ExpressionStatement" && + statement.expression.type === "Literal" && + typeof statement.expression.value === "string" && + // Reject parenthesized strings. + (this.input[statement.start] === "\"" || this.input[statement.start] === "'") + ) +}; + +var pp$2 = Parser.prototype; + +// Convert existing expression atom to assignable pattern +// if possible. + +pp$2.toAssignable = function(node, isBinding, refDestructuringErrors) { + var this$1 = this; + + if (this.options.ecmaVersion >= 6 && node) { + switch (node.type) { + case "Identifier": + if (this.inAsync && node.name === "await") + { this.raise(node.start, "Can not use 'await' as identifier inside an async function"); } + break + + case "ObjectPattern": + case "ArrayPattern": + case "RestElement": + break + + case "ObjectExpression": + node.type = "ObjectPattern"; + if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } + for (var i = 0, list = node.properties; i < list.length; i += 1) { + var prop = list[i]; + + this$1.toAssignable(prop, isBinding); + // Early error: + // AssignmentRestProperty[Yield, Await] : + // `...` DestructuringAssignmentTarget[Yield, Await] + // + // It is a Syntax Error if |DestructuringAssignmentTarget| is an |ArrayLiteral| or an |ObjectLiteral|. + if ( + prop.type === "RestElement" && + (prop.argument.type === "ArrayPattern" || prop.argument.type === "ObjectPattern") + ) { + this$1.raise(prop.argument.start, "Unexpected token"); + } + } + break + + case "Property": + // AssignmentProperty has type == "Property" + if (node.kind !== "init") { this.raise(node.key.start, "Object pattern can't contain getter or setter"); } + this.toAssignable(node.value, isBinding); + break + + case "ArrayExpression": + node.type = "ArrayPattern"; + if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } + this.toAssignableList(node.elements, isBinding); + break + + case "SpreadElement": + node.type = "RestElement"; + this.toAssignable(node.argument, isBinding); + if (node.argument.type === "AssignmentPattern") + { this.raise(node.argument.start, "Rest elements cannot have a default value"); } + break + + case "AssignmentExpression": + if (node.operator !== "=") { this.raise(node.left.end, "Only '=' operator can be used for specifying default value."); } + node.type = "AssignmentPattern"; + delete node.operator; + this.toAssignable(node.left, isBinding); + // falls through to AssignmentPattern + + case "AssignmentPattern": + break + + case "ParenthesizedExpression": + this.toAssignable(node.expression, isBinding); + break + + case "MemberExpression": + if (!isBinding) { break } + + default: + this.raise(node.start, "Assigning to rvalue"); + } + } else if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } + return node +}; + +// Convert list of expression atoms to binding list. + +pp$2.toAssignableList = function(exprList, isBinding) { + var this$1 = this; + + var end = exprList.length; + for (var i = 0; i < end; i++) { + var elt = exprList[i]; + if (elt) { this$1.toAssignable(elt, isBinding); } + } + if (end) { + var last = exprList[end - 1]; + if (this.options.ecmaVersion === 6 && isBinding && last && last.type === "RestElement" && last.argument.type !== "Identifier") + { this.unexpected(last.argument.start); } + } + return exprList +}; + +// Parses spread element. + +pp$2.parseSpread = function(refDestructuringErrors) { + var node = this.startNode(); + this.next(); + node.argument = this.parseMaybeAssign(false, refDestructuringErrors); + return this.finishNode(node, "SpreadElement") +}; + +pp$2.parseRestBinding = function() { + var node = this.startNode(); + this.next(); + + // RestElement inside of a function parameter must be an identifier + if (this.options.ecmaVersion === 6 && this.type !== types.name) + { this.unexpected(); } + + node.argument = this.parseBindingAtom(); + + return this.finishNode(node, "RestElement") +}; + +// Parses lvalue (assignable) atom. + +pp$2.parseBindingAtom = function() { + if (this.options.ecmaVersion >= 6) { + switch (this.type) { + case types.bracketL: + var node = this.startNode(); + this.next(); + node.elements = this.parseBindingList(types.bracketR, true, true); + return this.finishNode(node, "ArrayPattern") + + case types.braceL: + return this.parseObj(true) + } + } + return this.parseIdent() +}; + +pp$2.parseBindingList = function(close, allowEmpty, allowTrailingComma) { + var this$1 = this; + + var elts = [], first = true; + while (!this.eat(close)) { + if (first) { first = false; } + else { this$1.expect(types.comma); } + if (allowEmpty && this$1.type === types.comma) { + elts.push(null); + } else if (allowTrailingComma && this$1.afterTrailingComma(close)) { + break + } else if (this$1.type === types.ellipsis) { + var rest = this$1.parseRestBinding(); + this$1.parseBindingListItem(rest); + elts.push(rest); + if (this$1.type === types.comma) { this$1.raise(this$1.start, "Comma is not permitted after the rest element"); } + this$1.expect(close); + break + } else { + var elem = this$1.parseMaybeDefault(this$1.start, this$1.startLoc); + this$1.parseBindingListItem(elem); + elts.push(elem); + } + } + return elts +}; + +pp$2.parseBindingListItem = function(param) { + return param +}; + +// Parses assignment pattern around given atom if possible. + +pp$2.parseMaybeDefault = function(startPos, startLoc, left) { + left = left || this.parseBindingAtom(); + if (this.options.ecmaVersion < 6 || !this.eat(types.eq)) { return left } + var node = this.startNodeAt(startPos, startLoc); + node.left = left; + node.right = this.parseMaybeAssign(); + return this.finishNode(node, "AssignmentPattern") +}; + +// Verify that a node is an lval — something that can be assigned +// to. +// bindingType can be either: +// 'var' indicating that the lval creates a 'var' binding +// 'let' indicating that the lval creates a lexical ('let' or 'const') binding +// 'none' indicating that the binding should be checked for illegal identifiers, but not for duplicate references + +pp$2.checkLVal = function(expr, bindingType, checkClashes) { + var this$1 = this; + + switch (expr.type) { + case "Identifier": + if (this.strict && this.reservedWordsStrictBind.test(expr.name)) + { this.raiseRecoverable(expr.start, (bindingType ? "Binding " : "Assigning to ") + expr.name + " in strict mode"); } + if (checkClashes) { + if (has(checkClashes, expr.name)) + { this.raiseRecoverable(expr.start, "Argument name clash"); } + checkClashes[expr.name] = true; + } + if (bindingType && bindingType !== "none") { + if ( + bindingType === "var" && !this.canDeclareVarName(expr.name) || + bindingType !== "var" && !this.canDeclareLexicalName(expr.name) + ) { + this.raiseRecoverable(expr.start, ("Identifier '" + (expr.name) + "' has already been declared")); + } + if (bindingType === "var") { + this.declareVarName(expr.name); + } else { + this.declareLexicalName(expr.name); + } + } + break + + case "MemberExpression": + if (bindingType) { this.raiseRecoverable(expr.start, "Binding member expression"); } + break + + case "ObjectPattern": + for (var i = 0, list = expr.properties; i < list.length; i += 1) + { + var prop = list[i]; + + this$1.checkLVal(prop, bindingType, checkClashes); + } + break + + case "Property": + // AssignmentProperty has type == "Property" + this.checkLVal(expr.value, bindingType, checkClashes); + break + + case "ArrayPattern": + for (var i$1 = 0, list$1 = expr.elements; i$1 < list$1.length; i$1 += 1) { + var elem = list$1[i$1]; + + if (elem) { this$1.checkLVal(elem, bindingType, checkClashes); } + } + break + + case "AssignmentPattern": + this.checkLVal(expr.left, bindingType, checkClashes); + break + + case "RestElement": + this.checkLVal(expr.argument, bindingType, checkClashes); + break + + case "ParenthesizedExpression": + this.checkLVal(expr.expression, bindingType, checkClashes); + break + + default: + this.raise(expr.start, (bindingType ? "Binding" : "Assigning to") + " rvalue"); + } +}; + +// A recursive descent parser operates by defining functions for all +// syntactic elements, and recursively calling those, each function +// advancing the input stream and returning an AST node. Precedence +// of constructs (for example, the fact that `!x[1]` means `!(x[1])` +// instead of `(!x)[1]` is handled by the fact that the parser +// function that parses unary prefix operators is called first, and +// in turn calls the function that parses `[]` subscripts — that +// way, it'll receive the node for `x[1]` already parsed, and wraps +// *that* in the unary operator node. +// +// Acorn uses an [operator precedence parser][opp] to handle binary +// operator precedence, because it is much more compact than using +// the technique outlined above, which uses different, nesting +// functions to specify precedence, for all of the ten binary +// precedence levels that JavaScript defines. +// +// [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser + +var pp$3 = Parser.prototype; + +// Check if property name clashes with already added. +// Object/class getters and setters are not allowed to clash — +// either with each other or with an init property — and in +// strict mode, init properties are also not allowed to be repeated. + +pp$3.checkPropClash = function(prop, propHash, refDestructuringErrors) { + if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement") + { return } + if (this.options.ecmaVersion >= 6 && (prop.computed || prop.method || prop.shorthand)) + { return } + var key = prop.key; + var name; + switch (key.type) { + case "Identifier": name = key.name; break + case "Literal": name = String(key.value); break + default: return + } + var kind = prop.kind; + if (this.options.ecmaVersion >= 6) { + if (name === "__proto__" && kind === "init") { + if (propHash.proto) { + if (refDestructuringErrors && refDestructuringErrors.doubleProto < 0) { refDestructuringErrors.doubleProto = key.start; } + // Backwards-compat kludge. Can be removed in version 6.0 + else { this.raiseRecoverable(key.start, "Redefinition of __proto__ property"); } + } + propHash.proto = true; + } + return + } + name = "$" + name; + var other = propHash[name]; + if (other) { + var redefinition; + if (kind === "init") { + redefinition = this.strict && other.init || other.get || other.set; + } else { + redefinition = other.init || other[kind]; + } + if (redefinition) + { this.raiseRecoverable(key.start, "Redefinition of property"); } + } else { + other = propHash[name] = { + init: false, + get: false, + set: false + }; + } + other[kind] = true; +}; + +// ### Expression parsing + +// These nest, from the most general expression type at the top to +// 'atomic', nondivisible expression types at the bottom. Most of +// the functions will simply let the function(s) below them parse, +// and, *if* the syntactic construct they handle is present, wrap +// the AST node that the inner parser gave them in another node. + +// Parse a full expression. The optional arguments are used to +// forbid the `in` operator (in for loops initalization expressions) +// and provide reference for storing '=' operator inside shorthand +// property assignment in contexts where both object expression +// and object pattern might appear (so it's possible to raise +// delayed syntax error at correct position). + +pp$3.parseExpression = function(noIn, refDestructuringErrors) { + var this$1 = this; + + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseMaybeAssign(noIn, refDestructuringErrors); + if (this.type === types.comma) { + var node = this.startNodeAt(startPos, startLoc); + node.expressions = [expr]; + while (this.eat(types.comma)) { node.expressions.push(this$1.parseMaybeAssign(noIn, refDestructuringErrors)); } + return this.finishNode(node, "SequenceExpression") + } + return expr +}; + +// Parse an assignment expression. This includes applications of +// operators like `+=`. + +pp$3.parseMaybeAssign = function(noIn, refDestructuringErrors, afterLeftParse) { + if (this.inGenerator && this.isContextual("yield")) { return this.parseYield() } + + var ownDestructuringErrors = false, oldParenAssign = -1, oldTrailingComma = -1; + if (refDestructuringErrors) { + oldParenAssign = refDestructuringErrors.parenthesizedAssign; + oldTrailingComma = refDestructuringErrors.trailingComma; + refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = -1; + } else { + refDestructuringErrors = new DestructuringErrors; + ownDestructuringErrors = true; + } + + var startPos = this.start, startLoc = this.startLoc; + if (this.type == types.parenL || this.type == types.name) + { this.potentialArrowAt = this.start; } + var left = this.parseMaybeConditional(noIn, refDestructuringErrors); + if (afterLeftParse) { left = afterLeftParse.call(this, left, startPos, startLoc); } + if (this.type.isAssign) { + var node = this.startNodeAt(startPos, startLoc); + node.operator = this.value; + node.left = this.type === types.eq ? this.toAssignable(left, false, refDestructuringErrors) : left; + if (!ownDestructuringErrors) { DestructuringErrors.call(refDestructuringErrors); } + refDestructuringErrors.shorthandAssign = -1; // reset because shorthand default was used correctly + this.checkLVal(left); + this.next(); + node.right = this.parseMaybeAssign(noIn); + return this.finishNode(node, "AssignmentExpression") + } else { + if (ownDestructuringErrors) { this.checkExpressionErrors(refDestructuringErrors, true); } + } + if (oldParenAssign > -1) { refDestructuringErrors.parenthesizedAssign = oldParenAssign; } + if (oldTrailingComma > -1) { refDestructuringErrors.trailingComma = oldTrailingComma; } + return left +}; + +// Parse a ternary conditional (`?:`) operator. + +pp$3.parseMaybeConditional = function(noIn, refDestructuringErrors) { + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseExprOps(noIn, refDestructuringErrors); + if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } + if (this.eat(types.question)) { + var node = this.startNodeAt(startPos, startLoc); + node.test = expr; + node.consequent = this.parseMaybeAssign(); + this.expect(types.colon); + node.alternate = this.parseMaybeAssign(noIn); + return this.finishNode(node, "ConditionalExpression") + } + return expr +}; + +// Start the precedence parser. + +pp$3.parseExprOps = function(noIn, refDestructuringErrors) { + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseMaybeUnary(refDestructuringErrors, false); + if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } + return expr.start == startPos && expr.type === "ArrowFunctionExpression" ? expr : this.parseExprOp(expr, startPos, startLoc, -1, noIn) +}; + +// Parse binary operators with the operator precedence parsing +// algorithm. `left` is the left-hand side of the operator. +// `minPrec` provides context that allows the function to stop and +// defer further parser to one of its callers when it encounters an +// operator that has a lower precedence than the set it is parsing. + +pp$3.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) { + var prec = this.type.binop; + if (prec != null && (!noIn || this.type !== types._in)) { + if (prec > minPrec) { + var logical = this.type === types.logicalOR || this.type === types.logicalAND; + var op = this.value; + this.next(); + var startPos = this.start, startLoc = this.startLoc; + var right = this.parseExprOp(this.parseMaybeUnary(null, false), startPos, startLoc, prec, noIn); + var node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical); + return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn) + } + } + return left +}; + +pp$3.buildBinary = function(startPos, startLoc, left, right, op, logical) { + var node = this.startNodeAt(startPos, startLoc); + node.left = left; + node.operator = op; + node.right = right; + return this.finishNode(node, logical ? "LogicalExpression" : "BinaryExpression") +}; + +// Parse unary operators, both prefix and postfix. + +pp$3.parseMaybeUnary = function(refDestructuringErrors, sawUnary) { + var this$1 = this; + + var startPos = this.start, startLoc = this.startLoc, expr; + if (this.inAsync && this.isContextual("await")) { + expr = this.parseAwait(); + sawUnary = true; + } else if (this.type.prefix) { + var node = this.startNode(), update = this.type === types.incDec; + node.operator = this.value; + node.prefix = true; + this.next(); + node.argument = this.parseMaybeUnary(null, true); + this.checkExpressionErrors(refDestructuringErrors, true); + if (update) { this.checkLVal(node.argument); } + else if (this.strict && node.operator === "delete" && + node.argument.type === "Identifier") + { this.raiseRecoverable(node.start, "Deleting local variable in strict mode"); } + else { sawUnary = true; } + expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); + } else { + expr = this.parseExprSubscripts(refDestructuringErrors); + if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } + while (this.type.postfix && !this.canInsertSemicolon()) { + var node$1 = this$1.startNodeAt(startPos, startLoc); + node$1.operator = this$1.value; + node$1.prefix = false; + node$1.argument = expr; + this$1.checkLVal(expr); + this$1.next(); + expr = this$1.finishNode(node$1, "UpdateExpression"); + } + } + + if (!sawUnary && this.eat(types.starstar)) + { return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false), "**", false) } + else + { return expr } +}; + +// Parse call, dot, and `[]`-subscript expressions. + +pp$3.parseExprSubscripts = function(refDestructuringErrors) { + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseExprAtom(refDestructuringErrors); + var skipArrowSubscripts = expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")"; + if (this.checkExpressionErrors(refDestructuringErrors) || skipArrowSubscripts) { return expr } + var result = this.parseSubscripts(expr, startPos, startLoc); + if (refDestructuringErrors && result.type === "MemberExpression") { + if (refDestructuringErrors.parenthesizedAssign >= result.start) { refDestructuringErrors.parenthesizedAssign = -1; } + if (refDestructuringErrors.parenthesizedBind >= result.start) { refDestructuringErrors.parenthesizedBind = -1; } + } + return result +}; + +pp$3.parseSubscripts = function(base, startPos, startLoc, noCalls) { + var this$1 = this; + + var maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" && + this.lastTokEnd == base.end && !this.canInsertSemicolon() && this.input.slice(base.start, base.end) === "async"; + for (var computed = (void 0);;) { + if ((computed = this$1.eat(types.bracketL)) || this$1.eat(types.dot)) { + var node = this$1.startNodeAt(startPos, startLoc); + node.object = base; + node.property = computed ? this$1.parseExpression() : this$1.parseIdent(true); + node.computed = !!computed; + if (computed) { this$1.expect(types.bracketR); } + base = this$1.finishNode(node, "MemberExpression"); + } else if (!noCalls && this$1.eat(types.parenL)) { + var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this$1.yieldPos, oldAwaitPos = this$1.awaitPos; + this$1.yieldPos = 0; + this$1.awaitPos = 0; + var exprList = this$1.parseExprList(types.parenR, this$1.options.ecmaVersion >= 8, false, refDestructuringErrors); + if (maybeAsyncArrow && !this$1.canInsertSemicolon() && this$1.eat(types.arrow)) { + this$1.checkPatternErrors(refDestructuringErrors, false); + this$1.checkYieldAwaitInDefaultParams(); + this$1.yieldPos = oldYieldPos; + this$1.awaitPos = oldAwaitPos; + return this$1.parseArrowExpression(this$1.startNodeAt(startPos, startLoc), exprList, true) + } + this$1.checkExpressionErrors(refDestructuringErrors, true); + this$1.yieldPos = oldYieldPos || this$1.yieldPos; + this$1.awaitPos = oldAwaitPos || this$1.awaitPos; + var node$1 = this$1.startNodeAt(startPos, startLoc); + node$1.callee = base; + node$1.arguments = exprList; + base = this$1.finishNode(node$1, "CallExpression"); + } else if (this$1.type === types.backQuote) { + var node$2 = this$1.startNodeAt(startPos, startLoc); + node$2.tag = base; + node$2.quasi = this$1.parseTemplate({isTagged: true}); + base = this$1.finishNode(node$2, "TaggedTemplateExpression"); + } else { + return base + } + } +}; + +// Parse an atomic expression — either a single token that is an +// expression, an expression started by a keyword like `function` or +// `new`, or an expression wrapped in punctuation like `()`, `[]`, +// or `{}`. + +pp$3.parseExprAtom = function(refDestructuringErrors) { + var node, canBeArrow = this.potentialArrowAt == this.start; + switch (this.type) { + case types._super: + if (!this.inFunction) + { this.raise(this.start, "'super' outside of function or class"); } + node = this.startNode(); + this.next(); + // The `super` keyword can appear at below: + // SuperProperty: + // super [ Expression ] + // super . IdentifierName + // SuperCall: + // super Arguments + if (this.type !== types.dot && this.type !== types.bracketL && this.type !== types.parenL) + { this.unexpected(); } + return this.finishNode(node, "Super") + + case types._this: + node = this.startNode(); + this.next(); + return this.finishNode(node, "ThisExpression") + + case types.name: + var startPos = this.start, startLoc = this.startLoc, containsEsc = this.containsEsc; + var id = this.parseIdent(this.type !== types.name); + if (this.options.ecmaVersion >= 8 && !containsEsc && id.name === "async" && !this.canInsertSemicolon() && this.eat(types._function)) + { return this.parseFunction(this.startNodeAt(startPos, startLoc), false, false, true) } + if (canBeArrow && !this.canInsertSemicolon()) { + if (this.eat(types.arrow)) + { return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], false) } + if (this.options.ecmaVersion >= 8 && id.name === "async" && this.type === types.name && !containsEsc) { + id = this.parseIdent(); + if (this.canInsertSemicolon() || !this.eat(types.arrow)) + { this.unexpected(); } + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], true) + } + } + return id + + case types.regexp: + var value = this.value; + node = this.parseLiteral(value.value); + node.regex = {pattern: value.pattern, flags: value.flags}; + return node + + case types.num: case types.string: + return this.parseLiteral(this.value) + + case types._null: case types._true: case types._false: + node = this.startNode(); + node.value = this.type === types._null ? null : this.type === types._true; + node.raw = this.type.keyword; + this.next(); + return this.finishNode(node, "Literal") + + case types.parenL: + var start = this.start, expr = this.parseParenAndDistinguishExpression(canBeArrow); + if (refDestructuringErrors) { + if (refDestructuringErrors.parenthesizedAssign < 0 && !this.isSimpleAssignTarget(expr)) + { refDestructuringErrors.parenthesizedAssign = start; } + if (refDestructuringErrors.parenthesizedBind < 0) + { refDestructuringErrors.parenthesizedBind = start; } + } + return expr + + case types.bracketL: + node = this.startNode(); + this.next(); + node.elements = this.parseExprList(types.bracketR, true, true, refDestructuringErrors); + return this.finishNode(node, "ArrayExpression") + + case types.braceL: + return this.parseObj(false, refDestructuringErrors) + + case types._function: + node = this.startNode(); + this.next(); + return this.parseFunction(node, false) + + case types._class: + return this.parseClass(this.startNode(), false) + + case types._new: + return this.parseNew() + + case types.backQuote: + return this.parseTemplate() + + default: + this.unexpected(); + } +}; + +pp$3.parseLiteral = function(value) { + var node = this.startNode(); + node.value = value; + node.raw = this.input.slice(this.start, this.end); + this.next(); + return this.finishNode(node, "Literal") +}; + +pp$3.parseParenExpression = function() { + this.expect(types.parenL); + var val = this.parseExpression(); + this.expect(types.parenR); + return val +}; + +pp$3.parseParenAndDistinguishExpression = function(canBeArrow) { + var this$1 = this; + + var startPos = this.start, startLoc = this.startLoc, val, allowTrailingComma = this.options.ecmaVersion >= 8; + if (this.options.ecmaVersion >= 6) { + this.next(); + + var innerStartPos = this.start, innerStartLoc = this.startLoc; + var exprList = [], first = true, lastIsComma = false; + var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, spreadStart; + this.yieldPos = 0; + this.awaitPos = 0; + while (this.type !== types.parenR) { + first ? first = false : this$1.expect(types.comma); + if (allowTrailingComma && this$1.afterTrailingComma(types.parenR, true)) { + lastIsComma = true; + break + } else if (this$1.type === types.ellipsis) { + spreadStart = this$1.start; + exprList.push(this$1.parseParenItem(this$1.parseRestBinding())); + if (this$1.type === types.comma) { this$1.raise(this$1.start, "Comma is not permitted after the rest element"); } + break + } else { + exprList.push(this$1.parseMaybeAssign(false, refDestructuringErrors, this$1.parseParenItem)); + } + } + var innerEndPos = this.start, innerEndLoc = this.startLoc; + this.expect(types.parenR); + + if (canBeArrow && !this.canInsertSemicolon() && this.eat(types.arrow)) { + this.checkPatternErrors(refDestructuringErrors, false); + this.checkYieldAwaitInDefaultParams(); + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + return this.parseParenArrowList(startPos, startLoc, exprList) + } + + if (!exprList.length || lastIsComma) { this.unexpected(this.lastTokStart); } + if (spreadStart) { this.unexpected(spreadStart); } + this.checkExpressionErrors(refDestructuringErrors, true); + this.yieldPos = oldYieldPos || this.yieldPos; + this.awaitPos = oldAwaitPos || this.awaitPos; + + if (exprList.length > 1) { + val = this.startNodeAt(innerStartPos, innerStartLoc); + val.expressions = exprList; + this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc); + } else { + val = exprList[0]; + } + } else { + val = this.parseParenExpression(); + } + + if (this.options.preserveParens) { + var par = this.startNodeAt(startPos, startLoc); + par.expression = val; + return this.finishNode(par, "ParenthesizedExpression") + } else { + return val + } +}; + +pp$3.parseParenItem = function(item) { + return item +}; + +pp$3.parseParenArrowList = function(startPos, startLoc, exprList) { + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList) +}; + +// New's precedence is slightly tricky. It must allow its argument to +// be a `[]` or dot subscript expression, but not a call — at least, +// not without wrapping it in parentheses. Thus, it uses the noCalls +// argument to parseSubscripts to prevent it from consuming the +// argument list. + +var empty$1 = []; + +pp$3.parseNew = function() { + var node = this.startNode(); + var meta = this.parseIdent(true); + if (this.options.ecmaVersion >= 6 && this.eat(types.dot)) { + node.meta = meta; + var containsEsc = this.containsEsc; + node.property = this.parseIdent(true); + if (node.property.name !== "target" || containsEsc) + { this.raiseRecoverable(node.property.start, "The only valid meta property for new is new.target"); } + if (!this.inFunction) + { this.raiseRecoverable(node.start, "new.target can only be used in functions"); } + return this.finishNode(node, "MetaProperty") + } + var startPos = this.start, startLoc = this.startLoc; + node.callee = this.parseSubscripts(this.parseExprAtom(), startPos, startLoc, true); + if (this.eat(types.parenL)) { node.arguments = this.parseExprList(types.parenR, this.options.ecmaVersion >= 8, false); } + else { node.arguments = empty$1; } + return this.finishNode(node, "NewExpression") +}; + +// Parse template expression. + +pp$3.parseTemplateElement = function(ref) { + var isTagged = ref.isTagged; + + var elem = this.startNode(); + if (this.type === types.invalidTemplate) { + if (!isTagged) { + this.raiseRecoverable(this.start, "Bad escape sequence in untagged template literal"); + } + elem.value = { + raw: this.value, + cooked: null + }; + } else { + elem.value = { + raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"), + cooked: this.value + }; + } + this.next(); + elem.tail = this.type === types.backQuote; + return this.finishNode(elem, "TemplateElement") +}; + +pp$3.parseTemplate = function(ref) { + var this$1 = this; + if ( ref === void 0 ) ref = {}; + var isTagged = ref.isTagged; if ( isTagged === void 0 ) isTagged = false; + + var node = this.startNode(); + this.next(); + node.expressions = []; + var curElt = this.parseTemplateElement({isTagged: isTagged}); + node.quasis = [curElt]; + while (!curElt.tail) { + this$1.expect(types.dollarBraceL); + node.expressions.push(this$1.parseExpression()); + this$1.expect(types.braceR); + node.quasis.push(curElt = this$1.parseTemplateElement({isTagged: isTagged})); + } + this.next(); + return this.finishNode(node, "TemplateLiteral") +}; + +pp$3.isAsyncProp = function(prop) { + return !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" && + (this.type === types.name || this.type === types.num || this.type === types.string || this.type === types.bracketL || this.type.keyword || (this.options.ecmaVersion >= 9 && this.type === types.star)) && + !lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) +}; + +// Parse an object literal or binding pattern. + +pp$3.parseObj = function(isPattern, refDestructuringErrors) { + var this$1 = this; + + var node = this.startNode(), first = true, propHash = {}; + node.properties = []; + this.next(); + while (!this.eat(types.braceR)) { + if (!first) { + this$1.expect(types.comma); + if (this$1.afterTrailingComma(types.braceR)) { break } + } else { first = false; } + + var prop = this$1.parseProperty(isPattern, refDestructuringErrors); + if (!isPattern) { this$1.checkPropClash(prop, propHash, refDestructuringErrors); } + node.properties.push(prop); + } + return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression") +}; + +pp$3.parseProperty = function(isPattern, refDestructuringErrors) { + var prop = this.startNode(), isGenerator, isAsync, startPos, startLoc; + if (this.options.ecmaVersion >= 9 && this.eat(types.ellipsis)) { + if (isPattern) { + prop.argument = this.parseIdent(false); + if (this.type === types.comma) { + this.raise(this.start, "Comma is not permitted after the rest element"); + } + return this.finishNode(prop, "RestElement") + } + // To disallow parenthesized identifier via `this.toAssignable()`. + if (this.type === types.parenL && refDestructuringErrors) { + if (refDestructuringErrors.parenthesizedAssign < 0) { + refDestructuringErrors.parenthesizedAssign = this.start; + } + if (refDestructuringErrors.parenthesizedBind < 0) { + refDestructuringErrors.parenthesizedBind = this.start; + } + } + // Parse argument. + prop.argument = this.parseMaybeAssign(false, refDestructuringErrors); + // To disallow trailing comma via `this.toAssignable()`. + if (this.type === types.comma && refDestructuringErrors && refDestructuringErrors.trailingComma < 0) { + refDestructuringErrors.trailingComma = this.start; + } + // Finish + return this.finishNode(prop, "SpreadElement") + } + if (this.options.ecmaVersion >= 6) { + prop.method = false; + prop.shorthand = false; + if (isPattern || refDestructuringErrors) { + startPos = this.start; + startLoc = this.startLoc; + } + if (!isPattern) + { isGenerator = this.eat(types.star); } + } + var containsEsc = this.containsEsc; + this.parsePropertyName(prop); + if (!isPattern && !containsEsc && this.options.ecmaVersion >= 8 && !isGenerator && this.isAsyncProp(prop)) { + isAsync = true; + isGenerator = this.options.ecmaVersion >= 9 && this.eat(types.star); + this.parsePropertyName(prop, refDestructuringErrors); + } else { + isAsync = false; + } + this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc); + return this.finishNode(prop, "Property") +}; + +pp$3.parsePropertyValue = function(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) { + if ((isGenerator || isAsync) && this.type === types.colon) + { this.unexpected(); } + + if (this.eat(types.colon)) { + prop.value = isPattern ? this.parseMaybeDefault(this.start, this.startLoc) : this.parseMaybeAssign(false, refDestructuringErrors); + prop.kind = "init"; + } else if (this.options.ecmaVersion >= 6 && this.type === types.parenL) { + if (isPattern) { this.unexpected(); } + prop.kind = "init"; + prop.method = true; + prop.value = this.parseMethod(isGenerator, isAsync); + } else if (!isPattern && !containsEsc && + this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" && + (prop.key.name === "get" || prop.key.name === "set") && + (this.type != types.comma && this.type != types.braceR)) { + if (isGenerator || isAsync) { this.unexpected(); } + prop.kind = prop.key.name; + this.parsePropertyName(prop); + prop.value = this.parseMethod(false); + var paramCount = prop.kind === "get" ? 0 : 1; + if (prop.value.params.length !== paramCount) { + var start = prop.value.start; + if (prop.kind === "get") + { this.raiseRecoverable(start, "getter should have no params"); } + else + { this.raiseRecoverable(start, "setter should have exactly one param"); } + } else { + if (prop.kind === "set" && prop.value.params[0].type === "RestElement") + { this.raiseRecoverable(prop.value.params[0].start, "Setter cannot use rest params"); } + } + } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") { + this.checkUnreserved(prop.key); + prop.kind = "init"; + if (isPattern) { + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key); + } else if (this.type === types.eq && refDestructuringErrors) { + if (refDestructuringErrors.shorthandAssign < 0) + { refDestructuringErrors.shorthandAssign = this.start; } + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key); + } else { + prop.value = prop.key; + } + prop.shorthand = true; + } else { this.unexpected(); } +}; + +pp$3.parsePropertyName = function(prop) { + if (this.options.ecmaVersion >= 6) { + if (this.eat(types.bracketL)) { + prop.computed = true; + prop.key = this.parseMaybeAssign(); + this.expect(types.bracketR); + return prop.key + } else { + prop.computed = false; + } + } + return prop.key = this.type === types.num || this.type === types.string ? this.parseExprAtom() : this.parseIdent(true) +}; + +// Initialize empty function node. + +pp$3.initFunction = function(node) { + node.id = null; + if (this.options.ecmaVersion >= 6) { + node.generator = false; + node.expression = false; + } + if (this.options.ecmaVersion >= 8) + { node.async = false; } +}; + +// Parse object or class method. + +pp$3.parseMethod = function(isGenerator, isAsync) { + var node = this.startNode(), oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction; + + this.initFunction(node); + if (this.options.ecmaVersion >= 6) + { node.generator = isGenerator; } + if (this.options.ecmaVersion >= 8) + { node.async = !!isAsync; } + + this.inGenerator = node.generator; + this.inAsync = node.async; + this.yieldPos = 0; + this.awaitPos = 0; + this.inFunction = true; + this.enterFunctionScope(); + + this.expect(types.parenL); + node.params = this.parseBindingList(types.parenR, false, this.options.ecmaVersion >= 8); + this.checkYieldAwaitInDefaultParams(); + this.parseFunctionBody(node, false); + + this.inGenerator = oldInGen; + this.inAsync = oldInAsync; + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + this.inFunction = oldInFunc; + return this.finishNode(node, "FunctionExpression") +}; + +// Parse arrow function expression with given parameters. + +pp$3.parseArrowExpression = function(node, params, isAsync) { + var oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction; + + this.enterFunctionScope(); + this.initFunction(node); + if (this.options.ecmaVersion >= 8) + { node.async = !!isAsync; } + + this.inGenerator = false; + this.inAsync = node.async; + this.yieldPos = 0; + this.awaitPos = 0; + this.inFunction = true; + + node.params = this.toAssignableList(params, true); + this.parseFunctionBody(node, true); + + this.inGenerator = oldInGen; + this.inAsync = oldInAsync; + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + this.inFunction = oldInFunc; + return this.finishNode(node, "ArrowFunctionExpression") +}; + +// Parse function body and check parameters. + +pp$3.parseFunctionBody = function(node, isArrowFunction) { + var isExpression = isArrowFunction && this.type !== types.braceL; + var oldStrict = this.strict, useStrict = false; + + if (isExpression) { + node.body = this.parseMaybeAssign(); + node.expression = true; + this.checkParams(node, false); + } else { + var nonSimple = this.options.ecmaVersion >= 7 && !this.isSimpleParamList(node.params); + if (!oldStrict || nonSimple) { + useStrict = this.strictDirective(this.end); + // If this is a strict mode function, verify that argument names + // are not repeated, and it does not try to bind the words `eval` + // or `arguments`. + if (useStrict && nonSimple) + { this.raiseRecoverable(node.start, "Illegal 'use strict' directive in function with non-simple parameter list"); } + } + // Start a new scope with regard to labels and the `inFunction` + // flag (restore them to their old value afterwards). + var oldLabels = this.labels; + this.labels = []; + if (useStrict) { this.strict = true; } + + // Add the params to varDeclaredNames to ensure that an error is thrown + // if a let/const declaration in the function clashes with one of the params. + this.checkParams(node, !oldStrict && !useStrict && !isArrowFunction && this.isSimpleParamList(node.params)); + node.body = this.parseBlock(false); + node.expression = false; + this.adaptDirectivePrologue(node.body.body); + this.labels = oldLabels; + } + this.exitFunctionScope(); + + if (this.strict && node.id) { + // Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval' + this.checkLVal(node.id, "none"); + } + this.strict = oldStrict; +}; + +pp$3.isSimpleParamList = function(params) { + for (var i = 0, list = params; i < list.length; i += 1) + { + var param = list[i]; + + if (param.type !== "Identifier") { return false + } } + return true +}; + +// Checks function params for various disallowed patterns such as using "eval" +// or "arguments" and duplicate parameters. + +pp$3.checkParams = function(node, allowDuplicates) { + var this$1 = this; + + var nameHash = {}; + for (var i = 0, list = node.params; i < list.length; i += 1) + { + var param = list[i]; + + this$1.checkLVal(param, "var", allowDuplicates ? null : nameHash); + } +}; + +// Parses a comma-separated list of expressions, and returns them as +// an array. `close` is the token type that ends the list, and +// `allowEmpty` can be turned on to allow subsequent commas with +// nothing in between them to be parsed as `null` (which is needed +// for array literals). + +pp$3.parseExprList = function(close, allowTrailingComma, allowEmpty, refDestructuringErrors) { + var this$1 = this; + + var elts = [], first = true; + while (!this.eat(close)) { + if (!first) { + this$1.expect(types.comma); + if (allowTrailingComma && this$1.afterTrailingComma(close)) { break } + } else { first = false; } + + var elt = (void 0); + if (allowEmpty && this$1.type === types.comma) + { elt = null; } + else if (this$1.type === types.ellipsis) { + elt = this$1.parseSpread(refDestructuringErrors); + if (refDestructuringErrors && this$1.type === types.comma && refDestructuringErrors.trailingComma < 0) + { refDestructuringErrors.trailingComma = this$1.start; } + } else { + elt = this$1.parseMaybeAssign(false, refDestructuringErrors); + } + elts.push(elt); + } + return elts +}; + +pp$3.checkUnreserved = function(ref) { + var start = ref.start; + var end = ref.end; + var name = ref.name; + + if (this.inGenerator && name === "yield") + { this.raiseRecoverable(start, "Can not use 'yield' as identifier inside a generator"); } + if (this.inAsync && name === "await") + { this.raiseRecoverable(start, "Can not use 'await' as identifier inside an async function"); } + if (this.isKeyword(name)) + { this.raise(start, ("Unexpected keyword '" + name + "'")); } + if (this.options.ecmaVersion < 6 && + this.input.slice(start, end).indexOf("\\") != -1) { return } + var re = this.strict ? this.reservedWordsStrict : this.reservedWords; + if (re.test(name)) { + if (!this.inAsync && name === "await") + { this.raiseRecoverable(start, "Can not use keyword 'await' outside an async function"); } + this.raiseRecoverable(start, ("The keyword '" + name + "' is reserved")); + } +}; + +// Parse the next token as an identifier. If `liberal` is true (used +// when parsing properties), it will also convert keywords into +// identifiers. + +pp$3.parseIdent = function(liberal, isBinding) { + var node = this.startNode(); + if (liberal && this.options.allowReserved == "never") { liberal = false; } + if (this.type === types.name) { + node.name = this.value; + } else if (this.type.keyword) { + node.name = this.type.keyword; + + // To fix https://github.com/acornjs/acorn/issues/575 + // `class` and `function` keywords push new context into this.context. + // But there is no chance to pop the context if the keyword is consumed as an identifier such as a property name. + // If the previous token is a dot, this does not apply because the context-managing code already ignored the keyword + if ((node.name === "class" || node.name === "function") && + (this.lastTokEnd !== this.lastTokStart + 1 || this.input.charCodeAt(this.lastTokStart) !== 46)) { + this.context.pop(); + } + } else { + this.unexpected(); + } + this.next(); + this.finishNode(node, "Identifier"); + if (!liberal) { this.checkUnreserved(node); } + return node +}; + +// Parses yield expression inside generator. + +pp$3.parseYield = function() { + if (!this.yieldPos) { this.yieldPos = this.start; } + + var node = this.startNode(); + this.next(); + if (this.type == types.semi || this.canInsertSemicolon() || (this.type != types.star && !this.type.startsExpr)) { + node.delegate = false; + node.argument = null; + } else { + node.delegate = this.eat(types.star); + node.argument = this.parseMaybeAssign(); + } + return this.finishNode(node, "YieldExpression") +}; + +pp$3.parseAwait = function() { + if (!this.awaitPos) { this.awaitPos = this.start; } + + var node = this.startNode(); + this.next(); + node.argument = this.parseMaybeUnary(null, true); + return this.finishNode(node, "AwaitExpression") +}; + +var pp$4 = Parser.prototype; + +// This function is used to raise exceptions on parse errors. It +// takes an offset integer (into the current `input`) to indicate +// the location of the error, attaches the position to the end +// of the error message, and then raises a `SyntaxError` with that +// message. + +pp$4.raise = function(pos, message) { + var loc = getLineInfo(this.input, pos); + message += " (" + loc.line + ":" + loc.column + ")"; + var err = new SyntaxError(message); + err.pos = pos; err.loc = loc; err.raisedAt = this.pos; + throw err +}; + +pp$4.raiseRecoverable = pp$4.raise; + +pp$4.curPosition = function() { + if (this.options.locations) { + return new Position(this.curLine, this.pos - this.lineStart) + } +}; + +var pp$5 = Parser.prototype; + +// Object.assign polyfill +var assign = Object.assign || function(target) { + var sources = [], len = arguments.length - 1; + while ( len-- > 0 ) sources[ len ] = arguments[ len + 1 ]; + + for (var i = 0, list = sources; i < list.length; i += 1) { + var source = list[i]; + + for (var key in source) { + if (has(source, key)) { + target[key] = source[key]; + } + } + } + return target +}; + +// The functions in this module keep track of declared variables in the current scope in order to detect duplicate variable names. + +pp$5.enterFunctionScope = function() { + // var: a hash of var-declared names in the current lexical scope + // lexical: a hash of lexically-declared names in the current lexical scope + // childVar: a hash of var-declared names in all child lexical scopes of the current lexical scope (within the current function scope) + // parentLexical: a hash of lexically-declared names in all parent lexical scopes of the current lexical scope (within the current function scope) + this.scopeStack.push({var: {}, lexical: {}, childVar: {}, parentLexical: {}}); +}; + +pp$5.exitFunctionScope = function() { + this.scopeStack.pop(); +}; + +pp$5.enterLexicalScope = function() { + var parentScope = this.scopeStack[this.scopeStack.length - 1]; + var childScope = {var: {}, lexical: {}, childVar: {}, parentLexical: {}}; + + this.scopeStack.push(childScope); + assign(childScope.parentLexical, parentScope.lexical, parentScope.parentLexical); +}; + +pp$5.exitLexicalScope = function() { + var childScope = this.scopeStack.pop(); + var parentScope = this.scopeStack[this.scopeStack.length - 1]; + + assign(parentScope.childVar, childScope.var, childScope.childVar); +}; + +/** + * A name can be declared with `var` if there are no variables with the same name declared with `let`/`const` + * in the current lexical scope or any of the parent lexical scopes in this function. + */ +pp$5.canDeclareVarName = function(name) { + var currentScope = this.scopeStack[this.scopeStack.length - 1]; + + return !has(currentScope.lexical, name) && !has(currentScope.parentLexical, name) +}; + +/** + * A name can be declared with `let`/`const` if there are no variables with the same name declared with `let`/`const` + * in the current scope, and there are no variables with the same name declared with `var` in the current scope or in + * any child lexical scopes in this function. + */ +pp$5.canDeclareLexicalName = function(name) { + var currentScope = this.scopeStack[this.scopeStack.length - 1]; + + return !has(currentScope.lexical, name) && !has(currentScope.var, name) && !has(currentScope.childVar, name) +}; + +pp$5.declareVarName = function(name) { + this.scopeStack[this.scopeStack.length - 1].var[name] = true; +}; + +pp$5.declareLexicalName = function(name) { + this.scopeStack[this.scopeStack.length - 1].lexical[name] = true; +}; + +var Node = function Node(parser, pos, loc) { + this.type = ""; + this.start = pos; + this.end = 0; + if (parser.options.locations) + { this.loc = new SourceLocation(parser, loc); } + if (parser.options.directSourceFile) + { this.sourceFile = parser.options.directSourceFile; } + if (parser.options.ranges) + { this.range = [pos, 0]; } +}; + +// Start an AST node, attaching a start offset. + +var pp$6 = Parser.prototype; + +pp$6.startNode = function() { + return new Node(this, this.start, this.startLoc) +}; + +pp$6.startNodeAt = function(pos, loc) { + return new Node(this, pos, loc) +}; + +// Finish an AST node, adding `type` and `end` properties. + +function finishNodeAt(node, type, pos, loc) { + node.type = type; + node.end = pos; + if (this.options.locations) + { node.loc.end = loc; } + if (this.options.ranges) + { node.range[1] = pos; } + return node +} + +pp$6.finishNode = function(node, type) { + return finishNodeAt.call(this, node, type, this.lastTokEnd, this.lastTokEndLoc) +}; + +// Finish node at given position + +pp$6.finishNodeAt = function(node, type, pos, loc) { + return finishNodeAt.call(this, node, type, pos, loc) +}; + +// The algorithm used to determine whether a regexp can appear at a +// given point in the program is loosely based on sweet.js' approach. +// See https://github.com/mozilla/sweet.js/wiki/design + +var TokContext = function TokContext(token, isExpr, preserveSpace, override, generator) { + this.token = token; + this.isExpr = !!isExpr; + this.preserveSpace = !!preserveSpace; + this.override = override; + this.generator = !!generator; +}; + +var types$1 = { + b_stat: new TokContext("{", false), + b_expr: new TokContext("{", true), + b_tmpl: new TokContext("${", false), + p_stat: new TokContext("(", false), + p_expr: new TokContext("(", true), + q_tmpl: new TokContext("`", true, true, function (p) { return p.tryReadTemplateToken(); }), + f_stat: new TokContext("function", false), + f_expr: new TokContext("function", true), + f_expr_gen: new TokContext("function", true, false, null, true), + f_gen: new TokContext("function", false, false, null, true) +}; + +var pp$7 = Parser.prototype; + +pp$7.initialContext = function() { + return [types$1.b_stat] +}; + +pp$7.braceIsBlock = function(prevType) { + var parent = this.curContext(); + if (parent === types$1.f_expr || parent === types$1.f_stat) + { return true } + if (prevType === types.colon && (parent === types$1.b_stat || parent === types$1.b_expr)) + { return !parent.isExpr } + + // The check for `tt.name && exprAllowed` detects whether we are + // after a `yield` or `of` construct. See the `updateContext` for + // `tt.name`. + if (prevType === types._return || prevType == types.name && this.exprAllowed) + { return lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) } + if (prevType === types._else || prevType === types.semi || prevType === types.eof || prevType === types.parenR || prevType == types.arrow) + { return true } + if (prevType == types.braceL) + { return parent === types$1.b_stat } + if (prevType == types._var || prevType == types.name) + { return false } + return !this.exprAllowed +}; + +pp$7.inGeneratorContext = function() { + var this$1 = this; + + for (var i = this.context.length - 1; i >= 1; i--) { + var context = this$1.context[i]; + if (context.token === "function") + { return context.generator } + } + return false +}; + +pp$7.updateContext = function(prevType) { + var update, type = this.type; + if (type.keyword && prevType == types.dot) + { this.exprAllowed = false; } + else if (update = type.updateContext) + { update.call(this, prevType); } + else + { this.exprAllowed = type.beforeExpr; } +}; + +// Token-specific context update code + +types.parenR.updateContext = types.braceR.updateContext = function() { + if (this.context.length == 1) { + this.exprAllowed = true; + return + } + var out = this.context.pop(); + if (out === types$1.b_stat && this.curContext().token === "function") { + out = this.context.pop(); + } + this.exprAllowed = !out.isExpr; +}; + +types.braceL.updateContext = function(prevType) { + this.context.push(this.braceIsBlock(prevType) ? types$1.b_stat : types$1.b_expr); + this.exprAllowed = true; +}; + +types.dollarBraceL.updateContext = function() { + this.context.push(types$1.b_tmpl); + this.exprAllowed = true; +}; + +types.parenL.updateContext = function(prevType) { + var statementParens = prevType === types._if || prevType === types._for || prevType === types._with || prevType === types._while; + this.context.push(statementParens ? types$1.p_stat : types$1.p_expr); + this.exprAllowed = true; +}; + +types.incDec.updateContext = function() { + // tokExprAllowed stays unchanged +}; + +types._function.updateContext = types._class.updateContext = function(prevType) { + if (prevType.beforeExpr && prevType !== types.semi && prevType !== types._else && + !((prevType === types.colon || prevType === types.braceL) && this.curContext() === types$1.b_stat)) + { this.context.push(types$1.f_expr); } + else + { this.context.push(types$1.f_stat); } + this.exprAllowed = false; +}; + +types.backQuote.updateContext = function() { + if (this.curContext() === types$1.q_tmpl) + { this.context.pop(); } + else + { this.context.push(types$1.q_tmpl); } + this.exprAllowed = false; +}; + +types.star.updateContext = function(prevType) { + if (prevType == types._function) { + var index = this.context.length - 1; + if (this.context[index] === types$1.f_expr) + { this.context[index] = types$1.f_expr_gen; } + else + { this.context[index] = types$1.f_gen; } + } + this.exprAllowed = true; +}; + +types.name.updateContext = function(prevType) { + var allowed = false; + if (this.options.ecmaVersion >= 6) { + if (this.value == "of" && !this.exprAllowed || + this.value == "yield" && this.inGeneratorContext()) + { allowed = true; } + } + this.exprAllowed = allowed; +}; + +// Object type used to represent tokens. Note that normally, tokens +// simply exist as properties on the parser object. This is only +// used for the onToken callback and the external tokenizer. + +var Token = function Token(p) { + this.type = p.type; + this.value = p.value; + this.start = p.start; + this.end = p.end; + if (p.options.locations) + { this.loc = new SourceLocation(p, p.startLoc, p.endLoc); } + if (p.options.ranges) + { this.range = [p.start, p.end]; } +}; + +// ## Tokenizer + +var pp$8 = Parser.prototype; + +// Are we running under Rhino? +var isRhino = typeof Packages == "object" && Object.prototype.toString.call(Packages) == "[object JavaPackage]"; + +// Move to the next token + +pp$8.next = function() { + if (this.options.onToken) + { this.options.onToken(new Token(this)); } + + this.lastTokEnd = this.end; + this.lastTokStart = this.start; + this.lastTokEndLoc = this.endLoc; + this.lastTokStartLoc = this.startLoc; + this.nextToken(); +}; + +pp$8.getToken = function() { + this.next(); + return new Token(this) +}; + +// If we're in an ES6 environment, make parsers iterable +if (typeof Symbol !== "undefined") + { pp$8[Symbol.iterator] = function() { + var this$1 = this; + + return { + next: function () { + var token = this$1.getToken(); + return { + done: token.type === types.eof, + value: token + } + } + } + }; } + +// Toggle strict mode. Re-reads the next number or string to please +// pedantic tests (`"use strict"; 010;` should fail). + +pp$8.curContext = function() { + return this.context[this.context.length - 1] +}; + +// Read a single token, updating the parser object's token-related +// properties. + +pp$8.nextToken = function() { + var curContext = this.curContext(); + if (!curContext || !curContext.preserveSpace) { this.skipSpace(); } + + this.start = this.pos; + if (this.options.locations) { this.startLoc = this.curPosition(); } + if (this.pos >= this.input.length) { return this.finishToken(types.eof) } + + if (curContext.override) { return curContext.override(this) } + else { this.readToken(this.fullCharCodeAtPos()); } +}; + +pp$8.readToken = function(code) { + // Identifier or keyword. '\uXXXX' sequences are allowed in + // identifiers, so '\' also dispatches to that. + if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */) + { return this.readWord() } + + return this.getTokenFromCode(code) +}; + +pp$8.fullCharCodeAtPos = function() { + var code = this.input.charCodeAt(this.pos); + if (code <= 0xd7ff || code >= 0xe000) { return code } + var next = this.input.charCodeAt(this.pos + 1); + return (code << 10) + next - 0x35fdc00 +}; + +pp$8.skipBlockComment = function() { + var this$1 = this; + + var startLoc = this.options.onComment && this.curPosition(); + var start = this.pos, end = this.input.indexOf("*/", this.pos += 2); + if (end === -1) { this.raise(this.pos - 2, "Unterminated comment"); } + this.pos = end + 2; + if (this.options.locations) { + lineBreakG.lastIndex = start; + var match; + while ((match = lineBreakG.exec(this.input)) && match.index < this.pos) { + ++this$1.curLine; + this$1.lineStart = match.index + match[0].length; + } + } + if (this.options.onComment) + { this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos, + startLoc, this.curPosition()); } +}; + +pp$8.skipLineComment = function(startSkip) { + var this$1 = this; + + var start = this.pos; + var startLoc = this.options.onComment && this.curPosition(); + var ch = this.input.charCodeAt(this.pos += startSkip); + while (this.pos < this.input.length && !isNewLine(ch)) { + ch = this$1.input.charCodeAt(++this$1.pos); + } + if (this.options.onComment) + { this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos, + startLoc, this.curPosition()); } +}; + +// Called at the start of the parse and after every token. Skips +// whitespace and comments, and. + +pp$8.skipSpace = function() { + var this$1 = this; + + loop: while (this.pos < this.input.length) { + var ch = this$1.input.charCodeAt(this$1.pos); + switch (ch) { + case 32: case 160: // ' ' + ++this$1.pos; + break + case 13: + if (this$1.input.charCodeAt(this$1.pos + 1) === 10) { + ++this$1.pos; + } + case 10: case 8232: case 8233: + ++this$1.pos; + if (this$1.options.locations) { + ++this$1.curLine; + this$1.lineStart = this$1.pos; + } + break + case 47: // '/' + switch (this$1.input.charCodeAt(this$1.pos + 1)) { + case 42: // '*' + this$1.skipBlockComment(); + break + case 47: + this$1.skipLineComment(2); + break + default: + break loop + } + break + default: + if (ch > 8 && ch < 14 || ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) { + ++this$1.pos; + } else { + break loop + } + } + } +}; + +// Called at the end of every token. Sets `end`, `val`, and +// maintains `context` and `exprAllowed`, and skips the space after +// the token, so that the next one's `start` will point at the +// right position. + +pp$8.finishToken = function(type, val) { + this.end = this.pos; + if (this.options.locations) { this.endLoc = this.curPosition(); } + var prevType = this.type; + this.type = type; + this.value = val; + + this.updateContext(prevType); +}; + +// ### Token reading + +// This is the function that is called to fetch the next token. It +// is somewhat obscure, because it works in character codes rather +// than characters, and because operator parsing has been inlined +// into it. +// +// All in the name of speed. +// +pp$8.readToken_dot = function() { + var next = this.input.charCodeAt(this.pos + 1); + if (next >= 48 && next <= 57) { return this.readNumber(true) } + var next2 = this.input.charCodeAt(this.pos + 2); + if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.' + this.pos += 3; + return this.finishToken(types.ellipsis) + } else { + ++this.pos; + return this.finishToken(types.dot) + } +}; + +pp$8.readToken_slash = function() { // '/' + var next = this.input.charCodeAt(this.pos + 1); + if (this.exprAllowed) { ++this.pos; return this.readRegexp() } + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(types.slash, 1) +}; + +pp$8.readToken_mult_modulo_exp = function(code) { // '%*' + var next = this.input.charCodeAt(this.pos + 1); + var size = 1; + var tokentype = code === 42 ? types.star : types.modulo; + + // exponentiation operator ** and **= + if (this.options.ecmaVersion >= 7 && code == 42 && next === 42) { + ++size; + tokentype = types.starstar; + next = this.input.charCodeAt(this.pos + 2); + } + + if (next === 61) { return this.finishOp(types.assign, size + 1) } + return this.finishOp(tokentype, size) +}; + +pp$8.readToken_pipe_amp = function(code) { // '|&' + var next = this.input.charCodeAt(this.pos + 1); + if (next === code) { return this.finishOp(code === 124 ? types.logicalOR : types.logicalAND, 2) } + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(code === 124 ? types.bitwiseOR : types.bitwiseAND, 1) +}; + +pp$8.readToken_caret = function() { // '^' + var next = this.input.charCodeAt(this.pos + 1); + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(types.bitwiseXOR, 1) +}; + +pp$8.readToken_plus_min = function(code) { // '+-' + var next = this.input.charCodeAt(this.pos + 1); + if (next === code) { + if (next == 45 && !this.inModule && this.input.charCodeAt(this.pos + 2) == 62 && + (this.lastTokEnd === 0 || lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))) { + // A `-->` line comment + this.skipLineComment(3); + this.skipSpace(); + return this.nextToken() + } + return this.finishOp(types.incDec, 2) + } + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(types.plusMin, 1) +}; + +pp$8.readToken_lt_gt = function(code) { // '<>' + var next = this.input.charCodeAt(this.pos + 1); + var size = 1; + if (next === code) { + size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2; + if (this.input.charCodeAt(this.pos + size) === 61) { return this.finishOp(types.assign, size + 1) } + return this.finishOp(types.bitShift, size) + } + if (next == 33 && code == 60 && !this.inModule && this.input.charCodeAt(this.pos + 2) == 45 && + this.input.charCodeAt(this.pos + 3) == 45) { + // `",device.name); + device.getPorts(function(devicePorts, instantPorts) { + //console.log("getPorts <--",device.name); + if (instantPorts===false) shouldCallAgain = true; + if (devicePorts) { + devicePorts.forEach(function(port) { + var ignored = false; + if (Espruino.Config.SERIAL_IGNORE) + Espruino.Config.SERIAL_IGNORE.split("|").forEach(function(wildcard) { + var regexp = "^"+wildcard.replace(/\./g,"\\.").replace(/\*/g,".*")+"$"; + if (port.path.match(new RegExp(regexp))) + ignored = true; + }); + + if (!ignored) { + if (port.usb && port.usb[0]==0x0483 && port.usb[1]==0x5740) + port.description = "Espruino board"; + ports.push(port); + newPortToDevice[port.path] = device; + } + }); + } + responses++; + if (responses == devices.length) { + portToDevice = newPortToDevice; + ports.sort(function(a,b) { + if (a.unimportant && !b.unimportant) return 1; + if (b.unimportant && !a.unimportant) return -1; + return 0; + }); + callback(ports, shouldCallAgain); + } + }); + }); + }; + + var openSerial=function(serialPort, connectCallback, disconnectCallback) { + return openSerialInternal(serialPort, connectCallback, disconnectCallback, 2); + } + + var openSerialInternal=function(serialPort, connectCallback, disconnectCallback, attempts) { + /* If openSerial is called, we need to have called getPorts first + in order to figure out which one of the serial_ implementations + we must call into. */ + if (portToDevice === undefined) { + portToDevice = []; // stop recursive calls if something errors + return getPorts(function() { + openSerialInternal(serialPort, connectCallback, disconnectCallback, attempts); + }); + } + + if (!(serialPort in portToDevice)) { + if (serialPort.toLowerCase() in portToDevice) { + serialPort = serialPort.toLowerCase(); + } else { + if (attempts>0) { + console.log("Port "+JSON.stringify(serialPort)+" not found - checking ports again ("+attempts+" attempts left)"); + return getPorts(function() { + openSerialInternal(serialPort, connectCallback, disconnectCallback, attempts-1); + }); + } else { + console.error("Port "+JSON.stringify(serialPort)+" not found"); + return connectCallback(undefined); + } + } + } + + var portInfo = { port:serialPort }; + connectionInfo = undefined; + flowControlXOFF = false; + currentDevice = portToDevice[serialPort]; + currentDevice.open(serialPort, function(cInfo) { // CONNECT + if (!cInfo) { +// Espruino.Core.Notifications.error("Unable to connect"); + console.error("Unable to open device (connectionInfo="+cInfo+")"); + connectCallback(undefined); + } else { + connectionInfo = cInfo; + connectedPort = serialPort; + console.log("Connected", cInfo); + if (connectionInfo.portName) + portInfo.portName = connectionInfo.portName; + Espruino.callProcessor("connected", portInfo, function() { + connectCallback(cInfo); + }); + } + }, function(data) { // RECEIEVE DATA + if (!(data instanceof ArrayBuffer)) console.warn("Serial port implementation is not returning ArrayBuffers"); + if (Espruino.Config.SERIAL_FLOW_CONTROL) { + var u = new Uint8Array(data); + for (var i=0;i resume upload"); + flowControlXOFF = false; + } + if (u[i]==19) { // XOFF + console.log("XOFF received => pause upload"); + flowControlXOFF = true; + } + } + } + if (readListener) readListener(data); + }, function() { // DISCONNECT + currentDevice = undefined; + if (!connectionInfo) { + // we got a disconnect when we hadn't connected... + // Just call connectCallback(undefined), don't bother sending disconnect + connectCallback(undefined); + return; + } + connectionInfo = undefined; + if (writeTimeout!==undefined) + clearTimeout(writeTimeout); + writeTimeout = undefined; + writeData = []; + sendingBinary = false; + flowControlXOFF = false; + + Espruino.callProcessor("disconnected", portInfo, function() { + disconnectCallback(portInfo); + }); + }); + }; + + var str2ab=function(str) { + var buf=new ArrayBuffer(str.length); + var bufView=new Uint8Array(buf); + for (var i=0; i=256) { + console.warn("Attempted to send non-8 bit character - code "+ch); + ch = "?".charCodeAt(0); + } + bufView[i] = ch; + } + return buf; + }; + + var closeSerial=function() { + if (currentDevice) { + currentDevice.close(); + currentDevice = undefined; + } else + console.error("Close called, but serial port not open"); + }; + + var isConnected = function() { + return currentDevice!==undefined; + }; + + var writeSerialWorker = function(isStarting) { + writeTimeout = undefined; // we've been called + // check flow control + if (flowControlXOFF) { + /* flow control was enabled - bit hacky (we could use a callback) + but safe - just check again in a bit to see if we should send */ + writeTimeout = setTimeout(function() { + writeSerialWorker(); + }, 50); + return; + } + + // if we disconnected while sending, empty queue + if (currentDevice === undefined) { + if (writeData[0].callback) + writeData[0].callback(); + writeData.shift(); + if (writeData.length) setTimeout(function() { + writeSerialWorker(false); + }, 1); + return; + } + + if (writeData[0].data === "") { + if (writeData[0].showStatus) + Espruino.Core.Status.setStatus("Sent"); + if (writeData[0].callback) + writeData[0].callback(); + writeData.shift(); // remove this empty first element + if (!writeData.length) return; // anything left to do? + isStarting = true; + } + + if (isStarting) { + var blockSize = 512; + if (currentDevice.maxWriteLength) + blockSize = currentDevice.maxWriteLength; + /* if we're throttling our writes we want to send small + * blocks of data at once. We still limit the size of + * sent blocks to 512 because on Mac we seem to lose + * data otherwise (not on any other platforms!) */ + if (slowWrite) blockSize=19; + writeData[0].blockSize = blockSize; + + writeData[0].showStatus &= writeData[0].data.length>writeData[0].blockSize; + if (writeData[0].showStatus) { + Espruino.Core.Status.setStatus("Sending...", writeData[0].data.length); + console.log("---> "+JSON.stringify(writeData[0].data)); + } + } + + // Initial split use previous, or don't + var d = undefined; + var split = writeData[0].nextSplit || { start:0, end:writeData[0].data.length, delay:0 }; + // if we get something like Ctrl-C or `reset`, wait a bit for it to complete + if (!sendingBinary) { + function findSplitIdx(prev, substr, delay, reason) { + var match = writeData[0].data.match(substr); + // not found + if (match===null) return prev; + // or previous find was earlier in str + var end = match.index + match[0].length; + if (end > prev.end) return prev; + // found, and earlier + prev.start = match.index; + prev.end = end; + prev.delay = delay; + prev.match = match[0]; + prev.reason = reason; + return prev; + } + split = findSplitIdx(split, /\x03/, 250, "Ctrl-C"); // Ctrl-C + split = findSplitIdx(split, /reset\(\);\n/, 250, "reset()"); // Reset + split = findSplitIdx(split, /load\(\);\n/, 250, "load()"); // Load + split = findSplitIdx(split, /Modules.addCached\("[^\n]*"\);\n/, 250, "Modules.addCached"); // Adding a module + split = findSplitIdx(split, /\x10require\("Storage"\).write\([^\n]*\);\n/, 500, "Storage.write"); // Write chunk of data + } + // Otherwise split based on block size + if (!split.match || split.end >= writeData[0].blockSize) { + if (split.match) writeData[0].nextSplit = split; + split = { start:0, end:writeData[0].blockSize, delay:0 }; + } + if (split.match) console.log("Splitting for "+split.reason+", delay "+split.delay); + // Only send some of the data + if (writeData[0].data.length>split.end) { + if (slowWrite && split.delay==0) split.delay=50; + d = writeData[0].data.substr(0,split.end); + writeData[0].data = writeData[0].data.substr(split.end); + if (writeData[0].nextSplit) { + writeData[0].nextSplit.start -= split.end; + writeData[0].nextSplit.end -= split.end; + if (writeData[0].nextSplit.end<=0) + writeData[0].nextSplit = undefined; + } + } else { + d = writeData[0].data; + writeData[0].data = ""; + writeData[0].nextSplit = undefined; + } + // update status + if (writeData[0].showStatus) + Espruino.Core.Status.incrementProgress(d.length); + // actually write data + //console.log("Sending block "+JSON.stringify(d)+", wait "+split.delay+"ms"); + currentDevice.write(d, function() { + // Once written, start timeout + writeTimeout = setTimeout(function() { + writeSerialWorker(); + }, split.delay); + }); + } + + // Throttled serial write + var writeSerial = function(data, showStatus, callback) { + if (showStatus===undefined) showStatus=true; + + /* Queue our data to write. If there was previous data and no callback to + invoke on this data or the previous then just append data. This would happen + if typing in the terminal for example. */ + if (!callback && writeData.length && !writeData[writeData.length-1].callback) { + writeData[writeData.length-1].data += data; + } else { + writeData.push({data:data,callback:callback,showStatus:showStatus}); + /* if this is our first data, start sending now. Otherwise we're already + busy sending and will pull data off writeData when ready */ + if (writeData.length==1) + writeSerialWorker(true); + } + }; + + + // ---------------------------------------------------------- + Espruino.Core.Serial = { + "devices" : [], // List of devices that can provide a serial API + "init" : init, + "getPorts": getPorts, + "open": openSerial, + "isConnected": isConnected, + "startListening": startListening, + "write": writeSerial, + "close": closeSerial, + "isSlowWrite": function() { return slowWrite; }, + "setSlowWrite": function(isOn, force) { + if ((!force) && Espruino.Config.SERIAL_THROTTLE_SEND) { + console.log("ForceThrottle option is set - set Slow Write = true"); + isOn = true; + } else + console.log("Set Slow Write = "+isOn); + slowWrite = isOn; + }, + "setBinary": function(isOn) { + sendingBinary = isOn; + } + }; +})(); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + The plugin that actually writes code out to Espruino + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.Core.Config.add("RESET_BEFORE_SEND", { + section : "Communications", + name : "Reset before Send", + description : "Reset Espruino before sending code from the editor pane?", + type : "boolean", + defaultValue : true + }); + Espruino.Core.Config.add("STORE_LINE_NUMBERS", { + section : "Communications", + name : "Store line numbers", + description : "Should Espruino store line numbers for each function? This uses one extra variable per function, but allows you to get source code debugging in the Web IDE", + type : "boolean", + defaultValue : true + }); + + } + + function writeToEspruino(code, callback) { + /* hack around non-K&R code formatting that would have + broken Espruino CLI's bracket counting */ + code = reformatCode(code); + if (code === undefined) return; // it should already have errored + + // We want to make sure we've got a prompt before sending. If not, + // this will issue a Ctrl+C + Espruino.Core.Utils.getEspruinoPrompt(function() { + // Make sure code ends in 2 newlines + while (code[code.length-2]!="\n" || code[code.length-1]!="\n") + code += "\n"; + + // If we're supposed to reset Espruino before sending... + if (Espruino.Config.RESET_BEFORE_SEND) { + code = "\x10reset();\n"+code; + } + + //console.log("Sending... "+data); + Espruino.Core.Serial.write(code, true, function() { + // give 5 seconds for sending with save and 2 seconds without save + var count = Espruino.Config.SAVE_ON_SEND ? 50 : 20; + setTimeout(function cb() { + if (Espruino.Core.Terminal!==undefined && + Espruino.Core.Terminal.getTerminalLine()!=">") { + count--; + if (count>0) { + setTimeout(cb, 100); + } else { + Espruino.Core.Notifications.error("Prompt not detected - upload failed. Trying to recover..."); + Espruino.Core.Serial.write("\x03\x03echo(1)\n", false, callback); + } + } else { + if (callback) callback(); + } + }, 100); + }); + }); + }; + + /// Parse and fix issues like `if (false)\n foo` in the root scope + function reformatCode(code) { + var APPLY_LINE_NUMBERS = false; + var lineNumberOffset = 0; + var ENV = Espruino.Core.Env.getData(); + if (ENV && ENV.VERSION_MAJOR && ENV.VERSION_MINOR) { + if (ENV.VERSION_MAJOR>1 || + ENV.VERSION_MINOR>=81.086) { + if (Espruino.Config.STORE_LINE_NUMBERS) + APPLY_LINE_NUMBERS = true; + } + } + // Turn cr/lf into just lf (eg. windows -> unix) + code = code.replace(/\r\n/g,"\n"); + // First off, try and fix funky characters + for (var i=0;i255) && ch!=9/*Tab*/ && ch!=10/*LF*/ && ch!=13/*CR*/) { + console.warn("Funky character code "+ch+" at position "+i+". Replacing with ?"); + code = code.substr(0,i)+"?"+code.substr(i+1); + } + } + + /* Search for lines added to the start of the code by the module handler. + Ideally there would be a better way of doing this so line numbers stayed correct, + but this hack works for now. Fixes EspruinoWebIDE#140 */ + if (APPLY_LINE_NUMBERS) { + var l = code.split("\n"); + var i = 0; + while (l[i] && (l[i].substr(0,8)=="Modules." || + l[i].substr(0,8)=="setTime(")) i++; + lineNumberOffset = -i; + } + + var resultCode = "\x10"; // 0x10 = echo off for line + /** we're looking for: + * `a = \n b` + * `for (.....) \n X` + * `if (.....) \n X` + * `if (.....) { } \n else foo` + * `while (.....) \n X` + * `do \n X` + * `function (.....) \n X` + * `function N(.....) \n X` + * `var a \n , b` `var a = 0 \n, b` + * `var a, \n b` `var a = 0, \n b` + * `a \n . b` + * `foo() \n . b` + * `try { } \n catch \n () \n {}` + * + * These are divided into two groups - where there are brackets + * after the keyword (statementBeforeBrackets) and where there aren't + * (statement) + * + * We fix them by replacing \n with what you get when you press + * Alt+Enter (Ctrl + LF). This tells Espruino that it's a newline + * but NOT to execute. + */ + var lex = Espruino.Core.Utils.getLexer(code); + var brackets = 0; + var curlyBrackets = 0; + var statementBeforeBrackets = false; + var statement = false; + var varDeclaration = false; + var lastIdx = 0; + var lastTok = {str:""}; + var tok = lex.next(); + while (tok!==undefined) { + var previousString = code.substring(lastIdx, tok.startIdx); + var tokenString = code.substring(tok.startIdx, tok.endIdx); + //console.log("prev "+JSON.stringify(previousString)+" next "+tokenString); + + /* Inserting Alt-Enter newline, which adds newline without trying + to execute */ + if (brackets>0 || // we have brackets - sending the alt-enter special newline means Espruino doesn't have to do a search itself - faster. + statement || // statement was before brackets - expecting something else + statementBeforeBrackets || // we have an 'if'/etc + varDeclaration || // variable declaration then newline + tok.str=="," || // comma on newline - there was probably something before + tok.str=="." || // dot on newline - there was probably something before + tok.str=="+" || tok.str=="-" || // +/- on newline - there was probably something before + tok.str=="=" || // equals on newline - there was probably something before + tok.str=="else" || // else on newline + lastTok.str=="else" || // else befgore newline + tok.str=="catch" || // catch on newline - part of try..catch + lastTok.str=="catch" + ) { + //console.log("Possible"+JSON.stringify(previousString)); + previousString = previousString.replace(/\n/g, "\x1B\x0A"); + } + + var previousBrackets = brackets; + if (tok.str=="(" || tok.str=="{" || tok.str=="[") brackets++; + if (tok.str=="{") curlyBrackets++; + if (tok.str==")" || tok.str=="}" || tok.str=="]") brackets--; + if (tok.str=="}") curlyBrackets--; + + if (brackets==0) { + if (tok.str=="for" || tok.str=="if" || tok.str=="while" || tok.str=="function" || tok.str=="throw") { + statementBeforeBrackets = true; + varDeclaration = false; + } else if (tok.str=="var") { + varDeclaration = true; + } else if (tok.type=="ID" && lastTok.str=="function") { + statementBeforeBrackets = true; + } else if (tok.str=="try" || tok.str=="catch") { + statementBeforeBrackets = true; + } else if (tok.str==")" && statementBeforeBrackets) { + statementBeforeBrackets = false; + statement = true; + } else if (["=","^","&&","||","+","+=","-","-=","*","*=","/","/=","%","%=","&","&=","|","|="].indexOf(tok.str)>=0) { + statement = true; + } else { + if (tok.str==";") varDeclaration = false; + statement = false; + statementBeforeBrackets = false; + } + } + /* If we're at root scope and had whitespace/comments between code, + remove it all and replace it with a single newline and a + 0x10 (echo off for line) character. However DON'T do this if we had + an alt-enter in the line, as it was there to stop us executing + prematurely */ + if (previousBrackets==0 && + previousString.indexOf("\n")>=0 && + previousString.indexOf("\x1B\x0A")<0) { + previousString = "\n\x10"; + // Apply line numbers to each new line sent, to aid debugger + if (APPLY_LINE_NUMBERS && tok.lineNumber && (tok.lineNumber+lineNumberOffset)>0) { + // Esc [ 1234 d + // This is the 'set line number' command that we're abusing :) + previousString += "\x1B\x5B"+(tok.lineNumber+lineNumberOffset)+"d"; + } + } + + // add our stuff back together + resultCode += previousString+tokenString; + // next + lastIdx = tok.endIdx; + lastTok = tok; + tok = lex.next(); + } + //console.log(resultCode); + if (brackets>0) { + Espruino.Core.Notifications.error("You have more open brackets than close brackets. Please see the hints in the Editor window."); + return undefined; + } + if (brackets<0) { + Espruino.Core.Notifications.error("You have more close brackets than open brackets. Please see the hints in the Editor window."); + return undefined; + } + return resultCode; + }; + + Espruino.Core.CodeWriter = { + init : init, + writeToEspruino : writeToEspruino, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Automatically load any referenced modules + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.Core.Config.add("MODULE_URL", { + section : "Communications", + name : "Module URL", + description : "Where to search online for modules when `require()` is used", + type : "string", + defaultValue : "https://www.espruino.com/modules" + }); + Espruino.Core.Config.add("MODULE_EXTENSIONS", { + section : "Communications", + name : "Module Extensions", + description : "The file extensions to use for each module. These are checked in order and the first that exists is used. One or more file extensions (including the dot) separated by `|`", + type : "string", + defaultValue : ".min.js|.js" + }); + Espruino.Core.Config.add("MODULE_AS_FUNCTION", { + section : "Communications", + name : "Modules uploaded as functions", + description : "Espruino 1v90 and later ONLY. Upload modules as Functions, allowing any functions inside them to be loaded directly from flash when 'Save on Send' is enabled.", + type : "boolean", + defaultValue : true + }); + + Espruino.Core.Config.add("MODULE_PROXY_ENABLED", { + section : "Communications", + name : "Enable Proxy", + description : "Enable Proxy for loading the modules when `require()` is used (only in native IDE)", + type : "boolean", + defaultValue : false + }); + + Espruino.Core.Config.add("MODULE_PROXY_URL", { + section : "Communications", + name : "Proxy URL", + description : "Proxy URL for loading the modules when `require()` is used (only in native IDE)", + type : "string", + defaultValue : "" + }); + + Espruino.Core.Config.add("MODULE_PROXY_PORT", { + section : "Communications", + name : "Proxy Port", + description : "Proxy Port for loading the modules when `require()` is used (only in native IDE)", + type : "string", + defaultValue : "" + }); + + // When code is sent to Espruino, search it for modules and add extra code required to load them + Espruino.addProcessor("transformForEspruino", function(code, callback) { + loadModules(code, callback); + }); + + // Append the 'getModule' processor as the last (plugins get initialized after Espruino.Core modules) + Espruino.Plugins.CoreModules = { + init: function() { + Espruino.addProcessor("getModule", function(data, callback) { + if (data.moduleCode!==undefined) { // already provided be previous getModule processor + return callback(data); + } + + fetchGetModule(data, callback); + }); + } + }; + } + + function isBuiltIn(module) { + var d = Espruino.Core.Env.getData(); + // If we got data from the device itself, use that as the + // definitive answer + if ("string" == typeof d.MODULES) + return d.MODULES.split(",").indexOf(module)>=0; + // Otherwise try and figure it out from JSON + if ("info" in d && + "builtin_modules" in d.info && + d.info.builtin_modules.indexOf(module)>=0) + return true; + // Otherwise assume we don't have it + return false; + } + + /** Find any instances of require(...) in the code string and return a list */ + var getModulesRequired = function(code) { + var modules = []; + + var lex = Espruino.Core.Utils.getLexer(code); + var tok = lex.next(); + var state = 0; + while (tok!==undefined) { + if (state==0 && tok.str=="require") { + state=1; + } else if (state==1 && tok.str=="(") { + state=2; + } else if (state==2 && (tok.type=="STRING")) { + state=0; + var module = tok.value; + if (!isBuiltIn(module) && modules.indexOf(module)<0) + modules.push(module); + } else + state = 0; + tok = lex.next(); + } + + return modules; + }; + + /** Download modules from MODULE_URL/.. */ + function fetchGetModule(data, callback) { + var fullModuleName = data.moduleName; + + // try and load the module the old way... + console.log("loadModule("+fullModuleName+")"); + + var urls = []; // Array of where to look for this module + var modName; // Simple name of the module + if(Espruino.Core.Utils.isURL(fullModuleName)) { + modName = fullModuleName.substr(fullModuleName.lastIndexOf("/") + 1).split(".")[0]; + urls = [ fullModuleName ]; + } else { + modName = fullModuleName; + Espruino.Config.MODULE_URL.split("|").forEach(function (url) { + url = url.trim(); + if (url.length!=0) + Espruino.Config.MODULE_EXTENSIONS.split("|").forEach(function (extension) { + urls.push(url + "/" + fullModuleName + extension); + }) + }); + }; + + // Recursively go through all the urls + (function download(urls) { + if (urls.length==0) { + return callback(data); + } + var dlUrl = urls[0]; + Espruino.Core.Utils.getURL(dlUrl, function (code) { + if (code!==undefined) { + // we got it! + data.moduleCode = code; + data.isMinified = dlUrl.substr(-7)==".min.js"; + return callback(data); + } else { + // else try next + download(urls.slice(1)); + } + }); + })(urls); + } + + + /** Called from loadModule when a module is loaded. Parse it for other modules it might use + * and resolve dfd after all submodules have been loaded */ + function moduleLoaded(resolve, requires, modName, data, loadedModuleData, alreadyMinified){ + // Check for any modules used from this module that we don't already have + var newRequires = getModulesRequired(data); + console.log(" - "+modName+" requires "+JSON.stringify(newRequires)); + // if we need new modules, set them to load and get their promises + var newPromises = []; + for (var i in newRequires) { + if (requires.indexOf(newRequires[i])<0) { + console.log(" Queueing "+newRequires[i]); + requires.push(newRequires[i]); + newPromises.push(loadModule(requires, newRequires[i], loadedModuleData)); + } else { + console.log(" Already loading "+newRequires[i]); + } + } + + var loadProcessedModule = function (module) { + // if we needed to load something, wait until it's loaded before resolving this + Promise.all(newPromises).then(function(){ + // add the module to end of our array + if (Espruino.Config.MODULE_AS_FUNCTION) + loadedModuleData.push("Modules.addCached(" + JSON.stringify(module.name) + ",function(){" + module.code + "});"); + else + loadedModuleData.push("Modules.addCached(" + JSON.stringify(module.name) + "," + JSON.stringify(module.code) + ");"); + // We're done + resolve(); + }); + } + if (alreadyMinified) + loadProcessedModule({code:data,name:modName}); + else + Espruino.callProcessor("transformModuleForEspruino", {code:data,name:modName}, loadProcessedModule); + } + + /** Given a module name (which could be a URL), try and find it. Return + * a deferred thingybob which signals when we're done. */ + function loadModule(requires, fullModuleName, loadedModuleData) { + return new Promise(function(resolve, reject) { + // First off, try and find this module using callProcessor + Espruino.callProcessor("getModule", + { moduleName:fullModuleName, moduleCode:undefined, isMinified:false }, + function(data) { + if (data.moduleCode===undefined) { + Espruino.Core.Notifications.warning("Module "+fullModuleName+" not found"); + return resolve(); + } + + // great! it found something. Use it. + moduleLoaded(resolve, requires, fullModuleName, data.moduleCode, loadedModuleData, data.isMinified); + }); + }); + } + + /** Finds instances of 'require' and then ensures that + those modules are loaded into the module cache beforehand + (by inserting the relevant 'addCached' commands into 'code' */ + function loadModules(code, callback){ + var loadedModuleData = []; + var requires = getModulesRequired(code); + if (requires.length == 0) { + // no modules needed - just return + callback(code); + } else { + Espruino.Core.Status.setStatus("Loading modules"); + // Kick off the module loading (each returns a promise) + var promises = requires.map(function (moduleName) { + return loadModule(requires, moduleName, loadedModuleData); + }); + // When all promises are complete + Promise.all(promises).then(function(){ + callback(loadedModuleData.join("\n") + "\n" + code); + }); + } + }; + + + Espruino.Core.Modules = { + init : init + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Board Environment variables (process.env) - queried when board connects + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + var environmentData = {}; + var boardData = {}; + + function init() { + Espruino.Core.Config.add("ENV_ON_CONNECT", { + section : "Communications", + name : "Request board details on connect", + description : 'Just after the board is connected, should we query `process.env` to find out which board we\'re connected to? '+ + 'This enables the Web IDE\'s code completion, compiler features, and firmware update notice.', + type : "boolean", + defaultValue : true, + }); + + Espruino.addProcessor("connected", function(data, callback) { + // Give us some time for any stored data to come in + setTimeout(queryBoardProcess, 200, data, callback); + }); + } + + function queryBoardProcess(data, callback) { + if ((!Espruino.Config.ENV_ON_CONNECT) || + (Espruino.Core.MenuFlasher && Espruino.Core.MenuFlasher.isFlashing())) { + return callback(data); + } + + Espruino.Core.Utils.executeExpression("process.env", function(result) { + var json = {}; + if (result!==undefined) { + try { + json = JSON.parse(result); + } catch (e) { + console.log("JSON parse failed - " + e + " in " + JSON.stringify(result)); + } + } + if (Object.keys(json).length==0) { + Espruino.Core.Notifications.error("Unable to retrieve board information.\nConnection Error?"); + // make sure we don't remember a previous board's info + json = { + VERSION : undefined, + BOARD : undefined, + MODULES : undefined, + EXPTR : undefined + }; + } else { + if (json.BOARD && json.VERSION) + Espruino.Core.Notifications.info("Found " +json.BOARD+", "+json.VERSION); + } + // now process the enviroment variables + for (var k in json) { + boardData[k] = json[k]; + environmentData[k] = json[k]; + } + if (environmentData.VERSION) { + var v = environmentData.VERSION; + var vIdx = v.indexOf("v"); + if (vIdx>=0) { + environmentData.VERSION_MAJOR = parseInt(v.substr(0,vIdx)); + var minor = v.substr(vIdx+1); + var dot = minor.indexOf("."); + if (dot>=0) + environmentData.VERSION_MINOR = parseInt(minor.substr(0,dot)) + parseInt(minor.substr(dot+1))*0.001; + else + environmentData.VERSION_MINOR = parseFloat(minor); + } + } + + Espruino.callProcessor("environmentVar", environmentData, function(envData) { + environmentData = envData; + callback(data); + }); + }); + } + + /** Get all data merged in from the board */ + function getData() { + return environmentData; + } + + /** Get just the board's environment data */ + function getBoardData() { + return boardData; + } + + /** Get a list of boards that we know about */ + function getBoardList(callback) { + var jsonDir = Espruino.Config.BOARD_JSON_URL; + + // ensure jsonDir ends with slash + if (jsonDir.indexOf('/', jsonDir.length - 1) === -1) { + jsonDir += '/'; + } + + Espruino.Core.Utils.getJSONURL(jsonDir + "boards.json", function(boards){ + // now load all the individual JSON files + var promises = []; + for (var boardId in boards) { + promises.push((function() { + var id = boardId; + return new Promise(function(resolve, reject) { + Espruino.Core.Utils.getJSONURL(jsonDir + boards[boardId].json, function (data) { + boards[id]["json"] = data; + resolve(); + }); + }); + })()); + } + + // When all are loaded, load the callback + Promise.all(promises).then(function() { + callback(boards); + }); + }); + } + + Espruino.Core.Env = { + init : init, + getData : getData, + getBoardData : getBoardData, + getBoardList : getBoardList, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Automatically run an assembler on inline assembler statements + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + var base64_encode; + if (typeof btoa == "undefined") + base64_encode = function(s) { return Buffer.from(s).toString('base64'); }; + else + base64_encode = btoa; + +/* Thumb reference : + http://ece.uwaterloo.ca/~ece222/ARM/ARM7-TDMI-manual-pt3.pdf + + ARM reference + https://web.eecs.umich.edu/~prabal/teaching/eecs373-f11/readings/ARMv7-M_ARM.pdf +*/ + + // list of registers (for push/pop type commands) + function rlist_lr(value) { + var regs = value.split(","); + var vals = { r0:1,r1:2,r2:4,r3:8,r4:16,r5:32,r6:64,r7:128,lr:256 }; + var bits = 0; + for (var i in regs) { + var reg = regs[i].trim(); + if (!(reg in vals)) throw "Unknown register name "+reg; + bits |= vals[reg]; + } + return bits; + } + + function reg(reg_offset) { + return function(reg) { + var vals = { r0:0,r1:1,r2:2,r3:3,r4:4,r5:5,r6:6,r7:7 }; + if (!(reg in vals)) throw "Unknown register name "+reg; + return vals[reg]<=0 && regVal<8) + return ((regVal&7)<=0 && v<65536) { + // https://web.eecs.umich.edu/~prabal/teaching/eecs373-f11/readings/ARMv7-M_ARM.pdf page 347 + var imm4,i,imm3,imm8; // what the...? + imm4 = (v>>12)&15; + i = (v>>11)&1; + imm3 = (v>>8)&7; + imm8 = v&255; + return (i<<26) | (imm4<<16) | (imm3<<12) | imm8; + } + throw "Invalid number '"+value+"' - must be between 0 and 65535"; + } + + function _int(offset, bits, shift, signed) { + return function(value, labels) { + var maxValue = ((1<= 0) { + addValue = parseInt(value.substr(maths)); + value = value.substr(0,maths); + } + if (value in labels) + binValue = labels[value] + addValue - labels["PC"]; + else + throw "Unknown label '"+value+"'"; + } + + + //console.log("VALUE----------- "+binValue+" PC "+labels["PC"]+" L "+labels[value]); + + if (binValue>=minValue && binValue<=maxValue && (binValue&((1<> shift) & ((1<>11)&0x7FF)<<16 | (v&0x7FF); + }; + } + + var ops = { + // Format 1: move shifted register + "lsl" :[{ base:"00000-----___---", regex : /(r[0-7]),(r[0-7]),(#[0-9]+)$/, args:[reg(0),reg(3),uint(6,5,0)] }, + { base:"0100000010___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // 5.4 d = d << s + "lsr" :[{ base:"00001-----___---", regex : /(r[0-7]),(r[0-7]),(#[0-9]+)$/, args:[reg(0),reg(3),uint(6,5,0)] }, + { base:"0100000011___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // 5.4 d = d >> s + "asr" :[{ base:"00010-----___---", regex : /(r[0-7]),(r[0-7]),(#[0-9]+)$/, args:[reg(0),reg(3),uint(6,5,0)] }, + { base:"0100000100___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // 5.4 d = d >>> s + // 5.2 Format 2: add/subtract + // 00011 + // 5.3 Format 3: move/compare/add/subtract immediate + "cmp" :[{ base:"00101---________", regex : /(r[0-7]),(#[0-9]+)$/, args:[reg(8),uint(0,8,0)] }, // move/compare/subtract immediate + { base:"0100001010___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // 5.4 test d-s + // 5.4 Format 4: ALU operations + "and" :[{ base:"0100000000___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], + "eor" :[{ base:"0100000001___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], + // lsl is above + // lsr is above + // asr is above + "adc" :[{ base:"0100000101___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // d + s + carry + "sbc" :[{ base:"0100000110___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // d - s - !carry + "ror" :[{ base:"0100000111___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // rotate right + "tst" :[{ base:"0100001000___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // test + "neg" :[{ base:"0100001001___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // - s + // cmp is above + "cmn" :[{ base:"0100001011___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // test d+s + "orr" :[{ base:"0100001100___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // | + "mul" :[{ base:"0100001101___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // s*d + "bic" :[{ base:"0100001110___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // d & ~s + "mvn" :[{ base:"0100001111___---", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }], // ~s + // 5.5 Format 5: Hi register operations/branch exchange + // 5.6 Format 6: PC-relative load + // done (below) + // 5.7 Format 7: load/store with register offset + // done (below) + // 5.8 Format 8: load/store sign-extended byte/halfword + // 5.9 Format 9: load/store with immediate offset + // done (below) + // 5.10 Format 10: load/store halfword + // 5.11 Format 11: SP-relative load/store + // 5.12 Format 12: load address + // done (below) + // 5.13 Format 13: add offset to Stack Pointer + // 5.14 Format 14: push/pop registers + // done (below) + // 5.16 Format 16: conditional branch + "beq" :[{ base:"11010000________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bne" :[{ base:"11010001________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bcs" :[{ base:"11010010________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bcc" :[{ base:"11010011________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bmi" :[{ base:"11010100________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bpl" :[{ base:"11010101________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bvs" :[{ base:"11010110________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bvc" :[{ base:"11010111________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bhi" :[{ base:"11011000________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bls" :[{ base:"11011001________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bge" :[{ base:"11011010________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "blt" :[{ base:"11011011________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "bgt" :[{ base:"11011100________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + "ble" :[{ base:"11011101________", regex : /^(.*)$/, args:[sint(0,8,1)] }], // 5.16 Format 16: conditional branch + // 5.17 Format 17: software interrupt + // 5.18 Format 18: unconditional branch + "b" :[{ base:"11100___________", regex : /^(.*)$/, args:[sint(0,11,1)] }], + // 5.19 Format 19: long branch with link + "bl" :[{ base:"11110___________11111___________", regex : /^(.*)$/, args:[bl_addr()] }], + "bx" :[{ base:"010001110----000", regex : /(lr|r[0-9]+)$/, args:[reg4(3)] }], + // .... + + + "adr" :[{ base:"10100---________", regex : /^(r[0-7]),([a-zA-Z_][0-9a-zA-Z_]*)$/,args:[reg(8),uint(0,8,2)] }, // ADR pseudo-instruction to save address (actually ADD PC) + { base:"10100---________", regex : /^(r[0-7]),([a-zA-Z_][0-9a-zA-Z_]*\+[0-9]+)$/,args:[reg(8),uint(0,8,2)] }], + "push" :[{ base:"1011010-________", regex : /^{(.*)}$/, args:[rlist_lr] }], // 5.14 Format 14: push/pop registers + "pop" :[{ base:"1011110-________", regex : /^{(.*)}$/, args:[rlist_lr] }], // 5.14 Format 14: push/pop registers + "add" :[{ base:"00110---________", regex : /(r[0-7]),(#[0-9]+)$/, args:[reg(8),uint(0,8,0)] }, // move/compare/subtract immediate + { base:"10100---________", regex : /^(r[0-7]),pc,(#[0-9]+)$/,args:[reg(8),uint(0,8,2)] }, + { base:"10101---________", regex : /^(r[0-7]),sp,(#[0-9]+)$/, args:[reg(8),uint(0,8,2)] }, + { base:"101100000_______", regex : /^sp,(#[0-9]+)$/, args:[uint(0,7,2)] }, + { base:"00011-0___---___", regex : /^(r[0-7]),(r[0-7]),([^,]+)$/, args:[reg(0),reg(3),reg_or_immediate(6,10)] } ], // Format 2: add/subtract + "adds" :[{ base:"00011-0___---___", regex : /^(r[0-7]),(r[0-7]),([^,]+)$/, args:[reg(0),reg(3),reg_or_immediate(6,10)] } ], //? + "adc.w":[{ base:"111010110100----________--------", regex : /^(r[0-7]),(r[0-7]),(r[0-7])$/,args:[reg(16),reg(8),reg(0)] }], // made this up. probably wrong + "add.w":[{ base:"11110001--------________--------", regex : /^(r[0-7]),(r[0-7]),(#[0-9]+)$/,args:[reg(16),reg(8),uint(0,8,0)] }], // made this up. probably wrong + "sub" :[{ base:"00111---________", regex : /(r[0-7]),(#[0-9]+)$/, args:[reg(8),uint(0,8,0)] }, // move/compare/subtract immediate + /*{ base:"10100---________", regex : /^([^,]+),pc,(#[0-9]+)$/,args:[reg(8),uint(0,8,2)] },*/ + { base:"101100001_______", regex : /^sp,(#[0-9]+)$/, args:[uint(0,7,2)] }, + { base:"00011-1___---___", regex : /^([^,]+),([^,]+),([^,]+)$/, args:[reg(0),reg(3),reg_or_immediate(6,10)] } ], + + "str" :[{ base:"0101000---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }, // 5.7 Format 7: load/store with register offset + { base:"10010---________", regex : /(r[0-7]),\[sp,(#[0-9]+)\]$/, args:[reg(8),uint(0,8,2)] }, // 5.11 SP-relative store + { base:"0110000000___---", regex : /(r[0-7]),\[(r[0-7])\]$/, args:[reg(0),reg(3)] }, // 5.9 Format 9: load/store with no offset + { base:"0110000---___---", regex : /(r[0-7]),\[(r[0-7]),(#[0-9]+)\]$/, args:[reg(0),reg(3), uint(6,5,2)] }], // 5.9 Format 9: load/store with immediate offset + "strb" :[{ base:"0101010---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }, // 5.7 Format 7: load/store with register offset + { base:"01110-----___---", regex : /(r[0-7]),\[(r[0-7]),(#[0-9]+)\]$/, args:[reg(0),reg(3), uint(6,5,0)] }], // 5.9 Format 9: load/store with immediate offset + "strh" :[{ base:"0101001---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }, // 5.7 Format 7: load/store with register offset + { base:"10000-----___---", regex : /(r[0-7]),\[(r[0-7]),(#[0-9]+)\]$/, args:[reg(0),reg(3), uint(6,5,1)] }], // 5.9 Format 9: load/store with immediate offset + "ldr" :[{ base:"01001---________", regex : /(r[0-7]),\[pc,(#[0-9]+)\]$/, args:[reg(8),uint(0,8,2)] }, // 5.6 Format 6: PC-relative load + { base:"10011---________", regex : /(r[0-7]),\[sp,(#[0-9]+)\]$/, args:[reg(8),uint(0,8,2)] }, // 5.11 SP-relative load + { base:"01001---________", regex : /(r[0-7]),([a-zA-Z_][0-9a-zA-Z_]*)$/, args:[reg(8),uint(0,8,2)] }, // 5.6 Format 6: PC-relative load (using label) + { base:"01001---________", regex : /(r[0-7]),([a-zA-Z_][0-9a-zA-Z_]*\+[0-9]+)$/, args:[reg(8),uint(0,8,2)] }, // 5.6 Format 6: PC-relative load (using label and maths - huge hack) + { base:"0101100---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }, // 5.7 Format 7: load/store with register offset + { base:"0110100000___---", regex : /(r[0-7]),\[(r[0-7])\]$/, args:[reg(0),reg(3)] }, // 5.9 Format 9: load/store with no offset + { base:"0110100---___---", regex : /(r[0-7]),\[(r[0-7]),(#[0-9]+)\]$/, args:[reg(0),reg(3), uint(6,5,2)] }], // 5.9 Format 9: load/store with immediate offset + + "ldrb" :[{ base:"0101110---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }, // 5.7 Format 7: load/store with register offset + { base:"01111-----___---", regex : /(r[0-7]),\[(r[0-7]),(#[0-9]+)\]$/, args:[reg(0),reg(3), uint(6,5,0)] }], // 5.9 Format 9: load/store with immediate offset + "ldrsb":[{ base:"0101011---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }], // 5.7 Format 7: load/store with register offset + "ldrh" :[{ base:"0101101---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }, // 5.7 Format 7: load/store with register offset + { base:"10001-----___---", regex : /(r[0-7]),\[(r[0-7]),(#[0-9]+)\]$/, args:[reg(0),reg(3), uint(6,5,1)] }], // 5.9 Format 9: load/store with immediate offset + "ldrsh":[{ base:"0101111---___---", regex : /(r[0-7]),\[(r[0-7]),(r[0-7])\]$/, args:[reg(0),reg(3),reg(6)] }], // 5.7 Format 7: load/store with register offset + "mov" :[{ base:"00100---________", regex : /(r[0-7]),(#[0-9]+)$/, args:[reg(8),uint(0,8,0)] }, // move/compare/subtract immediate + { base:"0001110000---___", regex : /(r[0-7]),(r[0-7])$/, args:[reg(0),reg(3)] }, // actually 'add Rd,Rs,#0' + { base:"0100011010---101", regex : /sp,(r[0-7])$/, args:[reg(3)] }], // made up again + "movs" :[{ base:"00100---________", regex : /(r[0-7]),(#[0-9]+)$/, args:[reg(8),uint(0,8,0)] }], // is this even in thumb? + "movw" :[{ base:"11110-100100----0___----________", regex : /(r[0-7]),(#[0-9]+)$/, args:[reg4(8),thumb2_immediate_t3] }], + + ".word":[{ base:"--------------------------------", regex : /0x([0-9A-Fa-f]+)$/, args:[function(v){v=parseInt(v,16);return (v>>16)|(v<<16);}] }, + { base:"--------------------------------", regex : /([0-9]+)$/, args:[function(v){v=parseInt(v);return (v>>16)|(v<<16);}] }], + "nop" :[{ base:"0100011011000000", regex : "", args:[] }], // MOV R8,R8 (Format 5) + "cpsie" :[{ base:"1011011001100010", regex : /i/, args:[] }], // made up again + "cpsid" :[{ base:"1011011001110010", regex : /i/, args:[] }], // made up again + "wfe" :[{ base:"1011111100100000", regex : /i/, args:[] }], + "wfi" :[{ base:"1011111100110000", regex : /i/, args:[] }], + + // for this, uint needs to work without a hash +// "swi" :[{ base:"11011111--------", regex : /([0-9]+)$/, args:[uint(0,8,0)] }], // Format 17: software interrupt + }; + + + function getOpCode(binary) { + var base = ""; + for (var b in binary) + if ("-_".indexOf(binary[b])>=0) + base += "0"; + else + base += binary[b]; + var opCode = parseInt(base,2); + if (opCode<0) opCode = opCode + 2147483648.0; + return opCode; + } + + function assemble_internal(asmLines, wordCallback, labels) { + var addr = 0; + var newLabels = {}; + asmLines.forEach(function (line) { + // setup labels + if (labels!==undefined) + labels["PC"] = addr+4; + // handle line + line = line.trim(); + if (line=="") return; + if (line.substr(-1)==":") { + // it's a label + var labelName = line.substr(0,line.length-1); + if (newLabels[labelName] !== undefined) + throw "Label '"+labelName+"' was already defined"; + newLabels[labelName] = addr; + return; + } + + // parse instruction + var firstArgEnd = line.indexOf("\t"); + if (firstArgEnd<0) firstArgEnd = line.indexOf(" "); + if (firstArgEnd<0) firstArgEnd=line.length; + var opName = line.substr(0,firstArgEnd); + var args = line.substr(firstArgEnd).replace(/[ \t]/g,"").trim(); + if (!(opName in ops)) throw "Unknown Op '"+opName+"' in '"+line+"'"; + // search ops + var found = false; + for (var n in ops[opName]) { + var op = ops[opName][n]; + var m; + if (m=args.match(op.regex)) { + found = true; + // work out the base opcode + var opCode = getOpCode(op.base); + + if (labels!==undefined) { + /* If we're properly generating code, parse each argument. + Otherwise we're just working out the size in bytes of each line + and we can skip this */ + for (var i in op.args) { + //console.log(i,m[(i|0)+1]); + var argFunction = op.args[i]; + var bits = argFunction(m[(i|0)+1], labels); + //console.log(" ",bits) + opCode |= bits; + } + } + + if (op.base.length > 16) { + wordCallback((opCode>>>16)); + wordCallback(opCode&0xFFFF); + addr += 4; + } else { + wordCallback(opCode); + addr += 2; + } + break; + } + } + // now parse args + if (!found) + throw "Unknown arg style '"+args+"' in '"+line+"'"; + }); + return newLabels; + } + + function assemble(asmLines, wordCallback) { + // remove line comments + asmLines = asmLines.map(function(l) { + var i; + i = l.indexOf(";"); + if (i>=0) l = l.substr(0,i); + i = l.indexOf("//"); + if (i>=0) l = l.substr(0,i); + return l; + }); + // process assembly to grab labels + var labels = assemble_internal(asmLines, function() {}, undefined); + console.log("Assembler Labels:",labels); + // process again to actually get an output + assemble_internal(asmLines, wordCallback, labels); + } + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- + + function init() { + // When code is sent to Espruino, search it for bits of assembler and then assemble them + Espruino.addProcessor("transformForEspruino", function(code, callback) { + findASMBlocks(code, "", callback); + }); + // When a module is sent to Espruino... + Espruino.addProcessor("transformModuleForEspruino", function(module, callback) { + findASMBlocks(module.code, " in "+module.name, function(code) { + module.code = code; + callback(module); + }); + }); + } + + function assembleBlock(asmLines, description) { + var machineCode = []; + try { + assemble(asmLines, function(word) { machineCode.push("0x"+word.toString(16)); }); + } catch (err) { + console.log("Assembler failed: "+err+description); + Espruino.Core.Notifications.error("Assembler failed: "+err+description); + return undefined; + } + + return machineCode; + } + + /* Finds instances of 'E.asm' and replaces them */ + function findASMBlocks(code, description, callback){ + + function match(str, type) { + if (str!==undefined && tok.str!=str) { + Espruino.Core.Notifications.error("Expecting '"+str+"' but got '"+tok.str+description+"'. Should have E.asm('arg spec', 'asmline1', ..., 'asmline2'"); + return false; + } + if (type!==undefined && tok.type!=type) { + Espruino.Core.Notifications.error("Expecting a "+type+" but got "+tok.type+description+". Should have E.asm('arg spec', 'asmline1', ..., 'asmline2'"); + return false; + } + tok = lex.next(); + return true; + } + + var foundAsm = true; + var assembledCode = ""; + var asmBlockCount = 1; + while (foundAsm) { + foundAsm = false; + var lex = Espruino.Core.Utils.getLexer(code); + var tok = lex.next(); + var state = 0; + var startIndex = -1; + while (tok!==undefined) { + if (state==0 && tok.str=="E") { state=1; startIndex = tok.startIdx; tok = lex.next(); + } else if (state==1 && tok.str==".") { state=2; tok = lex.next(); + } else if (state==2 && (tok.str=="asm")) { state=3; tok = lex.next(); + } else if (state==3 && (tok.str=="(")) { + foundAsm = true; + state=0; + tok = lex.next(); // skip ( + var argSpec = tok.value; + var asmLines = []; + if (!match(undefined,"STRING")) return; + if (!match(",",undefined)) return; + while (tok && tok.str!=")") { + var lines = tok.value.split("\n"); + lines.forEach(function(l) { + asmLines.push(l); + }); + if (!match(undefined,"STRING")) return; + if (tok.str!=")") + if (!match(",",undefined)) return; + } + if (!match(")",undefined)) return; + var endIndex = tok.endIdx; + + var machineCode = assembleBlock(asmLines, description); + //console.log(machineCode); + if (machineCode===undefined) return; // There was an error - just leave and don't try to flash + var raw = ""; + machineCode.forEach(function(short) { + var v = parseInt(short,16); + raw += String.fromCharCode(v&255,v>>8); + }); + var base64 = base64_encode(raw); + code = code.substr(0,startIndex) + + 'E.nativeCall(1, '+JSON.stringify(argSpec)+', atob('+JSON.stringify(base64)+'))'+ + code.substr(endIndex); + asmBlockCount++; + + // Break out + tok = undefined; + } else { + state = 0; + tok = lex.next(); + } + } + } + + if (assembledCode!="") { + code = "var ASM_BASE=process.memory().stackEndAddress;\n"+ + assembledCode+ + code; + } + callback(code); + }; + + + Espruino.Plugins.Assembler = { + init : init, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Try and get any URLS that are from GitHub + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.addProcessor("getURL", getGitHub); + } + + function getGitHub(data, callback) { + var match = data.url.match(/^https?:\/\/github.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.*)$/); + if (match) { + var git = { + owner : match[1], + repo : match[2], + branch : match[3], + path : match[4] + }; + + var url = "https://raw.githubusercontent.com/"+git.owner+"/"+git.repo+"/"+git.branch+"/"+git.path; + console.log("Found GitHub", JSON.stringify(git)); + callback({url: url}); + } else + callback(data); // no match - continue as normal + } + + Espruino.Plugins.GetGitHub = { + init : init, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Pretokenise code before it uploads + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + if (typeof acorn == "undefined") { + console.log("pretokenise: needs acorn, disabling."); + return; + } + + function init() { + Espruino.Core.Config.add("PRETOKENISE", { + section : "Minification", + name : "Pretokenise code before upload (BETA)", + description : "All whitespace and comments are removed and all reserved words are converted to tokens before upload. This means a faster upload, less memory used, and increased performance (+10%) at the expense of code readability.", + type : "boolean", + defaultValue : false + }); + + // When code is sent to Espruino, search it for modules and add extra code required to load them + Espruino.addProcessor("transformForEspruino", function(code, callback) { + if (!Espruino.Config.PRETOKENISE) return callback(code); + pretokenise(code, callback); + }); + // When code is sent to Espruino, search it for modules and add extra code required to load them + Espruino.addProcessor("transformModuleForEspruino", function(module, callback) { + if (!Espruino.Config.PRETOKENISE) return callback(module); + pretokenise(module.code, function(code) { + module.code = code; + callback(module); + }); + }); + } + + + var LEX_OPERATOR_START = 138; + var TOKENS = [// plundered from jslex.c +/* LEX_EQUAL : */ "==", +/* LEX_TYPEEQUAL : */ "===", +/* LEX_NEQUAL : */ "!=", +/* LEX_NTYPEEQUAL : */ "!==", +/* LEX_LEQUAL : */ "<=", +/* LEX_LSHIFT : */ "<<", +/* LEX_LSHIFTEQUAL : */ "<<=", +/* LEX_GEQUAL : */ ">=", +/* LEX_RSHIFT : */ ">>", +/* LEX_RSHIFTUNSIGNED */ ">>>", +/* LEX_RSHIFTEQUAL : */ ">>=", +/* LEX_RSHIFTUNSIGNEDEQUAL */ ">>>=", +/* LEX_PLUSEQUAL : */ "+=", +/* LEX_MINUSEQUAL : */ "-=", +/* LEX_PLUSPLUS : */ "++", +/* LEX_MINUSMINUS */ "--", +/* LEX_MULEQUAL : */ "*=", +/* LEX_DIVEQUAL : */ "/=", +/* LEX_MODEQUAL : */ "%=", +/* LEX_ANDEQUAL : */ "&=", +/* LEX_ANDAND : */ "&&", +/* LEX_OREQUAL : */ "|=", +/* LEX_OROR : */ "||", +/* LEX_XOREQUAL : */ "^=", +/* LEX_ARROW_FUNCTION */ "=>", +// reserved words +/*LEX_R_IF : */ "if", +/*LEX_R_ELSE : */ "else", +/*LEX_R_DO : */ "do", +/*LEX_R_WHILE : */ "while", +/*LEX_R_FOR : */ "for", +/*LEX_R_BREAK : */ "break", +/*LEX_R_CONTINUE */ "continue", +/*LEX_R_FUNCTION */ "function", +/*LEX_R_RETURN */ "return", +/*LEX_R_VAR : */ "var", +/*LEX_R_LET : */ "let", +/*LEX_R_CONST : */ "const", +/*LEX_R_THIS : */ "this", +/*LEX_R_THROW : */ "throw", +/*LEX_R_TRY : */ "try", +/*LEX_R_CATCH : */ "catch", +/*LEX_R_FINALLY : */ "finally", +/*LEX_R_TRUE : */ "true", +/*LEX_R_FALSE : */ "false", +/*LEX_R_NULL : */ "null", +/*LEX_R_UNDEFINED */ "undefined", +/*LEX_R_NEW : */ "new", +/*LEX_R_IN : */ "in", +/*LEX_R_INSTANCEOF */ "instanceof", +/*LEX_R_SWITCH */ "switch", +/*LEX_R_CASE */ "case", +/*LEX_R_DEFAULT */ "default", +/*LEX_R_DELETE */ "delete", +/*LEX_R_TYPEOF : */ "typeof", +/*LEX_R_VOID : */ "void", +/*LEX_R_DEBUGGER : */ "debugger", +/*LEX_R_CLASS : */ "class", +/*LEX_R_EXTENDS : */ "extends", +/*LEX_R_SUPER : */ "super", +/*LEX_R_STATIC : */ "static", +/*LEX_R_OF : */ "of" +]; + + + function pretokenise(code, callback) { + var lex = (function() { + var t = acorn.tokenizer(code); + return { next : function() { + var tk = t.getToken(); + if (tk.type.label=="eof") return undefined; + var tp = "?"; + if (tk.type.label=="template" || tk.type.label=="string") tp="STRING"; + if (tk.type.label=="num") tp="NUMBER"; + if (tk.type.keyword || tk.type.label=="name") tp="ID"; + if (tp=="?" && tk.start+1==tk.end) tp="CHAR"; + return { + startIdx : tk.start, + endIdx : tk.end, + str : code.substring(tk.start, tk.end), + type : tp + }; + }}; + })(); + var brackets = 0; + var resultCode = ""; + var lastIdx = 0; + var lastTok = {str:""}; + var tok = lex.next(); + while (tok!==undefined) { + var previousString = code.substring(lastIdx, tok.startIdx); + var tokenString = code.substring(tok.startIdx, tok.endIdx); + var tokenId = LEX_OPERATOR_START + TOKENS.indexOf(tokenString); + if (tokenId=0) + resultCode += "\n"; + if (tok.str==")" || tok.str=="}" || tok.str=="]") brackets--; + // if we have a token for something, use that - else use the string + if (tokenId) { + //console.log(JSON.stringify(tok.str)+" => "+tokenId); + resultCode += String.fromCharCode(tokenId); + tok.type = "TOKENISED"; + } else { + if ((tok.type=="ID" || tok.type=="NUMBER") && + (lastTok.type=="ID" || lastTok.type=="NUMBER")) + resultCode += " "; + resultCode += tokenString; + } + // next + lastIdx = tok.endIdx; + lastTok = tok; + tok = lex.next(); + } + callback(resultCode); + } + + Espruino.Plugins.Pretokenise = { + init : init, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk), + Victor Nakoryakov (victor@amperka.ru) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Wrap whole code in `onInit` function before send and save() it + after upload. Wrapping is necessary to avoid execution start + before save() is executed + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.Core.Config.add("SAVE_ON_SEND", { + section : "Communications", + name : "Save on Send", + descriptionHTML : 'How should code be uploaded? See espruino.com/Saving for more information.
'+ + "NOTE: Avoid 'Direct to flash, even after reset()' for normal development - it can make it hard to recover if your code crashes the device.", + type : { + 0: "To RAM (default) - execute code while uploading. Use 'save()' to save a RAM image to Flash", + 1: "Direct to Flash (execute code at boot)", + 2: "Direct to Flash (execute code at boot, even after 'reset()') - USE WITH CARE", + 3: "To Storage File (see 'File in Storage to send to')", + }, + defaultValue : 0 + }); + Espruino.Core.Config.add("SAVE_STORAGE_FILE", { + section : "Communications", + name : "Send to File in Storage", + descriptionHTML : "If Save on Send is set to To Storage File, this is the name of the file to write to.", + type : "string", + defaultValue : "myapp" + }); + Espruino.Core.Config.add("LOAD_STORAGE_FILE", { + section : "Communications", + name : "Load after saving", + descriptionHTML : "This applies only if saving to Flash (not RAM)", + type : { + 0: "Don't load", + 1: "Load default application", + 2: "Load the Storage File just written to" + }, + defaultValue : 2 + }); + Espruino.addProcessor("transformForEspruino", function(code, callback) { + wrap(code, callback); + }); + } + + function wrap(code, callback) { + var isFlashPersistent = Espruino.Config.SAVE_ON_SEND == 2; + var isStorageUpload = Espruino.Config.SAVE_ON_SEND == 3; + var isFlashUpload = Espruino.Config.SAVE_ON_SEND == 1 || isFlashPersistent || isStorageUpload; + if (!isFlashUpload) return callback(code); + + // Check environment vars + var hasStorage = false; + var ENV = Espruino.Core.Env.getData(); + if (ENV && + ENV.VERSION_MAJOR && + ENV.VERSION_MINOR!==undefined) { + if (ENV.VERSION_MAJOR>1 || + ENV.VERSION_MINOR>=96) { + hasStorage = true; + } + } + + // + console.log("Uploading "+code.length+" bytes to flash"); + if (!hasStorage) { // old style + if (isStorageUpload) { + Espruino.Core.Notifications.error("You have pre-1v96 firmware - unable to upload to Storage"); + code = ""; + } else { + Espruino.Core.Notifications.error("You have pre-1v96 firmware. Upload size is limited by available RAM"); + code = "E.setBootCode("+JSON.stringify(code)+(isFlashPersistent?",true":"")+");load()\n"; + } + } else { // new style + var filename; + if (isStorageUpload) + filename = Espruino.Config.SAVE_STORAGE_FILE; + else + filename = isFlashPersistent ? ".bootrst" : ".bootcde"; + if (!filename || filename.length>28) { + Espruino.Core.Notifications.error("Invalid Storage file name "+JSON.stringify(filename)); + code = ""; + } else { + var CHUNKSIZE = 1024; + var newCode = []; + var len = code.length; + newCode.push('require("Storage").write("'+filename+'",'+JSON.stringify(code.substr(0,CHUNKSIZE))+',0,'+len+');'); + for (var i=CHUNKSIZE;i{ try { @@ -14,11 +20,24 @@ httpGet("apps.json").then(apps=>{ refreshFilter(); }); +httpGet("appdates.csv").then(csv=>{ + document.querySelector(".sort-nav").classList.remove("hidden"); + csv.split("\n").forEach(line=>{ + var l = line.split(","); + appSortInfo[l[0]] = { + created : Date.parse(l[1]), + modified : Date.parse(l[2]) + }; + }); +}).catch(err=>{ + console.log("No recent.csv - app sort disabled"); +}); + // =========================================== Top Navigation function showChangeLog(appid) { var app = appNameToApp(appid); function show(contents) { - showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});; + showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{}); } httpGet(`apps/${appid}/ChangeLog`). then(show).catch(()=>show("No Change Log available")); @@ -63,9 +82,15 @@ function handleCustomApp(appTemplate) { var iframe = modal.getElementsByTagName("iframe")[0]; iframe.contentWindow.addEventListener("message", function(event) { var appFiles = event.data; - var app = {}; - Object.keys(appTemplate).forEach(k => app[k] = appTemplate[k]); - Object.keys(appFiles).forEach(k => app[k] = appFiles[k]); + var app = JSON.parse(JSON.stringify(appTemplate)); // clone template + // copy extra keys from appFiles + Object.keys(appFiles).forEach(k => { + if (k!="storage") app[k] = appFiles[k] + }); + appFiles.storage.forEach(f => { + app.storage = app.storage.filter(s=>s.name!=f.name); // remove existing item + app.storage.push(f); // add new + }); console.log("Received custom app", app); modal.remove(); Comms.uploadApp(app).then(()=>{ @@ -142,6 +167,21 @@ function handleAppInterface(app) { }); } +function changeAppFavourite(favourite, app) { + var favourites = SETTINGS.favourites; + if (favourite) { + SETTINGS.favourites = SETTINGS.favourites.concat([app.id]); + } else { + if ([ "boot","setting"].includes(app.id)) { + showToast(app.name + ' is required, can\'t remove it' , 'warning'); + }else { + SETTINGS.favourites = SETTINGS.favourites.filter(e => e != app.id); + } + } + saveSettings(); + refreshLibrary(); +} + // =========================================== Top Navigation function showTab(tabname) { htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => { @@ -156,36 +196,62 @@ function showTab(tabname) { // =========================================== Library -var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value) +var chips = Array.from(document.querySelectorAll('.filter-nav .chip')).map(chip => chip.attributes.filterid.value); var hash = window.location.hash ? window.location.hash.slice(1) : ''; var activeFilter = !!~chips.indexOf(hash) ? hash : ''; -var currentSearch = ''; +var activeSort = ''; +var currentSearch = activeFilter ? '' : hash; function refreshFilter(){ var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); filtersContainer.querySelector('.active').classList.remove('active'); - if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active') - else filtersContainer.querySelector('.chip[filterid]').classList.add('active') + if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active'); + else filtersContainer.querySelector('.chip[filterid]').classList.add('active'); +} +function refreshSort(){ + var sortContainer = document.querySelector("#librarycontainer .sort-nav"); + sortContainer.querySelector('.active').classList.remove('active'); + if(activeSort) sortContainer.querySelector('.chip[sortid="'+activeSort+'"]').classList.add('active'); + else sortContainer.querySelector('.chip[sortid]').classList.add('active'); } function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; + var favourites = SETTINGS.favourites; if (activeFilter) { - visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); + if ( activeFilter == "favourites" ) { + visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length)); + } else { + visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); + } } if (currentSearch) { visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch)); } + if (activeSort) { + visibleApps = visibleApps.slice(); // clone the array so sort doesn't mess with original + if (activeSort=="created" || activeSort=="modified") { + visibleApps = visibleApps.sort((a,b) => appSortInfo[b.id][activeSort] - appSortInfo[a.id][activeSort]); + } else throw new Error("Unknown sort type "+activeSort); + } + panelbody.innerHTML = visibleApps.map((app,idx) => { var appInstalled = appsInstalled.find(a=>a.id==app.id); var version = getVersionInfo(app, appInstalled); var versionInfo = version.text; if (versionInfo) versionInfo = " ("+versionInfo+")"; - var readme = `Read more...`; + var readme = `Read more...`; + var favourite = favourites.find(e => e == app.id); + + var username = "espruino"; + var githubMatch = window.location.href.match(/\/(\w+)\.github\.io/); + if(githubMatch) username = githubMatch[1]; + var url = `https://github.com/${username}/BangleApps/tree/master/apps/${app.id}`; + return `
${escapeHtml(app.name)}

@@ -193,9 +259,10 @@ function refreshLibrary() {

${escapeHtml(app.name)} ${versionInfo}

${escapeHtml(app.description)}${app.readme?`
${readme}`:""}

- See the code on GitHub + See the code on GitHub
+ @@ -232,7 +299,7 @@ function refreshLibrary() { // upload icon.classList.remove("icon-upload"); icon.classList.add("loading"); - uploadApp(app) + uploadApp(app); } else if (icon.classList.contains("icon-menu")) { // custom HTML update icon.classList.remove("icon-menu"); @@ -250,6 +317,10 @@ function refreshLibrary() { updateApp(app); } else if (icon.classList.contains("icon-download")) { handleAppInterface(app); + } else if ( button.innerText == String.fromCharCode(0x2661)) { + changeAppFavourite(true, app); + } else if ( button.innerText == String.fromCharCode(0x2665) ) { + changeAppFavourite(false, app); } }); }); @@ -262,17 +333,17 @@ refreshLibrary(); function uploadApp(app) { return getInstalledApps().then(()=>{ if (appsInstalled.some(i => i.id === app.id)) { - return updateApp(app) + return updateApp(app); } Comms.uploadApp(app).then((appJSON) => { - Progress.hide({ sticky: true }) + Progress.hide({ sticky: true }); if (appJSON) { - appsInstalled.push(appJSON) + appsInstalled.push(appJSON); } - showToast(app.name + ' Uploaded!', 'success') + showToast(app.name + ' Uploaded!', 'success'); }).catch(err => { - Progress.hide({ sticky: true }) - showToast('Upload failed, ' + err, 'error') + Progress.hide({ sticky: true }); + showToast('Upload failed, ' + err, 'error'); }).finally(()=>{ refreshMyApps(); refreshLibrary(); @@ -286,8 +357,8 @@ function removeApp(app) { return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => { return getInstalledApps().then(()=>{ // a = from appid.info, app = from apps.json - return Comms.removeApp(appsInstalled.find(a => a.id === app.id)) - }) + return Comms.removeApp(appsInstalled.find(a => a.id === app.id)); + }); }).then(()=>{ appsInstalled = appsInstalled.filter(a=>a.id!=app.id); showToast(app.name+" removed successfully","success"); @@ -315,13 +386,21 @@ function updateApp(app) { if (app.custom) return customApp(app); return getInstalledApps().then(() => { // a = from appid.info, app = from apps.json - let remove = appsInstalled.find(a => a.id === app.id) + let remove = appsInstalled.find(a => a.id === app.id); // no need to remove files which will be overwritten anyway remove.files = remove.files.split(',') .filter(f => f !== app.id + '.info') .filter(f => !app.storage.some(s => s.name === f)) - .join(',') - return Comms.removeApp(remove) + .join(','); + let data = AppInfo.parseDataString(remove.data) + if ('data' in app) { + // only remove data files which are no longer declared in new app version + const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f) + data.dataFiles = data.dataFiles.filter(removeData) + data.storageFiles = data.storageFiles.filter(removeData) + } + remove.data = AppInfo.makeDataString(data) + return Comms.removeApp(remove); }).then(()=>{ showToast(`Updating ${app.name}...`); appsInstalled = appsInstalled.filter(a=>a.id!=app.id); @@ -365,10 +444,18 @@ function showLoadingIndicator(id) { panelbody.innerHTML = '
'; } +function getAppsToUpdate() { + var appsToUpdate = []; + appsInstalled.forEach(appInstalled => { + var app = appNameToApp(appInstalled.id); + if (app.version != appInstalled.version) + appsToUpdate.push(app); + }); + return appsToUpdate; +} + function refreshMyApps() { var panelbody = document.querySelector("#myappscontainer .panel-body"); - var tab = document.querySelector("#tab-myappscontainer a"); - tab.setAttribute("data-badge", appsInstalled.length); panelbody.innerHTML = appsInstalled.map(appInstalled => { var app = appNameToApp(appInstalled.id); var version = getVersionInfo(app, appInstalled); @@ -397,15 +484,26 @@ return `
// check icon to figure out what we should do if (icon.classList.contains("icon-delete")) removeApp(app); if (icon.classList.contains("icon-refresh")) updateApp(app); - if (icon.classList.contains("icon-download")) handleAppInterface(app) + if (icon.classList.contains("icon-download")) handleAppInterface(app); }); }); + var appsToUpdate = getAppsToUpdate(); + var tab = document.querySelector("#tab-myappscontainer a"); + var updateApps = document.querySelector("#myappscontainer .updateapps"); + if (appsToUpdate.length) { + updateApps.innerHTML = `Update ${appsToUpdate.length} apps`; + updateApps.classList.remove("hidden"); + tab.setAttribute("data-badge", `${appsInstalled.length} ⬆${appsToUpdate.length}`); + } else { + updateApps.classList.add("hidden"); + tab.setAttribute("data-badge", appsInstalled.length); + } } let haveInstalledApps = false; function getInstalledApps(refresh) { if (haveInstalledApps && !refresh) { - return Promise.resolve(appsInstalled) + return Promise.resolve(appsInstalled); } showLoadingIndicator("myappscontainer"); // Get apps and files @@ -423,6 +521,43 @@ function getInstalledApps(refresh) { }); } +/// Removes everything and install the given apps, eg: installMultipleApps(["boot","mclock"], "minimal") +function installMultipleApps(appIds, promptName) { + var apps = appIds.map( appid => appJSON.find(app=>app.id==appid) ); + if (apps.some(x=>x===undefined)) + return Promise.reject("Not all apps found"); + var appCount = apps.length; + return showPrompt("Install Defaults",`Remove everything and install ${promptName} apps?`).then(() => { + return Comms.removeAllApps(); + }).then(()=>{ + Progress.hide({sticky:true}); + appsInstalled = []; + showToast(`Existing apps removed. Installing ${appCount} apps...`); + return new Promise((resolve,reject) => { + function upload() { + var app = apps.shift(); + if (app===undefined) return resolve(); + Progress.show({title:`${app.name} (${appCount-apps.length}/${appCount})`,sticky:true}); + Comms.uploadApp(app,"skip_reset").then((appJSON) => { + Progress.hide({sticky:true}); + if (appJSON) appsInstalled.push(appJSON); + showToast(`(${appCount-apps.length}/${appCount}) ${app.name} Uploaded`); + upload(); + }).catch(function() { + Progress.hide({sticky:true}); + reject(); + }); + } + upload(); + }); + }).then(()=>{ + return Comms.setTime(); + }).then(()=>{ + showToast("Apps successfully installed!","success"); + return getInstalledApps(true); + }); +} + var connectMyDeviceBtn = document.getElementById("connectmydevice"); function handleConnectionChange(connected) { @@ -435,6 +570,22 @@ htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addE showToast("Getting app list failed, "+err,"error"); }); })); +htmlToArray(document.querySelectorAll(".btn.updateapps")).map(button => button.addEventListener("click", () => { + var appsToUpdate = getAppsToUpdate(); + var count = appsToUpdate.length; + function updater() { + if (!appsToUpdate.length) return; + var app = appsToUpdate.pop(); + return updateApp(app).then(function() { + return updater(); + }); + } + updater().then(err => { + showToast(`Updated ${count} apps`,"success"); + }).catch(err => { + showToast("Update failed, "+err,"error"); + }); +})); connectMyDeviceBtn.addEventListener("click", () => { if (connectMyDeviceBtn.classList.contains('is-connected')) { Comms.disconnectDevice(); @@ -453,18 +604,84 @@ filtersContainer.addEventListener('click', ({ target }) => { activeFilter = target.getAttribute('filterid') || ''; refreshFilter(); refreshLibrary(); - window.location.hash = activeFilter + window.location.hash = activeFilter; }); var librarySearchInput = document.querySelector("#searchform input"); - +librarySearchInput.value = currentSearch; librarySearchInput.addEventListener('input', evt => { currentSearch = evt.target.value.toLowerCase(); refreshLibrary(); }); +var sortContainer = document.querySelector("#librarycontainer .sort-nav"); +sortContainer.addEventListener('click', ({ target }) => { + if (target.classList.contains('active')) return; + + activeSort = target.getAttribute('sortid') || ''; + refreshSort(); + refreshLibrary(); + window.location.hash = activeFilter; +}); + // =========================================== About +if (window.location.host=="banglejs.com") { + document.getElementById("apploaderlinks").innerHTML = + 'This is the official Bangle.js App Loader - you can also try the Development Version for the most recent apps.'; +} else if (window.location.host=="espruino.github.io") { + document.title += " [Development]"; + document.getElementById("apploaderlinks").innerHTML = + 'This is the development Bangle.js App Loader - you can also try the Official Version for stable apps.'; +} else { + document.title += " [Unofficial]"; + document.getElementById("apploaderlinks").innerHTML = + 'This is not the official Bangle.js App Loader - you can try the Official Version here.'; +} + +// Settings +var SETTINGS_HOOKS = {}; // stuff to get called when a setting is loaded +/// Load settings and update controls +function loadSettings() { + var j = localStorage.getItem("settings"); + if (typeof j != "string") return; + try { + var s = JSON.parse(j); + Object.keys(s).forEach( k => { + SETTINGS[k]=s[k]; + if (SETTINGS_HOOKS[k]) SETTINGS_HOOKS[k](); + } ); + } catch (e) { + console.error("Invalid settings"); + } +} +/// Save settings +function saveSettings() { + localStorage.setItem("settings", JSON.stringify(SETTINGS)); + console.log("Changed settings", SETTINGS); +} +// Link in settings DOM elements +function settingsCheckbox(id, name) { + var setting = document.getElementById(id); + function update() { + setting.checked = SETTINGS[name]; + } + SETTINGS_HOOKS[name] = update; + setting.addEventListener('click', function() { + SETTINGS[name] = setting.checked; + saveSettings(); + }); +} +settingsCheckbox("settings-pretokenise", "pretokenise"); +loadSettings(); + +document.getElementById("defaultsettings").addEventListener("click",event=>{ + SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone + saveSettings(); + loadSettings(); // update all settings + refreshLibrary(); // favourites were in settings +}); + document.getElementById("settime").addEventListener("click",event=>{ Comms.setTime().then(()=>{ showToast("Time set successfully","success"); @@ -487,44 +704,19 @@ document.getElementById("removeall").addEventListener("click",event=>{ }); // Install all default apps in one go document.getElementById("installdefault").addEventListener("click",event=>{ - var defaultApps, appCount; httpGet("defaultapps.json").then(json=>{ - defaultApps = JSON.parse(json); - defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) ); - if (defaultApps.some(x=>x===undefined)) - throw "Not all apps found"; - appCount = defaultApps.length; - return showPrompt("Install Defaults","Remove everything and install default apps?"); - }).then(() => { - return Comms.removeAllApps(); - }).then(()=>{ - Progress.hide({sticky:true}); - appsInstalled = []; - showToast(`Existing apps removed. Installing ${appCount} apps...`); - return new Promise((resolve,reject) => { - function upload() { - var app = defaultApps.shift(); - if (app===undefined) return resolve(); - Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true}); - Comms.uploadApp(app,"skip_reset").then((appJSON) => { - Progress.hide({sticky:true}); - if (appJSON) appsInstalled.push(appJSON); - showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`); - upload(); - }).catch(function() { - Progress.hide({sticky:true}); - reject() - }); - } - upload(); - }); - }).then(()=>{ - return Comms.setTime(); - }).then(()=>{ - showToast("Default apps successfully installed!","success"); - return getInstalledApps(true); + return installMultipleApps(JSON.parse(json), "default"); }).catch(err=>{ Progress.hide({sticky:true}); showToast("App Install failed, "+err,"error"); }); }); + +// Install all favourite apps in one go +document.getElementById("installfavourite").addEventListener("click",event=>{ + var favApps = SETTINGS.favourites; + installMultipleApps(favApps, "favourite").catch(err=>{ + Progress.hide({sticky:true}); + showToast("App Install failed, "+err,"error"); + }); +}); diff --git a/js/ui.js b/js/ui.js index 616a92555..ea6885eac 100644 --- a/js/ui.js +++ b/js/ui.js @@ -86,6 +86,7 @@ function showToast(message, type) { var style = "toast-primary"; if (type=="success") style = "toast-success"; else if (type=="error") style = "toast-error"; + else if (type=="warning") style = "toast-warning"; else if (type!==undefined) console.log("showToast: unknown toast "+type); var toastcontainer = document.getElementById("toastcontainer"); var msgDiv = htmlElement(`
`); diff --git a/js/utils.js b/js/utils.js index 85b6eb0a1..2b6a6e4c3 100644 --- a/js/utils.js +++ b/js/utils.js @@ -8,6 +8,18 @@ function escapeHtml(text) { }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } +// simple glob to regex conversion, only supports "*" and "?" wildcards +function globToRegex(pattern) { + const ESCAPE = '.*+-?^${}()|[]\\'; + const regex = pattern.replace(/./g, c => { + switch (c) { + case '?': return '.'; + case '*': return '.*'; + default: return ESCAPE.includes(c) ? ('\\' + c) : c; + } + }); + return new RegExp('^'+regex+'$'); +} function htmlToArray(collection) { return [].slice.call(collection); } @@ -25,7 +37,10 @@ function httpGet(url) { }); oReq.addEventListener("error", () => reject()); oReq.addEventListener("abort", () => reject()); - oReq.open("GET", url); + oReq.open("GET", url, true); + oReq.onerror = function () { + reject("HTTP Request failed"); + }; oReq.send(); }); } @@ -49,14 +64,14 @@ function getVersionInfo(appListing, appInstalled) { var versionText = ""; var canUpdate = false; function clicky(v) { - return `${v}`; + return `${v}`; } if (!appInstalled) { if (appListing.version) versionText = clicky("v"+appListing.version); } else { - versionText = (appInstalled.version ? ("v"+appInstalled.version) : "Unknown version"); + versionText = (appInstalled.version ? (clicky("v"+appInstalled.version)) : "Unknown version"); if (appListing.version != appInstalled.version) { if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version); canUpdate = true; diff --git a/lib/heatshrink.js b/lib/heatshrink.js new file mode 100644 index 000000000..8e4e9a8df --- /dev/null +++ b/lib/heatshrink.js @@ -0,0 +1,100 @@ +/* + Compiled to JS with Emscripten by Gordon Williams + heatshrink_config.h matches that of Espruino. + Source for conversion at http://github.com/gfwilliams/heatshrink-js +*/ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.heatshrink = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { +/* +Copyright (c) 2013-2015, Scott Vokes +All rights reserved. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +var Module=typeof Module!=="undefined"?Module:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var arguments_=[];var thisProgram="./this.program";var quit_=function(status,toThrow){throw toThrow};var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_HAS_NODE=false;var ENVIRONMENT_IS_SHELL=false;ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_HAS_NODE=typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string";ENVIRONMENT_IS_NODE=ENVIRONMENT_HAS_NODE&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_NODE){scriptDirectory=__dirname+"/";var nodeFS;var nodePath;read_=function shell_read(filename,binary){var ret;ret=tryParseAsDataURI(filename);if(!ret){if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename)}return binary?ret:ret.toString()};readBinary=function readBinary(filename){var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){thisProgram=process["argv"][1].replace(/\\/g,"/")}arguments_=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",abort);quit_=function(status){process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){read_=function shell_read(f){var data=tryParseAsDataURI(f);if(data){return intArrayToString(data)}return read(f)}}readBinary=function readBinary(f){var data;data=tryParseAsDataURI(f);if(data){return data}if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){arguments_=scriptArgs}else if(typeof arguments!="undefined"){arguments_=arguments}if(typeof quit==="function"){quit_=function(status){quit(status)}}if(typeof print!=="undefined"){if(typeof console==="undefined")console={};console.log=print;console.warn=console.error=typeof printErr!=="undefined"?printErr:print}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}read_=function shell_read(url){try{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText}catch(err){var data=tryParseAsDataURI(url);if(data){return intArrayToString(data)}throw err}};if(ENVIRONMENT_IS_WORKER){readBinary=function readBinary(url){try{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}catch(err){var data=tryParseAsDataURI(url);if(data){return data}throw err}}}readAsync=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}var data=tryParseAsDataURI(url);if(data){onload(data.buffer);return}onerror()};xhr.onerror=onerror;xhr.send(null)};setWindowTitle=function(title){document.title=title}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var STACK_ALIGN=16;function dynamicAlloc(size){var ret=HEAP32[DYNAMICTOP_PTR>>2];var end=ret+size+15&-16;if(end>_emscripten_get_heap_size()){abort()}HEAP32[DYNAMICTOP_PTR>>2]=end;return ret}function getNativeTypeSize(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return 4}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0,"getNativeTypeSize invalid bits "+bits+", type "+type);return bits/8}else{return 0}}}}function warnOnce(text){if(!warnOnce.shown)warnOnce.shown={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;err(text)}}var jsCallStartIndex=1;var functionPointers=new Array(0);var funcWrappers={};function dynCall(sig,ptr,args){if(args&&args.length){return Module["dynCall_"+sig].apply(null,[ptr].concat(args))}else{return Module["dynCall_"+sig].call(null,ptr)}}var tempRet0=0;var setTempRet0=function(value){tempRet0=value};var getTempRet0=function(){return tempRet0};var GLOBAL_BASE=8;var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime;if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];function setValue(ptr,value,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":HEAP8[ptr>>0]=value;break;case"i8":HEAP8[ptr>>0]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":tempI64=[value>>>0,(tempDouble=value,+Math_abs(tempDouble)>=+1?tempDouble>+0?(Math_min(+Math_floor(tempDouble/+4294967296),+4294967295)|0)>>>0:~~+Math_ceil((tempDouble-+(~~tempDouble>>>0))/+4294967296)>>>0:0)],HEAP32[ptr>>2]=tempI64[0],HEAP32[ptr+4>>2]=tempI64[1];break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;default:abort("invalid type for setValue: "+type)}}var ABORT=false;var EXITSTATUS=0;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function getCFunc(ident){var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func}function ccall(ident,returnType,argTypes,args,opts){var toC={"string":function(str){var ret=0;if(str!==null&&str!==undefined&&str!==0){var len=(str.length<<2)+1;ret=stackAlloc(len);stringToUTF8(str,ret,len)}return ret},"array":function(arr){var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string")return UTF8ToString(ret);if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i=endIdx))++endPtr;if(endPtr-idx>16&&u8Array.subarray&&UTF8Decoder){return UTF8Decoder.decode(u8Array.subarray(idx,endPtr))}else{var str="";while(idx>10,56320|ch&1023)}}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function stringToUTF8Array(str,outU8Array,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;outU8Array[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;outU8Array[outIdx++]=192|u>>6;outU8Array[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;outU8Array[outIdx++]=224|u>>12;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;outU8Array[outIdx++]=240|u>>18;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}}outU8Array[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127)++len;else if(u<=2047)len+=2;else if(u<=65535)len+=3;else len+=4}return len}var UTF16Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf-16le"):undefined;function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferAndViews(buf){buffer=buf;Module["HEAP8"]=HEAP8=new Int8Array(buf);Module["HEAP16"]=HEAP16=new Int16Array(buf);Module["HEAP32"]=HEAP32=new Int32Array(buf);Module["HEAPU8"]=HEAPU8=new Uint8Array(buf);Module["HEAPU16"]=HEAPU16=new Uint16Array(buf);Module["HEAPU32"]=HEAPU32=new Uint32Array(buf);Module["HEAPF32"]=HEAPF32=new Float32Array(buf);Module["HEAPF64"]=HEAPF64=new Float64Array(buf)}var STACK_BASE=2928,DYNAMIC_BASE=5245808,DYNAMICTOP_PTR=2896;var INITIAL_TOTAL_MEMORY=Module["TOTAL_MEMORY"]||16777216;if(Module["buffer"]){buffer=Module["buffer"]}else{buffer=new ArrayBuffer(INITIAL_TOTAL_MEMORY)}INITIAL_TOTAL_MEMORY=buffer.byteLength;updateGlobalBufferAndViews(buffer);HEAP32[DYNAMICTOP_PTR>>2]=DYNAMIC_BASE;function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Module["dynCall_v"](func)}else{Module["dynCall_vi"](func,callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var Math_abs=Math.abs;var Math_ceil=Math.ceil;var Math_floor=Math.floor;var Math_min=Math.min;var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};var memoryInitializer=null;var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return String.prototype.startsWith?filename.startsWith(dataURIPrefix):filename.indexOf(dataURIPrefix)===0}var tempDouble;var tempI64;memoryInitializer="data:application/octet-stream;base64,AAAAAAAAAAARAAoAERERAAAAAAUAAAAAAAAJAAAAAAsAAAAAAAAAABEADwoREREDCgcAARMJCwsAAAkGCwAACwAGEQAAABEREQAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAARAAoKERERAAoAAAIACQsAAAAJAAsAAAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAADAAAAAAMAAAAAAkMAAAAAAAMAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAA0AAAAEDQAAAAAJDgAAAAAADgAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAPAAAAAA8AAAAACRAAAAAAABAAABAAABIAAAASEhIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgAAABISEgAAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAAoAAAAACgAAAAAJCwAAAAAACwAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAMAAAAAAwAAAAACQwAAAAAAAwAAAwAADAxMjM0NTY3ODlBQkNERUYFAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAEgEAAAABAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAK/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApeXiBDT01QUkVTU0lORyAlZCBieXRlcwoAQXNzZXJ0IGF0IGhlYXRzaHJpbmtfd3JhcHBlci5jOiVkCgBeXiBzdW5rICV6ZAoAXl4gcG9sbGVkICV6ZAoAaW46ICV1IGNvbXByZXNzZWQ6ICV1CgAKXl4gREVDT01QUkVTU0lORyAlZCBieXRlcwoALSsgICAwWDB4AChudWxsKQAtMFgrMFggMFgtMHgrMHggMHgAaW5mAElORgBuYW4ATkFOAC4=";var tempDoublePtr=2912;function demangle(func){return func}function demangleAll(text){var regex=/\b__Z[\w\d_]+/g;return text.replace(regex,function(x){var y=demangle(x);return x===y?x:y+" ["+x+"]"})}function jsStackTrace(){var err=new Error;if(!err.stack){try{throw new Error(0)}catch(e){err=e}if(!err.stack){return"(no stack trace available)"}}return err.stack.toString()}function stackTrace(){var js=jsStackTrace();if(Module["extraStackTrace"])js+="\n"+Module["extraStackTrace"]();return demangleAll(js)}function flush_NO_FILESYSTEM(){var fflush=Module["_fflush"];if(fflush)fflush(0);var buffers=SYSCALLS.buffers;if(buffers[1].length)SYSCALLS.printChar(1,10);if(buffers[2].length)SYSCALLS.printChar(2,10)}var PATH={splitPath:function(filename){var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:function(parts,allowAboveRoot){var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:function(path){var isAbsolute=path.charAt(0)==="/",trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(function(p){return!!p}),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:function(path){var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:function(path){if(path==="/")return"/";var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},extname:function(path){return PATH.splitPath(path)[3]},join:function(){var paths=Array.prototype.slice.call(arguments,0);return PATH.normalize(paths.join("/"))},join2:function(l,r){return PATH.normalize(l+"/"+r)}};var SYSCALLS={buffers:[null,[],[]],printChar:function(stream,curr){var buffer=SYSCALLS.buffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}},varargs:0,get:function(varargs){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(){var ret=UTF8ToString(SYSCALLS.get());return ret},get64:function(){var low=SYSCALLS.get(),high=SYSCALLS.get();return low},getZero:function(){SYSCALLS.get()}};function _fd_write(stream,iov,iovcnt,pnum){try{var num=0;for(var i=0;i>2];var len=HEAP32[iov+(i*8+4)>>2];for(var j=0;j>2]=num;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function ___wasi_fd_write(){return _fd_write.apply(null,arguments)}function _emscripten_get_heap_size(){return HEAP8.length}function _emscripten_memcpy_big(dest,src,num){HEAPU8.set(HEAPU8.subarray(src,src+num),dest)}function ___setErrNo(value){if(Module["___errno_location"])HEAP32[Module["___errno_location"]()>>2]=value;return value}function abortOnCannotGrowMemory(requestedSize){abort("OOM")}function _emscripten_resize_heap(requestedSize){abortOnCannotGrowMemory(requestedSize)}var ASSERTIONS=false;function intArrayToString(array){var ret=[];for(var i=0;i255){if(ASSERTIONS){assert(false,"Character code "+chr+" ("+String.fromCharCode(chr)+") at offset "+i+" not in 0x00-0xFF.")}chr&=255}ret.push(String.fromCharCode(chr))}return ret.join("")}var decodeBase64=typeof atob==="function"?atob:function(input){var keyStr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{enc1=keyStr.indexOf(input.charAt(i++));enc2=keyStr.indexOf(input.charAt(i++));enc3=keyStr.indexOf(input.charAt(i++));enc4=keyStr.indexOf(input.charAt(i++));chr1=enc1<<2|enc2>>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}while(i>2]=0;n=(f|0)>1;if(n){c[g>>2]=b;ab(888,g)|0}l=(d|0)==0;g=0;h=0;a:while(1){if(h>>>0>=b>>>0){h=26;break}if((X(p,a+h|0,b-h|0,o)|0)<=-1){h=6;break}i=c[o>>2]|0;h=i+h|0;if(n){c[r>>2]=i;ab(949,r)|0}k=(h|0)==(b|0);if(k?(ma(p)|0)!=1:0){h=12;break}b:while(1){if(l)j=Z(p,m,64,o)|0;else j=Z(p,d+g|0,e-g|0,o)|0;if((j|0)<=-1){h=17;break a}i=c[o>>2]|0;g=i+g|0;if(n){c[q>>2]=i;ab(962,q)|0}switch(j|0){case 1:break;case 0:break b;default:{h=21;break a}}}if(k?ma(p)|0:0){h=25;break}}if((h|0)==6){c[s>>2]=21;ab(914,s)|0;g=0}else if((h|0)==12){c[x>>2]=25;ab(914,x)|0;g=0}else if((h|0)==17){c[t>>2]=36;ab(914,t)|0;g=0}else if((h|0)==21){c[u>>2]=40;ab(914,u)|0;g=0}else if((h|0)==25){c[v>>2]=42;ab(914,v)|0;g=0}else if((h|0)==26)if((f|0)>0){c[w>>2]=b;c[w+4>>2]=g;ab(977,w)|0}I=y;return g|0}function V(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0;y=I;I=I+448|0;w=y+128|0;v=y+120|0;u=y+112|0;q=y+104|0;t=y+96|0;x=y+88|0;r=y+80|0;s=y+72|0;g=y+64|0;p=y+140|0;o=y+136|0;m=y;na(p);c[o>>2]=0;n=(f|0)>1;if(n){c[g>>2]=b;ab(1e3,g)|0}l=(d|0)==0;g=0;h=0;a:while(1){if(h>>>0>=b>>>0){h=27;break}if((oa(p,a+h|0,b-h|0,o)|0)<=-1){h=6;break}i=c[o>>2]|0;h=i+h|0;if(n){c[r>>2]=i;ab(949,r)|0}k=(h|0)==(b|0);if(k?(za(p)|0)!=1:0){h=12;break}do{if(l)j=pa(p,m,64,o)|0;else j=pa(p,d+g|0,e-g|0,o)|0;if((j|0)<=-1){h=17;break a}i=c[o>>2]|0;g=i+g|0;if(n){c[q>>2]=i;ab(962,q)|0;i=c[o>>2]|0}}while((j|0)==1&(i|0)!=0);if(j|0){h=22;break}if(k?za(p)|0:0){h=26;break}}if((h|0)==6){c[s>>2]=63;ab(914,s)|0;g=0}else if((h|0)==12){c[x>>2]=67;ab(914,x)|0;g=0}else if((h|0)==17){c[t>>2]=78;ab(914,t)|0;g=0}else if((h|0)==22){c[u>>2]=82;ab(914,u)|0;g=0}else if((h|0)==26){c[v>>2]=84;ab(914,v)|0;g=0}else if((h|0)==27)if((f|0)>0){c[w>>2]=b;c[w+4>>2]=g;ab(977,w)|0}I=y;return g|0}function W(c){c=c|0;ob(c+15|0,0,512)|0;b[c>>1]=0;a[c+12>>0]=0;b[c+2>>1]=0;a[c+11>>0]=0;a[c+14>>0]=-128;a[c+13>>0]=0;b[c+4>>1]=0;b[c+8>>1]=0;a[c+10>>0]=0;return}function X(d,e,f,g){d=d|0;e=e|0;f=f|0;g=g|0;var h=0,i=0,j=0,k=0;if(!((d|0)==0|(e|0)==0|(g|0)==0))if((Y(d)|0)==0?(h=d+12|0,(a[h>>0]|0)==0):0){j=b[d>>1]|0;i=256-j&65535;k=i>>>0>>0?i:f;mb((j+256&65535)+(d+15)|0,e|0,k|0)|0;c[g>>2]=k;b[d>>1]=k+(j&65535);if(i>>>0>f>>>0)d=0;else{a[h>>0]=1;d=0}}else d=-2;else d=-1;return d|0}function Y(b){b=b|0;return a[b+11>>0]&1|0}function Z(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0;k=I;I=I+16|0;i=k;if(!((b|0)==0|(d|0)==0|(f|0)==0))if(!e)d=-2;else{c[f>>2]=0;c[i>>2]=d;c[i+4>>2]=e;c[i+8>>2]=f;h=b+12|0;g=a[h>>0]|0;a:while(1){switch(g<<24>>24){case 9:case 0:{d=0;j=15;break a}case 8:{j=11;break a}case 1:{d=2;break}case 2:{d=(_(b)|0)&255;break}case 3:{d=($(b,i)|0)&255;break}case 4:{d=(aa(b,i)|0)&255;break}case 5:{d=(ba(b,i)|0)&255;break}case 6:{d=(ca(b,i)|0)&255;break}case 7:{da(b);d=0;break}default:{d=-2;break a}}a[h>>0]=d;if(d<<24>>24==g<<24>>24?(c[f>>2]|0)==(e|0):0){d=1;j=15;break}g=d}if((j|0)==11){a[h>>0]=ea(b,i)|0;d=0}}else d=-1;I=k;return d|0}function _(a){a=a|0;var c=0,d=0,f=0,g=0,h=0,i=0,j=0;j=I;I=I+16|0;h=j;i=a+2|0;c=b[i>>1]|0;g=(Y(a)|0)!=0;d=c&65535;f=e[a>>1]|0;if((f-(g?1:64)|0)<(d|0))c=g?8:7;else{g=f-d|0;b[h>>1]=0;c=la(a,(c+256&65535)+65280&65535,d+256&65535,((g|0)<64?g:64)&65535,h)|0;if(c<<16>>16==-1){b[i>>1]=(b[i>>1]|0)+1<<16>>16;c=0}else{b[a+6>>1]=c;c=b[h>>1]|0}b[a+4>>1]=c;c=3}I=j;return c|0}function $(c,d){c=c|0;d=d|0;do if(fa(d)|0)if(!(b[c+4>>1]|0)){ka(c,d,1);c=4;break}else{ka(c,d,0);b[c+8>>1]=(e[c+6>>1]|0)+65535;a[c+10>>0]=8;c=5;break}else c=3;while(0);return c|0}function aa(a,b){a=a|0;b=b|0;if(!(fa(b)|0))a=4;else{ja(a,b);a=2}return a|0}function ba(c,d){c=c|0;d=d|0;if((fa(d)|0)!=0?(ha(c,d)|0)<<24>>24==0:0){b[c+8>>1]=(e[c+4>>1]|0)+65535;a[c+10>>0]=6;c=6}else c=5;return c|0}function ca(a,c){a=a|0;c=c|0;if((fa(c)|0)!=0?(ha(a,c)|0)<<24>>24==0:0){c=a+4|0;a=a+2|0;b[a>>1]=(e[a>>1]|0)+(e[c>>1]|0);b[c>>1]=0;a=2}else a=6;return a|0}function da(a){a=a|0;ga(a);return}function ea(b,d){b=b|0;d=d|0;var e=0,f=0;if((a[b+14>>0]|0)!=-128)if(!(fa(d)|0))b=8;else{f=a[b+13>>0]|0;e=c[d>>2]|0;d=c[d+8>>2]|0;b=c[d>>2]|0;c[d>>2]=b+1;a[e+b>>0]=f;b=9}else b=9;return b|0}function fa(a){a=a|0;return (c[c[a+8>>2]>>2]|0)>>>0<(c[a+4>>2]|0)>>>0|0}function ga(a){a=a|0;var c=0,d=0,f=0;d=a+2|0;f=256-(b[d>>1]|0)<<16>>16;c=256-(f&65535)|0;nb(a+15|0,a+15+c|0,f+256&65535|0)|0;b[d>>1]=0;b[a>>1]=(e[a>>1]|0)-c;return}function ha(c,f){c=c|0;f=f|0;var g=0,h=0,i=0,j=0;i=c+10|0;g=a[i>>0]|0;if((g&255)<=8)if(!(g<<24>>24))g=0;else{h=b[c+8>>1]&255;j=4}else{h=(e[c+8>>1]|0)>>>((g&255)+-8|0)&255;g=8;j=4}if((j|0)==4){ia(c,g,h,f);a[i>>0]=(d[i>>0]|0)-(g&255)}return g|0}function ia(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0;h=d&255;j=b+14|0;if(d<<24>>24==8?(a[j>>0]|0)==-128:0){j=c[f>>2]|0;i=c[f+8>>2]|0;f=c[i>>2]|0;c[i>>2]=f+1;a[j+f>>0]=e}else g=4;a:do if((g|0)==4){i=e&255;g=b+13|0;b=f+8|0;e=h;while(1){d=e+-1|0;if((e|0)<=0)break a;e=a[j>>0]|0;if(1<>0]=a[g>>0]|e;h=(e&255)>>>1;a[j>>0]=h;if(!(h<<24>>24)){a[j>>0]=-128;k=a[g>>0]|0;e=c[f>>2]|0;l=c[b>>2]|0;h=c[l>>2]|0;c[l>>2]=h+1;a[e+h>>0]=k;a[g>>0]=0}e=d}}while(0);return}function ja(c,d){c=c|0;d=d|0;ia(c,8,a[((b[c+2>>1]|0)+255&65535)+(c+15)>>0]|0,d);return}function ka(a,b,c){a=a|0;b=b|0;c=c|0;ia(a,1,c,b);return}function la(c,d,e,f,g){c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;var h=0,i=0,j=0,k=0,l=0,m=0,n=0;n=e&65535;l=c+15+n|0;h=-1;e=0;m=n+65535&65535;while(1){if(m<<16>>16>16)break;i=(m<<16>>16)+(c+15)|0;k=e&65535;if((a[i+k>>0]|0)==(a[l+k>>0]|0)?(a[i>>0]|0)==(a[l>>0]|0):0){k=1;while(1){j=k&65535;if((k&65535)>=(f&65535))break;if((a[i+j>>0]|0)!=(a[l+j>>0]|0))break;k=k+1<<16>>16}if((k&65535)>(e&65535))if(k<<16>>16==f<<16>>16){h=m;e=f;break}else{h=m;e=k}}m=m+-1<<16>>16}if((e&65535)>1){b[g>>1]=e;e=n-(h&65535)&65535}else e=-1;return e|0}function ma(b){b=b|0;var c=0;if(!b)b=-1;else{c=b+11|0;a[c>>0]=a[c>>0]|1;c=b+12|0;b=a[c>>0]|0;if(!(b<<24>>24)){a[c>>0]=1;b=1}b=b<<24>>24!=9&1}return b|0}function na(a){a=a|0;ob(a|0,0,301)|0;return}function oa(a,d,f,g){a=a|0;d=d|0;f=f|0;g=g|0;var h=0,i=0,j=0;if((a|0)==0|(d|0)==0|(g|0)==0)f=-1;else{i=e[a>>1]|0;j=32-i|0;h=j>>>0>>0?j:f;if(!j){f=1;h=0}else{mb(a+13+i|0,d|0,h|0)|0;b[a>>1]=h+i;f=0}c[g>>2]=h}return f|0}function pa(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0;k=I;I=I+16|0;i=k;if((b|0)==0|(d|0)==0|(f|0)==0)d=-1;else{c[f>>2]=0;c[i>>2]=d;c[i+4>>2]=e;c[i+8>>2]=f;h=b+10|0;d=a[h>>0]|0;a:while(1){switch(d<<24>>24){case 0:{g=qa(b)|0;break}case 1:{g=ra(b,i)|0;break}case 2:{g=sa(b)|0;break}case 3:{g=ta(b)|0;break}case 4:{g=ua(b)|0;break}case 5:{g=va(b)|0;break}case 6:{g=wa(b,i)|0;break}default:{d=-2;break a}}l=d;d=g&255;a[h>>0]=d;if(l<<24>>24==d<<24>>24){j=12;break}}if((j|0)==12)d=(c[f>>2]|0)==(e|0)&1}I=k;return d|0}function qa(a){a=a|0;switch((ya(a,1)|0)<<16>>16){case -1:{a=0;break}case 0:{b[a+6>>1]=0;a=3;break}default:a=1}return a|0}function ra(d,e){d=d|0;e=e|0;var f=0,g=0,h=0;if((c[c[e+8>>2]>>2]|0)>>>0<(c[e+4>>2]|0)>>>0?(f=ya(d,8)|0,f<<16>>16!=-1):0){f=f&255;h=d+8|0;g=b[h>>1]|0;b[h>>1]=g+1<<16>>16;a[d+45+(g&255)>>0]=f;xa(e,f);f=0}else f=1;return f|0}function sa(a){a=a|0;var c=0;c=ya(a,0)|0;if(c<<16>>16==-1)c=2;else{b[a+6>>1]=(c&65535)<<8;c=3}return c|0}function ta(a){a=a|0;var c=0,d=0;c=ya(a,8)|0;if(c<<16>>16==-1)c=3;else{d=a+6|0;b[d>>1]=(b[d>>1]|c)+1<<16>>16;b[a+4>>1]=0;c=5}return c|0}function ua(a){a=a|0;var c=0;c=ya(a,-2)|0;if(c<<16>>16==-1)c=4;else{b[a+4>>1]=(c&65535)<<8;c=5}return c|0}function va(a){a=a|0;var c=0;c=ya(a,6)|0;if(c<<16>>16==-1)c=5;else{a=a+4|0;b[a>>1]=(b[a>>1]|c)+1<<16>>16;c=6}return c|0}function wa(d,f){d=d|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;g=(c[f+4>>2]|0)-(c[c[f+8>>2]>>2]|0)|0;if(g){k=d+4|0;j=e[k>>1]|0;j=g>>>0>j>>>0?j:g;h=d+45|0;i=d+8|0;g=e[d+6>>1]|0;d=0;while(1){if(d>>>0>=j>>>0)break;n=a[h+((e[i>>1]|0)-g&255)>>0]|0;xa(f,n);m=b[i>>1]|0;a[h+(m&255)>>0]=n;b[i>>1]=m+1<<16>>16;d=d+1|0}n=(e[k>>1]|0)-j|0;b[k>>1]=n;if(!(n&65535))g=0;else l=6}else l=6;if((l|0)==6)g=6;return g|0}function xa(b,d){b=b|0;d=d|0;var e=0,f=0;e=c[b>>2]|0;f=c[b+8>>2]|0;b=c[f>>2]|0;c[f>>2]=b+1;a[e+b>>0]=d;return}function ya(c,e){c=c|0;e=e|0;var f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;m=e&255;a:do if((e&255)>15)e=-1;else{e=b[c>>1]|0;j=c+12|0;if(e<<16>>16==0?(1<(d[j>>0]|0|0):0){e=-1;break}k=c+11|0;l=c+2|0;g=e;e=0;i=0;while(1){if(i>>>0>=m>>>0)break a;f=a[j>>0]|0;if(!(f<<24>>24)){if(!(g<<16>>16)){e=-1;break a}h=b[l>>1]|0;f=h+1<<16>>16;b[l>>1]=f;h=a[(h&65535)+(c+13)>>0]|0;a[k>>0]=h;if(f<<16>>16==g<<16>>16){b[l>>1]=0;b[c>>1]=0;g=0}a[j>>0]=-128;f=-128}else h=a[k>>0]|0;a[j>>0]=(f&255)>>>1;e=((e&65535)<<1|(f&h)<<24>>24!=0)&65535;i=i+1|0}}while(0);return e|0}function za(c){c=c|0;a:do if(!c)c=-1;else switch(a[c+10>>0]|0){case 0:{c=(b[c>>1]|0)!=0&1;break a}case 4:case 5:case 2:case 3:{c=(b[c>>1]|0)!=0&1;break a}case 1:{c=(b[c>>1]|0)!=0&1;break a}default:{c=1;break a}}while(0);return c|0}function Aa(a){a=a|0;return 0}function Ba(a,b,d){a=a|0;b=b|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;l=I;I=I+32|0;g=l;i=l+16|0;j=a+28|0;f=c[j>>2]|0;c[g>>2]=f;k=a+20|0;f=(c[k>>2]|0)-f|0;c[g+4>>2]=f;c[g+8>>2]=b;c[g+12>>2]=d;e=a+60|0;h=2;f=f+d|0;while(1){if(!((x(c[e>>2]|0,g|0,h|0,i|0)|0)<<16>>16))b=c[i>>2]|0;else{c[i>>2]=-1;b=-1}if((f|0)==(b|0)){b=6;break}if((b|0)<0){b=8;break}p=c[g+4>>2]|0;m=b>>>0>p>>>0;n=m?g+8|0:g;p=b-(m?p:0)|0;c[n>>2]=(c[n>>2]|0)+p;o=n+4|0;c[o>>2]=(c[o>>2]|0)-p;g=n;h=h+(m<<31>>31)|0;f=f-b|0}if((b|0)==6){p=c[a+44>>2]|0;c[a+16>>2]=p+(c[a+48>>2]|0);c[j>>2]=p;c[k>>2]=p}else if((b|0)==8){c[a+16>>2]=0;c[j>>2]=0;c[k>>2]=0;c[a>>2]=c[a>>2]|32;if((h|0)==2)d=0;else d=d-(c[g+4>>2]|0)|0}I=l;return d|0}function Ca(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;u(0);return 0}function Da(){return 2128}function Ea(a,b,c){a=a|0;b=b|0;c=c|0;return Ha(a,b,c,1,1)|0}function Fa(b,e,f,g,h,i){b=b|0;e=+e;f=f|0;g=g|0;h=h|0;i=i|0;var j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,s=0.0,t=0,u=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0;H=I;I=I+560|0;m=H+32|0;u=H+536|0;G=H;F=G;l=H+540|0;c[u>>2]=0;E=l+12|0;_a(e)|0;j=v()|0;if((j|0)<0){e=-e;_a(e)|0;j=v()|0;D=1;B=1045}else{D=(h&2049|0)!=0&1;B=(h&2048|0)==0?((h&1|0)==0?1046:1051):1048}do if(0==0&(j&2146435072|0)==2146435072){G=(i&32|0)!=0;j=D+3|0;Ta(b,32,f,j,h&-65537);La(b,B,D);La(b,e!=e|0.0!=0.0?(G?1072:1076):G?1064:1068,3);Ta(b,32,f,j,h^8192)}else{s=+$a(e,u)*2.0;j=s!=0.0;if(j)c[u>>2]=(c[u>>2]|0)+-1;x=i|32;if((x|0)==97){o=i&32;q=(o|0)==0?B:B+9|0;p=D|2;j=12-g|0;do if(!(g>>>0>11|(j|0)==0)){e=8.0;do{j=j+-1|0;e=e*16.0}while((j|0)!=0);if((a[q>>0]|0)==45){e=-(e+(-s-e));break}else{e=s+e-e;break}}else e=s;while(0);k=c[u>>2]|0;j=(k|0)<0?0-k|0:k;j=Ra(j,((j|0)<0)<<31>>31,E)|0;if((j|0)==(E|0)){j=l+11|0;a[j>>0]=48}a[j+-1>>0]=(k>>31&2)+43;n=j+-2|0;a[n>>0]=i+15;k=(g|0)<1;l=(h&8|0)==0;j=G;while(1){D=~~e;m=j+1|0;a[j>>0]=o|d[480+D>>0];e=(e-+(D|0))*16.0;if((m-F|0)==1?!(l&(k&e==0.0)):0){a[m>>0]=46;m=j+2|0}if(!(e!=0.0))break;else j=m}if((g|0)!=0?(-2-F+m|0)<(g|0):0){k=E;l=n;j=g+2+k-l|0}else{k=E;l=n;j=k-F-l+m|0}E=j+p|0;Ta(b,32,f,E,h);La(b,q,p);Ta(b,48,f,E,h^65536);F=m-F|0;La(b,G,F);G=k-l|0;Ta(b,48,j-(F+G)|0,0,0);La(b,n,G);Ta(b,32,f,E,h^8192);j=E;break}k=(g|0)<0?6:g;if(j){l=(c[u>>2]|0)+-28|0;c[u>>2]=l;e=s*268435456.0}else{l=c[u>>2]|0;e=s}C=(l|0)<0?m:m+288|0;m=C;do{z=~~e>>>0;c[m>>2]=z;m=m+4|0;e=(e-+(z>>>0))*1.0e9}while(e!=0.0);z=C;if((l|0)>0){j=C;do{o=(l|0)<29?l:29;l=m+-4|0;if(l>>>0>=j>>>0){n=0;do{t=lb(c[l>>2]|0,0,o|0)|0;t=fb(t|0,v()|0,n|0,0)|0;w=v()|0;n=jb(t|0,w|0,1e9,0)|0;y=eb(n|0,v()|0,1e9,0)|0;y=gb(t|0,w|0,y|0,v()|0)|0;v()|0;c[l>>2]=y;l=l+-4|0}while(l>>>0>=j>>>0);if(n){j=j+-4|0;c[j>>2]=n}}a:do if(m>>>0>j>>>0)while(1){l=m+-4|0;if(c[l>>2]|0)break a;if(l>>>0>j>>>0)m=l;else{m=l;break}}while(0);l=(c[u>>2]|0)-o|0;c[u>>2]=l}while((l|0)>0)}else j=C;if((l|0)<0){g=((k+25|0)/9|0)+1|0;t=(x|0)==102;do{q=0-l|0;q=(q|0)<9?q:9;if(j>>>0>>0){o=(1<>>q;p=0;l=j;do{y=c[l>>2]|0;c[l>>2]=(y>>>q)+p;p=r(y&o,n)|0;l=l+4|0}while(l>>>0>>0);j=(c[j>>2]|0)==0?j+4|0:j;if(p){c[m>>2]=p;m=m+4|0}}else j=(c[j>>2]|0)==0?j+4|0:j;l=t?C:j;m=(m-l>>2|0)>(g|0)?l+(g<<2)|0:m;l=(c[u>>2]|0)+q|0;c[u>>2]=l}while((l|0)<0);t=m}else t=m;if(j>>>0>>0){l=(z-j>>2)*9|0;n=c[j>>2]|0;if(n>>>0>=10){m=10;do{m=m*10|0;l=l+1|0}while(n>>>0>=m>>>0)}}else l=0;u=(x|0)==103;w=(k|0)!=0;m=k-((x|0)==102?0:l)+((w&u)<<31>>31)|0;if((m|0)<(((t-z>>2)*9|0)+-9|0)){y=m+9216|0;m=(y|0)/9|0;g=C+4+(m+-1024<<2)|0;m=y-(m*9|0)|0;if((m|0)<8){n=10;while(1){n=n*10|0;if((m|0)<7)m=m+1|0;else break}}else n=10;p=c[g>>2]|0;m=(p>>>0)/(n>>>0)|0;q=p-(r(m,n)|0)|0;o=(g+4|0)==(t|0);if(!(o&(q|0)==0)){s=(m&1|0)==0?9007199254740992.0:9007199254740994.0;y=n>>>1;e=q>>>0>>0?.5:o&(q|0)==(y|0)?1.0:1.5;if(D){y=(a[B>>0]|0)==45;s=y?-s:s;e=y?-e:e}m=p-q|0;c[g>>2]=m;if(s+e!=s){y=m+n|0;c[g>>2]=y;if(y>>>0>999999999){l=g;while(1){m=l+-4|0;c[l>>2]=0;if(m>>>0>>0){j=j+-4|0;c[j>>2]=0}y=(c[m>>2]|0)+1|0;c[m>>2]=y;if(y>>>0>999999999)l=m;else break}}else m=g;l=(z-j>>2)*9|0;o=c[j>>2]|0;if(o>>>0>=10){n=10;do{n=n*10|0;l=l+1|0}while(o>>>0>=n>>>0)}}else m=g}else m=g;x=m+4|0;y=j;j=t>>>0>x>>>0?x:t}else{y=j;j=t}q=0-l|0;b:do if(j>>>0>y>>>0)while(1){m=j+-4|0;if(c[m>>2]|0){t=1;x=j;break b}if(m>>>0>y>>>0)j=m;else{t=0;x=m;break}}else{t=0;x=j}while(0);do if(u){j=k+((w^1)&1)|0;if((j|0)>(l|0)&(l|0)>-5){k=j+-1-l|0;n=i+-1|0}else{k=j+-1|0;n=i+-2|0}if(!(h&8)){if(t?(A=c[x+-4>>2]|0,(A|0)!=0):0)if(!((A>>>0)%10|0)){j=10;m=0;do{j=j*10|0;m=m+1|0}while(!((A>>>0)%(j>>>0)|0|0))}else m=0;else m=9;j=((x-z>>2)*9|0)+-9|0;if((n|32|0)==102){i=j-m|0;i=(i|0)>0?i:0;k=(k|0)<(i|0)?k:i;break}else{i=j+l-m|0;i=(i|0)>0?i:0;k=(k|0)<(i|0)?k:i;break}}}else n=i;while(0);g=(k|0)!=0;o=g?1:h>>>3&1;p=(n|32|0)==102;if(p){w=0;j=(l|0)>0?l:0}else{j=(l|0)<0?q:l;j=Ra(j,((j|0)<0)<<31>>31,E)|0;m=E;if((m-j|0)<2)do{j=j+-1|0;a[j>>0]=48}while((m-j|0)<2);a[j+-1>>0]=(l>>31&2)+43;j=j+-2|0;a[j>>0]=n;w=j;j=m-j|0}j=D+1+k+o+j|0;Ta(b,32,f,j,h);La(b,B,D);Ta(b,48,f,j,h^65536);if(p){o=y>>>0>C>>>0?C:y;q=G+9|0;p=q;n=G+8|0;m=o;do{l=Ra(c[m>>2]|0,0,q)|0;if((m|0)==(o|0)){if((l|0)==(q|0)){a[n>>0]=48;l=n}}else if(l>>>0>G>>>0){ob(G|0,48,l-F|0)|0;do l=l+-1|0;while(l>>>0>G>>>0)}La(b,l,p-l|0);m=m+4|0}while(m>>>0<=C>>>0);if(!((h&8|0)==0&(g^1)))La(b,1080,1);if(m>>>0>>0&(k|0)>0)while(1){l=Ra(c[m>>2]|0,0,q)|0;if(l>>>0>G>>>0){ob(G|0,48,l-F|0)|0;do l=l+-1|0;while(l>>>0>G>>>0)}La(b,l,(k|0)<9?k:9);m=m+4|0;l=k+-9|0;if(!(m>>>0>>0&(k|0)>9)){k=l;break}else k=l}Ta(b,48,k+9|0,9,0)}else{g=t?x:y+4|0;if(y>>>0>>0&(k|0)>-1){q=G+9|0;u=(h&8|0)==0;t=q;n=0-F|0;p=G+8|0;o=y;do{l=Ra(c[o>>2]|0,0,q)|0;if((l|0)==(q|0)){a[p>>0]=48;l=p}do if((o|0)==(y|0)){m=l+1|0;La(b,l,1);if(u&(k|0)<1){l=m;break}La(b,1080,1);l=m}else{if(l>>>0<=G>>>0)break;ob(G|0,48,l+n|0)|0;do l=l+-1|0;while(l>>>0>G>>>0)}while(0);F=t-l|0;La(b,l,(k|0)>(F|0)?F:k);k=k-F|0;o=o+4|0}while(o>>>0>>0&(k|0)>-1)}Ta(b,48,k+18|0,18,0);La(b,w,E-w|0)}Ta(b,32,f,j,h^8192)}while(0);I=H;return ((j|0)<(f|0)?f:j)|0}function Ga(a,b){a=a|0;b=b|0;var d=0.0,e=0;e=(c[b>>2]|0)+(8-1)&~(8-1);d=+g[e>>3];c[b>>2]=e+8;g[a>>3]=d;return}function Ha(b,d,e,f,g){b=b|0;d=d|0;e=e|0;f=f|0;g=g|0;var h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;t=I;I=I+224|0;o=t+208|0;s=t+160|0;r=t+80|0;q=t;h=s;i=h+40|0;do{c[h>>2]=0;h=h+4|0}while((h|0)<(i|0));c[o>>2]=c[e>>2];if((Ia(0,d,o,r,s,f,g)|0)<0)e=-1;else{if((c[b+76>>2]|0)>-1)p=Ja(b)|0;else p=0;e=c[b>>2]|0;n=e&32;if((a[b+74>>0]|0)<1)c[b>>2]=e&-33;j=b+48|0;if(!(c[j>>2]|0)){i=b+44|0;e=c[i>>2]|0;c[i>>2]=q;k=b+28|0;c[k>>2]=q;m=b+20|0;c[m>>2]=q;c[j>>2]=80;l=b+16|0;c[l>>2]=q+80;h=Ia(b,d,o,r,s,f,g)|0;if(e){N[c[b+36>>2]&1](b,0,0)|0;h=(c[m>>2]|0)==0?-1:h;c[i>>2]=e;c[j>>2]=0;c[l>>2]=0;c[k>>2]=0;c[m>>2]=0}}else h=Ia(b,d,o,r,s,f,g)|0;e=c[b>>2]|0;c[b>>2]=e|n;if(p|0)Ka(b);e=(e&32|0)==0?h:-1}I=t;return e|0}function Ia(d,e,f,h,i,j,k){d=d|0;e=e|0;f=f|0;h=h|0;i=i|0;j=j|0;k=k|0;var l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,J=0,K=0,L=0;K=I;I=I+64|0;H=K+56|0;F=K+40|0;z=K;J=K+48|0;G=K+60|0;c[H>>2]=e;C=(d|0)!=0;y=z+40|0;B=y;z=z+39|0;A=J+4|0;e=0;l=0;o=0;a:while(1){do{do if((e|0)>-1)if((l|0)>(2147483647-e|0)){c[(Da()|0)>>2]=75;e=-1;break}else{e=l+e|0;break}while(0);s=c[H>>2]|0;l=a[s>>0]|0;if(!(l<<24>>24)){x=92;break a}m=s;b:while(1){switch(l<<24>>24){case 37:{x=10;break b}case 0:{l=m;break b}default:{}}w=m+1|0;c[H>>2]=w;l=a[w>>0]|0;m=w}c:do if((x|0)==10){x=0;n=m;l=m;do{if((a[n+1>>0]|0)!=37)break c;l=l+1|0;n=n+2|0;c[H>>2]=n}while((a[n>>0]|0)==37)}while(0);l=l-s|0;if(C)La(d,s,l)}while((l|0)!=0);w=(Ma(a[(c[H>>2]|0)+1>>0]|0)|0)==0;l=c[H>>2]|0;if(!w?(a[l+2>>0]|0)==36:0){m=3;q=(a[l+1>>0]|0)+-48|0;p=1}else{m=1;q=-1;p=o}m=l+m|0;c[H>>2]=m;l=a[m>>0]|0;n=(l<<24>>24)+-32|0;if(n>>>0>31|(1<>2]=m;l=a[m>>0]|0;n=(l<<24>>24)+-32|0;if(n>>>0>31|(1<>24==42){if((Ma(a[m+1>>0]|0)|0)!=0?(D=c[H>>2]|0,(a[D+2>>0]|0)==36):0){l=D+1|0;c[i+((a[l>>0]|0)+-48<<2)>>2]=10;n=1;m=D+3|0;l=c[h+((a[l>>0]|0)+-48<<3)>>2]|0}else{if(p|0){e=-1;break}if(C){w=(c[f>>2]|0)+(4-1)&~(4-1);l=c[w>>2]|0;c[f>>2]=w+4}else l=0;n=0;m=(c[H>>2]|0)+1|0}c[H>>2]=m;u=(l|0)<0;o=u?o|8192:o;w=n;u=u?0-l|0:l}else{l=Na(H)|0;if((l|0)<0){e=-1;break}m=c[H>>2]|0;w=p;u=l}do if((a[m>>0]|0)==46){l=m+1|0;if((a[l>>0]|0)!=42){c[H>>2]=l;t=Na(H)|0;l=c[H>>2]|0;break}if(Ma(a[m+2>>0]|0)|0?(E=c[H>>2]|0,(a[E+3>>0]|0)==36):0){t=E+2|0;c[i+((a[t>>0]|0)+-48<<2)>>2]=10;t=c[h+((a[t>>0]|0)+-48<<3)>>2]|0;l=E+4|0;c[H>>2]=l;break}if(w|0){e=-1;break a}if(C){t=(c[f>>2]|0)+(4-1)&~(4-1);m=c[t>>2]|0;c[f>>2]=t+4}else m=0;l=(c[H>>2]|0)+2|0;c[H>>2]=l;t=m}else{l=m;t=-1}while(0);r=0;while(1){if(((a[l>>0]|0)+-65|0)>>>0>57){e=-1;break a}m=l;l=l+1|0;c[H>>2]=l;m=a[(a[m>>0]|0)+-65+(16+(r*58|0))>>0]|0;p=m&255;if((p+-1|0)>>>0>=8)break;else r=p}if(!(m<<24>>24)){e=-1;break}n=(q|0)>-1;do if(m<<24>>24==19)if(n){e=-1;break a}else x=54;else{if(n){c[i+(q<<2)>>2]=p;p=h+(q<<3)|0;q=c[p+4>>2]|0;x=F;c[x>>2]=c[p>>2];c[x+4>>2]=q;x=54;break}if(!C){e=0;break a}Oa(F,p,f,k);l=c[H>>2]|0;x=55}while(0);if((x|0)==54){x=0;if(C)x=55;else l=0}d:do if((x|0)==55){x=0;n=a[l+-1>>0]|0;n=(r|0)!=0&(n&15|0)==3?n&-33:n;l=o&-65537;q=(o&8192|0)==0?o:l;e:do switch(n|0){case 110:switch((r&255)<<24>>24){case 0:{c[c[F>>2]>>2]=e;l=0;break d}case 1:{c[c[F>>2]>>2]=e;l=0;break d}case 2:{l=c[F>>2]|0;c[l>>2]=e;c[l+4>>2]=((e|0)<0)<<31>>31;l=0;break d}case 3:{b[c[F>>2]>>1]=e;l=0;break d}case 4:{a[c[F>>2]>>0]=e;l=0;break d}case 6:{c[c[F>>2]>>2]=e;l=0;break d}case 7:{l=c[F>>2]|0;c[l>>2]=e;c[l+4>>2]=((e|0)<0)<<31>>31;l=0;break d}default:{l=0;break d}}case 112:{l=q|8;m=t>>>0>8?t:8;n=120;x=67;break}case 88:case 120:{l=q;m=t;x=67;break}case 111:{o=F;o=Qa(c[o>>2]|0,c[o+4>>2]|0,y)|0;m=B-o|0;l=q;m=(q&8|0)==0|(t|0)>(m|0)?t:m+1|0;r=0;p=1028;x=73;break}case 105:case 100:{m=F;l=c[m>>2]|0;m=c[m+4>>2]|0;if((m|0)<0){l=gb(0,0,l|0,m|0)|0;m=v()|0;n=F;c[n>>2]=l;c[n+4>>2]=m;n=1;p=1028;x=72;break e}else{n=(q&2049|0)!=0&1;p=(q&2048|0)==0?((q&1|0)==0?1028:1030):1029;x=72;break e}}case 117:{m=F;l=c[m>>2]|0;m=c[m+4>>2]|0;n=0;p=1028;x=72;break}case 99:{a[z>>0]=c[F>>2];s=z;q=l;o=1;n=0;m=1028;l=B;break}case 115:{p=c[F>>2]|0;p=(p|0)==0?1038:p;r=Sa(p,0,t)|0;L=(r|0)==0;s=p;q=l;o=L?t:r-p|0;n=0;m=1028;l=L?p+t|0:r;break}case 67:{c[J>>2]=c[F>>2];c[A>>2]=0;c[F>>2]=J;o=-1;x=79;break}case 83:{if(!t){Ta(d,32,u,0,q);l=0;x=89}else{o=t;x=79}break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{l=M[j&1](d,+g[F>>3],u,t,q,n)|0;break d}default:{o=t;n=0;m=1028;l=B}}while(0);f:do if((x|0)==67){o=F;o=Pa(c[o>>2]|0,c[o+4>>2]|0,y,n&32)|0;p=F;p=(l&8|0)==0|(c[p>>2]|0)==0&(c[p+4>>2]|0)==0;r=p?0:2;p=p?1028:1028+(n>>>4)|0;x=73}else if((x|0)==72){o=Ra(l,m,y)|0;l=q;m=t;r=n;x=73}else if((x|0)==79){x=0;l=0;p=c[F>>2]|0;while(1){m=c[p>>2]|0;if(!m)break;m=Ua(G,m)|0;n=(m|0)<0;if(n|m>>>0>(o-l|0)>>>0){x=83;break}l=m+l|0;if(o>>>0>l>>>0)p=p+4|0;else break}if((x|0)==83){x=0;if(n){e=-1;break a}}Ta(d,32,u,l,q);if(!l){l=0;x=89}else{n=0;o=c[F>>2]|0;while(1){m=c[o>>2]|0;if(!m){x=89;break f}m=Ua(G,m)|0;n=m+n|0;if((n|0)>(l|0)){x=89;break f}La(d,G,m);if(n>>>0>=l>>>0){x=89;break}else o=o+4|0}}}while(0);if((x|0)==73){x=0;n=F;n=(c[n>>2]|0)!=0|(c[n+4>>2]|0)!=0;L=(m|0)!=0|n;n=B-o+((n^1)&1)|0;s=L?o:y;q=(m|0)>-1?l&-65537:l;o=L?((m|0)>(n|0)?m:n):0;n=r;m=p;l=B}else if((x|0)==89){x=0;Ta(d,32,u,l,q^8192);l=(u|0)>(l|0)?u:l;break}t=l-s|0;r=(o|0)<(t|0)?t:o;L=r+n|0;l=(u|0)<(L|0)?L:u;Ta(d,32,l,L,q);La(d,m,n);Ta(d,48,l,L,q^65536);Ta(d,48,r,t,0);La(d,s,t);Ta(d,32,l,L,q^8192)}while(0);o=w}g:do if((x|0)==92)if(!d)if(!o)e=0;else{e=1;while(1){l=c[i+(e<<2)>>2]|0;if(!l)break;Oa(h+(e<<3)|0,l,f,k);e=e+1|0;if(e>>>0>=10){e=1;break g}}while(1){if(c[i+(e<<2)>>2]|0){e=-1;break g}e=e+1|0;if(e>>>0>=10){e=1;break}}}while(0);I=K;return e|0}function Ja(a){a=a|0;return 1}function Ka(a){a=a|0;return}function La(a,b,d){a=a|0;b=b|0;d=d|0;if(!(c[a>>2]&32))Ya(b,d,a)|0;return}function Ma(a){a=a|0;return (a+-48|0)>>>0<10|0}function Na(b){b=b|0;var d=0,e=0;if(!(Ma(a[c[b>>2]>>0]|0)|0))d=0;else{d=0;do{e=c[b>>2]|0;d=(d*10|0)+-48+(a[e>>0]|0)|0;e=e+1|0;c[b>>2]=e}while((Ma(a[e>>0]|0)|0)!=0)}return d|0}function Oa(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,h=0.0;a:do if(b>>>0<=20)do switch(b|0){case 9:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;c[a>>2]=b;break a}case 10:{b=(c[d>>2]|0)+(4-1)&~(4-1);e=c[b>>2]|0;c[d>>2]=b+4;b=a;c[b>>2]=e;c[b+4>>2]=((e|0)<0)<<31>>31;break a}case 11:{b=(c[d>>2]|0)+(4-1)&~(4-1);e=c[b>>2]|0;c[d>>2]=b+4;b=a;c[b>>2]=e;c[b+4>>2]=0;break a}case 12:{b=(c[d>>2]|0)+(8-1)&~(8-1);e=b;f=c[e>>2]|0;e=c[e+4>>2]|0;c[d>>2]=b+8;b=a;c[b>>2]=f;c[b+4>>2]=e;break a}case 13:{f=(c[d>>2]|0)+(4-1)&~(4-1);b=c[f>>2]|0;c[d>>2]=f+4;b=(b&65535)<<16>>16;f=a;c[f>>2]=b;c[f+4>>2]=((b|0)<0)<<31>>31;break a}case 14:{f=(c[d>>2]|0)+(4-1)&~(4-1);b=c[f>>2]|0;c[d>>2]=f+4;f=a;c[f>>2]=b&65535;c[f+4>>2]=0;break a}case 15:{f=(c[d>>2]|0)+(4-1)&~(4-1);b=c[f>>2]|0;c[d>>2]=f+4;b=(b&255)<<24>>24;f=a;c[f>>2]=b;c[f+4>>2]=((b|0)<0)<<31>>31;break a}case 16:{f=(c[d>>2]|0)+(4-1)&~(4-1);b=c[f>>2]|0;c[d>>2]=f+4;f=a;c[f>>2]=b&255;c[f+4>>2]=0;break a}case 17:{f=(c[d>>2]|0)+(8-1)&~(8-1);h=+g[f>>3];c[d>>2]=f+8;g[a>>3]=h;break a}case 18:{P[e&1](a,d);break a}default:break a}while(0);while(0);return}function Pa(b,c,e,f){b=b|0;c=c|0;e=e|0;f=f|0;if(!((b|0)==0&(c|0)==0))do{e=e+-1|0;a[e>>0]=d[480+(b&15)>>0]|0|f;b=kb(b|0,c|0,4)|0;c=v()|0}while(!((b|0)==0&(c|0)==0));return e|0}function Qa(b,c,d){b=b|0;c=c|0;d=d|0;if(!((b|0)==0&(c|0)==0))do{d=d+-1|0;a[d>>0]=b&7|48;b=kb(b|0,c|0,3)|0;c=v()|0}while(!((b|0)==0&(c|0)==0));return d|0}function Ra(b,c,d){b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0;if(c>>>0>0|(c|0)==0&b>>>0>4294967295)do{e=b;b=jb(b|0,c|0,10,0)|0;f=c;c=v()|0;g=eb(b|0,c|0,10,0)|0;g=gb(e|0,f|0,g|0,v()|0)|0;v()|0;d=d+-1|0;a[d>>0]=g&255|48}while(f>>>0>9|(f|0)==9&e>>>0>4294967295);if(b)do{g=b;b=(b>>>0)/10|0;d=d+-1|0;a[d>>0]=g-(b*10|0)|48}while(g>>>0>=10);return d|0}function Sa(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;h=d&255;f=(e|0)!=0;a:do if(f&(b&3|0)!=0){g=d&255;while(1){if((a[b>>0]|0)==g<<24>>24){i=6;break a}b=b+1|0;e=e+-1|0;f=(e|0)!=0;if(!(f&(b&3|0)!=0)){i=5;break}}}else i=5;while(0);if((i|0)==5)if(f)i=6;else i=16;b:do if((i|0)==6){g=d&255;if((a[b>>0]|0)==g<<24>>24)if(!e){i=16;break}else break;f=r(h,16843009)|0;c:do if(e>>>0>3)while(1){h=c[b>>2]^f;if((h&-2139062144^-2139062144)&h+-16843009|0)break c;b=b+4|0;e=e+-4|0;if(e>>>0<=3){i=11;break}}else i=11;while(0);if((i|0)==11)if(!e){i=16;break}while(1){if((a[b>>0]|0)==g<<24>>24)break b;e=e+-1|0;if(!e){i=16;break}else b=b+1|0}}while(0);if((i|0)==16)b=0;return b|0}function Ta(a,b,c,d,e){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;var f=0,g=0;g=I;I=I+256|0;f=g;if((c|0)>(d|0)&(e&73728|0)==0){e=c-d|0;ob(f|0,b<<24>>24|0,(e>>>0<256?e:256)|0)|0;if(e>>>0>255){b=c-d|0;do{La(a,f,256);e=e+-256|0}while(e>>>0>255);e=b&255}La(a,f,e)}I=g;return}function Ua(a,b){a=a|0;b=b|0;if(!a)a=0;else a=Va(a,b,0)|0;return a|0}function Va(b,d,e){b=b|0;d=d|0;e=e|0;do if(b){if(d>>>0<128){a[b>>0]=d;b=1;break}if(!(c[c[(Wa()|0)+188>>2]>>2]|0))if((d&-128|0)==57216){a[b>>0]=d;b=1;break}else{c[(Da()|0)>>2]=84;b=-1;break}if(d>>>0<2048){a[b>>0]=d>>>6|192;a[b+1>>0]=d&63|128;b=2;break}if(d>>>0<55296|(d&-8192|0)==57344){a[b>>0]=d>>>12|224;a[b+1>>0]=d>>>6&63|128;a[b+2>>0]=d&63|128;b=3;break}if((d+-65536|0)>>>0<1048576){a[b>>0]=d>>>18|240;a[b+1>>0]=d>>>12&63|128;a[b+2>>0]=d>>>6&63|128;a[b+3>>0]=d&63|128;b=4;break}else{c[(Da()|0)>>2]=84;b=-1;break}}else b=1;while(0);return b|0}function Wa(){return Xa()|0}function Xa(){return 644}function Ya(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0,j=0;g=e+16|0;f=c[g>>2]|0;if(!f)if(!(Za(e)|0)){f=c[g>>2]|0;h=5}else f=0;else h=5;a:do if((h|0)==5){j=e+20|0;i=c[j>>2]|0;g=i;if((f-i|0)>>>0>>0){f=N[c[e+36>>2]&1](e,b,d)|0;break}b:do if((a[e+75>>0]|0)<0|(d|0)==0){h=g;e=0;g=d;f=b}else{i=d;while(1){f=i+-1|0;if((a[b+f>>0]|0)==10)break;if(!f){h=g;e=0;g=d;f=b;break b}else i=f}f=N[c[e+36>>2]&1](e,b,i)|0;if(f>>>0>>0)break a;h=c[j>>2]|0;e=i;g=d-i|0;f=b+i|0}while(0);mb(h|0,f|0,g|0)|0;c[j>>2]=(c[j>>2]|0)+g;f=e+g|0}while(0);return f|0}function Za(b){b=b|0;var d=0,e=0;d=b+74|0;e=a[d>>0]|0;a[d>>0]=e+255|e;d=c[b>>2]|0;if(!(d&8)){c[b+8>>2]=0;c[b+4>>2]=0;d=c[b+44>>2]|0;c[b+28>>2]=d;c[b+20>>2]=d;c[b+16>>2]=d+(c[b+48>>2]|0);d=0}else{c[b>>2]=d|32;d=-1}return d|0}function _a(a){a=+a;var b=0;g[h>>3]=a;b=c[h>>2]|0;u(c[h+4>>2]|0);return b|0}function $a(a,b){a=+a;b=b|0;var d=0,e=0,f=0;g[h>>3]=a;d=c[h>>2]|0;e=c[h+4>>2]|0;f=kb(d|0,e|0,52)|0;v()|0;switch(f&2047){case 0:{if(a!=0.0){a=+$a(a*18446744073709551616.0,b);d=(c[b>>2]|0)+-64|0}else d=0;c[b>>2]=d;break}case 2047:break;default:{c[b>>2]=(f&2047)+-1022;c[h>>2]=d;c[h+4>>2]=e&-2146435073|1071644672;a=+g[h>>3]}}return +a}function ab(a,b){a=a|0;b=b|0;var d=0,e=0;d=I;I=I+16|0;e=d;c[e>>2]=b;b=Ea(c[160]|0,a,e)|0;I=d;return b|0}function bb(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0;w=I;I=I+16|0;n=w;do if(a>>>0<245){k=a>>>0<11?16:a+11&-8;a=k>>>3;m=c[549]|0;d=m>>>a;if(d&3|0){e=(d&1^1)+a|0;f=2236+(e<<1<<2)|0;b=f+8|0;a=c[b>>2]|0;g=a+8|0;d=c[g>>2]|0;if((d|0)==(f|0))c[549]=m&~(1<>2]=f;c[b>>2]=d}v=e<<3;c[a+4>>2]=v|3;v=a+v+4|0;c[v>>2]=c[v>>2]|1;v=g;I=w;return v|0}l=c[551]|0;if(k>>>0>l>>>0){if(d|0){i=2<>>12&16;d=d>>>i;a=d>>>5&8;d=d>>>a;g=d>>>2&4;d=d>>>g;b=d>>>1&2;d=d>>>b;e=d>>>1&1;e=(a|i|g|b|e)+(d>>>e)|0;d=2236+(e<<1<<2)|0;b=d+8|0;g=c[b>>2]|0;i=g+8|0;a=c[i>>2]|0;if((a|0)==(d|0)){a=m&~(1<>2]=d;c[b>>2]=a;a=m}v=e<<3;h=v-k|0;c[g+4>>2]=k|3;f=g+k|0;c[f+4>>2]=h|1;c[g+v>>2]=h;if(l|0){e=c[554]|0;b=l>>>3;d=2236+(b<<1<<2)|0;b=1<>2]|0}c[b>>2]=e;c[a+12>>2]=e;c[e+8>>2]=a;c[e+12>>2]=d}c[551]=h;c[554]=f;v=i;I=w;return v|0}g=c[550]|0;if(g){i=(g&0-g)+-1|0;f=i>>>12&16;i=i>>>f;e=i>>>5&8;i=i>>>e;h=i>>>2&4;i=i>>>h;d=i>>>1&2;i=i>>>d;j=i>>>1&1;j=c[2500+((e|f|h|d|j)+(i>>>j)<<2)>>2]|0;i=(c[j+4>>2]&-8)-k|0;d=j;while(1){a=c[d+16>>2]|0;if(!a){a=c[d+20>>2]|0;if(!a)break}d=(c[a+4>>2]&-8)-k|0;h=d>>>0>>0;i=h?d:i;d=a;j=h?a:j}h=j+k|0;if(h>>>0>j>>>0){f=c[j+24>>2]|0;b=c[j+12>>2]|0;do if((b|0)==(j|0)){a=j+20|0;b=c[a>>2]|0;if(!b){a=j+16|0;b=c[a>>2]|0;if(!b){d=0;break}}while(1){e=b+20|0;d=c[e>>2]|0;if(!d){e=b+16|0;d=c[e>>2]|0;if(!d)break;else{b=d;a=e}}else{b=d;a=e}}c[a>>2]=0;d=b}else{d=c[j+8>>2]|0;c[d+12>>2]=b;c[b+8>>2]=d;d=b}while(0);do if(f|0){b=c[j+28>>2]|0;a=2500+(b<<2)|0;if((j|0)==(c[a>>2]|0)){c[a>>2]=d;if(!d){c[550]=g&~(1<>2]|0)==(j|0)?v:f+20|0)>>2]=d;if(!d)break}c[d+24>>2]=f;b=c[j+16>>2]|0;if(b|0){c[d+16>>2]=b;c[b+24>>2]=d}b=c[j+20>>2]|0;if(b|0){c[d+20>>2]=b;c[b+24>>2]=d}}while(0);if(i>>>0<16){v=i+k|0;c[j+4>>2]=v|3;v=j+v+4|0;c[v>>2]=c[v>>2]|1}else{c[j+4>>2]=k|3;c[h+4>>2]=i|1;c[h+i>>2]=i;if(l|0){e=c[554]|0;b=l>>>3;d=2236+(b<<1<<2)|0;b=1<>2]|0}c[b>>2]=e;c[a+12>>2]=e;c[e+8>>2]=a;c[e+12>>2]=d}c[551]=i;c[554]=h}v=j+8|0;I=w;return v|0}else m=k}else m=k}else m=k}else if(a>>>0<=4294967231){a=a+11|0;k=a&-8;e=c[550]|0;if(e){d=0-k|0;a=a>>>8;if(a)if(k>>>0>16777215)j=31;else{m=(a+1048320|0)>>>16&8;q=a<>>16&4;q=q<>>16&2;j=14-(i|m|j)+(q<>>15)|0;j=k>>>(j+7|0)&1|j<<1}else j=0;a=c[2500+(j<<2)>>2]|0;a:do if(!a){f=0;a=0;q=61}else{f=0;h=k<<((j|0)==31?0:25-(j>>>1)|0);i=a;a=0;while(1){g=(c[i+4>>2]&-8)-k|0;if(g>>>0>>0)if(!g){d=0;f=i;a=i;q=65;break a}else{d=g;a=i}q=c[i+20>>2]|0;i=c[i+16+(h>>>31<<2)>>2]|0;f=(q|0)==0|(q|0)==(i|0)?f:q;if(!i){q=61;break}else h=h<<1}}while(0);if((q|0)==61){if((f|0)==0&(a|0)==0){a=2<>>12&16;a=a>>>i;h=a>>>5&8;a=a>>>h;j=a>>>2&4;a=a>>>j;m=a>>>1&2;a=a>>>m;f=a>>>1&1;f=c[2500+((h|i|j|m|f)+(a>>>f)<<2)>>2]|0;a=0}if(!f){i=d;g=a}else q=65}if((q|0)==65)while(1){m=(c[f+4>>2]&-8)-k|0;g=m>>>0>>0;d=g?m:d;g=g?f:a;a=c[f+16>>2]|0;if(!a)a=c[f+20>>2]|0;if(!a){i=d;break}else{f=a;a=g}}if(((g|0)!=0?i>>>0<((c[551]|0)-k|0)>>>0:0)?(l=g+k|0,l>>>0>g>>>0):0){h=c[g+24>>2]|0;b=c[g+12>>2]|0;do if((b|0)==(g|0)){a=g+20|0;b=c[a>>2]|0;if(!b){a=g+16|0;b=c[a>>2]|0;if(!b){b=0;break}}while(1){f=b+20|0;d=c[f>>2]|0;if(!d){f=b+16|0;d=c[f>>2]|0;if(!d)break;else{b=d;a=f}}else{b=d;a=f}}c[a>>2]=0}else{v=c[g+8>>2]|0;c[v+12>>2]=b;c[b+8>>2]=v}while(0);do if(h){a=c[g+28>>2]|0;d=2500+(a<<2)|0;if((g|0)==(c[d>>2]|0)){c[d>>2]=b;if(!b){e=e&~(1<>2]|0)==(g|0)?v:h+20|0)>>2]=b;if(!b)break}c[b+24>>2]=h;a=c[g+16>>2]|0;if(a|0){c[b+16>>2]=a;c[a+24>>2]=b}a=c[g+20>>2]|0;if(a){c[b+20>>2]=a;c[a+24>>2]=b}}while(0);b:do if(i>>>0<16){v=i+k|0;c[g+4>>2]=v|3;v=g+v+4|0;c[v>>2]=c[v>>2]|1}else{c[g+4>>2]=k|3;c[l+4>>2]=i|1;c[l+i>>2]=i;b=i>>>3;if(i>>>0<256){d=2236+(b<<1<<2)|0;a=c[549]|0;b=1<>2]|0}c[b>>2]=l;c[a+12>>2]=l;c[l+8>>2]=a;c[l+12>>2]=d;break}b=i>>>8;if(b)if(i>>>0>16777215)d=31;else{u=(b+1048320|0)>>>16&8;v=b<>>16&4;v=v<>>16&2;d=14-(t|u|d)+(v<>>15)|0;d=i>>>(d+7|0)&1|d<<1}else d=0;b=2500+(d<<2)|0;c[l+28>>2]=d;a=l+16|0;c[a+4>>2]=0;c[a>>2]=0;a=1<>2]=l;c[l+24>>2]=b;c[l+12>>2]=l;c[l+8>>2]=l;break}b=c[b>>2]|0;c:do if((c[b+4>>2]&-8|0)!=(i|0)){e=i<<((d|0)==31?0:25-(d>>>1)|0);while(1){d=b+16+(e>>>31<<2)|0;a=c[d>>2]|0;if(!a)break;if((c[a+4>>2]&-8|0)==(i|0)){b=a;break c}else{e=e<<1;b=a}}c[d>>2]=l;c[l+24>>2]=b;c[l+12>>2]=l;c[l+8>>2]=l;break b}while(0);u=b+8|0;v=c[u>>2]|0;c[v+12>>2]=l;c[u>>2]=l;c[l+8>>2]=v;c[l+12>>2]=b;c[l+24>>2]=0}while(0);v=g+8|0;I=w;return v|0}else m=k}else m=k}else m=-1;while(0);d=c[551]|0;if(d>>>0>=m>>>0){a=d-m|0;b=c[554]|0;if(a>>>0>15){v=b+m|0;c[554]=v;c[551]=a;c[v+4>>2]=a|1;c[b+d>>2]=a;c[b+4>>2]=m|3}else{c[551]=0;c[554]=0;c[b+4>>2]=d|3;v=b+d+4|0;c[v>>2]=c[v>>2]|1}v=b+8|0;I=w;return v|0}h=c[552]|0;if(h>>>0>m>>>0){t=h-m|0;c[552]=t;v=c[555]|0;u=v+m|0;c[555]=u;c[u+4>>2]=t|1;c[v+4>>2]=m|3;v=v+8|0;I=w;return v|0}if(!(c[667]|0)){c[669]=4096;c[668]=4096;c[670]=-1;c[671]=-1;c[672]=0;c[660]=0;c[667]=n&-16^1431655768;a=4096}else a=c[669]|0;i=m+48|0;j=m+47|0;g=a+j|0;e=0-a|0;k=g&e;if(k>>>0<=m>>>0){v=0;I=w;return v|0}a=c[659]|0;if(a|0?(l=c[657]|0,n=l+k|0,n>>>0<=l>>>0|n>>>0>a>>>0):0){v=0;I=w;return v|0}d:do if(!(c[660]&4)){d=c[555]|0;e:do if(d){f=2644;while(1){n=c[f>>2]|0;if(n>>>0<=d>>>0?(n+(c[f+4>>2]|0)|0)>>>0>d>>>0:0)break;a=c[f+8>>2]|0;if(!a){q=128;break e}else f=a}b=g-h&e;if(b>>>0<2147483647){a=pb(b|0)|0;if((a|0)==((c[f>>2]|0)+(c[f+4>>2]|0)|0)){if((a|0)!=(-1|0)){h=a;g=b;q=145;break d}}else{e=a;q=136}}else b=0}else q=128;while(0);do if((q|0)==128){d=pb(0)|0;if((d|0)!=(-1|0)?(b=d,o=c[668]|0,p=o+-1|0,b=((p&b|0)==0?0:(p+b&0-o)-b|0)+k|0,o=c[657]|0,p=b+o|0,b>>>0>m>>>0&b>>>0<2147483647):0){n=c[659]|0;if(n|0?p>>>0<=o>>>0|p>>>0>n>>>0:0){b=0;break}a=pb(b|0)|0;if((a|0)==(d|0)){h=d;g=b;q=145;break d}else{e=a;q=136}}else b=0}while(0);do if((q|0)==136){d=0-b|0;if(!(i>>>0>b>>>0&(b>>>0<2147483647&(e|0)!=(-1|0))))if((e|0)==(-1|0)){b=0;break}else{h=e;g=b;q=145;break d}a=c[669]|0;a=j-b+a&0-a;if(a>>>0>=2147483647){h=e;g=b;q=145;break d}if((pb(a|0)|0)==(-1|0)){pb(d|0)|0;b=0;break}else{h=e;g=a+b|0;q=145;break d}}while(0);c[660]=c[660]|4;q=143}else{b=0;q=143}while(0);if(((q|0)==143?k>>>0<2147483647:0)?(r=pb(k|0)|0,p=pb(0)|0,t=p-r|0,s=t>>>0>(m+40|0)>>>0,!((r|0)==(-1|0)|s^1|r>>>0

>>0&((r|0)!=(-1|0)&(p|0)!=(-1|0))^1)):0){h=r;g=s?t:b;q=145}if((q|0)==145){b=(c[657]|0)+g|0;c[657]=b;if(b>>>0>(c[658]|0)>>>0)c[658]=b;j=c[555]|0;f:do if(j){e=2644;while(1){b=c[e>>2]|0;a=c[e+4>>2]|0;if((h|0)==(b+a|0)){q=154;break}d=c[e+8>>2]|0;if(!d)break;else e=d}if(((q|0)==154?(u=e+4|0,(c[e+12>>2]&8|0)==0):0)?h>>>0>j>>>0&b>>>0<=j>>>0:0){c[u>>2]=a+g;v=(c[552]|0)+g|0;t=j+8|0;t=(t&7|0)==0?0:0-t&7;u=j+t|0;t=v-t|0;c[555]=u;c[552]=t;c[u+4>>2]=t|1;c[j+v+4>>2]=40;c[556]=c[671];break}if(h>>>0<(c[553]|0)>>>0)c[553]=h;d=h+g|0;a=2644;while(1){if((c[a>>2]|0)==(d|0)){q=162;break}b=c[a+8>>2]|0;if(!b)break;else a=b}if((q|0)==162?(c[a+12>>2]&8|0)==0:0){c[a>>2]=h;l=a+4|0;c[l>>2]=(c[l>>2]|0)+g;l=h+8|0;l=h+((l&7|0)==0?0:0-l&7)|0;b=d+8|0;b=d+((b&7|0)==0?0:0-b&7)|0;k=l+m|0;i=b-l-m|0;c[l+4>>2]=m|3;g:do if((j|0)==(b|0)){v=(c[552]|0)+i|0;c[552]=v;c[555]=k;c[k+4>>2]=v|1}else{if((c[554]|0)==(b|0)){v=(c[551]|0)+i|0;c[551]=v;c[554]=k;c[k+4>>2]=v|1;c[k+v>>2]=v;break}a=c[b+4>>2]|0;if((a&3|0)==1){h=a&-8;e=a>>>3;h:do if(a>>>0<256){a=c[b+8>>2]|0;d=c[b+12>>2]|0;if((d|0)==(a|0)){c[549]=c[549]&~(1<>2]=d;c[d+8>>2]=a;break}}else{g=c[b+24>>2]|0;a=c[b+12>>2]|0;do if((a|0)==(b|0)){e=b+16|0;d=e+4|0;a=c[d>>2]|0;if(!a){a=c[e>>2]|0;if(!a){a=0;break}else d=e}while(1){f=a+20|0;e=c[f>>2]|0;if(!e){f=a+16|0;e=c[f>>2]|0;if(!e)break;else{a=e;d=f}}else{a=e;d=f}}c[d>>2]=0}else{v=c[b+8>>2]|0;c[v+12>>2]=a;c[a+8>>2]=v}while(0);if(!g)break;d=c[b+28>>2]|0;e=2500+(d<<2)|0;do if((c[e>>2]|0)!=(b|0)){v=g+16|0;c[((c[v>>2]|0)==(b|0)?v:g+20|0)>>2]=a;if(!a)break h}else{c[e>>2]=a;if(a|0)break;c[550]=c[550]&~(1<>2]=g;e=b+16|0;d=c[e>>2]|0;if(d|0){c[a+16>>2]=d;c[d+24>>2]=a}d=c[e+4>>2]|0;if(!d)break;c[a+20>>2]=d;c[d+24>>2]=a}while(0);b=b+h|0;f=h+i|0}else f=i;b=b+4|0;c[b>>2]=c[b>>2]&-2;c[k+4>>2]=f|1;c[k+f>>2]=f;b=f>>>3;if(f>>>0<256){d=2236+(b<<1<<2)|0;a=c[549]|0;b=1<>2]|0}c[b>>2]=k;c[a+12>>2]=k;c[k+8>>2]=a;c[k+12>>2]=d;break}b=f>>>8;do if(!b)e=0;else{if(f>>>0>16777215){e=31;break}u=(b+1048320|0)>>>16&8;v=b<>>16&4;v=v<>>16&2;e=14-(t|u|e)+(v<>>15)|0;e=f>>>(e+7|0)&1|e<<1}while(0);a=2500+(e<<2)|0;c[k+28>>2]=e;b=k+16|0;c[b+4>>2]=0;c[b>>2]=0;b=c[550]|0;d=1<>2]=k;c[k+24>>2]=a;c[k+12>>2]=k;c[k+8>>2]=k;break}b=c[a>>2]|0;i:do if((c[b+4>>2]&-8|0)!=(f|0)){e=f<<((e|0)==31?0:25-(e>>>1)|0);while(1){d=b+16+(e>>>31<<2)|0;a=c[d>>2]|0;if(!a)break;if((c[a+4>>2]&-8|0)==(f|0)){b=a;break i}else{e=e<<1;b=a}}c[d>>2]=k;c[k+24>>2]=b;c[k+12>>2]=k;c[k+8>>2]=k;break g}while(0);u=b+8|0;v=c[u>>2]|0;c[v+12>>2]=k;c[u>>2]=k;c[k+8>>2]=v;c[k+12>>2]=b;c[k+24>>2]=0}while(0);v=l+8|0;I=w;return v|0}a=2644;while(1){b=c[a>>2]|0;if(b>>>0<=j>>>0?(v=b+(c[a+4>>2]|0)|0,v>>>0>j>>>0):0)break;a=c[a+8>>2]|0}f=v+-47|0;a=f+8|0;a=f+((a&7|0)==0?0:0-a&7)|0;f=j+16|0;a=a>>>0>>0?j:a;b=a+8|0;d=g+-40|0;t=h+8|0;t=(t&7|0)==0?0:0-t&7;u=h+t|0;t=d-t|0;c[555]=u;c[552]=t;c[u+4>>2]=t|1;c[h+d+4>>2]=40;c[556]=c[671];d=a+4|0;c[d>>2]=27;c[b>>2]=c[661];c[b+4>>2]=c[662];c[b+8>>2]=c[663];c[b+12>>2]=c[664];c[661]=h;c[662]=g;c[664]=0;c[663]=b;b=a+24|0;do{u=b;b=b+4|0;c[b>>2]=7}while((u+8|0)>>>0>>0);if((a|0)!=(j|0)){g=a-j|0;c[d>>2]=c[d>>2]&-2;c[j+4>>2]=g|1;c[a>>2]=g;b=g>>>3;if(g>>>0<256){d=2236+(b<<1<<2)|0;a=c[549]|0;b=1<>2]|0}c[b>>2]=j;c[a+12>>2]=j;c[j+8>>2]=a;c[j+12>>2]=d;break}b=g>>>8;if(b)if(g>>>0>16777215)e=31;else{u=(b+1048320|0)>>>16&8;v=b<>>16&4;v=v<>>16&2;e=14-(t|u|e)+(v<>>15)|0;e=g>>>(e+7|0)&1|e<<1}else e=0;d=2500+(e<<2)|0;c[j+28>>2]=e;c[j+20>>2]=0;c[f>>2]=0;b=c[550]|0;a=1<>2]=j;c[j+24>>2]=d;c[j+12>>2]=j;c[j+8>>2]=j;break}b=c[d>>2]|0;j:do if((c[b+4>>2]&-8|0)!=(g|0)){e=g<<((e|0)==31?0:25-(e>>>1)|0);while(1){d=b+16+(e>>>31<<2)|0;a=c[d>>2]|0;if(!a)break;if((c[a+4>>2]&-8|0)==(g|0)){b=a;break j}else{e=e<<1;b=a}}c[d>>2]=j;c[j+24>>2]=b;c[j+12>>2]=j;c[j+8>>2]=j;break f}while(0);u=b+8|0;v=c[u>>2]|0;c[v+12>>2]=j;c[u>>2]=j;c[j+8>>2]=v;c[j+12>>2]=b;c[j+24>>2]=0}}else{v=c[553]|0;if((v|0)==0|h>>>0>>0)c[553]=h;c[661]=h;c[662]=g;c[664]=0;c[558]=c[667];c[557]=-1;c[562]=2236;c[561]=2236;c[564]=2244;c[563]=2244;c[566]=2252;c[565]=2252;c[568]=2260;c[567]=2260;c[570]=2268;c[569]=2268;c[572]=2276;c[571]=2276;c[574]=2284;c[573]=2284;c[576]=2292;c[575]=2292;c[578]=2300;c[577]=2300;c[580]=2308;c[579]=2308;c[582]=2316;c[581]=2316;c[584]=2324;c[583]=2324;c[586]=2332;c[585]=2332;c[588]=2340;c[587]=2340;c[590]=2348;c[589]=2348;c[592]=2356;c[591]=2356;c[594]=2364;c[593]=2364;c[596]=2372;c[595]=2372;c[598]=2380;c[597]=2380;c[600]=2388;c[599]=2388;c[602]=2396;c[601]=2396;c[604]=2404;c[603]=2404;c[606]=2412;c[605]=2412;c[608]=2420;c[607]=2420;c[610]=2428;c[609]=2428;c[612]=2436;c[611]=2436;c[614]=2444;c[613]=2444;c[616]=2452;c[615]=2452;c[618]=2460;c[617]=2460;c[620]=2468;c[619]=2468;c[622]=2476;c[621]=2476;c[624]=2484;c[623]=2484;v=g+-40|0;t=h+8|0;t=(t&7|0)==0?0:0-t&7;u=h+t|0;t=v-t|0;c[555]=u;c[552]=t;c[u+4>>2]=t|1;c[h+v+4>>2]=40;c[556]=c[671]}while(0);b=c[552]|0;if(b>>>0>m>>>0){t=b-m|0;c[552]=t;v=c[555]|0;u=v+m|0;c[555]=u;c[u+4>>2]=t|1;c[v+4>>2]=m|3;v=v+8|0;I=w;return v|0}}c[(Da()|0)>>2]=12;v=0;I=w;return v|0}function cb(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0;if(!a)return;d=a+-8|0;e=c[553]|0;a=c[a+-4>>2]|0;b=a&-8;k=d+b|0;do if(!(a&1)){f=c[d>>2]|0;if(!(a&3))return;g=d+(0-f)|0;h=f+b|0;if(g>>>0>>0)return;if((c[554]|0)==(g|0)){b=k+4|0;a=c[b>>2]|0;if((a&3|0)!=3){i=g;j=g;b=h;break}c[551]=h;c[b>>2]=a&-2;c[g+4>>2]=h|1;c[g+h>>2]=h;return}d=f>>>3;if(f>>>0<256){a=c[g+8>>2]|0;b=c[g+12>>2]|0;if((b|0)==(a|0)){c[549]=c[549]&~(1<>2]=b;c[b+8>>2]=a;i=g;j=g;b=h;break}}f=c[g+24>>2]|0;a=c[g+12>>2]|0;do if((a|0)==(g|0)){d=g+16|0;b=d+4|0;a=c[b>>2]|0;if(!a){a=c[d>>2]|0;if(!a){d=0;break}else b=d}while(1){e=a+20|0;d=c[e>>2]|0;if(!d){e=a+16|0;d=c[e>>2]|0;if(!d)break;else{a=d;b=e}}else{a=d;b=e}}c[b>>2]=0;d=a}else{d=c[g+8>>2]|0;c[d+12>>2]=a;c[a+8>>2]=d;d=a}while(0);if(f){a=c[g+28>>2]|0;b=2500+(a<<2)|0;if((c[b>>2]|0)==(g|0)){c[b>>2]=d;if(!d){c[550]=c[550]&~(1<>2]|0)==(g|0)?j:f+20|0)>>2]=d;if(!d){i=g;j=g;b=h;break}}c[d+24>>2]=f;b=g+16|0;a=c[b>>2]|0;if(a|0){c[d+16>>2]=a;c[a+24>>2]=d}a=c[b+4>>2]|0;if(a){c[d+20>>2]=a;c[a+24>>2]=d;i=g;j=g;b=h}else{i=g;j=g;b=h}}else{i=g;j=g;b=h}}else{i=d;j=d}while(0);if(i>>>0>=k>>>0)return;a=k+4|0;d=c[a>>2]|0;if(!(d&1))return;if(!(d&2)){if((c[555]|0)==(k|0)){k=(c[552]|0)+b|0;c[552]=k;c[555]=j;c[j+4>>2]=k|1;if((j|0)!=(c[554]|0))return;c[554]=0;c[551]=0;return}if((c[554]|0)==(k|0)){k=(c[551]|0)+b|0;c[551]=k;c[554]=i;c[j+4>>2]=k|1;c[i+k>>2]=k;return}f=(d&-8)+b|0;e=d>>>3;do if(d>>>0<256){b=c[k+8>>2]|0;a=c[k+12>>2]|0;if((a|0)==(b|0)){c[549]=c[549]&~(1<>2]=a;c[a+8>>2]=b;break}}else{g=c[k+24>>2]|0;a=c[k+12>>2]|0;do if((a|0)==(k|0)){d=k+16|0;b=d+4|0;a=c[b>>2]|0;if(!a){a=c[d>>2]|0;if(!a){d=0;break}else b=d}while(1){e=a+20|0;d=c[e>>2]|0;if(!d){e=a+16|0;d=c[e>>2]|0;if(!d)break;else{a=d;b=e}}else{a=d;b=e}}c[b>>2]=0;d=a}else{d=c[k+8>>2]|0;c[d+12>>2]=a;c[a+8>>2]=d;d=a}while(0);if(g|0){a=c[k+28>>2]|0;b=2500+(a<<2)|0;if((c[b>>2]|0)==(k|0)){c[b>>2]=d;if(!d){c[550]=c[550]&~(1<>2]|0)==(k|0)?h:g+20|0)>>2]=d;if(!d)break}c[d+24>>2]=g;b=k+16|0;a=c[b>>2]|0;if(a|0){c[d+16>>2]=a;c[a+24>>2]=d}a=c[b+4>>2]|0;if(a|0){c[d+20>>2]=a;c[a+24>>2]=d}}}while(0);c[j+4>>2]=f|1;c[i+f>>2]=f;if((j|0)==(c[554]|0)){c[551]=f;return}}else{c[a>>2]=d&-2;c[j+4>>2]=b|1;c[i+b>>2]=b;f=b}a=f>>>3;if(f>>>0<256){d=2236+(a<<1<<2)|0;b=c[549]|0;a=1<>2]|0}c[a>>2]=j;c[b+12>>2]=j;c[j+8>>2]=b;c[j+12>>2]=d;return}a=f>>>8;if(a)if(f>>>0>16777215)e=31;else{i=(a+1048320|0)>>>16&8;k=a<>>16&4;k=k<>>16&2;e=14-(h|i|e)+(k<>>15)|0;e=f>>>(e+7|0)&1|e<<1}else e=0;b=2500+(e<<2)|0;c[j+28>>2]=e;c[j+20>>2]=0;c[j+16>>2]=0;a=c[550]|0;d=1<>2]=j;c[j+24>>2]=b;c[j+12>>2]=j;c[j+8>>2]=j}else{a=c[b>>2]|0;b:do if((c[a+4>>2]&-8|0)!=(f|0)){e=f<<((e|0)==31?0:25-(e>>>1)|0);while(1){d=a+16+(e>>>31<<2)|0;b=c[d>>2]|0;if(!b)break;if((c[b+4>>2]&-8|0)==(f|0)){a=b;break b}else{e=e<<1;a=b}}c[d>>2]=j;c[j+24>>2]=a;c[j+12>>2]=j;c[j+8>>2]=j;break a}while(0);i=a+8|0;k=c[i>>2]|0;c[k+12>>2]=j;c[i>>2]=j;c[j+8>>2]=k;c[j+12>>2]=a;c[j+24>>2]=0}while(0);k=(c[557]|0)+-1|0;c[557]=k;if(k|0)return;a=2652;while(1){a=c[a>>2]|0;if(!a)break;else a=a+8|0}c[557]=-1;return}function db(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0;f=a&65535;e=b&65535;c=r(e,f)|0;d=a>>>16;a=(c>>>16)+(r(e,d)|0)|0;e=b>>>16;b=r(e,f)|0;return (u((a>>>16)+(r(e,d)|0)+(((a&65535)+b|0)>>>16)|0),a+b<<16|c&65535|0)|0}function eb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0;e=a;f=c;c=db(e,f)|0;a=v()|0;return (u((r(b,f)|0)+(r(d,e)|0)+a|a&0|0),c|0|0)|0}function fb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;c=a+c>>>0;return (u(b+d+(c>>>0>>0|0)>>>0|0),c|0)|0}function gb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;d=b-d-(c>>>0>a>>>0|0)>>>0;return (u(d|0),a-c>>>0|0)|0}function hb(a){a=a|0;return (a?31-(s(a^a-1)|0)|0:32)|0}function ib(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;l=a;j=b;k=j;h=d;n=e;i=n;if(!k){g=(f|0)!=0;if(!i){if(g){c[f>>2]=(l>>>0)%(h>>>0);c[f+4>>2]=0}n=0;f=(l>>>0)/(h>>>0)>>>0;return (u(n|0),f)|0}else{if(!g){n=0;f=0;return (u(n|0),f)|0}c[f>>2]=a|0;c[f+4>>2]=b&0;n=0;f=0;return (u(n|0),f)|0}}g=(i|0)==0;do if(h){if(!g){g=(s(i|0)|0)-(s(k|0)|0)|0;if(g>>>0<=31){m=g+1|0;i=31-g|0;b=g-31>>31;h=m;a=l>>>(m>>>0)&b|k<>>(m>>>0)&b;g=0;i=l<>2]=a|0;c[f+4>>2]=j|b&0;n=0;f=0;return (u(n|0),f)|0}g=h-1|0;if(g&h|0){i=(s(h|0)|0)+33-(s(k|0)|0)|0;p=64-i|0;m=32-i|0;j=m>>31;o=i-32|0;b=o>>31;h=i;a=m-1>>31&k>>>(o>>>0)|(k<>>(i>>>0))&b;b=b&k>>>(i>>>0);g=l<>>(o>>>0))&j|l<>31;break}if(f|0){c[f>>2]=g&l;c[f+4>>2]=0}if((h|0)==1){o=j|b&0;p=a|0|0;return (u(o|0),p)|0}else{p=hb(h|0)|0;o=k>>>(p>>>0)|0;p=k<<32-p|l>>>(p>>>0)|0;return (u(o|0),p)|0}}else{if(g){if(f|0){c[f>>2]=(k>>>0)%(h>>>0);c[f+4>>2]=0}o=0;p=(k>>>0)/(h>>>0)>>>0;return (u(o|0),p)|0}if(!l){if(f|0){c[f>>2]=0;c[f+4>>2]=(k>>>0)%(i>>>0)}o=0;p=(k>>>0)/(i>>>0)>>>0;return (u(o|0),p)|0}g=i-1|0;if(!(g&i)){if(f|0){c[f>>2]=a|0;c[f+4>>2]=g&k|b&0}o=0;p=k>>>((hb(i|0)|0)>>>0);return (u(o|0),p)|0}g=(s(i|0)|0)-(s(k|0)|0)|0;if(g>>>0<=30){b=g+1|0;i=31-g|0;h=b;a=k<>>(b>>>0);b=k>>>(b>>>0);g=0;i=l<>2]=a|0;c[f+4>>2]=j|b&0;o=0;p=0;return (u(o|0),p)|0}while(0);if(!h){k=i;j=0;i=0}else{m=d|0|0;l=n|e&0;k=fb(m|0,l|0,-1,-1)|0;d=v()|0;j=i;i=0;do{e=j;j=g>>>31|j<<1;g=i|g<<1;e=a<<1|e>>>31|0;n=a>>>31|b<<1|0;gb(k|0,d|0,e|0,n|0)|0;p=v()|0;o=p>>31|((p|0)<0?-1:0)<<1;i=o&1;a=gb(e|0,n|0,o&m|0,(((p|0)<0?-1:0)>>31|((p|0)<0?-1:0)<<1)&l|0)|0;b=v()|0;h=h-1|0}while((h|0)!=0);k=j;j=0}h=0;if(f|0){c[f>>2]=a;c[f+4>>2]=b}o=(g|0)>>>31|(k|h)<<1|(h<<1|g>>>31)&0|j;p=(g<<1|0>>>31)&-2|i;return (u(o|0),p)|0}function jb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return ib(a,b,c,d,0)|0}function kb(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){u(b>>>c|0);return a>>>c|(b&(1<>>c-32|0}function lb(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){u(b<>>32-c|0);return a<=8192){z(b|0,d|0,e|0)|0;return b|0}h=b|0;g=b+e|0;if((b&3)==(d&3)){while(b&3){if(!e)return h|0;a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}e=g&-4|0;f=e-64|0;while((b|0)<=(f|0)){c[b>>2]=c[d>>2];c[b+4>>2]=c[d+4>>2];c[b+8>>2]=c[d+8>>2];c[b+12>>2]=c[d+12>>2];c[b+16>>2]=c[d+16>>2];c[b+20>>2]=c[d+20>>2];c[b+24>>2]=c[d+24>>2];c[b+28>>2]=c[d+28>>2];c[b+32>>2]=c[d+32>>2];c[b+36>>2]=c[d+36>>2];c[b+40>>2]=c[d+40>>2];c[b+44>>2]=c[d+44>>2];c[b+48>>2]=c[d+48>>2];c[b+52>>2]=c[d+52>>2];c[b+56>>2]=c[d+56>>2];c[b+60>>2]=c[d+60>>2];b=b+64|0;d=d+64|0}while((b|0)<(e|0)){c[b>>2]=c[d>>2];b=b+4|0;d=d+4|0}}else{e=g-4|0;while((b|0)<(e|0)){a[b>>0]=a[d>>0]|0;a[b+1>>0]=a[d+1>>0]|0;a[b+2>>0]=a[d+2>>0]|0;a[b+3>>0]=a[d+3>>0]|0;b=b+4|0;d=d+4|0}}while((b|0)<(g|0)){a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0}return h|0}function nb(b,c,d){b=b|0;c=c|0;d=d|0;var e=0;if((c|0)<(b|0)&(b|0)<(c+d|0)){e=b;c=c+d|0;b=b+d|0;while((d|0)>0){b=b-1|0;c=c-1|0;d=d-1|0;a[b>>0]=a[c>>0]|0}b=e}else mb(b,c,d)|0;return b|0}function ob(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;h=b+e|0;d=d&255;if((e|0)>=67){while(b&3){a[b>>0]=d;b=b+1|0}f=h&-4|0;i=d|d<<8|d<<16|d<<24;g=f-64|0;while((b|0)<=(g|0)){c[b>>2]=i;c[b+4>>2]=i;c[b+8>>2]=i;c[b+12>>2]=i;c[b+16>>2]=i;c[b+20>>2]=i;c[b+24>>2]=i;c[b+28>>2]=i;c[b+32>>2]=i;c[b+36>>2]=i;c[b+40>>2]=i;c[b+44>>2]=i;c[b+48>>2]=i;c[b+52>>2]=i;c[b+56>>2]=i;c[b+60>>2]=i;b=b+64|0}while((b|0)<(f|0)){c[b>>2]=i;b=b+4|0}}while((b|0)<(h|0)){a[b>>0]=d;b=b+1|0}return h-e|0}function pb(a){a=a|0;var b=0,d=0,e=0;e=y()|0;d=c[i>>2]|0;b=d+a|0;if((a|0)>0&(b|0)<(d|0)|(b|0)<0){C(b|0)|0;w(12);return -1}if((b|0)>(e|0))if(!(A(b|0)|0)){w(12);return -1}c[i>>2]=b;return d|0}function qb(a,b){a=a|0;b=b|0;return L[a&1](b|0)|0}function rb(a,b,c,d,e,f,g){a=a|0;b=b|0;c=+c;d=d|0;e=e|0;f=f|0;g=g|0;return M[a&1](b|0,+c,d|0,e|0,f|0,g|0)|0}function sb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return N[a&1](b|0,c|0,d|0)|0}function tb(a,b,c,d,e){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;return O[a&1](b|0,c|0,d|0,e|0)|0}function ub(a,b,c){a=a|0;b=b|0;c=c|0;P[a&1](b|0,c|0)}function vb(a){a=a|0;t(0);return 0}function wb(a,b,c,d,e,f){a=a|0;b=+b;c=c|0;d=d|0;e=e|0;f=f|0;t(1);return 0}function xb(a,b,c){a=a|0;b=b|0;c=c|0;t(2);return 0}function yb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;t(3);return 0}function zb(a,b){a=a|0;b=b|0;t(4)} + +// EMSCRIPTEN_END_FUNCS +var L=[vb,Aa];var M=[wb,Fa];var N=[xb,Ba];var O=[yb,Ca];var P=[zb,Ga];return{___errno_location:Da,___muldi3:eb,___udivdi3:jb,_bitshift64Lshr:kb,_bitshift64Shl:lb,_compress:U,_decompress:V,_free:cb,_i64Add:fb,_i64Subtract:gb,_malloc:bb,_memcpy:mb,_memmove:nb,_memset:ob,_sbrk:pb,dynCall_ii:qb,dynCall_iidiiii:rb,dynCall_iiii:sb,dynCall_iiiii:tb,dynCall_vii:ub,establishStackSpace:T,stackAlloc:Q,stackRestore:S,stackSave:R}}) + + +// EMSCRIPTEN_END_ASM +(asmGlobalArg,asmLibraryArg,buffer);var ___errno_location=Module["___errno_location"]=asm["___errno_location"];var ___muldi3=Module["___muldi3"]=asm["___muldi3"];var ___udivdi3=Module["___udivdi3"]=asm["___udivdi3"];var _bitshift64Lshr=Module["_bitshift64Lshr"]=asm["_bitshift64Lshr"];var _bitshift64Shl=Module["_bitshift64Shl"]=asm["_bitshift64Shl"];var _compress=Module["_compress"]=asm["_compress"];var _decompress=Module["_decompress"]=asm["_decompress"];var _free=Module["_free"]=asm["_free"];var _i64Add=Module["_i64Add"]=asm["_i64Add"];var _i64Subtract=Module["_i64Subtract"]=asm["_i64Subtract"];var _malloc=Module["_malloc"]=asm["_malloc"];var _memcpy=Module["_memcpy"]=asm["_memcpy"];var _memmove=Module["_memmove"]=asm["_memmove"];var _memset=Module["_memset"]=asm["_memset"];var _sbrk=Module["_sbrk"]=asm["_sbrk"];var establishStackSpace=Module["establishStackSpace"]=asm["establishStackSpace"];var stackAlloc=Module["stackAlloc"]=asm["stackAlloc"];var stackRestore=Module["stackRestore"]=asm["stackRestore"];var stackSave=Module["stackSave"]=asm["stackSave"];var dynCall_ii=Module["dynCall_ii"]=asm["dynCall_ii"];var dynCall_iidiiii=Module["dynCall_iidiiii"]=asm["dynCall_iidiiii"];var dynCall_iiii=Module["dynCall_iiii"]=asm["dynCall_iiii"];var dynCall_iiiii=Module["dynCall_iiiii"]=asm["dynCall_iiiii"];var dynCall_vii=Module["dynCall_vii"]=asm["dynCall_vii"];Module["asm"]=asm;Module["ccall"]=ccall;if(memoryInitializer){if(!isDataURI(memoryInitializer)){memoryInitializer=locateFile(memoryInitializer)}if(ENVIRONMENT_IS_NODE||ENVIRONMENT_IS_SHELL){var data=readBinary(memoryInitializer);HEAPU8.set(data,GLOBAL_BASE)}else{addRunDependency("memory initializer");var applyMemoryInitializer=function(data){if(data.byteLength)data=new Uint8Array(data);HEAPU8.set(data,GLOBAL_BASE);if(Module["memoryInitializerRequest"])delete Module["memoryInitializerRequest"].response;removeRunDependency("memory initializer")};var doBrowserLoad=function(){readAsync(memoryInitializer,applyMemoryInitializer,function(){throw"could not load memory initializer "+memoryInitializer})};var memoryInitializerBytes=tryParseAsDataURI(memoryInitializer);if(memoryInitializerBytes){applyMemoryInitializer(memoryInitializerBytes.buffer)}else if(Module["memoryInitializerRequest"]){var useRequest=function(){var request=Module["memoryInitializerRequest"];var response=request.response;if(request.status!==200&&request.status!==0){var data=tryParseAsDataURI(Module["memoryInitializerRequestURL"]);if(data){response=data.buffer}else{console.warn("a problem seems to have happened with Module.memoryInitializerRequest, status: "+request.status+", retrying "+memoryInitializer);doBrowserLoad();return}}applyMemoryInitializer(response)};if(Module["memoryInitializerRequest"].response){setTimeout(useRequest,0)}else{Module["memoryInitializerRequest"].addEventListener("load",useRequest)}}else{doBrowserLoad()}}}var calledRun;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(args){args=args||arguments_;if(runDependencies>0){return}preRun();if(runDependencies>0)return;function doRun(){if(calledRun)return;calledRun=true;if(ABORT)return;initRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}Module["run"]=run;function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what+="";out(what);err(what);ABORT=true;EXITSTATUS=1;throw"abort("+what+"). Build with -s ASSERTIONS=1 for more info."}Module["abort"]=abort;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}noExitRuntime=true;run(); + +var HS_LOG_LEVEL = 0; + +function heatshrink_compress(inputBuffer) { + if (inputBuffer.BYTES_PER_ELEMENT!=1) throw new Error("Expecting Byte Array"); + var input_size = inputBuffer.length; + var output_size = input_size + (input_size/2) + 4; + + var bufIn = Module._malloc(input_size); + Module.HEAPU8.set(inputBuffer, bufIn); + // int compress(uint8_t *input, uint32_t input_size, uint8_t *output, uint32_t output_size, int log_lvl) + output_size = Module.ccall('compress', 'number', ['number','number','number','number','number'], [bufIn,input_size,0,0,HS_LOG_LEVEL/*log level*/])+1; + var bufOut = Module._malloc(output_size); + output_size = Module.ccall('compress', 'number', ['number','number','number','number','number'], [bufIn,input_size,bufOut,output_size,HS_LOG_LEVEL/*log level*/]); + // console.log("Compressed to "+output_size); + + var outputBuffer = new Uint8Array(output_size); + for (var i=0;i>16; + var pg=(p>>8)&255; + var pb=p&255; + var dr = r-pr; + var dg = g-pg; + var db = b-pb; + var d = dr*dr + dg*dg + db*db; + if (dthresh; + }, + "2bitbw":function(r,g,b) { + var c = (r+g+b) / 3; + c += 31; // rounding + if (c>255)c=255; + return c>>6; + }, + "4bit":function(r,g,b,a) { + var thresh = 128; + return ( + ((r>thresh)?1:0) | + ((g>thresh)?2:0) | + ((b>thresh)?4:0) | + ((a>thresh)?8:0)); + }, + "4bitmac":function(r,g,b,a) { + return PALETTE.lookup(PALETTE.MAC16,r,g,b,a, true /* no transparency */); + }, + "vga":function(r,g,b,a) { + return PALETTE.lookup(PALETTE.VGA,r,g,b,a); + }, + "web":function(r,g,b,a) { + return PALETTE.lookup(PALETTE.WEB,r,g,b,a); + }, + "rgb565":function(r,g,b,a) { + return ( + ((r&0xF8)<<8) | + ((g&0xFC)<<3) | + ((b&0xF8)>>3)); + }, + }; + var COL_TO_RGB = { + "1bit":function(c) { + return c ? 0xFFFFFFFF : 0xFF000000; + }, + "2bitbw":function(c) { + c = c&3; + c = c | (c<<2) | (c<<4) | (c<<6); + return 0xFF000000|(c<<16)|(c<<8)|c; + }, + "4bit":function(c) { + if (!(c&8)) return 0; + return ((c&1 ? 0xFF0000 : 0xFF000000) | + (c&2 ? 0x00FF00 : 0xFF000000) | + (c&4 ? 0x0000FF : 0xFF000000)); + }, + "4bitmac":function(c) { + return 0xFF000000|PALETTE.MAC16[c]; + }, + "vga":function(c) { + if (c==TRANSPARENT_8BIT) return 0; + return 0xFF000000|PALETTE.VGA[c]; + }, + "web":function(c) { + if (c==TRANSPARENT_8BIT) return 0; + return 0xFF000000|PALETTE.WEB[c]; + }, + "rgb565":function(c) { + var r = (c>>8)&0xF8; + var g = (c>>3)&0xFC; + var b = (c<<3)&0xF8; + return 0xFF000000|(r<<16)|(g<<8)|b; + }, + }; + // What Espruino uses by default + var BPP_TO_COLOR_FORMAT = { + 1 : "1bit", + 2 : "2bitbw", + 4 : "4bitmac", + 8 : "web", + 16 : "rgb565" + }; + + function clip(x) { + if (x<0) return 0; + if (x>255) return 255; + return x; + } + + + /* + See 'getOptions' for possible options + */ + function RGBAtoString(rgba, options) { + options = options||{}; + if (!rgba) throw new Error("No dataIn specified"); + if (!options.width) throw new Error("No Width specified"); + if (!options.height) throw new Error("No Height specified"); + if ("string"!=typeof options.diffusion) + options.diffusion = "none"; + options.compression = options.compression || false; + options.brightness = options.brightness | 0; + options.mode = options.mode || "1bit"; + options.output = options.output || "object"; + options.inverted = options.inverted || false; + options.transparent = !!options.transparent; + var transparentCol = undefined; + if (options.transparent) { + if (options.mode=="4bit") + transparentCol=0; + if (options.mode=="vga" || options.mode=="web") + transparentCol=TRANSPARENT_8BIT; + } + var bpp = COL_BPP[options.mode]; + var bitData = new Uint8Array(((options.width*options.height)*bpp+7)/8); + + function readImage() { + var pixels = new Int32Array(options.width*options.height); + var n = 0; + var er=0,eg=0,eb=0; + for (var y=0; y>>24; + var or = (cr>>16)&255; + var og = (cr>>8)&255; + var ob = cr&255; + if (options.diffusion.startsWith("error") && a>128) { + er = r-or; + eg = g-og; + eb = b-ob; + } else { + er = 0; + eg = 0; + eb = 0; + } + + n++; + } + } + return pixels; + } + function writeImage(pixels) { + var n = 0; + for (var y=0; y>3] |= c ? 128>>(n&7) : 0; + else if (bpp==2) bitData[n>>2] |= c<<((3-(n&3))*2); + else if (bpp==4) bitData[n>>1] |= c<<((n&1)?0:4); + else if (bpp==8) bitData[n] = c; + else if (bpp==16) { bitData[n<<1] = c>>8; bitData[1+(n<<1)] = c&0xFF; } + else throw new Error("Unhandled BPP"); + // Write preview + var cr = COL_TO_RGB[options.mode](c); + if (c===transparentCol) + cr = ((((x>>2)^(y>>2))&1)?0xFFFFFF:0); // pixel pattern + var oa = cr>>>24; + var or = (cr>>16)&255; + var og = (cr>>8)&255; + var ob = cr&255; + if (options.rgbaOut) { + options.rgbaOut[n*4] = or; + options.rgbaOut[n*4+1]= og; + options.rgbaOut[n*4+2]= ob; + options.rgbaOut[n*4+3]=255; + } + n++; + } + } + } + + var pixels = readImage(); + if (options.transparent && transparentCol===undefined && bpp<=16) { + // we have no fixed transparent colour - pick one that's unused + var colors = new Uint32Array(1<=0) + colors[pixels[i]]++; + // find an empty one + for (var i=0;i>2)^(y>>2))&1)?0xFFFFFF:0); + rgba[n*4] = rgba[n*4]*na + chequerboard*a; + rgba[n*4+1] = rgba[n*4+1]*na + chequerboard*a; + rgba[n*4+2] = rgba[n*4+2]*na + chequerboard*a; + rgba[n*4+3] = 255; + n++; + } + } + } + + /* RGBAtoString options, PLUS: + + updateCanvas: update canvas with the quantized image + */ + function canvastoString(canvas, options) { + options = options||{}; + options.width = canvas.width; + options.height = canvas.height; + var ctx = canvas.getContext("2d"); + var imageData = ctx.getImageData(0, 0, options.width, options.height); + var rgba = imageData.data; + if (options.updateCanvas) + options.rgbaOut = rgba; + var str = RGBAtoString(rgba, options); + if (options.updateCanvas) + ctx.putImageData(imageData,0,0); + return str; + } + + /* RGBAtoString options, PLUS: + + */ + function imagetoString(img, options) { + options = options||{}; + var canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + var ctx = canvas.getContext("2d"); + ctx.drawImage(img,0,0); + return canvastoString(canvas, options); + } + + function getOptions() { + return { + width : "int", + height : "int", + rgbaOut : "Uint8Array", // to store quantised data + diffusion : ["none"], + compression : "bool", + transparent : "bool", + brightness : "int", + mode : Object.keys(COL_BPP), + output : ["object","string","raw"], + inverted : "bool", + } + } + + /* Decode an Espruino image string into a URL, return undefined if it's not valid. + options = { + transparent : bool // should the image be transparent, or just chequered where transparent? + } */ + function stringToImageURL(data, options) { + options = options||{}; + var p = 0; + var width = 0|data.charCodeAt(p++); + var height = 0|data.charCodeAt(p++); + var bpp = 0|data.charCodeAt(p++); + var transparentCol = -1; + if (bpp&128) { + bpp &= 127; + transparentCol = 0|data.charCodeAt(p++); + } + var mode = BPP_TO_COLOR_FORMAT[bpp]; + if (!mode) return undefined; // unknown format + var bitmapSize = ((width*height*bpp)+7) >> 3; + // If it's the wrong length, it's not a bitmap or it's corrupt! + if (data.length != p+bitmapSize) + return undefined; + // Ok, build the picture + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext("2d"); + var imageData = ctx.getImageData(0, 0, width, height); + var rgba = imageData.data; + var no = 0; + var nibits = 0; + var nidata = 0; + for (var i=0;i>(nibits-bpp)) & ((1<>16)&255; // r + rgba[no++] = (cr>>8)&255; // g + rgba[no++] = cr&255; // b + rgba[no++] = cr>>>24; // a + } + if (!options.transparent) + RGBAtoCheckerboard(rgba, {width:width, height:height}); + ctx.putImageData(imageData,0,0); + return canvas.toDataURL(); + } + +// decode an Espruino image string into an HTML string, return undefined if it's not valid. See stringToImageURL + function stringToImageHTML(data, options) { + var url = stringToImageURL(data, options); + if (!url) return undefined; + return ''; + } + + // ======================================================= + return { + RGBAtoString : RGBAtoString, + RGBAtoCheckerboard : RGBAtoCheckerboard, + canvastoString : canvastoString, + imagetoString : imagetoString, + getOptions : getOptions, + + stringToImageHTML : stringToImageHTML, + stringToImageURL : stringToImageURL + }; +})); diff --git a/package.json b/package.json index 400385139..34ee837d8 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "scripts": { "test": "node bin/sanitycheck.js", "start": "npx http-server" + }, + "dependencies": { + "acorn": "^7.2.0" } }