Merge branch 'espruino:master' into Messages-Light
|
|
@ -4,4 +4,8 @@ apps/schoolCalendar/fullcalendar/main.js
|
|||
apps/authentiwatch/qr_packed.js
|
||||
apps/qrcode/qr-scanner.umd.min.js
|
||||
apps/gipy/pkg/gpconv.js
|
||||
apps/health/chart.min.js
|
||||
*.test.js
|
||||
|
||||
# typescript/generated files
|
||||
apps/btadv/*.js
|
||||
|
|
|
|||
|
|
@ -58,3 +58,7 @@ body:
|
|||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: apps
|
||||
attributes:
|
||||
label: Installed apps
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
|
@ -98,7 +98,7 @@ This is the best way to test...
|
|||
|
||||
**Note:** It's a great idea to get a local copy of the repository on your PC,
|
||||
then run `bin/sanitycheck.js` - it'll run through a bunch of common issues
|
||||
that there might be.
|
||||
that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule --init && git submodule update`.
|
||||
|
||||
Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,140 +1,142 @@
|
|||
class TwoK {
|
||||
constructor() {
|
||||
this.b = Array(4).fill().map(() => Array(4).fill(0));
|
||||
this.score = 0;
|
||||
this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
|
||||
}
|
||||
drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
|
||||
g.setColor(c);
|
||||
for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
|
||||
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
|
||||
}
|
||||
render() {
|
||||
const yo = 20;
|
||||
const xo = yo/2;
|
||||
h = g.getHeight()-yo;
|
||||
w = g.getWidth()-yo;
|
||||
bh = Math.floor(h/4);
|
||||
bw = Math.floor(w/4);
|
||||
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
|
||||
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
|
||||
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
|
||||
for (y=0; y<4; ++y)
|
||||
for (x=0; x<4; ++x) {
|
||||
b = this.b[y][x];
|
||||
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
|
||||
if (b > 4) g.setColor(1, 1, 1);
|
||||
else g.setColor(0, 0, 0);
|
||||
g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
|
||||
if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
|
||||
}
|
||||
}
|
||||
shift(d) { // +/-1: shift x, +/- 2: shift y
|
||||
var crc = E.CRC32(this.b.toString());
|
||||
if (d==-1) { // shift x left
|
||||
for (y=0; y<4; ++y) {
|
||||
for (x=2; x>=0; x--)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
|
||||
this.b[y][3] = 0;
|
||||
}
|
||||
for (x=0; x<3; ++x)
|
||||
if (this.b[y][x]==this.b[y][x+1]) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y][x+1];
|
||||
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
|
||||
{ // wrap app in scope to prevent minifier from removing the class definition completely
|
||||
class TwoK {
|
||||
constructor() {
|
||||
this.b = Array(4).fill().map(() => Array(4).fill(0));
|
||||
this.score = 0;
|
||||
this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
|
||||
}
|
||||
drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
|
||||
g.setColor(c);
|
||||
for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
|
||||
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
|
||||
}
|
||||
render() {
|
||||
const yo = 20;
|
||||
const xo = yo/2;
|
||||
h = g.getHeight()-yo;
|
||||
w = g.getWidth()-yo;
|
||||
bh = Math.floor(h/4);
|
||||
bw = Math.floor(w/4);
|
||||
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
|
||||
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
|
||||
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
|
||||
for (y=0; y<4; ++y)
|
||||
for (x=0; x<4; ++x) {
|
||||
b = this.b[y][x];
|
||||
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
|
||||
if (b > 4) g.setColor(1, 1, 1);
|
||||
else g.setColor(0, 0, 0);
|
||||
g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
|
||||
if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
|
||||
}
|
||||
}
|
||||
shift(d) { // +/-1: shift x, +/- 2: shift y
|
||||
var crc = E.CRC32(this.b.toString());
|
||||
if (d==-1) { // shift x left
|
||||
for (y=0; y<4; ++y) {
|
||||
for (x=2; x>=0; x--)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
|
||||
this.b[y][3] = 0;
|
||||
}
|
||||
}
|
||||
for (x=0; x<3; ++x)
|
||||
if (this.b[y][x]==this.b[y][x+1]) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y][x+1];
|
||||
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
|
||||
this.b[y][3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (d==1) { // shift x right
|
||||
for (y=0; y<4; ++y) {
|
||||
for (x=1; x<4; x++)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
|
||||
this.b[y][0] = 0;
|
||||
}
|
||||
for (x=3; x>0; --x)
|
||||
if (this.b[y][x]==this.b[y][x-1]) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y][x-1] ;
|
||||
for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
|
||||
this.b[y][0] = 0;
|
||||
}
|
||||
else if (d==1) { // shift x right
|
||||
for (y=0; y<4; ++y) {
|
||||
for (x=1; x<4; x++)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
|
||||
this.b[y][0] = 0;
|
||||
}
|
||||
for (x=3; x>0; --x)
|
||||
if (this.b[y][x]==this.b[y][x-1]) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y][x-1] ;
|
||||
for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
|
||||
this.b[y][0] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (d==-2) { // shift y down
|
||||
for (x=0; x<4; ++x) {
|
||||
for (y=1; y<4; y++)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
|
||||
this.b[0][x] = 0;
|
||||
}
|
||||
for (y=3; y>0; y--)
|
||||
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y-1][x];
|
||||
for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
|
||||
this.b[0][x] = 0;
|
||||
}
|
||||
else if (d==-2) { // shift y down
|
||||
for (x=0; x<4; ++x) {
|
||||
for (y=1; y<4; y++)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
|
||||
this.b[0][x] = 0;
|
||||
}
|
||||
for (y=3; y>0; y--)
|
||||
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y-1][x];
|
||||
for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
|
||||
this.b[0][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (d==2) { // shift y up
|
||||
for (x=0; x<4; ++x) {
|
||||
for (y=2; y>=0; y--)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
|
||||
this.b[3][x] = 0;
|
||||
}
|
||||
for (y=0; y<3; ++y)
|
||||
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y+1][x];
|
||||
for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
|
||||
this.b[3][x] = 0;
|
||||
}
|
||||
else if (d==2) { // shift y up
|
||||
for (x=0; x<4; ++x) {
|
||||
for (y=2; y>=0; y--)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
|
||||
this.b[3][x] = 0;
|
||||
}
|
||||
for (y=0; y<3; ++y)
|
||||
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y+1][x];
|
||||
for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
|
||||
this.b[3][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (E.CRC32(this.b.toString())!=crc);
|
||||
}
|
||||
addDigit() {
|
||||
var d = Math.random()>0.9 ? 4 : 2;
|
||||
var id = Math.floor(Math.random()*16);
|
||||
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
|
||||
this.b[Math.floor(id/4)][id%4] = d;
|
||||
}
|
||||
return (E.CRC32(this.b.toString())!=crc);
|
||||
}
|
||||
addDigit() {
|
||||
var d = Math.random()>0.9 ? 4 : 2;
|
||||
var id = Math.floor(Math.random()*16);
|
||||
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
|
||||
this.b[Math.floor(id/4)][id%4] = d;
|
||||
}
|
||||
}
|
||||
|
||||
function dragHandler(e) {
|
||||
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
|
||||
var res = false;
|
||||
if (Math.abs(e.dx)>Math.abs(e.dy)) {
|
||||
if (e.dx>0) res = twok.shift(1);
|
||||
if (e.dx<0) res = twok.shift(-1);
|
||||
function dragHandler(e) {
|
||||
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
|
||||
var res = false;
|
||||
if (Math.abs(e.dx)>Math.abs(e.dy)) {
|
||||
if (e.dx>0) res = twok.shift(1);
|
||||
if (e.dx<0) res = twok.shift(-1);
|
||||
}
|
||||
else {
|
||||
if (e.dy>0) res = twok.shift(-2);
|
||||
if (e.dy<0) res = twok.shift(2);
|
||||
}
|
||||
if (res) twok.addDigit();
|
||||
twok.render();
|
||||
}
|
||||
else {
|
||||
if (e.dy>0) res = twok.shift(-2);
|
||||
if (e.dy<0) res = twok.shift(2);
|
||||
}
|
||||
if (res) twok.addDigit();
|
||||
twok.render();
|
||||
}
|
||||
}
|
||||
|
||||
function swipeHandler() {
|
||||
|
||||
}
|
||||
function swipeHandler() {
|
||||
|
||||
}
|
||||
|
||||
function buttonHandler() {
|
||||
|
||||
}
|
||||
function buttonHandler() {
|
||||
|
||||
}
|
||||
|
||||
var twok = new TwoK();
|
||||
twok.addDigit(); twok.addDigit();
|
||||
twok.render();
|
||||
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
|
||||
if (process.env.HWVERSION==1) {
|
||||
Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
|
||||
setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
|
||||
setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
|
||||
}
|
||||
var twok = new TwoK();
|
||||
twok.addDigit(); twok.addDigit();
|
||||
twok.render();
|
||||
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
|
||||
if (process.env.HWVERSION==1) {
|
||||
Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
|
||||
setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
|
||||
setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New app!
|
||||
0.02: Better support for watch themes
|
||||
0.03: Workaround minifier bug
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "2047pp",
|
||||
"shortName":"2047pp",
|
||||
"icon": "app.png",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "Bangle version of a tile shifting game",
|
||||
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fullscreen settings.
|
||||
0.03: Tell clock widgets to hide.
|
||||
0.04: Use widget_utils.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const SETTINGS_FILE = "90sclk.setting.json";
|
||||
const locale = require('locale');
|
||||
const storage = require('Storage');
|
||||
const widget_utils = require('widget_utils');
|
||||
|
||||
|
||||
/*
|
||||
|
|
@ -109,7 +110,7 @@ function draw() {
|
|||
|
||||
// Draw widgets if not fullscreen
|
||||
if(settings.fullscreen){
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
widget_utils.hide();
|
||||
} else {
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "90sclk",
|
||||
"name": "90s Clock",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "A 90s style watch-face",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
0.01: Beta version for Bangle 2 (2021/11/28)
|
||||
0.02: Shows night time on the map (2022/12/28)
|
||||
0.03: Add 1 minute timer with upper taps (2023/01/05)
|
||||
1.00: Page to set up custom time zones (2023/01/06)
|
||||
|
|
@ -2,14 +2,17 @@
|
|||
|
||||
* Works with Bangle 2
|
||||
* Timer
|
||||
* Right tap: start/increase by 10 minutes; Left tap: decrease by 5 minutes
|
||||
* Top Right tap: increase by 1 minute
|
||||
* Top Left tap: decrease by 1 minute
|
||||
* Bottom Right tap: increase by 10 minutes
|
||||
* Bottom Left tap: decrease by 5 minutes
|
||||
* Short buzz at T-30, T-20, T-10 ; Double buzz at T
|
||||
* Other time zones
|
||||
* Currently hardcoded to Paris and Tokyo (this will be customizable in a future version)
|
||||
* Showing Paris and Tokyo by default, but you can customize this using the dedicated configuration page on the app store
|
||||
* World Map
|
||||
* The yellow line shows the position of the sun
|
||||
* The map shows day and night on Earth and the position of the Sun (yellow line)
|
||||
|
||||

|
||||
 
|
||||
|
||||
## Creator
|
||||
[@alainsaas](https://github.com/alainsaas)
|
||||
|
|
|
|||
|
|
@ -18,19 +18,29 @@ var timervalue = 0;
|
|||
var istimeron = false;
|
||||
var timertick;
|
||||
|
||||
Bangle.on('touch',t=>{
|
||||
if (t == 1) {
|
||||
Bangle.on('touch',(touchside, touchdata)=>{
|
||||
if (touchside == 1) {
|
||||
Bangle.buzz(30);
|
||||
if (timervalue < 5*60) { timervalue = 1 ; }
|
||||
else { timervalue -= 5*60; }
|
||||
var changevalue = 0;
|
||||
if(touchdata.y > 88) {
|
||||
changevalue += 60*5;
|
||||
} else {
|
||||
changevalue += 60*1;
|
||||
}
|
||||
if (timervalue < changevalue) { timervalue = 1 ; }
|
||||
else { timervalue -= changevalue; }
|
||||
}
|
||||
else if (t == 2) {
|
||||
else if (touchside == 2) {
|
||||
Bangle.buzz(30);
|
||||
if (!istimeron) {
|
||||
istimeron = true;
|
||||
timertick = setInterval(countDown, 1000);
|
||||
}
|
||||
timervalue += 60*10;
|
||||
if(touchdata.y > 88) {
|
||||
timervalue += 60*10;
|
||||
} else {
|
||||
timervalue += 60*1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -73,12 +83,13 @@ function countDown() {
|
|||
function showWelcomeMessage() {
|
||||
g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6);
|
||||
g.setFontAlign(0, 0).setFont("6x8");
|
||||
g.drawString("Touch right to", 44, 80);
|
||||
g.drawString("Tap right to", 44, 80);
|
||||
g.drawString("start timer", 44, 88);
|
||||
setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000);
|
||||
}
|
||||
|
||||
// time
|
||||
var offsets = require("Storage").readJSON("a_clock_timer.settings.json") || [ ["PAR",1], ["TYO",9] ];
|
||||
var drawTimeout;
|
||||
|
||||
function getGmt() {
|
||||
|
|
@ -102,20 +113,34 @@ function queueNextDraw() {
|
|||
function draw() {
|
||||
g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT);
|
||||
g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT);
|
||||
|
||||
var x_sun = 176 - (getGmt().getHours() / 24 * 176 + 4);
|
||||
|
||||
var gmtHours = getGmt().getHours();
|
||||
|
||||
var x_sun = 176 - (gmtHours / 24 * 176 + 4);
|
||||
g.setColor('#ff0').drawLine(x_sun, g.getHeight()-IMAGEHEIGHT, x_sun, g.getHeight());
|
||||
g.reset();
|
||||
|
||||
var x_night_start = (176 - (((gmtHours-6)%24) / 24 * 176 + 4)) % 176;
|
||||
var x_night_end = 176 - (((gmtHours+6)%24) / 24 * 176 + 4);
|
||||
g.setColor('#000');
|
||||
for (let x = x_night_start; x < (x_night_end < x_night_start ? 176 : x_night_end); x+=2) {
|
||||
g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight());
|
||||
}
|
||||
if (x_night_end < x_night_start) {
|
||||
for (let x = 0; x < x_night_end; x+=2) {
|
||||
g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
var locale = require("locale");
|
||||
|
||||
|
||||
var date = new Date();
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46);
|
||||
g.setFont("6x8");
|
||||
g.drawString(locale.date(new Date(),1), 125, 68);
|
||||
g.drawString("PAR "+locale.time(getTimeFromTimezone(1),1), 125, 80);
|
||||
g.drawString("TYO "+locale.time(getTimeFromTimezone(9),1), 125, 88);
|
||||
g.drawString(offsets[0][0]+" "+locale.time(getTimeFromTimezone(offsets[0][1]),1), 125, 80);
|
||||
g.drawString(offsets[1][0]+" "+locale.time(getTimeFromTimezone(offsets[1][1]),1), 125, 88);
|
||||
|
||||
queueNextDraw();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<p>You can set the 2 additional timezones displayed by the clock.</p>
|
||||
<table id="a_clock_timer-offsets">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>UTC Offset (Hours)</th>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<script>
|
||||
var offsets=[];
|
||||
try{
|
||||
var stored = localStorage.getItem('a_clock_timer-offset-list')
|
||||
if(stored) offsets = JSON.parse(stored);
|
||||
if (!offsets || offsets.length!=2) {
|
||||
throw "Offsets invalid";
|
||||
}
|
||||
} catch(e){
|
||||
offsets=[
|
||||
["PAR",1],
|
||||
["TYO",9],
|
||||
];
|
||||
}
|
||||
console.log(offsets);
|
||||
var tbl=document.getElementById("a_clock_timer-offsets");
|
||||
for (var i=0; i<2; i++) {
|
||||
var $offset = document.createElement('tr')
|
||||
$offset.innerHTML = `
|
||||
<td><input type="text" size="4" maxlength="3" id="name_${i}" value="${offsets[i][0]}"></td>
|
||||
<td><input type="number" id="offset_${i}" value="${offsets[i][1]}"></td>`
|
||||
tbl.append($offset);
|
||||
}
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
var storage_offsets=[];
|
||||
var app_offsets=[];
|
||||
for (var i=0; i<2; i++) {
|
||||
var name=document.getElementById("name_"+i).value;
|
||||
var offset=document.getElementById("offset_"+i).value;
|
||||
app_offsets.push([name,offset]);
|
||||
storage_offsets.push([name,offset]);
|
||||
}
|
||||
console.log(storage_offsets);
|
||||
console.log(app_offsets);
|
||||
localStorage.setItem('a_clock_timer-offset-list',JSON.stringify(storage_offsets));
|
||||
sendCustomizedApp({
|
||||
storage:[
|
||||
{name:"a_clock_timer.settings.json", content:JSON.stringify(app_offsets)},
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
{
|
||||
"id": "a_clock_timer",
|
||||
"name": "A Clock with Timer",
|
||||
"version": "0.01",
|
||||
"version": "1.00",
|
||||
"description": "A Clock with Timer, Map and Time Zones",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-1.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"custom": "custom.html",
|
||||
"storage": [
|
||||
{"name":"a_clock_timer.app.js","url":"app.js"},
|
||||
{"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
],
|
||||
"data": [{"name":"a_clock_timer.settings.json"}]
|
||||
}
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
|
@ -1,2 +1,3 @@
|
|||
1.00: Release (2021/12/01)
|
||||
1.01: Grey font when timer is frozen (2021/12/04)
|
||||
1.02: Force light theme, since the app is not designed for dark theme (2022/12/28)
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ function draw() {
|
|||
g.drawRect(88+8,138-24, 176-10, 138+22);
|
||||
}
|
||||
|
||||
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
|
||||
require("FontHaxorNarrow7x17").add(Graphics);
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id":"a_speech_timer",
|
||||
"name":"Speech Timer",
|
||||
"icon": "app.png",
|
||||
"version":"1.01",
|
||||
"version":"1.02",
|
||||
"description": "A timer designed to help keeping your speeches and presentations to time.",
|
||||
"tags": "tool,timer",
|
||||
"readme":"README.md",
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Exit as first menu option, dont show decimal places for seconds
|
||||
0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware
|
||||
0.05: Add max G values during recording, record actual G values and magnitude to CSV
|
||||
0.06: Convert Yes/No On/Off in settings to checkboxes
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ function showMenu() {
|
|||
viewLogs();
|
||||
},
|
||||
/*LANG*/"Log raw data" : {
|
||||
value : logRawData,
|
||||
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
|
||||
value : !!logRawData,
|
||||
onchange : v => { logRawData=v; }
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "accellog",
|
||||
"name": "Acceleration Logger",
|
||||
"shortName": "Accel Log",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
|
||||
"icon": "app.png",
|
||||
"tags": "outdoor",
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@
|
|||
0.08: Use default Bangle formatter for booleans
|
||||
0.09: New app screen (instead of showing settings or the alert) and some optimisations
|
||||
0.10: Add software back button via setUI
|
||||
0.11: Add setting to unlock screen
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@
|
|||
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
|
||||
Bangle.buzz(400);
|
||||
}
|
||||
|
||||
if ((storage.readJSON('activityreminder.s.json', 1) || {}).unlock) {
|
||||
Bangle.setLocked(false);
|
||||
Bangle.setLCDPower(1);
|
||||
}
|
||||
|
||||
setTimeout(load, 20000);
|
||||
}
|
||||
|
||||
|
|
@ -34,4 +40,4 @@
|
|||
Bangle.drawWidgets();
|
||||
run();
|
||||
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Activity Reminder",
|
||||
"shortName":"Activity Reminder",
|
||||
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
|
||||
"version":"0.10",
|
||||
"version":"0.11",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,activity",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,14 @@
|
|||
settings.tempThreshold = v;
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
}
|
||||
},
|
||||
'Unlock on alarm': {
|
||||
value: !!settings.unlock,
|
||||
onchange: v => {
|
||||
settings.unlock = v;
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return mainMenu;
|
||||
|
|
|
|||
|
|
@ -9,3 +9,7 @@
|
|||
Fix clkinfo icon
|
||||
0.09: Ensure Agenda supplies an image for clkinfo items
|
||||
0.10: Update clock_info to avoid a redraw
|
||||
0.11: Setting to use "Today" and "Yesterday" instead of dates
|
||||
Added dynamic, short and range fields to clkinfo
|
||||
0.12: Added color field and updating clkinfo periodically (running events)
|
||||
0.13: Show day of the week in date
|
||||
|
|
|
|||
|
|
@ -1,7 +1,59 @@
|
|||
(function() {
|
||||
function getPassedSec(date) {
|
||||
var now = new Date();
|
||||
var passed = (now-date)/1000;
|
||||
if(passed<0) return 0;
|
||||
return passed;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the array [interval, switchTimeout]
|
||||
* `interval` is the refresh rate (hourly or per minute)
|
||||
* `switchTimeout` is the time before the refresh rate should change (or expiration)
|
||||
*/
|
||||
function getRefreshIntervals(ev) {
|
||||
const threshold = 2 * 60 * 1000; //2 mins
|
||||
const slices = 16;
|
||||
var now = new Date();
|
||||
var passed = now - (ev.timestamp*1000);
|
||||
var remaining = (ev.durationInSeconds*1000) - passed;
|
||||
if(remaining<0)
|
||||
return [];
|
||||
if(passed<0) //check once it's started
|
||||
return [ 2*-passed, -passed ];
|
||||
var slice = Math.round(remaining/slices);
|
||||
if(slice < threshold) { //no need to refresh frequently
|
||||
return [ threshold, remaining ];
|
||||
}
|
||||
return [ slice, remaining ];
|
||||
}
|
||||
|
||||
function _doInterval(interval) {
|
||||
return setTimeout(()=>{
|
||||
this.emit("redraw");
|
||||
this.interval = setInterval(()=>{
|
||||
this.emit("redraw");
|
||||
}, interval);
|
||||
}, interval);
|
||||
}
|
||||
function _doSwitchTimeout(ev, switchTimeout) {
|
||||
return setTimeout(()=>{
|
||||
this.emit("redraw");
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
var tmp = getRefreshIntervals(ev);
|
||||
var interval = tmp[0];
|
||||
var switchTimeout = tmp[1];
|
||||
if(!interval) return;
|
||||
this.interval = _doInterval.call(this, interval);
|
||||
this.switchTimeout = _doSwitchTimeout.call(this, ev, switchTimeout);
|
||||
}, switchTimeout);
|
||||
}
|
||||
|
||||
var agendaItems = {
|
||||
name: "Agenda",
|
||||
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
|
||||
dynamic: true,
|
||||
items: []
|
||||
};
|
||||
var locale = require("locale");
|
||||
|
|
@ -15,13 +67,34 @@
|
|||
var title = entry.title.slice(0,12);
|
||||
var date = new Date(entry.timestamp*1000);
|
||||
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
|
||||
var shortStr = ((date-now) > 86400000 || entry.allDay) ? dateStr : locale.time(date,1);
|
||||
var color = "#"+(0x1000000+Number(entry.color)).toString(16).padStart(6,"0");
|
||||
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
|
||||
shortStr = shortStr.trim().replace(" ", "\n");
|
||||
|
||||
agendaItems.items.push({
|
||||
name: "Agenda "+i,
|
||||
get: () => ({ text: title + "\n" + dateStr, img: agendaItems.img }),
|
||||
show: function() {},
|
||||
hide: function () {}
|
||||
hasRange: true,
|
||||
get: () => ({ text: title + "\n" + dateStr,
|
||||
img: agendaItems.img, short: shortStr,
|
||||
color: color,
|
||||
v: getPassedSec(date), min: 0, max: entry.durationInSeconds}),
|
||||
show: function() {
|
||||
var tmp = getRefreshIntervals(entry);
|
||||
var interval = tmp[0];
|
||||
var switchTimeout = tmp[1];
|
||||
if(!interval) return;
|
||||
this.interval = _doInterval.call(this, interval);
|
||||
this.switchTimeout = _doSwitchTimeout.call(this, entry, switchTimeout);
|
||||
},
|
||||
hide: function() {
|
||||
if(this.interval)
|
||||
clearInterval(this.interval);
|
||||
if(this.switchTimeout)
|
||||
clearTimeout(this.switchTimeout);
|
||||
this.interval = undefined;
|
||||
this.switchTimeout = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -33,16 +33,33 @@ CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
|
|||
function getDate(timestamp) {
|
||||
return new Date(timestamp*1000);
|
||||
}
|
||||
function formatDay(date) {
|
||||
let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,"");
|
||||
if (!settings.useToday) {
|
||||
return formattedDate;
|
||||
}
|
||||
const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd
|
||||
const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd
|
||||
if (dateformatted == today) {
|
||||
return /*LANG*/"Today ";
|
||||
} else {
|
||||
const tomorrow = new Date(Date.now() + 86400 * 1000).toISOString().split('T')[0]; // yyyy-mm-dd
|
||||
if (dateformatted == tomorrow) {
|
||||
return /*LANG*/"Tomorrow ";
|
||||
}
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
function formatDateLong(date, includeDay, allDay) {
|
||||
let shortTime = Locale.time(date,1)+Locale.meridian(date);
|
||||
if(allDay) shortTime = "";
|
||||
if(includeDay || allDay)
|
||||
return Locale.date(date)+" "+shortTime;
|
||||
if(includeDay || allDay) {
|
||||
return formatDay(date)+" "+shortTime;
|
||||
}
|
||||
return shortTime;
|
||||
}
|
||||
function formatDateShort(date, allDay) {
|
||||
return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay?
|
||||
"" : Locale.time(date,1)+Locale.meridian(date));
|
||||
return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
|
||||
}
|
||||
|
||||
var lines = [];
|
||||
|
|
@ -59,25 +76,29 @@ function showEvent(ev) {
|
|||
if (titleCnt) lines.push(""); // add blank line after title
|
||||
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
|
||||
includeDay = false;
|
||||
if(includeDay || ev.allDay) {
|
||||
if(includeDay && ev.allDay) {
|
||||
//single day all day (average to avoid getting previous day)
|
||||
lines = lines.concat(
|
||||
/*LANG*/"Start:",
|
||||
g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10));
|
||||
} else if(includeDay || ev.allDay) {
|
||||
lines = lines.concat(
|
||||
/*LANG*/"Start"+":",
|
||||
g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||
/*LANG*/"End:",
|
||||
/*LANG*/"End"+":",
|
||||
g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||
} else {
|
||||
lines = lines.concat(
|
||||
g.wrapString(Locale.date(start), g.getWidth()-10),
|
||||
g.wrapString(formatDateShort(start,true), g.getWidth()-10),
|
||||
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||
}
|
||||
if(ev.location)
|
||||
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
||||
if(ev.description)
|
||||
lines = lines.concat("",/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
||||
if(ev.description && ev.description.trim())
|
||||
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
|
||||
if(ev.calName)
|
||||
lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
|
||||
lines = lines.concat(["",/*LANG*/"< Back"]);
|
||||
lines = lines.concat("",/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
|
||||
lines = lines.concat("",/*LANG*/"< Back");
|
||||
E.showScroller({
|
||||
h : g.getFontHeight(), // height of each menu item in pixels
|
||||
c : lines.length, // number of menu items
|
||||
|
|
@ -104,7 +125,7 @@ function showList() {
|
|||
CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
|
||||
}
|
||||
if(CALENDAR.length == 0) {
|
||||
E.showMessage("No events");
|
||||
E.showMessage(/*LANG*/"No events");
|
||||
return;
|
||||
}
|
||||
E.showScroller({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "agenda",
|
||||
"name": "Agenda",
|
||||
"version": "0.10",
|
||||
"version": "0.13",
|
||||
"description": "Simple agenda",
|
||||
"icon": "agenda.png",
|
||||
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@
|
|||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Use 'Today',..." : {
|
||||
value : !!settings.useToday,
|
||||
onchange: v => {
|
||||
settings.useToday = v;
|
||||
updateSettings();
|
||||
}
|
||||
},
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@
|
|||
0.03: Do not load AGPS data on boot
|
||||
Increase minimum interval to 6 hours
|
||||
0.04: Write AGPS data chunks with delay to improve reliability
|
||||
0.05: Show last success date
|
||||
Do not start A-GPS update automatically
|
||||
0.06: Switch off gps after updating
|
||||
|
|
@ -23,12 +23,26 @@ Bangle.drawWidgets();
|
|||
|
||||
let waiting = false;
|
||||
|
||||
function start() {
|
||||
function start(restart) {
|
||||
g.reset();
|
||||
g.clear();
|
||||
waiting = false;
|
||||
display("Retry?", "touch to retry");
|
||||
if (!restart) {
|
||||
display("Start?", "touch to start");
|
||||
}
|
||||
else {
|
||||
display("Retry?", "touch to retry");
|
||||
}
|
||||
Bangle.on("touch", () => { updateAgps(); });
|
||||
|
||||
const file = "agpsdata.json";
|
||||
let data = require("Storage").readJSON(file, 1) || {};
|
||||
if (data.lastUpdate) {
|
||||
g.setFont("Vector", 11);
|
||||
g.drawString("last success:", 5, g.getHeight() - 22);
|
||||
g.drawString(new Date(data.lastUpdate).toISOString(), 5, g.getHeight() - 11);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateAgps() {
|
||||
|
|
@ -36,7 +50,7 @@ function updateAgps() {
|
|||
g.clear();
|
||||
if (!waiting) {
|
||||
waiting = true;
|
||||
display("Updating A-GPS...", "takes ~ 10 seconds");
|
||||
display("Updating A-GPS...", "takes ~10 seconds");
|
||||
require("agpsdata").pull(function() {
|
||||
waiting = false;
|
||||
display("A-GPS updated.", "touch to close");
|
||||
|
|
@ -45,10 +59,10 @@ function updateAgps() {
|
|||
function(error) {
|
||||
waiting = false;
|
||||
E.showAlert(error, "Error")
|
||||
.then(() => { start(); });
|
||||
.then(() => { start(true); });
|
||||
});
|
||||
} else {
|
||||
display("Waiting...");
|
||||
}
|
||||
}
|
||||
updateAgps();
|
||||
start(false);
|
||||
|
|
|
|||
|
|
@ -10,20 +10,24 @@ readSettings();
|
|||
|
||||
function setAGPS(b64) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
|
||||
const gnsstype = settings.gnsstype || 1; // default GPS
|
||||
initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode
|
||||
// What about:
|
||||
// NAV-TIMEUTC (0x01 0x10)
|
||||
// NAV-PV (0x01 0x03)
|
||||
// or AGPS.zip uses AID-INI (0x0B 0x01)
|
||||
|
||||
eval(initCommands);
|
||||
Bangle.setGPSPower(1,"agpsdata"); // turn GPS on
|
||||
Serial1.println(CASIC_CHECKSUM("$PCAS04," + gnsstype)); // set GNSS mode
|
||||
|
||||
try {
|
||||
writeChunks(atob(b64), resolve);
|
||||
writeChunks(atob(b64), ()=>{
|
||||
setTimeout(()=>{
|
||||
Bangle.setGPSPower(0,"agpsdata");
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("error:", e);
|
||||
Bangle.setGPSPower(0,"agpsdata");
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
|
@ -36,9 +40,8 @@ function writeChunks(bin, resolve) {
|
|||
setTimeout(function() {
|
||||
if (chunkI < bin.length) {
|
||||
var chunk = bin.substr(chunkI, chunkSize);
|
||||
js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
|
||||
eval(js);
|
||||
|
||||
Serial1.write(atob(btoa(chunk)));
|
||||
|
||||
chunkI += chunkSize;
|
||||
writeChunks(bin, resolve);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "A-GPS Data Downloader App",
|
||||
"shortName":"A-GPS Data",
|
||||
"icon": "agpsdata.png",
|
||||
"version":"0.04",
|
||||
"version":"0.06",
|
||||
"description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
|
||||
"tags": "boot,tool,assisted,gps,agps,http",
|
||||
"allow_emulator":true,
|
||||
|
|
|
|||
|
|
@ -2,4 +2,7 @@
|
|||
0.02: Design improvements and fixes.
|
||||
0.03: Indicate battery level through line occurrence.
|
||||
0.04: Use widget_utils module.
|
||||
0.05: Support for clkinfo.
|
||||
0.05: Support for clkinfo.
|
||||
0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc.
|
||||
0.07: Use clock_info.addInteractive instead of a custom implementation
|
||||
0.08: Use clock_info module as an app
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ The original output of stable diffusion is shown here:
|
|||
My implementation is shown below. Note that horizontal lines occur randomly, but the
|
||||
probability is correlated with the battery level. So if your screen contains only
|
||||
a few lines its time to charge your bangle again ;) Also note that the upper text
|
||||
implementes the clkinfo module and can be configured via touch left/right/up/down.
|
||||
Touch at the center to trigger the selected action.
|
||||
implements the clkinfo module and can be configured via touch and swipe left/right and up/down.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/************************************************
|
||||
* AI Clock
|
||||
*/
|
||||
const storage = require('Storage');
|
||||
const clock_info = require("clock_info");
|
||||
|
||||
|
||||
|
|
@ -21,147 +20,14 @@ Graphics.prototype.setFontGochiHand = function(scale) {
|
|||
return this;
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* Set some important constants such as width, height and center
|
||||
*/
|
||||
var W = g.getWidth(),R=W/2;
|
||||
var H = g.getHeight();
|
||||
var cx = W/2;
|
||||
var cy = H/2;
|
||||
var drawTimeout;
|
||||
var lock_input = false;
|
||||
|
||||
|
||||
/************************************************
|
||||
* SETTINGS
|
||||
*/
|
||||
const SETTINGS_FILE = "aiclock.setting.json";
|
||||
let settings = {
|
||||
menuPosX: 0,
|
||||
menuPosY: 0,
|
||||
};
|
||||
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
||||
for (const key in saved_settings) {
|
||||
settings[key] = saved_settings[key]
|
||||
}
|
||||
|
||||
|
||||
/************************************************
|
||||
* Menu
|
||||
*/
|
||||
function getDate(){
|
||||
var date = new Date();
|
||||
return ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2)
|
||||
}
|
||||
|
||||
|
||||
// Custom clockItems menu - therefore, its added here and not in a clkinfo.js file.
|
||||
var clockItems = {
|
||||
name: getDate(),
|
||||
img: null,
|
||||
items: [
|
||||
{ name: "Week",
|
||||
get: () => ({ text: "Week " + weekOfYear(), img: null}),
|
||||
show: function() { clockItems.items[0].emit("redraw"); },
|
||||
hide: function () {}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
function weekOfYear() {
|
||||
var date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// Thursday in current week decides the year.
|
||||
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
||||
// January 4 is always in week 1.
|
||||
var week1 = new Date(date.getFullYear(), 0, 4);
|
||||
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
|
||||
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
|
||||
- 3 + (week1.getDay() + 6) % 7) / 7);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Load menu
|
||||
var menu = clock_info.load();
|
||||
menu = menu.concat(clockItems);
|
||||
|
||||
|
||||
// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
|
||||
if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
|
||||
settings.menuPosX = 0;
|
||||
settings.menuPosY = 0;
|
||||
}
|
||||
|
||||
// Set draw functions for each item
|
||||
menu.forEach((menuItm, x) => {
|
||||
menuItm.items.forEach((item, y) => {
|
||||
function drawItem() {
|
||||
// For the clock, we have a special case, as we don't wanna redraw
|
||||
// immediately when something changes. Instead, we update data each minute
|
||||
// to save some battery etc. Therefore, we hide (and disable the listener)
|
||||
// immedeately after redraw...
|
||||
item.hide();
|
||||
|
||||
// After drawing the item, we enable inputs again...
|
||||
lock_input = false;
|
||||
|
||||
var info = item.get();
|
||||
drawMenuItem(info.text, info.img);
|
||||
}
|
||||
|
||||
item.on('redraw', drawItem);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
function canRunMenuItem(){
|
||||
if(settings.menuPosY == 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
var menuEntry = menu[settings.menuPosX];
|
||||
var item = menuEntry.items[settings.menuPosY-1];
|
||||
return item.run !== undefined;
|
||||
}
|
||||
|
||||
|
||||
function runMenuItem(){
|
||||
if(settings.menuPosY == 0){
|
||||
return;
|
||||
}
|
||||
|
||||
var menuEntry = menu[settings.menuPosX];
|
||||
var item = menuEntry.items[settings.menuPosY-1];
|
||||
try{
|
||||
var ret = item.run();
|
||||
if(ret){
|
||||
Bangle.buzz(300, 0.6);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Simply ignore it...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Based on the great multi clock from https://github.com/jeffmer/BangleApps/
|
||||
*/
|
||||
Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
|
||||
angle = angle % 360;
|
||||
var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
|
||||
return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
|
||||
{x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
|
||||
};
|
||||
|
||||
|
||||
function drawBackground() {
|
||||
function drawBackground(start, end) {
|
||||
g.setFontAlign(0,0);
|
||||
g.setColor(g.theme.fg);
|
||||
g.setColor("#000");
|
||||
|
||||
var bat = E.getBattery() / 100.0;
|
||||
var y = 0;
|
||||
while(y < H){
|
||||
var y = start;
|
||||
while(y < end){
|
||||
// Show less lines in case of small battery level.
|
||||
if(Math.random() > bat){
|
||||
y += 5;
|
||||
|
|
@ -177,6 +43,30 @@ function drawBackground() {
|
|||
}
|
||||
|
||||
|
||||
/************************************************
|
||||
* Set some important constants such as width, height and center
|
||||
*/
|
||||
var W = g.getWidth(),R=W/2;
|
||||
var H = g.getHeight();
|
||||
var cx = W/2;
|
||||
var cy = H/2;
|
||||
var drawTimeout;
|
||||
|
||||
var clkInfoY = 60;
|
||||
|
||||
|
||||
/*
|
||||
* Based on the great multi clock from https://github.com/jeffmer/BangleApps/
|
||||
*/
|
||||
Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
|
||||
angle = angle % 360;
|
||||
var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
|
||||
return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
|
||||
{x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
|
||||
};
|
||||
|
||||
|
||||
|
||||
function drawCircle(isLocked){
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillCircle(cx, cy, 12);
|
||||
|
|
@ -186,56 +76,6 @@ function drawCircle(isLocked){
|
|||
g.fillCircle(cx, cy, 6);
|
||||
}
|
||||
|
||||
function toAngle(a){
|
||||
if (a < 0){
|
||||
return 360 + a;
|
||||
}
|
||||
|
||||
if(a > 360) {
|
||||
return 360 - a;
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
|
||||
function drawMenuItem(text, image){
|
||||
if(text == null){
|
||||
drawTime();
|
||||
return
|
||||
}
|
||||
// image = atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==");
|
||||
|
||||
text = String(text);
|
||||
|
||||
g.reset().setBgColor("#fff").setColor("#000");
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont("Vector", 20);
|
||||
|
||||
var imgWidth = image == null ? 0 : 24;
|
||||
var strWidth = g.stringWidth(text);
|
||||
var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2);
|
||||
var w = imgWidth + strWidth;
|
||||
|
||||
g.clearRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2)
|
||||
|
||||
// Draw right line as designed by stable diffusion
|
||||
g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2);
|
||||
g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2);
|
||||
g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2);
|
||||
|
||||
// And finally the text
|
||||
g.drawString(text, cx+imgWidth/2, 42);
|
||||
g.drawString(text, cx+1+imgWidth/2, 41);
|
||||
|
||||
if(image != null) {
|
||||
var scale = image.width ? imgWidth / image.width : 1;
|
||||
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale});
|
||||
}
|
||||
|
||||
drawTime();
|
||||
}
|
||||
|
||||
|
||||
function drawTime(){
|
||||
// Draw digital time first
|
||||
|
|
@ -292,35 +132,23 @@ function drawDigits(){
|
|||
}
|
||||
|
||||
|
||||
function drawDate(){
|
||||
var menuEntry = menu[settings.menuPosX];
|
||||
|
||||
// The first entry is the overview...
|
||||
if(settings.menuPosY == 0){
|
||||
drawMenuItem(menuEntry.name, menuEntry.img);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw item if needed
|
||||
lock_input = true;
|
||||
var item = menuEntry.items[settings.menuPosY-1];
|
||||
item.show();
|
||||
function draw(){
|
||||
// Note that we force a redraw also of the clock info as
|
||||
// we want to ensure (for design purpose) that the hands
|
||||
// are above the clkinfo section.
|
||||
clockInfoMenu.redraw();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function draw(){
|
||||
function drawMainClock(){
|
||||
// Queue draw in one minute
|
||||
queueDraw();
|
||||
|
||||
g.reset();
|
||||
g.clearRect(0, 0, g.getWidth(), g.getHeight());
|
||||
g.setColor(1,1,1);
|
||||
g.setColor("#fff");
|
||||
g.reset().clearRect(0, clkInfoY, g.getWidth(), g.getHeight());
|
||||
|
||||
drawBackground();
|
||||
drawDate();
|
||||
drawBackground(clkInfoY, H);
|
||||
drawTime();
|
||||
drawCircle(Bangle.isLocked());
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +158,7 @@ function draw(){
|
|||
*/
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (on) {
|
||||
draw(true);
|
||||
draw();
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
|
|
@ -341,66 +169,10 @@ Bangle.on('lock', function(isLocked) {
|
|||
drawCircle(isLocked);
|
||||
});
|
||||
|
||||
Bangle.on('touch', function(btn, e){
|
||||
var left = parseInt(g.getWidth() * 0.22);
|
||||
var right = g.getWidth() - left;
|
||||
var upper = parseInt(g.getHeight() * 0.22);
|
||||
var lower = g.getHeight() - upper;
|
||||
|
||||
var is_upper = e.y < upper;
|
||||
var is_lower = e.y > lower;
|
||||
var is_left = e.x < left && !is_upper && !is_lower;
|
||||
var is_right = e.x > right && !is_upper && !is_lower;
|
||||
var is_center = !is_upper && !is_lower && !is_left && !is_right;
|
||||
|
||||
if(lock_input){
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_lower){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
if(is_upper){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosY = settings.menuPosY-1;
|
||||
settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
if(is_right){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosX = (settings.menuPosX+1) % menu.length;
|
||||
settings.menuPosY = 0;
|
||||
draw();
|
||||
}
|
||||
|
||||
if(is_left){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosY = 0;
|
||||
settings.menuPosX = settings.menuPosX-1;
|
||||
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
|
||||
draw();
|
||||
}
|
||||
|
||||
if(is_center){
|
||||
if(canRunMenuItem()){
|
||||
runMenuItem();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
E.on("kill", function(){
|
||||
try{
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
} catch(ex){
|
||||
// If this fails, we still kill the app...
|
||||
}
|
||||
clockInfoMenu.remove();
|
||||
delete clockInfoMenu;
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -416,6 +188,55 @@ function queueDraw() {
|
|||
}
|
||||
|
||||
|
||||
/************************************************
|
||||
* Clock Info
|
||||
*/
|
||||
let clockInfoItems = clock_info.load();
|
||||
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
|
||||
x : 0,
|
||||
y: 0,
|
||||
w: W,
|
||||
h: clkInfoY,
|
||||
draw : (itm, info, options) => {
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont("Vector", 20);
|
||||
|
||||
g.setColor("#fff");
|
||||
g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
|
||||
drawBackground(0, clkInfoY+2);
|
||||
|
||||
// Set text and font
|
||||
var image = info.img;
|
||||
var text = String(info.text);
|
||||
|
||||
var imgWidth = image == null ? 0 : 24;
|
||||
var strWidth = g.stringWidth(text);
|
||||
var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2);
|
||||
var w = imgWidth + strWidth;
|
||||
|
||||
// Draw right line as designed by stable diffusion
|
||||
g.setColor(options.focus ? "#0f0" : "#fff");
|
||||
g.fillRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2)
|
||||
|
||||
g.setColor("#000");
|
||||
g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2);
|
||||
g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2);
|
||||
g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2);
|
||||
|
||||
// Draw text and image
|
||||
g.drawString(text, cx+imgWidth/2, 42);
|
||||
g.drawString(text, cx+1+imgWidth/2, 41);
|
||||
|
||||
if(image != null) {
|
||||
var scale = image.width ? imgWidth / image.width : 1;
|
||||
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale});
|
||||
}
|
||||
|
||||
drawMainClock();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* Lets start widgets, listen for btn etc.
|
||||
*/
|
||||
|
|
@ -430,7 +251,7 @@ Bangle.loadWidgets();
|
|||
require('widget_utils').hide();
|
||||
|
||||
// Clear the screen once, at startup and draw clock
|
||||
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
|
||||
g.setTheme({bg:"#fff",fg:"#000",dark:false});
|
||||
draw();
|
||||
|
||||
// After drawing the watch face, we can draw the widgets
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
"name": "AI Clock",
|
||||
"shortName":"AI Clock",
|
||||
"icon": "aiclock.png",
|
||||
"version":"0.05",
|
||||
"version":"0.08",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies" : { "clock_info":"module" },
|
||||
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
|
|
|
|||
|
|
@ -37,3 +37,7 @@
|
|||
0.34: Add "Confirm" option to alarm/timer edit menus
|
||||
0.35: Add automatic translation of more strings
|
||||
0.36: alarm widget moved out of app
|
||||
0.37: add message input and dated Events
|
||||
0.38: Display date in locale
|
||||
When switching 'repeat' from 'Workdays', 'Weekends' to 'Custom' preset Custom menu with previous selection
|
||||
Display alarm label in delete prompt
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
# Alarms & Timers
|
||||
|
||||
This app allows you to add/modify any alarms and timers.
|
||||
This app allows you to add/modify any alarms, timers and events.
|
||||
|
||||
Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered.
|
||||
|
||||
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.
|
||||
|
||||
## Menu overview
|
||||
|
||||
- `New...`
|
||||
- `New Alarm` → Configure a new alarm
|
||||
- `New Alarm` → Configure a new alarm (triggered based on time and day of week)
|
||||
- `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
|
||||
- `New Timer` → Configure a new timer
|
||||
- `New Timer` → Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds)
|
||||
- `New Event` → Configure a new event (triggered based on time and date)
|
||||
- `Advanced`
|
||||
- `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
|
||||
- `Enable All` → Enable _all_ disabled alarms & timers
|
||||
|
|
|
|||
|
|
@ -40,6 +40,14 @@ function handleFirstDayOfWeek(dow) {
|
|||
// Check the first day of week and update the dow field accordingly (alarms only!)
|
||||
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
|
||||
|
||||
function getLabel(e) {
|
||||
const dateStr = e.date && require("locale").date(new Date(e.date), 1);
|
||||
return (e.timer
|
||||
? require("time_utils").formatDuration(e.timer)
|
||||
: (dateStr ? `${dateStr} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""))
|
||||
) + (e.msg ? " " + e.msg : "");
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
const menu = {
|
||||
"": { "title": /*LANG*/"Alarms & Timers" },
|
||||
|
|
@ -48,10 +56,7 @@ function showMainMenu() {
|
|||
};
|
||||
|
||||
alarms.forEach((e, index) => {
|
||||
var label = e.timer
|
||||
? require("time_utils").formatDuration(e.timer)
|
||||
: require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : "");
|
||||
menu[label] = {
|
||||
menu[getLabel(e)] = {
|
||||
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
|
||||
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
|
||||
};
|
||||
|
|
@ -67,11 +72,12 @@ function showNewMenu() {
|
|||
"": { "title": /*LANG*/"New..." },
|
||||
"< Back": () => showMainMenu(),
|
||||
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
|
||||
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined)
|
||||
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined),
|
||||
/*LANG*/"Event": () => showEditAlarmMenu(undefined, undefined, true)
|
||||
});
|
||||
}
|
||||
|
||||
function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
||||
function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
||||
var isNew = alarmIndex === undefined;
|
||||
|
||||
var alarm = require("sched").newDefaultAlarm();
|
||||
|
|
@ -82,11 +88,16 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
|||
}
|
||||
|
||||
var time = require("time_utils").decodeTime(alarm.t);
|
||||
if (withDate && !alarm.date) alarm.date = new Date().toLocalISOString().slice(0,10);
|
||||
var date = alarm.date ? new Date(alarm.date) : undefined;
|
||||
var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm");
|
||||
var keyboard = "textinput";
|
||||
try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
|
||||
|
||||
const menu = {
|
||||
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
|
||||
"": { "title": title },
|
||||
"< Back": () => {
|
||||
prepareAlarmForSave(alarm, alarmIndex, time);
|
||||
prepareAlarmForSave(alarm, alarmIndex, time, date);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
},
|
||||
|
|
@ -106,6 +117,36 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
|||
wrap: true,
|
||||
onchange: v => time.m = v
|
||||
},
|
||||
/*LANG*/"Day": {
|
||||
value: date ? date.getDate() : null,
|
||||
min: 1,
|
||||
max: 31,
|
||||
wrap: true,
|
||||
onchange: v => date.setDate(v)
|
||||
},
|
||||
/*LANG*/"Month": {
|
||||
value: date ? date.getMonth() + 1 : null,
|
||||
format: v => require("date_utils").month(v),
|
||||
onchange: v => date.setMonth((v+11)%12)
|
||||
},
|
||||
/*LANG*/"Year": {
|
||||
value: date ? date.getFullYear() : null,
|
||||
min: new Date().getFullYear(),
|
||||
max: 2100,
|
||||
onchange: v => date.setFullYear(v)
|
||||
},
|
||||
/*LANG*/"Message": {
|
||||
value: alarm.msg,
|
||||
onchange: () => {
|
||||
setTimeout(() => {
|
||||
keyboard.input({text:alarm.msg}).then(result => {
|
||||
alarm.msg = result;
|
||||
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
|
||||
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Enabled": {
|
||||
value: alarm.on,
|
||||
onchange: v => alarm.on = v
|
||||
|
|
@ -115,8 +156,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
|||
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => {
|
||||
alarm.rp = repeat;
|
||||
alarm.dow = dow;
|
||||
alarm.t = require("time_utils").encodeTime(time);
|
||||
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
|
||||
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
|
||||
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
|
||||
})
|
||||
},
|
||||
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
|
||||
|
|
@ -136,16 +177,25 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
|||
}
|
||||
};
|
||||
|
||||
if (!keyboard) delete menu[/*LANG*/"Message"];
|
||||
if (alarm.date || withDate) {
|
||||
delete menu[/*LANG*/"Repeat"];
|
||||
} else {
|
||||
delete menu[/*LANG*/"Day"];
|
||||
delete menu[/*LANG*/"Month"];
|
||||
delete menu[/*LANG*/"Year"];
|
||||
}
|
||||
|
||||
if (!isNew) {
|
||||
menu[/*LANG*/"Delete"] = () => {
|
||||
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
|
||||
E.showPrompt(getLabel(alarm) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
|
||||
if (confirm) {
|
||||
alarms.splice(alarmIndex, 1);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
} else {
|
||||
alarm.t = require("time_utils").encodeTime(time);
|
||||
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
|
||||
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -154,14 +204,17 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
|||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function prepareAlarmForSave(alarm, alarmIndex, time) {
|
||||
function prepareAlarmForSave(alarm, alarmIndex, time, date, temp) {
|
||||
alarm.t = require("time_utils").encodeTime(time);
|
||||
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
|
||||
if(date) alarm.date = date.toLocalISOString().slice(0,10);
|
||||
|
||||
if (alarmIndex === undefined) {
|
||||
alarms.push(alarm);
|
||||
} else {
|
||||
alarms[alarmIndex] = alarm;
|
||||
if(!temp) {
|
||||
if (alarmIndex === undefined) {
|
||||
alarms.push(alarm);
|
||||
} else {
|
||||
alarms[alarmIndex] = alarm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +268,7 @@ function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
|
|||
},
|
||||
/*LANG*/"Custom": {
|
||||
value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false,
|
||||
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow)
|
||||
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -255,6 +308,8 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
|||
}
|
||||
|
||||
var time = require("time_utils").decodeTime(timer.timer);
|
||||
var keyboard = "textinput";
|
||||
try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
|
||||
|
||||
const menu = {
|
||||
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
|
||||
|
|
@ -285,6 +340,18 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
|||
wrap: true,
|
||||
onchange: v => time.s = v
|
||||
},
|
||||
/*LANG*/"Message": {
|
||||
value: timer.msg,
|
||||
onchange: () => {
|
||||
setTimeout(() => {
|
||||
keyboard.input({text:timer.msg}).then(result => {
|
||||
timer.msg = result;
|
||||
prepareTimerForSave(timer, timerIndex, time, true);
|
||||
setTimeout(showEditTimerMenu, 10, timer, timerIndex);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Enabled": {
|
||||
value: timer.on,
|
||||
onchange: v => timer.on = v
|
||||
|
|
@ -306,9 +373,10 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
|||
}
|
||||
};
|
||||
|
||||
if (!keyboard) delete menu[/*LANG*/"Message"];
|
||||
if (!isNew) {
|
||||
menu[/*LANG*/"Delete"] = () => {
|
||||
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
|
||||
E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
|
||||
if (confirm) {
|
||||
alarms.splice(timerIndex, 1);
|
||||
saveAndReload();
|
||||
|
|
@ -324,15 +392,17 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
|||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function prepareTimerForSave(timer, timerIndex, time) {
|
||||
function prepareTimerForSave(timer, timerIndex, time, temp) {
|
||||
timer.timer = require("time_utils").encodeTime(time);
|
||||
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
|
||||
timer.last = 0;
|
||||
|
||||
if (timerIndex === undefined) {
|
||||
alarms.push(timer);
|
||||
} else {
|
||||
alarms[timerIndex] = timer;
|
||||
if (!temp) {
|
||||
if (timerIndex === undefined) {
|
||||
alarms.push(timer);
|
||||
} else {
|
||||
alarms[timerIndex] = timer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.36",
|
||||
"version": "0.38",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
(function () {
|
||||
function dismissAlarm(alarm) {
|
||||
// Run only for alarms, not timers
|
||||
if (!alarm.timer) {
|
||||
if ("qmsched" in WIDGETS) {
|
||||
require("qmsched").setMode(0);
|
||||
} else {
|
||||
// Code from qmsched.js, so we can work without it
|
||||
require("Storage").writeJSON(
|
||||
"setting.json",
|
||||
Object.assign(require("Storage").readJSON("setting.json", 1) || {}, {
|
||||
quiet: 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.on("alarmDismiss", dismissAlarm);
|
||||
})();
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{ "id": "alarmqm",
|
||||
"name": "Alarm Quiet Mode",
|
||||
"shortName":"AlarmQM",
|
||||
"version":"0.01",
|
||||
"description": "Service that turns off quiet mode after alarm dismiss",
|
||||
"icon": "app.png",
|
||||
"tags": "quiet,alarm",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"type": "bootloader",
|
||||
"storage": [
|
||||
{"name":"alarmqm.boot.js","url":"boot.js"}
|
||||
]
|
||||
}
|
||||
|
|
@ -18,3 +18,7 @@
|
|||
0.18: Use new message library
|
||||
If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
|
||||
0.19: Add automatic translation for a couple of strings.
|
||||
0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly.
|
||||
0.21: Fix broken 'Messages' button in menu
|
||||
0.22: Handle connection events for GPS forwarding from phone
|
||||
0.23: Handle 'act' Gadgetbridge messages for realtime activity monitoring
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
Bluetooth.println("");
|
||||
Bluetooth.println(JSON.stringify(message));
|
||||
}
|
||||
var lastMsg;
|
||||
var lastMsg; // for music messages - may not be needed now...
|
||||
var actInterval; // Realtime activity reporting interval when `act` is true
|
||||
var actHRMHandler; // For Realtime activity reporting
|
||||
|
||||
// this settings var is deleted after this executes to save memory
|
||||
var settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||
//default alarm settings
|
||||
if (settings.rp == undefined) settings.rp = true;
|
||||
|
|
@ -60,6 +63,7 @@
|
|||
title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
// {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
|
||||
"alarm" : function() {
|
||||
//wipe existing GB alarms
|
||||
var sched;
|
||||
|
|
@ -92,6 +96,7 @@
|
|||
},
|
||||
//TODO perhaps move those in a library (like messages), used also for viewing events?
|
||||
//add and remove events based on activity on phone (pebble-like)
|
||||
// {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
|
||||
"calendar" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
|
|
@ -102,6 +107,7 @@
|
|||
cal[i] = event;
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
// {t:"calendar-", id:int}
|
||||
"calendar-" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
//if any of those happen we are out of sync!
|
||||
|
|
@ -110,11 +116,13 @@
|
|||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
//triggered by GB, send all ids
|
||||
// { t:"force_calendar_sync_start" }
|
||||
"force_calendar_sync_start" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
|
||||
},
|
||||
// {t:"http",resp:"......",[id:"..."]}
|
||||
"http":function() {
|
||||
//get the promise and call the promise resolve
|
||||
if (Bangle.httpRequest === undefined) return;
|
||||
|
|
@ -127,17 +135,44 @@
|
|||
else
|
||||
request.r(event); //r = resolve function
|
||||
},
|
||||
// {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
|
||||
"gps": function() {
|
||||
const settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||
if (!settings.overwriteGps) return;
|
||||
delete event.t;
|
||||
event.satellites = NaN;
|
||||
event.course = NaN;
|
||||
if (!isFinite(event.course)) event.course = NaN;
|
||||
event.fix = 1;
|
||||
Bangle.emit('gps', event);
|
||||
if (event.long!==undefined) { // for earlier Gadgetbridge implementations
|
||||
event.lon = event.long;
|
||||
delete event.long;
|
||||
}
|
||||
Bangle.emit('GPS', event);
|
||||
},
|
||||
// {t:"is_gps_active"}
|
||||
"is_gps_active": function() {
|
||||
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
|
||||
gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
|
||||
},
|
||||
// {t:"act", hrm:bool, stp:bool, int:int}
|
||||
"act": function() {
|
||||
if (actInterval) clearInterval(actInterval);
|
||||
actInterval = undefined;
|
||||
if (actHRMHandler)
|
||||
actHRMHandler = undefined;
|
||||
Bangle.setHRMPower(event.hrm,"androidact");
|
||||
if (!(event.hrm || event.stp)) return;
|
||||
if (!isFinite(event.int)) event.int=1;
|
||||
var lastSteps = Bangle.getStepCount();
|
||||
var lastBPM = 0;
|
||||
actHRMHandler = function(e) {
|
||||
lastBPM = e.bpm;
|
||||
};
|
||||
Bangle.on('HRM',actHRMHandler);
|
||||
actInterval = setInterval(function() {
|
||||
var steps = Bangle.getStepCount();
|
||||
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM });
|
||||
lastSteps = steps;
|
||||
}, event.int*1000);
|
||||
}
|
||||
};
|
||||
var h = HANDLERS[event.t];
|
||||
|
|
@ -174,21 +209,28 @@
|
|||
},options.timeout||30000)};
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
|
||||
// Battery monitor
|
||||
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
|
||||
Bangle.on("charging", sendBattery);
|
||||
NRF.on("connect", () => setTimeout(function() {
|
||||
sendBattery();
|
||||
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
|
||||
}, 2000));
|
||||
Bangle.on("charging", sendBattery);
|
||||
if (!settings.keep)
|
||||
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
|
||||
NRF.on("disconnect", () => {
|
||||
// disable HRM/activity monitoring ('act' message)
|
||||
GB({t:"act",stp:0,hrm:0,int:0}); // just call the handler to save duplication
|
||||
// remove all messages on disconnect (if enabled)
|
||||
var settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||
if (!settings.keep)
|
||||
require("messages").clearAll();
|
||||
});
|
||||
setInterval(sendBattery, 10*60*1000);
|
||||
// Health tracking
|
||||
Bangle.on('health', health=>{
|
||||
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
|
||||
if (actInterval===undefined) // if 'realtime' we do it differently
|
||||
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
|
||||
});
|
||||
// Music control
|
||||
Bangle.musicControl = cmd => {
|
||||
|
|
@ -203,13 +245,39 @@
|
|||
};
|
||||
// GPS overwrite logic
|
||||
if (settings.overwriteGps) { // if the overwrite option is set../
|
||||
// Save current logic
|
||||
const originalSetGpsPower = Bangle.setGPSPower;
|
||||
const origSetGPSPower = Bangle.setGPSPower;
|
||||
// migrate all GPS clients to the other variant on connection events
|
||||
let handleConnection = (state) => {
|
||||
if (Bangle.isGPSOn()){
|
||||
let orig = Bangle._PWR.GPS;
|
||||
delete Bangle._PWR.GPS;
|
||||
origSetGPSPower(state);
|
||||
Bangle._PWR.GPS = orig;
|
||||
}
|
||||
};
|
||||
NRF.on('connect', ()=>{handleConnection(0);});
|
||||
NRF.on('disconnect', ()=>{handleConnection(1);});
|
||||
|
||||
// Work around Serial1 for GPS not working when connected to something
|
||||
let serialTimeout;
|
||||
let wrap = function(f){
|
||||
return (s)=>{
|
||||
if (serialTimeout) clearTimeout(serialTimeout);
|
||||
handleConnection(1);
|
||||
f(s);
|
||||
serialTimeout = setTimeout(()=>{
|
||||
serialTimeout = undefined;
|
||||
if (NRF.getSecurityStatus().connected) handleConnection(0);
|
||||
}, 10000);
|
||||
};
|
||||
};
|
||||
Serial1.println = wrap(Serial1.println);
|
||||
Serial1.write = wrap(Serial1.write);
|
||||
|
||||
// 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 not connected use internal GPS power function
|
||||
if (!NRF.getSecurityStatus().connected) return origSetGPSPower(isOn, appID);
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
|
||||
if (!appID) appID="?";
|
||||
|
|
@ -218,11 +286,15 @@
|
|||
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
|
||||
};
|
||||
// Allow checking for GPS via GadgetBridge
|
||||
Bangle.isGPSOn = () => {
|
||||
return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
|
||||
}
|
||||
return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
|
||||
};
|
||||
// stop GPS on boot if not activated
|
||||
setTimeout(()=>{
|
||||
if (!Bangle.isGPSOn()) gbSend({ t: "gps_power", status: false });
|
||||
},3000);
|
||||
}
|
||||
|
||||
// remove settings object so it's not taking up RAM
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.19",
|
||||
"version": "0.23",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
(function(back) {
|
||||
|
||||
|
||||
|
||||
|
||||
function gb(j) {
|
||||
Bluetooth.println(JSON.stringify(j));
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
var mainmenu = {
|
||||
"" : { "title" : "Android" },
|
||||
"< Back" : back,
|
||||
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
|
||||
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" },
|
||||
/*LANG*/"Find Phone" : () => E.showMenu({
|
||||
"" : { "title" : /*LANG*/"Find Phone" },
|
||||
"< Back" : ()=>E.showMenu(mainmenu),
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Messages" : ()=>require("message").openGUI(),
|
||||
/*LANG*/"Messages" : ()=>require("messages").openGUI(),
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
let result = true;
|
||||
|
||||
function assertTrue(condition, text) {
|
||||
if (!condition) {
|
||||
result = false;
|
||||
print("FAILURE: " + text);
|
||||
} else print("OK: " + text);
|
||||
}
|
||||
|
||||
function assertFalse(condition, text) {
|
||||
assertTrue(!condition, text);
|
||||
}
|
||||
|
||||
function assertUndefinedOrEmpty(array, text) {
|
||||
assertTrue(!array || array.length == 0, text);
|
||||
}
|
||||
|
||||
function assertNotEmpty(array, text) {
|
||||
assertTrue(array && array.length > 0, text);
|
||||
}
|
||||
|
||||
let internalOn = () => {
|
||||
return getPinMode((process.env.HWVERSION==2)?D30:D26) == "input";
|
||||
};
|
||||
|
||||
let sec = {
|
||||
connected: false
|
||||
};
|
||||
|
||||
NRF.getSecurityStatus = () => sec;
|
||||
|
||||
setTimeout(() => {
|
||||
// add an empty starting point to make the asserts work
|
||||
Bangle._PWR={};
|
||||
|
||||
print("Not connected, should use internal GPS");
|
||||
assertTrue(!NRF.getSecurityStatus().connected, "Not connected");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
|
||||
assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on");
|
||||
|
||||
assertNotEmpty(Bangle._PWR.GPS, "GPS");
|
||||
assertTrue(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertTrue(internalOn(), "Internal GPS on");
|
||||
|
||||
assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
print("Connected, should use GB GPS");
|
||||
sec.connected = true;
|
||||
|
||||
assertTrue(NRF.getSecurityStatus().connected, "Connected");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on");
|
||||
|
||||
assertNotEmpty(Bangle._PWR.GPS, "GPS");
|
||||
assertTrue(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
print("Connected, then reconnect cycle");
|
||||
sec.connected = true;
|
||||
|
||||
assertTrue(NRF.getSecurityStatus().connected, "Connected");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on");
|
||||
|
||||
assertNotEmpty(Bangle._PWR.GPS, "GPS");
|
||||
assertTrue(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
NRF.emit("disconnect", {});
|
||||
print("disconnect");
|
||||
sec.connected = false;
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
assertNotEmpty(Bangle._PWR.GPS, "GPS");
|
||||
assertTrue(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertTrue(internalOn(), "Internal GPS on");
|
||||
|
||||
print("connect");
|
||||
sec.connected = true;
|
||||
NRF.emit("connect", {});
|
||||
|
||||
setTimeout(() => {
|
||||
assertNotEmpty(Bangle._PWR.GPS, "GPS");
|
||||
assertTrue(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
setTimeout(() => {
|
||||
print("Test disconnect without gps on");
|
||||
|
||||
assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS");
|
||||
assertFalse(Bangle.isGPSOn(), "isGPSOn");
|
||||
assertFalse(internalOn(), "Internal GPS off");
|
||||
|
||||
print("Result Overall is " + (result ? "OK" : "FAIL"));
|
||||
}, 0);
|
||||
}, 0);
|
||||
}, 0);
|
||||
}, 5000);
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
0.01: New App!
|
||||
0.02: Update to work with Bangle.js 2
|
||||
0.03: Select GNSS systems to use for Bangle.js 2
|
||||
0.04: Now turns GPS off after upload
|
||||
0.05: Fix regression in 0.04 that caused AGPS data not to get loaded
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@
|
|||
function jsFromBase64(b64) {
|
||||
var bin = atob(b64);
|
||||
var chunkSize = 128;
|
||||
var js = "\x10Bangle.setGPSPower(1);\n"; // turn GPS on
|
||||
var js = "\x10Bangle.setGPSPower(1,'agps');\n"; // turn GPS on
|
||||
if (isB1) { // UBLOX
|
||||
//js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`;
|
||||
//js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec
|
||||
|
|
@ -158,6 +158,7 @@
|
|||
var chunk = bin.substr(i,chunkSize);
|
||||
js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`;
|
||||
}
|
||||
js += "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
|
||||
return js;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "assistedgps",
|
||||
"name": "Assisted GPS Updater (AGPS)",
|
||||
"version": "0.03",
|
||||
"version": "0.05",
|
||||
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
|
||||
"sortorder": -1,
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Don't fire if the app uses swipes already.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Service that allows you to use an app's back button using left to right swipe gesture.
|
||||
|
||||
## Settings
|
||||
|
||||
Mode: Blacklist/Whitelist/Always On/Disabled
|
||||
App List: Black-/whitelisted apps
|
||||
Standard # of swipe handlers: 0-10 (Default: 0, must be changed for backswipe to work at all)
|
||||
Standard # of drag handlers: 0-10 (Default: 0, must be changed for backswipe to work at all)
|
||||
|
||||
|
||||
Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however.
|
||||
|
||||
To get an indication for standard # of handlers `Bangle["#onswipe"]` and `Bangle["#ondrag"]` can be entered in the [Espruino Web IDE](https://www.espruino.com/ide) console field. They return `undefined` if no handler is active, a function if one is active, or a list of functions if multiple are active. Calling this on the clock app is a good start.
|
||||
|
||||
## TODO
|
||||
|
||||
- Possibly add option to tweak standard # of handlers on per app basis.
|
||||
|
||||
## Creator
|
||||
Kedlub
|
||||
|
||||
## Contributors
|
||||
thyttan
|
||||
|
After Width: | Height: | Size: 764 B |
|
|
@ -0,0 +1,60 @@
|
|||
(function () {
|
||||
var DEFAULTS = {
|
||||
mode: 0,
|
||||
apps: [],
|
||||
};
|
||||
var settings = require("Storage").readJSON("backswipe.json", 1) || DEFAULTS;
|
||||
|
||||
// Overrride the default setUI method, so we can save the back button callback
|
||||
var setUI = Bangle.setUI;
|
||||
Bangle.setUI = function (mode, cb) {
|
||||
var options = {};
|
||||
if ("object"==typeof mode) {
|
||||
options = mode;
|
||||
}
|
||||
|
||||
var currentFile = global.__FILE__ || "";
|
||||
|
||||
if (global.BACK) delete global.BACK;
|
||||
if (options && options.back && enabledForApp(currentFile)) {
|
||||
global.BACK = options.back;
|
||||
}
|
||||
setUI(mode, cb);
|
||||
};
|
||||
|
||||
function countHandlers(eventType) {
|
||||
if (Bangle["#on"+eventType] === undefined) {
|
||||
return 0;
|
||||
} else if (Bangle["#on"+eventType] instanceof Array) {
|
||||
return Bangle["#on"+eventType].length;
|
||||
} else if (Bangle["#on"+eventType] !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack(lr, _) {
|
||||
// if it is a left to right swipe
|
||||
if (lr === 1) {
|
||||
// if we're in an app that has a back button, run the callback for it
|
||||
if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) {
|
||||
global.BACK();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the back button should be enabled for the current app
|
||||
// app is the src file of the app
|
||||
function enabledForApp(app) {
|
||||
if (!settings) return true;
|
||||
if (settings.mode === 0) {
|
||||
return !(settings.apps.filter((a) => a.src === app).length > 0);
|
||||
} else if (settings.mode === 1) {
|
||||
return settings.apps.filter((a) => a.src === app).length > 0;
|
||||
} else {
|
||||
return settings.mode === 2 ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to left to right swipe
|
||||
Bangle.on("swipe", goBack);
|
||||
})();
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{ "id": "backswipe",
|
||||
"name": "Back Swipe",
|
||||
"shortName":"BackSwipe",
|
||||
"version":"0.02",
|
||||
"description": "Service that allows you to use an app's back button using left to right swipe gesture",
|
||||
"icon": "app.png",
|
||||
"tags": "back,gesture,swipe",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"type": "bootloader",
|
||||
"storage": [
|
||||
{"name":"backswipe.boot.js","url":"boot.js"},
|
||||
{"name":"backswipe.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"backswipe.json"}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
(function(back) {
|
||||
var FILE = 'backswipe.json';
|
||||
// Mode can be 'blacklist', 'whitelist', 'on' or 'disabled'
|
||||
// Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode
|
||||
var DEFAULTS = {
|
||||
'mode': 0,
|
||||
'apps': [],
|
||||
'standardNumSwipeHandlers': 0,
|
||||
'standardNumDragHandlers': 0
|
||||
};
|
||||
|
||||
var settings = {};
|
||||
|
||||
var loadSettings = function() {
|
||||
settings = require('Storage').readJSON(FILE, 1) || DEFAULTS;
|
||||
};
|
||||
|
||||
var saveSettings = function(settings) {
|
||||
require('Storage').write(FILE, settings);
|
||||
};
|
||||
|
||||
// Get all app info files
|
||||
var getApps = function() {
|
||||
var apps = require('Storage').list(/\.info$/).map(appInfoFileName => {
|
||||
var appInfo = require('Storage').readJSON(appInfoFileName, 1);
|
||||
return appInfo && {
|
||||
'name': appInfo.name,
|
||||
'sortorder': appInfo.sortorder,
|
||||
'src': appInfo.src
|
||||
};
|
||||
}).filter(app => app && !!app.src);
|
||||
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;
|
||||
});
|
||||
return apps;
|
||||
};
|
||||
|
||||
var showMenu = function() {
|
||||
var menu = {
|
||||
'': { 'title': 'Backswipe' },
|
||||
'< Back': () => {
|
||||
back();
|
||||
},
|
||||
'Mode': {
|
||||
value: settings.mode,
|
||||
min: 0,
|
||||
max: 3,
|
||||
format: v => ["Blacklist", "Whitelist", "Always On", "Disabled"][v],
|
||||
onchange: v => {
|
||||
settings.mode = v;
|
||||
saveSettings(settings);
|
||||
},
|
||||
},
|
||||
'App List': () => {
|
||||
showAppSubMenu();
|
||||
},
|
||||
'Standard # of swipe handlers' : { // If more than this many handlers are present backswipe will not go back
|
||||
value: 0|settings.standardNumSwipeHandlers,
|
||||
min: 0,
|
||||
max: 10,
|
||||
format: v=>v,
|
||||
onchange: v => {
|
||||
settings.standardNumSwipeHandlers = v;
|
||||
saveSettings(settings);
|
||||
},
|
||||
},
|
||||
'Standard # of drag handlers' : { // If more than this many handlers are present backswipe will not go back
|
||||
value: 0|settings.standardNumDragHandlers,
|
||||
min: 0,
|
||||
max: 10,
|
||||
format: v=>v,
|
||||
onchange: v => {
|
||||
settings.standardNumDragHandlers = v;
|
||||
saveSettings(settings);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
E.showMenu(menu);
|
||||
};
|
||||
|
||||
var showAppSubMenu = function() {
|
||||
var menu = {
|
||||
'': { 'title': 'Backswipe' },
|
||||
'< Back': () => {
|
||||
showMenu();
|
||||
},
|
||||
'Add App': () => {
|
||||
showAppList();
|
||||
}
|
||||
};
|
||||
settings.apps.forEach(app => {
|
||||
menu[app.name] = () => {
|
||||
settings.apps.splice(settings.apps.indexOf(app), 1);
|
||||
saveSettings(settings);
|
||||
showAppSubMenu();
|
||||
}
|
||||
});
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
var showAppList = function() {
|
||||
var apps = getApps();
|
||||
var menu = {
|
||||
'': { 'title': 'Backswipe' },
|
||||
'< Back': () => {
|
||||
showMenu();
|
||||
}
|
||||
};
|
||||
apps.forEach(app => {
|
||||
menu[app.name] = () => {
|
||||
settings.apps.push(app);
|
||||
saveSettings(settings);
|
||||
showAppSubMenu();
|
||||
}
|
||||
});
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
showMenu();
|
||||
})
|
||||
|
|
@ -14,3 +14,4 @@
|
|||
0.14: Use ClockFace_menu.addItems
|
||||
0.15: Add Power saving option
|
||||
0.16: Support Fast Loading
|
||||
0.17: Hide widgets instead of not loading them at all
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "barclock",
|
||||
"name": "Bar Clock",
|
||||
"version": "0.16",
|
||||
"version": "0.17",
|
||||
"description": "A simple digital clock showing seconds as a bar",
|
||||
"icon": "clock-bar.png",
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
(function(back) {
|
||||
let s = require("Storage").readJSON("barclock.settings.json", true) || {};
|
||||
// migrate "don't load widgets" to "hide widgets"
|
||||
if (!("hideWidgets" in s) && ("loadWidgets" in s) && !s.loadWidgets) {
|
||||
s.hideWidgets = 1;
|
||||
}
|
||||
delete s.loadWidgets;
|
||||
|
||||
function save(key, value) {
|
||||
s[key] = value;
|
||||
|
|
@ -19,7 +24,7 @@
|
|||
};
|
||||
let items = {
|
||||
showDate: s.showDate,
|
||||
loadWidgets: s.loadWidgets,
|
||||
hideWidgets: s.hideWidgets,
|
||||
};
|
||||
// Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway)
|
||||
if (process.env.HWVERSION>1) {
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@
|
|||
0.05: Update *on* the minute rather than every 15 secs
|
||||
Now show widgets
|
||||
Make compatible with themes, and Bangle.js 2
|
||||
0.06: Enable fastloading
|
||||
0.07: Adds fullscreen mode setting
|
||||
|
|
@ -1,32 +1,41 @@
|
|||
{
|
||||
// Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr
|
||||
// https://github.com/eska-muc/BangleApps
|
||||
|
||||
var settings = require('Storage').readJSON("berlinc.json", true) || {};
|
||||
const fields = [4, 4, 11, 4];
|
||||
const offset = 24;
|
||||
const width = g.getWidth() - 2 * offset;
|
||||
const height = g.getHeight() - 2 * offset;
|
||||
const rowHeight = height / 4;
|
||||
|
||||
var show_date = false;
|
||||
var show_time = false;
|
||||
var yy = 0;
|
||||
let fullscreen = !!settings.fullscreen;
|
||||
|
||||
var rowlights = [];
|
||||
var time_digit = [];
|
||||
let show_date = false;
|
||||
let show_time = false;
|
||||
let yy = 0;
|
||||
|
||||
let rowlights = [];
|
||||
let time_digit = [];
|
||||
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
let drawTimeout;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
let queueDraw = () => {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
};
|
||||
|
||||
function draw() {
|
||||
g.reset().clearRect(0,24,g.getWidth(),g.getHeight());
|
||||
let draw = () => {
|
||||
let width = Math.min(Bangle.appRect.w,Bangle.appRect.h);
|
||||
let height = width;
|
||||
let offset = g.getHeight() - height;
|
||||
let x = Math.floor((g.getWidth() - width)/2);
|
||||
|
||||
if (show_date) height -= 8;
|
||||
let rowHeight = (height - 1) / 4;
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.reset().clearRect(Bangle.appRect);
|
||||
var now = new Date();
|
||||
|
||||
// show date below the clock
|
||||
|
|
@ -37,7 +46,7 @@ function draw() {
|
|||
var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
|
||||
var strWidth = g.stringWidth(dateString);
|
||||
g.setColor(g.theme.fg).setFontAlign(-1,-1);
|
||||
g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4);
|
||||
g.drawString(dateString, ( Bangle.appRect.x + Bangle.appRect.w - strWidth ) / 2, Bangle.appRect.y2 - 5);
|
||||
}
|
||||
|
||||
rowlights[0] = Math.floor(now.getHours() / 5);
|
||||
|
|
@ -50,15 +59,16 @@ function draw() {
|
|||
time_digit[2] = Math.floor(now.getMinutes() / 10);
|
||||
time_digit[3] = now.getMinutes() % 10;
|
||||
|
||||
g.drawRect(offset, offset, width + offset, height + offset);
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawRect(x, offset, x + width - 1, height + offset - 1);
|
||||
for (row = 0; row < 4; row++) {
|
||||
nfields = fields[row];
|
||||
boxWidth = width / nfields;
|
||||
boxWidth = (width - 1) / nfields;
|
||||
|
||||
for (col = 0; col < nfields; col++) {
|
||||
x1 = col * boxWidth + offset;
|
||||
x1 = col * boxWidth + x;
|
||||
y1 = row * rowHeight + offset;
|
||||
x2 = (col + 1) * boxWidth + offset;
|
||||
x2 = (col + 1) * boxWidth + x;
|
||||
y2 = (row + 1) * rowHeight + offset;
|
||||
|
||||
g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2);
|
||||
|
|
@ -84,33 +94,53 @@ function draw() {
|
|||
queueDraw();
|
||||
}
|
||||
|
||||
function toggleDate() {
|
||||
let toggleDate = () => {
|
||||
show_date = ! show_date;
|
||||
draw();
|
||||
}
|
||||
|
||||
function toggleTime() {
|
||||
let toggleTime = () => {
|
||||
show_time = ! show_time;
|
||||
draw();
|
||||
}
|
||||
|
||||
// Stop updates when LCD is off, restart when on
|
||||
Bangle.on('lcdPower',on=>{
|
||||
let clear = () => {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
|
||||
let onLcdPower = on => {
|
||||
if (on) {
|
||||
draw(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let cleanup = () => {
|
||||
clear();
|
||||
Bangle.removeListener("lcdPower", onLcdPower);
|
||||
require("widget_utils").show();
|
||||
}
|
||||
|
||||
// Stop updates when LCD is off, restart when on
|
||||
Bangle.on('lcdPower',onLcdPower);
|
||||
|
||||
// Show launcher when button pressed, handle up/down
|
||||
Bangle.setUI("clockupdown", dir=> {
|
||||
Bangle.setUI({mode: "clockupdown", remove: cleanup}, dir=> {
|
||||
if (dir<0) toggleTime();
|
||||
if (dir>0) toggleDate();
|
||||
});
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
|
||||
if (fullscreen){
|
||||
if (process.env.HWVERSION == 2) require("widget_utils").swipeOn();
|
||||
else require("widget_utils").hide();
|
||||
}
|
||||
|
||||
Bangle.drawWidgets();
|
||||
|
||||
draw();
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "berlinc",
|
||||
"name": "Berlin Clock",
|
||||
"version": "0.05",
|
||||
"version": "0.07",
|
||||
"description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)",
|
||||
"icon": "berlin-clock.png",
|
||||
"type": "clock",
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
"screenshots": [{"url":"berlin-clock-screenshot.png"}],
|
||||
"storage": [
|
||||
{"name":"berlinc.app.js","url":"berlin-clock.js"},
|
||||
{"name":"berlinc.settings.js","url":"settings.js"},
|
||||
{"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true}
|
||||
]
|
||||
],
|
||||
"data": [{"name":"berlinc.json"}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
(function(back) {
|
||||
var FILE = "berlinc.json";
|
||||
var settings = Object.assign({
|
||||
fullscreem: false,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
var mainmenu = {
|
||||
"": {
|
||||
"title": "Berlin clock"
|
||||
},
|
||||
"< Back": () => back(),
|
||||
"Fullscreen": {
|
||||
value: !!settings.fullscreen,
|
||||
onchange: v => {
|
||||
settings.fullscreen = v;
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ARkQAHBwsBiIACiAHBgQXIkAXJiIuKGAwWEC4cjmYABn//AAMyC63yC653FC6HwC5aQBC5ybIC44WChGAWxMgC44rCxGIZxYXFIoYXBGAQNCAAQXILYYXBGAUDBoK0EC5AsBC4QwEC5wAEC853BhAWDI6CPCFwp3OX4ouCC8xHXCAJ3VX94XCwBHVGIiPTU4oNCAAQWBX5gDBgQRCAAoXGGAUIFwQXHkAXHJIgABCw4IBC5sAiIAEiAgHAAQXLHBAYIC+6wJQYIADgIXGGBJ3FC4iOBAH4A/ACAA=="))
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
E.showMessage("Scanning...");
|
||||
var devices = [];
|
||||
|
||||
setInterval(function() {
|
||||
NRF.findDevices(function(devs) {
|
||||
devs.forEach(dev=>{
|
||||
var existing = devices.find(d=>d.id==dev.id);
|
||||
if (existing) {
|
||||
existing.timeout = 0;
|
||||
existing.rssi = (existing.rssi*3 + dev.rssi)/4;
|
||||
} else {
|
||||
dev.timeout = 0;
|
||||
dev.new = 0;
|
||||
devices.push(dev);
|
||||
}
|
||||
});
|
||||
devices.forEach(d=>{d.timeout++;d.new++});
|
||||
devices = devices.filter(dev=>dev.timeout<8);
|
||||
devices.sort((a,b)=>b.rssi - a.rssi);
|
||||
g.clear(1).setFont("12x20");
|
||||
var wasNew = false;
|
||||
devices.forEach((d,y)=>{
|
||||
y*=20;
|
||||
var n = d.name;
|
||||
if (!n) n=d.id.substr(0,22);
|
||||
if (d.new<4) {
|
||||
g.fillRect(0,y,g.getWidth(),y+19);
|
||||
g.setColor(g.theme.bg);
|
||||
if (d.rssi > -70) wasNew = true;
|
||||
} else {
|
||||
g.setColor(g.theme.fg);
|
||||
}
|
||||
g.setFontAlign(-1,-1);
|
||||
g.drawString(n,0,y);
|
||||
g.setFontAlign(1,-1);
|
||||
g.drawString(0|d.rssi,g.getWidth()-1,y);
|
||||
});
|
||||
g.flip();
|
||||
Bangle.setLCDBrightness(wasNew);
|
||||
}, 1200);
|
||||
}, 1500);
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
{ "id": "blescanner",
|
||||
"name": "BLE Scanner",
|
||||
"shortName":"BLE Scan",
|
||||
"version":"0.01",
|
||||
"description": "Scans for bluetooth devices nearby and shows their names on the screen ordered by signal strength. The most recently discovered items are highlighted.",
|
||||
"icon": "app.png",
|
||||
"screenshots" : [ { "url":"screenshot.png" } ],
|
||||
"tags": "tool,bluetooth",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"blescanner.app.js","url":"app.js"},
|
||||
{"name":"blescanner.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -64,3 +64,4 @@
|
|||
0.55: Add toLocalISOString polyfill for pre-2v15 firmwares
|
||||
Only add boot info comments if settings.bootDebug was set
|
||||
If settings.bootDebug is set, output timing for each section of .boot0
|
||||
0.56: Settings.log = 0,1,2,3 for off,display, log, both
|
||||
|
|
|
|||
|
|
@ -32,14 +32,12 @@ if (s.ble!==false) {
|
|||
boot += `bleServiceOptions.hid=Bangle.HID;\n`;
|
||||
}
|
||||
}
|
||||
if (s.log==2) { // logging to file
|
||||
boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
`;
|
||||
} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
|
||||
if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);});
|
||||
// settings.log 0-off, 1-display, 2-log, 3-both
|
||||
if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
|
||||
if (s.log>=2) { boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}});
|
||||
LoopbackA.setConsole(true);\n`;
|
||||
else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
|
||||
} else if (s.log==1) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
|
||||
else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console
|
||||
/* If not programmable add our own handler for Bluetooth data
|
||||
to allow Gadgetbridge commands to be received*/
|
||||
|
|
@ -56,10 +54,10 @@ Bluetooth.on('line',function(l) {
|
|||
try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {}
|
||||
});\n`;
|
||||
} else {
|
||||
if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);});
|
||||
if (s.log>=2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}});
|
||||
if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`;
|
||||
else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
|
||||
else if (s.log==1) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
|
||||
else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth
|
||||
}
|
||||
// we just reset, so BLE should be on.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boot",
|
||||
"name": "Bootloader",
|
||||
"version": "0.55",
|
||||
"version": "0.56",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"icon": "bootloader.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New app!
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Bluetooth Advert
|
||||
|
||||
This app advertises and exports (over Bluetooth) live data from the bangle's sensors:
|
||||
|
||||
- Heart Rate
|
||||
- Accelerometer readings
|
||||
- Pressure
|
||||
- GPS information
|
||||
- Magnetic flux
|
||||
|
||||
Swipe in any direction to access settings, and tap a setting to toggle it.
|
||||
Hit back to return to the details screen, which shows sensor data being exported.
|
||||
|
||||
# TypeScript
|
||||
|
||||
This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
"use strict";
|
||||
var __assign = Object.assign;
|
||||
var Layout = require("Layout");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
var HRM_MIN_CONFIDENCE = 75;
|
||||
var services = ["0x180d", "0x181a", "0x1819"];
|
||||
var acc;
|
||||
var bar;
|
||||
var gps;
|
||||
var hrm;
|
||||
var hrmAny;
|
||||
var mag;
|
||||
var btnsShown = false;
|
||||
var prevBtnsShown = undefined;
|
||||
var hrmAnyClear;
|
||||
var settings = {
|
||||
bar: false,
|
||||
gps: false,
|
||||
hrm: false,
|
||||
mag: false,
|
||||
};
|
||||
var idToName = {
|
||||
acc: "Acceleration",
|
||||
bar: "Barometer",
|
||||
gps: "GPS",
|
||||
hrm: "HRM",
|
||||
mag: "Magnetometer",
|
||||
};
|
||||
var infoFont = "6x8:2";
|
||||
var colour = {
|
||||
on: "#0f0",
|
||||
off: "#fff",
|
||||
};
|
||||
var makeToggle = function (id) { return function () {
|
||||
settings[id] = !settings[id];
|
||||
var entry = btnLayout[id];
|
||||
var col = settings[id] ? colour.on : colour.off;
|
||||
entry.btnBorder = entry.col = col;
|
||||
btnLayout.update();
|
||||
btnLayout.render();
|
||||
enableSensors();
|
||||
}; };
|
||||
var btnStyle = {
|
||||
font: "Vector:14",
|
||||
fillx: 1,
|
||||
filly: 1,
|
||||
col: g.theme.fg,
|
||||
bgCol: g.theme.bg,
|
||||
btnBorder: "#fff",
|
||||
};
|
||||
var btnLayout = new Layout({
|
||||
type: "v",
|
||||
c: [
|
||||
{
|
||||
type: "h",
|
||||
c: [
|
||||
__assign({ type: "btn", label: idToName.bar, id: "bar", cb: makeToggle('bar') }, btnStyle),
|
||||
__assign({ type: "btn", label: idToName.gps, id: "gps", cb: makeToggle('gps') }, btnStyle),
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h",
|
||||
c: [
|
||||
__assign({ type: "btn", label: idToName.hrm, id: "hrm", cb: makeToggle('hrm') }, btnStyle),
|
||||
__assign({ type: "btn", label: idToName.mag, id: "mag", cb: makeToggle('mag') }, btnStyle),
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h",
|
||||
c: [
|
||||
__assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour.on, btnBorder: colour.on }),
|
||||
__assign({ type: "btn", label: "Back", cb: function () {
|
||||
setBtnsShown(false);
|
||||
} }, btnStyle),
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {
|
||||
lazy: true,
|
||||
back: function () {
|
||||
setBtnsShown(false);
|
||||
},
|
||||
});
|
||||
var setBtnsShown = function (b) {
|
||||
btnsShown = b;
|
||||
hook(!btnsShown);
|
||||
setIntervals();
|
||||
redraw();
|
||||
};
|
||||
var drawInfo = function (force) {
|
||||
var _a = Bangle.appRect, y = _a.y, x = _a.x, w = _a.w;
|
||||
var mid = x + w / 2;
|
||||
var drawn = false;
|
||||
if (!force && !bar && !gps && !hrm && !mag)
|
||||
return;
|
||||
g.reset()
|
||||
.clearRect(Bangle.appRect)
|
||||
.setFont(infoFont)
|
||||
.setFontAlign(0, -1);
|
||||
if (bar) {
|
||||
g.drawString("".concat(bar.altitude.toFixed(1), "m"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
g.drawString("".concat(bar.pressure.toFixed(1), " hPa"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
g.drawString("".concat(bar.temperature.toFixed(1), "C"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
drawn = true;
|
||||
}
|
||||
if (gps) {
|
||||
g.drawString("".concat(gps.lat.toFixed(4), " lat, ").concat(gps.lon.toFixed(4), " lon"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
g.drawString("".concat(gps.alt, "m (").concat(gps.satellites, " sat)"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
drawn = true;
|
||||
}
|
||||
if (hrm) {
|
||||
g.drawString("".concat(hrm.bpm, " BPM (").concat(hrm.confidence, "%)"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
drawn = true;
|
||||
}
|
||||
else if (hrmAny) {
|
||||
g.drawString("~".concat(hrmAny.bpm, " BPM (").concat(hrmAny.confidence, "%)"), mid, y);
|
||||
y += g.getFontHeight();
|
||||
drawn = true;
|
||||
if (!settings.hrm && !hrmAnyClear) {
|
||||
hrmAnyClear = setTimeout(function () {
|
||||
hrmAny = undefined;
|
||||
hrmAnyClear = undefined;
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
if (mag) {
|
||||
g.drawString("".concat(mag.x, " ").concat(mag.y, " ").concat(mag.z), mid, y);
|
||||
y += g.getFontHeight();
|
||||
g.drawString("heading: ".concat(mag.heading.toFixed(1)), mid, y);
|
||||
y += g.getFontHeight();
|
||||
drawn = true;
|
||||
}
|
||||
if (!drawn) {
|
||||
if (!force || Object.values(settings).every(function (x) { return !x; })) {
|
||||
g.drawString("swipe to enable", mid, y);
|
||||
}
|
||||
else {
|
||||
g.drawString("events pending", mid, y);
|
||||
}
|
||||
y += g.getFontHeight();
|
||||
}
|
||||
};
|
||||
var onTap = function () {
|
||||
setBtnsShown(true);
|
||||
};
|
||||
var redraw = function () {
|
||||
if (btnsShown) {
|
||||
if (!prevBtnsShown) {
|
||||
prevBtnsShown = btnsShown;
|
||||
Bangle.removeListener("swipe", onTap);
|
||||
btnLayout.setUI();
|
||||
btnLayout.forgetLazyState();
|
||||
g.clearRect(Bangle.appRect);
|
||||
}
|
||||
btnLayout.render();
|
||||
}
|
||||
else {
|
||||
if (prevBtnsShown) {
|
||||
prevBtnsShown = btnsShown;
|
||||
Bangle.setUI();
|
||||
Bangle.on("swipe", onTap);
|
||||
drawInfo(true);
|
||||
}
|
||||
else {
|
||||
drawInfo();
|
||||
}
|
||||
}
|
||||
};
|
||||
var encodeHrm = function (hrm) {
|
||||
return [0, hrm.bpm];
|
||||
};
|
||||
encodeHrm.maxLen = 2;
|
||||
var encodePressure = function (data) {
|
||||
return toByteArray(Math.round(data.pressure * 10), 4, false);
|
||||
};
|
||||
encodePressure.maxLen = 4;
|
||||
var encodeElevation = function (data) {
|
||||
return toByteArray(Math.round(data.altitude * 100), 3, true);
|
||||
};
|
||||
encodeElevation.maxLen = 3;
|
||||
var encodeTemp = function (data) {
|
||||
return toByteArray(Math.round(data.temperature * 10), 2, true);
|
||||
};
|
||||
encodeTemp.maxLen = 2;
|
||||
var encodeGps = function (data) {
|
||||
var speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
|
||||
var lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
|
||||
var lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
|
||||
var elevation = toByteArray(Math.round(data.alt * 100), 3, true);
|
||||
var heading = toByteArray(Math.round(data.course * 100), 2, false);
|
||||
return [
|
||||
157,
|
||||
2,
|
||||
speed[0], speed[1],
|
||||
lat[0], lat[1], lat[2], lat[3],
|
||||
lon[0], lon[1], lon[2], lon[3],
|
||||
elevation[0], elevation[1], elevation[2],
|
||||
heading[0], heading[1]
|
||||
];
|
||||
};
|
||||
encodeGps.maxLen = 17;
|
||||
var encodeGpsHeadingOnly = function (data) {
|
||||
var heading = toByteArray(Math.round(data.heading * 100), 2, false);
|
||||
return [
|
||||
16,
|
||||
16,
|
||||
heading[0], heading[1]
|
||||
];
|
||||
};
|
||||
encodeGpsHeadingOnly.maxLen = 17;
|
||||
var encodeMag = function (data) {
|
||||
var x = toByteArray(data.x, 2, true);
|
||||
var y = toByteArray(data.y, 2, true);
|
||||
var z = toByteArray(data.z, 2, true);
|
||||
return [x[0], x[1], y[0], y[1], z[0], z[1]];
|
||||
};
|
||||
encodeMag.maxLen = 6;
|
||||
var toByteArray = function (value, numberOfBytes, isSigned) {
|
||||
var byteArray = new Array(numberOfBytes);
|
||||
if (isSigned && (value < 0)) {
|
||||
value += 1 << (numberOfBytes * 8);
|
||||
}
|
||||
for (var index = 0; index < numberOfBytes; index++) {
|
||||
byteArray[index] = (value >> (index * 8)) & 0xff;
|
||||
}
|
||||
return byteArray;
|
||||
};
|
||||
var enableSensors = function () {
|
||||
Bangle.setBarometerPower(settings.bar, "btadv");
|
||||
if (!settings.bar)
|
||||
bar = undefined;
|
||||
Bangle.setGPSPower(settings.gps, "btadv");
|
||||
if (!settings.gps)
|
||||
gps = undefined;
|
||||
Bangle.setHRMPower(settings.hrm, "btadv");
|
||||
if (!settings.hrm)
|
||||
hrm = hrmAny = undefined;
|
||||
Bangle.setCompassPower(settings.mag, "btadv");
|
||||
if (!settings.mag)
|
||||
mag = undefined;
|
||||
};
|
||||
var haveServiceData = function (serv) {
|
||||
switch (serv) {
|
||||
case "0x180d": return !!hrm;
|
||||
case "0x181a": return !!(bar || mag);
|
||||
case "0x1819": return !!(gps && gps.lat && gps.lon || mag);
|
||||
}
|
||||
};
|
||||
var serviceToAdvert = function (serv, initial) {
|
||||
var _a, _b, _c;
|
||||
if (initial === void 0) { initial = false; }
|
||||
switch (serv) {
|
||||
case "0x180d":
|
||||
if (hrm || initial) {
|
||||
var o = {
|
||||
maxLen: encodeHrm.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (hrm) {
|
||||
o.value = encodeHrm(hrm);
|
||||
hrm = undefined;
|
||||
}
|
||||
return _a = {}, _a["0x2a37"] = o, _a;
|
||||
}
|
||||
return {};
|
||||
case "0x1819":
|
||||
if (gps || initial) {
|
||||
var o = {
|
||||
maxLen: encodeGps.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (gps) {
|
||||
o.value = encodeGps(gps);
|
||||
gps = undefined;
|
||||
}
|
||||
return _b = {}, _b["0x2a67"] = o, _b;
|
||||
}
|
||||
else if (mag) {
|
||||
var o = {
|
||||
maxLen: encodeGpsHeadingOnly.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
value: encodeGpsHeadingOnly(mag),
|
||||
};
|
||||
return _c = {}, _c["0x2a67"] = o, _c;
|
||||
}
|
||||
return {};
|
||||
case "0x181a": {
|
||||
var o = {};
|
||||
if (bar || initial) {
|
||||
o["0x2a6c"] = {
|
||||
maxLen: encodeElevation.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
o["0x2A1F"] = {
|
||||
maxLen: encodeTemp.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
o["0x2a6d"] = {
|
||||
maxLen: encodePressure.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (bar) {
|
||||
o["0x2a6c"].value = encodeElevation(bar);
|
||||
o["0x2A1F"].value = encodeTemp(bar);
|
||||
o["0x2a6d"].value = encodePressure(bar);
|
||||
bar = undefined;
|
||||
}
|
||||
}
|
||||
if (mag || initial) {
|
||||
o["0x2aa1"] = {
|
||||
maxLen: encodeMag.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (mag) {
|
||||
o["0x2aa1"].value = encodeMag(mag);
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
};
|
||||
var getBleAdvert = function (map, all) {
|
||||
if (all === void 0) { all = false; }
|
||||
var advert = {};
|
||||
for (var _i = 0, services_1 = services; _i < services_1.length; _i++) {
|
||||
var serv = services_1[_i];
|
||||
if (all || haveServiceData(serv)) {
|
||||
advert[serv] = map(serv);
|
||||
}
|
||||
}
|
||||
mag = undefined;
|
||||
return advert;
|
||||
};
|
||||
var updateServices = function () {
|
||||
var newAdvert = getBleAdvert(serviceToAdvert);
|
||||
NRF.updateServices(newAdvert);
|
||||
};
|
||||
var onAccel = function (newAcc) { return acc = newAcc; };
|
||||
var onPressure = function (newBar) { return bar = newBar; };
|
||||
var onGPS = function (newGps) { return gps = newGps; };
|
||||
var onHRM = function (newHrm) {
|
||||
if (newHrm.confidence >= HRM_MIN_CONFIDENCE)
|
||||
hrm = newHrm;
|
||||
hrmAny = newHrm;
|
||||
};
|
||||
var onMag = function (newMag) { return mag = newMag; };
|
||||
var hook = function (enable) {
|
||||
if (enable) {
|
||||
Bangle.on("accel", onAccel);
|
||||
Bangle.on("pressure", onPressure);
|
||||
Bangle.on("GPS", onGPS);
|
||||
Bangle.on("HRM", onHRM);
|
||||
Bangle.on("mag", onMag);
|
||||
}
|
||||
else {
|
||||
Bangle.removeListener("accel", onAccel);
|
||||
Bangle.removeListener("pressure", onPressure);
|
||||
Bangle.removeListener("GPS", onGPS);
|
||||
Bangle.removeListener("HRM", onHRM);
|
||||
Bangle.removeListener("mag", onMag);
|
||||
}
|
||||
};
|
||||
var setIntervals = function (locked, connected) {
|
||||
if (locked === void 0) { locked = Bangle.isLocked(); }
|
||||
if (connected === void 0) { connected = NRF.getSecurityStatus().connected; }
|
||||
changeInterval(redrawInterval, locked ? 15000 : 5000);
|
||||
if (connected) {
|
||||
var interval = btnsShown ? 5000 : 1000;
|
||||
if (bleInterval) {
|
||||
changeInterval(bleInterval, interval);
|
||||
}
|
||||
else {
|
||||
bleInterval = setInterval(updateServices, interval);
|
||||
}
|
||||
}
|
||||
else if (bleInterval) {
|
||||
clearInterval(bleInterval);
|
||||
bleInterval = undefined;
|
||||
}
|
||||
};
|
||||
var redrawInterval = setInterval(redraw, 1000);
|
||||
Bangle.on("lock", function (locked) { return setIntervals(locked); });
|
||||
var bleInterval;
|
||||
NRF.on("connect", function () { return setIntervals(undefined, true); });
|
||||
NRF.on("disconnect", function () { return setIntervals(undefined, false); });
|
||||
setIntervals();
|
||||
setBtnsShown(true);
|
||||
enableSensors();
|
||||
{
|
||||
var ad = getBleAdvert(function (serv) { return serviceToAdvert(serv, true); }, true);
|
||||
var adServices = Object
|
||||
.keys(ad)
|
||||
.map(function (k) { return k.replace("0x", ""); });
|
||||
NRF.setServices(ad, {
|
||||
advertise: adServices,
|
||||
uart: false,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,715 @@
|
|||
// ts helpers:
|
||||
const __assign = Object.assign;
|
||||
|
||||
const Layout = require("Layout") as Layout_.Layout;
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
const enum Intervals {
|
||||
// BLE_ADVERT = 60 * 1000,
|
||||
BLE = 1000, // info screen
|
||||
BLE_BACKGROUND = 5000, // button screen
|
||||
UI_INFO = 5 * 1000, // info refresh, wake
|
||||
UI_INFO_SLEEP = 15 * 1000, // info refresh, asleep
|
||||
}
|
||||
|
||||
type Hrm = { bpm: number, confidence: number };
|
||||
|
||||
const HRM_MIN_CONFIDENCE = 75;
|
||||
|
||||
// https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/
|
||||
const enum BleServ {
|
||||
// org.bluetooth.service.heart_rate
|
||||
// contains: HRM
|
||||
HRM = "0x180d",
|
||||
|
||||
// org.bluetooth.service.environmental_sensing
|
||||
// contains: Elevation, Temp(Celsius), Pressure, Mag
|
||||
EnvSensing = "0x181a",
|
||||
|
||||
// org.bluetooth.service.location_and_navigation
|
||||
// contains: LocationAndSpeed
|
||||
LocationAndNavigation = "0x1819",
|
||||
|
||||
// Acc // none known for this
|
||||
}
|
||||
|
||||
const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation];
|
||||
|
||||
const enum BleChar {
|
||||
// org.bluetooth.characteristic.heart_rate_measurement
|
||||
// <see encode function>
|
||||
HRM = "0x2a37",
|
||||
|
||||
// org.bluetooth.characteristic.elevation
|
||||
// s24, meters 0.01
|
||||
Elevation = "0x2a6c",
|
||||
|
||||
// org.bluetooth.characteristic.temperature
|
||||
// s16 *10^2
|
||||
Temp = "0x2a6e",
|
||||
// org.bluetooth.characteristic.temperature_celsius
|
||||
// s16 *10^2
|
||||
TempCelsius = "0x2A1F",
|
||||
|
||||
// org.bluetooth.characteristic.pressure
|
||||
// u32 *10
|
||||
Pressure = "0x2a6d",
|
||||
|
||||
// org.bluetooth.characteristic.location_and_speed
|
||||
// <see encodeGps>
|
||||
LocationAndSpeed = "0x2a67",
|
||||
|
||||
// org.bluetooth.characteristic.magnetic_flux_density_3d
|
||||
// s16: x, y, z, tesla (10^-7)
|
||||
MagneticFlux3D = "0x2aa1",
|
||||
}
|
||||
|
||||
type BleCharAdvert = {
|
||||
value?: Array<number>,
|
||||
readable?: true,
|
||||
notify?: true,
|
||||
indicate?: true, // notify + ACK
|
||||
maxLen?: number,
|
||||
};
|
||||
|
||||
type BleServAdvert = {
|
||||
[key in BleChar]?: BleCharAdvert;
|
||||
};
|
||||
|
||||
type LenFunc<T> = {
|
||||
(_: T): Array<number>,
|
||||
maxLen: number,
|
||||
}
|
||||
|
||||
let acc: undefined | AccelData;
|
||||
let bar: undefined | PressureData;
|
||||
let gps: undefined | GPSFix;
|
||||
let hrm: undefined | Hrm;
|
||||
let hrmAny: undefined | Hrm;
|
||||
let mag: undefined | CompassData;
|
||||
let btnsShown = false;
|
||||
let prevBtnsShown: boolean | undefined = undefined;
|
||||
let hrmAnyClear: undefined | number;
|
||||
|
||||
type BtAdvType<IncludeAcc = false> = "bar" | "gps" | "hrm" | "mag" | (IncludeAcc extends true ? "acc" : never);
|
||||
type BtAdvMap<T, IncludeAcc = false> = { [key in BtAdvType<IncludeAcc>]: T };
|
||||
|
||||
const settings: BtAdvMap<boolean> = {
|
||||
bar: false,
|
||||
gps: false,
|
||||
hrm: false,
|
||||
mag: false,
|
||||
};
|
||||
|
||||
const idToName: BtAdvMap<string, true> = {
|
||||
acc: "Acceleration",
|
||||
bar: "Barometer",
|
||||
gps: "GPS",
|
||||
hrm: "HRM",
|
||||
mag: "Magnetometer",
|
||||
};
|
||||
|
||||
// 15 characters per line
|
||||
const infoFont: FontNameWithScaleFactor = "6x8:2";
|
||||
|
||||
const colour = {
|
||||
on: "#0f0",
|
||||
off: "#fff",
|
||||
} as const;
|
||||
|
||||
const makeToggle = (id: BtAdvType) => () => {
|
||||
settings[id] = !settings[id];
|
||||
|
||||
const entry = btnLayout[id]!;
|
||||
const col = settings[id] ? colour.on : colour.off;
|
||||
|
||||
entry.btnBorder = entry.col = col;
|
||||
|
||||
btnLayout.update();
|
||||
btnLayout.render();
|
||||
|
||||
//require('Storage').writeJSON(SETTINGS_FILENAME, settings);
|
||||
enableSensors();
|
||||
};
|
||||
|
||||
const btnStyle: {
|
||||
font: FontNameWithScaleFactor,
|
||||
fillx?: 1,
|
||||
filly?: 1,
|
||||
col: ColorResolvable,
|
||||
bgCol: ColorResolvable,
|
||||
btnBorder: ColorResolvable,
|
||||
} = {
|
||||
font: "Vector:14",
|
||||
fillx: 1,
|
||||
filly: 1,
|
||||
col: g.theme.fg,
|
||||
bgCol: g.theme.bg,
|
||||
btnBorder: "#fff",
|
||||
};
|
||||
|
||||
const btnLayout = new Layout(
|
||||
{
|
||||
type: "v",
|
||||
c: [
|
||||
{
|
||||
type: "h",
|
||||
c: [
|
||||
{
|
||||
type: "btn",
|
||||
label: idToName.bar,
|
||||
id: "bar",
|
||||
cb: makeToggle('bar'),
|
||||
...btnStyle,
|
||||
},
|
||||
{
|
||||
type: "btn",
|
||||
label: idToName.gps,
|
||||
id: "gps",
|
||||
cb: makeToggle('gps'),
|
||||
...btnStyle,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h",
|
||||
c: [
|
||||
// hrm, mag
|
||||
{
|
||||
type: "btn",
|
||||
label: idToName.hrm,
|
||||
id: "hrm",
|
||||
cb: makeToggle('hrm'),
|
||||
...btnStyle,
|
||||
},
|
||||
{
|
||||
type: "btn",
|
||||
label: idToName.mag,
|
||||
id: "mag",
|
||||
cb: makeToggle('mag'),
|
||||
...btnStyle,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h",
|
||||
c: [
|
||||
{
|
||||
type: "btn",
|
||||
label: idToName.acc,
|
||||
id: "acc",
|
||||
cb: () => {},
|
||||
...btnStyle,
|
||||
col: colour.on,
|
||||
btnBorder: colour.on,
|
||||
},
|
||||
{
|
||||
type: "btn",
|
||||
label: "Back",
|
||||
cb: () => {
|
||||
setBtnsShown(false);
|
||||
},
|
||||
...btnStyle,
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
lazy: true,
|
||||
back: () => {
|
||||
setBtnsShown(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const setBtnsShown = (b: boolean) => {
|
||||
btnsShown = b;
|
||||
|
||||
hook(!btnsShown);
|
||||
setIntervals();
|
||||
|
||||
redraw();
|
||||
};
|
||||
|
||||
const drawInfo = (force?: true) => {
|
||||
let { y, x, w } = Bangle.appRect;
|
||||
const mid = x + w / 2
|
||||
let drawn = false;
|
||||
|
||||
if (!force && !bar && !gps && !hrm && !mag)
|
||||
return;
|
||||
|
||||
g.reset()
|
||||
.clearRect(Bangle.appRect)
|
||||
.setFont(infoFont)
|
||||
.setFontAlign(0, -1);
|
||||
|
||||
if (bar) {
|
||||
g.drawString(`${bar.altitude.toFixed(1)}m`, mid, y);
|
||||
y += g.getFontHeight();
|
||||
|
||||
g.drawString(`${bar.pressure.toFixed(1)} hPa`, mid, y);
|
||||
y += g.getFontHeight();
|
||||
|
||||
g.drawString(`${bar.temperature.toFixed(1)}C`, mid, y);
|
||||
y += g.getFontHeight();
|
||||
|
||||
drawn = true;
|
||||
}
|
||||
|
||||
if (gps) {
|
||||
g.drawString(
|
||||
`${gps.lat.toFixed(4)} lat, ${gps.lon.toFixed(4)} lon`,
|
||||
mid,
|
||||
y,
|
||||
);
|
||||
y += g.getFontHeight();
|
||||
|
||||
g.drawString(
|
||||
`${gps.alt}m (${gps.satellites} sat)`,
|
||||
mid,
|
||||
y,
|
||||
);
|
||||
y += g.getFontHeight();
|
||||
|
||||
drawn = true;
|
||||
}
|
||||
|
||||
if (hrm) {
|
||||
g.drawString(`${hrm.bpm} BPM (${hrm.confidence}%)`, mid, y);
|
||||
y += g.getFontHeight();
|
||||
|
||||
drawn = true;
|
||||
} else if (hrmAny) {
|
||||
g.drawString(`~${hrmAny.bpm} BPM (${hrmAny.confidence}%)`, mid, y);
|
||||
y += g.getFontHeight();
|
||||
|
||||
drawn = true;
|
||||
|
||||
if (!settings.hrm && !hrmAnyClear) {
|
||||
// hrm is erased, but hrmAny will remain until cleared (or reset)
|
||||
// if it runs via health check, we reset it here
|
||||
hrmAnyClear = setTimeout(() => {
|
||||
hrmAny = undefined;
|
||||
hrmAnyClear = undefined;
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
if (mag) {
|
||||
g.drawString(
|
||||
`${mag.x} ${mag.y} ${mag.z}`,
|
||||
mid,
|
||||
y
|
||||
);
|
||||
y += g.getFontHeight();
|
||||
|
||||
g.drawString(
|
||||
`heading: ${mag.heading.toFixed(1)}`,
|
||||
mid,
|
||||
y
|
||||
);
|
||||
y += g.getFontHeight();
|
||||
|
||||
drawn = true;
|
||||
}
|
||||
|
||||
if (!drawn) {
|
||||
if (!force || Object.values(settings).every((x: boolean) => !x)) {
|
||||
g.drawString(`swipe to enable`, mid, y);
|
||||
} else {
|
||||
g.drawString(`events pending`, mid, y);
|
||||
}
|
||||
y += g.getFontHeight();
|
||||
}
|
||||
};
|
||||
|
||||
const onTap = (/* _: { ... } */) => {
|
||||
setBtnsShown(true);
|
||||
};
|
||||
|
||||
const redraw = () => {
|
||||
if (btnsShown) {
|
||||
if (!prevBtnsShown) {
|
||||
prevBtnsShown = btnsShown;
|
||||
|
||||
Bangle.removeListener("swipe", onTap);
|
||||
|
||||
btnLayout.setUI();
|
||||
btnLayout.forgetLazyState();
|
||||
g.clearRect(Bangle.appRect); // in case btnLayout isn't full screen
|
||||
}
|
||||
|
||||
btnLayout.render();
|
||||
} else {
|
||||
if (prevBtnsShown) {
|
||||
prevBtnsShown = btnsShown;
|
||||
|
||||
Bangle.setUI(); // remove all existing input handlers
|
||||
Bangle.on("swipe", onTap);
|
||||
|
||||
drawInfo(true);
|
||||
} else {
|
||||
drawInfo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const encodeHrm: LenFunc<Hrm> = (hrm: Hrm) =>
|
||||
// {
|
||||
// flags: u8,
|
||||
// bytes: [u8...]
|
||||
// }
|
||||
// flags {
|
||||
// 1 << 0: 16bit bpm
|
||||
// 1 << 1: sensor contact available
|
||||
// 1 << 2: sensor contact boolean
|
||||
// 1 << 3: energy expended, next 16 bits
|
||||
// 1 << 4: "rr" data available, u16s, intervals
|
||||
// }
|
||||
[0, hrm.bpm];
|
||||
encodeHrm.maxLen = 2;
|
||||
|
||||
const encodePressure: LenFunc<PressureData> = (data: PressureData) =>
|
||||
toByteArray(Math.round(data.pressure * 10), 4, false);
|
||||
encodePressure.maxLen = 4;
|
||||
|
||||
const encodeElevation: LenFunc<PressureData> = (data: PressureData) =>
|
||||
toByteArray(Math.round(data.altitude * 100), 3, true);
|
||||
encodeElevation.maxLen = 3;
|
||||
|
||||
const encodeTemp: LenFunc<PressureData> = (data: PressureData) =>
|
||||
toByteArray(Math.round(data.temperature * 10), 2, true);
|
||||
encodeTemp.maxLen = 2;
|
||||
|
||||
const encodeGps: LenFunc<GPSFix> = (data: GPSFix) => {
|
||||
// flags: 16 bits
|
||||
// bit 0: Instantaneous Speed Present
|
||||
// bit 1: Total Distance Present
|
||||
// bit 2: Location Present
|
||||
// bit 3: Elevation Present
|
||||
// bit 4: Heading Present
|
||||
// bit 5: Rolling Time Present
|
||||
// bit 6: UTC Time Present
|
||||
//
|
||||
// bit 7-8: position status
|
||||
// 0 (0b00): no position
|
||||
// 1 (0b01): position ok
|
||||
// 2 (0b10): estimated position
|
||||
// 3 (0b11): last known position
|
||||
//
|
||||
// bit 9: speed & distance format
|
||||
// 0: 2d
|
||||
// 1: 3d
|
||||
//
|
||||
// bit 10-11: elevation source
|
||||
// 0: Positioning System
|
||||
// 1: Barometric Air Pressure
|
||||
// 2: Database Service (or similiar)
|
||||
// 3: Other
|
||||
//
|
||||
// bit 12: Heading Source
|
||||
// 0: Heading based on movement
|
||||
// 1: Heading based on magnetic compass
|
||||
//
|
||||
// speed: u16 (m/s), 1/100
|
||||
// distance: u24, 1/10
|
||||
// lat: s32, 1/10^7
|
||||
// lon: s32, 1/10^7
|
||||
// elevation: s24, 1/100
|
||||
// heading: u16 (deg), 1/100
|
||||
// rolling time: u8 (s)
|
||||
// utc time: org.bluetooth.characteristic.date_time
|
||||
|
||||
const speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
|
||||
const lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
|
||||
const lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
|
||||
const elevation = toByteArray(Math.round(data.alt * 100), 3, true);
|
||||
const heading = toByteArray(Math.round(data.course * 100), 2, false);
|
||||
|
||||
return [
|
||||
0b10011101, // speed, location, elevation, heading [...]
|
||||
0b00000010, // position ok, 3d speed/distance
|
||||
speed[0]!, speed[1]!,
|
||||
lat[0]!, lat[1]!, lat[2]!, lat[3]!,
|
||||
lon[0]!, lon[1]!, lon[2]!, lon[3]!,
|
||||
elevation[0]!, elevation[1]!, elevation[2]!,
|
||||
heading[0]!, heading[1]!
|
||||
];
|
||||
};
|
||||
encodeGps.maxLen = 17;
|
||||
|
||||
const encodeGpsHeadingOnly: LenFunc<CompassData> = (data: CompassData) => {
|
||||
// see encodeGps()
|
||||
const heading = toByteArray(Math.round(data.heading * 100), 2, false);
|
||||
|
||||
return [
|
||||
0b00010000, // heading present
|
||||
0b00010000, // heading source: mag
|
||||
heading[0]!, heading[1]!
|
||||
];
|
||||
};
|
||||
encodeGpsHeadingOnly.maxLen = 17;
|
||||
|
||||
const encodeMag: LenFunc<CompassData> = (data: CompassData) => {
|
||||
const x = toByteArray(data.x, 2, true);
|
||||
const y = toByteArray(data.y, 2, true);
|
||||
const z = toByteArray(data.z, 2, true);
|
||||
|
||||
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
|
||||
};
|
||||
encodeMag.maxLen = 6;
|
||||
|
||||
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
|
||||
const byteArray: Array<number> = new Array(numberOfBytes);
|
||||
|
||||
if(isSigned && (value < 0)) {
|
||||
value += 1 << (numberOfBytes * 8);
|
||||
}
|
||||
|
||||
for(let index = 0; index < numberOfBytes; index++) {
|
||||
byteArray[index] = (value >> (index * 8)) & 0xff;
|
||||
}
|
||||
|
||||
return byteArray;
|
||||
};
|
||||
|
||||
const enableSensors = () => {
|
||||
Bangle.setBarometerPower(settings.bar, "btadv");
|
||||
if (!settings.bar)
|
||||
bar = undefined;
|
||||
|
||||
Bangle.setGPSPower(settings.gps, "btadv");
|
||||
if (!settings.gps)
|
||||
gps = undefined;
|
||||
|
||||
Bangle.setHRMPower(settings.hrm, "btadv");
|
||||
if (!settings.hrm)
|
||||
hrm = hrmAny = undefined;
|
||||
|
||||
Bangle.setCompassPower(settings.mag, "btadv");
|
||||
if (!settings.mag)
|
||||
mag = undefined;
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
|
||||
const haveServiceData = (serv: BleServ): boolean => {
|
||||
switch (serv) {
|
||||
case BleServ.HRM: return !!hrm;
|
||||
case BleServ.EnvSensing: return !!(bar || mag);
|
||||
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
|
||||
}
|
||||
};
|
||||
|
||||
const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
|
||||
switch (serv) {
|
||||
case BleServ.HRM:
|
||||
if (hrm || initial) {
|
||||
const o: BleCharAdvert = {
|
||||
maxLen: encodeHrm.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (hrm) {
|
||||
o.value = encodeHrm(hrm);
|
||||
hrm = undefined;
|
||||
}
|
||||
|
||||
return { [BleChar.HRM]: o };
|
||||
}
|
||||
return {};
|
||||
|
||||
case BleServ.LocationAndNavigation:
|
||||
if (gps || initial) {
|
||||
const o: BleCharAdvert = {
|
||||
maxLen: encodeGps.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (gps) {
|
||||
o.value = encodeGps(gps);
|
||||
gps = undefined;
|
||||
}
|
||||
|
||||
return { [BleChar.LocationAndSpeed]: o };
|
||||
} else if (mag) {
|
||||
const o: BleCharAdvert = {
|
||||
maxLen: encodeGpsHeadingOnly.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
value: encodeGpsHeadingOnly(mag),
|
||||
};
|
||||
|
||||
return { [BleChar.LocationAndSpeed]: o };
|
||||
}
|
||||
return {};
|
||||
|
||||
case BleServ.EnvSensing: {
|
||||
const o: BleServAdvert = {};
|
||||
|
||||
if (bar || initial) {
|
||||
o[BleChar.Elevation] = {
|
||||
maxLen: encodeElevation.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
o[BleChar.TempCelsius] = {
|
||||
maxLen: encodeTemp.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
o[BleChar.Pressure] = {
|
||||
maxLen: encodePressure.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
|
||||
if (bar) {
|
||||
o[BleChar.Elevation]!.value = encodeElevation(bar);
|
||||
o[BleChar.TempCelsius]!.value = encodeTemp(bar);
|
||||
o[BleChar.Pressure]!.value = encodePressure(bar);
|
||||
bar = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (mag || initial) {
|
||||
o[BleChar.MagneticFlux3D] = {
|
||||
maxLen: encodeMag.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
|
||||
if (mag) {
|
||||
o[BleChar.MagneticFlux3D]!.value = encodeMag(mag);
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getBleAdvert = <T>(map: (s: BleServ) => T, all = false) => {
|
||||
const advert: { [key in BleServ]?: T } = {};
|
||||
|
||||
for (const serv of services) {
|
||||
if (all || haveServiceData(serv)) {
|
||||
advert[serv] = map(serv);
|
||||
}
|
||||
}
|
||||
|
||||
// clear mag only after both EnvSensing and LocationAndNavigation have run
|
||||
mag = undefined;
|
||||
|
||||
return advert;
|
||||
};
|
||||
|
||||
// done via advertise in setServices()
|
||||
//const updateBleAdvert = () => {
|
||||
// let bleAdvert: ReturnType<typeof getBleAdvert<undefined>>;
|
||||
//
|
||||
// if (!(bleAdvert = (Bangle as any).bleAdvert)) {
|
||||
// bleAdvert = getBleAdvert(_ => undefined);
|
||||
//
|
||||
// (Bangle as any).bleAdvert = bleAdvert;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// NRF.setAdvertising(bleAdvert);
|
||||
// } catch (e) {
|
||||
// console.log("couldn't setAdvertising():", e);
|
||||
// }
|
||||
//};
|
||||
|
||||
const updateServices = () => {
|
||||
const newAdvert = getBleAdvert(serviceToAdvert);
|
||||
|
||||
NRF.updateServices(newAdvert);
|
||||
};
|
||||
|
||||
const onAccel = (newAcc: NonNull<typeof acc>) => acc = newAcc;
|
||||
const onPressure = (newBar: NonNull<typeof bar>) => bar = newBar;
|
||||
const onGPS = (newGps: NonNull<typeof gps>) => gps = newGps;
|
||||
const onHRM = (newHrm: NonNull<typeof hrm>) => {
|
||||
if (newHrm.confidence >= HRM_MIN_CONFIDENCE)
|
||||
hrm = newHrm;
|
||||
hrmAny = newHrm;
|
||||
};
|
||||
const onMag = (newMag: NonNull<typeof mag>) => mag = newMag;
|
||||
|
||||
const hook = (enable: boolean) => {
|
||||
// need to disable for perf reasons, when buttons are shown
|
||||
if (enable) {
|
||||
Bangle.on("accel", onAccel);
|
||||
Bangle.on("pressure", onPressure);
|
||||
Bangle.on("GPS", onGPS);
|
||||
Bangle.on("HRM", onHRM);
|
||||
Bangle.on("mag", onMag);
|
||||
} else {
|
||||
Bangle.removeListener("accel", onAccel);
|
||||
Bangle.removeListener("pressure", onPressure);
|
||||
Bangle.removeListener("GPS", onGPS);
|
||||
Bangle.removeListener("HRM", onHRM);
|
||||
Bangle.removeListener("mag", onMag);
|
||||
}
|
||||
}
|
||||
|
||||
// --- intervals ---
|
||||
|
||||
const setIntervals = (
|
||||
locked: boolean = Bangle.isLocked(),
|
||||
connected: boolean = NRF.getSecurityStatus().connected,
|
||||
) => {
|
||||
changeInterval(
|
||||
redrawInterval,
|
||||
locked ? Intervals.UI_INFO_SLEEP : Intervals.UI_INFO,
|
||||
);
|
||||
|
||||
if (connected) {
|
||||
const interval = btnsShown ? Intervals.BLE_BACKGROUND : Intervals.BLE;
|
||||
|
||||
if (bleInterval) {
|
||||
changeInterval(bleInterval, interval);
|
||||
} else {
|
||||
bleInterval = setInterval(updateServices, interval);
|
||||
}
|
||||
} else if (bleInterval) {
|
||||
clearInterval(bleInterval);
|
||||
bleInterval = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const redrawInterval = setInterval(redraw, /*replaced*/1000);
|
||||
Bangle.on("lock", locked => setIntervals(locked));
|
||||
|
||||
let bleInterval: undefined | number;
|
||||
NRF.on("connect", () => setIntervals(undefined, true));
|
||||
NRF.on("disconnect", () => setIntervals(undefined, false));
|
||||
|
||||
setIntervals();
|
||||
|
||||
// turn things on
|
||||
setBtnsShown(true);
|
||||
enableSensors();
|
||||
|
||||
// set services/advert once at startup:
|
||||
{
|
||||
// must have fixed services from the start:
|
||||
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
|
||||
|
||||
const adServices = Object
|
||||
.keys(ad)
|
||||
.map((k: string) => k.replace("0x", ""));
|
||||
|
||||
NRF.setServices(
|
||||
ad,
|
||||
{
|
||||
advertise: adServices,
|
||||
uart: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwZC/gECoARQpARQpIRRkARQkgRRwBrPkmQBpIvDCIMEyQQIgIvDR4WSSRIvDCIUSSRIvDCISSCJRAvCCIoaDCIgvCGooaCiRNEDoRZFBwQRFgDCBPooOCOI0JkihDhApBHARxDgiSCyTFCHYQRGUIQRDHYIRCHYIRBiChDBAJKBHYICBpIRDyQyCSQQgBCJNBCIbCDCIZNDF4R0DEYwRCIIa5BI5ARDyAdCNZIFCCIKYBR5QRBVoJ6BWZY7CTwTXJWYQRFfZYRFRgQRCAoT4DCIgICCIQpCHARlCfYRBDCIhlDCIZuDGor1BCIgCBLgZZEAAiABEYIGCPooALUIYRQVQYRLdIQRPKAQROCBzjELJ4RPAHoA=="))
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "btadv",
|
||||
"name": "btadv",
|
||||
"shortName": "btadv",
|
||||
"version": "0.01",
|
||||
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
|
||||
"icon": "icon.png",
|
||||
"tags": "health,tool,sensors,bluetooth",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"btadv.app.js","url":"app.js"},
|
||||
{"name":"btadv.img","url":"icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ function draw(){
|
|||
if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%";
|
||||
if (bt.rr) layout.btRR.label = bt.rr.join(",");
|
||||
if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location];
|
||||
if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No";
|
||||
if (bt.contact !== undefined) layout.btContact.label = bt.contact ? /*LANG*/"Yes":/*LANG*/"No";
|
||||
if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ";
|
||||
} else {
|
||||
layout.bt.label = "--";
|
||||
|
|
|
|||
|
|
@ -22,3 +22,12 @@
|
|||
0.22: Use the new clkinfo module for the menu.
|
||||
0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo.
|
||||
0.24: Update clock_info to avoid a redraw
|
||||
0.25: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on fw2v16.
|
||||
ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc.
|
||||
0.26: Use clkinfo.addInteractive instead of a custom implementation
|
||||
0.27: Clean out some leftovers in the remove function after switching to
|
||||
clkinfo.addInteractive that would cause ReferenceError.
|
||||
0.28: Option to show (1) time only and (2) week of year.
|
||||
0.29: use setItem of clockInfoMenu to change the active item
|
||||
0.30: Use widget_utils
|
||||
0.31: Use clock_info module as an app
|
||||
|
|
|
|||
|
|
@ -5,16 +5,13 @@ A very minimalistic clock.
|
|||
|
||||
## Features
|
||||
The BW clock implements features that are exposed by other apps through the `clkinfo` module.
|
||||
For example, if you install the HomeAssistant app, this menu item will be shown if you click right
|
||||
and additionally allows you to send triggers directly from the clock (select triggers via up/down and
|
||||
send via click center). Here are examples of other apps that are integrated:
|
||||
For example, if you install the HomeAssistant app, this menu item will be shown if you first
|
||||
touch the bottom of the screen and then swipe left/right to the home assistant menu. To select
|
||||
sub-items simply swipe up/down. To run an action (e.g. trigger home assistant), simply select the clkinfo (border) and touch on the item again. See also the screenshot below:
|
||||
|
||||
- Bangle data such as steps, heart rate, battery or charging state.
|
||||
- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled*
|
||||
- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
|
||||
- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
|
||||

|
||||
|
||||
Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden.
|
||||
Note: Check out the settings to change different themes.
|
||||
|
||||
## Settings
|
||||
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).
|
||||
|
|
@ -22,25 +19,6 @@ Note: If some apps are not installed (e.gt. weather app), then this menu item is
|
|||
- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further.
|
||||
- Your bangle uses the sys color settings so you can change the color too.
|
||||
|
||||
## Menu structure
|
||||
2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to trigger HomeAssistant.
|
||||
|
||||
Simply click left / right to go through the menu entries such as Bangle, Weather etc.
|
||||
and click up/down to move into this sub-menu. You can then click in the middle of the screen
|
||||
to e.g. send a trigger via HomeAssistant once you selected it. The actions really depend
|
||||
on the app that provide this sub-menu through the `clkinfo` module.
|
||||
|
||||
```
|
||||
Bangle -- Agenda -- Weather -- HomeAssistant
|
||||
| | | |
|
||||
Battery Entry 1 Temperature Trigger1
|
||||
| | | |
|
||||
Steps ... ... ...
|
||||
|
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Thanks to
|
||||
- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located.
|
||||
- <a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
{ // must be inside our own scope here so that when we are unloaded everything disappears
|
||||
|
||||
/************************************************
|
||||
* Includes
|
||||
*/
|
||||
const locale = require('locale');
|
||||
const storage = require('Storage');
|
||||
const clock_info = require("clock_info");
|
||||
|
||||
const widget_utils = require("widget_utils");
|
||||
|
||||
/************************************************
|
||||
* Globals
|
||||
|
|
@ -12,8 +14,6 @@ const clock_info = require("clock_info");
|
|||
const SETTINGS_FILE = "bwclk.setting.json";
|
||||
const W = g.getWidth();
|
||||
const H = g.getHeight();
|
||||
var lock_input = false;
|
||||
|
||||
|
||||
/************************************************
|
||||
* Settings
|
||||
|
|
@ -28,7 +28,20 @@ let settings = {
|
|||
|
||||
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
||||
for (const key in saved_settings) {
|
||||
settings[key] = saved_settings[key]
|
||||
settings[key] = saved_settings[key];
|
||||
}
|
||||
|
||||
let isFullscreen = function() {
|
||||
var s = settings.screen.toLowerCase();
|
||||
if(s == "dynamic"){
|
||||
return Bangle.isLocked();
|
||||
} else {
|
||||
return s == "full";
|
||||
}
|
||||
};
|
||||
|
||||
let getLineY = function(){
|
||||
return H/5*2 + (isFullscreen() ? 0 : 8);
|
||||
}
|
||||
|
||||
/************************************************
|
||||
|
|
@ -74,32 +87,22 @@ Graphics.prototype.setMiniFont = function(scale) {
|
|||
return this;
|
||||
};
|
||||
|
||||
function imgLock(){
|
||||
let imgLock = function() {
|
||||
return {
|
||||
width : 16, height : 16, bpp : 1,
|
||||
transparent : 0,
|
||||
buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w="))
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/************************************************
|
||||
* Menu
|
||||
* Clock Info
|
||||
*/
|
||||
// Custom bwItems menu - therefore, its added here and not in a clkinfo.js file.
|
||||
var bwItems = {
|
||||
name: null,
|
||||
img: null,
|
||||
items: [
|
||||
{ name: "WeekOfYear",
|
||||
get: () => ({ text: "Week " + weekOfYear(), img: null}),
|
||||
show: function() {},
|
||||
hide: function () {}
|
||||
},
|
||||
]
|
||||
};
|
||||
let clockInfoItems = clock_info.load();
|
||||
|
||||
function weekOfYear() {
|
||||
// Add some custom clock-infos
|
||||
let weekOfYear = function() {
|
||||
var date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// Thursday in current week decides the year.
|
||||
|
|
@ -111,87 +114,99 @@ function weekOfYear() {
|
|||
- 3 + (week1.getDay() + 6) % 7) / 7);
|
||||
}
|
||||
|
||||
clockInfoItems[0].items.unshift({ name : "weekofyear",
|
||||
get : function() { return { text : "Week " + weekOfYear(),
|
||||
img : null}},
|
||||
show : function() {},
|
||||
hide : function() {},
|
||||
})
|
||||
|
||||
// Load menu
|
||||
var menu = clock_info.load();
|
||||
menu = menu.concat(bwItems);
|
||||
// Empty for large time
|
||||
clockInfoItems[0].items.unshift({ name : "nop",
|
||||
get : function() { return { text : null,
|
||||
img : null}},
|
||||
show : function() {},
|
||||
hide : function() {},
|
||||
})
|
||||
|
||||
|
||||
// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
|
||||
if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
|
||||
settings.menuPosX = 0;
|
||||
settings.menuPosY = 0;
|
||||
}
|
||||
|
||||
// Set draw functions for each item
|
||||
menu.forEach((menuItm, x) => {
|
||||
menuItm.items.forEach((item, y) => {
|
||||
function drawItem() {
|
||||
// For the clock, we have a special case, as we don't wanna redraw
|
||||
// immediately when something changes. Instead, we update data each minute
|
||||
// to save some battery etc. Therefore, we hide (and disable the listener)
|
||||
// immedeately after redraw...
|
||||
item.hide();
|
||||
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
|
||||
app: "bwclk",
|
||||
x : 0,
|
||||
y: 135,
|
||||
w: W,
|
||||
h: H-135,
|
||||
draw : (itm, info, options) => {
|
||||
var hideClkInfo = info.text == null;
|
||||
|
||||
// After drawing the item, we enable inputs again...
|
||||
lock_input = false;
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
|
||||
|
||||
var info = item.get();
|
||||
drawMenuItem(info.text, info.img);
|
||||
g.setFontAlign(0,0);
|
||||
g.setColor(g.theme.bg);
|
||||
|
||||
if (options.focus){
|
||||
var y = hideClkInfo ? options.y+20 : options.y+2;
|
||||
var h = hideClkInfo ? options.h-20 : options.h-2;
|
||||
g.drawRect(options.x, y, options.x+options.w-2, y+h-1); // show if focused
|
||||
g.drawRect(options.x+1, y+1, options.x+options.w-3, y+h-2); // show if focused
|
||||
}
|
||||
|
||||
item.on('redraw', drawItem);
|
||||
})
|
||||
// In case we hide the clkinfo, we show the time again as the time should
|
||||
// be drawn larger.
|
||||
if(hideClkInfo){
|
||||
drawTime();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set text and font
|
||||
var image = info.img;
|
||||
var text = String(info.text);
|
||||
if(text.split('\n').length > 1){
|
||||
g.setMiniFont();
|
||||
} else {
|
||||
g.setSmallFont();
|
||||
}
|
||||
|
||||
// Compute sizes
|
||||
var strWidth = g.stringWidth(text);
|
||||
var imgWidth = image == null ? 0 : 24;
|
||||
var midx = options.x+options.w/2;
|
||||
|
||||
// Draw
|
||||
if (image) {
|
||||
var scale = imgWidth / image.width;
|
||||
g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale});
|
||||
}
|
||||
g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20);
|
||||
|
||||
// In case we are in focus and the focus box changes (fullscreen yes/no)
|
||||
// we draw the time again. Otherwise it could happen that a while line is
|
||||
// not cleared correctly.
|
||||
if(options.focus) drawTime();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function canRunMenuItem(){
|
||||
if(settings.menuPosY == 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
var menuEntry = menu[settings.menuPosX];
|
||||
var item = menuEntry.items[settings.menuPosY-1];
|
||||
return item.run !== undefined;
|
||||
}
|
||||
|
||||
|
||||
function runMenuItem(){
|
||||
if(settings.menuPosY == 0){
|
||||
return;
|
||||
}
|
||||
|
||||
var menuEntry = menu[settings.menuPosX];
|
||||
var item = menuEntry.items[settings.menuPosY-1];
|
||||
try{
|
||||
var ret = item.run();
|
||||
if(ret){
|
||||
Bangle.buzz(300, 0.6);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Simply ignore it...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/************************************************
|
||||
* Draw
|
||||
*/
|
||||
function draw() {
|
||||
let draw = function() {
|
||||
// Queue draw again
|
||||
queueDraw();
|
||||
|
||||
// Draw clock
|
||||
drawDate();
|
||||
drawMenuAndTime();
|
||||
drawTime();
|
||||
drawLock();
|
||||
drawWidgets();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function drawDate(){
|
||||
let drawDate = function() {
|
||||
// Draw background
|
||||
var y = H/5*2 + (isFullscreen() ? 0 : 8);
|
||||
var y = getLineY()
|
||||
g.reset().clearRect(0,0,W,y);
|
||||
|
||||
// Draw date
|
||||
|
|
@ -216,17 +231,17 @@ function drawDate(){
|
|||
g.setMediumFont();
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawString(dateStr, W/2 - fullDateW / 2, y+2);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function drawTime(y, smallText){
|
||||
let drawTime = function() {
|
||||
var hideClkInfo = clockInfoMenu.menuA == 0 && clockInfoMenu.menuB == 0;
|
||||
|
||||
// Draw background
|
||||
var y1 = getLineY();
|
||||
var y = y1;
|
||||
var date = new Date();
|
||||
|
||||
// Draw time
|
||||
g.setColor(g.theme.bg);
|
||||
g.setFontAlign(0,0);
|
||||
|
||||
var hours = String(date.getHours());
|
||||
var minutes = date.getMinutes();
|
||||
minutes = minutes < 10 ? String("0") + minutes : minutes;
|
||||
|
|
@ -236,212 +251,93 @@ function drawTime(y, smallText){
|
|||
// Set y coordinates correctly
|
||||
y += parseInt((H - y)/2) + 5;
|
||||
|
||||
// Show large or small time depending on info entry
|
||||
if(smallText){
|
||||
if (hideClkInfo){
|
||||
g.setLargeFont();
|
||||
} else {
|
||||
y -= 15;
|
||||
g.setMediumFont();
|
||||
} else {
|
||||
g.setLargeFont();
|
||||
}
|
||||
g.drawString(timeStr, W/2, y);
|
||||
}
|
||||
|
||||
function drawMenuItem(text, image){
|
||||
// First clear the time region
|
||||
var y = H/5*2 + (isFullscreen() ? 0 : 8);
|
||||
|
||||
// Clear region and draw time
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillRect(0,y,W,H);
|
||||
g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0));
|
||||
|
||||
// Draw menu text
|
||||
var hasText = (text != null && text != "");
|
||||
if(hasText){
|
||||
g.setFontAlign(0,0);
|
||||
|
||||
// For multiline text we show an even smaller font...
|
||||
text = String(text);
|
||||
if(text.split('\n').length > 1){
|
||||
g.setMiniFont();
|
||||
} else {
|
||||
g.setSmallFont();
|
||||
}
|
||||
|
||||
var imgWidth = image == null ? 0 : 24;
|
||||
var strWidth = g.stringWidth(text);
|
||||
g.setColor(g.theme.fg).fillRect(0, 149-14, W, H);
|
||||
g.setColor(g.theme.bg).drawString(text, W/2 + imgWidth/2 + 2, 149+3);
|
||||
|
||||
if(image != null){
|
||||
var scale = imgWidth / image.width;
|
||||
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 149 - parseInt(imgWidth/2), {scale: scale});
|
||||
}
|
||||
}
|
||||
|
||||
// Draw time
|
||||
drawTime(y, hasText);
|
||||
}
|
||||
g.setColor(g.theme.bg);
|
||||
g.setFontAlign(0,0);
|
||||
g.drawString(timeStr, W/2, y);
|
||||
};
|
||||
|
||||
|
||||
function drawMenuAndTime(){
|
||||
var menuEntry = menu[settings.menuPosX];
|
||||
|
||||
// The first entry is the overview...
|
||||
if(settings.menuPosY == 0){
|
||||
drawMenuItem(menuEntry.name, menuEntry.img);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw item if needed
|
||||
lock_input = true;
|
||||
var item = menuEntry.items[settings.menuPosY-1];
|
||||
item.show();
|
||||
}
|
||||
|
||||
|
||||
function drawLock(){
|
||||
let drawLock = function() {
|
||||
if(settings.showLock && Bangle.isLocked()){
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawImage(imgLock(), W-16, 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function drawWidgets(){
|
||||
let drawWidgets = function() {
|
||||
if(isFullscreen()){
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
widget_utils.hide();
|
||||
} else {
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isFullscreen(){
|
||||
var s = settings.screen.toLowerCase();
|
||||
if(s == "dynamic"){
|
||||
return Bangle.isLocked()
|
||||
} else {
|
||||
return s == "full"
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/************************************************
|
||||
* Listener
|
||||
*/
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
let drawTimeout;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
let queueDraw = function() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Stop updates when LCD is off, restart when on
|
||||
Bangle.on('lcdPower',on=>{
|
||||
let lcdListenerBw = function(on) {
|
||||
if (on) {
|
||||
draw(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
Bangle.on('lcdPower', lcdListenerBw);
|
||||
|
||||
Bangle.on('lock', function(isLocked) {
|
||||
let lockListenerBw = function(isLocked) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
|
||||
if(!isLocked && settings.screen.toLowerCase() == "dynamic"){
|
||||
// If we have to show the widgets again, we load it from our
|
||||
// cache and not through Bangle.loadWidgets as its much faster!
|
||||
for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;}
|
||||
widget_utils.show();
|
||||
}
|
||||
|
||||
draw();
|
||||
});
|
||||
|
||||
Bangle.on('charging',function(charging) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
};
|
||||
Bangle.on('lock', lockListenerBw);
|
||||
|
||||
let charging = function(charging){
|
||||
// Jump to battery
|
||||
settings.menuPosX = 0;
|
||||
settings.menuPosY = 1;
|
||||
draw();
|
||||
});
|
||||
|
||||
Bangle.on('touch', function(btn, e){
|
||||
var widget_size = isFullscreen() ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better...
|
||||
var left = parseInt(g.getWidth() * 0.22);
|
||||
var right = g.getWidth() - left;
|
||||
var upper = parseInt(g.getHeight() * 0.22) + widget_size;
|
||||
var lower = g.getHeight() - upper;
|
||||
|
||||
var is_upper = e.y < upper;
|
||||
var is_lower = e.y > lower;
|
||||
var is_left = e.x < left && !is_upper && !is_lower;
|
||||
var is_right = e.x > right && !is_upper && !is_lower;
|
||||
var is_center = !is_upper && !is_lower && !is_left && !is_right;
|
||||
|
||||
if(lock_input){
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_lower){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
|
||||
|
||||
drawMenuAndTime();
|
||||
}
|
||||
|
||||
if(is_upper){
|
||||
if(e.y < widget_size){
|
||||
return;
|
||||
}
|
||||
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosY = settings.menuPosY-1;
|
||||
settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
|
||||
|
||||
drawMenuAndTime();
|
||||
}
|
||||
|
||||
if(is_right){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosX = (settings.menuPosX+1) % menu.length;
|
||||
settings.menuPosY = 0;
|
||||
drawMenuAndTime();
|
||||
}
|
||||
|
||||
if(is_left){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.menuPosY = 0;
|
||||
settings.menuPosX = settings.menuPosX-1;
|
||||
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
|
||||
drawMenuAndTime();
|
||||
}
|
||||
|
||||
if(is_center){
|
||||
if(canRunMenuItem()){
|
||||
runMenuItem();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
E.on("kill", function(){
|
||||
try{
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
} catch(ex){
|
||||
// If this fails, we still kill the app...
|
||||
}
|
||||
});
|
||||
clockInfoMenu.setItem(0, 2);
|
||||
drawTime();
|
||||
}
|
||||
Bangle.on('charging', charging);
|
||||
|
||||
let kill = function(){
|
||||
clockInfoMenu.remove();
|
||||
delete clockInfoMenu;
|
||||
};
|
||||
E.on("kill", kill);
|
||||
|
||||
/************************************************
|
||||
* Startup Clock
|
||||
|
|
@ -450,17 +346,31 @@ E.on("kill", function(){
|
|||
// The upper part is inverse i.e. light if dark and dark if light theme
|
||||
// is enabled. In order to draw the widgets correctly, we invert the
|
||||
// dark/light theme as well as the colors.
|
||||
let themeBackup = g.theme;
|
||||
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
// Called to unload all of the clock app
|
||||
Bangle.removeListener('lcdPower', lcdListenerBw);
|
||||
Bangle.removeListener('lock', lockListenerBw);
|
||||
Bangle.removeListener('charging', charging);
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
// save settings
|
||||
kill();
|
||||
E.removeListener("kill", kill);
|
||||
g.setTheme(themeBackup);
|
||||
widget_utils.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Load widgets and draw clock the first time
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Cache draw function for dynamic screen to hide / show widgets
|
||||
// Bangle.loadWidgets() could also be called later on but its much slower!
|
||||
for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;}
|
||||
|
||||
// Draw first time
|
||||
draw();
|
||||
|
||||
} // End of app scope
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"id": "bwclk",
|
||||
"name": "BW Clock",
|
||||
"version": "0.24",
|
||||
"description": "A very minimalistic clock to mainly show date and time.",
|
||||
"version": "0.31",
|
||||
"description": "A very minimalistic clock.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}],
|
||||
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock,clkinfo",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies" : { "clock_info":"module" },
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"bwclk.app.js","url":"app.js"},
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -10,3 +10,6 @@
|
|||
0.09: Fix scope of let variables
|
||||
0.10: Use default Bangle formatter for booleans
|
||||
0.11: Fix off-by-one-error on next year
|
||||
0.12: Mark dated events on a day
|
||||
0.13: Switch to swipe left/right for month and up/down for year selection
|
||||
Display events for current month on touch
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ Basic calendar
|
|||
|
||||
## Usage
|
||||
|
||||
- Use `BTN4` (left screen tap) to go to the previous month
|
||||
- Use `BTN5` (right screen tap) to go to the next month
|
||||
- Swipe left to go to the previous month
|
||||
- Swipe right to go to the next month
|
||||
- Swipe up (Bangle.js 2 only) to go to the previous year
|
||||
- Swipe down (Bangle.js 2 only) to go to the next year
|
||||
- Touch to display events for current month
|
||||
- Press the button (button 3 on Bangle.js 1) to exit
|
||||
|
||||
## Settings
|
||||
|
||||
- Starts Sunday: whether the calendar should start on Sunday (default is Monday).
|
||||
- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
|
||||
|
||||
|
|
|
|||
|
|
@ -22,15 +22,27 @@ let bgColorDow = color2;
|
|||
let bgColorWeekend = color3;
|
||||
let fgOtherMonth = gray1;
|
||||
let fgSameMonth = white;
|
||||
let bgEvent = blue;
|
||||
const eventsPerDay=6; // how much different events per day we can display
|
||||
const date = new Date();
|
||||
|
||||
const timeutils = require("time_utils");
|
||||
let settings = require('Storage').readJSON("calendar.json", true) || {};
|
||||
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
|
||||
if (settings.ndColors === undefined)
|
||||
if (process.env.HWVERSION == 2) {
|
||||
settings.ndColors = true;
|
||||
} else {
|
||||
settings.ndColors = false;
|
||||
}
|
||||
// all alarms that run on a specific date
|
||||
const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
|
||||
const date = new Date(a.date);
|
||||
const time = timeutils.decodeTime(a.t);
|
||||
date.setHours(time.h);
|
||||
date.setMinutes(time.m);
|
||||
date.setSeconds(time.s);
|
||||
return {date: date, msg: a.msg};
|
||||
});
|
||||
events.sort((a,b) => a.date - b.date);
|
||||
|
||||
if (settings.ndColors === undefined) {
|
||||
settings.ndColors = !g.theme.dark;
|
||||
}
|
||||
|
||||
if (settings.ndColors === true) {
|
||||
bgColor = white;
|
||||
|
|
@ -39,6 +51,7 @@ if (settings.ndColors === true) {
|
|||
bgColorWeekend = yellow;
|
||||
fgOtherMonth = blue;
|
||||
fgSameMonth = black;
|
||||
bgEvent = color2;
|
||||
}
|
||||
|
||||
function getDowLbls(locale) {
|
||||
|
|
@ -103,6 +116,12 @@ function getDowLbls(locale) {
|
|||
return dowLbls;
|
||||
}
|
||||
|
||||
function sameDay(d1, d2) {
|
||||
return d1.getFullYear() === d2.getFullYear() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getDate() === d2.getDate();
|
||||
}
|
||||
|
||||
function drawCalendar(date) {
|
||||
g.setBgColor(bgColor);
|
||||
g.clearRect(0, 0, maxX, maxY);
|
||||
|
|
@ -183,19 +202,26 @@ function drawCalendar(date) {
|
|||
}
|
||||
}
|
||||
|
||||
const weekBeforeMonth = new Date(date.getTime());
|
||||
weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7);
|
||||
const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
|
||||
week2AfterMonth.setDate(week2AfterMonth.getDate() + 14);
|
||||
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
|
||||
|
||||
let i = 0;
|
||||
for (y = 0; y < rowN - 1; y++) {
|
||||
for (x = 0; x < colN; x++) {
|
||||
i++;
|
||||
const day = days[i];
|
||||
const isToday =
|
||||
today.year === year && today.month === month && today.day === day - 50;
|
||||
const curMonth = day < 15 ? month+1 : day < 50 ? month-1 : month;
|
||||
const curDay = new Date(year, curMonth, day > 50 ? day-50 : day);
|
||||
const isToday = sameDay(curDay, new Date());
|
||||
const x1 = x * colW;
|
||||
const y1 = y * rowH + headerH + rowH;
|
||||
const x2 = x * colW + colW;
|
||||
const y2 = y * rowH + headerH + rowH + rowH;
|
||||
if (isToday) {
|
||||
g.setColor(red);
|
||||
let x1 = x * colW;
|
||||
let y1 = y * rowH + headerH + rowH;
|
||||
let x2 = x * colW + colW;
|
||||
let y2 = y * rowH + headerH + rowH + rowH;
|
||||
g.drawRect(x1, y1, x2, y2);
|
||||
g.drawRect(
|
||||
x1 + 1,
|
||||
|
|
@ -204,6 +230,24 @@ function drawCalendar(date) {
|
|||
y2 - 1
|
||||
);
|
||||
}
|
||||
|
||||
if (eventsThisMonth.length > 0) {
|
||||
// Display events for this day
|
||||
g.setColor(bgEvent);
|
||||
eventsThisMonth.forEach((ev, idx) => {
|
||||
if (sameDay(ev.date, curDay)) {
|
||||
const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
|
||||
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
|
||||
const height = (y2-2) - (y1+2); // height of a cell
|
||||
const sliceHeight = height/eventsPerDay;
|
||||
const ystart = (y1+2) + slice*sliceHeight;
|
||||
g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
|
||||
|
||||
eventsThisMonth.splice(idx, 1); // this event is no longer needed
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
require("Font8x12").add(Graphics);
|
||||
g.setFont("8x12", fontSize);
|
||||
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
|
||||
|
|
@ -216,28 +260,51 @@ function drawCalendar(date) {
|
|||
}
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const today = {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear()
|
||||
};
|
||||
drawCalendar(date);
|
||||
clearWatch();
|
||||
Bangle.on("touch", area => {
|
||||
const month = date.getMonth();
|
||||
if (area == 1) {
|
||||
let prevMonth = month > 0 ? month - 1 : 11;
|
||||
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
|
||||
date.setMonth(prevMonth);
|
||||
} else {
|
||||
let nextMonth = month < 11 ? month + 1 : 0;
|
||||
if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1);
|
||||
date.setMonth(nextMonth);
|
||||
}
|
||||
drawCalendar(date);
|
||||
});
|
||||
function setUI() {
|
||||
Bangle.setUI({
|
||||
mode : "custom",
|
||||
swipe: (dirLR, dirUD) => {
|
||||
if (dirLR<0) { // left
|
||||
const month = date.getMonth();
|
||||
let prevMonth = month > 0 ? month - 1 : 11;
|
||||
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
|
||||
date.setMonth(prevMonth);
|
||||
drawCalendar(date);
|
||||
} else if (dirLR>0) { // right
|
||||
const month = date.getMonth();
|
||||
let nextMonth = month < 11 ? month + 1 : 0;
|
||||
if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1);
|
||||
date.setMonth(nextMonth);
|
||||
drawCalendar(date);
|
||||
} else if (dirUD<0) { // up
|
||||
date.setFullYear(date.getFullYear() - 1);
|
||||
drawCalendar(date);
|
||||
} else if (dirUD>0) { // down
|
||||
date.setFullYear(date.getFullYear() + 1);
|
||||
drawCalendar(date);
|
||||
}
|
||||
},
|
||||
btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(),
|
||||
touch: (n,e) => {
|
||||
const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => {
|
||||
const dateStr = require("locale").date(e.date, 1);
|
||||
const timeStr = require("locale").time(e.date, 1);
|
||||
return { title: `${dateStr} ${timeStr}` + (e.msg ? " " + e.msg : "") };
|
||||
});
|
||||
if (menu.length === 0) {
|
||||
menu.push({title: /*LANG*/"No events"});
|
||||
}
|
||||
menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() };
|
||||
menu["< Back"] = () => {
|
||||
E.showMenu();
|
||||
drawCalendar(date);
|
||||
setUI();
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show launcher when button pressed
|
||||
setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" });
|
||||
drawCalendar(date);
|
||||
setUI();
|
||||
// No space for widgets!
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "calendar",
|
||||
"name": "Calendar",
|
||||
"version": "0.11",
|
||||
"version": "0.13",
|
||||
"description": "Simple calendar",
|
||||
"icon": "calendar.png",
|
||||
"screenshots": [{"url":"screenshot_calendar.png"}],
|
||||
"tags": "calendar",
|
||||
"tags": "calendar,tool",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
0.01: First version
|
||||
0.02: Support BangleJS2
|
||||
0.03: Added threshold
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Charge Gently
|
||||
|
||||
Charging Li-ion batteries to their full capacity has a significant impact on their lifespan. If possible, it is good practice to charge more often, but only to a certain lower capacity.
|
||||
|
||||
The first stage of charging Li-ion ends at ~80% capacity when the charge voltage reaches its peak*. When that happens, the watch will buzz twice every 30s to remind you to disconnect the watch.
|
||||
|
||||
This app has no UI and no configuration. To disable the app, you have to uninstall it.
|
||||
|
||||
New in v0.03: before the very first buzz, the average value after the peak is written to chargent.json and used as threshold for future charges. This reduces the time spent in the second charge stage.
|
||||
|
||||
Side notes
|
||||
- Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100.
|
||||
- This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise.
|
||||
|
||||
\* according to https://batteryuniversity.com/article/bu-409-charging-lithium-ion assuming similar characteristics and readouts from pin `D30` approximate charge voltage
|
||||