messages: auto-open music
commit
d492c95442
|
|
@ -243,6 +243,7 @@ and which gives information about the app for the Launcher.
|
||||||
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
|
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
|
||||||
"type":"...", // optional(if app) -
|
"type":"...", // optional(if app) -
|
||||||
// 'app' - an application
|
// 'app' - an application
|
||||||
|
// 'clock' - a clock - required for clocks to automatically start
|
||||||
// 'widget' - a widget
|
// 'widget' - a widget
|
||||||
// 'launch' - replacement launcher app
|
// 'launch' - replacement launcher app
|
||||||
// 'bootloader' - code that runs at startup only
|
// 'bootloader' - code that runs at startup only
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,4 @@
|
||||||
0.07: Recorder icon only blue if values actually arive
|
0.07: Recorder icon only blue if values actually arive
|
||||||
Adds some preset modes and a custom one
|
Adds some preset modes and a custom one
|
||||||
Restructure the settings menu
|
Restructure the settings menu
|
||||||
|
0.08: Allow scanning for devices in settings
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCache(){
|
function getCache(){
|
||||||
return require('Storage').readJSON("bthrm.cache.json", true) || {};
|
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||||
|
if (settings.btname && settings.btname == cache.name) return cache;
|
||||||
|
clearCache();
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNotificationHandler(characteristic){
|
function addNotificationHandler(characteristic){
|
||||||
|
|
@ -361,7 +364,13 @@
|
||||||
var promise;
|
var promise;
|
||||||
|
|
||||||
if (!device){
|
if (!device){
|
||||||
promise = NRF.requestDevice({ filters: serviceFilters });
|
var filters = serviceFilters;
|
||||||
|
if (settings.btname){
|
||||||
|
log("Configured device name", settings.btname);
|
||||||
|
filters = [{name: settings.btname}];
|
||||||
|
}
|
||||||
|
log("Requesting device with filters", filters);
|
||||||
|
promise = NRF.requestDevice({ filters: filters });
|
||||||
|
|
||||||
if (settings.gracePeriodRequest){
|
if (settings.gracePeriodRequest){
|
||||||
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
||||||
|
|
@ -488,11 +497,15 @@
|
||||||
if (gatt) {
|
if (gatt) {
|
||||||
if (gatt.connected){
|
if (gatt.connected){
|
||||||
log("Disconnect with gatt: ", gatt);
|
log("Disconnect with gatt: ", gatt);
|
||||||
|
try{
|
||||||
gatt.disconnect().then(()=>{
|
gatt.disconnect().then(()=>{
|
||||||
log("Successful disconnect");
|
log("Successful disconnect");
|
||||||
}).catch((e)=>{
|
}).catch((e)=>{
|
||||||
log("Error during disconnect", e);
|
log("Error during disconnect promise", e);
|
||||||
});
|
});
|
||||||
|
} catch (e){
|
||||||
|
log("Error during disconnect attempt", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.07",
|
"version": "0.08",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
var settings;
|
var settings;
|
||||||
readSettings();
|
readSettings();
|
||||||
|
|
||||||
|
function buildMainMenu(){
|
||||||
var mainmenu = {
|
var mainmenu = {
|
||||||
'': { 'title': 'Bluetooth HRM' },
|
'': { 'title': 'Bluetooth HRM' },
|
||||||
'< Back': back,
|
'< Back': back,
|
||||||
|
|
@ -57,14 +58,32 @@
|
||||||
}
|
}
|
||||||
writeSettings("mode",v);
|
writeSettings("mode",v);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
'Custom Mode': function() { E.showMenu(submenu_custom); },
|
|
||||||
'Debug': function() { E.showMenu(submenu_debug); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (settings.btname){
|
||||||
|
var name = "Clear " + settings.btname;
|
||||||
|
mainmenu[name] = function() {
|
||||||
|
E.showPrompt("Clear current device name?").then((r)=>{
|
||||||
|
if (r) {
|
||||||
|
writeSettings("btname",undefined);
|
||||||
|
}
|
||||||
|
E.showMenu(buildMainMenu());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mainmenu["BLE Scan"] = ()=> createMenuFromScan();
|
||||||
|
mainmenu["Custom Mode"] = function() { E.showMenu(submenu_custom); };
|
||||||
|
mainmenu.Debug = function() { E.showMenu(submenu_debug); };
|
||||||
|
return mainmenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var submenu_debug = {
|
var submenu_debug = {
|
||||||
'' : { title: "Debug"},
|
'' : { title: "Debug"},
|
||||||
'< Back': function() { E.showMenu(mainmenu); },
|
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||||
'Alert on disconnect': {
|
'Alert on disconnect': {
|
||||||
value: !!settings.warnDisconnect,
|
value: !!settings.warnDisconnect,
|
||||||
format: v => settings.warnDisconnect ? "On" : "Off",
|
format: v => settings.warnDisconnect ? "On" : "Off",
|
||||||
|
|
@ -82,9 +101,40 @@
|
||||||
'Grace periods': function() { E.showMenu(submenu_grace); }
|
'Grace periods': function() { E.showMenu(submenu_grace); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createMenuFromScan(){
|
||||||
|
E.showMenu();
|
||||||
|
E.showMessage("Scanning");
|
||||||
|
|
||||||
|
var submenu_scan = {
|
||||||
|
'' : { title: "Scan"},
|
||||||
|
'< Back': function() { E.showMenu(buildMainMenu()); }
|
||||||
|
};
|
||||||
|
var packets=10;
|
||||||
|
var scanStart=Date.now();
|
||||||
|
NRF.setScan(function(d) {
|
||||||
|
packets--;
|
||||||
|
if (packets<=0 || Date.now() - scanStart > 5000){
|
||||||
|
NRF.setScan();
|
||||||
|
E.showMenu(submenu_scan);
|
||||||
|
} else if (d.name){
|
||||||
|
print("Found device", d);
|
||||||
|
submenu_scan[d.name] = function(){
|
||||||
|
E.showPrompt("Set "+d.name+"?").then((r)=>{
|
||||||
|
if (r) {
|
||||||
|
writeSettings("btname",d.name);
|
||||||
|
}
|
||||||
|
E.showMenu(buildMainMenu());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, { filters: [{services: [ "180d" ]}]});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var submenu_custom = {
|
var submenu_custom = {
|
||||||
'' : { title: "Custom mode"},
|
'' : { title: "Custom mode"},
|
||||||
'< Back': function() { E.showMenu(mainmenu); },
|
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||||
'Replace HRM': {
|
'Replace HRM': {
|
||||||
value: !!settings.custom_replace,
|
value: !!settings.custom_replace,
|
||||||
format: v => settings.custom_replace ? "On" : "Off",
|
format: v => settings.custom_replace ? "On" : "Off",
|
||||||
|
|
@ -165,7 +215,7 @@
|
||||||
|
|
||||||
var submenu = {
|
var submenu = {
|
||||||
'' : { title: "Grace periods"},
|
'' : { title: "Grace periods"},
|
||||||
'< Back': function() { E.showMenu(mainmenu); },
|
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||||
'Request': {
|
'Request': {
|
||||||
value: settings.gracePeriodRequest,
|
value: settings.gracePeriodRequest,
|
||||||
min: 0,
|
min: 0,
|
||||||
|
|
@ -208,5 +258,5 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
E.showMenu(mainmenu);
|
E.showMenu(buildMainMenu());
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jshint/2.11.0/jshint.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jshint/2.11.0/jshint.min.js"></script>
|
||||||
<p>Type your javascript code here</p>
|
<p>Type your javascript code here</p>
|
||||||
<p><textarea id="custom-js"></textarea></p>
|
<p><textarea id="custom-js"></textarea></p>
|
||||||
<p>Then click <button id="upload" class="btn btn-primary">Upload</button></p>
|
<p>Then click <button id="upload" class="btn btn-primary">Upload</button> <span id="btninfo" style="color:orange"></span></p>
|
||||||
<script>
|
<script>
|
||||||
const item = "custom.boot.js";
|
const item = "custom.boot.js";
|
||||||
const id = "custom-js";
|
const id = "custom-js";
|
||||||
const sample = "//Bangle.setOptions({wakeOnBTN2:false});";
|
const sample = "//Bangle.setOptions({wakeOnBTN2:false});";
|
||||||
var localeModule = null;
|
var customBootCode = null;
|
||||||
var editor = {};
|
var editor = {};
|
||||||
|
|
||||||
if (localStorage.getItem(item) === null) {
|
if (localStorage.getItem(item) === null) {
|
||||||
|
|
@ -48,13 +48,28 @@
|
||||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-lint-markers"],
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-lint-markers"],
|
||||||
lineNumbers: true
|
lineNumbers: true
|
||||||
});
|
});
|
||||||
|
function hasWarnings() {
|
||||||
|
return editor.state.lint.marked.length!=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.on("change", function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
if (hasWarnings()) {
|
||||||
|
document.getElementById("btninfo").innerHTML = "There are warnings in the code to be uploaded";
|
||||||
|
document.getElementById("upload").classList.add("disabled");
|
||||||
|
} else {
|
||||||
|
document.getElementById("btninfo").innerHTML = "";
|
||||||
|
document.getElementById("upload").classList.remove("disabled");
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
|
|
||||||
document.getElementById("upload").addEventListener("click", function() {
|
document.getElementById("upload").addEventListener("click", function() {
|
||||||
if (!editor.state.lint.marked.length) {
|
if (!hasWarnings()) {
|
||||||
localeModule = editor.getValue();
|
customBootCode = editor.getValue();
|
||||||
localStorage.setItem(item, localeModule);
|
localStorage.setItem(item, customBootCode);
|
||||||
sendCustomizedApp({
|
sendCustomizedApp({
|
||||||
storage: [{ name: item, content: localeModule }]
|
storage: [{ name: item, content: customBootCode }]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "gbridge",
|
"id": "gbridge",
|
||||||
"name": "Gadgetbridge",
|
"name": "Gadgetbridge",
|
||||||
"version": "0.26",
|
"version": "0.26",
|
||||||
"description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android' Bangle.js app instead.",
|
"description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android Integration' Bangle.js app instead.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
"tags": "tool,system,android,widget",
|
"tags": "tool,system,android,widget",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
0.06: Remove translations if not required
|
0.06: Remove translations if not required
|
||||||
Ensure 'on' is always supplied for translations
|
Ensure 'on' is always supplied for translations
|
||||||
0.07: Improve handling of non-ASCII characters (fix #469)
|
0.07: Improve handling of non-ASCII characters (fix #469)
|
||||||
0.08: Added Mavigation units and en_NAV
|
0.08: Added Navigation units and en_NAV
|
||||||
0.09: Added New Zealand en_NZ
|
0.09: Added New Zealand en_NZ
|
||||||
0.10: Apply 12hour setting to time
|
0.10: Apply 12hour setting to time
|
||||||
0.11: Added translations for nl_NL and changes one formatting
|
0.11: Added translations for nl_NL and changes one formatting
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,5 @@
|
||||||
0.23: Change message colors to match current theme instead of using green
|
0.23: Change message colors to match current theme instead of using green
|
||||||
Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
|
Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
|
||||||
0.24: Remove left-over debug statement
|
0.24: Remove left-over debug statement
|
||||||
0.25: Setting to auto-open music
|
0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
|
||||||
|
0.26: Setting to auto-open music
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "messages",
|
"id": "messages",
|
||||||
"name": "Messages",
|
"name": "Messages",
|
||||||
"version": "0.25",
|
"version": "0.26",
|
||||||
"description": "App to display notifications from iOS and Gadgetbridge/Android",
|
"description": "App to display notifications from iOS and Gadgetbridge/Android",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
WIDGETS["messages"]={area:"tl", width:0, iconwidth:24,
|
WIDGETS["messages"]={area:"tl", width:0, iconwidth:24,
|
||||||
draw:function() {
|
draw:function() {
|
||||||
|
// If we had a setTimeout queued from the last time we were called, remove it
|
||||||
|
if (WIDGETS["messages"].i) {
|
||||||
|
clearTimeout(WIDGETS["messages"].i);
|
||||||
|
delete WIDGETS["messages"].i;
|
||||||
|
}
|
||||||
Bangle.removeListener('touch', this.touch);
|
Bangle.removeListener('touch', this.touch);
|
||||||
if (!this.width) return;
|
if (!this.width) return;
|
||||||
var c = (Date.now()-this.t)/1000;
|
var c = (Date.now()-this.t)/1000;
|
||||||
|
|
@ -11,7 +16,7 @@ draw:function() {
|
||||||
this.l = Date.now();
|
this.l = Date.now();
|
||||||
WIDGETS["messages"].buzz(); // buzz every 4 seconds
|
WIDGETS["messages"].buzz(); // buzz every 4 seconds
|
||||||
}
|
}
|
||||||
setTimeout(()=>WIDGETS["messages"].draw(), 1000);
|
WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(), 1000);
|
||||||
if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
|
if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
|
||||||
},show:function(quiet) {
|
},show:function(quiet) {
|
||||||
WIDGETS["messages"].t=Date.now(); // first time
|
WIDGETS["messages"].t=Date.now(); // first time
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Power manager
|
||||||
|
|
||||||
|
Manages settings for charging. You can set a warning threshold to be able to disconnect the charger at a given percentage. Also allows to set the battery calibration offset.
|
||||||
|
|
||||||
|
## Internals
|
||||||
|
|
||||||
|
Battery calibration offset is set by writing `batFullVoltage` in setting.json
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* Optionally keep battery history and show as graph
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
[halemmerich](https://github.com/halemmerich)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,29 @@
|
||||||
|
(function() {
|
||||||
|
var settings = Object.assign(
|
||||||
|
require('Storage').readJSON("powermanager.default.json", true) || {},
|
||||||
|
require('Storage').readJSON("powermanager.json", true) || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (settings.warnEnabled){
|
||||||
|
print("Charge warning enabled");
|
||||||
|
var chargingInterval;
|
||||||
|
|
||||||
|
function handleCharging(charging){
|
||||||
|
if (charging){
|
||||||
|
if (chargingInterval) clearInterval(chargingInterval);
|
||||||
|
chargingInterval = setInterval(()=>{
|
||||||
|
if (E.getBattery() > settings.warn){
|
||||||
|
Bangle.buzz(1000);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
if (chargingInterval && !charging){
|
||||||
|
clearInterval(chargingInterval);
|
||||||
|
chargingInterval = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on("charging",handleCharging);
|
||||||
|
handleCharging(Bangle.isCharging());
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"warnEnabled": false,
|
||||||
|
"warn": 96
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"id": "powermanager",
|
||||||
|
"name": "Power Manager",
|
||||||
|
"shortName": "Power Manager",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "bootloader",
|
||||||
|
"tags": "tool",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"powermanager.boot.js","url":"boot.js"},
|
||||||
|
{"name":"powermanager.settings.js","url":"settings.js"},
|
||||||
|
{"name":"powermanager.default.json","url":"default.json"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
(function(back) {
|
||||||
|
var systemsettings = require("Storage").readJSON("setting.json") || {};
|
||||||
|
|
||||||
|
function writeSettings(key, value) {
|
||||||
|
var s = require('Storage').readJSON(FILE, true) || {};
|
||||||
|
s[key] = value;
|
||||||
|
require('Storage').writeJSON(FILE, s);
|
||||||
|
readSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSettings() {
|
||||||
|
settings = Object.assign(
|
||||||
|
require('Storage').readJSON("powermanager.default.json", true) || {},
|
||||||
|
require('Storage').readJSON(FILE, true) || {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var FILE = "powermanager.json";
|
||||||
|
var settings;
|
||||||
|
readSettings();
|
||||||
|
|
||||||
|
var mainmenu = {
|
||||||
|
'': {
|
||||||
|
'title': 'Power Manager'
|
||||||
|
},
|
||||||
|
'< Back': back,
|
||||||
|
'Charge warning': function() {
|
||||||
|
E.showMenu(submenu_chargewarn);
|
||||||
|
},
|
||||||
|
'Calibrate': function() {
|
||||||
|
E.showMenu(submenu_calibrate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function roundToDigits(number, stepsize) {
|
||||||
|
return Math.round(number / stepsize) * stepsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentVoltageDirect() {
|
||||||
|
return (analogRead(D3) + analogRead(D3) + analogRead(D3) + analogRead(D3)) / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stepsize = 0.0002;
|
||||||
|
var full = 0.32;
|
||||||
|
|
||||||
|
function getInitialCalibrationOffset() {
|
||||||
|
return roundToDigits(systemsettings.batFullVoltage - full, stepsize) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var submenu_calibrate = {
|
||||||
|
'': {
|
||||||
|
title: "Calibrate"
|
||||||
|
},
|
||||||
|
'< Back': function() {
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
},
|
||||||
|
'Offset': {
|
||||||
|
min: -0.05,
|
||||||
|
max: 0.05,
|
||||||
|
step: stepsize,
|
||||||
|
value: getInitialCalibrationOffset(),
|
||||||
|
format: v => roundToDigits(v, stepsize).toFixed((""+stepsize).length - 2),
|
||||||
|
onchange: v => {
|
||||||
|
print(typeof v);
|
||||||
|
systemsettings.batFullVoltage = v + full;
|
||||||
|
require("Storage").writeJSON("setting.json", systemsettings);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Auto': function() {
|
||||||
|
var newVoltage = getCurrentVoltageDirect();
|
||||||
|
E.showAlert("Please charge fully before auto setting").then(() => {
|
||||||
|
E.showPrompt("Set current charge as full").then((r) => {
|
||||||
|
if (r) {
|
||||||
|
systemsettings.batFullVoltage = newVoltage;
|
||||||
|
require("Storage").writeJSON("setting.json", systemsettings);
|
||||||
|
//reset value shown in menu to the newly set one
|
||||||
|
submenu_calibrate.Offset.value = getInitialCalibrationOffset();
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'Clear': function() {
|
||||||
|
E.showPrompt("Clear charging offset?").then((r) => {
|
||||||
|
if (r) {
|
||||||
|
delete systemsettings.batFullVoltage;
|
||||||
|
require("Storage").writeJSON("setting.json", systemsettings);
|
||||||
|
//reset value shown in menu to the newly set one
|
||||||
|
submenu_calibrate.Offset.value = getInitialCalibrationOffset();
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var submenu_chargewarn = {
|
||||||
|
'': {
|
||||||
|
title: "Charge warning"
|
||||||
|
},
|
||||||
|
'< Back': function() {
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
},
|
||||||
|
'Enabled': {
|
||||||
|
value: !!settings.warnEnabled,
|
||||||
|
format: v => settings.warnEnabled ? "On" : "Off",
|
||||||
|
onchange: v => {
|
||||||
|
writeSettings("warnEnabled", v);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Percentage': {
|
||||||
|
min: 80,
|
||||||
|
max: 100,
|
||||||
|
step: 2,
|
||||||
|
value: settings.warn,
|
||||||
|
format: v => v + "%",
|
||||||
|
onchange: v => {
|
||||||
|
writeSettings("warn", v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
})
|
||||||
|
|
@ -6,3 +6,4 @@
|
||||||
0.06: Fix: don't try to redraw widget when widgets not loaded
|
0.06: Fix: don't try to redraw widget when widgets not loaded
|
||||||
0.07: Option to switch theme
|
0.07: Option to switch theme
|
||||||
Changed time selection to 5-minute intervals
|
Changed time selection to 5-minute intervals
|
||||||
|
0.08: Support new Bangle.js 2 menu
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
const modeNames = ["Off", "Alarms", "Silent"];
|
const modeNames = [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"];
|
||||||
|
const B2 = process.env.HWVERSION===2;
|
||||||
// load global settings
|
// load global settings
|
||||||
let bSettings = require('Storage').readJSON('setting.json',true)||{};
|
let bSettings = require('Storage').readJSON('setting.json',true)||{};
|
||||||
let current = 0|bSettings.quiet;
|
let current = 0|bSettings.quiet;
|
||||||
|
|
@ -109,34 +109,26 @@ function setAppQuietMode(mode) {
|
||||||
|
|
||||||
let m;
|
let m;
|
||||||
function showMainMenu() {
|
function showMainMenu() {
|
||||||
let menu = {
|
let menu = {"": {"title": /*LANG*/"Quiet Mode"},};
|
||||||
"": {"title": "Quiet Mode"},
|
menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Exit"] = () => {load();};
|
||||||
"< Exit": () => load()
|
menu[/*LANG*/"Current Mode"] = {
|
||||||
};
|
|
||||||
// "Current Mode""Silent" won't fit on Bangle.js 2
|
|
||||||
menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = {
|
|
||||||
value: current,
|
value: current,
|
||||||
min:0, max:2, wrap: true,
|
min:0, max:2, wrap: true,
|
||||||
format: () => modeNames[current],
|
format: v => modeNames[v],
|
||||||
onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current`
|
onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current`
|
||||||
};
|
};
|
||||||
scheds.sort((a, b) => (a.hr-b.hr));
|
scheds.sort((a, b) => (a.hr-b.hr));
|
||||||
scheds.forEach((sched, idx) => {
|
scheds.forEach((sched, idx) => {
|
||||||
menu[formatTime(sched.hr)] = {
|
menu[formatTime(sched.hr)] = () => { showEditMenu(idx); };
|
||||||
format: () => modeNames[sched.mode], // abuse format to right-align text
|
menu[formatTime(sched.hr)].format = () => modeNames[sched.mode]+' >'; // this does nothing :-(
|
||||||
onchange: () => {
|
|
||||||
m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange)
|
|
||||||
showEditMenu(idx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
menu["Add Schedule"] = () => showEditMenu(-1);
|
menu[/*LANG*/"Add Schedule"] = () => showEditMenu(-1);
|
||||||
menu["Switch Theme"] = {
|
menu[/*LANG*/"Switch Theme"] = {
|
||||||
value: !!get("switchTheme"),
|
value: !!get("switchTheme"),
|
||||||
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
|
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
|
||||||
onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
|
onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
|
||||||
};
|
};
|
||||||
menu["LCD Settings"] = () => showOptionsMenu();
|
menu[/*LANG*/"LCD Settings"] = () => showOptionsMenu();
|
||||||
m = E.showMenu(menu);
|
m = E.showMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,25 +142,23 @@ function showEditMenu(index) {
|
||||||
mins = Math.round((s.hr-hrs)*60);
|
mins = Math.round((s.hr-hrs)*60);
|
||||||
mode = s.mode;
|
mode = s.mode;
|
||||||
}
|
}
|
||||||
const menu = {
|
let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}};
|
||||||
"": {"title": (isNew ? "Add" : "Edit")+" Schedule"},
|
menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Cancel"] = () => showMainMenu();
|
||||||
"< Cancel": () => showMainMenu(),
|
menu[/*LANG*/"Hours"] = {
|
||||||
"Hours": {
|
|
||||||
value: hrs,
|
value: hrs,
|
||||||
min:0, max:23, wrap:true,
|
min:0, max:23, wrap:true,
|
||||||
onchange: v => {hrs = v;},
|
onchange: v => {hrs = v;},
|
||||||
},
|
};
|
||||||
"Minutes": {
|
menu[/*LANG*/"Minutes"] = {
|
||||||
value: mins,
|
value: mins,
|
||||||
min:0, max:55, step:5, wrap:true,
|
min:0, max:55, step:5, wrap:true,
|
||||||
onchange: v => {mins = v;},
|
onchange: v => {mins = v;},
|
||||||
},
|
};
|
||||||
"Switch to": {
|
menu[/*LANG*/"Switch to"] = {
|
||||||
value: mode,
|
value: mode,
|
||||||
min:0, max:2, wrap:true,
|
min:0, max:2, wrap:true,
|
||||||
format: v => modeNames[v],
|
format: v => modeNames[v],
|
||||||
onchange: v => {mode = v;},
|
onchange: v => {mode = v;},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
function getSched() {
|
function getSched() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -176,7 +166,7 @@ function showEditMenu(index) {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
menu["> Save"] = function() {
|
menu[B2 ? /*LANG*/"Save" : /*LANG*/"> Save"] = function() {
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
scheds.push(getSched());
|
scheds.push(getSched());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -186,7 +176,7 @@ function showEditMenu(index) {
|
||||||
showMainMenu();
|
showMainMenu();
|
||||||
};
|
};
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
menu["> Delete"] = function() {
|
menu[B2 ? /*LANG*/"Delete" : /*LANG*/"> Delete"] = function() {
|
||||||
scheds.splice(index, 1);
|
scheds.splice(index, 1);
|
||||||
save();
|
save();
|
||||||
showMainMenu();
|
showMainMenu();
|
||||||
|
|
@ -196,7 +186,7 @@ function showEditMenu(index) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOptionsMenu() {
|
function showOptionsMenu() {
|
||||||
const disabledFormat = v => v ? "Off" : "-";
|
const disabledFormat = v => v ? /*LANG*/"Off" : "-";
|
||||||
function toggle(option) {
|
function toggle(option) {
|
||||||
// we disable wakeOn* events by setting them to `false` in options
|
// we disable wakeOn* events by setting them to `false` in options
|
||||||
// not disabled = not present in options at all
|
// not disabled = not present in options at all
|
||||||
|
|
@ -209,9 +199,9 @@ function showOptionsMenu() {
|
||||||
}
|
}
|
||||||
let resetTimeout;
|
let resetTimeout;
|
||||||
const oMenu = {
|
const oMenu = {
|
||||||
"": {"title": "LCD Settings"},
|
"": {"title": /*LANG*/"LCD Settings"},
|
||||||
"< Back": () => showMainMenu(),
|
/*LANG*/"< Back": () => showMainMenu(),
|
||||||
"LCD Brightness": {
|
/*LANG*/"LCD Brightness": {
|
||||||
value: get("brightness", 0),
|
value: get("brightness", 0),
|
||||||
min: 0, // 0 = use default
|
min: 0, // 0 = use default
|
||||||
max: 1,
|
max: 1,
|
||||||
|
|
@ -233,7 +223,7 @@ function showOptionsMenu() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"LCD Timeout": {
|
/*LANG*/"LCD Timeout": {
|
||||||
value: get("timeout", 0),
|
value: get("timeout", 0),
|
||||||
min: 0, // 0 = use default (no constant on for quiet mode)
|
min: 0, // 0 = use default (no constant on for quiet mode)
|
||||||
max: 60,
|
max: 60,
|
||||||
|
|
@ -246,17 +236,17 @@ function showOptionsMenu() {
|
||||||
},
|
},
|
||||||
// we disable wakeOn* events by overwriting them as false in options
|
// we disable wakeOn* events by overwriting them as false in options
|
||||||
// not disabled = not present in options at all
|
// not disabled = not present in options at all
|
||||||
"Wake on FaceUp": {
|
/*LANG*/"Wake on FaceUp": {
|
||||||
value: "wakeOnFaceUp" in options,
|
value: "wakeOnFaceUp" in options,
|
||||||
format: disabledFormat,
|
format: disabledFormat,
|
||||||
onchange: () => {toggle("wakeOnFaceUp");},
|
onchange: () => {toggle("wakeOnFaceUp");},
|
||||||
},
|
},
|
||||||
"Wake on Touch": {
|
/*LANG*/"Wake on Touch": {
|
||||||
value: "wakeOnTouch" in options,
|
value: "wakeOnTouch" in options,
|
||||||
format: disabledFormat,
|
format: disabledFormat,
|
||||||
onchange: () => {toggle("wakeOnTouch");},
|
onchange: () => {toggle("wakeOnTouch");},
|
||||||
},
|
},
|
||||||
"Wake on Twist": {
|
/*LANG*/"Wake on Twist": {
|
||||||
value: "wakeOnTwist" in options,
|
value: "wakeOnTwist" in options,
|
||||||
format: disabledFormat,
|
format: disabledFormat,
|
||||||
onchange: () => {toggle("wakeOnTwist");},
|
onchange: () => {toggle("wakeOnTwist");},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "qmsched",
|
"id": "qmsched",
|
||||||
"name": "Quiet Mode Schedule and Widget",
|
"name": "Quiet Mode Schedule and Widget",
|
||||||
"shortName": "Quiet Mode",
|
"shortName": "Quiet Mode",
|
||||||
"version": "0.07",
|
"version": "0.08",
|
||||||
"description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
|
"description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
|
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@
|
||||||
0.05: exstats updated so update 'distance' label is updated, option for 'speed'
|
0.05: exstats updated so update 'distance' label is updated, option for 'speed'
|
||||||
0.06: Add option to record a run using the recorder app automatically
|
0.06: Add option to record a run using the recorder app automatically
|
||||||
0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
|
0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
|
||||||
|
0.08: Added support for notifications from exstats. Support all stats from exstats
|
||||||
|
|
@ -13,7 +13,7 @@ the red `STOP` in the bottom right turns to a green `RUN`.
|
||||||
the GPS updates your position as it gets more satellites your position changes and the distance
|
the GPS updates your position as it gets more satellites your position changes and the distance
|
||||||
shown will increase, even if you are standing still.
|
shown will increase, even if you are standing still.
|
||||||
* `TIME` - the elapsed time for your run
|
* `TIME` - the elapsed time for your run
|
||||||
* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far**
|
* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far**
|
||||||
* `HEART` - Your heart rate
|
* `HEART` - Your heart rate
|
||||||
* `STEPS` - Steps since you started exercising
|
* `STEPS` - Steps since you started exercising
|
||||||
* `CADENCE` - Steps per second based on your step rate *over the last minute*
|
* `CADENCE` - Steps per second based on your step rate *over the last minute*
|
||||||
|
|
@ -24,9 +24,8 @@ so if you have no GPS lock you just need to wait.
|
||||||
|
|
||||||
## Recording Tracks
|
## Recording Tracks
|
||||||
|
|
||||||
`Run` doesn't directly allow you to record your tracks at the moment.
|
When the `Recorder` app is installed, `Run` will automatically start and stop tracks
|
||||||
However you can just install the `Recorder` app, turn recording on in
|
as needed, prompting you to overwrite or begin a new track if necessary.
|
||||||
that, and then start the `Run` app.
|
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
|
|
@ -35,13 +34,29 @@ Under `Settings` -> `App` -> `Run` you can change settings for this app.
|
||||||
* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically
|
* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically
|
||||||
record GPS/HRM/etc data every time you start a run?
|
record GPS/HRM/etc data every time you start a run?
|
||||||
* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon
|
* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon
|
||||||
* `Box 1/2/3/4/5/6` are what should be shown in each of the 6 boxes on the display. From the top left, down.
|
* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display.
|
||||||
If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information
|
Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence".
|
||||||
if you wish by setting the last 2 boxes to `-`.
|
Any box set to "-" will display no information.
|
||||||
|
* Box 1 is the top left (defaults to "Distance")
|
||||||
|
* Box 2 is the top right (defaults to "Time")
|
||||||
|
* Box 3 is the middle left (defaults to "Pace (avg)")
|
||||||
|
* Box 4 is the middle right (defaults to "Heart (BPM)")
|
||||||
|
* Box 5 is the bottom left (defaults to "Steps")
|
||||||
|
* Box 6 is the bottom right (defaults to "Cadence")
|
||||||
|
* `Notifications` leads to a submenu where you can configure if the app will notify you after
|
||||||
|
your distance, steps, or time repeatedly pass your configured thresholds
|
||||||
|
* `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options
|
||||||
|
* "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon"
|
||||||
|
* `Ntfy Steps`: The number of steps that must pass before you are notified.
|
||||||
|
* "Off" (default), 100, 500, 1000, 5000, 10000
|
||||||
|
* `Ntfy Time`: The amount of time that must pass before you are notified.
|
||||||
|
* "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour"
|
||||||
|
* `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold
|
||||||
|
* `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold
|
||||||
|
* `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* Allow this app to trigger the `Recorder` app on and off directly.
|
|
||||||
* Keep a log of each run's stats (distance/steps/etc)
|
* Keep a log of each run's stats (distance/steps/etc)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
var ExStats = require("exstats");
|
var ExStats = require("exstats");
|
||||||
var B2 = process.env.HWVERSION==2;
|
var B2 = process.env.HWVERSION===2;
|
||||||
var Layout = require("Layout");
|
var Layout = require("Layout");
|
||||||
var locale = require("locale");
|
var locale = require("locale");
|
||||||
var fontHeading = "6x8:2";
|
var fontHeading = "6x8:2";
|
||||||
|
|
@ -14,22 +14,60 @@ Bangle.drawWidgets();
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
let settings = Object.assign({
|
let settings = Object.assign({
|
||||||
record : true,
|
record: true,
|
||||||
B1 : "dist",
|
B1: "dist",
|
||||||
B2 : "time",
|
B2: "time",
|
||||||
B3 : "pacea",
|
B3: "pacea",
|
||||||
B4 : "bpm",
|
B4: "bpm",
|
||||||
B5 : "step",
|
B5: "step",
|
||||||
B6 : "caden",
|
B6: "caden",
|
||||||
paceLength : 1000
|
paceLength: 1000,
|
||||||
|
notify: {
|
||||||
|
dist: {
|
||||||
|
value: 0,
|
||||||
|
notifications: [],
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
value: 0,
|
||||||
|
notifications: [],
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
value: 0,
|
||||||
|
notifications: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
}, require("Storage").readJSON("run.json", 1) || {});
|
}, require("Storage").readJSON("run.json", 1) || {});
|
||||||
var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!="");
|
var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!=="");
|
||||||
var exs = ExStats.getStats(statIDs, settings);
|
var exs = ExStats.getStats(statIDs, settings);
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
// Called to start/stop running
|
// Called to start/stop running
|
||||||
function onStartStop() {
|
function onStartStop() {
|
||||||
var running = !exs.state.active;
|
var running = !exs.state.active;
|
||||||
|
var prepPromises = [];
|
||||||
|
|
||||||
|
// start/stop recording
|
||||||
|
// Do this first in case recorder needs to prompt for
|
||||||
|
// an overwrite before we start tracking exstats
|
||||||
|
if (settings.record && WIDGETS["recorder"]) {
|
||||||
|
if (running) {
|
||||||
|
isMenuDisplayed = true;
|
||||||
|
prepPromises.push(
|
||||||
|
WIDGETS["recorder"].setRecording(true).then(() => {
|
||||||
|
isMenuDisplayed = false;
|
||||||
|
layout.forgetLazyState();
|
||||||
|
layout.render();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
prepPromises.push(
|
||||||
|
WIDGETS["recorder"].setRecording(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(prepPromises)
|
||||||
|
.then(() => {
|
||||||
if (running) {
|
if (running) {
|
||||||
exs.start();
|
exs.start();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -41,19 +79,7 @@ function onStartStop() {
|
||||||
// if stopping running, don't clear state
|
// if stopping running, don't clear state
|
||||||
// so we can at least refer to what we've done
|
// so we can at least refer to what we've done
|
||||||
layout.render();
|
layout.render();
|
||||||
// start/stop recording
|
|
||||||
if (settings.record && WIDGETS["recorder"]) {
|
|
||||||
if (running) {
|
|
||||||
isMenuDisplayed = true;
|
|
||||||
WIDGETS["recorder"].setRecording(true).then(() => {
|
|
||||||
isMenuDisplayed = false;
|
|
||||||
layout.forgetLazyState();
|
|
||||||
layout.render();
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
WIDGETS["recorder"].setRecording(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var lc = [];
|
var lc = [];
|
||||||
|
|
@ -84,11 +110,27 @@ var layout = new Layout( {
|
||||||
delete lc;
|
delete lc;
|
||||||
layout.render();
|
layout.render();
|
||||||
|
|
||||||
|
function configureNotification(stat) {
|
||||||
|
stat.on('notify', (e)=>{
|
||||||
|
settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) {
|
||||||
|
return promise.then(function () {
|
||||||
|
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(settings.notify).forEach((statType) => {
|
||||||
|
if (settings.notify[statType].increment > 0) {
|
||||||
|
configureNotification(exs.stats[statType]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle GPS state change for icon
|
// Handle GPS state change for icon
|
||||||
Bangle.on("GPS", function(fix) {
|
Bangle.on("GPS", function(fix) {
|
||||||
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
|
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
|
||||||
if (!fix.fix) return; // only process actual fixes
|
if (!fix.fix) return; // only process actual fixes
|
||||||
if (fixCount++ == 0) {
|
if (fixCount++ === 0) {
|
||||||
Bangle.buzz(); // first fix, does not need to respect quiet mode
|
Bangle.buzz(); // first fix, does not need to respect quiet mode
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{ "id": "run",
|
{ "id": "run",
|
||||||
"name": "Run",
|
"name": "Run",
|
||||||
"version":"0.07",
|
"version":"0.08",
|
||||||
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "run,running,fitness,outdoors,gps",
|
"tags": "run,running,fitness,outdoors,gps",
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,28 @@
|
||||||
// This way saved values are preserved if a new version adds more settings
|
// This way saved values are preserved if a new version adds more settings
|
||||||
const storage = require('Storage')
|
const storage = require('Storage')
|
||||||
let settings = Object.assign({
|
let settings = Object.assign({
|
||||||
record : true,
|
record: true,
|
||||||
B1 : "dist",
|
B1: "dist",
|
||||||
B2 : "time",
|
B2: "time",
|
||||||
B3 : "pacea",
|
B3: "pacea",
|
||||||
B4 : "bpm",
|
B4: "bpm",
|
||||||
B5 : "step",
|
B5: "step",
|
||||||
B6 : "caden",
|
B6: "caden",
|
||||||
paceLength : 1000
|
paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale
|
||||||
|
notify: {
|
||||||
|
dist: {
|
||||||
|
increment: 0,
|
||||||
|
notifications: [],
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
increment: 0,
|
||||||
|
notifications: [],
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
increment: 0,
|
||||||
|
notifications: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
}, storage.readJSON(SETTINGS_FILE, 1) || {});
|
}, storage.readJSON(SETTINGS_FILE, 1) || {});
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
storage.write(SETTINGS_FILE, settings)
|
storage.write(SETTINGS_FILE, settings)
|
||||||
|
|
@ -24,7 +38,7 @@
|
||||||
|
|
||||||
function getBoxChooser(boxID) {
|
function getBoxChooser(boxID) {
|
||||||
return {
|
return {
|
||||||
min :0, max: statsIDs.length-1,
|
min: 0, max: statsIDs.length-1,
|
||||||
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
|
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
|
||||||
format: v => statsList[v].name,
|
format: v => statsList[v].name,
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
|
|
@ -34,6 +48,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sampleBuzz(buzzPatterns) {
|
||||||
|
return buzzPatterns.reduce(function (promise, buzzPattern) {
|
||||||
|
return promise.then(function () {
|
||||||
|
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
var menu = {
|
var menu = {
|
||||||
'': { 'title': 'Run' },
|
'': { 'title': 'Run' },
|
||||||
'< Back': back,
|
'< Back': back,
|
||||||
|
|
@ -47,8 +69,55 @@
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var notificationsMenu = {
|
||||||
|
'< Back': function() { E.showMenu(menu) },
|
||||||
|
}
|
||||||
|
menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)};
|
||||||
ExStats.appendMenuItems(menu, settings, saveSettings);
|
ExStats.appendMenuItems(menu, settings, saveSettings);
|
||||||
Object.assign(menu,{
|
ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings);
|
||||||
|
var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"];
|
||||||
|
var vibTimes = [
|
||||||
|
[],
|
||||||
|
[[100, 1]],
|
||||||
|
[[300, 1]],
|
||||||
|
[[300, 1], [300, 0], [300, 1]],
|
||||||
|
[[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]],
|
||||||
|
[[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]],
|
||||||
|
];
|
||||||
|
notificationsMenu[/*LANG*/"Dist Pattern"] = {
|
||||||
|
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
|
||||||
|
min: 0, max: vibPatterns.length,
|
||||||
|
format: v => vibPatterns[v]||"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.notify.dist.notifications = vibTimes[v];
|
||||||
|
sampleBuzz(vibTimes[v]);
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notificationsMenu[/*LANG*/"Step Pattern"] = {
|
||||||
|
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
|
||||||
|
min: 0, max: vibPatterns.length,
|
||||||
|
format: v => vibPatterns[v]||"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.notify.step.notifications = vibTimes[v];
|
||||||
|
sampleBuzz(vibTimes[v]);
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notificationsMenu[/*LANG*/"Time Pattern"] = {
|
||||||
|
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
|
||||||
|
min: 0, max: vibPatterns.length,
|
||||||
|
format: v => vibPatterns[v]||"Off",
|
||||||
|
onchange: v => {
|
||||||
|
settings.notify.time.notifications = vibTimes[v];
|
||||||
|
sampleBuzz(vibTimes[v]);
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var boxMenu = {
|
||||||
|
'< Back': function() { E.showMenu(menu) },
|
||||||
|
}
|
||||||
|
Object.assign(boxMenu,{
|
||||||
'Box 1': getBoxChooser("B1"),
|
'Box 1': getBoxChooser("B1"),
|
||||||
'Box 2': getBoxChooser("B2"),
|
'Box 2': getBoxChooser("B2"),
|
||||||
'Box 3': getBoxChooser("B3"),
|
'Box 3': getBoxChooser("B3"),
|
||||||
|
|
@ -56,5 +125,6 @@
|
||||||
'Box 5': getBoxChooser("B5"),
|
'Box 5': getBoxChooser("B5"),
|
||||||
'Box 6': getBoxChooser("B6"),
|
'Box 6': getBoxChooser("B6"),
|
||||||
});
|
});
|
||||||
|
menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)};
|
||||||
E.showMenu(menu);
|
E.showMenu(menu);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: Initial version
|
||||||
|
0.02: Do not warn multiple times for the same exceedance
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Barometer alarm widget
|
||||||
|
|
||||||
|
Get a notification when the pressure reaches defined thresholds.
|
||||||
|
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
* Interval: check interval of sensor data in minutes. 0 to disable automatic check.
|
||||||
|
* Low alarm: Toggle low alarm
|
||||||
|
* Low threshold: Warn when pressure drops below this value
|
||||||
|
* High alarm: Toggle high alarm
|
||||||
|
* High threshold: Warn when pressure exceeds above this value
|
||||||
|
* Drop alarm: Warn when pressure drops more than this value in the recent 3 hours (having at least 30 min of data)
|
||||||
|
0 to disable this alarm.
|
||||||
|
* Raise alarm: Warn when pressure raises more than this value in the recent 3 hours (having at least 30 min of data)
|
||||||
|
0 to disable this alarm.
|
||||||
|
* Show widget: Enable/disable widget visibility
|
||||||
|
* Buzz on alarm: Enable/disable buzzer on alarm
|
||||||
|
|
||||||
|
|
||||||
|
## Widget
|
||||||
|
The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours.
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
Marco ([myxor](https://github.com/myxor))
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"buzz": true,
|
||||||
|
"lowalarm": false,
|
||||||
|
"min": 950,
|
||||||
|
"highalarm": false,
|
||||||
|
"max": 1030,
|
||||||
|
"drop3halarm": 2,
|
||||||
|
"raise3halarm": 0,
|
||||||
|
"show": true,
|
||||||
|
"interval": 15
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"id": "widbaroalarm",
|
||||||
|
"name": "Barometer Alarm Widget",
|
||||||
|
"shortName": "Barometer Alarm",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "tool,barometer",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"dependencies": {"notify":"type"},
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"widbaroalarm.wid.js","url":"widget.js"},
|
||||||
|
{"name":"widbaroalarm.settings.js","url":"settings.js"},
|
||||||
|
{"name":"widbaroalarm.default.json","url":"default.json"}
|
||||||
|
],
|
||||||
|
"data": [{"name":"widbaroalarm.json"}, {"name":"widbaroalarm.log"}]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
(function(back) {
|
||||||
|
const SETTINGS_FILE = "widbaroalarm.json";
|
||||||
|
const storage = require('Storage');
|
||||||
|
let settings = Object.assign(
|
||||||
|
storage.readJSON("widbaroalarm.default.json", true) || {},
|
||||||
|
storage.readJSON(SETTINGS_FILE, true) || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
function save(key, value) {
|
||||||
|
settings[key] = value;
|
||||||
|
storage.write(SETTINGS_FILE, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMainMenu() {
|
||||||
|
let menu ={
|
||||||
|
'': { 'title': 'Barometer alarm widget' },
|
||||||
|
/*LANG*/'< Back': back,
|
||||||
|
"Interval": {
|
||||||
|
value: settings.interval,
|
||||||
|
min: 0,
|
||||||
|
max: 120,
|
||||||
|
step: 1,
|
||||||
|
format: x => {
|
||||||
|
return x != 0 ? x + ' min' : 'off';
|
||||||
|
},
|
||||||
|
onchange: x => save("interval", x)
|
||||||
|
},
|
||||||
|
"Low alarm": {
|
||||||
|
value: settings.lowalarm,
|
||||||
|
format: x => {
|
||||||
|
return x ? 'Yes' : 'No';
|
||||||
|
},
|
||||||
|
onchange: x => save("lowalarm", x),
|
||||||
|
},
|
||||||
|
"Low threshold": {
|
||||||
|
value: settings.min,
|
||||||
|
min: 600,
|
||||||
|
max: 1000,
|
||||||
|
step: 5,
|
||||||
|
onchange: x => save("min", x),
|
||||||
|
},
|
||||||
|
"High alarm": {
|
||||||
|
value: settings.highalarm,
|
||||||
|
format: x => {
|
||||||
|
return x ? 'Yes' : 'No';
|
||||||
|
},
|
||||||
|
onchange: x => save("highalarm", x),
|
||||||
|
},
|
||||||
|
"High threshold": {
|
||||||
|
value: settings.max,
|
||||||
|
min: 700,
|
||||||
|
max: 1100,
|
||||||
|
step: 5,
|
||||||
|
onchange: x => save("max", x),
|
||||||
|
},
|
||||||
|
"Drop alarm": {
|
||||||
|
value: settings.drop3halarm,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
format: x => {
|
||||||
|
return x != 0 ? x + ' hPa/3h' : 'off';
|
||||||
|
},
|
||||||
|
onchange: x => save("drop3halarm", x)
|
||||||
|
},
|
||||||
|
"Raise alarm": {
|
||||||
|
value: settings.raise3halarm,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
format: x => {
|
||||||
|
return x != 0 ? x + ' hPa/3h' : 'off';
|
||||||
|
},
|
||||||
|
onchange: x => save("raise3halarm", x)
|
||||||
|
},
|
||||||
|
"Show widget": {
|
||||||
|
value: settings.show,
|
||||||
|
format: x => {
|
||||||
|
return x ? 'Yes' : 'No';
|
||||||
|
},
|
||||||
|
onchange: x => save('show', x)
|
||||||
|
},
|
||||||
|
"Buzz on alarm": {
|
||||||
|
value: settings.buzz,
|
||||||
|
format: x => {
|
||||||
|
return x ? 'Yes' : 'No';
|
||||||
|
},
|
||||||
|
onchange: x => save('buzz', x)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
E.showMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
showMainMenu();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
(function() {
|
||||||
|
let medianPressure;
|
||||||
|
let threeHourAvrPressure;
|
||||||
|
let currentPressures = [];
|
||||||
|
|
||||||
|
const LOG_FILE = "widbaroalarm.log.json";
|
||||||
|
const SETTINGS_FILE = "widbaroalarm.json";
|
||||||
|
const storage = require('Storage');
|
||||||
|
|
||||||
|
let settings;
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
settings = Object.assign(
|
||||||
|
storage.readJSON("widbaroalarm.default.json", true) || {},
|
||||||
|
storage.readJSON(SETTINGS_FILE, true) || {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
|
||||||
|
|
||||||
|
function setting(key) {
|
||||||
|
return settings[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSetting(key, value) {
|
||||||
|
settings[key] = value;
|
||||||
|
storage.write(SETTINGS_FILE, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setting("interval");
|
||||||
|
|
||||||
|
let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours
|
||||||
|
|
||||||
|
function showAlarm(body, title) {
|
||||||
|
if (body == undefined) return;
|
||||||
|
|
||||||
|
require("notify").show({
|
||||||
|
title: title || "Pressure",
|
||||||
|
body: body,
|
||||||
|
icon: require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA"))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (setting("buzz") &&
|
||||||
|
!(storage.readJSON('setting.json', 1) || {}).quiet) {
|
||||||
|
Bangle.buzz();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function didWeAlreadyWarn(key) {
|
||||||
|
return setting(key) == undefined || setting(key) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForAlarms(pressure) {
|
||||||
|
if (pressure == undefined || pressure <= 0) return;
|
||||||
|
|
||||||
|
let alreadyWarned = false;
|
||||||
|
|
||||||
|
const ts = Math.round(Date.now() / 1000); // seconds
|
||||||
|
const d = {
|
||||||
|
"ts": ts,
|
||||||
|
"p": pressure
|
||||||
|
};
|
||||||
|
|
||||||
|
// delete entries older than 3h
|
||||||
|
for (let i = 0; i < history3.length; i++) {
|
||||||
|
if (history3[i]["ts"] < ts - (3 * 60 * 60)) {
|
||||||
|
history3.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete oldest entries until we have max 50
|
||||||
|
while (history3.length > 50) {
|
||||||
|
history3.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting("lowalarm")) {
|
||||||
|
// Is below the alarm threshold?
|
||||||
|
if (pressure <= setting("min")) {
|
||||||
|
if (!didWeAlreadyWarn("lastLowWarningTs")) {
|
||||||
|
showAlarm("Pressure low: " + Math.round(pressure) + " hPa");
|
||||||
|
saveSetting("lastLowWarningTs", ts);
|
||||||
|
alreadyWarned = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastLowWarningTs", 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastLowWarningTs", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting("highalarm")) {
|
||||||
|
// Is above the alarm threshold?
|
||||||
|
if (pressure >= setting("max")) {
|
||||||
|
if (!didWeAlreadyWarn("lastHighWarningTs")) {
|
||||||
|
showAlarm("Pressure high: " + Math.round(pressure) + " hPa");
|
||||||
|
saveSetting("lastHighWarningTs", ts);
|
||||||
|
alreadyWarned = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastHighWarningTs", 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastHighWarningTs", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyWarned) {
|
||||||
|
// 3h change detection
|
||||||
|
const drop3halarm = setting("drop3halarm");
|
||||||
|
const raise3halarm = setting("raise3halarm");
|
||||||
|
if (drop3halarm > 0 || raise3halarm > 0) {
|
||||||
|
// we need at least 30min of data for reliable detection
|
||||||
|
if (history3[0]["ts"] > ts - (30 * 60)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get oldest entry:
|
||||||
|
const oldestPressure = history3[0]["p"];
|
||||||
|
if (oldestPressure != undefined && oldestPressure > 0) {
|
||||||
|
const diff = oldestPressure - pressure;
|
||||||
|
|
||||||
|
// drop alarm
|
||||||
|
if (drop3halarm > 0 && oldestPressure > pressure) {
|
||||||
|
if (Math.abs(diff) > drop3halarm) {
|
||||||
|
if (!didWeAlreadyWarn("lastDropWarningTs")) {
|
||||||
|
showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
|
||||||
|
Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop");
|
||||||
|
saveSetting("lastDropWarningTs", ts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastDropWarningTs", 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastDropWarningTs", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// raise alarm
|
||||||
|
if (raise3halarm > 0 && oldestPressure < pressure) {
|
||||||
|
if (Math.abs(diff) > raise3halarm) {
|
||||||
|
if (!didWeAlreadyWarn("lastRaiseWarningTs")) {
|
||||||
|
showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
|
||||||
|
Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise");
|
||||||
|
saveSetting("lastRaiseWarningTs", ts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastRaiseWarningTs", 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveSetting("lastRaiseWarningTs", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history3.push(d);
|
||||||
|
// write data to storage
|
||||||
|
storage.writeJSON(LOG_FILE, history3);
|
||||||
|
|
||||||
|
// calculate 3h average for widget
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < history3.length; i++) {
|
||||||
|
sum += history3[i]["p"];
|
||||||
|
}
|
||||||
|
threeHourAvrPressure = sum / history3.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function baroHandler(data) {
|
||||||
|
if (data) {
|
||||||
|
const pressure = Math.round(data.pressure);
|
||||||
|
if (pressure == undefined || pressure <= 0) return;
|
||||||
|
currentPressures.push(pressure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
turn on barometer power
|
||||||
|
take 5 measurements
|
||||||
|
sort the results
|
||||||
|
take the middle one (median)
|
||||||
|
turn off barometer power
|
||||||
|
*/
|
||||||
|
function check() {
|
||||||
|
Bangle.setBarometerPower(true, "widbaroalarm");
|
||||||
|
setTimeout(function() {
|
||||||
|
currentPressures = [];
|
||||||
|
|
||||||
|
Bangle.getPressure().then(baroHandler);
|
||||||
|
Bangle.getPressure().then(baroHandler);
|
||||||
|
Bangle.getPressure().then(baroHandler);
|
||||||
|
Bangle.getPressure().then(baroHandler);
|
||||||
|
Bangle.getPressure().then(baroHandler);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
Bangle.setBarometerPower(false, "widbaroalarm");
|
||||||
|
|
||||||
|
currentPressures.sort();
|
||||||
|
|
||||||
|
// take median value
|
||||||
|
medianPressure = currentPressures[3];
|
||||||
|
checkForAlarms(medianPressure);
|
||||||
|
}, 1000);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (global.WIDGETS != undefined && typeof WIDGETS === "object") {
|
||||||
|
WIDGETS["baroalarm"] = {
|
||||||
|
width: setting("show") ? 24 : 0,
|
||||||
|
reload: reload,
|
||||||
|
area: "tr",
|
||||||
|
draw: draw
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
g.reset();
|
||||||
|
if (setting("show") && medianPressure != undefined) {
|
||||||
|
g.setFont("6x8", 1).setFontAlign(1, 0);
|
||||||
|
g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6);
|
||||||
|
if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) {
|
||||||
|
g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's delay the first check a bit
|
||||||
|
setTimeout(function() {
|
||||||
|
check();
|
||||||
|
if (interval > 0) {
|
||||||
|
setInterval(check, interval * 60000);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Simple new widget!
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Widget Name
|
||||||
|
|
||||||
|
The days left in month widget is simple and just prints the number of days left in the month in the top left corner.
|
||||||
|
The idea is to encourage people to keep track of time and keep goals they may have for the month.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Hopefully you just have to Install it and it'll work. Customizing the location would just be changing tl to tr.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Shows days left in month
|
||||||
|
* Only updates at midnight.
|
||||||
|
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
Complaints,compliments,problems,suggestions,annoyances,bugs, and all other feedback can be filed at [this repo](https://github.com/N-Onorato/BangleApps)
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Nick
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{ "id": "widmnth",
|
||||||
|
"name": "Days left in month widget",
|
||||||
|
"shortName":"Month Countdown",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "A simple widget that displays the number of days left in the month.",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "widget,date,time,countdown,month",
|
||||||
|
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"widmnth.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
var days_left;
|
||||||
|
var clearCode;
|
||||||
|
|
||||||
|
function getDaysLeft(day) {
|
||||||
|
let year = day.getMonth() == 11 ? day.getFullYear() + 1 : day.getFullYear(); // rollover if december.
|
||||||
|
next_month = new Date(year, (day.getMonth() + 1) % 12, 1, 0, 0, 0);
|
||||||
|
let days_left = Math.floor((next_month - day) / 86400000); // ms left in month divided by ms in a day
|
||||||
|
return days_left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeTilMidnight(now) {
|
||||||
|
let midnight = new Date(now.getTime());
|
||||||
|
midnight.setHours(23);
|
||||||
|
midnight.setMinutes(59);
|
||||||
|
midnight.setSeconds(59);
|
||||||
|
midnight.setMilliseconds(999);
|
||||||
|
return (midnight - now) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
let now = new Date();
|
||||||
|
days_left = getDaysLeft(now);
|
||||||
|
let ms_til_midnight = getTimeTilMidnight(now);
|
||||||
|
clearCode = setTimeout(update, ms_til_midnight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
g.reset();
|
||||||
|
g.setFont("4x6", 3);
|
||||||
|
if(!clearCode) update(); // On first run calculate days left and setup interval to update state.
|
||||||
|
g.drawString(days_left < 10 ? "0" + days_left : days_left.toString(), this.x + 2, this.y + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add your widget
|
||||||
|
WIDGETS.widmonthcountdown={
|
||||||
|
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
|
||||||
|
width: 24,
|
||||||
|
draw:draw
|
||||||
|
};
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 470 B |
2
core
2
core
|
|
@ -1 +1 @@
|
||||||
Subproject commit a7a80a13fa187a4ff5f89669992babca2d95812c
|
Subproject commit affb0b15b41eb35a1548373831af7001bad64435
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */
|
/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Take a look at README.md for hints on developing with this library.
|
Take a look at README.md for hints on developing with this library.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */
|
/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */
|
||||||
/* Exercise Stats module
|
/* Exercise Stats module
|
||||||
|
|
||||||
Take a look at README.md for hints on developing with this library.
|
Take a look at README.md for hints on developing with this library.
|
||||||
|
|
@ -48,6 +48,15 @@ var menu = { ... };
|
||||||
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
|
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
|
||||||
E.showMenu(menu);
|
E.showMenu(menu);
|
||||||
|
|
||||||
|
// Additionally, if your app makes use of the stat notifications, you can display additional menu
|
||||||
|
// settings for configuring when to notify (note the added line in the example below)W
|
||||||
|
|
||||||
|
var menu = { ... };
|
||||||
|
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
|
||||||
|
ExStats.appendNotifyMenuItems(menu, settings, saveSettingsFunction);
|
||||||
|
E.showMenu(menu);
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
var state = {
|
var state = {
|
||||||
active : false, // are we working or not?
|
active : false, // are we working or not?
|
||||||
|
|
@ -63,15 +72,31 @@ var state = {
|
||||||
// cadence // steps per minute adjusted if <1 minute
|
// cadence // steps per minute adjusted if <1 minute
|
||||||
// BPM // beats per minute
|
// BPM // beats per minute
|
||||||
// BPMage // how many seconds was BPM set?
|
// BPMage // how many seconds was BPM set?
|
||||||
|
// Notifies: 0 for disabled, otherwise how often to notify in meters, seconds, or steps
|
||||||
|
notify: {
|
||||||
|
dist: {
|
||||||
|
increment: 0,
|
||||||
|
next: 0,
|
||||||
|
},
|
||||||
|
steps: {
|
||||||
|
increment: 0,
|
||||||
|
next: 0,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
increment: 0,
|
||||||
|
next: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// list of active stats (indexed by ID)
|
// list of active stats (indexed by ID)
|
||||||
var stats = {};
|
var stats = {};
|
||||||
|
|
||||||
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
||||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||||
|
// (Equirectangular approximation)
|
||||||
function calcDistance(a,b) {
|
function calcDistance(a,b) {
|
||||||
function radians(a) { return a*Math.PI/180; }
|
function radians(a) { return a*Math.PI/180; }
|
||||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||||
var y = radians(b.lat-a.lat);
|
var y = radians(b.lat-a.lat);
|
||||||
return Math.sqrt(x*x + y*y) * 6371000;
|
return Math.sqrt(x*x + y*y) * 6371000;
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +139,10 @@ Bangle.on("GPS", function(fix) {
|
||||||
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
|
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
|
||||||
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
|
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
|
||||||
if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]);
|
if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]);
|
||||||
|
if (state.notify.dist.increment > 0 && state.notify.dist.next <= stats["dist"]) {
|
||||||
|
stats["dist"].emit("notify",stats["dist"]);
|
||||||
|
state.notify.dist.next = stats["dist"] + state.notify.dist.increment;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Bangle.on("step", function(steps) {
|
Bangle.on("step", function(steps) {
|
||||||
|
|
@ -121,12 +150,16 @@ Bangle.on("step", function(steps) {
|
||||||
if (stats["step"]) stats["step"].emit("changed",stats["step"]);
|
if (stats["step"]) stats["step"].emit("changed",stats["step"]);
|
||||||
state.stepHistory[0] += steps-state.lastStepCount;
|
state.stepHistory[0] += steps-state.lastStepCount;
|
||||||
state.lastStepCount = steps;
|
state.lastStepCount = steps;
|
||||||
|
if (state.notify.step.increment > 0 && state.notify.step.next <= steps) {
|
||||||
|
stats["step"].emit("notify",stats["step"]);
|
||||||
|
state.notify.step.next = steps + state.notify.step.increment;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Bangle.on("HRM", function(h) {
|
Bangle.on("HRM", function(h) {
|
||||||
if (h.confidence>=60) {
|
if (h.confidence>=60) {
|
||||||
state.BPM = h.bpm;
|
state.BPM = h.bpm;
|
||||||
state.BPMage = 0;
|
state.BPMage = 0;
|
||||||
stats["bpm"].emit("changed",stats["bpm"]);
|
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -137,20 +170,34 @@ exports.getList = function() {
|
||||||
{name: "Distance", id:"dist"},
|
{name: "Distance", id:"dist"},
|
||||||
{name: "Steps", id:"step"},
|
{name: "Steps", id:"step"},
|
||||||
{name: "Heart (BPM)", id:"bpm"},
|
{name: "Heart (BPM)", id:"bpm"},
|
||||||
{name: "Pace (avr)", id:"pacea"},
|
{name: "Pace (avg)", id:"pacea"},
|
||||||
{name: "Pace (current)", id:"pacec"},
|
{name: "Pace (curr)", id:"pacec"},
|
||||||
{name: "Speed", id:"speed"},
|
{name: "Speed", id:"speed"},
|
||||||
{name: "Cadence", id:"caden"},
|
{name: "Cadence", id:"caden"},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
/** Instatiate the given list of statistic IDs (see comments at top)
|
/** Instantiate the given list of statistic IDs (see comments at top)
|
||||||
options = {
|
options = {
|
||||||
paceLength : meters to measure pace over
|
paceLength : meters to measure pace over
|
||||||
|
notify: {
|
||||||
|
dist: {
|
||||||
|
increment: 0 to not notify on distance milestones, otherwise the number of meters to notify after, repeating
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
increment: 0 to not notify on step milestones, otherwise the number of steps to notify after, repeating
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
increment: 0 to not notify on time milestones, otherwise the number of milliseconds to notify after, repeating
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
exports.getStats = function(statIDs, options) {
|
exports.getStats = function(statIDs, options) {
|
||||||
options = options||{};
|
options = options||{};
|
||||||
options.paceLength = options.paceLength||1000;
|
options.paceLength = options.paceLength||1000;
|
||||||
|
options.notify.dist.increment = (options.notify && options.notify.dist && options.notify.dist.increment)||0;
|
||||||
|
options.notify.step.increment = (options.notify && options.notify.step && options.notify.step.increment)||0;
|
||||||
|
options.notify.time.increment = (options.notify && options.notify.time && options.notify.time.increment)||0;
|
||||||
var needGPS,needHRM;
|
var needGPS,needHRM;
|
||||||
// ======================
|
// ======================
|
||||||
if (statIDs.includes("time")) {
|
if (statIDs.includes("time")) {
|
||||||
|
|
@ -159,7 +206,7 @@ exports.getStats = function(statIDs, options) {
|
||||||
getValue : function() { return Date.now()-state.startTime; },
|
getValue : function() { return Date.now()-state.startTime; },
|
||||||
getString : function() { return formatTime(this.getValue()) },
|
getString : function() { return formatTime(this.getValue()) },
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
if (statIDs.includes("dist")) {
|
if (statIDs.includes("dist")) {
|
||||||
needGPS = true;
|
needGPS = true;
|
||||||
stats["dist"]={
|
stats["dist"]={
|
||||||
|
|
@ -221,7 +268,8 @@ exports.getStats = function(statIDs, options) {
|
||||||
setInterval(function() { // run once a second....
|
setInterval(function() { // run once a second....
|
||||||
if (!state.active) return;
|
if (!state.active) return;
|
||||||
// called once a second
|
// called once a second
|
||||||
var duration = Date.now() - state.startTime; // in ms
|
var now = Date.now();
|
||||||
|
var duration = now - state.startTime; // in ms
|
||||||
// set cadence -> steps over last minute
|
// set cadence -> steps over last minute
|
||||||
state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000));
|
state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000));
|
||||||
if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]);
|
if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]);
|
||||||
|
|
@ -235,6 +283,10 @@ exports.getStats = function(statIDs, options) {
|
||||||
state.BPM = 0;
|
state.BPM = 0;
|
||||||
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
|
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
|
||||||
}
|
}
|
||||||
|
if (state.notify.time.increment > 0 && state.notify.time.next <= now) {
|
||||||
|
stats["time"].emit("notify",stats["time"]);
|
||||||
|
state.notify.time.next = now + state.notify.time.increment;
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
function reset() {
|
function reset() {
|
||||||
state.startTime = Date.now();
|
state.startTime = Date.now();
|
||||||
|
|
@ -247,6 +299,16 @@ exports.getStats = function(statIDs, options) {
|
||||||
state.curSpeed = 0;
|
state.curSpeed = 0;
|
||||||
state.BPM = 0;
|
state.BPM = 0;
|
||||||
state.BPMage = 0;
|
state.BPMage = 0;
|
||||||
|
state.notify = options.notify;
|
||||||
|
if (options.notify.dist.increment > 0) {
|
||||||
|
state.notify.dist.next = state.distance + options.notify.dist.increment;
|
||||||
|
}
|
||||||
|
if (options.notify.step.increment > 0) {
|
||||||
|
state.notify.step.next = state.startSteps + options.notify.step.increment;
|
||||||
|
}
|
||||||
|
if (options.notify.time.increment > 0) {
|
||||||
|
state.notify.time.next = state.startTime + options.notify.time.increment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reset();
|
reset();
|
||||||
return {
|
return {
|
||||||
|
|
@ -262,15 +324,50 @@ exports.getStats = function(statIDs, options) {
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.appendMenuItems = function(menu, settings, saveSettings) {
|
exports.appendMenuItems = function(menu, settings, saveSettings) {
|
||||||
var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",];
|
var paceNames = ["1000m", "1 mile", "1/2 Mthn", "Marathon",];
|
||||||
var paceAmts = [1000,1609,21098,42195];
|
var paceAmts = [1000, 1609, 21098, 42195];
|
||||||
menu['Pace'] = {
|
menu['Pace'] = {
|
||||||
min :0, max: paceNames.length-1,
|
min: 0, max: paceNames.length - 1,
|
||||||
value: Math.max(paceAmts.indexOf(settings.paceLength),0),
|
value: Math.max(paceAmts.indexOf(settings.paceLength), 0),
|
||||||
format: v => paceNames[v],
|
format: v => paceNames[v],
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
settings.paceLength = paceAmts[v];
|
settings.paceLength = paceAmts[v];
|
||||||
saveSettings();
|
saveSettings();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
exports.appendNotifyMenuItems = function(menu, settings, saveSettings) {
|
||||||
|
var distNames = ['Off', "1000m","1 mile","1/2 Mthn", "Marathon",];
|
||||||
|
var distAmts = [0, 1000,1609,21098,42195];
|
||||||
|
menu['Ntfy Dist'] = {
|
||||||
|
min: 0, max: distNames.length-1,
|
||||||
|
value: Math.max(distAmts.indexOf(settings.notify.dist.increment),0),
|
||||||
|
format: v => distNames[v],
|
||||||
|
onchange: v => {
|
||||||
|
settings.notify.dist.increment = distAmts[v];
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var stepNames = ['Off', '100', '500', '1000', '5000', '10000'];
|
||||||
|
var stepAmts = [0, 100, 500, 1000, 5000, 10000];
|
||||||
|
menu['Ntfy Steps'] = {
|
||||||
|
min: 0, max: stepNames.length-1,
|
||||||
|
value: Math.max(stepAmts.indexOf(settings.notify.step.increment),0),
|
||||||
|
format: v => stepNames[v],
|
||||||
|
onchange: v => {
|
||||||
|
settings.notify.step.increment = stepAmts[v];
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var timeNames = ['Off', '30s', '1min', '2min', '5min', '10min', '30min', '1hr'];
|
||||||
|
var timeAmts = [0, 30000, 60000, 120000, 300000, 600000, 1800000, 3600000];
|
||||||
|
menu['Ntfy Time'] = {
|
||||||
|
min: 0, max: timeNames.length-1,
|
||||||
|
value: Math.max(timeAmts.indexOf(settings.notify.time.increment),0),
|
||||||
|
format: v => timeNames[v],
|
||||||
|
onchange: v => {
|
||||||
|
settings.notify.time.increment = timeAmts[v];
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue