Merge pull request #2 from espruino/master

Pull from upstream
pull/370/head
Will Murray 2020-05-01 19:14:09 +01:00 committed by GitHub
commit f4a2f518f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
233 changed files with 15819 additions and 620 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.htaccess
node_modules
package-lock.json
.DS_Store

View File

@ -6,4 +6,9 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong
* Added optional `README.md` file for apps
* Remove 2v04 version warning, add links in About to official/developer versions
* Fix issue removing an app that was just installed (Fix #253)
* Fix issue removing an app that was just installed (fix #253)
* Add `Favourite` functionality
* Version number now clickable even when you're at the latest version (fix #291)
* Rewrite 'getInstalledApps' to minimize RAM usage
* Added code to handle Settings
* Added espruinotools.js for pretokenisation

View File

@ -202,6 +202,11 @@ and which gives information about the app for the Launcher.
"files:"file1,file2,file3",
// added by BangleApps loader on upload - lists all files
// that belong to the app so it can be deleted
"data":"appid.data.json,appid.data?.json;appidStorageFile,appidStorageFile*"
// added by BangleApps loader on upload - lists files that
// the app might write, so they can be deleted on uninstall
// typically these files are not uploaded, but created by the app
// these can include '*' or '?' wildcards
}
```
@ -240,16 +245,24 @@ and which gives information about the app for the Launcher.
"evaluate":true // if supplied, data isn't quoted into a String before upload
// (eg it's evaluated as JS)
},
]
"data": [ // list of files the app writes to
{"name":"appid.data.json", // filename used in storage
"storageFile":true // if supplied, file is treated as storageFile
},
{"wildcard":"appid.data.*" // wildcard of filenames used in storage
}, // this is mutually exclusive with using "name"
],
"sortorder" : 0, // optional - choose where in the list this goes.
// this should only really be used to put system
// stuff at the top
]
}
```
* name, icon and description present the app in the app loader.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
* storage is used to identify the app files and how to handle them
* data is used to clean up files when the app is uninstalled
### `apps.json`: `custom` element
@ -335,10 +348,10 @@ Example `settings.js`
```js
// make sure to enclose the function in parentheses
(function(back) {
let settings = require('Storage').readJSON('app.settings.json',1)||{};
let settings = require('Storage').readJSON('app.json',1)||{};
function save(key, value) {
settings[key] = value;
require('Storage').write('app.settings.json',settings);
require('Storage').write('app.json',settings);
}
const appMenu = {
'': {'title': 'App Settings'},
@ -351,19 +364,20 @@ Example `settings.js`
E.showMenu(appMenu)
})
```
In this example the app needs to add both `app.settings.js` and
`app.settings.json` to `apps.json`:
In this example the app needs to add `app.settings.js` to `storage` in `apps.json`.
It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled.
```json
{ "id": "app",
...
"storage": [
...
{"name":"app.settings.js","url":"settings.js"},
{"name":"app.settings.json","content":"{}"}
],
"data": [
{"name":"app.json"}
]
},
```
That way removing the app also cleans up `app.settings.json`.
## Coding hints

472
apps.json
View File

@ -2,7 +2,7 @@
{ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
"version":"0.14",
"version":"0.15",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"tags": "tool,system",
"type":"bootloader",
@ -41,7 +41,7 @@
"name": "Default Launcher",
"shortName":"Launcher",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"description": "This is needed by Bangle.js to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"tags": "tool,system,launcher",
"type":"launch",
@ -53,7 +53,7 @@
{ "id": "about",
"name": "About",
"icon": "app.png",
"version":"0.04",
"version":"0.05",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"tags": "tool,system",
"allow_emulator":true,
@ -78,7 +78,7 @@
{ "id": "welcome",
"name": "Welcome",
"icon": "app.png",
"version":"0.06",
"version":"0.08",
"description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome",
"allow_emulator":true,
@ -87,12 +87,15 @@
{"name":"welcome.app.js","url":"app.js"},
{"name":"welcome.settings.js","url":"settings.js"},
{"name":"welcome.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"welcome.json"}
]
},
{ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
"version":"0.08",
"version":"0.10",
"description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget",
"type":"widget",
@ -105,7 +108,7 @@
{ "id": "mclock",
"name": "Morphing Clock",
"icon": "clock-morphing.png",
"version":"0.03",
"version":"0.04",
"description": "7 segment clock that morphs between minutes and hours",
"tags": "clock",
"type":"clock",
@ -119,13 +122,13 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
"version":"0.12",
"version":"0.19",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"readme": "README.md",
"storage": [
{"name":"setting.app.js","url":"settings.js"},
{"name":"setting.boot.js","url":"boot.js"},
{"name":"setting.json","url":"settings-default.json","evaluate":true},
{"name":"setting.img","url":"settings-icon.js","evaluate":true}
],
"sortorder" : -2
@ -134,16 +137,18 @@
"name": "Default Alarm",
"shortName":"Alarms",
"icon": "app.png",
"version":"0.06",
"version":"0.07",
"description": "Set and respond to alarms",
"tags": "tool,alarm,widget",
"storage": [
{"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.boot.js","url":"boot.js"},
{"name":"alarm.js","url":"alarm.js"},
{"name":"alarm.json","content":"[]"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"}
],
"data": [
{"name":"alarm.json"}
]
},
{ "id": "wclock",
@ -159,10 +164,23 @@
{"name":"wclock.img","url":"clock-word-icon.js","evaluate":true}
]
},
{ "id": "impwclock",
"name": "Imprecise Word Clock",
"icon": "clock-impword.png",
"version":"0.01",
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"impwclock.app.js","url":"clock-impword.js"},
{"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true}
]
},
{ "id": "aclock",
"name": "Analog Clock",
"icon": "clock-analog.png",
"version": "0.11",
"version": "0.12",
"description": "An Analog Clock",
"tags": "clock",
"type":"clock",
@ -234,7 +252,7 @@
{ "id": "compass",
"name": "Compass",
"icon": "compass.png",
"version":"0.01",
"version":"0.02",
"description": "Simple compass that points North",
"tags": "tool,outdoors",
"storage": [
@ -279,29 +297,48 @@
{ "id": "gpsrec",
"name": "GPS Recorder",
"icon": "app.png",
"version":"0.06",
"version":"0.08",
"interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget",
"storage": [
{"name":"gpsrec.app.js","url":"app.js"},
{"name":"gpsrec.json","url":"app-settings.json","evaluate":true},
{"name":"gpsrec.img","url":"app-icon.js","evaluate":true},
{"name":"gpsrec.wid.js","url":"widget.js"}
],
"data": [
{"name":"gpsrec.json"},
{"wildcard":".gpsrc?","storageFile": true}
]
},
{ "id": "gpsnav",
"name": "GPS Navigation",
"icon": "icon.png",
"version":"0.01",
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"storage": [
{"name":"gpsnav.app.js","url":"app.js"},
{"name":"waypoints.json","url":"waypoints.json","evaluate":false},
{"name":"gpsnav.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "heart",
"name": "Heart Rate Recorder",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget",
"storage": [
{"name":"heart.app.js","url":"app.js"},
{"name":"heart.json","url":"app-settings.json","evaluate":true},
{"name":"heart.img","url":"app-icon.js","evaluate":true},
{"name":"heart.wid.js","url":"widget.js"}
],
"data": [
{"name":"heart.json"},
{"wildcard":".heart?","storageFile": true}
]
},
{ "id": "slevel",
@ -318,7 +355,7 @@
{ "id": "files",
"name": "App Manager",
"icon": "files.png",
"version":"0.02",
"version":"0.03",
"description": "Show currently installed apps, free space, and allow their deletion from the watch",
"tags": "tool,system,files",
"storage": [
@ -326,10 +363,27 @@
{"name":"files.img","url":"files-icon.js","evaluate":true}
]
},
{ "id": "weather",
"name": "Weather",
"icon": "icon.png",
"version":"0.01",
"description": "Show Gadgetbridge weather report",
"readme": "readme.md",
"tags": "widget,outdoors",
"storage": [
{"name":"weather.app.js","url":"app.js"},
{"name":"weather.wid.js","url":"widget.js"},
{"name":"weather","url":"lib.js"},
{"name":"weather.img","url":"icon.js","evaluate":true}
],
"data": [
{"name": "weather.json"}
]
},
{ "id": "widbat",
"name": "Battery Level Widget",
"icon": "widget.png",
"version":"0.04",
"version":"0.05",
"description": "Show the current battery level and charging status in the top right of the clock",
"tags": "widget,battery",
"type":"widget",
@ -341,20 +395,22 @@
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"icon": "widget.png",
"version":"0.08",
"version":"0.11",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery",
"type":"widget",
"storage": [
{"name":"widbatpc.wid.js","url":"widget.js"},
{"name":"widbatpc.settings.js","url":"settings.js"},
{"name":"widbatpc.settings.json","content": "{}"}
{"name":"widbatpc.settings.js","url":"settings.js"}
],
"data": [
{"name":"widbatpc.json"}
]
},
{ "id": "widbt",
"name": "Bluetooth Widget",
"icon": "widget.png",
"version":"0.03",
"version":"0.04",
"description": "Show the current Bluetooth connection status in the top right of the clock",
"tags": "widget,bluetooth",
"type":"widget",
@ -362,6 +418,18 @@
{"name":"widbt.wid.js","url":"widget.js"}
]
},
{ "id": "widram",
"name": "RAM Widget",
"shortName":"RAM Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Display your Bangle's available RAM percentage in a widget",
"tags": "widget",
"type": "widget",
"storage": [
{"name":"widram.wid.js","url":"widget.js"}
]
},
{ "id": "hrm",
"name": "Heart Rate Monitor",
"icon": "heartrate.png",
@ -398,7 +466,7 @@
{ "id": "swatch",
"name": "Stopwatch",
"icon": "stopwatch.png",
"version":"0.05",
"version":"0.07",
"interface": "interface.html",
"description": "Simple stopwatch with Lap Time logging to a JSON file",
"tags": "health",
@ -413,7 +481,7 @@
"name": "Bluetooth Music Controls",
"shortName": "Music Control",
"icon": "hid-music.png",
"version":"0.01",
"version":"0.02",
"description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!",
"tags": "bluetooth",
"storage": [
@ -425,7 +493,7 @@
"name": "Bluetooth Keyboard",
"shortName": "Bluetooth Kbd",
"icon": "hid-keyboard.png",
"version":"0.01",
"version":"0.02",
"description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps",
"tags": "bluetooth",
"storage": [
@ -437,7 +505,7 @@
"name": "Binary Bluetooth Keyboard",
"shortName": "Binary BT Kbd",
"icon": "hid-binary-keyboard.png",
"version":"0.01",
"version":"0.02",
"description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want",
"tags": "bluetooth",
"storage": [
@ -504,7 +572,7 @@
"id": "ncstart",
"name": "NCEU Startup",
"icon": "start.png",
"version":"0.03",
"version":"0.05",
"description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome",
"storage": [
@ -517,6 +585,9 @@
{"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true},
{"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true},
{"name":"nc-tf.img","url":"start-tf.js","evaluate":true}
],
"data": [
{"name":"ncstart.json"}
]
},
{ "id": "ncfrun",
@ -775,7 +846,7 @@
{ "id": "widclk",
"name": "Digital clock widget",
"icon": "widget.png",
"version":"0.03",
"version":"0.04",
"description": "A simple digital clock widget",
"tags": "widget,clock",
"type":"widget",
@ -864,8 +935,8 @@
"name": "Torch",
"shortName":"Torch",
"icon": "app.png",
"version":"0.01",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN3 four times in quick succession to start when in normal clock mode",
"version":"0.02",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets",
"tags": "tool,torch",
"storage": [
{"name":"torch.app.js","url":"app.js"},
@ -876,7 +947,8 @@
{ "id": "wohrm",
"name": "Workout HRM",
"icon": "app.png",
"version":"0.06",
"version":"0.07",
"readme": "README.md",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"tags": "hrm,workout",
"type": "app",
@ -915,7 +987,7 @@
{ "id": "marioclock",
"name": "Mario Clock",
"icon": "marioclock.png",
"version":"0.09",
"version":"0.12",
"description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.",
"tags": "clock,mario,retro",
"type": "clock",
@ -954,7 +1026,7 @@
{ "id": "barclock",
"name": "Bar Clock",
"icon": "clock-bar.png",
"version":"0.04",
"version":"0.05",
"description": "A simple digital clock showing seconds as a bar",
"tags": "clock",
"type":"clock",
@ -1003,7 +1075,7 @@
{ "id": "astrocalc",
"name": "Astrocalc",
"icon": "astrocalc.png",
"version":"0.01",
"version":"0.02",
"description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
"tags": "app,sun,moon,cycles,tool,outdoors",
"allow_emulator":true,
@ -1034,14 +1106,18 @@
},
{ "id": "toucher",
"name": "Touch Launcher",
"shortName":"Menu",
"shortName":"Toucher",
"icon": "app.png",
"version":"0.05",
"version":"0.06",
"description": "Touch enable left to right launcher.",
"tags": "tool,system,launcher",
"type":"launch",
"data": [
{"name":"toucher.json"}
],
"storage": [
{"name":"toucher.app.js","url":"app.js"}
{"name":"toucher.app.js","url":"app.js"},
{"name":"toucher.settings.js","url":"settings.js"}
],
"sortorder" : -10
},
@ -1086,7 +1162,7 @@
{ "id": "minionclk",
"name": "Minion clock",
"icon": "minionclk.png",
"version": "0.01",
"version": "0.02",
"description": "Minion themed clock.",
"tags": "clock,minion",
"type": "clock",
@ -1109,6 +1185,35 @@
{"name":"openstmap.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "activepedom",
"name": "Active Pedometer",
"shortName":"Active Pedometer",
"icon": "app.png",
"version":"0.04",
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
"tags": "outdoors,widget",
"readme": "README.md",
"storage": [
{"name":"activepedom.wid.js","url":"widget.js"},
{"name":"activepedom.settings.js","url":"settings.js"},
{"name":"activepedom.img","url":"app-icon.js","evaluate":true},
{"name":"activepedom.app.js","url":"app.js"}
]
},
{ "id": "chronowid",
"name": "Chrono Widget",
"shortName":"Chrono Widget",
"icon": "app.png",
"version":"0.03",
"description": "Chronometer (timer) which runs as widget.",
"tags": "tools,widget",
"readme": "README.md",
"storage": [
{"name":"chronowid.wid.js","url":"widget.js"},
{"name":"chronowid.app.js","url":"app.js"},
{"name":"chronowid.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "tabata",
"name": "Tabata",
"shortName": "Tabata - Control High-Intensity Interval Training",
@ -1148,9 +1253,10 @@
},
{ "id": "batchart",
"name": "Battery Chart",
"shortName":"BatChart",
"shortName":"Battery Chart",
"icon": "app.png",
"version":"0.03",
"version":"0.09",
"readme": "README.md",
"description": "A widget and an app for recording and visualizing battery percentage over time.",
"tags": "app,widget,battery,time,record,chart,tool",
"storage": [
@ -1158,5 +1264,291 @@
{"name":"batchart.app.js","url":"app.js"},
{"name":"batchart.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "nato",
"name": "NATO Alphabet",
"shortName" : "NATOAlphabet",
"icon": "nato.png",
"version":"0.01",
"type": "app",
"description": "Learn the NATO Phonetic alphabet plus some numbers.",
"tags": "app,learn,visual",
"allow_emulator":true,
"storage": [
{"name":"nato.app.js","url":"nato.js"},
{"name":"nato.img","url":"nato-icon.js","evaluate":true}
]
},
{ "id": "numerals",
"name": "Numerals Clock",
"shortName": "Numerals Clock",
"icon": "numerals.png",
"version":"0.04",
"description": "A simple big numerals clock",
"tags": "numerals,clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"numerals.app.js","url":"numerals.app.js"},
{"name":"numerals.img","url":"numerals-icon.js","evaluate":true},
{"name":"numerals.settings.js","url":"numerals.settings.js"}
],
"data":[
{"name":"numerals.json"}
]
},
{ "id": "bledetect",
"name": "BLE Detector",
"shortName":"BLE Detector",
"icon": "bledetect.png",
"version":"0.02",
"description": "Detect BLE devices and show some informations.",
"tags": "app,bluetooth,tool",
"readme": "README.md",
"storage": [
{"name":"bledetect.app.js","url":"bledetect.js"},
{"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true}
]
},
{ "id": "snake",
"name": "Snake",
"shortName":"Snake",
"icon": "snake.png",
"version":"0.02",
"description": "The classic snake game. Eat apples and don't bite your tail.",
"tags": "game,fun",
"readme": "README.md",
"storage": [
{"name":"snake.app.js","url":"snake.js"},
{"name":"snake.img","url":"snake-icon.js","evaluate":true}
]
},
{ "id": "calculator",
"name": "Calculator",
"shortName":"Calculator",
"icon": "calculator.png",
"version":"0.02",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"tags": "app,tool",
"storage": [
{"name":"calculator.app.js","url":"app.js"},
{"name":"calculator.img","url":"calculator-icon.js","evaluate":true}
]
},
{
"id": "dane",
"name": "Digital Assistant, not EDITH",
"shortName": "DANE",
"icon": "app.png",
"version": "0.07",
"description": "A Watchface inspired by Tony Stark's EDITH",
"tags": "clock",
"type": "clock",
"allow_emulator": true,
"storage": [
{
"name": "dane.app.js",
"url": "app.js"
},
{
"name": "dane.img",
"url": "app-icon.js",
"evaluate": true
}
]
},
{
"id": "buffgym",
"name": "BuffGym",
"icon": "buffgym.png",
"version":"0.02",
"description": "BuffGym is the famous 5x5 workout program for the BangleJS",
"tags": "tool,outdoors,gym,exercise",
"type": "app",
"interface": "buffgym.html",
"allow_emulator": false,
"readme": "README.md",
"storage": [
{"name":"buffgym.app.js", "url": "buffgym.app.js"},
{"name":"buffgym-set.js","url":"buffgym-set.js"},
{"name":"buffgym-exercise.js","url":"buffgym-exercise.js"},
{"name":"buffgym-workout.js","url":"buffgym-workout.js"},
{"name":"buffgym-workout-a.json","url":"buffgym-workout-a.json"},
{"name":"buffgym-workout-b.json","url":"buffgym-workout-b.json"},
{"name":"buffgym-workout-index.json","url":"buffgym-workout-index.json"},
{"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true}
]
},
{
"id": "banglerun",
"name": "BangleRun",
"shortName": "BangleRun",
"icon": "banglerun.png",
"version": "0.01",
"description": "An app for running sessions.",
"tags": "run,running,fitness,outdoors",
"allow_emulator": false,
"storage": [
{
"name": "banglerun.app.js",
"url": "app.js"
},
{
"name": "banglerun.img",
"url": "app-icon.js",
"evaluate": true
}
]
},
{
"id": "metronome",
"name": "Metronome",
"icon": "metronome_icon.png",
"version": "0.03",
"readme": "README.md",
"description": "Makes the watch blinking and vibrating with a given rate",
"tags": "tool",
"allow_emulator": true,
"storage": [
{
"name": "metronome.app.js",
"url": "metronome.js"
},
{
"name": "metronome.img",
"url": "metronome-icon.js",
"evaluate": true
}
]
},
{ "id": "blackjack",
"name": "Black Jack game",
"shortName":"Black Jack game",
"icon": "blackjack.png",
"version":"0.01",
"description": "Simple implementation of card game Black Jack",
"tags": "game",
"allow_emulator":true,
"storage": [
{"name":"blackjack.app.js","url":"blackjack.app.js"},
{"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true}
]
},
{ "id": "hidcam",
"name": "Camera shutter",
"shortName":"Cam shutter",
"icon": "app.png",
"version":"0.03",
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
"readme": "README.md",
"tags": "bluetooth,tool",
"storage": [
{"name":"hidcam.app.js","url":"app.js"},
{"name":"hidcam.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "rclock",
"name": "Round clock with seconds, minutes and date",
"shortName":"Round Clock",
"icon": "app.png",
"version":"0.01",
"description": "Designed round clock with ticks for minutes and seconds",
"tags": "clock",
"type": "clock",
"storage": [
{"name":"rclock.app.js","url":"rclock.app.js"},
{"name":"rclock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "hamloc",
"name": "QTH Locator / Maidenhead Locator System",
"shortName": "QTH Locator",
"icon": "app.png",
"version":"0.01",
"description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"storage": [
{"name":"hamloc.app.js","url":"app.js"},
{"name":"hamloc.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "osmpoi",
"name": "POI Compass",
"icon": "app.png",
"version":"0.03",
"description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"custom": "osmpoi.html",
"storage": [
{"name":"osmpoi.app.js"},
{"name":"osmpoi.img"}
]
},
{ "id": "pong",
"name": "Pong",
"shortName": "Pong",
"icon": "pong.png",
"version": "0.02",
"description": "A clone of the Atari game Pong",
"tags": "game",
"type": "app",
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"pong.app.js","url":"app.js"},
{"name":"pong.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "ballmaze",
"name": "Ball Maze",
"icon": "icon.png",
"version": "0.01",
"description": "Navigate a ball through a maze by tilting your watch.",
"readme": "README.md",
"tags": "game",
"type": "app",
"storage": [
{"name": "ballmaze.app.js","url":"app.js"},
{"name": "ballmaze.img","url":"icon.js","evaluate": true}
],
"data": [
{"name": "ballmaze.json"}
]
},
{
"id": "calendar",
"name": "Calendar",
"icon": "calendar.png",
"version": "0.01",
"description": "Simple calendar",
"tags": "calendar",
"readme": "README.md",
"allow_emulator": true,
"storage": [
{
"name": "calendar.app.js",
"url": "calendar.js"
},
{
"name": "calendar.img",
"url": "calendar-icon.js",
"evaluate": true
}
]
},
{ "id": "hidjoystick",
"name": "Bluetooth Joystick",
"shortName": "Joystick",
"icon": "app.png",
"version":"0.01",
"description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.",
"tags": "bluetooth",
"storage": [
{"name":"hidjoystick.app.js","url":"app.js"},
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
]
}
]

View File

@ -0,0 +1,25 @@
# App Name
Describe the app...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -6,6 +6,7 @@
"version":"0.01",
"description": "A detailed description of my great app",
"tags": "",
"readme": "README.md",
"storage": [
{"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true}

View File

@ -0,0 +1,25 @@
# Widget Name
Describe the app...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -7,6 +7,7 @@
"description": "A detailed description of my great widget",
"tags": "widget",
"type": "widget",
"readme": "README.md",
"storage": [
{"name":"7chname.wid.js","url":"widget.js"}
]

View File

@ -2,3 +2,4 @@
0.02: Update version checker for new filename type
0.03: Actual pixels as of 5 Mar 2020
0.04: Actual pixels as of 9 Mar 2020
0.05: Actual pixels as of 27 Apr 2020

File diff suppressed because one or more lines are too long

View File

@ -6,3 +6,4 @@
0.09: center date, remove box around it, internal refactor to remove redundant code.
0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed
0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date
0.12: Fix regression after 0.11

View File

@ -1,7 +1,3 @@
// eliminate ide undefined errors
let g;
let Bangle;
// http://forum.espruino.com/conversations/345155/#comment15172813
const locale = require('locale');
const p = Math.PI / 2;

BIN
apps/activepedom/10600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
apps/activepedom/1600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

BIN
apps/activepedom/600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

View File

@ -0,0 +1,4 @@
0.01: New Widget!
0.02: Distance calculation and display
0.03: Data logging and display
0.04: Steps are set to 0 in log on new day

View File

@ -0,0 +1,72 @@
# Active Pedometer
Pedometer that filters out arm movement and displays a step goal progress.
I changed the step counting algorithm completely.
Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long.
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
When you reach the step threshold, the steps needed to reach the threshold are counted as well.
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
## Screenshots
* 600 steps
![](600.png)
* 1600 steps
![](1600.png)
* 10600 steps
![](10600.png)
## Features Widget
* Two line display
* Can display distance (in km) or steps in each line
* Large number for good readability
* Small number with the exact steps counted or more exact distance
* Large number is displayed in green when status is 'active'
* Progress bar for step goal
* Counts steps only if they are reached in a certain time
* Filters out steps where time between two steps is too long or too short
* Step detection sensitivity from firmware can be configured
* Steps are saved to a file and read-in at start (to not lose step progress)
* Settings can be changed in Settings - App/widget settings - Active Pedometer
## Features App
* The app accesses the data stored for the current day
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
## Data storage
* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data)
* One file is created for each day
* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime
* 'now' is UNIX timestamp in ms
* You can use the app to watch a steps graph
* You can import the file into Excel
* The file does not include a header
* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400)
* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss
## Settings
* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100
* Min time (ms): Minimum time between two steps in milliseconds, steps will not be counted if fallen below. Standard: 240
* Step threshold: How many steps are needed to reach 'active' mode. If you do not reach the threshold in the 'Active Reset' time, the steps are not counted. Standard: 30
* Act.Res. (ms): Active Reset. After how many miliseconds will the 'active mode' reset. You have to reach the step threshold in this time, otherwise the steps are not counted. Standard: 30000
* Step sens.: Step Sensitivity. How sensitive should the sted detection be? This changes sensitivity in step detection in the firmware. Standard in firmware: 80
* Step goal: This is your daily step goal. Standard: 10000
* Step length: Length of one step in cm. Standard: 75
* Line One: What to display in line one, steps or distance. Standard: steps
* Line Two: What to display in line two, steps or distance. Standard: distance
## Releases
* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/activepedom (https://banglejs.com/apps)
* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/activepedom (https://purple-tentacle.github.io/BangleApps/#widget)
* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer
## Requests
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I="))

165
apps/activepedom/app.js Normal file
View File

@ -0,0 +1,165 @@
(() => {
//Graph module, as long as modules are not added by the app loader
Modules.addCached("graph",function(){exports.drawAxes=function(b,c,a){function h(a){return e+m*(a-t)/x}function l(a){return f+g-g*(a-n)/u}var k=a.padx||0,d=a.pady||0,t=-k,w=c.length+k-1,n=(void 0!==a.miny?a.miny:a.miny=c.reduce(function(a,b){return Math.min(a,b)},c[0]))-d;c=(void 0!==a.maxy?a.maxy:a.maxy=c.reduce(function(a,b){return Math.max(a,b)},c[0]))+d;a.gridy&&(d=a.gridy,n=d*Math.floor(n/d),c=d*Math.ceil(c/d));var e=a.x||0,f=a.y||0,m=a.width||b.getWidth()-(e+1),g=a.height||b.getHeight()-(f+1);a.axes&&(null!==a.ylabel&&
(e+=6,m-=6),null!==a.xlabel&&(g-=6));a.title&&(f+=6,g-=6);a.axes&&(b.drawLine(e,f,e,f+g),b.drawLine(e,f+g,e+m,f+g));a.title&&(b.setFontAlign(0,-1),b.drawString(a.title,e+m/2,f-6));var x=w-t,u=c-n;u||(u=1);if(a.gridx){b.setFontAlign(0,-1,0);var v=a.gridx;for(d=Math.ceil((t+k)/v)*v;d<=w-k;d+=v){var r=h(d),p=a.xlabel?a.xlabel(d):d;b.setPixel(r,f+g-1);var q=b.stringWidth(p)/2;null!==a.xlabel&&r>q&&b.getWidth()>r+q&&b.drawString(p,r,f+g+2)}}if(a.gridy)for(b.setFontAlign(0,0,1),d=n;d<=c;d+=a.gridy)k=l(d),
p=a.ylabel?a.ylabel(d):d,b.setPixel(e+1,k),q=b.stringWidth(p)/2,null!==a.ylabel&&k>q&&b.getHeight()>k+q&&b.drawString(p,e-5,k+1);b.setFontAlign(-1,-1,0);return{x:e,y:f,w:m,h:g,getx:h,gety:l}};exports.drawLine=function(b,c,a){a=a||{};a=exports.drawAxes(b,c,a);var h=!0,l;for(l in c)h?b.moveTo(a.getx(l),a.gety(c[l])):b.lineTo(a.getx(l),a.gety(c[l])),h=!1;return a};exports.drawBar=function(b,c,a){a=a||{};a.padx=1;a=exports.drawAxes(b,c,a);for(var h in c)b.fillRect(a.getx(h-.5)+1,a.gety(c[h]),a.getx(h+
.5)-1,a.gety(0));return a}});
const storage = require("Storage");
const SETTINGS_FILE = 'activepedom.settings.json';
var history = 86400000; // 28800000=8h 43200000=12h //86400000=24h
//return setting
function setting(key) {
//define default settings
const DEFAULTS = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
'stepLength' : 75,
};
if (!settings) { loadSettings(); }
return (key in settings) ? settings[key] : DEFAULTS[key];
}
//Convert ms to time
function getTime(t) {
date = new Date(t);
offset = date.getTimezoneOffset() / 60;
//var milliseconds = parseInt((t % 1000) / 100),
seconds = Math.floor((t / 1000) % 60);
minutes = Math.floor((t / (1000 * 60)) % 60);
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
hours = hours - offset;
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
}
function getDate(t) {
date = new Date(t*1);
year = date.getFullYear();
month = date.getMonth()+1; //month is zero-based
day = date.getDate();
month = (month < 10) ? "0" + month : month;
day = (day < 10) ? "0" + day : day;
return year + "-" + month + "-" + day;
}
//columns: 0=time, 1=stepsCounted, 2=active, 3=stepsTooShort, 4=stepsTooLong, 5=stepsOutsideTime
function getArrayFromCSV(file, column) {
i = 0;
array = [];
now = new Date();
while ((nextLine = file.readLine())) { //as long as there is a next line
if(nextLine) {
dataSplitted = nextLine.split(','); //split line,
diff = now - dataSplitted[0]; //calculate difference between now and stored time
if (diff <= history) { //only entries from the last x ms
array.push(dataSplitted[column]);
}
}
i++;
}
return array;
}
function drawGraph() {
//times
// actives = getArrayFromCSV(csvFile, 2);
// shorts = getArrayFromCSV(csvFile, 3);
// longs = getArrayFromCSV(csvFile, 4);
// outsides = getArrayFromCSV(csvFile, 5); //array.push(dataSplitted[5].slice(0,-1));
now = new Date();
month = now.getMonth() + 1;
if (month < 10) month = "0" + month;
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data";
var csvFile = storage.open(filename, "r");
times = getArrayFromCSV(csvFile, 0);
first = getDate(times[0]) + " " + getTime(times[0]); //first entry in datafile
last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); //last entry in datafile
//free memory
csvFile = undefined;
times = undefined;
//steps
var csvFile = storage.open(filename, "r");
steps = getArrayFromCSV(csvFile, 1);
first = first + " " + steps[0] + "/" + setting('stepGoal');
last = last + " " + steps[steps.length-1] + "/" + setting('stepGoal');
//define y-axis grid labels
stepsLastEntry = steps[steps.length-1];
if (stepsLastEntry < 1000) gridyValue = 100;
if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 1000;
if (stepsLastEntry > 10000) gridyValue = 5000;
//draw
drawMenu();
g.drawString("First: " + first, 10, 30);
g.drawString(" Last: " + last, 10, 40);
require("graph").drawLine(g, steps, {
//title: "Steps Counted",
axes : true,
gridy : gridyValue,
y : 60, //offset on screen
x : 5, //offset on screen
});
//free memory from big variables
allData = undefined;
allDataFile = undefined;
csvFile = undefined;
times = undefined;
}
function drawMenu () {
g.clear();
g.setFont("6x8", 1);
g.drawString("BTN1:Timespan | BTN2:Draw", 20, 10);
g.drawString("Timespan: " + history/1000/60/60 + " hours", 20, 20);
}
setWatch(function() { //BTN1
switch(history) {
case 3600000 : //1h
history = 14400000; //4h
break;
case 86400000 : //24
history = 3600000; //1h
break;
default :
history = history + 14400000; //4h
break;
}
drawMenu();
}, BTN1, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN2
g.setFont("6x8", 2);
g.drawString ("Drawing...",30,60);
drawGraph();
}, BTN2, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN3
}, BTN3, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN4
}, BTN4, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN5
}, BTN5, {edge:"rising", debounce:50, repeat:true});
//load settings
let settings;
function loadSettings() {
settings = storage.readJSON(SETTINGS_FILE, 1) || {};
}
drawMenu();
})();

BIN
apps/activepedom/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,112 @@
// This file should contain exactly one function, which shows the app's settings
/**
* @param {function} back Use back() to return to settings menu
*/
(function(back) {
const SETTINGS_FILE = 'activepedom.settings.json';
const LINES = ['Steps', 'Distance'];
// initialize with default settings...
let s = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
'stepLength' : 75,
'lineOne': LINES[0],
'lineTwo': LINES[1],
};
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage');
const saved = storage.readJSON(SETTINGS_FILE, 1) || {};
for (const key in saved) {
s[key] = saved[key];
}
// creates a function to safe a specific setting, e.g. save('color')(1)
function save(key) {
return function (value) {
s[key] = value;
storage.write(SETTINGS_FILE, s);
//WIDGETS["activepedom"].draw();
};
}
const menu = {
'': { 'title': 'Active Pedometer' },
'< Back': back,
'Max time (ms)': {
value: s.cMaxTime,
min: 0,
max: 10000,
step: 100,
onchange: save('cMaxTime'),
},
'Min time (ms)': {
value: s.cMinTime,
min: 0,
max: 500,
step: 10,
onchange: save('cMinTime'),
},
'Step threshold': {
value: s.stepThreshold,
min: 0,
max: 100,
step: 1,
onchange: save('stepThreshold'),
},
'Act.Res. (ms)': {
value: s.intervalResetActive,
min: 100,
max: 100000,
step: 1000,
onchange: save('intervalResetActive'),
},
'Step sens.': {
value: s.stepSensitivity,
min: 0,
max: 1000,
step: 10,
onchange: save('stepSensitivity'),
},
'Step goal': {
value: s.stepGoal,
min: 1000,
max: 100000,
step: 1000,
onchange: save('stepGoal'),
},
'Step length (cm)': {
value: s.stepLength,
min: 1,
max: 150,
step: 1,
onchange: save('stepLength'),
},
'Line One': {
format: () => s.lineOne,
onchange: function () {
// cycles through options
const oldIndex = LINES.indexOf(s.lineOne)
const newIndex = (oldIndex + 1) % LINES.length
s.lineOne = LINES[newIndex]
save('lineOne')(s.lineOne)
},
},
'Line Two': {
format: () => s.lineTwo,
onchange: function () {
// cycles through options
const oldIndex = LINES.indexOf(s.lineTwo)
const newIndex = (oldIndex + 1) % LINES.length
s.lineTwo = LINES[newIndex]
save('lineTwo')(s.lineTwo)
},
},
};
E.showMenu(menu);
});

232
apps/activepedom/widget.js Normal file
View File

@ -0,0 +1,232 @@
(() => {
var stepTimeDiff = 9999; //Time difference between two steps
var startTimeStep = new Date(); //set start time
var stopTimeStep = 0; //Time after one step
var timerResetActive = 0; //timer to reset active
var timerStoreData = 0; //timer to store data
var steps = 0; //steps taken
var stepsCounted = 0; //active steps counted
var active = 0; //x steps in y seconds achieved
var stepGoalPercent = 0; //percentage of step goal
var stepGoalBarLength = 0; //length og progress bar
var lastUpdate = new Date(); //used to reset counted steps on new day
var width = 46; //width of widget
//used for statistics and debugging
var stepsTooShort = 0;
var stepsTooLong = 0;
var stepsOutsideTime = 0;
var distance = 0; //distance travelled
const s = require('Storage');
const SETTINGS_FILE = 'activepedom.settings.json';
const PEDOMFILE = "activepedom.steps.json";
var dataFile;
var storeDataInterval = 5*60*1000; //ms
let settings;
//load settings
function loadSettings() {
settings = s.readJSON(SETTINGS_FILE, 1) || {};
}
function storeData() {
now = new Date();
month = now.getMonth() + 1; //month is 0-based
if (month < 10) month = "0" + month; //leading 0
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; //new file for each day
dataFile = s.open(filename,"a");
if (dataFile) { //check if filen already exists
if (dataFile.getLength() == 0) {
//new day, set steps to 0
stepsCounted = 0;
stepsTooShort = 0;
stepsTooLong = 0;
stepsOutsideTime = 0;
}
dataFile.write([
now.getTime(),
stepsCounted,
active,
stepsTooShort,
stepsTooLong,
stepsOutsideTime,
].join(",")+"\n");
}
dataFile = undefined; //save memory
}
//return setting
function setting(key) {
//define default settings
const DEFAULTS = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
'stepLength' : 75,
};
if (!settings) { loadSettings(); }
return (key in settings) ? settings[key] : DEFAULTS[key];
}
function setStepSensitivity(s) {
function sqr(x) { return x*x; }
var X=sqr(8192-s);
var Y=sqr(8192+s);
Bangle.setOptions({stepCounterThresholdLow:X,stepCounterThresholdHigh:Y});
}
//format number to make them shorter
function kFormatterSteps(num) {
if (num <= 999) return num; //smaller 1.000, return 600 as 600
if (num >= 1000 && num < 10000) { //between 1.000 and 10.000
num = Math.floor(num/100)*100;
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 1600 as 1.6k
}
if (num >= 10000) { //greater 10.000
num = Math.floor(num/1000)*1000;
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 10.600 as 10k
}
}
//Set Active to 0
function resetActive() {
active = 0;
steps = 0;
if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw();
}
function calcSteps() {
stopTimeStep = new Date(); //stop time after each step
stepTimeDiff = stopTimeStep - startTimeStep; //time between steps in milliseconds
startTimeStep = new Date(); //start time again
//Remove step if time between first and second step is too long
if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds
stepsTooLong++; //count steps which are not counted, because time too long
steps--;
}
//Remove step if time between first and second step is too short
if (stepTimeDiff <= setting('cMinTime')) { //milliseconds
stepsTooShort++; //count steps which are not counted, because time too short
steps--;
}
//Step threshold reached
if (steps >= setting('stepThreshold')) {
if (active == 0) {
stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1
stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reach active status
}
active = 1;
clearInterval(timerResetActive); //stop timer which resets active
timerResetActive = setInterval(resetActive, setting('intervalResetActive')); //reset active after timer runs out
steps = 0;
}
if (active == 1) {
stepsCounted++; //count steps
}
else {
stepsOutsideTime++;
}
settings = 0; //reset settings to save memory
}
function draw() {
var height = 23; //width is deined globally
distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km
//Check if same day
let date = new Date();
if (lastUpdate.getDate() == date.getDate()){ //if same day
}
else { //different day, set all steps to 0
stepsCounted = 0;
stepsTooShort = 0;
stepsTooLong = 0;
stepsOutsideTime = 0;
}
lastUpdate = date;
g.reset();
g.clearRect(this.x, this.y, this.x+width, this.y+height);
//draw numbers
if (active == 1) g.setColor(0x07E0); //green
else g.setColor(0xFFFF); //white
g.setFont("6x8", 2);
if (setting('lineOne') == 'Steps') {
g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps
}
if (setting('lineOne') == 'Distance') {
g.drawString(distance.toFixed(2),this.x+1,this.y); //first line, big number, distance
}
g.setFont("6x8", 1);
g.setColor(0xFFFF); //white
if (setting('lineTwo') == 'Steps') {
g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number, steps
}
if (setting('lineTwo') == 'Distance') {
g.drawString(distance.toFixed(3) + "km",this.x+1,this.y+14); //second line, small number, distance
}
//draw step goal bar
stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100;
stepGoalBarLength = width / 100 * stepGoalPercent;
if (stepGoalBarLength > width) stepGoalBarLength = width; //do not draw across width of widget
g.setColor(0x7BEF); //grey
g.fillRect(this.x, this.y+height, this.x+width, this.y+height); // draw background bar
g.setColor(0xFFFF); //white
g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar
g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar
g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar
settings = 0; //reset settings to save memory
}
//This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off()
E.on('kill', () => {
let d = { //define array to write to file
lastUpdate : lastUpdate.toISOString(),
stepsToday : stepsCounted,
stepsTooShort : stepsTooShort,
stepsTooLong : stepsTooLong,
stepsOutsideTime : stepsOutsideTime
};
s.write(PEDOMFILE,d); //write array to file
});
//When Step is registered by firmware
Bangle.on('step', (up) => {
steps++; //increase step count
calcSteps();
if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw();
});
// redraw when the LCD turns on
Bangle.on('lcdPower', function(on) {
if (on) WIDGETS["activepedom"].draw();
});
//Read data from file and set variables
let pedomData = s.readJSON(PEDOMFILE,1);
if (pedomData) {
if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate);
stepsCounted = pedomData.stepsToday|0;
stepsTooShort = pedomData.stepsTooShort;
stepsTooLong = pedomData.stepsTooLong;
stepsOutsideTime = pedomData.stepsOutsideTime;
}
pedomdata = 0; //reset pedomdata to save memory
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly
//Add widget
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw};
})();

View File

@ -4,3 +4,4 @@
0.04: Tweaks for variable size widget system
0.05: Add alarm.boot.js and move code from the bootloader
0.06: Change 'New Alarm' to 'Save', allow Deletion of Alarms
0.07: Don't overwrite existing settings on app update

View File

@ -1 +1,2 @@
0.01: Create astrocalc app
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start

View File

@ -1,8 +1,18 @@
/**
* BangleJS ASTROCALC
*
* Inspired by: https://www.timeanddate.com
*
* Original Author: Paul Cockrell https://github.com/paulcockrell
* Created: April 2020
*
* Calculate the Sun and Moon positions based on watch GPS and display graphically
*/
const SunCalc = require("suncalc.js");
const storage = require("Storage");
const LAST_GPS_FILE = "astrocalc.gps.json";
let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null);
function drawMoon(phase, x, y) {
const moonImgFiles = [
@ -296,22 +306,49 @@ function indexPageMenu(gps) {
return E.showMenu(menu);
}
function getCenterStringX(str) {
return (g.getWidth() - g.stringWidth(str)) / 2;
}
/**
* GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page
*/
function drawGPSWaitPage() {
const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="))
const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="));
const str1 = "Astrocalc v0.02";
const str2 = "Locating GPS";
const str3 = "Please wait...";
g.clear();
g.drawImage(img, 100, 50);
g.setFont("6x8", 1);
g.drawString("Astrocalc v0.01", 80, 105);
g.drawString("Locating GPS", 85, 140);
g.drawString("Please wait...", 80, 155);
g.drawString(str1, getCenterStringX(str1), 105);
g.drawString(str2, getCenterStringX(str2), 140);
g.drawString(str3, getCenterStringX(str3), 155);
if (lastGPS) {
lastGPS = JSON.parse(lastGPS);
lastGPS.time = new Date();
const str4 = "Press Button 3 to use last GPS";
g.setColor("#d32e29");
g.fillRect(0, 190, g.getWidth(), 215);
g.setColor("#ffffff");
g.drawString(str4, getCenterStringX(str4), 200);
setWatch(() => {
clearWatch();
Bangle.setGPSPower(0);
m = indexPageMenu(lastGPS);
}, BTN3, {repeat: false});
}
g.flip();
const DEBUG = false;
if (DEBUG) {
clearWatch();
const gps = {
"lat": 56.45783133333,
"lon": -3.02188583333,
@ -330,7 +367,10 @@ function drawGPSWaitPage() {
Bangle.on('GPS', (gps) => {
if (gps.fix === 0) return;
clearWatch();
if (isNaN(gps.course)) gps.course = 0;
require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps));
Bangle.setGPSPower(0);
Bangle.buzz();
Bangle.setLCDPower(true);

15
apps/ballmaze/README.md Normal file
View File

@ -0,0 +1,15 @@
# Ball Maze
Navigate a ball through a maze by tilting your watch.
![Screenshot](size_select.png)
![Screenshot](maze.png)
## Usage
Select a maze size to begin the game.
Tilt your watch to steer the ball towards the target and advance to the next level.
## Creator
Richard de Boer <rigrig+banglejs@tubul.net>

552
apps/ballmaze/app.js Normal file
View File

@ -0,0 +1,552 @@
(() => {
let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {};
// density, elasticity of bounces, "drag coefficient"
const rho = 100, e = 0.3, C = 0.01;
// screen width & height in pixels
const sW = 240, sH = 160;
// gravity constant (lowercase was already taken)
const G = 9.80665;
// wall bit flags
const TOP = 1<<0, LEFT = 1<<1, BOTTOM = 1<<2, RIGHT = 1<<3,
LINKED = 1<<4; // used in maze generation
// The play area is 240x160, sizes are the ball radius, so we can use common
// denominators of 120x80 to get square rooms
// Reverse the order to show the easiest on top of the menu
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(),
// even size 1 actually works, but larger mazes take forever to generate
minSize = 4, defaultSize = 10;
const sizeNames = {
1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large",
10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial",
};
/**
* Draw something to all screen buffers
* @param draw {function} Callback which performs the drawing
*/
function drawAll(draw) {
draw();
g.flip();
draw();
g.flip();
}
/**
* Clear all buffers
*/
function clearAll() {
drawAll(() => g.clear());
}
// use unbuffered graphics for UI stuff
function showMessage(message, title) {
Bangle.setLCDMode();
return E.showMessage(message, title);
}
function showPrompt(prompt, options) {
Bangle.setLCDMode();
return E.showPrompt(prompt, options);
}
function showMenu(menu) {
Bangle.setLCDMode();
return E.showMenu(menu);
}
const sign = (n) => n<0?-1:1; // we don't really care about zero
/**
* Play the game, using a ball with radius size
* @param size {number}
*/
function playMaze(size) {
const r = size;
// ball mass, weight, "drag"
// Yes, larger maze = larger ball = heavier ball
// (atm our physics is so oversimplified that mass cancels out though)
const m = rho*(r*r*r), w = G*m, d = C*w;
// number of columns/rows
const cols = Math.round(sW/(r*2.5)),
rows = Math.round(sH/(r*2.5));
// width & height of one column/row in pixels
const cW = sW/cols, rH = sH/rows;
// list of rooms, every room can have one or more wall bits set
// actual layout: 0 1 2
// 3 4 5
// this means that for room with index "i": (except edge cases!)
// i-1 = room to the left
// i+1 = room to the right
// i-cols = room above
// i+cols = room below
let rooms = new Uint8Array(rows*cols);
// shortest route from start to finish
let route;
let x, y, // current position
px, py, ppx, ppy, // previous positions (for erasing old image)
vx, vy; // velocity
function start() {
// start in top left corner
x = cW/2;
y = rH/2;
vx = vy = 0;
ppx = px = x;
ppy = py = y;
generateMaze(); // this shows unbuffered progress messages
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
Bangle.setLCDMode("doublebuffered");
clearAll();
drawAll(drawMaze);
intervalID = setInterval(tick, 100);
}
// Position conversions
// index: index of room in rooms[]
// rowcol: position measured in roomsizes
// xy: position measured in pixels
/**
* Index from RowCol
* @param row {number}
* @param col {number}
* @returns {number} rooms[] index
*/
function iFromRC(row, col) {
return row*cols+col;
}
/**
* RowCol from index
* @param index {number}
* @returns {(number)[]} [row,column]
*/
function rcFromI(index) {
return [
Math.floor(index/cols),
index%cols,
];
}
/**
* RowCol from Xy
* @param x {number}
* @param y {number}
* @returns {(number)[]} [row,column]
*/
function rcFromXy(x, y) {
return [
Math.floor(y/sH*rows),
Math.floor(x/sW*cols),
];
}
/**
* Link another room up
* @param index {number} Dig from already linked room with this index
* @param dir {number} in this direction
* @return {number} index of room we just linked up
*/
function dig(index, dir) {
rooms[index] &= ~dir;
let neighbour;
switch(dir) {
case LEFT:
neighbour = index-1;
rooms[neighbour] &= ~RIGHT;
break;
case RIGHT:
neighbour = index+1;
rooms[neighbour] &= ~LEFT;
break;
case TOP:
neighbour = index-cols;
rooms[neighbour] &= ~BOTTOM;
break;
case BOTTOM:
neighbour = index+cols;
rooms[neighbour] &= ~TOP;
break;
}
rooms[neighbour] |= LINKED;
return neighbour;
}
/**
* Generate the maze
*/
function generateMaze() {
// Maze generation basically works like this:
// 1. Start with all rooms set to completely walled off and "unlinked"
// 2. Then mark a room as "linked", and add it to the "to do" list
// 3. When the "to do" list is empty, we're done
// 4. pick a random room from the list
// 5. if all adjacent rooms are linked -> remove room from list, goto 3
// 6. pick a random unlinked adjacent room
// 7. remove the walls between the rooms
// 8. mark the adjacent room as linked and add it to the "to do" list
// 9. go to 4
let pdotnum = 0;
const title = "Please wait",
message = "Generating maze\n",
showProgress = (done, total) => {
const dotnum = Math.floor(done/total*10);
if (dotnum>pdotnum) {
const dots = ".".repeat(dotnum)+" ".repeat(10-dotnum);
showMessage(message+dots, title);
pdotnum = dotnum;
}
};
showProgress(0, 100);
// start with all rooms completely walled off
rooms.fill(TOP|LEFT|BOTTOM|RIGHT);
const
// is room at row,col already linked?
linked = (row, col) => !!(rooms[iFromRC(row, col)]&LINKED),
// pick random array element
pickRandom = (arr) => arr[Math.floor(Math.random()*arr.length)];
// starting with top-right room seems to generate more interesting mazes
rooms[cols] |= LINKED;
let todo = [cols], done = 1;
while(todo.length) {
const index = pickRandom(todo);
const rc = rcFromI(index),
row = rc[0], col = rc[1];
let sides = [];
if ((col>0) && !linked(row, col-1)) sides.push(LEFT);
if ((col<cols-1) && !linked(row, col+1)) sides.push(RIGHT);
if ((row>0) && !linked(row-1, col)) sides.push(TOP);
if ((row<rows-1) && !linked(row+1, col)) sides.push(BOTTOM);
if (sides.length<=1) {
// no need to visit this room again
todo.splice(todo.indexOf(index), 1);
}
if (!sides.length) {
// no neighbours need linking
continue;
}
todo.push(dig(index, pickRandom(sides)));
showProgress(done++, rooms.length);
}
}
/**
* We wouldn't want to generate a maze we can't solve ourselves...
*/
function findRoute() {
let dist = new Uint16Array(rooms.length), todo = [0];
dist.fill(-1);
dist[0] = 0;
while(true) {
const i = todo.shift(), d = dist[i], walls = rooms[i],
rc = rcFromI(i),
row = rc[0], col = rc[1];
if (i===rooms.length-1) { break; }
if (col>0 && !(walls&LEFT) && dist[i-1]>d+1) {
dist[i-1] = d+1;
todo.push(i-1);
}
if (row>0 && !(walls&TOP) && dist[i-cols]>d+1) {
dist[i-cols] = d+1;
todo.push(i-cols);
}
if (col<cols-1 && !(walls&RIGHT) && dist[i+1]>d+1) {
dist[i+1] = d+1;
todo.push(i+1);
}
if (row<rows-1 && !(walls&BOTTOM) && dist[i+cols]>d+1) {
dist[i+cols] = d+1;
todo.push(i+cols);
}
}
route = [rooms.length-1];
while(true) {
const i = route[0], d = dist[i], walls = rooms[i],
rc = rcFromI(i),
row = rc[0], col = rc[1];
if (i===0) { break; }
if (col<cols-1 && !(walls&RIGHT) && dist[i+1]<d) {
route.unshift(i+1);
continue;
}
if (row<rows-1 && !(walls&BOTTOM) && dist[i+cols]<d) {
route.unshift(i+cols);
continue;
}
if (row>0 && !(walls&TOP) && dist[i-cols]<d) {
route.unshift(i-cols);
continue;
}
if (col>0 && !(walls&LEFT) && dist[i-1]<d) {
route.unshift(i-1);
continue;
}
// this should never happen!
console.log("No route found!");
break;
}
}
/**
* Draw the maze:
* - room borders
* - maze border
* - exit
*/
function drawMaze() {
const range = {top: 0, left: 0, bottom: rows, right: cols};
const w = sW/cols, h = sH/rows;
g.clear();
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++) {
const walls = rooms[row*cols+col], x = col*w, y = row*h;
if (walls&BOTTOM) g.drawLine(x, y+h, x+w, y+h);
if (walls&RIGHT) g.drawLine(x+w, y, x+w, y+h);
}
}
// outline
g.setColor(0.29, 0.23, 0.17).drawRect(0, 0, sW-1, sH-1);
// target
g.setColor(0, 0.5, 0).fillCircle(sW-cW/2, sH-rH/2, r-1);
if (route) drawRoute();
}
/**
* Redraw a part of the maze (after we erased the ball image)
* @param range Draw rooms in this range {top,left,bottom,right}
*/
function redrawMaze(range) {
const w = sW/cols, h = sH/rows;
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++) {
const walls = rooms[row*cols+col], x = col*w, y = row*h;
if (row===range.top && walls&TOP) g.drawLine(x, y, x+w, y);
if (col===range.left && walls&LEFT) g.drawLine(x, y, x, y+h);
if (walls&BOTTOM) g.drawLine(x, y+h, x+w, y+h);
if (walls&RIGHT) g.drawLine(x+w, y, x+w, y+h);
}
}
g.setColor(0.29, 0.23, 0.17).drawRect(0, 0, sW-1, sH-1);
}
/**
* Draw the ball, with glare offset depending on ball position
*/
function drawBall() {
g.setColor(0.7, 0.7, 0.8).fillCircle(x, y, r-1);
const gx = -x/sW, gy = -y/sH;
g.setColor(0.8, 0.8, 0.9).fillCircle(x+gx*r/5, y+gy*r/5, r/2)
.setColor(0.85, 0.85, 0.95).fillCircle(x+gx*r/4, y+gy*r/4.5, r/2.5)
.setColor(0.9, 0.9, 1).fillCircle(x+gx*r/3, y+gy*r/3, r/3.5)
.setColor(1, 1, 1).fillCircle(x+gx*r/3, y+gy*r/3, r/6);
}
/**
* Update the screen:
* - erase previous ball image
* - redraw maze around the erased area
* - draw the ball
*/
function drawUpdate() {
g.clearRect(ppx-r, ppy-r, ppx+r, ppy+r);
const rc = rcFromXy(ppx, ppy),
row = rc[0], col = rc[1];
redrawMaze({top: row-1, left: col-1, bottom: row+1, right: col+1});
drawBall();
g.flip();
}
function drawRoute() {
let i = route[0], rc = rcFromI(i),
row = rc[0], col = rc[1],
x = (col+0.5)*cW, y = (row+0.5)*rH;
g.setColor(1, 0, 0).moveTo(x, y);
route.forEach(i => {
const rc = rcFromI(i),
row = rc[0], col = rc[1],
x = (col+0.5)*cW, y = (row+0.5)*rH;
g.lineTo(x, y);
});
}
/**
* Move the ball
*/
function move() {
const a = Bangle.getAccel();
const fx = (-a.x*w)-(sign(vx)*d*a.z), fy = (-a.y*w)-(sign(vy)*d*a.z);
vx += fx/m;
vy += fy/m;
const s = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy)));
for(let n = s; n>0; n--) {
x += vx/s;
y += vy/s;
bounce();
}
if (x>sW-cW && y>sH-rH) win();
}
/**
* Check whether we hit any walls, and if so: Bounce.
*
* Bounce = reverse velocity in bounce direction, multiply with elasticity
* Also apply drag in perpendicular direction ("friction with the wall")
*/
function bounce() {
const row = Math.floor(y/sH*rows), col = Math.floor(x/sW*cols),
i = row*cols+col, walls = rooms[i];
const left = col*cW,
right = (col+1)*cW,
top = row*rH,
bottom = (row+1)*rH;
let bounced = false;
if (vx<0) {
if ((walls&LEFT) && x<=left+r) {
x += (1+e)*(left+r-x);
const fy = sign(vy)*d*Math.abs(vx);
vy -= fy/m;
vx = -vx*e;
bounced = true;
}
} else {
if ((walls&RIGHT) && x>=right-r) {
x -= (1+e)*(x+r-right);
const fy = sign(vy)*d*Math.abs(vx);
vy -= fy/m;
vx = -vx*e;
bounced = true;
}
}
if (vy<0) {
if ((walls&TOP) && y<=top+r) {
y += (1+e)*(top+r-y);
const fx = sign(vx)*d*Math.abs(vy);
vx -= fx/m;
vy = -vy*e;
bounced = true;
}
} else {
if ((walls&BOTTOM) && y>=bottom-r) {
y -= (1+e)*(y+r-bottom);
const fx = sign(vx)*d*Math.abs(vy);
vx -= fx/m;
vy = -vy*e;
bounced = true;
}
}
if (bounced) return;
let cx, cy;
if ((rooms[i-1]&TOP) || rooms[i-cols]&LEFT) {
if ((x-left)*(x-left)+(y-top)*(y-top)<=r*r) {
cx = left;
cy = top;
}
}
else if ((rooms[i-1]&BOTTOM) || rooms[i+cols]&LEFT) {
if ((x-left)*(x-left)+(bottom-y)*(bottom-y)<=r*r) {
cx = left;
cy = bottom;
}
}
else if ((rooms[i+1]&TOP) || rooms[i-cols]&RIGHT) {
if ((right-x)*(right-x)+(y-top)*(y-top)<=r*r) {
cx = right;
cy = top;
}
}
else if ((rooms[i+1]&BOTTOM) || rooms[i+cols]&RIGHT) {
if ((right-x)*(right-x)+(bottom-y)*(bottom-y)<=r*r) {
cx = right;
cy = bottom;
}
}
if (!cx) return;
let nx = x-cx, ny = y-cy;
const l = Math.sqrt(nx*nx+ny*ny);
nx /= l;
ny /= l;
const p = vx*nx+vy*ny;
vx -= 2*p*nx*e;
vy -= 2*p*ny*e;
}
/**
* You reached the bottom-right corner, you win!
*/
function win() {
clearInterval(intervalID);
Bangle.buzz().then(askAgain);
}
/**
* You solved the maze, try the next one?
*/
function askAgain() {
const nextLevel = (size>minSize)?"next level":"again";
const nextSize = (size>minSize)?sizes[sizes.indexOf(size)+1]:size;
showPrompt(`Well done!\n\nPlay ${nextLevel}?`,
{"title": "Congratulations!"})
.then(function(again) {
if (again) {
playMaze(nextSize);
} else {
startGame();
}
});
}
function tick() {
ppx = px;
ppy = py;
px = x;
py = y;
move();
drawUpdate();
}
start();
}
/**
* Ask player what size maze they would like to play
*/
function startGame() {
let menu = {
"": {
title: "Select Maze Size",
selected: sizes.indexOf(settings.size || defaultSize),
},
};
sizes.filter(s => s>=minSize).forEach(size => {
let name = sizeNames[size];
if (size<minSize) name = "! "+size;
let cols = Math.round(sW/(size*2.5)),
rows = Math.round(sH/(size*2.5));
if (rows<10) rows = " "+rows;
if (cols<10) cols = " "+cols;
name += " ".repeat(14-name.length);
name += `${cols}x${rows}`;
menu[name] = () => {
// remember chosen size
settings.size = size;
require("Storage").write("ballmaze.json", settings);
playMaze(size);
};
});
menu["< Exit"] = () => load();
showMenu(menu);
}
startGame();
})();

1
apps/ballmaze/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA"))

BIN
apps/ballmaze/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
apps/ballmaze/maze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

1
apps/banglerun/ChangeLog Executable file
View File

@ -0,0 +1 @@
0.01: First release

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwMB/4ACx4ED/0DApP8AqAXB84GDg/DAgXj/+DCAUABgIFB4EAv4FCwEAj0PAoJPBgwFEgEfDgMOAoM/AoMegFAAoP8jkA8F/AoM8gP4DgP4nBvD/F4KQfwuAFE+A/CAoPgAofx8A/CKYRwELIIFDLII6BAoZSBLIYeC/0BwAFDgfAGAQFBHgf8g4BBIIUH/wFBSYMPAoXwAog/Bj4FEv4FDDQQCBQoQFCZYYFi/6KE/+P/4A="))

314
apps/banglerun/app.js Normal file
View File

@ -0,0 +1,314 @@
/** Global constants */
const DEG_TO_RAD = Math.PI / 180;
const EARTH_RADIUS = 6371008.8;
/** Utilities for handling vectors */
class Vector {
static magnitude(a) {
let sum = 0;
for (const key of Object.keys(a)) {
sum += a[key] * a[key];
}
return Math.sqrt(sum);
}
static add(a, b) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] + b[key];
}
return result;
}
static sub(a, b) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] - b[key];
}
return result;
}
static multiplyScalar(a, x) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] * x;
}
return result;
}
static divideScalar(a, x) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] / x;
}
return result;
}
}
/** Interquartile range filter, to detect outliers */
class IqrFilter {
constructor(size, threshold) {
const q = Math.floor(size / 4);
this._buffer = [];
this._size = 4 * q + 2;
this._i1 = q;
this._i3 = 3 * q + 1;
this._threshold = threshold;
}
isReady() {
return this._buffer.length === this._size;
}
isOutlier(point) {
let result = true;
if (this._buffer.length === this._size) {
result = false;
for (const key of Object.keys(point)) {
const data = this._buffer.map(item => item[key]);
data.sort((a, b) => (a - b) / Math.abs(a - b));
const q1 = data[this._i1];
const q3 = data[this._i3];
const iqr = q3 - q1;
const lower = q1 - this._threshold * iqr;
const upper = q3 + this._threshold * iqr;
if (point[key] < lower || point[key] > upper) {
result = true;
break;
}
}
}
this._buffer.push(point);
this._buffer = this._buffer.slice(-this._size);
return result;
}
}
/** Process GPS data */
class Gps {
constructor() {
this._lastCall = Date.now();
this._lastValid = 0;
this._coords = null;
this._filter = new IqrFilter(10, 1.5);
this._shift = { x: 0, y: 0, z: 0 };
}
isReady() {
return this._filter.isReady();
}
getDistance(gps) {
const time = Date.now();
const interval = (time - this._lastCall) / 1000;
this._lastCall = time;
if (!gps.fix) {
return { t: interval, d: 0 };
}
const p = gps.lat * DEG_TO_RAD;
const q = gps.lon * DEG_TO_RAD;
const coords = {
x: EARTH_RADIUS * Math.sin(p) * Math.cos(q),
y: EARTH_RADIUS * Math.sin(p) * Math.sin(q),
z: EARTH_RADIUS * Math.cos(p),
};
if (!this._coords) {
this._coords = coords;
this._lastValid = time;
return { t: interval, d: 0 };
}
const ds = Vector.sub(coords, this._coords);
const dt = (time - this._lastValid) / 1000;
const v = Vector.divideScalar(ds, dt);
if (this._filter.isOutlier(v)) {
return { t: interval, d: 0 };
}
this._shift = Vector.add(this._shift, ds);
const length = Vector.magnitude(this._shift);
const remainder = length % 10;
const distance = length - remainder;
this._coords = coords;
this._lastValid = time;
if (distance > 0) {
this._shift = Vector.multiplyScalar(this._shift, remainder / length);
}
return { t: interval, d: distance };
}
}
/** Process step counter data */
class Step {
constructor(size) {
this._buffer = [];
this._size = size;
}
getCadence() {
this._buffer.push(Date.now() / 1000);
this._buffer = this._buffer.slice(-this._size);
const interval = this._buffer[this._buffer.length - 1] - this._buffer[0];
return interval ? Math.round(60 * (this._buffer.length - 1) / interval) : 0;
}
}
const gps = new Gps();
const step = new Step(10);
let totDist = 0;
let totTime = 0;
let totSteps = 0;
let speed = 0;
let cadence = 0;
let heartRate = 0;
let gpsReady = false;
let hrmReady = false;
let running = false;
function formatClock(date) {
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
}
function formatDistance(m) {
return ('0' + (m / 1000).toFixed(2) + ' km').substr(-7);
}
function formatTime(s) {
const hrs = Math.floor(s / 3600);
const min = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
function formatSpeed(kmh) {
if (kmh <= 0.6) {
return `__'__"`;
}
const skm = 3600 / kmh;
const min = Math.floor(skm / 60);
const sec = Math.floor(skm % 60);
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
}
function drawBackground() {
g.setColor(running ? 0x00E0 : 0x0000);
g.fillRect(0, 30, 240, 240);
g.setColor(0xFFFF);
g.setFontAlign(0, -1, 0);
g.setFont('6x8', 2);
g.drawString('DISTANCE', 120, 50);
g.drawString('TIME', 60, 100);
g.drawString('PACE', 180, 100);
g.drawString('STEPS', 60, 150);
g.drawString('STP/m', 180, 150);
g.drawString('SPEED', 40, 200);
g.drawString('HEART', 120, 200);
g.drawString('CADENCE', 200, 200);
}
function draw() {
const totSpeed = totTime ? 3.6 * totDist / totTime : 0;
const totCadence = totTime ? Math.round(60 * totSteps / totTime) : 0;
g.setColor(running ? 0x00E0 : 0x0000);
g.fillRect(0, 30, 240, 50);
g.fillRect(0, 70, 240, 100);
g.fillRect(0, 120, 240, 150);
g.fillRect(0, 170, 240, 200);
g.fillRect(0, 220, 240, 240);
g.setFont('6x8', 2);
g.setFontAlign(-1, -1, 0);
g.setColor(gpsReady ? 0x07E0 : 0xF800);
g.drawString(' GPS', 6, 30);
g.setFontAlign(1, -1, 0);
g.setColor(0xFFFF);
g.drawString(formatClock(new Date()), 234, 30);
g.setFontAlign(0, -1, 0);
g.setFontVector(20);
g.drawString(formatDistance(totDist), 120, 70);
g.drawString(formatTime(totTime), 60, 120);
g.drawString(formatSpeed(totSpeed), 180, 120);
g.drawString(totSteps, 60, 170);
g.drawString(totCadence, 180, 170);
g.setFont('6x8', 2);
g.drawString(formatSpeed(speed), 40, 220);
g.setColor(hrmReady ? 0x07E0 : 0xF800);
g.drawString(heartRate, 120, 220);
g.setColor(0xFFFF);
g.drawString(cadence, 200, 220);
}
function handleGps(coords) {
const step = gps.getDistance(coords);
gpsReady = coords.fix > 0 && gps.isReady();
speed = isFinite(gps.speed) ? gps.speed : 0;
if (running) {
totDist += step.d;
totTime += step.t;
}
}
function handleHrm(hrm) {
hrmReady = hrm.confidence > 50;
heartRate = hrm.bpm;
}
function handleStep() {
cadence = step.getCadence();
if (running) {
totSteps += 1;
}
}
function start() {
running = true;
drawBackground();
draw();
}
function stop() {
if (!running) {
totDist = 0;
totTime = 0;
totSteps = 0;
}
running = false;
drawBackground();
draw();
}
Bangle.on('GPS', handleGps);
Bangle.on('HRM', handleHrm);
Bangle.on('step', handleStep);
Bangle.setGPSPower(1);
Bangle.setHRMPower(1);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawBackground();
draw();
setInterval(draw, 500);
setWatch(start, BTN1, { repeat: true });
setWatch(stop, BTN3, { repeat: true });

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -2,3 +2,4 @@
0.02: Apply locale, 12-hour setting
0.03: Fix dates drawing over each other at midnight
0.04: Small bugfix
0.05: Clock does not start if app Languages is not installed

View File

@ -12,7 +12,12 @@
date.setMonth(1, 3) // februari: months are zero-indexed
const localized = locale.date(date, true)
locale.dayFirst = /3.*2/.test(localized)
locale.hasMeridian = (locale.meridian(date) !== '')
locale.hasMeridian = false
if(typeof locale.meridian === 'function') { // function does not exists if languages app is not installed
locale.hasMeridian = (locale.meridian(date) !== '')
}
}
const screen = {
width: g.getWidth(),

View File

@ -1,3 +1,9 @@
0.01: New app and widget
0.02: Widget stores data to file (1 dataset/10min)
0.03: Rotate log files once a week.
0.04: chart in the app is now active.
0.05: Display temperature and LCD state in chart
0.06: Fixes widget events and charting of component states
0.07: Improve logging and charting of component states and add widget icon
0.08: Fix for Home button in the app and README added.
0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state

67
apps/batchart/README.md Normal file
View File

@ -0,0 +1,67 @@
# Summary
Battery Chart contains a widget that records the battery usage as well as information that might influence this usage.
The app that comes with provides a graph that accumulates this information in a single screen.
## How the widget works
The widget records data in a fixed interval of ten minutes.
When this timespan has passed, it saves the following information to a file called `bclogx` where `x` is
the current day retrieved by `new Date().getDay()`:
- Battery percentage
- Temperature (of the die)
- LCD state
- Compass state
- HRM state
- GPS state
After seven days the logging rolls over and the previous data is overwritten.
To properly handle the roll-over, the day of the previous logging operation is stored in `bcprvday`.
The value is changed with the first recording operation of the new day.
## How the App works
### Events
The app charts the last 144 (6/h * 24h) datapoints that have been recorded.
If for the current day the 144 events have not been reached the list is padded with
events from the previous `bclog` file(s).
### Graph
The graph then contains the battery percentage (left y-axis) and the temperature (right y-axis).
In case the recorded temperature is outside the limits of the graph, the value is set to a minimum of 19 or a maximum of 41 and thus should be clearly visible outside of the graph's boundaries for the y-axis.
The states of the various SoC devices are depicted below the graph. If at the time of recording the device was enabled a marker in the respective color is set, if not the pixels for this point in time stay black.
If a device was not enabled during the 144 selected events, the name is not displayed.
## File schema
You can download the `bclog` files for your own analysis. They are `CSV` files without header rows and contain
```
timestamp,batteryPercentage,temperatureInDegreeC,deviceStates
```
with the `deviceStates` resembling a flag set consisting of
```
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
bluetooth: 4,
gps: 8,
hrm: 16
};
```

View File

@ -1,34 +1,246 @@
// place your const, vars, functions or classes here
const GraphXZero = 40;
const GraphYZero = 180;
const GraphY100 = 80;
function renderBatteryChart(){
g.drawString("t", 215, 175);
g.drawLine(40,190,40,80);
const GraphMarkerOffset = 5;
const MaxValueCount = 144;
const GraphXMax = GraphXZero + MaxValueCount;
const GraphLcdY = GraphYZero + 10;
const GraphCompassY = GraphYZero + 16;
const GraphBluetoothY = GraphYZero + 22;
const GraphGpsY = GraphYZero + 28;
const GraphHrmY = GraphYZero + 34;
const Storage = require("Storage");
function renderCoordinateSystem() {
g.setFont("6x8", 1);
g.drawString("%", 39, 70);
g.drawString("100", 15, 75);
g.drawLine(35,80,40,80);
// Left Y axis (Battery)
g.setColor(1, 1, 0);
g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100);
g.drawString("%", 39, GraphY100 - 10);
g.drawString("50", 20,125);
g.drawLine(35,130,40,130);
g.setFontAlign(1, -1, 0);
g.drawString("100", 30, GraphY100 - GraphMarkerOffset);
g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100);
g.drawString("0", 25, 175);
g.drawLine(35,180,210,180);
g.drawString("50", 30, GraphYZero - 50 - GraphMarkerOffset);
g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130);
g.drawString("Chart not yet functional", 60, 125);
g.drawString("0", 30, GraphYZero - GraphMarkerOffset);
g.setColor(1,1,1);
g.setFontAlign(1, -1, 0);
g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero);
// Right Y axis (Temperature)
g.setColor(0.4, 0.4, 1);
g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100);
g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10);
g.setFontAlign(-1, -1, 0);
g.drawString("20", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - GraphMarkerOffset);
g.drawLine(GraphXMax + GraphMarkerOffset, 130, GraphXMax, 130);
g.drawString("30", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - 50 - GraphMarkerOffset);
g.drawLine(GraphXMax + GraphMarkerOffset, 80, GraphXMax, 80);
g.drawString("40", GraphXMax + 2 * GraphMarkerOffset, GraphY100 - GraphMarkerOffset);
g.setColor(1,1,1);
}
function decrementDay(dayToDecrement) {
return dayToDecrement === 0 ? 6 : dayToDecrement-1;
}
function loadData() {
const startingDay = new Date().getDay();
// Load data for the current day
let logFileName = "bclog" + startingDay;
let dataLines = loadLinesFromFile(MaxValueCount, logFileName);
// Top up to MaxValueCount from previous days as required
let previousDay = decrementDay(startingDay);
while (dataLines.length < MaxValueCount && previousDay !== startingDay) {
let topUpLogFileName = "bclog" + previousDay;
let remainingLines = MaxValueCount - dataLines.length;
let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName);
if(topUpLines) {
dataLines = topUpLines.concat(dataLines);
}
previousDay = decrementDay(previousDay);
}
return dataLines;
}
function loadLinesFromFile(requestedLineCount, fileName) {
let allLines = [];
let returnLines = [];
var readFile = Storage.open(fileName, "r");
while ((nextLine = readFile.readLine())) {
if(nextLine) {
allLines.push(nextLine);
}
}
readFile = null;
if (allLines.length <= 0) return;
let linesToReadCount = Math.min(requestedLineCount, allLines.length);
let startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1);
for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) {
if(allLines[i]) {
returnLines.push(allLines[i]);
}
}
allLines = null;
return returnLines;
}
function renderData(dataArray) {
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
bluetooth: 4,
gps: 8,
hrm: 16
};
//const timestampIndex = 0;
const batteryIndex = 1;
const temperatureIndex = 2;
const switchabelsIndex = 3;
const minTemperature = 20;
const maxTemparature = 40;
const belowMinIndicatorValue = minTemperature - 1;
const aboveMaxIndicatorValue = maxTemparature + 1;
var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm;
for (let i = 0; i < dataArray.length; i++) {
const element = dataArray[i];
var dataInfo = element.split(",");
// Battery percentage
g.setColor(1, 1, 0);
g.setPixel(GraphXZero + i, GraphYZero - parseInt(dataInfo[batteryIndex]));
// Temperature
g.setColor(0.4, 0.4, 1);
let datapointTemp = parseFloat(dataInfo[temperatureIndex]);
if (datapointTemp < minTemperature) {
datapointTemp = belowMinIndicatorValue;
}
if (datapointTemp > maxTemparature) {
datapointTemp = aboveMaxIndicatorValue;
}
// Scale down the range of 20 - 40°C to a 100px y-axis, where 1px = .25°
let scaledTemp = Math.floor(((datapointTemp * 100) - 2000) / 20) + ((((datapointTemp * 100) - 2000) % 100) / 25);
g.setPixel(GraphXZero + i, GraphYZero - scaledTemp);
// LCD state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd) {
g.setColor(1, 1, 1);
g.setFontAlign(1, -1, 0);
g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true);
g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1);
}
// Compass state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.compass) {
g.setColor(0, 1, 0);
g.setFontAlign(-1, -1, 0);
g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true);
g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1);
}
// Bluetooth state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.bluetooth) {
g.setColor(0, 0, 1);
g.setFontAlign(1, -1, 0);
g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true);
g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1);
}
// Gps state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) {
g.setColor(0.8, 0.5, 0.24);
g.setFontAlign(-1, -1, 0);
g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true);
g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1);
}
// Hrm state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.hrm) {
g.setColor(1, 0, 0);
g.setFontAlign(1, -1, 0);
g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true);
g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1);
}
}
dataArray = null;
}
function renderHomeIcon() {
//Home for Btn2
g.setColor(1, 1, 1);
g.drawLine(220, 118, 227, 110);
g.drawLine(227, 110, 234, 118);
g.drawPoly([222,117,222,125,232,125,232,117], false);
g.drawRect(226,120,229,125);
}
function renderBatteryChart() {
renderCoordinateSystem();
let data = loadData();
renderData(data);
data = null;
}
// Show launcher when middle button pressed
function switchOffApp(){
Bangle.showLauncher();
}
// special function to handle display switch on
Bangle.on('lcdPower', (on) => {
if (on) {
// call your app function here
// If you clear the screen, do Bangle.drawWidgets();
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
renderBatteryChart();
}
});
setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
// call your app function here
renderHomeIcon();
renderBatteryChart();

