1
0
Fork 0

Merge branch 'espruino:master' into sleeplog

master
storm64 2022-11-07 22:00:37 +01:00 committed by GitHub
commit b2f471dc54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
489 changed files with 13257 additions and 7058 deletions

View File

@ -10,7 +10,7 @@ var img = atob("sIwDkm2S66DYwA2AAAAAHAHGSRxJEkAAgmGGBxDIADIdAFJIbAHF9HP00kBUC6Dt
var imgHeight = g.imageMetrics(img).height;
var imgScroll = Math.floor(Math.random()*imgHeight);
g.reset().setFont("6x15").setFontAlign(0,0);
g.clear(1).setFont("6x15").setFontAlign(0,0);
g.drawString(ENV.VERSION + " " + NRF.getAddress(), g.getWidth()/2, 171);
g.drawImage(img,0,24);

View File

@ -2,3 +2,4 @@
0.02: Use the new multiplatform 'Layout' library
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

View File

@ -1,5 +1,6 @@
var fileNumber = 0;
var MAXLOGS = 9;
var logRawData = false;
function getFileName(n) {
return "accellog."+n+".csv";
@ -24,6 +25,11 @@ function showMenu() {
/*LANG*/"View Logs" : function() {
viewLogs();
},
/*LANG*/"Log raw data" : {
value : logRawData,
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
onchange : v => { logRawData=v; }
},
};
E.showMenu(menu);
}
@ -78,6 +84,7 @@ function viewLogs() {
}
function startRecord(force) {
var stopped = false;
if (!force) {
// check for existing file
var f = require("Storage").open(getFileName(fileNumber), "r");
@ -92,39 +99,101 @@ function startRecord(force) {
var Layout = require("Layout");
var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
]
},{btns:[ // Buttons...
{label:/*LANG*/"STOP", cb:()=>{
Bangle.removeListener('accel', accelHandler);
showMenu();
{ type: "h", c: [
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
]},
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
]},
]},
{ type: "h", c: [
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Max X", pad:2},
{type:"txt", id:"maxX", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
]},
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Max Y", pad:2},
{type:"txt", id:"maxY", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
]},
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Max Z", pad:2},
{type:"txt", id:"maxZ", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
]},
]},
{type:"txt", font:"6x8", label:/*LANG*/"Max G", pad:2},
{type:"txt", id:"maxMag", font:"6x8:4", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", id:"state", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
]},
{
btns:[ // Buttons...
{id: "btnStop", label:/*LANG*/"STOP", cb:()=>{
if (stopped) {
showMenu();
}
else {
Bangle.removeListener('accel', accelHandler);
layout.state.label = /*LANG*/"STOPPED";
layout.state.bgCol = /*LANG*/"#0f0";
stopped = true;
layout.render();
}
}}
]});
layout.render();
// now start writing
var f = require("Storage").open(getFileName(fileNumber), "w");
f.write("Time (ms),X,Y,Z\n");
f.write("Time (ms),X,Y,Z,Total\n");
var start = getTime();
var sampleCount = 0;
var maxMag = 0;
var maxX = 0;
var maxY = 0;
var maxZ = 0;
function accelHandler(accel) {
var t = getTime()-start;
f.write([
t*1000,
accel.x*8192,
accel.y*8192,
accel.z*8192].map(n=>Math.round(n)).join(",")+"\n");
if (logRawData) {
f.write([
t*1000,
accel.x*8192,
accel.y*8192,
accel.z*8192,
accel.mag*8192,
].map(n=>Math.round(n)).join(",")+"\n");
} else {
f.write([
Math.round(t*1000),
accel.x,
accel.y,
accel.z,
accel.mag,
].join(",")+"\n");
}
if (accel.mag > maxMag) {
maxMag = accel.mag.toFixed(2);
}
if (accel.x > maxX) {
maxX = accel.x.toFixed(2);
}
if (accel.y > maxY) {
maxY = accel.y.toFixed(2);
}
if (accel.z > maxZ) {
maxZ = accel.z.toFixed(2);
}
sampleCount++;
layout.samples.label = sampleCount;
layout.time.label = Math.round(t)+"s";
layout.render(layout.samples);
layout.render(layout.time);
layout.maxX.label = maxX;
layout.maxY.label = maxY;
layout.maxZ.label = maxZ;
layout.maxMag.label = maxMag;
layout.render();
}
Bangle.setPollInterval(80); // 12.5 Hz - the default

View File

@ -2,7 +2,7 @@
"id": "accellog",
"name": "Acceleration Logger",
"shortName": "Accel Log",
"version": "0.04",
"version": "0.05",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png",
"tags": "outdoor",

View File

@ -3,4 +3,5 @@
0.03: Disable past events display from settings
0.04: Added awareness of allDay field
0.05: Displaying calendar colour and name
0.06: Added clkinfo for clocks.
0.06: Added clkinfo for clocks.
0.07: Clkinfo improvements.

View File

