Merge branch 'espruino:master' into master

master
Bundyo (Kamen Bundev) 2022-02-14 13:24:34 +02:00 committed by GitHub
commit 33a360c6d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 1922 additions and 799 deletions

View File

@ -43,3 +43,7 @@ charge.
This app is based in the work done by [jeffmer](https://github.com/jeffmer/JeffsBangleAppsDev)
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.01: Display pressure as number and hand
0.02: Use theme color
0.03: workaround for some firmwares that return 'undefined' for first call to barometer

View File

@ -110,9 +110,13 @@ drawScaleLabels();
drawIcons();
try {
Bangle.getPressure().then(data => {
drawHand(Math.round(data.pressure));
});
function baroHandler(data) {
if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429
setTimeout(() => Bangle.getPressure().then(baroHandler), 500);
else
drawHand(Math.round(data.pressure));
}
Bangle.getPressure().then(baroHandler);
} catch(e) {
print(e.message);
print("barometer not supporter, show a demo value");

View File

@ -1,7 +1,7 @@
{ "id": "barometer",
"name": "Barometer",
"shortName":"Barometer",
"version":"0.02",
"version":"0.03",
"description": "A simple barometer that displays the current air pressure",
"icon": "barometer.png",
"tags": "tool,outdoors",

View File

@ -45,3 +45,4 @@
0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035)
0.40: Bootloader now rebuilds for new firmware versions
0.41: Add Keyboard and Mouse Bluetooth HID option
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.boot.js

View File

@ -195,7 +195,19 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
require('Storage').list(/\.boot\.js/).forEach(bootFile=>{
var getPriority = /.*\.(\d+)\.boot\.js$/;
require('Storage').list(/\.boot\.js/).sort((a,b)=>{
var aPriority = a.match(getPriority);
var bPriority = b.match(getPriority);
if (aPriority && bPriority){
return parseInt(aPriority[1]) - parseInt(bPriority[1]);
} else if (aPriority && !bPriority){
return -1;
} else if (!aPriority && bPriority){
return 1;
}
return a > b;
}).forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error!

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.41",
"version": "0.42",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",

6
apps/bowserWF/README.md Normal file
View File

@ -0,0 +1,6 @@
# Bowser Watchface
Show your evil character.
With style!
Bowser jumps once every minute to advance the clock. hehe

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("Ly+EARERERERERERERERER//8RERERERERERERERERERERERERERERzP//d3EREREREREREREREREREREREREcz//3dxERERERERERERERERERERERERHMz/93dxERERERERERERERERERERERERd8zHd3f/8REREREREREREREREREREREXfMx3d3//ERERERERERERERERERERERF3d3d3d//3cRHBEREREREREREREREREXd3d3d3f/93fMDMERERERERERERERERF3d3d3d3//d3zAzBERERERERERERERERd3d3fMd3f//8zMwREREREREREREREREXd3d8zMd3f/zMzMERERERERERERERERF3d3fMzHd3/8zMzBERERERERER93d3//d3d3x3/Mx3fMzP8REREREREXzP/3d//3d3d8d3fMzMz/8RERERERERF8z/93f/93d3fHd3zMzM//ERERER//93zM//93//d3d3z/d/9////xEREREcz/d3zM/3d///d3d8zHERH/ERERERERHM/3d8zP93f//3d3fMxxER/xERERER/3zMd3d3d3d3f///d3zM8REREREREREf93d3d3d3d3d3d///d3zMERERERERERH/d3d3d3d3d3d3f//3d8zBERERERERERd3d3f///d3d3d3//93fMzP8RERERERH//3d3zP/3d//////3d3d8wRERERERER//93d8z/93f/////93d3fMEREREREREc//d3fMx3f//MzMd3d3d3dxERERERERHMx3d3d3d//8zMAPd3zMzHERERERERH/d3d3d3d3f3fMzwDMzAD8zBERERERER/3d3d3d3d393zM8AzMwA/MwRERERERH913d///d3d/d3AAzMzMAAzMERERERERREd3fM/3d//3d//MzMzMwP/BEREREREURHd3zP93f/93f/zMzMzMD/wRERERERFP/3d8zHd3//d3d3zMzMzAAMERERERER//93d3d3d//3d3d8zMzMwP8BEREREREf//d3d3d3f/93d3fMzMzMD/ARERERERFMzHf/93d3/3d3d3EczM/wABERERERERR3x3fMd3f/93d3dxEREREREREREREREUd8d3zHd3//d3d3cRERERERERERERERFHd3d3d///d3d3dxERERERERERERERERR3d3f///93d3dxEREREREREREREREREUd3d3////d3d3cRERERERERERERERERH//////3d3d3d8ERERERERERERERERER/////MzMx3fMz/8REREREREREREREREf////zMzMd3zM//EREREREREREREREREf/8zMzMzMzMzP//ERERERERERERERERERzMzM/8zP/xEREREREREREREREREREREczMzP/Mz/8REREREREREREREREREREczMzMz//8///xERERERERERERERERERA="))

102
apps/bowserWF/app.js Normal file
View File

@ -0,0 +1,102 @@
var sprite = {
width : 47, height : 47, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA"))
};
const boxes = {
width : 122, height : 56, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA="))
};
const background = {
width : 176, height : 176, bpp : 3,
transparent : 5,
buffer : require("heatshrink").decompress(atob("kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA="))
};
numbersDims = {
width: 20,
height: 44
};
const numbers = [
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA=")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA=")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA=")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A=")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA=")),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA=")),
];
digitPositions = [ // relative to the box
{x:13, y:6}, {x:32, y:6},
{x:74, y:6}, {x:93, y:6},
];
var drawTimeout;
const animation_duration = 1; // seconds
const animation_steps = 20;
const jump_height = 45; // top coordinate of the jump
const seconds_per_minute = 60;
function draw() {
const now = new Date();
g.drawImage(background, 0, 0);
var boxTL_x = 27; var boxTL_y = 29;
var sprite_TL_x = 72; var sprite_TL_y = 161 - sprite.height;
const seconds = now.getSeconds()%seconds_per_minute + now.getMilliseconds()/1000;
const hours = now.getHours();
const minutes = now.getMinutes();
var time_advance = seconds / animation_duration;
if (time_advance < 0.5) {
sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2;
} else if (time_advance < 1) {
sprite_TL_y = jump_height + (sprite_TL_y-jump_height) * (time_advance-0.5) * 2;
}
const box_penetration = boxTL_y + boxes.height - sprite_TL_y;
if (box_penetration > 0) {
boxTL_y -= box_penetration;
}
g.drawImage(boxes, boxTL_x, boxTL_y);
g.drawImage(numbers[(hours / 10) >> 0], boxTL_x+digitPositions[0].x, boxTL_y+digitPositions[0].y);
g.drawImage(numbers[(hours % 10) >> 0], boxTL_x+digitPositions[1].x, boxTL_y+digitPositions[1].y);
g.drawImage(numbers[(minutes / 10) >> 0], boxTL_x+digitPositions[2].x, boxTL_y+digitPositions[2].y);
g.drawImage(numbers[(minutes % 10) >> 0], boxTL_x+digitPositions[3].x, boxTL_y+digitPositions[3].y);
g.drawImage(sprite, sprite_TL_x, sprite_TL_y);
Bangle.drawWidgets();
const timeout = time_advance <= 1?
animation_duration / animation_steps
: (seconds_per_minute - seconds);
setTimeout( _=>{
drawTimeout = undefined;
draw();
}, timeout * 1000);
}
// Clear the screen once, at startup
g.setTheme({bg:"#00f",fg:"#fff",dark:true}).clear();
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) {
clearTimeout(drawTimeout);
}
drawTimeout = undefined;
}
});
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
draw();

BIN
apps/bowserWF/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,14 @@
{ "id": "bowserWF",
"name": "Bowser Watchface",
"shortName":"Bowser Watchface",
"version":"0.01",
"description": "Let bowser show you the time",
"icon": "app.png",
"tags": "",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bowserWF.app.js","url":"app.js"},
{"name":"bowserWF.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -17,3 +17,6 @@
0.06: Fix bug if no request waiting time is set
Fix bug if no connection data was cached
Fix error during disconnect
0.07: Recorder icon only blue if values actually arive
Adds some preset modes and a custom one
Restructure the settings menu

View File

@ -29,6 +29,7 @@ Heart Rate Service (`180D`) and characteristic (`2A37`).
So far it has been tested on:
* CooSpo Bluetooth Heart Rate Monitor
* Wahoo TICKR X 2
## Internals
@ -36,7 +37,6 @@ This replaces `Bangle.setHRMPower` with its own implementation.
## TODO
* Maybe a `bthrm.settings.js` and app (that calls it) to enable it to be turned on and off
* A widget to show connection state?
* Specify a specific device by address?

View File

@ -548,9 +548,7 @@
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
var promise = gatt.disconnect();
promise.then(()=>log("Disconnected on kill"));
promise.catch((e)=>log("Error during disconnnect on kill", e));
var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}

View File

@ -1,4 +1,5 @@
{
"mode": 1,
"enabled": true,
"replace": true,
"debuglog": false,
@ -6,6 +7,12 @@
"allowFallback": true,
"warnDisconnect": false,
"fallbackTimeout": 10,
"custom_replace": false,
"custom_debuglog": false,
"custom_startWithHrm": false,
"custom_allowFallback": false,
"custom_warnDisconnect": false,
"custom_fallbackTimeout": 10,
"gracePeriodNotification": 0,
"gracePeriodConnect": 0,
"gracePeriodService": 0,

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.06",
"version": "0.07",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
@ -12,7 +12,7 @@
"storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.js"},
{"name":"bthrm.0.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"},
{"name":"bthrm.default.json","url":"default.json"}

View File

@ -32,7 +32,7 @@
Bangle.removeListener('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor((Bangle.isBTHRMConnected && Bangle.isBTHRMConnected())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
draw : (x,y) => g.setColor((bpm != "")?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
}
})

View File