View File

@ -1,5 +1,7 @@
(() => {
var switchableConsumers = {
const Storage = require("Storage");
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
@ -8,72 +10,117 @@
hrm: 16
};
var settings = {};
var batChartFile; // file for battery percentage recording
const recordingInterval10Min = 60 * 10 * 1000;
const recordingInterval1Min = 60*1000; //For testing
const recordingInterval10S = 10*1000; //For testing
const recordingInterval1Min = 60 * 1000; //For testing
const recordingInterval10S = 10 * 1000; //For testing
var recordingInterval = null;
var compassEventReceived = false;
var gpsEventReceived = false;
var hrmEventReceived = false;
// draw your widget
function draw() {
let x = this.x;
let y = this.y;
g.setColor(0, 1, 0);
g.fillPoly([x + 5, y, x + 5, y + 4, x + 1, y + 4, x + 1, y + 20, x + 18, y + 20, x + 18, y + 4, x + 13, y + 4, x + 13, y], true);
g.setColor(0, 0, 0);
g.drawPoly([x + 5, y + 6, x + 8, y + 12, x + 13, y + 12, x + 16, y + 18], false);
g.reset();
g.drawString("BC", this.x, this.y);
}
function onMag() {
compassEventReceived = true;
// Stop handling events when no longer necessarry
Bangle.removeListener("mag", onMag);
}
function onGps() {
gpsEventReceived = true;
Bangle.removeListener("GPS", onGps);
}
function onHrm() {
hrmEventReceived = true;
Bangle.removeListener("HRM", onHrm);
}
function getEnabledConsumersValue() {
// Wait for an event from each of the devices to see if they are switched on
var enabledConsumers = switchableConsumers.none;
Bangle.on('mag', onMag);
Bangle.on('GPS', onGps);
Bangle.on('HRM', onHrm);
// Wait two seconds, that should be enough for each of the events to get raised once
setTimeout(() => {
Bangle.removeAllListeners();
}, 2000);
if (Bangle.isLCDOn())
enabledConsumers = enabledConsumers | switchableConsumers.lcd;
// Already added in the hope they will be available soon to get more details
// if (Bangle.isCompassOn())
// enabledConsumers = enabledConsumers | switchableConsumers.compass;
// if (Bangle.isBluetoothOn())
// enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
// if (Bangle.isGpsOn())
// enabledConsumers = enabledConsumers | switchableConsumers.gps;
// if (Bangle.isHrmOn())
// enabledConsumers = enabledConsumers | switchableConsumers.hrm;
if (compassEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.compass;
if (gpsEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.gps;
if (hrmEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.hrm;
return enabledConsumers;
// Very coarse first approach to check if the BLE device is on.
if (NRF.getSecurityStatus().connected)
enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
// Reset the event registration vars
compassEventReceived = false;
gpsEventReceived = false;
hrmEventReceived = false;
return enabledConsumers.toString();
}
function logBatteryData() {
const previousWriteLogName = "bcprvday";
const previousWriteDay = require("Storage").read(previousWriteLogName);
const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine());
const currentWriteDay = new Date().getDay();
const logFileName = "bclog" + currentWriteDay;
// Change log target on day change
if (previousWriteDay != currentWriteDay) {
if (!isNaN(previousWriteDay) && previousWriteDay != currentWriteDay) {
//Remove a log file containing data from a week ago
require("Storage").erase(logFileName);
require("Storage").write(previousWriteLogName, currentWriteDay);
Storage.open(logFileName, "r").erase();
Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay));
}
var bcLogFileA = require("Storage").open(logFileName, "a");
var bcLogFileA = Storage.open(logFileName, "a");
if (bcLogFileA) {
console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(","));
bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n");
let logTime = getTime().toFixed(0);
let logPercent = E.getBattery();
let logTemperature = E.getTemperature();
let logConsumers = getEnabledConsumersValue();
let logString = [logTime, logPercent, logTemperature, logConsumers].join(",");
bcLogFileA.write(logString + "\n");
}
}
// Called by the heart app to reload settings and decide what's
function reload() {
WIDGETS["batchart"].width = 24;
recordingInterval = setInterval(logBatteryData, recordingInterval10Min);
logBatteryData();
}
// add the widget
WIDGETS["batchart"]={area:"tl",width:24,draw:draw,reload:function() {
reload();
Bangle.drawWidgets(); // relayout all widgets
}};
// load settings, set correct widget width
WIDGETS["batchart"] = {
area: "tl", width: 24, draw: draw, reload: reload
};
reload();
})()
})();

