1
0
Fork 0
David Peer 2022-03-20 20:42:25 +01:00
commit 68ba3a5cf7
96 changed files with 2082 additions and 391 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

12
apps/bikespeedo/README.md Normal file
View File

@ -0,0 +1,12 @@
## GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude...
![](Hochrad120px.png)...all taken from internal sources.
#### To speed-up GPS reception it is strongly recommended to upload AGPS data with ["Assisted GPS Update"](https://banglejs.com/apps/?id=assistedgps)
#### If "CALIB!" is shown on the display or the compass heading differs too much from GPS heading, compass calibration should be done with the ["Navigation Compass" App](https://banglejs.com/apps/?id=magnav)
**Credits:**<br>
Bike Speedometer App by <i>github.com/HilmarSt</i><br>
Big parts of the software are based on <i>github.com/espruino/BangleApps/tree/master/apps/speedalt</i><br>
Compass and Compass Calibration based on <i>github.com/espruino/BangleApps/tree/master/apps/magnav</i>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+64A/AC+sF1uBgAwsq1W1krGEmswIFDlcAFoMrqyGjlcrGAQDB1guBBQJghKYZZCMYhqBlYugFAesgAuFYgQIHAE2sYMZDfwIABbgIuowMAqwABb4wAjFVQAEqyMrF4cAlYABqwypR4RgBwIyplYnF1hnBGIo8BAAQvhGIj6C1hpBgChBGCqGBqwdCRQQnCB4gJBGAgtWc4WBPoi9JH4ILBGYQATPoRHJRYoACwLFBLi4tGLIyLEA5QuPCoYpEMhBBBGDIuFgArIYQIUHA4b+GABLUBAwoQIXorDGI5RNGCB9WRQ0AJwwHGDxChOH4oDCRI4/GXpAaB1gyLEwlWKgTrBT46ALCogQKZoryFCwzgGBgz/NZpaQHHBCdEF5hKBBxWBUwoGBgEAEoIyHHYesBg7aBJQ7SBBAIvEIIJCBD4IFBgBIGEAcAUA8rGAIWHS4QvDCAJAHG4JfRCgKCFeAovCdRIiBDYq/NABi0Cfo5IEBgjUGACZ6BqwcGwLxBFYRsEHIKBIJwLkBNoIHDF468GYgIBBXY4EDE4IHDYwSwCN4IGBCIp5CJYtWgBZBHAgFEMoRjEE4QDCLYJUEUoaCBPYoQCgA4FGozxFLYwfEQgqrGexIYFBoxbDS4YHCIAYVEEAZcCYwwvGfoQHEcwQHHIg9WIAS9BIoYYESoowIABQuBUgg1DVwwACEpIwBChDLFDQ5JLlZnHJAajBQwgLEO4LDBHKAhBFxQxFCIIACAwadLHgJJBAAUrQJxYFAAbKPCwRGCCqAAm"))

546
apps/bikespeedo/app.js Normal file
View File

@ -0,0 +1,546 @@
// Bike Speedometer by https://github.com/HilmarSt
// Big parts of this software are based on https://github.com/espruino/BangleApps/tree/master/apps/speedalt
// Compass and Compass Calibration based on https://github.com/espruino/BangleApps/tree/master/apps/magnav
const BANGLEJS2 = 1;
const screenH = g.getHeight();
const screenYstart = 24; // 0..23 for widgets
const screenY_Half = screenH / 2 + screenYstart;
const screenW = g.getWidth();
const screenW_Half = screenW / 2;
const fontFactorB2 = 2/3;
const colfg=g.theme.fg, colbg=g.theme.bg;
const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
var altiGPS=0, altiBaro=0;
var hdngGPS=0, hdngCompass=0, calibrateCompass=false;
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
var KalmanFilter = (function () {
'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
/**
* KalmanFilter
* @class
* @author Wouter Bulten
* @see {@link http://github.com/wouterbulten/kalmanjs}
* @version Version: 1.0.0-beta
* @copyright Copyright 2015-2018 Wouter Bulten
* @license MIT License
* @preserve
*/
var KalmanFilter =
/*#__PURE__*/
function () {
/**
* Create 1-dimensional kalman filter
* @param {Number} options.R Process noise
* @param {Number} options.Q Measurement noise
* @param {Number} options.A State vector
* @param {Number} options.B Control vector
* @param {Number} options.C Measurement vector
* @return {KalmanFilter}
*/
function KalmanFilter() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$R = _ref.R,
R = _ref$R === void 0 ? 1 : _ref$R,
_ref$Q = _ref.Q,
Q = _ref$Q === void 0 ? 1 : _ref$Q,
_ref$A = _ref.A,
A = _ref$A === void 0 ? 1 : _ref$A,
_ref$B = _ref.B,
B = _ref$B === void 0 ? 0 : _ref$B,
_ref$C = _ref.C,
C = _ref$C === void 0 ? 1 : _ref$C;
_classCallCheck(this, KalmanFilter);
this.R = R; // noise power desirable
this.Q = Q; // noise power estimated
this.A = A;
this.C = C;
this.B = B;
this.cov = NaN;
this.x = NaN; // estimated signal without noise
}
/**
* Filter a new value
* @param {Number} z Measurement
* @param {Number} u Control
* @return {Number}
*/
_createClass(KalmanFilter, [{
key: "filter",
value: function filter(z) {
var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
if (isNaN(this.x)) {
this.x = 1 / this.C * z;
this.cov = 1 / this.C * this.Q * (1 / this.C);
} else {
// Compute prediction
var predX = this.predict(u);
var predCov = this.uncertainty(); // Kalman gain
var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction
this.x = predX + K * (z - this.C * predX);
this.cov = predCov - K * this.C * predCov;
}
return this.x;
}
/**
* Predict next value
* @param {Number} [u] Control
* @return {Number}
*/
}, {
key: "predict",
value: function predict() {
var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
return this.A * this.x + this.B * u;
}
/**
* Return uncertainty of filter
* @return {Number}
*/
}, {
key: "uncertainty",
value: function uncertainty() {
return this.A * this.cov * this.A + this.R;
}
/**
* Return the last filtered measurement
* @return {Number}
*/
}, {
key: "lastMeasurement",
value: function lastMeasurement() {
return this.x;
}
/**
* Set measurement noise Q
* @param {Number} noise
*/
}, {
key: "setMeasurementNoise",
value: function setMeasurementNoise(noise) {
this.Q = noise;
}
/**
* Set the process noise R
* @param {Number} noise
*/
}, {
key: "setProcessNoise",
value: function setProcessNoise(noise) {
this.R = noise;
}
}]);
return KalmanFilter;
}();
return KalmanFilter;
}());
//==================================== MAIN ====================================
var lf = {fix:0,satellites:0};
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
var canDraw = 1;
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
var sec; // actual seconds for testing purposes
var max = {};
max.spd = 0;
max.alt = 0;
max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
var wp = {}; // Waypoint to use for distance from cur position.
var SATinView = 0;
function radians(a) {
return a*Math.PI/180;
}
function distance(a,b){
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
// Distance in selected units
var d = Math.sqrt(x*x + y*y) * 6371000;
d = (d/parseFloat(cfg.dist)).toFixed(2);
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
return d;
}
function drawFix(dat) {
if (!canDraw) return;
g.clearRect(0,screenYstart,screenW,screenH);
var v = '';
var u='';
// Primary Display
v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
// Primary Units
u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units;
drawPrimary(v,u);
// Secondary Display
v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString();
// Secondary Units
u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit;
drawSecondary(v,u);
// Time
drawTime();
//Sats
if ( dat.age > 10 ) {
if ( dat.age > 90 ) dat.age = '>90';
drawSats('Age:'+dat.age);
}
else if (!BANGLEJS2) {
drawSats('Sats:'+dat.sats);
} else {
if (lf.fix) {
drawSats('Sats:'+dat.sats);
} else {
drawSats('View:' + SATinView);
}
}
g.reset();
}
function drawClock() {
if (!canDraw) return;
g.clearRect(0,screenYstart,screenW,screenH);
drawTime();
g.reset();
}
function drawPrimary(n,u) {
//if(emulator)console.log("\n1: " + n +" "+ u);
var s=40; // Font size
var l=n.length;
if ( l <= 7 ) s=48;
if ( l <= 6 ) s=55;
if ( l <= 5 ) s=66;
if ( l <= 4 ) s=85;
if ( l <= 3 ) s=110;
// X -1=left (default), 0=center, 1=right
// Y -1=top (default), 0=center, 1=bottom
g.setFontAlign(0,-1); // center, top
if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
if (BANGLEJS2) s *= fontFactorB2;
g.setFontVector(s);
g.drawString(n, screenW_Half - 10, screenYstart);
// Primary Units
s = 35; // Font size
g.setFontAlign(1,-1,3); // right, top, rotate
g.setColor(col1);
if (BANGLEJS2) s = 20;
g.setFontVector(s);
g.drawString(u, screenW - 20, screenYstart + 2);
}
function drawSecondary(n,u) {
//if(emulator)console.log("2: " + n +" "+ u);
if (calibrateCompass) hdngCompass = "CALIB!";
else hdngCompass +="°";
g.setFontAlign(0,1);
g.setColor(col1);
g.setFontVector(12).drawString("Altitude GPS / Barometer", screenW_Half - 5, screenY_Half - 10);
g.setFontVector(20);
g.drawString(n+" "+u+" / "+altiBaro+" "+u, screenW_Half, screenY_Half + 11);
g.setFontVector(12).drawString("Heading GPS / Compass", screenW_Half - 10, screenY_Half + 26);
g.setFontVector(20);
g.drawString(hdngGPS+"° / "+hdngCompass, screenW_Half, screenY_Half + 47);
}
function drawTime() {
var x = 0, y = screenH;
g.setFontAlign(-1,1); // left, bottom
g.setFont("6x8", 2);
g.setColor(colbg);
g.drawString(time,x+1,y); // clear old time
time = require("locale").time(new Date(),1);
g.setColor(colfg); // draw new time
g.drawString(time,x+2,y);
}
function drawSats(sats) {
g.setColor(col1);
g.setFont("6x8", 2);
g.setFontAlign(1,1); //right, bottom
g.drawString(sats,screenW,screenH);
g.setFontVector(18);
g.setColor(col1);
if ( cfg.modeA == 1 ) {
if ( showMax ) {
g.setFontAlign(0,1); //centre, bottom
g.drawString('MAX',120,164);
}
}
}
function onGPS(fix) {
if ( emulator ) {
fix.fix = 1;
fix.speed = Math.random()*30; // calmed by Kalman filter if cfg.spdFilt
fix.alt = Math.random()*200 -20; // calmed by Kalman filter if cfg.altFilt
fix.lat = 50.59; // google.de/maps/@50.59,8.53,17z
fix.lon = 8.53;
fix.course = 365;
fix.satellites = sec;
fix.time = new Date();
fix.smoothed = 0;
}
var m;
var sp = '---';
var al = '---';
var di = '---';
var age = '---';
if (fix.fix) lf = fix;
hdngGPS = lf.course;
if (isNaN(hdngGPS)) hdngGPS = "---";
else if (0 == hdngGPS) hdngGPS = "0?";
else hdngGPS = hdngGPS.toFixed(0);
if (emulator) hdngCompass = hdngGPS;
if (emulator) altiBaro = lf.alt.toFixed(0);
if (lf.fix) {
if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw);
// Smooth data
if ( lf.smoothed !== 1 ) {
if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
lf.smoothed = 1;
if ( max.n <= 15 ) max.n++;
}
// Speed
if ( cfg.spd == 0 ) {
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
sp = parseFloat(m[1]);
cfg.spd_unit = m[2];
}
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
if ( sp < 10 ) sp = sp.toFixed(1);
else sp = Math.round(sp);
if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
// Altitude
al = lf.alt;
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al);
// Distance to waypoint
di = distance(lf,wp);
if (isNaN(di)) di = 0;
// Age of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
}
if ( cfg.modeA == 1 ) {
if ( showMax )
drawFix({
speed:max.spd,
sats:lf.satellites,
alt:max.alt,
alt_units:cfg.alt_unit,
age:age,
fix:lf.fix
}); // Speed and alt maximums
else
drawFix({
speed:sp,
sats:lf.satellites,
alt:al,
alt_units:cfg.alt_unit,
age:age,
fix:lf.fix
}); // Show speed/altitude
}
}
function setButtons(){
setWatch(_=>load(), BTN1);
onGPS(lf);
}
function updateClock() {
if (!canDraw) return;
drawTime();
g.reset();
if ( emulator ) {
max.spd++; max.alt++;
d=new Date(); sec=d.getSeconds();
onGPS(lf);
}
}
//###
let cfg = {};
cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = 'km/h'; // Displayed speed unit
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
cfg.dist = 1000; // Multiplier for distnce unit conversions.
cfg.dist_unit = 'km'; // Displayed distnce units
cfg.modeA = 1;
cfg.primSpd = 1; // 1 = Spd in primary, 0 = Spd in secondary
cfg.spdFilt = false;
cfg.altFilt = false;
if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
function onGPSraw(nmea) {
var nofGP = 0, nofBD = 0, nofGL = 0;
if (nmea.slice(3,6) == "GSV") {
// console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
SATinView = nofGP + nofBD + nofGL;
} }
if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
function onPressure(dat) { altiBaro = dat.altitude.toFixed(0); }
Bangle.setBarometerPower(1); // needs some time...
g.clearRect(0,screenYstart,screenW,screenH);
onGPS(lf);
Bangle.setGPSPower(1);
Bangle.on('GPS', onGPS);
Bangle.on('pressure', onPressure);
Bangle.setCompassPower(1);
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
if (!CALIBDATA) calibrateCompass = true;
function Compass_tiltfixread(O,S){
"ram";
//console.log(O.x+" "+O.y+" "+O.z);
var m = Bangle.getCompass();
var g = Bangle.getAccel();
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
if (d<0) d+=360;
var phi = Math.atan(-g.x/-g.z);
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
var yh = m.dz*sinphi - m.dx*cosphi;
var psi = Math.atan2(yh,xh)*180/Math.PI;
if (psi<0) psi+=360;
return psi;
}
var Compass_heading = 0;
function Compass_newHeading(m,h){
var s = Math.abs(m - h);
var delta = (m>h)?1:-1;
if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360;
if (hd>360)hd-= 360;
return hd;
}
function Compass_reading() {
"ram";
var d = Compass_tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
Compass_heading = Compass_newHeading(d,Compass_heading);
hdngCompass = Compass_heading.toFixed(0);
}
if (!calibrateCompass) setInterval(Compass_reading,200);
setButtons();
if (emulator) setInterval(updateClock, 2000);
else setInterval(updateClock, 10000);
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/bikespeedo/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

