Merge branch 'espruino:master' into master
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: Initial release.
|
|
@ -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.
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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
|
||||
}
|
||||
|
||||
})();
|
|
@ -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))})();
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"enabled": false,
|
||||
"interval": 10000
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4X//8HA4IEBgH4C5cFqgJHitQjWpBY9q0gLvI5ar/AAkgBRMC1ALJlX6CxOrBZMq34LJ1f/9QKHhW//2gCxP6wAWHy/+KREqq4WIgGtr+qLhG1vw5IgX1KBALBywWIIwNaHJEAlNqUZOltAuJyouKqwuKrQuhywuJNIIuJlIuJHQLGIBYQ6IgtU1Q6GitQjWplQVGtWkBYIhHBcpHBBY5HBM5IABA"))
|
|
@ -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);*/
|
||||
}
|
After Width: | Height: | Size: 12 KiB |
|
@ -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}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
|
@ -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);
|
||||
});
|
|
@ -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"}
|
||||
]
|
||||
}]
|
||||
}
|
|
@ -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"}
|
||||
]
|
||||
}]
|
||||
}
|
|
@ -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();
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
*/
|
|
@ -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;
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
})
|
||||
|
|
@ -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();
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial release.
|
|
@ -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)
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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();
|
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
.prettierignore
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4f/gUA///j32o8h9v6glA+P9+/3tu27YCLvICBgEGCJlFmwRBgALFxQRIgIdF28JmIIEknG7cMyVJk4nBC4PJk4dBC4OJkmSrYRCkuACQM26/88wRGgQHB2iGCm33//8GoXtonbraYGgwRB/+bNY4AEg9/CIPbth3BxYRJn4RB/YRBgEUTwIRGne275IBCIQABjYRGrpCB+VK1gJDgYQFgXN23YQwIjEAA0WMwPQ0mSqgRK2QRBy6cBCJUFGIO12wjBpgRMlsAqmSqTOCAA0sCINogEIyVKCJdLoEAhQRNpQFCyVII5IRGqARQNZUECIcGyRLBPpPSCIQWBsCzK3VJoEB0mTCBUAitplEA0WYCJb7B1EBCYIAsjJjCknDpMkyUAmlKNwuEyEAgMSwwQBpNhAQM4CAcDkgRBe4ODogOB4MT0MldgcxCIXEyWAi3axNgykECIcBxIRBEwIRBYoK3BykGkw1DxPEyEZksSCIMEpcDjAIBGocbhMQiMzCIUyqALB5KmFhMghMk0VLQANYPoLeBCI0SRgOKJYOAgOSpihFCIMaCIOTgMk4ACBqVICIyTBKIMZkkAhpyBo4RHgOk4EZPAIjByVFNYYIBAoU2AoOwAQO5kmMf7QALpMg2VJ23Mn////8OIVThmUCIs27FvCIP+eQOSpUIyXAgFNEYfQCIX8P4OSp0MCIVZgEWFoNgrMktuwgdt23YOIQ"))
|
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -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();
|
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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!
|
||||
|
||||
      
|
||||
|
||||
|
||||
## 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)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA///gElq3X0ELJf4AiitAAYMBqgKEgNVrgEBmtVCAQABgtVr/Agf1qtQEQlpq6QB6tpEgkVywLDywLEq2uyoLB6wEBBZAECBYda32lBYIECBZ9W3wjDAgILPquWqoACAgILEtILDAgKOEAAyQCRwIAGSAUVBY6ECBZYGD7WnAoYLF9WrBYupAoWq1QECtQLBtWdBYt21QLC1LfBBYVfA4ILBlWq1f9rWVv/q1WoBYMKCgOvTYP6AoOgBYMCAoIAFwCQCBY6nDGAIAEFwQkIEQZVCBQZRCAAcGBYeQBYoYDCwwYECw5KC0gKIAH4APA="))
|
|
@ -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();
|
||||
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
0.01: New Clock Info!
|
|
@ -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!
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Remove 's' after seconds (on some clocks this looks like '5')
|
After Width: | Height: | Size: 901 B |
|
@ -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;
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
|
@ -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"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Moved settings into 'Settings->Apps'
|
||||
0.03: Add 'Squares' option for random squares background
|
|
@ -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
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 470 B |
After Width: | Height: | Size: 474 B |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 664 B |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 664 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 684 B |
After Width: | Height: | Size: 664 B |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 651 B |