1
apps/blackjack/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New game! BTN4- Hit card, BTN5- Stand

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIQNgfAAgU///wAgMH/4dBAoMMAQMQAQMIAQMYAQ4RCApcPwAFDgIwBAoQ4BAoMP8EHwfghk//AXBuEMv38n+AjEMvl8/4FDvoFBmEMvF994FBg04vgdBAoMAAot4AoNgAoPwAoZFBAongAoPggyIBAoPAg0HwAFDh4BBAoUeh0PwOAg08AocDv/+Ao3DAod//a3BAorBDAohRBgf+AocBAokApgCBhzSCWIkHVYgYCWIngYwQrB/gFDgF//AFDD4QAD8AFEAAIA="))

View File

@ -0,0 +1,191 @@
const Clubs = { width : 48, height : 48, bpp : 1,
buffer : require("heatshrink").decompress(atob("ACcP+AFDn/8Aod//wFD///AgUBAoOAApsDAoPAAr4vLI4pTEgP8L4M/wEH/5rB//gh//x/x//wj//9/3//4n4iBAAIZBAol/Aof+Apv5z4FP+OPAo41BAoX8I4Pj45HBAoPD4YFBLIOD4JZBRAMD4CKC/AFBj59Cg/gQYYFXAB4="))
};
const Spades = { width : 48, height : 48, bpp : 1,
buffer : require("heatshrink").decompress(atob("ABsBwAFDgfAAocH8AFDh/wAocf/AFDn/8Aod//wFD///FwYFBGAUDAoIwCg4FBGAUPAoIwCj4FBGAU/AoIwCv4FBGAQEBGAQuCGAQuCGAQFLHQQ8CAupHLL4prB+fPTgU/8fHVwbLLApbXFbpYFLdIoADA=="))
};
const Hearts = { width : 48, height : 48, bpp : 4,
buffer : require("heatshrink").decompress(atob("ADlVqtQBQ8FBYIKIrnMAAINGqoKC4okGCwYAB4AKDhgKE4oWKAAILDBQwYEBYwwDFwojFgoLHEgQ6H5hhCBZAkCBRAjLEgI6IC4YLIC5Y7BBZXBjgjVABYX/C8CnKABbXLABTvMC8sMC6fAC4KQURwIABRypgULwRgULwRIUCwhIRIwiRSRoZITCwx5POoowRCxAwNFxIwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQwDFycAgoXBqAXTgFc4oWUJAJGUJARGVAEo"))
};
const Diamonds = { width : 48, height : 48, bpp : 4,
buffer : require("heatshrink").decompress(atob("AHUFC60M4AXV5nFIyvM5hGVC4JIUCwJIUIwRIUIwRIUCwZISIwgABqBGUJCQWFPKBGGJCFcC455OCw4wOOox5QIxB5NOpBIOFxZ5LCxYwKOpQwMIxh5KOxipLL6xgNR5QwMX5TvXPJZ1JJBpGLPJR1LJBZGNPJIWOJA5GOPJB1NJBIWQPIpGRJApGRPIoWSJAa8PJA5GTJAYWUJAJGVAAJGVAHo="))
};
var deck = [];
var player = {Hand:[]};
var computer = {Hand:[]};
function createDeck() {
var suits = ["Spades", "Hearts", "Diamonds", "Clubs"];
var values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];
var dck = [];
for (var i = 0 ; i < values.length; i++) {
for(var x = 0; x < suits.length; x++) {
dck.push({ Value: values[i], Suit: suits[x] });
}
}
return dck;
}
function shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
function EndGameMessdage(msg){
g.drawString(msg, 155, 200);
setTimeout(function(){
startGame();
}, 2500);
}
function hitMe() {
player.Hand.push(deck.pop());
renderOnScreen(1);
var playerWeight = calcWeight(player.Hand, 0);
if(playerWeight == 21)
EndGameMessdage('WINNER');
else if(playerWeight > 21)
EndGameMessdage('LOOSER');
}
function calcWeight(hand, hideCard) {
if(hideCard === 1) {
if (hand[0].Value == "J" || hand[0].Value == "Q" || hand[0].Value == "K")
return "10 +";
else if (hand[0].Value == "A")
return "11 +";
else
return parseInt(hand[0].Value) +" +";
}
else {
var weight = 0;
for(i=0; i<hand.length; i++){
if (hand[i].Value == "J" || hand[i].Value == "Q" || hand[i].Value == "K") {
weight += 10;
}
else if (hand[i].Value == "A") {
weight += 1;
}
else
weight += parseInt(hand[i].Value);
}
// Find count of aces because it may be 11 or 1
var numOfAces = hand.filter(function(x){ return x.Value === "A"; }).length;
for (var j = 0; j < numOfAces; j++) {
if (weight + 10 <= 21) {
weight +=10;
}
}
return weight;
}
}
function stand(){
function sleepFor( sleepDuration ){
console.log("Sleeping...");
var now = new Date().getTime();
while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
}
renderOnScreen(0);
var playerWeight = calcWeight(player.Hand, 0);
var bangleWeight = calcWeight(computer.Hand, 0);
while(bangleWeight<17){
sleepFor(500);
computer.Hand.push(deck.pop());
renderOnScreen(0);
bangleWeight = calcWeight(computer.Hand, 0);
}
if (bangleWeight == playerWeight)
EndGameMessdage('TIES');
else if(playerWeight==21 || bangleWeight > 21 || bangleWeight < playerWeight)
EndGameMessdage('WINNER');
else if(bangleWeight > playerWeight)
EndGameMessdage('LOOSER');
}
function renderOnScreen(HideCard) {
const fontName = "6x8";
g.clear(); // clear screen
g.reset(); // default draw styles
g.setFont(fontName, 1);
g.drawString('RST', 220, 35);
g.drawString('Hit', 60, 230);
g.drawString('Stand', 165, 230);
g.setFont(fontName, 3);
for(i=0; i<computer.Hand.length; i++){
g.drawImage(eval(computer.Hand[i].Suit), i*48, 10);
if(i == 1 && HideCard == 1)
g.drawString("?", i*48+18, 58);
else
g.drawString(computer.Hand[i].Value, i*48+18, 58);
}
g.setFont(fontName, 2);
g.drawString('BangleJS has '+ calcWeight(computer.Hand, HideCard), 5, 85);
g.setFont(fontName, 3);
for(i=0; i<player.Hand.length; i++){
g.drawImage(eval(player.Hand[i].Suit), i*48, 125);
g.drawString(player.Hand[i].Value, i*48+18, 175);
}
g.setFont(fontName, 2);
g.drawString('You have ' + calcWeight(player.Hand, 0), 5, 202);
}
function dealHands() {
player.Hand= [];
computer.Hand= [];
setTimeout(function(){
player.Hand.push(deck.pop());
renderOnScreen(0);
}, 500);
setTimeout(function(){
computer.Hand.push(deck.pop());
renderOnScreen(1);
}, 1000);
setTimeout(function(){
player.Hand.push(deck.pop());
renderOnScreen(1);
}, 1500);
setTimeout(function(){
computer.Hand.push(deck.pop());
renderOnScreen(1);
}, 2000);
}
function startGame(){
deck = createDeck();
deck = shuffle(deck);
dealHands();
}
setWatch(hitMe, BTN4, {repeat:true, edge:"falling"});
setWatch(stand, BTN5, {repeat:true, edge:"falling"});
setWatch(startGame, BTN1, {repeat:true, edge:"falling"});
startGame();