View File

@ -0,0 +1,18 @@
{
"id": "bikespeedo",
"name": "Bike Speedometer (beta)",
"shortName": "Bike Speedomet.",
"version": "0.01",
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
"icon": "app.png",
"screenshots": [{"url":"Screenshot.png"}],
"type": "app",
"tags": "tool,cycling,bicycle,outdoors,sport",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"bikespeedo.app.js","url":"app.js"},
{"name":"bikespeedo.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -46,3 +46,5 @@
0.40: Bootloader now rebuilds for new firmware versions
0.41: Add Keyboard and Mouse Bluetooth HID option
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.boot.js
0.43: Fix Gadgetbridge handling with Programmable:off
0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)

View File

@ -4,7 +4,7 @@ of the time. */
E.showMessage("Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var boot = "";
var boot = "", bootPost = "";
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
@ -15,6 +15,7 @@ if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't chang
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
boot += `E.setFlags({pretokenise:1});\n`;
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
@ -38,7 +39,7 @@ LoopbackA.setConsole(true);\n`;
boot += `
Bluetooth.line="";
Bluetooth.on('data',function(d) {
var l = (Bluetooth.line + d).split("\n");
var l = (Bluetooth.line + d).split(/[\\n\\r]/);
Bluetooth.line = l.pop();
l.forEach(n=>Bluetooth.emit("line",n));
});
@ -195,8 +196,8 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
var getPriority = /.*\.(\d+)\.boot\.js$/;
require('Storage').list(/\.boot\.js/).sort((a,b)=>{
var aPriority = a.match(getPriority);
var bPriority = b.match(getPriority);
if (aPriority && bPriority){
@ -206,18 +207,40 @@ require('Storage').list(/\.boot\.js/).sort((a,b)=>{
} else if (!aPriority && bPriority){
return 1;
}
return a > b;
}).forEach(bootFile=>{
return a==b ? 0 : (a>b ? 1 : -1);
});
// precalculate file size
var fileSize = boot.length + bootPost.length;
bootFiles.forEach(bootFile=>{
// match the size of data we're adding below in bootFiles.forEach
fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+1;
});
// write file in chunks (so as not to use up all RAM)
require('Storage').write('.boot0',boot,0,fileSize);
var fileOffset = boot.length;
bootFiles.forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error!
boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
// we write:
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
// but we need to do this without ever loading everything into RAM as some
// boot files seem to be getting pretty big now.
require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
fileOffset+=2+bootFile.length+1;
var bf = require('Storage').read(bootFile);
require('Storage').write('.boot0',bf,fileOffset);
fileOffset+=bf.length;
require('Storage').write('.boot0',"\n",fileOffset);
fileOffset+=1;
});
// update ble
boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`;
// write file
require('Storage').write('.boot0',boot);
require('Storage').write('.boot0',bootPost,fileOffset);
delete boot;
delete bootPost;
delete bootFiles;
delete fileSize;
delete fileOffset;
E.showMessage("Reloading...");
eval(require('Storage').read('.boot0'));
// .bootcde should be run automatically after if required, since

View File

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

17
apps/bordle/README.md Normal file
View File

@ -0,0 +1,17 @@
# Bordle
The Bangle version of a popular word guessing game. The goal is to guess a 5 letter word in 6 tries or less. After each guess, the letters in the guess are
marked in colors: yellow for a letter that appears in the to-be-guessed word, but in a different location and green for a letter in the correct position.
Only words contained in the internal dictionary are allowed as valid guesses. At app launch, a target word is picked from the dictionary at random.
On startup, a grid of 6 lines with 5 (empty) letter boxes is displayed. Swiping left or right at any time switches between grid view and keyboard view.
The keyboad was inspired by the 'Scribble' app (it is a simplified version using the layout library). The letter group "Z ..." contains the delete key and
the enter key. Hitting enter after the 5th letter will add the guess to the grid view and color mark it.
The (English language) dictionary was derived from the the Unix ispell word list by filtering out plurals and past particples (and some hand editing) from all 5 letter words.
It is contained in the file 'wordlencr.txt' which contains one long string (no newline characters) of all the words concatenated. It would not be too difficult to swap it
out for a different language version. The keyboard currently only supports the 26 characters of the latin alphabet (no accents or umlauts).

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AA/TADwoIFkYyOF0owIF04wGUSqvVBZQtZGJYJIFzomKF0onIF07EKF0owLF9wNEnwACE6oZILxovbMBov/F/4v/C54uWF/4vKBQQLLF/4YPFwYMLF7AZGF5Y5KF5xJIFwoMJD44vaBhwvcLQpgHF8gGRF6xYNBpQvTXBoNOF65QJBIgvjBywvUV5YOOF64OIB54v/cQwAKB5ov/F84wKADYuIF+AwkFIwwnE45hmExCSlEpTEiERr3KADw+PF0ownUSoseA=="))

BIN
apps/bordle/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

159
apps/bordle/bordle.app.js Normal file
View File

@ -0,0 +1,159 @@
var Layout = require("Layout");
var gameState = 0;
var keyState = 0;
var keyStateIdx = 0;
function buttonPushed(b) {
if (keyState==0) {
keyState++;
keyStateIdx = b;
if (b<6) {
for (i=1; i<=5; ++i) {
var c = String.fromCharCode(i+64+(b-1)*5);
layout["bt"+i.toString()].label = c;
layout["bt"+i.toString()].bgCol = wordle.keyColors[c]||g.theme.bg;
}
layout.bt6.label = "<";
}
else {
layout.bt1.label = "Z";
layout.bt1.bgCol = wordle.keyColors.Z||g.theme.bg;
layout.bt2.label = "<del>";
layout.bt4.label = "<ent>";
layout.bt3.label = layout.bt5.label = " ";
layout.bt6.label = "<";
}
}
else { // actual button pushed
inp = layout.input.label;
if (b!=6) {
if ((keyStateIdx<=5 || b<=1) && inp.length<5) inp += String.fromCharCode(b+(keyStateIdx-1)*5+64);
else if (layout.input.label.length>0 && b==2) inp = inp.slice(0,-1);
layout.input.label = inp;
}
layout = getKeyLayout(inp);
keyState = 0;
if (inp.length==5 && keyStateIdx==6 && b==4) {
rc = wordle.addGuess(inp);
layout.input.label = "";
layout.update();
gameState = 0;
if (rc>0) return;
g.clear();
wordle.render();
return;
}
}
layout.update();
g.clear();
layout.render();
}
function getKeyLayout(text) {
return new Layout( {
type: "v", c: [
{type:"txt", font:"6x8:2", id:"input", label:text, pad: 3},
{type: "h", c: [
{type:"btn", font:"6x8:2", id:"bt1", label:"ABCDE", cb: l=>buttonPushed(1), pad:4, filly:1, fillx:1 },
{type:"btn", font:"6x8:2", id:"bt2", label:"FGHIJ", cb: l=>buttonPushed(2), pad:4, filly:1, fillx:1 },
]},
{type: "h", c: [
{type:"btn", font:"6x8:2", id:"bt3", label:"KLMNO", cb: l=>buttonPushed(3), pad:4, filly:1, fillx:1 },
{type:"btn", font:"6x8:2", id:"bt4", label:"PQRST", cb: l=>buttonPushed(4), pad:4, filly:1, fillx:1 },
]},
{type: "h", c: [
{type:"btn", font:"6x8:2", id:"bt5", label:"UVWXY", cb: l=>buttonPushed(5), pad:4, filly:1, fillx:1 },
{type:"btn", font:"6x8:2", id:"bt6", label:"Z ...", cb: l=>buttonPushed(6), pad:4, filly:1, fillx:1 },
]}
]});
}
class Wordle {
constructor(word) {
this.word = word;
this.guesses = [];
this.guessColors = [];
this.keyColors = [];
this.nGuesses = -1;
if (word == "rnd") {
this.words = require("Storage").read("wordlencr.txt");
i = Math.floor(Math.floor(this.words.length/5)*Math.random())*5;
this.word = this.words.slice(i, i+5).toUpperCase();
}
console.log(this.word);
}
render(clear) {
h = g.getHeight();
bh = Math.floor(h/6);
bbh = Math.floor(0.85*bh);
w = g.getWidth();
bw = Math.floor(w/5);
bbw = Math.floor(0.85*bw);
if (clear) g.clear();
g.setFont("Vector", Math.floor(bbh*0.95)).setFontAlign(0,0);
g.setColor(g.theme.fg);
for (i=0; i<6; ++i) {
for (j=0; j<5; ++j) {
if (i<=this.nGuesses) {
g.setColor(this.guessColors[i][j]).fillRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
g.setColor(g.theme.fg).drawString(this.guesses[i][j], 2+j*bw+bw/2, 2+i*bh+bh/2);
}
g.setColor(g.theme.fg).drawRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
}
}
}
addGuess(w) {
if ((this.words.indexOf(w.toLowerCase())%5)!=0) {
E.showAlert(w+"\nis not a word", "Invalid word").then(function() {
layout = getKeyLayout("");
wordle.render(true);
});
return 3;
}
this.guesses.push(w);
this.nGuesses++;
this.guessColors.push([]);
correct = 0;
var sol = this.word;
for (i=0; i<w.length; ++i) {
c = w[i];
col = g.theme.bg;
if (sol[i]==c) {
sol = sol.substr(0,i) + '?' + sol.substr(i+1);
col = "#0f0";
++correct;
}
else if (sol.includes(c)) col = "#ff0";
if (col!=g.theme.bg) this.keyColors[c] = this.keyColors[c] || col;
else this.keyColors[c] = "#00f";
this.guessColors[this.nGuesses].push(col);
}
if (correct==5) {
E.showAlert("The word is\n"+this.word, "You won in "+(this.nGuesses+1)+" guesses!").then(function(){load();});
return 1;
}
if (this.nGuesses==5) {
E.showAlert("The word was\n"+this.word, "You lost!").then(function(){load();});
return 2;
}
}
}
wordle = new Wordle("rnd");
layout = getKeyLayout("");
wordle.render(true);
Bangle.on('swipe', function (dir) {
if (dir==1 || dir==-1) {
g.clear();
if (gameState==0) {
layout.render();
gameState = 1;
}
else if (gameState==1) {
wordle.render();
gameState = 0;
}
}
});

15
apps/bordle/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "bordle",
"name": "Bordle",
"shortName":"Bordle",
"icon": "app.png",
"version":"0.01",
"description": "Bangle version of a popular word search game",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"tags": "game, text",
"storage": [
{"name":"bordle.app.js","url":"bordle.app.js"},
{"name":"wordlencr.txt","url":"wordlencr.txt"},
{"name":"bordle.img","url":"app-icon.js","evaluate":true}
]
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1,2 @@
0.01: Initial upload
0.2: Added scrollable calendar and swipe gestures

View File

@ -3,10 +3,14 @@
This is my "Hello World". I first made this watchface almost 10 years ago for my original Pebble and Pebble Time and I missed this so much, that I had to write it for the BangleJS2.
I know that it seems redundant because there already **is** a *time&cal*-app, but it didn't fit my style.
- locked screen with only one minimal update/minute
- ![locked screen](https://foostuff.github.io/BangleApps/apps/clockcal/screenshot.png)
- unlocked screen (twist?) with seconds
- ![unlocked screen](https://foostuff.github.io/BangleApps/apps/clockcal/screenshot2.png)
|Screenshot|description|
|:--:|:-|
|![locked screen](screenshot.png)|locked: triggers only one minimal update/min|
|![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds|
|![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)|
## Configurable Features
- Number of calendar rows (weeks)
@ -15,6 +19,14 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu
- First day of the week
- Red Saturday
- Red Sunday
- Swipes (to disable all gestures)
- Swipes: music (swipe down)
- Spipes: messages (swipe right)
## Auto detects your message/music apps:
- swiping down will search your files for an app with the string "music" in its filename and launch it
- swiping right will search your files for an app with the string "message" in its filename and launch it.
- Configurable apps coming soon.
## Feedback
The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings.

View File

@ -7,15 +7,116 @@ var s = Object.assign({
FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
DRAGENABLED: true,
DRAGMUSIC: true,
DRAGMESSAGES: true
}, require('Storage').readJSON("clockcal.json", true) || {});
const h = g.getHeight();
const w = g.getWidth();
const CELL_W = w / 7;
const CELL2_W = w / 8;//full calendar
const CELL_H = 15;
const CAL_Y = h - s.CAL_ROWS * CELL_H;
const DEBUG = false;
var state = "watch";
var monthOffset = 0;
/*
* Calendar features
*/
function drawFullCalendar(monthOffset) {
addMonths = function (_d, _am) {
var ay = 0, m = _d.getMonth(), y = _d.getFullYear();
while ((m + _am) > 11) { ay++; _am -= 12; }
while ((m + _am) < 0) { ay--; _am += 12; }
n = new Date(_d.getTime());
n.setMonth(m + _am);
n.setFullYear(y + ay);
return n;
};
monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset;
state = "calendar";
var start = Date().getTime();
const months = ['Jan.', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec.'];
const monthclr = ['#0f0', '#f0f', '#00f', '#ff0', '#0ff', '#fff'];
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
d = addMonths(Date(), monthOffset);
tdy = Date().getDate() + "." + Date().getMonth();
newmonth=false;
c_y = 0;
g.reset();
g.setBgColor(0);
g.clear();
var prevmonth = addMonths(d, -1)
const today = prevmonth.getDate();
var rD = new Date(prevmonth.getTime());
rD.setDate(rD.getDate() - (today - 1));
const dow = (s.FIRSTDAYOFFSET + rD.getDay()) % 7;
rD.setDate(rD.getDate() - dow);
var rDate = rD.getDate();
bottomrightY = c_y - 3;
clrsun=s.REDSUN?'#f00':'#fff';
clrsat=s.REDSUN?'#f00':'#fff';
var fg=[clrsun,'#fff','#fff','#fff','#fff','#fff',clrsat];
for (var y = 1; y <= 11; y++) {
bottomrightY += CELL_H;
bottomrightX = -2;
for (var x = 1; x <= 7; x++) {
bottomrightX += CELL2_W;
rMonth = rD.getMonth();
rDate = rD.getDate();
if (tdy == rDate + "." + rMonth) {
caldrawToday(rDate);
} else if (rDate == 1) {
caldrawFirst(rDate);
} else {
caldrawNormal(rDate,fg[rD.getDay()]);
}
if (newmonth && x == 7) {
caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD);
}
rD.setDate(rDate + 1);
}
}
delete addMonths;
if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start));
}
function caldrawMonth(rDate,c,m,rD) {
g.setColor(c);
g.setFont("Vector", 18);
g.setFontAlign(-1, 1, 1);
drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : "";
g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1);
newmonth = false;
}
function caldrawToday(rDate) {
g.setFont("Vector", 16);
g.setFontAlign(1, 1);
g.setColor('#0f0');
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000');
g.drawString(rDate, bottomrightX, bottomrightY);
}
function caldrawFirst(rDate) {
g.flip();
g.setFont("Vector", 16);
g.setFontAlign(1, 1);
bottomrightY += 3;
newmonth = true;
g.setColor('#0ff');
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000');
g.drawString(rDate, bottomrightX, bottomrightY);
}
function caldrawNormal(rDate,c) {
g.setFont("Vector", 16);
g.setFontAlign(1, 1);
g.setColor(c);
g.drawString(rDate, bottomrightX, bottomrightY);//100
}
function drawMinutes() {
if (DEBUG) console.log("|-->minutes");
var d = new Date();
@ -52,8 +153,10 @@ function drawSeconds() {
if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000);
}
function drawCalendar() {
function drawWatch() {
if (DEBUG) console.log("CALENDAR");
monthOffset = 0;
state = "watch";
var d = new Date();
g.reset();
g.setBgColor(0);
@ -91,7 +194,7 @@ function drawCalendar() {
var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1);
if (DEBUG) console.log("Next Day:" + (nextday / 3600));
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
dayInterval = setTimeout(drawCalendar, nextday * 1000);
dayInterval = setTimeout(drawWatch, nextday * 1000);
}
function BTevent() {
@ -103,17 +206,87 @@ function BTevent() {
}
}
function input(dir) {
if (s.DRAGENABLED) {
Bangle.buzz(100,1);
console.log("swipe:"+dir);
switch (dir) {
case "r":
if (state == "calendar") {
drawWatch();
} else {
if (s.DRAGMUSIC) {
l=require("Storage").list(RegExp("music.*app"));
if (l.length > 0) {
load(l[0]);
} else Bangle.buzz(3000,1);//not found
}
}
break;
case "l":
if (state == "calendar") {
drawWatch();
}
break;
case "d":
if (state == "calendar") {
monthOffset--;
drawFullCalendar(monthOffset);
} else {
if (s.DRAGMESSAGES) {
l=require("Storage").list(RegExp("message.*app"));
if (l.length > 0) {
load(l[0]);
} else Bangle.buzz(3000,1);//not found
}
}
break;
case "u":
if (state == "watch") {
state = "calendar";
drawFullCalendar(0);
} else if (state == "calendar") {
monthOffset++;
drawFullCalendar(monthOffset);
}
break;
default:
if (state == "calendar") {
drawWatch();
}
break;
}
}
}
let drag;
Bangle.on("drag", e => {
if (s.DRAGENABLED) {
if (!drag) {
drag = { x: e.x, y: e.y };
} else if (!e.b) {
const dx = e.x - drag.x, dy = e.y - drag.y;
var dir = "t";
if (Math.abs(dx) > Math.abs(dy) + 10) {
dir = (dx > 0) ? "r" : "l";
} else if (Math.abs(dy) > Math.abs(dx) + 10) {
dir = (dy > 0) ? "d" : "u";
}
drag = null;
input(dir);
}
}
});
//register events
Bangle.on('lock', locked => {
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
dimSeconds = locked; //dim seconds if lock=on
drawCalendar();
drawWatch();
});
NRF.on('connect', BTevent);
NRF.on('disconnect', BTevent);
dimSeconds = Bangle.isLocked();
drawCalendar();
drawWatch();
Bangle.setUI("clock");

View File

@ -1,7 +1,7 @@
{
"id": "clockcal",
"name": "Clock & Calendar",
"version": "0.01",
"version": "0.2",
"description": "Clock with Calendar",
"readme":"README.md",
"icon": "app.png",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -8,6 +8,9 @@
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
DRAGENABLED: true, //Enable drag gestures (bigger calendar etc)
DRAGMUSIC: true, //Enable drag down for music (looks for "music*app")
DRAGMESSAGES: true //Enable drag right for messages (looks for "message*app")
}, require('Storage').readJSON(FILE, true) || {});
@ -67,6 +70,30 @@
writeSettings();
}
},
'Swipes (big cal.)?': {
value: settings.DRAGENABLED,
format: v => v ? "On" : "Off",
onchange: v => {
settings.DRAGENABLED = v;
writeSettings();
}
},
'Swipes (music)?': {
value: settings.DRAGMUSIC,
format: v => v ? "On" : "Off",
onchange: v => {
settings.DRAGMUSIC = v;
writeSettings();
}
},
'Swipes (messg)?': {
value: settings.DRAGMESSAGES,
format: v => v ? "On" : "Off",
onchange: v => {
settings.DRAGMESSAGES = v;
writeSettings();
}
},
'Load deafauls?': {
value: 0,
min: 0, max: 1,
@ -80,13 +107,16 @@
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
DRAGENABLED: true,
DRAGMUSIC: true,
DRAGMESSAGES: true
};
writeSettings();
load()
load();
}
}
},
}
};
// Show the menu
E.showMenu(menu);
})
});

View File

@ -5,3 +5,4 @@
0.05: Add cadence sensor support
0.06: Now read wheel rev as well as cadence sensor
Improve connection code
0.07: Make Bangle.js 2 compatible

View File

@ -11,9 +11,9 @@ Currently the app displays the following data:
- total distance traveled
- an icon with the battery status of the remote sensor
Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app.
If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor.
Button 2 switches between the display for cycling speed and cadence.
Button 1 (swipe up on Bangle.js 2) resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app.
If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 (swipe down on Bangle.js 2) will attempt to reconnect to the sensor.
Button 2 (tap on Bangle.js 2) switches between the display for cycling speed and cadence.
Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app.

View File

@ -7,6 +7,11 @@ const SETTINGS_FILE = 'cscsensor.json';
const storage = require('Storage');
const W = g.getWidth();
const H = g.getHeight();
const yStart = 48;
const rowHeight = (H-yStart)/6;
const yCol1 = W/2.7586;
const fontSizeLabel = W/12.632;
const fontSizeValue = W/9.2308;
class CSCSensor {
constructor() {
@ -22,7 +27,6 @@ class CSCSensor {
this.speed = 0;
this.maxSpeed = 0;
this.lastSpeed = 0;
this.qUpdateScreen = true;
this.lastRevsStart = -1;
this.qMetric = !require("locale").speed(1).toString().endsWith("mph");
this.speedUnit = this.qMetric ? "km/h" : "mph";
@ -49,6 +53,7 @@ class CSCSensor {
toggleDisplayCadence() {
this.showCadence = !this.showCadence;
this.screenInit = true;
g.setBgColor(0, 0, 0);
}
setBatteryLevel(level) {
@ -63,14 +68,16 @@ class CSCSensor {
}
drawBatteryIcon() {
g.setColor(1, 1, 1).drawRect(10, 55, 20, 75).fillRect(14, 53, 16, 55).setColor(0).fillRect(11, 56, 19, 74);
g.setColor(1, 1, 1).drawRect(10*W/240, yStart+0.029167*H, 20*W/240, yStart+0.1125*H)
.fillRect(14*W/240, yStart+0.020833*H, 16*W/240, yStart+0.029167*H)
.setColor(0).fillRect(11*W/240, yStart+0.033333*H, 19*W/240, yStart+0.10833*H);
if (this.batteryLevel!=-1) {
if (this.batteryLevel<25) g.setColor(1, 0, 0);
else if (this.batteryLevel<50) g.setColor(1, 0.5, 0);
else g.setColor(0, 1, 0);
g.fillRect(11, 74-18*this.batteryLevel/100, 19, 74);
g.fillRect(11*W/240, (yStart+0.10833*H)-18*this.batteryLevel/100, 19*W/240, yStart+0.10833*H);
}
else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66);
else g.setFontVector(W/17.143).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16*W/240, yStart+0.075*H);
}
updateScreenRevs() {
@ -88,36 +95,36 @@ class CSCSensor {
for (var i=0; i<6; ++i) {
if ((i&1)==0) g.setColor(0, 0, 0);
else g.setColor(0x30cd);
g.fillRect(0, 48+i*32, 86, 48+(i+1)*32);
g.fillRect(0, yStart+i*rowHeight, yCol1-1, yStart+(i+1)*rowHeight);
if ((i&1)==1) g.setColor(0);
else g.setColor(0x30cd);
g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239);
g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight);
g.setColor(0.5, 0.5, 0.5).drawRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight).drawLine(0, H-1, W-1, H-1);
g.moveTo(0, yStart+0.13333*H).lineTo(30*W/240, yStart+0.13333*H).lineTo(30*W/240, yStart).lineTo(yCol1, yStart).lineTo(yCol1, H-1).lineTo(0, H-1).lineTo(0, yStart+0.13333*H);
}
g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0);
g.drawString("Time:", 87, 66);
g.drawString("Speed:", 87, 98);
g.drawString("Ave spd:", 87, 130);
g.drawString("Max spd:", 87, 162);
g.drawString("Trip:", 87, 194);
g.drawString("Total:", 87, 226);
g.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0);
g.drawString("Time:", yCol1, yStart+rowHeight/2+0*rowHeight);
g.drawString("Speed:", yCol1, yStart+rowHeight/2+1*rowHeight);
g.drawString("Avg spd:", yCol1, yStart+rowHeight/2+2*rowHeight);
g.drawString("Max spd:", yCol1, yStart+rowHeight/2+3*rowHeight);
g.drawString("Trip:", yCol1, yStart+rowHeight/2+4*rowHeight);
g.drawString("Total:", yCol1, yStart+rowHeight/2+5*rowHeight);
this.drawBatteryIcon();
this.screenInit = false;
}
g.setFontAlign(-1, 0, 0).setFontVector(26);
g.setColor(0x30cd).fillRect(88, 49, 238, 79);
g.setColor(0xffff).drawString(dmins+":"+dsecs, 92, 66);
g.setColor(0).fillRect(88, 81, 238, 111);
g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, 92, 98);
g.setColor(0x30cd).fillRect(88, 113, 238, 143);
g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, 92, 130);
g.setColor(0).fillRect(88, 145, 238, 175);
g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, 92, 162);
g.setColor(0x30cd).fillRect(88, 177, 238, 207);
g.setColor(0xffff).drawString(ddist + " " + this.distUnit, 92, 194);
g.setColor(0).fillRect(88, 209, 238, 238);
g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226);
g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue);
g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*0, 238, 47+1*rowHeight);
g.setColor(0xffff).drawString(dmins+":"+dsecs, yCol1+5, 50+rowHeight/2+0*rowHeight);
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight);
g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, yCol1+5, 50+rowHeight/2+1*rowHeight);
g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*2, 238, 47+3*rowHeight);
g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+2*rowHeight);
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*3, 238, 47+4*rowHeight);
g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+3*rowHeight);
g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*4, 238, 47+5*rowHeight);
g.setColor(0xffff).drawString(ddist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+4*rowHeight);
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*5, 238, 47+6*rowHeight);
g.setColor(0xffff).drawString(tdist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+5*rowHeight);
}
updateScreenCadence() {
@ -125,21 +132,21 @@ class CSCSensor {
for (var i=0; i<2; ++i) {
if ((i&1)==0) g.setColor(0, 0, 0);
else g.setColor(0x30cd);
g.fillRect(0, 48+i*32, 86, 48+(i+1)*32);
g.fillRect(0, yStart+i*rowHeight, yCol1-1, yStart+(i+1)*rowHeight);
if ((i&1)==1) g.setColor(0);
else g.setColor(0x30cd);
g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239);
g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight);
g.setColor(0.5, 0.5, 0.5).drawRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight).drawLine(0, H-1, W-1, H-1);
g.moveTo(0, yStart+0.13333*H).lineTo(30*W/240, yStart+0.13333*H).lineTo(30*W/240, yStart).lineTo(yCol1, yStart).lineTo(yCol1, H-1).lineTo(0, H-1).lineTo(0, yStart+0.13333*H);
}
g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0);
g.drawString("Cadence:", 87, 98);
g.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0);
g.drawString("Cadence:", yCol1, yStart+rowHeight/2+1*rowHeight);
this.drawBatteryIcon();
this.screenInit = false;
}
g.setFontAlign(-1, 0, 0).setFontVector(26);
g.setColor(0).fillRect(88, 81, 238, 111);
g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98);
g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue);
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight);
g.setColor(0xffff).drawString(Math.round(this.cadence), yCol1+5, 50+rowHeight/2+1*rowHeight);
}
updateScreen() {
@ -163,7 +170,7 @@ class CSCSensor {
}
this.lastCrankRevs = crankRevs;
this.lastCrankTime = crankTime;
}
} else {
// wheel revolution
var wheelRevs = event.target.value.getUint32(1, true);
var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
@ -189,8 +196,7 @@ class CSCSensor {
this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
this.speedFailed = 0;
this.movingTime += dT;
}
else {
} else if (!this.showCadence) {
this.speedFailed++;
qChanged = false;
if (this.speedFailed>3) {
@ -201,7 +207,8 @@ class CSCSensor {
this.lastSpeed = this.speed;
if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
}
if (qChanged && this.qUpdateScreen) this.updateScreen();
}
if (qChanged) this.updateScreen();
}
}
@ -253,9 +260,9 @@ E.on('kill',()=>{
});
NRF.on('disconnect', connection_setup); // restart if disconnected
Bangle.setUI("updown", d=>{
if (d<0) { mySensor.reset(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); }
if (d==0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
if (d>0) { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); }
if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
});
Bangle.loadWidgets();

View File

@ -2,11 +2,11 @@
"id": "cscsensor",
"name": "Cycling speed sensor",
"shortName": "CSCSensor",
"version": "0.06",
"version": "0.07",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"cscsensor.app.js","url":"cscsensor.app.js"},

View File

@ -2,3 +2,4 @@
0.02: added settings menu to change color
0.03: fix metadata.json to allow setting as clock
0.04: added heart rate which is switched on when cycled to it through up/down touch on rhs
0.05: changed text to uppercase, just looks better, removed colons on text

View File

@ -109,10 +109,10 @@ function updateSunRiseSunSet(now, lat, lon, line){
const infoData = {
ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} },
ID_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} },
ID_SR: { calc: () => 'Sunrise: ' + sunRise },
ID_SS: { calc: () => 'Sunset: ' + sunSet },
ID_STEP: { calc: () => 'Steps: ' + getSteps() },
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' },
ID_SR: { calc: () => 'Sunrise ' + sunRise },
ID_SS: { calc: () => 'Sunset ' + sunSet },
ID_STEP: { calc: () => 'Steps ' + getSteps() },
ID_BATT: { calc: () => 'Battery ' + E.getBattery() + '%' },
ID_HRM: { calc: () => hrmCurrent }
};
@ -158,7 +158,7 @@ function drawInfo() {
g.setColor('#f00'); // red
drawHeartIcon();
} else {
g.drawString((infoData[infoMode].calc()), w/2, infoLine);
g.drawString((infoData[infoMode].calc().toUpperCase()), w/2, infoLine);
}
}

View File

@ -1,6 +1,6 @@
{ "id": "daisy",
"name": "Daisy",
"version":"0.04",
"version":"0.05",
"dependencies": {"mylocation":"app"},
"description": "A clock based on the Pastel clock with large ring guage for steps",
"icon": "app.png",

View File

@ -2,3 +2,6 @@
0.02: added emulator capability and display of widgets
0.03: bug of advancing time fixed; doztime now correct within ca. 1 second
0.04: changed time colour from slightly off white to pure white
0.05: extraneous comments and code removed
display improved
now supports Adjust Clock widget, if installed

View File

@ -1,23 +1,23 @@
// Positioning values for graphics buffers
const g_height = 80; // total graphics height
const g_x_off = 0; // position from left was 16, then 8 here
const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240
const g_x_off = 0; // position from left
const g_y_off = (180 - g_height)/2; // vertical center for graphics region
const g_width = 240 - 2 * g_x_off; // total graphics width
const g_height_d = 28; // height of date region was 32
const g_height_d = 28; // height of date region
const g_y_off_d = 0; // y position of date region within graphics region
const spacing = 0; // space between date and time in graphics region
const spacing = 6; // space between date and time in graphics region
const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region
const g_height_t = 44; // height of time region was 48
const g_height_t = 44; // height of time region
// Other vars
const A1 = [30,30,30,30,31,31,31,31,31,31,30,30];
const B1 = [30,30,30,30,30,31,31,31,31,31,30,30];
const B2 = [30,30,30,30,31,31,31,31,31,30,30,30];
const timeColour = "#ffffff";
const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff
const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20
const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20
const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30
const dateColours = ["#ff0000","#ff8000","#ffff00","#00ff00","#0080ff","#ff00ff","#ffffff"];
const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line
const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line
const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line
const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30
const baseYear = 11584;
const baseDate = Date(2020,11,21); // month values run from 0 to 11
@ -59,11 +59,8 @@ g.flip = function()
}, g_x_off, g_y_off + g_y_off_t);
};
setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1
setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2
//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true});
//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true});
//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true});
setWatch(function(){ modeTime(); }, BTN, {repeat:true} );
setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" });
Bangle.on('touch', function(button, xy) { //from Gordon Williams
if (button==1) toggleTimeDigits();
@ -134,8 +131,8 @@ function writeDozTime(text,def){
g_t.clear();
g_t.setFont("Vector",def.size);
for(let i in text){
if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new
else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new
if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); }
else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); }
else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
x = x+def.step[0];
y = y+def.step[1];
@ -150,18 +147,25 @@ function writeDozDate(text,def,colour){
g_d.clear();
g_d.setFont("Vector",def.size);
for(let i in text){
if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new
else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new
if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); }
else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); }
else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
x = x+def.step[0];
y = y+def.step[1];
}
}
Bangle.loadWidgets();
//for malaire's Adjust Clock widget, if used
function adjustedNow() {
return WIDGETS.adjust ? new Date(WIDGETS.adjust.now()) : new Date();
}
Bangle.drawWidgets();
// Functions for time mode
function drawTime()
{
let dt = new Date();
let dt = adjustedNow();
let date = "";
let timeDef;
let x = 0;
@ -204,41 +208,17 @@ function drawTime()
}
function modeTime()
{
timeActiveUntil = new Date();
timeActiveUntil = adjustedNow();
timeActiveUntil.setDate(timeActiveUntil.getDate());
timeActiveUntil.setSeconds(timeActiveUntil.getSeconds()+86400);
timeActiveUntil.setSeconds(timeActiveUntil.getSeconds()+604800);
if (typeof drawtime_timeout !== 'undefined')
{
clearTimeout(drawtime_timeout);
}
drawTime();
}
Bangle.loadWidgets();
Bangle.drawWidgets();
// Functions for weather mode - TODO
// function drawWeather() {}
// function modeWeather() {}
// Start time on twist
Bangle.on('twist',function() {
modeTime();
});
// Time fix with GPS
function fixTime() {
Bangle.on("GPS",function cb(g) {
Bangle.setGPSPower(0,"time");
Bangle.removeListener("GPS",cb);
if (!g.time || (g.time.getFullYear()<2000) ||
(g.time.getFullYear()>2200)) {
} else {
// We have a GPS time. Set time
setTime(g.time.getTime()/1000);
}
});
Bangle.setGPSPower(1,"time");
setTimeout(fixTime, 10*60*1000); // every 10 minutes
}
// Start time fixing with GPS on next 10 minute interval
setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000);

View File

@ -2,7 +2,7 @@
"id": "doztime",
"name": "Dozenal Time",
"shortName": "Dozenal Time",
"version": "0.04",
"version": "0.05",
"description": "A dozenal Holocene calendar and dozenal diurnal clock",
"icon": "app.png",
"type": "clock",

View File

@ -7,7 +7,7 @@ screen. Very useful if combined with pattern launcher ;)
![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_2.png)
![](screenshot_3.png)
## Contributors

View File

@ -38,3 +38,4 @@
Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
0.24: Remove left-over debug statement
0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
0.26: Setting to auto-open music

View File

@ -13,12 +13,13 @@ and `Messages`:
* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
* `Unread Timer` - when a new message is received we go into the Messages app.
* `Unread Timer` - When a new message is received we go into the Messages app.
If there is no user input for this amount of time then the app will exit and return
to the clock where a ringing bell will be shown in the Widget bar.
* `Min Font` - the minimum font size used when displaying messages on the screen. A bigger font
* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
is chosen if there isn't much message text, but this specifies the smallest the font should get before
it starts getting clipped.
* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
## New Messages

View File

@ -26,6 +26,8 @@ var fontSmall = "6x8";
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
var active; // active screen
var openMusic = false; // go back to music screen after we handle something else?
// hack for 2v10 firmware's lack of ':size' font handling
try {
g.setFont("6x8:2");
@ -50,10 +52,14 @@ 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 && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
if (msg && msg.id!=="music" && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
if (WIDGETS["messages"]) WIDGETS["messages"].buzz();
else Bangle.buzz();
}
if (msg && msg.id=="music") {
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to
if (active!="music") return; // don't open music over other screens
}
showMessage(msg&&msg.id);
};
function saveMessages() {
@ -135,6 +141,7 @@ function getMessageImageCol(msg,def) {
}
function showMapMessage(msg) {
active = "map";
var m;
var distance, street, target, eta;
m=msg.title.match(/(.*) - (.*)/);
@ -168,12 +175,14 @@ function showMapMessage(msg) {
msg.new = false;
saveMessages();
layout = undefined;
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0});
});
}
function showMusicMessage(msg) {
var updateLabelsInterval;
function showMusicMessage(msg) {
active = "music";
openMusic = msg.state=="play";
var trackScrollOffset = 0;
var artistScrollOffset = 0;
var albumScrollOffset = 0;
@ -194,10 +203,14 @@ function showMusicMessage(msg) {
function back() {
clearInterval(updateLabelsInterval);
updateLabelsInterval = undefined;
openMusic = false;
var wasNew = msg.new;
msg.new = false;
saveMessages();
layout = undefined;
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0});
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
}
function updateLabels() {
trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13);
@ -243,6 +256,7 @@ function showMusicMessage(msg) {
}
function showMessageScroller(msg) {
active = "scroller";
var bodyFont = fontBig;
g.setFont(bodyFont);
var lines = [];
@ -272,6 +286,7 @@ function showMessageScroller(msg) {
}
function showMessageSettings(msg) {
active = "settings";
E.showMenu({"":{"title":/*LANG*/"Message"},
"< Back" : () => showMessage(msg.id),
/*LANG*/"View Message" : () => {
@ -280,12 +295,12 @@ function showMessageSettings(msg) {
/*LANG*/"Delete" : () => {
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0});
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
},
/*LANG*/"Mark Unread" : () => {
msg.new = true;
saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0});
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
},
/*LANG*/"Delete all messages" : () => {
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
@ -293,7 +308,7 @@ function showMessageSettings(msg) {
MESSAGES = [];
saveMessages();
}
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0});
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
});
},
});
@ -301,15 +316,20 @@ function showMessageSettings(msg) {
function showMessage(msgid) {
var msg = MESSAGES.find(m=>m.id==msgid);
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found
if (msg.src=="Maps") {
cancelReloadTimeout(); // don't auto-reload to clock now
return showMapMessage(msg);
if (updateLabelsInterval) {
clearInterval(updateLabelsInterval);
updateLabelsInterval=undefined;
}
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found
if (msg.id=="music") {
cancelReloadTimeout(); // don't auto-reload to clock now
return showMusicMessage(msg);
}
if (msg.src=="Maps") {
cancelReloadTimeout(); // don't auto-reload to clock now
return showMapMessage(msg);
}
active = "message";
// Normal text message display
var title=msg.title, titleFont = fontLarge, lines;
if (title) {
@ -342,7 +362,7 @@ function showMessage(msgid) {
function goBack() {
msg.new = false; saveMessages(); // read mail
cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic});
}
var buttons = [
{type:"btn", src:getBackImage(), cb:goBack} // back
@ -353,7 +373,7 @@ function showMessage(msgid) {
msg.new = false; saveMessages();
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,true);
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
}});
}
if (msg.negative) {
@ -362,7 +382,7 @@ function showMessage(msgid) {
msg.new = false; saveMessages();
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,false);
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
}});
}
@ -411,15 +431,19 @@ function checkMessages(options) {
return load();
}
// we have >0 messages
var newMessages = MESSAGES.filter(m=>m.new);
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
// If we have a new message, show it
if (options.showMsgIfUnread && newMessages.length)
return showMessage(newMessages[0].id);
// no new messages: show playing music? (only if we have playing music to show)
if (options.openMusic && MESSAGES.some(m=>m.id=="music" && m.track && m.state=="play"))
return showMessage('music');
// no new messages - go to clock?
if (options.clockIfAllRead && newMessages.length==0)
return load();
// we don't have to time out of this screen...
cancelReloadTimeout();
active = "main";
// Otherwise show a menu
E.showScroller({
h : 48,
@ -482,5 +506,7 @@ setTimeout(() => {
print("Message not seen - reloading");
load();
}, unreadTimeoutSecs*1000);
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1});
// only openMusic on launch if music is new
var newMusic = MESSAGES.some(m=>m.id==="music"&&m.new);
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1,openMusic:newMusic&&settings.openMusic});
},10); // if checkMessages wants to 'load', do that

View File

@ -1,3 +1,10 @@
function openMusic() {
// only read settings file for first music message
if ("undefined"==typeof exports._openMusic) {
exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic);
}
return exports._openMusic;
}
/* Push a new message onto messages queue, event is:
{t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool}
{t:"add",id:int, id:"music", state, artist, track, etc} // add new
@ -26,6 +33,9 @@ exports.pushMessage = function(event) {
messages.unshift(event); // add new messages to the beginning
}
else Object.assign(messages[mIdx], event);
if (event.id=="music" && messages[mIdx].state=="play") {
messages[mIdx].new = true; // new track, or playback (re)started
}
}
require("Storage").writeJSON("messages.json",messages);
// if in app, process immediately
@ -34,8 +44,12 @@ exports.pushMessage = function(event) {
if (event.t=="remove" && !messages.some(m=>m.new)) {
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide();
}
// ok, saved now - we only care if it's new
if (event.t!="add") {
// ok, saved now
if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) {
// just load the app to display music: no buzzing
load("messages.app.js");
} else if (event.t!="add") {
// we only care if it's new
return;
} else if(event.new == false) {
return;

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.25",
"version": "0.26",
"description": "App to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -4,6 +4,7 @@
if (settings.vibrate===undefined) settings.vibrate=".";
if (settings.repeat===undefined) settings.repeat=4;
if (settings.unreadTimeout===undefined) settings.unreadTimeout=60;
settings.openMusic=!!settings.openMusic;
settings.maxUnreadTimeout=240;
return settings;
}
@ -43,6 +44,11 @@
format: v => [/*LANG*/"Small",/*LANG*/"Medium"][v],
onchange: v => updateSetting("fontSize", v)
},
/*LANG*/'Auto-Open Music': {
value: !!settings().openMusic,
format: v => v?/*LANG*/'Yes':/*LANG*/'No',
onchange: v => updateSetting("openMusic", v)
},
};
E.showMenu(mainmenu);
})

View File

@ -50,5 +50,5 @@ message but then the watch was never viewed. In that case we don't
want to buzz but should still show that there are unread messages. */
if (global.MESSAGES===undefined) (function() {
var messages = require("Storage").readJSON("messages.json",1)||[];
if (messages.some(m=>m.new)) WIDGETS["messages"].show(true);
if (messages.some(m=>m.new&&m.id!="music")) WIDGETS["messages"].show(true);
})();

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Allow forcing monotonic battery voltage/percentage

View File

@ -1,6 +1,10 @@
# Power manager
Manages settings for charging. You can set a warning threshold to be able to disconnect the charger at a given percentage. Also allows to set the battery calibration offset.
Manages settings for charging.
Features:
* Warning threshold to be able to disconnect the charger at a given percentage
* Set the battery calibration offset.
* Force monotonic battery percentage or voltage
## Internals

View File

@ -26,4 +26,26 @@
Bangle.on("charging",handleCharging);
handleCharging(Bangle.isCharging());
}
if (settings.forceMonoPercentage){
var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4;
var op = E.getBattery;
E.getBattery = function() {
var current = Math.round((op()+op()+op()+op())/4);
if (Bangle.isCharging() && current > p) p = current;
if (!Bangle.isCharging() && current < p) p = current;
return p;
};
}
if (settings.forceMonoVoltage){
var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/4;
var ov = NRF.getBattery;
NRF.getBattery = function() {
var current = (ov()+ov()+ov()+ov())/4;
if (Bangle.isCharging() && current > v) v = current;
if (!Bangle.isCharging() && current < v) v = current;
return v;
};
}
})();

View File

@ -1,4 +1,6 @@
{
"warnEnabled": false,
"warn": 96
"warn": 96,
"forceMonoVoltage": false,
"forceMonoPercentage": false
}

View File

@ -2,7 +2,7 @@
"id": "powermanager",
"name": "Power Manager",
"shortName": "Power Manager",
"version": "0.01",
"version": "0.02",
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
"icon": "app.png",
"type": "bootloader",

View File

@ -24,6 +24,20 @@
'title': 'Power Manager'
},
'< Back': back,
'Monotonic percentage': {
value: !!settings.forceMonoPercentage,
format: v => settings.forceMonoPercentage ? "On" : "Off",
onchange: v => {
writeSettings("forceMonoPercentage", v);
}
},
'Monotonic voltage': {
value: !!settings.forceMonoVoltage,
format: v => settings.forceMonoVoltage ? "On" : "Off",
onchange: v => {
writeSettings("forceMonoVoltage", v);
}
},
'Charge warning': function() {
E.showMenu(submenu_chargewarn);
},

View File

@ -2,3 +2,4 @@
0.02: Fix typo to Purple
0.03: Added dependancy on Pedometer Widget
0.04: Fixed icon and png to 48x48 pixels
0.05: added charging icon

View File

@ -2,7 +2,7 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
"version": "0.04",
"version": "0.05",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",

View File

@ -204,6 +204,14 @@ function drawBattery(x,y,wi,hi) {
g.setColor(g.theme.fg);
g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact
g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level
if( Bangle.isCharging() )
{
g.setBgColor(settings.bg);
image = ()=> { return require("heatshrink").decompress(atob("j8OwMB/4AD94DC44DCwP//n/gH//EOgE/+AdBh/gAYMH4EAvkDAYP/+/AFAX+FgfzGAnAA=="));}
g.drawImage(image(),x+3,y+4);
}
}
function getSteps() {
@ -270,3 +278,14 @@ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
loadSettings();
loadLocation();
draw(); // queues the next draw for a minutes time
Bangle.on('charging', function(charging) {
//redraw the sidebar ( with the battery )
switch(sideBar) {
case 0:
drawSideBar1();
break;
case 1:
drawSideBar2();
break;
}
});

View File

@ -16,3 +16,4 @@
0.10: Fix broken recorder settings (when launched from settings app)
0.11: Fix KML and GPX export when there is no GPS data
0.12: Fix 'Back' label positioning on track/graph display, make translateable
0.13: Fix for when widget is used before app

View File

@ -1,6 +1,6 @@
{
"recording":false,
"file":"record.log0.csv",
"file":"recorder.log0.csv",
"period":10,
"record" : ["gps"]
}

View File

@ -31,7 +31,12 @@ function updateSettings() {
}
function getTrackNumber(filename) {
return parseInt(filename.match(/^recorder\.log(.*)\.csv$/)[1]||0);
var trackNum = 0;
var matches = filename.match(/^recorder\.log(.*)\.csv$/);
if (matches) {
trackNum = parseInt(matches[1]||0);
}
return trackNum;
}
function showMainMenu() {

View File

@ -2,7 +2,7 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
"version": "0.12",
"version": "0.13",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",
@ -15,5 +15,5 @@
{"name":"recorder.wid.js","url":"widget.js"},
{"name":"recorder.settings.js","url":"settings.js"}
],
"data": [{"name":"recorder.json"},{"wildcard":"recorder.log?.csv","storageFile":true}]
"data": [{"name":"recorder.json","url":"app-settings.json"},{"wildcard":"recorder.log?.csv","storageFile":true}]
}

View File

@ -233,7 +233,9 @@
Bangle.drawWidgets(); // relayout all widgets
},setRecording:function(isOn) {
var settings = loadSettings();
if (isOn && !settings.recording && require("Storage").list(settings.file).length){
if (isOn && !settings.recording && !settings.file) {
settings.file = "recorder.log0.csv";
} else if (isOn && !settings.recording && require("Storage").list(settings.file).length){
var logfiles=require("Storage").list(/recorder.log.*/);
var maxNumber=0;
for (var c of logfiles){
@ -247,9 +249,11 @@
var buttons={Yes:"yes",No:"no"};
if (newFileName) buttons["New"] = "new";
var prompt = E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{
if (selection=="no") return false; // just cancel
if (selection=="yes") require("Storage").open(settings.file,"r").erase();
if (selection=="new"){
if (selection==="no") return false; // just cancel
if (selection==="yes") {
require("Storage").open(settings.file,"r").erase();
}
if (selection==="new"){
settings.file = newFileName;
updateSettings(settings);
}

View File

@ -7,3 +7,5 @@
0.06: Add option to record a run using the recorder app automatically
0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
0.08: Added support for notifications from exstats. Support all stats from exstats
0.09: Fix broken start/stop if recording not enabled (fix #1561)
0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578)

View File

@ -66,6 +66,9 @@ function onStartStop() {
}
}
if (!prepPromises.length) // fix for Promise.all bug in 2v12
prepPromises.push(Promise.resolve());
Promise.all(prepPromises)
.then(() => {
if (running) {

View File

@ -1,6 +1,6 @@
{ "id": "run",
"name": "Run",
"version":"0.08",
"version":"0.10",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",

View File

@ -42,6 +42,11 @@
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
format: v => statsList[v].name,
onchange: v => {
for (var i=1;i<=6;i++)
if (settings["B"+i]==statsIDs[v]) {
settings["B"+i]="";
boxMenu["Box "+i].value=0;
}
settings[boxID] = statsIDs[v];
saveSettings();
},
@ -60,7 +65,7 @@
'': { 'title': 'Run' },
'< Back': back,
};
if (WIDGETS["recorder"])
if (global.WIDGETS&&WIDGETS["recorder"])
menu[/*LANG*/"Record Run"] = {
value : !!settings.record,
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
@ -87,7 +92,7 @@
notificationsMenu[/*LANG*/"Dist Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.dist.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
@ -97,7 +102,7 @@
notificationsMenu[/*LANG*/"Step Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.step.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
@ -107,7 +112,7 @@
notificationsMenu[/*LANG*/"Time Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
min: 0, max: vibPatterns.length,
format: v => vibPatterns[v]||"Off",
format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.time.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);

View File

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

View File

@ -0,0 +1,16 @@
# Seiko 5actus
![](screenshot.png)
This is built on the knowledge of what I gained through designing the rolex watch face and improves on it by getting more done right at the start.
This watch is modeled after one I personally own and love, I have spent quite a bit of time designing this in a pixel art editor to try and make it as clean as possible and am quite happy with how it came out.
This watch face works in both the light and dark themes but I personally think it looks a lot cleaner in the dark them.
This watch whilst technically designed in a way that would work with the BangleJs has been only listed to work with the BangleJs2, if someones wants to test it on a first gen and let me know if it works then i'll allow it to be installed on both devices but I assume with how the images have been designed it would look strange on a first gen watch.
Special thanks to:
* rozek (for his updated widget draw code for utilization with background images)
* Gordon Williams (Bangle.js, watchapps for reference code and documentation)
* The community (for helping drive such a wonderful project)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEBpMB+fziAjRCQQXBHyoXEgIRLgMwC5EAj8gC5MC+QXJn4XKBgJHJMhkfJAYXEh/xC5cDBofzJocvIxKiCHpBGNExMCIxi2KeRIJFgMiiYBCkQ1Jh67EAAMSCgICBiQjFn8xDYX/AgQANn4qEgf/JIcDkcxiUSiMRY4cv+ZaFj6bDgZGBkMRDIIXD/7CHn5TDFYIADFIcRSxgAvXQwAQgRyDACcje4wAESQ4RDmMQgSGBj8zAAnyTgauH/65Cj7EBkMicAPyBYIABCgcRkYWCmYvCewMhmUiiMyF4gUBDoXzn7/Dj4RBF4IXB+QrDj40DmJgBiEyBYMDL4qcEgUikYqDCgSGKAAUSn8hbh8BgICBl/yFggwEbhMC/4sHAAcTIhIsBGYLcJGAQOHgLAEGBM/Jg0vRgsQZAMQBAQUBif/AwLJDfoQ1DkDOBkIOCgQDBFAKCDaIRPGSwqGBgM/dwUDeQiZEEAowCgXxb5jGGgQ+GaAjaHeIbmISYToHRYTPKYQIODE4cfEA4AEPghtEgQJCI5Mv+AXHJAiIIBggXFj/xDBMCcAYXGga7LcYIXIUpY1GC4SiJVRAXCiAWJA"))

181
apps/seiko-5actus/app.js Normal file
View File

@ -0,0 +1,181 @@
var imgBg = {
width : 176, height : 176, bpp : 2,
transparent : 1,
buffer : require("heatshrink").decompress(atob("qoASv/8//1C6YASrorD6opjuorHBAIriQYwriV9FXFZldFjrSEFY6FjQcorPSYqtZFZaxaVoiDkZRArLv4rV/orT/5rGABtf/4rSq//+79UFaptIK5puHFZQUBFda5IABZuBCw4rKTAPVq4rUNw4rK/4rBroqRQAIrVQSd1N4QrQNZIAOFdbzBQ4IrOCQIIDWKQYFFZhqBHwaeBACBwBFazZQThQrJ/7CHABaSEFaTaWOIYrONJArTToorIdpDdRDQLJOCBDhRIxBoPABdXAwwrRVKQ+GZR7aZDYRHNM4IraOZyTQZbQrKaIwAKr7LOHRNdFaJzOr56RuppZACBgRAEtW1Wl1WqzWm1Nqy2qC5hyTrWq1IrFzWqyoXLv7mFQRlqqtp0tVy2V0uptNVA4JWK/72Fr4yFFY9VLINWyqJBFZtfFYwGGQZIrS////peF/6xM1KDDQQIrMKwJQFA4SEKQYLbGAoIrKv5PGGY4rGEYIAHyrZKQQoIDQhoARKwR6GBJIAXJpKECMIoAXUpaELBYQAICZR4CPYo3Kq4IBr7RITIzZFq7mOGwXXBYIjB//3/9drt/+5AGZ4RKHuoNEFZVdupQBuorBupjCJIyNIrqELr4mBr99vorEK476PBxAYC79//orCr4QBu5XIXAwiIcwxXCKYJlBFYSvIQRI7HTiLjBFYzZHAAzdBDA4AKDY6CMQgYQOABT8CEQjdJFTAAKuo8MADokkAH4A/AH4A/ABn1FldXFlfVTX4A/AH4A/AH4APr//ABVVrorcv4rL6tXFbgqL//1Qbv/1QAE1AED14re/wrK1Yr/Ff4rEcY3VFf4r/Ff4r/EaoAH/4rK14r/FZYALFb1/FZbTWAA9fFZYNBFjoA/AH4A/AH4A/vots+pu/AH4A/AH4A/ADdX6ousr4GFrohgABP/FbN/+o7O/6NZv5HOB4IrZ///LBlXB5wrO/qCNB5pzOOhgNBK7RICDhQNCX5P96td/91u4FBvpJLAoQPDrplEQRNdFYPVu93qvVO5JYJurZDSJV1FYP9FYP16oXBfJRKIbJpQB7vVv/3AwJvCbpTZVVIP9/9d6/9AALdTbJgAVEJDZMACoiCLAjZNAAqSKbpjZNSoo7PQg4zCIx9/FaBQBJ4rZRHqJQBFYzZRLCN/HooYRCQIRQYQ1dDCFVQR4A/ADFXCSK5RDJ40Iq6nPW6LcIq//fh39SrNf/6EN/47OFZp0NFbd/K5wPPI5obNM5F1FaYPNdYLcGfpA3IDQIrXABZ6FDSLcZTxAIBW4zcBFa4ZBFaLsNFZYZGFZBpIACCdBFZ7BFq7dSZJArOfQwAHHQYYBFaA+JABwhBIggrObirIJFZLuBbioXJFZI/JWJorBEJIJJS4KFRrqbKFZLvDupYSeZIrJbiwrBNwIrnTQYrReBIrNCpArKBRQAKIJQrLNhCvNJidXQSZYCPCiCTIQS6KEI4pVAAddFaF1FbAZHfioAVFYyUJWbRXHLrqxFFYhVeLI4rq6orCMAoAhFa4"))
};
/* Set hour hand image */
var imgHour = {
width : 16, height : 176, bpp : 2,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AEk//gDp///gEDAYPAh4DB+E/AYP8AaYbDEYYrDLdgD/Af4DXh/wAYIA/AGwA="))
};
/* Set minute hand image */
var imgMin = {
width : 8, height : 176, bpp : 2,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AH4A/AB8P+AB/AP4B/AIcA4DPHA="))
};
/* Set second hand image */
var imgSec = {
width : 6, height : 176, bpp : 2,
transparent : 1,
buffer : require("heatshrink").decompress(atob("qoA/ADFf6v9AU1c6vNFlICWvtXAXlVA="))
};
/* Sets the font for the date at an appropriate size of 14 */
Graphics.prototype.setFontWorkSans = function(scale) {
// Actual height 12 (12 - 1)
this.setFontCustom(atob("AAAAAAADwAAAPAAAAAAAAAvAAC/0AH/gAL/QAD9AAAIAAAAACQAAL/9AC/r9APAA8A8ADwDwAfAH//wAH/8AAAAAADwAAAtAAAH//8Af//wAAAAAAAAAABwAUAfgHwDwA/APAP8A8DzwD/9PAD/Q8AAABQAAA0AHwDwA9AHwDw4PAPDw8A///gA/f8AAAAAAAAQAAAvAAAP8AALzwAD8PAAv//wB///AAAPAAAAUAAAAAAC/88AP/y8A8NDwDywPAPD18A8L/AAAGgAAAAAAC/8AA//9ALTx8A8ODwDw4PALz/8ALD/AAAAAAPAAAA8AAADwB/APC/8A9/gAD/AAAPgAAAAAAAAAAAAB8fwAf//wDw8PAPDw8A8PDwB///AC8vwAAAAAAAAAAD/DgAv/PQDwsPAPC08A8PDwB//8AB//QAAAAAAAAAAA8DwADwPAABAE"), 46, atob("BAYJBggICQgJCAkJBA=="), 14+(scale<<8)+(2<<16));
return this;
};
/* Sets the font for the day at an appropriate size of 12 */
Graphics.prototype.setFontWorkSansSmall = function(scale) {
// Actual height 12 (11 - 0)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAABAP/zwGqjQAAAAP0AAEAAAP4AAGAAAAEoAAs/gL//QL88AAt/gL/+QK88AAYAAAUFAD+PAHPDgv//8fr70Hj7QCx+AAAAAC8AAL/AAPHBgL/PQB68AAPgAC9/APT7wEDjwAC/QAAAAAAuAC7/gL/DwPPywL9/gCwvAAD7wAABgAAAAP0AAEAAAACkAC//wP0C8sAAPYAAGPQA+D//0Af9ABQAADzAAB/AAv8AAB/AADyAABAAAACAAADgAADgAC//AAr5AADgAADQAAABAAAD9AAD0AAAAACwAACwAACwAACwAAAgAAABAAADwAADQAAAkAAv0Af9AL+AAvAAAAKgAD//ALgLgPADwPADwD//QA/9AAAAAAwAADwAAL//gL//gAAAAAgBQD4DwPAPwPA/wLnzwD/TwAUBQAAFADwPQLADwPDDwPvjwD+/AAQYAAAoAAD8AAv8AD08AP//gL//gAA8AAAAAL/PAPrDwPPDwPPDwPH/AAAoAAKgAC/+AHzrgPPDwPPDwH3/gBS+AKAAAPAAAPAbgPL/gP/QAPwAALAAAAAYAD+/ALvjwPLDwPLDwH//gBk+AAYAAD/PALTzwPDzwPDjwD//AAv8AAQBAA8DwA4DgAAAAAEBAAPD9AOD4AAAAABQAADwAAP4AANsAA8OAA4PABwHAAIUAAM8AAM8AAM8AAM8AAM8AAMoABgCAA4LAA8OAAdsAAP8AAHwAADgABgAADwAAPCzgPDzwLvBAD9AAAQAAABoAAv/wC0A8DD/OLPX3OMDjONHDHP/7Dfi5B4HQAP9AAACgAC/gB/8AP48AP48AB/8AAC/gAACgAAAAL//gP//wPDDwPDDwLv3gD+/AAAYAAGQAB/+AH0fQPADwPADwPADwDwPQBgNAAAAAL//gP//wPADwPADwHQDgD//AA/8AAAAAAAAAL//gP//wPDDwPDDwPDDwPADwAAAAAAAAL//gP//gPDAAPDAAPDAAPAAAAGQAB/+AD0fQPADwPCjwPDzwHz/QBy/gAAAAAAAAL//gL//gADAAADAAADAAL//gL//gAAAAAAAAL//gL//gAAAAAAeAAAvgAADwAADwL//gP//AAAAAAAAAL//gL//gAPAAA/0ADx/APAPwIABgAAAAL//gL//wAADwAADwAADwAACgAAAAL//gP6qQC/gAAH/QAAvwAv9AL9AAP//wGqqQAAAAL//gL6qQC+AAAP0AAB/AL//wL//gAAAAAGQAB/+AH0fQPADwPADwPADwD6vQB/9AAGQAAAAAL//gP//gPDwAPDwAH7gAD/AAAAAAAGQAB/+AH0fQPADwPAD9PAD/D6vbB/9PAGQAAAAAL//gP//gPDgAPD0ALr/AD/LwAUAgAUFAD+PALvDwPHDwPDjwLT7gDx/AAAAALAAAPAAAPAAAP//wPqqQPAAAPAAAAAAAP/+AGqvgAADwAADwAADwL//AL/4AAAAAKAAAL+AAAv9AAAvwAB/gAv8AL9AAKAAAKQAAL/QAAf/gAAPwAv/QL+AAL/gAAL/gAAvwC/+AP9AAEAAAEABgPgLwD9+AAfwAA/8AL0fgPADwOAAAL0AAB/AAAH/wA/qQL4AAPAAAFACgPAPwPA/wPHzwPvDwP4DwLQDwAAAAAAAAv///uqqvsAAPdAAAf4AAB/0AAC/wAAC4cAAKsAAPv///Kqqp"), 32, atob("BAMFCAgLCAMEBAcHAwYDBQgFBwcHBwcHBwcEBAcHBwcLCAgICQgHCQkEBwgHCgkJCAkICAcJCAwHBwgEBQQ="), 12+(scale<<8)+(2<<16));
return this;
};
/* Set variables to get screen width, height and center points */
let W = g.getWidth();
let H = g.getHeight();
let cx = W/2;
let cy = H/2;
let Timeout;
Bangle.loadWidgets();
/* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */
Bangle.drawWidgets = function () {
var w = g.getWidth(), h = g.getHeight();
var pos = {
tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left
tr:{x:w-1, y:0, r:1, c:0},
bl:{x:0, y:h-24, r:0, c:0},
br:{x:w-1, y:h-24, r:1, c:0}
};
if (global.WIDGETS) {
for (var wd of WIDGETS) {
var p = pos[wd.area];
if (!p) continue;
wd.x = p.x - p.r*wd.width;
wd.y = p.y;
p.x += wd.width*(1-2*p.r);
p.c++;
}
g.reset(); // also loads the current theme
try {
for (var wd of WIDGETS) {
g.setClipRect(wd.x,wd.y, wd.x+wd.width-1,23);
wd.draw(wd);
}
} catch (e) { print(e); }
g.reset(); // clears the clipping rectangle!
}
};
/* Draws the clock hands and date */
function drawHands() {
let d = new Date();
let hour = d.getHours() % 12;
let min = d.getMinutes();
let sec = d.getSeconds();
let twoPi = 2*Math.PI;
let Pi = Math.PI;
let hourAngle = (hour+(min/60))/12 * twoPi - Pi;
let minAngle = (min/60) * twoPi - Pi;
let secAngle = (sec/60) * twoPi - Pi;
g.setFontWorkSans();
g.setColor(g.theme.bg);
g.setFontAlign(0,0,0);
g.drawString(d.getDate(),162,90);
g.setFontWorkSansSmall();
let weekDay = d.toString().split(" ");
if (weekDay[0] == "Sat"){g.setColor(0,0,1);}
else if (weekDay[0] == "Sun"){g.setColor(1,0,0);}
else {g.setColor(g.theme.bg);}
g.drawString(weekDay[0].toUpperCase(), 137, 90);
handLayers = [
{x:cx,
y:cy,
image:imgHour,
rotate:hourAngle,
center:true
},
{x:cx,
y:cy,
image:imgMin,
rotate:minAngle,
center:true
},
{x:cx,
y:cy,
image:imgSec,
rotate:secAngle,
center:true
}];
g.setColor(g.theme.fg);
g.drawImages(handLayers);
}
function drawBackground() {
g.clear(1);
g.setBgColor(g.theme.bg);
g.setColor(g.theme.fg);
bgLayer = [
{x:cx,
y:cy,
image:imgBg,
center:true
}];
g.drawImages(bgLayer);
g.reset();
}
/* Refresh the display every second */
function displayRefresh() {
g.clear(true);
drawBackground();
drawHands();
Bangle.drawWidgets();
let Pause = 1000 - (Date.now() % 1000);
Timeout = setTimeout(displayRefresh,Pause);
}
Bangle.on('lcdPower', (on) => {
if (on) {
if (Timeout != null) { clearTimeout(Timeout); Timeout = undefined;}
displayRefresh();
}
});
Bangle.setUI("clock");
// load widgets after 'setUI' so they're aware there is a clock active
Bangle.loadWidgets();
displayRefresh();

View File

@ -0,0 +1,17 @@
{ "id": "seiko-5actus",
"name": "Seiko 5actus",
"shortName":"5actus",
"icon": "seiko-5actus.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.01",
"description": "A watch designed after then Seiko 5actus from the 1970's",
"tags": "clock",
"type": "clock",
"supports":["BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"seiko-5actus.app.js","url":"app.js"},
{"name":"seiko-5actus.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -9,3 +9,4 @@
0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight.
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
0.11: Now also runs on Bangle.js 2 with basic functionality
0.12: Full functionality on Bangle.js 2: Bangle.js 1 buttons mapped to touch areas.

View File

@ -2,23 +2,21 @@
You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint.
*Note for **Bangle.js 2:** Currently only the BTN3 functionality is working with the Bangle.js 2 button.*
Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed.
The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information.
## Buttons and Controls
BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint
*(Mapping for **Bangle.js 2**: BTN2 = Touch upper right side; BTN3 = Touch lower right side; BTN4 = Touch left side)*
***Bangle.js 2:** Currently only this button function is working*
BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint
### [A]ltitude mode
BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum speed/alt values recorded.
BTN1 : Long press > 2 secs resets the recorded maximum values.
BTN1 : Long press > 2 secs resets the recorded maximum values. *(Bangle.js 2: Long press > 0.4 secs)*
### [D]istance mode
@ -32,7 +30,7 @@ BTN1 : Select next waypoint.
BTN2 : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts.
BTN3 : Long press exit and return to watch.
BTN3 : Long press exit and return to watch. *(Bangle.js 2: Long press BTN > 2 secs)*
BTN4 : Left Display Tap : Swaps which figure is in the large display. You can have either speed or [A]ltitude/[D]istance on the large primary display.

View File

@ -349,7 +349,7 @@ function drawSecondary(n,u) {
s = 30; // Font size
if (BANGLEJS2) s *= fontFactorB2;
buf.setFontVector(s);
buf.drawString(u,xu - (BANGLEJS2*20),screenH_TwoThirds-25);
buf.drawString(u,xu - (BANGLEJS2*xu/5),screenH_TwoThirds-25);
}
function drawTime() {
@ -391,7 +391,7 @@ function drawWP() { // from waypoints.json - see README.md
buf.setFontAlign(-1,1); //left, bottom
if (BANGLEJS2) s *= fontFactorB2;
buf.setFontVector(s);
buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 20));
buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 15));
}
if ( cfg.modeA == 2 ) { // clock/large mode
@ -421,7 +421,7 @@ function drawSats(sats) {
buf.drawString('A',screenW,140-(BANGLEJS2 * 40));
if ( showMax ) {
buf.setFontAlign(0,1); //centre, bottom
buf.drawString('MAX',120,164);
buf.drawString('MAX',screenW_Half,screenH_TwoThirds + 4);
}
}
if ( cfg.modeA == 0 ) buf.drawString('D',screenW,140-(BANGLEJS2 * 40));
@ -536,22 +536,18 @@ function onGPS(fix) {
}
function setButtons(){
if (!BANGLEJS2) { // Buttons for Bangle.js
// Spd+Dist : Select next waypoint
setWatch(function(e) {
var dur = e.time - e.lastTime;
if ( cfg.modeA == 1 ) {
// Spd+Alt mode - Switch between fix and MAX
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
function btn1press(longpress) {
if(emulator) console.log("Btn1, long="+longpress);
if ( cfg.modeA == 1 ) { // Spd+Alt mode - Switch between fix and MAX
if ( !longpress ) showMax = !showMax; // Short press toggle fix/max display
else { max.spd = 0; max.alt = 0; } // Long press resets max values.
}
else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint
onGPS(lf);
}, BTN1, { edge:"falling",repeat:true});
// Power saving on/off
setWatch(function(e){
}
function btn2press(){
if(emulator) console.log("Btn2");
pwrSav=!pwrSav;
if ( pwrSav ) {
LED1.reset();
@ -564,52 +560,51 @@ if (!BANGLEJS2) { // Buttons for Bangle.js
Bangle.setLCDPower(1);
LED1.set();
}
}, BTN2, {repeat:true,edge:"falling"});
// Toggle between alt or dist
setWatch(function(e){
}
function btn3press(){
if(emulator) console.log("Btn3");
cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 2 ) cfg.modeA = 0;
if(emulator)console.log("cfg.modeA="+cfg.modeA);
savSettings();
onGPS(lf);
}, BTN3, {repeat:true,edge:"falling"});
// Touch left screen to toggle display
setWatch(function(e){
}
function btn4press(){
if(emulator) console.log("Btn4");
cfg.primSpd = !cfg.primSpd;
savSettings();
onGPS(lf); // Update display
}, BTN4, {repeat:true,edge:"falling"});
}
function setButtons(){
if (!BANGLEJS2) { // Buttons for Bangle.js 1
setWatch(function(e) {
btn1press(( e.time - e.lastTime) > 2); // > 2 sec. is long press
}, BTN1, { edge:"falling",repeat:true});
// Power saving on/off (red dot visible if off)
setWatch(btn2press, BTN2, {repeat:true,edge:"falling"});
// Toggle between alt or dist
setWatch(btn3press, BTN3, {repeat:true,edge:"falling"});
// Touch left screen to toggle display
setWatch(btn4press, BTN4, {repeat:true,edge:"falling"});
} else { // Buttons for Bangle.js 2
setWatch(function(e){ // Bangle.js BTN3
cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 2 ) cfg.modeA = 0;
if(emulator)console.log("cfg.modeA="+cfg.modeA);
savSettings();
onGPS(lf);
}, BTN1, {repeat:true,edge:"falling"});
setWatch(function(e) {
btn1press(( e.time - e.lastTime) > 0.4); // > 0.4 sec. is long press
}, BTN1, { edge:"falling",repeat:true});
/* Bangle.on('tap', function(data) { // data - {dir, double, x, y, z}
cfg.primSpd = !cfg.primSpd;
if(emulator)console.log("!cfg.primSpd");
}); */
/* Bangle.on('swipe', function(dir) {
if (dir < 0) { // left: Bangle.js BTN3
cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 2 ) cfg.modeA = 0;
if(emulator)console.log("cfg.modeA="+cfg.modeA);
}
Bangle.on('touch', function(btn_l_r, e) {
if(e.x < screenW_Half) btn4press();
else
{ // right: Bangle.js BTN4
cfg.primSpd = !cfg.primSpd;
if(emulator)console.log("!cfg.primSpd");
}
if (e.y < screenH_Half)
btn2press();
else
btn3press();
});
*/
savSettings();
onGPS(lf);
}
}
@ -700,18 +695,6 @@ Bangle.on('lcdPower',function(on) {
else stopDraw();
});
/*
function onGPSraw(nmea) {
var nofGP = 0, nofBD = 0, nofGL = 0;
if (nmea.slice(3,6) == "GSV") {
// console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
SATinView = nofGP + nofBD + nofGL;
} }
if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
*/
var gpssetup;
try {

View File

@ -2,7 +2,7 @@
"id": "speedalt",
"name": "GPS Adventure Sports",
"shortName": "GPS Adv Sport",
"version": "0.11",
"version": "0.12",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"icon": "app.png",
"type": "app",

View File

@ -3,7 +3,7 @@
"name": "Terminal Clock",
"shortName":"Terminal Clock",
"description": "A terminal cli like clock displaying multiple sensor data",
"version":"0.01",
"version":"0.02",
"icon": "app.png",
"type": "clock",
"tags": "clock",

1
apps/todolist/ChangeLog Normal file
View File

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

40
apps/todolist/README.md Normal file
View File

@ -0,0 +1,40 @@
Todo List
========
This is a simple Todo List application.
![](screenshot2.png)
The content is loaded from a JSON file.
You can mark a task as completed.
JSON file content example:
```javascript
[
{
name: "Pro",
children: [
{
name: "Read doc",
done: true,
children: [],
}
],
},
{
name: "Pers",
children: [
{
name: "Grocery",
children: [
{ name: "Milk", done: false, children: [] },
{ name: "Eggs", done: false, children: [] },
{ name: "Cheese", done: false, children: [] },
],
},
{ name: "Workout", done: false, children: [] },
{ name: "Learn Rust", done: false, children: [] },
],
},
]
```

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgmjiMRiAWTgIXUCoYZQB4IADC4YHECxkSkIECkQYLEwMSkQQBkcyCAMTmYKEiIuGif/AAIXBmciiUzC4MvBQPyC44LCC4YADBYpIFiM/BYZDBC5EhC4wKCBYKLFEYkxC5UxCwsSBYgXK/5GEmYuDC5oAKC/4XUmK5DC6PziMfC6cimTRB+bbDiSpCC5ItBaIXxbIg2CF5QqBB4IcCAAQvMCYMhdIi//X7P/X6sz+S/CkQADX8gXCif/GQIADMwS/LZ4a//BgkyJBK/ll/zmYADX54FBX9cyB4ZHEO5wPDa/7RJAAshC4xyCABacBC40SGBsxiIWEgEBW4gAKFwowCABwWGACgA=="))

129
apps/todolist/app.js Normal file
View File

@ -0,0 +1,129 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
// Const
let TODOLIST_FILE = "todolist.json";
let MAX_DESCRIPTION_LEN = 14;
// Clear todolist file
// require("Storage").erase(TODOLIST_FILE);
let DEFAULT_TODOLIST = [
{
name: "Pro",
children: [
{
name: "Read doc",
done: true,
children: [],
},
],
},
{
name: "Pers",
children: [
{
name: "Grocery",
children: [
{ name: "Milk", done: false, children: [] },
{ name: "Eggs", done: false, children: [] },
{ name: "Cheese", done: false, children: [] },
],
},
{ name: "Workout", done: false, children: [] },
{ name: "Learn Rust", done: false, children: [] },
],
},
];
// Load todolist
let todolist =
require("Storage").readJSON(TODOLIST_FILE, true) || DEFAULT_TODOLIST;
let menus = {};
function writeData() {
require("Storage").writeJSON(TODOLIST_FILE, todolist);
}
function getChild(todolist, indexes) {
let childData = todolist;
for (let i = 0; i < indexes.length; i++) {
childData = childData[indexes[i]];
childData = childData.children;
}
return childData;
}
function getName(item) {
let title = item.name.substr(0, MAX_DESCRIPTION_LEN);
return title;
}
function getParentTitle(todolist, indexes) {
let parentIndexes = indexes.slice(0, indexes.length - 1);
let lastIndex = indexes[indexes.length - 1];
let item = getItem(todolist, parentIndexes, lastIndex);
return getName(item);
}
function getItem(todolist, parentIndexes, index) {
let childData = getChild(todolist, parentIndexes, index);
return childData[index];
}
function toggleableStatus(todolist, indexes, index) {
const reminder = getItem(todolist, indexes, index);
return {
value: !!reminder.done, // !! converts undefined to false
format: (val) => (val ? "[X]" : "[-]"),
onchange: (val) => {
reminder.done = val;
writeData();
},
};
}
function showSubMenu(key) {
const sub_menu = menus[key];
return E.showMenu(sub_menu);
}
function createListItem(todolist, indexes, index) {
let reminder = getItem(todolist, indexes, index);
if (reminder.children.length > 0) {
let childIndexes = [];
for (let i = 0; i < indexes.length; i++) {
childIndexes.push(indexes[i]);
}
childIndexes.push(index);
createMenus(todolist, childIndexes);
return () => showSubMenu(childIndexes);
} else {
return toggleableStatus(todolist, indexes, index);
}
}
function showMainMenu() {
const mainmenu = menus[""];
return E.showMenu(mainmenu);
}
function createMenus(todolist, indexes) {
const menuItem = {};
if (indexes.length == 0) {
menuItem[""] = { title: "todolist" };
} else {
menuItem[""] = { title: getParentTitle(todolist, indexes) };
menuItem["< Back"] = () =>
showSubMenu(indexes.slice(0, indexes.length - 1));
}
for (let i = 0; i < getChild(todolist, indexes).length; i++) {
const item = getItem(todolist, indexes, i);
const name = getName(item);
menuItem[name] = createListItem(todolist, indexes, i);
}
menus[indexes] = menuItem;
}
createMenus(todolist, []);
showMainMenu();

BIN
apps/todolist/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,23 @@
{
"id": "todolist",
"name": "TodoList",
"shortName": "TodoList",
"version": "0.01",
"type": "app",
"description": "Simple Todo List",
"icon": "app.png",
"allow_emulator": true,
"tags": "tool,todo",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{ "name": "todolist.app.js", "url": "app.js" },
{ "name": "todolist.img", "url": "app-icon.js", "evaluate": true }
],
"data": [{ "name": "todolist.json" }],
"screenshots": [
{ "url": "screenshot1.png" },
{ "url": "screenshot2.png" },
{ "url": "screenshot3.png" }
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -5,3 +5,4 @@
0.05: "Chime the time" (buzz or beep) with up/down swipe added
0.06: Redraw widgets when time is updated
0.07: Fix problem with "Bangle.CLOCK": github.com/espruino/BangleApps/issues/1437
0.08: Redraw widgets only once per minute

View File

@ -81,7 +81,7 @@ function draw() {
executeCommands();
Bangle.drawWidgets();
if (process.env.HWVERSION==2) Bangle.drawWidgets();
}
var timeout;

View File

@ -1,7 +1,7 @@
{
"id": "vectorclock",
"name": "Vector Clock",
"version": "0.07",
"version": "0.08",
"description": "A digital clock that uses the built-in vector font.",
"icon": "app.png",
"type": "clock",

1
apps/widclose/ChangeLog Normal file
View File

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

7
apps/widclose/README.md Normal file
View File

@ -0,0 +1,7 @@
# Close Button
Adds a ![X](preview.png) button to close the current app and go back to the clock.
(Widget is not visible on the clock screen)
![Light theme screenshot](screenshot_light.png)
![Dark theme screenshot](screenshot_dark.png)

BIN
apps/widclose/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,15 @@
{
"id": "widclose",
"name": "Close Button",
"version": "0.01",
"description": "A button to close the current app",
"readme": "README.md",
"icon": "icon.png",
"type": "widget",
"tags": "widget,tools",
"supports": ["BANGLEJS2"],
"screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}],
"storage": [
{"name":"widclose.wid.js","url":"widget.js"}
]
}

BIN
apps/widclose/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

14
apps/widclose/widget.js Normal file
View File

@ -0,0 +1,14 @@
if (!Bangle.CLOCK) WIDGETS.close = {
area: "tr", width: 24, sortorder: 10, // we want the right-most spot please
draw: function() {
Bangle.removeListener("touch", this.touch);
Bangle.on("touch", this.touch);
g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button
// b/w version of preview.png, 24x24
"GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA"
), this.x, this.y);
}, touch: function(_, c) {
const w = WIDGETS.close;
if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) load();
}
};

122
backup.js Normal file
View File

@ -0,0 +1,122 @@
/* Code to handle Backup/Restore functionality */
const BACKUP_STORAGEFILE_DIR = "storage-files";
function bangleDownload() {
var zip = new JSZip();
Progress.show({title:"Scanning...",sticky:true});
var normalFiles, storageFiles;
console.log("Listing normal files...");
Comms.reset()
.then(() => Comms.showMessage("Backing up..."))
.then(() => Comms.listFiles({sf:false}))
.then(f => {
normalFiles = f;
console.log(" - "+f.join(","));
console.log("Listing StorageFiles...");
return Comms.listFiles({sf:true});
}).then(f => {
storageFiles = f;
console.log(" - "+f.join(","));
var fileCount = normalFiles.length + storageFiles.length;
var promise = Promise.resolve();
// Normal files
normalFiles.forEach((filename,n) => {
if (filename==".firmware") {
console.log("Ignoring .firmware file");
return;
}
promise = promise.then(() => {
Progress.hide({sticky: true});
var percent = n/fileCount;
Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0});
return Comms.readFile(filename).then(data => zip.file(filename,data));
});
});
// Storage files
if (storageFiles.length) {
var zipStorageFiles = zip.folder(BACKUP_STORAGEFILE_DIR);
storageFiles.forEach((filename,n) => {
promise = promise.then(() => {
Progress.hide({sticky: true});
var percent = (normalFiles.length+n)/fileCount;
Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0});
return Comms.readStorageFile(filename).then(data => zipStorageFiles.file(filename,data));
});
});
}
return promise;
}).then(() => {
return Comms.showMessage(Const.MESSAGE_RELOAD);
}).then(() => {
return zip.generateAsync({type:"binarystring"});
}).then(content => {
Progress.hide({ sticky: true });
showToast('Backup complete!', 'success');
Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip");
}).catch(err => {
Progress.hide({ sticky: true });
showToast('Backup failed, ' + err, 'error');
});
}
function bangleUpload() {
Espruino.Core.Utils.fileOpenDialog({
id:"backup",
type:"arraybuffer",
mimeType:".zip,application/zip"}, function(data) {
if (data===undefined) return;
var promise = Promise.resolve();
var zip = new JSZip();
var cmds = "";
zip.loadAsync(data).then(function(zip) {
return showPrompt("Restore from ZIP","Are you sure? This will remove all existing apps");
}).then(()=>{
Progress.show({title:`Reading ZIP`});
zip.forEach(function (path, file){
console.log("path");
promise = promise
.then(() => file.async("string"))
.then(data => {
console.log("decoded", path);
if (path.startsWith(BACKUP_STORAGEFILE_DIR)) {
path = path.substr(BACKUP_STORAGEFILE_DIR.length+1);
cmds += AppInfo.getStorageFileUploadCommands(path, data)+"\n";
} else if (!path.includes("/")) {
cmds += AppInfo.getFileUploadCommands(path, data)+"\n";
} else console.log("Ignoring "+path);
});
});
return promise;
})
.then(() => {
Progress.hide({sticky:true});
Progress.show({title:`Erasing...`});
return Comms.removeAllApps(); })
.then(() => {
Progress.hide({sticky:true});
Progress.show({title:`Restoring...`, sticky:true});
return Comms.showMessage(`Restoring...`); })
.then(() => Comms.write("\x10"+Comms.getProgressCmd()+"\n"))
.then(() => Comms.uploadCommandList(cmds, 0, cmds.length))
.then(() => Comms.showMessage(Const.MESSAGE_RELOAD))
.then(() => {
Progress.hide({sticky:true});
showToast('Restore complete!', 'success');
})
.catch(err => {
Progress.hide({sticky:true});
showToast('Restore failed, ' + err, 'error');
});
return promise;
});
}
window.addEventListener('load', (event) => {
document.getElementById("downloadallapps").addEventListener("click",event=>{
bangleDownload();
});
document.getElementById("uploadallapps").addEventListener("click",event=>{
bangleUpload();
});
});

2
core

@ -1 +1 @@
Subproject commit affb0b15b41eb35a1548373831af7001bad64435
Subproject commit 27c7db6035832837ca3909ea52939f60803df72f

View File

@ -81,7 +81,7 @@ a.btn.btn-link.dropdown-toggle {
min-height: 8em;
}
.tile-content { position: relative; }
.tile-content { position: relative; word-break: break-all; }
.link-github {
position:absolute;
top: 36px;

View File

@ -131,6 +131,8 @@
<button class="btn" id="removeall">Remove all Apps</button>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite">Install favourite apps</button></p>
<p><button class="btn tooltip tooltip-right" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip tooltip-right" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button></p>
<h3>Settings</h3>
<div class="form-group">
<label class="form-switch">
@ -170,6 +172,8 @@
<script src="core/lib/heatshrink.js"></script>
<script src="core/js/utils.js"></script>
<script src="loader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> <!-- for backup.js -->
<script src="backup.js"></script>
<script src="core/js/ui.js"></script>
<script src="core/js/comms.js"></script>
<script src="core/js/appinfo.js"></script>