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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
(() => {
(() => {
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDTimeout(0);
let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {};
@ -6,7 +7,9 @@
// density, elasticity of bounces, "drag coefficient"
const rho = 100, e = 0.3, C = 0.01;
// 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)
const G = 9.80665;
@ -17,14 +20,16 @@
// The play area is 240x160, sizes are the ball radius, so we can use common
// denominators of 120x80 to get square rooms
// 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 = {
1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large",
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
* @param draw {function} Callback which performs the drawing
@ -45,17 +50,17 @@
// use unbuffered graphics for UI stuff
function showMessage(message, title) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMessage(message, title);
}
function showPrompt(prompt, options) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showPrompt(prompt, options);
}
function showMenu(menu) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMenu(menu);
}
@ -105,7 +110,7 @@
generateMaze(); // this shows unbuffered progress messages
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
Bangle.setLCDMode("doublebuffered");
if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
clearAll();
drawAll(drawMaze);
intervalID = setInterval(tick, 100);
@ -307,6 +312,7 @@
const range = {top: 0, left: 0, bottom: rows, right: cols};
const w = sW/cols, h = sH/rows;
g.clear();
if (BANGLEJS2) g.setBgColor(bgColour);
g.setColor(0.76, 0.60, 0.42);
for(let row = range.top; row<=range.bottom; row++) {
for(let col = range.left; col<=range.right; col++) {

View File

@ -1,2 +1,3 @@
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.setLCDMode("doublebuffered");
if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
Bangle.setLCDTimeout(0);
let points = 0;
let level = 1;
let levelSpeedStart = 0.8;
let nextLevelPoints = 20;
let nextLevelPoints = 10;
let levelSpeedFactor = 0.2;
let counterWidth = 10;
let gWidth = g.getWidth() - counterWidth;
@ -81,12 +82,23 @@ function drawLevelText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
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() {
//bg
g.setColor("#71c6cf");
if (!BANGLEJS2) {
g.setColor("#71c6cf");
} else {
g.setColor("#002000");
}
g.fillRect(0, 0, g.getWidth(), g.getHeight());
//counter
@ -94,6 +106,7 @@ function draw() {
//draw level
drawLevelText();
drawPointsText();
//dot
g.setColor("#ff0000");
@ -152,7 +165,7 @@ function count() {
if (counter <= 0) {
running = false;
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",
"name": "Balltastic",
"version": "0.02",
"version": "0.03",
"description": "Simple but fun ball eats dots game.",
"icon": "app.png",
"screenshots": [{"url":"bangle2-balltastic-screenshot.png"}],
"type": "app",
"tags": "game,fun",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"balltastic.app.js","url":"app.js"},
{"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
Mark events with src property
Show actual source of event in app
0.04: Automatically reconnect BT sensor
App buzzes if no BTHRM events for more than 3 seconds

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# Circles clock
A clock with circles for different data at the bottom in a probably familiar style
A clock with three or four circles for different data at the bottom in a probably familiar style
By default the time, date and day of week is shown.
@ -18,6 +18,8 @@ It can show the following information (this can be configured):
## Screenshots
![Screenshot dark theme](screenshot-dark.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
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 sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY"));
let settings;
function loadSettings() {
settings = storage.readJSON("circlesclock.json", 1) || {
'minHR': 40,
'maxHR': 200,
'stepGoal': 10000,
'stepDistanceGoal': 8000,
'stepLength': 0.8,
'batteryWarn': 30,
'showWidgets': false,
'weatherCircleData': 'humidity',
'circle1': 'hr',
'circle2': 'steps',
'circle3': 'battery'
};
// Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
const d = require('Storage').readJSON("wpedom.json", 1) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
let settings = storage.readJSON("circlesclock.json", 1) || {
'minHR': 40,
'maxHR': 200,
'confidence': 0,
'stepGoal': 10000,
'stepDistanceGoal': 8000,
'stepLength': 0.8,
'batteryWarn': 30,
'showWidgets': false,
'weatherCircleData': 'humidity',
'circleCount': 3,
'circle1': 'hr',
'circle2': 'steps',
'circle3': 'battery',
'circle4': 'weather'
};
// Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
const d = require('Storage').readJSON("wpedom.json", 1) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
loadSettings();
/*
* Read location from myLocation app
@ -57,6 +54,7 @@ function getLocation() {
let location = getLocation();
const showWidgets = settings.showWidgets || false;
const circleCount = settings.circleCount || 3;
let hrtValue;
let now = Math.round(new Date().getTime() / 1000);
@ -77,11 +75,33 @@ const hOffset = 30 - widgetOffset;
const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions
const radiusOuter = 25;
const radiusInner = 20;
const circleFont = "Vector:15";
const circleFontBig = "Vector:16";
/*
* circle x positions
* depending on circleCount
*
* | 1 2 3 4 5 6 |
* | (1) (2) (3) |
* => circles start at 1,3,5 / 6
*
* | 1 2 3 4 5 6 7 8 |
* | (1) (2) (3) (4) |
* => circles start at 1,3,5,7 / 8
*/
const parts = circleCount * 2;
const circlePosX = [
Math.round(1 * w / parts), // circle1
Math.round(3 * w / parts), // circle2
Math.round(5 * w / parts), // circle3
Math.round(7 * w / parts), // circle4
];
const radiusOuter = circleCount == 3 ? 25 : 20;
const radiusInner = circleCount == 3 ? 20 : 15;
const circleFont = circleCount == 3 ? "Vector:15" : "Vector:12";
const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:13";
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
function draw() {
g.clear(true);
@ -121,10 +141,9 @@ function draw() {
drawCircle(1);
drawCircle(2);
drawCircle(3);
if (circleCount >= 4) drawCircle(4);
}
const defaultCircleTypes = ["steps", "hr", "battery"];
function drawCircle(index) {
let type = settings['circle' + index];
if (!type) type = defaultCircleTypes[index - 1];
@ -146,6 +165,7 @@ function drawCircle(index) {
drawWeather(w);
break;
case "sunprogress":
case "sunProgress":
drawSunProgress(w);
break;
case "empty":
@ -168,7 +188,7 @@ function getCirclePosition(type) {
if (circlePositionsCache[type] >= 0) {
return circlePosX[circlePositionsCache[type]];
}
for (let i = 1; i <= 3; i++) {
for (let i = 1; i <= circleCount; i++) {
const setting = settings['circle' + i];
if (setting == type) {
circlePositionsCache[type] = i - 1;
@ -318,6 +338,8 @@ function drawWeather(w) {
if (code > 0) {
const icon = getWeatherIconByCode(code);
if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10);
} else {
g.drawString("?", w, h3 + radiusOuter);
}
}
@ -599,9 +621,11 @@ Bangle.on('lock', function(isLocked) {
Bangle.on('HRM', function(hrm) {
if (isCircleEnabled("hr")) {
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
drawHeartRate();
if (hrm.confidence >= (settings.confidence || 0)) {
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
drawHeartRate();
}
}
});

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,28 @@
(function(){
var settings = require("Storage").readJSON("health.json",1)||{};
var hrm = 0|settings.hrm;
if (hrm==1) {
function onHealth() {
Bangle.setHRMPower(1, "health");
setTimeout(()=>Bangle.setHRMPower(0, "health"),2*60000); // give it 2 minutes
var settings = require("Storage").readJSON("health.json",1)||{};
var hrm = 0|settings.hrm;
if (hrm == 1 || hrm == 2) {
function onHealth() {
Bangle.setHRMPower(1, "health");
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 => {
if (h.confidence>80) Bangle.setHRMPower(0, "health");
});
if (Bangle.getHealthStatus().bpmConfidence) return;
onHealth();
} else Bangle.setHRMPower(hrm!=0, "health");
}
Bangle.on("health", onHealth);
Bangle.on('HRM', h => {
if (h.confidence>80) Bangle.setHRMPower(0, "health");
});
if (Bangle.getHealthStatus().bpmConfidence) return;
onHealth();
} else Bangle.setHRMPower(hrm!=0, "health");
})();
Bangle.on("health", health => {

View File

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

View File

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

View File

@ -6,3 +6,4 @@
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.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');
var settings = storage.readJSON('hourstrike.json', 1);
const chimes = ["Buzz", "Beep"];
function updateSettings() {
storage.write('hourstrike.json', settings);
@ -26,6 +27,12 @@ function showMainMenu() {
mainmenu.Strength = {
value: settings.vlevel*10, min: 1, max: 10, format: v=>v/10,
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();
return E.showMenu(mainmenu);
}

View File

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

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",
"name": "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!",
"icon": "app-icon.png",
"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.03: Move to Bangle.setUI to launcher support
0.04: Tweaks for compatibility with BangleJS2
0.05: Time-word now readable on Bangle.js 2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,9 @@ Access different screens via tap on the left/ right side of the screen
![](screenshot_2.png)
# Ideas
- Tap top / bottom to disable steps (also icon) and start a timer
## Contributors
- [David Peer](https://github.com/peerdavid).
- [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.02: Set pace format to mm:ss, time format to h:mm:ss,
added settings to opt out of GPS and HRM
0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2
0.04: Use the exstats module, and make what is displayed configurable

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

View File

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

View File

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

View File

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

View File

@ -2,4 +2,5 @@
0.02: Corrected variable initialisation
0.03: Advertise app name, added screenshots
0.04: Advertise bar, GPS, HRM and mag services
0.05: Refactored for efficiency, corrected sensor value inaccuracies
0.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
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
@ -22,7 +24,7 @@ in the menu display, and broadcasts all sensor data readings _except_ accelerati
## Controls
Browse and control sensors using the standard Espruino menu interface.
Browse and control sensors using the standard Espruino menu interface. By default, all sensors _except_ the accelerometer are disabled. Sensors can be individually enabled/disabled via the menu. These settings are written to persistent storage (flash) and will be applied each time the SensiBLE app is loaded.
## Requests

View File

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

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

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.05: Decided against custom font as it inceases the code size
minimalism is useful when narrowing down issues
0.06: renamed some files

View File

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

View File

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

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