Binary file not shown.

View File

@ -0,0 +1 @@
{"id":"blackjack","name":"Black Jack","src":"blackjack.app.js","icon":"blackjack.img","version":"0.1","files":"blackjack.info,blackjack.app.js,blackjack.img"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

2
apps/bledetect/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Fixed issue with wrong device informations

14
apps/bledetect/README.md Normal file
View File

@ -0,0 +1,14 @@
# BLE Detector
BLE Detector it's an app born for testing purpose that aim to show as informations as possible about near BLE devices.
## Features
BLE Detector shows:
- Device name (if available)
- Received Signal Strength Indication (RSSI)
- Manufacturer
- MAC Address
More informations will coming with future versions.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgJGhGAEKuIxAXXGCoXBGCoXCDCgXDJKYXDGCYUBhAwUFgQwPEogTCGBwNFFYYYNHwoEGJJQlFCIgKCdR4XHJBQNEI6IOFO6IPEDQYGDahoYEa6BJFxBFPJJIuQGAouRGAoWSGAgXTSIoAEgUgL6cCkQACDJCOFGAYWDAAJFLX4gWFGA4sFC40gJQYuHwBEDAQISCMYowEFgoJDCAwYBAwZYEC45AEgIHERAgXMA4i4FC6bPDC4hXFC5B7FC57CHI54XIawgXRVwS/JC5SuDC4wGGC45HBFAQRCAooXIVwYRBAAoXLLIwAFC5IuDGCIuFDAyQLABphKABgwaC6owB"))

View File

@ -0,0 +1,59 @@
let menu = {
"": { "title": "BLE Detector" },
"RE-SCAN": () => scan()
};
function showMainMenu() {
menu["< Back"] = () => load();
return E.showMenu(menu);
}
function showDeviceInfo(device){
const deviceMenu = {
"": { "title": "Device Info" },
"name": {
value: device.name
},
"rssi": {
value: device.rssi
},
"manufacturer": {
value: device.manufacturer
}
};
deviceMenu[device.id] = () => {};
deviceMenu["< Back"] = () => showMainMenu();
return E.showMenu(deviceMenu);
}
function scan() {
menu = {
"": { "title": "BLE Detector" },
"RE-SCAN": () => scan()
};
waitMessage();
NRF.findDevices(devices => {
devices.forEach(device =>{
let deviceName = device.id.substring(0,17);
if (device.name) {
deviceName = device.name;
}
menu[deviceName] = () => showDeviceInfo(device);
});
showMainMenu(menu);
}, { active: true });
}
function waitMessage() {
E.showMenu();
E.showMessage("scanning");
}
scan();
waitMessage();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -13,3 +13,4 @@
0.13: Now automatically load *.boot.js at startup
Move alarm code into alarm.boot.js
0.14: Move welcome loaders to *.boot.js
0.15: Added BLE HID option for Joystick and bare Keyboard

View File

@ -4,7 +4,9 @@ E.setFlags({pretokenise:1});
var s = require('Storage').readJSON('setting.json',1)||{};
if (s.ble!==false) {
if (s.HID) { // Human interface device
Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));
else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));
else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
NRF.setServices({}, {uart:true, hid:Bangle.HID});
}
}

