1
0
Fork 0

Merge remote-tracking branch 'upstream/master' into wohrm-bjs2

master
Adam Schmalhofer 2022-01-09 18:54:45 +01:00
commit 4343e9ba6d
281 changed files with 10766 additions and 1248 deletions

513
apps.json
View File

@ -2,7 +2,7 @@
{
"id": "fwupdate",
"name": "Firmware Update",
"version": "0.02",
"version": "0.03",
"description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates",
"icon": "app.png",
"type": "RAM",
@ -16,7 +16,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.39",
"version": "0.40",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
@ -77,7 +77,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.14",
"version": "0.17",
"description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png",
"type": "app",
@ -116,7 +116,7 @@
{
"id": "ios",
"name": "iOS Integration",
"version": "0.07",
"version": "0.08",
"description": "Display notifications/music/etc from iOS devices",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",
@ -167,7 +167,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.38",
"version": "0.40",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
@ -768,7 +768,7 @@
"id": "recorder",
"name": "Recorder (BETA)",
"shortName": "Recorder",
"version": "0.04",
"version": "0.06",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",
@ -845,7 +845,7 @@
{
"id": "weather",
"name": "Weather",
"version": "0.13",
"version": "0.15",
"description": "Show Gadgetbridge weather report",
"icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}],
@ -936,7 +936,7 @@
"id": "widbatpc",
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"version": "0.14",
"version": "0.16",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"icon": "widget.png",
"type": "widget",
@ -971,7 +971,7 @@
{
"id": "widbt",
"name": "Bluetooth Widget",
"version": "0.07",
"version": "0.08",
"description": "Show the current Bluetooth connection status in the top right of the clock",
"icon": "widget.png",
"type": "widget",
@ -1040,16 +1040,19 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.01",
"version": "0.02",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "boot",
"type": "app",
"tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true}
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"}
]
},
{
@ -1148,7 +1151,7 @@
{
"id": "qrcode",
"name": "Custom QR Code",
"version": "0.04",
"version": "0.05",
"description": "Use this to upload a customised QR code to Bangle.js",
"icon": "app.png",
"tags": "qrcode",
@ -1324,7 +1327,7 @@
"icon": "gesture.png",
"type": "app",
"tags": "gesture,ai",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"gesture.app.js","url":"gesture.js"},
{"name":".tfnames","url":"gesture-tfnames.js","evaluate":true},
@ -1501,7 +1504,7 @@
{
"id": "gpsinfo",
"name": "GPS Info",
"version": "0.05",
"version": "0.08",
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
@ -1590,7 +1593,7 @@
{
"id": "widpedom",
"name": "Pedometer widget",
"version": "0.20",
"version": "0.22",
"description": "Daily pedometer widget",
"icon": "widget.png",
"type": "widget",
@ -1750,8 +1753,9 @@
"icon": "grocery.png",
"type": "app",
"tags": "tool,outdoors,shopping,list",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"custom": "grocery.html",
"allow_emulator": true,
"storage": [
{"name":"grocery.app.js","url":"app.js"},
{"name":"grocery.img","url":"grocery-icon.js","evaluate":true}
@ -2028,7 +2032,7 @@
"id": "chronowid",
"name": "Chrono Widget",
"shortName": "Chrono Widget",
"version": "0.04",
"version": "0.05",
"description": "Chronometer (timer) which runs as widget.",
"icon": "app.png",
"tags": "tool,widget",
@ -2185,7 +2189,7 @@
"id": "calculator",
"name": "Calculator",
"shortName": "Calculator",
"version": "0.04",
"version": "0.05",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}],
@ -2430,7 +2434,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.04",
"version": "0.06",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
@ -2971,11 +2975,11 @@
{
"id": "cprassist",
"name": "CPR Assist",
"version": "0.01",
"version": "0.02",
"description": "Provides assistance while performing a CPR",
"icon": "cprassist-icon.png",
"tags": "tool,firstaid",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"screenshots": [{"url":"bangle1-CPR-assist-screenshot.png"}],
@ -3533,7 +3537,7 @@
"id": "mclockplus",
"name": "Morph Clock+",
"shortName": "Morph Clock+",
"version": "0.02",
"version": "0.03",
"description": "Morphing Clock with more readable seconds and date and additional stopwatch",
"icon": "mclockplus.png",
"type": "clock",
@ -3987,11 +3991,12 @@
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"doztime.app.js","url":"app.js"},
{"name":"doztime.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]},
{"name":"doztime.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]},
{"name":"doztime.img","url":"app-icon.js","evaluate":true}
]
},
@ -4065,7 +4070,7 @@
{
"id": "hcclock",
"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.",
"icon": "hcclock-icon.png",
"type": "clock",
@ -4211,13 +4216,13 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"version": "0.09",
"description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"version": "0.10",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"icon": "pastel.png",
"dependencies": {"mylocation":"app", "widpedom":"app"},
"screenshots": [{"url":"screenshot_pastel.png"}],
"dependencies": {"mylocation":"app", "widpedom":"app","weather":"app"},
"screenshots": [{"url":"screenshot_pastel.png"}, {"url":"weather_icons.png"}],
"type": "clock",
"tags": "clock",
"tags": "clock, weather, tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
@ -4238,8 +4243,9 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.03",
"description": "A simple clock using the bold Anton font.",
"version": "0.04",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
@ -4248,8 +4254,10 @@
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"antonclk.json"}]
},
{
"id": "waveclk",
@ -4383,7 +4391,7 @@
{
"id": "gpstouch",
"name": "GPS Touch",
"version": "0.01",
"version": "0.02",
"description": "A touch based GPS watch, shows OS map reference",
"icon": "gpstouch.png",
"screenshots": [{"url":"screenshot4.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot1.png"}],
@ -4473,7 +4481,7 @@
"name": "A Battery Widget (with percentage)",
"shortName":"A Battery Widget",
"icon": "widget.png",
"version":"1.02",
"version":"1.03",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
@ -4488,7 +4496,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.07",
"version":"0.09",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
@ -4622,7 +4630,7 @@
"shortName":"93 Dub",
"icon": "93dub.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.05",
"version":"0.06",
"description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo",
"tags": "clock",
"type": "clock",
@ -4653,7 +4661,7 @@
"id": "sensible",
"name": "SensiBLE",
"shortName": "SensiBLE",
"version": "0.04",
"version": "0.05",
"description": "Collect, display and advertise real-time sensor data.",
"icon": "sensible.png",
"screenshots": [
@ -4698,6 +4706,8 @@
"tags": "tool,timer",
"readme":"README.md",
"supports":["BANGLEJS2"],
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
"allow_emulator": true,
"storage": [
{"name":"a_speech_timer.app.js","url":"app.js"},
{"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true}
@ -4709,7 +4719,7 @@
"icon": "mylocation.png",
"type": "app",
"screenshots": [{"url":"screenshot_1.png"}],
"version":"0.01",
"version":"0.02",
"description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README",
"readme": "README.md",
"tags": "tool,utility",
@ -4726,7 +4736,7 @@
"id": "pebble",
"name": "Pebble Clock",
"shortName": "Pebble",
"version": "0.06",
"version": "0.07",
"description": "A pebble style clock to keep the rebellion going",
"dependencies": {"widpedom":"app"},
"readme": "README.md",
@ -4734,7 +4744,7 @@
"screenshots": [{"url":"pebble_screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"pebble.app.js","url":"pebble.app.js"},
{"name":"pebble.settings.js","url":"pebble.settings.js"},
@ -4768,7 +4778,7 @@
"screenshots": [{"url":"screenshot_widbata_1.png"}],
"version":"0.01",
"type": "widget",
"supports": ["BANGLEJS2"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Shows the current battery level status in the top right using the clocks colour theme",
"tags": "widget,battery",
@ -4779,7 +4789,7 @@
{
"id": "weatherClock",
"name": "Weather Clock",
"version": "0.04",
"version": "0.05",
"description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).",
"icon": "app.png",
"screenshots": [{"url":"screens/screen1.png"}],
@ -4839,6 +4849,25 @@
{"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",
"name": "Scribble",
"shortName":"Scribble",
@ -4862,7 +4891,7 @@
"id": "ptlaunch",
"name": "Pattern Launcher",
"shortName": "Pattern Launcher",
"version": "0.11",
"version": "0.13",
"description": "Directly launch apps from the clock screen with custom patterns.",
"icon": "app.png",
"screenshots": [{"url":"manage_patterns_light.png"}],
@ -4876,11 +4905,25 @@
],
"data": [{"name":"ptlaunch.patterns.json"}]
},
{ "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",
"version": "0.04",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
@ -4932,10 +4975,12 @@
"id":"awairmonitor",
"name":"Awair Monitor",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"allow_emulator": true,
"version":"0.01",
"version":"0.03",
"description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.",
"tags": "tool,health",
"type": "clock",
"tags": "clock,tool,health",
"readme":"README.md",
"supports":["BANGLEJS2"],
"storage": [
@ -4946,7 +4991,7 @@
{ "id": "pooqround",
"name": "pooq Round watch face",
"shortName":"pooq Round",
"version":"0.01",
"version":"0.02",
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
"icon": "app.png",
"type": "clock",
@ -4964,8 +5009,8 @@
},
{
"id": "coretemp",
"name": "Core Temp Display",
"version": "0.01",
"name": "CoreTemp",
"version": "0.02",
"description": "Display CoreTemp device sensor data",
"icon": "coretemp.png",
"type": "app",
@ -4973,10 +5018,14 @@
"readme": "README.md",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"coretemp.boot.js","url":"boot.js"},
{"name":"coretemp.wid.js","url":"widget.js"},
{"name":"coretemp.app.js","url":"coretemp.js"},
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true}
]
{"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",
@ -5030,9 +5079,10 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.02",
"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",
@ -5048,6 +5098,22 @@
{"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",
@ -5063,5 +5129,342 @@
{"name":"ltherm.app.js","url":"app.js"},
{"name":"ltherm.img","url":"icon.js","evaluate":true}
]
},
{
"id": "presentor",
"name": "Presentor",
"version": "3.0",
"description": "Use your Bangle to present!",
"icon": "app.png",
"type": "app",
"tags": "tool,bluetooth",
"interface": "interface.html",
"readme":"README.md",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"presentor.app.js","url":"app.js"},
{"name":"presentor.img","url":"app-icon.js","evaluate":true},
{"name":"presentor.json","url":"settings.json"}
]
},
{
"id": "slash",
"name": "Slash Watch",
"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.02",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png",
"type": "boot",
"tags": "system",
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"pro-menu-screenshot.png"}],
"storage": [
{"name":"promenu.boot.js","url":"boot.js","supports": ["BANGLEJS"]},
{"name":"promenu.boot.js","url":"bootb2.js","supports": ["BANGLEJS2"]},
{"name":"promenu.img","url":"promenuIcon.js","evaluate":true}
]
},
{
"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 }
]
},
{
"id": "sonicclk",
"name": "Sonic Clock",
"version": "1.01",
"description": "A classic sonic clock featuring run, stop and wait animations.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"sonicclk.app.js","url":"app.js"},
{"name":"sonicclk.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "touchmenu",
"name": "TouchMenu",
"version": "0.01",
"description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2",
"screenshots": [{"url":"touchmenu.gif"}],
"icon": "touchmenu.png",
"type": "bootloader",
"tags": "tool",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"touchmenu.boot.js","url":"touchmenu.boot.js"}
]
},
{
"id": "puzzle15",
"name": "15 puzzle",
"version": "0.05",
"description": "A 15 puzzle game with drag gesture interface",
"readme":"README.md",
"icon": "puzzle15.app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "game",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"puzzle15.app.js","url":"puzzle15.app.js"},
{"name":"puzzle15.settings.js","url":"puzzle15.settings.js"},
{"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true}
],
"data": [{"name":"puzzle15.json"}]
},
{
"id": "flipper",
"name": "flipper",
"version": "0.01",
"description": "Switch between dark and light theme and vice versa, combine with pattern launcher and swipe to flip.",
"readme":"README.md",
"screenshots": [{"url":"flipper.png"}],
"icon": "flipper.png",
"type": "app",
"tags": "game",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"flipper.app.js","url":"flipper.app.js"},
{"name":"flipper.img","url":"flipper.icon.js","evaluate":true}
]
},
{ "id": "ruuviwatch",
"name": "Ruuvi Watch",
"shortName":"Ruuvi Watch",
"icon": "ruuviwatch.png",
"version":"1.01",
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.",
"readme":"README.md",
"tags": "bluetooth",
"supports": ["BANGLEJS"],
"storage": [
{"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"},
{"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true}
]
}
]

View File

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

View File

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

4
apps/andark/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: Release
0.02: Rename app
0.03: Add type "clock"
0.04: changed update cylce, when locked

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

@ -0,0 +1,10 @@
# Analog Clock
## Features
* second hand
* date
* battery percantage
* no widgets
![logo](andark_screen.png)

BIN
apps/andark/andark_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

125
apps/andark/app.js Normal file
View File

@ -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");

1
apps/andark/app_icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIEBoUAiAKCgUCBQUEColEAYUQhAmKCwgeCAAcCgEDjwEBkEAg8TBocNgYFDh8GAYMDxkPjEA8EAwkHJgIcBAoPfAoYWCBYYFIgfvAoX4FYRJEAp9gAomYNAOAArPwAogAC4AFiRoIFJLgIFJuADCg//Q4U//4FDj4FEAAV4Aoi0CSxBsCA=="))

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide
0.03: Clock now shows day of week under date.
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.

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

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 1.6 KiB

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

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

View File

@ -1 +1,3 @@
0.01: 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)

View File

@ -5,11 +5,10 @@ Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awai
* 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 retrive the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection
* 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
* Open awair_to_bangle.html with a text/code editor and input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx")
* Launch the Awair Monitor app on your BangleJS
* Open awair_to_bangle.html on Chrome and click "Connect BangleJS" - it connects to your watch the same way as the Bangle app store
* 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
![](screenshot.png)

View File

@ -30,6 +30,8 @@ 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());
@ -47,14 +49,8 @@ function draw() {
g.drawString("Humi", 125, 100);
g.drawString("Temp", 160, 100);
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);
if (last_update != bt_last_update) {
display_frozen = false;
last_update = bt_last_update;
internal_last_update = last_update;
if (last_update % 10 == 0) {
@ -65,16 +61,29 @@ function draw() {
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) {
} 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++) {
// max height = 32
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);
@ -91,6 +100,7 @@ function draw() {
}
// init
Bangle.setUI("clock");
require("FontHaxorNarrow7x17").add(Graphics);
g.clear();
Bangle.loadWidgets();

View File

@ -7,15 +7,15 @@
// 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_ip_1 = "192.168.2.2"; // <- INPUT YOUR AWAIR IP ADDRESS HERE
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;
window.onload = function() {
function initChart() {
var chart_co2;
var chart_voc;
var chart_pm;
@ -23,6 +23,8 @@ window.onload = function() {
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){
@ -105,11 +107,12 @@ window.onload = function() {
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.isOpen) {
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.isOpen) {
} else if (is_connected && bt_connection && !bt_connection.isOpen) {
console.log("Disconnected - Next attempt to reconnect in " + reconnect_counter);
reconnect_counter--;
@ -131,7 +134,6 @@ window.onload = function() {
}
}
setTimeout(function(){updateChart()}, 1000);
});
}
@ -148,10 +150,16 @@ function connectBT() {
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;
@ -167,23 +175,21 @@ function disconnectBT() {
<p style="color: #F7FAFC">
<b>How to use</b>
<br/><br/>
<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: Modify this HTML file to input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx")
<br/><br/>
Step 3: Launch the Awair Monitor app on your BangleJS
<br/><br/>
Step 4: Click "Connect BangleJS"
<br/><br/>
Step 5: Optionally, open the web inspector's console (Right click > Inspector > Console) to read the bluetooth logs
<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 &gt; Inspector &gt; 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>
<center>
<button onclick="connectBT();">Connect BangleJS</button>
<button onclick="disconnectBT();">Disconnect BangleJS</button>
</center>
<br/><br/>
<div id="chartContainer_co2" style="height: 300px; max-width: 920px; margin: 0px auto; margin-bottom: 64px;"></div>
@ -192,4 +198,476 @@ Step 5: Optionally, open the web inspector's console (Right click > Inspector >
<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>
</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_

View File

@ -68,7 +68,7 @@ ${track.map(pt=>` <gx:value>${pt.distance}</gx:value>\n`).join("")}
function saveGPX(track, title) {
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>
<time>${track[0].date.toISOString()}</time>
</metadata>

View File

@ -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}
]
}

View File

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

View File

@ -6,11 +6,11 @@ var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var boot = "";
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`;
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} else {
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/));
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`;
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
}
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
boot += `E.setFlags({pretokenise:1});\n`;

