Merge branch 'espruino:master' into roman-autolight

pull/1118/head
stephenPspackman 2021-12-16 12:25:26 -08:00 committed by GitHub
commit 4f6a2452fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 3902 additions and 408 deletions

157
apps.json
View File

@ -16,7 +16,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.38",
"version": "0.39",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
@ -77,7 +77,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.11",
"version": "0.13",
"description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png",
"type": "app",
@ -92,14 +92,15 @@
{"name":"messages","url":"lib.js"}
],
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}],
"sortorder": -9
},
{
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.04",
"description": "Display notifications/music/etc from Gadgetbridge on Android. This replaces the old Gadgetbridge widget.",
"version": "0.05",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications",
"dependencies": {"messages":"app"},
@ -115,7 +116,7 @@
{
"id": "ios",
"name": "iOS Integration",
"version": "0.06",
"version": "0.07",
"description": "Display notifications/music/etc from iOS devices",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",
@ -166,7 +167,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.36",
"version": "0.37",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
@ -217,7 +218,7 @@
{
"id": "locale",
"name": "Languages",
"version": "0.13",
"version": "0.14",
"description": "Translations for different countries",
"icon": "locale.png",
"type": "locale",
@ -303,7 +304,7 @@
"id": "gbridge",
"name": "Gadgetbridge",
"version": "0.25",
"description": "(NOT RECOMMENDED) Handles Gadgetbridge notifications from Android. This is now replaced by the 'Android' app.",
"description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android' Bangle.js app instead.",
"icon": "app.png",
"type": "widget",
"tags": "tool,system,android,widget",
@ -1589,7 +1590,7 @@
{
"id": "widpedom",
"name": "Pedometer widget",
"version": "0.19",
"version": "0.20",
"description": "Daily pedometer widget",
"icon": "widget.png",
"type": "widget",
@ -1698,14 +1699,15 @@
"id": "rtorch",
"name": "Red Torch",
"shortName": "RedTorch",
"version": "0.01",
"description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets",
"version": "0.02",
"description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or on Bangle 1 press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets",
"icon": "app.png",
"tags": "tool,torch",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"rtorch.app.js","url":"app.js"},
{"name":"rtorch.wid.js","url":"widget.js"},
{"name":"rtorch.wid.js","url":"widget.js", "supports": ["BANGLEJS"]},
{"name":"rtorch.img","url":"app-icon.js","evaluate":true}
]
},
@ -2167,14 +2169,15 @@
{ "id": "snek",
"name": "The snek game",
"shortName":"Snek",
"version": "0.01",
"version": "0.02",
"description": "A snek game where you control a snek to eat all the apples!",
"icon": "snek-icon.js",
"screenshots": [{"url":"screenshot_snek.png"}],
"icon": "snek.png",
"supports": ["BANGLEJS2"],
"tags": "game,fun",
"storage": [
{"name":"snek.app.js","url":"snek.js"},
{"name":"snek.img","url":"snek-icon.js","evaluate":true}
{"name":"snek.img","url":"snek.icon.js","evaluate":true}
]
},
{
@ -3294,7 +3297,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.05",
"version": "0.07",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",
@ -3305,8 +3308,11 @@
"storage": [
{"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]},
{"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]},
{"name":"dtlaunch.settings.js","url":"settings-b1.js", "supports": ["BANGLEJS"]},
{"name":"dtlaunch.settings.js","url":"settings-b2.js", "supports": ["BANGLEJS2"]},
{"name":"dtlaunch.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"dtlaunch.json"}]
},
{
"id": "HRV",
@ -3472,8 +3478,8 @@
{
"id": "speedalt2",
"name": "GPS Adventure Sports II",
"shortName": "GPS Adv Sport II",
"version": "0.07",
"shortName":"GPS Adv Sport II",
"version":"1.10",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"icon": "app.png",
"type": "app",
@ -4008,11 +4014,11 @@
{
"id": "thermom",
"name": "Thermometer",
"version": "0.03",
"version": "0.04",
"description": "Displays the current temperature in degree Celsius, updated every 20 seconds",
"icon": "app.png",
"tags": "tool",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"thermom.app.js","url":"app.js"},
@ -4203,10 +4209,10 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"version": "0.08",
"version": "0.09",
"description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"icon": "pastel.png",
"dependencies": {"mylocation":"app"},
"dependencies": {"mylocation":"app", "widpedom":"app"},
"screenshots": [{"url":"screenshot_pastel.png"}],
"type": "clock",
"tags": "clock",
@ -4644,7 +4650,7 @@
"id": "sensible",
"name": "SensiBLE",
"shortName": "SensiBLE",
"version": "0.03",
"version": "0.04",
"description": "Collect, display and advertise real-time sensor data.",
"icon": "sensible.png",
"screenshots": [
@ -4717,8 +4723,9 @@
"id": "pebble",
"name": "Pebble Clock",
"shortName": "Pebble",
"version": "0.04",
"version": "0.06",
"description": "A pebble style clock to keep the rebellion going",
"dependencies": {"widpedom":"app"},
"readme": "README.md",
"icon": "pebble.png",
"screenshots": [{"url":"pebble_screenshot.png"}],
@ -4852,10 +4859,10 @@
"id": "ptlaunch",
"name": "Pattern Launcher",
"shortName": "Pattern Launcher",
"version": "0.10",
"version": "0.11",
"description": "Directly launch apps from the clock screen with custom patterns.",
"icon": "app.png",
"screenshots": [{"url":"main_menu_add.png"}, {"url":"add_pattern.png"}, {"url":"select_app.png"}, {"url":"main_menu_manage.png"}, {"url":"manage_patterns.png"}],
"screenshots": [{"url":"manage_patterns_light.png"}],
"tags": "tools",
"supports": ["BANGLEJS2"],
"readme": "README.md",
@ -4870,11 +4877,11 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
"version": "0.01",
"version": "0.03",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
"dependencies": {"mylocation":"app"},
"dependencies": {"mylocation":"app", "widpedom":"app"},
"screenshots": [{"url":"screenshot_rebble.png"}],
"type": "clock",
"tags": "clock",
@ -4912,6 +4919,7 @@
"supports" : ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"clicompleteclk.app.js","url":"app.js"},
{"name":"clicompleteclk.img","url":"app-icon.js","evaluate":true},
{"name":"clicompleteclk.settings.js","url":"settings.js"}
],
@ -4931,5 +4939,92 @@
{"name":"awairmonitor.app.js","url":"app.js"},
{"name":"awairmonitor.img","url":"app-icon.js","evaluate":true}
]
}
},
{ "id": "pooqround",
"name": "pooq Round watch face",
"shortName":"pooq Round",
"version":"0.01",
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"pooqround.app.js","url":"app.js"},
{"name":"pooqround.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"pooqround.json"}
]
},
{
"id": "coretemp",
"name": "Core Temp Display",
"version": "0.01",
"description": "Display CoreTemp device sensor data",
"icon": "coretemp.png",
"type": "app",
"tags": "health",
"readme": "README.md",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"coretemp.boot.js","url":"boot.js"},
{"name":"coretemp.app.js","url":"coretemp.js"},
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true}
]
},
{
"id": "showimg",
"name": "simple image viewer",
"shortName":"showImage",
"version":"0.2",
"description": "Displays the image in \"showimage.user.img\". The file has to be uploaded via the espruino IDE. Returns to watch face after 60s or button push. I use it to display my vaccination certificate.",
"icon": "app.png",
"tags": "tool",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"showimg.app.js","url":"app.js"},
{"name":"showimg.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "lapcounter",
"name": "Lap Counter",
"version": "0.01",
"description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "tool,outdoors",
"readme":"README.md",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"lapcounter.app.js","url":"app.js"},
{"name":"lapcounter.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.01",
"description": "A clock with circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"dependencies": {"widpedom":"app"},
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"circlesclock.app.js","url":"app.js"},
{"name":"circlesclock.img","url":"app-icon.js","evaluate":true},
{"name":"circlesclock.settings.js","url":"settings.js"}
],
"data": [
{"name":"circlesclock.json"}
]
}
]

View File

