Merge branch 'master' into master

pull/1199/head
7kasper 2022-01-05 15:27:10 +01:00 committed by GitHub
commit 382bc6a35f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
226 changed files with 7548 additions and 970 deletions

409
apps.json
View File

@ -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.05",
"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.15",
"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",
@ -1148,7 +1148,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",
@ -1501,7 +1501,7 @@
{
"id": "gpsinfo",
"name": "GPS Info",
"version": "0.05",
"version": "0.06",
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
@ -1749,8 +1749,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}
@ -2027,7 +2028,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",
@ -2184,7 +2185,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"}],
@ -2429,7 +2430,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.04",
"version": "0.05",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
@ -2970,11 +2971,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"}],
@ -3532,7 +3533,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",
@ -4064,7 +4065,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",
@ -4210,13 +4211,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": [
@ -4382,7 +4383,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"}],
@ -4487,7 +4488,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.07",
"version":"0.08",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
@ -4621,7 +4622,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",
@ -4652,7 +4653,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": [
@ -4697,6 +4698,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}
@ -4708,7 +4711,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",
@ -4725,7 +4728,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",
@ -4733,7 +4736,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"},
@ -4767,7 +4770,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",
@ -4778,7 +4781,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"}],
@ -4838,6 +4841,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",
@ -4861,7 +4883,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"}],
@ -4875,11 +4897,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",
@ -4931,10 +4967,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": [
@ -4963,8 +5001,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",
@ -4972,10 +5010,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",
@ -5029,9 +5071,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",
@ -5047,6 +5090,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",
@ -5080,5 +5139,273 @@
{"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.01",
"description": "Replace Bangle.js 1's built in menu function.",
"icon": "icon.png",
"type": "boot",
"tags": "system",
"supports": ["BANGLEJS"],
"screenshots": [{"url":"pro-menu-screenshot.png"}],
"storage": [
{"name":"promenu.boot.js","url":"boot.js"},
{"name":"promenu.img","url":"promenuIcon.js","evaluate":true}
]
},
{
"id": "touchtimer",
"name": "Touch Timer",
"shortName": "Touch Timer",
"version": "0.02",
"description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.",
"icon": "app.png",
"tags": "tools",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"screenshots": [{"url":"0_light_timer_edit.png"},{"url":"1_light_timer_ready.png"},{"url":"2_light_timer_running.png"},{"url":"3_light_timer_finished.png"}],
"storage": [
{ "name": "touchtimer.app.js", "url": "app.js" },
{ "name":"touchtimer.settings.js", "url":"settings.js"},
{ "name": "touchtimer.img", "url": "app-icon.js", "evaluate": true }
],
"data": [{"name":"touchtimer.data.json"}]
},
{
"id": "teatimer",
"name": "Tea Timer",
"version": "1.00",
"description": "A simple timer. You can easyly set up the time.",
"icon": "teatimer.png",
"type": "app",
"tags": "tool",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"teatimer.app.js","url":"app.js"},
{"name":"teatimer.img","url":"app-icon.js","evaluate":true}
],
"screenshots": [
{"url":"TeatimerStart.jpg"},
{"url":"TeatimerHelp.jpg"},
{"url":"TeatimerRun.jpg"},
{"url":"TeatimerUp.jpg"}
]
},
{
"id": "swp2clk",
"name": "Swipe back to the Clock",
"shortName": "Swipe to Clock",
"version": "0.01",
"description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.",
"icon": "app.png",
"type": "boot",
"tags": "tools",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{ "name": "swp2clk.boot.js", "url": "boot.js" },
{"name":"swp2clk.settings.js","url":"settings.js"}
],
"data": [{"name":"swp2clk.data.json"}]
},
{
"id":"colorwheel",
"name":"Color Wheel",
"tags":"app,tool",
"version":"0.01",
"description":"a tappable wheel of good-looking colors",
"readme":"README.md",
"supports":["BANGLEJS2"],
"allow_emulator":true,
"icon":"colorwheel.png",
"storage": [
{"name":"colorwheel.app.js","url":"app.js"},
{"name":"colorwheel.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "minimal_clock",
"name": "Minimal Analog Clock",
"shortName":"Minimal Clock",
"version":"0.03",
"description": "a minimal analog clock - just with some hands and no clock face",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"minimal_clock.app.js","url":"app.js"},
{"name":"minimal_clock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "simple_clock",
"name": "Simple Analog Clock",
"shortName":"Simple Clock",
"version":"0.02",
"description": "a simple, yet stylish, analog clock",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"simple_clock.app.js","url":"app.js"},
{"name":"simple_clock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "colorful_clock",
"name": "Colorful Analog Clock",
"shortName":"Colorful Clock",
"version":"0.02",
"description": "a colorful analog clock",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"colorful_clock.app.js","url":"app.js"},
{"name":"colorful_clock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "themesetter",
"name": "Theme Setter",
"shortName":"Theme Setter",
"version":"0.04",
"description": "a comfortable way to configure theme colors",
"icon": "app-icon.png",
"type": "app",
"tags": "tool",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"themesetter.app.js","url":"app.js"},
{"name":"themesetter.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "widviztime",
"name": "Widget Autohide Widget",
"shortName": "Viz Time Widget",
"version": "0.01",
"description": "The widgets will be shown for four seconds after the device is unlocked.",
"icon": "eye.png",
"type": "widget",
"tags": "widget",
"readme":"README.md",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"widviztime.wid.js","url":"widget.js"}
]
},
{
"id": "supf",
"name": "Simple Clock with Date",
"shortName": "supf Clock",
"version": "0.01",
"description": "Simple Clock with seconds and date in custom language. Install 'Languages' to get localized names.",
"icon": "icon.png",
"screenshots": [{"url":"screenshot_supf.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"supf.app.js","url":"app.js"},
{"name":"supf.img","url":"icon.js","evaluate":true}
]
},
{ "id": "andark",
"name": "Analog Dark",
"shortName":"AnDark",
"version":"0.04",
"description": "analog clock face without disturbing widgets",
"icon": "andark_icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"andark.app.js","url":"app.js"},
{"name":"andark.img","url":"app_icon.js","evaluate":true}
]
},
{
"id": "diract",
"name": "DirAct",
"shortName": "DirAct",
"version": "0.01",
"description": "Proximity interaction detection.",
"icon": "diract.png",
"type": "app",
"tags": "tool,sensors",
"supports" : [ "BANGLEJS2" ],
"allow_emulator": false,
"readme": "README.md",
"storage": [
{ "name": "diract.app.js", "url": "diract.js" },
{ "name": "diract.img", "url": "diract-icon.js", "evaluate": true }
]
},
{
"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"}
]
}
]

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 +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) {
@ -68,13 +64,26 @@ function draw() {
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;
@ -24,6 +24,8 @@ window.onload = function() {
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] = []; }
@ -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

@ -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,4 @@
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

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

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;
var log = function() {};//print
var settings = {};
var device;
var gatt;
var service;
var characteristic;
//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
});
});
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

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