88
apps/boot/hid_info.txt Normal file
View File

@ -0,0 +1,88 @@
## Joystick:
https://github.com/espruino/BangleApps/issues/349#issuecomment-620231524
```
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x04, // Usage (Joystick)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
// Buttons
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Variable, Absolute)
// padding bits
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x03, // Input (Constant)
// Stick
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7f, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data, Variable, Absolute)
0xC0, // End Collection (Physical)
0xC0 // End Collection (Application)
```
## Keyboard
http://www.espruino.com/BLE+Keyboard
```
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0xe0, // Usage Minimum (224)
0x29, 0xe7, // Usage Maximum (231)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant) reserved byte(1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x05, 0x08, // Usage Page (Page# for LEDs)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x91, 0x02, // Output (Data, Variable, Absolute), Led report
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x01, // Output (Data, Variable, Absolute), Led report padding
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x73, // Logical Maximum (115 - include F13, etc)
0x05, 0x07, // Usage Page (Key codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x73, // Usage Maximum (115 - include F13, etc)
0x81, 0x00, // Input (Data, Array) Key array(6 bytes)
0x09, 0x05, // Usage (Vendor Defined)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Count (2)
0x95, 0x02, // Report Size (8 bit)
0xB1, 0x02, // Feature (Data, Variable, Absolute)
0xC0 // End Collection (Application)
```

View File

@ -0,0 +1,33 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
}

2
apps/buffgym/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Create BuffGym app
0.02: Add web interface for personalising workout

60
apps/buffgym/README.md Normal file
View File