View File

@ -1 +1,4 @@
0.01: New App!
0.02: Make overriding the HRM event optional
Emit BTHRM event for external sensor
Add recorder app plugin

View File

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

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

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

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

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

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

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

View File

@ -2,3 +2,4 @@
0.02: fix precision rounding issue + no reset when equals pressed
0.03: Support for different screen sizes and touchscreen
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)

View File

@ -8,7 +8,7 @@
g.clear();
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 RESULT_HEIGHT = 40;
var COLORS = {
@ -18,97 +18,45 @@ var COLORS = {
SPECIAL: ['#65686C', '#7F8183']
};
var keys = {
'0': {
xy: [0, 200, 120, 240],
trbl: '2.00'
},
'.': {
xy: [120, 200, 180, 240],
trbl: '3=.0'
},
'=': {
xy: [181, 200, 240, 240],
trbl: '+==.',
color: COLORS.OPERATOR
},
'1': {
xy: [0, 160, 60, 200],
trbl: '4201'
},
'2': {
xy: [60, 160, 120, 200],
trbl: '5301'
},
'3': {
xy: [120, 160, 180, 200],
trbl: '6+.2'
},
'+': {
xy: [181, 160, 240, 200],
trbl: '-+=3',
color: COLORS.OPERATOR
},
'4': {
xy: [0, 120, 60, 160],
trbl: '7514'
},
'5': {
xy: [60, 120, 120, 160],
trbl: '8624'
},
'6': {
xy: [120, 120, 180, 160],
trbl: '9-35'
},
'-': {
xy: [181, 120, 240, 160],
trbl: '*-+6',
color: COLORS.OPERATOR
},
'7': {
xy: [0, 80, 60, 120],
trbl: 'R847'
},
'8': {
xy: [60, 80, 120, 120],
trbl: 'N957'
},
'9': {
xy: [120, 80, 180, 120],
trbl: '%*68'
},
'*': {
xy: [181, 80, 240, 120],
trbl: '/*-9',
color: COLORS.OPERATOR
},
'R': {
xy: [0, 40, 60, 79],
trbl: 'RN7R',
color: COLORS.SPECIAL,
val: 'AC'
},
'N': {
xy: [60, 40, 120, 79],
trbl: 'N%8R',
color: COLORS.SPECIAL,
val: '+/-'
},
'%': {
xy: [120, 40, 180, 79],
trbl: '%/9N',
color: COLORS.SPECIAL
},
'/': {
xy: [181, 40, 240, 79],
trbl: '//*%',
color: COLORS.OPERATOR
}
var KEY_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()];
var screen, screenColor;
var globalGrid = [4, 5];
var swipeEnabled;
var numbersGrid = [3, 4];
var numbers = {
'0': {grid: [1, 3], globalGrid: [1, 4], trbl: '2.00'},
'.': {grid: [2, 3], globalGrid: [2, 4], trbl: '3=.0'},
'1': {grid: [0, 2], globalGrid: [0, 3], trbl: '4201'},
'2': {grid: [1, 2], globalGrid: [1, 3], trbl: '5301'},
'3': {grid: [2, 2], globalGrid: [2, 3], trbl: '6+.2'},
'4': {grid: [0, 1], globalGrid: [0, 2], trbl: '7514'},
'5': {grid: [1, 1], globalGrid: [1, 2], trbl: '8624'},
'6': {grid: [2, 1], globalGrid: [2, 2], trbl: '9-35'},
'7': {grid: [0, 0], globalGrid: [0, 1], trbl: 'R847'},
'8': {grid: [1, 0], globalGrid: [1, 1], trbl: 'N957'},
'9': {grid: [2, 0], globalGrid: [2, 1], trbl: '%*68'},
};
var selected = DEFAULT_SELECTION;
var prevSelected = DEFAULT_SELECTION;
var operatorsGrid = [2, 3];
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 currNumber = null;
var operator = null;
@ -116,6 +64,27 @@ var results = null;
var isDecimal = 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) {
var rMargin = 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);
}
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) {
var xStr = x.toString();
var xRadix = xStr.indexOf('.');
@ -218,8 +237,8 @@ function displayOutput(num) {
hasPressedEquals = false;
prevNumber = null;
operator = null;
keys.R.val = 'AC';
drawKey('R', keys.R);
specials.R.val = 'AC';
if (!swipeEnabled) drawKey('R', specials.R);
g.setFont('Vector', 22);
} else {
// might not be a number due to display of dot "."
@ -299,12 +318,12 @@ function buttonPress(val) {
results = null;
isDecimal = false;
hasPressedEquals = false;
if (keys.R.val == 'AC') {
if (specials.R.val == 'AC') {
prevNumber = null;
operator = null;
} else {
keys.R.val = 'AC';
drawKey('R', keys.R, true);
specials.R.val = 'AC';
drawKey('R', specials.R, true);
}
wasPressedEquals = false;
hasPressedNumber = false;
@ -331,10 +350,11 @@ function buttonPress(val) {
case '+':
calculatorLogic(val);
hasPressedNumber = false;
if (swipeEnabled) drawNumbers();
break;
case '.':
keys.R.val = 'C';
drawKey('R', keys.R);
specials.R.val = 'C';
if (!swipeEnabled) drawKey('R', specials.R);
isDecimal = true;
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
break;
@ -348,8 +368,8 @@ function buttonPress(val) {
hasPressedNumber = false;
break;
default:
keys.R.val = 'C';
drawKey('R', keys.R);
specials.R.val = 'C';
if (!swipeEnabled) drawKey('R', specials.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
if (isDecimal) {
currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
@ -367,23 +387,31 @@ function buttonPress(val) {
}
function moveDirection(d) {
drawKey(selected, keys[selected]);
drawKey(selected, screen[selected]);
prevSelected = selected;
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d];
drawKey(selected, keys[selected], true);
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : screen[selected].trbl[d];
drawKey(selected, screen[selected], true);
}
if (global.BTN4) {
if (process.env.HWVERSION==1) {
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
swipeEnabled = false;
drawGlobal();
} else { // touchscreen?
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)=>{
for (var key in keys) {
var r = keys[key].xy;
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] &&
e.x<r[2] && e.y<r[3]) {
//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);

View File

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

View File

@ -110,10 +110,19 @@ function drawCalendar(date) {
g.clearRect(0, 0, maxX, maxY);
g.setBgColor(bgColorMonth);
g.clearRect(0, 0, maxX, headerH);
g.setBgColor(bgColorDow);
g.clearRect(0, headerH, maxX, headerH + rowH);
g.setBgColor(bgColorWeekend);
g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
if (settings.startOnSun){
g.setBgColor(bgColorWeekend);
g.clearRect(0, headerH + rowH, colW, maxY);
g.setBgColor(bgColorDow);
g.clearRect(0, headerH, maxX, headerH + rowH);
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);
}
for (let y = headerH; y < maxY; y += rowH) {
g.drawLine(0, y, maxX, y);
}
@ -197,6 +206,8 @@ function drawCalendar(date) {
y2 - 1
);
}
require("Font8x12").add(Graphics);
g.setFont("8x12", fontSize);
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
g.drawString(
(day > 50 ? day - 50 : day).toString(),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -3,3 +3,4 @@
0.03: Display only minutes:seconds when less than 1 hour left
0.04: Change to 7 segment font, move to top widget bar
Better auto-update behaviour, less RAM used
0.05: Fix error running app on new firmwares (fix #1140)

View File

@ -36,13 +36,7 @@ E.on('kill', () => {
function showMenu() {
const timerMenu = {
'': {
'title': 'Set timer',
'predraw': function() {
timerMenu.hours.value = settingsChronowid.hours;
timerMenu.minutes.value = settingsChronowid.minutes;
timerMenu.seconds.value = settingsChronowid.seconds;
timerMenu.started.value = settingsChronowid.started;
}
'title': 'Set timer'
},
'< Back' : ()=>{load();},
'Reset values': function() {

View File

@ -1,2 +1,3 @@
0.01: New clock
0.02: Fix icon & add battery warn functionality
0.03: Theming support & minor fixes

View File

@ -13,6 +13,8 @@ It shows besides time, date and day of week the following information:
## TODO
* Show weather information
* Configure which information to show in each circle
* Configure visibility of widgets
## Creator
Marco ([myxor](https://github.com/myxor))

View File

@ -7,19 +7,23 @@ const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbB
const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI"));
const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA"));
const SETTINGS_FILE = "circlesclock.json";
let settings;
function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {
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 = '#fff';
const colorBg = '#000';
const colorFg = g.theme.dark ? '#fff' : '#000';
const colorBg = g.theme.dark ? '#000' : '#fff';
const colorGrey = '#808080';
const colorRed = '#ff0000';
const colorGreen = '#00ff00';
@ -73,7 +77,7 @@ function drawSteps() {
g.setColor(colorGrey);
g.fillCircle(w1, h3, radiusOuter);
const stepGoal = settings.stepGoal;
const stepGoal = settings.stepGoal || 10000;
if (stepGoal > 0) {
let percent = steps / stepGoal;
if (stepGoal < steps) percent = 1;
@ -97,8 +101,9 @@ function drawHeartRate() {
g.setColor(colorGrey);
g.fillCircle(w2, h3, radiusOuter);
if (hrtValue != undefined) {
const percent = hrtValue / settings.maxHR;
if (hrtValue != undefined && hrtValue > 0) {
const minHR = 40;
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
drawGauge(w2, h3, percent, colorRed);
}
@ -156,25 +161,26 @@ 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);
var endrot = startrot - ((end - offset) * percent) - 15;
g.setColor(color);
const size = 4;
// draw gauge
for (i = startrot; i > endrot; i -= 4) {
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, 4);
g.fillCircle(x, y, size);
}
}
@ -201,6 +207,10 @@ function getSteps() {
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
Bangle.setHRMPower(1, "watch");
if (hrtValue == undefined) {
hrtValue = '...';
drawHeartRate();
}
} else {
Bangle.setHRMPower(0, "watch");
}
@ -218,6 +228,10 @@ Bangle.on('HRM', function(hrm) {
//}
});
Bangle.on('charging', function(charging) {
drawBattery();
});
g.clear();
Bangle.loadWidgets();
/*
@ -225,9 +239,11 @@ Bangle.loadWidgets();
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
if (typeof WIDGETS === "object") {
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
}
}
loadSettings();
setInterval(draw, 60000);

View File

@ -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).
![](app-screenshot.png)
This clock also acts as an example for the building blocks found in the author's
[GitHub repository](https://github.com/rozek/banglejs-2-activities)
## License ##
[MIT License](LICENSE)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgZC/AB0BkmCCBsShEAiFBkARLwEBkGSgEECBQdBCIMSAwMYCBEKCgeQgE2gFgCA0C6Moiw1Dk0AhoRGikACIIHDgzECCI5ECg/gEYOACI+ggMti2ACIUkCIImCABARCAAMNCYIADgu0hdACI1twHACQm0rdoCI0BEYsoilRCI9sUgkBoG2rY1JgYjDCINLCI4fCa5ARHAAggCfYIjLgUB0AECCIy7BFwUCR4IKChIRFm1ACJAjGgwRL+AFDiwREI4YABn41FI4hxFn6IJPoh1B/AQFUI4ABh4RGUIsEyARC4ALEwAjECIl/CIkECIsICId+EQkkwEIA4gRDAAojBLwwHFexAADhaFDgETBw6UChdgA4cbCIKuGggCBCIMDCIkQCI8BEwMbCgMSAQIRGgGQQoQRCEYJrGAAMGIgZKDmBzIjARFTwpuHAARoGAAsMwQVCzARLAAPbtq5KAH4AEA"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

247
apps/colorful_clock/app.js Normal file
View File

@ -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');

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

21
apps/colorwheel/LICENSE Normal file
View File

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

12
apps/colorwheel/README.md Normal file
View File

@ -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
![](BangleJS2-ColorWheel-2.png)
![](BangleJS2-ColorWheel-3.png)
Please note: you may also tap outside the wheel (for black) or inside it (for white).
## License ##
[MIT License](LICENSE)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgYtr4cEiAQMku27ckyVICBUDlmy5YRBpMkCBEE7dtEYYRBwARHm3LEY3QCA0BEAIjFk3boARFhoOBEYs06dNCIogCEYoHCNAoOCEYlNEYUgCIcbEZekCIYODtgjHmgRHtu3///yQrESQfTBIYQBAAPNEYU2SQUGEYd/CIf9EYYRCDAYRF/4KBCIioBAwPfCAn/+wKC7QjEmgRG/xADZAIyBAwIQFCIgjFmoRKEYL4DRgQAFGoojKCIoje7Nly1ZEYLzCkojLNYIRCNZAjIkm/EZ4RH/1ZEYYRMWYpZDy4jJrARBggRBlrYG+VJEYgRBDIVfCIgtCy1QCIZhDCAfkKIW24ARBgJeBEYNbvoQBvOkCIQjDgE2EYYCD2gRCyQQCgEGEYYRBzVp0wRCyAREEY+2CIWAEY4OCEYoQDAAMbEY/SpMgCAkCjIjHzVJEQoABWYIjF7VICI8BBwYjDe4IAHSQ3QCBBuBLgQjCCBIAChu26dMCBgAdA"))

80
apps/colorwheel/app.js Normal file
View File

@ -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);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgJC/ABsH4/wv/H/EMlkMsF4hkYmEEwEwg0gmHCwEh4VAmPi/0j8Vkkcj4MjkU8kckocx4UEmPMoUQgkEEYNGnAFBnEGxFwg0Ek/jzFh8UEkEjkOikUcnFH8MiFIM3wnA8PisEwhnAkECAoMc4EYgk///3//n/Cl/AFYA="))

54
apps/contourclock/app.js Normal file
View File

@ -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();

BIN
apps/contourclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1 +1,2 @@
0.01: New app
0.02: Cleanup interface and add settings, widget, add skin temp reporting.

View File

@ -1,19 +1,18 @@
# CoreTemp display
Basic bare-bones example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current body core temperature readings.
Basic example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current skin and body core temperature readings.
## Usage
On startup connects to a CoreTemp device (1809/2A1C) and emits a "Core, temp" value for each reading.
The app simply displays these readings on screen.
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 device selection
* Provide enable/disable option
* Check status, add Retry/reconnect
* Also provide skin temp reading
* Add specific device selection
## Creator

View File

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

View File

@ -1,23 +1,114 @@
//
// If enabled in settings run constantly in background
//
(function() {
var gatt;
//Would it be better to scan by uuid rather than name?
NRF.requestDevice({ timeout: 20000, filters: [{ name: 'CORE [a]' }] }).then(function(device) {
return device.gatt.connect();
}).then(function(g) {
gatt = g;
return gatt.getPrimaryService("1809");
}).then(function(service) {
return service.getCharacteristic("2A1C");
}).then(function(characteristic) {
characteristic.on('characteristicvaluechanged', function(event) {
var dv = event.target.value;
var core = (dv.buffer[2]*256+dv.buffer[1])/100;
Bangle.emit('Core',{
temp:core
});
});
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() {
});
})
.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(); });
})();

View File

@ -1,19 +1,66 @@
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
var btm = g.getHeight()-1;
// 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) {
var px = g.getWidth()/2;
g.setFontAlign(0,0);
g.clearRect(0,24,g.getWidth(),80);
var str = c.temp + "C";
g.setFontVector(40).drawString(str,px,45);
// 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);
}
Bangle.on('Core', onCore);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
// Background task will activate once settings are enabled.
function enableSensor() {
settings = require("Storage").readJSON("coretemp.json", 1) || {};
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
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});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

47
apps/coretemp/settings.js Normal file
View File

@ -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);
})

66
apps/coretemp/widget.js Normal file
View File

@ -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();
})()

View File

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

View File

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

1
apps/diract/ChangeLog Normal file
View File

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

28
apps/diract/README.md Normal file
View File

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

View File

@ -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"))

548
apps/diract/diract.js Normal file
View File

@ -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();

BIN
apps/diract/diract.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

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

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

View File

@ -1,8 +1,18 @@
# Desktop style App Launcher
Bangle 1:
![](screenshot.jpg)
In the picture above, the Settings app is selected.
Bangle 2:
![shot1](https://user-images.githubusercontent.com/89286474/146471756-ec6d16de-6916-4fde-b991-ba88c2c8fa1a.png)
![shot2](https://user-images.githubusercontent.com/89286474/146471758-3a9fce80-e047-4c7a-829a-e780c764b921.png)
![shot3](https://user-images.githubusercontent.com/89286474/146471760-5497fd1b-8e82-4fd5-a4e3-4734701a7dbd.png)
## Controls- Bangle
**BTN1** - move backward through app icons on a page
@ -21,4 +31,4 @@ In the picture above, the Settings app is selected.
**Swipe Left** - move to next page of app icons
**Swipe Right** - move to previous page of app icons
**Swipe Right** - move to previous page of app icons

1
apps/flipper/ChangeLog Normal file
View File

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

20
apps/flipper/README.md Normal file
View File

@ -0,0 +1,20 @@
# Flipper
![](flipper.png)
*A utility to switch from the dark to the light theme and vice versa*
* If the current theme is dark it will switch to the light theme
* If the current theme is light it will switch to the dark theme
* Combine with the awesome pattern launcher and it saves loads of time
## Demo Video
There's no screenshot but there is a [demo video.](https://espruino.microco.sm/api/v1/files/9caa1afef7e4cce1d9b518af2dd271f1a57c5ecc.mp4)
## Future Enhancements
* Nothing left to add
Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)

View File

@ -0,0 +1,39 @@
const storage = require('Storage');
let settings = storage.readJSON('setting.json', 1);
function cl(x) { return g.setColor(x).getColor(); }
function upd(th) {
g.theme = th;
settings.theme = th;
storage.write('setting.json', settings);
delete g.reset;
g._reset = g.reset;
g.reset = function(n) { return g._reset().setColor(th.fg).setBgColor(th.bg); };
g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); };
g.clear(1);
}
function flipTheme() {
if (!g.theme.dark) {
upd({
fg:cl("#fff"), bg:cl("#000"),
fg2:cl("#0ff"), bg2:cl("#000"),
fgH:cl("#fff"), bgH:cl("#00f"),
dark:true
});
} else {
upd({
fg:cl("#000"), bg:cl("#fff"),
fg2:cl("#000"), bg2:cl("#cff"),
fgH:cl("#000"), bgH:cl("#0ff"),
dark:false
});
}
}
Bangle.loadWidgets();
Bangle.drawWidgets();
flipTheme();
setTimeout(load, 20);

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X/AAO/mMUzs975K+ggLKysUAYNVqoLFitUoAKBqtQBYkJBIQABqwLEgQLEqtABggJDqkVBaoNCBZQwEgILWgoJENYsVBIcVBYpDEgpSIBYMBKQg6CuogCBY1UgoLCXAQLDqAsDBYhSBqEJHAoLDoEBcQ4LBEwILIMooLdIg4LaVoyaGERLcFao4LIdRAACYYUQBY5RKAH4Ar"))

BIN
apps/flipper/flipper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

View File

@ -2,3 +2,5 @@
0.02: Add support for ZIPs
Find and download ZIPs direct from the Espruino website
Take 'beta' tag off
0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later
Add CRC checks for common bootloaders that we know don't work

View File

@ -60,6 +60,7 @@ function onInit(device) {
document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = "";
}
}
function checkForFileOnServer() {
@ -264,6 +265,8 @@ function createJS_app(binary, startAddress, endAddress) {
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 2v10.219 needs update"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("BOOTLOADER 2v10.236 needs update"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
hexJS += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 2048;
@ -291,20 +294,14 @@ function createJS_bootloader(binary, startAddress, endAddress) {
var chunk = btoa(new Uint8Array(binary.buffer, binary.byteOffset+i, l));
hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n';
}
// hexJS += `\x10(function() {
// if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC!";
// var f = require("Flash");
// for (var i=${startAddress};i<${endAddress};i+=4096) f.erasePage(i);
// f.write(_fw,${startAddress});
// E.reboot();
// })();\n`;
hexJS += `\x10if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += '\x10var f = require("Flash");\n';
hexJS += `\x10(function() { if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += 'E.showMessage("Flashing Bootloader...")\n';
hexJS += 'E.setFlags({unsafeFlash:1})\n';
hexJS += 'var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096)
hexJS += '\x10f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `\x10f.write(_fw,${startAddress});\n`;
// hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
// hexJS += '\x10setTimeout(()=>E.reboot(), 2000);\n';
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += `})()\n`;
}
function fileLoaded() {

View File

@ -2,3 +2,6 @@
0.03: Show number of satellites while waiting for fix
0.04: Add Maidenhead readout of GPS location
0.05: Refactor to use 'layout' library for multi-device support
0.06: Add number of satellites in view and fix crash with GPS time
0.07: Resolve one FIFO_FULL case and exit App with button press
0.08: Leave GPS power switched on on exit (will switch off after 0.5 seconds anyway)

View File

@ -16,13 +16,20 @@ var lastFix = {
time: 0,
satellites: 0
};
var nofix = 0;
var SATinView = 0;
var nofBD = 0;
var nofGP = 0;
var listenerGPSraw = 1;
function formatTime(now) {
var fd = now.toUTCString().split(" ");
var time = fd[4].substr(0, 5);
var date = [fd[0], fd[1], fd[2]].join(" ");
return time + " - " + date;
if (now == undefined) {
return "no GPS time available";
} else {
var fd = now.toUTCString().split(" ");
var time = fd[4].substr(0, 5);
var date = [fd[0], fd[1], fd[2]].join(" ");
return time + " - " + date;
}
}
function getMaidenHead(param1,param2){
var lat=-100.0;
@ -77,9 +84,9 @@ function onGPS(fix) {
{type:"txt", font:"6x8", label:"Waiting for GPS" },
{type:"h", c: [
{type:"txt", font:"10%", label:fix.satellites, pad:2, id:"sat" },
{type:"txt", font:"6x8", pad:3, label:"Satellites" }
{type:"txt", font:"6x8", pad:3, label:"Satellites used" }
]},
{type:"txt", font:"6x8", label:"", id:"progress" }
{type:"txt", font:"6x8", label:"", fillx:true, id:"progress" }
]},{lazy:true});
}
g.clearRect(0,24,g.getWidth(),g.getHeight());
@ -87,7 +94,10 @@ function onGPS(fix) {
}
lastFix = fix;
if (fix.fix) {
nofix = 0;
if (listenerGPSraw == 1) {
Bangle.removeListener('GPS-raw', onGPSraw);
listenerGPSraw = 0;
}
var locale = require("locale");
var satellites = fix.satellites;
var maidenhead = getMaidenHead(fix.lat,fix.lon);
@ -99,13 +109,34 @@ function onGPS(fix) {
layout.sat.label = "Satellites: "+satellites;
layout.maidenhead.label = "Maidenhead: "+maidenhead;
} else {
if (listenerGPSraw == 0) {
Bangle.on('GPS-raw', onGPSraw);
listenerGPSraw = 1;
}
layout.sat.label = fix.satellites;
nofix = (nofix+1) % 4;
layout.progress.label = ".".repeat(nofix) + " ".repeat(4-nofix);
layout.progress.label = "in view: " + SATinView;
}
layout.render();
}
function onGPSraw(nmea) {
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
SATinView = nofBD + nofGP;
}
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.on('GPS', onGPS);
Bangle.on('GPS-raw', onGPSraw);
function exitApp() {
load();
}
setWatch(_=>exitApp(), BTN1);
if (global.BTN2) {
setWatch(_=>exitApp(), BTN2);
setWatch(_=>exitApp(), BTN3);
}

View File

@ -1 +1,2 @@
0.01: First version
0.02: Enchanced contrast of icon image

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA="))
require("heatshrink").decompress(atob("mEw4UA///iADCn+EqoAWqAuJgoLcn/8BZENGwNwBY/VBYNXBY0DJ4fABYoiCEggLDmtX1Wq6tcBYvVrQLB0owCBYdVtQLB1NVBYg6BBQIABHgQLCgIuCGAVABYcNqwtBGIOVJAILFyoCCBY5eBBdo7IgIIB1t6BYJfENZaDB9QKB1aDFBYKbEBYizBrwLB2qnFdwSmCX401cYdUBZTjGfYgHCBZB2BBYhUBAARSBBYhICAAIGCBYkVBQJSCBYpICIwQLFHgQ6CBYo8CHQQLFHgQFDBYsVQIQLHgo6DBY0BHQYLGgY6DBYwAFBbCjDACY"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,2 +1,3 @@
0.01: base code
0.02: saved settings when switching color scheme
0.01: Base code
0.02: Saved settings when switching color scheme
0.03: Added Button 3 opening messages (if app is installed)

View File

@ -5,6 +5,7 @@ A High-contrast, black-on-white or white-on-black clock displaying huge pixel di
## Usage
* BTN 1 switches between the two modes : black-on-white or white-on-black
* BTN 3 opens the messages (if installed, and there are new messages)
* That's it!
## Issues and Requests

View File

@ -129,6 +129,7 @@ function updateTime()
g.setFontAlign(0, -1, 0);
g.drawString(fmtDate(d,mo,y,hour), 120, 120);
}
drawMessages();
}
function drawDigits(x, value)
@ -222,6 +223,55 @@ function flipColors()
setColorScheme(0);
}
//////////////////////////////////////////
//
// MESSAGE HANDLING()
//
let messages_installed = require("Storage").read("messages.app.js") != undefined;
function handleMessages()
{
if(messages_installed && hasMessages() > 0)
{
E.showMessage("Loading Messages...");
load("messages.app.js");
}
}
function hasMessages()
{
if(!messages_installed)
return false;
var messages = require("Storage").readJSON("messages.json",1)||[];
if (messages.some(m=>m.new))
return true;
else
return false;
}
let msg = atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==");
let had_messages = false;
function drawMessages()
{
if(!had_messages && hasMessages()) {
g.setColor(255,255,255);
g.drawImage(msg, 184, 212);
g.setFont("6x8", 2);
g.setFontAlign(0, -1, 0);
g.drawString(">", 224, 216);
had_messages = true;
}
else if (had_messages && !hasMessages())
{
g.setColor(0,0,0);
g.fillRect(180, 210, 240, 240);
had_messages = false;
}
}
//////////////////////////////////////////
//
// MAIN FUNCTION()
@ -238,6 +288,7 @@ setInterval(updateTime, interval);
// Handle Button Press
setWatch(flipColors, BTN1, true);
setWatch(Bangle.showLauncher, BTN2, false);
setWatch(handleMessages, BTN3, true);
// Handle redraw on LCD on / fullscreen notifications dismissed
Bangle.on('lcdPower', (on) => { if(on) redraw(); });

View File

@ -2,9 +2,7 @@
0.02: Remove messages on disconnect
0.03: Handling of message actions (ok/clear)
0.04: Added common bundleId's
0.05: Added more bundleId's (app-id's which can be used to
determine a friendly app name in the notifications)
0.05: Added more bundleId's (app-id's which can be used to determine a friendly app name in the notifications)
0.06: Fix (not) popupping up old messages
0.07: Added more details from music (instead of Undefined)
Added more app identifiers
0.07: Added more details from music (instead of Undefined), added more app identifiers
0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements

View File

@ -66,6 +66,7 @@ E.on('notify',msg=>{
"com.apple.mobilecal": "Calendar",
"com.apple.mobilemail": "Mail",
"com.apple.mobilephone": "Phone",
"com.apple.mobileslideshow": "Pictures",
"com.apple.MobileSMS": "SMS Message",
"com.apple.Passbook": "iOS Wallet",
"com.apple.podcasts": "Podcasts",
@ -83,6 +84,7 @@ E.on('notify',msg=>{
"com.ifttt.ifttt" : "IFTTT",
"com.jumbo.app" : "Jumbo",
"com.linkedin.LinkedIn" : "LinkedIn",
"com.marktplaats.iphone": "Marktplaats",
"com.microsoft.Office.Outlook" : "Outlook Mail",
"com.nestlabs.jasper.release" : "Nest",
"com.netflix.Netflix" : "Netflix",
@ -90,6 +92,7 @@ E.on('notify',msg=>{
"com.skype.skype": "Skype",
"com.skype.SkypeForiPad": "Skype",
"com.spotify.client": "Spotify",
"com.storytel.iphone": "Storytel",
"com.strava.stravaride": "Strava",
"com.tinyspeck.chatlyio": "Slack",
"com.toyopagroup.picaboo": "Snapchat",
@ -98,6 +101,8 @@ E.on('notify',msg=>{
"com.vilcsak.bitcoin2": "Coinbase",
"com.wordfeud.free": "WordFeud",
"com.zhiliaoapp.musically": "TikTok",
"io.robbie.HomeAssistant": "Home Assistant",
"net.weks.prowl": "Prowl",
"net.whatsapp.WhatsApp": "WhatsApp",
"nl.ah.Appie": "Albert Heijn",
"nl.postnl.TrackNTrace": "PostNL",
@ -118,7 +123,7 @@ E.on('notify',msg=>{
new : msg.new,
title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer),
subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer),
body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer)
body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) || "Cannot display"
});
// TODO: posaction/negaction?
});

View File

@ -4,4 +4,6 @@
0.04: Inluded LCARS Logo.
0.05: Additional icons for (1) charging and (2) bat < 30%.
0.06: Fix - Alarm disabled, if clock was closed.
0.07: Added settings to adjust data that is shown for each row.
0.07: Added settings to adjust data that is shown for each row.
0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode.
0.09: Tab anywhere to open the launcher.

View File

@ -1,18 +1,35 @@
# LCARS clock
A simple LCARS inspired clock.
Note: To display the steps, its necessary to install
the [Pedometer widget](https://banglejs.com/apps/#pedometer%20widget).
Note: To display the steps, the health app is required. If this app is not installed, the data will not be shown.
To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerdavid/BangleApps)
## Features
* LCARS Style watch face
* Shows satate (charging, out of battery etc.)
* SHows data that can be configured (steps, HRM, temperature etc.)
* Swipe left/right to activate an alarm
* LCARS Style watch face.
* Full screen mode - widgets are still loaded.
* Supports multiple screens with different data.
* Tab anywhere to open the launcher.
* [Screen 1] Date + Time + Lock status.
* [Screen 1] Shows randomly images of real planets.
* [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.)
* [Screen 1] Swipe up/down to activate an alarm.
* [Screen 1] Shows 3 customizable datapoints on the first screen.
* [Screen 1] The lower orange line indicates the battery level.
* [Screen 2] Display graphs for steps + hrm on the second screen.
* [Screen 2] Switch between day/month via swipe up/down.
## Multiple screens support
Access different screens via swipe left/ right
![](screenshot.png)
![](screenshot_2.png)
## Icons
<div>Icons made by <a href="https://www.flaticon.com/authors/smashicons" title="Smashicons">Smashicons</a>, <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
## Creator
Made by [David Peer](https://github.com/peerdavid)
## Contributors
- Creator: [David Peer](https://github.com/peerdavid).
- Improvements: [Adam Schmalhofer](https://github.com/adamschmalhofer).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
apps/lcars/bg_left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

BIN
apps/lcars/bg_right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -15,85 +15,97 @@ let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
/*
* Colors to use
*/
let cBlue = "#0094FF";
let cOrange = "#FF9900";
let cPurple = "#FF00DC";
let cWhite = "#FFFFFF";
let cBlack = "#000000";
let cGrey = "#9E9E9E";
/*
* Global lcars variables
*/
let lcarsViewPos = 0;
let drag;
let hrmValue = 0;
var connected = NRF.getSecurityStatus().connected;
var plotWeek = false;
/*
* Requirements and globals
*/
const locale = require('locale');
var backgroundImage = {
width : 176, height : 151, bpp : 3,
transparent : 2,
buffer : require("heatshrink").decompress(atob("AAdx48cATsAg4daIAX3799ATv2wEFDrUAgNHQDyDghaAeQcJKG86D4gRKGgAA4jxKFuBB5iaDF6BB5ZwyD6QAYCC4CD/Qf6Dzg/gQf8H/iD/n//wCD9gP///wQfpBKQf6D4h5BB/yD8jl/IIIABjiD5n4/DAAWAQe8B//8QYfHj//PAaDzHwICCAAP4gYCBQep6DIIYFBRgKD1j/+gB9BQYYKBn/gQen/+BBFQAUH/iDzGoZBHJoOAQeRBDj5BHj6PB0WKlACDJQIAofYZBFBAZBBAGMHPQZB8QYZAEIIcDIOiDI/hB3QZBBFjlx44CDuBBpg4DCIJEfIIPnz15AQeAQeH8gIDBGoJBCnnz54CDZ1UHPQMHIIUAIIKD3II6MBQYQCCQeI1B+BBC/BKCBASGCQeK5B/xBC4BKEn/gAoKDyj//45BFj/xZYSDzgF/IAP+JQrLCQecAgKDBF4cHQYKJDQecAn6EBAAiJEQeZBB/jICAAMcvwMDQevgQwR0CIIiDzgP/BA1/4CD3nAHGhyD3ABqD0ABiD/Qf4ADjiD/gEnQYuQQf6D7gaDFzxB5gFzQYnz4BB5hyDFATfkEoIdagEBQYoCcgEHDrReBhKDhwEBQbYABjiD/AH4A/AH4AGiFx48cATsAg4daIIWSpMkATuQEbkAgJfbQckJQDyDhZxQA1gRKFpBA4gEQQYtwIPMSQYtAIPKADQfqADAQRA5Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4A/AH4A/AH4A/AFkcuPHAQdAIPOSpMkAQaD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4A/AH4A/AH4A/AGUcuPHAQdwIPOSpMkAQaD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4AciSDFoCD/QfcCQYtIIPMAQYoC6gEJQYgC6gEBQf7HCQf4ABiiD9"))
}
var bgLeft = {
width : 27, height : 176, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAFCh/eX5Q/KAwdCAGVbtu27YCCoAJBkuWrNlAQRGCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A=="))
};
var bgRight = {
width : 27, height : 176, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAnUP7y/KH4yGeVYAJrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgAA="))
};
var iconEarth = {
text: "EARTH",
width : 50, height : 50, bpp : 3,
buffer : require("heatshrink").decompress(atob("AFtx48ECBsDwU5k/yhARLjgjBjlzAQMQEZcIkOP/fn31IEZgCBnlz58cEpM4geugEgwU/8+WNZJHDuHHvgmBCQ8goEOnVgJoMnyV58mACItHI4X8uAFBuVHnnz4BuGxk4////Egz3IkmWvPgNw8f/prB//BghTC+AjE7848eMjNnzySBwUJkmf/BuGuPDAQIjBiPHhhTCSQnjMo0ITANJn44Dg8MuFBggCCiFBcAJ0Bv5xEh+ITo2OhHkyf/OIQdBWwVHhgjBNwUE+fP/5EEgePMoYLBhMgyVJk/+BQQdC688I4XxOIc8v//NAvr+QEBj/5NwKVBy1/QYUciPBhk1EAJrC+KeC489QYaMBgU/8BNB9+ChEjz1Jkn/QYMBDQIgCcYTCCiP/nlzJQmenMAgV4//uy/9wRaB/1J8iVCcAfHjt9TYYICnhKCgRKBw159/v//r927OIeeoASBDQccvv3791KYVDBYPLJQeCnPnz//AAP6ocEjEkXgMgJQtz79fLAP8KYkccAcJ8Gf/f/xu/cAMQ4eP5MlyQRCMolx40YsOGBAPfnnzU4KVDpKMBvz8Dh0/8me7IICgkxJQXPIgZTD58sEgcJk+eNoONnFBhk4/5uB/pcDg5KD+4mEv4CBXISVDhEn31/8/+mH7x//JQK5CAAMB4JBCnnxJQf/+fJEgkAa4L+CAQOOjMn/1bXIRxDJQXx58f//Hhlz/88EgsChMgz/Zs/+nfkyV/8huDOI6SD498NwoACi1Z8+S/Plz17/+QCI7jC+ZxBmfPnojIAAMDcYWSp//2wRJEwq2GABECjMgNYwAmA="))
}
};
var iconSaturn = {
text: "SATURN",
width : 50, height : 50, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("AH4A/AEkQuPHCJ0ChEAwARNjAjBjgjOhs06Q2OEYVx4ARMhEggUMkANIDoIgBoEEgEBNxJEC6ZrBAAMwNxAjDNYcHNxIjB7dtEwIHBwRoKj158+cuPEjlwCRAjC23bpu0wRNDAAsHEYWeEwaSJ6YjCAQUNSRQjEzxQBWZMNEYlsmg2JWAIjCz95SoJuJggjDtuw6dMG5JKCz998wFBJRVNEYW0yaVBJRNhJQN9+4pCzhKJmBKC4YpB/fINxIgCzFxSoQ3J4ENm3CAQPb98wbpEcAQMYWwKYBNxMDXgc2/fv3g2IEAOAgAjBjy5CEhEMfYICBgfPnjdLjj+CgMHiC3JknDhhoINw4jCAB0IJQIANR4QjPAH4A/AFA"))
}
};
var iconMoon = {
text: "MOON",
width : 50, height : 50, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("AH4AQjlx44CCCZsg8eOkHDwAQKEYgmPhEgEQM48AOIgMHEYoCB4ATI8UAmH/x04JoRuJsImHuBKLn37EwZuIgEQOI8cEpXj/yYBhE8+YNGgkYoJxITBUPnAaC///nC+FjBuIOJZEB8YeCh/8AoYACoMEEAnEjhQDPQJKJ/DCDAoi5DoLdHAoMQgLjFWYPOnngh02IwXzwDjEgPGEYS8BI4MBYoSVG4fP/nghkAgZrDkngJQqSG4gvBg4sBQgkImHihEAWwP8ZBMBEYl5/+cSoVAGQIUFh04weJn///0gj/OEw5KEz45BzhuCTYQAEgePB4IACAoJuBnAQEa4XHjxKB//xFgWHJQsCRgMDEonipwjENwUBDQNx8+evvn/hTDLw3igE+EgZxB8UOXIvEJQUfEYOfv53DEQkgga5BJQvzx84cAj+CDoNh8/eEYJKDuCSEcocnEon+/7xEgFBIIcfB4Mf/IICXI2DgDdBAAn758gCIq5Dv4zBvJuIOIfjEgvP/ARHgwdCB4P3AoTdFAAk4EYk8SQgAFTALaDSQwAGh08//vnDmBABYmEEZYAzA=="))
}
};
var iconMars = {
text: "MARS",
width : 50, height : 50, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("AH4ATjlwCJ+Dh0wwAQMg0cuPHjFhCZkDps0yVJkmQCBMEjFx42atOmzQmLhMkEYQCCCREQoOGEYmmzB0IEY4CBkARGoJKBEYQCEzgSGkGSpAjDyYCCphuGiFhJQgCD8ASFgRHGAQKbB6BuHJRGeOIsINxEk6dNmARDgMEjQjHAQPnVQojIyZKB6YSDNwK5FAQt54BuDXJIjBEwK5EgxKKXgq5BJRdgXIojJAQJKMcAM0EwM2JUApDoCVFExa7FkGCgAmIkAREEwUEjAmHCIgABhEggQmFpACBCIojBEwRQCzVhwkQU4YADgQmBwQCCI4IFBCAojFAQojGJQQjDAQgRGEZICBEo4gFyUIkilFJQUYEAZrBAQMYNw5KDSQSbCNwwABgOGEwgCBsPACQ5xGwdNnARJcAVh48evvnCJK8Chs+/fv33gCRcB48cuPHCBYA/ADAA=="))
}
};
var iconSatellite = {
text: "GPS ON",
width : 50, height : 50, bpp : 3,
transparent : 2,
buffer : require("heatshrink").decompress(atob("pMkyQC/ATGXhIRPyNl0gmPjlwCJ9ly1aCJ1c+fHJR1Hy1ZJR1I+fPnlx6QRLpe+/JKBr5KMuYjBJQMdCJce/fvJQW0CJUlEYQCBSpvvJQbXJjl0NwnzNxGQwEOnHhgF78+WqQyIrFx48cAQXz4ShJgAABh0+8cP//9LJEhg4jDuP3//0LhGQgYlBgeAn///5cIy8MuAmDCIP/9I4HkmCEYMOgHfCQWkCI0cuBuDgF/CIP+CI1Ny1IkeAgHANwIAB/QRFrj7BhkxEwQRC/4RFpbXDgSVBg4RCSorXDI4MJAQMfCIP8cwImDn37fwN58+kwHgLgSVFub7CI4NyBAJKDLgkuEYX78+evKtCLg0jEYRKC58JMoRcFkwjDJQTFDl65EkojEAQMdcwn/+gFC3YjEJQLXEpYRDWwQmEdI6SHAQO0CJUkx4jDF4gCIJQgRMXIjCEARIjCCJ2XEYPKCJqJBJQIROcAUpCJ0kybaDARtdCKAC2kAA="))
}
var iconAlarm = {
text: "TIMER",
width : 50, height : 50, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("kmSpICEp//BAwCJn/+CJ8k//5CKAABCJs8uPH//x48EI5YjCAARNKEYUcv//jgFBExEnEYoAC+QmHIgIgC/gpCuPBCI2fIgU4AQXjA4P8CIuTEYZKBAolwHApXBEAWP//jxwpBAALaFDoYCIiQmDDIP4EAT+CEwnJEwYjLAQLaFEYomDKALmDNwoCIOIZuD8AkFgCYDHAQjMAQTdDNwOAEg0Dx0/cYeREZtxQYOTHgJuHOIvkXJy8DNwIACJQ8Ah4NDAAfxEZARHOIIkHg4jQAQb1CQ4KVJgEOnDIBSoIjNAQPBcAaVJcAKVBcDGOcD7OBMQM48BuH8f//JKCnhKNggRBkmfTQJxBEwhuD/gRCyVHJRlyCIVJXgYmB8ZQBAoIKBXIQmCOIt/NxAUCOIImCIgIpCBAJuDAQZEE/huIAQWTDgImBTYQGC8gRFcYpKFCI8kDwQAFCJBfBEAX/+IjBiQRIEw4jJAQc8v//NYwCIOgJrIJpA1OcwbaFAQWQA="))
}
};
var iconCharging = {
text: "CHARGE",
width : 50, height : 50, bpp : 3,
transparent : 5,
buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A"))
}
};
var iconNoBattery = {
text: "NO BAT",
width : 50, height : 50, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA"))
}
};
// Font to use:
// <link href="https://fonts.googleapis.com/css2?family=Antonio:wght@400;700&display=swap" rel="stylesheet">
Graphics.prototype.setFontAntonioSmall = function(scale) {
// Actual height 18 (17 - 0)
g.setFontCustom(atob("AAAAAAAAAAAAAAAf4Mf/sYAMAAAAAAfgAfAAAAAfgAeAAAAAAiAAj8H/4fyEAv8f/gfiAAgAAAAD54H98eOPHn8Hz8AhwAAAP8Af+AYGAYCAf+AP8MAB8AHwA+AD4AfAAcf4A/8AwMAwMA/8Af4AAAAAwGD8f/8f8MY/cfz4PD8AHMAAAfAAeAAAAAAAAP/+f//YADAAAQABYADf//P/+AAAAAANAAPAAfwAfgAPAANAAAAAAEAAEAA/AA/AAEAAEAAAAAAZAAfAAYAAAAIAAIAAIAAIAAAAAAAAAMAAMAAAAAAAAEAB8Af4H+AfwAcAAAAAP/4f/8YAMf/8f/8H/wAAAAAAEAAMAAf/8f/8f/8AAAAAAAAAHgcfh8cH8YPMf8MPwEAAAAAAOB4eB8YYMY4Mf/8Pn4AAAAAgAHwA/wPwwf/8f/8AAwAAgAAAf54f58ZwMZwMY/8Qf4AAAAAAP/4f/8YYMYYMff8HP4AAAQAAYAAYD8Y/8f/AfgAcAAAAAAAAPv4f/8YYMY8Mf/8Pn4AAAAAAP94f98YGMcMMf/8H/wAAAAAABgwBgwAAAAAABgABg/Bg8AAAAEAAOAAbAA7gAxgBwwASAAbAAbAAbAAbAASAAAAAxwA5gAbAAPAAOAAAAPAAfHcYPcf8Af4AHgAAAAAAAB/gH/wOA4Y/MZ/sbAsbBkb/MZ/sOBsH/AAAAAAMAP8f/4fwwf4wH/8AH8AAMAAAf/8f/8YYMYYMf/8P/4ADgAAAP/4f/8YAMYAMfj8Pj4AAAAAAf/8f/8YAMYAMf/8P/4B/AAAAf/8f/8YMMYMMYIMAAAAAAf/8f/8YYAYYAYYAAAAAAAP/4f/8YAMYIMfP8Pv8AAAAAAf/8f/8AMAAMAf/8f/8f/8AAAAAAf/8f/8AAAAAAAD4AB8AAMf/8f/4f/gAAAAAAf/8f/8A+AD/gfj4eA8QAEAAAf/8f/8AAMAAMAAMAAAf/8f/8f8AB/wAB8AP8P/Af/8f/8AAAAAAf/8f/8HwAA+AAPwf/8f/8AAAAAAP/4f/8YAMYAMf/8P/4AAAAAAf/8f/8YGAYGAf8AP8ABAAAAAf/w//4wAYwAc//+f/yAAAAAAf/8f/8YMAYMAf/8f/8DA8CAAPj4fz8Y4MeeMfP8HD4YAAYAAf/8f/8YAAQAAAAAf/4f/8AAMAAMf/8f/4AAAYAAf4AP/4AP8AP8f/4fwAQAAYAAf8AP/8AD8D/8f8Af8AD/8AD8f/8f8AAAAQAEeB8P/4B/AP/4fA8QAEYAAfAAP4AB/8H/8fwAcAAAAMYD8Y/8f/MfwMcAMAAAf/+f//YADYADAAAAAAfAAf8AB/wAH8AAMQACYADf//f//AAAAA"), 32, atob("BAUHCAcTCAQFBQgGBAYFBggICAgICAgICAgEBQYGBggNCAgICAcHCAkECAgGCwkICAgIBwYICAwHBwYGBgY="), 18+(scale<<8)+(1<<16));
}
Graphics.prototype.setFontAntonioMedium = function(scale) {
// Actual height 20 (19 - 0)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAA//mP/5gAAAAAAAAAAAAA/gAMAAAAAA/gAPAAAEIIBP+H/8D+IYBP+H/8D+IABCAAwIAfnwP8+PHh448eP3+B4fAAAAAAAH/AD/4AwGAMBgD/4Af8GAAPgAPgAfgAfAAfAA+AAOP/AH/4BgGAYBgH/4A/8AAAAAAAAAQAA/B+f4/+GMPhjv/4/h8Dg/gAcYwAAPwADgAAAAAAAAB//8///sAAaAACAAAMAAb//+f//AAAAAAAbAAGwAA4AA/wADgABsAAbAAAAAAAgAAMAAPwAD8AAMAADAAAAAAAAAAHAAB/AAOAAAAAAAAMAADAAAwAAMAACAAAAAAAAAABgAAYAAAAAAAAA4AD+AP+A/4A/gAOAAAAAAAAAH//j//8wADMAAz//8f/+AAAAAAAMAADAABgAA//+P//gAAAAAAAAAAAAAfgfP4fzAfswfDP/gx/gMAAAHgPj4D8wMDMHAz//8f3+AAEAAAAADwAH8APzA/AwP//j//4AAwAAAD/Hw/x+MwBjOAYz/+Mf/AAAAAAAH//j//8wYDMGAz9/8fP+AAcDAAAwAAMAfjB/4z/wP+AD4AAwAAAAOB/f4///MHAzBwM///H9/gAAAAAAH/Pj/78wGDMBgz//8f/+AAAAAAADhwA4cAAAAAAAAAAAAAADh/A4fgAAAAOAAHwABsAA7gAccAGDAAAAANgADYAA2AANgADYAA2AAAAAAAABgwAccADuAAbAAHwAA4AAAAHwAD8c4/POMHAD/wAfwAAAAAAAAD/wD//B4B4Y/HMf8zMBMyATMwczP+M4BzHwcgf+AA+AAAAAAD4A/+P/8D+DA/4wH/+AB/4AAeAAAAAAA//+P//jBgYwYGP//j//4PH4AAAAAAAf/+P//zgAcwADP4fz+P4Ph8AAAAAAA//+P//jAAYwAGPADj//4P/4AAAAAAA//+P//jBgYwYGMGBgAAAAAAP//j//4wYAMGADBgAAAAAAAA//w///PAHzAQM4MHP7/x+/8AAAAAAD//4//+AGAABgAAYAP//j//4AAAAAAAAAA//+P//gAAAAAAAAAAAHwAB+AABgAAY//+P//AAAAAAAAAAD//4//+APgAf+Afj8PgPjAAYAAAAAAD//4//+AABgAAYAAGAAAAAAA//+P//j/gAD/wAB/gAP4B/4P/AD//4//+AAAAAAAAAAP//j//4P4AAfwAA/g//+P//gAAAAAAAAAA//g//+PAHjAAY4AOP//h//wAAAAAAD//4//+MDADAwA4cAP/AB/gAAAAAAAA//g//+PAHjAAc4APv//5//yAAAAAAD//4//+MGADBgA48AP//h+f4AAAAAAB+Pw/z+MOBjBwY/P+Hx/AAHgwAAMAAD//4//+MAADAAAAAAP//D//4AAOAABgAA4//+P//AAAAwAAP8AD//AA/+AAfgP/4//gPwAAAAA+AAP/4Af/4AD+A//j/wA/wAD/+AA/4B/+P/+D+AAAAAMADj8P4P/4A/4B//w+A+MABgAAA4AAPwAB/gAB/+A//j/gA+AAMAAAAAYwB+MH/jf+Y/8GPwBjAAAAAAP//7//+wABsAAYAAAAAAPAAD/gAH/gAD/gAD4AACAAADAAGwABv//7//+AAAA=="), 32, atob("BQUHCAgVCQQFBQkHBQcFBwgICAgICAgICAgFBQcHBwgPCQkJCQcHCQoFCQkHDQoJCQkJCAYJCQ0ICAcGBwY="), 20+(scale<<8)+(1<<16));
};
Graphics.prototype.setFontAntonioLarge = function(scale) {
// Actual height 34 (34 - 1)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAADwAAAAAeAAAAADwAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAD+AAAAH/wAAAP/+AAAf/+AAA//8AAB//4AAD//wAAD//gAAAf/AAAAD+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAB////gA/////AP////8D/////wfAAAA+DwAAADweAAAAeDwAAADwf////+D/////wP////8Af///+AAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAOAAAAADwAAAAAeAAAAAHgAAAAB/////wf////+D/////wf////+D/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAPwH/4AP+B//AH/wf/4D/+D4AB/9weAAf4ODwAP8BweAP/AOD///gBwP//wAOA//4ABwB/4AAOAAAAAAAAAAAAAAAAAAAAB8AA/gA/gAH/AP8AA/8D/gAH/wfAHAA+DwA4ADweAHgAeDwB8ADwf7/+H+D/////gP/9//8A//H/+AA/AH/AAAAAAAAAAAAAAAAAABwAAAAD+AAAAD/wAAAH/+AAAH/5wAAH/wOAAP/gBwAP/gAOAD/////wf////+D/////wf////+AAAABwAAAAAOAAAAABwAAAAAAAAAAAAAAAAAAeAD//4D/Af//Af8D//4D/wf//Af+DwPAADweB4AAeDwPAADweB///+DwP///weA///8DwD//+AAAA/8AAAAAAAAAAAAAAAAAAAAAA////AA/////AP////8D/////wfgPAB+DwB4ADweAOAAeDwBwADwf+PAA+D/x///wP+H//8A/wf//AAAA//gAAAAAAAAAAAAADgAAAAAeAAAAADwAAAAAeAAAD+DwAAP/weAA//+DwA///weB///8Dx//8AAf//wAAD//gAAAf/AAAAD/AAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAD/wf/wB//v//AP////8D/////weAPwAeDwA8ADwcAHAAeDwB8ADwf////+D/////wP/9//8A//H//AA/AD/AAAAAAAAAAAAAAAAAAAAAD//gfAA///D/AP//8f8D///j/weAA8A+DwADgDweAAcAeDwAHgDwf////+B/////gP////8Af///+AAP//4AAAAAAAAAAAAAAAAAAAAAAD4AfAAAfAD4AAD4AfAAAfAD4AAD4AfAAAAAAAAAAAAAA=="), 46, atob("Cg4QEBAQEBAQEBAQCQ=="), 39+(scale<<8)+(1<<16));
}
// Actual height 39 (39 - 1)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAPgAAAAAB8AAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAD8AAAAAH/gAAAAP/8AAAAf//gAAA///AAAB//+AAAD//8AAAH//4AAAP//wAAAB//gAAAAP/AAAAAB+AAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///AAAf////8AP/////4B//////Af/////8D8AAAAfgeAAAAA8DwAAAAHgeAAAAA8D//////gf/////8B//////AP/////wAf////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAHgAAAAAA8AAAAAAPgAAAAAB4AAAAAAf/////gP/////8B//////gP/////8B//////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAD/+AAP8A//wAP/gP/+AH/8D//wD//gfgAA//8DwAAf+HgeAAP/A8DwAH/gHgfgP/wA8D///4AHgP//+AA8A///AAHgB//AAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AA/gAD/AAH/gA/4AA/+AP/AAH/4D/4AA//gfgA4AB8DwAPAAHgeAB4AA8DwAPgAHgfAD+AB8D//////gP/////4B//5//+AD/+H//gAH/AH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAP/AAAAAP/4AAAAP//AAAAP/x4AAAf/wPAAAf/gB4AAf/AAPAAP/AAB4AB//////gP/////8B//////gP/////8AAAAAPAAAAAAB4AAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//wD/AB///Af+AP//4D/4B///Af/gP//4B/8B4D4AAPgPAeAAA8B4DwAAHgPAfAAB8B4D////gPAf///4B4B////APAD///gAAAD//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAP////4AH/////wB//////Af/////8D8APAA/geADwAB8DwAeAAHgeADwAA8D4AeAAPgf/j+AH8B/8f///gP/h///4Af8H//+AAPgP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAPAAAAAAB4AAAABgPAAAA/8B4AAB//gPAAD//8B4AH///gPAH///8B4P//+AAPH//wAAB///gAAAP//AAAAB/+AAAAAP+AAAAAB+AAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4A/+AAf/w//+AP//v//4B//////Af/////8D4AfwAPgeAB8AA8DwAHAAHgeAB8AA8D4Af4APgf/////8B//////AP//v//4A//4//8AA/4A/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+AAAAD//+D/gB///4f+AP///j/4D///8f/gfAAHgB8DwAA8AHgeAAHgA8DwAA8AHgfgAHgB8D//////gP/////4A/////+AD/////gAD////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAfgAAB+AD8AAAPwAfgAAB+AD8AAAPwAfgAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DBATExMTExMTExMTCw=="), 45+(scale<<8)+(1<<16));
};
/*
* Draw watch face
@ -108,117 +120,303 @@ function queueDraw() {
}
function printData(key, y){
function printData(key, y, c){
g.setFontAlign(-1,-1,0);
var text = "ERR";
var value = "NOT FOUND";
if(key == "Battery"){
var bat = E.getBattery();
g.drawString("BAT:", 30, y);
g.drawString(bat+ "%", 68, y);
text = "BAT";
value = E.getBattery() + "%";
} else if(key == "Steps"){
var steps = getSteps();
g.drawString("STEP:", 30, y);
g.drawString(steps, 68, y);
text = "STEP";
value = getSteps();
} else if(key == "Temp."){
var temperature = Math.floor(E.getTemperature());
g.drawString("TEMP:", 30, y);
g.drawString(temperature + "C", 69, y);
text = "TEMP";
value = Math.floor(E.getTemperature()) + "C";
} else if(key == "HRM"){
g.drawString("HRM:", 30, y);
g.drawString(hrmValue, 69, y);
text = "HRM";
value = hrmValue;
} else if (key == "VREF"){
text = "VREF";
value = E.getAnalogVRef().toFixed(2) + "V";
}
g.setColor(c);
g.fillRect(79, y-2, 87 ,y+18);
g.setFontAlign(1,-1,0);
g.drawString(value, 131, y);
g.setColor(c);
g.setFontAlign(-1,-1,0);
g.fillRect(133, y-2, 165 ,y+18);
g.fillCircle(161, y+8, 10);
g.setColor(cBlack);
g.drawString(text, 135, y);
}
function drawHorizontalBgLine(color, x1, x2, y, h){
g.setColor(color);
for(var i=0; i<h; i++){
g.drawLine(x1, y+i, x2,y+i);
}
}
function drawLock(){
if(lcarsViewPos != 0){
return;
}
g.setFontAntonioMedium();
g.setColor(cOrange);
g.clearRect(120, 10, g.getWidth(), 75);
g.drawString("LCARS", 128, 13);
if(connected){
g.drawString("CONN", 128, 33);
} else {
g.drawString("NOT FOUND", 30, y);
g.drawString("NOCON", 128, 33);
}
if(Bangle.isLocked()){
g.setColor(cPurple);
g.drawString("LOCK", 128, 53);
}
}
function drawState(){
if(lcarsViewPos != 0){
return;
}
g.clearRect(20, 93, 75, 170);
g.setFontAlign(0, 0, 0);
g.setFontAntonioMedium();
if(!isAlarmEnabled()){
var bat = E.getBattery();
var current = new Date();
var hours = current.getHours();
var iconImg =
Bangle.isCharging() ? iconCharging :
bat < 30 ? iconNoBattery :
Bangle.isGPSOn() ? iconSatellite :
hours % 4 == 0 ? iconSaturn :
hours % 4 == 1 ? iconMars :
hours % 4 == 2 ? iconMoon :
iconEarth;
g.drawImage(iconImg, 24, 118);
g.setColor(cWhite);
g.drawString("STATUS", 24+25, 108);
} else {
// Alarm within symbol
g.setColor(cOrange);
g.drawString("ALARM", 24+25, 108);
g.setColor(cWhite);
g.setFontAntonioLarge();
g.drawString(getAlarmMinutes(), 24+25, 108+35);
}
g.setFontAlign(-1, -1, 0);
}
function drawPosition0(){
// Draw background image
g.drawImage(bgLeft, 0, 0);
drawHorizontalBgLine(cBlue, 25, 120, 0, 4);
drawHorizontalBgLine(cBlue, 130, 176, 0, 4);
drawHorizontalBgLine(cPurple, 20, 70, 80, 4);
drawHorizontalBgLine(cPurple, 80, 176, 80, 4);
drawHorizontalBgLine(cOrange, 35, 110, 87, 4);
drawHorizontalBgLine(cOrange, 120, 176, 87, 4);
// The last line is a battery indicator too
var bat = E.getBattery() / 100.0;
var batX2 = parseInt((172 - 35) * bat + 35);
drawHorizontalBgLine(cOrange, 35, batX2, 171, 5);
drawHorizontalBgLine(cGrey, batX2+10, 172, 171, 5);
// Draw logo
drawLock();
// Write time
g.setFontAlign(-1, -1, 0);
g.setColor(cWhite);
var currentDate = new Date();
var timeStr = locale.time(currentDate,1);
g.setFontAntonioLarge();
g.drawString(timeStr, 29, 10);
// Write date
g.setColor(cWhite);
g.setFontAntonioMedium();
var dayStr = locale.dow(currentDate, true).toUpperCase();
dayStr += " " + currentDate.getDate();
dayStr += " " + currentDate.getFullYear();
g.drawString(dayStr, 32, 56);
// Draw data
g.setFontAlign(-1, -1, 0);
g.setColor(cWhite);
printData(settings.dataRow1, 97, cOrange);
printData(settings.dataRow2, 122, cPurple);
printData(settings.dataRow3, 147, cBlue);
// Draw state
drawState();
}
function drawPosition1(){
// Draw background image
g.drawImage(bgRight, 149, 0);
drawHorizontalBgLine(cBlue, 0, 140, 0, 4);
drawHorizontalBgLine(cPurple, 0, 80, 80, 4);
drawHorizontalBgLine(cPurple, 90, 150, 80, 4);
drawHorizontalBgLine(cOrange, 0, 50, 87, 4);
drawHorizontalBgLine(cOrange, 60, 140, 87, 4);
drawHorizontalBgLine(cOrange, 0, 150, 171, 5);
// Draw steps bars
g.setColor(cWhite);
let health;
try {
health = require("health");
} catch(ex) {
g.setFontAntonioMedium();
g.drawString("MODULE HEALTH", 20, 110);
g.drawString("REQUIRED.", 20, 130);
g.drawString("MODULE HEALTH", 20, 20);
g.drawString("REQUIRED.", 20, 40);
return;
}
// Plot HRM graph
if(plotWeek){
var data = new Uint16Array(32);
var cnt = new Uint8Array(32);
health.readDailySummaries(new Date(), h=>{
data[h.day]+=h.bpm;
if (h.bpm) cnt[h.day]++;
});
require("graph").drawBar(g, data, {
axes : true,
minx: 1,
gridx : 5,
gridy : 100,
width : 140,
height : 50,
x: 5,
y: 25
});
// Plot step graph
var data = new Uint16Array(32);
health.readDailySummaries(new Date(), h=>data[h.day]+=h.steps/1000);
var gridY = parseInt(Math.max.apply(Math, data)/2);
gridY = gridY <= 0 ? 1 : gridY;
require("graph").drawBar(g, data, {
axes : true,
minx: 1,
gridx : 5,
gridy : gridY,
width : 140,
height : 50,
x: 5,
y: 115
});
g.setFontAlign(1, 1, 0);
g.setFontAntonioMedium();
g.setColor(cWhite);
g.drawString("WEEK HRM", 154, 27);
g.drawString("WEEK STEPS [K]", 154, 115);
// Plot day
} else {
var data = new Uint16Array(24);
var cnt = new Uint8Array(24);
health.readDay(new Date(), h=>{
data[h.hr]+=h.bpm;
if (h.bpm) cnt[h.hr]++;
});
require("graph").drawBar(g, data, {
axes : true,
minx: 1,
gridx : 4,
gridy : 100,
width : 140,
height : 50,
x: 5,
y: 25
});
// Plot step graph
var data = new Uint16Array(24);
health.readDay(new Date(), h=>data[h.hr]+=h.steps);
var gridY = parseInt(Math.max.apply(Math, data)/1000)*1000;
gridY = gridY <= 0 ? 1000 : gridY;
require("graph").drawBar(g, data, {
axes : true,
minx: 1,
gridx : 4,
gridy : gridY,
width : 140,
height : 50,
x: 5,
y: 115
});
g.setFontAlign(1, 1, 0);
g.setFontAntonioMedium();
g.setColor(cWhite);
g.drawString("DAY HRM", 154, 27);
g.drawString("DAY STEPS", 154, 115);
}
}
function draw(){
// First handle alarm to show this correctly afterwards
handleAlarm();
// Next draw the watch face
g.reset();
g.clearRect(0, 24, g.getWidth(), g.getHeight());
g.clearRect(0, 0, g.getWidth(), g.getHeight());
// Draw background image
g.drawImage(backgroundImage, 0, 24);
// Draw symbol
var bat = E.getBattery();
var timeInMinutes = getCurrentTimeInMinutes();
var iconImg =
isAlarmEnabled() ? iconAlarm :
Bangle.isCharging() ? iconCharging :
bat < 30 ? iconNoBattery :
Bangle.isGPSOn() ? iconSatellite :
timeInMinutes % 4 == 0 ? iconSaturn :
timeInMinutes % 4 == 1 ? iconMars :
timeInMinutes % 4 == 2 ? iconMoon :
iconEarth;
g.drawImage(iconImg, 115, 115);
// Alarm within symbol
g.setFontAlign(0,0,0);
g.setFontAntonioSmall();
g.drawString(iconImg.text, 115+25, 105);
if(isAlarmEnabled() > 0){
g.drawString(getAlarmMinutes(), 115+25, 115+25);
// Draw current lcars position
if(lcarsViewPos == 0){
drawPosition0();
} else if (lcarsViewPos == 1) {
drawPosition1();
}
// Write time
var currentDate = new Date();
var timeStr = locale.time(currentDate,1);
g.setFontAlign(0,0,0);
g.setFontAntonioLarge();
g.drawString(timeStr, 60, 55);
// Write date
g.setFontAlign(-1,-1, 0);
g.setFontAntonioSmall();
var dayName = locale.dow(currentDate, true).toUpperCase();
var day = currentDate.getDate();
g.drawString(day, 100, 35);
g.drawString(dayName, 100, 55);
// Draw battery
printData(settings.dataRow1, 98);
printData(settings.dataRow2, 121);
printData(settings.dataRow3, 144);
// Queue draw in one minute
queueDraw();
}
/*
* Step counter via widget
*/
function getSteps() {
if (stepsWidget() !== undefined)
return stepsWidget().getSteps();
return "???";
}
function stepsWidget() {
if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom;
} else if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom;
var steps = 0;
try {
health = require("health");
} catch(ex) {
return steps;
}
return undefined;
health.readDay(new Date(), h=>steps+=h.steps);
return steps;
}
/*
* HRM Listener
*/
Bangle.on('HRM', function (hrm) {
hrmValue = hrm.bpm;
});
/*
* Handle alarm
@ -228,7 +426,7 @@ function getCurrentTimeInMinutes(){
}
function isAlarmEnabled(){
return settings.alarm > 0;
return settings.alarm >= 0;
}
function getAlarmMinutes(){
@ -253,65 +451,134 @@ function handleAlarm(){
.then(() => new Promise(resolve => setTimeout(resolve, t)))
.then(() => Bangle.buzz(t, 1))
.then(() => new Promise(resolve => setTimeout(resolve, t)))
.then(() => Bangle.buzz(t, 1));
// Update alarm state to disabled
settings.alarm = -1;
Storage.writeJSON(SETTINGS_FILE, settings);
.then(() => Bangle.buzz(t, 1))
.then(() => new Promise(resolve => setTimeout(resolve, 5E3)))
.then(() => {
// Update alarm state to disabled
settings.alarm = -1;
Storage.writeJSON(SETTINGS_FILE, settings);
});
}
/*
* Swipe to set an alarm
*/
Bangle.on('swipe',function(dir) {
// Increase alarm
if(dir == -1){
if(isAlarmEnabled()){
settings.alarm += 5;
} else {
settings.alarm = getCurrentTimeInMinutes() + 5;
}
}
// Decrease alarm
if(dir == +1){
if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){
settings.alarm -= 5;
} else {
settings.alarm = -1;
}
}
// Update UI
draw();
// Update alarm state
Storage.writeJSON(SETTINGS_FILE, settings);
});
/*
* Stop updates when LCD is off, restart when on
* Listeners
*/
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
// Whenever we connect to Gadgetbridge, reading data from
// health failed. Therefore, we update and read data from
// health iff the connection state did not change.
if(connected == NRF.getSecurityStatus().connected) {
draw();
} else {
connected = NRF.getSecurityStatus().connected
drawLock();
}
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
connected = NRF.getSecurityStatus().connected
});
Bangle.on('lock', function(isLocked) {
drawLock();
});
Bangle.on('charging',function(charging) {
drawState();
});
Bangle.on('HRM', function (hrm) {
hrmValue = hrm.bpm;
});
function increaseAlarm(){
if(isAlarmEnabled()){
settings.alarm += 5;
} else {
settings.alarm = getCurrentTimeInMinutes() + 5;
}
Storage.writeJSON(SETTINGS_FILE, settings);
}
function decreaseAlarm(){
if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){
settings.alarm -= 5;
} else {
settings.alarm = -1;
}
Storage.writeJSON(SETTINGS_FILE, settings);
}
// Thanks to the app "gbmusic" for this code to detect swipes in all 4 directions.
Bangle.on("drag", e => {
if (!drag) { // start dragging
drag = {x: e.x, y: e.y};
} else if (!e.b) { // released
const dx = e.x-drag.x, dy = e.y-drag.y;
drag = null;
// Horizontal swipe
if (Math.abs(dx)>Math.abs(dy)+10) {
if(dx > 0){
lcarsViewPos = 0;
} else {
lcarsViewPos = 1;
}
// Vertical swipe
} else if (Math.abs(dy)>Math.abs(dx)+10) {
if(lcarsViewPos == 0){
if(dy > 0){
decreaseAlarm();
} else {
increaseAlarm();
}
// Only update the state and return to
// avoid a full draw as this is much faster.
drawState();
return;
}
if(lcarsViewPos == 1){
plotWeek = dy < 0 ? true : false;
}
}
draw();
}
});
Bangle.on("touch", e => {
Bangle.showLauncher();
});
/*
* Lets start widgets, listen for btn etc.
*/
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets - needed by draw
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.
*/
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
// Clear the screen once, at startup and draw clock
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
draw();
// After drawing the watch face, we can draw the widgets
Bangle.drawWidgets();
// Bangle.drawWidgets();

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwgeevPnAQsc+fPngCE+/fvoCEvAbIA4/AgFzEZwRBjwjNvBUBEZ3eCIMOEZtwCIMBEZuARYU5EZecTocHEZf0CIcBEbvgaggjKTwIAEbQpoHAAiSEeoYQHJQr1CCBJKEIgcBI4xKFaIdt3AOFgfuAYMeEYLRBj1pLQ4ICuYjBAgPbtoRHhu3AYN5VoMGzVpI49502AgPPVoM27dsK48N23cgE5CgOmzVoCI4LBzCSB8EP2wjJgILBAYMAhIjBsAjJzVwg47C7YRJEYhfBEZXmEZ53CI4q2BEAiVCkwjCNYaMGboQjDkBfDCAbdB04EBgyPDC4YAD/dt2wRCHIM5njXCCAcHboOmCIQ0B5/nfYT6DFIIjBeAcOvM8+EAjitFEYJEBAANzEYOeeowjCFgUDzwjB+YrDgAgBEYWcA4Mc+YjCvAQCgftEANuDIYOBEYXPNwIAIg4OCCgXkCBEOEZDvBEAhEB4AjF/inB8+OJQOOvILBoAjGU4IFDAQYjGbQIdCAQt4EY0DEZACDEYceEZACDC4bLBEZwCO"))
require("heatshrink").decompress(atob("mEwgeevPnAQsc+fPngCE+/fvoCEvAbIA4/AgFzEZwRBjwjNvBUBEZ3eCIMOEZtwCIMBEZuARYU5EZecTocHEZf0CIcBEbvgaggjKTwIAEbQpoHAAiSEeoYQHJQr1CCBJKEIgcBI4xKFaIdt3AOFgfuAYMeEYLRBj1pLQ4ICuYjBAgPbtoRHhu3AYN5VoMGzVpI49502AgPPVoM27dsK48N23cgE5CgOmzVoCI4LBzCSB8EP2wjJgILBAYMAhIjBsAjJzVwg47C7YRJEYhfBEZXmEZ53CI4q2BEAiVCkwjCNYaMGboQjDkBfDCAbdB04EBgyPDC4YAD/dt2wRCHIM5njXCCAcHboOmCIQ0B5/nfYT6DFIIjBeAcOvM8+EAjitFEYJEBAANzEYOeeowjCFgUDzwjB+YrDgAgBEYWcA4Mc+YjCvAQCgftEANuDIYOBEYXPNwIAIg4OCCgXkCBEOEZDvBEAhEB4AjF/inB8+OJQOOvILBoAjGU4IFDAQYjGbQIdCAQt4EY0DEZACDEYceEZACDC4bLBEZwCO"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -18,14 +18,14 @@
storage.write(SETTINGS_FILE, settings)
}
var data_options = ['Battery', 'Steps', 'Temp.', "HRM"];
var data_options = ["Battery", "Steps", "Temp.", "HRM", "VREF"];
E.showMenu({
'': { 'title': 'LCARS Clock' },
'< Back': back,
'Row 1': {
value: 0 | data_options.indexOf(settings.dataRow1),
min: 0, max: 3,
min: 0, max: 4,
format: v => data_options[v],
onchange: v => {
settings.dataRow1 = data_options[v];
@ -34,7 +34,7 @@
},
'Row 2': {
value: 0 | data_options.indexOf(settings.dataRow2),
min: 0, max: 3,
min: 0, max: 4,
format: v => data_options[v],
onchange: v => {
settings.dataRow2 = data_options[v];
@ -43,7 +43,7 @@
},
'Row 3': {
value: 0 | data_options.indexOf(settings.dataRow3),
min: 0, max: 3,
min: 0, max: 4,
format: v => data_options[v],
onchange: v => {
settings.dataRow3 = data_options[v];

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