1
0
Fork 0

Merge remote-tracking branch 'upstream/master'

master
hughbarney 2021-08-26 17:59:27 +01:00
commit 93cefbcd33
15 changed files with 1259 additions and 67 deletions

View File

@ -616,7 +616,7 @@
{ "id": "widbat",
"name": "Battery Level Widget",
"icon": "widget.png",
"version":"0.07",
"version":"0.08",
"description": "Show the current battery level and charging status in the top right of the clock",
"tags": "widget,battery,b2",
"type":"widget",
@ -2409,10 +2409,10 @@
"name": "Acceleration Logger",
"shortName":"Accel Log",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"interface": "interface.html",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"tags": "outdoor",
"tags": "outdoor,b2",
"readme": "README.md",
"storage": [
{"name":"accellog.app.js","url":"app.js"},
@ -3411,6 +3411,21 @@
{"name":"thermomF.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "nixie",
"name": "Nixie Clock",
"shortName":"Nixie",
"icon": "nixie.png",
"version":"0.01",
"description": "A nixie tube clock for both Bangle 1 and 2.",
"tags": "clock",
"type":"clock",
"readme": "README.md",
"storage": [
{"name":"nixie.app.js","url":"app.js"},
{"name":"nixie.img","url":"app-icon.js","evaluate":true},
{"name":"m_vatch.js","url":"m_vatch.js"}
]
},
{ "id": "carcrazy",
"name": "Car Crazy",
"shortName":"Car Crazy",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Use the new multiplatform 'Layout' library

View File

@ -90,16 +90,26 @@ function startRecord(force) {
// display
g.clear(1);
Bangle.drawWidgets();
var w = g.getWidth();
var h = g.getHeight();
g.setColor("#ff0000").fillRect(0,h-48,w,h);
g.setColor("#ffffff").setFont("6x8",2).setFontAlign(0,0).drawString("RECORDING", w/2,h-24);
g.setFont("6x8").drawString("Samples:",w/2,h/3 - 20);
g.setFont("6x8").drawString("Time:",w/2,h*2/3 - 20);
g.setFont("6x8",2).setFontAlign(0,0,1).drawString("STOP",w-10,h/2);
var Layout = require("Layout");
var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5},
{type:"txt", font:"6x8", label:"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5},
{type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:true},
]
},[ // Buttons...
{label:"STOP", cb:()=>{
Bangle.removeListener('accel', accelHandler);
showMenu();
}}
]);
layout.update();
layout.render();
// now start writing
f = require("Storage").open(getFileName(fileNumber), "w");
var f = require("Storage").open(getFileName(fileNumber), "w");
f.write("Time (ms),X,Y,Z\n");
var start = getTime();
var sampleCount = 0;
@ -113,17 +123,14 @@ function startRecord(force) {
accel.z*8192].map(n=>Math.round(n)).join(",")+"\n");
sampleCount++;
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString(" "+sampleCount+" ",w/2,h/3,true);
g.drawString(" "+Math.round(t)+"s ",w/2,h*2/3,true);
layout.samples.label = sampleCount;
layout.time.label = Math.round(t)+"s";
layout.render(layout.samples);
layout.render(layout.time);
}
Bangle.setPollInterval(80); // 12.5 Hz
Bangle.setPollInterval(80); // 12.5 Hz - the default
Bangle.on('accel', accelHandler);
setWatch(()=>{
Bangle.removeListener('accel', accelHandler);
showMenu();
}, BTN2);
}

View File