@ -21,8 +21,8 @@ function showAlarm(alarm) {
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showPrompt(msg,{
title:alarm.timer ? "TIMER!" : "ALARM!",
buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins
title:alarm.timer ? /*LANG*/"TIMER!" : /*LANG*/"ALARM!",
buttons : {/*LANG*/"Sleep":true,/*LANG*/"Ok":false} // default is sleep so it'll come back in 10 mins
}).then(function(sleep) {
buzzCount = 0;
if (sleep) {

View File

@ -33,16 +33,16 @@ function getCurrentHr() {
function showMainMenu() {
const menu = {
'': { 'title': 'Alarm/Timer' },
'< Back' : ()=>{load();},
'New Alarm': ()=>editAlarm(-1),
'New Timer': ()=>editTimer(-1)
/*LANG*/'< Back' : ()=>{load();},
/*LANG*/'New Alarm': ()=>editAlarm(-1),
/*LANG*/'New Timer': ()=>editTimer(-1)
};
alarms.forEach((alarm,idx)=>{
if (alarm.timer) {
txt = "TIMER "+(alarm.on?"on ":"off ")+formatMins(alarm.timer);
txt = /*LANG*/"TIMER "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatMins(alarm.timer);
} else {
txt = "ALARM "+(alarm.on?"on ":"off ")+formatTime(alarm.hr);
if (alarm.rp) txt += " (repeat)";
txt = /*LANG*/"ALARM "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatTime(alarm.hr);
if (alarm.rp) txt += /*LANG*/" (repeat)";
}
menu[txt] = function() {
if (alarm.timer) editTimer(idx);
@ -70,27 +70,27 @@ function editAlarm(alarmIndex) {
as = a.as;
}
const menu = {
'': { 'title': 'Alarm' },
'< Back' : showMainMenu,
'Hours': {
'': { 'title': /*LANG*/'Alarm' },
/*LANG*/'< Back' : showMainMenu,
/*LANG*/'Hours': {
value: hrs,
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
},
'Minutes': {
/*LANG*/'Minutes': {
value: mins,
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
},
'Enabled': {
/*LANG*/'Enabled': {
value: en,
format: v=>v?"On":"Off",
onchange: v=>en=v
},
'Repeat': {
/*LANG*/'Repeat': {
value: en,
format: v=>v?"Yes":"No",
onchange: v=>repeat=v
},
'Auto snooze': {
/*LANG*/'Auto snooze': {
value: as,
format: v=>v?"Yes":"No",
onchange: v=>as=v
@ -108,14 +108,14 @@ function editAlarm(alarmIndex) {
last : day, rp : repeat, as: as
};
}
menu["> Save"] = function() {
menu[/*LANG*/"> Save"] = function() {
if (newAlarm) alarms.push(getAlarm());
else alarms[alarmIndex] = getAlarm();
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
if (!newAlarm) {
menu["> Delete"] = function() {
menu[/*LANG*/"> Delete"] = function() {
alarms.splice(alarmIndex,1);
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
@ -136,18 +136,18 @@ function editTimer(alarmIndex) {
en = a.on;
}
const menu = {
'': { 'title': 'Timer' },
'Hours': {
'': { 'title': /*LANG*/'Timer' },
/*LANG*/'Hours': {
value: hrs,
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
},
'Minutes': {
/*LANG*/'Minutes': {
value: mins,
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
},
'Enabled': {
/*LANG*/'Enabled': {
value: en,
format: v=>v?"On":"Off",
format: v=>v?/*LANG*/"On":/*LANG*/"Off",
onchange: v=>en=v
}
};

View File

@ -7,7 +7,7 @@
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
if (!require('Storage').read("alarm.js")) {
console.log("No alarm app!");
console.log(/*LANG*/"No alarm app!");
require('Storage').write('alarm.json',"[]");
} else {
var t = 3600000*(active[0].hr-hr);

View File

@ -3,3 +3,4 @@
Fix music control
0.03: Handling of message actions (ok/clear)
0.04: Android icon now goes to settings page with 'find phone'
0.05: Fix handling of message actions

View File

@ -65,7 +65,7 @@
// Message response
Bangle.messageResponse = (msg,response) => {
if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS" });
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here?
};
})();

View File

@ -14,6 +14,8 @@ Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awai
![](screenshot.png)
![](awair-monitor-photo.jpg)
## Creator
[@alainsaas](https://github.com/alainsaas)

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -42,3 +42,4 @@
0.36: Add comments to .boot0 to make debugging a bit easier
0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app
0.38: Option to log to file if settings.log==2
0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035)

View File

@ -88,7 +88,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
// Apply any settings-specific stuff
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
// Pre-2v10 firmwares without a theme/setUI
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted

View File

@ -0,0 +1 @@
0.01: New clock

View File

@ -0,0 +1,19 @@
# Circles clock
A clock with circles for different data at the bottom in a probably familiar style
It shows besides time, date and day of week the following information:
* Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer))
* Heart rate (when screen is on and unlocked)
* Battery
## Screenshot
![Screenshot](screenshot.png)
## TODO
* Show weather information
## Creator
Marco ([myxor](https://github.com/myxor))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("2GwwcCIf4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AF0D/4AHwAVKh4OHgIIB+BB9v4YC4BBzHAQOEj4ZEIOQUDBwcHDIv8IOJ6DBwc/IP5BHcBgAXgImMGowUC/wFBh5BlEwKqKfwhBF+AFHIOp9GZYJBjv5BLfwhBECghQBZYRBi8ALIWwXxIPq8CwJBwgYxBBhI4CQwRB0j///CPFIIwFFgE///wIMI7BIJJNC8BBIHYQFFIMI7DIJB9JX4TLBBYhBqAoZBGg4GBAAf8IEMAEoPAIJALBIPw1CBYJBGC4QAD8BAhGogLIfYRByGoQAGn//+BBIYtJBKHYRBJJoIAFR4gAcO4hBIAAzXCC4JZCh5B6R5AdIAC4jLIJZ9GRIhBgU5BBN/gSDg5B/IMYpGIP6VSC40/IMN/IKwFI+BBh8BBXHYSJBINMf//4IJi/CAAoLDADcDEQIIFIP5BSg5AF/jEfHAJB/HBBBQLgYACID5BbgF/IAXAIMAjIIKQIC+BAgAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AOj///4ROgf+AgU//gMFh4dD//wBA+AIKosGCJBBCF4I1DJoQdDn4EB4AIEg5BXC5omBIK8BFJxBHwBZOg/8vwEBv4yBZYYdBI4P/wK/Bh/4BAosBIKgmDIJcAIIQCCAA44B/BBCBAnAILUDIgUBEwYADIIc/XgJBQFIRBWHwTpCXIP/8BBIBYP/TAzUBLIRBDBAIsEILIjBGoJ3GIJiMBIIyVDILJoDgf+gBBK4AOCAAcBTAJBFBARBZj5BBOQP/RIQAGIIQCBII1/HYRBEBARB0gf/4BBFBAZBZeQMHUIRBC/4gFIJYFCIIoOEIK0/HAMH/gsDAoZBGv/ATAIdEAoUB/4OJIKi/BHAQEBUgN/BAYABaIfgh4DBGQoMCMQQdBBAeBAYSPBIKbCCj6kCGoIQEIIh3BaIpBECIIdBILQA/AH4A/AH4A/AH4A/ABsf/4AB/0A/gXQgYUBIP5B/INQABn4DCIP5B/IIl+AYICBj/wn8fwAIBh/AAYMH8ZBBgfx/5HDDQRBi////BBF/44CBgMAgIDBBAIDBBAIUBRkRBFFgZBD//AIIXgIJF/BwPwIMuAAoJBE8EOAoUH8EP/B6Bg/8I4LRCBwJBk/gFB8BBEBYUfaIQ4BIISJCBAP4j+AIOC5BYoJBIgP4TwJBxBYP8IJP/DQJBov/A/7FFAoKDBXgJBBI4JBBJoRBpF4JBFgYHBPoX//0AAYJBD8BBpGoTFFv/4CgRBCj5BnADhWBIHyPBIP7REAHt+IH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AANJkmSAQOAFctt23bAQQUOHwQCCyAsQuPHjlx4ATOHwYCDN5kEIIuSIB/jx04AQXgCZkGII+wCpY+EAQOT44rMgKACAQlwCpc2II+2ChUJII2SNxsOQAYCEChUNHwwCC7AVJHwqDDNxYQBQY9x4AVJHw4CDChECII6DBNxUECAKDInAVIgZBLsAVHiQ+DkAICyJuLCYUnSQcBQwZBIjY7D2AICIIdsVxItBoAJENxUBKofgBQgUCBAo4GPQpKDwCuIkmQBQsHNxMJS4wADCgMcBI0GIIXYMQyMGVwskJgxuDBIzZDPA8OTYIgGmxBCc44LDIJBsHNwZBJbIpuDQYNwGpB3GaIpBRgbyIIJcAQYOOILUBVxTyJgRBCCpMHQYz7DeA4ABjZBJpArJeQKDFIIWQCpMAQYxBCtgUJgZBGhJBMeQQHEiRBMQYNx4AHDhpBXeBLyDUwhBCVxKDIIIVgCpRBBWAhBNQZRBLQZJBM26DLj/+g6DRgf/4AXBQYs4IJARC//wn/guBBC3CDHAwf8h/HeQwaCIIhWDwP4C4J9DQZIpE8F+NAPwWBBBGJoKDPHAcB/HgIIkDQZApCNYV+n8DEwUOnCDL/7FBgZWCQZzFBIIqDLFIRBBDQJBCQZqbCCgaDNgZBHQZcfIIn8BwSDNTYRQEQZuBYoyDLNYRBCHYaDNIIX/QaEcgJBGQZYpCIIMH8f+QZ7dCgY7DQZrFBC4IODQZYpC//wFgOOQZ8DCgMAHYaDMVoQXBDoiDKCIUfwE/C4aDNAA6DMABCDLABKDJoAVKQZIHEAA3jQZFgCpSDJIJRWGIJ6DJIJdx44GEQcwGEQasBIINIQaMCIIOQCpMHQY0BIINsQaJBNKwxBOQY5BNgeOnAIFIINJKxaDFgBBBySDLuAIFm3btrcJTAKDFIIcgKxSDFIIdAFZE4QYxBD2CYKQZJBIbQ5BNgKYBQZJBJQYPABAsEIIMkTQ5WIgEJbhUOQYIgGgxBB2w2GTBIABIIWQd46DIgKaKCgMcFY5BC7CYIQY8AiSxCKxCDHbgckBIsDCgPgCo8bIIPbTBCDIgRBIQYRWHbgjvHTA5NCIJCDCuAWIYojIEKxLcDYoyDCCpLFIWAWACpEJkgLCQwaDBKxLcCDIagBAoKYJAAMN2wMDhiDECpLzBIIK0BBAbvITQhBDRILyCCpc2IIdsQYYVLgi0DCBYAEhDfDZZAAHgwEDIIYAQIIMkCiJBSAAcDtuwIScBIKTFFIM0SIIOAIM8btoqRIIiXTyVIINDFUgBBBoArTtgUTACsEyQWUIKsBkAVTyArUsBBqAH4AiA=="))

218
apps/circlesclock/app.js Normal file
View File

@ -0,0 +1,218 @@
const locale = require("locale");
const heatshrink = require("heatshrink");
var shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA="));
var heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
var powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI"));
const SETTINGS_FILE = "circlesclock.json";
let settings;
function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {
'maxHR': 200,
'stepGoal': 10000
};
}
const colorFg = '#fff';
const colorBg = '#000';
const colorGrey = '#808080';
let hrtValue;
const h = g.getHeight();
const w = g.getWidth();
const hOffset = 30;
const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset);
const w1 = Math.round(w / 6);
const w2 = Math.round(3 * w / 6);
const w3 = Math.round(5 * w / 6);
const radiusOuter = 22;
const radiusInner = 16;
function draw() {
g.reset();
g.setColor(colorBg);
g.fillRect(0, 0, w, h);
// time
g.setFont("Vector:50");
g.setFontAlign(-1, -1);
g.setColor(colorFg);
g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8);
// date & dow
g.setFont("Vector:20");
g.setFontAlign(-1, 0);
g.drawString(locale.date(new Date()), w / 10, h2);
g.drawString(locale.dow(new Date()), w / 10, h2 + 22);
// Steps circle
drawSteps();
// Heart circle
drawHeartRate();
// Battery circle
drawBattery();
}
function drawSteps() {
const steps = getSteps();
const blue = '#0000ff';
g.setColor(colorGrey);
g.fillCircle(w1, h3, radiusOuter);
const stepGoal = settings.stepGoal;
if (stepGoal > 0) {
let percent = steps / stepGoal;
if (stepGoal < steps) percent = 1;
drawGauge(w1, h3, percent, blue);
}
g.setColor(colorBg);
g.fillCircle(w1, h3, radiusInner);
g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(steps), w1 + 2, h3);
g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6);
}
function drawHeartRate() {
const red = '#ff0000';
g.setColor(colorGrey);
g.fillCircle(w2, h3, radiusOuter);
if (hrtValue != undefined) {
const percent = hrtValue / settings.maxHR;
drawGauge(w2, h3, percent, red);
}
g.setColor(colorBg);
g.fillCircle(w2, h3, radiusInner);
g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(hrtValue != undefined ? hrtValue : 0, w2, h3);
g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6);
}
function drawBattery() {
const battery = E.getBattery();
const yellow = '#ffff00';
g.setColor(colorGrey);
g.fillCircle(w3, h3, radiusOuter);
if (battery > 0) {
const percent = battery / 100;
drawGauge(w3, h3, percent, yellow);
}
g.setColor(colorBg);
g.fillCircle(w3, h3, radiusInner);
g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(battery + '%', w3, h3);
g.drawImage(powerIcon, w3 - 6, h3 + radiusOuter - 6);
}
function radians(a) {
return a * Math.PI / 180;
}
function drawGauge(cx, cy, percent, color) {
let offset = 30;
let end = 300;
var i = 0;
var r = radiusInner + 3;
if (percent > 1) percent = 1;
var startrot = -offset;
var endrot = startrot - ((end - offset) * percent);
g.setColor(color);
// draw gauge
for (i = startrot; i > endrot; i -= 4) {
x = cx + r * Math.sin(radians(i));
y = cy + r * Math.cos(radians(i));
g.fillCircle(x, y, 4);
}
}
function shortValue(v) {
if (isNaN(v)) return '-';
if (v <= 999) return v;
if (v >= 1000 && v < 10000) {
v = Math.floor(v / 100) * 100;
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
if (v >= 10000) {
v = Math.floor(v / 1000) * 1000;
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
}
function getSteps() {
if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}
return 0;
}
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
Bangle.setHRMPower(1, "watch");
} else {
Bangle.setHRMPower(0, "watch");
}
drawHeartRate();
drawSteps();
});
Bangle.on('HRM', function(hrm) {
//if(hrm.confidence > 90){
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
drawHeartRate();
//} else {
// hrtValue = undefined;
//}
});
g.clear();
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
}
loadSettings();
setInterval(draw, 60000);
draw();
Bangle.setUI("clock");

BIN
apps/circlesclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,33 @@
(function(back) {
const SETTINGS_FILE = "circlesclock.json";
const storage = require('Storage');
let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
function save(key, value) {
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
E.showMenu({
'': { 'title': 'circlesclock' },
'max heartrate': {
value: "maxHR" in settings ? settings.maxHR : 200,
min: 20,
max : 250,
step: 10,
format: x => {
return x;
},
onchange: x => save('maxHR', x),
},
'step goal': {
value: "stepGoal" in settings ? settings.stepGoal : 10000,
min: 2000,
max : 50000,
step: 2000,
format: x => {
return x;
},
onchange: x => save('stepGoal', x),
},
'< Back': back,
});
});

1
apps/coretemp/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.1: New app

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

@ -0,0 +1,20 @@
# CoreTemp display
Basic bare-bones example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current body core temperature readings.
## Usage
On startup connects to a CoreTemp device (1809/2A1C) and emits a "Core, temp" value for each reading.
The app simply displays these readings on screen.
## TODO
* Integrate with other tracking/sports apps to log data.
* Add device selection
* Provide enable/disable option
* Check status, add Retry/reconnect
* Also provide skin temp reading
## Creator
Ivor Hewitt

23
apps/coretemp/boot.js Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///k0DxUFgsDCY8KwAfJlQLHhWglWq1WgBIcCA4QCB1WoComq0+iBYWqCwl//4OBAAQxChWlv/2BYIlCBYUqv9VvQLBwA9BBYWlqtV/QLBGoRIBgQLBr9aBYQ2BBYMKroLBtQLCgALClIKC1AXG1NVuoFBF4sC09V+woCBAJHCgWXq9oPQZrDgWdq9gBZG9rqgCTwSbCgVVqysDBYkK6tWYoa/DkEJ6vaaIgWBaAILCbQhUCBYXoc4wNBBZWqBfBtB1ALKKZILCR4J3FToQLBU4KPEWoQLNZYILIa4NVcYReEcYOnqtaDAbvDgALBcg4EBlNVqtqDoOgd4YoBBYNWytWCwQdCgQLBAAVaBYkA0oLDuwLFkv1BgZGDAAMJuoKCroWEGAOnDAVftShGr////1tDdG14LB+wiEAAdqHAjTHBYgA=="))

19
apps/coretemp/coretemp.js Normal file
View File

@ -0,0 +1,19 @@
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
var btm = g.getHeight()-1;
function onCore(c) {
var px = g.getWidth()/2;
g.setFontAlign(0,0);
g.clearRect(0,24,g.getWidth(),80);
var str = c.temp + "C";
g.setFontVector(40).drawString(str,px,45);
}
Bangle.on('Core', onCore);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);

BIN
apps/coretemp/coretemp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -3,3 +3,5 @@
0.03: cycle thru pages
0.04: reset to clock after 2 mins of inactivity
0.05: add Bangle 2 version
0.06: Adds settings page (hide clocks or launchers)
0.06: Adds setting for directly launching app on touch for Bangle 2

View File

@ -2,6 +2,11 @@
*
*/
var settings = Object.assign({
showClocks: true,
showLaunchers: true,
}, require('Storage').readJSON("dtlaunch.json", true) || {});
function wdog(handle,timeout){
if(handle !== undefined){
wdog.handle = handle;
@ -17,7 +22,13 @@ function wdog(handle,timeout){
wdog(load,120000)
var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
var apps = s.list(/\.info$/).map(app=>{
var a=s.readJSON(app,1);
return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first

View File

@ -2,8 +2,20 @@
*
*/
var settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false,
}, require('Storage').readJSON("dtlaunch.json", true) || {});
var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
var apps = s.list(/\.info$/).map(app=>{
var a=s.readJSON(app,1);
return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
@ -28,7 +40,7 @@ const YOFF = 30;
function draw_icon(p,n,selected) {
var x = (n%2)*72+XOFF;
var y = n>1?72+YOFF:YOFF;
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+10,y+2,x+60,y+52);
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
g.clearRect(x+12,y+4,x+59,y+51);
g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
@ -52,7 +64,7 @@ function drawPage(p){
}
for (var i=0;i<4;i++) {
if (!apps[p*4+i]) return i;
draw_icon(p,i,selected==i);
draw_icon(p,i,selected==i && !settings.direct);
}
g.flip();
}
@ -81,9 +93,9 @@ Bangle.on("touch",(_,p)=>{
for (i=0;i<4;i++){
if((page*4+i)<Napps){
if (isTouched(p,i)) {
draw_icon(page,i,true);
if (selected>=0) {
if (selected!=i){
draw_icon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){
draw_icon(page,selected,false);
} else {
load(apps[page*4+i].src);

View File

@ -0,0 +1,33 @@
(function(back) {
var FILE = "dtlaunch.json";
var settings = Object.assign({
showClocks: true,
showLaunchers: true
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "Desktop launcher" },
"< Back" : () => back(),
'Show clocks': {
value: settings.showClocks,
format: v => v?"On":"Off",
onchange: v => {
settings.showClocks = v;
writeSettings();
}
},
'Show launchers': {
value: settings.showLaunchers,
format: v => v?"On":"Off",
onchange: v => {
settings.showLaunchers = v;
writeSettings();
}
}
});
})

View File

@ -0,0 +1,42 @@
(function(back) {
var FILE = "dtlaunch.json";
var settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "Desktop launcher" },
"< Back" : () => back(),
'Show clocks': {
value: settings.showClocks,
format: v => v?"On":"Off",
onchange: v => {
settings.showClocks = v;
writeSettings();
}
},
'Show launchers': {
value: settings.showLaunchers,
format: v => v?"On":"Off",
onchange: v => {
settings.showLaunchers = v;
writeSettings();
}
},
'Direct launch': {
value: settings.direct,
format: v => v?"On":"Off",
onchange: v => {
settings.direct = v;
writeSettings();
}
}
});
})

View File

@ -4,4 +4,7 @@
0.04: Added common bundleId's
0.05: Added more bundleId's (app-id's which can be used to
determine a friendly app name in the notifications)
0.06: Fix (not) popupping up old messages
0.06: Fix (not) popupping up old messages
0.07: Added more details from music (instead of Undefined)
Added more app identifiers

31
apps/ios/README.md Normal file
View File

@ -0,0 +1,31 @@
# iOS integration app
This is the iOS integration app for Bangle.js. This app allows you to receive
notifications from your iPhone. The Apple Notification Center Service (ANCS)
sends all the messages to your watch.
You can allow this if you connect your Bangle to your iPhone. It will be
prompted for immediatly after you connect the Bangle to the iPhone.
### Connecting your Bangle(2).js to your iPhone
The Bangle watches are Bluetooth Low Energy (BLE) devices. Sometimes they
will not be seen/detected by the Bluetooth scanner in your iPhone settings
menu.
To resolve this, you can download numerous apps who can actually scan
for BLE devices. There are great ones out there, free and paid.
We really like WebBLE, which we also recommend to load apps on your
watch with your iOS device, as Safari does not support WebBluetooth
for now. It's just a few bucks/pounds/euro's.
If you like to try a free app first, you can always use NRF Toolbox or
Bluetooth BLE Device Finder to find and connect your Bangle.
## Requests
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=ios%20app
## Creator
Gordon Williams

View File

@ -65,14 +65,16 @@ E.on('notify',msg=>{
"com.apple.facetime": "FaceTime",
"com.apple.mobilecal": "Calendar",
"com.apple.mobilemail": "Mail",
"com.apple.mobilephone": "Phone",
"com.apple.MobileSMS": "SMS Message",
"com.apple.Passbook": "iOS Wallet",
"com.apple.podcasts": "Podcasts",
"com.apple.reminders": "Reminders",
"com.apple.shortcuts": "Shortcuts",
"com.atebits.Tweetie2": "Twitter",
"com.burbn.instagram" : "Instagram",
"com.facebook.Facebook": "Facebook",
"com.facebook.Messenger": "FB Messenger",
"com.facebook.Messenger": "Messenger",
"com.google.Chromecast" : "Google Home",
"com.google.Gmail" : "GMail",
"com.google.hangouts" : "Hangouts",
@ -81,22 +83,26 @@ E.on('notify',msg=>{
"com.ifttt.ifttt" : "IFTTT",
"com.jumbo.app" : "Jumbo",
"com.linkedin.LinkedIn" : "LinkedIn",
"com.microsoft.Office.Outlook" : "Outlook Mail",
"com.nestlabs.jasper.release" : "Nest",
"com.netflix.Netflix" : "Netflix",
"com.reddit.Reddit" : "Reddit",
"com.skype.skype": "Skype",
"com.skype.SkypeForiPad": "Skype",
"com.spotify.client": "Spotify",
"com.strava.stravaride": "Strava",
"com.tinyspeck.chatlyio": "Slack",
"com.toyopagroup.picaboo": "Snapchat",
"com.ubercab.UberClient": "Uber",
"com.ubercab.UberEats": "UberEats",
"com.vilcsak.bitcoin2": "Coinbase",
"com.wordfeud.free": "WordFeud",
"com.zhiliaoapp.musically": "TikTok",
"net.whatsapp.WhatsApp": "WhatsApp",
"nl.ah.Appie": "Albert Heijn",
"nl.postnl.TrackNTrace": "PostNL",
"ph.telegra.Telegraph": "Telegram",
"tv.twitch": "Twitch",
// could also use NRF.ancsGetAppInfo(msg.appId) here
};
@ -104,7 +110,7 @@ E.on('notify',msg=>{
'2019':"'"
};
var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16));
if (appNames[msg.appId]) msg.a
//if (appNames[msg.appId]) msg.a
require("messages").pushMessage({
t : msg.event,
id : msg.uid,
@ -122,9 +128,10 @@ E.on('AMS',a=>{
function push(m) {
var msg = { t : "modify", id : "music", title:"Music" };
if (a.id=="artist") msg.artist = m;
else if (a.id=="album") msg.artist = m;
else if (a.id=="title") msg.tracl = m;
else return; // duration? need to reformat
else if (a.id=="album") msg.album = m;
else if (a.id=="title") msg.track = m;
else if (a.id=="duration") msg.dur = m;
else return;
require("messages").pushMessage(msg);
}
if (a.truncated) NRF.amsGetMusicInfo(a.id).then(push)

View File

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

19
apps/lapcounter/README.md Normal file
View File

@ -0,0 +1,19 @@
# Lap Counter
Click button to count laps (e.g. in a swimming pool).
Also shows total duration snapshot (like a stopwatch, but laid back).
![Screenshot](screenshot.png)
## Usage
* Click BTN1 to start counting. Counter becomes `0`, duration becomes `00:00.0`
* Each time you click BTN1, counter is incremented, and you see duration between first and last clicks.
## Features
Disables LCD timeout (so that you can be _sure_ what BTN1 would do).
## Creator
[Nimrod Kerrett](https://zzzen.com)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap"))

53
apps/lapcounter/app.js Normal file
View File

@ -0,0 +1,53 @@
const w = g.getWidth();
const h = g.getHeight();
const wid_h = 24;
let tStart;
let tNow;
let counter=-1;
const icon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap"));
function timeToText(t) { // Courtesy of stopwatch app
let hrs = Math.floor(t/3600000);
let mins = Math.floor(t/60000)%60;
let secs = Math.floor(t/1000)%60;
let tnth = Math.floor(t/100)%10;
let text;
if (hrs === 0)
text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
else
text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
//log_debug(text);
return text;
}
function doCounter() {
if (counter<0) {
tStart = Date.now();
tNow = tStart;
} else {
tNow = Date.now();
}
counter++;
let dT = tNow-tStart;
g.clearRect(0,wid_h,w,h-wid_h);
g.setFontAlign(0,0);
g.setFont("Vector",72);
g.drawString(counter,w/2,h/2);
g.setFont("Vector",24);
g.drawString(timeToText(dT),w/2,h/2+50);
}
setWatch(doCounter, BTN1, true);
g.clear(true);
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setLCDTimeout(0);
g.drawImage(icon,w/2-24,h/2-24);
g.setFontAlign(0,0);
require("Font8x12").add(Graphics);
g.setFont("8x12");
g.drawString("Click button to count.", w/2, h/2+22);

BIN
apps/lapcounter/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -13,3 +13,4 @@
0.11: Added translations for nl_NL and changes one formatting
0.12: Fixed nl_NL formatting, because the full months won't fit on the Bangle.js2's screen
0.13: Now use shorter de_DE date format to more closely match other languages for size
0.14: Added some first translations for Messages in nl_NL

View File

@ -37,20 +37,30 @@ const codePages = {
/* When it's not in the codepage, try and use
these conversions */
const charFallbacks = {
"ą":"a",
"ā":"a",
"č":"c",
"ř":"r",
"ő":"o",
"ć":"c",
"ě":"e",
"ę":"e",
"ą":"a",
"ē":"e",
"ģ":"g",
"i":"ī",
"ķ":"k",
"ļ":"l",
"ł":"l",
"ń":"n",
"ņ":"n",
"ő":"o",
"ó":"o",
"ř":"r",
"ś":"s",
"š":"s",
"ū":"u",
"ż":"z",
"ź":"z",
"ń":"n",
"ł":"l",
"ś":"s",
"ć":"c",
};
"ž":"z",
};
/*
timePattern / datePattern:
@ -198,7 +208,8 @@ var locales = {
day: "zondag,maandag,dinsdag,woensdag,donderdag,vrijdag,zaterdag",
abmonth: "jan,feb,mrt,apr,mei,jun,jul,aug,sep,okt,nov,dec",
month: "januari,februari,maart,april,mei,juni,juli,augustus,september,oktober,november,december",
trans: { yes: "ja", Yes: "Ja", no: "nee", No: "Nee", ok: "ok", on: "aan", off: "uit", "< Back": "< Terug" }
trans: { yes: "ja", Yes: "Ja", no: "nee", No: "Nee", ok: "ok", on: "aan", off: "uit",
"< Back": "< Terug", "Delete": "Verwijderen", "Mark Unread": "Markeer als ongelezen" }
},
"en_NL": { // English date units with Dutch number, currency and navigation units.
lang: "en_NL",
@ -630,6 +641,24 @@ var locales = {
day: "Niedziela,Poniedziałek,Wtorek,Środa,Czwartek,Piątek,Sobota",
trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "on", off: "off", "< Back": "< Wstecz" }
},
"lv_LV": { // Using charfallbacks
lang: "lv_LV",
decimal_point: ",",
thousands_sep: " ",
currency_symbol: "€",
int_curr_symbol: "EUR",
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2020 // 01.03.20
abmonth: "Jan,Feb,Mar,Apr,Mai,Jūn,Jūl,Aug,Sep,Okt,Nov,Dec",
month: "Janvāris,Februāris,Marts,Aprīlis,Maijs,Jūnijs,Jūlijs,Augusts,Septemberis,Oktobris,Novembris,Decembris",
abday: "Pr,Ot,Tr,Ce,Pk,Se,Sv",
day: "Pirmdiena,Otrdiena,Trešdiena,Ceturtdiena,Piektdiena,Sestdiena,Svētdiena",
trans: { yes: "jā", Yes: "Jā", no: "nē", No: "Nē", ok: "labi", on: "Ieslēgt", off: "Izslēgt", "< Back": "< Atpakaļ" }
},
/*,
"he_IL": { // This won't work until we get a font - see https://github.com/espruino/BangleApps/issues/399
codePage : "ISO8859-8",

View File

@ -13,4 +13,10 @@
0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
Fix phone icon (#1014)
0.10: Respect the 'new' attribute if it was set from iOS integrations
0.11: Open app when touching the widget (Bangle.js 2 only)
0.11: Open app when touching the widget (Bangle.js 2 only)
0.12: Extra app-specific notification icons
New animated notifcationicon (instead of large blinking 'MESSAGES')
Added screenshots
0.13: Add /*LANG*/ comments for internationalisation
Add 'Delete All' option to message options
Now update correctly when 'require("messages").clearAll()' is called

View File

@ -1,7 +1,5 @@
# Messages app
**THIS APP IS CURRENTLY BETA**
This app handles the display of messages and message notifications. It stores
a list of currently received messages and allows them to be listed, viewed,
and responded to.
@ -17,7 +15,17 @@ and `Messages`:
* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
* `Unread Timer` - when a new message is received we go into the Messages app.
If there is no user input for this amount of time then the app will exit and return
to the clock where `MESSAGES` will be shown in the Widget bar.
to the clock where a ringing bell will be shown in the Widget bar.
## Images
_1. Screenshot of a notification_
![](screenshot.png)
_2. What the notify icon looks like (it's touchable on Bangle.js2!)_
![](screenshot-notify.gif)
## Requests
@ -27,3 +35,11 @@ Please file any issues on https://github.com/espruino/BangleApps/issues/new?titl
## Creator
Gordon Williams
## Contributors
[Jeroen Peters](https://github.com/jeroenpeters1986)
## Attributions
Icons used in this app are from https://icons8.com

View File

@ -52,11 +52,11 @@ 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.new) {
if (msg && msg.new) {
if (WIDGETS["messages"]) WIDGETS["messages"].buzz();
else Bangle.buzz();
}
showMessage(msg.id);
showMessage(msg&&msg.id);
};
function saveMessages() {
require("Storage").writeJSON("messages.json",MESSAGES)
@ -65,6 +65,12 @@ function saveMessages() {
function getBackImage() {
return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA==");
}
function getNotificationImage() {
return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A==");
}
function getFBIcon() {
return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA==");
}
function getPosImage() {
return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA==");
}
@ -74,18 +80,28 @@ function getNegImage() {
function getMessageImage(msg) {
if (msg.img) return atob(msg.img);
var s = (msg.src||"").toLowerCase();
if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
if (s=="facebook") return getFBIcon();
if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
if (s=="instagram") return atob("GBiBAf////////////////wAP/n/n/P/z/f/b/eB7/c87/d+7/d+7/d+7/d+7/c87/eB7/f/7/P/z/n/n/wAP////////////////w==");
if (s=="gmail") return getNotificationImage();
if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA==");
if (s=="mail") return getNotificationImage();
if (s=="messenger") return getFBIcon();
if (s=="outlook mail") return getNotificationImage();
if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA==");
if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA==");
if (s=="sms message") return getNotificationImage();
if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA");
if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql");
if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
if (msg.id=="back") return getBackImage();
return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A==");
return getNotificationImage();
}
function showMapMessage(msg) {
var m;
var distance, street, target, eta;
@ -127,7 +143,7 @@ function showMapMessage(msg) {
function showMusicMessage(msg) {
function fmtTime(s) {
var m = Math.floor(s/60);
s = (s%60).toString().padStart(2,0);
s = (parseInt(s%60)).toString().padStart(2,0);
return m+":"+s;
}
@ -141,7 +157,7 @@ function showMusicMessage(msg) {
{type:"h", fillx:1, bgCol:colBg, c: [
{ type:"btn", src:getBackImage, cb:back },
{ type:"v", fillx:1, c: [
{ type:"txt", font:fontLarge, label:msg.artist, pad:2 },
{ type:"txt", font:fontMedium, label:msg.artist, pad:2 },
{ type:"txt", font:fontMedium, label:msg.album, pad:2 }
]}
]},
@ -158,24 +174,33 @@ function showMusicMessage(msg) {
}
function showMessageSettings(msg) {
E.showMenu({"":{"title":"Message"},
E.showMenu({"":{"title":/*LANG*/"Message"},
"< Back" : () => showMessage(msg.id),
"Delete" : () => {
/*LANG*/"Delete" : () => {
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0});
},
"Mark Unread" : () => {
/*LANG*/"Mark Unread" : () => {
msg.new = true;
saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0});
},
/*LANG*/"Delete all messages" : () => {
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
if (isYes) {
MESSAGES = [];
saveMessages();
}
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0});
});
},
});
}
function showMessage(msgid) {
var msg = MESSAGES.find(m=>m.id==msgid);
if (!msg) return checkMessages(); // go home if no message found
if (!msg) return checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found
if (msg.src=="Maps") {
cancelReloadTimeout(); // don't auto-reload to clock now
return showMapMessage(msg);
@ -212,7 +237,6 @@ function showMessage(msgid) {
}
if (msg.negative) {
buttons.push({type:"btn", src:getNegImage(), cb:()=>{
console.log("Response");
msg.new = false; saveMessages();
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,false);
@ -223,7 +247,7 @@ function showMessage(msgid) {
var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
layout = new Layout({ type:"v", c: [
{type:"h", fillx:1, bgCol:colBg, c: [
{ type:"btn", src:getMessageImage(msg), cb:()=>{
{ type:"btn", src:getMessageImage(msg), pad: 3, cb:()=>{
cancelReloadTimeout(); // don't auto-reload to clock now
showMessageSettings(msg);
}},
@ -250,10 +274,10 @@ function checkMessages(options) {
options=options||{};
// If no messages, just show 'no messages' and return
if (!MESSAGES.length) {
if (!options.clockIfNoMsg) return E.showPrompt("No Messages",{
title:"Messages",
if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{
title:/*LANG*/"Messages",
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
buttons : {"Ok":1}
buttons : {/*LANG*/"Ok":1}
}).then(() => { load() });
return load();
}
@ -281,7 +305,7 @@ function checkMessages(options) {
var x = r.x+2, title = msg.title, body = msg.body;
var img = getMessageImage(msg);
if (msg.id=="music") {
title = msg.artist || "Music";
title = msg.artist || /*LANG*/"Music";
body = msg.track;
}
if (img) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -2,9 +2,8 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() {
Bangle.removeListener('touch', this.touch);
if (!this.width) return;
var c = (Date.now()-this.t)/1000;
g.reset().setBgColor((c&1) ? "#0f0" : "#030").setColor((c&1) ? "#000" : "#fff");
g.clearRect(this.x,this.y,this.x+this.width,this.y+23);
g.setFont("6x8:1x2").setFontAlign(0,0).drawString("MESSAGES", this.x+this.width/2, this.y+12);
g.reset().clearRect(this.x,this.y,this.x+this.width,this.y+23);
g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y);
//if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute
let settings = require('Storage').readJSON("messages.settings.json", true) || {};
if (settings.repeat===undefined) settings.repeat = 4;

View File

@ -6,3 +6,4 @@
0.06: Converted fonts to font modules
0.07: Added info line that cycles on BTN1/BTN3 (or vitual buttons on a bangle 2)
0.08: Added dependancy on MyLocation
0.09: Added dependancy on Pedometer Widget

View File

@ -2,3 +2,5 @@
0.02: included deployment of pebble.settings.js in apps.json
0.03: Changed time+calendar font to LECO1976Regular, changed to slanting boot
0.04: Fix widget hiding code (fix #1046)
0.05: Fix typo in settings - Purple
0.06: Added dependancy on Pedometer Widget

View File

@ -18,7 +18,7 @@
storage.write(SETTINGS_FILE, settings)
}
var color_options = ['Green','Orange','Cyan','Perple','Red','Blue'];
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
E.showMenu({

2
apps/pooqround/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.00: Initial check-in.
0.01: Add tap-to-decorate feature. Bugfixes.

41
apps/pooqround/README.md Normal file
View File

@ -0,0 +1,41 @@
# pooq Round: a maximally readable, naturally 24hr, analogue watch face
This is a normal watch face for telling the time.
It is unusual in that it uses a pie chart for the hour hand. This is much easier and
more precise to read at a glance than a conventional hand, and as a bonus can distinguish
midnight (all black) from noon (all white).
The day and date are optionally displayed, typographically smooshed into the corners.
Either you'll like that, or you won't.
## Options
Although we generally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face.
We don't observe the system 12/24 setting, since it the design of the face is equally good in either interpretation.
If you like an uncluttered display style, you can still bring up the day, date and minute hand transiently with a tap on the watchface (when unlocked).
Similarly, because sometimes I don't want to burn what I'm cooking and others I'm lazy and just want to know if it's afternoon yet,
you can quickly alter the number of hands on the display. When the watch is unlocked, slide up to add dots representing the minute and second,
or down to remove the distraction. There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky,
in case you want the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails.
In some versions of the Bangle.js firmware, the backlight doesn't come on automatically when you twist your wrist. There's currently a workaround
for this integrated into the watchface; you can disable it in the menu, if you prefer.
## Limitations
Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets.
Sorry about that, but control of all the pixels was just too important to me.
There's also no support for internationalisation at present. This irks me, but since every month and day name is hand-drawn,
there's no fix other than hard work. Talk to me about it if there's a language you'd like.
## Feedback
[I'd be happy to hear your feedback](https://www.github.com/stephenPspackman) if you have comments or find any bugs, or (most especially)
if you find this work interesting.
## By
Made by [Stephen P Spackman](https://www.github.com/stephenPspackman).

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkB/4AW+ABCgAJEE4IXMh8ADAMPCoYXUK4gXMAAJHCN4oSG+I+CC4gEBC5gNBO4wuGC44IBGASPEC5ovHIox3JL4hdIR5xdIC54uIC5wWIC5hGKC5pGJC5QKCC6YKDCxIXIBQTCBC6IKDC6QKEC6IKFh52KC4gLHC5wLIC5oLJC5gLKC5YALC/4XfQZYAKh4X/C5B4V/4XYJChGBC7JIT/4wVh/wGCouBC6vwI4hIQagQWDDB5dBC45JNEwIXHGBhFDLwgYNCQQXKDBCgEC5QZFB4oGBA4IA="))

606
apps/pooqround/app.js Normal file
View File

@ -0,0 +1,606 @@
/* -*- mode: Javascript; c-basic-offset: 2; indent-tabs-mode: nil; coding: latin-1 -*- */
// pooqRound
// Copyright (c) 2021 Stephen P Spackman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Notes:
//
// This only works for Bangle 2.
const isString = x => typeof x === 'string';
const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
//////////////////////////////////////////////////////////////////////////////
/* System integration */
const storage = require('Storage');
//////////////////////////////////////////////////////////////////////////////
/* Face-specific options */
class Options {
// Protocol: subclasses must have static id and defaults fields.
// Only fields named in the defaults will be saved.
constructor() {
this.id = this.constructor.id;
this.file = `${this.id}.json`;
this.backing = storage.readJSON(this.file, true) || {};
Object.setPrototypeOf(this.backing, this.constructor.defaults);
this.reactivator = _ => this.active();
Object.keys(this.constructor.defaults).forEach(k => this.bless(k));
}
writeBack(delay) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(
() => {
this.timeout = null;
storage.writeJSON(this.file, this.backing);
},
delay
);
}
bless(k) {
Object.defineProperty(this, k, {
get: () => this.backing[k],
set: v => {
this.backing[k] = v;
// Ten second writeback delay, since the user will roll values up and down.
this.writeBack(10000);
}
});
}
showMenu(m) {
if (m instanceof Function) m = m();
if (m) {
for (const k in m) if ('init' in m[k]) m[k].value = m[k].init();
m[''].selected = -1; // Workaround for self-selection bug.
Bangle.on('drag', this.reactivator);
this.active();
} else {
if (this.bored) clearTimeout(this.bored);
this.bored = null;
Bangle.removeListener('drag', this.reactivator);
this.emit('done');
}
E.showMenu(m);
}
active() {
if (this.bored) clearTimeout(this.bored);
this.bored = setTimeout(_ => this.showMenu(), 15000);
}
reset() {
this.backing = {__proto__: this.constructor.defaults};
this.writeBack(0);
}
}
class RoundOptions extends Options {
constructor() {
super();
this.menu = () => ({
'': {title: '* face options *'},
'< Back': _ => this.showMenu(),
Ticks: {
init: _ => this.resolution,
min: 0, max: 3,
onchange: x => this.resolution = x,
format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x]
},
Calendar: {
init: _ => this.calendric,
min: 0, max: 5,
onchange: x => this.calendric = x,
format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x],
},
'Auto-Illum.': {
init: _ => this.autolight,
onchange: x => this.autolight = x
},
Defaults: _ => {this.reset(); this.interact();}
});
}
interact() {this.showMenu(this.menu);}
}
RoundOptions.id = 'pooqround';
RoundOptions.defaults = {
resolution: 1,
calendric: 5,
dayFg: '#fff',
nightFg: '#000',
autolight: true,
};
//////////////////////////////////////////////////////////////////////////////
/* Assets (generated by resourcer.js, in this directory) */
const heatshrink = require('heatshrink');
const dec = x => E.toString(heatshrink.decompress(atob(x)));
const y10F = [
dec(
'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' +
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' +
'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' +
'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' +
'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' +
'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' +
'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' +
'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' +
'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' +
'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA='
), 48, dec('hgAI'), 34
];const y1F = [
dec(
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' +
'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' +
'+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' +
'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' +
'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' +
'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' +
'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' +
'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' +
'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' +
'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' +
'BCQQUAA='
), 48, dec('hgAI'), 48
];const y10sF = [
dec(
'j/+gP//0PgE8mEAmHwgfBBQINB8AWDgcAoEGAYMMj///H///wBwNgAQPAAQMgg8B' +
'wE+hkA9kwg8Y+F4mP/4Fg/AVD4EBgcCg0MnEMmfgmH94PD4f+hkHIIgbBg44B/ng' +
'h/H/H8n4IBg4QBhwUC//Bgf+FYMwAIPAjHDwPjg//gEPLgUAOYMAn/+DAM8j1gmH' +
'h8fDBAMIHIRwDQAJtBg/8mH+gHPwEDCII/DAAM+n8B/v+h0+jkwuEw8fhV4UD8Yr' +
'DjxDB/0Ch88CoLEB+fPwK0BKIOACoQA='
), 48, dec('hAAI'), 22
];const y1sF = [
dec(
'j///0A/4ABgfAgEPgwNBg0MAYMMjwDBvAWB//gh4DBEAUDgEgAYQeBgcDEwQSCCY' +
'oDCiACBwFgGoOBwEAnODBwPhw/Ag+Bw/gv0Bwf/+EBwAkBgPgCYOA4EQgIeB8ASB' +
'g/AgcGnuAg0N8fAnkfIwPwnEB/40BgE8IYX8AYN/7hDB/kcg4xBv4TBC4kcLgUcv' +
'4ZBIgJIBHoNgHoJ8BgOGKQMHhijBnkYHoQlEv4DBRYWAv+eOgPwmEDg4mBXIXwni' +
'SBDwRICSwIABWIM/HoM//57BEoMGv7dC/DrCLoU4eYfAv4kB8f/wPB98HLgP4TQM' +
'B+EGh0PvE8QwN/+EP8E/LAK6CBIMAwPg+EDDwNgh8GJQP8h8Hz/gN4P+gBMBJIMA'
), 48, dec('hEHhAAGA'), 31
];const d10F = [
dec(
'AAXgjEAjkHgEDwPAgFwvEAh0f///44CB/ICB/4aDAQMcAQMDwAhBuAhBj0B4EH4E' +
'wgP4h0Av4JBj3gnEHzkHgPjwF4/Fwh/+CQP/HwMD4E4gJLCvAuBj0ADgOGg+B8fA' +
'uF5FoMeDQPH/l4vP8g/+vg4BzkAg/gA='
), 49, dec('hcMhYA=='), 27
];const d1F = [
dec(
'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' +
'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' +
'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' +
'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' +
'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' +
'4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' +
'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' +
'4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' +
'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' +
'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' +
'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' +
'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' +
'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' +
'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' +
'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA=='
), 48, dec('ikPigAGA'), 48
];const dowF = [
dec(
'gf8AYNwgEP/4FBvEAj//wEAnkAn0H4EAjwNBgPgAoQZBAoMOgHwAongCIQFDDoIF' +
'FDoPggYFBF4IFBGoI7B+AFCE4NwCIIlCuAdBIYU4gPwn5VBjEA//+M4d//AFDh4W' +
'BB4IgBAAX/B4n/PoQACJQIcEAokHAqAXFEYhLF/6tCApIADn4ED/zFBAAX8gaGBA' +
'AZZFQIR2GdQQYRBYgXFEYoWRKQQWCLoRrEHgoAIg7LEj7LEn4bEvk+AodwhwFD+C' +
'5E8DFEAqIdFFIo1FIIpNFLIoEEAtShCVwQEDVwIFDKAJBvAAv/Bgn/RIjzGjwFEW' +
'YicBAqAXFEYh6CRIgFKTYzjEAwt/AxxvDHAkf//AAgMDPIgVBGAnwAoYRBIYk/S4' +
'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' +
'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' +
'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' +
'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' +
'//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' +
'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' +
'p/zC4n5EYj1BAoc//4RDU4IFDA=='
), 48, dec('kElkMljsljw='), 48
];const mF = [
dec(
'/AEDvEH4AFCgPAnwMDh0B+AGD8EPwAFCg8AvgMDuED8AMEj4MDDwI0DhwOB/4ACC' +
'4M/AoX8HgIMDCoI0EAAI0EgA0DnACBGgXHL4Q0Bjn+IYXAgfOCwRpBnPHEQmcuAG' +
'DBg3csAGDj4mCAAX/QwhkBWSEDDIp3BAoZ3BBgkeDIp9FOYQMJDIomGh5NFv/wVo' +
'YABYIgZBYIYABgKWBHAcPHAKsCgF4VoJDD4AVCIYbtBfAnwgYDBg+Ag6bBEQM8EQ' +
'KoCDwMDwP9EQI0Bnk9540DZ4Y/CZ4Y0BbggwBDIY0BgP8JIbcB7yBE/pjDEAOQbZ' +
'8fRwT7DAAL7E/4zEjh9EKwLCEnB9BBhIZFgPzEwkP/jcFe4iYBdYLcEAwr5CBgYj' +
'Hh65BAxU/AwjNCIhEH/BkEGYqTCRwYMFACE4AonHZ4kcIQkB5yOEnPHIYmcuAMK7' +
'lgNJJQBJojkBKSB3BDIk/DIkBBgseDIpmEOYwMGDIsAOYkAgxBGGYjzBIwoMDXYI' +
'tCaAQFCCwP8jiECCwMBBhAZGEwwzHIAxNGTY5UKTYIMEjkORwomEnEHBhQZFgPzT' +
'gkP/hBEv+ACYivFe3adBAAfwAwoNFGYJkGh/+Axc/AwkfAoggFg/4ZgwzDj4GDiD' +
'7CAAPxRQswNIp1FBgnH4TPE/0gC4fO8wMDnPHsAMDzl2BhXcsxpFBgZQB+xqE/4z' +
'DAAMCLRJ3BwaWFBgvjDAkfuAGEu4MFfoYZBW4v/eIn/8CzEvEHBocB4E+BgcOgIn' +
'DgHgh+ANAcAvgMDuED8AMEj4MDDwI0DhyECAAQXBn4FCf4MBBgYVBGggABGghrBD' +
'gQqCGgJ0BL4QJBTYJDCBIMBJYRpCJoIAEUIoMGPIgmDVAYMFKgQAODJh3BBgkeDI' +
'p9FnAMLDIomGh5NFv/we372/exgZDe0BpCDIbBBDIl/EwonBAogMEHIIZDD4KUBH' +
'wYFDCAPBOwQWCjgMHDI4mGGYwcC+JNFiDAFOIswEAmDDAn8kAME8QYEjwMDAAN2Y' +
'QtgTonmYQoMDEwP2YQoZEgECJoozEv5NEj/+LQaYB8YMDn0fM4mAu4MDnEHuAMD8' +
'KVEIAPgEwn+WAuAK4LABj7PDwEAvhJBCwUB8EP8EffQMOgH4C4ITB+EHAYN4RwMA' +
'ng/BE4PwDYITCnw2BF4YKBF4LwDgInBKYLoFFQIAJgZCBAAZdCTYjOE/p6DgE954' +
'fEziUDgE544ME7gtEj/OExUP7hAEnJTKAAxuBFoa4BOokfBgkB4AzEniZBewhaEB' +
'goZGj61BRxMHWQIADjwJCIgLICJQQABDIL9BAAKoBg4iCgYTBKoZABhwnDJoJCDg' +
'4OCAAQXBewIABJoI5DHQSLBAAP8B4I6CcQgANgbVEOg0fEAkB8KOEnBNBVBIMMjh' +
'yEWo0MhhSPgJoBwCZDNwp2BJor2LJpjAFAAImEJwI2BAAfwj4GEXYgMBAwKlFv4G' +
'GFQpYFXQx0BAwx6DLQIGCIIgeCIAkHBgoAPn4FEh/8HQpPEn0fVCPhO4kfZ4hvGg' +
'YSEgRGFngFEgf4AwkfSws/EwgtBBhQZFEw0cOwIHEuF4AocHWIL2LBgsHGoaBBn7' +
'SD+DZEnzIFI4MPAoS1CAwbVRTYqoGWosB/p7EnvPD4mcbgk544ME7jcF5wmKh/cI' +
'Ak5LUvhGYk4VAIfDwBaEBgsB4AZEjkOGYnA4AA=='
), 49, dec('k0jk0kksmj0lk8lAwIA='), 52
];
const lockI = dec('hURwMAj0P485w1h3/4g15wFgjPmgOAs+Yg0B//AA');
const lockSI = dec('hMNwMAjkfjHMt/8g1zgOc4FnmEf/AA==');
const batteryI = dec('hERwMAjH/ABw');
const chargeI = dec('g8NwMAgkYsHDh0fw8MmFhwUA');
const HRMI = dec('iERwMAjk4l10t/29/3AIfn+ek6VTlPX9d3/U3/Ef/EP+EH8ED4EBwAA=');
const compassI = dec('hMJwMAhEEg8Dwfh2Pc43BwA=');
const y100I = dec('h8RwMAvk5/n6nOwm9w9lnzH+mO4sc4405xk7jE2mEssEd4EbgE+gE4A=');
const y100sI = dec('hcKwMAsOWvHZ+c2s1s4uYmcD4EwA');
//////////////////////////////////////////////////////////////////////////////
/* Status */
const status = (p, i) => function (g, x, y, rl) { // Nested arrows are currently broken!
if (!p()) return x;
if (rl) x -= imageWidth(i);
g.setColor(g.theme.fg).drawImage(i, x, y);
return rl ? x - 1 : x + imageWidth(i) + 1;
};
const doLocked = status(_ => Bangle.isLocked(), lockI);
const doPower = (g, x, y, rl) => {
const c = Bangle.isCharging();
const b = E.getBattery();
if (!c && b > 50) return x;
if (rl) x -= imageWidth(batteryI);
g.setColor(g.theme.fg).drawImage(batteryI, x, y);
g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0');
let h = 13 * (100 - b) / 100;
g.fillRect(x + 1, y + 2 + h, x + 6, y + 15);
if (c) g.setColor(g.theme.bg).drawImage(chargeI, x, y + 2);
return rl ? x - 1 : x + imageWidth(batteryI) + 1;
};
const doHRM = status(_ => Bangle.isHRMOn(), HRMI); // Might show Bangle.getHRM().bpm if confident?
//////////////////////////////////////////////////////////////////////////////
/* Watch face */
class Round {
constructor(g) {
this.g = g;
this.b = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb: true});
this.bI = {
width: this.b.getWidth(), height: this.b.getHeight(), bpp: this.b.getBPP(),
buffer: this.b.buffer, transparent: 0
};
this.c = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb: true});
this.cI = {
width: this.c.getWidth(), height: this.c.getHeight(), bpp: this.c.getBPP(),
buffer: this.c.buffer, transparent: 0
};
this.options = new RoundOptions();
this.timescales = [1000, [1000, 60000], 60000, 900000];
this.state = {};
// Precomputed polygons for the border areas.
this.tl = [0, 0, 58, 0, 0, 58];
this.tr = [176, 0, 176, 58, 119, 0];
this.bl = [0, 176, 0, 119, 58, 176];
this.br = [176, 176, 119, 176, 176, 119];
this.xc = g.getWidth() / 2;
this.yc = g.getHeight() / 2;
this.minR = 5;
this.secR = 3;
this.r = this.xc - this.minR;
}
reset(clear) {this.state = {}; clear && this.g.clear(true);}
doIcons(which) {
this.state[which] = null;
this.render(new Date()); // Not quite right, I think.
}
enhanceUntil(t) {this.enhance = t;}
pie(f, a0, a1, invert) {
if (!invert) return this.pie(f, a1, a0 + 1, true);
let t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI);
let i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5);
let x = f.getWidth() / 2, y = f.getHeight() / 2;
let poly = [
x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1),
y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1),
x,
y,
x + (i0 & 2 ? -x : x) * (i0 & 1 ? 1 : t0),
y + (i0 & 2 ? y : -y) / (i0 & 1 ? t0 : 1),
];
if (i1 - i0 > 4) i1 = i0 + 4;
for (i0++; i0 <= i1; i0++) poly.push(
3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0
);
f.setColor(0).fillPoly(poly);
}
hand(t, d, c0, r0, c1, r1) {
t *= Math.PI / 30;
const r = this.r;
const z = 2 * r0 + 1;
const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t);
const x0 = x - r0, y0 = y - r0;
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true});
for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) {
d.setPixel(i, j, g.getPixel(x0 + i, y0 + j));
}
g.setColor(c0).fillCircle(x, y, r0);
if (c1 !== undefined) g.setColor(c1).fillCircle(x, y, r1);
return [d, x0, y0];
}
render(d) {
const g = this.g;
const b = this.b, bI = this.bI;
const c = this.c, cI = this.cI;
const e = d < this.enhance;
const state = this.state;
const options = this.options;
const cal = options.calendric;
const res = options.resolution;
const dow = (e || cal == 1 || cal > 2) && d.getDay();
const ts = res < 2 && d.getSeconds();
const tm = (e || res < 3) && d.getMinutes() + ts / 60;
const th = d.getHours() + d.getMinutes() / 60;
const dd = (e || cal > 1) && d.getDate();
const dm = (e || cal > 3) && d.getMonth();
const dy = (e || cal > 4) && d.getFullYear();
const xc = this.xc, yc = this.yc, r = this.r;
const dlr = xc * 3/4, dlw = 8, dlhw = 4;
// Restore saveunders for fast-moving, overdrawing indicators.
if (state.sd) g.drawImage.apply(g, state.sd);
if (state.md) g.drawImage.apply(g, state.md);
if (dow !== state.dow) {
g.setColor(g.theme.bg).fillPoly(this.tl);
if (dow === +dow) {
g.setColor(g.theme.fg).setFontCustom.apply(g, dowF).drawString(dow, 5, 5);
}
state.dow = dow;
}
const locked = Bangle.isLocked();
const charging = Bangle.isCharging();
const battery = E.getBattery();
const HRMOn = Bangle.isHRMOn();
if (dy !== state.dy ||
locked !== state.locked ||
charging !== state.charging ||
Math.abs(battery - state.battery) > 2 ||
HRMOn !== state.HRMOn
) {
g.setColor(g.theme.bg).fillPoly(this.tr);
const u = dy % 10;
if (charging || battery < 50 || HRMOn || locked && dy !== +dy) {
let x = 172, y = 5;
x = doLocked(g, x, y, true);
x = doPower(g, x, y, true);
x = doHRM(g, x, y, true);
if (dy === +dy) {
g.setColor(g.theme.fg).drawImage(y100sI, 145, 23);
g.setFontCustom.apply(g, y10sF).drawString((dy - u) / 10 % 10, 157, 23);
g.setFontCustom.apply(g, y1sF).drawString(u, 165, 23);
}
} else if (dy === +dy) {
g.setColor(g.theme.fg);
if (locked) g.drawImage(lockSI, 136, 5);
else g.drawImage(y100I, 130, 5);
g.setFontCustom.apply(g, y10F).drawString((dy - u) / 10 % 10, 146, 5);
g.setFontCustom.apply(g, y1F).drawString(u, 160, 5);
}
state.dy = dy;
state.locked = Bangle.isLocked();
state.charging = Bangle.isCharging();
state.battery = E.getBattery() - E.getBattery() % 2;
state.HRMOn = Bangle.isHRMOn();
}
if (dm !== state.dm) {
g.setColor(g.theme.bg).fillPoly(this.bl);
if (dm === +dm) {
g.setColor(g.theme.fg).setFontCustom.apply(g, mF);
g.drawString(String.fromCharCode(49 + dm), 5, 124);
}
state.dm = dm;
}
if (dd !== state.dd) {
g.setColor(g.theme.bg).fillPoly(this.br);
if (dd === +dd) {
let u = dd % 10;
g.setColor(g.theme.fg).setFontAlign(1, 1);
g.setFontCustom.apply(g, d10F).drawString((dd - u) / 10, 152, 172);
g.setFontAlign(-1, 1);
g.setFontCustom.apply(g, d1F).drawString(u, 152, 172);
g.setFontAlign(-1, -1);
}
}
if (th !== state.th) {
state.th = th;
b.clear(true).fillCircle(88, 88, r - 1);
g.setColor(options.nightFg).drawImage(bI);
if (th < 12) this.pie(b, th / 12, 1, true);
else this.pie(b, 1, th / 12, true);
g.setColor(options.dayFg).drawImage(bI);
}
state.md = tm === +tm ?
this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) :
null;
state.sd = ts === +ts ?
this.hand(ts, state.sd, g.theme.fg2, this.secR) :
null;
}
}
//////////////////////////////////////////////////////////////////////////////
/* Master clock */
class Clock {
constructor(face) {
this.face = face;
this.timescales = face.timescales;
this.options = face.options;
this.rates = {};
this.options.on('done', () => this.start());
this.listeners = {
lcdPower: on => on ? this.active() : this.inactive(),
charging: () => {face.doIcons('charging'); this.active();},
lock: () => {face.doIcons('locked'); this.active();},
faceUp: up => {
this.conservative = !up;
this.active();
},
twist: _ => this.options.autolight && Bangle.setLCDPower(true),
drag: e => {
if (this.t0) {
if (e.b) {
e.x < this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x);
e.y < this.yN && (this.yN = e.y) || e.y > this.yX && (this.yX = e.y);
} else if (this.xX - this.xN < 20) {
if (e.y - this.e0.y < -50) {
this.options.resolution > 0 && this.options.resolution--;
this.rates.clock = this.timescales[this.options.resolution];
this.active();
} else if (e.y - this.e0.y > 50) {
this.options.resolution < this.timescales.length - 1 &&
this.options.resolution++;
this.rates.clock = this.timescales[this.options.resolution];
this.active();
} else if (this.yX - this.yN < 20) {
const now = new Date();
if (now - this.t0 < 250) {
face.enhanceUntil(now + 30000);
face.render(now);
} else if (now - this.t0 > 500) {
this.stop();
this.options.interact();
}
}
this.t0 = null;
}
} else if (e.b) {
this.t0 = Date.now(); this.e0 = e;
this.xN = this.xX = e.x; this.yN = this.yX = e.y;
}
}
};
}
redraw(rate) {
const now = this.updated = new Date();
if (this.refresh) this.face.reset(true);
this.refresh = false;
rate = this.face.render(now, rate);
if (rate !== this.rates.face) {
this.rates.face = rate;
this.active();
}
return this;
}
inactive() {
this.timeout && clearTimeout(this.timeout);
this.exception && clearTimeout(this.exception);
this.interval && clearInterval(this.interval);
this.timeout = this.exception = this.interval = this.rate = null;
this.face.reset(false); // Cancel any ongoing background rendering
return this;
}
active() {
const prev = this.rate;
const now = Date.now();
let rate = Infinity;
for (const k in this.rates) {
let r = this.rates[k];
r === +r || (r = r[+this.conservative])
r < rate && (rate = r);
}
const delay = rate - now % rate + 1;
this.refresh = true;
if (rate !== prev) {
this.inactive();
this.redraw(rate);
if (rate < 31622400000) { // A year!
this.timeout = setTimeout(
() => {
this.inactive();
this.interval = setInterval(() => this.redraw(rate), rate);
if (delay > 1000) this.redraw(rate);
this.rate = rate;
}, delay
);
}
} else if (rate > 1000) {
if (!this.exception) this.exception = setTimeout(() => {
this.redraw(rate);
this.exception = null;
}, this.updated + 1000 - Date.now());
}
return this;
}
stop() {
this.inactive();
for (const l in this.listeners) {
Bangle.removeListener(l, this.listeners[l]);
}
return this;
}
start() {
this.inactive(); // Reset to known state.
this.conservative = false;
this.rates.clock = this.timescales[this.options.resolution];
this.active();
for (const l in this.listeners) {
Bangle.on(l, this.listeners[l]);
}
Bangle.setUI('clock');
return this;
}
}
//////////////////////////////////////////////////////////////////////////////
/* Main */
const clock = new Clock(new Round(g)).start();

BIN
apps/pooqround/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

1671
apps/pooqround/resourcer.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
0.01: Initial creation of the pattern launch app
0.02: Turn on lcd when launching an app if the lock screen was disabled in the settings
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.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

View File

@ -10,14 +10,21 @@ Then launch the linked apps directly from the clock screen by simply drawing the
## Add Pattern Screenshots
![](main_menu_add.png)
![](add_pattern.png)
![](select_app.png)
![](main_menu_add_light.png)
![](add_pattern_light.png)
![](select_app_light.png)
![](main_menu_add_dark.png)
![](add_pattern_dark.png)
![](select_app_dark.png)
## Manage Pattern Screenshots
![](main_menu_manage.png)
![](manage_patterns.png)
![](main_menu_manage_light.png)
![](manage_patterns_light.png)
![](main_menu_manage_dark.png)
![](manage_patterns_dark.png)
## Detailed Steps

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -119,8 +119,7 @@ var recognizeAndDrawPattern = () => {
return new Promise((resolve) => {
E.showMenu();
g.clear();
g.setColor(0, 0, 0);
CIRCLES.forEach((circle) => drawCircle(circle));
drawCirclesWithPattern([]);
var pattern = [];
@ -369,7 +368,6 @@ var drawAppWithPattern = (i, r, storedPatterns) => {
offset: { x: 1, y: 3 + r.y },
});
g.setColor(0, 0, 0);
if (!storedPattern.wrappedAppName) {
storedPattern.wrappedAppName = g
.wrapString(app.name, g.getWidth() - 64)
@ -490,7 +488,7 @@ var drawCircle = (circle, drawBuffer, scale) => {
log("drawing circle");
log({ x: x, y: y, r: r });
drawBuffer.fillCircle(x, y, r);
drawBuffer.drawCircle(x, y, r);
};
var cachedCirclesDrawings = {};
@ -535,17 +533,15 @@ var drawCirclesWithPattern = (pattern, options) => {
{ msb: true }
);
drawBuffer.setColor(1);
CIRCLES.forEach((circle) => drawCircle(circle, drawBuffer, scale));
drawBuffer.setColor(0);
drawBuffer.setFontAlign(0, 0);
drawBuffer.setFont("6x8", 4 * scale);
drawBuffer.setFont("Vector", 40 * scale);
pattern.forEach((circleIndex, patternIndex) => {
var circle = CIRCLES[circleIndex];
drawBuffer.drawString(
patternIndex + 1,
circle.x * scale,
(circle.x + (scale === 1 ? 1 : 5)) * scale,
circle.y * scale
);
});

View File

@ -1,167 +1,167 @@
var DEBUG = true;
var log = (message) => {
if (DEBUG) {
console.log(JSON.stringify(message));
}
};
var storedPatterns;
var positions = [];
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 (pattern) {
if (storedPatterns[pattern]) {
var app = storedPatterns[pattern].app;
if (!!app && !!app.src) {
if (storedPatterns.settings) {
if (storedPatterns.settings.lockDisabled) {
Bangle.setLCDPower(true);
}
}
Bangle.removeListener("drag", dragHandler);
load(app.src);
}
}
}
});
};
var debounceTimeoutId;
var debounce = (delay) => {
if (debounceTimeoutId) {
clearTimeout(debounceTimeoutId);
}
return new Promise((resolve) => {
debounceTimeoutId = setTimeout(() => {
debounceTimeoutId = undefined;
resolve();
}, delay || 500);
});
};
(function () {
var DEBUG = false;
var log = (message) => {
if (DEBUG) {
console.log(JSON.stringify(message));
}
};
var storedPatterns;
var positions = [];
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 (pattern) {
if (storedPatterns[pattern]) {
var app = storedPatterns[pattern].app;
if (!!app && !!app.src) {
if (storedPatterns.settings) {
if (storedPatterns.settings.lockDisabled) {
Bangle.setLCDPower(true);
}
}
Bangle.removeListener("drag", dragHandler);
load(app.src);
}
}
}
});
};
var debounceTimeoutId;
var debounce = (delay) => {
if (debounceTimeoutId) {
clearTimeout(debounceTimeoutId);
}
return new Promise((resolve) => {
debounceTimeoutId = setTimeout(() => {
debounceTimeoutId = undefined;
resolve();
}, delay || 500);
});
};
var sui = Bangle.setUI;
Bangle.setUI = function (mode, cb) {
sui(mode, cb);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1 +1,3 @@
0.01: First release
0.02: Fix typo to Purple
0.03: Added dependancy on Pedometer Widget

View File

@ -18,7 +18,7 @@
storage.write(SETTINGS_FILE, settings)
}
var color_options = ['Green','Orange','Cyan','Perple','Red','Blue'];
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
E.showMenu({

View File

@ -1 +1,2 @@
0.01: Cloning torch and making it red :D
0.02: Modify for setUI and Bangle 2

View File

@ -2,21 +2,38 @@ Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
g.reset();
c = 1;
function setColor(delta){
c+=delta;
c = Math.max(c,0);
c = Math.min(c,2);
if (c<1){
g.setColor(c,0,0);
Bangle.setLCDBrightness(c >= 0.1 ? c : 0.1);
}else{
g.setColor(1,c-1,c-1);
Bangle.setLCDBrightness(1);
}
g.fillRect(0,0,g.getWidth(),g.getHeight());
}
setColor(0)
// BTN1 light up toward white
// BTN3 light down to red
// BTN2 to reset
setWatch(()=>setColor(0.1), BTN1, { repeat:true, edge:"rising", debounce: 50 });
setWatch(()=>load(), BTN2);
setWatch(()=>setColor(-0.1), BTN3, { repeat:true, edge:"rising", debounce: 50 });
function updownHandler(direction){
if (direction == undefined){
c=1;
setColor(0);
} else {
setColor(-direction * 0.1);
}
}
setColor(0);
// Bangle 1:
// BTN1: light up toward white
// BTN3: light down to red
// BTN2: reset
// Bangle 2:
// Swipe up: light up toward white
// Swipe down: light down to red
// BTN1: reset
Bangle.setUI("updown", updownHandler);

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Corrected variable initialisation
0.03: Advertise app name, added screenshots
0.04: Advertise bar, GPS, HRM and mag services

View File

@ -17,7 +17,7 @@ Currently implements:
- Heart Rate Monitor
- Magnetometer
in the menu display but NOT YET in Bluetooth Low Energy advertising (which will be implemented in a subsequent version).
in the menu display, and broadcasts all sensor data readings _except_ acceleration in Bluetooth Low Energy advertising packets as GATT characteristic services.
## Controls

View File

@ -7,6 +7,9 @@
// Non-user-configurable constants
const APP_ID = 'sensible';
const ESPRUINO_COMPANY_CODE = 0x0590;
const APP_ADVERTISING_DATA = [ 0x12, 0xff, 0x90, 0x05, 0x7b, 0x6e, 0x61, 0x6d,
0x65, 0x3a, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x62,
0x6c, 0x65, 0x7d ];
// Global variables
@ -20,6 +23,12 @@ let isBarEnabled = true;
let isGpsEnabled = true;
let isHrmEnabled = true;
let isMagEnabled = true;
let isNewAccData = false;
let isNewBarData = false;
let isNewGpsData = false;
let isNewHrmData = false;
let isNewMagData = false;
// Menus
@ -91,22 +100,121 @@ let magMenu = {
};
// Transmit the app name under the Espruino company code to facilitate discovery
function transmitAppName() {
let options = {
showName: false,
manufacturer: ESPRUINO_COMPANY_CODE,
manufacturerData: JSON.stringify({ name: APP_ID }),
interval: 2000
// Check for new sensor data and update the advertising sequence
function transmitUpdatedSensorData() {
let data = [ APP_ADVERTISING_DATA ]; // Always advertise at least app name
if(isNewBarData) {
data.push(encodeBarServiceData());
isNewBarData = false;
}
NRF.setAdvertising({}, options);
if(isNewGpsData && gps.lat && gps.lon) {
data.push(encodeGpsServiceData());
isNewGpsData = false;
}
if(isNewHrmData) {
data.push({ 0x2a37: [ 0, hrm.bpm ] });
isNewHrmData = false;
}
if(isNewMagData) {
data.push(encodeMagServiceData());
isNewMagData = false;
}
NRF.setAdvertising(data, { showName: false, interval: 200 });
}
// 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 ];
return [
0x02, 0x01, 0x06, // Flags
0x05, 0x16, 0x6e, 0x2a, t[0], t[1], // Temperature
0x07, 0x16, 0x6d, 0x2a, p[0], p[1], p[2], p[3], // Pressure
0x06, 0x16, 0x6c, 0x2a, e[0], e[1], e[2] // Elevation
];
}
// 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 ];
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
];
}
// 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 ];
return [
0x02, 0x01, 0x06, // Flags
0x09, 0x16, 0xa1, 0x2a, x[0], x[1], y[0], y[1], z[0], z[1] // Mag 3D
];
}
// Update acceleration
Bangle.on('accel', function(newAcc) {
acc = newAcc;
isNewAccData = true;
if(isAccMenu) {
accMenu.x.value = acc.x.toFixed(2);
@ -119,6 +227,7 @@ Bangle.on('accel', function(newAcc) {
// Update barometer
Bangle.on('pressure', function(newBar) {
bar = newBar;
isNewBarData = true;
if(isBarMenu) {
barMenu.Altitude.value = bar.altitude.toFixed(1) + 'm';
@ -131,6 +240,7 @@ Bangle.on('pressure', function(newBar) {
// Update GPS
Bangle.on('GPS', function(newGps) {
gps = newGps;
isNewGpsData = true;
if(isGpsMenu) {
gpsMenu.Lat.value = gps.lat.toFixed(4);
@ -145,6 +255,7 @@ Bangle.on('GPS', function(newGps) {
// Update heart rate monitor
Bangle.on('HRM', function(newHrm) {
hrm = newHrm;
isNewHrmData = true;
if(isHrmMenu) {
hrmMenu.BPM.value = hrm.bpm;
@ -156,6 +267,7 @@ Bangle.on('HRM', function(newHrm) {
// Update magnetometer
Bangle.on('mag', function(newMag) {
mag = newMag;
isNewMagData = true;
if(isMagMenu) {
magMenu.x.value = mag.x;
@ -169,9 +281,9 @@ Bangle.on('mag', function(newMag) {
// On start: enable sensors and display main menu
g.clear();
transmitAppName();
Bangle.setBarometerPower(isBarEnabled, APP_ID);
Bangle.setGPSPower(isGpsEnabled, APP_ID);
Bangle.setHRMPower(isHrmEnabled, APP_ID);
Bangle.setCompassPower(isMagEnabled, APP_ID);
E.showMenu(mainMenu);
E.showMenu(mainMenu);
setInterval(transmitUpdatedSensorData, 1000);

View File

@ -39,3 +39,4 @@
0.34: Remove Quiet Mode LCD settings: now handled by Quiet Mode Schedule app
0.35: Change App/Widget settings to 'App Settings' so it fits on Bangle screen
0.36: Added 'Utils' menu with helpful utilities for restoring Bangle.js
0.37: Going into passkey menu now saves settings with passkey

View File

@ -58,7 +58,7 @@ function resetSettings() {
settings = storage.readJSON('setting.json', 1);
if (!settings) resetSettings();
const boolFormat = v => v ? "On" : "Off";
const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
function showMainMenu() {
var beepMenuItem;
@ -77,7 +77,7 @@ function showMainMenu() {
};
} else { // Bangle.js 1
var beepV = [false, true, "vib"];
var beepN = ["Off", "Piezo", "Vibrate"];
var beepN = [/*LANG*/"Off", /*LANG*/"Piezo", /*LANG*/"Vibrate"];
beepMenuItem = {
value: Math.max(0 | beepV.indexOf(settings.beep),0),
min: 0, max: beepV.length-1,
@ -95,10 +95,10 @@ function showMainMenu() {
const mainmenu = {
'': { 'title': 'Settings' },
'< Back': ()=>load(),
'App Settings': ()=>showAppSettingsMenu(),
'BLE': ()=>showBLEMenu(),
'Beep': beepMenuItem,
'Vibration': {
/*LANG*/'App Settings': ()=>showAppSettingsMenu(),
/*LANG*/'BLE': ()=>showBLEMenu(),
/*LANG*/'Beep': beepMenuItem,
/*LANG*/'Vibration': {
value: settings.vibrate,
format: boolFormat,
onchange: () => {
@ -110,7 +110,7 @@ function showMainMenu() {
}
}
},
"Quiet Mode": {
/*LANG*/"Quiet Mode": {
value: settings.quiet|0,
format: v => ["Off", "Alarms", "Silent"][v%3],
onchange: v => {
@ -120,13 +120,13 @@ function showMainMenu() {
if ("qmsched" in WIDGETS) WIDGETS["qmsched"].draw();
},
},
'Locale': ()=>showLocaleMenu(),
'Select Clock': ()=>showClockMenu(),
'Set Time': ()=>showSetTimeMenu(),
'LCD': ()=>showLCDMenu(),
'Theme': ()=>showThemeMenu(),
'Utils': ()=>showUtilMenu(),
'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() },
/*LANG*/'Locale': ()=>showLocaleMenu(),
/*LANG*/'Select Clock': ()=>showClockMenu(),
/*LANG*/'Set Time': ()=>showSetTimeMenu(),
/*LANG*/'LCD': ()=>showLCDMenu(),
/*LANG*/'Theme': ()=>showThemeMenu(),
/*LANG*/'Utils': ()=>showUtilMenu(),
/*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() },
};
return E.showMenu(mainmenu);
@ -276,8 +276,10 @@ function showPasskeyMenu() {
showBLEMenu();
}
};
if (!settings.passkey || settings.passkey.length!=6)
if (!settings.passkey || settings.passkey.length!=6) {
settings.passkey = "123456";
updateSettings();
}
for (var i=0;i<6;i++) (function(i){
menu[`Digit ${i+1}`] = {
value : 0|settings.passkey[i],

2
apps/showimg/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.1: Initial release
0.2: Fixed launcher image

3
apps/showimg/README.md Normal file
View File

@ -0,0 +1,3 @@
Displays an image. I use this app to show my vaccination certificate.
The image is read from the file "showimage.user.img".
Returns to watch face after 60s/button push.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBIf4Aah//BRQAMDowUNC5AARC4YKKL5gTC+B3TCpAyIC5oNBEA4XNJwS4GC55pHC8TEHC57QHC4wSEC5YpEC6YwEC5oEEC5x3DC6ZHbC7PwcYxfNAYYXPJA4XQDAwKEBYQXJIoReHC5gMFAAojBC5QUIC5Y5JMgYXIUQYJFPggXMAwICCBAYXMCAQJDDwQUCC5QOCUwQdEC5QqFDghNFC5wrEC5gQDPgoTCDYYXFMAgXaCQoXJEwZ4FLQbhFC4imDAAglFC5QAGBgYXKIoYWIC5YYFG4ZkDC4YjCYYwAJC4gASC6THFH5pqGAAY"))

16
apps/showimg/app.js Normal file
View File

@ -0,0 +1,16 @@
g.reset();
g.clear();
g.drawImage(require("Storage").read("showimg.user.img"),0,0);
drawTimeout = setTimeout(function() {
load();
}, 60000);
setWatch(function() {
load();
}, BTN, { repeat:false, edge:'falling' });
var savedOptions=Bangle.getOptions();
Bangle.setLCDBrightness(1);
var newOptions={
lockTimeout:60000,
backlightTimeout:60000
};
Bangle.setOptions(newOptions);

BIN
apps/showimg/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

2
apps/snek/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: First release
0.02: Fixed snek.png and snek.icon.js to 64x64 to display in launcher, added screenshots, updated apps.json

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1 +0,0 @@
require("heatshrink").decompress(atob("pFIwkB+MRAEH/EUIA7i93u9xEUIABEf4AC+93/4CBETpDBv//gEHEf6MB9wkB/8HSDl3vwjCAAfnErIjiEQYdBAAXuAoSNXEYIdDAAoj4j/3DpN3v6NWAA3/fDYjgRgIjLu9xESj2BAAN/SQwLBEe4XDdwghDBQQjSCgN+C5D9FEebTDEZJEWEQSDVEdZpDZYPnETYhDAAhpeEbzREI0rTbEdXuETb4Bvz1BAYIj/EYxrg9yQDv/3JoS9WEcoaGAAQtBOwYABEaSMBWoYeFJKgjjiIUD9ySEEjwAJFogj0SQgAFBQ4jRABcfXoQj/TowjCOgIkeEf7lHvz+CEb93Ef4jHR8Rr/fY4jCuIifAAQj/EIojbeohGeEcQhHfLRFDeoIicEcQbDv3uEYiNXIgnu87SgEcf/DKnxBJEf/4ACESf/EcYA=="))

1
apps/snek/snek.icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("oFA4X/AAOJksvr2rmokYgWqB7sq/2AB5krgYPMgW8ioPc1X9i/oLplVqv+1BdK1OV//q9QPMv4PL1eqy/q1SRK3tVu+AgWCFxP96t+Vhn9qoPLgWr/+//wFBSBEq3/qlW+JwJ/I3eXDQIOBB5OrB5sC3xMD1WAH4+r6xsOtSpKLoYPN1fV1bpKTYf+RJAeDytXFxoPOdQYPNPpkCy1VtQPc6wvO62Vu+CbhfVN4P//+q//uMgwPH9QPH3tqqtpqoABv4wHfoOpBoP/6tVUg7uBFwIvB3xlIB4v+OpJsC1WA1fVQpiGCB52+uzlMB58A31XB5sqy4PNlYPfH50rywPN3++BxgPPgW9V5kCZ4L/HBwmq/tX1APM/4PMBwNVvxuKgW/tP/HxUq1X+1eqFxQPRAAKsLB4KqNAFY="))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,2 +1,3 @@
0.01: Initial import.
0.07: Add swipe to change screens.
1.06: Misc memory and screen optimisations.

View File

@ -1,11 +1,9 @@
/*
Speed and Altitude [speedalt2]
Mike Bennett mike[at]kereru.com
0.01 : Initial
0.06 : Add Posn screen
0.07 : Add swipe to change screens same as BTN3
1.10 : add inverted colours
*/
var v = '1.05';
var v = '1.10';
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
var KalmanFilter = (function () {
@ -173,6 +171,15 @@ var KalmanFilter = (function () {
var buf = Graphics.createArrayBuffer(240,160,2,{msb:true});
let LED = // LED as minimal and only definition (as instance / singleton)
{ isOn: false // status on / off, not needed if you don't need to ask for it
, set: function(v) { // turn on w/ no arg or truey, else off
g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(40,10,10); }
, reset: function() { this.set(false); } // turn off
, write: function(v) { this.set(v); } // turn on w/ no arg or truey, else off
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
}, LED1 = LED; // LED1 as 'synonym' for LED
// Load fonts
//require("Font7x11Numeric7Seg").add(Graphics);
@ -183,17 +190,16 @@ var canDraw = 1;
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
var tmrLP; // Timer for delay in switching to low power after screen turns off
var max = {};
max.spd = 0;
max.alt = 0;
max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
var maxSpd = 0;
var maxAlt = 0;
var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
var wp = {}; // Waypoint to use for distance from cur position.
function nxtWp(inc){
cfg.wp+=inc;
function nxtWp(){
cfg.wp++;
loadWp();
}
@ -227,7 +233,8 @@ function drawScrn(dat) {
if (!canDraw) return;
buf.clear();
buf.setBgColor(0);
var n;
n = dat.val.toString();
@ -252,29 +259,21 @@ function drawScrn(dat) {
buf.setFontVector(35);
buf.drawString(dat.unit,5,164);
if ( dat.max ) drawMax(); // MAX display indicator
if ( dat.wp ) drawWP(); // Waypoint name
//Sats
if ( dat.sat ) {
if ( dat.age > 10 ) {
if ( dat.age > 90 ) dat.age = '>90';
drawSats('Age:'+dat.age);
}
else drawSats('Sats:'+dat.sats);
}
drawMax(dat.max); // MAX display indicator
drawWP(dat.wp); // Waypoint name
drawSats(dat.sats);
g.reset();
g.drawImage(img,0,40);
if ( pwrSav ) LED1.reset();
else LED1.set();
LED1.write(!pwrSav);
}
function drawPosn(dat) {
if (!canDraw) return;
buf.clear();
buf.setBgColor(0);
var x, y;
x=210;
@ -293,20 +292,12 @@ function drawPosn(dat) {
buf.drawString(dat.ew,x,y+70);
//Sats
if ( dat.sat ) {
if ( dat.age > 10 ) {
if ( dat.age > 90 ) dat.age = '>90';
drawSats('Age:'+dat.age);
}
else drawSats('Sats:'+dat.sats);
}
drawSats(dat.sats);
g.reset();
g.drawImage(img,0,40);
if ( pwrSav ) LED1.reset();
else LED1.set();
LED1.write(!pwrSav);
}
@ -314,6 +305,8 @@ function drawClock() {
if (!canDraw) return;
buf.clear();
buf.setBgColor(0);
var x, y;
x=185;
y=0;
@ -329,19 +322,14 @@ function drawClock() {
g.reset();
g.drawImage(img,0,40);
if ( pwrSav ) LED1.reset();
else LED1.set();
LED1.write(!pwrSav);
}
function drawWP() {
var nm = wp.name;
if ( nm == undefined || nm == 'NONE' || cfg.modeA ==1 ) nm = '';
buf.setColor(2);
function drawWP(wp) {
buf.setColor(3);
buf.setFontAlign(0,1); //left, bottom
buf.setFontVector(48);
buf.drawString(nm.substring(0,8),120,140);
buf.drawString(wp,120,140);
}
function drawSats(sats) {
@ -351,16 +339,15 @@ function drawSats(sats) {
buf.drawString(sats,240,160);
}
function drawMax() {
function drawMax(max) {
buf.setFontVector(30);
buf.setColor(2);
buf.setFontAlign(0,1); //centre, bottom
buf.drawString('MAX',120,164);
buf.drawString(max,120,164);
}
function onGPS(fix) {
if ( emulator ) {
if ( emulator ) {
fix.fix = 1;
fix.speed = 10 + (Math.random()*5);
fix.alt = 354 + (Math.random()*50);
@ -382,10 +369,15 @@ function onGPS(fix) {
var ns = '';
var ew = '';
var lon = '---.--';
var sats = '---';
// Waypoint name
var wpName = wp.name;
if ( wpName == undefined || wpName == 'NONE' ) wpName = '';
wpName = wpName.substring(0,8);
if (fix.fix) lf = fix;
if (lf.fix) {
// Smooth data
@ -393,10 +385,9 @@ function onGPS(fix) {
if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
lf.smoothed = 1;
if ( max.n <= 15 ) max.n++;
if ( maxN <= 15 ) maxN++;
}
// Speed
if ( cfg.spd == 0 ) {
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
@ -407,18 +398,19 @@ function onGPS(fix) {
if ( sp < 10 ) sp = sp.toFixed(1);
else sp = Math.round(sp);
if (isNaN(sp)) sp = '---';
if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = sp;
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;
// Altitude
al = lf.alt;
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = al;
if (parseFloat(al) > parseFloat(maxAlt) && maxN > 15 ) maxAlt = al;
if (isNaN(al)) al = '---';
// Distance to waypoint
di = distance(lf,wp);
if (isNaN(di)) di = 0;
if (isNaN(di)) di = '--------';
// Age of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
@ -431,6 +423,13 @@ function onGPS(fix) {
ew = 'E';
if ( lf.lon < 0 ) ew = 'W';
lon = Math.abs(lf.lon.toFixed(2));
// Sats
if ( age > 10 ) {
sats = 'Age:'+Math.round(age);
if ( age > 90 ) sats = 'Age:>90';
}
else sats = 'Sats:'+lf.satellites;
}
@ -438,23 +437,21 @@ function onGPS(fix) {
// Speed
if ( showMax )
drawScrn({
val:max.spd,
val:maxSpd,
unit:cfg.spd_unit,
sats:lf.satellites,
sats:sats,
age:age,
max:true,
wp:false,
sat:true
max:'MAX',
wp:''
}); // Speed maximums
else
drawScrn({
val:sp,
unit:cfg.spd_unit,
sats:lf.satellites,
sats:sats,
age:age,
max:false,
wp:false,
sat:true
max:'SPD',
wp:''
});
}
@ -462,23 +459,21 @@ function onGPS(fix) {
// Alt
if ( showMax )
drawScrn({
val:max.alt,
val:maxAlt,
unit:cfg.alt_unit,
sats:lf.satellites,
sats:sats,
age:age,
max:true,
wp:false,
sat:true
max:'MAX',
wp:''
}); // Alt maximums
else
drawScrn({
val:al,
unit:cfg.alt_unit,
sats:lf.satellites,
sats:sats,
age:age,
max:false,
wp:false,
sat:true
max:'ALT',
wp:''
});
}
@ -487,24 +482,22 @@ function onGPS(fix) {
drawScrn({
val:di,
unit:cfg.dist_unit,
sats:lf.satellites,
sats:sats,
age:age,
max:false,
wp:true,
sat:true
max:'DST',
wp:wpName
});
}
if ( cfg.modeA == 3 ) {
// Position
drawPosn({
sats:lf.satellites,
drawPosn({
sats:sats,
age:age,
lat:lat,
lon:lon,
ns:ns,
ew:ew,
sat:true
ew:ew
});
}
@ -534,9 +527,9 @@ function nextFunc(dur) {
if ( cfg.modeA == 0 || cfg.modeA == 1 ) {
// Spd+Alt mode - Switch between fix and MAX
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
else { max.spd = 0; max.alt = 0; } // Long press resets max values.
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values.
}
else if ( cfg.modeA == 2) nxtWp(1); // Dist mode - Select next waypoint
else if ( cfg.modeA == 2) nxtWp(); // Dist mode - Select next waypoint
onGPS(lf);
}
@ -545,7 +538,7 @@ function updateClock() {
if (!canDraw) return;
if ( cfg.modeA != 4 ) return;
drawClock();
if ( emulator ) {max.spd++;max.alt++;}
if ( emulator ) {maxSpd++;maxAlt++;}
}
function startDraw(){
@ -585,7 +578,6 @@ function setButtons(){
setWatch(function(e){
pwrSav=!pwrSav;
if ( pwrSav ) {
LED1.reset();
var s = require('Storage').readJSON('setting.json',1)||{};
var t = s.timeout||10;
Bangle.setLCDTimeout(t);
@ -593,8 +585,8 @@ function setButtons(){
else {
Bangle.setLCDTimeout(0);
// Bangle.setLCDPower(1);
LED1.set();
}
LED1.write(!pwrSav);
}, BTN2, {repeat:true,edge:"falling"});
// BTN3 - next screen
@ -690,7 +682,8 @@ var img = {
};
if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]);
if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xFF800,0xFAE0,0xF813]);
if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xF800,0xFAE0,0xF813]);
if ( cfg.colour == 3 ) img.palette = new Uint16Array([0xFFFF,0x007F,0x0054,0x0054]);
var SCREENACCESS = {
withApp:true,

View File

@ -65,7 +65,8 @@
'< Back': function() { E.showMenu(appMenu); },
'Default' : function() { setColour(0); },
'Hi Contrast' : function() { setColour(1); },
'Night' : function() { setColour(2); }
'Night' : function() { setColour(2); },
'Inverted' : function() { setColour(3); }
};
const kalMenu = {

View File

@ -31,3 +31,6 @@ Which one is which ?
![](A.jpg)
![](B.jpg)
Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)

View File

@ -1,2 +1,3 @@
0.02: New App!
0.03: Improved messages and added Celsius sign
0.04: Make temperature value readable on smaller screens

View File

@ -4,7 +4,7 @@ function onTemperature(p) {
var x = g.getWidth()/2;
var y = g.getHeight()/2 + 10;
g.drawString("Temperature:", x, y - 45);
g.setFontVector(70).setFontAlign(0,0);
g.setFontVector(g.getWidth() > 200 ? 70 : 40).setFontAlign(0,0);
g.drawString(p.temperature.toFixed(1) + " °C", x, y);
}

View File

@ -19,3 +19,4 @@
Stop goal drawing outside widget area
Fix issue with widget overwrite in large font mode
Memory usage enhancements
0.20: Fix issue where step count would randomly reset

View File

@ -53,7 +53,7 @@
E.on('kill', () => {
if (!settings) { loadSettings() }
let d = {
lastUpdate : lastUpdate.toISOString(),
lastUpdate : lastUpdate.valueOf(),
stepsToday : stp_today,
settings : settings,
};

69
bin/language_scan.js Executable file
View File

@ -0,0 +1,69 @@
#!/usr/bin/nodejs
/* Scans for strings that may be in English in each app, and
outputs a list of strings that have been found.
Early work towards internationalisation.
See https://github.com/espruino/BangleApps/issues/136
*/
var BASEDIR = __dirname+"/../";
Espruino = require(BASEDIR+"core/lib/espruinotools.js");
var fs = require("fs");
var APPSDIR = BASEDIR+"apps/";
function ERROR(s) {
console.error("ERROR: "+s);
process.exit(1);
}
function WARN(s) {
console.log("Warning: "+s);
}
var appsFile, apps;
try {
appsFile = fs.readFileSync(BASEDIR+"apps.json").toString();
} catch (e) {
ERROR("apps.json not found");
}
try{
apps = JSON.parse(appsFile);
} catch (e) {
ERROR("apps.json not valid JSON");
}
// Given a string value, work out if it's obviously not a text string
function isNotString(s) {
if (s.length<2) return true; // too short
if (s.length>40) return true; // too long
if (s[0]=="#") return true; // a color
if (s.endsWith(".json") || s.endsWith(".img")) return true; // a filename
if (s.endsWith("=")) return true; // probably base64
if (s.startsWith("BTN")) return true; // button name
return false;
}
var textStrings = [];
console.log("Scanning...");
apps.forEach((app,appIdx) => {
var appDir = APPSDIR+app.id+"/";
app.storage.forEach((file) => {
if (!file.url || !file.name.endsWith(".js")) return;
var fileContents = fs.readFileSync(appDir+file.url).toString();
var lex = Espruino.Core.Utils.getLexer(fileContents);
var tok = lex.next();
while (tok!==undefined) {
if (tok.type=="STRING") {
if (!isNotString(tok.value)) {
//console.log(tok.str);
if (!textStrings.includes(tok.value))
textStrings.push(tok.value);
}
}
tok = lex.next();
}
});
});
console.log("Done");
textStrings.sort();
console.log(textStrings.join("\n"));

23
lang/de_DE.json Normal file
View File

@ -0,0 +1,23 @@
{
"//":"German language translations",
"GLOBAL": {
"//":"Translations that apply for all apps",
"Alarm" : "Wecker",
"Hours" : "Stunden",
"Minutes" : "Minuten",
"Enabled" : "Aktiviert",
"Settings" : "Einstellungen",
"Save" : "Speichern",
"Back" : "Zurück",
"Repeat" : "Wiederholen",
"Delete" : "Löschen",
"Sleep" : "Schlummern",
"Alarms" : "Wecker",
"New Alarm" : "Neuer Wecker",
"ALARM!" : "ALARM!"
},
"alarm": {
"//":"App-specific overrides",
"rpt" : "Wdh."
}
}

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