Merge branch 'espruino:master' into master

master
Sebastian Di Luzio 2022-01-15 14:07:49 +01:00 committed by GitHub
commit 2d9080da72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
288 changed files with 10821 additions and 1263 deletions

461
apps.json
View File

@ -2,7 +2,7 @@
{
"id": "fwupdate",
"name": "Firmware Update",
"version": "0.02",
"version": "0.03",
"description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates",
"icon": "app.png",
"type": "RAM",
@ -16,7 +16,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.39",
"version": "0.41",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
@ -29,6 +29,24 @@
],
"sortorder": -10
},
{ "id": "ac_ac",
"name": "A Configurable Analog Clock",
"shortName":"Configurable Clock",
"version":"0.03",
"description": "AC-AC, a highly customizable analog clock with several clock faces, hands and complications to choose from",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": false,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"custom": "Customizer.html",
"storage": [
{"name":"ac_ac.app.js","url":"app.js"},
{"name":"ac_ac.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "hebrew_calendar",
"name": "Hebrew Calendar",
@ -77,7 +95,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.16",
"version": "0.18",
"description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png",
"type": "app",
@ -99,24 +117,26 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.05",
"version": "0.06",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications",
"tags": "tool,system,messages,notifications,gadgetbridge",
"dependencies": {"messages":"app"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"android.app.js","url":"app.js"},
{"name":"android.settings.js","url":"settings.js"},
{"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"}
],
"data": [{"name":"android.settings.json"}],
"sortorder": -8
},
{
"id": "ios",
"name": "iOS Integration",
"version": "0.07",
"version": "0.08",
"description": "Display notifications/music/etc from iOS devices",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",
@ -167,7 +187,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.39",
"version": "0.41",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
@ -218,7 +238,7 @@
{
"id": "locale",
"name": "Languages",
"version": "0.14",
"version": "0.15",
"description": "Translations for different countries",
"icon": "locale.png",
"type": "locale",
@ -307,7 +327,7 @@
"description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android' Bangle.js app instead.",
"icon": "app.png",
"type": "widget",
"tags": "tool,system,android,widget",
"tags": "tool,system,android,widget,gadgetbridge",
"supports": ["BANGLEJS","BANGLEJS2"],
"dependencies": {"notify":"type"},
"readme": "README.md",
@ -324,7 +344,7 @@
"version":"0.01",
"description": "Debug info for Gadgetbridge. Run this app and when Gadgetbridge messages arrive they are displayed on-screen.",
"icon": "app.png",
"tags": "",
"tags": "tool,debug,gadgetbridge",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
@ -768,7 +788,7 @@
"id": "recorder",
"name": "Recorder (BETA)",
"shortName": "Recorder",
"version": "0.05",
"version": "0.07",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",
@ -845,7 +865,7 @@
{
"id": "weather",
"name": "Weather",
"version": "0.14",
"version": "0.15",
"description": "Show Gadgetbridge weather report",
"icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}],
@ -922,12 +942,13 @@
{
"id": "widlock",
"name": "Lock Widget",
"version": "0.03",
"version": "0.04",
"description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked",
"icon": "widget.png",
"type": "widget",
"tags": "widget,lock",
"supports": ["BANGLEJS","BANGLEJS2"],
"sortorder": -1,
"storage": [
{"name":"widlock.wid.js","url":"widget.js"}
]
@ -936,7 +957,7 @@
"id": "widbatpc",
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"version": "0.14",
"version": "0.16",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"icon": "widget.png",
"type": "widget",
@ -1040,16 +1061,19 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.01",
"version": "0.03",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "boot",
"type": "app",
"tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true}
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"}
]
},
{
@ -1324,7 +1348,7 @@
"icon": "gesture.png",
"type": "app",
"tags": "gesture,ai",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"gesture.app.js","url":"gesture.js"},
{"name":".tfnames","url":"gesture-tfnames.js","evaluate":true},
@ -1348,6 +1372,22 @@
{"name":"pparrot.img","url":"party-parrot-icon.js","evaluate":true}
]
},
{
"id": "hralarm",
"name": "Heart rate alarm",
"shortName":"HR Alarm",
"version":"0.01",
"description": "This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits",
"icon": "widget.png",
"type": "widget",
"tags": "widget",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"hralarm.wid.js","url":"widget.js"},
{"name":"hralarm.settings.js","url":"settings.js"}
]
},
{
"id": "hrings",
"name": "Hypno Rings",
@ -1501,7 +1541,7 @@
{
"id": "gpsinfo",
"name": "GPS Info",
"version": "0.06",
"version": "0.09",
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
@ -1515,13 +1555,14 @@
{
"id": "assistedgps",
"name": "Assisted GPS Update (AGPS)",
"version": "0.01",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"version": "0.03",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"icon": "app.png",
"type": "RAM",
"tags": "tool,outdoors,agps",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": []
},
{
@ -1590,7 +1631,7 @@
{
"id": "widpedom",
"name": "Pedometer widget",
"version": "0.20",
"version": "0.22",
"description": "Daily pedometer widget",
"icon": "widget.png",
"type": "widget",
@ -1714,17 +1755,18 @@
{
"id": "wohrm",
"name": "Workout HRM",
"version": "0.08",
"version": "0.09",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"icon": "app.png",
"type": "app",
"tags": "hrm,workout",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}],
"storage": [
{"name":"wohrm.app.js","url":"app.js"},
{"name":"wohrm.settings.js","url":"settings.js"},
{"name":"wohrm.img","url":"app-icon.js","evaluate":true}
]
},
@ -1749,8 +1791,9 @@
"icon": "grocery.png",
"type": "app",
"tags": "tool,outdoors,shopping,list",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"custom": "grocery.html",
"allow_emulator": true,
"storage": [
{"name":"grocery.app.js","url":"app.js"},
{"name":"grocery.img","url":"grocery-icon.js","evaluate":true}
@ -1889,13 +1932,15 @@
{
"id": "widhwt",
"name": "Hand Wash Timer",
"version": "0.01",
"description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.",
"version": "0.02",
"description": "On Bangle.js 1 swipe your wrist over the watch face to start your personal Bangle.js 1 hand wash timer. On Bangle.js2 the Pattern Launcher is recommended to start the timer. Start washing after the short buzz and stop after the long buzz 35sec. later.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,tool",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"widhwt.app.js","url":"app.js"},
{"name":"widhwt.wid.js","url":"widget.js"}
]
},
@ -2072,12 +2117,13 @@
"id": "devstopwatch",
"name": "Dev Stopwatch",
"shortName": "Dev Stopwatch",
"version": "0.03",
"version": "0.04",
"description": "Stopwatch with 5 laps supported (cyclically replaced)",
"icon": "app.png",
"tags": "stopwatch,chrono,timer,chronometer",
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"bangle1-dev-stopwatch-screenshot.png"}],
"screenshots": [{"url":"bangle1-dev-stopwatch-screenshot.png"},{"url":"bangle2-dev-stopwatch-screenshot.png"}],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"devstopwatch.app.js","url":"app.js"},
@ -2250,6 +2296,20 @@
{"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true}
]
},
{ "id": "run",
"name": "Run",
"version":"0.01",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",
"supports" : ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"run.app.js","url":"app.js"},
{"name":"run.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "banglerun",
"name": "BangleRun",
@ -2429,7 +2489,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.05",
"version": "0.06",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
@ -2967,14 +3027,28 @@
],
"data": [{"wildcard":"accellog.?.csv"}]
},
{ "id": "accelgraph",
"name": "Accelerometer Graph",
"shortName":"Accel Graph",
"version":"0.01",
"description": "A simple app to draw a graph of data from the accelerometer on the screen",
"icon": "app.png",
"tags": "tool,debug",
"supports" : ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"accelgraph.app.js","url":"app.js"},
{"name":"accelgraph.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "cprassist",
"name": "CPR Assist",
"version": "0.01",
"version": "0.02",
"description": "Provides assistance while performing a CPR",
"icon": "cprassist-icon.png",
"tags": "tool,firstaid",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"screenshots": [{"url":"bangle1-CPR-assist-screenshot.png"}],
@ -3532,7 +3606,7 @@
"id": "mclockplus",
"name": "Morph Clock+",
"shortName": "Morph Clock+",
"version": "0.02",
"version": "0.03",
"description": "Morphing Clock with more readable seconds and date and additional stopwatch",
"icon": "mclockplus.png",
"type": "clock",
@ -3790,7 +3864,7 @@
{
"id": "simplest",
"name": "Simplest Clock",
"version": "0.03",
"version": "0.05",
"description": "The simplest working clock, acts as a tutorial piece",
"icon": "simplest.png",
"screenshots": [{"url":"screenshot_simplest.png"}],
@ -3896,8 +3970,8 @@
"id": "qmsched",
"name": "Quiet Mode Schedule and Widget",
"shortName": "Quiet Mode",
"version": "0.06",
"description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.",
"version": "0.07",
"description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
"icon": "app.png",
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
{"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}],
@ -3986,11 +4060,12 @@
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"doztime.app.js","url":"app.js"},
{"name":"doztime.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]},
{"name":"doztime.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]},
{"name":"doztime.img","url":"app-icon.js","evaluate":true}
]
},
@ -4210,13 +4285,13 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"version": "0.09",
"description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"version": "0.11",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"icon": "pastel.png",
"dependencies": {"mylocation":"app", "widpedom":"app"},
"screenshots": [{"url":"screenshot_pastel.png"}],
"dependencies": {"mylocation":"app", "widpedom":"app","weather":"app"},
"screenshots": [{"url":"screenshot_pastel.png"}, {"url":"weather_icons.png"}],
"type": "clock",
"tags": "clock",
"tags": "clock, weather, tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
@ -4237,8 +4312,9 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.03",
"description": "A simple clock using the bold Anton font.",
"version": "0.06",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
@ -4247,8 +4323,10 @@
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"antonclk.json"}]
},
{
"id": "waveclk",
@ -4327,8 +4405,10 @@
"allow_emulator": true,
"storage": [
{"name":"ffcniftya.app.js","url":"app.js"},
{"name":"ffcniftya.img","url":"app-icon.js","evaluate":true}
]
{"name":"ffcniftya.img","url":"app-icon.js","evaluate":true},
{"name":"ffcniftya.settings.js","url":"settings.js"}
],
"data": [{"name":"ffcniftya.json"}]
},
{
"id": "ffcniftyb",
@ -4382,7 +4462,7 @@
{
"id": "gpstouch",
"name": "GPS Touch",
"version": "0.01",
"version": "0.02",
"description": "A touch based GPS watch, shows OS map reference",
"icon": "gpstouch.png",
"screenshots": [{"url":"screenshot4.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot1.png"}],
@ -4414,7 +4494,7 @@
"name": "Q Alarm and Timer",
"shortName": "Q Alarm",
"icon": "app.png",
"version": "0.03",
"version": "0.04",
"description": "Alarm and timer app with days of week and 'hard' option.",
"tags": "tool,alarm,widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
@ -4472,7 +4552,7 @@
"name": "A Battery Widget (with percentage)",
"shortName":"A Battery Widget",
"icon": "widget.png",
"version":"1.02",
"version":"1.03",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
@ -4487,7 +4567,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.08",
"version":"0.13",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
@ -4621,7 +4701,7 @@
"shortName":"93 Dub",
"icon": "93dub.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.05",
"version":"0.06",
"description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo",
"tags": "clock",
"type": "clock",
@ -4710,7 +4790,7 @@
"icon": "mylocation.png",
"type": "app",
"screenshots": [{"url":"screenshot_1.png"}],
"version":"0.01",
"version":"0.02",
"description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README",
"readme": "README.md",
"tags": "tool,utility",
@ -4727,7 +4807,7 @@
"id": "pebble",
"name": "Pebble Clock",
"shortName": "Pebble",
"version": "0.06",
"version": "0.07",
"description": "A pebble style clock to keep the rebellion going",
"dependencies": {"widpedom":"app"},
"readme": "README.md",
@ -4735,7 +4815,7 @@
"screenshots": [{"url":"pebble_screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"pebble.app.js","url":"pebble.app.js"},
{"name":"pebble.settings.js","url":"pebble.settings.js"},
@ -4769,7 +4849,7 @@
"screenshots": [{"url":"screenshot_widbata_1.png"}],
"version":"0.01",
"type": "widget",
"supports": ["BANGLEJS2"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Shows the current battery level status in the top right using the clocks colour theme",
"tags": "widget,battery",
@ -4797,7 +4877,7 @@
{
"id": "menuwheel",
"name": "Wheel Menus",
"version": "0.01",
"version": "0.02",
"description": "Replace Bangle.js 2's menus with a version that contains variable-size text and a back button",
"readme": "README.md",
"icon": "icon.png",
@ -4914,7 +4994,7 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
"version": "0.03",
"version": "0.04",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
@ -4982,7 +5062,7 @@
{ "id": "pooqround",
"name": "pooq Round watch face",
"shortName":"pooq Round",
"version":"0.01",
"version":"0.02",
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
"icon": "app.png",
"type": "clock",
@ -5001,7 +5081,7 @@
{
"id": "coretemp",
"name": "CoreTemp",
"version": "0.02",
"version": "0.03",
"description": "Display CoreTemp device sensor data",
"icon": "coretemp.png",
"type": "app",
@ -5011,6 +5091,7 @@
"storage": [
{"name":"coretemp.wid.js","url":"widget.js"},
{"name":"coretemp.app.js","url":"coretemp.js"},
{"name":"coretemp.recorder.js","url":"recorder.js"},
{"name":"coretemp.settings.js","url":"settings.js"},
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true},
{"name":"coretemp.boot.js","url":"boot.js"}
@ -5035,7 +5116,7 @@
{
"id": "lapcounter",
"name": "Lap Counter",
"version": "0.01",
"version": "0.02",
"description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
@ -5070,10 +5151,10 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.03",
"version":"0.05",
"description": "A clock with circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}],
"dependencies": {"widpedom":"app"},
"type": "clock",
"tags": "clock",
@ -5121,6 +5202,60 @@
{"name":"ltherm.img","url":"icon.js","evaluate":true}
]
},
{
"id": "ftclock",
"name": "Four Twenty Clock",
"version": "0.02",
"description": "A clock that tells when and where it's going to be 4:20 next",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"ftclock.app.js","url":"app.js"},
{"name":"fourTwenty","url":"fourTwenty.js"},
{"name":"fourTwentyTz","url":"fourTwentyTz.js"},
{"name":"ftclock.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "mmind",
"name": "Classic Mind Game",
"shortName":"Master Mind",
"icon": "mmind.png",
"version":"0.01",
"description": "This is the classic game for masterminds",
"screenshots": [{"url":"screenshot_mmind.png"}],
"type": "app",
"tags": "game",
"readme":"README.md",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"mmind.app.js","url":"mmind.app.js"},
{"name":"mmind.img","url":"mmind.icon.js","evaluate":true}
]
},
{
"id": "presentor",
"name": "Presentor",
"version": "3.0",
"description": "Use your Bangle to present!",
"icon": "app.png",
"type": "app",
"tags": "tool,bluetooth",
"interface": "interface.html",
"readme":"README.md",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"presentor.app.js","url":"app.js"},
{"name":"presentor.img","url":"app-icon.js","evaluate":true},
{"name":"presentor.json","url":"settings.json"}
]
},
{
"id": "slash",
"name": "Slash Watch",
@ -5142,15 +5277,16 @@
{
"id": "promenu",
"name": "Pro Menu",
"version": "0.01",
"description": "Replace Bangle.js 1's built in menu function.",
"version": "0.02",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png",
"type": "boot",
"tags": "system",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"pro-menu-screenshot.png"}],
"storage": [
{"name":"promenu.boot.js","url":"boot.js"},
{"name":"promenu.boot.js","url":"boot.js","supports": ["BANGLEJS"]},
{"name":"promenu.boot.js","url":"bootb2.js","supports": ["BANGLEJS2"]},
{"name":"promenu.img","url":"promenuIcon.js","evaluate":true}
]
},
@ -5262,7 +5398,7 @@
{ "id": "colorful_clock",
"name": "Colorful Analog Clock",
"shortName":"Colorful Clock",
"version":"0.02",
"version":"0.03",
"description": "a colorful analog clock",
"icon": "app-icon.png",
"type": "clock",
@ -5357,5 +5493,188 @@
{ "name": "diract.app.js", "url": "diract.js" },
{ "name": "diract.img", "url": "diract-icon.js", "evaluate": true }
]
},
{
"id": "sonicclk",
"name": "Sonic Clock",
"version": "1.01",
"description": "A classic sonic clock featuring run, stop and wait animations.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"sonicclk.app.js","url":"app.js"},
{"name":"sonicclk.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "touchmenu",
"name": "TouchMenu",
"version": "0.01",
"description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2",
"screenshots": [{"url":"touchmenu.gif"}],
"icon": "touchmenu.png",
"type": "bootloader",
"tags": "tool",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"touchmenu.boot.js","url":"touchmenu.boot.js"}
]
},
{
"id": "puzzle15",
"name": "15 puzzle",
"version": "0.05",
"description": "A 15 puzzle game with drag gesture interface",
"readme":"README.md",
"icon": "puzzle15.app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "game",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"puzzle15.app.js","url":"puzzle15.app.js"},
{"name":"puzzle15.settings.js","url":"puzzle15.settings.js"},
{"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true}
],
"data": [{"name":"puzzle15.json"}]
},
{
"id": "flipper",
"name": "Flipper",
"version": "0.01",
"description": "Switch between dark and light theme and vice versa, combine with pattern launcher and swipe to flip.",
"readme":"README.md",
"screenshots": [{"url":"flipper.png"}],
"icon": "flipper.png",
"type": "app",
"tags": "game",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"flipper.app.js","url":"flipper.app.js"},
{"name":"flipper.img","url":"flipper.icon.js","evaluate":true}
]
},
{ "id": "ruuviwatch",
"name": "Ruuvi Watch",
"shortName":"Ruuvi Watch",
"icon": "ruuviwatch.png",
"version":"1.01",
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.",
"readme":"README.md",
"tags": "bluetooth",
"supports": ["BANGLEJS"],
"storage": [
{"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"},
{"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true}
]
},
{
"id": "limelight",
"name": "Limelight",
"version": "0.01",
"description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)",
"icon": "limelight.png",
"readme":"README.md",
"screenshots": [{"url":"screenshot_limelight.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"limelight.app.js","url":"limelight.app.js"},
{"name":"limelight.settings.js","url":"limelight.settings.js"},
{"name":"limelight.img","url":"limelight.icon.js","evaluate":true}
]
},
{ "id": "banglexercise",
"name": "BanglExercise",
"shortName":"BanglExercise",
"version":"0.02",
"description": "Can automatically track exercises while wearing the Bangle.js watch.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "sport",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"banglexercise.app.js","url":"app.js"},
{"name":"banglexercise.img","url":"app-icon.js","evaluate":true},
{"name":"banglexercise.settings.js","url":"settings.js"}
],
"data": [
{"name":"banglexercise.json"}
]
},
{
"id": "widpa",
"name": "Simple Pedometer",
"shortName":"Simple Pedometer",
"icon": "screenshot_widpa.png",
"screenshots": [{"url":"screenshot_widpa.png"}],
"version":"0.02",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in 12x16 font, requires firmware v2.11.21 or later",
"tags": "widget,battery",
"storage": [
{"name":"widpa.wid.js","url":"widpa.wid.js"}
]
},
{
"id": "widpb",
"name": "Lato Pedometer",
"shortName":"Lato Pedometer",
"icon": "screenshot_widpb.png",
"screenshots": [{"url":"screenshot_widpb.png"}],
"version":"0.02",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in the Lato font, requires firmware v2.11.21 or later",
"tags": "widget,battery",
"storage": [
{"name":"widpb.wid.js","url":"widpb.wid.js"}
]
},
{
"id": "timeandlife",
"name": "Time and Life",
"shortName":"Time and Lfie",
"icon": "app.png",
"version":"0.1",
"description": "A simple watchface which displays the time when the screen is tapped and decays according to the rules of Conway's game of life.",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"timeandlife.app.js","url":"app.js"},
{"name":"timeandlife.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "acmaze",
"name": "AccelaMaze",
"shortName":"AccelaMaze",
"version":"0.01",
"description": "Tilt the watch to roll a ball through a maze",
"icon": "app.png",
"tags": "game",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"acmaze.app.js","url":"app.js"},
{"name":"acmaze.img","url":"app-icon.js","evaluate":true}
]
}
]

