Merge branch 'master' into master

pull/1162/head
Gordon Williams 2022-01-04 10:00:54 +00:00 committed by GitHub
commit 8cebf0ea6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 2094 additions and 563 deletions

View File

@ -77,7 +77,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.14",
"version": "0.15",
"description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png",
"type": "app",
@ -4064,7 +4064,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",
@ -4487,7 +4487,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.",
@ -4652,7 +4652,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": [
@ -4861,7 +4861,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,6 +4875,20 @@
],
"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",
@ -5029,9 +5043,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 +5062,22 @@
{"name":"circlesclock.json"}
]
},
{ "id": "contourclock",
"name": "Contour Clock",
"shortName" : "Contour Clock",
"version":"0.01",
"icon": "app.png",
"description": "A Minimalist clockface with large Digits. Looks best with the dark theme",
"screenshots" : [{"url":"screenshot.png"}],
"tags": "clock",
"allow_emulator":true,
"supports" : ["BANGLEJS2"],
"type": "clock",
"storage": [
{"name":"contourclock.app.js","url":"app.js"},
{"name":"contourclock.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "ltherm",
"name": "Localized Thermometer",
@ -5063,7 +5094,7 @@
{"name":"ltherm.img","url":"icon.js","evaluate":true}
]
},
{
{
"id": "supf",
"name": "Simple Clock with Date",
"shortName": "supf Clock",
@ -5080,5 +5111,37 @@
{"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 }
]
}
]

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

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

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

@ -21,3 +21,4 @@
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

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

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

@ -26,6 +26,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];

View File

@ -3,3 +3,5 @@
0.03: Make tap to confirm new pattern more reliable. Also allow for easier creation of single circle patterns.
0.10: Improve the management of existing patterns: Draw the linked pattern on the left hand side of the app name within a scroller, similar to the default launcher. Slighlty clean up the code to make it less horrible.
0.11: Respect theme colors. Fix: Do not pollute global space with internal variables ans functions in boot.js
0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/
0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/

View File