@ -20,182 +20,147 @@
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
'Mode': {
value: 0 | settings.mode,
min: 0,
max: 3,
format: v => ["Off", "Default", "Both", "Custom"][v],
onchange: v => {
writeSettings("enabled",v);
settings.mode = v;
switch (v){
case 0:
writeSettings("enabled",false);
break;
case 1:
writeSettings("enabled",true);
writeSettings("replace",true);
writeSettings("debuglog",false);
writeSettings("startWithHrm",true);
writeSettings("allowFallback",true);
writeSettings("fallbackTimeout",10);
break;
case 2:
writeSettings("enabled",true);
writeSettings("replace",false);
writeSettings("debuglog",false);
writeSettings("startWithHrm",false);
writeSettings("allowFallback",false);
break;
case 3:
writeSettings("enabled",true);
writeSettings("replace",settings.custom_replace);
writeSettings("debuglog",settings.custom_debuglog);
writeSettings("startWithHrm",settings.custom_startWithHrm);
writeSettings("allowFallback",settings.custom_allowFallback);
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
break;
}
writeSettings("mode",v);
}
},
'Replace HRM': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
onchange: v => {
writeSettings("replace",v);
}
},
'Start with HRM': {
value: !!settings.startWithHrm,
format: v => settings.startWithHrm ? "On" : "Off",
onchange: v => {(function(back) {
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
'Custom Mode': function() { E.showMenu(submenu_custom); },
'Debug': function() { E.showMenu(submenu_debug); }
};
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
var FILE="bthrm.json";
var settings;
readSettings();
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
var submenu_debug = {
'' : { title: "Debug"},
'< Back': function() { E.showMenu(mainmenu); },
'Alert on disconnect': {
value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
onchange: v => {
writeSettings("enabled",v);
writeSettings("warnDisconnect",v);
}
},
'Replace HRM': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
'Debug log': {
value: !!settings.debuglog,
format: v => settings.debuglog ? "On" : "Off",
onchange: v => {
writeSettings("replace",v);
writeSettings("debuglog",v);
}
},
'Grace periods': function() { E.showMenu(submenu_grace); }
};
var submenu_custom = {
'' : { title: "Custom mode"},
'< Back': function() { E.showMenu(mainmenu); },
'Replace HRM': {
value: !!settings.custom_replace,
format: v => settings.custom_replace ? "On" : "Off",
onchange: v => {
writeSettings("custom_replace",v);
}
},
'Start w. HRM': {
value: !!settings.startWithHrm,
format: v => settings.startWithHrm ? "On" : "Off",
value: !!settings.custom_startWithHrm,
format: v => settings.custom_startWithHrm ? "On" : "Off",
onchange: v => {
writeSettings("startWithHrm",v);
writeSettings("custom_startWithHrm",v);
}
},
'HRM Fallback': {
value: !!settings.allowFallback,
format: v => settings.allowFallback ? "On" : "Off",
value: !!settings.custom_allowFallback,
format: v => settings.custom_allowFallback ? "On" : "Off",
onchange: v => {
writeSettings("allowFallback",v);
writeSettings("custom_allowFallback",v);
}
},
'Fallback Timeout': {
value: settings.fallbackTimeout,
value: settings.custom_fallbackTimeout,
min: 5,
max: 60,
step: 5,
format: v=>v+"s",
onchange: v => {
writeSettings("fallbackTimout",v*1000);
writeSettings("custom_fallbackTimout",v*1000);
}
},
'Conn. Alert': {
value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
};
var submenu_grace = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(submenu_debug); },
'Request': {
value: settings.gracePeriodRequest,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("warnDisconnect",v);
writeSettings("gracePeriodRequest",v);
}
},
'Debug log': {
value: !!settings.debuglog,
format: v => settings.debuglog ? "On" : "Off",
'Connect': {
value: settings.gracePeriodConnect,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("debuglog",v);
writeSettings("gracePeriodConnect",v);
}
},
'Grace periods >': function() { E.showMenu(submenu); }
};
var submenu = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(mainmenu); },
'Request': {
value: settings.gracePeriodRequest,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodRequest",v);
}
},
'Connect': {
value: settings.gracePeriodConnect,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodConnect",v);
}
},
'Notification': {
value: settings.gracePeriodNotification,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodNotification",v);
}
},
'Service': {
value: settings.gracePeriodService,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodService",v);
}
}
};
E.showMenu(mainmenu);
})
writeSettings("startWithHrm",v);
}
},
'Fallback to HRM': {
value: !!settings.allowFallback,
format: v => settings.allowFallback ? "On" : "Off",
onchange: v => {
writeSettings("allowFallback",v);
}
},
'Fallback Timeout': {
value: settings.fallbackTimeout,
min: 5,
max: 60,
step: 5,
format: v=>v+"s",
onchange: v => {
writeSettings("fallbackTimout",v*1000);
}
},
'Conn. Alert': {
value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
onchange: v => {
writeSettings("warnDisconnect",v);
}
},
'Debug log': {
value: !!settings.debuglog,
format: v => settings.debuglog ? "On" : "Off",
onchange: v => {
writeSettings("debuglog",v);
}
},
'Grace periods': function() { E.showMenu(submenu); }
'Notification': {
value: settings.gracePeriodNotification,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodNotification",v);
}
},
'Service': {
value: settings.gracePeriodService,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodService",v);
}
}
};
var submenu = {

View File

@ -3,3 +3,4 @@
0.21: Fixed settings menu, four more fonts
0.22: Changed timing code, original "Nunito" Font is back!
0.23: Customizer! Unused fonts no longer take up precious memory.
0.24: Added previews to the customizer.

View File

@ -1,70 +1,74 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<style>
input[type=checkbox] {
opacity:0;
}
input[type=checkbox] + label {
opacity:0.2;
}
input[type=checkbox]:checked + label {
opacity:1;
}
</style>
</head>
<body>
<h3>&nbsp;&nbsp;&nbsp;Select Fonts to upload:</h3>
<form>
<input type="checkbox" id="BarlowCond">
<label for="BarlowCond">Barlow Condensed</label><br>
<label for="BarlowCond"><img src="fonts/BarlowCond-p1.png"> <img src="fonts/BarlowCond-p2.png"></label><br>
<input type="checkbox" id="BebasNeue">
<label for="BebasNeue">Bebas Neue</label><br>
<label for="BebasNeue"><img src="fonts/BebasNeue-p1.png"> <img src="fonts/BebasNeue-p2.png"></label><br>
<input type="checkbox" id="Dekko">
<label for="Dekko">Dekko</label><br>
<label for="Dekko"><img src="fonts/Dekko-p1.png"> <img src="fonts/Dekko-p2.png"></label><br>
<input type="checkbox" id="DinAlternate">
<label for="DinAlternate">Din Alternate</label><br>
<input type="checkbox" id="Dosis">
<label for="Dosis">Dosis</label><br>
<label for="DinAlternate"><img src="fonts/DinAlternate-p1.png"> <img src="fonts/DinAlternate-p2.png"></label><br>
<input type="checkbox" id="Impact">
<label for="Impact">Impact</label><br>
<label for="Impact"><img src="fonts/Impact-p1.png"> <img src="fonts/Impact-p2.png"></label><br>
<input type="checkbox" id="Nunito">
<label for="Nunito">Nunito</label><br>
<label for="Nunito"><img src="fonts/Nunito-p1.png"> <img src="fonts/Nunito-p2.png"></label><br>
<input type="checkbox" id="OpenSansEC">
<label for="OpenSansEC">Open Sans Extra Condensed</label><br>
<label for="OpenSansEC"><img src="fonts/OpenSansEC-p1.png"> <img src="fonts/OpenSansEC-p2.png"></label><br>
<input type="checkbox" id="Phosphate">
<label for="Phosphate">Phosphate</label><br>
<label for="Phosphate"><img src="fonts/Phosphate-p1.png"> <img src="fonts/Phosphate-p2.png"></label><br>
<input type="checkbox" id="Quicksand">
<label for="Quicksand">Quicksand</label><br>
<input type="checkbox" id="SairaCond">
<label for="SairaCond">Saira Condensed</label><br>
<label for="Quicksand"><img src="fonts/Quicksand-p1.png"> <img src="fonts/Quicksand-p2.png"></label><br>
<input type="checkbox" id="SairaEC">
<label for="SairaEC">Saira Extra Condensed</label><br>
<label for="SairaEC"><img src="fonts/SairaEC-p1.png"> <img src="fonts/SairaEC-p2.png"></label><br>
<input type="checkbox" id="Teko">
<label for="Teko">Teko</label><br>
<label for="Teko"><img src="fonts/Teko-p1.png"> <img src="fonts/Teko-p2.png"></label><br>
<input type="checkbox" id="Yumaro">
<label for="Yumaro">Yumaro</label><br>
<label for="Yumaro"><img src="fonts/Yumaro-p1.png"> <img src="fonts/Yumaro-p2.png"></label><br>
<input type="checkbox" id="YuseiMagic">
<label for="YuseiMagic">Yusei Magic</label><br>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<label for="YuseiMagic"><img src="fonts/YuseiMagic-p1.png"> <img src="fonts/YuseiMagic-p2.png"></label><br>
<p><button id="upload" class="btn btn-primary">Upload selected Fonts</button></p>
<script src="../../core/lib/customize.js"></script>
<script>
FontList = ["BarlowCond", "BebasNeue", "Dekko", "DinAlternate", "Dosis",
"Impact", "Nunito", "OpenSansEC", "Phosphate", "Quicksand", "SairaCond", "SairaEC",
"Yumaro", "YuseiMagic"]
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
var n=0;
var fonts = [];
for (fontName of FontList) {
if (document.getElementById(fontName).checked==true) {
var f = new Object();
f.name="contourclock-"+n+".json";
f.url="font-"+fontName+".json";
//fonts.push({name:"contourclock-"+n+".json", url:"font-"+fontName+".json"});
fonts.push(f);
//console.log("contourclock-"+n+".json <- font-"+fontName+".json");
n++;
FontList = ["BarlowCond", "BebasNeue", "Dekko", "DinAlternate",
"Impact", "Nunito", "OpenSansEC", "Phosphate", "Quicksand", "SairaEC",
"Yumaro", "YuseiMagic"]
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
var n=0;
var fonts = [];
for (fontName of FontList) {
if (document.getElementById(fontName).checked==true) {
var f = new Object();
fonts.push({
name:"contourclock-"+n+".json",
url:"font-"+fontName+".json"
});
n++;
}
}
}
//console.log(fonts[0]);
sendCustomizedApp(storage=fonts);
/*sendCustomizedApp({
storage:[
{name:"myapp.app.js", url:"app.js", content:app},
]
});*/
});
</script>
if (n>0)
sendCustomizedApp({storage:fonts});
else
alert("Please select at least one Font!");
});
</script>
</form>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

View File

@ -0,0 +1 @@

View File

