Merge remote-tracking branch 'upstream/master'
|
@ -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/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
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Faster maze generation
|
||||
0.03: Avoid clearing bottom widgets
|
||||
|
|
|
@ -11,13 +11,10 @@ function Maze(n) {
|
|||
this.margin = Math.floor((g.getHeight()-this.total_length)/2);
|
||||
this.ball_x = 0;
|
||||
this.ball_y = 0;
|
||||
this.clearScreen = function() {
|
||||
g.clearRect(
|
||||
0, this.margin,
|
||||
g.getWidth(), this.margin+this.total_length
|
||||
);
|
||||
};
|
||||
this.clearScreen();
|
||||
// This voodoo is needed because otherwise
|
||||
// bottom line widgets (like digital clock)
|
||||
// disappear during maze generation
|
||||
Bangle.drawWidgets();
|
||||
g.setColor(g.theme.fg);
|
||||
for (let i=0; i<=n; i++) {
|
||||
g.drawRect(
|
||||
|
@ -66,7 +63,7 @@ function Maze(n) {
|
|||
if (Math.random()<0.5 && candidates_down.length || !candidates_right.length) {
|
||||
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),
|
||||
cell = candidates.splice(candidate_index, 1)[0],
|
||||
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) {
|
||||
if (!r && !c) {
|
||||
g.setColor("#ffff00");
|
||||
|
@ -263,7 +255,7 @@ let mazeMenu = {
|
|||
"< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock
|
||||
};
|
||||
|
||||
g.clear(true);
|
||||
g.reset();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
Bangle.setLocked(false);
|
||||
|
@ -289,7 +281,7 @@ let maze_interval = setInterval(
|
|||
duration = Date.now()-start_time;
|
||||
g.setFontAlign(0,0).setColor(g.theme.fg);
|
||||
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);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "acmaze",
|
||||
"name": "AccelaMaze",
|
||||
"shortName":"AccelaMaze",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "Tilt the watch to roll a ball through a maze.",
|
||||
"icon": "app.png",
|
||||
"tags": "game",
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
0.03: Prevent readings from internal sensor mixing into BT values
|
||||
Mark events with src property
|
||||
Show actual source of event in app
|
||||
0.04: Automatically reconnect BT sensor
|
||||
App buzzes if no BTHRM events for more than 3 seconds
|
||||
|
|
|
@ -1,13 +1,28 @@
|
|||
(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 status;
|
||||
var currentRetryTimeout;
|
||||
var initialRetryTime = 40;
|
||||
var maxRetryTime = 60000;
|
||||
var retryTime = initialRetryTime;
|
||||
|
||||
var origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
Bangle.isBTHRMOn = function(){
|
||||
return (status=="searching" || status=="connecting") || (gatt!==undefined);
|
||||
}
|
||||
return (gatt!==undefined && gatt.connected);
|
||||
};
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
|
@ -18,16 +33,135 @@
|
|||
return 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) {
|
||||
|
||||
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
|
||||
// Do app power handling
|
||||
if (!app) app="?";
|
||||
log("setBTHRMPower ->", isOn, app);
|
||||
if (Bangle._PWR===undefined) Bangle._PWR={};
|
||||
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
|
||||
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
|
||||
|
@ -35,63 +169,19 @@
|
|||
isOn = Bangle._PWR.BTHRM.length;
|
||||
// so now we know if we're really on
|
||||
if (isOn) {
|
||||
log("setBTHRMPower on", app);
|
||||
if (!Bangle.isBTHRMOn()) {
|
||||
log("BTHRM not already on");
|
||||
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";
|
||||
});
|
||||
initBt();
|
||||
}
|
||||
} else { // not on
|
||||
log("setBTHRMPower off", app);
|
||||
log("Power off for " + app);
|
||||
if (gatt) {
|
||||
log("BTHRM connected - disconnecting");
|
||||
status = undefined;
|
||||
try {gatt.disconnect();}catch(e) {
|
||||
log("BTHRM disconnect error", e);
|
||||
try {
|
||||
log("Disconnect with gatt: ", gatt);
|
||||
gatt.disconnect();
|
||||
} catch(e) {
|
||||
log("Error during disconnect", e);
|
||||
}
|
||||
blockInit = false;
|
||||
gatt = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -100,24 +190,29 @@
|
|||
var origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
Bangle.setHRMPower = function(isOn, app) {
|
||||
log("setHRMPower for " + app + ":" + (isOn?"on":"off"));
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
if (settings.enabled || !isOn){
|
||||
log("Enable BTHRM power");
|
||||
Bangle.setBTHRMPower(isOn, app);
|
||||
}
|
||||
if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){
|
||||
log("Enable HRM power");
|
||||
origSetHRMPower(isOn, app);
|
||||
}
|
||||
}
|
||||
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
if (settings.enabled && settings.replace){
|
||||
log("Replace HRM event");
|
||||
if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){
|
||||
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
|
||||
var app = Bangle._PWR.HRM[i];
|
||||
log("Moving app " + app);
|
||||
origSetHRMPower(0, app);
|
||||
Bangle.setBTHRMPower(1, app);
|
||||
if (Bangle._PWR.HRM===undefined) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -10,7 +10,9 @@ function draw(y, event, type, counter) {
|
|||
g.reset();
|
||||
g.setFontAlign(0,0);
|
||||
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 + "";
|
||||
g.setFontVector(40).drawString(str,px,y+20);
|
||||
str = "Confidence: " + event.confidence;
|
||||
|
@ -21,21 +23,27 @@ function draw(y, event, type, counter) {
|
|||
}
|
||||
|
||||
function onBtHrm(e) {
|
||||
print("Event for BT " + JSON.stringify(e));
|
||||
counterBt += 5;
|
||||
//print("Event for BT " + JSON.stringify(e));
|
||||
if (e.bpm == 0){
|
||||
Bangle.buzz(100,0.2);
|
||||
}
|
||||
if (counterBt == 0){
|
||||
Bangle.buzz(200,0.5);
|
||||
}
|
||||
counterBt += 3;
|
||||
eventBt = e;
|
||||
}
|
||||
|
||||
function onHrm(e) {
|
||||
print("Event for Int " + JSON.stringify(e));
|
||||
counterInt += 5;
|
||||
//print("Event for Int " + JSON.stringify(e));
|
||||
counterInt += 3;
|
||||
eventInt = e;
|
||||
}
|
||||
|
||||
Bangle.on('BTHRM', onBtHrm);
|
||||
Bangle.on('HRM', onHrm);
|
||||
|
||||
Bangle.setHRMPower(1,'bthrm')
|
||||
Bangle.setHRMPower(1,'bthrm');
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
|
@ -47,13 +55,13 @@ g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
|
|||
function drawInt(){
|
||||
counterInt--;
|
||||
if (counterInt < 0) counterInt = 0;
|
||||
if (counterInt > 5) counterInt = 5;
|
||||
if (counterInt > 3) counterInt = 3;
|
||||
draw(24, eventInt, "HRM", counterInt);
|
||||
}
|
||||
function drawBt(){
|
||||
counterBt--;
|
||||
if (counterBt < 0) counterBt = 0;
|
||||
if (counterBt > 5) counterBt = 5;
|
||||
if (counterBt > 3) counterBt = 3;
|
||||
draw(100, eventBt, "BTHRM", counterBt);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -12,3 +12,4 @@
|
|||
Support to show time and progress until next sunrise or sunset
|
||||
Load daily steps from Bangle health if available
|
||||
0.07: Allow configuration of minimal heart rate confidence
|
||||
0.08: Allow configuration of up to 4 circles in a row
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
|
@ -18,6 +18,8 @@ It can show the following information (this can be configured):
|
|||
## Screenshots
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Creator
|
||||
Marco ([myxor](https://github.com/myxor))
|
||||
|
|
|
@ -23,10 +23,7 @@ const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED
|
|||
const sunSetDown = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wLDg1ggfACoo"));
|
||||
const sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY"));
|
||||
|
||||
let settings;
|
||||
|
||||
function loadSettings() {
|
||||
settings = storage.readJSON("circlesclock.json", 1) || {
|
||||
let settings = storage.readJSON("circlesclock.json", 1) || {
|
||||
'minHR': 40,
|
||||
'maxHR': 200,
|
||||
'confidence': 0,
|
||||
|
@ -36,18 +33,17 @@ function loadSettings() {
|
|||
'batteryWarn': 30,
|
||||
'showWidgets': false,
|
||||
'weatherCircleData': 'humidity',
|
||||
'circleCount': 3,
|
||||
'circle1': 'hr',
|
||||
'circle2': 'steps',
|
||||
'circle3': 'battery'
|
||||
};
|
||||
// Load step goal from pedometer widget as fallback
|
||||
if (settings.stepGoal == undefined) {
|
||||
'circle3': 'battery',
|
||||
'circle4': 'weather'
|
||||
};
|
||||
// Load step goal from pedometer widget as fallback
|
||||
if (settings.stepGoal == undefined) {
|
||||
const d = require('Storage').readJSON("wpedom.json", 1) || {};
|
||||
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
|
||||
}
|
||||
}
|
||||
loadSettings();
|
||||
|
||||
|
||||
/*
|
||||
* Read location from myLocation app
|
||||
|
@ -58,6 +54,7 @@ function getLocation() {
|
|||
let location = getLocation();
|
||||
|
||||
const showWidgets = settings.showWidgets || false;
|
||||
const circleCount = settings.circleCount || 3;
|
||||
|
||||
let hrtValue;
|
||||
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 h2 = Math.round(3 * h / 5 - hOffset);
|
||||
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;
|
||||
const circleFont = "Vector:15";
|
||||
const circleFontBig = "Vector:16";
|
||||
|
||||
/*
|
||||
* circle x positions
|
||||
* depending on circleCount
|
||||
*
|
||||
* | 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() {
|
||||
g.clear(true);
|
||||
|
@ -122,10 +141,9 @@ function draw() {
|
|||
drawCircle(1);
|
||||
drawCircle(2);
|
||||
drawCircle(3);
|
||||
if (circleCount >= 4) drawCircle(4);
|
||||
}
|
||||
|
||||
const defaultCircleTypes = ["steps", "hr", "battery"];
|
||||
|
||||
function drawCircle(index) {
|
||||
let type = settings['circle' + index];
|
||||
if (!type) type = defaultCircleTypes[index - 1];
|
||||
|
@ -147,6 +165,7 @@ function drawCircle(index) {
|
|||
drawWeather(w);
|
||||
break;
|
||||
case "sunprogress":
|
||||
case "sunProgress":
|
||||
drawSunProgress(w);
|
||||
break;
|
||||
case "empty":
|
||||
|
@ -169,7 +188,7 @@ function getCirclePosition(type) {
|
|||
if (circlePositionsCache[type] >= 0) {
|
||||
return circlePosX[circlePositionsCache[type]];
|
||||
}
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
for (let i = 1; i <= circleCount; i++) {
|
||||
const setting = settings['circle' + i];
|
||||
if (setting == type) {
|
||||
circlePositionsCache[type] = i - 1;
|
||||
|
@ -319,6 +338,8 @@ function drawWeather(w) {
|
|||
if (code > 0) {
|
||||
const icon = getWeatherIconByCode(code);
|
||||
if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10);
|
||||
} else {
|
||||
g.drawString("?", w, h3 + radiusOuter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{ "id": "circlesclock",
|
||||
"name": "Circles clock",
|
||||
"shortName":"Circles clock",
|
||||
"version":"0.07",
|
||||
"description": "A clock with circles for different data at the bottom in a probably familiar style",
|
||||
"version":"0.08",
|
||||
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
|
||||
"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",
|
||||
"tags": "clock",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
|
|
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
@ -96,23 +96,36 @@
|
|||
format: v => weatherData[v],
|
||||
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,
|
||||
min: 0, max: 6,
|
||||
format: v => namesCircleTypes[v],
|
||||
onchange: x => save('circle1', valuesCircleTypes[x]),
|
||||
},
|
||||
'middle': {
|
||||
'circle2': {
|
||||
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
|
||||
min: 0, max: 6,
|
||||
format: v => namesCircleTypes[v],
|
||||
onchange: x => save('circle2', valuesCircleTypes[x]),
|
||||
},
|
||||
'right': {
|
||||
'circle3': {
|
||||
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
|
||||
min: 0, max: 6,
|
||||
format: v => namesCircleTypes[v],
|
||||
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]),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Tweaked proximity identification settings
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
## 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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright reelyActive 2017-2021
|
||||
* Copyright reelyActive 2017-2022
|
||||
* We believe in an open Internet of Things
|
||||
*
|
||||
* 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 ];
|
||||
const EXCITER_INSTANCE_IDS = new Uint32Array([ 0xe8c17e45 ]);
|
||||
const RESETTER_INSTANCE_IDS = new Uint32Array([ 0x4e5e77e4 ]);
|
||||
const PROXIMITY_RSSI_THRESHOLD = -65;
|
||||
const PROXIMITY_LED_RSSI_THRESHOLD = -65;
|
||||
const PROXIMITY_RSSI_THRESHOLD = -85;
|
||||
const PROXIMITY_LED_RSSI_THRESHOLD = -85;
|
||||
const PROXIMITY_TABLE_SIZE = 8;
|
||||
const DIGEST_TABLE_SIZE = 32;
|
||||
const OBSERVE_PERIOD_MILLISECONDS = 400;
|
||||
const BROADCAST_PERIOD_MILLISECONDS = 3600;
|
||||
const BROADCAST_PERIOD_MILLISECONDS = 1600;
|
||||
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_TIME_CYCLE_THRESHOLD = 86400;
|
||||
const EXCITER_HOLDOFF_SECONDS = 60;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "diract",
|
||||
"name": "DirAct",
|
||||
"shortName": "DirAct",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Proximity interaction detection.",
|
||||
"icon": "diract.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
Take 'beta' tag off
|
||||
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
|
||||
0.04: Include a precompiled bootloader for easy bootloader updates
|
||||
|
|
|
@ -3,33 +3,42 @@
|
|||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<p><b>THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE
|
||||
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>
|
||||
<p>This tool allows you to update the bootloader on <a href="https://www.espruino.com/Bangle.js2">Bangle.js 2</a> devices
|
||||
from within the App Loader.</p>
|
||||
|
||||
<div id="fw-unknown">
|
||||
<p><b>Firmware updates using the App Loader are only possible on
|
||||
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>
|
||||
</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">
|
||||
<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">
|
||||
<p>The currently available Espruino firmware releases are:</p>
|
||||
<ul id="latest-firmware-list">
|
||||
</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>
|
||||
|
||||
<p>Or you can upload a hex or zip file here. This file should be an <code>.app_hex</code>
|
||||
<a href="#" id="advanced-btn">Advanced ▼</a>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
|
||||
</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>
|
||||
|
||||
|
@ -38,7 +47,6 @@
|
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
|
||||
|
||||
<script>
|
||||
var hex;
|
||||
var hexJS; // JS to upload hex
|
||||
var HEADER_LEN = 16; // size of app flash header
|
||||
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 DEBUG = false;
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log').innerText = "";
|
||||
console.log("Log Cleared");
|
||||
}
|
||||
function log(t) {
|
||||
document.getElementById('log').innerText += t+"\n";
|
||||
console.log(t);
|
||||
}
|
||||
|
||||
function onInit(device) {
|
||||
console.log(device);
|
||||
console.log("fwupdate init", device);
|
||||
if (device && device.version)
|
||||
document.getElementById("fw-version").innerText = device.version;
|
||||
if (device && device.id=="BANGLEJS2") {
|
||||
document.getElementById("fw-unknown").style = "display:none";
|
||||
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 getURL(url, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onload = callback;
|
||||
baseURL = url;
|
||||
xhr.open("GET", baseURL);
|
||||
xhr.open("GET", url);
|
||||
xhr.responseType = "document";
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getFilesFromURL(url, regex, callback) {
|
||||
getURL(url, function() {
|
||||
console.log(this.responseXML)
|
||||
//console.log(this.responseXML)
|
||||
var files = [];
|
||||
var elements = this.responseXML.getElementsByTagName("a");
|
||||
for (var i=0;i<elements.length;i++) {
|
||||
|
@ -113,8 +132,7 @@ function checkForFileOnServer() {
|
|||
for (var i=0;i<fwlinks.length;i++)
|
||||
fwlinks[i].addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
var url = e.target.href;
|
||||
downloadZipFile(url).then(info=>{
|
||||
downloadURL(e.target.href).then(info=>{
|
||||
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) {
|
||||
return new Promise((resolve,reject) => {
|
||||
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!");
|
||||
info.storageContents = new Uint8Array(info.bin_file.byteLength + 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);
|
||||
log("Download complete");
|
||||
console.log("Download complete",info);
|
||||
document.getElementById("upload").style = ""; // show upload
|
||||
return info;
|
||||
}).catch(err => log("ERROR:" + err));
|
||||
}
|
||||
|
||||
function handleFileSelect(event) {
|
||||
clearLog();
|
||||
if (event.target.files.length!=1) {
|
||||
log("More than one file selected!");
|
||||
return;
|
||||
|
@ -172,13 +216,14 @@ function handleFileSelect(event) {
|
|||
var reader = new FileReader();
|
||||
if (file.name.endsWith(".hex") || file.name.endsWith(".app_hex")) {
|
||||
reader.onload = function(event) {
|
||||
hex = event.target.result.split("\n");
|
||||
log("HEX uploaded");
|
||||
document.getElementById("upload").style = ""; // show upload
|
||||
fileLoaded();
|
||||
hexFileLoaded(event.target.result);
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
} else if (file.name.endsWith(".zip")) {
|
||||
reader.onload = function(event) {
|
||||
log("ZIP uploaded");
|
||||
convertZipFile(event.target.result);
|
||||
};
|
||||
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) {
|
||||
var crc = 0xFFFFFFFF;
|
||||
data.forEach(function(d) {
|
||||
|
@ -278,6 +304,7 @@ function createJS_app(binary, startAddress, endAddress) {
|
|||
}
|
||||
hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\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.write(_fw,${startAddress});\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
|
||||
var startAddress, endAddress = 0;
|
||||
parseLines(function(addr, data) {
|
||||
hexParseLines(function(addr, data) {
|
||||
if (addr>MAX_ADDRESS) return; // ignore data out of range
|
||||
if (startAddress === undefined || addr<startAddress)
|
||||
startAddress = addr;
|
||||
|
@ -319,7 +366,7 @@ function fileLoaded() {
|
|||
// Work out data
|
||||
var binary = new Uint8Array(HEADER_LEN + endAddress-startAddress);
|
||||
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
|
||||
var binAddr = HEADER_LEN + addr - startAddress;
|
||||
binary.set(data, binAddr);
|
||||
|
@ -351,6 +398,10 @@ function handleUpload() {
|
|||
|
||||
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
|
||||
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);
|
||||
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "fwupdate",
|
||||
"name": "Firmware Update",
|
||||
"version": "0.03",
|
||||
"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",
|
||||
"version": "0.04",
|
||||
"description": "Uploads new Espruino firmwares to Bangle.js 2",
|
||||
"icon": "app.png",
|
||||
"type": "RAM",
|
||||
"tags": "tools,system",
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
0.07: Added coloured 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.10: Adds additional 3 minute setting for HRM
|
||||
|
|
|
@ -28,8 +28,8 @@ function menuSettings() {
|
|||
"< Back":()=>menuMain(),
|
||||
"Heart Rt":{
|
||||
value : 0|s.hrm,
|
||||
min : 0, max : 2,
|
||||
format : v=>["Off","10 mins","Always"][v],
|
||||
min : 0, max : 3,
|
||||
format : v=>["Off","3 mins","10 mins","Always"][v],
|
||||
onchange : v => { s.hrm=v;setSettings(s); }
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
(function(){
|
||||
var settings = require("Storage").readJSON("health.json",1)||{};
|
||||
var hrm = 0|settings.hrm;
|
||||
if (hrm==1) {
|
||||
if (hrm == 1 || hrm == 2) {
|
||||
function onHealth() {
|
||||
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('HRM', h => {
|
||||
|
|
|
@ -51,7 +51,7 @@ function saveCSV(data, date, title) {
|
|||
}
|
||||
|
||||
function downloadHealth(filename, callback) {
|
||||
Util.showModal("Downloading Track...");
|
||||
Util.showModal("Downloading Health info...");
|
||||
Util.readStorage(filename, data => {
|
||||
Util.hideModal();
|
||||
callback(data);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "health",
|
||||
"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)",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,health",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
After Width: | Height: | Size: 553 B |
|
@ -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>
|
|
@ -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": [ ]
|
||||
}
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Stopped watchface from flashing every interval
|
||||
0.03: Move to Bangle.setUI to launcher support
|
||||
0.04: Tweaks for compatibility with BangleJS2
|
||||
0.05: Time-word now readable on Bangle.js 2
|
||||
|
|
|
@ -46,7 +46,7 @@ const dy = big ? 22 : 16;
|
|||
const fontSize = big ? 3 : 2; // "6x8"
|
||||
const passivColor = 0x3186 /*grey*/ ;
|
||||
const activeColorNight = 0xF800 /*red*/ ;
|
||||
const activeColorDay = 0xFFFF /* white */;
|
||||
const activeColorDay = g.theme.fg;
|
||||
|
||||
var hidxPrev;
|
||||
var showDigitalTime = false;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "impwclock",
|
||||
"name": "Imprecise Word Clock",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
|
||||
"icon": "clock-impword.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: first release
|
||||
0.02: Themeable app icon
|
||||
0.03: Behave better on Bangle.js 1
|
||||
|
|
|
@ -5,7 +5,7 @@ let tStart;
|
|||
let tNow;
|
||||
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
|
||||
let hrs = Math.floor(t/3600000);
|
||||
|
@ -50,4 +50,5 @@ g.drawImage(icon,w/2-24,h/2-24);
|
|||
g.setFontAlign(0,0);
|
||||
require("Font8x12").add(Graphics);
|
||||
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);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "lapcounter",
|
||||
"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).",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
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.
|
|
@ -1,20 +1,21 @@
|
|||
# NotAnalog
|
||||
# Not Analog
|
||||
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
|
||||
4 numbers on the watch face ;)
|
||||
|
||||
The red hand shows the number of steps (0 = 0°, 2.5k = 90°, 5k = 180°, ...) and the
|
||||
black one the battery level (100% = 0°, 75% = 270°, 50% = 180°, ...).
|
||||
The red hand shows the number of steps (12k steps = 360 degrees) and the
|
||||
black one the battery level (100% = 360 degrees).
|
||||
The selected theme is also respected. Note that this watch face is in fullscreen
|
||||
mode, but widgets are still loaded in background.
|
||||
|
||||
## Other features
|
||||
- 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 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.
|
||||
- Shows current lock status of your bangle va a colored dot in the middle.
|
||||
- Sows symbol if GPS is on.
|
||||
|
||||
|
||||
## Screenshots
|
||||

|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Not Analog",
|
||||
"shortName":"Not Analog",
|
||||
"icon": "notanalog.png",
|
||||
"version":"0.1",
|
||||
"version":"0.03",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"description": "An analog watch face for people that can not read analog watch faces.",
|
||||
|
|
|
@ -28,7 +28,8 @@ var state = {
|
|||
maxSteps: 10000,
|
||||
bat: 0,
|
||||
has_weather: false,
|
||||
temp: "-"
|
||||
temp: "-",
|
||||
sleep: false,
|
||||
}
|
||||
|
||||
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="))
|
||||
};
|
||||
|
||||
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/
|
||||
|
@ -83,17 +90,18 @@ Graphics.prototype.setNormalFont = function(scale) {
|
|||
|
||||
|
||||
function getSteps() {
|
||||
var steps = 0;
|
||||
let health;
|
||||
try {
|
||||
health = require("health");
|
||||
try{
|
||||
if (WIDGETS.wpedom !== undefined) {
|
||||
return WIDGETS.wpedom.getSteps();
|
||||
} else if (WIDGETS.activepedom !== undefined) {
|
||||
return WIDGETS.activepedom.getSteps();
|
||||
}
|
||||
} catch(ex) {
|
||||
return steps;
|
||||
// In case we failed, we can only show 0 steps.
|
||||
}
|
||||
|
||||
health.readDay(new Date(), h=>steps+=h.steps);
|
||||
return steps;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
function drawBackground() {
|
||||
|
@ -169,7 +177,12 @@ function drawData() {
|
|||
}
|
||||
|
||||
// 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.setColor(g.theme.fg);
|
||||
|
||||
var currentDate = new Date();
|
||||
var posX = 14;
|
||||
var posY = 14;
|
||||
|
||||
// Hour
|
||||
var h = currentDate.getHours();
|
||||
var h = state.currentDate.getHours();
|
||||
var h1 = parseInt(h / 10);
|
||||
var h2 = h < 10 ? h : h - h1*10;
|
||||
g.drawString(h1, cx, posY+8);
|
||||
g.drawString(h2, W-posX, cy+5);
|
||||
drawTextCleared(h1, cx, posY+8);
|
||||
drawTextCleared(h2, W-posX, cy+5);
|
||||
|
||||
// Minutes
|
||||
var m = currentDate.getMinutes();
|
||||
var m = state.currentDate.getMinutes();
|
||||
var m1 = parseInt(m / 10);
|
||||
var m2 = m < 10 ? m : m - m1*10;
|
||||
g.drawString(m2, cx, H-posY);
|
||||
g.drawString(m1, posX-1, cy+5);
|
||||
drawTextCleared(m2, cx, H-posY);
|
||||
drawTextCleared(m1, posX-1, cy+5);
|
||||
}
|
||||
|
||||
|
||||
function drawDate(){
|
||||
var currentDate = new Date();
|
||||
|
||||
// Date
|
||||
g.setFontAlign(-1,0,0);
|
||||
g.setNormalFont();
|
||||
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(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){
|
||||
// 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.steps > state.maxSteps ? "#00ff00" :
|
||||
"#ff0000";
|
||||
|
||||
if(fastUpdate){
|
||||
/*
|
||||
* 5 Minute updates
|
||||
*/
|
||||
if(minutes % 5 == 0 && fastUpdate){
|
||||
return;
|
||||
}
|
||||
|
||||
// Set battery
|
||||
state.bat = E.getBattery();
|
||||
|
||||
// Set steps
|
||||
state.steps = getSteps();
|
||||
state.maxSteps = 10000;
|
||||
|
||||
// Set weather
|
||||
state.has_weather = true;
|
||||
try {
|
||||
|
@ -251,7 +275,16 @@ function handleState(fastUpdate){
|
|||
} catch(ex) {
|
||||
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);
|
||||
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
|
||||
if(fastUpdate){
|
||||
var innerRect = 20;
|
||||
|
@ -279,8 +319,8 @@ function draw(fastUpdate){
|
|||
drawDate();
|
||||
drawLock();
|
||||
drawState();
|
||||
drawData();
|
||||
drawTime();
|
||||
drawData();
|
||||
|
||||
// Queue draw in one minute
|
||||
queueDraw();
|
||||
|
@ -292,7 +332,7 @@ function draw(fastUpdate){
|
|||
*/
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (on) {
|
||||
draw(false);
|
||||
draw(true);
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
|
@ -300,11 +340,16 @@ Bangle.on('lcdPower',on=>{
|
|||
});
|
||||
|
||||
Bangle.on('charging',function(charging) {
|
||||
draw(false);
|
||||
draw(true);
|
||||
});
|
||||
|
||||
Bangle.on('lock', function(isLocked) {
|
||||
if(state.sleep){
|
||||
state.sleep=false;
|
||||
draw(false);
|
||||
} else {
|
||||
drawLock();
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on('touch', function(btn, e){
|
||||
|
@ -335,7 +380,7 @@ function queueDraw() {
|
|||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw(false);
|
||||
draw(true);
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
|
|
|
@ -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=="))
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.0 KiB |
|
@ -2,3 +2,4 @@
|
|||
0.02: Set pace format to mm:ss, time format to h:mm:ss,
|
||||
added settings to opt out of GPS and HRM
|
||||
0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2
|
||||
0.04: Use the exstats module, and make what is displayed configurable
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
* Allow this app to trigger the `Recorder` app on and off directly.
|
||||
* 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)
|
||||
|
|
184
apps/run/app.js
|
@ -1,68 +1,37 @@
|
|||
var ExStats = require("exstats");
|
||||
var B2 = process.env.HWVERSION==2;
|
||||
var Layout = require("Layout");
|
||||
var locale = require("locale");
|
||||
var fontHeading = "6x8:2";
|
||||
var fontValue = B2 ? "6x15:2" : "6x8:3";
|
||||
var headingCol = "#888";
|
||||
var running = false;
|
||||
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();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// ---------------------------
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
let settings = Object.assign({
|
||||
B1 : "dist",
|
||||
B2 : "time",
|
||||
B3 : "pacea",
|
||||
B4 : "bpm",
|
||||
B5 : "step",
|
||||
B6 : "caden",
|
||||
paceLength : 1000
|
||||
}, require("Storage").readJSON("run.json", 1) || {});
|
||||
var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!="");
|
||||
var exs = ExStats.getStats(statIDs, settings);
|
||||
// ---------------------------
|
||||
|
||||
function clearState() {
|
||||
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";
|
||||
}
|
||||
|
||||
// Called to start/stop running
|
||||
function onStartStop() {
|
||||
running = !running;
|
||||
var running = !exs.state.active;
|
||||
if (running) {
|
||||
clearState();
|
||||
startTime = Date.now();
|
||||
exs.start();
|
||||
} else {
|
||||
exs.stop();
|
||||
}
|
||||
layout.button.label = running ? "STOP" : "START";
|
||||
layout.status.label = running ? "RUN" : "STOP";
|
||||
|
@ -72,107 +41,44 @@ function onStartStop() {
|
|||
layout.render();
|
||||
}
|
||||
|
||||
var layout = new Layout( {
|
||||
type:"v", c: [
|
||||
{ type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol },
|
||||
{type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 },
|
||||
{type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 }
|
||||
]}, { 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 }
|
||||
var lc = [];
|
||||
// Load stats in pair by pair
|
||||
for (var i=0;i<statIDs.length;i+=2) {
|
||||
var sa = exs.stats[statIDs[i+0]];
|
||||
var sb = exs.stats[statIDs[i+1]];
|
||||
lc.push({ type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol },
|
||||
{type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol }
|
||||
]}, { 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:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
|
||||
{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"}]});
|
||||
clearState();
|
||||
delete lc;
|
||||
layout.render();
|
||||
|
||||
function onTimer() {
|
||||
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);
|
||||
}
|
||||
|
||||
// Handle GPS state change for icon
|
||||
Bangle.on("GPS", function(fix) {
|
||||
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
|
||||
if (!fix.fix) { return; } // only process actual fixes
|
||||
if (!fix.fix) return; // only process actual fixes
|
||||
if (fixCount++ == 0) {
|
||||
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) {
|
||||
layout.hrm.label = h.bpm;
|
||||
});
|
||||
Bangle.on("step", function(steps) {
|
||||
if (running) {
|
||||
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");
|
||||
// We always call ourselves once a second to update
|
||||
setInterval(function() {
|
||||
layout.clock.label = locale.time(new Date(),1);
|
||||
layout.render();
|
||||
}, 1000);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "run",
|
||||
"name": "Run",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors,gps",
|
||||
|
|
|
@ -1,44 +1,50 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = "run.json";
|
||||
|
||||
// initialize with default settings...
|
||||
let s = {
|
||||
'use_gps': true,
|
||||
'use_hrm': true
|
||||
}
|
||||
var ExStats = require("exstats");
|
||||
var statsList = ExStats.getList();
|
||||
statsList.unshift({name:"-",id:""}); // add blank menu item
|
||||
var statsIDs = statsList.map(s=>s.id);
|
||||
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
const saved = settings || {}
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key]
|
||||
}
|
||||
|
||||
function save() {
|
||||
settings = s
|
||||
let settings = Object.assign({
|
||||
B1 : "dist",
|
||||
B2 : "time",
|
||||
B3 : "pacea",
|
||||
B4 : "bpm",
|
||||
B5 : "step",
|
||||
B6 : "caden",
|
||||
paceLength : 1000
|
||||
}, storage.readJSON(SETTINGS_FILE, 1) || {});
|
||||
function saveSettings() {
|
||||
storage.write(SETTINGS_FILE, settings)
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Run' },
|
||||
'< Back': back,
|
||||
'Use GPS': {
|
||||
value: s.use_gps,
|
||||
format: () => (s.use_gps ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.use_gps = !s.use_gps;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Use HRM': {
|
||||
value: s.use_hrm,
|
||||
format: () => (s.use_hrm ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.use_hrm = !s.use_hrm;
|
||||
save();
|
||||
function getBoxChooser(boxID) {
|
||||
return {
|
||||
min :0, max: statsIDs.length-1,
|
||||
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
|
||||
format: v => statsList[v].name,
|
||||
onchange: v => {
|
||||
settings[boxID] = statsIDs[v];
|
||||
saveSettings();
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Advertise app name, added screenshots
|
||||
0.04: Advertise bar, GPS, HRM and mag services
|
||||
0.05: Refactored for efficiency, corrected sensor value inaccuracies
|
||||
0.06: User settings are written to persistent storage, loaded on app start
|
|
@ -5,7 +5,9 @@ Collect all the sensor data from the Bangle.js 2, display the live readings in m
|
|||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
@ -22,7 +24,7 @@ in the menu display, and broadcasts all sensor data readings _except_ accelerati
|
|||
|
||||
## 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
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
{
|
||||
"id": "sensible",
|
||||
"name": "SensiBLE",
|
||||
"shortName": "SensiBLE",
|
||||
"version": "0.05",
|
||||
"description": "Collect, display and advertise real-time sensor data.",
|
||||
"icon": "sensible.png",
|
||||
"screenshots": [
|
||||
"id": "sensible",
|
||||
"name": "SensiBLE",
|
||||
"shortName": "SensiBLE",
|
||||
"version": "0.06",
|
||||
"description": "Collect, display and advertise real-time sensor data.",
|
||||
"icon": "sensible.png",
|
||||
"screenshots": [
|
||||
{ "url": "screenshot-top.png" },
|
||||
{ "url": "screenshot-acc.png" },
|
||||
{ "url": "screenshot-bar.png" },
|
||||
{ "url": "screenshot-gps.png" },
|
||||
{ "url": "screenshot-hrm.png" },
|
||||
{ "url": "screenshot-mag.png" }
|
||||
],
|
||||
"type": "app",
|
||||
"tags": "tool,sensors,bluetooth",
|
||||
"supports" : [ "BANGLEJS2" ],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
],
|
||||
"type": "app",
|
||||
"tags": "tool,sensors,bluetooth",
|
||||
"supports" : [ "BANGLEJS2" ],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{ "name": "sensible.app.js", "url": "sensible.js" },
|
||||
{ "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true }
|
||||
]
|
||||
],
|
||||
"data": [
|
||||
{ "name": "sensible.data.json", "url": "settings.json", "storageFile": true }
|
||||
]
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 45 KiB |
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright reelyActive 2021
|
||||
* Copyright reelyActive 2021-2022
|
||||
* We believe in an open Internet of Things
|
||||
*/
|
||||
|
||||
|
@ -7,6 +7,8 @@
|
|||
// Non-user-configurable constants
|
||||
const APP_ID = 'sensible';
|
||||
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,
|
||||
0x65, 0x3a, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x62,
|
||||
0x6c, 0x65, 0x7d ];
|
||||
|
@ -19,16 +21,12 @@ let isBarMenu = false;
|
|||
let isGpsMenu = false;
|
||||
let isHrmMenu = false;
|
||||
let isMagMenu = false;
|
||||
let isBarEnabled = true;
|
||||
let isGpsEnabled = true;
|
||||
let isHrmEnabled = true;
|
||||
let isMagEnabled = true;
|
||||
let isNewAccData = false;
|
||||
let isNewBarData = false;
|
||||
let isNewGpsData = false;
|
||||
let isNewHrmData = false;
|
||||
let isNewMagData = false;
|
||||
|
||||
let settings = require('Storage').readJSON(SETTINGS_FILENAME);
|
||||
|
||||
|
||||
// Menus
|
||||
|
@ -51,9 +49,9 @@ let accMenu = {
|
|||
let barMenu = {
|
||||
"": { "title" : "- Barometer -" },
|
||||
"State": {
|
||||
value: isBarEnabled,
|
||||
value: settings.isBarEnabled,
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => { isBarEnabled = v; Bangle.setBarometerPower(v, APP_ID); }
|
||||
onchange: v => { updateSetting('isBarEnabled', v); }
|
||||
},
|
||||
"Altitude": { value: null },
|
||||
"Press": { value: null },
|
||||
|
@ -63,9 +61,9 @@ let barMenu = {
|
|||
let gpsMenu = {
|
||||
"": { "title" : "- GPS -" },
|
||||
"State": {
|
||||
value: isGpsEnabled,
|
||||
value: settings.isGpsEnabled,
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => { isGpsEnabled = v; Bangle.setGPSPower(v, APP_ID); }
|
||||
onchange: v => { updateSetting('isGpsEnabled', v); }
|
||||
},
|
||||
"Lat": { value: null },
|
||||
"Lon": { value: null },
|
||||
|
@ -77,9 +75,9 @@ let gpsMenu = {
|
|||
let hrmMenu = {
|
||||
"": { "title" : "- Heart Rate -" },
|
||||
"State": {
|
||||
value: isHrmEnabled,
|
||||
value: settings.isHrmEnabled,
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => { isHrmEnabled = v; Bangle.setHRMPower(v, APP_ID); }
|
||||
onchange: v => { updateSetting('isHrmEnabled', v); }
|
||||
},
|
||||
"BPM": { value: null },
|
||||
"Confidence": { value: null },
|
||||
|
@ -88,9 +86,9 @@ let hrmMenu = {
|
|||
let magMenu = {
|
||||
"": { "title" : "- Magnetometer -" },
|
||||
"State": {
|
||||
value: isMagEnabled,
|
||||
value: settings.isMagEnabled,
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => { isMagEnabled = v; Bangle.setCompassPower(v, APP_ID); }
|
||||
onchange: v => { updateSetting('isMagEnabled', v); }
|
||||
},
|
||||
"x": { value: null },
|
||||
"y": { value: null },
|
||||
|
@ -124,7 +122,7 @@ function transmitUpdatedSensorData() {
|
|||
isNewMagData = false;
|
||||
}
|
||||
|
||||
let interval = 1000 / data.length;
|
||||
let interval = UPDATE_MILLISECONDS / data.length;
|
||||
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
|
||||
Bangle.on('accel', function(newAcc) {
|
||||
acc = newAcc;
|
||||
|
@ -260,9 +275,6 @@ Bangle.on('mag', function(newMag) {
|
|||
|
||||
// On start: enable sensors and display main menu
|
||||
g.clear();
|
||||
Bangle.setBarometerPower(isBarEnabled, APP_ID);
|
||||
Bangle.setGPSPower(isGpsEnabled, APP_ID);
|
||||
Bangle.setHRMPower(isHrmEnabled, APP_ID);
|
||||
Bangle.setCompassPower(isMagEnabled, APP_ID);
|
||||
enableSensors();
|
||||
E.showMenu(mainMenu);
|
||||
setInterval(transmitUpdatedSensorData, 1000);
|
||||
setInterval(transmitUpdatedSensorData, UPDATE_MILLISECONDS);
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"isBarEnabled": false,
|
||||
"isGpsEnabled": false,
|
||||
"isHrmEnabled": false,
|
||||
"isMagEnabled": false
|
||||
}
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Use queueDraw(), update every minute, respect theme, use Lato font
|
||||
0.05: Decided against custom font as it inceases the code size
|
||||
minimalism is useful when narrowing down issues
|
||||
0.06: renamed some files
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "simplest",
|
||||
"name": "Simplest Clock",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "The simplest working clock, acts as a tutorial piece",
|
||||
"icon": "simplest.png",
|
||||
"screenshots": [{"url":"screenshot_simplest.png"}],
|
||||
|
@ -9,7 +9,7 @@
|
|||
"tags": "clock",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"simplest.app.js","url":"app.js"},
|
||||
{"name":"simplest.img","url":"icon.js","evaluate":true}
|
||||
{"name":"simplest.app.js","url":"simplest.app.js"},
|
||||
{"name":"simplest.img","url":"simplest.icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ function draw() {
|
|||
g.setFontAlign(0, 0);
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawString(timeStr, w/2, h/2);
|
||||
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
|
@ -42,13 +41,12 @@ Bangle.on('lcdPower',on=>{
|
|||
g.clear();
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
//Bangle.setUI("clock");
|
||||
// Bangle.setUI("clock");
|
||||
// use clockupdown as it tests for issue #1249
|
||||
Bangle.setUI("clockupdown", btn=> {
|
||||
draw();
|
||||
});
|
||||
|
||||
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Respect Quiet Mode
|
||||
0.03: Add compatibility for Bangle.js 2 and new firmware, added "Alarm at " for the alarm time
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const BANGLEJS2 = process.env.HWVERSION == 2; //# check for bangle 2
|
||||
const alarms = require("Storage").readJSON("alarm.json",1)||[];
|
||||
const active = alarms.filter(a=>a.on);
|
||||
|
||||
|
@ -52,21 +53,21 @@ active.forEach(alarm => {
|
|||
}
|
||||
});
|
||||
|
||||
function drawString(s, x, y) {
|
||||
g.clearRect(0,y-15,239,y+15);
|
||||
g.reset();
|
||||
g.setFont("Vector",20);
|
||||
g.setFontAlign(0,0); // align right bottom
|
||||
g.drawString(s, x, y);
|
||||
function drawString(s, y) { //# replaced x: always centered
|
||||
g.reset(); //# moved up to prevent blue background
|
||||
g.clearRect(0, y - 12, 239, y + 8); //# minimized upper+lower clearing
|
||||
g.setFont("Vector", 20);
|
||||
g.setFontAlign(0, 0); // align centered
|
||||
g.drawString(s, g.getWidth() / 2, y); //# set x to center
|
||||
}
|
||||
|
||||
function drawApp() {
|
||||
g.clearRect(0,24,239,215);
|
||||
g.clearRect(0,24,239,215); //# no problem
|
||||
var alarmHour = nextAlarm.getHours();
|
||||
var alarmMinute = nextAlarm.getMinutes();
|
||||
if (alarmHour < 10) alarmHour = "0" + alarmHour;
|
||||
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");
|
||||
|
||||
function drawTime() {
|
||||
|
@ -78,12 +79,20 @@ function drawApp() {
|
|||
if (nowHour < 10) nowHour = "0" + nowHour;
|
||||
if (nowMinute < 10) nowMinute = "0" + nowMinute;
|
||||
if (nowSecond < 10) nowSecond = "0" + nowSecond;
|
||||
const time = nowHour + ":" + nowMinute + ":" + nowSecond;
|
||||
drawString(time, 120, 140);
|
||||
const time = nowHour + ":" + nowMinute + (BANGLEJS2 ? "" : ":" + nowSecond); //# hide seconds on bangle 2
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var buzzCount = 19;
|
||||
|
@ -104,8 +113,8 @@ function buzz() {
|
|||
var minAlarm = new Date();
|
||||
var measure = true;
|
||||
if (nextAlarm !== undefined) {
|
||||
Bangle.loadWidgets(); //# correct widget load draw order
|
||||
Bangle.drawWidgets();
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// minimum alert 30 minutes early
|
||||
minAlarm.setTime(nextAlarm.getTime() - (30*60*1000));
|
||||
|
@ -116,7 +125,7 @@ if (nextAlarm !== undefined) {
|
|||
|
||||
if (swest !== undefined) {
|
||||
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');
|
||||
setTimeout(load, 1000);
|
||||
}
|
||||
// BTN2 to menu, BTN3 to main
|
||||
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
|
||||
setWatch(() => load(), BTN3, { repeat: false, edge: "falling" });
|
||||
// BTN2 to menu, BTN3 to main # on bangle 2 only BTN to main
|
||||
if (!BANGLEJS2) setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
|
||||
setWatch(() => load(), BANGLEJS2 ? BTN : BTN3, { repeat: false, edge: "falling" });
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "sleepphasealarm",
|
||||
"name": "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.",
|
||||
"icon": "app.png",
|
||||
"tags": "alarm",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"sleepphasealarm.app.js","url":"app.js"},
|
||||
{"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true}
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
0.01: Initial import.
|
||||
0.02: Add swipe to change screens.
|
||||
0.03: Misc memory and screen optimisations.
|
||||
0.02: Misc development.
|
||||
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.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "speedalt2",
|
||||
"name": "GPS Adventure Sports 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.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New Widget!
|
||||
0.02: Now also visible on Bangle.js 2
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"id": "widram",
|
||||
"name": "RAM Widget",
|
||||
"shortName": "RAM Widget",
|
||||
"version": "0.01",
|
||||
"description": "Display your Bangle's available RAM percentage in a widget",
|
||||
"version": "0.02",
|
||||
"description": "Display your Bangle's RAM usage percentage in a widget",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"tags": "widget",
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
(() => {
|
||||
function draw() {
|
||||
BANGLEJS2 = process.env.HWVERSION==2;
|
||||
g.reset();
|
||||
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.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff"));
|
||||
g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/);
|
||||
if (!BANGLEJS2)
|
||||
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;
|
||||
Bangle.on('lcdPower', function(on) {
|
||||
|
@ -20,4 +24,4 @@
|
|||
}
|
||||
});
|
||||
WIDGETS["ram"]={area:"tl",width: 24,draw:draw};
|
||||
})()
|
||||
})();
|
||||
|
|
|
@ -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.
|
||||
|
||||

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

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

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

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

|
||||
|
||||
This will load up the waypoint editor:
|
||||
|
||||

|
||||
|
||||
### 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 »](handlebar-mount.stl)
|
||||
|
||||
[Download the handlebar mount FreeCAD source »](handlebar-mount.FCStd)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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.
|
|
@ -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();
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 106 KiB |
|
@ -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"))
|
|
@ -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"}]
|
||||
}
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 93 KiB |
|
@ -0,0 +1,5 @@
|
|||
[
|
||||
{
|
||||
"name":"NONE"
|
||||
},
|
||||
]
|
|
@ -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>
|
After Width: | Height: | Size: 15 KiB |
|
@ -37,7 +37,7 @@ for app in apps/*/; do
|
|||
done
|
||||
echo "]" >> "$outfile"
|
||||
|
||||
if [ -z "$1"]; then
|
||||
if [ -z "$1" ]; then
|
||||
# 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
|
||||
# overwrite and still commit apps.json
|
||||
|
|
|
@ -57,7 +57,7 @@ function fileGetter(url) {
|
|||
|
||||
Promise.all(APPS.map(appid => {
|
||||
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) {
|
||||
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 = js.replace(/\x10/g,""); // remove the echo-off characters (for testing only)
|
||||
fs.writeFileSync(OUTFILE, js);
|
||||
console.log("Output written to "+OUTFILE);
|
||||
});
|
||||
|
|
|
@ -133,7 +133,7 @@ function evaluateFile(file) {
|
|||
|
||||
Promise.all(APPS.map(appid => {
|
||||
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) {
|
||||
throw new Error(`App ${appid} not found`);
|
||||
}
|
||||
|
|
2
core
|
@ -1 +1 @@
|
|||
Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b
|
||||
Subproject commit f97a128a28b409c576f66c63c87905f26a5cfd8b
|
|
@ -51,8 +51,8 @@
|
|||
<li class="tab-item" id="tab-myappscontainer">
|
||||
<a href="javascript:showTab('myappscontainer')">My Apps</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-aboutcontainer">
|
||||
<a href="javascript:showTab('aboutcontainer')">About</a>
|
||||
<li class="tab-item" id="tab-morecontainer">
|
||||
<a href="javascript:showTab('morecontainer')">More...</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -111,7 +111,7 @@
|
|||
</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-body">
|
||||
<a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a>
|
||||
|
|
|
@ -32,12 +32,13 @@ DEVICEINFO = DEVICEINFO.filter(x=>x.id.startsWith("BANGLEJS"));
|
|||
|
||||
// When a device is found, filter the apps accordingly
|
||||
function onFoundDeviceInfo(deviceId, deviceVersion) {
|
||||
var fwURL = "#";
|
||||
var fwURL = "#", fwExtraText = "";
|
||||
if (deviceId == "BANGLEJS") {
|
||||
fwURL = "https://www.espruino.com/Bangle.js#firmware-updates";
|
||||
Const.MESSAGE_RELOAD = 'Hold BTN3\nto reload';
|
||||
}
|
||||
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";
|
||||
Const.MESSAGE_RELOAD = 'Hold button\nto reload';
|
||||
}
|
||||
|
@ -45,7 +46,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) {
|
|||
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);
|
||||
} 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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
```
|
||||
|
|
|
@ -1,9 +1,54 @@
|
|||
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");
|
||||
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.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* Copyright (c) 2020 OmegaRogue. See the file LICENSE for copying permission. */
|
||||
/*
|
||||
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 = {
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|