1
0
Fork 0

Merge pull request #4 from espruino/master

update 20200420
master
ps-igel 2020-04-20 22:18:15 +02:00 committed by GitHub
commit f0ef8b2b57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 2891 additions and 328 deletions

View File

@ -202,6 +202,11 @@ and which gives information about the app for the Launcher.
"files:"file1,file2,file3", "files:"file1,file2,file3",
// added by BangleApps loader on upload - lists all files // added by BangleApps loader on upload - lists all files
// that belong to the app so it can be deleted // 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 "evaluate":true // if supplied, data isn't quoted into a String before upload
// (eg it's evaluated as JS) // (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. "sortorder" : 0, // optional - choose where in the list this goes.
// this should only really be used to put system // this should only really be used to put system
// stuff at the top // stuff at the top
]
} }
``` ```
* name, icon and description present the app in the app loader. * 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. * 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 * 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 ### `apps.json`: `custom` element
@ -335,10 +348,10 @@ Example `settings.js`
```js ```js
// make sure to enclose the function in parentheses // make sure to enclose the function in parentheses
(function(back) { (function(back) {
let settings = require('Storage').readJSON('app.settings.json',1)||{}; let settings = require('Storage').readJSON('app.json',1)||{};
function save(key, value) { function save(key, value) {
settings[key] = value; settings[key] = value;
require('Storage').write('app.settings.json',settings); require('Storage').write('app.json',settings);
} }
const appMenu = { const appMenu = {
'': {'title': 'App Settings'}, '': {'title': 'App Settings'},
@ -351,19 +364,20 @@ Example `settings.js`
E.showMenu(appMenu) E.showMenu(appMenu)
}) })
``` ```
In this example the app needs to add both `app.settings.js` and In this example the app needs to add `app.settings.js` to `storage` in `apps.json`.
`app.settings.json` to `apps.json`: It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled.
```json ```json
{ "id": "app", { "id": "app",
... ...
"storage": [ "storage": [
... ...
{"name":"app.settings.js","url":"settings.js"}, {"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 ## Coding hints

208
apps.json
View File

@ -41,7 +41,7 @@
"name": "Default Launcher", "name": "Default Launcher",
"shortName":"Launcher", "shortName":"Launcher",
"icon": "app.png", "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.", "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", "tags": "tool,system,launcher",
"type":"launch", "type":"launch",
@ -78,7 +78,7 @@
{ "id": "welcome", { "id": "welcome",
"name": "Welcome", "name": "Welcome",
"icon": "app.png", "icon": "app.png",
"version":"0.07", "version":"0.08",
"description": "Appears at first boot and explains how to use Bangle.js", "description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome", "tags": "start,welcome",
"allow_emulator":true, "allow_emulator":true,
@ -86,14 +86,16 @@
{"name":"welcome.boot.js","url":"boot.js"}, {"name":"welcome.boot.js","url":"boot.js"},
{"name":"welcome.app.js","url":"app.js"}, {"name":"welcome.app.js","url":"app.js"},
{"name":"welcome.settings.js","url":"settings.js"}, {"name":"welcome.settings.js","url":"settings.js"},
{"name":"welcome.settings.json","url":"settings-default.json","evaluate":true},
{"name":"welcome.img","url":"app-icon.js","evaluate":true} {"name":"welcome.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"welcome.json"}
] ]
}, },
{ "id": "gbridge", { "id": "gbridge",
"name": "Gadgetbridge", "name": "Gadgetbridge",
"icon": "app.png", "icon": "app.png",
"version":"0.09", "version":"0.10",
"description": "The default notification handler for Gadgetbridge notifications from Android", "description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget", "tags": "tool,system,android,widget",
"type":"widget", "type":"widget",
@ -120,13 +122,12 @@
{ "id": "setting", { "id": "setting",
"name": "Settings", "name": "Settings",
"icon": "settings.png", "icon": "settings.png",
"version":"0.15", "version":"0.18",
"description": "A menu for setting up Bangle.js", "description": "A menu for setting up Bangle.js",
"tags": "tool,system", "tags": "tool,system",
"storage": [ "storage": [
{"name":"setting.app.js","url":"settings.js"}, {"name":"setting.app.js","url":"settings.js"},
{"name":"setting.boot.js","url":"boot.js"}, {"name":"setting.boot.js","url":"boot.js"},
{"name":"setting.json","url":"settings-default.json","evaluate":true},
{"name":"setting.img","url":"settings-icon.js","evaluate":true} {"name":"setting.img","url":"settings-icon.js","evaluate":true}
], ],
"sortorder" : -2 "sortorder" : -2
@ -135,16 +136,18 @@
"name": "Default Alarm", "name": "Default Alarm",
"shortName":"Alarms", "shortName":"Alarms",
"icon": "app.png", "icon": "app.png",
"version":"0.06", "version":"0.07",
"description": "Set and respond to alarms", "description": "Set and respond to alarms",
"tags": "tool,alarm,widget", "tags": "tool,alarm,widget",
"storage": [ "storage": [
{"name":"alarm.app.js","url":"app.js"}, {"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.boot.js","url":"boot.js"}, {"name":"alarm.boot.js","url":"boot.js"},
{"name":"alarm.js","url":"alarm.js"}, {"name":"alarm.js","url":"alarm.js"},
{"name":"alarm.json","content":"[]"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true}, {"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"} {"name":"alarm.wid.js","url":"widget.js"}
],
"data": [
{"name":"alarm.json"}
] ]
}, },
{ "id": "wclock", { "id": "wclock",
@ -235,7 +238,7 @@
{ "id": "compass", { "id": "compass",
"name": "Compass", "name": "Compass",
"icon": "compass.png", "icon": "compass.png",
"version":"0.01", "version":"0.02",
"description": "Simple compass that points North", "description": "Simple compass that points North",
"tags": "tool,outdoors", "tags": "tool,outdoors",
"storage": [ "storage": [
@ -280,29 +283,47 @@
{ "id": "gpsrec", { "id": "gpsrec",
"name": "GPS Recorder", "name": "GPS Recorder",
"icon": "app.png", "icon": "app.png",
"version":"0.07", "version":"0.08",
"interface": "interface.html", "interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background", "description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget", "tags": "tool,outdoors,gps,widget",
"storage": [ "storage": [
{"name":"gpsrec.app.js","url":"app.js"}, {"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.img","url":"app-icon.js","evaluate":true},
{"name":"gpsrec.wid.js","url":"widget.js"} {"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",
"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", { "id": "heart",
"name": "Heart Rate Recorder", "name": "Heart Rate Recorder",
"icon": "app.png", "icon": "app.png",
"version":"0.01", "version":"0.02",
"interface": "interface.html", "interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background", "description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget", "tags": "tool,health,widget",
"storage": [ "storage": [
{"name":"heart.app.js","url":"app.js"}, {"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.img","url":"app-icon.js","evaluate":true},
{"name":"heart.wid.js","url":"widget.js"} {"name":"heart.wid.js","url":"widget.js"}
],
"data": [
{"name":"heart.json"},
{"wildcard":".heart?","storageFile": true}
] ]
}, },
{ "id": "slevel", { "id": "slevel",
@ -319,7 +340,7 @@
{ "id": "files", { "id": "files",
"name": "App Manager", "name": "App Manager",
"icon": "files.png", "icon": "files.png",
"version":"0.02", "version":"0.03",
"description": "Show currently installed apps, free space, and allow their deletion from the watch", "description": "Show currently installed apps, free space, and allow their deletion from the watch",
"tags": "tool,system,files", "tags": "tool,system,files",
"storage": [ "storage": [
@ -342,14 +363,16 @@
"name": "Battery Level Widget (with percentage)", "name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget", "shortName": "Battery Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.09", "version":"0.11",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery", "tags": "widget,battery",
"type":"widget", "type":"widget",
"storage": [ "storage": [
{"name":"widbatpc.wid.js","url":"widget.js"}, {"name":"widbatpc.wid.js","url":"widget.js"},
{"name":"widbatpc.settings.js","url":"settings.js"}, {"name":"widbatpc.settings.js","url":"settings.js"}
{"name":"widbatpc.settings.json","content": "{}"} ],
"data": [
{"name":"widbatpc.json"}
] ]
}, },
{ "id": "widbt", { "id": "widbt",
@ -411,7 +434,7 @@
{ "id": "swatch", { "id": "swatch",
"name": "Stopwatch", "name": "Stopwatch",
"icon": "stopwatch.png", "icon": "stopwatch.png",
"version":"0.05", "version":"0.06",
"interface": "interface.html", "interface": "interface.html",
"description": "Simple stopwatch with Lap Time logging to a JSON file", "description": "Simple stopwatch with Lap Time logging to a JSON file",
"tags": "health", "tags": "health",
@ -517,20 +540,22 @@
"id": "ncstart", "id": "ncstart",
"name": "NCEU Startup", "name": "NCEU Startup",
"icon": "start.png", "icon": "start.png",
"version":"0.04", "version":"0.05",
"description": "NodeConfEU 2019 'First Start' Sequence", "description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome", "tags": "start,welcome",
"storage": [ "storage": [
{"name":"ncstart.app.js","url":"start.js"}, {"name":"ncstart.app.js","url":"start.js"},
{"name":"ncstart.boot.js","url":"boot.js"}, {"name":"ncstart.boot.js","url":"boot.js"},
{"name":"ncstart.settings.js","url":"settings.js"}, {"name":"ncstart.settings.js","url":"settings.js"},
{"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true},
{"name":"ncstart.img","url":"start-icon.js","evaluate":true}, {"name":"ncstart.img","url":"start-icon.js","evaluate":true},
{"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
{"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
{"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true}, {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true},
{"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true}, {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true},
{"name":"nc-tf.img","url":"start-tf.js","evaluate":true} {"name":"nc-tf.img","url":"start-tf.js","evaluate":true}
],
"data": [
{"name":"ncstart.json"}
] ]
}, },
{ "id": "ncfrun", { "id": "ncfrun",
@ -890,7 +915,8 @@
{ "id": "wohrm", { "id": "wohrm",
"name": "Workout HRM", "name": "Workout HRM",
"icon": "app.png", "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.", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"tags": "hrm,workout", "tags": "hrm,workout",
"type": "app", "type": "app",
@ -1017,7 +1043,7 @@
{ "id": "astrocalc", { "id": "astrocalc",
"name": "Astrocalc", "name": "Astrocalc",
"icon": "astrocalc.png", "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.", "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", "tags": "app,sun,moon,cycles,tool,outdoors",
"allow_emulator":true, "allow_emulator":true,
@ -1050,7 +1076,7 @@
"name": "Touch Launcher", "name": "Touch Launcher",
"shortName":"Menu", "shortName":"Menu",
"icon": "app.png", "icon": "app.png",
"version":"0.05", "version":"0.06",
"description": "Touch enable left to right launcher.", "description": "Touch enable left to right launcher.",
"tags": "tool,system,launcher", "tags": "tool,system,launcher",
"type":"launch", "type":"launch",
@ -1123,20 +1149,35 @@
{"name":"openstmap.img","url":"app-icon.js","evaluate":true} {"name":"openstmap.img","url":"app-icon.js","evaluate":true}
] ]
}, },
{ "id": "activepedom", { "id": "activepedom",
"name": "Active Pedometer", "name": "Active Pedometer",
"shortName":"Active Pedometer", "shortName":"Active Pedometer",
"icon": "app.png", "icon": "app.png",
"version":"0.01", "version":"0.02",
"description": "Pedometer that filters out arm movement and displays a step goal progress.", "description": "Pedometer that filters out arm movement and displays a step goal progress.",
"tags": "outdoors,widget", "tags": "outdoors,widget",
"type":"widget", "type":"widget",
"readme": "README.md",
"storage": [ "storage": [
{"name":"activepedom.wid.js","url":"widget.js"}, {"name":"activepedom.wid.js","url":"widget.js"},
{"name":"activepedom.settings.js","url":"settings.js"}, {"name":"activepedom.settings.js","url":"settings.js"},
{"name":"activepedom.img","url":"app-icon.js","evaluate":true} {"name":"activepedom.img","url":"app-icon.js","evaluate":true}
] ]
}, },
{ "id": "chronowid",
"name": "Chrono Widget",
"shortName":"Chrono Widget",
"icon": "app.png",
"version":"0.02",
"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", { "id": "tabata",
"name": "Tabata", "name": "Tabata",
"shortName": "Tabata - Control High-Intensity Interval Training", "shortName": "Tabata - Control High-Intensity Interval Training",
@ -1178,7 +1219,8 @@
"name": "Battery Chart", "name": "Battery Chart",
"shortName":"Battery Chart", "shortName":"Battery Chart",
"icon": "app.png", "icon": "app.png",
"version":"0.07", "version":"0.08",
"readme": "README.md",
"description": "A widget and an app for recording and visualizing battery percentage over time.", "description": "A widget and an app for recording and visualizing battery percentage over time.",
"tags": "app,widget,battery,time,record,chart,tool", "tags": "app,widget,battery,time,record,chart,tool",
"storage": [ "storage": [
@ -1205,7 +1247,7 @@
"name": "Numerals Clock", "name": "Numerals Clock",
"shortName": "Numerals Clock", "shortName": "Numerals Clock",
"icon": "numerals.png", "icon": "numerals.png",
"version":"0.03", "version":"0.04",
"description": "A simple big numerals clock", "description": "A simple big numerals clock",
"tags": "numerals,clock", "tags": "numerals,clock",
"type":"clock", "type":"clock",
@ -1213,8 +1255,10 @@
"storage": [ "storage": [
{"name":"numerals.app.js","url":"numerals.app.js"}, {"name":"numerals.app.js","url":"numerals.app.js"},
{"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, {"name":"numerals.img","url":"numerals-icon.js","evaluate":true},
{"name":"numerals.settings.js","url":"numerals.settings.js"}, {"name":"numerals.settings.js","url":"numerals.settings.js"}
{"name":"numerals.json","url":"numerals-default.json","evaluate":true} ],
"data":[
{"name":"numerals.json"}
] ]
}, },
{ "id": "bledetect", { "id": "bledetect",
@ -1247,12 +1291,114 @@
"name": "Calculator", "name": "Calculator",
"shortName":"Calculator", "shortName":"Calculator",
"icon": "calculator.png", "icon": "calculator.png",
"version":"0.01", "version":"0.02",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"tags": "app,tool", "tags": "app,tool",
"storage": [ "storage": [
{"name":"calculator.app.js","url":"app.js"}, {"name":"calculator.app.js","url":"app.js"},
{"name":"calculator.img","url":"calculator-icon.js","evaluate":true} {"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": "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",
"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.01",
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
"tags": "tools",
"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}
]
} }
] ]

View File

@ -1 +1,2 @@
0.01: New Widget! 0.01: New Widget!
0.02: Distance calculation and display

View File

@ -1,4 +1,4 @@
# Improved pedometer # Active Pedometer
Pedometer that filters out arm movement and displays a step goal progress. Pedometer that filters out arm movement and displays a step goal progress.
I changed the step counting algorithm completely. I changed the step counting algorithm completely.
@ -19,8 +19,9 @@ When you reach the step threshold, the steps needed to reach the threshold are c
## Features ## Features
* Two line display * Two line display
* Can display distance (in km) or steps in each line
* Large number for good readability * Large number for good readability
* Small number with the exact steps counted * Small number with the exact steps counted or more exact distance
* Large number is displayed in green when status is 'active' * Large number is displayed in green when status is 'active'
* Progress bar for step goal * Progress bar for step goal
* Counts steps only if they are reached in a certain time * Counts steps only if they are reached in a certain time
@ -29,9 +30,23 @@ When you reach the step threshold, the steps needed to reach the threshold are c
* Steps are saved to a file and read-in at start (to not lose step progress) * 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 * Settings can be changed in Settings - App/widget settings - Active Pedometer
## Development version ## Settings
* https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer * 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 ## Requests

View File

@ -4,6 +4,7 @@
*/ */
(function(back) { (function(back) {
const SETTINGS_FILE = 'activepedom.settings.json'; const SETTINGS_FILE = 'activepedom.settings.json';
const LINES = ['Steps', 'Distance'];
// initialize with default settings... // initialize with default settings...
let s = { let s = {
@ -13,6 +14,9 @@
'intervalResetActive' : 30000, 'intervalResetActive' : 30000,
'stepSensitivity' : 80, 'stepSensitivity' : 80,
'stepGoal' : 10000, 'stepGoal' : 10000,
'stepLength' : 75,
'lineOne': LINES[0],
'lineTwo': LINES[1],
}; };
// ...and overwrite them with any saved values // ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings // This way saved values are preserved if a new version adds more settings
@ -27,7 +31,7 @@
return function (value) { return function (value) {
s[key] = value; s[key] = value;
storage.write(SETTINGS_FILE, s); storage.write(SETTINGS_FILE, s);
WIDGETS["activepedom"].draw(); //WIDGETS["activepedom"].draw();
}; };
} }
@ -76,6 +80,33 @@
step: 1000, step: 1000,
onchange: save('stepGoal'), 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); E.showMenu(menu);
}); });

View File

@ -8,22 +8,16 @@
var active = 0; //x steps in y seconds achieved var active = 0; //x steps in y seconds achieved
var stepGoalPercent = 0; //percentage of step goal var stepGoalPercent = 0; //percentage of step goal
var stepGoalBarLength = 0; //length og progress bar var stepGoalBarLength = 0; //length og progress bar
var lastUpdate = new Date(); var lastUpdate = new Date(); //used to reset counted steps on new day
var width = 45; var width = 45; //width of widget
var stepsTooShort = 0; //used for statistics and debugging
var stepsTooShort = 0;
var stepsTooLong = 0; var stepsTooLong = 0;
var stepsOutsideTime = 0; var stepsOutsideTime = 0;
//define default settings var distance = 0; //distance travelled
const DEFAULTS = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
};
const SETTINGS_FILE = 'activepedom.settings.json'; const SETTINGS_FILE = 'activepedom.settings.json';
const PEDOMFILE = "activepedom.steps.json"; const PEDOMFILE = "activepedom.steps.json";
@ -32,10 +26,21 @@
function loadSettings() { function loadSettings() {
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
} }
//return setting //return setting
function setting(key) { function setting(key) {
if (!settings) { loadSettings(); } //define default settings
return (key in settings) ? settings[key] : DEFAULTS[key]; 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 setStepSensitivity(s) {
@ -46,7 +51,7 @@
} }
//format number to make them shorter //format number to make them shorter
function kFormatter(num) { function kFormatterSteps(num) {
if (num <= 999) return num; //smaller 1.000, return 600 as 600 if (num <= 999) return num; //smaller 1.000, return 600 as 600
if (num >= 1000 && num < 10000) { //between 1.000 and 10.000 if (num >= 1000 && num < 10000) { //between 1.000 and 10.000
num = Math.floor(num/100)*100; num = Math.floor(num/100)*100;
@ -99,11 +104,12 @@
else { else {
stepsOutsideTime++; stepsOutsideTime++;
} }
settings = 0; //reset settings to save memory
} }
function draw() { function draw() {
var height = 23; //width is deined globally var height = 23; //width is deined globally
var stepsDisplayLarge = kFormatter(stepsCounted); distance = (stepsCounted * setting('stepLength')) / 100 /1000 //distance in km
//Check if same day //Check if same day
let date = new Date(); let date = new Date();
@ -121,10 +127,21 @@
if (active == 1) g.setColor(0x07E0); //green if (active == 1) g.setColor(0x07E0); //green
else g.setColor(0xFFFF); //white else g.setColor(0xFFFF); //white
g.setFont("6x8", 2); g.setFont("6x8", 2);
g.drawString(stepsDisplayLarge,this.x+1,this.y); //first line, big number
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.setFont("6x8", 1);
g.setColor(0xFFFF); //white g.setColor(0xFFFF); //white
g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number 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 //draw step goal bar
stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100; stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100;
@ -136,6 +153,8 @@
g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar 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+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 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() //This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off()
@ -164,6 +183,7 @@
//Read data from file and set variables //Read data from file and set variables
let pedomData = require("Storage").readJSON(PEDOMFILE,1); let pedomData = require("Storage").readJSON(PEDOMFILE,1);
if (pedomData) { if (pedomData) {
if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate);
stepsCounted = pedomData.stepsToday|0; stepsCounted = pedomData.stepsToday|0;
@ -172,6 +192,8 @@
stepsOutsideTime = pedomData.stepsOutsideTime; stepsOutsideTime = pedomData.stepsOutsideTime;
} }
pedomdata = 0; //reset pedomdata to save memory
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
//Add widget //Add widget

View File

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

View File

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

View File

@ -1,8 +1,18 @@
/** /**
* BangleJS ASTROCALC
*
* Inspired by: https://www.timeanddate.com * 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 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) { function drawMoon(phase, x, y) {
const moonImgFiles = [ const moonImgFiles = [
@ -296,22 +306,49 @@ function indexPageMenu(gps) {
return E.showMenu(menu); 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 * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page
*/ */
function drawGPSWaitPage() { 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.clear();
g.drawImage(img, 100, 50); g.drawImage(img, 100, 50);
g.setFont("6x8", 1); g.setFont("6x8", 1);
g.drawString("Astrocalc v0.01", 80, 105); g.drawString(str1, getCenterStringX(str1), 105);
g.drawString("Locating GPS", 85, 140); g.drawString(str2, getCenterStringX(str2), 140);
g.drawString("Please wait...", 80, 155); 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(); g.flip();
const DEBUG = false; const DEBUG = false;
if (DEBUG) { if (DEBUG) {
clearWatch();
const gps = { const gps = {
"lat": 56.45783133333, "lat": 56.45783133333,
"lon": -3.02188583333, "lon": -3.02188583333,
@ -330,7 +367,10 @@ function drawGPSWaitPage() {
Bangle.on('GPS', (gps) => { Bangle.on('GPS', (gps) => {
if (gps.fix === 0) return; 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.setGPSPower(0);
Bangle.buzz(); Bangle.buzz();
Bangle.setLCDPower(true); Bangle.setLCDPower(true);

1
apps/banglerun/ChangeLog Executable file
View File

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

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -4,4 +4,5 @@
0.04: chart in the app is now active. 0.04: chart in the app is now active.
0.05: Display temperature and LCD state in chart 0.05: Display temperature and LCD state in chart
0.06: Fixes widget events and charting of component states 0.06: Fixes widget events and charting of component states
0.07: Improve logging and charting of component states and add widget icon 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.

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

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

View File

@ -65,9 +65,7 @@ function loadData() {
// Top up to MaxValueCount from previous days as required // Top up to MaxValueCount from previous days as required
let previousDay = decrementDay(startingDay); let previousDay = decrementDay(startingDay);
while (dataLines.length < MaxValueCount while (dataLines.length < MaxValueCount && previousDay !== startingDay) {
&& previousDay !== startingDay) {
let topUpLogFileName = "bclog" + previousDay; let topUpLogFileName = "bclog" + previousDay;
let remainingLines = MaxValueCount - dataLines.length; let remainingLines = MaxValueCount - dataLines.length;
let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName);
@ -126,6 +124,12 @@ function renderData(dataArray) {
const batteryIndex = 1; const batteryIndex = 1;
const temperatureIndex = 2; const temperatureIndex = 2;
const switchabelsIndex = 3; 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; var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm;
@ -140,8 +144,19 @@ function renderData(dataArray) {
// Temperature // Temperature
g.setColor(0.4, 0.4, 1); g.setColor(0.4, 0.4, 1);
let scaledTemp = Math.floor(((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000)/20) + ((((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000) % 100)/25);
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); g.setPixel(GraphXZero + i, GraphYZero - scaledTemp);
// LCD state // LCD state
@ -213,8 +228,6 @@ function switchOffApp(){
// special function to handle display switch on // special function to handle display switch on
Bangle.on('lcdPower', (on) => { Bangle.on('lcdPower', (on) => {
if (on) { if (on) {
// call your app function here
// If you clear the screen, do Bangle.drawWidgets();
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
@ -222,12 +235,11 @@ Bangle.on('lcdPower', (on) => {
} }
}); });
setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true});
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
// call your app function here
renderHomeIcon(); renderHomeIcon();

View File

@ -12,8 +12,8 @@
var batChartFile; // file for battery percentage recording var batChartFile; // file for battery percentage recording
const recordingInterval10Min = 60 * 10 * 1000; const recordingInterval10Min = 60 * 10 * 1000;
const recordingInterval1Min = 60*1000; //For testing const recordingInterval1Min = 60 * 1000; //For testing
const recordingInterval10S = 10*1000; //For testing const recordingInterval10S = 10 * 1000; //For testing
var recordingInterval = null; var recordingInterval = null;
var compassEventReceived = false; var compassEventReceived = false;
@ -26,15 +26,15 @@
let y = this.y; let y = this.y;
g.setColor(0, 1, 0); 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.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.setColor(0, 0, 0);
g.drawPoly([x+5, y+6, x+8, y+12, x+13, y+12, x+16, y+18], false); g.drawPoly([x + 5, y + 6, x + 8, y + 12, x + 13, y + 12, x + 16, y + 18], false);
g.reset(); g.reset();
} }
function onMag(){ function onMag() {
compassEventReceived = true; compassEventReceived = true;
// Stop handling events when no longer necessarry // Stop handling events when no longer necessarry
Bangle.removeListener("mag", onMag); Bangle.removeListener("mag", onMag);
@ -51,6 +51,7 @@
} }
function getEnabledConsumersValue() { function getEnabledConsumersValue() {
// Wait for an event from each of the devices to see if they are switched on
var enabledConsumers = switchableConsumers.none; var enabledConsumers = switchableConsumers.none;
Bangle.on('mag', onMag); Bangle.on('mag', onMag);
@ -58,13 +59,12 @@
Bangle.on('HRM', onHrm); Bangle.on('HRM', onHrm);
// Wait two seconds, that should be enough for each of the events to get raised once // Wait two seconds, that should be enough for each of the events to get raised once
setTimeout(() => { setTimeout(() => {
Bangle.removeAllListeners(); Bangle.removeAllListeners();
}, 2000); }, 2000);
if (Bangle.isLCDOn()) if (Bangle.isLCDOn())
enabledConsumers = enabledConsumers | switchableConsumers.lcd; enabledConsumers = enabledConsumers | switchableConsumers.lcd;
// Already added in the hope they will be available soon to get more details
if (compassEventReceived) if (compassEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.compass; enabledConsumers = enabledConsumers | switchableConsumers.compass;
if (gpsEventReceived) if (gpsEventReceived)
@ -90,8 +90,7 @@
const logFileName = "bclog" + currentWriteDay; const logFileName = "bclog" + currentWriteDay;
// Change log target on day change // Change log target on day change
if (!isNaN(previousWriteDay) if (!isNaN(previousWriteDay) && previousWriteDay != currentWriteDay) {
&& previousWriteDay != currentWriteDay) {
//Remove a log file containing data from a week ago //Remove a log file containing data from a week ago
Storage.open(logFileName, "r").erase(); Storage.open(logFileName, "r").erase();
Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay)); Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay));
@ -111,7 +110,7 @@
} }
function reload() { function reload() {
WIDGETS["batchart"].width = 24; WIDGETS.batchart.width = 24;
recordingInterval = setInterval(logBatteryData, recordingInterval10Min); recordingInterval = setInterval(logBatteryData, recordingInterval10Min);
@ -119,11 +118,12 @@
} }
// add the widget // add the widget
WIDGETS["batchart"]={area:"tl",width:24,draw:draw,reload:function() { WIDGETS.batchart = {
reload(); area: "tl", width: 24, draw: draw, reload: function () {
Bangle.drawWidgets(); // relayout all widgets reload();
}}; Bangle.drawWidgets();
}
};
// load settings, set correct widget width
reload(); reload();
})() })();

1
apps/blackjack/ChangeLog Normal file
View File

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

View File

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

View File

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

Binary file not shown.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

View File

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

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

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

View File

@ -144,19 +144,57 @@ function drawKey(name, k, selected) {
g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); 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) { function doMath(x, y, operator) {
// might not be a number due to display of dot "." algo
x = Number(x);
y = Number(y);
switch (operator) { switch (operator) {
case '/': case '/':
return x / y; return divide(x, y);
case '*': case '*':
return x * y; return multiply(x, y);
case '+': case '+':
return x + y; return sum(x, y);
case '-': case '-':
return x - y; return subtract(x, y);
} }
} }
@ -204,7 +242,7 @@ function displayOutput(num) {
} }
len = (num + '').length; len = (num + '').length;
if (numNumeric < 0) { if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) {
// minus is not available in font 7x11Numeric7Seg, we use Vector // minus is not available in font 7x11Numeric7Seg, we use Vector
g.setFont('Vector', 20); g.setFont('Vector', 20);
g.drawString('-', 220 - (len * 15), 10); g.drawString('-', 220 - (len * 15), 10);
@ -214,18 +252,33 @@ function displayOutput(num) {
} }
g.drawString(num, 220 - (len * 15) + minusMarge, 10); g.drawString(num, 220 - (len * 15) + minusMarge, 10);
} }
var wasPressedEquals = false;
var hasPressedNumber = false;
function calculatorLogic(x) { function calculatorLogic(x) {
if (hasPressedEquals) { if (wasPressedEquals && hasPressedNumber !== false) {
currNumber = results;
prevNumber = null; prevNumber = null;
operator = null; currNumber = hasPressedNumber;
results = null; wasPressedEquals = false;
isDecimal = null; hasPressedNumber = false;
displayOutput(currNumber); return;
hasPressedEquals = false;
} }
if (prevNumber != null && currNumber != null && operator != null) { 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 // we execute the calculus only when there was a previous number entered before and an operator
results = doMath(prevNumber, currNumber, operator); results = doMath(prevNumber, currNumber, operator);
operator = x; operator = x;
@ -255,8 +308,10 @@ function buttonPress(val) {
operator = null; operator = null;
} else { } else {
keys.R.val = 'AC'; keys.R.val = 'AC';
drawKey('R', keys.R); drawKey('R', keys.R, true);
} }
wasPressedEquals = false;
hasPressedNumber = false;
displayOutput(0); displayOutput(0);
break; break;
case '%': case '%':
@ -265,11 +320,12 @@ function buttonPress(val) {
} else if (currNumber != null) { } else if (currNumber != null) {
displayOutput(currNumber /= 100); displayOutput(currNumber /= 100);
} }
hasPressedNumber = false;
break; break;
case 'N': case 'N':
if (results != null) { if (results != null) {
displayOutput(results *= -1); displayOutput(results *= -1);
} else if (currNumber != null) { } else {
displayOutput(currNumber *= -1); displayOutput(currNumber *= -1);
} }
break; break;
@ -278,6 +334,7 @@ function buttonPress(val) {
case '-': case '-':
case '+': case '+':
calculatorLogic(val); calculatorLogic(val);
hasPressedNumber = false;
break; break;
case '.': case '.':
keys.R.val = 'C'; keys.R.val = 'C';
@ -290,18 +347,24 @@ function buttonPress(val) {
results = doMath(prevNumber, currNumber, operator); results = doMath(prevNumber, currNumber, operator);
prevNumber = results; prevNumber = results;
displayOutput(results); displayOutput(results);
hasPressedEquals = true; hasPressedEquals = 1;
} }
hasPressedNumber = false;
break; break;
default: default:
keys.R.val = 'C'; keys.R.val = 'C';
drawKey('R', keys.R); drawKey('R', keys.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
if (isDecimal) { if (isDecimal) {
currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val; currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
isDecimal = false; isDecimal = false;
} else { } else {
currNumber = currNumber == null ? val : currNumber + val; currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val);
} }
if (hasPressedEquals === 1) {
hasPressedEquals = 2;
}
hasPressedNumber = currNumber;
displayOutput(currNumber); displayOutput(currNumber);
break; break;
} }
@ -315,38 +378,15 @@ for (var k in keys) {
g.setFont('7x11Numeric7Seg', 2.8); g.setFont('7x11Numeric7Seg', 2.8);
g.drawString('0', 205, 10); g.drawString('0', 205, 10);
function moveDirection(d) {
setWatch(function() {
drawKey(selected, keys[selected]);
// key 0 is 2 keys wide, go up to 1 if it was previously selected
if (selected == '0' && prevSelected === '1') {
prevSelected = selected;
selected = '1';
} else {
prevSelected = selected;
selected = keys[selected].trbl[0];
}
drawKey(selected, keys[selected], true);
}, BTN1, {repeat: true, debounce: 100});
setWatch(function() {
drawKey(selected, keys[selected]); drawKey(selected, keys[selected]);
prevSelected = selected; prevSelected = selected;
selected = keys[selected].trbl[2]; selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d];
drawKey(selected, keys[selected], true); drawKey(selected, keys[selected], true);
}, BTN3, {repeat: true, debounce: 100}); }
Bangle.on('touch', function(direction) { setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
drawKey(selected, keys[selected]); setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
prevSelected = selected; setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
if (direction == 1) { setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
selected = keys[selected].trbl[3]; setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
} else if (direction == 2) {
selected = keys[selected].trbl[1];
}
drawKey(selected, keys[selected], true);
});
setWatch(function() {
buttonPress(selected);
}, BTN2, {repeat: true, debounce: 100});

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

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

2
apps/chronowid/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New widget and app!
0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme)

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

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

View File

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

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

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

BIN
apps/chronowid/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

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

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

2
apps/compass/ChangeLog Normal file
View File

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

View File

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

5
apps/dane/ChangeLog Normal file
View File

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

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

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

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

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

BIN
apps/dane/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

@ -30,29 +30,80 @@ function showMainMenu() {
return E.showMenu(mainmenu); return E.showMenu(mainmenu);
} }
function eraseApp(app) { function isGlob(f) {return /[?*]/.test(f)}
E.showMessage('Erasing\n' + app.name + '...'); 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)); 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) { function showAppMenu(app) {
const appmenu = { let appmenu = {
'': { '': {
'title': app.name, 'title': app.name,
}, },
'< Back': () => m = showApps(), '< Back': () => m = showApps(),
'Erase': () => { }
E.showPrompt('Erase\n' + app.name + '?').then((v) => { if (app.data) {
if (v) { appmenu['Erase Completely'] = () => eraseOne(app, true, true)
Bangle.buzz(100, 1); appmenu['Erase App,Keep Data'] = () => eraseOne(app,true, false)
eraseApp(app); appmenu['Only Erase Data'] = () => eraseOne(app,false, true)
m = showApps(); } else {
} else { appmenu['Erase'] = () => eraseOne(app,true, false)
m = showAppMenu(app) }
}
});
}
};
return E.showMenu(appmenu); return E.showMenu(appmenu);
} }
@ -78,13 +129,12 @@ function showApps() {
return menu; return menu;
}, appsmenu); }, appsmenu);
appsmenu['Erase All'] = () => { appsmenu['Erase All'] = () => {
E.showPrompt('Erase all?').then((v) => { E.showMenu({
if (v) { '': {'title': 'Erase All'},
Bangle.buzz(100, 1); 'Erase Everything': () => eraseAll(list, true, true),
for (var n = 0; n < list.length; n++) 'Erase Apps,Keep Data': () => eraseAll(list, true, false),
eraseApp(list[n]); 'Only Erase Data': () => eraseAll(list, false, true),
} '< Back': () => showApps(),
m = showApps();
}); });
}; };
} else { } else {

View File

@ -8,3 +8,4 @@
0.07: Move configuration to settings menu 0.07: Move configuration to settings menu
0.08: Don't turn on LCD at start of every song 0.08: Don't turn on LCD at start of every song
0.09: Update Bluetooth connection state automatically 0.09: Update Bluetooth connection state automatically
0.10: Make widget play well with other Gadgetbridge widgets/apps

View File

@ -145,6 +145,7 @@
} }
} }
var _GB = global.GB;
global.GB = (event) => { global.GB = (event) => {
switch (event.t) { switch (event.t) {
case "notify": case "notify":
@ -160,6 +161,7 @@
handleCallEvent(event); handleCallEvent(event);
break; break;
} }
if(_GB)setTimeout(_GB,0,event);
}; };
// Touch control // Touch control

1
apps/gpsnav/ChangeLog Normal file
View File

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

66
apps/gpsnav/README.md Normal file
View File

@ -0,0 +1,66 @@
## gpsnav - navigate to waypoints
The app is aimed at small boat navigation although it can also be used to mark the location of your car, bicycle etc and then get directions back to it. Please note that it would be foolish in the extreme to rely on this as your only boat navigation aid!
The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix.
![](first_screen.jpg)
The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn white. Then use BTN1 and BTN3 to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected.
![](waypoint_screen.jpg)
The display shows that Stone Henge is 108.75Km from the location where I made the screenshot and the direction is 255 degrees - approximately west. The display shows that I am currently moving approximately north - albeit slowly!. The position of the blue circle indicates that I need to turn left to get on course to Stone Henge. When the circle and red triangle line up you are on course and course will equal bearing.
### Marking Waypoints
The app lets you mark your current location as follows. There are vacant slots in the waypoint file which can be allocated a location. In the distributed waypoint file these are labelled WP0 to WP4. Select one of these - WP2 is shown below.
![](select_screen.jpg)
Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2.
![](marked_screen.jpg)
The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot as you have just marked your current location.
### Waypoint JSON file
When the app is loaded from the app loader, a file named waypoints.json is loaded along with the javascript etc. The file has the following contents:
~~~
[
{
"mark":0,
"name":"NONE"
},
{
"mark":1,
"name":"No10",
"lat":51.5032,
"lon":-0.1269
},
{
"mark":1,
"name":"Stone",
"lat":51.1788,
"lon":-1.8260
},
{ "name":"WP0" },
{ "name":"WP1" },
{ "name":"WP2" },
{ "name":"WP3" },
{ "name":"WP4" }
]
~~~
The file contains the initial NONE waypoint which is useful if you just want to display course and speed. The next two entries are waypoints to No 10 Downing Street and to Stone Henge - obtained from Google Maps. The last five entries are entries which can be *marked*.
You add and delete entries using the Web IDE to load and then save the file from and to watch storage. The app itself does not limit the number of entries although it does load the entire file into RAM which will obviously limit this.
I plan to release an accompanying watch app to edit waypoint files in the near future and a way to download your own waypoint file using the app loader.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AFmACysDC9+IC6szC/8AgUgLwYXBPAgLDAA8kC5MyC5cyogXHmYiDURMkDAMzC4JgBmcyoAXMGANCC4YDBkgXMHwVEC4hQDC5kyF4kjJ4QAMOgMjC4eCohGNMARbCC4ODkilLAAQSBCYJ3EmYVLhAWCCgQaCAAUwCpowCFwYADIRAYHC4wZFRQIAGnAhJXgwAFxAYHwC9JFwiQCFhIZISAQwDX5sCoQTCDYUjUpAAFglElAXDmS9JAAtEoUyC4ckkbvMC4QQBC4YeBC5sEB4IXEkgfBJBkEH4QXCCYMkoQXMHwcIC4ZQCUpYMDC4oiBC5YEDC40AkCRNAAIXBCJ4X2URgAJhAXvCyoA/ACoA="))

224
apps/gpsnav/app.js Normal file
View File

@ -0,0 +1,224 @@
const Yoff = 40;
var pal2color = new Uint16Array([0x0000,0xffff,0x07ff,0xC618],0,2);
var buf = Graphics.createArrayBuffer(240,50,2,{msb:true});
function flip(b,y) {
g.drawImage({width:240,height:50,bpp:2,buffer:b.buffer, palette:pal2color},0,y);
b.clear();
}
var brg=0;
var wpindex=0;
const labels = ["N","NE","E","SE","S","SW","W","NW"];
function drawCompass(course) {
buf.setColor(1);
buf.setFont("Vector",16);
var start = course-90;
if (start<0) start+=360;
buf.fillRect(28,45,212,49);
var xpos = 30;
var frag = 15 - start%15;
if (frag<15) xpos+=frag; else frag = 0;
for (var i=frag;i<=180-frag;i+=15){
var res = start + i;
if (res%90==0) {
buf.drawString(labels[Math.floor(res/45)%8],xpos-8,0);
buf.fillRect(xpos-2,25,xpos+2,45);
} else if (res%45==0) {
buf.drawString(labels[Math.floor(res/45)%8],xpos-12,0);
buf.fillRect(xpos-2,30,xpos+2,45);
} else if (res%15==0) {
buf.fillRect(xpos,35,xpos+1,45);
}
xpos+=15;
}
if (wpindex!=0) {
var bpos = brg - course;
if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360;
bpos+=120;
if (bpos<30) bpos = 14;
if (bpos>210) bpos = 226;
buf.setColor(2);
buf.fillCircle(bpos,40,8);
}
flip(buf,Yoff);
}
//displayed heading
var heading = 0;
function newHeading(m,h){
var s = Math.abs(m - h);
var delta = 1;
if (s<2) return h;
if (m > h){
if (s >= 180) { delta = -1; s = 360 - s;}
} else if (m <= h){
if (s < 180) delta = -1;
else s = 360 -s;
}
delta = delta * (1 + Math.round(s/15));
heading+=delta;
if (heading<0) heading += 360;
if (heading>360) heading -= 360;
return heading;
}
var course =0;
var speed = 0;
var satellites = 0;
var wp;
var dist=0;
function radians(a) {
return a*Math.PI/180;
}
function degrees(a) {
var d = a*180/Math.PI;
return (d+360)%360;
}
function bearing(a,b){
var delta = radians(b.lon-a.lon);
var alat = radians(a.lat);
var blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
}
function distance(a,b){
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
}
var selected = false;
function drawN(){
buf.setColor(1);
buf.setFont("6x8",2);
buf.drawString("o",100,0);
buf.setFont("6x8",1);
buf.drawString("kph",220,40);
buf.setFont("Vector",40);
var cs = course.toString();
cs = course<10?"00"+cs : course<100 ?"0"+cs : cs;
buf.drawString(cs,10,0);
var txt = (speed<10) ? speed.toFixed(1) : Math.round(speed);
buf.drawString(txt,140,4);
flip(buf,Yoff+70);
buf.setColor(1);
buf.setFont("Vector",20);
var bs = brg.toString();
bs = brg<10?"00"+bs : brg<100 ?"0"+bs : bs;
buf.setColor(3);
buf.drawString("Brg: ",0,0);
buf.drawString("Dist: ",0,30);
buf.setColor(selected?1:2);
buf.drawString(wp.name,140,0);
buf.setColor(1);
buf.drawString(bs,60,0);
if (dist<1000)
buf.drawString(dist.toString()+"m",60,30);
else
buf.drawString((dist/1000).toFixed(2)+"Km",60,30);
flip(buf,Yoff+130);
g.setFont("6x8",1);
g.setColor(0,0,0);
g.fillRect(10,230,60,239);
g.setColor(1,1,1);
g.drawString("Sats " + satellites.toString(),10,230);
}
var savedfix;
function onGPS(fix) {
savedfix = fix;
if (fix!==undefined){
course = isNaN(fix.course) ? course : Math.round(fix.course);
speed = isNaN(fix.speed) ? speed : fix.speed;
satellites = fix.satellites;
}
if (Bangle.isLCDOn()) {
if (fix!==undefined && fix.fix==1){
dist = distance(fix,wp);
if (isNaN(dist)) dist = 0;
brg = bearing(fix,wp);
if (isNaN(brg)) brg = 0;
}
drawN();
}
}
var intervalRef;
function clearTimers() {
if(intervalRef) {clearInterval(intervalRef);}
}
function startTimers() {
intervalRefSec = setInterval(function() {
newHeading(course,heading);
if (course!=heading) drawCompass(heading);
},200);
}
Bangle.on('lcdPower',function(on) {
if (on) {
g.clear();
Bangle.drawWidgets();
startTimers();
drawAll();
}else {
clearTimers();
}
});
function drawAll(){
g.setColor(1,0.5,0.5);
g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]);
g.setColor(1,1,1);
drawN();
drawCompass(heading);
}
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
wp=waypoints[0];
function nextwp(inc){
if (!selected) return;
wpindex+=inc;
if (wpindex>=waypoints.length) wpindex=0;
if (wpindex<0) wpindex = waypoints.length-1;
wp = waypoints[wpindex];
drawN();
}
function doselect(){
if (selected && waypoints[wpindex].mark===undefined && savedfix.fix) {
waypoints[wpindex] ={mark:1, name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon};
wp = waypoints[wpindex];
require("Storage").writeJSON("waypoints.json", waypoints);
}
selected=!selected;
drawN();
}
g.clear();
Bangle.setLCDBrightness(1);
Bangle.loadWidgets();
Bangle.drawWidgets();
// load widgets can turn off GPS
Bangle.setGPSPower(1);
drawAll();
startTimers();
Bangle.on('GPS', onGPS);
// Toggle selected
setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"});

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
apps/gpsnav/gpsnav.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
apps/gpsnav/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,23 @@
[
{
"mark":0,
"name":"NONE"
},
{
"mark":1,
"name":"No10",
"lat":51.5032,
"lon":-0.1269
},
{
"mark":1,
"name":"Stone",
"lat":51.1788,
"lon":-1.8260
},
{ "name":"WP0" },
{ "name":"WP1" },
{ "name":"WP2" },
{ "name":"WP3" },
{ "name":"WP4" }
]

View File

@ -5,3 +5,5 @@
0.05: Tweaks for variable size widget system 0.05: Tweaks for variable size widget system
0.06: Ensure widget update itself (fix #118) and change to using icons 0.06: Ensure widget update itself (fix #118) and change to using icons
0.07: Added @jeffmer's awesome track viewer 0.07: Added @jeffmer's awesome track viewer
0.08: Don't overwrite existing settings on app update
Clean up recorded tracks on app removal

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Don't overwrite existing settings on app update
Clean up recordings on app removal

1
apps/hidcam/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Core functionnality

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

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDCEAzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMxERERERETMzMzMzMzMzMzMzMzMzMzMzMRERERERERMzMzMzMzMzMzMzMzMzMzMzMREREREREREzMzMzMzMzMzMzMzMAAAAzEREREREREREzMzMzMzMzMzMzMzMAAAAxERERERERERETMzMzMzMzMzMzMxERERERERERERERERERERERETMzMzMzMRERERERERERERERERERERERERMzMzMzEREREREREREAAAAAEREREREREREzMzMzEREREREREQAAAAAAABERESIiIREzMzMzEREREREREAAAAAAAAAERESIiIREzMzMzEREREREQAAAKqqqgAAABESIiIREzMzMzEREREREQAAqqqqqqoAABESIiIREzMzMzEREREREAAKqqqqqqqgAAEREREREzMzMzERERERAACqqqqqqqqqAAAREREREzMzMzERERERAAqqqiIiIqqqoAAREREREzMzMzqqqqqgAAqqoiIiIiKqoAAKqqqqozMzMzqqqqqgAKqqIiIiIiKqqgAKqqqqozMzMzqqqqqgAKqqIiqqqiKqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAAqqqqqqqqqqoAAKqqqqozMzMzqqqqqqAAqqqqqqqqqqoACqqqqqozMzMzqqqqqqAACqqqqqqqqqAACqqqqqozMzMzqqqqqqoAAKqqqqqqqgAAqqqqqqozMzMzqqqqqqoAAAqqqqqqoAAAqqqqqqozMzMzqqqqqqqgAAAKqqqgAAAKqqqqqqozMzMzqqqqqqqqAAAAAAAAAACqqqqqqqozMzMzqqqqqqqqqgAAAAAAAKqqqqqqqqozMzMzqqqqqqqqqqoAAAAAqqqqqqqqqqozMzMzOqqqqqqqqqqqqqqqqqqqqqqqqqMzMzMzM6qqqqqqqqqqqqqqqqqqqqqqqjMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMw=="))

52
apps/hidcam/app.js Normal file
View File

@ -0,0 +1,52 @@
var storage = require('Storage');
const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, camShot, profile;
if (settings.HID) {
profile = 'camShutter';
sendHid = function (code, cb) {
try {
NRF.sendHIDReport([1,code], () => {
NRF.sendHIDReport([1,0], () => {
if (cb) cb();
});
});
} catch(e) {
print(e);
}
};
camShot = function (cb) { sendHid(0x80, cb); };
} else {
E.showMessage('HID disabled');
setTimeout(load, 1000);
}
function drawApp() {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.fillCircle(122,127,60);
g.drawImage(storage.read("hidcam.img"),100,105);
const d = g.getWidth() - 18;
function c(a) {
return {
width: 8,
height: a.length,
bpp: 1,
buffer: (new Uint8Array(a)).buffer
};
}
g.fillRect(180,130, 240, 124);
}
if (camShot) {
setWatch(function(e) {
E.showMessage('camShot !');
setTimeout(drawApp, 1000);
camShot(() => {});
}, BTN2, { edge:"falling",repeat:true,debounce:50});
drawApp();
}

BIN
apps/hidcam/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

2
apps/launch/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Only store relevant app data (saves RAM when many apps)

View File

@ -1,5 +1,5 @@
var s = require("Storage"); var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>s.readJSON(app,1)||{name:"DEAD: "+app.substr(1)}).filter(app=>app.type=="app" || app.type=="clock" || !app.type); var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src}}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
apps.sort((a,b)=>{ apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder); var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first if (n) return n; // do sortorder first

View File

@ -220,7 +220,7 @@ var locales = {
int_curr_symbol: "ILS", int_curr_symbol: "ILS",
speed: "kmh", speed: "kmh",
distance: { 0: "m", 1: "km" }, distance: { 0: "m", 1: "km" },
temperature: F", temperature: C",
ampm: { 0: "am", 1: "pm" }, ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "%A, %B %d, %Y", "1": "%d/%m/%Y" }, // Sunday, 1 March 2020 // 01/03/2020 datePattern: { 0: "%A, %B %d, %Y", "1": "%d/%m/%Y" }, // Sunday, 1 March 2020 // 01/03/2020

10
apps/metronome/README.md Normal file
View File

@ -0,0 +1,10 @@
# Metronome
This metronome makes your watch blink and vibrate with a given rate.
## Usage
* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat.
* Use `BTN1` to increase the bmp value by one.
* Use `BTN3` to decrease the bmp value by one.
* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+ABt4AB4fOFyFOABtUGDotOAAYvcp4ARqovbq0rACAvbqwABF98yGCAvdGAcHgAAEF8tWmIuGGA6QaF4lWFw4vgFwovPmIvuYDIvd0ejF59cF6qQFFwIvnMAguSqxfaFyQvYvOi0QuTF64uCAAQuRXwIvUqouEF6guFF5+cAAiOZF6iOaF5sxv+iF6xfRmVWFwWjv8rp4tSL6YvBqwuDMgQvnFwovURwIvQRggAELygvPgwuIF8ouEBwIvnFwwwXF54uBvwuFq0yF6buCF5guClQuFGAgvfFwcAF49WmIvRFwQvKFwkAmQvHYQMxF7l+FwgvKGAIvalQuGF5dWFx1VABVUvF4p0qAAdPCZNPF51OAD4vOKQIACF/4waF9wuEqgv/F/gwMF97vvAAUqADYtQAAMAADYuRGDgmLA="))

View File

@ -0,0 +1,93 @@
var tStart = Date.now();
var cindex=0; // index to iterate through colous
var bpm=60; // ininital bpm value
var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm
var tindex=0; //index to iterate through time_diffs
Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app
function changecolor() {
const maxColors = 2;
const colors = {
0: { value: 0xFFFF, name: "White" },
1: { value: 0x000F, name: "Navy" },
// 2: { value: 0x03E0, name: "DarkGreen" },
// 3: { value: 0x03EF, name: "DarkCyan" },
// 4: { value: 0x7800, name: "Maroon" },
// 5: { value: 0x780F, name: "Purple" },
// 6: { value: 0x7BE0, name: "Olive" },
// 7: { value: 0xC618, name: "LightGray" },
// 8: { value: 0x7BEF, name: "DarkGrey" },
// 9: { value: 0x001F, name: "Blue" },
// 10: { value: 0x07E0, name: "Green" },
// 11: { value: 0x07FF, name: "Cyan" },
// 12: { value: 0xF800, name: "Red" },
// 13: { value: 0xF81F, name: "Magenta" },
// 14: { value: 0xFFE0, name: "Yellow" },
// 15: { value: 0xFFFF, name: "White" },
// 16: { value: 0xFD20, name: "Orange" },
// 17: { value: 0xAFE5, name: "GreenYellow" },
// 18: { value: 0xF81F, name: "Pink" },
};
g.setColor(colors[cindex].value);
if (cindex == maxColors-1) {
cindex = 0;
}
else {
cindex += 1;
}
return cindex;
}
function updateScreen() {
g.clear();
changecolor();
Bangle.buzz(50, 0.75);
g.setFont("Vector",48);
g.drawString(Math.floor(bpm)+"bpm", -1, 70);
}
Bangle.on('touch', function(button) {
// setting bpm by tapping the screen. Uses the mean time difference between several tappings.
if (tindex < time_diffs.length) {
if (Date.now()-tStart < 5000) {
time_diffs[tindex] = Date.now()-tStart;
}
} else {
tindex=0;
time_diffs[tindex] = Date.now()-tStart;
}
tindex += 1;
mean_time = 0.0;
for(count = 0; count < time_diffs.length; count++) {
mean_time += time_diffs[count];
}
time_diff = mean_time/count;
tStart = Date.now();
clearInterval(time_diff);
g.clear();
g.setFont("Vector",48);
bpm = (60 * 1000/(time_diff));
g.drawString(Math.floor(bpm)+"bpm", -1, 70);
clearInterval(interval);
interval = setInterval(updateScreen, 60000 / bpm);
return bpm;
});
// enable bpm finetuning via buttons.
setWatch(() => {
bpm += 1;
clearInterval(interval);
interval = setInterval(updateScreen, 60000 / bpm);
}, BTN1, {repeat:true});
setWatch(() => {
if (bpm > 1) {
bpm -= 1;
clearInterval(interval);
interval = setInterval(updateScreen, 60000 / bpm);
}
}, BTN3, {repeat:true});
interval = setInterval(updateScreen, 60000 / bpm);

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -5,3 +5,4 @@
0.04: Run again when updated 0.04: Run again when updated
Don't run again when settings app is updated (or absent) Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings Add "Run Now" option to settings
0.05: Don't overwrite existing settings on app update

View File

@ -1,11 +1,11 @@
(function() { (function() {
let s = require('Storage').readJSON('ncstart.settings.json', 1) let s = require('Storage').readJSON('ncstart.json', 1)
|| require('Storage').readJSON('setting.json', 1) || require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent || {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('ncstart.app.js')) { if (!s.welcomed && require('Storage').read('ncstart.app.js')) {
setTimeout(() => { setTimeout(() => {
s.welcomed = true s.welcomed = true
require('Storage').write('ncstart.settings.json', s) require('Storage').write('ncstart.json', s)
load('ncstart.app.js') load('ncstart.app.js')
}) })
} }

View File

@ -1,3 +0,0 @@
{
"welcomed": false
}

View File

@ -1,13 +1,12 @@
// The welcome app is special, and gets to use global settings
(function(back) { (function(back) {
let settings = require('Storage').readJSON('ncstart.settings.json', 1) let settings = require('Storage').readJSON('ncstart.json', 1)
|| require('Storage').readJSON('setting.json', 1) || {} || require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({ E.showMenu({
'': { 'title': 'NCEU Startup' }, '': { 'title': 'NCEU Startup' },
'Run on Next Boot': { 'Run on Next Boot': {
value: !settings.welcomed, value: !settings.welcomed,
format: v => v ? 'OK' : 'No', format: v => v ? 'OK' : 'No',
onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}), onchange: v => require('Storage').write('ncstart.json', {welcomed: !v}),
}, },
'Run Now': () => load('ncstart.app.js'), 'Run Now': () => load('ncstart.app.js'),
'< Back': back, '< Back': back,

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Use BTN2 for settings menu like other clocks 0.02: Use BTN2 for settings menu like other clocks
0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting 0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting
0.04: Don't overwrite existing settings on app update

View File

@ -1,5 +0,0 @@
{
color:0,
drawMode:"fill",
menuButton:22
}

1
apps/rclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First published version of app

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+If4A/AH4AXqwBEF9VWlYxEAoIAllYuGGwIxnSxAwkR4InCFIbGmF4TCCGAYEBSgK/kXYQxFetDzCLYhgjeBQ3EGE69ESwgwoZYiSpMAgCEGFRfqYQrDblRfRMDdU0QFDp2iAAN4HIowBLYYwXvHG4w0D4wtB0QDBGApcCGYqLSEgIvEAwIqCHQNUYArdaKwIlBRwYpDGgIvEL4QxBYDIvEAAhpBpxZaF6BeBvAIFL4qVXF44uIF4pffFxI0GF7ouKlbrClaNXF4wEB0VUAAUqF4qTEF7heBAAhjDLQS+CL7MqqgECLgZfNGDIAORIaNZACCOBLIbvaFxy/ERtDpCAgYCDF1DsnFgS2ERk4sBF4hhBMYgAiE4bsDF0zAKMFABBXkxZEX4QunWwS4CFtCMEFsN4AAOiAYcAqgGB0UqgGip2iqgvcD4IuCAYgwBAoINBAIN4F7gkBAAplCGgVUNQhfcqlOAAIDCgEqAQIBBAoKXBAQIAL"))

BIN
apps/rclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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

@ -0,0 +1,165 @@
{
var minutes;
var seconds;
var hours;
var date;
var first = true;
const screen = {
width: g.getWidth(),
height: g.getWidth(),
middle: g.getWidth() / 2,
center: g.getHeight() / 2,
};
// Ssettings
const settings = {
time: {
color: '#f0af00',
shadow: '#CF7500',
font: 'Vector',
size: 60,
middle: screen.middle - 30,
center: screen.center,
},
date: {
color: '#f0af00',
shadow: '#CF7500',
font: 'Vector',
size: 15,
middle: screen.height - 20, // at bottom of screen
center: screen.center,
},
circle: {
colormin: '#eeeeee',
colorsec: '#bbbbbb',
width: 10,
middle: screen.middle,
center: screen.center,
height: screen.height
}
};
const dateStr = function (date) {
day = date.getDate();
month = date.getMonth();
year = date.getFullYear();
if (day < 10) {
day = "0" + day;
}
if (month < 10) {
month = "0" + month;
}
return year + "-" + month + "-" + day;
};
const getArcXY = function (centerX, centerY, radius, angle) {
var s, r = [];
s = 2 * Math.PI * angle / 360;
r.push(centerX + Math.round(Math.cos(s) * radius));
r.push(centerY + Math.round(Math.sin(s) * radius));
return r;
};
const drawMinArc = function (sections, color) {
g.setColor(color);
rad = (settings.circle.height / 2) - 20;
r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90);
//g.setPixel(r[0],r[1]);
r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90);
//g.setPixel(r[0],r[1]);
g.drawLine(r1[0], r1[1], r2[0], r2[1]);
g.setColor('#333333');
g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width-4)
};
const drawSecArc = function (sections, color) {
g.setColor(color);
rad = (settings.circle.height / 2) - 40;
r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90);
//g.setPixel(r[0],r[1]);
r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90);
//g.setPixel(r[0],r[1]);
g.drawLine(r1[0], r1[1], r2[0], r2[1]);
g.setColor('#333333');
g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width-4)
};
const drawClock = function () {
currentTime = new Date();
//Set to initial time when started
if (first == true) {
minutes = currentTime.getMinutes();
seconds = currentTime.getSeconds();
for (count = 0; count <= minutes; count++) {
drawMinArc(count, settings.circle.colormin);
}
for (count = 0; count <= seconds; count++) {
drawSecArc(count, settings.circle.colorsec);
}
first = false;
}
// Reset seconds
if (seconds == 59) {
g.setColor('#000000');
g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 40);
}
// Reset minutes
if (minutes == 59 && seconds == 59) {
g.setColor('#000000');
g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 20);
}
//Get date as a string
date = dateStr(currentTime);
// Update minutes when needed
if (minutes != currentTime.getMinutes()) {
minutes = currentTime.getMinutes();
drawMinArc(minutes, settings.circle.colormin);
}
//Update seconds when needed
if (seconds != currentTime.getSeconds()) {
seconds = currentTime.getSeconds();
drawSecArc(seconds, settings.circle.colorsec);
}
//Write the time as configured in the settings
hours = currentTime.getHours();
g.setColor(settings.time.color);
g.setFont(settings.time.font, settings.time.size);
g.drawString(hours, settings.time.center, settings.time.middle);
//Write the date as configured in the settings
g.setColor(settings.date.color);
g.setFont(settings.date.font, settings.date.size);
g.drawString(date, settings.date.center, settings.date.middle);
};
Bangle.on('lcdPower', function (on) {
if (on) drawClock();
});
// clean app screen
g.clear();
g.setFontAlign( 0, 0, 0);
Bangle.loadWidgets();
Bangle.drawWidgets();
// refesh every 30 sec
setInterval(drawClock, 1E3);
// draw now
drawClock();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
}

View File

@ -17,3 +17,6 @@
Move LCD Brightness menu into more general LCD menu Move LCD Brightness menu into more general LCD menu
0.14: Reduce memory usage when running app settings page 0.14: Reduce memory usage when running app settings page
0.15: Reduce memory usage when running default clock chooser (#294) 0.15: Reduce memory usage when running default clock chooser (#294)
0.16: Reduce memory usage further when running app settings page
0.17: Remove need for "settings" in appid.info
0.18: Don't overwrite existing settings on app update

View File

@ -1,25 +0,0 @@
{
ble: true, // Bluetooth enabled by default
blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used?
log: false, // Do log messages appear on screen?
timeout: 10, // Default LCD timeout in seconds
vibrate: true, // Vibration enabled by default. App must support
beep: "vib", // Beep enabled by default. App must support
timezone: 0, // Set the timezone for the device
HID : false, // BLE HID mode, off by default
clock: null, // a string for the default clock's name
"12hour" : false, // 12 or 24 hour clock?
// welcomed : undefined/true (whether welcome app should show)
brightness: 1, // LCD brightness from 0 to 1
options: {
wakeOnBTN1: true,
wakeOnBTN2: true,
wakeOnBTN3: true,
wakeOnFaceUp: false,
wakeOnTouch: false,
wakeOnTwist: true,
twistThreshold: 819.2,
twistMaxY: -800,
twistTimeout: 1000
}
}

View File

@ -416,10 +416,19 @@ function showAppSettingsMenu() {
'': { 'title': 'App Settings' }, '': { 'title': 'App Settings' },
'< Back': ()=>showMainMenu(), '< Back': ()=>showMainMenu(),
} }
const apps = storage.list(/\.info$/) const apps = storage.list(/\.settings\.js$/)
.map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?a:undefined}) .map(s => s.substr(0, s.length-12))
.filter(app => app) // filter out any undefined apps .map(id => {
.sort((a, b) => a.sortorder - b.sortorder) const a=storage.readJSON(id+'.info',1);
return {id:id,name:a.name,sortorder:a.sortorder};
})
.sort((a, b) => {
const n = (0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
})
if (apps.length === 0) { if (apps.length === 0) {
appmenu['No app has settings'] = () => { }; appmenu['No app has settings'] = () => { };
} }
@ -433,10 +442,7 @@ function showAppSettings(app) {
E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`); E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`);
setWatch(showAppSettingsMenu, BTN1, { repeat: false }); setWatch(showAppSettingsMenu, BTN1, { repeat: false });
} }
let appSettings = storage.read(app.settings); let appSettings = storage.read(app.id+'.settings.js');
if (!appSettings) {
return showError('Missing settings');
}
try { try {
appSettings = eval(appSettings); appSettings = eval(appSettings);
} catch (e) { } catch (e) {

View File

@ -5,3 +5,4 @@
Fixed bug from 0.01 where BN1 (reset) could clear the lap log when timer is running Fixed bug from 0.01 where BN1 (reset) could clear the lap log when timer is running
0.04: Changed save file filename, add interface.html to allow laps to be loaded 0.04: Changed save file filename, add interface.html to allow laps to be loaded
0.05: Added widgets 0.05: Added widgets
0.06: Added total running time, moved lap time to smaller display, total run time now appends as first entry in array, saving now saves last lap as well

View File

@ -17,11 +17,12 @@ function getLapTimes() {
<div class="columns">\n`; <div class="columns">\n`;
lapData.forEach((lap,lapIndex) => { lapData.forEach((lap,lapIndex) => {
lap.date = lap.n.substr(7,16).replace("_"," "); lap.date = lap.n.substr(7,16).replace("_"," ");
lap.elapsed = lap.d.shift(); // remove first item
html += ` html += `
<div class="column col-12"> <div class="column col-12">
<div class="card-header"> <div class="card-header">
<div class="card-title h5">${lap.date}</div> <div class="card-title h5">${lap.date}</div>
<div class="card-subtitle text-gray">${lap.d.length} Laps</div> <div class="card-subtitle text-gray">${lap.d.length} Laps, total time ${lap.elapsed}</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">

View File

@ -1,8 +1,11 @@
var tTotal = Date.now();
var tStart = Date.now(); var tStart = Date.now();
var tCurrent = Date.now(); var tCurrent = Date.now();
var started = false; var started = false;
var timeY = 60; var timeY = 45;
var hsXPos = 0; var hsXPos = 0;
var TtimeY = 75;
var ThsXPos = 0;
var lapTimes = []; var lapTimes = [];
var displayInterval; var displayInterval;
@ -12,6 +15,7 @@ function timeToText(t) {
var hs = Math.floor(t/10)%100; var hs = Math.floor(t/10)%100;
return mins+":"+("0"+secs).substr(-2)+"."+("0"+hs).substr(-2); return mins+":"+("0"+secs).substr(-2)+"."+("0"+hs).substr(-2);
} }
function updateLabels() { function updateLabels() {
g.reset(1); g.reset(1);
g.clearRect(0,23,g.getWidth()-1,g.getHeight()-24); g.clearRect(0,23,g.getWidth()-1,g.getHeight()-24);
@ -23,36 +27,54 @@ function updateLabels() {
g.setFont("6x8",1); g.setFont("6x8",1);
g.setFontAlign(-1,-1); g.setFontAlign(-1,-1);
for (var i in lapTimes) { for (var i in lapTimes) {
if (i<16) if (i<15)
{g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 30 + i*8);} {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 40 + i*8);}
else if (i<32) else if (i<30)
{g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 30 + (i-16)*8);} {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 40 + (i-15)*8);}
} }
drawsecs(); drawsecs();
} }
function drawsecs() { function drawsecs() {
var t = tCurrent-tStart; var t = tCurrent-tStart;
g.reset(1); var Tt = tCurrent-tTotal;
g.setFont("Vector",48);
g.setFontAlign(0,0);
var secs = Math.floor(t/1000)%60; var secs = Math.floor(t/1000)%60;
var mins = Math.floor(t/60000); var mins = Math.floor(t/60000);
var txt = mins+":"+("0"+secs).substr(-2); var txt = mins+":"+("0"+secs).substr(-2);
var Tsecs = Math.floor(Tt/1000)%60;
var Tmins = Math.floor(Tt/60000);
var Ttxt = Tmins+":"+("0"+Tsecs).substr(-2);
var x = 100; var x = 100;
g.clearRect(0,timeY-26,200,timeY+26); var Tx = 125;
g.drawString(txt,x,timeY); g.reset(1);
g.setFont("Vector",38);
g.setFontAlign(0,0);
g.clearRect(0,timeY-21,200,timeY+21);
g.drawString(Ttxt,x,timeY);
hsXPos = 5+x+g.stringWidth(txt)/2; hsXPos = 5+x+g.stringWidth(txt)/2;
g.setFont("6x8",2);
g.clearRect(0,TtimeY-7,200,TtimeY+7);
g.drawString(txt,Tx,TtimeY);
ThsXPos = 5+Tx+g.stringWidth(Ttxt)/2;
drawms(); drawms();
} }
function drawms() { function drawms() {
var t = tCurrent-tStart; var t = tCurrent-tStart;
var hs = Math.floor(t/10)%100; var hs = Math.floor(t/10)%100;
var Tt = tCurrent-tTotal;
var Ths = Math.floor(Tt/10)%100;
g.setFontAlign(-1,0); g.setFontAlign(-1,0);
g.setFont("6x8",2); g.setFont("6x8",2);
g.clearRect(hsXPos,timeY,220,timeY+20); g.clearRect(hsXPos,timeY,220,timeY+20);
g.drawString("."+("0"+hs).substr(-2),hsXPos,timeY+10); g.drawString("."+("0"+Ths).substr(-2),hsXPos-5,timeY+14);
g.setFont("6x8",1);
g.clearRect(ThsXPos,TtimeY,220,TtimeY+5);
g.drawString("."+("0"+hs).substr(-2),ThsXPos-5,TtimeY+3);
} }
function getLapTimesArray() { function getLapTimesArray() {
lapTimes.push(tCurrent-tTotal);
return lapTimes.map(timeToText).reverse(); return lapTimes.map(timeToText).reverse();
} }
@ -61,7 +83,8 @@ setWatch(function() { // Start/stop
Bangle.beep(); Bangle.beep();
if (started) if (started)
tStart = Date.now()+tStart-tCurrent; tStart = Date.now()+tStart-tCurrent;
tCurrent = Date.now(); tTotal = Date.now()+tTotal-tCurrent;
tCurrent = Date.now();
if (displayInterval) { if (displayInterval) {
clearInterval(displayInterval); clearInterval(displayInterval);
displayInterval = undefined; displayInterval = undefined;
@ -77,29 +100,33 @@ setWatch(function() { // Start/stop
drawms(); drawms();
}, 20); }, 20);
}, BTN2, {repeat:true}); }, BTN2, {repeat:true});
setWatch(function() { // Lap setWatch(function() { // Lap
Bangle.beep(); Bangle.beep();
if (started) { if (started) {
tCurrent = Date.now(); tCurrent = Date.now();
lapTimes.unshift(tCurrent-tStart); lapTimes.unshift(tCurrent-tStart);
} }
tStart = tCurrent;
if (!started) { // save if (!started) { // save
var timenow= Date();
var filename = "swatch-"+(new Date()).toISOString().substr(0,16).replace("T","_")+".json"; var filename = "swatch-"+(new Date()).toISOString().substr(0,16).replace("T","_")+".json";
if (tCurrent!=tStart)
lapTimes.unshift(tCurrent-tStart);
// this maxes out the 28 char maximum // this maxes out the 28 char maximum
require("Storage").writeJSON(filename, getLapTimesArray()); require("Storage").writeJSON(filename, getLapTimesArray());
tStart = tCurrent = tTotal = Date.now();
lapTimes = [];
E.showMessage("Laps Saved","Stopwatch"); E.showMessage("Laps Saved","Stopwatch");
setTimeout(updateLabels, 1000); setTimeout(updateLabels, 1000);
} else { } else {
tStart = tCurrent;
updateLabels(); updateLabels();
} }
}, BTN1, {repeat:true}); }, BTN1, {repeat:true});
setWatch(function() { // Reset setWatch(function() { // Reset
if (!started) { if (!started) {
Bangle.beep(); Bangle.beep();
tStart = tCurrent = Date.now(); tStart = tCurrent = tTotal = Date.now();
lapTimes = []; lapTimes = [];
} }
updateLabels(); updateLabels();
}, BTN3, {repeat:true}); }, BTN3, {repeat:true});

View File

@ -2,4 +2,5 @@
0.02: Add swipe support and doucle tap to run application 0.02: Add swipe support and doucle tap to run application
0.03: Close launcher when lcd turn off 0.03: Close launcher when lcd turn off
0.04: Complete rewrite to add animation and loop ( issue #210 ) 0.04: Complete rewrite to add animation and loop ( issue #210 )
0.05: Improve perf 0.05: Improve perf
0.06: Only store relevant app data (saves RAM when many apps)

View File

@ -5,8 +5,8 @@ g.flip();
const Storage = require("Storage"); const Storage = require("Storage");
function getApps(){ function getApps(){
return Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) }) return Storage.list(/\.info$/).map(app=>{var a=Storage.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src,version:a.version}})
.filter(app=>app.type=="app" || app.type=="clock" || !app.type) .filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type))
.sort((a,b)=>{ .sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder); var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first if (n) return n; // do sortorder first
@ -19,7 +19,7 @@ function getApps(){
const HEIGHT = g.getHeight(); const HEIGHT = g.getHeight();
const WIDTH = g.getWidth(); const WIDTH = g.getWidth();
const HALF = WIDTH/2; const HALF = WIDTH/2;
const ANIMATION_FRAME = 4; const ANIMATION_FRAME = 4;
const ANIMATION_STEP = HALF / ANIMATION_FRAME; const ANIMATION_STEP = HALF / ANIMATION_FRAME;
function getPosition(index){ function getPosition(index){
@ -192,4 +192,4 @@ Bangle.on('swipe', dir => {
// close launcher when lcd is off // close launcher when lcd is off
Bangle.on('lcdPower', on => { Bangle.on('lcdPower', on => {
if(!on) return load(); if(!on) return load();
}); });

View File

@ -7,3 +7,4 @@
0.07: Run again when updated 0.07: Run again when updated
Don't run again when settings app is updated (or absent) Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings Add "Run Now" option to settings
0.08: Don't overwrite existing settings on app update

View File

@ -1,11 +1,11 @@
(function() { (function() {
let s = require('Storage').readJSON('welcome.settings.json', 1) let s = require('Storage').readJSON('welcome.json', 1)
|| require('Storage').readJSON('setting.json', 1) || require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent || {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('welcome.app.js')) { if (!s.welcomed && require('Storage').read('welcome.app.js')) {
setTimeout(() => { setTimeout(() => {
s.welcomed = true s.welcomed = true
require('Storage').write('welcome.settings.json', {welcomed: "yes"}) require('Storage').write('welcome.json', {welcomed: "yes"})
load('welcome.app.js') load('welcome.app.js')
}) })
} }

View File

@ -1,3 +0,0 @@
{
"welcomed": false
}

View File

@ -1,13 +1,12 @@
// The welcome app is special, and gets to use global settings
(function(back) { (function(back) {
let settings = require('Storage').readJSON('welcome.settings.json', 1) let settings = require('Storage').readJSON('welcome.json', 1)
|| require('Storage').readJSON('setting.json', 1) || {} || require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({ E.showMenu({
'': { 'title': 'Welcome App' }, '': { 'title': 'Welcome App' },
'Run on Next Boot': { 'Run on Next Boot': {
value: !settings.welcomed, value: !settings.welcomed,
format: v => v ? 'OK' : 'No', format: v => v ? 'OK' : 'No',
onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}), onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
}, },
'Run Now': () => load('welcome.app.js'), 'Run Now': () => load('welcome.app.js'),
'< Back': back, '< Back': back,

View File

@ -6,3 +6,5 @@
0.07: Add settings: percentage/color/charger icon 0.07: Add settings: percentage/color/charger icon
0.08: Draw percentage as inverted on monochrome battery 0.08: Draw percentage as inverted on monochrome battery
0.09: Fix regression stopping correct widget updates 0.09: Fix regression stopping correct widget updates
0.10: Add 'hide if charge greater than'
0.11: Don't overwrite existing settings on app update

View File

@ -3,7 +3,7 @@
* @param {function} back Use back() to return to settings menu * @param {function} back Use back() to return to settings menu
*/ */
(function(back) { (function(back) {
const SETTINGS_FILE = 'widbatpc.settings.json' const SETTINGS_FILE = 'widbatpc.json'
const COLORS = ['By Level', 'Green', 'Monochrome'] const COLORS = ['By Level', 'Green', 'Monochrome']
// initialize with default settings... // initialize with default settings...
@ -11,21 +11,22 @@
'color': COLORS[0], 'color': COLORS[0],
'percentage': true, 'percentage': true,
'charger': true, 'charger': true,
'hideifmorethan': 100,
} }
// ...and overwrite them with any saved values // ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings // This way saved values are preserved if a new version adds more settings
const storage = require('Storage') const storage = require('Storage')
const saved = storage.readJSON(SETTINGS_FILE, 1) || {} const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
for (const key in saved) { for (const key in saved) {
s[key] = saved[key] s[key] = saved[key];
} }
// creates a function to safe a specific setting, e.g. save('color')(1) // creates a function to safe a specific setting, e.g. save('color')(1)
function save(key) { function save(key) {
return function (value) { return function (value) {
s[key] = value s[key] = value;
storage.write(SETTINGS_FILE, s) storage.write(SETTINGS_FILE, s);
WIDGETS["batpc"].reload() WIDGETS["batpc"].reload();
} }
} }
@ -51,8 +52,16 @@
const newIndex = (oldIndex + 1) % COLORS.length const newIndex = (oldIndex + 1) % COLORS.length
s.color = COLORS[newIndex] s.color = COLORS[newIndex]
save('color')(s.color) save('color')(s.color)
}, }
}, },
} 'Hide if >': {
value: s.hideifmorethan||100,
min: 10,
max : 100,
step: 10,
format: x => x+"%",
onchange: save('hideifmorethan'),
},
}
E.showMenu(menu) E.showMenu(menu)
}) })

View File

@ -1,9 +1,4 @@
(function(){ (function(){
const DEFAULTS = {
'color': 'By Level',
'percentage': true,
'charger': true,
}
const COLORS = { const COLORS = {
'white': -1, 'white': -1,
'charging': 0x07E0, // "Green" 'charging': 0x07E0, // "Green"
@ -11,15 +6,24 @@ const COLORS = {
'ok': 0xFD20, // "Orange" 'ok': 0xFD20, // "Orange"
'low':0xF800, // "Red" 'low':0xF800, // "Red"
} }
const SETTINGS_FILE = 'widbatpc.settings.json' const SETTINGS_FILE = 'widbatpc.json'
let settings let settings
function loadSettings() { function loadSettings() {
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}
const DEFAULTS = {
'color': 'By Level',
'percentage': true,
'charger': true,
'hideifmorethan': 100,
};
Object.keys(DEFAULTS).forEach(k=>{
if (settings[k]===undefined) settings[k]=DEFAULTS[k]
});
} }
function setting(key) { function setting(key) {
if (!settings) { loadSettings() } if (!settings) { loadSettings() }
return (key in settings) ? settings[key] : DEFAULTS[key] return settings[key];
} }
const levelColor = (l) => { const levelColor = (l) => {
@ -45,16 +49,27 @@ const levelColor = (l) => {
const chargerColor = () => { const chargerColor = () => {
return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging
} }
// sets width, returns true if it changed
function setWidth() { function setWidth() {
WIDGETS["batpc"].width = 40; var w = 40;
if (Bangle.isCharging() && setting('charger')) { if (Bangle.isCharging() && setting('charger'))
WIDGETS["batpc"].width += 16; w += 16;
} if (E.getBattery() > setting('hideifmorethan'))
w = 0;
var changed = WIDGETS["batpc"].width != w;
WIDGETS["batpc"].width = w;
return changed;
} }
function draw() { function draw() {
// if hidden, don't draw
if (!WIDGETS["batpc"].width) return;
// else...
var s = 39; var s = 39;
var x = this.x, y = this.y; var x = this.x, y = this.y;
const l = E.getBattery(),
c = levelColor(l);
const xl = x+4+l*(s-12)/100
if (Bangle.isCharging() && setting('charger')) { if (Bangle.isCharging() && setting('charger')) {
g.setColor(chargerColor()).drawImage(atob( g.setColor(chargerColor()).drawImage(atob(
"DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
@ -64,9 +79,7 @@ function draw() {
g.fillRect(x,y+2,x+s-4,y+21); g.fillRect(x,y+2,x+s-4,y+21);
g.clearRect(x+2,y+4,x+s-6,y+19); g.clearRect(x+2,y+4,x+s-6,y+19);
g.fillRect(x+s-3,y+10,x+s,y+14); g.fillRect(x+s-3,y+10,x+s,y+14);
const l = E.getBattery(),
c = levelColor(l);
const xl = x+4+l*(s-12)/100
g.setColor(c).fillRect(x+4,y+6,xl,y+17); g.setColor(c).fillRect(x+4,y+6,xl,y+17);
g.setColor(-1); g.setColor(-1);
if (!setting('percentage')) { if (!setting('percentage')) {
@ -97,20 +110,24 @@ function reload() {
g.clear(); g.clear();
Bangle.drawWidgets(); Bangle.drawWidgets();
} }
// update widget - redraw just widget, or all widgets if size changed
function update() {
if (setWidth()) Bangle.drawWidgets();
else WIDGETS["batpc"].draw();
}
Bangle.on('charging',function(charging) { Bangle.on('charging',function(charging) {
if(charging) Bangle.buzz(); if(charging) Bangle.buzz();
setWidth(); update();
Bangle.drawWidgets(); // relayout widgets
g.flip(); g.flip();
}); });
var batteryInterval; var batteryInterval;
Bangle.on('lcdPower', function(on) { Bangle.on('lcdPower', function(on) {
if (on) { if (on) {
WIDGETS["batpc"].draw(); update();
// refresh once a minute if LCD on // refresh once a minute if LCD on
if (!batteryInterval) if (!batteryInterval)
batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000); batteryInterval = setInterval(update, 60000);
} else { } else {
if (batteryInterval) { if (batteryInterval) {
clearInterval(batteryInterval); clearInterval(batteryInterval);

View File

@ -4,3 +4,4 @@
0.04: Only buzz on high confidence (>85%) 0.04: Only buzz on high confidence (>85%)
0.05: Improved buzz timing and rendering 0.05: Improved buzz timing and rendering
0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed
0.07: Home button fixed and README added

29
apps/wohrm/README.md Normal file
View File

@ -0,0 +1,29 @@
# Summary
Workout heart rate monitor that buzzes when your heart rate hits the limits.
This app is for the [Bangle.js watch](https://banglejs.com/). While active it monitors your heart rate
and will notify you with a buzz whenever your heart rate falls below or jumps above the set limits.
# How it works
[Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html).
## Setting the limits
For setting the lower limit press button 4 (left part of the watch's touch screen).
Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch.
For setting the upper limit act accordingly after pressing button 5 (the right part of the watch's screen).
## Reading Reliability
As per the specs of the watch the HR monitor is not 100% reliable all the time.
That's why the WOHRM displays a confidence value for each reading of the current heart rate.
To the left and right of the "Current" value two colored bars indicate the confidence in
the received value: For 85% and above the bars are green, between 84% and 50% the bars are yellow
and below 50% they turn red.
## Closing the app
Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher.
# HRM usage
The HRM is switched on when the app is started. It stays switch on while the app is running, even
when the watch screen goes to stand-by.

View File

@ -287,13 +287,11 @@ function resetHighlightTimeout() {
setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000);
} }
// Show launcher when middle button pressed
function switchOffApp(){ function switchOffApp(){
Bangle.setHRMPower(0); Bangle.setHRMPower(0);
Bangle.showLauncher(); Bangle.showLauncher();
} }
// special function to handle display switch on
Bangle.on('lcdPower', (on) => { Bangle.on('lcdPower', (on) => {
g.clear(); g.clear();
if (on) { if (on) {
@ -312,19 +310,18 @@ Bangle.setHRMPower(1);
Bangle.on('HRM', onHrm); Bangle.on('HRM', onHrm);
setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true});
setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true});
setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true});
setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true});
setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true });
setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true});
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
//drawTrainingHeartRate();
renderHomeIcon(); renderHomeIcon();
renderLowerLimitBackground(); renderLowerLimitBackground();
renderUpperLimitBackground(); renderUpperLimitBackground();
// refesh every sec
setInterval(drawTrainingHeartRate, 1000); setInterval(drawTrainingHeartRate, 1000);

View File

@ -39,10 +39,26 @@ try{
const APP_KEYS = [ const APP_KEYS = [
'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type',
'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator', 'sortorder', 'readme', 'custom', 'interface', 'storage', 'data', 'allow_emulator',
]; ];
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate'];
const DATA_KEYS = ['name', 'wildcard', 'storageFile'];
const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info
function globToRegex(pattern) {
const ESCAPE = '.*+-?^${}()|[]\\';
const regex = pattern.replace(/./g, c => {
switch (c) {
case '?': return '.';
case '*': return '.*';
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
}
});
return new RegExp('^'+regex+'$');
}
const isGlob = f => /[?*]/.test(f)
// All storage+data files in all apps: {app:<appid>,[file:<storage.name> | data:<data.name|data.wildcard>]}
let allFiles = [];
apps.forEach((app,appIdx) => { apps.forEach((app,appIdx) => {
if (!app.id) ERROR(`App ${appIdx} has no id`); if (!app.id) ERROR(`App ${appIdx} has no id`);
//console.log(`Checking ${app.id}...`); //console.log(`Checking ${app.id}...`);
@ -74,9 +90,13 @@ apps.forEach((app,appIdx) => {
var fileNames = []; var fileNames = [];
app.storage.forEach((file) => { app.storage.forEach((file) => {
if (!file.name) ERROR(`App ${app.id} has a file with no name`); if (!file.name) ERROR(`App ${app.id} has a file with no name`);
if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`);
let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS)
if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`)
if (fileNames.includes(file.name)) if (fileNames.includes(file.name))
ERROR(`App ${app.id} file ${file.name} is a duplicate`); ERROR(`App ${app.id} file ${file.name} is a duplicate`);
fileNames.push(file.name); fileNames.push(file.name);
allFiles.push({app: app.id, file: file.name});
if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`);
if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`); if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`);
var fileContents = ""; var fileContents = "";
@ -115,6 +135,54 @@ apps.forEach((app,appIdx) => {
if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`);
} }
}); });
let dataNames = [];
(app.data||[]).forEach((data)=>{
if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`);
if (dataNames.includes(data.name||data.wildcard))
ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`);
dataNames.push(data.name||data.wildcard)
allFiles.push({app: app.id, data: (data.name||data.wildcard)});
if ('name' in data && 'wildcard' in data)
ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`);
if (isGlob(data.name))
ERROR(`App ${app.id} data file name ${data.name} contains wildcards`);
if (data.wildcard) {
if (!isGlob(data.wildcard))
ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`);
if (data.wildcard.replace(/\?|\*/g,'') === '')
ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`);
else if (data.wildcard.replace(/\?|\*/g,'').length < 3)
WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`);
else if (!data.wildcard.includes(app.id))
WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`);
}
let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS)
if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`)
if ('storageFile' in data && typeof data.storageFile !== 'boolean')
ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`);
for (const key in data) {
if (!DATA_KEYS.includes(key))
ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`);
}
});
// prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?)
if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json"))
WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`)
// settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?)
if (fileNames.includes(app.id+".settings.json"))
WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`)
if (fileNames.includes(app.id+".json"))
WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`)
// warn if storage file matches data file of same app
dataNames.forEach(dataName=>{
const glob = globToRegex(dataName)
fileNames.forEach(fileName=>{
if (glob.test(fileName)) {
if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`)
else WARN(`App ${app.id} storage file ${fileName} is also listed in data`)
}
})
})
//console.log(fileNames); //console.log(fileNames);
if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`);
if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`);
@ -123,3 +191,20 @@ apps.forEach((app,appIdx) => {
if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`);
} }
}); });
// Do not allow files from different apps to collide
let fileA
while(fileA=allFiles.pop()) {
const nameA = (fileA.file||fileA.data),
globA = globToRegex(nameA),
typeA = fileA.file?'storage':'data'
allFiles.forEach(fileB => {
const nameB = (fileB.file||fileB.data),
globB = globToRegex(nameB),
typeB = fileB.file?'storage':'data'
if (globA.test(nameB)||globB.test(nameA)) {
if (isGlob(nameA)||isGlob(nameB))
ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`)
else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
}
})
}

View File

@ -113,6 +113,7 @@
<div class="panel"> <div class="panel">
<div class="panel-header" style="text-align:right"> <div class="panel-header" style="text-align:right">
<button class="btn refresh">Refresh...</button> <button class="btn refresh">Refresh...</button>
<button class="btn btn-primary updateapps hidden">Update X apps</button>
</div> </div>
<div class="panel-body columns"><!-- apps go here --></div> <div class="panel-body columns"><!-- apps go here --></div>
</div> </div>

View File

@ -60,8 +60,6 @@ var AppInfo = {
if (app.type && app.type!="app") json.type = app.type; if (app.type && app.type!="app") json.type = app.type;
if (fileContents.find(f=>f.name==app.id+".app.js")) if (fileContents.find(f=>f.name==app.id+".app.js"))
json.src = app.id+".app.js"; json.src = app.id+".app.js";
if (fileContents.find(f=>f.name==app.id+".settings.js"))
json.settings = app.id+".settings.js";
if (fileContents.find(f=>f.name==app.id+".img")) if (fileContents.find(f=>f.name==app.id+".img"))
json.icon = app.id+".img"; json.icon = app.id+".img";
if (app.sortorder) json.sortorder = app.sortorder; if (app.sortorder) json.sortorder = app.sortorder;
@ -69,13 +67,48 @@ var AppInfo = {
var fileList = fileContents.map(storageFile=>storageFile.name); var fileList = fileContents.map(storageFile=>storageFile.name);
fileList.unshift(appJSONName); // do we want this? makes life easier! fileList.unshift(appJSONName); // do we want this? makes life easier!
json.files = fileList.join(","); json.files = fileList.join(",");
if ('data' in app) {
let data = {dataFiles: [], storageFiles: []};
// add "data" files to appropriate list
app.data.forEach(d=>{
if (d.storageFile) data.storageFiles.push(d.name||d.wildcard)
else data.dataFiles.push(d.name||d.wildcard)
})
const dataString = AppInfo.makeDataString(data)
if (dataString) json.data = dataString
}
fileContents.push({ fileContents.push({
name : appJSONName, name : appJSONName,
content : JSON.stringify(json) content : JSON.stringify(json)
}); });
resolve(fileContents); resolve(fileContents);
}); });
} },
// (<appid>.info).data holds filenames of data: both regular and storageFiles
// These are stored as: (note comma vs semicolons)
// "fil1,file2", "file1,file2;storageFileA,storageFileB" or ";storageFileA"
/**
* Convert appid.info "data" to object with file names/patterns
* Passing in undefined works
* @param data "data" as stored in appid.info
* @returns {{storageFiles:[], dataFiles:[]}}
*/
parseDataString(data) {
data = data || '';
let [files = [], storage = []] = data.split(';').map(d => d.split(','))
return {dataFiles: files, storageFiles: storage}
},
/**
* Convert object with file names/patterns to appid.info "data" string
* Passing in an incomplete object will not work
* @param data {{storageFiles:[], dataFiles:[]}}
* @returns {string} "data" to store in appid.info
*/
makeDataString(data) {
if (!data.dataFiles.length && !data.storageFiles.length) { return '' }
if (!data.storageFiles.length) { return data.dataFiles.join(',') }
return [data.dataFiles.join(','),data.storageFiles.join(',')].join(';')
},
}; };
if ("undefined"!=typeof module) if ("undefined"!=typeof module)

View File

@ -94,10 +94,29 @@ getInstalledApps : () => {
}); });
}, },
removeApp : app => { // expects an appid.info structure (i.e. with `files`) removeApp : app => { // expects an appid.info structure (i.e. with `files`)
if (app.files === '') return Promise.resolve(); // nothing to erase if (!app.files && !app.data) return Promise.resolve(); // nothing to erase
Progress.show({title:`Removing ${app.name}`,sticky:true}); Progress.show({title:`Removing ${app.name}`,sticky:true});
var cmds = app.files.split(',').map(file=>{ let cmds = '\x10const s=require("Storage");\n';
return `\x10require("Storage").erase(${toJS(file)});\n`; // remove App files: regular files, exact names only
cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join("");
// remove app Data: (dataFiles and storageFiles)
const data = AppInfo.parseDataString(app.data)
const isGlob = f => /[?*]/.test(f)
// regular files, can use wildcards
cmds += data.dataFiles.map(file => {
if (!isGlob(file)) return `\x10s.erase(${toJS(file)});\n`;
const regex = new RegExp(globToRegex(file))
return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`;
}).join("");
// storageFiles, can use wildcards
cmds += data.storageFiles.map(file => {
if (!isGlob(file)) return `\x10s.open(${toJS(file)},'r').erase();\n`;
// storageFiles have a chunk number appended to their real name
const regex = globToRegex(file+'\u0001')
// open() doesn't want the chunk number though
let cmd = `\x10s.list(${regex}).forEach(f=>s.open(f.substring(0,f.length-1),'r').erase());\n`
// using a literal \u0001 char fails (not sure why), so escape it
return cmd.replace('\u0001', '\\x01')
}).join(""); }).join("");
console.log("removeApp", cmds); console.log("removeApp", cmds);
return Comms.reset().then(new Promise((resolve,reject) => { return Comms.reset().then(new Promise((resolve,reject) => {

View File

@ -207,7 +207,7 @@ function refreshLibrary() {
var version = getVersionInfo(app, appInstalled); var version = getVersionInfo(app, appInstalled);
var versionInfo = version.text; var versionInfo = version.text;
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>"; if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
var readme = `<a href="#" onclick="showReadme('${app.id}')">Read more...</a>`; var readme = `<a class="c-hand" onclick="showReadme('${app.id}')">Read more...</a>`;
var favourite = favourites.find(e => e == app.id); var favourite = favourites.find(e => e == app.id);
return `<div class="tile column col-6 col-sm-12 col-xs-12"> return `<div class="tile column col-6 col-sm-12 col-xs-12">
<div class="tile-icon"> <div class="tile-icon">
@ -218,7 +218,7 @@ function refreshLibrary() {
<p class="tile-subtitle">${escapeHtml(app.description)}${app.readme?`<br/>${readme}`:""}</p> <p class="tile-subtitle">${escapeHtml(app.description)}${app.readme?`<br/>${readme}`:""}</p>
<a href="https://github.com/espruino/BangleApps/tree/master/apps/${app.id}" target="_blank" class="link-github"><img src="img/github-icon-sml.png" alt="See the code on GitHub"/></a> <a href="https://github.com/espruino/BangleApps/tree/master/apps/${app.id}" target="_blank" class="link-github"><img src="img/github-icon-sml.png" alt="See the code on GitHub"/></a>
</div> </div>
<div class="tile-action"> <div class="tile-action">
<button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"&#x2665;":"&#x2661;"}</button> <button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"&#x2665;":"&#x2661;"}</button>
<button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button> <button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button>
<button class="btn btn-link btn-action btn-lg ${app.allow_emulator?"":"d-hide"}" appid="${app.id}" title="Try in Emulator"><i class="icon icon-share"></i></button> <button class="btn btn-link btn-action btn-lg ${app.allow_emulator?"":"d-hide"}" appid="${app.id}" title="Try in Emulator"><i class="icon icon-share"></i></button>
@ -349,6 +349,14 @@ function updateApp(app) {
.filter(f => f !== app.id + '.info') .filter(f => f !== app.id + '.info')
.filter(f => !app.storage.some(s => s.name === f)) .filter(f => !app.storage.some(s => s.name === f))
.join(','); .join(',');
let data = AppInfo.parseDataString(remove.data)
if ('data' in app) {
// only remove data files which are no longer declared in new app version
const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f)
data.dataFiles = data.dataFiles.filter(removeData)
data.storageFiles = data.storageFiles.filter(removeData)
}
remove.data = AppInfo.makeDataString(data)
return Comms.removeApp(remove); return Comms.removeApp(remove);
}).then(()=>{ }).then(()=>{
showToast(`Updating ${app.name}...`); showToast(`Updating ${app.name}...`);
@ -393,10 +401,18 @@ function showLoadingIndicator(id) {
panelbody.innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>'; panelbody.innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>';
} }
function getAppsToUpdate() {
var appsToUpdate = [];
appsInstalled.forEach(appInstalled => {
var app = appNameToApp(appInstalled.id);
if (app.version != appInstalled.version)
appsToUpdate.push(app);
});
return appsToUpdate;
}
function refreshMyApps() { function refreshMyApps() {
var panelbody = document.querySelector("#myappscontainer .panel-body"); var panelbody = document.querySelector("#myappscontainer .panel-body");
var tab = document.querySelector("#tab-myappscontainer a");
tab.setAttribute("data-badge", appsInstalled.length);
panelbody.innerHTML = appsInstalled.map(appInstalled => { panelbody.innerHTML = appsInstalled.map(appInstalled => {
var app = appNameToApp(appInstalled.id); var app = appNameToApp(appInstalled.id);
var version = getVersionInfo(app, appInstalled); var version = getVersionInfo(app, appInstalled);
@ -428,6 +444,17 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
if (icon.classList.contains("icon-download")) handleAppInterface(app); if (icon.classList.contains("icon-download")) handleAppInterface(app);
}); });
}); });
var appsToUpdate = getAppsToUpdate();
var tab = document.querySelector("#tab-myappscontainer a");
var updateApps = document.querySelector("#myappscontainer .updateapps");
if (appsToUpdate.length) {
updateApps.innerHTML = `Update ${appsToUpdate.length} apps`;
updateApps.classList.remove("hidden");
tab.setAttribute("data-badge", `${appsInstalled.length}${appsToUpdate.length}`);
} else {
updateApps.classList.add("hidden");
tab.setAttribute("data-badge", appsInstalled.length);
}
} }
let haveInstalledApps = false; let haveInstalledApps = false;
@ -463,6 +490,22 @@ htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addE
showToast("Getting app list failed, "+err,"error"); showToast("Getting app list failed, "+err,"error");
}); });
})); }));
htmlToArray(document.querySelectorAll(".btn.updateapps")).map(button => button.addEventListener("click", () => {
var appsToUpdate = getAppsToUpdate();
var count = appsToUpdate.length;
function updater() {
if (!appsToUpdate.length) return;
var app = appsToUpdate.pop();
return updateApp(app).then(function() {
return updater();
});
}
updater().then(err => {
showToast(`Updated ${count} apps`,"success");
}).catch(err => {
showToast("Update failed, "+err,"error");
});
}));
connectMyDeviceBtn.addEventListener("click", () => { connectMyDeviceBtn.addEventListener("click", () => {
if (connectMyDeviceBtn.classList.contains('is-connected')) { if (connectMyDeviceBtn.classList.contains('is-connected')) {
Comms.disconnectDevice(); Comms.disconnectDevice();
@ -613,4 +656,3 @@ document.getElementById("installfavourite").addEventListener("click",event=>{
showToast("App Install failed, "+err,"error"); showToast("App Install failed, "+err,"error");
}); });
}); });

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