1
0
Fork 0

Merge branch 'espruino:master' into master

master
Andrew Gregory 2022-01-29 09:44:35 +08:00 committed by GitHub
commit 97673461a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
137 changed files with 4994 additions and 521 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
(() => { (() => {
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDTimeout(0); Bangle.setLCDTimeout(0);
let intervalID; let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {}; let settings = require("Storage").readJSON("ballmaze.json",true) || {};
@ -6,7 +7,9 @@
// density, elasticity of bounces, "drag coefficient" // density, elasticity of bounces, "drag coefficient"
const rho = 100, e = 0.3, C = 0.01; const rho = 100, e = 0.3, C = 0.01;
// screen width & height in pixels // screen width & height in pixels
const sW = 240, sH = 160; const sW = g.getWidth();
const sH = g.getHeight()*2/3;
const bgColour ="#f00"; // only for Bangle.js 2
// gravity constant (lowercase was already taken) // gravity constant (lowercase was already taken)
const G = 9.80665; const G = 9.80665;
@ -17,14 +20,16 @@
// The play area is 240x160, sizes are the ball radius, so we can use common // The play area is 240x160, sizes are the ball radius, so we can use common
// denominators of 120x80 to get square rooms // denominators of 120x80 to get square rooms
// Reverse the order to show the easiest on top of the menu // Reverse the order to show the easiest on top of the menu
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(),
// even size 1 actually works, but larger mazes take forever to generate
minSize = 4, defaultSize = 10;
const sizeNames = { const sizeNames = {
1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large", 1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large",
10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial", 10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial",
}; };
// even size 1 actually works, but larger mazes take forever to generate
if (!BANGLEJS2) {
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), minSize = 4, defaultSize = 10;
} else {
const sizes = [1, 2, 4, 5, 8, 10, 16, 20 ].reverse(), minSize = 4, defaultSize = 10;
}
/** /**
* Draw something to all screen buffers * Draw something to all screen buffers
* @param draw {function} Callback which performs the drawing * @param draw {function} Callback which performs the drawing
@ -45,17 +50,17 @@
// use unbuffered graphics for UI stuff // use unbuffered graphics for UI stuff
function showMessage(message, title) { function showMessage(message, title) {
Bangle.setLCDMode(); if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMessage(message, title); return E.showMessage(message, title);
} }
function showPrompt(prompt, options) { function showPrompt(prompt, options) {
Bangle.setLCDMode(); if (!BANGLEJS2) Bangle.setLCDMode();
return E.showPrompt(prompt, options); return E.showPrompt(prompt, options);
} }
function showMenu(menu) { function showMenu(menu) {
Bangle.setLCDMode(); if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMenu(menu); return E.showMenu(menu);
} }
@ -105,7 +110,7 @@
generateMaze(); // this shows unbuffered progress messages generateMaze(); // this shows unbuffered progress messages
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-( if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
Bangle.setLCDMode("doublebuffered"); if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
clearAll(); clearAll();
drawAll(drawMaze); drawAll(drawMaze);
intervalID = setInterval(tick, 100); intervalID = setInterval(tick, 100);
@ -307,6 +312,7 @@
const range = {top: 0, left: 0, bottom: rows, right: cols}; const range = {top: 0, left: 0, bottom: rows, right: cols};
const w = sW/cols, h = sH/rows; const w = sW/cols, h = sH/rows;
g.clear(); g.clear();
if (BANGLEJS2) g.setBgColor(bgColour);
g.setColor(0.76, 0.60, 0.42); g.setColor(0.76, 0.60, 0.42);
for(let row = range.top; row<=range.bottom; row++) { for(let row = range.top; row<=range.bottom; row++) {
for(let col = range.left; col<=range.right; col++) { for(let col = range.left; col<=range.right; col++) {

View File

@ -1,2 +1,3 @@
0.01: Initial version of Balltastic released! Happy! 0.01: Initial version of Balltastic released! Happy!
0.02: Set LCD timeout for Espruino 2v10 compatibility 0.02: Set LCD timeout for Espruino 2v10 compatibility
0.03: Now also works on Bangle.js 2

View File

@ -1,11 +1,12 @@
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDBrightness(1); Bangle.setLCDBrightness(1);
Bangle.setLCDMode("doublebuffered"); if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
Bangle.setLCDTimeout(0); Bangle.setLCDTimeout(0);
let points = 0; let points = 0;
let level = 1; let level = 1;
let levelSpeedStart = 0.8; let levelSpeedStart = 0.8;
let nextLevelPoints = 20; let nextLevelPoints = 10;
let levelSpeedFactor = 0.2; let levelSpeedFactor = 0.2;
let counterWidth = 10; let counterWidth = 10;
let gWidth = g.getWidth() - counterWidth; let gWidth = g.getWidth() - counterWidth;
@ -81,12 +82,23 @@ function drawLevelText() {
g.setColor("#26b6c7"); g.setColor("#26b6c7");
g.setFontAlign(0, 0); g.setFontAlign(0, 0);
g.setFont("4x6", 5); g.setFont("4x6", 5);
g.drawString("Level " + level, 120, 80); g.drawString("Level " + level, g.getWidth()/2, g.getHeight()/2);
}
function drawPointsText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
g.setFont("4x6", 2);
g.drawString("Points " + points, g.getWidth()/2, g.getHeight()-20);
} }
function draw() { function draw() {
//bg //bg
g.setColor("#71c6cf"); if (!BANGLEJS2) {
g.setColor("#71c6cf");
} else {
g.setColor("#002000");
}
g.fillRect(0, 0, g.getWidth(), g.getHeight()); g.fillRect(0, 0, g.getWidth(), g.getHeight());
//counter //counter
@ -94,6 +106,7 @@ function draw() {
//draw level //draw level
drawLevelText(); drawLevelText();
drawPointsText();
//dot //dot
g.setColor("#ff0000"); g.setColor("#ff0000");
@ -152,7 +165,7 @@ function count() {
if (counter <= 0) { if (counter <= 0) {
running = false; running = false;
clearInterval(drawInterval); clearInterval(drawInterval);
setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Gameover!");},50); setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Game over!");},50);
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,12 +1,13 @@
{ {
"id": "balltastic", "id": "balltastic",
"name": "Balltastic", "name": "Balltastic",
"version": "0.02", "version": "0.03",
"description": "Simple but fun ball eats dots game.", "description": "Simple but fun ball eats dots game.",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"bangle2-balltastic-screenshot.png"}],
"type": "app", "type": "app",
"tags": "game,fun", "tags": "game,fun",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [
{"name":"balltastic.app.js","url":"app.js"}, {"name":"balltastic.app.js","url":"app.js"},
{"name":"balltastic.img","url":"app-icon.js","evaluate":true} {"name":"balltastic.img","url":"app-icon.js","evaluate":true}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,3 +11,5 @@
Support to choose between humidity and wind speed for weather circle progress Support to choose between humidity and wind speed for weather circle progress
Support to show time and progress until next sunrise or sunset Support to show time and progress until next sunrise or sunset
Load daily steps from Bangle health if available Load daily steps from Bangle health if available
0.07: Allow configuration of minimal heart rate confidence
0.08: Allow configuration of up to 4 circles in a row

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

@ -24,12 +24,7 @@ if (!settings) resetSettings();
function showMenu() { function showMenu() {
const datemenu = { const datemenu = {
'': { '': {
'title': 'Set Date', 'title': 'Set Date'
'predraw': function() {
datemenu.Day.value = settings.day;
datemenu.Month.value = settings.month;
datemenu.Year.value = settings.year;
}
}, },
'Day': { 'Day': {
value: settings.day, value: settings.day,
@ -65,4 +60,3 @@ function showMenu() {
} }
showMenu(); showMenu();

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
"description": "Simple file manager, allows user to examine watch storage and display, load or delete individual files", "description": "Simple file manager, allows user to examine watch storage and display, load or delete individual files",
"icon": "icons8-filing-cabinet-48.png", "icon": "icons8-filing-cabinet-48.png",
"tags": "tools", "tags": "tools",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"fileman.app.js","url":"fileman.app.js"}, {"name":"fileman.app.js","url":"fileman.app.js"},

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,3 +6,4 @@
0.06: Move the next strike time to the first row of display 0.06: Move the next strike time to the first row of display
0.07: Change the boot function to avoid reloading the entire watch 0.07: Change the boot function to avoid reloading the entire watch
0.08: Default to no strikes. Fix file-not-found issue during the first boot. Add data file. 0.08: Default to no strikes. Fix file-not-found issue during the first boot. Add data file.
0.09: Add some customisation options

View File

@ -1,5 +1,6 @@
const storage = require('Storage'); const storage = require('Storage');
var settings = storage.readJSON('hourstrike.json', 1); var settings = storage.readJSON('hourstrike.json', 1);
const chimes = ["Buzz", "Beep"];
function updateSettings() { function updateSettings() {
storage.write('hourstrike.json', settings); storage.write('hourstrike.json', settings);
@ -26,6 +27,12 @@ function showMainMenu() {
mainmenu.Strength = { mainmenu.Strength = {
value: settings.vlevel*10, min: 1, max: 10, format: v=>v/10, value: settings.vlevel*10, min: 1, max: 10, format: v=>v/10,
onchange: v=> {settings.vlevel = v/10; updateSettings();}}; onchange: v=> {settings.vlevel = v/10; updateSettings();}};
mainmenu.Strikecount = {
value: settings.scount, min: 1, max: 2, format: v=>v,
onchange: v=> {settings.scount = v; updateSettings();}};
mainmenu.Chimetype = {
value: settings.buzzOrBeep, min: 0, max: 1, format: v => chimes[v],
onchange: v=> {settings.buzzOrBeep = v; updateSettings();}};
mainmenu['< Back'] = ()=>load(); mainmenu['< Back'] = ()=>load();
return E.showMenu(mainmenu); return E.showMenu(mainmenu);
} }

View File

@ -30,9 +30,23 @@
} }
function strike_func () { function strike_func () {
var setting = require('Storage').readJSON('hourstrike.json',1)||[]; var setting = require('Storage').readJSON('hourstrike.json',1)||[];
Bangle.buzz(200, setting.vlevel||0.5) if (0 == setting.buzzOrBeep) {
.then(() => new Promise(resolve => setTimeout(resolve,200))) if (2 == setting.scount) {
.then(() => Bangle.buzz(200, setting.vlevel||0.5)); Bangle.buzz(200, setting.vlevel||0.5)
.then(() => new Promise(resolve => setTimeout(resolve,200)))
.then(() => Bangle.buzz(200, setting.vlevel||0.5));
} else {
Bangle.buzz(200, setting.vlevel||0.5);
}
} else {
if (2 == setting.scount) {
Bangle.beep(200)
.then(() => new Promise(resolve => setTimeout(resolve,100)))
.then(() => Bangle.beep(300));
} else {
Bangle.beep(200);
}
}
setup(); setup();
} }
setup(); setup();

View File

@ -1 +1 @@
{"interval":-1,"start":9,"end":21,"vlevel":0.5,"next_hour":-1,"next_minute":-1} {"interval":-1,"start":9,"end":21,"vlevel":0.5,"scount":2,"buzzOrBeep":0,"next_hour":-1,"next_minute":-1}

View File

@ -2,7 +2,7 @@
"id": "hourstrike", "id": "hourstrike",
"name": "Hour Strike", "name": "Hour Strike",
"shortName": "Hour Strike", "shortName": "Hour Strike",
"version": "0.08", "version": "0.09",
"description": "Strike the clock on the hour. A great tool to remind you an hour has passed!", "description": "Strike the clock on the hour. A great tool to remind you an hour has passed!",
"icon": "app-icon.png", "icon": "app-icon.png",
"tags": "tool,alarm", "tags": "tool,alarm",

View File

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

BIN
apps/hrmaccevents/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,9 @@ Access different screens via tap on the left/ right side of the screen
![](screenshot_2.png) ![](screenshot_2.png)
# Ideas
- Tap top / bottom to disable steps (also icon) and start a timer
## Contributors ## Contributors
- [David Peer](https://github.com/peerdavid). - [David Peer](https://github.com/peerdavid).
- [Adam Schmalhofer](https://github.com/adamschmalhofer). - [Adam Schmalhofer](https://github.com/adamschmalhofer).

View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Add the option to enable touching the widget only on clock and settings.

102
apps/lightswitch/README.md Normal file
View File

@ -0,0 +1,102 @@
# Light Switch Widget
Whis this widget I wanted to create a solution to quickly en-/disable the LCD backlight and even change the brightness.
In addition it shows the lock status with the option to personalize the lock icon with a tiny image.
---
### Control
---
* __On / off__
Single touch the widget to en-/disable the backlight.
* __Change brightness__ _(can be disabled)_
First touch the widget, then quickly touch the screen again and drag up/down until you reach your wished brigthness.
* __Double tap to flash backlight__ _(can be disabled)_
By defaut you can double tap on the right side of your bangle to flash the backlight for a short duration.
(While the backlight is active your bangle will be unlocked.)
* __Double tap to unlock__ _(disabled by default)_
If a side is defined in the app settings, your bangle will be unlocked if you double tap on that side.
---
### Settings
---
#### Widget - Change the apperance of the widget:
* __Bulb col__
_red_ / _yellow_ / _green_ / __cyan__ / _blue_ / _magenta_
Define the color used for the lightbulbs inner circle.
The selected color will be dimmed depending on the actual brightness value.
* __Image__
__default__ / _random_ / _..._
Set your favourite lock icon image. (If no image file is found _no image_ will be displayed.)
* _random_ -> Select a random image on each time the widget is drawn.
#### Control - Change when and how to use the widget:
* __Touch__
_on def clk_ / _on all clk_ / _clk+setting_ / _clk+launch_ / _except apps_ / __always on__
Select when touching the widget is active to en-/disable the backlight.
* _on def clk_ -> only on your selected main clock face
* _on all clk_ -> on all apps of the type _clock_
* _clk+setting_ -> on all apps of the type _clock_ and in the settings
* _clk+launch_ -> on all apps of the types _clock_ and _launch_
* _except apps_ -> on all apps of the types _clock_ and _launch_ and in the settings
* _always on_ -> always enabled when the widget is displayed
* __Drag Delay__
_off_ / _50ms_ / _100ms_ / _..._ / __500ms__ / _..._ / _1000ms_
Change the maximum delay between first touch and re-touch/drag to change the brightness or disable changing the brightness completely.
* __Min Value__
_1%_ / _2%_ / _..._ / __10%__ / _..._ / _100%_
Set the minimal level of brightness you can change to.
#### Unlock - Set double tap side to unlock:
* __TapSide__
__off__ / _left_ / _right_ / _top_ / _bottom_ / _front_ / _back_
#### Flash - Change if and how to flash the backlight:
* __TapSide__
_off_ / _left_ / __right__ / _top_ / _bottom_ / _front_ / _back_
Set double tap side to flash the backlight or disable completely.
* __Tap__
_on locked_ / _on unlocked_ / __always on__
Select when a double tap is recognised.
* __Timeout__
_0.5s_ / _1s_ / _..._ / __2s__ / _..._ / _10s_
Change how long the backlight will be activated on a flash.
* __Min Value__
_1%_ / _2%_ / _..._ / __20%__ / _..._ / _100%_
Set the minimal level of brightness for the backlight on a flash.
---
### Images
---
| Lightbulb | Default lock icon |
|:-----------------------------:|:-----------------------:|
| ![](images/lightbulb.png) | ![](images/default.png) |
| ( _full_ / _dimmed_ / _off_ ) | ( _on_ / _off_ ) |
Examples in default light and dark theme.
| Lock | Heart | Invader | JS | Smiley | Skull | Storm |
|:----:|:-----:|:-------:|:--:|:------:|:-----:|:-----:|
| ![](images/image_lock.png) | ![](images/image_heart.png) | ![](images/image_invader.png) | ![](images/image_js.png) | ![](images/image_smiley.png) | ![](images/image_skull.png) | ![](images/image_storm.png) |
This images are stored in a seperate file _(lightswitch.images.json)_.
---
### Worth Mentioning
---
#### To do list
* Catch the touch and draw input related to this widget to prevent actions in the active app.
_(For now I have no idea how to achieve this, help is appreciated)_
* Manage images for the lock icon through a _Customize and Upload App_ page.
#### Requests, Bugs and Feedback
Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) or send me a [mail](mailto:banglejs@storm64.de).
#### Thanks
Huge thanks to Gordon Williams and all the motivated developers.
#### Creator
Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
#### License
[MIT License](LICENSE)

17
apps/lightswitch/boot.js Normal file
View File

@ -0,0 +1,17 @@
// load settings
var settings = Object.assign({
value: 1,
isOn: true
}, require("Storage").readJSON("lightswitch.json", true) || {});
// set brightness
Bangle.setLCDBrightness(settings.isOn ? settings.value : 0);
// remove tap listener to prevent uncertainties
Bangle.removeListener("tap", require("lightswitch.js").tapListener);
// add tap listener to unlock and/or flash backlight
if (settings.unlockSide || settings.tapSide) Bangle.on("tap", require("lightswitch.js").tapListener);
// clear variable
settings = undefined;

View File

@ -0,0 +1,37 @@
{
"lock": {
"str": "BQcBAAEYxiA=",
"x": 9,
"y": 15,
},
"heart": {
"str": "CQjBAQD4//+chAAACA4Pj+8=",
"x": 7,
"y": 14,
},
"invader": {
"str": "DQqDASQASQASSEAAECSQEAEASQEkkkAQEgkgkAEkkkkkAgkkkggEEAAEEAAEgkAASQAAASQ=",
"x": 5,
"y": 13,
},
"js": {
"str": "CAqBAd//2NfZ3tHfX78=",
"x": 7,
"y": 13,
},
"skull": {
"str": "CQqBAcHAZTKcH/+OfMGfAA==",
"x": 7,
"y": 13,
},
"smiley": {
"str": "CwqDASQAAASQNtsAQNttsANgMBsBsBgNgNtttsBsNsNgBsANgCBttgCSAAACQA==",
"x": 6,
"y": 13,
},
"storm": {
"str": "CQmDASAAACBttgBgABgBttgCMAACQNsASRgASSBgCSSACSA=",
"x": 7,
"y": 13,
}
}

View File

@ -0,0 +1 @@
# Light Switch Images

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

124
apps/lightswitch/lib.js Normal file
View File

@ -0,0 +1,124 @@
// from boot accassible functions
exports = {
// listener function //
// tap listener to flash backlight
tapListener: function(data) {
// check for double tap and direction
if (data.double) {
// setup shortcut to this widget or load from storage
var w = global.WIDGETS ? WIDGETS.lightswitch : Object.assign({
unlockSide: "",
tapSide: "right",
tapOn: "always",
}, require("Storage").readJSON("lightswitch.json", true) || {});
// cache lock status
var locked = Bangle.isLocked();
// check to unlock
if (locked && data.dir === w.unlockSide) Bangle.setLocked();
// check to flash
if (data.dir === w.tapSide && (w.tapOn === "always" || locked === (w.tapOn === "locked"))) require("lightswitch.js").flash();
// clear variables
w = undefined;
locked = undefined;
}
},
// external function //
// function to flash backlight
flash: function(tOut) {
// setup shortcut to this widget or load from storage
var w = global.WIDGETS ? WIDGETS.lightswitch : Object.assign({
tOut: 3000,
minFlash: 0.2,
value: 1,
isOn: true
}, require("Storage").readJSON("lightswitch.json", true) || {});
// chack if locked, backlight off or actual value lower then minimal flash value
if (Bangle.isLocked() || !w.isOn || w.value < w.minFlash) {
// set inner bulb and brightness
var setBrightness = function(w, value) {
if (w.drawInnerBulb) w.drawInnerBulb(value);
Bangle.setLCDBrightness(value);
};
// override timeout if defined
if (!tOut) tOut = w.tOut;
// check lock state
if (Bangle.isLocked()) {
// cache options
var options = Bangle.getOptions();
// set shortened lock and backlight timeout
Bangle.setOptions({
lockTimeout: tOut,
backlightTimeout: tOut
});
// unlock
Bangle.setLocked(false);
// set timeout to reset options
setTimeout(Bangle.setOptions, tOut + 100, options);
// clear variable
options = undefined;
} else {
// set timeout to reset backlight
setTimeout((w, funct) => {
if (!Bangle.isLocked()) funct(w, w.isOn ? w.value : 0);
}, tOut, w, setBrightness);
}
// enable backlight
setTimeout((w, funct) => {
funct(w, w.value < w.minFlash ? w.minFlash : w.value);
}, 10, w, setBrightness);
// clear variable
setBrightness = undefined;
}
// clear variable
w = undefined;
},
// external access to internal function //
// refference to widget function or set backlight and write to storage if not skipped
changeValue: function(value, skipWrite) {
// check if widgets are loaded
if (global.WIDGETS) {
// execute inside widget
WIDGETS.lightswitch.changeValue(value, skipWrite);
} else {
// load settings from storage
var filename = "lightswitch.json";
var storage = require("Storage");
var settings = Object.assign({
value: 1,
isOn: true
}, storage.readJSON(filename, true) || {});
// check value
if (value) {
// set new value
settings.value = value;
} else {
// switch backlight status
settings.isOn = !settings.isOn;
}
// set brightness
Bangle.setLCDBrightness(settings.isOn ? settings.value : 0);
// write changes to storage if not skipped
if (!skipWrite) storage.writeJSON(filename, settings);
// clear variables
filename = undefined;
storage = undefined;
settings = undefined;
}
}
};

View File

@ -0,0 +1,28 @@
{
"id": "lightswitch",
"name": "Light Switch Widget",
"shortName": "Light Switch",
"version": "0.02",
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
"icon": "images/app.png",
"screenshots": [
{"url": "images/screenshot_1.png"},
{"url": "images/screenshot_2.png"},
{"url": "images/screenshot_3.png"},
{"url": "images/screenshot_4.png"}
],
"type": "widget",
"tags": "tool,widget,brightness,lock",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name": "lightswitch.boot.js", "url": "boot.js"},
{"name": "lightswitch.js", "url": "lib.js"},
{"name": "lightswitch.settings.js", "url": "settings.js"},
{"name": "lightswitch.wid.js", "url": "widget.js"}
],
"data": [
{"name": "lightswitch.json"},
{"name": "lightswitch.images.json", "url": "images.json"}
]
}

View File

@ -0,0 +1,155 @@
(function(back) {
var filename = "lightswitch.json";
// set Storage and load settings
var storage = require("Storage");
var settings = Object.assign({
colors: "011",
image: "default",
touchOn: "clock,launch",
dragDelay: 500,
minValue: 0.1,
unlockSide: "",
tapSide: "right",
tapOn: "always",
tOut: 2000,
minFlash: 0.2
}, storage.readJSON(filename, true) || {});
var images = storage.readJSON(filename.replace(".", ".images."), true) || false;
// write change to storage and widget
function writeSetting(key, value, drawWidgets) {
// reread settings to only change key
settings = Object.assign(settings, storage.readJSON(filename, true) || {});
// change the value of key
settings[key] = value;
// write to storage
storage.writeJSON(filename, settings);
// check if widgets are loaded
if (global.WIDGETS) {
// setup shortcut to the widget
var w = WIDGETS.lightswitch;
// assign changes to widget
w = Object.assign(w, settings);
// redraw widgets if neccessary
if (drawWidgets) Bangle.drawWidgets();
}
}
// generate entry for circulating values
function getEntry(key) {
var entry = entries[key];
// check for existing titles to decide value type
if (entry.value) {
// return entry for string value
return {
value: entry.value.indexOf(settings[key]),
format: v => entry.title ? entry.title[v] : entry.value[v],
onchange: function(v) {
this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v;
writeSetting(key, entry.value[v], entry.drawWidgets);
if (entry.exec) entry.exec(entry.value[v]);
}
};
} else {
// return entry for numerical value
return {
value: settings[key] * entry.factor,
step: entry.step,
format: v => v > 0 ? v + entry.unit : "off",
onchange: function(v) {
this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v;
writeSetting(key, v / entry.factor, entry.drawWidgets);
},
};
}
}
// define menu entries with circulating values
var entries = {
colors: {
title: ["red", "yellow", "green", "cyan", "blue", "magenta"],
value: ["100", "110", "010", "011", "001", "101"],
drawWidgets: true
},
image: {
title: images ? undefined : ["no found"],
value: images ? ["default", "random"].concat(Object.keys(images)) : ["default"],
exec: function(value) {
// draw selected image in upper right corner
var x = 152,
y = 26,
i = images ? images[value] : false;
g.reset();
if (!i) g.setColor(g.theme.bg);
g.drawImage(atob("Dw+BADAYYDDAY//v////////////////////////3/8A"), x + 4, y);
if (i) g.drawImage(atob(i.str), x + i.x, y - 9 + i.y);
i = undefined;
}
},
touchOn: {
title: ["on def clk", "on all clk", "clk+launch", "clk+setting", "except apps", "always on"],
value: ["", "clock", "clock,setting.app.js", "clock,launch", "clock,setting.app.js,launch", "always"],
drawWidgets: true
},
dragDelay: {
factor: 1,
unit: "ms",
min: 0,
max: 1000,
step: 50
},
minValue: {
factor: 100,
unit: "%",
min: 1,
max: 100,
step: 1
},
unlockSide: {
title: ["off", "left", "right", "top", "bottom", "front", "back"],
value: ["", "left", "right", "top", "bottom", "front", "back"]
},
tapOn: {
title: ["on locked", "on unlocked", "always on"],
value: ["locked", "unlocked", "always"]
},
tOut: {
factor: 0.001,
unit: "s",
min: 0.5,
max: 10,
step: 0.5
}
};
// copy duplicated entries
entries.tapSide = entries.unlockSide;
entries.minFlash = entries.minValue;
// show main menu
function showMain() {
var mainMenu = E.showMenu({
"": {
title: "Light Switch"
},
"< Back": () => back(),
"-- Widget --------": 0,
"Bulb col": getEntry("colors"),
"Image": getEntry("image"),
"-- Control -------": 0,
"Touch": getEntry("touchOn"),
"Drag Delay": getEntry("dragDelay"),
"Min Value": getEntry("minValue"),
"-- Unlock --------": 0,
"TapSide": getEntry("unlockSide"),
"-- Flash ---------": 0,
"TapSide ": getEntry("tapSide"),
"Tap": getEntry("tapOn"),
"Timeout": getEntry("tOut"),
"Min Value ": getEntry("minFlash")
});
}
// draw main menu
showMain();
})

View File

@ -0,0 +1,72 @@
/*** Available settings for lightswitch ***
* colors: string // colors used for the bulb
// set with g.setColor(val*col[0], val*col[1], val*col[2])
"100" -> red
"110" -> yellow
"010" -> green
"011" -> cyan (default)
"001" -> blue
"101" -> magenta
* image: string //
"default" ->
"random" ->
* touchOn: string // select when widget touch is active
"" -> only on default clock
"clock" -> on all clocks
"clock,launch" -> on all clocks and lanchers (default)
"always" -> always
* dragDelay: int // drag listener reset time in ms
// time until a drag is needed to activate backlight changing mode
0 -> disabled
500 -> (default)
* minValue: float // minimal brightness level that can be set by dragging
0.05 to 1, 0.1 as default
* unlockSide: string // side of the watch to double tap on to flash backlight
0/false/undefined -> backlight flash disabled
right/left/up/down/front/back -> side to tap on (default: right)
* tapSide: string // side of the watch to double tap on to flash backlight
0/false/undefined -> backlight flash disabled
right/left/up/down/front/back -> side to tap on (default: right)
* tapOn: string // select when tap to flash backlight is active
"locked" -> only when locked
"unlocked" -> only when unlocked (default)
"always" -> always
* tOut: int // backlight flash timeout in ms
3000 (default)
* minFlash: float // minimal brightness level when
0.05 to 1, 0.2 as default
*** Cached values ***
* value: float // active brightness value (0-1)
1 (default)
* isOn: bool // active backlight status
true (default)
*/
{
// settings
"colors": "011",
"image": "default",
"touchOn": "clock,launch",
"dragDelay": 500,
"minValue": 0.1,
"unlockSide": "",
"tapSide": "right",
"tapOn": "always",
"tOut": 2000,
"minFlash": 0.2,
// cached values
"value": 1,
"isOn": true
}

255
apps/lightswitch/widget.js Normal file
View File

@ -0,0 +1,255 @@
(function() {
// load settings
var settings = Object.assign({
colors: "011",
image: "default",
touchOn: "clock,launch",
dragDelay: 500,
minValue: 0.1,
unlockSide: "",
tapSide: "right",
tapOn: "always",
tOut: 3000,
value: 1,
isOn: true
}, require("Storage").readJSON("lightswitch.json", true) || {});
// write widget with loaded settings
WIDGETS.lightswitch = Object.assign(settings, {
// set area, sortorder, width and dragStatus
area: "tr",
sortorder: 10,
width: 23,
dragStatus: "off",
// internal function //
// write settings to storage
writeSettings: function(changes) {
// define variables
var filename = "lightswitch.json";
var storage = require("Storage");
// write changes into json file
storage.writeJSON(filename, Object.assign(
storage.readJSON(filename, true) || {}, changes
));
// clear variables
filename = undefined;
storage = undefined;
},
// internal function //
// draw inner bulb circle
drawInnerBulb: function(value) {
// check if active or value is set
if (value || this.isOn) {
// use set value or load from widget
value = value || this.value;
// calculate color
g.setColor(
value * this.colors[0],
value * this.colors[1],
value * this.colors[2]
);
} else {
// backlight off
g.setColor(0);
}
// draw circle
g.drawImage(atob("CwuBAB8H8f9/////////f8fwfAA="), this.x + 6, this.y + 6);
},
// internal function //
// draw widget icon
drawIcon: function(locked) {
// define icons
var icons = {
bulb: "DxSBAAAAD4BgwYDCAIgAkAEgAkAEgAiAIYDBgwH8A/gH8A/gH8AfABwA",
shine: "FxeBAAgQIAgggBBBABAECAAALAABhAAEAAAAAAAAAAAAAAAHAABwAAAAAAAAAAAAAAAQABDAABoAAAgQBABABACACAIACAA=",
lock: "DxCBAAAAH8B/wMGBgwMGBgwf/H/8+Pnx8/fn78/fn/8f/A==",
image: "DxSBAA/gP+Dg4YDDAYYDDAYYDH/9////////////////////////+//g"
};
// read images
var images = require("Storage").readJSON("lightswitch.images.json", true) || false;
// select image if images are found
var image = (!images || image === "default") ? false :
(function(i) {
if (i === "random") {
i = Object.keys(images);
i = i[parseInt(Math.random() * i.length)];
}
return images[i];
})(this.image);
// clear widget area
g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 24);
// draw shine if backlight is active
if (this.isOn) g.drawImage(atob(icons.shine), this.x, this.y);
// draw icon depending on lock status and image
g.drawImage(atob(!locked ? icons.bulb : image ? icons.image : icons.lock), this.x + 4, this.y + 4);
// draw image on lock
if (locked && image) g.drawImage(atob(image.str), this.x + image.x, this.y + image.y);
// draw bulb color depending on backlight status
if (!locked) this.drawInnerBulb();
// clear variables
icons = undefined;
images = undefined;
image = undefined;
},
// internal function //
// change or switch backlight and icon and write to storage if not skipped
changeValue: function(value, skipWrite) {
// check value
if (value) {
// set new value
this.value = value;
// check backlight status
if (this.isOn) {
// redraw only inner bulb circle
this.drawInnerBulb();
} else {
// activate backlight
this.isOn = true;
// redraw complete widget icon
this.drawIcon(false);
}
} else {
// switch backlight status
this.isOn = !this.isOn;
// redraw widget icon
this.drawIcon(false);
}
// set brightness
Bangle.setLCDBrightness(this.isOn ? this.value : 0);
// write changes to storage if not skipped
if (!skipWrite) this.writeSettings({
isOn: this.isOn,
value: this.value
});
},
// listener function //
// drag listener for brightness change mode
dragListener: function(event) {
// setup shortcut to this widget
var w = WIDGETS.lightswitch;
// first drag recognised
if (event.b && typeof w.dragStatus === "number") {
// reset drag timeout
clearTimeout(w.dragStatus);
// change drag status to indicate ongoing drag action
w.dragStatus = "ongoing";
// feedback for brightness change mode
Bangle.buzz(50);
}
// read y position, pleasant usable area 20-170
var y = event.y;
y = y < 20 ? 0 : y > 170 ? 150 : y - 20;
// calculate brightness respecting minimal value in settings
var value = (1 - Math.round(y / 1.5) / 100) * (1 - w.minValue) + w.minValue;
// change brigthness value, skip write to storage while still touching
w.changeValue(value, event.b);
// on touch release remove drag listener and reset drag status to indicate stopped drag action
if (!event.b) {
Bangle.removeListener("drag", w.dragListener);
w.dragStatus = "off";
}
// clear variables
w = undefined;
y = undefined;
value = undefined;
},
// listener function //
// touch listener for light control
touchListener: function(button, cursor) {
// setup shortcut to this widget
var w = WIDGETS.lightswitch;
// skip all if drag action ongoing
if (w.dragStatus === "off") {
// check if inside widget area
if (!(!w || cursor.x < w.x || cursor.x > w.x + w.width ||
cursor.y < w.y || cursor.y > w.y + 23)) {
// first touch feedback
Bangle.buzz(25);
// check if drag is disabled
if (w.dragDelay) {
// add drag listener
Bangle.on("drag", w.dragListener);
// set drag timeout
w.dragStatus = setTimeout((w) => {
// remove drag listener
Bangle.removeListener("drag", w.dragListener);
// clear drag timeout
if (typeof w.dragStatus === "number") clearTimeout(w.dragStatus);
// reset drag status to indicate stopped drag action
w.dragStatus = "off";
}, w.dragDelay, w);
}
// switch backlight
w.changeValue();
}
}
// clear variable
w = undefined;
},
// main widget function //
// display and setup/reset function
draw: function(locked) {
// setup shortcut to this widget
var w = WIDGETS.lightswitch;
// set lcd brightness on unlocking
// all other cases are catched by the boot file
if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
// read lock status
locked = Bangle.isLocked();
// remove listeners to prevent uncertainties
Bangle.removeListener("lock", w.draw);
Bangle.removeListener("touch", w.touchListener);
Bangle.removeListener("tap", require("lightswitch.js").tapListener);
// draw widget icon
w.drawIcon(locked);
// add lock listener
Bangle.on("lock", w.draw);
// add touch listener to control the light depending on settings
if (w.touchOn === "always" || !global.__FILE__ ||
w.touchOn.includes(__FILE__) ||
w.touchOn.includes(require("Storage").readJSON(__FILE__.replace("app.js", "info")).type))
Bangle.on("touch", w.touchListener);
// add tap listener to unlock and/or flash backlight
if (w.unlockSide || w.tapSide) Bangle.on("tap", require("lightswitch.js").tapListener);
// clear variables
w = undefined;
}
});
// clear variable
settings = undefined;
})()

3
apps/notanalog/ChangeLog Normal file
View File

@ -0,0 +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.

33
apps/notanalog/README.md Normal file
View File

@ -0,0 +1,33 @@
# 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 (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.
- 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.
## Screenshots
![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_3.png)
# Thanks
Thanks to the multiclock from https://github.com/jeffmer/BangleApps/
which helped a lot for this development.
Icons from <a href="https://www.flaticon.com/free-icons" title="icons">by Freepik - Flaticon</a>
## Contributors
- [David Peer](https://github.com/peerdavid).

View File

@ -0,0 +1,20 @@
{
"id": "notanalog",
"name": "Not Analog",
"shortName":"Not Analog",
"icon": "notanalog.png",
"version":"0.03",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "An analog watch face for people that can not read analog watch faces.",
"type": "clock",
"tags": "clock",
"screenshots": [
{"url":"screenshot_1.png"},
{"url":"screenshot_2.png"}
],
"storage": [
{"name":"notanalog.app.js","url":"notanalog.app.js"},
{"name":"notanalog.img","url":"notanalog.icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,474 @@
/**
* NOT ANALOG CLOCK
*/
const locale = require('locale');
const storage = require('Storage')
const SETTINGS_FILE = "notanalog.setting.json";
let settings = {
alarm: -1,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
/*
* Set some important constants such as width, height and center
*/
var W = g.getWidth(),R=W/2;
var H = g.getHeight();
var cx = W/2;
var cy = H/2;
var drawTimeout;
var state = {
color: "#ff0000",
steps: 0,
maxSteps: 10000,
bat: 0,
has_weather: false,
temp: "-",
sleep: false,
}
var chargeImg = {
width : 32, height : 32, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob("AAAMAAAAHgAAADMAAABjAAAAxgAAD44AAB8cAAA7uAAAcfAMAODgPgDAcHMBgDjjAYAdxgGBj4wBg8cYAYZjsAGGYeABg8DgAQGAYAMAAOAHgAHAB8ADgAzgBwAYc/4AGD/4AAw4AAAOcAAAH8AAADmAAABwAAAA4AAAAMAAAAA="))
};
var alarmImg = {
width : 32, height : 32, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob("AA/wAAAP8AAAD/AAAAGAAAABgAAAA8AABh/4YAd//uAH+B/gA+AHwAOAAcAHAPDgDgD4cA4A/HAcAP44HAD+OBwA/zgYAP8YGAD/GBj//xgc//84HH/+OBx//jgOP/xwDh/4cAcP8OAHg8HgA8ADwAHwD4AA//8AAD/8AAAP8AA="))
};
var stepsImg = {
width : 32, height : 32, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob("AcAAAAPwAAAH8AAAB/gAAAf4AAAH/AAAD/wAAAf8AAAH/AfAB/wP4Af8H+AH/B/gB/wf4AP8P+AD+D/gAfg/4AGAP+AAPD/gAPw/4AD+P+AAfj/AAH4/wAB+H8AAPAeAAAAwAAAAPgAAAH8AAAB/AAAAfgAAAH4AAAA8AAAAOAA="))
};
var gpsImg = {
width : 32, height : 32, bpp : 1,
transparent : 0,
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/
*/
Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
angle = angle % 360;
var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
{x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
};
// The following font was used:
// <link href="https://fonts.googleapis.com/css2?family=Staatliches&display=swap" rel="stylesheet">
Graphics.prototype.setTimeFont = function(scale) {
// Actual height 26 (26 - 1)
this.setFontCustom(atob("AAAAAAAAAD4AAAAD4AAAAD4AAAAD4AAAAB4AAAAAAAAAAAAAAAAD4AAAD/4AAD//4AD///4Af///gAf//gAAf/gAAAfwAAAAQAAAAAAAAAAAAAAAAAB//+AAH///gAP///wAP///wAfwAP4AfAAD4AfAAD4AfAAD4AfAAD4AfAAD4AfgAH4AP///wAP///wAH///gAB//+AAAP/wAAAAAAAAAAAAAAf///4Af///4Af///4Af///4Af///4AAAAAAAAAAAAAAAAAAAB+AD4AH+AP4AP+Af4AP+B/4AfwD/4AfAH/4AfAf74AfA/z4AfD/j4Af/+D4AP/8D4AH/wD4AD/gD4AB+AD4AAAAAAAB8B8AAD8B/AAH8B/gAP8B/wAf8A/4AfAAD4AfB8D4AfB8D4AfD8D4Af3/P4AP///wAP///wAH///gAB/H+AAAAAAAAAAAAAAAAB+AAAAP+AAAA/+AAAD/+AAAP/+AAA/8+AAD/w+AAf/A+AAf///4Af///4Af///4Af///4AD///4AAAA+AAAAA+AAAAAAAAAAAYAAf/8/AAf/8/gAf/8/wAf/8/wAfD4H4AfD4D4AfD4D4AfD4D4AfD8H4AfD//4AfB//wAfA//gAeAf/AAAAH8AAAAAAAAAAAAAAB//+AAD///AAH///gAP///wAf///4AfD4D4AfD4D4AfD4D4AfD4D4Afz+P4AP5//wAP5//wAH4//gAB4P+AAAAAAAAAAAAAAfAAAAAfAAAAAfAAAIAfAAB4AfAAP4AfAB/4AfAP/4AfA//4AfH//AAf//4AAf/+AAAf/wAAAf+AAAAfwAAAAAAAAAAAAAAAAB8P+AAH///gAP///wAP///wAf/+P4AfH4D4AfD4D4AfD4D4Afn8D4Af///4AP///wAH///gAD/f/AAA4P+AAAAAAAAAAAQAAB/w+AAH/4/gAP/8/wAP/+/wAfh+P4AfA/D4AfAfD4AfAfD4AfA+H4Af///4AP///wAH///gAD///AAA//8AAAAAAAAAAAAAAAB8D4AAB8D4AAB8D4AAB8D4AAA8B4AAAAAAA=="), 46, atob("BwsSCBAQEBAQEBAQBw=="), 36+(scale<<8)+(1<<16));
return this;
};
Graphics.prototype.setNormalFont = function(scale) {
// Actual height 19 (18 - 0)
this.setFontCustom(atob("AAAAAAAAAAAAAAD/5wP/3A/+cAAAAPwAA/AADAAAAgAA/AAD8AAIAAAAAAAxgADGAA/+AD/4ADGAAMYAD/4AP/gAMYAAxgAB84AP7wD3jwPHPA+f8A+/AB54AAAAB+AAP8BAwwcDnHwP8/AfPwAD8AA/AAPxwB+fgPh/A4GMCAfwAA+AAAAA+/AH/+A//8DjhwOOHA4/8Dj/wAP/AA4AADgAAAAAAAAD8AAPwAAwAAAAAAB/4Af/4D//wPAPA4AcDgBwAAAAAAADgBwOAHA//8B//gD/8AD/AAoAAGwAA/gAD+AAHwAAbAAAAAAAAAAAAAABgAAGAAAYAA/+AD/4AAYAABgAAGAAAYAAAByAAH4AAfAAAAAGAAA4AADgAAOAAA4AAAAAAAAAAAAAABwAAHAAAcAAAAAA/AD/8D//gP+AA8AAAAAAD/8Af/4D//wOAHA4AcDgBwPAPA//8B//gB/4AAAAAAAAP//A//8D//wAAAADAMA8DwHw/A+H8Dg/wOP3A/8cB/hwD4HAAAAAYEAHw8AfD4D4HwOOHA44cD//wH/+APvwAAAAAB4AAfgAH+AB/4AfjgD//wP//A//8AAOAAA4AAAAD/vAP++A/58DnBwOcHA5/8Dj/gOH8AAHAA//AH/+A//8DnhwOcHA548D7/wHv+AOPgAAAAOAAA4AADgBwOA/A4f8Dv/AP/gA/wAD4AAAAAAACAA9/AH/+A//8DnBwOcHA//8B//gD38AAHAA/HAH++A/98DhzwOHHA4c8D//wH/+AP/wAAAAAAAAA4cADhwAOHAA4cgDh+AOHwAAAABgAAPAAB8AAH4AA5wAHDgAYGAAAQAAAAAGYAAZgABmAAGYAAZgABmAAGYAAZgABmAAAAAAAQAGDgAcOAA5wAB+AAHwAAPAAAYAAAAADwAAfgAD8AAOD3A4fcDz9wP+AAfwAAcAAAAAAP/wB//gP//A4A8Dn5wOf3A5/cD/9wH/3AP+cAABwAAAAAH8AP/wP//A/84D/zgP//AH/8AAfwAABAAAAD//wP//A//8DjhwOOHA488D//wH/+APngA//AH/+A//8DgBwOAHA8A8D8PwHw+AHDgAAAAAAAA//8D//wP//A4AcDgBwPAPA//8B//gD/4AAAAD//wP//A//8DjhwOOHA44cDjhwOOHAAAAD//wP//A//8DjgAOOAA44ADjgAOOAAP/wB//gP//A4AcDhxwOHPA/f8B9/gDn4AAAAAAAAP//A//8D//wAOAAA4AADgAP//A//8D//wAAAA//8D//wP//AAAAAAPAAA+AAD8AABwAAHAAA8D//wP/+A//gAAAAAAAA//8D//wP//AD8AA/8AH/8A+H8DgHwIAHAAAAD//wP//A//8AABwAAHAAAcAABwAAHAAAAD//wP//A//8D8AAD4AAPgAB8AAP//A//8D//wAAAAAAAD//wP//Af/8Af4AAf4A//4D//wP//AAAAA//AH/+A//8DgBwOAHA4A8D//wH/+AP/wAAAAAAAA//8D//wP//A4cADhwAOPAA/8AB/gAD4AAP/wB//gP//A4AcDgBwPAPA//8B//4D//gAAEAAAAP//A//8D//wOOAA44ADjwAP//Af/8A+fwAAAAPjwB/PgP+/A48cDhxwPn/Afv4B+fgBw4AAAADgAAOAAA4AAD//wP//A//8DgAAOAAA4AAD//AP/+A//8AABwAAHAAA8D//wP/+A//wAAAAPAAA/4AD//gA//AAP8Af/wP/8A/4ADgAAAAAA4AAD/gAP//AH/8AB/wP//A//AD//gB//AAP8D//wP/4A/gACAAAOAHA/D8D//wB/4AH/gD//wPx/A4AcAAAAMAAA+AAD/AAD//AB/8B//wP8AA+AADAAAOAfA4H8Dh/wOf3A/8cD/BwPwHA8AcAAAAP//A//8D//wOAHA4AcDgBwAAAAAAAD8AAP/wAf/8AD/wAAPAAAAAAAAOAHA4AcDgBwP//A//8D//wA=="), 32, atob("AwQJCggPCwUHBwgKBAcEBwsFCgoKCgoKCgoEBAkKCQoMCQoKCgkJCgoFCgoJDAoKCgoLCgkKCg4JCQkHBwc="), 22+(scale<<8)+(1<<16));
return this;
};
function getSteps() {
try{
if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom.getSteps();
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
}
return 0;
}
function drawBackground() {
g.setFontAlign(0,0,0);
g.setNormalFont();
g.setColor(g.theme.fg);
for (let a=0;a<360;a+=6){
if (a % 30 == 0 || (a > 345 || a < 15) || (a > 90-15 && a < 90+15) || (a > 180-15 && a < 180+15) || (a > 270-15 && a < 270+15)) {
continue;
}
var theta=a*Math.PI/180;
g.drawLine(cx,cy,cx+125*Math.sin(theta),cy-125*Math.cos(theta));
}
g.clearRect(10,10,W-10,H-10);
for (let a=0;a<360;a+=30){
if(a == 0 || a == 90 || a == 180 || a == 270){
continue;
}
g.drawRotRect(6,R-80,125,a);
}
g.clearRect(16,16,W-16,H-16);
}
function drawState(){
g.setFontAlign(1,0,0);
// Draw alarm
var highPrioImg = isAlarmEnabled() ? alarmImg :
Bangle.isCharging() ? chargeImg :
Bangle.isGPSOn() ? gpsImg :
undefined;
var imgColor = isAlarmEnabled() ? state.color :
Bangle.isCharging() ? g.theme.fg :
Bangle.isGPSOn() ? g.theme.fg :
state.color;
// As default, we draw weather if available, otherwise the steps symbol is shown.
if(!highPrioImg && state.has_weather){
g.setColor(g.theme.fg);
g.drawString(state.temp, cx+cx/2+15, cy+cy/2+10);
} else {
g.setColor(imgColor);
var img = highPrioImg ? highPrioImg : stepsImg;
g.drawImage(img, cx+cx/2 - img.width/2 + 5, cy+cy/2 - img.height/2+5);
}
}
function drawData() {
g.setFontAlign(0,0,0);
g.setNormalFont();
// Set hand functions
var drawBatteryHand = g.drawRotRect.bind(g,6,12,R-38);
var drawDataHand = g.drawRotRect.bind(g,5,12,R-24);
// Draw battery hand
g.setColor(g.theme.fg);
g.setFontAlign(0,0,0);
drawBatteryHand(parseInt(state.bat*360/100));
// Draw data hand - depending on state
g.setColor(state.color);
if(isAlarmEnabled()){
var alrm = getAlarmMinutes();
drawDataHand(parseInt(alrm*360/60));
return;
}
// Default are the steps
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);
}
function drawTime(){
g.setTimeFont();
g.setFontAlign(0,0,0);
g.setColor(g.theme.fg);
var posX = 14;
var posY = 14;
// Hour
var h = state.currentDate.getHours();
var h1 = parseInt(h / 10);
var h2 = h < 10 ? h : h - h1*10;
drawTextCleared(h1, cx, posY+8);
drawTextCleared(h2, W-posX, cy+5);
// Minutes
var m = state.currentDate.getMinutes();
var m1 = parseInt(m / 10);
var m2 = m < 10 ? m : m - m1*10;
drawTextCleared(m2, cx, H-posY);
drawTextCleared(m1, posX-1, cy+5);
}
function drawDate(){
// Date
g.setFontAlign(-1,0,0);
g.setNormalFont();
g.setColor(g.theme.fg);
var dayStr = locale.dow(state.currentDate, true).toUpperCase();
g.drawString(dayStr, cx/2-15, cy/2-5);
g.drawString(state.currentDate.getDate(), cx/2-15, cy/2+17);
}
function drawLock(){
g.setColor(g.theme.fg);
g.fillCircle(cx, cy, 7);
var c = Bangle.isLocked() ? state.color : g.theme.bg;
g.setColor(c);
g.fillCircle(cx, cy, 4);
}
function handleState(fastUpdate){
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";
/*
* 5 Minute updates
*/
if(minutes % 5 == 0 && fastUpdate){
return;
}
// Set battery
state.bat = E.getBattery();
// Set weather
state.has_weather = true;
try {
weather = require('weather').get();
if (weather === undefined){
state.has_weather = false;
state.temp = "-";
} else {
state.temp = locale.temp(Math.round(weather.temp-273.15));
}
} 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);
}
function draw(fastUpdate){
// Execute handlers
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;
g.clearRect(innerRect, innerRect, g.getWidth()-innerRect, g.getHeight()-innerRect);
} else {
g.reset();
g.clearRect(0, 0, g.getWidth(), g.getHeight());
}
// Draw again
g.setColor(1,1,1);
if(!fastUpdate){
drawBackground();
}
drawDate();
drawLock();
drawState();
drawTime();
drawData();
// Queue draw in one minute
queueDraw();
}
/*
* Listeners
*/
Bangle.on('lcdPower',on=>{
if (on) {
draw(true);
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.on('charging',function(charging) {
draw(true);
});
Bangle.on('lock', function(isLocked) {
if(state.sleep){
state.sleep=false;
draw(false);
} else {
drawLock();
}
});
Bangle.on('touch', function(btn, e){
var upper = parseInt(g.getHeight() * 0.2);
var lower = g.getHeight() - upper;
var is_upper = e.y < upper;
var is_lower = e.y > lower;
if(is_upper){
feedback();
increaseAlarm();
draw(true);
}
if(is_lower){
feedback();
decreaseAlarm();
draw(true);
}
});
/*
* Some helpers
*/
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw(true);
}, 60000 - (Date.now() % 60000));
}
/*
* Handle alarm
*/
function getCurrentTimeInMinutes(){
return Math.floor(Date.now() / (1000*60));
}
function isAlarmEnabled(){
return settings.alarm >= 0;
}
function getAlarmMinutes(){
var currentTime = getCurrentTimeInMinutes();
return settings.alarm - currentTime;
}
function handleAlarm(){
if(!isAlarmEnabled()){
return;
}
if(getAlarmMinutes() > 0){
return;
}
// Alarm
var t = 300;
Bangle.buzz(t, 1)
.then(() => new Promise(resolve => setTimeout(resolve, t)))
.then(() => Bangle.buzz(t, 1))
.then(() => new Promise(resolve => setTimeout(resolve, t)))
.then(() => Bangle.buzz(t, 1))
.then(() => new Promise(resolve => setTimeout(resolve, t)))
.then(() => Bangle.buzz(t, 1))
.then(() => new Promise(resolve => setTimeout(resolve, 5E3)))
.then(() => {
// Update alarm state to disabled
settings.alarm = -1;
storage.writeJSON(SETTINGS_FILE, settings);
});
}
function increaseAlarm(){
if(isAlarmEnabled()){
settings.alarm += 5;
} else {
settings.alarm = getCurrentTimeInMinutes() + 5;
}
storage.writeJSON(SETTINGS_FILE, settings);
}
function decreaseAlarm(){
if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){
settings.alarm -= 5;
} else {
settings.alarm = -1;
}
storage.writeJSON(SETTINGS_FILE, settings);
}
function feedback(){
Bangle.buzz(40, 0.6);
}
/*
* Lets start widgets, listen for btn etc.
*/
// Show launcher when middle button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
// Clear the screen once, at startup and draw clock
// g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
draw(false);
// After drawing the watch face, we can draw the widgets
// Bangle.drawWidgets();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkE/4AM+cikMRkU/CZoWDkMAgMQgESDB4WBgMSkcykMQDB8xgMjAwcyiETFxoPHD4IwMBxAgBG4gLFCYMgHxExgQXI+USEoMvBhBIJ+URmQMJERQKBiI8BmQZHKRJTBgETmURC48xC5PxgERaBPxga9KgDnJ+KQJKYJFIQoQXKOhAvK+cRgBeBC5ZfF+QVBAAacKBQgWGAALNIX4iJCAA0Bd5kwCw4ABWw3ygJrC+YWJAAJeGRwboBIQhMFj5GFLwcgCAoeFW4kxIwf/IAoXGgARCmQuEUgwXHiczCwMCFwfwC5sBfIMRYwilGC5MSkaTEagwXImbbGC54WGRwwXIbIwXh+YXVh6YHC453GN4IwFO5AXGJAIwFgQXHHwwwHgYXH+AXGGAxnBAAyfHGAwdBAAyfHCQaOKAAMgGBEhOxRIKGYoAJC5YWKVJClLbRjsJAAvyC48vC5v/mJ0RYgyiCiU/CyAASA=="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

View File

@ -28,7 +28,24 @@ so if you have no GPS lock you just need to wait.
However you can just install the `Recorder` app, turn recording on in However you can just install the `Recorder` app, turn recording on in
that, and then start the `Run` app. that, and then start the `Run` app.
## Settings
Under `Settings` -> `App` -> `Run` you can change settings for this app.
* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon
* `Box 1/2/3/4/5/6` are what should be shown in each of the 6 boxes on the display. From the top left, down.
If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information
if you wish by setting the last 2 boxes to `-`.
## TODO ## TODO
* Allow this app to trigger the `Recorder` app on and off directly. * Allow this app to trigger the `Recorder` app on and off directly.
* Keep a log of each run's stats (distance/steps/etc) * Keep a log of each run's stats (distance/steps/etc)
## Development
This app uses the [`exstats` module](/modules/exstats.js). When uploaded via the
app loader, the module is automatically included in the app's source. However
when developing via the IDE the module won't get pulled in by default.
There are some options to fix this easily - please check out the [modules README.md file](/modules/README.md)

View File

@ -1,67 +1,37 @@
var ExStats = require("exstats");
var B2 = process.env.HWVERSION==2; var B2 = process.env.HWVERSION==2;
var Layout = require("Layout"); var Layout = require("Layout");
var locale = require("locale"); var locale = require("locale");
var fontHeading = "6x8:2"; var fontHeading = "6x8:2";
var fontValue = B2 ? "6x15:2" : "6x8:3"; var fontValue = B2 ? "6x15:2" : "6x8:3";
var headingCol = "#888"; var headingCol = "#888";
var running = false; var fixCount = 0;
var startTime;
var startSteps;
// This & previous GPS readings
var lastGPS, thisGPS;
var distance = 0; ///< distance in meters
var startSteps = Bangle.getStepCount(); ///< number of steps when we started
var lastStepCount = startSteps; // last time 'step' was called
var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute)
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
// --------------------------- // ---------------------------
let settings = Object.assign({
function formatTime(ms) { B1 : "dist",
let hrs = Math.floor(ms/3600000).toString(); B2 : "time",
let mins = (Math.floor(ms/60000)%60).toString(); B3 : "pacea",
let secs = (Math.floor(ms/1000)%60).toString(); B4 : "bpm",
B5 : "step",
if (hrs === '0') B6 : "caden",
return mins.padStart(2,0)+":"+secs.padStart(2,0); paceLength : 1000
else }, require("Storage").readJSON("run.json", 1) || {});
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!="");
} var exs = ExStats.getStats(statIDs, settings);
// Format speed in meters/second
function formatPace(speed) {
if (speed < 0.1667) {
return `__:__`;
}
const pace = Math.round(1000 / speed); // seconds for 1km
const min = Math.floor(pace / 60); // minutes for 1km
const sec = pace % 60;
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
// --------------------------- // ---------------------------
function clearState() { // Called to start/stop running
distance = 0;
startSteps = Bangle.getStepCount();
stepHistory.fill(0);
layout.dist.label=locale.distance(distance);
layout.time.label="00:00";
layout.pace.label=formatPace(0);
layout.hrm.label="--";
layout.steps.label=0;
layout.cadence.label= "0";
layout.status.bgCol = "#f00";
}
function onStartStop() { function onStartStop() {
running = !running; var running = !exs.state.active;
if (running) { if (running) {
clearState(); exs.start();
startTime = Date.now(); } else {
exs.stop();
} }
layout.button.label = running ? "STOP" : "START"; layout.button.label = running ? "STOP" : "START";
layout.status.label = running ? "RUN" : "STOP"; layout.status.label = running ? "RUN" : "STOP";
@ -71,94 +41,44 @@ function onStartStop() {
layout.render(); layout.render();
} }
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( { var layout = new Layout( {
type:"v", c: [ type:"v", c: lc
{ 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 }
]}, { 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 }
]},
]
},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]}); },{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]});
clearState(); delete lc;
layout.render(); layout.render();
// Handle GPS state change for icon
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;
}
Bangle.on("GPS", function(fix) { Bangle.on("GPS", function(fix) {
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
lastGPS = thisGPS; if (!fix.fix) return; // only process actual fixes
thisGPS = fix; if (fixCount++ == 0) {
if (running && fix.fix && lastGPS.fix) { Bangle.buzz(); // first fix, does not need to respect quiet mode
// work out distance - moving from a to b
var a = Bangle.project(lastGPS);
var b = Bangle.project(thisGPS);
var dx = a.x-b.x, dy = a.y-b.y;
var d = Math.sqrt(dx*dx+dy*dy); // this should be the distance in meters
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);
} }
}); });
Bangle.on("HRM", function(h) { // We always call ourselves once a second to update
layout.hrm.label = h.bpm; setInterval(function() {
}); layout.clock.label = locale.time(new Date(),1);
Bangle.on("step", function(steps) { layout.render();
if (running) { }, 1000);
layout.steps.label = steps-Bangle.getStepCount();
stepHistory[0] += steps-lastStepCount;
}
lastStepCount = steps;
});
let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true};
// We always call ourselves once a second, if only to update the time
setInterval(onTimer, 1000);
/* Turn GPS and HRM on right at the start to ensure
we get the highest chance of a lock. */
if (settings.use_hrm) Bangle.setHRMPower(true,"app");
if (settings.use_gps) Bangle.setGPSPower(true,"app");

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More