@ -29,32 +29,41 @@ Then launch the linked apps directly from the clock screen by simply drawing the
## Detailed Steps
From the main menu you can:
- Add a new pattern and link it to an app (first entry)
- To create a new pattern first select "Add Pattern"
- Now draw any pattern you like, this will later launch the linked app from the clock screen
- You can also draw a single-circle pattern (meaning a single tap on one circle) instead of drawing a 'complex' pattern
- If you don't like the pattern, simply re-draw it. The previous pattern will be discarded.
- If you are happy with the pattern tap on screen or press the button to continue
- Now select the app you want to launch with the pattern.
- Note, you can bind multiple patterns to the same app.
- To create a new pattern first select "Add Pattern"
- Now draw any pattern you like, this will later launch the linked app from the clock screen
- You can also draw a single-circle pattern (meaning a single tap on one circle) instead of drawing a 'complex' pattern
- If you don't like the pattern, simply re-draw it. The previous pattern will be discarded.
- If you are happy with the pattern press the button to continue
- Now select the app you want to launch with the pattern.
- Note, you can bind multiple patterns to the same app.
- Manage created patterns (second entry)
- To manage your patterns first select "Manage Patterns"
- You will now see a scrollabe list of patterns + linked apps
- If you want to deletion a pattern (and unlink the app) simply tap on it, and confirm the deletion
- To manage your patterns first select "Manage Patterns"
- You will now see a scrollabe list of patterns + linked apps
- If you want to deletion a pattern (and unlink the app) simply tap on it, and confirm the deletion
- Disable the lock screen on the clock screen from the settings (third entry)
- To launch the app from the pattern on the clock screen the watch must be unlocked.
- If this annoys you, you can disable the lock on the clock screen from the setting here
- To launch the app from the pattern on the clock screen the watch must be unlocked.
- If this annoys you, you can disable the lock on the clock screen from the setting here
## FAQ
1) Nothing happens when I draw on the clock screen!
1. Nothing happens when I draw on the clock screen!
Please double-check if you actually have a pattern linked to an app.
2) I have a pattern linked to an app and still nothing happens when I draw on the clock screen!
2. I have a pattern linked to an app and still nothing happens when I draw on the clock screen!
Make sure the watch is unlocked before you start drawing. If this bothers you, you can permanently disable the watch-lock from within the Pattern Launcher app (via the Settings).
3) I have done all that and still nothing happens!
3. I have done all that and still nothing happens!
Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry!
## Authors
Initial creation: [crazysaem](https://github.com/crazysaem)
Improve pattern detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/)
Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -114,7 +114,6 @@ var showMainMenu = () => {
E.showMenu(mainmenu);
};
var positions = [];
var recognizeAndDrawPattern = () => {
return new Promise((resolve) => {
E.showMenu();
@ -135,150 +134,55 @@ var recognizeAndDrawPattern = () => {
resolve(pattern.join(""));
};
setWatch(() => finishHandler(), BTN);
setTimeout(() => Bangle.on("tap", finishHandler), 250);
// setTimeout(() => Bangle.on("tap", finishHandler), 250);
positions = [];
var positions = [];
var getPattern = (positions) => {
var circles = [
{ x: 25, y: 25, i: 0 },
{ x: 87, y: 25, i: 1 },
{ x: 150, y: 25, i: 2 },
{ x: 25, y: 87, i: 3 },
{ x: 87, y: 87, i: 4 },
{ x: 150, y: 87, i: 5 },
{ x: 25, y: 150, i: 6 },
{ x: 87, y: 150, i: 7 },
{ x: 150, y: 150, i: 8 },
];
return positions.reduce((pattern, p, i, arr) => {
var idx = circles.findIndex((c) => {
var dx = p.x > c.x ? p.x - c.x : c.x - p.x;
if (dx > CIRCLE_RADIUS) {
return false;
}
var dy = p.y > c.y ? p.y - c.y : c.y - p.y;
if (dy > CIRCLE_RADIUS) {
return false;
}
if (dx + dy <= CIRCLE_RADIUS) {
return true;
}
return dx * dx + dy * dy <= CIRCLE_RADIUS_2;
});
if (idx >= 0) {
pattern += circles[idx].i;
circles.splice(idx, 1);
}
if (circles.length === 0) {
arr.splice(1);
}
return pattern;
}, "");
};
var dragHandler = (position) => {
log(position);
positions.push(position);
debounce().then(() => {
if (isFinished) {
return;
}
// This might actually be a 'tap' event.
// Use this check in addition to the actual tap handler to make it more reliable
if (pattern.length > 0 && positions.length === 2) {
if (
positions[0].x === positions[1].x &&
positions[0].y === positions[1].y
) {
finishHandler();
positions = [];
return;
}
}
E.showMessage("Calculating...");
var t0 = Date.now();
log(positions.length);
var circlesClone = cloneCirclesArray();
pattern = [];
var step = Math.floor(positions.length / 100) + 1;
var p, a, b, circle;
for (var i = 0; i < positions.length; i += step) {
p = positions[i];
circle = circlesClone[0];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(0, 1);
}
}
circle = circlesClone[1];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(1, 1);
}
}
circle = circlesClone[2];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(2, 1);
}
}
circle = circlesClone[3];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(3, 1);
}
}
circle = circlesClone[4];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(4, 1);
}
}
circle = circlesClone[5];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(5, 1);
}
}
circle = circlesClone[6];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(6, 1);
}
}
circle = circlesClone[7];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(7, 1);
}
}
circle = circlesClone[8];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circlesClone.splice(8, 1);
}
}
}
var tx = Date.now();
log(tx - t0);
positions = [];
var t1 = Date.now();
log(t1 - t0);
log("pattern:");
log(pattern);
log("redrawing");
if (position.b === 0 || positions.length >= 200) {
pattern = getPattern(positions).split("");
g.clear();
drawCirclesWithPattern(pattern);
});
positions = [];
}
};
Bangle.on("drag", dragHandler);
});
};
@ -488,7 +392,8 @@ var drawCircle = (circle, drawBuffer, scale) => {
log("drawing circle");
log({ x: x, y: y, r: r });
drawBuffer.drawCircle(x, y, r);
drawBuffer.setColor(0);
drawBuffer.fillCircle(x, y, r);
};
var cachedCirclesDrawings = {};
@ -533,8 +438,11 @@ var drawCirclesWithPattern = (pattern, options) => {
{ msb: true }
);
CIRCLES.forEach((circle) => drawCircle(circle, drawBuffer, scale));
drawBuffer.setColor(1);
drawBuffer.fillRect(0, 0, drawBuffer.getWidth(), drawBuffer.getHeight());
CIRCLES.forEach((circle) => drawCircle(circle, drawBuffer, scale));
drawBuffer.setColor(1);
drawBuffer.setFontAlign(0, 0);
drawBuffer.setFont("Vector", 40 * scale);
pattern.forEach((circleIndex, patternIndex) => {
@ -545,12 +453,12 @@ var drawCirclesWithPattern = (pattern, options) => {
circle.y * scale
);
});
image = {
width: drawBuffer.getWidth(),
height: drawBuffer.getHeight(),
bpp: 1,
buffer: drawBuffer.buffer,
palette: new Uint16Array([g.theme.fg, g.theme.bg], 0, 1),
};
if (enableCaching) {
@ -563,16 +471,6 @@ var drawCirclesWithPattern = (pattern, options) => {
g.drawImage(image, offset.x, offset.y);
};
var cloneCirclesArray = () => {
var circlesClone = Array(CIRCLES.length);
for (var i = 0; i < CIRCLES.length; i++) {
circlesClone[i] = CIRCLES[i];
}
return circlesClone;
};
//////
// misc lib functions
//////
@ -583,20 +481,6 @@ var log = (message) => {
}
};
var debounceTimeoutId;
var debounce = (delay) => {
if (debounceTimeoutId) {
clearTimeout(debounceTimeoutId);
}
return new Promise((resolve) => {
debounceTimeoutId = setTimeout(() => {
debounceTimeoutId = undefined;
resolve();
}, delay || 500);
});
};
//////
// run main function
//////