@ -1,13 +1,12 @@
{ "id": "contourclock",
"name": "Contour Clock",
"shortName" : "Contour Clock",
"version":"0.23",
"version":"0.24",
"icon": "app.png",
"description": "A Minimalist clockface with large Digits. Now with more fonts!",
"screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}],
"tags": "clock",
"custom": "custom.html",
"allow_emulator":true,
"supports" : ["BANGLEJS2"],
"type": "clock",
"storage": [

View File

@ -5,3 +5,4 @@
0.05: add Bangle 2 version
0.06: Adds settings page (hide clocks or launchers)
0.07: Adds setting for directly launching app on touch for Bangle 2
0.08: Optimize line wrapping for Bangle 2

View File

@ -45,11 +45,23 @@ function draw_icon(p,n,selected) {
g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
g.setFontAlign(0,-1,0).setFont("6x8",1);
var txt = apps[p*4+n].name.split(" ");
for (var i = 0; i < txt.length; i++) {
txt[i] = txt[i].trim();
g.drawString(txt[i],x+36,y+54+i*8);
var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
var lineY = 0;
var line = "";
while (txt.length > 0){
var c = txt.shift();
if (c.length + 1 + line.length > 13){
if (line.length > 0){
g.drawString(line.trim(),x+36,y+54+lineY*8);
lineY++;
}
line = c;
} else {
line += " " + c;
}
}
g.drawString(line.trim(),x+36,y+54+lineY*8);
}
function drawPage(p){

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.07",
"version": "0.08",
"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",

View File

@ -107,3 +107,8 @@ try {
* Some useful code on Github can be found [here](https://portal.u-blox.com/s/question/0D52p0000925T00CAE/ublox-max-m8q-getting-stuck-when-sleeping-with-extint-pin-control)
and [here](https://github.com/thasti/utrak/blob/master/gps.c)
Written by: [Hugh Barney, with support from Gordon Williams](https://github.com/hughbarney) For support
and discussion please post in the [Bangle JS
Forum](http://forum.espruino.com/microcosms/1424/)

View File

@ -280,3 +280,8 @@ The following error codes will be displayed if one of the dependancies is not me
* Add a small graph to the heart rate monitor app
* Add a facility to call the Arrow calibration process
* Maybe create waypoints.json file if missing
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

@ -14,4 +14,5 @@
0.14: Added altitude as an option to display.
0.15: Using wpedom to count steps.
0.16: Improved stability. Wind can now be shown.
0.17: Settings for mph/kph and other minor improvements.
0.17: Settings for mph/kph and other minor improvements.
0.18: Fullscreen mode can now be enabled or disabled in the settings.

View File

@ -11,7 +11,7 @@ with Gadgetbride and the weather app must be installed.
## Features
* LCARS Style watch face.
* Full screen mode - widgets are still loaded but not shown.
* Enable or disable fullscreen mode (widgets are always loaded, but hidden if fullscreen).
* Tab on left/right to switch between different screens.
* Cusomizable data that is shown on screen 1 (steps, weather etc.)
* Shows random and real images of planets.
@ -33,7 +33,7 @@ with Gadgetbride and the weather app must be installed.
## Multiple screens support
Access different screens via tap on the left/ right side of the screen
![](screenshot.png)
![](screenshot_1.png)
![](screenshot_2.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

View File

@ -7,6 +7,7 @@ let settings = {
dataRow2: "Temp",
dataRow3: "Battery",
speed: "kph",
fullscreen: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -30,23 +31,39 @@ let lcarsViewPos = 0;
// let hrmValue = 0;
var plotMonth = false;
/*
* Requirements and globals
*/
var bgLeft = {
var bgLeftFullscreen = {
width : 27, height : 176, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAFCh/eX5Q/KAwdCAGVbtu27YCCoAJBkuWrNlAQRGCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A=="))
};
var bgRight = {
var bgLeftNotFullscreen = {
width : 27, height : 152, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAGVbtu27YCCoAJBkuWrNlAQRkCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A="))
};
var bgRightFullscreen = {
width : 27, height : 176, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAnUP7y/KH4yGeVYAJrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgAA="))
};
var bgRightNotFullscreen = {
width : 27, height : 152, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAxrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgA="))
};
var bgLeft = settings.fullscreen ? bgLeftFullscreen : bgLeftNotFullscreen;
var bgRight= settings.fullscreen ? bgRightFullscreen : bgRightNotFullscreen;
var iconEarth = {
width : 50, height : 50, bpp : 3,
buffer : require("heatshrink").decompress(atob("AFtx48ECBsDwU5k/yhARLjgjBjlzAQMQEZcIkOP/fn31IEZgCBnlz58cEpM4geugEgwU/8+WNZJHDuHHvgmBCQ8goEOnVgJoMnyV58mACItHI4X8uAFBuVHnnz4BuGxk4////Egz3IkmWvPgNw8f/prB//BghTC+AjE7848eMjNnzySBwUJkmf/BuGuPDAQIjBiPHhhTCSQnjMo0ITANJn44Dg8MuFBggCCiFBcAJ0Bv5xEh+ITo2OhHkyf/OIQdBWwVHhgjBNwUE+fP/5EEgePMoYLBhMgyVJk/+BQQdC688I4XxOIc8v//NAvr+QEBj/5NwKVBy1/QYUciPBhk1EAJrC+KeC489QYaMBgU/8BNB9+ChEjz1Jkn/QYMBDQIgCcYTCCiP/nlzJQmenMAgV4//uy/9wRaB/1J8iVCcAfHjt9TYYICnhKCgRKBw159/v//r927OIeeoASBDQccvv3791KYVDBYPLJQeCnPnz//AAP6ocEjEkXgMgJQtz79fLAP8KYkccAcJ8Gf/f/xu/cAMQ4eP5MlyQRCMolx40YsOGBAPfnnzU4KVDpKMBvz8Dh0/8me7IICgkxJQXPIgZTD58sEgcJk+eNoONnFBhk4/5uB/pcDg5KD+4mEv4CBXISVDhEn31/8/+mH7x//JQK5CAAMB4JBCnnxJQf/+fJEgkAa4L+CAQOOjMn/1bXIRxDJQXx58f//Hhlz/88EgsChMgz/Zs/+nfkyV/8huDOI6SD498NwoACi1Z8+S/Plz17/+QCI7jC+ZxBmfPnojIAAMDcYWSp//2wRJEwq2GABECjMgNYwAmA="))
@ -217,7 +234,7 @@ function drawHorizontalBgLine(color, x1, x2, y, h){
function drawInfo(){
if(lcarsViewPos != 0){
if(lcarsViewPos != 0 || !settings.fullscreen){
return;
}
@ -276,9 +293,10 @@ function drawState(){
function drawPosition0(){
// Draw background image
g.drawImage(bgLeft, 0, 0);
drawHorizontalBgLine(cBlue, 25, 120, 0, 4);
drawHorizontalBgLine(cBlue, 130, 176, 0, 4);
var offset = settings.fullscreen ? 0 : 24;
g.drawImage(bgLeft, 0, offset);
drawHorizontalBgLine(cBlue, 25, 120, offset, 4);
drawHorizontalBgLine(cBlue, 130, 176, offset, 4);
drawHorizontalBgLine(cPurple, 20, 70, 80, 4);
drawHorizontalBgLine(cPurple, 80, 176, 80, 4);
drawHorizontalBgLine(cOrange, 35, 110, 87, 4);
@ -304,15 +322,26 @@ function drawPosition0(){
var currentDate = new Date();
var timeStr = locale.time(currentDate,1);
g.setFontAntonioLarge();
g.drawString(timeStr, 27, 10);
if(settings.fullscreen){
g.drawString(timeStr, 27, 10);
} else {
g.drawString(timeStr, 27, 33);
}
// Write date
g.setColor(cWhite);
g.setFontAntonioMedium();
var dayStr = locale.dow(currentDate, true).toUpperCase();
dayStr += " " + currentDate.getDate();
dayStr += " " + locale.month(currentDate, 1).toUpperCase();
g.drawString(dayStr, 30, 56);
if(settings.fullscreen){
var dayStr = locale.dow(currentDate, true).toUpperCase();
dayStr += " " + currentDate.getDate();
dayStr += " " + locale.month(currentDate, 1).toUpperCase();
g.drawString(dayStr, 30, 56);
} else {
var dayStr = locale.dow(currentDate, true).toUpperCase();
var date = currentDate.getDate();
g.drawString(dayStr, 128, 35);
g.drawString(date, 128, 55);
}
// Draw data
g.setFontAlign(-1, -1, 0);
@ -327,8 +356,11 @@ function drawPosition0(){
function drawPosition1(){
// Draw background image
g.drawImage(bgRight, 149, 0);
drawHorizontalBgLine(cBlue, 0, 140, 0, 4);
var offset = settings.fullscreen ? 0 : 24;
g.drawImage(bgRight, 149, offset);
if(settings.fullscreen){
drawHorizontalBgLine(cBlue, 0, 140, offset, 4);
}
drawHorizontalBgLine(cPurple, 0, 80, 80, 4);
drawHorizontalBgLine(cPurple, 90, 150, 80, 4);
drawHorizontalBgLine(cOrange, 0, 50, 87, 4);
@ -388,8 +420,13 @@ function drawPosition1(){
g.setFontAlign(1, 1, 0);
g.setFontAntonioMedium();
g.setColor(cWhite);
g.drawString("M-HRM", 154, 27);
g.drawString("M-STEPS [K]", 154, 115);
if(settings.fullscreen){
g.drawString("M-HRM", 154, 27);
g.drawString("M-STEPS [K]", 154, 115);
} else {
g.drawString("MONTH", 154, 115);
}
// Plot day
} else {
@ -429,8 +466,13 @@ function drawPosition1(){
g.setFontAlign(1, 1, 0);
g.setFontAntonioMedium();
g.setColor(cWhite);
g.drawString("D-HRM", 154, 27);
g.drawString("D-STEPS", 154, 115);
if(settings.fullscreen){
g.drawString("D-HRM", 154, 27);
g.drawString("D-STEPS", 154, 115);
} else {
g.drawString("DAY", 154, 115);
}
}
}
@ -451,6 +493,13 @@ function draw(){
} else if (lcarsViewPos == 1) {
drawPosition1();
}
// After drawing the watch face, we can draw the widgets
if(settings.fullscreen){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
} else {
Bangle.drawWidgets();
}
}
@ -501,8 +550,9 @@ function getWeather(){
weather.hum = weather.hum + "%";
// Wind
var speedFactor = settings.speed == "kph" ? 1.60934 : 1.0;
weather.wind = Math.round(weather.wind * speedFactor);
const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
var speedFactor = settings.speed == "kph" ? 1.0 : 1.0 / 1.60934;
weather.wind = Math.round(wind[1] * speedFactor);
return weather
}
@ -652,16 +702,7 @@ Bangle.on('touch', function(btn, e){
// Show launcher when middle button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
// Clear the screen once, at startup and draw clock
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
draw();
// After drawing the watch face, we can draw the widgets
// Bangle.drawWidgets();

View File

@ -9,6 +9,7 @@
dataRow2: "Steps",
dataRow3: "Temp",
speed: "kph",
fullscreen: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -52,6 +53,14 @@
save();
},
},
'Full Screen': {
value: settings.fullscreen,
format: () => (settings.fullscreen ? 'Yes' : 'No'),
onchange: () => {
settings.fullscreen = !settings.fullscreen;
save();
},
},
'Speed': {
value: 0 | speedOptions.indexOf(settings.speed),
min: 0, max: 1,

View File

@ -3,13 +3,15 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.17",
"version":"0.18",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
"type": "clock",
"tags": "clock",
"screenshots": [{"url":"screenshot.png"}],
"screenshots": [
{"url":"screenshot_1.png"},
{"url":"screenshot_3.png"}],
"storage": [
{"name":"lcars.app.js","url":"lcars.app.js"},
{"name":"lcars.img","url":"lcars.icon.js","evaluate":true},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

BIN
apps/lcars/screenshot_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
apps/lcars/screenshot_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,2 +1,3 @@
0.01: First release
0.02: Enhanced icon, make it bolder
0.03: Fixed issue with defaulting back to London

View File

@ -4,7 +4,7 @@
"icon": "mylocation.png",
"type": "app",
"screenshots": [{"url":"screenshot_1.png"}],
"version":"0.02",
"version":"0.03",
"description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README",
"readme": "README.md",
"tags": "tool,utility",

View File

@ -9,32 +9,35 @@ let s = {
'lat': 51.5072,
'lon': 0.1276,
'location': "London"
}
};
function loadSettings() {
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || s;
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
for (const key in settings) {
s[key] = settings[key]
}
}
function save() {
settings = s
require('Storage').write(SETTINGS_FILE, settings)
settings = s;
require('Storage').write(SETTINGS_FILE, settings);
}
const locations = ["London", "Newcastle", "Edinburgh", "Paris", "New York", "Tokyo","???"];
const lats = [51.5072 ,54.9783 ,55.9533 ,48.8566 ,40.7128 ,35.6762, 0.0];
const lons = [-0.1276 ,-1.6178 ,-3.1883 ,2.3522 , -74.0060 ,139.6503, 0.0];
const locations = ["London" ,"Newcastle","Edinburgh", "Paris" , "New York" , "Tokyo" , "Frankfurt", "Auckland", "???"];
const lats = [ 51.5072 , 54.9783 , 55.9533 , 48.8566 , 40.7128 , 35.6762 , 50.1236 , -36.9 , 0.0 ];
const lons = [ -0.1276 , -1.6178 , -3.1883 , 2.3522 , -74.0060 , 139.6503 , 8.6553 , 174.7832 , 0.0 ];
function setFromGPS() {
Bangle.on('GPS', (gps) => {
//console.log(".");
if (gps.fix === 0) return;
//console.log("fix from GPS");
s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' }
s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' };
Bangle.buzz(1500); // buzz on first position
Bangle.setGPSPower(0);
save();
Bangle.setUI("updown", ()=>{ load() });
Bangle.setUI("updown", ()=>{ load(); });
E.showPrompt("Location has been saved from the GPS fix",{
title:"Location Saved",
buttons : {"OK":1}
@ -49,13 +52,13 @@ function setFromGPS() {
}
function showMainMenu() {
console.log("showMainMenu");
//console.log("showMainMenu");
const mainmenu = {
'': { 'title': 'My Location' },
'<Back': ()=>{ load(); },
'City': {
value: 0 | locations.indexOf(s.location),
min: 0, max: 6,
min: 0, max: locations.length - 1,
format: v => locations[v],
onchange: v => {
if (v != 6) {
@ -67,7 +70,7 @@ function showMainMenu() {
}
},
'Set From GPS': ()=>{ setFromGPS(); }
}
};
return E.showMenu(mainmenu);
}

View File

@ -13,3 +13,6 @@
which requires 2.11.27 firmware to reset at midnight
0.13: call process.memory(false) to avoid triggering a GC of memory
supported in pre 2.12.13 firmware
0.14: incorporated lazybones idle timer, configuration settings to come
0.15: fixed tendancy for mylocation to default to London
added setting to enable/disable idle timer warning

View File

@ -2,8 +2,8 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"version": "0.13",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times.",
"version": "0.15",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"icon": "pastel.png",
"dependencies": {"mylocation":"app","weather":"app"},
"screenshots": [{"url":"screenshot_pastel.png"}, {"url":"weather_icons.png"}],

View File

@ -4,9 +4,18 @@ const storage = require('Storage');
const locale = require("locale");
const SETTINGS_FILE = "pastel.json";
const LOCATION_FILE = "mylocation.json";
const w = g.getWidth();
const h = g.getHeight();
let settings;
let location;
// variable for controlling idle alert
let lastStep = getTime();
let lastStepTime = '??';
let warned = 0;
let idle = false;
let IDLE_MINUTES = 26;
// cloud, sun, partSun, snow, rain, storm, error
// create 1 bit, max contrast, brightness set to 85
var cloudIcon = require("heatshrink").decompress(atob("kEggIfcj+AAYM/8ADBuFwAYPAmADCCAMBwEf8ADBhFwg4aBnEPAYMYjAVBhgDDDoQDHCYc4jwDB+EP///FYIDBMTgA=="));
@ -16,16 +25,24 @@ var snowIcon = require("heatshrink").decompress(atob("kEggITQj/AAYM98ADBsEwAYPAj
var rainIcon = require("heatshrink").decompress(atob("kEggIPMh+AAYM/8ADBuFwAYPgmADB4EbAYOAj/ggOAhnwg4aBnAeCjEcCIMMjADCDoQDHjAPCnAXCuEP///8EDAYJECAAXBwkAgPDhwDBwUMgEEhkggEOjFgFgMQLYQAOA=="));
var errIcon = require("heatshrink").decompress(atob("kEggILIgOAAYsD4ADBg/gAYMGsADBhkwAYsYjADCjgDBmEMAYNxxwDBsOGAYPBwYDEgOBwOAgYDB4EDHYPAgwDBsADDhgDBFIcwjAHBjE4AYMcmADBhhNCKIcG/4AGOw4A=="));
// saves having to recode all the small font calls
function setSmallFont() {
g.setFontLatoSmall();
}
function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
settings.grid = settings.grid||false;
settings.font = settings.font||"Lato";
settings.idle_check = settings.idle_check||true;
}
// requires the myLocation app
function loadLocation() {
location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
location = require("Storage").readJSON(LOCATION_FILE,1)||{};
location.lat = location.lat||51.5072;
location.lon = location.lon||0.1276;
location.location = location.location||"London";
}
function extractTime(d){
@ -71,17 +88,18 @@ function getSteps() {
if (WIDGETS.wpedom !== undefined)
return WIDGETS.wpedom.getSteps();
else
return '???'
return '???';
}
}
const infoData = {
ID_BLANK: { calc: () => '' },
ID_DATE: { calc: () => {var d = (new Date).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} },
ID_DAY: { calc: () => {var d = require("locale").dow(new Date).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} },
ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} },
ID_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} },
ID_SR: { calc: () => 'Sunrise: ' + sunRise },
ID_SS: { calc: () => 'Sunset: ' + sunSet },
ID_STEP: { calc: () => 'Steps: ' + getSteps() },
ID_LAST: { calc: () => 'Last Step: ' + lastStepTime },
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' },
ID_MEM: { calc: () => {var val = process.memory(false); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
ID_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} },
@ -152,6 +170,14 @@ function getWeather() {
}
function draw() {
if (!idle)
drawClock();
else
drawIdle();
queueDraw();
}
function drawClock() {
var d = new Date();
var da = d.toString().split(" ");
var time = da[4].substr(0,5);
@ -166,11 +192,8 @@ function draw() {
if (parseInt(hh) > 12)
hh = h2.substr(h2.length -2);
var w = g.getWidth();
var h = g.getHeight();
var x = (g.getWidth()/2);
var y = (g.getHeight()/3);
var weatherJson = getWeather();
var w_temp;
var w_icon;
@ -190,7 +213,8 @@ function draw() {
}
g.reset();
g.clearRect(0, 30, w, h - 24);
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
// draw a grid like graph paper
if (settings.grid && process.env.HWVERSION !=1) {
@ -249,6 +273,141 @@ function draw() {
queueDraw();
}
///////////////// IDLE TIMER /////////////////////////////////////
function log_debug(o) {
//print(o);
}
function drawIdle() {
let mins = Math.round((getTime() - lastStep) / 60);
g.reset();
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
g.setColor(g.theme.fg);
setSmallFont();
g.setFontAlign(0, 0);
g.drawString('Last step was', w/2, (h/3));
g.drawString(mins + ' minutes ago', w/2, 20+(h/3));
dismissBtn.draw();
}
/////////////// BUTTON CLASS ///////////////////////////////////////////
// simple on screen button class
function BUTTON(name,x,y,w,h,c,f,tx) {
this.name = name;
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.color = c;
this.callback = f;
this.text = tx;
}
// if pressed the callback
BUTTON.prototype.check = function(x,y) {
//console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
log_debug(this.name + ":callback\n");
this.callback();
return true;
}
return false;
};
BUTTON.prototype.draw = function() {
g.setColor(this.color);
g.fillRect(this.x, this.y, this.x + this.w, this.y + this.h);
g.setColor("#000"); // the icons and boxes are drawn black
setSmallFont();
g.setFontAlign(0, 0);
g.drawString(this.text, (this.x + this.w/2), (this.y + this.h/2));
g.drawRect(this.x, this.y, (this.x + this.w), (this.y + this.h));
};
function dismissPrompt() {
idle = false;
warned = false;
lastStep = getTime();
Bangle.buzz(100);
draw();
}
var dismissBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", dismissPrompt, "Dismiss");
Bangle.on('touch', function(button, xy) {
if (idle && dismissBtn.check(xy.x, xy.y)) return;
});
// if we get a step then we are not idle
Bangle.on('step', s => {
setLastStepTime();
lastStep = getTime();
// redraw if we had been idle
if (idle == true) {
dismissPrompt();
}
idle = false;
warned = 0;
});
function setLastStepTime() {
var date = new Date();
lastStepTime = require("locale").time(date,1);
}
function checkIdle() {
if (!settings.idle_check) {
idle = false;
warned = false;
return;
}
let hour = (new Date()).getHours();
let active = (hour >= 9 && hour < 21);
//let active = true;
let dur = getTime() - lastStep;
if (active && dur > IDLE_MINUTES * 60) {
drawIdle();
if (warned++ < 3) {
buzzer(warned);
log_debug("checkIdle: warned=" + warned);
Bangle.setLocked(false);
}
idle = true;
} else {
idle = false;
warned = 0;
}
}
setLastStepTime();
// timeout for multi-buzzer
var buzzTimeout;
// n buzzes
function buzzer(n) {
log_debug("buzzer n=" + n);
if (n-- < 1) return;
Bangle.buzz(250);
if (buzzTimeout) clearTimeout(buzzTimeout);
buzzTimeout = setTimeout(function() {
buzzTimeout = undefined;
buzzer(n);
}, 500);
}
///////////////////////////////////////////////////////////////////////////////
// timeout used to update every minute
var drawTimeout;
@ -258,6 +417,7 @@ function queueDraw() {
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
prevInfo();
checkIdle();
draw();
}, 60000 - (Date.now() % 60000));
}

View File

@ -5,6 +5,7 @@
let s = {
'grid': false,
'weather': false,
'idle_check': true,
'font': "Lato"
}
@ -51,6 +52,24 @@
s.weather = !s.weather;
save();
},
},
// for use when the new menu system goes live
/*
'Idle Warning': {
value: s.idle_check,
onchange : v => {
s.idle_check = v;
save();
},
},
*/
'Idle Warning': {
value: s.idle_check,
format: () => (s.idle_check ? 'Yes' : 'No'),
onchange: () => {
s.idle_check = !s.idle_check;
save();
},
}
})
})

View File

@ -4,8 +4,11 @@
</head>
<body>
<div id="tracks"></div>
<div class="container" id="toastcontainer" stlye="position:fixed; bottom:8px; left:0px; right:0px; z-index: 100;"></div>
<script src="../../core/lib/interface.js"></script>
<script src="../../core/js/ui.js"></script>
<script src="../../core/js/utils.js"></script>
<script>
var domTracks = document.getElementById("tracks");
@ -70,9 +73,15 @@ ${track.map(pt=>` <gx:value>${0|pt.Skin}</gx:value>\n`).join("")}
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
showToast("Download finished.", "success");
}
function saveGPX(track, title) {
if (!track || !track[0] || !"Time" in track[0] || !track[0].Time) {
showToast("Error in trackfile.", "error");
return;
}
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
<metadata>
@ -109,6 +118,7 @@ function saveGPX(track, title) {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
showToast("Download finished.", "success");
}
function saveCSV(track, title) {
@ -121,6 +131,7 @@ function saveCSV(track, title) {
}).join(",")+"\n";
});
Util.saveCSV(title, csv);
showToast("Download finished.", "success");
}
function trackLineToObject(headers, l) {
@ -171,6 +182,10 @@ function getTrackList() {
return {headers:headers,l:data};
})(${JSON.stringify(filename)})`, trackInfo=>{
console.log(filename," => ",trackInfo);
if (!trackInfo || !"headers" in trackInfo) {
showToast("Error loading track list.", "error");
resolve();
}
trackInfo.headers = trackInfo.headers.split(",");
trackList.push({
filename : filename,

View File

@ -1 +1,3 @@
1.0: Initial version of game
0.01: Initial version of game
0.02: Fix mistake preventing game from ending in some cases.
0.03: Update help screen with more details.

View File

@ -9,7 +9,7 @@ For rules, see [here](https://asmadigames.com/Red7Rules.pdf).
## Usage
Current rule card is shown in center of screen when viewing your hand.
Swipe left to see your palettes and right on the palette screen to go back to your hand. Tap on a card to see it's details and then swipe either left or right to play the card as a rule or a palette card.
Swipe left to see your palettes and right on the palette screen to go back to your hand. Tap on a card to see it's details and then swipe either left or right to play the card as a rule or a palette card. Taping anywhere besides the card will dismis the card details.
Press the watch button to bring up the menu, which you can undo your card plays, end your turn, or start a new game.
## Creator

View File

@ -2,7 +2,7 @@
"name": "Red 7 Card Game",
"shortName" : "Red 7",
"icon": "icon.png",
"version":"1.0",
"version":"0.03",
"description": "An implementation of the card game Red 7 for your watch. Play against the AI and be the last player still in the game to win!",
"tags": "game",
"supports":["BANGLEJS2"],

View File

@ -654,7 +654,52 @@ function drawScreen2() {
}
function drawScreenHelp() {
E.showAlert("Rules can be found on asmadigames.com").then(function(){drawMainMenu();});
//E.showAlert("Rules can be found on asmadigames.com").then(function(){drawMainMenu();});
E.showScroller({
h: 25,
c: 10,
draw: (idx,r) => {
g.setBgColor("#000").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
g.setColor("#fff");
switch(idx) {
case 0:
g.setFont("6x8:2").drawString("Rules can be",r.x+10,r.y+4);
break;
case 1:
g.setFont("6x8:2").drawString("found on",r.x+10,r.y+4);
break;
case 2:
g.setFont("Vector:18").drawString("asmadigames.com",r.x+10,r.y+4);
break;
case 3:
g.setFont("6x8:1").drawString("Use button to show menu.",r.x+10,r.y+4);
break;
case 4:
g.setFont("6x8:1").drawString("Swipe L/R for hand/palette.",r.x+10,r.y+4);
break;
case 5:
g.setFont("6x8:1").drawString("Tap card to see details.",r.x+10,r.y+4);
break;
case 6:
g.setFont("6x8:1").drawString("Swipe card L/R to play.",r.x+10,r.y+4);
break;
case 7:
g.setFont("6x8:1").drawString("Finish turn in menu.",r.x+10,r.y+4);
break;
case 9:
g.fillRect(r.x+40,r.y+0,r.x+140,r.y+20);
g.setColor(0,0,0);
g.setFont("Vector:14").drawString("OK",r.x+80,r.y+4);
break;
}
},
select: (idx) => {
if(idx === 9){
E.showScroller();
drawMainMenu();
}
}
});
}
function drawGameOver(win) {
@ -678,9 +723,7 @@ function finishTurn() {
} else if(playerHand.handCards.length === 0) {
drawGameOver(false);
} else if(!canPlay(playerHand, playerPalette, AIPalette)) {
console.log("no play");
//drawGameOver(false);
drawScreen1();
drawGameOver(false);
} else {
E.showMenu();
drawScreen1();

View File

@ -1,3 +1,4 @@
0.01: Hello Ruuvi Watch!
0.02: Clear gfx on startup.
0.03: Improve design and code, reduce flicker.
0.04: Ability to rename tags. Sauna, Fridge & Freezer alert. Support °F based on locale.

View File

@ -2,6 +2,8 @@
Watch the status of [RuuviTags](https://ruuvi.com) in range.
By Marc Englund [GitHub](https://github.com/emarc) | [Twitter](https://twitter.com/marcenglund)
![Ruuvi Watch in action](/BangleApps/apps/ruuviwatch/ruuviwatch-in-action.jpg)
- Id
@ -9,18 +11,23 @@ Watch the status of [RuuviTags](https://ruuvi.com) in range.
- Humidity (%)
- Pressure (hPa)
- Battery voltage
Also shows how "fresh" the data is (age of reading).
- Reading "freshness" (age)
- Ability to name tags
- Alerts for Sauna, Fridge, Freezer
## Usage
- Scans for devices when launched and every N seconds.
- Page trough devices with BTN1/BTN3.
- Trigger scan with BTN2.
- Page trough devices with left/right swipe or BTN1/BTN3.
- Page past last/first to trigger scan.
- BTN2 = Menu; name tag & trigger scan
- Change locale (via App Loader) to get Farenheit.
## Todo / ideas
- Settings for scan frequency, units
- Allow to "name" known devices
- Include more data
- Bangle 2 support (I don't have one, let me know if you want to help with testing!)
- Settings for scan frequency
- Settings for alert limits
- Alert for "Wine cellar"
- Alert for Washer & Dryer (stops shaking = ready)
- Support older Ruuvi protocols

View File

@ -2,7 +2,7 @@
"name": "Ruuvi Watch",
"shortName":"Ruuvi Watch",
"icon": "ruuviwatch.png",
"version":"0.03",
"version":"0.04",
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.",
"readme":"README.md",
"tags": "bluetooth",

View File

@ -7,11 +7,21 @@ require("Storage").write("ruuviwatch.info", {
const lookup = {};
const ruuvis = [];
const names = require("Storage").readJSON("RuuviNames") || {};
let current = 0;
let scanning = false;
let paused = false;
const SCAN_FREQ = 1000 * 30;
// ALERT LIMITS
LIMIT_SAUNA = 60;
LIMIT_FRIDGE = 4;
LIMIT_FREEZER = -18;
// TODO add wine cellar limits
// TODO configurable limits
// Fonts
const FONT_L = "Vector:60";
const FONT_M = "Vector:20";
@ -80,8 +90,8 @@ function p(data) {
int2Hex(data[OFFSET + 23]),
].join(":");
robject.name =
"Ruuvi " + int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
robject.id = int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
return robject;
}
@ -114,6 +124,7 @@ function drawAge() {
}
function redrawAge() {
if (paused) return;
const originalColor = g.getColor();
g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10);
g.setFont(FONT_S);
@ -128,9 +139,15 @@ function redrawAge() {
g.setColor(originalColor);
}
function getName(id) {
let name = names[id] || "Ruuvi";
return name + " (" + id + ")";
}
function redraw() {
g.clear();
g.setColor("#ffffff");
g.setFontAlign(0, 0);
if (ruuvis.length > 0 && ruuvis[current]) {
const ruuvi = ruuvis[current];
@ -145,14 +162,22 @@ function redraw() {
// name
g.setFont(FONT_M);
g.drawString(ruuvi.name, CENTER, NAME_Y);
g.drawString(getName(ruuvi.id), CENTER, NAME_Y);
// age
redrawAge();
// temp
g.setFont(FONT_L);
g.drawString(ruuvi.temperature.toFixed(2) + "°c", CENTER, TEMP_Y);
if (
(ruuvi.name.startsWith("Sauna") && ruuvi.temperature > LIMIT_SAUNA) ||
(ruuvi.name.startsWith("Fridge") && ruuvi.temperature > LIMIT_FRIDGE) ||
(ruuvi.name.startsWith("Freezer") && ruuvi.temperature > LIMIT_FREEZER)
) {
g.setColor("#ffe800");
}
g.drawString(getTempString(ruuvi.temperature), CENTER, TEMP_Y);
g.setColor("#ffffff");
// humid & pressure
g.setFont(FONT_M);
@ -175,8 +200,28 @@ function redraw() {
}
}
function getTempString(temp) {
// workaround: built-in 'locale' looses precision :-(
let unit = "°C";
const isF = require("locale").temp(1).endsWith("F");
if (isF) {
unit = "°F";
temp = (temp + 40) * 1.8 - 40;
}
return temp.toFixed(2) + unit;
}
function attention(message) {
// message ignored for now
Bangle.beep();
Bangle.beep();
Bangle.beep();
Bangle.buzz();
}
function scan() {
if (scanning) return;
if (paused) return;
scanning = true;
NRF.findDevices(
function (devices) {
@ -184,11 +229,36 @@ function scan() {
devices.forEach((device) => {
const data = p(device.data);
data.time = new Date().getTime();
const idx = lookup[data.name];
data.name = names[data.id] || "Ruuvi";
const idx = lookup[data.id];
if (idx !== undefined) {
const old = ruuvis[idx];
if (
data.name.startsWith("Sauna") &&
old.temperature < LIMIT_SAUNA &&
data.temperature > LIMIT_SAUNA
) {
current = idx;
attention(data.name + " ready!");
} else if (
data.name.startsWith("Fridge") &&
old.temperature < LIMIT_FRIDGE &&
data.temperature > LIMIT_FRIDGE
) {
current = idx;
attention(data.name + " warning!");
} else if (
data.name.startsWith("Freezer") &&
old.temperature < LIMIT_FREEZER &&
data.temperature > LIMIT_FREEZER
) {
current = idx;
attention(data.name + " warning!");
}
ruuvis[idx] = data;
} else {
lookup[data.name] = ruuvis.push(data) - 1;
lookup[data.id] = ruuvis.push(data) - 1;
foundNew = true;
}
});
@ -202,23 +272,195 @@ function scan() {
);
}
function setName(newName) {
const ruuvi = ruuvis[current];
ruuvi.name = newName;
names[ruuvi.id] = ruuvi.name;
require("Storage").writeJSON("RuuviNames", names);
}
function closeMenu() {
E.showMenu();
paused = false;
redraw();
}
function showMenu() {
// TODO make this DRY + indicate current in menu
if (!ruuvis.length) {
scan();
return;
}
paused = true;
const ruuvi = ruuvis[current];
const id = ruuvi.id;
const name = getName(id);
var mainmenu = {
"": { title: name },
"Scan now": function () {
closeMenu();
scan();
},
"Rename tag": function () {
E.showMenu(namemenu);
},
"< Back": function () {
closeMenu();
}, // remove the menu
};
// Submenu
var namemenu = {
"": { title: "Rename " + name },
Ruuvi: function () {
setName("Ruuvi");
closeMenu();
},
Indoors: function () {
setName("Indoors");
closeMenu();
},
Downstairs: function () {
setName("Downstairs");
closeMenu();
},
Upstairs: function () {
setName("Upstairs");
closeMenu();
},
Attic: function () {
setName("Attic");
closeMenu();
},
Basement: function () {
setName("Basement");
closeMenu();
},
Kitchen: function () {
setName("Kitchen");
closeMenu();
},
Pantry: function () {
setName("Pantry");
closeMenu();
},
"Living room": function () {
setName("Living room");
closeMenu();
},
"Dining room": function () {
setName("Dining room");
closeMenu();
},
Office: function () {
setName("Office");
closeMenu();
},
Bedroom: function () {
setName("Bedroom");
closeMenu();
},
Bathroom: function () {
setName("Bathroom");
closeMenu();
},
Sauna: function () {
setName("Sauna");
closeMenu();
},
"Wine cellar": function () {
setName("Wine cellar");
closeMenu();
},
Outdoors: function () {
setName("Outdoors");
closeMenu();
},
Porch: function () {
setName("Porch");
closeMenu();
},
Backyard: function () {
setName("Backyard");
closeMenu();
},
Garage: function () {
setName("Garage");
closeMenu();
},
Greenhouse: function () {
setName("Greenhouse");
closeMenu();
},
Shed: function () {
setName("Shed");
closeMenu();
},
Fridge: function () {
setName("Fridge");
closeMenu();
},
Freezer: function () {
setName("Freezer");
closeMenu();
},
Dryer: function () {
setName("Dryer");
closeMenu();
},
Washer: function () {
setName("Washer");
closeMenu();
},
"< Back": function () {
E.showMenu(mainmenu);
},
};
// Actually display the menu
E.showMenu(mainmenu);
}
function nextPage() {
current++;
if (current >= ruuvis.length) {
current = 0;
scan();
}
redraw();
}
function prevPage() {
current--;
if (current < 0) {
current = ruuvis.length - 1;
scan();
}
redraw();
}
// START
Bangle.on("swipe", function (dir) {
if (paused) return;
if (dir > 0) {
prevPage();
} else {
nextPage();
}
});
// Button 1 pages up
setWatch(
() => {
current--;
if (current < 0) {
current = ruuvis.length - 1;
}
redraw();
if (paused) return;
prevPage();
},
BTN1,
{ repeat: true }
);
// button triggers scan
// button triggers menu
setWatch(
() => {
scan();
if (paused) return;
showMenu();
},
BTN2,
{ repeat: true }
@ -226,11 +468,8 @@ setWatch(
// button 3 pages down
setWatch(
() => {
current++;
if (current >= ruuvis.length) {
current = 0;
}
redraw();
if (paused) return;
nextPage();
},
BTN3,
{ repeat: true }

View File

@ -3,6 +3,7 @@
"name": "Simplest Clock",
"version": "0.06",
"description": "The simplest working clock, acts as a tutorial piece",
"readme": "README.md",
"icon": "simplest.png",
"screenshots": [{"url":"screenshot_simplest.png"}],
"type": "clock",

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Fix crash on start
0.03: Added power saving mode, move all read/write log actions into lib/module, fix #1445

View File

@ -2,20 +2,26 @@
This app logs and displays the four following states:
_unknown, not worn, awake, sleeping_
It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm) and uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments ([ESS](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en)) and the internal temperature to decide _sleeping_ or _not worn_ when the watch is resting.
It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm) and uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments ([ESS](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en)) and
also provides a power saving mode using the built in movement calculation. The internal temperature is used to decide if the status is _sleeping_ or _not worn_.
#### Operating Principle
* __ESS calculation__
The accelerometer polls values with 12.5Hz. On each poll the magnitude value is saved. When 13 values are collected, every 1.04 seconds, the standard deviation over this values is calculated.
Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset.
When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_)
To check if a resting watch indicates as sleeping, the internal temperature must be greater than the temperature threshold (__TempThresh__). Otherwise the watch is considered as not worn.
Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. The first time no movement is detected the actual timestamp is cached (in _sleeplog.firstnomodate_) for logging.
When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_)
* __Power Saving Mode__
On power saving mode the movement value of bangle's build in health event is checked against the maximal movement threshold (__MaxMove__). The event is only triggered every 10 minutes which decreases the battery impact but also reduces accurracy.
* ___Sleeping___ __or__ ___Not Worn___
To check if a resting watch indicates a sleeping status, the internal temperature must be greater than the temperature threshold (__TempThresh__). Otherwise the watch is considered as not worn.
* __True Sleep__
The true sleep value is a simple addition of all registert sleeping periods.
* __Consecutive Sleep__
In addition the consecutive sleep value tries to predict the complete time you were asleep, even the light sleeping phases with registered movements. All periods after a sleeping period will be summarized til the first following non sleeping period that is longer then the maximal awake duration (__MaxAwake__). If this sum is lower than the minimal consecutive sleep duration (__MinConsec__) it is not considered, otherwise it will be added to the consecutive sleep value.
* __Logging__
To minimize the log size only a changed state is logged.
To minimize the log size only a changed state is logged. The logged timestamp is matching the beginning of its measurement period.
When not on power saving mode a movement is detected nearly instantaneous and the detection of a no movement period is delayed by the minimal no movement duration. To match the beginning of the measurement period a cached timestamp (_sleeplog.firstnomodate_) is logged.
On power saving mode the measurement period is fixed to 10 minutes and all logged timestamps are also set back 10 minutes.
---
### Control
@ -28,28 +34,37 @@ It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm
---
### Settings
---
* __BreakTod__ break at time of day
* __BreakTod__ | break at time of day
_0_ / _1_ / _..._ / __10__ / _..._ / _12_
Change time of day on wich the lower graph starts and the upper graph ends.
* __MaxAwake__ maximal awake duration
* __MaxAwake__ | maximal awake duration
_15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_
Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period.
* __MinConsec__ minimal consecutive sleep duration
* __MinConsec__ | minimal consecutive sleep duration
_15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_
Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value.
* __TempThresh__ temperature threshold
* __TempThresh__ | temperature threshold
_20°C_ / _20.5°C_ / _..._ / __25°C__ / _..._ / _40°C_
The internal temperature must be greater than this threshold to log _sleeping_, otherwise it is _not worn_.
* __NoMoThresh__ no movement threshold
* __PowerSaving__
_on_ / __off__
En-/Disable power saving mode. _Saves battery, but might decrease accurracy._
* __MaxMove__ | maximal movement threshold
(only available when on power saving mode)
_50_ / _51_ / _..._ / __100__ / _..._ / _200_
On power saving mode the watch is considered resting if this threshold is lower or equal to the movement value of bangle's health event.
* __NoMoThresh__ | no movement threshold
(only available when not on power saving mode)
_0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_
The standard deviation over the measured values needs to be lower then this threshold to count as not moving.
The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise.
* __MinDuration__ minimal no movement duration
* __MinDuration__ | minimal no movement duration
(only available when not on power saving mode)
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
If no movement is detected for this duration, the watch is considered as resting.
* __Enabled__
__on__ / _off_
En-/Disable the service (all background activities). _Saves battery, but might make this app useless._
En-/Disable the service (all background activities). _Saves the most battery, but might make this app useless._
* __Logfile__
__default__ / _off_
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
@ -65,8 +80,9 @@ For easy access from the console or other apps the following parameters, values
enabled: true, // bool / service status indicator
logfile: "sleeplog.log", // string / used logfile
resting: false, // bool / indicates if the watch is resting
status: 2, // int / actual status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement
status: 2, // int / actual status:
/ undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement, not available in power saving mode
stop: function () { ... }, // funct / stops the service until the next load()
start: function () { ... }, // funct / restarts the service
...
@ -74,42 +90,54 @@ For easy access from the console or other apps the following parameters, values
>require("sleeplog")
={
setEnabled: function (enable, logfile) { ... },
// en-/disable the service and/or logging
// * enable / bool / service status to change to
// * logfile / bool or string
setEnabled: function (enable, logfile, powersaving) { ... },
// restarts the service with changed settings
// * enable / bool / new service status
// * logfile / bool or string
// - true = enables logging to "sleeplog.log"
// - "some_file.log" = enables logging to "some_file.log"
// - false = disables logging
// returns: bool or undefined
// - true = changes executed
// - false = no changes needed
// * (powersaving) / bool / new power saving status, default: false
// returns: true or undefined
// - true = service restart executed
// - undefined = no global.sleeplog found
readLog: function (since, until) { ... },
readLog: function (logfile, since, until) { ... },
// read the raw log data for a specific time period
// * since / Date or number / startpoint of period
// * until / Date or number / endpoint of period
// * logfile / string / on no string uses logfile from global object or "sleeplog.log"
// * (since) / Date or number / startpoint of period, default: 0
// * (until) / Date or number / endpoint of period, default: 1E14
// returns: array
// * [[number, int, string], [...], ... ] / sorting: latest first
// - number // timestamp in ms
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// - string // additional information
// * [] = no data available or global.sleeplog found
getReadableLog: function (printLog, since, until) { ... }
// * [] = no data available or global.sleeplog not found
writeLog: function (logfile, input) { ... },
// append or replace log depending on input
// * logfile / string / on no string uses logfile from global object or default
// * input / array
// - append input if array length >1 and element[0] >9E11
// - replace log with input if at least one entry like above is inside another array
// returns: true or undefined
// - true = changest written to storage
// - undefined = wrong input
getReadableLog: function (printLog, since, until, logfile) { ... }
// read the log data as humanreadable string for a specific time period
// * since / Date or number / startpoint of period
// * until / Date or number / endpoint of period
// * (printLog) / bool / direct print output with additional information, default: false
// * (since) / Date or number / see readLog(..)
// * (until) / Date or number / see readLog(..)
// * (logfile) / string / see readLog(..)
// returns: string
// * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last
// * undefined = no data available or global.sleeplog found
restoreLog: function (logfile) { ... }
// eliminate some errors inside a specific logfile
// * logfile / string / name of the logfile that will be restored
// * (logfile) / string / see readLog(..)
// returns: int / number of changes that were made
reinterpretTemp: function (logfile, tempthresh) { ... }
// reinterpret worn status based on given temperature threshold
// * logfile / string / name of the logfile
// * tempthresh / float / new temperature threshold
// * (logfile) / string / see readLog(..)
// * (tempthresh) / float / new temperature threshold, on default uses tempthresh from global object or 27
// returns: int / number of changes that were made
}
```
@ -120,7 +148,9 @@ For easy access from the console or other apps the following parameters, values
#### To do list
* Send the logged information to Gadgetbridge.
_(For now I have no idea how to achieve this, help is appreciated.)_
* View, down- and upload log functions via App Loader.
* Calculate and display overall sleep statistics.
* Option to automatically change power saving mode depending on time of day.
#### Requests, Bugs and Feedback
Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) or send me a [mail](mailto:banglejs@storm64.de).

View File

@ -25,7 +25,7 @@ function drawLog(topY, viewUntil) {
var y = topY + graphHeight;
// read 12h wide log
var log = require("sleeplog").readLog(timestamp0, viewUntil.valueOf());
var log = require("sleeplog").readLog(0, timestamp0, viewUntil.valueOf());
// format log array if not empty
if (log.length) {
@ -149,8 +149,8 @@ function drawNightTo(prevDays) {
// reduce date by 1s to ensure correct headline
date = Date(date.valueOf() - 1E3);
// draw headline, on red bg if service or loggging disabled
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? g.theme.bg : 63488);
// draw headline, on red bg if service or loggging disabled or green bg if powersaving enabled
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
g.fillRect(0, 30, width, 66).reset();
g.setFont("12x20").setFontAlign(0, -1);
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +

View File

@ -2,139 +2,161 @@
// Marko Borazio, Eugen Berlin, Nagihan Kücükyildiz, Philipp M. Scholl and Kristof Van Laerhoven, "Towards a Benchmark for Wearable Sleep Analysis with Inertial Wrist-worn Sensing Units", ICHI 2014, Verona, Italy, IEEE Press, 2014.
// https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en
// sleeplog.status values: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// sleeplog.status values: undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// load settings into global object
global.sleeplog = Object.assign({
enabled: true, // en-/disable completely
logfile: "sleeplog.log", // logfile
powersaving: false, // disables ESS and uses build in movement detection
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
maxmove: 100, // movement threshold on power saving mode
tempthresh: 27, // every temperature above ist registered as worn
}, require("Storage").readJSON("sleeplog.json", true) || {});
// delete app settings
["breaktod", "maxawake", "minconsec"].forEach(property => delete global.sleeplog[property]);
["breaktod", "maxawake", "minconsec"].forEach(property => delete sleeplog[property]);
// check if service enabled
if (global.sleeplog.enabled) {
if (sleeplog.enabled) {
// add cached values and functions to global object
global.sleeplog = Object.assign(global.sleeplog, {
// add always used values and functions to global object
sleeplog = Object.assign(sleeplog, {
// set cached values
ess_values: [],
nomocount: 0,
firstnomodate: undefined,
resting: undefined,
status: 0,
// define acceleration listener function
accel: function(xyz) {
// save acceleration magnitude and start calculation on enough saved data
if (global.sleeplog.ess_values.push(xyz.mag) >= global.sleeplog.winwidth) global.sleeplog.calc();
},
// define calculator function
calc: function() {
// calculate standard deviation over
var mean = this.ess_values.reduce((prev, cur) => cur + prev) / this.winwidth;
var stddev = Math.sqrt(this.ess_values.map(val => Math.pow(val - mean, 2)).reduce((prev, cur) => prev + cur) / this.winwidth);
// reset saved acceleration data
this.ess_values = [];
// check for non-movement according to the threshold
if (stddev < this.nomothresh) {
// increment non-movement sections count, set date of first non-movement
if (++this.nomocount == 1) this.firstnomodate = Math.floor(Date.now());
// check resting state and non-movement count against threshold
if (this.resting !== true && this.nomocount >= this.sleepthresh) {
// change resting state, status and write to log
this.resting = true;
// check if the watch is worn
if (E.getTemperature() > this.tempthresh) {
// set status and write to log as sleping
this.status = 3;
this.log(this.firstnomodate, 3, E.getTemperature());
} else {
// set status and write to log as not worn
this.status = 1;
this.log(this.firstnomodate, 1, E.getTemperature());
}
}
} else {
// reset non-movement sections count
this.nomocount = 0;
// check resting state
if (this.resting !== false) {
// change resting state
this.resting = false;
// set status and write to log as awake
this.status = 2;
this.log(Math.floor(Date.now()), 2);
}
}
},
// define logging function
log: function(date, status, temperature, info) {
// skip logging if logfile is undefined or does not end with ".log"
if (!this.logfile || !this.logfile.endsWith(".log")) return;
// prevent logging on implausible date
if (date < 9E11 || Date() < 9E11) return;
// set default value for status
status = status || 0;
// define storage
var storage = require("Storage");
// read previous logfile
var logContent = storage.read(this.logfile) || "";
// parse previous logfile
var log = JSON.parse(logContent.length > 0 ? atob(logContent) : "[]") ;
// remove last state if it was unknown and is less then 10min ago
if (log.length > 0 && log[0][1] === 0 &&
Math.floor(Date.now()) - log[0][0] < 600000) log.shift();
// add actual status at the first position if it has changed
if (log.length === 0 || log[0][1] !== status)
log.unshift(info ? [date, status, temperature, info] : temperature ? [date, status, temperature] : [date, status]);
// write log to storage
storage.write(this.logfile, btoa(JSON.stringify(log)));
// clear variables
log = undefined;
storage = undefined;
},
status: undefined,
// define stop function (logging will restart if enabled and boot file is executed)
stop: function() {
// remove acceleration and kill listener
Bangle.removeListener('accel', global.sleeplog.accel);
E.removeListener('kill', global.sleeplog.stop);
// remove all listeners
Bangle.removeListener('accel', sleeplog.accel);
Bangle.removeListener('health', sleeplog.health);
E.removeListener('kill', () => sleeplog.stop());
// exit on missing global object
if (!global.sleeplog) return;
// write log with undefined sleeping status
global.sleeplog.log(Math.floor(Date.now()));
// reset cached values
global.sleeplog.ess_values = [];
global.sleeplog.nomocount = 0;
global.sleeplog.firstnomodate = undefined;
global.sleeplog.resting = undefined;
global.sleeplog.status = 0;
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
// reset always used cached values
sleeplog.resting = undefined;
sleeplog.status = undefined;
sleeplog.ess_values = [];
sleeplog.nomocount = 0;
sleeplog.firstnomodate = undefined;
},
// define restart function (also use for initial starting)
start: function() {
// add acceleration listener
Bangle.on('accel', global.sleeplog.accel);
// exit on missing global object
if (!global.sleeplog) return;
// add health listener if defined and
if (sleeplog.health) Bangle.on('health', sleeplog.health);
// add acceleration listener if defined and set status to unknown
if (sleeplog.accel) Bangle.on('accel', sleeplog.accel);
// add kill listener
E.on('kill', global.sleeplog.stop);
},
E.on('kill', () => sleeplog.stop());
// read log since 5min ago and restore status to last known state or unknown
sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1]
// update resting according to status
sleeplog.resting = sleeplog.status % 2;
// write restored status to log
require("sleeplog").writeLog(0, [Math.floor(Date.now()), sleeplog.status]);
}
});
// check for power saving mode
if (sleeplog.powersaving) {
// power saving mode using build in movement detection
// delete unused settings
["winwidth", "nomothresh", "sleepthresh"].forEach(property => delete sleeplog[property]);
// add cached values and functions to global object
sleeplog = Object.assign(sleeplog, {
// define health listener function
health: function(data) {
// set global object and check for existence
var gObj = global.sleeplog;
if (!gObj) return;
// calculate timestamp for this measurement
var timestamp = Math.floor(Date.now() - 6E5);
// check for non-movement according to the threshold
if (data.movement <= gObj.maxmove) {
// check resting state
if (gObj.resting !== true) {
// change resting state
gObj.resting = true;
// set status to sleeping or worn
gObj.status = E.getTemperature() > gObj.tempthresh ? 3 : 1;
// write status to log,
require("sleeplog").writeLog(0, [timestamp, gObj.status, E.getTemperature()]);
}
} else {
// check resting state
if (gObj.resting !== false) {
// change resting state, set status and write status to log
gObj.resting = false;
gObj.status = 2;
require("sleeplog").writeLog(0, [timestamp, 2]);
}
}
}
});
} else {
// full ESS calculation
// add cached values and functions to global object
sleeplog = Object.assign(sleeplog, {
// set cached values
ess_values: [],
nomocount: 0,
firstnomodate: undefined,
// define acceleration listener function
accel: function(xyz) {
// save acceleration magnitude and start calculation on enough saved data
if (global.sleeplog && sleeplog.ess_values.push(xyz.mag) >= sleeplog.winwidth) sleeplog.calc();
},
// define calculator function
calc: function() {
// exit on wrong this
if (this.enabled === undefined) return;
// calculate standard deviation over
var mean = this.ess_values.reduce((prev, cur) => cur + prev) / this.winwidth;
var stddev = Math.sqrt(this.ess_values.map(val => Math.pow(val - mean, 2)).reduce((prev, cur) => prev + cur) / this.winwidth);
// reset saved acceleration data
this.ess_values = [];
// check for non-movement according to the threshold
if (stddev < this.nomothresh) {
// increment non-movement sections count, set date of first non-movement
if (++this.nomocount == 1) this.firstnomodate = Math.floor(Date.now());
// check resting state and non-movement count against threshold
if (this.resting !== true && this.nomocount >= this.sleepthresh) {
// change resting state
this.resting = true;
// set status to sleeping or worn
this.status = E.getTemperature() > this.tempthresh ? 3 : 1;
// write status to log, with first no movement timestamp
require("sleeplog").writeLog(0, [this.firstnomodate, this.status, E.getTemperature()]);
}
} else {
// reset non-movement sections count
this.nomocount = 0;
// check resting state
if (this.resting !== false) {
// change resting state and set status
this.resting = false;
this.status = 2;
// write status to log
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 2]);
}
}
}
});
}
// initial starting
global.sleeplog.start();
}

View File

@ -1,17 +1,12 @@
exports = {
// define en-/disable function
setEnabled: function(enable, logfile) {
// define en-/disable function, restarts the service to make changes take effect
setEnabled: function(enable, logfile, powersaving) {
// check if sleeplog is available
if (typeof global.sleeplog !== "object") return;
// set default logfile
logfile = logfile.endsWith(".log") ? logfile :
logfile === false ? undefined :
"sleeplog.log";
// check if status needs to be changed
if (enable === global.sleeplog.enabled ||
logfile === global.sleeplog.logfile) return false;
logfile = (typeof logfile === "string" && logfile.endsWith(".log")) ? logfile :
logfile === false ? undefined : "sleeplog.log";
// stop if enabled
if (global.sleeplog.enabled) global.sleeplog.stop();
@ -23,7 +18,8 @@ exports = {
// change enabled value in settings
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
enabled: enable,
logfile: logfile
logfile: logfile,
powersaving: powersaving || false
}));
// force changes to take effect by executing the boot script
@ -42,32 +38,77 @@ exports = {
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// - float // internal temperature
// - string // additional information
readLog: function(since, until) {
// set logfile
var logfile = (global.sleeplog || {}).logfile || "sleeplog.log";
readLog: function(logfile, since, until) {
// check/set logfile
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
(global.sleeplog || {}).logfile || "sleeplog.log";
// check if since is in the future
if (since > Date()) return [];
// read log json to array
var log = JSON.parse(atob(require("Storage").read(logfile)));
// read logfile
var log = require("Storage").read(logfile);
// return empty log
if (!log) return [];
// decode data if needed
if (log[0] !== "[") log = atob(log);
// do a simple check before parsing
if (!log.startsWith("[[") || !log.endsWith("]]")) return [];
log = JSON.parse(log) || [];
// search for latest entry befor since
since = (log.find(element => element[0] <= since) || [0])[0];
// filter selected time period
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
// check if filtering is needed
if (since || until) {
// search for latest entry befor since
if (since) since = (log.find(element => element[0] <= since) || [0])[0];
// filter selected time period
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
}
// output log
return log;
},
// define write log function, append or replace log depending on input
// append input if array length >1 and element[0] >9E11
// replace log with input if at least one entry like above is inside another array
writeLog: function(logfile, input) {
// check/set logfile
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
(global.sleeplog || {}).logfile || "sleeplog.log";
// check if input is an array
if (typeof input !== "object" || typeof input.length !== "number") return;
// check for entry plausibility
if (input.length > 1 && input[0] * 1 > 9E11) {
// read log
var log = this.readLog(logfile);
// remove last state if it was unknown and less then 5min ago
if (log.length > 0 && log[0][1] === 0 &&
Math.floor(Date.now()) - log[0][0] < 3E5) log.shift();
// add entry at the first position if it has changed
if (log.length === 0 || input.some((e, index) => index > 0 && input[index] !== log[0][index])) log.unshift(input);
// map log as input
input = log;
}
// simple check for log plausibility
if (input[0].length > 1 && input[0][0] * 1 > 9E11) {
// write log to storage
require("Storage").write(logfile, btoa(JSON.stringify(input)));
return true;
}
},
// define log to humanreadable string function
// sorting: latest last, format:
// "{substring of ISO date} - {status} for {duration}min\n..."
getReadableLog: function(printLog, since, until) {
getReadableLog: function(printLog, since, until, logfile) {
// read log and check
var log = this.readLog(since, until);
var log = this.readLog(logfile, since, until);
if (!log.length) return;
// reverse array to set last timestamp to the end
log.reverse();
@ -81,8 +122,11 @@ exports = {
logString[index] = "" +
Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
statusText[element[1]] +
(index === log.length - 1 ? "" : " for " + Math.round((log[index + 1][0] - element[0]) / 60000) + "min") +
(element[2] ? " | Temp: " + element[2] + "°C" : "") +
(index === log.length - 1 ?
element.length < 3 ? "" : " ".repeat(12) :
" for " + ("" + Math.round((log[index + 1][0] - element[0]) / 60000)).padStart(4) + "min"
) +
(element[2] ? " | Temp: " + ("" + element[2]).padEnd(5) + "°C" : "") +
(element[3] ? " | " + element[3] : "");
});
logString = logString.join("\n");
@ -100,11 +144,9 @@ exports = {
// define function to eliminate some errors inside the log
restoreLog: function(logfile) {
// define storage
var storage = require("Storage");
// read log json to array
var log = JSON.parse(atob(storage.read(logfile)));
// read log and check
var log = this.readLog(logfile);
if (!log.length) return;
// define output variable to show number of changes
var output = log.length;
@ -112,8 +154,8 @@ exports = {
// remove non decremental entries
log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
// write log to storage
storage.write(logfile, btoa(JSON.stringify(log)));
// write log
this.writeLog(logfile, log);
// return difference in length
return output - log.length;
@ -121,11 +163,12 @@ exports = {
// define function to reinterpret worn status based on given temperature threshold
reinterpretTemp: function(logfile, tempthresh) {
// define storage
var storage = require("Storage");
// read log and check
var log = this.readLog(logfile);
if (!log.length) return;
// read log json to array
var log = JSON.parse(atob(storage.read(logfile)));
// set default tempthresh
tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
// define output variable to show number of changes
var output = 0;
@ -140,8 +183,8 @@ exports = {
return element;
});
// write log to storage
storage.write(logfile, btoa(JSON.stringify(log)));
// write log
this.writeLog(logfile, log);
// return output
return output;

View File

@ -2,8 +2,8 @@
"id":"sleeplog",
"name":"Sleep Log",
"shortName": "SleepLog",
"version": "0.02",
"description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS).",
"version": "0.03",
"description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.",
"icon": "app.png",
"type": "app",
"tags": "tool,boot",

View File

@ -8,6 +8,8 @@
maxawake: 36E5, // 60min in ms
minconsec: 18E5, // 30min in ms
tempthresh: 27, // every temperature above ist registered as worn
powersaving: false, // disables ESS and uses build in movement detection
maxmove: 100, // movement threshold on power saving mode
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
@ -32,14 +34,20 @@
return value > max ? min : value < min ? max : value;
}
// define function to change values that need a restart of the service
function changeRestart() {
require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving);
}
// calculate sleepthresh factor
var stFactor = settings.winwidth / 12.5 / 60;
// show main menu
function showMain() {
var mainMenu = E.showMenu({
function showMain(selected) {
var mainMenu = {
"": {
title: "Sleep Log"
title: "Sleep Log",
selected: selected
},
"< Exit": () => load(),
"< Back": () => back(),
@ -78,6 +86,23 @@
writeSetting("tempthresh", v);
}
},
"PowerSaving": {
value: settings.powersaving,
format: v => v ? "on" : "off",
onchange: function(v) {
settings.powersaving = v;
changeRestart();
showMain(7);
}
},
"MaxMove": {
value: settings.maxmove,
step: 1,
onchange: function(v) {
this.value = v = circulate(50, 200, v);
writeSetting("maxmove", v);
}
},
"NoMoThresh": {
value: settings.nomothresh,
step: 0.001,
@ -100,17 +125,27 @@
value: settings.enabled,
format: v => v ? "on" : "off",
onchange: function(v) {
writeSetting("enabled", v);
settings.enabled = v;
changeRestart();
}
},
"Logfile ": {
value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false,
format: v => v === true ? "default" : v ? "custom" : "off",
onchange: function(v) {
if (v !== "custom") writeSetting("logfile", v ? "sleeplog.log" : undefined);
if (v !== "custom") {
settings.logfile = v ? "sleeplog.log" : false;
changeRestart();
}
}
},
});
}
};
// check power saving mode to delete unused entries
(settings.powersaving ? ["NoMoThresh", "MinDuration"] : ["MaxMove"]).forEach(property => delete mainMenu[property]);
var menu = E.showMenu(mainMenu);
// workaround to display changed entries correct
// https://github.com/espruino/Espruino/issues/2149
if (selected) setTimeout(m => m.draw(), 1, menu);
}
// draw main menu

View File

@ -34,3 +34,7 @@ clothing catches it.
is 160*160 and is larger than required. The reason for this is that
I plan to use this watch face with others in a multiclock format
and want to be able to reuse the arrayBuffer with other clocks.
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

@ -15,3 +15,8 @@ Reminiscent of a Telegram or Turing machine tape.
**Swipe Left** - move forward to the next app icon
**Swipe Right** - move backwards (to the left) to the previous app
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

@ -59,3 +59,8 @@ on the second line of the watch.
## Future Enhancements
* Ability to turn on the heart rate monitor and display the rate on the info line
* Maybe a simple stopwatch capability
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

@ -174,3 +174,7 @@ The majority of the code in this application is a merge of
[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS
Navigation and Compass Navigation Applications.
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/)

1
apps/widcal/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First version

13
apps/widcal/metadata.json Normal file
View File

@ -0,0 +1,13 @@
{
"id": "widcal",
"name": "Calendar Widget",
"version": "0.01",
"description": "Widget with the current date",
"icon": "widget.png",
"type": "widget",
"tags": "widget,calendar",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"widcal.wid.js","url":"widget.js"}
]
}

30
apps/widcal/widget.js Normal file
View File

@ -0,0 +1,30 @@
(function() {
WIDGETS["cal"] = {
area: "tl", width: 22, draw: function() {
const x = this.x, y = this.y,
x2 = x+21, y2 = y+23,
date = new Date(),
month = require("locale").month(date, true),
day = date.getDate();
g.reset().setFontAlign(0, 0) // center all text
// header
.setBgColor("#f00").setColor("#fff")
.clearRect(x, y, x2, y+8).setFont("4x6").drawString(month, (x+x2)/2+1, y+5)
// date
.setBgColor("#fff").setColor("#000")
.clearRect(x, y+9, x2, y2).setFont("Vector:16").drawString(day, (x+x2)/2+2, y+17);
if (!g.theme.dark) {
// black border around date for light themes
g.setColor("#000").drawPoly([
x, y+9,
x, y2,
x2, y2,
x2, y+9
]);
}
// redraw when date changes
setTimeout(()=>WIDGETS["cal"].draw(), (86401 - Math.floor(date/1000) % 86400)*1000);
}
};
})();

BIN
apps/widcal/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

View File

@ -9,3 +9,8 @@ aware.
- Uses Bangle.isCompassOn(), requires firmware v2.08.167 or later
- Shows in grey when the compass is off
- Shows in amber when the compass is on
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

@ -8,3 +8,7 @@ it is useful to know if it has been switched on or not.
- Uses Bangle.isGPSOn()
- Shows in grey when the GPS is off
- Shows in amber when the GPS is on
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/)

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