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 "screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) - "type":"...", // optional(if app) -
// 'app' - an application // 'app' - an application
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget // 'widget' - a widget
// 'launch' - replacement launcher app // 'launch' - replacement launcher app
// 'bootloader' - code that runs at startup only // 'bootloader' - code that runs at startup only

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> <script src="https://cdnjs.cloudflare.com/ajax/libs/jshint/2.11.0/jshint.min.js"></script>
<p>Type your javascript code here</p> <p>Type your javascript code here</p>
<p><textarea id="custom-js"></textarea></p> <p><textarea id="custom-js"></textarea></p>
<p>Then click <button id="upload" class="btn btn-primary">Upload</button></p> <p>Then click <button id="upload" class="btn btn-primary">Upload</button>&nbsp;<span id="btninfo" style="color:orange"></span></p>
<script> <script>
const item = "custom.boot.js"; const item = "custom.boot.js";
const id = "custom-js"; const id = "custom-js";
const sample = "//Bangle.setOptions({wakeOnBTN2:false});"; const sample = "//Bangle.setOptions({wakeOnBTN2:false});";
var localeModule = null; var customBootCode = null;
var editor = {}; var editor = {};
if (localStorage.getItem(item) === null) { if (localStorage.getItem(item) === null) {
@ -48,15 +48,30 @@
gutters: ["CodeMirror-linenumbers", "CodeMirror-lint-markers"], gutters: ["CodeMirror-linenumbers", "CodeMirror-lint-markers"],
lineNumbers: true lineNumbers: true
}); });
function hasWarnings() {
return editor.state.lint.marked.length!=0;
}
editor.on("change", function() {
setTimeout(function() {
if (hasWarnings()) {
document.getElementById("btninfo").innerHTML = "There are warnings in the code to be uploaded";
document.getElementById("upload").classList.add("disabled");
} else {
document.getElementById("btninfo").innerHTML = "";
document.getElementById("upload").classList.remove("disabled");
}
}, 500);
})
document.getElementById("upload").addEventListener("click", function() { document.getElementById("upload").addEventListener("click", function() {
if (!editor.state.lint.marked.length) { if (!hasWarnings()) {
localeModule = editor.getValue(); customBootCode = editor.getValue();
localStorage.setItem(item, localeModule); localStorage.setItem(item, customBootCode);
sendCustomizedApp({ sendCustomizedApp({
storage: [{ name: item, content: localeModule }] storage: [{ name: item, content: customBootCode }]
}); });
} }
}); });
</script> </script>
</body> </body>

View File

