forked from FOSS/BangleApps
Merge branch 'master' of https://github.com/peerdavid/BangleApps
commit
3fe7f59b58
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial upload
|
|
@ -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
|
||||
- 
|
||||
- unlocked screen (twist?) with seconds
|
||||
- 
|
||||
|
||||
## 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
|
|
@ -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=="))
|
|
@ -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");
|
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
|
@ -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 |
|
@ -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);
|
||||
})
|
|
@ -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> <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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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");},
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial release
|
|
@ -0,0 +1,15 @@
|
|||
# Rachet Launcher
|
||||
|
||||
Ratchet Launcher is a fork of the default Launcher with modified user interaction. Instead of free scrolling, apps are selected by swiping up and down, but in discrete "ticks", just like in the settings menus.
|
||||
|
||||
**WARNING: Untested on Bangle.js v1! Please test and give feedback.**
|
||||
|
||||
## Usage
|
||||
- Choose app: Swipe up/down (top/bottom button on Bangle.js v1)
|
||||
- Launch app: Tap screen (center button on Bangle.js v1)
|
||||
- Return to clock: Swipe three ticks beyond first/last app in list
|
||||
|
||||
## Installation
|
||||
1. Install Ratchet Launcher using App Loader
|
||||
2. Uninstall default Launcher
|
||||
3. Reload
|
|
@ -0,0 +1,149 @@
|
|||
var Storage = require("Storage");
|
||||
var Layout = require("Layout");
|
||||
|
||||
var font = g.getFonts().includes("6x15") ? "6x15" : "6x8:2";
|
||||
var largeFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:3";
|
||||
var currentApp = 0;
|
||||
var overscroll = 0;
|
||||
var blankImage = Graphics.createImage(` `);
|
||||
var rowHeight = g.getHeight()/3;
|
||||
|
||||
// Load apps list
|
||||
var apps = Storage.list(/\.info$/).map(app=>{
|
||||
var a=Storage.readJSON(app,1);
|
||||
return a&&{
|
||||
name:a.name,
|
||||
type:a.type,
|
||||
icon:a.icon ? Storage.read(a.icon) : a.icon,
|
||||
sortorder:a.sortorder,
|
||||
src:a.src
|
||||
};
|
||||
}).filter(app=>app && (
|
||||
app.type=="app"
|
||||
// || (app.type=="clock" && settings.showClocks)
|
||||
|| !app.type
|
||||
));
|
||||
apps.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
if (a.name<b.name) return -1;
|
||||
if (a.name>b.name) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Uncomment for testing in the emulator without apps:
|
||||
// apps = [
|
||||
// {
|
||||
// name:"Test",
|
||||
// type:"app",
|
||||
// icon:blankImage,
|
||||
// sortorder:undefined,
|
||||
// src:""
|
||||
// },
|
||||
// {
|
||||
// name:"Test 2",
|
||||
// type:"app",
|
||||
// icon:blankImage,
|
||||
// sortorder:undefined,
|
||||
// src:""
|
||||
// },
|
||||
// ];
|
||||
|
||||
// Initialize layout
|
||||
var layout = new Layout({
|
||||
type:"v", c:[
|
||||
// A row for the previous app
|
||||
{ type:"h", height:rowHeight, c:[
|
||||
{type: "img", id:"prev_icon", src:blankImage, width:48, height:48, scale:0.8, pad:8},
|
||||
{type: "txt", id:"prev_name", label:"", font:font, fillx:1, wrap:1},
|
||||
]},
|
||||
// A row for the current app
|
||||
{ type:"h", height:rowHeight, c:[
|
||||
{type: "img", id:"cur_icon", src:blankImage, width:48, height:48},
|
||||
{type: "txt", id:"cur_name", label:"", font:largeFont, fillx:1, wrap:1},
|
||||
]},
|
||||
// A row for the next app
|
||||
{ type:"h", height:rowHeight, c:[
|
||||
{type: "img", id:"next_icon", src:blankImage, width:48, height:48, scale:0.8, pad:8},
|
||||
{type: "txt", id:"next_name", label:"", font:font, fillx:1, wrap:1},
|
||||
]},
|
||||
]
|
||||
});
|
||||
|
||||
// Drawing logic
|
||||
function render() {
|
||||
if (!apps.length) {
|
||||
E.showMessage(/*LANG*/"No apps");
|
||||
return load();
|
||||
}
|
||||
|
||||
// Previous app
|
||||
if (currentApp > 0) {
|
||||
layout.prev_icon.src = apps[currentApp-1].icon;
|
||||
layout.prev_name.label = apps[currentApp-1].name;
|
||||
} else {
|
||||
layout.prev_icon.src = blankImage;
|
||||
layout.prev_name.label = "";
|
||||
}
|
||||
|
||||
// Current app
|
||||
layout.cur_icon.src = apps[currentApp].icon;
|
||||
layout.cur_name.label = apps[currentApp].name;
|
||||
|
||||
// Next app
|
||||
if (currentApp < apps.length-1) {
|
||||
layout.next_icon.src = apps[currentApp+1].icon;
|
||||
layout.next_name.label = apps[currentApp+1].name;
|
||||
} else {
|
||||
layout.next_icon.src = blankImage;
|
||||
layout.next_name.label = "";
|
||||
}
|
||||
|
||||
g.clear();
|
||||
layout.render();
|
||||
}
|
||||
|
||||
// Launch the currently selected app
|
||||
function launch() {
|
||||
var app = apps[currentApp];
|
||||
if (!app) return;
|
||||
if (!app.src || Storage.read(app.src)===undefined) {
|
||||
E.showMessage(/*LANG*/"App Source\nNot found");
|
||||
setTimeout(render, 2000);
|
||||
} else {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
load(app.src);
|
||||
}
|
||||
}
|
||||
|
||||
// Select previous/next app
|
||||
function move(step) {
|
||||
if ((currentApp == 0 && step < 0) || (currentApp >= apps.length-1 && step > 0)) {
|
||||
// When we hit the end of the list (top or bottom), the step is
|
||||
// counted towards the overscroll value. When the overscroll
|
||||
// threshold is exceeded, we return to the clock face.
|
||||
overscroll += step;
|
||||
} else {
|
||||
// This is the default case: the step is countedf towards the currentApp index
|
||||
currentApp += step;
|
||||
overscroll = 0;
|
||||
return render();
|
||||
}
|
||||
|
||||
// Overscroll threshold reached, return to clock
|
||||
if (Math.abs(overscroll) > 3) {
|
||||
Bangle.buzz(500, 1);
|
||||
return load();
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up user input
|
||||
Bangle.setUI('updown', dir => {
|
||||
if (!dir) launch();
|
||||
else {
|
||||
if (process.env.HWVERSION==2) dir *= -1; // "natural scrolling" on touch screen
|
||||
move(dir);
|
||||
}
|
||||
});
|
||||
|
||||
render();
|
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"id": "ratchet_launch",
|
||||
"name": "Ratchet Launcher",
|
||||
"shortName": "Ratchet",
|
||||
"version": "0.01",
|
||||
"description": "Launcher with discrete scrolling for quicker app selection",
|
||||
"icon": "app.png",
|
||||
"type": "launch",
|
||||
"tags": "tool,system,launcher",
|
||||
"supports": ["BANGLEJS2","BANGLEJS"],
|
||||
"storage": [
|
||||
{"name":"ratchet_launch.app.js","url":"app.js"}
|
||||
],
|
||||
"sortorder": -10,
|
||||
"readme":"README.md"
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial version
|
|
@ -0,0 +1,26 @@
|
|||
# Barometer alarm widget
|
||||
|
||||
Get a notification when the pressure reaches defined thresholds.
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
|
||||
* Interval: check interval of sensor data in minutes. 0 to disable automatic check.
|
||||
* Low alarm: Toggle low alarm
|
||||
* Low threshold: Warn when pressure drops below this value
|
||||
* High alarm: Toggle high alarm
|
||||
* High threshold: Warn when pressure exceeds above this value
|
||||
* Drop alarm: Warn when pressure drops more than this value in the recent 3 hours (having at least 30 min of data)
|
||||
0 to disable this alarm.
|
||||
* Raise alarm: Warn when pressure raises more than this value in the recent 3 hours (having at least 30 min of data)
|
||||
0 to disable this alarm.
|
||||
* Show widget: Enable/disable widget visibility
|
||||
* Buzz on alarm: Enable/disable buzzer on alarm
|
||||
|
||||
|
||||
## Widget
|
||||
The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours.
|
||||
|
||||
## Creator
|
||||
Marco ([myxor](https://github.com/myxor))
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"buzz": true,
|
||||
"lowalarm": false,
|
||||
"min": 950,
|
||||
"highalarm": false,
|
||||
"max": 1030,
|
||||
"drop3halarm": 2,
|
||||
"raise3halarm": 0,
|
||||
"show": true,
|
||||
"interval": 15
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id": "widbaroalarm",
|
||||
"name": "Barometer alarm widget",
|
||||
"shortName": "Barometer alarm",
|
||||
"version": "0.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"}]
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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 |
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue