diff --git a/README.md b/README.md index ed6a501ef..8d906e1d7 100644 --- a/README.md +++ b/README.md @@ -1,574 +1,74 @@ -Bangle.js App Loader (and Apps) -================================ +# Regatta Timer 5-4-1 countdown -[![Build Status](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml/badge.svg)](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml) +## Modes -* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) -* Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/) +* **Idle** + On startup the application is in idle mode showing a large 5 in the centre of the screen and the time of day below. + `Button` switches to start mode. +* **Start** + During the countdown, the screen changes the layout several times to use as much space as + possible to display the numbers. + When time is up the buzzer sounds and the application switches to race mode. + `Button` switches to idle mode. +* **Race** + Race time, local time, SOA, number reachable GPS satellites and battery level are shown. + `Button` switches to "stopped mode". +* **Stoped** + The race counter stops. + `Button` switches to idle mode. -**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. +## Screenshots -## How does it work? +*Idle mode: showing a big 5 and time of day below* -* A list of apps is in `apps.json` (this is auto-generated from all the `apps/yourapp/metadata.json` using Jekyll or `bin/create_apps_json.sh`) -* Each element references an app in `apps/` which is uploaded -* When it starts, BangleAppLoader checks the JSON and compares -it with the files it sees in the watch's storage. -* To upload an app, BangleAppLoader checks the files that are -listed in `apps.json`, loads them, and sends them over Web Bluetooth. +![Idle mode: showing a big 5 and time of day below](screenshot-1.png) -## Getting Started +*Start mode: minutes and seconds* -Check out: +![Start mode: minutes and seconds](screenshot-2.png) -* [Building your first Bangle.js Application](https://www.espruino.com/Bangle.js+First+App) -* [Adding an app to the Bangle.js App Loader](https://www.espruino.com/Bangle.js+App+Loader) -* [Customising the App Loader](https://www.espruino.com/Bangle.js+App+Loader+Custom) +*Start mode: seconds* -## What filenames are used +![Start mode: seconds](screenshot-3.png) -Filenames in storage are limited to 28 characters. To -easily distinguish between file types, we use the following: +*Race mode: elapsed time, time of day, speed, satellites, battery* -* `stuff.info` is JSON that describes an app - this is auto-generated by the App Loader -* `stuff.img` is an image -* `stuff.app.js` is JS code for applications -* `stuff.wid.js` is JS code for widgets -* `stuff.settings.js` is JS code for the settings menu -* `stuff.boot.js` is JS code that automatically gets run at boot time -* `stuff.json` is used for JSON settings for an app +![Race mode: elapsed time, time of day, speed, satellites, battery](screenshot-4.png) -## Developing your own app +*Race mode: with german abbreviations* -* Head over to [the Web IDE](https://www.espruino.com/ide/) and ensure `Save on Send` in settings set to the *default setting* of `To RAM` -* We'd recommend that you start off using code from 'Example Applications' (below) to get started... -* Load [`app.js`](apps/_example_app/app.js) or [`widget.js`](apps/_example_widget/widget.js) into the IDE and start developing. -* The `Upload` button will load your app to Bangle.js temporarily +![Race mode: with german abbreviations](screenshot-5.png) -## Adding your app to the menu +*Settings page: main* -* Come up with a unique (all lowercase, no spaces) name, we'll assume `myappid`. Bangle.js -is limited to 28 char filenames and appends a file extension (eg `.js`) so please -try and keep filenames short to avoid overflowing the buffer. -* Create a folder called `apps/`, lets assume `apps/myappid` -* We'd recommend that you copy files from one of the Examples in `apps/_example_*` (see below), or... -* `apps/myappid/app.png` should be a 48px icon -* Use http://www.espruino.com/Image+Converter to create `apps/myappid/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String" -* Create/modify `apps/myappid/metadata.json` as follows: +![Settings page: main](screenshot-6.png) -``` -{ "id": "myappid", - "name": "My app's human readable name", - "shortName" : "Short Name", - "icon": "app.png", - "description": "A detailed description of my great app", - "tags": "", - "storage": [ - {"name":"myappid.app.js","url":"app.js"}, - {"name":"myappid.img","url":"app-icon.js","evaluate":true} - ], -}, -``` +*Settings page: choose the theme* -### Screenshots +![Settings page: choose the theme](screenshot-7.png) -In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { "url":"screenshot.png" } ],` +## Localization -To get a screenshot you can: +Localization is done by the Bangle.js 2 app "Languages" +* Go to [banglejs.com/apps](https://banglejs.com/apps/) +* Search for app "Languages" +* Click the "arrow up" or "burger" icon +* Choose your language from the dropdown +* Click `upload` -* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then -right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is -unable to read data back from the LCD controller). -* Run your code in the emulator and use the screenshot button in the bottom right of the window. +**Some nautical abbreviations which are not part of the Bangle.js 2 app "Languages" app are stored in `translations.json`.** +## Feedback -## Testing +Report bugs or request a feature at [github.com/naden](https://github.com/naden) -### Online +## Roadmap +* add a seconds coundown layout; mimic a classic regatta chronograph +* add recording of gps course and race time +* add icons for light mode +* add flag icons -This is the best way to test... +## Created by +© 2021 - 2024 [naden.de](https://naden.de) -* Fork the https://github.com/espruino/BangleApps git repository -* Add your files -* Go to GitHub Settings and activate GitHub Pages -* Run your personal `Bangle App Loader` at https://\.github.io/BangleApps/index.html to load apps onto your device -* Your apps should be inside it - if there are problems, check your web browser's 'developer console' for errors - -**Note:** It's a great idea to get a local copy of the repository on your PC, -then run `bin/sanitycheck.js` - it'll run through a bunch of common issues -that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule update --init`. - -Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect. - -### Offline - -Using the 'Storage' icon in [the Web IDE](https://www.espruino.com/ide/) -(4 discs), upload your files into the places described in your JSON: - -* `app-icon.js` -> `myappid.img` - -Now load `app.js` up in the editor, and click the down-arrow to the bottom -right of the `Send to Espruino` icon. Click `Storage` and then either choose -`myappid.app.js` (if you'd uploaded your app previously), or `New File` -and then enter `myappid.app.js` as the name. - -Now, clicking the `Send to Espruino` icon will load the app directly into -Espruino **and** will automatically run it. - -When you upload code this way, your app will even be uploaded to Bangle.js's menu -without you having to use the `Bangle App Loader` - -**Note:** Widgets need to be run inside a clock or app, so if you're -developing a widget you need to go go `Settings` -> `Communications` -> `Load after saving` -and set it to `Load default application`. - -## Example Applications - -To make the process easier we've come up with some example applications that you can use as a base -when creating your own. Just come up with a unique name (ideally lowercase, under 20 chars), copy `apps/_example_app` -or `apps/_example_widget` to `apps/myappid`, and edit `apps/myappid/metadata.json` accordingly. - -**Note:** the max filename length is 28 chars, so we suggest an app ID of under -20 so that when `.app.js`/etc gets added to the end the filename isn't cropped. - -**If you're making a widget** please start the name with `wid` to make -it easy to find! - -### App Example - -The app example is available in [`apps/_example_app`](apps/_example_app) - -Apps are listed in the Bangle.js menu, accessible from a clock app via the middle button. - -* `metadata.json` - describes the app to bootloader and loader -* `app.png` - app icon - 48x48px -* `app-icon.js` - JS version of the icon (made with http://www.espruino.com/Image+Converter) for use in Bangle.js's menu -* `app.js` - app code -* `ChangeLog` - A file containing a list of changes to your app so users can see what's changed - -#### `app-icon.js` - -The icon image and short description is used in Bangle.js's launcher. - -Use the Espruino [image converter](https://www.espruino.com/Image+Converter) and upload your `app.png` file. - -Follow this steps to create a readable icon as image string. - -1. upload a 48x48 png file - THE IMAGE SHOULD BE 48x48 OR LESS -2. set _X_ Use Compression -3. set _X_ Transparency (optional) -4. set Diffusion: _flat_ -5. set Colours: _1 bit_, any of the Optimised options, or _8 bit Web Palette_ are best -6. set Output as: _Image String_ - -Replace this line with the image converter output: - -``` -require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) -``` - -**Do not add a trailing semicolon** - -You can also use this converter for creating images you like to draw with `g.drawImage()` with your app. - -Apps that need widgets can call `Bangle.loadWidgets()` **once** at startup to load -them, and then `Bangle.drawWidgets()` to draw them onto the screen whenever the app -has call to completely clear the screen. Widgets themselves will update as and when needed. - -### Widget Example - -The widget example is available in [`apps/_example_widget`](apps/_example_widget) - -* `metadata.json` - describes the widget to bootloader and loader -* `widget.js` - widget code - -Widgets are just small bits of code that run whenever an app that supports them -calls `Bangle.loadWidgets()`. If they want to display something in the 24px high -widget bar at the top of the screen they can add themselves to the global -`WIDGETS` array with: - -``` -WIDGETS["mywidget"]={ - area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) - sortorder:0, // (Optional) determines order of widgets in the same corner - width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout - draw:draw // called to draw the widget -}; -``` - -When the widget is to be drawn, `x` and `y` values are set up in `WIDGETS["mywidget"]` -and `draw` can then use `this.x` and `this.y` to figure out where it needs to draw to. - - -### ChangeLog - -This is a file containing a list of changes to your app so users can see what's changed, for example: - -``` -0.01: New App! -0.02: Changed the colors -0.03: Made the app run quicker -``` - -Entries should be newest last, with the version number of the last entry matching the version in `metadata.json` - -Please keep the same format at the example as the file needs to be parsed by the BangleApps tools. - -### `app.info` format - -This is the file that's **auto-generated** from `metadata.json` and loaded onto Bangle.js by the App Loader, -and which gives information about the app for the Launcher. - -``` -{ - "name":"Short Name", // for Bangle.js menu - "icon":"*myappid", // for Bangle.js menu - "src":"-myappid", // source file - "type":"widget/clock/app/bootloader/...", // optional, default "app" - // see 'type' in 'metadata.json format' below for more options/info - "version":"1.23", - // added by BangleApps loader on upload based on metadata.json - "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 -} -``` - -### `metadata.json` format - -``` -{ "id": "appid", // 7 character app id - "name": "Readable name", // readable name - "shortName": "Short name", // short name for launcher - "version": "0v01", // the version of this app - "description": "...", // long description (can contain markdown) - "icon": "icon.png", // icon in apps/ - "screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app - "type":"...", // optional(if app) - - // 'app' - an application - // 'clock' - a clock - required for clocks to automatically start - // 'widget' - a widget - // 'module' - this provides a module that can be used with 'require'. - // 'provides_modules' should be used if type:module is specified - // 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js' - // 'settings' - apps that appear in Settings->Apps (with appname.settings.js) but that have no 'app.js' - // 'clkinfo' - Provides a 'myapp.clkinfo.js' file that can be used to display info in clocks - see modules/clock_info.js - // 'RAM' - code that runs and doesn't upload anything to storage - // 'launch' - replacement 'Launcher' - // 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle - // 'scheduler' - provides 'sched' library and boot code for scheduling alarms/timers - // (currently only 'sched' app) - // 'notify' - provides 'notify' library for showing notifications - // 'locale' - provides 'locale' library for language-specific date/distance/etc - // (a version of 'locale' is included in the firmware) - "tags": "", // comma separated tag list for searching - // common types are: - // 'clock' - it's a clock - // 'widget' - it is (or provides) a widget - // 'outdoors' - useful for outdoor activities - // 'tool' - a useful utility (timer, calculator, etc) - // 'game' - a game - // 'bluetooth' - uses Bluetooth LE - // 'system' - used by the system - // 'clkinfo' - provides or uses clock_info module for data on your clock face or clocks that support it (see apps/clock_info/README.md) - // 'health' - e.g. heart rate monitors or step counting - "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 - "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above) - "dependencies" : { "messages":"app" } // optional, depend on a specific app ID - // for instance this will use notify/notifyfs is they exist, or will pull in 'notify' - "dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' - see provides_modules - "dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets - "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require' - "provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message' - "default" : true, // set if an app is the default implementer of something (a widget/module/etc) - "readme": "README.md", // if supplied, a link to a markdown-style text file - // that contains more information about this app (usage, etc) - // A 'Read more...' link will be added under the app - - "custom": "custom.html", // if supplied, apps/custom.html is loaded in an - // iframe, and it must post back an 'app' structure - // like this one with 'storage','name' and 'id' set up - // see below for more info - - "customConnect": true, // if supplied, ensure we are connected to a device - // before the "custom.html" iframe is loaded. An - // onInit function in "custom.html" is then called - // with info on the currently connected device. - - "interface": "interface.html", // if supplied, apps/interface.html is loaded in an - // iframe, and it may interact with the connected Bangle - // to retrieve information from it - // see below for more info - - "allow_emulator":true, // if 'app.js' will run in the emulator, set to true to - // add an icon to allow your app to be tested - - "storage": [ // list of files to add to storage - {"name":"appid.js", // filename to use in storage. - // If name=='RAM', the code is sent directly to Bangle.js and is not saved to a file - "url":"", // URL of file to load (currently relative to apps/) - "content":"...", // if supplied, this content is loaded directly - "evaluate":true, // if supplied, data isn't quoted into a String before upload - // (eg it's evaluated as JS) - "noOverwrite":true // if supplied, this file will not be overwritten if it - // already exists - "supports": ["BANGLEJS2"]// if supplied, this file will ONLY be uploaded to the device - // types named in the array. This allows different versions of - // the app to be uploaded for different platforms - }, - ] - "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 - "url":"", // if supplied URL of file to load (currently relative to apps/) - "content":"...", // if supplied, this content is loaded directly - "evaluate":true, // if supplied, data isn't quoted into a String before upload - // (eg it's evaluated as JS) - }, - {"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`, `bluetooth` 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 - -### `metadata.json`: `custom` element - -Apps that can be customised need to define a `custom` element in `metadata.json`, -which names an HTML file in that app's folder. - -When `custom` is defined, the 'upload' button is replaced by a customize -button, and when clicked it opens the HTML page specified in an iframe. - -In that HTML file you're then responsible for handling a button -press and calling `sendCustomizedApp` with your own customised -version of what's in `metadata.json`: - -``` - - - - - -

