Merge remote-tracking branch 'upstream/master'

master
Andreas Rozek 2022-01-12 14:56:15 +01:00
commit 0d8efbe015
25 changed files with 773 additions and 180 deletions

View File

@ -99,18 +99,20 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.05",
"version": "0.06",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications",
"dependencies": {"messages":"app"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"android.app.js","url":"app.js"},
{"name":"android.settings.js","url":"settings.js"},
{"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"}
],
"data": [{"name":"android.settings.json"}],
"sortorder": -8
},
{
@ -167,7 +169,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.40",
"version": "0.41",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
@ -1534,13 +1536,14 @@
{
"id": "assistedgps",
"name": "Assisted GPS Update (AGPS)",
"version": "0.01",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"version": "0.02",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"icon": "app.png",
"type": "RAM",
"tags": "tool,outdoors,agps",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": []
},
{
@ -4515,7 +4518,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.11",
"version":"0.12",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
@ -5540,21 +5543,25 @@
{"name":"limelight.img","url":"limelight.icon.js","evaluate":true}
]
},
{ "id": "configurable_clock",
"name": "Configurable Analog Clock",
"shortName":"Configurable Clock",
"version":"0.02",
"description": "an analog clock with several kinds of faces, hands and colors to choose from",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
{ "id": "banglexercise",
"name": "BanglExercise",
"shortName":"BanglExercise",
"version":"0.01",
"description": "Can automatically track exercises while wearing the Bangle.js watch.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "sport",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"configurable_clock.app.js","url":"app.js"},
{"name":"configurable_clock.img","url":"app-icon.js","evaluate":true}
]
{"name":"banglexercise.app.js","url":"app.js"},
{"name":"banglexercise.img","url":"app-icon.js","evaluate":true},
{"name":"banglexercise.settings.js","url":"settings.js"}
],
"data": [
{"name":"banglexercise.json"}
]
}
]

View File

@ -4,3 +4,4 @@
0.03: Handling of message actions (ok/clear)
0.04: Android icon now goes to settings page with 'find phone'
0.05: Fix handling of message actions
0.06: Option to keep messages after a disconnect (default false) (fix #1186)

48
apps/android/README.md Normal file
View File

@ -0,0 +1,48 @@
# Android Integration
This app allows your Bangle.js to receive notifications [from the Gadgetbridge app on Android](http://www.espruino.com/Gadgetbridge)
See [this link](http://www.espruino.com/Gadgetbridge) for notes on how to install
the Android app (and how it works).
It requires the `Messages` app on Bangle.js (which should be automatically installed) to
display any notifications that are received.
## Settings
You can access the settings menu either from the `Android` icon in the launcher,
or from `App Settings` in the `Settings` menu.
It contains:
* `Connected` - shows whether there is an active Bluetooth connection or not
* `Find Phone` - opens a submenu where you can activate the `Find Phone` functionality
of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them?
* `Messages` - launches the messages app, showing a list of messages
## How it works
Gadgetbridge on Android connects to Bangle.js, and sends commands over the
BLE UART connection. These take the form of `GB({ ... JSON ... })\n` - so they
call a global function called `GB` which then interprets the JSON.
Responses are sent back to Gadgetbridge simply as one line of JSON.
More info on message formats on http://www.espruino.com/Gadgetbridge
## Testing
Bangle.js can only hold one connection open at a time, so it's hard to see
if there are any errors when handling Gadgetbridge messages.
However you can:
* Use the `Gadgetbridge Debug` app on Bangle.js to display/log the messages received from Gadgetbridge
* Connect with the Web IDE and manually enter the Gadgetbridge messages on the left-hand side to
execute them as if they came from Gadgetbridge, for instance:
```
GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"})
```

View File

@ -4,6 +4,7 @@
Bluetooth.println(JSON.stringify(message));
}
var settings = require("Storage").readJSON("android.settings.json",1)||{};
var _GB = global.GB;
global.GB = (event) => {
// feed a copy to other handlers if there were any
@ -51,7 +52,8 @@
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); }
NRF.on("connect", () => setTimeout(sendBattery, 2000));
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
setInterval(sendBattery, 10*60*1000);
// Health tracking
Bangle.on('health', health=>{
@ -68,4 +70,6 @@
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here?
};
// remove settings object so it's not taking up RAM
delete settings;
})();

View File

@ -2,17 +2,29 @@
function gb(j) {
Bluetooth.println(JSON.stringify(j));
}
var settings = require("Storage").readJSON("android.settings.json",1)||{};
function updateSettings() {
require("Storage").writeJSON("android.settings.json", settings);
}
var mainmenu = {
"" : { "title" : "Android" },
"< Back" : back,
"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
"Find Phone" : () => E.showMenu({
"" : { "title" : "Find Phone" },
"< Back" : ()=>E.showMenu(mainmenu),
"On" : _=>gb({t:"findPhone",n:true}),
"Off" : _=>gb({t:"findPhone",n:false}),
/*LANG*/"On" : _=>gb({t:"findPhone",n:true}),
/*LANG*/"Off" : _=>gb({t:"findPhone",n:false}),
}),
"Messages" : ()=>load("messages.app.js")
/*LANG*/"Keep Msgs" : {
value : !!settings.keep,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.keep = v;
updateSettings();
}
},
/*LANG*/"Messages" : ()=>load("messages.app.js")
};
E.showMenu(mainmenu);
})

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Update to work with Bangle.js 2

View File

