Merge branch 'master' into split-message-library

master
Gordon Williams 2022-11-28 14:24:10 +00:00 committed by GitHub
commit 20c153033e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1064 additions and 53 deletions

View File

@ -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
![](600.png)
@ -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/

View File

@ -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"],

View File

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

View File

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

View File

@ -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;
})();

View File

@ -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);

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Add lightning

View File

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

View File

@ -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"}
]
}

36
apps/f9lander/settings.js Normal file
View File

@ -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);
})

11
apps/hrmmar/README.md Normal file
View File

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

BIN
apps/hrmmar/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

40
apps/hrmmar/boot.js Normal file
View File

@ -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);
};
}

190
apps/hrmmar/fftelim.js Normal file
View File

@ -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;
}
}
}
}
});
};

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

@ -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"}]
}

26
apps/hrmmar/settings.js Normal file
View File

@ -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();
}
},
});
})

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -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"}
]

1
apps/qcenter/ChangeLog Normal file
View File

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

20
apps/qcenter/README.md Normal file
View File

@ -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.
![](screenshot.png)
## 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

1
apps/qcenter/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA="))

120
apps/qcenter/app.js Normal file
View File

@ -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();
});

BIN
apps/qcenter/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

View File

@ -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 }
]
}

BIN
apps/qcenter/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

141
apps/qcenter/settings.js Normal file
View File

@ -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();
});

View File

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

View File

@ -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",

View File

@ -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);

View File

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

73
apps/sched/clkinfo.js Normal file
View File

@ -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;
})

View File

@ -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"}]
}

View File

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

View File

@ -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();

View File

@ -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"}],

View File

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

View File

@ -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();

View File

@ -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"}],

8
apps/tetris/README.md Normal file
View File

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

1
apps/tetris/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/ABe5AA0jABwvYAIovBgABEFAQHFL7IuEL4QuFA45fcF4YuNL7i/FFwoHHL7QvFFxpfaF4wAOF/4nHF5+0AAy3SXYoHGW4QBDF4MAAIgvRFwwHHdAbqDFIQuDL6ouJL4ovDFwpfUAAoHFL4a/FFwhfTFxZfDF4ouFL6QANFopfDF/4vNjwAGF8ABFF4MAAIgvBX4IBDX4YBDL6TyFFIIuEL4QuEL4QuEL6ovDFwpfFF4YuFL6i/FFwhfEX4ouEL6YvFFwpfDF4ouFL6QvGAAwtFL4Yv/AAonHAB4vHG563CAIbuDA5i/CAIb2DA4hfJEwoHPFApZEGwpfLFyJfFFxJfMAAoHNFAa5GX54uTL4YuLL5QAVFowAIF+4A/AH4A/AH4A/AHY"))

14
apps/tetris/metadata.json Normal file
View File

@ -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}
]
}

170
apps/tetris/tetris.app.js Normal file
View File

@ -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);

BIN
apps/tetris/tetris.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

View File

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

View File

@ -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 () {}
},

View File

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

@ -1 +1 @@
Subproject commit aba9b6a51fe02dfbde307c303560b8382857916d
Subproject commit f3f54106b3d7f84927ff734b715023a49a87cc6f

View File

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

View File

@ -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(); },