Merge branch 'espruino:master' into Messages-Light

master
Luca Pozzi 2023-02-28 18:57:34 +01:00 committed by GitHub
commit 5b0225e58b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
634 changed files with 22572 additions and 12031 deletions

View File

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

View File

@ -58,3 +58,7 @@ body:
validations:
required: true
- type: textarea
id: apps
attributes:
label: Installed apps

7
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "daily"

View File

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

View File

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

View File

@ -1,2 +1,3 @@
0.01: New app!
0.02: Better support for watch themes
0.03: Workaround minifier bug

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Fullscreen settings.
0.03: Tell clock widgets to hide.
0.04: Use widget_utils.

View File

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

View File

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

View File

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

View File

@ -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)
![](screenshot.png)
![](screenshot-1.png) ![](screenshot.png)
## Creator
[@alainsaas](https://github.com/alainsaas)

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,13 @@
updateSettings();
}
},
/*LANG*/"Use 'Today',..." : {
value : !!settings.useToday,
onchange: v => {
settings.useToday = v;
updateSettings();
}
},
};
E.showMenu(mainmenu);
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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` &rarr; Configure a new alarm
- `New Alarm` &rarr; Configure a new alarm (triggered based on time and day of week)
- `Repeat` &rarr; 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` &rarr; Configure a new timer
- `New Timer` &rarr; Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds)
- `New Event` &rarr; Configure a new event (triggered based on time and date)
- `Advanced`
- `Scheduler settings` &rarr; 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` &rarr; Enable _all_ disabled alarms & timers

View File

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

View File

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

1
apps/alarmqm/ChangeLog Normal file
View File

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

BIN
apps/alarmqm/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

20
apps/alarmqm/boot.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

126
apps/android/test.js Normal file
View File

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

View File

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

View File

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

View File

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

2
apps/backswipe/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Don't fire if the app uses swipes already.

23
apps/backswipe/README.md Normal file
View File

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

BIN
apps/backswipe/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

60
apps/backswipe/boot.js Normal file
View File

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

View File

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

126
apps/backswipe/settings.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4ARkQAHBwsBiIACiAHBgQXIkAXJiIuKGAwWEC4cjmYABn//AAMyC63yC653FC6HwC5aQBC5ybIC44WChGAWxMgC44rCxGIZxYXFIoYXBGAQNCAAQXILYYXBGAUDBoK0EC5AsBC4QwEC5wAEC853BhAWDI6CPCFwp3OX4ouCC8xHXCAJ3VX94XCwBHVGIiPTU4oNCAAQWBX5gDBgQRCAAoXGGAUIFwQXHkAXHJIgABCw4IBC5sAiIAEiAgHAAQXLHBAYIC+6wJQYIADgIXGGBJ3FC4iOBAH4A/ACAA=="))

41
apps/blescanner/app.js Normal file
View File

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

BIN
apps/blescanner/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

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

View File

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

View File

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

1
apps/btadv/ChangeLog Normal file
View File

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

16
apps/btadv/README.md Normal file
View File

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

412
apps/btadv/app.js Normal file
View File

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

715
apps/btadv/app.ts Normal file
View File

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

1
apps/btadv/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwZC/gECoARQpARQpIRRkARQkgRRwBrPkmQBpIvDCIMEyQQIgIvDR4WSSRIvDCIUSSRIvDCISSCJRAvCCIoaDCIgvCGooaCiRNEDoRZFBwQRFgDCBPooOCOI0JkihDhApBHARxDgiSCyTFCHYQRGUIQRDHYIRCHYIRBiChDBAJKBHYICBpIRDyQyCSQQgBCJNBCIbCDCIZNDF4R0DEYwRCIIa5BI5ARDyAdCNZIFCCIKYBR5QRBVoJ6BWZY7CTwTXJWYQRFfZYRFRgQRCAoT4DCIgICCIQpCHARlCfYRBDCIhlDCIZuDGor1BCIgCBLgZZEAAiABEYIGCPooALUIYRQVQYRLdIQRPKAQROCBzjELJ4RPAHoA=="))

BIN
apps/btadv/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

View File

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

View File

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

View File

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

3
apps/chargent/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: First version
0.02: Support BangleJS2
0.03: Added threshold

15
apps/chargent/README.md Normal file
View File

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

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