View File

@ -2,3 +2,4 @@
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: Added number of satellites in view and fixed crash with GPS time

View File

@ -16,13 +16,19 @@ var lastFix = {
time: 0,
satellites: 0
};
var nofix = 0;
var SATinView = 0;
var nofBD = 0;
var nofGP = 0;
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 +83,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 +93,6 @@ function onGPS(fix) {
}
lastFix = fix;
if (fix.fix) {
nofix = 0;
var locale = require("locale");
var satellites = fix.satellites;
var maidenhead = getMaidenHead(fix.lat,fix.lon);
@ -100,12 +105,21 @@ function onGPS(fix) {
layout.maidenhead.label = "Maidenhead: "+maidenhead;
} else {
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(3,6) == "GSV") {
// console.log(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);

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

@ -5,3 +5,4 @@
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.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode.

View File

@ -1,18 +1,33 @@
# 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.
* [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 month graphs for steps + hrm on the second screen.
## 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,96 @@ 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";
/*
* 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 +119,299 @@ 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(133, y-2, 165 ,y+18);
g.fillCircle(161, y+8, 10);
g.setColor(cBlack);
g.drawString(text, 135, y);
g.setColor(c);
g.setFontAlign(1,-1,0);
g.drawString(value, 130, 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, 77, 170);
g.setColor(cWhite);
var bat = E.getBattery();
var current = new Date();
var hours = current.getHours();
if(!isAlarmEnabled()){
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, 29, 104);
} else {
// Alarm within symbol
g.setFontAntonioMedium();
g.setFontAlign(0, 0, 0);
g.setColor(cOrange);
g.drawString("ALARM", 29+25, 107);
g.setColor(cWhite);
g.setFontAntonioLarge();
g.drawString(getAlarmMinutes(), 29+25, 107+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(cPurple, 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, 28, 10);
// Write date
g.setColor(cWhite);
g.setFontAntonioMedium();
var dayStr = locale.dow(currentDate, true).toUpperCase();
dayStr += " " + currentDate.getDate();
dayStr += " " + currentDate.getFullYear();
g.drawString(dayStr, 29, 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 +421,7 @@ function getCurrentTimeInMinutes(){
}
function isAlarmEnabled(){
return settings.alarm > 0;
return settings.alarm >= 0;
}
function getAlarmMinutes(){
@ -253,65 +446,130 @@ 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();
}
});
/*
* 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();

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];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
apps/lcars/screenshot_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -457,12 +457,12 @@ var locales = {
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00
datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020
timePattern: { 0: "%HH:%MM.%SS ", 1: "%HH:%MM" }, // 17:00.00 // 17:00
datePattern: { 0: "%d %b %Y", "1": "%d/%m/%Y" }, // 1 marzo 2020 // 01/03/2020
abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic",
month: "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre",
abday: "dom,lun,mar,mer,gio,ven,sab",
day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì, sabato",
day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì,sabato",
trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" }
},
"it_IT": {
@ -475,12 +475,12 @@ var locales = {
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00
datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020
timePattern: { 0: "%HH:%MM.%SS ", 1: "%HH:%MM" }, // 17:00.00 // 17:00
datePattern: { 0: "%d %b %Y", "1": "%d/%m/%Y" }, // 1 marzo 2020 // 01/03/2020
abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic",
month: "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre",
abday: "dom,lun,mar,mer,gio,ven,sab",
day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì, sabato",
day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì,sabato",
trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" }
},
"wae_CH": {
@ -589,7 +589,7 @@ var locales = {
month: "leden,únor,březen,duben,květen,červen,červenec,srpen,září,říjen,listopad,prosinec",
abday: "ne,po,út,st,čt,pá,so",
day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota",
trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "na", off: "poza" }
trans: { yes: "ano", Yes: "Ano", no: "ne", No: "Ne", ok: "ok", on: "zap", off: "vyp" }
},
"sl_SI": {
lang: "sl_SI",

View File

@ -1,2 +1,3 @@
0.01: Created app
0.02: Use Bangle.setUI for button/launcher handling
0.03: Allow widgets to detect this is a clock

View File

@ -304,15 +304,14 @@ Bangle.on('lcdPower',function(on) {
});
g.clear();
// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
// Update time once a second
timeInterval = setInterval(showTime, 1000);
showTime();
// Show launcher when button pressed
Bangle.setUI("clock");
// Start stopwatch when BTN3 is pressed
setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"});
B3 = 1; // BTN3 is bound to start the stopwatch

View File

@ -21,3 +21,6 @@
Add 'Delete All' option to message options
Now update correctly when 'require("messages").clearAll()' is called
0.14: Hide widget when all unread notifications are dismissed from phone
0.15: Don't buzz when Quiet Mode is active
0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147)
0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font

View File

@ -52,7 +52,7 @@ var MESSAGES = require("Storage").readJSON("messages.json",1)||[];
if (!Array.isArray(MESSAGES)) MESSAGES=[];
var onMessagesModified = function(msg) {
// TODO: if new, show this new one
if (msg && msg.new) {
if (msg && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
if (WIDGETS["messages"]) WIDGETS["messages"].buzz();
else Bangle.buzz();
}
@ -243,7 +243,8 @@ function showMessage(msgid) {
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
}});
}
lines = g.wrapString(msg.body, g.getWidth()-10);
var bodyFont = fontMedium;
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10);
var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
layout = new Layout({ type:"v", c: [
{type:"h", fillx:1, bgCol:colBg, c: [
@ -256,7 +257,7 @@ function showMessage(msgid) {
title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{},
]},
]},
{type:"txt", font:fontMedium, label:body, fillx:1, filly:1, pad:2 },
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2 },
{type:"h",fillx:1, c: buttons}
]});
g.clearRect(Bangle.appRect);

View File

@ -43,7 +43,8 @@ exports.pushMessage = function(event) {
// otherwise load messages/show widget
var loadMessages = Bangle.CLOCK || event.important;
// first, buzz
if (loadMessages && global.WIDGETS && WIDGETS.messages)
var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
if (!quiet && loadMessages && global.WIDGETS && WIDGETS.messages)
WIDGETS.messages.buzz();
// after a delay load the app, to ensure we have all the messages
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
@ -51,7 +52,7 @@ exports.pushMessage = function(event) {
exports.messageTimeout = undefined;
// if we're in a clock or it's important, go straight to messages app
if (loadMessages) return load("messages.app.js");
if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know
if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz to let someone know
WIDGETS.messages.show();
}, 500);
}

View File

@ -1,8 +1,9 @@
WIDGETS["messages"]={area:"tl",width:0,draw:function() {
WIDGETS["messages"]={area:"tl", width:0, iconwidth:23,
draw:function() {
Bangle.removeListener('touch', this.touch);
if (!this.width) return;
var c = (Date.now()-this.t)/1000;
g.reset().clearRect(this.x,this.y,this.x+this.width,this.y+23);
g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+this.iconwidth);
g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y);
//if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute
let settings = require('Storage').readJSON("messages.settings.json", true) || {};
@ -17,7 +18,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() {
WIDGETS["messages"].t=Date.now(); // first time
WIDGETS["messages"].l=Date.now()-10000; // last buzz
if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing
WIDGETS["messages"].width=64;
WIDGETS["messages"].width=this.iconwidth;
Bangle.drawWidgets();
Bangle.setLCDPower(1);// turns screen on
},hide:function() {
@ -26,6 +27,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() {
WIDGETS["messages"].width=0;
Bangle.drawWidgets();
},buzz:function() {
if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return; // never buzz during Quiet Mode
let v = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || ".";
function b() {
var c = v[0];
@ -36,7 +38,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() {
b();
},touch:function(b,c) {
var w=WIDGETS["messages"];
if (!w||!w.width||c.x<w.x||c.x>w.x+w.width||c.y<w.y||c.y>w.y+23) return;
if (!w||!w.width||c.x<w.x||c.x>w.x+w.width||c.y<w.y||c.y>w.y+w.iconwidth) return;
load("messages.app.js");
}};
/* We might have returned here if we were in the Messages app for a

View File

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

View File

@ -0,0 +1,15 @@
# Minimal Analog Clock #
This app displays the perhaps most basic analog clock one can think of - just
some clock hands and no clock 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("mEwgJC/AEBhCjgCBgeAgF8AoXggHwCIXwgfADAX8h4TBAAM+jwkDj/4AocPDwIACgdgBYgoCAAMEuB+/AH4="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

230
apps/minimal_clock/app.js Normal file
View File

@ -0,0 +1,230 @@
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 HourHandLength = outerRadius * 0.5;
let HourHandWidth = 2*5, halfHourHandWidth = HourHandWidth/2;
let MinuteHandLength = outerRadius * 0.7;
let MinuteHandWidth = 2*3, halfMinuteHandWidth = MinuteHandWidth/2;
let SecondHandLength = outerRadius * 0.9;
let SecondHandOffset = halfHourHandWidth + 10;
let outerBoltRadius = halfHourHandWidth + 2, innerBoltRadius = outerBoltRadius - 4;
let HandOffset = outerBoltRadius + 4;
let twoPi = 2*Math.PI, deg2rad = Math.PI/180;
let Pi = Math.PI;
let halfPi = Math.PI/2;
let sin = Math.sin, cos = Math.cos;
let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1];
let HandPolygon = [
-sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3],
sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0],
sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3],
-sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0],
];
let HourHandPolygon = new Array(HandPolygon.length);
for (let i = 0, l = HandPolygon.length; i < l; i+=2) {
HourHandPolygon[i] = halfHourHandWidth*HandPolygon[i];
HourHandPolygon[i+1] = halfHourHandWidth*HandPolygon[i+1];
if (i < l/2) { HourHandPolygon[i+1] -= HourHandLength; }
if (i > l/2) { HourHandPolygon[i+1] += HandOffset; }
}
let MinuteHandPolygon = new Array(HandPolygon.length);
for (let i = 0, l = HandPolygon.length; i < l; i+=2) {
MinuteHandPolygon[i] = halfMinuteHandWidth*HandPolygon[i];
MinuteHandPolygon[i+1] = halfMinuteHandWidth*HandPolygon[i+1];
if (i < l/2) { MinuteHandPolygon[i+1] -= MinuteHandLength; }
if (i > l/2) { MinuteHandPolygon[i+1] += HandOffset; }
}
/**** transforme polygon ****/
let transformedPolygon = new Array(HandPolygon.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
);
g.setColor(g.theme.fg);
g.fillCircle(CenterX,CenterY, outerBoltRadius);
g.setColor(g.theme.bg);
g.drawCircle(CenterX,CenterY, outerBoltRadius);
g.fillCircle(CenterX,CenterY, innerBoltRadius);
}
/**** refreshDisplay ****/
let Timer;
function refreshDisplay () {
g.clear(true); // also loads current theme
Bangle.drawWidgets();
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');

View File

@ -1 +1,2 @@
0.01: First release
0.02: Enhanced icon, make it bolder

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEw4UA///t/7j/P3/vB4cBqtVoAbHBQIABBQ0FBYdQBYsVBYdUERIkGHIQADHoguEGAwuEGAwKFBZg8DHQw8EBYNf/1Vq3/8oLDIwNf/Wpv//0oLG9Wq3/qBYJUCBYuqBaBqBBYW+BepHEBbybCBYP+BYSnErYLDyoLFAANq/r8Ga5T7MBZZUBAAhSCfhA6DBZhIGBQg8FHQg8GHQgwGFwowFBQwwDFwwLMlS7Bqta1AKEn2q1K1C1WgBYf/1WqBYIDB1QKCgYLC0taBYoXB/QICBY0//7vBAAQ8EEgIABCwwME9QVEA"))
require("heatshrink").decompress(atob("mEw4UA///gH4AYPO/QPDgNVqtADY/1BYNfBQ0PBQIAB+ALFmoLDrgLF6oLDq4KEgYKDBYPABYcNBYlVuAuIGAwuEAANUBYYKFHgg6Bq4ZCr4DBHgQLBvWq2te1WlBYZGBBYOr1Wq1qSDBYNqBIILDKgQLLgoLHqBqDBfJHLBZBrOgKPCBYiPCU4NaBYe1WYrABBQLCCfgYGCrwVBa4kAirvKNgIAErgLDKgIAEKQQ8EAAY6DBZhIDIww8GHQg8GHQgwGFwowEFwx5EOog8GHQ0AlWpBYNq1AKFWIILBAYOgBYbICytWAgQKCgTgDcwYXGAAgvGAAY8EEgYWGBgoVEA=="))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -7,3 +7,4 @@
0.07: Added info line that cycles on BTN1/BTN3 (or vitual buttons on a bangle 2)
0.08: Added dependancy on MyLocation
0.09: Added dependancy on Pedometer Widget
0.10: Added Weather line, fixed issues on a Bangle 1, update every minute

View File

@ -1,45 +1,83 @@
# Pastel Clock
*a configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times*
*a configurable clock with custom fonts, background and optional weather icons. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times*
* Designed specifically for Bangle 1 and Bangle 2
* A choice of 7 different custom fonts
* Supports the Light and Dark themes
* Has a settings menu, change font, enable/disable the grid
* Has a settings menu, change font, enable/disable the grid, weather icons
* On Bangle 1 use BTN1,BTN3 to cycle through the info display (Date, ID, Batt %, Ram % etc)
* On Bangle 2 touch the top right/top left to cycle through the info display (Date, ID, Batt %, Ram % etc)
* The information display will cycle on each screen update
* Uses mylocation.json from MyLocation app to calculate sunrise and sunset times for your location
* Uses pedometer widget to get latest step count
* Use the weather widget to get weather status
* Dependant apps are installed when Pastel installs
* The screen is updated every minute to save battery power
* The weather display will display temperature and wind speed on alternate screen refreshes
I came up with the name Pastel due to the shade of the grid background.
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/)
## Lato
## Weather Support
![](screenshot_pastel.png)
Pastel installs the weather app and weather widget. You may want to
hide the weather widget display, this can be done through the weather
widget settings. You should first get the weather app working. If
the weather App is not working, then it is not going to work for
Pastel.
The following weather icons are supported.
![](weather_icons.png)
Mostly cloudy, Sunny, Mostly Sunny, Snow, Rain.
The triangle icon shows there is a problem connecting to GadgetBridge and the weather service.
You should follow the setup and trouble shooting guide for the Weather App.
If you find the weather / gadgetbridge service unreliable you can
disable weather updates to pastel through the settings app.
## Fonts
### Lato
![](screenshot_lato.png)
## Architect
### Architect
![](screenshot_architect.png)
## Gochihand
### Gochihand
![](screenshot_gochihand.png)
## Monoton
### Monoton
![](screenshot_monoton.png)
## Elite
### Elite
![](screenshot_elite.png)
## Cabin Sketch
### Cabin Sketch
![](screenshot_cabinsketch.png)
## Orbitron
### Orbitron
![](screenshot_orbitron.png)
### The Grid
Setting the grid on provides a graph paper style background to the App.
The grid is not supported on a Bangle 1 due to flicker issues.
![](screenshot_grid.png)

View File

@ -1,10 +1,22 @@
var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
require("f_latosmall").add(Graphics);
const storage = require('Storage');
const locale = require("locale");
const SETTINGS_FILE = "pastel.json";
const LOCATION_FILE = "mylocation.json";
let settings;
let location;
// cloud, sun, partSun, snow, rain, storm, error
// create 1 bit, max contrast, brightness set to 85
var cloudIcon = require("heatshrink").decompress(atob("kEggIfcj+AAYM/8ADBuFwAYPAmADCCAMBwEf8ADBhFwg4aBnEPAYMYjAVBhgDDDoQDHCYc4jwDB+EP///FYIDBMTgA=="));
var sunIcon = require("heatshrink").decompress(atob("kEggILIgOAAZkDAYPAgeBwPAgIFBBgPhw4TBp/yAYMcnADBnEcAYMwhgDBsEGgE/AYP8AYYLDCYgbDEYYrD8fHIwI7CIYZLDL54AHA=="));
var sunPartIcon = require("heatshrink").decompress(atob("kEggIHEmADJjEwsEAjkw8EAh0B4EAg35wEAgP+CYMDwv8AYMDBAP2g8HgH+g0DBYMMgPwAYX8gOMEwMG3kAg8OvgSBjg2BgcYGQIcBAY5CBg0Av//HAM///4MYgNBEIMOCoUMDoUAnBwGkEA"));
var snowIcon = require("heatshrink").decompress(atob("kEggITQj/AAYM98ADBsEwAYPAjADCj+AgOAj/gAYMIuEHwEAjEPAYQVChk4AYQhCAYcYBYQTDnEPgEB+EH///IAQACE4IAB8EICIPghwDB4EeBYNAjgDBg8EAYQYCg4bCgZuFA=="));
var rainIcon = require("heatshrink").decompress(atob("kEggIPMh+AAYM/8ADBuFwAYPgmADB4EbAYOAj/ggOAhnwg4aBnAeCjEcCIMMjADCDoQDHjAPCnAXCuEP///8EDAYJECAAXBwkAgPDhwDBwUMgEEhkggEOjFgFgMQLYQAOA=="));
var errIcon = require("heatshrink").decompress(atob("kEggILIgOAAYsD4ADBg/gAYMGsADBhkwAYsYjADCjgDBmEMAYNxxwDBsOGAYPBwYDEgOBwOAgYDB4EDHYPAgwDBsADDhgDBFIcwjAHBjE4AYMcmADBhhNCKIcG/4AGOw4A=="));
function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
settings.grid = settings.grid||false;
@ -93,7 +105,49 @@ function prevInfo() {
}
}
var mm_prev = "xx";
/**
Choose weather icon to display based on condition.
Based on function from the Bangle weather app so it should handle all of the conditions
sent from gadget bridge.
*/
function chooseIcon(condition) {
condition = condition.toLowerCase();
if (condition.includes("thunderstorm")) return stormIcon;
if (condition.includes("freezing")||condition.includes("snow")||
condition.includes("sleet")) {
return snowIcon;
}
if (condition.includes("drizzle")||
condition.includes("shower")) {
return rainIcon;
}
if (condition.includes("rain")) return rainIcon;
if (condition.includes("clear")) return sunIcon;
if (condition.includes("few clouds")) return partSunIcon;
if (condition.includes("scattered clouds")) return cloudIcon;
if (condition.includes("clouds")) return cloudIcon;
if (condition.includes("mist") ||
condition.includes("smoke") ||
condition.includes("haze") ||
condition.includes("sand") ||
condition.includes("dust") ||
condition.includes("fog") ||
condition.includes("ash") ||
condition.includes("squalls") ||
condition.includes("tornado")) {
return cloudIcon;
}
return cloudIcon;
}
/**
Get weather stored in json file by weather app.
*/
function getWeather() {
let jsonWeather = storage.readJSON('weather.json');
return jsonWeather;
}
function draw() {
var d = new Date();
@ -115,19 +169,27 @@ function draw() {
var x = (g.getWidth()/2);
var y = (g.getHeight()/3);
g.reset();
var weatherJson = getWeather();
var w_temp;
var w_icon;
var w_wind;
if (process.env.HWVERSION == 1) {
// avoid flicker on a bangle 1 by comparing with previous minute
if (mm_prev != mm) {
mm_prev = mm;
g.clearRect(0, 30, w, h - 24);
}
if (settings.weather && weatherJson && weatherJson.weather) {
var currentWeather = weatherJson.weather;
const temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/);
w_temp = temp[1] + " " + temp[2];
w_icon = chooseIcon(currentWeather.txt);
const wind = locale.speed(currentWeather.wind).match(/^(\D*\d*)(.*)$/);
w_wind = wind[1] + " " + wind[2] + " " + (currentWeather.wrose||'').toUpperCase();
} else {
// on a b2 safe to just clear anyway as there is no flicker
g.clearRect(0, 30, w, h - 24);
w_temp = "Err";
w_wind = "???";
w_icon = errIcon;
}
g.reset();
g.clearRect(0, 30, w, h - 24);
// draw a grid like graph paper
if (settings.grid && process.env.HWVERSION !=1) {
g.setColor("#0f0");
@ -139,6 +201,18 @@ function draw() {
g.setColor(g.theme.fg);
// draw weather line
if (settings.weather) {
g.drawImage(w_icon, (w/2) - 40, 24);
g.setFontLatoSmall();
g.setFontAlign(-1,0); // left aligned
if (drawCount % 2 == 0)
g.drawString(w_temp, (w/2) + 6, 24 + ((y - 24)/2));
else
g.drawString( (w_wind.split(' ').slice(0, 2).join(' ')), (w/2) + 6, 24 + ((y - 24)/2));
// display first 2 words of the wind string eg '4 mph'
}
if (settings.font == "Architect")
g.setFontArchitect();
else if (settings.font == "GochiHand")
@ -161,36 +235,39 @@ function draw() {
// for the colon
g.setFontAlign(0,-1); // centre aligned
if (d.getSeconds()&1) {
g.drawString(":", x,y);
} else {
// on bangle 1, we are not using clearRect(), hide : by printing over it in reverse color
if (process.env.HWVERSION == 1) {
g.setColor(g.theme.bg);
g.drawString(":", x,y);
g.setColor(g.theme.fg);
}
}
g.drawString(":", x,y);
g.setFontLatoSmall();
g.setFontAlign(0, -1);
g.drawString((infoData[infoMode].calc()), w/2, h - 24 - 24);
if (drawCount % 3600 == 0)
// recalc sunrise / sunset every hour
if (drawCount % 60 == 0)
updateSunRiseSunSet(new Date(), location.lat, location.lon);
drawCount++;
queueDraw();
}
// Only update when display turns on
if (process.env.BOARD!="SMAQ3") // hack for Q3 which is always-on
Bangle.on('lcdPower', function(on) {
if (secondInterval)
clearInterval(secondInterval);
secondInterval = undefined;
if (on)
secondInterval = setInterval(draw, 1000);
draw();
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
nextInfo();
draw();
}, 60000 - (Date.now() % 60000));
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.setUI("clockupdown", btn=> {
@ -204,8 +281,6 @@ loadFonts();
loadLocation();
g.clear();
var secondInterval = setInterval(draw, 1000);
draw();
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();

View File

@ -4,6 +4,7 @@
// initialize with default settings...
let s = {
'grid': false,
'weather': false,
'font': "Lato"
}
@ -39,8 +40,16 @@
value: s.grid,
format: () => (s.grid ? 'Yes' : 'No'),
onchange: () => {
s.grid = !s.grid
save()
s.grid = !s.grid;
save();
},
},
'Show Weather': {
value: s.weather,
format: () => (s.weather ? 'Yes' : 'No'),
onchange: () => {
s.weather = !s.weather;
save();
},
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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