1
0
Fork 0

Merge remote-tracking branch 'upstream/master'

master
Marko.Kl.Berkenbusch@gmail.com 2022-01-28 22:24:56 -05:00
commit ebcbf87828
97 changed files with 3666 additions and 538 deletions

View File

@ -514,7 +514,6 @@ The [`testing`](testing) folder contains snippets of code that might be useful f
* `testing/colors.js` - 16 bit colors as name value pairs * `testing/colors.js` - 16 bit colors as name value pairs
* `testing/gpstrack.js` - code to store a GPS track in Bangle.js storage and output it back to the console * `testing/gpstrack.js` - code to store a GPS track in Bangle.js storage and output it back to the console
* `testing/map` - code for splitting an image into map tiles and then displaying them
## Credits ## Credits

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Faster maze generation 0.02: Faster maze generation
0.03: Avoid clearing bottom widgets

View File

@ -11,13 +11,10 @@ function Maze(n) {
this.margin = Math.floor((g.getHeight()-this.total_length)/2); this.margin = Math.floor((g.getHeight()-this.total_length)/2);
this.ball_x = 0; this.ball_x = 0;
this.ball_y = 0; this.ball_y = 0;
this.clearScreen = function() { // This voodoo is needed because otherwise
g.clearRect( // bottom line widgets (like digital clock)
0, this.margin, // disappear during maze generation
g.getWidth(), this.margin+this.total_length Bangle.drawWidgets();
);
};
this.clearScreen();
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
for (let i=0; i<=n; i++) { for (let i=0; i<=n; i++) {
g.drawRect( g.drawRect(
@ -66,7 +63,7 @@ function Maze(n) {
if (Math.random()<0.5 && candidates_down.length || !candidates_right.length) { if (Math.random()<0.5 && candidates_down.length || !candidates_right.length) {
trying_down = true; trying_down = true;
} }
let candidates = trying_down ? candidates_down : candidates_right; let candidates = trying_down ? candidates_down : candidates_right,
candidate_index = Math.floor(Math.random()*candidates.length), candidate_index = Math.floor(Math.random()*candidates.length),
cell = candidates.splice(candidate_index, 1)[0], cell = candidates.splice(candidate_index, 1)[0],
r = Math.floor(cell/n), r = Math.floor(cell/n),
@ -105,11 +102,6 @@ function Maze(n) {
} }
} }
} }
this.clearScreen = function() {
g.clearRect(
0, MARGIN, g.getWidth(), g.getHeight()-MARGIN-1
);
};
this.clearCell = function(r, c) { this.clearCell = function(r, c) {
if (!r && !c) { if (!r && !c) {
g.setColor("#ffff00"); g.setColor("#ffff00");
@ -263,7 +255,7 @@ let mazeMenu = {
"< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock "< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock
}; };
g.clear(true); g.reset();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
Bangle.setLocked(false); Bangle.setLocked(false);
@ -289,7 +281,7 @@ let maze_interval = setInterval(
duration = Date.now()-start_time; duration = Date.now()-start_time;
g.setFontAlign(0,0).setColor(g.theme.fg); g.setFontAlign(0,0).setColor(g.theme.fg);
g.setFont("Vector",18); g.setFont("Vector",18);
g.drawString(`Solved ${maze.n}X${maze.n} in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true); g.drawString(`Solved ${maze.n}X${maze.n} in\n ${timeToText(duration)} \nBtn1 to play again`, g.getWidth()/2, g.getHeight()/2, true);
} }
} }
}, 25); }, 25);

View File

@ -1,7 +1,7 @@
{ "id": "acmaze", { "id": "acmaze",
"name": "AccelaMaze", "name": "AccelaMaze",
"shortName":"AccelaMaze", "shortName":"AccelaMaze",
"version":"0.02", "version":"0.03",
"description": "Tilt the watch to roll a ball through a maze.", "description": "Tilt the watch to roll a ball through a maze.",
"icon": "app.png", "icon": "app.png",
"tags": "game", "tags": "game",

View File

@ -5,3 +5,5 @@
0.03: Prevent readings from internal sensor mixing into BT values 0.03: Prevent readings from internal sensor mixing into BT values
Mark events with src property Mark events with src property
Show actual source of event in app Show actual source of event in app
0.04: Automatically reconnect BT sensor
App buzzes if no BTHRM events for more than 3 seconds

View File

@ -1,13 +1,28 @@
(function() { (function() {
var log = function() {};//print //var sf = require("Storage").open("bthrm.log","a");
var log = function(text, param){
/*var logline = Date.now().toFixed(3) + " - " + text;
if (param){
logline += " " + JSON.stringify(param);
}
sf.write(logline + "\n");
print(logline);*/
}
log("Start");
var blockInit = false;
var gatt; var gatt;
var status; var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var origIsHRMOn = Bangle.isHRMOn; var origIsHRMOn = Bangle.isHRMOn;
Bangle.isBTHRMOn = function(){ Bangle.isBTHRMOn = function(){
return (status=="searching" || status=="connecting") || (gatt!==undefined); return (gatt!==undefined && gatt.connected);
} };
Bangle.isHRMOn = function() { Bangle.isHRMOn = function() {
var settings = require('Storage').readJSON("bthrm.json", true) || {}; var settings = require('Storage').readJSON("bthrm.json", true) || {};
@ -18,16 +33,135 @@
return Bangle.isBTHRMOn(); return Bangle.isBTHRMOn();
} }
return origIsHRMOn() || Bangle.isBTHRMOn(); return origIsHRMOn() || Bangle.isBTHRMOn();
};
var serviceFilters = [{
services: [
"180d"
]
}];
function retry(){
log("Retry with time " + retryTime);
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
} }
var clampedTime = retryTime < 200 ? 200 : initialRetryTime;
currentRetryTimeout = setTimeout(() => {
log("Set timeout for retry as " + clampedTime);
initBt();
}, clampedTime);
retryTime = Math.pow(retryTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
}
function onDisconnect(reason) {
log("Disconnect: " + reason);
log("Gatt: ", gatt);
retry();
}
function onCharacteristic(event) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit(settings.replace ? "HRM" : "BTHRM", {
bpm: bpm,
confidence: bpm == 0 ? 0 : 100,
src: settings.replace ? "bthrm" : undefined
});
}
var reUseCounter=0;
function initBt() {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
return;
}
blockInit = true;
var connectionPromise;
if (reUseCounter > 3){
log("Reuse counter to high")
if (gatt.connected == true){
try {
log("Force disconnect with gatt: ", gatt);
gatt.disconnect();
} catch(e) {
log("Error during force disconnect", e);
}
}
gatt=undefined;
reUseCounter = 0;
}
if (!gatt){
var requestPromise = NRF.requestDevice({ filters: serviceFilters });
connectionPromise = requestPromise.then(function(device) {
gatt = device.gatt;
log("Gatt after request:", gatt);
gatt.device.on('gattserverdisconnected', onDisconnect);
});
} else {
reUseCounter++;
log("Reusing gatt:", gatt);
connectionPromise = gatt.connect();
}
var servicePromise = connectionPromise.then(function() {
return gatt.getPrimaryService(0x180d);
});
var characteristicPromise = servicePromise.then(function(service) {
log("Got service:", service);
return service.getCharacteristic(0x2A37);
});
var notificationPromise = characteristicPromise.then(function(c) {
log("Got characteristic:", c);
c.on('characteristicvaluechanged', onCharacteristic);
return c.startNotifications();
});
notificationPromise.then(()=>{
log("Wait for notifications");
retryTime = initialRetryTime;
blockInit=false;
});
notificationPromise.catch((e) => {
log("Error:", e);
blockInit = false;
retry();
});
}
Bangle.setBTHRMPower = function(isOn, app) { Bangle.setBTHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {}; var settings = require('Storage').readJSON("bthrm.json", true) || {};
// Do app power handling // Do app power handling
if (!app) app="?"; if (!app) app="?";
log("setBTHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={}; if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
@ -35,63 +169,19 @@
isOn = Bangle._PWR.BTHRM.length; isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on // so now we know if we're really on
if (isOn) { if (isOn) {
log("setBTHRMPower on", app);
if (!Bangle.isBTHRMOn()) { if (!Bangle.isBTHRMOn()) {
log("BTHRM not already on"); initBt();
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
status = "connecting";
device.on('gattserverdisconnected', function(reason) {
gatt = undefined;
});
return device.gatt.connect();
}).then(function(g) {
log("Connected");
gatt = g;
return gatt.getPrimaryService(0x180D);
}).then(function(service) {
return service.getCharacteristic(0x2A37);
}).then(function(characteristic) {
log("Got characteristic");
characteristic.on('characteristicvaluechanged', function(event) {
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags&1) ? (dv.getUint16(1)/100/* ? */) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit(settings.replace?"HRM":"BTHRM", {
bpm:bpm,
confidence:100,
src:settings.replace?"bthrm":undefined
});
});
return characteristic.startNotifications();
}).then(function() {
log("Ready");
status = "ok";
}).catch(function(err) {
log("Error",err);
gatt = undefined;
status = "error";
});
} }
} else { // not on } else { // not on
log("setBTHRMPower off", app); log("Power off for " + app);
if (gatt) { if (gatt) {
log("BTHRM connected - disconnecting"); try {
status = undefined; log("Disconnect with gatt: ", gatt);
try {gatt.disconnect();}catch(e) { gatt.disconnect();
log("BTHRM disconnect error", e); } catch(e) {
log("Error during disconnect", e);
} }
blockInit = false;
gatt = undefined; gatt = undefined;
} }
} }
@ -100,24 +190,29 @@
var origSetHRMPower = Bangle.setHRMPower; var origSetHRMPower = Bangle.setHRMPower;
Bangle.setHRMPower = function(isOn, app) { Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ":" + (isOn?"on":"off"));
var settings = require('Storage').readJSON("bthrm.json", true) || {}; var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled || !isOn){ if (settings.enabled || !isOn){
log("Enable BTHRM power");
Bangle.setBTHRMPower(isOn, app); Bangle.setBTHRMPower(isOn, app);
} }
if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){ if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){
log("Enable HRM power");
origSetHRMPower(isOn, app); origSetHRMPower(isOn, app);
} }
} }
var settings = require('Storage').readJSON("bthrm.json", true) || {}; var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled && settings.replace){ if (settings.enabled && settings.replace){
log("Replace HRM event");
if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){ if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){ for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i]; var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
origSetHRMPower(0, app); origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app); Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break; if (Bangle._PWR.HRM===undefined) break;
} }
} }
} }
})(); })();

View File

@ -10,7 +10,9 @@ function draw(y, event, type, counter) {
g.reset(); g.reset();
g.setFontAlign(0,0); g.setFontAlign(0,0);
g.clearRect(0,y,g.getWidth(),y+75); g.clearRect(0,y,g.getWidth(),y+75);
if (type == null || event == null || counter == 0) return; if (type == null || event == null || counter == 0){
return;
}
var str = event.bpm + ""; var str = event.bpm + "";
g.setFontVector(40).drawString(str,px,y+20); g.setFontVector(40).drawString(str,px,y+20);
str = "Confidence: " + event.confidence; str = "Confidence: " + event.confidence;
@ -21,21 +23,27 @@ function draw(y, event, type, counter) {
} }
function onBtHrm(e) { function onBtHrm(e) {
print("Event for BT " + JSON.stringify(e)); //print("Event for BT " + JSON.stringify(e));
counterBt += 5; if (e.bpm == 0){
Bangle.buzz(100,0.2);
}
if (counterBt == 0){
Bangle.buzz(200,0.5);
}
counterBt += 3;
eventBt = e; eventBt = e;
} }
function onHrm(e) { function onHrm(e) {
print("Event for Int " + JSON.stringify(e)); //print("Event for Int " + JSON.stringify(e));
counterInt += 5; counterInt += 3;
eventInt = e; eventInt = e;
} }
Bangle.on('BTHRM', onBtHrm); Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm); Bangle.on('HRM', onHrm);
Bangle.setHRMPower(1,'bthrm') Bangle.setHRMPower(1,'bthrm');
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
@ -47,13 +55,13 @@ g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
function drawInt(){ function drawInt(){
counterInt--; counterInt--;
if (counterInt < 0) counterInt = 0; if (counterInt < 0) counterInt = 0;
if (counterInt > 5) counterInt = 5; if (counterInt > 3) counterInt = 3;
draw(24, eventInt, "HRM", counterInt); draw(24, eventInt, "HRM", counterInt);
} }
function drawBt(){ function drawBt(){
counterBt--; counterBt--;
if (counterBt < 0) counterBt = 0; if (counterBt < 0) counterBt = 0;
if (counterBt > 5) counterBt = 5; if (counterBt > 3) counterBt = 3;
draw(100, eventBt, "BTHRM", counterBt); draw(100, eventBt, "BTHRM", counterBt);
} }

View File

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

View File

@ -12,3 +12,4 @@
Support to show time and progress until next sunrise or sunset Support to show time and progress until next sunrise or sunset
Load daily steps from Bangle health if available Load daily steps from Bangle health if available
0.07: Allow configuration of minimal heart rate confidence 0.07: Allow configuration of minimal heart rate confidence
0.08: Allow configuration of up to 4 circles in a row

View File

@ -1,6 +1,6 @@
# Circles clock # Circles clock
A clock with circles for different data at the bottom in a probably familiar style A clock with three or four circles for different data at the bottom in a probably familiar style
By default the time, date and day of week is shown. By default the time, date and day of week is shown.
@ -18,6 +18,8 @@ It can show the following information (this can be configured):
## Screenshots ## Screenshots
![Screenshot dark theme](screenshot-dark.png) ![Screenshot dark theme](screenshot-dark.png)
![Screenshot light theme](screenshot-light.png) ![Screenshot light theme](screenshot-light.png)
![Screenshot dark theme with four circles](screenshot-dark-4.png)
![Screenshot light theme with four circles](screenshot-light-4.png)
## Creator ## Creator
Marco ([myxor](https://github.com/myxor)) Marco ([myxor](https://github.com/myxor))

View File

@ -23,10 +23,7 @@ const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED
const sunSetDown = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wLDg1ggfACoo")); const sunSetDown = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wLDg1ggfACoo"));
const sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY")); const sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY"));
let settings; let settings = storage.readJSON("circlesclock.json", 1) || {
function loadSettings() {
settings = storage.readJSON("circlesclock.json", 1) || {
'minHR': 40, 'minHR': 40,
'maxHR': 200, 'maxHR': 200,
'confidence': 0, 'confidence': 0,
@ -36,18 +33,17 @@ function loadSettings() {
'batteryWarn': 30, 'batteryWarn': 30,
'showWidgets': false, 'showWidgets': false,
'weatherCircleData': 'humidity', 'weatherCircleData': 'humidity',
'circleCount': 3,
'circle1': 'hr', 'circle1': 'hr',
'circle2': 'steps', 'circle2': 'steps',
'circle3': 'battery' 'circle3': 'battery',
}; 'circle4': 'weather'
// Load step goal from pedometer widget as fallback };
if (settings.stepGoal == undefined) { // Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
const d = require('Storage').readJSON("wpedom.json", 1) || {}; const d = require('Storage').readJSON("wpedom.json", 1) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
} }
loadSettings();
/* /*
* Read location from myLocation app * Read location from myLocation app
@ -58,6 +54,7 @@ function getLocation() {
let location = getLocation(); let location = getLocation();
const showWidgets = settings.showWidgets || false; const showWidgets = settings.showWidgets || false;
const circleCount = settings.circleCount || 3;
let hrtValue; let hrtValue;
let now = Math.round(new Date().getTime() / 1000); let now = Math.round(new Date().getTime() / 1000);
@ -78,11 +75,33 @@ const hOffset = 30 - widgetOffset;
const h1 = Math.round(1 * h / 5 - hOffset); const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset); const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions
const radiusOuter = 25; /*
const radiusInner = 20; * circle x positions
const circleFont = "Vector:15"; * depending on circleCount
const circleFontBig = "Vector:16"; *
* | 1 2 3 4 5 6 |
* | (1) (2) (3) |
* => circles start at 1,3,5 / 6
*
* | 1 2 3 4 5 6 7 8 |
* | (1) (2) (3) (4) |
* => circles start at 1,3,5,7 / 8
*/
const parts = circleCount * 2;
const circlePosX = [
Math.round(1 * w / parts), // circle1
Math.round(3 * w / parts), // circle2
Math.round(5 * w / parts), // circle3
Math.round(7 * w / parts), // circle4
];
const radiusOuter = circleCount == 3 ? 25 : 20;
const radiusInner = circleCount == 3 ? 20 : 15;
const circleFont = circleCount == 3 ? "Vector:15" : "Vector:12";
const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:13";
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
function draw() { function draw() {
g.clear(true); g.clear(true);
@ -122,10 +141,9 @@ function draw() {
drawCircle(1); drawCircle(1);
drawCircle(2); drawCircle(2);
drawCircle(3); drawCircle(3);
if (circleCount >= 4) drawCircle(4);
} }
const defaultCircleTypes = ["steps", "hr", "battery"];
function drawCircle(index) { function drawCircle(index) {
let type = settings['circle' + index]; let type = settings['circle' + index];
if (!type) type = defaultCircleTypes[index - 1]; if (!type) type = defaultCircleTypes[index - 1];
@ -147,6 +165,7 @@ function drawCircle(index) {
drawWeather(w); drawWeather(w);
break; break;
case "sunprogress": case "sunprogress":
case "sunProgress":
drawSunProgress(w); drawSunProgress(w);
break; break;
case "empty": case "empty":
@ -169,7 +188,7 @@ function getCirclePosition(type) {
if (circlePositionsCache[type] >= 0) { if (circlePositionsCache[type] >= 0) {
return circlePosX[circlePositionsCache[type]]; return circlePosX[circlePositionsCache[type]];
} }
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= circleCount; i++) {
const setting = settings['circle' + i]; const setting = settings['circle' + i];
if (setting == type) { if (setting == type) {
circlePositionsCache[type] = i - 1; circlePositionsCache[type] = i - 1;
@ -319,6 +338,8 @@ function drawWeather(w) {
if (code > 0) { if (code > 0) {
const icon = getWeatherIconByCode(code); const icon = getWeatherIconByCode(code);
if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10); if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10);
} else {
g.drawString("?", w, h3 + radiusOuter);
} }
} }

View File

@ -1,10 +1,10 @@
{ "id": "circlesclock", { "id": "circlesclock",
"name": "Circles clock", "name": "Circles clock",
"shortName":"Circles clock", "shortName":"Circles clock",
"version":"0.07", "version":"0.08",
"description": "A clock with circles for different data at the bottom in a probably familiar style", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}], "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -96,23 +96,36 @@
format: v => weatherData[v], format: v => weatherData[v],
onchange: x => save('weatherCircleData', weatherData[x]), onchange: x => save('weatherCircleData', weatherData[x]),
}, },
'left': { 'circle count': {
value: "circleCount" in settings ? settings.circleCount : 3,
min: 3,
max : 4,
step: 1,
onchange: x => save('circleCount', x),
},
'circle1': {
value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0, value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0,
min: 0, max: 6, min: 0, max: 6,
format: v => namesCircleTypes[v], format: v => namesCircleTypes[v],
onchange: x => save('circle1', valuesCircleTypes[x]), onchange: x => save('circle1', valuesCircleTypes[x]),
}, },
'middle': { 'circle2': {
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2, value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
min: 0, max: 6, min: 0, max: 6,
format: v => namesCircleTypes[v], format: v => namesCircleTypes[v],
onchange: x => save('circle2', valuesCircleTypes[x]), onchange: x => save('circle2', valuesCircleTypes[x]),
}, },
'right': { 'circle3': {
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3, value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
min: 0, max: 6, min: 0, max: 6,
format: v => namesCircleTypes[v], format: v => namesCircleTypes[v],
onchange: x => save('circle3', valuesCircleTypes[x]), onchange: x => save('circle3', valuesCircleTypes[x]),
},
'circle4': {
value: settings.circle4 ? valuesCircleTypes.indexOf(settings.circle4) : 4,
min: 0, max: 6,
format: v => namesCircleTypes[v],
onchange: x => save('circle4', valuesCircleTypes[x]),
} }
}); });
}); });

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Tweaked proximity identification settings

View File

@ -5,7 +5,7 @@
## Usage ## Usage
Real-time interactions will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the [DirAct open standard](https://reelyactive.github.io/diract/). Real-time interactions will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the [DirAct open standard](https://reelyactive.github.io/diract/). See our [Bangle.js Development Guide](https://reelyactive.github.io/diy/banglejs-dev/) for details.
## Features ## Features

View File

@ -1,5 +1,5 @@
/** /**
* Copyright reelyActive 2017-2021 * Copyright reelyActive 2017-2022
* We believe in an open Internet of Things * We believe in an open Internet of Things
* *
* DirAct is jointly developed by reelyActive and Code Blue Consulting * DirAct is jointly developed by reelyActive and Code Blue Consulting
@ -11,14 +11,14 @@ const NAMESPACE_FILTER_ID = [ 0xc0, 0xde, 0xb1, 0x0e, 0x1d,
0xd1, 0xe0, 0x1b, 0xed, 0x0c ]; 0xd1, 0xe0, 0x1b, 0xed, 0x0c ];
const EXCITER_INSTANCE_IDS = new Uint32Array([ 0xe8c17e45 ]); const EXCITER_INSTANCE_IDS = new Uint32Array([ 0xe8c17e45 ]);
const RESETTER_INSTANCE_IDS = new Uint32Array([ 0x4e5e77e4 ]); const RESETTER_INSTANCE_IDS = new Uint32Array([ 0x4e5e77e4 ]);
const PROXIMITY_RSSI_THRESHOLD = -65; const PROXIMITY_RSSI_THRESHOLD = -85;
const PROXIMITY_LED_RSSI_THRESHOLD = -65; const PROXIMITY_LED_RSSI_THRESHOLD = -85;
const PROXIMITY_TABLE_SIZE = 8; const PROXIMITY_TABLE_SIZE = 8;
const DIGEST_TABLE_SIZE = 32; const DIGEST_TABLE_SIZE = 32;
const OBSERVE_PERIOD_MILLISECONDS = 400; const OBSERVE_PERIOD_MILLISECONDS = 400;
const BROADCAST_PERIOD_MILLISECONDS = 3600; const BROADCAST_PERIOD_MILLISECONDS = 1600;
const BROADCAST_DIGEST_PAGE_MILLISECONDS = 400; const BROADCAST_DIGEST_PAGE_MILLISECONDS = 400;
const PROXIMITY_PACKET_INTERVAL_MILLISECONDS = 400; const PROXIMITY_PACKET_INTERVAL_MILLISECONDS = 200;
const DIGEST_PACKET_INTERVAL_MILLISECONDS = 100; const DIGEST_PACKET_INTERVAL_MILLISECONDS = 100;
const DIGEST_TIME_CYCLE_THRESHOLD = 86400; const DIGEST_TIME_CYCLE_THRESHOLD = 86400;
const EXCITER_HOLDOFF_SECONDS = 60; const EXCITER_HOLDOFF_SECONDS = 60;

View File

@ -2,7 +2,7 @@
"id": "diract", "id": "diract",
"name": "DirAct", "name": "DirAct",
"shortName": "DirAct", "shortName": "DirAct",
"version": "0.01", "version": "0.02",
"description": "Proximity interaction detection.", "description": "Proximity interaction detection.",
"icon": "diract.png", "icon": "diract.png",
"type": "app", "type": "app",

View File

@ -4,3 +4,4 @@
Take 'beta' tag off Take 'beta' tag off
0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later 0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later
Add CRC checks for common bootloaders that we know don't work Add CRC checks for common bootloaders that we know don't work
0.04: Include a precompiled bootloader for easy bootloader updates

File diff suppressed because it is too large Load Diff

View File

@ -3,33 +3,42 @@
<link rel="stylesheet" href="../../css/spectre.min.css"> <link rel="stylesheet" href="../../css/spectre.min.css">
</head> </head>
<body> <body>
<p><b>THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE <p>This tool allows you to update the bootloader on <a href="https://www.espruino.com/Bangle.js2">Bangle.js 2</a> devices
INSTRUCTIONS FOR <a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">BANGLE.JS</a> 1 AND <a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">BANGLE.JS 2</a></b>. For usage on Bangle.js 2 you'll likely need to have an updated bootloader.</p> from within the App Loader.</p>
<div id="fw-unknown"> <div id="fw-unknown">
<p><b>Firmware updates using the App Loader are only possible on <p><b>Firmware updates using the App Loader are only possible on
Bangle.js 2. For firmware updates on Bangle.js 1 please Bangle.js 2. For firmware updates on Bangle.js 1 please
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p> <a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p>
</div> </div>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span></p> <ul>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and bootloader is <span id="boot-version" style="font-weight:bold">unknown</span></p>
</ul>
<div id="fw-ok" style="display:none"> <div id="fw-ok" style="display:none">
<p>If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x bootloader, the Firmware Update
will fail with a message about the bootloader version. If so, please <a href="bootloader_espruino_2v11.52_banglejs2.hex" class="fw-link">click here to update to bootloader 2v11.52</a> and then click the 'Upload' button that appears.</p>
<div id="latest-firmware" style="display:none"> <div id="latest-firmware" style="display:none">
<p>The currently available Espruino firmware releases are:</p> <p>The currently available Espruino firmware releases are:</p>
<ul id="latest-firmware-list"> <ul id="latest-firmware-list">
</ul> </ul>
<p>To update, click the link and then click the 'Upload' button that appears.</p> <p>To update, click a link above and then click the 'Upload' button that appears.</p>
</div> </div>
<a href="#" id="advanced-btn">Advanced ▼</a>
<p>Or you can upload a hex or zip file here. This file should be an <code>.app_hex</code> <div id="advanced-div" style="display:none">
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">the Bangle.js 2 page</a>. Firmware
is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies
the new firmware into internal Storage.</p>
<p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code>
file, *not* the normal <code>.hex</code> (as that contains the bootloader as well).</p> file, *not* the normal <code>.hex</code> (as that contains the bootloader as well).</p>
<p><b>DANGER!</b> No verification is performed on uploaded ZIP or HEX files - you could
potentially overwrite your bootloader with the wrong binary and brick your Bangle.</p>
<input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br> <input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br>
</div>
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p> <p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
</div> </div>
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">the Bangle.js page</a>. Firmware
is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies
the new firmware into internal Storage.</p>
<pre id="log"></pre> <pre id="log"></pre>
@ -38,7 +47,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
<script> <script>
var hex;
var hexJS; // JS to upload hex var hexJS; // JS to upload hex
var HEADER_LEN = 16; // size of app flash header var HEADER_LEN = 16; // size of app flash header
var APP_START = 0x26000; var APP_START = 0x26000;
@ -47,35 +55,46 @@ var MAX_ADDRESS = 0x1000000; // discount anything in hex file above this
var VERSION = 0x12345678; // VERSION! Use this to test firmware in JS land var VERSION = 0x12345678; // VERSION! Use this to test firmware in JS land
var DEBUG = false; var DEBUG = false;
function clearLog() {
document.getElementById('log').innerText = "";
console.log("Log Cleared");
}
function log(t) { function log(t) {
document.getElementById('log').innerText += t+"\n"; document.getElementById('log').innerText += t+"\n";
console.log(t); console.log(t);
} }
function onInit(device) { function onInit(device) {
console.log(device); console.log("fwupdate init", device);
if (device && device.version) if (device && device.version)
document.getElementById("fw-version").innerText = device.version; document.getElementById("fw-version").innerText = device.version;
if (device && device.id=="BANGLEJS2") { if (device && device.id=="BANGLEJS2") {
document.getElementById("fw-unknown").style = "display:none"; document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = ""; document.getElementById("fw-ok").style = "";
} }
Puck.eval("E.CRC32(E.memoryArea(0xF7000,0x7000))", crc => {
console.log("Bootloader CRC = "+crc);
var version = `unknown (CRC ${crc})`;
if (crc==1339551013) version = "2v10.219";
if (crc==1207580954) version = "2v10.236";
if (crc==3435933210) version = "2v11.52";
if (crc==46757280) version = "2v11.58";
document.getElementById("boot-version").innerText = version;
});
} }
function checkForFileOnServer() { function checkForFileOnServer() {
function getURL(url, callback) { function getURL(url, callback) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.onload = callback; xhr.onload = callback;
baseURL = url; xhr.open("GET", url);
xhr.open("GET", baseURL);
xhr.responseType = "document"; xhr.responseType = "document";
xhr.send(); xhr.send();
} }
function getFilesFromURL(url, regex, callback) { function getFilesFromURL(url, regex, callback) {
getURL(url, function() { getURL(url, function() {
console.log(this.responseXML) //console.log(this.responseXML)
var files = []; var files = [];
var elements = this.responseXML.getElementsByTagName("a"); var elements = this.responseXML.getElementsByTagName("a");
for (var i=0;i<elements.length;i++) { for (var i=0;i<elements.length;i++) {
@ -113,8 +132,7 @@ function checkForFileOnServer() {
for (var i=0;i<fwlinks.length;i++) for (var i=0;i<fwlinks.length;i++)
fwlinks[i].addEventListener("click", e => { fwlinks[i].addEventListener("click", e => {
e.preventDefault(); e.preventDefault();
var url = e.target.href; downloadURL(e.target.href).then(info=>{
downloadZipFile(url).then(info=>{
document.getElementById("upload").style = ""; // show upload document.getElementById("upload").style = ""; // show upload
}); });
}); });
@ -122,6 +140,32 @@ function checkForFileOnServer() {
}); });
} }
function downloadURL(url) {
clearLog();
log("Downloading "+url);
if (url.endsWith(".zip")) {
return downloadZipFile(url);
} else if (url.endsWith(".hex")) {
return downloadHexFile(url);
} else {
log("Unknown URL "+url+" - expecting .hex or .zip extension");
return Promise.reject();
}
}
function downloadHexFile(url) {
return new Promise(resolve => {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
hexFileLoaded(this.responseText.toString());
resolve();
};
xhr.open("GET", url);
xhr.responseType = "text";
xhr.send();
});
}
function downloadZipFile(url) { function downloadZipFile(url) {
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
Espruino.Core.Utils.getBinaryURL(url, (err, binary) => { Espruino.Core.Utils.getBinaryURL(url, (err, binary) => {
@ -154,15 +198,15 @@ function convertZipFile(binary) {
if (info.bin_file.byteLength > APP_MAX_LENGTH) throw new Error("Firmware file is too big!"); if (info.bin_file.byteLength > APP_MAX_LENGTH) throw new Error("Firmware file is too big!");
info.storageContents = new Uint8Array(info.bin_file.byteLength + HEADER_LEN) info.storageContents = new Uint8Array(info.bin_file.byteLength + HEADER_LEN)
info.storageContents.set(new Uint8Array(info.bin_file), HEADER_LEN); info.storageContents.set(new Uint8Array(info.bin_file), HEADER_LEN);
console.log("ZIP downloaded and decoded",info);
createJS_app(info.storageContents, APP_START, APP_START+info.bin_file.byteLength); createJS_app(info.storageContents, APP_START, APP_START+info.bin_file.byteLength);
log("Download complete");
console.log("Download complete",info);
document.getElementById("upload").style = ""; // show upload document.getElementById("upload").style = ""; // show upload
return info; return info;
}).catch(err => log("ERROR:" + err)); }).catch(err => log("ERROR:" + err));
} }
function handleFileSelect(event) { function handleFileSelect(event) {
clearLog();
if (event.target.files.length!=1) { if (event.target.files.length!=1) {
log("More than one file selected!"); log("More than one file selected!");
return; return;
@ -172,13 +216,14 @@ function handleFileSelect(event) {
var reader = new FileReader(); var reader = new FileReader();
if (file.name.endsWith(".hex") || file.name.endsWith(".app_hex")) { if (file.name.endsWith(".hex") || file.name.endsWith(".app_hex")) {
reader.onload = function(event) { reader.onload = function(event) {
hex = event.target.result.split("\n"); log("HEX uploaded");
document.getElementById("upload").style = ""; // show upload document.getElementById("upload").style = ""; // show upload
fileLoaded(); hexFileLoaded(event.target.result);
}; };
reader.readAsText(event.target.files[0]); reader.readAsText(event.target.files[0]);
} else if (file.name.endsWith(".zip")) { } else if (file.name.endsWith(".zip")) {
reader.onload = function(event) { reader.onload = function(event) {
log("ZIP uploaded");
convertZipFile(event.target.result); convertZipFile(event.target.result);
}; };
reader.readAsArrayBuffer(event.target.files[0]); reader.readAsArrayBuffer(event.target.files[0]);
@ -187,25 +232,6 @@ function handleFileSelect(event) {
} }
}; };
function parseLines(dataCallback) {
var addrHi = 0;
hex.forEach(function(hexline) {
if (DEBUG) console.log(hexline);
var bytes = hexline.substr(1,2);
var addrLo = parseInt(hexline.substr(3,4),16);
var cmd = hexline.substr(7,2);
if (cmd=="02") addrHi = parseInt(hexline.substr(9,4),16) << 4; // Extended Segment Address
else if (cmd=="04") addrHi = parseInt(hexline.substr(9,4),16) << 16; // Extended Linear Address
else if (cmd=="00") {
var addr = addrHi + addrLo;
var data = [];
for (var i=0;i<16;i++) data.push(parseInt(hexline.substr(9+(i*2),2),16));
dataCallback(addr,data);
}
});
}
function CRC32(data) { function CRC32(data) {
var crc = 0xFFFFFFFF; var crc = 0xFFFFFFFF;
data.forEach(function(d) { data.forEach(function(d) {
@ -278,6 +304,7 @@ function createJS_app(binary, startAddress, endAddress) {
} }
hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n'; hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
hexJS += '\x10setTimeout(()=>E.reboot(), 1000);\n'; hexJS += '\x10setTimeout(()=>E.reboot(), 1000);\n';
log("Firmware update ready for upload");
} }
@ -302,12 +329,32 @@ function createJS_bootloader(binary, startAddress, endAddress) {
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n'; hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`; hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += `})()\n`; hexJS += `})()\n`;
log("Bootloader ready for upload");
} }
function fileLoaded() { function hexFileLoaded(hexString) {
var hex = hexString.split("\n"); // array of lines of the hex file
function hexParseLines(dataCallback) {
var addrHi = 0;
hex.forEach(function(hexline) {
if (DEBUG) console.log(hexline);
var bytes = hexline.substr(1,2);
var addrLo = parseInt(hexline.substr(3,4),16);
var cmd = hexline.substr(7,2);
if (cmd=="02") addrHi = parseInt(hexline.substr(9,4),16) << 4; // Extended Segment Address
else if (cmd=="04") addrHi = parseInt(hexline.substr(9,4),16) << 16; // Extended Linear Address
else if (cmd=="00") {
var addr = addrHi + addrLo;
var data = [];
for (var i=0;i<16;i++) data.push(parseInt(hexline.substr(9+(i*2),2),16));
dataCallback(addr,data);
}
});
}
// Work out addresses // Work out addresses
var startAddress, endAddress = 0; var startAddress, endAddress = 0;
parseLines(function(addr, data) { hexParseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range if (addr>MAX_ADDRESS) return; // ignore data out of range
if (startAddress === undefined || addr<startAddress) if (startAddress === undefined || addr<startAddress)
startAddress = addr; startAddress = addr;
@ -319,7 +366,7 @@ function fileLoaded() {
// Work out data // Work out data
var binary = new Uint8Array(HEADER_LEN + endAddress-startAddress); var binary = new Uint8Array(HEADER_LEN + endAddress-startAddress);
binary.fill(0); // actually seems to assume a block is filled with 0 if not complete binary.fill(0); // actually seems to assume a block is filled with 0 if not complete
parseLines(function(addr, data) { hexParseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range if (addr>MAX_ADDRESS) return; // ignore data out of range
var binAddr = HEADER_LEN + addr - startAddress; var binAddr = HEADER_LEN + addr - startAddress;
binary.set(data, binAddr); binary.set(data, binAddr);
@ -351,6 +398,10 @@ function handleUpload() {
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false); document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload); document.getElementById("upload").addEventListener("click", handleUpload);
document.getElementById("advanced-btn").addEventListener("click", function() {
document.getElementById("advanced-btn").style = "display:none";
document.getElementById("advanced-div").style = "";
});
setTimeout(checkForFileOnServer, 10); setTimeout(checkForFileOnServer, 10);
</script> </script>

View File

@ -1,8 +1,8 @@
{ {
"id": "fwupdate", "id": "fwupdate",
"name": "Firmware Update", "name": "Firmware Update",
"version": "0.03", "version": "0.04",
"description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates", "description": "Uploads new Espruino firmwares to Bangle.js 2",
"icon": "app.png", "icon": "app.png",
"type": "RAM", "type": "RAM",
"tags": "tools,system", "tags": "tools,system",

View File

@ -8,3 +8,4 @@
0.07: Added coloured bar charts 0.07: Added coloured bar charts
0.08: Suppress bleed through of E.showMenu's when displaying bar charts 0.08: Suppress bleed through of E.showMenu's when displaying bar charts
0.09: Fix file naming so months are 1-based (not 0) (fix #1119) 0.09: Fix file naming so months are 1-based (not 0) (fix #1119)
0.10: Adds additional 3 minute setting for HRM

View File

@ -28,8 +28,8 @@ function menuSettings() {
"< Back":()=>menuMain(), "< Back":()=>menuMain(),
"Heart Rt":{ "Heart Rt":{
value : 0|s.hrm, value : 0|s.hrm,
min : 0, max : 2, min : 0, max : 3,
format : v=>["Off","10 mins","Always"][v], format : v=>["Off","3 mins","10 mins","Always"][v],
onchange : v => { s.hrm=v;setSettings(s); } onchange : v => { s.hrm=v;setSettings(s); }
} }
}); });

View File

@ -1,10 +1,20 @@
(function(){ (function(){
var settings = require("Storage").readJSON("health.json",1)||{}; var settings = require("Storage").readJSON("health.json",1)||{};
var hrm = 0|settings.hrm; var hrm = 0|settings.hrm;
if (hrm==1) { if (hrm == 1 || hrm == 2) {
function onHealth() { function onHealth() {
Bangle.setHRMPower(1, "health"); Bangle.setHRMPower(1, "health");
setTimeout(()=>Bangle.setHRMPower(0, "health"),2*60000); // give it 2 minutes setTimeout(()=>Bangle.setHRMPower(0, "health"),hrm*60000); // give it 1 minute detection time for 3 min setting and 2 minutes for 10 min setting
if (hrm == 1){
for (var i = 1; i <= 2; i++){
setTimeout(()=>{
Bangle.setHRMPower(1, "health");
setTimeout(()=>{
Bangle.setHRMPower(0, "health");
}, (i * 200000) + 60000);
}, (i * 200000));
}
}
} }
Bangle.on("health", onHealth); Bangle.on("health", onHealth);
Bangle.on('HRM', h => { Bangle.on('HRM', h => {

View File

@ -51,7 +51,7 @@ function saveCSV(data, date, title) {
} }
function downloadHealth(filename, callback) { function downloadHealth(filename, callback) {
Util.showModal("Downloading Track..."); Util.showModal("Downloading Health info...");
Util.readStorage(filename, data => { Util.readStorage(filename, data => {
Util.hideModal(); Util.hideModal();
callback(data); callback(data);

View File

@ -1,7 +1,7 @@
{ {
"id": "health", "id": "health",
"name": "Health Tracking", "name": "Health Tracking",
"version": "0.09", "version": "0.10",
"description": "Logs health data and provides an app to view it (requires firmware 2v10.100 or later)", "description": "Logs health data and provides an app to view it (requires firmware 2v10.100 or later)",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,health", "tags": "tool,system,health",

View File

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

BIN
apps/hrmaccevents/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

View File

@ -0,0 +1,190 @@
<html>
<head>
<title>Bangle.js Accelerometer streaming</title>
</head>
<body>
<script src="https://www.puck-js.com/puck.js"></script>
<button id="btnConnect">Connect</button>
<button id="btnStop">Stop</button>
<button id="btnReset">Reset</button>
<button id="btnSave">Save CSV</button>
<p id="result"></p>
<script>
var BANGLE_CODE = String.raw`
var accData=[];
var maxSize=0;
var filename="log.csv";
//0 print, 1 BT, 2 File
var method=1;
var running = true;
var gotHRMraw = false;
var gotBTHRM = false;
var gotHRM = false;
var gotAcc = false;
function gotAll(){
return running && gotBTHRM && gotHRM && gotHRMraw && gotAcc;
}
Bangle.setHRMPower(1);
if (Bangle.setBTHRMPower){
print("Use BTHRM");
Bangle.setBTHRMPower(1);
Bangle.setBTHRMPower(1);
} else {
gotBTHRM = true;
}
var write=null;
if (method == 2){
var f = require('Storage').open(filename,"w");
f.erase();
f = require('Storage').open(filename,"a");
write = function(str){f.write(str);};
} else if (method == 1){
write = function(str){Bluetooth.print("DATA: " + str);};
} else {
write=print;
}
write("Time,Acc_x,Acc_y,Acc_z,HRM_b,HRM_c,HRM_r,HRM_f,PPG_r,PPG_o,BTHRM\n");
function writeAcc(e){
gotAcc = true;
e.date=Date.now();
accData.push(e);
accData.splice(0, accData.length - maxSize);
}
function writeAccDirect(e){
gotAcc = true;
if (!gotAll()) return;
write(Date.now()+","+e.x+","+e.y+","+e.z+",,,,,,,,\n");
}
function writeBTHRM(e){
gotBTHRM = true;
if (!gotAll()) return;
write(Date.now()+",,,,,,,,,,"+e.bpm+"\n");
}
function writeHRM(e){
gotHRM = true;
if (!gotAll()) return;
while(accData.length > 0){
var c = accData.shift();
if (c) write(c.date+","+c.x+","+c.y+","+c.z+",,,,,,,,\n");
}
write(Date.now()+",,,,"+e.bpm+","+e.confidence+",,,,\n");
}
function writeHRMraw(e){
gotHRMraw = true;
if (!gotAll()) return;
write(Date.now()+",,,,,,"+e.raw+","+e.filt+","+e.vcPPG+","+e.vcPPGoffs+",\n");
}
if(maxSize){
Bangle.on("accel", writeAcc);
} else {
Bangle.on("accel", writeAccDirect);
}
Bangle.on("HRM-raw", writeHRMraw);
Bangle.on("HRM", writeHRM);
Bangle.on("BTHRM", writeBTHRM);
g.clear();
g.setColor(1,0,0);
g.fillRect(0,0,g.getWidth(),g.getHeight());
var intervalId = -1;
intervalId = setInterval(()=>{
print("Checking... Acc:" + gotAcc + " BTHRM:" + gotBTHRM + " HRM:" + gotHRM + " HRM raw:" + gotHRMraw);
if (gotAll()){
g.setColor(0,1,0);
g.fillRect(0,0,g.getWidth(),g.getHeight());
clearInterval(intervalId);
}
}, 1000);
if (Bangle.setBTHRMPower){
intervalId = setInterval(()=>{
if (!Bangle.isBTHRMOn()) Bangle.setBTHRMPower(1);
}, 5000);
}
`;
var connection;
var lineCount=-1;
function stop (){
connection.write("running = false; \n");
connection.close();
connection = undefined;
}
document.getElementById("btnSave").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/csv;charset=utf-8,' + encodeURI(localStorage.getItem("data"));
h.target = '_blank';
h.download = "DATA.csv";
h.click();
});
document.getElementById("btnReset").addEventListener("click", function() {
if (connection) {
stop();
}
document.getElementById("result").innerText="";
lineCount=-1;
localStorage.removeItem("data");
});
document.getElementById("btnStop").addEventListener("click", function() {
if (connection) {
stop();
}
});
document.getElementById("btnConnect").addEventListener("click", function() {
localStorage.setItem("data", "");
if (connection) {
stop();
document.getElementById("result").innerText="0";
lineCount=-1;
}
Puck.connect(function(c) {
if (!c) {
console.log("Couldn't connect!\n");
return;
}
connection = c;
var buf = "";
connection.on("data", function(d) {
buf += d;
var l = buf.split("\n");
buf = l.pop();
l.forEach(onLine);
});
connection.write("reset();\n", function() {
setTimeout(function() {
connection.write("\x03\x10if(1){"+BANGLE_CODE+"}\n",
function() { console.log("Ready..."); });
}, 1500);
});
});
});
function onLine(line) {
console.log("RECEIVED:"+line);
if (line.startsWith("DATA:")){
localStorage.setItem("data", localStorage.getItem("data") + line.substr(5) + "\n");
lineCount++;
document.getElementById("result").innerText="Captured events: " + lineCount;
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
{
"id": "hrmaccevents",
"name": "HRM Accelerometer event recorder",
"shortName": "HRM ACC recorder",
"version": "0.01",
"type": "ram",
"description": "Record HRM and accelerometer events in high resolution to CSV files in your browser",
"icon": "app.png",
"tags": "debug",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": [ ]
}

View File

@ -2,3 +2,4 @@
0.02: Stopped watchface from flashing every interval 0.02: Stopped watchface from flashing every interval
0.03: Move to Bangle.setUI to launcher support 0.03: Move to Bangle.setUI to launcher support
0.04: Tweaks for compatibility with BangleJS2 0.04: Tweaks for compatibility with BangleJS2
0.05: Time-word now readable on Bangle.js 2

View File

@ -46,7 +46,7 @@ const dy = big ? 22 : 16;
const fontSize = big ? 3 : 2; // "6x8" const fontSize = big ? 3 : 2; // "6x8"
const passivColor = 0x3186 /*grey*/ ; const passivColor = 0x3186 /*grey*/ ;
const activeColorNight = 0xF800 /*red*/ ; const activeColorNight = 0xF800 /*red*/ ;
const activeColorDay = 0xFFFF /* white */; const activeColorDay = g.theme.fg;
var hidxPrev; var hidxPrev;
var showDigitalTime = false; var showDigitalTime = false;

View File

@ -1,7 +1,7 @@
{ {
"id": "impwclock", "id": "impwclock",
"name": "Imprecise Word Clock", "name": "Imprecise Word Clock",
"version": "0.04", "version": "0.05",
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
"icon": "clock-impword.png", "icon": "clock-impword.png",
"type": "clock", "type": "clock",

View File

@ -1,2 +1,3 @@
0.01: first release 0.01: first release
0.02: Themeable app icon 0.02: Themeable app icon
0.03: Behave better on Bangle.js 1

View File

@ -5,7 +5,7 @@ let tStart;
let tNow; let tNow;
let counter=-1; let counter=-1;
const icon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap")); const icon = require("heatshrink").decompress(atob("mEwwI0xg+evPsAon+ApX8Aon4AonwAod78AFDv4FWvoFE/IFDz4FXvIFD3wFE/wFW7wFDh5xBAoUfAok/Aol/BZUXAogA6A="));
function timeToText(t) { // Courtesy of stopwatch app function timeToText(t) { // Courtesy of stopwatch app
let hrs = Math.floor(t/3600000); let hrs = Math.floor(t/3600000);
@ -50,4 +50,5 @@ g.drawImage(icon,w/2-24,h/2-24);
g.setFontAlign(0,0); g.setFontAlign(0,0);
require("Font8x12").add(Graphics); require("Font8x12").add(Graphics);
g.setFont("8x12"); g.setFont("8x12");
g.drawString("Click button to count.", w/2, h/2+22); g.drawString("Click button 1 to count.", w/2, h/2+22);

View File

@ -1,7 +1,7 @@
{ {
"id": "lapcounter", "id": "lapcounter",
"name": "Lap Counter", "name": "Lap Counter",
"version": "0.02", "version": "0.03",
"description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).", "description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],

View File

@ -1 +1,3 @@
0.01: Launch app. 0.01: Launch app.
0.02: 12k steps are 360 degrees - improves readability of steps.
0.03: Battery improvements through sleep (no minute updates) and partial updates of drawing.

View File

@ -1,20 +1,21 @@
# NotAnalog # Not Analog
An analog watch face for people (like me) that can not read analog watch faces. An analog watch face for people (like me) that can not read analog watch faces.
It looks like an analog clock, but its not! It shows the time digital - check the It looks like an analog clock, but its not! It shows the time digital - check the
4 numbers on the watch face ;) 4 numbers on the watch face ;)
The red hand shows the number of steps (0 = 0°, 2.5k = 90°, 5k = 180°, ...) and the The red hand shows the number of steps (12k steps = 360 degrees) and the
black one the battery level (100% = 0°, 75% = 270°, 50% = 180°, ...). black one the battery level (100% = 360 degrees).
The selected theme is also respected. Note that this watch face is in fullscreen The selected theme is also respected. Note that this watch face is in fullscreen
mode, but widgets are still loaded in background. mode, but widgets are still loaded in background.
## Other features ## Other features
- Set a timer - simply touch top (+5min.) or bottom (-5 min.). - Set a timer - simply touch top (+5min.) or bottom (-5 min.).
- If the weather is available through the weather app, the outside temp. will be shown. - If the weather is available through the weather app, the outside temp. will be shown.
- If the battery is charged, the icons will change. - Sleep modus at midnight to save more battery (no minute updates).
- Icons for charging and GPS.
- If you have done more than 10k steps, the red hand and icon will turn green. - If you have done more than 10k steps, the red hand and icon will turn green.
- Shows current lock status of your bangle va a colored dot in the middle. - Shows current lock status of your bangle va a colored dot in the middle.
- Sows symbol if GPS is on.
## Screenshots ## Screenshots
![](screenshot_1.png) ![](screenshot_1.png)

View File

@ -3,7 +3,7 @@
"name": "Not Analog", "name": "Not Analog",
"shortName":"Not Analog", "shortName":"Not Analog",
"icon": "notanalog.png", "icon": "notanalog.png",
"version":"0.1", "version":"0.03",
"readme": "README.md", "readme": "README.md",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"description": "An analog watch face for people that can not read analog watch faces.", "description": "An analog watch face for people that can not read analog watch faces.",

View File

@ -28,7 +28,8 @@ var state = {
maxSteps: 10000, maxSteps: 10000,
bat: 0, bat: 0,
has_weather: false, has_weather: false,
temp: "-" temp: "-",
sleep: false,
} }
var chargeImg = { var chargeImg = {
@ -55,6 +56,12 @@ var gpsImg = {
buffer : E.toArrayBuffer(atob("AAAMAAAAD4AAAAHAAAAA4AAADjABAA8YAYADmAPAAcwD4DzMB/B8zAf4fAAH/HwAB/74AAf/wAAH/4AAB//AAAP/4AAD//AAA//4AAH//AAA//4AAH//AAA//4ABH/4AAYP4AAHgAAAB/AAAA/4AAAP+AAAD/gAAP//gAD//4AA=")) buffer : E.toArrayBuffer(atob("AAAMAAAAD4AAAAHAAAAA4AAADjABAA8YAYADmAPAAcwD4DzMB/B8zAf4fAAH/HwAB/74AAf/wAAH/4AAB//AAAP/4AAD//AAA//4AAH//AAA//4AAH//AAA//4ABH/4AAYP4AAHgAAAB/AAAA/4AAAP+AAAD/gAAP//gAD//4AA="))
}; };
var sleepImg = {
width : 128, height : 128, bpp : 1,
transparent : 0,
buffer : require("heatshrink").decompress(atob("ABk//+AB5l///AB5wfDh4kIF4s/8AgIj4ED//wB5E+AYUB//8B5F8AYUD+F+B5H4AYUH8E/Bw8BHIcHwEfMA4PEh4RBQo8DNIYPBIIIPGDAkeEwJGDAAaZEB4MAOAisB+COEngCBOAn///4NAgPCMAgfCZ4gPCaIpWBd4l4QQZtFD4gPCgYPEQw3wRo41FgHxfw5tEB4sHfg7DC8IPDFQb8DB4XgB4ZDDWosD4DNCbAbsEB4zRDB5bRDfghKDB4bRCRwwPBuAFCbISOCgP/EYMPK4kPDgKOCgbiBDIJLDEoIYBRwQPD//DD4hQBbgPgF4QCB84PDBgICCDgJTBEQP/B4QFCwAIDKYIRB/84bQX/x+AD4YPCwF+nguC+B9FMYJuBngPBIgKmCeQoPEg5dBB4ryBB4kPPoMfdohRCB4McSYPAg5dBeQoPCjxOBCIIPBcQYUBL4N4j0B/hQBAATPBV4RnB/EegYFB//AbYYPCgfh+EeZgJNDAYYWBCQUedgN/NoUD/xhDEwUOj67BBQd/IAIFEh8+gZ3CNQMfSQkMBQN8g/wMATKBCQIAEh/4IAMPdoQlCB4vwn7sC/5OBSIQPE8F+KoRoBfIIPFPwP8cASyBQoIPG4JABJQUHAoJwEBAODIAUBAIIlBOAg/BgfgcAMDBYN+A4IPFC4I+BB4U/wKAFh8PwJ5BB4SFBB40fFANggPAg5nBSAsPzwwBDIRGB+F8L4v+NAIZCh8B+E8B4v8RAN4AwMOgH4jwPEY4M+gEwB4d8UA34E4sAn0PA4pHGgEeWApHBfA8HB4vgQ4oPBw4PF8IPGbALQEgfB8IXF4/DB4vD8YHG4LgEEwPDA4oPIA4w3BA4pWBF4poGdAJOEAAQPFQwyoDB4q2GB6VwB5twvAFDhwPIvAPFhwPNjwPTgaSDBwgPBj//wH//6qCnAPI4IPEvgPY4APEngPGjxPOL5KvER4gPFV5IPKZ4gPEZ4oPJd5QPF+APEg+AB5kHB5+HB40B8APFwfBVgIPCgeB8K0CB4fDB4kH4YXCLQfDB4oHBB43B8ZABB4UB4/DKgYPCCwRPDHAIPEKwgPDh+HB434B4yIDQwbGCB4ceB434ngPFnzIDewc+gEwB4MEgF8j4PFA4V4B4MOE4MeB4s8h+AB4QsBG4YADI4PA+APCgfwvgPFj8D8FwB4L2B8BnCAAcPwKQBL4UPEoIPFFwP8B4cfCwQPGvwPDv42BB4oHBn+AB4MB/gXBB4sB/Ef8BPC/B2BB4sADIP8B4M/8CeGAAN+gP/4fB//AWwIAGn5LB/4ABEwIPHj/Aj4OB/BGBB46ZBgYPBKAJ+GOAQZBj4sBEoIPHgP+Aod/Nw4KCDQQUFKAw6Ch5eIKAX/FYP/JxArCPwQSCABM/BwI+KGAYuLEAYeGA="))
};
/* /*
* Based on the great multi clock from https://github.com/jeffmer/BangleApps/ * Based on the great multi clock from https://github.com/jeffmer/BangleApps/
@ -83,17 +90,18 @@ Graphics.prototype.setNormalFont = function(scale) {
function getSteps() { function getSteps() {
var steps = 0; try{
let health; if (WIDGETS.wpedom !== undefined) {
try { return WIDGETS.wpedom.getSteps();
health = require("health"); } else if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom.getSteps();
}
} catch(ex) { } catch(ex) {
return steps; // In case we failed, we can only show 0 steps.
} }
health.readDay(new Date(), h=>steps+=h.steps); return 0;
return steps; }
}
function drawBackground() { function drawBackground() {
@ -169,7 +177,12 @@ function drawData() {
} }
// Default are the steps // Default are the steps
drawDataHand(parseInt(state.steps*360/state.maxSteps)); drawDataHand(parseInt(state.steps*360/12000));
}
function drawTextCleared(s, x, y){
g.clearRect(x-15, y-22, x+15, y+15);
g.drawString(s, x, y);
} }
@ -178,36 +191,33 @@ function drawTime(){
g.setFontAlign(0,0,0); g.setFontAlign(0,0,0);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
var currentDate = new Date();
var posX = 14; var posX = 14;
var posY = 14; var posY = 14;
// Hour // Hour
var h = currentDate.getHours(); var h = state.currentDate.getHours();
var h1 = parseInt(h / 10); var h1 = parseInt(h / 10);
var h2 = h < 10 ? h : h - h1*10; var h2 = h < 10 ? h : h - h1*10;
g.drawString(h1, cx, posY+8); drawTextCleared(h1, cx, posY+8);
g.drawString(h2, W-posX, cy+5); drawTextCleared(h2, W-posX, cy+5);
// Minutes // Minutes
var m = currentDate.getMinutes(); var m = state.currentDate.getMinutes();
var m1 = parseInt(m / 10); var m1 = parseInt(m / 10);
var m2 = m < 10 ? m : m - m1*10; var m2 = m < 10 ? m : m - m1*10;
g.drawString(m2, cx, H-posY); drawTextCleared(m2, cx, H-posY);
g.drawString(m1, posX-1, cy+5); drawTextCleared(m1, posX-1, cy+5);
} }
function drawDate(){ function drawDate(){
var currentDate = new Date();
// Date // Date
g.setFontAlign(-1,0,0); g.setFontAlign(-1,0,0);
g.setNormalFont(); g.setNormalFont();
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
var dayStr = locale.dow(currentDate, true).toUpperCase(); var dayStr = locale.dow(state.currentDate, true).toUpperCase();
g.drawString(dayStr, cx/2-15, cy/2-5); g.drawString(dayStr, cx/2-15, cy/2-5);
g.drawString(currentDate.getDate(), cx/2-15, cy/2+17); g.drawString(state.currentDate.getDate(), cx/2-15, cy/2+17);
} }
@ -222,22 +232,36 @@ function drawLock(){
function handleState(fastUpdate){ function handleState(fastUpdate){
// Set theme color state.currentDate = new Date();
/*
* Sleep modus
*/
var minutes = state.currentDate.getMinutes();
var hours = state.currentDate.getHours();
if(!isAlarmEnabled() && fastUpdate && hours == 00 && minutes == 01){
state.sleep = true;
return;
}
// Set steps
state.steps = getSteps();
// Color based on state
state.color = isAlarmEnabled() ? "#FF6A00" : state.color = isAlarmEnabled() ? "#FF6A00" :
state.steps > state.maxSteps ? "#00ff00" : state.steps > state.maxSteps ? "#00ff00" :
"#ff0000"; "#ff0000";
if(fastUpdate){ /*
* 5 Minute updates
*/
if(minutes % 5 == 0 && fastUpdate){
return; return;
} }
// Set battery // Set battery
state.bat = E.getBattery(); state.bat = E.getBattery();
// Set steps
state.steps = getSteps();
state.maxSteps = 10000;
// Set weather // Set weather
state.has_weather = true; state.has_weather = true;
try { try {
@ -251,7 +275,16 @@ function handleState(fastUpdate){
} catch(ex) { } catch(ex) {
state.has_weather = false; state.has_weather = false;
} }
}
function drawSleep(){
g.reset();
g.clearRect(0, 0, g.getWidth(), g.getHeight());
drawBackground();
g.setColor(1,1,1);
g.drawImage(sleepImg, cx - sleepImg.width/2, cy- sleepImg.height/2);
} }
@ -260,6 +293,13 @@ function draw(fastUpdate){
handleState(fastUpdate); handleState(fastUpdate);
handleAlarm(); handleAlarm();
if(state.sleep){
drawSleep();
// We don't queue draw again - so its sleeping until
// the user presses the btn again.
return;
}
// Clear watch face // Clear watch face
if(fastUpdate){ if(fastUpdate){
var innerRect = 20; var innerRect = 20;
@ -279,8 +319,8 @@ function draw(fastUpdate){
drawDate(); drawDate();
drawLock(); drawLock();
drawState(); drawState();
drawData();
drawTime(); drawTime();
drawData();
// Queue draw in one minute // Queue draw in one minute
queueDraw(); queueDraw();
@ -292,7 +332,7 @@ function draw(fastUpdate){
*/ */
Bangle.on('lcdPower',on=>{ Bangle.on('lcdPower',on=>{
if (on) { if (on) {
draw(false); draw(true);
} else { // stop draw timer } else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = undefined;
@ -300,11 +340,16 @@ Bangle.on('lcdPower',on=>{
}); });
Bangle.on('charging',function(charging) { Bangle.on('charging',function(charging) {
draw(false); draw(true);
}); });
Bangle.on('lock', function(isLocked) { Bangle.on('lock', function(isLocked) {
if(state.sleep){
state.sleep=false;
draw(false);
} else {
drawLock(); drawLock();
}
}); });
Bangle.on('touch', function(btn, e){ Bangle.on('touch', function(btn, e){
@ -335,7 +380,7 @@ function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() { drawTimeout = setTimeout(function() {
drawTimeout = undefined; drawTimeout = undefined;
draw(false); draw(true);
}, 60000 - (Date.now() % 60000)); }, 60000 - (Date.now() % 60000));
} }

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwwkAxAA/ADWP/AWV/4XVxAXX5/5Iif/xHvF6QXDF6YADF5GHv4lBAAP3hBfOw4VDAAd4F5gWIAAIxFF4wREAxB6EGAZbC5+PAAYZD+woFIozoIJI4uFFogxEIQPwEZLdKaoYJIFxBiEIxAuKEwZIGFxeM/ADBGA6MJFgYPDSIouKBYovHLxOPBQZgHL4YNCFxAXB+4XFEYgpEHIpAGKYoMBFw6IHF4oYDNA3//yPLC4QAHF4olFTo4KJX46SEF6AHCz7FLF5OJz70HR5IADxOYBAy/LF8nwAwowGF46OBwBOLF5AXB5AvTB4UIC4wvNH48PC4JfMLwxPCGAovGHw4JD/AvKC5JIBBRAkDXooACwwLBMQyMDRowwGGIwIC4A7JPQZkDAoYVKAAOACIYAE/BFJA=")) require("heatshrink").decompress(atob("mEwwkE/4AM+cikMRkU/CZoWDkMAgMQgESDB4WBgMSkcykMQDB8xgMjAwcyiETFxoPHD4IwMBxAgBG4gLFCYMgHxExgQXI+USEoMvBhBIJ+URmQMJERQKBiI8BmQZHKRJTBgETmURC48xC5PxgERaBPxga9KgDnJ+KQJKYJFIQoQXKOhAvK+cRgBeBC5ZfF+QVBAAacKBQgWGAALNIX4iJCAA0Bd5kwCw4ABWw3ygJrC+YWJAAJeGRwboBIQhMFj5GFLwcgCAoeFW4kxIwf/IAoXGgARCmQuEUgwXHiczCwMCFwfwC5sBfIMRYwilGC5MSkaTEagwXImbbGC54WGRwwXIbIwXh+YXVh6YHC453GN4IwFO5AXGJAIwFgQXHHwwwHgYXH+AXGGAxnBAAyfHGAwdBAAyfHCQaOKAAMgGBEhOxRIKGYoAJC5YWKVJClLbRjsJAAvyC48vC5v/mJ0RYgyiCiU/CyAASA=="))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -2,3 +2,4 @@
0.02: Set pace format to mm:ss, time format to h:mm:ss, 0.02: Set pace format to mm:ss, time format to h:mm:ss,
added settings to opt out of GPS and HRM added settings to opt out of GPS and HRM
0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2 0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2
0.04: Use the exstats module, and make what is displayed configurable

View File

@ -28,7 +28,24 @@ so if you have no GPS lock you just need to wait.
However you can just install the `Recorder` app, turn recording on in However you can just install the `Recorder` app, turn recording on in
that, and then start the `Run` app. that, and then start the `Run` app.
## Settings
Under `Settings` -> `App` -> `Run` you can change settings for this app.
* `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 `-`.
## TODO ## TODO
* Allow this app to trigger the `Recorder` app on and off directly. * 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
This app uses the [`exstats` module](/modules/exstats.js). When uploaded via the
app loader, the module is automatically included in the app's source. However
when developing via the IDE the module won't get pulled in by default.
There are some options to fix this easily - please check out the [modules README.md file](/modules/README.md)

View File

@ -1,68 +1,37 @@
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";
var fontValue = B2 ? "6x15:2" : "6x8:3"; var fontValue = B2 ? "6x15:2" : "6x8:3";
var headingCol = "#888"; var headingCol = "#888";
var running = false;
var fixCount = 0; var fixCount = 0;
var startTime;
var startSteps;
// This & previous GPS readings
var lastGPS, thisGPS;
var distance = 0; ///< distance in meters
var startSteps = Bangle.getStepCount(); ///< number of steps when we started
var lastStepCount = startSteps; // last time 'step' was called
var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute)
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
// --------------------------- // ---------------------------
let settings = Object.assign({
function formatTime(ms) { B1 : "dist",
let hrs = Math.floor(ms/3600000).toString(); B2 : "time",
let mins = (Math.floor(ms/60000)%60).toString(); B3 : "pacea",
let secs = (Math.floor(ms/1000)%60).toString(); B4 : "bpm",
B5 : "step",
if (hrs === '0') B6 : "caden",
return mins.padStart(2,0)+":"+secs.padStart(2,0); paceLength : 1000
else }, require("Storage").readJSON("run.json", 1) || {});
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!="");
} var exs = ExStats.getStats(statIDs, settings);
// Format speed in meters/second
function formatPace(speed) {
if (speed < 0.1667) {
return `__:__`;
}
const pace = Math.round(1000 / speed); // seconds for 1km
const min = Math.floor(pace / 60); // minutes for 1km
const sec = pace % 60;
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
// --------------------------- // ---------------------------
function clearState() { // Called to start/stop running
distance = 0;
startSteps = Bangle.getStepCount();
stepHistory.fill(0);
layout.dist.label=locale.distance(distance);
layout.time.label="00:00";
layout.pace.label=formatPace(0);
layout.hrm.label="--";
layout.steps.label=0;
layout.cadence.label= "0";
layout.status.bgCol = "#f00";
}
function onStartStop() { function onStartStop() {
running = !running; var running = !exs.state.active;
if (running) { if (running) {
clearState(); exs.start();
startTime = Date.now(); } else {
exs.stop();
} }
layout.button.label = running ? "STOP" : "START"; layout.button.label = running ? "STOP" : "START";
layout.status.label = running ? "RUN" : "STOP"; layout.status.label = running ? "RUN" : "STOP";
@ -72,107 +41,44 @@ function onStartStop() {
layout.render(); layout.render();
} }
var layout = new Layout( { var lc = [];
type:"v", c: [ // Load stats in pair by pair
{ type:"h", filly:1, c:[ for (var i=0;i<statIDs.length;i+=2) {
{type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol }, var sa = exs.stats[statIDs[i+0]];
{type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol } var sb = exs.stats[statIDs[i+1]];
]}, { type:"h", filly:1, c:[ lc.push({ type:"h", filly:1, c:[
{type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 }, {type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol },
{type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 } {type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol }
]}, { type:"h", filly:1, c:[
{type:"txt", font:fontHeading, label:"PACE", fillx:1, col:headingCol },
{type:"txt", font:fontHeading, label:"HEART", fillx:1, col:headingCol }
]}, { type:"h", filly:1, c:[
{type:"txt", font:fontValue, label:`__'__"`, id:"pace", fillx:1 },
{type:"txt", font:fontValue, label:"--", id:"hrm", fillx:1 }
]}, { type:"h", filly:1, c:[
{type:"txt", font:fontHeading, label:"STEPS", fillx:1, col:headingCol },
{type:"txt", font:fontHeading, label:"CADENCE", fillx:1, col:headingCol }
]}, { type:"h", filly:1, c:[
{type:"txt", font:fontValue, label:"0", id:"steps", fillx:1 },
{type:"txt", font:fontValue, label:"0", id:"cadence", fillx:1 }
]}, { type:"h", filly:1, c:[ ]}, { type:"h", filly:1, c:[
{type:"txt", font:fontValue, label:sa.getString(), id:sa.id, fillx:1 },
{type:"txt", font:fontValue, label:sb.getString(), id:sb.id, fillx:1 }
]});
sa.on('changed', e=>layout[e.id].label = e.getString());
sb.on('changed', e=>layout[e.id].label = e.getString());
}
// At the bottom put time/GPS state/etc
lc.push({ type:"h", filly:1, c:[
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
{type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 } {type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 }
]}, ]});
// Now calculate the layout
] var layout = new Layout( {
type:"v", c: lc
},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]}); },{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]});
clearState(); delete lc;
layout.render(); layout.render();
function onTimer() { // Handle GPS state change for icon
layout.clock.label = locale.time(new Date(),1);
if (!running) {
layout.render();
return;
}
// called once a second
var duration = Date.now() - startTime; // in ms
// set cadence based on steps over last minute
var stepsInMinute = E.sum(stepHistory);
var cadence = 60000 * stepsInMinute / Math.min(duration,60000);
// update layout
layout.time.label = formatTime(duration);
layout.steps.label = Bangle.getStepCount()-startSteps;
layout.cadence.label = Math.round(cadence);
layout.render();
// move step history onwards
stepHistory.set(stepHistory,1);
stepHistory[0]=0;
}
function radians(a) {
return a*Math.PI/180;
}
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
// https://www.movable-type.co.uk/scripts/latlong.html
function calcDistance(a,b) {
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
}
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
lastGPS = fix; // initialise on first fix
}
thisGPS = fix;
if (running) {
var d = calcDistance(lastGPS, thisGPS);
distance += d;
layout.dist.label=locale.distance(distance);
var duration = Date.now() - startTime; // in ms
var speed = distance * 1000 / duration; // meters/sec
layout.pace.label = formatPace(speed);
lastGPS = fix;
} }
}); });
Bangle.on("HRM", function(h) { // We always call ourselves once a second to update
layout.hrm.label = h.bpm; setInterval(function() {
}); layout.clock.label = locale.time(new Date(),1);
Bangle.on("step", function(steps) { layout.render();
if (running) { }, 1000);
layout.steps.label = steps-Bangle.getStepCount();
stepHistory[0] += steps-lastStepCount;
}
lastStepCount = steps;
});
let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true};
// We always call ourselves once a second, if only to update the time
setInterval(onTimer, 1000);
/* Turn GPS and HRM on right at the start to ensure
we get the highest chance of a lock. */
if (settings.use_hrm) Bangle.setHRMPower(true,"app");
if (settings.use_gps) Bangle.setGPSPower(true,"app");

View File

@ -1,6 +1,6 @@
{ "id": "run", { "id": "run",
"name": "Run", "name": "Run",
"version":"0.03", "version":"0.04",
"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

@ -1,44 +1,50 @@
(function(back) { (function(back) {
const SETTINGS_FILE = "run.json"; const SETTINGS_FILE = "run.json";
var ExStats = require("exstats");
// initialize with default settings... var statsList = ExStats.getList();
let s = { statsList.unshift({name:"-",id:""}); // add blank menu item
'use_gps': true, var statsIDs = statsList.map(s=>s.id);
'use_hrm': true
}
// ...and overwrite them with any saved values // ...and overwrite them with any saved values
// 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 = storage.readJSON(SETTINGS_FILE, 1) || {} let settings = Object.assign({
const saved = settings || {} B1 : "dist",
for (const key in saved) { B2 : "time",
s[key] = saved[key] B3 : "pacea",
} B4 : "bpm",
B5 : "step",
function save() { B6 : "caden",
settings = s paceLength : 1000
}, storage.readJSON(SETTINGS_FILE, 1) || {});
function saveSettings() {
storage.write(SETTINGS_FILE, settings) storage.write(SETTINGS_FILE, settings)
} }
E.showMenu({ function getBoxChooser(boxID) {
'': { 'title': 'Run' }, return {
'< Back': back, min :0, max: statsIDs.length-1,
'Use GPS': { value: Math.max(statsIDs.indexOf(settings[boxID]),0),
value: s.use_gps, format: v => statsList[v].name,
format: () => (s.use_gps ? 'Yes' : 'No'), onchange: v => {
onchange: () => { settings[boxID] = statsIDs[v];
s.use_gps = !s.use_gps; saveSettings();
save();
},
},
'Use HRM': {
value: s.use_hrm,
format: () => (s.use_hrm ? 'Yes' : 'No'),
onchange: () => {
s.use_hrm = !s.use_hrm;
save();
}, },
} }
}) }
var menu = {
'': { 'title': 'Run' },
'< Back': back
};
ExStats.appendMenuItems(menu, settings, saveSettings);
Object.assign(menu,{
'Box 1': getBoxChooser("B1"),
'Box 2': getBoxChooser("B2"),
'Box 3': getBoxChooser("B3"),
'Box 4': getBoxChooser("B4"),
'Box 5': getBoxChooser("B5"),
'Box 6': getBoxChooser("B6"),
});
E.showMenu(menu);
}) })

View File

@ -3,3 +3,4 @@
0.03: Advertise app name, added screenshots 0.03: Advertise app name, added screenshots
0.04: Advertise bar, GPS, HRM and mag services 0.04: Advertise bar, GPS, HRM and mag services
0.05: Refactored for efficiency, corrected sensor value inaccuracies 0.05: Refactored for efficiency, corrected sensor value inaccuracies
0.06: User settings are written to persistent storage, loaded on app start

View File

@ -5,7 +5,9 @@ Collect all the sensor data from the Bangle.js 2, display the live readings in m
## Usage ## Usage
The advertising packets will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the standard packet types. Also convenient for testing individual sensors of the Bangle.js 2 via the menu interface. The advertising packets will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the standard packet types. See our [Bangle.js Development Guide](https://reelyactive.github.io/diy/banglejs-dev/) for details. Also convenient for testing individual sensors of the Bangle.js 2 via the menu interface.
![SensiBLE in Pareto Anywhere](/BangleApps/apps/sensible/screenshot-pareto-anywhere.png)
## Features ## Features
@ -22,7 +24,7 @@ in the menu display, and broadcasts all sensor data readings _except_ accelerati
## Controls ## Controls
Browse and control sensors using the standard Espruino menu interface. Browse and control sensors using the standard Espruino menu interface. By default, all sensors _except_ the accelerometer are disabled. Sensors can be individually enabled/disabled via the menu. These settings are written to persistent storage (flash) and will be applied each time the SensiBLE app is loaded.
## Requests ## Requests

View File

@ -1,25 +1,28 @@
{ {
"id": "sensible", "id": "sensible",
"name": "SensiBLE", "name": "SensiBLE",
"shortName": "SensiBLE", "shortName": "SensiBLE",
"version": "0.05", "version": "0.06",
"description": "Collect, display and advertise real-time sensor data.", "description": "Collect, display and advertise real-time sensor data.",
"icon": "sensible.png", "icon": "sensible.png",
"screenshots": [ "screenshots": [
{ "url": "screenshot-top.png" }, { "url": "screenshot-top.png" },
{ "url": "screenshot-acc.png" }, { "url": "screenshot-acc.png" },
{ "url": "screenshot-bar.png" }, { "url": "screenshot-bar.png" },
{ "url": "screenshot-gps.png" }, { "url": "screenshot-gps.png" },
{ "url": "screenshot-hrm.png" }, { "url": "screenshot-hrm.png" },
{ "url": "screenshot-mag.png" } { "url": "screenshot-mag.png" }
], ],
"type": "app", "type": "app",
"tags": "tool,sensors,bluetooth", "tags": "tool,sensors,bluetooth",
"supports" : [ "BANGLEJS2" ], "supports" : [ "BANGLEJS2" ],
"allow_emulator": true, "allow_emulator": true,
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{ "name": "sensible.app.js", "url": "sensible.js" }, { "name": "sensible.app.js", "url": "sensible.js" },
{ "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true } { "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true }
] ],
"data": [
{ "name": "sensible.data.json", "url": "settings.json", "storageFile": true }
]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,5 +1,5 @@
/** /**
* Copyright reelyActive 2021 * Copyright reelyActive 2021-2022
* We believe in an open Internet of Things * We believe in an open Internet of Things
*/ */
@ -7,6 +7,8 @@
// Non-user-configurable constants // Non-user-configurable constants
const APP_ID = 'sensible'; const APP_ID = 'sensible';
const ESPRUINO_COMPANY_CODE = 0x0590; const ESPRUINO_COMPANY_CODE = 0x0590;
const SETTINGS_FILENAME = 'sensible.data.json';
const UPDATE_MILLISECONDS = 1000;
const APP_ADVERTISING_DATA = [ 0x12, 0xff, 0x90, 0x05, 0x7b, 0x6e, 0x61, 0x6d, const APP_ADVERTISING_DATA = [ 0x12, 0xff, 0x90, 0x05, 0x7b, 0x6e, 0x61, 0x6d,
0x65, 0x3a, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x62, 0x65, 0x3a, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x62,
0x6c, 0x65, 0x7d ]; 0x6c, 0x65, 0x7d ];
@ -19,16 +21,12 @@ let isBarMenu = false;
let isGpsMenu = false; let isGpsMenu = false;
let isHrmMenu = false; let isHrmMenu = false;
let isMagMenu = false; let isMagMenu = false;
let isBarEnabled = true;
let isGpsEnabled = true;
let isHrmEnabled = true;
let isMagEnabled = true;
let isNewAccData = false; let isNewAccData = false;
let isNewBarData = false; let isNewBarData = false;
let isNewGpsData = false; let isNewGpsData = false;
let isNewHrmData = false; let isNewHrmData = false;
let isNewMagData = false; let isNewMagData = false;
let settings = require('Storage').readJSON(SETTINGS_FILENAME);
// Menus // Menus
@ -51,9 +49,9 @@ let accMenu = {
let barMenu = { let barMenu = {
"": { "title" : "- Barometer -" }, "": { "title" : "- Barometer -" },
"State": { "State": {
value: isBarEnabled, value: settings.isBarEnabled,
format: v => v ? "On" : "Off", format: v => v ? "On" : "Off",
onchange: v => { isBarEnabled = v; Bangle.setBarometerPower(v, APP_ID); } onchange: v => { updateSetting('isBarEnabled', v); }
}, },
"Altitude": { value: null }, "Altitude": { value: null },
"Press": { value: null }, "Press": { value: null },
@ -63,9 +61,9 @@ let barMenu = {
let gpsMenu = { let gpsMenu = {
"": { "title" : "- GPS -" }, "": { "title" : "- GPS -" },
"State": { "State": {
value: isGpsEnabled, value: settings.isGpsEnabled,
format: v => v ? "On" : "Off", format: v => v ? "On" : "Off",
onchange: v => { isGpsEnabled = v; Bangle.setGPSPower(v, APP_ID); } onchange: v => { updateSetting('isGpsEnabled', v); }
}, },
"Lat": { value: null }, "Lat": { value: null },
"Lon": { value: null }, "Lon": { value: null },
@ -77,9 +75,9 @@ let gpsMenu = {
let hrmMenu = { let hrmMenu = {
"": { "title" : "- Heart Rate -" }, "": { "title" : "- Heart Rate -" },
"State": { "State": {
value: isHrmEnabled, value: settings.isHrmEnabled,
format: v => v ? "On" : "Off", format: v => v ? "On" : "Off",
onchange: v => { isHrmEnabled = v; Bangle.setHRMPower(v, APP_ID); } onchange: v => { updateSetting('isHrmEnabled', v); }
}, },
"BPM": { value: null }, "BPM": { value: null },
"Confidence": { value: null }, "Confidence": { value: null },
@ -88,9 +86,9 @@ let hrmMenu = {
let magMenu = { let magMenu = {
"": { "title" : "- Magnetometer -" }, "": { "title" : "- Magnetometer -" },
"State": { "State": {
value: isMagEnabled, value: settings.isMagEnabled,
format: v => v ? "On" : "Off", format: v => v ? "On" : "Off",
onchange: v => { isMagEnabled = v; Bangle.setCompassPower(v, APP_ID); } onchange: v => { updateSetting('isMagEnabled', v); }
}, },
"x": { value: null }, "x": { value: null },
"y": { value: null }, "y": { value: null },
@ -124,7 +122,7 @@ function transmitUpdatedSensorData() {
isNewMagData = false; isNewMagData = false;
} }
let interval = 1000 / data.length; let interval = UPDATE_MILLISECONDS / data.length;
NRF.setAdvertising(data, { showName: false, interval: interval }); NRF.setAdvertising(data, { showName: false, interval: interval });
} }
@ -190,6 +188,23 @@ function toByteArray(value, numberOfBytes, isSigned) {
} }
// Enable the sensors as per the current settings
function enableSensors() {
Bangle.setBarometerPower(settings.isBarEnabled, APP_ID);
Bangle.setGPSPower(settings.isGpsEnabled, APP_ID);
Bangle.setHRMPower(settings.isHrmEnabled, APP_ID);
Bangle.setCompassPower(settings.isMagEnabled, APP_ID);
}
// Update the given setting and write to persistent storage
function updateSetting(name, value) {
settings[name] = value;
require('Storage').writeJSON(SETTINGS_FILENAME, settings);
enableSensors();
}
// Update acceleration // Update acceleration
Bangle.on('accel', function(newAcc) { Bangle.on('accel', function(newAcc) {
acc = newAcc; acc = newAcc;
@ -260,9 +275,6 @@ Bangle.on('mag', function(newMag) {
// On start: enable sensors and display main menu // On start: enable sensors and display main menu
g.clear(); g.clear();
Bangle.setBarometerPower(isBarEnabled, APP_ID); enableSensors();
Bangle.setGPSPower(isGpsEnabled, APP_ID);
Bangle.setHRMPower(isHrmEnabled, APP_ID);
Bangle.setCompassPower(isMagEnabled, APP_ID);
E.showMenu(mainMenu); E.showMenu(mainMenu);
setInterval(transmitUpdatedSensorData, 1000); setInterval(transmitUpdatedSensorData, UPDATE_MILLISECONDS);

View File

@ -0,0 +1,6 @@
{
"isBarEnabled": false,
"isGpsEnabled": false,
"isHrmEnabled": false,
"isMagEnabled": false
}

View File

@ -4,3 +4,4 @@
0.04: Use queueDraw(), update every minute, respect theme, use Lato font 0.04: Use queueDraw(), update every minute, respect theme, use Lato font
0.05: Decided against custom font as it inceases the code size 0.05: Decided against custom font as it inceases the code size
minimalism is useful when narrowing down issues minimalism is useful when narrowing down issues
0.06: renamed some files

View File

@ -1,7 +1,7 @@
{ {
"id": "simplest", "id": "simplest",
"name": "Simplest Clock", "name": "Simplest Clock",
"version": "0.05", "version": "0.06",
"description": "The simplest working clock, acts as a tutorial piece", "description": "The simplest working clock, acts as a tutorial piece",
"icon": "simplest.png", "icon": "simplest.png",
"screenshots": [{"url":"screenshot_simplest.png"}], "screenshots": [{"url":"screenshot_simplest.png"}],
@ -9,7 +9,7 @@
"tags": "clock", "tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [
{"name":"simplest.app.js","url":"app.js"}, {"name":"simplest.app.js","url":"simplest.app.js"},
{"name":"simplest.img","url":"icon.js","evaluate":true} {"name":"simplest.img","url":"simplest.icon.js","evaluate":true}
] ]
} }

View File

@ -13,7 +13,6 @@ function draw() {
g.setFontAlign(0, 0); g.setFontAlign(0, 0);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.drawString(timeStr, w/2, h/2); g.drawString(timeStr, w/2, h/2);
queueDraw(); queueDraw();
} }
@ -42,13 +41,12 @@ Bangle.on('lcdPower',on=>{
g.clear(); g.clear();
// Show launcher when middle button pressed // Show launcher when middle button pressed
//Bangle.setUI("clock"); // Bangle.setUI("clock");
// use clockupdown as it tests for issue #1249 // use clockupdown as it tests for issue #1249
Bangle.setUI("clockupdown", btn=> { Bangle.setUI("clockupdown", btn=> {
draw(); draw();
}); });
// Load widgets // Load widgets
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Respect Quiet Mode 0.02: Respect Quiet Mode
0.03: Add compatibility for Bangle.js 2 and new firmware, added "Alarm at " for the alarm time

View File

@ -1,3 +1,4 @@
const BANGLEJS2 = process.env.HWVERSION == 2; //# check for bangle 2
const alarms = require("Storage").readJSON("alarm.json",1)||[]; const alarms = require("Storage").readJSON("alarm.json",1)||[];
const active = alarms.filter(a=>a.on); const active = alarms.filter(a=>a.on);
@ -52,21 +53,21 @@ active.forEach(alarm => {
} }
}); });
function drawString(s, x, y) { function drawString(s, y) { //# replaced x: always centered
g.clearRect(0,y-15,239,y+15); g.reset(); //# moved up to prevent blue background
g.reset(); g.clearRect(0, y - 12, 239, y + 8); //# minimized upper+lower clearing
g.setFont("Vector",20); g.setFont("Vector", 20);
g.setFontAlign(0,0); // align right bottom g.setFontAlign(0, 0); // align centered
g.drawString(s, x, y); g.drawString(s, g.getWidth() / 2, y); //# set x to center
} }
function drawApp() { function drawApp() {
g.clearRect(0,24,239,215); g.clearRect(0,24,239,215); //# no problem
var alarmHour = nextAlarm.getHours(); var alarmHour = nextAlarm.getHours();
var alarmMinute = nextAlarm.getMinutes(); var alarmMinute = nextAlarm.getMinutes();
if (alarmHour < 10) alarmHour = "0" + alarmHour; if (alarmHour < 10) alarmHour = "0" + alarmHour;
if (alarmMinute < 10) alarmMinute = "0" + alarmMinute; if (alarmMinute < 10) alarmMinute = "0" + alarmMinute;
const s = alarmHour + ":" + alarmMinute + "\n\n"; const s = "Alarm at " + alarmHour + ":" + alarmMinute + "\n\n"; //# make distinct to time
E.showMessage(s, "Sleep Phase Alarm"); E.showMessage(s, "Sleep Phase Alarm");
function drawTime() { function drawTime() {
@ -78,12 +79,20 @@ function drawApp() {
if (nowHour < 10) nowHour = "0" + nowHour; if (nowHour < 10) nowHour = "0" + nowHour;
if (nowMinute < 10) nowMinute = "0" + nowMinute; if (nowMinute < 10) nowMinute = "0" + nowMinute;
if (nowSecond < 10) nowSecond = "0" + nowSecond; if (nowSecond < 10) nowSecond = "0" + nowSecond;
const time = nowHour + ":" + nowMinute + ":" + nowSecond; const time = nowHour + ":" + nowMinute + (BANGLEJS2 ? "" : ":" + nowSecond); //# hide seconds on bangle 2
drawString(time, 120, 140); drawString(time, BANGLEJS2 ? 85 : 105); //# remove x, adjust height for bangle 2 an newer firmware
} }
} }
if (BANGLEJS2) {
drawTime();
setTimeout(_ => {
drawTime();
setInterval(drawTime, 60000);
}, 60000 - Date.now() % 60000); //# every new minute on bangle 2
} else {
setInterval(drawTime, 500); // 2Hz setInterval(drawTime, 500); // 2Hz
}
} }
var buzzCount = 19; var buzzCount = 19;
@ -104,8 +113,8 @@ function buzz() {
var minAlarm = new Date(); var minAlarm = new Date();
var measure = true; var measure = true;
if (nextAlarm !== undefined) { if (nextAlarm !== undefined) {
Bangle.loadWidgets(); //# correct widget load draw order
Bangle.drawWidgets(); Bangle.drawWidgets();
Bangle.loadWidgets();
// minimum alert 30 minutes early // minimum alert 30 minutes early
minAlarm.setTime(nextAlarm.getTime() - (30*60*1000)); minAlarm.setTime(nextAlarm.getTime() - (30*60*1000));
@ -116,7 +125,7 @@ if (nextAlarm !== undefined) {
if (swest !== undefined) { if (swest !== undefined) {
if (Bangle.isLCDOn()) { if (Bangle.isLCDOn()) {
drawString(swest ? "Sleep" : "Awake", 120, 180); drawString(swest ? "Sleep" : "Awake", BANGLEJS2 ? 150 : 180); //# remove x, adjust height
} }
} }
@ -133,6 +142,6 @@ if (nextAlarm !== undefined) {
E.showMessage('No Alarm'); E.showMessage('No Alarm');
setTimeout(load, 1000); setTimeout(load, 1000);
} }
// BTN2 to menu, BTN3 to main // BTN2 to menu, BTN3 to main # on bangle 2 only BTN to main
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); if (!BANGLEJS2) setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
setWatch(() => load(), BTN3, { repeat: false, edge: "falling" }); setWatch(() => load(), BANGLEJS2 ? BTN : BTN3, { repeat: false, edge: "falling" });

View File

@ -2,11 +2,11 @@
"id": "sleepphasealarm", "id": "sleepphasealarm",
"name": "SleepPhaseAlarm", "name": "SleepPhaseAlarm",
"shortName": "SleepPhaseAlarm", "shortName": "SleepPhaseAlarm",
"version": "0.02", "version": "0.03",
"description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
"icon": "app.png", "icon": "app.png",
"tags": "alarm", "tags": "alarm",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [
{"name":"sleepphasealarm.app.js","url":"app.js"}, {"name":"sleepphasealarm.app.js","url":"app.js"},
{"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true} {"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true}

View File

@ -1,3 +1,12 @@
0.01: Initial import. 0.01: Initial import.
0.02: Add swipe to change screens. 0.02: Misc development.
0.03: Misc memory and screen optimisations. 0.03: Enable screen off.
0.04: Vibrate once on no fix, twice on fix.
0.05: Add setting to turn vibrate on/off.
0.06: Tweaks to vibration settings.
0.07: Switch to BTN1 for Max toggle and reset function.
0.08: New features. Added waypoints file and distance to selected waypoint display. Added integration with GPS Setup module to switch GPS to low power mode when screen off. Save display settings and restore when app restarted.
0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight.
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
1.06: Misc memory and screen optimisations.
1.10: Adds Kalman filter.

View File

@ -2,7 +2,7 @@
"id": "speedalt2", "id": "speedalt2",
"name": "GPS Adventure Sports II", "name": "GPS Adventure Sports II",
"shortName":"GPS Adv Sport II", "shortName":"GPS Adv Sport II",
"version":"0.03", "version":"1.10",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -1 +1,2 @@
0.01: New Widget! 0.01: New Widget!
0.02: Now also visible on Bangle.js 2

View File

@ -2,8 +2,8 @@
"id": "widram", "id": "widram",
"name": "RAM Widget", "name": "RAM Widget",
"shortName": "RAM Widget", "shortName": "RAM Widget",
"version": "0.01", "version": "0.02",
"description": "Display your Bangle's available RAM percentage in a widget", "description": "Display your Bangle's RAM usage percentage in a widget",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",
"tags": "widget", "tags": "widget",

View File

@ -1,11 +1,15 @@
(() => { (() => {
function draw() { function draw() {
BANGLEJS2 = process.env.HWVERSION==2;
g.reset(); g.reset();
var m = process.memory(); var m = process.memory();
var pc = Math.round(m.usage*100/m.total); var percent = Math.round(m.usage*100/m.total);
g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4); g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4);
g.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff")); if (!BANGLEJS2)
g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/); g.setColor(percent>70 ? "#ff0000" : (percent>50 ? "#ffff00" : "#ffffff"));
else
g.setColor(percent>70 ? "#f00" : (percent>50 ? "#00f" : "#0f0"));
g.setFont("6x8").setFontAlign(0,0).drawString(percent+"%", this.x+12, this.y+20, true/*solid*/);
} }
var ramInterval; var ramInterval;
Bangle.on('lcdPower', function(on) { Bangle.on('lcdPower', function(on) {
@ -20,4 +24,4 @@
} }
}); });
WIDGETS["ram"]={area:"tl",width: 24,draw:draw}; WIDGETS["ram"]={area:"tl",width: 24,draw:draw};
})() })();

158
apps/wpmoto/README.md Normal file
View File

@ -0,0 +1,158 @@
# Waypointer Moto
Waypointer Moto is a GPS navigation aid intended to be attached to
the handlebars of a motorcycle.
It uses the GPS to find out which direction it's
travelling and shows the direction and distance to the destination
"as the crow flies". It gives you an indication of where to go,
but exploring and navigating the environment is left up to the user.
![](watch-on-bike.jpeg)
(Please note that it would be foolish in the extreme to rely on this
as your only navigation aid! Make sure you read this entire document
before using the app for navigation so that you know the drawbacks
and shortcomings.)
## App usage
### Main screen
![](screenshot.png)
The main screen shows the direction arrow, the distance to the waypoint,
and the name of the selected waypoint.
It also shows the status of the GPS fix in the colour of the arrow:
* Red: no GPS fix at all
* Yellow: GPS location, but no GPS course (probably you're moving too slowly);
in this case the direction of travel comes from the compass bearing instead
of the GPS course, but note that the compass is unreliable
* White: GPS fix includes both location and course, and the GPS course is used
to determine the direction of travel
### Select a waypoint
![](screenshot-menu.png)
Press the middle button (`BTN2`) to enter the menu, choose a waypoint using
the up/down arrows, and use the middle button again to select a waypoint and
return to the main screen.
### Add a waypoint
Press the middle button (`BTN2`) to enter the menu, and select the "+ Here"
option. This will add a waypoint named "WP*n*" marking your current location,
where "*n*" is the next unused number.
### Delete a waypoint
![](screenshot-delete.png)
Select a waypoint using the menu. Once the waypoint is selected and you're
back on the main screen, press either the top or bottom button (`BTN1` or
`BTN3`). Confirm that you want to delete the waypoint with the middle
button (`BTN2`).
## Waypoint editor
With the Bangle.js app loader connected to the watch, find the
Waypointer Moto app and click on the floppy disk icon:
![](floppy-disk.png)
This will load up the waypoint editor:
![](editor.png)
### Add a waypoint
Use the map to find your destination. Clicking on the map will
populate the latitude/longitude input boxes with the coordinates
of the point you clicked on. Type in a name for the waypoint and
click "Add Waypoint". Click "Upload" to send the updated list of
waypoints to the watch.
### Edit a waypoint
Click on the pencil icon next to the waypoint you wish to edit.
This will remove the waypoint from the list and populate the
input boxes.
Edit the coordinates by hand, or by clicking on the map. Edit
the name if you want. Click "Add Waypoint" to save the waypoint
back to the list. Click "Upload" to send the updated list of
waypoints to the watch.
### Delete a waypoint
Click on the pencil icon next to the waypoint you wish to edit.
This will remove the waypoint from the list.
Click "Upload" to send the updated list of waypoints to the watch.
## Mounting the watch on the bike
There is a 3d-printable "artificial wrist" which will fit over a 7/8"
handlebar and allow the watch strap to tighten up.
Alternatively, in a pinch you can strap the watch around a glove or a sponge
or anything else that will pad out the space so that the watch is a tight
fit.
The 3d-printed part should be a snug fit on the handlebar so that it does
not flop around. If it is too loose, line it with a layer or 2 of tape.
[Download the handlebar mount STL &raquo;](handlebar-mount.stl)
[Download the handlebar mount FreeCAD source &raquo;](handlebar-mount.FCStd)
![](handlebar-mount.png)
![](handlebar-mount.jpeg)
## Comparison to Way Pointer
Compared to the original Way Pointer app, Waypointer Moto:
* removes the numerical display of compass bearing
* makes the distance text bigger
* uses a higher-resolution arrow icon
* has a visual indication of the GPS status (the arrow colour)
* uses GPS course instead of compass bearing
* has OpenStreetMap integration in the waypoint editor
* uses Bangle.js menus to select waypoints instead of custom UI
* can add new waypoints from inside the app without requiring a blank slot
* can delete waypoints from inside the app without needing the PC
* still uses the same `waypoints.json` file
## Gotchas
Waypointer Moto derives your current heading from the GPS course
rather than the compass, whenever GPS course is available.
The compass bearing is based on the angle the watch is held, but
the GPS course is based on the direction it's *travelling*. If the
watch is not aligned with the direction of travel of the vehicle
then the arrow will not point in the correct direction.
When travelling too slowly, there is no GPS course information, so the
app reverts to using the compass (and draws it in yellow), but
the compass is not very reliable, and I
have especially found it not to be reliable when placed on a motorcyle,
maybe because of all the metal in the immediate vicinity. So if
the arrow is not drawn in white, then you should probably not trust
it. If you're not sure, just ride in a straight line until the arrow
turns white again.
## Possible Future Enhancements
- "routes" with multiple waypoints; automatically step from one
waypoint to the next when you get near to it
- some way to manually input coordinates directly on the watch
- make the text & arrow more legible in direct sunlight
- integrate a charging connector into the handlebar mount
- upstream the map integration to the other waypoint apps
## Acknowledgements
Waypointer Moto is a project by [James Stanley](https://incoherency.co.uk/). It is a derivative of [Adam Schmalhofer's](https://github.com/adamschmalhofer) Way Pointer app, which is in turn a derivative of
[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS
Navigation and Compass Navigation apps.

288
apps/wpmoto/app.js Normal file
View File

@ -0,0 +1,288 @@
var loc = require("locale");
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
var wp = waypoints[0];
var wp_bearing = 0;
var candraw = true;
var direction = 0;
var dist = 0;
var savedfix;
var previous = {
dst: '',
wp_name: '',
course: 180,
selected: false,
};
/*** Drawing ***/
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red
var pal_compass = pal_by;
var buf = Graphics.createArrayBuffer(160,160,1, {msb:true});
var arrow_img = require("heatshrink").decompress(atob("vF4wJC/AEMYBxs8Bxt+Bxv/BpkB/+ABxcD//ABxcH//gBxcP//wBxcf//4Bxc///8Bxd///+OxgABOxgABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBQNCPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBQNEPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBANGPAx2HPAx2HPAx2HPAx2HPAx2HPAx2HeJTeJB34O/B34O/B34O/B34O/B34O/B34O/B34O/B34OTAH4AT"));
function flip1(x,y,palette) {
g.drawImage({width:160,height:160,bpp:1,buffer:buf.buffer, palette:palette},x,y);
buf.clear();
}
function flip2_bw(x,y) {
g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bw},x,y);
buf.clear();
}
function flip2_bb(x,y) {
g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bb},x,y);
buf.clear();
}
function drawCompass(course) {
if (!candraw) return;
previous.course = course;
buf.setColor(1);
buf.fillCircle(80,80, 79);
buf.setColor(0);
buf.fillCircle(80,80, 69);
buf.setColor(1);
buf.drawImage(arrow_img, 80, 80, {rotate:radians(course)} );
var palette = pal_br;
if (savedfix !== undefined && savedfix.fix !== 0) palette = pal_compass;
flip1(40, 30, palette);
}
function drawN(force){
if (!candraw) return;
buf.setFont("Vector",24);
var dst = loc.distance(dist);
// distance on left
if (force || previous.dst !== dst) {
previous.dst = dst;
buf.setColor(1);
buf.setFontAlign(-1, -1);
buf.setFont("Vector",40);
buf.drawString(dst,0,0);
flip2_bw(8, 200);
}
// waypoint name on right
if (force || previous.wp_name !== wp.name) {
previous.wp_name = wp.name;
buf.setColor(1);
buf.setFontAlign(1, -1);
buf.setFont("Vector", 15);
buf.drawString(wp.name, 80, 0);
flip2_bw(160, 220);
}
}
function drawAll(force) {
if (!candraw) return;
g.setColor(1,1,1);
drawN(force);
drawCompass(direction);
}
/*** Heading ***/
var heading = 0;
function newHeading(m,h){
var s = Math.abs(m - h);
var delta = (m>h)?1:-1;
if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360;
if (hd>360)hd-= 360;
return hd;
}
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function tiltfixread(O,S){
var start = Date.now();
var m = Bangle.getCompass();
var g = Bangle.getAccel();
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
if (d<0) d+=360;
var phi = Math.atan(-g.x/-g.z);
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
var yh = m.dz*sinphi - m.dx*cosphi;
var psi = Math.atan2(yh,xh)*180/Math.PI;
if (psi<0) psi+=360;
return psi;
}
function read_heading() {
if (savedfix !== undefined && !isNaN(savedfix.course)) {
Bangle.setCompassPower(0);
heading = savedfix.course;
pal_compass = pal_bw;
} else {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
Bangle.setCompassPower(1);
heading = newHeading(d,heading);
pal_compass = pal_by;
}
direction = wp_bearing - heading;
if (direction < 0) direction += 360;
if (direction > 360) direction -= 360;
drawCompass(direction);
}
/*** Maths ***/
function radians(a) {
return a*Math.PI/180;
}
function degrees(a) {
var d = a*180/Math.PI;
return (d+360)%360;
}
function bearing(a,b){
var delta = radians(b.lon-a.lon);
var alat = radians(a.lat);
var blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) - Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
}
function distance(a,b){
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
}
/*** Waypoints ***/
function addCurrentWaypoint() {
var wpnum = 0;
var ok = false;
// XXX: O(n^2) search for lowest unused WP number
while (!ok) {
ok = true;
for (var i = 0; i < waypoints.length && ok; i++) {
if (waypoints[i].name == ("WP"+wpnum)) {
wpnum++;
ok = false;
}
}
}
waypoints.push({
name: "WP" + wpnum,
lat: savedfix.lat,
lon: savedfix.lon,
});
wp = waypoints[waypoints.length-1];
saveWaypoints();
}
function saveWaypoints() {
require("Storage").writeJSON("waypoints.json", waypoints);
}
function deleteWaypoint(w) {
for (var i = 0; i < waypoints.length; i++) {
if (waypoints[i] == w) {
waypoints.splice(i, 1);
saveWaypoints();
wp = {name:"NONE"};
}
}
}
/*** Setup ***/
function onGPS(fix) {
savedfix = fix;
if (fix !== undefined && fix.fix == 1){
dist = distance(fix, wp);
if (isNaN(dist)) dist = 0;
wp_bearing = bearing(fix, wp);
if (isNaN(wp_bearing)) wp_bearing = 0;
drawN();
}
}
function startTimers() {
setInterval(function() {
Bangle.setLCDPower(1);
read_heading();
}, 500);
}
function addWaypointToMenu(menu, i) {
menu[waypoints[i].name] = function() {
wp = waypoints[i];
mainScreen();
};
}
function mainScreen() {
E.showMenu();
candraw = true;
drawAll(true);
Bangle.setUI("updown", function(v) {
if (v === undefined) {
candraw = false;
var menu = {
"": { "title": "-- Waypoints --" },
};
for (let i = 0; i < waypoints.length; i++) {
addWaypointToMenu(menu, i);
}
menu["+ Here"] = function() {
addCurrentWaypoint();
mainScreen();
};
menu["< Back"] = mainScreen;
E.showMenu(menu);
} else {
candraw = false;
E.showPrompt("Delete waypoint: " + wp.name + "?").then(function(confirmed) {
var name = wp.name;
if (confirmed) {
deleteWaypoint(wp);
E.showAlert("Waypoint deleted: " + name).then(mainScreen);
} else {
mainScreen();
}
});
}
});
}
Bangle.on('kill',()=>{
Bangle.setCompassPower(0);
Bangle.setGPSPower(0);
});
g.clear();
Bangle.setLCDBrightness(1);
Bangle.setGPSPower(1);
startTimers();
Bangle.on('GPS', onGPS);
mainScreen();

BIN
apps/wpmoto/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
apps/wpmoto/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
apps/wpmoto/floppy-disk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

1
apps/wpmoto/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4kA///tVK/feuekkEh1dSnnn5P2imlgdr221vvv0E5x9z8dqoEpMf4AqgMQCysRiIYUgMd6IXVqc1iIXXAAo2NiIXBivdAAfVGwxwCAAgXBicmswACsVRGAsRqoAEqIXC0QXDs0lGAkBisrBomBC4WqGAloMIkRqW72wNDlvTC4QwEwIvDLoOr3e7KwkzmsdmczxAABwaQFF4QwEC4SAGR4pfBGAO2kQABlQXIX4wlHL4LQNX5IXNDw4XygdZ96MFF58NvP8C6Mzu93uF9+oXQrQWBAAPU/5kPgoWDu+UoIQJ7vR6IGCm4XEz9MC5HMAAvHC4mc4gWHk93zPsC5F58oWHgQOE//8//M//+5mf9vAFxFwOQef5nu9wbB5nOiNQBoInCCYMCk8FjnM7qkB5lEp3//udRoKlBuueDwMyC4MojnJz2dmEA/1EovDn3d6nEiEHv3pzOc4oXB1nJ0Wo7JVBjnE/td6IxBgtQu/u4ci1vs6EAj+S3eynvBgEP92Z/+XzIXBiF59GOte27goB5OrlGDlWcMAP3SgJvBusBgF/61sycrlPFgvJsWZzMrziGB84XDvlBg+excp9N73P1gbQB5//+lEiMAi93vwaBz4XC1ep5OS2f9gPJlWZ93d+lBqNfzn//N5+lAg+cF4OZF4UA52LlWi3RfBSANEAAVBgEHz+W3VzlepVANczW73ep+L4CgNTnrfBAAPc8drUAOcR4MB/PYwefAwIXCjvRdgIABgvO7Ui1PMX4MA5n+96+BC4cRiIXDKAPOzPvIwIABhsc4oPEDAIGFhqhB4K2BACY+EAH4AG"))

18
apps/wpmoto/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"id": "wpmoto",
"name": "Waypointer Moto",
"shortName": "Waypointer Moto",
"version": "0.01",
"description": "Waypoint-based motorcycle navigation aid",
"icon": "wpmoto.png",
"tags": "tool,outdoors,gps",
"supports": ["BANGLEJS"],
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}],
"readme": "README.md",
"interface": "wpmoto.html",
"storage": [
{"name":"wpmoto.app.js","url":"app.js"},
{"name":"wpmoto.img","url":"icon.js","evaluate":true}
],
"data": [{"name":"waypoints.json","url":"waypoints.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
apps/wpmoto/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -0,0 +1,5 @@
[
{
"name":"NONE"
},
]

198
apps/wpmoto/wpmoto.html Normal file
View File

@ -0,0 +1,198 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/css/ol.css" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/ol-geocoder@latest/dist/ol-geocoder.min.css" rel="stylesheet">
</head>
<body>
<h4>List of waypoints</h4>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Lat.</th>
<th>Long.</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="waypoints">
</tbody>
</table>
<br>
<h4>Add a new waypoint</h4>
<form id="add_waypoint_form">
<div class="columns">
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" type="text" id="add_waypoint_name" placeholder="Name">
</div>
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_latitude" placeholder="Lat">
</div>
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_longitude" placeholder="Long">
</div>
</div>
<div class="columns">
<div class="column col-3 col-xs-8">
<button id="add_waypoint_button" class="btn btn-primary btn-sm">Add Waypoint</button>
</div>
</div>
</form>
<br>
<button id="Download" class="btn btn-error">Reload</button> <button id="Upload" class="btn btn-primary">Upload</button>
<br>
<div id="map" class="map" style="width:100%; height:400px"></div>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/build/ol.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ol-geocoder"></script>
<script src="../../core/lib/interface.js"></script>
<script>
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
var geocoder = new Geocoder('nominatim', {
provider: 'osm',
lang: 'en-GB',
placeholder: 'Search...',
targetType: 'text-input',
});
map.addControl(geocoder);
geocoder.on('addresschosen', function(e) {
map.getView().animate({
center: e.coordinate,
zoom: Math.max(map.getView().getZoom(),16)
});
var lonlat = ol.proj.toLonLat(e.coordinate);
$longitude.value = lonlat[0];
$latitude.value = lonlat[1];
});
var waypoints = []
var $name = document.getElementById('add_waypoint_name')
var $form = document.getElementById('add_waypoint_form')
var $button = document.getElementById('add_waypoint_button')
var $latitude = document.getElementById('add_latitude')
var $longitude = document.getElementById('add_longitude')
var $list = document.getElementById('waypoints')
map.on('click', function(e) {
var lonlat = ol.proj.toLonLat(e.coordinate);
$longitude.value = lonlat[0];
$latitude.value = lonlat[1];
});
function compare(a, b){
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
if (x=="none") {return -1};
if (y=="none") {return 1};
if (x < y) {return -1;}
if (x > y) {return 1;}
return 0;
}
$button.addEventListener('click', event => {
event.preventDefault()
var name = $name.value.trim()
if(!name) return;
var lat = parseFloat($latitude.value).toPrecision(8);
var lon = parseFloat($longitude.value).toPrecision(8);
waypoints.push({
name, lat,lon,
});
waypoints.sort(compare);
renderWaypoints()
$name.value = ''
$latitude.value = (0).toPrecision(8);
$longitude.value = (0).toPrecision(8);
});
function removeWaypoint(index){
$name.value = waypoints[index].name
if (waypoints[index].lat !== undefined && waypoints[index].lon !== undefined
&& !isNaN(waypoints[index].lat) && !isNaN(waypoints[index].lon)) {
$latitude.value = waypoints[index].lat
$longitude.value = waypoints[index].lon
map.getView().animate({
center: ol.proj.fromLonLat([waypoints[index].lon, waypoints[index].lat]),
zoom: Math.max(map.getView().getZoom(),16)
});
}
waypoints = waypoints.filter((p,i) => i!==index)
renderWaypoints()
}
function renderWaypoints(){
$list.innerHTML = ''
waypoints.forEach((waypoint,index) => {
var $waypoint = document.createElement('tr')
if (index==0){
$waypoint.innerHTML = `<td>${waypoint.name}</td>`
} else if(waypoint.lat==undefined){
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>------</td><td>-----</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
} else {
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>${waypoint.lat}</td><td>${waypoint.lon}</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
}
$list.appendChild($waypoint)
})
$name.focus()
}
function downloadJSONfile(fileid, callback) {
Puck.write(`\x10(function() {
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
Bluetooth.print(JSON.stringify(pts));
})()\n`,contents=>{
var storedpts = JSON.parse(contents);
callback(storedpts);
});
}
function uploadFile(fileid, contents) {
Puck.write(`\x10(function() {
require("Storage").write("${fileid}",'${contents}');
Bluetooth.print("OK");
})()\n`,ret=>{
console.log("uploadFile",ret);
});
}
function gotStored(pts){
waypoints = pts;
renderWaypoints();
}
function onInit() {
downloadJSONfile("waypoints.json", gotStored);
}
document.getElementById("Download").addEventListener("click", function() {
downloadJSONfile("waypoints.json", gotStored);
});
document.getElementById("Upload").addEventListener("click", function() {
var data = JSON.stringify(waypoints);
uploadFile("waypoints.json",data);
});
</script>
</body>
</html>

BIN
apps/wpmoto/wpmoto.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -37,7 +37,7 @@ for app in apps/*/; do
done done
echo "]" >> "$outfile" echo "]" >> "$outfile"
if [ -z "$1"]; then if [ -z "$1" ]; then
# Running with no arguments: prevent accidental commit of modified apps.json. # Running with no arguments: prevent accidental commit of modified apps.json.
# You can use `create_apps.json.sh apps.json` if you really want to both # You can use `create_apps.json.sh apps.json` if you really want to both
# overwrite and still commit apps.json # overwrite and still commit apps.json

View File

@ -57,7 +57,7 @@ function fileGetter(url) {
Promise.all(APPS.map(appid => { Promise.all(APPS.map(appid => {
try { try {
var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "metadata.json").toString()); var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "/metadata.json").toString());
} catch (e) { } catch (e) {
throw new Error(`App ${appid} not found`); throw new Error(`App ${appid} not found`);
} }
@ -77,6 +77,7 @@ Promise.all(APPS.map(appid => {
js += `\x10if (E.CRC32(require('Storage').read(${JSON.stringify(file.name)}))!=${file.crc}){console.log("${file.name} invalid");FAIL++}\n`; js += `\x10if (E.CRC32(require('Storage').read(${JSON.stringify(file.name)}))!=${file.crc}){console.log("${file.name} invalid");FAIL++}\n`;
}*/ }*/
}); });
// js = js.replace(/\x10/g,""); // remove the echo-off characters (for testing only)
fs.writeFileSync(OUTFILE, js); fs.writeFileSync(OUTFILE, js);
console.log("Output written to "+OUTFILE); console.log("Output written to "+OUTFILE);
}); });

View File

@ -133,7 +133,7 @@ function evaluateFile(file) {
Promise.all(APPS.map(appid => { Promise.all(APPS.map(appid => {
try { try {
var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "metadata.json").toString()); var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "/metadata.json").toString());
} catch (e) { } catch (e) {
throw new Error(`App ${appid} not found`); throw new Error(`App ${appid} not found`);
} }

2
core

@ -1 +1 @@
Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b Subproject commit f97a128a28b409c576f66c63c87905f26a5cfd8b

View File

@ -51,8 +51,8 @@
<li class="tab-item" id="tab-myappscontainer"> <li class="tab-item" id="tab-myappscontainer">
<a href="javascript:showTab('myappscontainer')">My Apps</a> <a href="javascript:showTab('myappscontainer')">My Apps</a>
</li> </li>
<li class="tab-item" id="tab-aboutcontainer"> <li class="tab-item" id="tab-morecontainer">
<a href="javascript:showTab('aboutcontainer')">About</a> <a href="javascript:showTab('morecontainer')">More...</a>
</li> </li>
</ul> </ul>
@ -111,7 +111,7 @@
</div> </div>
</div> </div>
<div class="container apploader-tab" id="aboutcontainer" style="display:none"> <div class="container apploader-tab" id="morecontainer" style="display:none">
<div class="hero bg-gray"> <div class="hero bg-gray">
<div class="hero-body"> <div class="hero-body">
<a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a> <a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a>

View File

@ -32,12 +32,13 @@ DEVICEINFO = DEVICEINFO.filter(x=>x.id.startsWith("BANGLEJS"));
// When a device is found, filter the apps accordingly // When a device is found, filter the apps accordingly
function onFoundDeviceInfo(deviceId, deviceVersion) { function onFoundDeviceInfo(deviceId, deviceVersion) {
var fwURL = "#"; var fwURL = "#", fwExtraText = "";
if (deviceId == "BANGLEJS") { if (deviceId == "BANGLEJS") {
fwURL = "https://www.espruino.com/Bangle.js#firmware-updates"; fwURL = "https://www.espruino.com/Bangle.js#firmware-updates";
Const.MESSAGE_RELOAD = 'Hold BTN3\nto reload'; Const.MESSAGE_RELOAD = 'Hold BTN3\nto reload';
} }
if (deviceId == "BANGLEJS2") { if (deviceId == "BANGLEJS2") {
fwExtraText = "with the <b>Firmware Update</b> app in this App Loader, or "
fwURL = "https://www.espruino.com/Bangle.js2#firmware-updates"; fwURL = "https://www.espruino.com/Bangle.js2#firmware-updates";
Const.MESSAGE_RELOAD = 'Hold button\nto reload'; Const.MESSAGE_RELOAD = 'Hold button\nto reload';
} }
@ -45,7 +46,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) {
if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") {
showToast(`You're using ${deviceId}, not a Bangle.js. Did you want <a href="https://espruino.com/apps">espruino.com/apps</a> instead?` ,"warning", 20000); showToast(`You're using ${deviceId}, not a Bangle.js. Did you want <a href="https://espruino.com/apps">espruino.com/apps</a> instead?` ,"warning", 20000);
} else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) {
showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (<a href="http://www.espruino.com/ChangeLog" target="_blank">see changes</a>). You can <a href="${fwURL}" target="_blank">update with the instructions here</a>` ,"warning", 20000); showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (<a href="http://www.espruino.com/ChangeLog" target="_blank">see changes</a>). You can update ${fwExtraText}<a href="${fwURL}" target="_blank">with the instructions here</a>` ,"warning", 20000);
} }

View File

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

View File

@ -1,9 +1,54 @@
App Modules App Modules
=========== ===========
These are modules used by apps - you can use them with: These are modules used by apps - you can use them from a Bangle.js app with:
``` ```
var testmodule = require("testmodule"); var testmodule = require("testmodule");
testmodule.test() testmodule.test()
``` ```
Development
-----------
When apps that use these modules are uploaded via the
app loader, the module is automatically included in the app's source. However
when developing via the IDE the module won't get pulled in by default.
To fix this you have three options:
### Host your own App Loader and upload from that
This is reasonably easy to set up, but it's more difficult to make changes and upload:
* Follow the steps here to set up your own App Loader: https://www.espruino.com/Bangle.js+App+Loader
* Make changes to that repository
* Refresh and upload your app from the app loader (you can have the IDE connected
at the same time so you can see any error messages)
### Upload the module to the Bangle's internal storage
This allows you to develop both the app and module very quickly, but the app is
uploaded in a slightly different way to what you'd get when you use the App Loader
or the method below:
* Load the module's source file in the Web IDE
* Click the down-arrow below the upload button, then `Storage`
* Click `New File`, type `your_module_name` as the name (with no `.js` extension), click `Ok`
* Now Click the `Upload` icon.
You can now upload the app direct from the IDE. You can even leave a second Web IDE window open
(one for the app, one for the module) to allow you to change the module.
### Change the Web IDE search path to include Bangle.js modules
This is nice and easy (and the results are the same as if the app was
uploaded via the app loader), however you cannot then make/test changes
to the module.
* In the IDE, Click the `Settings` icon in the top right
* Click `Communications` and scroll down to `Module URL`
* Now change the module URL from the default of `https://www.espruino.com/modules`
to `https://banglejs.com/apps/modules|https://www.espruino.com/modules`
The next time you upload your app, the module will automatically be included.

View File

@ -1,6 +1,8 @@
/* Copyright (c) 2020 OmegaRogue. See the file LICENSE for copying permission. */ /* Copyright (c) 2020 OmegaRogue. See the file LICENSE for copying permission. */
/* /*
Graphics Functions based on the React Sci-Fi UI Framework Arwes Graphics Functions based on the React Sci-Fi UI Framework Arwes
Take a look at README.md for hints on developing with this library.
*/ */
var C = { var C = {

262
modules/exstats.js Normal file
View File

@ -0,0 +1,262 @@
/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */
/* Exercise Stats module
Take a look at README.md for hints on developing with this library.
Usage
-----
var ExStats = require("exstats");
// Get a list of available types of run statistic
print(ExStats.getList());
// returns list of available stat IDs like
[
{name: "Time", id:"time"},
{name: "Distance", id:"dist"},
{name: "Steps", id:"step"},
{name: "Heart (BPM)", id:"bpm"},
{name: "Pace (avr)", id:"pacea"},
{name: "Pace (current)", id:"pacec"},
{name: "Cadence", id:"caden"},
]
// Setup and load all statistic types
var exs = ExStats.getStats(["dist", "time", "pacea","bpm","step","caden"], options);
// exs contains
{
stats : { time : {
id : "time"
title : "Time" // title to use when rendering
getValue : function // get a floating point value for this stat
getString : function // get a formatted string for this stat
// also fires a 'changed' event
},
dist : { ... },
pacea : { ... },
...
},
state : { active : bool,
.. other internal-ish state info
},
start : function, // call to start exercise and reset state
stop : function, // call to stop exercise
}
/// Or you can display a menu where the settings can be configured - these are passed as the 'options' argument of getStats
var menu = { ... };
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
E.showMenu(menu);
*/
var state = {
active : false, // are we working or not?
// startTime, // time exercise started
lastGPS:{}, thisGPS:{}, // This & previous GPS readings
// distance : 0, ///< distance in meters
// avrSpeed : 0, ///< in m/sec
startSteps : Bangle.getStepCount(), ///< number of steps when we started
lastSteps : Bangle.getStepCount(), // last time 'step' was called
stepHistory : new Uint8Array(60), // steps each second for the last minute (0 = current minute)
// stepsInMinute // steps over the last minute
// cadence // steps per minute adjusted if <1 minute
// BPM // beats per minute
// BPMage // how many seconds was BPM set?
};
// 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
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 y = radians(b.lat-a.lat);
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
}
// Given milliseconds, return a time
function formatTime(ms) {
let hrs = Math.floor(ms/3600000).toString();
let mins = (Math.floor(ms/60000)%60).toString();
let secs = (Math.floor(ms/1000)%60).toString();
if (hrs === '0')
return mins.padStart(2,0)+":"+secs.padStart(2,0);
else
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours
}
// Format speed in meters/second, paceLength=length in m for pace over
function formatPace(speed, paceLength) {
if (speed < 0.1667) {
return `__:__`;
}
const pace = Math.round(paceLength / speed); // seconds for paceLength (1000=1km)
const min = Math.floor(pace / 60); // minutes for paceLength
const sec = pace % 60;
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
Bangle.on("GPS", function(fix) {
if (!fix.fix) return; // only process actual fixes
if (!state.active) return;
if( state.lastGPS.fix)
state.distance += calcDistance(state.lastGPS, fix);
var duration = Date.now() - state.startTime; // in ms
state.avrSpeed = state.distance * 1000 / duration; // meters/sec
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
state.lastGPS = state.thisGPS;
state.thisGPS = fix;
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
});
Bangle.on("step", function(steps) {
if (!state.active) return;
if (stats["step"]) stats["step"].emit("changed",stats["step"]);
state.lastStepCount = steps;
});
Bangle.on("HRM", function(h) {
if (h.confidence>=60) {
state.BPM = h.bpm;
state.BPMage = 0;
stats["bpm"].emit("changed",stats["bpm"]);
}
});
/** Get list of available statistic types */
exports.getList = function() {
return [
{name: "Time", id:"time"},
{name: "Distance", id:"dist"},
{name: "Steps", id:"step"},
{name: "Heart (BPM)", id:"bpm"},
{name: "Pace (avr)", id:"pacea"},
{name: "Pace (current)", id:"pacec"},
{name: "Cadence", id:"caden"},
];
};
/** Instatiate the given list of statistic IDs (see comments at top)
options = {
paceLength : meters to measure pace over
}
*/
exports.getStats = function(statIDs, options) {
options = options||{};
options.paceLength = options.paceLength||1000;
var needGPS,needHRM;
// ======================
if (statIDs.includes("time")) {
stats["time"]={
title : "Time",
getValue : function() { return Date.now()-state.startTime; },
getString : function() { return formatTime(this.getValue()) },
};
};
if (statIDs.includes("dist")) {
needGPS = true;
stats["dist"]={
title : "Dist",
getValue : function() { return state.distance; },
getString : function() { return require("locale").distance(state.distance); },
};
}
if (statIDs.includes("step")) {
stats["step"]={
title : "Steps",
getValue : function() { return Bangle.getStepCount() - state.startSteps; },
getString : function() { return this.getValue().toString() },
};
}
if (statIDs.includes("bpm")) {
needHRM = true;
stats["bpm"]={
title : "BPM",
getValue : function() { return state.BPM; },
getString : function() { return state.BPM||"--" },
};
}
if (statIDs.includes("pacea")) {
needGPS = true;
stats["pacea"]={
title : "Pace(avr)",
getValue : function() { return state.avrSpeed; }, // in m/sec
getString : function() { return formatPace(state.avrSpeed, options.paceLength); },
};
}
if (statIDs.includes("pacec")) {
needGPS = true;
stats["pacec"]={
title : "Pace(now)",
getValue : function() { return (state.thisGPS.speed||0)/3.6; }, // in m/sec
getString : function() { return formatPace(this.getValue(), options.paceLength); },
};
}
if (statIDs.includes("caden")) {
needGPS = true;
stats["caden"]={
title : "Cadence",
getValue : function() { return state.stepsPerMin; },
getString : function() { return state.stepsPerMin; },
};
}
// ======================
for (var i in stats) stats[i].id=i; // set up ID field
if (needGPS) Bangle.setGPSPower(true,"exs");
if (needHRM) Bangle.setHRMPower(true,"exs");
setInterval(function() { // run once a second....
if (!state.active) return;
// called once a second
var duration = Date.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"]);
// move step history onwards
state.stepHistory.set(state.stepHistory,1);
state.stepHistory[0]=0;
if (stats["time"]) stats["time"].emit("changed",stats["time"]);
// update BPM - if nothing valid in 60s remove the reading
state.BPMage++;
if (state.BPM && state.BPMage>60) {
state.BPM = 0;
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
}
}, 1000);
function reset() {
state.startTime = Date.now();
state.startSteps = state.lastSteps = Bangle.getStepCount();
state.lastSteps = 0;
state.stepHistory.fill(0);
state.stepsPerMin = 0;
state.distance = 0;
state.avrSpeed = 0;
state.BPM = 0;
state.BPMage = 0;
}
reset();
return {
stats : stats, state : state,
start : function() {
state.active = true;
reset();
},
stop : function() {
state.active = false;
}
};
};
exports.appendMenuItems = function(menu, settings, saveSettings) {
var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",];
var paceAmts = [1000,1609,21098,42195];
menu['Pace'] = {
min :0, max: paceNames.length-1,
value: Math.max(paceAmts.indexOf(settings.paceLength),0),
format: v => paceNames[v],
onchange: v => {
settings.paceLength = paceAmts[v];
saveSettings();
},
};
};