messages: auto-open music

master
Richard de Boer 2022-03-10 20:41:53 +01:00
commit d492c95442
No known key found for this signature in database
GPG Key ID: 8721727971871937
42 changed files with 1142 additions and 180 deletions

View File

@ -243,6 +243,7 @@ and which gives information about the app for the Launcher.
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) -
// 'app' - an application
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget
// 'launch' - replacement launcher app
// 'bootloader' - code that runs at startup only

View File

@ -20,3 +20,4 @@
0.07: Recorder icon only blue if values actually arive
Adds some preset modes and a custom one
Restructure the settings menu
0.08: Allow scanning for devices in settings

View File

@ -23,7 +23,10 @@
}
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){
@ -361,7 +364,13 @@
var promise;
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){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
@ -488,11 +497,15 @@
if (gatt) {
if (gatt.connected){
log("Disconnect with gatt: ", gatt);
gatt.disconnect().then(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect", e);
});
try{
gatt.disconnect().then(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect promise", e);
});
} catch (e){
log("Error during disconnect attempt", e);
}
}
}
}

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.07",
"version": "0.08",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",

View File

@ -17,54 +17,73 @@
var settings;
readSettings();
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Mode': {
value: 0 | settings.mode,
min: 0,
max: 3,
format: v => ["Off", "Default", "Both", "Custom"][v],
onchange: v => {
settings.mode = v;
switch (v){
case 0:
writeSettings("enabled",false);
break;
case 1:
writeSettings("enabled",true);
writeSettings("replace",true);
writeSettings("debuglog",false);
writeSettings("startWithHrm",true);
writeSettings("allowFallback",true);
writeSettings("fallbackTimeout",10);
break;
case 2:
writeSettings("enabled",true);
writeSettings("replace",false);
writeSettings("debuglog",false);
writeSettings("startWithHrm",false);
writeSettings("allowFallback",false);
break;
case 3:
writeSettings("enabled",true);
writeSettings("replace",settings.custom_replace);
writeSettings("debuglog",settings.custom_debuglog);
writeSettings("startWithHrm",settings.custom_startWithHrm);
writeSettings("allowFallback",settings.custom_allowFallback);
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
break;
function buildMainMenu(){
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Mode': {
value: 0 | settings.mode,
min: 0,
max: 3,
format: v => ["Off", "Default", "Both", "Custom"][v],
onchange: v => {
settings.mode = v;
switch (v){
case 0:
writeSettings("enabled",false);
break;
case 1:
writeSettings("enabled",true);
writeSettings("replace",true);
writeSettings("debuglog",false);
writeSettings("startWithHrm",true);
writeSettings("allowFallback",true);
writeSettings("fallbackTimeout",10);
break;
case 2:
writeSettings("enabled",true);
writeSettings("replace",false);
writeSettings("debuglog",false);
writeSettings("startWithHrm",false);
writeSettings("allowFallback",false);
break;
case 3:
writeSettings("enabled",true);
writeSettings("replace",settings.custom_replace);
writeSettings("debuglog",settings.custom_debuglog);
writeSettings("startWithHrm",settings.custom_startWithHrm);
writeSettings("allowFallback",settings.custom_allowFallback);
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
break;
}
writeSettings("mode",v);
}
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 = {
'' : { title: "Debug"},
'< Back': function() { E.showMenu(mainmenu); },
'< Back': function() { E.showMenu(buildMainMenu()); },
'Alert on disconnect': {
value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
@ -81,10 +100,41 @@
},
'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 = {
'' : { title: "Custom mode"},
'< Back': function() { E.showMenu(mainmenu); },
'< Back': function() { E.showMenu(buildMainMenu()); },
'Replace HRM': {
value: !!settings.custom_replace,
format: v => settings.custom_replace ? "On" : "Off",
@ -165,7 +215,7 @@
var submenu = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(mainmenu); },
'< Back': function() { E.showMenu(buildMainMenu()); },
'Request': {
value: settings.gracePeriodRequest,
min: 0,
@ -208,5 +258,5 @@
}
};
E.showMenu(mainmenu);
E.showMenu(buildMainMenu());
})

View File

@ -16,12 +16,12 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jshint/2.11.0/jshint.min.js"></script>
<p>Type your javascript code here</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>&nbsp;<span id="btninfo" style="color:orange"></span></p>
<script>
const item = "custom.boot.js";
const id = "custom-js";
const sample = "//Bangle.setOptions({wakeOnBTN2:false});";
var localeModule = null;
var customBootCode = null;
var editor = {};
if (localStorage.getItem(item) === null) {
@ -48,15 +48,30 @@
gutters: ["CodeMirror-linenumbers", "CodeMirror-lint-markers"],
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() {
if (!editor.state.lint.marked.length) {
localeModule = editor.getValue();
localStorage.setItem(item, localeModule);
sendCustomizedApp({
storage: [{ name: item, content: localeModule }]
});
}
if (!hasWarnings()) {
customBootCode = editor.getValue();
localStorage.setItem(item, customBootCode);
sendCustomizedApp({
storage: [{ name: item, content: customBootCode }]
});
}
});
</script>
</body>

View File

@ -2,7 +2,7 @@
"id": "gbridge",
"name": "Gadgetbridge",
"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",
"type": "widget",
"tags": "tool,system,android,widget",

View File

@ -7,7 +7,7 @@
0.06: Remove translations if not required
Ensure 'on' is always supplied for translations
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.10: Apply 12hour setting to time
0.11: Added translations for nl_NL and changes one formatting

View File

@ -35,6 +35,7 @@
Allow repeat to be switched Off, so there is no buzzing repetition.
Also gave the widget a pixel more room to the right
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.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

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.25",
"version": "0.26",
"description": "App to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -1,5 +1,10 @@
WIDGETS["messages"]={area:"tl", width:0, iconwidth:24,
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);
if (!this.width) return;
var c = (Date.now()-this.t)/1000;
@ -11,7 +16,7 @@ draw:function() {
this.l = Date.now();
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);
},show:function(quiet) {
WIDGETS["messages"].t=Date.now(); // first time

View File

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

View File

@ -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)

