Merge branch 'espruino:master' into master
616
apps.json
|
@ -3,7 +3,7 @@
|
||||||
"id": "fwupdate",
|
"id": "fwupdate",
|
||||||
"name": "Firmware Update",
|
"name": "Firmware Update",
|
||||||
"version": "0.02",
|
"version": "0.02",
|
||||||
"description": "Uploads new Espruino firmwares to Bangle.js 2",
|
"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",
|
"icon": "app.png",
|
||||||
"type": "RAM",
|
"type": "RAM",
|
||||||
"tags": "tools,system",
|
"tags": "tools,system",
|
||||||
|
@ -11,12 +11,12 @@
|
||||||
"custom": "custom.html",
|
"custom": "custom.html",
|
||||||
"customConnect": true,
|
"customConnect": true,
|
||||||
"storage": [],
|
"storage": [],
|
||||||
"sortorder": -20
|
"sortorder": 20
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "boot",
|
"id": "boot",
|
||||||
"name": "Bootloader",
|
"name": "Bootloader",
|
||||||
"version": "0.38",
|
"version": "0.39",
|
||||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||||
"icon": "bootloader.png",
|
"icon": "bootloader.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
|
@ -33,10 +33,11 @@
|
||||||
"id": "hebrew_calendar",
|
"id": "hebrew_calendar",
|
||||||
"name": "Hebrew Calendar",
|
"name": "Hebrew Calendar",
|
||||||
"shortName": "HebCal",
|
"shortName": "HebCal",
|
||||||
"version": "0.03",
|
"version": "0.04",
|
||||||
"description": "lists the date according to the hebrew calendar",
|
"description": "lists the date according to the hebrew calendar",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "",
|
"allow_emulator": false,
|
||||||
|
"tags": "tool,locale",
|
||||||
"supports": [
|
"supports": [
|
||||||
"BANGLEJS",
|
"BANGLEJS",
|
||||||
"BANGLEJS2"
|
"BANGLEJS2"
|
||||||
|
@ -47,6 +48,10 @@
|
||||||
"name": "hebrew_calendar.app.js",
|
"name": "hebrew_calendar.app.js",
|
||||||
"url": "app.js"
|
"url": "app.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "hebrewDate",
|
||||||
|
"url": "hebrewDate.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "hebrew_calendar.img",
|
"name": "hebrew_calendar.img",
|
||||||
"url": "app-icon.js",
|
"url": "app-icon.js",
|
||||||
|
@ -54,10 +59,25 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{ "id": "golfscore",
|
||||||
|
"name": "Golf Score",
|
||||||
|
"shortName":"golfscore",
|
||||||
|
"version":"0.02",
|
||||||
|
"description": "keeps track of strokes during a golf game",
|
||||||
|
"icon": "app.png",
|
||||||
|
"tags": "outdoors",
|
||||||
|
"allow_emulator": true,
|
||||||
|
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"golfscore.app.js","url":"app.js"},
|
||||||
|
{"name":"golfscore.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "messages",
|
"id": "messages",
|
||||||
"name": "Messages",
|
"name": "Messages",
|
||||||
"version": "0.11",
|
"version": "0.16",
|
||||||
"description": "App to display notifications from iOS and Gadgetbridge",
|
"description": "App to display notifications from iOS and Gadgetbridge",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
@ -72,14 +92,15 @@
|
||||||
{"name":"messages","url":"lib.js"}
|
{"name":"messages","url":"lib.js"}
|
||||||
],
|
],
|
||||||
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
|
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
|
||||||
|
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}],
|
||||||
"sortorder": -9
|
"sortorder": -9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "android",
|
"id": "android",
|
||||||
"name": "Android Integration",
|
"name": "Android Integration",
|
||||||
"shortName": "Android",
|
"shortName": "Android",
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "Display notifications/music/etc from Gadgetbridge on Android. This replaces the old Gadgetbridge widget.",
|
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,messages,notifications",
|
"tags": "tool,system,messages,notifications",
|
||||||
"dependencies": {"messages":"app"},
|
"dependencies": {"messages":"app"},
|
||||||
|
@ -95,7 +116,7 @@
|
||||||
{
|
{
|
||||||
"id": "ios",
|
"id": "ios",
|
||||||
"name": "iOS Integration",
|
"name": "iOS Integration",
|
||||||
"version": "0.06",
|
"version": "0.07",
|
||||||
"description": "Display notifications/music/etc from iOS devices",
|
"description": "Display notifications/music/etc from iOS devices",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,ios,apple,messages,notifications",
|
"tags": "tool,system,ios,apple,messages,notifications",
|
||||||
|
@ -111,7 +132,7 @@
|
||||||
{
|
{
|
||||||
"id": "health",
|
"id": "health",
|
||||||
"name": "Health Tracking",
|
"name": "Health Tracking",
|
||||||
"version": "0.08",
|
"version": "0.09",
|
||||||
"description": "Logs health data and provides an app to view it (requires firmware 2v10.100 or later)",
|
"description": "Logs health data and provides an app to view it (requires firmware 2v10.100 or later)",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,health",
|
"tags": "tool,system,health",
|
||||||
|
@ -146,7 +167,7 @@
|
||||||
{
|
{
|
||||||
"id": "setting",
|
"id": "setting",
|
||||||
"name": "Settings",
|
"name": "Settings",
|
||||||
"version": "0.36",
|
"version": "0.39",
|
||||||
"description": "A menu for setting up Bangle.js",
|
"description": "A menu for setting up Bangle.js",
|
||||||
"icon": "settings.png",
|
"icon": "settings.png",
|
||||||
"tags": "tool,system",
|
"tags": "tool,system",
|
||||||
|
@ -197,7 +218,7 @@
|
||||||
{
|
{
|
||||||
"id": "locale",
|
"id": "locale",
|
||||||
"name": "Languages",
|
"name": "Languages",
|
||||||
"version": "0.13",
|
"version": "0.14",
|
||||||
"description": "Translations for different countries",
|
"description": "Translations for different countries",
|
||||||
"icon": "locale.png",
|
"icon": "locale.png",
|
||||||
"type": "locale",
|
"type": "locale",
|
||||||
|
@ -282,8 +303,8 @@
|
||||||
{
|
{
|
||||||
"id": "gbridge",
|
"id": "gbridge",
|
||||||
"name": "Gadgetbridge",
|
"name": "Gadgetbridge",
|
||||||
"version": "0.24",
|
"version": "0.25",
|
||||||
"description": "(NOT RECOMMENDED) Handles Gadgetbridge notifications from Android. This is now replaced by the 'Android' app.",
|
"description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android' Bangle.js app instead.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
"tags": "tool,system,android,widget",
|
"tags": "tool,system,android,widget",
|
||||||
|
@ -533,12 +554,12 @@
|
||||||
{
|
{
|
||||||
"id": "impwclock",
|
"id": "impwclock",
|
||||||
"name": "Imprecise Word Clock",
|
"name": "Imprecise Word Clock",
|
||||||
"version": "0.03",
|
"version": "0.04",
|
||||||
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
|
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
|
||||||
"icon": "clock-impword.png",
|
"icon": "clock-impword.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"screenshots": [{"url":"bangle1-impercise-word-clock-screenshot.png"}],
|
"screenshots": [{"url":"bangle1-impercise-word-clock-screenshot.png"}],
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
"storage": [
|
"storage": [
|
||||||
|
@ -747,7 +768,7 @@
|
||||||
"id": "recorder",
|
"id": "recorder",
|
||||||
"name": "Recorder (BETA)",
|
"name": "Recorder (BETA)",
|
||||||
"shortName": "Recorder",
|
"shortName": "Recorder",
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,outdoors,gps,widget",
|
"tags": "tool,outdoors,gps,widget",
|
||||||
|
@ -824,7 +845,7 @@
|
||||||
{
|
{
|
||||||
"id": "weather",
|
"id": "weather",
|
||||||
"name": "Weather",
|
"name": "Weather",
|
||||||
"version": "0.12",
|
"version": "0.14",
|
||||||
"description": "Show Gadgetbridge weather report",
|
"description": "Show Gadgetbridge weather report",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
@ -950,7 +971,7 @@
|
||||||
{
|
{
|
||||||
"id": "widbt",
|
"id": "widbt",
|
||||||
"name": "Bluetooth Widget",
|
"name": "Bluetooth Widget",
|
||||||
"version": "0.07",
|
"version": "0.08",
|
||||||
"description": "Show the current Bluetooth connection status in the top right of the clock",
|
"description": "Show the current Bluetooth connection status in the top right of the clock",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
|
@ -1127,7 +1148,7 @@
|
||||||
{
|
{
|
||||||
"id": "qrcode",
|
"id": "qrcode",
|
||||||
"name": "Custom QR Code",
|
"name": "Custom QR Code",
|
||||||
"version": "0.03",
|
"version": "0.05",
|
||||||
"description": "Use this to upload a customised QR code to Bangle.js",
|
"description": "Use this to upload a customised QR code to Bangle.js",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "qrcode",
|
"tags": "qrcode",
|
||||||
|
@ -1480,7 +1501,7 @@
|
||||||
{
|
{
|
||||||
"id": "gpsinfo",
|
"id": "gpsinfo",
|
||||||
"name": "GPS Info",
|
"name": "GPS Info",
|
||||||
"version": "0.05",
|
"version": "0.06",
|
||||||
"description": "An application that displays information about altitude, lat/lon, satellites and time",
|
"description": "An application that displays information about altitude, lat/lon, satellites and time",
|
||||||
"icon": "gps-info.png",
|
"icon": "gps-info.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
@ -1569,7 +1590,7 @@
|
||||||
{
|
{
|
||||||
"id": "widpedom",
|
"id": "widpedom",
|
||||||
"name": "Pedometer widget",
|
"name": "Pedometer widget",
|
||||||
"version": "0.19",
|
"version": "0.20",
|
||||||
"description": "Daily pedometer widget",
|
"description": "Daily pedometer widget",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
|
@ -1678,14 +1699,15 @@
|
||||||
"id": "rtorch",
|
"id": "rtorch",
|
||||||
"name": "Red Torch",
|
"name": "Red Torch",
|
||||||
"shortName": "RedTorch",
|
"shortName": "RedTorch",
|
||||||
"version": "0.01",
|
"version": "0.02",
|
||||||
"description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets",
|
"description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or on Bangle 1 press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,torch",
|
"tags": "tool,torch",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"rtorch.app.js","url":"app.js"},
|
{"name":"rtorch.app.js","url":"app.js"},
|
||||||
{"name":"rtorch.wid.js","url":"widget.js"},
|
{"name":"rtorch.wid.js","url":"widget.js", "supports": ["BANGLEJS"]},
|
||||||
{"name":"rtorch.img","url":"app-icon.js","evaluate":true}
|
{"name":"rtorch.img","url":"app-icon.js","evaluate":true}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2005,7 +2027,7 @@
|
||||||
"id": "chronowid",
|
"id": "chronowid",
|
||||||
"name": "Chrono Widget",
|
"name": "Chrono Widget",
|
||||||
"shortName": "Chrono Widget",
|
"shortName": "Chrono Widget",
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "Chronometer (timer) which runs as widget.",
|
"description": "Chronometer (timer) which runs as widget.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,widget",
|
"tags": "tool,widget",
|
||||||
|
@ -2147,21 +2169,22 @@
|
||||||
{ "id": "snek",
|
{ "id": "snek",
|
||||||
"name": "The snek game",
|
"name": "The snek game",
|
||||||
"shortName":"Snek",
|
"shortName":"Snek",
|
||||||
"version": "0.01",
|
"version": "0.02",
|
||||||
"description": "A snek game where you control a snek to eat all the apples!",
|
"description": "A snek game where you control a snek to eat all the apples!",
|
||||||
"icon": "snek-icon.js",
|
"screenshots": [{"url":"screenshot_snek.png"}],
|
||||||
|
"icon": "snek.png",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"tags": "game,fun",
|
"tags": "game,fun",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"snek.app.js","url":"snek.js"},
|
{"name":"snek.app.js","url":"snek.js"},
|
||||||
{"name":"snek.img","url":"snek-icon.js","evaluate":true}
|
{"name":"snek.img","url":"snek.icon.js","evaluate":true}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "calculator",
|
"id": "calculator",
|
||||||
"name": "Calculator",
|
"name": "Calculator",
|
||||||
"shortName": "Calculator",
|
"shortName": "Calculator",
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
|
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
|
||||||
"icon": "calculator.png",
|
"icon": "calculator.png",
|
||||||
"screenshots": [{"url":"screenshot_calculator.png"}],
|
"screenshots": [{"url":"screenshot_calculator.png"}],
|
||||||
|
@ -2406,7 +2429,7 @@
|
||||||
{
|
{
|
||||||
"id": "calendar",
|
"id": "calendar",
|
||||||
"name": "Calendar",
|
"name": "Calendar",
|
||||||
"version": "0.03",
|
"version": "0.05",
|
||||||
"description": "Simple calendar",
|
"description": "Simple calendar",
|
||||||
"icon": "calendar.png",
|
"icon": "calendar.png",
|
||||||
"screenshots": [{"url":"screenshot_calendar.png"}],
|
"screenshots": [{"url":"screenshot_calendar.png"}],
|
||||||
|
@ -3274,7 +3297,7 @@
|
||||||
{
|
{
|
||||||
"id": "dtlaunch",
|
"id": "dtlaunch",
|
||||||
"name": "Desktop Launcher",
|
"name": "Desktop Launcher",
|
||||||
"version": "0.05",
|
"version": "0.07",
|
||||||
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
||||||
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
|
@ -3285,8 +3308,11 @@
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]},
|
{"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]},
|
||||||
{"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]},
|
{"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]},
|
||||||
|
{"name":"dtlaunch.settings.js","url":"settings-b1.js", "supports": ["BANGLEJS"]},
|
||||||
|
{"name":"dtlaunch.settings.js","url":"settings-b2.js", "supports": ["BANGLEJS2"]},
|
||||||
{"name":"dtlaunch.img","url":"app-icon.js","evaluate":true}
|
{"name":"dtlaunch.img","url":"app-icon.js","evaluate":true}
|
||||||
]
|
],
|
||||||
|
"data": [{"name":"dtlaunch.json"}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "HRV",
|
"id": "HRV",
|
||||||
|
@ -3453,7 +3479,7 @@
|
||||||
"id": "speedalt2",
|
"id": "speedalt2",
|
||||||
"name": "GPS Adventure Sports II",
|
"name": "GPS Adventure Sports II",
|
||||||
"shortName":"GPS Adv Sport II",
|
"shortName":"GPS Adv Sport II",
|
||||||
"version": "0.07",
|
"version":"1.10",
|
||||||
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
@ -3870,7 +3896,7 @@
|
||||||
"id": "qmsched",
|
"id": "qmsched",
|
||||||
"name": "Quiet Mode Schedule and Widget",
|
"name": "Quiet Mode Schedule and Widget",
|
||||||
"shortName": "Quiet Mode",
|
"shortName": "Quiet Mode",
|
||||||
"version": "0.04",
|
"version": "0.06",
|
||||||
"description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.",
|
"description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
|
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
|
||||||
|
@ -3988,11 +4014,12 @@
|
||||||
{
|
{
|
||||||
"id": "thermom",
|
"id": "thermom",
|
||||||
"name": "Thermometer",
|
"name": "Thermometer",
|
||||||
"version": "0.03",
|
"version": "0.05",
|
||||||
"description": "Displays the current temperature in degree Celsius, updated every 20 seconds",
|
"description": "Displays the current temperature in degree Celsius/Fahrenheit (depending on locale), updates every 10 seconds with average of last 5 readings.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool",
|
"tags": "tool",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"thermom.app.js","url":"app.js"},
|
{"name":"thermom.app.js","url":"app.js"},
|
||||||
|
@ -4037,7 +4064,7 @@
|
||||||
{
|
{
|
||||||
"id": "hcclock",
|
"id": "hcclock",
|
||||||
"name": "Hi-Contrast Clock",
|
"name": "Hi-Contrast Clock",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
|
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
|
||||||
"icon": "hcclock-icon.png",
|
"icon": "hcclock-icon.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
|
@ -4054,7 +4081,7 @@
|
||||||
"id": "thermomF",
|
"id": "thermomF",
|
||||||
"name": "Fahrenheit Temp",
|
"name": "Fahrenheit Temp",
|
||||||
"version": "0.01",
|
"version": "0.01",
|
||||||
"description": "A modification of the Thermometer App to display temprature in Fahrenheit",
|
"description": "[NOT RECOMMENDED] A modification of the Thermometer App to display temprature in Fahrenheit. Please use the 'Thermometer App' and install 'Languages' to get the temperature in the correct format for your locale.",
|
||||||
"icon": "thermf.png",
|
"icon": "thermf.png",
|
||||||
"tags": "tool",
|
"tags": "tool",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS"],
|
||||||
|
@ -4117,14 +4144,17 @@
|
||||||
{
|
{
|
||||||
"id": "vectorclock",
|
"id": "vectorclock",
|
||||||
"name": "Vector Clock",
|
"name": "Vector Clock",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "A digital clock that uses the built-in vector font.",
|
"description": "A digital clock that uses the built-in vector font.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
"screenshots": [{"url":"bangle1-vector-clock-screenshot.png"}],
|
"screenshots": [
|
||||||
|
{"url":"bangle2-vector-clock-screenshot.png"},
|
||||||
|
{"url":"bangle1-vector-clock-screenshot.png"}
|
||||||
|
],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"vectorclock.app.js","url":"app.js"},
|
{"name":"vectorclock.app.js","url":"app.js"},
|
||||||
{"name":"vectorclock.img","url":"app-icon.js","evaluate":true}
|
{"name":"vectorclock.img","url":"app-icon.js","evaluate":true}
|
||||||
|
@ -4180,10 +4210,10 @@
|
||||||
"id": "pastel",
|
"id": "pastel",
|
||||||
"name": "Pastel Clock",
|
"name": "Pastel Clock",
|
||||||
"shortName": "Pastel",
|
"shortName": "Pastel",
|
||||||
"version": "0.08",
|
"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",
|
"description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
|
||||||
"icon": "pastel.png",
|
"icon": "pastel.png",
|
||||||
"dependencies": {"mylocation":"app"},
|
"dependencies": {"mylocation":"app", "widpedom":"app"},
|
||||||
"screenshots": [{"url":"screenshot_pastel.png"}],
|
"screenshots": [{"url":"screenshot_pastel.png"}],
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
|
@ -4442,9 +4472,9 @@
|
||||||
"name": "A Battery Widget (with percentage)",
|
"name": "A Battery Widget (with percentage)",
|
||||||
"shortName":"A Battery Widget",
|
"shortName":"A Battery Widget",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"version":"1.01",
|
"version":"1.02",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"description": "Simple and slim battery widget with charge status and percentage",
|
"description": "Simple and slim battery widget with charge status and percentage",
|
||||||
"tags": "widget,battery",
|
"tags": "widget,battery",
|
||||||
|
@ -4457,7 +4487,7 @@
|
||||||
"name": "LCARS Clock",
|
"name": "LCARS Clock",
|
||||||
"shortName":"LCARS",
|
"shortName":"LCARS",
|
||||||
"icon": "lcars.png",
|
"icon": "lcars.png",
|
||||||
"version":"0.06",
|
"version":"0.08",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
||||||
|
@ -4466,7 +4496,8 @@
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"lcars.app.js","url":"lcars.app.js"},
|
{"name":"lcars.app.js","url":"lcars.app.js"},
|
||||||
{"name":"lcars.img","url":"lcars.icon.js","evaluate":true}
|
{"name":"lcars.img","url":"lcars.icon.js","evaluate":true},
|
||||||
|
{"name":"lcars.settings.js","url":"lcars.settings.js"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ "id": "binwatch",
|
{ "id": "binwatch",
|
||||||
|
@ -4621,7 +4652,7 @@
|
||||||
"id": "sensible",
|
"id": "sensible",
|
||||||
"name": "SensiBLE",
|
"name": "SensiBLE",
|
||||||
"shortName": "SensiBLE",
|
"shortName": "SensiBLE",
|
||||||
"version": "0.03",
|
"version": "0.05",
|
||||||
"description": "Collect, display and advertise real-time sensor data.",
|
"description": "Collect, display and advertise real-time sensor data.",
|
||||||
"icon": "sensible.png",
|
"icon": "sensible.png",
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
|
@ -4666,6 +4697,8 @@
|
||||||
"tags": "tool,timer",
|
"tags": "tool,timer",
|
||||||
"readme":"README.md",
|
"readme":"README.md",
|
||||||
"supports":["BANGLEJS2"],
|
"supports":["BANGLEJS2"],
|
||||||
|
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
|
||||||
|
"allow_emulator": true,
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"a_speech_timer.app.js","url":"app.js"},
|
{"name":"a_speech_timer.app.js","url":"app.js"},
|
||||||
{"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true}
|
{"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true}
|
||||||
|
@ -4694,8 +4727,9 @@
|
||||||
"id": "pebble",
|
"id": "pebble",
|
||||||
"name": "Pebble Clock",
|
"name": "Pebble Clock",
|
||||||
"shortName": "Pebble",
|
"shortName": "Pebble",
|
||||||
"version": "0.04",
|
"version": "0.06",
|
||||||
"description": "A pebble style clock to keep the rebellion going",
|
"description": "A pebble style clock to keep the rebellion going",
|
||||||
|
"dependencies": {"widpedom":"app"},
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"icon": "pebble.png",
|
"icon": "pebble.png",
|
||||||
"screenshots": [{"url":"pebble_screenshot.png"}],
|
"screenshots": [{"url":"pebble_screenshot.png"}],
|
||||||
|
@ -4711,7 +4745,7 @@
|
||||||
{ "id": "pooqroman",
|
{ "id": "pooqroman",
|
||||||
"name": "pooq Roman watch face",
|
"name": "pooq Roman watch face",
|
||||||
"shortName":"pooq Roman",
|
"shortName":"pooq Roman",
|
||||||
"version":"0.0.0",
|
"version":"0.03",
|
||||||
"description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!",
|
"description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
|
@ -4746,7 +4780,7 @@
|
||||||
{
|
{
|
||||||
"id": "weatherClock",
|
"id": "weatherClock",
|
||||||
"name": "Weather Clock",
|
"name": "Weather Clock",
|
||||||
"version": "0.03",
|
"version": "0.05",
|
||||||
"description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).",
|
"description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screens/screen1.png"}],
|
"screenshots": [{"url":"screens/screen1.png"}],
|
||||||
|
@ -4806,6 +4840,25 @@
|
||||||
{"name": "flow.img", "url": "app-icon.js","evaluate": true }
|
{"name": "flow.img", "url": "app-icon.js","evaluate": true }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{ "id": "tinydraw",
|
||||||
|
"name": "TinyDraw",
|
||||||
|
"shortName":"TinyDraw",
|
||||||
|
"version":"0.01",
|
||||||
|
"type": "app",
|
||||||
|
"description": "Draw stuff in your wrist",
|
||||||
|
"icon": "app.png",
|
||||||
|
"allow_emulator": true,
|
||||||
|
"tags": "tools, keyboard, text, scribble",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"tinydraw.app.js","url":"app.js"},
|
||||||
|
{"name":"tinydraw.img","url":"app-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"screenshots":[
|
||||||
|
{ "url":"screenshot.png" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{ "id": "scribble",
|
{ "id": "scribble",
|
||||||
"name": "Scribble",
|
"name": "Scribble",
|
||||||
"shortName":"Scribble",
|
"shortName":"Scribble",
|
||||||
|
@ -4829,9 +4882,10 @@
|
||||||
"id": "ptlaunch",
|
"id": "ptlaunch",
|
||||||
"name": "Pattern Launcher",
|
"name": "Pattern Launcher",
|
||||||
"shortName": "Pattern Launcher",
|
"shortName": "Pattern Launcher",
|
||||||
"version": "0.02",
|
"version": "0.13",
|
||||||
"description": "Directly launch apps from the clock screen with custom patterns.",
|
"description": "Directly launch apps from the clock screen with custom patterns.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
|
"screenshots": [{"url":"manage_patterns_light.png"}],
|
||||||
"tags": "tools",
|
"tags": "tools",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
|
@ -4842,10 +4896,58 @@
|
||||||
],
|
],
|
||||||
"data": [{"name":"ptlaunch.patterns.json"}]
|
"data": [{"name":"ptlaunch.patterns.json"}]
|
||||||
},
|
},
|
||||||
{ "id": "clicompleteclk",
|
{ "id": "slimehunt",
|
||||||
|
"name": "Slime Hunt",
|
||||||
|
"shortName":"SlimeHunt",
|
||||||
|
"icon": "app.png",
|
||||||
|
"version":"0.02",
|
||||||
|
"description": "Fight against slimes in turn based combat, try to get the highscore!",
|
||||||
|
"tags": "rpg,slime",
|
||||||
|
"supports" : ["BANGLEJS"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"slimehunt.app.js","url":"app.js"},
|
||||||
|
{"name":"slimehunt.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rebble",
|
||||||
|
"name": "Rebble Clock",
|
||||||
|
"shortName": "Rebble",
|
||||||
|
"version": "0.03",
|
||||||
|
"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",
|
||||||
|
"dependencies": {"mylocation":"app", "widpedom":"app"},
|
||||||
|
"screenshots": [{"url":"screenshot_rebble.png"}],
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"rebble.app.js","url":"rebble.app.js"},
|
||||||
|
{"name":"rebble.settings.js","url":"rebble.settings.js"},
|
||||||
|
{"name":"rebble.img","url":"rebble.icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "snaky",
|
||||||
|
"name": "Snaky",
|
||||||
|
"shortName":"Snaky",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "The classic snake game. Eat apples and don't bite your tail. Control the snake with the touch screen.",
|
||||||
|
"tags": "game,fun",
|
||||||
|
"icon": "snaky.png",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"snaky.app.js","url":"snaky.js"},
|
||||||
|
{"name":"snaky.img","url":"snaky-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clicompleteclk",
|
||||||
"name": "CLI complete clock",
|
"name": "CLI complete clock",
|
||||||
"shortName":"CLI cmplt clock",
|
"shortName":"CLI cmplt clock",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "Command line styled clock with lots of information",
|
"description": "Command line styled clock with lots of information",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
|
@ -4855,7 +4957,405 @@
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"clicompleteclk.app.js","url":"app.js"},
|
{"name":"clicompleteclk.app.js","url":"app.js"},
|
||||||
{"name":"clicompleteclk.img","url":"app-icon.js","evaluate":true}
|
{"name":"clicompleteclk.img","url":"app-icon.js","evaluate":true},
|
||||||
|
{"name":"clicompleteclk.settings.js","url":"settings.js"}
|
||||||
|
],
|
||||||
|
"data": [{"name":"clicompleteclk.json"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"awairmonitor",
|
||||||
|
"name":"Awair Monitor",
|
||||||
|
"icon": "app.png",
|
||||||
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"version":"0.03",
|
||||||
|
"description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock,tool,health",
|
||||||
|
"readme":"README.md",
|
||||||
|
"supports":["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"awairmonitor.app.js","url":"app.js"},
|
||||||
|
{"name":"awairmonitor.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "pooqround",
|
||||||
|
"name": "pooq Round watch face",
|
||||||
|
"shortName":"pooq Round",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"allow_emulator":true,
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"pooqround.app.js","url":"app.js"},
|
||||||
|
{"name":"pooqround.img","url":"app-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"name":"pooqround.json"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "coretemp",
|
||||||
|
"name": "CoreTemp",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "Display CoreTemp device sensor data",
|
||||||
|
"icon": "coretemp.png",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "health",
|
||||||
|
"readme": "README.md",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"coretemp.wid.js","url":"widget.js"},
|
||||||
|
{"name":"coretemp.app.js","url":"coretemp.js"},
|
||||||
|
{"name":"coretemp.settings.js","url":"settings.js"},
|
||||||
|
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true},
|
||||||
|
{"name":"coretemp.boot.js","url":"boot.js"}
|
||||||
|
],
|
||||||
|
"data": [{"name":"coretemp.json","url":"app-settings.json"}],
|
||||||
|
"screenshots": [{"url":"screenshot.png"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "showimg",
|
||||||
|
"name": "simple image viewer",
|
||||||
|
"shortName":"showImage",
|
||||||
|
"version":"0.2",
|
||||||
|
"description": "Displays the image in \"showimg.user.img\". The file has to be uploaded via the espruino IDE. Returns to watch face after 60s or button push. I use it to display my vaccination certificate.",
|
||||||
|
"icon": "app.png",
|
||||||
|
"tags": "tool",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"showimg.app.js","url":"app.js"},
|
||||||
|
{"name":"showimg.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lapcounter",
|
||||||
|
"name": "Lap Counter",
|
||||||
|
"version": "0.01",
|
||||||
|
"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"}],
|
||||||
|
"type": "app",
|
||||||
|
"tags": "tool,outdoors",
|
||||||
|
"readme":"README.md",
|
||||||
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"storage": [
|
||||||
|
{"name":"lapcounter.app.js","url":"app.js"},
|
||||||
|
{"name":"lapcounter.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pebbled",
|
||||||
|
"name": "Pebble Clock with distance",
|
||||||
|
"shortName": "Pebble + distance",
|
||||||
|
"version": "0.1",
|
||||||
|
"description": "Fork of Pebble Clock with distance in KM. Both step count and the distance are on the main screen. Default step length = 0.75m (can be changed in settings).",
|
||||||
|
"readme": "README.md",
|
||||||
|
"icon": "pebbled.png",
|
||||||
|
"screenshots": [{"url":"pebble_screenshot.png"}],
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock,distance",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"pebbled.app.js","url":"pebbled.app.js"},
|
||||||
|
{"name":"pebbled.settings.js","url":"pebbled.settings.js"},
|
||||||
|
{"name":"pebbled.img","url":"pebbled.icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "circlesclock",
|
||||||
|
"name": "Circles clock",
|
||||||
|
"shortName":"Circles clock",
|
||||||
|
"version":"0.03",
|
||||||
|
"description": "A clock with circles for different data at the bottom in a probably familiar style",
|
||||||
|
"icon": "app.png",
|
||||||
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
"dependencies": {"widpedom":"app"},
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"allow_emulator":true,
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"circlesclock.app.js","url":"app.js"},
|
||||||
|
{"name":"circlesclock.img","url":"app-icon.js","evaluate":true},
|
||||||
|
{"name":"circlesclock.settings.js","url":"settings.js"}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"name":"circlesclock.json"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "contourclock",
|
||||||
|
"name": "Contour Clock",
|
||||||
|
"shortName" : "Contour Clock",
|
||||||
|
"version":"0.01",
|
||||||
|
"icon": "app.png",
|
||||||
|
"description": "A Minimalist clockface with large Digits. Looks best with the dark theme",
|
||||||
|
"screenshots" : [{"url":"screenshot.png"}],
|
||||||
|
"tags": "clock",
|
||||||
|
"allow_emulator":true,
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"type": "clock",
|
||||||
|
"storage": [
|
||||||
|
{"name":"contourclock.app.js","url":"app.js"},
|
||||||
|
{"name":"contourclock.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ltherm",
|
||||||
|
"name": "Localized Thermometer",
|
||||||
|
"shortName": "Thermometer",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Displays the current temperature in localized units.",
|
||||||
|
"icon": "thermf.png",
|
||||||
|
"tags": "tool",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"ltherm.app.js","url":"app.js"},
|
||||||
|
{"name":"ltherm.img","url":"icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "slash",
|
||||||
|
"name": "Slash Watch",
|
||||||
|
"shortName":"Slash",
|
||||||
|
"icon": "slash.png",
|
||||||
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Slash Watch based on Pebble watch face by Nikki.",
|
||||||
|
"tags": "clock",
|
||||||
|
"type": "clock",
|
||||||
|
"supports":["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"allow_emulator": true,
|
||||||
|
"storage": [
|
||||||
|
{"name":"slash.app.js","url":"app.js"},
|
||||||
|
{"name":"slash.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "promenu",
|
||||||
|
"name": "Pro Menu",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Replace Bangle.js 1's built in menu function.",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"type": "boot",
|
||||||
|
"tags": "system",
|
||||||
|
"supports": ["BANGLEJS"],
|
||||||
|
"screenshots": [{"url":"pro-menu-screenshot.png"}],
|
||||||
|
"storage": [
|
||||||
|
{"name":"promenu.boot.js","url":"boot.js"},
|
||||||
|
{"name":"promenu.img","url":"promenuIcon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "touchtimer",
|
||||||
|
"name": "Touch Timer",
|
||||||
|
"shortName": "Touch Timer",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.",
|
||||||
|
"icon": "app.png",
|
||||||
|
"tags": "tools",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"screenshots": [{"url":"0_light_timer_edit.png"},{"url":"1_light_timer_ready.png"},{"url":"2_light_timer_running.png"},{"url":"3_light_timer_finished.png"}],
|
||||||
|
"storage": [
|
||||||
|
{ "name": "touchtimer.app.js", "url": "app.js" },
|
||||||
|
{ "name":"touchtimer.settings.js", "url":"settings.js"},
|
||||||
|
{ "name": "touchtimer.img", "url": "app-icon.js", "evaluate": true }
|
||||||
|
],
|
||||||
|
"data": [{"name":"touchtimer.data.json"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "teatimer",
|
||||||
|
"name": "Tea Timer",
|
||||||
|
"version": "1.00",
|
||||||
|
"description": "A simple timer. You can easyly set up the time.",
|
||||||
|
"icon": "teatimer.png",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "tool",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"teatimer.app.js","url":"app.js"},
|
||||||
|
{"name":"teatimer.img","url":"app-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"screenshots": [
|
||||||
|
{"url":"TeatimerStart.jpg"},
|
||||||
|
{"url":"TeatimerHelp.jpg"},
|
||||||
|
{"url":"TeatimerRun.jpg"},
|
||||||
|
{"url":"TeatimerUp.jpg"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "swp2clk",
|
||||||
|
"name": "Swipe back to the Clock",
|
||||||
|
"shortName": "Swipe to Clock",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "boot",
|
||||||
|
"tags": "tools",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{ "name": "swp2clk.boot.js", "url": "boot.js" },
|
||||||
|
{"name":"swp2clk.settings.js","url":"settings.js"}
|
||||||
|
],
|
||||||
|
"data": [{"name":"swp2clk.data.json"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"colorwheel",
|
||||||
|
"name":"Color Wheel",
|
||||||
|
"tags":"app,tool",
|
||||||
|
"version":"0.01",
|
||||||
|
"description":"a tappable wheel of good-looking colors",
|
||||||
|
"readme":"README.md",
|
||||||
|
"supports":["BANGLEJS2"],
|
||||||
|
"allow_emulator":true,
|
||||||
|
"icon":"colorwheel.png",
|
||||||
|
"storage": [
|
||||||
|
{"name":"colorwheel.app.js","url":"app.js"},
|
||||||
|
{"name":"colorwheel.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "minimal_clock",
|
||||||
|
"name": "Minimal Analog Clock",
|
||||||
|
"shortName":"Minimal Clock",
|
||||||
|
"version":"0.03",
|
||||||
|
"description": "a minimal analog clock - just with some hands and no clock face",
|
||||||
|
"icon": "app-icon.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"screenshots": [{"url":"app-screenshot.png"}],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"minimal_clock.app.js","url":"app.js"},
|
||||||
|
{"name":"minimal_clock.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "simple_clock",
|
||||||
|
"name": "Simple Analog Clock",
|
||||||
|
"shortName":"Simple Clock",
|
||||||
|
"version":"0.02",
|
||||||
|
"description": "a simple, yet stylish, analog clock",
|
||||||
|
"icon": "app-icon.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"screenshots": [{"url":"app-screenshot.png"}],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"simple_clock.app.js","url":"app.js"},
|
||||||
|
{"name":"simple_clock.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "colorful_clock",
|
||||||
|
"name": "Colorful Analog Clock",
|
||||||
|
"shortName":"Colorful Clock",
|
||||||
|
"version":"0.02",
|
||||||
|
"description": "a colorful analog clock",
|
||||||
|
"icon": "app-icon.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"screenshots": [{"url":"app-screenshot.png"}],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"colorful_clock.app.js","url":"app.js"},
|
||||||
|
{"name":"colorful_clock.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "themesetter",
|
||||||
|
"name": "Theme Setter",
|
||||||
|
"shortName":"Theme Setter",
|
||||||
|
"version":"0.04",
|
||||||
|
"description": "a comfortable way to configure theme colors",
|
||||||
|
"icon": "app-icon.png",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "tool",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"screenshots": [{"url":"app-screenshot.png"}],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"themesetter.app.js","url":"app.js"},
|
||||||
|
{"name":"themesetter.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "widviztime",
|
||||||
|
"name": "Widget Autohide Widget",
|
||||||
|
"shortName": "Viz Time Widget",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "The widgets will be shown for four seconds after the device is unlocked.",
|
||||||
|
"icon": "eye.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "widget",
|
||||||
|
"readme":"README.md",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"widviztime.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "supf",
|
||||||
|
"name": "Simple Clock with Date",
|
||||||
|
"shortName": "supf Clock",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Simple Clock with seconds and date in custom language. Install 'Languages' to get localized names.",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"screenshots": [{"url":"screenshot_supf.png"}],
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"supf.app.js","url":"app.js"},
|
||||||
|
{"name":"supf.img","url":"icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "andark",
|
||||||
|
"name": "Analog Dark",
|
||||||
|
"shortName":"AnDark",
|
||||||
|
"version":"0.04",
|
||||||
|
"description": "analog clock face without disturbing widgets",
|
||||||
|
"icon": "andark_icon.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"andark.app.js","url":"app.js"},
|
||||||
|
{"name":"andark.img","url":"app_icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diract",
|
||||||
|
"name": "DirAct",
|
||||||
|
"shortName": "DirAct",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Proximity interaction detection.",
|
||||||
|
"icon": "diract.png",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "tool,sensors",
|
||||||
|
"supports" : [ "BANGLEJS2" ],
|
||||||
|
"allow_emulator": false,
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{ "name": "diract.app.js", "url": "diract.js" },
|
||||||
|
{ "name": "diract.img", "url": "diract-icon.js", "evaluate": true }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,8 +21,8 @@ function showAlarm(alarm) {
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
E.showPrompt(msg,{
|
E.showPrompt(msg,{
|
||||||
title:alarm.timer ? "TIMER!" : "ALARM!",
|
title:alarm.timer ? /*LANG*/"TIMER!" : /*LANG*/"ALARM!",
|
||||||
buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins
|
buttons : {/*LANG*/"Sleep":true,/*LANG*/"Ok":false} // default is sleep so it'll come back in 10 mins
|
||||||
}).then(function(sleep) {
|
}).then(function(sleep) {
|
||||||
buzzCount = 0;
|
buzzCount = 0;
|
||||||
if (sleep) {
|
if (sleep) {
|
||||||
|
|
|
@ -33,16 +33,16 @@ function getCurrentHr() {
|
||||||
function showMainMenu() {
|
function showMainMenu() {
|
||||||
const menu = {
|
const menu = {
|
||||||
'': { 'title': 'Alarm/Timer' },
|
'': { 'title': 'Alarm/Timer' },
|
||||||
'< Back' : ()=>{load();},
|
/*LANG*/'< Back' : ()=>{load();},
|
||||||
'New Alarm': ()=>editAlarm(-1),
|
/*LANG*/'New Alarm': ()=>editAlarm(-1),
|
||||||
'New Timer': ()=>editTimer(-1)
|
/*LANG*/'New Timer': ()=>editTimer(-1)
|
||||||
};
|
};
|
||||||
alarms.forEach((alarm,idx)=>{
|
alarms.forEach((alarm,idx)=>{
|
||||||
if (alarm.timer) {
|
if (alarm.timer) {
|
||||||
txt = "TIMER "+(alarm.on?"on ":"off ")+formatMins(alarm.timer);
|
txt = /*LANG*/"TIMER "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatMins(alarm.timer);
|
||||||
} else {
|
} else {
|
||||||
txt = "ALARM "+(alarm.on?"on ":"off ")+formatTime(alarm.hr);
|
txt = /*LANG*/"ALARM "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatTime(alarm.hr);
|
||||||
if (alarm.rp) txt += " (repeat)";
|
if (alarm.rp) txt += /*LANG*/" (repeat)";
|
||||||
}
|
}
|
||||||
menu[txt] = function() {
|
menu[txt] = function() {
|
||||||
if (alarm.timer) editTimer(idx);
|
if (alarm.timer) editTimer(idx);
|
||||||
|
@ -70,27 +70,27 @@ function editAlarm(alarmIndex) {
|
||||||
as = a.as;
|
as = a.as;
|
||||||
}
|
}
|
||||||
const menu = {
|
const menu = {
|
||||||
'': { 'title': 'Alarm' },
|
'': { 'title': /*LANG*/'Alarm' },
|
||||||
'< Back' : showMainMenu,
|
/*LANG*/'< Back' : showMainMenu,
|
||||||
'Hours': {
|
/*LANG*/'Hours': {
|
||||||
value: hrs,
|
value: hrs,
|
||||||
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||||
},
|
},
|
||||||
'Minutes': {
|
/*LANG*/'Minutes': {
|
||||||
value: mins,
|
value: mins,
|
||||||
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||||
},
|
},
|
||||||
'Enabled': {
|
/*LANG*/'Enabled': {
|
||||||
value: en,
|
value: en,
|
||||||
format: v=>v?"On":"Off",
|
format: v=>v?"On":"Off",
|
||||||
onchange: v=>en=v
|
onchange: v=>en=v
|
||||||
},
|
},
|
||||||
'Repeat': {
|
/*LANG*/'Repeat': {
|
||||||
value: en,
|
value: en,
|
||||||
format: v=>v?"Yes":"No",
|
format: v=>v?"Yes":"No",
|
||||||
onchange: v=>repeat=v
|
onchange: v=>repeat=v
|
||||||
},
|
},
|
||||||
'Auto snooze': {
|
/*LANG*/'Auto snooze': {
|
||||||
value: as,
|
value: as,
|
||||||
format: v=>v?"Yes":"No",
|
format: v=>v?"Yes":"No",
|
||||||
onchange: v=>as=v
|
onchange: v=>as=v
|
||||||
|
@ -108,14 +108,14 @@ function editAlarm(alarmIndex) {
|
||||||
last : day, rp : repeat, as: as
|
last : day, rp : repeat, as: as
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
menu["> Save"] = function() {
|
menu[/*LANG*/"> Save"] = function() {
|
||||||
if (newAlarm) alarms.push(getAlarm());
|
if (newAlarm) alarms.push(getAlarm());
|
||||||
else alarms[alarmIndex] = getAlarm();
|
else alarms[alarmIndex] = getAlarm();
|
||||||
require("Storage").write("alarm.json",JSON.stringify(alarms));
|
require("Storage").write("alarm.json",JSON.stringify(alarms));
|
||||||
showMainMenu();
|
showMainMenu();
|
||||||
};
|
};
|
||||||
if (!newAlarm) {
|
if (!newAlarm) {
|
||||||
menu["> Delete"] = function() {
|
menu[/*LANG*/"> Delete"] = function() {
|
||||||
alarms.splice(alarmIndex,1);
|
alarms.splice(alarmIndex,1);
|
||||||
require("Storage").write("alarm.json",JSON.stringify(alarms));
|
require("Storage").write("alarm.json",JSON.stringify(alarms));
|
||||||
showMainMenu();
|
showMainMenu();
|
||||||
|
@ -136,18 +136,18 @@ function editTimer(alarmIndex) {
|
||||||
en = a.on;
|
en = a.on;
|
||||||
}
|
}
|
||||||
const menu = {
|
const menu = {
|
||||||
'': { 'title': 'Timer' },
|
'': { 'title': /*LANG*/'Timer' },
|
||||||
'Hours': {
|
/*LANG*/'Hours': {
|
||||||
value: hrs,
|
value: hrs,
|
||||||
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||||
},
|
},
|
||||||
'Minutes': {
|
/*LANG*/'Minutes': {
|
||||||
value: mins,
|
value: mins,
|
||||||
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||||
},
|
},
|
||||||
'Enabled': {
|
/*LANG*/'Enabled': {
|
||||||
value: en,
|
value: en,
|
||||||
format: v=>v?"On":"Off",
|
format: v=>v?/*LANG*/"On":/*LANG*/"Off",
|
||||||
onchange: v=>en=v
|
onchange: v=>en=v
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
||||||
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||||
if (!require('Storage').read("alarm.js")) {
|
if (!require('Storage').read("alarm.js")) {
|
||||||
console.log("No alarm app!");
|
console.log(/*LANG*/"No alarm app!");
|
||||||
require('Storage').write('alarm.json',"[]");
|
require('Storage').write('alarm.json',"[]");
|
||||||
} else {
|
} else {
|
||||||
var t = 3600000*(active[0].hr-hr);
|
var t = 3600000*(active[0].hr-hr);
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
0.01: Release
|
||||||
|
0.02: Rename app
|
||||||
|
0.03: Add type "clock"
|
||||||
|
0.04: changed update cylce, when locked
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Analog Clock
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* second hand
|
||||||
|
* date
|
||||||
|
* battery percantage
|
||||||
|
* no widgets
|
||||||
|
|
||||||
|

|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 3.6 KiB |
|
@ -0,0 +1,125 @@
|
||||||
|
const c={"x":g.getWidth()/2,"y":g.getHeight()/2};
|
||||||
|
let zahlpos=[];
|
||||||
|
let unlock = false;
|
||||||
|
|
||||||
|
function zeiger(len,dia,tim){
|
||||||
|
const x =c.x+ Math.cos(tim)*len/2,
|
||||||
|
y =c.y + Math.sin(tim)*len/2,
|
||||||
|
d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)},
|
||||||
|
pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y];
|
||||||
|
return pol;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(){
|
||||||
|
const d=new Date();
|
||||||
|
let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds();
|
||||||
|
//draw black rectangle in the middle to clear screen from scale and hands
|
||||||
|
g.setColor(0,0,0);
|
||||||
|
g.fillRect(10,10,2*c.x-10,2*c.x-10);
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
|
||||||
|
if(h>12){
|
||||||
|
h=h-12;
|
||||||
|
}
|
||||||
|
//calculates the position of the minute, second and hour hand
|
||||||
|
h=2*Math.PI/12*(h+m/60)-Math.PI/2;
|
||||||
|
//more accurate
|
||||||
|
//m=2*Math.PI/60*(m+s/60)-Math.PI/2;
|
||||||
|
m=2*Math.PI/60*(m)-Math.PI/2;
|
||||||
|
|
||||||
|
s=2*Math.PI/60*s-Math.PI/2;
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setFont("Vector",10);
|
||||||
|
let dateStr = " "+require("locale").date(d)+" ";
|
||||||
|
g.drawString(dateStr, c.x, c.y+20, true);
|
||||||
|
// g.drawString(d.getDate(),1.4*c.x,c.y,true);
|
||||||
|
g.drawString(Math.round(E.getBattery()/5)*5+"%",c.x,c.y+40,true);
|
||||||
|
drawlet();
|
||||||
|
//g.setColor(1,0,0);
|
||||||
|
const hz = zeiger(100,5,h);
|
||||||
|
g.fillPoly(hz,true);
|
||||||
|
// g.setColor(1,1,1);
|
||||||
|
const minz = zeiger(150,5,m);
|
||||||
|
g.fillPoly(minz,true);
|
||||||
|
if (unlock){
|
||||||
|
const sekz = zeiger(150,2,s);
|
||||||
|
g.fillPoly(sekz,true);
|
||||||
|
}
|
||||||
|
g.fillCircle(c.x,c.y,4);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
//draws the scale once the app is startet
|
||||||
|
function drawScale(){
|
||||||
|
for(let i=-14;i<47;i++){
|
||||||
|
const win=i*2*Math.PI/60;
|
||||||
|
let d=2;
|
||||||
|
if(i%5==0){d=5;}
|
||||||
|
g.fillPoly(zeiger(300,d,win),true);
|
||||||
|
g.setColor(0,0,0);
|
||||||
|
g.fillRect(10,10,2*c.x-10,2*c.x-10);
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//draws the numbers on the screen
|
||||||
|
|
||||||
|
function drawlet(){
|
||||||
|
g.setFont("Vector",20);
|
||||||
|
for(let i = 0;i<12;i++){
|
||||||
|
g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//calcultes the Position of the numbers when app starts and saves them in an array
|
||||||
|
function setlet(){
|
||||||
|
let sk=1;
|
||||||
|
for(let i=-10;i<50;i+=5){
|
||||||
|
let win=i*2*Math.PI/60;
|
||||||
|
let xsk =c.x+2+Math.cos(win)*(c.x-10),
|
||||||
|
ysk =c.y+2+Math.sin(win)*(c.x-10);
|
||||||
|
if(sk==3){xsk-=10;}
|
||||||
|
if(sk==6){ysk-=10;}
|
||||||
|
if(sk==9){xsk+=10;}
|
||||||
|
if(sk==12){ysk+=10;}
|
||||||
|
if(sk==10){xsk+=3;}
|
||||||
|
zahlpos.push([sk,xsk,ysk]);
|
||||||
|
sk+=1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setlet();
|
||||||
|
// Clear the screen once, at startup
|
||||||
|
g.setBgColor(0,0,0);
|
||||||
|
g.clear();
|
||||||
|
drawScale();
|
||||||
|
draw();
|
||||||
|
|
||||||
|
let secondInterval= setInterval(draw, 1000);
|
||||||
|
// Stop updates when LCD is off, restart when on
|
||||||
|
|
||||||
|
Bangle.on('lcdPower',on=>{
|
||||||
|
if (secondInterval) clearInterval(secondInterval);
|
||||||
|
secondInterval = undefined;
|
||||||
|
if (on) {
|
||||||
|
secondInterval = setInterval(draw, 1000);
|
||||||
|
draw(); // draw immediately
|
||||||
|
}else{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Bangle.on('lock',on=>{
|
||||||
|
if (secondInterval) clearInterval(secondInterval);
|
||||||
|
secondInterval = undefined;
|
||||||
|
if (!on) {
|
||||||
|
secondInterval = setInterval(draw, 1000);
|
||||||
|
unlock = true;
|
||||||
|
draw(); // draw immediately
|
||||||
|
}else{
|
||||||
|
secondInterval = setInterval(draw, 60000);
|
||||||
|
unlock = false;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show launcher when middle button pressed
|
||||||
|
Bangle.setUI("clock");
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwgIEBoUAiAKCgUCBQUEColEAYUQhAmKCwgeCAAcCgEDjwEBkEAg8TBocNgYFDh8GAYMDxkPjEA8EAwkHJgIcBAoPfAoYWCBYYFIgfvAoX4FYRJEAp9gAomYNAOAArPwAogAC4AFiRoIFJLgIFJuADCg//Q4U//4FDj4FEAAV4Aoi0CSxBsCA=="))
|
|
@ -3,3 +3,4 @@
|
||||||
Fix music control
|
Fix music control
|
||||||
0.03: Handling of message actions (ok/clear)
|
0.03: Handling of message actions (ok/clear)
|
||||||
0.04: Android icon now goes to settings page with 'find phone'
|
0.04: Android icon now goes to settings page with 'find phone'
|
||||||
|
0.05: Fix handling of message actions
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
// Message response
|
// Message response
|
||||||
Bangle.messageResponse = (msg,response) => {
|
Bangle.messageResponse = (msg,response) => {
|
||||||
if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
|
if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
|
||||||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS" });
|
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||||
// error/warn here?
|
// error/warn here?
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
|
||||||
0.03: Add "Calculating" placeholder, update JSON save format
|
|
||||||
0.02: Fix JSON save format
|
|
||||||
0.01: First release
|
0.01: First release
|
||||||
|
0.02: Fix JSON save format
|
||||||
|
0.03: Add "Calculating" placeholder, update JSON save format
|
||||||
|
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11)
|
||||||
|
0.02: The app is now a clock, the data is greyed after the connection is lost (2021/12/22)
|
||||||
|
0.03: Set the Awair's IP directly on the webpage (2021/12/27)
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Awair Monitor
|
||||||
|
|
||||||
|
Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.
|
||||||
|
|
||||||
|
* What you need:
|
||||||
|
* A BangleJS 2
|
||||||
|
* An Awair device [with local API enabled](https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature)
|
||||||
|
* The web app [awair_to_bangle.html](awair_to_bangle.html) that will retrieve the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection
|
||||||
|
* How to get started
|
||||||
|
* Launch the Awair Monitor app on your BangleJS
|
||||||
|
* Open awair_to_bangle.html on Chrome (desktop or Android), input the IP address of your Awair device, and click "Connect BangleJS" - it connects to your watch the same way as the Bangle app store
|
||||||
|
* Once connected to the watch with the app running, the watch app is updated once per second
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Creator
|
||||||
|
[@alainsaas](https://github.com/alainsaas)
|
||||||
|
|
||||||
|
Contributions are welcome, send me your Pull Requests!
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwgP/AD38g4FD8EAAoeAgE/AoUD/EfAgP+AYMPDgQPBw4FB/F///DAoPwAQPjAQPBAQPxDgJVCAoP4gYaCCwIcBAoM/8P8h0HjEP8f4h0Gp0H4/44lj5+H4/54lzj/jx/5/lyDgIFDh/xAoQRBAoXsuY8Bx4jCAoeEkYFB447CAoRxBOAPxM4RmC8IFD4ZZD/8H/DHDh/+AoaSBUAIABCoYATVwS2Ct4FE84REXQQLCk4RJAo0XGxY="))
|
|
@ -0,0 +1,108 @@
|
||||||
|
Graphics.prototype.setFontMichroma36 = function() {
|
||||||
|
g.setFontCustom(atob("AAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAAAAAAAAAAAAAAAAAAAGAAAAA+AAAAD+AAAAP+AAAA/8AAAD/wAAAf/AAAB/4AAAH/gAAAf+AAAB/4AAAH/gAAAf+AAAAfwAAAAfAAAAAcAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AP///8APwAD+APAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAeAPAAAeAPwAD+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAEAAAAAOAAAAAfAAAAA+AAAAB8AAAAD8AAAAH4AAAAPwAAAAPgAAAAfAAAAAf///+Af///+Af///+Af///+AAAAAAAAAAAAAAAAAAAAAAAAAA/Af+AD/A/+AH/B/+AP/D/+APwD4eAPADweAfADweAeADweAeADweAeADweAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAPgeAeAPAeAeAPAeAeAPAeAeAPAeAfAPAeAPw/AeAP/+AeAH/+AeAD/8AeAB/wAOAAAAAAAAAAAAAAAAAAAAAAAAAB8APgAD8AP4AH8AP8AP8AP8APgAB+AfAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAeAfAeAeAPx/h+AP///+AH///8AD///4AB/h/gAAAAAAAAAAAAAAAAAAAAAAeAAAAA/AAAAA/AAAAB/AAAAD/AAAAH/AAAAPvAAAAPPAAAAfPAAAA+PAAAB8PAAAD4PAAADwPAAAHwPAAAPgPAAAfAPAAA+APAAA8APAAB8APAAD4APAAHwAPAAPgAPAAPAAPAAfAAPAAf///+Af///+Af///+Af///+AAAAPAAAAAPAAAAAPAAAAAPAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAf/8PgAf/8P4Af/8P8Af/8P8AeB4A+AeB4AeAeDwAeAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAfAeDwAeAeD4A+AeD+D+AeB//8AeB//4AeA//4AAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AH///8AP4fB+APAeAeAfA8AeAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAfA8APAPA+AeAPgeAeAP8fh+AH8f/8AD8P/8AA8H/4AAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAACAeAAAGAeAAAOAeAAAeAeAAA+AeAAD+AeAAH8AeAAP4AeAAfwAeAA/gAeAB/AAeAD+AAeAP4AAeAfwAAeA/gAAeB/AAAeD+AAAeH8AAAefwAAAe/gAAAf/AAAAf+AAAAf8AAAAf4AAAAfgAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAMAAB+B/wAD/j/4AH/3/8AP///+AP//A+AfB+AeAeA+AeAeA+APAeA+APAeA+APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA+APAeA+APAeA+APAeA+AOAeA+AeAPh/A+AP///+AP/3/8AH/3/8AB/D/wAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAD/4HAAH/8HwAP/+H4AP5/H8AfAfA8AeAPAeAeAPAeAeAPAeAeAHgfAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHAPAeAPAOAeAPAeAPAPAeAPwfB+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAAAAAAAB8DwAAB8HwAAB8HwAAB8DwAAAAAAAAAAAAA"), 46, atob("CBIkESMjJCMjIyMjCA=="), 36+(1<<8)+(1<<16));
|
||||||
|
};
|
||||||
|
|
||||||
|
var drawTimeout;
|
||||||
|
|
||||||
|
function queueNextDraw() {
|
||||||
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
|
drawTimeout = setTimeout(function() {
|
||||||
|
drawTimeout = undefined;
|
||||||
|
draw();
|
||||||
|
}, 1000 - (Date.now() % 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
var locale = require("locale");
|
||||||
|
|
||||||
|
var bt_current_co2 = 0;
|
||||||
|
var bt_current_voc = 0;
|
||||||
|
var bt_current_pm25 = 0;
|
||||||
|
var bt_current_humi = 0;
|
||||||
|
var bt_current_temp = 0;
|
||||||
|
var bt_last_update = 0;
|
||||||
|
|
||||||
|
var last_update = 0;
|
||||||
|
var bt_co2_history = new Array(10).fill(0);
|
||||||
|
var bt_voc_history = new Array(10).fill(0);
|
||||||
|
var bt_pm25_history = new Array(10).fill(0);
|
||||||
|
var bt_humi_history = new Array(10).fill(0);
|
||||||
|
var bt_temp_history = new Array(10).fill(0);
|
||||||
|
|
||||||
|
var internal_last_update = -1;
|
||||||
|
|
||||||
|
var display_frozen = false;
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
g.reset().clearRect(0,24,g.getWidth(),g.getHeight());
|
||||||
|
|
||||||
|
var date = new Date();
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 56);
|
||||||
|
|
||||||
|
g.setFont("6x8");
|
||||||
|
g.drawString(locale.date(new Date(),1), g.getWidth()/2, 80);
|
||||||
|
|
||||||
|
g.setFont("6x8");
|
||||||
|
g.drawString("CO2", 20, 100);
|
||||||
|
g.drawString("VOC", 55, 100);
|
||||||
|
g.drawString("PM25", 90, 100);
|
||||||
|
g.drawString("Humi", 125, 100);
|
||||||
|
g.drawString("Temp", 160, 100);
|
||||||
|
|
||||||
|
if (last_update != bt_last_update) {
|
||||||
|
display_frozen = false;
|
||||||
|
last_update = bt_last_update;
|
||||||
|
internal_last_update = last_update;
|
||||||
|
if (last_update % 10 == 0) {
|
||||||
|
bt_co2_history.shift(); bt_co2_history.push(bt_current_co2);
|
||||||
|
bt_voc_history.shift(); bt_voc_history.push(bt_current_voc);
|
||||||
|
bt_pm25_history.shift(); bt_pm25_history.push(bt_current_pm25);
|
||||||
|
bt_humi_history.shift(); bt_humi_history.push(bt_current_humi);
|
||||||
|
bt_temp_history.shift(); bt_temp_history.push(bt_current_temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (internal_last_update == -1) {
|
||||||
|
g.drawString("Waiting for connection", 88, 164);
|
||||||
|
} else if ((internal_last_update > last_update + 5) && (internal_last_update < last_update + 60)) {
|
||||||
|
g.drawString("Trying to reconnect since " + (internal_last_update - last_update), 88, 164);
|
||||||
|
} else if (internal_last_update > last_update + 5) {
|
||||||
|
display_frozen = true;
|
||||||
|
g.drawString("Waiting for connection", 88, 164);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_frozen) { g.setColor("#888"); }
|
||||||
|
|
||||||
|
g.setFont("HaxorNarrow7x17");
|
||||||
|
g.drawString(""+bt_current_co2, 18, 110);
|
||||||
|
g.drawString(""+bt_current_voc, 53, 110);
|
||||||
|
g.drawString(""+bt_current_pm25, 88, 110);
|
||||||
|
g.drawString(""+bt_current_humi, 123, 110);
|
||||||
|
g.drawString(""+bt_current_temp, 158, 110);
|
||||||
|
|
||||||
|
for (i = 0; i < 10; i++) {
|
||||||
|
if (display_frozen) { g.setColor("#888"); }
|
||||||
|
|
||||||
|
// max height = 32
|
||||||
|
g.drawLine(10+i*2, 150-(Math.min(Math.max(bt_co2_history[i],400), 1200)-400)/25, 10+i*2, 150);
|
||||||
|
g.drawLine(45+i*2, 150-(Math.min(Math.max(bt_voc_history[i],0), 1440)-0)/45, 45+i*2, 150);
|
||||||
|
g.drawLine(80+i*2, 150-(Math.min(Math.max(bt_pm25_history[i],0), 32)-0)/1, 80+i*2, 150);
|
||||||
|
g.drawLine(115+i*2, 150-(Math.min(Math.max(bt_humi_history[i],20), 60)-20)/1.25, 115+i*2, 150);
|
||||||
|
g.drawLine(150+i*2, 150-(Math.min(Math.max(bt_temp_history[i],19), 27)-19)*4, 150+i*2, 150);
|
||||||
|
|
||||||
|
// target humidity level
|
||||||
|
g.setColor("#00F").drawLine(115, 150-(40-20)/1.25, 115+18, 150-(40-20)/1.25);
|
||||||
|
g.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (internal_last_update != -1) { internal_last_update++; }
|
||||||
|
queueNextDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// init
|
||||||
|
Bangle.setUI("clock");
|
||||||
|
require("FontHaxorNarrow7x17").add(Graphics);
|
||||||
|
g.clear();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
draw();
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 146 KiB |
|
@ -0,0 +1,673 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://puck-js.com/puck.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
// Don't forget to enable the Local API on your Awair before using this
|
||||||
|
// https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature
|
||||||
|
|
||||||
|
const awair_name_1 = "Awair";
|
||||||
|
|
||||||
|
var bt_connection;
|
||||||
|
var is_connected = false;
|
||||||
|
var reconnect_counter = 5;
|
||||||
|
var reconnect_attempt_counter = 1;
|
||||||
|
var is_chart_started = false;
|
||||||
|
|
||||||
|
function initChart() {
|
||||||
|
var chart_co2;
|
||||||
|
var chart_voc;
|
||||||
|
var chart_pm;
|
||||||
|
var chart_temperature;
|
||||||
|
var chart_humidity;
|
||||||
|
var dataPoints_1 = [];
|
||||||
|
var posx = 0;
|
||||||
|
|
||||||
|
var awair_ip_1 = document.getElementById('inputawairip').value;
|
||||||
|
|
||||||
|
$.getJSON("http://"+awair_ip_1+"/air-data/latest", function(data) {
|
||||||
|
$.each(data, function(key, value){
|
||||||
|
if (dataPoints_1[key] === undefined) { dataPoints_1[key] = []; }
|
||||||
|
if (key === "temp" || key === "humid") { dataPoints_1[key].push({x: posx, y: parseFloat(value)}); }
|
||||||
|
else { dataPoints_1[key].push({x: posx, y: parseInt(value)}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
posx++;
|
||||||
|
|
||||||
|
chart_co2 = new CanvasJS.Chart("chartContainer_co2",{
|
||||||
|
title:{ text:"CO2", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ minimum: 0, labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.co2 }]
|
||||||
|
});
|
||||||
|
chart_voc = new CanvasJS.Chart("chartContainer_voc",{
|
||||||
|
title:{ text:"VOC", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ minimum: 0, labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.voc }]
|
||||||
|
});
|
||||||
|
chart_pm = new CanvasJS.Chart("chartContainer_pm",{
|
||||||
|
title:{ text:"PM", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ minimum: 0, labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.pm25 }]
|
||||||
|
});
|
||||||
|
chart_humidity = new CanvasJS.Chart("chartContainer_humidity",{
|
||||||
|
title:{ text:"Humidity", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.humid }]
|
||||||
|
});
|
||||||
|
chart_temperature = new CanvasJS.Chart("chartContainer_temperature",{
|
||||||
|
title:{ text:"Temperature", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.temp }]
|
||||||
|
});
|
||||||
|
|
||||||
|
chart_co2.set("backgroundColor", "#1A202C");
|
||||||
|
chart_voc.set("backgroundColor", "#1A202C");
|
||||||
|
chart_pm.set("backgroundColor", "#1A202C");
|
||||||
|
chart_humidity.set("backgroundColor", "#1A202C");
|
||||||
|
chart_temperature.set("backgroundColor", "#1A202C");
|
||||||
|
|
||||||
|
updateChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateChart() {
|
||||||
|
$.getJSON("http://"+awair_ip_1+"/air-data/latest", function(data) {
|
||||||
|
$.each(data, function(key, value){
|
||||||
|
if (dataPoints_1[key] === undefined) { dataPoints_1[key] = []; }
|
||||||
|
if (key === "temp" || key === "humid") { dataPoints_1[key].push({x: posx, y: parseFloat(value)}); }
|
||||||
|
else { dataPoints_1[key].push({x: posx, y: parseInt(value)}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
posx++;
|
||||||
|
chart_co2.render();
|
||||||
|
chart_voc.render();
|
||||||
|
chart_pm.render();
|
||||||
|
chart_temperature.render();
|
||||||
|
chart_humidity.render();
|
||||||
|
|
||||||
|
chart_co2.title.set("text", "CO2 level (ppm)");
|
||||||
|
chart_voc.title.set("text", "VOC level (ppb)");
|
||||||
|
chart_pm.title.set("text", "PM2.5 level (ug/m³)");
|
||||||
|
chart_humidity.title.set("text", "Humidity level (%)");
|
||||||
|
chart_temperature.title.set("text", "Temperature level (°C)");
|
||||||
|
|
||||||
|
let current_co2 = dataPoints_1['co2'][dataPoints_1['co2'].length-1].y;
|
||||||
|
let current_voc = dataPoints_1['voc'][dataPoints_1['voc'].length-1].y;
|
||||||
|
let current_pm25 = dataPoints_1['pm25'][dataPoints_1['pm25'].length-1].y;
|
||||||
|
let current_humi = dataPoints_1['humid'][dataPoints_1['humid'].length-1].y;
|
||||||
|
let current_temp = dataPoints_1['temp'][dataPoints_1['temp'].length-1].y;
|
||||||
|
let last_update = dataPoints_1['temp'].length-1;
|
||||||
|
|
||||||
|
if (is_connected && bt_connection && bt_connection.isOpen) {
|
||||||
|
bt_connection.write('\x10bt_current_co2='+current_co2+';bt_current_voc='+current_voc+';bt_current_pm25='+current_pm25+';bt_current_humi='+current_humi+';bt_current_temp='+current_temp+';bt_last_update='+last_update+';\n');
|
||||||
|
|
||||||
|
console.log("Sent data through Bluetooth");
|
||||||
|
} else if (is_connected && bt_connection && !bt_connection.isOpen) {
|
||||||
|
console.log("Disconnected - Next attempt to reconnect in " + reconnect_counter);
|
||||||
|
reconnect_counter--;
|
||||||
|
|
||||||
|
if (reconnect_counter <= 0) {
|
||||||
|
reconnect_counter = 10 * reconnect_attempt_counter;
|
||||||
|
reconnect_attempt_counter++;
|
||||||
|
|
||||||
|
console.log("Trying to reconnect");
|
||||||
|
bt_connection.reconnect(function(c) {
|
||||||
|
console.log("Reconnect callback");
|
||||||
|
if (!c) {
|
||||||
|
console.log("Couldn't reconnect");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bt_connection = c;
|
||||||
|
is_connected = true;
|
||||||
|
reconnect_attempt_counter = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function(){updateChart()}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectBT() {
|
||||||
|
console.log("Connect BT");
|
||||||
|
Puck.connect(function(c) {
|
||||||
|
console.log("Connect callback");
|
||||||
|
if (!c) {
|
||||||
|
console.log("Couldn't connect");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bt_connection = c;
|
||||||
|
is_connected = true;
|
||||||
|
reconnect_attempt_counter = 1;
|
||||||
|
if (!is_chart_started) {
|
||||||
|
initChart();
|
||||||
|
is_chart_started = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectBT() {
|
||||||
|
console.log("Disconnect Bluetooth button pressed. bt_connection value below.")
|
||||||
|
console.log(bt_connection);
|
||||||
|
if (is_connected && bt_connection) {
|
||||||
|
bt_connection.close();
|
||||||
|
is_connected = false;
|
||||||
|
console.log("Closed Bluetooth connection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="https://canvasjs.com/assets/script/jquery-1.11.1.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="background-color:#1A202C;">
|
||||||
|
|
||||||
|
<p style="color: #F7FAFC">
|
||||||
|
<b>How to use</b>
|
||||||
|
<br><br>
|
||||||
|
Step 1: Enable the Local API on your Awair: https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature
|
||||||
|
<br><br>
|
||||||
|
Step 2: Launch the Awair Monitor app on your BangleJS
|
||||||
|
<br><br>
|
||||||
|
Step 3: Input your Awair IP address and click the Connect button:
|
||||||
|
<input type="text" id="inputawairip" value="192.168.2.1">
|
||||||
|
<input type="button" value="Connect BangleJS" onclick="connectBT();">
|
||||||
|
<br><br>
|
||||||
|
Step 4: Optionally, open the web inspector's console (Right click > Inspector > Console) to read the Bluetooth logs
|
||||||
|
<br><br>
|
||||||
|
Step 5: Once you are done, click the Disconnect button to properly close the Blutooth connection
|
||||||
|
<center><button onclick="disconnectBT();">Disconnect BangleJS</button></center>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<div id="chartContainer_co2" style="height: 300px; max-width: 920px; margin: 0px auto; margin-bottom: 64px;"></div>
|
||||||
|
<div id="chartContainer_voc" style="height: 300px; max-width: 920px; margin: 0px auto; margin-bottom: 64px;"></div>
|
||||||
|
<div id="chartContainer_pm" style="height: 300px; max-width: 920px; margin: 0px auto; margin-bottom: 64px;"></div>
|
||||||
|
<div id="chartContainer_humidity" style="height: 300px; max-width: 920px; margin: 0px auto; margin-bottom: 64px;"></div>
|
||||||
|
<div id="chartContainer_temperature" style="height: 300px; max-width: 920px; margin: 0px auto; margin-bottom: 64px;"></div>
|
||||||
|
</body>
|
||||||
|
</html>(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
function str2ab(str) {
|
||||||
|
var buf = new ArrayBuffer(str.length);
|
||||||
|
var bufView = new Uint8Array(buf);
|
||||||
|
for (var i=0, strLen=str.length; i<strLen; i++) {
|
||||||
|
bufView[i] = str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQueue() {
|
||||||
|
if (!queue.length) return;
|
||||||
|
var q = queue.shift();
|
||||||
|
log(3,"Executing "+JSON.stringify(q)+" from queue");
|
||||||
|
if (q.type == "write") puck.write(q.data, q.callback, q.callbackNewline);
|
||||||
|
else log(1,"Unknown queue item "+JSON.stringify(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(callback) {
|
||||||
|
if (!checkIfSupported()) return;
|
||||||
|
|
||||||
|
var connection = {
|
||||||
|
on : function(evt,cb) { this["on"+evt]=cb; },
|
||||||
|
emit : function(evt,data) { if (this["on"+evt]) this["on"+evt](data); },
|
||||||
|
isOpen : false,
|
||||||
|
isOpening : true,
|
||||||
|
txInProgress : false
|
||||||
|
};
|
||||||
|
var btServer = undefined;
|
||||||
|
var btService;
|
||||||
|
var connectionDisconnectCallback;
|
||||||
|
var txCharacteristic;
|
||||||
|
var rxCharacteristic;
|
||||||
|
var txDataQueue = [];
|
||||||
|
var flowControlXOFF = false;
|
||||||
|
var chunkSize = DEFAULT_CHUNKSIZE;
|
||||||
|
|
||||||
|
connection.close = function() {
|
||||||
|
connection.isOpening = false;
|
||||||
|
if (connection.isOpen) {
|
||||||
|
connection.isOpen = false;
|
||||||
|
connection.emit('close');
|
||||||
|
} else {
|
||||||
|
if (callback) callback(null);
|
||||||
|
}
|
||||||
|
if (btServer) {
|
||||||
|
btServer.disconnect();
|
||||||
|
btServer = undefined;
|
||||||
|
txCharacteristic = undefined;
|
||||||
|
rxCharacteristic = undefined;
|
||||||
|
chunkSize = DEFAULT_CHUNKSIZE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connection.write = function(data, callback) {
|
||||||
|
if (data) txDataQueue.push({data:data,callback:callback,maxLength:data.length});
|
||||||
|
if (connection.isOpen && !connection.txInProgress) writeChunk();
|
||||||
|
|
||||||
|
function writeChunk() {
|
||||||
|
if (flowControlXOFF) { // flow control - try again later
|
||||||
|
setTimeout(writeChunk, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var chunk;
|
||||||
|
if (!txDataQueue.length) {
|
||||||
|
puck.writeProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var txItem = txDataQueue[0];
|
||||||
|
puck.writeProgress(txItem.maxLength - txItem.data.length, txItem.maxLength);
|
||||||
|
if (txItem.data.length <= chunkSize) {
|
||||||
|
chunk = txItem.data;
|
||||||
|
txItem.data = undefined;
|
||||||
|
} else {
|
||||||
|
chunk = txItem.data.substr(0,chunkSize);
|
||||||
|
txItem.data = txItem.data.substr(chunkSize);
|
||||||
|
}
|
||||||
|
connection.txInProgress = true;
|
||||||
|
log(2, "Sending "+ JSON.stringify(chunk));
|
||||||
|
txCharacteristic.writeValue(str2ab(chunk)).then(function() {
|
||||||
|
log(3, "Sent");
|
||||||
|
if (!txItem.data) {
|
||||||
|
txDataQueue.shift(); // remove this element
|
||||||
|
if (txItem.callback)
|
||||||
|
txItem.callback();
|
||||||
|
}
|
||||||
|
connection.txInProgress = false;
|
||||||
|
writeChunk();
|
||||||
|
}).catch(function(error) {
|
||||||
|
log(1, 'SEND ERROR: ' + error);
|
||||||
|
txDataQueue = [];
|
||||||
|
connection.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.bluetooth.requestDevice({
|
||||||
|
filters:[
|
||||||
|
{ namePrefix: 'Puck.js' },
|
||||||
|
{ namePrefix: 'Pixl.js' },
|
||||||
|
{ namePrefix: 'MDBT42Q' },
|
||||||
|
{ namePrefix: 'RuuviTag' },
|
||||||
|
{ namePrefix: 'iTracker' },
|
||||||
|
{ namePrefix: 'Thingy' },
|
||||||
|
{ namePrefix: 'Espruino' },
|
||||||
|
{ services: [ NORDIC_SERVICE ] }
|
||||||
|
], optionalServices: [ NORDIC_SERVICE ]}).then(function(device) {
|
||||||
|
log(1, 'Device Name: ' + device.name);
|
||||||
|
log(1, 'Device ID: ' + device.id);
|
||||||
|
// Was deprecated: Should use getPrimaryServices for this in future
|
||||||
|
//log('BT> Device UUIDs: ' + device.uuids.join('\n' + ' '.repeat(21)));
|
||||||
|
device.addEventListener('gattserverdisconnected', function() {
|
||||||
|
log(1, "Disconnected (gattserverdisconnected)");
|
||||||
|
connection.close();
|
||||||
|
});
|
||||||
|
connection.device = device;
|
||||||
|
connection.reconnect(callback);
|
||||||
|
}).catch(function(error) {
|
||||||
|
log(1, 'ERROR: ' + error);
|
||||||
|
connection.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.reconnect = function(callback) {
|
||||||
|
connection.device.gatt.connect().then(function(server) {
|
||||||
|
log(1, "Connected");
|
||||||
|
btServer = server;
|
||||||
|
return server.getPrimaryService(NORDIC_SERVICE);
|
||||||
|
}).then(function(service) {
|
||||||
|
log(2, "Got service");
|
||||||
|
btService = service;
|
||||||
|
return btService.getCharacteristic(NORDIC_RX);
|
||||||
|
}).then(function (characteristic) {
|
||||||
|
rxCharacteristic = characteristic;
|
||||||
|
log(2, "RX characteristic:"+JSON.stringify(rxCharacteristic));
|
||||||
|
rxCharacteristic.addEventListener('characteristicvaluechanged', function(event) {
|
||||||
|
var dataview = event.target.value;
|
||||||
|
var data = ab2str(dataview.buffer);
|
||||||
|
if (data.length > chunkSize) {
|
||||||
|
log(2, "Received packet of length "+data.length+", increasing chunk size");
|
||||||
|
chunkSize = data.length;
|
||||||
|
}
|
||||||
|
if (puck.flowControl) {
|
||||||
|
for (var i=0;i<data.length;i++) {
|
||||||
|
var ch = data.charCodeAt(i);
|
||||||
|
var remove = true;
|
||||||
|
if (ch==19) {// XOFF
|
||||||
|
log(2,"XOFF received => pause upload");
|
||||||
|
flowControlXOFF = true;
|
||||||
|
} else if (ch==17) {// XON
|
||||||
|
log(2,"XON received => resume upload");
|
||||||
|
flowControlXOFF = false;
|
||||||
|
} else
|
||||||
|
remove = false;
|
||||||
|
if (remove) { // remove character
|
||||||
|
data = data.substr(0,i-1)+data.substr(i+1);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(3, "Received "+JSON.stringify(data));
|
||||||
|
connection.emit('data', data);
|
||||||
|
});
|
||||||
|
return rxCharacteristic.startNotifications();
|
||||||
|
}).then(function() {
|
||||||
|
return btService.getCharacteristic(NORDIC_TX);
|
||||||
|
}).then(function (characteristic) {
|
||||||
|
txCharacteristic = characteristic;
|
||||||
|
log(2, "TX characteristic:"+JSON.stringify(txCharacteristic));
|
||||||
|
}).then(function() {
|
||||||
|
connection.txInProgress = false;
|
||||||
|
connection.isOpen = true;
|
||||||
|
connection.isOpening = false;
|
||||||
|
isBusy = false;
|
||||||
|
queue = [];
|
||||||
|
callback(connection);
|
||||||
|
connection.emit('open');
|
||||||
|
// if we had any writes queued, do them now
|
||||||
|
connection.write();
|
||||||
|
}).catch(function(error) {
|
||||||
|
log(1, 'ERROR: ' + error);
|
||||||
|
connection.close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
var connection;
|
||||||
|
/* convenience function... Write data, call the callback with data:
|
||||||
|
callbackNewline = false => if no new data received for ~0.2 sec
|
||||||
|
callbackNewline = true => after a newline */
|
||||||
|
function write(data, callback, callbackNewline) {
|
||||||
|
if (!checkIfSupported()) return;
|
||||||
|
|
||||||
|
let result;
|
||||||
|
/// If there wasn't a callback function, then promisify
|
||||||
|
if (typeof callback !== 'function') {
|
||||||
|
callbackNewline = callback;
|
||||||
|
|
||||||
|
result = new Promise((resolve, reject) => callback = (value, err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBusy) {
|
||||||
|
log(3, "Busy - adding Puck.write to queue");
|
||||||
|
queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cbTimeout;
|
||||||
|
function onWritten() {
|
||||||
|
if (callbackNewline) {
|
||||||
|
connection.cb = function(d) {
|
||||||
|
var newLineIdx = connection.received.indexOf("\n");
|
||||||
|
if (newLineIdx>=0) {
|
||||||
|
var l = connection.received.substr(0,newLineIdx);
|
||||||
|
connection.received = connection.received.substr(newLineIdx+1);
|
||||||
|
connection.cb = undefined;
|
||||||
|
if (cbTimeout) clearTimeout(cbTimeout);
|
||||||
|
cbTimeout = undefined;
|
||||||
|
if (callback)
|
||||||
|
callback(l);
|
||||||
|
isBusy = false;
|
||||||
|
handleQueue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// wait for any received data if we have a callback...
|
||||||
|
var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data
|
||||||
|
var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/;
|
||||||
|
var maxDataTime = dataWaitTime; // max time we wait after having received data
|
||||||
|
cbTimeout = setTimeout(function timeout() {
|
||||||
|
cbTimeout = undefined;
|
||||||
|
if (maxTime) maxTime--;
|
||||||
|
if (maxDataTime) maxDataTime--;
|
||||||
|
if (connection.hadData) maxDataTime=dataWaitTime;
|
||||||
|
if (maxDataTime && maxTime) {
|
||||||
|
cbTimeout = setTimeout(timeout, 100);
|
||||||
|
} else {
|
||||||
|
connection.cb = undefined;
|
||||||
|
if (callback)
|
||||||
|
callback(connection.received);
|
||||||
|
isBusy = false;
|
||||||
|
handleQueue();
|
||||||
|
connection.received = "";
|
||||||
|
}
|
||||||
|
connection.hadData = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection && (connection.isOpen || connection.isOpening)) {
|
||||||
|
if (!connection.txInProgress) connection.received = "";
|
||||||
|
isBusy = true;
|
||||||
|
connection.write(data, onWritten);
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
connection = connect(function(puck) {
|
||||||
|
if (!puck) {
|
||||||
|
connection = undefined;
|
||||||
|
if (callback) callback(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connection.received = "";
|
||||||
|
connection.on('data', function(d) {
|
||||||
|
connection.received += d;
|
||||||
|
connection.hadData = true;
|
||||||
|
if (connection.cb) connection.cb(d);
|
||||||
|
});
|
||||||
|
connection.on('close', function(d) {
|
||||||
|
connection = undefined;
|
||||||
|
});
|
||||||
|
isBusy = true;
|
||||||
|
connection.write(data, onWritten);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
|
||||||
|
var puck = {
|
||||||
|
/// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
|
||||||
|
debug : 1,
|
||||||
|
/// Should we use flow control? Default is true
|
||||||
|
flowControl : true,
|
||||||
|
/// Used internally to write log information - you can replace this with your own function
|
||||||
|
log : function(level, s) { if (level <= this.debug) console.log("<BLE> "+s)},
|
||||||
|
/// Called with the current send progress or undefined when done - you can replace this with your own function
|
||||||
|
writeProgress : function (charsSent, charsTotal) {
|
||||||
|
//console.log(charsSent + "/" + charsTotal);
|
||||||
|
},
|
||||||
|
/** Connect to a new device - this creates a separate
|
||||||
|
connection to the one `write` and `eval` use. */
|
||||||
|
connect : connect,
|
||||||
|
/// Write to Puck.js and call back when the data is written. Creates a connection if it doesn't exist
|
||||||
|
write : write,
|
||||||
|
/// Evaluate an expression and call cb with the result. Creates a connection if it doesn't exist
|
||||||
|
eval : function(expr, cb) {
|
||||||
|
|
||||||
|
const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true)
|
||||||
|
.then(function (d) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(d);
|
||||||
|
} catch (e) {
|
||||||
|
log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString());
|
||||||
|
return Promise.reject(d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
return void response.then(cb, (err) => cb(null, err));
|
||||||
|
} else {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
/// Write the current time to the Puck
|
||||||
|
setTime : function(cb) {
|
||||||
|
var d = new Date();
|
||||||
|
var cmd = 'setTime('+(d.getTime()/1000)+');';
|
||||||
|
// in 1v93 we have timezones too
|
||||||
|
cmd += 'if (E.setTimeZone) E.setTimeZone('+d.getTimezoneOffset()/-60+');\n';
|
||||||
|
write(cmd, cb);
|
||||||
|
},
|
||||||
|
/// Did `write` and `eval` manage to create a connection?
|
||||||
|
isConnected : function() {
|
||||||
|
return connection!==undefined;
|
||||||
|
},
|
||||||
|
/// get the connection used by `write` and `eval`
|
||||||
|
getConnection : function() {
|
||||||
|
return connection;
|
||||||
|
},
|
||||||
|
/// Close the connection used by `write` and `eval`
|
||||||
|
close : function() {
|
||||||
|
if (connection)
|
||||||
|
connection.close();
|
||||||
|
},
|
||||||
|
/** Utility function to fade out everything on the webpage and display
|
||||||
|
a window saying 'Click to continue'. When clicked it'll disappear and
|
||||||
|
'callback' will be called. This is useful because you can't initialise
|
||||||
|
Web Bluetooth unless you're doing so in response to a user input.*/
|
||||||
|
modal : function(callback) {
|
||||||
|
var e = document.createElement('div');
|
||||||
|
e.style = 'position:absolute;top:0px;left:0px;right:0px;bottom:0px;opacity:0.5;z-index:100;background:black;';
|
||||||
|
e.innerHTML = '<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-family: Sans-Serif;font-size:400%;color:white;">Click to Continue...</div>';
|
||||||
|
e.onclick = function(evt) {
|
||||||
|
callback();
|
||||||
|
evt.preventDefault();
|
||||||
|
document.body.removeChild(e);
|
||||||
|
};
|
||||||
|
document.body.appendChild(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return puck;
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
// Don't forget to enable the Local API on your Awair before using this
|
||||||
|
// https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature
|
||||||
|
|
||||||
|
const awair_name_1 = "Awair";
|
||||||
|
|
||||||
|
var bt_connection;
|
||||||
|
var is_connected = false;
|
||||||
|
var reconnect_counter = 5;
|
||||||
|
var reconnect_attempt_counter = 1;
|
||||||
|
var is_chart_started = false;
|
||||||
|
|
||||||
|
function initChart() {
|
||||||
|
var chart_co2;
|
||||||
|
var chart_voc;
|
||||||
|
var chart_pm;
|
||||||
|
var chart_temperature;
|
||||||
|
var chart_humidity;
|
||||||
|
var dataPoints_1 = [];
|
||||||
|
var posx = 0;
|
||||||
|
|
||||||
|
var awair_ip_1 = document.getElementById('inputawairip').value;
|
||||||
|
|
||||||
|
$.getJSON("http://"+awair_ip_1+"/air-data/latest", function(data) {
|
||||||
|
$.each(data, function(key, value){
|
||||||
|
if (dataPoints_1[key] === undefined) { dataPoints_1[key] = []; }
|
||||||
|
if (key === "temp" || key === "humid") { dataPoints_1[key].push({x: posx, y: parseFloat(value)}); }
|
||||||
|
else { dataPoints_1[key].push({x: posx, y: parseInt(value)}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
posx++;
|
||||||
|
|
||||||
|
chart_co2 = new CanvasJS.Chart("chartContainer_co2",{
|
||||||
|
title:{ text:"CO2", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ minimum: 0, labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.co2 }]
|
||||||
|
});
|
||||||
|
chart_voc = new CanvasJS.Chart("chartContainer_voc",{
|
||||||
|
title:{ text:"VOC", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ minimum: 0, labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.voc }]
|
||||||
|
});
|
||||||
|
chart_pm = new CanvasJS.Chart("chartContainer_pm",{
|
||||||
|
title:{ text:"PM", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ minimum: 0, labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.pm25 }]
|
||||||
|
});
|
||||||
|
chart_humidity = new CanvasJS.Chart("chartContainer_humidity",{
|
||||||
|
title:{ text:"Humidity", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.humid }]
|
||||||
|
});
|
||||||
|
chart_temperature = new CanvasJS.Chart("chartContainer_temperature",{
|
||||||
|
title:{ text:"Temperature", fontFamily: "helvetica", fontColor: "#F7FAFC", fontSize: 16, horizontalAlign: "left", padding: { left: 30 } },
|
||||||
|
axisX:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
axisY:{ labelFontColor: "#F7FAFC", gridColor: "#2D3748", lineColor: "#2D3748", tickColor: "#2D3748" },
|
||||||
|
legend: { fontColor: "#F7FAFC", horizontalAlign: "center", verticalAlign: "bottom" },
|
||||||
|
data: [ { type: "line", lineColor: "#6648FF", showInLegend: true, legendText: awair_name_1, dataPoints : dataPoints_1.temp }]
|
||||||
|
});
|
||||||
|
|
||||||
|
chart_co2.set("backgroundColor", "#1A202C");
|
||||||
|
chart_voc.set("backgroundColor", "#1A202C");
|
||||||
|
chart_pm.set("backgroundColor", "#1A202C");
|
||||||
|
chart_humidity.set("backgroundColor", "#1A202C");
|
||||||
|
chart_temperature.set("backgroundColor", "#1A202C");
|
||||||
|
|
||||||
|
updateChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateChart() {
|
||||||
|
$.getJSON("http://"+awair_ip_1+"/air-data/latest", function(data) {
|
||||||
|
$.each(data, function(key, value){
|
||||||
|
if (dataPoints_1[key] === undefined) { dataPoints_1[key] = []; }
|
||||||
|
if (key === "temp" || key === "humid") { dataPoints_1[key].push({x: posx, y: parseFloat(value)}); }
|
||||||
|
else { dataPoints_1[key].push({x: posx, y: parseInt(value)}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
posx++;
|
||||||
|
chart_co2.render();
|
||||||
|
chart_voc.render();
|
||||||
|
chart_pm.render();
|
||||||
|
chart_temperature.render();
|
||||||
|
chart_humidity.render();
|
||||||
|
|
||||||
|
chart_co2.title.set("text", "CO2 level (ppm)");
|
||||||
|
chart_voc.title.set("text", "VOC level (ppb)");
|
||||||
|
chart_pm.title.set("text", "PM2.5 level (ug/m³)");
|
||||||
|
chart_humidity.title.set("text", "Humidity level (%)");
|
||||||
|
chart_temperature.title.set("text", "Temperature level (°C)");
|
||||||
|
|
||||||
|
let current_co2 = dataPoints_1['co2'][dataPoints_1['co2'].length-1].y;
|
||||||
|
let current_voc = dataPoints_1['voc'][dataPoints_1['voc'].length-1].y;
|
||||||
|
let current_pm25 = dataPoints_1['pm25'][dataPoints_1['pm25'].length-1].y;
|
||||||
|
let current_humi = dataPoints_1['humid'][dataPoints_1['humid'].length-1].y;
|
||||||
|
let current_temp = dataPoints_1['temp'][dataPoints_1['temp'].length-1].y;
|
||||||
|
let last_update = dataPoints_1['temp'].length-1;
|
||||||
|
|
||||||
|
if (is_connected && bt_connection && bt_connection.isOpen) {
|
||||||
|
bt_connection.write('\x10bt_current_co2='+current_co2+';bt_current_voc='+current_voc+';bt_current_pm25='+current_pm25+';bt_current_humi='+current_humi+';bt_current_temp='+current_temp+';bt_
|
After Width: | Height: | Size: 3.9 KiB |
|
@ -68,7 +68,7 @@ ${track.map(pt=>` <gx:value>${pt.distance}</gx:value>\n`).join("")}
|
||||||
|
|
||||||
function saveGPX(track, title) {
|
function saveGPX(track, title) {
|
||||||
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
|
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
|
||||||
<metadata>
|
<metadata>
|
||||||
<time>${track[0].date.toISOString()}</time>
|
<time>${track[0].date.toISOString()}</time>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAMGAAAAAAYDAAAAAAwBgAAAABgAwAAAABAAQAAAABAAQAAAABAAQAAAABAAQAAAABAAQAAAABgAwAAAAAwBgAAAAAYDAAAAAAMGAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAP+AAAAAAf/AAAAAA//gAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAA//gAAAAAf/AAAAAAP+AAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))
|
require("heatshrink").decompress(atob("mEwgIurg/wAocMjAFDjEMIAkGAodggYFDoBLEAq4jFF4o7FI4pTFOLsP/AFDj/8Aoc//wFDv//As4vFHYpHFOLoAPA=="))
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
// Create an entry in apps.json as follows:
|
|
||||||
{ "id": "bluetoothdock",
|
|
||||||
"name": "Bluetooth Dock",
|
|
||||||
"shortName":"Dock",
|
|
||||||
"icon": "app.png",
|
|
||||||
"version":"0.01",
|
|
||||||
"description": "When charging shows the time, scans Bluetooth for known devices (eg temperature) and shows them on the screen",
|
|
||||||
"tags": "bluetooth",
|
|
||||||
"readme": "README.md",
|
|
||||||
"storage": [
|
|
||||||
{"name":"bluetoothdock.app.js","url":"app.js"},
|
|
||||||
{"name":"bluetoothdock.boot.js","url":"boot.js"},
|
|
||||||
{"name":"bluetoothdock.img","url":"app-icon.js","evaluate":true}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -42,3 +42,4 @@
|
||||||
0.36: Add comments to .boot0 to make debugging a bit easier
|
0.36: Add comments to .boot0 to make debugging a bit easier
|
||||||
0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app
|
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.38: Option to log to file if settings.log==2
|
||||||
|
0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035)
|
||||||
|
|
|
@ -88,7 +88,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
|
||||||
// Apply any settings-specific stuff
|
// Apply any settings-specific stuff
|
||||||
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
|
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
|
||||||
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
|
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
|
||||||
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`;
|
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
|
||||||
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
|
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
|
||||||
// Pre-2v10 firmwares without a theme/setUI
|
// Pre-2v10 firmwares without a theme/setUI
|
||||||
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted
|
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
0.02: fix precision rounding issue + no reset when equals pressed
|
0.02: fix precision rounding issue + no reset when equals pressed
|
||||||
0.03: Support for different screen sizes and touchscreen
|
0.03: Support for different screen sizes and touchscreen
|
||||||
0.04: Display current operation on LHS
|
0.04: Display current operation on LHS
|
||||||
|
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
g.clear();
|
g.clear();
|
||||||
require("Font7x11Numeric7Seg").add(Graphics);
|
require("Font7x11Numeric7Seg").add(Graphics);
|
||||||
|
|
||||||
var DEFAULT_SELECTION = '5';
|
var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R';
|
||||||
var RIGHT_MARGIN = 20;
|
var RIGHT_MARGIN = 20;
|
||||||
var RESULT_HEIGHT = 40;
|
var RESULT_HEIGHT = 40;
|
||||||
var COLORS = {
|
var COLORS = {
|
||||||
|
@ -18,97 +18,45 @@ var COLORS = {
|
||||||
SPECIAL: ['#65686C', '#7F8183']
|
SPECIAL: ['#65686C', '#7F8183']
|
||||||
};
|
};
|
||||||
|
|
||||||
var keys = {
|
var KEY_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()];
|
||||||
'0': {
|
|
||||||
xy: [0, 200, 120, 240],
|
var screen, screenColor;
|
||||||
trbl: '2.00'
|
var globalGrid = [4, 5];
|
||||||
},
|
var swipeEnabled;
|
||||||
'.': {
|
|
||||||
xy: [120, 200, 180, 240],
|
var numbersGrid = [3, 4];
|
||||||
trbl: '3=.0'
|
var numbers = {
|
||||||
},
|
'0': {grid: [1, 3], globalGrid: [1, 4], trbl: '2.00'},
|
||||||
'=': {
|
'.': {grid: [2, 3], globalGrid: [2, 4], trbl: '3=.0'},
|
||||||
xy: [181, 200, 240, 240],
|
'1': {grid: [0, 2], globalGrid: [0, 3], trbl: '4201'},
|
||||||
trbl: '+==.',
|
'2': {grid: [1, 2], globalGrid: [1, 3], trbl: '5301'},
|
||||||
color: COLORS.OPERATOR
|
'3': {grid: [2, 2], globalGrid: [2, 3], trbl: '6+.2'},
|
||||||
},
|
'4': {grid: [0, 1], globalGrid: [0, 2], trbl: '7514'},
|
||||||
'1': {
|
'5': {grid: [1, 1], globalGrid: [1, 2], trbl: '8624'},
|
||||||
xy: [0, 160, 60, 200],
|
'6': {grid: [2, 1], globalGrid: [2, 2], trbl: '9-35'},
|
||||||
trbl: '4201'
|
'7': {grid: [0, 0], globalGrid: [0, 1], trbl: 'R847'},
|
||||||
},
|
'8': {grid: [1, 0], globalGrid: [1, 1], trbl: 'N957'},
|
||||||
'2': {
|
'9': {grid: [2, 0], globalGrid: [2, 1], trbl: '%*68'},
|
||||||
xy: [60, 160, 120, 200],
|
|
||||||
trbl: '5301'
|
|
||||||
},
|
|
||||||
'3': {
|
|
||||||
xy: [120, 160, 180, 200],
|
|
||||||
trbl: '6+.2'
|
|
||||||
},
|
|
||||||
'+': {
|
|
||||||
xy: [181, 160, 240, 200],
|
|
||||||
trbl: '-+=3',
|
|
||||||
color: COLORS.OPERATOR
|
|
||||||
},
|
|
||||||
'4': {
|
|
||||||
xy: [0, 120, 60, 160],
|
|
||||||
trbl: '7514'
|
|
||||||
},
|
|
||||||
'5': {
|
|
||||||
xy: [60, 120, 120, 160],
|
|
||||||
trbl: '8624'
|
|
||||||
},
|
|
||||||
'6': {
|
|
||||||
xy: [120, 120, 180, 160],
|
|
||||||
trbl: '9-35'
|
|
||||||
},
|
|
||||||
'-': {
|
|
||||||
xy: [181, 120, 240, 160],
|
|
||||||
trbl: '*-+6',
|
|
||||||
color: COLORS.OPERATOR
|
|
||||||
},
|
|
||||||
'7': {
|
|
||||||
xy: [0, 80, 60, 120],
|
|
||||||
trbl: 'R847'
|
|
||||||
},
|
|
||||||
'8': {
|
|
||||||
xy: [60, 80, 120, 120],
|
|
||||||
trbl: 'N957'
|
|
||||||
},
|
|
||||||
'9': {
|
|
||||||
xy: [120, 80, 180, 120],
|
|
||||||
trbl: '%*68'
|
|
||||||
},
|
|
||||||
'*': {
|
|
||||||
xy: [181, 80, 240, 120],
|
|
||||||
trbl: '/*-9',
|
|
||||||
color: COLORS.OPERATOR
|
|
||||||
},
|
|
||||||
'R': {
|
|
||||||
xy: [0, 40, 60, 79],
|
|
||||||
trbl: 'RN7R',
|
|
||||||
color: COLORS.SPECIAL,
|
|
||||||
val: 'AC'
|
|
||||||
},
|
|
||||||
'N': {
|
|
||||||
xy: [60, 40, 120, 79],
|
|
||||||
trbl: 'N%8R',
|
|
||||||
color: COLORS.SPECIAL,
|
|
||||||
val: '+/-'
|
|
||||||
},
|
|
||||||
'%': {
|
|
||||||
xy: [120, 40, 180, 79],
|
|
||||||
trbl: '%/9N',
|
|
||||||
color: COLORS.SPECIAL
|
|
||||||
},
|
|
||||||
'/': {
|
|
||||||
xy: [181, 40, 240, 79],
|
|
||||||
trbl: '//*%',
|
|
||||||
color: COLORS.OPERATOR
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var selected = DEFAULT_SELECTION;
|
var operatorsGrid = [2, 3];
|
||||||
var prevSelected = DEFAULT_SELECTION;
|
var operators = {
|
||||||
|
'+': {grid: [0, 0], globalGrid: [3, 3], trbl: '-+=3'},
|
||||||
|
'-': {grid: [1, 0], globalGrid: [3, 2], trbl: '*-+6'},
|
||||||
|
'*': {grid: [0, 1], globalGrid: [3, 1], trbl: '/*-9'},
|
||||||
|
'/': {grid: [1, 1], globalGrid: [3, 0], trbl: '//*%'},
|
||||||
|
'=': {grid: [1, 2], globalGrid: [3, 4], trbl: '+==.'},
|
||||||
|
};
|
||||||
|
|
||||||
|
var specialsGrid = [2, 2];
|
||||||
|
var specials = {
|
||||||
|
'R': {grid: [0, 0], globalGrid: [0, 0], trbl: 'RN7R', val: 'AC'},
|
||||||
|
'N': {grid: [1, 0], globalGrid: [1, 0], trbl: 'N%8R', val: '+/-'},
|
||||||
|
'%': {grid: [0, 1], globalGrid: [2, 0], trbl: '%/9N'},
|
||||||
|
};
|
||||||
|
|
||||||
|
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||||
var prevNumber = null;
|
var prevNumber = null;
|
||||||
var currNumber = null;
|
var currNumber = null;
|
||||||
var operator = null;
|
var operator = null;
|
||||||
|
@ -116,6 +64,27 @@ var results = null;
|
||||||
var isDecimal = false;
|
var isDecimal = false;
|
||||||
var hasPressedEquals = false;
|
var hasPressedEquals = false;
|
||||||
|
|
||||||
|
function prepareScreen(screen, grid, defaultColor) {
|
||||||
|
for (var k in screen) {
|
||||||
|
if (screen.hasOwnProperty(k)) {
|
||||||
|
screen[k].color = screen[k].color || defaultColor;
|
||||||
|
var position = [];
|
||||||
|
var xGrid = (KEY_AREA[2]-KEY_AREA[0])/grid[0];
|
||||||
|
var yGrid = (KEY_AREA[3]-KEY_AREA[1])/grid[1];
|
||||||
|
if (swipeEnabled) {
|
||||||
|
position[0] = KEY_AREA[0]+xGrid*screen[k].grid[0];
|
||||||
|
position[1] = KEY_AREA[1]+yGrid*screen[k].grid[1];
|
||||||
|
} else {
|
||||||
|
position[0] = KEY_AREA[0]+xGrid*screen[k].globalGrid[0];
|
||||||
|
position[1] = KEY_AREA[1]+yGrid*screen[k].globalGrid[1];
|
||||||
|
}
|
||||||
|
position[2] = position[0]+xGrid-1;
|
||||||
|
position[3] = position[1]+yGrid-1;
|
||||||
|
screen[k].xy = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawKey(name, k, selected) {
|
function drawKey(name, k, selected) {
|
||||||
var rMargin = 0;
|
var rMargin = 0;
|
||||||
var bMargin = 0;
|
var bMargin = 0;
|
||||||
|
@ -142,6 +111,56 @@ function drawKey(name, k, selected) {
|
||||||
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
|
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawKeys() {
|
||||||
|
g.setColor(screenColor[0]);
|
||||||
|
g.fillRect(KEY_AREA[0], KEY_AREA[1], KEY_AREA[2], KEY_AREA[3]);
|
||||||
|
for (var k in screen) {
|
||||||
|
if (screen.hasOwnProperty(k)) {
|
||||||
|
drawKey(k, screen[k], k == selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function drawGlobal() {
|
||||||
|
screen = {};
|
||||||
|
screenColor = COLORS.DEFAULT;
|
||||||
|
prepareScreen(numbers, globalGrid, COLORS.DEFAULT);
|
||||||
|
for (var k in numbers) {
|
||||||
|
screen[k] = numbers[k];
|
||||||
|
}
|
||||||
|
prepareScreen(operators, globalGrid, COLORS.OPERATOR);
|
||||||
|
for (var k in operators) {
|
||||||
|
screen[k] = operators[k];
|
||||||
|
}
|
||||||
|
prepareScreen(specials, globalGrid, COLORS.SPECIAL);
|
||||||
|
for (var k in specials) {
|
||||||
|
screen[k] = specials[k];
|
||||||
|
}
|
||||||
|
drawKeys();
|
||||||
|
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
}
|
||||||
|
function drawNumbers() {
|
||||||
|
screen = numbers;
|
||||||
|
screenColor = COLORS.DEFAULT;
|
||||||
|
drawKeys();
|
||||||
|
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
}
|
||||||
|
function drawOperators() {
|
||||||
|
screen = operators;
|
||||||
|
screenColor =COLORS.OPERATOR;
|
||||||
|
drawKeys();
|
||||||
|
var selected = DEFAULT_SELECTION_OPERATORS;
|
||||||
|
var prevSelected = DEFAULT_SELECTION_OPERATORS;
|
||||||
|
}
|
||||||
|
function drawSpecials() {
|
||||||
|
screen = specials;
|
||||||
|
screenColor = COLORS.SPECIAL;
|
||||||
|
drawKeys();
|
||||||
|
var selected = DEFAULT_SELECTION_SPECIALS;
|
||||||
|
var prevSelected = DEFAULT_SELECTION_SPECIALS;
|
||||||
|
}
|
||||||
|
|
||||||
function getIntWithPrecision(x) {
|
function getIntWithPrecision(x) {
|
||||||
var xStr = x.toString();
|
var xStr = x.toString();
|
||||||
var xRadix = xStr.indexOf('.');
|
var xRadix = xStr.indexOf('.');
|
||||||
|
@ -218,8 +237,8 @@ function displayOutput(num) {
|
||||||
hasPressedEquals = false;
|
hasPressedEquals = false;
|
||||||
prevNumber = null;
|
prevNumber = null;
|
||||||
operator = null;
|
operator = null;
|
||||||
keys.R.val = 'AC';
|
specials.R.val = 'AC';
|
||||||
drawKey('R', keys.R);
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
g.setFont('Vector', 22);
|
g.setFont('Vector', 22);
|
||||||
} else {
|
} else {
|
||||||
// might not be a number due to display of dot "."
|
// might not be a number due to display of dot "."
|
||||||
|
@ -299,12 +318,12 @@ function buttonPress(val) {
|
||||||
results = null;
|
results = null;
|
||||||
isDecimal = false;
|
isDecimal = false;
|
||||||
hasPressedEquals = false;
|
hasPressedEquals = false;
|
||||||
if (keys.R.val == 'AC') {
|
if (specials.R.val == 'AC') {
|
||||||
prevNumber = null;
|
prevNumber = null;
|
||||||
operator = null;
|
operator = null;
|
||||||
} else {
|
} else {
|
||||||
keys.R.val = 'AC';
|
specials.R.val = 'AC';
|
||||||
drawKey('R', keys.R, true);
|
drawKey('R', specials.R, true);
|
||||||
}
|
}
|
||||||
wasPressedEquals = false;
|
wasPressedEquals = false;
|
||||||
hasPressedNumber = false;
|
hasPressedNumber = false;
|
||||||
|
@ -331,10 +350,11 @@ function buttonPress(val) {
|
||||||
case '+':
|
case '+':
|
||||||
calculatorLogic(val);
|
calculatorLogic(val);
|
||||||
hasPressedNumber = false;
|
hasPressedNumber = false;
|
||||||
|
if (swipeEnabled) drawNumbers();
|
||||||
break;
|
break;
|
||||||
case '.':
|
case '.':
|
||||||
keys.R.val = 'C';
|
specials.R.val = 'C';
|
||||||
drawKey('R', keys.R);
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
isDecimal = true;
|
isDecimal = true;
|
||||||
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
|
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
|
||||||
break;
|
break;
|
||||||
|
@ -348,8 +368,8 @@ function buttonPress(val) {
|
||||||
hasPressedNumber = false;
|
hasPressedNumber = false;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
keys.R.val = 'C';
|
specials.R.val = 'C';
|
||||||
drawKey('R', keys.R);
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
|
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
|
||||||
if (isDecimal) {
|
if (isDecimal) {
|
||||||
currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
|
currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
|
||||||
|
@ -367,23 +387,31 @@ function buttonPress(val) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveDirection(d) {
|
function moveDirection(d) {
|
||||||
drawKey(selected, keys[selected]);
|
drawKey(selected, screen[selected]);
|
||||||
prevSelected = selected;
|
prevSelected = selected;
|
||||||
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d];
|
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : screen[selected].trbl[d];
|
||||||
drawKey(selected, keys[selected], true);
|
drawKey(selected, screen[selected], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (global.BTN4) {
|
if (process.env.HWVERSION==1) {
|
||||||
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
|
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
|
||||||
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
|
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
|
||||||
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
|
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
|
||||||
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
|
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
|
||||||
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
|
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
|
||||||
|
swipeEnabled = false;
|
||||||
|
drawGlobal();
|
||||||
} else { // touchscreen?
|
} else { // touchscreen?
|
||||||
selected = "NONE";
|
selected = "NONE";
|
||||||
|
swipeEnabled = true;
|
||||||
|
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
|
||||||
|
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
|
||||||
|
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
|
||||||
|
drawNumbers();
|
||||||
Bangle.on('touch',(n,e)=>{
|
Bangle.on('touch',(n,e)=>{
|
||||||
for (var key in keys) {
|
for (var key in screen) {
|
||||||
var r = keys[key].xy;
|
if (typeof screen[key] == "undefined") break;
|
||||||
|
var r = screen[key].xy;
|
||||||
if (e.x>=r[0] && e.y>=r[1] &&
|
if (e.x>=r[0] && e.y>=r[1] &&
|
||||||
e.x<r[2] && e.y<r[3]) {
|
e.x<r[2] && e.y<r[3]) {
|
||||||
//print("Press "+key);
|
//print("Press "+key);
|
||||||
|
@ -391,19 +419,26 @@ if (global.BTN4) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
var lastX = 0, lastY = 0;
|
||||||
|
Bangle.on('drag', (e) => {
|
||||||
|
if (!e.b) {
|
||||||
|
if (lastX > 50) { // right
|
||||||
|
drawSpecials();
|
||||||
|
} else if (lastX < -50) { // left
|
||||||
|
drawOperators();
|
||||||
|
} else if (lastY > 50) { // down
|
||||||
|
drawNumbers();
|
||||||
|
} else if (lastY < -50) { // up
|
||||||
|
drawNumbers();
|
||||||
|
}
|
||||||
|
lastX = 0;
|
||||||
|
lastY = 0;
|
||||||
|
} else {
|
||||||
|
lastX = lastX + e.dx;
|
||||||
|
lastY = lastY + e.dy;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// rescale for non-240px screens
|
|
||||||
if (g.getWidth()!=240) {
|
|
||||||
RESULT_HEIGHT = RESULT_HEIGHT*g.getWidth()/240;
|
|
||||||
for (var k in keys) {
|
|
||||||
keys[k].xy = keys[k].xy.map(n => n*g.getWidth()/240);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// draw keys
|
|
||||||
for (var k in keys) {
|
|
||||||
if (keys.hasOwnProperty(k)) {
|
|
||||||
drawKey(k, keys[k], k == selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
displayOutput(0);
|
displayOutput(0);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
0.01: Basic calendar
|
0.01: Basic calendar
|
||||||
0.02: Make Bangle 2 compatible
|
0.02: Make Bangle 2 compatible
|
||||||
0.03: Add setting to start week on Sunday
|
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
|
||||||
|
|
|
@ -9,5 +9,6 @@ Basic calendar
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
- Starts on Sunday: whether the calendar should start on Sunday (default is Monday).
|
- Starts Sunday: whether the calendar should start on Sunday (default is Monday).
|
||||||
|
- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1 @@
|
||||||
require("heatshrink").decompress(
|
require("heatshrink").decompress(atob("mEwwcCpMkyQC3wAIFgIRJn8JAoeQ/gRYwB0Bn57F/gCBHAgfCn8EDgdI/kSAoIR8oBkFgAFCCIysKCPM//4AKZAgR3/0Aj+Ag/ggP4gF/CPpr/Nf5r/NfYRhw4RL8IRDyEAABUJCIYC/AVI="))
|
||||||
atob(
|
|
||||||
"mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA=="
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -10,27 +10,119 @@ const color1 = "#035AA6";
|
||||||
const color2 = "#4192D9";
|
const color2 = "#4192D9";
|
||||||
const color3 = "#026873";
|
const color3 = "#026873";
|
||||||
const color4 = "#038C8C";
|
const color4 = "#038C8C";
|
||||||
const color5 = "#03A696";
|
const gray1 = "#bbbbbb";
|
||||||
const black = "#000000";
|
const black = "#000000";
|
||||||
const white = "#ffffff";
|
const white = "#ffffff";
|
||||||
const gray1 = "#444444";
|
|
||||||
const gray2 = "#888888";
|
|
||||||
const gray3 = "#bbbbbb";
|
|
||||||
const red = "#d41706";
|
const red = "#d41706";
|
||||||
|
const blue = "#0000ff";
|
||||||
|
const yellow = "#ffff00";
|
||||||
|
|
||||||
let settings = require('Storage').readJSON("calendar.json", true) || {};
|
let settings = require('Storage').readJSON("calendar.json", true) || {};
|
||||||
if (settings.startOnSun === undefined)
|
if (settings.startOnSun === undefined)
|
||||||
settings.startOnSun = false;
|
settings.startOnSun = false;
|
||||||
|
if (settings.ndColors === undefined)
|
||||||
|
if (process.env.HWVERSION == 2) {
|
||||||
|
settings.ndColors = true;
|
||||||
|
} else {
|
||||||
|
settings.ndColors = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.ndColors === true) {
|
||||||
|
let bgColor = white;
|
||||||
|
let bgColorMonth = blue;
|
||||||
|
let bgColorDow = black;
|
||||||
|
let bgColorWeekend = yellow;
|
||||||
|
let fgOtherMonth = blue;
|
||||||
|
let fgSameMonth = black;
|
||||||
|
} else {
|
||||||
|
let bgColor = color4;
|
||||||
|
let bgColorMonth = color1;
|
||||||
|
let bgColorDow = color2;
|
||||||
|
let bgColorWeekend = color3;
|
||||||
|
let fgOtherMonth = gray1;
|
||||||
|
let fgSameMonth = white;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDowLbls(locale) {
|
||||||
|
let dowLbls;
|
||||||
|
//TODO: Find some clever way to generate this programmatically from locale lib
|
||||||
|
switch (locale) {
|
||||||
|
case "de_AT":
|
||||||
|
case "de_CH":
|
||||||
|
case "de_DE":
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "nl_NL":
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["zo", "ma", "di", "wo", "do", "vr", "za"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["ma", "di", "wo", "do", "vr", "za", "zo"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "fr_BE":
|
||||||
|
case "fr_CH":
|
||||||
|
case "fr_FR":
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "sv_SE":
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "it_CH":
|
||||||
|
case "it_IT":
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "oc_FR":
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["dg", "dl", "dm", "dc", "dj", "dv", "ds"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["dl", "dm", "dc", "dj", "dv", "ds", "dg"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (settings.startOnSun) {
|
||||||
|
dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
||||||
|
} else {
|
||||||
|
dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return dowLbls;
|
||||||
|
}
|
||||||
|
|
||||||
function drawCalendar(date) {
|
function drawCalendar(date) {
|
||||||
g.setBgColor(color4);
|
g.setBgColor(bgColor);
|
||||||
g.clearRect(0, 0, maxX, maxY);
|
g.clearRect(0, 0, maxX, maxY);
|
||||||
g.setBgColor(color1);
|
g.setBgColor(bgColorMonth);
|
||||||
g.clearRect(0, 0, maxX, headerH);
|
g.clearRect(0, 0, maxX, headerH);
|
||||||
g.setBgColor(color2);
|
if (settings.startOnSun){
|
||||||
|
g.setBgColor(bgColorWeekend);
|
||||||
|
g.clearRect(0, headerH + rowH, colW, maxY);
|
||||||
|
g.setBgColor(bgColorDow);
|
||||||
g.clearRect(0, headerH, maxX, headerH + rowH);
|
g.clearRect(0, headerH, maxX, headerH + rowH);
|
||||||
g.setBgColor(color3);
|
g.setBgColor(bgColorWeekend);
|
||||||
|
g.clearRect(colW * 6, headerH + rowH, maxX, maxY);
|
||||||
|
} else {
|
||||||
|
g.setBgColor(bgColorDow);
|
||||||
|
g.clearRect(0, headerH, maxX, headerH + rowH);
|
||||||
|
g.setBgColor(bgColorWeekend);
|
||||||
g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
|
g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
|
||||||
|
}
|
||||||
for (let y = headerH; y < maxY; y += rowH) {
|
for (let y = headerH; y < maxY; y += rowH) {
|
||||||
g.drawLine(0, y, maxX, y);
|
g.drawLine(0, y, maxX, y);
|
||||||
}
|
}
|
||||||
|
@ -40,24 +132,11 @@ function drawCalendar(date) {
|
||||||
|
|
||||||
const month = date.getMonth();
|
const month = date.getMonth();
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const monthMap = {
|
const localeMonth = require('locale').month(date);
|
||||||
0: "January",
|
|
||||||
1: "February",
|
|
||||||
2: "March",
|
|
||||||
3: "April",
|
|
||||||
4: "May",
|
|
||||||
5: "June",
|
|
||||||
6: "July",
|
|
||||||
7: "August",
|
|
||||||
8: "September",
|
|
||||||
9: "October",
|
|
||||||
10: "November",
|
|
||||||
11: "December"
|
|
||||||
};
|
|
||||||
g.setFontAlign(0, 0);
|
g.setFontAlign(0, 0);
|
||||||
g.setFont("6x8", fontSize);
|
g.setFont("6x8", fontSize);
|
||||||
g.setColor(white);
|
g.setColor(white);
|
||||||
g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2);
|
g.drawString(`${localeMonth} ${year}`, maxX / 2, headerH / 2);
|
||||||
g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true);
|
g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true);
|
||||||
g.drawPoly(
|
g.drawPoly(
|
||||||
[maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10],
|
[maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10],
|
||||||
|
@ -65,12 +144,7 @@ function drawCalendar(date) {
|
||||||
);
|
);
|
||||||
|
|
||||||
g.setFont("6x8", fontSize);
|
g.setFont("6x8", fontSize);
|
||||||
let dowLbls;
|
let dowLbls = getDowLbls(require('locale').name);
|
||||||
if (settings.startOnSun) {
|
|
||||||
dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
||||||
} else {
|
|
||||||
dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
|
||||||
}
|
|
||||||
dowLbls.forEach((lbl, i) => {
|
dowLbls.forEach((lbl, i) => {
|
||||||
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
||||||
});
|
});
|
||||||
|
@ -120,14 +194,19 @@ function drawCalendar(date) {
|
||||||
today.year === year && today.month === month && today.day === day - 50;
|
today.year === year && today.month === month && today.day === day - 50;
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
g.setColor(red);
|
g.setColor(red);
|
||||||
|
let x1 = x * colW;
|
||||||
|
let y1 = y * rowH + headerH + rowH;
|
||||||
|
let x2 = x * colW + colW;
|
||||||
|
let y2 = y * rowH + headerH + rowH + rowH;
|
||||||
|
g.drawRect(x1, y1, x2, y2);
|
||||||
g.drawRect(
|
g.drawRect(
|
||||||
x * colW,
|
x1 + 1,
|
||||||
y * rowH + headerH + rowH,
|
y1 + 1,
|
||||||
x * colW + colW - 1,
|
x2 - 1,
|
||||||
y * rowH + headerH + rowH + rowH
|
y2 - 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
g.setColor(day < 50 ? gray3 : white);
|
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
|
||||||
g.drawString(
|
g.drawString(
|
||||||
(day > 50 ? day - 50 : day).toString(),
|
(day > 50 ? day - 50 : day).toString(),
|
||||||
x * colW + colW / 2,
|
x * colW + colW / 2,
|
||||||
|
|
Before Width: | Height: | Size: 540 B After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -2,7 +2,13 @@
|
||||||
var FILE = "calendar.json";
|
var FILE = "calendar.json";
|
||||||
var settings = require('Storage').readJSON(FILE, true) || {};
|
var settings = require('Storage').readJSON(FILE, true) || {};
|
||||||
if (settings.startOnSun === undefined)
|
if (settings.startOnSun === undefined)
|
||||||
settings.startOnSun = true;
|
settings.startOnSun = false;
|
||||||
|
if (settings.ndColors === undefined)
|
||||||
|
if (process.env.HWVERSION == 2) {
|
||||||
|
settings.ndColors = true;
|
||||||
|
} else {
|
||||||
|
settings.ndColors = false;
|
||||||
|
}
|
||||||
|
|
||||||
function writeSettings() {
|
function writeSettings() {
|
||||||
require('Storage').writeJSON(FILE, settings);
|
require('Storage').writeJSON(FILE, settings);
|
||||||
|
@ -11,7 +17,7 @@
|
||||||
E.showMenu({
|
E.showMenu({
|
||||||
"": { "title": "Calendar" },
|
"": { "title": "Calendar" },
|
||||||
"< Back": () => back(),
|
"< Back": () => back(),
|
||||||
'Start on Sunday': {
|
'Start Sunday': {
|
||||||
value: settings.startOnSun,
|
value: settings.startOnSun,
|
||||||
format: v => v ? "Yes" : "No",
|
format: v => v ? "Yes" : "No",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
|
@ -19,6 +25,14 @@
|
||||||
writeSettings();
|
writeSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'B2 Colors': {
|
||||||
|
value: settings.ndColors,
|
||||||
|
format: v => v ? "Yes" : "No",
|
||||||
|
onchange: v => {
|
||||||
|
settings.ndColors = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
0.03: Display only minutes:seconds when less than 1 hour left
|
0.03: Display only minutes:seconds when less than 1 hour left
|
||||||
0.04: Change to 7 segment font, move to top widget bar
|
0.04: Change to 7 segment font, move to top widget bar
|
||||||
Better auto-update behaviour, less RAM used
|
Better auto-update behaviour, less RAM used
|
||||||
|
0.05: Fix error running app on new firmwares (fix #1140)
|
||||||
|
|
|
@ -36,13 +36,7 @@ E.on('kill', () => {
|
||||||
function showMenu() {
|
function showMenu() {
|
||||||
const timerMenu = {
|
const timerMenu = {
|
||||||
'': {
|
'': {
|
||||||
'title': 'Set timer',
|
'title': 'Set timer'
|
||||||
'predraw': function() {
|
|
||||||
timerMenu.hours.value = settingsChronowid.hours;
|
|
||||||
timerMenu.minutes.value = settingsChronowid.minutes;
|
|
||||||
timerMenu.seconds.value = settingsChronowid.seconds;
|
|
||||||
timerMenu.started.value = settingsChronowid.started;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'< Back' : ()=>{load();},
|
'< Back' : ()=>{load();},
|
||||||
'Reset values': function() {
|
'Reset values': function() {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
0.01: New clock
|
||||||
|
0.02: Fix icon & add battery warn functionality
|
||||||
|
0.03: Theming support & minor fixes
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Circles clock
|
||||||
|
|
||||||
|
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:
|
||||||
|
* Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer))
|
||||||
|
* Heart rate (when screen is on and unlocked)
|
||||||
|
* Battery (including charging and battery low)
|
||||||
|
|
||||||
|
## Screenshot
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## TODO
|
||||||
|
* Show weather information
|
||||||
|
* Configure which information to show in each circle
|
||||||
|
* Configure visibility of widgets
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
Marco ([myxor](https://github.com/myxor))
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwcCIf4ALv///gFCv0Agf+CJP/wAODAwPAEpAjCCIX8h4RMj/+g/8gP4CA4LBDoP/GpkH8EP4/8LIIRMAQIOCCJU/CgQOBEwMPI5ARCR4YRJgP/gB3CI5Z0CCIiABfHRfEj+BAoN+n4FBLIkP/8chwRBx5cC//8v4REhytDgYRCv//8fxEYwRFgfxA4I1FRgI1D+JHE/7FINZzCBAAc4CRU4/kB44FCjgRKLQRlBPQ4RHgYCB/jpBABB6BPoKzBCJYAGuD/vAB1JkgLJm3bAgUCpMnwDdCPwIFChu27dgAoMSCIP+FAQRB+AFBtoRBtgFByQCBRIIoBAocDtonBAQWQdgXAgVIAocDEAUNwEEyEHBYUSoE//gRCsI7BxvACIILDCIcBCIYFCCJ3/wIRCIIYRBI4h6CAoJrDLJYRDDwJ9LAoKhBoMDUIcEgFwUIQREgUBaAcIkhPCAAQzBAAUBdIhhDAAMGCIkAkAFEdAQAFA=="))
|
|
@ -0,0 +1,251 @@
|
||||||
|
const locale = require("locale");
|
||||||
|
const heatshrink = require("heatshrink");
|
||||||
|
|
||||||
|
const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA="));
|
||||||
|
const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
|
||||||
|
const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI"));
|
||||||
|
const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI"));
|
||||||
|
const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA"));
|
||||||
|
|
||||||
|
let settings;
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
settings = require("Storage").readJSON("circlesclock.json", 1) || {
|
||||||
|
'maxHR': 200,
|
||||||
|
'stepGoal': 10000,
|
||||||
|
'batteryWarn': 30
|
||||||
|
};
|
||||||
|
// Load step goal from pedometer widget as fallback
|
||||||
|
if (settings.stepGoal == undefined) {
|
||||||
|
const d = require('Storage').readJSON("wpedom.json", 1) || {};
|
||||||
|
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 w = g.getWidth();
|
||||||
|
const hOffset = 30;
|
||||||
|
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;
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
g.reset();
|
||||||
|
g.setColor(colorBg);
|
||||||
|
g.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
|
// time
|
||||||
|
g.setFont("Vector:50");
|
||||||
|
g.setFontAlign(-1, -1);
|
||||||
|
g.setColor(colorFg);
|
||||||
|
g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8);
|
||||||
|
|
||||||
|
// date & dow
|
||||||
|
g.setFont("Vector:20");
|
||||||
|
g.setFontAlign(-1, 0);
|
||||||
|
g.drawString(locale.date(new Date()), w / 10, h2);
|
||||||
|
g.drawString(locale.dow(new Date()), w / 10, h2 + 22);
|
||||||
|
|
||||||
|
// Steps circle
|
||||||
|
drawSteps();
|
||||||
|
|
||||||
|
// Heart circle
|
||||||
|
drawHeartRate();
|
||||||
|
|
||||||
|
// Battery circle
|
||||||
|
drawBattery();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function drawSteps() {
|
||||||
|
const steps = getSteps();
|
||||||
|
const blue = '#0000ff';
|
||||||
|
g.setColor(colorGrey);
|
||||||
|
g.fillCircle(w1, h3, radiusOuter);
|
||||||
|
|
||||||
|
const stepGoal = settings.stepGoal || 10000;
|
||||||
|
if (stepGoal > 0) {
|
||||||
|
let percent = steps / stepGoal;
|
||||||
|
if (stepGoal < steps) percent = 1;
|
||||||
|
drawGauge(w1, h3, percent, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColor(colorBg);
|
||||||
|
g.fillCircle(w1, h3, radiusInner);
|
||||||
|
|
||||||
|
g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]);
|
||||||
|
|
||||||
|
g.setFont("Vector:12");
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
g.setColor(colorFg);
|
||||||
|
g.drawString(shortValue(steps), w1 + 2, h3);
|
||||||
|
|
||||||
|
g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHeartRate() {
|
||||||
|
g.setColor(colorGrey);
|
||||||
|
g.fillCircle(w2, h3, radiusOuter);
|
||||||
|
|
||||||
|
if (hrtValue != undefined && hrtValue > 0) {
|
||||||
|
const minHR = 40;
|
||||||
|
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
|
||||||
|
drawGauge(w2, h3, percent, colorRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColor(colorBg);
|
||||||
|
g.fillCircle(w2, h3, radiusInner);
|
||||||
|
|
||||||
|
g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]);
|
||||||
|
|
||||||
|
g.setFont("Vector:12");
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
g.setColor(colorFg);
|
||||||
|
g.drawString(hrtValue != undefined ? hrtValue : "-", w2, h3);
|
||||||
|
|
||||||
|
g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBattery() {
|
||||||
|
const battery = E.getBattery();
|
||||||
|
const yellow = '#ffff00';
|
||||||
|
g.setColor(colorGrey);
|
||||||
|
g.fillCircle(w3, h3, radiusOuter);
|
||||||
|
|
||||||
|
if (battery > 0) {
|
||||||
|
const percent = battery / 100;
|
||||||
|
drawGauge(w3, h3, percent, yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColor(colorBg);
|
||||||
|
g.fillCircle(w3, h3, radiusInner);
|
||||||
|
|
||||||
|
g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]);
|
||||||
|
|
||||||
|
g.setFont("Vector:12");
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
|
||||||
|
let icon = powerIcon;
|
||||||
|
let color = colorFg;
|
||||||
|
if (Bangle.isCharging()) {
|
||||||
|
color = colorGreen;
|
||||||
|
icon = powerIconGreen;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) {
|
||||||
|
color = colorRed;
|
||||||
|
icon = powerIconRed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.setColor(color);
|
||||||
|
g.drawString(battery + '%', w3, h3);
|
||||||
|
|
||||||
|
g.drawImage(icon, w3 - 6, h3 + radiusOuter - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function radians(a) {
|
||||||
|
return a * Math.PI / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGauge(cx, cy, percent, color) {
|
||||||
|
let offset = 30;
|
||||||
|
let end = 300;
|
||||||
|
var i = 0;
|
||||||
|
var r = radiusInner + 3;
|
||||||
|
|
||||||
|
if (percent <= 0) return;
|
||||||
|
if (percent > 1) percent = 1;
|
||||||
|
|
||||||
|
var startrot = -offset;
|
||||||
|
var endrot = startrot - ((end - offset) * percent) - 15;
|
||||||
|
|
||||||
|
g.setColor(color);
|
||||||
|
|
||||||
|
const size = 4;
|
||||||
|
// draw gauge
|
||||||
|
for (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortValue(v) {
|
||||||
|
if (isNaN(v)) return '-';
|
||||||
|
if (v <= 999) return v;
|
||||||
|
if (v >= 1000 && v < 10000) {
|
||||||
|
v = Math.floor(v / 100) * 100;
|
||||||
|
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
|
||||||
|
}
|
||||||
|
if (v >= 10000) {
|
||||||
|
v = Math.floor(v / 1000) * 1000;
|
||||||
|
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSteps() {
|
||||||
|
if (WIDGETS.wpedom !== undefined) {
|
||||||
|
return WIDGETS.wpedom.getSteps();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on('lock', function(isLocked) {
|
||||||
|
if (!isLocked) {
|
||||||
|
Bangle.setHRMPower(1, "watch");
|
||||||
|
if (hrtValue == undefined) {
|
||||||
|
hrtValue = '...';
|
||||||
|
drawHeartRate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Bangle.setHRMPower(0, "watch");
|
||||||
|
}
|
||||||
|
drawHeartRate();
|
||||||
|
drawSteps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.on('HRM', function(hrm) {
|
||||||
|
//if(hrm.confidence > 90){
|
||||||
|
hrtValue = hrm.bpm;
|
||||||
|
if (Bangle.isLCDOn())
|
||||||
|
drawHeartRate();
|
||||||
|
//} else {
|
||||||
|
// hrtValue = undefined;
|
||||||
|
//}
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.on('charging', function(charging) {
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadSettings();
|
||||||
|
setInterval(draw, 60000);
|
||||||
|
draw();
|
||||||
|
Bangle.setUI("clock");
|
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,43 @@
|
||||||
|
(function(back) {
|
||||||
|
const SETTINGS_FILE = "circlesclock.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': 'circlesclock' },
|
||||||
|
'max heartrate': {
|
||||||
|
value: "maxHR" in settings ? settings.maxHR : 200,
|
||||||
|
min: 20,
|
||||||
|
max : 250,
|
||||||
|
step: 10,
|
||||||
|
format: x => {
|
||||||
|
return x;
|
||||||
|
},
|
||||||
|
onchange: x => save('maxHR', x),
|
||||||
|
},
|
||||||
|
'step goal': {
|
||||||
|
value: "stepGoal" in settings ? settings.stepGoal : 10000,
|
||||||
|
min: 2000,
|
||||||
|
max : 50000,
|
||||||
|
step: 2000,
|
||||||
|
format: x => {
|
||||||
|
return x;
|
||||||
|
},
|
||||||
|
onchange: x => save('stepGoal', x),
|
||||||
|
},
|
||||||
|
'battery warn lvl': {
|
||||||
|
value: "batteryWarn" in settings ? settings.batteryWarn : 30,
|
||||||
|
min: 10,
|
||||||
|
max : 100,
|
||||||
|
step: 10,
|
||||||
|
format: x => {
|
||||||
|
return x + '%';
|
||||||
|
},
|
||||||
|
onchange: x => save('batteryWarn', x),
|
||||||
|
},
|
||||||
|
'< Back': back,
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New clock!
|
0.01: New clock!
|
||||||
0.02: Load steps from Health Tracking app (if installed)
|
0.02: Load steps from Health Tracking app (if installed)
|
||||||
|
0.03: ...
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
Command line styled clock with lots of information:
|
Command line styled clock with lots of information:
|
||||||
|
|
||||||
It can show the following (depending on availability) information:
|
It can show the following (depending on availability) information:
|
||||||
|
* Time data:
|
||||||
* Time
|
* Time
|
||||||
* Day of week
|
* Day of week
|
||||||
* Date
|
* Date
|
||||||
|
* Additional information (can be toggled via settings):
|
||||||
* Weather conditions and temperature (requires app [Weather](https://banglejs.com/apps/#weather))
|
* Weather conditions and temperature (requires app [Weather](https://banglejs.com/apps/#weather))
|
||||||
* Steps (requires app [Health Tracking](https://banglejs.com/apps/#health%20tracking) or a step widget)
|
* Steps (requires app [Health Tracking](https://banglejs.com/apps/#health%20tracking) or a step widget)
|
||||||
* Heart rate (when screen is on and unlocked)
|
* Heart rate (when screen is on and unlocked)
|
||||||
|
|
|
@ -1,31 +1,60 @@
|
||||||
const storage = require('Storage');
|
const storage = require('Storage');
|
||||||
const locale = require("locale");
|
const locale = require("locale");
|
||||||
|
|
||||||
const font = "12x20";
|
const font12 = g.getFonts().includes("12x20");
|
||||||
const fontsize = 1;
|
const font = font12 ? "12x20" : "6x8";
|
||||||
|
const fontsize = font12 ? 1: 2;
|
||||||
const fontheight = 19;
|
const fontheight = 19;
|
||||||
|
|
||||||
const marginTop = 10;
|
const marginTop = 5;
|
||||||
const marginLeftTopic = 3; // margin of topics
|
const marginLeftTopic = 3; // margin of topics
|
||||||
const marginLeftData = 68; // margin of data values
|
const marginLeftData = font12 ? 64 : 75; // margin of data values
|
||||||
|
|
||||||
const topicColor = g.theme.dark ? "#fff" : "#000";
|
const topicColor = g.theme.dark ? "#fff" : "#000";
|
||||||
const textColor = g.theme.dark ? "#0f0" : "#080";
|
const textColor = g.theme.dark ? "#0f0" : "#080";
|
||||||
|
const textColorRed = g.theme.dark ? "#FF0000" : "#FF0000";
|
||||||
|
|
||||||
let hrtValue;
|
let hrtValue;
|
||||||
let hrtValueIsOld = false;
|
let hrtValueIsOld = false;
|
||||||
|
|
||||||
let localTempValue;
|
let localTempValue;
|
||||||
let weatherTempString;
|
let weatherTempString;
|
||||||
let lastHeartRateRowIndex;
|
let lastHeartRateRowIndex;
|
||||||
|
let lastStepsRowIndex;
|
||||||
|
let i = 2;
|
||||||
|
|
||||||
|
let settings;
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
settings = storage.readJSON('clicompleteclk.json', 1) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setting(key) {
|
||||||
|
if (!settings) { loadSettings(); }
|
||||||
|
const DEFAULTS = {
|
||||||
|
'battery': true,
|
||||||
|
'batteryLvl': 30,
|
||||||
|
'weather': true,
|
||||||
|
'steps': true,
|
||||||
|
'heartrate': true
|
||||||
|
};
|
||||||
|
return (key in settings) ? settings[key] : DEFAULTS[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let showBattery = setting('battery');
|
||||||
|
let batteryWarnLevel = setting('batteryLvl');
|
||||||
|
let showWeather = setting('weather');
|
||||||
|
let showSteps = setting('steps');
|
||||||
|
let showHeartRate = setting('heartrate');
|
||||||
|
|
||||||
|
|
||||||
// timeout used to update every minute
|
|
||||||
var drawTimeout;
|
var drawTimeout;
|
||||||
// schedule a draw for the next minute
|
|
||||||
function queueDraw() {
|
function queueDraw() {
|
||||||
if (drawTimeout) clearTimeout(drawTimeout);
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
drawTimeout = setTimeout(function() {
|
drawTimeout = setTimeout(function() {
|
||||||
drawTimeout = undefined;
|
drawTimeout = undefined;
|
||||||
drawAll(false);
|
drawAll(true);
|
||||||
}, 60000 - (Date.now() % 60000));
|
}, 60000 - (Date.now() % 60000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,15 +71,13 @@ function updateTime(now){
|
||||||
if (!Bangle.isLCDOn()) return;
|
if (!Bangle.isLCDOn()) return;
|
||||||
writeLineTopic("TIME", 1);
|
writeLineTopic("TIME", 1);
|
||||||
writeLine(locale.time(now,1),1);
|
writeLine(locale.time(now,1),1);
|
||||||
if(now.getMinutes() == 0)
|
|
||||||
drawInfo(now);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawInfo(now) {
|
function drawInfo(now) {
|
||||||
if (now == undefined)
|
if (now == undefined)
|
||||||
now = new Date();
|
now = new Date();
|
||||||
|
|
||||||
let i = 2;
|
i = 2;
|
||||||
|
|
||||||
writeLineTopic("DOWK", i);
|
writeLineTopic("DOWK", i);
|
||||||
writeLine(locale.dow(now),i);
|
writeLine(locale.dow(now),i);
|
||||||
|
@ -60,14 +87,28 @@ function drawInfo(now) {
|
||||||
writeLine(locale.date(now,1),i);
|
writeLine(locale.date(now,1),i);
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
/*
|
if (showBattery) {
|
||||||
writeLineTopic("BAT", i);
|
writeLineTopic("BATT", i);
|
||||||
const b = E.getBattery();
|
const b = E.getBattery();
|
||||||
writeLine(b + "%", i); // TODO make bars
|
writeLine(b + "%", i, b < batteryWarnLevel ? textColorRed : textColor);
|
||||||
i++;
|
i++;
|
||||||
*/
|
}
|
||||||
|
|
||||||
// weather
|
if (showWeather) {
|
||||||
|
drawWeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSteps) {
|
||||||
|
drawSteps(i);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHeartRate) {
|
||||||
|
drawHeartRate(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWeather() {
|
||||||
const weatherJson = getWeather();
|
const weatherJson = getWeather();
|
||||||
if(weatherJson && weatherJson.weather){
|
if(weatherJson && weatherJson.weather){
|
||||||
const currentWeather = weatherJson.weather;
|
const currentWeather = weatherJson.weather;
|
||||||
|
@ -82,19 +123,22 @@ function drawInfo(now) {
|
||||||
writeLine(weatherTempValue,i);
|
writeLine(weatherTempValue,i);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// steps
|
function drawSteps(i) {
|
||||||
|
if (!showSteps) return;
|
||||||
|
if (i == undefined)
|
||||||
|
i = lastStepsRowIndex;
|
||||||
const steps = getSteps();
|
const steps = getSteps();
|
||||||
if (steps != undefined) {
|
if (steps != undefined) {
|
||||||
writeLineTopic("STEP", i);
|
writeLineTopic("STEP", i);
|
||||||
writeLine(steps, i);
|
writeLine(steps, i);
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
lastStepsRowIndex = i;
|
||||||
drawHeartRate(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawHeartRate(i) {
|
function drawHeartRate(i) {
|
||||||
|
if (!showHeartRate) return;
|
||||||
if (i == undefined)
|
if (i == undefined)
|
||||||
i = lastHeartRateRowIndex;
|
i = lastHeartRateRowIndex;
|
||||||
writeLineTopic("HRTM", i);
|
writeLineTopic("HRTM", i);
|
||||||
|
@ -155,15 +199,21 @@ function getWeather() {
|
||||||
// turn on HRM when the LCD is unlocked
|
// turn on HRM when the LCD is unlocked
|
||||||
Bangle.on('lock', function(isLocked) {
|
Bangle.on('lock', function(isLocked) {
|
||||||
if (!isLocked) {
|
if (!isLocked) {
|
||||||
|
if (showHeartRate) {
|
||||||
Bangle.setHRMPower(1,"clicompleteclk");
|
Bangle.setHRMPower(1,"clicompleteclk");
|
||||||
if (hrtValue == undefined)
|
if (hrtValue == undefined)
|
||||||
hrtValue = "...";
|
hrtValue = "...";
|
||||||
else
|
else
|
||||||
hrtValueIsOld = true;
|
hrtValueIsOld = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (showHeartRate) {
|
||||||
hrtValueIsOld = true;
|
hrtValueIsOld = true;
|
||||||
Bangle.setHRMPower(0,"clicompleteclk");
|
Bangle.setHRMPower(0,"clicompleteclk");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Update steps and heart rate
|
||||||
|
drawSteps();
|
||||||
drawHeartRate();
|
drawHeartRate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -171,12 +221,15 @@ Bangle.on('lcdPower',function(on) {
|
||||||
if (on) {
|
if (on) {
|
||||||
drawAll(true);
|
drawAll(true);
|
||||||
} else {
|
} else {
|
||||||
|
if (showHeartRate) {
|
||||||
hrtValueIsOld = true;
|
hrtValueIsOld = true;
|
||||||
|
}
|
||||||
if (drawTimeout) clearTimeout(drawTimeout);
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
drawTimeout = undefined;
|
drawTimeout = undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (showHeartRate) {
|
||||||
Bangle.on('HRM', function(hrm) {
|
Bangle.on('HRM', function(hrm) {
|
||||||
//if(hrm.confidence > 90){
|
//if(hrm.confidence > 90){
|
||||||
hrtValueIsOld = false;
|
hrtValueIsOld = false;
|
||||||
|
@ -187,9 +240,11 @@ Bangle.on('HRM', function(hrm) {
|
||||||
// hrtValue = undefined;
|
// hrtValue = undefined;
|
||||||
//}
|
//}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
g.clear();
|
g.clear();
|
||||||
Bangle.setUI("clock");
|
Bangle.setUI("clock");
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
loadSettings();
|
||||||
drawAll(true);
|
drawAll(true);
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
(function(back) {
|
||||||
|
const storage = require('Storage');
|
||||||
|
let settings = storage.readJSON('clicompleteclk.json', 1) || {};
|
||||||
|
function save(key, value) {
|
||||||
|
settings[key] = value;
|
||||||
|
storage.write('clicompleteclk.json', settings);
|
||||||
|
}
|
||||||
|
E.showMenu({
|
||||||
|
'': { 'title': 'CLI complete clk' },
|
||||||
|
'Show battery': {
|
||||||
|
value: "battery" in settings ? settings.battery : false,
|
||||||
|
format: () => (settings.battery ? 'Yes' : 'No'),
|
||||||
|
onchange: () => {
|
||||||
|
settings.battery = !settings.battery;
|
||||||
|
save('battery', settings.battery);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Battery warn': {
|
||||||
|
value: "batteryLvl" in settings ? settings.batteryLvl : 30,
|
||||||
|
min: 0,
|
||||||
|
max : 100,
|
||||||
|
step: 10,
|
||||||
|
format: x => {
|
||||||
|
return x + "%";
|
||||||
|
},
|
||||||
|
onchange: x => save('batteryLvl', x),
|
||||||
|
},
|
||||||
|
'Show weather': {
|
||||||
|
value: "weather" in settings ? settings.weather : false,
|
||||||
|
format: () => (settings.weather ? 'Yes' : 'No'),
|
||||||
|
onchange: () => {
|
||||||
|
settings.weather = !settings.weather;
|
||||||
|
save('weather', settings.weather);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Show steps': {
|
||||||
|
value: "steps" in settings ? settings.steps : false,
|
||||||
|
format: () => (settings.steps ? 'Yes' : 'No'),
|
||||||
|
onchange: () => {
|
||||||
|
settings.steps = !settings.steps;
|
||||||
|
save('steps', settings.steps);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Show heartrate': {
|
||||||
|
value: "heartrate" in settings ? settings.heartrate : false,
|
||||||
|
format: () => (settings.heartrate ? 'Yes' : 'No'),
|
||||||
|
onchange: () => {
|
||||||
|
settings.heartrate = !settings.heartrate;
|
||||||
|
save('heartrate', settings.heartrate);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'< Back': back,
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Colorful Analog Clock #
|
||||||
|
|
||||||
|
This app displays an analog clock with a colorful face. It considers the
|
||||||
|
currently configured "theme" (and may therefore look different than shown in
|
||||||
|
the screenshot on your watch depending on which theme you prefer).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwgZC/AB0BkmCCBsShEAiFBkARLwEBkGSgEECBQdBCIMSAwMYCBEKCgeQgE2gFgCA0C6Moiw1Dk0AhoRGikACIIHDgzECCI5ECg/gEYOACI+ggMti2ACIUkCIImCABARCAAMNCYIADgu0hdACI1twHACQm0rdoCI0BEYsoilRCI9sUgkBoG2rY1JgYjDCINLCI4fCa5ARHAAggCfYIjLgUB0AECCIy7BFwUCR4IKChIRFm1ACJAjGgwRL+AFDiwREI4YABn41FI4hxFn6IJPoh1B/AQFUI4ABh4RGUIsEyARC4ALEwAjECIl/CIkECIsICId+EQkkwEIA4gRDAAojBLwwHFexAADhaFDgETBw6UChdgA4cbCIKuGggCBCIMDCIkQCI8BEwMbCgMSAQIRGgGQQoQRCEYJrGAAMGIgZKDmBzIjARFTwpuHAARoGAAsMwQVCzARLAAPbtq5KAH4AEA"))
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 3.1 KiB |
|
@ -0,0 +1,247 @@
|
||||||
|
let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2;
|
||||||
|
let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2;
|
||||||
|
|
||||||
|
let outerRadius = Math.min(CenterX,CenterY) * 0.9;
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
|
||||||
|
/**** updateClockFaceSize ****/
|
||||||
|
|
||||||
|
function updateClockFaceSize () {
|
||||||
|
CenterX = ScreenWidth/2;
|
||||||
|
CenterY = ScreenHeight/2;
|
||||||
|
|
||||||
|
outerRadius = Math.min(CenterX,CenterY) * 0.9;
|
||||||
|
|
||||||
|
if (global.WIDGETS == null) { return; }
|
||||||
|
|
||||||
|
let WidgetLayouts = {
|
||||||
|
tl:{ x:0, y:0, Direction:0 },
|
||||||
|
tr:{ x:ScreenWidth-1, y:0, Direction:1 },
|
||||||
|
bl:{ x:0, y:ScreenHeight-24, Direction:0 },
|
||||||
|
br:{ x:ScreenWidth-1, y:ScreenHeight-24, Direction:1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let Widget of WIDGETS) {
|
||||||
|
let WidgetLayout = WidgetLayouts[Widget.area]; // reference, not copy!
|
||||||
|
if (WidgetLayout == null) { continue; }
|
||||||
|
|
||||||
|
Widget.x = WidgetLayout.x - WidgetLayout.Direction * Widget.width;
|
||||||
|
Widget.y = WidgetLayout.y;
|
||||||
|
|
||||||
|
WidgetLayout.x += Widget.width * (1-2*WidgetLayout.Direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x,y, dx,dy;
|
||||||
|
let cx = CenterX, cy = CenterY, r = outerRadius, r2 = r*r;
|
||||||
|
|
||||||
|
x = WidgetLayouts.tl.x; y = WidgetLayouts.tl.y+24; dx = x - cx; dy = y - cy;
|
||||||
|
if (dx*dx + dy*dy < r2) {
|
||||||
|
cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
x = WidgetLayouts.tr.x; y = WidgetLayouts.tr.y+24; dx = x - cx; dy = y - cy;
|
||||||
|
if (dx*dx + dy*dy < r2) {
|
||||||
|
cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
x = WidgetLayouts.bl.x; y = WidgetLayouts.bl.y; dx = x - cx; dy = y - cy;
|
||||||
|
if (dx*dx + dy*dy < r2) {
|
||||||
|
cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
x = WidgetLayouts.br.x; y = WidgetLayouts.br.y; dx = x - cx; dy = y - cy;
|
||||||
|
if (dx*dx + dy*dy < r2) {
|
||||||
|
cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
CenterX = cx; CenterY = cy; outerRadius = r * 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClockFaceSize();
|
||||||
|
|
||||||
|
/**** custom version of Bangle.drawWidgets (does not clear the widget areas) ****/
|
||||||
|
|
||||||
|
Bangle.drawWidgets = function () {
|
||||||
|
var w = g.getWidth(), h = g.getHeight();
|
||||||
|
|
||||||
|
var pos = {
|
||||||
|
tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left
|
||||||
|
tr:{x:w-1, y:0, r:1, c:0},
|
||||||
|
bl:{x:0, y:h-24, r:0, c:0},
|
||||||
|
br:{x:w-1, y:h-24, r:1, c:0}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (global.WIDGETS) {
|
||||||
|
for (var wd of WIDGETS) {
|
||||||
|
var p = pos[wd.area];
|
||||||
|
if (!p) continue;
|
||||||
|
|
||||||
|
wd.x = p.x - p.r*wd.width;
|
||||||
|
wd.y = p.y;
|
||||||
|
|
||||||
|
p.x += wd.width*(1-2*p.r);
|
||||||
|
p.c++;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.reset(); // also loads the current theme
|
||||||
|
|
||||||
|
if (pos.tl.c || pos.tr.c) {
|
||||||
|
g.setClipRect(0,h-24,w-1,h-1);
|
||||||
|
g.reset(); // also (re)loads the current theme
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.bl.c || pos.br.c) {
|
||||||
|
g.setClipRect(0,h-24,w-1,h-1);
|
||||||
|
g.reset(); // also (re)loads the current theme
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (wd of WIDGETS) {
|
||||||
|
g.clearRect(wd.x,wd.y, wd.x+wd.width-1,23);
|
||||||
|
wd.draw(wd);
|
||||||
|
}
|
||||||
|
} catch (e) { print(e); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let innerRadius = Math.min(CenterX,CenterY) * 0.8 - 14;
|
||||||
|
|
||||||
|
let HourHandLength = outerRadius * 0.5;
|
||||||
|
let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2;
|
||||||
|
|
||||||
|
let MinuteHandLength = outerRadius * 0.7;
|
||||||
|
let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2;
|
||||||
|
|
||||||
|
let SecondHandLength = outerRadius * 0.9;
|
||||||
|
let SecondHandOffset = 6;
|
||||||
|
|
||||||
|
let twoPi = 2*Math.PI;
|
||||||
|
let Pi = Math.PI;
|
||||||
|
let halfPi = Math.PI/2;
|
||||||
|
|
||||||
|
let sin = Math.sin, cos = Math.cos;
|
||||||
|
|
||||||
|
let HourHandPolygon = [
|
||||||
|
-halfHourHandWidth,halfHourHandWidth,
|
||||||
|
-halfHourHandWidth,halfHourHandWidth-HourHandLength,
|
||||||
|
halfHourHandWidth,halfHourHandWidth-HourHandLength,
|
||||||
|
halfHourHandWidth,halfHourHandWidth,
|
||||||
|
];
|
||||||
|
|
||||||
|
let MinuteHandPolygon = [
|
||||||
|
-halfMinuteHandWidth,halfMinuteHandWidth,
|
||||||
|
-halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
|
||||||
|
halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
|
||||||
|
halfMinuteHandWidth,halfMinuteHandWidth,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**** drawClockFace ****/
|
||||||
|
|
||||||
|
function drawClockFace () {
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
let Phi = i * twoPi/60;
|
||||||
|
|
||||||
|
let x = CenterX + outerRadius * sin(Phi);
|
||||||
|
let y = CenterY - outerRadius * cos(Phi);
|
||||||
|
|
||||||
|
let Color = E.HSBtoRGB(i/60,1,1, true);
|
||||||
|
g.setColor(Color[0]/255,Color[1]/255,Color[2]/255);
|
||||||
|
|
||||||
|
g.fillCircle(x,y, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setFont('Vector', 20);
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
let Phi = i * twoPi/12;
|
||||||
|
|
||||||
|
let Radius = innerRadius;
|
||||||
|
if (i >= 10) { Radius -= 4; }
|
||||||
|
|
||||||
|
let x = CenterX + Radius * sin(Phi);
|
||||||
|
let y = CenterY - Radius * cos(Phi);
|
||||||
|
|
||||||
|
let Color = E.HSBtoRGB(i/12,1,1, true);
|
||||||
|
g.setColor(Color[0]/255,Color[1]/255,Color[2]/255);
|
||||||
|
|
||||||
|
g.drawString(i == 0 ? '12' : '' + i, x,y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**** transforme polygon ****/
|
||||||
|
|
||||||
|
let transformedPolygon = new Array(HourHandPolygon.length);
|
||||||
|
|
||||||
|
function transformPolygon (originalPolygon, OriginX,OriginY, Phi) {
|
||||||
|
let sPhi = sin(Phi), cPhi = cos(Phi), x,y;
|
||||||
|
|
||||||
|
for (let i = 0, l = originalPolygon.length; i < l; i+=2) {
|
||||||
|
x = originalPolygon[i];
|
||||||
|
y = originalPolygon[i+1];
|
||||||
|
|
||||||
|
transformedPolygon[i] = OriginX + x*cPhi + y*sPhi;
|
||||||
|
transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**** draw clock hands ****/
|
||||||
|
|
||||||
|
function drawClockHands () {
|
||||||
|
let now = new Date();
|
||||||
|
|
||||||
|
let Hours = now.getHours() % 12;
|
||||||
|
let Minutes = now.getMinutes();
|
||||||
|
let Seconds = now.getSeconds();
|
||||||
|
|
||||||
|
let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi;
|
||||||
|
let MinutesAngle = (Minutes/60) * twoPi - Pi;
|
||||||
|
let SecondsAngle = (Seconds/60) * twoPi - Pi;
|
||||||
|
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
|
||||||
|
transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle);
|
||||||
|
g.fillPoly(transformedPolygon);
|
||||||
|
|
||||||
|
transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle);
|
||||||
|
g.fillPoly(transformedPolygon);
|
||||||
|
|
||||||
|
let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle);
|
||||||
|
|
||||||
|
g.setColor(g.theme.fg2);
|
||||||
|
g.drawLine(
|
||||||
|
CenterX + SecondHandOffset*sPhi,
|
||||||
|
CenterY - SecondHandOffset*cPhi,
|
||||||
|
CenterX - SecondHandLength*sPhi,
|
||||||
|
CenterY + SecondHandLength*cPhi
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**** refreshDisplay ****/
|
||||||
|
|
||||||
|
let Timer;
|
||||||
|
function refreshDisplay () {
|
||||||
|
g.clear(true); // also loads current theme
|
||||||
|
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
drawClockFace();
|
||||||
|
drawClockHands();
|
||||||
|
|
||||||
|
let Pause = 1000 - (Date.now() % 1000);
|
||||||
|
Timer = setTimeout(refreshDisplay,Pause);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(refreshDisplay, 500); // enqueue first draw request
|
||||||
|
|
||||||
|
Bangle.on('lcdPower', (on) => {
|
||||||
|
if (on) {
|
||||||
|
if (Timer != null) { clearTimeout(Timer); Timer = undefined; }
|
||||||
|
refreshDisplay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
|
||||||
|
Bangle.setUI('clock');
|
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.0 KiB |
|
@ -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.
|
|
@ -0,0 +1,12 @@
|
||||||
|
# ColorWheel #
|
||||||
|
|
||||||
|
Choosing the right color on a Bangle.js 2 is not always easy. This little app therefore displays a wheel of rather good looking colors and reveals the associated color code by tapping on it
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Please note: you may also tap outside the wheel (for black) or inside it (for white).
|
||||||
|
|
||||||
|
## License ##
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwgYtr4cEiAQMku27ckyVICBUDlmy5YRBpMkCBEE7dtEYYRBwARHm3LEY3QCA0BEAIjFk3boARFhoOBEYs06dNCIogCEYoHCNAoOCEYlNEYUgCIcbEZekCIYODtgjHmgRHtu3///yQrESQfTBIYQBAAPNEYU2SQUGEYd/CIf9EYYRCDAYRF/4KBCIioBAwPfCAn/+wKC7QjEmgRG/xADZAIyBAwIQFCIgjFmoRKEYL4DRgQAFGoojKCIoje7Nly1ZEYLzCkojLNYIRCNZAjIkm/EZ4RH/1ZEYYRMWYpZDy4jJrARBggRBlrYG+VJEYgRBDIVfCIgtCy1QCIZhDCAfkKIW24ARBgJeBEYNbvoQBvOkCIQjDgE2EYYCD2gRCyQQCgEGEYYRBzVp0wRCyAREEY+2CIWAEY4OCEYoQDAAMbEY/SpMgCAkCjIjHzVJEQoABWYIjF7VICI8BBwYjDe4IAHSQ3QCBBuBLgQjCCBIAChu26dMCBgAdA"))
|
|
@ -0,0 +1,80 @@
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
//-- ColorWheel - draws a "wheel" of good looking colors --//
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
|
||||||
|
let ColorList = [
|
||||||
|
'#0000FF', '#8000FF', '#FF00FF', '#FF0080', '#FF0000', '#FF8000',
|
||||||
|
'#FFFF00', '#80FF00', '#00FF00', '#00FF80', '#00FFFF', '#0080FF'
|
||||||
|
];
|
||||||
|
|
||||||
|
let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2;
|
||||||
|
let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2;
|
||||||
|
|
||||||
|
let outerRadius = Math.min(CenterX,CenterY) * 0.9;
|
||||||
|
let innerRadius = outerRadius*0.5;
|
||||||
|
|
||||||
|
let sin = Math.sin, cos = Math.cos;
|
||||||
|
let twoPi = 2*Math.PI, halfPi = Math.PI/2;
|
||||||
|
|
||||||
|
let DeltaPhi = twoPi/72;
|
||||||
|
let Epsilon = 0.001;
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
|
||||||
|
g.setColor(0,0,0);
|
||||||
|
g.fillRect(0,0, ScreenWidth,ScreenHeight);
|
||||||
|
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
let Phi0 = i * twoPi/12, Phi1 = (i+1) * twoPi/12;
|
||||||
|
|
||||||
|
let Polygon = [];
|
||||||
|
for (let Phi = Phi0; Phi <= Phi1+Epsilon; Phi += DeltaPhi) {
|
||||||
|
Polygon.push(CenterX + outerRadius * sin(Phi));
|
||||||
|
Polygon.push(CenterY - outerRadius * cos(Phi));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let Phi = Phi1; Phi >= Phi0-Epsilon; Phi -= DeltaPhi) {
|
||||||
|
Polygon.push(CenterX + innerRadius * sin(Phi));
|
||||||
|
Polygon.push(CenterY - innerRadius * cos(Phi));
|
||||||
|
}
|
||||||
|
g.setColor(ColorList[i]);
|
||||||
|
g.fillPoly(Polygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
g.fillCircle(CenterX,CenterY, innerRadius);
|
||||||
|
|
||||||
|
g.setFont12x20();
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setColor(0,0,0);
|
||||||
|
|
||||||
|
g.drawString('Tap', CenterX,CenterY-20);
|
||||||
|
g.drawString('on a', CenterX,CenterY);
|
||||||
|
g.drawString('Color', CenterX,CenterY+20);
|
||||||
|
|
||||||
|
Bangle.on('touch', function (Button,Position) {
|
||||||
|
Bangle.buzz();
|
||||||
|
|
||||||
|
let dx = Position.x - CenterX;
|
||||||
|
let dy = Position.y - CenterY;
|
||||||
|
|
||||||
|
let Radius = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
|
||||||
|
let Color;
|
||||||
|
switch (true) {
|
||||||
|
case (Radius > outerRadius): Color = '#000000'; break;
|
||||||
|
case (Radius < innerRadius): Color = '#FFFFFF'; break;
|
||||||
|
default:
|
||||||
|
let Phi = Math.atan2(dy,dx) + halfPi;
|
||||||
|
if (Phi < 0) { Phi += twoPi; }
|
||||||
|
if (Phi > twoPi) { Phi -= twoPi; }
|
||||||
|
|
||||||
|
let Index = Math.floor(12*Phi/twoPi);
|
||||||
|
Color = ColorList[Index];
|
||||||
|
}
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
g.fillCircle(CenterX,CenterY, innerRadius);
|
||||||
|
|
||||||
|
g.setColor(0,0,0);
|
||||||
|
g.drawString(Color, CenterX,CenterY);
|
||||||
|
});
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwgJC/ABsH4/wv/H/EMlkMsF4hkYmEEwEwg0gmHCwEh4VAmPi/0j8Vkkcj4MjkU8kckocx4UEmPMoUQgkEEYNGnAFBnEGxFwg0Ek/jzFh8UEkEjkOikUcnFH8MiFIM3wnA8PisEwhnAkECAoMc4EYgk///3//n/Cl/AFYA="))
|
|
@ -0,0 +1,54 @@
|
||||||
|
const digits = [
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVqlVVVVVVVVVVVVVaqqqqpVVVVVVVVVVWqqqqqqqVVVVVVVVVWqqqAKqqpVVVVVVVVaqgAAAACqpVVVVVVVaqAAAAAACqlVVVVVVaoAAAAAAACqVVVVVVaoAAAAAAAAKpVVVVVaoAAAAAAAAAqlVVVVaoAAAAAAAAACqVVVVWoAAAAAAAAAAKlVVVWoAAAAAAAAAAAqVVVWqAAAAAAAAAAAKpVVVqAAAAACgAAAAAqVVVagAAAAKqgAAAAKlVVagAAAAqqqgAAAAqVVWoAAAAKpaoAAAAKlVVqAAAAKlVagAAAAqVVqAAAACpVWoAAAAKlVagAAACpVVagAAACpVWoAAAAqVVWoAAAAqVVqAAAAqVVVagAAAKlVqAAAAKlVVWoAAAAqVagAAACpVVVqAAAAKlWoAAAAqVVVagAAACpVqAAAAKlVVWoAAAAqVagAAACpVVVqAAAAKlWoAAAAqVVVWoAAACpVqAAAAKlVVVqAAAAqVagAAAKlVVVagAAAKlWoAAACpVVVWoAAACpVqAAAAqVVVVqAAAAqVagAAAKlVVVagAAAKlWoAAACpVVVWoAAACpVqAAAAqVVVVqAAAAqVagAAACpVVVagAAAKlWoAAAAqVVVWoAAACpVqAAAAKlVVWoAAAAqVagAAACpVVVqAAAAKlWoAAAAqVVVagAAACpVqAAAAKlVVWoAAAAqVagAAACpVVVqAAAAKlVqAAAAqVVVagAAACpVagAAACpVVWoAAACpVWoAAAAqVVWoAAAAqVVqAAAAKlVVqAAAAKlVagAAAAqVVqAAAAKlVVqAAAAKpVqgAAACpVVagAAAAqqqgAAAAqVVVqAAAACqqgAAAAKlVVagAAAACqAAAAAKlVVWoAAAAAAAAAAACpVVVagAAAAAAAAAACpVVVWqAAAAAAAAAACqVVVVagAAAAAAAAAAqVVVVVqAAAAAAAAAAqVVVVVaoAAAAAAAAAqlVVVVVqgAAAAAAAAqlVVVVVWqgAAAAAAAqlVVVVVVaqAAAAAACqlVVVVVVVaqgAAAAqqlVVVVVVVVqqqqqqqqVVVVVVVVVVqqqqqqpVVVVVVVVVVVaqqqqVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVVVVVVVVVVVVVVVVVVaqqqVVVVVVVVVVVVVqqqqqVVVVVVVVVVVWqqqqqpVVVVVVVVVVaqgAAAqVVVVVVVVVVaqAAAACpVVVVVVVVVqoAAAAAqVVVVVVVVVqoAAAAAKlVVVVVVVWqgAAAAACpVVVVVVVaqgAAAAAAqVVVVVVVaqAAAAAAAKlVVVVVVaoAAAAAAACpVVVVVVqoAAAAAAAAqVVVVVVaoAAAAAAAAKlVVVVVagAAAAAAAACpVVVVVWoAAAAAAAAAqVVVVVVqAAAAAAAAAKlVVVVVagAAAAAAAACpVVVVVWoAAAAAAAAAqVVVVVVqAAAAAAAAAKlVVVVVagAAAgAAAACpVVVVVWoAACogAAAAqVVVVVVagAKqoAAAAKlVVVVVWqqqqagAAACpVVVVVVaqqpWoAAAAqVVVVVVVaqlVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVaqqqAAAAAqqqpVVVWqqqqgAAAAKqqqpVVWqqqqAAAAAAKqqqlVVqgAAAAAAAAAAACpVVqAAAAAAAAAAAAAKlVagAAAAAAAAAAAACpVWoAAAAAAAAAAAAAqVWoAAAAAAAAAAAAAKlVagAAAAAAAAAAAACpVWoAAAAAAAAAAAAAqVVqAAAAAAAAAAAAAKlVagAAAAAAAAAAAACpVWqAAAAAAAAAAAACqVVaqqqqqqqqqqqqqqVVVqqqqqqqqqqqqqqVVVWqqqqqqqqqqqqqVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVWqVVVVVVVVVVVVVaqqqqqlVVVVVVVVVaqqqqqqqqVVVVVVVVqqqqoAqqqqVVVVVVWqqgAAAAAKqpVVVVVaqgAAAAAAAAqpVVVVaqAAAAAAAAACqlVVVWoAAAAAAAAAACpVVVWoAAAAAAAAAAAKlVVVqAAAAAAAAAAACqVVVagAAAAAAAAAAAKlVVWoAAAAAAAAAAAAqVVVqAAAAAAAAAAAAKlVVagAAAAAAAAAAAAqVVWoAAAKqqAAAAAAKlVVagACqqqqAAAAACpVVWqqqqqqqqAAAAAqVVVaqqqlVVqgAAAAKlVVVaqpVVVVqAAAACpVVVVVVVVVVagAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVqAAAACpVVVVVVVVVVagAAAAqVVVVVVVVVVagAAAAqVVVVVVVVVVaoAAAAKlVVVVVVVVVaoAAAACpVVVVVVVVVaoAAAACpVVVVVVVVVWoAAAACqVVVVVVVVVWoAAAAAqVVVVVVVVVWqAAAAAqVVVVVVVVVWqAAAAAqlVVVVVVVVWqAAAAAKlVVVVVVVVWqAAAAAKlVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAKpVVVVVVVVWqAAAAAqpVVVVVVVVWqAAAAACpVVVVVVVVWqAAAAAKlVVVVVVVVVqAAAAAACqqqqpVVVVqAAAAAAKqqqqqpVVVqgAAAAAAKqqqqqpVVagAAAAAAAAAAACqVVWoAAAAAAAAAAAACpVWoAAAAAAAAAAAAAqVVqAAAAAAAAAAAAAKlVagAAAAAAAAAAAACpVWoAAAAAAAAAAAAAqVVqAAAAAAAAAAAAAKlVWoAAAAAAAAAAAACpVVqAAAAAAAAAAAAAqVVaoAAAAAAAAAAAAqVVVqqqqqqqqqqqqqqlVVWqqqqqqqqqqqqqlVVVaqqqqqqqqqqqqVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVapVVVVVVVVVVVVVaqqqqqlVVVVVVVVVaqqqqqqqqVVVVVVVWqqqqgCqqqpVVVVVVaqqgAAAAAKqpVVVVVaqgAAAAAAACqpVVVVaoAAAAAAAAACqlVVVaoAAAAAAAAAACqVVVWoAAAAAAAAAAAKlVVVqAAAAAAAAAAAAqVVVagAAAAAAAAAAAKlVVWoAAAAAAAAAAAAqVVVqAAAAAAAAAAAAKlVVagAAAAAAAAAAAAqVVVqAAAKqqgAAAAAKlVVaoAKqqqqgAAAACpVVVqqqqqqqqAAAAAqVVVWqqqlVVaoAAAAKlVVVaqlVVVVqAAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVaoAAAAqVVVVVVVVaqqqAAAAKlVVVVVVaqqqoAAAAKlVVVVVVaqqqgAAAAKpVVVVVVaqgAAAAAAKpVVVVVVaoAAAAAAAKpVVVVVVWoAAAAAAACpVVVVVVVqAAAAAAACpVVVVVVVagAAAAAAAqVVVVVVVWoAAAAAAACpVVVVVVVqAAAAAAAAqpVVVVVVagAAAAAAACqlVVVVVWoAAAAAAAACqVVVVVVaoAAAAAAAAKpVVVVVWqqqqgAAAAAqVVVVVVWqqqqgAAAACpVVVVVVaqqqqgAAAAqVVVVVVVVVVaqAAAAKlVVVVVVVVVVagAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVlVVVVVVqAAAAKlVVaqqVVVVVqAAAACpVVaqqqpVVWqgAAAAqVVaqKqqqqqqgAAAAKlVaoAAqqqqqAAAAACpVaoAAACqqoAAAAACpVWoAAAAAAAAAAAAAqVVqAAAAAAAAAAAAAKlVagAAAAAAAAAAAAKlVWoAAAAAAAAAAAAKpVVqAAAAAAAAAAAACpVVagAAAAAAAAAAAKpVVVqAAAAAAAAAAAKqVVVaqAAAAAAAAAAKpVVVVqqAAAAAAAACqpVVVVVqqoAAAAACqqpVVVVVWqqqqqqqqqqlVVVVVVVqqqqqqqqpVVVVVVVVVWqqqqqpVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVVVVVVVVVVVVVVVVVVVVaqqlVVVVVVVVVVVVVqqqqlVVVVVVVVVVVVqqqqqVVVVVVVVVVVVqgAAKpVVVVVVVVVVVqgAAAqVVVVVVVVVVVqgAAACpVVVVVVVVVVqgAAAAqVVVVVVVVVVagAAAAKlVVVVVVVVVagAAAACpVVVVVVVVVaoAAAAAqVVVVVVVVVWoAAAAAKlVVVVVVVVWoAAAAACpVVVVVVVVWqAAAAAAqVVVVVVVVVqAAAAAAKlVVVVVVVVqAAAAAACpVVVVVVVVqgAAAAAAqVVVVVVVVagAAAAAAKlVVVVVVVagAAAAAACpVVVVVVVaoAAAAAAAqVVVVVVVaoAAAAAAAKlVVVVVVWoAAAAAAACpVVVVVVWoAAAAAAAAqVVVVVVWqAAAAAAAAKlVVVVVVqAAAAAAAACpVVVVVVqAAAAAAAAAqVVVVVVqgAAAAAAAAKlVVVVVagAAAgAAAACpVVVVVagAACogAAAAqVVVVVaoAACqoAAAAKlVVVVWoAAAqagAAACpVVVVWoAAAqWoAAAAqVVVVWqAAAqlqAAAAKlVVVVqAAAKlagAAACpVVVVqAAAKlWoAAAAqVVVVqgAAKpVqAAAAKlVVVagAACpVagAAACpVVVagAACpVWoAAAAqVVVaoAAAqVVqAAAAKlVVWoAAACqqqAAAAAqpVVqAAAAqqqgAAAAKqpVqAAAAAqqAAAAAAKqpagAAAAAAAAAAAAACqWoAAAAAAAAAAAAAACpqAAAAAAAAAAAAAAAqagAAAAAAAAAAAAAAKmoAAAAAAAAAAAAAACpqAAAAAAAAAAAAAAAqagAAAAAAAAAAAAAAKlqAAAAAAAAAAAAAACpaqAAAAAAAAAAAAACpVqqqqqqqgAAAAACqqVVqqqqqqqgAAAAKqqVVVqqqqqqoAAAACqpVVVVVVVVVagAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAACpVVVVVVVVVVVqAAACpVVVVVVVVVVVagAAAqVVVVVVVVVVVVqgACqVVVVVVVVVVVVaqqqqlVVVVVVVVVVVVaqqqVVVVVVVVVVVVVVqqqVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVVVVVVVVVVVVVVaqqqqqqqqpVVVVVVaqqqqqqqqqqqlVVVVaqqqqqqqqqqqqVVVVaqgAAAAAAAACqpVVVaoAAAAAAAAAAAqlVVWoAAAAAAAAAAACpVVVqAAAAAAAAAAAAqVVVagAAAAAAAAAAAKlVVWoAAAAAAAAAAAAqVVVqAAAAAAAAAAAAKlVVagAAAAAAAAAAAKlVVWoAAAAAAAAAAACpVVVqAAAAAAAAAAAAqVVVagAAAAAAAAAAKqVVVWoAAAACqqqqqqqlVVVqAAAAKqqqqqqqVVVVagAAACqqqqqqlVVVVWoAAACpVVVVVVVVVVVqAAAAqVVVVVVVVVVVagAAAKlVVVVVVVVVVWoAAACpVVVVVVVVVVVqAAAAqVVVVVVVVVVVagAAACqqqqqVVVVVVWoAAAAqqqqqqlVVVVVqAAAAAqqqqqqlVVVVagAAAAAAAAAqqVVVVWoAAAAAAAAAAKpVVVVqAAAAAAAAAAAqlVVVagAAAAAAAAAACqVVVWoAAAAAAAAAAAKpVVVqAAAAAAAAAAAAqlVVagAAAAAAAAAAACpVVWoAAAAAAAAAAAAKlVVqAAAAAAAAAAAACpVVagAAAACoAAAAAAKlVWoAAACqqqAAAAACpVVqAAAKqqqqAAAAAqVVWqgqqqpWqoAAAAKlVVqqqqpVVVqAAAACpVVWqqqlVVVWoAAAAqVVVVaVVVVVVqAAAACpVVVVVVVVVVWoAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAAKlVVWqpVVVVVagAAACpVVaqqqVVVVaoAAAAqVVaqqqqlVVqoAAAAKlVWoACqqqqqoAAAACpVWoAAAqqqqgAAAAAqVVqAAAAKqqAAAAAAqVVqAAAAAAAAAAAAAKlVagAAAAAAAAAAAAKlVWoAAAAAAAAAAAACpVVqAAAAAAAAAAAACpVVagAAAAAAAAAAACqVVVqAAAAAAAAAAACqVVVaoAAAAAAAAAAKqVVVVqgAAAAAAAAAKqVVVVWqoAAAAAAAAqpVVVVVaqqgAAAAAqqpVVVVVVaqqqqqqqqqlVVVVVVVWqqqqqqqqVVVVVVVVVVaqqqqqVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVVVqVVVVVVVVVVVVVVqqqqqpVVVVVVVVVVqqqqqqqqVVVVVVVVWqqqqAqqqqlVVVVVVWqqAAAAACqqlVVVVVaqAAAAAAAAqpVVVVVaqAAAAAAAAAKlVVVVaoAAAAAAAAACqVVVVqoAAAAAAAAAAKlVVVaoAAAAAAAAAACpVVVagAAAAAAAAAAAqVVVaoAAAAAAAAAAAKlVVaoAAAAAAAAAAACpVVWoAAAAAAAAAAAAqVVWoAAAAACqqqAAAqVVVqAAAAAKqqqqoAqlVVqAAAAAqqqqqqqqlVVagAAAAqpVVVqqqlVVagAAAAqlVVVVWqVVVWoAAAAqlVVVVVVVVVVqAAAAKlVVVVVVVVVVagAAAKlVVVVVVVVVVagAAACpVaqqqVVVVVWoAAAAqVqqqqqVVVVVqAAAAqWqqqqqqVVVVagAAAImqgAAAqpVVVWoAAAAoqAAAAAqlVVVqAAAAIqAAAAACqlVVqAAAAAIAAAAAAKpVVagAAAAAAAAAAAAKlVWoAAAAAAAAAAAACqVVqAAAAAAAAAAAAAKlVagAAAAAAAAAAAAAqVWoAAAAAAAAAAAAAKlVqAAAAAACqgAAAACpVagAAAAAKqqgAAAAKlWoAAAAAKqqqAAAACpVagAAAACpVaoAAAAqVWoAAAACpVVqAAAAKlVqAAAAAqVVWoAAACpVagAAAAqVVVqAAAAKlWoAAAAKlVVagAAACpVqAAAACpVVWoAAAAqVagAAAAqVVVqAAAAKlVqAAAAKlVVagAAAKlVagAAACpVVWoAAACpVWoAAAAKlVVqAAAAqVVagAAACpVVqAAAAKlVWoAAAAKlVqgAAACpVVqAAAACqqqgAAAAqVVWoAAAAKqqgAAAAqVVVqAAAAAKqAAAAAKlVVWoAAAAAAAAAAAKlVVVqgAAAAAAAAAACpVVVWoAAAAAAAAAACpVVVVagAAAAAAAAACqVVVVWqAAAAAAAAACqVVVVVaqAAAAAAAACqVVVVVVqoAAAAAAACqVVVVVVVqoAAAAAAKqVVVVVVVWqqAAAACqqVVVVVVVVWqqqqqqqpVVVVVVVVVWqqqqqqlVVVVVVVVVVVqqqqpVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVVVVVVVVVVVVVqqqqqqqqqqqlVVVVqqqqqqqqqqqqqqVVVqqqqqqqqqqqqqqpVVqqAAAAAAAAAAAKqlVqgAAAAAAAAAAAACqVagAAAAAAAAAAAAAKlWoAAAAAAAAAAAAAAqVqAAAAAAAAAAAAAAKlagAAAAAAAAAAAAACpWoAAAAAAAAAAAAAAqVqAAAAAAAAAAAAAAKlagAAAAAAAAAAAAACpWqAAAAAAAAAAAAAAqVaqgAAAAAAAAAAAAqVVqqqqqqqqAAAAAAKlVWqqqqqqqqAAAAACpVVVaqqqqqqAAAAACpVVVVVVVVVVqAAAAAqVVVVVVVVVVqAAAAAqVVVVVVVVVVqgAAAAKlVVVVVVVVVagAAAAKlVVVVVVVVVagAAAACpVVVVVVVVVWoAAAACpVVVVVVVVVWoAAAACqVVVVVVVVVVqAAAAAqVVVVVVVVVVqAAAAAqVVVVVVVVVVagAAAAKlVVVVVVVVVagAAAAKlVVVVVVVVVaoAAAACpVVVVVVVVVWoAAAACpVVVVVVVVVWoAAAAAqVVVVVVVVVVqAAAAAqVVVVVVVVVVqAAAAAKlVVVVVVVVVagAAAAKlVVVVVVVVVagAAAAKpVVVVVVVVVaoAAAACpVVVVVVVVVWoAAAACpVVVVVVVVVWoAAAAAqVVVVVVVVVVqAAAAAqVVVVVVVVVVqAAAAAKlVVVVVVVVVagAAAAKlVVVVVVVVVagAAAACpVVVVVVVVVWoAAAACpVVVVVVVVVWoAAAAAqVVVVVVVVVWqAAAAAqVVVVVVVVVVqAAAAAqlVVVVVVVVVqAAAAAKlVVVVVVVVVagAAAAKlVVVVVVVVVagAAAACpVVVVVVVVVWoAAAACpVVVVVVVVVWoAAAAAqVVVVVVVVVVqAAAAAqVVVVVVVVVVqAAAAAKlVVVVVVVVVagAAAAKlVVVVVVVVVWoAAAAKpVVVVVVVVVVqAAAACpVVVVVVVVVVqAAAACpVVVVVVVVVVWoAAACqVVVVVVVVVVVqAAACqVVVVVVVVVVVaoAACqVVVVVVVVVVVVqqqqqVVVVVVVVVVVVWqqqqVVVVVVVVVVVVVWqqpVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVqVVVVVVVVVVVVVWqqqqqpVVVVVVVVVVqqqqqqqqlVVVVVVVWqqqqAqqqqlVVVVVVaqoAAAAACqqVVVVVVaqAAAAAAAAKqVVVVVaoAAAAAAAAAqpVVVVaoAAAAAAAAAAqlVVVaoAAAAAAAAAACqVVVaoAAAAAAAAAAAKlVVWoAAAAAAAAAAAAqVVWoAAAAAAAAAAAAKlVVqAAAAAAAAAAAAAqVVagAAAACqqAAAAAKlVagAAAAKqqqAAAACpVWoAAAAKqqqoAAAAqVVqAAAAKpVVqAAAAKlVagAAACpVVWoAAAAqVWoAAACpVVVqAAAAKlVqAAAAqVVVagAAAKlVagAAAKlVVWoAAACpVWoAAACpVVVqAAAAqVVagAAAKlVVagAAAKlVWoAAACqVVagAAACpVVqAAAAKqqqoAAACpVVWoAAAAqqqoAAAAqVVVqgAAAAqqgAAAAqVVVWqAAAAAAAAAAAqlVVVaoAAAAAAAAAAqlVVVVqAAAAAAAAAAqlVVVVWoAAAAAAAAAKlVVVVVqAAAAAAAAAKlVVVVVqAAAAAAAAAAKlVVVVqgAAAAAAAAAKqVVVWqgAAAAAAAAAAKpVVVqgAAAAAAAAAAAqlVVqAAAAAAAAAAAACqVVqgAAAAKqqAAAAAKlVagAAAAqqqqAAAAAqVWoAAAAqqqqqAAAAKlWoAAAAqlVVqoAAACpVqAAAAKlVVVqAAAAKlagAAAKlVVVWoAAACpWoAAACpVVVVqAAAAqVqAAACpVVVVagAAAKlqAAAAKlVVVWoAAACpagAAACpVVVVqAAAAqVqAAAAKlVVVqAAAAKlagAAACqVVWqgAAACpWoAAAAKqqqqgAAAAqVqAAAAAqqqqAAAAAKlagAAAAAqqoAAAAAKlVqAAAAAAAAAAAAACpVagAAAAAAAAAAAACpVVqAAAAAAAAAAAAAqVVaoAAAAAAAAAAAAqVVVqgAAAAAAAAAAAqlVVWqAAAAAAAAAACqlVVVaqAAAAAAAAACqlVVVVqqAAAAAAAAKqVVVVVVqqoAAAAAKqqVVVVVVVqqqqqqqqqpVVVVVVVVqqqqqqqqlVVVVVVVVVWqqqqqlVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 52 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVValVVVVVVVVVVVVVWqqqqqVVVVVVVVVVVqqqqqqqVVVVVVVVVWqqqgKqqqVVVVVVVVWqoAAAAAqqVVVVVVVaqAAAAAAAqpVVVVVVaqAAAAAAAAqpVVVVVaoAAAAAAAACqVVVVVWoAAAAAAAAACpVVVVWoAAAAAAAAAAqlVVVWqAAAAAAAAAACqVVVVqAAAAAAAAAAAKpVVVqAAAAAAAAAAAAqVVVagAAAAKqgAAAACpVVagAAAAqqqgAAAAqVVWoAAAAKqqoAAAACpVVqAAAAKlVagAAAAqVVqAAAAKpVWqAAAAKlVagAAACpVVagAAACpVWoAAACpVVVqAAAAKlVqAAAAqVVVagAAACpVagAAAKlVVWoAAAAqVWoAAACpVVVqAAAACpVqAAAAqVVVagAAAAqVagAAACpVVagAAAAKlWoAAAAqVVWoAAAACpVqAAAACpVWoAAAAAqVagAAAAqlWqAAAAAKlVqAAAACqqqAAAAACpVagAAAAKqqAAAAAAqVWoAAAAAKoAAAAAAKlVagAAAAAAAAAAAACpVWoAAAAAAAAAAAAAqVVagAAAAAAAAAAAAKlVWqAAAAAAAAAAAACpVVagAAAAAACAAAAAqVVVqAAAAAAKiAAAAKlVVaqAAAAAKigAAACpVVVqoAAAAKpiAAAAqVVVVqoAAAqpagAAAKlVVVWqqqqqpagAAACpVVVVWqqqqlWoAAACpVVVVVWqqqVVqAAAAqVVVVVVVVVVVqAAAAKlVVVVVVVVVVqgAAACpVVVVVVVVVVqgAAACpVVVaqlVVVVqgAAAAqVVVqqqqVVWqgAAAAKlVVqqqqqqqqgAAAAKlVVagAKqqqqAAAAACpVVagAAAqqoAAAAACpVVWoAAAAAAAAAAACqVVVqAAAAAAAAAAAAqVVVagAAAAAAAAAAAqVVVWoAAAAAAAAAAAqlVVVqAAAAAAAAAAAqlVVVagAAAAAAAAAAqlVVVWoAAAAAAAAACqlVVVVagAAAAAAAACqlVVVVWqoAAAAAAAqqVVVVVVaqqAAAAAKqqVVVVVVVaqqqqqqqqpVVVVVVVVWqqqqqqqVVVVVVVVVVVqqqqqlVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU= "))},
|
||||||
|
{width : 25 , height : 86 , bpp : 2, transparent : 1, buffer : E.toArrayBuffer(atob(" VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVaqlVVVVWqqqlVVVWqqqqlVVWqgAKqVVWqAAAKlVWqAAAAqVVqAAAAKlVagAAAAqVWoAAAAKlVqAAAACpVagAAAAqVWoAAAAKlVqAAAACpVagAAACpVWqAAAAqVVagAAAqlVVqgAAqlVVaqqqqlVVVaqqqlVVVVaqqVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVqqqVVVVVqqqqVVVWqqqqpVVVqgAAqlVVqAAACqVVqgAAAKlVagAAAAqVWoAAAAKlVqAAAACpVagAAAAqVWoAAAAKlVqAAAACpVagAAAAqVWoAAAAqVVagAAAKlVWqAAAKlVVaqACqpVVVqqqqpVVVWqqqlVVVVVqpVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV "))}
|
||||||
|
];
|
||||||
|
|
||||||
|
var drawTimeout;
|
||||||
|
|
||||||
|
function queueDraw() {
|
||||||
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
|
drawTimeout = setTimeout(function() {
|
||||||
|
drawTimeout = undefined;
|
||||||
|
draw();
|
||||||
|
}, 60000 - (Date.now() % 60000));
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
var x = g.getWidth()/2;
|
||||||
|
var y = g.getHeight()/2-31;
|
||||||
|
g.reset();
|
||||||
|
var date = new Date();
|
||||||
|
var timeStr = require("locale").time(date,1);
|
||||||
|
// draw time
|
||||||
|
g.clearRect(0,y,g.getWidth()-1,y+73+24+18);
|
||||||
|
//use custom font spacing for overlapping digits
|
||||||
|
g.drawImage(digits[parseInt(date.getHours()/10)],0,y);
|
||||||
|
g.drawImage(digits[parseInt(date.getHours()%10)],37,y);
|
||||||
|
g.drawImage(digits[10],74,y);
|
||||||
|
g.drawImage(digits[parseInt(date.getMinutes()/10)],86,y);
|
||||||
|
g.drawImage(digits[parseInt(date.getMinutes()%10)],123,y);
|
||||||
|
// Draw day of the week
|
||||||
|
y += 73;
|
||||||
|
g.setFontAlign(0,-1).setFont("Teletext10x18Ascii");
|
||||||
|
g.drawString(require("locale").dow(date).toUpperCase(),x,y);
|
||||||
|
// Draw Date
|
||||||
|
y += 24;
|
||||||
|
g.drawString(require('locale').date(new Date(),1),x,y);
|
||||||
|
queueDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
require("FontTeletext10x18Ascii").add(Graphics);
|
||||||
|
Bangle.setUI("clock");
|
||||||
|
g.clear();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
draw();
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: New app
|
||||||
|
0.02: Cleanup interface and add settings, widget, add skin temp reporting.
|
|
@ -0,0 +1,19 @@
|
||||||
|
# CoreTemp display
|
||||||
|
|
||||||
|
Basic example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current skin and body core temperature readings.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Background task connects to any CoreTemp device (2100/2101) and emits a CoreTemp signal value for each reading.
|
||||||
|
Application contains three components, one is a background task that monitors the sensor and emits a 'CoreTemp' signal on activity if activated in settings.
|
||||||
|
The widget shows when the sensor is enabled with a mini value and blinks on use.
|
||||||
|
The app listens for 'CoreTemp' signals and shows the current skin and core temperatures in large numbers.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* Integrate with other tracking/sports apps to log data.
|
||||||
|
* Add specific device selection
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Ivor Hewitt
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"enabled":false
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
//
|
||||||
|
// If enabled in settings run constantly in background
|
||||||
|
//
|
||||||
|
(function() {
|
||||||
|
var log = function() {};//print
|
||||||
|
var settings = {};
|
||||||
|
var device;
|
||||||
|
var gatt;
|
||||||
|
var service;
|
||||||
|
var characteristic;
|
||||||
|
|
||||||
|
class CoreSensor {
|
||||||
|
constructor() {
|
||||||
|
this.unit = "";
|
||||||
|
this.core = -1;
|
||||||
|
this.skin = -1;
|
||||||
|
this.battery = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSensor(event) {
|
||||||
|
if (event.target.uuid == "00002101-5b1e-4347-b07c-97b514dae121") {
|
||||||
|
var dv = event.target.value;
|
||||||
|
var flags = dv.buffer[0];
|
||||||
|
|
||||||
|
if (flags & 8) {
|
||||||
|
this.unit = "F";
|
||||||
|
} else {
|
||||||
|
this.unit = "C";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & 1) {
|
||||||
|
this.skin = (dv.buffer[4] * 256 + dv.buffer[3]) / 100;
|
||||||
|
} else {
|
||||||
|
this.skin = 0;
|
||||||
|
}
|
||||||
|
if (flags & 2) {
|
||||||
|
this.core = (dv.buffer[2] * 256 + dv.buffer[1]) / 100;
|
||||||
|
} else {
|
||||||
|
this.core = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.emit('CoreTemp',
|
||||||
|
{core : this.core, skin : this.skin, unit : this.unit});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBatteryLevel(event) {
|
||||||
|
if (event.target.uuid == "0x2a19")
|
||||||
|
this.battery = event.target.value.getUint8(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mySensor = new CoreSensor();
|
||||||
|
|
||||||
|
function getSensorBatteryLevel(gatt) {
|
||||||
|
gatt.getPrimaryService("180f")
|
||||||
|
.then(function(s) { return s.getCharacteristic("2a19"); })
|
||||||
|
.then(function(c) {
|
||||||
|
c.on('characteristicvaluechanged',
|
||||||
|
(event) => mySensor.updateBatteryLevel(event));
|
||||||
|
return c.startNotifications();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connection_setup() {
|
||||||
|
log("Scanning for CoreTemp sensor...");
|
||||||
|
NRF.requestDevice({active:true,timeout : 20000, filters : [ {namePrefix : 'CORE'} ]})
|
||||||
|
.then(function(d) {
|
||||||
|
device = d;
|
||||||
|
log("Found device");
|
||||||
|
return device.gatt.connect();
|
||||||
|
})
|
||||||
|
.then(function(g) {
|
||||||
|
gatt = g;
|
||||||
|
return gatt.getPrimaryService('00002100-5b1e-4347-b07c-97b514dae121');
|
||||||
|
})
|
||||||
|
.then(function(s) {
|
||||||
|
service = s;
|
||||||
|
return service.getCharacteristic(
|
||||||
|
'00002101-5b1e-4347-b07c-97b514dae121');
|
||||||
|
})
|
||||||
|
.then(function(c) {
|
||||||
|
characteristic = c;
|
||||||
|
characteristic.on('characteristicvaluechanged',
|
||||||
|
(event) => mySensor.updateSensor(event));
|
||||||
|
return characteristic.startNotifications();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
log("Done!");
|
||||||
|
// getSensorBatteryLevel(gatt);
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
log(e.toString(), "ERROR");
|
||||||
|
log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connection_end() {
|
||||||
|
if (gatt != undefined)
|
||||||
|
gatt.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||||
|
log("Settings:");
|
||||||
|
log(settings);
|
||||||
|
|
||||||
|
if (settings.enabled) {
|
||||||
|
connection_setup();
|
||||||
|
NRF.on('disconnect', connection_setup);
|
||||||
|
}
|
||||||
|
|
||||||
|
E.on('kill', () => { connection_end(); });
|
||||||
|
|
||||||
|
})();
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4UA///k0DxUFgsDCY8KwAfJlQLHhWglWq1WgBIcCA4QCB1WoComq0+iBYWqCwl//4OBAAQxChWlv/2BYIlCBYUqv9VvQLBwA9BBYWlqtV/QLBGoRIBgQLBr9aBYQ2BBYMKroLBtQLCgALClIKC1AXG1NVuoFBF4sC09V+woCBAJHCgWXq9oPQZrDgWdq9gBZG9rqgCTwSbCgVVqysDBYkK6tWYoa/DkEJ6vaaIgWBaAILCbQhUCBYXoc4wNBBZWqBfBtB1ALKKZILCR4J3FToQLBU4KPEWoQLNZYILIa4NVcYReEcYOnqtaDAbvDgALBcg4EBlNVqtqDoOgd4YoBBYNWytWCwQdCgQLBAAVaBYkA0oLDuwLFkv1BgZGDAAMJuoKCroWEGAOnDAVftShGr////1tDdG14LB+wiEAAdqHAjTHBYgA=="))
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Simply listen for core events and show data
|
||||||
|
|
||||||
|
var btm = g.getHeight() - 1;
|
||||||
|
var px = g.getWidth() / 2;
|
||||||
|
|
||||||
|
// Dark or light logo
|
||||||
|
var col = (process.env.HWVERSION == 1) ? 65535 : 0;
|
||||||
|
|
||||||
|
var corelogo = {
|
||||||
|
width : 146,
|
||||||
|
height : 48,
|
||||||
|
bpp : 4,
|
||||||
|
transparent : 0,
|
||||||
|
palette : new Uint16Array([ col, col, 2854, 1419 ]),
|
||||||
|
buffer :
|
||||||
|
require("heatshrink")
|
||||||
|
.decompress(atob(
|
||||||
|
"AEUDmczmBD/I4xJ/AAMCkBHFAAJG8kQABJAJHFSVURAAUQRphHCkQGBJAySngJHDJRhHEJALZDAgiSBEQ0RPBIAKHAwQQI4xIEaoQFEEZpIULSRHFkDZDBwZIMEYhITa44SKSAxIDSARIDJ4IjKJCpHNEoiQGJDA2CJCQSOCYaQGJDBsCGiKQGTZIJCI4xBEJBAAEFpQAPDQoMGBQyOGIJJPGF6AALC5glCbJAQEgZCEAoowTSBypJBwKQMIQaSBAgZIJWw5ITB5RTDSBLbEAAjDOPRIVabIiQFJBCQKPYhIVCRxIEBg7WDSBpIVbJ5IQJIqQBgZIiCh7ZLJIriDbhJI3JoxIebIZITI6BIjCZ5IRI4RIPHAYAJJH4AIUAJIzHIhI/SAwzBJH6QGJH5HIHApI2HCIAJL4pITkATOJQJIMHCJeFJD8zaZCQHJCEBJCUCJCKPBJBhWGJEcia5oACJBSfHJB4QMJA6SLI4ZIKPAg3QJCUAJCbbBJETbPJAbbKbIhIBYJpIQbZ5UDbZzZFPBxIVSRIOBJA5JISAhIIF4ZIUfQpJHEwQKDJAhJHbJbBJJCIZECY4KGSQoABBIZOBSBbbIJC6IEBQqSJJoyQLbZBIRbYoAKJAaSHJAjbCF541RSRISLSRkgJAKQKbY5ISJJyQDSRyQMbYxITChhHFSRhGMbY5IUCpRHHJJZITiBIVbpBHJbpJHPFhBITfI4ANIwcgI6AAV"))
|
||||||
|
};
|
||||||
|
|
||||||
|
function onCore(c) {
|
||||||
|
// Large or small font
|
||||||
|
var sz = ((process.env.HWVERSION == 1) ? 3 : 2);
|
||||||
|
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
g.clearRect(0, 32 + 48, g.getWidth(), 32 + 48 + 24 * 4);
|
||||||
|
g.setColor(g.theme.dark ? "#CCC" : "#333"); // gray
|
||||||
|
g.setFont("6x8", sz).drawString(
|
||||||
|
"Core: " + ((c.core < 327) ? (c.core + c.unit) : 'n/a'), px, 48 + 48);
|
||||||
|
g.setFont("6x8", sz).drawString("Skin: " + c.skin + c.unit, px, 48 + 48 + 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background task will activate once settings are enabled.
|
||||||
|
function enableSensor() {
|
||||||
|
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||||
|
|
||||||
|
if (!settings.enabled) {
|
||||||
|
settings.enabled = true;
|
||||||
|
require("Storage").write("coretemp.json", settings);
|
||||||
|
|
||||||
|
drawBackground("Waiting for\ndata...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBackground(message) {
|
||||||
|
g.clear();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
g.reset().setFont("6x8", 2).setFontAlign(0, 0);
|
||||||
|
g.drawImage(corelogo, px - 146 / 2, 30);
|
||||||
|
g.drawString(message, g.getWidth() / 2, g.getHeight() / 2 + 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on('CoreTemp', onCore);
|
||||||
|
|
||||||
|
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||||
|
|
||||||
|
if (!settings.enabled) {
|
||||||
|
drawBackground("Sensor off\nBTN" +
|
||||||
|
((process.env.HWVERSION == 1) ? '2' : '1') + " to enable");
|
||||||
|
} else {
|
||||||
|
drawBackground("Waiting for\ndata...");
|
||||||
|
}
|
||||||
|
|
||||||
|
setWatch(() => { enableSensor(); }, (process.env.HWVERSION == 1) ? BTN2 : BTN1,
|
||||||
|
{repeat : false});
|
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,47 @@
|
||||||
|
// This file should contain exactly one function, which shows the app's settings
|
||||||
|
/**
|
||||||
|
* @param {function} back Use back() to return to settings menu
|
||||||
|
*/
|
||||||
|
(function(back) {
|
||||||
|
const SETTINGS_FILE = 'coretemp.json'
|
||||||
|
// initialize with default settings...
|
||||||
|
let s = {
|
||||||
|
'enabled': true,
|
||||||
|
}
|
||||||
|
// ...and overwrite them with any saved values
|
||||||
|
// This way saved values are preserved if a new version adds more settings
|
||||||
|
const storage = require('Storage')
|
||||||
|
const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||||
|
for (const key in saved) {
|
||||||
|
s[key] = saved[key];
|
||||||
|
}
|
||||||
|
// creates a function to safe a specific setting, e.g. save('color')(1)
|
||||||
|
function save(key) {
|
||||||
|
return function (value) {
|
||||||
|
s[key] = value;
|
||||||
|
storage.write(SETTINGS_FILE, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSettings() {
|
||||||
|
require("Storage").write("coretemp.json", s);
|
||||||
|
if (WIDGETS["coretemp"])
|
||||||
|
WIDGETS["coretemp"].reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = {
|
||||||
|
'' : {'title' : 'CoreTemp sensor'},
|
||||||
|
'< Back' : back,
|
||||||
|
'Enabled' : {
|
||||||
|
value : !!s.enabled,
|
||||||
|
format : v => v ? "Yes" : "No",
|
||||||
|
onchange : v => {
|
||||||
|
s.enabled = v;
|
||||||
|
updateSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
E.showMenu(menu);
|
||||||
|
})
|
|
@ -0,0 +1,66 @@
|
||||||
|
// TODO Change to a generic multiple sensor widget?
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
var settings = {};
|
||||||
|
var count = 0;
|
||||||
|
var core = 0;
|
||||||
|
|
||||||
|
// draw your widget
|
||||||
|
function draw() {
|
||||||
|
if (!settings.enabled)
|
||||||
|
return;
|
||||||
|
g.reset();
|
||||||
|
g.setFont("6x8", 1).setFontAlign(0, 0);
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
g.clearRect(this.x, this.y, this.x + 23, this.y + 23);
|
||||||
|
|
||||||
|
if (count & 1) {
|
||||||
|
g.setColor("#0f0"); // green
|
||||||
|
} else {
|
||||||
|
g.setColor(g.theme.dark ? "#333" : "#CCC"); // off = grey
|
||||||
|
}
|
||||||
|
|
||||||
|
g.drawImage(
|
||||||
|
atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),
|
||||||
|
this.x+(24-12)/2,this.y+1);
|
||||||
|
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.drawString(parseInt(core)+"\n."+parseInt((core*100)%100), this.x + 24 / 2, this.y + 18);
|
||||||
|
|
||||||
|
g.setColor(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a listener to 'blink'
|
||||||
|
function onTemp(temp) {
|
||||||
|
count = count + 1;
|
||||||
|
core = temp.core;
|
||||||
|
WIDGETS["coretemp"].draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by sensor app to update status
|
||||||
|
function reload() {
|
||||||
|
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||||
|
|
||||||
|
Bangle.removeListener('CoreTemp', onTemp);
|
||||||
|
|
||||||
|
if (settings.enabled) {
|
||||||
|
WIDGETS["coretemp"].width = 24;
|
||||||
|
Bangle.on('CoreTemp', onTemp);
|
||||||
|
} else {
|
||||||
|
WIDGETS["coretemp"].width = 0;
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add the widget
|
||||||
|
WIDGETS["coretemp"] = {
|
||||||
|
area : "tl",
|
||||||
|
width : 24,
|
||||||
|
draw : draw,
|
||||||
|
reload : function() {
|
||||||
|
reload();
|
||||||
|
Bangle.drawWidgets(); // relayout all widgets
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// load settings, set correct widget width
|
||||||
|
reload();
|
||||||
|
})()
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("/wA/AH4A/AH4A/ACmsAEQuMlcAAD0rGBQKBFr4ADGBOsqwvjqwvJRsCRFF/8Gg4ADEZYQEgwvWg8+AAgwKCJgvQDgoABF5IRMF5xEBJpBhGCJwvNDQM4AYMNAAQaBnCAFCJ4vNIwQeBAAkxQAwGCmIRFFwIRDF64dDgwGBgwRNF/4v/F/4v/F/4v/F/4dJmIdECIkxF7MHFwUHhoACg4eCAYIACCJ4vNDQIgCAAgICKwoROF5yAEAAgtFCKAvQJpAAICJgvQgEGg4ADFxIwCAAcGBYovRADov6qwvjqwvJ1gvjEoIvHGASRgRoIuJGAYAhFxQA/AH4A/AH4A/ABQ"))
|
require("heatshrink").decompress(atob("mEw4UA///A4N551ulxL/ACkK1QAG0ALBlNVAA1oBYOlBY9aBYO1BY9eBYOVBY9WBbf/+oIBr//BYlX//9BYN///VC599qtX6oBBqt9BYYRBr/1AIIdBBf4L/BY6bLZcb7MBau1BY9eBYOlBY9aBYMpBY9oBYMK1QAG0ALBAH4ASA"))
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
|
@ -0,0 +1,28 @@
|
||||||
|
# DirAct
|
||||||
|
|
||||||
|
[DirAct](https://www.reelyactive.com/diract/) implementation for the Bangle.js.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Real-time interactions will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the [DirAct open standard](https://reelyactive.github.io/diract/).
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Currently implements DirAct real-time functionality.
|
||||||
|
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates.
|
||||||
|
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com). DirAct is jointly developed by reelyActive and Code Blue Consulting.
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwkE/4AImUQgMjBpIAI+UQFAMBn4XRmJBDiYXRFwQwCC9HzF93/kAXDgSPWj4XRMAZGSDAUhiTWSAH4Ag+URAAUvRBkSAofxgUzmchX4khSw3ygIID+UiFwMiF4bIBGowIBiYvEmUjF4kxEwgABmU/mMCC4cBiUhiAXDkET+cjC4cgj5IE+MAI4MAC4RGCHQIXEgRREF44kCCIIeCDoIIBMAYkDHQJeDEwJBBn/xgYGBn8hC5cS+YoBmEfFoP/IoMxgYXJmETJIISBj/zgBOBDgITCSgYXDBoYUCDQIXBiYXNBIIXBA4IXHI44XEJIJHIC5JHEVwRkBO5qKBbYqPGgMikTXDmKPDEAPyAIJJCX4cAAAQXDIoQtBl6IEGIIXJFoYmCSAQ4BSYIXJRYJWBDQIACkM/eYQXJdYcSC4fzG4J2CC5MwK4I+CAAR4BLwQXJBwowDiQfDC5HzLAIXFGwoXILAQALC5IANC/4X/C+w"))
|
|
@ -0,0 +1,548 @@
|
||||||
|
/**
|
||||||
|
* Copyright reelyActive 2017-2021
|
||||||
|
* We believe in an open Internet of Things
|
||||||
|
*
|
||||||
|
* DirAct is jointly developed by reelyActive and Code Blue Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
// User-configurable constants
|
||||||
|
const INSTANCE_ID = [ 0x00, 0x00, 0x00, 0x01 ];
|
||||||
|
const NAMESPACE_FILTER_ID = [ 0xc0, 0xde, 0xb1, 0x0e, 0x1d,
|
||||||
|
0xd1, 0xe0, 0x1b, 0xed, 0x0c ];
|
||||||
|
const EXCITER_INSTANCE_IDS = new Uint32Array([ 0xe8c17e45 ]);
|
||||||
|
const RESETTER_INSTANCE_IDS = new Uint32Array([ 0x4e5e77e4 ]);
|
||||||
|
const PROXIMITY_RSSI_THRESHOLD = -65;
|
||||||
|
const PROXIMITY_LED_RSSI_THRESHOLD = -65;
|
||||||
|
const PROXIMITY_TABLE_SIZE = 8;
|
||||||
|
const DIGEST_TABLE_SIZE = 32;
|
||||||
|
const OBSERVE_PERIOD_MILLISECONDS = 400;
|
||||||
|
const BROADCAST_PERIOD_MILLISECONDS = 3600;
|
||||||
|
const BROADCAST_DIGEST_PAGE_MILLISECONDS = 400;
|
||||||
|
const PROXIMITY_PACKET_INTERVAL_MILLISECONDS = 400;
|
||||||
|
const DIGEST_PACKET_INTERVAL_MILLISECONDS = 100;
|
||||||
|
const DIGEST_TIME_CYCLE_THRESHOLD = 86400;
|
||||||
|
const EXCITER_HOLDOFF_SECONDS = 60;
|
||||||
|
const BLINK_ON_PROXIMITY = true;
|
||||||
|
const BLINK_ON_DISTANCING = true;
|
||||||
|
const BLINK_ON_DIGEST = true;
|
||||||
|
const BLINK_ON_RESET = true;
|
||||||
|
|
||||||
|
|
||||||
|
// Eddystone protocol constants
|
||||||
|
const EDDYSTONE_UUID = 'feaa';
|
||||||
|
const EDDYSTONE_UID_FRAME = 0x00;
|
||||||
|
const EDDYSTONE_NAMESPACE_OFFSET = 2;
|
||||||
|
const EDDYSTONE_NAMESPACE_LENGTH = 10;
|
||||||
|
const EDDYSTONE_INSTANCE_OFFSET = 14;
|
||||||
|
|
||||||
|
|
||||||
|
// DirAct constants
|
||||||
|
const DIRACT_MANUFACTURER_ID = 0x0583; // Code Blue Consulting
|
||||||
|
const DIRACT_PROXIMITY_FRAME = 0x01;
|
||||||
|
const DIRACT_DIGEST_FRAME = 0x11;
|
||||||
|
const DIRACT_DEFAULT_COUNT_LENGTH = 0x07;
|
||||||
|
const DIRACT_INSTANCE_LENGTH = 4;
|
||||||
|
const DIRACT_INSTANCE_OFFSET = 2;
|
||||||
|
const MAX_NUMBER_STRONGEST = 3;
|
||||||
|
const MAX_BATTERY_VOLTAGE = 3.3;
|
||||||
|
const MIN_BATTERY_VOLTAGE = 3.0;
|
||||||
|
const MAX_RSSI_TO_ENCODE = -28;
|
||||||
|
const MIN_RSSI_TO_ENCODE = -92;
|
||||||
|
const MAX_ACCELERATION_TO_ENCODE = 2;
|
||||||
|
const MAX_ACCELERATION_MAGNITUDE = 0x1f;
|
||||||
|
const INVALID_ACCELERATION_CODE = 0x20;
|
||||||
|
const SCAN_OPTIONS = {
|
||||||
|
filters: [
|
||||||
|
{ manufacturerData: { 0x0583: {} } },
|
||||||
|
{ services: [ EDDYSTONE_UUID ] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Other constants
|
||||||
|
const BITS_PER_BYTE = 8;
|
||||||
|
const DUMMY_INSTANCE_ID = 0;
|
||||||
|
const DUMMY_RSSI = MIN_RSSI_TO_ENCODE;
|
||||||
|
|
||||||
|
|
||||||
|
// Global variables
|
||||||
|
let proximityInstances = new Uint32Array(PROXIMITY_TABLE_SIZE);
|
||||||
|
let proximityRssis = new Int8Array(PROXIMITY_TABLE_SIZE);
|
||||||
|
let digestInstances = new Uint32Array(DIGEST_TABLE_SIZE);
|
||||||
|
let digestCounts = new Uint16Array(DIGEST_TABLE_SIZE);
|
||||||
|
let digestTime = new Uint8Array([ 0, 0, 0 ]);
|
||||||
|
let numberOfDigestPages = 0;
|
||||||
|
let sensorData = [ 0x82, 0x08, 0x3f ];
|
||||||
|
let cyclicCount = 0;
|
||||||
|
let lastDigestTime = Math.round(getTime());
|
||||||
|
let lastResetTime = Math.round(getTime());
|
||||||
|
let isExciterPresent = false;
|
||||||
|
let isResetterPresent = false;
|
||||||
|
let isProximityDetected = false;
|
||||||
|
let menu = {
|
||||||
|
"": { "title": "-- DirAct --" }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate observer mode, scanning for devices in proximity.
|
||||||
|
*/
|
||||||
|
function observe() {
|
||||||
|
proximityInstances.fill(DUMMY_INSTANCE_ID); // Reset proximity
|
||||||
|
proximityRssis.fill(DUMMY_RSSI); // table data
|
||||||
|
isExciterPresent = false;
|
||||||
|
isResetterPresent = false;
|
||||||
|
|
||||||
|
NRF.setScan(handleDiscoveredDevice, SCAN_OPTIONS); // Start scanning
|
||||||
|
setTimeout(broadcast, OBSERVE_PERIOD_MILLISECONDS); // ...until period end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile the scan results and initiate broadcaster mode, advertising either
|
||||||
|
* proximity or digest packets, in consequence.
|
||||||
|
*/
|
||||||
|
function broadcast() {
|
||||||
|
NRF.setScan(); // Stop scanning
|
||||||
|
|
||||||
|
let sortedProximityIndices = getSortedIndices(proximityRssis);
|
||||||
|
|
||||||
|
updateDigestTable(sortedProximityIndices);
|
||||||
|
updateSensorData();
|
||||||
|
|
||||||
|
let currentTime = Math.round(getTime());
|
||||||
|
let isExcited = isExciterPresent &&
|
||||||
|
((currentTime - lastDigestTime) > EXCITER_HOLDOFF_SECONDS);
|
||||||
|
|
||||||
|
if(isResetterPresent) {
|
||||||
|
if(BLINK_ON_RESET) {
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
}
|
||||||
|
lastResetTime = currentTime;
|
||||||
|
resetDigest();
|
||||||
|
broadcastProximity(sortedProximityIndices);
|
||||||
|
}
|
||||||
|
else if(isExcited) {
|
||||||
|
let sortedDigestIndices = getSortedIndices(digestCounts);
|
||||||
|
compileDigest();
|
||||||
|
broadcastDigest(sortedDigestIndices, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
broadcastProximity(sortedProximityIndices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate broadcaster mode advertising proximity packets.
|
||||||
|
* @param {TypedArray} sortedIndices The sorted proximity table indices.
|
||||||
|
*/
|
||||||
|
function broadcastProximity(sortedIndices) {
|
||||||
|
let advertisingOptions = {
|
||||||
|
interval: PROXIMITY_PACKET_INTERVAL_MILLISECONDS,
|
||||||
|
showName: false,
|
||||||
|
manufacturer: DIRACT_MANUFACTURER_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
advertisingOptions.manufacturerData = compileProximityData(sortedIndices);
|
||||||
|
NRF.setAdvertising({}, advertisingOptions); // Start advertising
|
||||||
|
setTimeout(observe, BROADCAST_PERIOD_MILLISECONDS); // ...until period end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate broadcaster mode advertising digest packets.
|
||||||
|
* @param {TypedArray} sortedIndices The sorted digest table indices.
|
||||||
|
* @param {Number} pageNumber The page number to broadcast.
|
||||||
|
*/
|
||||||
|
function broadcastDigest(sortedIndices, pageNumber) {
|
||||||
|
let isLastPage = (pageNumber === (numberOfDigestPages - 1));
|
||||||
|
let advertisingOptions = {
|
||||||
|
interval: DIGEST_PACKET_INTERVAL_MILLISECONDS,
|
||||||
|
showName: false,
|
||||||
|
manufacturer: DIRACT_MANUFACTURER_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
advertisingOptions.manufacturerData = compileDigestData(sortedIndices,
|
||||||
|
pageNumber);
|
||||||
|
NRF.setAdvertising({}, advertisingOptions); // Start advertising
|
||||||
|
|
||||||
|
if(isLastPage) {
|
||||||
|
setTimeout(observe, BROADCAST_DIGEST_PAGE_MILLISECONDS);
|
||||||
|
if(getTime() > DIGEST_TIME_CYCLE_THRESHOLD) {
|
||||||
|
lastResetTime = Math.round(getTime());
|
||||||
|
resetDigest();
|
||||||
|
}
|
||||||
|
if(BLINK_ON_DIGEST) {
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setTimeout(broadcastDigest, BROADCAST_DIGEST_PAGE_MILLISECONDS,
|
||||||
|
sortedIndices, ++pageNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the given device discovered on scan and process further if
|
||||||
|
* Eddystone-UID or DirAct.
|
||||||
|
* @param {BluetoothDevice} device The discovered device.
|
||||||
|
*/
|
||||||
|
function handleDiscoveredDevice(device) {
|
||||||
|
let isEddystone = (device.hasOwnProperty('services') &&
|
||||||
|
device.hasOwnProperty('serviceData') &&
|
||||||
|
(device.services[0] === EDDYSTONE_UUID));
|
||||||
|
let isManufacturer = (device.hasOwnProperty('manufacturer') &&
|
||||||
|
device.manufacturer === DIRACT_MANUFACTURER_ID);
|
||||||
|
|
||||||
|
if(isEddystone) {
|
||||||
|
let isEddystoneUID = (device.serviceData[EDDYSTONE_UUID][0] ===
|
||||||
|
EDDYSTONE_UID_FRAME);
|
||||||
|
if(isEddystoneUID) {
|
||||||
|
handleEddystoneUidDevice(device.serviceData[EDDYSTONE_UUID], device.rssi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(isManufacturer) {
|
||||||
|
let isDirAct = ((device.manufacturerData[0] === DIRACT_PROXIMITY_FRAME) ||
|
||||||
|
(device.manufacturerData[0] === DIRACT_DIGEST_FRAME));
|
||||||
|
if(isDirAct) {
|
||||||
|
handleDirActDevice(device.manufacturerData, device.rssi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the given Eddystone-UID device, adding to the devices in range if
|
||||||
|
* it meets the filter criteria.
|
||||||
|
* @param {Array} serviceData The Eddystone service data.
|
||||||
|
* @param {Number} rssi The received signal strength.
|
||||||
|
*/
|
||||||
|
function handleEddystoneUidDevice(serviceData, rssi) {
|
||||||
|
for(let cByte = 0; cByte < EDDYSTONE_NAMESPACE_LENGTH; cByte++) {
|
||||||
|
let namespaceIndex = EDDYSTONE_NAMESPACE_OFFSET + cByte;
|
||||||
|
if(serviceData[namespaceIndex] !== NAMESPACE_FILTER_ID[cByte]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instanceId = 0;
|
||||||
|
let bitShift = (DIRACT_INSTANCE_LENGTH - 1) * BITS_PER_BYTE;
|
||||||
|
|
||||||
|
for(let cByte = 0; cByte < DIRACT_INSTANCE_LENGTH; cByte++) {
|
||||||
|
let instanceByte = serviceData[EDDYSTONE_INSTANCE_OFFSET + cByte];
|
||||||
|
instanceId += instanceByte << bitShift;
|
||||||
|
bitShift -= BITS_PER_BYTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsignedInstanceId = new Uint32Array([instanceId])[0];
|
||||||
|
|
||||||
|
if(EXCITER_INSTANCE_IDS.indexOf(unsignedInstanceId) >= 0) {
|
||||||
|
isExciterPresent = true;
|
||||||
|
}
|
||||||
|
else if(RESETTER_INSTANCE_IDS.indexOf(unsignedInstanceId) >= 0) {
|
||||||
|
isResetterPresent = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateProximityTable(instanceId, rssi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the given DirAct device, adding to the devices in range if
|
||||||
|
* it meets the filter criteria.
|
||||||
|
* @param {Array} manufacturerData The DirAct manufacturer data.
|
||||||
|
* @param {Number} rssi The received signal strength.
|
||||||
|
*/
|
||||||
|
function handleDirActDevice(manufacturerData, rssi) {
|
||||||
|
let instanceId = 0;
|
||||||
|
let bitShift = (DIRACT_INSTANCE_LENGTH - 1) * BITS_PER_BYTE;
|
||||||
|
|
||||||
|
for(let cByte = DIRACT_INSTANCE_OFFSET;
|
||||||
|
cByte < DIRACT_INSTANCE_OFFSET + DIRACT_INSTANCE_LENGTH; cByte++) {
|
||||||
|
let instanceByte = manufacturerData[cByte];
|
||||||
|
instanceId += instanceByte << bitShift;
|
||||||
|
bitShift -= BITS_PER_BYTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProximityTable(instanceId, rssi);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the proximity table with the given instance's RSSI. If the instance
|
||||||
|
* already exists, combine RSSI values in a weighted average.
|
||||||
|
* @param {String} instanceId The DirAct 4-byte instance id as a 32-bit integer.
|
||||||
|
* @param {Number} rssi The received signal strength.
|
||||||
|
*/
|
||||||
|
function updateProximityTable(instanceId, rssi) {
|
||||||
|
let instanceIndex = proximityInstances.indexOf(instanceId);
|
||||||
|
let isNewInstance = (instanceIndex < 0);
|
||||||
|
|
||||||
|
if(isNewInstance) {
|
||||||
|
let nextIndex = proximityInstances.indexOf(DUMMY_INSTANCE_ID);
|
||||||
|
if(nextIndex >= 0) {
|
||||||
|
proximityInstances[nextIndex] = instanceId;
|
||||||
|
proximityRssis[nextIndex] = rssi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
proximityRssis[instanceIndex] = (proximityRssis[instanceIndex] + rssi) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the digest table based on the proximity table and its sorted indices.
|
||||||
|
* @param {TypedArray} sortedIndices The sorted proximity table indices.
|
||||||
|
*/
|
||||||
|
function updateDigestTable(sortedIndices) {
|
||||||
|
for(let cInstance = 0; cInstance < PROXIMITY_TABLE_SIZE; cInstance++) {
|
||||||
|
let proximityIndex = sortedIndices[cInstance];
|
||||||
|
|
||||||
|
if(proximityRssis[proximityIndex] >= PROXIMITY_RSSI_THRESHOLD) {
|
||||||
|
let instanceId = proximityInstances[proximityIndex];
|
||||||
|
let instanceIndex = digestInstances.indexOf(instanceId);
|
||||||
|
let isNewInstance = (instanceIndex < 0);
|
||||||
|
|
||||||
|
if(isNewInstance) {
|
||||||
|
let nextIndex = digestInstances.indexOf(DUMMY_INSTANCE_ID);
|
||||||
|
if(nextIndex >= 0) {
|
||||||
|
digestInstances[nextIndex] = instanceId;
|
||||||
|
digestCounts[nextIndex] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(digestCounts[instanceIndex] < 65535) {
|
||||||
|
digestCounts[instanceIndex]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cInstance = PROXIMITY_TABLE_SIZE; // Break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compile the digest from the digest table.
|
||||||
|
*/
|
||||||
|
function compileDigest() {
|
||||||
|
let numberOfEntries = digestCounts.findIndex(count => count === 0);
|
||||||
|
let currentTime = Math.round(getTime());
|
||||||
|
let elapsedTime = currentTime - lastResetTime;
|
||||||
|
if(numberOfEntries < 0) {
|
||||||
|
numberOfEntries = DIGEST_TABLE_SIZE;
|
||||||
|
}
|
||||||
|
digestTime[0] = (elapsedTime >> 16) & 0xff;
|
||||||
|
digestTime[1] = (elapsedTime >> 8) & 0xff;
|
||||||
|
digestTime[2] = elapsedTime & 0xff;
|
||||||
|
numberOfDigestPages = Math.max(1, Math.min(8, Math.ceil(numberOfEntries/3)));
|
||||||
|
lastDigestTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clear the digest table.
|
||||||
|
*/
|
||||||
|
function resetDigest() {
|
||||||
|
digestInstances.fill(DUMMY_INSTANCE_ID);
|
||||||
|
digestCounts.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile the DirAct proximity data.
|
||||||
|
* @param {TypedArray} sortedIndices The sorted proximity table indices.
|
||||||
|
*/
|
||||||
|
function compileProximityData(sortedIndices) {
|
||||||
|
let data = [
|
||||||
|
DIRACT_PROXIMITY_FRAME, DIRACT_DEFAULT_COUNT_LENGTH,
|
||||||
|
INSTANCE_ID[0], INSTANCE_ID[1], INSTANCE_ID[2], INSTANCE_ID[3],
|
||||||
|
sensorData[0], sensorData[1], sensorData[2]
|
||||||
|
];
|
||||||
|
let isNewProximityDetected = false;
|
||||||
|
|
||||||
|
for(let cInstance = 0; cInstance < MAX_NUMBER_STRONGEST; cInstance++) {
|
||||||
|
let index = sortedIndices[cInstance];
|
||||||
|
|
||||||
|
if(proximityRssis[index] >= PROXIMITY_RSSI_THRESHOLD) {
|
||||||
|
let instanceId = proximityInstances[index];
|
||||||
|
data.push((instanceId >> 24) & 0xff, (instanceId >> 16) & 0xff,
|
||||||
|
(instanceId >> 8) & 0xff, instanceId & 0xff,
|
||||||
|
encodeRssi(proximityRssis[index]));
|
||||||
|
if(proximityRssis[index] >= PROXIMITY_LED_RSSI_THRESHOLD) {
|
||||||
|
isNewProximityDetected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cInstance = PROXIMITY_TABLE_SIZE; // Break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cyclicCount = (cyclicCount + 1) % 8;
|
||||||
|
|
||||||
|
data[1] = (cyclicCount << 5) + (data.length - 2);
|
||||||
|
|
||||||
|
if(isProximityDetected && !isNewProximityDetected && BLINK_ON_DISTANCING) {
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
}
|
||||||
|
else if(isNewProximityDetected && BLINK_ON_PROXIMITY) {
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
}
|
||||||
|
isProximityDetected = isNewProximityDetected;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile the DirAct digest data.
|
||||||
|
* @param {TypedArray} sortedIndices The sorted digest table indices.
|
||||||
|
* @param {Number} digestPage The page of the digest to compile.
|
||||||
|
*/
|
||||||
|
function compileDigestData(sortedIndices, digestPage) {
|
||||||
|
let isLastPage = (digestPage === (numberOfDigestPages - 1));
|
||||||
|
|
||||||
|
let digestStatus = digestTime[0] & 0x7f;
|
||||||
|
if(isLastPage) {
|
||||||
|
digestStatus |= 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
DIRACT_DIGEST_FRAME, DIRACT_DEFAULT_COUNT_LENGTH,
|
||||||
|
INSTANCE_ID[0], INSTANCE_ID[1], INSTANCE_ID[2], INSTANCE_ID[3],
|
||||||
|
digestStatus, digestTime[1], digestTime[2]
|
||||||
|
];
|
||||||
|
let pageIndex = digestPage * 3;
|
||||||
|
|
||||||
|
for(let cInstance = pageIndex; cInstance < (pageIndex + 3); cInstance++) {
|
||||||
|
let index = sortedIndices[cInstance];
|
||||||
|
|
||||||
|
if(digestCounts[index] > 0) {
|
||||||
|
let instanceId = digestInstances[index];
|
||||||
|
let encodedCount = digestCounts[index];
|
||||||
|
if(encodedCount > 127) {
|
||||||
|
encodedCount = 0x80 | (Math.min((encodedCount >> 8), 0x7f) & 0x7f);
|
||||||
|
}
|
||||||
|
data.push((instanceId >> 24) & 0xff, (instanceId >> 16) & 0xff,
|
||||||
|
(instanceId >> 8) & 0xff, instanceId & 0xff,
|
||||||
|
encodedCount);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cInstance = pageIndex + 3; // Break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data[1] = (digestPage << 5) + (data.length - 2);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the given RSSI.
|
||||||
|
* @param {Number} rssi The given RSSI.
|
||||||
|
* @return {Number} The encoded RSSI.
|
||||||
|
*/
|
||||||
|
function encodeRssi(rssi) {
|
||||||
|
rssi = Math.round(rssi);
|
||||||
|
|
||||||
|
if(rssi >= MAX_RSSI_TO_ENCODE) {
|
||||||
|
return 0x3f;
|
||||||
|
}
|
||||||
|
if(rssi <= MIN_RSSI_TO_ENCODE) {
|
||||||
|
return 0x00;
|
||||||
|
}
|
||||||
|
return rssi - MIN_RSSI_TO_ENCODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the battery percentage.
|
||||||
|
* @return {Number} The battery percentage.
|
||||||
|
*/
|
||||||
|
function encodeBatteryPercentage() {
|
||||||
|
let voltage = NRF.getBattery();
|
||||||
|
|
||||||
|
if(voltage <= MIN_BATTERY_VOLTAGE) {
|
||||||
|
return 0x00;
|
||||||
|
}
|
||||||
|
if(voltage >= MAX_BATTERY_VOLTAGE) {
|
||||||
|
return 0x3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(0x3f * (voltage - MIN_BATTERY_VOLTAGE) /
|
||||||
|
(MAX_BATTERY_VOLTAGE - MIN_BATTERY_VOLTAGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the acceleration.
|
||||||
|
* @return {Array} The encoded acceleration [ x, y, z ].
|
||||||
|
*/
|
||||||
|
function encodeAcceleration() {
|
||||||
|
let encodedAcceleration = { x: INVALID_ACCELERATION_CODE,
|
||||||
|
y: INVALID_ACCELERATION_CODE,
|
||||||
|
z: INVALID_ACCELERATION_CODE };
|
||||||
|
|
||||||
|
let acceleration = { x: Bangle.getAccel().x,
|
||||||
|
y: Bangle.getAccel().y,
|
||||||
|
z: Bangle.getAccel().z };
|
||||||
|
|
||||||
|
for(let axis in acceleration) {
|
||||||
|
let magnitude = acceleration[axis];
|
||||||
|
let encodedMagnitude = Math.min(MAX_ACCELERATION_MAGNITUDE,
|
||||||
|
Math.round(MAX_ACCELERATION_MAGNITUDE *
|
||||||
|
(Math.abs(magnitude) /
|
||||||
|
MAX_ACCELERATION_TO_ENCODE)));
|
||||||
|
if(magnitude < 0) {
|
||||||
|
encodedMagnitude = 0x3f - encodedMagnitude;
|
||||||
|
}
|
||||||
|
encodedAcceleration[axis] = encodedMagnitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedAcceleration;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the sensor data (battery & acceleration) for the advertising packet.
|
||||||
|
*/
|
||||||
|
function updateSensorData() {
|
||||||
|
|
||||||
|
// Update the battery measurement each time the cyclic count resets
|
||||||
|
if(cyclicCount === 0) {
|
||||||
|
encodedBattery = encodeBatteryPercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedAcceleration = encodeAcceleration();
|
||||||
|
|
||||||
|
sensorData[0] = ((encodedAcceleration.x << 2) & 0xfc) |
|
||||||
|
((encodedAcceleration.y >> 4) & 0x3f);
|
||||||
|
sensorData[1] = ((encodedAcceleration.y << 4) & 0xf0) |
|
||||||
|
((encodedAcceleration.z >> 2) & 0x0f);
|
||||||
|
sensorData[2] = ((encodedAcceleration.z << 6) & 0xc0) |
|
||||||
|
(encodedBattery & 0x3f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the sorted order of the indices of the given array.
|
||||||
|
* @return {Uint8Array} The array of indices sorted in descending order.
|
||||||
|
*/
|
||||||
|
function getSortedIndices(unsortedArray) {
|
||||||
|
let sortedIndices = new Uint8Array(unsortedArray.length);
|
||||||
|
|
||||||
|
sortedIndices.forEach((value, index) => sortedIndices[index] = index);
|
||||||
|
sortedIndices.sort((a, b) => unsortedArray[b] - unsortedArray[a]);
|
||||||
|
|
||||||
|
return sortedIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// On start: begin DirAct operation and display the menu
|
||||||
|
g.clear();
|
||||||
|
E.showMenu(menu);
|
||||||
|
observe();
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -3,3 +3,5 @@
|
||||||
0.03: cycle thru pages
|
0.03: cycle thru pages
|
||||||
0.04: reset to clock after 2 mins of inactivity
|
0.04: reset to clock after 2 mins of inactivity
|
||||||
0.05: add Bangle 2 version
|
0.05: add Bangle 2 version
|
||||||
|
0.06: Adds settings page (hide clocks or launchers)
|
||||||
|
0.07: Adds setting for directly launching app on touch for Bangle 2
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
# Desktop style App Launcher
|
# Desktop style App Launcher
|
||||||
|
|
||||||
|
Bangle 1:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
In the picture above, the Settings app is selected.
|
In the picture above, the Settings app is selected.
|
||||||
|
|
||||||
|
Bangle 2:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Controls- Bangle
|
## Controls- Bangle
|
||||||
|
|
||||||
**BTN1** - move backward through app icons on a page
|
**BTN1** - move backward through app icons on a page
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var settings = Object.assign({
|
||||||
|
showClocks: true,
|
||||||
|
showLaunchers: true,
|
||||||
|
}, require('Storage').readJSON("dtlaunch.json", true) || {});
|
||||||
|
|
||||||
function wdog(handle,timeout){
|
function wdog(handle,timeout){
|
||||||
if(handle !== undefined){
|
if(handle !== undefined){
|
||||||
wdog.handle = handle;
|
wdog.handle = handle;
|
||||||
|
@ -17,7 +22,13 @@ function wdog(handle,timeout){
|
||||||
wdog(load,120000)
|
wdog(load,120000)
|
||||||
|
|
||||||
var s = require("Storage");
|
var s = require("Storage");
|
||||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
var apps = s.list(/\.info$/).map(app=>{
|
||||||
|
var a=s.readJSON(app,1);
|
||||||
|
return a && {
|
||||||
|
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
|
||||||
|
};}).filter(
|
||||||
|
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
|
||||||
|
|
||||||
apps.sort((a,b)=>{
|
apps.sort((a,b)=>{
|
||||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||||
if (n) return n; // do sortorder first
|
if (n) return n; // do sortorder first
|
||||||
|
|
|
@ -2,8 +2,20 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var settings = Object.assign({
|
||||||
|
showClocks: true,
|
||||||
|
showLaunchers: true,
|
||||||
|
direct: false,
|
||||||
|
}, require('Storage').readJSON("dtlaunch.json", true) || {});
|
||||||
|
|
||||||
var s = require("Storage");
|
var s = require("Storage");
|
||||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
var apps = s.list(/\.info$/).map(app=>{
|
||||||
|
var a=s.readJSON(app,1);
|
||||||
|
return a && {
|
||||||
|
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
|
||||||
|
};}).filter(
|
||||||
|
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
|
||||||
|
|
||||||
apps.sort((a,b)=>{
|
apps.sort((a,b)=>{
|
||||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||||
if (n) return n; // do sortorder first
|
if (n) return n; // do sortorder first
|
||||||
|
@ -28,7 +40,7 @@ const YOFF = 30;
|
||||||
function draw_icon(p,n,selected) {
|
function draw_icon(p,n,selected) {
|
||||||
var x = (n%2)*72+XOFF;
|
var x = (n%2)*72+XOFF;
|
||||||
var y = n>1?72+YOFF:YOFF;
|
var y = n>1?72+YOFF:YOFF;
|
||||||
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+10,y+2,x+60,y+52);
|
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
|
||||||
g.clearRect(x+12,y+4,x+59,y+51);
|
g.clearRect(x+12,y+4,x+59,y+51);
|
||||||
g.setColor(g.theme.fg);
|
g.setColor(g.theme.fg);
|
||||||
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
|
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
|
||||||
|
@ -52,7 +64,7 @@ function drawPage(p){
|
||||||
}
|
}
|
||||||
for (var i=0;i<4;i++) {
|
for (var i=0;i<4;i++) {
|
||||||
if (!apps[p*4+i]) return i;
|
if (!apps[p*4+i]) return i;
|
||||||
draw_icon(p,i,selected==i);
|
draw_icon(p,i,selected==i && !settings.direct);
|
||||||
}
|
}
|
||||||
g.flip();
|
g.flip();
|
||||||
}
|
}
|
||||||
|
@ -81,9 +93,9 @@ Bangle.on("touch",(_,p)=>{
|
||||||
for (i=0;i<4;i++){
|
for (i=0;i<4;i++){
|
||||||
if((page*4+i)<Napps){
|
if((page*4+i)<Napps){
|
||||||
if (isTouched(p,i)) {
|
if (isTouched(p,i)) {
|
||||||
draw_icon(page,i,true);
|
draw_icon(page,i,true && !settings.direct);
|
||||||
if (selected>=0) {
|
if (selected>=0 || settings.direct) {
|
||||||
if (selected!=i){
|
if (selected!=i && !settings.direct){
|
||||||
draw_icon(page,selected,false);
|
draw_icon(page,selected,false);
|
||||||
} else {
|
} else {
|
||||||
load(apps[page*4+i].src);
|
load(apps[page*4+i].src);
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
(function(back) {
|
||||||
|
var FILE = "dtlaunch.json";
|
||||||
|
|
||||||
|
var settings = Object.assign({
|
||||||
|
showClocks: true,
|
||||||
|
showLaunchers: true
|
||||||
|
}, require('Storage').readJSON(FILE, true) || {});
|
||||||
|
|
||||||
|
function writeSettings() {
|
||||||
|
require('Storage').writeJSON(FILE, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
E.showMenu({
|
||||||
|
"" : { "title" : "Desktop launcher" },
|
||||||
|
"< Back" : () => back(),
|
||||||
|
'Show clocks': {
|
||||||
|
value: settings.showClocks,
|
||||||
|
format: v => v?"On":"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.showClocks = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Show launchers': {
|
||||||
|
value: settings.showLaunchers,
|
||||||
|
format: v => v?"On":"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.showLaunchers = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
|
@ -0,0 +1,42 @@
|
||||||
|
(function(back) {
|
||||||
|
var FILE = "dtlaunch.json";
|
||||||
|
|
||||||
|
var settings = Object.assign({
|
||||||
|
showClocks: true,
|
||||||
|
showLaunchers: true,
|
||||||
|
direct: false
|
||||||
|
}, require('Storage').readJSON(FILE, true) || {});
|
||||||
|
|
||||||
|
function writeSettings() {
|
||||||
|
require('Storage').writeJSON(FILE, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
E.showMenu({
|
||||||
|
"" : { "title" : "Desktop launcher" },
|
||||||
|
"< Back" : () => back(),
|
||||||
|
'Show clocks': {
|
||||||
|
value: settings.showClocks,
|
||||||
|
format: v => v?"On":"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.showClocks = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Show launchers': {
|
||||||
|
value: settings.showLaunchers,
|
||||||
|
format: v => v?"On":"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.showLaunchers = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Direct launch': {
|
||||||
|
value: settings.direct,
|
||||||
|
format: v => v?"On":"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.direct = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
|
@ -3,6 +3,8 @@
|
||||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<p><b>THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE
|
||||||
|
INSTRUCTIONS FOR <a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">BANGLE.JS</a> 1 AND <a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">BANGLE.JS 2</a></b>. For usage on Bangle.js 2 you'll likely need to have an updated bootloader.</p>
|
||||||
<div id="fw-unknown">
|
<div id="fw-unknown">
|
||||||
<p><b>Firmware updates using the App Loader are only possible on
|
<p><b>Firmware updates using the App Loader are only possible on
|
||||||
Bangle.js 2. For firmware updates on Bangle.js 1 please
|
Bangle.js 2. For firmware updates on Bangle.js 1 please
|
||||||
|
|
|
@ -24,3 +24,5 @@
|
||||||
0.22: Respect Quiet Mode
|
0.22: Respect Quiet Mode
|
||||||
0.23: Allow notification dismiss to remove from phone too
|
0.23: Allow notification dismiss to remove from phone too
|
||||||
0.24: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799)
|
0.24: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799)
|
||||||
|
0.25: workaround call notification
|
||||||
|
Fix inflated step number
|
||||||
|
|
|
@ -184,7 +184,7 @@
|
||||||
case "call":
|
case "call":
|
||||||
var note = { size: 55, title: event.name, id: "call",
|
var note = { size: 55, title: event.name, id: "call",
|
||||||
body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))}
|
body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))}
|
||||||
if (event.cmd === "incoming") {
|
if (event.cmd === "incoming" || event.cmd === "") {
|
||||||
require("notify").show(note);
|
require("notify").show(note);
|
||||||
if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
|
if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
|
||||||
Bangle.buzz();
|
Bangle.buzz();
|
||||||
|
@ -262,7 +262,7 @@
|
||||||
// Send a summary of activity to Gadgetbridge
|
// Send a summary of activity to Gadgetbridge
|
||||||
function sendActivity(hrm) {
|
function sendActivity(hrm) {
|
||||||
var steps = currentSteps - lastSentSteps;
|
var steps = currentSteps - lastSentSteps;
|
||||||
lastSentSteps = 0;
|
lastSentSteps = currentSteps;
|
||||||
gbSend({ t: "act", stp: steps, hrm:hrm });
|
gbSend({ t: "act", stp: steps, hrm:hrm });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: New App!
|
||||||
|
0.02: multiple player score support
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Golf Score
|
||||||
|
|
||||||
|
Lets you keep track of strokes during a game of Golf.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Open the app,
|
||||||
|
1. scroll to setup
|
||||||
|
2. set the number of holes (18 by default, but can be configured)
|
||||||
|
3. set the number of players (4 by default, but can be 1-20)
|
||||||
|
4. click back
|
||||||
|
5. scroll to a hole (hole 1)
|
||||||
|
6. scroll to a player and set the number of strokes they took (repeat as needed)
|
||||||
|
7. click next hole and repeat #6 and #7 as needed; or click back
|
||||||
|
8. at any time, check the score card for a sum total of all the strokes for each player
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Track strokes for multiple players (1-20)
|
||||||
|
Set number of holes on course
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
N/A
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
Michael Salaverry (github.com/barakplasma)
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Michael Salaverry
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwIEBgOABQcD4AFDg1wAokYDokOAokDDwkBDwkADwn4nAFD/geDgP8gYFEDwn8gFgDocA+AFCkE/A4IABg//Aoc//4RDn/+Goc/8AFJj4FLEQYFGh4FLIAYFGg4FKh5sBApEfnhTEAok+Aol8vihEAon4AocB+F4ZQYFF8AFDg/AAocPAouAKYcfXQQFHjzEEhjvDA"))
|
|
@ -0,0 +1,113 @@
|
||||||
|
// @ts-check
|
||||||
|
// @ts-ignore
|
||||||
|
const menu = require("graphical_menu");
|
||||||
|
/**
|
||||||
|
* @type {{showMenu: (config) => void}}
|
||||||
|
*/
|
||||||
|
let E;
|
||||||
|
/**
|
||||||
|
* @type {{clear: () => void}}
|
||||||
|
*/
|
||||||
|
let g;
|
||||||
|
|
||||||
|
let holes_count = 18;
|
||||||
|
let player_count = 4;
|
||||||
|
/**
|
||||||
|
* @type {number[][]}
|
||||||
|
*/
|
||||||
|
let course = new Array(holes_count).map(() => new Array(player_count).fill(0));
|
||||||
|
|
||||||
|
const main_menu = {
|
||||||
|
"": {
|
||||||
|
"title": "-- Golf --"
|
||||||
|
},
|
||||||
|
"Setup": function () { E.showMenu(setup_menu); },
|
||||||
|
"Score Card": function () {
|
||||||
|
calculate_score();
|
||||||
|
E.showMenu(score_card);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculate_score() {
|
||||||
|
let scores = course.reduce((acc, hole) => {
|
||||||
|
hole.forEach((stroke_count, player) => {
|
||||||
|
acc[player] = acc[player]+stroke_count;
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, new Array(player_count).fill(0));
|
||||||
|
|
||||||
|
score_card = {
|
||||||
|
"": {
|
||||||
|
"title": "score card"
|
||||||
|
},
|
||||||
|
"< Back": function () { E.showMenu(main_menu); },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let player = 0; player < player_count; player++) {
|
||||||
|
score_card["Player - " + (player + 1)] = {
|
||||||
|
value: scores[player]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let score_card = {};
|
||||||
|
|
||||||
|
const setup_menu = {
|
||||||
|
"": {
|
||||||
|
"title": "-- Golf Setup --"
|
||||||
|
},
|
||||||
|
"Holes": {
|
||||||
|
value: holes_count,
|
||||||
|
min: 1, max: 20, step: 1, wrap: true,
|
||||||
|
onchange: v => { holes_count = v; add_holes(); }
|
||||||
|
},
|
||||||
|
"Players": {
|
||||||
|
value: player_count,
|
||||||
|
min: 1, max: 10, step: 1, wrap: true,
|
||||||
|
onchange: v => { player_count = v; }
|
||||||
|
},
|
||||||
|
"< Back": function () { E.showMenu(main_menu); },
|
||||||
|
};
|
||||||
|
|
||||||
|
function inc_hole(i, player) { return function (v) { course[i][player] = v; }; }
|
||||||
|
|
||||||
|
function add_holes() {
|
||||||
|
for (let j = 0; j < 20; j++) {
|
||||||
|
delete main_menu["Hole - " + (j + 1)];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < holes_count; i++) {
|
||||||
|
course[i] = new Array(player_count).fill(0);
|
||||||
|
main_menu["Hole - " + (i + 1)] = goto_hole_menu(i);
|
||||||
|
}
|
||||||
|
E.showMenu(main_menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goto_hole_menu(i) {
|
||||||
|
return function () {
|
||||||
|
E.showMenu(hole_menu(i));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hole_menu(i) {
|
||||||
|
let menu = {
|
||||||
|
"": {
|
||||||
|
"title": `-- Hole ${i + 1}--`
|
||||||
|
},
|
||||||
|
"Next hole": goto_hole_menu(i + 1),
|
||||||
|
"< Back": function () { E.showMenu(main_menu); },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let player = 0; player < player_count; player++) {
|
||||||
|
menu[`player - ${player + 1}`] = {
|
||||||
|
value: course[i][player],
|
||||||
|
min: 1, max: 20, step: 1, wrap: true,
|
||||||
|
onchange: inc_hole(i, player)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
g.clear();
|
||||||
|
add_holes();
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
|
@ -2,3 +2,4 @@
|
||||||
0.03: Show number of satellites while waiting for fix
|
0.03: Show number of satellites while waiting for fix
|
||||||
0.04: Add Maidenhead readout of GPS location
|
0.04: Add Maidenhead readout of GPS location
|
||||||
0.05: Refactor to use 'layout' library for multi-device support
|
0.05: Refactor to use 'layout' library for multi-device support
|
||||||
|
0.06: Added number of satellites in view and fixed crash with GPS time
|
||||||
|
|