1
0
Fork 0

Merge branch 'espruino:master' into master

master
Nick Onorato 2022-03-08 20:45:52 -07:00 committed by GitHub
commit b003dd9f74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 971 additions and 146 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

1
apps/clockcal/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial upload

21
apps/clockcal/README.md Normal file
View File

@ -0,0 +1,21 @@
# Clock & Calendar by Michael
This is my "Hello World". I first made this watchface almost 10 years ago for my original Pebble and Pebble Time and I missed this so much, that I had to write it for the BangleJS2.
I know that it seems redundant because there already **is** a *time&cal*-app, but it didn't fit my style.
- locked screen with only one minimal update/minute
- ![locked screen](https://foostuff.github.io/BangleApps/apps/clockcal/screenshot.png)
- unlocked screen (twist?) with seconds
- ![unlocked screen](https://foostuff.github.io/BangleApps/apps/clockcal/screenshot2.png)
## Configurable Features
- Number of calendar rows (weeks)
- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included)
- Clock Mode (24h/12h). Doesn't have an am/pm indicator. It's only there because it was easy.
- First day of the week
- Red Saturday
- Red Sunday
## Feedback
The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings.
So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkECqMCkQACiEDkIXQuUnkUBkESiYXPgN/u8jgEx/8vC6E3k9xiH//8/C6BHCPQMSL6EDO4cgaf4A/ACEC+YFDl4FEAAM/+ISHbIIECh4FB+QWEA4PwCQsfC4gVBkYGDgP/mQ4CCQk/iAXEAQTiCgMiDQQSFiATDBgQXCgILBEQkQBwYrEC4sPLQRpCBwoXECgUCC4oSBAggXHNQRfDV4X/JgQXJBIIXFgYuDC5QKBiE/C4f/bwgXJmanGJgoSDiTQBmQMBE4JYBfwJ5BBYMiYQISEB4IAB+KdCAgfwAwTrCn4SDiczAAMwGwMTmR0CmECBgRSBCQwA/AGsBgEQAgYABAwcHu93s4GBqAXEmLrCiYICmICBj4XEgvABIMMqECiIXCgQXCegLYBC4NwF4VcAQNV4EPkEhF4REBgYXCiQvCu4UCAQMFJYRfKgxGBuxfGLgkjFgMCkMBmEjgEigZaBI4XFMYcRC4kBmRhBkMQgI5DF4MFgAXCLARfCFoIvDkZmBhnF4sA5gvDYghfEHIQJDAAhQBIAPwVQMTgQvCNIMhAwJfBR4MMU4JRB+RJBiUQgUDVwMgYwMBgcwX4amBqBQBiTqBgUQh8RmJhCL4IvC4HMR4ZaEAgIBBL4LBDL5EBmI5BkQvBXwIGBmMPMwMvkEFR4VcR4UgU4MSC4UQmIJBn7dBiQNBqoXBPYNQh8Q+MB+MvgEvG4JyBj8A+RkBhlQd4ZHBiBYCL4bBELxEAA=="))

119
apps/clockcal/app.js Normal file
View File

@ -0,0 +1,119 @@
Bangle.loadWidgets();
var s = Object.assign({
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode
FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
}, require('Storage').readJSON("clockcal.json", true) || {});
const h = g.getHeight();
const w = g.getWidth();
const CELL_W = w / 7;
const CELL_H = 15;
const CAL_Y = h - s.CAL_ROWS * CELL_H;
const DEBUG = false;
function drawMinutes() {
if (DEBUG) console.log("|-->minutes");
var d = new Date();
var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' ');
var minutes = d.getMinutes().toString().padStart(2, '0');
var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00';
var size = 50;
var clock_x = (w - 20) / 2;
if (dimSeconds) {
size = 65;
clock_x = 4 + (w / 2);
}
g.setBgColor(0);
g.setColor(textColor);
g.setFont("Vector", size);
g.setFontAlign(0, 1);
g.drawString(hours + ":" + minutes, clock_x, CAL_Y - 10, 1);
var nextminute = (61 - d.getSeconds());
if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
minuteInterval = setTimeout(drawMinutes, nextminute * 1000);
}
function drawSeconds() {
if (DEBUG) console.log("|--->seconds");
var d = new Date();
g.setColor();
g.fillRect(w - 31, CAL_Y - 36, w - 3, CAL_Y - 19);
g.setBgColor(0);
g.setColor('#fff');
g.setFont("Vector", 24);
g.setFontAlign(1, 1);
g.drawString(" " + d.getSeconds().toString().padStart(2, '0'), w, CAL_Y - 13);
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000);
}
function drawCalendar() {
if (DEBUG) console.log("CALENDAR");
var d = new Date();
g.reset();
g.setBgColor(0);
g.clear();
drawMinutes();
if (!dimSeconds) drawSeconds();
const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6
const today = d.getDate();
var rD = new Date(d.getTime());
rD.setDate(rD.getDate() - dow);
var rDate = rD.getDate();
g.setFontAlign(1, 1);
for (var y = 1; y <= s.CAL_ROWS; y++) {
for (var x = 1; x <= 7; x++) {
bottomrightX = x * CELL_W - 2;
bottomrightY = y * CELL_H + CAL_Y;
g.setFont("Vector", 16);
var fg = ((s.REDSUN && rD.getDay() == 0) || (s.REDSAT && rD.getDay() == 6)) ? '#f00' : '#fff';
if (y == 1 && today == rDate) {
g.setColor('#0f0');
g.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000');
g.drawString(rDate, bottomrightX, bottomrightY);
}
else {
g.setColor(fg);
g.drawString(rDate, bottomrightX, bottomrightY);
}
rD.setDate(rDate + 1);
rDate = rD.getDate();
}
}
Bangle.drawWidgets();
var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1);
if (DEBUG) console.log("Next Day:" + (nextday / 3600));
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
dayInterval = setTimeout(drawCalendar, nextday * 1000);
}
function BTevent() {
drawMinutes();
if (s.BUZZ_ON_BT) {
var interval = (NRF.getSecurityStatus().connected) ? 100 : 500;
Bangle.buzz(interval);
setTimeout(function () { Bangle.buzz(interval); }, interval * 3);
}
}
//register events
Bangle.on('lock', locked => {
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
dimSeconds = locked; //dim seconds if lock=on
drawCalendar();
});
NRF.on('connect', BTevent);
NRF.on('disconnect', BTevent);
dimSeconds = Bangle.isLocked();
drawCalendar();
Bangle.setUI("clock");

