1
0
Fork 0

Merge branch 'espruino:master' into master

master
jla-42 2024-07-02 16:25:46 +02:00 committed by GitHub
commit 5a6637b523
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
253 changed files with 8979 additions and 0 deletions

249
.eslintrc.js Normal file
View File

@ -0,0 +1,249 @@
const lintExemptions = require("./apps/lint_exemptions.js");
const fs = require("fs");
const path = require("path");
function findGeneratedJS(roots) {
function* listFiles(dir, allow) {
for (const f of fs.readdirSync(dir)) {
const filepath = path.join(dir, f);
const stat = fs.statSync(filepath);
if (stat.isDirectory()) {
yield* listFiles(filepath, allow);
} else if(allow(filepath)) {
yield filepath;
}
}
}
return roots.flatMap(root =>
[...listFiles(root, f => f.endsWith(".ts"))]
.map(f => f.replace(/\.ts$/, ".js"))
);
}
module.exports = {
"env": {
// TODO: "espruino": false
// TODO: "banglejs": false
// For a prototype of the above, see https://github.com/espruino/BangleApps/pull/3237
},
"extends": "eslint:recommended",
"globals": {
// Methods and Fields at https://banglejs.com/reference
"Array": "readonly",
"ArrayBuffer": "readonly",
"ArrayBufferView": "readonly",
"Bangle": "readonly",
"BluetoothDevice": "readonly",
"BluetoothRemoteGATTCharacteristic": "readonly",
"BluetoothRemoteGATTServer": "readonly",
"BluetoothRemoteGATTService": "readonly",
"Boolean": "readonly",
"console": "readonly",
"DataView": "readonly",
"Date": "readonly",
"E": "readonly",
"Error": "readonly",
"Flash": "readonly",
"Float32Array": "readonly",
"Float64Array": "readonly",
"Function": "readonly",
"Graphics": "readonly",
"I2C": "readonly",
"Int16Array": "readonly",
"Int32Array": "readonly",
"Int8Array": "readonly",
"InternalError": "readonly",
"JSON": "readonly",
"Math": "readonly",
"Modules": "readonly",
"NRF": "readonly",
"Number": "readonly",
"Object": "readonly",
"OneWire": "readonly",
"Pin": "readonly",
"process": "readonly",
"Promise": "readonly",
"ReferenceError": "readonly",
"RegExp": "readonly",
"Serial": "readonly",
"SPI": "readonly",
"StorageFile": "readonly",
"String": "readonly",
"SyntaxError": "readonly",
"TFMicroInterpreter": "readonly",
"TypeError": "readonly",
"Uint16Array": "readonly",
"Uint24Array": "readonly",
"Uint32Array": "readonly",
"Uint8Array": "readonly",
"Uint8ClampedArray": "readonly",
"Unistroke": "readonly",
"Waveform": "readonly",
// Methods and Fields at https://banglejs.com/reference
"__FILE__": "readonly",
"analogRead": "readonly",
"analogWrite": "readonly",
"arguments": "readonly",
"atob": "readonly",
"Bluetooth": "readonly",
"BTN": "readonly",
"BTN1": "readonly",
"BTN2": "readonly",
"BTN3": "readonly",
"BTN4": "readonly",
"BTN5": "readonly",
"btoa": "readonly",
"changeInterval": "readonly",
"clearInterval": "readonly",
"clearTimeout": "readonly",
"clearWatch": "readonly",
"decodeURIComponent": "readonly",
"digitalPulse": "readonly",
"digitalRead": "readonly",
"digitalWrite": "readonly",
"dump": "readonly",
"echo": "readonly",
"edit": "readonly",
"encodeURIComponent": "readonly",
"eval": "readonly",
"getPinMode": "readonly",
"getSerial": "readonly",
"getTime": "readonly",
"global": "readonly",
"HIGH": "readonly",
"I2C1": "readonly",
"Infinity": "readonly",
"isFinite": "readonly",
"isNaN": "readonly",
"LED": "readonly",
"LED1": "readonly",
"LED2": "readonly",
"load": "readonly",
"LoopbackA": "readonly",
"LoopbackB": "readonly",
"LOW": "readonly",
"NaN": "readonly",
"parseFloat": "readonly",
"parseInt": "readonly",
"peek16": "readonly",
"peek32": "readonly",
"peek8": "readonly",
"pinMode": "readonly",
"poke16": "readonly",
"poke32": "readonly",
"poke8": "readonly",
"print": "readonly",
"require": "readonly",
"reset": "readonly",
"save": "readonly",
"Serial1": "readonly",
"setBusyIndicator": "readonly",
"setInterval": "readonly",
"setSleepIndicator": "readonly",
"setTime": "readonly",
"setTimeout": "readonly",
"setWatch": "readonly",
"shiftOut": "readonly",
"SPI1": "readonly",
"Terminal": "readonly",
"trace": "readonly",
"VIBRATE": "readonly",
// Aliases and not defined at https://banglejs.com/reference
"g": "readonly",
"WIDGETS": "readonly",
"module": "readonly",
"exports": "writable",
"D0": "readonly",
"D1": "readonly",
"D2": "readonly",
"D3": "readonly",
"D4": "readonly",
"D5": "readonly",
"D6": "readonly",
"D7": "readonly",
"D8": "readonly",
"D9": "readonly",
"D10": "readonly",
"D11": "readonly",
"D12": "readonly",
"D13": "readonly",
"D14": "readonly",
"D15": "readonly",
"D16": "readonly",
"D17": "readonly",
"D18": "readonly",
"D19": "readonly",
"D20": "readonly",
"D21": "readonly",
"D22": "readonly",
"D23": "readonly",
"D24": "readonly",
"D25": "readonly",
"D26": "readonly",
"D27": "readonly",
"D28": "readonly",
"D29": "readonly",
"D30": "readonly",
"D31": "readonly",
"bleServiceOptions": "writable", // available in boot.js code that's called ad part of bootupdate
},
"parserOptions": {
"ecmaVersion": 11
},
"rules": {
"indent": [
"off",
2,
{
"SwitchCase": 1
}
],
"no-constant-condition": "off",
"no-delete-var": "off",
"no-empty": ["warn", { "allowEmptyCatch": true }],
"no-global-assign": "off",
"no-inner-declarations": "off",
"no-prototype-builtins": "off",
"no-redeclare": "off",
"no-unreachable": "warn",
"no-cond-assign": "warn",
"no-useless-catch": "warn",
"no-undef": "warn",
"no-unused-vars": ["warn", { "args": "none" } ],
"no-useless-escape": "off",
"no-control-regex" : "off"
},
overrides: [
{
files: ["*.ts"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
rules: {
"no-delete-var": "off",
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-prototype-builtins": "off",
"prefer-const": "off",
"prefer-rest-params": "off",
"no-control-regex" : "off",
"@typescript-eslint/no-delete-var": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
}
},
...Object.entries(lintExemptions).map(([filePath, {rules}]) => ({
files: [filePath],
rules: Object.fromEntries(rules.map(rule => [rule, "off"])),
})),
],
ignorePatterns: findGeneratedJS(["apps/", "modules/"]),
reportUnusedDisableDirectives: true,
}

9
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,9 @@
Contributing to BangleApps
==========================
https://github.com/espruino/BangleApps?tab=readme-ov-file#getting-started
has some links to tutorials on developing for Bangle.js.
Please check out the Wiki to get an idea what sort of things
we'd like to see for contributed apps: https://github.com/espruino/BangleApps/wiki/App-Contribution

View File

@ -0,0 +1,48 @@
{
// timeout used to update every minute
let drawTimeout;
// schedule a draw for the next minute
let queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
};
let draw = function() {
// queue next draw in one minute
queueDraw();
// Work out where to draw...
var x = g.getWidth()/2;
var y = g.getHeight()/2;
g.reset();
// work out locale-friendly date/time
var date = new Date();
var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date);
// draw time
g.setFontAlign(0,0).setFont("Vector",48);
g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
g.drawString(timeStr,x,y);
// draw date
y += 35;
g.setFontAlign(0,0).setFont("6x8");
g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
g.drawString(dateStr,x,y);
};
// Clear the screen once, at startup
g.clear();
// draw immediately at first, queue update
draw();
// Show launcher when middle button pressed
Bangle.setUI({mode:"clock", remove:function() {
// free any memory we allocated to allow fast loading
}});
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

