Merge branch 'master' of https://github.com/espruino/BangleApps into app/sonic-clock

pull/1367/head
Joseph Moroney 2022-01-29 08:52:06 +10:00
commit 9d5ce3ae7d
766 changed files with 20721 additions and 7455 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ appdates.csv
_config.yml
tests/Layout/bin/tmp.*
tests/Layout/testresult.bmp
apps.local.json

View File

@ -12,7 +12,7 @@ and that it is not licensed in another way that would make this impossible.
## How does it work?
* A list of apps is in `apps.json`
* 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/<id>` which is uploaded
* When it starts, BangleAppLoader checks the JSON and compares
it with the files it sees in the watch's storage.
@ -53,10 +53,10 @@ easily distinguish between file types, we use the following:
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/<id>`, lets assume `apps/myappid`
* We'd recommend that you copy files from 'Example Applications' (below) as a base, or...
* 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 an entry in `apps.json` as follows:
* Create/modify `apps/myappid/metadata.json` as follows:
```
{ "id": "myappid",
@ -116,8 +116,7 @@ and set it to `Load default application`.
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 add `apps/_example_X/add_to_apps.json` to
`apps.json`.
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.
@ -131,7 +130,7 @@ 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.
* `add_to_apps.json` - insert into `apps.json`, describes the app to bootloader and loader
* `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
@ -144,11 +143,11 @@ Use the Espruino [image converter](https://www.espruino.com/Image+Converter) and
Follow this steps to create a readable icon as image string.
1. upload a png file
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_, _4 bit_ or _8 bit Web Palette_
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:
@ -157,6 +156,8 @@ 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
@ -167,17 +168,18 @@ has call to completely clear the screen. Widgets themselves will update as and w
The widget example is available in [`apps/_example_widget`](apps/_example_widget)
* `add_to_apps.json` - insert into `apps.json`, describes the widget to bootloader and loader
* `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 bars at the top and bottom of the screen they can add themselves to
the global `WIDGETS` array with:
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)
area:"tl", // tl (top left), tr (top 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
};
@ -202,7 +204,7 @@ and which gives information about the app for the Launcher.
// if it's 'clock' then it'll be loaded by default at boot time
// if this is 'bootloader' then it's code that is run at boot time, but is not in a menu
"version":"1.23",
// added by BangleApps loader on upload based on apps.json
// 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
@ -214,7 +216,7 @@ and which gives information about the app for the Launcher.
}
```
### `apps.json` format
### `metadata.json` format
```
{ "id": "appid", // 7 character app id
@ -293,9 +295,9 @@ and which gives information about the app for the Launcher.
* 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
### `metadata.json`: `custom` element
Apps that can be customised need to define a `custom` element in `apps.json`,
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
@ -303,7 +305,7 @@ 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 `apps.json`:
version of what's in `metadata.json`:
```
<html>
@ -335,9 +337,9 @@ for a clean example.
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.
### `apps.json`: `interface` element
### `metadata.json`: `interface` element
Apps that create data that can be read back can define a `interface` element in `apps.json`,
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
@ -401,7 +403,7 @@ Example `settings.js`
E.showMenu(appMenu)
})
```
In this example the app needs to add `myappid.settings.js` to `storage` in `apps.json`.
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",
@ -461,16 +463,13 @@ 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) |
| Widget bottom bar (optional) | (0,216,239,239) |
| Apps | (0,24,239,239) (see below) |
| Apps | (0,24,239,239) |
| BTN1 | (230, 55) |
| BTN2 | (230, 140) |
| BTN3 | (230, 210) |
| BTN4 | (0,0,119, 239)|
| BTN5 | (120,0,239,239) |
- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215)
- Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`.
- For BTN4-5 the touch area is named
@ -515,7 +514,6 @@ The [`testing`](testing) folder contains snippets of code that might be useful f
* `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
* `testing/map` - code for splitting an image into map tiles and then displaying them
## Credits

5465
apps.json

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"id": "1button",
"name": "One-Button-Tracker",
"version": "0.01",
"description": "A widget that turns BTN1 into a tracker, records time of button press/release.",
"icon": "widget.png",
"type": "widget",
"tags": "tool,quantifiedself,widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"1button.wid.js","url":"widget.js"}
],
"data": [{"name":"one_button_presses.csv","storageFile":true}]
}

17
apps/93dub/metadata.json Normal file
View File

@ -0,0 +1,17 @@
{ "id": "93dub",
"name": "93 Dub",
"shortName":"93 Dub",
"icon": "93dub.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.06",
"description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo",
"tags": "clock",
"type": "clock",
"supports":["BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"93dub.app.js","url":"app.js"},
{"name":"93dub.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "BLEcontroller",
"name": "BLE Customisable Controller with Joystick",
"shortName": "BLE Controller",
"version": "0.01",
"description": "A configurable controller for BLE devices and robots, with a basic four direction joystick. Designed to be easy to customise so you can add your own menus.",
"icon": "BLEcontroller.png",
"tags": "tool,bluetooth",
"supports": ["BANGLEJS"],
"readme": "README.md",
"allow_emulator": false,
"storage": [
{"name":"BLEcontroller.app.js","url":"app.js"},
{"name":"BLEcontroller.img","url":"app-icon.js","evaluate":true}
]
}

15
apps/HRV/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "HRV",
"name": "Heart Rate Variability monitor",
"shortName": "HRV monitor",
"version": "0.04",
"description": "Heart Rate Variability monitor, see Readme for more info",
"icon": "hrv.png",
"tags": "",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"HRV.app.js","url":"app.js"},
{"name":"HRV.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "UI4swatch",
"name": "UI 4 swatch",
"shortName": "UI 4 swatch",
"version": "0.01",
"description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.",
"icon": "app.png",
"tags": "Color,input,buttons,touch,UI",
"supports": ["BANGLEJS"],
"readme": "README.md",
"screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}],
"storage": [
{"name":"UI4swatch.app.js","url":"app.js"},
{"name":"UI4swatch.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,4 +1,3 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My app's human readable name",
"shortName":"Short Name",

View File

@ -1,4 +1,3 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My widget's human readable name",
"shortName":"Short Name",

View File

@ -0,0 +1,17 @@
{
"id": "a_clock_timer",
"name": "A Clock with Timer",
"version": "0.01",
"description": "A Clock with Timer, Map and Time Zones",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"a_clock_timer.app.js","url":"app.js"},
{"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id":"a_speech_timer",
"name":"Speech Timer",
"icon": "app.png",
"version":"1.01",
"description": "A timer designed to help keeping your speeches and presentations to time.",
"tags": "tool,timer",
"readme":"README.md",
"supports":["BANGLEJS2"],
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
"allow_emulator": true,
"storage": [
{"name":"a_speech_timer.app.js","url":"app.js"},
{"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true}
]
}

17
apps/about/metadata.json Normal file
View File

@ -0,0 +1,17 @@
{
"id": "about",
"name": "About",
"version": "0.12",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"icon": "app.png",
"tags": "tool,system",
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"bangle1-about-screenshot.png"}],
"allow_emulator": true,
"storage": [
{"name":"about.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]},
{"name":"about.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]},
{"name":"about.img","url":"app-icon.js","evaluate":true}
],
"sortorder": -4
}

890
apps/ac_ac/Customizer.html Normal file
View File

@ -0,0 +1,890 @@
<!DOCTYPE html>
<html lang="en" style="width:100%; height:100%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="minimal-ui, width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="format-detection" content="telephone=no">
<style type="text/css">
html {
text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-o-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
</style>
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' 'unsafe-inline' 'unsafe-eval' file: https:;
">
<meta http-equiv="X-Content-Security-Policy" content="
default-src 'self' 'unsafe-inline' 'unsafe-eval' file: https:;
">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" href="../../css/spectre.min.css">
<script src="../../core/lib/customize.js"></script>
<style>
body { background:white; color:black }
table.centered td {
text-align:center; vertical-align:top;
}
label.Preview {
display:inline-block; position:relative;
}
label.Preview > input[type="radio"] {
display:none; position:relative;
-webkit-appearance:none; -moz-appearance:none; appearance:none;
width:0px; height:0px;
}
label.Preview > input[type="radio"] + img {
display:inline-block; position:relative;
width:88px; height:88px;
margin:0px; padding:0px;
border:solid 3px white;
box-shadow:0px 0px 1px 1px lightgray;
}
label.Preview > input[type="radio"]:checked + img {
border:solid 3px red;
}
.ColorPatch {
display:inline-block; position:relative;
-webkit-appearance:none; -moz-appearance:none; appearance:none;
width:30px; height:30px; box-sizing:border-box;
border:solid 3px white;
margin:0px; padding:0px;
box-shadow:inset 0px 0px 1px 1px black;
vertical-align:top;
}
.ColorPatch:checked {
border:solid 3px red;
}
label.ColorPatch {
display:inline-block; position:relative;
width:70px;
border:none;
box-shadow:none;
}
label.ColorPatch > input[type="radio"] {
display:none; position:relative;
-webkit-appearance:none; -moz-appearance:none; appearance:none;
width:0px; height:0px;
}
label.ColorPatch > input[type="radio"] + span {
display:inline-block; position:absolute;
left:0px; top:0px; right:0px; bottom:0px;
margin:0px; padding:0px;
border:solid 3px white;
box-shadow:inset 0px 0px 1px 1px black;
font-size:16px; line-height:24px;
text-align:center;
}
label.ColorPatch > input[type="radio"]:checked + span {
border:solid 3px red;
}
</style>
<script>
$(function () {
let ClockSize, ClockSizeURL
let ClockFace, ClockFaceURL, ClockFaceNumerals, ClockFaceDots
let ClockHands, ClockHandsURL, SecondHand, FillColor
let ComplicationTL, ComplicationTLURL
let ComplicationT, ComplicationTURL
let ComplicationTR, ComplicationTRURL
let ComplicationL, ComplicationLURL
let ComplicationR, ComplicationRURL
let ComplicationBL, ComplicationBLURL
let ComplicationB, ComplicationBURL
let ComplicationBR, ComplicationBRURL
let Foreground, Background
/**** backupConfiguration ****/
function backupConfiguration () {
let Configuration = {
ClockSize, ClockSizeURL,
ClockFace, ClockFaceURL, ClockFaceNumerals, ClockFaceDots,
ClockHands, ClockHandsURL, SecondHand, FillColor,
ComplicationTL, ComplicationTLURL,
ComplicationT, ComplicationTURL,
ComplicationTR, ComplicationTRURL,
ComplicationL, ComplicationLURL,
ComplicationR, ComplicationRURL,
ComplicationBL, ComplicationBLURL,
ComplicationB, ComplicationBURL,
ComplicationBR, ComplicationBRURL,
Foreground, Background
}
try {
localStorage.setItem('ac_ac',JSON.stringify(Configuration))
} catch (Signal) {
console.error('could not backup clock configuration, reason',Signal)
}
}
/**** restoreConfiguration - warning: no input validations yet! ****/
function restoreConfiguration () {
let Configuration = {}
try {
Configuration = JSON.parse(localStorage.getItem('ac_ac') || '')
} catch (Signal) { /* nop */ }
for (let Key in Configuration) {
if (Configuration.hasOwnProperty(Key)) {
if ((Key == null) || (typeof Configuration[Key] !== 'string')) {
Configuration[Key] = ''
} else {
Configuration[Key] = Configuration[Key].trim()
}
}
}
$('input[name="clock-size"][value="' + Configuration.ClockSize + '"]').attr('checked','checked')
$('#clock-size-custom-url').val(Configuration.ClockSizeURL)
$('input[name="clock-face"][value="' + Configuration.ClockFace + '"]').attr('checked','checked')
$('input[name="clock-face-numerals"][value="' + Configuration.ClockFaceNumerals + '"]').attr('checked','checked')
$('input[name="clock-face-dots"][value="' + Configuration.ClockFaceDots + '"]').attr('checked','checked')
$('#clock-face-custom-url').val(Configuration.ClockFaceURL)
$('input[name="clock-hands"][value="' + Configuration.ClockHands + '"]').attr('checked','checked')
$('input[name="fill-color"][value="' + Configuration.FillColor + '"]').attr('checked','checked')
$('input[name="second-hand"][value="' + Configuration.SecondHand + '"]').attr('checked','checked')
$('#clock-hands-custom-url').val(Configuration.ClockHandsURL)
$('#complication-tl').val(Configuration.ComplicationTL)
$('#complication-tl-custom-url').val(Configuration.ComplicationTLURL)
$('#complication-t').val(Configuration.ComplicationT)
$('#complication-t-custom-url').val(Configuration.ComplicationTURL)
$('#complication-tr').val(Configuration.ComplicationTR)
$('#complication-tr-custom-url').val(Configuration.ComplicationTRURL)
$('#complication-l').val(Configuration.ComplicationL)
$('#complication-l-custom-url').val(Configuration.ComplicationLURL)
$('#complication-r').val(Configuration.ComplicationR)
$('#complication-r-custom-url').val(Configuration.ComplicationRURL)
$('#complication-bl').val(Configuration.ComplicationBL)
$('#complication-bl-custom-url').val(Configuration.ComplicationBLURL)
$('#complication-b').val(Configuration.ComplicationB)
$('#complication-b-custom-url').val(Configuration.ComplicationBURL)
$('#complication-br').val(Configuration.ComplicationBR)
$('#complication-br-custom-url').val(Configuration.ComplicationBRURL)
$('input[name="foreground"][value="' + Configuration.Foreground + '"]').attr('checked','checked')
$('input[name="background"][value="' + Configuration.Background + '"]').attr('checked','checked')
}
restoreConfiguration();
/**** retrieveInputs ****/
function retrieveInputs () {
ClockSize = $('input[name="clock-size"]:checked').val()
ClockSizeURL = ($('#clock-size-custom-url').val() || '').trim()
ClockFace = $('input[name="clock-face"]:checked').val()
ClockFaceNumerals = $('input[name="clock-face-numerals"]:checked').val()
ClockFaceDots = $('input[name="clock-face-dots"]:checked').val()
ClockFaceURL = ($('#clock-face-custom-url').val() || '').trim()
ClockHands = $('input[name="clock-hands"]:checked').val()
FillColor = $('input[name="fill-color"]:checked').val()
SecondHand = $('input[name="second-hand"]:checked').val()
ClockHandsURL = ($('#clock-hands-custom-url').val() || '').trim()
ComplicationTL = $('#complication-tl').val()
ComplicationTLURL = ($('#complication-tl-custom-url').val() || '').trim()
ComplicationT = $('#complication-t').val()
ComplicationTURL = ($('#complication-t-custom-url').val() || '').trim()
ComplicationTR = $('#complication-tr').val()
ComplicationTRURL = ($('#complication-tr-custom-url').val() || '').trim()
ComplicationL = $('#complication-l').val()
ComplicationLURL = ($('#complication-l-custom-url').val() || '').trim()
ComplicationR = $('#complication-r').val()
ComplicationRURL = ($('#complication-r-custom-url').val() || '').trim()
ComplicationBL = $('#complication-bl').val()
ComplicationBLURL = ($('#complication-bl-custom-url').val() || '').trim()
ComplicationB = $('#complication-b').val()
ComplicationBURL = ($('#complication-b-custom-url').val() || '').trim()
ComplicationBR = $('#complication-br').val()
ComplicationBRURL = ($('#complication-br-custom-url').val() || '').trim()
Foreground = $('input[name="foreground"]:checked').val()
Background = $('input[name="background"]:checked').val()
}
retrieveInputs()
/**** validateInputs ****/
function validateInputs () {
function withError (Message) {
showMessage(Message)
$('#UploadButton').attr('disabled',true)
}
switch (true) {
case (ClockSize === 'custom') && (ClockSizeURL === ''):
return withError('please enter the URL of your custom "Clock Size Calculator"')
case (ClockFace === 'custom') && (ClockFaceURL === ''):
return withError('please enter the URL of your custom clock face')
case (ClockHands === 'custom') && (ClockHandsURL === ''):
return withError('please enter the URL of your custom clock hands')
case (ComplicationTL === 'custom') && (ComplicationTLURL === ''):
return withError('please enter the URL of your custom complication in the top-left corner')
case (ComplicationT === 'custom') && (ComplicationTURL === ''):
return withError('please enter the URL of your custom complication at the top edge')
case (ComplicationTR === 'custom') && (ComplicationTRURL === ''):
return withError('please enter the URL of your custom complication in the top-right corner')
case (ComplicationL === 'custom') && (ComplicationLURL === ''):
return withError('please enter the URL of your custom complication at the left edge')
case (ComplicationR === 'custom') && (ComplicationRURL === ''):
return withError('please enter the URL of your custom complication at the right edge')
case (ComplicationBL === 'custom') && (ComplicationBLURL === ''):
return withError('please enter the URL of your custom complication in the bottom-left corner')
case (ComplicationB === 'custom') && (ComplicationBURL === ''):
return withError('please enter the URL of your custom complication at the bottom edge')
case (ComplicationBR === 'custom') && (ComplicationBRURL === ''):
return withError('please enter the URL of your custom complication in the bottom-right corner')
}
hideMessage()
$('#UploadButton').removeAttr('disabled')
}
/**** hide/showMesssage ****/
function hideMessage () { $('#MessageView').hide() }
function showMessage (Message) {
$('#MessageView').text(Message).show()
}
/**** createAndUploadApp ****/
function createAndUploadApp () {
function WidgetsOnBackground () {
return (
ClockSize === 'smart'
? "require('https://raw.githubusercontent.com/rozek/banglejs-2-widgets-on-background/main/drawWidgets.js');"
: ''
)
}
function chosenClockSize () {
switch (ClockSize) {
case 'simple': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-simple-clock-size/main/ClockSize.js')"
case 'smart': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-smart-clock-size/main/ClockSize.js')"
case 'custom': return "require('" + ClockSizeURL + "')"
}
}
function chosenClockFace () {
switch (ClockFace) {
case 'none': return "undefined"
case 'four-numbered': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-four-numbered-clock-face/main/ClockFace.js')"
case 'twelve-numbered': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-twelve-numbered-clock-face/main/ClockFace.js')"
case 'rainbow': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-rainbow-clock-face/main/ClockFace.js')"
case 'custom': return "require('" + ClockFaceURL + "')"
}
}
function chosenClockHands () {
switch (ClockHands) {
case 'simple': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-simpled-clock-hands/main/ClockHands.js')"
case 'rounded': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-rounded-clock-hands/main/ClockHands.js')"
case 'hollow': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-hollow-clock-hands/main/ClockHands.js')"
case 'custom': return "require('" + ClockHandsURL + "')"
}
}
function chosenComplication (Complication, customURL) {
switch (Complication) {
case 'none': return "undefined"
case 'date': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-date-complication/main/Complication.js')"
case 'weekday': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-weekday-complication/main/Complication.js')"
case 'calendar-week': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-calendar-week-complication/main/Complication.js')"
case 'moon-phase': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-moon-phase-complication/main/Complication.js')"
case 'custom': return "require('" + customURL + "')"
}
}
function chosenComplicationAt (Position) {
switch (Position) {
case 'tl': return chosenComplication(ComplicationTL, ComplicationTLURL)
case 't': return chosenComplication(ComplicationT, ComplicationTURL)
case 'tr': return chosenComplication(ComplicationTR, ComplicationTRURL)
case 'l': return chosenComplication(ComplicationL, ComplicationLURL)
case 'r': return chosenComplication(ComplicationR, ComplicationRURL)
case 'bl': return chosenComplication(ComplicationBL, ComplicationBLURL)
case 'b': return chosenComplication(ComplicationB, ComplicationBURL)
case 'br': return chosenComplication(ComplicationBR, ComplicationBRURL)
}
}
function chosenColor (ColorChoice) {
return (ColorChoice === 'none' ? 'undefined' : "'" + ColorChoice + "'")
}
function chosenForeground () { return chosenColor(Foreground) }
function chosenBackground () { return chosenColor(Background) }
function chosenSecondHand () { return chosenColor(SecondHand) }
function chosenFillColor () { return chosenColor(FillColor) }
function chosenNumerals () { return (ClockFaceNumerals === 'roman' ? 'true' : 'false') }
function chosenDots () { return (ClockFaceDots === 'with-dots' ? 'true' : 'false') }
let AppSource = `
${WidgetsOnBackground()}
let Clockwork = require('https://raw.githubusercontent.com/rozek/banglejs-2-simple-clockwork/main/Clockwork.js');
Clockwork.windUp({
size: ${chosenClockSize()},
background:null,
face: ${chosenClockFace()},
hands: ${chosenClockHands()},
complications:{
tl:${chosenComplicationAt('tl')},
t: ${chosenComplicationAt('t')},
tr:${chosenComplicationAt('tr')},
l: ${chosenComplicationAt('l')},
r: ${chosenComplicationAt('r')},
bl:${chosenComplicationAt('bl')},
b: ${chosenComplicationAt('b')},
br:${chosenComplicationAt('br')},
}
},{
Foreground: ${chosenForeground()},
Background: ${chosenBackground()},
Seconds: ${chosenSecondHand()},
withDots: ${chosenDots()},
romanNumerals:${chosenNumerals()},
FillColor: ${chosenFillColor()}
});
`
console.log('the configured AC-AC app looks as follows:')
console.log(AppSource)
backupConfiguration()
sendCustomizedApp({
storage:[
{name:'ac_ac.app.js', url:'app.js', content:AppSource},
]
})
}
/**** register event handlers ****/
function retrieveAndValidateInputs () {
retrieveInputs ()
validateInputs ()
}
$('input[type="radio"]').on('change',retrieveAndValidateInputs)
$('input[type="url"]'). on('input', retrieveAndValidateInputs)
$('select'). on('change',retrieveAndValidateInputs)
$('#UploadButton').on('click',createAndUploadApp)
})
</script>
</head>
<body>
<p>
Please customize your analog clock for the Bangle.js 2 according to your needs.
When finished, click on "Upload" at the bottom of this form.
</p><p>
(Pressing "Upload" will also backup your current configuration so that you
won't have to enter the same settings over and over again when you come back
to this page later)
</p>
<h3>Clock Size Calculation</h3>
<p>
Click on the desired clock size calculator (if you installed some widgets
on your Bangle.js 2, the smart one may produce larger clock faces than the
simple one):
</p><p>
<table class="centered"><tbody>
<tr>
<td>
<label class="Preview">
<input type="radio" name="clock-size" value="simple">
<img src="simpleClockSize.png"/>
</label><br>
simple
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-size" value="smart" checked>
<img src="smartClockSize.png"/>
</label><br>
smart
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-size" value="custom">
<img src="custom.png"/>
</label><br>
(custom)
</td>
</tr>
</tbody></table>
</p><p>
If you prefer a "custom" clock size calculator, please enter the URL
of its JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-size-custom-url" size="50">
</p>
<h3>Clock Face</h3>
<p>
Click on the desired clock face:
</p><p>
<table class="centered"><tbody>
<tr>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="none" checked>
<img src="none.png"/>
</label><br>
(none)
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="four-numbered">
<img src="fournumberedClockFace.png"/>
</label><br>
four-numbered
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="twelve-numbered">
<img src="twelvenumberedClockFace.png"/>
</label><br>
twelve-numbered
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="rainbow">
<img src="RainbowClockFace.png"/>
</label><br>
"rainbow"<br>colored
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="custom">
<img src="custom.png"/>
</label><br>
(custom)
</td>
</tr>
</tbody></table>
</p><p>
If you prefer a "custom" clock face, please enter the URL
of its JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-face-custom-url" size="50">
</p><p>
Clock faces are drawn in the configured foreground and background colors
(you may select them at the end of this form)
</p><p>
"Four-numbered" clock faces may draw indian-arabic or roman numerals. Which do you prefer?
</p><p>
<input type="radio" name="clock-face-numerals" value="indian" checked> indian-arabic (3, 6, 9, 12)<br>
<input type="radio" name="clock-face-numerals" value="roman"> roman (III, VI, IX, XII)
</p><p>
The "twelve-numbered" and "rainbow"-colored faces may be drawn with or without
dots marking the position of every minute. Which variant do you prefer?
</p><p>
<input type="radio" name="clock-face-dots" value="without-dots" checked> without dots <br>
<input type="radio" name="clock-face-dots" value="with-dots"> with dots
</p>
<h3>Clock Hands</h3>
<p>
Click on the desired clock hands:
</p><p>
<table class="centered"><tbody>
<tr>
<td>
<label class="Preview">
<input type="radio" name="clock-hands" value="simple">
<img src="simpleClockHands.png"/>
</label><br>
simple
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-hands" value="rounded" checked>
<img src="roundedClockHands.png"/>
</label><br>
rounded
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-hands" value="hollow">
<img src="hollowClockHands.png"/>
</label><br>
hollow
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-hands" value="custom">
<img src="custom.png"/>
</label><br>
(custom)
</td>
</tr>
</tbody></table>
</p><p>
If you prefer "custom" clock hands, please enter the URL
of their JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-hands-custom-url" size="50">
</p><p>
Clock hands are drawn in the configured foreground and background colors
(you may select them at the end of this form)
</p><p>
Hollow clock hands may optionally be filled with a given color. If you have
chosen hollow hands, please specify the desired fill mode and color below:
</p><p>
<b>Hollow Hand Fill Color:</b>
</p><p>
<label class="ColorPatch">
<input type="radio" name="fill-color" value="none" checked/>
<span>none</span>
</label>
<label class="ColorPatch">
<input type="radio" name="fill-color" value="Theme"/>
<span>themed</span>
</label>
<input type="radio" name="fill-color" value="#000000" class="ColorPatch" style="background:#000000"/>
<input type="radio" name="fill-color" value="#FF0000" class="ColorPatch" style="background:#FF0000"/>
<input type="radio" name="fill-color" value="#00FF00" class="ColorPatch" style="background:#00FF00"/>
<input type="radio" name="fill-color" value="#0000FF" class="ColorPatch" style="background:#0000FF"/>
<input type="radio" name="fill-color" value="#FFFF00" class="ColorPatch" style="background:#FFFF00"/>
<input type="radio" name="fill-color" value="#FF00FF" class="ColorPatch" style="background:#FF00FF"/>
<input type="radio" name="fill-color" value="#00FFFF" class="ColorPatch" style="background:#00FFFF"/>
<input type="radio" name="fill-color" value="#FFFFFF" class="ColorPatch" style="background:#FFFFFF"/>
</p><p>
Additionally, all clock hands may be drawn with or without second hands.
If you want them to be drawn, please click on their desired color below
(or choose "themed" to use your Bangle's configured theme) - if not, just
select "none":
</p><p>
<b>Second Hand Color:</b>
</p><p>
<label class="ColorPatch">
<input type="radio" name="second-hand" value="none" checked/>
<span>none</span>
</label>
<label class="ColorPatch">
<input type="radio" name="second-hand" value="Theme"/>
<span>themed</span>
</label>
<input type="radio" name="second-hand" value="#000000" class="ColorPatch" style="background:#000000"/>
<input type="radio" name="second-hand" value="#FF0000" class="ColorPatch" style="background:#FF0000"/>
<input type="radio" name="second-hand" value="#00FF00" class="ColorPatch" style="background:#00FF00"/>
<input type="radio" name="second-hand" value="#0000FF" class="ColorPatch" style="background:#0000FF"/>
<input type="radio" name="second-hand" value="#FFFF00" class="ColorPatch" style="background:#FFFF00"/>
<input type="radio" name="second-hand" value="#FF00FF" class="ColorPatch" style="background:#FF00FF"/>
<input type="radio" name="second-hand" value="#00FFFF" class="ColorPatch" style="background:#00FFFF"/>
<input type="radio" name="second-hand" value="#FFFFFF" class="ColorPatch" style="background:#FFFFFF"/>
</p>
<h3>Complications</h3>
<p>
Complications are small displays for additional information. If you want
one or multiple complications to be added to your clock, you'll have to
specify which one to be loaded and where it should be placed.
</p><p>
Up to 6 possible positions exist (top-left, top-right, left, right,
bottom-left and bottom-right). Alternatively, the positions "top-left" and
"top-right" may be traded for a slightly larger complication at position
"top" or "bottom-left" and "bottom-right" for one at the "bottom":
</p>
<img src="smallPlaceholders.png" width="88" height="88"/>
<img src="largePlaceholders.png" width="88" height="88"/>
<p>
<table><tbody>
<tr>
<td colspan="3"><b>top-left:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-tl">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-tl-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>top:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-t">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-t-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>top-right:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-tr">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-tr-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>left:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-l">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-l-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>right:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-r">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-r-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>bottom-left:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-bl">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-bl-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>bottom:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-b">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-b-custom-url" size="50"></td>
</tr>
<tr>
<td colspan="3"><b>bottom-right:</b></td>
</tr><tr>
<td> &nbsp; </td>
<td> Complication:</td>
<td>
<select id="complication-br">
<option value="none" selected>(none)</option>
<option value="date"> date</option>
<option value="weekday"> weekday</option>
<option value="calendar-week">calendar week</option>
<option value="moon-phase"> moon phase</option>
<option value="custom"> (custom)</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>custom URL:</td>
<td><input type="url" id="complication-br-custom-url" size="50"></td>
</tr>
</tbody></table>
</p>
<h3>Settings</h3>
<p>
Color faces, hands and complications are often drawn using configurable
foreground and background colors.
</p><p>
Here you may specify these colors. Click on a color to select it - or on
"themed" if you want the clock to use the currently configured theme on
your Bangle.js 2:
</p><p>
<b>Background Color:</b>
</p><p>
<label class="ColorPatch">
<input type="radio" name="background" value="Theme" checked/>
<span>themed</span>
</label>
<input type="radio" name="background" value="#000000" class="ColorPatch" style="background:#000000"/>
<input type="radio" name="background" value="#FF0000" class="ColorPatch" style="background:#FF0000"/>
<input type="radio" name="background" value="#00FF00" class="ColorPatch" style="background:#00FF00"/>
<input type="radio" name="background" value="#0000FF" class="ColorPatch" style="background:#0000FF"/>
<input type="radio" name="background" value="#FFFF00" class="ColorPatch" style="background:#FFFF00"/>
<input type="radio" name="background" value="#FF00FF" class="ColorPatch" style="background:#FF00FF"/>
<input type="radio" name="background" value="#00FFFF" class="ColorPatch" style="background:#00FFFF"/>
<input type="radio" name="background" value="#FFFFFF" class="ColorPatch" style="background:#FFFFFF"/>
</p><p>
<b>Foreground Color:</b>
</p><p>
<label class="ColorPatch">
<input type="radio" name="foreground" value="Theme" checked/>
<span>themed</span>
</label>
<input type="radio" name="foreground" value="#000000" class="ColorPatch" style="background:#000000"/>
<input type="radio" name="foreground" value="#FF0000" class="ColorPatch" style="background:#FF0000"/>
<input type="radio" name="foreground" value="#00FF00" class="ColorPatch" style="background:#00FF00"/>
<input type="radio" name="foreground" value="#0000FF" class="ColorPatch" style="background:#0000FF"/>
<input type="radio" name="foreground" value="#FFFF00" class="ColorPatch" style="background:#FFFF00"/>
<input type="radio" name="foreground" value="#FF00FF" class="ColorPatch" style="background:#FF00FF"/>
<input type="radio" name="foreground" value="#00FFFF" class="ColorPatch" style="background:#00FFFF"/>
<input type="radio" name="foreground" value="#FFFFFF" class="ColorPatch" style="background:#FFFFFF"/>
</p><p>
When you are satisfied with your configuration, just click on "Upload" in
order to generate the specified clock and upload it to your Bangle.js 2:
</p>
<p id="MessageView" style="display:none; color:red"></p>
<button id="UploadButton">Upload</button>
</p><p>
This application is based on the author's
<a href="https://github.com/rozek/banglejs-2-analog-clock-construction-kit">Analog Clock Construction Kit (ACCK)</a>.
If you need a different "clockwork", clock size calculation or clock face,
or specific clock hands or complications, just follow the link to learn how to
implement your own clock parts.
</p>
</body>
</html>

34
apps/ac_ac/README.md Normal file
View File

@ -0,0 +1,34 @@
# AC-AC - A Configurable Analog Clock #
This app implements an analog clock with various faces, hands and complications
to choose from before uploading to a Bangle.js 2.
It is based on the [Analog Clock Construction Kit (ACCK)](https://github.com/rozek/banglejs-2-analog-clock-construction-kit)
and makes most of the currently implemented parts available with a few mouse
clicks - just click on "Upload" and you will be directed to a web form where
you compose your very own, personal analog clock.
You currently have the choice between
* 2 different clock sizes,
* 4 different clock faces,
* 3 different clock hands and
* 4 different complications
Alternatively, you may specify the GitHub URL of ACCK compatible modules for
external clock sizes, faces, hands or complications.
Additionally, you may use the currently configured global theme or configure
your own colors for clock fore- and background and second hands.
Consequently, even without external modules you already have the choice between
102144 combinations!
<!--
1 + (8 Fg colors * 7 Bg colors) * 2 sizes * 4(7) faces * 3(4) hands *
8 positions * 4 complications (w/o placeholder)
-->
## License ##
[MIT License](LICENSE)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

1
apps/ac_ac/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgn/ABH+AQPvBpIAI/n8/3f5/PCp/v9oHF7w1CABffGxAYMH4f9z/514YDCxW/O4gFBxwHD/ZEL7/9GgX8GwQLCBQQXH/uP/Hf/2N44IBAgIXJ7oaD/3v/3uAYIIB9wQGAA2+/iRG5oSIM4f+1nrPYgAB3aHIAC77QYYRoCAAP676ICABXYFIntDoPf3+PC5f+BoPOX4vPNBn7IogEB/eu3QXC9wNEAAeKBIP+dgbSCDYMwgEApQVEygPCeRH8iAWBAAMHPwXDgoRGAonACwYABgN5uMAC4q8GC4U0DQsAggRF9gXFgggB/2hC4kdVAQCBVAX7xwXCVAnGCwUadAeeDYfr7IhEAAf93e+A4gpB9yRB/mqcgndRgQAHzqRE1gEC/KoCjLZEsgCB9evO4gOC/RyEgqdC2KnFO4S/KgFYsC/Ga5EBs1AX5bXHgx1C2YXEnp7GCARgB4AfE64WCnawFCgf9VAK/G/3M7zWDz4PF/maXJIAD7D8EVAP85QXN3OP/42DfoQXN/wvE/ySGABa8FAC37AgepVwQ9E1SfBAAJIEAAnrBQ39xgwJ7pRHFQX+3QECCAbyG9bPDzwXC9QMBdgQXIAAf41wEC5pLCJJBcF9fZQ5IAGYYn81q7RJQwWC/wXM9/tA4veCxooDIAPv55PEABwpB97rDAAw"))

BIN
apps/ac_ac/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

2
apps/ac_ac/app.js Normal file
View File

@ -0,0 +1,2 @@
let Clockwork = require('https://raw.githubusercontent.com/rozek/banglejs-2-simple-clockwork/main/Clockwork.js');
Clockwork.windUp();

BIN
apps/ac_ac/custom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

18
apps/ac_ac/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{ "id": "ac_ac",
"name": "A Configurable Analog Clock",
"shortName":"Configurable Clock",
"version":"0.03",
"description": "AC-AC, a highly customizable analog clock with several clock faces, hands and complications to choose from",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": false,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"custom": "Customizer.html",
"storage": [
{"name":"ac_ac.app.js","url":"app.js"},
{"name":"ac_ac.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/ac_ac/none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA/4AB304ief85L/ABNVAAwKCgILHoALBgoLHqALOrVVr4BEBZIFBBYiaCAAPq2oLQEYlqF5VrBZWnBZWvBZNWz4LGBoQLHJ4O///6v/1BZHa/4LFLYOlr9pR49r1ILJ09qr4ZBBY2vrWdBY5PBq2uyoLIquqBY5bBKoZTFLYILJJ4STDBY77IJ4QLUJ4QLU1QAE0oLPqoAGBZ0BBY9ABYMABY4KCAH4AGA="))

24
apps/accelgraph/app.js Normal file
View File

@ -0,0 +1,24 @@
Bangle.loadWidgets();
g.clear(1);
Bangle.drawWidgets();
var R = Bangle.appRect;
var x = 0;
var last;
function getY(v) {
return (R.y+R.y2 + v*R.h/2)/2;
}
Bangle.on('accel', a => {
g.reset();
if (last) {
g.setColor("#f00").drawLine(x-1,getY(last.x),x,getY(a.x));
g.setColor("#0f0").drawLine(x-1,getY(last.y),x,getY(a.y));
g.setColor("#00f").drawLine(x-1,getY(last.z),x,getY(a.z));
}
last = a;x++;
if (x>=g.getWidth()) {
x = 1;
g.clearRect(R);
}
});

BIN
apps/accelgraph/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

View File

@ -0,0 +1,14 @@
{ "id": "accelgraph",
"name": "Accelerometer Graph",
"shortName":"Accel Graph",
"version":"0.01",
"description": "A simple app to draw a graph of data from the accelerometer on the screen",
"icon": "app.png",
"tags": "tool,debug",
"supports" : ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"accelgraph.app.js","url":"app.js"},
{"name":"accelgraph.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,17 @@
{
"id": "accellog",
"name": "Acceleration Logger",
"shortName": "Accel Log",
"version": "0.03",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png",
"tags": "outdoor",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"accellog.app.js","url":"app.js"},
{"name":"accellog.img","url":"app-icon.js","evaluate":true}
],
"data": [{"wildcard":"accellog.?.csv"}]
}

View File

@ -0,0 +1,17 @@
{
"id": "accelrec",
"name": "Acceleration Recorder",
"shortName": "Accel Rec",
"version": "0.02",
"description": "This app puts the Bangle's accelerometer into 100Hz mode and reads 2 seconds worth of data after movement starts. The data can then be exported back to the PC.",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"accelrec.app.js","url":"app.js"},
{"name":"accelrec.img","url":"app-icon.js","evaluate":true}
],
"data": [{"wildcard":"accelrec.?.csv"}]
}

16
apps/aclock/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "aclock",
"name": "Analog Clock",
"version": "0.15",
"description": "An Analog Clock",
"icon": "clock-analog.png",
"screenshots": [{"url":"screenshot_analog.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"aclock.app.js","url":"clock-analog.js"},
{"name":"aclock.img","url":"clock-analog-icon.js","evaluate":true}
]
}

3
apps/acmaze/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Faster maze generation
0.03: Avoid clearing bottom widgets

17
apps/acmaze/README.md Normal file
View File

@ -0,0 +1,17 @@
# AccelaMaze
Tilt the watch to roll a ball through a maze.
![Screenshot](screenshot.png)
## Usage
* Use the menu to select difficulty level (or exit).
* Wait until the maze gets generated and a red ball appears.
* Tilt the watch to get the ball into the green cell.
At any time you can click the button to return to the menu.
## Creator
[Nimrod Kerrett](https://zzzen.com)

1
apps/acmaze/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwggaXh3M53/AA3yl4IHn//+EM5nMAoIX/C4RfCC4szmcxC4QFBAAUxC4UPAwIOB+YCCiMRkAFCkIGBAAQfBC4IUEAQhHIAAQX/C5EDmcyCgUTAoYXDR4kzC4UBPoKVB+YFFAQSPBiAKBiCnDGoZECABDUCa4YX/C5qPBQwoXGkczmC/FQYSSCVQSSCEwQOCC4hKFX4QXCd5YX/C4qMEmQXITAinDPoIADTwSPFkKMBX47RGI47XIC/4XCgZ9DQYYABmKYBmIXFkczmEBRIK/CQYQIBkECSoiSCA4MQa5pEFd6IX/RgMyC6H/QASVCRIS/EAQrXFJQoX/C6kDRQIXCiYFD+QFBmIUCkYFD+CJBiSPCRwIFFSoQFCiF3u9wI4gAO+wXW+IXygAAW"))

287
apps/acmaze/app.js Normal file
View File

@ -0,0 +1,287 @@
const MARGIN = 25;
const WALL_RIGHT = 1, WALL_DOWN = 2;
const STATUS_GENERATING = 0, STATUS_PLAYING = 1,
STATUS_SOLVED = 2, STATUS_ABORTED = -1;
function Maze(n) {
this.n = n;
this.status = STATUS_GENERATING;
this.wall_length = Math.floor((g.getHeight()-2*MARGIN)/n);
this.total_length = this.wall_length*n;
this.margin = Math.floor((g.getHeight()-this.total_length)/2);
this.ball_x = 0;
this.ball_y = 0;
// This voodoo is needed because otherwise
// bottom line widgets (like digital clock)
// disappear during maze generation
Bangle.drawWidgets();
g.setColor(g.theme.fg);
for (let i=0; i<=n; i++) {
g.drawRect(
this.margin, this.margin+i*this.wall_length,
g.getWidth()-this.margin, this.margin+i*this.wall_length
);
g.drawRect(
this.margin+i*this.wall_length, this.margin,
this.margin+i*this.wall_length, g.getHeight() - this.margin
);
}
this.walls = new Uint8Array(n*n);
this.groups = new Uint8Array(n*n);
for (let cell = 0; cell<n*n; cell++) {
this.walls[cell] = WALL_RIGHT|WALL_DOWN;
this.groups[cell] = cell;
}
// Candidates of walls to break when digging the maze.
// If candidate failed (breaking it would create a loop),
// it would never succeed, so no need to retry it.
let candidates_down = [],
candidates_right = [];
for (let r=0 ; r<n; r++) {
for (let c=0; c<n; c++) {
let cell = n*r+c;
if (r<(n-1)) { // Don't break wall down for bottom row.
candidates_down.push(cell);
}
if (c<(n-1)) { // Don't break wall right for rightmost column.
candidates_right.push(cell);
}
}
}
let from_group, to_group;
let ngroups = n*n;
while (--ngroups) {
// Abort if BTN1 pressed [grace period for menu]
// (for some reason setWatch() fails inside constructor)
if (ngroups<n*n-16 && digitalRead(BTN1)) {
aborting = true;
return;
}
from_group = to_group = -1;
while (from_group<0) {
let trying_down = false;
if (Math.random()<0.5 && candidates_down.length || !candidates_right.length) {
trying_down = true;
}
let candidates = trying_down ? candidates_down : candidates_right,
candidate_index = Math.floor(Math.random()*candidates.length),
cell = candidates.splice(candidate_index, 1)[0],
r = Math.floor(cell/n),
c = cell%n;
if (trying_down) { // try to break a wall down
if (this.groups[cell]!=this.groups[cell+n]) {
this.walls[cell] &= ~WALL_DOWN;
g.clearRect(
this.margin+c*this.wall_length+1,
this.margin+(r+1)*this.wall_length,
this.margin+(c+1)*this.wall_length-1,
this.margin+(r+1)*this.wall_length
);
g.flip(); // show progress.
from_group = this.groups[cell];
to_group = this.groups[cell+n];
}
} else { // try to break a wall right
if (this.groups[cell]!=this.groups[cell+1]) {
this.walls[cell] &= ~WALL_RIGHT;
g.clearRect(
this.margin+(c+1)*this.wall_length,
this.margin+r*this.wall_length+1,
this.margin+(c+1)*this.wall_length,
this.margin+(r+1)*this.wall_length-1
);
g.flip(); // show progress.
from_group = this.groups[cell];
to_group = this.groups[cell+1];
}
}
}
for (let cell = 0; cell<n*n; cell++) {
if (this.groups[cell]==from_group) {
this.groups[cell] = to_group;
}
}
}
this.clearCell = function(r, c) {
if (!r && !c) {
g.setColor("#ffff00");
} else if (r==this.n-1 && c==this.n-1) {
g.setColor("#00ff00");
} else {
g.setColor(g.theme.bg);
}
g.fillRect(
this.margin+this.wall_length*c+1,
this.margin+this.wall_length*r+1,
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*(r+1)
);
g.setColor(g.theme.fg);
if (this.walls[r*n+c]&WALL_RIGHT) {
g.fillRect(
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*r,
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*(r+1)
);
}
if (this.walls[r*n+c]&WALL_DOWN) {
g.fillRect(
this.margin+this.wall_length*c,
this.margin+this.wall_length*(r+1),
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*(r+1)
);
}
};
this.drawBall = function(x, y) {
g.setColor("#ff0000");
g.fillEllipse(
this.margin+x+1,
this.margin+y+1,
this.margin+x+this.wall_length-1,
this.margin+y+this.wall_length-1
);
g.setColor(g.theme.fg);
};
this.move = function(dx, dy) {
let next_x = this.ball_x,
next_y = this.ball_y,
ball_r = Math.floor(this.ball_y/this.wall_length),
ball_c = Math.floor(this.ball_x/this.wall_length);
if (this.ball_x%this.wall_length) {
if (dx) {
next_x += dx;
} else {
return false;
}
} else if (this.ball_y%this.wall_length) {
if (dy) {
next_y += dy;
} else {
return false;
}
} else { // exactly in a cell. Check walls
if (dy<0 && ball_r>0 && !(this.walls[n*(ball_r-1)+ball_c]&WALL_DOWN)) {
next_y--;
} else if (dy>0 && ball_r<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_DOWN)) {
next_y++;
} else if (dx<0 && ball_c>0 && !(this.walls[n*ball_r+ball_c-1]&WALL_RIGHT)) {
next_x--;
} else if (dx>0 && ball_c<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_RIGHT)) {
next_x++;
} else {
return false;
}
}
this.clearCell(ball_r, ball_c);
if (this.ball_x%this.wall_length) {
this.clearCell(ball_r, ball_c+1);
}
if (this.ball_y%this.wall_length) {
this.clearCell(ball_r+1, ball_c);
}
this.ball_x = next_x;
this.ball_y = next_y;
this.drawBall(this.ball_x, this.ball_y);
if (this.ball_x==(n-1)*this.wall_length && this.ball_y==(n-1)*this.wall_length) {
this.status = STATUS_SOLVED;
}
return true;
};
this.try_move_horizontally = function(accel_x) {
if (accel_x>0.15) {
return this.move(-1, 0);
} else if (accel_x<-0.15) {
return this.move(1, 0);
}
return false;
};
this.try_move_vertically = function(accel_y) {
if (accel_y<-0.15) {
return this.move(0,1);
} else if (accel_y>0.15) {
return this.move(0,-1);
}
return false;
};
this.tick = function() {
accel = Bangle.getAccel();
if (this.ball_x%this.wall_length) {
this.try_move_horizontally(accel.x);
} else if (this.ball_y%this.wall_length) {
this.try_move_vertically(accel.y);
} else {
if (Math.abs(accel.x)>Math.abs(accel.y)) { // prefer horizontally
if (!this.try_move_horizontally(accel.x)) {
this.try_move_vertically(accel.y);
}
} else { // prefer vertically
if (!this.try_move_vertically(accel.y)) {
this.try_move_horizontally(accel.x);
}
}
}
};
this.clearCell(0,0);
this.clearCell(n-1,n-1);
this.drawBall(0,0);
this.status = STATUS_PLAYING;
}
function timeToText(t) { // Courtesy of stopwatch app
let hrs = Math.floor(t/3600000);
let mins = Math.floor(t/60000)%60;
let secs = Math.floor(t/1000)%60;
let tnth = Math.floor(t/100)%10;
let text;
if (hrs === 0)
text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
else
text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
return text;
}
let aborting = false;
let start_time = 0;
let duration = 0;
let maze=null;
let mazeMenu = {
"": { "title": "Maze size", "selected": 1 },
"Easy (8x8)": function() { E.showMenu(); maze = new Maze(8); },
"Medium (10x10)": function() { E.showMenu(); maze = new Maze(10); },
"Hard (14x14)": function() { E.showMenu(); maze = new Maze(14); },
"< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock
};
g.reset();
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setLocked(false);
Bangle.setLCDTimeout(0);
E.showMenu(mazeMenu);
let maze_interval = setInterval(
function() {
if (maze) {
if (digitalRead(BTN1) || maze.status==STATUS_ABORTED) {
maze = null;
start_time = duration = 0;
aborting = false;
setTimeout(function() {E.showMenu(mazeMenu); }, 100);
return;
}
if (!start_time) {
start_time = Date.now();
}
if (maze.status==STATUS_PLAYING) {
maze.tick();
}
if (maze.status==STATUS_SOLVED && !duration) {
duration = Date.now()-start_time;
g.setFontAlign(0,0).setColor(g.theme.fg);
g.setFont("Vector",18);
g.drawString(`Solved ${maze.n}X${maze.n} in\n ${timeToText(duration)} \nBtn1 to play again`, g.getWidth()/2, g.getHeight()/2, true);
}
}
}, 25);

BIN
apps/acmaze/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

15
apps/acmaze/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "acmaze",
"name": "AccelaMaze",
"shortName":"AccelaMaze",
"version":"0.03",
"description": "Tilt the watch to roll a ball through a maze.",
"icon": "app.png",
"tags": "game",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"acmaze.app.js","url":"app.js"},
{"name":"acmaze.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/acmaze/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,18 @@
{
"id": "activepedom",
"name": "Active Pedometer",
"shortName": "Active Pedometer",
"version": "0.09",
"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.",
"icon": "app.png",
"tags": "outdoors,widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"screenshots": [{"url":"600.png"},{"url":"10600.png"},{"url":"1600.png"}],
"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"}
]
}

18
apps/alarm/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"id": "alarm",
"name": "Default Alarm & Timer",
"shortName": "Alarms",
"version": "0.14",
"description": "Set and respond to alarms and timers",
"icon": "app.png",
"tags": "tool,alarm,widget",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"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"}]
}

View File

@ -0,0 +1,14 @@
{
"id": "alpinenav",
"name": "Alpine Nav",
"version": "0.01",
"description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime",
"icon": "app-icon.png",
"tags": "outdoors,gps",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"alpinenav.app.js","url":"app.js"},
{"name":"alpinenav.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "analogimgclk",
"name": "Analog Clock (Image background)",
"shortName": "Analog Clock",
"version": "0.03",
"description": "An analog clock with an image background",
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"storage": [
{"name":"analogimgclk.app.js","url":"app.js"},
{"name":"analogimgclk.bg.img","url":"bg.img"},
{"name":"analogimgclk.img","url":"app-icon.js","evaluate":true}
]
}

15
apps/andark/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "andark",
"name": "Analog Dark",
"shortName":"AnDark",
"version":"0.04",
"description": "analog clock face without disturbing widgets",
"icon": "andark_icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"andark.app.js","url":"app.js"},
{"name":"andark.img","url":"app_icon.js","evaluate":true}
]
}

View File

@ -4,3 +4,4 @@
0.03: Handling of message actions (ok/clear)
0.04: Android icon now goes to settings page with 'find phone'
0.05: Fix handling of message actions
0.06: Option to keep messages after a disconnect (default false) (fix #1186)

48
apps/android/README.md Normal file
View File

@ -0,0 +1,48 @@
# Android Integration
This app allows your Bangle.js to receive notifications [from the Gadgetbridge app on Android](http://www.espruino.com/Gadgetbridge)
See [this link](http://www.espruino.com/Gadgetbridge) for notes on how to install
the Android app (and how it works).
It requires the `Messages` app on Bangle.js (which should be automatically installed) to
display any notifications that are received.
## Settings
You can access the settings menu either from the `Android` icon in the launcher,
or from `App Settings` in the `Settings` menu.
It contains:
* `Connected` - shows whether there is an active Bluetooth connection or not
* `Find Phone` - opens a submenu where you can activate the `Find Phone` functionality
of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them?
* `Messages` - launches the messages app, showing a list of messages
## How it works
Gadgetbridge on Android connects to Bangle.js, and sends commands over the
BLE UART connection. These take the form of `GB({ ... JSON ... })\n` - so they
call a global function called `GB` which then interprets the JSON.
Responses are sent back to Gadgetbridge simply as one line of JSON.
More info on message formats on http://www.espruino.com/Gadgetbridge
## Testing
Bangle.js can only hold one connection open at a time, so it's hard to see
if there are any errors when handling Gadgetbridge messages.
However you can:
* Use the `Gadgetbridge Debug` app on Bangle.js to display/log the messages received from Gadgetbridge
* Connect with the Web IDE and manually enter the Gadgetbridge messages on the left-hand side to
execute them as if they came from Gadgetbridge, for instance:
```
GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"})
```

View File

@ -4,6 +4,7 @@
Bluetooth.println(JSON.stringify(message));
}
var settings = require("Storage").readJSON("android.settings.json",1)||{};
var _GB = global.GB;
global.GB = (event) => {
// feed a copy to other handlers if there were any
@ -51,7 +52,8 @@
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); }
NRF.on("connect", () => setTimeout(sendBattery, 2000));
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
setInterval(sendBattery, 10*60*1000);
// Health tracking
Bangle.on('health', health=>{
@ -68,4 +70,6 @@
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here?
};
// remove settings object so it's not taking up RAM
delete settings;
})();

View File

@ -0,0 +1,20 @@
{
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.06",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",
"dependencies": {"messages":"app"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"android.app.js","url":"app.js"},
{"name":"android.settings.js","url":"settings.js"},
{"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"}
],
"data": [{"name":"android.settings.json"}],
"sortorder": -8
}

View File

@ -2,17 +2,29 @@
function gb(j) {
Bluetooth.println(JSON.stringify(j));
}
var settings = require("Storage").readJSON("android.settings.json",1)||{};
function updateSettings() {
require("Storage").writeJSON("android.settings.json", settings);
}
var mainmenu = {
"" : { "title" : "Android" },
"< Back" : back,
"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
"Find Phone" : () => E.showMenu({
"" : { "title" : "Find Phone" },
"< Back" : ()=>E.showMenu(mainmenu),
"On" : _=>gb({t:"findPhone",n:true}),
"Off" : _=>gb({t:"findPhone",n:false}),
/*LANG*/"On" : _=>gb({t:"findPhone",n:true}),
/*LANG*/"Off" : _=>gb({t:"findPhone",n:false}),
}),
"Messages" : ()=>load("messages.app.js")
/*LANG*/"Keep Msgs" : {
value : !!settings.keep,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.keep = v;
updateSettings();
}
},
/*LANG*/"Messages" : ()=>load("messages.app.js")
};
E.showMenu(mainmenu);
})

View File

@ -0,0 +1,21 @@
{
"id": "animals",
"name": "Animals Game",
"version": "0.01",
"description": "Simple toddler's game - displays a different number of animals each time the screen is pressed",
"icon": "animals.png",
"tags": "game",
"supports": ["BANGLEJS"],
"storage": [
{"name":"animals.app.js","url":"animals.js"},
{"name":"animals.img","url":"animals-icon.js","evaluate":true},
{"name":"animals-snake.img","url":"animals-snake.js","evaluate":true},
{"name":"animals-duck.img","url":"animals-duck.js","evaluate":true},
{"name":"animals-swan.img","url":"animals-swan.js","evaluate":true},
{"name":"animals-fox.img","url":"animals-fox.js","evaluate":true},
{"name":"animals-camel.img","url":"animals-camel.js","evaluate":true},
{"name":"animals-pig.img","url":"animals-pig.js","evaluate":true},
{"name":"animals-sheep.img","url":"animals-sheep.js","evaluate":true},
{"name":"animals-mouse.img","url":"animals-mouse.js","evaluate":true}
]
}

View File

@ -0,0 +1,18 @@
{
"id": "animclk",
"name": "Animated Clock",
"shortName": "Anim Clock",
"version": "0.03",
"description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art",
"icon": "app.png",
"type": "clock",
"tags": "clock,animated",
"supports": ["BANGLEJS"],
"storage": [
{"name":"animclk.app.js","url":"app.js"},
{"name":"animclk.pixels1","url":"animclk.pixels1"},
{"name":"animclk.pixels2","url":"animclk.pixels2"},
{"name":"animclk.pal","url":"animclk.pal"},
{"name":"animclk.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,3 +1,10 @@
0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide
0.03: Clock now shows day of week under date.
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
when weekday name "Off": week #:<num>
when weekday name "On": weekday name is cut at 6th position and .#<week num> is added
0.06: fixes #1271 - wrong settings name
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
week is buffered until date or timezone changes

79
apps/antonclk/README.md Normal file
View File

@ -0,0 +1,79 @@
# Anton Clock - Large font digital watch with seconds and date
Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
## Features
The basic time representation only shows hours and minutes of the current time. However, Anton clock can show additional information:
* Seconds can be shown, either always or only if the screen is unlocked.
* To help easy recognition, the seconds can be coloured in blue on the Bangle.js 2.
* Date can be shown in three different formats:
* ISO-8601: 2021-12-19
* short local format: 19/12/2021, 19.12.2021
* long local format: DEC 19 2021
* Weekday can be shown (on seconds screen only instead of year)
## Usage
Install Anton clock through the Bangle.js app loader.
Configure it through the default Bangle.js configuration mechanism
(Settings app, "Apps" menu, "Anton clock" submenu).
If you like it, make it your default watch face
(Settings app, "System" menu, "Clock" submenu, select "Anton clock").
## Configuration
Anton clock is configured by the standard settings mechanism of Bangle.js's operating system:
Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu.
You configure Anton clock through several "on/off" switches in two menus.
### The main menu
The main menu contains several settings covering Anton clock in general.
* **Seconds...** - Opens the submenu for configuring the presentation of the current time's seconds.
* **Date** - Format of the date representation. Possible values are
* **Long** - "Long" date format in the current locale. Usually with the month as name, not number.
* **Short** - "Short" date format in the current locale. Usually with the month as number.
* **ISO8601** - Show the date in ISO-8601 format (YYYY-MM-DD), irrespective of the current locale.
* **Show Weekday** - Weekday is shown in the time presentation without seconds.
Weekday name depends on the current locale.
If seconds are shown, the weekday is never shown as there is not enough space on the watch face.
* **Show CalWeek** - Week-number (ISO-8601) is shown. (default: Off)
If "Show Weekday" is "Off" displays the week-number as "week #<num>".
If "Show Weekday" is "On" displays "weekday name short" with " #<num>" .
If seconds are shown, the week number is never shown as there is not enough space on the watch face.
* **Vector font** - Use the built-in vector font for dates and weekday.
This can improve readability.
Otherwise, a scaled version of the built-in 6x8 pixels font is used.
### The "Seconds" submenu
The "Seconds" submenu configures how (and if) seconds are shown on the "Anton" watch face.
* **Show** - Configure when the seconds should be shown at all:
* **Never** - Seconds are never shown.
In this case, hour and minute are a bit more centered on the screen and the clock will always only update every minute.
This saves battery power.
* **Unlocked** - Seconds are shown if the display is unlocked.
On locked displays, only hour, minutes, date and optionally the weekday are shown.
_This option is highly recommended on the Bangle.js 2!_
* **Always** - Seconds are _always_ shown, irrespective of the display's unlock state.
_Enabling this option increases power consumption as the watch face will update once per second instead of once per minute._
* **With ":"** - If enabled, a colon ":" is prepended to the seconds.
This resembles the usual time representation "hh:mm:ss", even though the seconds are printed on a separate line.
* **Color** - If enabled, seconds are shown in blue instead of black.
If the date is shown on the seconds screen, it is colored read instead of black.
This make the visual orientation much easier on the watch face.
* **Date** - It is possible to show the date together with the seconds:
* **No** - Date is _not_ shown in the seconds screen.
In this case, the seconds are centered below hour and minute.
* **Year** - Date is shown with day, month, and year. If "Date" in the main settings is configured to _ISO8601_, this is used here, too. Otherwise, the short local format is used.
* **Weekday** - Date is shown with day, month, and weekday.
The date is coloured in red if the "Coloured" option is chosen.
## Compatibility
Anton clock makes use of core Bangle.js 2 features (coloured display, display lock state). It also runs on the Bangle.js 1 but these features are not available there due to hardware restrictions.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,19 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.06",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"antonclk.json"}]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 1.6 KiB

107
apps/antonclk/settings.js Normal file
View File

@ -0,0 +1,107 @@
// Settings menu for the enhanced Anton clock
(function(back) {
var FILE = "antonclk.json";
// Load settings
var settings = Object.assign({
secondsOnUnlock: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values
function stringItems(startvalue, writer, values) {
return {
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => values[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
onchange: v => {
writer(values[v]);
writeSettings();
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values) {
return stringItems(settings[name], v => settings[name] = v, values);
}
var mainmenu = {
"": {
"title": "Anton clock"
},
"< Back": () => back(),
"Seconds...": () => E.showMenu(secmenu),
"Date": stringInSettings("dateOnMain", ["Short", "Long", "ISO8601"]),
"Show Weekday": {
value: (settings.weekDay !== undefined ? settings.weekDay : true),
format: v => v ? "On" : "Off",
onchange: v => {
settings.weekDay = v;
writeSettings();
}
},
"Show CalWeek": {
value: (settings.calWeek !== undefined ? settings.calWeek : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.calWeek = v;
writeSettings();
}
},
"Uppercase": {
value: (settings.upperCase !== undefined ? settings.upperCase : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.upperCase = v;
writeSettings();
}
},
"Vector font": {
value: (settings.vectorFont !== undefined ? settings.vectorFont : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.vectorFont = v;
writeSettings();
}
},
};
// Submenu
var secmenu = {
"": {
"title": "Show seconds..."
},
"< Back": () => E.showMenu(mainmenu),
"Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]),
"With \":\"": {
value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.secondsWithColon = v;
writeSettings();
}
},
"Color": {
value: (settings.secondsColoured !== undefined ? settings.secondsColoured : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.secondsColoured = v;
writeSettings();
}
},
"Date": stringInSettings("dateOnSecs", ["No", "Year", "Weekday"])
};
// Actually display the menu
E.showMenu(mainmenu);
});
// end of file

15
apps/arrow/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "arrow",
"name": "Arrow Compass",
"version": "0.05",
"description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass",
"icon": "arrow.png",
"type": "app",
"tags": "tool,outdoors",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"arrow.app.js","url":"app.js"},
{"name":"arrow.img","url":"icon.js","evaluate":true}
]
}

View File

@ -1 +1,3 @@
0.01: New App!
0.02: Update to work with Bangle.js 2
0.03: Select GNSS systems to use for Bangle.js 2

View File

@ -8,34 +8,72 @@
<p>GPS can take a long time (~5 minutes) to get an accurate position the first time it is used.
AGPS uploads a few hints to the GPS receiver about satellite positions that allow it
to get a faster, more accurate fix - however they are only valid for a short period of time.</p>
<p>You can upload data that covers a longer period of time, but the upload will take longer.</p>
<div class="form-group">
<label class="form-label">AGPS Validity time</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1d"><i class="form-icon"></i> 1 day (8kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="2d" checked><i class="form-icon"></i> 2 days (14kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="3d"><i class="form-icon"></i> 3 days (20kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1wk"><i class="form-icon"></i> 1 week (46kB)
</label>
<div id="banglejs1-info" style="display:none">
<p>You can upload data that covers a longer period of time, but the upload will take longer.</p>
<div class="form-group">
<label class="form-label">AGPS Validity time</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1d"><i class="form-icon"></i> 1 day (8kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="2d" checked><i class="form-icon"></i> 2 days (14kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="3d"><i class="form-icon"></i> 3 days (20kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1wk"><i class="form-icon"></i> 1 week (46kB)
</label>
</div>
</div>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<div id="banglejs2-info" style="display:none">
<p>Using fewer GNSS systems may decrease the time to fix. (If unsure, select only GPS)</p>
<div class="form-group">
<label class="form-label">Select which GNSS system you want.</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="1" checked><i class="form-icon"></i> GPS
</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="2"><i class="form-icon"></i> BDS
</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="3"><i class="form-icon"></i> GPS+BDS
</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="4"><i class="form-icon"></i> GLONASS
</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="5"><i class="form-icon"></i> GPS+GLONASS
</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> BDS+GLONASS
</label>
<label class="form-radio">
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> GPS+BDS+GLONASS
</label>
</div>
</div>
<p id="upload-wrap" style="display:none">Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../core/lib/customize.js"></script>
<script>
var isB1; // is Bangle.js 1?
var isB2; // is Bangle.js 2?
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
var radios = document.getElementsByName('agpsperiod');
var url = "https://www.espruino.com/agps/assistnow_1d.base64";
for (var i=0; i<radios.length; i++)
if (radios[i].checked)
url = "https://www.espruino.com/agps/assistnow_"+radios[i].value+".base64";
var url;
if (isB1) {
var radios = document.getElementsByName('agpsperiod');
url = "https://www.espruino.com/agps/assistnow_1d.base64";
for (var i=0; i<radios.length; i++)
if (radios[i].checked)
url = "https://www.espruino.com/agps/assistnow_"+radios[i].value+".base64";
}
if (isB2) {
url = "https://www.espruino.com/agps/casic.base64";
}
console.log("Sending...");
//var text = document.getElementById("agpsperiod").value;
get(url, function(b64) {
@ -48,6 +86,8 @@
});
});
// =================================================== Bangle.js 1 UBLOX
function UBX_CMD(cmd) {
var d = [0xB5,0x62]; // sync chars
d = d.concat(cmd);
@ -79,13 +119,40 @@
return UBX_CMD([].slice.call(a));
}
// =================================================== Bangle.js 2 CASIC
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i=1;i<cmd.length;i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
}
// ===================================================
function jsFromBase64(b64) {
var bin = atob(b64);
var chunkSize = 128;
var js = "\x10Bangle.setGPSPower(1);\n"; // turn GPS on
//js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`;
//js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
if (isB1) { // UBLOX
//js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`;
//js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
}
if (isB2) { // CASIC
// Select what GNSS System to use for decreased fix time.
var radios = document.getElementsByName('gnss_select');
var gnss_select="1";
for (var i=0; i<radios.length; i++)
if (radios[i].checked)
gnss_select=radios[i].value;
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
}
for (var i=0;i<bin.length;i+=chunkSize) {
var chunk = bin.substr(i,chunkSize);
@ -106,6 +173,15 @@
oReq.send();
}
// Called when we know what device we're using
function onInit(device) {
isB2 = (device && device.id=="BANGLEJS2");
isB1 = !isB2;
document.getElementById("banglejs1-info").style = isB1?"":"display:none";
document.getElementById("banglejs2-info").style = isB2?"":"display:none";
document.getElementById("upload-wrap").style = "";
}
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
{
"id": "assistedgps",
"name": "Assisted GPS Update (AGPS)",
"version": "0.03",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"icon": "app.png",
"type": "RAM",
"tags": "tool,outdoors,agps",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": []
}

