Merge branch 'master' of github.com:ff2005/BangleApps into feature/nifty-clock-b

master
Filipe Fradique 2021-10-22 23:30:27 +01:00
commit 496cdbfa79
97 changed files with 4555 additions and 2436 deletions

View File

@ -217,8 +217,9 @@ and which gives information about the app for the Launcher.
{ "id": "appid", // 7 character app id
"name": "Readable name", // readable name
"shortName": "Short name", // short name for launcher
"icon": "icon.png", // icon in apps/
"version": "0v01", // the version of this app
"description": "...", // long description (can contain markdown)
"icon": "icon.png", // icon in apps/
"type":"...", // optional(if app) -
// 'app' - an application
// 'widget' - a widget
@ -226,6 +227,7 @@ and which gives information about the app for the Launcher.
// 'bootloader' - code that runs at startup only
// 'RAM' - code that runs and doesn't upload anything to storage
"tags": "", // comma separated tag list for searching
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"readme": "README.md", // if supplied, a link to a markdown-style text file

3329
apps.json

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,11 @@
{ "id": "7chname",
"name": "My app's human readable name",
"shortName":"Short Name",
"icon": "app.png",
"version":"0.01",
"description": "A detailed description of my great app",
"icon": "app.png",
"tags": "",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"7chname.app.js","url":"app.js"},

View File

@ -2,11 +2,12 @@
{ "id": "7chname",
"name": "My widget's human readable name",
"shortName":"Short Name",
"icon": "widget.png",
"version":"0.01",
"description": "A detailed description of my great widget",
"tags": "widget",
"icon": "widget.png",
"type": "widget",
"tags": "widget",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"7chname.wid.js","url":"widget.js"}

View File

@ -1,4 +1,6 @@
0.01: First version
0.02: Moved arrow image load to global scope
0.03: faster drawCompass() function, does not cause buttons to become unresponsive
0.04: removed LCD1.write() as it was keeping LCD on
0.04: removed LED1.write() as it was keeping LCD on
0.05: Turn compass off when screen off
Calibrate at start if no info

View File

@ -1,5 +1,5 @@
var pal1color = new Uint16Array([0x0000,0xFFC0],0,1);
var pal2color = new Uint16Array([0x0000,0xffff],0,1);
var pal1color = new Uint16Array([g.theme.bg,0xFFC0],0,1);
var pal2color = new Uint16Array([g.theme.bg,g.theme.fg],0,1);
var buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true});
var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true});
var intervalRef;
@ -7,6 +7,7 @@ var bearing=0; // always point north
var heading = 0;
var oldHeading = 0;
var candraw = false;
var isCalibrating = false;
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function flip1(x,y) {
@ -76,7 +77,7 @@ function tiltfixread(O,S){
return psi;
}
function reading() {
function reading(m) {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
heading = newHeading(d,heading);
var dir = bearing - heading;
@ -97,18 +98,19 @@ function reading() {
function calibrate(){
var max={x:-32000, y:-32000, z:-32000},
min={x:32000, y:32000, z:32000};
var ref = setInterval(()=>{
var m = Bangle.getCompass();
function onMag(m) {
max.x = m.x>max.x?m.x:max.x;
max.y = m.y>max.y?m.y:max.y;
max.z = m.z>max.z?m.z:max.z;
min.x = m.x<min.x?m.x:min.x;
min.y = m.y<min.y?m.y:min.y;
min.z = m.z<min.z?m.z:min.z;
}, 100);
}
Bangle.on('mag', onMag);
Bangle.setCompassPower(1, "app");
return new Promise((resolve) => {
setTimeout(()=>{
if(ref) clearInterval(ref);
Bangle.removeListener('mag', onMag);
var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2};
var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2};
var avg = (delta.x+delta.y+delta.z)/3;
@ -132,6 +134,7 @@ function docalibrate(e,first){
flip1(56,56);
calibrate().then((r)=>{
isCalibrating = false;
require("Storage").write("magnav.json",r);
Bangle.buzz();
CALIBDATA = r;
@ -143,9 +146,13 @@ function docalibrate(e,first){
setTimeout(setButtons,1000);
}
}
if (first===undefined) first=false;
if (first === undefined) first = false;
stopdraw();
clearWatch();
isCalibrating = true;
if (first)
E.showAlert(msg,title).then(action.bind(null,true));
else
@ -153,16 +160,24 @@ function docalibrate(e,first){
}
function startdraw(){
Bangle.setCompassPower(1, "app");
g.clear();
g.setColor(1,1,1);
Bangle.drawWidgets();
candraw = true;
intervalRef = setInterval(reading,500);
if (intervalRef) clearInterval(intervalRef);
intervalRef = setInterval(reading,200);
}
function stopdraw() {
candraw=false;
if(intervalRef) {clearInterval(intervalRef);}
Bangle.setCompassPower(0, "app");
if (intervalRef) {
clearInterval(intervalRef);
intervalRef = undefined;
}
}
function setButtons(){
@ -172,6 +187,7 @@ function setButtons(){
}
Bangle.on('lcdPower',function(on) {
if (isCalibrating) return;
if (on) {
startdraw();
} else {
@ -179,9 +195,8 @@ Bangle.on('lcdPower',function(on) {
}
});
Bangle.on('kill',()=>{Bangle.setCompassPower(0);});
Bangle.loadWidgets();
Bangle.setCompassPower(1);
startdraw();
setButtons();
Bangle.setLCDPower(1);
if (CALIBDATA) startdraw(); else docalibrate({},true);

View File

@ -1,3 +1,6 @@
0.02: Modified for use with new bootloader and firmware
0.03: Shrinked size to avoid cut-off edges on the physical device. BTN3: show date. BTN1: show time in decimal.
0.04: Update to use Bangle.setUI instead of setWatch
0.05: Update *on* the minute rather than every 15 secs
Now show widgets
Make compatible with themes, and Bangle.js 2

View File

@ -1,7 +1,7 @@
// Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr
// https://github.com/eska-muc/BangleApps
const fields = [4, 4, 11, 4];
const offset = 20;
const offset = 24;
const width = g.getWidth() - 2 * offset;
const height = g.getHeight() - 2 * offset;
const rowHeight = height / 4;
@ -10,11 +10,23 @@ var show_date = false;
var show_time = false;
var yy = 0;
rowlights = [];
time_digit = [];
var rowlights = [];
var time_digit = [];
function drawBerlinClock() {
g.clear();
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
g.reset().clearRect(0,24,g.getWidth(),g.getHeight());
var now = new Date();
// show date below the clock
@ -24,8 +36,7 @@ function drawBerlinClock() {
var day = now.getDate();
var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
var strWidth = g.stringWidth(dateString);
g.setColor(1, 1, 1);
g.setFontAlign(-1,-1);
g.setColor(g.theme.fg).setFontAlign(-1,-1);
g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4);
}
@ -50,8 +61,7 @@ function drawBerlinClock() {
x2 = (col + 1) * boxWidth + offset;
y2 = (row + 1) * rowHeight + offset;
g.setColor(1, 1, 1);
g.drawRect(x1, y1, x2, y2);
g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2);
if (col < rowlights[row]) {
if (row === 2) {
if (((col + 1) % 3) === 0) {
@ -65,46 +75,42 @@ function drawBerlinClock() {
g.fillRect(x1 + 2, y1 + 2, x2 - 2, y2 - 2);
}
if (row == 3 && show_time) {
g.setColor(1,1,1);
g.setFontAlign(0,0);
g.setColor(g.theme.fg).setFontAlign(0,0);
g.drawString(time_digit[col],(x1+x2)/2,(y1+y2)/2);
}
}
}
queueDraw();
}
function toggleDate() {
show_date = ! show_date;
drawBerlinClock();
draw();
}
function toggleTime() {
show_time = ! show_time;
drawBerlinClock();
draw();
}
// special function to handle display switch on
Bangle.on('lcdPower', (on) => {
g.clear();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
Bangle.drawWidgets();
// call your app function here
drawBerlinClock();
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// refesh every 15 sec
setInterval(drawBerlinClock, 15E3);
// Show launcher when button pressed, handle up/down
Bangle.setUI("clockupdown", dir=> {
if (dir<0) toggleTime();
if (dir>0) toggleDate();
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawBerlinClock();
if (BTN3) {
// Toggle date display, when BTN3 is pressed
setWatch(toggleTime,BTN1, { repeat : true, edge: "falling"});
// Toggle date display, when BTN3 is pressed
setWatch(toggleDate,BTN3, { repeat : true, edge: "falling"});
}
// Show launcher when button pressed
Bangle.setUI("clock");
draw();

View File

@ -31,3 +31,6 @@
Fix issues where 'Uncaught Error: Function not found' could happen with multiple .boot.js
0.30: Remove 'Get GPS time' at boot. Latest firmwares keep time through reboots, so this is not needed now
0.31: Add polyfills for g.wrapString, g.imageMetrics, g.stringMetrics
0.32: Fix single quote error in g.wrapString polyfill
improve g.stringMetrics polyfill
Fix issue where re-running bootupdate could disable existing polyfills

View File

@ -81,9 +81,11 @@ if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, 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`;
}
delete Bangle.setUI; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!Bangle.setUI) { // assume this is just for F18 - Q3 should already have it
boot += `Bangle.setUI=function(mode, cb) {
if (Bangle.btnWatches) {
@ -131,6 +133,7 @@ else if (mode=="updown") {
throw new Error("Unknown UI mode");
};\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]};
@ -141,15 +144,18 @@ if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfi
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) {
return {width:this.stringWidth(txt), height:this.getFontHeight()};
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")) {
for (var unwrappedLine of str.split("\\n")) {
var words = unwrappedLine.split(" ");
var line = words.shift();
for (var word of words) {

1
apps/bthrm/ChangeLog Normal file
View File

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

45
apps/bthrm/README.md Normal file
View File

@ -0,0 +1,45 @@
# Bluetooth Heart Rate Monitor
When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.
HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor.
This means it's compatible with many Bangle.js apps including:
* [Heart Rate Widget](https://banglejs.com/apps/#widhrt)
* [Heart Rate Recorder](https://banglejs.com/apps/#heart)
It it NOT COMPATIBLE with [Heart Rate Monitor](https://banglejs.com/apps/#hrm)
as that requires live sensor data (rather than just BPM readings).
## Usage
Just install the app, then install an app that uses the heart rate monitor.
Once installed it'll automatically try and connect to the first bluetooth
heart rate monitor it finds.
**To disable this and return to normal HRM, uninstall the app**
## Compatible Heart Rate Monitors
This works with any heart rate monitor providing the standard Bluetooth
Heart Rate Service (`180D`) and characteristic (`2A37`).
So far it has been tested on:
* CooSpo Bluetooth Heart Rate Monitor
## Internals
This replaces `Bangle.setHRMPower` with its own implementation.
## TODO
* Maybe a `bthrm.settings.js` and app (that calls it) to enable it to be turned on and off
* A widget to show connection state?
* Specify a specific device by address?
## Creator
Gordon Williams

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///g3yy06AoIZNitUAg8AgtVqtQAgoRCAwITBAggABAoIABAgsAgIGDoIEDoApDAAwwBFIV1BYo1E+oLTAgQLGJon9BZNXBatdBYRVFBYN/r9fHoxTBBYYlEL4QLFq/a1WUgE///fr4xBv/+1Wq1EAh/3/tX6/fv/6BYOqwCzBBYf9tWq9QLF79X+oLBDIOgKgILEEIIxBGAMVNAP/BYf/BYUFBYJSB6wLC9QLBeAQLBqwLCGAL9BBYmr9X+GAILBbIIlBBYP6/wwBBYMFBYZGB/4XDGAILD34vEcwYLB15HBBYYkBBYWrFwILDKoRTCVIQLCEgQXIEgVaF44YCoRHHAAMUgQuBNgILFgECO4W/BZCPFBYinGBY6/CAArXFBY7vDAAsq1QuB0ALIOwOABY0KEgJGGGAguHDAYDBA=="))

BIN
apps/bthrm/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

79
apps/bthrm/boot.js Normal file
View File

@ -0,0 +1,79 @@
(function() {
var log = function() {};//print
var gatt;
var status;
Bangle.isHRMOn = function() {
return (status=="searching" || status=="connecting") || (gatt!==undefined);
}
Bangle.setHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
log("setHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[];
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app);
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app);
isOn = Bangle._PWR.HRM.length;
// so now we know if we're really on
if (isOn) {
log("setHRMPower on", app);
if (!Bangle.isHRMOn()) {
log("HRM not already on");
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
status = "connecting";
device.on('gattserverdisconnected', function(reason) {
gatt = undefined;
});
return device.gatt.connect();
}).then(function(g) {
log("Connected");
gatt = g;
return gatt.getPrimaryService(0x180D);
}).then(function(service) {
return service.getCharacteristic(0x2A37);
}).then(function(characteristic) {
log("Got characteristic");
characteristic.on('characteristicvaluechanged', function(event) {
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags&1) ? (dv.getUint16(1)/100/* ? */) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit('HRM',{
bpm:bpm,
confidence:100
});
});
return characteristic.startNotifications();
}).then(function() {
log("Ready");
status = "ok";
}).catch(function(err) {
log("Error",err);
gatt = undefined;
status = "error";
});
}
} else { // not on
log("setHRMPower off", app);
if (gatt) {
log("HRM connected - disconnecting");
status = undefined;
try {gatt.disconnect();}catch(e) {
log("HRM disconnect error", e);
}
gatt = undefined;
}
}
};
})();

View File

@ -5,3 +5,5 @@
0.11: added Heart Rate Monitor status and ability to turn on/off
0.12: added support for different locales
0.13: Use setUI, work with smaller screens and themes
0.14: Fix BTN1 (fix #853)
Add light/dark theme support

View File

@ -20,6 +20,8 @@ const HRT_FN_MODE = "fn_hrt";
let infoMode = NONE_MODE;
let functionMode = NONE_FN_MODE;
let textCol = g.theme.dark ? "#0f0" : "#080";
function drawAll(){
updateTime();
updateRest(new Date());
@ -45,9 +47,7 @@ function writeLineStart(line){
function writeLine(str,line){
var y = marginTop+line*fontheight;
g.setFont("6x8",fontsize);
//g.setColor(0,1,0);
g.setColor("#0f0");
g.setFontAlign(-1,-1);
g.setColor(textCol).setFontAlign(-1,-1);
g.clearRect(0,y,((str.length+1)*20),y+fontheight-1);
writeLineStart(line);
g.drawString(str,25,y);
@ -56,7 +56,7 @@ function writeLine(str,line){
function drawInfo(line) {
let val;
let str = "";
let col = "#0f0"; // green
let col = textCol; // green
//console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode);
@ -64,7 +64,7 @@ function drawInfo(line) {
case NONE_FN_MODE:
break;
case HRT_FN_MODE:
col = "#0ff"; // cyan
col = g.theme.dark ? "#0ff": "#088"; // cyan
str = "HRM: " + (hrtOn ? "ON" : "OFF");
drawModeLine(line,str,col);
return;
@ -72,7 +72,7 @@ function drawInfo(line) {
switch(infoMode) {
case NONE_MODE:
col = "#fff";
col = g.theme.bg;
str = "";
break;
case HRT_MODE:
@ -104,9 +104,8 @@ function drawModeLine(line, str, col) {
g.setColor(col);
var y = marginTop+line*fontheight;
g.fillRect(0, y, 239, y+fontheight-1);
g.setColor(0);
g.setFontAlign(0, -1);
g.drawString(str, g.getWidth()/2, y);
g.setColor(g.theme.bg).setFontAlign(0, 0);
g.drawString(str, g.getWidth()/2, y+fontheight/2);
}
function changeInfoMode() {
@ -193,7 +192,7 @@ Bangle.on('lcdPower',function(on) {
var click = setInterval(updateTime, 1000);
// Show launcher when button pressed
Bangle.setUI("clockupdown", btn=>{
if (btn==0) changeInfoMode();
if (btn==1) changeFunctionMode();
if (btn<0) changeInfoMode();
if (btn>0) changeFunctionMode();
drawAll();
});

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Show text if uncalibrated
0.03: Eliminate flickering
0.04: Fix for Bangle.js 2 and themes

View File

@ -1,60 +1,72 @@
var tg = Graphics.createArrayBuffer(120,20,1,{msb:true});
var timg = {
width:tg.getWidth(),
height:tg.getHeight(),
bpp:1,
buffer:tg.buffer
};
var ag = Graphics.createArrayBuffer(160,160,2,{msb:true});
var W = g.getWidth();
var M = W/2; // middle of screen
// Angle buffer
var AGS = W > 200 ? 160 : 120; // buffer size
var AGM = AGS/2; // midpoint/radius
var AGH = AGM-10; // hand size
var ag = Graphics.createArrayBuffer(AGS,AGS,2,{msb:true});
var aimg = {
width:ag.getWidth(),
height:ag.getHeight(),
bpp:2,
buffer:ag.buffer,
palette:new Uint16Array([0,0x03FF,0xF800,0x001F])
palette:new Uint16Array([
g.theme.bg,
g.toColor("#07f"),
g.toColor("#f00"),
g.toColor("#00f")])
};
ag.setColor(1);
ag.fillCircle(80,80,79,79);
ag.setColor(0);
ag.fillCircle(80,80,69,69);
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;
var p = Math.PI/2;
ag.setColor(c);
ag.fillPoly([
80+60*Math.sin(r), 80-60*Math.cos(r),
80+10*Math.sin(r+p), 80-10*Math.cos(r+p),
80+10*Math.sin(r-p), 80-10*Math.cos(r-p),
ag.setColor(c).fillPoly([
AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r),
AGM+10*Math.sin(r+p), AGM-10*Math.cos(r+p),
AGM+10*Math.sin(r-p), AGM-10*Math.cos(r-p),
]);
}
var wasUncalibrated = false;
var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
tg.clear();
tg.setFont("6x8",1);
tg.setColor(1);
g.reset();
if (isNaN(m.heading)) {
tg.setFontAlign(0,-1);
tg.setFont("6x8",1);
tg.drawString("Uncalibrated",60,4);
tg.drawString("turn 360° around",60,12);
if (!wasUncalibrated) {
g.clearRect(0,24,W,48);
g.setFontAlign(0,-1).setFont("6x8");
g.drawString("Uncalibrated\nturn 360° around",M,24+4);
wasUncalibrated = true;
}
else {
tg.setFontAlign(0,0);
tg.setFont("6x8",2);
tg.drawString(Math.round(m.heading),60,12);
} else {
if (wasUncalibrated) {
g.clearRect(0,24,W,48);
wasUncalibrated = false;
}
g.drawImage(timg,0,0,{scale:2});
g.setFontAlign(0,0).setFont("6x8",3);
var y = 36;
g.clearRect(M-40,y,M+40,y+24);
g.drawString(Math.round(m.heading),M,y,true);
}
ag.setColor(0);
arrow(oldHeading,0);
arrow(oldHeading+180,0);
arrow(m.heading,2);
arrow(m.heading+180,3);
g.drawImage(aimg,40,50);
g.drawImage(aimg,
(W-ag.getWidth())/2,
g.getHeight()-(ag.getHeight()+4));
oldHeading = m.heading;
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setCompassPower(1);
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);

1
apps/fwupdate/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial version

BIN
apps/fwupdate/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

284
apps/fwupdate/custom.html Normal file
View File

@ -0,0 +1,284 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="fw-unknown">
<p>Firmware updates using the App Loader are only possible on
Bangle.js 2. For firmware updates on Bangle.js 1 please
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></p>
</div>
<div id="fw-ok" style="display:none">
<p>Please upload a hex file here. This file should be the <code>.app_hex</code>
file, *not* the normal <code>.hex</code> (as that contains the bootloader as well).</p>
<input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex"/><br>
<p><button id="upload" class="btn btn-primary">Upload</button></p>
</div>
<pre id="log"></pre>
<script src="../../core/lib/customize.js"></script>
<script>
var hex;
var hexJS; // JS to upload hex
var HEADER_LEN = 16; // size of app flash header
var MAX_ADDRESS = 0x1000000; // discount anything in hex file above this
var VERSION = 0x12345678; // VERSION! Use this to test firmware in JS land
var DEBUG = false;
function log(t) {
document.getElementById('log').innerText += t+"\n";
console.log(t);
}
function onInit(device) {
console.log(device);
if (device && device.id=="BANGLEJS2") {
document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = "";
}
}
function checkForFileOnServer() {
/*function getURL(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = callback;
baseURL = url;
xhr.open("GET", baseURL);
xhr.responseType = "document";
xhr.send();
}
function getFilesFromURL(url, regex, callback) {
getURL(url, function() {
var files = [];
var elements = this.responseXML.getElementsByTagName("a");
for (var i=0;i<elements.length;i++) {
var href = elements[i].href;
if (regex.exec(href)) {
files.push(href);
}
}
callback(files);
});
}
var regex = new RegExp("_bangle2");
var domFirmware = document.getElementById("latest-firmware");
getFilesFromURL("https://www.espruino.com/binaries/", regex, function(releaseFiles) {
releaseFiles.sort().reverse().forEach(function(f) {
var name = f.substr(f.substr(0,f.length-1).lastIndexOf('/')+1);
domFirmware.innerHTML += 'Release: <a href="'+f+'">'+name+'</a><br/>';
});
getFilesFromURL("https://www.espruino.com/binaries/travis/master/",regex, function(travisFiles) {
travisFiles.forEach(function(f) {
var name = f.substr(f.lastIndexOf('/')+1);
domFirmware.innerHTML += 'Cutting Edge build: <a href="'+f+'">'+name+'</a><br/>';
});
document.getElementById("checking-server").style = "display:none";
document.getElementById("main-ui").style = "";
});
});*/
}
function downloadFile() {
/*response = await fetch(APP_HEX_PATH+"readlink.php?link="+APP_HEX_FILE, {
method: 'GET',
cache: 'no-cache',
});
if (response.ok) {
blob = await response.blob();
data = await blob.text();
document.getElementById("latest-firmware").innerHTML="(<b>"+data.toString()+"</b>)";
}*/
}
function handleFileSelect(event) {
if (event.target.files.length!=1) {
log("More than one file selected!");
return;
}
var reader = new FileReader();
reader.onload = function(event) {
hex = event.target.result.split("\n");
document.getElementById("upload").style = ""; // show upload
fileLoaded();
};
reader.readAsText(event.target.files[0]);
};
function parseLines(dataCallback) {
var addrHi = 0;
hex.forEach(function(hexline) {
if (DEBUG) console.log(hexline);
var bytes = hexline.substr(1,2);
var addrLo = parseInt(hexline.substr(3,4),16);
var cmd = hexline.substr(7,2);
if (cmd=="02") addrHi = parseInt(hexline.substr(9,4),16) << 4; // Extended Segment Address
else if (cmd=="04") addrHi = parseInt(hexline.substr(9,4),16) << 16; // Extended Linear Address
else if (cmd=="00") {
var addr = addrHi + addrLo;
var data = [];
for (var i=0;i<16;i++) data.push(parseInt(hexline.substr(9+(i*2),2),16));
dataCallback(addr,data);
}
});
}
function CRC32(data) {
var crc = 0xFFFFFFFF;
data.forEach(function(d) {
crc^=d;
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
});
return (~crc)>>>0; // >>>0 converts to unsigned 32-bit integer
}
function btoa(input) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var out = "";
var i=0;
while (i<input.length) {
var octet_a = 0|input[i++];
var octet_b = 0;
var octet_c = 0;
var padding = 0;
if (i<input.length) {
octet_b = 0|input[i++];
if (i<input.length) {
octet_c = 0|input[i++];
padding = 0;
} else
padding = 1;
} else
padding = 2;
var triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
out += b64[(triple >> 18) & 63] +
b64[(triple >> 12) & 63] +
((padding>1)?'=':b64[(triple >> 6) & 63]) +
((padding>0)?'=':b64[triple & 63]);
}
return out;
}
// To upload the app, we write to external flash
function createJS_app(binary, bin32, startAddress, endAddress, HEADER_LEN) {
/* typedef struct {
uint32_t address;
uint32_t size;
uint32_t CRC;
uint32_t version;
} FlashHeader; */
bin32[0] = startAddress;
bin32[1] = endAddress - startAddress;
bin32[2] = CRC32(new Uint8Array(binary.buffer, HEADER_LEN));
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) {
var l = binary.length-i;
if (l>CHUNKSIZE) l=CHUNKSIZE;
var chunk = btoa(new Uint8Array(binary.buffer, i, l));
hexJS += `\x10s.write('.firmware', atob("${chunk}"), 0x${i.toString(16)}, ${binary.length});\n`;
}
hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
hexJS += '\x10setTimeout(()=>E.reboot(), 1000);\n';
}
// To upload the bootloader, we write to internal flash, right over bootloader
function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary);
console.log("CRC 0x"+crc.toString(16));
hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("BOOTLOADER UP TO DATE!"); load();}\n`;
hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`;
var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) {
var l = binary.length-i;
if (l>CHUNKSIZE) l=CHUNKSIZE;
var chunk = btoa(new Uint8Array(binary.buffer, binary.byteOffset+i, l));
hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n';
}
// hexJS += `\x10(function() {
// if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC!";
// var f = require("Flash");
// for (var i=${startAddress};i<${endAddress};i+=4096) f.erasePage(i);
// f.write(_fw,${startAddress});
// E.reboot();
// })();\n`;
hexJS += `\x10if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += '\x10var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096)
hexJS += '\x10f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `\x10f.write(_fw,${startAddress});\n`;
// hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
// hexJS += '\x10setTimeout(()=>E.reboot(), 2000);\n';
}
function fileLoaded() {
// Work out addresses
var startAddress, endAddress = 0;
parseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range
if (startAddress === undefined || addr<startAddress)
startAddress = addr;
var end = addr + data.length;
if (end > endAddress)
endAddress = end;
});
console.log(`// Data from 0x${startAddress.toString(16)} to 0x${endAddress.toString(16)} (${endAddress-startAddress} bytes)`);
// Work out data
var HEADER_LEN = 16;
var binary = new Uint8Array(HEADER_LEN + endAddress-startAddress);
binary.fill(0); // actually seems to assume a block is filled with 0 if not complete
var bin32 = new Uint32Array(binary.buffer);
parseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range
var binAddr = HEADER_LEN + addr - startAddress;
binary.set(data, binAddr);
if (DEBUG) console.log("i",addr.toString(16).padStart(8,0), data.map(x=>x.toString(16).padStart(2,0)).join(" "));
//console.log("o",new Uint8Array(binary.buffer, binAddr, data.length));
});
if (startAddress == 0xf7000) {
console.log("Bootloader - Writing to internal flash");
createJS_bootloader(new Uint8Array(binary.buffer, HEADER_LEN), startAddress, endAddress);
} else {
console.log("App - Writing to external flash");
createJS_app(binary, bin32, startAddress, endAddress);
}
}
function handleUpload() {
if (!hexJS) {
log("Hex file not loaded!");
return;
}
sendCustomizedApp({
storage:[
{name:"RAM", content:hexJS},
]
});
}
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload);
checkForFileOnServer();
</script>
</body>
</html>

View File

@ -1,2 +1,3 @@
0.03: Fix time output on new firmwares when no GPS time set (fix #104)
0.04: Fix shown UTC time zone sign
0.05: Use new 'layout library for Bangle2, fix #764 by adding a back button

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="))
require("heatshrink").decompress(atob("mEw4UA////G161hyd8Jf4ALlQLK1WABREC1WgBZEK32oFxPW1QuJ7QwIFwOqvQLHhW31NaBY8qy2rtUFoAuG3W61EVqALF1+qr2gqtUHQu11dawNVqo6F22q9XFBYIwEhWqz2r6oLBGAheBqwuBBYx2CFwQLGlWqgoLCMAsKLoILChR6EgQuDqkqYYsBFweqYYoLDoWnYYoLD/WVYYv8FwXqPoIwEn52BqGrPoILEh/1FwOl9SsBBYcD/pdB2uq/QvEh/8LoOu1xHFh8/gGp9WWL4oMBgWltXeO4owBgWt1ReFYYh2GYYmXEQzDD3wiHegYKIGAJRGAAguJAH4AC"))

View File

@ -1,68 +1,75 @@
var img = require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="));
function satelliteImage() {
return require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AGnE4F1wvsF34wgFldcLdyMYsoACF1WJF4YxPFzOtF4wxNFzAvKSiIvU1ovIGAkJAAQucF5QxCFwYwbF4QwLrwvjYIVfrwABrtdq9Wqwvkq4oCAAtXmYvi1teE4NXrphCrxoCGAbvdSIoAHNQNeFzQvGeRQvCsowrYYNfF8YwHZQQFCF8QwGF4owjeYovBroHEMERhEF8IwNrtWryYFF8YwCq4vhGBeJF5AwaxIwKwVXFwwvandfMJeJF8M6nZiLGQIvdstfGAVlGBZkCxJeZJQIwCGIRjMFzYACGIc6r/+FsIvGGIYABEzYvPGQYvusovkAH4A/AH4A/ACo="));
}
var fix;
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
var Layout = require("Layout");
Bangle.setGPSPower(1, "app");
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showMessage("Loading..."); // avoid showing rubbish on screen
g.clear();
var fix;
Bangle.setGPSPower(1);
Bangle.on('GPS',function(f) {
fix = f;
g.reset(1);
g.setFont("6x8",2);
g.setFontAlign(0,0);
g.clearRect(90,30,239,90);
if (fix.fix) {
g.drawString("GPS",170,40);
g.drawString("Acquired",170,60);
function setGPSTime() {
if (fix.time!==undefined) {
setTime(fix.time.getTime()/1000);
E.showMessage("System time set", {img:require("heatshrink").decompress(atob("lEo4UBvvv///vEFBYNVAAWq1QFDBAgKGrQJD0oJDtQJD1IICqwGBFoIDByocDwAJBgQeDtWoJwcqDwWq0EAgfAgEKHoQcCBIQeBGAQaBBIQzBytaEwQJDlWlrQmBBIkK0tqBI+ptRNCBIcCBKhECBIh6CAgUL8AJHl/4BI8+3gJRl/8GJH/BI8Ah6MDLIZQB+BjGAAIoBBI84BIaVCAAaVBVIYJEWYLkEXobRDAAbRBcoYACcoT5DEwYJCtQoElWpBINaDwYcB0oJBGQIzCAYIwBDwQGBAAIcCDwYACDgQACBIYIEBQYFDA="))});
} else {
g.drawString("Waiting for",170,40);
g.drawString("GPS Fix",170,60);
E.showMessage("No GPS time to set");
}
g.setFont("6x8");
g.drawString(fix.satellites+" satellites",170,80);
g.clearRect(0,100,239,239);
var t = ["","","","---",""];
if (fix.time!==undefined)
Bangle.removeListener('GPS',onGPS);
setTimeout(function() {
fix = undefined;
layout.forgetLazyState(); // redraw all next time
Bangle.on('GPS',onGPS);
}, 2000);
}
var layout = new Layout( {
type:"v", c: [
{type:"h", c:[
{type:"img", src:satelliteImage },
{ type:"v", fillx:1, c: [
{type:"txt", font:"6x8:2", label:"Waiting\nfor GPS", id:"status" },
{type:"txt", font:"6x8", label:"---", id:"sat" },
]},
]},
{type:"txt", fillx:1, filly:1, font:"6x8:2", label:"---", id:"gpstime" }
]},{lazy:true, btns: [
{ label : "Set", cb : setGPSTime},
{ label : "Back", cb : ()=>load() }
]});
function onGPS(f) {
if (fix===undefined) {
g.clear();
Bangle.drawWidgets();
}
fix = f;
if (fix.fix) {
layout.status.label = "GPS\nAcquired";
} else {
layout.status.label = "Waiting\nfor GPS";
}
layout.sat.label = fix.satellites+" satellites";
var t = ["","---",""];
if (fix.time!==undefined) {
t = fix.time.toString().split(" ");
/*
[
"Sun",
"Nov",
"10",
"2019",
"15:55:35",
"GMT+0100"
]
*/
//g.setFont("6x8",2);
//g.drawString(t[0],120,110); // day
g.setFont("6x8",3);
g.drawString(t[1]+" "+t[2],120,135); // date
g.setFont("6x8",2);
g.drawString(t[3],120,160); // year
g.setFont("6x8",3);
g.drawString(t[4],120,185); // time
if (fix.time) {
// timezone
var tz = (new Date()).getTimezoneOffset()/-60;
if (tz==0) tz="UTC";
else if (tz>0) tz="UTC+"+tz;
else tz="UTC"+tz;
g.setFont("6x8",2);
g.drawString(tz,120,210); // gmt
g.setFontAlign(0,0,3);
g.drawString("Set",230,120);
g.setFontAlign(0,0);
}
});
setInterval(function() {
g.drawImage(img,48,48,{scale:1.5,rotate:Math.sin(getTime()*2)/2});
},100);
setWatch(function() {
if (fix.time!==undefined)
setTime(fix.time.getTime()/1000);
}, BTN2, {repeat:true});
t = [t[1]+" "+t[2],t[3],t[4],t[5],tz];
}
layout.gpstime.label = t.join("\n");
layout.render();
}
Bangle.on('GPS',onGPS);

View File

@ -1,2 +1,2 @@
0.01: base code
0.02: saved settings when switching color scheme

View File

@ -180,13 +180,46 @@ function fmtDate(day,month,year,hour)
return months[month] + ". " + day + " " + year;
}
// Handles Flipping colors, then refreshes the UI
//////////////////////////////////////////
//
// HANDLE COLORS + SETTINGS
//
function getColorScheme()
{
let settings = require('Storage').readJSON("hcclock.json", true) || {};
if (!("scheme" in settings)) {
settings.scheme = 0;
}
return settings.scheme;
}
function setColorScheme(value)
{
let settings = require('Storage').readJSON("hcclock.json", true) || {};
settings.scheme = value;
require('Storage').writeJSON('hcclock.json', settings);
if(value == 0) // White
{
bg = 255;
fg = 0;
}
else // Black
{
bg = 0;
fg = 255;
}
redraw();
}
function flipColors()
{
let t = bg;
bg = fg;
fg = t;
redraw();
if(getColorScheme() == 0)
setColorScheme(1);
else
setColorScheme(0);
}
//////////////////////////////////////////
@ -197,7 +230,7 @@ function flipColors()
// Initialize
g.clear();
Bangle.loadWidgets();
redraw();
setColorScheme(getColorScheme());
// Define Refresh Interval
setInterval(updateTime, interval);

View File

@ -12,3 +12,4 @@
Generate scale based on defined minimum and maximum measurement
Added background line on 50% to ease estimation of drawn values
0.06: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799)
0.07: theme support

View File

@ -221,9 +221,9 @@ function graphRecord(n) {
if (tempCount == startLine) {
// generating rgaph in loop when reaching startLine to keep loading
// message on screen until graph can be drawn
g.clear().
g.reset().clearRect(0,24,g.getWidth(),g.getHeight()).
// Home for Btn2
setColor(1, 1, 1).
setColor(g.theme.fg).
drawLine(220, 118, 227, 110).
drawLine(227, 110, 234, 118).
drawPoly([222,117,222,125,232,125,232,117], false).
@ -245,7 +245,7 @@ function graphRecord(n) {
// scale indicator line for 50%
drawLine(GraphXZero - GraphMarkerOffset, GraphY100 + (GraphYZero - GraphY100)/2, GraphXZero, GraphY100 + (GraphYZero - GraphY100)/2).
// background line for 50%
setColor(1, 1, 1).
setColor(g.theme.fg).
drawLine(GraphXZero + 1, GraphY100 + (GraphYZero - GraphY100)/2, GraphXMax, GraphY100 + (GraphYZero - GraphY100)/2).
setFontAlign(1, -1, 0).
setFont("Vector", 10);
@ -303,7 +303,7 @@ function graphRecord(n) {
log("Finished rendering data");
Bangle.buzz(200, 0.3);
g.flip();
setWatch(stop, BTN2, {edge:"falling", debounce:50, repeat:false});
setWatch(stop, (global.BTN2!==undefined)?BTN2:BTN1, {edge:"falling", debounce:50, repeat:false});
return;
}

View File

@ -3,3 +3,4 @@
0.03: Fix timing issues, and use 1/2 scale to keep graph on screen
0.04: Update for new firmwares that have a 'HRM-raw' event
0.05: Tweaks for 'HRM-raw' handling
0.06: Add widgets

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwghC/AH4AThnMAAXABJoMHBwgJJAAYMFAAIJLFxImCBJIuLABYuI4gXNNZFCC6AIFkZIQA4szC6vEmdMC60sC6nDmc8C6RDBC4irLC4gTBocymgGBoYXO4UyUwNEAYKrMC4ZEBUwNMVAR7LC4dDCoYBBSYJ7DoZQCC4kCmczkc0JIVM4UzmgaBAAQWD4AXBggJBJAIkBocs4c0BAQXJJARBD4c8oc8HAKZCI4gWCVAYXEJIJoCOovNC4cMUIQPB4RFBTAYAFIwapEC4JyCZAalHGAvCJYZYCVAYuIMIhjE5heGCwxhDMYTtIFw4wFoYsGFxIwF4YuRGAh7DFxxhGFyIYKCxqrGIpwwKFx4YGCyJJFCyQYDCygA/AH4AFA="))
require("heatshrink").decompress(atob("mEw4UA///g3yrv/7f+Jf4AJgNVoAEGAANVAAIEGCIQABoAEEBYMFAwVQAggLBioGCqgEEFIgAGFwdXBYw1Dr4LKrwLHIIVaBYxNDvXVBanVteVBZGVt+VKooLBq+19u1JItQgNW0vlBYIxEL4Ne1u18taGIN9BYUD1XvBYN62+q1a0D1d7ytttYLEWYV6BYNt93VEYKzCita6t59vqX4sFIgN70tqa4pUBTgO1vbvFgB0BKQNZawYACdYNeytdFwgwCBYJ2DFwQwCqoxBFwwABBYoKEGAKyDFwgwDFw4kDERBVDEQ4kEEQ4kDBRAYBERBuCNAoA/AA4="))

View File

@ -4,13 +4,14 @@ Bangle.setHRMPower(1);
var hrmInfo, hrmOffset = 0;
var hrmInterval;
var btm = g.getHeight()-1;
var lastHrmPt = []; // last xy coords we draw a line to
function onHRM(h) {
if (counter!==undefined) {
// the first time we're called remove
// the countdown
counter = undefined;
g.clear();
g.clearRect(0,24,g.getWidth(),g.getHeight());
}
hrmInfo = h;
/* On 2v09 and earlier firmwares the only solution for realtime
@ -28,7 +29,7 @@ function onHRM(h) {
var px = g.getWidth()/2;
g.setFontAlign(0,0);
g.clearRect(0,24,239,80);
g.clearRect(0,24,g.getWidth(),80);
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
var str = hrmInfo.bpm;
g.setFontVector(40).drawString(str,px,45);
@ -43,17 +44,18 @@ Bangle.on('HRM-raw', function(v) {
hrmOffset++;
if (hrmOffset>g.getWidth()) {
hrmOffset=0;
g.clearRect(0,80,239,239);
g.moveTo(-100,0);
g.clearRect(0,80,g.getWidth(),g.getHeight());
lastHrmPt = [-100,0];
}
y = E.clip(btm-v.filt/4,btm-10,btm);
g.setColor(1,0,0).fillRect(hrmOffset,btm, hrmOffset, y);
y = E.clip(170 - (v.raw/2),80,btm);
g.setColor(g.theme.fg).lineTo(hrmOffset, y);
g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y);
lastHrmPt = [hrmOffset, y];
if (counter !==undefined) {
counter = undefined;
g.clear();
g.clearRect(0,24,g.getWidth(),g.getHeight());
}
});
@ -65,7 +67,10 @@ function countDown() {
setTimeout(countDown, 1000);
}
}
g.clear().setFont("6x8",2).setFontAlign(0,0);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
countDown();
@ -79,13 +84,14 @@ function readHRM() {
if (!hrmInfo) return;
if (hrmOffset==0) {
g.clearRect(0,100,239,239);
g.moveTo(-100,0);
g.clearRect(0,100,g.getWidth(),g.getHeight());
lastHrmPt = [-100,0];
}
for (var i=0;i<2;i++) {
var a = hrmInfo.raw[hrmOffset];
hrmOffset++;
y = E.clip(170 - (a*2),100,230);
g.setColor(g.theme.fg).lineTo(hrmOffset, y);
g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y);
lastHrmPt = [hrmOffset, y];
}
}

View File

@ -16,7 +16,7 @@ function drawMenu() {
var w = g.getWidth();
var h = g.getHeight();
var m = w/2;
var n = (h-48)/64;
var n = Math.floor((h-48)/64);
if (selected>=n+menuScroll) menuScroll = 1+selected-n;
if (selected<menuScroll) menuScroll = selected;
// arrows

21
apps/messages/README.md Normal file
View File

@ -0,0 +1,21 @@
# Messages app
**THIS APP IS CURRENTLY BETA**
This app handles the display of messages and message notifications. It stores
a list of currently received messages and allows them to be listed, viewed,
and responded to.
It is a replacement for the old `notify`/`gadgetbridge` apps.
## Usage
...
## Requests
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app
## Creator
Gordon Williams

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA="))

273
apps/messages/app.js Normal file
View File

@ -0,0 +1,273 @@
/* MESSAGES is a list of:
{id:int,
src,
title,
subject,
body,
sender,
tel:string,
new:true // not read yet
}
*/
/* For example for maps:
GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"})
GB({"t":"notify","id":2,"src":"Hangouts","title":"Gordon","body":"Hello world quite a lot of text here..."})
GB({"t":"notify","id":3,"src":"Messages","title":"Ted","body":"Bed time."})
GB({"t":"notify","id":4,"src":"Messages","title":"Kailo","body":"Mmm... food"})
GB({"t":"notify-","id":1})
GB({"t":"notify","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"Y2MBAA....AAAAAAAAAAAAAA="})
GB({"t":"notify~","id":1,"body":"Campton - 11:54 ETA"})
GB({"t":"notify~","id":1,"title":"High St"})
GB({"t":"notify~","id":1,"body":"Campton - 11:55 ETA"})
GB({"t":"notify~","id":1,"title":"0 yd - High St"})
GB({"t":"notify~","id":1,"body":"Campton - 11:56 ETA"})
*/
var Layout = require("Layout");
var MESSAGES = require("Storage").readJSON("messages.json",1)||[];
if (!Array.isArray(MESSAGES)) MESSAGES=[];
var onMessagesModified = function(msg) {
// TODO: if new, show this new one
if (msg.new) Bangle.buzz();
showMessage(msg.id);
};
function saveMessages() {
require("Storage").writeJSON("messages.json",MESSAGES)
}
function showMapMessage(msg) {
var m;
var distance, street, target, eta;
m=msg.title.match(/(.*) - (.*)/);
if (m) {
distance = m[1];
street = m[2];
} else street=msg.title;
m=msg.body.match(/(.*) - (.*)/);
if (m) {
target = m[1];
eta = m[2];
} else target=msg.body;
layout = new Layout({
type:"v", c: [
{type:"txt", font:"6x15", label:target, bgCol:"#0f0", fillx:1, pad:2 },
{type:"h", bgCol:"#0f0", fillx:1, c: [
{type:"txt", font:"6x8", label:"Towards" },
{type:"txt", font:"6x15:2", label:street }
]},
{type:"h",fillx:1, filly:1, c: [
{type:"img",src:atob(msg.img)},
{type:"v", fillx:1, c: [
{type:"txt", font:"6x15:2", label:distance||"" }
]},
]},
{type:"txt", font:"6x8:2", label:eta }
]
});
g.clearRect(0,24,g.getWidth()-1,g.getHeight()-1);
layout.render();
Bangle.setUI("updown",function() {
// any input to mark as not new and return to menu
msg.new = false;
saveMessages();
checkMessages();
});
}
function showMessage(msgid) {
var msg = MESSAGES.find(m=>m.id==msgid);
if (!msg) return checkMessages(); // go home if no message found
if (msg.src=="Maps") return showMapMessage(msg);
var m = msg.title+"\n"+msg.body;
E.showPrompt(m,{title:"Message", buttons : {"Read":"read", "Back":"back"}}).then(chosen => {
if (chosen=="read") {
// any input to mark as not new and return to menu
msg.new = false;
saveMessages();
checkMessages();
} else {
checkMessages(true);
}
});
}
// Show a single menu item for the message
function showMessageMenuItem(y, idx) {
var msg = MESSAGES[idx];
var W = g.getWidth(), H=48;
if (msg.new) g.setBgColor("#4F4");
else g.setBgColor("#CFC");
g.clearRect(0,y,W-1,y+H-1).setColor(g.theme.fg);
var m = msg.title+"\n"+msg.body;
if (msg.src) g.setFontAlign(1,-1).drawString(msg.src, W-2, y+2);
if (msg.title) g.setFontAlign(-1,-1).setFont("12x20").drawString(msg.title, 2,y+2);
if (msg.body) {
g.setFontAlign(-1,-1).setFont("6x8");
var l = g.wrapString(msg.body, W-14);
if (l.length>3) {
l = l.slice(0,3);
l[l.length-1]+="...";
}
g.drawString(l.join("\n"), 12,y+20);
}
}
//test
//g.clear(1); showMessageMenuItem(MESSAGES[0],24)
if (process.env.HWVERSION==1) { // Bangle.js 1
showBigMenu = function(options) {
/* options = {
h = height
items = # of items
draw = function(y, idx)
onSelect = function(idx)
}*/
var selected = 0;
var menuScroll = 0;
var menuShowing = false;
var w = g.getWidth();
var h = g.getHeight();
var m = w/2;
var n = Math.floor((h-48)/options.h);
function drawMenu() {
g.reset();
if (selected>=n+menuScroll) menuScroll = 1+selected-n;
if (selected<menuScroll) menuScroll = selected;
// draw
g.setColor(g.theme.fg);
for (var i=0;i<n;i++) {
var idx = i+menuScroll;
if (idx<0 || idx>=options.items) break;
var y = 24+i*options.h;
options.draw(y, idx);
// border for selected
if (i+menuScroll==selected) {
g.setColor(g.theme.fgG).drawRect(0,y,w-1,y+options.h-1).drawRect(1,y+1,w-2,y+options.h-2);
}
}
// arrows
g.setColor(menuScroll ? g.theme.fg : g.theme.bg);
g.fillPoly([m,6,m-14,20,m+14,20]);
g.setColor((options.items>n+menuScroll) ? g.theme.fg : g.theme.bg);
g.fillPoly([m,h-7,m-14,h-21,m+14,h-21]);
}
g.clearRect(0,24,w-1,h-1);
drawMenu();
Bangle.setUI("updown",dir=>{
if (dir) {
selected += dir;
if (selected<0) selected = options.items-1;
if (selected>=options.items) selected = 0;
drawMenu();
} else {
options.onSelect(selected);
}
});
}
} else { // Bangle.js 2
showBigMenu = function(options) {
/* options = {
h = height
items = # of items
draw = function(y, idx)
onSelect = function(idx)
}*/
var menuScroll = 0;
var menuShowing = false;
var w = g.getWidth();
var h = g.getHeight();
var n = Math.ceil((h-24)/options.h);
var menuScrollMax = options.h*options.items - (h-24);
function drawItem(i) {
var y = 24+i*options.h-menuScroll;
if (i<0 || i>=options.items || y<-options.h || y>=h) return;
options.draw(y, i);
}
function drawMenu() {
g.reset().clearRect(0,24,w-1,h-1);
g.setClipRect(0,24,w-1,h-1);
for (var i=0;i<n;i++) drawItem(i);
g.setClipRect(0,0,w-1,h-1);
}
drawMenu();
g.flip(); // force an update now to make this snappier
Bangle.dragHandler = e=>{
var dy = e.dy;
if (menuScroll - dy < 0)
dy = menuScroll;
if (menuScroll - dy > menuScrollMax)
dy = menuScroll - menuScrollMax;
if (!dy) return;
g.reset().setClipRect(0,24,g.getWidth()-1,g.getHeight()-1);
g.scroll(0,dy);
menuScroll -= dy;
if (e.dy < 0) {
drawItem(Math.floor((menuScroll+24+g.getHeight())/options.h)-1);
if (e.dy <= -options.h) drawItem(Math.floor((menuScroll+24+h)/options.h)-2);
} else {
drawItem(Math.floor((menuScroll+24)/options.h));
if (e.dy >= options.h) drawItem(Math.floor((menuScroll+24)/options.h)+1);
}
g.setClipRect(0,0,w-1,h-1);
};
Bangle.on('drag',Bangle.dragHandler);
Bangle.touchHandler = (_,e)=>{
if (e.y<20) return;
var i = Math.floor((e.y+menuScroll-24) / options.h);
if (i>=0 && i<options.items)
options.onSelect(i);
};
Bangle.on("touch", Bangle.touchHandler);
}
}
function checkMessages(forceShowMenu) {
// If no messages, just show 'no messages' and return
if (!MESSAGES.length)
return E.showPrompt("No Messages",{
title:"Messages",
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
buttons : {"Ok":1}
}).then(() => { load() });
// we have >0 messages
// TODO: IF A NEW MESSAGE, SHOW IT
if (!forceShowMenu) {
var newMessages = MESSAGES.filter(m=>m.new);
if (newMessages.length)
return showMessage(newMessages[0].id);
}
// Otherwise show a menu
var m = {
"":{title:"Messages"},
"< Back": ()=>load()
};
/*g.setFont("6x8");
MESSAGES.forEach(msg=>{
// "id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"
var title = g.wrapString(msg.title, g.getWidth())[0];
m[title] = function() {
showMessage(msg.id);
}
});
E.showMenu(m);*/
showBigMenu({
h : 48,
items : MESSAGES.length,
draw : showMessageMenuItem,
onSelect : idx => showMessage(MESSAGES[idx].id)
});
}
Bangle.loadWidgets();
Bangle.drawWidgets();
checkMessages();

BIN
apps/messages/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

36
apps/messages/boot.js Normal file
View File

@ -0,0 +1,36 @@
(function() {
var _GB = global.GB;
global.GB = (event) => {
if (_GB) setTimeout(_GB,0,event);
// call handling?
if (!event.t.startsWith("notify")) return;
/* event is:
{t:"notify",id:int, src,title,subject,body,sender,tel:string}
{t:"notify~",id:int, title:string} // modified
{t:"notify-",id:int} // remove
*/
var messages, inApp = "undefined"!=typeof MESSAGES;
if (inApp)
messages = MESSAGES; // we're in an app that has already loaded messages
else // no app - load messages
messages = require("Storage").readJSON("messages.json",1)||[];
// now modify/delete as appropriate
var mIdx = messages.findIndex(m=>m.id==event.id);
if (event.t=="notify-") {
if (mIdx>=0) messages.splice(mIdx, 1); // remove item
mIdx=-1;
} else { // add/modify
if (event.t=="notify") event.new=true; // new message
if (mIdx<0) mIdx=messages.push(event)-1;
else Object.assign(messages[mIdx], event);
}
require("Storage").writeJSON("messages.json",messages);
if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]);
// ok, saved now - we only care if it's new
if (event.t!="notify") return;
// if we're in a clock, go straight to messages app
if (Bangle.CLOCK) return load("messages.app.js");
if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know
WIDGETS.messages.newMessage();
};
})()

0
apps/messages/lib.js Normal file
View File

20
apps/messages/widget.js Normal file
View File

@ -0,0 +1,20 @@
WIDGETS["messages"]={area:"tl",width:0,draw:function() {
if (!this.width) return;
var c = (Date.now()-this.t)/1000;
g.reset().setBgColor((c&1) ? "#0f0" : "#030").setColor((c&1) ? "#000" : "#fff");
g.clearRect(this.x,this.y,this.x+this.width,this.y+23);
g.setFont("6x8:1x2").setFontAlign(0,0).drawString("MESSAGES", this.x+this.width/2, this.y+12);
//if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute
if (c<120 && (Date.now()-this.l)>4000) {
this.l = Date.now();
Bangle.buzz(); // buzz every 4 seconds
}
setTimeout(()=>WIDGETS["messages"].draw(), 1000);
},newMessage:function() {
WIDGETS["messages"].t=Date.now(); // first time
WIDGETS["messages"].l=Date.now()-10000; // last buzz
if (WIDGETS["messages"].c!==undefined) return; // already called
WIDGETS["messages"].width=64;
Bangle.drawWidgets();
Bangle.setLCDPower(1);// turns screen on
}};

View File

@ -3,3 +3,4 @@
0.04: Make this clock do 12h and 24h
0.05: setUI, screen size changes
0.06: Use Bangle.setUI for button/launcher handling
0.07: Update *on* the minute rather than every 15 secs

View File

@ -1,4 +1,3 @@
/* jshint esversion: 6 */
const big = g.getWidth()>200;
const timeFontSize = big?6:5;
const dateFontSize = big?3:2;
@ -14,7 +13,19 @@ const yposGMT = xyCenter*1.9;
// Check settings for what type our clock should be
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
function drawSimpleClock() {
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
// get date
var d = new Date();
var da = d.toString().split(" ");
@ -60,11 +71,18 @@ function drawSimpleClock() {
var gmt = da[5];
g.setFont(font, gmtFontSize);
g.drawString(gmt, xyCenter, yposGMT, true);
queueDraw();
}
// handle switch display on by pressing BTN1
Bangle.on('lcdPower', function(on) {
if (on) drawSimpleClock();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// clean app screen
@ -74,8 +92,5 @@ Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
// refesh every 15 sec
setInterval(drawSimpleClock, 15E3);
// draw now
drawSimpleClock();
draw();

View File

@ -1,2 +1,3 @@
0.01: Modified for use with new bootloader and firmware
0.02: Use Bangle.setUI for button/launcher handling
0.03: Fix display for Bangle 2

View File

@ -1,14 +1,17 @@
const h = g.getHeight();
const w = g.getWidth();
function draw() {
var d = new Date();
var da = d.toString().split(" ");
var time = da[4].substr(0,5);
g.reset();
g.clearRect(0, 30, 239, 99);
g.clearRect(0, 30, w, 99);
g.setFontAlign(0, -1);
g.setFont("Vector", 80);
g.drawString(time, 120, 40);
g.setFont("Vector", w/3);
g.drawString(time, w/2, 40);
}
// handle switch display on by pressing BTN1

BIN
apps/stopwatch/A.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
apps/stopwatch/B.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

1
apps/stopwatch/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: first release

33
apps/stopwatch/README.md Normal file
View File

@ -0,0 +1,33 @@
# Stopwatch Touch
A touch screen based stop watch for Bangle 2
## Screenshots
![](screenshot1.png)
![](screenshot2.png)
![](screenshot3.png)
## Features
* Attractive UI design
* Will run up to 99 hours
* Shows 10th of seconds up to 1 hour
* Start / Pause button
* Reset button
## Future features
I'm keen to complete this project with
* Ability to dismiss the app and leave it running in the background
* A small widget to show the elapsed time on the current active clock
* Laptimes, with a way to view all the laptimes on a scrollable screen
## One of these is a genuine Bangle Js 2 Open Source Smartwatch, the other isn't
Which one is which ?
![](A.jpg)
![](B.jpg)

BIN
apps/stopwatch/pause-24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

BIN
apps/stopwatch/play-24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
apps/stopwatch/stop-24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

BIN
apps/stopwatch/stop-24a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1,220 @@
let w = g.getWidth();
let h = g.getHeight();
let tTotal = Date.now();
let tStart = tTotal;
let tCurrent = tTotal;
let running = false;
let timeY = 2*h/5;
let displayInterval;
let redrawButtons = true;
const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size
// 24 pixel images, scale to watch
// 1 bit optimal, image string, no E.toArrayBuffer()
const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP////////////////w==");
const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA=");
const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w==");
function log_debug(o) {
//console.log(o);
}
function timeToText(t) {
let hrs = Math.floor(t/3600000);
let mins = Math.floor(t/60000)%60;
let secs = Math.floor(t/1000)%60;
let tnth = Math.floor(t/100)%10;
let text;
if (hrs === 0)
text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
else
text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
//log_debug(text);
return text;
}
function drawButtons() {
log_debug("drawButtons()");
if (!running && tCurrent == tTotal) {
bigPlayPauseBtn.draw();
} else if (!running && tCurrent != tTotal) {
resetBtn.draw();
smallPlayPauseBtn.draw();
} else {
bigPlayPauseBtn.draw();
}
redrawButtons = false;
}
function drawTime() {
log_debug("drawTime()");
let Tt = tCurrent-tTotal;
let Ttxt = timeToText(Tt);
// total time
g.setFont("Vector",38); // check
g.setFontAlign(0,0);
g.clearRect(0, timeY - 21, w, timeY + 21);
g.setColor(g.theme.fg);
g.drawString(Ttxt, w/2, timeY);
}
function draw() {
let last = tCurrent;
if (running) tCurrent = Date.now();
g.setColor(g.theme.fg);
if (redrawButtons) drawButtons();
drawTime();
}
function startTimer() {
log_debug("startTimer()");
draw();
displayInterval = setInterval(draw, 100);
}
function stopTimer() {
log_debug("stopTimer()");
if (displayInterval) {
clearInterval(displayInterval);
displayInterval = undefined;
}
}
// BTN stop start
function stopStart() {
log_debug("stopStart()");
if (running)
stopTimer();
running = !running;
Bangle.buzz();
if (running)
tStart = Date.now() + tStart- tCurrent;
tTotal = Date.now() + tTotal - tCurrent;
tCurrent = Date.now();
setButtonImages();
redrawButtons = true;
if (running) {
startTimer();
} else {
draw();
}
}
function setButtonImages() {
if (running) {
bigPlayPauseBtn.setImage(pause_img);
smallPlayPauseBtn.setImage(pause_img);
resetBtn.setImage(reset_img);
} else {
bigPlayPauseBtn.setImage(play_img);
smallPlayPauseBtn.setImage(play_img);
resetBtn.setImage(reset_img);
}
}
// lap or reset
function lapReset() {
log_debug("lapReset()");
if (!running && tStart != tCurrent) {
redrawButtons = true;
Bangle.buzz();
tStart = tCurrent = tTotal = Date.now();
g.clearRect(0,24,w,h);
draw();
}
}
// simple on screen button class
function BUTTON(name,x,y,w,h,c,f,i) {
this.name = name;
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.color = c;
this.callback = f;
this.img = i;
}
BUTTON.prototype.setImage = function(i) {
this.img = i;
}
// if pressed the callback
BUTTON.prototype.check = function(x,y) {
//console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
log_debug(this.name + ":callback\n");
this.callback();
return true;
}
return false;
};
BUTTON.prototype.draw = function() {
g.setColor(this.color);
g.fillRect(this.x, this.y, this.x + this.w, this.y + this.h);
g.setColor("#000"); // the icons and boxes are drawn black
if (this.img != undefined) {
let iw = iconScale * 24; // the images were loaded as 24 pixels, we will scale
let ix = this.x + ((this.w - iw) /2);
let iy = this.y + ((this.h - iw) /2);
log_debug("g.drawImage(" + ix + "," + iy + "{scale: " + iconScale + "})");
g.drawImage(this.img, ix, iy, {scale: iconScale});
}
g.drawRect(this.x, this.y, this.x + this.w, this.y + this.h);
};
var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
bigPlayPauseBtn.setImage(play_img);
smallPlayPauseBtn.setImage(play_img);
resetBtn.setImage(pause_img);
Bangle.on('touch', function(button, xy) {
// not running, and reset
if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(xy.x, xy.y)) return;
// paused and hit play
if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(xy.x, xy.y)) return;
// paused and press reset
if (!running && tCurrent != tTotal && resetBtn.check(xy.x, xy.y)) return;
// must be running
if (running && bigPlayPauseBtn.check(xy.x, xy.y)) return;
});
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// Clear the screen once, at startup
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
// above not working, hence using next 2 lines
g.setColor("#000");
g.fillRect(0,0,w,h);
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();
Bangle.setUI("clock"); // Show launcher when button pressed

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///vvvvEF/muH+cDHgPABf4AElWoKhILClALH1WqAQIWHBYIABwAKEgQKD1WgBYkK1X1r4XHlWtqtVvQLG1XVBYNXHYsC1YJBBoPqC4kKEQILCvQ7EhW1BYdeBYkqytVqwCCQwkqCgILCq4LFIoILCqoLEIwIsBGQJIBBZ+pA4Na0oDBtQLGvSFCBaYjIHYR3CI5AADBYhrCAAaDHAASDGQASGCBYizCAASzFZYQACZYrjCIwb7QHgIkCvQ6EGAWq+tf1QuEGAWqAAQuFEgQKBEQw9DHIwAuA="));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,3 +1,4 @@
0.01: Modification of SimpleClock 0.04 to use Vectorfont
0.02: Use Bangle.setUI for button/launcher handling
0.03: Scale to BangleJS 2 and add locale
0.04: Fix rendering issue on real hardware, now update *on* the minute rather than every 15 secs

View File

@ -1,4 +1,3 @@
/* jshint esversion: 6 */
const locale = require("locale");
var timeFontSize;
@ -12,8 +11,7 @@ var yposDate;
var yposYear;
var yposGMT;
switch (process.env.BOARD) {
case "EMSCRIPTEN":
if (g.getWidth() > 200) {
timeFontSize = 65;
dateFontSize = 20;
gmtFontSize = 10;
@ -22,8 +20,7 @@ switch (process.env.BOARD) {
yposDate = 130;
yposYear = 175;
yposGMT = 220;
break;
case "EMSCRIPTEN2":
} else {
timeFontSize = 48;
dateFontSize = 15;
gmtFontSize = 10;
@ -32,12 +29,23 @@ switch (process.env.BOARD) {
yposDate = 95;
yposYear = 128;
yposGMT = 161;
break;
}
// Check settings for what type our clock should be
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
function drawSimpleClock() {
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
g.clear();
Bangle.drawWidgets();
@ -76,23 +84,26 @@ function drawSimpleClock() {
// draw gmt
g.setFont(font, gmtFontSize);
g.drawString(d.toString().match(/GMT[+-]\d+/), xyCenter, yposGMT, true);
queueDraw();
}
// handle switch display on by pressing BTN1
Bangle.on('lcdPower', function(on) {
if (on) drawSimpleClock();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// Show launcher when button pressed
Bangle.setUI("clock");
// clean app screen
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
// refesh every 15 sec
setInterval(drawSimpleClock, 15E3);
// draw now
drawSimpleClock();
// Show launcher when button pressed
Bangle.setUI("clock");
draw();

View File

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///9nou30h/qJf8Ah/wBasK0ALhHcMBqALBgtABYsVqgLBAYILFqtUF4MVqoKEgoLFqALGEYQLFAwILEGAlV6tUlWoitXGAgLC1WqBYsBq+VqwLBAYPVMIUFBYN61Uq1oLBHgQLC1WohWqrwLDiteEIOggQDB2pICivqA4IFBlWq1YLCq/V9WkAoMa1YHBKQVf9XUAoMX1f1KgVVEYIpDEYILBLwIuBC4YFBMAMBLAILFLQILBrdV12UBYMW3VV8tAgt9q+2BYee6t9qEFuoLHroLBqte6wvDy+1ZoILBvdWSoeV9oLD+tfBYYFBBYTEBrq5CgN1aQNQgNVAALdEAAISBAYL1EfgISCAgIKDDAQSEAH4AQ"))

View File

@ -0,0 +1,256 @@
// get settings
var settings = require("Storage").readJSON("vernierrespirate.json",1)||{};
settings.vibrateBPM = settings.vibrateBPM||27;
// settings.vibrate; // undefined / "calculated" / "vernier"
function saveSettings() {
require("Storage").writeJSON("vernierrespirate.json", settings);
}
g.clear();
var graphHeight = g.getHeight()-100;
var last = {
time : Date.now(),
x : 0,
y : 24,
};
var avrValue;
var aboveAvr = false;
var lastBreath;
var lastBreaths = [];
var vibrateInterval;
function onMsg(txt) {
print(txt);
E.showMessage(txt);
}
function setVibrate(isOn) {
var wasOn = vibrateInterval!==undefined;
if (isOn == wasOn) return;
if (isOn) {
vibrateInterval = setInterval(function() {
Bangle.buzz();
}, 1000);
} else {
clearInterval(vibrateInterval);
vibrateInterval = undefined;
}
}
function onBreath() {
var t = Date.now();
if (lastBreath!==undefined) {
// time between breaths
var value = 60000 / (t-lastBreath);
// average of last 3
while (lastBreaths.length>=3) lastBreaths.shift(); // keep length small
lastBreaths.push(value);
value = E.sum(lastBreaths) / lastBreaths.length;
// draw value
g.reset();
g.clearRect(0,g.getHeight()-100,g.getWidth(),g.getHeight()-50);
g.setFont("6x8").setFontAlign(0,0);
g.drawString("Calculated measurement", g.getWidth()/2, g.getHeight()-95);
g.setFont("Vector",40).setFontAlign(0,0);
g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-70);
// set vibration IF we're doing it from our calculations
if (settings.vibrate == "calculated")
setVibrate(value > settings.vibrateBPM);
}
lastBreath = t;
}
function onData(n, value) {
g.reset();
if (n==2) {
function scale(v) {
return Math.max(graphHeight - (1+v*4),24);
}
if (avrValue==undefined) avrValue=value;
avrValue = avrValue*0.95 + value*0.05;
if (avrValue < 1) avrValue = 1;
if (value > avrValue) {
if (!aboveAvr) onBreath();
aboveAvr = true;
} else aboveAvr = false;
var t = Date.now();
var x = Math.round((t - last.time) / 100) // 10 per second
if (last.x>=g.getWidth()) {
x = 0;
last.x = 0;
last.time = t;
g.clearRect(0,24,g.getWidth(),graphHeight);
}
var y = scale(value);
g.setPixel(x, scale(avrValue), "#f00");
g.drawLine(last.x, last.y, x, y);
last.x = x;
last.y = y;
}
if (n==4) {
g.clearRect(0,g.getHeight()-50,g.getWidth(),g.getHeight());
g.setFont("6x8").setFontAlign(0,0);
g.drawString("GoDirect measurement", g.getWidth()/2, g.getHeight()-45);
g.setFont("Vector",40).setFontAlign(0,0);
g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-20);
// set vibration IF we're doing it from our calculations
if (settings.vibrate == "vernier")
setVibrate(value > settings.vibrateBPM);
}
Bangle.setLCDPower(1); // ensure LCD is on
}
function connect() {
var gatt, service, rx, tx;
var rollingCounter = 0xFF;
// any button to exit
Bangle.setUI("updown", function() {
setVibrate(false);
Bangle.buzz();
try {
if (gatt) gatt.disconnect();
} catch (e) {
}
setTimeout(mainMenu, 1000);
});
function sendCommand(subCommand) {
const command = new Uint8Array(4 + subCommand.length);
command.set(new Uint8Array(subCommand), 4);
// Populate the packet header bytes
command[0] = 0x58; // header
command[1] = command.length;
command[2] = --rollingCounter;
command[3] = E.sum(command) & 0xFF; // checksum
return tx.writeValue(command);
}
function firstSetBit(v) {
return v & -v;
}
function handleResponse(dv) {
//print(dv.buffer);
var resType = dv.getUint8(0);
if (resType==0x20) {
// [32, 25, 207, 216, 6, 6, 0, 2, 252, 128, 138, 7, 191, 0, 0, 192, 127, 128, 49, 8, 191, 0, 0, 192, 127])
// 6 = data type = real
// 6,0 = bit mask for sensors
// 2 = value count
if (dv.getUint8(4)!=6) return; //throw "Not float32 data";
var sensorIds = dv.getUint16(5, true);
// var count = dv.getUint8(7); doesn't seem right
var offs = 9;
while (sensorIds) {
var value = dv.getFloat32(offs, true);
var s = firstSetBit(sensorIds);
if (isFinite(value)) onData(s,value);
//else print(s,value);
sensorIds &= ~s;
offs += 4;
}
} else {
var cmd = dv.getUint8(4); // cmd
//print("CMD",dv.buffer);
}
}
onMsg("Searching...");
NRF.requestDevice({ filters: [{ namePrefix: 'GDX-RB' }] }).then(function(device) {
device.on("gattserverdisconnected", function() {
onMsg("Device disconnected");
});
onMsg("Found. Connecting...");
return device.gatt.connect({minInterval:20, maxInterval:20});
}).then(function(g) {
gatt = g;
return gatt.getPrimaryService("d91714ef-28b9-4f91-ba16-f0d9a604f112");
}).then(function(s) {
service = s;
return service.getCharacteristic("f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb");
}).then(function(c) {
tx = c;
return service.getCharacteristic("b41e6675-a329-40e0-aa01-44d2f444babe");
}).then(function(c) {
rx = c;
rx.on('characteristicvaluechanged', function(event) {
//print("EVT",event.target.value.buffer);
handleResponse(event.target.value);
});
return rx.startNotifications();
}).then(function() {
onMsg("Init");
sendCommand([ // init
0x1a, 0xa5, 0x4a, 0x06,
0x49, 0x07, 0x48, 0x08,
0x47, 0x09, 0x46, 0x0a,
0x45, 0x0b, 0x44, 0x0c,
0x43, 0x0d, 0x42, 0x0e,
0x41,
]);
/*setTimeout(function() {
print("Set measurement period");
var us = 100000; // period in us
sendCommand([0x1b, 0xff, 0x00,
us & 255,
(us >> 8) & 255,
(us >> 16) & 255,
(us >> 24) & 255,
0x00,
0x00,
0x00,
0x00]);
}, 100);*/
/* setTimeout(function() {
print("Get sensor info");
sendCommand([0x51, 0]); // get sensor IDs
// returns [152, 10, 1, 39, 81, 253, 54, 0, 0, 0]
// 54 is the bit mask of available channels
//sendCommand([106, 16]); // get sensor info
}, 2000);*/
setTimeout(function() {
onMsg("Start measurements");
//https://github.com/VernierST/godirect-js/blob/main/src/Device.js#L588
var channels = 6; // data channels 4 and 2
sendCommand([ // start measurements
0x18, 0xff, 0x01, channels,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00
]);
}, 500);
}).catch(function() {
onMsg("Connect Fail");
});
}
Bangle.loadWidgets();
Bangle.drawWidgets();
function mainMenu() {
var vibText = ["No","Calculated","Vernier"];
var vibValue = ["","calculated","vernier"];
E.showMenu({"":{title:"Respiration Belt"},
"< Back" : () => { saveSettings(); load(); },
"Connect" : () => { saveSettings(); E.showMenu(); connect(); },
"Vib" : {
value : Math.max(vibValue.indexOf(settings.vibrate),0),
format : v => vibText[v],
min:0,max:2,
onchange : v => { settings.vibrate=vibValue[v]; }
},
"BPM" : {
value : settings.vibrateBPM,
min:10,max:50,
onchange : v => { settings.vibrateBPM=v; }
}
});
}
mainMenu();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -5,3 +5,4 @@
0.06: Use 'g.theme' (requires bootloader 0.23)
0.07: Move CHARGING variable to more readable string
0.08: Ensure battery updates every 60s even if LCD was on at boot and stays on
0.09: Misc speed/memory tweaks

View File

@ -1,26 +1,11 @@
(function(){
function setWidth() {
WIDGETS["bat"].width = 40 + (Bangle.isCharging()?16:0);
}
function draw() {
var s = 39;
var x = this.x, y = this.y;
g.reset();
if (Bangle.isCharging()) {
g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
x+=16;
}
g.setColor(g.theme.fg);
g.fillRect(x,y+2,x+s-4,y+21);
g.clearRect(x+2,y+4,x+s-6,y+19);
g.fillRect(x+s-3,y+10,x+s,y+14);
g.setColor("#0f0").fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17);
}
Bangle.on('charging',function(charging) {
if(charging) Bangle.buzz();
setWidth();
Bangle.drawWidgets(); // relayout widgets
Bangle.drawWidgets(); // re-layout widgets
g.flip();
});
var batteryInterval = Bangle.isLCDOn() ? setInterval(()=>WIDGETS["bat"].draw(), 60000) : undefined;
@ -37,6 +22,16 @@
}
}
});
WIDGETS["bat"]={area:"tr",width:40,draw:draw};
WIDGETS["bat"]={area:"tr",width:40,draw:function() {
var s = 39;
var x = this.x, y = this.y;
g.reset();
if (Bangle.isCharging()) {
g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
x+=16;
}
g.setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14);
g.setColor("#0f0").fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17);
}};
setWidth();
})()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 280 B

1
apps/widbatv/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New widget

19
apps/widbatv/widget.js Normal file
View File

@ -0,0 +1,19 @@
Bangle.on('charging',function(charging) {
if(charging) Bangle.buzz();
WIDGETS["batv"].draw();
});
setInterval(()=>WIDGETS["batv"].draw(), 60000);
Bangle.on('lcdPower', function(on) {
if (on) WIDGETS["batv"].draw(); // refresh at power on
});
WIDGETS["batv"]={area:"tr",width:14,draw:function() {
var x = this.x, y = this.y;
g.reset();
if (Bangle.isCharging()) {
g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
} else {
g.clearRect(x,y,x+14,y+24);
g.setColor(g.theme.fg).fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2);
g.setColor("#0f0").fillRect(x+4,y+20-(E.getBattery()*16/100),x+10,y+20);
}
}};

BIN
apps/widbatv/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

View File

@ -3,3 +3,4 @@
0.04: Fix automatic update of Bluetooth connection status
0.05: Make Bluetooth widget thinner, and when on a bright theme use light grey for disabled color
0.06: Tweaking colors for dark/light themes and low bpp screens
0.07: Memory usage improvements

View File

@ -1,17 +1,13 @@
(function(){
function draw() {
WIDGETS["bluetooth"]={area:"tr",width:15,draw:function() {
g.reset();
if (NRF.getSecurityStatus().connected)
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
else
g.setColor(g.theme.dark ? "#666" : "#999");
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y);
}
function changed() {
},changed:function() {
WIDGETS["bluetooth"].draw();
g.flip();// turns screen on
}
NRF.on('connect',changed);
NRF.on('disconnect',changed);
WIDGETS["bluetooth"]={area:"tr",width:15,draw:draw};
})()
Bangle.setLCDPower(1); // turn screen on
}};
NRF.on('connect',WIDGETS["bluetooth"].changed);
NRF.on('disconnect',WIDGETS["bluetooth"].changed);

3
apps/widcom/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.02: Works with light theme
Doesn't drain battery by updating every 2 secs
Fix alignment

View File

@ -1,30 +1,17 @@
(function(){
//var img = E.toArrayBuffer(atob("FBSBAAAAAAAAA/wAf+AP/wH/2D/zw/w8PwfD9nw+b8Pg/Dw/w8/8G/+A//AH/gA/wAAAAAAA"));
//var img = E.toArrayBuffer(atob("GBiBAAB+AAP/wAeB4A4AcBgAGDAADHAADmABhmAHhsAfA8A/A8BmA8BmA8D8A8D4A2HgBmGABnAADjAADBgAGA4AcAeB4AP/wAB+AA=="));
var img = E.toArrayBuffer(atob("FBSBAAH4AH/gHAODgBwwAMYABkAMLAPDwPg8CYPBkDwfA8PANDACYABjAAw4AcHAOAf+AB+A"));
var cp = Bangle.setCompassPower;
Bangle.setCompassPower = () => {
cp.apply(Bangle, arguments);
WIDGETS.compass.draw();
};
function draw() {
WIDGETS.compass={area:"tr",width:24,draw:function() {
g.reset();
if (Bangle.isCompassOn()) {
g.setColor(1,0.8,0); // on = amber
g.setColor(g.theme.dark ? "#FC0" : "#F00");
} else {
g.setColor(0.3,0.3,0.3); // off = grey
g.setColor(g.theme.dark ? "#333" : "#CCC");
}
g.drawImage(img, 10+this.x, 2+this.var);
}
var timerInterval;
Bangle.on('lcdPower', function(on) {
if (on) {
WIDGETS.compass.draw();
if (!timerInterval) timerInterval = setInterval(()=>WIDGETS.compass.draw(), 2000);
} else {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = undefined;
}
}
});
WIDGETS.compass={area:"tr",width:24,draw:draw};
g.drawImage(atob("FBSBAAH4AH/gHAODgBwwAMYABkAMLAPDwPg8CYPBkDwfA8PANDACYABjAAw4AcHAOAf+AB+A"), 2+this.x, 2+this.var);
}};
})();

View File

@ -2,3 +2,4 @@
0.02: Tweaks for variable size widget system
0.03: Ensure redrawing works with variable size widget system
0.04: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799)
0.05: Use new 'lock' event, not LCD (so it works on Bangle.js 2)

View File

@ -1,13 +1,38 @@
(() => {
var currentBPM = undefined;
var lastBPM = undefined;
var firstBPM = true; // first reading since sensor turned on
if (!Bangle.isLocked) return; // old firmware
var currentBPM;
var lastBPM;
var isHRMOn = false;
function draw() {
// turn on sensor when the LCD is unlocked
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
Bangle.setHRMPower(1,"widhrm");
currentBPM = undefined;
WIDGETS["hrm"].draw();
} else {
Bangle.setHRMPower(0,"widhrm");
}
});
var hp = Bangle.setHRMPower;
Bangle.setHRMPower = () => {
hp.apply(Bangle, arguments);
isHRMOn = Bangle.isHRMOn();
WIDGETS["hrm"].draw();
};
Bangle.on('HRM',function(d) {
currentBPM = d.bpm;
lastBPM = currentBPM;
WIDGETS["hrm"].draw();
});
// add your widget
WIDGETS["hrm"]={area:"tl",width:24,draw:function() {
var width = 24;
g.reset();
g.setFont("6x8", 1);
g.setFontAlign(0, 0);
g.setFont("6x8", 1).setFontAlign(0, 0);
g.clearRect(this.x,this.y+15,this.x+width,this.y+23); // erase background
var bpm = currentBPM, isCurrent = true;
if (bpm===undefined) {
@ -16,36 +41,12 @@
}
if (bpm===undefined)
bpm = "--";
g.setColor(isCurrent ? "#ffffff" : "#808080");
g.setColor(isCurrent ? g.theme.fg : "#808080");
g.drawString(bpm, this.x+width/2, this.y+19);
g.setColor(isCurrent ? "#ff0033" : "#808080");
g.setColor(isHRMOn ? "#ff0033" : "#808080");
g.drawImage(atob("CgoCAAABpaQ//9v//r//5//9L//A/+AC+AAFAA=="),this.x+(width-10)/2,this.y+1);
g.setColor(-1);
}
}};
// redraw when the LCD turns on
Bangle.on('lcdPower', function(on) {
if (on) {
Bangle.setHRMPower(1,"widhrm");
firstBPM = true;
currentBPM = undefined;
WIDGETS["hrm"].draw();
} else {
Bangle.setHRMPower(0,"widhrm");
}
});
Bangle.on('HRM',function(d) {
if (firstBPM)
firstBPM=false; // ignore the first one as it's usually rubbish
else {
currentBPM = d.bpm;
lastBPM = currentBPM;
}
WIDGETS["hrm"].draw();
});
Bangle.setHRMPower(Bangle.isLCDOn(),"widhrm");
// add your widget
WIDGETS["hrm"]={area:"tl",width:24,draw:draw};
Bangle.setHRMPower(!Bangle.isLocked(),"widhrm");
})();

View File

@ -1,3 +1,5 @@
0.01: First version
0.02: Don't break if running on 2v08 firmware (just don't display anything)
0.03: Works with light theme
Doesn't drain battery by updating every 2 secs
fix alignment

View File

@ -1,28 +1,18 @@
(function(){
if (!Bangle.isHRMOn) return; // old firmware
var hp = Bangle.setHRMPower;
Bangle.setHRMPower = () => {
hp.apply(Bangle, arguments);
WIDGETS.widhrt.draw();
};
function draw() {
WIDGETS.widhrt={area:"tr",width:24,draw:function() {
g.reset();
if (Bangle.isHRMOn()) {
g.setColor(1,0,0); // on = red
g.setColor("#f00"); // on = red
} else {
g.setColor(0.3,0.3,0.3); // off = grey
g.setColor(g.theme.dark ? "#333" : "#CCC"); // off = grey
}
g.drawImage(atob("FhaBAAAAAAAAAAAAAcDgD8/AYeGDAwMMDAwwADDAAMOABwYAGAwAwBgGADAwAGGAAMwAAeAAAwAAAAAAAAAAAAA="), 10+this.x, 2+this.y);
}
var timerInterval;
Bangle.on('lcdPower', function(on) {
if (on) {
WIDGETS.widhrt.draw();
if (!timerInterval) timerInterval = setInterval(()=>WIDGETS["widhrt"].draw(), 2000);
} else {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = undefined;
}
}
});
WIDGETS.widhrt={area:"tr",width:24,draw:draw};
g.drawImage(atob("FhaBAAAAAAAAAAAAAcDgD8/AYeGDAwMMDAwwADDAAMOABwYAGAwAwBgGADAwAGGAAMwAAeAAAwAAAAAAAAAAAAA="), 1+this.x, 1+this.y);
}};
})();

View File

@ -1 +1,2 @@
0.01: New Widget!
0.02: Theme support, memory savings

View File

@ -1,17 +1,11 @@
/* jshint esversion: 6 */
(() => {
const CBS = 0x41f, CBC = 0x07E0;
var xo = 6, xl = 22, yo = 9, h = 17;
function draw() {
g.reset().setColor(CBS).drawImage(require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE")), this.x + 1, this.y + 4);
g.setColor(0).fillRect(this.x + xo, this.y + yo, this.x + xl, this.y + h);
var cbc = (Bangle.isCharging()) ? CBC : CBS;
g.setColor(cbc).fillRect(this.x + xo, this.y + yo, this.x + (xl - xo) / 100 * E.getBattery() + xo, this.y + h);
}
Bangle.on('charging', function(charging) {
Bangle.on('charging', function(charging) {
if (charging) Bangle.buzz();
Bangle.drawWidgets();
});
WIDGETS["widtbat"] = { area:"tr", width:32, draw: draw };
})();
WIDGETS["widtbat"].draw();
});
WIDGETS["widtbat"] = { area:"tr", width:32, draw: function() {
const xo = 6, xl = 22, yo = 9, h = 17;
g.reset().setColor("#08f").drawImage(require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE")), this.x + 1, this.y + 4);
g.clearRect(this.x + xo, this.y + yo, this.x + xl, this.y + h);
var cbc = (Bangle.isCharging()) ? "#0f0" : "#08f";
g.setColor(cbc).fillRect(this.x + xo, this.y + yo, this.x + (xl - xo) / 100 * E.getBattery() + xo, this.y + h);
} };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 238 B

View File

@ -2,3 +2,5 @@
0.02: Update custom.html for refactor; add README
0.03: Update for larger secondary timezone display (#610)
0.04: setUI, different screen sizes
0.05: Now update *on* the minute rather than every 15 secs
Fix rendering of single extra timezone on Bangle.js 2

View File

@ -1,5 +1,3 @@
/* jshint esversion: 6 */
const big = g.getWidth()>200;
// Font for primary time and date
const primaryTimeFontSize = big?6:5;
@ -16,8 +14,13 @@ const xcol2 = g.getWidth() - xcol1;
const font = "6x8";
/* TODO: we could totally use 'Layout' here and
avoid a whole bunch of hard-coded offsets */
const xyCenter = g.getWidth() / 2;
const yposTime = big ? 75 : 60;
const yposTime2 = yposTime + (big ? 100 : 60);
const yposDate = big ? 130 : 90;
const yposWorld = big ? 170 : 120;
@ -29,41 +32,52 @@ var offsets = require("Storage").readJSON("worldclock.settings.json") || [];
// TESTING CODE
// Used to test offset array values during development.
// Uncomment to override secondary offsets value
// const mockOffsets = {
// zeroOffsets: [],
// oneOffset: [["UTC", 0]],
// twoOffsets: [
// ["Tokyo", 9],
// ["UTC", 0],
// ],
// fourOffsets: [
// ["Tokyo", 9],
// ["UTC", 0],
// ["Denver", -7],
// ["Miami", -5],
// ],
// fiveOffsets: [
// ["Tokyo", 9],
// ["UTC", 0],
// ["Denver", -7],
// ["Chicago", -6],
// ["Miami", -5],
// ],
// };
/*
const mockOffsets = {
zeroOffsets: [],
oneOffset: [["UTC", 0]],
twoOffsets: [
["Tokyo", 9],
["UTC", 0],
],
fourOffsets: [
["Tokyo", 9],
["UTC", 0],
["Denver", -7],
["Miami", -5],
],
fiveOffsets: [
["Tokyo", 9],
["UTC", 0],
["Denver", -7],
["Chicago", -6],
["Miami", -5],
],
};*/
// Uncomment one at a time to test various offsets array scenarios
// offsets = mockOffsets.zeroOffsets; // should render nothing below primary time
// offsets = mockOffsets.oneOffset; // should render larger in two rows
// offsets = mockOffsets.twoOffsets; // should render two in columns
// offsets = mockOffsets.fourOffsets; // should render in columns
// offsets = mockOffsets.fiveOffsets; // should render first four in columns
//offsets = mockOffsets.zeroOffsets; // should render nothing below primary time
//offsets = mockOffsets.oneOffset; // should render larger in two rows
//offsets = mockOffsets.twoOffsets; // should render two in columns
//offsets = mockOffsets.fourOffsets; // should render in columns
//offsets = mockOffsets.fiveOffsets; // should render first four in columns
// END TESTING CODE
// Check settings for what type our clock should be
//var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
var secondInterval;
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function doublenum(x) {
return x < 10 ? "0" + x : "" + x;
@ -73,7 +87,7 @@ function getCurrentTimeFromOffset(dt, offset) {
return new Date(dt.getTime() + offset * 60 * 60 * 1000);
}
function drawSimpleClock() {
function draw() {
// get date
var d = new Date();
var da = d.toString().split(" ");
@ -111,9 +125,9 @@ function drawSimpleClock() {
// For a single secondary timezone, draw it bigger and drop time zone to second line
const xOffset = 30;
g.setFont(font, secondaryTimeFontSize);
g.drawString(`${hours}:${minutes}`, xyCenter, yposTime + 100, true);
g.drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true);
g.setFont(font, secondaryTimeZoneFontSize);
g.drawString(offset[OFFSET_TIME_ZONE], xyCenter, yposTime + 130, true);
g.drawString(offset[OFFSET_TIME_ZONE], xyCenter, yposTime2 + 30, true);
// draw Day, name of month, Date
g.setFont(font, secondaryTimeZoneFontSize);
@ -132,6 +146,8 @@ function drawSimpleClock() {
g.drawString(`${hours}:${minutes}`, xcol2, yposWorld + index * 15, true);
}
});
queueDraw();
}
// clean app screen
@ -141,18 +157,15 @@ Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
// refesh every 15 sec when screen is on
Bangle.on("lcdPower", (on) => {
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
secondInterval = setInterval(drawSimpleClock, 15e3);
drawSimpleClock(); // draw immediately
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// draw now and every 15 sec until display goes off
drawSimpleClock();
if (Bangle.isLCDOn()) {
secondInterval = setInterval(drawSimpleClock, 15e3);
}
// draw now
draw();

View File

@ -0,0 +1,83 @@
#!/usr/bin/nodejs
/* Quick hack to add proper 'supports' field to apps.json
*/
var fs = require("fs");
var BASEDIR = __dirname+"/../";
var appsFile, apps;
try {
appsFile = fs.readFileSync(BASEDIR+"apps.json").toString();
} catch (e) {
ERROR("apps.json not found");
}
try{
apps = JSON.parse(appsFile);
} catch (e) {
console.log(e);
var m = e.toString().match(/in JSON at position (\d+)/);
if (m) {
var char = parseInt(m[1]);
console.log("===============================================");
console.log("LINE "+appsFile.substr(0,char).split("\n").length);
console.log("===============================================");
console.log(appsFile.substr(char-10, 20));
console.log("===============================================");
}
console.log(m);
ERROR("apps.json not valid JSON");
}
apps = apps.map((app,appIdx) => {
var tags = [];
if (app.tags) tags = app.tags.split(",").map(t=>t.trim());
var supportsB1 = true;
var supportsB2 = false;
if (tags.includes("b2")) {
tags = tags.filter(x=>x!="b2");
supportsB2 = true;
}
if (tags.includes("bno2")) {
tags = tags.filter(x=>x!="bno2");
supportsB2 = false;
}
if (tags.includes("bno1")) {
tags = tags.filter(x=>x!="bno1");
supportsB1 = false;
}
app.tags = tags.join(",");
app.supports = [];
if (supportsB1) app.supports.push("BANGLEJS");
if (supportsB2) app.supports.push("BANGLEJS2");
return app;
});
var KEY_ORDER = [
"id","name","shortName","version","description","icon","type","tags","supports",
"dependencies", "readme", "custom", "customConnect", "interface",
"allow_emulator", "storage", "data", "sortorder"
];
var JS = JSON.stringify;
var json = "[\n "+apps.map(app=>{
var keys = KEY_ORDER.filter(k=>k in app);
Object.keys(app).forEach(k=>{
if (!KEY_ORDER.includes(k))
throw new Error(`Key named ${k} not known!`);
});
return "{\n "+keys.map(k=>{
var js = JS(app[k]);
if (k=="storage")
js = "[\n "+app.storage.map(s=>JS(s)).join(",\n ")+"\n ]";
return JS(k)+": "+js;
}).join(",\n ")+"\n }";
}).join(",\n ")+"\n]\n";
//console.log(json);
console.log("new apps.json written");
fs.writeFileSync(BASEDIR+"apps.json", json);

View File

@ -51,7 +51,8 @@ try{
const APP_KEYS = [
'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type',
'sortorder', 'readme', 'custom', 'customConnect', 'interface', 'storage', 'data', 'allow_emulator',
'sortorder', 'readme', 'custom', 'customConnect', 'interface', 'storage', 'data',
'supports', 'allow_emulator',
'dependencies'
];
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite'];
@ -81,6 +82,14 @@ apps.forEach((app,appIdx) => {
if (!app.name) ERROR(`App ${app.id} has no name`);
var isApp = !app.type || app.type=="app";
if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`);
if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`);
else {
app.supports.forEach(dev => {
if (!["BANGLEJS","BANGLEJS2"].includes(dev))
ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`);
});
}
if (!app.version) WARN(`App ${app.id} has no version`);
else {
if (!fs.existsSync(appDir+"ChangeLog")) {

2
core

@ -1 +1 @@
Subproject commit 0fd608f085deff9b39f2db3559ecc88edb232aba
Subproject commit 3a2c706b4cdf02e5365b191103c80d587b3ace5a

View File

@ -23,6 +23,9 @@
.filter-nav {
display: inline-block;
}
.device-nav {
display: inline-block;
}
.sort-nav {
float: right;
}
@ -32,6 +35,21 @@
top: 36px;
left: -24px;
}
.btn-favourite {
color: red;
.btn.btn-favourite { color: red; }
.btn.btn-favourite:hover { color: red; }
.icon.icon-emulator { text-indent: 0px; } /*override spectre*/
.icon.icon-emulator::before {
content: "\01F5B5";
}
.icon.icon-favourite { text-indent: 0px; } /*override spectre*/
.icon.icon-favourite::before {
content: "\02661"; /* 0x2661 = empty heart; 0x2606 = empty star */
}
.icon.icon-favourite-active::before {
content: "\02665"; /* 0x2665 = solid heart; 0x2605 = solid star */
}
.icon.icon-interface {text-indent: 0px;font-size: 130%;vertical-align: -30%;} /*override spectre*/
.icon.icon-interface::before {
content: "\01F5AB";
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
css/spectre.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
["boot","launch","s7clk","setting","about","widbat","widbt","widlock","widid"]

View File

@ -60,6 +60,17 @@
<div class="container apploader-tab" id="librarycontainer">
<div>
<div class="dropdown devicetype-nav">
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0">
<span>All apps</span><i class="icon icon-caret"></i>
</a>
<!-- menu component -->
<ul class="menu">
<li class="menu-item"><a>All apps</a></li>
<li class="menu-item"><a dt="BANGLEJS">Bangle.js 1</a></li>
<li class="menu-item"><a dt="BANGLEJS2">Bangle.js 2</a></li>
</ul>
</div>
<div class="filter-nav">
<label class="chip active" filterid="">Default</label>
<label class="chip" filterid="clock">Clocks</label>

127
loader.js
View File

@ -14,6 +14,10 @@ if (window.location.host=="banglejs.com") {
var RECOMMENDED_VERSION = "2v10";
// could check http://www.espruino.com/json/BANGLEJS.json for this
// We're only interested in Bangles
DEVICEINFO = DEVICEINFO.filter(x=>x.id.startsWith("BANGLEJS"));
// Set up source code URL
(function() {
let username = "espruino";
let githubMatch = window.location.href.match(/\/(\w+)\.github\.io/);
@ -21,6 +25,7 @@ var RECOMMENDED_VERSION = "2v10";
Const.APP_SOURCECODE_URL = `https://github.com/${username}/BangleApps/tree/master/apps`;
})();
// When a device is found, filter the apps accordingly
function onFoundDeviceInfo(deviceId, deviceVersion) {
if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") {
showToast(`You're using ${deviceId}, not a Bangle.js. Did you want <a href="https://espruino.com/apps">espruino.com/apps</a> instead?` ,"warning", 20000);
@ -33,4 +38,126 @@ function onFoundDeviceInfo(deviceId, deviceVersion) {
if (deviceId == "BANGLEJS2") {
Const.MESSAGE_RELOAD = 'Hold button\nto reload';
}
// check against features shown?
filterAppsForDevice(deviceId);
/* if we'd saved a device ID but this device is different, ensure
we ask again next time */
var savedDeviceId = getSavedDeviceId();
if (savedDeviceId!==undefined && savedDeviceId!=deviceId)
setSavedDeviceId(undefined);
}
var originalAppJSON = undefined;
function filterAppsForDevice(deviceId) {
if (originalAppJSON===undefined)
originalAppJSON = appJSON;
var device = DEVICEINFO.find(d=>d.id==deviceId);
// set the device dropdown
document.querySelector(".devicetype-nav span").innerText = device ? device.name : "All apps";
if (!device) {
if (deviceId!==undefined)
showToast(`Device ID ${deviceId} not recognised. Some apps may not work`, "warning");
appJSON = originalAppJSON;
} else {
// Now filter apps
appJSON = originalAppJSON.filter(app => {
var supported = ["BANGLEJS"];
if (!app.supports) {
console.log(`App ${app.id} doesn't include a 'supports' field - ignoring`);
return false;
}
if (app.supports.includes(deviceId)) return true;
//console.log(`Dropping ${app.id} because ${deviceId} is not in supported list ${app.supports.join(",")}`);
return false;
});
}
refreshLibrary();
}
// If 'remember' was checked in the window below, this is the device
function getSavedDeviceId() {
let deviceId = localStorage.getItem("deviceId");
if (("string"==typeof deviceId) && DEVICEINFO.find(d=>d.id == deviceId))
return deviceId;
return undefined;
}
function setSavedDeviceId(deviceId) {
localStorage.setItem("deviceId", deviceId);
}
// At boot, show a window to choose which type of device you have...
window.addEventListener('load', (event) => {
let deviceId = getSavedDeviceId()
if (deviceId !== undefined)
return filterAppsForDevice(deviceId);
var html = `<div class="columns">
${DEVICEINFO.map(d=>`
<div class="column col-6 col-xs-6">
<div class="card devicechooser" deviceid="${d.id}" style="cursor:pointer">
<div class="card-header">
<div class="card-title h5">${d.name}</div>
<!--<div class="card-subtitle text-gray">...</div>-->
</div>
<div class="card-image">
<img src="${d.img}" alt="${d.name}" width="256" height="256" class="img-responsive">
</div>
</div>
</div>`).join("\n")}
</div><div class="columns">
<div class="column col-12">
<div class="form-group">
<label class="form-switch">
<input type="checkbox" id="remember_device">
<i class="form-icon"></i> Don't ask again
</label>
</div>
</div>
</div>`;
showPrompt("Which Bangle.js?",html,{},false);
htmlToArray(document.querySelectorAll(".devicechooser")).forEach(button => {
button.addEventListener("click",event => {
let rememberDevice = document.getElementById("remember_device").checked;
let button = event.currentTarget;
let deviceId = button.getAttribute("deviceid");
hidePrompt();
console.log("Chosen device", deviceId);
setSavedDeviceId(rememberDevice ? deviceId : undefined);
filterAppsForDevice(deviceId);
});
});
});
window.addEventListener('load', (event) => {
// Hook onto device chooser dropdown
htmlToArray(document.querySelectorAll(".devicetype-nav .menu-item")).forEach(button => {
button.addEventListener("click", event => {
var a = event.target;
var deviceId = a.getAttribute("dt")||undefined;
filterAppsForDevice(deviceId); // also sets the device dropdown
setSavedDeviceId(undefined); // ask at startup next time
document.querySelector(".devicetype-nav span").innerText = a.innerText;
});
});
// Button to install all default apps in one go
document.getElementById("installdefault").addEventListener("click",event=>{
getInstalledApps().then(() => {
if (device.id == "BANGLEJS")
return httpGet("defaultapps_banglejs.json");
if (device.id == "BANGLEJS2")
return httpGet("defaultapps_banglejs2.json");
throw new Error("Unknown device "+device.id);
}).then(json=>{
return installMultipleApps(JSON.parse(json), "default");
}).catch(err=>{
Progress.hide({sticky:true});
showToast("App Install failed, "+err,"error");
});
});
});

View File

@ -169,14 +169,23 @@ function Layout(layout, options) {
Bangle.on('touch',Bangle.touchHandler);
}
// add IDs
// recurse over layout doing some fixing up if needed
var ll = this;
function idRecurser(l) {
function recurser(l) {
// add IDs
if (l.id) ll[l.id] = l;
// fix type up
if (!l.type) l.type="";
if (l.c) l.c.forEach(idRecurser);
// FIXME ':'/fsz not needed in new firmwares - Font:12 is handled internally
// fix fonts for pre-2v11 firmware
if (l.font && l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
}
idRecurser(layout);
if (l.c) l.c.forEach(recurser);
}
recurser(this._l);
this.updateNeeded = true;
}
@ -352,12 +361,6 @@ Layout.prototype.update = function() {
"txt" : function(l) {
if (l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
// FIXME ':'/fsz not needed in new firmwares - it's handled internally
if (l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
}
if (l.wrap) {
l._h = l._w = 0;
} else {