@ -0,0 +1,19 @@
# Accerleration Data Provider
This app provides acceleration data via Bluetooth, which can be used in Gadgetbridge.
## Usage
This boot code runs in the background and has no user interface.
Currently this app is used to enable Sleep as Android tracking for your Banglejs using Gadgetbridge.
**Please Note**: This app only listens to "accel" events and sends them to your phone using Bluetooth.
## Creator
[Another Stranger](https://github.com/anotherstranger)
## Aknowledgements
Special thanks to [José Rebelo](https://github.com/joserebelo) and [Rob Pilling](https://github.com/bobrippling)
for their Code Reviews and guidance.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

55
apps/accelsender/boot.js Normal file
View File

@ -0,0 +1,55 @@
(() => {
/**
* Sends a message to the gadgetbridge via Bluetooth.
* @param {Object} message - The message to be sent.
*/
function gbSend(message) {
try {
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
} catch (error) {
console.error("Failed to send message via Bluetooth:", error);
}
}
var max_acceleration = { x: 0, y: 0, z: 0, diff: 0, td: 0, mag: 0 };
var hasData = false;
/**
* Updates the maximum acceleration if the current acceleration is greater.
* @param {Object} accel - The current acceleration object with x, y, z, and mag properties.
*/
function updateAcceleration(accel) {
hasData = true;
var current_max_raw = accel.mag;
var max_raw = max_acceleration.mag;
if (current_max_raw > max_raw) {
max_acceleration = accel;
}
}
/**
* Updates the acceleration data and sends it to gadgetbridge.
* Resets the maximum acceleration.
* Note: If your interval setting is too short, the last value gets sent again.
*/
function sendAccelerationData() {
var accel = hasData ? max_acceleration : Bangle.getAccel();
var update_data = {
t: "accel", accel: accel
};
gbSend(update_data);
max_acceleration = { x: 0, y: 0, z: 0, mag: 0, diff: 0, td: 0 };
hasData = false;
}
var config = require("Storage").readJSON("accelsender.json") || {};
if (config.enabled) { // Gadgetbridge needs to enable and disable tracking by writing {enabled: true} to "accelsender.json" and reloading
setInterval(sendAccelerationData, config.interval);
Bangle.on("accel", updateAcceleration); // Log all acceleration events
}
})();

1
apps/accelsender/boot.min.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>{function e(a){c=!0;a.mag>b.mag&&(b=a)}function f(){var a={t:"accel",accel:c?b:Bangle.getAccel()};try{Bluetooth.println(""),Bluetooth.println(JSON.stringify(a))}catch(g){console.error("Failed to send message via Bluetooth:",g)}b={x:0,y:0,z:0,mag:0,diff:0,td:0};c=!1}var b={x:0,y:0,z:0,diff:0,td:0,mag:0},c=!1,d=require("Storage").readJSON("accelsender.json")||{};d.enabled&&(setInterval(f,d.interval),Bangle.on("accel",e))})();

View File

@ -0,0 +1,4 @@
{
"enabled": false,
"interval": 10000
}

View File

@ -0,0 +1,27 @@
{
"id": "accelsender",
"name": "Acceleration Data Provider",
"shortName": "Accel Data Provider",
"version": "0.01",
"description": "This app sends accelerometer and heart rate data from your Bangle.js via Bluetooth.",
"icon": "bluetooth.png",
"type": "bootloader",
"tags": "accel",
"supports": [
"BANGLEJS",
"BANGLEJS2"
],
"readme": "README.md",
"storage": [
{
"name": "accelsender.boot.js",
"url": "boot.min.js"
}
],
"data": [
{
"name": "accelsender.json",
"url": "config.json"
}
]
}

View File

@ -0,0 +1,3 @@
0.01: New Clock!
0.02: Fix fastloading memory leak and clockinfo overwritten by hands
0.03: Use new clockinfo lib with function to render images wirh borders

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X//8HA4IEBgH4C5cFqgJHitQjWpBY9q0gLvI5ar/AAkgBRMC1ALJlX6CxOrBZMq34LJ1f/9QKHhW//2gCxP6wAWHy/+KREqq4WIgGtr+qLhG1vw5IgX1KBALBywWIIwNaHJEAlNqUZOltAuJyouKqwuKrQuhywuJNIIuJlIuJHQLGIBYQ6IgtU1Q6GitQjWplQVGtWkBYIhHBcpHBBY5HBM5IABA"))

247
apps/analogquadclk/app.js Normal file
View File

@ -0,0 +1,247 @@
{
const W = g.getWidth();
const H = g.getHeight();
const background = require("clockbg"); // image backgrounds
let drawTimeout; // timeout used to update every minute
let date = new Date(); // date at last draw
let lastModified = {x1:0,y1:0,x2:W-1,y2:H-1,first:true}; // rect that was covered by hands
const HOUR_LEN = 55; // how far forwards does hand go?
const MIN_LEN = 72;
const HOUR_BACK = 10; // how far backwards dows hand go?
const MIN_BACK = 10;
const HOUR_W = 10; // width of cleared area
const MIN_W = 8;
const get_hand = function(len, w, cornerw, overhang) {
return new Int8Array([
0, overhang+w,
-cornerw, overhang+cornerw,
-w, overhang,
-w, -len,
-cornerw, -len - cornerw,
0, -len - w,
cornerw, -len - cornerw,
w, -len,
w, overhang,
cornerw, overhang+cornerw
]);
};
const hand_hour = get_hand(HOUR_LEN, 6, 4, HOUR_BACK);
const hand_hour_bg = get_hand(HOUR_LEN, HOUR_W, 8, HOUR_BACK);
const hand_minute = get_hand(MIN_LEN, 4, 3, MIN_BACK);
const hand_minute_bg = get_hand(MIN_LEN, MIN_W, 6, MIN_BACK);
// schedule a draw for the next minute
let queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
};
// draw the clock hands
let drawHands = function() {
let h = (date.getHours() + date.getMinutes()/60)*Math.PI/6, m = date.getMinutes()*Math.PI/30;
g.setColor(g.theme.bg).fillPolyAA(g.transformVertices(hand_hour_bg,{x:W/2,y:H/2,rotate:h}));
g.fillPolyAA(g.transformVertices(hand_minute_bg,{x:W/2,y:H/2,rotate:m}));
g.setColor("#f00").fillPolyAA(g.transformVertices(hand_hour,{x:W/2,y:H/2,rotate:h}));
g.setColor(g.theme.fg).fillPolyAA(g.transformVertices(hand_minute,{x:W/2,y:H/2,rotate:m}));
};
// return the screen area covered by clock hands (used for filling in background)
let getHandBounds = function() {
let h = (date.getHours() + date.getMinutes()/60)*Math.PI/6, m = date.getMinutes()*Math.PI/30;
let sh = Math.sin(h), ch = Math.cos(h), sm = Math.sin(m), cm = Math.cos(m);
return {
x1 : Math.round((W/2)+Math.min(sh*HOUR_LEN, sm*MIN_LEN, -sh*HOUR_BACK, -sm*MIN_BACK)-HOUR_W),
y1 : Math.round((H/2)-Math.max(ch*HOUR_LEN, cm*MIN_LEN, -ch*HOUR_BACK, -cm*MIN_BACK)-HOUR_W),
x2 : Math.round((W/2)+Math.max(sh*HOUR_LEN, sm*MIN_LEN, -sh*HOUR_BACK, -sm*MIN_BACK)+HOUR_W),
y2 : Math.round((H/2)-Math.min(ch*HOUR_LEN, cm*MIN_LEN, -ch*HOUR_BACK, -cm*MIN_BACK)+HOUR_W),
};
};
let draw = function() {
// queue next draw in one minute
queueDraw();
// work out locale-friendly date/time
date = new Date();
//var timeStr = require("locale").time(date,1);
//var dateStr = require("locale").date(date);
// fill in area that we changed last time
background.fillRect(lastModified.x1, lastModified.y1, lastModified.x2, lastModified.y2);
if (!lastModified.first) { // first draw we don't have clockInfoMenuA/etc defined
//print(lastModified);
if (lastModified.y1<40) {
if (lastModified.x1 < 40 ||
(lastModified.x1 < W/2 && lastModified.y1 < 16)) clockInfoMenuA.redraw();
if (lastModified.x2 > W-40 ||
(lastModified.x1 > W/2 && lastModified.y1 < 16)) clockInfoMenuB.redraw();
}
if (lastModified.y2>W-40) {
if (lastModified.x1 < 40 ||
(lastModified.x1 < W/2 && lastModified.y2>W-16)) clockInfoMenuD.redraw();
if (lastModified.x2 > W-40 ||
(lastModified.x1 > W/2 && lastModified.y2>W-16)) clockInfoMenuC.redraw();
}
}
// draw hands
drawHands();
lastModified = getHandBounds();
//g.drawRect(lastModified); // debug
};
// Clear the screen once, at startup
background.fillRect(0, 0, W - 1, H - 1);
// draw immediately at first, queue update
draw();
let clockInfoMenuA, clockInfoMenuB, clockInfoMenuC, clockInfoMenuD;
// Show launcher when middle button pressed
Bangle.setUI({
mode: "clock",
redraw : draw,
remove: function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
if (clockInfoMenuA) clockInfoMenuA.remove();
if (clockInfoMenuB) clockInfoMenuB.remove();
if (clockInfoMenuC) clockInfoMenuC.remove();
if (clockInfoMenuD) clockInfoMenuD.remove();
require("widget_utils").show(); // re-show widgets
}
});
// Load widgets
Bangle.loadWidgets();
require("widget_utils").hide();
// render clockinfos
let clockInfoDraw = function(itm, info, options) {
// itm: the item containing name/hasRange/etc
// info: data returned from itm.get() containing text/img/etc
// options: options passed into addInteractive
const left = options.x < 88,
top = options.y < 88,
imgx = left ? 1 : W - 28, imgy = top ? 19 : H - 42,
textx = left ? 2 : W - 1, texty = top ? 2 : H - 16;
let bg = g.theme.bg, fg = g.theme.fg;
// Clear the background
g.reset();
background.fillRect(imgx, imgy, imgx+25, imgy+25); // erase image
background.fillRect(left?0:W/2, texty-1, left?W/2:W-1, texty+15); // erase text
// indicate focus - change colours
if (options.focus) {
bg = g.theme.fg;
fg = g.toColor("#f00");
}
if (info.img)
require("clock_info").drawBorderedImage(info.img,imgx,imgy);
g.setFont("6x8:2").setFontAlign(left ? -1 : 1, -1);
g.setColor(bg).drawString(info.text, textx-2, texty). // draw the text background
drawString(info.text, textx+2, texty).
drawString(info.text, textx, texty-2).
drawString(info.text, textx, texty+2);
g.setColor(fg).drawString(info.text, textx, texty); // draw the text
// redraw hands if needed
if ((top && lastModified.x1<texty+15) ||
(!top && lastModified.y2>=texty)) {
g.reset();
drawHands();
}
};
// Load the clock infos
let clockInfoItems = require("clock_info").load();
let clockInfoItemsBangle = clockInfoItems.find(i=>i.name=="Bangle");
// Add extra Calendar and digital clock ClockInfos
if (clockInfoItemsBangle) {
if (!clockInfoItemsBangle.items.find(i=>i.name=="Date")) {
clockInfoItemsBangle.items.push({ name : "Date",
get : () => {
let d = new Date();
let g = Graphics.createArrayBuffer(24,24,1,{msb:true});
g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0);
g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17);
return {
text : require("locale").dow(d,1).toUpperCase(),
img : g.asImage("string")
};
},
show : function() {
this.interval = setTimeout(()=>{
this.emit("redraw");
this.interval = setInterval(()=>{
this.emit("redraw");
}, 86400000);
}, 86400000 - (Date.now() % 86400000));
},
hide : function() {
clearInterval(this.interval);
this.interval = undefined;
}
});
}
if (!clockInfoItemsBangle.items.find(i=>i.name=="Clock")) {
clockInfoItemsBangle.items.push({ name : "Clock",
get : () => {
return {
text : require("locale").time(new Date(),1),
img : atob("GBiBAAAAAAB+AAD/AAD/AAH/gAP/wAP/wAYAYAYAYAYAYAYAYAYAcAYAcAYAYAYAYAYAYAYAYAP/wAP/wAH/gAD/AAD/AAB+AAAAAA==")
};
},
show : function() {
this.interval = setTimeout(()=>{
this.emit("redraw");
this.interval = setInterval(()=>{
this.emit("redraw");
}, 60000);
}, 60000 - (Date.now() % 60000));
},
hide : function() {
clearInterval(this.interval);
this.interval = undefined;
}
});
}
}
// Add the 4 clockinfos
const CLOCKINFOSIZE = 50;
clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, {
x: 0,
y: 0,
w: CLOCKINFOSIZE,
h: CLOCKINFOSIZE,
draw: clockInfoDraw
});
clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, {
x: W - CLOCKINFOSIZE,
y: 0,
w: CLOCKINFOSIZE,
h: CLOCKINFOSIZE,
draw: clockInfoDraw
});
clockInfoMenuC = require("clock_info").addInteractive(clockInfoItems, {
x: W - CLOCKINFOSIZE,
y: H - CLOCKINFOSIZE,
w: CLOCKINFOSIZE,
h: CLOCKINFOSIZE,
draw: clockInfoDraw
});
clockInfoMenuD = require("clock_info").addInteractive(clockInfoItems, {
x: 0,
y: H - CLOCKINFOSIZE,
w: CLOCKINFOSIZE,
h: CLOCKINFOSIZE,
draw: clockInfoDraw
});
/*setInterval(function() {
date.ms += 60000; draw();
}, 500);*/
}