@ -0,0 +1,60 @@
# BuffGym
This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program.
## Configuration
### Setting your start weight values
You will want to set your own starting weight values for your 5x5 training program. To do this is easy! After installing this app, go to the BangleJS app store, connect to your watch, and navigate to the `My Apps` tab. In there you will find this app in the list, and an icon (a down arrow) to the right of the app title. Click that icon to reveal a configuration page. Enter your weights and other details, and click upload. That is it, you are now ready to train!
## Usage
### Start screen
When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start
![](buffgym-scrn1.png)
### Workouts menu
You are then presented with the workouts menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the workout.
![](buffgym-scrn2.png)
### Recording your training
You will now begin moving through the exercises in the workout. You will see the exercise information on the display.
![](buffgym-scrn3.png)
1. At the top is the exercise name, e.g 'Squats'
2. Next is the weight you must train
3. In the center is where you record the number of *reps* you completed (more on that shortly)
4. Below the *reps* value, is the target reps you must try to reach.
5. Below the target reps is the current set you are training, out of the total sets for the exercise.
6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps
### Rest timers
You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise.
![](buffgym-scrn4.png)
![](buffgym-scrn5.png)
### Workout completed
Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are.
![](buffgym-scrn6.png)
## Features
* If you successfully complete all reps and sets for an exercise, it will automatically update your weights for next time
* Has a neat rest timer to make sure you are training optimally
* Doesn't require a mobile phone, most 'smart watches' are just a visual presentation of the mobile phone app, this runs purley on the watch. So why not leave your phone and its distractions out of the gym!
* Clear and simple user interface
## Created by
[Paul Cockrell](https://github.com/paulcockrell) April 2020.

View File

@ -0,0 +1,153 @@
exports = class Exercise {
constructor(params) {
this.completed = false;
this.sets = [];
this.title = params.title;
this.weight = params.weight;
this.weightIncrement = params.weightIncrement;
this.unit = params.unit;
this.restPeriod = params.restPeriod;
this._originalRestPeriod = params.restPeriod;
this._restTimeout = null;
this._restInterval = null;
this._state = null;
}
get humanTitle() {
return `${this.title} ${this.weight}${this.unit}`;
}
get subTitle() {
const totalSets = this.sets.length;
const uncompletedSets = this.sets.filter((set) => !set.isCompleted()).length;
const currentSet = (totalSets - uncompletedSets) + 1;
return `Set ${currentSet} of ${totalSets}`;
}
decRestPeriod() {
this.restPeriod--;
}
addSet(set) {
this.sets.push(set);
}
currentSet() {
return this.sets.filter(set => !set.isCompleted())[0];
}
isLastSet() {
return this.sets.filter(set => !set.isCompleted()).length === 1;
}
isCompleted() {
return !!this.completed;
}
canSetCompleted() {
return this.sets.filter(set => set.isCompleted()).length === this.sets.length;
}
setCompleted() {
if (!this.canSetCompleted()) throw "All sets must be completed";
if (this.canProgress()) this.weight += this.weightIncrement;
this.completed = true;
}
canProgress() {
let completedRepsTotalSum = 0;
let targetRepsTotalSum = 0;
this.sets.forEach(set => completedRepsTotalSum += set.reps);
this.sets.forEach(set => targetRepsTotalSum += set.maxReps);
return (targetRepsTotalSum - completedRepsTotalSum) === 0;
}
startRestTimer(workout) {
this._restTimeout = setTimeout(() => {
this.next(workout);
}, 1000 * this.restPeriod);
this._restInterval = setInterval(() => {
this.decRestPeriod();
if (this.restPeriod < 0) {
this.resetRestTimer();
this.next();
return;
}
workout.emit("redraw");
}, 1000 );
}
resetRestTimer() {
clearTimeout(this._restTimeout);
clearInterval(this._restInterval);
this._restTimeout = null;
this._restInterval = null;
this.restPeriod = this._originalRestPeriod;
}
isRestTimerRunning() {
return this._restTimeout != null;
}
setupStartedButtons(workout) {
clearWatch();
setWatch(() => {
this.currentSet().incReps();
workout.emit("redraw");
}, BTN1, {repeat: true});
setWatch(workout.next.bind(workout), BTN2, {repeat: false});
setWatch(() => {
this.currentSet().decReps();
workout.emit("redraw");
}, BTN3, {repeat: true});
}
setupRestingButtons(workout) {
clearWatch();
setWatch(workout.next.bind(workout), BTN2, {repeat: false});
}
next(workout) {
const STARTED = 1;
const RESTING = 2;
const COMPLETED = 3;
switch(this._state) {
case null:
this._state = STARTED;
this.setupStartedButtons(workout);
break;
case STARTED:
this._state = RESTING;
this.startRestTimer(workout);
this.setupRestingButtons(workout);
break;
case RESTING:
this.resetRestTimer();
this.currentSet().setCompleted();
if (this.canSetCompleted()) {
this._state = COMPLETED;
this.setCompleted();
} else {
this._state = null;
}
// As we are changing state and require it to be reprocessed
// invoke the next step of workout
workout.next();
break;
default:
throw "Exercise: Attempting to move to an unknown state";
}
workout.emit("redraw");
}
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA="))

BIN
apps/buffgym/buffgym-scrn1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
apps/buffgym/buffgym-scrn2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/buffgym/buffgym-scrn3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
apps/buffgym/buffgym-scrn4.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
apps/buffgym/buffgym-scrn5.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
apps/buffgym/buffgym-scrn6.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,28 @@
exports = class Set {
constructor(maxReps) {
this.completed = false;
this.minReps = 0;
this.reps = 0;
this.maxReps = maxReps;
}
isCompleted() {
return !!this.completed;
}
setCompleted() {
this.completed = true;
}
incReps() {
if (this.completed) return;
if (this.reps >= this.maxReps) return;
this.reps++;
}
decReps() {
if (this.completed) return;
if (this.reps <= this.minReps) return;
this.reps--;
}
}

View File

@ -0,0 +1,33 @@
{
"title": "Workout A",
"exercises": [
{
"title": "Squats",
"weight": 40,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Overhead press",
"weight": 20,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Deadlift",
"weight": 20,
"unit": "Kg",
"sets": [5],
"restPeriod": 90
},
{
"title": "Pullups",
"weight": 0,
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
}

View File

@ -0,0 +1,33 @@
{
"title": "Workout B",
"exercises": [
{
"title": "Squats",
"weight": 40,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Bench press",
"weight": 20,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Row",
"weight": 20,
"unit":"Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Tricep extension",
"weight": 20,
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
}

View File

@ -0,0 +1,10 @@
[
{
"title": "Workout A",
"file": "buffgym-workout-a.json"
},
{
"title": "Workout B",
"file": "buffgym-workout-b.json"
}
]

View File

@ -0,0 +1,83 @@
exports = class Workout {
constructor(params) {
this.title = params.title;
this.exercises = [];
this.completed = false;
this.on("redraw", redraw.bind(null, this));
}
addExercises(exercises) {
exercises.forEach(exercise => this.exercises.push(exercise));
}
currentExercise() {
return this.exercises.filter(exercise => !exercise.isCompleted())[0];
}
canComplete() {
return this.exercises.filter(exercise => exercise.isCompleted()).length === this.exercises.length;
}
setCompleted() {
if (!this.canComplete()) throw "All exercises must be completed";
this.completed = true;
}
isCompleted() {
return !!this.completed;
}
static fromJSON(workoutJSON) {
const Set = require("buffgym-set.js");
const Exercise = require("buffgym-exercise.js");
const workout = new this({
title: workoutJSON.title,
});
const exercises = workoutJSON.exercises.map(exerciseJSON => {
const exercise = new Exercise({
title: exerciseJSON.title,
weight: exerciseJSON.weight,
weightIncrement: exerciseJSON.weightIncrement,
unit: exerciseJSON.unit,
restPeriod: exerciseJSON.restPeriod,
});
exerciseJSON.sets.forEach(setJSON => {
exercise.addSet(new Set(setJSON));
});
return exercise;
});
workout.addExercises(exercises);
return workout;
}
toJSON() {
return {
title: this.title,
exercises: this.exercises.map(exercise => {
return {
title: exercise.title,
weight: exercise.weight,
weightIncrement: exercise.weightIncrement,
unit: exercise.unit,
sets: exercise.sets.map(set => set.maxReps),
restPeriod: exercise.restPeriod,
};
}),
};
}
// State machine
next() {
if (this.canComplete()) {
this.setCompleted();
this.emit("redraw");
return;
}
// Call current exercise state machine
this.currentExercise().next(this);
}
}

261
apps/buffgym/buffgym.app.js Executable file
View File

@ -0,0 +1,261 @@
/**
* BangleJS Stronglifts 5x5 training aid
*
* Original Author: Paul Cockrell https://github.com/paulcockrell
* Created: April 2020
*
* Inspired by:
* - Stronglifts 5x5 training workout https://stronglifts.com/5x5/
* - Stronglifts smart watch app
*/
Bangle.setLCDMode("120x120");
const W = g.getWidth();
const H = g.getHeight();
const RED = "#d32e29";
const PINK = "#f05a56";
const WHITE = "#ffffff";
function drawMenu(params) {
const hs = require("heatshrink");
const incImg = hs.decompress(atob("gsFwMAkM+oUA"));
const decImg = hs.decompress(atob("gsFwIEBnwCBA"));
const okImg = hs.decompress(atob("gsFwMAhGFo0A"));
const DEFAULT_PARAMS = {
showBTN1: false,
showBTN2: false,
showBTN3: false,
};
const p = Object.assign({}, DEFAULT_PARAMS, params);
if (p.showBTN1) g.drawImage(incImg, W - 10, 10);
if (p.showBTN2) g.drawImage(okImg, W - 10, 60);
if (p.showBTN3) g.drawImage(decImg, W - 10, 110);
}
function drawSet(exercise) {
const set = exercise.currentSet();
if (set.isCompleted()) return;
g.clear();
// Draw exercise title
g.setColor(PINK);
g.fillRect(15, 0, W - 15, 18);
g.setFontAlign(0, -1);
g.setFont("6x8", 1);
g.setColor(WHITE);
g.drawString(exercise.title, W / 2, 5);
g.setFont("6x8", 1);
g.drawString(exercise.weight + " " + exercise.unit, W / 2, 27);
// Draw completed reps counter
g.setFontAlign(0, 0);
g.setColor(PINK);
g.fillRect(15, 42, W - 15, 80);
g.setColor(WHITE);
g.setFont("6x8", 5);
g.drawString(set.reps, (W / 2) + 2, (H / 2) + 1);
g.setFont("6x8", 1);
const note = `Target reps: ${set.maxReps}`;
g.drawString(note, W / 2, H - 24);
// Draw sets monitor
g.drawString(exercise.subTitle, W / 2, H - 12);
drawMenu({showBTN1: true, showBTN2: true, showBTN3: true});
g.flip();
}
function drawWorkoutDone() {
const title1 = "You did";
const title2 = "GREAT!";
const msg = "That's the workout\ncompleted. Now eat\nsome food and\nget plenty of rest.";
clearWatch();
setWatch(Bangle.showLauncher, BTN2, {repeat: false});
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString(title1, W / 2, 10);
g.drawString(title2, W / 2, 30);
g.setFont("6x8", 1);
g.drawString(msg, (W / 2) + 3, 70);
g.flip();
}
function drawSetComp(exercise) {
const title = "Good work";
const msg1= "No need to rest\nmove straight on\nto the next\nexercise.";
const msg2 = exercise.canProgress()?
"Your\nweight has been\nincreased for\nnext time!":
"You'll\nsmash it next\ntime!";
g.clear();
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString(title, W / 2, 10);
g.setFont("6x8", 1);
g.drawString(msg1 + msg2, (W / 2) - 2, 45);
g.flip();
}
function drawRestTimer(exercise) {
g.clear();
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(PINK);
g.fillRect(15, 42, W - 15, 80);
g.setColor(WHITE);
g.setFont("6x8", 1);
g.drawString("Have a short\nrest period.", W / 2, 10);
g.setFont("6x8", 5);
g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19);
g.flip();
}
function redraw(workout) {
const exercise = workout.currentExercise();
g.clear();
if (workout.isCompleted()) {
saveWorkout(workout);
drawWorkoutDone();
return;
}
if (exercise.isRestTimerRunning()) {
if (exercise.isLastSet()) {
drawSetComp(exercise);
} else {
drawRestTimer(exercise);
}
return;
}
drawSet(exercise);
}
function drawWorkoutMenu(workouts, selWorkoutIdx) {
g.clear();
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString("BuffGym", W / 2, 10);
g.setFont("6x8", 1);
g.setFontAlign(-1, -1);
let selectedWorkout = workouts[selWorkoutIdx].title;
let yPos = 50;
workouts.forEach(workout => {
g.setColor("#f05a56");
g.fillRect(0, yPos, W, yPos + 11);
g.setColor("#ffffff");
if (selectedWorkout === workout.title) {
g.drawRect(0, yPos, W - 1, yPos + 11);
}
g.drawString(workout.title, 10, yPos + 2);
yPos += 15;
});
g.flip();
}
function setupMenu() {
clearWatch();
const workouts = getWorkoutIndex();
let selWorkoutIdx = 0;
drawWorkoutMenu(workouts, selWorkoutIdx);
setWatch(()=>{
selWorkoutIdx--;
if (selWorkoutIdx< 0) selWorkoutIdx = 0;
drawWorkoutMenu(workouts, selWorkoutIdx);
}, BTN1, {repeat: true});
setWatch(()=>{
const workout = buildWorkout(workouts[selWorkoutIdx].file);
workout.next();
}, BTN2, {repeat: false});
setWatch(()=>{
selWorkoutIdx++;
if (selWorkoutIdx > workouts.length - 1) selWorkoutIdx = workouts.length - 1;
drawWorkoutMenu(workouts, selWorkoutIdx);
}, BTN3, {repeat: true});
}
function drawSplash() {
g.reset();
g.setBgColor(RED);
g.clear();
g.setColor(WHITE);
g.setFontAlign(0,-1);
g.setFont("6x8", 2);
g.drawString("BuffGym", W / 2, 10);
g.setFont("6x8", 1);
g.drawString("5x5", W / 2, 42);
g.drawString("training app", W / 2, 55);
g.drawRect(19, 38, 100, 99);
const img = require("heatshrink").decompress(atob("lkdxH+AB/I5ASQACwpB5vNFkwpBAIfNFdZZkFYwskFZAsiFZBZiVYawEFf6ETFUwsIFUYmB54ADAwIskFYoRKBoIroB4grV58kkgCDFRotWFZwqHFiwYMFZIsTC5wLDFjGlCoWlkgJDRQIABCRAsLCwodCFAIABCwIOCFQYABr4RCCQIrMC4gqEAAwpFFZosFC5ArHFQ4rFNYQrGEgosMBxIrFLQwrLAB4sFSw4rFFjYrQFi4rNbASeEFjIoJFQYsGMAgAPEQgAIGwosCRoorbA="));
g.drawImage(img, 40, 70);
g.flip();
let flasher = false;
let bgCol, txtCol;
const i = setInterval(() => {
if (flasher) {
bgCol = WHITE;
txtCol = RED;
} else {
bgCol = RED;
txtCol = WHITE;
}
flasher = !flasher;
g.setColor(bgCol);
g.fillRect(0, 108, W, 120);
g.setColor(txtCol);
g.drawString("Press btn to begin", W / 2, 110);
g.flip();
}, 250);
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN1, {repeat: false});
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN2, {repeat: false});
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN3, {repeat: false});
}
function getWorkoutIndex() {
const workoutIdx = require("Storage").readJSON("buffgym-workout-index.json");
return workoutIdx;
}
function buildWorkout(fName) {
const Workout = require("buffgym-workout.js");
const workoutJSON = require("Storage").readJSON(fName);
const workout = Workout.fromJSON(workoutJSON);
return workout;
}
function saveWorkout(workout) {
const fName = getWorkoutIndex().find(w => w.title === workout.title).file;
require("Storage").writeJSON(fName, workout.toJSON());
}
drawSplash();

250
apps/buffgym/buffgym.html Normal file
View File

@ -0,0 +1,250 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<h1>BuffGym</h1>
<p>
Enter in your weights for each exercise, start light and keep consistent with your training. The weight increment field is how much the app will increase your weights for an exercise if you successfully complete all the reps and sets for an exercise. Make sure its a value that matches the weights in your gym.
</p>
<p>
For more information on how to train this program refer the <a href="https://stronglifts.com/5x5/" target="_BLANK">Stronglifts website</a>
</p>
<form id="workouts-form">
<h4>Workout A</h4>
<table class="table">
<thead>
<tr>
<th>Exercise</th>
<th>Sets / Reps</th>
<th>Weight</th>
<th>Weight increment</th>
</tr>
</thead>
<tbody id="workout-a-exercises">
<tr>
<td>
Squats
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-squats-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-squats-weight-increment" />
</td>
</tr>
<tr>
<td>
Overhead press
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-overhead-press-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-overhead-press-weight-increment" />
</td>
</tr>
<tr>
<td>
Deadlift
</td>
<td>
1x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-deadlift-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-deadlift-weight-increment" />
</td>
</tr>
<tr>
<td>
Pullups
</td>
<td>
3x10
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-pullups-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-pullups-weight-increment" />
</td>
</tr>
</tbody>
</table>
<h4>Workout B</h4>
<table class="table">
<thead>
<tr>
<th>Exercise</th>
<th>Sets / Reps</th>
<th>Weight</th>
<th>Weight increment</th>
</tr>
</thead>
<tbody id="workout-b-exercises">
<tr>
<td>
Squats
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-squats-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-squats-weight-increment" />
</td>
</tr>
<tr>
<td>
Bench press
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-bench-press-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-bench-press-weight-increment" />
</td>
</tr>
<tr>
<td>
Row
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-row-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-row-weight-increment" />
</td>
</tr>
<tr>
<td>
Tricep extension
</td>
<td>
3x10
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-triceps-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-triceps-weight-increment" />
</td>
</tr>
</tbody>
</table>
</form>
<br><br>
<button id="upload" class="btn btn-primary">Upload</button>
<script src="../../lib/interface.js"></script>
<script>
function workoutA() {
return {
"title": "Workout A",
"exercises": [
{
"title": "Squats",
"weight": Number(document.getElementById("buffgym-workout-a-squats-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-squats-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Overhead press",
"weight": Number(document.getElementById("buffgym-workout-a-overhead-press-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-overhead-press-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Deadlift",
"weight": Number(document.getElementById("buffgym-workout-a-deadlift-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-deadlift-weight-increment").value),
"unit": "Kg",
"sets": [5],
"restPeriod": 90
},
{
"title": "Pullups",
"weight": Number(document.getElementById("buffgym-workout-a-pullups-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-pullups-weight-increment").value),
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
};
}
function workoutB() {
return {
"title": "Workout B",
"exercises": [
{
"title": "Squats",
"weight": Number(document.getElementById("buffgym-workout-b-squats-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-squats-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Bench press",
"weight": Number(document.getElementById("buffgym-workout-b-bench-press-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-bench-press-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Row",
"weight": Number(document.getElementById("buffgym-workout-b-row-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-row-weight-increment").value),
"unit":"Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Tricep extension",
"weight": Number(document.getElementById("buffgym-workout-b-triceps-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-triceps-weight-increment").value),
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
};
}
document.getElementById("upload").addEventListener("click", function() {
Puck.eval(`require("Storage").writeJSON("buffgym-workout-a.json",${JSON.stringify(workoutA())})`, ()=>{
Puck.eval(`require("Storage").writeJSON("buffgym-workout-b.json",${JSON.stringify(workoutB())})`, ()=>{
Puck.eval(`Bangle.buzz()`, () => {
console.log("all done");
})
})
});
});
</script>
</body>
</html>

BIN
apps/buffgym/buffgym.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: fix precision rounding issue + no reset when equals pressed

23
apps/calculator/README.md Normal file
View File

@ -0,0 +1,23 @@
# Calculator
Basic calculator reminiscent of MacOs's one. Handy for small calculus.
<img src="https://user-images.githubusercontent.com/702227/79086938-bd3f4380-7d35-11ea-9988-a1a42916643f.png" height="384" width="384" />
## Features
- add / substract / divide / multiply
- handles floats
- basic memory button
## Controls
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- SELECT: BTN2
## Creator
<https://twitter.com/fredericrous>

392
apps/calculator/app.js Normal file
View File

@ -0,0 +1,392 @@
/**
* BangleJS Calculator
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*/
g.clear();
Graphics.prototype.setFont7x11Numeric7Seg = function() {
this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11);
};
var DEFAULT_SELECTION = '5';
var BOTTOM_MARGIN = 10;
var RIGHT_MARGIN = 20;
var COLORS = {
// [normal, selected]
DEFAULT: ['#7F8183', '#A6A6A7'],
OPERATOR: ['#F99D1C', '#CA7F2A'],
SPECIAL: ['#65686C', '#7F8183']
};
var keys = {
'0': {
xy: [0, 200, 120, 240],
trbl: '2.00'
},
'.': {
xy: [120, 200, 180, 240],
trbl: '3=.0'
},
'=': {
xy: [181, 200, 240, 240],
trbl: '+==.',
color: COLORS.OPERATOR
},
'1': {
xy: [0, 160, 60, 200],
trbl: '4201'
},
'2': {
xy: [60, 160, 120, 200],
trbl: '5301'
},
'3': {
xy: [120, 160, 180, 200],
trbl: '6+.2'
},
'+': {
xy: [181, 160, 240, 200],
trbl: '-+=3',
color: COLORS.OPERATOR
},
'4': {
xy: [0, 120, 60, 160],
trbl: '7514'
},
'5': {
xy: [60, 120, 120, 160],
trbl: '8624'
},
'6': {
xy: [120, 120, 180, 160],
trbl: '9-35'
},
'-': {
xy: [181, 120, 240, 160],
trbl: '*-+6',
color: COLORS.OPERATOR
},
'7': {
xy: [0, 80, 60, 120],
trbl: 'R847'
},
'8': {
xy: [60, 80, 120, 120],
trbl: 'N957'
},
'9': {
xy: [120, 80, 180, 120],
trbl: '%*68'
},
'*': {
xy: [181, 80, 240, 120],
trbl: '/*-9',
color: COLORS.OPERATOR
},
'R': {
xy: [0, 40, 60, 79],
trbl: 'RN7R',
color: COLORS.SPECIAL,
val: 'AC'
},
'N': {
xy: [60, 40, 120, 79],
trbl: 'N%8R',
color: COLORS.SPECIAL,
val: '+/-'
},
'%': {
xy: [120, 40, 180, 79],
trbl: '%/9N',
color: COLORS.SPECIAL
},
'/': {
xy: [181, 40, 240, 79],
trbl: '//*%',
color: COLORS.OPERATOR
}
};
var selected = DEFAULT_SELECTION;
var prevSelected = DEFAULT_SELECTION;
var prevNumber = null;
var currNumber = null;
var operator = null;
var results = null;
var isDecimal = false;
var hasPressedEquals = false;
function drawKey(name, k, selected) {
var rMargin = 0;
var bMargin = 0;
var color = k.color || COLORS.DEFAULT;
g.setColor(color[selected ? 1 : 0]);
g.setFont('Vector', 20);
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
g.setColor(-1);
// correct margins to center the texts
if (name == '0') {
rMargin = (RIGHT_MARGIN * 2) - 7;
} else if (name === '/') {
rMargin = 5;
} else if (name === '*') {
bMargin = 5;
rMargin = 3;
} else if (name === '-') {
rMargin = 3;
} else if (name === 'R' || name === 'N') {
rMargin = k.val === 'C' ? 0 : -9;
} else if (name === '%') {
rMargin = -3;
}
g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin);
}
function getIntWithPrecision(x) {
var xStr = x.toString();
var xRadix = xStr.indexOf('.');
var xPrecision = xRadix === -1 ? 0 : xStr.length - xRadix - 1;
return {
num: Number(xStr.replace('.', '')),
p: xPrecision
};
}
function multiply(x, y) {
var xNum = getIntWithPrecision(x);
var yNum = getIntWithPrecision(y);
return xNum.num * yNum.num / Math.pow(10, xNum.p + yNum.p);
}
function divide(x, y) {
var xNum = getIntWithPrecision(x);
var yNum = getIntWithPrecision(y);
return xNum.num / yNum.num / Math.pow(10, xNum.p - yNum.p);
}
function sum(x, y) {
let xNum = getIntWithPrecision(x);
let yNum = getIntWithPrecision(y);
let diffPrecision = Math.abs(xNum.p - yNum.p);
if (diffPrecision > 0) {
if (xNum.p > yNum.p) {
yNum.num = yNum.num * Math.pow(10, diffPrecision);
} else {
xNum.num = xNum.num * Math.pow(10, diffPrecision);
}
}
return (xNum.num + yNum.num) / Math.pow(10, Math.max(xNum.p, yNum.p));
}
function subtract(x, y) {
return sum(x, -y);
}
function doMath(x, y, operator) {
switch (operator) {
case '/':
return divide(x, y);
case '*':
return multiply(x, y);
case '+':
return sum(x, y);
case '-':
return subtract(x, y);
}
}
function displayOutput(num) {
var len;
var minusMarge = 0;
g.setColor(0);
g.fillRect(0, 0, 240, 39);
g.setColor(-1);
if (num === Infinity || num === -Infinity || isNaN(num)) {
// handle division by 0
if (num === Infinity) {
num = 'INFINITY';
} else if (num === -Infinity) {
num = '-INFINITY';
} else {
num = 'NOT A NUMBER';
minusMarge = -25;
}
len = (num + '').length;
currNumber = null;
results = null;
isDecimal = false;
hasPressedEquals = false;
prevNumber = null;
operator = null;
keys.R.val = 'AC';
drawKey('R', keys.R);
g.setFont('Vector', 22);
} else {
// might not be a number due to display of dot "."
var numNumeric = Number(num);
if (typeof num === 'string') {
if (num.indexOf('.') !== -1) {
// display a 0 before a lonely dot
if (numNumeric == 0) {
num = '0.';
}
} else {
// remove preceding 0
while (num.length > 1 && num[0] === '0')
num = num.substr(1);
}
}
len = (num + '').length;
if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) {
// minus is not available in font 7x11Numeric7Seg, we use Vector
g.setFont('Vector', 20);
g.drawString('-', 220 - (len * 15), 10);
minusMarge = 15;
}
g.setFont('7x11Numeric7Seg', 2);
}
g.drawString(num, 220 - (len * 15) + minusMarge, 10);
}
var wasPressedEquals = false;
var hasPressedNumber = false;
function calculatorLogic(x) {
if (wasPressedEquals && hasPressedNumber !== false) {
prevNumber = null;
currNumber = hasPressedNumber;
wasPressedEquals = false;
hasPressedNumber = false;
return;
}
if (hasPressedEquals) {
if (hasPressedNumber) {
prevNumber = null;
hasPressedNumber = false;
operator = null;
} else {
currNumber = null;
prevNumber = results;
}
hasPressedEquals = false;
wasPressedEquals = true;
}
if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) {
operator = x;
displayOutput(prevNumber);
} else if (prevNumber != null && currNumber != null && operator != null) {
// we execute the calculus only when there was a previous number entered before and an operator
results = doMath(prevNumber, currNumber, operator);
operator = x;
prevNumber = results;
currNumber = null;
displayOutput(results);
} else if (prevNumber == null && currNumber != null && operator == null) {
// no operator yet, save the current number for later use when an operator is pressed
operator = x;
prevNumber = currNumber;
currNumber = null;
displayOutput(prevNumber);
} else if (prevNumber == null && currNumber == null && operator == null) {
displayOutput(0);
}
}
function buttonPress(val) {
switch (val) {
case 'R':
currNumber = null;
results = null;
isDecimal = false;
hasPressedEquals = false;
if (keys.R.val == 'AC') {
prevNumber = null;
operator = null;
} else {
keys.R.val = 'AC';
drawKey('R', keys.R, true);
}
wasPressedEquals = false;
hasPressedNumber = false;
displayOutput(0);
break;
case '%':
if (results != null) {
displayOutput(results /= 100);
} else if (currNumber != null) {
displayOutput(currNumber /= 100);
}
hasPressedNumber = false;
break;
case 'N':
if (results != null) {
displayOutput(results *= -1);
} else {
displayOutput(currNumber *= -1);
}
break;
case '/':
case '*':
case '-':
case '+':
calculatorLogic(val);
hasPressedNumber = false;
break;
case '.':
keys.R.val = 'C';
drawKey('R', keys.R);
isDecimal = true;
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
break;
case '=':
if (prevNumber != null && currNumber != null && operator != null) {
results = doMath(prevNumber, currNumber, operator);
prevNumber = results;
displayOutput(results);
hasPressedEquals = 1;
}
hasPressedNumber = false;
break;
default:
keys.R.val = 'C';
drawKey('R', keys.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
if (isDecimal) {
currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
isDecimal = false;
} else {
currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val);
}
if (hasPressedEquals === 1) {
hasPressedEquals = 2;
}
hasPressedNumber = currNumber;
displayOutput(currNumber);
break;
}
}
for (var k in keys) {
if (keys.hasOwnProperty(k)) {
drawKey(k, keys[k], k == '5');
}
}
g.setFont('7x11Numeric7Seg', 2.8);
g.drawString('0', 205, 10);
function moveDirection(d) {
drawKey(selected, keys[selected]);
prevSelected = selected;
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d];
drawKey(selected, keys[selected], true);
}
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

273
apps/calculator/tests.html Normal file
View File

@ -0,0 +1,273 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Calculator tests</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/7.1.0/mocha.min.css">
<style>
#header {
margin: 60px 50px;
font: 1em "Helvetica Neue",Helvetica,Arial,sans-serif;
font-weight: 200;
}
</style>
</head>
<body>
<div id="header"></div>
<div id="mocha"></div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/7.1.0/mocha.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js"></script>
<script>
// mocks
const _ = () => {}
setWatch = _
drawKey = _
currentOutput = 0;
g = {
setFont: _,
drawString: n => currentOutput = n,
setColor: _,
fillRect: _,
clear: _
}
_graphics = function () {
this.setFontCustom = _
}
Graphics = _graphics
BTN1 = 1
BTN2 = 2
BTN3 = 3
BTN4 = 4
BTN5 = 5
Bangle = {
on: _
}
Terminal = { println: console.log }
</script>
<script src="app.js"></script>
<script>
header`
// Unit tests for the BangleJS's Calculator app
`
mocha.setup({ui:'bdd'})
chai.should()
var expect = chai.expect
const sequencePress = x => x.split('').forEach(y => buttonPress(y))
const sequenceReset = _ => [...Array(2)].forEach(x => buttonPress('R'))
describe("Simple arithmetic", function(){
it("multiplication", function(){
multiply(1.4,2.4).should.equal(3.36)
})
it("division", function(){
divide(4.4,2).should.equal(2.2)
})
it("sum", function(){
sum(4.1,2).should.equal(6.1)
})
it("subtract", function(){
subtract(4.1,2).should.equal(2.1)
})
})
describe("Simple Operation with Reset", function(){
it("Simple addition", function(){
sequencePress("50+3=")
currentOutput.should.equal(53)
})
it("Reset output 'C' then 'AC'", function(){
sequenceReset()
currentOutput.should.equal(0)
})
it("Complex calculus", function(){
sequenceReset()
sequencePress("3*3+3-2/2=")
currentOutput.should.equal(5)
})
it("Change operator", function(){
sequenceReset()
sequencePress("3*+/3=")
currentOutput.should.equal(1)
})
})
describe("Operations on Double-s", function(){
it("Simple addition", function(){
sequenceReset()
sequencePress("1.3+1.7=")
currentOutput.should.equal(3)
})
it("some calculation", function(){
sequenceReset()
sequencePress("1.3+1.7*2.22/2=")
currentOutput.should.equal(3.33)
})
it("No corrupt opposed to what javascript Number would", function(){
sequenceReset()
sequencePress("1.3+1.7*2.2/2=")
currentOutput.should.equal(3.3)
})
it("Complex calcul", function(){
sequenceReset()
sequencePress("48/.2/")
currentOutput.should.equal(240)
})
})
describe("Negative Operations", function(){
it("Negative on first number", function(){
sequenceReset()
sequencePress("50N+3=")
currentOutput.should.equal(-47)
})
it("Substract negative", function(){
sequenceReset()
sequencePress("50N-3=")
currentOutput.should.equal(-53)
})
it("Negative before number is typed", function(){
sequenceReset()
sequencePress("N50-3=")
currentOutput.should.equal(-53)
})
it("Negative addition on second number", function(){
sequenceReset()
sequencePress("50-N33=")
currentOutput.should.equal(83)
})
it("Negative zero", function(){
sequenceReset()
sequencePress("N")
currentOutput.should.equal(-0)
sequenceReset()
sequencePress("0N")
currentOutput.should.equal(-0)
sequenceReset()
sequencePress("N0")
currentOutput.should.equal('-0')
sequenceReset()
sequencePress("0N")
currentOutput.should.equal(-0)
sequencePress("N0")
currentOutput.should.equal('0')
})
})
describe("Zero division", function(){
it("Divide 0 by 0", function(){
sequenceReset()
sequencePress("0/0=")
currentOutput.should.equal('NOT A NUMBER')
})
it("Divde N by 0", function(){
sequenceReset()
sequencePress("1/0=")
currentOutput.should.equal('INFINITY')
})
it("Divde -N by 0", function(){
sequenceReset()
sequencePress("N1/0=")
currentOutput.should.equal('-INFINITY')
})
})
describe("Press equals '='", function(){
it("should display result when new operation button is pressed", function(){
sequenceReset()
sequencePress("5+6+")
currentOutput.should.equal(11)
sequenceReset()
sequencePress("5-6*4/2/")
currentOutput.should.equal(-2)
})
it("New operation after '='", function(){
sequenceReset()
sequencePress("5+4=5")
currentOutput.should.equal('5')
sequenceReset()
sequencePress("N5+4*3-3/-1=5")
currentOutput.should.equal('5')
})
it("Double '=' repeats last operation", function(){
sequenceReset()
sequencePress("2+2==")
currentOutput.should.equal(6)
})
it("New operation applied to calculated result", function(){
sequenceReset()
sequencePress("9*9=*9=")
currentOutput.should.equal(729)
})
it("Turn result negative, do addition", function(){
sequenceReset()
sequencePress("9*9=N+1=")
currentOutput.should.equal(-80)
})
it("New operation after '=' dissociated from previous one", function(){
sequenceReset()
sequencePress("9*9=9*")
currentOutput.should.equal('9')
sequenceReset()
sequencePress("9*9=99+1=*2=")
currentOutput.should.equal(200)
})
})
describe("Memory", function(){
it("Reset 1st number with 'C'", function(){
sequenceReset()
sequencePress("50R3+6=")
currentOutput.should.equal(9)
})
it("Reset 2nd number with 'C'", function(){
sequenceReset()
sequencePress("50+3R+6=")
currentOutput.should.equal(56)
})
it("Complex calcul", function(){
sequenceReset()
sequencePress("/3*3+3R-+2/2=")
currentOutput.should.equal(5.5)
})
})
mocha.run()
function header(str) { document.getElementById('header').innerHTML = str[0].replace(/\n/, '').replace(/\n/g, '<br>') }
</script>
</body>
</html>

1
apps/calendar/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Basic calendar

8
apps/calendar/README.md Normal file
View File

@ -0,0 +1,8 @@
# Calendar
Basic calendar
## Usage
- Use `BTN4` (left screen tap) to go to the previous month
- Use `BTN5` (right screen tap) to go to the next month

View File

@ -0,0 +1,5 @@
require("heatshrink").decompress(
atob(
"mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA=="
)
)

160
apps/calendar/calendar.js Normal file
View File

@ -0,0 +1,160 @@
const maxX = 240;
const maxY = 240;
const rowN = 7;
const colN = 7;
const headerH = maxY / 7;
const rowH = (maxY - headerH) / rowN;
const colW = maxX / colN;
const color1 = "#035AA6";
const color2 = "#4192D9";
const color3 = "#026873";
const color4 = "#038C8C";
const color5 = "#03A696";
const black = "#000000";
const white = "#ffffff";
const gray1 = "#444444";
const gray2 = "#888888";
const gray3 = "#bbbbbb";
const red = "#d41706";
function drawCalendar(date) {
g.setBgColor(color4);
g.clearRect(0, 0, maxX, maxY);
g.setBgColor(color1);
g.clearRect(0, 0, maxX, headerH);
g.setBgColor(color2);
g.clearRect(0, headerH, maxX, headerH + rowH);
g.setBgColor(color3);
g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
for (let y = headerH; y < maxY; y += rowH) {
g.drawLine(0, y, maxX, y);
}
for (let x = 0; x < maxX; x += colW) {
g.drawLine(x, headerH, x, maxY);
}
const month = date.getMonth();
const year = date.getFullYear();
const monthMap = {
0: "January",
1: "February",
2: "March",
3: "April",
4: "May",
5: "June",
6: "July",
7: "August",
8: "September",
9: "October",
10: "November",
11: "December"
};
g.setFontAlign(0, 0);
g.setFont("6x8", 2);
g.setColor(white);
g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2);
g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true);
g.drawPoly(
[maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10],
true
);
g.setFont("6x8", 2);
const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
dowLbls.forEach((lbl, i) => {
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
});
date.setDate(1);
const dow = date.getDay();
const dowNorm = dow === 0 ? 7 : dow;
const monthMaxDayMap = {
0: 31,
1: (2020 - year) % 4 === 0 ? 29 : 28,
2: 31,
3: 30,
4: 31,
5: 30,
6: 31,
7: 31,
8: 30,
9: 31,
10: 30,
11: 31
};
let days = [];
let nextMonthDay = 1;
let thisMonthDay = 51;
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm;
for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
if (i < dowNorm) {
days.push(prevMonthDay);
prevMonthDay++;
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
days.push(thisMonthDay);
thisMonthDay++;
} else {
days.push(nextMonthDay);
nextMonthDay++;
}
}
let i = 0;
for (y = 0; y < rowN - 1; y++) {
for (x = 0; x < colN; x++) {
i++;
const day = days[i];
const isToday =
today.year === year && today.month === month && today.day === day - 50;
if (isToday) {
g.setColor(red);
g.drawRect(
x * colW,
y * rowH + headerH + rowH,
x * colW + colW - 1,
y * rowH + headerH + rowH + rowH
);
}
g.setColor(day < 50 ? gray3 : white);
g.drawString(
(day > 50 ? day - 50 : day).toString(),
x * colW + colW / 2,
headerH + rowH + y * rowH + rowH / 2
);
}
}
}
const date = new Date();
const today = {
day: date.getDate(),
month: date.getMonth(),
year: date.getFullYear()
};
drawCalendar(date);
clearWatch();
setWatch(
() => {
const month = date.getMonth();
const prevMonth = month > 0 ? month - 1 : 11;
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
date.setMonth(prevMonth);
drawCalendar(date);
},
BTN4,
{ repeat: true }
);
setWatch(
() => {
const month = date.getMonth();
const prevMonth = month < 11 ? month + 1 : 0;
if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1);
date.setMonth(month + 1);
drawCalendar(date);
},
BTN5,
{ repeat: true }
);
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });

BIN
apps/calendar/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

3
apps/chronowid/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New widget and app!
0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme)
0.03: Display only minutes:seconds when less than 1 hour left

38
apps/chronowid/README.md Normal file
View File

@ -0,0 +1,38 @@
# Chronometer Widget
Chronometer (timer) that runs as a widget.
The advantage is, that you can still see your normal watchface and other widgets when the timer is running.
The widget is always active, but only shown when the timer is on.
Hours, minutes, seconds and timer status can be set with an app.
Depending on when you start the timer, it may alert up to 0,999 seconds early. This is because it checks only for full seconds. When there is less than one seconds left, it buzzes. This cannot be avoided without checking more than every second, which I would like to avoid.
## Screenshots
TBD
## Features
* Using other apps does not interrupt the timer, no need to keep the widget open (BUT: there will be no buzz when the time is up, for that the widget has to be loaded)
* Target time is saved to a file and timer picks up again when widget is loaded again.
## Settings
There are no settings section in the settings app, timer can be set using an app.
* Reset values: Reset hours, minutes, seconds to 0; set timer on to false; write to settings file
* Hours: Set the hours for the timer
* Minutes: Set the minutes for the timer
* Seconds: Set the seconds for the timer
* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on.
## Releases
* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/)
* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#)
* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid
## Requests
If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIFCn/8BYYFRABcD4AFFgIFCh/wgeAAoP//8HCYMDAoPD8EAg4FB8PwgEf+EP/H4HQOAgP8uEAvwfBv0ggBFCn4CB/EBwEfgEB+AFBh+AgfgAoI1BIoQJB4AHBAoXgg4uBAIIFCCYQFGh5rDJQJUBK4IFCNYIFVDoopDGoJiBHYYFKVYRZBWIYDBA4IFBNIQzBG4IbBToKkBAQKVFUIYICVoQUCXIQmCYoIsCaITqDAoLvDNYUAA="))

