update submodules

master
Bryan 2024-08-21 09:18:03 -06:00
commit 7c3dcfc654
286 changed files with 8126 additions and 816 deletions

View File

@ -245,4 +245,5 @@ module.exports = {
})), })),
], ],
ignorePatterns: findGeneratedJS(["apps/", "modules/"]), ignorePatterns: findGeneratedJS(["apps/", "modules/"]),
reportUnusedDisableDirectives: true,
} }

View File

@ -405,7 +405,7 @@ in an iframe.
<link rel="stylesheet" href="../../css/spectre.min.css"> <link rel="stylesheet" href="../../css/spectre.min.css">
</head> </head>
<body> <body>
<script src="../../lib/interface.js"></script> <script src="../../core/lib/interface.js"></script>
<div id="t">Loading...</div> <div id="t">Loading...</div>
<script> <script>
function onInit() { function onInit() {

BIN
apps/8ball/8ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

1
apps/8ball/ChangeLog Normal file
View File

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

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

@ -0,0 +1 @@
atob("MDCBAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/4AAAAB4AeAAAAHgAHgAAAOAABwAAAcAAA4AAA4AMAcAABwAMAOAABgA/AGAADAAeADAAHAAMADgAGAAAABgAGAAAABgAMAAAAAwAMBAAAAwAMDAAAAwAMHwAAAwAMHwAAAwAMDAACAwAMBAADAwAMAAAPgwAMAAAPgwAMAAADAwAGAAACBgAGAAAABgAHAAAADgADAAAADAADgAAAHAAB////+AAB////+AABgAAAGAABgAAAGAABgAAAGAADAAAADAADAAAADAADAAAADAAGAAAABgAGAAAABgAH/////gAP/////wAYAAAAAYAYAAAAAYAf/////4AP/////wAAAAAAAAAAAAAAAAA==")

92
apps/8ball/app.js Normal file
View File

@ -0,0 +1,92 @@
var keyboard = "textinput";
var Name = "";
Bangle.setLCDTimeout(0);
var menuOpen = 1;
var answers = new Array("no", "yes","WHAT????","What do you think", "That was a bad question", "YES!!!", "NOOOOO!!", "nope","100%","yup","why should I answer that?","think for yourself","ask again later, I'm busy", "what Was that horrible question","how dare you?","you wanted to hear yes? okay, yes", "Don't get angry when I say no","you are 100% wrong","totally, for sure","hmmm... I'll ponder it and get back to you later","wow, you really have a lot of questions", "NOPE","is the sky blue, hmmm...","I don't have time to answer","How many more questions before you change my name?","theres this thing called wikipedia","hmm... I don't seem to be able to reach the internet right now","if you phrase it like that, yes","Huh, never thought so hard in my life","The winds of time say no");
var consonants = new Array("b","c","d","f","g","h","j","k","l","m","n","p","q","r","s","t","v","w","x","y","z");
var vowels = new Array("a","e","i","o","u");
try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
function generateName()
{
Name = "";
var nameLength = Math.round(Math.random()*5);
for(var i = 0; i < nameLength; i++){
var cosonant = consonants[Math.round(Math.random()*consonants.length/2)];
var vowel = vowels[Math.round(Math.random()*vowels.length/2)];
Name = Name + cosonant + vowel;
if(Name == "")
{
generateName();
}
}
}
generateName();
function menu()
{
g.clear();
E.showMenu();
menuOpen = 1;
E.showMenu({
"" : { title : Name },
"< Back" : () => menu(),
"Start" : () => {
E.showMenu();
g.clear();
menuOpen = 0;
Drawtext("ask " + Name + " a yes or no question");
},
"regenerate name" : () => {
menu();
generateName();
},
"show answers" : () => {
var menu = new Array([]);
for(var i = 0; i < answers.length; i++){
menu.push({title : answers[i]});
}
E.showMenu(menu);
},
"Add answer" : () => {
E.showMenu();
keyboard.input({}).then(result => {if(result != ""){answers.push(result);} menu();});
},
"Edit name" : () => {
E.showMenu();
keyboard.input({}).then(result => {if(result != ""){Name = result;} menu();});
},
"Exit" : () => load(),
});
}
menu();
var answer;
function Drawtext(text)
{
g.clear();
g.setFont("Vector", 20);
g.drawString(g.wrapString(text, g.getWidth(), -20).join("\n"));
}
function WriteAnswer()
{
if (menuOpen == 0)
{
var randomnumber = Math.round(Math.random()*answers.length);
answer = answers[randomnumber];
Drawtext(answer);
setTimeout(function() {
Drawtext("ask " + Name + " a yes or no question");
}, 3000);
}
}
setWatch(function() {
menu();
}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true, edge:"falling"});
Bangle.on('touch', function(button, xy) { WriteAnswer(); });

19
apps/8ball/metadata.json Normal file
View File

@ -0,0 +1,19 @@
{ "id": "8ball",
"name": "Magic 8 ball",
"shortName":"8ball",
"icon": "8ball.png",
"version":"0.01",
"screenshots": [
{"url":"screenshot.png"},
{"url":"screenshot-1.png"},
{"url":"screenshot-2.png"}
],
"allow_emulator": true,
"description": "A very sarcastic magic 8ball",
"tags": "game",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"8ball.app.js","url":"app.js"},
{"name":"8ball.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/8ball/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
apps/8ball/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
apps/8ball/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -4,3 +4,4 @@
Trigger on 1.04g now, and record 10 samples before trigger Trigger on 1.04g now, and record 10 samples before trigger
0.03: Bangle.js 2 compatibility 0.03: Bangle.js 2 compatibility
0.04: Minor code improvements 0.04: Minor code improvements
0.05: Can record 100hz, z-axis color changed to yellow, autosave to file, no need select, delete old records

View File

@ -1,16 +1,20 @@
//var acc; //var acc;
var HZ = 100; var HZ = 100;
var SAMPLES = 5*HZ; // 5 seconds var SAMPLES = 6 * HZ; // 6 seconds
var SCALE = 5000; var SCALE = 2000;
var THRESH = 1.04; var THRESH = 1.4;
var accelx = new Int16Array(SAMPLES); var accelx = new Int16Array(SAMPLES);
var accely = new Int16Array(SAMPLES); // North var accely = new Int16Array(SAMPLES); // North
var accelz = new Int16Array(SAMPLES); // Into clock face var accelz = new Int16Array(SAMPLES); // Into clock face
var timestep = new Int16Array(SAMPLES); // Into clock face
var accelIdx = 0; var accelIdx = 0;
var lastAccel; var lastAccel;
function accelHandlerTrigger(a) {"ram" var timestep_start = 0;
function accelHandlerTrigger(a) {
"ram"
if (a.mag * 2 > THRESH) { // *2 because 8g mode if (a.mag * 2 > THRESH) { // *2 because 8g mode
tStart = getTime(); timestep_start = getTime();
g.drawString("Recording", g.getWidth() / 2, g.getHeight() / 2, 1); g.drawString("Recording", g.getWidth() / 2, g.getHeight() / 2, 1);
Bangle.removeListener('accel', accelHandlerTrigger); Bangle.removeListener('accel', accelHandlerTrigger);
Bangle.on('accel', accelHandlerRecord); Bangle.on('accel', accelHandlerRecord);
@ -21,14 +25,19 @@ function accelHandlerTrigger(a) {"ram"
lastAccel.push(a); lastAccel.push(a);
} }
} }
function accelHandlerRecord(a) {"ram"
function accelHandlerRecord(a) {
"ram"
var i = accelIdx++; var i = accelIdx++;
accelx[i] = a.x*SCALE*2; accelx[i] = a.x * SCALE * 2; // *2 because of 8g mode
accely[i] = -a.y * SCALE * 2; accely[i] = -a.y * SCALE * 2;
accelz[i] = a.z * SCALE * 2; accelz[i] = a.z * SCALE * 2;
timestep[i] = (getTime() - timestep_start) * 1000;
if (accelIdx >= SAMPLES) recordStop(); if (accelIdx >= SAMPLES) recordStop();
} }
function recordStart() {"ram"
function recordStart() {
"ram"
Bangle.setLCDTimeout(0); // force LCD on Bangle.setLCDTimeout(0); // force LCD on
accelIdx = 0; accelIdx = 0;
lastAccel = []; lastAccel = [];
@ -44,7 +53,8 @@ function recordStart() {"ram"
} }
function recordStop() {"ram" function recordStop() {
"ram"
//console.log("Length:",getTime()-tStart); //console.log("Length:",getTime()-tStart);
Bangle.setPollInterval(80); // default poll interval Bangle.setPollInterval(80); // default poll interval
Bangle.accelWr(0x18, 0b01101100); // off, +-4g Bangle.accelWr(0x18, 0b01101100); // off, +-4g
@ -52,12 +62,15 @@ function recordStop() {"ram"
Bangle.accelWr(0x18, 0b11101100); // +-4g Bangle.accelWr(0x18, 0b11101100); // +-4g
Bangle.removeListener('accel', accelHandlerRecord); Bangle.removeListener('accel', accelHandlerRecord);
E.showMessage("Finished"); E.showMessage("Finished");
showData(); showData(true);
} }
function showData() { function showData(save_file) {
g.clear(1); g.clear(1);
let csv_files_N = require("Storage").list(/^acc.*\.csv$/).length;
let w_full = g.getWidth();
let h = g.getHeight();
var w = g.getWidth() - 20; // width var w = g.getWidth() - 20; // width
var m = g.getHeight() / 2; // middle var m = g.getHeight() / 2; // middle
var s = 12; // how many pixels per G var s = 12; // how many pixels per G
@ -71,7 +84,7 @@ function showData() {
for (var i = 0; i < SAMPLES; i++) for (var i = 0; i < SAMPLES; i++)
g.lineTo(10 + i * w / SAMPLES, m - a[i] * s / SCALE); g.lineTo(10 + i * w / SAMPLES, m - a[i] * s / SCALE);
} }
g.setColor("#0000ff"); g.setColor("#FFFA5F");
plot(accelz); plot(accelz);
g.setColor("#ff0000"); g.setColor("#ff0000");
plot(accelx); plot(accelx);
@ -80,28 +93,31 @@ function showData() {
// work out stats // work out stats
var maxAccel = 0; var maxAccel = 0;
var tStart = SAMPLES, tEnd = 0; var tStart = SAMPLES,
var vel = 0, maxVel = 0; tEnd = 0;
var max_YZ = 0;
for (var i = 0; i < SAMPLES; i++) { for (var i = 0; i < SAMPLES; i++) {
var a = accely[i]/SCALE; var a = Math.abs(accely[i] / SCALE);
let a_yz = Math.sqrt(Math.pow(accely[i] / SCALE, 2) + Math.pow(accelz[i] / SCALE, 2));
if (a > 0.1) { if (a > 0.1) {
if (i < tStart) tStart = i; if (i < tStart) tStart = i;
if (i > tEnd) tEnd = i; if (i > tEnd) tEnd = i;
} }
if (a > maxAccel) maxAccel = a; if (a > maxAccel) maxAccel = a;
vel += a/HZ; if (a_yz > max_YZ) max_YZ = a_yz;
if (vel>maxVel) maxVel=vel;
} }
g.reset(); g.reset();
g.setFont("6x8").setFontAlign(1, 0); g.setFont("6x8").setFontAlign(1, 0);
g.drawString("Max Y Accel: "+maxAccel.toFixed(2)+" g",g.getWidth()-14,g.getHeight()-50); g.drawString("Max X Accel: " + maxAccel.toFixed(2) + " g", g.getWidth() - 14, g.getHeight() - 50);
g.drawString("Max Y Vel: "+maxVel.toFixed(2)+" m/s",g.getWidth()-14,g.getHeight()-40); g.drawString("Max YZ Accel: " + max_YZ.toFixed(2) + " g", g.getWidth() - 14, g.getHeight() - 40);
g.drawString("Time moving: " + (tEnd - tStart) / HZ + " s", g.getWidth() - 14, g.getHeight() - 30); g.drawString("Time moving: " + (tEnd - tStart) / HZ + " s", g.getWidth() - 14, g.getHeight() - 30);
//console.log("End Velocity "+vel); g.setFont("6x8", 2).setFontAlign(0, 0);
g.drawString("File num: " + (csv_files_N + 1), w_full / 2, h - 20);
g.setFont("6x8").setFontAlign(0, 0, 1); g.setFont("6x8").setFontAlign(0, 0, 1);
g.drawString("FINISH", g.getWidth() - 4, g.getHeight() / 2); g.drawString("FINISH", g.getWidth() - 4, g.getHeight() / 2);
setWatch(function() { setWatch(function() {
showMenu(); if (save_file) showSaveMenu(); // when select only plot, don't ask for save option
else showMenu();
}, global.BTN2 ? BTN2 : BTN); }, global.BTN2 ? BTN2 : BTN);
} }
@ -133,20 +149,23 @@ function showMenu() {
E.showMenu(); E.showMenu();
if (accelIdx == 0) countDown(); if (accelIdx == 0) countDown();
else E.showPrompt("Overwrite Recording?").then(ok => { else E.showPrompt("Overwrite Recording?").then(ok => {
if (ok) countDown(); else showMenu(); if (ok) countDown();
else showMenu();
}); });
}, },
"Plot": function() { "Plot": function() {
E.showMenu(); E.showMenu();
if (accelIdx) showData(); if (accelIdx) showData(false);
else E.showAlert("No Data").then(() => { else E.showAlert("No Data").then(() => {
showMenu(); showMenu();
}); });
}, },
"Save" : function() { "Storage": function() {
E.showMenu(); E.showMenu();
if (accelIdx) showSaveMenu(); if (require("Storage").list(/^acc.*\.csv$/).length)
else E.showAlert("No Data").then(()=>{ StorageMenu();
else
E.showAlert("No Data").then(() => {
showMenu(); showMenu();
}); });
}, },
@ -158,22 +177,77 @@ function showMenu() {
} }
function showSaveMenu() { function showSaveMenu() {
var menu = { E.showPrompt("Save recording?").then(ok => {
"" : { title : "Save" } if (ok)
}; SaveFile();
[1,2,3,4,5,6].forEach(i=>{ else
var fn = "accelrec."+i+".csv"; showMenu();
var exists = require("Storage").read(fn)!==undefined; });
menu["Recording "+i+(exists?" *":"")] = function() { }
var csv = "";
function SaveFile() {
let csv_files_N = require("Storage").list(/^acc.*\.csv$/).length;
//if (csv_files_N > 20)
// E.showMessage("Storage is full");
// showMenu();
let csv = "";
let date = new Date();
let fn = "accelrec_" + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + "_" + (csv_files_N + 1) + ".csv";
E.showMessage("Saveing to file \n" + fn);
for (var i = 0; i < SAMPLES; i++) for (var i = 0; i < SAMPLES; i++)
csv += `${accelx[i]/SCALE},${accely[i]/SCALE},${accelz[i]/SCALE}\n`; csv += `${timestep[i]},${accelx[i]/SCALE},${accely[i]/SCALE},${accelz[i]/SCALE}\n`;
require("Storage").write(fn, csv); require("Storage").write(fn, csv);
showMenu(); showMenu();
}
//Show saved csv files
function StorageMenu() {
var menu = {
"": {
title: "Storage"
}
};
let csv_files = require("Storage").list(/^acc.*\.csv$/);
var inx = 0;
csv_files.forEach(fn => {
inx++;
menu[inx + ". " + fn] = function() {
StorageOptions(fn);
}; };
}); });
menu["< Back"] = function() {showMenu();}; menu["< Back"] = function() {
showMenu();
};
E.showMenu(menu); E.showMenu(menu);
} }
function StorageOptions(file) {
let menu = {
"": {
title: "Options"
},
"Plot": function() {
showMenu();
},
"Delete": function() {
E.showMenu();
E.showPrompt("Delete recording?").then(ok => {
if (ok)
DeleteRecord(file);
else
StorageMenu();
});
},
"< Back": function() {
StorageMenu();
},
};
E.showMenu(menu);
}
function DeleteRecord(file) {
E.showMessage("Deleteing file \n" + file);
require("Storage").erase(file);
StorageMenu();
}
showMenu(); showMenu();

View File

@ -37,7 +37,7 @@ function getData() {
</div>`; </div>`;
promise = promise.then(function() { promise = promise.then(function() {
document.querySelector(`.btn[fn='${fn}'][act='save']`).addEventListener("click", function() { document.querySelector(`.btn[fn='${fn}'][act='save']`).addEventListener("click", function() {
Util.saveCSV(fn.slice(0,-4), "X,Y,Z\n"+fileData[fn]); Util.saveCSV(fn.slice(0,-4), "Time,X,Y,Z\n"+fileData[fn]);
}); });
document.querySelector(`.btn[fn='${fn}'][act='delete']`).addEventListener("click", function() { document.querySelector(`.btn[fn='${fn}'][act='delete']`).addEventListener("click", function() {
Util.showModal("Deleting..."); Util.showModal("Deleting...");

View File

@ -2,7 +2,7 @@
"id": "accelrec", "id": "accelrec",
"name": "Acceleration Recorder", "name": "Acceleration Recorder",
"shortName": "Accel Rec", "shortName": "Accel Rec",
"version": "0.04", "version": "0.05",
"description": "This app puts the Bangle's accelerometer into 100Hz mode and reads 2 seconds worth of data after movement starts. The data can then be exported back to the PC.", "description": "This app puts the Bangle's accelerometer into 100Hz mode and reads 2 seconds worth of data after movement starts. The data can then be exported back to the PC.",
"icon": "app.png", "icon": "app.png",
"tags": "", "tags": "",

View File

@ -50,3 +50,4 @@
0.45: Fix new alarm when selectedAlarm is undefined 0.45: Fix new alarm when selectedAlarm is undefined
0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it. 0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it.
0.47: Fix wrap around when snoozed through midnight 0.47: Fix wrap around when snoozed through midnight
0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year.

View File

@ -2,7 +2,7 @@
This app allows you to add/modify any alarms, timers and events. This app allows you to add/modify any alarms, timers and events.
Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered. Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered. If a datetime input app (e.g. datetime_picker) is detected, it will be used for the selection of the date+time of events.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps. It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.

View File

@ -50,13 +50,17 @@ function handleFirstDayOfWeek(dow) {
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
function getLabel(e) { function getLabel(e) {
const dateStr = e.date && require("locale").date(new Date(e.date), 1); const dateStr = getDateText(e.date);
return (e.timer return (e.timer
? require("time_utils").formatDuration(e.timer) ? require("time_utils").formatDuration(e.timer)
: (dateStr ? `${dateStr}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : "")) : (dateStr ? `${dateStr}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : ""))
) + (e.msg ? ` ${e.msg}` : ""); ) + (e.msg ? ` ${e.msg}` : "");
} }
function getDateText(d) {
return d && (settings.menuDateFormat === "mmdd" ? d.substring(d.startsWith(new Date().getFullYear()) ? 5 : 0) : require("locale").date(new Date(d), 1));
}
function trimLabel(label, maxLength) { function trimLabel(label, maxLength) {
if(settings.showOverflow) return label; if(settings.showOverflow) return label;
return (label.length > maxLength return (label.length > maxLength
@ -75,10 +79,10 @@ function formatAlarmProperty(msg) {
} }
} }
function showMainMenu(scroll, group) { function showMainMenu(scroll, group, scrollback) {
const menu = { const menu = {
"": { "title": group || /*LANG*/"Alarms & Timers", scroll: scroll }, "": { "title": group || /*LANG*/"Alarms & Timers", scroll: scroll },
"< Back": () => group ? showMainMenu() : load(), "< Back": () => group ? showMainMenu(scrollback) : load(),
/*LANG*/"New...": () => showNewMenu(group) /*LANG*/"New...": () => showNewMenu(group)
}; };
const getGroups = settings.showGroup && !group; const getGroups = settings.showGroup && !group;
@ -98,7 +102,7 @@ function showMainMenu(scroll, group) {
}); });
if (!group) { if (!group) {
Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g)); Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller.scroll));
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu(); menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
} }
@ -138,6 +142,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) {
var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm"); var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm");
var keyboard = "textinput"; var keyboard = "textinput";
try {keyboard = require(keyboard);} catch(e) {keyboard = null;} try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
var datetimeinput;
try {datetimeinput = require("datetimeinput");} catch(e) {datetimeinput = null;}
const menu = { const menu = {
"": { "title": title }, "": { "title": title },
@ -145,7 +151,28 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) {
prepareAlarmForSave(alarm, alarmIndex, time, date); prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload(); saveAndReload();
showMainMenu(scroll, group); showMainMenu(scroll, group);
}, }
};
if (alarm.date && datetimeinput) {
menu[`${getDateText(date.toLocalISOString().slice(0,10))} ${require("time_utils").formatTime(time)}`] = {
value: date,
format: v => "",
onchange: v => {
setTimeout(() => {
var datetime = new Date(v.getTime());
datetime.setHours(time.h, time.m);
datetimeinput.input({datetime}).then(result => {
time.h = result.getHours();
time.m = result.getMinutes();
prepareAlarmForSave(alarm, alarmIndex, time, result, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group);
});
}, 100);
}
};
} else {
Object.assign(menu, {
/*LANG*/"Hour": { /*LANG*/"Hour": {
value: time.h, value: time.h,
format: v => ("0" + v).substr(-2), format: v => ("0" + v).substr(-2),
@ -179,7 +206,11 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) {
min: new Date().getFullYear(), min: new Date().getFullYear(),
max: 2100, max: 2100,
onchange: v => date.setFullYear(v) onchange: v => date.setFullYear(v)
}, }
});
}
Object.assign(menu, {
/*LANG*/"Message": { /*LANG*/"Message": {
value: alarm.msg, value: alarm.msg,
format: formatAlarmProperty, format: formatAlarmProperty,
@ -241,7 +272,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) {
saveAndReload(); saveAndReload();
showMainMenu(scroll, group); showMainMenu(scroll, group);
} }
}; });
if (!keyboard) delete menu[/*LANG*/"Message"]; if (!keyboard) delete menu[/*LANG*/"Message"];
if (!keyboard || !settings.showGroup) delete menu[/*LANG*/"Group"]; if (!keyboard || !settings.showGroup) delete menu[/*LANG*/"Group"];

View File

@ -2,7 +2,7 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.47", "version": "0.48",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm", "tags": "tool,alarm",

BIN
apps/ashadyclock/0.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/1.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/2.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/3.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/4.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/5.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/6.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/7.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/8.bin Normal file

Binary file not shown.

BIN
apps/ashadyclock/9.bin Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDADAAAAHA4HHAAAHA4HA4HA4HAAAAA4//////AAH//////6///AAAA///6//9AAV/////////XAAAA//////6AAv//X//////4AAAA//////4AA////X/////4AAAA//////4AA/////6////4AAAA//////4AA///6vX///v4AAAAAA////4AA///4A//6//AAAAAAF///64AAHA4CFX////AAAAAAH//X/oAAAAAAH6///4AAAAAAH////AAAAAAAH////QAAAAAAH////AAAAAAA/////AAAAAAAH////AAAAAAC////4AAAAAAAH////AAAAAAH////4AAAAAAA6v///AAAAAA//X/9QAAAAAAA////VAAAAAA////4AAAAAAAA////4AAAAAH////4AAAAAAAA////4AAAAA/6///AAAAAAAAA////4AAAAA////6AAAAAAAAF////4AAAAH/X/94AAAAAAAAH/X//oAAAAX////AAAAAAAAAH////AAAAA/////AAAAAAAAAH////AEQAB+///4AwEAAAAAAH////AigAH////4EUGEAAAAAX////A0GA///3/AmEUigAAAAjGMYxEw0AxxGOIGg0w0igAAGikUwmGmmgwEUwmmGmk0wwAAmmmmmmmGmmgimmmmmmmGmgAGmmmGgGmmmmAmmmmiE0w00wAE0000wE000wwmmmmiA0000wAE00U0AGmmmmA0000wE0U00wAAAAAAAmmmmmAAAAAAGmmmmAAAAAAAE0000wAAAAAE00U00AAAAAACk0000QAAAAAmmmmmgAAAAAAGmmmmigAAAAA00000AAAAAAA00000UAAAAAmmmmmEAAAAAA000w00AAAAAGmmmmmAAAAAAE00000QAAAAE0U000wAAAAAA00000wAAAAA000mmiAAAAAAmmmmmiAAAAAmimmmkQAAAAAGmmmmmAAAAAE00000QAAAAAA00000wmEwmAmmmmmmEwmEwAE0000w00000wmmmmmmimmmgAGmmmmmmmmmGEw00mmmmmmmgAGmmmmmmmmmGE0w00000000AA0000U000mmmA000000000wwAGmmmmmmmmmEU0GmmmmmmmmAAAAiAEACgAECAAgCEAAiAEAAA"))

107
apps/ashadyclock/app.js Normal file
View File

@ -0,0 +1,107 @@
var settings = Object.assign({
// default values
showWidgets: false,
alternativeColor: false,
}, require('Storage').readJSON("ashadyclock.json", true) || {});
let drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
let palBottom;
if (settings.alternativeColor) {
palBottom = new Uint16Array(E.toArrayBuffer(E.toFlatString(new Uint16Array([
g.toColor("#000"),
g.toColor("#000"),
g.toColor("#0FF"),
g.toColor("#0FF"),
g.toColor("#00F"),
g.toColor("#000"),
g.toColor("#00F"),
g.toColor("#000")
]).buffer)));
} else {
palBottom = new Uint16Array(E.toArrayBuffer(E.toFlatString(new Uint16Array([
g.toColor("#000"),
g.toColor("#000"),
g.toColor("#F00"),
g.toColor("#FF0"),
g.toColor("#00F"),
g.toColor("#000"),
g.toColor("#FF0"),
g.toColor("#000")
]).buffer)));
}
let palTop = new Uint16Array(E.toArrayBuffer(E.toFlatString(new Uint16Array([
g.toColor("#FFF"),
g.toColor("#000"),
g.toColor("#FFF"),
g.toColor("#FFF"),
g.toColor("#00F"),
g.toColor("#000"),
g.toColor("#FFF"),
g.toColor("#000"),
]).buffer)));
let xOffset = (g.getWidth() - 176) / 2;
let yOffset = (g.getHeight() - 176) / 2;
function drawTop(d0, d1) {
if (settings.showWidgets && g.getHeight()<=176) {
drawNumber(d1, 82 + xOffset, 24 + yOffset, palTop, {scale: 0.825});
drawNumber(d0, 13 + xOffset, 24 + yOffset, palTop, {scale: 0.825});
} else {
drawNumber(d1, 80, 0, palTop);
drawNumber(d0, -1, 0, palTop);
}
}
function drawBottom(d0, d1) {
if (settings.showWidgets && g.getHeight()<=176) {
drawNumber(d1, 82 + xOffset, 92 + yOffset, palBottom, {scale: 0.825});
drawNumber(d0, 13 + xOffset, 92 + yOffset, palBottom, {scale: 0.825});
} else {
drawNumber(d1, 80, 75, palBottom);
drawNumber(d0, -1, 75, palBottom);
}
}
function drawNumber(number, x, y, palette, options) {
let image =
{
width : 98, height : 100, bpp : 3,
transparent: 4,
buffer : require("Storage").read("ashadyclock." + number +".bin")
};
image.palette = palette;
g.drawImage(image, x, y, options);
}
function draw() {
let d = new Date();
g.clearRect(0, settings.showWidgets ? 24 : 0, g.getWidth(),g.getHeight());
drawBottom(Math.floor(d.getMinutes()/10), d.getMinutes() % 10);
drawTop(Math.floor(d.getHours()/10), d.getHours() % 10);
queueDraw();
}
g.clear();
// draw immediately at first
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");
if(settings.showWidgets) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}

BIN
apps/ashadyclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,27 @@
{ "id": "ashadyclock",
"name": "A Shady Clock",
"shortName":"Shady Clk",
"icon": "app.png",
"version":"0.01",
"description": "A nice clock with drop shadow. Hours and minutes. Configure color and widgets in settings. Create any color combination with the existing images by changing only the app color values.",
"type": "clock",
"tags": "clock",
"screenshots": [{"url":"screenshot-1.png"},{"url":"screenshot.png"}],
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"ashadyclock.app.js","url":"app.js"},
{"name":"ashadyclock.img","url":"app-icon.js","evaluate":true},
{"name":"ashadyclock.0.bin","url":"0.bin"},
{"name":"ashadyclock.1.bin","url":"1.bin"},
{"name":"ashadyclock.2.bin","url":"2.bin"},
{"name":"ashadyclock.3.bin","url":"3.bin"},
{"name":"ashadyclock.4.bin","url":"4.bin"},
{"name":"ashadyclock.5.bin","url":"5.bin"},
{"name":"ashadyclock.6.bin","url":"6.bin"},
{"name":"ashadyclock.7.bin","url":"7.bin"},
{"name":"ashadyclock.8.bin","url":"8.bin"},
{"name":"ashadyclock.9.bin","url":"9.bin"},
{"name":"ashadyclock.settings.js","url":"settings.js"}
],
"data": [{"name":"ashadyclock.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

View File

@ -0,0 +1,32 @@
(function(back) {
var FILE = "ashadyclock.json";
// Load settings
var settings = Object.assign({
showWidgets: false,
alternativeColor: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Show the menu
E.showMenu({
"" : { "title" : "Shady Clck" },
"< Back" : () => back(),
'Show Widgets': {
value: !!settings.showWidgets, // !! converts undefined to false
onchange: v => {
settings.showWidgets = v;
writeSettings();
}
},
'Blue Color': {
value: !!settings.alternativeColor, // !! converts undefined to false
onchange: v => {
settings.alternativeColor = v;
writeSettings();
}
},
});
})

1
apps/beeptest/ChangeLog Normal file
View File

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

27
apps/beeptest/README.md Normal file
View File

@ -0,0 +1,27 @@
# App Name
Beep Test
## Usage
Mark out a 20m space
Click the side button to start the test
Shuttle run between your markers when the watch buzzes
Push the button when you need to stop
## Features
Buzzing on each shuttle run
Results page with vO2max and total distance covered.
## Controls
Side button starts, stops and resets the app.
## Requests
bb0x88 on giuthub
## Creator
Blade

View File

@ -0,0 +1,2 @@
require("heatshrink").decompress(atob("mEw4UA///+f8lky6f8HFmqBRMK1WgBAtUBYUABYtVqtAgEoAIQACioLBqALHBQIABBZMFEgIjHgEBqtUHY4aDKZA+CoBrIBYJJBBZJuCAA3VBYkC1QABGoJhDBYxTBBYUFEQoLDoEVSgIADO4ILCUASdGqtRGIYLFKoY7CIwdUEwJtBBYY6CqADBFwoLDDYIuFIwQUBigLITJQLFHYKNEHAgLGXw6NDBZbKHTIYLLKg6lDBY4KDEY5EIIwahFHQoKIBYIrHIwYLLuALJHRTcHAAjcGAEwA=="))

274
apps/beeptest/beeptest.js Normal file
View File

@ -0,0 +1,274 @@
var Layout = require("Layout");
// Beep Test Data
const BEET_TEST_DATA = [
{ shuttles: 7, timePerShuttle: 9.0, totalTime: 63.0, distancePerLevel: 140 },
{ shuttles: 8, timePerShuttle: 8.0, totalTime: 64.0, distancePerLevel: 160 },
{ shuttles: 8, timePerShuttle: 7.58, totalTime: 60.6, distancePerLevel: 160 },
{ shuttles: 9, timePerShuttle: 7.2, totalTime: 64.8, distancePerLevel: 180 },
{ shuttles: 9, timePerShuttle: 6.86, totalTime: 61.7, distancePerLevel: 180 },
{
shuttles: 10,
timePerShuttle: 6.55,
totalTime: 65.5,
distancePerLevel: 200,
},
{
shuttles: 10,
timePerShuttle: 6.26,
totalTime: 62.6,
distancePerLevel: 200,
},
{ shuttles: 11, timePerShuttle: 6.0, totalTime: 66.0, distancePerLevel: 220 },
{
shuttles: 11,
timePerShuttle: 5.76,
totalTime: 63.4,
distancePerLevel: 220,
},
{
shuttles: 11,
timePerShuttle: 5.54,
totalTime: 60.9,
distancePerLevel: 220,
},
{
shuttles: 12,
timePerShuttle: 5.33,
totalTime: 64.0,
distancePerLevel: 240,
},
{
shuttles: 12,
timePerShuttle: 5.14,
totalTime: 61.7,
distancePerLevel: 240,
},
{
shuttles: 13,
timePerShuttle: 4.97,
totalTime: 64.6,
distancePerLevel: 260,
},
{ shuttles: 13, timePerShuttle: 4.8, totalTime: 62.4, distancePerLevel: 260 },
{
shuttles: 13,
timePerShuttle: 4.65,
totalTime: 60.4,
distancePerLevel: 260,
},
{ shuttles: 14, timePerShuttle: 4.5, totalTime: 63.0, distancePerLevel: 280 },
{
shuttles: 14,
timePerShuttle: 4.36,
totalTime: 61.1,
distancePerLevel: 280,
},
{
shuttles: 15,
timePerShuttle: 4.24,
totalTime: 63.5,
distancePerLevel: 300,
},
{
shuttles: 15,
timePerShuttle: 4.11,
totalTime: 61.7,
distancePerLevel: 300,
},
{ shuttles: 16, timePerShuttle: 4.0, totalTime: 64.0, distancePerLevel: 320 },
{
shuttles: 16,
timePerShuttle: 3.89,
totalTime: 62.3,
distancePerLevel: 320,
},
];
// VO2max Data
const VO2MAX_DATA = [
{ level: 1, vo2max: 16.7 },
{ level: 2, vo2max: 23.0 },
{ level: 3, vo2max: 26.2 },
{ level: 4, vo2max: 29.3 },
{ level: 5, vo2max: 32.5 },
{ level: 6, vo2max: 35.7 },
{ level: 7, vo2max: 38.8 },
{ level: 8, vo2max: 42.0 },
{ level: 9, vo2max: 45.1 },
{ level: 10, vo2max: 48.3 },
{ level: 11, vo2max: 51.5 },
{ level: 12, vo2max: 54.6 },
{ level: 13, vo2max: 57.8 },
{ level: 14, vo2max: 60.9 },
{ level: 15, vo2max: 64.1 },
{ level: 16, vo2max: 67.3 },
{ level: 17, vo2max: 70.4 },
{ level: 18, vo2max: 73.6 },
{ level: 19, vo2max: 76.7 },
{ level: 20, vo2max: 79.9 },
{ level: 21, vo2max: 83.0 },
];
let currentLevel = 0;
let currentShuttle = 0;
let timeRemaining = 0;
let intervalId;
let beepTestLayout;
let testState = "start"; // 'start' | 'running' | 'result'
function initBeepTestLayout() {
beepTestLayout = new Layout(
{
type: "v",
c: [
{ type: "txt", font: "30%", pad: 0, label: "Start Test", id: "status" },
{ type: "txt", font: "15%", pad: 0, label: "", id: "level" },
{ type: "txt", font: "10%", pad: 0, label: "", id: "vo2max" }, // Smaller font for VO2max
{ type: "txt", font: "10%", pad: 0, label: "", id: "distance" }, // Smaller font for Distance
],
},
{
btns: [
{
label: "Start/Stop",
cb: (l) => {
if (testState === "start") {
startTest();
} else if (testState === "running") {
stopTest();
} else {
showStartScreen();
}
},
},
],
},
);
}
function showStartScreen() {
testState = "start";
g.clear();
beepTestLayout.clear(beepTestLayout.status);
beepTestLayout.status.label = "Start\nTest";
beepTestLayout.clear(beepTestLayout.level);
beepTestLayout.level.label = "";
beepTestLayout.clear(beepTestLayout.vo2max); // Clear VO2max text
beepTestLayout.vo2max.label = "";
beepTestLayout.clear(beepTestLayout.distance); // Clear Distance text
beepTestLayout.distance.label = "";
beepTestLayout.render();
}
function startTest() {
testState = "running";
currentLevel = 0;
currentShuttle = 0;
Bangle.buzz(2000); // Buzz for 2 seconds at the start of the test
runLevel();
}
function runLevel() {
if (currentLevel >= BEET_TEST_DATA.length) {
stopTest();
return;
}
const levelData = BEET_TEST_DATA[currentLevel];
timeRemaining = levelData.timePerShuttle * 1000; // Convert to milliseconds
updateDisplay();
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
if (timeRemaining <= 0) {
currentShuttle++;
Bangle.buzz(100); // Short buzz after each shuttle
if (currentShuttle >= levelData.shuttles) {
// Buzz longer or twice at the end of each level
Bangle.buzz(1000); // Buzz for 1 second at level end
setTimeout(() => Bangle.buzz(1000), 500); // Buzz again after 0.5 seconds
currentLevel++;
currentShuttle = 0;
runLevel();
return;
}
timeRemaining = levelData.timePerShuttle * 1000; // Reset to original time for the next shuttle
}
updateDisplay();
timeRemaining -= 100; // Decrement time by 100 milliseconds
}, 100); // Update every 100 milliseconds
}
function updateDisplay() {
g.clear(); // Clear the entire screen
beepTestLayout.status.label = formatTime(timeRemaining);
beepTestLayout.level.label = `Level: ${currentLevel + 1}.${currentShuttle + 1}`;
beepTestLayout.render();
}
function stopTest() {
g.clear(); // Clear the entire screen
testState = "result";
clearInterval(intervalId);
// Determine previous level and shuttle
let prevLevel = currentLevel;
let prevShuttle = currentShuttle;
if (prevShuttle === 0) {
if (prevLevel > 0) {
prevLevel--;
prevShuttle = BEET_TEST_DATA[prevLevel].shuttles - 1;
} else {
prevShuttle = 0;
}
} else {
prevShuttle--;
}
// Determine VO2max and total distance
const vo2max = getVO2max(prevLevel + 1);
const totalDistance = calculateTotalDistance(prevLevel + 1);
beepTestLayout.clear(beepTestLayout.status);
beepTestLayout.status.label = "Result";
beepTestLayout.clear(beepTestLayout.level);
beepTestLayout.level.label = `Level: ${prevLevel + 1}.${prevShuttle + 1}`;
beepTestLayout.clear(beepTestLayout.vo2max);
beepTestLayout.vo2max.label = `VO2max: ${vo2max}`;
beepTestLayout.clear(beepTestLayout.distance);
beepTestLayout.distance.label = `Distance: ${totalDistance} m`;
beepTestLayout.render();
}
function getVO2max(level) {
const result = VO2MAX_DATA.find((item) => item.level === level);
return result ? result.vo2max : "N/A";
}
function calculateTotalDistance(level) {
// Calculate the total number of shuttles completed
let totalShuttles = 0;
for (let i = 0; i < level - 1; i++) {
totalShuttles += BEET_TEST_DATA[i].shuttles;
}
const levelData = BEET_TEST_DATA[level - 1];
totalShuttles += levelData.shuttles; // Add the shuttles completed in the current level
const distancePerShuttle = 20; // Distance per shuttle in meters
return totalShuttles * distancePerShuttle; // Total distance
}
function formatTime(milliseconds) {
let seconds = Math.floor(milliseconds / 1000);
let tenths = Math.floor((milliseconds % 1000) / 100); // Get tenths of a second
return (seconds < 10 ? "" : "") + seconds + "." + tenths; // Display only the tenths digit
}
// Initialize the app
Bangle.setLCDPower(1); // Keep the watch LCD lit up
initBeepTestLayout();
showStartScreen();

BIN
apps/beeptest/beeptest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,15 @@
{
"id": "beeptest",
"name": "Beep Test",
"shortName": "Beep Test",
"version": "0.01",
"description": "Aerobic fitness test created by Léger & Lambert",
"icon": "beeptest.png",
"tags": "Health",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{ "name": "beeptest.app.js", "url": "beeptest.js" },
{ "name": "beeptest.img", "url": "app-icon.js", "evaluate": true }
]
}

View File

@ -70,3 +70,5 @@
0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds 0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds
0.60: Minor code improvements 0.60: Minor code improvements
0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined') 0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined')
0.62: Handle setting for configuring BLE privacy
0.63: Only set BLE `display:1` if we have a passkey

View File

@ -78,7 +78,12 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
// Apply any settings-specific stuff // Apply any settings-specific stuff
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; if (s.bleprivacy || (s.passkey!==undefined && s.passkey.length==6)) {
let passkey = s.passkey ? `passkey:${E.toJS(s.passkey.toString())},display:1,mitm:1,` : "";
let privacy = s.bleprivacy ? `privacy:${E.toJS(s.bleprivacy)},` : "";
boot+=`NRF.setSecurity({${passkey}${privacy}});\n`;
}
if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`;
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`; if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES // ================================================== FIXING OLDER FIRMWARES

View File

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

View File

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

View File

@ -0,0 +1,11 @@
# BLE BTHome Battery Service
Broadcasts battery remaining percentage over BLE using the [BTHome protocol](https://bthome.io/) - which makes for easy integration into [Home Assistant](https://www.home-assistant.io/)
## Usage
This boot code runs in the background and has no user interface.
## Creator
[Deirdre O'Byrne](https://github.com/deirdreobyrne)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,14 @@
var btHomeBatterySequence = 0;
function advertiseBTHomeBattery() {
var advert = [0x40, 0x00, btHomeBatterySequence, 0x01, E.getBattery()];
require("ble_advert").set(0xFCD2, advert);
btHomeBatterySequence = (btHomeBatterySequence + 1) & 255;
}
setInterval(function() {
advertiseBTHomeBattery();
}, 300000); // update every 5 min
advertiseBTHomeBattery();

View File

@ -0,0 +1,15 @@
{
"id": "bootbthomebatt",
"name": "BLE BTHome Battery Service",
"shortName": "BTHome Battery Service",
"version": "0.01",
"description": "Broadcasts battery remaining over bluetooth using the BTHome protocol - makes for easy integration with Home Assistant.\n",
"icon": "bluetooth.png",
"type": "bootloader",
"tags": "battery,ble,bluetooth,bthome",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthomebat.boot.js","url":"boot.js"}
]
}

View File

@ -3,3 +3,4 @@
0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false 0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false
0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher 0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher
0.05: Fixes step count not resetting after a new day starts 0.05: Fixes step count not resetting after a new day starts
0.06 Added clockbackground app functionality

View File

@ -5,11 +5,11 @@
* --------------------------------------------------------------- * ---------------------------------------------------------------
*/ */
let background = require("clockbg");
let storage = require("Storage"); let storage = require("Storage");
let locale = require("locale"); let locale = require("locale");
let widgets = require("widget_utils"); let widgets = require("widget_utils");
let date = new Date(); let date = new Date();
let bgImage;
let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0; let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0;
let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json'; let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json';
// Add a condition to check if the file exists, if it does not, default to 'boxclk.json' // Add a condition to check if the file exists, if it does not, default to 'boxclk.json'
@ -71,14 +71,6 @@
* --------------------------------------------------------------- * ---------------------------------------------------------------
*/ */
for (let key in boxesConfig) {
if (key === 'bg' && boxesConfig[key].img) {
bgImage = storage.read(boxesConfig[key].img);
} else if (key !== 'selectedConfig') {
boxes[key] = Object.assign({}, boxesConfig[key]);
}
}
let boxKeys = Object.keys(boxes); let boxKeys = Object.keys(boxes);
boxKeys.forEach((key) => { boxKeys.forEach((key) => {
@ -224,9 +216,7 @@
return function(boxes) { return function(boxes) {
date = new Date(); date = new Date();
g.clear(); g.clear();
if (bgImage) { background.fillRect(Bangle.appRect);
g.drawImage(bgImage, 0, 0);
}
if (boxes.time) { if (boxes.time) {
boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0)); boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0));
updatePerMinute = isBool(boxes.time.short, true); updatePerMinute = isBool(boxes.time.short, true);

Binary file not shown.

View File

@ -4,6 +4,7 @@
"version": "0.05", "version": "0.05",
"description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background",
"icon": "app.png", "icon": "app.png",
"dependencies" : { "clockbg":"module" },
"screenshots": [ "screenshots": [
{"url":"screenshot.png"}, {"url":"screenshot.png"},
{"url":"screenshot-1.png"}, {"url":"screenshot-1.png"},

View File

@ -1,3 +1,4 @@
0.01: New app! 0.01: New app!
0.02: Advertise accelerometer data and sensor location 0.02: Advertise accelerometer data and sensor location
0.03: Use the bleAdvert module 0.03: Use the bleAdvert module
0.04: Actually use the ble_advert module

View File

@ -1,4 +1,3 @@
var _a;
{ {
var __assign = Object.assign; var __assign = Object.assign;
var Layout_1 = require("Layout"); var Layout_1 = require("Layout");
@ -441,8 +440,6 @@ var _a;
NRF.setServices(ad, { NRF.setServices(ad, {
uart: false, uart: false,
}); });
var bangle2 = Bangle;
var cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
for (var id in ad) { for (var id in ad) {
var serv = ad[id]; var serv = ad[id];
var value = void 0; var value = void 0;
@ -450,11 +447,7 @@ var _a;
value = serv[ch].value; value = serv[ch].value;
break; break;
} }
cycle.push((_a = {}, _a[id] = value || [], _a)); require("ble_advert").set(id, value || []);
} }
bangle2.bleAdvert = cycle;
NRF.setAdvertising(cycle, {
interval: 100,
});
} }
} }

View File

@ -1,6 +1,6 @@
{ {
// @ts-expect-error helper // @ts-expect-error helper
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __assign = Object.assign; const __assign = Object.assign;
const Layout = require("Layout"); const Layout = require("Layout");
@ -767,12 +767,6 @@ enableSensors();
}, },
); );
type BleAdvert = { [key: string]: number[] };
const bangle2 = Bangle as {
bleAdvert?: BleAdvert | BleAdvert[];
};
const cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
for(const id in ad){ for(const id in ad){
const serv = ad[id as BleServ]; const serv = ad[id as BleServ];
let value; let value;
@ -783,16 +777,7 @@ enableSensors();
break; break;
} }
cycle.push({ [id]: value || [] }); require("ble_advert").set(id, value || []);
} }
bangle2.bleAdvert = cycle;
NRF.setAdvertising(
cycle,
{
interval: 100,
}
);
} }
} }