- - - - -``` - -This'll then be loaded in to the watch. See [apps/qrcode/grcode.html](the QR Code app) -for a clean example. - -**Note:** we specify a `url` for JS files even though it doesn't have to exist -and will never be loaded. This is so the app loader can tell if it's a JavaScript -file based on the extension, and if so it can minify and pretokenise it. - -### `metadata.json`: `interface` element - -Apps that create data that can be read back can define a `interface` element in `metadata.json`, -which names an HTML file in that app's folder. - -When `interface` is defined, a `Download from App` button is added to -the app's description, and when clicked it opens the HTML page specified -in an iframe. - -``` - - - - - - -
Loading...
- - - -``` - -When the page is ready a function called `onInit` is called, -and in that you can call `Puck.write` and `Puck.eval` to get -the data you require from Bangle.js. - -See [apps/gpsrec/interface.html](the GPS Recorder) for a full example. - -### Adding configuration to the "Settings" menu - -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 -argument, `back`: a callback to return to the settings menu. - -Usually it will save any information in `myappid.json` where `myappid` 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('myappid.json',1)||{}; - if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value - function save(key, value) { - settings[key] = value; - require('Storage').write('myappid.json', settings); - } - const appMenu = { - '': {'title': 'App Settings'}, - '< Back': back, - 'Monkeys': { - value: settings.monkeys, - onchange: (m) => {save('monkeys', m)} - } - }; - E.showMenu(appMenu) -}) -``` -In this example the app needs to add `myappid.settings.js` to `storage` in `metadata.json`. -It should also add `myappid.json` to `data`, to make sure it is cleaned up when the app is uninstalled. -```json - { "id": "myappid", - ... - "storage": [ - ... - {"name":"myappid.settings.js","url":"settings.js"} - ], - "data": [ - {"name":"myappid.json"} - ] - }, -``` - -## Modules - -You can include any of [Espruino's modules](https://www.espruino.com/Modules) as -normal with `require("modulename")`. To include [Bangle's modules](modules) for use in the Web -IDE, [upload the modules to internal storage](modules#upload-the-module-to-the-bangles-internal-storage) -or [change the IDE's search path](modules#change-the-web-ide-search-path-to-include-banglejs-modules). -If you want to develop your own module for your -app(s) then you can do that too. Just add the module into the `modules` folder -then you can use it from your app as normal. - -You won't be able to develop apps using your own modules with the IDE, -so instead we'd recommend you write your module to a Storage File called -`modulename` on Bangle.js. You can then develop your app as normal on Bangle.js -from the IDE. - -## Coding hints - -- use `g.setFont(.., size)` to multiply the font size, eg ("6x8",3) : "18x24" - -- use `g.drawString(text,x,y,true)` to draw with background color to overwrite existing text - -- use `g.clearRect()` to clear parts of the screen, instead of using `g.clear()` - -- use `g.fillPoly()` or `g.drawImage()` for complex graphic elements - -- using `g.clear()` can cause screen flicker - -- using `g.setLCDBrightness()` can save you power during long periods with lcd on - -- chaining graphics methods, eg `g.setColor(0xFD20).setFontAlign(0,0).setfont("6x8",3)` - -### Misc Notes - -- Need to save state? Use the `E.on('kill',...)` event to save JSON to a file called `myappid.json`, then load it at startup. - -- 'Alarm' apps define a file called `alarm.js` which handles the actual alarm window. - -- Locale is handled by `require("locale")`. An app may create a `locale` file in Storage which is -a module that overwrites Bangle.js's default locale. - - -### Graphic areas - -The screen is parted in a widget and app area for lcd mode `direct`(default). - -| areas | as rectangle or point | -| :-:| :-: | -| Widget | (0,0,239,23) | -| Apps | (0,24,239,239) | -| BTN1 | (230, 55) | -| BTN2 | (230, 140) | -| BTN3 | (230, 210) | -| BTN4 | (0,0,119, 239)| -| BTN5 | (120,0,239,239) | - -- Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`. - -- For BTN4-5 the touch area is named - -## Available colors - -You can use `g.setColor(r,g,b)` OR `g.setColor(16bitnumber)` - some common 16 bit colors are below: - -| color-name | color-value| -| :-: | :-: | -| Black | 0x0000 | -| Navy | 0x000F | -| DarkGreen | 0x03E0 | -| DarkCyan | 0x03EF | -| Maroon | 0x7800 | -| Purple | 0x780F | -| Olive | 0x7BE0 -| LightGray | 0xC618 -| DarkGrey | 0x7BEF -| Blue | 0x001F -| Green | 0x07E0 | -| Cyan | 0x07FF | -| RED | 0xF800 | -| Magenta | 0xF81F | -| Yellow | 0xFFE0 | -| White | 0xFFFF | -| Orange | 0xFD20 | -| GreenYellow | 0xAFE5 | -| Pink | 0xF81F | - -## API Reference - -[Reference](http://www.espruino.com/Reference#software) - -[Bangle Class](https://banglejs.com/reference#Bangle) - -[Graphics Class](https://banglejs.com/reference#Graphics) - -## 'Testing' folder - -The [`testing`](testing) folder contains snippets of code that might be useful for your apps. - -* `testing/colors.js` - 16 bit colors as name value pairs -* `testing/gpstrack.js` - code to store a GPS track in Bangle.js storage and output it back to the console - -## Credits - -The majority of icons used for these apps are from [Icons8](https://icons8.com/) - we have a commercial license but icons are also free for Open Source projects. +Icons by [Icons8](https://icons8.com/) diff --git a/app-icon.js b/app-icon.js new file mode 100644 index 000000000..3c6f27c44 --- /dev/null +++ b/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("qFQ4UB8H/AAIJBoGtqoACDZYPDCRwUGqATNgoTDoATNgISCqhtPio6QHgg6OHggJGn+q1X8PJAHFnwSBAAO8CYxiFgeq1/Agf61XAMgpiFnWvAof61hkFCYkD1YhEgfqAwkFOwk62EAhkCwEwgEOFAkBCYgfCnYTB9gvCCZECDwMshgGBmE4GAOACY8KHQIjBAAQTBh2gCYY6EgHwTIsPgA8EAAeogAeDGAcAlQSGgQRBE5EKKAYdDA4wfGYgo6HHgbKFgHrgB3BAA0OBgQAEDQw0Hcog5IHojyEgWwWAgAFncOOAkO4ATLgZbENYIAMJIk7CZo0ElcDCRfA9AFD9A8M0ErFgjlBABXwJQiyMWgyyMQwrGBmASLhjIDUgPs//wBoc8ZAwTEmGqKQiEDh2shATDdwMA16nE1ADCh4dBeAcCAYJlEgQTDYoQPCAYevFwQfBYAgTH0eqFAcKlATEhQTG2GsgeqgE4CYM6LQITHDoUD/8A9fq4E/SYI7HgfwO4fq1RVDCY2wgevT4krfgqLDWYXqI4IACE4cO1XMY4vsn41DgBSBBgX/bYugmD/HAAcMhQgDYogAJhRWFABk6XQkPCRfwnYFD9AtEAA+gSQgEEABPoAgYsEABLTDNAo8KAgakBDQgAFnbCBAwbwBCZbuDZAayMc4i0NWQgAB9cAIYhbEBgQaGHpCDBGg0KKwgAFgQeGA4XwBIr5BD5GoHg46BlQwH9QnJ1YGDoAeDO4MsBYc4O4IwDgITDgRsBlgiBFgITBnRODCYg8BXgM6gWADIMDHQgTFnQ8BhgTBmA6BTokBqDyE1blEgYvCAAUFCYgoB14FD/TEFgtUAwkD1Wv4ED/WqEwkAitVTIs+1QAC3gLFqoTGgE/CQP8BQwTBPAgALgITBMgoAKgoTBMgoAKMQI8QHQQ8QHQQACCZoSEChoPD")) diff --git a/app.js b/app.js new file mode 100644 index 000000000..30d03daed --- /dev/null +++ b/app.js @@ -0,0 +1,335 @@ +/** + * Regatta Timer + */ +const Layout = require("Layout"); +const locale = require("locale").name == "system" ? "en" : require("locale").name.substring(0, 2); +const hs = require("heatshrink"); + +// "Anton" bold font +Graphics.prototype.setFontAnton = function(scale) { + // Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16)); +}; + +function Regattatimer() { + return { + layout: undefined, + /* + layouts: { + idle: function() { + switch(settings.dial) { + case "Discs": + break; + case "Numeric": + default: + break; + } + }, + start: function(phase) { + switch(settings.dial) { + case "Discs": + break; + case "Numeric": + default: + break; + } + }, + race: function() { + + } + }, + */ + mode: "idle", // idle, start, race" + countdown: 300, // 5 minutes + counter: undefined, + interval: undefined, + theme: null, + themes: { + "Light": { + "fgColor": "#000000", + "bgColor": "#FFFF00", + "icons": { + "satellites": function() { + return hs.decompress(atob("jEYxH+AH4Ab6QIIBJAfNAAQtSC4gxSCwgYHHBYYMC6IYPC5AZOC8QYMC5YYLC5inSDH4waVbAYJCpgA/AAI=")); + }, + "battery": function() { + return hs.decompress(atob("jEYxH+AHHSAAgXmCgoaRC/4X/C/4X/C/4X/C64Ap")); + } + } + }, + "Dark": { + "fgColor": "#FFFF00", + "bgColor": "#000000", + "icons": { + "satellites": function() { + return hs.decompress(atob("jEYxH+AH4Ab6QIIBJAfNAAQtSC4gxSCwgYHHBYYMC6IYPC5AZOC8QYMC5YYLC5inSDH4waVbAYJCpgA/AAI=")); + }, + "battery": function() { + return hs.decompress(atob("jEYxH+AHHSAAgXmCgoaRC/4X/C/4X/C/4X/C64Ap")); + } + } + } + }, + settings: Object.assign({ + "debug": false, + "buzzer": true, + "dial": "Numeric", + "gps": true, + "record": false, + "theme": "Dark", + }, require('Storage').readJSON("regattatimer.json", true) || {}), + + translations: Object.assign({ + "de": { + "speed": "FüG", // Fahrt über Grund + "speed_unit": "kn" + }, + "en": { + "speed": "SOA", // SOA speed of advance + "speed_unit": "kn" + } + }, require('Storage').readJSON("translations.json", true) || {}), + + init: function() { + + if(this.settings.debug) { + this.countdown = 1; + } + + this.theme = this.themes[this.settings.theme]; + + Bangle.setLCDPower(1); + Bangle.setLCDTimeout(0); + + // in "idle", "start" or "stoped" mode, a button click (re)starts the countdown + // in "race" mode, a button click stops the counter + var onButtonClick = (function(ev) { + switch(this.mode) { + case "idle": + this.resetCounter(); + this.mode = "start"; + this.setLayoutStartMinSec(); + this.startCounter(); + this.interval = setInterval((function() { + this.startCounter(); + }).bind(this), 1000); + break; + case "stoped": + case "start": + this.resetCounter(); + this.setLayoutIdle(); + break; + case "race": + this.raceCounterStop(); + break; + } + }).bind(this); + + setWatch(onButtonClick, BTN1, true); + + this.setLayoutIdle(); + }, + + onGPS: function(fix) { + if(this.mode == "race") { + if(fix.fix && isFinite(fix.speed)) { + this.layout.clear(layout.speed); + this.layout.speed.label = fix.speed.toFixed(2); + this.layout.render(this.layout.speed); + } + this.layout.satellites.label = fix.satellites; + } + }, + + translate: function(slug) { + return this.translations[locale][slug]; + }, + // during the start phase, the clock counts down 5 4 1 0 minutes + // a button click restarts the countdown + startCounter: function() { + + this.counter --; + + if(this.counter >= 0) { + var counterMinutes = parseInt(this.counter / 60); + + if(counterMinutes > 0) { + this.layout.minutes.label = counterMinutes; + // this.layout.seconds.label = "0".concat(this.counter - counterMinutes * 60).toString().slice(-2); + this.layout.seconds.label = this.padZeroLeft(this.counter - counterMinutes * 60); + this.layout.render(); + } + else { + this.setLayoutStartSec(); + this.layout.seconds.label = this.counter.toString(); + this.layout.render(); + } + // this keeps the watch LCD lit up + g.flip(); + } + // time is up + else { + this.raceCounterStart(); + } + }, + padZeroLeft: function(str) { + return str.toString().padStart(2, "0"); + }, + formatTime: function(time) { + var + minutes = parseInt(time / 60), + seconds = time - (minutes * 60); + + return this.padZeroLeft(parseInt(time / 3600)) + ":" + this.padZeroLeft(minutes) + ":" + this.padZeroLeft(seconds); + }, + raceCounter: function() { + + if(this.counter % 60 == 0) { + this.layout.clear(this.layout.battery); + this.layout.battery.label = E.getBattery() + "%"; + this.layout.render(this.layout.battery); + } + + this.counter ++; + + this.layout.racetime.label = this.formatTime(this.counter); + this.layout.daytime.label = require("locale").time(new Date(), 1); + this.layout.render(); + + // keeps the watch screen lit up + g.flip(); + }, + raceCounterStop: function() { + if(this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + this.mode = "stoped"; + }, + raceCounterStart: function() { + if(this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + + if(this.settings.buzzer) { + Bangle.buzz(); + } + + this.counter = 0; + // switch to race mode + this.mode = "race"; + this.setLayoutRace(); + this.raceCounter(); + this.interval = setInterval((function() { + this.raceCounter(); + }).bind(this), 1000); + }, + + resetCounter: function() { + if(this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + this.counter = this.countdown; + }, + + setLayoutIdle: function() { + + g.clear(); + + this.mode = "idle"; + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c: [ + { + type: "v", + c: [ + {type: "txt", font: "Anton", label: "5", col: this.theme.fgColor, id: "minutes", fillx: 1, filly: 1}, + {type: "txt", font: "20%", label: "--:--", col: this.theme.fgColor, id: "daytime", fillx: 1, filly: 1} + ] + } + ]}, {lazy: true}); + + this.interval = setInterval((function() { + this.layout.daytime.label = require("locale").time(new Date(), 1); + this.layout.render(); + + // keeps the watch screen lit up + g.flip(); + }).bind(this), 1000); + }, + setLayoutStartMinSec: function() { + g.clear(); + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c: [ + { + type: "h", + c: [ + {type: "txt", font: "Anton", label: "4", col: this.theme.fgColor, id: "minutes", fillx: 1, filly: 1}, + {type: "txt", font: "Anton", label: "59", col: this.theme.fgColor, id: "seconds", fillx: 1, filly: 1}, + ] + } + ]}, {lazy: true} + ); + }, + setLayoutStartSec: function() { + g.clear(); + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c:[ + {type: "txt", font: "Anton", label: "", fillx: true, filly: true, col: this.theme.fgColor, id: "seconds"}, + ]}, {lazy: true}); + }, + setLayoutRace: function() { + g.clear(); + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c: [ + {type: "txt", font: "20%", label: "00:00:00", col: this.theme.fgColor, pad: 4, filly: 1, fillx: 1, id: "racetime"}, + {type: "txt", font: "15%", label: "-", col: this.theme.fgColor, pad: 4, filly:1, fillx:1, id: "daytime"}, + // horizontal + {type: "h", c: [ + {type: "txt", font: "10%", label: this.translate("speed"), col: this.theme.fgColor, pad:4, fillx:1, filly:1}, + {type: "txt", font: "20%", label: "0", col: this.theme.fgColor, pad:4, fillx:1, filly:1, id: "speed"}, + {type: "txt", font: "10%", label: this.translate("speed_unit"), col: this.theme.fgColor, pad:4, fillx:1, filly:1}, + ]}, + {type: "h", c: [ + {type:"img", pad: 2, src: this.theme["icons"].satellites()}, + {type: "txt", font: "10%", label: "0", col: this.theme.fgColor, pad: 2, filly:1, id: "satellites"}, + // hacky, use empty element with fillx to push the other elments to the left an right side + {type: undefined, pad: 2, fillx: 1}, + {type:"img", pad: 2, src: this.theme["icons"].battery()}, + {type: "txt", font: "10%", label: "-", col: this.theme.fgColor, pad: 2, filly: 1, id: "battery"}, + ]} + ]}, {lazy: true}); + } + }; +} + +var regattatimer = Regattatimer(); +regattatimer.init(); + +if(regattatimer.settings.gps) { + Bangle.setGPSPower(1); + Bangle.on('GPS', regattatimer.onGPS.bind(regattatimer)); +} + +Bangle.on('kill', function() { + Bangle.setLCDPower(0); + Bangle.setLCDTimeout(10); + + if(regattatimer.settings.gps) { + Bangle.setGPSPower(0); + } +}); + diff --git a/icon.png b/icon.png new file mode 100644 index 000000000..47712c7ed Binary files /dev/null and b/icon.png differ diff --git a/regattatimer.json b/regattatimer.json new file mode 100644 index 000000000..e2ea78f76 --- /dev/null +++ b/regattatimer.json @@ -0,0 +1,8 @@ +{ + "debug": false, + "buzzer": true, + "dial": "Numeric", + "gps": true, + "record": false, + "theme": "Dark" +} diff --git a/screenshot-6.png b/screenshot-6.png new file mode 100644 index 000000000..391eaafd2 Binary files /dev/null and b/screenshot-6.png differ diff --git a/settings.js b/settings.js new file mode 100644 index 000000000..cdcdf53f5 --- /dev/null +++ b/settings.js @@ -0,0 +1,78 @@ +(function(back) { + var + file = "regattatimer.json", + + storage = require("Storage"), + + dials = ["Numeric", "Discs"], + + themes = ["Light", "Dark"], + + settings = Object.assign({ + "debug": false, + "buzzer": true, + "dial": "Numeric", + "gps": true, + "record": false, + "theme": "Dark", + }, storage.readJSON(file, true) || {}); + + function save(key, value) { + settings[key] = value; + storage.writeJSON(file, settings); + } + + E.showMenu({ + "" : { "title" : "Regatta Timer" }, + "< Back" : () => back(), + "GPS": { + value: !!settings.gps, // !! converts undefined to false + onchange: v => { + save("gps", v); + } + }, + "THEME": { + value: themes.indexOf(settings.theme), + min: 0, + max: themes.length - 1, + step: 1, + wrap: true, + format: v => themes[v], + onchange: (d) => { + save("theme", themes[d]); + } + }, + "BUZZER": { + value: !!settings.buzzer, // !! converts undefined to false + onchange: v => { + save("buzzer", v); + } + }, + /* + "DIAL": { + value: dials.indexOf(settings.dial), + min: 0, + max: dials.length - 1, + step: 1, + wrap: true, + format: v => dials[v], + onchange: (d) => { + save("dial", dials[d]); + } + }, + "RECORD": { + value: !!settings.record, // 0| converts undefined to 0 + onchange: v => { + settings.record = v; + save("record", v); + } + }, + */ + "DEBUG": { + value: !!settings.debug, // 0| converts undefined to 0 + onchange: v => { + save("debug", v); + } + }, + }); +})(load)