98
apps/chronowid/app.js Normal file
View File

@ -0,0 +1,98 @@
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
const storage = require('Storage');
const boolFormat = v => v ? "On" : "Off";
let settingsChronowid;
function updateSettings() {
var now = new Date();
const goal = new Date(now.getFullYear(), now.getMonth(), now.getDate(),
now.getHours() + settingsChronowid.hours, now.getMinutes() + settingsChronowid.minutes, now.getSeconds() + settingsChronowid.seconds);
settingsChronowid.goal = goal.getTime();
storage.writeJSON('chronowid.json', settingsChronowid);
}
function resetSettings() {
settingsChronowid = {
hours : 0,
minutes : 0,
seconds : 0,
started : false,
counter : 0,
goal : 0,
};
updateSettings();
}
settingsChronowid = storage.readJSON('chronowid.json',1);
if (!settingsChronowid) resetSettings();
E.on('kill', () => {
updateSettings();
});
function showMenu() {
const timerMenu = {
'': {
'title': 'Set timer',
'predraw': function() {
timerMenu.hours.value = settingsChronowid.hours;
timerMenu.minutes.value = settingsChronowid.minutes;
timerMenu.seconds.value = settingsChronowid.seconds;
timerMenu.started.value = settingsChronowid.started;
}
},
'Reset values': function() {
settingsChronowid.hours = 0;
settingsChronowid.minutes = 0;
settingsChronowid.seconds = 0;
settingsChronowid.started = false;
updateSettings();
showMenu();
},
'Hours': {
value: settingsChronowid.hours,
min: 0,
max: 24,
step: 1,
onchange: v => {
settingsChronowid.hours = v;
updateSettings();
}
},
'Minutes': {
value: settingsChronowid.minutes,
min: 0,
max: 59,
step: 1,
onchange: v => {
settingsChronowid.minutes = v;
updateSettings();
}
},
'Seconds': {
value: settingsChronowid.seconds,
min: 0,
max: 59,
step: 1,
onchange: v => {
settingsChronowid.seconds = v;
updateSettings();
}
},
'Timer on': {
value: settingsChronowid.started,
format: boolFormat,
onchange: v => {
settingsChronowid.started = v;
updateSettings();
}
},
};
timerMenu['-Exit-'] = ()=>{load();};
return E.showMenu(timerMenu);
}
showMenu();