View File

@ -3,3 +3,4 @@
0.03: Code style cleanup
0.04: Set 00:00 to 12:00 for 12 hour time
0.05: Display time, even on Thursday
0.06: Fix light theme issue, where widgets would end up on a light strip

View File

@ -122,7 +122,13 @@ function draw(){
queueDraw();
}
/**
* This watch is mostly dark, it does not make sense to respect the
* light theme as you end up with a white strip at the top for the
* widgets and black watch. So set the colours to the dark theme.
*
*/
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
draw();
//the following section is also from waveclk

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

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

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

BIN
apps/ac_ac/custom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
apps/ac_ac/none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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

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

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

BIN
apps/accelgraph/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

1
apps/acmaze/ChangeLog Normal file
View File

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

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

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

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

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

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

@ -0,0 +1,276 @@
const MARGIN = 25;
const WALL_RIGHT = 1, WALL_DOWN = 2;
const STATUS_GENERATING = 0, STATUS_PLAYING = 1,
STATUS_SOLVED = 2, STATUS_ABORTED = -1;
function Maze(n) {
this.n = n;
this.status = STATUS_GENERATING;
this.wall_length = Math.floor((g.getHeight()-2*MARGIN)/n);
this.total_length = this.wall_length*n;
this.margin = Math.floor((g.getHeight()-this.total_length)/2);
this.ball_x = 0;
this.ball_y = 0;
this.clearScreen = function() {
g.clearRect(
0, this.margin,
g.getWidth(), this.margin+this.total_length
);
};
this.clearScreen();
g.setColor(g.theme.fg);
for (let i=0; i<=n; i++) {
g.drawRect(
this.margin, this.margin+i*this.wall_length,
g.getWidth()-this.margin, this.margin+i*this.wall_length
);
g.drawRect(
this.margin+i*this.wall_length, this.margin,
this.margin+i*this.wall_length, g.getHeight() - this.margin
);
}
this.walls = new Uint8Array(n*n);
this.groups = new Uint8Array(n*n);
for (let cell = 0; cell<n*n; cell++) {
this.walls[cell] = WALL_RIGHT|WALL_DOWN;
this.groups[cell] = cell;
}
let from_group, to_group;
let ngroups = n*n;
while (--ngroups) {
// Abort if BTN1 pressed [grace period for menu]
// (for some reason setWatch() fails inside constructor)
if (ngroups<n*n-4 && digitalRead(BTN1)) {
aborting = true;
return;
}
from_group = to_group = -1;
while (from_group<0) {
if (Math.random()<0.5) { // try to break a wall right
let r = Math.floor(Math.random()*n);
let c = Math.floor(Math.random()*(n-1));
let cell = r*n+c;
if (this.groups[cell]!=this.groups[cell+1]) {
this.walls[cell] &= ~WALL_RIGHT;
g.clearRect(
this.margin+(c+1)*this.wall_length,
this.margin+r*this.wall_length+1,
this.margin+(c+1)*this.wall_length,
this.margin+(r+1)*this.wall_length-1
);
g.flip(); // show progress.
from_group = this.groups[cell];
to_group = this.groups[cell+1];
}
} else { // try to break a wall down
let r = Math.floor(Math.random()*(n-1));
let c = Math.floor(Math.random()*n);
let cell = r*n+c;
if (this.groups[cell]!=this.groups[cell+n]) {
this.walls[cell] &= ~WALL_DOWN;
g.clearRect(
this.margin+c*this.wall_length+1,
this.margin+(r+1)*this.wall_length,
this.margin+(c+1)*this.wall_length-1,
this.margin+(r+1)*this.wall_length
);
from_group = this.groups[cell];
to_group = this.groups[cell+n];
}
}
}
for (let cell = 0; cell<n*n; cell++) {
if (this.groups[cell]==from_group) {
this.groups[cell] = to_group;
}
}
}
this.clearScreen = function() {
g.clearRect(
0, MARGIN, g.getWidth(), g.getHeight()-MARGIN-1
);
};
this.clearCell = function(r, c) {
if (!r && !c) {
g.setColor("#ffff00");
} else if (r==this.n-1 && c==this.n-1) {
g.setColor("#00ff00");
} else {
g.setColor(g.theme.bg);
}
g.fillRect(
this.margin+this.wall_length*c+1,
this.margin+this.wall_length*r+1,
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*(r+1)
);
g.setColor(g.theme.fg);
if (this.walls[r*n+c]&WALL_RIGHT) {
g.fillRect(
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*r,
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*(r+1)
);
}
if (this.walls[r*n+c]&WALL_DOWN) {
g.fillRect(
this.margin+this.wall_length*c,
this.margin+this.wall_length*(r+1),
this.margin+this.wall_length*(c+1),
this.margin+this.wall_length*(r+1)
);
}
};
this.drawBall = function(x, y) {
g.setColor("#ff0000");
g.fillEllipse(
this.margin+x+1,
this.margin+y+1,
this.margin+x+this.wall_length-1,
this.margin+y+this.wall_length-1
);
g.setColor(g.theme.fg);
};
this.move = function(dx, dy) {
let next_x = this.ball_x,
next_y = this.ball_y,
ball_r = Math.floor(this.ball_y/this.wall_length),
ball_c = Math.floor(this.ball_x/this.wall_length);
if (this.ball_x%this.wall_length) {
if (dx) {
next_x += dx;
} else {
return false;
}
} else if (this.ball_y%this.wall_length) {
if (dy) {
next_y += dy;
} else {
return false;
}
} else { // exactly in a cell. Check walls
if (dy<0 && ball_r>0 && !(this.walls[n*(ball_r-1)+ball_c]&WALL_DOWN)) {
next_y--;
} else if (dy>0 && ball_r<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_DOWN)) {
next_y++;
} else if (dx<0 && ball_c>0 && !(this.walls[n*ball_r+ball_c-1]&WALL_RIGHT)) {
next_x--;
} else if (dx>0 && ball_c<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_RIGHT)) {
next_x++;
} else {
return false;
}
}
this.clearCell(ball_r, ball_c);
if (this.ball_x%this.wall_length) {
this.clearCell(ball_r, ball_c+1);
}
if (this.ball_y%this.wall_length) {
this.clearCell(ball_r+1, ball_c);
}
this.ball_x = next_x;
this.ball_y = next_y;
this.drawBall(this.ball_x, this.ball_y);
if (this.ball_x==(n-1)*this.wall_length && this.ball_y==(n-1)*this.wall_length) {
this.status = STATUS_SOLVED;
}
return true;
};
this.try_move_horizontally = function(accel_x) {
if (accel_x>0.15) {
return this.move(-1, 0);
} else if (accel_x<-0.15) {
return this.move(1, 0);
}
return false;
};
this.try_move_vertically = function(accel_y) {
if (accel_y<-0.15) {
return this.move(0,1);
} else if (accel_y>0.15) {
return this.move(0,-1);
}
return false;
};
this.tick = function() {
accel = Bangle.getAccel();
if (this.ball_x%this.wall_length) {
this.try_move_horizontally(accel.x);
} else if (this.ball_y%this.wall_length) {
this.try_move_vertically(accel.y);
} else {
if (Math.abs(accel.x)>Math.abs(accel.y)) { // prefer horizontally
if (!this.try_move_horizontally(accel.x)) {
this.try_move_vertically(accel.y);
}
} else { // prefer vertically
if (!this.try_move_vertically(accel.y)) {
this.try_move_horizontally(accel.x);
}
}
}
};
this.clearCell(0,0);
this.clearCell(n-1,n-1);
this.drawBall(0,0);
this.status = STATUS_PLAYING;
}
function timeToText(t) { // Courtesy of stopwatch app
let hrs = Math.floor(t/3600000);
let mins = Math.floor(t/60000)%60;
let secs = Math.floor(t/1000)%60;
let tnth = Math.floor(t/100)%10;
let text;
if (hrs === 0)
text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
else
text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
return text;
}
let aborting = false;
let start_time = 0;
let duration = 0;
let maze=null;
let mazeMenu = {
"": { "title": "Maze size", "selected": 1 },
"Easy (8x8)": function() { E.showMenu(); maze = new Maze(8); },
"Medium (10x10)": function() { E.showMenu(); maze = new Maze(10); },
"Hard (14x14)": function() { E.showMenu(); maze = new Maze(14); },
"< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock
};
g.clear(true);
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setLocked(false);
Bangle.setLCDTimeout(0);
E.showMenu(mazeMenu);
let maze_interval = setInterval(
function() {
if (maze) {
if (digitalRead(BTN1) || maze.status==STATUS_ABORTED) {
console.log(`aborting ${start_time}`);
maze = null;
start_time = duration = 0;
aborting = false;
setTimeout(function() {E.showMenu(mazeMenu); }, 100);
return;
}
if (!start_time) {
start_time = Date.now();
}
if (maze.status==STATUS_PLAYING) {
maze.tick();
}
if (maze.status==STATUS_SOLVED && !duration) {
duration = Date.now()-start_time;
g.setFontAlign(0,0).setColor(g.theme.fg);
g.setFont("Vector",18);
g.drawString(`Solved in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true);
}
}
}, 25);

BIN
apps/acmaze/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
apps/acmaze/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

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

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

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

View File

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

View File

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

View File

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

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

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 1.6 KiB

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

BIN
apps/banglexercise/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

View File

@ -43,3 +43,5 @@
0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app
0.38: Option to log to file if settings.log==2
0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035)
0.40: Bootloader now rebuilds for new firmware versions
0.41: Add Keyboard and Mouse Bluetooth HID option

View File

@ -6,11 +6,11 @@ var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var boot = "";
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`;
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} else {
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/));
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`;
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
}
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
boot += `E.setFlags({pretokenise:1});\n`;
@ -18,6 +18,7 @@ boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
else if (s.HID=="com") boot += `Bangle.HID = E.toUint8Array(atob("BQEJAqEBhQEJAaEABQkZASkFFQAlAZUFdQGBApUBdQOBAwUBCTAJMQk4FYElf3UIlQOBBgUMCjgCFYElf3UIlQGBBsDABQEJBqEBhQIFBxngKecVACUBdQGVCIECdQiVAYEBGQApcxUAJXOVBXUIgQDA"));`
else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));`
else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`;
boot += `bleServiceOptions.hid=Bangle.HID;\n`;

View File

@ -1 +1,7 @@
0.01: New App!
0.02: Make overriding the HRM event optional
Emit BTHRM event for external sensor
Add recorder app plugin
0.03: Prevent readings from internal sensor mixing into BT values
Mark events with src property
Show actual source of event in app

View File

@ -3,23 +3,41 @@
var gatt;
var status;
Bangle.isHRMOn = function() {
var origIsHRMOn = Bangle.isHRMOn;
Bangle.isBTHRMOn = function(){
return (status=="searching" || status=="connecting") || (gatt!==undefined);
}
Bangle.setHRMPower = function(isOn, app) {
Bangle.isHRMOn = function() {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled && !settings.replace){
return origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return origIsHRMOn() || Bangle.isBTHRMOn();
}
Bangle.setBTHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
// Do app power handling
if (!app) app="?";
log("setHRMPower ->", isOn, app);
log("setBTHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[];
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app);
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app);
isOn = Bangle._PWR.HRM.length;
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
log("setHRMPower on", app);
if (!Bangle.isHRMOn()) {
log("HRM not already on");
log("setBTHRMPower on", app);
if (!Bangle.isBTHRMOn()) {
log("BTHRM not already on");
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
@ -49,9 +67,11 @@
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit('HRM',{
Bangle.emit(settings.replace?"HRM":"BTHRM", {
bpm:bpm,
confidence:100
confidence:100,
src:settings.replace?"bthrm":undefined
});
});
return characteristic.startNotifications();
@ -65,15 +85,39 @@
});
}
} else { // not on
log("setHRMPower off", app);
log("setBTHRMPower off", app);
if (gatt) {
log("HRM connected - disconnecting");
log("BTHRM connected - disconnecting");
status = undefined;
try {gatt.disconnect();}catch(e) {
log("HRM disconnect error", e);
log("BTHRM disconnect error", e);
}
gatt = undefined;
}
}
};
var origSetHRMPower = Bangle.setHRMPower;
Bangle.setHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled || !isOn){
Bangle.setBTHRMPower(isOn, app);
}
if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){
origSetHRMPower(isOn, app);
}
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled && settings.replace){
if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break;
}
}
}
})();

61
apps/bthrm/bthrm.js Normal file
View File

@ -0,0 +1,61 @@
var btm = g.getHeight()-1;
var eventInt = null;
var eventBt = null;
var counterInt = 0;
var counterBt = 0;
function draw(y, event, type, counter) {
var px = g.getWidth()/2;
g.reset();
g.setFontAlign(0,0);
g.clearRect(0,y,g.getWidth(),y+75);
if (type == null || event == null || counter == 0) return;
var str = event.bpm + "";
g.setFontVector(40).drawString(str,px,y+20);
str = "Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+50);
str = "Event: " + type;
if (type == "HRM") str += " Source: " + (event.src ? event.src : "internal");
g.setFontVector(12).drawString(str,px,y+60);
}
function onBtHrm(e) {
print("Event for BT " + JSON.stringify(e));
counterBt += 5;
eventBt = e;
}
function onHrm(e) {
print("Event for Int " + JSON.stringify(e));
counterInt += 5;
eventInt = e;
}
Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm);
Bangle.setHRMPower(1,'bthrm')
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
function drawInt(){
counterInt--;
if (counterInt < 0) counterInt = 0;
if (counterInt > 5) counterInt = 5;
draw(24, eventInt, "HRM", counterInt);
}
function drawBt(){
counterBt--;
if (counterBt < 0) counterBt = 0;
if (counterBt > 5) counterBt = 5;
draw(100, eventBt, "BTHRM", counterBt);
}
var interval = setInterval(drawInt, 1000);
var interval = setInterval(drawBt, 1000);

27
apps/bthrm/recorder.js Normal file
View File

@ -0,0 +1,27 @@
(function(recorders) {
recorders.bthrm = function() {
var bpm = "";
function onHRM(h) {
bpm = h.bpm;
}
return {
name : "BTHR",
fields : ["BT Heartrate"],
getValues : () => {
result = [bpm];
bpm = "";
return result;
},
start : () => {
Bangle.on('BTHRM', onHRM);
Bangle.setBTHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('BTHRM', onHRM);
Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
}
})

33
apps/bthrm/settings.js Normal file
View File

@ -0,0 +1,33 @@
(function(back) {
var FILE = "bthrm.json";
var settings = Object.assign({
enabled: true,
replace: true,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
onchange: v => {
settings.enabled = v;
writeSettings();
}
},
'Use HRM event': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
onchange: v => {
settings.replace = v;
writeSettings();
}
}
});
})

View File

@ -3,3 +3,4 @@
0.03: Add setting to start week on Sunday
0.04: Add setting to switch color schemes. On Bangle 2 non-dithering colors will be used by default. Use localized names for months and days of the week (Language app needed).
0.05: Update calendar weekend colors for start on Sunday
0.06: Use larger font for dates

View File

@ -206,6 +206,8 @@ function drawCalendar(date) {
y2 - 1
);
}
require("Font8x12").add(Graphics);
g.setFont("8x12", fontSize);
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
g.drawString(
(day > 50 ? day - 50 : day).toString(),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,3 +1,9 @@
0.01: New clock
0.02: Fix icon & add battery warn functionality
0.03: Theming support & minor fixes
0.04: Make configurable what to show in each circle
Add step distance and weather
Allow switching visibility of widgets
Make circles and text slightly bigger
0.05: Show correct percentage values in circles
Show humidity as weather circle data

View File

@ -2,19 +2,25 @@
A clock with circles for different data at the bottom in a probably familiar style
It shows besides time, date and day of week the following information:
By default the time, date and day of week is shown.
It can show the following information (this can be configured):
* Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer))
* Heart rate (when screen is on and unlocked)
* Battery (including charging and battery low)
* Steps distance (depending on steps)
* Heart rate (automatically updates when screen is on and unlocked)
* Battery (including charging status and battery low warning)
* Weather (requires [weather app](https://banglejs.com/apps/#weather))
* Humidity as circle progress
* Temperature inside circle
* Condition as icon below circle
## Screenshot
## Screenshots
![Screenshot dark theme](screenshot-dark.png)
![Screenshot light theme](screenshot-light.png)
![Screenshot](screenshot.png)
## TODO
* Show weather information
* Configure which information to show in each circle
* Configure visibility of widgets
# TODO
* Add sunrise and sunset
* Display moon instead of sun during night on weather circle
## Creator
Marco ([myxor](https://github.com/myxor))

View File

@ -1,19 +1,37 @@
const locale = require("locale");
const heatshrink = require("heatshrink");
const storage = require("Storage");
const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA="));
const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD"));
const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI"));
const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI"));
const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA"));
const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA"));
const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA="));
const weatherPartlyCloudy = heatshrink.decompress(atob("iEQwYQNv0AjgGDn4EDh///gFChwREC4MfxwIBv0//+AC4X4j4FCv/AgfwgED/wIBuAaBBwgFDgP4gf/AAXABwIEBDQQAEA=="));
const weatherRainy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AocAnAFBFIU4EAM//gRBEAIOBhw1C/AmDAosAC4JNIAAg"));
const weatherPartlyRainy = heatshrink.decompress(atob("h0OwYJGjkAnAFCj+AAgU//4FCuEA8EAg8ch/4gEB4////AAoIIBCIMD/wgCg4bBg/8BwMD+AgBh4ZBDQf/FIIABh4IBgAA=="));
const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN/4EDx/4j/x4EAgIIBwAXBAogRFDoopFGoxBGABIA="));
const weatherFoggy = heatshrink.decompress(atob("iEQwYROn/8AgUB/EfwAFBh/AgfwgED/wIBuEABwd/4EcDQgFDgE4Fosf///8f//A/Lj/xCQIRNA="));
const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AoX8gE4AoQpBnAdBF4IRBDQMH/kOHgY7DAo4AOA=="));
let settings;
function loadSettings() {
settings = require("Storage").readJSON("circlesclock.json", 1) || {
settings = storage.readJSON("circlesclock.json", 1) || {
'minHR': 40,
'maxHR': 200,
'stepGoal': 10000,
'batteryWarn': 30
'stepDistanceGoal': 8000,
'stepLength': 0.8,
'batteryWarn': 30,
'showWidgets': false,
'circle1': 'hr',
'circle2': 'steps',
'circle3': 'battery'
};
// Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
@ -21,122 +39,229 @@ function loadSettings() {
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
}
loadSettings();
const showWidgets = settings.showWidgets || false;
let hrtValue;
// layout values:
const colorFg = g.theme.dark ? '#fff' : '#000';
const colorBg = g.theme.dark ? '#000' : '#fff';
const colorGrey = '#808080';
const colorRed = '#ff0000';
const colorGreen = '#00ff00';
let hrtValue;
const h = g.getHeight();
const colorGreen = '#008000';
const colorBlue = '#0000ff';
const colorYellow = '#ffff00';
const widgetOffset = showWidgets ? 24 : 0;
const h = g.getHeight() - widgetOffset;
const w = g.getWidth();
const hOffset = 30;
const hOffset = 30 - widgetOffset;
const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset);
const w1 = Math.round(w / 6);
const w2 = Math.round(3 * w / 6);
const w3 = Math.round(5 * w / 6);
const radiusOuter = 22;
const radiusInner = 16;
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions
const radiusOuter = 25;
const radiusInner = 20;
const circleFont = "Vector:15";
const circleFontBig = "Vector:16";
const circleFontSmall = "Vector:13";
function draw() {
g.reset();
g.clear(true);
if (!showWidgets) {
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
if (WIDGETS && typeof WIDGETS === "object") {
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
}
}
} else {
Bangle.drawWidgets();
}
g.setColor(colorBg);
g.fillRect(0, 0, w, h);
g.fillRect(0, widgetOffset, w, h);
// time
g.setFont("Vector:50");
g.setFontAlign(-1, -1);
g.setFontAlign(0, -1);
g.setColor(colorFg);
g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8);
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8);
// date & dow
g.setFont("Vector:20");
g.setFont("Vector:21");
g.setFontAlign(-1, 0);
g.drawString(locale.date(new Date()), w / 10, h2);
g.drawString(locale.dow(new Date()), w / 10, h2 + 22);
g.drawString(locale.date(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2);
g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + 22);
// Steps circle
drawSteps();
// Heart circle
drawHeartRate();
// Battery circle
drawBattery();
drawCircle(1);
drawCircle(2);
drawCircle(3);
}
const defaultCircleTypes = ["steps", "hr", "battery"];
function drawCircle(index) {
let type = settings['circle' + index];
if (!type) type = defaultCircleTypes[index - 1];
const w = getCirclePosition(type);
switch (type) {
case "steps":
drawSteps(w);
break;
case "stepsDist":
drawStepsDistance(w);
break;
case "hr":
drawHeartRate(w);
break;
case "battery":
drawBattery(w);
break;
case "weather":
drawWeather(w);
break;
}
}
function drawSteps() {
function getCirclePosition(type) {
for (let i = 1; i <= 3; i++) {
const setting = settings['circle' + i];
if (setting == type) return circlePosX[i - 1];
}
for (let i = 0; i < defaultCircleTypes.length; i++) {
if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
return circlePosX[i];
}
}
return undefined;
}
function isCircleEnabled(type) {
return getCirclePosition(type) != undefined;
}
function drawSteps(w) {
if (!w) w = getCirclePosition("steps");
const steps = getSteps();
const blue = '#0000ff';
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w1, h3, radiusOuter);
g.fillCircle(w, h3, radiusOuter);
const stepGoal = settings.stepGoal || 10000;
if (stepGoal > 0) {
let percent = steps / stepGoal;
if (stepGoal < steps) percent = 1;
drawGauge(w1, h3, percent, blue);
drawGauge(w, h3, percent, colorBlue);
}
g.setColor(colorBg);
g.fillCircle(w1, h3, radiusInner);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(steps), w1 + 2, h3);
g.drawString(shortValue(steps), w + 2, h3);
g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6);
g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6);
}
function drawHeartRate() {
function drawStepsDistance(w) {
if (!w) w = getCirclePosition("steps");
const steps = getSteps();
const stepDistance = settings.stepLength || 0.8;
const stepsDistance = Math.round(steps * stepDistance);
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w2, h3, radiusOuter);
g.fillCircle(w, h3, radiusOuter);
const stepDistanceGoal = settings.stepDistanceGoal || 8000;
if (stepDistanceGoal > 0) {
let percent = stepsDistance / stepDistanceGoal;
if (stepDistanceGoal < stepsDistance) percent = 1;
drawGauge(w, h3, percent, colorGreen);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(stepsDistance), w + 2, h3);
g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6);
}
function drawHeartRate(w) {
if (!w) w = getCirclePosition("hr");
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
if (hrtValue != undefined && hrtValue > 0) {
const minHR = 40;
const minHR = settings.minHR || 40;
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
drawGauge(w2, h3, percent, colorRed);
drawGauge(w, h3, percent, colorRed);
}
g.setColor(colorBg);
g.fillCircle(w2, h3, radiusInner);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFont(circleFontBig);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(hrtValue != undefined ? hrtValue : "-", w2, h3);
g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3);
g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6);
g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6);
}
function drawBattery() {
function drawBattery(w) {
if (!w) w = getCirclePosition("battery");
const battery = E.getBattery();
const yellow = '#ffff00';
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w3, h3, radiusOuter);
g.fillCircle(w, h3, radiusOuter);
if (battery > 0) {
const percent = battery / 100;
drawGauge(w3, h3, percent, yellow);
drawGauge(w, h3, percent, colorYellow);
}
g.setColor(colorBg);
g.fillCircle(w3, h3, radiusInner);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFont(circleFont);
g.setFontAlign(0, 0);
let icon = powerIcon;
@ -144,17 +269,100 @@ function drawBattery() {
if (Bangle.isCharging()) {
color = colorGreen;
icon = powerIconGreen;
}
else {
} else {
if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) {
color = colorRed;
icon = powerIconRed;
}
}
g.setColor(color);
g.drawString(battery + '%', w3, h3);
g.drawString(battery + '%', w, h3);
g.drawImage(icon, w3 - 6, h3 + radiusOuter - 6);
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
}
function drawWeather(w) {
if (!w) w = getCirclePosition("weather");
const weather = getWeather();
const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
const humidity = weather ? weather.hum : undefined;
const code = weather ? weather.code : -1;
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
if (humidity >= 0) {
drawGauge(w, h3, humidity / 100, colorYellow);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w, h3, w - 25, h3 + radiusOuter + 5, w + 25, h3 + radiusOuter + 5]);
const content = tempString ? tempString : "?";
g.setFont(content.length < 4 ? circleFont : circleFontSmall);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(content, w, h3);
if (code > 0) {
const icon = getWeatherIconByCode(code);
if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10);
}
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
function getWeatherIconByCode(code) {
const codeGroup = Math.round(code / 100);
switch (codeGroup) {
case 2:
return weatherStormy;
case 3:
return weatherCloudy;
case 5:
switch (code) {
case 511:
return weatherSnowy;
case 520:
return weatherPartlyRainy;
case 521:
return weatherPartlyRainy;
case 522:
return weatherPartlyRainy;
case 531:
return weatherPartlyRainy;
default:
return weatherRainy;
}
break;
case 6:
return weatherSnowy;
case 7:
return weatherFoggy;
case 8:
switch (code) {
case 800:
return weatherSunny;
case 801:
return weatherPartlyCloudy;
case 802:
return weatherPartlyCloudy;
default:
return weatherCloudy;
}
break;
default:
return undefined;
}
return undefined;
}
function radians(a) {
@ -162,22 +370,21 @@ function radians(a) {
}
function drawGauge(cx, cy, percent, color) {
let offset = 30;
let end = 300;
var i = 0;
var r = radiusInner + 3;
const offset = 15;
const end = 345;
const r = radiusInner + 3;
if (percent <= 0) return;
if (percent > 1) percent = 1;
var startrot = -offset;
var endrot = startrot - ((end - offset) * percent) - 15;
const startrot = -offset;
const endrot = startrot - ((end - offset) * percent);
g.setColor(color);
const size = 4;
const size = radiusOuter - radiusInner - 2;
// draw gauge
for (i = startrot; i > endrot - size; i -= size) {
for (let i = startrot; i > endrot - size; i -= size) {
x = cx + r * Math.sin(radians(i));
y = cy + r * Math.cos(radians(i));
g.fillCircle(x, y, size);
@ -198,54 +405,56 @@ function shortValue(v) {
}
function getSteps() {
if (WIDGETS.wpedom !== undefined) {
if (WIDGETS && WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}
return 0;
}
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
Bangle.setHRMPower(1, "watch");
function getWeather() {
const jsonWeather = storage.readJSON('weather.json');
return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
}
function enableHRMSensor() {
Bangle.setHRMPower(1, "circleclock");
if (hrtValue == undefined) {
hrtValue = '...';
drawHeartRate();
}
} else {
Bangle.setHRMPower(0, "watch");
}
drawHeartRate();
drawSteps();
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
draw();
} else {
Bangle.setHRMPower(0, "circleclock");
}
});
Bangle.on('HRM', function(hrm) {
//if(hrm.confidence > 90){
if (isCircleEnabled("hr")) {
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
drawHeartRate();
//} else {
// hrtValue = undefined;
//}
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
draw();
setInterval(draw, 60000);
Bangle.on('charging', function(charging) {
drawBattery();
if (isCircleEnabled("battery")) drawBattery();
});
g.clear();
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
if (typeof WIDGETS === "object") {
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
}
loadSettings();
setInterval(draw, 60000);
draw();
Bangle.setUI("clock");

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -6,13 +6,26 @@
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"];
var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"];
E.showMenu({
'': { 'title': 'circlesclock' },
'< Back': back,
'min heartrate': {
value: "minHR" in settings ? settings.minHR : 40,
min: 0,
max : 250,
step: 5,
format: x => {
return x;
},
onchange: x => save('minHR', x),
},
'max heartrate': {
value: "maxHR" in settings ? settings.maxHR : 200,
min: 20,
max : 250,
step: 10,
step: 5,
format: x => {
return x;
},
@ -28,7 +41,27 @@
},
onchange: x => save('stepGoal', x),
},
'battery warn lvl': {
'step length': {
value: "stepLength" in settings ? settings.stepLength : 0.8,
min: 0.1,
max : 1.5,
step: 0.01,
format: x => {
return x;
},
onchange: x => save('stepLength', x),
},
'step dist goal': {
value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000,
min: 2000,
max : 30000,
step: 1000,
format: x => {
return x;
},
onchange: x => save('stepDistanceGoal', x),
},
'battery warn': {
value: "batteryWarn" in settings ? settings.batteryWarn : 30,
min: 10,
max : 100,
@ -38,6 +71,28 @@
},
onchange: x => save('batteryWarn', x),
},
'< Back': back,
'show widgets': {
value: "showWidgets" in settings ? settings.showWidgets : false,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x),
},
'left': {
value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle1', valuesCircleTypes[x]),
},
'middle': {
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle2', valuesCircleTypes[x]),
},
'right': {
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle3', valuesCircleTypes[x]),
}
});
});

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Andreas Rozek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,27 @@
# Variable Analog Clock #
This app implements an analog clock with various faces, hands and colors to
choose from.
You have the choice between:
* 4 different clock faces ![](Screenshot_01.png) ![](Screenshot_02.png) ![](Screenshot_03.png) ![](Screenshot_04.png) and
* 3 different clock hands (optionally with or without second hands) ![](Screenshot_11.png) ![](Screenshot_12.png) ![](Screenshot_13.png)
Additionally, you may use the currently configured global theme or configure
your own colors for clock fore- and background and second hands.
Just swipe up or down to switch from clock display to configuration screen
![](Screenshot_21.png) ![](Screenshot_22.png) ![](Screenshot_23.png)
![](Screenshot_24.png) ![](Screenshot_25.png)
Chosen settings will be written to the Bangle.js's flash memory and restored
whenever the clock is started again.
This clock also acts as an example for the building blocks found in the author's
[GitHub repository](https://github.com/rozek/banglejs-2-activities)
## License ##
[MIT License](LICENSE)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Andreas Rozek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,29 @@
# Configurable Analog Clock #
This app implements an analog clock with various faces, hands and colors to
choose from.
You have the choice between:
* 4 different clock faces<br>![](Screenshot-01.png) &nbsp; ![](Screenshot-02.png) &nbsp; ![](Screenshot-03.png) &nbsp; ![](Screenshot-04.png) and
* 3 different clock hands (optionally with or without second hands)<br>![](Screenshot-11.png) &nbsp; ![](Screenshot-12.png) &nbsp; ![](Screenshot-13.png)
Additionally, you may use the currently configured global theme or configure
your own colors for clock fore- and background and second hands.
Just swipe up or down to switch from clock display to the first configuration
screen and continue from there
![](Screenshot-21.png) &nbsp; ![](Screenshot-22.png) &nbsp;
![](Screenshot-23.png) &nbsp; ![](Screenshot-24.png) &nbsp;
![](Screenshot-25.png)
Chosen settings will be written to the Bangle.js's flash memory and restored
whenever the clock is started again.
This clock also acts as an example for the building blocks found in the author's
[GitHub repository](https://github.com/rozek/banglejs-2-activities)
## License ##
[MIT License](LICENSE)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgZC/AB1RgkQsAQMyUKAYMIkAPJgNFiEBgACBg0YCRMogEJkGSAwMSEZNAAQMAEAMGgBKHgXAlECwMgzcAmkAhgRGilRssUgMEEYcBwARFiBHBgQKB7AjCawIQEgoCCigDBjEBwwEBEwIAGlmSEYYABI4PAEYhEBNYIjCAYVtwCSElG2xdoAwQjDhpZEEAMUqAHDCIaPBEYlAiwjItkAgYjFqJHDCIdhI4j1CAAhlEZoTUEAAcGEYZKEEYWgCIgjEWYkBoqwCCITLBgcMmPXhgjCgUB2iFDm3pw0YLAMygEgc4QjF49cmA3BbQQjDgGkI5OwNZZ9FEYoRLEYxmBCI5jBEYQACyQRHgmAEYsEEZEka4kAhEEEY8BCIMJCIYjKgGChAFDCwKzDNYyKEJgUDlgRBAoPDRQQjEZQZzEjScIhgjBEwQjEH4aXEgIjBjYCBjQCBMYYADmAjDFIjcGKocAjBKCgJRCAAwaCEARQBmARIhBrEgSMEAApEBmHAAQJrCABUCjFhwwQMI4oA7"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,3 @@
0.01: New app
0.02: Cleanup interface and add settings, widget, add skin temp reporting.
0.03: Move code for recording to this app

31
apps/coretemp/recorder.js Normal file
View File

@ -0,0 +1,31 @@
(function(recorders) {
recorders.coretemp = function() {
var core = "", skin = "";
var hasCore = false;
function onCore(c) {
core=c.core;
skin=c.skin;
hasCore = true;
}
return {
name : "Core",
fields : ["Core","Skin"],
getValues : () => {
var r = [core,skin];
core = "";
skin = "";
return r;
},
start : () => {
hasCore = false;
Bangle.on('CoreTemp', onCore);
},
stop : () => {
hasCore = false;
Bangle.removeListener('CoreTemp', onCore);
},
draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
};
}
})

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Ported to Banglejs2

View File

@ -35,23 +35,24 @@ function provideFeedback() {
}
function drawHeart() {
g.fillCircle(40, 92, 12);
g.fillCircle(60, 92, 12);
g.fillPoly([29, 98, 50, 120, 71, 98]);
var lowestPoint = g.getHeight()*3/5;
g.fillCircle(40, lowestPoint-29, 12);
g.fillCircle(60, lowestPoint-29, 12);
g.fillPoly([29, lowestPoint-22, 50, lowestPoint, 71, lowestPoint-22]);
}
function updateScreen() {
const colors = [0xFFFF, 0x9492];
g.reset().clearRect(0, 50, 250, 150);
const colors = [0xFFFF-g.getBgColor(), 0x9492];
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight()*5/6);
if (counter > 0) {
g.setFont("Vector", 40).setFontAlign(0, 0);
g.setColor(colors[counter%2]);
drawHeart();
g.drawString(counter + "", g.getWidth()/2, 100);
g.drawString(counter, 120, g.getHeight()*3/5-20);
} else {
g.setFont("Vector", 20).setFontAlign(0, 0);
g.drawString("RESCUE", g.getWidth()/2, 70);
g.drawString("BREATHS", g.getWidth()/2, 120);
g.drawString("RESCUE", g.getWidth()/2, g.getHeight()/3);
g.drawString("BREATHS", g.getWidth()/2, g.getHeight()*3/5);
}
}
@ -73,7 +74,7 @@ function tick() {
interval = setInterval(tick, 60000/setting('compression_rpm'));
g.clear(1).setFont("6x8");
g.drawString(setting('compression_count') + ' / ' + setting('breath_count'), 30, 200);
g.drawString(setting('compression_count') + ' / ' + setting('breath_count'), 30, g.getHeight()*5/6);
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -1,3 +1,8 @@
0.01: App created
0.02: Persist state to storage to enable stopwatch to continue in the background
0.03: Modified to use setUI, theme and different screens
0.04: *bugfix* stopwatch broken with v0.03 setUI
realigned quick n dirty screen positions
help adjusted to fit bangle1 & bangle2 screen-size with widgets
fixed bangle2 colors for chrono and last lap highlight
added screen for bangle2 and a small README

View File

@ -0,0 +1,18 @@
# dev stop watch
stores state at kill
## Bangle 1
![](bangle1-dev-stopwatch-screenshot.png)
* BTN1: start/lap
* BTN2: launcher
* BTN3: reset
## Bangle 2
![](bangle2-dev-stopwatch-screenshot.png)
* TAP top right: start/lap
* TAP bottom right: reset
* Use BTN to get to launcher

View File

@ -3,11 +3,11 @@ const EMPTY_H = '00:00:000';
const MAX_LAPS = 6;
const XY_CENTER = g.getWidth() / 2;
const big = g.getWidth()>200;
const Y_CHRONO = 40;
const Y_HEADER = big?80:60;
const Y_LAPS = big?125:90;
const Y_CHRONO = big?40:30;
const Y_HEADER = big?95:65;
const Y_LAPS = big?125:80;
const H_LAPS = big?15:8;
const Y_BTN3 = big?225:165;
const Y_HELP = big?225:135;
const FONT = '6x8';
const CHRONO = '/* C H R O N O */';
@ -27,18 +27,17 @@ var state = require("Storage").readJSON("devstopwatch.state.json",1) || {
// Show launcher when button pressed
Bangle.setUI("clockupdown", btn=>{
if (btn==0) {
reset = false;
switch (btn) {
case -1:
if (state.started) {
changeLap();
} else {
if (!reset) {
chronoInterval = setInterval(chronometer, 10);
}
break;
case 1: resetChrono(); break;
default: Bangle.showLauncher(); break; //launcher handeled by ROM
}
}
if (btn==1) resetChrono();
});
function resetChrono() {
@ -105,6 +104,7 @@ function printChrono() {
var print = '';
g.setColor(g.theme.fg);
g.setFont(FONT, big?2:1);
print = CHRONO;
g.drawString(print, XY_CENTER, Y_CHRONO, true);
@ -124,7 +124,8 @@ function printChrono() {
let suffix = ' ';
if (state.currentLapIndex === i) {
let suffix = '*';
g.setColor("#f70");
if (process.env.HWVERSION==2) g.setColor("#0ee");
else g.setColor("#f70");
}
const lapLine = `L${i - 1} ${state.laps[i]} ${suffix}\n`;
@ -133,8 +134,17 @@ function printChrono() {
g.setColor(g.theme.fg);
g.setFont(FONT, 1);
print = 'Press 3 to reset';
g.drawString(print, XY_CENTER, Y_BTN3, true);
//help for model 2 or 1
if (process.env.HWVERSION==2) {
print = /*LANG*/'TAP right top/bottom';
g.drawString(print, XY_CENTER, Y_HELP, true);
print = /*LANG*/'start&lap/reset, BTN1: EXIT';
g.drawString(print, XY_CENTER, Y_HELP+10, true);
}
else {
print = /*LANG*/'BTNs 1:startlap 2:exit 3:reset';
g.drawString(print, XY_CENTER, Y_HELP, true);
}
g.flip();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -11,4 +11,4 @@ The year itself begins on the December solstice. Because that always happens, th
The epoch (year numbering) begins in the last year when the perihelion coincided with the June solstice, near the beginning of the Holocene era. That astronomical basis makes the calendar free from politics, religion, or geography.
While the year number remains cardinal, BTN5 toggles between cardinal and ordinal for the rest of the calendar segments. BTN4 adds or removes a quickly changing digit to or from the clock.
While the year number remains cardinal, tapping on the right side of the watch face toggles between cardinal and ordinal for the rest of the calendar segments. Tapping on the left adds or removes a quickly changing digit to or from the clock.

244
apps/doztime/app-bangle2.js Normal file
View File

@ -0,0 +1,244 @@
// Positioning values for graphics buffers
const g_height = 80; // total graphics height
const g_x_off = 0; // position from left was 16, then 8 here
const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240
const g_width = 240 - 2 * g_x_off; // total graphics width
const g_height_d = 28; // height of date region was 32
const g_y_off_d = 0; // y position of date region within graphics region
const spacing = 0; // space between date and time in graphics region
const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region
const g_height_t = 44; // height of time region was 48
// Other vars
const A1 = [30,30,30,30,31,31,31,31,31,31,30,30];
const B1 = [30,30,30,30,30,31,31,31,31,31,30,30];
const B2 = [30,30,30,30,31,31,31,31,31,30,30,30];
const timeColour = "#ffffff";
const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff
const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20
const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20
const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30
const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30
const baseYear = 11584;
const baseDate = Date(2020,11,21); // month values run from 0 to 11
let accum = new Date(baseDate.getTime());
let sequence = [];
let timeActiveUntil;
let addTimeDigit = false;
let dateFormat = false;
let lastX = 999999999;
let res = {};
//var last_time_log = 0;
var drawtime_timeout;
// Date and time graphics buffers
var dateColour = "#ffffff"; // override later
var timeColour2 = timeColour;
var g_d = Graphics.createArrayBuffer(g_width,g_height_d,1,{'msb':true});
var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true});
// Set screen mode and function to write graphics buffers
//Bangle.setLCDMode();
g.clear(); // start with blank screen
g.flip = function()
{
g.setBgColor(0,0,0);
g.setColor(dateColour);
g.drawImage(
{
width:g_width,
height:g_height_d,
buffer:g_d.buffer
}, g_x_off, g_y_off + g_y_off_d);
g.setColor(timeColour2);
g.drawImage(
{
width:g_width,
height:g_height_t,
buffer:g_t.buffer
}, g_x_off, g_y_off + g_y_off_t);
};
setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1
setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2
//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true});
//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true});
//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true});
Bangle.on('touch', function(button, xy) { //from Gordon Williams
if (button==1) toggleTimeDigits();
if (button==2) toggleDateFormat();
});
function buildSequence(targ){
for(let i=0;i<targ.length;++i){
sequence.push(new Date(accum.getTime()));
accum.setDate(accum.getDate()+targ[i]);
}
}
buildSequence(B2);
buildSequence(B2);
buildSequence(A1);
buildSequence(B1);
buildSequence(B2);
buildSequence(B2);
buildSequence(A1);
buildSequence(B1);
buildSequence(B2);
buildSequence(B2);
buildSequence(A1);
buildSequence(B1);
buildSequence(B2);
function getDate(dt){
let index = sequence.findIndex(n => n > dt)-1;
let year = baseYear+parseInt(index/12);
let month = index % 12;
let day = parseInt((dt-sequence[index])/86400000);
let colour = dateColours[day % 6];
if(day==30){ colour=dateColours[6]; }
return({"year":year,"month":month,"day":day,"colour":colour});
}
function toggleTimeDigits(){
addTimeDigit = !addTimeDigit;
modeTime();
}
function toggleDateFormat(){
dateFormat = !dateFormat;
modeTime();
}
function formatDate(res,dateFormat){
let yyyy = res.year.toString(12);
calenDef = calen10;
if(!dateFormat){ //ordinal format
let mm = ("0"+(res.month+1).toString(12)).substr(-2);
let dd = ("0"+(res.day+1).toString(12)).substr(-2);
if(res.day==30){
calenDef = calen7;
let m = ((res.month+1).toString(12)).substr(-2);
return(yyyy+"-"+"S"+m); // ordinal format
}
return(yyyy+"-"+mm+"-"+dd);
}
let m = res.month.toString(12); // cardinal format
let w = parseInt(res.day/6);
let d = res.day%6;
//return(yyyy+"-"+res.month+"-"+w+"-"+d);
return(yyyy+"-"+m+"-"+w+"-"+d);
}
function writeDozTime(text,def){
let pts = def.pts;
let x=def.pt0[0];
let y=def.pt0[1];
g_t.clear();
g_t.setFont("Vector",def.size);
for(let i in text){
if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new
else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new
else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
x = x+def.step[0];
y = y+def.step[1];
}
}
function writeDozDate(text,def,colour){
dateColour = colour;
let pts = def.pts;
let x=def.pt0[0];
let y=def.pt0[1];
g_d.clear();
g_d.setFont("Vector",def.size);
for(let i in text){
if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new
else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new
else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
x = x+def.step[0];
y = y+def.step[1];
}
}
// Functions for time mode
function drawTime()
{
let dt = new Date();
let date = "";
let timeDef;
let x = 0;
dt.setDate(dt.getDate());
if(addTimeDigit){
x =
10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds();
let msg = "00000"+Math.floor(x).toString(12);
let time = msg.substr(-5,3)+"."+msg.substr(-2);
let wait = 347*(1-(x%1));
timeDef = time6;
} else {
x =
864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds();
let msg = "0000"+Math.floor(x).toString(12);
let time = msg.substr(-4,3)+"."+msg.substr(-1);
let wait = 4167*(1-(x%1));
timeDef = time5;
}
if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day
date = formatDate(res,dateFormat);
if(dt<timeActiveUntil)
{
// Write to background buffers, then display on screen
writeDozDate(date,calenDef,res.colour);
writeDozTime(time,timeDef);
g.flip();
// Ready next interval
drawtime_timeout = setTimeout(drawTime,wait);
}
else
{
// Clear screen
g_d.clear();
g_t.clear();
g.flip();
}
lastX = x;
}
function modeTime()
{
timeActiveUntil = new Date();
timeActiveUntil.setDate(timeActiveUntil.getDate());
timeActiveUntil.setSeconds(timeActiveUntil.getSeconds()+86400);
if (typeof drawtime_timeout !== 'undefined')
{
clearTimeout(drawtime_timeout);
}
drawTime();
}
Bangle.loadWidgets();
Bangle.drawWidgets();
// Functions for weather mode - TODO
// function drawWeather() {}
// function modeWeather() {}
// Start time on twist
Bangle.on('twist',function() {
modeTime();
});
// Time fix with GPS
function fixTime() {
Bangle.on("GPS",function cb(g) {
Bangle.setGPSPower(0,"time");
Bangle.removeListener("GPS",cb);
if (!g.time || (g.time.getFullYear()<2000) ||
(g.time.getFullYear()>2200)) {
} else {
// We have a GPS time. Set time
setTime(g.time.getTime()/1000);
}
});
Bangle.setGPSPower(1,"time");
setTimeout(fixTime, 10*60*1000); // every 10 minutes
}
// Start time fixing with GPS on next 10 minute interval
setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000);

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