1
0
Fork 0
David Peer 2022-03-08 19:12:37 +01:00
commit 3fe7f59b58
38 changed files with 1152 additions and 146 deletions

View File

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

1
apps/clockcal/ChangeLog Normal file
View File

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

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

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

View File

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

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

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

BIN
apps/clockcal/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

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

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

View File

@ -16,12 +16,12 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jshint/2.11.0/jshint.min.js"></script>
<p>Type your javascript code here</p>
<p><textarea id="custom-js"></textarea></p>
<p>Then click <button id="upload" class="btn btn-primary">Upload</button></p>
<p>Then click <button id="upload" class="btn btn-primary">Upload</button>&nbsp;<span id="btninfo" style="color:orange"></span></p>
<script>
const item = "custom.boot.js";
const id = "custom-js";
const sample = "//Bangle.setOptions({wakeOnBTN2:false});";
var localeModule = null;
var customBootCode = null;
var editor = {};
if (localStorage.getItem(item) === null) {
@ -48,13 +48,28 @@
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);
if (!hasWarnings()) {
customBootCode = editor.getValue();
localStorage.setItem(item, customBootCode);
sendCustomizedApp({
storage: [{ name: item, content: localeModule }]
storage: [{ name: item, content: customBootCode }]
});
}
});

View File

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

View File

@ -6,3 +6,4 @@
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
0.08: Support new Bangle.js 2 menu

View File

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

View File

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

View File

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

View File

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

149
apps/ratchet_launch/app.js Normal file
View File

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

BIN
apps/ratchet_launch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -6,3 +6,4 @@
0.05: exstats updated so update 'distance' label is updated, option for 'speed'
0.06: Add option to record a run using the recorder app automatically
0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
0.08: Added support for notifications from exstats. Support all stats from exstats

View File

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

View File

@ -1,5 +1,5 @@
var ExStats = require("exstats");
var B2 = process.env.HWVERSION==2;
var B2 = process.env.HWVERSION===2;
var Layout = require("Layout");
var locale = require("locale");
var fontHeading = "6x8:2";
@ -21,15 +21,53 @@ let settings = Object.assign({
B4: "bpm",
B5: "step",
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) || {});
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;
var prepPromises = [];
// start/stop recording
// Do this first in case recorder needs to prompt for
// an overwrite before we start tracking exstats
if (settings.record && WIDGETS["recorder"]) {
if (running) {
isMenuDisplayed = true;
prepPromises.push(
WIDGETS["recorder"].setRecording(true).then(() => {
isMenuDisplayed = false;
layout.forgetLazyState();
layout.render();
})
);
} else {
prepPromises.push(
WIDGETS["recorder"].setRecording(false)
);
}
}
Promise.all(prepPromises)
.then(() => {
if (running) {
exs.start();
} else {
@ -41,19 +79,7 @@ function onStartStop() {
// if stopping running, don't clear state
// so we can at least refer to what we've done
layout.render();
// start/stop recording
if (settings.record && WIDGETS["recorder"]) {
if (running) {
isMenuDisplayed = true;
WIDGETS["recorder"].setRecording(true).then(() => {
isMenuDisplayed = false;
layout.forgetLazyState();
layout.render();
});
} else {
WIDGETS["recorder"].setRecording(false);
}
}
}
var lc = [];
@ -84,11 +110,27 @@ var layout = new Layout( {
delete lc;
layout.render();
function configureNotification(stat) {
stat.on('notify', (e)=>{
settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) {
return promise.then(function () {
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
});
}, Promise.resolve());
});
}
Object.keys(settings.notify).forEach((statType) => {
if (settings.notify[statType].increment > 0) {
configureNotification(exs.stats[statType]);
}
});
// Handle GPS state change for icon
Bangle.on("GPS", function(fix) {
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
if (!fix.fix) return; // only process actual fixes
if (fixCount++ == 0) {
if (fixCount++ === 0) {
Bangle.buzz(); // first fix, does not need to respect quiet mode
}
});

View File

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

View File

@ -16,7 +16,21 @@
B4: "bpm",
B5: "step",
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) || {});
function saveSettings() {
storage.write(SETTINGS_FILE, settings)
@ -34,6 +48,14 @@
}
}
function sampleBuzz(buzzPatterns) {
return buzzPatterns.reduce(function (promise, buzzPattern) {
return promise.then(function () {
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
});
}, Promise.resolve());
}
var menu = {
'': { 'title': 'Run' },
'< Back': back,
@ -47,8 +69,55 @@
saveSettings();
}
};
var notificationsMenu = {
'< Back': function() { E.showMenu(menu) },
}
menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)};
ExStats.appendMenuItems(menu, settings, saveSettings);
Object.assign(menu,{
ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings);
var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"];
var vibTimes = [
[],
[[100, 1]],
[[300, 1]],
[[300, 1], [300, 0], [300, 1]],
[[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]],
[[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]],
];
notificationsMenu[/*LANG*/"Dist Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
onchange: v => {
settings.notify.dist.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
saveSettings();
}
}
notificationsMenu[/*LANG*/"Step Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
onchange: v => {
settings.notify.step.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
saveSettings();
}
}
notificationsMenu[/*LANG*/"Time Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
onchange: v => {
settings.notify.time.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
saveSettings();
}
}
var boxMenu = {
'< Back': function() { E.showMenu(menu) },
}
Object.assign(boxMenu,{
'Box 1': getBoxChooser("B1"),
'Box 2': getBoxChooser("B2"),
'Box 3': getBoxChooser("B3"),
@ -56,5 +125,6 @@
'Box 5': getBoxChooser("B5"),
'Box 6': getBoxChooser("B6"),
});
menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)};
E.showMenu(menu);
})

