Merge branch 'master' into master
|
@ -1,2 +1,3 @@
|
||||||
0.01: Display pressure as number and hand
|
0.01: Display pressure as number and hand
|
||||||
0.02: Use theme color
|
0.02: Use theme color
|
||||||
|
0.03: workaround for some firmwares that return 'undefined' for first call to barometer
|
||||||
|
|
|
@ -110,9 +110,13 @@ drawScaleLabels();
|
||||||
drawIcons();
|
drawIcons();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Bangle.getPressure().then(data => {
|
function baroHandler(data) {
|
||||||
drawHand(Math.round(data.pressure));
|
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) {
|
} catch(e) {
|
||||||
print(e.message);
|
print(e.message);
|
||||||
print("barometer not supporter, show a demo value");
|
print("barometer not supporter, show a demo value");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "barometer",
|
{ "id": "barometer",
|
||||||
"name": "Barometer",
|
"name": "Barometer",
|
||||||
"shortName":"Barometer",
|
"shortName":"Barometer",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "A simple barometer that displays the current air pressure",
|
"description": "A simple barometer that displays the current air pressure",
|
||||||
"icon": "barometer.png",
|
"icon": "barometer.png",
|
||||||
"tags": "tool,outdoors",
|
"tags": "tool,outdoors",
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Bowser Watchface
|
||||||
|
|
||||||
|
Show your evil character.
|
||||||
|
With style!
|
||||||
|
|
||||||
|
Bowser jumps once every minute to advance the clock. hehe
|
|
@ -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="))
|
|
@ -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();
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
|
@ -17,3 +17,6 @@
|
||||||
0.06: Fix bug if no request waiting time is set
|
0.06: Fix bug if no request waiting time is set
|
||||||
Fix bug if no connection data was cached
|
Fix bug if no connection data was cached
|
||||||
Fix error during disconnect
|
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
|
||||||
|
|
|
@ -29,6 +29,7 @@ Heart Rate Service (`180D`) and characteristic (`2A37`).
|
||||||
So far it has been tested on:
|
So far it has been tested on:
|
||||||
|
|
||||||
* CooSpo Bluetooth Heart Rate Monitor
|
* CooSpo Bluetooth Heart Rate Monitor
|
||||||
|
* Wahoo TICKR X 2
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
|
|
||||||
|
@ -36,7 +37,6 @@ This replaces `Bangle.setHRMPower` with its own implementation.
|
||||||
|
|
||||||
## TODO
|
## 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?
|
* A widget to show connection state?
|
||||||
* Specify a specific device by address?
|
* Specify a specific device by address?
|
||||||
|
|
||||||
|
|
|
@ -548,9 +548,7 @@
|
||||||
E.on("kill", ()=>{
|
E.on("kill", ()=>{
|
||||||
if (gatt && gatt.connected){
|
if (gatt && gatt.connected){
|
||||||
log("Got killed, trying to disconnect");
|
log("Got killed, trying to disconnect");
|
||||||
var promise = gatt.disconnect();
|
var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||||
promise.then(()=>log("Disconnected on kill"));
|
|
||||||
promise.catch((e)=>log("Error during disconnnect on kill", e));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"mode": 1,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"replace": true,
|
"replace": true,
|
||||||
"debuglog": false,
|
"debuglog": false,
|
||||||
|
@ -6,6 +7,12 @@
|
||||||
"allowFallback": true,
|
"allowFallback": true,
|
||||||
"warnDisconnect": false,
|
"warnDisconnect": false,
|
||||||
"fallbackTimeout": 10,
|
"fallbackTimeout": 10,
|
||||||
|
"custom_replace": false,
|
||||||
|
"custom_debuglog": false,
|
||||||
|
"custom_startWithHrm": false,
|
||||||
|
"custom_allowFallback": false,
|
||||||
|
"custom_warnDisconnect": false,
|
||||||
|
"custom_fallbackTimeout": 10,
|
||||||
"gracePeriodNotification": 0,
|
"gracePeriodNotification": 0,
|
||||||
"gracePeriodConnect": 0,
|
"gracePeriodConnect": 0,
|
||||||
"gracePeriodService": 0,
|
"gracePeriodService": 0,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "bthrm",
|
"id": "bthrm",
|
||||||
"name": "Bluetooth Heart Rate Monitor",
|
"name": "Bluetooth Heart Rate Monitor",
|
||||||
"shortName": "BT HRM",
|
"shortName": "BT HRM",
|
||||||
"version": "0.06",
|
"version": "0.07",
|
||||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"bthrm.app.js","url":"bthrm.js"},
|
{"name":"bthrm.app.js","url":"bthrm.js"},
|
||||||
{"name":"bthrm.recorder.js","url":"recorder.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.img","url":"app-icon.js","evaluate":true},
|
||||||
{"name":"bthrm.settings.js","url":"settings.js"},
|
{"name":"bthrm.settings.js","url":"settings.js"},
|
||||||
{"name":"bthrm.default.json","url":"default.json"}
|
{"name":"bthrm.default.json","url":"default.json"}
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
Bangle.removeListener('BTHRM', onHRM);
|
Bangle.removeListener('BTHRM', onHRM);
|
||||||
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
|
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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,182 +20,147 @@
|
||||||
var mainmenu = {
|
var mainmenu = {
|
||||||
'': { 'title': 'Bluetooth HRM' },
|
'': { 'title': 'Bluetooth HRM' },
|
||||||
'< Back': back,
|
'< Back': back,
|
||||||
'Use BT HRM': {
|
'Mode': {
|
||||||
value: !!settings.enabled,
|
value: 0 | settings.mode,
|
||||||
format: v => settings.enabled ? "On" : "Off",
|
min: 0,
|
||||||
|
max: 3,
|
||||||
|
format: v => ["Off", "Default", "Both", "Custom"][v],
|
||||||
onchange: 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': {
|
'Custom Mode': function() { E.showMenu(submenu_custom); },
|
||||||
value: !!settings.replace,
|
'Debug': function() { E.showMenu(submenu_debug); }
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
function readSettings(){
|
var submenu_debug = {
|
||||||
settings = Object.assign(
|
'' : { title: "Debug"},
|
||||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
'< Back': function() { E.showMenu(mainmenu); },
|
||||||
require('Storage').readJSON(FILE, true) || {}
|
'Alert on disconnect': {
|
||||||
);
|
value: !!settings.warnDisconnect,
|
||||||
}
|
format: v => settings.warnDisconnect ? "On" : "Off",
|
||||||
|
|
||||||
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",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("enabled",v);
|
writeSettings("warnDisconnect",v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Replace HRM': {
|
'Debug log': {
|
||||||
value: !!settings.replace,
|
value: !!settings.debuglog,
|
||||||
format: v => settings.replace ? "On" : "Off",
|
format: v => settings.debuglog ? "On" : "Off",
|
||||||
onchange: v => {
|
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': {
|
'Start w. HRM': {
|
||||||
value: !!settings.startWithHrm,
|
value: !!settings.custom_startWithHrm,
|
||||||
format: v => settings.startWithHrm ? "On" : "Off",
|
format: v => settings.custom_startWithHrm ? "On" : "Off",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("startWithHrm",v);
|
writeSettings("custom_startWithHrm",v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'HRM Fallback': {
|
'HRM Fallback': {
|
||||||
value: !!settings.allowFallback,
|
value: !!settings.custom_allowFallback,
|
||||||
format: v => settings.allowFallback ? "On" : "Off",
|
format: v => settings.custom_allowFallback ? "On" : "Off",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("allowFallback",v);
|
writeSettings("custom_allowFallback",v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Fallback Timeout': {
|
'Fallback Timeout': {
|
||||||
value: settings.fallbackTimeout,
|
value: settings.custom_fallbackTimeout,
|
||||||
min: 5,
|
min: 5,
|
||||||
max: 60,
|
max: 60,
|
||||||
step: 5,
|
step: 5,
|
||||||
format: v=>v+"s",
|
format: v=>v+"s",
|
||||||
onchange: v => {
|
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 => {
|
onchange: v => {
|
||||||
writeSettings("warnDisconnect",v);
|
writeSettings("gracePeriodRequest",v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Debug log': {
|
'Connect': {
|
||||||
value: !!settings.debuglog,
|
value: settings.gracePeriodConnect,
|
||||||
format: v => settings.debuglog ? "On" : "Off",
|
min: 0,
|
||||||
|
max: 3000,
|
||||||
|
step: 100,
|
||||||
|
format: v=>v+"ms",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("debuglog",v);
|
writeSettings("gracePeriodConnect",v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Grace periods >': function() { E.showMenu(submenu); }
|
'Notification': {
|
||||||
};
|
value: settings.gracePeriodNotification,
|
||||||
|
min: 0,
|
||||||
var submenu = {
|
max: 3000,
|
||||||
'' : { title: "Grace periods"},
|
step: 100,
|
||||||
'< Back': function() { E.showMenu(mainmenu); },
|
format: v=>v+"ms",
|
||||||
'Request': {
|
onchange: v => {
|
||||||
value: settings.gracePeriodRequest,
|
writeSettings("gracePeriodNotification",v);
|
||||||
min: 0,
|
}
|
||||||
max: 3000,
|
},
|
||||||
step: 100,
|
'Service': {
|
||||||
format: v=>v+"ms",
|
value: settings.gracePeriodService,
|
||||||
onchange: v => {
|
min: 0,
|
||||||
writeSettings("gracePeriodRequest",v);
|
max: 3000,
|
||||||
}
|
step: 100,
|
||||||
},
|
format: v=>v+"ms",
|
||||||
'Connect': {
|
onchange: v => {
|
||||||
value: settings.gracePeriodConnect,
|
writeSettings("gracePeriodService",v);
|
||||||
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); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var submenu = {
|
var submenu = {
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
0.21: Fixed settings menu, four more fonts
|
0.21: Fixed settings menu, four more fonts
|
||||||
0.22: Changed timing code, original "Nunito" Font is back!
|
0.22: Changed timing code, original "Nunito" Font is back!
|
||||||
0.23: Customizer! Unused fonts no longer take up precious memory.
|
0.23: Customizer! Unused fonts no longer take up precious memory.
|
||||||
|
0.24: Added previews to the customizer.
|
||||||
|
|
|
@ -1,70 +1,74 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<h3> Select Fonts to upload:</h3>
|
||||||
<form>
|
<form>
|
||||||
<input type="checkbox" id="BarlowCond">
|
<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">
|
<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">
|
<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">
|
<input type="checkbox" id="DinAlternate">
|
||||||
<label for="DinAlternate">Din Alternate</label><br>
|
<label for="DinAlternate"><img src="fonts/DinAlternate-p1.png"> <img src="fonts/DinAlternate-p2.png"></label><br>
|
||||||
<input type="checkbox" id="Dosis">
|
|
||||||
<label for="Dosis">Dosis</label><br>
|
|
||||||
<input type="checkbox" id="Impact">
|
<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">
|
<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">
|
<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">
|
<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">
|
<input type="checkbox" id="Quicksand">
|
||||||
<label for="Quicksand">Quicksand</label><br>
|
<label for="Quicksand"><img src="fonts/Quicksand-p1.png"> <img src="fonts/Quicksand-p2.png"></label><br>
|
||||||
<input type="checkbox" id="SairaCond">
|
|
||||||
<label for="SairaCond">Saira Condensed</label><br>
|
|
||||||
<input type="checkbox" id="SairaEC">
|
<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">
|
<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">
|
<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">
|
<input type="checkbox" id="YuseiMagic">
|
||||||
<label for="YuseiMagic">Yusei Magic</label><br>
|
<label for="YuseiMagic"><img src="fonts/YuseiMagic-p1.png"> <img src="fonts/YuseiMagic-p2.png"></label><br>
|
||||||
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
|
<p><button id="upload" class="btn btn-primary">Upload selected Fonts</button></p>
|
||||||
<script src="../../core/lib/customize.js"></script>
|
<script src="../../core/lib/customize.js"></script>
|
||||||
<script>
|
<script>
|
||||||
FontList = ["BarlowCond", "BebasNeue", "Dekko", "DinAlternate", "Dosis",
|
FontList = ["BarlowCond", "BebasNeue", "Dekko", "DinAlternate",
|
||||||
"Impact", "Nunito", "OpenSansEC", "Phosphate", "Quicksand", "SairaCond", "SairaEC",
|
"Impact", "Nunito", "OpenSansEC", "Phosphate", "Quicksand", "SairaEC",
|
||||||
"Yumaro", "YuseiMagic"]
|
"Yumaro", "YuseiMagic"]
|
||||||
// When the 'upload' button is clicked...
|
// When the 'upload' button is clicked...
|
||||||
document.getElementById("upload").addEventListener("click", function() {
|
document.getElementById("upload").addEventListener("click", function() {
|
||||||
var n=0;
|
var n=0;
|
||||||
var fonts = [];
|
var fonts = [];
|
||||||
for (fontName of FontList) {
|
for (fontName of FontList) {
|
||||||
if (document.getElementById(fontName).checked==true) {
|
if (document.getElementById(fontName).checked==true) {
|
||||||
var f = new Object();
|
var f = new Object();
|
||||||
f.name="contourclock-"+n+".json";
|
fonts.push({
|
||||||
f.url="font-"+fontName+".json";
|
name:"contourclock-"+n+".json",
|
||||||
//fonts.push({name:"contourclock-"+n+".json", url:"font-"+fontName+".json"});
|
url:"font-"+fontName+".json"
|
||||||
fonts.push(f);
|
});
|
||||||
//console.log("contourclock-"+n+".json <- font-"+fontName+".json");
|
n++;
|
||||||
n++;
|
}
|
||||||
}
|
}
|
||||||
}
|
if (n>0)
|
||||||
//console.log(fonts[0]);
|
sendCustomizedApp({storage:fonts});
|
||||||
sendCustomizedApp(storage=fonts);
|
else
|
||||||
/*sendCustomizedApp({
|
alert("Please select at least one Font!");
|
||||||
storage:[
|
|
||||||
{name:"myapp.app.js", url:"app.js", content:app},
|
|
||||||
]
|
|
||||||
});*/
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
After Width: | Height: | Size: 726 B |
After Width: | Height: | Size: 662 B |
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 585 B |
After Width: | Height: | Size: 763 B |
After Width: | Height: | Size: 660 B |
After Width: | Height: | Size: 633 B |
After Width: | Height: | Size: 551 B |
After Width: | Height: | Size: 670 B |
After Width: | Height: | Size: 600 B |
After Width: | Height: | Size: 753 B |
After Width: | Height: | Size: 690 B |
After Width: | Height: | Size: 845 B |
After Width: | Height: | Size: 759 B |
After Width: | Height: | Size: 659 B |
After Width: | Height: | Size: 572 B |
After Width: | Height: | Size: 843 B |
After Width: | Height: | Size: 749 B |
After Width: | Height: | Size: 609 B |
After Width: | Height: | Size: 519 B |
After Width: | Height: | Size: 611 B |
After Width: | Height: | Size: 521 B |
After Width: | Height: | Size: 774 B |
After Width: | Height: | Size: 670 B |
After Width: | Height: | Size: 838 B |
After Width: | Height: | Size: 717 B |
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
{ "id": "contourclock",
|
{ "id": "contourclock",
|
||||||
"name": "Contour Clock",
|
"name": "Contour Clock",
|
||||||
"shortName" : "Contour Clock",
|
"shortName" : "Contour Clock",
|
||||||
"version":"0.23",
|
"version":"0.24",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"description": "A Minimalist clockface with large Digits. Now with more fonts!",
|
"description": "A Minimalist clockface with large Digits. Now with more fonts!",
|
||||||
"screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}],
|
"screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}],
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"custom": "custom.html",
|
"custom": "custom.html",
|
||||||
"allow_emulator":true,
|
|
||||||
"supports" : ["BANGLEJS2"],
|
"supports" : ["BANGLEJS2"],
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"storage": [
|
"storage": [
|
||||||
|
|
|
@ -14,4 +14,5 @@
|
||||||
0.14: Added altitude as an option to display.
|
0.14: Added altitude as an option to display.
|
||||||
0.15: Using wpedom to count steps.
|
0.15: Using wpedom to count steps.
|
||||||
0.16: Improved stability. Wind can now be shown.
|
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.
|
|
@ -11,7 +11,7 @@ with Gadgetbride and the weather app must be installed.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* LCARS Style watch face.
|
* 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.
|
* Tab on left/right to switch between different screens.
|
||||||
* Cusomizable data that is shown on screen 1 (steps, weather etc.)
|
* Cusomizable data that is shown on screen 1 (steps, weather etc.)
|
||||||
* Shows random and real images of planets.
|
* Shows random and real images of planets.
|
||||||
|
@ -33,7 +33,7 @@ with Gadgetbride and the weather app must be installed.
|
||||||
## Multiple screens support
|
## Multiple screens support
|
||||||
Access different screens via tap on the left/ right side of the screen
|
Access different screens via tap on the left/ right side of the screen
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 772 B |
After Width: | Height: | Size: 769 B |
|
@ -7,6 +7,7 @@ let settings = {
|
||||||
dataRow2: "Temp",
|
dataRow2: "Temp",
|
||||||
dataRow3: "Battery",
|
dataRow3: "Battery",
|
||||||
speed: "kph",
|
speed: "kph",
|
||||||
|
fullscreen: false,
|
||||||
};
|
};
|
||||||
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
||||||
for (const key in saved_settings) {
|
for (const key in saved_settings) {
|
||||||
|
@ -30,23 +31,39 @@ let lcarsViewPos = 0;
|
||||||
// let hrmValue = 0;
|
// let hrmValue = 0;
|
||||||
var plotMonth = false;
|
var plotMonth = false;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Requirements and globals
|
* Requirements and globals
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var bgLeft = {
|
var bgLeftFullscreen = {
|
||||||
width : 27, height : 176, bpp : 3,
|
width : 27, height : 176, bpp : 3,
|
||||||
transparent : 0,
|
transparent : 0,
|
||||||
buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAFCh/eX5Q/KAwdCAGVbtu27YCCoAJBkuWrNlAQRGCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A=="))
|
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,
|
width : 27, height : 176, bpp : 3,
|
||||||
transparent : 0,
|
transparent : 0,
|
||||||
buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAnUP7y/KH4yGeVYAJrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgAA="))
|
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 = {
|
var iconEarth = {
|
||||||
width : 50, height : 50, bpp : 3,
|
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="))
|
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(){
|
function drawInfo(){
|
||||||
if(lcarsViewPos != 0){
|
if(lcarsViewPos != 0 || !settings.fullscreen){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,9 +293,10 @@ function drawState(){
|
||||||
|
|
||||||
function drawPosition0(){
|
function drawPosition0(){
|
||||||
// Draw background image
|
// Draw background image
|
||||||
g.drawImage(bgLeft, 0, 0);
|
var offset = settings.fullscreen ? 0 : 24;
|
||||||
drawHorizontalBgLine(cBlue, 25, 120, 0, 4);
|
g.drawImage(bgLeft, 0, offset);
|
||||||
drawHorizontalBgLine(cBlue, 130, 176, 0, 4);
|
drawHorizontalBgLine(cBlue, 25, 120, offset, 4);
|
||||||
|
drawHorizontalBgLine(cBlue, 130, 176, offset, 4);
|
||||||
drawHorizontalBgLine(cPurple, 20, 70, 80, 4);
|
drawHorizontalBgLine(cPurple, 20, 70, 80, 4);
|
||||||
drawHorizontalBgLine(cPurple, 80, 176, 80, 4);
|
drawHorizontalBgLine(cPurple, 80, 176, 80, 4);
|
||||||
drawHorizontalBgLine(cOrange, 35, 110, 87, 4);
|
drawHorizontalBgLine(cOrange, 35, 110, 87, 4);
|
||||||
|
@ -304,15 +322,26 @@ function drawPosition0(){
|
||||||
var currentDate = new Date();
|
var currentDate = new Date();
|
||||||
var timeStr = locale.time(currentDate,1);
|
var timeStr = locale.time(currentDate,1);
|
||||||
g.setFontAntonioLarge();
|
g.setFontAntonioLarge();
|
||||||
g.drawString(timeStr, 27, 10);
|
if(settings.fullscreen){
|
||||||
|
g.drawString(timeStr, 27, 10);
|
||||||
|
} else {
|
||||||
|
g.drawString(timeStr, 27, 33);
|
||||||
|
}
|
||||||
|
|
||||||
// Write date
|
// Write date
|
||||||
g.setColor(cWhite);
|
g.setColor(cWhite);
|
||||||
g.setFontAntonioMedium();
|
g.setFontAntonioMedium();
|
||||||
var dayStr = locale.dow(currentDate, true).toUpperCase();
|
if(settings.fullscreen){
|
||||||
dayStr += " " + currentDate.getDate();
|
var dayStr = locale.dow(currentDate, true).toUpperCase();
|
||||||
dayStr += " " + locale.month(currentDate, 1).toUpperCase();
|
dayStr += " " + currentDate.getDate();
|
||||||
g.drawString(dayStr, 30, 56);
|
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
|
// Draw data
|
||||||
g.setFontAlign(-1, -1, 0);
|
g.setFontAlign(-1, -1, 0);
|
||||||
|
@ -327,8 +356,11 @@ function drawPosition0(){
|
||||||
|
|
||||||
function drawPosition1(){
|
function drawPosition1(){
|
||||||
// Draw background image
|
// Draw background image
|
||||||
g.drawImage(bgRight, 149, 0);
|
var offset = settings.fullscreen ? 0 : 24;
|
||||||
drawHorizontalBgLine(cBlue, 0, 140, 0, 4);
|
g.drawImage(bgRight, 149, offset);
|
||||||
|
if(settings.fullscreen){
|
||||||
|
drawHorizontalBgLine(cBlue, 0, 140, offset, 4);
|
||||||
|
}
|
||||||
drawHorizontalBgLine(cPurple, 0, 80, 80, 4);
|
drawHorizontalBgLine(cPurple, 0, 80, 80, 4);
|
||||||
drawHorizontalBgLine(cPurple, 90, 150, 80, 4);
|
drawHorizontalBgLine(cPurple, 90, 150, 80, 4);
|
||||||
drawHorizontalBgLine(cOrange, 0, 50, 87, 4);
|
drawHorizontalBgLine(cOrange, 0, 50, 87, 4);
|
||||||
|
@ -388,8 +420,13 @@ function drawPosition1(){
|
||||||
g.setFontAlign(1, 1, 0);
|
g.setFontAlign(1, 1, 0);
|
||||||
g.setFontAntonioMedium();
|
g.setFontAntonioMedium();
|
||||||
g.setColor(cWhite);
|
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
|
// Plot day
|
||||||
} else {
|
} else {
|
||||||
|
@ -429,8 +466,13 @@ function drawPosition1(){
|
||||||
g.setFontAlign(1, 1, 0);
|
g.setFontAlign(1, 1, 0);
|
||||||
g.setFontAntonioMedium();
|
g.setFontAntonioMedium();
|
||||||
g.setColor(cWhite);
|
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) {
|
} else if (lcarsViewPos == 1) {
|
||||||
drawPosition1();
|
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 + "%";
|
weather.hum = weather.hum + "%";
|
||||||
|
|
||||||
// Wind
|
// Wind
|
||||||
var speedFactor = settings.speed == "kph" ? 1.60934 : 1.0;
|
const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
||||||
weather.wind = Math.round(weather.wind * speedFactor);
|
var speedFactor = settings.speed == "kph" ? 1.0 : 1.0 / 1.60934;
|
||||||
|
weather.wind = Math.round(wind[1] * speedFactor);
|
||||||
|
|
||||||
return weather
|
return weather
|
||||||
}
|
}
|
||||||
|
@ -652,16 +702,7 @@ Bangle.on('touch', function(btn, e){
|
||||||
// Show launcher when middle button pressed
|
// Show launcher when middle button pressed
|
||||||
Bangle.setUI("clock");
|
Bangle.setUI("clock");
|
||||||
Bangle.loadWidgets();
|
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
|
// Clear the screen once, at startup and draw clock
|
||||||
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
|
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
|
||||||
draw();
|
draw();
|
||||||
|
|
||||||
// After drawing the watch face, we can draw the widgets
|
|
||||||
// Bangle.drawWidgets();
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
dataRow2: "Steps",
|
dataRow2: "Steps",
|
||||||
dataRow3: "Temp",
|
dataRow3: "Temp",
|
||||||
speed: "kph",
|
speed: "kph",
|
||||||
|
fullscreen: false,
|
||||||
};
|
};
|
||||||
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
||||||
for (const key in saved_settings) {
|
for (const key in saved_settings) {
|
||||||
|
@ -52,6 +53,14 @@
|
||||||
save();
|
save();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'Full Screen': {
|
||||||
|
value: settings.fullscreen,
|
||||||
|
format: () => (settings.fullscreen ? 'Yes' : 'No'),
|
||||||
|
onchange: () => {
|
||||||
|
settings.fullscreen = !settings.fullscreen;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
'Speed': {
|
'Speed': {
|
||||||
value: 0 | speedOptions.indexOf(settings.speed),
|
value: 0 | speedOptions.indexOf(settings.speed),
|
||||||
min: 0, max: 1,
|
min: 0, max: 1,
|
||||||
|
|
|
@ -3,13 +3,15 @@
|
||||||
"name": "LCARS Clock",
|
"name": "LCARS Clock",
|
||||||
"shortName":"LCARS",
|
"shortName":"LCARS",
|
||||||
"icon": "lcars.png",
|
"icon": "lcars.png",
|
||||||
"version":"0.17",
|
"version":"0.18",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [
|
||||||
|
{"url":"screenshot_1.png"},
|
||||||
|
{"url":"screenshot_3.png"}],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"lcars.app.js","url":"lcars.app.js"},
|
{"name":"lcars.app.js","url":"lcars.app.js"},
|
||||||
{"name":"lcars.img","url":"lcars.icon.js","evaluate":true},
|
{"name":"lcars.img","url":"lcars.icon.js","evaluate":true},
|
||||||
|
|
Before Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
|
@ -9,7 +9,7 @@ let s = {
|
||||||
'lat': 51.5072,
|
'lat': 51.5072,
|
||||||
'lon': 0.1276,
|
'lon': 0.1276,
|
||||||
'location': "London"
|
'location': "London"
|
||||||
}
|
};
|
||||||
|
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
|
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
|
||||||
|
@ -19,25 +19,25 @@ function loadSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
settings = s
|
settings = s;
|
||||||
require('Storage').write(SETTINGS_FILE, settings)
|
require('Storage').write(SETTINGS_FILE, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const locations = ["London", "Newcastle", "Edinburgh", "Paris", "New York", "Tokyo","???"];
|
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, 0.0];
|
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, 0.0];
|
const lons = [ -0.1276 , -1.6178 , -3.1883 , 2.3522 , -74.0060 , 139.6503 , 8.6553 , 174.7832 , 0.0 ];
|
||||||
|
|
||||||
function setFromGPS() {
|
function setFromGPS() {
|
||||||
Bangle.on('GPS', (gps) => {
|
Bangle.on('GPS', (gps) => {
|
||||||
//console.log(".");
|
//console.log(".");
|
||||||
if (gps.fix === 0) return;
|
if (gps.fix === 0) return;
|
||||||
//console.log("fix from GPS");
|
//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.buzz(1500); // buzz on first position
|
||||||
Bangle.setGPSPower(0);
|
Bangle.setGPSPower(0);
|
||||||
save();
|
save();
|
||||||
|
|
||||||
Bangle.setUI("updown", ()=>{ load() });
|
Bangle.setUI("updown", ()=>{ load(); });
|
||||||
E.showPrompt("Location has been saved from the GPS fix",{
|
E.showPrompt("Location has been saved from the GPS fix",{
|
||||||
title:"Location Saved",
|
title:"Location Saved",
|
||||||
buttons : {"OK":1}
|
buttons : {"OK":1}
|
||||||
|
@ -58,7 +58,7 @@ function showMainMenu() {
|
||||||
'<Back': ()=>{ load(); },
|
'<Back': ()=>{ load(); },
|
||||||
'City': {
|
'City': {
|
||||||
value: 0 | locations.indexOf(s.location),
|
value: 0 | locations.indexOf(s.location),
|
||||||
min: 0, max: 6,
|
min: 0, max: locations.length - 1,
|
||||||
format: v => locations[v],
|
format: v => locations[v],
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
if (v != 6) {
|
if (v != 6) {
|
||||||
|
@ -70,7 +70,7 @@ function showMainMenu() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Set From GPS': ()=>{ setFromGPS(); }
|
'Set From GPS': ()=>{ setFromGPS(); }
|
||||||
}
|
};
|
||||||
return E.showMenu(mainmenu);
|
return E.showMenu(mainmenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="tracks"></div>
|
<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/lib/interface.js"></script>
|
||||||
|
<script src="../../core/js/ui.js"></script>
|
||||||
|
<script src="../../core/js/utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var domTracks = document.getElementById("tracks");
|
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);
|
document.body.removeChild(a);
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
showToast("Download finished.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveGPX(track, title) {
|
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"?>
|
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">
|
<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>
|
<metadata>
|
||||||
|
@ -109,6 +118,7 @@ function saveGPX(track, title) {
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
showToast("Download finished.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCSV(track, title) {
|
function saveCSV(track, title) {
|
||||||
|
@ -121,6 +131,7 @@ function saveCSV(track, title) {
|
||||||
}).join(",")+"\n";
|
}).join(",")+"\n";
|
||||||
});
|
});
|
||||||
Util.saveCSV(title, csv);
|
Util.saveCSV(title, csv);
|
||||||
|
showToast("Download finished.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackLineToObject(headers, l) {
|
function trackLineToObject(headers, l) {
|
||||||
|
@ -171,6 +182,10 @@ function getTrackList() {
|
||||||
return {headers:headers,l:data};
|
return {headers:headers,l:data};
|
||||||
})(${JSON.stringify(filename)})`, trackInfo=>{
|
})(${JSON.stringify(filename)})`, trackInfo=>{
|
||||||
console.log(filename," => ",trackInfo);
|
console.log(filename," => ",trackInfo);
|
||||||
|
if (!trackInfo || !"headers" in trackInfo) {
|
||||||
|
showToast("Error loading track list.", "error");
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
trackInfo.headers = trackInfo.headers.split(",");
|
trackInfo.headers = trackInfo.headers.split(",");
|
||||||
trackList.push({
|
trackList.push({
|
||||||
filename : filename,
|
filename : filename,
|
||||||
|
|
|
@ -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.
|
|
@ -9,7 +9,7 @@ For rules, see [here](https://asmadigames.com/Red7Rules.pdf).
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Current rule card is shown in center of screen when viewing your hand.
|
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.
|
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
|
## Creator
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Red 7 Card Game",
|
"name": "Red 7 Card Game",
|
||||||
"shortName" : "Red 7",
|
"shortName" : "Red 7",
|
||||||
"icon": "icon.png",
|
"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!",
|
"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",
|
"tags": "game",
|
||||||
"supports":["BANGLEJS2"],
|
"supports":["BANGLEJS2"],
|
||||||
|
|
|
@ -654,7 +654,52 @@ function drawScreen2() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawScreenHelp() {
|
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) {
|
function drawGameOver(win) {
|
||||||
|
@ -678,9 +723,7 @@ function finishTurn() {
|
||||||
} else if(playerHand.handCards.length === 0) {
|
} else if(playerHand.handCards.length === 0) {
|
||||||
drawGameOver(false);
|
drawGameOver(false);
|
||||||
} else if(!canPlay(playerHand, playerPalette, AIPalette)) {
|
} else if(!canPlay(playerHand, playerPalette, AIPalette)) {
|
||||||
console.log("no play");
|
drawGameOver(false);
|
||||||
//drawGameOver(false);
|
|
||||||
drawScreen1();
|
|
||||||
} else {
|
} else {
|
||||||
E.showMenu();
|
E.showMenu();
|
||||||
drawScreen1();
|
drawScreen1();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: Hello Ruuvi Watch!
|
0.01: Hello Ruuvi Watch!
|
||||||
0.02: Clear gfx on startup.
|
0.02: Clear gfx on startup.
|
||||||
0.03: Improve design and code, reduce flicker.
|
0.03: Improve design and code, reduce flicker.
|
||||||
|
0.04: Ability to rename tags. Sauna, Fridge & Freezer alert. Support °F based on locale.
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Watch the status of [RuuviTags](https://ruuvi.com) in range.
|
Watch the status of [RuuviTags](https://ruuvi.com) in range.
|
||||||
|
|
||||||
|
By Marc Englund [GitHub](https://github.com/emarc) | [Twitter](https://twitter.com/marcenglund)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Id
|
- Id
|
||||||
|
@ -9,18 +11,23 @@ Watch the status of [RuuviTags](https://ruuvi.com) in range.
|
||||||
- Humidity (%)
|
- Humidity (%)
|
||||||
- Pressure (hPa)
|
- Pressure (hPa)
|
||||||
- Battery voltage
|
- Battery voltage
|
||||||
|
- Reading "freshness" (age)
|
||||||
Also shows how "fresh" the data is (age of reading).
|
- Ability to name tags
|
||||||
|
- Alerts for Sauna, Fridge, Freezer
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- Scans for devices when launched and every N seconds.
|
- Scans for devices when launched and every N seconds.
|
||||||
- Page trough devices with BTN1/BTN3.
|
- Page trough devices with left/right swipe or BTN1/BTN3.
|
||||||
- Trigger scan with BTN2.
|
- Page past last/first to trigger scan.
|
||||||
|
- BTN2 = Menu; name tag & trigger scan
|
||||||
|
- Change locale (via App Loader) to get Farenheit.
|
||||||
|
|
||||||
## Todo / ideas
|
## Todo / ideas
|
||||||
|
|
||||||
- Settings for scan frequency, units
|
- Bangle 2 support (I don't have one, let me know if you want to help with testing!)
|
||||||
- Allow to "name" known devices
|
- Settings for scan frequency
|
||||||
- Include more data
|
- Settings for alert limits
|
||||||
|
- Alert for "Wine cellar"
|
||||||
|
- Alert for Washer & Dryer (stops shaking = ready)
|
||||||
- Support older Ruuvi protocols
|
- Support older Ruuvi protocols
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Ruuvi Watch",
|
"name": "Ruuvi Watch",
|
||||||
"shortName":"Ruuvi Watch",
|
"shortName":"Ruuvi Watch",
|
||||||
"icon": "ruuviwatch.png",
|
"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.",
|
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.",
|
||||||
"readme":"README.md",
|
"readme":"README.md",
|
||||||
"tags": "bluetooth",
|
"tags": "bluetooth",
|
||||||
|
|
|
@ -7,11 +7,21 @@ require("Storage").write("ruuviwatch.info", {
|
||||||
|
|
||||||
const lookup = {};
|
const lookup = {};
|
||||||
const ruuvis = [];
|
const ruuvis = [];
|
||||||
|
const names = require("Storage").readJSON("RuuviNames") || {};
|
||||||
let current = 0;
|
let current = 0;
|
||||||
let scanning = false;
|
let scanning = false;
|
||||||
|
|
||||||
|
let paused = false;
|
||||||
|
|
||||||
const SCAN_FREQ = 1000 * 30;
|
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
|
// Fonts
|
||||||
const FONT_L = "Vector:60";
|
const FONT_L = "Vector:60";
|
||||||
const FONT_M = "Vector:20";
|
const FONT_M = "Vector:20";
|
||||||
|
@ -80,8 +90,8 @@ function p(data) {
|
||||||
int2Hex(data[OFFSET + 23]),
|
int2Hex(data[OFFSET + 23]),
|
||||||
].join(":");
|
].join(":");
|
||||||
|
|
||||||
robject.name =
|
robject.id = int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
|
||||||
"Ruuvi " + int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
|
|
||||||
return robject;
|
return robject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +124,7 @@ function drawAge() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function redrawAge() {
|
function redrawAge() {
|
||||||
|
if (paused) return;
|
||||||
const originalColor = g.getColor();
|
const originalColor = g.getColor();
|
||||||
g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10);
|
g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10);
|
||||||
g.setFont(FONT_S);
|
g.setFont(FONT_S);
|
||||||
|
@ -128,9 +139,15 @@ function redrawAge() {
|
||||||
g.setColor(originalColor);
|
g.setColor(originalColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getName(id) {
|
||||||
|
let name = names[id] || "Ruuvi";
|
||||||
|
return name + " (" + id + ")";
|
||||||
|
}
|
||||||
|
|
||||||
function redraw() {
|
function redraw() {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.setColor("#ffffff");
|
g.setColor("#ffffff");
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
|
||||||
if (ruuvis.length > 0 && ruuvis[current]) {
|
if (ruuvis.length > 0 && ruuvis[current]) {
|
||||||
const ruuvi = ruuvis[current];
|
const ruuvi = ruuvis[current];
|
||||||
|
@ -145,14 +162,22 @@ function redraw() {
|
||||||
|
|
||||||
// name
|
// name
|
||||||
g.setFont(FONT_M);
|
g.setFont(FONT_M);
|
||||||
g.drawString(ruuvi.name, CENTER, NAME_Y);
|
g.drawString(getName(ruuvi.id), CENTER, NAME_Y);
|
||||||
|
|
||||||
// age
|
// age
|
||||||
redrawAge();
|
redrawAge();
|
||||||
|
|
||||||
// temp
|
// temp
|
||||||
g.setFont(FONT_L);
|
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
|
// humid & pressure
|
||||||
g.setFont(FONT_M);
|
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() {
|
function scan() {
|
||||||
if (scanning) return;
|
if (scanning) return;
|
||||||
|
if (paused) return;
|
||||||
scanning = true;
|
scanning = true;
|
||||||
NRF.findDevices(
|
NRF.findDevices(
|
||||||
function (devices) {
|
function (devices) {
|
||||||
|
@ -184,11 +229,36 @@ function scan() {
|
||||||
devices.forEach((device) => {
|
devices.forEach((device) => {
|
||||||
const data = p(device.data);
|
const data = p(device.data);
|
||||||
data.time = new Date().getTime();
|
data.time = new Date().getTime();
|
||||||
const idx = lookup[data.name];
|
data.name = names[data.id] || "Ruuvi";
|
||||||
|
|
||||||
|
const idx = lookup[data.id];
|
||||||
if (idx !== undefined) {
|
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;
|
ruuvis[idx] = data;
|
||||||
} else {
|
} else {
|
||||||
lookup[data.name] = ruuvis.push(data) - 1;
|
lookup[data.id] = ruuvis.push(data) - 1;
|
||||||
foundNew = true;
|
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
|
// START
|
||||||
|
Bangle.on("swipe", function (dir) {
|
||||||
|
if (paused) return;
|
||||||
|
if (dir > 0) {
|
||||||
|
prevPage();
|
||||||
|
} else {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
// Button 1 pages up
|
// Button 1 pages up
|
||||||
setWatch(
|
setWatch(
|
||||||
() => {
|
() => {
|
||||||
current--;
|
if (paused) return;
|
||||||
if (current < 0) {
|
prevPage();
|
||||||
current = ruuvis.length - 1;
|
|
||||||
}
|
|
||||||
redraw();
|
|
||||||
},
|
},
|
||||||
BTN1,
|
BTN1,
|
||||||
{ repeat: true }
|
{ repeat: true }
|
||||||
);
|
);
|
||||||
// button triggers scan
|
// button triggers menu
|
||||||
setWatch(
|
setWatch(
|
||||||
() => {
|
() => {
|
||||||
scan();
|
if (paused) return;
|
||||||
|
showMenu();
|
||||||
},
|
},
|
||||||
BTN2,
|
BTN2,
|
||||||
{ repeat: true }
|
{ repeat: true }
|
||||||
|
@ -226,11 +468,8 @@ setWatch(
|
||||||
// button 3 pages down
|
// button 3 pages down
|
||||||
setWatch(
|
setWatch(
|
||||||
() => {
|
() => {
|
||||||
current++;
|
if (paused) return;
|
||||||
if (current >= ruuvis.length) {
|
nextPage();
|
||||||
current = 0;
|
|
||||||
}
|
|
||||||
redraw();
|
|
||||||
},
|
},
|
||||||
BTN3,
|
BTN3,
|
||||||
{ repeat: true }
|
{ repeat: true }
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Fix crash on start
|
0.02: Fix crash on start
|
||||||
|
0.03: Added power saving mode, move all read/write log actions into lib/module, fix #1445
|
||||||
|
|
|
@ -2,20 +2,26 @@
|
||||||
|
|
||||||
This app logs and displays the four following states:
|
This app logs and displays the four following states:
|
||||||
_unknown, not worn, awake, sleeping_
|
_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
|
#### Operating Principle
|
||||||
* __ESS calculation__
|
* __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.
|
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.
|
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_)
|
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.
|
* __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__
|
* __True Sleep__
|
||||||
The true sleep value is a simple addition of all registert sleeping periods.
|
The true sleep value is a simple addition of all registert sleeping periods.
|
||||||
* __Consecutive Sleep__
|
* __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.
|
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__
|
* __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
|
### Control
|
||||||
|
@ -28,28 +34,37 @@ It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm
|
||||||
---
|
---
|
||||||
### Settings
|
### Settings
|
||||||
---
|
---
|
||||||
* __BreakTod__ break at time of day
|
* __BreakTod__ | break at time of day
|
||||||
_0_ / _1_ / _..._ / __10__ / _..._ / _12_
|
_0_ / _1_ / _..._ / __10__ / _..._ / _12_
|
||||||
Change time of day on wich the lower graph starts and the upper graph ends.
|
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_
|
_15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_
|
||||||
Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period.
|
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_
|
_15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_
|
||||||
Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value.
|
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_
|
_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_.
|
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_
|
_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 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.
|
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_
|
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
|
||||||
If no movement is detected for this duration, the watch is considered as resting.
|
If no movement is detected for this duration, the watch is considered as resting.
|
||||||
* __Enabled__
|
* __Enabled__
|
||||||
__on__ / _off_
|
__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__
|
* __Logfile__
|
||||||
__default__ / _off_
|
__default__ / _off_
|
||||||
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
|
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
|
enabled: true, // bool / service status indicator
|
||||||
logfile: "sleeplog.log", // string / used logfile
|
logfile: "sleeplog.log", // string / used logfile
|
||||||
resting: false, // bool / indicates if the watch is resting
|
resting: false, // bool / indicates if the watch is resting
|
||||||
status: 2, // int / actual status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
status: 2, // int / actual status:
|
||||||
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement
|
/ 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()
|
stop: function () { ... }, // funct / stops the service until the next load()
|
||||||
start: function () { ... }, // funct / restarts the service
|
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")
|
>require("sleeplog")
|
||||||
={
|
={
|
||||||
setEnabled: function (enable, logfile) { ... },
|
setEnabled: function (enable, logfile, powersaving) { ... },
|
||||||
// en-/disable the service and/or logging
|
// restarts the service with changed settings
|
||||||
// * enable / bool / service status to change to
|
// * enable / bool / new service status
|
||||||
// * logfile / bool or string
|
// * logfile / bool or string
|
||||||
// - true = enables logging to "sleeplog.log"
|
// - true = enables logging to "sleeplog.log"
|
||||||
// - "some_file.log" = enables logging to "some_file.log"
|
// - "some_file.log" = enables logging to "some_file.log"
|
||||||
// - false = disables logging
|
// - false = disables logging
|
||||||
// returns: bool or undefined
|
// * (powersaving) / bool / new power saving status, default: false
|
||||||
// - true = changes executed
|
// returns: true or undefined
|
||||||
// - false = no changes needed
|
// - true = service restart executed
|
||||||
// - undefined = no global.sleeplog found
|
// - undefined = no global.sleeplog found
|
||||||
readLog: function (since, until) { ... },
|
readLog: function (logfile, since, until) { ... },
|
||||||
// read the raw log data for a specific time period
|
// read the raw log data for a specific time period
|
||||||
// * since / Date or number / startpoint of period
|
// * logfile / string / on no string uses logfile from global object or "sleeplog.log"
|
||||||
// * until / Date or number / endpoint of period
|
// * (since) / Date or number / startpoint of period, default: 0
|
||||||
|
// * (until) / Date or number / endpoint of period, default: 1E14
|
||||||
// returns: array
|
// returns: array
|
||||||
// * [[number, int, string], [...], ... ] / sorting: latest first
|
// * [[number, int, string], [...], ... ] / sorting: latest first
|
||||||
// - number // timestamp in ms
|
// - number // timestamp in ms
|
||||||
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
||||||
// - string // additional information
|
// - string // additional information
|
||||||
// * [] = no data available or global.sleeplog found
|
// * [] = no data available or global.sleeplog not found
|
||||||
getReadableLog: function (printLog, since, until) { ... }
|
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
|
// read the log data as humanreadable string for a specific time period
|
||||||
// * since / Date or number / startpoint of period
|
// * (printLog) / bool / direct print output with additional information, default: false
|
||||||
// * until / Date or number / endpoint of period
|
// * (since) / Date or number / see readLog(..)
|
||||||
|
// * (until) / Date or number / see readLog(..)
|
||||||
|
// * (logfile) / string / see readLog(..)
|
||||||
// returns: string
|
// returns: string
|
||||||
// * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last
|
// * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last
|
||||||
// * undefined = no data available or global.sleeplog found
|
// * undefined = no data available or global.sleeplog found
|
||||||
restoreLog: function (logfile) { ... }
|
restoreLog: function (logfile) { ... }
|
||||||
// eliminate some errors inside a specific 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
|
// returns: int / number of changes that were made
|
||||||
reinterpretTemp: function (logfile, tempthresh) { ... }
|
reinterpretTemp: function (logfile, tempthresh) { ... }
|
||||||
// reinterpret worn status based on given temperature threshold
|
// reinterpret worn status based on given temperature threshold
|
||||||
// * logfile / string / name of the logfile
|
// * (logfile) / string / see readLog(..)
|
||||||
// * tempthresh / float / new temperature threshold
|
// * (tempthresh) / float / new temperature threshold, on default uses tempthresh from global object or 27
|
||||||
// returns: int / number of changes that were made
|
// 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
|
#### To do list
|
||||||
* Send the logged information to Gadgetbridge.
|
* Send the logged information to Gadgetbridge.
|
||||||
_(For now I have no idea how to achieve this, help is appreciated.)_
|
_(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.
|
* Calculate and display overall sleep statistics.
|
||||||
|
* Option to automatically change power saving mode depending on time of day.
|
||||||
|
|
||||||
#### Requests, Bugs and Feedback
|
#### 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).
|
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).
|
||||||
|
|
|
@ -25,7 +25,7 @@ function drawLog(topY, viewUntil) {
|
||||||
var y = topY + graphHeight;
|
var y = topY + graphHeight;
|
||||||
|
|
||||||
// read 12h wide log
|
// 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
|
// format log array if not empty
|
||||||
if (log.length) {
|
if (log.length) {
|
||||||
|
@ -149,8 +149,8 @@ function drawNightTo(prevDays) {
|
||||||
|
|
||||||
// reduce date by 1s to ensure correct headline
|
// reduce date by 1s to ensure correct headline
|
||||||
date = Date(date.valueOf() - 1E3);
|
date = Date(date.valueOf() - 1E3);
|
||||||
// draw headline, on red bg if service or loggging disabled
|
// draw headline, on red bg if service or loggging disabled or green bg if powersaving enabled
|
||||||
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? g.theme.bg : 63488);
|
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
|
||||||
g.fillRect(0, 30, width, 66).reset();
|
g.fillRect(0, 30, width, 66).reset();
|
||||||
g.setFont("12x20").setFontAlign(0, -1);
|
g.setFont("12x20").setFontAlign(0, -1);
|
||||||
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +
|
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +
|
||||||
|
|
|
@ -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.
|
// 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
|
// 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
|
// load settings into global object
|
||||||
global.sleeplog = Object.assign({
|
global.sleeplog = Object.assign({
|
||||||
enabled: true, // en-/disable completely
|
enabled: true, // en-/disable completely
|
||||||
logfile: "sleeplog.log", // logfile
|
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
|
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
|
||||||
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
|
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
|
||||||
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
|
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
|
tempthresh: 27, // every temperature above ist registered as worn
|
||||||
}, require("Storage").readJSON("sleeplog.json", true) || {});
|
}, require("Storage").readJSON("sleeplog.json", true) || {});
|
||||||
|
|
||||||
// delete app settings
|
// delete app settings
|
||||||
["breaktod", "maxawake", "minconsec"].forEach(property => delete global.sleeplog[property]);
|
["breaktod", "maxawake", "minconsec"].forEach(property => delete sleeplog[property]);
|
||||||
|
|
||||||
// check if service enabled
|
// check if service enabled
|
||||||
if (global.sleeplog.enabled) {
|
if (sleeplog.enabled) {
|
||||||
|
|
||||||
// add cached values and functions to global object
|
// add always used values and functions to global object
|
||||||
global.sleeplog = Object.assign(global.sleeplog, {
|
sleeplog = Object.assign(sleeplog, {
|
||||||
// set cached values
|
// set cached values
|
||||||
ess_values: [],
|
|
||||||
nomocount: 0,
|
|
||||||
firstnomodate: undefined,
|
|
||||||
resting: undefined,
|
resting: undefined,
|
||||||
status: 0,
|
status: undefined,
|
||||||
|
|
||||||
// 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;
|
|
||||||
},
|
|
||||||
|
|
||||||
// define stop function (logging will restart if enabled and boot file is executed)
|
// define stop function (logging will restart if enabled and boot file is executed)
|
||||||
stop: function() {
|
stop: function() {
|
||||||
// remove acceleration and kill listener
|
// remove all listeners
|
||||||
Bangle.removeListener('accel', global.sleeplog.accel);
|
Bangle.removeListener('accel', sleeplog.accel);
|
||||||
E.removeListener('kill', global.sleeplog.stop);
|
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
|
// write log with undefined sleeping status
|
||||||
global.sleeplog.log(Math.floor(Date.now()));
|
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
|
||||||
// reset cached values
|
// reset always used cached values
|
||||||
global.sleeplog.ess_values = [];
|
sleeplog.resting = undefined;
|
||||||
global.sleeplog.nomocount = 0;
|
sleeplog.status = undefined;
|
||||||
global.sleeplog.firstnomodate = undefined;
|
sleeplog.ess_values = [];
|
||||||
global.sleeplog.resting = undefined;
|
sleeplog.nomocount = 0;
|
||||||
global.sleeplog.status = 0;
|
sleeplog.firstnomodate = undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
// define restart function (also use for initial starting)
|
// define restart function (also use for initial starting)
|
||||||
start: function() {
|
start: function() {
|
||||||
// add acceleration listener
|
// exit on missing global object
|
||||||
Bangle.on('accel', global.sleeplog.accel);
|
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
|
// 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
|
// initial starting
|
||||||
global.sleeplog.start();
|
global.sleeplog.start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
exports = {
|
exports = {
|
||||||
// define en-/disable function
|
// define en-/disable function, restarts the service to make changes take effect
|
||||||
setEnabled: function(enable, logfile) {
|
setEnabled: function(enable, logfile, powersaving) {
|
||||||
// check if sleeplog is available
|
// check if sleeplog is available
|
||||||
if (typeof global.sleeplog !== "object") return;
|
if (typeof global.sleeplog !== "object") return;
|
||||||
|
|
||||||
// set default logfile
|
// set default logfile
|
||||||
logfile = logfile.endsWith(".log") ? logfile :
|
logfile = (typeof logfile === "string" && logfile.endsWith(".log")) ? logfile :
|
||||||
logfile === false ? undefined :
|
logfile === false ? undefined : "sleeplog.log";
|
||||||
"sleeplog.log";
|
|
||||||
|
|
||||||
// check if status needs to be changed
|
|
||||||
if (enable === global.sleeplog.enabled ||
|
|
||||||
logfile === global.sleeplog.logfile) return false;
|
|
||||||
|
|
||||||
// stop if enabled
|
// stop if enabled
|
||||||
if (global.sleeplog.enabled) global.sleeplog.stop();
|
if (global.sleeplog.enabled) global.sleeplog.stop();
|
||||||
|
@ -23,7 +18,8 @@ exports = {
|
||||||
// change enabled value in settings
|
// change enabled value in settings
|
||||||
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
|
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
|
||||||
enabled: enable,
|
enabled: enable,
|
||||||
logfile: logfile
|
logfile: logfile,
|
||||||
|
powersaving: powersaving || false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// force changes to take effect by executing the boot script
|
// 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
|
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
||||||
// - float // internal temperature
|
// - float // internal temperature
|
||||||
// - string // additional information
|
// - string // additional information
|
||||||
readLog: function(since, until) {
|
readLog: function(logfile, since, until) {
|
||||||
// set logfile
|
// check/set logfile
|
||||||
var logfile = (global.sleeplog || {}).logfile || "sleeplog.log";
|
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
|
||||||
|
(global.sleeplog || {}).logfile || "sleeplog.log";
|
||||||
|
|
||||||
// check if since is in the future
|
// check if since is in the future
|
||||||
if (since > Date()) return [];
|
if (since > Date()) return [];
|
||||||
|
|
||||||
// read log json to array
|
// read logfile
|
||||||
var log = JSON.parse(atob(require("Storage").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
|
// check if filtering is needed
|
||||||
since = (log.find(element => element[0] <= since) || [0])[0];
|
if (since || until) {
|
||||||
|
// search for latest entry befor since
|
||||||
// filter selected time period
|
if (since) since = (log.find(element => element[0] <= since) || [0])[0];
|
||||||
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
|
// filter selected time period
|
||||||
|
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
|
||||||
|
}
|
||||||
|
|
||||||
// output log
|
// output log
|
||||||
return 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
|
// define log to humanreadable string function
|
||||||
// sorting: latest last, format:
|
// sorting: latest last, format:
|
||||||
// "{substring of ISO date} - {status} for {duration}min\n..."
|
// "{substring of ISO date} - {status} for {duration}min\n..."
|
||||||
getReadableLog: function(printLog, since, until) {
|
getReadableLog: function(printLog, since, until, logfile) {
|
||||||
// read log and check
|
// read log and check
|
||||||
var log = this.readLog(since, until);
|
var log = this.readLog(logfile, since, until);
|
||||||
if (!log.length) return;
|
if (!log.length) return;
|
||||||
// reverse array to set last timestamp to the end
|
// reverse array to set last timestamp to the end
|
||||||
log.reverse();
|
log.reverse();
|
||||||
|
@ -81,8 +122,11 @@ exports = {
|
||||||
logString[index] = "" +
|
logString[index] = "" +
|
||||||
Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
|
Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
|
||||||
statusText[element[1]] +
|
statusText[element[1]] +
|
||||||
(index === log.length - 1 ? "" : " for " + Math.round((log[index + 1][0] - element[0]) / 60000) + "min") +
|
(index === log.length - 1 ?
|
||||||
(element[2] ? " | Temp: " + element[2] + "°C" : "") +
|
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] : "");
|
(element[3] ? " | " + element[3] : "");
|
||||||
});
|
});
|
||||||
logString = logString.join("\n");
|
logString = logString.join("\n");
|
||||||
|
@ -100,11 +144,9 @@ exports = {
|
||||||
|
|
||||||
// define function to eliminate some errors inside the log
|
// define function to eliminate some errors inside the log
|
||||||
restoreLog: function(logfile) {
|
restoreLog: function(logfile) {
|
||||||
// define storage
|
// read log and check
|
||||||
var storage = require("Storage");
|
var log = this.readLog(logfile);
|
||||||
|
if (!log.length) return;
|
||||||
// read log json to array
|
|
||||||
var log = JSON.parse(atob(storage.read(logfile)));
|
|
||||||
|
|
||||||
// define output variable to show number of changes
|
// define output variable to show number of changes
|
||||||
var output = log.length;
|
var output = log.length;
|
||||||
|
@ -112,8 +154,8 @@ exports = {
|
||||||
// remove non decremental entries
|
// remove non decremental entries
|
||||||
log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
|
log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
|
||||||
|
|
||||||
// write log to storage
|
// write log
|
||||||
storage.write(logfile, btoa(JSON.stringify(log)));
|
this.writeLog(logfile, log);
|
||||||
|
|
||||||
// return difference in length
|
// return difference in length
|
||||||
return output - log.length;
|
return output - log.length;
|
||||||
|
@ -121,11 +163,12 @@ exports = {
|
||||||
|
|
||||||
// define function to reinterpret worn status based on given temperature threshold
|
// define function to reinterpret worn status based on given temperature threshold
|
||||||
reinterpretTemp: function(logfile, tempthresh) {
|
reinterpretTemp: function(logfile, tempthresh) {
|
||||||
// define storage
|
// read log and check
|
||||||
var storage = require("Storage");
|
var log = this.readLog(logfile);
|
||||||
|
if (!log.length) return;
|
||||||
|
|
||||||
// read log json to array
|
// set default tempthresh
|
||||||
var log = JSON.parse(atob(storage.read(logfile)));
|
tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
|
||||||
|
|
||||||
// define output variable to show number of changes
|
// define output variable to show number of changes
|
||||||
var output = 0;
|
var output = 0;
|
||||||
|
@ -140,8 +183,8 @@ exports = {
|
||||||
return element;
|
return element;
|
||||||
});
|
});
|
||||||
|
|
||||||
// write log to storage
|
// write log
|
||||||
storage.write(logfile, btoa(JSON.stringify(log)));
|
this.writeLog(logfile, log);
|
||||||
|
|
||||||
// return output
|
// return output
|
||||||
return output;
|
return output;
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
"id":"sleeplog",
|
"id":"sleeplog",
|
||||||
"name":"Sleep Log",
|
"name":"Sleep Log",
|
||||||
"shortName": "SleepLog",
|
"shortName": "SleepLog",
|
||||||
"version": "0.02",
|
"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).",
|
"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",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
"tags": "tool,boot",
|
"tags": "tool,boot",
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
maxawake: 36E5, // 60min in ms
|
maxawake: 36E5, // 60min in ms
|
||||||
minconsec: 18E5, // 30min in ms
|
minconsec: 18E5, // 30min in ms
|
||||||
tempthresh: 27, // every temperature above ist registered as worn
|
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
|
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
|
||||||
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
|
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
|
||||||
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
|
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
|
||||||
|
@ -32,14 +34,20 @@
|
||||||
return value > max ? min : value < min ? max : value;
|
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
|
// calculate sleepthresh factor
|
||||||
var stFactor = settings.winwidth / 12.5 / 60;
|
var stFactor = settings.winwidth / 12.5 / 60;
|
||||||
|
|
||||||
// show main menu
|
// show main menu
|
||||||
function showMain() {
|
function showMain(selected) {
|
||||||
var mainMenu = E.showMenu({
|
var mainMenu = {
|
||||||
"": {
|
"": {
|
||||||
title: "Sleep Log"
|
title: "Sleep Log",
|
||||||
|
selected: selected
|
||||||
},
|
},
|
||||||
"< Exit": () => load(),
|
"< Exit": () => load(),
|
||||||
"< Back": () => back(),
|
"< Back": () => back(),
|
||||||
|
@ -78,6 +86,23 @@
|
||||||
writeSetting("tempthresh", v);
|
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": {
|
"NoMoThresh": {
|
||||||
value: settings.nomothresh,
|
value: settings.nomothresh,
|
||||||
step: 0.001,
|
step: 0.001,
|
||||||
|
@ -100,17 +125,27 @@
|
||||||
value: settings.enabled,
|
value: settings.enabled,
|
||||||
format: v => v ? "on" : "off",
|
format: v => v ? "on" : "off",
|
||||||
onchange: function(v) {
|
onchange: function(v) {
|
||||||
writeSetting("enabled", v);
|
settings.enabled = v;
|
||||||
|
changeRestart();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logfile ": {
|
"Logfile ": {
|
||||||
value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false,
|
value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false,
|
||||||
format: v => v === true ? "default" : v ? "custom" : "off",
|
format: v => v === true ? "default" : v ? "custom" : "off",
|
||||||
onchange: function(v) {
|
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
|
// draw main menu
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: First version
|
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
After Width: | Height: | Size: 783 B |
|
@ -1,3 +1,4 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Fix position and overdraw bugs
|
0.02: Fix position and overdraw bugs
|
||||||
Better memory usage, theme support
|
0.03: Better memory usage, theme support
|
||||||
|
0.04: Replace the 8 phases by a more exact drawing, see forum.espruino.com/conversations/371985
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"id": "widmp",
|
"id": "widmp",
|
||||||
"name": "Moon Phase Widget",
|
"name": "Moon Phase Widget",
|
||||||
"version": "0.02",
|
"version": "0.04",
|
||||||
"description": "Display the current moon phase in blueish for the northern hemisphere in eight phases",
|
"description": "Display the current moon phase in blueish for both hemispheres. In the southern hemisphere the 'My Location' app is needed.",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
"tags": "widget,tools",
|
"tags": "widget,tools",
|
||||||
|
|
|
@ -1,26 +1,63 @@
|
||||||
WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() {
|
WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() {
|
||||||
const MC = 29.5305882, NM = 694039.09;
|
const CenterX = this.x + 12, CenterY = this.y + 12, Radius = 11;
|
||||||
var r = 11, mx = this.x + 12; my = this.y + 12;
|
var southernHemisphere = false; // when in southern hemisphere, use the "My Location" App
|
||||||
|
|
||||||
function moonPhase(d) {
|
const simulate = false; // simulate one month in one minute
|
||||||
var tmp, month = d.getMonth(), year = d.getFullYear(), day = d.getDate();
|
const updateR = 1000; // update every x ms in simulation
|
||||||
|
|
||||||
|
function moonPhase() {
|
||||||
|
const d = Date();
|
||||||
|
var month = d.getMonth(), year = d.getFullYear(), day = d.getDate();
|
||||||
|
if (simulate) day = d.getSeconds() / 2 +1;
|
||||||
if (month < 3) {year--; month += 12;}
|
if (month < 3) {year--; month += 12;}
|
||||||
tmp = ((365.25 * year + 30.6 * ++month + day - NM) / MC);
|
mproz = ((365.25 * year + 30.6 * ++month + day - 694039.09) / 29.5305882);
|
||||||
return Math.round(((tmp - (tmp | 0)) * 7)+1);
|
mproz = mproz - (mproz | 0); // strip integral digits, result is between 0 and <1
|
||||||
|
if (simulate) console.log(mproz + " " + day);
|
||||||
|
return (mproz);
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLACK = g.theme.bg, MOON = 0x41f;
|
function loadLocation() {
|
||||||
var moon = {
|
// "mylocation.json" is created by the "My Location" app
|
||||||
0: () => { g.reset().setColor(BLACK).fillRect(mx - r, my - r, mx + r, my + r);},
|
location = require("Storage").readJSON("mylocation.json",1)||{"lat":50.1236,"lon":8.6553,"location":"Frankfurt"};
|
||||||
1: () => { moon[0](); g.setColor(MOON).drawCircle(mx, my, r);},
|
if (location.lat < 0) southernHemisphere = true;
|
||||||
2: () => { moon[3](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
|
}
|
||||||
3: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx - r, my - r, mx, my + r);},
|
|
||||||
4: () => { moon[3](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
|
|
||||||
5: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r);},
|
|
||||||
6: () => { moon[7](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
|
|
||||||
7: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx, my - r, mx + r, my + r);},
|
|
||||||
8: () => { moon[7](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}
|
|
||||||
};
|
|
||||||
moon[moonPhase(Date())]();
|
|
||||||
} };
|
|
||||||
|
|
||||||
|
// code source: github.com/rozek/banglejs-2-activities/blob/main/README.md#drawmoonphase
|
||||||
|
function drawMoonPhase (CenterX,CenterY, Radius, leftFactor,rightFactor) {
|
||||||
|
let x = Radius, y = 0, Error = Radius;
|
||||||
|
g.drawLine(CenterX-leftFactor*x,CenterY, CenterX+rightFactor*x,CenterY);
|
||||||
|
let dx,dy;
|
||||||
|
while (y <= x) {
|
||||||
|
dy = 1 + 2*y; y++; Error -= dy;
|
||||||
|
if (Error < 0) {
|
||||||
|
dx = 1 - 2*x; x--; Error -= dx;
|
||||||
|
}
|
||||||
|
g.drawLine(CenterX-leftFactor*x,CenterY-y, CenterX+rightFactor*x,CenterY-y);
|
||||||
|
g.drawLine(CenterX-leftFactor*x,CenterY+y, CenterX+rightFactor*x,CenterY+y);
|
||||||
|
g.drawLine(CenterX-leftFactor*y,CenterY-x, CenterX+rightFactor*y,CenterY-x);
|
||||||
|
g.drawLine(CenterX-leftFactor*y,CenterY+x, CenterX+rightFactor*y,CenterY+x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidget() {
|
||||||
|
g.reset().setColor(g.theme.bg);
|
||||||
|
g.fillRect(CenterX - Radius, CenterY - Radius, CenterX + Radius, CenterY + Radius);
|
||||||
|
g.setColor(0x41f);
|
||||||
|
|
||||||
|
mproz = moonPhase(); // mproz = 0..<1
|
||||||
|
|
||||||
|
leftFactor = mproz * 4 - 1;
|
||||||
|
rightFactor = (1 - mproz) * 4 - 1;
|
||||||
|
if (mproz >= 0.5) leftFactor = 1; else rightFactor = 1;
|
||||||
|
if (true == southernHemisphere) {
|
||||||
|
var tmp=leftFactor; leftFactor=rightFactor; rightFactor=tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMoonPhase(CenterX,CenterY, Radius, leftFactor,rightFactor);
|
||||||
|
|
||||||
|
if (simulate) setTimeout(updateWidget, updateR);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocation();
|
||||||
|
updateWidget();
|
||||||
|
} };
|
||||||
|
|
|
@ -52,7 +52,8 @@ where "*n*" is the next unused number.
|
||||||
|
|
||||||
Select a waypoint using the menu. Once the waypoint is selected and you're
|
Select a waypoint using the menu. Once the waypoint is selected and you're
|
||||||
back on the main screen, press either the top or bottom button (`BTN1` or
|
back on the main screen, press either the top or bottom button (`BTN1` or
|
||||||
`BTN3`). Confirm that you want to delete the waypoint with the middle
|
`BTN3`), or, on Bangle.js 2, scroll the screen up or down.
|
||||||
|
Confirm that you want to delete the waypoint with the middle
|
||||||
button (`BTN2`).
|
button (`BTN2`).
|
||||||
|
|
||||||
## Waypoint editor
|
## Waypoint editor
|
||||||
|
@ -68,27 +69,43 @@ This will load up the waypoint editor:
|
||||||
|
|
||||||
### Add a waypoint
|
### Add a waypoint
|
||||||
|
|
||||||
Use the map to find your destination. Clicking on the map will
|
Click on the map to add a waypoint. You'll be prompted to give it
|
||||||
populate the latitude/longitude input boxes with the coordinates
|
a name.
|
||||||
of the point you clicked on. Type in a name for the waypoint and
|
|
||||||
click "Add Waypoint". Click "Upload" to send the updated list of
|
|
||||||
waypoints to the watch.
|
|
||||||
|
|
||||||
### Edit a waypoint
|
### Edit a waypoint
|
||||||
|
|
||||||
Click on the pencil icon next to the waypoint you wish to edit.
|
Click on the map marker of the waypoint you wish to edit. You
|
||||||
This will remove the waypoint from the list and populate the
|
can then click on the blue pencil icon to edit the name of the
|
||||||
input boxes.
|
waypoint. If you want to move the waypoint to a new location then
|
||||||
Edit the coordinates by hand, or by clicking on the map. Edit
|
you need to delete it and re-add it.
|
||||||
the name if you want. Click "Add Waypoint" to save the waypoint
|
|
||||||
back to the list. Click "Upload" to send the updated list of
|
|
||||||
waypoints to the watch.
|
|
||||||
|
|
||||||
### Delete a waypoint
|
### Delete a waypoint
|
||||||
|
|
||||||
Click on the pencil icon next to the waypoint you wish to edit.
|
Click on the map marker of the waypoint you wish to delete. You
|
||||||
This will remove the waypoint from the list.
|
can then click on the red bin icon to delete the waypoint.
|
||||||
Click "Upload" to send the updated list of waypoints to the watch.
|
|
||||||
|
### Add a route
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click on the map to place the first waypoint. The name of the first
|
||||||
|
waypoint will become the name of the route.
|
||||||
|
|
||||||
|
Click on the map marker for the new waypoint and click "Make route".
|
||||||
|
Now every time you click on the map it will add another point
|
||||||
|
on this route. When you're done either right click or click the
|
||||||
|
"Close route" button above the map.
|
||||||
|
|
||||||
|
Points along the route don't have names by default, but if you wish
|
||||||
|
to add one you can click on the waypoint and use the blue pencil icon
|
||||||
|
to give it a name.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Delete a route
|
||||||
|
|
||||||
|
Click on the map marker for any point on the route, and select
|
||||||
|
"Delete entire route".
|
||||||
|
|
||||||
## Mounting the watch on the bike
|
## Mounting the watch on the bike
|
||||||
|
|
||||||
|
@ -123,6 +140,7 @@ Compared to the original Way Pointer app, Waypointer Moto:
|
||||||
* can add new waypoints from inside the app without requiring a blank slot
|
* can add new waypoints from inside the app without requiring a blank slot
|
||||||
* can delete waypoints from inside the app without needing the PC
|
* can delete waypoints from inside the app without needing the PC
|
||||||
* still uses the same `waypoints.json` file
|
* still uses the same `waypoints.json` file
|
||||||
|
* supports "routes" which automatically step from one waypoint to the next
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
|
@ -144,8 +162,6 @@ turns white again.
|
||||||
|
|
||||||
## Possible Future Enhancements
|
## Possible Future Enhancements
|
||||||
|
|
||||||
- "routes" with multiple waypoints; automatically step from one
|
|
||||||
waypoint to the next when you get near to it
|
|
||||||
- some way to manually input coordinates directly on the watch
|
- some way to manually input coordinates directly on the watch
|
||||||
- make the text & arrow more legible in direct sunlight
|
- make the text & arrow more legible in direct sunlight
|
||||||
- integrate a charging connector into the handlebar mount
|
- integrate a charging connector into the handlebar mount
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
var loc = require("locale");
|
var loc = require("locale");
|
||||||
|
|
||||||
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
|
var waypoints = require("Storage").readJSON("waypoints.json") || [];
|
||||||
var wp = waypoints[0];
|
var wp = waypoints[0];
|
||||||
|
if (wp == undefined) wp = {name:"NONE"};
|
||||||
var wp_bearing = 0;
|
var wp_bearing = 0;
|
||||||
|
var routeidx = 0;
|
||||||
var candraw = true;
|
var candraw = true;
|
||||||
|
|
||||||
|
const ROUTE_STEP = 50; // metres
|
||||||
|
const EPSILON = 1; // degrees
|
||||||
|
|
||||||
var direction = 0;
|
var direction = 0;
|
||||||
var dist = 0;
|
var dist = 0;
|
||||||
|
|
||||||
|
@ -15,138 +20,130 @@ var previous = {
|
||||||
wp_name: '',
|
wp_name: '',
|
||||||
course: 180,
|
course: 180,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
routeidx: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*** Drawing ***/
|
/*** Drawing ***/
|
||||||
|
|
||||||
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
|
var W = g.getWidth();
|
||||||
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
|
var H = g.getHeight();
|
||||||
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
|
// layout (XXX: this should probably use the Layout library instead)
|
||||||
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red
|
var L = { // banglejs1
|
||||||
var pal_compass = pal_by;
|
arrow: {
|
||||||
|
x: 120,
|
||||||
|
y: 80,
|
||||||
|
r1: 79,
|
||||||
|
r2: 69,
|
||||||
|
bufh: 160,
|
||||||
|
bufy: 40,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
bufh: 40,
|
||||||
|
bufy: 200,
|
||||||
|
largesize: 40,
|
||||||
|
smallsize: 15,
|
||||||
|
waypointy: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (W == 176) {
|
||||||
|
L = { // banglejs2
|
||||||
|
arrow: {
|
||||||
|
x: 88,
|
||||||
|
y: 70,
|
||||||
|
r1: 70,
|
||||||
|
r2: 62,
|
||||||
|
bufh: 160,
|
||||||
|
bufy: 0,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
bufh: 40,
|
||||||
|
bufy: 142,
|
||||||
|
largesize: 40,
|
||||||
|
smallsize: 14,
|
||||||
|
waypointy: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var buf = Graphics.createArrayBuffer(160,160,1, {msb:true});
|
var pal_by = new Uint16Array([0x0000,0xffc0],0,1); // black, yellow
|
||||||
|
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
|
||||||
|
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red
|
||||||
|
|
||||||
|
var buf = Graphics.createArrayBuffer(240,160, 1, {msb:true});
|
||||||
var arrow_img = require("heatshrink").decompress(atob("vF4wJC/AEMYBxs8Bxt+Bxv/BpkB/+ABxcD//ABxcH//gBxcP//wBxcf//4Bxc///8Bxd///+OxgABOxgABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBQNCPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBQNEPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBANGPAx2HPAx2HPAx2HPAx2HPAx2HPAx2HeJTeJB34O/B34O/B34O/B34O/B34O/B34O/B34O/B34OTAH4AT"));
|
var arrow_img = require("heatshrink").decompress(atob("vF4wJC/AEMYBxs8Bxt+Bxv/BpkB/+ABxcD//ABxcH//gBxcP//wBxcf//4Bxc///8Bxd///+OxgABOxgABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBQNCPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBQNEPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBANGPAx2HPAx2HPAx2HPAx2HPAx2HPAx2HeJTeJB34O/B34O/B34O/B34O/B34O/B34O/B34O/B34OTAH4AT"));
|
||||||
|
|
||||||
function flip1(x,y,palette) {
|
function flip(y,h,palette) {
|
||||||
g.drawImage({width:160,height:160,bpp:1,buffer:buf.buffer, palette:palette},x,y);
|
g.drawImage({width:240,height:h,bpp:1,buffer:buf.buffer, palette:palette},0,y);
|
||||||
buf.clear();
|
buf.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function flip2_bw(x,y) {
|
function draw(force) {
|
||||||
g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bw},x,y);
|
|
||||||
buf.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function flip2_bb(x,y) {
|
|
||||||
g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bb},x,y);
|
|
||||||
buf.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCompass(course) {
|
|
||||||
if (!candraw) return;
|
if (!candraw) return;
|
||||||
|
|
||||||
previous.course = course;
|
var course = direction;
|
||||||
|
|
||||||
buf.setColor(1);
|
|
||||||
buf.fillCircle(80,80, 79);
|
|
||||||
buf.setColor(0);
|
|
||||||
buf.fillCircle(80,80, 69);
|
|
||||||
buf.setColor(1);
|
|
||||||
buf.drawImage(arrow_img, 80, 80, {rotate:radians(course)} );
|
|
||||||
var palette = pal_br;
|
|
||||||
if (savedfix !== undefined && savedfix.fix !== 0) palette = pal_compass;
|
|
||||||
flip1(40, 30, palette);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawN(force){
|
|
||||||
if (!candraw) return;
|
|
||||||
|
|
||||||
buf.setFont("Vector",24);
|
|
||||||
var dst = loc.distance(dist);
|
var dst = loc.distance(dist);
|
||||||
|
|
||||||
// distance on left
|
if (force || previous.dst !== dst || previous.wp_name !== wp.name || previous.routeidx !== routeidx || Math.abs(course-previous.course)>EPSILON) {
|
||||||
if (force || previous.dst !== dst) {
|
previous.course = course;
|
||||||
|
|
||||||
|
var palette = pal_br;
|
||||||
|
if (savedfix !== undefined && savedfix.fix !== 0)
|
||||||
|
palette = isNaN(savedfix.course) ? pal_by : pal_bw;
|
||||||
|
|
||||||
|
buf.setColor(1);
|
||||||
|
buf.fillCircle(L.arrow.x,L.arrow.y, L.arrow.r1);
|
||||||
|
buf.setColor(0);
|
||||||
|
buf.fillCircle(L.arrow.x,L.arrow.y, L.arrow.r2);
|
||||||
|
buf.setColor(1);
|
||||||
|
buf.drawImage(arrow_img, L.arrow.x, L.arrow.y, {rotate:radians(course)} );
|
||||||
|
flip(L.arrow.bufy,L.arrow.bufh,palette);
|
||||||
|
|
||||||
|
// distance on left
|
||||||
previous.dst = dst;
|
previous.dst = dst;
|
||||||
|
previous.wp_name = wp.name;
|
||||||
|
previous.routeidx = routeidx;
|
||||||
|
|
||||||
buf.setColor(1);
|
buf.setColor(1);
|
||||||
buf.setFontAlign(-1, -1);
|
buf.setFontAlign(-1, -1);
|
||||||
buf.setFont("Vector",40);
|
buf.setFont("Vector",L.text.largesize);
|
||||||
buf.drawString(dst,0,0);
|
buf.drawString(dst,0,0);
|
||||||
flip2_bw(8, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// waypoint name on right
|
// waypoint name on right
|
||||||
if (force || previous.wp_name !== wp.name) {
|
|
||||||
previous.wp_name = wp.name;
|
|
||||||
buf.setColor(1);
|
buf.setColor(1);
|
||||||
buf.setFontAlign(1, -1);
|
buf.setFontAlign(1, -1);
|
||||||
buf.setFont("Vector", 15);
|
buf.setFont("Vector", L.text.smallsize);
|
||||||
buf.drawString(wp.name, 80, 0);
|
buf.drawString(wp.name, W, L.text.waypointy);
|
||||||
flip2_bw(160, 220);
|
|
||||||
|
// if this is a route, draw the step name above the route name
|
||||||
|
if (wp.route) {
|
||||||
|
buf.drawString((wp.route[routeidx].name||'') + " " + (routeidx+1) + "/" + wp.route.length, W, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
flip(L.text.bufy,L.text.bufh,pal_bw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAll(force) {
|
|
||||||
if (!candraw) return;
|
|
||||||
|
|
||||||
g.setColor(1,1,1);
|
|
||||||
drawN(force);
|
|
||||||
drawCompass(direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Heading ***/
|
/*** Heading ***/
|
||||||
|
|
||||||
var heading = 0;
|
var heading = 0;
|
||||||
function newHeading(m,h){
|
|
||||||
var s = Math.abs(m - h);
|
|
||||||
var delta = (m>h)?1:-1;
|
|
||||||
if (s>=180){s=360-s; delta = -delta;}
|
|
||||||
if (s<2) return h;
|
|
||||||
var hd = h + delta*(1 + Math.round(s/5));
|
|
||||||
if (hd<0) hd+=360;
|
|
||||||
if (hd>360)hd-= 360;
|
|
||||||
return hd;
|
|
||||||
}
|
|
||||||
|
|
||||||
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
|
|
||||||
|
|
||||||
function tiltfixread(O,S){
|
|
||||||
var start = Date.now();
|
|
||||||
var m = Bangle.getCompass();
|
|
||||||
var g = Bangle.getAccel();
|
|
||||||
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
|
|
||||||
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
|
|
||||||
if (d<0) d+=360;
|
|
||||||
var phi = Math.atan(-g.x/-g.z);
|
|
||||||
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
|
|
||||||
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
|
|
||||||
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
|
|
||||||
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
|
|
||||||
var yh = m.dz*sinphi - m.dx*cosphi;
|
|
||||||
var psi = Math.atan2(yh,xh)*180/Math.PI;
|
|
||||||
if (psi<0) psi+=360;
|
|
||||||
return psi;
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_heading() {
|
function read_heading() {
|
||||||
if (savedfix !== undefined && !isNaN(savedfix.course)) {
|
if (savedfix !== undefined && savedfix.satellites > 0 && !isNaN(savedfix.course)) {
|
||||||
Bangle.setCompassPower(0);
|
Bangle.setCompassPower(0);
|
||||||
heading = savedfix.course;
|
heading = savedfix.course;
|
||||||
pal_compass = pal_bw;
|
|
||||||
} else {
|
} else {
|
||||||
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
|
||||||
Bangle.setCompassPower(1);
|
Bangle.setCompassPower(1);
|
||||||
heading = newHeading(d,heading);
|
var d = 0;
|
||||||
pal_compass = pal_by;
|
var m = Bangle.getCompass();
|
||||||
|
if (!isNaN(m.heading)) d = -m.heading;
|
||||||
|
heading = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
direction = wp_bearing - heading;
|
direction = wp_bearing - heading;
|
||||||
if (direction < 0) direction += 360;
|
if (direction < 0) direction += 360;
|
||||||
if (direction > 360) direction -= 360;
|
if (direction > 360) direction -= 360;
|
||||||
drawCompass(direction);
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*** Maths ***/
|
/*** Maths ***/
|
||||||
|
|
||||||
function radians(a) {
|
function radians(a) {
|
||||||
|
@ -218,23 +215,39 @@ function onGPS(fix) {
|
||||||
savedfix = fix;
|
savedfix = fix;
|
||||||
|
|
||||||
if (fix !== undefined && fix.fix == 1){
|
if (fix !== undefined && fix.fix == 1){
|
||||||
dist = distance(fix, wp);
|
if (wp.route) {
|
||||||
|
while (true) {
|
||||||
|
dist = distance(fix, wp.route[routeidx]);
|
||||||
|
// step to next point if we're within ROUTE_STEP metres
|
||||||
|
if (!isNaN(dist) && dist < ROUTE_STEP && routeidx < wp.route.length-1)
|
||||||
|
routeidx++;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dist = distance(fix, wp);
|
||||||
|
}
|
||||||
if (isNaN(dist)) dist = 0;
|
if (isNaN(dist)) dist = 0;
|
||||||
wp_bearing = bearing(fix, wp);
|
|
||||||
|
if (wp.route) {
|
||||||
|
wp_bearing = bearing(fix, wp.route[routeidx]);
|
||||||
|
} else {
|
||||||
|
wp_bearing = bearing(fix, wp);
|
||||||
|
}
|
||||||
if (isNaN(wp_bearing)) wp_bearing = 0;
|
if (isNaN(wp_bearing)) wp_bearing = 0;
|
||||||
drawN();
|
draw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTimers() {
|
function startTimers() {
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
Bangle.setLCDPower(1);
|
if (W==240) Bangle.setLCDPower(1); // keep banglejs1 display on
|
||||||
read_heading();
|
read_heading();
|
||||||
}, 500);
|
}, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWaypointToMenu(menu, i) {
|
function addWaypointToMenu(menu, i) {
|
||||||
menu[waypoints[i].name] = function() {
|
menu[waypoints[i].name + (waypoints[i].route ? " (R)" : "")] = function() {
|
||||||
wp = waypoints[i];
|
wp = waypoints[i];
|
||||||
mainScreen();
|
mainScreen();
|
||||||
};
|
};
|
||||||
|
@ -243,7 +256,9 @@ function addWaypointToMenu(menu, i) {
|
||||||
function mainScreen() {
|
function mainScreen() {
|
||||||
E.showMenu();
|
E.showMenu();
|
||||||
candraw = true;
|
candraw = true;
|
||||||
drawAll(true);
|
g.setColor(0,0,0);
|
||||||
|
g.fillRect(0,0,W,H);
|
||||||
|
draw(true);
|
||||||
|
|
||||||
Bangle.setUI("updown", function(v) {
|
Bangle.setUI("updown", function(v) {
|
||||||
if (v === undefined) {
|
if (v === undefined) {
|
||||||
|
@ -262,11 +277,13 @@ function mainScreen() {
|
||||||
E.showMenu(menu);
|
E.showMenu(menu);
|
||||||
} else {
|
} else {
|
||||||
candraw = false;
|
candraw = false;
|
||||||
E.showPrompt("Delete waypoint: " + wp.name + "?").then(function(confirmed) {
|
var thing = wp.route ? "route" : "waypoint";
|
||||||
|
E.showPrompt("Delete " + thing + ": " + wp.name + "?").then(function(confirmed) {
|
||||||
var name = wp.name;
|
var name = wp.name;
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
var thing = wp.route ? "Route" : "Waypoint";
|
||||||
deleteWaypoint(wp);
|
deleteWaypoint(wp);
|
||||||
E.showAlert("Waypoint deleted: " + name).then(mainScreen);
|
E.showAlert(thing + " deleted: " + name).then(mainScreen);
|
||||||
} else {
|
} else {
|
||||||
mainScreen();
|
mainScreen();
|
||||||
}
|
}
|
||||||
|
@ -281,7 +298,6 @@ Bangle.on('kill',()=>{
|
||||||
});
|
});
|
||||||
|
|
||||||
g.clear();
|
g.clear();
|
||||||
Bangle.setLCDBrightness(1);
|
|
||||||
Bangle.setGPSPower(1);
|
Bangle.setGPSPower(1);
|
||||||
startTimers();
|
startTimers();
|
||||||
Bangle.on('GPS', onGPS);
|
Bangle.on('GPS', onGPS);
|
||||||
|
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 140 KiB |
|
@ -2,11 +2,11 @@
|
||||||
"id": "wpmoto",
|
"id": "wpmoto",
|
||||||
"name": "Waypointer Moto",
|
"name": "Waypointer Moto",
|
||||||
"shortName": "Waypointer Moto",
|
"shortName": "Waypointer Moto",
|
||||||
"version": "0.01",
|
"version": "0.02",
|
||||||
"description": "Waypoint-based motorcycle navigation aid",
|
"description": "Waypoint-based motorcycle navigation aid",
|
||||||
"icon": "wpmoto.png",
|
"icon": "wpmoto.png",
|
||||||
"tags": "tool,outdoors,gps",
|
"tags": "tool,outdoors,gps",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}],
|
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"interface": "wpmoto.html",
|
"interface": "wpmoto.html",
|
||||||
|
|
After Width: | Height: | Size: 125 KiB |
|
@ -1,198 +1,352 @@
|
||||||
<html>
|
<!doctype html>
|
||||||
<head>
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/css/ol.css" type="text/css">
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="anonymous">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/ol-geocoder@latest/dist/ol-geocoder.min.css" rel="stylesheet">
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css">
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h4>List of waypoints</h4>
|
<style type="text/css">
|
||||||
<table class="table">
|
html, body { height: 100% }
|
||||||
<thead>
|
.flex-col { display:flex; flex-direction:column; height:100% }
|
||||||
<tr>
|
#map { width:100%; height:100% }
|
||||||
<th>Name</th>
|
|
||||||
<th>Lat.</th>
|
/* https://stackoverflow.com/a/58686215 */
|
||||||
<th>Long.</th>
|
.arrow-icon {
|
||||||
<th>Actions</th>
|
width: 14px;
|
||||||
</tr>
|
height: 14px;
|
||||||
</thead>
|
}
|
||||||
<tbody id="waypoints">
|
.arrow-icon > div {
|
||||||
|
margin-left: -1px;
|
||||||
</tbody>
|
margin-top: -3px;
|
||||||
</table>
|
transform-origin: center center;
|
||||||
<br>
|
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
<h4>Add a new waypoint</h4>
|
}
|
||||||
<form id="add_waypoint_form">
|
</style>
|
||||||
<div class="columns">
|
</head>
|
||||||
<div class="column col-3 col-xs-8">
|
<body>
|
||||||
<input class="form-input input-sm" type="text" id="add_waypoint_name" placeholder="Name">
|
<div class="flex-col">
|
||||||
|
<div id="statusarea">
|
||||||
|
<button id="download" class="btn btn-error">Reload</button> <button id="upload" class="btn btn-primary">Upload</button>
|
||||||
|
<span id="status"></span>
|
||||||
|
<span id="routestatus"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column col-3 col-xs-8">
|
<div style="flex: 1">
|
||||||
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_latitude" placeholder="Lat">
|
<div id="map"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column col-3 col-xs-8">
|
</div>
|
||||||
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_longitude" placeholder="Long">
|
|
||||||
</div>
|
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin="anonymous"></script>
|
||||||
</div>
|
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
|
||||||
<div class="columns">
|
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
|
||||||
<div class="column col-3 col-xs-8">
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||||
<button id="add_waypoint_button" class="btn btn-primary btn-sm">Add Waypoint</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br>
|
|
||||||
<button id="Download" class="btn btn-error">Reload</button> <button id="Upload" class="btn btn-primary">Upload</button>
|
|
||||||
<br>
|
|
||||||
<div id="map" class="map" style="width:100%; height:400px"></div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/build/ol.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/ol-geocoder"></script>
|
|
||||||
<script src="../../core/lib/interface.js"></script>
|
<script src="../../core/lib/interface.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var map = new ol.Map({
|
var map;
|
||||||
target: 'map',
|
var waypoints = [];
|
||||||
layers: [
|
var mapmarkers = L.layerGroup();
|
||||||
new ol.layer.Tile({
|
var searchresult = L.layerGroup();
|
||||||
source: new ol.source.OSM()
|
var dynamicarrow = L.layerGroup();
|
||||||
})
|
var editingroute = null;
|
||||||
],
|
var lastroutepoint;
|
||||||
view: new ol.View({
|
|
||||||
center: ol.proj.fromLonLat([37.41, 8.82]),
|
|
||||||
zoom: 4
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
var geocoder = new Geocoder('nominatim', {
|
/*** map ***/
|
||||||
provider: 'osm',
|
|
||||||
lang: 'en-GB',
|
map = L.map('map').setView([51.505, -0.09], 8);
|
||||||
placeholder: 'Search...',
|
|
||||||
targetType: 'text-input',
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
});
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
map.addControl(geocoder);
|
subdomains: ['a','b','c'],
|
||||||
geocoder.on('addresschosen', function(e) {
|
}).addTo(map);
|
||||||
map.getView().animate({
|
|
||||||
center: e.coordinate,
|
L.Control.geocoder({defaultMarkGeocode: false}).addTo(map).on('markgeocode', function(e) {
|
||||||
zoom: Math.max(map.getView().getZoom(),16)
|
searchresult.clearLayers();
|
||||||
|
var bbox = e.geocode.bbox;
|
||||||
|
var poly = L.polygon([
|
||||||
|
bbox.getSouthEast(),
|
||||||
|
bbox.getNorthEast(),
|
||||||
|
bbox.getNorthWest(),
|
||||||
|
bbox.getSouthWest()
|
||||||
|
], {fill:false}).addTo(searchresult);
|
||||||
|
map.addLayer(searchresult);
|
||||||
|
map.fitBounds(poly.getBounds());
|
||||||
});
|
});
|
||||||
|
|
||||||
var lonlat = ol.proj.toLonLat(e.coordinate);
|
map.on('click', function(e) {
|
||||||
$longitude.value = lonlat[0];
|
if (editingroute != null) {
|
||||||
$latitude.value = lonlat[1];
|
searchresult.clearLayers();
|
||||||
});
|
addWaypoint(waypoints[editingroute].route, e.latlng.lat, e.latlng.lng, "");
|
||||||
|
} else {
|
||||||
|
swal({
|
||||||
|
icon: 'info',
|
||||||
|
text: "Enter a name for the waypoint:",
|
||||||
|
buttons: true,
|
||||||
|
content: 'input',
|
||||||
|
}).then((name) => {
|
||||||
|
if (name != null && name != "") {
|
||||||
|
searchresult.clearLayers();
|
||||||
|
addWaypoint(waypoints, e.latlng.lat, e.latlng.lng, name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.on('mousemove', function(e) {
|
||||||
|
if (editingroute == null) return;
|
||||||
|
let latlngs = [lastroutepoint, [e.latlng.lat, e.latlng.lng]];
|
||||||
|
dynamicarrow.clearLayers();
|
||||||
|
L.polyline(latlngs, { color:'black'}).addTo(dynamicarrow);
|
||||||
|
L.featureGroup(getArrows(latlngs, 'black', 2, map)).addTo(dynamicarrow)
|
||||||
|
map.addLayer(dynamicarrow);
|
||||||
|
});
|
||||||
|
clean();
|
||||||
|
renderAllWaypoints();
|
||||||
|
|
||||||
var waypoints = []
|
/*** status ***/
|
||||||
|
|
||||||
var $name = document.getElementById('add_waypoint_name')
|
function clean() {
|
||||||
var $form = document.getElementById('add_waypoint_form')
|
$('#status').html('<i class="icon icon-check"></i> No pending changes.');
|
||||||
var $button = document.getElementById('add_waypoint_button')
|
routestatus();
|
||||||
var $latitude = document.getElementById('add_latitude')
|
|
||||||
var $longitude = document.getElementById('add_longitude')
|
|
||||||
var $list = document.getElementById('waypoints')
|
|
||||||
|
|
||||||
map.on('click', function(e) {
|
|
||||||
var lonlat = ol.proj.toLonLat(e.coordinate);
|
|
||||||
$longitude.value = lonlat[0];
|
|
||||||
$latitude.value = lonlat[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
function compare(a, b){
|
|
||||||
var x = a.name.toLowerCase();
|
|
||||||
var y = b.name.toLowerCase();
|
|
||||||
if (x=="none") {return -1};
|
|
||||||
if (y=="none") {return 1};
|
|
||||||
if (x < y) {return -1;}
|
|
||||||
if (x > y) {return 1;}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$button.addEventListener('click', event => {
|
function dirty() {
|
||||||
event.preventDefault()
|
$('#status').html('<b><i class="icon icon-edit"></i> Changes have not been sent to the watch.</b>');
|
||||||
var name = $name.value.trim()
|
routestatus();
|
||||||
if(!name) return;
|
}
|
||||||
var lat = parseFloat($latitude.value).toPrecision(8);
|
|
||||||
var lon = parseFloat($longitude.value).toPrecision(8);
|
|
||||||
|
|
||||||
waypoints.push({
|
function routestatus() {
|
||||||
name, lat,lon,
|
if (editingroute == null) {
|
||||||
});
|
$('#routestatus').html('');
|
||||||
|
dynamicarrow.clearLayers();
|
||||||
waypoints.sort(compare);
|
} else {
|
||||||
|
$('#routestatus').html('Editing route: ' + escapeHTML(waypoints[editingroute].name) + ' <button class="btn btn-sm btn-primary" onclick="closeRoute()">close route</button>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderWaypoints()
|
/*** waypoints ***/
|
||||||
$name.value = ''
|
|
||||||
$latitude.value = (0).toPrecision(8);
|
|
||||||
$longitude.value = (0).toPrecision(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeWaypoint(index){
|
function addWaypoint(arr, lat, lon, name) {
|
||||||
$name.value = waypoints[index].name
|
arr.push({lat:lat, lon:lon, name:name});
|
||||||
if (waypoints[index].lat !== undefined && waypoints[index].lon !== undefined
|
renderAllWaypoints();
|
||||||
&& !isNaN(waypoints[index].lat) && !isNaN(waypoints[index].lon)) {
|
dirty();
|
||||||
$latitude.value = waypoints[index].lat
|
}
|
||||||
$longitude.value = waypoints[index].lon
|
|
||||||
map.getView().animate({
|
function deleteWaypoint(arr, i) {
|
||||||
center: ol.proj.fromLonLat([waypoints[index].lon, waypoints[index].lat]),
|
arr.splice(i, 1);
|
||||||
zoom: Math.max(map.getView().getZoom(),16)
|
if (editingroute != null) {
|
||||||
|
// XXX: ugly: fix editingroute index
|
||||||
|
if (editingroute == i) editingroute = null;
|
||||||
|
else if (editingroute > i) editingroute--;
|
||||||
|
}
|
||||||
|
renderAllWaypoints();
|
||||||
|
dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameWaypoint(arr, i) {
|
||||||
|
var name = prompt("Enter new name for the waypoint:", arr[i].name);
|
||||||
|
if (name == null || name == "" || name == arr[i].name)
|
||||||
|
return;
|
||||||
|
arr[i].name = name;
|
||||||
|
renderAllWaypoints();
|
||||||
|
dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWaypoints(wps, isroute, parentidx) {
|
||||||
|
var latlngs = [];
|
||||||
|
for (var i = 0; i < wps.length; i++) {
|
||||||
|
if (wps[i].route) {
|
||||||
|
renderWaypoints(wps[i].route, true, i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (wps[i].lat == null || wps[i].lon == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (isroute) {
|
||||||
|
L.marker([wps[i].lat, wps[i].lon], {title: wps[i].name})
|
||||||
|
.bindPopup(`<h4><b>${wps[i].name}</b> <a class="btn btn-primary" href="javascript:renameWaypoint(waypoints[${parentidx}].route,${i})"><i class="icon icon-edit"></i></a> <a class="btn btn-error" href="javascript:deleteWaypoint(waypoints[${parentidx}].route, ${i})"><i class="icon icon-delete"></i></a></h4><button class="btn btn-sm btn-error" onclick="javascript:deleteEntireRoute(${parentidx})">Delete entire route</button>`)
|
||||||
|
.addTo(mapmarkers);
|
||||||
|
latlngs.push([wps[i].lat, wps[i].lon]);
|
||||||
|
lastroutepoint = [wps[i].lat, wps[i].lon];
|
||||||
|
} else {
|
||||||
|
L.marker([wps[i].lat, wps[i].lon], {title: wps[i].name})
|
||||||
|
.bindPopup(`<h4><b>${wps[i].name}</b> <a class="btn btn-primary" href="javascript:renameWaypoint(waypoints,${i})"><i class="icon icon-edit"></i></a> <a class="btn btn-error" href="javascript:deleteWaypoint(waypoints, ${i})"><i class="icon icon-delete"></i></a></h4><button class="btn btn-sm btn-primary" onclick="javascript:makeRoute(${i})">Make route</button>`)
|
||||||
|
.addTo(mapmarkers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isroute) {
|
||||||
|
L.polyline(latlngs, { color:'black'}).addTo(mapmarkers);
|
||||||
|
L.featureGroup(getArrows(latlngs, 'black', 2, map)).addTo(mapmarkers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAllWaypoints() {
|
||||||
|
mapmarkers.clearLayers();
|
||||||
|
renderWaypoints(waypoints, false, 0);
|
||||||
|
map.addLayer(mapmarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** routes ***/
|
||||||
|
|
||||||
|
function openRoute(i) {
|
||||||
|
editingroute = i;
|
||||||
|
console.log("edit route "+ i);
|
||||||
|
map.on('contextmenu', closeRoute);
|
||||||
|
renderAllWaypoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRoute(i) {
|
||||||
|
waypoints[i].route = [{
|
||||||
|
name: waypoints[i].name,
|
||||||
|
lat: waypoints[i].lat,
|
||||||
|
lon: waypoints[i].lon,
|
||||||
|
}];
|
||||||
|
openRoute(i);
|
||||||
|
dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRoute() {
|
||||||
|
map.off('contextmenu', closeRoute);
|
||||||
|
editingroute = null;
|
||||||
|
renderAllWaypoints();
|
||||||
|
routestatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntireRoute(i) {
|
||||||
|
swal({
|
||||||
|
icon: 'warning',
|
||||||
|
text: "Really delete entire route '" + waypoints[i].name + "'?",
|
||||||
|
buttons: true,
|
||||||
|
}).then((v) => {
|
||||||
|
console.log(v)
|
||||||
|
if (v) {
|
||||||
|
deleteWaypoint(waypoints, i);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
waypoints = waypoints.filter((p,i) => i!==index)
|
|
||||||
renderWaypoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWaypoints(){
|
/*** util ***/
|
||||||
$list.innerHTML = ''
|
|
||||||
waypoints.forEach((waypoint,index) => {
|
|
||||||
var $waypoint = document.createElement('tr')
|
|
||||||
if (index==0){
|
|
||||||
$waypoint.innerHTML = `<td>${waypoint.name}</td>`
|
|
||||||
} else if(waypoint.lat==undefined){
|
|
||||||
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>------</td><td>-----</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
|
|
||||||
} else {
|
|
||||||
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>${waypoint.lat}</td><td>${waypoint.lon}</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
|
|
||||||
}
|
|
||||||
$list.appendChild($waypoint)
|
|
||||||
})
|
|
||||||
$name.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadJSONfile(fileid, callback) {
|
// https://stackoverflow.com/a/22706073
|
||||||
Puck.write(`\x10(function() {
|
function escapeHTML(str){
|
||||||
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
|
return new Option(str).innerHTML;
|
||||||
Bluetooth.print(JSON.stringify(pts));
|
}
|
||||||
})()\n`,contents=>{
|
|
||||||
var storedpts = JSON.parse(contents);
|
/*** Bangle.js ***/
|
||||||
callback(storedpts);
|
|
||||||
|
function downloadJSONfile(fileid, callback) {
|
||||||
|
Puck.write(`\x10(function() {
|
||||||
|
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
|
||||||
|
Bluetooth.print(JSON.stringify(pts));
|
||||||
|
})()\n`, contents => {
|
||||||
|
var storedpts = JSON.parse(contents);
|
||||||
|
callback(storedpts);
|
||||||
|
clean();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile(fileid, contents) {
|
||||||
|
Puck.write(`\x10(function() {
|
||||||
|
require("Storage").write("${fileid}",'${contents}');
|
||||||
|
Bluetooth.print("OK");
|
||||||
|
})()\n`, ret => {
|
||||||
|
console.log("uploadFile", ret);
|
||||||
|
if (ret == "OK")
|
||||||
|
clean();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotStored(pts) {
|
||||||
|
waypoints = pts;
|
||||||
|
|
||||||
|
var latlngs = waypoints.map(p => [p.lat, p.lon]);
|
||||||
|
var poly = L.polygon(latlngs);
|
||||||
|
map.fitBounds(poly.getBounds());
|
||||||
|
|
||||||
|
renderAllWaypoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
downloadJSONfile("waypoints.json", gotStored);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#download').on('click', function() {
|
||||||
|
downloadJSONfile("waypoints.json", gotStored);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function uploadFile(fileid, contents) {
|
$('#upload').click(function() {
|
||||||
Puck.write(`\x10(function() {
|
var data = JSON.stringify(waypoints);
|
||||||
require("Storage").write("${fileid}",'${contents}');
|
uploadFile("waypoints.json",data);
|
||||||
Bluetooth.print("OK");
|
|
||||||
})()\n`,ret=>{
|
|
||||||
console.log("uploadFile",ret);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function gotStored(pts){
|
$('#statusarea').click(closeRoute);
|
||||||
waypoints = pts;
|
|
||||||
renderWaypoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInit() {
|
/*** map arrows ***/
|
||||||
downloadJSONfile("waypoints.json", gotStored);
|
// https://stackoverflow.com/a/58686215
|
||||||
}
|
function getArrows(arrLatlngs, color, arrowCount, mapObj) {
|
||||||
|
if (typeof arrLatlngs === undefined || arrLatlngs == null ||
|
||||||
|
(!arrLatlngs.length) || arrLatlngs.length < 2)
|
||||||
|
return [];
|
||||||
|
|
||||||
document.getElementById("Download").addEventListener("click", function() {
|
if (typeof arrowCount === 'undefined' || arrowCount == null)
|
||||||
downloadJSONfile("waypoints.json", gotStored);
|
arrowCount = 1;
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("Upload").addEventListener("click", function() {
|
if (typeof color === 'undefined' || color == null)
|
||||||
var data = JSON.stringify(waypoints);
|
color = '';
|
||||||
uploadFile("waypoints.json",data);
|
else
|
||||||
});
|
color = 'color:' + color;
|
||||||
|
|
||||||
|
var result = [];
|
||||||
|
for (var i = 1; i < arrLatlngs.length; i++) {
|
||||||
|
var icon = L.divIcon({ className: 'arrow-icon', bgPos: [5, 5], html: '<div style="' + color + ';transform: rotate(' + getAngle(arrLatlngs[i - 1], arrLatlngs[i], -1).toString() + 'deg)">▶</div>' });
|
||||||
|
for (var c = 1; c <= arrowCount; c++) {
|
||||||
|
result.push(L.marker(myMidPoint(arrLatlngs[i], arrLatlngs[i - 1], (c / (arrowCount + 1)), mapObj), { icon: icon }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAngle(latLng1, latlng2, coef) {
|
||||||
|
var dy = latlng2[0] - latLng1[0];
|
||||||
|
var dx = Math.cos(Math.PI / 180 * latLng1[0]) * (latlng2[1] - latLng1[1]);
|
||||||
|
var ang = ((Math.atan2(dy, dx) / Math.PI) * 180 * coef);
|
||||||
|
return (ang).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function myMidPoint(latlng1, latlng2, per, mapObj) {
|
||||||
|
if (!mapObj)
|
||||||
|
throw new Error('map is not defined');
|
||||||
|
|
||||||
|
var halfDist, segDist, dist, p1, p2, ratio,
|
||||||
|
points = [];
|
||||||
|
|
||||||
|
p1 = mapObj.project(new L.latLng(latlng1));
|
||||||
|
p2 = mapObj.project(new L.latLng(latlng2));
|
||||||
|
|
||||||
|
halfDist = distanceTo(p1, p2) * per;
|
||||||
|
|
||||||
|
if (halfDist === 0)
|
||||||
|
return mapObj.unproject(p1);
|
||||||
|
|
||||||
|
dist = distanceTo(p1, p2);
|
||||||
|
|
||||||
|
if (dist > halfDist) {
|
||||||
|
ratio = (dist - halfDist) / dist;
|
||||||
|
var res = mapObj.unproject(new Point(p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y)));
|
||||||
|
return [res.lat, res.lng];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function distanceTo(p1, p2) {
|
||||||
|
var x = p2.x - p1.x,
|
||||||
|
y = p2.y - p1.y;
|
||||||
|
|
||||||
|
return Math.sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Point(x, y, round) {
|
||||||
|
this.x = (round ? Math.round(x) : x);
|
||||||
|
this.y = (round ? Math.round(y) : y);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
|
2
core
|
@ -1 +1 @@
|
||||||
Subproject commit 187af1527e0b830c804049aae834ed658ffeed08
|
Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b
|