View File

@ -7,128 +7,51 @@
};
var storedPatterns;
var CIRCLE_RADIUS = 25;
var CIRCLE_RADIUS_2 = Math.pow(CIRCLE_RADIUS, 2);
var positions = [];
var getPattern = (positions) => {
var circles = [
{ x: 25, y: 25, i: 0 },
{ x: 87, y: 25, i: 1 },
{ x: 150, y: 25, i: 2 },
{ x: 25, y: 87, i: 3 },
{ x: 87, y: 87, i: 4 },
{ x: 150, y: 87, i: 5 },
{ x: 25, y: 150, i: 6 },
{ x: 87, y: 150, i: 7 },
{ x: 150, y: 150, i: 8 },
];
return positions.reduce((pattern, p, i, arr) => {
var idx = circles.findIndex((c) => {
var dx = p.x > c.x ? p.x - c.x : c.x - p.x;
if (dx > CIRCLE_RADIUS) {
return false;
}
var dy = p.y > c.y ? p.y - c.y : c.y - p.y;
if (dy > CIRCLE_RADIUS) {
return false;
}
if (dx + dy <= CIRCLE_RADIUS) {
return true;
}
return dx * dx + dy * dy <= CIRCLE_RADIUS_2;
});
if (idx >= 0) {
pattern += circles[idx].i;
circles.splice(idx, 1);
}
if (circles.length === 0) {
arr.splice(1);
}
return pattern;
}, "");
};
var dragHandler = (position) => {
positions.push(position);
debounce().then(() => {
log(positions.length);
var CIRCLE_RADIUS = 25;
var CIRCLE_RADIUS_2 = CIRCLE_RADIUS * CIRCLE_RADIUS;
var circles = [
{ x: 25, y: 25, i: 0 },
{ x: 87, y: 25, i: 1 },
{ x: 150, y: 25, i: 2 },
{ x: 25, y: 87, i: 3 },
{ x: 87, y: 87, i: 4 },
{ x: 150, y: 87, i: 5 },
{ x: 25, y: 150, i: 6 },
{ x: 87, y: 150, i: 7 },
{ x: 150, y: 150, i: 8 },
];
var pattern = [];
var step = Math.floor(positions.length / 100) + 1;
var p, a, b, circle;
for (var i = 0; i < positions.length; i += step) {
p = positions[i];
circle = circles[0];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(0, 1);
}
}
circle = circles[1];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(1, 1);
}
}
circle = circles[2];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(2, 1);
}
}
circle = circles[3];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(3, 1);
}
}
circle = circles[4];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(4, 1);
}
}
circle = circles[5];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(5, 1);
}
}
circle = circles[6];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(6, 1);
}
}
circle = circles[7];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(7, 1);
}
}
circle = circles[8];
if (circle) {
a = p.x - circle.x;
b = p.y - circle.y;
if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) {
pattern.push(circle.i);
circles.splice(8, 1);
}
}
}
positions = [];
pattern = pattern.join("");
if (position.b === 0 || positions.length >= 200) {
var pattern = getPattern(positions);
log(pattern);
if (pattern) {
if (storedPatterns[pattern]) {
@ -145,21 +68,9 @@
}
}
}
});
};
var debounceTimeoutId;
var debounce = (delay) => {
if (debounceTimeoutId) {
clearTimeout(debounceTimeoutId);
positions = [];
}
return new Promise((resolve) => {
debounceTimeoutId = setTimeout(() => {
debounceTimeoutId = undefined;
resolve();
}, delay || 500);
});
};
var sui = Bangle.setUI;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -2,3 +2,4 @@
0.02: Corrected variable initialisation
0.03: Advertise app name, added screenshots
0.04: Advertise bar, GPS, HRM and mag services
0.05: Refactored for efficiency, corrected sensor value inaccuracies