15
apps/astral/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "astral",
"name": "Astral Clock",
"version": "0.03",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"astral.app.js","url":"app.js"},
{"name":"astral.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,23 @@
{
"id": "astrocalc",
"name": "Astrocalc",
"version": "0.02",
"description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
"icon": "astrocalc.png",
"tags": "app,sun,moon,cycles,tool,outdoors",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"storage": [
{"name":"astrocalc.app.js","url":"astrocalc-app.js"},
{"name":"suncalc.js","url":"suncalc.js"},
{"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true},
{"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true},
{"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true},
{"name":"waning-crescent.img","url":"waning-crescent-icon.js","evaluate":true},
{"name":"waning-gibbous.img","url":"waning-gibbous-icon.js","evaluate":true},
{"name":"full.img","url":"full-icon.js","evaluate":true},
{"name":"new.img","url":"new-icon.js","evaluate":true},
{"name":"waxing-gibbous.img","url":"waxing-gibbous-icon.js","evaluate":true},
{"name":"waxing-crescent.img","url":"waxing-crescent-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,15 @@
{
"id": "astroid",
"name": "Asteroids!",
"version": "0.03",
"description": "Retro asteroids game",
"icon": "asteroids.png",
"screenshots": [{"url":"screenshot_asteroids.png"}],
"tags": "game",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"astroid.app.js","url":"asteroids.js"},
{"name":"astroid.img","url":"asteroids-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,19 @@
{
"id": "authentiwatch",
"name": "2FA Authenticator",
"shortName": "AuthWatch",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"version": "0.04",
"description": "Google Authenticator compatible tool.",
"tags": "tool",
"interface": "interface.html",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"authentiwatch.app.js","url":"app.js"},
{"name":"authentiwatch.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"authentiwatch.json"}]
}

View File

@ -0,0 +1,17 @@
{
"id":"awairmonitor",
"name":"Awair Monitor",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"allow_emulator": true,
"version":"0.03",
"description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.",
"type": "clock",
"tags": "clock,tool,health",
"readme":"README.md",
"supports":["BANGLEJS2"],
"storage": [
{"name":"awairmonitor.app.js","url":"app.js"},
{"name":"awairmonitor.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,4 +1,5 @@
(() => {
(() => {
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDTimeout(0);
let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {};
@ -6,7 +7,9 @@
// 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;
const sW = g.getWidth();
const sH = g.getHeight()*2/3;
const bgColour ="#f00"; // only for Bangle.js 2
// gravity constant (lowercase was already taken)
const G = 9.80665;
@ -17,14 +20,16 @@
// 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",
};
// even size 1 actually works, but larger mazes take forever to generate
if (!BANGLEJS2) {
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), minSize = 4, defaultSize = 10;
} else {
const sizes = [1, 2, 4, 5, 8, 10, 16, 20 ].reverse(), minSize = 4, defaultSize = 10;
}
/**
* Draw something to all screen buffers
* @param draw {function} Callback which performs the drawing
@ -45,17 +50,17 @@
// use unbuffered graphics for UI stuff
function showMessage(message, title) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMessage(message, title);
}
function showPrompt(prompt, options) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showPrompt(prompt, options);
}
function showMenu(menu) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMenu(menu);
}
@ -105,7 +110,7 @@
generateMaze(); // this shows unbuffered progress messages
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
Bangle.setLCDMode("doublebuffered");
if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
clearAll();
drawAll(drawMaze);
intervalID = setInterval(tick, 100);
@ -307,6 +312,7 @@
const range = {top: 0, left: 0, bottom: rows, right: cols};
const w = sW/cols, h = sH/rows;
g.clear();
if (BANGLEJS2) g.setBgColor(bgColour);
g.setColor(0.76, 0.60, 0.42);
for(let row = range.top; row<=range.bottom; row++) {
for(let col = range.left; col<=range.right; col++) {

View File

@ -0,0 +1,16 @@
{
"id": "ballmaze",
"name": "Ball Maze",
"version": "0.02",
"description": "Navigate a ball through a maze by tilting your watch.",
"icon": "icon.png",
"type": "app",
"tags": "game",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"ballmaze.app.js","url":"app.js"},
{"name":"ballmaze.img","url":"icon.js","evaluate":true}
],
"data": [{"name":"ballmaze.json"}]
}

View File

@ -1,2 +1,3 @@
0.01: Initial version of Balltastic released! Happy!
0.02: Set LCD timeout for Espruino 2v10 compatibility
0.02: Set LCD timeout for Espruino 2v10 compatibility
0.03: Now also works on Bangle.js 2

View File

@ -1,11 +1,12 @@
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDBrightness(1);
Bangle.setLCDMode("doublebuffered");
if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
Bangle.setLCDTimeout(0);
let points = 0;
let level = 1;
let levelSpeedStart = 0.8;
let nextLevelPoints = 20;
let nextLevelPoints = 10;
let levelSpeedFactor = 0.2;
let counterWidth = 10;
let gWidth = g.getWidth() - counterWidth;
@ -81,12 +82,23 @@ function drawLevelText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
g.setFont("4x6", 5);
g.drawString("Level " + level, 120, 80);
g.drawString("Level " + level, g.getWidth()/2, g.getHeight()/2);
}
function drawPointsText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
g.setFont("4x6", 2);
g.drawString("Points " + points, g.getWidth()/2, g.getHeight()-20);
}
function draw() {
//bg
g.setColor("#71c6cf");
if (!BANGLEJS2) {
g.setColor("#71c6cf");
} else {
g.setColor("#002000");
}
g.fillRect(0, 0, g.getWidth(), g.getHeight());
//counter
@ -94,6 +106,7 @@ function draw() {
//draw level
drawLevelText();
drawPointsText();
//dot
g.setColor("#ff0000");
@ -152,7 +165,7 @@ function count() {
if (counter <= 0) {
running = false;
clearInterval(drawInterval);
setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Gameover!");},50);
setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Game over!");},50);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,15 @@
{
"id": "balltastic",
"name": "Balltastic",
"version": "0.03",
"description": "Simple but fun ball eats dots game.",
"icon": "app.png",
"screenshots": [{"url":"bangle2-balltastic-screenshot.png"}],
"type": "app",
"tags": "game,fun",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"balltastic.app.js","url":"app.js"},
{"name":"balltastic.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "banglebridge",
"name": "BangleBridge",
"shortName": "BangleBridge",
"version": "0.01",
"description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App",
"icon": "widget.png",
"type": "widget",
"tags": "widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"banglebridge.wid.js","url":"widget.js"},
{"name":"banglebridge.watch.img","url":"watch.img"},
{"name":"banglebridge.heart.img","url":"heart.img"}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "banglerun",
"name": "BangleRun",
"shortName": "BangleRun",
"version": "0.10",
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
"icon": "banglerun.png",
"tags": "run,running,fitness,outdoors",
"supports": ["BANGLEJS"],
"interface": "interface.html",
"allow_emulator": false,
"storage": [
{"name":"banglerun.app.js","url":"app.js"},
{"name":"banglerun.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,4 @@
0.01: New App!
0.02: Add sit ups
Add more feedback to the user about the exercises
Clean up code

View File

@ -0,0 +1,40 @@
# BanglExercise
Can automatically track exercises while wearing the Bangle.js watch.
Currently only push ups, curls and sit ups are supported.
## Disclaimer
This app is experimental but it seems to work quiet reliable for me.
It could be and is likely that the threshold values for detecting exercises do not work for everyone.
Therefore it would be great if we could improve this app together :-)
## Usage
Select the exercise type you want to practice and go for it!
Press stop to end your exercise.
## Screenshots
![](screenshot.png)
## TODO
* Add other exercise types:
* Rope jumps
* Star jumps
* ...
* Save exercise summaries to file system
* Configure daily goal for exercises
* Find a nicer icon
## Contribute
Feel free to send in improvements and remarks.
## Creator
Marco ([myxor](https://github.com/myxor))
## Icons
Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIbYh/8AYM/+EP/wFBv4FB/4FB/4FHAwIEBAv4FPAgIGCAosHAofggYFD4EABgXgOgIFLDAQWBAo0BAoOAVIV/UYQABj/4AocDCwQFTg46CEY4vFAopBBApIAVA=="))

384
apps/banglexercise/app.js Normal file
View File

@ -0,0 +1,384 @@
const Layout = require("Layout");
const heatshrink = require('heatshrink');
const storage = require('Storage');
let tStart;
let historyY = [];
let historyZ = [];
let historyAvgY = [];
let historyAvgZ = [];
let historySlopeY = [];
let historySlopeZ = [];
let lastZeroPassCameFromPositive;
let lastZeroPassTime = 0;
let lastExerciseCompletionTime = 0;
let lastExerciseHalfCompletionTime = 0;
let exerciseType = {
"id": "",
"name": ""
};
// add new exercises here:
const exerciseTypes = [{
"id": "pushup",
"name": "push ups",
"useYaxis": true,
"useZaxis": false,
"threshold": 2500,
"thresholdMinTime": 800, // mininmal time between two push ups in ms
"thresholdMaxTime": 5000, // maximal time between two push ups in ms
"thresholdMinDurationTime": 600, // mininmal duration of half a push up in ms
},
{
"id": "curl",
"name": "curls",
"useYaxis": true,
"useZaxis": false,
"threshold": 2500,
"thresholdMinTime": 800, // mininmal time between two curls in ms
"thresholdMaxTime": 5000, // maximal time between two curls in ms
"thresholdMinDurationTime": 500, // mininmal duration of half a curl in ms
},
{
"id": "situp",
"name": "sit ups",
"useYaxis": false,
"useZaxis": true,
"threshold": 3500,
"thresholdMinTime": 800, // mininmal time between two sit ups in ms
"thresholdMaxTime": 5000, // maximal time between two sit ups in ms
"thresholdMinDurationTime": 500, // mininmal duration of half a sit up in ms
}
];
let exerciseCounter = 0;
let layout;
let recordActive = false;
// Size of average window for data analysis
const avgSize = 6;
let hrtValue;
let settings = storage.readJSON("banglexercise.json", 1) || {
'buzz': true
};
function showMainMenu() {
let menu;
menu = {
"": {
title: "BanglExercise"
}
};
exerciseTypes.forEach(function(et) {
menu[et.name] = function() {
exerciseType = et;
E.showMenu();
startTraining();
};
});
if (exerciseCounter > 0) {
menu["--------"] = {
value: ""
};
menu["Last:"] = {
value: exerciseCounter + " " + exerciseType.name
};
}
menu.exit = function() {
load();
};
E.showMenu(menu);
}
function accelHandler(accel) {
if (!exerciseType) return;
const t = Math.round(new Date().getTime()); // time in ms
const y = exerciseType.useYaxis ? accel.y * 8192 : 0;
const z = exerciseType.useZaxis ? accel.z * 8192 : 0;
//console.log(t, y, z);
if (exerciseType.useYaxis) {
while (historyY.length > avgSize)
historyY.shift();
historyY.push(y);
if (historyY.length > avgSize / 2) {
const avgY = E.sum(historyY) / historyY.length;
historyAvgY.push([t, avgY]);
while (historyAvgY.length > avgSize)
historyAvgY.shift();
}
}
if (exerciseType.useZaxis) {
while (historyZ.length > avgSize)
historyZ.shift();
historyZ.push(z);
if (historyZ.length > avgSize / 2) {
const avgZ = E.sum(historyZ) / historyZ.length;
historyAvgZ.push([t, avgZ]);
while (historyAvgZ.length > avgSize)
historyAvgZ.shift();
}
}
// slope for Y
if (exerciseType.useYaxis) {
let l = historyAvgY.length;
if (l > 1) {
const p1 = historyAvgY[l - 2];
const p2 = historyAvgY[l - 1];
const slopeY = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000);
// we use this data for exercises which can be detected by using Y axis data
isValidExercise(slopeY, t);
}
}
// slope for Z
if (exerciseType.useZaxis) {
l = historyAvgZ.length;
if (l > 1) {
const p1 = historyAvgZ[l - 2];
const p2 = historyAvgZ[l - 1];
const slopeZ = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000);
// we use this data for some exercises which can be detected by using Z axis data
isValidExercise(slopeZ, t);
}
}
}
/*
* Check if slope value of Y-axis or Z-axis data (depending on exercise type) looks like an exercise
*
* In detail we look for slop values which are bigger than the configured threshold for the current exercise type
* Then we look for two consecutive slope values of which one is above 0 and the other is below zero.
* If we find one pair of these values this could be part of one exercise.
* Then we look for a pair of values which cross the zero from the otherwise direction
*/
function isValidExercise(slope, t) {
if (!exerciseType) return;
const threshold = exerciseType.threshold;
const historySlopeValues = exerciseType.useYaxis ? historySlopeY : historySlopeZ;
const thresholdMinTime = exerciseType.thresholdMinTime;
const thresholdMaxTime = exerciseType.thresholdMaxTime;
const thresholdMinDurationTime = exerciseType.thresholdMinDurationTime;
const exerciseName = exerciseType.name;
if (Math.abs(slope) >= threshold) {
historySlopeValues.push([t, slope]);
//console.log(t, Math.abs(slope));
const lSlopeHistory = historySlopeValues.length;
if (lSlopeHistory > 1) {
const p1 = historySlopeValues[lSlopeHistory - 1][1];
const p2 = historySlopeValues[lSlopeHistory - 2][1];
if (p1 > 0 && p2 < 0) {
if (lastZeroPassCameFromPositive == false) {
lastExerciseHalfCompletionTime = t;
console.log(t, exerciseName + " half complete...");
layout.progress.label = "½";
layout.recording.label = "TRAINING";
g.clear();
layout.render();
}
lastZeroPassCameFromPositive = true;
lastZeroPassTime = t;
}
if (p2 > 0 && p1 < 0) {
if (lastZeroPassCameFromPositive == true) {
const tDiffLastExercise = t - lastExerciseCompletionTime;
const tDiffStart = t - tStart;
console.log(t, exerciseName + " maybe complete?", Math.round(tDiffLastExercise), Math.round(tDiffStart));
// check minimal time between exercises:
if ((lastExerciseCompletionTime <= 0 && tDiffStart >= thresholdMinTime) || tDiffLastExercise >= thresholdMinTime) {
// check maximal time between exercises:
if (lastExerciseCompletionTime <= 0 || tDiffLastExercise <= thresholdMaxTime) {
// check minimal duration of exercise:
const tDiffExerciseHalfCompletion = t - lastExerciseHalfCompletionTime;
if (tDiffExerciseHalfCompletion > thresholdMinDurationTime) {
//console.log(t, exerciseName + " complete!!!");
lastExerciseCompletionTime = t;
exerciseCounter++;
layout.count.label = exerciseCounter;
layout.progress.label = "";
layout.recording.label = "Good!";
g.clear();
layout.render();
if (settings.buzz)
Bangle.buzz(200, 0.5);
} else {
console.log(t, exerciseName + " too quick for duration time threshold!"); // thresholdMinDurationTime
lastExerciseCompletionTime = t;
layout.recording.label = "Go slower!";
g.clear();
layout.render();
}
} else {
console.log(t, exerciseName + " top slow for time threshold!"); // thresholdMaxTime
lastExerciseCompletionTime = t;
layout.recording.label = "Go faster!";
g.clear();
layout.render();
}
} else {
console.log(t, exerciseName + " too quick for time threshold!"); // thresholdMinTime
lastExerciseCompletionTime = t;
layout.recording.label = "Go slower!";
g.clear();
layout.render();
}
}
lastZeroPassCameFromPositive = false;
lastZeroPassTime = t;
}
}
}
}
function reset() {
historyY = [];
historyZ = [];
historyAvgY = [];
historyAvgZ = [];
historySlopeY = [];
historySlopeZ = [];
lastZeroPassCameFromPositive = undefined;
lastZeroPassTime = 0;
lastExerciseHalfCompletionTime = 0;
lastExerciseCompletionTime = 0;
exerciseCounter = 0;
tStart = 0;
}
function startTraining() {
if (recordActive) return;
g.clear(1);
reset();
Bangle.setLCDTimeout(0); // force LCD on
Bangle.setHRMPower(1, "banglexercise");
if (!hrtValue) hrtValue = "...";
layout = new Layout({
type: "v",
c: [{
type: "txt",
id: "type",
font: "6x8:2",
label: exerciseType.name,
pad: 5
},
{
type: "h",
c: [{
type: "txt",
id: "count",
font: exerciseCounter < 100 ? "6x8:9" : "6x8:8",
label: exerciseCounter,
pad: 5
},
{
type: "txt",
id: "progress",
font: "6x8:2",
label: "",
pad: 5
},
]
},
{
type: "h",
c: [{
type: "img",
pad: 4,
src: function() {
return heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
}
},
{
type: "txt",
id: "hrtRate",
font: "6x8:2",
label: hrtValue,
pad: 5
},
]
},
{
type: "txt",
id: "recording",
font: "6x8:2",
label: "TRAINING",
bgCol: "#f00",
pad: 5,
fillx: 1
},
]
}, {
btns: [{
label: "STOP",
cb: () => {
stopTraining();
}
}],
lazy: false
});
layout.render();
Bangle.setPollInterval(80); // 12.5 Hz
tStart = new Date().getTime();
recordActive = true;
if (settings.buzz)
Bangle.buzz(200, 1);
// delay start a little bit
setTimeout(() => {
Bangle.on('accel', accelHandler);
}, 1000);
}
function stopTraining() {
if (!recordActive) return;
g.clear(1);
Bangle.removeListener('accel', accelHandler);
Bangle.setHRMPower(0, "banglexercise");
showMainMenu();
recordActive = false;
}
Bangle.on('HRM', function(hrm) {
hrtValue = hrm.bpm;
});
g.clear(1);
showMainMenu();

BIN
apps/banglexercise/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

View File

@ -0,0 +1,21 @@
{ "id": "banglexercise",
"name": "BanglExercise",
"shortName":"BanglExercise",
"version":"0.02",
"description": "Can automatically track exercises while wearing the Bangle.js watch.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "sport",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"banglexercise.app.js","url":"app.js"},
{"name":"banglexercise.img","url":"app-icon.js","evaluate":true},
{"name":"banglexercise.settings.js","url":"settings.js"}
],
"data": [
{"name":"banglexercise.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,21 @@
(function(back) {
const SETTINGS_FILE = "banglexercise.json";
const storage = require('Storage');
let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
function save(key, value) {
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
E.showMenu({
'': { 'title': 'BanglExercise' },
'< Back': back,
'Buzz': {
value: "buzz" in settings ? settings.buzz : false,
format: () => (settings.buzz ? 'Yes' : 'No'),
onchange: () => {
settings.buzz = !settings.buzz;
save('buzz', settings.buzz);
}
}
});
});

View File

@ -0,0 +1,17 @@
{
"id": "barclock",
"name": "Bar Clock",
"version": "0.09",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"barclock.app.js","url":"clock-bar.js"},
{"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "batchart",
"name": "Battery Chart",
"shortName": "Battery Chart",
"version": "0.10",
"description": "A widget and an app for recording and visualizing battery percentage over time.",
"icon": "app.png",
"tags": "app,widget,battery,time,record,chart,tool",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"batchart.wid.js","url":"widget.js"},
{"name":"batchart.app.js","url":"app.js"},
{"name":"batchart.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "batclock",
"name": "Bat Clock",
"shortName": "Bat Clock",
"version": "0.02",
"description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.",
"icon": "bat-clock.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"batclock.app.js","url":"bat-clock.app.js"},
{"name":"batclock.img","url":"bat-clock.icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "battleship",
"name": "Battleship",
"version": "0.01",
"description": "The classic game of battleship",
"icon": "battleship-icon.png",
"tags": "game",
"supports": ["BANGLEJS"],
"screenshots": [{"url":"bangle1-battle-ship-screenshot.png"}],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"battleship.app.js","url":"battleship.js"},
{"name":"battleship.img","url":"battleship-icon.js","evaluate":true}
]
}

16
apps/bclock/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "bclock",
"name": "Binary Clock",
"version": "0.03",
"description": "A simple binary clock watch face",
"icon": "clock-binary.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"screenshots": [{"url":"bangle1-binary-clock-screenshot.png"}],
"storage": [
{"name":"bclock.app.js","url":"clock-binary.js"},
{"name":"bclock.img","url":"clock-binary-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "beebclock",
"name": "Beeb Clock",
"version": "0.05",
"description": "Clock face that may be coincidentally familiar to BBC viewers",
"icon": "beebclock.png",
"type": "clock",
"tags": "clock",
"screenshots": [{"url":"bangle1-beeb-clock-screenshot.png"}],
"supports": ["BANGLEJS"],
"allow_emulator": true,
"storage": [
{"name":"beebclock.app.js","url":"beebclock.js"},
{"name":"beebclock.img","url":"beebclock-icon.js","evaluate":true}
]
}

14
apps/beer/metadata.json Normal file
View File

@ -0,0 +1,14 @@
{
"id": "beer",
"name": "Beer Compass",
"version": "0.01",
"description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS"],
"custom": "custom.html",
"storage": [
{"name":"beer.app.js"},
{"name":"beer.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "berlinc",
"name": "Berlin Clock",
"version": "0.05",
"description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)",
"icon": "berlin-clock.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"berlin-clock-screenshot.png"}],
"storage": [
{"name":"berlinc.app.js","url":"berlin-clock.js"},
{"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true}
]
}

Some files were not shown because too many files have changed in this diff Show More