BIN
apps/analogquadclk/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,16 @@
{ "id": "analogquadclk",
"name": "Analog Quad Clock",
"shortName":"Quad Clock",
"version":"0.03",
"description": "An analog clock with clockinfos in each of the 4 corners, allowing 4 different data types to be rendered at once",
"icon": "icon.png",
"screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ],
"type": "clock",
"tags": "clock,clkinfo,analog,clockbg",
"supports" : ["BANGLEJS2"],
"dependencies" : { "clock_info":"module", "clockbg":"module" },
"storage": [
{"name":"analogquadclk.app.js","url":"app.js"},
{"name":"analogquadclk.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

28
apps/andark/settings.js Normal file
View File

@ -0,0 +1,28 @@
(function(back) {
const defaultSettings = {
loadWidgets : false,
textAboveHands : false,
shortHrHand : false
}
let settings = Object.assign(defaultSettings, require('Storage').readJSON('andark.json',1)||{});
const save = () => require('Storage').write('andark.json', settings);
const appMenu = {
'': {title: 'andark'}, '< Back': back,
/*LANG*/'Load widgets': {
value : !!settings.loadWidgets,
onchange : v => { settings.loadWidgets=v; save();}
},
/*LANG*/'Text above hands': {
value : !!settings.textAboveHands,
onchange : v => { settings.textAboveHands=v; save();}
},
/*LANG*/'Short hour hand': {
value : !!settings.shortHrHand,
onchange : v => { settings.shortHrHand=v; save();}
},
};
E.showMenu(appMenu);
});

99
apps/android/test.json Normal file
View File

@ -0,0 +1,99 @@
{
"app" : "android",
"setup" : [{
"id": "default",
"steps" : [
{"t":"cmd", "js": "Bangle.setGPSPower=(isOn, appID)=>{if (!appID) appID='?';if (!Bangle._PWR) Bangle._PWR={};if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);return Bangle._PWR.GPS.length>0;};", "text": "Fake the setGPSPower"},
{"t":"wrap", "fn": "Bangle.setGPSPower", "id": "gpspower"},
{"t":"cmd", "js": "Serial1.println = () => { }", "text": "Fake the serial port println"},
{"t":"cmd", "js": "Bluetooth.println = () => { }", "text": "Fake the Bluetooth println"},
{"t":"cmd", "js": "Bangle._PWR={}", "text": "Prepare an empty _PWR for following asserts"},
{"t":"cmd", "js": "require('Storage').writeJSON('android.settings.json', {overwriteGps: true})", "text": "Enable GPS overwrite"},
{"t":"cmd", "js": "eval(require('Storage').read('android.boot.js'))", "text": "Load the boot code"}
]
},{
"id": "connected",
"steps" : [
{"t":"cmd", "js": "NRF.getSecurityStatus = () => { return { connected: true };}", "text": "Control the security status to be connected"}
]
},{
"id": "disconnected",
"steps" : [
{"t":"cmd", "js": "NRF.getSecurityStatus = () => { return { connected: false };}", "text": "Control the security status to be disconnected"}
]
}],
"tests" : [{
"description": "Check setGPSPower is replaced",
"steps" : [
{"t":"cmd", "js": "Serial1.println = () => { }", "text": "Fake the serial port"},
{"t":"cmd", "js": "Bluetooth.println = () => { }", "text": "Fake the Bluetooth println"},
{"t":"cmd", "js": "require('Storage').writeJSON('android.settings.json', {overwriteGps: true})", "text": "Enable GPS overwrite"},
{"t":"cmd", "js": "eval(require('Storage').read('android.boot.js'))", "text": "Load the boot code"},
{"t":"assert", "js": "Bangle.setGPSPower.toString().includes('native')", "is":"false", "text": "setGPSPower has been replaced"}
]
},{
"description": "Test switching hardware GPS on and off",
"steps" : [
{"t":"setup", "id": "default"},
{"t":"setup", "id": "connected"},
{"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"},
{"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"},
{"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"},
{"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients"},
{"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn shows GPS as on"},
{"t":"assertCall", "id": "gpspower", "count": 1, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ] , "text": "internal GPS switched on"},
{"t":"assert", "js": "Bangle.setGPSPower(0, 'test')", "is":"falsy", "text": "setGPSPower returns falsy when switching off"},
{"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"},
{"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"},
{"t":"assertCall", "id": "gpspower", "count": 2, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ] , "text": "internal GPS switched off"}
]
},{
"description": "Test switching when GB GPS is available, internal GPS active until GB GPS event arrives",
"steps" : [
{"t":"setup", "id": "default"},
{"t":"setup", "id": "connected"},
{"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"},
{"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"},
{"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"},
{"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients"},
{"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn shows GPS as on"},
{"t":"assertCall", "id": "gpspower", "count": 1, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ], "text": "internal GPS switched on"},
{"t":"gb", "obj":{"t":"gps"}},
{"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients still there"},
{"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn still shows GPS as on"},
{"t":"assertCall", "id": "gpspower", "count": 2, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"}
]
},{
"description": "Test switching when GB GPS is available, internal stays off",
"steps" : [
{"t":"setup", "id": "default"},
{"t":"setup", "id": "connected"},
{"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"},
{"t":"gb", "obj":{"t":"gps"}},
{"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"},
{"t":"assert", "js": "Bangle.setGPSPower(0, 'test')", "is":"falsy", "text": "setGPSPower returns truthy when switching on"},
{"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS still switched off"}
]
},{
"description": "Test switching when GB GPS is available, but no event arrives",
"steps" : [
{"t":"setup", "id": "default"},
{"t":"setup", "id": "connected"},
{"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"},
{"t":"resetCall", "id": "gpspower"},
{"t":"gb", "obj":{"t":"gps"}, "text": "trigger switch"},
{"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"},
{"t":"resetCall", "id": "gpspower"},
{"t":"advanceTimers", "ms":"12000", "text": "wait for fallback"},
{"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ], "text": "internal GPS switched on caused by missing GB event"}
]
}]
}

15
apps/antonclk/test.json Normal file
View File

@ -0,0 +1,15 @@
{
"app" : "antonclk",
"tests" : [{
"description": "Check memory usage after setUI",
"steps" : [
{"t":"cmd", "js": "Bangle.loadWidgets()"},
{"t":"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"},
{"t":"cmd", "js": "Bangle.setUI()"},
{"t":"saveMemoryUsage"},
{"t":"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"},
{"t":"cmd", "js":"Bangle.setUI()"},
{"t":"checkMemoryUsage"}
]
}]
}

115
apps/autoreset/settings.js Normal file
View File

@ -0,0 +1,115 @@
(function(back) {
var FILE = 'autoreset.json';
// Mode can be 'blacklist' or 'whitelist'
// Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode
var DEFAULTS = {
'mode': 0,
'apps': [],
'timeout': 10
};
var settings = {};
var loadSettings = function() {
settings = require('Storage').readJSON(FILE, 1) || DEFAULTS;
};
var saveSettings = function(settings) {
require('Storage').write(FILE, settings);
};
// Get all app info files
var getApps = function() {
var apps = require('Storage').list(/\.info$/).map(appInfoFileName => {
var appInfo = require('Storage').readJSON(appInfoFileName, 1);
return appInfo && {
'name': appInfo.name,
'sortorder': appInfo.sortorder,
'src': appInfo.src,
'files': appInfo.files
};
}).filter(app => app && !!app.src);
apps.sort((a, b) => {
var n = (0 | a.sortorder) - (0 | b.sortorder);
if (n) return n; // do sortorder first
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
return apps;
};
var showMenu = function() {
var menu = {
'': { 'title': 'Auto Reset' },
/*LANG*/'< Back': () => {
back();
},
/*LANG*/'Mode': {
value: settings.mode,
min: 0,
max: 1,
format: v => ["Blacklist", "Whitelist"][v],
onchange: v => {
settings.mode = v;
saveSettings(settings);
},
},
/*LANG*/'App List': () => {
showAppSubMenu();
},
/*LANG*/'Timeout [min]': {
value: settings.timeout,
min: 0.25, max: 30, step : 0.25,
format: v => v,
onchange: v => {
settings.timeout = v;
saveSettings(settings);
},
},
};
E.showMenu(menu);
};
var showAppSubMenu = function() {
var menu = {
'': { 'title': 'Auto Reset' },
'< Back': () => {
showMenu();
},
'Add App': () => {
showAppList();
}
};
settings.apps.forEach(app => {
menu[app.name] = () => {
settings.apps.splice(settings.apps.indexOf(app), 1);
saveSettings(settings);
showAppSubMenu();
}
});
E.showMenu(menu);
}
var showAppList = function() {
var apps = getApps();
var menu = {
'': { 'title': 'Auto Reset' },
/*LANG*/'< Back': () => {
showMenu();
}
};
apps.forEach(app => {
menu[app.name] = () => {
settings.apps.push(app);
saveSettings(settings);
showAppSubMenu();
}
});
E.showMenu(menu);
}
loadSettings();
showMenu();
})

5
apps/blecsc/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.01: Initial version
0.02: Minor code improvements
0.03: Moved from cycling app, fixed connection issues and cadence
0.04: Added support for <1 wheel/crank event/second (using idle counters) (ref #3434)
0.05: Fix <1 event/second issue

32
apps/blecsc/README.md Normal file
View File

@ -0,0 +1,32 @@
# BLE Cycling Speed Sencor (CSC)
Displays data from a BLE Cycling Speed and Cadence sensor.
Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over.
## Settings
Accessible from `Settings -> Apps -> BLE CSC`
Here you can set the wheel diameter
## Development
```
var csc = require("blecsc").getInstance();
csc.on("status", txt => {
print("##", txt);
E.showMessage(txt);
});
csc.on("data", e => print(e));
csc.start();
```
The `data` event contains:
* cwr/ccr => wheel/crank cumulative revs
* lwet/lcet => wheel/crank last event time in 1/1024s
* wrps/crps => calculated wheel/crank revs per second
* wdt/cdt => time period in seconds between events
* wr => wheel revs
* kph => kilometers per hour

230
apps/blecsc/blecsc.js Normal file
View File

@ -0,0 +1,230 @@
/**
* This library communicates with a Bluetooth CSC peripherial using the Espruino NRF library.
*
* ## Usage:
* 1. Register event handlers using the \`on(eventName, handlerFunction)\` method
* You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can
* have raw characteristic values passed through using the \`value\` event.
* 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method
* 3. To tear down the connection, call the \`disconnect()\` method
*
* ## Events
* - \`status\` - string containing connection status
* - \`data\` - the peripheral sends a notification containing wheel/crank event data
* - \`disconnect\` - the peripheral ends the connection or the connection is lost
*
* cwr/ccr => wheel/crank cumulative revs
* lwet/lcet => wheel/crank last event time in 1/1024s
* wrps/crps => calculated wheel/crank revs per second
* wdt/cdt => time period in seconds between events
* wr => wheel revs
* kph => kilometers per hour
*/
class BLECSC {
constructor() {
this.reconnect = false; // set when start called
this.device = undefined; // set when device found
this.gatt = undefined; // set when connected
// .on("status", => string
// .on("data"
// .on("disconnect"
this.resetStats();
// Set default values and merge with stored values
this.settings = Object.assign({
circum: 2068 // circumference in mm
}, (require('Storage').readJSON('blecsc.json', true) || {}));
}
resetStats() {
this.cwr = undefined;
this.ccr = undefined;
this.lwet = undefined;
this.lcet = undefined;
this.lastCwr = undefined;
this.lastCcr = undefined;
this.lastLwet = undefined;
this.lastLcet = undefined;
this.kph = undefined;
this.wrps = 0; // wheel revs per second
this.crps = 0; // crank revs per second
this.widle = 0; // wheel idle counter
this.cidle = 0; // crank idle counter
//this.batteryLevel = undefined;
}
getDeviceAddress() {
if (!this.device || !this.device.id)
return '00:00:00:00:00:00';
return this.device.id.split(" ")[0];
}
status(txt) {
this.emit("status", txt);
}
/**
* Find and connect to a device which exposes the CSC service.
*
* @return {Promise}
*/
connect() {
this.status("Scanning");
// Find a device, then get the CSC Service and subscribe to
// notifications on the CSC Measurement characteristic.
// NRF.setLowPowerConnection(true);
var reconnect = this.reconnect; // auto-reconnect
return NRF.requestDevice({
timeout: 5000,
filters: [{
services: ["1816"]
}],
}).then(device => {
this.status("Connecting");
this.device = device;
this.device.on('gattserverdisconnected', event => {
this.device = undefined;
this.gatt = undefined;
this.resetStats();
this.status("Disconnected");
this.emit("disconnect", event);
if (reconnect) {// auto-reconnect
reconnect = false;
setTimeout(() => {
if (this.reconnect) this.connect().then(() => {}, () => {});
}, 500);
}
});
return new Promise(resolve => setTimeout(resolve, 150)); // On CooSpo we get a 'Connection Timeout' if we try and connect too soon
}).then(() => {
return this.device.gatt.connect();
}).then(gatt => {
this.status("Connected");
this.gatt = gatt;
return gatt.getPrimaryService("1816");
}).then(service => {
return service.getCharacteristic("2a5b"); // UUID of the CSC measurement characteristic
}).then(characteristic => {
// register for changes on 2a5b
characteristic.on('characteristicvaluechanged', event => {
const flags = event.target.value.getUint8(0);
var offs = 0;
var data = {};
if (flags & 1) { // FLAGS_WREV_BM
this.lastCwr = this.cwr;
this.lastLwet = this.lwet;
this.cwr = event.target.value.getUint32(1, true);
this.lwet = event.target.value.getUint16(5, true);
if (this.lastCwr === undefined) this.lastCwr = this.cwr;
if (this.lastLwet === undefined) this.lastLwet = this.lwet;
if (this.lwet < this.lastLwet) this.lastLwet -= 65536;
let secs = (this.lwet - this.lastLwet) / 1024;
if (secs) {
this.wrps = (this.cwr - this.lastCwr) / secs;
this.widle = 0;
} else {
if (this.widle<5) this.widle++;
else this.wrps = 0;
}
this.kph = this.wrps * this.settings.circum / 3600;
Object.assign(data, { // Notify the 'wheelEvent' handler
cwr: this.cwr, // cumulative wheel revolutions
lwet: this.lwet, // last wheel event time
wrps: this.wrps, // wheel revs per second
wr: this.cwr - this.lastCwr, // wheel revs
wdt : secs, // time period
kph : this.kph
});
offs += 6;
}
if (flags & 2) { // FLAGS_CREV_BM
this.lastCcr = this.ccr;
this.lastLcet = this.lcet;
this.ccr = event.target.value.getUint16(offs + 1, true);
this.lcet = event.target.value.getUint16(offs + 3, true);
if (this.lastCcr === undefined) this.lastCcr = this.ccr;
if (this.lastLcet === undefined) this.lastLcet = this.lcet;
if (this.lcet < this.lastLcet) this.lastLcet -= 65536;
let secs = (this.lcet - this.lastLcet) / 1024;
if (secs) {
this.crps = (this.ccr - this.lastCcr) / secs;
this.cidle = 0;
} else {
if (this.cidle<5) this.cidle++;
else this.crps = 0;
}
Object.assign(data, { // Notify the 'crankEvent' handler
ccr: this.ccr, // cumulative crank revolutions
lcet: this.lcet, // last crank event time
crps: this.crps, // crank revs per second
cdt : secs, // time period
});
}
this.emit("data",data);
});
return characteristic.startNotifications();
/* }).then(() => {
return this.gatt.getPrimaryService("180f");
}).then(service => {
return service.getCharacteristic("2a19");
}).then(characteristic => {
characteristic.on('characteristicvaluechanged', (event)=>{
this.batteryLevel = event.target.value.getUint8(0);
});
return characteristic.startNotifications();*/
}).then(() => {
this.status("Ready");
}, err => {
this.status("Error: " + err);
if (reconnect) { // auto-reconnect
reconnect = false;
setTimeout(() => {
if (this.reconnect) this.connect().then(() => {}, () => {});
}, 500);
}
throw err;
});
}
/**
* Disconnect the device.
*/
disconnect() {
if (!this.gatt) return;
this.gatt.disconnect();
this.gatt = undefined;
}
/* Start trying to connect - will keep searching and attempting to connect*/
start() {
this.reconnect = true;
if (!this.device)
this.connect().then(() => {}, () => {});
}
/* Stop trying to connect, and disconnect */
stop() {
this.reconnect = false;
this.disconnect();
}
}
// Get an instance of BLECSC or create one if it doesn't exist
BLECSC.getInstance = function() {
if (!BLECSC.instance) {
BLECSC.instance = new BLECSC();
}
return BLECSC.instance;
};
exports = BLECSC;
/*
var csc = require("blecsc").getInstance();
csc.on("status", txt => {
print("##", txt);
E.showMessage(txt);
});
csc.on("data", e => print(e));
csc.start();
*/

74
apps/blecsc/clkinfo.js Normal file
View File

@ -0,0 +1,74 @@
(function() {
var csc = require("blecsc").getInstance();
//csc.on("status", txt => { print("CSC",txt); });
csc.on("data", e => {
ci.items.forEach(it => { if (it._visible) it.emit('redraw'); });
});
csc.on("disconnect", e => {
// redraw all with no info
ci.items.forEach(it => { if (it._visible) it.emit('redraw'); });
});
var uses = 0;
var ci = {
name: "CSC",
items: [
{ name : "Speed",
get : () => {
return {
text : (csc.kph === undefined) ? "--" : require("locale").speed(csc.kph),
img : atob("GBiBAAAAAAAAAAAAAAABwAABwAeBgAMBgAH/gAH/wAPDwA/DcD9m/Ge35sW9o8//M8/7E8CBA2GBhn8A/h4AeAAAAAAAAAAAAAAAAA==")
};
},
show : function() {
uses++;
if (uses==1) csc.start();
this._visible = true;
},
hide : function() {
this._visible = false;
uses--;
if (uses==0) csc.stop();
}
},
{ name : "Distance",
get : () => {
return {
text : (csc.kph === undefined) ? "--" : require("locale").distance(csc.cwr * csc.settings.circum / 1000),
img : atob("GBiBAAAAAB8AADuAAGDAAGTAAGRAAEBAAGBAAGDAADCAADGAIB8B+A/BjAfjBgAyJgAyIgAyAj/jBnADBmABjGAA2HAA8D//4AAAAA==")
};
},
show : function() {
uses++;
if (uses==1) csc.start();
this._visible = true;
},
hide : function() {
this._visible = false;
uses--;
if (uses==0) csc.stop();
}
},
{ name : "Cadence",
get : () => {
return {
text : (csc.crps === undefined) ? "--" : Math.round(csc.crps*60),
img : atob("GBiBAAAAAAAAAAB+EAH/sAeB8A4A8AwB8BgAABgAADAAADAAADAAADAADDAADDAAABgAABgAGAwAEA4AAAeAwAH8gAB8AAAAAAAAAA==")
};
},
show : function() {
uses++;
if (uses==1) csc.start();
this._visible = true;
},
hide : function() {
this._visible = false;
uses--;
if (uses==0) csc.stop();
}
}
]
};
return ci;
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

22
apps/blecsc/metadata.json Normal file
View File

@ -0,0 +1,22 @@
{
"id": "blecsc",
"name": "BLE Cycling Speed Sensor Library",
"shortName": "BLE CSC",
"version": "0.05",
"description": "Module to get live values from a BLE Cycle Speed (CSC) sensor. Includes recorder and clockinfo plugins",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth,clkinfo",
"type":"module",
"provides_modules" : ["blecsc"],
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"blecsc","url":"blecsc.js"},
{"name":"blecsc.settings.js","url":"settings.js"},
{"name":"blecsc.recorder.js","url":"recorder.js"},
{"name":"blecsc.clkinfo.js","url":"clkinfo.js"}
],
"data": [
{"name":"blecsc.json"}
]
}

28
apps/blecsc/recorder.js Normal file
View File

@ -0,0 +1,28 @@
(function(recorders) {
recorders.blecsc = function() {
var csc = require("blecsc").getInstance();
var speed, cadence;
csc.on("data", e => {
speed = e.kph; // speed in KPH
cadence = (e.crps===undefined)?"":Math.round(e.crps*60); // crank rotations per minute
});
return {
name : "CSC",
fields : ["Speed (kph)","Cadence (rpm)"],
getValues : () => {
var r = [speed,cadence];
speed = "";
cadence = "";
return r;
},
start : () => {
csc.start();
},
stop : () => {
csc.stop();
},
draw : (x,y) => g.setColor(csc.device?"#0f0":"#8f8").drawImage(atob("Dw+BAAAAAAABgOIA5gHcBxw9fpfTPqYRC8HgAAAAAAAA"),x,y)
};
}
})

85
apps/blecsc/settings.js Normal file
View File

@ -0,0 +1,85 @@
(function(back) {
const storage = require('Storage')
const SETTINGS_FILE = 'blecsc.json'
// Set default values and merge with stored values
let settings = Object.assign({
circum: 2068 // circumference in mm
}, (storage.readJSON(SETTINGS_FILE, true) || {}));
function saveSettings() {
storage.writeJSON(SETTINGS_FILE, settings);
}
function circumMenu() {
var v = 0|settings.circum;
var cm = 0|(v/10);
var mm = v-(cm*10);
E.showMenu({
'': { title: /*LANG*/"Circumference", back: mainMenu },
'cm': {
value: cm,
min: 80, max: 240, step: 1,
onchange: (v) => {
cm = v;
settings.circum = (cm*10)+mm;
saveSettings();
},
},
'+ mm': {
value: mm,
min: 0, max: 9, step: 1,
onchange: (v) => {
mm = v;
settings.circum = (cm*10)+mm;
saveSettings();
},
},
/*LANG*/'Std Wheels': function() {
// https://support.wahoofitness.com/hc/en-us/articles/115000738484-Tire-Size-Wheel-Circumference-Chart
E.showMenu({
'': { title: /*LANG*/'Std Wheels', back: circumMenu },
'650x38 wheel' : function() {
settings.circum = 1995;
saveSettings();
mainMenu();
},
'700x32c wheel' : function() {
settings.circum = 2152;
saveSettings();
mainMenu();
},
'24"x1.75 wheel' : function() {
settings.circum = 1890;
saveSettings();
mainMenu();
},
'26"x1.5 wheel' : function() {
settings.circum = 2010;
saveSettings();
mainMenu();
},
'27.5"x1.5 wheel' : function() {
settings.circum = 2079;
saveSettings();
mainMenu();
}
});
}
});
}
function mainMenu() {
E.showMenu({
'': { 'title': 'BLE CSC' },
'< Back': back,
/*LANG*/'Circumference': {
value: settings.circum+"mm",
onchange: circumMenu
},
});
}
mainMenu();
})

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"}
]
}

1
apps/burn/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.prettierignore

7
apps/burn/ChangeLog Normal file
View File

@ -0,0 +1,7 @@
0.01: New App!
0.02: Added README.md
0.03: Icon update
0.04: Icon Fix
0.05: Misc cleanup for PR
0.06: Implementing fixes from PR comments
0.07: Bug fix

30
apps/burn/README.md Normal file
View File

@ -0,0 +1,30 @@
# Burn: Calorie Counter
Burn is a simple calorie counter application. It is based on the original Counter app and has been enhanced with additional features (I recommend using
it with the "Digital Clock Widget", if you intend to keep it running).
## Features
- **Persistent counter**: The counter value is saved to flash, so it persists even when the app is closed or the device is restarted.
- **Daily reset**: The counter resets each day, allowing you to track your calorie intake on a daily basis.
- **Adjustable increment value**: You can adjust the increment value to suit your needs.
## Controls
### Bangle.js 1
- **BTN1**: Increase (or tap right)
- **BTN3**: Decrease (or tap left)
- **Press BTN2**: Change increment
### Bangle.js 2
- **Swipe up**: Increase
- **Swipe down**: Decrease
- **Press BTN**: Change increment
## How it Works
The counter value and the date are stored in a file named "kcal.txt". The counter value is read from the file when the app starts and written to the file whenever the counter value is updated.
The app uses the current date to determine whether to reset the counter. If the date has changed since the last time the counter was updated, the counter is reset to 0.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4f/gUA///j32o8h9v6glA+P9+/3tu27YCLvICBgEGCJlFmwRBgALFxQRIgIdF28JmIIEknG7cMyVJk4nBC4PJk4dBC4OJkmSrYRCkuACQM26/88wRGgQHB2iGCm33//8GoXtonbraYGgwRB/+bNY4AEg9/CIPbth3BxYRJn4RB/YRBgEUTwIRGne275IBCIQABjYRGrpCB+VK1gJDgYQFgXN23YQwIjEAA0WMwPQ0mSqgRK2QRBy6cBCJUFGIO12wjBpgRMlsAqmSqTOCAA0sCINogEIyVKCJdLoEAhQRNpQFCyVII5IRGqARQNZUECIcGyRLBPpPSCIQWBsCzK3VJoEB0mTCBUAitplEA0WYCJb7B1EBCYIAsjJjCknDpMkyUAmlKNwuEyEAgMSwwQBpNhAQM4CAcDkgRBe4ODogOB4MT0MldgcxCIXEyWAi3axNgykECIcBxIRBEwIRBYoK3BykGkw1DxPEyEZksSCIMEpcDjAIBGocbhMQiMzCIUyqALB5KmFhMghMk0VLQANYPoLeBCI0SRgOKJYOAgOSpihFCIMaCIOTgMk4ACBqVICIyTBKIMZkkAhpyBo4RHgOk4EZPAIjByVFNYYIBAoU2AoOwAQO5kmMf7QALpMg2VJ23Mn////8OIVThmUCIs27FvCIP+eQOSpUIyXAgFNEYfQCIX8P4OSp0MCIVZgEWFoNgrMktuwgdt23YOIQ"))

BIN
apps/burn/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

243
apps/burn/app.js Normal file
View File

@ -0,0 +1,243 @@
/*
* Burn: Calories Counter for Bangle.js (Espruino). Based on the original Counter app.
* Features:
* - Persistent counter: saved to a file.
* - Daily reset: counter resets each day.
* - Adjustable increment value.
*
* Bangle.js 1 Controls:
* - BTN1: Increase (or tap right)
* - BTN3: Decrease (or tap left)
* - Press BTN2: Change increment
*
* Bangle.js 2 Controls:
* - Swipe up: Increase
* - Swipe down: Decrease
* - Press BTN: Change increment
*/
// File variable to handle file operations
let file;
// Check if the hardware version is Bangle.js 2
const BANGLEJS2 = process.env.HWVERSION == 2;
// Importing the Storage module for file operations
const Storage = require("Storage");
// File path for the counter data
const PATH = "kcal.txt";
// Function to get the current date as a string
function dayString() {
const date = new Date();
// Month is 0-indexed, so we add 1 to get the correct month number
return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`;
}
// Counter object to keep track of the count and the date
let counter = { count: 0, date: dayString() };
// Function to read the counter from the file
function readCounterFromFile() {
try {
// Open the file in read mode
file = Storage.open(PATH, "r");
let line = file.readLine();
// If the file has content, parse it and update the counter
if (line) {
let splitLine = line.trim().split(",");
counter = { count: parseInt(splitLine[0]), date: splitLine[1] };
}
} catch (err) {
// If the file does not exist, the counter will remain 0
}
}
// Function to write the counter to the file
function writeCounterToFile() {
// Open the file in write mode
file = Storage.open(PATH, "w");
// Write the counter and date to the file
file.write(counter.count.toString() + "," + counter.date + "\n");
}
// Function to reset the counter
function resetCounter() {
// Reset the counter to 0 and update the date
counter = { count: 0, date: dayString() };
}
// Function to update the counter value
function updateCounterValue(value) {
// Update the counter with the new value, ensuring it's not less than 0
counter = { count: Math.max(0, value), date: dayString() };
}
// Function to update the counter
function updateCounter(value) {
// If the date has changed, reset the counter
if (counter.date != dayString()) {
resetCounter();
} else {
// Otherwise, update the counter value
updateCounterValue(value);
}
// Write the updated counter to the file
writeCounterToFile();
// Update the screen with the new counter value
updateScreen();
}
// Function to set a watch on a button to update the counter when pressed
function counterButtonWatch(button, increment) {
setWatch(
() => {
// If the button is for incrementing, or the counter is greater than 0, update the counter
if (increment || counter.count > 0) {
updateCounter(
counter.count + (increment ? getInterval() : -getInterval())
);
// Update the screen with the new counter value
updateScreen();
}
},
button,
{ repeat: true }
);
}
// Function to create interval functions
const createIntervalFunctions = function () {
// Array of intervals
const intervals = [50, 100, 200, 10];
// Current location in the intervals array
let location = 0;
// Function to get the current interval
const getInterval = function () {
return intervals[location];
};
// Function to rotate the increment
const rotateIncrement = function () {
// Update the location to the next index in the intervals array, wrapping around if necessary
location = (location + 1) % intervals.length;
// Update the screen with the new increment
updateScreen();
};
// Return the getInterval and rotateIncrement functions
return { getInterval, rotateIncrement };
};
// Create the interval functions
const intervalFunctions = createIntervalFunctions();
const getInterval = intervalFunctions.getInterval;
const rotateIncrement = intervalFunctions.rotateIncrement;
// Function to update the screen
function updateScreen() {
// Clear the screen area for the counter
g.clearRect(0, 50, 250, BANGLEJS2 ? 130 : 150)
.setBgColor(g.theme.bg)
.setColor(g.theme.fg)
.setFont("Vector", 40)
.setFontAlign(0, 0)
// Draw the counter value
.drawString(Math.floor(counter.count), g.getWidth() / 2, 100)
.setFont("6x8")
// Clear the screen area for the increment
.clearRect(g.getWidth() / 2 - 50, 140, g.getWidth() / 2 + 50, 160)
// Draw the increment value
.drawString("Increment: " + getInterval(), g.getWidth() / 2, 150);
// If the hardware version is Bangle.js 1, draw the increment and decrement buttons
if (!BANGLEJS2) {
g.drawString("-", 45, 100).drawString("+", 185, 100);
}
}
// If the hardware version is Bangle.js 2, set up the drag handling and button watch
let drag;
if (BANGLEJS2) {
// Set up drag handling
Bangle.on("drag", (e) => {
// If this is the start of a drag, record the initial coordinates
if (!drag) {
drag = { x: e.x, y: e.y };
return;
}
// If the button is still being pressed, ignore this event
if (e.b) return;
// Calculate the change in x and y from the start of the drag
const dx = e.x - drag.x;
const dy = e.y - drag.y;
// Reset the drag start coordinates
drag = null;
// Determine if the drag is primarily horizontal or vertical
const isHorizontalDrag = Math.abs(dx) > Math.abs(dy) + 10;
const isVerticalDrag = Math.abs(dy) > Math.abs(dx) + 10;
// If the drag is primarily horizontal, ignore it
if (isHorizontalDrag) {
return;
}
// If the drag is primarily vertical, update the counter
if (isVerticalDrag) {
// If the drag is downwards and the counter is greater than 0, decrease the counter
if (dy > 0 && counter.count > 0) {
updateCounter(counter.count - getInterval());
} else if (dy < 0) {
// If the drag is upwards, increase the counter
updateCounter(counter.count + getInterval());
}
// Update the screen with the new counter value
updateScreen();
}
});
// Set a watch on the button to rotate the increment when pressed
setWatch(rotateIncrement, BTN1, { repeat: true });
} else {
// If the hardware version is Bangle.js 1, set up the button watches
// Set watch on button to increase the counter
counterButtonWatch(BTN1, true);
counterButtonWatch(BTN5, true); // screen tap
// Set watch on button to decrease the counter
counterButtonWatch(BTN3, false);
counterButtonWatch(BTN4, false); // screen tap
// Set a watch on button to rotate the increment when pressed
setWatch(
() => {
rotateIncrement();
},
BTN2,
{ repeat: true }
);
}
// clear the screen
g.clear();
// Set the background and foreground colors
g.setBgColor(g.theme.bg).setColor(g.theme.fg);
// Load and draw the widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Read the counter from the file
readCounterFromFile();
// Update the screen with the counter value
updateScreen();

16
apps/burn/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "burn",
"name": "Burn",
"version": "0.07",
"description": "Simple Calorie Counter -- saves to flash and resets at midnight. I often keep mine running while the digital clock widget is at the top",
"icon": "app-icon.png",
"tags": "tool",
"readme":"README.md",
"supports": ["BANGLEJS", "BANGLEJS2"],
"screenshots": [{"url":"app-screenshot.png"}],
"allow_emulator": true,
"storage": [
{"name":"burn.app.js","url":"app.js"},
{"name":"burn.img","url":"app-icon.js","evaluate":true}
]
}

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;

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;

1
apps/chronlog/ChangeLog Normal file
View File

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

53
apps/chronlog/README.md Normal file
View File

@ -0,0 +1,53 @@
# Chrono Logger
Record times active on a task, course, work or anything really.
**Disclaimer:** No one is responsible for any loss of data you recorded with this app. If you run into problems please report as advised under **Requests** below.
With time on your side and a little help from your friends - you'll surely triumph over Lavos in the end!
![dump](dump.png) ![dump1](dump1.png) ![dump2](dump2.png) ![dump3](dump3.png) ![dump4](dump4.png) ![dump5](dump5.png) ![dump6](dump6.png)
## Usage
Click the large green button to log the start of your activity. Click the now red button again to log that you stopped.
## Features
- Saves to file on every toggling of the active state.
- csv file contents looks like:
```
1,Start,2024-03-02T15:18:09 GMT+0200
2,Note,Critical hit!
3,Stop,2024-03-02T15:19:17 GMT+0200
```
- Add annotations to the log.
- Create and switch between multiple logs.
- Sync log files to an Android device through Gadgetbridge (Needs pending code changes to Gadgetbridge).
- App state is restored when you start the app again.
## Controls
- Large button to toggle active state.
- Menu icon to access additional functionality.
- Hardware button exits menus, closes the app on the main screen.
## TODO and notes
- Delete individual tasks/logs through the app?
- Reset everything through the app?
- Scan for chronlog storage files that somehow no longer have tasks associated with it?
- Complete the Gadgetbridge side of things for sync.
- Sync to iOS?
- Inspect log files through the app, similarly to Recorder app?
- Changes to Android file system permissions makes it not always trivial to access the synced files.
## Requests
Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions.
## Creator
[thyttan](https://github.com/thyttan)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///gElq3X0ELJf4AiitAAYMBqgKEgNVrgEBmtVCAQABgtVr/Agf1qtQEQlpq6QB6tpEgkVywLDywLEq2uyoLB6wEBBZAECBYda32lBYIECBZ9W3wjDAgILPquWqoACAgILEtILDAgKOEAAyQCRwIAGSAUVBY6ECBZYGD7WnAoYLF9WrBYupAoWq1QECtQLBtWdBYt21QLC1LfBBYVfA4ILBlWq1f9rWVv/q1WoBYMKCgOvTYP6AoOgBYMCAoIAFwCQCBY6nDGAIAEFwQkIEQZVCBQZRCAAcGBYeQBYoYDCwwYECw5KC0gKIAH4APA="))

376
apps/chronlog/app.js Normal file
View File

@ -0,0 +1,376 @@
// TODO:
// - Add more /*LANG*/ tags for translations.
// - Check if there are chronlog storage files that should be added to tasks.
{
const storage = require("Storage");
let appData = storage.readJSON("chronlog.json", true) || {
currentTask : "default",
tasks : {
default: {
file : "chronlog_default.csv", // Existing default task log file
state : "stopped",
lineNumber : 0,
lastLine : "",
lastSyncedLine : "",
},
// Add more tasks as needed
},
};
let currentTask = appData.currentTask;
let tasks = appData.tasks;
delete appData;
let themeColors = g.theme;
let logEntry; // Avoid previous lint warning
// Function to draw the Start/Stop button with play and pause icons
let drawButton = ()=>{
var btnWidth = g.getWidth() - 40;
var btnHeight = 50;
var btnX = 20;
var btnY = (g.getHeight() - btnHeight) / 2;
var cornerRadius = 25;
var isStopped = tasks[currentTask].state === "stopped";
g.setColor(isStopped ? "#0F0" : "#F00"); // Set color to green when stopped and red when started
// Draw rounded corners of the button
g.fillCircle(btnX + cornerRadius, btnY + cornerRadius, cornerRadius);
g.fillCircle(btnX + btnWidth - cornerRadius, btnY + cornerRadius, cornerRadius);
g.fillCircle(btnX + cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius);
g.fillCircle(btnX + btnWidth - cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius);
// Draw rectangles to fill in the button
g.fillRect(btnX + cornerRadius, btnY, btnX + btnWidth - cornerRadius, btnY + btnHeight);
g.fillRect(btnX, btnY + cornerRadius, btnX + btnWidth, btnY + btnHeight - cornerRadius);
g.setColor(themeColors.bg); // Set icon color to contrast against the button's color
// Center the icon within the button
var iconX = btnX + btnWidth / 2;
var iconY = btnY + btnHeight / 2;
if (isStopped) {
// Draw play icon
var playSize = 10; // Side length of the play triangle
var offset = playSize / Math.sqrt(3) - 3;
g.fillPoly([
iconX - playSize, iconY - playSize + offset,
iconX - playSize, iconY + playSize + offset,
iconX + playSize * 2 / Math.sqrt(3), iconY + offset
]);
} else {
// Draw pause icon
var barWidth = 5; // Width of pause bars
var barHeight = btnHeight / 2; // Height of pause bars
var barSpacing = 5; // Spacing between pause bars
g.fillRect(iconX - barSpacing / 2 - barWidth, iconY - barHeight / 2, iconX - barSpacing / 2, iconY + barHeight / 2);
g.fillRect(iconX + barSpacing / 2, iconY - barHeight / 2, iconX + barSpacing / 2 + barWidth, iconY + barHeight / 2);
}
};
let drawHamburgerMenu = ()=>{
var x = g.getWidth() / 2; // Center the hamburger menu horizontally
var y = (7/8)*g.getHeight(); // Position it near the bottom
var lineLength = 18; // Length of the hamburger lines
var spacing = 6; // Space between the lines
g.setColor(themeColors.fg); // Set color to foreground color for the icon
// Draw three horizontal lines
for (var i = -1; i <= 1; i++) {
g.fillRect(x - lineLength/2, y + i * spacing - 1, x + lineLength/2, y + i * spacing + 1);
}
};
// Function to draw the task name centered between the widget field and the start/stop button
let drawTaskName = ()=>{
g.setFont("Vector", 20); // Set a smaller font for the task name display
// Calculate position to center the task name horizontally
var x = (g.getWidth()) / 2;
// Calculate position to center the task name vertically between the widget field and the start/stop button
var y = g.getHeight()/4; // Center vertically
g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color
g.drawString(currentTask, x, y); // Draw the task name centered on the screen
};
// Function to draw the last log entry of the current task
let drawLastLogEntry = ()=>{
g.setFont("Vector", 10); // Set a smaller font for the task name display
// Calculate position to center the log entry horizontally
var x = (g.getWidth()) / 2;
// Calculate position to place the log entry properly between the start/stop button and hamburger menu
var btnBottomY = (g.getHeight() + 50) / 2; // Y-coordinate of the bottom of the start/stop button
var menuBtnYTop = g.getHeight() * (5 / 6); // Y-coordinate of the top of the hamburger menu button
var y = btnBottomY + (menuBtnYTop - btnBottomY) / 2 + 2; // Center vertically between button and menu
g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color
g.drawString(g.wrapString(tasks[currentTask].lastLine, 150).join("\n"), x, y);
};
/*
// Helper function to read the last log entry from the current task's log file
let updateLastLogEntry = ()=>{
var filename = tasks[currentTask].file;
var file = require("Storage").open(filename, "r");
var lastLine = "";
var line;
while ((line = file.readLine()) !== undefined) {
lastLine = line; // Keep reading until the last line
}
tasks[currentTask].lastLine = lastLine;
};
*/
// Main UI drawing function
let drawMainMenu = ()=>{
g.clear();
Bangle.drawWidgets(); // Draw any active widgets
g.setColor(themeColors.bg); // Set color to theme's background color
g.fillRect(Bangle.appRect); // Fill the app area with the background color
drawTaskName(); // Draw the centered task name
drawLastLogEntry(); // Draw the last log entry of the current task
drawButton(); // Draw the Start/Stop toggle button
drawHamburgerMenu(); // Draw the hamburger menu button icon
//g.flip(); // Send graphics to the display
};
// Function to toggle the active state
let toggleChronlog = ()=>{
var dateObj = new Date();
var dateObjStrSplit = dateObj.toString().split(" ");
var currentTime = dateObj.getFullYear().toString() + "-" + (dateObj.getMonth()<10?"0":"") + dateObj.getMonth().toString() + "-" + (dateObj.getDate()<10?"0":"") + dateObj.getDate().toString() + "T" + (dateObj.getHours()<10?"0":"") + dateObj.getHours().toString() + ":" + (dateObj.getMinutes()<10?"0":"") + dateObj.getMinutes().toString() + ":" + (dateObj.getSeconds()<10?"0":"") + dateObj.getSeconds().toString() + " " + dateObjStrSplit[dateObjStrSplit.length-1];
tasks[currentTask].lineNumber = Number(tasks[currentTask].lineNumber) + 1;
logEntry = tasks[currentTask].lineNumber + (tasks[currentTask].state === "stopped" ? ",Start," : ",Stop,") + currentTime + "\n";
var filename = tasks[currentTask].file;
// Open the appropriate file and append the log entry
var file = require("Storage").open(filename, "a");
file.write(logEntry);
tasks[currentTask].lastLine = logEntry;
// Toggle the state and update the button text
tasks[currentTask].state = tasks[currentTask].state === "stopped" ? "started" : "stopped";
drawMainMenu(); // Redraw the main UI
};
// Define the touch handler function for the main menu
let handleMainMenuTouch = (button, xy)=>{
var btnTopY = (g.getHeight() - 50) / 2;
var btnBottomY = btnTopY + 50;
var menuBtnYTop = (7/8)*g.getHeight() - 15;
var menuBtnYBottom = (7/8)*g.getHeight() + 15;
var menuBtnXLeft = (g.getWidth() / 2) - 15;
var menuBtnXRight = (g.getWidth() / 2) + 15;
// Detect if the touch is within the toggle button area
if (xy.x >= 20 && xy.x <= (g.getWidth() - 20) && xy.y > btnTopY && xy.y < btnBottomY) {
toggleChronlog();
}
// Detect if the touch is within the hamburger menu button area
else if (xy.x >= menuBtnXLeft && xy.x <= menuBtnXRight && xy.y >= menuBtnYTop && xy.y <= menuBtnYBottom) {
showMenu();
}
};
// Function to attach the touch event listener
let setMainUI = ()=>{
Bangle.setUI({
mode: "custom",
back: load,
touch: handleMainMenuTouch
});
};
let saveAppState = ()=>{
let appData = {
currentTask : currentTask,
tasks : tasks,
};
require("Storage").writeJSON("chronlog.json", appData);
};
// Set up a listener for the 'kill' event
E.on('kill', saveAppState);
// Function to switch to a selected task
let switchTask = (taskName)=>{
currentTask = taskName; // Update the current task
// Reinitialize the UI elements
setMainUI();
drawMainMenu(); // Redraw UI to reflect the task change and the button state
};
// Function to create a new task
let createNewTask = ()=>{
// Prompt the user to input the task's name
require("textinput").input({
text: "" // Default empty text for new task
}).then(result => {
var taskName = result; // Store the result from text input
if (taskName) {
if (tasks.hasOwnProperty(taskName)) {
// Task already exists, handle this case as needed
E.showAlert(/*LANG*/"Task already exists", "Error").then(drawMainMenu);
} else {
// Create a new task log file for the new task
var filename = "chronlog_" + taskName.replace(/\W+/g, "_") + ".csv";
tasks[taskName] = {
file : filename,
state : "stopped",
lineNumber : 0,
lastLine : "",
lastSyncedLine : "",
};
currentTask = taskName;
setMainUI();
drawMainMenu(); // Redraw UI with the new task
}
} else {
setMainUI();
drawMainMenu(); // User cancelled, redraw main menu
}
}).catch(e => {
console.log("Text input error", e);
setMainUI();
drawMainMenu(); // In case of error also redraw main menu
});
};
// Function to display the list of tasks for selection
let chooseTask = ()=>{
// Construct the tasks menu from the tasks object
var taskMenu = {
"": { "title": /*LANG*/"Choose Task",
"back" : function() {
setMainUI(); // Reattach when the menu is closed
drawMainMenu(); // Cancel task selection
}
}
};
for (var taskName in tasks) {
if (!tasks.hasOwnProperty(taskName)) continue;
taskMenu[taskName] = (function(name) {
return function() {
switchTask(name);
};
})(taskName);
}
// Add a menu option for creating a new task
taskMenu[/*LANG*/"Create New Task"] = createNewTask;
E.showMenu(taskMenu); // Display the task selection
};
// Function to annotate the current or last work session
let annotateTask = ()=>{
// Prompt the user to input the annotation text
require("textinput").input({
text: "" // Default empty text for annotation
}).then(result => {
var annotationText = result.trim();
if (annotationText) {
// Append annotation to the last or current log entry
tasks[currentTask].lineNumber ++;
var annotatedEntry = tasks[currentTask].lineNumber + /*LANG*/",Note," + annotationText + "\n";
var filename = tasks[currentTask].file;
var file = require("Storage").open(filename, "a");
file.write(annotatedEntry);
tasks[currentTask].lastLine = annotatedEntry;
setMainUI();
drawMainMenu(); // Redraw UI after adding the annotation
} else {
// User cancelled, so we do nothing and just redraw the main menu
setMainUI();
drawMainMenu();
}
}).catch(e => {
console.log("Annotation input error", e);
setMainUI();
drawMainMenu(); // In case of error also redraw main menu
});
};
let syncToAndroid = (taskName, isFullSync)=>{
let mode = "a";
if (isFullSync) mode = "w";
let lastSyncedLine = tasks[taskName].lastSyncedLine || 0;
let taskNameValidFileName = taskName.replace(" ","_"); // FIXME: Should use something similar to replaceAll using a regular expression to catch all illegal characters.
let storageFile = require("Storage").open("chronlog_"+taskNameValidFileName+".csv", "r");
let contents = storageFile.readLine();
let lineNumber = contents ? contents.slice(0, contents.indexOf(",")) : 0;
let shouldSyncLine = ()=>{return (contents && (isFullSync || (Number(lineNumber)>Number(lastSyncedLine))));};
let doSyncLine = (mde)=>{Bluetooth.println(JSON.stringify({t:"file", n:"chronlog_"+taskNameValidFileName+".csv", c:contents, m:mde}));};
if (shouldSyncLine()) doSyncLine(mode);
contents = storageFile.readLine();
while (contents) {
lineNumber = contents.slice(0, contents.indexOf(",")); // Could theoretically do with `lineNumber++`, but this is more robust in case numbering in file ended up irregular.
if (shouldSyncLine()) doSyncLine("a");
contents = storageFile.readLine();
}
tasks[taskName].lastSyncedLine = lineNumber;
};
// Function to display the list of tasks for selection
let syncTasks = ()=>{
let isToDoFullSync = false;
// Construct the tasks menu from the tasks object
var syncMenu = {
"": { "title": /*LANG*/"Sync Tasks",
"back" : function() {
setMainUI(); // Reattach when the menu is closed
drawMainMenu(); // Cancel task selection
}
}
};
syncMenu[/*LANG*/"Full Resyncs"] = {
value: !!isToDoFullSync, // !! converts undefined to false
onchange: ()=>{
isToDoFullSync = !isToDoFullSync
},
}
for (var taskName in tasks) {
if (!tasks.hasOwnProperty(taskName)) continue;
syncMenu[taskName] = (function(name) {
return function() {syncToAndroid(name,isToDoFullSync);};
})(taskName);
}
E.showMenu(syncMenu); // Display the task selection
};
let showMenu = ()=>{
var menu = {
"": { "title": /*LANG*/"Menu",
"back": function() {
setMainUI(); // Reattach when the menu is closed
drawMainMenu(); // Redraw the main UI when closing the menu
},
},
/*LANG*/"Annotate": annotateTask, // Now calls the real annotation function
/*LANG*/"Change Task": chooseTask, // Opens the task selection screen
/*LANG*/"Sync to Android": syncTasks,
};
E.showMenu(menu);
};
Bangle.loadWidgets();
drawMainMenu(); // Draw the main UI when the app starts
// When the application starts, attach the touch event listener
setMainUI();
}

BIN
apps/chronlog/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/chronlog/dump.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
apps/chronlog/dump1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/chronlog/dump2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/chronlog/dump3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/chronlog/dump4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
apps/chronlog/dump5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
apps/chronlog/dump6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,14 @@
{ "id": "chronlog",
"name": "Chrono Logger",
"version":"0.01",
"description": "Record time active on a task, course, work or anything really.",
"icon": "app.png",
"tags": "logging, record, work, tasks",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"screenshots" : [ { "url":"dump.png"}, { "url":"dump1.png" }, { "url":"dump2.png" }, { "url":"dump3.png" }, { "url":"dump4.png" }, { "url":"dump5.png" }, { "url":"dump6.png" } ],
"storage": [
{"name":"chronlog.app.js","url":"app.js"},
{"name":"chronlog.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1 @@
0.01: New Clock Info!

View File

@ -0,0 +1,27 @@
(function() {
var speed;
function gpsHandler(e) {
speed = e.speed;
ci.items[0].emit('redraw');
}
var ci = {
name: "GPS",
items: [
{ name : "Speed",
get : function() { return { text : isFinite(speed) ? require("locale").speed(speed) : "--",
v : 0, min : isFinite(speed) ? speed : 0, max : 150,
img : atob("GBiBAAAAAAAAAAAAAAAAAAD/AAHDgAMYwAbDYAwAMAoA0BgDmBgfGB4ceBgYGBgAGBoAWAwAMAwAMAf/4AP/wAAAAAAAAAAAAAAAAA==") }},
show : function() {
Bangle.setGPSPower(1, "clkinfogpsspeed");
Bangle.on("GPS", gpsHandler);
},
hide : function() {
Bangle.removeListener("GPS", gpsHandler);
Bangle.setGPSPower(0, "clkinfogpsspeed");
}
// run : function() {} optional (called when tapped)
}
]
};
return ci;
}) // must not have a semi-colon!

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,13 @@
{ "id": "clkinfogpsspeed",
"name": "GPS Speed Clockinfo",
"shortName":"GPS Speed",
"version":"0.01",
"description": "A Clockinfo that displays your current speed according to the GPS",
"icon": "icon.png",
"type": "clkinfo",
"tags": "clkinfo",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfogpsspeed.clkinfo.js","url":"clkinfo.js"}
]
}

View File

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

BIN
apps/clkinfosec/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

View File

@ -0,0 +1,33 @@
(function() {
return {
name: "Bangle",
items: [
{ name : "Seconds",
get : () => {
let d = new Date(), s = d.getSeconds(), sr = s*Math.PI/30,
x = 11+9*Math.sin(sr), y = 11-9*Math.cos(sr),
g = Graphics.createArrayBuffer(24,24,1,{msb:true});
g.transparent = 0;
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);
return {
text : s.toString().padStart(2,0),
img : g.asImage("string")
};
},
show : function() {
this.interval = setTimeout(()=>{
this.emit("redraw");
this.interval = setInterval(()=>{
this.emit("redraw");
}, 1000);
}, 1000 - (Date.now() % 1000));
},
hide : function() {
clearInterval(this.interval);
this.interval = undefined;
}
}
]
};
})

View File

@ -0,0 +1,13 @@
{ "id": "clkinfosec",
"name": "Seconds Clockinfo",
"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)",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo,seconds,time",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfosec.clkinfo.js","url":"clkinfo.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

3
apps/clockbg/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Moved settings into 'Settings->Apps'
0.03: Add 'Squares' option for random squares background

46
apps/clockbg/README.md Normal file
View File

@ -0,0 +1,46 @@
# Clock Backgrounds
This app provides a library (`clockbg`) that can be used by clocks to
provide different backgrounds for them.
## Usage
By default the app provides just a red/green/blue background but it can easily be configured.
You can either:
* Go to [the Clock Backgrounds app](https://banglejs.com/apps/?id=clockbg) in the App Loader and use pre-made image backgrounds (or upload your own)
* Go to the `Backgrounds` app on the Bangle itself, and choose between:
* `Solid Color` - one color that never changes
* `Random Color` - a new color every time the clock starts
* `Image` - choose from a previously uploaded image
* `Squares` - a randomly generated pattern of squares in the selected color palette
## Usage in code
Just use the following to use this library within your code:
```JS
// once at the start
let background = require("clockbg");
// to fill the whole area
background.fillRect(Bangle.appRect);
// to fill just one part of the screen
background.fillRect(x1, y1, x2, y2);
```
You should also add `"dependencies" : { "clockbg":"module" },` to your app's metadata to
ensure that the clock background library is automatically loaded.
## Features to be added
A few features could be added that would really improve functionality:
* When 'fast loading', 'random' backgrounds don't update at the moment
* Support for >1 image to be uploaded (requires some image management in `interface.html`), and choose randomly between them
* Support for gradients (random colors)
* More types of auto-generated pattern (as long as they can be generated quickly or in the background)
* Storing 'clear' areas of uploaded images so clocks can easily position themselves

BIN
apps/clockbg/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,19 @@
Clock Images
=============
If you want to add your own images ensure they're in the same style and then also list the image file in custom.html in the root directory.
## Flags
The flags come from https://icons8.com/icon/set/flags/color and are 480x480px
If your flag is listed in https://icons8.com/icon/set/flags/color and you can't download it in the right size, please file an issue and we'll download it with our account.
## Other backgrounds
Backgrounds prefixed `ai_` are generated by the AI [Bing Image Creator](https://www.bing.com/images/create)

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

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