BIN
apps/clockcal/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,19 @@
{
"id": "clockcal",
"name": "Clock & Calendar",
"version": "0.01",
"description": "Clock with Calendar",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"clockcal.app.js","url":"app.js"},
{"name":"clockcal.settings.js","url":"settings.js"},
{"name":"clockcal.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"clockcal.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

92
apps/clockcal/settings.js Normal file
View File

@ -0,0 +1,92 @@
(function (back) {
var FILE = "clockcal.json";
settings = Object.assign({
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
menu = {
"": { "title": "Clock & Calendar" },
"< Back": () => back(),
'Buzz(dis)conn.?': {
value: settings.BUZZ_ON_BT,
format: v => v ? "On" : "Off",
onchange: v => {
settings.BUZZ_ON_BT = v;
writeSettings();
}
},
'#Calendar Rows': {
value: settings.CAL_ROWS,
min: 0, max: 6,
onchange: v => {
settings.CAL_ROWS = v;
writeSettings();
}
},
'Clock mode': {
value: settings.MODE24,
format: v => v ? "24h" : "12h",
onchange: v => {
settings.MODE24 = v;
writeSettings();
}
},
'First Day': {
value: settings.FIRSTDAY,
min: 0, max: 6,
format: v => ["Sun", "Sat", "Fri", "Thu", "Wed", "Tue", "Mon"][v],
onchange: v => {
settings.FIRSTDAY = v;
writeSettings();
}
},
'Red Saturday?': {
value: settings.REDSAT,
format: v => v ? "On" : "Off",
onchange: v => {
settings.REDSAT = v;
writeSettings();
}
},
'Red Sunday?': {
value: settings.REDSUN,
format: v => v ? "On" : "Off",
onchange: v => {
settings.REDSUN = v;
writeSettings();
}
},
'Load deafauls?': {
value: 0,
min: 0, max: 1,
format: v => ["No", "Yes"][v],
onchange: v => {
if (v == 1) {
settings = {
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect.
MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
};
writeSettings();
load()
}
}
},
}
// Show the menu
E.showMenu(menu);
})

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

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

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

@ -15,3 +15,4 @@
0.09: Show correct number for log in overwrite prompt
0.10: Fix broken recorder settings (when launched from settings app)
0.11: Fix KML and GPX export when there is no GPS data
0.12: Fix 'Back' label positioning on track/graph display, make translateable

View File

@ -49,11 +49,11 @@ function showMainMenu() {
};
}
const mainmenu = {
'': { 'title': 'Recorder' },
'': { 'title': /*LANG*/'Recorder' },
'< Back': ()=>{load();},
'RECORD': {
/*LANG*/'RECORD': {
value: !!settings.recording,
format: v=>v?"On":"Off",
format: v=>v?/*LANG*/"On":/*LANG*/"Off",
onchange: v => {
setTimeout(function() {
E.showMenu();
@ -66,7 +66,7 @@ function showMainMenu() {
}, 1);
}
},
'File #': {
/*LANG*/'File #': {
value: getTrackNumber(settings.file),
min: 0,
max: 99,
@ -77,8 +77,8 @@ function showMainMenu() {
updateSettings();
}
},
'View Tracks': ()=>{viewTracks();},
'Time Period': {
/*LANG*/'View Tracks': ()=>{viewTracks();},
/*LANG*/'Time Period': {
value: settings.period||10,
min: 1,
max: 120,
@ -103,15 +103,15 @@ function showMainMenu() {
function viewTracks() {
const menu = {
'': { 'title': 'Tracks' }
'': { 'title': /*LANG*/'Tracks' }
};
var found = false;
require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{
found = true;
menu["Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false);
menu[/*LANG*/"Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false);
});
if (!found)
menu["No Tracks found"] = function(){};
menu[/*LANG*/"No Tracks found"] = function(){};
menu['< Back'] = () => { showMainMenu(); };
return E.showMenu(menu);
}
@ -175,38 +175,38 @@ function asTime(v){
function viewTrack(filename, info) {
if (!info) {
E.showMessage("Loading...","Track "+getTrackNumber(filename));
E.showMessage(/*LANG*/"Loading...",/*LANG*/"Track "+getTrackNumber(filename));
info = getTrackInfo(filename);
}
//console.log(info);
const menu = {
'': { 'title': 'Track '+info.fn }
'': { 'title': /*LANG*/'Track '+info.fn }
};
if (info.time)
menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){};
menu["Duration"] = { value : asTime(info.duration)};
menu["Records"] = { value : ""+info.records };
if (info.fields.includes("Latitude"))
menu['Plot Map'] = function() {
menu[/*LANG*/'Plot Map'] = function() {
info.qOSTM = false;
plotTrack(info);
};
if (osm && info.fields.includes("Latitude"))
menu['Plot OpenStMap'] = function() {
menu[/*LANG*/'Plot OpenStMap'] = function() {
info.qOSTM = true;
plotTrack(info);
}
if (info.fields.includes("Altitude"))
menu['Plot Alt.'] = function() {
menu[/*LANG*/'Plot Alt.'] = function() {
plotGraph(info, "Altitude");
};
if (info.fields.includes("Latitude"))
menu['Plot Speed'] = function() {
menu[/*LANG*/'Plot Speed'] = function() {
plotGraph(info, "Speed");
};
// TODO: steps, heart rate?
menu['Erase'] = function() {
E.showPrompt("Delete Track?").then(function(v) {
menu[/*LANG*/'Erase'] = function() {
E.showPrompt(/*LANG*/"Delete Track?").then(function(v) {
if (v) {
settings.recording = false;
updateSettings();
@ -238,7 +238,7 @@ function viewTrack(filename, info) {
}
E.showMenu(); // remove menu
E.showMessage("Drawing...","Track "+info.fn);
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
g.flip(); // on buffered screens, draw a not saying we're busy
g.clear(1);
var s = require("Storage");
@ -305,17 +305,18 @@ function viewTrack(filename, info) {
g.drawString(require("locale").distance(dist),g.getWidth() / 2, g.getHeight() - 20);
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40);
var isBTN3 = "BTN3" in global;
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
setWatch(function() {
viewTrack(info.fn, info);
}, global.BTN3||BTN1);
}, isBTN3?BTN3:BTN1);
Bangle.drawWidgets();
g.flip();
}
function plotGraph(info, style) { "ram"
E.showMenu(); // remove menu
E.showMessage("Calculating...","Track "+info.fn);
E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
var filename = info.filename;
var infn = new Float32Array(80);
var infc = new Uint16Array(80);
@ -334,7 +335,7 @@ function viewTrack(filename, info) {
strt = c[timeIdx];
}
if (style=="Altitude") {
title = "Altitude (m)";
title = /*LANG*/"Altitude (m)";
var altIdx = info.fields.indexOf("Altitude");
while(l!==undefined) {
++nl;c=l.split(",");l = f.readLine(f);
@ -344,7 +345,7 @@ function viewTrack(filename, info) {
infc[i]++;
}
} else if (style=="Speed") {
title = "Speed (m/s)";
title = /*LANG*/"Speed (m/s)";
var latIdx = info.fields.indexOf("Latitude");
var lonIdx = info.fields.indexOf("Longitude");
// skip until we find our first data
@ -404,10 +405,11 @@ function viewTrack(filename, info) {
});
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40);
var isBTN3 = "BTN3" in global;
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
setWatch(function() {
viewTrack(info.filename, info);
}, global.BTN3||BTN1);
}, isBTN3?BTN3:BTN1);
g.flip();
}

View File

@ -2,7 +2,7 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
"version": "0.11",
"version": "0.12",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",

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 @@
0.01: Initial version

View File

@ -0,0 +1,26 @@
# Barometer alarm widget
Get a notification when the pressure reaches defined thresholds.
![Screenshot](screenshot.png)
## 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.01",
"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: 10,
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: 1000,
max: 1100,
step: 10,
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();
});

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

@ -0,0 +1,185 @@
(function() {
let medianPressure;
let threeHourAvrPressure;
let currentPressures = [];
const LOG_FILE = "widbaroalarm.log.json";
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 setting(key) {
return settings[key];
}
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();
}
}
let alreadyWarned = false;
function checkForAlarms(pressure) {
if (pressure == undefined || pressure <= 0) return;
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();
}
history3.push(d);
// write data to storage
storage.writeJSON(LOG_FILE, history3);
if (setting("lowalarm") && pressure <= setting("min")) {
showAlarm("Pressure low: " + Math.round(pressure) + " hPa");
alreadyWarned = true;
}
if (setting("highalarm") && pressure >= setting("max")) {
showAlarm("Pressure high: " + Math.round(pressure) + " hPa");
alreadyWarned = true;
}
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) {
showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop");
}
}
// raise alarm
if (raise3halarm > 0 && oldestPressure < pressure) {
if (Math.abs(diff) > raise3halarm) {
showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise");
}
}
}
}
}
// 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() {
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);
}
}
}
if (global.WIDGETS != undefined && typeof WIDGETS === "object") {
WIDGETS["baroalarm"] = {
width: setting("show") ? 24 : 0,
reload: reload,
area: "tr",
draw: draw
};
}
// Let's delay the first check a bit
setTimeout(function() {
check();
if (interval > 0) {
setInterval(check, interval * 60000);
}
}, 5000);
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

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