|
@ -1,3 +1,4 @@
|
|||
.htaccess
|
||||
node_modules
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
|
|
|
@ -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
|
||||
|
|
28
README.md
|
@ -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
|
@ -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}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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 
|
||||
|
||||
## 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
|
|
@ -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}
|
||||
|
|
|
@ -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 
|
||||
|
||||
## 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
|
|
@ -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"}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 374 B |
After Width: | Height: | Size: 338 B |
|
@ -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
|
|
@ -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
|
||||

|
||||
|
||||
* 1600 steps
|
||||

|
||||
|
||||
* 10600 steps
|
||||

|
||||
|
||||
## 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/
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I="))
|
|
@ -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();
|
||||
|
||||
})();
|
After Width: | Height: | Size: 836 B |
|
@ -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);
|
||||
});
|
|
@ -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};
|
||||
})();
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# Ball Maze
|
||||
|
||||
Navigate a ball through a maze by tilting your watch.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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>
|
|
@ -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();
|
||||
})();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA"))
|
After Width: | Height: | Size: 444 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: First release
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwMB/4ACx4ED/0DApP8AqAXB84GDg/DAgXj/+DCAUABgIFB4EAv4FCwEAj0PAoJPBgwFEgEfDgMOAoM/AoMegFAAoP8jkA8F/AoM8gP4DgP4nBvD/F4KQfwuAFE+A/CAoPgAofx8A/CKYRwELIIFDLII6BAoZSBLIYeC/0BwAFDgfAGAQFBHgf8g4BBIIUH/wFBSYMPAoXwAog/Bj4FEv4FDDQQCBQoQFCZYYFi/6KE/+P/4A="))
|
|
@ -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 });
|
After Width: | Height: | Size: 10 KiB |
|
@ -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
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
};
|
||||
```
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
})()
|
||||
})();
|
|
@ -0,0 +1 @@
|
|||
0.01: New game! BTN4- Hit card, BTN5- Stand
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgIQNgfAAgU///wAgMH/4dBAoMMAQMQAQMIAQMYAQ4RCApcPwAFDgIwBAoQ4BAoMP8EHwfghk//AXBuEMv38n+AjEMvl8/4FDvoFBmEMvF994FBg04vgdBAoMAAot4AoNgAoPwAoZFBAongAoPggyIBAoPAg0HwAFDh4BBAoUeh0PwOAg08AocDv/+Ao3DAod//a3BAorBDAohRBgf+AocBAokApgCBhzSCWIkHVYgYCWIngYwQrB/gFDgF//AFDD4QAD8AFEAAIA="))
|
|
@ -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();
|
|
@ -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"}
|
After Width: | Height: | Size: 646 B |
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Fixed issue with wrong device informations
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwgJGhGAEKuIxAXXGCoXBGCoXCDCgXDJKYXDGCYUBhAwUFgQwPEogTCGBwNFFYYYNHwoEGJJQlFCIgKCdR4XHJBQNEI6IOFO6IPEDQYGDahoYEa6BJFxBFPJJIuQGAouRGAoWSGAgXTSIoAEgUgL6cCkQACDJCOFGAYWDAAJFLX4gWFGA4sFC40gJQYuHwBEDAQISCMYowEFgoJDCAwYBAwZYEC45AEgIHERAgXMA4i4FC6bPDC4hXFC5B7FC57CHI54XIawgXRVwS/JC5SuDC4wGGC45HBFAQRCAooXIVwYRBAAoXLLIwAFC5IuDGCIuFDAyQLABphKABgwaC6owB"))
|
|
@ -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();
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -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
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
```
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Create BuffGym app
|
||||
0.02: Add web interface for personalising workout
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### Recording your training
|
||||
|
||||
You will now begin moving through the exercises in the workout. You will see the exercise information on the display.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
### Workout completed
|
||||
|
||||
Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are.
|
||||
|
||||

|
||||
|
||||
## 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.
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA="))
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.8 KiB |
|
@ -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--;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"title": "Workout A",
|
||||
"file": "buffgym-workout-a.json"
|
||||
},
|
||||
{
|
||||
"title": "Workout B",
|
||||
"file": "buffgym-workout-b.json"
|
||||
}
|
||||
]
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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>
|
After Width: | Height: | Size: 7.4 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: fix precision rounding issue + no reset when equals pressed
|
|
@ -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>
|
|
@ -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});
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44="))
|
After Width: | Height: | Size: 10 KiB |
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
0.01: Basic calendar
|
|
@ -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
|
|
@ -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=="
|
||||
)
|
||||
)
|
|
@ -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" });
|
After Width: | Height: | Size: 540 B |
|
@ -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
|
|
@ -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/
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwIFCn/8BYYFRABcD4AFFgIFCh/wgeAAoP//8HCYMDAoPD8EAg4FB8PwgEf+EP/H4HQOAgP8uEAvwfBv0ggBFCn4CB/EBwEfgEB+AFBh+AgfgAoI1BIoQJB4AHBAoXgg4uBAIIFCCYQFGh5rDJQJUBK4IFCNYIFVDoopDGoJiBHYYFKVYRZBWIYDBA4IFBNIQzBG4IbBToKkBAQKVFUIYICVoQUCXIQmCYoIsCaITqDAoLvDNYUAA="))
|
|
@ -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();
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -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();
|
||||
})();
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Show text if uncalibrated
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("l8wxH+AH4A/AH4A/AFUvl8Cu4AEgUCBQIrfFQMRAAe/Aw4xbDYIlBiUS7AjCAAY5BBYMSiJkBGC4sCicTiRQJHoUSCAIwBF6sv30SikUiRMMMIISD7AvTl/YiYtPF40TF6R4BicVFqAWDF4MViaPRIwQWTF4O/IwiKRCoMRUiZHEDJ5cXJAxeOOQuQhQuShWQJIe/JJkviIuC74tTFwORRqKLD+3cmVLpsLFZtNAANKhXeDYKNOu4uEmdlDwVNBoNlsoDDmoKBhYQChcyFycVFwOTFwJcBpomBhYjCmouBAwYMCmZdBa4d3FyonBKoIoCAwIECLooucEIIjCRIYuFms1Lqq7CFwS7DLQQsDhYrBHIZdHXZkCdQpQDXoIQDFwIDBeoQQCpYuSl8RFwMT70KCRYAIhUSFwMTiMvFxm/CQUSFyp5Did3Fxi8DOBwuLDSEv7ETfoRCNDI13DIMT34ZPIYSgOaxJ3SIgZeTC7COBdgMCC58vOoakWiQvQFoQTBFqgvEiURF5gRDOKIdIDwMRiO/axMCBoMRLQItXF4Z9B7F3BxF37BZBAAQnRIYobDMAKqIl5aDAA5zJFwaCBAA6PBFxQQEAAYKBFxjSCU4IECA4YuJCAoAEFx0UikTAAIEBAwQuKCIoADFxsCI5RdiUAoAEVgIVJABRDHAH4A/AH4A/ADAA="))
|
|
@ -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);
|
After Width: | Height: | Size: 15 KiB |
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|