BIN
apps/powermanager/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

29
apps/powermanager/boot.js Normal file
View File

@ -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());
}
})();

View File

@ -0,0 +1,4 @@
{
"warnEnabled": false,
"warn": 96
}

View File

@ -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"}
]
}

View File

@ -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);
})

View File

@ -5,4 +5,5 @@
0.05: Avoid immediately redrawing widgets on load
0.06: Fix: don't try to redraw widget when widgets not loaded
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

View File

@ -1,8 +1,8 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
const modeNames = ["Off", "Alarms", "Silent"];
const modeNames = [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"];
const B2 = process.env.HWVERSION===2;
// load global settings
let bSettings = require('Storage').readJSON('setting.json',true)||{};
let current = 0|bSettings.quiet;
@ -109,34 +109,26 @@ function setAppQuietMode(mode) {
let m;
function showMainMenu() {
let menu = {
"": {"title": "Quiet Mode"},
"< Exit": () => load()
};
// "Current Mode""Silent" won't fit on Bangle.js 2
menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = {
let menu = {"": {"title": /*LANG*/"Quiet Mode"},};
menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Exit"] = () => {load();};
menu[/*LANG*/"Current Mode"] = {
value: current,
min:0, max:2, wrap: true,
format: () => modeNames[current],
format: v => modeNames[v],
onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current`
};
scheds.sort((a, b) => (a.hr-b.hr));
scheds.forEach((sched, idx) => {
menu[formatTime(sched.hr)] = {
format: () => modeNames[sched.mode], // abuse format to right-align text
onchange: () => {
m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange)
showEditMenu(idx);
}
};
menu[formatTime(sched.hr)] = () => { showEditMenu(idx); };
menu[formatTime(sched.hr)].format = () => modeNames[sched.mode]+' >'; // this does nothing :-(
});
menu["Add Schedule"] = () => showEditMenu(-1);
menu["Switch Theme"] = {
menu[/*LANG*/"Add Schedule"] = () => showEditMenu(-1);
menu[/*LANG*/"Switch Theme"] = {
value: !!get("switchTheme"),
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
};
menu["LCD Settings"] = () => showOptionsMenu();
menu[/*LANG*/"LCD Settings"] = () => showOptionsMenu();
m = E.showMenu(menu);
}
@ -150,25 +142,23 @@ function showEditMenu(index) {
mins = Math.round((s.hr-hrs)*60);
mode = s.mode;
}
const menu = {
"": {"title": (isNew ? "Add" : "Edit")+" Schedule"},
"< Cancel": () => showMainMenu(),
"Hours": {
value: hrs,
min:0, max:23, wrap:true,
onchange: v => {hrs = v;},
},
"Minutes": {
value: mins,
min:0, max:55, step:5, wrap:true,
onchange: v => {mins = v;},
},
"Switch to": {
value: mode,
min:0, max:2, wrap:true,
format: v => modeNames[v],
onchange: v => {mode = v;},
},
let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}};
menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Cancel"] = () => showMainMenu();
menu[/*LANG*/"Hours"] = {
value: hrs,
min:0, max:23, wrap:true,
onchange: v => {hrs = v;},
};
menu[/*LANG*/"Minutes"] = {
value: mins,
min:0, max:55, step:5, wrap:true,
onchange: v => {mins = v;},
};
menu[/*LANG*/"Switch to"] = {
value: mode,
min:0, max:2, wrap:true,
format: v => modeNames[v],
onchange: v => {mode = v;},
};
function getSched() {
return {
@ -176,7 +166,7 @@ function showEditMenu(index) {
mode: mode,
};
}
menu["> Save"] = function() {
menu[B2 ? /*LANG*/"Save" : /*LANG*/"> Save"] = function() {
if (isNew) {
scheds.push(getSched());
} else {
@ -186,7 +176,7 @@ function showEditMenu(index) {
showMainMenu();
};
if (!isNew) {
menu["> Delete"] = function() {
menu[B2 ? /*LANG*/"Delete" : /*LANG*/"> Delete"] = function() {
scheds.splice(index, 1);
save();
showMainMenu();
@ -196,7 +186,7 @@ function showEditMenu(index) {
}
function showOptionsMenu() {
const disabledFormat = v => v ? "Off" : "-";
const disabledFormat = v => v ? /*LANG*/"Off" : "-";
function toggle(option) {
// we disable wakeOn* events by setting them to `false` in options
// not disabled = not present in options at all
@ -209,9 +199,9 @@ function showOptionsMenu() {
}
let resetTimeout;
const oMenu = {
"": {"title": "LCD Settings"},
"< Back": () => showMainMenu(),
"LCD Brightness": {
"": {"title": /*LANG*/"LCD Settings"},
/*LANG*/"< Back": () => showMainMenu(),
/*LANG*/"LCD Brightness": {
value: get("brightness", 0),
min: 0, // 0 = use default
max: 1,
@ -233,7 +223,7 @@ function showOptionsMenu() {
}
},
},
"LCD Timeout": {
/*LANG*/"LCD Timeout": {
value: get("timeout", 0),
min: 0, // 0 = use default (no constant on for quiet mode)
max: 60,
@ -246,17 +236,17 @@ function showOptionsMenu() {
},
// we disable wakeOn* events by overwriting them as false in options
// not disabled = not present in options at all
"Wake on FaceUp": {
/*LANG*/"Wake on FaceUp": {
value: "wakeOnFaceUp" in options,
format: disabledFormat,
onchange: () => {toggle("wakeOnFaceUp");},
},
"Wake on Touch": {
/*LANG*/"Wake on Touch": {
value: "wakeOnTouch" in options,
format: disabledFormat,
onchange: () => {toggle("wakeOnTouch");},
},
"Wake on Twist": {
/*LANG*/"Wake on Twist": {
value: "wakeOnTwist" in options,
format: disabledFormat,
onchange: () => {toggle("wakeOnTwist");},

View File

@ -2,7 +2,7 @@
"id": "qmsched",
"name": "Quiet Mode Schedule and Widget",
"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.",
"icon": "app.png",
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},

View File

@ -6,3 +6,4 @@
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.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

View File

@ -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
shown will increase, even if you are standing still.
* `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
* `STEPS` - Steps since you started exercising
* `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
`Run` doesn't directly allow you to record your tracks at the moment.
However you can just install the `Recorder` app, turn recording on in
that, and then start the `Run` app.
When the `Recorder` app is installed, `Run` will automatically start and stop tracks
as needed, prompting you to overwrite or begin a new track if necessary.
## 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 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
* `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.
If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information
if you wish by setting the last 2 boxes to `-`.
* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display.
Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence".
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
* Allow this app to trigger the `Recorder` app on and off directly.
* Keep a log of each run's stats (distance/steps/etc)
## Development

View File

@ -1,5 +1,5 @@
var ExStats = require("exstats");
var B2 = process.env.HWVERSION==2;
var B2 = process.env.HWVERSION===2;
var Layout = require("Layout");
var locale = require("locale");
var fontHeading = "6x8:2";
@ -14,46 +14,72 @@ Bangle.drawWidgets();
// ---------------------------
let settings = Object.assign({
record : true,
B1 : "dist",
B2 : "time",
B3 : "pacea",
B4 : "bpm",
B5 : "step",
B6 : "caden",
paceLength : 1000
record: true,
B1: "dist",
B2: "time",
B3: "pacea",
B4: "bpm",
B5: "step",
B6: "caden",
paceLength: 1000,
notify: {
dist: {
value: 0,
notifications: [],
},
step: {
value: 0,
notifications: [],
},
time: {
value: 0,
notifications: [],
},
},
}, 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);
// ---------------------------
// Called to start/stop running
function onStartStop() {
var running = !exs.state.active;
if (running) {
exs.start();
} else {
exs.stop();
}
layout.button.label = running ? "STOP" : "START";
layout.status.label = running ? "RUN" : "STOP";
layout.status.bgCol = running ? "#0f0" : "#f00";
// if stopping running, don't clear state
// so we can at least refer to what we've done
layout.render();
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;
WIDGETS["recorder"].setRecording(true).then(() => {
isMenuDisplayed = false;
layout.forgetLazyState();
layout.render();
});
prepPromises.push(
WIDGETS["recorder"].setRecording(true).then(() => {
isMenuDisplayed = false;
layout.forgetLazyState();
layout.render();
})
);
} else {
WIDGETS["recorder"].setRecording(false);
prepPromises.push(
WIDGETS["recorder"].setRecording(false)
);
}
}
Promise.all(prepPromises)
.then(() => {
if (running) {
exs.start();
} else {
exs.stop();
}
layout.button.label = running ? "STOP" : "START";
layout.status.label = running ? "RUN" : "STOP";
layout.status.bgCol = running ? "#0f0" : "#f00";
// if stopping running, don't clear state
// so we can at least refer to what we've done
layout.render();
});
}
var lc = [];
@ -84,11 +110,27 @@ var layout = new Layout( {
delete lc;
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
Bangle.on("GPS", function(fix) {
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
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
}
});

View File

@ -1,6 +1,6 @@
{ "id": "run",
"name": "Run",
"version":"0.07",
"version":"0.08",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",

View File

@ -9,14 +9,28 @@
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage')
let settings = Object.assign({
record : true,
B1 : "dist",
B2 : "time",
B3 : "pacea",
B4 : "bpm",
B5 : "step",
B6 : "caden",
paceLength : 1000
record: true,
B1: "dist",
B2: "time",
B3: "pacea",
B4: "bpm",
B5: "step",
B6: "caden",
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) || {});
function saveSettings() {
storage.write(SETTINGS_FILE, settings)
@ -24,7 +38,7 @@
function getBoxChooser(boxID) {
return {
min :0, max: statsIDs.length-1,
min: 0, max: statsIDs.length-1,
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
format: v => statsList[v].name,
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 = {
'': { 'title': 'Run' },
'< Back': back,
@ -47,8 +69,55 @@
saveSettings();
}
};
var notificationsMenu = {
'< Back': function() { E.showMenu(menu) },
}
menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)};
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 2': getBoxChooser("B2"),
'Box 3': getBoxChooser("B3"),
@ -56,5 +125,6 @@
'Box 5': getBoxChooser("B5"),
'Box 6': getBoxChooser("B6"),
});
menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)};
E.showMenu(menu);
})