@ -1,3 +1,18 @@
![Screenshot 2021-08-23 100720](https://user-images.githubusercontent.com/89286474/130498574-0b5246cb-8553-455c-b6c0-096e5ca1644c.png)
# Car Crazy
Car crazy is a fun game where you tilt your wrist left and right to avoid incoming cars. If you get hit by a car you lose a heart. In the game you have three hearts, if you get hit 3 times you are sent to the game over screen. Your goal is to try to last as long as you can. Because this game is still in beta please report any bugs here: https://forms.office.com/r/HnwYzG9Sk7.
Form Link: https://forms.office.com/r/HnwYzG9Sk7
### Images:
(Coming Soon)
### Instructions:
BNT2: Hold down this button to start the game if you are on the starting page and game over page.
Tilting Left-Right: Tilt your wrist left and right to steer your car and try not to get hit by the enemy car.
### Feautures Coming Soon:
0.02: Levels are creating making the game get harder as it goes along.
0.03: Optional soundtrack in settings. More levels.
0.04: With higher scores you can now unlock different colors of cars.

1
apps/nixie/ChangeLog Normal file
View File

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

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

@ -0,0 +1,17 @@
## Nixie clock
This clock displays the time in nixie-inspired numerals and works on both Bangle versions (1 and 2). It uses a generic
coordinate system (0 <= width < 1) and has helper functions to use inline.
The app makes use of a module called "m_vatch" which manages all the timers, and makes calls to functions in the 'main' file
to manage drawing the background, time, and any data like sensor info, step counters, battery, etc. The idea is that it is
reusable if you write many watch apps... you just need to implement functions to draw the background (called on start, and every
time the 'mode' changes (regular and night mode), the time (which gets a call every second), and the data (also every second,
except not in night mode)).
Night mode is a mode that can be set manually or automatically, allowing the watch code to adjust colors and detail. Mainly,
used as a night clock, you can draw no background, and use dim colors for your digits. If set to auto, the accelerometer is used so
when the watch is placed on its side, it switches to night mode (your watch may need a tweak... and Bangle 2 is a different story!)
It also handles step counting so that it's stored on a daily
basis - survives a system reset, zeroes when the date changes and keeps a record in a history file by day.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBIf4A/AH4A/AH4AtgtVAANQAwIFCAwYTGBIQDBqsF6AXCqFQroXDBQQXFAxNUBRAQKAAXVAYUEFBY7EBgtVC5UEDotdERwQBC4pGDTgQXJgoRDMoQGCqisFR5pICDQQwDCAxVJZAYXKQo7nEC6AtBCYgXGCYYDCJQYXBF5ThDKwoNCB4UMC4yyBToIGDAYNUDoRiBO5CyBLwi5Ea4RyGAH4A/AH4A/AH4A/ACQ="))

429
apps/nixie/app.js Normal file
View File

@ -0,0 +1,429 @@
const EMULATOR = false;
// which Bangle?
const isB2 = g.getWidth() < 200;
// global coordinate system
const wX = g.getWidth();
const wY = g.getHeight();
const midX = wX/2, midY = wY/2;
// relative positioning: send 0 <= coord < 1
function relX(x) { return Math.floor(x*wX); }
function relY(y) { return Math.floor(y*wY); }
// colors
const col_bg = 0;
const col_nm =isB2 ? 1 :"#206040";
const col_sep = isB2 ? 6 :"#202020";
const col_off = isB2 ? 1 : "#202020";
const col_shad1 = isB2 ? 4 :"#FF0000";
const col_shad2 = isB2 ? 6 :"#FF6000";
const col_hi =isB2 ? 7 : "#FFC000";
const col_data = isB2 ? 6 :"#C06000";
g.setBgColor(col_bg);
g.clear();
var imgTube = {
width : 64, height : 128, bpp : 2,
buffer : require("heatshrink").decompress(atob("AE9AB7sQD54AOiFQB5tVsgPN0uoBxkByEFB5kGyIPNhVVB5tpLwKAMoJuOgNQggMJgtVDhsVqtEZ5cVrWlEBcFtWq1WlJxUaBwOq1IgJgIdCqoABEBEC1WVBwTkGKgUGFYIOCgIRDC4kaFoVUOQQKCQ4IgCB4YKDCYIgCq2QgEqHwJLIEoOkgFqB4KaIEoNkB4Z7JHQVqquqD5EVDYQPCVRIPE1IPKgsAtJTCAA8GyEBD4TrKqAPOgNRB5sRB5wfPgAPOiA/RP4IPaiD6BB5oCBB5kAdQIPNH5wPCvIPMBgIPMR4QPcL4QPNgIPQvS/MqtAB59+B9cVB91VL91BF91RF79RB4OVD5wPsH59BB51FB5sQB/0AD7xvPV4elD5wPLqIPOgJPeX/6//X8YPMH5wPPL74PfN55PQB6TfPB5afDB51/D57P/Z/7P/B97vOB5kAB58VoAA="))
};
var imgTubeBW = {
width : 46, height : 92, bpp : 1,
buffer : require("heatshrink").decompress(atob("AD0EAomAAgcCBQkQEykwAgcP/gFD/wKECok4AgcB4A7DgwQEjAFEsYWExg2DhkgAoVAE4kA8AEDgZqEhw+JgA+DCwIKEhhrJCyJELFqBbQIiByLIk6gWZyC3WOSItWOVq3nCywA="))
};
require("Font8x12").add(Graphics);
g.setFont("8x12", 1);
let interval = null;
let alarming = false;
let nightMode = false;
// our scale factor
let xs = 0.5 * wX/240;
let ys = 0.75 * wY/240;
let prevH1 = -1;
let prevH2 = -1;
let prevM1 = -1;
let prevM2 = -1;
let points0 = new Uint8Array([
0, 40,
1, 35,
7, 20,
16, 8,
28, 2,
40, 0,
51, 2,
63, 10,
72, 20,
77, 35,
78, 40,
78, 59,
77, 64,
72, 79,
63, 89,
51, 97,
40, 99,
28, 97,
16, 91,
7, 79,
1, 64,
0, 59,
0, 40
]);
let points1 = new Uint8Array([ 40, 99, 40, 0]);
let points2 = new Uint8Array([ 0, 25,
2, 22,
6, 13,
17, 5,
28, 2,
40, 0,
52, 2,
63, 5,
74, 13,
79, 23,
79, 28,
74, 38,
63, 46,
51, 54,
40, 58,
29, 62,
17, 68,
8, 80,
0, 99,
79, 99
]);
let points4 = new Uint8Array([ 60, 99, 60, 0, 0, 75, 79, 75 ]);
let points8 = new Uint8Array([
40, 40,
26, 42,
15, 46,
4, 56,
1, 66,
1, 77,
6, 87,
17, 94,
28, 97,
38, 99,
42, 99,
52, 97,
63, 94,
74, 87,
79, 77,
79, 66,
75, 56,
64, 46,
54, 42,
40, 40,
52, 39,
62, 34,
69, 29,
72, 23,
72, 19,
69, 12,
62, 6,
52, 2,
40, 0,
28, 2,
18, 6,
11, 12,
8, 19,
8, 23,
11, 29,
18, 34,
28, 39,
40, 40,
]);
let points6 = new Uint8Array([
50, 0,
4, 56,
1, 66,
1, 77,
6, 87,
17, 94,
28, 97,
40, 99,
52, 97,
63, 94,
74, 87,
79, 77,
79, 66,
75, 56,
64, 46,
52, 42,
40, 40,
26, 42,
15, 46,
4, 56,
]);
let points3 = new Uint8Array([
1, 77,
6, 87,
17, 94,
28, 97,
40, 99,
52, 97,
63, 94,
74, 87,
79, 77,
79, 66,
75, 56,
64, 46,
52, 42,
39, 40,
79, 0,
1, 0
]);
let points7 = new Uint8Array([ 0, 0, 79, 0, 30, 99 ]);
let points9 = new Uint8Array(points6.length);
let points5 = new Uint8Array([
1, 77,
6, 87,
17, 94,
28, 97,
38, 99,
42, 99,
52, 97,
63, 94,
74, 87,
79, 77,
79, 66,
75, 56,
64, 46,
54, 42,
40, 40,
26, 42,
15, 46,
27, 0,
79, 0,
]);
function drawPoints(points, x0, y0) {
let x = points[0]*xs+x0, y = points[1]*ys+y0;
//g.drawEllipse(x-2, y-2, x+2, y+2);
g.moveTo(x, y);
for(let idx=1; idx*2 < points.length; idx ++) {
let x = points[idx*2]*xs+x0;
let y = points[idx*2+1]*ys+y0;
//g.drawEllipse(x-2, y-2, x+2, y+2);
g.lineTo(x, y);
}
}
/* create 5 from 2 */
/* uncomment if you want the 5 to look more authentic (but uglier)
for (let idx=0; idx*2 < points2.length; idx++) {
points5[idx*2] = points2[idx*2];
points5[idx*2+1] = 99-points2[idx*2+1];
}
*/
/* create 9 from 6 */
for (let idx=0; idx*2 < points6.length; idx++) {
points9[idx*2] = 79-points6[idx*2];
points9[idx*2+1] = 99-points6[idx*2+1];
}
pointsArray = [points0, points1, points2, points3, points4, points5, points6, points7, points8, points9];
function eraseDigit(d, x, y) {
if(d < 0 || d > 9) return;
g.setColor(col_bg);
if(nightMode) {
drawPoints(pointsArray[d], x, y);
return;
}
drawPoints(pointsArray[d], x-2, y-2);
drawPoints(pointsArray[d], x+2, y-2);
drawPoints(pointsArray[d], x-2, y+2);
drawPoints(pointsArray[d], x+2, y+2);
drawPoints(pointsArray[d], x-1, y-1);
drawPoints(pointsArray[d], x+1, y-1);
drawPoints(pointsArray[d], x-1, y+1);
drawPoints(pointsArray[d], x+1, y+1);
}
function drawDigit(d, x, y) {
if(nightMode) {
g.setColor(col_nm);
drawPoints(pointsArray[d], x, y);
return;
}
g.setColor(col_off);
for (let idx = pointsArray.length - 1; idx >= 0 ; idx--) {
if(idx == d) {
g.setColor(col_shad1);
drawPoints(pointsArray[d], x-2, y-2);
drawPoints(pointsArray[d], x+2, y-2);
drawPoints(pointsArray[d], x-2, y+2);
drawPoints(pointsArray[d], x+2, y+2);
g.setColor(col_shad2);
drawPoints(pointsArray[d], x-1, y-1);
drawPoints(pointsArray[d], x+1, y-1);
drawPoints(pointsArray[d], x-1, y+1);
drawPoints(pointsArray[d], x+1, y+1);
g.setColor(col_hi);
drawPoints(pointsArray[d], x, y);
g.setColor(col_off);
} else {
drawPoints(pointsArray[idx], x, y);
}
}
}
function drawBkgd(nm) {
g.clear();
prevH1=-1;prevH2=-1;prevM1=-1;prevM2=-1;
if(nm) return;
if(!isB2) {
// tube images
g.setColor(col_shad2);
[relX(0),relX(0.25),relX(0.5),relX(0.75)].forEach((v,i,a) => {
g.drawImage(imgTube,v,relY(0.225));
});
// something to sit on
g.setColor(col_shad2);
g.fillRect(0, relY(0.76),wX,relY(0.76));
} else {
// simple tubes
[1,45,89,133].forEach((v,i,a) => {
g.setColor(col_shad1);
g.drawEllipse(v, 52, v+41, 90);
g.drawRect(v,66,v+41,125);
g.clearRect(v+1,66,v+40,124);
});
}
g.setColor(col_shad2);
g.moveTo(relX(0.125), 0);
g.lineTo(relX(0.25), relY(0.125));
g.lineTo(relX(0.75), relY(0.125));
g.lineTo(relX(0.875),0);
g.moveTo(relX(0.125), wY);
g.lineTo(relX(0.25), relY(0.875));
g.lineTo(relX(0.75), relY(0.875));
g.lineTo(relX(0.875), wY);
}
function drawTime(d,nm) {
const dx = [relX(0.042), relX(0.29), relX(0.55), relX(0.791)]; //[ 10, 65, 135, 190];
const dy = [relY(0.38),relY(0.38),relY(0.38),relY(0.38)];
let h1 = Math.floor(d.hour / 10);
let h2 = d.hour % 10;
let m1 = Math.floor(d.min / 10);
let m2 = d.min % 10;
if(h1 == prevH1 && h2 == prevH2 && m1 == prevM1 && m2 == prevM2) {
return;
}
nightMode = nm;
if(h1 != prevH1) {
eraseDigit(prevH1, dx[0], dy[0]);
drawDigit(h1, dx[0], dy[0]);
}
if(h2 != prevH2) {
eraseDigit(prevH2, dx[1], dy[1]);
drawDigit(h2, dx[1], dy[1]);
}
if(m1 != prevM1) {
eraseDigit(prevM1, dx[2], dy[2]);
drawDigit(m1, dx[2], dy[2]);
}
if(m2 != prevM2) {
eraseDigit(prevM2, dx[3], dy[3]);
drawDigit(m2, dx[3], dy[3]);
}
prevH1 = h1;
prevH2 = h2;
prevM1 = m1;
prevM2 = m2;
}
function drawData(d) {
if(!nightMode) {
g.setColor(col_data);
g.setFontAlign(0, -1);
g.drawString(` ${d.dow}, ${d.mon3} ${d.date} `, wX/2, relX(0.042), true);
g.setFontAlign(-1,-1);
g.drawString("STEP ", 0, relY(0.82), true);
g.drawString(`${d.steps} `,0, relY(0.875), true);
g.setFontAlign(1,-1);
g.drawString(" BTY", relX(0.999), relY(0.82), true);
g.drawString(` ${d.batt}`, relX(0.999), relY(0.875), true);
g.setFontAlign(0,-1);
g.setColor(col_shad2);
g.drawString('BANGLE.JS', wX/2, relY(0.925));
}
}
//setWatch(E.showLauncher, BTN1, {repeat:true,edge:"falling"});
if(EMULATOR) {
let d = new Date();
let hour = d.getHours();
let minute = d.getMinutes();
let h1 = Math.floor(hour / 10);
let h2 = hour % 10;
let m1 = Math.floor(minute / 10);
let m2 = minute % 10;
let data = {
h1: h1,
h2: h2,
m1: m1,
m2: m2,
hour: hour,
min: minute,
};
drawBkgd(nightMode);
drawTime(data, nightMode);
const mstr="JanFebMarAprMayJunJulAugSepOctNovDec";
const dowstr = "SunMonTueWedThuFriSat";
let month = d.getMonth();
let dow = d.getDay();
data.month = month;
data.date = d.getDate();
data.mon3 = mstr.slice(month*3,month*3+3);
data.dow = dowstr.substr(dow*3,3);
data.dateStr = data.dow + " " + data.mon3 + " " + data.date;
data.steps = 12345;
data.batt = E.getBattery() + (Bangle.isCharging() ? "+" : "");
data.charging = Bangle.isCharging();
drawData(data);
} else {
Bangle.setUI("clock");
let v = require("m_vatch.js");
v.setDrawTime(drawTime);
v.setDrawBackground(drawBkgd);
v.setDrawData(drawData);
v.begin();
}

317
apps/nixie/m_vatch.js Normal file
View File

@ -0,0 +1,317 @@
const _Storage = require('Storage');
let interval = null;
let nightMode = false;
let stepFile = 'v.steps.json';
let stepArchiveFile = 'v.stephist.json';
let _Options = {};
let optsFile = 'm_vatch.opts.json';
let _Alarm = {
inAlarm: false,
reload: () => {},
scheduleAlarms: () => {},
showMsg: (title, msg) => {},
showNotes: () => {},
};
let _StepData = {};
const pad0 = (n) => (n > 9) ? n : ("0"+n);
const getToday = () => {
let d = new Date();
return d.getFullYear()+'-'+ pad0(d.getMonth()+1) + '-' + pad0(d.getDate());
};
function reload() {
_StepData = _Storage.readJSON(stepFile);
if(!_StepData) {
_StepData = {
lastDate: '2020-01-01',
stepCache: 0,
lastStepCount: 0,
updated: true,
};
}
if(getToday() === _StepData.lastDate) {
_StepData.stepCache += _StepData.lastStepCount;
_StepData.lastStepCount = 0;
}
}
function stringFromArray(data)
{
var count = data.length;
var str = "";
for(var index = 0; index < count; index += 1)
str += String.fromCharCode(data[index]);
return str;
}
function logD(str) {
if(_Options.debug) console.log(str);
}
let lastH1 = -1;
let lastH2 = -1;
let lastM1 = -1;
let lastM2 = -1;
let drawBackground = () => {};
let drawTime = () => {};
let drawData = () => {};
function timeCheck() {
if(_Alarm.inAlarm) return;
logD('Again, ' + JSON.stringify(_Options));
logD('opt.nm = '+_Options.autoNightMode);
if(_Options.autoNightMode) {
// this may vary by Bangle.. adjust to taste
let a = Bangle.getAccel();
a.x = Math.floor(a.x * 100);
logD('a.x = ' + a.x);
if(a.x <= 101 && a.x >= 99) {
if(!nightMode) {
nightMode = ! nightMode;
redrawScreen();
}
} else {
if(nightMode) {
nightMode = ! nightMode;
redrawScreen();
}
}
}
let d = new Date();
let hour = d.getHours();
let minute = d.getMinutes();
let h1 = Math.floor(hour / 10);
let h2 = hour % 10;
let m1 = Math.floor(minute / 10);
let m2 = minute % 10;
logD("lastH1 = "+lastH1+": lastM2 = "+lastM2);
if(h1 == lastH1 && h2 == lastH2 && m1 == lastM1 && m2 == lastM2) {
return;
}
logD("drawing time");
let data = {
h1: h1,
h2: h2,
m1: m1,
m2: m2,
hour: hour,
min: minute,
};
drawTime(data, nightMode);
lastH1 = h1;
lastH2 = h2;
lastM1 = m1;
lastM2 = m2;
if(!nightMode && !_Alarm.inAlarm) {
logD("drawing data...");
const mstr="JanFebMarAprMayJunJulAugSepOctNovDec";
const dowstr = "SunMonTueWedThuFriSat";
let month = d.getMonth();
let dow = d.getDay();
data.month = month;
data.date = d.getDate();
data.mon3 = mstr.slice(month*3,month*3+3);
data.dow = dowstr.substr(dow*3,3);
data.dateStr = data.dow + " " + data.mon3 + " " + data.date;
data.steps = _StepData.stepCache + _StepData.lastStepCount;
data.batt = E.getBattery() + (Bangle.isCharging() ? "+" : "");
data.charging = Bangle.isCharging();
drawData(data);
}
if(_StepData.updated) {
_Storage.writeJSON(stepFile, _StepData);
logD(JSON.stringify(_StepData));
_StepData.updated = false;
}
}
function stop () {
if (interval) {
clearInterval(interval);
}
}
function start () {
if (interval) {
clearInterval(interval);
}
// first time init
interval = setInterval(timeCheck, 1000);
timeCheck();
}
function btn1Func() {
logD("btn1Func");
if(_Alarm.inAlarm ) {
_Alarm.inAlarm = false;
} else {
if( ! _Options.autoNightMode) {
nightMode = ! nightMode;
logD('nm is '+nightMode);
}
}
redrawScreen();
}
function redrawScreen() {
logD("redrawScreen");
if(nightMode) {
g.setRotation(1,0);
} else {
g.setRotation(0,0);
}
lastM1 = -1;
lastM2 = -1;
lastH1 = -1;
lastH2 = -1;
drawBackground(nightMode);
timeCheck();
}
function btn2Func() {
_Alarm.reload();
_Alarm.scheduleAlarms();
_Alarm.showNotes();
}
Bangle.on('step', function(cnt) {
if(!_StepData.lastDate) return;
if(_StepData.lastDate !== getToday()) {
// save previous day's step count
try {
let sf = _Storage.readJSON(stepArchiveFile);
if(!sf) sf = [];
logD('sf is '+ (typeof sf) +':'+sf);
// trim to 30
if(sf.length >= 30 ) sf.shift();
let steps = _StepData.stepCache +_StepData.lastStepCount;
let sd = `${_StepData.lastDate},${steps}`;
sf.push(sd);
_Storage.writeJSON(stepArchiveFile, sf);
} catch (err) {
_Storage.write('err.txt',err);
}
/* individual step files by date
_Storage.write(_StepData.lastDate +'.steps', JSON.stringify(
_StepData.stepCache +_StepData.lastStepCount
));
*/
_StepData.stepCache = 0 - cnt;
_StepData.lastDate = getToday();
}
_StepData.lastStepCount = cnt;
_StepData.updated = true;
});
/*
** Advertise a writeable characteristic. Accepts text (in 20 char
** chunks) terminated with __EOM__ by itself. If there's text, show
** it (as an alarm), otherwise reload the alarm & msg files (empty
** string signals another BLE process updated those files)
*/
/*
var BLEMessage = "";
NRF.setServices({
"feb10001-f00d-ea75-7192-abbadabadebb": {
"feb10002-f00d-ea75-7192-abbadabadebb": {
value : [0],
maxLen : 20,
writable : true,
onWrite : function(evt) {
let str = stringFromArray(evt.data);
if(str === "__EOM__") {
if(BLEMessage) {
showMsg('Message',BLEMessage);
} else {
reload();
scheduleAlarms();
showMsg('', 'Reloading...');
}
BLEMessage = '';
} else {
BLEMessage += str;
}
}
}
}
}, { });
*/
exports.setDrawBackground = function(dBkgd) {
drawBackground = dBkgd;
};
exports.setDrawTime = function(dTime) {
drawTime = dTime;
};
exports.setDrawData = function( dData) {
drawData = dData;
};
exports.begin = function() {
_Options = _Storage.readJSON(optsFile);
if(!_Options) _Options = {
autoNightMode: true,
useAlarms: false,
stepManager: true,
debug: true,
};
console.log(JSON.stringify(_Options));
if(_Options.useAlarms) {
_Alarm = require('m_alarms');
_Alarm.reload();
_Alarm.scheduleAlarms();
}
// separate the Bangles now
const isB2 = g.getWidth() < 200;
if(!isB2) {
Bangle.on('lcdPower', function (on) {
if (on) {
start();
} else {
stop();
}
});
setWatch(btn1Func, BTN1, {repeat:true,edge:"falling"});
if(_Options.useAlarms) {
setWatch(btn2Func, BTN2, {repeat:true,edge:"falling"});
}
setWatch(Bangle.showLauncher, BTN3, {repeat:false,edge:"falling"});
}
reload();
drawBackground(nightMode);
start();
};

10
apps/nixie/nixie.info Normal file
View File

@ -0,0 +1,10 @@
{
"id":"jvNixie",
"name":"Nixie Clock",
"type":"clock",
"src":"nixie.app.js",
"icon": "nixie.img",
"sortorder":1,
"version":"1.1",
"files":"nixie.info,nixie.app.js,nixie.img, m_vatch.js"
}

BIN
apps/nixie/nixie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

View File

@ -4,3 +4,4 @@
0.05: Fix regression stopping correct widget updates
0.06: Use 'g.theme' (requires bootloader 0.23)
0.07: Move CHARGING variable to more readable string
0.08: Ensure battery updates every 60s even if LCD was on at boot and stays on

View File

@ -23,7 +23,7 @@
Bangle.drawWidgets(); // relayout widgets
g.flip();
});
var batteryInterval;
var batteryInterval = Bangle.isLCDOn() ? setInterval(()=>WIDGETS["bat"].draw(), 60000) : undefined;
Bangle.on('lcdPower', function(on) {
if (on) {
WIDGETS["bat"].draw();

View File

@ -1,39 +1,101 @@
if (!g.theme) {
g.theme = {
fg:-1,bg:0,fgH:-1,bgH:"#008"
};
}
/*
Usage:
var layout = new Layout( layoutObject, btns )
layout.render(optionalObject);
layoutObject has:
* A `type` field of:
* `undefined` - blank, can be used for padding
* `"txt"` - a text label, with value `label` and `r` for text rotation
* `"btn"` - a button, with value `label` and callback `cb`
* `"img"` - an image where the function `src` is called to return an image to draw
* `"custom"` - a custom block where `render(layoutObj)` is called to render
* `"h"` - Horizontal layout, `c` is an array of more `layoutObject`
* `"v"` - Veritical layout, `c` is an array of more `layoutObject`
* A `id` field. If specified the object is added with this name to the
returned `layout` object, so can be referenced as `layout.foo`
* A `font` field, eg `6x8` or `30%` to use a percentage of screen height
* A `col` field, eg `#f00` for red
* A `bgCol` field for background color (will automatically fill on render)
* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center
* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center
* A `pad` integer field to set pixels padding
* A `fillx` boolean to choose if the object should fill available space in x
* A `filly` boolean to choose if the object should fill available space in y
* `width` and `height` fields to optionally specify minimum size
btns is an array of objects containing:
* `label` - the text on the button
* `cb` - a callback function
* `cbl` - a callback function for long presses
Once `layout.update()` is called, the following fields are added
to each object:
* `x` and `y` for the top left position
* `w` and `h` for the width and height
* `_w` and `_h` for the **minimum** width and height
Other functions:
* `layout.update()` - update positions of everything if contents have changed
* `layout.debug(obj)` - draw outlines for objects on screen
* `layout.clear(obj)` - clear the given object (you can also just specify `bgCol` to clear before each render)
*/
function Layout(layout, buttons) {
this._l = this.l = layout;
this.b = buttons;
// Do we have physical buttons?
this.physBtn = process.env.HWVERSION!=2;
// Do we have >1 physical buttons?
this.physBtns = (process.env.HWVERSION==2) ? 1 : 3;
this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0;
if (buttons) {
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / buttons.length);
if (this.physBtn) {
if (buttons) {
if (this.physBtns >= buttons.length) {
// enough physical buttons
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / this.physBtns);
if (Bangle.btnWatch) Bangle.btnWatch.forEach(clearWatch);
Bangle.btnWatch = [];
if (this.physBtns > 2 && buttons.length==1)
buttons.unshift({label:""}); // pad so if we have a button in the middle
while (this.physBtns > buttons.length)
buttons.push({label:""});
if (buttons[0]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,0), BTN1, {repeat:true,edge:-1}));
if (buttons[1]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,1), BTN2, {repeat:true,edge:-1}));
if (buttons[2]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,2), BTN3, {repeat:true,edge:-1}));
this._l.width = g.getWidth()-8; // text width
this._l = {type:"h", content: [
this._l = {type:"h", filly:1, c: [
this._l,
{type:"v", content: buttons.map(b=>(b.type="txt",b.font="6x8",b.height=btnHeight,b.r=1,b))}
{type:"v", pad:1, filly:1, c: buttons.map(b=>(b.type="txt",b.font="6x8",b.height=btnHeight,b.r=1,b))}
]};
} else { // no physical buttons, use touchscreen
} else {
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / buttons.length);
this._l.width = g.getWidth()-20; // button width
this._l = {type:"h", content: [
this._l = {type:"h", c: [
this._l,
{type:"v", content: buttons.map(b=>(b.type="btn",b.height=btnHeight,b.width=32,b.r=1,b))}
{type:"v", c: buttons.map(b=>(b.type="btn",b.h=btnHeight,b.w=32,b.r=1,b))}
]};
Bangle.touchHandler = (_,e) => touchHandler(this._l,e);
Bangle.on('touch',Bangle.touchHandler);
}
}
// add IDs
var ll = this;
function idRecurser(l) {
if (l.id) ll[l.id] = l;
if (l.c) l.c.forEach(idRecurser);
}
idRecurser(layout);
this.update();
}
Layout.prototype.remove = function (l) {
@ -59,7 +121,7 @@ function pressHandler(btn,e) {
function touchHandler(l,e) {
if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h)
l.cb(e);
if (l.content) l.content.forEach(n => touchHandler(n,e));
if (l.c) l.c.forEach(n => touchHandler(n,e));
}
@ -68,7 +130,13 @@ function updateMin(l) {
case "txt": {
if (l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
g.setFont(l.font);
// Not needed in new firmwares - 'font' is enough
if (l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
}
g.setFont(l.font,l.fsz);
l._h = g.getFontHeight();
l._w = g.stringWidth(l.label);
break;
@ -84,6 +152,7 @@ function updateMin(l) {
l._w = im.charCodeAt(1);
break;
}
case undefined:
case "custom": {
// size should already be set up in width/height
l._w = 0;
@ -91,17 +160,17 @@ function updateMin(l) {
break;
}
case "h": {
l.content.forEach(updateMin);
l._h = l.content.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.content.reduce((a,b)=>a+b._w+(b.pad<<1),0);
l.fill |= l.content.some(c=>c.fill);
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0);
l.fillx |= l.c.some(c=>c.fillx);
break;
}
case "v": {
l.content.forEach(updateMin);
l._h = l.content.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.content.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
l.fill |= l.content.some(c=>c.fill);
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
l.filly |= l.c.some(c=>c.filly);
break;
}
default: throw "Unknown item type "+l.type;
@ -116,9 +185,10 @@ function render(l) {
if (!l) l = this.l;
g.reset();
if (l.col) g.setColor(l.col);
if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w,l.y+l.h);
switch (l.type) {
case "txt":
g.setFont(l.font).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1));
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1), true/*solid bg*/);
break;
case "btn":
var poly = [
@ -141,7 +211,7 @@ function render(l) {
l.render(l);
break;
}
if (l.content) l.content.forEach(render);
if (l.c) l.c.forEach(render);
}
Layout.prototype.render = function (l) {
@ -152,23 +222,24 @@ Layout.prototype.render = function (l) {
Layout.prototype.layout = function (l) {
// l = current layout element
// exw,exh = extra width/height available
var fill = l.content.reduce((a,l)=>a+(0|l.fill),0);
var fillx = l.c.reduce((a,l)=>a+(0|l.fillx),0);
var filly = l.c.reduce((a,l)=>a+(0|l.filly),0);
switch (l.type) {
case "h": {
let x = l.x + (l.w-l._w)/2;
if (fill) { x = l.x; }
l.content.forEach(c => {
c.w = c._w + (c.fill?(l.w-l._w)/fill:0);
c.h = c.fill ? l.h : c._h;
if (fillx) { x = l.x; }
l.c.forEach(c => {
c.w = c._w + (c.fillx?(l.w-l._w)/fillx:0);
c.h = c.filly ? l.h : c._h;
c.x = x;
c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2;
x += c.w;
if (c.pad) {
x += c.pad*2;
c.x += c.pad;
c.y += c.pad;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.content) {
if (c.c) {
this.layout(c);
}
});
@ -176,19 +247,19 @@ Layout.prototype.layout = function (l) {
}
case "v": {
let y = l.y + (l.h-l._h)/2;
if (fill) { y = l.y; }
l.content.forEach(c => {
c.w = c.fill ? l.w : c._w;
c.h = c._h + (c.fill?(l.h-l._h)/fill:0);
if (filly) { y = l.y; }
l.c.forEach(c => {
c.w = c.fillx ? l.w : c._w;
c.h = c._h + (c.filly?(l.h-l._h)/filly:0);
c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2;
c.y = y;
y += c.h;
if (c.pad) {
y += c.pad*2;
c.x += c.pad;
c.y += c.pad;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.content) this.layout(c);
if (c.c) this.layout(c);
});
break;
}
@ -199,7 +270,7 @@ Layout.prototype.debug = function(l,c) {
c=c||1;
g.setColor(c&1,c&2,c&4).drawRect(l.x+c-1, l.y+c-1, l.x+l.w-c, l.y+l.h-c);
c++;
if (l.content) l.content.forEach(n => this.debug(n,c));
if (l.c) l.c.forEach(n => this.debug(n,c));
};
Layout.prototype.update = function() {
var l = this._l;
@ -209,7 +280,7 @@ Layout.prototype.update = function() {
// update sizes
updateMin(l);
// center
if (l.fill) {
if (l.fillx || l.filly) {
l.w = w;
l.h = h;
l.x = 0;
@ -226,7 +297,9 @@ Layout.prototype.update = function() {
Layout.prototype.clear = function(l) {
if (!l) l = this._l;
g.reset().clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1);
g.reset();
if (l.bgCol!==undefined) g.setBgColor(l.bgCol);
g.clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1);
};
exports = Layout;

305
modules/Layout.min.js vendored Normal file
View File

@ -0,0 +1,305 @@
/*
Usage:
var layout = new Layout( layoutObject, btns )
layout.render(optionalObject);
layoutObject has:
* A `type` field of:
* `undefined` - blank, can be used for padding
* `"txt"` - a text label, with value `label` and `r` for text rotation
* `"btn"` - a button, with value `label` and callback `cb`
* `"img"` - an image where the function `src` is called to return an image to draw
* `"custom"` - a custom block where `render(layoutObj)` is called to render
* `"h"` - Horizontal layout, `c` is an array of more `layoutObject`
* `"v"` - Veritical layout, `c` is an array of more `layoutObject`
* A `id` field. If specified the object is added with this name to the
returned `layout` object, so can be referenced as `layout.foo`
* A `font` field, eg `6x8` or `30%` to use a percentage of screen height
* A `col` field, eg `#f00` for red
* A `bgCol` field for background color (will automatically fill on render)
* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center
* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center
* A `pad` integer field to set pixels padding
* A `fillx` boolean to choose if the object should fill available space in x
* A `filly` boolean to choose if the object should fill available space in y
* `width` and `height` fields to optionally specify minimum size
btns is an array of objects containing:
* `label` - the text on the button
* `cb` - a callback function
* `cbl` - a callback function for long presses
Once `layout.update()` is called, the following fields are added
to each object:
* `x` and `y` for the top left position
* `w` and `h` for the width and height
* `_w` and `_h` for the **minimum** width and height
Other functions:
* `layout.update()` - update positions of everything if contents have changed
* `layout.debug(obj)` - draw outlines for objects on screen
* `layout.clear(obj)` - clear the given object (you can also just specify `bgCol` to clear before each render)
*/
function Layout(layout, buttons) {
this._l = this.l = layout;
this.b = buttons;
// Do we have >1 physical buttons?
this.physBtns = (process.env.HWVERSION==2) ? 1 : 3;
this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0;
if (buttons) {
if (this.physBtns >= buttons.length) {
// enough physical buttons
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / this.physBtns);
if (Bangle.btnWatch) Bangle.btnWatch.forEach(clearWatch);
Bangle.btnWatch = [];
if (this.physBtns > 2 && buttons.length==1)
buttons.unshift({label:""}); // pad so if we have a button in the middle
while (this.physBtns > buttons.length)
buttons.push({label:""});
if (buttons[0]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,0), BTN1, {repeat:true,edge:-1}));
if (buttons[1]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,1), BTN2, {repeat:true,edge:-1}));
if (buttons[2]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,2), BTN3, {repeat:true,edge:-1}));
this._l.width = g.getWidth()-8; // text width
this._l = {type:"h", filly:1, c: [
this._l,
{type:"v", pad:1, filly:1, c: buttons.map(b=>(b.type="txt",b.font="6x8",b.height=btnHeight,b.r=1,b))}
]};
} else {
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / buttons.length);
this._l.width = g.getWidth()-20; // button width
this._l = {type:"h", c: [
this._l,
{type:"v", c: buttons.map(b=>(b.type="btn",b.h=btnHeight,b.w=32,b.r=1,b))}
]};
Bangle.touchHandler = (_,e) => touchHandler(this._l,e);
Bangle.on('touch',Bangle.touchHandler);
}
}
// add IDs
var ll = this;
function idRecurser(l) {
if (l.id) ll[l.id] = l;
if (l.c) l.c.forEach(idRecurser);
}
idRecurser(layout);
this.update();
}
Layout.prototype.remove = function (l) {
if (Bangle.btnWatch) {
Bangle.btnWatch.forEach(clearWatch);
delete Bangle.btnWatch;
}
if (Bangle.touchHandler) {
Bangle.removeListener("touch",Bangle.touchHandler);
delete Bangle.touchHandler;
}
};
// Handler for button watch events
function pressHandler(btn,e) {
if (e.time-e.lastTime > 0.75 && this.b[btn].cbl)
this.b[btn].cbl(e);
else
if (this.b[btn].cb) this.b[btn].cb(e);
}
// Handler for touch events
function touchHandler(l,e) {
if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h)
l.cb(e);
if (l.c) l.c.forEach(n => touchHandler(n,e));
}
function updateMin(l) {
switch (l.type) {
case "txt": {
if (l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
// Not needed in new firmwares - 'font' is enough
if (l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
}
g.setFont(l.font,l.fsz);
l._h = g.getFontHeight();
l._w = g.stringWidth(l.label);
break;
}
case "btn": {
l._h = 24;
l._w = 14 + l.label.length*8;
break;
}
case "img": {
var im = E.toString(l.src());
l._h = im.charCodeAt(0);
l._w = im.charCodeAt(1);
break;
}
case undefined:
case "custom": {
// size should already be set up in width/height
l._w = 0;
l._h = 0;
break;
}
case "h": {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0);
l.fillx |= l.c.some(c=>c.fillx);
break;
}
case "v": {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
l.filly |= l.c.some(c=>c.filly);
break;
}
default: throw "Unknown item type "+l.type;
}
if (l.r&1) { // rotation
var t = l._w;l._w=l._h;l._h=t;
}
l._w = Math.max(l._w, 0|l.width);
l._h = Math.max(l._h, 0|l.height);
}
function render(l) {
if (!l) l = this.l;
g.reset();
if (l.col) g.setColor(l.col);
if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w,l.y+l.h);
switch (l.type) {
case "txt":
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1), true/*solid bg*/);
break;
case "btn":
var poly = [
l.x,l.y+4,
l.x+4,l.y,
l.x+l.w-5,l.y,
l.x+l.w-1,l.y+4,
l.x+l.w-1,l.y+l.h-5,
l.x+l.w-5,l.y+l.h-1,
l.x+4,l.y+l.h-1,
l.x,l.y+l.h-5,
l.x,l.y+4
];
g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
break;
case "img":
g.drawImage(l.src(), l.x, l.y);
break;
case "custom":
l.render(l);
break;
}
if (l.c) l.c.forEach(render);
}
Layout.prototype.render = function (l) {
if (!l) l = this._l;
render(l);
};
Layout.prototype.layout = function (l) {
// l = current layout element
// exw,exh = extra width/height available
var fillx = l.c.reduce((a,l)=>a+(0|l.fillx),0);
var filly = l.c.reduce((a,l)=>a+(0|l.filly),0);
switch (l.type) {
case "h": {
let x = l.x + (l.w-l._w)/2;
if (fillx) { x = l.x; }
l.c.forEach(c => {
c.w = c._w + (c.fillx?(l.w-l._w)/fillx:0);
c.h = c.filly ? l.h : c._h;
c.x = x;
c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2;
x += c.w;
if (c.pad) {
x += c.pad*2;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.c) {
this.layout(c);
}
});
break;
}
case "v": {
let y = l.y + (l.h-l._h)/2;
if (filly) { y = l.y; }
l.c.forEach(c => {
c.w = c.fillx ? l.w : c._w;
c.h = c._h + (c.filly?(l.h-l._h)/filly:0);
c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2;
c.y = y;
y += c.h;
if (c.pad) {
y += c.pad*2;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.c) this.layout(c);
});
break;
}
}
};
Layout.prototype.debug = function(l,c) {
if (!l) l = this._l;
c=c||1;
g.setColor(c&1,c&2,c&4).drawRect(l.x+c-1, l.y+c-1, l.x+l.w-c, l.y+l.h-c);
c++;
if (l.c) l.c.forEach(n => this.debug(n,c));
};
Layout.prototype.update = function() {
var l = this._l;
var w = g.getWidth();
var y = this.yOffset;
var h = g.getHeight()-y;
// update sizes
updateMin(l);
// center
if (l.fillx || l.filly) {
l.w = w;
l.h = h;
l.x = 0;
l.y = y;
} else {
l.w = l._w;
l.h = l._h;
l.x = (w-l.w)/2;
l.y = y+(h-l.h)/2;
}
// layout children
this.layout(l);
};
Layout.prototype.clear = function(l) {
if (!l) l = this._l;
g.reset();
if (l.bgCol!==undefined) g.setBgColor(l.bgCol);
g.clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1);
};
exports = Layout;