@ -7,7 +7,7 @@
0.06: Remove translations if not required 0.06: Remove translations if not required
Ensure 'on' is always supplied for translations Ensure 'on' is always supplied for translations
0.07: Improve handling of non-ASCII characters (fix #469) 0.07: Improve handling of non-ASCII characters (fix #469)
0.08: Added Mavigation units and en_NAV 0.08: Added Navigation units and en_NAV
0.09: Added New Zealand en_NZ 0.09: Added New Zealand en_NZ
0.10: Apply 12hour setting to time 0.10: Apply 12hour setting to time
0.11: Added translations for nl_NL and changes one formatting 0.11: Added translations for nl_NL and changes one formatting

View File

@ -5,4 +5,5 @@
0.05: Avoid immediately redrawing widgets on load 0.05: Avoid immediately redrawing widgets on load
0.06: Fix: don't try to redraw widget when widgets not loaded 0.06: Fix: don't try to redraw widget when widgets not loaded
0.07: Option to switch theme 0.07: Option to switch theme
Changed time selection to 5-minute intervals Changed time selection to 5-minute intervals
0.08: Support new Bangle.js 2 menu

View File

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

View File

@ -2,7 +2,7 @@
"id": "qmsched", "id": "qmsched",
"name": "Quiet Mode Schedule and Widget", "name": "Quiet Mode Schedule and Widget",
"shortName": "Quiet Mode", "shortName": "Quiet Mode",
"version": "0.07", "version": "0.08",
"description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.", "description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},

View File

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

View File

@ -2,7 +2,7 @@
"id": "recorder", "id": "recorder",
"name": "Recorder", "name": "Recorder",
"shortName": "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.", "description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,outdoors,gps,widget", "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.05: exstats updated so update 'distance' label is updated, option for 'speed'
0.06: Add option to record a run using the recorder app automatically 0.06: Add option to record a run using the recorder app automatically
0.07: Fix crash if an odd number of active boxes are configured (fix #1473) 0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
0.08: Added support for notifications from exstats. Support all stats from exstats

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 the GPS updates your position as it gets more satellites your position changes and the distance
shown will increase, even if you are standing still. shown will increase, even if you are standing still.
* `TIME` - the elapsed time for your run * `TIME` - the elapsed time for your run
* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far** * `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far**
* `HEART` - Your heart rate * `HEART` - Your heart rate
* `STEPS` - Steps since you started exercising * `STEPS` - Steps since you started exercising
* `CADENCE` - Steps per second based on your step rate *over the last minute* * `CADENCE` - Steps per second based on your step rate *over the last minute*
@ -24,9 +24,8 @@ so if you have no GPS lock you just need to wait.
## Recording Tracks ## Recording Tracks
`Run` doesn't directly allow you to record your tracks at the moment. When the `Recorder` app is installed, `Run` will automatically start and stop tracks
However you can just install the `Recorder` app, turn recording on in as needed, prompting you to overwrite or begin a new track if necessary.
that, and then start the `Run` app.
## Settings ## Settings
@ -35,13 +34,29 @@ Under `Settings` -> `App` -> `Run` you can change settings for this app.
* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically * `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically
record GPS/HRM/etc data every time you start a run? record GPS/HRM/etc data every time you start a run?
* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon * `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon
* `Box 1/2/3/4/5/6` are what should be shown in each of the 6 boxes on the display. From the top left, down. * `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display.
If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence".
if you wish by setting the last 2 boxes to `-`. Any box set to "-" will display no information.
* Box 1 is the top left (defaults to "Distance")
* Box 2 is the top right (defaults to "Time")
* Box 3 is the middle left (defaults to "Pace (avg)")
* Box 4 is the middle right (defaults to "Heart (BPM)")
* Box 5 is the bottom left (defaults to "Steps")
* Box 6 is the bottom right (defaults to "Cadence")
* `Notifications` leads to a submenu where you can configure if the app will notify you after
your distance, steps, or time repeatedly pass your configured thresholds
* `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options
* "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon"
* `Ntfy Steps`: The number of steps that must pass before you are notified.
* "Off" (default), 100, 500, 1000, 5000, 10000
* `Ntfy Time`: The amount of time that must pass before you are notified.
* "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour"
* `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold
* `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold
* `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold
## TODO ## TODO
* Allow this app to trigger the `Recorder` app on and off directly.
* Keep a log of each run's stats (distance/steps/etc) * Keep a log of each run's stats (distance/steps/etc)
## Development ## Development

View File

@ -1,5 +1,5 @@
var ExStats = require("exstats"); var ExStats = require("exstats");
var B2 = process.env.HWVERSION==2; var B2 = process.env.HWVERSION===2;
var Layout = require("Layout"); var Layout = require("Layout");
var locale = require("locale"); var locale = require("locale");
var fontHeading = "6x8:2"; var fontHeading = "6x8:2";
@ -14,46 +14,72 @@ Bangle.drawWidgets();
// --------------------------- // ---------------------------
let settings = Object.assign({ let settings = Object.assign({
record : true, record: true,
B1 : "dist", B1: "dist",
B2 : "time", B2: "time",
B3 : "pacea", B3: "pacea",
B4 : "bpm", B4: "bpm",
B5 : "step", B5: "step",
B6 : "caden", B6: "caden",
paceLength : 1000 paceLength: 1000,
notify: {
dist: {
value: 0,
notifications: [],
},
step: {
value: 0,
notifications: [],
},
time: {
value: 0,
notifications: [],
},
},
}, require("Storage").readJSON("run.json", 1) || {}); }, require("Storage").readJSON("run.json", 1) || {});
var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!=""); var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!=="");
var exs = ExStats.getStats(statIDs, settings); var exs = ExStats.getStats(statIDs, settings);
// --------------------------- // ---------------------------
// Called to start/stop running // Called to start/stop running
function onStartStop() { function onStartStop() {
var running = !exs.state.active; var running = !exs.state.active;
if (running) { var prepPromises = [];
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();
// start/stop recording // 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 (settings.record && WIDGETS["recorder"]) {
if (running) { if (running) {
isMenuDisplayed = true; isMenuDisplayed = true;
WIDGETS["recorder"].setRecording(true).then(() => { prepPromises.push(
isMenuDisplayed = false; WIDGETS["recorder"].setRecording(true).then(() => {
layout.forgetLazyState(); isMenuDisplayed = false;
layout.render(); layout.forgetLazyState();
}); layout.render();
})
);
} else { } 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 = []; var lc = [];
@ -84,11 +110,27 @@ var layout = new Layout( {
delete lc; delete lc;
layout.render(); layout.render();
function configureNotification(stat) {
stat.on('notify', (e)=>{
settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) {
return promise.then(function () {
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
});
}, Promise.resolve());
});
}
Object.keys(settings.notify).forEach((statType) => {
if (settings.notify[statType].increment > 0) {
configureNotification(exs.stats[statType]);
}
});
// Handle GPS state change for icon // Handle GPS state change for icon
Bangle.on("GPS", function(fix) { Bangle.on("GPS", function(fix) {
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
if (!fix.fix) return; // only process actual fixes if (!fix.fix) return; // only process actual fixes
if (fixCount++ == 0) { if (fixCount++ === 0) {
Bangle.buzz(); // first fix, does not need to respect quiet mode Bangle.buzz(); // first fix, does not need to respect quiet mode
} }
}); });

View File

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

View File

@ -9,14 +9,28 @@
// This way saved values are preserved if a new version adds more settings // This way saved values are preserved if a new version adds more settings
const storage = require('Storage') const storage = require('Storage')
let settings = Object.assign({ let settings = Object.assign({
record : true, record: true,
B1 : "dist", B1: "dist",
B2 : "time", B2: "time",
B3 : "pacea", B3: "pacea",
B4 : "bpm", B4: "bpm",
B5 : "step", B5: "step",
B6 : "caden", B6: "caden",
paceLength : 1000 paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale
notify: {
dist: {
increment: 0,
notifications: [],
},
step: {
increment: 0,
notifications: [],
},
time: {
increment: 0,
notifications: [],
},
},
}, storage.readJSON(SETTINGS_FILE, 1) || {}); }, storage.readJSON(SETTINGS_FILE, 1) || {});
function saveSettings() { function saveSettings() {
storage.write(SETTINGS_FILE, settings) storage.write(SETTINGS_FILE, settings)
@ -24,7 +38,7 @@
function getBoxChooser(boxID) { function getBoxChooser(boxID) {
return { return {
min :0, max: statsIDs.length-1, min: 0, max: statsIDs.length-1,
value: Math.max(statsIDs.indexOf(settings[boxID]),0), value: Math.max(statsIDs.indexOf(settings[boxID]),0),
format: v => statsList[v].name, format: v => statsList[v].name,
onchange: v => { onchange: v => {
@ -34,6 +48,14 @@
} }
} }
function sampleBuzz(buzzPatterns) {
return buzzPatterns.reduce(function (promise, buzzPattern) {
return promise.then(function () {
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
});
}, Promise.resolve());
}
var menu = { var menu = {
'': { 'title': 'Run' }, '': { 'title': 'Run' },
'< Back': back, '< Back': back,
@ -47,8 +69,55 @@
saveSettings(); saveSettings();
} }
}; };
var notificationsMenu = {
'< Back': function() { E.showMenu(menu) },
}
menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)};
ExStats.appendMenuItems(menu, settings, saveSettings); ExStats.appendMenuItems(menu, settings, saveSettings);
Object.assign(menu,{ ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings);
var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"];
var vibTimes = [
[],
[[100, 1]],
[[300, 1]],
[[300, 1], [300, 0], [300, 1]],
[[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]],
[[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]],
];
notificationsMenu[/*LANG*/"Dist Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
onchange: v => {
settings.notify.dist.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
saveSettings();
}
}
notificationsMenu[/*LANG*/"Step Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
onchange: v => {
settings.notify.step.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
saveSettings();
}
}
notificationsMenu[/*LANG*/"Time Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
onchange: v => {
settings.notify.time.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
saveSettings();
}
}
var boxMenu = {
'< Back': function() { E.showMenu(menu) },
}
Object.assign(boxMenu,{
'Box 1': getBoxChooser("B1"), 'Box 1': getBoxChooser("B1"),
'Box 2': getBoxChooser("B2"), 'Box 2': getBoxChooser("B2"),
'Box 3': getBoxChooser("B3"), 'Box 3': getBoxChooser("B3"),
@ -56,5 +125,6 @@
'Box 5': getBoxChooser("B5"), 'Box 5': getBoxChooser("B5"),
'Box 6': getBoxChooser("B6"), 'Box 6': getBoxChooser("B6"),
}); });
menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)};
E.showMenu(menu); E.showMenu(menu);
}) })

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. 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 /* Exercise Stats module
Take a look at README.md for hints on developing with this library. Take a look at README.md for hints on developing with this library.
@ -48,6 +48,15 @@ var menu = { ... };
ExStats.appendMenuItems(menu, settings, saveSettingsFunction); ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
E.showMenu(menu); E.showMenu(menu);
// Additionally, if your app makes use of the stat notifications, you can display additional menu
// settings for configuring when to notify (note the added line in the example below)W
var menu = { ... };
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
ExStats.appendNotifyMenuItems(menu, settings, saveSettingsFunction);
E.showMenu(menu);
*/ */
var state = { var state = {
active : false, // are we working or not? active : false, // are we working or not?
@ -63,15 +72,31 @@ var state = {
// cadence // steps per minute adjusted if <1 minute // cadence // steps per minute adjusted if <1 minute
// BPM // beats per minute // BPM // beats per minute
// BPMage // how many seconds was BPM set? // BPMage // how many seconds was BPM set?
// Notifies: 0 for disabled, otherwise how often to notify in meters, seconds, or steps
notify: {
dist: {
increment: 0,
next: 0,
},
steps: {
increment: 0,
next: 0,
},
time: {
increment: 0,
next: 0,
},
},
}; };
// list of active stats (indexed by ID) // list of active stats (indexed by ID)
var stats = {}; var stats = {};
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km // distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
// https://www.movable-type.co.uk/scripts/latlong.html // https://www.movable-type.co.uk/scripts/latlong.html
// (Equirectangular approximation)
function calcDistance(a,b) { function calcDistance(a,b) {
function radians(a) { return a*Math.PI/180; } function radians(a) { return a*Math.PI/180; }
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat); var y = radians(b.lat-a.lat);
return Math.sqrt(x*x + y*y) * 6371000; return Math.sqrt(x*x + y*y) * 6371000;
} }
@ -114,6 +139,10 @@ Bangle.on("GPS", function(fix) {
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]); if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]); if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]); if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]);
if (state.notify.dist.increment > 0 && state.notify.dist.next <= stats["dist"]) {
stats["dist"].emit("notify",stats["dist"]);
state.notify.dist.next = stats["dist"] + state.notify.dist.increment;
}
}); });
Bangle.on("step", function(steps) { Bangle.on("step", function(steps) {
@ -121,12 +150,16 @@ Bangle.on("step", function(steps) {
if (stats["step"]) stats["step"].emit("changed",stats["step"]); if (stats["step"]) stats["step"].emit("changed",stats["step"]);
state.stepHistory[0] += steps-state.lastStepCount; state.stepHistory[0] += steps-state.lastStepCount;
state.lastStepCount = steps; state.lastStepCount = steps;
if (state.notify.step.increment > 0 && state.notify.step.next <= steps) {
stats["step"].emit("notify",stats["step"]);
state.notify.step.next = steps + state.notify.step.increment;
}
}); });
Bangle.on("HRM", function(h) { Bangle.on("HRM", function(h) {
if (h.confidence>=60) { if (h.confidence>=60) {
state.BPM = h.bpm; state.BPM = h.bpm;
state.BPMage = 0; state.BPMage = 0;
stats["bpm"].emit("changed",stats["bpm"]); if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
} }
}); });
@ -137,20 +170,34 @@ exports.getList = function() {
{name: "Distance", id:"dist"}, {name: "Distance", id:"dist"},
{name: "Steps", id:"step"}, {name: "Steps", id:"step"},
{name: "Heart (BPM)", id:"bpm"}, {name: "Heart (BPM)", id:"bpm"},
{name: "Pace (avr)", id:"pacea"}, {name: "Pace (avg)", id:"pacea"},
{name: "Pace (current)", id:"pacec"}, {name: "Pace (curr)", id:"pacec"},
{name: "Speed", id:"speed"}, {name: "Speed", id:"speed"},
{name: "Cadence", id:"caden"}, {name: "Cadence", id:"caden"},
]; ];
}; };
/** Instatiate the given list of statistic IDs (see comments at top) /** Instantiate the given list of statistic IDs (see comments at top)
options = { options = {
paceLength : meters to measure pace over paceLength : meters to measure pace over
notify: {
dist: {
increment: 0 to not notify on distance milestones, otherwise the number of meters to notify after, repeating
},
step: {
increment: 0 to not notify on step milestones, otherwise the number of steps to notify after, repeating
},
time: {
increment: 0 to not notify on time milestones, otherwise the number of milliseconds to notify after, repeating
}
}
} }
*/ */
exports.getStats = function(statIDs, options) { exports.getStats = function(statIDs, options) {
options = options||{}; options = options||{};
options.paceLength = options.paceLength||1000; options.paceLength = options.paceLength||1000;
options.notify.dist.increment = (options.notify && options.notify.dist && options.notify.dist.increment)||0;
options.notify.step.increment = (options.notify && options.notify.step && options.notify.step.increment)||0;
options.notify.time.increment = (options.notify && options.notify.time && options.notify.time.increment)||0;
var needGPS,needHRM; var needGPS,needHRM;
// ====================== // ======================
if (statIDs.includes("time")) { if (statIDs.includes("time")) {
@ -159,7 +206,7 @@ exports.getStats = function(statIDs, options) {
getValue : function() { return Date.now()-state.startTime; }, getValue : function() { return Date.now()-state.startTime; },
getString : function() { return formatTime(this.getValue()) }, getString : function() { return formatTime(this.getValue()) },
}; };
}; }
if (statIDs.includes("dist")) { if (statIDs.includes("dist")) {
needGPS = true; needGPS = true;
stats["dist"]={ stats["dist"]={
@ -221,7 +268,8 @@ exports.getStats = function(statIDs, options) {
setInterval(function() { // run once a second.... setInterval(function() { // run once a second....
if (!state.active) return; if (!state.active) return;
// called once a second // called once a second
var duration = Date.now() - state.startTime; // in ms var now = Date.now();
var duration = now - state.startTime; // in ms
// set cadence -> steps over last minute // set cadence -> steps over last minute
state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000)); state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000));
if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]); if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]);
@ -235,6 +283,10 @@ exports.getStats = function(statIDs, options) {
state.BPM = 0; state.BPM = 0;
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]); if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
} }
if (state.notify.time.increment > 0 && state.notify.time.next <= now) {
stats["time"].emit("notify",stats["time"]);
state.notify.time.next = now + state.notify.time.increment;
}
}, 1000); }, 1000);
function reset() { function reset() {
state.startTime = Date.now(); state.startTime = Date.now();
@ -247,6 +299,16 @@ exports.getStats = function(statIDs, options) {
state.curSpeed = 0; state.curSpeed = 0;
state.BPM = 0; state.BPM = 0;
state.BPMage = 0; state.BPMage = 0;
state.notify = options.notify;
if (options.notify.dist.increment > 0) {
state.notify.dist.next = state.distance + options.notify.dist.increment;
}
if (options.notify.step.increment > 0) {
state.notify.step.next = state.startSteps + options.notify.step.increment;
}
if (options.notify.time.increment > 0) {
state.notify.time.next = state.startTime + options.notify.time.increment;
}
} }
reset(); reset();
return { return {
@ -262,15 +324,50 @@ exports.getStats = function(statIDs, options) {
}; };
exports.appendMenuItems = function(menu, settings, saveSettings) { exports.appendMenuItems = function(menu, settings, saveSettings) {
var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",]; var paceNames = ["1000m", "1 mile", "1/2 Mthn", "Marathon",];
var paceAmts = [1000,1609,21098,42195]; var paceAmts = [1000, 1609, 21098, 42195];
menu['Pace'] = { menu['Pace'] = {
min :0, max: paceNames.length-1, min: 0, max: paceNames.length - 1,
value: Math.max(paceAmts.indexOf(settings.paceLength),0), value: Math.max(paceAmts.indexOf(settings.paceLength), 0),
format: v => paceNames[v], format: v => paceNames[v],
onchange: v => { onchange: v => {
settings.paceLength = paceAmts[v]; settings.paceLength = paceAmts[v];
saveSettings(); saveSettings();
}, },
}; };
}
exports.appendNotifyMenuItems = function(menu, settings, saveSettings) {
var distNames = ['Off', "1000m","1 mile","1/2 Mthn", "Marathon",];
var distAmts = [0, 1000,1609,21098,42195];
menu['Ntfy Dist'] = {
min: 0, max: distNames.length-1,
value: Math.max(distAmts.indexOf(settings.notify.dist.increment),0),
format: v => distNames[v],
onchange: v => {
settings.notify.dist.increment = distAmts[v];
saveSettings();
},
};
var stepNames = ['Off', '100', '500', '1000', '5000', '10000'];
var stepAmts = [0, 100, 500, 1000, 5000, 10000];
menu['Ntfy Steps'] = {
min: 0, max: stepNames.length-1,
value: Math.max(stepAmts.indexOf(settings.notify.step.increment),0),
format: v => stepNames[v],
onchange: v => {
settings.notify.step.increment = stepAmts[v];
saveSettings();
},
};
var timeNames = ['Off', '30s', '1min', '2min', '5min', '10min', '30min', '1hr'];
var timeAmts = [0, 30000, 60000, 120000, 300000, 600000, 1800000, 3600000];
menu['Ntfy Time'] = {
min: 0, max: timeNames.length-1,
value: Math.max(timeAmts.indexOf(settings.notify.time.increment),0),
format: v => timeNames[v],
onchange: v => {
settings.notify.time.increment = timeAmts[v];
saveSettings();
},
};
}; };