View File

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

View File

@ -0,0 +1,26 @@
# Barometer alarm widget
Get a notification when the pressure reaches defined thresholds.
![Screenshot](screenshot.png)
## Settings
* Interval: check interval of sensor data in minutes. 0 to disable automatic check.
* Low alarm: Toggle low alarm
* Low threshold: Warn when pressure drops below this value
* High alarm: Toggle high alarm
* High threshold: Warn when pressure exceeds above this value
* Drop alarm: Warn when pressure drops more than this value in the recent 3 hours (having at least 30 min of data)
0 to disable this alarm.
* Raise alarm: Warn when pressure raises more than this value in the recent 3 hours (having at least 30 min of data)
0 to disable this alarm.
* Show widget: Enable/disable widget visibility
* Buzz on alarm: Enable/disable buzzer on alarm
## Widget
The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours.
## Creator
Marco ([myxor](https://github.com/myxor))

View File

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

View File

@ -0,0 +1,19 @@
{
"id": "widbaroalarm",
"name": "Barometer alarm widget",
"shortName": "Barometer alarm",
"version": "0.01",
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
"icon": "widget.png",
"type": "widget",
"tags": "tool,barometer",
"supports": ["BANGLEJS2"],
"dependencies": {"notify":"type"},
"readme": "README.md",
"storage": [
{"name":"widbaroalarm.wid.js","url":"widget.js"},
{"name":"widbaroalarm.settings.js","url":"settings.js"},
{"name":"widbaroalarm.default.json","url":"default.json"}
],
"data": [{"name":"widbaroalarm.json"}, {"name":"widbaroalarm.log"}]
}

View File

@ -0,0 +1,95 @@
(function(back) {
const SETTINGS_FILE = "widbaroalarm.json";
const storage = require('Storage');
let settings = Object.assign(
storage.readJSON("widbaroalarm.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {}
);
function save(key, value) {
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
function showMainMenu() {
let menu ={
'': { 'title': 'Barometer alarm widget' },
/*LANG*/'< Back': back,
"Interval": {
value: settings.interval,
min: 0,
max: 120,
step: 1,
format: x => {
return x != 0 ? x + ' min' : 'off';
},
onchange: x => save("interval", x)
},
"Low alarm": {
value: settings.lowalarm,
format: x => {
return x ? 'Yes' : 'No';
},
onchange: x => save("lowalarm", x),
},
"Low threshold": {
value: settings.min,
min: 600,
max: 1000,
step: 10,
onchange: x => save("min", x),
},
"High alarm": {
value: settings.highalarm,
format: x => {
return x ? 'Yes' : 'No';
},
onchange: x => save("highalarm", x),
},
"High threshold": {
value: settings.max,
min: 1000,
max: 1100,
step: 10,
onchange: x => save("max", x),
},
"Drop alarm": {
value: settings.drop3halarm,
min: 0,
max: 10,
step: 1,
format: x => {
return x != 0 ? x + ' hPa/3h' : 'off';
},
onchange: x => save("drop3halarm", x)
},
"Raise alarm": {
value: settings.raise3halarm,
min: 0,
max: 10,
step: 1,
format: x => {
return x != 0 ? x + ' hPa/3h' : 'off';
},
onchange: x => save("raise3halarm", x)
},
"Show widget": {
value: settings.show,
format: x => {
return x ? 'Yes' : 'No';
},
onchange: x => save('show', x)
},
"Buzz on alarm": {
value: settings.buzz,
format: x => {
return x ? 'Yes' : 'No';
},
onchange: x => save('buzz', x)
},
};
E.showMenu(menu);
}
showMainMenu();
});

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

@ -0,0 +1,185 @@
(function() {
let medianPressure;
let threeHourAvrPressure;
let currentPressures = [];
const LOG_FILE = "widbaroalarm.log.json";
const SETTINGS_FILE = "widbaroalarm.json";
const storage = require('Storage');
let settings = Object.assign(
storage.readJSON("widbaroalarm.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {}
);
function setting(key) {
return settings[key];
}
const interval = setting("interval");
let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours
function showAlarm(body, title) {
if (body == undefined) return;
require("notify").show({
title: title || "Pressure",
body: body,
icon: require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA"))
});
if (setting("buzz") &&
!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz();
}
}
let alreadyWarned = false;
function checkForAlarms(pressure) {
if (pressure == undefined || pressure <= 0) return;
const ts = Math.round(Date.now() / 1000); // seconds
const d = {
"ts": ts,
"p": pressure
};
// delete entries older than 3h
for (let i = 0; i < history3.length; i++) {
if (history3[i]["ts"] < ts - (3 * 60 * 60)) {
history3.shift();
}
}
// delete oldest entries until we have max 50
while (history3.length > 50) {
history3.shift();
}
history3.push(d);
// write data to storage
storage.writeJSON(LOG_FILE, history3);
if (setting("lowalarm") && pressure <= setting("min")) {
showAlarm("Pressure low: " + Math.round(pressure) + " hPa");
alreadyWarned = true;
}
if (setting("highalarm") && pressure >= setting("max")) {
showAlarm("Pressure high: " + Math.round(pressure) + " hPa");
alreadyWarned = true;
}
if (!alreadyWarned) {
// 3h change detection
const drop3halarm = setting("drop3halarm");
const raise3halarm = setting("raise3halarm");
if (drop3halarm > 0 || raise3halarm > 0) {
// we need at least 30min of data for reliable detection
if (history3[0]["ts"] > ts - (30 * 60)) {
return;
}
// Get oldest entry:
const oldestPressure = history3[0]["p"];
if (oldestPressure != undefined && oldestPressure > 0) {
const diff = oldestPressure - pressure;
// drop alarm
if (drop3halarm > 0 && oldestPressure > pressure) {
if (Math.abs(diff) > drop3halarm) {
showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop");
}
}
// raise alarm
if (raise3halarm > 0 && oldestPressure < pressure) {
if (Math.abs(diff) > raise3halarm) {
showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise");
}
}
}
}
}
// calculate 3h average for widget
let sum = 0;
for (let i = 0; i < history3.length; i++) {
sum += history3[i]["p"];
}
threeHourAvrPressure = sum / history3.length;
}
function baroHandler(data) {
if (data) {
const pressure = Math.round(data.pressure);
if (pressure == undefined || pressure <= 0) return;
currentPressures.push(pressure);
}
}
/*
turn on barometer power
take 5 measurements
sort the results
take the middle one (median)
turn off barometer power
*/
function check() {
Bangle.setBarometerPower(true, "widbaroalarm");
setTimeout(function() {
currentPressures = [];
Bangle.getPressure().then(baroHandler);
Bangle.getPressure().then(baroHandler);
Bangle.getPressure().then(baroHandler);
Bangle.getPressure().then(baroHandler);
Bangle.getPressure().then(baroHandler);
setTimeout(function() {
Bangle.setBarometerPower(false, "widbaroalarm");
currentPressures.sort();
// take median value
medianPressure = currentPressures[3];
checkForAlarms(medianPressure);
}, 1000);
}, 500);
}
function reload() {
check();
}
function draw() {
g.reset();
if (setting("show") && medianPressure != undefined) {
g.setFont("6x8", 1).setFontAlign(1, 0);
g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6);
if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) {
g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10);
}
}
}
if (global.WIDGETS != undefined && typeof WIDGETS === "object") {
WIDGETS["baroalarm"] = {
width: setting("show") ? 24 : 0,
reload: reload,
area: "tr",
draw: draw
};
}
// Let's delay the first check a bit
setTimeout(function() {
check();
if (interval > 0) {
setInterval(check, interval * 60000);
}
}, 5000);
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */
/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */
/*
Take a look at README.md for hints on developing with this library.

View File

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