@ -8,34 +8,47 @@
<p>GPS can take a long time (~5 minutes) to get an accurate position the first time it is used.
AGPS uploads a few hints to the GPS receiver about satellite positions that allow it
to get a faster, more accurate fix - however they are only valid for a short period of time.</p>
<p>You can upload data that covers a longer period of time, but the upload will take longer.</p>
<div class="form-group">
<label class="form-label">AGPS Validity time</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1d"><i class="form-icon"></i> 1 day (8kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="2d" checked><i class="form-icon"></i> 2 days (14kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="3d"><i class="form-icon"></i> 3 days (20kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1wk"><i class="form-icon"></i> 1 week (46kB)
</label>
<div id="banglejs1-info" style="display:none">
<p>You can upload data that covers a longer period of time, but the upload will take longer.</p>
<div class="form-group">
<label class="form-label">AGPS Validity time</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1d"><i class="form-icon"></i> 1 day (8kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="2d" checked><i class="form-icon"></i> 2 days (14kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="3d"><i class="form-icon"></i> 3 days (20kB)
</label>
<label class="form-radio">
<input type="radio" name="agpsperiod" value="1wk"><i class="form-icon"></i> 1 week (46kB)
</label>
</div>
</div>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<div id="banglejs2-info" style="display:none">
</div>
<p id="upload-wrap" style="display:none">Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../core/lib/customize.js"></script>
<script>
var isB1; // is Bangle.js 1?
var isB2; // is Bangle.js 2?
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
var radios = document.getElementsByName('agpsperiod');
var url = "https://www.espruino.com/agps/assistnow_1d.base64";
for (var i=0; i<radios.length; i++)
if (radios[i].checked)
url = "https://www.espruino.com/agps/assistnow_"+radios[i].value+".base64";
var url;
if (isB1) {
var radios = document.getElementsByName('agpsperiod');
url = "https://www.espruino.com/agps/assistnow_1d.base64";
for (var i=0; i<radios.length; i++)
if (radios[i].checked)
url = "https://www.espruino.com/agps/assistnow_"+radios[i].value+".base64";
}
if (isB2) {
url = "https://www.espruino.com/agps/casic.base64";
}
console.log("Sending...");
//var text = document.getElementById("agpsperiod").value;
get(url, function(b64) {
@ -48,6 +61,8 @@
});
});
// =================================================== Bangle.js 1 UBLOX
function UBX_CMD(cmd) {
var d = [0xB5,0x62]; // sync chars
d = d.concat(cmd);
@ -79,13 +94,35 @@
return UBX_CMD([].slice.call(a));
}
// =================================================== Bangle.js 2 CASIC
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i=1;i<cmd.length;i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
}
// ===================================================
function jsFromBase64(b64) {
var bin = atob(b64);
var chunkSize = 128;
var js = "\x10Bangle.setGPSPower(1);\n"; // turn GPS on
//js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`;
//js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
if (isB1) { // UBLOX
//js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`;
//js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
}
if (isB2) { // CASIC
// Disable BDS, use just GPS (supposedly improve lock time)
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,1")}")\n`; // set GPS-only mode
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
}
for (var i=0;i<bin.length;i+=chunkSize) {
var chunk = bin.substr(i,chunkSize);
@ -106,6 +143,15 @@
oReq.send();
}
// Called when we know what device we're using
function onInit(device) {
isB2 = (device && device.id=="BANGLEJS2");
isB1 = !isB2;
document.getElementById("banglejs1-info").style = isB1?"":"display:none";
document.getElementById("banglejs2-info").style = isB2?"":"display:none";
document.getElementById("upload-wrap").style = "";
}
</script>
</body>
</html>

View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1,40 @@
# BanglExercise
Can automatically track exercises while wearing the Bangle.js watch.
Currently only push ups and curls are supported.
## Disclaimer
This app is experimental but it seems to work quiet reliable for me.
It could be and is likely that the threshold values for detecting exercises do not work for everyone.
Therefore it would be great if we could improve this app together :-)
## Usage
Select the exercise type you want to practice and go for it!
Press stop to end your exercise.
## Screenshots
![](screenshot.png)
## TODO
* Add other exercise types:
* Rope jumps
* Sit ups
* ...
* Save exercise summaries to file system
* Configure daily goal for exercises
* Find a nicer icon
## Contribute
Feel free to send in improvements and remarks.
## Creator
Marco ([myxor](https://github.com/myxor))
## Icons
Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIbYh/8AYM/+EP/wFBv4FB/4FB/4FHAwIEBAv4FPAgIGCAosHAofggYFD4EABgXgOgIFLDAQWBAo0BAoOAVIV/UYQABj/4AocDCwQFTg46CEY4vFAopBBApIAVA=="))

362
apps/banglexercise/app.js Normal file
View File

@ -0,0 +1,362 @@
const Layout = require("Layout");
const heatshrink = require('heatshrink');
const storage = require('Storage');
let tStart;
let historyY = [];
let historyZ = [];
let historyAvgY = [];
let historyAvgZ = [];
let historySlopeY = [];
let historySlopeZ = [];
let lastZeroPassCameFromPositive;
let lastZeroPassTime = 0;
let lastExerciseCompletionTime = 0;
let lastExerciseHalfCompletionTime = 0;
let exerciseType = {
"id": "",
"name": ""
};
// add new exercises here:
const exerciseTypes = [{
"id": "pushup",
"name": "push ups",
"useYaxe": true,
"useZaxe": false,
"thresholdY": 2500,
"thresholdMinTime": 1400, // mininmal time between two push ups in ms
"thresholdMaxTime": 5000, // maximal time between two push ups in ms
"thresholdMinDurationTime": 700, // mininmal duration of half a push ups in ms
},
{
"id": "curl",
"name": "curls",
"useYaxe": true,
"useZaxe": false,
"thresholdY": 2500,
"thresholdMinTime": 1000, // mininmal time between two curls in ms
"thresholdMaxTime": 5000, // maximal time between two curls in ms
"thresholdMinDurationTime": 500, // mininmal duration of half a push ups in ms
}
];
let exerciseCounter = 0;
let layout;
let recordActive = false;
// Size of average window for data analysis
const avgSize = 6;
let hrtValue;
let settings = storage.readJSON("banglexercise.json", 1) || {
'buzz': true
};
function showMainMenu() {
let menu;
menu = {
"": {
title: "BanglExercise"
}
};
exerciseTypes.forEach(function(et) {
menu["Do " + et.name] = function() {
exerciseType = et;
E.showMenu();
startTraining();
};
});
if (exerciseCounter > 0) {
menu["--------"] = {
value: ""
};
menu["Last:"] = {
value: exerciseCounter + " " + exerciseType.name
};
}
menu.Exit = function() {
load();
};
E.showMenu(menu);
}
function accelHandler(accel) {
if (!exerciseType) return;
const t = Math.round(new Date().getTime()); // time in ms
const y = exerciseType.useYaxe ? accel.y * 8192 : 0;
const z = exerciseType.useZaxe ? accel.z * 8192 : 0;
//console.log(t, y, z);
if (exerciseType.useYaxe) {
while (historyY.length > avgSize)
historyY.shift();
historyY.push(y);
if (historyY.length > avgSize / 2) {
const avgY = E.sum(historyY) / historyY.length;
historyAvgY.push([t, avgY]);
while (historyAvgY.length > avgSize)
historyAvgY.shift();
}
}
if (exerciseType.useYaxe) {
while (historyZ.length > avgSize)
historyZ.shift();
historyZ.push(z);
if (historyZ.length > avgSize / 2) {
const avgZ = E.sum(historyZ) / historyZ.length;
historyAvgZ.push([t, avgZ]);
while (historyAvgZ.length > avgSize)
historyAvgZ.shift();
}
}
// slope for Y
if (exerciseType.useYaxe) {
let l = historyAvgY.length;
if (l > 1) {
const p1 = historyAvgY[l - 2];
const p2 = historyAvgY[l - 1];
const slopeY = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000);
// we use this data for exercises which can be detected by using Y axis data
switch (exerciseType.id) {
case "pushup":
isValidYAxisExercise(slopeY, t);
break;
case "curl":
isValidYAxisExercise(slopeY, t);
break;
}
}
}
// slope for Z
if (exerciseType.useZaxe) {
l = historyAvgZ.length;
if (l > 1) {
const p1 = historyAvgZ[l - 2];
const p2 = historyAvgZ[l - 1];
const slopeZ = (p2[1] - p1[1]) / (p2[0] - p1[0]);
historyAvgZ.shift();
historySlopeZ.push([p2[0] - p1[0], slopeZ]);
// TODO: we can use this data for some exercises which can be detected by using Z axis data
}
}
}
/*
* Check if slope value of Y-axis data looks like an exercise
*
* In detail we look for slop values which are bigger than the configured Y threshold for the current exercise
* Then we look for two consecutive slope values of which one is above 0 and the other is below zero.
* If we find one pair of these values this could be part of one exercise.
* Then we look for a pair of values which cross the zero from the otherwise direction
*/
function isValidYAxisExercise(slopeY, t) {
if (!exerciseType) return;
const thresholdY = exerciseType.thresholdY;
const thresholdMinTime = exerciseType.thresholdMinTime;
const thresholdMaxTime = exerciseType.thresholdMaxTime;
const thresholdMinDurationTime = exerciseType.thresholdMinDurationTime;
const exerciseName = exerciseType.name;
if (Math.abs(slopeY) >= thresholdY) {
historyAvgY.shift();
historySlopeY.push([t, slopeY]);
//console.log(t, Math.abs(slopeY));
const lSlopeY = historySlopeY.length;
if (lSlopeY > 1) {
const p1 = historySlopeY[lSlopeY - 1][1];
const p2 = historySlopeY[lSlopeY - 2][1];
if (p1 > 0 && p2 < 0) {
if (lastZeroPassCameFromPositive == false) {
lastExerciseHalfCompletionTime = t;
//console.log(t, exerciseName + " half complete...");
layout.progress.label = "½";
g.clear();
layout.render();
}
lastZeroPassCameFromPositive = true;
lastZeroPassTime = t;
}
if (p2 > 0 && p1 < 0) {
if (lastZeroPassCameFromPositive == true) {
const tDiffLastExercise = t - lastExerciseCompletionTime;
const tDiffStart = t - tStart;
//console.log(t, exerciseName + " maybe complete?", Math.round(tDiffLastExercise), Math.round(tDiffStart));
// check minimal time between exercises:
if ((lastExerciseCompletionTime <= 0 && tDiffStart >= thresholdMinTime) || tDiffLastExercise >= thresholdMinTime) {
// check maximal time between exercises:
if (lastExerciseCompletionTime <= 0 || tDiffLastExercise <= thresholdMaxTime) {
// check minimal duration of exercise:
const tDiffExerciseHalfCompletion = t - lastExerciseHalfCompletionTime;
if (tDiffExerciseHalfCompletion > thresholdMinDurationTime) {
//console.log(t, exerciseName + " complete!!!");
lastExerciseCompletionTime = t;
exerciseCounter++;
layout.count.label = exerciseCounter;
layout.progress.label = "";
g.clear();
layout.render();
if (settings.buzz)
Bangle.buzz(100, 0.4);
} else {
//console.log(t, exerciseName + " to quick for duration time threshold!");
lastExerciseCompletionTime = t;
}
} else {
//console.log(t, exerciseName + " to slow for time threshold!");
lastExerciseCompletionTime = t;
}
} else {
//console.log(t, exerciseName + " to quick for time threshold!");
lastExerciseCompletionTime = t;
}
}
lastZeroPassCameFromPositive = false;
lastZeroPassTime = t;
}
}
}
}
function reset() {
historyY = [];
historyZ = [];
historyAvgY = [];
historyAvgZ = [];
historySlopeY = [];
historySlopeZ = [];
lastZeroPassCameFromPositive = undefined;
lastZeroPassTime = 0;
lastExerciseHalfCompletionTime = 0;
lastExerciseCompletionTime = 0;
exerciseCounter = 0;
tStart = 0;
}
function startTraining() {
if (recordActive) return;
g.clear(1);
reset();
Bangle.setHRMPower(1, "banglexercise");
if (!hrtValue) hrtValue = "...";
layout = new Layout({
type: "v",
c: [{
type: "txt",
id: "type",
font: "6x8:2",
label: exerciseType.name,
pad: 5
},
{
type: "h",
c: [{
type: "txt",
id: "count",
font: exerciseCounter < 100 ? "6x8:9" : "6x8:8",
label: 10,
pad: 5
},
{
type: "txt",
id: "progress",
font: "6x8:2",
label: "",
pad: 5
},
]
},
{
type: "h",
c: [{
type: "img",
pad: 4,
src: function() {
return heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
}
},
{
type: "txt",
id: "hrtRate",
font: "6x8:2",
label: hrtValue,
pad: 5
},
]
},
{
type: "txt",
id: "recording",
font: "6x8:2",
label: "TRAINING",
bgCol: "#f00",
pad: 5,
fillx: 1
},
]
}, {
btns: [{
label: "STOP",
cb: () => {
stopTraining();
}
}],
lazy: false
});
layout.render();
Bangle.setPollInterval(80); // 12.5 Hz
Bangle.on('accel', accelHandler);
tStart = new Date().getTime();
recordActive = true;
if (settings.buzz)
Bangle.buzz(200, 1);
}
function stopTraining() {
if (!recordActive) return;
g.clear(1);
Bangle.removeListener('accel', accelHandler);
Bangle.setHRMPower(0, "banglexercise");
showMainMenu();
recordActive = false;
}
Bangle.on('HRM', function(hrm) {
hrtValue = hrm.bpm;
});
g.clear(1);
showMainMenu();

BIN
apps/banglexercise/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,21 @@
(function(back) {
const SETTINGS_FILE = "banglexercise.json";
const storage = require('Storage');
let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
function save(key, value) {
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
E.showMenu({
'': { 'title': 'BanglExercise' },
'< Back': back,
'Buzz': {
value: "buzz" in settings ? settings.buzz : false,
format: () => (settings.buzz ? 'Yes' : 'No'),
onchange: () => {
settings.buzz = !settings.buzz;
save('buzz', settings.buzz);
}
}
});
});

View File

@ -7,5 +7,6 @@
0.07: Added settings to adjust data that is shown for each row.
0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode.
0.09: Tab anywhere to open the launcher.
0.10: Fix - Clock is unresponsive, if gadgetbridge connects.
0.11: Added getting the gadgetbridge weather
0.10: Removed swipes to be compatible with the Pattern Launcher. Stability improvements.
0.11: Show the gadgetbridge weather temperature (settings).
0.12: Added humidity to data.

View File

@ -4,20 +4,28 @@ A simple LCARS inspired clock.
Note: To display the steps, the health app is required. If this app is not installed, the data will not be shown.
To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerdavid/BangleApps)
## Control
* Tap left / right to change between screens.
* Tap top / bottom to control the current screen.
## Features
* LCARS Style watch face.
* Full screen mode - widgets are still loaded.
* Supports multiple screens with different data.
* Tab anywhere to open the launcher.
* [Screen 1] Date + Time + Lock status.
* [Screen 1] Shows randomly images of real planets.
* [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.)
* [Screen 1] Swipe up/down to activate an alarm.
* [Screen 1] Shows 3 customizable datapoints on the first screen.
* [Screen 1] The lower orange line indicates the battery level.
* [Screen 2] Display graphs for steps + hrm on the second screen.
* [Screen 2] Switch between day/month via swipe up/down.
* Full screen mode - widgets are still loaded but not shown.
* Tab on left/right to switch between different screens.
* Cusomizable data that is shown on screen 1 (steps, weather etc.)
* Shows random images of real planets.
* Tap on top/bottom of screen 1 to activate an alarm.
* The lower orange line indicates the battery level.
* Display graphs for steps + hrm on the second screen.
## Data that can be configured
* Steps - Steps loaded via the health module
* Battery - Current battery level in %
* VREF - Voltage of battery
* HRM - Last measured HRM
* Temp - Weather temperature loaded via the weather module + gadgetbridge
* Humidity - Humidity loaded via the weather module + gadgetbridge
* CoreT - Temperature of device
## Multiple screens support
Access different screens via swipe left/ right
@ -26,10 +34,7 @@ Access different screens via swipe left/ right
![](screenshot_2.png)
## Icons
<div>Icons made by <a href="https://www.flaticon.com/authors/smashicons" title="Smashicons">Smashicons</a>, <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
## Contributors
- Creator: [David Peer](https://github.com/peerdavid).
- Initial creation and improvements: [David Peer](https://github.com/peerdavid).
- Improvements: [Adam Schmalhofer](https://github.com/adamschmalhofer).
- Improvements: [Jon Warrington](https://github.com/BartokW).

View File

@ -1,16 +1,11 @@
const SETTINGS_FILE = "lcars.setting.json";
const Storage = require("Storage");
const weather = require('weather');
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const locale = require('locale');
const storage = require('Storage')
let settings = {
alarm: -1,
dataRow1: "Battery",
dataRow2: "Steps",
dataRow3: "Temp."
dataRow1: "Steps",
dataRow2: "Temp",
dataRow3: "Battery"
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -33,13 +28,13 @@ let cGrey = "#9E9E9E";
let lcarsViewPos = 0;
let drag;
let hrmValue = 0;
var plotWeek = false;
var plotMonth = false;
var disableInfoUpdate = true; // When gadgetbridge connects, step infos cannot be loaded
/*
* Requirements and globals
*/
const locale = require('locale');
var bgLeft = {
width : 27, height : 176, bpp : 3,
@ -123,37 +118,35 @@ function queueDraw() {
function printData(key, y, c){
g.setFontAlign(-1,-1,0);
var text = "ERR";
var value = "NOT FOUND";
key = key.toUpperCase()
var text = key;
var value = "ERR";
if(key == "Battery"){
text = "BAT";
value = E.getBattery() + "%";
} else if(key == "Steps"){
if(key == "STEPS"){
text = "STEP";
value = getSteps();
} else if(key == "Temp."){
text = "TEMP";
value = Math.floor(E.getTemperature()) + "C";
} else if(key == "HRM"){
text = "HRM";
value = hrmValue;
} else if(key == "BATTERY"){
text = "BAT";
value = E.getBattery() + "%";
} else if (key == "VREF"){
text = "VREF";
value = E.getAnalogVRef().toFixed(2) + "V";
} else if (key == "Weather"){
text = "TEMP";
const w = weather.get();
if (!w) {
value = "ERR";
} else {
value = require('locale').temp(w.temp-273.15); // applies conversion
}
} else if(key == "HRM"){
value = hrmValue;
} else if (key == "TEMP"){
var weather = getWeather();
value = weather.temp;
} else if (key == "HUMIDITY"){
text = "HUM";
var weather = getWeather();
value = parseInt(weather.hum) + "%";
} else if(key == "CORET"){
value = locale.temp(parseInt(E.getTemperature()));
}
g.setColor(c);
@ -309,7 +302,7 @@ function drawPosition1(){
}
// Plot HRM graph
if(plotWeek){
if(plotMonth){
var data = new Uint16Array(32);
var cnt = new Uint8Array(32);
health.readDailySummaries(new Date(), h=>{
@ -346,8 +339,8 @@ function drawPosition1(){
g.setFontAlign(1, 1, 0);
g.setFontAntonioMedium();
g.setColor(cWhite);
g.drawString("WEEK HRM", 154, 27);
g.drawString("WEEK STEPS [K]", 154, 115);
g.drawString("M-HRM", 154, 27);
g.drawString("M-STEPS [K]", 154, 115);
// Plot day
} else {
@ -387,8 +380,8 @@ function drawPosition1(){
g.setFontAlign(1, 1, 0);
g.setFontAntonioMedium();
g.setColor(cWhite);
g.drawString("DAY HRM", 154, 27);
g.drawString("DAY STEPS", 154, 115);
g.drawString("D-HRM", 154, 27);
g.drawString("D-STEPS", 154, 115);
}
}
@ -429,6 +422,32 @@ function getSteps() {
}
function getWeather(){
var weather;
try {
weather = require('weather').get();
} catch(ex) {
// Return default
}
if (weather === undefined){
weather = {
temp: "-",
hum: "-",
txt: "-",
wind: "-",
wdir: "-",
wrose: "-"
};
} else {
weather.temp = locale.temp(parseInt(weather.temp-273.15))
}
return weather;
}
/*
* Handle alarm
*/
@ -467,7 +486,7 @@ function handleAlarm(){
.then(() => {
// Update alarm state to disabled
settings.alarm = -1;
Storage.writeJSON(SETTINGS_FILE, settings);
storage.writeJSON(SETTINGS_FILE, settings);
});
}
@ -507,7 +526,7 @@ function increaseAlarm(){
settings.alarm = getCurrentTimeInMinutes() + 5;
}
Storage.writeJSON(SETTINGS_FILE, settings);
storage.writeJSON(SETTINGS_FILE, settings);
}
@ -518,7 +537,7 @@ function decreaseAlarm(){
settings.alarm = -1;
}
Storage.writeJSON(SETTINGS_FILE, settings);
storage.writeJSON(SETTINGS_FILE, settings);
}
function feedback(){
@ -562,9 +581,9 @@ Bangle.on('touch', function(btn, e){
drawState();
return;
}
} else if (lcarsViewPos == 1 && (is_upper || is_lower) && plotWeek != is_lower){
} else if (lcarsViewPos == 1 && (is_upper || is_lower) && plotMonth != is_lower){
feedback();
plotWeek = is_lower;
plotMonth = is_lower;
draw();
return;
}

View File

@ -7,7 +7,7 @@
alarm: -1,
dataRow1: "Battery",
dataRow2: "Steps",
dataRow3: "Temp."
dataRow3: "Temp"
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -18,14 +18,14 @@
storage.write(SETTINGS_FILE, settings)
}
var data_options = ["Battery", "Steps", "Temp.", "HRM", "VREF", "Weather"];
var data_options = ["Steps", "Battery", "VREF", "HRM", "Temp", "Humidity", "CoreT"];
E.showMenu({
'': { 'title': 'LCARS Clock' },
'< Back': back,
'Row 1': {
value: 0 | data_options.indexOf(settings.dataRow1),
min: 0, max: 5,
min: 0, max: 6,
format: v => data_options[v],
onchange: v => {
settings.dataRow1 = data_options[v];
@ -34,7 +34,7 @@
},
'Row 2': {
value: 0 | data_options.indexOf(settings.dataRow2),
min: 0, max: 5,
min: 0, max: 6,
format: v => data_options[v],
onchange: v => {
settings.dataRow2 = data_options[v];
@ -43,7 +43,7 @@
},
'Row 3': {
value: 0 | data_options.indexOf(settings.dataRow3),
min: 0, max: 5,
min: 0, max: 6,
format: v => data_options[v],
onchange: v => {
settings.dataRow3 = data_options[v];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -27,3 +27,4 @@
0.18: Use app-specific icon colors
Spread message action buttons out
Back button now goes back to list of messages
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)

View File

@ -225,7 +225,7 @@ function showMessageSettings(msg) {
function showMessage(msgid) {
var msg = MESSAGES.find(m=>m.id==msgid);
if (!msg) return checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found
if (msg.src=="Maps") {
cancelReloadTimeout(); // don't auto-reload to clock now
return showMapMessage(msg);

View File

@ -43,3 +43,4 @@
0.38: Restructed menus as per forum discussion
0.39: Fix misbehaving debug info option
0.40: Moved off into Utils, put System after Apps
0.41: Stop users disabling all wake-up methods and locking themselves out (fix #1272)

View File

@ -31,9 +31,12 @@ This is Bangle.js's settings menu
* **LCD Brightness** set how bright the LCD is. Due to hardware limitations in the LCD backlight, you may notice flicker if the LCD is not at 100% brightness.
* **LCD Timeout** how long should the LCD stay on for if no activity is detected. 0=stay on forever
* **Wake on X** should the given activity wake up the Bangle.js LCD?
* On Bangle.js 2 when locked the touchscreen is turned off to save power. Because of this,
`Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js.
* **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement.
## Quiet Mode
Quiet Mode is a hint to apps and widgets that you do not want to be disturbed.

View File

@ -11,8 +11,18 @@ function updateSettings() {
}
function updateOptions() {
var o = settings.options;
// Check to make sure nobody disabled all wakeups and locked themselves out!
if (BANGLEJS2) {
if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) {
o.wakeOnBTN1 = true;
}
} else {
if (!(o.wakeOnBTN1||o.wakeOnBTN2||o.wakeOnBTN3||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist))
o.wakeOnBTN2 = true;
}
updateSettings();
Bangle.setOptions(settings.options)
Bangle.setOptions(o)
}
function gToInternal(g) {
@ -63,7 +73,7 @@ const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
function showMainMenu() {
const mainmenu = {
'': { 'title': 'Settings' },
'': { 'title': /*LANG*/'Settings' },
'< Back': ()=>load(),
/*LANG*/'Apps': ()=>showAppSettingsMenu(),
/*LANG*/'System': ()=>showSystemMenu(),
@ -78,7 +88,7 @@ function showMainMenu() {
function showSystemMenu() {
const mainmenu = {
'': { 'title': 'System' },
'': { 'title': /*LANG*/'System' },
'< Back': ()=>showMainMenu(),
/*LANG*/'Theme': ()=>showThemeMenu(),
/*LANG*/'LCD': ()=>showLCDMenu(),
@ -122,7 +132,7 @@ function showAlertsMenu() {
}
const mainmenu = {
'': { 'title': 'Alerts' },
'': { 'title': /*LANG*/'Alerts' },
'< Back': ()=>showMainMenu(),
/*LANG*/'Beep': beepMenuItem,
/*LANG*/'Vibration': {
@ -159,8 +169,8 @@ function showBLEMenu() {
E.showMenu({
'': { 'title': 'Bluetooth' },
'< Back': ()=>showMainMenu(),
'Make Connectable': ()=>makeConnectable(),
'BLE': {
/*LANG*/'Make Connectable': ()=>makeConnectable(),
/*LANG*/'BLE': {
value: settings.ble,
format: boolFormat,
onchange: () => {
@ -168,7 +178,7 @@ function showBLEMenu() {
updateSettings();
}
},
'Programmable': {
/*LANG*/'Programmable': {
value: settings.blerepl,
format: boolFormat,
onchange: () => {
@ -176,7 +186,7 @@ function showBLEMenu() {
updateSettings();
}
},
'HID': {
/*LANG*/'HID': {
value: Math.max(0,0 | hidV.indexOf(settings.HID)),
min: 0, max: 3,
format: v => hidN[v],
@ -185,11 +195,11 @@ function showBLEMenu() {
updateSettings();
}
},
'Passkey BETA': {
/*LANG*/'Passkey BETA': {
value: settings.passkey?settings.passkey:"none",
onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call
},
'Whitelist': {
/*LANG*/'Whitelist': {
value: settings.whitelist?(settings.whitelist.length+" devs"):"off",
onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call
}
@ -213,7 +223,7 @@ function showThemeMenu() {
var m = E.showMenu({
'':{title:'Theme'},
'< Back': ()=>showSystemMenu(),
'Dark BW': ()=>{
/*LANG*/'Dark BW': ()=>{
upd({
fg:cl("#fff"), bg:cl("#000"),
fg2:cl("#0ff"), bg2:cl("#000"),
@ -221,7 +231,7 @@ function showThemeMenu() {
dark:true
});
},
'Light BW': ()=>{
/*LANG*/'Light BW': ()=>{
upd({
fg:cl("#000"), bg:cl("#fff"),
fg2:cl("#000"), bg2:cl("#cff"),
@ -229,7 +239,7 @@ function showThemeMenu() {
dark:false
});
},
'Customize': ()=>showCustomThemeMenu(),
/*LANG*/'Customize': ()=>showCustomThemeMenu(),
});
function showCustomThemeMenu() {
@ -261,9 +271,9 @@ function showThemeMenu() {
"< Back": () => showThemeMenu()
};
const labels = {
fg: 'Foreground', bg: 'Background',
fg2: 'Foreground 2', bg2: 'Background 2',
fgH: 'Highlight FG', bgH: 'Highlight BG',
fg: /*LANG*/'Foreground', bg: /*LANG*/'Background',
fg2: /*LANG*/'Foreground 2', bg2: /*LANG*/'Background 2',
fgH: /*LANG*/'Highlight FG', bgH: /*LANG*/'Highlight BG',
};
["fg", "bg", "fg2", "bg2", "fgH", "bgH"].forEach(t => {
menu[labels[t]] = {
@ -292,7 +302,7 @@ function showThemeMenu() {
function showPasskeyMenu() {
var menu = {
"< Back" : ()=>showBLEMenu(),
"Disable" : () => {
/*LANG*/"Disable" : () => {
settings.passkey = undefined;
updateSettings();
showBLEMenu();
@ -320,7 +330,7 @@ function showPasskeyMenu() {
function showWhitelistMenu() {
var menu = {
"< Back" : ()=>showBLEMenu(),
"Disable" : () => {
/*LANG*/"Disable" : () => {
settings.whitelist = undefined;
updateSettings();
showBLEMenu();
@ -328,7 +338,7 @@ function showWhitelistMenu() {
};
if (settings.whitelist) settings.whitelist.forEach(function(d){
menu[d.substr(0,17)] = function() {
E.showPrompt('Remove\n'+d).then((v) => {
E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => {
if (v) {
settings.whitelist.splice(settings.whitelist.indexOf(d),1);
updateSettings();
@ -337,8 +347,8 @@ function showWhitelistMenu() {
});
}
});
menu['Add Device']=function() {
E.showAlert("Connect device\nto add to\nwhitelist","Whitelist").then(function() {
menu[/*LANG*/'Add Device']=function() {
E.showAlert(/*LANG*/"Connect device\nto add to\nwhitelist",/*LANG*/"Whitelist").then(function() {
NRF.removeAllListeners('connect');
showWhitelistMenu();
});
@ -358,7 +368,7 @@ function showLCDMenu() {
const lcdMenu = {
'': { 'title': 'LCD' },
'< Back': ()=>showSystemMenu(),
'LCD Brightness': {
/*LANG*/'LCD Brightness': {
value: settings.brightness,
min: 0.1,
max: 1,
@ -369,7 +379,7 @@ function showLCDMenu() {
Bangle.setLCDBrightness(settings.brightness);
}
},
'LCD Timeout': {
/*LANG*/'LCD Timeout': {
value: settings.timeout,
min: 0,
max: 60,
@ -380,7 +390,7 @@ function showLCDMenu() {
Bangle.setLCDTimeout(settings.timeout);
}
},
'Wake on BTN1': {
/*LANG*/'Wake on BTN1': {
value: settings.options.wakeOnBTN1,
format: boolFormat,
onchange: () => {
@ -391,7 +401,7 @@ function showLCDMenu() {
};
if (!BANGLEJS2)
Object.assign(lcdMenu, {
'Wake on BTN2': {
/*LANG*/'Wake on BTN2': {
value: settings.options.wakeOnBTN2,
format: boolFormat,
onchange: () => {
@ -399,7 +409,7 @@ function showLCDMenu() {
updateOptions();
}
},
'Wake on BTN3': {
/*LANG*/'Wake on BTN3': {
value: settings.options.wakeOnBTN3,
format: boolFormat,
onchange: () => {
@ -408,7 +418,7 @@ function showLCDMenu() {
}
}});
Object.assign(lcdMenu, {
'Wake on FaceUp': {
/*LANG*/'Wake on FaceUp': {
value: settings.options.wakeOnFaceUp,
format: boolFormat,
onchange: () => {
@ -416,7 +426,7 @@ function showLCDMenu() {
updateOptions();
}
},
'Wake on Touch': {
/*LANG*/'Wake on Touch': {
value: settings.options.wakeOnTouch,
format: boolFormat,
onchange: () => {
@ -424,7 +434,7 @@ function showLCDMenu() {
updateOptions();
}
},
'Wake on Twist': {
/*LANG*/'Wake on Twist': {
value: settings.options.wakeOnTwist,
format: boolFormat,
onchange: () => {
@ -432,7 +442,7 @@ function showLCDMenu() {
updateOptions();
}
},
'Twist Threshold': {
/*LANG*/'Twist Threshold': {
value: internalToG(settings.options.twistThreshold),
min: -0.5,
max: 0.5,
@ -442,7 +452,7 @@ function showLCDMenu() {
updateOptions();
}
},
'Twist Max Y': {
/*LANG*/'Twist Max Y': {
value: settings.options.twistMaxY,
min: -1500,
max: 1500,
@ -452,7 +462,7 @@ function showLCDMenu() {
updateOptions();
}
},
'Twist Timeout': {
/*LANG*/'Twist Timeout': {
value: settings.options.twistTimeout,
min: 0,
max: 2000,
@ -468,9 +478,9 @@ function showLCDMenu() {
function showLocaleMenu() {
const localemenu = {
'': { 'title': 'Locale' },
'': { 'title': /*LANG*/'Locale' },
'< Back': ()=>showSystemMenu(),
'Time Zone': {
/*LANG*/'Time Zone': {
value: settings.timezone,
min: -11,
max: 13,
@ -480,7 +490,7 @@ function showLocaleMenu() {
updateSettings();
}
},
'Clock Style': {
/*LANG*/'Clock Style': {
value: !!settings["12hour"],
format: v => v ? "12hr" : "24hr",
onchange: v => {
@ -494,29 +504,29 @@ function showLocaleMenu() {
function showUtilMenu() {
var menu = {
'': { 'title': 'Utilities' },
'': { 'title': /*LANG*/'Utilities' },
'< Back': ()=>showMainMenu(),
'Debug Info': {
/*LANG*/'Debug Info': {
value: E.clip(0|settings.log,0,2),
min: 0,
max: 2,
format: v => ["Hide","Show","Log"][E.clip(0|v,0,2)],
format: v => [/*LANG*/"Hide",/*LANG*/"Show",/*LANG*/"Log"][E.clip(0|v,0,2)],
onchange: v => {
settings.log = v;
updateSettings();
}
},
'Compact Storage': () => {
E.showMessage("Compacting...\nTakes approx\n1 minute",{title:"Storage"});
/*LANG*/'Compact Storage': () => {
E.showMessage(/*LANG*/"Compacting...\nTakes approx\n1 minute",{title:/*LANG*/"Storage"});
require("Storage").compact();
showUtilMenu();
},
'Rewrite Settings': () => {
/*LANG*/'Rewrite Settings': () => {
require("Storage").write(".boot0","eval(require('Storage').read('bootupdate.js'));");
load("setting.app.js");
},
'Flatten Battery': () => {
E.showMessage('Flattening battery - this can take hours.\nLong-press button to cancel.');
/*LANG*/'Flatten Battery': () => {
E.showMessage(/*LANG*/'Flattening battery - this can take hours.\nLong-press button to cancel.');
Bangle.setLCDTimeout(0);
Bangle.setLCDPower(1);
if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat");
@ -528,8 +538,8 @@ function showUtilMenu() {
var i=1000;while (i--);
}, 1);
},
'Reset Settings': () => {
E.showPrompt('Reset to Defaults?',{title:"Settings"}).then((v) => {
/*LANG*/'Reset Settings': () => {
E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => {
if (v) {
E.showMessage('Resetting');
resetSettings();
@ -540,8 +550,8 @@ function showUtilMenu() {
/*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }
};
if (Bangle.factoryReset) {
menu['Factory Reset'] = ()=>{
E.showPrompt('This will remove everything!',{title:"Factory Reset"}).then((v) => {
menu[/*LANG*/'Factory Reset'] = ()=>{
E.showPrompt(/*LANG*/'This will remove everything!',{title:/*LANG*/"Factory Reset"}).then((v) => {
if (v) {
E.showMessage();
Terminal.setConsole();
@ -558,7 +568,7 @@ function makeConnectable() {
try { NRF.wake(); } catch (e) { }
Bluetooth.setConsole(1);
var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", "");
E.showPrompt(name + "\nStay Connectable?", { title: "Connectable" }).then(r => {
E.showPrompt(name + /*LANG*/"\nStay Connectable?", { title: /*LANG*/"Connectable" }).then(r => {
if (settings.ble != r) {
settings.ble = r;
updateSettings();
@ -574,7 +584,7 @@ function showClockMenu() {
.sort((a, b) => a.sortorder - b.sortorder);
const clockMenu = {
'': {
'title': 'Select Clock',
'title': /*LANG*/'Select Clock',
},
'< Back': ()=>showSystemMenu(),
};
@ -592,7 +602,7 @@ function showClockMenu() {
};
});
if (clockApps.length === 0) {
clockMenu["No Clocks Found"] = () => { };
clockMenu[/*LANG*/"No Clocks Found"] = () => { };
}
return E.showMenu(clockMenu);
}
@ -600,47 +610,47 @@ function showClockMenu() {
function showSetTimeMenu() {
d = new Date();
const timemenu = {
'': { 'title': 'Set Time' },
'': { 'title': /*LANG*/'Set Time' },
'< Back': function () {
setTime(d.getTime() / 1000);
showSystemMenu();
},
'Hour': {
/*LANG*/'Hour': {
value: d.getHours(),
onchange: function (v) {
this.value = (v+24)%24;
d.setHours(this.value);
}
},
'Minute': {
/*LANG*/'Minute': {
value: d.getMinutes(),
onchange: function (v) {
this.value = (v+60)%60;
d.setMinutes(this.value);
}
},
'Second': {
/*LANG*/'Second': {
value: d.getSeconds(),
onchange: function (v) {
this.value = (v+60)%60;
d.setSeconds(this.value);
}
},
'Date': {
/*LANG*/'Date': {
value: d.getDate(),
onchange: function (v) {
this.value = ((v+30)%31)+1;
d.setDate(this.value);
}
},
'Month': {
/*LANG*/'Month': {
value: d.getMonth() + 1,
onchange: function (v) {
this.value = ((v+11)%12)+1;
d.setMonth(this.value - 1);
}
},
'Year': {
/*LANG*/'Year': {
value: d.getFullYear(),
min: 2019,
max: 2100,
@ -654,7 +664,7 @@ function showSetTimeMenu() {
function showAppSettingsMenu() {
let appmenu = {
'': { 'title': 'App Settings' },
'': { 'title': /*LANG*/'App Settings' },
'< Back': ()=>showMainMenu(),
}
const apps = storage.list(/\.settings\.js$/)
@ -671,7 +681,7 @@ function showAppSettingsMenu() {
return 0;
})
if (apps.length === 0) {
appmenu['No app has settings'] = () => { };
appmenu[/*LANG*/'No app has settings'] = () => { };
}
apps.forEach(function (app) {
appmenu[app.name] = () => { showAppSettings(app) };
@ -688,17 +698,17 @@ function showAppSettings(app) {
appSettings = eval(appSettings);
} catch (e) {
console.log(`${app.name} settings error:`, e)
return showError('Error in settings');
return showError(/*LANG*/'Error in settings');
}
if (typeof appSettings !== "function") {
return showError('Invalid settings');
return showError(/*LANG*/'Invalid settings');
}
try {
// pass showAppSettingsMenu as "back" argument
appSettings(()=>showAppSettingsMenu());
} catch (e) {
console.log(`${app.name} settings error:`, e)
return showError('Error in settings');
return showError(/*LANG*/'Error in settings');
}
}

View File

@ -58,6 +58,7 @@ const APP_KEYS = [
];
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports'];
const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate'];
const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports'
const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info
const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ];
const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"];
@ -90,7 +91,7 @@ apps.forEach((app,appIdx) => {
if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`);
else {
app.supports.forEach(dev => {
if (!["BANGLEJS","BANGLEJS2"].includes(dev))
if (!SUPPORTS_DEVICES.includes(dev))
ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`);
});
}
@ -140,6 +141,13 @@ apps.forEach((app,appIdx) => {
if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`)
if (fileNames.includes(file.name) && !file.supports) // assume that there aren't duplicates if 'supports' is set
ERROR(`App ${app.id} file ${file.name} is a duplicate`);
if (file.supports && !Array.isArray(file.supports))
ERROR(`App ${app.id} file ${file.name} supports field must be an array`);
if (file.supports)
file.supports.forEach(dev => {
if (!SUPPORTS_DEVICES.includes(dev))
ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`);
});
fileNames.push(file.name);
allFiles.push({app: app.id, file: file.name});
if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`);
@ -271,7 +279,8 @@ while(fileA=allFiles.pop()) {
if (globA.test(nameB)||globB.test(nameA)) {
if (isGlob(nameA)||isGlob(nameB))
ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`)
else WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
else if (fileA.app != fileB.app)
WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
}
})
}