View File

@ -2,7 +2,7 @@
"id": "btadv", "id": "btadv",
"name": "btadv", "name": "btadv",
"shortName": "btadv", "shortName": "btadv",
"version": "0.03", "version": "0.04",
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth", "description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
"icon": "icon.png", "icon": "icon.png",
"tags": "health,tool,sensors,bluetooth", "tags": "health,tool,sensors,bluetooth",

View File

@ -4,3 +4,4 @@
Set 'n' for buttons in Bangle.btHomeData correctly (avoids adding extra buttons on end of advertising) Set 'n' for buttons in Bangle.btHomeData correctly (avoids adding extra buttons on end of advertising)
0.04: Fix duplicate button on edit->save 0.04: Fix duplicate button on edit->save
0.05: Use the bleAdvert module 0.05: Use the bleAdvert module
0.06: button number can't be 0. Now generates number automatically

View File

@ -1,7 +1,7 @@
{ "id": "bthome", { "id": "bthome",
"name": "BTHome", "name": "BTHome",
"shortName":"BTHome", "shortName":"BTHome",
"version":"0.05", "version":"0.06",
"description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth", "description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth",
"icon": "icon.png", "icon": "icon.png",
"type": "app", "type": "app",

View File

@ -11,10 +11,14 @@
require("Storage").writeJSON("bthome.json",settings) require("Storage").writeJSON("bthome.json",settings)
} }
// Get id number for button that is sent to bthome
function getNewIdNumber(){
return [1, 2, 3, 4, 5, 6, 7, 8, 9].find(id => settings.buttons.every(button => id != button.n));
}
function showButtonMenu(button, isNew) { function showButtonMenu(button, isNew) {
var isNew = false;
if (!button) { if (!button) {
button = {name:"home", icon:"home", n:0, v:"press"}; button = {name:"home", icon:"home", n:getNewIdNumber(), v:"press"};
isNew = true; isNew = true;
} }
var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"]; var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"];
@ -51,10 +55,6 @@
format : v => actions[v], format : v => actions[v],
onchange : v => button.v=actions[v] onchange : v => button.v=actions[v]
}, },
/*LANG*/"Button #" : {
value : button.n, min:0, max:3,
onchange : v => button.n=v
},
/*LANG*/"Save" : () => { /*LANG*/"Save" : () => {
if (isNew) settings.buttons.push(button); if (isNew) settings.buttons.push(button);
saveSettings(); saveSettings();

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Handle the case where other apps have set bleAdvert to an array 0.02: Handle the case where other apps have set bleAdvert to an array
0.03: Use the ble_advert module

View File

@ -38,21 +38,7 @@ function onTemperature(p) {
pressure100&255,(pressure100>>8)&255,pressure100>>16 pressure100&255,(pressure100>>8)&255,pressure100>>16
]; ];
if(Array.isArray(Bangle.bleAdvert)){ require("ble_advert").set(0xFCD2, advert);
var found = false;
for(var ad in Bangle.bleAdvert){
if(ad[0xFCD2]){
ad[0xFCD2] = advert;
found = true;
break;
}
}
if(!found)
Bangle.bleAdvert.push({ 0xFCD2: advert });
}else{
Bangle.bleAdvert[0xFCD2] = advert;
}
NRF.setAdvertising(Bangle.bleAdvert);
} }
// Gets the temperature in the most accurate way with pressure sensor // Gets the temperature in the most accurate way with pressure sensor
@ -60,7 +46,6 @@ function drawTemperature() {
Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); Bangle.getPressure().then(p =>{if (p) onTemperature(p);});
} }
if (!Bangle.bleAdvert) Bangle.bleAdvert = {};
setInterval(function() { setInterval(function() {
drawTemperature(); drawTemperature();
}, 10000); // update every 10s }, 10000); // update every 10s

View File

@ -1,7 +1,7 @@
{ "id": "bthometemp", { "id": "bthometemp",
"name": "BTHome Temperature and Pressure", "name": "BTHome Temperature and Pressure",
"shortName":"BTHome T", "shortName":"BTHome T",
"version":"0.02", "version":"0.03",
"description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard", "description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard",
"icon": "app.png", "icon": "app.png",
"tags": "bthome,bluetooth,temperature", "tags": "bthome,bluetooth,temperature",

View File

@ -2,3 +2,4 @@
0.02: Hiding widgets while showing the code 0.02: Hiding widgets while showing the code
0.03: Added option to use max brightness when showing code 0.03: Added option to use max brightness when showing code
0.04: Minor code improvements 0.04: Minor code improvements
0.05: Add EAN & UPC codes

73
apps/cards/EAN.js Normal file
View File

@ -0,0 +1,73 @@
/*
* JS source adapted from https://github.com/lindell/JsBarcode
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
const encode = require("cards.encode.js");
const Barcode = require("cards.Barcode.js");
// Standard start end and middle bits
const SIDE_BIN = '101';
const MIDDLE_BIN = '01010';
// Base class for EAN8 & EAN13
class EAN extends Barcode {
constructor(data, options) {
super(data, options);
}
leftText(from, to) {
return this.text.substr(from, to);
}
leftEncode(data, structure) {
return encode(data, structure);
}
rightText(from, to) {
return this.text.substr(from, to);
}
rightEncode(data, structure) {
return encode(data, structure);
}
encode() {
const data = [
SIDE_BIN,
this.leftEncode(),
MIDDLE_BIN,
this.rightEncode(),
SIDE_BIN
];
return {
data: data.join(''),
text: this.text
};
}
}
module.exports = EAN;

92
apps/cards/EAN13.js Normal file
View File

@ -0,0 +1,92 @@
// Encoding documentation:
// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Binary_encoding_of_data_digits_into_EAN-13_barcode
/*
* JS source adapted from https://github.com/lindell/JsBarcode
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
const EAN = require("cards.EAN.js");
const EAN13_STRUCTURE = [
'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL'
];
// Calculate the checksum digit
// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Calculation_of_checksum_digit
const checksum = (number) => {
const res = number
.substr(0, 12)
.split('')
.map((n) => +n)
.reduce((sum, a, idx) => (
idx % 2 ? sum + a * 3 : sum + a
), 0);
return (10 - (res % 10)) % 10;
};
class EAN13 extends EAN {
constructor(data, options) {
// Add checksum if it does not exist
if (/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)) {
data += checksum(data);
}
super(data, options);
// Adds a last character to the end of the barcode
this.lastChar = options.lastChar;
}
valid() {
return (
/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) &&
+this.data[12] === checksum(this.data)
);
}
leftText() {
return super.leftText(1, 6);
}
leftEncode() {
const data = this.data.substr(1, 6);
const structure = EAN13_STRUCTURE[this.data[0]];
return super.leftEncode(data, structure);
}
rightText() {
return super.rightText(7, 6);
}
rightEncode() {
const data = this.data.substr(7, 6);
return super.rightEncode(data, 'RRRRRR');
}
}
module.exports = EAN13;

82
apps/cards/EAN8.js Normal file
View File

@ -0,0 +1,82 @@
// Encoding documentation:
// http://www.barcodeisland.com/ean8.phtml
/*
* JS source adapted from https://github.com/lindell/JsBarcode
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
const EAN = require("cards.EAN.js");
// Calculate the checksum digit
const checksum = (number) => {
const res = number
.substr(0, 7)
.split('')
.map((n) => +n)
.reduce((sum, a, idx) => (
idx % 2 ? sum + a : sum + a * 3
), 0);
return (10 - (res % 10)) % 10;
};
class EAN8 extends EAN {
constructor(data, options) {
// Add checksum if it does not exist
if (/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)) {
data += checksum(data);
}
super(data, options);
}
valid() {
return (
/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) &&
+this.data[7] === checksum(this.data)
);
}
leftText() {
return super.leftText(0, 4);
}
leftEncode() {
const data = this.data.substr(0, 4);
return super.leftEncode(data, 'LLLL');
}
rightText() {
return super.rightText(4, 4);
}
rightEncode() {
const data = this.data.substr(4, 4);
return super.rightEncode(data, 'RRRR');
}
}
module.exports = EAN8;

79
apps/cards/UPC.js Normal file
View File

@ -0,0 +1,79 @@
// Encoding documentation:
// https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding
/*
* JS source adapted from https://github.com/lindell/JsBarcode
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
const encode = require("cards.encode.js");
const Barcode = require("cards.Barcode.js");
class UPC extends Barcode{
constructor(data, options){
// Add checksum if it does not exist
if(/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){
data += checksum(data);
}
super(data, options);
}
valid(){
return /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) &&
this.data[11] == checksum(this.data);
}
encode(){
var result = "";
result += "101";
result += encode(this.data.substr(0, 6), "LLLLLL");
result += "01010";
result += encode(this.data.substr(6, 6), "RRRRRR");
result += "101";
return {
data: result,
text: this.text
};
}
}
// Calulate the checksum digit
// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Calculation_of_checksum_digit
function checksum(number){
var result = 0;
var i;
for(i = 1; i < 11; i += 2){
result += parseInt(number[i]);
}
for(i = 0; i < 11; i += 2){
result += parseInt(number[i]) * 3;
}
return (10 - (result % 10)) % 10;
}
module.exports = { UPC, checksum };

134
apps/cards/UPCE.js Normal file
View File

@ -0,0 +1,134 @@
// Encoding documentation:
// https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding
//
// UPC-E documentation:
// https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
/*
* JS source adapted from https://github.com/lindell/JsBarcode
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
const encode = require("cards.encode.js");
const Barcode = require("cards.Barcode.js");
const upc = require("cards.UPC.js");
const EXPANSIONS = [
"XX00000XXX",
"XX10000XXX",
"XX20000XXX",
"XXX00000XX",
"XXXX00000X",
"XXXXX00005",
"XXXXX00006",
"XXXXX00007",
"XXXXX00008",
"XXXXX00009"
];
const PARITIES = [
["EEEOOO", "OOOEEE"],
["EEOEOO", "OOEOEE"],
["EEOOEO", "OOEEOE"],
["EEOOOE", "OOEEEO"],
["EOEEOO", "OEOOEE"],
["EOOEEO", "OEEOOE"],
["EOOOEE", "OEEEOO"],
["EOEOEO", "OEOEOE"],
["EOEOOE", "OEOEEO"],
["EOOEOE", "OEEOEO"]
];
class UPCE extends Barcode{
constructor(data, options){
// Code may be 6 or 8 digits;
// A 7 digit code is ambiguous as to whether the extra digit
// is a UPC-A check or number system digit.
super(data, options);
this.isValid = false;
if(/^[0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){
this.middleDigits = data;
this.upcA = expandToUPCA(data, "0");
this.text = options.text ||
`${this.upcA[0]}${data}${this.upcA[this.upcA.length - 1]}`;
this.isValid = true;
}
else if(/^[01][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){
this.middleDigits = data.substring(1, data.length - 1);
this.upcA = expandToUPCA(this.middleDigits, data[0]);
if(this.upcA[this.upcA.length - 1] === data[data.length - 1]){
this.isValid = true;
}
else{
// checksum mismatch
return;
}
}
}
valid(){
return this.isValid;
}
encode(){
var result = "";
result += "101";
result += this.encodeMiddleDigits();
result += "010101";
return {
data: result,
text: this.text
};
}
encodeMiddleDigits() {
const numberSystem = this.upcA[0];
const checkDigit = this.upcA[this.upcA.length - 1];
const parity = PARITIES[parseInt(checkDigit)][parseInt(numberSystem)];
return encode(this.middleDigits, parity);
}
}
function expandToUPCA(middleDigits, numberSystem) {
const lastUpcE = parseInt(middleDigits[middleDigits.length - 1]);
const expansion = EXPANSIONS[lastUpcE];
let result = "";
let digitIndex = 0;
for(let i = 0; i < expansion.length; i++) {
let c = expansion[i];
if (c === 'X') {
result += middleDigits[digitIndex++];
} else {
result += c;
}
}
result = `${numberSystem}${result}`;
return `${result}${upc.checksum(result)}`;
}
module.exports = UPCE;

View File

@ -82,16 +82,17 @@ function printSquareCode(binary, size) {
} }
} }
function printLinearCode(binary) { function printLinearCode(binary) {
var padding = 5;
var yFrom = 15; var yFrom = 15;
var yTo = 28; var yTo = 28;
var width = g.getWidth()/binary.length; var width = (g.getWidth()-(2*padding))/binary.length;
for(var b = 0; b < binary.length; b++){ for(var b = 0; b < binary.length; b++){
var x = b * width; var x = b * width;
if(binary[b] === "1"){ if(binary[b] === "1"){
g.setColor(BLACK).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); g.setColor(BLACK).fillRect({x:x+padding, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)});
} }
else if(binary[b]){ else if(binary[b]){
g.setColor(WHITE).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); g.setColor(WHITE).fillRect({x:x+padding, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)});
} }
} }
} }
@ -132,6 +133,42 @@ function showCode(card) {
printLinearCode(code.encode().data); printLinearCode(code.encode().data);
break; break;
} }
case "EAN_8": {
g.setFont("Vector:20");
g.setFontAlign(0,1).setColor(BLACK);
g.drawString(card.value, g.getWidth()/2, g.getHeight());
const EAN8 = require("cards.EAN8.js");
let code = new EAN8(card.value, {});
printLinearCode(code.encode().data);
break;
}
case "EAN_13": {
g.setFont("Vector:20");
g.setFontAlign(0,1).setColor(BLACK);
g.drawString(card.value, g.getWidth()/2, g.getHeight());
const EAN13 = require("cards.EAN13.js");
let code = new EAN13(card.value, {});
printLinearCode(code.encode().data);
break;
}
case "UPC_A": {
g.setFont("Vector:20");
g.setFontAlign(0,1).setColor(BLACK);
g.drawString(card.value, g.getWidth()/2, g.getHeight());
const UPC = require("cards.UPC.js");
let code = new UPC.UPC(card.value, {});
printLinearCode(code.encode().data);
break;
}
case "UPC_E": {
g.setFont("Vector:20");
g.setFontAlign(0,1).setColor(BLACK);
g.drawString(card.value, g.getWidth()/2, g.getHeight());
const UPCE = require("cards.UPCE.js");
let code = new UPCE(card.value, {});
printLinearCode(code.encode().data);
break;
}
default: default:
g.clear(true); g.clear(true);
g.setFont("Vector:30"); g.setFont("Vector:30");

67
apps/cards/encode.js Normal file
View File

@ -0,0 +1,67 @@
/*
* JS source adapted from https://github.com/lindell/JsBarcode
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
const BINARIES = {
'L': [ // The L (left) type of encoding
'0001101', '0011001', '0010011', '0111101', '0100011',
'0110001', '0101111', '0111011', '0110111', '0001011'
],
'G': [ // The G type of encoding
'0100111', '0110011', '0011011', '0100001', '0011101',
'0111001', '0000101', '0010001', '0001001', '0010111'
],
'R': [ // The R (right) type of encoding
'1110010', '1100110', '1101100', '1000010', '1011100',
'1001110', '1010000', '1000100', '1001000', '1110100'
],
'O': [ // The O (odd) encoding for UPC-E
'0001101', '0011001', '0010011', '0111101', '0100011',
'0110001', '0101111', '0111011', '0110111', '0001011'
],
'E': [ // The E (even) encoding for UPC-E
'0100111', '0110011', '0011011', '0100001', '0011101',
'0111001', '0000101', '0010001', '0001001', '0010111'
]
};
// Encode data string
const encode = (data, structure, separator) => {
let encoded = data
.split('')
.map((val, idx) => BINARIES[structure[idx]])
.map((val, idx) => val ? val[data[idx]] : '');
if (separator) {
const last = data.length - 1;
encoded = encoded.map((val, idx) => (
idx < last ? val + separator : val
));
}
return encoded.join('');
};
module.exports = encode;

View File

@ -1,12 +1,13 @@
{ {
"id": "cards", "id": "cards",
"name": "Cards", "name": "Cards",
"version": "0.04", "version": "0.05",
"description": "Display loyalty cards", "description": "Display loyalty cards",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}],
"tags": "cards", "tags": "cards",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"cards.app.js","url":"app.js"}, {"name":"cards.app.js","url":"app.js"},
@ -15,6 +16,12 @@
{"name":"cards.qrcode.js","url":"qrcode.js"}, {"name":"cards.qrcode.js","url":"qrcode.js"},
{"name":"cards.codabar.js","url":"codabar.js"}, {"name":"cards.codabar.js","url":"codabar.js"},
{"name":"cards.code39.js","url":"code39.js"}, {"name":"cards.code39.js","url":"code39.js"},
{"name":"cards.EAN.js","url":"EAN.js"},
{"name":"cards.EAN8.js","url":"EAN8.js"},
{"name":"cards.EAN13.js","url":"EAN13.js"},
{"name":"cards.UPC.js","url":"UPC.js"},
{"name":"cards.UPCE.js","url":"UPCE.js"},
{"name":"cards.encode.js","url":"encode.js"},
{"name":"cards.img","url":"app-icon.js","evaluate":true} {"name":"cards.img","url":"app-icon.js","evaluate":true}
], ],
"data": [{"name":"cards.settings.json"}] "data": [{"name":"cards.settings.json"}]

View File

@ -71,7 +71,7 @@ OsGridRef.latLongToOsGrid = function(point) {
* *
*/ */
function to_map_ref(digits, easting, northing) { function to_map_ref(digits, easting, northing) {
if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`);
let e = easting; let e = easting;
let n = northing; let n = northing;

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Remove 's' after seconds (on some clocks this looks like '5')

View File

@ -11,7 +11,7 @@
g.drawImage(atob("GBgBAP4AA/+ABwHAHABwGAAwMAAYYAAMYAAMwAAGwAAGwAAGwAAGwAAGwAAGwAAGYAAMYAAMMAAYGAAwHABwBwHAA/+AAP4AAAAA")); g.drawImage(atob("GBgBAP4AA/+ABwHAHABwGAAwMAAYYAAMYAAMwAAGwAAGwAAGwAAGwAAGwAAGwAAGYAAMYAAMMAAYGAAwHABwBwHAA/+AAP4AAAAA"));
g.drawLine(11,11,x,y).drawLine(12,11,x+1,y).drawLine(11,12,x,y+1).drawLine(12,12,x+1,y+1); g.drawLine(11,11,x,y).drawLine(12,11,x+1,y).drawLine(11,12,x,y+1).drawLine(12,12,x+1,y+1);
return { return {
text : s.toString().padStart(2,0)+"s", text : s.toString().padStart(2,0),
img : g.asImage("string") img : g.asImage("string")
}; };
}, },

View File

@ -1,6 +1,6 @@
{ "id": "clkinfosec", { "id": "clkinfosec",
"name": "Secondx Clockinfo", "name": "Seconds Clockinfo",
"version":"0.01", "version":"0.02",
"description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the time in seconds (many clocks only display minutes)", "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the time in seconds (many clocks only display minutes)",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],

View File

@ -9,3 +9,6 @@
0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
0.10: Use widget_utils. 0.10: Use widget_utils.
0.11: Minor code improvements 0.11: Minor code improvements
0.12: Added setting to change Battery estimate to hours
0.13: Fixed Battery estimate Default to percentage and improved setting string
0.14: Use `power_usage` module

View File

@ -10,7 +10,7 @@ Forum](http://forum.espruino.com/microcosms/1424/)
* Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel) * Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel)
* Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer * Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer
* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate) * Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate, Battery Estimate)
* The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle * The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle
* The heart value is displayed in RED if the confidence value is less than 50% * The heart value is displayed in RED if the confidence value is less than 50%
* NOTE: The heart rate monitor of Bangle JS 2 is not very accurate when moving about. * NOTE: The heart rate monitor of Bangle JS 2 is not very accurate when moving about.
@ -20,6 +20,7 @@ See [#1248](https://github.com/espruino/BangleApps/issues/1248)
[MyLocation](https://banglejs.com/apps/?id=mylocation) [MyLocation](https://banglejs.com/apps/?id=mylocation)
* The screen is updated every minute to save battery power * The screen is updated every minute to save battery power
* Uses the [BloggerSansLight](https://www.1001fonts.com/rounded-fonts.html?page=3) font, which if free for commercial use * Uses the [BloggerSansLight](https://www.1001fonts.com/rounded-fonts.html?page=3) font, which if free for commercial use
* You need to run >2V22 to show the battery estimate in hours
## Future Development ## Future Development
* Use mini icons in the information line rather that text * Use mini icons in the information line rather that text

View File

@ -83,6 +83,7 @@ function loadSettings() {
settings.gy = settings.gy||'#020'; settings.gy = settings.gy||'#020';
settings.fg = settings.fg||'#0f0'; settings.fg = settings.fg||'#0f0';
settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check);
settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours);
assignPalettes(); assignPalettes();
} }
@ -112,13 +113,39 @@ function updateSunRiseSunSet(now, lat, lon, line){
sunSet = extractTime(times.sunset); sunSet = extractTime(times.sunset);
} }
function batteryString(){
let stringToInsert;
if (settings.batt_hours) {
var batt_usage = require("power_usage").get().hrsLeft;
let rounded;
if (batt_usage > 24) {
var days = Math.floor(batt_usage/24);
var hours = Math.round((batt_usage/24 - days) * 24);
stringToInsert = '\n' + days + ((days < 2) ? 'd' : 'ds') + ' ' + hours + ((hours < 2) ? 'h' : 'hs');
}
else if (batt_usage > 9) {
rounded = Math.round(200000/E.getPowerUsage().total * 10) / 10;
}
else {
rounded = Math.round(200000/E.getPowerUsage().total * 100) / 100;
}
if (batt_usage < 24) {
stringToInsert = '\n' + rounded + ' ' + ((batt_usage < 2) ? 'h' : 'hs');
}
}
else{
stringToInsert = ' ' + E.getBattery() + '%';
}
return 'BATTERY' + stringToInsert;
}
const infoData = { const infoData = {
ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, 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_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} },
ID_SR: { calc: () => 'SUNRISE ' + sunRise }, ID_SR: { calc: () => 'SUNRISE ' + sunRise },
ID_SS: { calc: () => 'SUNSET ' + sunSet }, ID_SS: { calc: () => 'SUNSET ' + sunSet },
ID_STEP: { calc: () => 'STEPS ' + getSteps() }, ID_STEP: { calc: () => 'STEPS ' + getSteps() },
ID_BATT: { calc: () => 'BATTERY ' + E.getBattery() + '%' }, ID_BATT: { calc: batteryString},
ID_HRM: { calc: () => hrmCurrent } ID_HRM: { calc: () => hrmCurrent }
}; };

View File

@ -1,6 +1,6 @@
{ "id": "daisy", { "id": "daisy",
"name": "Daisy", "name": "Daisy",
"version": "0.11", "version": "0.14",
"dependencies": {"mylocation":"app"}, "dependencies": {"mylocation":"app"},
"description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times",
"icon": "app.png", "icon": "app.png",

View File

@ -5,7 +5,8 @@
let s = {'gy' : '#020', let s = {'gy' : '#020',
'fg' : '#0f0', 'fg' : '#0f0',
'color': 'Green', 'color': 'Green',
'check_idle' : true}; 'check_idle' : true,
'batt_hours' : false};
// ...and overwrite them with any saved values // ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings // This way saved values are preserved if a new version adds more settings
@ -45,6 +46,14 @@
s.idle_check = v; s.idle_check = v;
save(); save();
}, },
},
'Expected Battery Life In Days Not Percentage': {
value: !!s.batt_hours,
onchange: v => {
s.batt_hours = v;
save();
},
} }
}); });
}) })

View File

@ -0,0 +1 @@
0.01: New drag/swipe date time picker, e.g. for use with dated events alarms

View File

@ -0,0 +1,36 @@
# App Name
Datetime Picker allows to swipe along the bars to select date and time elements, e.g. for the datetime of Events in the Alarm App.
Screenshot: ![datetime with swipe controls](screenshot.png)
## Controls
Swipe to increase or decrease date and time elements. Press button or go back to select shown datetime.
![datetime with numbered swipe controls](screenshot2.png)
1. Year: swipe up to increase, down to decrease
2. Month: swipe right to increase, left to decrease
3. Day: swipe up to increase, down to decrease
4. Week: swipe up to increase week (same day next week), down to decrease (same day previous week)
5. Weekday: swipe right to increase, left to decrease (basically the same effect as 3, but with a focus on the weekday)
6. Hour: swipe right to increase, left to decrease
7. Minutes: swipe right to increase, left to decrease
8. 15 minutes: 00, 15, 30 or 45 minutes; swipe up to increase, down to decrease; wrap-around i.e. goes back to 00 after increasing from 45
## How to use it in code
Sample code which would show a prompt with the number of days and hours between now and the selected datetime:
require("datetimeinput").input().then(result => {
E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() {
load();
});
});
To set the initial value, pass a Date object named _datetime_, e.g. for today at 9:30 :
var datetime = new Date();
datetime.setHours(9, 30);
require("datetimeinput").input({datetime}).then(...

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgP/AAnfAgf9z4FD/AFE/gFECIoFB98+tv+voFB//C/99z3Z7+J84XC3/7DpAFhKYP3AgP3AoPAOQMD/v/84LB+Z2FABiDKPoqJFKaWe/P/9Pznuf+wKB/29z+2//uTYOeTYPtRMxZKQaPAh6hBnEBwEGAoMYgHf9+/dwP5A=="))

View File

@ -0,0 +1,5 @@
require("datetimeinput").input().then(result => {
E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() {
load();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

145
apps/datetime_picker/lib.js Normal file
View File

@ -0,0 +1,145 @@
exports.input = function(options) {
options = options||{};
var selectedDate;
if (options.datetime instanceof Date) {
selectedDate = new Date(options.datetime.getTime());
} else {
selectedDate = new Date();
selectedDate.setMinutes(0);
selectedDate.setSeconds(0);
selectedDate.setMilliseconds(0);
selectedDate.setHours(selectedDate.getHours() + 1);
}
var R;
var tip = {w: 12, h: 10};
var arrowRectArray;
var dragging = null;
var startPos = null;
var dateAtDragStart = null;
var SELECTEDFONT = '6x8:2';
function drawDateTime() {
g.clearRect(R.x+tip.w,R.y,R.x2-tip.w,R.y+40);
g.clearRect(R.x+tip.w,R.y2-60,R.x2-tip.w,R.y2-40);
g.setFont(SELECTEDFONT).setColor(g.theme.fg).setFontAlign(-1, -1, 0);
var dateUtils = require('date_utils');
g.drawString(selectedDate.getFullYear(), R.x+tip.w+10, R.y+15)
.drawString(dateUtils.month(selectedDate.getMonth()+1,1), R.x+tip.w+65, R.y+15)
.drawString(selectedDate.getDate(), R.x2-tip.w-40, R.y+15)
.drawString(`${dateUtils.dow(selectedDate.getDay(), 1)} ${selectedDate.toLocalISOString().slice(11,16)}`, R.x+tip.w+10, R.y2-60);
}
let dragHandler = function(event) {
"ram";
if (event.b) {
if (dragging === null) {
// determine which component we are affecting
var rect = arrowRectArray.find(rect => rect.y2
? (event.y >= rect.y && event.y <= rect.y2 && event.x >= rect.x - 10 && event.x <= rect.x + tip.w + 10)
: (event.x >= rect.x && event.x <= rect.x2 && event.y >= rect.y - tip.w - 5 && event.y <= rect.y + 5));
if (rect) {
dragging = rect;
startPos = dragging.y2 ? event.y : event.x;
dateAtDragStart = selectedDate;
}
}
if (dragging) {
dragging.swipe(dragging.y2 ? startPos - event.y : event.x - startPos);
drawDateTime();
}
} else {
dateAtDragStart = null;
dragging = null;
startPos = null;
}
};
let catchSwipe = ()=>{
E.stopEventPropagation&&E.stopEventPropagation();
};
return new Promise((resolve,reject) => {
// Interpret touch input
Bangle.setUI({
mode: 'custom',
back: ()=>{
Bangle.setUI();
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe listener if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(selectedDate);
},
drag: dragHandler
});
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
R = Bangle.appRect;
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
function drawArrow(rect) {
if(rect.x2) {
g.fillRect(rect.x + tip.h, rect.y - tip.w + 4, rect.x2 - tip.h, rect.y - 4)
.fillPoly([rect.x + tip.h, rect.y, rect.x + tip.h, rect.y - tip.w, rect.x, rect.y - (tip.w / 2)])
.fillPoly([rect.x2-tip.h, rect.y, rect.x2 - tip.h, rect.y - tip.w, rect.x2, rect.y - (tip.w / 2)]);
} else {
g.fillRect(rect.x + 4, rect.y + tip.h, rect.x + tip.w - 4, rect.y2 - tip.h)
.fillPoly([rect.x, rect.y + tip.h, rect.x + tip.w, rect.y + tip.h, rect.x + (tip.w / 2), rect.y])
.fillPoly([rect.x, rect.y2 - tip.h, rect.x + tip.w, rect.y2 - tip.h, rect.x + (tip.w / 2), rect.y2]);
}
}
var yearArrowRect = {x: R.x, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setFullYear(dateAtDragStart.getFullYear() + Math.floor(d/10));
if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0);
}};
var weekArrowRect = {x: R.x, y: yearArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setDate(dateAtDragStart.getDate() + (Math.floor(d/10) * 7));
}};
var dayArrowRect = {x: R.x2 - tip.w, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10));
}};
var fifteenMinutesArrowRect = {x: R.x2 - tip.w, y: dayArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setMinutes((((dateAtDragStart.getMinutes() - (dateAtDragStart.getMinutes() % 15) + (Math.floor(d/14) * 15)) % 60) + 60) % 60);
}};
var weekdayArrowRect = {x: R.x, y: R.y2, x2: (R.x2 - R.x) * 0.3 - 5, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10));
}};
var hourArrowRect = {x: weekdayArrowRect.x2 + 5, y: R.y2, x2: weekdayArrowRect.x2 + (R.x2 - R.x) * 0.38, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setHours((((dateAtDragStart.getHours() + Math.floor(d/10)) % 24) + 24) % 24);
}};
var minutesArrowRect = {x: hourArrowRect.x2 + 5, y: R.y2, x2: R.x2, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setMinutes((((dateAtDragStart.getMinutes() + Math.floor(d/7)) % 60) + 60) % 60);
}};
var monthArrowRect = {x: (R.x2 - R.x) * 0.2, y: R.y2 / 2 + 5, x2: (R.x2 - R.x) * 0.8, swipe: d => {
selectedDate = new Date(dateAtDragStart.valueOf());
selectedDate.setMonth(dateAtDragStart.getMonth() + Math.floor(d/10));
if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0);
}};
arrowRectArray = [yearArrowRect, weekArrowRect, dayArrowRect, fifteenMinutesArrowRect,
weekdayArrowRect, hourArrowRect, minutesArrowRect, monthArrowRect];
drawDateTime();
arrowRectArray.forEach(drawArrow);
});
};

View File

@ -0,0 +1,17 @@
{ "id": "datetime_picker",
"name": "Datetime picker",
"shortName":"Datetime picker",
"version":"0.01",
"description": "A library that allows to pick a date and time by swiping.",
"icon":"app.png",
"type":"module",
"tags":"datetimeinput",
"supports" : ["BANGLEJS2"],
"provides_modules" : ["datetimeinput"],
"readme": "README.md",
"screenshots" : [ { "url":"screenshot.png" } ],
"storage": [
{"name":"datetimeinput","url":"lib.js"},
{"name":"datetime_picker.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

1
apps/dedreckon/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: attempt to import

20
apps/dedreckon/README.md Normal file
View File

@ -0,0 +1,20 @@
# Ded Reckon
Dead Reckoning using compass and step counter.
This allows logging track using "dead reckoning" -- that's logging
angles from compass and distances from step counter. You need to mark
turns, and point watch to direction of the turn. Simultaneously, it
tries to log positions using GPS. You can use it to calibrate your
step length by comparing GPS and step counter data. It can also get
pretty accurate recording of track walked in right circumstances.
Tap bottom part of the screen to select display (text or map for
now). Point watch to new direction, then tap top left part of screen
to indicate a turn.
Map shows blue line for track from dead reckonging, and green line for
track from GPS.
You probably want magnav installed (and calibrated) for useful
results, as it provides library with better compass.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AFsAFtoADF1wwqF4wwhEI5goGGIjFYN4wFF1KbHGUolIMc4lGSdIwJd9DstAH7FrBywwgad4veDwojJBIIvcFwIACGBYICGDYvEGBYvdFwqyLL8i+LF7oxFRxgveGAQ0EF5IwfMY4vpL5AFLAEYv/F8owoE44vrAY4vmAQIEEF85dGGE0AE4gvoFwpmHd0oINAH4A/AH4AvA"))

BIN
apps/dedreckon/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,442 @@
/* Ded Reckon */
/* eslint-disable no-unused-vars */
/* fmt library v0.1.3 */
let fmt = {
icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3",
icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
/* 0 .. DD.ddddd
1 .. DD MM.mmm'
2 .. DD MM'ss"
*/
geo_mode : 1,
init: function() {},
fmtDist: function(km) {
if (km >= 1.0) return km.toFixed(1) + this.icon_km;
return (km*1000).toFixed(0) + this.icon_m;
},
fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); },
fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; },
draw_dot : 1,
add0: function(i) {
if (i > 9) {
return ""+i;
} else {
return "0"+i;
}
},
fmtTOD: function(now) {
this.draw_dot = !this.draw_dot;
let dot = ":";
if (!this.draw_dot)
dot = ".";
return now.getHours() + dot + this.add0(now.getMinutes());
},
fmtNow: function() { return this.fmtTOD(new Date()); },
fmtTimeDiff: function(d) {
if (d < 180)
return ""+d.toFixed(0);
d = d/60;
return ""+d.toFixed(0)+"m";
},
fmtAngle: function(x) {
switch (this.geo_mode) {
case 0:
return "" + x;
case 1: {
let d = Math.floor(x);
let m = x - d;
m = m*60;
return "" + d + " " + m.toFixed(3) + "'";
}
case 2: {
let d = Math.floor(x);
let m = x - d;
m = m*60;
let mf = Math.floor(m);
let s = m - mf;
s = s*60;
return "" + d + " " + mf + "'" + s.toFixed(0) + '"';
}
}
return "bad mode?";
},
fmtPos: function(pos) {
let x = pos.lat;
let c = "N";
if (x<0) {
c = "S";
x = -x;
}
let s = c+this.fmtAngle(x) + "\n";
c = "E";
if (x<0) {
c = "W";
x = -x;
}
return s + c + this.fmtAngle(x);
},
fmtFix: function(fix, t) {
if (fix && fix.fix && fix.lat) {
return this.fmtSpeed(fix.speed) + " " +
this.fmtAlt(fix.alt);
} else {
return "N/FIX " + this.fmtTimeDiff(t);
}
},
fmtSpeed: function(kph) {
return kph.toFixed(1) + this.icon_kph;
},
};
/* gps library v0.1.1 */
let gps = {
emulator: -1,
init: function(x) {
this.emulator = (process.env.BOARD=="EMSCRIPTEN"
|| process.env.BOARD=="EMSCRIPTEN2")?1:0;
},
state: {},
on_gps: function(f) {
let fix = this.getGPSFix();
f(fix);
/*
"lat": number, // Latitude in degrees
"lon": number, // Longitude in degrees
"alt": number, // altitude in M
"speed": number, // Speed in kph
"course": number, // Course in degrees
"time": Date, // Current Time (or undefined if not known)
"satellites": 7, // Number of satellites
"fix": 1 // NMEA Fix state - 0 is no fix
"hdop": number, // Horizontal Dilution of Precision
*/
this.state.timeout = setTimeout(this.on_gps, 1000, f);
},
off_gps: function() {
clearTimeout(this.state.timeout);
},
getGPSFix: function() {
if (!this.emulator)
return Bangle.getGPSFix();
let fix = {};
fix.fix = 1;
fix.lat = 50;
fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */
fix.alt = 200;
fix.speed = 5;
fix.course = 30;
fix.time = Date();
fix.satellites = 5;
fix.hdop = 12;
return fix;
},
gps_start : -1,
start_gps: function() {
Bangle.setGPSPower(1, "libgps");
this.gps_start = getTime();
},
stop_gps: function() {
Bangle.setGPSPower(0, "libgps");
},
};
/* ui library 0.1 */
let ui = {
display: 0,
numScreens: 2,
drawMsg: function(msg) {
g.reset().setFont("Vector", 35)
.setColor(1,1,1)
.fillRect(0, this.wi, 176, 176)
.setColor(0,0,0)
.drawString(msg, 5, 30);
},
drawBusy: function() {
this.drawMsg("\n.oO busy");
},
nextScreen: function() {
print("nextS");
this.display = this.display + 1;
if (this.display == this.numScreens)
this.display = 0;
this.drawBusy();
},
prevScreen: function() {
print("prevS");
this.display = this.display - 1;
if (this.display < 0)
this.display = this.numScreens - 1;
this.drawBusy();
},
onSwipe: function(dir) {
this.nextScreen();
},
h: 176,
w: 176,
wi: 32,
last_b: 0,
touchHandler: function(d) {
let x = Math.floor(d.x);
let y = Math.floor(d.y);
if (d.b != 1 || this.last_b != 0) {
this.last_b = d.b;
return;
}
print("touch", x, y, this.h, this.w);
/*
if ((x<this.h/2) && (y<this.w/2)) {
}
if ((x>this.h/2) && (y<this.w/2)) {
}
*/
if ((x<this.h/2) && (y>this.w/2)) {
print("prev");
this.prevScreen();
}
if ((x>this.h/2) && (y>this.w/2)) {
print("next");
this.nextScreen();
}
},
init: function() {
}
};
var last_steps = Bangle.getStepCount(), last_time = getTime(), speed = 0, step_phase = 0;
var mpstep = 0.719 * 1.15;
function updateSteps() {
if (step_phase ++ > 9) {
step_phase =0;
let steps = Bangle.getStepCount();
let time = getTime();
speed = 3.6 * mpstep * ((steps-last_steps) / (time-last_time));
last_steps = steps;
last_time = time;
}
return "" + fmt.fmtSpeed(speed) + " " + step_phase + "\n" + fmt.fmtDist(log_dist/1000) + " " + fmt.fmtDist(log_last/1000);
}
/* compensated compass */
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
const tiltfixread = require("magnav").tiltfixread;
var heading;
var cancel_gps = false;
function drawStats() {
let fix = gps.getGPSFix();
let msg = fmt.fmtFix(fix, getTime() - gps.gps_start);
msg += "\n" + fmt.fmtDist(gps_dist/1000) + " " + fmt.fmtDist(gps_last/1000) + "\n" + updateSteps();
let c = Bangle.getCompass();
if (c) msg += "\n" + c.heading.toFixed(0) + "/" + heading.toFixed(0) + "deg " + log.length + "\n";
g.reset().clear().setFont("Vector", 31)
.setColor(1,1,1)
.fillRect(0, 24, 176, 100)
.setColor(0,0,0)
.drawString(msg, 3, 25);
}
function updateGps() {
if (cancel_gps)
return;
heading = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
if (ui.display == 0) {
setTimeout(updateGps, 1000);
drawLog();
drawStats();
}
if (ui.display == 1) {
setTimeout(updateGps, 1000);
drawLog();
}
}
function stopGps() {
cancel_gps=true;
gps.stop_gps();
}
var log = [], log_dist = 0, gps_dist = 0;
var log_last = 0, gps_last = 0;
function logEntry() {
let e = {};
e.time = getTime();
e.fix = gps.getGPSFix();
e.steps = Bangle.getStepCount();
if (0) {
let c = Bangle.getCompass();
if (c)
e.dir = c.heading;
else
e.dir = -1;
} else {
e.dir = heading;
}
return e;
}
function onTurn() {
let e = logEntry();
log.push(e);
}
function radians(a) { return a*Math.PI/180; }
function degrees(a) { return a*180/Math.PI; }
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
// https://www.movable-type.co.uk/scripts/latlong.html
// (Equirectangular approximation)
function calcDistance(a,b) {
var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
return Math.sqrt(x*x + y*y) * 6371000;
}
var dn, de;
function initConv(fix) {
let n = { lat: fix.lat+1, lon: fix.lon };
let e = { lat: fix.lat, lon: fix.lon+1 };
dn = calcDistance(fix, n);
de = calcDistance(fix, e);
print("conversion is ", dn, 108000, de, 50000);
}
function toM(start, fix) {
return { x: (fix.lon - start.lon) * de, y: (fix.lat - start.lat) * dn };
}
var mpp = 4;
function toPix(q) {
let p = { x: q.x, y: q.y };
p.x /= mpp; /* 10 m / pix */
p.y /= -mpp;
p.x += 85;
p.y += 85;
return p;
}
function drawLog() {
let here = logEntry();
if (!here.fix.lat) {
here.fix.lat = 50;
here.fix.lon = 14;
}
initConv(here.fix);
log.push(here);
let l = log;
log_dist = 0;
log_last = -1;
gps_last = -1;
g.reset().clear();
g.setColor(0, 0, 1);
let last = { x: 0, y: 0 };
for (let i = l.length - 2; i >= 0; i--) {
let next = {};
let m = (l[i+1].steps - l[i].steps) * mpstep;
let dir = radians(180 + l[i].dir);
next.x = last.x + m * Math.sin(dir);
next.y = last.y + m * Math.cos(dir);
print(dir, m, last, next);
let lp = toPix(last);
let np = toPix(next);
g.drawLine(lp.x, lp.y, np.x, np.y);
g.drawCircle(np.x, np.y, 3);
last = next;
if (log_last == -1)
log_last = m;
log_dist += m;
}
g.setColor(0, 1, 0);
last = { x: 0, y: 0 };
gps_dist = 0;
for (let i = l.length - 2; i >= 0; i--) {
let fix = l[i].fix;
if (fix.fix && fix.lat) {
let next = toM(here.fix, fix);
let lp = toPix(last);
let np = toPix(next);
let d = Math.sqrt((next.x-last.x)*(next.x-last.x)+(next.y-last.y)*(next.y-last.y));
if (gps_last == -1)
gps_last = d;
gps_dist += d;
g.drawLine(lp.x, lp.y, np.x, np.y);
g.drawCircle(np.x, np.y, 3);
last = next;
}
}
log.pop();
}
function testPaint() {
let pos = gps.getGPSFix();
log = [];
let e = { fix: pos, steps: 100, dir: 0 };
log.push(e);
e = { fix: pos, steps: 200, dir: 90 };
log.push(e);
e = { fix: pos, steps: 300, dir: 0 };
log.push(e);
print(log, log.length, log[0], log[1]);
drawLog();
}
function touchHandler(d) {
let x = Math.floor(d.x);
let y = Math.floor(d.y);
if (d.b != 1 || ui.last_b != 0) {
ui.last_b = d.b;
return;
}
if ((x<ui.h/2) && (y<ui.w/2)) {
ui.drawMsg("Turn");
onTurn();
}
if ((x>ui.h/2) && (y<ui.w/2)) {
ui.drawMsg("Writing");
require('Storage').writeJSON("speedstep."+getTime()+".json", log);
ui.drawMsg("Wrote");
}
ui.touchHandler(d);
}
fmt.init();
gps.init();
ui.init();
ui.drawBusy();
gps.start_gps();
Bangle.setCompassPower(1, "speedstep");
Bangle.on("drag", touchHandler);
Bangle.setUI({
mode : "custom",
swipe : (s) => ui.onSwipe(s),
clock : 0
});
if (0)
testPaint();
if (1) {
g.reset();
updateGps();
}

View File

@ -0,0 +1,13 @@
{ "id": "dedreckon",
"name": "Ded Reckon",
"version": "0.01",
"description": "Dead Reckoning using compass and step counter",
"icon": "app.png",
"readme": "README.md",
"supports" : ["BANGLEJS2"],
"tags": "outdoors",
"storage": [
{"name":"dedreckon.app.js","url":"dedreckon.app.js"},
{"name":"dedreckon.img","url":"app-icon.js","evaluate":true}
]
}

1
apps/delaylock/ChangeLog Normal file
View File

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

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

@ -0,0 +1,23 @@
# Delayed Locking
Delay the locking of the touchscreen to 5 seconds after the backlight turns off. Giving you the chance to interact with the watch without having to press the hardware button again.
## Usage
Just install and the behavior is tweaked at boot time.
## Features
- respects the LCD Timeout and Brightness as configured in the settings app.
## Requests
Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions.
## Creator
thyttan
## Acknowledgements
Inspired by the conversation between Gordon Williams and user156427 linked here: https://forum.espruino.com/conversations/392219/

BIN
apps/delaylock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

21
apps/delaylock/boot.js Normal file
View File

@ -0,0 +1,21 @@
{
let backlightTimeout = Bangle.getOptions().backlightTimeout;
let brightness = require("Storage").readJSON("setting.json", true);
brightness = brightness?brightness.brightness:1;
Bangle.setOptions({
backlightTimeout: backlightTimeout,
lockTimeout: backlightTimeout+5000
});
let turnLightsOn = (_,numOrObj)=>{
if (!Bangle.isBacklightOn()) {
Bangle.setLCDPower(brightness);
if (typeof numOrObj !== "number") E.stopEventPropagation(); // Touches will not be passed on to other listeners, but swipes will.
}
};
setWatch(turnLightsOn, BTN1, { repeat: true, edge: 'rising' });
Bangle.prependListener("swipe", turnLightsOn);
Bangle.prependListener("touch", turnLightsOn);
}

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