Merge branch 'master' into split-message-library
commit
20c153033e
|
|
@ -1,6 +1,11 @@
|
|||
# Active Pedometer
|
||||
|
||||
Pedometer that filters out arm movement and displays a step goal progress.
|
||||
|
||||
**Note:** Since creation of this app, Bangle.js's step counting algorithm has
|
||||
improved significantly - and as a result the algorithm in this app (which
|
||||
runs *on top* of Bangle.js's algorithm) may no longer be accurate.
|
||||
|
||||
I changed the step counting algorithm completely.
|
||||
Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long.
|
||||
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
|
||||
|
|
@ -9,6 +14,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c
|
|||
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
|
||||
|
||||
## Screenshots
|
||||
|
||||
* 600 steps
|
||||

|
||||
|
||||
|
|
@ -70,4 +76,4 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
|
|||
|
||||
## Requests
|
||||
|
||||
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
|
||||
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Active Pedometer",
|
||||
"shortName": "Active Pedometer",
|
||||
"version": "0.09",
|
||||
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
|
||||
"description": "(NOT RECOMMENDED) Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph. The `Health` app now provides step logging and graphs.",
|
||||
"icon": "app.png",
|
||||
"tags": "outdoors,widget",
|
||||
"supports": ["BANGLEJS"],
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@
|
|||
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
|
||||
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
|
||||
0.18: Use new message library
|
||||
If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
|
||||
|
|
@ -20,6 +20,8 @@ It contains:
|
|||
of Gadgetbridge - making your phone make noise so you can find it.
|
||||
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
|
||||
keep any messages it has received, or should it delete them?
|
||||
* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS
|
||||
but instead asks Gadgetbridge on the phone to use the phone's GPS
|
||||
* `Messages` - launches the messages app, showing a list of messages
|
||||
|
||||
## How it works
|
||||
|
|
|
|||
|
|
@ -126,6 +126,18 @@
|
|||
request.j(event.err); //r = reJect function
|
||||
else
|
||||
request.r(event); //r = resolve function
|
||||
},
|
||||
"gps": function() {
|
||||
const settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||
if (!settings.overwriteGps) return;
|
||||
delete event.t;
|
||||
event.satellites = NaN;
|
||||
event.course = NaN;
|
||||
event.fix = 1;
|
||||
Bangle.emit('gps', event);
|
||||
},
|
||||
"is_gps_active": function() {
|
||||
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
|
||||
}
|
||||
};
|
||||
var h = HANDLERS[event.t];
|
||||
|
|
@ -189,6 +201,30 @@
|
|||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||
// error/warn here?
|
||||
};
|
||||
// GPS overwrite logic
|
||||
if (settings.overwriteGps) { // if the overwrite option is set../
|
||||
// Save current logic
|
||||
const originalSetGpsPower = Bangle.setGPSPower;
|
||||
// Replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
|
||||
Bangle.setGPSPower = (isOn, appID) => {
|
||||
// if not connected, use old logic
|
||||
if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
|
||||
// Emulate old GPS power logic
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
|
||||
if (!appID) appID="?";
|
||||
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
|
||||
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
|
||||
let pwr = Bangle._PWR.GPS.length>0;
|
||||
gbSend({ t: "gps_power", status: pwr });
|
||||
return pwr;
|
||||
}
|
||||
// Replace check if the GPS is on to check the _PWR variable
|
||||
Bangle.isGPSOn = () => {
|
||||
return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
|
||||
}
|
||||
}
|
||||
|
||||
// remove settings object so it's not taking up RAM
|
||||
delete settings;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
(function(back) {
|
||||
|
||||
|
||||
|
||||
function gb(j) {
|
||||
Bluetooth.println(JSON.stringify(j));
|
||||
}
|
||||
|
|
@ -23,6 +26,16 @@
|
|||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Overwrite GPS" : {
|
||||
value : !!settings.overwriteGps,
|
||||
onchange: newValue => {
|
||||
if (newValue) {
|
||||
Bangle.setGPSPower(false, 'android');
|
||||
}
|
||||
settings.overwriteGps = newValue;
|
||||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Messages" : ()=>require("message").openGUI(),
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Add lightning
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2,
|
|||
var exploded = false;
|
||||
var nExplosions = 0;
|
||||
var landed = false;
|
||||
var lightning = 0;
|
||||
|
||||
var settings = require("Storage").readJSON('f9settings.json', 1) || {};
|
||||
|
||||
const gravity = 4;
|
||||
const dt = 0.1;
|
||||
|
|
@ -61,18 +64,40 @@ function flameImageGen (throttle) {
|
|||
|
||||
function drawFalcon(x, y, throttle, angle) {
|
||||
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
|
||||
if (throttle>0) {
|
||||
if (throttle>0 || lightning>0) {
|
||||
var flameImg = flameImageGen(throttle);
|
||||
var r = falcon9.height/2 + flameImg.height/2-1;
|
||||
var xoffs = -Math.sin(angle)*r;
|
||||
var yoffs = Math.cos(angle)*r;
|
||||
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
|
||||
else g.setColor(1, 1, 0);
|
||||
g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
|
||||
if (throttle>0) g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
|
||||
if (lightning>1 && lightning<30) {
|
||||
for (var i=0; i<6; ++i) {
|
||||
var r = Math.random()*6;
|
||||
var x = Math.random()*5 - xoffs;
|
||||
var y = Math.random()*5 - yoffs;
|
||||
g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawLightning() {
|
||||
var c = {x:cloudOffs+50, y:30};
|
||||
var dx = c.x-booster.x;
|
||||
var dy = c.y-booster.y;
|
||||
var m1 = {x:booster.x+0.6*dx+Math.random()*20, y:booster.y+0.6*dy+Math.random()*10};
|
||||
var m2 = {x:booster.x+0.4*dx+Math.random()*20, y:booster.y+0.4*dy+Math.random()*10};
|
||||
g.setColor(1, 1, 1).drawLine(c.x, c.y, m1.x, m1.y).drawLine(m1.x, m1.y, m2.x, m2.y).drawLine(m2.x, m2.y, booster.x, booster.y);
|
||||
}
|
||||
|
||||
function drawBG() {
|
||||
if (lightning==1) {
|
||||
g.setBgColor(1, 1, 1).clear();
|
||||
Bangle.buzz(200);
|
||||
return;
|
||||
}
|
||||
g.setBgColor(0.2, 0.2, 1).clear();
|
||||
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
|
||||
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
|
||||
|
|
@ -88,6 +113,7 @@ function renderScreen(input) {
|
|||
drawBG();
|
||||
showFuel();
|
||||
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
|
||||
if (lightning>1 && lightning<6) drawLightning();
|
||||
}
|
||||
|
||||
function getInputs() {
|
||||
|
|
@ -97,6 +123,7 @@ function getInputs() {
|
|||
if (t > 1) t = 1;
|
||||
if (t < 0) t = 0;
|
||||
if (booster.fuel<=0) t = 0;
|
||||
if (lightning>0 && lightning<20) t = 0;
|
||||
return {throttle: t, angle: a};
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +148,6 @@ function gameStep() {
|
|||
else {
|
||||
var input = getInputs();
|
||||
if (booster.y >= targetY) {
|
||||
// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle);
|
||||
if (Math.abs(booster.x-droneX-droneShip.width/2)<droneShip.width/2 && Math.abs(input.angle)<Math.PI/8 && booster.vy<maxV) {
|
||||
renderScreen({angle:0, throttle:0});
|
||||
epilogue("You landed!");
|
||||
|
|
@ -129,6 +155,8 @@ function gameStep() {
|
|||
else exploded = true;
|
||||
}
|
||||
else {
|
||||
if (lightning) ++lightning;
|
||||
if (settings.lightning && (lightning==0||lightning>40) && Math.random()>0.98) lightning = 1;
|
||||
booster.x += booster.vx*dt;
|
||||
booster.y += booster.vy*dt;
|
||||
booster.vy += gravity*dt;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "f9lander",
|
||||
"name": "Falcon9 Lander",
|
||||
"shortName":"F9lander",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Land a rocket booster",
|
||||
"icon": "f9lander.png",
|
||||
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"f9lander.app.js","url":"app.js"},
|
||||
{"name":"f9lander.img","url":"app-icon.js","evaluate":true}
|
||||
{"name":"f9lander.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"f9lander.settings.js", "url":"settings.js"}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
// This file should contain exactly one function, which shows the app's settings
|
||||
/**
|
||||
* @param {function} back Use back() to return to settings menu
|
||||
*/
|
||||
const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
|
||||
(function(back) {
|
||||
const SETTINGS_FILE = 'f9settings.json'
|
||||
// initialize with default settings...
|
||||
let settings = {
|
||||
'lightning': false,
|
||||
}
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
for (const key in saved) {
|
||||
settings[key] = saved[key];
|
||||
}
|
||||
// creates a function to safe a specific setting, e.g. save('color')(1)
|
||||
function save(key) {
|
||||
return function (value) {
|
||||
settings[key] = value;
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
}
|
||||
}
|
||||
const menu = {
|
||||
'': { 'title': 'OpenWind' },
|
||||
'< Back': back,
|
||||
'Lightning': {
|
||||
value: settings.lightning,
|
||||
format: boolFormat,
|
||||
onchange: save('lightning'),
|
||||
}
|
||||
}
|
||||
E.showMenu(menu);
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# HRM Motion Artifacts removal
|
||||
|
||||
Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these.
|
||||
|
||||
## Settings
|
||||
|
||||
* **MA removal**
|
||||
|
||||
Select the algorithm to Remove Motion artifacts:
|
||||
- None: (default) No Motion Artifact removal.
|
||||
- fft elim: (*experimental*) Remove Motion Artifacts by cutting out the frequencies from the HRM frequency spectrum that are noisy in acceleration spectrum. Under motion this can report a heart rate that is closer to the real one but will fail if motion frequency and heart rate overlap.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
let bpm_corrected; // result of algorithm
|
||||
|
||||
const updateHrm = (bpm) => {
|
||||
bpm_corrected = bpm;
|
||||
};
|
||||
|
||||
Bangle.on('HRM', (hrm) => {
|
||||
if (bpm_corrected > 0) {
|
||||
// replace bpm data in event
|
||||
hrm.bpm_orig = hrm.bpm;
|
||||
hrm.confidence_orig = hrm.confidence;
|
||||
hrm.bpm = bpm_corrected;
|
||||
hrm.confidence = 0;
|
||||
}
|
||||
});
|
||||
|
||||
let run = () => {
|
||||
const settings = Object.assign({
|
||||
mAremoval: 0
|
||||
}, require("Storage").readJSON("hrmmar.json", true) || {});
|
||||
|
||||
// select motion artifact removal algorithm
|
||||
switch(settings.mAremoval) {
|
||||
case 1:
|
||||
require("hrmfftelim").run(settings, updateHrm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// override setHRMPower so we can run our code on HRM enable
|
||||
const oldSetHRMPower = Bangle.setHRMPower;
|
||||
Bangle.setHRMPower = function(on, id) {
|
||||
if (on && run !== undefined) {
|
||||
run();
|
||||
run = undefined; // Make sure we run only once
|
||||
}
|
||||
return oldSetHRMPower(on, id);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
exports.run = (settings, updateHrm) => {
|
||||
const SAMPLE_RATE = 12.5;
|
||||
const NUM_POINTS = 256; // fft size
|
||||
const ACC_PEAKS = 2; // remove this number of ACC peaks
|
||||
|
||||
// ringbuffers
|
||||
const hrmvalues = new Int16Array(8*SAMPLE_RATE);
|
||||
const accvalues = new Int16Array(8*SAMPLE_RATE);
|
||||
// fft buffers
|
||||
const hrmfftbuf = new Int16Array(NUM_POINTS);
|
||||
const accfftbuf = new Int16Array(NUM_POINTS);
|
||||
let BPM_est_1 = 0;
|
||||
let BPM_est_2 = 0;
|
||||
|
||||
let hrmdata;
|
||||
let idx=0, wraps=0;
|
||||
|
||||
// init settings
|
||||
Bangle.setOptions({hrmPollInterval: 40, powerSave: false}); // hrm=25Hz
|
||||
Bangle.setPollInterval(80); // 12.5Hz
|
||||
|
||||
calcfft = (values, idx, normalize, fftbuf) => {
|
||||
fftbuf.fill(0);
|
||||
let i_out=0;
|
||||
let avg = 0;
|
||||
if (normalize) {
|
||||
const sum = values.reduce((a, b) => a + b, 0);
|
||||
avg = sum/values.length;
|
||||
}
|
||||
// sort ringbuffer to fft buffer
|
||||
for(let i_in=idx; i_in<values.length; i_in++, i_out++) {
|
||||
fftbuf[i_out] = values[i_in]-avg;
|
||||
}
|
||||
for(let i_in=0; i_in<idx; i_in++, i_out++) {
|
||||
fftbuf[i_out] = values[i_in]-avg;
|
||||
}
|
||||
|
||||
E.FFT(fftbuf);
|
||||
return fftbuf;
|
||||
};
|
||||
|
||||
getMax = (values) => {
|
||||
let maxVal = -Number.MAX_VALUE;
|
||||
let maxIdx = 0;
|
||||
|
||||
values.forEach((value,i) => {
|
||||
if (value > maxVal) {
|
||||
maxVal = value;
|
||||
maxIdx = i;
|
||||
}
|
||||
});
|
||||
return {idx: maxIdx, val: maxVal};
|
||||
};
|
||||
|
||||
getSign = (value) => {
|
||||
return value < 0 ? -1 : 1;
|
||||
};
|
||||
|
||||
// idx in fft buffer to frequency
|
||||
getFftFreq = (idx, rate, size) => {
|
||||
return idx*rate/(size-1);
|
||||
};
|
||||
|
||||
// frequency to idx in fft buffer
|
||||
getFftIdx = (freq, rate, size) => {
|
||||
return Math.round(freq*(size-1)/rate);
|
||||
};
|
||||
|
||||
calc2ndDeriative = (values) => {
|
||||
const result = new Int16Array(values.length-2);
|
||||
for(let i=1; i<values.length-1; i++) {
|
||||
const diff = values[i+1]-2*values[i]+values[i-1];
|
||||
result[i-1] = diff;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const minFreqIdx = getFftIdx(1.0, SAMPLE_RATE, NUM_POINTS); // 60 BPM
|
||||
const maxFreqIdx = getFftIdx(3.0, SAMPLE_RATE, NUM_POINTS); // 180 BPM
|
||||
let rangeIdx = [0, maxFreqIdx-minFreqIdx]; // range of search for the next estimates
|
||||
const freqStep=getFftFreq(1, SAMPLE_RATE, NUM_POINTS)*60;
|
||||
const maxBpmDiffIdxDown = Math.ceil(5/freqStep); // maximum down BPM
|
||||
const maxBpmDiffIdxUp = Math.ceil(10/freqStep); // maximum up BPM
|
||||
|
||||
calculate = (idx) => {
|
||||
// fft
|
||||
const ppg_fft = calcfft(hrmvalues, idx, true, hrmfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
|
||||
const acc_fft = calcfft(accvalues, idx, false, accfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
|
||||
|
||||
// remove spectrum that have peaks in acc fft from ppg fft
|
||||
const accGlobalMax = getMax(acc_fft);
|
||||
const acc2nddiff = calc2ndDeriative(acc_fft); // calculate second derivative
|
||||
for(let iClean=0; iClean < ACC_PEAKS; iClean++) {
|
||||
// get max peak in ACC
|
||||
const accMax = getMax(acc_fft);
|
||||
|
||||
if (accMax.val >= 10 && accMax.val/accGlobalMax.val > 0.75) {
|
||||
// set all values in PPG FFT to zero until second derivative of ACC has zero crossing
|
||||
for (let k = accMax.idx-1; k>=0; k--) {
|
||||
ppg_fft[k] = 0;
|
||||
acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
|
||||
if (k-2 > 0 && getSign(acc2nddiff[k-1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// set all values in PPG FFT to zero until second derivative of ACC has zero crossing
|
||||
for (let k = accMax.idx; k < acc_fft.length-1; k++) {
|
||||
ppg_fft[k] = 0;
|
||||
acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
|
||||
if (k-2 >= 0 && getSign(acc2nddiff[k+1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bpm result is maximum peak in PPG fft
|
||||
const hrRangeMax = getMax(ppg_fft.subarray(rangeIdx[0], rangeIdx[1]));
|
||||
const hrTotalMax = getMax(ppg_fft);
|
||||
const maxDiff = hrTotalMax.val/hrRangeMax.val;
|
||||
let idxMaxPPG = hrRangeMax.idx+rangeIdx[0]; // offset range limit
|
||||
|
||||
if ((maxDiff > 3 && idxMaxPPG != hrTotalMax.idx) || hrRangeMax.val === 0) { // prevent tracking from loosing the real heart rate by checking the full spectrum
|
||||
if (hrTotalMax.idx > idxMaxPPG) {
|
||||
idxMaxPPG = idxMaxPPG+Math.ceil(6/freqStep); // step 6 BPM up into the direction of max peak
|
||||
} else {
|
||||
idxMaxPPG = idxMaxPPG-Math.ceil(2/freqStep); // step 2 BPM down into the direction of max peak
|
||||
}
|
||||
}
|
||||
|
||||
idxMaxPPG = idxMaxPPG + minFreqIdx;
|
||||
const BPM_est_0 = getFftFreq(idxMaxPPG, SAMPLE_RATE, NUM_POINTS)*60;
|
||||
|
||||
// smooth with moving average
|
||||
let BPM_est_res;
|
||||
if (BPM_est_2 > 0) {
|
||||
BPM_est_res = 0.9*BPM_est_0 + 0.05*BPM_est_1 + 0.05*BPM_est_2;
|
||||
} else {
|
||||
BPM_est_res = BPM_est_0;
|
||||
}
|
||||
|
||||
return BPM_est_res.toFixed(1);
|
||||
};
|
||||
|
||||
Bangle.on('HRM-raw', (hrm) => {
|
||||
hrmdata = hrm;
|
||||
});
|
||||
|
||||
Bangle.on('accel', (acc) => {
|
||||
if (hrmdata !== undefined) {
|
||||
hrmvalues[idx] = hrmdata.filt;
|
||||
accvalues[idx] = acc.x*1000 + acc.y*1000 + acc.z*1000;
|
||||
idx++;
|
||||
if (idx >= 8*SAMPLE_RATE) {
|
||||
idx = 0;
|
||||
wraps++;
|
||||
}
|
||||
|
||||
if (idx % (SAMPLE_RATE*2) == 0) { // every two seconds
|
||||
if (wraps === 0) { // use rate of firmware until hrmvalues buffer is filled
|
||||
updateHrm(undefined);
|
||||
BPM_est_2 = BPM_est_1;
|
||||
BPM_est_1 = hrmdata.bpm;
|
||||
} else {
|
||||
let bpm_result;
|
||||
if (hrmdata.confidence >= 90) { // display firmware value if good
|
||||
bpm_result = hrmdata.bpm;
|
||||
updateHrm(undefined);
|
||||
} else {
|
||||
bpm_result = calculate(idx);
|
||||
bpm_corrected = bpm_result;
|
||||
updateHrm(bpm_result);
|
||||
}
|
||||
BPM_est_2 = BPM_est_1;
|
||||
BPM_est_1 = bpm_result;
|
||||
|
||||
// set search range of next BPM
|
||||
const est_res_idx = getFftIdx(bpm_result/60, SAMPLE_RATE, NUM_POINTS)-minFreqIdx;
|
||||
rangeIdx = [est_res_idx-maxBpmDiffIdxDown, est_res_idx+maxBpmDiffIdxUp];
|
||||
if (rangeIdx[0] < 0) {
|
||||
rangeIdx[0] = 0;
|
||||
}
|
||||
if (rangeIdx[1] > maxFreqIdx-minFreqIdx) {
|
||||
rangeIdx[1] = maxFreqIdx-minFreqIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "hrmmar",
|
||||
"name": "HRM Motion Artifacts removal",
|
||||
"shortName":"HRM MA removal",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data.",
|
||||
"type": "bootloader",
|
||||
"tags": "health",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"hrmmar.boot.js","url":"boot.js"},
|
||||
{"name":"hrmfftelim","url":"fftelim.js"},
|
||||
{"name":"hrmmar.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"hrmmar.json"}]
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
(function(back) {
|
||||
var FILE = "hrmmar.json";
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
mAremoval: 0,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "HRM MA removal" },
|
||||
"< Back" : () => back(),
|
||||
'MA removal': {
|
||||
value: settings.mAremoval,
|
||||
min: 0, max: 1,
|
||||
format: v => ["None", "fft elim."][v],
|
||||
onchange: v => {
|
||||
settings.mAremoval = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
@ -19,3 +19,4 @@
|
|||
0.17: Don't display 'Loading...' now the watch has its own loading screen
|
||||
0.18: Add 'back' icon in top-left to go back to clock
|
||||
0.19: Fix regression after back button added (returnToClock was called twice!)
|
||||
0.20: Use Bangle.showClock for changing to clock
|
||||
|
|
|
|||
|
|
@ -42,16 +42,6 @@ let apps = launchCache.apps;
|
|||
if (!settings.fullscreen)
|
||||
Bangle.loadWidgets();
|
||||
|
||||
let returnToClock = function() {
|
||||
// unload everything manually
|
||||
// ... or we could just call `load();` but it will be slower
|
||||
Bangle.setUI(); // remove scroller's handling
|
||||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
Bangle.removeListener("lock", lockHandler);
|
||||
// now load the default clock - just call .bootcde as this has the code already
|
||||
setTimeout(eval,0,s.read(".bootcde"));
|
||||
}
|
||||
|
||||
E.showScroller({
|
||||
h : 64*scaleval, c : apps.length,
|
||||
draw : (i, r) => {
|
||||
|
|
@ -74,7 +64,12 @@ E.showScroller({
|
|||
load(app.src);
|
||||
}
|
||||
},
|
||||
back : returnToClock // button press or tap in top left calls returnToClock now
|
||||
back : Bangle.showClock, // button press or tap in top left shows clock now
|
||||
remove : () => {
|
||||
// cleanup the timeout to not leave anything behind after being removed from ram
|
||||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
Bangle.removeListener("lock", lockHandler);
|
||||
}
|
||||
});
|
||||
g.flip(); // force a render before widgets have finished drawing
|
||||
|
||||
|
|
@ -85,7 +80,7 @@ let lockHandler = function(locked) {
|
|||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
lockTimeout = undefined;
|
||||
if (locked)
|
||||
lockTimeout = setTimeout(returnToClock, 10000);
|
||||
lockTimeout = setTimeout(Bangle.showClock, 10000);
|
||||
}
|
||||
Bangle.on("lock", lockHandler);
|
||||
if (!settings.fullscreen) // finally draw widgets
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "launch",
|
||||
"name": "Launcher",
|
||||
"shortName": "Launcher",
|
||||
"version": "0.19",
|
||||
"version": "0.20",
|
||||
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@
|
|||
0.14: Added ability to upload multiple sets of map tiles
|
||||
Support for zooming in on map
|
||||
Satellite count moved to widget bar to leave more room for the map
|
||||
0.15: Make track drawing an option (default off)
|
||||
|
|
|
|||
|
|
@ -26,11 +26,16 @@ can change settings, move the map around, and click `Get Map` again.
|
|||
## Bangle.js App
|
||||
|
||||
The Bangle.js app allows you to view a map - it also turns the GPS on and marks
|
||||
the path that you've been travelling.
|
||||
the path that you've been travelling (if enabled).
|
||||
|
||||
* Drag on the screen to move the map
|
||||
* Press the button to bring up a menu, where you can zoom, go to GPS location
|
||||
or put the map back in its default location
|
||||
, put the map back in its default location, or choose whether to draw the currently
|
||||
recording GPS track (from the `Recorder` app).
|
||||
|
||||
**Note:** If enabled, drawing the currently recorded GPS track can take a second
|
||||
or two (which happens after you've finished scrolling the screen with your finger).
|
||||
|
||||
|
||||
## Library
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,23 @@ var R;
|
|||
var fix = {};
|
||||
var mapVisible = false;
|
||||
var hasScrolled = false;
|
||||
var settings = require("Storage").readJSON("openstmap.json",1)||{};
|
||||
|
||||
// Redraw the whole page
|
||||
function redraw() {
|
||||
g.setClipRect(R.x,R.y,R.x2,R.y2);
|
||||
m.draw();
|
||||
drawMarker();
|
||||
if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
|
||||
g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
|
||||
WIDGETS["gpsrec"].plotTrack(m);
|
||||
}
|
||||
if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) {
|
||||
g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
|
||||
WIDGETS["recorder"].plotTrack(m);
|
||||
// if track drawing is enabled...
|
||||
if (settings.drawTrack) {
|
||||
if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
|
||||
g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
|
||||
WIDGETS["gpsrec"].plotTrack(m);
|
||||
}
|
||||
if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) {
|
||||
g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
|
||||
WIDGETS["recorder"].plotTrack(m);
|
||||
}
|
||||
}
|
||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||
}
|
||||
|
|
@ -76,6 +80,10 @@ function showMap() {
|
|||
m.scale *= 2;
|
||||
showMap();
|
||||
},
|
||||
/*LANG*/"Draw Track": {
|
||||
value : !!settings.drawTrack,
|
||||
onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); }
|
||||
},
|
||||
/*LANG*/"Center Map": () =>{
|
||||
m.lat = m.map.lat;
|
||||
m.lon = m.map.lon;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "openstmap",
|
||||
"name": "OpenStreetMap",
|
||||
"shortName": "OpenStMap",
|
||||
"version": "0.14",
|
||||
"version": "0.15",
|
||||
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
{"name":"openstmap.app.js","url":"app.js"},
|
||||
{"name":"openstmap.img","url":"app-icon.js","evaluate":true}
|
||||
], "data": [
|
||||
{"name":"openstmap.json"},
|
||||
{"wildcard":"openstmap.*.json"},
|
||||
{"wildcard":"openstmap.*.img"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Quick Center
|
||||
|
||||
An app with a status bar showing various information and up to six shortcuts for your favorite apps!
|
||||
Designed for use with any kind of quick launcher, such as Quick Launch or Pattern Launcher.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Pin your apps with settings, then launch them with your favorite quick launcher to access them quickly.
|
||||
If you don't have any apps pinned, the settings and about apps will be shown as an example.
|
||||
|
||||
## Features
|
||||
|
||||
Battery and GPS status display (for now)
|
||||
Up to six shortcuts to your favorite apps
|
||||
|
||||
## Upcoming features
|
||||
- Quick switches for toggleable features such as Bluetooth or HID mode
|
||||
- Customizable status information
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA="))
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
require("Font8x12").add(Graphics);
|
||||
|
||||
// load pinned apps from config
|
||||
var settings = require("Storage").readJSON("qcenter.json", 1) || {};
|
||||
var pinnedApps = settings.pinnedApps || [];
|
||||
var exitGesture = settings.exitGesture || "swipeup";
|
||||
|
||||
// if empty load a default set of apps as an example
|
||||
if (pinnedApps.length == 0) {
|
||||
pinnedApps = [
|
||||
{ src: "setting.app.js", icon: "setting.img" },
|
||||
{ src: "about.app.js", icon: "about.img" },
|
||||
];
|
||||
}
|
||||
|
||||
// button drawing from Layout.js, edited to have completely custom button size with icon
|
||||
function drawButton(l) {
|
||||
var x = l.x + (0 | l.pad),
|
||||
y = l.y + (0 | l.pad),
|
||||
w = l.w - (l.pad << 1),
|
||||
h = l.h - (l.pad << 1);
|
||||
var poly = [
|
||||
x,
|
||||
y + 4,
|
||||
x + 4,
|
||||
y,
|
||||
x + w - 5,
|
||||
y,
|
||||
x + w - 1,
|
||||
y + 4,
|
||||
x + w - 1,
|
||||
y + h - 5,
|
||||
x + w - 5,
|
||||
y + h - 1,
|
||||
x + 4,
|
||||
y + h - 1,
|
||||
x,
|
||||
y + h - 5,
|
||||
x,
|
||||
y + 4,
|
||||
],
|
||||
bg = l.selected ? g.theme.bgH : g.theme.bg2;
|
||||
g.setColor(bg)
|
||||
.fillPoly(poly)
|
||||
.setColor(l.selected ? g.theme.fgH : g.theme.fg2)
|
||||
.drawPoly(poly);
|
||||
if (l.src)
|
||||
g.setBgColor(bg).drawImage(
|
||||
"function" == typeof l.src ? l.src() : l.src,
|
||||
l.x + l.w / 2,
|
||||
l.y + l.h / 2,
|
||||
{ scale: l.scale || undefined, rotate: Math.PI * 0.5 * (l.r || 0) }
|
||||
);
|
||||
}
|
||||
|
||||
// function to split array into group of 3, for button placement
|
||||
function groupBy3(data) {
|
||||
var result = [];
|
||||
for (var i = 0; i < data.length; i += 3) result.push(data.slice(i, i + 3));
|
||||
return result;
|
||||
}
|
||||
|
||||
// generate object with buttons for apps by group of 3
|
||||
var appButtons = groupBy3(pinnedApps).map((appGroup, i) => {
|
||||
return appGroup.map((app, j) => {
|
||||
return {
|
||||
type: "custom",
|
||||
render: drawButton,
|
||||
width: 50,
|
||||
height: 50,
|
||||
pad: 5,
|
||||
src: require("Storage").read(app.icon),
|
||||
scale: 0.75,
|
||||
cb: (l) => Bangle.load(app.src),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// create basic layout content with status info and sensor status on top
|
||||
var layoutContent = [
|
||||
{
|
||||
type: "h",
|
||||
pad: 5,
|
||||
fillx: 1,
|
||||
c: [
|
||||
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: E.getBattery() + "%" },
|
||||
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: "GPS: " + (Bangle.isGPSOn() ? "ON" : "OFF") },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// create rows for buttons and add them to layoutContent
|
||||
appButtons.forEach((appGroup) => {
|
||||
layoutContent.push({
|
||||
type: "h",
|
||||
pad: 2,
|
||||
c: appGroup,
|
||||
});
|
||||
});
|
||||
|
||||
// create layout with content
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
||||
var Layout = require("Layout");
|
||||
var layout = new Layout({
|
||||
type: "v",
|
||||
c: layoutContent,
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// swipe event listener for exit gesture
|
||||
Bangle.on("swipe", function (lr, ud) {
|
||||
if(exitGesture == "swipeup" && ud == -1) Bangle.showClock();
|
||||
if(exitGesture == "swipedown" && ud == 1) Bangle.showClock();
|
||||
if(exitGesture == "swipeleft" && lr == -1) Bangle.showClock();
|
||||
if(exitGesture == "swiperight" && lr == 1) Bangle.showClock();
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 265 B |
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "qcenter",
|
||||
"name": "Quick Center",
|
||||
"shortName": "QCenter",
|
||||
"version": "0.01",
|
||||
"description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
|
||||
"icon": "app.png",
|
||||
"tags": "",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"screenshots": [{ "url": "screenshot.png" }],
|
||||
"storage": [
|
||||
{ "name": "qcenter.app.js", "url": "app.js" },
|
||||
{ "name": "qcenter.settings.js", "url": "settings.js" },
|
||||
{ "name": "qcenter.img", "url": "app-icon.js", "evaluate": true }
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1,141 @@
|
|||
// make sure to enclose the function in parentheses
|
||||
(function (back) {
|
||||
let settings = require("Storage").readJSON("qcenter.json", 1) || {};
|
||||
var apps = require("Storage")
|
||||
.list(/\.info$/)
|
||||
.map((app) => {
|
||||
var a = require("Storage").readJSON(app, 1);
|
||||
return (
|
||||
a && {
|
||||
name: a.name,
|
||||
type: a.type,
|
||||
sortorder: a.sortorder,
|
||||
src: a.src,
|
||||
icon: a.icon,
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter(
|
||||
(app) =>
|
||||
app &&
|
||||
(app.type == "app" ||
|
||||
app.type == "launch" ||
|
||||
app.type == "clock" ||
|
||||
!app.type)
|
||||
);
|
||||
apps.sort((a, b) => {
|
||||
var n = (0 | a.sortorder) - (0 | b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
if (a.name < b.name) return -1;
|
||||
if (a.name > b.name) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
function save(key, value) {
|
||||
settings[key] = value;
|
||||
require("Storage").write("qcenter.json", settings);
|
||||
}
|
||||
|
||||
var pinnedApps = settings.pinnedApps || [];
|
||||
var exitGesture = settings.exitGesture || "swipeup";
|
||||
|
||||
function showMainMenu() {
|
||||
var mainmenu = {
|
||||
"": { title: "Quick Center" },
|
||||
"< Back": () => {
|
||||
load();
|
||||
},
|
||||
};
|
||||
|
||||
// Set exit gesture
|
||||
mainmenu["Exit Gesture: " + exitGesture] = function () {
|
||||
E.showMenu(exitGestureMenu);
|
||||
};
|
||||
|
||||
//List all pinned apps, redirecting to menu with options to unpin and reorder
|
||||
pinnedApps.forEach((app, i) => {
|
||||
mainmenu[app.name] = function () {
|
||||
E.showMenu({
|
||||
"": { title: app.name },
|
||||
"< Back": () => {
|
||||
showMainMenu();
|
||||
},
|
||||
"Unpin": () => {
|
||||
pinnedApps.splice(i, 1);
|
||||
save("pinnedApps", pinnedApps);
|
||||
showMainMenu();
|
||||
},
|
||||
"Move Up": () => {
|
||||
if (i > 0) {
|
||||
pinnedApps.splice(i - 1, 0, pinnedApps.splice(i, 1)[0]);
|
||||
save("pinnedApps", pinnedApps);
|
||||
showMainMenu();
|
||||
}
|
||||
},
|
||||
"Move Down": () => {
|
||||
if (i < pinnedApps.length - 1) {
|
||||
pinnedApps.splice(i + 1, 0, pinnedApps.splice(i, 1)[0]);
|
||||
save("pinnedApps", pinnedApps);
|
||||
showMainMenu();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Show pin app menu, or show alert if max amount of apps are pinned
|
||||
mainmenu["Pin App"] = function () {
|
||||
if (pinnedApps.length < 6) {
|
||||
E.showMenu(pinAppMenu);
|
||||
} else {
|
||||
E.showAlert("Max apps pinned").then(showMainMenu);
|
||||
}
|
||||
};
|
||||
|
||||
return E.showMenu(mainmenu);
|
||||
}
|
||||
|
||||
// menu for adding apps to the quick launch menu, listing all apps
|
||||
var pinAppMenu = {
|
||||
"": { title: "Add App" },
|
||||
"< Back": showMainMenu,
|
||||
};
|
||||
apps.forEach((a) => {
|
||||
pinAppMenu[a.name] = function () {
|
||||
// strip unncecessary properties
|
||||
delete a.type;
|
||||
delete a.sortorder;
|
||||
pinnedApps.push(a);
|
||||
save("pinnedApps", pinnedApps);
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
|
||||
// menu for setting exit gesture
|
||||
var exitGestureMenu = {
|
||||
"": { title: "Exit Gesture" },
|
||||
"< Back": showMainMenu,
|
||||
};
|
||||
exitGestureMenu["Swipe Up"] = function () {
|
||||
exitGesture = "swipeup";
|
||||
save("exitGesture", "swipeup");
|
||||
showMainMenu();
|
||||
};
|
||||
exitGestureMenu["Swipe Down"] = function () {
|
||||
exitGesture = "swipedown";
|
||||
save("exitGesture", "swipedown");
|
||||
showMainMenu();
|
||||
};
|
||||
exitGestureMenu["Swipe Left"] = function () {
|
||||
exitGesture = "swipeleft";
|
||||
save("exitGesture", "swipeleft");
|
||||
showMainMenu();
|
||||
};
|
||||
exitGestureMenu["Swipe Right"] = function () {
|
||||
exitGesture = "swiperight";
|
||||
save("exitGesture", "swiperight");
|
||||
showMainMenu();
|
||||
};
|
||||
|
||||
showMainMenu();
|
||||
});
|
||||
|
|
@ -22,3 +22,4 @@
|
|||
0.16: Ability to append to existing track (fix #1712)
|
||||
0.17: Use default Bangle formatter for booleans
|
||||
0.18: Improve widget load speed, allow currently recording track to be plotted in openstmap
|
||||
0.19: Fix track plotting code
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "recorder",
|
||||
"name": "Recorder",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.18",
|
||||
"version": "0.19",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
|
|
|
|||
|
|
@ -276,8 +276,8 @@
|
|||
if (l===undefined) return; // empty file?
|
||||
var mp, c = l.split(",");
|
||||
var la=c.indexOf("Latitude"),lo=c.indexOf("Longitude");
|
||||
if (la<0 || lb<0) return; // no GPS!
|
||||
l = f.readLine();
|
||||
if (la<0 || lo<0) return; // no GPS!
|
||||
l = f.readLine();c=[];
|
||||
while (l && !c[la]) {
|
||||
c = l.split(",");
|
||||
l = f.readLine(f);
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@
|
|||
Improve timer message using formatDuration
|
||||
Fix wrong fallback for buzz pattern
|
||||
0.13: Ask to delete a timer after stopping it
|
||||
0.14: Added clkinfo for alarms and timers
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
(function() {
|
||||
const alarm = require('sched');
|
||||
const iconAlarmOn = atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA==");
|
||||
const iconAlarmOff = atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/nAB/mAB/geB/5/g/5tg/zAwfzhwPzhwHzAwB5tgAB/gAAeA==");
|
||||
const iconTimerOn = atob("GBiBAAAAAAAAAAAAAAf/4Af/4AGBgAGBgAGBgAD/AAD/AAB+AAA8AAA8AAB+AADnAADDAAGBgAGBgAGBgAf/4Af/4AAAAAAAAAAAAA==");
|
||||
const iconTimerOff = atob("GBiBAAAAAAAAAAAAAAf/4Af/4AGBgAGBgAGBgAD/AAD/AAB+AAA8AAA8AAB+AADkeADB/gGBtgGDAwGDhwfzhwfzAwABtgAB/gAAeA==");
|
||||
|
||||
//from 0 to max, the higher the closer to fire (as in a progress bar)
|
||||
function getAlarmValue(a){
|
||||
let min = Math.round(alarm.getTimeToAlarm(a)/(60*1000));
|
||||
if(!min) return 0; //not active or more than a day
|
||||
return getAlarmMax(a)-min;
|
||||
}
|
||||
|
||||
function getAlarmMax(a) {
|
||||
if(a.timer)
|
||||
return Math.round(a.timer/(60*1000));
|
||||
//minutes cannot be more than a full day
|
||||
return 1440;
|
||||
}
|
||||
|
||||
function getAlarmIcon(a) {
|
||||
if(a.on) {
|
||||
if(a.timer) return iconTimerOn;
|
||||
return iconAlarmOn;
|
||||
} else {
|
||||
if(a.timer) return iconTimerOff;
|
||||
return iconAlarmOff;
|
||||
}
|
||||
}
|
||||
|
||||
function getAlarmText(a){
|
||||
if(a.timer) {
|
||||
if(!a.on) return "off";
|
||||
let time = Math.round(alarm.getTimeToAlarm(a)/(60*1000));
|
||||
if(time > 60)
|
||||
time = Math.round(time / 60) + "h";
|
||||
else
|
||||
time += "m";
|
||||
return time;
|
||||
}
|
||||
return require("time_utils").formatTime(a.t);
|
||||
}
|
||||
|
||||
//workaround for sorting undefined values
|
||||
function getAlarmOrder(a) {
|
||||
let val = alarm.getTimeToAlarm(a);
|
||||
if(typeof val == "undefined") return 86400*1000;
|
||||
return val;
|
||||
}
|
||||
|
||||
var img = iconAlarmOn;
|
||||
//get only alarms not created by other apps
|
||||
var alarmItems = {
|
||||
name: "Alarms",
|
||||
img: img,
|
||||
dynamic: true,
|
||||
items: alarm.getAlarms().filter(a=>!a.appid)
|
||||
//.sort((a,b)=>alarm.getTimeToAlarm(a)-alarm.getTimeToAlarm(b))
|
||||
.sort((a,b)=>getAlarmOrder(a)-getAlarmOrder(b))
|
||||
.map((a, i)=>({
|
||||
name: null,
|
||||
hasRange: true,
|
||||
get: () => ({ text: getAlarmText(a), img: getAlarmIcon(a),
|
||||
v: getAlarmValue(a), min:0, max:getAlarmMax(a)}),
|
||||
show: function() { alarmItems.items[i].emit("redraw"); },
|
||||
hide: function () {},
|
||||
run: function() { }
|
||||
})),
|
||||
};
|
||||
|
||||
return alarmItems;
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "sched",
|
||||
"name": "Scheduler",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Scheduling library for alarms and timers",
|
||||
"icon": "app.png",
|
||||
"type": "scheduler",
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
{"name":"sched.js","url":"sched.js"},
|
||||
{"name":"sched.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"sched","url":"lib.js"},
|
||||
{"name":"sched.settings.js","url":"settings.js"}
|
||||
{"name":"sched.settings.js","url":"settings.js"},
|
||||
{"name":"sched.clkinfo.js","url":"clkinfo.js"}
|
||||
],
|
||||
"data": [{"name":"sched.json"}, {"name":"sched.settings.json"}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Reset font to save some memory during remove
|
||||
0.03: Added support for locale based time
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ let draw = function() {
|
|||
x = R.w / 2;
|
||||
y = R.y + R.h / 2 - 12; // 12 = room for date
|
||||
var date = new Date();
|
||||
var hourStr = date.getHours();
|
||||
var minStr = date.getMinutes().toString().padStart(2,0);
|
||||
var local_time = require("locale").time(date, 1);
|
||||
var hourStr = local_time.split(":")[0].trim().padStart(2,'0');
|
||||
var minStr = local_time.split(":")[1].trim().padStart(2, '0');
|
||||
dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+
|
||||
require("locale").date(date, 0).toUpperCase();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "slopeclock",
|
||||
"name": "Slope Clock",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@
|
|||
Made fonts smaller to avoid overlap when (eg) 22:00
|
||||
Allowed black/white background (as that can look nice too)
|
||||
0.05: Images in clkinfo are optional now
|
||||
0.06: Added support for locale based time
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ let draw = function() {
|
|||
x = R.w / 2;
|
||||
y = R.y + R.h / 2 - 12; // 12 = room for date
|
||||
var date = new Date();
|
||||
var hourStr = date.getHours();
|
||||
var minStr = date.getMinutes().toString().padStart(2,0);
|
||||
var local_time = require("locale").time(date, 1);
|
||||
var hourStr = local_time.split(":")[0].trim().padStart(2,'0');
|
||||
var minStr = local_time.split(":")[1].trim().padStart(2, '0');
|
||||
dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+
|
||||
require("locale").date(date, 0).toUpperCase();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "slopeclockpp",
|
||||
"name": "Slope Clock ++",
|
||||
"version":"0.05",
|
||||
"version":"0.06",
|
||||
"description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows extra information and allows the colors to be selected.",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# Tetris
|
||||
|
||||
Bangle version of the classic game of Tetris.
|
||||
|
||||
## Controls
|
||||
|
||||
Tapping the screen rotates the pieces once, swiping left, right or down moves the
|
||||
piece in that direction, if possible.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/ABe5AA0jABwvYAIovBgABEFAQHFL7IuEL4QuFA45fcF4YuNL7i/FFwoHHL7QvFFxpfaF4wAOF/4nHF5+0AAy3SXYoHGW4QBDF4MAAIgvRFwwHHdAbqDFIQuDL6ouJL4ovDFwpfUAAoHFL4a/FFwhfTFxZfDF4ouFL6QANFopfDF/4vNjwAGF8ABFF4MAAIgvBX4IBDX4YBDL6TyFFIIuEL4QuEL4QuEL6ovDFwpfFF4YuFL6i/FFwhfEX4ouEL6YvFFwpfDF4ouFL6QvGAAwtFL4Yv/AAonHAB4vHG563CAIbuDA5i/CAIb2DA4hfJEwoHPFApZEGwpfLFyJfFFxJfMAAoHNFAa5GX54uTL4YuLL5QAVFowAIF+4A/AH4A/AH4A/AHY"))
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{ "id": "tetris",
|
||||
"name": "Tetris",
|
||||
"shortName":"Tetris",
|
||||
"version":"0.01",
|
||||
"description": "Tetris",
|
||||
"icon": "tetris.png",
|
||||
"readme": "README.md",
|
||||
"tags": "games",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"tetris.app.js","url":"tetris.app.js"},
|
||||
{"name":"tetris.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
const block = Graphics.createImage(`
|
||||
########
|
||||
# # # ##
|
||||
## # ###
|
||||
# # ####
|
||||
## #####
|
||||
# ######
|
||||
########
|
||||
########
|
||||
`);
|
||||
const tcols = [ {r:0, g:0, b:1}, {r:0, g:1, b:0}, {r:0, g:1, b:1}, {r:1, g:0, b:0}, {r:1, g:0, b:1}, {r:1, g:1, b:0}, {r:1, g:0.5, b:0.5} ];
|
||||
const tiles = [
|
||||
[[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0]],
|
||||
[[0, 0, 0],
|
||||
[0, 1, 0],
|
||||
[1, 1, 1]],
|
||||
[[0, 0, 0],
|
||||
[1, 0, 0],
|
||||
[1, 1, 1]],
|
||||
[[0, 0, 0],
|
||||
[0, 0, 1],
|
||||
[1, 1, 1]],
|
||||
[[0, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 1]],
|
||||
[[0, 0, 0],
|
||||
[0, 1, 1],
|
||||
[1, 1, 0]],
|
||||
[[1, 1],
|
||||
[1, 1]]
|
||||
];
|
||||
|
||||
const ox = 176/2 - 5*8;
|
||||
const oy = 8;
|
||||
|
||||
var pf = Array(23).fill().map(()=>Array(12).fill(0)); // field is really 10x20, but adding a border for collision checks
|
||||
pf[20].fill(1);
|
||||
pf[21].fill(1);
|
||||
pf[22].fill(1);
|
||||
pf.forEach((x,i) => { pf[i][0] = 1; pf[i][11] = 1; });
|
||||
|
||||
function rotateTile(t, r) {
|
||||
var nt = JSON.parse(JSON.stringify(t));
|
||||
if (t.length==2) return nt;
|
||||
var s = t.length;
|
||||
for (m=0; m<r; ++m) {
|
||||
tl = JSON.parse(JSON.stringify(nt));
|
||||
for (i=0; i<s; ++i)
|
||||
for (j=0; j<s; ++j)
|
||||
nt[i][j] = tl[s-1-j][i];
|
||||
}
|
||||
return nt;
|
||||
}
|
||||
|
||||
function drawBoundingBox() {
|
||||
g.setBgColor(0, 0, 0).clear().setColor(1, 1, 1);
|
||||
g.theme.bg = 0;
|
||||
for (i=0; i<4; ++i) g.drawRect(ox-i-1, oy-i-1, ox+10*8+i, oy+20*8+i);
|
||||
}
|
||||
|
||||
function drawTile (tile, n, x, y, qClear) {
|
||||
if (qClear) g.setColor(0);
|
||||
else g.setColor(tcols[n].r, tcols[n].g, tcols[n].b);
|
||||
for (i=0; i<tile.length; ++i)
|
||||
for (j=0; j<tile.length; ++j)
|
||||
if (tile[j][i]>0)
|
||||
if (qClear) g.fillRect(x+8*i, y+8*j, x+8*(i+1)-1, y+8*(j+1)-1);
|
||||
else g.drawImage(block, x+8*i, y+8*j);
|
||||
}
|
||||
|
||||
function showNext(n, r) {
|
||||
var nt = rotateTile(tiles[n], r);
|
||||
g.setColor(0).fillRect(176-33, 40, 176-33+33, 82);
|
||||
drawTile(nt, ntn, 176-33, 40);
|
||||
}
|
||||
|
||||
var time = Date.now();
|
||||
var px=4, py=0;
|
||||
var ctn = Math.floor(Math.random()*7); // current tile number
|
||||
var ntn = Math.floor(Math.random()*7); // next tile number
|
||||
var ntr = Math.floor(Math.random()*4); // next tile rotation
|
||||
var ct = rotateTile(tiles[ctn], Math.floor(Math.random()*4)); // current tile (rotated)
|
||||
var dropInterval = 450;
|
||||
var nlines = 0;
|
||||
|
||||
function redrawPF(ly) {
|
||||
for (y=0; y<=ly; ++y)
|
||||
for (x=1; x<11; ++x) {
|
||||
c = pf[y][x];
|
||||
if (c>0) g.setColor(tcols[c-1].r, tcols[c-1].g, tcols[c-1].b).drawImage(block, ox+(x-1)*8, oy+y*8);
|
||||
else g.setColor(0, 0, 0).fillRect(ox+(x-1)*8, oy+y*8, ox+x*8-1, oy+(y+1)*8-1);
|
||||
}
|
||||
}
|
||||
|
||||
function insertAndCheck() {
|
||||
for (y=0; y<ct.length; ++y)
|
||||
for (x=0; x<ct[y].length; ++x)
|
||||
if (ct[y][x]>0) pf[py+y][px+x+1] = ctn+1;
|
||||
// check for full lines
|
||||
for (y=19; y>0; y--) {
|
||||
var qFull = true;
|
||||
for (x=1; x<11; ++x) qFull &= pf[y][x]>0;
|
||||
if (qFull) {
|
||||
nlines++;
|
||||
dropInterval -= 5;
|
||||
Bangle.buzz(30);
|
||||
for (ny=y; ny>0; ny--) pf[ny] = JSON.parse(JSON.stringify(pf[ny-1]));
|
||||
redrawPF(y);
|
||||
g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50);
|
||||
}
|
||||
}
|
||||
// spawn new tile
|
||||
px = 4; py = 0;
|
||||
ctn = ntn;
|
||||
ntn = Math.floor(Math.random()*7);
|
||||
ct = rotateTile(tiles[ctn], ntr);
|
||||
ntr = Math.floor(Math.random()*4);
|
||||
showNext(ntn, ntr);
|
||||
}
|
||||
|
||||
function moveOk(t, dx, dy) {
|
||||
var ok = true;
|
||||
for (y=0; y<t.length; ++y)
|
||||
for (x=0; x<t[y].length; ++x)
|
||||
if (t[y][x]*pf[py+dy+y][px+dx+x+1] > 0) ok = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
function gameStep() {
|
||||
if (Date.now()-time > dropInterval) { // drop one step
|
||||
time = Date.now();
|
||||
if (moveOk(ct, 0, 1)) {
|
||||
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
|
||||
py++;
|
||||
}
|
||||
else { // reached the bottom
|
||||
insertAndCheck(ct, ctn, px, py);
|
||||
}
|
||||
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.setUI();
|
||||
Bangle.on("touch", (e) => {
|
||||
t = rotateTile(ct, 3);
|
||||
if (moveOk(t, 0, 0)) {
|
||||
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
|
||||
ct = t;
|
||||
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on("swipe", (x,y) => {
|
||||
if (y<0) y = 0;
|
||||
if (moveOk(ct, x, y)) {
|
||||
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
|
||||
px += x;
|
||||
py += y;
|
||||
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
|
||||
}
|
||||
});
|
||||
|
||||
drawBoundingBox();
|
||||
g.setColor(1, 1, 1).setFontAlign(0, 1, 0).setFont("6x15", 1).drawString("Lines", 22, 30).drawString("Next", 176-22, 30);
|
||||
showNext(ntn, ntr);
|
||||
g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50);
|
||||
var gi = setInterval(gameStep, 20);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 492 B |
|
|
@ -13,4 +13,5 @@
|
|||
0.14: Use weather condition code for icon selection
|
||||
0.15: Fix widget icon
|
||||
0.16: Don't mark app as clock
|
||||
0.17: Added clkinfo for clocks.
|
||||
0.17: Added clkinfo for clocks.
|
||||
0.18: Added hasRange to clkinfo.
|
||||
|
|
|
|||
|
|
@ -5,34 +5,41 @@
|
|||
wind: "?",
|
||||
};
|
||||
|
||||
var weatherJson = storage.readJSON('weather.json');
|
||||
var weatherJson = require("Storage").readJSON('weather.json');
|
||||
if(weatherJson !== undefined && weatherJson.weather !== undefined){
|
||||
weather = weatherJson.weather;
|
||||
weather.temp = locale.temp(weather.temp-273.15);
|
||||
weather.temp = require("locale").temp(weather.temp-273.15);
|
||||
weather.hum = weather.hum + "%";
|
||||
weather.wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
||||
weather.wind = require("locale").speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
||||
weather.wind = Math.round(weather.wind[1]) + "kph";
|
||||
}
|
||||
|
||||
//FIXME ranges are somehow arbitrary
|
||||
var weatherItems = {
|
||||
name: "Weather",
|
||||
img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="),
|
||||
items: [
|
||||
{
|
||||
name: "temperature",
|
||||
get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA==")}),
|
||||
hasRange : true,
|
||||
get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA=="),
|
||||
v: parseInt(weather.temp), min: -30, max: 55}),
|
||||
show: function() { weatherItems.items[0].emit("redraw"); },
|
||||
hide: function () {}
|
||||
},
|
||||
{
|
||||
name: "humidity",
|
||||
get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A==")}),
|
||||
hasRange : true,
|
||||
get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A=="),
|
||||
v: parseInt(weather.hum), min: 0, max: 100}),
|
||||
show: function() { weatherItems.items[1].emit("redraw"); },
|
||||
hide: function () {}
|
||||
},
|
||||
{
|
||||
name: "wind",
|
||||
get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA==")}),
|
||||
hasRange : true,
|
||||
get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA=="),
|
||||
v: parseInt(weather.wind), min: 0, max: 118}),
|
||||
show: function() { weatherItems.items[2].emit("redraw"); },
|
||||
hide: function () {}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "weather",
|
||||
"name": "Weather",
|
||||
"version": "0.17",
|
||||
"version": "0.18",
|
||||
"description": "Show Gadgetbridge weather report",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
|||
2
core
2
core
|
|
@ -1 +1 @@
|
|||
Subproject commit aba9b6a51fe02dfbde307c303560b8382857916d
|
||||
Subproject commit f3f54106b3d7f84927ff734b715023a49a87cc6f
|
||||
|
|
@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") {
|
|||
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.';
|
||||
}
|
||||
|
||||
var RECOMMENDED_VERSION = "2v15";
|
||||
var RECOMMENDED_VERSION = "2v16";
|
||||
// could check http://www.espruino.com/json/BANGLEJS.json for this
|
||||
|
||||
// We're only interested in Bangles
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
that can be scrolled through on the clock face.
|
||||
|
||||
`load()` returns an array of menu objects, where each object contains a list of menu items:
|
||||
|
||||
* `name` : text to display and identify menu object (e.g. weather)
|
||||
* `img` : a 24x24px image
|
||||
* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date)
|
||||
* `items` : menu items such as temperature, humidity, wind etc.
|
||||
|
||||
Note that each item is an object with:
|
||||
|
|
@ -15,6 +15,7 @@ Note that each item is an object with:
|
|||
|
||||
{
|
||||
'text' // the text to display for this item
|
||||
'short' : (optional) a shorter text to display for this item (at most 6 characters)
|
||||
'img' // optional: a 24x24px image to display for this item
|
||||
'v' // (if hasRange==true) a numerical value
|
||||
'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage)
|
||||
|
|
@ -48,6 +49,15 @@ example.clkinfo.js :
|
|||
|
||||
*/
|
||||
|
||||
let storage = require("Storage");
|
||||
let stepGoal = undefined;
|
||||
// Load step goal from health app and pedometer widget
|
||||
let d = storage.readJSON("health.json", true) || {};
|
||||
stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined;
|
||||
if (stepGoal == undefined) {
|
||||
d = storage.readJSON("wpedom.json", true) || {};
|
||||
stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
|
||||
}
|
||||
|
||||
exports.load = function() {
|
||||
// info used for drawing...
|
||||
|
|
@ -81,7 +91,7 @@ exports.load = function() {
|
|||
{ name : "Steps",
|
||||
hasRange : true,
|
||||
get : () => { let v = Bangle.getHealthStatus("day").steps; return {
|
||||
text : v, v : v, min : 0, max : 10000, // TODO: do we have a target step amount anywhere?
|
||||
text : v, v : v, min : 0, max : stepGoal,
|
||||
img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==")
|
||||
}},
|
||||
show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); },
|
||||
|
|
|
|||
Loading…
Reference in New Issue