@ -10,11 +10,21 @@ Basic agenda reading the events synchronised from GadgetBridge.
* Show the colour of the calendar in the list
* Display description, location and calendar name after tapping on events
### Troubleshooting
For the events sync to work, GadgetBridge needs to have the calendar permission and calendar sync should be enabled in the devices settings (gear sign in GB, also check the blacklisted calendars there, if events are missing).
Keep in mind that GadgetBridge won't synchronize all events on your calendar, just the ones in a time window of 7 days (you don't want your watch to explode), ideally every day old events get deleted since they appear out of such window.
#### Force Sync
If for any reason events still cannot sync or some are missing, you can try any of the following (just one, you normally don't need to do this):
1. from GB open the burger menu (side), tap debug and set time.
2. from the bangle, open settings > apps > agenda > Force calendar sync, then select not to delete the local events (this is equivalent to option 1).
3. do like option 2 but delete events, GB will synchronize a fresh database instead of patching the old one (good in case you somehow cannot get rid of older events)
After any of the options, you may need to disconnect/force close Gadgetbridge before reconnecting and let it sync (give it some time for that too), restart the agenda app on the bangle after a while to see the changes.
### Report a bug
You can easily open an issue in the espruino repo, but I won't be notified and it might take time.
If you want a (hopefully) quicker response, just report [on my fork](https://github.com/glemco/BangleApps).
### Known Problems
Any all-day event lasts just one day: that is a GB limitation that we will likely fix in the future.

View File

@ -18,7 +18,7 @@
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
agendaItems.items.push({
name: "agendaEntry-" + i,
name: null,
get: () => ({ text: title + "\n" + dateStr, img: null}),
show: function() { agendaItems.items[i].emit("redraw"); },
hide: function () {}

View File

@ -1,7 +1,7 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.06",
"version": "0.07",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],

View File

@ -1,2 +1,5 @@
0.01: First, proof of concept
0.02: Load AGPS data on app start and automatically in background
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

View File

@ -36,7 +36,7 @@ function updateAgps() {
g.clear();
if (!waiting) {
waiting = true;
display("Updating A-GPS...");
display("Updating A-GPS...", "takes ~ 10 seconds");
require("agpsdata").pull(function() {
waiting = false;
display("A-GPS updated.", "touch to close");

View File

@ -16,13 +16,6 @@
}
if (settings.enabled) {
let lastUpdate = settings.lastUpdate;
if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){
if (!waiting){
waiting = true;
require("agpsdata").pull(successCallback, errorCallback);
}
}
setInterval(() => {
if (!waiting && NRF.getSecurityStatus().connected){
waiting = true;

View File

@ -8,41 +8,52 @@ var FILE = "agpsdata.settings.json";
var settings;
readSettings();
function setAGPS(data) {
var js = jsFromBase64(data);
try {
eval(js);
return true;
}
catch(e) {
console.log("error:", e);
}
return false;
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);
try {
writeChunks(atob(b64), resolve);
} catch (e) {
console.log("error:", e);
reject();
}
});
}
function jsFromBase64(b64) {
var bin = atob(b64);
var chunkSize = 128;
var js = "Bangle.setGPSPower(1);\n"; // turn GPS on
var gnsstype = settings.gnsstype || 1; // default GPS
js += `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)
var chunkI = 0;
function writeChunks(bin, resolve) {
return new Promise(function(resolve2) {
const chunkSize = 128;
setTimeout(function() {
if (chunkI < bin.length) {
var chunk = bin.substr(chunkI, chunkSize);
js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
eval(js);
for (var i=0;i<bin.length;i+=chunkSize) {
var chunk = bin.substr(i,chunkSize);
js += `Serial1.write(atob("${btoa(chunk)}"))\n`;
}
return js;
chunkI += chunkSize;
writeChunks(bin, resolve);
} else {
if (resolve)
resolve(); // call outer resolve
}
}, 200);
});
}
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i=1;i<cmd.length;i++)
for (var i = 1; i < cmd.length; i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0');
}
function updateLastUpdate() {
@ -53,23 +64,30 @@ function updateLastUpdate() {
}
exports.pull = function(successCallback, failureCallback) {
let uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http){
Bangle.http(uri, {timeout:10000}).then(event => {
let result = setAGPS(event.resp);
if (result) {
updateLastUpdate();
if (successCallback) successCallback();
} else {
console.log("error applying AGPS data");
if (failureCallback) failureCallback("Error applying AGPS data");
}
}).catch((e)=>{
console.log("error", e);
if (failureCallback) failureCallback(e);
});
const uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http) {
Bangle.http(uri, {timeout : 10000})
.then(event => {
setAGPS(event.resp)
.then(r => {
updateLastUpdate();
if (successCallback)
successCallback();
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
});
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
});
} else {
console.log("error: No http method found");
if (failureCallback) failureCallback(/*LANG*/"No http method");
if (failureCallback)
failureCallback(/*LANG*/ "No http method");
}
};

View File

@ -2,7 +2,7 @@
"name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data",
"icon": "agpsdata.png",
"version":"0.02",
"version":"0.04",
"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

@ -35,7 +35,7 @@ function buildMainMenu() {
},
"Refresh every" : {
value : settings.refresh / 60,
min : 1,
min : 6,
max : 168,
step : 1,
format : v => v + "h",

3
apps/aiclock/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New app!
0.02: Design improvements and fixes.
0.03: Indicate battery level through line occurrence.

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

@ -0,0 +1,23 @@
# AI Clock
This clock was designed by stable diffusion ([paper](https://arxiv.org/abs/2112.10752)) using the following prompt:
`A rectangle banglejs watchface`
The original output of stable diffusion is shown here:
![](orig.png)
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 ;)
![](impl.png)
# Thanks to
The great open-source community: I used an open-source diffusion model (https://github.com/CompVis/stable-diffusion)
to generate a watch face for the open-source smartwatch BangleJs.
## Creator
- [David Peer](https://github.com/peerdavid).

225
apps/aiclock/aiclock.app.js Normal file
View File

@ -0,0 +1,225 @@
/**
* AI Clock
*/
require("Font7x11Numeric7Seg").add(Graphics);
Graphics.prototype.setFontGochiHand = function(scale) {
// Actual height 27 (29 - 3)
this.setFontCustom(
atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAA8AAAAADwAAAAAPAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAA/+AAAB//4AAH///gAH///gAAf//AAAB/+AAAAH8AAAAAAAAAAAAAAAAAAAAH8AAAAB/8AAAAP/4AAAB//wAAAPx/AAAB8B+AAAHgD4AAA+AHgAADwAeAAAPAB4AAA8AHgAAD4AeAAAPgB4AAAeAPgAAB8A8AAAH4HwAAAP/+AAAAf/wAAAA/+AAAAB/wAAAAB8AAAAAAAAAAADgAAAAAfAAAAAB4AAAAAPAAAAAB8AAAAAHgAAAAA8AAAAADwAAAAAf4AAAAB//8AAAD//4AAAH//gAAAD/+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AHgAAHgA+AAA/AD4AAD4AfgAAfAD+AAB4Af4AAHgD/gAAeAfeAAB4D54AAHw/HgAAf/4fAAA//B8AAD/4DwAAH+APAAAHgA8AAAAADwAAAAAOAAAAAAAAABgAAAAAPAAAAAB8AAAAAHwAYAAAeAD4AAD4APwAAPA4fgAA8Hw+AADwfB4AAPh4HwAA+HgPAAB/+A8AAH/4DwAAP/weAAAf/j4AAAc//gAAAB/8AAAAD/gAAAAD8AAAAAAAAAAAAAAAAAADAAAAAA+AAAAAP4AAAAB/wAAAAP/AAAAD+8AAAAfzwAAAf8HAAAB/gcAAAH/hwAAAf//gAAA//+AAAAf//gAAAP//gAAAD/+AAAAB/4AAAAH/AAAAAeAAAAAAgAAAAAAAAAAAAcAAAB8H8AAAP4f4AAA/x/wAAD/H/gAAf+A+AAB74B4AAHnwHgAAefAfAAB58A8AAHj4DwAAePgPAAB4fA8AAHh+HgAAeD8+AAB4P/4AAHgf/AAAeA/4AAAAA+AAAAAAAAAAAAAAAAAAHgAAAAD/wAAAA//gAAAH//AAAA//+AAAD4H8AAAfA/wAAB4D/AAAHgP+AAAeB54AAB4HngAAHweeAAAfB54AAA4HngAAAAeeAAAAB/4AAAAH/AAAAAP4AAAAAfAAADwAAAAAPAAAAAA8HgAAADweAAAAPB4AAAA8HgAAADweAAAAPh4AAAA+HgAAAB4eAAAAHx4AAAAf//8AAA///wAAD//+AAAH//4AAAAeAAAAAB4AAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAD+AAAA+f+AAAH//8AAA///wAAH/4fgAAePgeAAB4+B4AAHj4HwAAePgPAAB4+A8AAHz4DwAAfngeAAA//B4AAD/+HgAAH//8AAAP//wAAAAf+AAAAA/wAAAAAYAAAAAAAAAAA/gAAAAH/AAAAA/8AAAAD34AAAAeHgAAAB4eAAAAHh4AAAA8HgAAADweAAAAPDwAAAA8PAAAADx4AAAAPvgAAAAf///AAB///8AAH///wAAP///AAA/wA4AABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOA4AAAA8DwAAADwPAAAAPA8AAAAYBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='),
46,
atob("DQoXEBQVExUUFRYUDQ=="),
40+(scale<<8)+(1<<16)
);
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;
/*
* 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() {
g.setFontAlign(0,0);
g.setColor(g.theme.fg);
var bat = E.getBattery() / 100.0;
var y = 0;
while(y < H){
// Show less lines in case of small battery level.
if(Math.random() > bat){
y += 5;
continue;
}
y += 3 + Math.floor(Math.random() * 10);
g.drawLine(0, y, W, y);
g.drawLine(0, y+1, W, y+1);
g.drawLine(0, y+2, W, y+2);
y += 2;
}
}
function drawCircle(isLocked){
g.setColor(g.theme.fg);
g.fillCircle(cx, cy, 12);
var c = isLocked ? "#f00" : g.theme.bg;
g.setColor(c);
g.fillCircle(cx, cy, 6);
}
function toAngle(a){
if (a < 0){
return 360 + a;
}
if(a > 360) {
return 360 - a;
}
return a
}
function drawTime(){
var drawHourHand = g.drawRotRect.bind(g,8,12,R-38);
var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 );
g.setFontAlign(0,0);
// Compute angles
var date = new Date();
var m = parseInt(date.getMinutes() * 360 / 60);
var h = date.getHours();
h = h > 12 ? h-12 : h;
h += date.getMinutes()/60.0;
h = parseInt(h*360/12);
// Draw minute and hour bg
g.setColor(g.theme.bg);
drawHourHand(toAngle(h-3));
drawHourHand(toAngle(h+3));
drawMinuteHand(toAngle(m-2));
drawMinuteHand(toAngle(m+3));
// Draw minute and hour fg
g.setColor(g.theme.fg);
drawHourHand(h);
drawMinuteHand(m);
}
function drawDate(){
var date = new Date();
g.setFontAlign(0,0);
g.setFontGochiHand();
var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2);
var w = g.stringWidth(text);
g.setColor(g.theme.bg);
g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12);
g.setColor(g.theme.fg);
// Draw right line as designed by stable diffusion
g.drawLine(cx+w/2+5, 20, cx+w/2+5, 40+12);
g.drawLine(cx+w/2+6, 20, cx+w/2+6, 40+12);
g.drawLine(cx+w/2+7, 20, cx+w/2+7, 40+12);
// And finally the text
g.drawString(text, cx, 40);
}
function drawDigits(){
var date = new Date();
g.setFontAlign(0,0);
g.setFont("7x11Numeric7Seg",3);
var text = ("0"+date.getHours()).substr(-2) + ":" + ("0"+date.getMinutes()).substr(-2); //Bangle.getHealthStatus("day").steps;
var w = g.stringWidth(text);
g.setColor(g.theme.bg);
g.fillRect(cx-w/2-4, 120, cx+w/2+4, 140+20);
// Draw right line as designed by stable diffusion
g.setColor(g.theme.fg);
g.drawLine(cx+w/2+5, 120, cx+w/2+5, 140+20);
g.drawLine(cx+w/2+6, 120, cx+w/2+6, 140+20);
g.drawLine(cx+w/2+7, 120, cx+w/2+7, 140+20);
// And the 7set text
g.setColor("#BBB");
g.drawString("88:88", cx, 140);
g.drawString("88:88", cx+1, 140);
g.drawString("88:88", cx, 141);
g.setColor(g.theme.fg);
g.drawString(text, cx, 140);
g.drawString(text, cx+1, 140);
g.drawString(text, cx, 141);
}
function draw(){
// Queue draw in one minute
queueDraw();
g.reset();
g.clearRect(0, 0, g.getWidth(), g.getHeight());
g.setColor(1,1,1);
drawBackground();
drawDate();
drawDigits();
drawTime();
drawCircle(Bangle.isLocked());
}
/*
* Listeners
*/
Bangle.on('lcdPower',on=>{
if (on) {
draw(true);
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.on('lock', function(isLocked) {
drawCircle(isLocked);
});
/*
* Some helpers
*/
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
/*
* Lets start widgets, listen for btn etc.
*/
// Show launcher when middle button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
// Clear the screen once, at startup and draw clock
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
draw();
// After drawing the watch face, we can draw the widgets
// Bangle.drawWidgets();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgP/ACfAEZU/ECZELIKhSR/+PAoWAv4FDhk/x/ggP+j0fx/AgP8n8PCIX8CwIFC/F/w4FBgP4gEHC4QFE//w//DC4QFB8YFC+P/8IdCAoYdBAoPxDoQAd+CiKh4dQwDhfAA4A="))

BIN
apps/aiclock/aiclock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
apps/aiclock/impl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,20 @@
{
"id": "aiclock",
"name": "AI Clock",
"shortName":"AI Clock",
"icon": "aiclock.png",
"version":"0.03",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
"type": "clock",
"tags": "clock",
"screenshots": [
{"url":"orig.png"},
{"url":"impl.png"}
],
"storage": [
{"name":"aiclock.app.js","url":"aiclock.app.js"},
{"name":"aiclock.img","url":"aiclock.icon.js","evaluate":true}
]
}

BIN
apps/aiclock/orig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

2
apps/alpinenav/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -224,7 +224,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading))
compass_heading = "---";
else
compass_heading = 360 - Math.round(m.heading);
compass_heading = Math.round(m.heading);
current_colour = g.getColor();
g.reset();
g.setColor(background_colour);

View File

@ -1,7 +1,7 @@
{
"id": "alpinenav",
"name": "Alpine Nav",
"version": "0.01",
"version": "0.02",
"description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime",
"icon": "app-icon.png",
"tags": "outdoors,gps",

View File

@ -13,3 +13,4 @@
0.13: Added Bangle.http function (see Readme file for more info)
0.14: Fix timeout of http function not being cleaned up
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)

View File

@ -139,6 +139,8 @@
// options = {id,timeout,xpath}
Bangle.http = (url,options)=>{
options = options||{};
if (!NRF.getSecurityStatus().connected)
return Promise.reject("Not connected to Bluetooth");
if (Bangle.httpRequest === undefined)
Bangle.httpRequest={};
if (options.id === undefined) {

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.15",
"version": "0.16",
"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

@ -12,3 +12,5 @@
0.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
0.11: Moved enhanced Anton clock to 'Anton Clock Plus' and stripped this clock back down to make it faster for new users (270ms -> 170ms)
Modified to avoid leaving functions defined when using setUI({remove:...})

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,9 +1,8 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.10",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"version": "0.11",
"description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
@ -12,8 +11,6 @@
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"antonclk.json"}]
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,15 @@
0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide
0.03: Clock now shows day of week under date.
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
when weekday name "Off": week #:<num>
when weekday name "On": weekday name is cut at 6th position and .#<week num> is added
0.06: fixes #1271 - wrong settings name
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
week is buffered until date or timezone changes
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
Modified to avoid leaving functions defined when using setUI({remove:...})

View File

@ -1,6 +1,6 @@
# Anton Clock - Large font digital watch with seconds and date
# Anton Clock Plus - Large font digital watch with seconds and date
Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
Anton Clock Plus uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
## Features
@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time.
## Usage
Install Anton clock through the Bangle.js app loader.
Configure it through the default Bangle.js configuration mechanism
* Install Anton Clock Plus through the Bangle.js app loader.
* Configure it through the default Bangle.js configuration mechanism
(Settings app, "Apps" menu, "Anton clock" submenu).
If you like it, make it your default watch face
* If you like it, make it your default watch face
(Settings app, "System" menu, "Clock" submenu, select "Anton clock").
## Configuration
Anton clock is configured by the standard settings mechanism of Bangle.js's operating system:
Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu.
Anton Clock is configured by the standard settings mechanism of Bangle.js's operating system:
Open the `Settings` app, then the `Apps` submenu and below it the `Anton Clock+` menu.
You configure Anton clock through several "on/off" switches in two menus.
### The main menu

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgf/AH4At/l/Aofgh4DB+EAj4REQoM/AgP4AoeACIoLCg4FB4AFDCIwLCgAROgYIB8EBAoUH/gVBCIxQBCKYHBCJp9DI4ICBLJYRCn4RQEYMOR5ARDIgIRMYQZZBgARGZwZBDCKQrCgEDR5AdBUIQRJDoLXFCJD7J/xrICIQFCn4RH/4LDAoTaCCI4Ar/LLDCBfypMkCgMkyV/CJOSCIOf5IRGFwOfCJNP//JnmT588z/+pM/BYIRCk4RC/88+f/n4RCngRCz1JCIf5/nzGoQRIHwXPCIPJI4f8CJHJGQJKCCI59LCI5ZCCJ/+v/kBoM/+V/HIJrHBYJWB/JKB5x9JEYP8AQKdBpwRL841Dp41KZoTxBHYTXBWY77PCKKhJ/4/CcgMkXoQAiA="))

238
apps/antonclkplus/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/antonclkplus/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,20 @@
{
"id": "antonclkplus",
"name": "Anton Clock Plus",
"shortName": "Anton Clock+",
"version": "0.10",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"antonclkplus.app.js","url":"app.js"},
{"name":"antonclkplus.settings.js","url":"settings.js"},
{"name":"antonclkplus.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"antonclkplus.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -2,3 +2,4 @@
0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget
0.03: Update to use Bangle.setUI instead of setWatch
0.04: Tell clock widgets to hide.
0.05: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -834,7 +834,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading))
compass_heading = "---";
else
compass_heading = 360 - Math.round(m.heading);
compass_heading = Math.round(m.heading);
// g.setColor("#000000");
// g.fillRect(160, 10, 160, 20);
g.setColor(display_colour);

View File

@ -1,7 +1,7 @@
{
"id": "astral",
"name": "Astral Clock",
"version": "0.04",
"version": "0.05",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",

View File

@ -1,3 +1,4 @@
0.01: Display pressure as number and hand
0.02: Use theme color
0.03: workaround for some firmwares that return 'undefined' for first call to barometer
0.04: Update every second, go back with short button press

View File

@ -59,6 +59,7 @@ function drawTicks(){
function drawScaleLabels(){
g.setColor(g.theme.fg);
g.setFont("Vector",12);
g.setFontAlign(-1,-1);
let label = MIN;
for (let i=0;i <= NUMBER_OF_LABELS; i++){
@ -103,22 +104,29 @@ function drawIcons() {
}
g.setBgColor(g.theme.bg);
g.clear();
drawTicks();
drawScaleLabels();
drawIcons();
try {
function baroHandler(data) {
if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429
setTimeout(() => Bangle.getPressure().then(baroHandler), 500);
else
g.clear();
drawTicks();
drawScaleLabels();
drawIcons();
if (data!==undefined) {
drawHand(Math.round(data.pressure));
}
}
Bangle.getPressure().then(baroHandler);
setInterval(() => Bangle.getPressure().then(baroHandler), 1000);
} catch(e) {
print(e.message);
print("barometer not supporter, show a demo value");
if (e !== undefined) {
print(e.message);
}
print("barometer not supported, show a demo value");
drawHand(MIN);
}
Bangle.setUI({
mode : "custom",
back : function() {load();}
});

View File

@ -1,7 +1,7 @@
{ "id": "barometer",
"name": "Barometer",
"shortName":"Barometer",
"version":"0.03",
"version":"0.04",
"description": "A simple barometer that displays the current air pressure",
"icon": "barometer.png",
"tags": "tool,outdoors",

3
apps/beer/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Added adjustment for Bangle.js magnetometer heading fix
Bangle.js 2 compatibility

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))
require("heatshrink").decompress(atob("mEw4cA///wH9/++1P+u3//3/qv/gv+KHkJkmABxcBBwNJkmQCJYOByQCCCBUCCItJkARQkgQHggLBku25IRDJQ4LCtu27Mt2RKJCInbAQIRLpYROglt24OB6wSC7dwLQ4LB9u2EgfbsARJ8u2mwRO+u3CNJtHCJFpCINALJoRCpCiGBoMSdQcpegIRGyaPB+QRDkARIyQRBc4YRKyet23iCJxHB6QRBzOJCJ+dCJY1CpfMGphrCp2YNZlL54CBEZgLBAQoRBiTFFCNMvmQRPndiEcJHEyQQECJMpAYIRQyARQwAROI4IAGB4wCBNAoRmhIRHCA4A/AAo"))

View File

@ -127,6 +127,8 @@
var img_nofix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF8u02gziGBoyhQ5gwDGRozRGCQydGCgybGCwyZC5gAaGPQwnGRAwpGQ4xwGFYyFDKsrlYxYDCsBmUyg4yXLyUsFwMyq1WAgUsNCRjUmVXroAEq8yMbcllkskwCEkplDmQwDq0sC54xEHQ9RqQAGqIwCFgOBAASYBSgMBltRAA0sgJsOGJeBxAAGwMrgIXIloxOJYNSvl8CwIDCqMBlYxNC4wxQDIOCwVYDIIDBGJ9YwV8rADBwRJCSqAVCAYaVMC4oxCPYYxQSo4xMSpIxPY4T5HY54XIMbIxKgwXKfKjhEllWGJNWlgXJGLNXruCGI+CrtXGKP+GJB9HMZ6VO/wxJcI8lfJclfKAxKfJEAGJIXLGKSvBWYQZCMZbfEqTHBGJYyFfIo1DGJ4tDGJQwCGJB9IMZyVNGIYyEfJQxPfJgwEMgoZJgAxMltRAA0tGJQyEksslkmAQklGINXxDTBFwIDCq8rC4YACC4gwJMowAJldWAAwwBABowIGJ4AYGJIymGBQylGBgyjGBwyhGCAzeF6YycGCwzYF7IzVF7o1PDqYA=="));
var img_fix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF94zaDYkq6wAOlQyYJo2A63VAAIoC2m0GI16My5/H5/V64ABGQIwBGQ+rTKwWHkhiBGIYwDGQ3VZioVIqoiBGAJhEGRFPGSYTIYwQxCGA4yFqodJGKeqSgQwJGQmkGKQSJfAYwLGQfPDxQwRgHVfAi/EAA4xLGQwRLYwb5BABoxQCBcA43G5wABAgIAMEBgxQ0QxB54xB5gAG4xgBBYOiGJ4PMGInPGIhcCGIt4EJoxPvHM5oxBGAnO6xrCGoXMqgxdpwxD5qQFL4QADlQxdgAhBGILIDMYoADEBwwPgCHBfQzHDAAb4NACTIIAA74OACLIIMo7GOACQoBZAoHBHQPNA4QwggGiZBA5B54HBY0DIKMYtUGMMqFYLIGY4jGhZAr6FAAYwiZAgxIY0TIFfQgADvAfR/zISGJTGR/wxRkj6CGJBiSGKL6DGP4xOGSKVDGAwxRGAQxU5oxcGR75DGJEkGCYxPlXM5vPGA/MlQxUGR1OGIL4I5lOGCgyOqgxBShHMqgwVGJt4GJd4GKwyMvHG5vGABAxMGBQyM1mtABWsGC4yLGBYABGDAyKGKwwQGZKVUF6b/OABowWGbAvZGaovdGp4dTA"));
var W = g.getWidth(), H = g.getHeight();
// https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js
function project(latlong) {
var d = Math.PI / 180,
@ -170,32 +172,30 @@ Bangle.on('GPS', function(f) {
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
var headingrad = m.heading*Math.PI/180; // in radians
var headingrad = (360-m.heading)*Math.PI/180; // in radians
if (!isFinite(headingrad)) headingrad=0;
if (nearest)
g.drawImage(img_fix,120,120,{
g.drawImage(img_fix,W/2,H/2,{
rotate: (Math.PI/2)+headingrad-nearestangle,
scale:3,
});
else
g.drawImage(img_nofix,120,120,{
g.drawImage(img_nofix,W/2,H/2,{
rotate: headingrad,
scale:2,
});
g.clearRect(60,0,180,24);
g.setFontAlign(0,0);
g.setFont("6x8");
g.clearRect(0,0,W,24).setFontAlign(0,0).setFont("6x8");
if (fix.fix) {
g.drawString(nearest ? nearest.name : "---",120,4);
g.drawString(nearest ? nearest.name : "---",W/2,4);
g.setFont("6x8",2);
g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16);
g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",W/2,16);
} else {
g.drawString(fix.satellites+" satellites",120,4);
g.drawString(fix.satellites+" satellites",W/2,4);
}
});
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
g.clear();`;
g.setColor("#fff").setBgColor("#000").clear();`;
sendCustomizedApp({
storage:[

View File

@ -1,11 +1,11 @@
{
"id": "beer",
"name": "Beer Compass",
"version": "0.01",
"version": "0.02",
"description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"storage": [
{"name":"beer.app.js"},

View File

@ -52,3 +52,10 @@
0.46: Fix no clock found error on Bangle.js 2
0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed)
0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks)
0.49: Store first found clock as a setting to speed up further boots
0.50: Allow setting of screen rotation
Remove support for 2v11 and earlier firmware
0.51: Remove patches for 2v10 firmware (BEEPSET and setUI)
Add patch to ensure that compass heading is corrected on pre-2v15.68 firmware
Ensure clock is only fast-loaded if it doesn't contain widgets
0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass

View File

@ -1,8 +1,13 @@
// This runs after a 'fresh' boot
var clockApp=(require("Storage").readJSON("setting.json",1)||{}).clock;
if (clockApp) clockApp = require("Storage").read(clockApp);
if (!clockApp) {
clockApp = require("Storage").list(/\.info$/)
var s = require("Storage").readJSON("setting.json",1)||{};
/* If were being called from JS code in order to load the clock quickly (eg from a launcher)
and the clock in question doesn't have widgets, force a normal 'load' as this will then
reset everything and remove the widgets. */
if (global.__FILE__ && !s.clockHasWidgets) {load();throw "Clock has no widgets, can't fast load";}
// Otherwise continue to try and load the clock
var _clkApp = require("Storage").read(s.clock);
if (!_clkApp) {
_clkApp = require("Storage").list(/\.info$/)
.map(file => {
const app = require("Storage").readJSON(file,1);
if (app && app.type == "clock") {
@ -11,9 +16,14 @@ if (!clockApp) {
})
.filter(x=>x)
.sort((a, b) => a.sortorder - b.sortorder)[0];
if (clockApp)
clockApp = require("Storage").read(clockApp.src);
if (_clkApp){
s.clock = _clkApp.src;
_clkApp = require("Storage").read(_clkApp.src);
s.clockHasWidgets = _clkApp.includes("Bangle.loadWidgets");
require("Storage").writeJSON("setting.json", s);
}
}
if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
eval(clockApp);
delete clockApp;
delete s;
if (!_clkApp) _clkApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
eval(_clkApp);
delete _clkApp;

View File

@ -62,23 +62,6 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
boot += `E.setTimeZone(${s.timezone});`;
// Set vibrate, beep, etc IF on older firmwares
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n`
if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n`
else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};\n`;
}
// Draw out of memory errors onto the screen
boot += `E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
@ -92,76 +75,14 @@ if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
// Pre-2v10 firmwares without a theme/setUI
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.theme) {
boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`;
}
try {
Bangle.setUI({}); // In 2v12.xx we added the option for mode to be an object - for 2v12 and earlier, add a fix if it fails with an object supplied
} catch(e) {
boot += `Bangle._setUI = Bangle.setUI;
Bangle.setUI=function(mode, cb) {
if (Bangle.uiRemove) {
Bangle.uiRemove();
delete Bangle.uiRemove;
}
if ("object"==typeof mode) {
// TODO: handle mode.back?
mode = mode.mode;
}
Bangle._setUI(mode, cb);
};\n`;
}
delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill
boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);b<c&&(c=b);g.setColor(g.theme.fg);for(var d=0;d<l;d++){var m=d+c;if(0>m||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(),
k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`;
}
delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.imageMetrics=function(src) {
if (src[0]) return {width:src[0],height:src[1]};
else if ('object'==typeof src) return {
width:("width" in src) ? src.width : src.getWidth(),
height:("height" in src) ? src.height : src.getHeight()};
var im = E.toString(src);
return {width:im.charCodeAt(0), height:im.charCodeAt(1)};
};\n`;
}
delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.stringMetrics=function(txt) {
txt = txt.toString().split("\\n");
return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length};
};\n`;
}
delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.wrapString=function(str, maxWidth) {
var lines = [];
for (var unwrappedLine of str.split("\\n")) {
var words = unwrappedLine.split(" ");
var line = words.shift();
for (var word of words) {
if (g.stringWidth(line + " " + word) > maxWidth) {
lines.push(line);
line = word;
} else {
line += " " + word;
}
}
lines.push(line);
}
return lines;
};\n`;
}
delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight());
(lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`;
}
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES
// 2v15.68 and before had compass heading inverted.
if (process.version.replace("v","")<215.68)
boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
// ================================================== BOOT.JS
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.48",
"version": "0.52",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",

View File

@ -29,3 +29,8 @@
Use default boolean formatter in custom menu and directly apply config if useful
Allow recording unmodified internal HR
Better connection retry handling
0.13: Less time used during boot if disabled
0.14: Allow bonding (Debug menu)
Prevent mixing of BT and internal HRM events if both are enabled
Always use a grace period (default 0 ms) to decouple some connection steps
Device not found errors now utilize increasing timeouts

View File

@ -1,633 +1 @@
(function() {
var settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
if (global.showStatusInfo)
showStatusInfo(text);
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
if (param) logline += ": " + JSON.stringify(param);
print(logline);
}
};
log("Settings: ", settings);
if (settings.enabled){
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
};
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
};
var addNotificationHandler = function(characteristic) {
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
};
var writeCache = function(cache) {
var oldCache = getCache();
if (oldCache !== cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
};
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
for (var c of characteristics){
//"handle_value":16,"handle_decl":15
log("Saving handle " + c.handle_value + " for characteristic: ", c);
cache.characteristics[c.uuid] = {
"handle": c.handle_value,
"uuid": c.uuid,
"notify": c.properties.notify,
"read": c.properties.read
};
}
writeCache(cache);
};
var characteristicsFromCache = function(device) {
var service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
var restored = [];
for (var c in cache.characteristics){
var cached = cache.characteristics[c];
var r = new BluetoothRemoteGATTCharacteristic();
log("Restoring characteristic ", cached);
r.handle_value = cached.handle;
r.uuid = cached.uuid;
r.properties = {};
r.properties.notify = cached.notify;
r.properties.read = cached.read;
r.service = service;
addNotificationHandler(r);
log("Restored characteristic: ", r);
restored.push(r);
}
return restored;
};
log("Start");
var lastReceivedData={
};
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var bpmTimeout;
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
active: false,
handler: function (dv){
var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
if (supportedCharacteristics["0x2a37"].active) stopFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
var sensorContact;
if (flags & 2){
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
idx += 2;
}
var interval;
if (flags & 16) {
interval = [];
var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2;
}
}
var location;
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
location = lastReceivedData["0x180d"]["0x2a38"];
}
var battery;
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace){
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM", repEvent);
Bangle.emit("HRM_int", repEvent);
}
var newEvent = {
bpm: bpm
};
if (location) newEvent.location = location;
if (interval) newEvent.rr = interval;
if (energyExpended) newEvent.energy = energyExpended;
if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact;
log("Emitting BTHRM", newEvent);
Bangle.emit("BTHRM", newEvent);
}
},
"0x2a38": {
//Body sensor location
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
};
var device;
var gatt;
var characteristics = [];
var blockInit = false;
var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var connectSettings = {
minInterval: 7.5,
maxInterval: 1500
};
var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
log("Done waiting for " + timeout);
resolve();
}, timeout);
});
};
if (settings.enabled){
Bangle.isBTHRMActive = function (){
return supportedCharacteristics["0x2a37"].active;
};
Bangle.isBTHRMOn = function(){
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
};
Bangle.isBTHRMConnected = function(){
return gatt && gatt.connected;
};
}
if (settings.replace){
Bangle.origIsHRMOn = Bangle.isHRMOn;
Bangle.isHRMOn = function() {
if (settings.enabled && !settings.replace){
return Bangle.origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
};
}
var clearRetryTimeout = function(resetTime) {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
if (resetTime) {
log("Resetting retry time");
retryTime = initialRetryTime;
}
};
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
var clampedTime = retryTime < 100 ? 100 : retryTime;
log("Set timeout for retry as " + clampedTime);
clearRetryTimeout();
currentRetryTimeout = setTimeout(() => {
log("Retrying");
currentRetryTimeout = undefined;
initBt();
}, clampedTime);
retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
};
var buzzing = false;
var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT", gatt);
log("Characteristics", characteristics);
clearRetryTimeout(reason != "Connection Timeout");
supportedCharacteristics["0x2a37"].active = false;
startFallback();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
buzzing = true;
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
}
if (Bangle.isBTHRMOn()){
retry();
}
};
var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise", newCharacteristic);
var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){
result = result.then(()=>{
log("Reading data", newCharacteristic);
return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data);
}
});
});
}
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
}
return startPromise;
});
}
return result.then(()=>log("Handled characteristic", newCharacteristic));
};
var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
};
var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promis ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
if (!supportedCharacteristics[c.uuid]) continue;
log("Supporting characteristic", c);
characteristics.push(c);
if (c.properties.notify){
addNotificationHandler(c);
}
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
};
var createServicePromise = function(service) {
log("Create service promise", service);
var result = Promise.resolve();
result = result.then(()=>{
log("Handling service" + service.uuid);
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
};
var attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
};
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
return;
}
blockInit = true;
var promise;
var filters;
if (!device){
if (settings.btid){
log("Configured device id", settings.btid);
filters = [{ id: settings.btid }];
} else {
return;
}
log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters, active: true });
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
}
promise = promise.then((d)=>{
log("Got device", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
log("Reuse device", device);
}
promise = promise.then(()=>{
if (gatt){
log("Reuse GATT", gatt);
} else {
log("GATT is new", gatt);
characteristics = [];
var cachedId = getCache().id;
if (device.id !== cachedId){
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
return Promise.resolve(gatt);
});
promise = promise.then((gatt)=>{
if (!gatt.connected){
log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
if (settings.gracePeriodConnect > 0){
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
}
return connectPromise;
} else {
return Promise.resolve();
}
});
/* promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});*/
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache(device);
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services", services);
var result = Promise.resolve();
for (var service of services){
if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
if (settings.gracePeriodService > 0) {
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
}
return result;
});
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
return characteristicsPromise;
});
return promise.then(()=>{
log("Connection established, waiting for notifications");
characteristicsToCache(characteristics);
clearRetryTimeout(true);
}).catch((e) => {
characteristics = [];
log("Error:", e);
onDisconnect(e);
});
};
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on
log("Power off for " + app);
clearRetryTimeout(true);
if (gatt) {
if (gatt.connected){
log("Disconnect with gatt", gatt);
try{
gatt.disconnect().then(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect promise", e);
});
} catch (e){
log("Error during disconnect attempt", e);
}
}
}
}
};
if (settings.replace){
Bangle.on("HRM", (e) => {
e.modified = true;
Bangle.emit("HRM_int", e);
});
Bangle.origOn = Bangle.on;
Bangle.on = function(name, callback) {
if (name == "HRM") {
Bangle.origOn("HRM_int", callback);
} else {
Bangle.origOn(name, callback);
}
};
Bangle.origRemoveListener = Bangle.removeListener;
Bangle.removeListener = function(name, callback) {
if (name == "HRM") {
Bangle.origRemoveListener("HRM_int", callback);
} else {
Bangle.origRemoveListener(name, callback);
}
};
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){
Bangle.setBTHRMPower(isOn, app);
}
if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app);
}
};
}
var fallbackActive = false;
var inSwitch = false;
var stopFallback = function(){
if (fallbackActive){
Bangle.origSetHRMPower(0, "bthrm_fallback");
fallbackActive = false;
log("Fallback to HRM disabled");
}
};
var startFallback = function(){
if (!fallbackActive && settings.allowFallback) {
fallbackActive = true;
Bangle.origSetHRMPower(1, "bthrm_fallback");
log("Fallback to HRM enabled");
}
};
var switchFallback = function() {
log("Check falling back to HRM");
if (!inSwitch){
inSwitch = true;
if (Bangle.isBTHRMActive()){
stopFallback();
} else {
startFallback();
}
}
inSwitch = false;
};
if (settings.replace){
log("Replace HRM event");
if (Bangle._PWR && Bangle._PWR.HRM){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
Bangle.origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break;
}
}
}
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}
})();
if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable();

View File

@ -16,5 +16,6 @@
"gracePeriodNotification": 0,
"gracePeriodConnect": 0,
"gracePeriodService": 0,
"gracePeriodRequest": 0
"gracePeriodRequest": 0,
"bonding": false
}

634
apps/bthrm/lib.js Normal file
View File

@ -0,0 +1,634 @@
exports.enable = () => {
var settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
if (global.showStatusInfo)
showStatusInfo(text);
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
if (param) logline += ": " + JSON.stringify(param);
print(logline);
}
};
log("Settings: ", settings);
if (settings.enabled){
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
};
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
};
var addNotificationHandler = function(characteristic) {
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
};
var writeCache = function(cache) {
var oldCache = getCache();
if (oldCache !== cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
};
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
for (var c of characteristics){
//"handle_value":16,"handle_decl":15
log("Saving handle " + c.handle_value + " for characteristic: ", c);
cache.characteristics[c.uuid] = {
"handle": c.handle_value,
"uuid": c.uuid,
"notify": c.properties.notify,
"read": c.properties.read
};
}
writeCache(cache);
};
var characteristicsFromCache = function(device) {
var service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
var restored = [];
for (var c in cache.characteristics){
var cached = cache.characteristics[c];
var r = new BluetoothRemoteGATTCharacteristic();
log("Restoring characteristic ", cached);
r.handle_value = cached.handle;
r.uuid = cached.uuid;
r.properties = {};
r.properties.notify = cached.notify;
r.properties.read = cached.read;
r.service = service;
addNotificationHandler(r);
log("Restored characteristic: ", r);
restored.push(r);
}
return restored;
};
log("Start");
var lastReceivedData={
};
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var bpmTimeout;
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
active: false,
handler: function (dv){
var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
if (supportedCharacteristics["0x2a37"].active) stopFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
bpmTimeout = undefined;
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
var sensorContact;
if (flags & 2){
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
idx += 2;
}
var interval;
if (flags & 16) {
interval = [];
var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2;
}
}
var location;
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
location = lastReceivedData["0x180d"]["0x2a38"];
}
var battery;
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace){
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting aggregated HRM", repEvent);
Bangle.emit("HRM_R", repEvent);
}
var newEvent = {
bpm: bpm
};
if (location) newEvent.location = location;
if (interval) newEvent.rr = interval;
if (energyExpended) newEvent.energy = energyExpended;
if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact;
log("Emitting BTHRM", newEvent);
Bangle.emit("BTHRM", newEvent);
}
},
"0x2a38": {
//Body sensor location
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
};
var device;
var gatt;
var characteristics = [];
var blockInit = false;
var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var connectSettings = {
minInterval: 7.5,
maxInterval: 1500
};
var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
log("Done waiting for " + timeout);
resolve();
}, timeout);
});
};
if (settings.enabled){
Bangle.isBTHRMActive = function (){
return supportedCharacteristics["0x2a37"].active;
};
Bangle.isBTHRMOn = function(){
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
};
Bangle.isBTHRMConnected = function(){
return gatt && gatt.connected;
};
}
if (settings.replace){
Bangle.origIsHRMOn = Bangle.isHRMOn;
Bangle.isHRMOn = function() {
if (settings.enabled && !settings.replace){
return Bangle.origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
};
}
var clearRetryTimeout = function(resetTime) {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
if (resetTime) {
log("Resetting retry time");
retryTime = initialRetryTime;
}
};
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
var clampedTime = retryTime < 100 ? 100 : retryTime;
log("Set timeout for retry as " + clampedTime);
clearRetryTimeout();
currentRetryTimeout = setTimeout(() => {
log("Retrying");
currentRetryTimeout = undefined;
initBt();
}, clampedTime);
retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
};
var buzzing = false;
var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT", gatt);
log("Characteristics", characteristics);
var retryTimeResetNeeded = true;
retryTimeResetNeeded &= reason != "Connection Timeout";
retryTimeResetNeeded &= reason != "No device found matching filters";
clearRetryTimeout(retryTimeResetNeeded);
supportedCharacteristics["0x2a37"].active = false;
startFallback();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
buzzing = true;
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
}
if (Bangle.isBTHRMOn()){
retry();
}
};
var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise", newCharacteristic);
var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){
result = result.then(()=>{
log("Reading data", newCharacteristic);
return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data);
}
});
});
}
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
return startPromise;
});
}
return result.then(()=>log("Handled characteristic", newCharacteristic));
};
var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
};
var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promis ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
if (!supportedCharacteristics[c.uuid]) continue;
log("Supporting characteristic", c);
characteristics.push(c);
if (c.properties.notify){
addNotificationHandler(c);
}
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
};
var createServicePromise = function(service) {
log("Create service promise", service);
var result = Promise.resolve();
result = result.then(()=>{
log("Handling service" + service.uuid);
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
};
var attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
};
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
return;
}
blockInit = true;
var promise;
var filters;
if (!device){
if (settings.btid){
log("Configured device id", settings.btid);
filters = [{ id: settings.btid }];
} else {
return;
}
log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters, active: true });
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
}
promise = promise.then((d)=>{
log("Got device", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
log("Reuse device", device);
}
promise = promise.then(()=>{
if (gatt){
log("Reuse GATT", gatt);
} else {
log("GATT is new", gatt);
characteristics = [];
var cachedId = getCache().id;
if (device.id !== cachedId){
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
return Promise.resolve(gatt);
});
promise = promise.then((gatt)=>{
if (!gatt.connected){
log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
return connectPromise;
} else {
return Promise.resolve();
}
});
if (settings.bonding){
promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});
}
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache(device);
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services", services);
var result = Promise.resolve();
for (var service of services){
if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
return result;
});
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
return characteristicsPromise;
});
return promise.then(()=>{
log("Connection established, waiting for notifications");
characteristicsToCache(characteristics);
clearRetryTimeout(true);
}).catch((e) => {
characteristics = [];
log("Error:", e);
onDisconnect(e);
});
};
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on
log("Power off for " + app);
clearRetryTimeout(true);
if (gatt) {
if (gatt.connected){
log("Disconnect with gatt", gatt);
try{
gatt.disconnect().then(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect promise", e);
});
} catch (e){
log("Error during disconnect attempt", e);
}
}
}
}
};
if (settings.replace){
// register a listener for original HRM events and emit as HRM_int
Bangle.on("HRM", (e) => {
e.modified = true;
Bangle.emit("HRM_int", e);
if (fallbackActive){
// if fallback to internal HRM is active, emit as HRM_R to which everyone listens
Bangle.emit("HRM_R", e);
}
});
// force all apps wanting to listen to HRM to actually get events for HRM_R
Bangle.on = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.on);
Bangle.removeListener = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.removeListener);
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){
Bangle.setBTHRMPower(isOn, app);
}
if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app);
}
};
}
var fallbackActive = false;
var inSwitch = false;
var stopFallback = function(){
if (fallbackActive){
Bangle.origSetHRMPower(0, "bthrm_fallback");
fallbackActive = false;
log("Fallback to HRM disabled");
}
};
var startFallback = function(){
if (!fallbackActive && settings.allowFallback) {
fallbackActive = true;
Bangle.origSetHRMPower(1, "bthrm_fallback");
log("Fallback to HRM enabled");
}
};
var switchFallback = function() {
log("Check falling back to HRM");
if (!inSwitch){
inSwitch = true;
if (Bangle.isBTHRMActive()){
stopFallback();
} else {
startFallback();
}
}
inSwitch = false;
};
if (settings.replace){
log("Replace HRM event");
if (Bangle._PWR && Bangle._PWR.HRM){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
Bangle.origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break;
}
}
}
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}
};

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.12",
"version": "0.14",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
@ -15,6 +15,7 @@
{"name":"bthrm.0.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"},
{"name":"bthrm","url":"lib.js"},
{"name":"bthrm.default.json","url":"default.json"}
]
}

View File

@ -96,6 +96,12 @@
writeSettings("debuglog",v);
}
},
'Use bonding': {
value: !!settings.bonding,
onchange: v => {
writeSettings("bonding",v);
}
},
'Grace periods': function() { E.showMenu(submenu_grace); }
};

View File

@ -1,3 +1,4 @@
0.01: Initial version
0.02: More compact rendering & app icon
0.03: Tell clock widgets to hide.
0.04: Improve current time readability in light theme.

View File

@ -68,7 +68,7 @@ function drawEvent(event, y) {
var curEventHeight = 0;
function drawCurrentEvents(y) {
g.setColor("#0ff");
g.setColor(g.theme.dark ? "#0ff" : "#0000ff");
g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight);
curEventHeight = y;

View File

@ -2,7 +2,7 @@
"id": "calclock",
"name": "Calendar Clock",
"shortName": "CalClock",
"version": "0.03",
"version": "0.04",
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png",
"type": "clock",

View File

@ -3,3 +3,5 @@
0.03: Support for different screen sizes and touchscreen
0.04: Display current operation on LHS
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
0.06: Bangle.js 2: Exit with a short press of the physical button
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen

View File

@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus.
## Controls
Bangle.js 1
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- SELECT: BTN2
Bangle.js 2
- Swipes to change visible buttons
- Click physical button to exit
- Press upper left corner of screen to exit (where the red back button would be)
## Creator
<https://twitter.com/fredericrous>
## Contributors
[thyttan](https://github.com/thyttan)

View File

@ -3,6 +3,8 @@
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*
* Contributors: thyttan https://github.com/thyttan
*/
g.clear();
@ -402,43 +404,42 @@ if (process.env.HWVERSION==1) {
swipeEnabled = false;
drawGlobal();
} else { // touchscreen?
selected = "NONE";
selected = "NONE";
swipeEnabled = true;
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
drawNumbers();
Bangle.on('touch',(n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] &&
e.x<r[2] && e.y<r[3]) {
//print("Press "+key);
buttonPress(""+key);
Bangle.setUI({
mode : 'custom',
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
touch : (n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
//print("Press "+key);
buttonPress(""+key);
}
}
}
});
var lastX = 0, lastY = 0;
Bangle.on('drag', (e) => {
if (!e.b) {
if (lastX > 50) { // right
},
swipe : (LR, UD) => {
if (LR == 1) { // right
drawSpecials();
} else if (lastX < -50) { // left
}
if (LR == -1) { // left
drawOperators();
} else if (lastY > 50) { // down
drawNumbers();
} else if (lastY < -50) { // up
}
if (UD == 1) { // down
drawNumbers();
}
if (UD == -1) { // up
drawNumbers();
}
lastX = 0;
lastY = 0;
} else {
lastX = lastX + e.dx;
lastY = lastY + e.dy;
}
});
}
displayOutput(0);

View File

@ -2,7 +2,7 @@
"id": "calculator",
"name": "Calculator",
"shortName": "Calculator",
"version": "0.05",
"version": "0.07",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}],

View File

@ -9,3 +9,4 @@
read start of week from system settings
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

View File

@ -226,15 +226,14 @@ drawCalendar(date);
clearWatch();
Bangle.on("touch", area => {
const month = date.getMonth();
let prevMonth;
if (area == 1) {
let prevMonth = month > 0 ? month - 1 : 11;
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
date.setMonth(prevMonth);
} else {
let prevMonth = month < 11 ? month + 1 : 0;
if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1);
date.setMonth(month + 1);
let nextMonth = month < 11 ? month + 1 : 0;
if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1);
date.setMonth(nextMonth);
}
drawCalendar(date);
});

View File

@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.10",
"version": "0.11",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],

View File

@ -26,3 +26,7 @@
0.12: Allow configuration of update interval
0.13: Load step goal from Bangle health app as fallback
Memory optimizations
0.14: Support to show big weather info
0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
0.16: Fix const error
Use widget_utils if available

View File

@ -9,10 +9,11 @@ It can show the following information (this can be configured):
* Steps distance
* Heart rate (automatically updates when screen is on and unlocked)
* Battery (including charging status and battery low warning)
* Weather (requires [weather app](https://banglejs.com/apps/#weather))
* Weather (requires [OWM weather provider](https://banglejs.com/apps/?id=owmweather))
* Humidity or wind speed as circle progress
* Temperature inside circle
* Condition as icon below circle
* Big weather icon next to clock
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
* Temperature, air pressure or altitude from internal pressure sensor
@ -27,6 +28,8 @@ The color of each circle can be configured. The following colors are available:
![Screenshot light theme](screenshot-light.png)
![Screenshot dark theme with four circles](screenshot-dark-4.png)
![Screenshot light theme with four circles](screenshot-light-4.png)
![Screenshot light theme with big weather enabled](screenshot-light-with-big-weather.png)
## Ideas
* Show compass heading
@ -35,4 +38,5 @@ The color of each circle can be configured. The following colors are available:
Marco ([myxor](https://github.com/myxor))
## Icons
Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0
Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from
[icons8](https://icons8.com/icon/set/weather/small--static--black)

View File

@ -1,5 +1,5 @@
const locale = require("locale");
const storage = require("Storage");
let locale = require("locale");
let storage = require("Storage");
Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) {
// Actual height 39 (40 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16));
@ -12,7 +12,7 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) {
return this;
};
const SETTINGS_FILE = "circlesclock.json";
let SETTINGS_FILE = "circlesclock.json";
let settings = Object.assign(
storage.readJSON("circlesclock.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {}
@ -22,13 +22,16 @@ let settings = Object.assign(
if (settings.stepGoal == undefined) {
let d = storage.readJSON("health.json", true) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined;
if (settings.stepGoal == undefined) {
if (settings.stepGoal == undefined) {
d = storage.readJSON("wpedom.json", true) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
}
let timerHrm;
let drawTimeout;
/*
* Read location from myLocation app
*/
@ -37,29 +40,30 @@ function getLocation() {
}
let location = getLocation();
const showWidgets = settings.showWidgets || false;
const circleCount = settings.circleCount || 3;
let showWidgets = settings.showWidgets || false;
let circleCount = settings.circleCount || 3;
let showBigWeather = settings.showBigWeather || false;
let hrtValue;
let now = Math.round(new Date().getTime() / 1000);
// layout values:
const colorFg = g.theme.dark ? '#fff' : '#000';
const colorBg = g.theme.dark ? '#000' : '#fff';
const colorGrey = '#808080';
const colorRed = '#ff0000';
const colorGreen = '#008000';
const colorBlue = '#0000ff';
const colorYellow = '#ffff00';
const widgetOffset = showWidgets ? 24 : 0;
const dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
const h = g.getHeight() - widgetOffset;
const w = g.getWidth();
const hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
let colorFg = g.theme.dark ? '#fff' : '#000';
let colorBg = g.theme.dark ? '#000' : '#fff';
let colorGrey = '#808080';
let colorRed = '#ff0000';
let colorGreen = '#008000';
let colorBlue = '#0000ff';
let colorYellow = '#ffff00';
let widgetOffset = showWidgets ? 24 : 0;
let dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
let h = g.getHeight() - widgetOffset;
let w = g.getWidth();
let hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
let h1 = Math.round(1 * h / 5 - hOffset);
let h2 = Math.round(3 * h / 5 - hOffset);
let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
/*
* circle x positions
@ -73,21 +77,22 @@ const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
* | (1) (2) (3) (4) |
* => circles start at 1,3,5,7 / 8
*/
const parts = circleCount * 2;
const circlePosX = [
let parts = circleCount * 2;
let circlePosX = [
Math.round(1 * w / parts), // circle1
Math.round(3 * w / parts), // circle2
Math.round(5 * w / parts), // circle3
Math.round(7 * w / parts), // circle4
];
const radiusOuter = circleCount == 3 ? 25 : 20;
const radiusInner = circleCount == 3 ? 20 : 15;
const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
const iconOffset = circleCount == 3 ? 6 : 8;
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
let radiusOuter = circleCount == 3 ? 25 : 20;
let radiusInner = circleCount == 3 ? 20 : 15;
let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
let iconOffset = circleCount == 3 ? 6 : 8;
let defaultCircleTypes = ["steps", "hr", "battery", "weather"];
function hideWidgets() {
/*
@ -105,9 +110,16 @@ function hideWidgets() {
function draw() {
g.clear(true);
let widgetUtils;
try {
widgetUtils = require("widget_utils");
} catch (e) {
}
if (!showWidgets) {
hideWidgets();
if (widgetUtils) widgetUtils.hide(); else hideWidgets();
} else {
if (widgetUtils) widgetUtils.show();
Bangle.drawWidgets();
}
@ -116,27 +128,53 @@ function draw() {
// time
g.setFontRobotoRegular50NumericOnly();
g.setFontAlign(0, -1);
g.setColor(colorFg);
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
if (!showBigWeather) {
g.setFontAlign(0, -1);
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
}
else {
g.setFontAlign(-1, -1);
g.drawString(locale.time(new Date(), 1), 2, h1 + 6);
}
now = Math.round(new Date().getTime() / 1000);
// date & dow
g.setFontRobotoRegular21();
g.setFontAlign(0, 0);
g.drawString(locale.date(new Date()), w / 2, h2);
g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
if (!showBigWeather) {
g.setFontAlign(0, 0);
g.drawString(locale.date(new Date()), w / 2, h2);
g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
} else {
g.setFontAlign(-1, 0);
g.drawString(locale.date(new Date()), 2, h2);
g.drawString(locale.dow(new Date()), 2, h2 + dowOffset, 1);
}
// weather
if (showBigWeather) {
let weather = getWeather();
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
g.setFontAlign(1, 0);
if (tempString) g.drawString(tempString, w, h2);
let code = weather ? weather.code : -1;
let icon = getWeatherIconByCode(code, true);
if (icon) g.drawImage(icon, w - 48, h1, {scale:0.75});
}
drawCircle(1);
drawCircle(2);
drawCircle(3);
if (circleCount >= 4) drawCircle(4);
queueDraw();
}
function drawCircle(index) {
let type = settings['circle' + index];
if (!type) type = defaultCircleTypes[index - 1];
const w = getCircleXPosition(type);
let w = getCircleXPosition(type);
switch (type) {
case "steps":
@ -188,7 +226,7 @@ function getCirclePosition(type) {
return circlePositionsCache[type];
}
for (let i = 1; i <= circleCount; i++) {
const setting = settings['circle' + i];
let setting = settings['circle' + i];
if (setting == type) {
circlePositionsCache[type] = i - 1;
return i - 1;
@ -204,7 +242,7 @@ function getCirclePosition(type) {
}
function getCircleXPosition(type) {
const circlePos = getCirclePosition(type);
let circlePos = getCirclePosition(type);
if (circlePos != undefined) {
return circlePosX[circlePos];
}
@ -216,14 +254,14 @@ function isCircleEnabled(type) {
}
function getCircleColor(type) {
const pos = getCirclePosition(type);
const color = settings["circle" + (pos + 1) + "color"];
let pos = getCirclePosition(type);
let color = settings["circle" + (pos + 1) + "color"];
if (color && color != "") return color;
}
function getCircleIconColor(type, color, percent) {
const pos = getCirclePosition(type);
const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true;
let pos = getCirclePosition(type);
let colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true;
if (colorizeIcon) {
return getGradientColor(color, percent);
} else {
@ -234,18 +272,18 @@ function getCircleIconColor(type, color, percent) {
function getGradientColor(color, percent) {
if (isNaN(percent)) percent = 0;
if (percent > 1) percent = 1;
const colorList = [
let colorList = [
'#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000'
];
if (color == "fg") {
color = colorFg;
}
if (color == "green-red") {
const colorIndex = Math.round(colorList.length * percent);
let colorIndex = Math.round(colorList.length * percent);
return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00";
}
if (color == "red-green") {
const colorIndex = colorList.length - Math.round(colorList.length * percent);
let colorIndex = colorList.length - Math.round(colorList.length * percent);
return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000";
}
return color;
@ -268,14 +306,14 @@ function getImage(graphic, color) {
function drawSteps(w) {
if (!w) w = getCircleXPosition("steps");
const steps = getSteps();
let steps = getSteps();
drawCircleBackground(w);
const color = getCircleColor("steps");
let color = getCircleColor("steps");
let percent;
const stepGoal = settings.stepGoal;
let stepGoal = settings.stepGoal;
if (stepGoal > 0) {
percent = steps / stepGoal;
if (stepGoal < steps) percent = 1;
@ -291,16 +329,16 @@ function drawSteps(w) {
function drawStepsDistance(w) {
if (!w) w = getCircleXPosition("stepsDistance");
const steps = getSteps();
const stepDistance = settings.stepLength;
const stepsDistance = Math.round(steps * stepDistance);
let steps = getSteps();
let stepDistance = settings.stepLength;
let stepsDistance = Math.round(steps * stepDistance);
drawCircleBackground(w);
const color = getCircleColor("stepsDistance");
let color = getCircleColor("stepsDistance");
let percent;
const stepDistanceGoal = settings.stepDistanceGoal;
let stepDistanceGoal = settings.stepDistanceGoal;
if (stepDistanceGoal > 0) {
percent = stepsDistance / stepDistanceGoal;
if (stepDistanceGoal < stepsDistance) percent = 1;
@ -317,16 +355,16 @@ function drawStepsDistance(w) {
function drawHeartRate(w) {
if (!w) w = getCircleXPosition("hr");
const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
let heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
drawCircleBackground(w);
const color = getCircleColor("hr");
let color = getCircleColor("hr");
let percent;
if (hrtValue != undefined) {
const minHR = settings.minHR;
const maxHR = settings.maxHR;
let minHR = settings.minHR;
let maxHR = settings.maxHR;
percent = (hrtValue - minHR) / (maxHR - minHR);
if (isNaN(percent)) percent = 0;
drawGauge(w, h3, percent, color);
@ -341,9 +379,9 @@ function drawHeartRate(w) {
function drawBattery(w) {
if (!w) w = getCircleXPosition("battery");
const battery = E.getBattery();
let battery = E.getBattery();
const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
let powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
drawCircleBackground(w);
@ -371,18 +409,18 @@ function drawBattery(w) {
function drawWeather(w) {
if (!w) w = getCircleXPosition("weather");
const weather = getWeather();
const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
const code = weather ? weather.code : -1;
let weather = getWeather();
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
let code = weather ? weather.code : -1;
drawCircleBackground(w);
const color = getCircleColor("weather");
let color = getCircleColor("weather");
let percent;
const data = settings.weatherCircleData;
let data = settings.weatherCircleData;
switch (data) {
case "humidity":
const humidity = weather ? weather.hum : undefined;
let humidity = weather ? weather.hum : undefined;
if (humidity >= 0) {
percent = humidity / 100;
drawGauge(w, h3, percent, color);
@ -390,7 +428,7 @@ function drawWeather(w) {
break;
case "wind":
if (weather) {
const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
let wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
if (wind[1] >= 0) {
if (wind[2] == "kmh") {
wind[1] = windAsBeaufort(wind[1]);
@ -410,25 +448,24 @@ function drawWeather(w) {
writeCircleText(w, tempString ? tempString : "?");
if (code > 0) {
const icon = getWeatherIconByCode(code);
let icon = getWeatherIconByCode(code);
if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
} else {
g.drawString("?", w, h3 + radiusOuter);
}
}
function drawSunProgress(w) {
if (!w) w = getCircleXPosition("sunprogress");
const percent = getSunProgress();
let percent = getSunProgress();
// sunset icons:
const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
let sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
let sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
drawCircleBackground(w);
const color = getCircleColor("sunprogress");
let color = getCircleColor("sunprogress");
drawGauge(w, h3, percent, color);
@ -436,15 +473,15 @@ function drawSunProgress(w) {
let icon = sunSetDown;
let text = "?";
const times = getSunData();
let times = getSunData();
if (times != undefined) {
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
let sunRise = Math.round(times.sunrise.getTime() / 1000);
let sunSet = Math.round(times.sunset.getTime() / 1000);
if (!isDay()) {
// night
if (now > sunRise) {
// after sunRise
const upcomingSunRise = sunRise + 60 * 60 * 24;
let upcomingSunRise = sunRise + 60 * 60 * 24;
text = formatSeconds(upcomingSunRise - now);
} else {
text = formatSeconds(sunRise - now);
@ -468,12 +505,12 @@ function drawTemperature(w) {
getPressureValue("temperature").then((temperature) => {
drawCircleBackground(w);
const color = getCircleColor("temperature");
let color = getCircleColor("temperature");
let percent;
if (temperature) {
const min = -40;
const max = 85;
let min = -40;
let max = 85;
percent = (temperature - min) / (max - min);
drawGauge(w, h3, percent, color);
}
@ -482,7 +519,7 @@ function drawTemperature(w) {
if (temperature)
writeCircleText(w, locale.temp(temperature));
g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
});
@ -494,12 +531,12 @@ function drawPressure(w) {
getPressureValue("pressure").then((pressure) => {
drawCircleBackground(w);
const color = getCircleColor("pressure");
let color = getCircleColor("pressure");
let percent;
if (pressure && pressure > 0) {
const minPressure = 950;
const maxPressure = 1050;
let minPressure = 950;
let maxPressure = 1050;
percent = (pressure - minPressure) / (maxPressure - minPressure);
drawGauge(w, h3, percent, color);
}
@ -520,12 +557,12 @@ function drawAltitude(w) {
getPressureValue("altitude").then((altitude) => {
drawCircleBackground(w);
const color = getCircleColor("altitude");
let color = getCircleColor("altitude");
let percent;
if (altitude) {
const min = 0;
const max = 10000;
let min = 0;
let max = 10000;
percent = (altitude - min) / (max - min);
drawGauge(w, h3, percent, color);
}
@ -544,7 +581,7 @@ function drawAltitude(w) {
* wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
*/
function windAsBeaufort(windInKmh) {
const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
let beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
let l = 0;
while (l < beaufort.length && beaufort[l] < windInKmh) {
l++;
@ -557,20 +594,22 @@ function windAsBeaufort(windInKmh) {
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
function getWeatherIconByCode(code) {
const codeGroup = Math.round(code / 100);
function getWeatherIconByCode(code, big) {
let codeGroup = Math.round(code / 100);
if (big == undefined) big = false;
// weather icons:
const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
let weatherCloudy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
let weatherSunny = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAA//8AAA//8AD//wAAD//wAP//AAAP//AA//8AAA//8AAADwAADwAAAAAHgAAeAAAAAAeAAB4AAAAAB8AAPgAAAAADwAA8AAAAAAPgAHwAAAAAAfgB+AAAAAAD/gf8AAAAAAf///4AAAAAD7//3wAAAAAfD/8PgAAAAD4B+AfAAAAAfADwA+AAAAD4APAB8AAAAfAA8AD4AAAB4ADwAHgAAADAAPAAMAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
let weatherMoon = big ? atob("QEDBAP//wxgAAAYAAAAPAAAAD4AAAA8AAAAPwAAADwAAAA/gAAAPAAAAB/APAP/wAAAH+A8A//AAAAf4DwD/8AAAB/wPAP/wAAAH/gAADwAAAAe+AAAPAAAAB54AAA8AAAAHngAADwAAAAePAAAAAAAAD48OAAAAAAAPDw+AAAAAAB8PD8AAAAAAHg8P4AAAAAA+DwPwAAAAAHwfAfgAAAAB+D4A/AAA8AfwfgB/8AD//+D+AD/8AP//wfgAH/4Af/8B8AAf/wB//APgAAgfgD+AA8AAAAfAH8AHwAAAA+AP8B+AAAAB4Af//4AAAAHgA///gAAAAPAA//8AAAAA8AAf/wAAAADwAAAAAAAAAPAAAAAAAAAA8AcAAAAAAADwD+AAAAAAAfAfgAAAAAAB+D4AAAAAAAB8fAAAAAAAAD54AAAAAAAAHngAAAAAAAAe8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAPeAAAAAAAAB54AAAAAAAAHnwAAAAAAAA+PgAAAAAAAHwfgAAAAAAB+A/////////wB////////+AD////////wAB///////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
let weatherPartlyCloudy = big ? atob("QEDBAP//wxgAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAABwAPAA4AAAAHgA8AHgAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+BAAAAAP+B/wOAAAAAfgB+B8AAAAD4AD8H4AAAAPAA/wPwAAAB8AH+Af/AAAHgA/AA//AAAeAH4AB/+AADwAfAAH/8A//AD4AAIH4D/8AfAAAAHwP/wB4AAAAPg//AHgAAAAeAA8B+AAAAB4AB4fwAAAADwAHn/AAAAAPAAff8AAAAA8AA/8AAAAADwAD/AAAAAAPAEH4AAAAAA8A4PgAAAAAHwHgcAAAAAAfg+AwAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
let weatherRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4APAA8AAfg+AA8ADwAAfHwADwAPAAA+eAAPAA8AAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AADw8PDwAP8AAPDw8PAA/wAA8PDw8AD3gADw8PDwAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP/w8PDw8P8Af/Dw8PDw/gA/8PDw8PD8AAfw8PDw8OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
let weatherPartlyRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAA8AAfg+AAAADwAAfHwAAAAPAAA+eAAAAA8AAB54AAAADwAAHvAAAAAPAAAP8AAAAA8AAA/wAAAADwAAD/AAAA8PAAAP8AAADw8AAA/wAAAPDwAAD3gAAA8PAAAeeAAADw8AAB58AAAPDwAAPj4AAA8PAAB8H4AADw8AAfgP//8PDw//8Af//w8PD//gA///Dw8P/8AAf/8PDw/+AAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
let weatherSnowy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAADwAfg+AAAAAPAAfHwAAAAA8AA+eAAAAADwAB54AA8AD/8AHvAADwAP/wAP8AAPAA//AA/wAA8AD/8AD/AA//AA8AAP8AD/8ADwAA/wAP/wAPAAD3gA//AA8AAeeAAPAAAAAB58AA8AAAAAPj4ADwAAAAB8H4APAAAAAfgP/wAA8A//8Af/AADwD//gA/8AAPAP/8AAfwAA8A/+AAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
let weatherFoggy = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AD///AADwAAAP//8AAeAAAA///wAB4AAAD///AAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAB+AAAAAAAAAf8AAAAD///D/4AAAAP//8P3wAAAA///w8PgAAAD///CAfAAAAAAAAAA+AAAAAAAAAB8AAAAAAAAAD4AAAAAAAAAHgAAP//8PAAMAAA///w8AAAAAD///DwAAAAAP//8PAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
let weatherStormy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAD/AAHvAAAAAf4AAP8AAAAB/gAA/wAAAAP8AAD/AAAAA/gAAP8AAAAH+AAA/wAAAAfwAAD3gAAAD/AAAeeAAAAP4AAB58AAAB/AAAPj4AAAH8AAB8H4AAA/gAAfgP//+D//D/8Af//4f/4P/gA///B//B/8AAf/8P/8P+AAAAAAAPgAAAAAAAAB8AAAAAAAAAHwAAAAAAAAA+AAAAAAAAADwAAAAAAAAAfAAAAAAAAAB4AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
let unknown = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAH//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAA/AAD4AAAAAD4H4HwAAAAAfB/4PgAAAAB8P/weAAAAAHg//h4AAAAA+Hw+HwAAAAD4eB8PAAAAAP/wDw8AAAAA//APDwAAAAD/8A8PAAAAAH/gDw8AAAAAAAAfDwAAAAAAAH4fAAAAAAAB/B4AAAAAAAf4HgAAAAAAD/A+AAAAAAAfwHwAAAAAAD8A+AAAAAAAPgH4AAAAAAB8B/AAAAAAAHgf4AAAAAAA+H+AAAAAAADwfwAAAAAAAPD8AAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAADw8AAAAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : undefined;
switch (codeGroup) {
case 2:
return weatherStormy;
@ -607,16 +646,16 @@ function getWeatherIconByCode(code) {
return weatherCloudy;
}
default:
return undefined;
return unknown;
}
}
function isDay() {
const times = getSunData();
let times = getSunData();
if (times == undefined) return true;
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
let sunRise = Math.round(times.sunrise.getTime() / 1000);
let sunSet = Math.round(times.sunset.getTime() / 1000);
return (now > sunRise && now < sunSet);
}
@ -633,7 +672,7 @@ function formatSeconds(s) {
function getSunData() {
if (location != undefined && location.lat != undefined) {
const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
let SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
// get today's sunlight times for lat/lon
return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined;
}
@ -646,14 +685,14 @@ function getSunData() {
* Taken from rebble app and modified
*/
function getSunProgress() {
const times = getSunData();
let times = getSunData();
if (times == undefined) return 0;
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
let sunRise = Math.round(times.sunrise.getTime() / 1000);
let sunSet = Math.round(times.sunset.getTime() / 1000);
if (isDay()) {
// during day
const dayLength = sunSet - sunRise;
let dayLength = sunSet - sunRise;
if (now > sunRise) {
return (now - sunRise) / dayLength;
} else {
@ -662,10 +701,10 @@ function getSunProgress() {
} else {
// during night
if (now < sunRise) {
const prevSunSet = sunSet - 60 * 60 * 24;
let prevSunSet = sunSet - 60 * 60 * 24;
return 1 - (sunRise - now) / (sunRise - prevSunSet);
} else {
const upcomingSunRise = sunRise + 60 * 60 * 24;
let upcomingSunRise = sunRise + 60 * 60 * 24;
return (upcomingSunRise - now) / (upcomingSunRise - sunSet);
}
}
@ -700,16 +739,16 @@ function radians(a) {
* This draws the actual gauge consisting out of lots of little filled circles
*/
function drawGauge(cx, cy, percent, color) {
const offset = 15;
const end = 360 - offset;
const radius = radiusInner + (circleCount == 3 ? 3 : 2);
const size = radiusOuter - radiusInner - 2;
let offset = 15;
let end = 360 - offset;
let radius = radiusInner + (circleCount == 3 ? 3 : 2);
let size = radiusOuter - radiusInner - 2;
if (percent <= 0) return; // no gauge needed
if (percent > 1) percent = 1;
const startRotation = -offset;
const endRotation = startRotation - ((end - offset) * percent);
let startRotation = -offset;
let endRotation = startRotation - ((end - offset) * percent);
color = getGradientColor(color, percent);
g.setColor(color);
@ -723,7 +762,7 @@ function drawGauge(cx, cy, percent, color) {
function writeCircleText(w, content) {
if (content == undefined) return;
const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
let font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
g.setFont(font);
g.setFontAlign(0, 0);
@ -755,7 +794,7 @@ function getSteps() {
}
function getWeather() {
const jsonWeather = storage.readJSON('weather.json');
let jsonWeather = storage.readJSON('weather.json');
return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
}
@ -796,7 +835,7 @@ function getPressureValue(type) {
});
}
Bangle.on('lock', function(isLocked) {
function onLock(isLocked) {
if (!isLocked) {
draw();
if (isCircleEnabled("hr")) {
@ -805,11 +844,10 @@ Bangle.on('lock', function(isLocked) {
} else {
Bangle.setHRMPower(0, "circleclock");
}
});
}
Bangle.on('lock', onLock);
let timerHrm;
Bangle.on('HRM', function(hrm) {
function onHRM(hrm) {
if (isCircleEnabled("hr")) {
if (hrm.confidence >= (settings.confidence)) {
hrtValue = hrm.bpm;
@ -826,23 +864,48 @@ Bangle.on('HRM', function(hrm) {
}, settings.hrmValidity * 1000);
}
}
});
}
Bangle.on('HRM', onHRM);
Bangle.on('charging', function(charging) {
function onCharging(charging) {
if (isCircleEnabled("battery")) drawBattery();
});
}
Bangle.on('charging', onCharging);
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
Bangle.setUI("clock");
Bangle.setUI({
mode : "clock",
remove : function() {
// Called to unload all of the clock app
Bangle.removeListener('charging', onCharging);
Bangle.removeListener('lock', onLock);
Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0, "circleclock");
if (timerHrm) clearTimeout(timerHrm);
timerHrm = undefined;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
delete Graphics.prototype.setFontRobotoRegular50NumericOnly;
delete Graphics.prototype.setFontRobotoRegular21;
}});
Bangle.loadWidgets();
// schedule a draw for the next minute
setTimeout(function() {
// draw in interval
setInterval(draw, settings.updateInterval * 1000);
}, 60000 - (Date.now() % 60000));
// schedule a draw for the next second or minute
function queueDraw() {
let queueMillis = settings.updateInterval * 1000;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, queueMillis - (Date.now() % queueMillis));
}
draw();

View File

@ -22,5 +22,6 @@
"circle3colorizeIcon": true,
"circle4colorizeIcon": false,
"hrmValidity": 60,
"updateInterval": 60
"updateInterval": 60,
"showBigWeather": false
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -68,6 +68,11 @@
return x + 's';
},
onchange: x => save('updateInterval', x),
},
/*LANG*/'show big weather': {
value: !!settings.showBigWeather,
format: () => (settings.showBigWeather ? 'Yes' : 'No'),
onchange: x => save('showBigWeather', x),
}
};
E.showMenu(menu);

View File

@ -5,3 +5,4 @@
0.05: Fix bearing not clearing correctly (visible in single or double digit bearings)
0.06: Add button for force compass calibration
0.07: Use 360-heading to output the correct heading value (fix #1866)
0.08: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -20,7 +20,7 @@ ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1);
ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11);
function arrow(r,c) {
r=r*Math.PI/180;
r=(360-r)*Math.PI/180;
var p = Math.PI/2;
ag.setColor(c).fillPoly([
AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r),
@ -34,7 +34,7 @@ var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
g.reset();
if (isNaN(m.heading)) {
if (isNaN(m.heading)) {
if (!wasUncalibrated) {
g.clearRect(0,24,W,48);
g.setFontAlign(0,-1).setFont("6x8");
@ -49,7 +49,7 @@ Bangle.on('mag', function(m) {
g.setFontAlign(0,0).setFont("6x8",3);
var y = 36;
g.clearRect(M-40,24,M+40,48);
g.drawString(Math.round(360-m.heading),M,y,true);
g.drawString(Math.round(m.heading),M,y,true);
}

View File

@ -1,7 +1,7 @@
{
"id": "compass",
"name": "Compass",
"version": "0.07",
"version": "0.08",
"description": "Simple compass that points North",
"icon": "compass.png",
"screenshots": [{"url":"screenshot_compass.png"}],

View File

@ -1 +1,2 @@
0.01: Create dotmatrix clock app
0.02: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -186,7 +186,7 @@ function drawCompass(lastHeading) {
'NW'
];
const cps = Bangle.getCompass();
let angle = cps.heading;
let angle = 360-cps.heading;
let heading = angle?
directions[Math.round(((angle %= 360) < 0 ? angle + 360 : angle) / 45) % 8]:
"-- ";
@ -351,4 +351,4 @@ Bangle.on('faceUp', (up) => {
setSensors(1);
resetDisplayTimeout();
}
});
});

View File

@ -1,7 +1,7 @@
{
"id": "dotmatrixclock",
"name": "Dotmatrix Clock",
"version": "0.01",
"version": "0.02",
"description": "A clear white-on-blue dotmatrix simulated clock",
"icon": "dotmatrixclock.png",
"type": "clock",

View File

@ -14,3 +14,5 @@
0.14: Don't move pages when doing exit swipe - Bangle 2.
0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2.
0.16: Use default Bangle formatter for booleans
0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to
clock face by timeout.

View File

@ -1,28 +1,32 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
/* Desktop launcher
*
*/
var settings = Object.assign({
let settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false,
oneClickExit:false,
swipeExit: false
oneClickExit: false,
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON("dtlaunch.json", true) || {});
if( settings.oneClickExit)
setWatch(_=> load(), BTN1);
var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{
var a=s.readJSON(app,1);
if (settings.oneClickExit) {
var buttonWatch = setWatch(_=> returnToClock(), BTN1, {edge: 'falling'});
}
let s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{
let a=s.readJSON(app,1);
return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
let 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;
@ -33,29 +37,28 @@ apps.forEach(app=>{
app.icon = s.read(app.icon); // should just be a link to a memory area
});
var Napps = apps.length;
var Npages = Math.ceil(Napps/4);
var maxPage = Npages-1;
var selected = -1;
var oldselected = -1;
var page = 0;
let Napps = apps.length;
let Npages = Math.ceil(Napps/4);
let maxPage = Npages-1;
let selected = -1;
let oldselected = -1;
let page = 0;
const XOFF = 24;
const YOFF = 30;
function draw_icon(p,n,selected) {
var x = (n%2)*72+XOFF;
var y = n>1?72+YOFF:YOFF;
let drawIcon= function(p,n,selected) {
let x = (n%2)*72+XOFF;
let y = n>1?72+YOFF:YOFF;
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
g.clearRect(x+12,y+4,x+59,y+51);
g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
g.setFontAlign(0,-1,0).setFont("6x8",1);
var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
var lineY = 0;
var line = "";
while (txt.length > 0){
var c = txt.shift();
let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
let lineY = 0;
let line = "";
while (txt.length > 0){
let c = txt.shift();
if (c.length + 1 + line.length > 13){
if (line.length > 0){
g.drawString(line.trim(),x+36,y+54+lineY*8);
@ -67,29 +70,34 @@ function draw_icon(p,n,selected) {
}
}
g.drawString(line.trim(),x+36,y+54+lineY*8);
}
};
function drawPage(p){
let drawPage = function(p){
g.reset();
g.clearRect(0,24,175,175);
var O = 88+YOFF/2-12*(Npages/2);
for (var j=0;j<Npages;j++){
var y = O+j*12;
let O = 88+YOFF/2-12*(Npages/2);
for (let j=0;j<Npages;j++){
let y = O+j*12;
g.setColor(g.theme.fg);
if (j==page) g.fillCircle(XOFF/2,y,4);
else g.drawCircle(XOFF/2,y,4);
}
for (var i=0;i<4;i++) {
for (let i=0;i<4;i++) {
if (!apps[p*4+i]) return i;
draw_icon(p,i,selected==i && !settings.direct);
drawIcon(p,i,selected==i && !settings.direct);
}
g.flip();
}
};
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
Bangle.loadWidgets();
//g.clear();
Bangle.drawWidgets();
drawPage(0);
let swipeListenerDt = function(dirLeftRight, dirUpDown){
selected = 0;
oldselected=-1;
if(settings.swipeExit && dirLeftRight==1) load();
if(settings.swipeExit && dirLeftRight==1) returnToClock();
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
@ -97,24 +105,25 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
--page; if (page<0) page=maxPage;
drawPage(page);
}
});
};
Bangle.on("swipe",swipeListenerDt);
function isTouched(p,n){
let isTouched = function(p,n){
if (n<0 || n>3) return false;
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
var x2 = x1+71; var y2 = y1+81;
let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF;
let x2 = x1+71; let y2 = y1+81;
return (p.x>x1 && p.y>y1 && p.x<x2 && p.y<y2);
}
};
Bangle.on("touch",(_,p)=>{
var i;
let touchListenerDt = function(_,p){
let i;
for (i=0;i<4;i++){
if((page*4+i)<Napps){
if (isTouched(p,i)) {
draw_icon(page,i,true && !settings.direct);
drawIcon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){
draw_icon(page,selected,false);
drawIcon(page,selected,false);
} else {
load(apps[page*4+i].src);
}
@ -125,12 +134,39 @@ Bangle.on("touch",(_,p)=>{
}
}
if ((i==4 || (page*4+i)>Napps) && selected>=0) {
draw_icon(page,selected,false);
drawIcon(page,selected,false);
selected=-1;
}
});
};
Bangle.on("touch",touchListenerDt);
Bangle.loadWidgets();
g.clear();
Bangle.drawWidgets();
drawPage(0);
const returnToClock = function() {
Bangle.setUI();
if (buttonWatch) {
clearWatch(buttonWatch);
delete buttonWatch;
}
if (timeoutToClock) {
clearTimeout(timeoutToClock);
delete timeoutToClock;
}
Bangle.removeListener("swipe", swipeListenerDt);
Bangle.removeListener("touch", touchListenerDt);
var apps = [];
delete apps;
delete returnToClock;
setTimeout(eval, 0, s.read(".bootcde"));
};
// taken from Icon Launcher with minor alterations
var timeoutToClock;
const updateTimeoutToClock = function(){
if (settings.timeOut!="Off"){
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
if (timeoutToClock) clearTimeout(timeoutToClock);
timeoutToClock = setTimeout(returnToClock,time*1000);
}
};
updateTimeoutToClock();
} // end of app scope

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.16",
"version": "0.17",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

View File

@ -6,50 +6,63 @@
showLaunchers: true,
direct: false,
oneClickExit:false,
swipeExit: false
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
E.showMenu({
"" : { "title" : "Desktop launcher" },
"< Back" : () => back(),
'Show clocks': {
/*LANG*/"< Back" : () => back(),
/*LANG*/'Show clocks': {
value: settings.showClocks,
onchange: v => {
settings.showClocks = v;
writeSettings();
}
},
'Show launchers': {
/*LANG*/'Show launchers': {
value: settings.showLaunchers,
onchange: v => {
settings.showLaunchers = v;
writeSettings();
}
},
'Direct launch': {
/*LANG*/'Direct launch': {
value: settings.direct,
onchange: v => {
settings.direct = v;
writeSettings();
}
},
'Swipe Exit': {
/*LANG*/'Swipe Exit': {
value: settings.swipeExit,
onchange: v => {
settings.swipeExit = v;
writeSettings();
}
},
'One click exit': {
/*LANG*/'One click exit': {
value: settings.oneClickExit,
onchange: v => {
settings.oneClickExit = v;
writeSettings();
}
}
},
/*LANG*/'Time Out': { // Adapted from Icon Launcher
value: timeOutChoices.indexOf(settings.timeOut),
min: 0,
max: timeOutChoices.length-1,
format: v => timeOutChoices[v],
onchange: v => {
settings.timeOut = timeOutChoices[v];
writeSettings();
}
},
});
})
});

1
apps/entonclk/ChangeLog Normal file
View File

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

9
apps/entonclk/README.md Normal file
View File

@ -0,0 +1,9 @@
Enton - Enhanced Anton Clock
This clock face is based on the 'Anton Clock'.
Things I changed:
- The main font for the time is now Audiowide
- Removed the written out day name and replaced it with steps and bpm
- Changed the date string to a (for me) more readable string

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/AH4A/AH4Aw+cikf/mQDCAAIFBAwQDBBYgXCgEDAQIABn4JBkAFBgIKDgQwFmMD+UCmcgl/zEIMzmcQmYKBmYiCAAfxC4QrBl8wBwcgkYsGC4sAiMAF4UxiIGBn8QAgMSC48wgMRiEDBAISCiYcFC48v//yC4PzgJAGiAXIiczPgPzC4JyBmf/AYQXI+KcCj8wmYFCgEjAYQ3G+cjbQIABJIMzAoUin7XIADpSEK4rWGI4MhmRJBn8j+U/d4MimUTkUzIw5dBl4UBMgIXBAgMyLYKOBmQXHiSbCDgMyl8z+UjmJ1BHgJbHCgM/IYQABAgQJBYYYA/AH4AtaQU/mTvBBozWBd44KBkUSkLnBEo8jkcvBI0/CgMiDAIXHHYIXImUzJQJHH+Y+Bn6Z/ABQA=="))

67
apps/entonclk/app.js Normal file
View File

@ -0,0 +1,67 @@
Graphics.prototype.setFontAudiowide = function() {
// Actual height 33 (36 - 4)
var widths = atob("CiAsESQjJSQkHyQkDA==");
var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA");
var scale = 1; // size multiplier for this font
g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16));
};
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
return "?";
}
return Math.round(steps);
}
{ // must be inside our own scope here so that when we are unloaded everything disappears
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
let drawTimeout;
// Actually draw the watch face
let draw = function() {
var x = g.getWidth() / 2;
var y = g.getHeight() / 2;
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
var date = new Date();
var timeStr = require("locale").time(date, 1); // Hour and minute
g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y);
var dateStr = require("locale").date(date, 1).toUpperCase();
g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28);
g.setFontAlign(0, 0).setFont("6x8", 2);
g.drawString(getSteps(), 50, y+70);
g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70);
// queue next draw
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
};
// Show launcher when middle button pressed
Bangle.setUI({
mode : "clock",
remove : function() {
// Called to unload all of the clock app
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
delete Graphics.prototype.setFontAnton;
}});
// Load widgets
Bangle.loadWidgets();
draw();
setTimeout(Bangle.drawWidgets,0);
}

BIN
apps/entonclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View File

@ -0,0 +1,17 @@
{
"id": "entonclk",
"name": "Enton Clock",
"version": "0.1",
"description": "A simple clock using the Audiowide font. ",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme":"README.md",
"storage": [
{"name":"entonclk.app.js","url":"app.js"},
{"name":"entonclk.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

2
apps/gallery/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Submitted to app loader

18
apps/gallery/README.md Normal file
View File

@ -0,0 +1,18 @@
# Gallery
A simple gallery app
## Usage
Upon opening the gallery app, you will be presented with a list of images that you can display. Tap the image to show it. Brightness will be set to full, and the screen timeout will be disabled. When you are done viewing the image, you can tap the screen to go back to the list of images. Press BTN1 to flip the image upside down.
## Adding images
1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions.
2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings:
* 16 bit RGB565 for Bangle 1
* 3 bit RGB for Bangle 2
* 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.)
3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file.

52
apps/gallery/app.js Normal file
View File

@ -0,0 +1,52 @@
const storage = require('Storage');
let imageFiles = storage.list(/^gal-.*\.img/).sort();
let imageMenu = { '': { 'title': 'Gallery' } };
for (let fileName of imageFiles) {
let displayName = fileName.substr(4, fileName.length - 8); // Trim off the 'gal-' and '.img' for a friendly display name
imageMenu[displayName] = eval(`() => { drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this
}
let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu
let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason
let angle = 0; // Store the angle of rotation
let image; // Cache the image here because we access it in multiple places
function drawMenu() {
Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu
Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that
Bangle.setLCDBrightness(backlightSetting); // Restore backlight
image = undefined; // Delete the image from memory
E.showMenu(imageMenu);
}
function drawImage(fileName) {
E.showMenu(); // Remove the menu to prevent it from breaking things
setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger)
Bangle.setOptions({ // Disable display power saving while showing the image
lockTimeout: 0,
lcdPowerTimeout: 0,
backlightTimeout: 0
});
Bangle.setLCDBrightness(1); // Full brightness
image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this
g.clear().reset().drawImage(image, 88, 88, { rotate: angle });
}
setWatch(info => {
if (image) {
if (angle == 0) angle = Math.PI;
else angle = 0;
Bangle.buzz();
g.clear().reset().drawImage(image, 88, 88, { rotate: angle })
}
}, BTN1, { repeat: true });
// We don't load the widgets because there is no reasonable way to unload them
drawMenu();

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw"))

BIN
apps/gallery/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

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