BIN
apps/chronowid/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

93
apps/chronowid/widget.js Normal file
View File

@ -0,0 +1,93 @@
(() => {
const storage = require('Storage');
settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file
var height = 23;
var width = 58;
var interval = 0; //used for the 1 second interval timer
var now = new Date();
var time = 0;
var diff = settingsChronowid.goal - now;
//Convert ms to time
function getTime(t) {
var milliseconds = parseInt((t % 1000) / 100),
seconds = Math.floor((t / 1000) % 60),
minutes = Math.floor((t / (1000 * 60)) % 60),
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
}
function printDebug() {
print ("Nowtime: " + getTime(now));
print ("Now: " + now);
print ("Goaltime: " + getTime(settingsChronowid.goal));
print ("Goal: " + settingsChronowid.goal);
print("Difftime: " + getTime(diff));
print("Diff: " + diff);
print ("Started: " + settingsChronowid.started);
print ("----");
}
//counts down, calculates and displays
function countDown() {
now = new Date();
diff = settingsChronowid.goal - now; //calculate difference
WIDGETS["chronowid"].draw();
//time is up
if (settingsChronowid.started && diff < 1000) {
Bangle.buzz(1500);
//write timer off to file
settingsChronowid.started = false;
storage.writeJSON('chronowid.json', settingsChronowid);
clearInterval(interval); //stop interval
}
//printDebug();
}
// draw your widget
function draw() {
if (!settingsChronowid.started) {
width = 0;
return; //do not draw anything if timer is not started
}
g.reset();
if (diff >= 0) {
if (diff < 3600000) { //less than 1 hour left
width = 58;
g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.setFont("6x8", 2);
g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00
}
if (diff >= 3600000) { //one hour or more left
width = 48;
g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.setFont("6x8", 1);
g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00
}
}
// not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed.
// else {
// width = 58;
// g.clearRect(this.x,this.y,this.x+width,this.y+height);
// g.setFont("6x8", 2);
// g.drawString("END", this.x+15, this.y+5);
// }
}
if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second
// add the widget
WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() {
reload();
Bangle.drawWidgets(); // relayout all widgets
}};
//printDebug();
countDown();
})();

2
apps/compass/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Show text if uncalibrated

View File

@ -1,34 +1,43 @@
g.clear();
g.setColor(0,0.5,1);
g.fillCircle(120,130,80,80);
g.setColor(0,0,0);
g.fillCircle(120,130,70,70);
function arrow(r,c) {
r=r*Math.PI/180;
var p = Math.PI/2;
g.setColor(c);
g.fillPoly([
120+60*Math.sin(r), 130-60*Math.cos(r),
120+10*Math.sin(r+p), 130-10*Math.cos(r+p),
120+10*Math.sin(r+-p), 130-10*Math.cos(r-p),
]);
}
var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
g.setFont("6x8",3);
g.setColor(0);
g.fillRect(70,0,170,24);
g.setColor(0xffff);
g.setFontAlign(0,0);
g.drawString(isNaN(m.heading)?"---":Math.round(m.heading),120,12);
g.setColor(0,0,0);
arrow(oldHeading,0);
arrow(oldHeading+180,0);
arrow(m.heading,0xF800);
arrow(m.heading+180,0x001F);
oldHeading = m.heading;
});
Bangle.setCompassPower(1);
g.clear();
g.setColor(0,0.5,1);
g.fillCircle(120,130,80,80);
g.setColor(0,0,0);
g.fillCircle(120,130,70,70);
function arrow(r,c) {
r=r*Math.PI/180;
var p = Math.PI/2;
g.setColor(c);
g.fillPoly([
120+60*Math.sin(r), 130-60*Math.cos(r),
120+10*Math.sin(r+p), 130-10*Math.cos(r+p),
120+10*Math.sin(r+-p), 130-10*Math.cos(r-p),
]);
}
var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
g.setFont("6x8",3);
g.setColor(0);
g.fillRect(0,0,230,40);
g.setColor(0xffff);
if (isNaN(m.heading)) {
g.setFontAlign(-1,-1);
g.setFont("6x8",2);
g.drawString("Uncalibrated",50,12);
g.drawString("turn 360° around",25,26);
}
else {
g.setFontAlign(0,0);
g.setFont("6x8",3);
g.drawString(Math.round(m.heading),120,12);
}
g.setColor(0,0,0);
arrow(oldHeading,0);
arrow(oldHeading+180,0);
arrow(m.heading,0xF800);
arrow(m.heading+180,0x001F);
oldHeading = m.heading;
});
Bangle.setCompassPower(1);

5
apps/dane/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.01: New App!
0.04: Added Icon to watchface
0.05: bugfix
0.06: moved and resized icon
0.07: Added Description

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("l8wxH+AH4A/AH4A/AFUvl8Cu4AEgUCBQIrfFQMRAAe/Aw4xbDYIlBiUS7AjCAAY5BBYMSiJkBGC4sCicTiRQJHoUSCAIwBF6sv30SikUiRMMMIISD7AvTl/YiYtPF40TF6R4BicVFqAWDF4MViaPRIwQWTF4O/IwiKRCoMRUiZHEDJ5cXJAxeOOQuQhQuShWQJIe/JJkviIuC74tTFwORRqKLD+3cmVLpsLFZtNAANKhXeDYKNOu4uEmdlDwVNBoNlsoDDmoKBhYQChcyFycVFwOTFwJcBpomBhYjCmouBAwYMCmZdBa4d3FyonBKoIoCAwIECLooucEIIjCRIYuFms1Lqq7CFwS7DLQQsDhYrBHIZdHXZkCdQpQDXoIQDFwIDBeoQQCpYuSl8RFwMT70KCRYAIhUSFwMTiMvFxm/CQUSFyp5Did3Fxi8DOBwuLDSEv7ETfoRCNDI13DIMT34ZPIYSgOaxJ3SIgZeTC7COBdgMCC58vOoakWiQvQFoQTBFqgvEiURF5gRDOKIdIDwMRiO/axMCBoMRLQItXF4Z9B7F3BxF37BZBAAQnRIYobDMAKqIl5aDAA5zJFwaCBAA6PBFxQQEAAYKBFxjSCU4IECA4YuJCAoAEFx0UikTAAIEBAwQuKCIoADFxsCI5RdiUAoAEVgIVJABRDHAH4A/AH4A/ADAA="))

163
apps/dane/app.js Normal file
View File

@ -0,0 +1,163 @@
const font = "6x8";
const timeFontSize = 4;
const dateFontSize = 3;
const smallFontSize = 2;
const yOffset = 23;
const xyCenter = g.getWidth()/2;
const cornerSize = 14;
const cornerOffset = 3;
const borderWidth = 1;
const yposTime = 27+yOffset;
const yposDate = 65+yOffset;
const mainColor = "#26dafd";
const mainColorDark = "#029dbb";
const mainColorLight = "#8bebfe";
const secondaryColor = "#df9527";
const secondaryColorDark = "#8b5c15";
const secondaryColorLight = "#ecc180";
const success = "#00ff00";
const successDark = "#000900";
const successLight = "#060f06";
const alert = "#ff0000";
const alertDark = "#090000";
const alertLight = "#0f0606";
var img = {
width : 120, height : 120, bpp : 8,
transparent : 254,
buffer : require("heatshrink").decompress(atob("/wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AAdCABhN/OFM8ABU2P35zkM4U2hkAABwSBCwJ6/OjZxBgxyPABAZBPgJ6/OqbnBOg8rAAJyNCBEGhk2PX51PmBhHOhEGmwACPRQXFCoL1DOP51HdIh0IhkwnhcDAAoKBm0wDwYdEDwp5/Oo8MKxjQEABwiEkp5/Oxs2OpBTDOgwjOEyEMPHrJFJwxPCmx0QPRM8PIQpJFQjs8JZLEDJa55EUYMGFpMwPG5ICgzsQUrimCkryKnh40OyYxfPAQxIGQMGPGZ2EIZJ2iPCLxyOwRBMO0Z4/IIp2yPH4/Dhg9JHwJ2nPAg5Mgx3sFgMwgEqHhMMO1B4EeBQ7EO1U8HZSzBni0rHh0AmyzqHPB4FmDwLgC1BHGsMB4J3uWxY/Ed2ivBO1h4DmxAOG00MV2jwYmBBld354DmB3LeEo0Bgzu9eCMGIcYzOm1DoZ3wPAUMeF4yNg8Bnp3zGYM3gEHO5U2eEIhBdxcHg52zO4U9gJ3JPAMMO8U2O5k3odEO+VEPAKxBO5UAnh3tHgM9oh30AAMNO4tWO4s2O79CoUGdxcHn1EotFO+NFO4M3O5R4BgxXBO708dxR3BhB2Co1AO+J4BnCzBO/U4OwdAoIACN8goDAAVAow2Bnx3FAApTBnh3fmx3FljuFO4NGsmzAAWPxOJstlLpGJx4LGBIWJSIgIBCIVBsuPFYYsCsjwCO+ApEO5NlJAJ0BAAllegwRCPAwJC2YVEOIJ/BAAOJT4YoDeAVEhB3roVCdwsrqx3IJgJSDZYNlcoTbGNo53EDop3GBglBoB3KJAhUBmx3mmR3Fn53ILYjlDA4LQCMwYKDO4SCCDYQkEFQILDO40yd5h3nAAkHhx3BoB3EN4ZWHOgIGBPQQKE2YLBOIh3SnEHPBJ37boZWEOYJnCO44LBxKGCO5AWBAAZ4BO/53GDYhcGOQp8DNwoPBQ4Z3GAAINBAANlO/53TB4J3EAogREsrwCd59FO/53FPAhlHLggVENw4QCSRQABoB3/O5ZWGMIIABNAJ8BAAIMEPomPCAJ3Nox3+hB3HAAZeCKwQOCdwTwDO5ATCRYR38PAJ3Pox3HNIOPNIZ8BQozjBBpB+BO44cFoFAO6E8O782PBR3GJoIADdohpCAoIoEPAQJBO4YKCeAZ3FB4IVBAAVkeAJ3vnh3Mnx3BZgZ6DJoLmFOwoABO4ZpBsoLFx53CRQQqEAAKbBO/0HnFFotAoBvDNo4AXD4opEAAIyBGwNEm53Lg1CO79Cgx3MohBBoxyeACZ2Boh2KO+M3H4NFO2R3OgEAmx2ePAU2EoJ4Jho/Boh3zGoNDO5k8O90HodDO2Z3Boc9O5cMoR3hoUMO5UBO4J40GoM3gJ3IZAM2O0DwNg8Anp33IoMkO5M8O8c8O5IyBmFCO+lCoRELgwOBGUcMGRUAGUZDSO5TuleBozDPGQzBmxDKd0jwPmB31IRLunGocGVhh4wGIM8dxUMIE4nBmw2IVoZ3ymDuyG4cMG5TwwdxYIBmw+qHBjwvU4S2Khg9rWJrwuFoM2HhMGHfSyCWdlCOxU8O9p4LA4M2PFQqCgx2IHIZ2sPBy1CH8x2/PGwlBnkMO3p4zEYU8dpMGO2q8EIoJGFAwMwPEIhCmx2HGAMGVMZIYmBABg54GeQQtiOw7sCO25KEnkMIYJMEYAJKdFQQpHAAMMUgR25PAlCmx5GAoR5BFLM8gx1IUIh27PAp5BJYRUCKIgoXEYZ0EToZ2/PA7MBeYZ5DmBPWoTtBOos2ngxFO/5FGPQUwPAcMO64cEOhB2xnh3XPITPDKCocBDYZ1JPCEwO78MO7JbEZKqTGABhBLnk2O78Amw1KJBp3bmwaCHIwASDoJ3ggw+aO4c8O+M8hgbBhg2UIB0wIKx3DDQI2YLYLZCACEMZIIADO8YAEhgAEGgoAHlZ3bDgQAWlYaCO8QmDH7B3WmAcCGyoXCO9AAZgEMICdCoUMGrh3DPDp3iICR3/d+42BO8J2cO/53/IDU8GykGO/88O+g1ggB2dIIgAdO64AeO/cwmwACGyoZDADU8VqhBPEoIADoQATG7IuUGsBCjHswA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A1"))
}
function drawTopLeftCorner(x,y) {
g.setColor(mainColor);
var x1 = x-cornerOffset;
var y1 = y-cornerOffset;
g.fillRect(x1,y1,x1+cornerSize,y1+cornerSize);
g.setColor("#000000");
g.fillRect(x,y,x+cornerSize-cornerOffset,y+cornerSize-cornerOffset);
}
function drawTopRightCorner(x,y) {
g.setColor(mainColor);
var x1 = x+cornerOffset;
var y1 = y-cornerOffset;
g.fillRect(x1,y1,x1-cornerSize,y1+cornerSize);
g.setColor("#000000");
g.fillRect(x,y,x-cornerSize-cornerOffset,y+cornerSize-cornerOffset);
}
function drawBottomLeftCorner(x,y) {
g.setColor(mainColor);
var x1 = x-cornerOffset;
var y1 = y+cornerOffset;
g.fillRect(x1,y1,x1+cornerSize,y1-cornerSize);
g.setColor("#000000");
g.fillRect(x,y,x+cornerSize-cornerOffset,y-cornerSize+cornerOffset);
}
function drawBottomRightCorner(x,y) {
g.setColor(mainColor);
var x1 = x+cornerOffset;
var y1 = y+cornerOffset;
g.fillRect(x1,y1,x1-cornerSize,y1-cornerSize);
g.setColor("#000000");
g.fillRect(x,y,x-cornerSize+cornerOffset,y-cornerSize+cornerOffset);
}
function drawFrame(x1,y1,x2,y2) {
drawTopLeftCorner(x1,y1);
drawTopRightCorner(x2,y1);
drawBottomLeftCorner(x1,y2);
drawBottomRightCorner(x2,y2);
g.setColor(mainColorDark);
g.drawRect(x1,y1,x2,y2);
g.setColor("#000000");
g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth);
}
function drawTopFrame(x1,y1,x2,y2) {
drawBottomLeftCorner(x1,y2);
drawBottomRightCorner(x2,y2);
g.setColor(mainColorDark);
g.drawRect(x1,y1,x2,y2);
g.setColor("#000000");
g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth);
}
function drawBottomFrame(x1,y1,x2,y2) {
drawTopLeftCorner(x1,y1);
drawTopRightCorner(x2,y1);
g.setColor(mainColorDark);
g.drawRect(x1,y1,x2,y2);
g.setColor("#000000");
g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth);
}
function getUTCTime(d) {
return d.toUTCString().split(' ')[4].split(':').map(function(d){return Number(d);});
}
function drawTimeText() {
g.setFontAlign(0, 0);
var d = new Date();
var da = d.toString().split(" ");
var dutc = getUTCTime(d);
var time = da[4].split(":");
var hours = time[0],
minutes = time[1],
seconds = time[2];
g.setColor(mainColor);
g.setFont(font, timeFontSize);
g.drawString(`${hours}:${minutes}:${seconds}`, xyCenter, yposTime, true);
g.setFont(font, smallFontSize);
}
function drawDateText() {
g.setFontAlign(0, 0);
var d = new Date();
g.setFont(font, dateFontSize);
g.drawString(`${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`, xyCenter, yposDate, true);
}
function drawClock() {
// main frame
drawFrame(3,10+yOffset,g.getWidth()-3,g.getHeight()-3);
// time frame
drawTopFrame(20,10+yOffset,220,46+yOffset);
// date frame
drawTopFrame(28,46+yOffset,212,46+yOffset+35);
// texts
drawTimeText();
drawDateText();
g.drawImage(img,g.getWidth()/2-(img.width/2),g.getHeight()/2);
}
function updateClock() {
drawTimeText();
drawDateText();
}
Bangle.on('lcdPower', function(on) {
if (on) drawClock();
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawClock();
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
// refesh every 100 milliseconds
setInterval(updateClock, 100);

BIN
apps/dane/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1 +1,2 @@
0.02: Fix deletion of apps - now use files list in app.info (fix #262)
0.03: Add support for data files

View File

@ -30,29 +30,80 @@ function showMainMenu() {
return E.showMenu(mainmenu);
}
function eraseApp(app) {
E.showMessage('Erasing\n' + app.name + '...');
function isGlob(f) {return /[?*]/.test(f)}
function globToRegex(pattern) {
const ESCAPE = '.*+-?^${}()|[]\\';
const regex = pattern.replace(/./g, c => {
switch (c) {
case '?': return '.';
case '*': return '.*';
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
}
});
return new RegExp('^'+regex+'$');
}
function eraseFiles(app) {
app.files.split(",").forEach(f=>storage.erase(f));
}
function eraseData(app) {
if(!app.data) return;
const d=app.data.split(';'),
files=d[0].split(','),
sFiles=(d[1]||'').split(',');
let erase = f=>storage.erase(f);
files.forEach(f=>{
if (!isGlob(f)) erase(f);
else storage.list(globToRegex(f)).forEach(erase);
})
erase = sf=>storage.open(sf,'r').erase();
sFiles.forEach(sf=>{
if (!isGlob(sf)) erase(sf);
else storage.list(globToRegex(sf+'\u0001'))
.forEach(fs=>erase(fs.substring(0,fs.length-1)));
})
}
function eraseApp(app, files,data) {
E.showMessage('Erasing\n' + app.name + '...');
if (files) eraseFiles(app)
if (data) eraseData(app)
}
function eraseOne(app, files,data){
E.showPrompt('Erase\n'+app.name+'?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
eraseApp(app, files,data)
showApps();
} else {
showAppMenu(app)
}
})
}
function eraseAll(apps, files,data) {
E.showPrompt('Erase all?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
for(var n = 0; n<apps.length; n++)
eraseApp(apps[n], files,data);
}
showApps();
})
}
function showAppMenu(app) {
const appmenu = {
let appmenu = {
'': {
'title': app.name,
},
'< Back': () => m = showApps(),
'Erase': () => {
E.showPrompt('Erase\n' + app.name + '?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
eraseApp(app);
m = showApps();
} else {
m = showAppMenu(app)
}
});
}
};
}
if (app.data) {
appmenu['Erase Completely'] = () => eraseOne(app, true, true)
appmenu['Erase App,Keep Data'] = () => eraseOne(app,true, false)
appmenu['Only Erase Data'] = () => eraseOne(app,false, true)
} else {
appmenu['Erase'] = () => eraseOne(app,true, false)
}
return E.showMenu(appmenu);
}
@ -78,13 +129,12 @@ function showApps() {
return menu;
}, appsmenu);
appsmenu['Erase All'] = () => {
E.showPrompt('Erase all?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
for (var n = 0; n < list.length; n++)
eraseApp(list[n]);
}
m = showApps();
E.showMenu({
'': {'title': 'Erase All'},
'Erase Everything': () => eraseAll(list, true, true),
'Erase Apps,Keep Data': () => eraseAll(list, true, false),
'Only Erase Data': () => eraseAll(list, false, true),
'< Back': () => showApps(),
});
};
} else {

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