View File

@ -124,27 +124,16 @@ function transmitUpdatedSensorData() {
isNewMagData = false;
}
NRF.setAdvertising(data, { showName: false, interval: 200 });
let interval = 1000 / data.length;
NRF.setAdvertising(data, { showName: false, interval: interval });
}
// Encode the bar service data to fit in a Bluetooth PDU
function encodeBarServiceData() {
let tEncoded = Math.round(bar.temperature * 100);
let pEncoded = Math.round(bar.pressure * 100);
let eEncoded = Math.round(bar.altitude * 100);
if(bar.temperature < 0) {
tEncoded += 0x10000;
}
if(bar.altitude < 0) {
eEncoded += 0x1000000;
}
let t = [ tEncoded & 0xff, (tEncoded >> 8) & 0xff ];
let p = [ pEncoded & 0xff, (pEncoded >> 8) & 0xff, (pEncoded >> 16) & 0xff,
(pEncoded >> 24) & 0xff ];
let e = [ eEncoded & 0xff, (eEncoded >> 8) & 0xff, (eEncoded >> 16) & 0xff ];
let t = toByteArray(Math.round(bar.temperature * 100), 2, true);
let p = toByteArray(Math.round(bar.pressure * 1000), 4, false);
let e = toByteArray(Math.round(bar.altitude * 100), 3, true);
return [
0x02, 0x01, 0x06, // Flags
@ -157,52 +146,26 @@ function encodeBarServiceData() {
// Encode the GPS service data using the Location and Speed characteristic
function encodeGpsServiceData() {
let latEncoded = Math.round(gps.lat * 10000000);
let lonEncoded = Math.round(gps.lon * 10000000);
let hEncoded = Math.round(gps.course * 100);
let sEncoded = Math.round(1000 * gps.speed / 36);
if(gps.lat < 0) {
latEncoded += 0x100000000;
}
if(gps.lon < 0) {
lonEncoded += 0x100000000;
}
let s = [ sEncoded & 0xff, (sEncoded >> 8) & 0xff ];
let lat = [ latEncoded & 0xff, (latEncoded >> 8) & 0xff,
(latEncoded >> 16) & 0xff, (latEncoded >> 24) & 0xff ];
let lon = [ lonEncoded & 0xff, (lonEncoded >> 8) & 0xff,
(lonEncoded >> 16) & 0xff, (lonEncoded >> 24) & 0xff ];
let h = [ hEncoded & 0xff, (hEncoded >> 8) & 0xff ];
let s = toByteArray(Math.round(1000 * gps.speed / 36), 2, false);
let lat = toByteArray(Math.round(gps.lat * 10000000), 4, true);
let lon = toByteArray(Math.round(gps.lon * 10000000), 4, true);
let e = toByteArray(Math.round(gps.alt * 100), 3, true);
let h = toByteArray(Math.round(gps.course * 100), 2, false);
return [
0x02, 0x01, 0x06, // Flags
0x11, 0x16, 0x67, 0x2a, 0x95, 0x02, s[0], s[1], lat[0], lat[1], lat[2],
lat[3], lon[0], lon[1], lon[2], lon[3], h[0], h[1] // Location and Speed
0x02, 0x01, 0x06, // Flags
0x14, 0x16, 0x67, 0x2a, 0x9d, 0x02, s[0], s[1], lat[0], lat[1], lat[2],
lat[3], lon[0], lon[1], lon[2], lon[3], e[0], e[1], e[2], h[0], h[1]
// Location and Speed
];
}
// Encode the mag service data using the magnetic flux density 3D characteristic
function encodeMagServiceData() {
let xEncoded = mag.x; // TODO: units???
let yEncoded = mag.y;
let zEncoded = mag.z;
if(xEncoded < 0) {
xEncoded += 0x10000;
}
if(yEncoded < 0) {
yEncoded += 0x10000;
}
if(yEncoded < 0) {
yEncoded += 0x10000;
}
let x = [ xEncoded & 0xff, (xEncoded >> 8) & 0xff ];
let y = [ yEncoded & 0xff, (yEncoded >> 8) & 0xff ];
let z = [ zEncoded & 0xff, (zEncoded >> 8) & 0xff ];
let x = toByteArray(mag.x, 2, true);
let y = toByteArray(mag.y, 2, true);
let z = toByteArray(mag.z, 2, true);
return [
0x02, 0x01, 0x06, // Flags
@ -211,6 +174,22 @@ function encodeMagServiceData() {
}
// Convert the given value to a little endian byte array
function toByteArray(value, numberOfBytes, isSigned) {
let byteArray = new Array(numberOfBytes);
if(isSigned && (value < 0)) {
value += 1 << (numberOfBytes * 8);
}
for(let index = 0; index < numberOfBytes; index++) {
byteArray[index] = (value >> (index * 8)) & 0xff;
}
return byteArray;
}
// Update acceleration
Bangle.on('accel', function(newAcc) {
acc = newAcc;

2
apps/slimehunt/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Public version is a go!
0.02: Fixed bug where Critial Up wasn't letting player attack.

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

@ -0,0 +1,77 @@
<><><><>-SLIME HUNT-<><><><>
Slime Hunt is a RPG turn-based style combat game
where you fight slimes until your HP runs out.
The main goal is to beat your personal highscore!
============================
During each fight the player has 3 options,
BTN1) FIGHT
- Attacks the slime, dealing 1 hp worth of damage.
BTN2) DEFEND
- Defends against the slime, blocking 3 damage from the next slime attack.
BTN3) RUN
- Find a new slime to fight against. (This could change in the future!)
============================
There are currently 5 types of slime each with unique behavior.
<><>-BEHAVIORS-<><>
1. NEUTRAL
- Slime deals 0-1 damage on it's next attack.
2. ANGRY
- Slime deals 3-5 damage on it's next attack.
3. ERACTIC
- Slime deals 0-5 damage on it's next attack.
<><>-ITEMS-<><>
1. Attack Up
- +1 damage next battle.
2. Defence Up
- +1 defence next battle, stacks with block.
Setting defence to 4 when using DEFEND, and 1 otherwise.
3. HP Up
- +3 HP.
4. Block Up
- +2 block on DEFEND next battle,
setting Defence to 5 when using DEFEND command.
5. Critical Up
- 20% chance to crit next battle on each attack,
instantly defeating the Slime.
*****Using the RUN command causes you to lose your item!*****
<><>-SLIMES-<><>
1. GREEN SLIME
- Is always neutral. | 0% chance of item.
2. RED SLIME
- Can be either neutral or angry. | 10% chance of item.
3. GRAY SLIME
- Can be neutral, angry or eratic. | 20% chance of item.
4. YELLOW SLIME
- Is always eratic. | 50% chance of item.
5. PURPLE SLIME
- Is always angry. | 100% chance of item.
============================
Created by Colton LaChance!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH9bxgAM1gtsGTwtTGDVhFSPX64wZLqgwFF6iMVGAhgUF6owBMCzsWAAthL1AAGF/4vxrdhADVbeCQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AFA"))

477
apps/slimehunt/app.js Normal file
View File

@ -0,0 +1,477 @@
//Create constants------------------------------------------------------------------
//Slimes
const GREEN_SLIME = 1; //Normal slime, is always neutral. | 0% Item chance
const PINK_SLIME = 2; //Can get angry. | 10% Item chance
const GRAY_SLIME = 3; //Can be neutral, angry or erratic. | 20% Item chance
const YELLOW_SLIME = 4; //Is always erratic. | 50% Item chance
const PURPLE_SLIME = 5; //Is always angry. | 100% Item chance
//Items
const ITEM_ATK_UP = 1; //Raises damage dealt by +1 for next battle
const ITEM_DEF_UP = 2; //Reduces all damage by +1 for next battle
const ITEM_HP_UP = 3; //Increases HP by 3
const ITEM_BLOCK_UP = 4; //Raises defence when defending by from 3 to 5 for next battle
const ITEM_CRIT_UP = 5; //Gives attack a 20% chance to instantly KO slime for next battle
//Base stats
const BASE_ATK = 1;
const BASE_DEF = 0;
const BASE_BLOCK = 3;
const BASE_CRIT = 0;
//Initialize variables------------------------------------------------------------------
var playerHP = 20;
var slimeHP = 3;
var slimeType = GREEN_SLIME;
var turn = 0;
var screenWidth = g.getWidth();
var screenHeight = g.getHeight();
var slimeState = 0;
var showBattleResult = false;
var dmgDealt = 0;
var playerDefence = 0;
var playerItem = 0;
var critChance = 0;
//Stats (Modifiers)
var statAtk = 1;
var statDef = 0;
var statBlock = 3;
var statCrit = 0;
//Item vars
var itemName = "";
var itemDesc = "";
var itemChance = 0;
var refreshInterval;
var waitTime = 0;
var highscore = 0;
var score = 0;
var themeNote = 0;
//Load files------------------------------------------------------------------
var file = require("Storage").open("highscore.txt", "r");
highscore = file.readLine();
if (highscore == undefined) highscore = 0;
var greenSlime = require("Storage").read("slime.img");
var pinkSlime = require("Storage").read("slimered.img");
var graySlime = require("Storage").read("slimegray.img");
var yellowSlime = require("Storage").read("slimeyellow.img");
var purpleSlime = require("Storage").read("slimepurple.img");
//UI Stuff------------------------------------------------------------------
function drawOpeningUI() {
g.clear();
g.setFont("Vector", screenWidth / 15);
g.setFontAlign(0, 0); // center font
g.drawString("SLIME HUNT", screenWidth / 2, screenHeight * 0.1);
g.drawString("-SCORE TO BEAT-", screenWidth / 2, screenHeight * 0.3);
g.drawString("<><><> " + highscore + " <><><>", screenWidth / 2, screenHeight * 0.45);
g.setFont("Vector", screenWidth / 20);
g.drawString("A Slime approches...", screenWidth / 2, screenHeight * 0.6);
wait(8, waitForBattle);
}
function drawSlime() {
switch (slimeType) {
case GREEN_SLIME:
g.drawImage(greenSlime, screenWidth / 2, screenHeight / 2, {
scale: 4,
rotate: 0
});
break;
case PINK_SLIME:
g.drawImage(pinkSlime, screenWidth / 2, screenHeight / 2, {
scale: 4,
rotate: 0
});
break;
case GRAY_SLIME:
g.drawImage(graySlime, screenWidth / 2, screenHeight / 2, {
scale: 4,
rotate: 0
});
break;
case YELLOW_SLIME:
g.drawImage(yellowSlime, screenWidth / 2, screenHeight / 2, {
scale: 4,
rotate: 0
});
break;
case PURPLE_SLIME:
g.drawImage(purpleSlime, screenWidth / 2, screenHeight / 2, {
scale: 4,
rotate: 0
});
break;
}
}
function drawBattleUI() {
g.clear();
g.setFont("Vector", screenWidth / 8);
g.setFontAlign(0, 0); // center font
g.drawString("SLIME HP: " + slimeHP, screenWidth / 2, screenHeight * 0.1);
g.setFont("Vector", screenWidth / 20);
if (!showBattleResult) {
switch (slimeState) {
case 0:
g.drawString("The slime seems neutral...", screenWidth / 2, screenHeight * 0.25);
break;
case 1:
g.drawString("The slime seems angry...", screenWidth / 2, screenHeight * 0.25);
break;
case 2:
g.drawString("The slime seems eratic...", screenWidth / 2, screenHeight * 0.25);
break;
}
} else {
var brString = (turn == 0 ? "The Slime loses " : "You lose ");
g.drawString(brString + dmgDealt + "HP!", screenWidth / 2, screenHeight * 0.25);
}
drawSlime();
g.drawLine(0, screenHeight * 0.72, screenWidth, screenHeight * 0.72);
if (turn == 0) {
g.setFont("Vector", screenWidth / 15);
g.drawString("Your HP is " + playerHP + ".", screenWidth / 2, screenHeight * 0.8);
g.setFont("Vector", screenWidth / 20);
g.drawString("(B1) FIGHT\t|\t(B2) DEFEND\t|\t(B3) RUN", screenWidth / 2, screenHeight * 0.9);
}
}
//Win / lose functions------------------------------------------------------------------
function win() {
wait(5, winTheme);
calcScore(slimeType);
showBattleResult = false;
g.clear();
g.setFont("Vector", screenWidth / 8);
g.setFontAlign(0, 0); // center font
g.drawString("YOU WON!", screenWidth / 2, screenHeight * 0.1);
g.drawLine(0, screenHeight * 0.2, screenWidth, screenHeight * 0.2);
g.setFont("Vector", screenWidth / 12);
g.drawString((playerItem == 0 ? "No Item." : "GOT ITEM!"), screenWidth / 2, screenHeight * 0.27);
g.setFont("Vector", screenWidth / 15);
g.drawString((playerItem == 0 ? "" : "<><> " + itemName + " <><>"), screenWidth / 2, screenHeight * 0.40);
g.setFont("Vector", screenWidth / 20);
g.drawString((playerItem == 0 ? "" : itemDesc), screenWidth / 2, screenHeight * 0.52);
g.drawLine(0, screenHeight * 0.6, screenWidth, screenHeight * 0.6);
g.drawString("Your score is << " + score + " >>", screenWidth / 2, screenHeight * 0.75);
g.drawString("Press (B3) to find another slime!", screenWidth / 2, screenHeight * 0.9);
turn = 0;
setWatch(run, BTN3);
}
function lose() {
wait(5, loseTheme);
playerHP = 20;
showBattleResult = false;
g.clear();
g.setFont("Vector", screenWidth / 8);
g.setFontAlign(0, 0); // center font
g.drawString("You lose...", screenWidth / 2, screenHeight * 0.1);
g.drawLine(0, screenHeight * 0.2, screenWidth, screenHeight * 0.2);
g.setFont("Vector", screenWidth / 12);
g.drawString((score > highscore ? "-NEW HIGHSCORE-" : "-SCORE TO BEAT-"), screenWidth / 2, screenHeight * 0.27);
g.setFont("Vector", screenWidth / 15);
g.drawString((score > highscore ? "<><> " + score + " <><>" : "<><> " + highscore + " <><>"), screenWidth / 2, screenHeight * 0.43);
g.drawLine(0, screenHeight * 0.6, screenWidth, screenHeight * 0.6);
g.setFont("Vector", screenWidth / 20);
g.drawString("Your score is << " + score + " >>", screenWidth / 2, screenHeight * 0.75);
g.drawString("Press (B3) to try again...", screenWidth / 2, screenHeight * 0.9);
score = 0;
turn = 0;
setWatch(run, BTN3);
}
//Battle Stuff------------------------------------------------------------------
function nextTurn() {
turn = (turn == 0 ? 1 : 0);
}
function slimeFight() {
Bangle.beep(100, 500);
switch (slimeState) {
case 0:
dmgDealt = Math.floor(Math.random() * 2);
break;
case 1:
dmgDealt = Math.floor(Math.random() * 3) + 3;
break;
case 2:
dmgDealt = Math.floor(Math.random() * 6);
break;
}
dmgDealt = Math.max(0, dmgDealt - playerDefence);
playerHP -= dmgDealt;
slimeAI();
}
function fight() {
if (turn == 0 && waitTime <= 0) {
Bangle.beep(100, 1000);
dmgDealt = statAtk;
playerDefence = statDef;
if (statCrit == 0) {
slimeHP -= dmgDealt;
}else{
critChance = Math.floor(Math.random() * 100);
if (critChance >= 100-statCrit) {
slimeHP = 0;
dmgDealt = 99;
}else{
slimeHP -= dmgDealt;
}
critChance = 0;
}
showBattleResult = true;
drawBattleUI();
wait(5, waitForTurn);
}
}
function defend() {
if (turn == 0 && waitTime <= 0) {
dmgDealt = 0;
playerDefence = statBlock + statDef;
showBattleResult = true;
drawBattleUI();
wait(5, waitForTurn);
}
}
function run() {
if (turn == 0 && waitTime <= 0) {
showBattleResult = false;
Bangle.beep(200, 4000);
wait(3, waitForBattle);
}
}
function newBattle() {
showBattleResult = false;
slimeType = Math.floor(Math.random() * 5) + 1;
useItem(); //Use item at start of new battle
switch (slimeType) {
case GREEN_SLIME:
slimeHP = 3;
break;
case PINK_SLIME:
slimeHP = 3;
break;
case GRAY_SLIME:
slimeHP = 5;
break;
case YELLOW_SLIME:
slimeHP = 5;
break;
case PURPLE_SLIME:
slimeHP = 5;
break;
}
turn = 0;
battle();
slimeAI();
drawBattleUI();
}
function battle() {
setWatch(fight, BTN1);
setWatch(defend, BTN2);
setWatch(run, BTN3);
}
function slimeAI() {
switch (slimeType) {
case GREEN_SLIME:
slimeState = 0;
break;
case PINK_SLIME:
slimeState = Math.floor(Math.random() * 2);
break;
case GRAY_SLIME:
slimeState = Math.floor(Math.random() * 3);
break;
case YELLOW_SLIME:
slimeState = 2;
break;
case PURPLE_SLIME:
slimeState = 1;
break;
}
}
//Items------------------------------------------------------------------
function getItem() {
playerItem = Math.floor(Math.random() * 5) + 1;
switch (playerItem) {
case ITEM_ATK_UP:
itemName = "Attack Up";
itemDesc = "+1 damage next battle.";
break;
case ITEM_DEF_UP:
itemName = "Defence Up";
itemDesc = "+1 defence next battle.";
break;
case ITEM_HP_UP:
itemName = "HP Up";
itemDesc = "+3 HP.";
break;
case ITEM_BLOCK_UP:
itemName = "Block Up";
itemDesc = "+2 block on DEFEND next battle.";
break;
case ITEM_CRIT_UP:
itemName = "Critical Up";
itemDesc = "20% chance to crit next battle.";
break;
}
}
function useItem() {
statAtk = BASE_ATK;
statDef = BASE_DEF;
statBlock = BASE_BLOCK;
statCrit = BASE_CRIT;
switch (playerItem) {
case ITEM_ATK_UP:
statAtk = 2;
break;
case ITEM_DEF_UP:
statDef = 1;
break;
case ITEM_HP_UP:
playerHP += 3;
break;
case ITEM_BLOCK_UP:
statBlock = 5;
break;
case ITEM_CRIT_UP:
statCrit = 20;
break;
}
playerItem = 0;
}
//Timed transitions------------------------------------------------------------------
function wait(duration, waitFunc) {
waitTime = duration;
if (!refreshInterval)
refreshInterval = setInterval(waitFunc, 500);
}
function waitForTurn() {
waitTime--;
if (waitTime <= 0) {
clearInterval(refreshInterval);
refreshInterval = undefined;
nextTurn();
if (playerHP > 0 && slimeHP > 0) {
if (turn == 1) {
slimeFight();
wait(5, waitForTurn);
} else {
showBattleResult = false;
battle();
}
drawBattleUI();
} else {
if (playerHP <= 0) {
lose();
}
if (slimeHP <= 0) {
win();
}
}
}
Bangle.setLCDPower(1);
}
function waitForBattle() {
waitTime--;
Bangle.beep(100, 1000);
if (waitTime <= 0) {
clearInterval(refreshInterval);
refreshInterval = undefined;
showBattleResult = false;
newBattle();
}
Bangle.setLCDPower(1);
}
function winTheme() {
waitTime--;
Bangle.beep(200, 100 * themeNote);
themeNote++;
if (waitTime <= 0) {
themeNote = 0;
clearInterval(refreshInterval);
refreshInterval = undefined;
setWatch(run, BTN3);
}
Bangle.setLCDPower(1);
}
function loseTheme() {
waitTime--;
Bangle.beep(200, 600 - (100 * themeNote));
themeNote++;
if (waitTime <= 0) {
themeNote = 0;
clearInterval(refreshInterval);
refreshInterval = undefined;
setWatch(run, BTN3);
}
Bangle.setLCDPower(1);
}
//Calculations------------------------------------------------------------------
function calcScore(slimeType) {
switch (slimeType) {
case GREEN_SLIME:
score += 1;
//No items
break;
case PINK_SLIME:
score += 2;
itemChance = Math.floor(Math.random() * 100);
if (itemChance >= 100 - 10) { //100 - ITEM CHANCE %
getItem();
}
break;
case GRAY_SLIME:
score += 3;
itemChance = Math.floor(Math.random() * 100);
if (itemChance >= 100 - 25) { //100 - ITEM CHANCE %
getItem();
}
break;
case YELLOW_SLIME:
score += 5;
itemChance = Math.floor(Math.random() * 100);
if (itemChance >= 100 - 50) { //100 - ITEM CHANCE %
getItem();
}
break;
case PURPLE_SLIME:
score += 10;
getItem();
break;
}
if (score > highscore) {
file.erase();
file = require("Storage").open("highscore.txt", "w");
file.write(score);
}
}
//------------------------------------GAME STARTS HERE -----------------------------------------------
//Load opening UI
drawOpeningUI();

BIN
apps/slimehunt/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

View File

@ -4,3 +4,4 @@
0.05: Make Bluetooth widget thinner, and when on a bright theme use light grey for disabled color
0.06: Tweaking colors for dark/light themes and low bpp screens
0.07: Memory usage improvements
0.08: Disable LCD on, on bluetooth status change

View File

@ -7,7 +7,6 @@ WIDGETS["bluetooth"]={area:"tr",width:15,draw:function() {
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y);
},changed:function() {
WIDGETS["bluetooth"].draw();
Bangle.setLCDPower(1); // turn screen on
}};
NRF.on('connect',WIDGETS["bluetooth"].changed);
NRF.on('disconnect',WIDGETS["bluetooth"].changed);

View File

@ -76,8 +76,11 @@ function globToRegex(pattern) {
const isGlob = f => /[?*]/.test(f)
// All storage+data files in all apps: {app:<appid>,[file:<storage.name> | data:<data.name|data.wildcard>]}
let allFiles = [];
let existingApps = [];
apps.forEach((app,appIdx) => {
if (!app.id) ERROR(`App ${appIdx} has no id`);
if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`);
existingApps.push(app.id);
//console.log(`Checking ${app.id}...`);
var appDir = APPSDIR+app.id+"/";
if (!fs.existsSync(APPSDIR+app.id)) ERROR(`App ${app.id} has no directory`);