View File

@ -0,0 +1,2 @@
0.01: Initial version
0.02: Do not warn multiple times for the same exceedance

View File

@ -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))

View File

@ -0,0 +1,11 @@
{
"buzz": true,
"lowalarm": false,
"min": 950,
"highalarm": false,
"max": 1030,
"drop3halarm": 2,
"raise3halarm": 0,
"show": true,
"interval": 15
}

View File

@ -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"}]
}

View File

@ -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();
});

239
apps/widbaroalarm/widget.js Normal file
View File

@ -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

1
apps/widmnth/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Simple new widget!

22
apps/widmnth/README.md Normal file
View File

@ -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

View File

@ -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"}
]
}

42
apps/widmnth/widget.js Normal file
View File

@ -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
};
})();

BIN
apps/widmnth/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

2
core

@ -1 +1 @@
Subproject commit a7a80a13fa187a4ff5f89669992babca2d95812c
Subproject commit affb0b15b41eb35a1548373831af7001bad64435

View File

@ -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.

View File

@ -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
Take a look at README.md for hints on developing with this library.
@ -48,6 +48,15 @@ var menu = { ... };
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
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 = {
active : false, // are we working or not?
@ -63,15 +72,31 @@ var state = {
// cadence // steps per minute adjusted if <1 minute
// BPM // beats per minute
// 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)
var stats = {};
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
// https://www.movable-type.co.uk/scripts/latlong.html
// (Equirectangular approximation)
function calcDistance(a,b) {
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);
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["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
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) {
@ -121,12 +150,16 @@ Bangle.on("step", function(steps) {
if (stats["step"]) stats["step"].emit("changed",stats["step"]);
state.stepHistory[0] += steps-state.lastStepCount;
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) {
if (h.confidence>=60) {
state.BPM = h.bpm;
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: "Steps", id:"step"},
{name: "Heart (BPM)", id:"bpm"},
{name: "Pace (avr)", id:"pacea"},
{name: "Pace (current)", id:"pacec"},
{name: "Pace (avg)", id:"pacea"},
{name: "Pace (curr)", id:"pacec"},
{name: "Speed", id:"speed"},
{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 = {
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) {
options = options||{};
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;
// ======================
if (statIDs.includes("time")) {
@ -159,7 +206,7 @@ exports.getStats = function(statIDs, options) {
getValue : function() { return Date.now()-state.startTime; },
getString : function() { return formatTime(this.getValue()) },
};
};
}
if (statIDs.includes("dist")) {
needGPS = true;
stats["dist"]={
@ -221,7 +268,8 @@ exports.getStats = function(statIDs, options) {
setInterval(function() { // run once a second....
if (!state.active) return;
// 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
state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000));
if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]);
@ -235,6 +283,10 @@ exports.getStats = function(statIDs, options) {
state.BPM = 0;
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);
function reset() {
state.startTime = Date.now();
@ -247,6 +299,16 @@ exports.getStats = function(statIDs, options) {
state.curSpeed = 0;
state.BPM = 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();
return {
@ -262,15 +324,50 @@ exports.getStats = function(statIDs, options) {
};
exports.appendMenuItems = function(menu, settings, saveSettings) {
var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",];
var paceAmts = [1000,1609,21098,42195];
var paceNames = ["1000m", "1 mile", "1/2 Mthn", "Marathon",];
var paceAmts = [1000, 1609, 21098, 42195];
menu['Pace'] = {
min :0, max: paceNames.length-1,
value: Math.max(paceAmts.indexOf(settings.paceLength),0),
min: 0, max: paceNames.length - 1,
value: Math.max(paceAmts.indexOf(settings.paceLength), 0),
format: v => paceNames[v],
onchange: v => {
settings.paceLength = paceAmts[v];
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();
},
};
};