diff --git a/.eslintignore b/.eslintignore index f28e67b54..a82960313 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,11 +1,8 @@ -apps/animclk/V29.LBM.js -apps/banglerun/rollup.config.js -apps/schoolCalendar/fullcalendar/main.js -apps/authentiwatch/qr_packed.js -apps/qrcode/qr-scanner.umd.min.js -apps/gipy/pkg/gps.js -apps/health/chart.min.js -*.test.js -# typescript/generated files -apps/btadv/*.js +# Needs to be ignored because it uses ESM export/import +apps/gipy/pkg/gps.js +apps/gipy/pkg/gps.d.ts +apps/gipy/pkg/gps_bg.wasm.d.ts + +# Needs to be ignored because it includes broken JS +apps/health/chart.min.js diff --git a/modules/.eslintrc.json b/.eslintrc.js similarity index 58% rename from modules/.eslintrc.json rename to .eslintrc.js index d656c2555..b7590a77e 100644 --- a/modules/.eslintrc.json +++ b/.eslintrc.js @@ -1,7 +1,32 @@ -{ +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": { @@ -23,10 +48,8 @@ "Flash": "readonly", "Float32Array": "readonly", "Float64Array": "readonly", - "fs": "readonly", "Function": "readonly", "Graphics": "readonly", - "heatshrink": "readonly", "I2C": "readonly", "Int16Array": "readonly", "Int32Array": "readonly", @@ -46,11 +69,9 @@ "RegExp": "readonly", "Serial": "readonly", "SPI": "readonly", - "Storage": "readonly", "StorageFile": "readonly", "String": "readonly", "SyntaxError": "readonly", - "tensorflow": "readonly", "TFMicroInterpreter": "readonly", "TypeError": "readonly", "Uint16Array": "readonly", @@ -58,8 +79,10 @@ "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", @@ -89,6 +112,7 @@ "getSerial": "readonly", "getTime": "readonly", "global": "readonly", + "globalThis": "readonly", "HIGH": "readonly", "I2C1": "readonly", "Infinity": "readonly", @@ -129,7 +153,43 @@ "VIBRATE": "readonly", // Aliases and not defined at https://banglejs.com/reference "g": "readonly", - "WIDGETS": "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 @@ -142,22 +202,49 @@ "SwitchCase": 1 } ], - "no-case-declarations": "off", "no-constant-condition": "off", "no-delete-var": "off", - "no-empty": "off", + "no-empty": ["warn", { "allowEmptyCatch": true }], "no-global-assign": "off", "no-inner-declarations": "off", - "no-octal": "off", "no-prototype-builtins": "off", "no-redeclare": "off", "no-unreachable": "warn", "no-cond-assign": "warn", "no-useless-catch": "warn", - // TODO: "no-undef": "warn", - "no-undef": "off", - "no-unused-vars": "off", + "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, } diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 7c0cfca3a..bebe18748 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -11,10 +11,10 @@ jobs: uses: actions/checkout@v3 with: submodules: recursive - - name: Use Node.js 16.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x - name: Install testing dependencies run: npm ci - name: Test all apps and widgets diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..69aa0ab3d --- /dev/null +++ b/CONTRIBUTING.md @@ -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 + diff --git a/README.md b/README.md index 066ad0ad0..d595c7df1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Bangle.js App Loader (and Apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/) +The release version is manually refreshed with regular intervals while the development version is continuously updated as new code is committed to this repository. + **All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By submitting code to this repository you confirm that you are happy with it being MIT licensed, and that it is not licensed in another way that would make this impossible. @@ -251,7 +253,7 @@ and which gives information about the app for the Launcher. "description": "...", // long description (can contain markdown) "icon": "icon.png", // icon in apps/ "screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app - "type":"...", // optional(if app) - + "type":"...", // optional(if app) - // 'app' - an application // 'clock' - a clock - required for clocks to automatically start // 'widget' - a widget @@ -278,6 +280,7 @@ and which gives information about the app for the Launcher. // 'bluetooth' - uses Bluetooth LE // 'system' - used by the system // 'clkinfo' - provides or uses clock_info module for data on your clock face or clocks that support it (see apps/clock_info/README.md) + // 'health' - e.g. heart rate monitors or step counting "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above) "dependencies" : { "messages":"app" } // optional, depend on a specific app ID @@ -299,7 +302,7 @@ and which gives information about the app for the Launcher. "customConnect": true, // if supplied, ensure we are connected to a device // before the "custom.html" iframe is loaded. An // onInit function in "custom.html" is then called - // with info on the currently connected device. + // with info on the currently connected device. "interface": "interface.html", // if supplied, apps/interface.html is loaded in an // iframe, and it may interact with the connected Bangle @@ -327,9 +330,9 @@ and which gives information about the app for the Launcher. {"name":"appid.data.json", // filename used in storage "storageFile":true // if supplied, file is treated as storageFile "url":"", // if supplied URL of file to load (currently relative to apps/) - "content":"...", // if supplied, this content is loaded directly + "content":"...", // if supplied, this content is loaded directly "evaluate":true, // if supplied, data isn't quoted into a String before upload - // (eg it's evaluated as JS) + // (eg it's evaluated as JS) }, {"wildcard":"appid.data.*" // wildcard of filenames used in storage }, // this is mutually exclusive with using "name" @@ -402,7 +405,7 @@ in an iframe. - +
Loading...
+ + + + diff --git a/apps/avwx/metadata.json b/apps/avwx/metadata.json new file mode 100644 index 000000000..0b07f32d4 --- /dev/null +++ b/apps/avwx/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "avwx", + "name": "AVWX Module", + "shortName":"AVWX", + "version":"1.00", + "description": "Module/library for the AVWX API", + "icon": "avwx.png", + "type": "module", + "tags": "outdoors", + "supports": ["BANGLEJS2"], + "provides_modules": ["avwx"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + { "name":"avwx", "url":"avwx.js" } + ], + "data": [{ "name":"avwx.json" }] +} diff --git a/apps/awairmonitor/ChangeLog b/apps/awairmonitor/ChangeLog index 71d6399c4..17417e79b 100644 --- a/apps/awairmonitor/ChangeLog +++ b/apps/awairmonitor/ChangeLog @@ -1,3 +1,4 @@ 0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11) 0.02: The app is now a clock, the data is greyed after the connection is lost (2021/12/22) 0.03: Set the Awair's IP directly on the webpage (2021/12/27) +0.04: Minor code improvements diff --git a/apps/awairmonitor/app.js b/apps/awairmonitor/app.js index 9123a9c2c..ef2f33a9e 100644 --- a/apps/awairmonitor/app.js +++ b/apps/awairmonitor/app.js @@ -80,7 +80,7 @@ function draw() { g.drawString(""+bt_current_humi, 123, 110); g.drawString(""+bt_current_temp, 158, 110); - for (i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { if (display_frozen) { g.setColor("#888"); } // max height = 32 diff --git a/apps/awairmonitor/metadata.json b/apps/awairmonitor/metadata.json index a58175b1b..dd59b4965 100644 --- a/apps/awairmonitor/metadata.json +++ b/apps/awairmonitor/metadata.json @@ -4,7 +4,7 @@ "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "allow_emulator": true, - "version":"0.03", + "version": "0.04", "description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.", "type": "clock", "tags": "clock,tool,health", diff --git a/apps/backswipe/ChangeLog b/apps/backswipe/ChangeLog index 1e5479d6e..4e81269fe 100644 --- a/apps/backswipe/ChangeLog +++ b/apps/backswipe/ChangeLog @@ -1,2 +1,6 @@ 0.01: New App! 0.02: Don't fire if the app uses swipes already. +0.03: Only count defined handlers in the handler array. +0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs + a refresh by deselecting and reselecting the "Messages" app throught Back Swipe + settings. diff --git a/apps/backswipe/boot.js b/apps/backswipe/boot.js index e46f902eb..8ff277634 100644 --- a/apps/backswipe/boot.js +++ b/apps/backswipe/boot.js @@ -26,7 +26,7 @@ if (Bangle["#on"+eventType] === undefined) { return 0; } else if (Bangle["#on"+eventType] instanceof Array) { - return Bangle["#on"+eventType].length; + return Bangle["#on"+eventType].filter(x=>x).length; } else if (Bangle["#on"+eventType] !== undefined) { return 1; } @@ -47,9 +47,9 @@ function enabledForApp(app) { if (!settings) return true; if (settings.mode === 0) { - return !(settings.apps.filter((a) => a.src === app).length > 0); + return !(settings.apps.filter((a) => (a.src===app)||(a.files&&a.files.includes(app))).length > 0); // The `a.src===app` and `a.files&&...` checks are for backwards compatibility. Otherwise only `a.files.includes(app)` is needed. } else if (settings.mode === 1) { - return settings.apps.filter((a) => a.src === app).length > 0; + return settings.apps.filter((a) => (a.src===app)||(a.files&&a.files.includes(app))).length > 0; } else { return settings.mode === 2 ? true : false; } diff --git a/apps/backswipe/metadata.json b/apps/backswipe/metadata.json index c7cb2185f..4324286b5 100644 --- a/apps/backswipe/metadata.json +++ b/apps/backswipe/metadata.json @@ -1,7 +1,7 @@ { "id": "backswipe", "name": "Back Swipe", "shortName":"BackSwipe", - "version":"0.02", + "version":"0.04", "description": "Service that allows you to use an app's back button using left to right swipe gesture", "icon": "app.png", "tags": "back,gesture,swipe", diff --git a/apps/backswipe/settings.js b/apps/backswipe/settings.js index 42ca7ae7d..c98f706eb 100644 --- a/apps/backswipe/settings.js +++ b/apps/backswipe/settings.js @@ -26,7 +26,8 @@ return appInfo && { 'name': appInfo.name, 'sortorder': appInfo.sortorder, - 'src': appInfo.src + 'src': appInfo.src, + 'files': appInfo.files }; }).filter(app => app && !!app.src); apps.sort((a, b) => { diff --git a/apps/bad/ChangeLog b/apps/bad/ChangeLog new file mode 100644 index 000000000..263d4078d --- /dev/null +++ b/apps/bad/ChangeLog @@ -0,0 +1 @@ +0.01: attempt to import diff --git a/apps/bad/README.md b/apps/bad/README.md new file mode 100644 index 000000000..156b34cbf --- /dev/null +++ b/apps/bad/README.md @@ -0,0 +1,8 @@ +# Bad Apple demo ![](app.png) + +"Bad Apple" is like "Hello World" for graphics system. So this is it +for Bangle.js2. Watch have no speaker, so vibration motor is used, +instead, to produce (pretty quiet) sound. + +Tools for preparing bad.araw and bad.vraw are in prep/ directory. Full +3 minute demo should actually fit to the watch. \ No newline at end of file diff --git a/apps/bad/app-icon.js b/apps/bad/app-icon.js new file mode 100644 index 000000000..e153d6397 --- /dev/null +++ b/apps/bad/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIifiAFWj//Aod///gAgMH///+AFBn4FB/AQDAoYEB//8gEBAokDAoX+ApguCAAIFqGoYFNLIZHBApZxFAoyDCAoqJCSoqsBUIcPAoKtF4AFBJAS/DHIQAeA=")) diff --git a/apps/bad/app.png b/apps/bad/app.png new file mode 100644 index 000000000..e3e3dad82 Binary files /dev/null and b/apps/bad/app.png differ diff --git a/apps/bad/bad.app.js b/apps/bad/bad.app.js new file mode 100644 index 000000000..453f0003c --- /dev/null +++ b/apps/bad/bad.app.js @@ -0,0 +1,65 @@ +/* sox Rear_Right.wav -r 4k -b 8 -c 1 -e unsigned-integer 0.raw vol 2 + aplay -r 4000 /tmp/0.raw +*/ + +/* https://forum.espruino.com/conversations/348912/ */ + +let pin = D19; + +function play(name, callback) { + function playChar(offs) { + var l = 10240; + var s = require("Storage").read(name, offs, l); + //print("Waveform " + name + " " + s.length); + if (!s.length) { + digitalWrite(pin,0); + if (callback) callback(); + return; + } + var w = new Waveform(s.length); + var b = w.buffer; + b.set(s); + //print("Buffer", s.length); + //for (var i=s.length-1;i>=0;i--)b[i]/=4; + w.startOutput(pin, 4000); + w.on("finish", function(buf) { + playChar(offs+l); + }); + } + analogWrite(pin, 0.1, {freq:40000}); + playChar(0); +} + +function video(name, callback) { + function frame() { + var s = require("Storage").read(name, offs, l); + if (!s) + return; + g.drawImage(s, 0, 0, { scale: 2 }); + g.flip(); + offs += l; + } + g.clear(); + var offs = 0; + //var l = 3875; for 176x176 + //var l = 515; for 64x64 + var l = 971; + setInterval(frame, 200); +} + +function run() { + clearInterval(i); + print("Running"); + play('bad.araw'); + t1 = getTime(); + video('bad.vraw'); + print("100 frames in ", getTime()-t1); + // 1.7s, unscaled + // 2.68s, scale 1.01 + // 5.73s, scale 2.00 + // 9.93s, scale 2, full screen + // 14.4s scaled. 176/64 +} + +print("Loaded"); +i = setInterval(run, 100); \ No newline at end of file diff --git a/apps/bad/bad.araw b/apps/bad/bad.araw new file mode 100644 index 000000000..cfe5f4fdd Binary files /dev/null and b/apps/bad/bad.araw differ diff --git a/apps/bad/bad.vraw b/apps/bad/bad.vraw new file mode 100644 index 000000000..ff8fec1b0 Binary files /dev/null and b/apps/bad/bad.vraw differ diff --git a/apps/bad/metadata.json b/apps/bad/metadata.json new file mode 100644 index 000000000..882b7f066 --- /dev/null +++ b/apps/bad/metadata.json @@ -0,0 +1,16 @@ +{ "id": "bad", + "name": "Bad Apple", + "version":"0.01", + "description": "Bad Apple demo", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "allow_emulator": false, + "tags": "game", + "storage": [ + {"name":"bad.app.js","url":"bad.app.js"}, + {"name":"bad.vraw","url":"bad.vraw"}, + {"name":"bad.araw","url":"bad.araw"}, + {"name":"bad.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bad/prep/img_convert.py b/apps/bad/prep/img_convert.py new file mode 100755 index 000000000..2edbbdef5 --- /dev/null +++ b/apps/bad/prep/img_convert.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +from PIL import Image +import os + +def convert_image(input_path, output_width, output_height): + img = Image.open(input_path) + img_resized = img.resize((output_width, output_height), Image.ANTIALIAS) + img_gray = img_resized.convert('L') + img_1bpp = img_gray.point(lambda x: 0 if x < 128 else 255, '1') + return img_1bpp + +def convert_and_append_header(input_directory, size): + input_files = [f for f in os.listdir(input_directory) if f.startswith("image_") and f.endswith(".png")] + input_files.sort() + header_bytes = size.to_bytes(1, byteorder='big') + size.to_bytes(1, byteorder='big') + b'\x01' + + for i, input_file in enumerate(input_files): + input_path = os.path.join(input_directory, input_file) + img_1bpp = convert_image(input_path, size, size) + output_file = input_path + ".raw" + + with open(output_file, 'wb') as raw_file: + raw_file.write(header_bytes) + raw_file.write(img_1bpp.tobytes()) + +if __name__ == "__main__": + input_directory = "." # Replace with the path to your image directory + output_width = 88 + output_file_path = "output_with_header.raw" # Replace with the desired output file path + + convert_and_append_header(input_directory, output_width) diff --git a/apps/bad/prep/run b/apps/bad/prep/run new file mode 100755 index 000000000..907e711f3 --- /dev/null +++ b/apps/bad/prep/run @@ -0,0 +1,20 @@ +#!/bin/bash + +# aplay -r 4000 /tmp/0.raw +#bug: Terminal exists on b.js, it is dumb terminal, not vt100. + +rm image_*.png image_*.png.raw output.wav ../bad.araw ../bad.vraw + +I=bad.mp4 +S=1:18 +E=1:50 + +ffmpeg -i $I -ss $S -to $E -vn -acodec pcm_u8 -ar 4000 -ac 1 -y output.wav +./wav_divider.py +mv output.raw ../bad.araw + +ffmpeg -i $I -ss $S -to $E -r 5 -vf fps=5 image_%04d.png +./img_convert.py +cat *.png.raw > ../bad.vraw + +ls -al ../bad.* diff --git a/apps/bad/prep/wav_divider.py b/apps/bad/prep/wav_divider.py new file mode 100755 index 000000000..9ce08e28b --- /dev/null +++ b/apps/bad/prep/wav_divider.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +def divide_bytes(input_file, output_file): + with open(input_file, 'rb') as infile: + with open(output_file, 'wb') as outfile: + byte = infile.read(1) + while byte: + # Convert byte to integer, divide by 4, and write back as byte + new_byte = bytes([int.from_bytes(byte, byteorder='big') // 3]) + outfile.write(new_byte) + byte = infile.read(1) + +divide_bytes("output.wav", "output.raw") diff --git a/apps/ballmaze/ChangeLog b/apps/ballmaze/ChangeLog index de6240f46..fedae2fc5 100644 --- a/apps/ballmaze/ChangeLog +++ b/apps/ballmaze/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Set LCD timeout for Espruino 2v10 compatibility \ No newline at end of file +0.02: Set LCD timeout for Espruino 2v10 compatibility +0.03: Minor code improvements diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js index 2d55887f0..d54cf4372 100644 --- a/apps/ballmaze/app.js +++ b/apps/ballmaze/app.js @@ -25,6 +25,7 @@ 10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial", }; // even size 1 actually works, but larger mazes take forever to generate + // TODO: should `sizes`, `minSize` and `defaultSize` have been declared outside the if block? if (!BANGLEJS2) { const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), minSize = 4, defaultSize = 10; } else { diff --git a/apps/ballmaze/metadata.json b/apps/ballmaze/metadata.json index 3223789d4..fc011623a 100644 --- a/apps/ballmaze/metadata.json +++ b/apps/ballmaze/metadata.json @@ -1,7 +1,7 @@ { "id": "ballmaze", "name": "Ball Maze", - "version": "0.02", + "version": "0.03", "description": "Navigate a ball through a maze by tilting your watch.", "icon": "icon.png", "type": "app", diff --git a/apps/balltastic/ChangeLog b/apps/balltastic/ChangeLog index 6ed48b5df..c3411c031 100644 --- a/apps/balltastic/ChangeLog +++ b/apps/balltastic/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial version of Balltastic released! Happy! 0.02: Set LCD timeout for Espruino 2v10 compatibility 0.03: Now also works on Bangle.js 2 +0.04: Minor code improvements diff --git a/apps/balltastic/app.js b/apps/balltastic/app.js index d0262c3cb..d9241591e 100644 --- a/apps/balltastic/app.js +++ b/apps/balltastic/app.js @@ -1,4 +1,4 @@ -BANGLEJS2 = process.env.HWVERSION==2; +const BANGLEJS2 = process.env.HWVERSION==2; Bangle.setLCDBrightness(1); if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered"); Bangle.setLCDTimeout(0); diff --git a/apps/balltastic/metadata.json b/apps/balltastic/metadata.json index 09e265829..19ec3f901 100644 --- a/apps/balltastic/metadata.json +++ b/apps/balltastic/metadata.json @@ -1,7 +1,7 @@ { "id": "balltastic", "name": "Balltastic", - "version": "0.03", + "version": "0.04", "description": "Simple but fun ball eats dots game.", "icon": "app.png", "screenshots": [{"url":"bangle2-balltastic-screenshot.png"}], diff --git a/apps/banglebridge/ChangeLog b/apps/banglebridge/ChangeLog new file mode 100644 index 000000000..62542be60 --- /dev/null +++ b/apps/banglebridge/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Minor code improvements diff --git a/apps/banglebridge/metadata.json b/apps/banglebridge/metadata.json index 8a9eaa6e4..86e1face0 100644 --- a/apps/banglebridge/metadata.json +++ b/apps/banglebridge/metadata.json @@ -2,7 +2,7 @@ "id": "banglebridge", "name": "BangleBridge", "shortName": "BangleBridge", - "version": "0.01", + "version": "0.02", "description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App", "icon": "widget.png", "type": "widget", diff --git a/apps/banglebridge/widget.js b/apps/banglebridge/widget.js index 48078de30..692822b39 100644 --- a/apps/banglebridge/widget.js +++ b/apps/banglebridge/widget.js @@ -11,13 +11,13 @@ g.setFont("Vector", 100); //variabangle.Sensorss - let acclS, bttS, compssS, gpsS, hrmS, stepS; //Strings + //let acclS, bttS, compssS, gpsS, hrmS, stepS; //Strings let accelN, compssN, gpsN, hrmN, stepN; //Num - let prueba = 1; + //let prueba = 1; let data = [0, 0, 0, 0, 0, 0]; //Constants for redabangle.Sensors code let storage = require('Storage'); - let deCom = require('heatshrink'); + //let deCom = require('heatshrink'); @@ -129,10 +129,10 @@ finalS = s; } var z = d.getMilliseconds(); - var zFinal = new String(z); - zFinal = zFinal.replace('.', ''); + //var zFinal = new String(z); + //zFinal = zFinal.replace('.', ''); var completeTime = year + "-" + finalMonth + "-" + finalDay + "T" + finalh + ":" + finalM + ":" + finalS + "." + z + "Z"; - var time = h + ":" + ("0" + m).substr(-2); + //var time = h + ":" + ("0" + m).substr(-2); gpsN.time = completeTime; data[5] = gpsN; }, 2 * 1000); diff --git a/apps/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog index 6cf589541..06c4adf1b 100644 --- a/apps/banglexercise/ChangeLog +++ b/apps/banglexercise/ChangeLog @@ -3,3 +3,5 @@ Add more feedback to the user about the exercises Clean up code 0.03: Add software back button on main menu +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js index 9659ee81f..f4addc05a 100644 --- a/apps/banglexercise/app.js +++ b/apps/banglexercise/app.js @@ -11,7 +11,7 @@ let historySlopeY = []; let historySlopeZ = []; let lastZeroPassCameFromPositive; -let lastZeroPassTime = 0; +//let lastZeroPassTime = 0; let lastExerciseCompletionTime = 0; let lastExerciseHalfCompletionTime = 0; @@ -148,7 +148,7 @@ function accelHandler(accel) { // slope for Z if (exerciseType.useZaxis) { - l = historyAvgZ.length; + let l = historyAvgZ.length; if (l > 1) { const p1 = historyAvgZ[l - 2]; const p2 = historyAvgZ[l - 1]; @@ -198,7 +198,7 @@ function isValidExercise(slope, t) { } lastZeroPassCameFromPositive = true; - lastZeroPassTime = t; + //lastZeroPassTime = t; } if (p2 > 0 && p1 < 0) { if (lastZeroPassCameFromPositive == true) { @@ -256,7 +256,7 @@ function isValidExercise(slope, t) { } lastZeroPassCameFromPositive = false; - lastZeroPassTime = t; + //lastZeroPassTime = t; } } } @@ -272,7 +272,7 @@ function reset() { historySlopeZ = []; lastZeroPassCameFromPositive = undefined; - lastZeroPassTime = 0; + //lastZeroPassTime = 0; lastExerciseHalfCompletionTime = 0; lastExerciseCompletionTime = 0; exerciseCounter = 0; diff --git a/apps/banglexercise/metadata.json b/apps/banglexercise/metadata.json index f4ce1894b..b2f8e39ea 100644 --- a/apps/banglexercise/metadata.json +++ b/apps/banglexercise/metadata.json @@ -1,7 +1,7 @@ { "id": "banglexercise", "name": "BanglExercise", "shortName":"BanglExercise", - "version":"0.03", + "version": "0.05", "description": "Can automatically track exercises while wearing the Bangle.js watch.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/banglexercise/settings.js b/apps/banglexercise/settings.js index 3208c6eca..0b52acd72 100644 --- a/apps/banglexercise/settings.js +++ b/apps/banglexercise/settings.js @@ -11,7 +11,6 @@ '< Back': back, 'Buzz': { value: "buzz" in settings ? settings.buzz : false, - format: () => (settings.buzz ? 'Yes' : 'No'), onchange: () => { settings.buzz = !settings.buzz; save('buzz', settings.buzz); diff --git a/apps/barcode/ChangeLog b/apps/barcode/ChangeLog index 7ab5d8587..4974b7029 100644 --- a/apps/barcode/ChangeLog +++ b/apps/barcode/ChangeLog @@ -8,3 +8,4 @@ 0.08: Step count stored in memory to survive reloads. Now shows step count daily and since last reboot. 0.09: NOW it really should reset daily (instead of every other day...) 0.10: Tell clock widgets to hide. +0.11: Minor code improvements diff --git a/apps/barcode/barcode.app.js b/apps/barcode/barcode.app.js index 0d9df78d5..3242164ae 100644 --- a/apps/barcode/barcode.app.js +++ b/apps/barcode/barcode.app.js @@ -8,8 +8,8 @@ let digitBarHeight = 100; let textBarWidth = 56; let textBarHeight = 20; -let textWidth = 14; -let textHeight = 20; +//let textWidth = 14; +//let textHeight = 20; /* Offsets */ var startOffsetX = 17; @@ -39,7 +39,7 @@ let rightBarsStartY = upperTextBarRightOffsetY + textBarHeight; /* Utilities */ let stepCount = require("Storage").readJSON("stepCount",1); if(stepCount === undefined) stepCount = 0; -let intCaster = num => Number(num); +//let intCaster = num => Number(num); var drawTimeout; @@ -50,7 +50,7 @@ function renderWatch(l) { var d = new Date(); var h = d.getHours(), m = d.getMinutes(); - var time = h + ":" + ("0"+m).substr(-2); + //var time = h + ":" + ("0"+m).substr(-2); //var month = ("0" + (d.getMonth()+1)).slice(-2); var dayOfMonth = ('0' + d.getDate()).slice(-2); var dayOfWeek = d.getDay() || 7; diff --git a/apps/barcode/metadata.json b/apps/barcode/metadata.json index 3f6bf06e6..007be2778 100644 --- a/apps/barcode/metadata.json +++ b/apps/barcode/metadata.json @@ -2,7 +2,7 @@ "name": "Barcode clock", "shortName":"Barcode clock", "icon": "barcode.icon.png", - "version":"0.10", + "version": "0.11", "description": "EAN-8 compatible barcode clock.", "tags": "barcode,ean,ean-8,watchface,clock,clockface", "type": "clock", diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 394622c78..21ee5c3e7 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -10,3 +10,5 @@ 0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381) 0.11: Initial port to the BangleJS2 0.12: Remove debug log +0.13: Minor code improvements +0.14: Minor code improvements diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 8d700d0a1..b93f004d4 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -101,6 +101,7 @@ function loadLinesFromFile(requestedLineCount, fileName) { var readFile = Storage.open(fileName, "r"); + let nextLine; while ((nextLine = readFile.readLine())) { if(nextLine) { allLines.push(nextLine); @@ -146,7 +147,7 @@ function renderData(dataArray) { const belowMinIndicatorValue = minTemperature - 1; const aboveMaxIndicatorValue = maxTemparature + 1; - var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; + //var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; for (let i = 0; i < dataArray.length; i++) { const element = dataArray[i]; diff --git a/apps/batchart/metadata.json b/apps/batchart/metadata.json index be01ca767..7be1637f0 100644 --- a/apps/batchart/metadata.json +++ b/apps/batchart/metadata.json @@ -2,7 +2,7 @@ "id": "batchart", "name": "Battery Chart", "shortName": "Battery Chart", - "version": "0.12", + "version": "0.14", "description": "A widget and an app for recording and visualizing battery percentage over time.", "icon": "app.png", "tags": "app,widget,battery,time,record,chart,tool", diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index b1234f377..40198065b 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -11,10 +11,10 @@ hrm: 16 }; - var batChartFile; // file for battery percentage recording + //var batChartFile; // file for battery percentage recording const recordingInterval10Min = 60 * 10 * 1000; - const recordingInterval1Min = 60 * 1000; //For testing - const recordingInterval10S = 10 * 1000; //For testing + //const recordingInterval1Min = 60 * 1000; //For testing + //const recordingInterval10S = 10 * 1000; //For testing var compassEventReceived = false; var gpsEventReceived = false; diff --git a/apps/battleship/ChangeLog b/apps/battleship/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/battleship/ChangeLog +++ b/apps/battleship/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/battleship/battleship.js b/apps/battleship/battleship.js index 3661ef494..237eca3d2 100644 --- a/apps/battleship/battleship.js +++ b/apps/battleship/battleship.js @@ -1,6 +1,6 @@ const FIELD_WIDTH = [11, 11, 15]; // for each phase const FIELD_HEIGHT = FIELD_WIDTH; -const FIELD_LINE_WIDTH = 2; +//const FIELD_LINE_WIDTH = 2; const FIELD_MARGIN = 2; const FIELD_COUNT_X = 10; const FIELD_COUNT_Y = FIELD_COUNT_X; diff --git a/apps/battleship/metadata.json b/apps/battleship/metadata.json index 12e92c1d7..46399c68f 100644 --- a/apps/battleship/metadata.json +++ b/apps/battleship/metadata.json @@ -1,7 +1,7 @@ { "id": "battleship", "name": "Battleship", - "version": "0.01", + "version": "0.02", "description": "The classic game of battleship", "icon": "battleship-icon.png", "tags": "game", diff --git a/apps/beebclock/ChangeLog b/apps/beebclock/ChangeLog index 84ec7c1d7..4646b4010 100644 --- a/apps/beebclock/ChangeLog +++ b/apps/beebclock/ChangeLog @@ -4,3 +4,4 @@ 0.04: Update to use Bangle.setUI instead of setWatch 0.05: Avoid 'loadWidgets' at LCD on, which will cause memory leak Avoid clearTimeout() usage, as it may break other widgets +0.06: Minor code improvements diff --git a/apps/beebclock/beebclock.js b/apps/beebclock/beebclock.js index c85f68c55..d220096d2 100644 --- a/apps/beebclock/beebclock.js +++ b/apps/beebclock/beebclock.js @@ -108,10 +108,10 @@ for (let h=1; h<=12; h++) { // so we buffer once and minute, and draw the second hand dynamically // (with a bit of flicker) const drawFace = (G) => { - const fw = R1 * 2; - const fh = R1 * 2; - const fw2 = R1; - const fh2 = R1; + //const fw = R1 * 2; + //const fh = R1 * 2; + //const fw2 = R1; + //const fh2 = R1; let hs = []; // Wipe the image and start with white @@ -182,7 +182,7 @@ const drawAll = (force) => { if (!faceImg) force = true; let face_changed = force; - let date_changed = false; + //let date_changed = false; tmp = hours; hours = now.getHours(); @@ -214,7 +214,7 @@ const drawAll = (force) => { tmp = date; date = now.getDate(); if (tmp !== date) { - date_changed = true; + //date_changed = true; face_changed = true; // Should have changed anyway with hour/minute rollover } } diff --git a/apps/beebclock/metadata.json b/apps/beebclock/metadata.json index 31316a80c..5790cf564 100644 --- a/apps/beebclock/metadata.json +++ b/apps/beebclock/metadata.json @@ -1,7 +1,7 @@ { "id": "beebclock", "name": "Beeb Clock", - "version": "0.05", + "version": "0.06", "description": "Clock face that may be coincidentally familiar to BBC viewers", "icon": "beebclock.png", "type": "clock", diff --git a/apps/beeptest/ChangeLog b/apps/beeptest/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/beeptest/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/beeptest/README.md b/apps/beeptest/README.md new file mode 100644 index 000000000..dcb1f1574 --- /dev/null +++ b/apps/beeptest/README.md @@ -0,0 +1,27 @@ +# App Name + +Beep Test + +## Usage + +Mark out a 20m space +Click the side button to start the test +Shuttle run between your markers when the watch buzzes +Push the button when you need to stop + +## Features + +Buzzing on each shuttle run +Results page with vO2max and total distance covered. + +## Controls + +Side button starts, stops and resets the app. + +## Requests + +bb0x88 on giuthub + +## Creator + +Blade diff --git a/apps/beeptest/app-icon.js b/apps/beeptest/app-icon.js new file mode 100644 index 000000000..b29e2b459 --- /dev/null +++ b/apps/beeptest/app-icon.js @@ -0,0 +1,2 @@ +require("heatshrink").decompress(atob("mEw4UA///+f8lky6f8HFmqBRMK1WgBAtUBYUABYtVqtAgEoAIQACioLBqALHBQIABBZMFEgIjHgEBqtUHY4aDKZA+CoBrIBYJJBBZJuCAA3VBYkC1QABGoJhDBYxTBBYUFEQoLDoEVSgIADO4ILCUASdGqtRGIYLFKoY7CIwdUEwJtBBYY6CqADBFwoLDDYIuFIwQUBigLITJQLFHYKNEHAgLGXw6NDBZbKHTIYLLKg6lDBY4KDEY5EIIwahFHQoKIBYIrHIwYLLuALJHRTcHAAjcGAEwA==")) + diff --git a/apps/beeptest/beeptest.js b/apps/beeptest/beeptest.js new file mode 100644 index 000000000..5f6438c24 --- /dev/null +++ b/apps/beeptest/beeptest.js @@ -0,0 +1,274 @@ +var Layout = require("Layout"); + +// Beep Test Data +const BEET_TEST_DATA = [ + { shuttles: 7, timePerShuttle: 9.0, totalTime: 63.0, distancePerLevel: 140 }, + { shuttles: 8, timePerShuttle: 8.0, totalTime: 64.0, distancePerLevel: 160 }, + { shuttles: 8, timePerShuttle: 7.58, totalTime: 60.6, distancePerLevel: 160 }, + { shuttles: 9, timePerShuttle: 7.2, totalTime: 64.8, distancePerLevel: 180 }, + { shuttles: 9, timePerShuttle: 6.86, totalTime: 61.7, distancePerLevel: 180 }, + { + shuttles: 10, + timePerShuttle: 6.55, + totalTime: 65.5, + distancePerLevel: 200, + }, + { + shuttles: 10, + timePerShuttle: 6.26, + totalTime: 62.6, + distancePerLevel: 200, + }, + { shuttles: 11, timePerShuttle: 6.0, totalTime: 66.0, distancePerLevel: 220 }, + { + shuttles: 11, + timePerShuttle: 5.76, + totalTime: 63.4, + distancePerLevel: 220, + }, + { + shuttles: 11, + timePerShuttle: 5.54, + totalTime: 60.9, + distancePerLevel: 220, + }, + { + shuttles: 12, + timePerShuttle: 5.33, + totalTime: 64.0, + distancePerLevel: 240, + }, + { + shuttles: 12, + timePerShuttle: 5.14, + totalTime: 61.7, + distancePerLevel: 240, + }, + { + shuttles: 13, + timePerShuttle: 4.97, + totalTime: 64.6, + distancePerLevel: 260, + }, + { shuttles: 13, timePerShuttle: 4.8, totalTime: 62.4, distancePerLevel: 260 }, + { + shuttles: 13, + timePerShuttle: 4.65, + totalTime: 60.4, + distancePerLevel: 260, + }, + { shuttles: 14, timePerShuttle: 4.5, totalTime: 63.0, distancePerLevel: 280 }, + { + shuttles: 14, + timePerShuttle: 4.36, + totalTime: 61.1, + distancePerLevel: 280, + }, + { + shuttles: 15, + timePerShuttle: 4.24, + totalTime: 63.5, + distancePerLevel: 300, + }, + { + shuttles: 15, + timePerShuttle: 4.11, + totalTime: 61.7, + distancePerLevel: 300, + }, + { shuttles: 16, timePerShuttle: 4.0, totalTime: 64.0, distancePerLevel: 320 }, + { + shuttles: 16, + timePerShuttle: 3.89, + totalTime: 62.3, + distancePerLevel: 320, + }, +]; + +// VO2max Data +const VO2MAX_DATA = [ + { level: 1, vo2max: 16.7 }, + { level: 2, vo2max: 23.0 }, + { level: 3, vo2max: 26.2 }, + { level: 4, vo2max: 29.3 }, + { level: 5, vo2max: 32.5 }, + { level: 6, vo2max: 35.7 }, + { level: 7, vo2max: 38.8 }, + { level: 8, vo2max: 42.0 }, + { level: 9, vo2max: 45.1 }, + { level: 10, vo2max: 48.3 }, + { level: 11, vo2max: 51.5 }, + { level: 12, vo2max: 54.6 }, + { level: 13, vo2max: 57.8 }, + { level: 14, vo2max: 60.9 }, + { level: 15, vo2max: 64.1 }, + { level: 16, vo2max: 67.3 }, + { level: 17, vo2max: 70.4 }, + { level: 18, vo2max: 73.6 }, + { level: 19, vo2max: 76.7 }, + { level: 20, vo2max: 79.9 }, + { level: 21, vo2max: 83.0 }, +]; + +let currentLevel = 0; +let currentShuttle = 0; +let timeRemaining = 0; +let intervalId; +let beepTestLayout; +let testState = "start"; // 'start' | 'running' | 'result' + +function initBeepTestLayout() { + beepTestLayout = new Layout( + { + type: "v", + c: [ + { type: "txt", font: "30%", pad: 0, label: "Start Test", id: "status" }, + { type: "txt", font: "15%", pad: 0, label: "", id: "level" }, + { type: "txt", font: "10%", pad: 0, label: "", id: "vo2max" }, // Smaller font for VO2max + { type: "txt", font: "10%", pad: 0, label: "", id: "distance" }, // Smaller font for Distance + ], + }, + { + btns: [ + { + label: "Start/Stop", + cb: (l) => { + if (testState === "start") { + startTest(); + } else if (testState === "running") { + stopTest(); + } else { + showStartScreen(); + } + }, + }, + ], + }, + ); +} + +function showStartScreen() { + testState = "start"; + g.clear(); + beepTestLayout.clear(beepTestLayout.status); + beepTestLayout.status.label = "Start\nTest"; + beepTestLayout.clear(beepTestLayout.level); + beepTestLayout.level.label = ""; + beepTestLayout.clear(beepTestLayout.vo2max); // Clear VO2max text + beepTestLayout.vo2max.label = ""; + beepTestLayout.clear(beepTestLayout.distance); // Clear Distance text + beepTestLayout.distance.label = ""; + beepTestLayout.render(); +} + +function startTest() { + testState = "running"; + currentLevel = 0; + currentShuttle = 0; + Bangle.buzz(2000); // Buzz for 2 seconds at the start of the test + runLevel(); +} + +function runLevel() { + if (currentLevel >= BEET_TEST_DATA.length) { + stopTest(); + return; + } + + const levelData = BEET_TEST_DATA[currentLevel]; + timeRemaining = levelData.timePerShuttle * 1000; // Convert to milliseconds + updateDisplay(); + + if (intervalId) clearInterval(intervalId); + intervalId = setInterval(() => { + if (timeRemaining <= 0) { + currentShuttle++; + Bangle.buzz(100); // Short buzz after each shuttle + + if (currentShuttle >= levelData.shuttles) { + // Buzz longer or twice at the end of each level + Bangle.buzz(1000); // Buzz for 1 second at level end + setTimeout(() => Bangle.buzz(1000), 500); // Buzz again after 0.5 seconds + currentLevel++; + currentShuttle = 0; + runLevel(); + return; + } + + timeRemaining = levelData.timePerShuttle * 1000; // Reset to original time for the next shuttle + } + + updateDisplay(); + timeRemaining -= 100; // Decrement time by 100 milliseconds + }, 100); // Update every 100 milliseconds +} + +function updateDisplay() { + g.clear(); // Clear the entire screen + beepTestLayout.status.label = formatTime(timeRemaining); + beepTestLayout.level.label = `Level: ${currentLevel + 1}.${currentShuttle + 1}`; + beepTestLayout.render(); +} + +function stopTest() { + g.clear(); // Clear the entire screen + testState = "result"; + clearInterval(intervalId); + + // Determine previous level and shuttle + let prevLevel = currentLevel; + let prevShuttle = currentShuttle; + + if (prevShuttle === 0) { + if (prevLevel > 0) { + prevLevel--; + prevShuttle = BEET_TEST_DATA[prevLevel].shuttles - 1; + } else { + prevShuttle = 0; + } + } else { + prevShuttle--; + } + + // Determine VO2max and total distance + const vo2max = getVO2max(prevLevel + 1); + const totalDistance = calculateTotalDistance(prevLevel + 1); + + beepTestLayout.clear(beepTestLayout.status); + beepTestLayout.status.label = "Result"; + beepTestLayout.clear(beepTestLayout.level); + beepTestLayout.level.label = `Level: ${prevLevel + 1}.${prevShuttle + 1}`; + beepTestLayout.clear(beepTestLayout.vo2max); + beepTestLayout.vo2max.label = `VO2max: ${vo2max}`; + beepTestLayout.clear(beepTestLayout.distance); + beepTestLayout.distance.label = `Distance: ${totalDistance} m`; + beepTestLayout.render(); +} + +function getVO2max(level) { + const result = VO2MAX_DATA.find((item) => item.level === level); + return result ? result.vo2max : "N/A"; +} + +function calculateTotalDistance(level) { + // Calculate the total number of shuttles completed + let totalShuttles = 0; + for (let i = 0; i < level - 1; i++) { + totalShuttles += BEET_TEST_DATA[i].shuttles; + } + const levelData = BEET_TEST_DATA[level - 1]; + totalShuttles += levelData.shuttles; // Add the shuttles completed in the current level + const distancePerShuttle = 20; // Distance per shuttle in meters + return totalShuttles * distancePerShuttle; // Total distance +} + +function formatTime(milliseconds) { + let seconds = Math.floor(milliseconds / 1000); + let tenths = Math.floor((milliseconds % 1000) / 100); // Get tenths of a second + return (seconds < 10 ? "" : "") + seconds + "." + tenths; // Display only the tenths digit +} + +// Initialize the app +Bangle.setLCDPower(1); // Keep the watch LCD lit up +initBeepTestLayout(); +showStartScreen(); diff --git a/apps/beeptest/beeptest.png b/apps/beeptest/beeptest.png new file mode 100644 index 000000000..ee892bbe2 Binary files /dev/null and b/apps/beeptest/beeptest.png differ diff --git a/apps/beeptest/metadata.json b/apps/beeptest/metadata.json new file mode 100644 index 000000000..a9856bc93 --- /dev/null +++ b/apps/beeptest/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "beeptest", + "name": "Beep Test", + "shortName": "Beep Test", + "version": "0.01", + "description": "Aerobic fitness test created by Léger & Lambert", + "icon": "beeptest.png", + "tags": "Health", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "beeptest.app.js", "url": "beeptest.js" }, + { "name": "beeptest.img", "url": "app-icon.js", "evaluate": true } + ] +} diff --git a/apps/berlinc/ChangeLog b/apps/berlinc/ChangeLog index 1a0a9c9cf..6820ab11d 100644 --- a/apps/berlinc/ChangeLog +++ b/apps/berlinc/ChangeLog @@ -5,4 +5,5 @@ Now show widgets Make compatible with themes, and Bangle.js 2 0.06: Enable fastloading -0.07: Adds fullscreen mode setting \ No newline at end of file +0.07: Adds fullscreen mode setting +0.08: Minor code improvements diff --git a/apps/berlinc/berlin-clock.js b/apps/berlinc/berlin-clock.js index 9391d2cc1..94fbe5be1 100644 --- a/apps/berlinc/berlin-clock.js +++ b/apps/berlinc/berlin-clock.js @@ -9,7 +9,6 @@ let fullscreen = !!settings.fullscreen; let show_date = false; let show_time = false; -let yy = 0; let rowlights = []; let time_digit = []; diff --git a/apps/berlinc/metadata.json b/apps/berlinc/metadata.json index 85567868b..983602d90 100644 --- a/apps/berlinc/metadata.json +++ b/apps/berlinc/metadata.json @@ -1,7 +1,7 @@ { "id": "berlinc", "name": "Berlin Clock", - "version": "0.07", + "version": "0.08", "description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)", "icon": "berlin-clock.png", "type": "clock", diff --git a/apps/bigdclock/ChangeLog b/apps/bigdclock/ChangeLog index c92d139bb..4089c823d 100644 --- a/apps/bigdclock/ChangeLog +++ b/apps/bigdclock/ChangeLog @@ -5,3 +5,5 @@ 0.05: proper fix for the race condition in queueDraw() 0.06: Tell clock widgets to hide. 0.07: Better battery graphic - now has green, yellow and red sections; battery status reflected in the bar across the middle of the screen; current battery state checked only once every 15 minutes, leading to longer-lasting battery charge +0.08: Minor code improvements +0.09: Something was changing the value of the "width" variable, which caused the battery usage feature to malfunction. The "width" variable has been renamed - the cause remains a mystery. diff --git a/apps/bigdclock/bigdclock.app.js b/apps/bigdclock/bigdclock.app.js index a8e2b38df..0ebc33bed 100644 --- a/apps/bigdclock/bigdclock.app.js +++ b/apps/bigdclock/bigdclock.app.js @@ -12,7 +12,7 @@ Graphics.prototype.setFontOpenSans = function(scale) { var drawTimeout; var lastBattCheck = 0; -var width = 0; +var batteryUsedWidth = 0; function queueDraw(millis_now) { if (drawTimeout) clearTimeout(drawTimeout); @@ -31,9 +31,9 @@ function draw() { var dow = require("date_utils").dows(0,1)[date.getDay()]; if ((date.getTime() >= lastBattCheck + 15*60000) || Bangle.isCharging()) { - lastBattcheck = date.getTime(); - width = E.getBattery(); - width += width/2; + lastBattCheck = date.getTime(); + batteryUsedWidth = E.getBattery(); + batteryUsedWidth += batteryUsedWidth/2; } g.reset(); @@ -58,7 +58,7 @@ function draw() { g.fillRect(167,163,170,167); if (Bangle.isCharging()) { g.setColor(1,1,0); - g.fillRect(12,162,12+width,168); + g.fillRect(12,162,12+batteryUsedWidth,168); } else { g.setColor(1,0,0); g.fillRect(12,162,57,168); @@ -67,16 +67,16 @@ function draw() { g.setColor(0,1,0); g.fillRect(73,162,162,168); } - if (width < 150) { + if (batteryUsedWidth < 150) { g.setColor(g.theme.bg); - g.fillRect(12+width+1,162,162,168); + g.fillRect(12+batteryUsedWidth+1,162,162,168); } if (Bangle.isCharging()) { g.setColor(1,1,0); - } else if (width <= 45) { + } else if (batteryUsedWidth <= 45) { g.setColor(1,0,0); - } else if (width <= 60) { + } else if (batteryUsedWidth <= 60) { g.setColor(1,1,0); } else { g.setColor(0, 1, 0); diff --git a/apps/bigdclock/metadata.json b/apps/bigdclock/metadata.json index 30352ca1a..dc3bcb143 100644 --- a/apps/bigdclock/metadata.json +++ b/apps/bigdclock/metadata.json @@ -1,7 +1,7 @@ { "id": "bigdclock", "name": "Big digit clock containing just the essentials", "shortName":"Big digit clk", - "version":"0.07", + "version": "0.09", "description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.", "icon": "bigdclock.png", "type": "clock", diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog index 296b6f948..7330eb937 100644 --- a/apps/bikespeedo/ChangeLog +++ b/apps/bikespeedo/ChangeLog @@ -4,3 +4,4 @@ 0.04: Add options for units in locale and recording GPS 0.05: Allow toggling of "max" values (screen tap) and recording (button press) 0.06: Fix local unit setting +0.07: Minor code improvements diff --git a/apps/bikespeedo/app.js b/apps/bikespeedo/app.js index 2310f1656..a1f0b53ce 100644 --- a/apps/bikespeedo/app.js +++ b/apps/bikespeedo/app.js @@ -409,7 +409,8 @@ function updateClock() { if ( emulator ) { max.spd++; max.alt++; - d=new Date(); sec=d.getSeconds(); + const d=new Date(); + sec=d.getSeconds(); onGPS(lf); } } diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json index 4b8ff9d92..20c7d4f53 100644 --- a/apps/bikespeedo/metadata.json +++ b/apps/bikespeedo/metadata.json @@ -2,7 +2,7 @@ "id": "bikespeedo", "name": "Bike Speedometer (beta)", "shortName": "Bike Speedometer", - "version": "0.06", + "version": "0.07", "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources", "icon": "app.png", "screenshots": [{"url":"Screenshot.png"}], diff --git a/apps/binaryclk/ChangeLog b/apps/binaryclk/ChangeLog new file mode 100644 index 000000000..7f3507499 --- /dev/null +++ b/apps/binaryclk/ChangeLog @@ -0,0 +1,12 @@ +0.01: Added app +0.02: Removed unneeded squares +0.03: Added setting for fullscreen option +0.04: Added settings to hide unused squares and show date +0.05: Minor code improvements +0.06: Added setting to show battery and added artwork to date +0.07: Removed percentage from battery and cleaned up logic +0.08: Changed month to day and text color to black on date +0.09: Changed day color back to white +0.10: Add blinking when charging +0.11: Changed battery to buzz instead of blink and fixed battery counter +0.12: Got rid of battery counter \ No newline at end of file diff --git a/apps/binaryclk/app-icon.js b/apps/binaryclk/app-icon.js new file mode 100644 index 000000000..3cb526a4f --- /dev/null +++ b/apps/binaryclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AEUiAAQEEkECBRAX/C/4Xrd+hCDI4kgR/4X/C/4XIAF53/C/4X/A4gSDC4kgC5AAvR/4X/C/4A/ADoA==")) diff --git a/apps/binaryclk/app-icon.png b/apps/binaryclk/app-icon.png new file mode 100644 index 000000000..b28ffdab4 Binary files /dev/null and b/apps/binaryclk/app-icon.png differ diff --git a/apps/binaryclk/app.js b/apps/binaryclk/app.js new file mode 100644 index 000000000..afc72aabe --- /dev/null +++ b/apps/binaryclk/app.js @@ -0,0 +1,114 @@ +var settings = Object.assign({ + fullscreen: true, + hidesq: false, + showdate: true, + showbat: true, +}, require('Storage').readJSON("binaryclk.json", true) || {}); + +var gap = 4; +var mgn = 24; +var sq = 33; + +if (settings.fullscreen) { + gap = 8; + mgn = 0; + sq = 34; +} + +var pos = sq + gap; + +function drawbat() { + var bat = E.getBattery(); + if (bat < 20) { + g.setColor('#FF0000'); + } else if (bat < 40) { + g.setColor('#FFA500'); + } else { + g.setColor('#00FF00'); + } + g.fillRect(Math.floor(mgn/2) + gap + 2 * pos, mgn + gap, Math.floor(mgn/2) + gap + 2 * pos + Math.floor(bat * sq / 100), mgn + gap + sq); +} + +function drawbatrect() { + if (g.theme.dark) { + g.setColor(-1); + } else { + g.setColor(1); + } + g.drawRect(Math.floor(mgn/2) + gap + 2 * pos, mgn + gap, Math.floor(mgn/2) + gap + 2 * pos + sq, mgn + gap + sq); +} + +function draw() { + let i = 0; + var dt = new Date(); + var h = dt.getHours(); + var m = dt.getMinutes(); + var d = dt.getDate(); + var day = dt.toString().substring(0,3); + const t = []; + + t[0] = Math.floor(h/10); + t[1] = Math.floor(h%10); + t[2] = Math.floor(m/10); + t[3] = Math.floor(m%10); + + g.reset(); + g.clearRect(Bangle.appRect); + + for (let r = 3; r >= 0; r--) { + for (let c = 0; c < 4; c++) { + if (t[c] & Math.pow(2, r)) { + g.fillRect(Math.floor(mgn/2) + gap + c * pos, mgn + gap + i * pos, Math.floor(mgn/2) + gap + c * pos + sq, mgn + gap + i * pos + sq); + } else { + g.drawRect(Math.floor(mgn/2) + gap + c * pos, mgn + gap + i * pos, Math.floor(mgn/2) + gap + c * pos + sq, mgn + gap + i * pos + sq); + } + } + i++; + } + + var c1sqhide = 0; + var c3sqhide = 0; + + if (settings.hidesq) { + c1sqhide = 2; + c3sqhide = 1; + g.clearRect(Math.floor(mgn/2), mgn, Math.floor(mgn/2) + pos, mgn + c1sqhide * pos); + g.clearRect(Math.floor(mgn/2) + 2 * pos + gap, mgn, Math.floor(mgn/2) + 3 * pos, mgn + c3sqhide * pos); + } + + if (settings.showdate) { + g.setColor(-1).fillRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + sq); + g.setColor('#FF0000').fillRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + 12); + g.setFontAlign(0, -1); + g.setFont("Vector",12); + g.setColor(-1).drawString(day, Math.ceil(mgn/2) + gap + Math.ceil(sq/2) + 1, mgn + gap + 1); + g.setFontAlign(0, 1); + g.setFont("Vector",20); + g.setColor(1).drawString(d, Math.ceil(mgn/2) + gap + Math.ceil(sq/2) + 1, mgn + gap + sq + 2); + if (g.theme.dark) { + g.setColor(-1); + } else { + g.setColor(1); + g.drawLine(Math.floor(mgn/2) + gap, mgn + gap + 13, Math.floor(mgn/2) + gap + sq, mgn + gap + 13); + } + g.drawRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + sq); + } + + if (settings.showbat) { + drawbat(); + drawbatrect(); + } +} + +g.clear(); +draw(); +setInterval(draw, 60000); +Bangle.setUI("clock"); +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +Bangle.on('charging', function(charging) { + if(charging) Bangle.buzz(); +}); \ No newline at end of file diff --git a/apps/binaryclk/metadata.json b/apps/binaryclk/metadata.json new file mode 100644 index 000000000..432bed25e --- /dev/null +++ b/apps/binaryclk/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "binaryclk", + "name": "Bin Clock", + "version": "0.12", + "description": "Binary clock with date and battery", + "icon": "app-icon.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"binaryclk.app.js","url":"app.js"}, + {"name":"binaryclk.settings.js","url":"settings.js"}, + {"name":"binaryclk.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"binaryclk.json"}] +} diff --git a/apps/binaryclk/screenshot.png b/apps/binaryclk/screenshot.png new file mode 100644 index 000000000..2b459c87c Binary files /dev/null and b/apps/binaryclk/screenshot.png differ diff --git a/apps/binaryclk/settings.js b/apps/binaryclk/settings.js new file mode 100644 index 000000000..34b3ae180 --- /dev/null +++ b/apps/binaryclk/settings.js @@ -0,0 +1,46 @@ +(function(back) { + var FILE = "binaryclk.json"; + var settings = Object.assign({ + fullscreen: false, + hidesq: false, + showdate: false, + showbat: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Bin Clock" }, + "< Back" : () => back(), + 'Fullscreen': { + value: settings.fullscreen, + onchange: v => { + settings.fullscreen = v; + writeSettings(); + }, + }, + 'Hide Squares': { + value: settings.hidesq, + onchange: v => { + settings.hidesq = v; + writeSettings(); + }, + }, + 'Show Date': { + value: settings.showdate, + onchange: v => { + settings.showdate = v; + writeSettings(); + }, + }, + 'Show Battery': { + value: settings.showbat, + onchange: v => { + settings.showbat = v; + writeSettings(); + }, + }, + }); +}) diff --git a/apps/binwatch/ChangeLog b/apps/binwatch/ChangeLog index e355155b3..2d60667d2 100644 --- a/apps/binwatch/ChangeLog +++ b/apps/binwatch/ChangeLog @@ -4,3 +4,5 @@ 0.04: corrected format of background image (raw binary) 0.05: move setUI() up before draw() as to not have a false positive 'sanity check' when building on github. +0.06: Minor code improvements +0.07: Minor code improvements diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js index 153bebb32..a25595bf5 100644 --- a/apps/binwatch/app.js +++ b/apps/binwatch/app.js @@ -63,8 +63,8 @@ const V2_BAT_SIZE_Y = 2; const V2_SCREEN_SIZE_X = 176; const V2_SCREEN_SIZE_Y = 176; const V2_BACKGROUND_IMAGE = "binwatch.bg176.img"; -const V2_BG_COLOR = 0; -const V2_FG_COLOR = 1; +//const V2_BG_COLOR = 0; +//const V2_FG_COLOR = 1; /* Bangle 1: 240 x 240 */ @@ -91,15 +91,15 @@ const V1_BAT_SIZE_Y = 5; const V1_SCREEN_SIZE_X = 240; const V1_SCREEN_SIZE_Y = 240; const V1_BACKGROUND_IMAGE = "binwatch.bg240.img"; -const V1_BG_COLOR = 1; -const V1_FG_COLOR = 0; +//const V1_BG_COLOR = 1; +//const V1_FG_COLOR = 0; /* runtime settings */ var x_step = 0; var y_step = 0; -var time_y_offset = 0; +//var time_y_offset = 0; var hx = 0, hy = 0; var mx = 0, my = 0; var sx = 0, sy = 0; @@ -110,10 +110,9 @@ var bat_pos_x, bat_pos_y, bat_size_x, bat_size_y; var backgroundImage = ""; var screen_size_x = 0; var screen_size_y = 0; -var bg_color = 0; -var fg_color = 1; +//var bg_color = 0; +//var fg_color = 1; -/* global variables */ var showDateTime = 2; /* show noting, time or date */ var cg; @@ -137,7 +136,7 @@ var cgimg; */ function drawSquare(gfx, x, y, data, numOfBits) { - for(i = numOfBits; i > 0 ; i--) { + for(let i = numOfBits; i > 0 ; i--) { if( (data & 1) != 0) { gfx.fillRect(x + (i - 1) * x_step, y, x + i * x_step , y + y_step); @@ -246,7 +245,7 @@ function drawBattery(gfx, level) { var pos_y = bat_pos_y - 1; var stepLevel = Math.round((level + 10) / 20); - for(i = 0; i < stepLevel; i++) { + for(let i = 0; i < stepLevel; i++) { pos_y -= bat_size_y + 2; gfx.fillRect(bat_pos_x, pos_y, bat_pos_x + bat_size_x, pos_y + bat_size_y); @@ -271,7 +270,7 @@ function setRuntimeValues(resolution) { x_step = V1_X_STEP; y_step = V1_Y_STEP; - time_y_offset = V1_TIME_Y_OFFSET; + //time_y_offset = V1_TIME_Y_OFFSET; hx = V1_HX; hy = V1_HY; mx = V1_MX; @@ -298,7 +297,7 @@ function setRuntimeValues(resolution) { x_step = V2_X_STEP; y_step = V2_Y_STEP; - time_y_offset = V2_TIME_Y_OFFSET; + //time_y_offset = V2_TIME_Y_OFFSET; hx = V2_HX; hy = V2_HY; diff --git a/apps/binwatch/metadata.json b/apps/binwatch/metadata.json index 0b4dbc697..1e71362cb 100644 --- a/apps/binwatch/metadata.json +++ b/apps/binwatch/metadata.json @@ -3,7 +3,7 @@ "shortName":"BinWatch", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.05", + "version": "0.07", "supports": ["BANGLEJS2"], "readme": "README.md", "allow_emulator":true, diff --git a/apps/blc/ChangeLog b/apps/blc/ChangeLog new file mode 100644 index 000000000..4860c24a6 --- /dev/null +++ b/apps/blc/ChangeLog @@ -0,0 +1 @@ +0.10: New app introduced to the app loader! diff --git a/apps/blc/README.md b/apps/blc/README.md new file mode 100644 index 000000000..a3581cdc7 --- /dev/null +++ b/apps/blc/README.md @@ -0,0 +1,13 @@ +# Binary LED Clock + +A binary watch with LEDs, showing time and date. + +From top to bottom the watch face shows four rows of leds: + +* hours (red leds) +* minutes (green leds) +* day (yellow leds, top row) +* month (yellow leds, bottom row) + +As usual, luminous leds represent a logical one, dark leds a logcal '0'. +Widgets aren't affected and are shown as normal. diff --git a/apps/blc/blc-icon.js b/apps/blc/blc-icon.js new file mode 100644 index 000000000..a8d30baee --- /dev/null +++ b/apps/blc/blc-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/AAIHBqut8FPgH4sspk1T885/feoMI74TB1Fc51Dmfg28gKmMCrNSAgMlyo5BgV7uQIKgEhiMRkECAYMSgErolLBBIXBqIKBqEFAYMVgF0olEuAIIC4ORBQOQhIDBjMA2gOB2AIIF7JfXR67X0lvdHwQII7vSa4/TmYKBBBEtmc9a40NmYKBBBIbBmfQa4oOEBBAXFF65fXR64A/AG8IvN4AgOG62ABAuHy4IGgEHiMXAgNu91gBAtxiNwBAsAhMRjIEB73ucIIIEyMRyAIFF7BfXAH6/IttoKxRoIgEG93mQxSYIgEN93tWxTIIF7BfXAH4AGw93u/A44IDhl8vQRFBogXB0ECuGoBAcKxRxBC53Hhlyk8ggVyuQGBvlwhgNBk98BAN6I4UgC4N4BwWgAwWsC4fAk4IB0AvBAgIQBBwUIkQOBAwQXCJIIEBI4UAkQXE48sAwgXJF40mgAvDvRtCC4pfEC4WCPYJdBDYNyC4wAX")) diff --git a/apps/blc/blc-icon.png b/apps/blc/blc-icon.png new file mode 100644 index 000000000..8bbf6ae71 Binary files /dev/null and b/apps/blc/blc-icon.png differ diff --git a/apps/blc/blc.js b/apps/blc/blc.js new file mode 100644 index 000000000..8a0f93d70 --- /dev/null +++ b/apps/blc/blc.js @@ -0,0 +1,136 @@ +//Binary LED Clock (BLC) by aeMKai + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + let drawTimeout; + + // Actually draw the watch face + let draw = function() + { + // Bangle.js2 -> 176x176 + var x_rgt = g.getWidth(); + var y_bot = g.getHeight(); + //var x_cntr = x_rgt / 2; + var y_cntr = y_bot / 18*7; // not to high because of widget-field (1/3 is to high) + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + + let white = [1,1,1]; + let red = [1,0,0]; + let green = [0,1,0]; + //let blue = [0,0,1]; + let yellow = [1,1,0]; + //let magenta = [1,0,1]; + //let cyan = [0,1,1]; + let black = [0,0,0]; + let bord_col = white; + let col_off = black; + + var col = new Array(red, green, yellow, yellow); // [R,G,B] + + let pot_2 = [1, 2, 4, 8, 16, 32]; // array with powers of two, because power-op (**) + // doesn't work -> maybe also faster + + + var nr_lines = 4; // 4 rows: hour (hr), minute (min), day (day), month (mon) + + // Arrays: [hr, min, day, mon] + //No of Bits: 5 6 5 4 + let msbits = [4, 5, 4, 3]; // MSB = No bits - 1 + let rad = [12, 12, 8, 8]; // radiuses for each row + var x_dist = 28; + let y_dist = [0, 30, 60, 85]; // y-position from y_centr for each row from top + // don't calc. automatic as for x, because of different spaces + var x_offs_rgt = 16; // distance from right border (layout) + + // Date-Time-Array: 4x6 Bit + //var idx_hr = 0; + //var idx_min = 1; + //var idx_day = 2; + //var idx_mon = 3; + var dt_bit_arr = [[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0]]; + + var date_time = new Date(); + var hr = date_time.getHours(); // 0..23 + var min = date_time.getMinutes(); // 0..59 + var day = date_time.getDate(); // 1..31 + var mon = date_time.getMonth() + 1; // GetMonth() -> 0..11 + + let dt_array = [hr, min, day, mon]; + + +//////////////////////////////////////// +// compute bit-pattern from time/date and draw leds +//////////////////////////////////////// + var line_cnt = 0; + var cnt = 0; + var bit_cnt = 0; + + while (line_cnt < nr_lines) + { + + //////////////////////////////////////// + // compute bit-pattern + bit_cnt = msbits[line_cnt]; + + while (bit_cnt >= 0) + { + if (dt_array[line_cnt] >= pot_2[bit_cnt]) + { + dt_array[line_cnt] -= pot_2[bit_cnt]; + dt_bit_arr[line_cnt][bit_cnt] = 1; + } + else + { + dt_bit_arr[line_cnt][bit_cnt] = 0; + } + bit_cnt--; + } + + //////////////////////////////////////// + // draw leds (first white border for black screen, then led itself) + cnt = 0; + + while (cnt <= msbits[line_cnt]) + { + g.setColor(bord_col[0], bord_col[1], bord_col[2]); + g.drawCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-20+y_dist[line_cnt], rad[line_cnt]); + + if (dt_bit_arr[line_cnt][cnt] == 1) + { + g.setColor(col[line_cnt][0], col[line_cnt][1], col[line_cnt][2]); + } + else + { + g.setColor(col_off[0], col_off[1], col_off[2]); + } + g.fillCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-20+y_dist[line_cnt], rad[line_cnt]-1); + cnt++; + } + line_cnt++; + } + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() + { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + // Show launcher when middle button pressed + Bangle.setUI( + { + mode : "clock", + remove : function() + { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } + }); + // Load widgets + Bangle.loadWidgets(); + draw(); + setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/blc/metadata.json b/apps/blc/metadata.json new file mode 100644 index 000000000..174926ebc --- /dev/null +++ b/apps/blc/metadata.json @@ -0,0 +1,17 @@ +{ + "id":"blc", + "name":"Binary LED Clock", + "version": "0.10", + "description": "Binary LED Clock with date", + "icon":"blc-icon.png", + "screenshots": [{"url":"screenshot_blc.bmp"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"blc.app.js","url":"blc.js"}, + {"name":"blc.img","url":"blc-icon.js","evaluate":true} + ] +} diff --git a/apps/blc/screenshot_blc.bmp b/apps/blc/screenshot_blc.bmp new file mode 100644 index 000000000..50b8539bb Binary files /dev/null and b/apps/blc/screenshot_blc.bmp differ diff --git a/apps/blecsc/ChangeLog b/apps/blecsc/ChangeLog new file mode 100644 index 000000000..1500000b5 --- /dev/null +++ b/apps/blecsc/ChangeLog @@ -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 \ No newline at end of file diff --git a/apps/blecsc/README.md b/apps/blecsc/README.md new file mode 100644 index 000000000..5cde87168 --- /dev/null +++ b/apps/blecsc/README.md @@ -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 \ No newline at end of file diff --git a/apps/blecsc/blecsc.js b/apps/blecsc/blecsc.js new file mode 100644 index 000000000..9b7d8b751 --- /dev/null +++ b/apps/blecsc/blecsc.js @@ -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(); +*/ diff --git a/apps/blecsc/clkinfo.js b/apps/blecsc/clkinfo.js new file mode 100644 index 000000000..9a9515c3a --- /dev/null +++ b/apps/blecsc/clkinfo.js @@ -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; +}) + + diff --git a/apps/blecsc/icons8-cycling-48.png b/apps/blecsc/icons8-cycling-48.png new file mode 100644 index 000000000..0bc83859f Binary files /dev/null and b/apps/blecsc/icons8-cycling-48.png differ diff --git a/apps/blecsc/metadata.json b/apps/blecsc/metadata.json new file mode 100644 index 000000000..0daa01fc8 --- /dev/null +++ b/apps/blecsc/metadata.json @@ -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"} + ] +} diff --git a/apps/blecsc/recorder.js b/apps/blecsc/recorder.js new file mode 100644 index 000000000..510f50c3f --- /dev/null +++ b/apps/blecsc/recorder.js @@ -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) + }; + } +}) + diff --git a/apps/blecsc/settings.js b/apps/blecsc/settings.js new file mode 100644 index 000000000..b445b2541 --- /dev/null +++ b/apps/blecsc/settings.js @@ -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(); +}) \ No newline at end of file diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 6349d9213..1d8e44b72 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -68,3 +68,7 @@ 0.57: Handle the whitelist being disabled 0.58: "Make Connectable" temporarily bypasses the whitelist 0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds +0.60: Minor code improvements +0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined') +0.62: Handle setting for configuring BLE privacy +0.63: Only set BLE `display:1` if we have a passkey diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 1b11a3f16..aa4a7e7b5 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -4,7 +4,7 @@ of the time. */ { // execute in our own scope so we don't have to free variables... E.showMessage(/*LANG*/"Updating boot0..."); let s = require('Storage').readJSON('setting.json',1)||{}; -const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 +//const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 const FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1")); const DEBUG = s.bootDebug; // we can set this to enable debugging output in boot0 let boot = "", bootPost = ""; @@ -14,15 +14,15 @@ if (DEBUG) { } if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; + boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } else { let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; + boot += `if(E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } -boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; +boot += `{eval(require('Storage').read('bootupdate.js'));print("Storage Updated!")}else{\n`; boot += `E.setFlags({pretokenise:1});\n`; boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; -bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code +bootPost += `NRF.setServices(bleServices,bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code if (s.ble!==false) { if (s.HID) { // Human interface device if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; @@ -78,7 +78,12 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't // Apply any settings-specific stuff if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; -if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; +if (s.bleprivacy || (s.passkey!==undefined && s.passkey.length==6)) { + let passkey = s.passkey ? `passkey:${E.toJS(s.passkey.toString())},display:1,mitm:1,` : ""; + let privacy = s.bleprivacy ? `privacy:${E.toJS(s.bleprivacy)},` : ""; + boot+=`NRF.setSecurity({${passkey}${privacy}});\n`; +} +if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`; if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`; if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation // ================================================== FIXING OLDER FIRMWARES @@ -105,7 +110,7 @@ if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function( // show timings if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n` // ================================================== BOOT.JS -// Append *.boot.js files. +// Append *.boot.js files. // Name files with a number - eg 'foo.5.boot.js' to enforce order (lowest first). Numbered files get placed before non-numbered // These could change bleServices/bleServiceOptions if needed let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ @@ -122,6 +127,7 @@ let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ return a==b ? 0 : (a>b ? 1 : -1); }); // precalculate file size +bootPost += "}"; let fileSize = boot.length + bootPost.length; bootFiles.forEach(bootFile=>{ // match the size of data we're adding below in bootFiles.forEach diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 45f531776..dcc55da58 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.59", + "version": "0.63", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/bootbthomebatt/ChangeLog b/apps/bootbthomebatt/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/bootbthomebatt/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/bootbthomebatt/README.md b/apps/bootbthomebatt/README.md new file mode 100644 index 000000000..79c379f33 --- /dev/null +++ b/apps/bootbthomebatt/README.md @@ -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) diff --git a/apps/bootbthomebatt/bluetooth.png b/apps/bootbthomebatt/bluetooth.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/bootbthomebatt/bluetooth.png differ diff --git a/apps/bootbthomebatt/boot.js b/apps/bootbthomebatt/boot.js new file mode 100644 index 000000000..a9bdcd4d3 --- /dev/null +++ b/apps/bootbthomebatt/boot.js @@ -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(); diff --git a/apps/bootbthomebatt/metadata.json b/apps/bootbthomebatt/metadata.json new file mode 100644 index 000000000..0992edc24 --- /dev/null +++ b/apps/bootbthomebatt/metadata.json @@ -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"} + ] +} diff --git a/apps/bootgattbat/ChangeLog b/apps/bootgattbat/ChangeLog index df07f6ad0..3df6422e5 100644 --- a/apps/bootgattbat/ChangeLog +++ b/apps/bootgattbat/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial release. 0.02: Handle the case where other apps have set bleAdvert to an array +0.03: Use the bleAdvert module diff --git a/apps/bootgattbat/boot.js b/apps/bootgattbat/boot.js index 34d9f8d93..f9b5969e2 100644 --- a/apps/bootgattbat/boot.js +++ b/apps/bootgattbat/boot.js @@ -1,26 +1,8 @@ (() => { function advertiseBattery() { - if(Array.isArray(Bangle.bleAdvert)){ - // ensure we're in the cycle - var found = false; - for(var ad in Bangle.bleAdvert){ - if(ad[0x180F]){ - ad[0x180F] = [E.getBattery()]; - found = true; - break; - } - } - if(!found) - Bangle.bleAdvert.push({ 0x180F: [E.getBattery()] }); - }else{ - // simple object - Bangle.bleAdvert[0x180F] = [E.getBattery()]; - } - - NRF.setAdvertising(Bangle.bleAdvert); + require("ble_advert").set(0x180F, [E.getBattery()]); } - if (!Bangle.bleAdvert) Bangle.bleAdvert = {}; setInterval(advertiseBattery, 60 * 1000); advertiseBattery(); })(); diff --git a/apps/bootgattbat/metadata.json b/apps/bootgattbat/metadata.json index f67b4507d..b53708c2e 100644 --- a/apps/bootgattbat/metadata.json +++ b/apps/bootgattbat/metadata.json @@ -2,7 +2,7 @@ "id": "bootgattbat", "name": "BLE GATT Battery Service", "shortName": "BLE Battery Service", - "version": "0.02", + "version": "0.03", "description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n", "icon": "bluetooth.png", "type": "bootloader", diff --git a/apps/bootgatthrm/ChangeLog b/apps/bootgatthrm/ChangeLog index 1e772af29..205f036b5 100644 --- a/apps/bootgatthrm/ChangeLog +++ b/apps/bootgatthrm/ChangeLog @@ -1,2 +1,4 @@ 0.01: Initial release. -0.02: Added compatibility to OpenTracks and added HRM Location \ No newline at end of file +0.02: Added compatibility to OpenTracks and added HRM Location +0.03: Allow setting to keep BLE connected +0.04: Use the bleAdvert module diff --git a/apps/bootgatthrm/boot.js b/apps/bootgatthrm/boot.js index aad210f6f..efba1f453 100644 --- a/apps/bootgatthrm/boot.js +++ b/apps/bootgatthrm/boot.js @@ -4,18 +4,13 @@ * This function prepares BLE heart rate Advertisement. */ - NRF.setAdvertising( - { - 0x180d: undefined - }, - { - // We need custom Advertisement settings for Apps like OpenTracks - connectable: true, - discoverable: true, - scannable: true, - whenConnected: true, - } - ); + require("ble_advert").set(0x180d, undefined, { + // We need custom Advertisement settings for Apps like OpenTracks + connectable: true, + discoverable: true, + scannable: true, + whenConnected: true, + }); NRF.setServices({ 0x180D: { // heart_rate @@ -28,8 +23,10 @@ } } }); - } + + const keepConnected = (require("Storage").readJSON("gatthrm.settings.json", 1) || {}).keepConnected; + function updateBLEHeartRate(hrm) { /* * Send updated heart rate measurement via BLE @@ -50,13 +47,14 @@ } catch (error) { if (error.message.includes("BLE restart")) { /* - * BLE has to restart after service setup. + * BLE has to restart after service setup. */ - NRF.disconnect(); + if(!keepConnected) + NRF.disconnect(); } else if (error.message.includes("UUID 0x2a37")) { /* - * Setup service if it wasn't setup correctly for some reason + * Setup service if it wasn't setup correctly for some reason */ setupHRMAdvertising(); } else { @@ -66,5 +64,5 @@ } setupHRMAdvertising(); - Bangle.on("HRM", function (hrm) { updateBLEHeartRate(hrm); }); + Bangle.on("HRM", updateBLEHeartRate); })(); diff --git a/apps/bootgatthrm/metadata.json b/apps/bootgatthrm/metadata.json index 450066622..d32b51601 100644 --- a/apps/bootgatthrm/metadata.json +++ b/apps/bootgatthrm/metadata.json @@ -2,7 +2,7 @@ "id": "bootgatthrm", "name": "BLE GATT HRM Service", "shortName": "BLE HRM Service", - "version": "0.02", + "version": "0.04", "description": "Adds the GATT HRM Service to advertise the measured HRM over Bluetooth.\n", "icon": "bluetooth.png", "type": "bootloader", diff --git a/apps/bowserWF/ChangeLog b/apps/bowserWF/ChangeLog index dd2b05fb3..4417339fa 100644 --- a/apps/bowserWF/ChangeLog +++ b/apps/bowserWF/ChangeLog @@ -1,3 +1,4 @@ ... 0.02: First update with ChangeLog Added 0.03: updated watch face to use the ClockFace library +0.04: Minor code improvements diff --git a/apps/bowserWF/app.js b/apps/bowserWF/app.js index 956c43602..e93617b18 100644 --- a/apps/bowserWF/app.js +++ b/apps/bowserWF/app.js @@ -34,10 +34,10 @@ const background = { ), }; -numbersDims = { +/*const numbersDims = { width: 20, height: 44, -}; +};*/ const numbers = [ require("heatshrink").decompress( atob( @@ -88,7 +88,7 @@ const numbers = [ ) ), ]; -digitPositions = [ +const digitPositions = [ // relative to the box { x: 13, y: 6 }, { x: 32, y: 6 }, diff --git a/apps/bowserWF/metadata.json b/apps/bowserWF/metadata.json index bba15e5df..462592902 100644 --- a/apps/bowserWF/metadata.json +++ b/apps/bowserWF/metadata.json @@ -2,7 +2,7 @@ "id": "bowserWF", "name": "Bowser Watchface", "shortName": "Bowser Watchface", - "version": "0.03", + "version": "0.04", "description": "Let bowser show you the time", "icon": "app.png", "type": "clock", diff --git a/apps/boxclk/ChangeLog b/apps/boxclk/ChangeLog index cc73fbc08..65a6c979f 100644 --- a/apps/boxclk/ChangeLog +++ b/apps/boxclk/ChangeLog @@ -3,3 +3,14 @@ 0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false 0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher 0.05: Fixes step count not resetting after a new day starts +0.06: Added clockbackground app functionality +0.07: Allow custom backgrounds per boxclk config and from the clockbg module +0.08: Improves performance, responsiveness, and bug fixes +- [+] Added box size caching to reduce calculations +- [+] Improved step count with real-time updates +- [+] Improved battery level update logic to reduce unnecessary refreshes +- [+] Fixed optional seconds not displaying in time +- [+] Fixed drag handler by adding E.stopEventPropagation() +- [+] General code optimization and cleanup +0.09: Revised event handler code +0.10: Revised suffix formatting in getDate function \ No newline at end of file diff --git a/apps/boxclk/app.js b/apps/boxclk/app.js index 12c69e789..71dbda94f 100644 --- a/apps/boxclk/app.js +++ b/apps/boxclk/app.js @@ -1,58 +1,123 @@ { - /** - * --------------------------------------------------------------- - * 1. Module dependencies and initial configurations - * --------------------------------------------------------------- - */ - + // 1. Module dependencies and initial configurations + let background = require("clockbg"); let storage = require("Storage"); let locale = require("locale"); let widgets = require("widget_utils"); - let date = new Date(); let bgImage; let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0; let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json'; - // Add a condition to check if the file exists, if it does not, default to 'boxclk.json' if (!storage.read(fileName)) { fileName = 'boxclk.json'; } let boxesConfig = storage.readJSON(fileName, 1) || {}; let boxes = {}; - let boxPos = {}; - let isDragging = {}; - let wasDragging = {}; + let isDragging = false; let doubleTapTimer = null; let g_setColor; let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA")); - /** - * --------------------------------------------------------------- - * 2. Graphical and visual configurations - * --------------------------------------------------------------- - */ - + // 2. Graphical and visual configurations let w = g.getWidth(); let h = g.getHeight(); - let totalWidth, totalHeight; let drawTimeout; - /** - * --------------------------------------------------------------- - * 3. Touchscreen Handlers - * --------------------------------------------------------------- - */ - - let touchHandler; - let dragHandler; - let movementDistance = 0; - - /** - * --------------------------------------------------------------- - * 4. Font loading function - * --------------------------------------------------------------- - */ + // 3. Event handlers + let touchHandler = function(zone, e) { + let boxTouched = false; + let touchedBox = null; + + for (let boxKey in boxes) { + if (touchInText(e, boxes[boxKey])) { + touchedBox = boxKey; + boxTouched = true; + break; + } + } + + if (boxTouched) { + // Toggle the selected state of the touched box + boxes[touchedBox].selected = !boxes[touchedBox].selected; + + // Update isDragging based on whether any box is selected + isDragging = Object.values(boxes).some(box => box.selected); + + if (isDragging) { + widgets.hide(); + } else { + deselectAllBoxes(); + } + } else { + // If tapped outside any box, deselect all boxes + deselectAllBoxes(); + } + + // Always redraw after a touch event + draw(); + + // Handle double tap for saving + if (!boxTouched && !isDragging) { + if (doubleTapTimer) { + clearTimeout(doubleTapTimer); + doubleTapTimer = null; + for (let boxKey in boxes) { + boxesConfig[boxKey].boxPos.x = (boxes[boxKey].pos.x / w).toFixed(3); + boxesConfig[boxKey].boxPos.y = (boxes[boxKey].pos.y / h).toFixed(3); + } + storage.write(fileName, JSON.stringify(boxesConfig)); + displaySaveIcon(); + return; + } + + doubleTapTimer = setTimeout(() => { + doubleTapTimer = null; + }, 500); + } + }; + + let dragHandler = function(e) { + if (!isDragging) return; + + // Stop propagation of the drag event to prevent other handlers + E.stopEventPropagation(); + + for (let key in boxes) { + if (boxes[key].selected) { + let boxItem = boxes[key]; + calcBoxSize(boxItem); + let newX = boxItem.pos.x + e.dx; + let newY = boxItem.pos.y + e.dy; + + if (newX - boxItem.cachedSize.width / 2 >= 0 && + newX + boxItem.cachedSize.width / 2 <= w && + newY - boxItem.cachedSize.height / 2 >= 0 && + newY + boxItem.cachedSize.height / 2 <= h) { + boxItem.pos.x = newX; + boxItem.pos.y = newY; + } + } + } + + draw(); + }; + + let stepHandler = function(up) { + if (boxes.step && !isDragging) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + boxes.step.cachedSize = null; + draw(); + } + }; + + let lockHandler = function(isLocked) { + if (isLocked) { + deselectAllBoxes(); + draw(); + } + }; + // 4. Font loading function let loadCustomFont = function() { Graphics.prototype.setFontBrunoAce = function() { // Actual height 23 (24 - 2) @@ -60,50 +125,43 @@ E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))), 46, atob("CBEdChgYGhgaGBsaCQ=="), - 32|65536 + 32 | 65536 ); }; }; - /** - * --------------------------------------------------------------- - * 5. Initial settings of boxes and their positions - * --------------------------------------------------------------- - */ + // 5. Initial settings of boxes and their positions + let isBool = (val, defaultVal) => val !== undefined ? Boolean(val) : defaultVal; for (let key in boxesConfig) { if (key === 'bg' && boxesConfig[key].img) { bgImage = storage.read(boxesConfig[key].img); } else if (key !== 'selectedConfig') { boxes[key] = Object.assign({}, boxesConfig[key]); + // Set default values for short, shortMonth, and disableSuffix + boxes[key].short = isBool(boxes[key].short, true); + boxes[key].shortMonth = isBool(boxes[key].shortMonth, true); + boxes[key].disableSuffix = isBool(boxes[key].disableSuffix, false); + + // Set box position + boxes[key].pos = { + x: w * boxes[key].boxPos.x, + y: h * boxes[key].boxPos.y + }; + // Cache box size + boxes[key].cachedSize = null; } } - let boxKeys = Object.keys(boxes); + // 6. Text and drawing functions - boxKeys.forEach((key) => { - let boxConfig = boxes[key]; - boxPos[key] = { - x: w * boxConfig.boxPos.x, - y: h * boxConfig.boxPos.y - }; - isDragging[key] = false; - wasDragging[key] = false; - }); - - /** - * --------------------------------------------------------------- - * 6. Text and drawing functions - * --------------------------------------------------------------- + /* + Overwrite the setColor function to allow the + use of (x) in g.theme.x as a string + in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") */ - - // Overwrite the setColor function to allow the - // use of (x) in g.theme.x as a string - // in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") let modSetColor = function() { - // Save the original setColor function g_setColor = g.setColor; - // Overwrite setColor with the new function g.setColor = function(color) { if (typeof color === "string" && color in g.theme) { g_setColor.call(g, g.theme[color]); @@ -114,7 +172,6 @@ }; let restoreSetColor = function() { - // Restore the original setColor function if (g_setColor) { g.setColor = g_setColor; } @@ -138,25 +195,6 @@ } }; - let calcBoxSize = function(boxItem) { - g.reset(); - g.setFontAlign(0,0); - g.setFont(boxItem.font, boxItem.fontSize); - let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; - let fontHeight = g.getFontHeight() + 2 * boxItem.outline; - totalWidth = strWidth + 2 * boxItem.xPadding; - totalHeight = fontHeight + 2 * boxItem.yPadding; - }; - - let calcBoxPos = function(boxKey) { - return { - x1: boxPos[boxKey].x - totalWidth / 2, - y1: boxPos[boxKey].y - totalHeight / 2, - x2: boxPos[boxKey].x + totalWidth / 2, - y2: boxPos[boxKey].y + totalHeight / 2 - }; - }; - let displaySaveIcon = function() { draw(boxes); g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24); @@ -167,33 +205,26 @@ }, 2000); }; - /** - * --------------------------------------------------------------- - * 7. String forming helper functions - * --------------------------------------------------------------- - */ - - let isBool = function(val, defaultVal) { - return typeof val !== 'undefined' ? Boolean(val) : defaultVal; - }; - + // 7. String forming helper functions let getDate = function(short, shortMonth, disableSuffix) { const date = new Date(); - const dayOfMonth = date.getDate(); + const day = date.getDate(); const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); const year = date.getFullYear(); - let suffix; - if ([1, 21, 31].includes(dayOfMonth)) { - suffix = "st"; - } else if ([2, 22].includes(dayOfMonth)) { - suffix = "nd"; - } else if ([3, 23].includes(dayOfMonth)) { - suffix = "rd"; - } else { - suffix = "th"; - } - let dayOfMonthStr = disableSuffix ? dayOfMonth : dayOfMonth + suffix; - return month + " " + dayOfMonthStr + (short ? '' : (", " + year)); // not including year for short version + + const getSuffix = (day) => { + if (day >= 11 && day <= 13) return 'th'; + const lastDigit = day % 10; + switch (lastDigit) { + case 1: return 'st'; + case 2: return 'nd'; + case 3: return 'rd'; + default: return 'th'; + } + }; + + const dayStr = disableSuffix ? day : `${day}${getSuffix(day)}`; + return `${month} ${dayStr}${short ? '' : `, ${year}`}`; // not including year for short version }; let getDayOfWeek = function(date, short) { @@ -206,189 +237,215 @@ return short ? meridian[0] : meridian; }; - let modString = function(boxItem, data) { - let prefix = boxItem.prefix || ''; - let suffix = boxItem.suffix || ''; - return prefix + data + suffix; + let formatStr = function(boxItem, data) { + return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`; }; - /** - * --------------------------------------------------------------- - * 8. Main draw function - * --------------------------------------------------------------- - */ + // 8. Main draw function and update logic + let lastDay = -1; + const BATTERY_UPDATE_INTERVAL = 300000; - let draw = (function() { - let updatePerMinute = true; // variable to track the state of time display + let updateBoxData = function() { + let date = new Date(); + let currentDay = date.getDate(); + let now = Date.now(); - return function(boxes) { - date = new Date(); - g.clear(); - if (bgImage) { - g.drawImage(bgImage, 0, 0); - } + if (boxes.time || boxes.meridian || boxes.date || boxes.dow) { if (boxes.time) { - boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0)); - updatePerMinute = isBool(boxes.time.short, true); - } - if (boxes.meridian) { - boxes.meridian.string = modString(boxes.meridian, locale.meridian(date, isBool(boxes.meridian.short, true))); - } - if (boxes.date) { - boxes.date.string = ( - modString(boxes.date, - getDate(isBool(boxes.date.short, true), - isBool(boxes.date.shortMonth, true), - isBool(boxes.date.disableSuffix, false) - ))); - } - if (boxes.dow) { - boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true))); - } - if (boxes.batt) { - boxes.batt.string = modString(boxes.batt, E.getBattery()); - } - if (boxes.step) { - boxes.step.string = modString(boxes.step, Bangle.getHealthStatus("day").steps); - } - boxKeys.forEach((boxKey) => { - let boxItem = boxes[boxKey]; - calcBoxSize(boxItem); - const pos = calcBoxPos(boxKey); - if (isDragging[boxKey]) { - g.setColor(boxItem.border); - g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + let showSeconds = !boxes.time.short; + let timeString = locale.time(date, 1).trim(); + if (showSeconds) { + let seconds = date.getSeconds().toString().padStart(2, '0'); + timeString += ':' + seconds; + } + let newTimeString = formatStr(boxes.time, timeString); + if (newTimeString !== boxes.time.string) { + boxes.time.string = newTimeString; + boxes.time.cachedSize = null; } - g.drawString( - boxItem, - boxItem.string, - boxPos[boxKey].x + boxItem.xOffset, - boxPos[boxKey].y + boxItem.yOffset - ); - }); - if (!Object.values(isDragging).some(Boolean)) { - if (drawTimeout) clearTimeout(drawTimeout); - let interval = updatePerMinute ? 60000 - (Date.now() % 60000) : 1000; - drawTimeout = setTimeout(() => draw(boxes), interval); } - }; - })(); - /** - * --------------------------------------------------------------- - * 9. Helper function for touch event - * --------------------------------------------------------------- - */ + if (boxes.meridian) { + let newMeridianString = formatStr(boxes.meridian, locale.meridian(date, boxes.meridian.short)); + if (newMeridianString !== boxes.meridian.string) { + boxes.meridian.string = newMeridianString; + boxes.meridian.cachedSize = null; + } + } - let touchInText = function(e, boxItem, boxKey) { + if (boxes.date && currentDay !== lastDay) { + let newDateString = formatStr(boxes.date, + getDate(boxes.date.short, + boxes.date.shortMonth, + boxes.date.noSuffix) + ); + if (newDateString !== boxes.date.string) { + boxes.date.string = newDateString; + boxes.date.cachedSize = null; + } + } + + if (boxes.dow) { + let newDowString = formatStr(boxes.dow, getDayOfWeek(date, boxes.dow.short)); + if (newDowString !== boxes.dow.string) { + boxes.dow.string = newDowString; + boxes.dow.cachedSize = null; + } + } + + lastDay = currentDay; + } + + if (boxes.step) { + let newStepCount = Bangle.getHealthStatus("day").steps; + let newStepString = formatStr(boxes.step, newStepCount); + if (newStepString !== boxes.step.string) { + boxes.step.string = newStepString; + boxes.step.cachedSize = null; + } + } + + if (boxes.batt) { + if (!boxes.batt.lastUpdate || now - boxes.batt.lastUpdate >= BATTERY_UPDATE_INTERVAL) { + let currentLevel = E.getBattery(); + if (currentLevel !== boxes.batt.lastLevel) { + let newBattString = formatStr(boxes.batt, currentLevel); + if (newBattString !== boxes.batt.string) { + boxes.batt.string = newBattString; + boxes.batt.cachedSize = null; + boxes.batt.lastLevel = currentLevel; + } + } + boxes.batt.lastUpdate = now; + } + } + }; + + let draw = function() { + g.clear(); + + // Always draw backgrounds full screen + if (bgImage) { // Check for bg in boxclk config + g.drawImage(bgImage, 0, 0); + } else { // Otherwise use clockbg module + background.fillRect(0, 0, g.getWidth(), g.getHeight()); + } + + if (!isDragging) { + updateBoxData(); + } + + for (let boxKey in boxes) { + let boxItem = boxes[boxKey]; + + // Set font and alignment for each box individually + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + calcBoxSize(boxItem); + + const pos = calcBoxPos(boxItem); + + if (boxItem.selected) { + g.setColor(boxItem.border); + g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + } + + g.drawString( + boxItem, + boxItem.string, + boxItem.pos.x + boxItem.xOffset, + boxItem.pos.y + boxItem.yOffset + ); + } + + if (!isDragging) { + if (drawTimeout) clearTimeout(drawTimeout); + let updateInterval = boxes.time && !isBool(boxes.time.short, true) ? 1000 : 60000 - (Date.now() % 60000); + drawTimeout = setTimeout(draw, updateInterval); + } + }; + + // 9. Helper function for touch event + let calcBoxPos = function(boxItem) { calcBoxSize(boxItem); - const pos = calcBoxPos(boxKey); + return { + x1: boxItem.pos.x - boxItem.cachedSize.width / 2, + y1: boxItem.pos.y - boxItem.cachedSize.height / 2, + x2: boxItem.pos.x + boxItem.cachedSize.width / 2, + y2: boxItem.pos.y + boxItem.cachedSize.height / 2 + }; + }; + + // Use cached size if available, otherwise calculate and cache + let calcBoxSize = function(boxItem) { + if (boxItem.cachedSize) { + return boxItem.cachedSize; + } + + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; + let fontHeight = g.getFontHeight() + 2 * boxItem.outline; + let totalWidth = strWidth + 2 * boxItem.xPadding; + let totalHeight = fontHeight + 2 * boxItem.yPadding; + + boxItem.cachedSize = { + width: totalWidth, + height: totalHeight + }; + + return boxItem.cachedSize; + }; + + let touchInText = function(e, boxItem) { + calcBoxSize(boxItem); + const pos = calcBoxPos(boxItem); return e.x >= pos.x1 && - e.x <= pos.x2 && - e.y >= pos.y1 && - e.y <= pos.y2; + e.x <= pos.x2 && + e.y >= pos.y1 && + e.y <= pos.y2; }; let deselectAllBoxes = function() { - Object.keys(isDragging).forEach((boxKey) => { - isDragging[boxKey] = false; - }); + isDragging = false; + for (let boxKey in boxes) { + boxes[boxKey].selected = false; + } restoreSetColor(); widgets.show(); widgets.swipeOn(); modSetColor(); }; - /** - * --------------------------------------------------------------- - * 10. Setup function to configure event handlers - * --------------------------------------------------------------- - */ - + // 10. Setup function to configure event handlers let setup = function() { - // ------------------------------------ - // Define the touchHandler function - // ------------------------------------ - touchHandler = function(zone, e) { - wasDragging = Object.assign({}, isDragging); - let boxTouched = false; - boxKeys.forEach((boxKey) => { - if (touchInText(e, boxes[boxKey], boxKey)) { - isDragging[boxKey] = true; - wasDragging[boxKey] = true; - boxTouched = true; - } - }); - if (!boxTouched) { - if (!Object.values(isDragging).some(Boolean)) { // check if no boxes are being dragged - deselectAllBoxes(); - if (doubleTapTimer) { - clearTimeout(doubleTapTimer); - doubleTapTimer = null; - // Save boxesConfig on double tap outside of any box and when no boxes are being dragged - Object.keys(boxPos).forEach((boxKey) => { - boxesConfig[boxKey].boxPos.x = (boxPos[boxKey].x / w).toFixed(3); - boxesConfig[boxKey].boxPos.y = (boxPos[boxKey].y / h).toFixed(3); - }); - storage.write(fileName, JSON.stringify(boxesConfig)); - displaySaveIcon(); - return; - } - } else { - // if any box is being dragged, just deselect all without saving - deselectAllBoxes(); - } - } - if (Object.values(wasDragging).some(Boolean) || !boxTouched) { - draw(boxes); - } - doubleTapTimer = setTimeout(() => { - doubleTapTimer = null; - }, 500); // Increase or decrease this value based on the desired double tap timing - movementDistance = 0; - }; - - // ------------------------------------ - // Define the dragHandler function - // ------------------------------------ - dragHandler = function(e) { - // Check if any box is being dragged - if (!Object.values(isDragging).some(Boolean)) return; - // Calculate the movement distance - movementDistance += Math.abs(e.dx) + Math.abs(e.dy); - // Check if the movement distance exceeds a threshold - if (movementDistance > 1) { - boxKeys.forEach((boxKey) => { - if (isDragging[boxKey]) { - widgets.hide(); - let boxItem = boxes[boxKey]; - calcBoxSize(boxItem); - let newX = boxPos[boxKey].x + e.dx; - let newY = boxPos[boxKey].y + e.dy; - if (newX - totalWidth / 2 >= 0 && - newX + totalWidth / 2 <= w && - newY - totalHeight / 2 >= 0 && - newY + totalHeight / 2 <= h ) { - boxPos[boxKey].x = newX; - boxPos[boxKey].y = newY; - } - const pos = calcBoxPos(boxKey); - g.clearRect(pos.x1, pos.y1, pos.x2, pos.y2); - } - }); - draw(boxes); - } - }; - + Bangle.on('lock', lockHandler); Bangle.on('touch', touchHandler); Bangle.on('drag', dragHandler); - + + if (boxes.step) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + Bangle.on('step', stepHandler); + } + + if (boxes.batt) { + boxes.batt.lastLevel = E.getBattery(); + boxes.batt.string = formatStr(boxes.batt, boxes.batt.lastLevel); + boxes.batt.lastUpdate = Date.now(); + } + Bangle.setUI({ - mode : "clock", - remove : function() { - // Remove event handlers, stop draw timer, remove custom font if used + mode: "clock", + remove: function() { + // Remove event handlers, stop draw timer, remove custom font Bangle.removeListener('touch', touchHandler); Bangle.removeListener('drag', dragHandler); + Bangle.removeListener('lock', lockHandler); + if (boxes.step) { + Bangle.removeListener('step', stepHandler); + } if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; delete Graphics.prototype.setFontBrunoAce; @@ -398,18 +455,14 @@ widgets.show(); } }); + loadCustomFont(); - draw(boxes); + draw(); }; - /** - * --------------------------------------------------------------- - * 11. Main execution part - * --------------------------------------------------------------- - */ - + // 11. Main execution Bangle.loadWidgets(); widgets.swipeOn(); modSetColor(); setup(); -} \ No newline at end of file +} diff --git a/apps/boxclk/boxclk-2.json b/apps/boxclk/boxclk-2.json index 64b842f1c..1bdc89252 100644 --- a/apps/boxclk/boxclk-2.json +++ b/apps/boxclk/boxclk-2.json @@ -12,14 +12,14 @@ "yOffset": 0, "boxPos": { "x": "0.5", - "y": "0.33" + "y": "0.739" } }, "dow": { "font": "6x8", "fontSize": 3, - "outline": 1, - "color": "#5ccd73", + "outline": 2, + "color": "bgH", "outlineColor": "fg", "border": "#f0f", "xPadding": -1, @@ -28,15 +28,15 @@ "yOffset": 0, "boxPos": { "x": "0.5", - "y": "0.57" + "y": "0.201" }, "short": false }, "date": { "font": "6x8", "fontSize": 2, - "outline": 1, - "color": "#5ccd73", + "outline": 2, + "color": "bgH", "outlineColor": "fg", "border": "#f0f", "xPadding": -0.5, @@ -45,7 +45,7 @@ "yOffset": 0, "boxPos": { "x": "0.5", - "y": "0.75" + "y": "0.074" }, "shortMonth": false, "disableSuffix": true @@ -63,7 +63,7 @@ "yOffset": 1, "boxPos": { "x": "0.5", - "y": "0.92" + "y": "0.926" }, "prefix": "Steps: " }, @@ -79,8 +79,8 @@ "xOffset": 2, "yOffset": 2, "boxPos": { - "x": "0.85", - "y": "0.08" + "x": "0.8", + "y": "0.427" }, "suffix": "%" } diff --git a/apps/boxclk/metadata.json b/apps/boxclk/metadata.json index dd81ac436..48f9f82ae 100644 --- a/apps/boxclk/metadata.json +++ b/apps/boxclk/metadata.json @@ -1,9 +1,10 @@ { "id": "boxclk", "name": "Box Clock", - "version": "0.05", + "version": "0.10", "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", "icon": "app.png", + "dependencies" : { "clockbg":"module" }, "screenshots": [ {"url":"screenshot.png"}, {"url":"screenshot-1.png"}, @@ -23,4 +24,4 @@ "data": [ {"name":"boxclk.json","url":"boxclk.json"} ] -} +} \ No newline at end of file diff --git a/apps/boxclk/screenshot-2.png b/apps/boxclk/screenshot-2.png index b7a73d66a..361185406 100644 Binary files a/apps/boxclk/screenshot-2.png and b/apps/boxclk/screenshot-2.png differ diff --git a/apps/bradbury/ChangeLog b/apps/bradbury/ChangeLog new file mode 100644 index 000000000..62542be60 --- /dev/null +++ b/apps/bradbury/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Minor code improvements diff --git a/apps/bradbury/app.js b/apps/bradbury/app.js index 147242689..ae018f87f 100644 --- a/apps/bradbury/app.js +++ b/apps/bradbury/app.js @@ -2,7 +2,7 @@ require("Font7x11Numeric7Seg").add(Graphics); require("Font5x9Numeric7Seg").add(Graphics); require("Font8x12").add(Graphics); require("FontDylex7x13").add(Graphics); -const X = 98, Y = 46; +//const X = 98, Y = 46; var wizible = 0; function getImg() { diff --git a/apps/bradbury/metadata.json b/apps/bradbury/metadata.json index 456daa381..6b4fb2171 100644 --- a/apps/bradbury/metadata.json +++ b/apps/bradbury/metadata.json @@ -3,7 +3,7 @@ "shortName":"Bradbury", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.01", + "version": "0.02", "description": "A watch face based on the classic Seiko model worn by one of my favorite authors. I didn't follow the original lcd layout exactly, opting for larger font for more easily readable time, and adding date, battery level, and step count; read from the device. Tapping the screen toggles visibility of widgets.", "type": "clock", "supports":["BANGLEJS2"], diff --git a/apps/breath/ChangeLog b/apps/breath/ChangeLog new file mode 100644 index 000000000..03c748ea5 --- /dev/null +++ b/apps/breath/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02: Minor code improvements +0.03: Minor code improvements diff --git a/apps/breath/app.js b/apps/breath/app.js index 380308739..f9abf9d6b 100644 --- a/apps/breath/app.js +++ b/apps/breath/app.js @@ -4,7 +4,7 @@ var max_radius = 70; var direction = 1; var display_HR = "--"; var first_signal = true; -var interval; +//var interval; var timeout; var settings; var status = 0; @@ -51,9 +51,9 @@ g.setFont("6x8", 2); function circle() { g.clear(); - adjusted_radius = max_radius * Math.abs(origin); + const adjusted_radius = max_radius * Math.abs(origin); g.drawCircle(120, 120, adjusted_radius); - radius = Math.abs(Math.sin(origin)); + //const radius = Math.abs(Math.sin(origin)); angle += 2; origin = angle * (Math.PI / 180); if (angle >= 0 && angle < 90) { @@ -63,7 +63,7 @@ function circle() { g.drawString("<<", 220, 40); status = 7; timeout = setTimeout(function () { - interval = restart_interval(); + /*interval =*/ restart_interval(); }, settings.exhale_pause * 1000); } direction = 0; @@ -77,7 +77,7 @@ function circle() { g.drawString("<<", 220, 40); status = 7; timeout = setTimeout(function () { - interval = restart_interval(); + /*interval =*/ restart_interval(); }, settings.inhale_pause * 1000); } direction = 1; @@ -100,7 +100,7 @@ function restart_interval() { if(direction == 1 && settings.ex_in_ratio == "5:6"){ calc -= calc*0.2; } - interval = setInterval(circle, calc); + /*interval =*/ setInterval(circle, calc); } function update_menu() { diff --git a/apps/breath/metadata.json b/apps/breath/metadata.json index 070a9a79a..c4280c4ad 100644 --- a/apps/breath/metadata.json +++ b/apps/breath/metadata.json @@ -2,7 +2,7 @@ "id": "breath", "name": "Breathing App", "shortName": "Breathing App", - "version": "0.01", + "version": "0.03", "description": "app to aid relaxation and train breath syncronicity using haptics and visualisation, also displays HR", "icon": "app-icon.png", "tags": "tools,health", diff --git a/apps/btadv/ChangeLog b/apps/btadv/ChangeLog index 07e67157c..c019a97b9 100644 --- a/apps/btadv/ChangeLog +++ b/apps/btadv/ChangeLog @@ -1,2 +1,4 @@ 0.01: New app! 0.02: Advertise accelerometer data and sensor location +0.03: Use the bleAdvert module +0.04: Actually use the ble_advert module diff --git a/apps/btadv/app.js b/apps/btadv/app.js index b72a8127a..457973e47 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -1,4 +1,3 @@ -var _a; { var __assign = Object.assign; var Layout_1 = require("Layout"); @@ -441,8 +440,6 @@ var _a; NRF.setServices(ad, { uart: false, }); - var bangle2 = Bangle; - var cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : []; for (var id in ad) { var serv = ad[id]; var value = void 0; @@ -450,11 +447,7 @@ var _a; value = serv[ch].value; break; } - cycle.push((_a = {}, _a[id] = value || [], _a)); + require("ble_advert").set(id, value || []); } - bangle2.bleAdvert = cycle; - NRF.setAdvertising(cycle, { - interval: 100, - }); } } diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 1d9501175..4ae75fae3 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -1,5 +1,6 @@ { -// @ts-ignore helper +// @ts-expect-error helper + const __assign = Object.assign; const Layout = require("Layout"); @@ -666,6 +667,8 @@ const getBleAdvert = (map: (s: BleServ) => T, all = false) => { // done via advertise in setServices() //const updateBleAdvert = () => { +// require("ble_advert").set(...) +// // let bleAdvert: ReturnType>; // // if (!(bleAdvert = (Bangle as any).bleAdvert)) { @@ -764,12 +767,6 @@ enableSensors(); }, ); - type BleAdvert = { [key: string]: number[] }; - const bangle2 = Bangle as { - bleAdvert?: BleAdvert | BleAdvert[]; - }; - const cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : []; - for(const id in ad){ const serv = ad[id as BleServ]; let value; @@ -780,16 +777,7 @@ enableSensors(); break; } - cycle.push({ [id]: value || [] }); + require("ble_advert").set(id, value || []); } - - bangle2.bleAdvert = cycle; - - NRF.setAdvertising( - cycle, - { - interval: 100, - } - ); } } diff --git a/apps/btadv/metadata.json b/apps/btadv/metadata.json index efe024a2f..71a0fedaf 100644 --- a/apps/btadv/metadata.json +++ b/apps/btadv/metadata.json @@ -2,7 +2,7 @@ "id": "btadv", "name": "btadv", "shortName": "btadv", - "version": "0.02", + "version": "0.04", "description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth", "icon": "icon.png", "tags": "health,tool,sensors,bluetooth", diff --git a/apps/bthome/ChangeLog b/apps/bthome/ChangeLog new file mode 100644 index 000000000..1920ee3a8 --- /dev/null +++ b/apps/bthome/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App! +0.02: Fix double-button press if you press the next button within 30s (#3243) +0.03: Cope with identical duplicate buttons (fix #3260) + Set 'n' for buttons in Bangle.btHomeData correctly (avoids adding extra buttons on end of advertising) +0.04: Fix duplicate button on edit->save +0.05: Use the bleAdvert module +0.06: button number can't be 0. Now generates number automatically diff --git a/apps/bthome/README.md b/apps/bthome/README.md new file mode 100644 index 000000000..d232e8d64 --- /dev/null +++ b/apps/bthome/README.md @@ -0,0 +1,26 @@ +# BTHome + +This uses BTHome (https://bthome.io/) to allow easy control of [Home Assistant](https://www.home-assistant.io/) via Bluetooth advertisements. + +Other apps like [the Home Assistant app](https://banglejs.com/apps/?id=ha) communicate with Home Assistant +via your phone so work from anywhere, but require being in range of your phone. + +## Usage + +When the app is installed, go to the `BTHome` app and click Settings. + +Here, you can choose if you want to advertise your Battery status, but can also click `Add Button`. + +You can then add a custom button event: + +* `Icon` - the picture for the button +* `Name` - the name associated with the button +* `Action` - the action that Home Assistant will see when this button is pressed +* `Button #` - the button event 'number' - keep this at 0 for now + +Once you've saved, you will then get your button shown in the BTHome app. Tapping it will make Bangle.js advertise via BTHome that the button has been pressed. + +## ClockInfo + +When you've added one or more buttons, they will appear in a ClockInfo under the main `Bangle.js` heading. You can just tap to select the ClockInfo, scroll down until a BTHome one is visible and then tap again. It will immediately send the Advertisement. + diff --git a/apps/bthome/app-icon.js b/apps/bthome/app-icon.js new file mode 100644 index 000000000..ecdc205bc --- /dev/null +++ b/apps/bthome/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA")) \ No newline at end of file diff --git a/apps/bthome/app.js b/apps/bthome/app.js new file mode 100644 index 000000000..6fce4ff0b --- /dev/null +++ b/apps/bthome/app.js @@ -0,0 +1,33 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +function showMenu() { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + var menu = []; + menu[""] = {title:"BTHome", back:load }; + settings.buttons.forEach((button,idx) => { + var img = require("icons").getIcon(button.icon); + menu.push({ + title : /*LANG*/"\0"+img+" "+button.name, + onchange : function() { + Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); + E.showMenu(); + E.showMessage("Sending Event"); + Bangle.buzz(); + setTimeout(showMenu, 500); + } + }); + }); + menu.push({ + title : /*LANG*/"Settings", + onchange : function() { + eval(require("Storage").read("bthome.settings.js"))(()=>showMenu()); + }}); + E.showMenu(menu); +} + +showMenu(); + + diff --git a/apps/bthome/boot.js b/apps/bthome/boot.js new file mode 100644 index 000000000..00e08df90 --- /dev/null +++ b/apps/bthome/boot.js @@ -0,0 +1,54 @@ +Bangle.btHomeData = []; +{ + require("BTHome").packetId = 0|(Math.random()*256); // random packet id so new packets show up + let settings = require("Storage").readJSON("bthome.json",1)||{}; + if (settings.showBattery) + Bangle.btHomeData.push({ + type : "battery", + v : E.getBattery() + }); + // If buttons defined, add events for them + if (settings.buttons instanceof Array) { + let n = settings.buttons.reduce((n,b)=>b.n>n?b.n:n,-1); + for (var i=0;i<=n;i++) + Bangle.btHomeData.push({type:"button_event",v:"none",n:i}); + } +} + +/* Global function to allow advertising BTHome adverts + extras = array of extra data, see require("BTHome").getAdvertisement - can add {n:0/1/2} for different instances + options = { + event : an event - advertise fast, and when connected + } +*/ +Bangle.btHome = function(extras, options) { + options = options||{}; + // clear any existing events + Bangle.btHomeData.forEach(d => {if (d.type=="button_event") d.v="none";}); + // update with extras + if (extras) { + extras.forEach(extra => { + var n = Bangle.btHomeData.find(b=>b.type==extra.type && b.n==extra.n); + if (n) Object.assign(n, extra); + else Bangle.btHomeData.push(extra); + }); + } + var bat = Bangle.btHomeData.find(b=>b.type=="battery"); + if (bat) bat.v = E.getBattery(); + var advert = require("BTHome").getAdvertisement(Bangle.btHomeData)[0xFCD2]; + // Add to the list of available advertising + var advOptions = {}; + var updateTimeout = 10*60*1000; // update every 10 minutes + if (options.event) { // if it's an event... + advOptions.interval = 50; + advOptions.whenConnected = true; + updateTimeout = 30000; // slow down in 30 seconds + } + require("ble_advert").set(0xFCD2, advert, advOptions); + if (Bangle.btHomeTimeout) clearTimeout(Bangle.btHomeTimeout); + Bangle.btHomeTimeout = setTimeout(function() { + delete Bangle.btHomeTimeout; + // update + Bangle.btHome(); + }, updateTimeout); +}; diff --git a/apps/bthome/clkinfo.js b/apps/bthome/clkinfo.js new file mode 100644 index 000000000..8698c9828 --- /dev/null +++ b/apps/bthome/clkinfo.js @@ -0,0 +1,17 @@ +(function() { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + return { + name: "Bangle", + items: settings.buttons.map(button => { + return { name : button.name, + get : function() { return { text : button.name, + img : require("icons").getIcon(button.icon) }}, + show : function() {}, + hide : function() {}, + run : function() { Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); } + } + }) + }; +}) // must not have a semi-colon! \ No newline at end of file diff --git a/apps/bthome/icon.png b/apps/bthome/icon.png new file mode 100644 index 000000000..091784477 Binary files /dev/null and b/apps/bthome/icon.png differ diff --git a/apps/bthome/metadata.json b/apps/bthome/metadata.json new file mode 100644 index 000000000..bbfcfcfe5 --- /dev/null +++ b/apps/bthome/metadata.json @@ -0,0 +1,20 @@ +{ "id": "bthome", + "name": "BTHome", + "shortName":"BTHome", + "version":"0.06", + "description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth", + "icon": "icon.png", + "type": "app", + "tags": "clkinfo,bthome,bluetooth", + "supports" : ["BANGLEJS2"], + "dependencies": {"textinput":"type", "icons":"module"}, + "readme": "README.md", + "storage": [ + {"name":"bthome.img","url":"app-icon.js","evaluate":true}, + {"name":"bthome.clkinfo.js","url":"clkinfo.js"}, + {"name":"bthome.boot.js","url":"boot.js"}, + {"name":"bthome.app.js","url":"app.js"}, + {"name":"bthome.settings.js","url":"settings.js"} + ], + "data":[{"name":"bthome.json"}] +} diff --git a/apps/bthome/settings.js b/apps/bthome/settings.js new file mode 100644 index 000000000..19a854151 --- /dev/null +++ b/apps/bthome/settings.js @@ -0,0 +1,107 @@ +(function(back) { + var settings; + + function loadSettings() { + settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + } + + function saveSettings() { + require("Storage").writeJSON("bthome.json",settings) + } + + // Get id number for button that is sent to bthome + function getNewIdNumber(){ + return [1, 2, 3, 4, 5, 6, 7, 8, 9].find(id => settings.buttons.every(button => id != button.n)); + } + + function showButtonMenu(button, isNew) { + if (!button) { + button = {name:"home", icon:"home", n:getNewIdNumber(), v:"press"}; + isNew = true; + } + var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"]; + var menu = { + "":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back: () => { + loadSettings(); // revert changes + showMenu(); + }}, + /*LANG*/"Icon" : { + value : "\0"+require("icons").getIcon(button.icon), + onchange : () => { + require("icons").showIconChooser().then(function(iconName) { + button.icon = iconName; + button.name = iconName; + showButtonMenu(button, isNew); + }, function() { + showButtonMenu(button, isNew); + }); + } + }, + /*LANG*/"Name" : { + value : button.name, + onchange : () => { + require("textinput").input({text:button.name}).then(function(name) { + button.name = name; + showButtonMenu(button, isNew); + }, function() { + showButtonMenu(button, isNew); + }); + } + }, + /*LANG*/"Action" : { + value : Math.max(0,actions.indexOf(button.v)), min:0, max:actions.length-1, + format : v => actions[v], + onchange : v => button.v=actions[v] + }, + /*LANG*/"Save" : () => { + if (isNew) settings.buttons.push(button); + saveSettings(); + showMenu(); + } + }; + if (!isNew) menu[/*LANG*/"Delete"] = function() { + E.showPrompt("Delete Button?").then(function(yes) { + if (yes) { + settings.buttons.splice(settings.buttons.indexOf(button),1); + saveSettings(); + } + showMenu(); + }); + } + E.showMenu(menu); + } + + function showMenu() { + var menu = []; + menu[""] = {title:"BTHome", back:back}; + menu.push({ + title : /*LANG*/"Show Battery", + value : !!settings.showBattery, + onchange : v=>{ + settings.showBattery = v; + saveSettings(); + } + }); + settings.buttons.forEach((button,idx) => { + var img = require("icons").getIcon(button.icon); + menu.push({ + title : /*LANG*/"Button"+(img ? " \0"+img : (idx+1)), + onchange : function() { + showButtonMenu(button, false); + } + }); + }); + menu.push({ + title : /*LANG*/"Add Button", + onchange : function() { + showButtonMenu(undefined, true); + } + }); + E.showMenu(menu); + } + + loadSettings(); + showMenu(); +}) \ No newline at end of file diff --git a/apps/bthometemp/ChangeLog b/apps/bthometemp/ChangeLog index 480780ec5..94a60aa3e 100644 --- a/apps/bthometemp/ChangeLog +++ b/apps/bthometemp/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Handle the case where other apps have set bleAdvert to an array +0.03: Use the ble_advert module diff --git a/apps/bthometemp/app.js b/apps/bthometemp/app.js index cf74c7937..f18e33c20 100644 --- a/apps/bthometemp/app.js +++ b/apps/bthometemp/app.js @@ -38,21 +38,7 @@ function onTemperature(p) { pressure100&255,(pressure100>>8)&255,pressure100>>16 ]; - if(Array.isArray(Bangle.bleAdvert)){ - var found = false; - for(var ad in Bangle.bleAdvert){ - if(ad[0xFCD2]){ - ad[0xFCD2] = advert; - found = true; - break; - } - } - if(!found) - Bangle.bleAdvert.push({ 0xFCD2: advert }); - }else{ - Bangle.bleAdvert[0xFCD2] = advert; - } - NRF.setAdvertising(Bangle.bleAdvert); + require("ble_advert").set(0xFCD2, advert); } // Gets the temperature in the most accurate way with pressure sensor @@ -60,7 +46,6 @@ function drawTemperature() { Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); } -if (!Bangle.bleAdvert) Bangle.bleAdvert = {}; setInterval(function() { drawTemperature(); }, 10000); // update every 10s diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json index 8ffb22c83..3e96d95f8 100644 --- a/apps/bthometemp/metadata.json +++ b/apps/bthometemp/metadata.json @@ -1,8 +1,8 @@ { "id": "bthometemp", "name": "BTHome Temperature and Pressure", "shortName":"BTHome T", - "version":"0.02", - "description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard", + "version":"0.03", + "description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard", "icon": "app.png", "tags": "bthome,bluetooth,temperature", "supports" : ["BANGLEJS2"], diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 000c5e3f8..1480698a2 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -41,3 +41,9 @@ Additional logging on errors Add debug option for disabling active scanning 0.17: New GUI based on layout library +0.18: Minor code improvements +0.19: Move caching of characteristics into settings app + Changed default of active scanning to false + Fix setHRMPower method not returning new state + Only buzz for disconnect after switching on if there already was an actual connection + Fix recorder not switching BTHRM on and off diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md index 6234f3b78..570072dbf 100644 --- a/apps/bthrm/README.md +++ b/apps/bthrm/README.md @@ -21,6 +21,10 @@ Once installed you will have to go into this app's settings while your heart rat **To disable this and return to normal HRM, uninstall the app or change the settings** +The characteristics of your selected sensor are cached in the settings. That means if your sensor changes, e.g. by firmware updates or similar, you will need to re-scan in the settings to update the cache of characteristics. This is done to take some complexity (and time) out of the boot process. + +Scanning in the settings will do 10 retries and then give up on adding the sensor. Usually that works fine, if it does not for you just try multiple times. Currently saved sensor information is only replaced on a successful pairing. There are additional options in the Debug entry of the menu that can help with specific sensor oddities. Bonding and active scanning can help with connecting, but can also prevent some sensors from working. The "Grace Periods" just add some additional time at certain steps in the connection process which can help with stability or reconnect speed of some finicky sensors. Defaults should be fine for most. + ### Modes * Off - Internal HRM is used, no attempt on connecting to BT HRM. @@ -57,3 +61,7 @@ This replaces `Bangle.setHRMPower` with its own implementation. ## Creator Gordon Williams + +## Contributer + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index 246b539d4..14ef531c8 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -57,7 +57,7 @@ var layout = new Layout( { { type:undefined, height:8 } //dummy to protect debug output ] }, { - lazy:true + lazy:false }); var int,agg,bt; @@ -106,8 +106,7 @@ function draw(){ layout.btContact.label = "--"; layout.btEnergy.label = "--"; } - - layout.update(); + layout.clear(); layout.render(); let first = true; for (let c of layout.l.c){ @@ -122,26 +121,29 @@ function draw(){ // This can get called for the boot code to show what's happening -function showStatusInfo(txt) { +global.showStatusInfo = function(txt) { var R = Bangle.appRect; g.reset().clearRect(R.x,R.y2-8,R.x2,R.y2).setFont("6x8"); txt = g.wrapString(txt, R.w)[0]; g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2); -} +}; function onBtHrm(e) { bt = e; bt.time = Date.now(); + draw(); } function onInt(e) { int = e; int.time = Date.now(); + draw(); } function onAgg(e) { agg = e; agg.time = Date.now(); + draw(); } var settings = require('Storage').readJSON("bthrm.json", true) || {}; @@ -162,7 +164,6 @@ Bangle.drawWidgets(); if (Bangle.setBTHRMPower){ g.reset().setFont("6x8",2).setFontAlign(0,0); g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2); - setInterval(draw, 1000); } else { g.reset().setFont("6x8",2).setFontAlign(0,0); g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2); diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json index 79605b412..e1464a9d4 100644 --- a/apps/bthrm/default.json +++ b/apps/bthrm/default.json @@ -15,8 +15,7 @@ "custom_fallbackTimeout": 10, "gracePeriodNotification": 0, "gracePeriodConnect": 0, - "gracePeriodService": 0, "gracePeriodRequest": 0, "bonding": false, - "active": true + "active": false } diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js index a792167ca..b81bd6118 100644 --- a/apps/bthrm/lib.js +++ b/apps/bthrm/lib.js @@ -1,14 +1,14 @@ exports.enable = () => { - var settings = Object.assign( + let settings = Object.assign( require('Storage').readJSON("bthrm.default.json", true) || {}, require('Storage').readJSON("bthrm.json", true) || {} ); - var log = function(text, param){ + let log = function(text, param){ if (global.showStatusInfo) - showStatusInfo(text); + global.showStatusInfo(text); if (settings.debuglog){ - var logline = new Date().toISOString() + " - " + text; + let logline = new Date().toISOString() + " - " + text; if (param) logline += ": " + JSON.stringify(param); print(logline); } @@ -16,60 +16,33 @@ exports.enable = () => { log("Settings: ", settings); - if (settings.enabled){ + //this is for compatibility with 0.18 and older + let oldCache = require('Storage').readJSON("bthrm.cache.json", true); + if(oldCache){ + settings.cache = oldCache; + require('Storage').writeJSON("bthrm.json", settings); + require('Storage').erase("bthrm.cache.json"); + } - var clearCache = function() { - return require('Storage').erase("bthrm.cache.json"); - }; + if (settings.enabled && settings.cache){ - var getCache = function() { - var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; - if (settings.btid && settings.btid === cache.id) return cache; - clearCache(); - return {}; - }; + log("Start"); - var addNotificationHandler = function(characteristic) { + let addNotificationHandler = function(characteristic) { log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); }; - var writeCache = function(cache) { - var oldCache = getCache(); - if (oldCache !== cache) { - log("Writing cache"); - require('Storage').writeJSON("bthrm.cache.json", cache); - } else { - log("No changes, don't write cache"); - } - }; - var characteristicsToCache = function(characteristics) { - log("Cache characteristics"); - var cache = getCache(); - if (!cache.characteristics) cache.characteristics = {}; - for (var c of characteristics){ - //"handle_value":16,"handle_decl":15 - log("Saving handle " + c.handle_value + " for characteristic: ", c); - cache.characteristics[c.uuid] = { - "handle": c.handle_value, - "uuid": c.uuid, - "notify": c.properties.notify, - "read": c.properties.read - }; - } - writeCache(cache); - }; - - var characteristicsFromCache = function(device) { - var service = { device : device }; // fake a BluetoothRemoteGATTService + let characteristicsFromCache = function(device) { + let service = { device : device }; // fake a BluetoothRemoteGATTService log("Read cached characteristics"); - var cache = getCache(); + let cache = settings.cache; if (!cache.characteristics) return []; - var restored = []; - for (var c in cache.characteristics){ - var cached = cache.characteristics[c]; - var r = new BluetoothRemoteGATTCharacteristic(); + let restored = []; + for (let c in cache.characteristics){ + let cached = cache.characteristics[c]; + let r = new BluetoothRemoteGATTCharacteristic(); log("Restoring characteristic ", cached); r.handle_value = cached.handle; r.uuid = cached.uuid; @@ -84,26 +57,14 @@ exports.enable = () => { return restored; }; - log("Start"); - - var lastReceivedData={ - }; - - var supportedServices = [ - "0x180d", // Heart Rate - "0x180f", // Battery - ]; - - var bpmTimeout; - - var supportedCharacteristics = { + let supportedCharacteristics = { "0x2a37": { //Heart rate measurement active: false, handler: function (dv){ - var flags = dv.getUint8(0); + let flags = dv.getUint8(0); - var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit + let bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit supportedCharacteristics["0x2a37"].active = bpm > 0; log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active); switchFallback(); @@ -114,42 +75,42 @@ exports.enable = () => { startFallback(); }, 3000); - var sensorContact; + let sensorContact; if (flags & 2){ sensorContact = !!(flags & 4); } - var idx = 2 + (flags&1); + let idx = 2 + (flags&1); - var energyExpended; + let energyExpended; if (flags & 8){ energyExpended = dv.getUint16(idx,1); idx += 2; } - var interval; + let interval; if (flags & 16) { interval = []; - var maxIntervalBytes = (dv.byteLength - idx); + let maxIntervalBytes = (dv.byteLength - idx); log("Found " + (maxIntervalBytes / 2) + " rr data fields"); - for(var i = 0 ; i < maxIntervalBytes / 2; i++){ + for(let i = 0 ; i < maxIntervalBytes / 2; i++){ interval[i] = dv.getUint16(idx,1); // in milliseconds idx += 2; } } - var location; + let location; if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){ location = lastReceivedData["0x180d"]["0x2a38"]; } - var battery; + let battery; if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){ battery = lastReceivedData["0x180f"]["0x2a19"]; } if (settings.replace && bpm > 0){ - var repEvent = { + let repEvent = { bpm: bpm, confidence: (sensorContact || sensorContact === undefined)? 100 : 0, src: "bthrm" @@ -159,7 +120,7 @@ exports.enable = () => { Bangle.emit("HRM_R", repEvent); } - var newEvent = { + let newEvent = { bpm: bpm }; @@ -177,6 +138,7 @@ exports.enable = () => { //Body sensor location handler: function(dv){ if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; + log("Got location", dv); lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); } }, @@ -184,26 +146,27 @@ exports.enable = () => { //Battery handler: function (dv){ if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {}; + log("Got battery", dv); lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0); } } }; - var device; - var gatt; - var characteristics = []; - var blockInit = false; - var currentRetryTimeout; - var initialRetryTime = 40; - var maxRetryTime = 60000; - var retryTime = initialRetryTime; - - var connectSettings = { - minInterval: 7.5, - maxInterval: 1500 + let lastReceivedData={ }; - var waitingPromise = function(timeout) { + let bpmTimeout; + + let device; + let gatt; + let characteristics = []; + let blockInit = false; + let currentRetryTimeout; + let initialRetryTime = 40; + let maxRetryTime = 60000; + let retryTime = initialRetryTime; + + let waitingPromise = function(timeout) { return new Promise(function(resolve){ log("Start waiting for " + timeout); setTimeout(()=>{ @@ -240,7 +203,7 @@ exports.enable = () => { }; } - var clearRetryTimeout = function(resetTime) { + let clearRetryTimeout = function(resetTime) { if (currentRetryTimeout){ log("Clearing timeout " + currentRetryTimeout); clearTimeout(currentRetryTimeout); @@ -252,12 +215,12 @@ exports.enable = () => { } }; - var retry = function() { + let retry = function() { log("Retry"); if (!currentRetryTimeout && !powerdownRequested){ - var clampedTime = retryTime < 100 ? 100 : retryTime; + let clampedTime = retryTime < 100 ? 100 : retryTime; log("Set timeout for retry as " + clampedTime); clearRetryTimeout(); @@ -276,20 +239,21 @@ exports.enable = () => { } }; - var buzzing = false; - var onDisconnect = function(reason) { + let initialDisconnects = true; + let buzzing = false; + let onDisconnect = function(reason) { log("Disconnect: " + reason); log("GATT", gatt); log("Characteristics", characteristics); - var retryTimeResetNeeded = true; + let retryTimeResetNeeded = true; retryTimeResetNeeded &= reason != "Connection Timeout"; retryTimeResetNeeded &= reason != "No device found matching filters"; clearRetryTimeout(retryTimeResetNeeded); supportedCharacteristics["0x2a37"].active = false; if (!powerdownRequested) startFallback(); blockInit = false; - if (settings.warnDisconnect && !buzzing){ + if (settings.warnDisconnect && !buzzing && !initialDisconnects){ buzzing = true; Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;}); } @@ -298,9 +262,9 @@ exports.enable = () => { } }; - var createCharacteristicPromise = function(newCharacteristic) { + let createCharacteristicPromise = function(newCharacteristic) { log("Create characteristic promise", newCharacteristic); - var result = Promise.resolve(); + let result = Promise.resolve(); // For values that can be read, go ahead and read them, even if we might be notified in the future // Allows for getting initial state of infrequently updating characteristics, like battery if (newCharacteristic.readValue){ @@ -316,58 +280,29 @@ exports.enable = () => { if (newCharacteristic.properties.notify){ result = result.then(()=>{ log("Starting notifications", newCharacteristic); - var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); - - log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); - startPromise = startPromise.then(()=>{ - log("Wait after connect"); - return waitingPromise(settings.gracePeriodNotification); - }); - + let startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); + + if (settings.gracePeriodNotification){ + log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); + startPromise = startPromise.then(()=>{ + log("Wait after connect"); + return waitingPromise(settings.gracePeriodNotification); + }); + } return startPromise; }); } return result.then(()=>log("Handled characteristic", newCharacteristic)); }; - var attachCharacteristicPromise = function(promise, characteristic) { + let attachCharacteristicPromise = function(promise, characteristic) { return promise.then(()=>{ log("Handling characteristic:", characteristic); return createCharacteristicPromise(characteristic); }); }; - var createCharacteristicsPromise = function(newCharacteristics) { - log("Create characteristics promis ", newCharacteristics); - var result = Promise.resolve(); - for (var c of newCharacteristics){ - if (!supportedCharacteristics[c.uuid]) continue; - log("Supporting characteristic", c); - characteristics.push(c); - if (c.properties.notify){ - addNotificationHandler(c); - } - - result = attachCharacteristicPromise(result, c); - } - return result.then(()=>log("Handled characteristics")); - }; - - var createServicePromise = function(service) { - log("Create service promise", service); - var result = Promise.resolve(); - result = result.then(()=>{ - log("Handling service" + service.uuid); - return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); - }); - return result.then(()=>log("Handled service" + service.uuid)); - }; - - var attachServicePromise = function(promise, service) { - return promise.then(()=>createServicePromise(service)); - }; - - var initBt = function () { + let initBt = function () { log("initBt with blockInit: " + blockInit); if (blockInit && !powerdownRequested){ retry(); @@ -376,8 +311,8 @@ exports.enable = () => { blockInit = true; - var promise; - var filters; + let promise; + let filters; if (!device){ if (settings.btid){ @@ -394,9 +329,13 @@ exports.enable = () => { onDisconnect(e); return; } - + if (settings.gracePeriodRequest){ log("Add " + settings.gracePeriodRequest + "ms grace period after request"); + promise = promise.then((d)=>{ + log("Wait after request"); + return waitingPromise(settings.gracePeriodRequest).then(()=>Promise.resolve(d)); + }); } promise = promise.then((d)=>{ @@ -404,100 +343,42 @@ exports.enable = () => { d.on('gattserverdisconnected', onDisconnect); device = d; }); - - promise = promise.then(()=>{ - log("Wait after request"); - return waitingPromise(settings.gracePeriodRequest); - }); } else { promise = Promise.resolve(); log("Reuse device", device); } promise = promise.then(()=>{ - if (gatt){ - log("Reuse GATT", gatt); - } else { - log("GATT is new", gatt); - characteristics = []; - var cachedId = getCache().id; - if (device.id !== cachedId){ - log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache"); - clearCache(); - } - var newCache = getCache(); - newCache.id = device.id; - writeCache(newCache); - gatt = device.gatt; - } - + gatt = device.gatt; return Promise.resolve(gatt); }); promise = promise.then((gatt)=>{ if (!gatt.connected){ log("Connecting..."); - var connectPromise = gatt.connect(connectSettings).then(function() { + let connectPromise = gatt.connect().then(function() { log("Connected."); }); - log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); - connectPromise = connectPromise.then(()=>{ - log("Wait after connect"); - return waitingPromise(settings.gracePeriodConnect); - }); + if (settings.gracePeriodConnect){ + log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); + connectPromise = connectPromise.then(()=>{ + log("Wait after connect"); + return waitingPromise(settings.gracePeriodConnect); + }); + } return connectPromise; } else { return Promise.resolve(); } }); - - if (settings.bonding){ - promise = promise.then(() => { - log(JSON.stringify(gatt.getSecurityStatus())); - if (gatt.getSecurityStatus()['bonded']) { - log("Already bonded"); - return Promise.resolve(); - } else { - log("Start bonding"); - return gatt.startBonding() - .then(() => log("Security status" + gatt.getSecurityStatus())); - } - }); - } promise = promise.then(()=>{ - if (!characteristics || characteristics.length === 0){ + if (!characteristics || characteristics.length == 0){ characteristics = characteristicsFromCache(device); } - }); - - promise = promise.then(()=>{ - var characteristicsPromise = Promise.resolve(); - if (characteristics.length === 0){ - characteristicsPromise = characteristicsPromise.then(()=>{ - log("Getting services"); - return gatt.getPrimaryServices(); - }); - - characteristicsPromise = characteristicsPromise.then((services)=>{ - log("Got services", services); - var result = Promise.resolve(); - for (var service of services){ - if (!(supportedServices.includes(service.uuid))) continue; - log("Supporting service", service.uuid); - result = attachServicePromise(result, service); - } - log("Add " + settings.gracePeriodService + "ms grace period after services"); - result = result.then(()=>{ - log("Wait after services"); - return waitingPromise(settings.gracePeriodService); - }); - return result; - }); - } else { - for (var characteristic of characteristics){ - characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); - } + let characteristicsPromise = Promise.resolve(); + for (let characteristic of characteristics){ + characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); } return characteristicsPromise; @@ -505,7 +386,7 @@ exports.enable = () => { return promise.then(()=>{ log("Connection established, waiting for notifications"); - characteristicsToCache(characteristics); + initialDisconnects = false; clearRetryTimeout(true); }).catch((e) => { characteristics = []; @@ -514,7 +395,7 @@ exports.enable = () => { }); }; - var powerdownRequested = false; + let powerdownRequested = false; Bangle.setBTHRMPower = function(isOn, app) { // Do app power handling @@ -526,6 +407,7 @@ exports.enable = () => { isOn = Bangle._PWR.BTHRM.length; // so now we know if we're really on if (isOn) { + initialDisconnects = true; powerdownRequested = false; switchFallback(); if (!Bangle.isBTHRMConnected()) initBt(); @@ -598,17 +480,18 @@ exports.enable = () => { Bangle.setBTHRMPower(0); if (!isOn) stopFallback(); } + return Bangle.isBTHRMOn() || Bangle.isHRMOn(); } if ((settings.enabled && !settings.replace) || !settings.enabled){ - Bangle.origSetHRMPower(isOn, app); + return Bangle.origSetHRMPower(isOn, app); } }; } - var fallbackActive = false; - var inSwitch = false; + let fallbackActive = false; + let inSwitch = false; - var stopFallback = function(){ + let stopFallback = function(){ if (fallbackActive){ Bangle.origSetHRMPower(0, "bthrm_fallback"); fallbackActive = false; @@ -616,7 +499,7 @@ exports.enable = () => { } }; - var startFallback = function(){ + let startFallback = function(){ if (!fallbackActive && settings.allowFallback) { fallbackActive = true; Bangle.origSetHRMPower(1, "bthrm_fallback"); @@ -624,7 +507,7 @@ exports.enable = () => { } }; - var switchFallback = function() { + let switchFallback = function() { log("Check falling back to HRM"); if (!inSwitch){ inSwitch = true; @@ -640,8 +523,8 @@ exports.enable = () => { if (settings.replace){ log("Replace HRM event"); if (Bangle._PWR && Bangle._PWR.HRM){ - for (var i = 0; i < Bangle._PWR.HRM.length; i++){ - var app = Bangle._PWR.HRM[i]; + for (let i = 0; i < Bangle._PWR.HRM.length; i++){ + let app = Bangle._PWR.HRM[i]; log("Moving app " + app); Bangle.origSetHRMPower(0, app); Bangle.setBTHRMPower(1, app); diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 6c31759c9..56e1beffd 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,7 +2,7 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.17", + "version": "0.19", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "screenshots": [{"url":"screen.png"}], diff --git a/apps/bthrm/recorder.js b/apps/bthrm/recorder.js index fcfed47c3..ec880d553 100644 --- a/apps/bthrm/recorder.js +++ b/apps/bthrm/recorder.js @@ -16,7 +16,7 @@ name : "BT HR", fields : ["BT Heartrate", "BT Battery", "Energy expended", "Contact", "RR"], getValues : () => { - result = [bpm,bat,energy,contact,rr]; + const result = [bpm,bat,energy,contact,rr]; bpm = ""; rr = ""; bat = ""; @@ -26,11 +26,11 @@ }, start : () => { Bangle.on('BTHRM', onHRM); - if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder"); + if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(1,"recorder"); }, stop : () => { Bangle.removeListener('BTHRM', onHRM); - if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder"); + if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(0,"recorder"); }, draw : (x,y) => g.setColor((Bangle.isBTHRMActive && Bangle.isBTHRMActive())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) }; diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js index 459ed29fc..68e958db8 100644 --- a/apps/bthrm/settings.js +++ b/apps/bthrm/settings.js @@ -1,6 +1,6 @@ (function(back) { function writeSettings(key, value) { - var s = require('Storage').readJSON(FILE, true) || {}; + let s = require('Storage').readJSON(FILE, true) || {}; s[key] = value; require('Storage').writeJSON(FILE, s); readSettings(); @@ -13,10 +13,14 @@ ); } - var FILE="bthrm.json"; - var settings; + let FILE="bthrm.json"; + let settings; readSettings(); + let log = ()=>{}; + if (settings.debuglog) + log = print; + function applyCustomSettings(){ writeSettings("enabled",true); writeSettings("replace",settings.custom_replace); @@ -26,7 +30,7 @@ } function buildMainMenu(){ - var mainmenu = { + let mainmenu = { '': { 'title': 'Bluetooth HRM' }, '< Back': back, 'Mode': { @@ -63,12 +67,13 @@ }; if (settings.btname || settings.btid){ - var name = "Clear " + (settings.btname || settings.btid); + let name = "Clear " + (settings.btname || settings.btid); mainmenu[name] = function() { E.showPrompt("Clear current device?").then((r)=>{ if (r) { writeSettings("btname",undefined); writeSettings("btid",undefined); + writeSettings("cache", undefined); } E.showMenu(buildMainMenu()); }); @@ -81,7 +86,7 @@ return mainmenu; } - var submenu_debug = { + let submenu_debug = { '' : { title: "Debug"}, '< Back': function() { E.showMenu(buildMainMenu()); }, 'Alert on disconnect': { @@ -111,11 +116,135 @@ 'Grace periods': function() { E.showMenu(submenu_grace); } }; + let supportedServices = [ + "0x180d", // Heart Rate + "0x180f", // Battery + ]; + + let supportedCharacteristics = [ + "0x2a37", // Heart Rate + "0x2a38", // Body sensor location + "0x2a19", // Battery + ]; + + var characteristicsToCache = function(characteristics) { + log("Cache characteristics"); + let cache = {}; + if (!cache.characteristics) cache.characteristics = {}; + for (var c of characteristics){ + //"handle_value":16,"handle_decl":15 + log("Saving handle " + c.handle_value + " for characteristic: ", c.uuid); + cache.characteristics[c.uuid] = { + "handle": c.handle_value, + "uuid": c.uuid, + "notify": c.properties.notify, + "read": c.properties.read + }; + } + writeSettings("cache", cache); + }; + + let createCharacteristicPromise = function(newCharacteristic) { + log("Create characteristic promise", newCharacteristic.uuid); + return Promise.resolve().then(()=>log("Handled characteristic", newCharacteristic.uuid)); + }; + + let attachCharacteristicPromise = function(promise, characteristic) { + return promise.then(()=>{ + log("Handling characteristic:", characteristic.uuid); + return createCharacteristicPromise(characteristic); + }); + }; + + let characteristics; + + let createCharacteristicsPromise = function(newCharacteristics) { + log("Create characteristics promise ", newCharacteristics.length); + let result = Promise.resolve(); + for (let c of newCharacteristics){ + if (!supportedCharacteristics.includes(c.uuid)) continue; + log("Supporting characteristic", c.uuid); + characteristics.push(c); + + result = attachCharacteristicPromise(result, c); + } + return result.then(()=>log("Handled characteristics")); + }; + + let createServicePromise = function(service) { + log("Create service promise", service.uuid); + let result = Promise.resolve(); + result = result.then(()=>{ + log("Handling service", service.uuid); + return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); + }); + return result.then(()=>log("Handled service", service.uuid)); + }; + + let attachServicePromise = function(promise, service) { + return promise.then(()=>createServicePromise(service)); + }; + + function cacheDevice(deviceId){ + let promise; + let filters; + let gatt; + characteristics = []; + filters = [{ id: deviceId }]; + + log("Requesting device with filters", filters); + promise = NRF.requestDevice({ filters: filters, active: settings.active }); + + promise = promise.then((d)=>{ + log("Got device", d); + gatt = d.gatt; + log("Connecting..."); + return gatt.connect().then(function() { + log("Connected."); + }); + }); + + if (settings.bonding){ + promise = promise.then(() => { + log(JSON.stringify(gatt.getSecurityStatus())); + if (gatt.getSecurityStatus().bonded) { + log("Already bonded"); + return Promise.resolve(); + } else { + log("Start bonding"); + return gatt.startBonding() + .then(() => log("Security status after bonding" + gatt.getSecurityStatus())); + } + }); + } + + promise = promise.then(()=>{ + log("Getting services"); + return gatt.getPrimaryServices(); + }); + + promise = promise.then((services)=>{ + log("Got services", services.length); + let result = Promise.resolve(); + for (let service of services){ + if (!(supportedServices.includes(service.uuid))) continue; + log("Supporting service", service.uuid); + result = attachServicePromise(result, service); + } + return result; + }); + + return promise.then(()=>{ + log("Connection established, saving cache"); + characteristicsToCache(characteristics); + }); + } + function createMenuFromScan(){ E.showMenu(); E.showMessage("Scanning for 4 seconds"); - var submenu_scan = { + let submenu_scan = { '< Back': function() { E.showMenu(buildMainMenu()); } }; NRF.findDevices(function(devices) { @@ -126,18 +255,41 @@ return; } else { devices.forEach((d) => { - print("Found device", d); - var shown = (d.name || d.id.substr(0, 17)); + log("Found device", d); + let shown = (d.name || d.id.substr(0, 17)); submenu_scan[shown] = function () { - E.showPrompt("Set " + shown + "?").then((r) => { + E.showPrompt("Connect to\n" + shown + "?", {title: "Pairing"}).then((r) => { if (r) { - writeSettings("btid", d.id); - // Store the name for displaying later. Will connect by ID - if (d.name) { - writeSettings("btname", d.name); - } + E.showMessage("Connecting...", {img:require("Storage").read("bthrm.img")}); + let count = 0; + const successHandler = ()=>{ + E.showPrompt("Success!", { + img:require("Storage").read("bthrm.img"), + buttons: { "OK":true } + }).then(()=>{ + writeSettings("btid", d.id); + // Store the name for displaying later. Will connect by ID + if (d.name) { + writeSettings("btname", d.name); + } + E.showMenu(buildMainMenu()); + }); + }; + const errorHandler = (e)=>{ + count++; + log("ERROR", e); + if (count <= 10){ + E.showMessage("Error during caching\nRetry " + count + "/10", e); + return cacheDevice(d.id).then(successHandler).catch(errorHandler); + } else { + E.showAlert("Error during caching", e).then(()=>{ + E.showMenu(buildMainMenu()); + }); + } + }; + + return cacheDevice(d.id).then(successHandler).catch(errorHandler); } - E.showMenu(buildMainMenu()); }); }; }); @@ -146,7 +298,7 @@ }, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]}); } - var submenu_custom = { + let submenu_custom = { '' : { title: "Custom mode"}, '< Back': function() { E.showMenu(buildMainMenu()); }, 'Replace HRM': { @@ -183,7 +335,7 @@ }, }; - var submenu_grace = { + let submenu_grace = { '' : { title: "Grace periods"}, '< Back': function() { E.showMenu(submenu_debug); }, 'Request': { @@ -215,16 +367,6 @@ onchange: v => { writeSettings("gracePeriodNotification",v); } - }, - 'Service': { - value: settings.gracePeriodService, - min: 0, - max: 3000, - step: 100, - format: v=>v+"ms", - onchange: v => { - writeSettings("gracePeriodService",v); - } } }; diff --git a/apps/bthrv/ChangeLog b/apps/bthrv/ChangeLog index eefadac78..e4dc0f14d 100644 --- a/apps/bthrv/ChangeLog +++ b/apps/bthrv/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Write available data on reset or kill 0.03: Buzz short on every finished measurement and longer if all are done +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/bthrv/app.js b/apps/bthrv/app.js index fbd0e2d05..8378ba025 100644 --- a/apps/bthrv/app.js +++ b/apps/bthrv/app.js @@ -1,4 +1,4 @@ -var btm = g.getHeight()-1; +//var btm = g.getHeight()-1; var ui = false; function clear(y){ @@ -10,7 +10,7 @@ var startingTime; var currentSlot = 0; var hrvSlots = [10,20,30,60,120,300]; var hrvValues = {}; -var rrRmsProgress; +//var rrRmsProgress; var rrNumberOfValues = 0; var rrSquared = 0; @@ -120,7 +120,7 @@ function resetHrv(){ } -var settings = require('Storage').readJSON("bthrm.json", true) || {}; +//var settings = require('Storage').readJSON("bthrm.json", true) || {}; g.clear(); Bangle.loadWidgets(); diff --git a/apps/bthrv/metadata.json b/apps/bthrv/metadata.json index 7c57be682..e20e6bfb8 100644 --- a/apps/bthrv/metadata.json +++ b/apps/bthrv/metadata.json @@ -2,7 +2,7 @@ "id": "bthrv", "name": "Bluetooth Heart Rate variance calculator", "shortName": "BT HRV", - "version": "0.03", + "version": "0.05", "description": "Calculates HRV from a a BT HRM with interval data", "icon": "app.png", "type": "app", diff --git a/apps/bthrv/recorder.js b/apps/bthrv/recorder.js index 0fce6971e..f1e1d5040 100644 --- a/apps/bthrv/recorder.js +++ b/apps/bthrv/recorder.js @@ -31,7 +31,7 @@ } } } - result = [hrv]; + const result = [hrv]; hrv = ""; rrHistory = []; return result; diff --git a/apps/buffgym/.eslintrc.json b/apps/buffgym/.eslintrc.json deleted file mode 100644 index aaae0a0cb..000000000 --- a/apps/buffgym/.eslintrc.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "env": { - "browser": true, - "commonjs": true, - "es6": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "rules": { - "indent": [ - "error", - 2, - { "SwitchCase": 1 } - ], - "linebreak-style": [ - "error", - "windows" - ], - "quotes": [ - "error", - "double" - ] - /*, - "semi": [ - "error", - "always" - ]*/ - } -} \ No newline at end of file diff --git a/apps/burn/.gitignore b/apps/burn/.gitignore new file mode 100644 index 000000000..656b79624 --- /dev/null +++ b/apps/burn/.gitignore @@ -0,0 +1 @@ +.prettierignore \ No newline at end of file diff --git a/apps/burn/ChangeLog b/apps/burn/ChangeLog new file mode 100644 index 000000000..66c4f98bd --- /dev/null +++ b/apps/burn/ChangeLog @@ -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 diff --git a/apps/burn/README.md b/apps/burn/README.md new file mode 100644 index 000000000..44f1e260f --- /dev/null +++ b/apps/burn/README.md @@ -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. diff --git a/apps/burn/app-icon.js b/apps/burn/app-icon.js new file mode 100644 index 000000000..8cd3b7ca1 --- /dev/null +++ b/apps/burn/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/gUA///j32o8h9v6glA+P9+/3tu27YCLvICBgEGCJlFmwRBgALFxQRIgIdF28JmIIEknG7cMyVJk4nBC4PJk4dBC4OJkmSrYRCkuACQM26/88wRGgQHB2iGCm33//8GoXtonbraYGgwRB/+bNY4AEg9/CIPbth3BxYRJn4RB/YRBgEUTwIRGne275IBCIQABjYRGrpCB+VK1gJDgYQFgXN23YQwIjEAA0WMwPQ0mSqgRK2QRBy6cBCJUFGIO12wjBpgRMlsAqmSqTOCAA0sCINogEIyVKCJdLoEAhQRNpQFCyVII5IRGqARQNZUECIcGyRLBPpPSCIQWBsCzK3VJoEB0mTCBUAitplEA0WYCJb7B1EBCYIAsjJjCknDpMkyUAmlKNwuEyEAgMSwwQBpNhAQM4CAcDkgRBe4ODogOB4MT0MldgcxCIXEyWAi3axNgykECIcBxIRBEwIRBYoK3BykGkw1DxPEyEZksSCIMEpcDjAIBGocbhMQiMzCIUyqALB5KmFhMghMk0VLQANYPoLeBCI0SRgOKJYOAgOSpihFCIMaCIOTgMk4ACBqVICIyTBKIMZkkAhpyBo4RHgOk4EZPAIjByVFNYYIBAoU2AoOwAQO5kmMf7QALpMg2VJ23Mn////8OIVThmUCIs27FvCIP+eQOSpUIyXAgFNEYfQCIX8P4OSp0MCIVZgEWFoNgrMktuwgdt23YOIQ")) \ No newline at end of file diff --git a/apps/burn/app-icon.png b/apps/burn/app-icon.png new file mode 100644 index 000000000..23d4a13e6 Binary files /dev/null and b/apps/burn/app-icon.png differ diff --git a/apps/burn/app-screenshot.png b/apps/burn/app-screenshot.png new file mode 100644 index 000000000..fef0e701e Binary files /dev/null and b/apps/burn/app-screenshot.png differ diff --git a/apps/burn/app.js b/apps/burn/app.js new file mode 100644 index 000000000..348a19153 --- /dev/null +++ b/apps/burn/app.js @@ -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(); diff --git a/apps/burn/metadata.json b/apps/burn/metadata.json new file mode 100644 index 000000000..c032058c8 --- /dev/null +++ b/apps/burn/metadata.json @@ -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} + ] +} diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js index 116253fda..8bcf0ae0f 100644 --- a/apps/bwclk/settings.js +++ b/apps/bwclk/settings.js @@ -32,7 +32,6 @@ }, 'Show Lock': { value: settings.showLock, - format: () => (settings.showLock ? 'Yes' : 'No'), onchange: () => { settings.showLock = !settings.showLock; save(); @@ -40,7 +39,6 @@ }, 'Hide Colon': { value: settings.hideColon, - format: () => (settings.hideColon ? 'Yes' : 'No'), onchange: () => { settings.hideColon = !settings.hideColon; save(); diff --git a/apps/bwclklite/settings.js b/apps/bwclklite/settings.js index 2d3916a3d..4c59198c6 100644 --- a/apps/bwclklite/settings.js +++ b/apps/bwclklite/settings.js @@ -32,7 +32,6 @@ }, 'Show Lock': { value: settings.showLock, - format: () => (settings.showLock ? 'Yes' : 'No'), onchange: () => { settings.showLock = !settings.showLock; save(); @@ -40,7 +39,6 @@ }, 'Hide Colon': { value: settings.hideColon, - format: () => (settings.hideColon ? 'Yes' : 'No'), onchange: () => { settings.hideColon = !settings.hideColon; save(); diff --git a/apps/c25k/ChangeLog b/apps/c25k/ChangeLog new file mode 100644 index 000000000..0e7594334 --- /dev/null +++ b/apps/c25k/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add rep info to time screen +0.03: Add option to pause/resume workout (Bangle.js 1 only) +0.04: Add possibility of creating a custom exercise diff --git a/apps/c25k/README.md b/apps/c25k/README.md new file mode 100644 index 000000000..8237199d5 --- /dev/null +++ b/apps/c25k/README.md @@ -0,0 +1,96 @@ +# C25K + +Unofficial app for the Couch to 5k training plan. +From being a couch-potato to running 5k in 8 weeks! + +Each week has 3 training days, ideally with rest days between them. + +Each day's programme consists of running for a certain time with occasional walking/resting phases. +When walking is part of the programme, the (run+walk) stages are repeated a number of times. + +![](c25k-scrn1.png) +![](c25k-scrn2.png) +![](c25k-scrn3.png) + +## Features + +- Show remaining time in seconds for each phase +- Vibrates on phase changes +- Keeps screen on to allow quickly glancing at the time while running +- Shows time on button press + +## Usage + +If you know the week and day of the programme you'd like to start, set `Week` and `Day` to the appropriate values in the main menu and press `Start`. + +**Example**: +To start the programme of the **second day** of **week 4**: +![](c25k-scrn4.png) + +--- + +Alternatively, you can go to the `View plan` menu to look at all the programmes and select the one you'd like to start. + +**Example**: +Go to the `View plan` menu: +![](c25k-scrn5.png) + +Select the programme to start it: +![](c25k-scrn6.png) + +--- + +The format of the `View menu` is `w{week}d{day}(r:{run mins}|w:{walk mins}|x{number of reps})`. + +For example `w6d1(r:6|w:3|x2)` means: +`it's the programme of day 1 on week 6`, +`it consists of running for 6 minutes`, +`followed by walking for 3`, +`done 2 times back to back`. + +--- + +### Create a custom excercise + +Under the `Custom run` menu, it's possible to create a custom excercise. +![](c25k-scrn9.png) + +Some important details/limitations: + +- To disable walking: set `walk` to `0` +- When walking is set to `0`, the repetition count is set to `1`. +- When repetition is set to `2` or higher, `walk` is set to `1`. + +**Unfortunately, the value in the menu do not update to reflect the changes, so I recommend setting the values with the rules above in mind.** + +--- + +### Show extra info: + +If you ever need to peek at the time, just press the middle (or only) physical button on the watch: +![](c25k-scrn7.png) + +This view also shows `current rep / total reps` at the top. + +--- + +### Pause/resume workout: + +**This is currently only available on Bangle.js 1.** + +Press the top button to pause or to resume the active programme: +![](c25k-scrn8.png) + +--- + +## Disclaimer + +This app was hacked together in a day with no JS knowledge. +It's probably inefficient and buggy, but it does what I needed it to do: allow me to follow the C25K programme without a phone. + +The app was designed with a Bangle.js 1 in mind, as that's the one I have. +It *should* work fine on the Bangle.js 2, but I couldn't test it on real hardware. + +--- + +Made with <3 by [Erovia](https://github.com/Erovia/BangleApps/tree/c25k) diff --git a/apps/c25k/app-icon.js b/apps/c25k/app-icon.js new file mode 100644 index 000000000..6b85dbf29 --- /dev/null +++ b/apps/c25k/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoPk9G9gsj14lZhWq0AEBgtVqALmhQJBAQMFBIICCBc4ADBQYLnAQQKEBcibETQIABHggLiAEQqEh/wgACCBcpXDBAIKDBcqJDh//BQYLkHwg7GBcY7FU5ALgAEQA=")) diff --git a/apps/c25k/app.js b/apps/c25k/app.js new file mode 100644 index 000000000..eed918e46 --- /dev/null +++ b/apps/c25k/app.js @@ -0,0 +1,290 @@ +var week = 1; // Stock plan: programme week +var day = 1; // Stock plan: programe day +var run = 1; // Custom plan: running time +var walk = 0; // Custom plan: walking time +var reps = 1; // Custom plan: repetition count + +var time; // To store the date + +var loop; // To store how many times we will have to do a countdown +var rep; // The current rep counter +var counter; // The seconds counter +var currentMode; // Either "run" or "walk" +var mainInterval; // Ticks every second, checking if a new countdown is needed +var activityInterval; // Ticks every second, doing the countdown +var extraInfoWatch; // Watch for button presses to show additional info +var paused = false; // Track pause state +var pauseOrResumeWatch; // Watch for button presses to pause/resume countdown +var defaultFontSize = (process.env.HWVERSION == 2) ? 7 : 9; // Default font size, Banglejs 2 has smaller +var activityBgColour; // Background colour of current activity +var currentActivity; // To store the current activity + +function outOfTime() { + buzz(); + + // Once we're done + if (loop == 0) { + clearWatch(extraInfoWatch); // Don't watch for button presses anymore + if (pauseOrResumeWatch) clearWatch(pauseOrResumeWatch); // Don't watch for button presses anymore + g.setBgColor("#75C0E0"); // Blue background for the "Done" text + drawText("Done", defaultFontSize); // Write "Done" to screen + g.reset(); + setTimeout(E.showMenu, 5000, mainmenu); // Show the main menu again after 5secs + clearInterval(mainInterval); // Stop the main interval from starting a new activity + mainInterval = undefined; + currentMode = undefined; + } +} + +// Buzz 3 times on state transitions +function buzz() { + Bangle.buzz(500) + .then(() => new Promise(resolve => setTimeout(resolve, 200))) + .then(() => Bangle.buzz(500)) + .then(() => new Promise(resolve => setTimeout(resolve, 200))) + .then(() => Bangle.buzz(500)); +} + +function drawText(text, size){ + g.clear(); + g.setFontAlign(0, 0); // center font + g.setFont("6x8", size); + g.drawString(text, g.getWidth() / 2, g.getHeight() / 2); +} + +function countDown() { + if (!paused) { + var text = ""; + var size = defaultFontSize; + if (time) { + var total = ("walk" in currentActivity) ? currentActivity.repetition : 1; + text += rep + "/" + total + "\n"; // Show the current/total rep count when time is shown + size -= 2; // Use smaller font size to fit everything nicely on the screen + } + text += (currentMode === "run") ? "Run\n" + counter : "Walk\n" + counter; // Switches output text + if (time) text += "\n" + time; + drawText(text, size); // draw the current mode and seconds + Bangle.setLCDPower(1); // keep the watch LCD lit up + + counter--; // Reduce the seconds + + // If the current activity is done + if (counter < 0) { + clearInterval(activityInterval); + activityInterval = undefined; + outOfTime(); + return; + } + } +} + +function startTimer() { + // If something is already running, do nothing + if (activityInterval) return; + + // Switches between the two modes + if (!currentMode || currentMode === "walk") { + currentMode = "run"; + rep++; // Increase the rep counter every time a "run" activity starts + counter = currentActivity.run * 60; + activityBgColour = "#ff5733"; // Red background for running + } + else { + currentMode = "walk"; + counter = currentActivity.walk * 60; + activityBgColour = "#4da80a"; // Green background for walking + + } + + g.setBgColor(activityBgColour); + countDown(); + if (!activityInterval) { + loop--; // Reduce the number of iterations + activityInterval = setInterval(countDown, 1000); // Start a new activity + } +} + +function showTime() { + if (time) return; // If clock is already shown, don't do anything even if the button was pressed again + // Get the time and format it with a leading 0 if necessary + var d = new Date(); + var h = d.getHours(); + var m = d.getMinutes(); + time = h + ":" + m.toString().padStart(2, 0); + setTimeout(function() { time = undefined; }, 5000); // Hide clock after 5secs +} + +// Populate the PLAN menu +function populatePlan() { + for (var i = 0; i < PLAN.length; i++) { + for (var j = 0; j < PLAN[i].length; j++) { + // Ever line will have the following format: + // w{week}d{day}(r:{run mins}|w:{walk mins}|x{number of reps}) + var name = "w" + (i + 1) + "d" + (j + 1); + if (process.env.HWVERSION == 2) name += "\n"; // Print in 2 lines to accomodate the Bangle.js 2 screen + name += "(r:" + PLAN[i][j].run; + if ("walk" in PLAN[i][j]) name += "|w:" + PLAN[i][j].walk; + if ("repetition" in PLAN[i][j]) name += "|x" + PLAN[i][j].repetition; + name += ")"; + // Each menu item will have a function that start the program at the selected day + planmenu[name] = getFunc(i, j); + } + } +} + +// Helper function to generate functions for the activePlan menu +function getFunc(i, j) { + return function() { + currentActivity = PLAN[i][j]; + startActivity(); + }; +} + +function startActivity() { + loop = ("walk" in currentActivity) ? currentActivity.repetition * 2 : 1; + rep = 0; + + E.showMenu(); // Hide the main menu + extraInfoWatch = setWatch(showTime, (process.env.HWVERSION == 2) ? BTN1 : BTN2, {repeat: true}); // Show the clock on button press + if (process.env.HWVERSION == 1) pauseOrResumeWatch = setWatch(pauseOrResumeActivity, BTN1, {repeat: true}); // Pause or resume on button press (Bangle.js 1 only) + buzz(); + mainInterval = setInterval(function() {startTimer();}, 1000); // Check every second if we need to do something +} + +// Pause or resume current activity +function pauseOrResumeActivity() { + paused = !paused; + buzz(); + if (paused) { + g.setBgColor("#fdd835"); // Yellow background for pause screen + drawText("Paused", (process.env.HWVERSION == 2) ? defaultFontSize - 3 : defaultFontSize - 2); // Although the font size is configured here, this feature does not work on Bangle.js 2 as the only physical button is tied to the extra info screen already + } + else { + g.setBgColor(activityBgColour); + } +} + +const PLAN = [ + [ + {"run": 1, "walk": 1.5, "repetition": 8}, + {"run": 1, "walk": 1.5, "repetition": 8}, + {"run": 1, "walk": 1.5, "repetition": 8}, + ], + [ + {"run": 1.5, "walk": 2, "repetition": 6}, + {"run": 1.5, "walk": 2, "repetition": 6}, + {"run": 1.5, "walk": 2, "repetition": 6}, + ], + [ + {"run": 2, "walk": 2, "repetition": 5}, + {"run": 2.5, "walk": 2.5, "repetition": 4}, + {"run": 2.5, "walk": 2.5, "repetition": 4}, + ], + [ + {"run": 3, "walk": 2, "repetition": 5}, + {"run": 3, "walk": 2, "repetition": 5}, + {"run": 4, "walk": 2.5, "repetition": 3}, + ], + [ + {"run": 5, "walk": 2, "repetition": 3}, + {"run": 8, "walk": 5, "repetition": 2}, + {"run": 20}, + ], + [ + {"run": 6, "walk": 3, "repetition": 2}, + {"run": 10, "walk": 3, "repetition": 2}, + {"run": 25}, + ], + [ + {"run": 25}, + {"run": 25}, + {"run": 25}, + ], + [ + {"run": 30}, + {"run": 30}, + {"run": 30}, + ], +]; + +var customRun = {"run": 1}; + +// Main menu +var mainmenu = { + "": { "title": "-- C25K --" }, + "Week": { + value: week, + min: 1, max: PLAN.length, step: 1, + onchange : v => { week = v; } + }, + "Day": { + value: day, + min: 1, max: 3, step: 1, + onchange: v => { day = v; } + }, + "View plan": function() { E.showMenu(planmenu); }, + "Custom run": function() { E.showMenu(custommenu); }, + "Start": function() { + currentActivity = PLAN[week - 1][day -1]; + startActivity(); + }, + "Exit": function() { load(); }, +}; + +// Plan view +var planmenu = { + "": { title: "-- Plan --" }, + "< Back": function() { E.showMenu(mainmenu);}, +}; + +// Custom view +var custommenu = { + "": { title : "-- Cust. run --" }, + "< Back": function() { E.showMenu(mainmenu);}, + "Run (mins)": { + value: run, + min: 1, max: 150, step: 1, + wrap: true, + onchange: v => { customRun.run = v; } + }, + "Walk (mins)": { + value: walk, + min: 0, max: 10, step: 1, + onchange: v => { + if (v > 0) { + if (reps == 1) { reps = 2; } // Walking only makes sense with multiple reps + customRun.repetition = reps; + customRun.walk = v; + } + else { + // If no walking, delete both the reps and walk data + delete customRun.repetition; + delete customRun.walk; + } + walk = v; + } + }, + "Reps": { + value: reps, + min: 1, max: 10, step: 1, + onchange: v => { + if (v > 1) { + if (walk == 0) { walk = 1; } // Multiple reps only make sense with walking phases + customRun.walk = walk; + customRun.repetition = v; + } + else { + // If no multiple reps, delete both the reps and walk data + delete customRun.repetition; + delete customRun.walk; + } + reps = v; + } + }, + "Start": function() { currentActivity = customRun; startActivity(); } +}; + +// Populate the activePlan menu view +populatePlan(); +// Actually display the menu +E.showMenu(mainmenu); diff --git a/apps/c25k/app.png b/apps/c25k/app.png new file mode 100644 index 000000000..6b3a9ba95 Binary files /dev/null and b/apps/c25k/app.png differ diff --git a/apps/c25k/c25k-scrn1.png b/apps/c25k/c25k-scrn1.png new file mode 100644 index 000000000..c4d9ea24b Binary files /dev/null and b/apps/c25k/c25k-scrn1.png differ diff --git a/apps/c25k/c25k-scrn2.png b/apps/c25k/c25k-scrn2.png new file mode 100644 index 000000000..ba064200e Binary files /dev/null and b/apps/c25k/c25k-scrn2.png differ diff --git a/apps/c25k/c25k-scrn3.png b/apps/c25k/c25k-scrn3.png new file mode 100644 index 000000000..6901abf31 Binary files /dev/null and b/apps/c25k/c25k-scrn3.png differ diff --git a/apps/c25k/c25k-scrn4.png b/apps/c25k/c25k-scrn4.png new file mode 100644 index 000000000..ad64da947 Binary files /dev/null and b/apps/c25k/c25k-scrn4.png differ diff --git a/apps/c25k/c25k-scrn5.png b/apps/c25k/c25k-scrn5.png new file mode 100644 index 000000000..ca32abdfa Binary files /dev/null and b/apps/c25k/c25k-scrn5.png differ diff --git a/apps/c25k/c25k-scrn6.png b/apps/c25k/c25k-scrn6.png new file mode 100644 index 000000000..53f5221d7 Binary files /dev/null and b/apps/c25k/c25k-scrn6.png differ diff --git a/apps/c25k/c25k-scrn7.png b/apps/c25k/c25k-scrn7.png new file mode 100644 index 000000000..407afd48b Binary files /dev/null and b/apps/c25k/c25k-scrn7.png differ diff --git a/apps/c25k/c25k-scrn8.png b/apps/c25k/c25k-scrn8.png new file mode 100644 index 000000000..1cd92d876 Binary files /dev/null and b/apps/c25k/c25k-scrn8.png differ diff --git a/apps/c25k/c25k-scrn9.png b/apps/c25k/c25k-scrn9.png new file mode 100644 index 000000000..53dbaad1f Binary files /dev/null and b/apps/c25k/c25k-scrn9.png differ diff --git a/apps/c25k/metadata.json b/apps/c25k/metadata.json new file mode 100644 index 000000000..876926a0c --- /dev/null +++ b/apps/c25k/metadata.json @@ -0,0 +1,30 @@ +{ + "id": "c25k", + "name": "C25K", + "icon": "app.png", + "version":"0.04", + "description": "Unofficial app for the Couch to 5k training plan", + "readme": "README.md", + "type": "app", + "tags": "running,c25k,tool,outdoors,exercise", + "allow_emulator": true, + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "storage": [ + {"name": "c25k.app.js", "url": "app.js"}, + {"name": "c25k.img", "url": "app-icon.js", "evaluate": true} + ], + "screenshots": [ + {"url": "c25k-scrn1.png"}, + {"url": "c25k-scrn2.png"}, + {"url": "c25k-scrn3.png"}, + {"url": "c25k-scrn4.png"}, + {"url": "c25k-scrn5.png"}, + {"url": "c25k-scrn6.png"}, + {"url": "c25k-scrn7.png"}, + {"url": "c25k-scrn8.png"}, + {"url": "c25k-scrn9.png"} + ] +} diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog index 90bcfb9d4..effa87c4b 100644 --- a/apps/calclock/ChangeLog +++ b/apps/calclock/ChangeLog @@ -4,3 +4,6 @@ 0.04: Improve current time readability in light theme. 0.05: Show calendar colors & improved all day events. 0.06: Improved multi-line locations & titles +0.07: Buzz 30, 15 and 1 minute before an event +0.08: No buzz during quiet hours & tweaked 30-minute buzz +0.09: Minor code improvements diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js index 1f98502ef..32cce5fdb 100644 --- a/apps/calclock/calclock.js +++ b/apps/calclock/calclock.js @@ -117,6 +117,19 @@ function fullRedraw() { drawFutureEvents(y); } +function buzzForEvents() { + let nextEvent = next[0]; if (!nextEvent) return; + // No buzz for all day events or events before 7am + // TODO: make this configurable + if (nextEvent.allDay || (new Date(nextEvent.timestamp * 1000)).getHours() < 7) return; + let minToEvent = Math.round((nextEvent.timestamp - getTime()) / 60.0); + switch (minToEvent) { + case 30: require("buzz").pattern(":"); break; + case 15: require("buzz").pattern(", ,"); break; + case 1: require("buzz").pattern(": : :"); break; + } +} + function redraw() { g.reset(); if (current.find(e=>!isActive(e)) || next.find(isActive)) { @@ -124,11 +137,13 @@ function redraw() { } else { drawCurrentEvents(30); } + buzzForEvents(); } g.clear(); fullRedraw(); -var minuteInterval = setInterval(redraw, 60 * 1000); +buzzForEvents(); +/*var minuteInterval =*/ setInterval(redraw, 60 * 1000); Bangle.setUI("clock"); Bangle.loadWidgets(); diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json index bfd847595..c339e8ce0 100644 --- a/apps/calclock/metadata.json +++ b/apps/calclock/metadata.json @@ -2,7 +2,7 @@ "id": "calclock", "name": "Calendar Clock", "shortName": "CalClock", - "version": "0.06", + "version": "0.09", "description": "Show the current and upcoming events synchronized from Gadgetbridge", "icon": "calclock.png", "type": "clock", diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index 2e1ace7bf..7b47d3a4c 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -5,3 +5,4 @@ 0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2) 0.06: Bangle.js 2: Exit with a short press of the physical button 0.07: Bangle.js 2: Exit by pressing upper left corner of the screen +0.08: truncate long numbers (and append '...' to displayed value) diff --git a/apps/calculator/README.md b/apps/calculator/README.md index 62f6cef24..29edd433a 100644 --- a/apps/calculator/README.md +++ b/apps/calculator/README.md @@ -20,9 +20,9 @@ Bangle.js 1 - SELECT: BTN2 Bangle.js 2 -- Swipes to change visible buttons -- Click physical button to exit -- Press upper left corner of screen to exit (where the red back button would be) +- Swipe up or down to go back to the number input +- Swipe to the left for operators, swipe to the right for the special functions +- Exit by pressing the physical button or the upper left corner of screen to exit (where the red back button would be) ## Creator diff --git a/apps/calculator/app.js b/apps/calculator/app.js index d9a89a989..5f4e77a47 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -10,9 +10,9 @@ g.clear(); require("Font7x11Numeric7Seg").add(Graphics); -var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R'; -var RIGHT_MARGIN = 20; +var DEFAULT_SELECTION_NUMBERS = '5'; var RESULT_HEIGHT = 40; +var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14); var COLORS = { // [normal, selected] DEFAULT: ['#7F8183', '#A6A6A7'], @@ -88,28 +88,11 @@ function prepareScreen(screen, grid, defaultColor) { } function drawKey(name, k, selected) { - var rMargin = 0; - var bMargin = 0; var color = k.color || COLORS.DEFAULT; g.setColor(color[selected ? 1 : 0]); g.setFont('Vector', 20).setFontAlign(0,0); g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); g.setColor(-1); - // correct margins to center the texts - if (name == '0') { - rMargin = (RIGHT_MARGIN * 2) - 7; - } else if (name === '/') { - rMargin = 5; - } else if (name === '*') { - bMargin = 5; - rMargin = 3; - } else if (name === '-') { - rMargin = 3; - } else if (name === 'R' || name === 'N') { - rMargin = k.val === 'C' ? 0 : -9; - } else if (name === '%') { - rMargin = -3; - } g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2); } @@ -138,29 +121,21 @@ function drawGlobal() { screen[k] = specials[k]; } drawKeys(); - var selected = DEFAULT_SELECTION_NUMBERS; - var prevSelected = DEFAULT_SELECTION_NUMBERS; } function drawNumbers() { screen = numbers; screenColor = COLORS.DEFAULT; drawKeys(); - var selected = DEFAULT_SELECTION_NUMBERS; - var prevSelected = DEFAULT_SELECTION_NUMBERS; } function drawOperators() { screen = operators; screenColor =COLORS.OPERATOR; drawKeys(); - var selected = DEFAULT_SELECTION_OPERATORS; - var prevSelected = DEFAULT_SELECTION_OPERATORS; } function drawSpecials() { screen = specials; screenColor = COLORS.SPECIAL; drawKeys(); - var selected = DEFAULT_SELECTION_SPECIALS; - var prevSelected = DEFAULT_SELECTION_SPECIALS; } function getIntWithPrecision(x) { @@ -218,8 +193,6 @@ function doMath(x, y, operator) { } function displayOutput(num) { - var len; - var minusMarge = 0; g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); g.setColor(-1); if (num === Infinity || num === -Infinity || isNaN(num)) { @@ -230,9 +203,7 @@ function displayOutput(num) { num = '-INFINITY'; } else { num = 'NOT A NUMBER'; - minusMarge = -25; } - len = (num + '').length; currNumber = null; results = null; isDecimal = false; @@ -261,6 +232,9 @@ function displayOutput(num) { num = num.toString(); num = num.replace("-","- "); // fix padding for '-' g.setFont('7x11Numeric7Seg', 2); + if (num.length > RESULT_MAX_LEN) { + num = num.substr(0, RESULT_MAX_LEN - 1)+'...'; + } } g.setFontAlign(1,0); g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); @@ -369,7 +343,7 @@ function buttonPress(val) { } hasPressedNumber = false; break; - default: + default: { specials.R.val = 'C'; if (!swipeEnabled) drawKey('R', specials.R); const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity); @@ -385,6 +359,7 @@ function buttonPress(val) { hasPressedNumber = currNumber; displayOutput(currNumber); break; + } } } diff --git a/apps/calculator/metadata.json b/apps/calculator/metadata.json index 1674b7843..a88444e11 100644 --- a/apps/calculator/metadata.json +++ b/apps/calculator/metadata.json @@ -2,7 +2,7 @@ "id": "calculator", "name": "Calculator", "shortName": "Calculator", - "version": "0.07", + "version": "0.08", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "icon": "calculator.png", "screenshots": [{"url":"screenshot_calculator.png"}], diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 12776867f..9a4f81491 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -15,3 +15,7 @@ Display events for current month on touch 0.14: Add support for holidays 0.15: Edit holidays on device in settings +0.16: Add menu to fast open settings to edit holidays + Display Widgets in menus +0.17: Load holidays before events so the latter is not overpainted +0.18: Minor code improvements diff --git a/apps/calendar/README.md b/apps/calendar/README.md index 5f90d0d52..7fa7bea1c 100644 --- a/apps/calendar/README.md +++ b/apps/calendar/README.md @@ -1,6 +1,6 @@ # Calendar -Basic calendar +Monthly calendar, displays holidays uploaded from the web interface and scheduled events. ## Usage @@ -14,4 +14,4 @@ Basic calendar ## Settings -- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme. +B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme. diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 0ae852d83..e140ff576 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -1,3 +1,4 @@ +{ const maxX = g.getWidth(); const maxY = g.getHeight(); const fontSize = g.getWidth() > 200 ? 2 : 1; @@ -17,74 +18,87 @@ const red = "#d41706"; const blue = "#0000ff"; const yellow = "#ffff00"; const cyan = "#00ffff"; -let bgColor = color4; -let bgColorMonth = color1; -let bgColorDow = color2; -let bgColorWeekend = color3; -let fgOtherMonth = gray1; -let fgSameMonth = white; -let bgEvent = blue; -let bgOtherEvent = "#ff8800"; +let bgColor; +let bgColorMonth; +let bgColorDow; +let bgColorWeekend; +let fgOtherMonth; +let fgSameMonth; +let bgEvent; +let bgOtherEvent; const eventsPerDay=6; // how much different events per day we can display const date = new Date(); const timeutils = require("time_utils"); -let settings = require('Storage').readJSON("calendar.json", true) || {}; let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0; - // all alarms that run on a specific date -const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => { - const date = new Date(a.date); - const time = timeutils.decodeTime(a.t); - date.setHours(time.h); - date.setMinutes(time.m); - date.setSeconds(time.s); - return {date: date, msg: a.msg, type: "e"}; -}); -// add holidays & other events -(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => { - const date = new Date(d.date); - const o = {date: date, msg: d.name, type: d.type}; - if (d.repeat) { - o.repeat = d.repeat; - } - events.push(o); -}); - -if (settings.ndColors === undefined) { - settings.ndColors = !g.theme.dark; -} - -if (settings.ndColors === true) { - bgColor = white; - bgColorMonth = blue; - bgColorDow = black; - bgColorWeekend = yellow; - fgOtherMonth = blue; - fgSameMonth = black; - bgEvent = color2; - bgOtherEvent = cyan; -} - -function getDowLbls(locale) { - let days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0]; +let events; +const dowLbls = function() { + const days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0]; const d = new Date(); return days.map(i => { d.setDate(d.getDate() + (i + 7 - d.getDay()) % 7); return require("locale").dow(d, 1); }); -} +}(); -function sameDay(d1, d2) { +const loadEvents = () => { + // add holidays & other events + events = (require("Storage").readJSON("calendar.days.json",1) || []).map(d => { + const date = new Date(d.date); + const o = {date: date, msg: d.name, type: d.type}; + if (d.repeat) { + o.repeat = d.repeat; + } + return o; + }); + // all alarms that run on a specific date + events = events.concat((require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => { + const date = new Date(a.date); + const time = timeutils.decodeTime(a.t); + date.setHours(time.h); + date.setMinutes(time.m); + date.setSeconds(time.s); + return {date: date, msg: a.msg, type: "e"}; + })); +}; + +const loadSettings = () => { + let settings = require('Storage').readJSON("calendar.json", true) || {}; + if (settings.ndColors === undefined) { + settings.ndColors = !g.theme.dark; + } + if (settings.ndColors === true) { + bgColor = white; + bgColorMonth = blue; + bgColorDow = black; + bgColorWeekend = yellow; + fgOtherMonth = blue; + fgSameMonth = black; + bgEvent = color2; + bgOtherEvent = cyan; + } else { + bgColor = color4; + bgColorMonth = color1; + bgColorDow = color2; + bgColorWeekend = color3; + fgOtherMonth = gray1; + fgSameMonth = white; + bgEvent = blue; + bgOtherEvent = "#ff8800"; + } +}; + +const sameDay = function(d1, d2) { "jit"; return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); -} +}; -function drawEvent(ev, curDay, x1, y1, x2, y2) { +const drawEvent = function(ev, curDay, x1, y1, x2, y2) { "ram"; switch(ev.type) { - case "e": // alarm/event + case "e": { // alarm/event const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0; const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59 const height = (y2-2) - (y1+2); // height of a cell @@ -92,6 +106,7 @@ function drawEvent(ev, curDay, x1, y1, x2, y2) { const ystart = (y1+2) + slice*sliceHeight; g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight); break; + } case "h": // holiday g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1); break; @@ -99,9 +114,32 @@ function drawEvent(ev, curDay, x1, y1, x2, y2) { g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1); break; } -} +}; -function drawCalendar(date) { +const calcDays = (month, monthMaxDayMap, dowNorm) => { + "jit"; + const maxDay = colN * (rowN - 1) + 1; + const days = []; + let nextMonthDay = 1; + let thisMonthDay = 51; + let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1; + + for (let i = 0; i < maxDay; i++) { + if (i < dowNorm) { + days.push(prevMonthDay); + prevMonthDay++; + } else if (thisMonthDay <= monthMaxDayMap[month] + 50) { + days.push(thisMonthDay); + thisMonthDay++; + } else { + days.push(nextMonthDay); + nextMonthDay++; + } + } + return days; +}; + +const drawCalendar = function(date) { g.setBgColor(bgColor); g.clearRect(0, 0, maxX, maxY); g.setBgColor(bgColorMonth); @@ -139,7 +177,6 @@ function drawCalendar(date) { true ); - let dowLbls = getDowLbls(require('locale').name); dowLbls.forEach((lbl, i) => { g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); }); @@ -163,23 +200,7 @@ function drawCalendar(date) { 11: 31 }; - let days = []; - let nextMonthDay = 1; - let thisMonthDay = 51; - let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1; - for (let i = 0; i < colN * (rowN - 1) + 1; i++) { - if (i < dowNorm) { - days.push(prevMonthDay); - prevMonthDay++; - } else if (thisMonthDay <= monthMaxDayMap[month] + 50) { - days.push(thisMonthDay); - thisMonthDay++; - } else { - days.push(nextMonthDay); - nextMonthDay++; - } - } - + const days = calcDays(month, monthMaxDayMap, dowNorm); const weekBeforeMonth = new Date(date.getTime()); weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7); const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0); @@ -189,8 +210,15 @@ function drawCalendar(date) { ev.date.setFullYear(ev.date.getMonth() < 6 ? week2AfterMonth.getFullYear() : weekBeforeMonth.getFullYear()); } }); - const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth); - eventsThisMonth.sort((a,b) => a.date - b.date); + + const eventsThisMonthPerDay = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth).reduce((acc, ev) => { + const day = ev.date.getDate(); + if (!acc[day]) { + acc[day] = []; + } + acc[day].push(ev); + return acc; + }, []); let i = 0; g.setFont("8x12", fontSize); for (y = 0; y < rowN - 1; y++) { @@ -205,13 +233,13 @@ function drawCalendar(date) { const x2 = x * colW + colW; const y2 = y * rowH + headerH + rowH + rowH; - if (eventsThisMonth.length > 0) { + const eventsThisDay = eventsThisMonthPerDay[curDay.getDate()]; + if (eventsThisDay && eventsThisDay.length > 0) { // Display events for this day - eventsThisMonth.forEach((ev, idx) => { + eventsThisDay.forEach((ev, idx) => { if (sameDay(ev.date, curDay)) { drawEvent(ev, curDay, x1, y1, x2, y2); - - eventsThisMonth.splice(idx, 1); // this event is no longer needed + eventsThisDay.splice(idx, 1); // this event is no longer needed } }); } @@ -235,9 +263,42 @@ function drawCalendar(date) { ); } // end for (x = 0; x < colN; x++) } // end for (y = 0; y < rowN - 1; y++) -} // end function drawCalendar +}; // end function drawCalendar + +const showMenu = function() { + const menu = { + "" : { + title : "Calendar", + remove: () => { + require("widget_utils").show(); + } + }, + "< Back": () => { + require("widget_utils").hide(); + E.showMenu(); + setUI(); + }, + /*LANG*/"Exit": () => load(), + /*LANG*/"Settings": () => + eval(require('Storage').read('calendar.settings.js'))(() => { + loadSettings(); + loadEvents(); + showMenu(); + }), + }; + if (require("Storage").read("alarm.app.js")) { + menu[/*LANG*/"Launch Alarms"] = () => { + load("alarm.app.js"); + }; + } + require("widget_utils").show(); + E.showMenu(menu); +}; + +const setUI = function() { + require("widget_utils").hide(); // No space for widgets! + drawCalendar(date); -function setUI() { Bangle.setUI({ mode : "custom", swipe: (dirLR, dirUD) => { @@ -261,7 +322,14 @@ function setUI() { drawCalendar(date); } }, - btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(), + btn: (n) => { + if (process.env.HWVERSION === 2 || n === 2) { + showMenu(); + } else if (n === 3) { + // directly exit only on Bangle.js 1 + load(); + } + }, touch: (n,e) => { events.sort((a,b) => a.date - b.date); const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => { @@ -274,16 +342,19 @@ function setUI() { } menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() }; menu["< Back"] = () => { + require("widget_utils").hide(); E.showMenu(); - drawCalendar(date); setUI(); }; + require("widget_utils").show(); E.showMenu(menu); } }); -} +}; +loadSettings(); +loadEvents(); +Bangle.loadWidgets(); require("Font8x12").add(Graphics); -drawCalendar(date); setUI(); -// No space for widgets! +} diff --git a/apps/calendar/interface.html b/apps/calendar/interface.html index ea64632f8..8fa624a40 100644 --- a/apps/calendar/interface.html +++ b/apps/calendar/interface.html @@ -32,12 +32,14 @@ function readFile(input) { const jCalData = ICAL.parse(icalText); const comp = new ICAL.Component(jCalData); const vtz = comp.getFirstSubcomponent('vtimezone'); - const tz = new ICAL.Timezone(vtz); + const tz = vtz != null ? new ICAL.Timezone(vtz) : null; // Fetch the VEVENT part comp.getAllSubcomponents('vevent').forEach(vevent => { const event = new ICAL.Event(vevent); - event.startDate.zone = tz; + if (tz != null) { + event.startDate.zone = tz; + } holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists const holiday = eventToHoliday(event); @@ -178,9 +180,8 @@ function getData() { })()\n`, contents => { const fileNames = JSON.parse(contents); if (fileNames.length > 0) { - Util.readStorage('calendar.days.json',data=>{ - holidays = JSON.parse(data || "[]") || []; - + Util.readStorageJSON('calendar.days.json',data=>{ + holidays = data || []; Util.hideModal(); render(); }); diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index bd35c8879..468bceabb 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,8 +1,8 @@ { "id": "calendar", "name": "Calendar", - "version": "0.15", - "description": "Simple calendar", + "version": "0.18", + "description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], "tags": "calendar,tool", diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js index 40eca9f68..051826646 100644 --- a/apps/calendar/settings.js +++ b/apps/calendar/settings.js @@ -1,14 +1,15 @@ (function (back) { - var FILE = "calendar.json"; + const FILE = "calendar.json"; const HOLIDAY_FILE = "calendar.days.json"; - var settings = require('Storage').readJSON(FILE, true) || {}; - if (settings.ndColors === undefined) + const settings = require('Storage').readJSON(FILE, true) || {}; + if (settings.ndColors === undefined) { if (process.env.HWVERSION == 2) { settings.ndColors = true; } else { settings.ndColors = false; } - const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || []; + } + const holidays = (require("Storage").readJSON(HOLIDAY_FILE,1)||[]).sort((a,b) => new Date(a.date) - new Date(b.date)) || []; function writeSettings() { require('Storage').writeJSON(FILE, settings); @@ -26,7 +27,7 @@ const editdate = (i) => { const holiday = holidays[i]; const date = new Date(holiday.date); - const dateStr = require("locale").date(date, 1); + //const dateStr = require("locale").date(date, 1); const menu = { "": { "title" : holiday.name}, "< Back": () => { diff --git a/apps/calibration/ChangeLog b/apps/calibration/ChangeLog index 64bff2b31..3813bfdf8 100644 --- a/apps/calibration/ChangeLog +++ b/apps/calibration/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Use fractional numbers and scale the points to keep working consistently on whole screen 0.03: Use default Bangle formatter for booleans +0.04: Minor code improvements diff --git a/apps/calibration/app.js b/apps/calibration/app.js index 049430d45..b5faa8f81 100644 --- a/apps/calibration/app.js +++ b/apps/calibration/app.js @@ -129,7 +129,7 @@ class BanglejsApp { E.srand(Date.now()); -calibration = new BanglejsApp(); +const calibration = new BanglejsApp(); calibration.load_settings(); Bangle.disableCalibration = true; diff --git a/apps/calibration/metadata.json b/apps/calibration/metadata.json index f428bd538..11fc73828 100644 --- a/apps/calibration/metadata.json +++ b/apps/calibration/metadata.json @@ -2,7 +2,7 @@ "name": "Touchscreen Calibration", "shortName":"Calibration", "icon": "calibration.png", - "version":"0.03", + "version": "0.04", "description": "(NOT RECOMMENDED) A simple calibration app for the touchscreen. Please use the Touchscreen Calibration in the Settings app instead.", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", diff --git a/apps/carcrazy/ChangeLog b/apps/carcrazy/ChangeLog index f697617b4..f4fd9ab7b 100644 --- a/apps/carcrazy/ChangeLog +++ b/apps/carcrazy/ChangeLog @@ -1,3 +1,4 @@ 0.01: Car Crazy is now avialable for testing in beta! 0.02: 10 Levels are now added making the game harder as it goes along. Some of the levels include multiple cars and faster cars. More levels coming soon. 0.03: Settings are now added so that you can reset your high score. +0.04: Minor code improvements. diff --git a/apps/carcrazy/app.js b/apps/carcrazy/app.js index 0fb765871..2fdfc67e7 100644 --- a/apps/carcrazy/app.js +++ b/apps/carcrazy/app.js @@ -66,10 +66,8 @@ function moveEnemyPosition(){ enemyPositonCenterX2 = 120; }else if((randomRoadPositionIndicator2 == 3)){ enemyPositonCenterX2 = 155; - }else if(level == 7||level == 8){ - } - } + } // TODO: else if(level == 7) } function collision(){ @@ -168,17 +166,17 @@ var playerCarRightX; var playerCarFrontY; var playerCarFrontY; -var playerCarBackY; +//var playerCarBackY; var playerCarLeftX; var playerCarRightX; var enemyCarFrontY; -var enemyCarBackY; +//var enemyCarBackY; var enemyCarLeftX; var enemyCarRightX; var enemyCarFrontY2; -var enemyCarBackY2; +//var enemyCarBackY2; var enemyCarLeftX2; var enemyCarRightX2; @@ -239,17 +237,17 @@ function draw(){ } playerCarFrontY = playerCarCenterY-carHeight/2; - playerCarBackY = playerCarCenterY+carHeight/2; + //playerCarBackY = playerCarCenterY+carHeight/2; playerCarLeftX = playerCarCenterX-carWidth/2; playerCarRightX = playerCarCenterX+carWidth/2; enemyCarFrontY = enemyPositonCenterY+carHeight/2; - enemyCarBackY = enemyPositonCenterY-carHeight/2; + //enemyCarBackY = enemyPositonCenterY-carHeight/2; enemyCarLeftX = enemyPositonCenterX-carWidth/2; enemyCarRightX = enemyPositonCenterX+carWidth/2; enemyCarFrontY2 = enemyPositonCenterY2+carHeight/2; - enemyCarBackY2 = enemyPositonCenterY2-carHeight/2; + //enemyCarBackY2 = enemyPositonCenterY2-carHeight/2; enemyCarLeftX2 = enemyPositonCenterX2-carWidth/2; enemyCarRightX2 = enemyPositonCenterX2+carWidth/2; diff --git a/apps/carcrazy/metadata.json b/apps/carcrazy/metadata.json index 3898de962..4a1b359c8 100644 --- a/apps/carcrazy/metadata.json +++ b/apps/carcrazy/metadata.json @@ -2,7 +2,7 @@ "id": "carcrazy", "name": "Car Crazy", "shortName": "Car Crazy", - "version": "0.03", + "version": "0.04", "description": "A simple car game where you try to avoid the other cars by tilting your wrist left and right. Hold down button 2 to start.", "icon": "carcrash.png", "tags": "game", diff --git a/apps/cards/Barcode.js b/apps/cards/Barcode.js new file mode 100644 index 000000000..ea2448ca5 --- /dev/null +++ b/apps/cards/Barcode.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +class Barcode{ + constructor(data, options){ + this.data = data; + this.text = options.text || data; + this.options = options; + } +} + +module.exports = Barcode; diff --git a/apps/cards/ChangeLog b/apps/cards/ChangeLog new file mode 100644 index 000000000..b35947cda --- /dev/null +++ b/apps/cards/ChangeLog @@ -0,0 +1,5 @@ +0.01: Simple app to display loyalty cards +0.02: Hiding widgets while showing the code +0.03: Added option to use max brightness when showing code +0.04: Minor code improvements +0.05: Add EAN & UPC codes diff --git a/apps/cards/EAN.js b/apps/cards/EAN.js new file mode 100644 index 000000000..177874494 --- /dev/null +++ b/apps/cards/EAN.js @@ -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; \ No newline at end of file diff --git a/apps/cards/EAN13.js b/apps/cards/EAN13.js new file mode 100644 index 000000000..c91e385e3 --- /dev/null +++ b/apps/cards/EAN13.js @@ -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; \ No newline at end of file diff --git a/apps/cards/EAN8.js b/apps/cards/EAN8.js new file mode 100644 index 000000000..382ee647a --- /dev/null +++ b/apps/cards/EAN8.js @@ -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; diff --git a/apps/cards/README.md b/apps/cards/README.md new file mode 100644 index 000000000..001595452 --- /dev/null +++ b/apps/cards/README.md @@ -0,0 +1,32 @@ +# Cards + +Simple app to display loyalty cards synced from Catima through GadgetBridge. +The app can display the cards' info (balance, expiration, note, etc.) and tapping on the appropriate field will display the code, if the type is supported. + +To come back to the visualization of the card's details from the code view, simply press the button. + +Beware that the small screen of the Banglejs 2 cannot render properly complex barcodes (in fact the resolution is very limited to render most barcodes). + +### Supported codes types + +* `CODE_39` +* `CODABAR` +* `QR_CODE` + +### Disclaimer + +This app is a proof of concept, many codes are too complex to be rendered by the bangle's screen or hardware (at least with the current logic), keep that in mind. + +### How to sync + +We can synchronize cards with GadgetBridge and Catima, refer to those projects for further information. +The feature is currently available on nightly builds only. +It should be released from version 0.77 (not yet out at the time of writing). + +GadgetBridge syncronizes all cards at once, if you have too many cards you may want to explicitly select which ones to syncronize, keep in mind the limitations of the Banglejs. + +### Credits + +Barcode generation adapted from [lindell/JsBarcode](https://github.com/lindell/JsBarcode) + +QR code generation adapted from [ricmoo/QRCode](https://github.com/ricmoo/QRCode) diff --git a/apps/cards/UPC.js b/apps/cards/UPC.js new file mode 100644 index 000000000..6f581ca18 --- /dev/null +++ b/apps/cards/UPC.js @@ -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 }; diff --git a/apps/cards/UPCE.js b/apps/cards/UPCE.js new file mode 100644 index 000000000..9aaa464b7 --- /dev/null +++ b/apps/cards/UPCE.js @@ -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; \ No newline at end of file diff --git a/apps/cards/app-icon.js b/apps/cards/app-icon.js new file mode 100644 index 000000000..3ec6948c4 --- /dev/null +++ b/apps/cards/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AYoIAjF/4v/F/4v/F/4v/FAdNAAsoADgv/F/4v/F/4vqu4AjF/4v/F/4v6poAjF/4AfFAYAGF/4v/F/4v/F/4v/F94A/AH4A/AH4A/ABo")) diff --git a/apps/cards/app.js b/apps/cards/app.js new file mode 100644 index 000000000..45e72831c --- /dev/null +++ b/apps/cards/app.js @@ -0,0 +1,265 @@ +/* CARDS is a list of: + {id:int, + name, + value, + type, + expiration, + color, + balance, + note, + ... + } +*/ + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// get brightness +let brightness; + +function loadBrightness() { + const getBrightness = require('Storage').readJSON("setting.json", 1) || {}; + brightness = getBrightness.brightness || 0.1; +} + +//may make it configurable in the future +const WHITE=-1 +const BLACK=0 + +const Locale = require("locale"); +const widget_utils = require('widget_utils'); + +//var fontSmall = "6x8"; +var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +//var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; + +var CARDS = require("Storage").readJSON("android.cards.json",true)||[]; +var settings = require("Storage").readJSON("cards.settings.json",true)||{}; + +function getDate(timestamp) { + return new Date(timestamp*1000); +} +function formatDay(date) { + let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); + if (!settings.useToday) { + return formattedDate; + } + const today = new Date(Date.now()); + if (date.getDay() == today.getDay() && date.getMonth() == today.getMonth()) + return /*LANG*/"Today "; + else { + const tomorrow = new Date(Date.now() + 86400 * 1000); + if (date.getDay() == tomorrow.getDay() && date.getMonth() == tomorrow.getMonth()) { + return /*LANG*/"Tomorrow "; + } + return formattedDate; + } +} + +function getColor(intColor) { + return "#"+(0x1000000+Number(intColor)).toString(16).padStart(6,"0"); +} +function isLight(color) { + var r = +("0x"+color.slice(1,3)); + var g = +("0x"+color.slice(3,5)); + var b = +("0x"+color.slice(5,7)); + var threshold = 0x88 * 3; + return (r+g+b) > threshold; +} + +function printSquareCode(binary, size) { + var padding = 5; + var ratio = (g.getWidth()-(2*padding))/size; + for (var y = 0; y < size; y++) { + for (var x = 0; x < size; x++) { + if (binary[x + y * size]) { + g.setColor(BLACK).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); + } else { + g.setColor(WHITE).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); + } + } + } +} +function printLinearCode(binary) { + var padding = 5; + var yFrom = 15; + var yTo = 28; + var width = (g.getWidth()-(2*padding))/binary.length; + for(var b = 0; b < binary.length; b++){ + var x = b * width; + if(binary[b] === "1"){ + g.setColor(BLACK).fillRect({x:x+padding, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); + } + else if(binary[b]){ + g.setColor(WHITE).fillRect({x:x+padding, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); + } + } +} + +function showCode(card) { + // set to full bright when the setting is true + if(settings.fullBrightness) { + Bangle.setLCDBrightness(1); + } + widget_utils.hide(); + E.showScroller(); + // keeping it on rising edge would come back twice.. + setWatch(()=>showCard(card), BTN, {edge:"falling"}); + // theme independent + g.setColor(WHITE).fillRect(0, 0, g.getWidth(), g.getHeight()); + switch (card.type) { + case "QR_CODE": { + const getBinaryQR = require("cards.qrcode.js"); + let code = getBinaryQR(card.value); + printSquareCode(code.data, code.size); + break; + } + case "CODE_39": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const CODE39 = require("cards.code39.js"); + let code = new CODE39(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "CODABAR": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const codabar = require("cards.codabar.js"); + let code = new codabar(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "EAN_8": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const EAN8 = require("cards.EAN8.js"); + let code = new EAN8(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "EAN_13": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const EAN13 = require("cards.EAN13.js"); + let code = new EAN13(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "UPC_A": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const UPC = require("cards.UPC.js"); + let code = new UPC.UPC(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "UPC_E": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const UPCE = require("cards.UPCE.js"); + let code = new UPCE(card.value, {}); + printLinearCode(code.encode().data); + break; + } + default: + g.clear(true); + g.setFont("Vector:30"); + g.setFontAlign(0,0); + g.drawString(card.value, g.getWidth()/2, g.getHeight()/2); + } +} + +function showCard(card) { + // reset brightness to old value after maxing it out + if(settings.fullBrightness) { + Bangle.setLCDBrightness(brightness); + } + var lines = []; + var bodyFont = fontBig; + if(!card) return; + g.setFont(bodyFont); + //var lines = []; + if (card.name) lines = g.wrapString(card.name, g.getWidth()-10); + var titleCnt = lines.length; + lines = lines.concat("", /*LANG*/"View code"); + var valueLine = lines.length - 1; + if (card.expiration) + lines = lines.concat("",/*LANG*/"Expires"+": ", g.wrapString(formatDay(getDate(card.expiration)), g.getWidth()-10)); + if(card.balance) + lines = lines.concat("",/*LANG*/"Balance"+": ", g.wrapString(card.balance, g.getWidth()-10)); + if(card.note && card.note.trim()) + lines = lines.concat("",g.wrapString(card.note, g.getWidth()-10)); + lines = lines.concat("",/*LANG*/"< Back"); + var titleBgColor = card.color ? getColor(card.color) : g.theme.bg2; + var titleColor = g.theme.fg2; + if (card.color) + titleColor = isLight(titleBgColor) ? BLACK : WHITE; + widget_utils.show(); + E.showScroller({ + h : g.getFontHeight(), // height of each menu item in pixels + c : lines.length, // number of menu items + // a function to draw a menu item + draw : function(idx, r) { + // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 + g.setBgColor(idx=lines.length-2) + showList(); + else if (idx==valueLine) + showCode(card); + }, + back : () => showList() + }); +} + +// https://github.com/metafloor/bwip-js +// https://github.com/lindell/JsBarcode + +function showList() { + if(CARDS.length == 0) { + E.showMessage(/*LANG*/"No cards"); + return; + } + E.showScroller({ + h : 52, + c : Math.max(CARDS.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + draw : function(idx, r) {"ram" + var card = CARDS[idx]; + g.setColor(g.theme.fg); + g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); + if (!card) return; + var isPast = false; + var x = r.x+2, name = card.name; + var body = card.expiration ? formatDay(getDate(card.expiration)) : ""; + if (card.balance) body += "\n" + card.balance; + if (name) g.setFontAlign(-1,-1).setFont(fontBig) + .setColor(isPast ? "#888" : g.theme.fg).drawString(name, x+4,r.y+2); + if (body) { + g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg); + g.drawString(body, x+10,r.y+20); + } + g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items + if(card.color) { + g.setColor(getColor(card.color)); + g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4); + } + }, + select : idx => showCard(CARDS[idx]), + back : () => load() + }); +} +if(settings.fullBrightness) { + loadBrightness(); +} +showList(); diff --git a/apps/cards/app.png b/apps/cards/app.png new file mode 100644 index 000000000..b2bfa59f4 Binary files /dev/null and b/apps/cards/app.png differ diff --git a/apps/cards/codabar.js b/apps/cards/codabar.js new file mode 100644 index 000000000..2d245e091 --- /dev/null +++ b/apps/cards/codabar.js @@ -0,0 +1,88 @@ +// Encoding specification: +// http://www.barcodeisland.com/codabar.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 Barcode = require("cards.Barcode.js"); + +class codabar extends Barcode{ + constructor(data, options){ + if (/^[0-9\-\$\:\.\+\/]+$/.test(data)) { + data = "A" + data + "A"; + } + + super(data.toUpperCase(), options); + + this.text = this.options.text || this.text.replace(/[A-D]/g, ''); + } + + valid(){ + return /^[A-D][0-9\-\$\:\.\+\/]+[A-D]$/.test(this.data) + } + + encode(){ + var result = []; + var encodings = this.getEncodings(); + for(var i = 0; i < this.data.length; i++){ + result.push(encodings[this.data.charAt(i)]); + // for all characters except the last, append a narrow-space ("0") + if (i !== this.data.length - 1) { + result.push("0"); + } + } + return { + text: this.text, + data: result.join('') + }; + } + + getEncodings(){ + return { + "0": "101010011", + "1": "101011001", + "2": "101001011", + "3": "110010101", + "4": "101101001", + "5": "110101001", + "6": "100101011", + "7": "100101101", + "8": "100110101", + "9": "110100101", + "-": "101001101", + "$": "101100101", + ":": "1101011011", + "/": "1101101011", + ".": "1101101101", + "+": "1011011011", + "A": "1011001001", + "B": "1001001011", + "C": "1010010011", + "D": "1010011001" + }; + } +} + +module.exports = codabar diff --git a/apps/cards/code39.js b/apps/cards/code39.js new file mode 100644 index 000000000..c9b81d55c --- /dev/null +++ b/apps/cards/code39.js @@ -0,0 +1,130 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Code_39#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 Barcode = require("cards.Barcode.js"); + +class CODE39 extends Barcode { + constructor(data, options){ + data = data.toUpperCase(); + + // Calculate mod43 checksum if enabled + if(options.mod43){ + data += getCharacter(mod43checksum(data)); + } + + super(data, options); + } + + encode(){ + // First character is always a * + var result = getEncoding("*"); + + // Take every character and add the binary representation to the result + for(let i = 0; i < this.data.length; i++){ + result += getEncoding(this.data[i]) + "0"; + } + + // Last character is always a * + result += getEncoding("*"); + + return { + data: result, + text: this.text + }; + } + + valid(){ + return /^[0-9A-Z\-\.\ \$\/\+\%]+$/.test(this.data); + } +} + + + + + + +// All characters. The position in the array is the (checksum) value +var characters = [ + "0", "1", "2", "3", + "4", "5", "6", "7", + "8", "9", "A", "B", + "C", "D", "E", "F", + "G", "H", "I", "J", + "K", "L", "M", "N", + "O", "P", "Q", "R", + "S", "T", "U", "V", + "W", "X", "Y", "Z", + "-", ".", " ", "$", + "/", "+", "%", "*" +]; + +// The decimal representation of the characters, is converted to the +// corresponding binary with the getEncoding function +var encodings = [ + 20957, 29783, 23639, 30485, + 20951, 29813, 23669, 20855, + 29789, 23645, 29975, 23831, + 30533, 22295, 30149, 24005, + 21623, 29981, 23837, 22301, + 30023, 23879, 30545, 22343, + 30161, 24017, 21959, 30065, + 23921, 22385, 29015, 18263, + 29141, 17879, 29045, 18293, + 17783, 29021, 18269, 17477, + 17489, 17681, 20753, 35770 +]; + +// Get the binary representation of a character by converting the encodings +// from decimal to binary +function getEncoding(character){ + return getBinary(characterValue(character)); +} + +function getBinary(characterValue){ + return encodings[characterValue].toString(2); +} + +function getCharacter(characterValue){ + return characters[characterValue]; +} + +function characterValue(character){ + return characters.indexOf(character); +} + +function mod43checksum(data){ + var checksum = 0; + for(let i = 0; i < data.length; i++){ + checksum += characterValue(data[i]); + } + + checksum = checksum % 43; + return checksum; +} + +module.exports = CODE39; diff --git a/apps/cards/encode.js b/apps/cards/encode.js new file mode 100644 index 000000000..7cb1fcc87 --- /dev/null +++ b/apps/cards/encode.js @@ -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; \ No newline at end of file diff --git a/apps/cards/metadata.json b/apps/cards/metadata.json new file mode 100644 index 000000000..c8d19a375 --- /dev/null +++ b/apps/cards/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "cards", + "name": "Cards", + "version": "0.05", + "description": "Display loyalty cards", + "icon": "app.png", + "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], + "tags": "cards", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"cards.app.js","url":"app.js"}, + {"name":"cards.settings.js","url":"settings.js"}, + {"name":"cards.Barcode.js","url":"Barcode.js"}, + {"name":"cards.qrcode.js","url":"qrcode.js"}, + {"name":"cards.codabar.js","url":"codabar.js"}, + {"name":"cards.code39.js","url":"code39.js"}, + {"name":"cards.EAN.js","url":"EAN.js"}, + {"name":"cards.EAN8.js","url":"EAN8.js"}, + {"name":"cards.EAN13.js","url":"EAN13.js"}, + {"name":"cards.UPC.js","url":"UPC.js"}, + {"name":"cards.UPCE.js","url":"UPCE.js"}, + {"name":"cards.encode.js","url":"encode.js"}, + {"name":"cards.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"cards.settings.json"}] +} diff --git a/apps/cards/qrcode.js b/apps/cards/qrcode.js new file mode 100644 index 000000000..ff79d7bee --- /dev/null +++ b/apps/cards/qrcode.js @@ -0,0 +1,705 @@ +/* + * C source adapted from https://github.com/ricmoo/QRCode + * + * The MIT License (MIT) + * + * This library is written and maintained by Richard Moore. + * Major parts were derived from Project Nayuki's library. + * + * Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode) + * Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library) + * + * 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. + */ + +var c = E.compiledC(` +// int get_qr(int, int) + +typedef signed char __int8_t; +typedef unsigned char __uint8_t; +typedef signed short int __int16_t; +typedef unsigned short int __uint16_t; +typedef signed int __int32_t; +typedef unsigned int __uint32_t; + +typedef __int8_t int8_t; +typedef __int16_t int16_t; +typedef __int32_t int32_t; +typedef __uint8_t uint8_t; +typedef __uint16_t uint16_t; +typedef __uint32_t uint32_t; + +typedef struct QRCode { + uint8_t version; + uint8_t size; + uint8_t ecc; + uint8_t mode; + uint8_t mask; + uint8_t *modules; +} QRCode; +uint16_t qrcode_getBufferSize(uint8_t version); + +int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data); +int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length); + +bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y); + +static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = { + { 10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372}, + { 7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750}, + { 17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430}, + { 13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040}, +}; + +static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = { + { 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, + { 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, + { 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, + { 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, +}; + +static const uint16_t NUM_RAW_DATA_MODULES[40] = { + 208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523, + 7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587, + 19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648 +}; +static int max(int a, int b) { + if (a > b) { return a; } + return b; +} + +static int abs(int value) { + if (value < 0) { return -value; } + return value; +} + +static void *memset(void *s, int c, int n) { + char *arr = (char *)s; + for (int i = 0; i= '0' && c <= '9') { return (c - '0'); } + if (c >= 'A' && c <= 'Z') { return (c - 'A' + 10); } + switch (c) { + case ' ': return 36; + case '$': return 37; + case '%': return 38; + case '*': return 39; + case '+': return 40; + case '-': return 41; + case '.': return 42; + case '/': return 43; + case ':': return 44; + } + + return -1; +} + +static bool isAlphanumeric(const char *text, uint16_t length) { + while (length != 0) { + if (getAlphanumeric(text[--length]) == -1) { return false; } + } + return true; +} +static bool isNumeric(const char *text, uint16_t length) { + while (length != 0) { + char c = text[--length]; + if (c < '0' || c > '9') { return false; } + } + return true; +} +static char getModeBits(uint8_t version, uint8_t mode) { + unsigned int modeInfo = 0x7bbb80a; + if (version > 9) { modeInfo >>= 9; } + + if (version > 26) { modeInfo >>= 9; } + char result = 8 + ((modeInfo >> (3 * mode)) & 0x07); + if (result == 15) { result = 16; } + + return result; +} + +typedef struct BitBucket { + uint32_t bitOffsetOrWidth; + uint16_t capacityBytes; + uint8_t *data; +} BitBucket; +static uint16_t bb_getGridSizeBytes(uint8_t size) { + return (((size * size) + 7) / 8); +} + +static uint16_t bb_getBufferSizeBytes(uint32_t bits) { + return ((bits + 7) / 8); +} + +static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) { + bitBuffer->bitOffsetOrWidth = 0; + bitBuffer->capacityBytes = capacityBytes; + bitBuffer->data = data; + + memset(data, 0, bitBuffer->capacityBytes); +} + +static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) { + bitGrid->bitOffsetOrWidth = size; + bitGrid->capacityBytes = bb_getGridSizeBytes(size); + bitGrid->data = data; + + memset(data, 0, bitGrid->capacityBytes); +} + +static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) { + uint32_t offset = bitBuffer->bitOffsetOrWidth; + for (int8_t i = length - 1; i >= 0; i--, offset++) { + bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7)); + } + bitBuffer->bitOffsetOrWidth = offset; +} + +static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + uint8_t mask = 1 << (7 - (offset & 0x07)); + if (on) { + bitGrid->data[offset >> 3] |= mask; + } else { + bitGrid->data[offset >> 3] &= ~mask; + } +} + +static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + uint8_t mask = 1 << (7 - (offset & 0x07)); + bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0); + if (on ^ invert) { + bitGrid->data[offset >> 3] |= mask; + } else { + bitGrid->data[offset >> 3] &= ~mask; + } +} + +static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; +} + +static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) { + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t y = 0; y < size; y++) { + for (uint8_t x = 0; x < size; x++) { + if (bb_getBit(isFunction, x, y)) { continue; } + + bool invert = 0; + switch (mask) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + } + bb_invertBit(modules, x, y, invert); + } + } +} + +static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) { + bb_setBit(modules, x, y, on); + bb_setBit(isFunction, x, y, true); +} +static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) { + uint8_t size = modules->bitOffsetOrWidth; + + for (int8_t i = -4; i <= 4; i++) { + for (int8_t j = -4; j <= 4; j++) { + uint8_t dist = max(abs(i), abs(j)); + int16_t xx = x + j, yy = y + i; + if (0 <= xx && xx < size && 0 <= yy && yy < size) { + setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4); + } + } + } +} +static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) { + for (int8_t i = -2; i <= 2; i++) { + for (int8_t j = -2; j <= 2; j++) { + setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1); + } + } +} + +static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) { + + uint8_t size = modules->bitOffsetOrWidth; + uint32_t data = ecc << 3 | mask; + uint32_t rem = data; + for (int i = 0; i < 10; i++) { + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + } + + data = data << 10 | rem; + data ^= 0x5412; + for (uint8_t i = 0; i <= 5; i++) { + setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0); + } + + setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0); + setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0); + setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0); + + for (int8_t i = 9; i < 15; i++) { + setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0); + } + for (int8_t i = 0; i <= 7; i++) { + setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0); + } + + for (int8_t i = 8; i < 15; i++) { + setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0); + } + + setFunctionModule(modules, isFunction, 8, size - 8, true); +} +static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) { + + int8_t size = modules->bitOffsetOrWidth; + if (version < 7) { return; } + uint32_t rem = version; + for (uint8_t i = 0; i < 12; i++) { + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + } + + uint32_t data = version << 12 | rem; + for (uint8_t i = 0; i < 18; i++) { + bool bit = ((data >> i) & 1) != 0; + uint8_t a = size - 11 + i % 3, b = i / 3; + setFunctionModule(modules, isFunction, a, b, bit); + setFunctionModule(modules, isFunction, b, a, bit); + } +} + +static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) { + + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t i = 0; i < size; i++) { + setFunctionModule(modules, isFunction, 6, i, i % 2 == 0); + setFunctionModule(modules, isFunction, i, 6, i % 2 == 0); + } + drawFinderPattern(modules, isFunction, 3, 3); + drawFinderPattern(modules, isFunction, size - 4, 3); + drawFinderPattern(modules, isFunction, 3, size - 4); + + if (version > 1) { + + uint8_t alignCount = version / 7 + 2; + uint8_t step; + if (version != 32) { + step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2; + } else { + step = 26; + } + + uint8_t alignPositionIndex = alignCount - 1; + uint8_t alignPosition[alignCount]; + + alignPosition[0] = 6; + + uint8_t l_size = version * 4 + 17; + for (uint8_t i = 0, pos = l_size - 7; i < alignCount - 1; i++, pos -= step) { + alignPosition[alignPositionIndex--] = pos; + } + + for (uint8_t i = 0; i < alignCount; i++) { + for (uint8_t j = 0; j < alignCount; j++) { + if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) { + continue; + } else { + drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]); + } + } + } + } + drawFormatBits(modules, isFunction, ecc, 0); + drawVersion(modules, isFunction, version); +} +static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) { + + uint32_t bitLength = codewords->bitOffsetOrWidth; + uint8_t *data = codewords->data; + + uint8_t size = modules->bitOffsetOrWidth; + uint32_t i = 0; + for (int16_t right = size - 1; right >= 1; right -= 2) { + if (right == 6) { right = 5; } + + for (uint8_t vert = 0; vert < size; vert++) { + for (int j = 0; j < 2; j++) { + uint8_t x = right - j; + bool upwards = ((right & 2) == 0) ^ (x < 6); + uint8_t y = upwards ? size - 1 - vert : vert; + if (!bb_getBit(isFunction, x, y) && i < bitLength) { + bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0); + i++; + } + } + } + } +} +static uint32_t getPenaltyScore(BitBucket *modules) { + uint32_t result = 0; + + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t y = 0; y < size; y++) { + + bool colorX = bb_getBit(modules, 0, y); + for (uint8_t x = 1, runX = 1; x < size; x++) { + bool cx = bb_getBit(modules, x, y); + if (cx != colorX) { + colorX = cx; + runX = 1; + + } else { + runX++; + if (runX == 5) { + result += 3; + } else if (runX > 5) { + result++; + } + } + } + } + for (uint8_t x = 0; x < size; x++) { + bool colorY = bb_getBit(modules, x, 0); + for (uint8_t y = 1, runY = 1; y < size; y++) { + bool cy = bb_getBit(modules, x, y); + if (cy != colorY) { + colorY = cy; + runY = 1; + } else { + runY++; + if (runY == 5) { + result += 3; + } else if (runY > 5) { + result++; + } + } + } + } + + uint16_t black = 0; + for (uint8_t y = 0; y < size; y++) { + uint16_t bitsRow = 0, bitsCol = 0; + for (uint8_t x = 0; x < size; x++) { + bool color = bb_getBit(modules, x, y); + if (x > 0 && y > 0) { + bool colorUL = bb_getBit(modules, x - 1, y - 1); + bool colorUR = bb_getBit(modules, x, y - 1); + bool colorL = bb_getBit(modules, x - 1, y); + if (color == colorUL && color == colorUR && color == colorL) { + result += 3; + } + } + bitsRow = ((bitsRow << 1) & 0x7FF) | color; + bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x); + if (x >= 10) { + if (bitsRow == 0x05D || bitsRow == 0x5D0) { + result += 40; + } + if (bitsCol == 0x05D || bitsCol == 0x5D0) { + result += 40; + } + } + if (color) { black++; } + } + } + uint16_t total = size * size; + for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) { + result += 10; + } + + return result; +} + +static uint8_t rs_multiply(uint8_t x, uint8_t y) { + uint16_t z = 0; + for (int8_t i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + return z; +} + +static void rs_init(uint8_t degree, uint8_t *coeff) { + memset(coeff, 0, degree); + coeff[degree - 1] = 1; + uint16_t root = 1; + for (uint8_t i = 0; i < degree; i++) { + + for (uint8_t j = 0; j < degree; j++) { + coeff[j] = rs_multiply(coeff[j], root); + if (j + 1 < degree) { + coeff[j] ^= coeff[j + 1]; + } + } + root = (root << 1) ^ ((root >> 7) * 0x11D); + } +} + +static void rs_getRemainder(uint8_t degree, uint8_t *coeff, const uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) { + for (uint8_t i = 0; i < length; i++) { + uint8_t factor = data[i] ^ result[0]; + for (uint8_t j = 1; j < degree; j++) { + result[(j - 1) * stride] = result[j * stride]; + } + result[(degree - 1) * stride] = 0; + + for (uint8_t j = 0; j < degree; j++) { + result[j * stride] ^= rs_multiply(coeff[j], factor); + } + } +} +static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) { + int8_t mode = 2; + + if (isNumeric((char*)text, length)) { + mode = 0; + bb_appendBits(dataCodewords, 1 << 0, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 0)); + + uint16_t accumData = 0; + uint8_t accumCount = 0; + for (uint16_t i = 0; i < length; i++) { + accumData = accumData * 10 + ((char)(text[i]) - '0'); + accumCount++; + if (accumCount == 3) { + bb_appendBits(dataCodewords, accumData, 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) { + bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1); + } + + } else if (isAlphanumeric((char*)text, length)) { + mode = 1; + bb_appendBits(dataCodewords, 1 << 1, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 1)); + + uint16_t accumData = 0; + uint8_t accumCount = 0; + for (uint16_t i = 0; i < length; i++) { + accumData = accumData * 45 + getAlphanumeric((char)(text[i])); + accumCount++; + if (accumCount == 2) { + bb_appendBits(dataCodewords, accumData, 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) { + bb_appendBits(dataCodewords, accumData, 6); + } + + } else { + bb_appendBits(dataCodewords, 1 << 2, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 2)); + for (uint16_t i = 0; i < length; i++) { + bb_appendBits(dataCodewords, (char)(text[i]), 8); + } + } + + return mode; +} + +static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) { + uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1]; + uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1]; + uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; + uint8_t blockEccLen = totalEcc / numBlocks; + uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks; + uint8_t shortBlockLen = moduleCount / 8 / numBlocks; + + uint8_t shortDataBlockLen = shortBlockLen - blockEccLen; + + uint8_t result[data->capacityBytes]; + memset(result, 0, sizeof(result)); + + uint8_t coeff[blockEccLen]; + rs_init(blockEccLen, coeff); + + uint16_t offset = 0; + uint8_t *dataBytes = data->data; + + for (uint8_t i = 0; i < shortDataBlockLen; i++) { + uint16_t index = i; + uint8_t stride = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) { + result[offset++] = dataBytes[index]; + if (blockNum == numShortBlocks) { stride++; } + + index += stride; + } + } + { + uint16_t index = shortDataBlockLen * (numShortBlocks + 1); + uint8_t stride = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) { + result[offset++] = dataBytes[index]; + + if (blockNum == 0) { stride++; } + index += stride; + } + } + + uint8_t blockSize = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) { + if (blockNum == numShortBlocks) { blockSize++; } + + rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks); + dataBytes += blockSize; + } + + memcpy(data->data, result, data->capacityBytes); + data->bitOffsetOrWidth = moduleCount; +} + +static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0); + +uint16_t qrcode_getBufferSize(uint8_t version) { + return bb_getGridSizeBytes(4 * version + 17); +} +int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) { + uint8_t size = version * 4 + 17; + qrcode->version = version; + qrcode->size = size; + qrcode->ecc = ecc; + qrcode->modules = modules; + + uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03; + uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; + uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1]; + struct BitBucket codewords; + uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)]; + bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes)); + int8_t mode = encodeDataCodewords(&codewords, data, length, version); + + if (mode < 0) { return -1; } + qrcode->mode = mode; + uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth; + if (padding > 4) { padding = 4; } + bb_appendBits(&codewords, 0, padding); + bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8); + for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) { + bb_appendBits(&codewords, padByte, 8); + } + + BitBucket modulesGrid; + bb_initGrid(&modulesGrid, modules, size); + + BitBucket isFunctionGrid; + uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)]; + bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size); + drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits); + performErrorCorrection(version, eccFormatBits, &codewords); + drawCodewords(&modulesGrid, &isFunctionGrid, &codewords); + uint8_t mask = 0; + int32_t minPenalty = (2147483647); + for (uint8_t i = 0; i < 8; i++) { + drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i); + applyMask(&modulesGrid, &isFunctionGrid, i); + int penalty = getPenaltyScore(&modulesGrid); + if (penalty < minPenalty) { + mask = i; + minPenalty = penalty; + } + applyMask(&modulesGrid, &isFunctionGrid, i); + } + + qrcode->mask = mask; + drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask); + applyMask(&modulesGrid, &isFunctionGrid, mask); + + return 0; +} + +int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) { + return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data)); +} + +bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) { + if (x >= qrcode->size || y >= qrcode->size) { + return false; + } + + uint32_t offset = y * qrcode->size + x; + return (qrcode->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; +} + +int get_qr (char *string, uint8_t *qrcodeBitmap) { + // The structure to manage the QR code + QRCode qrcode; + + // Allocate a chunk of memory to store the QR code + uint8_t qrcodeBytes[qrcode_getBufferSize(3)]; + + qrcode_initText(&qrcode, qrcodeBytes, 3, 0, string); + for (uint8_t y = 0; y < qrcode.size; y++) { + for (uint8_t x = 0; x < qrcode.size; x++) { + qrcodeBitmap[x + y * qrcode.size] = qrcode_getModule(&qrcode, x, y); + } + } + return qrcode.size; +} +`); + +function getBinaryQR (value) { + var qrcodeBitmap = new Uint8Array(850); + var flatValue = Uint8Array(E.toArrayBuffer(E.toFlatString(value ,0))); + var valueAddr = E.getAddressOf(flatValue, true); + var qrAddr = E.getAddressOf(qrcodeBitmap, true); + if (valueAddr == 0 || qrAddr == 0) { + console.log ("Failed to get flat arrays.."); + //return; + } + var qrsize = c.get_qr(valueAddr, qrAddr); + return { data: qrcodeBitmap, size: qrsize }; +} + +module.exports = getBinaryQR; diff --git a/apps/cards/screenshot_cards_barcode.png b/apps/cards/screenshot_cards_barcode.png new file mode 100644 index 000000000..e57e9765a Binary files /dev/null and b/apps/cards/screenshot_cards_barcode.png differ diff --git a/apps/cards/screenshot_cards_card1.png b/apps/cards/screenshot_cards_card1.png new file mode 100644 index 000000000..1c8c9514c Binary files /dev/null and b/apps/cards/screenshot_cards_card1.png differ diff --git a/apps/cards/screenshot_cards_card2.png b/apps/cards/screenshot_cards_card2.png new file mode 100644 index 000000000..15ee1cac4 Binary files /dev/null and b/apps/cards/screenshot_cards_card2.png differ diff --git a/apps/cards/screenshot_cards_overview.png b/apps/cards/screenshot_cards_overview.png new file mode 100644 index 000000000..0a933ad0a Binary files /dev/null and b/apps/cards/screenshot_cards_overview.png differ diff --git a/apps/cards/screenshot_cards_qrcode.png b/apps/cards/screenshot_cards_qrcode.png new file mode 100644 index 000000000..5bace3e6e Binary files /dev/null and b/apps/cards/screenshot_cards_qrcode.png differ diff --git a/apps/cards/settings.js b/apps/cards/settings.js new file mode 100644 index 000000000..451b02204 --- /dev/null +++ b/apps/cards/settings.js @@ -0,0 +1,26 @@ +(function(back) { + var settings = require("Storage").readJSON("cards.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("cards.settings.json", settings); + } + var mainmenu = { + "" : { "title" : "Cards" }, + "< Back" : back, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, + /*LANG*/"Use 'Today',..." : { + value : !!settings.useToday, + onchange: v => { + settings.useToday = v; + updateSettings(); + } + }, + /*LANG*/"Full Brightness" : { + value : !!settings.fullBrightness, + onchange: v => { + settings.fullBrightness = v; + updateSettings(); + } + } + }; + E.showMenu(mainmenu); +}) diff --git a/apps/chargent/ChangeLog b/apps/chargent/ChangeLog index d7081ecfb..349618315 100644 --- a/apps/chargent/ChangeLog +++ b/apps/chargent/ChangeLog @@ -3,3 +3,5 @@ 0.03: Added threshold 0.04: Added notification 0.05: Fixed boot +0.06: Allow tap to silence notification/buzzing +0.07: Fix notification-tap silencing and notification length diff --git a/apps/chargent/README.md b/apps/chargent/README.md index db93615a6..5fd486217 100644 --- a/apps/chargent/README.md +++ b/apps/chargent/README.md @@ -6,10 +6,12 @@ The first stage of charging Li-ion ends at ~80% capacity when the charge voltage This app has no UI and no configuration. To disable the app, you have to uninstall it. +Tap the charged notification to prevent buzzing for this charging session. + New in v0.03: before the very first buzz, the average value after the peak is written to chargent.json and used as threshold for future charges. This reduces the time spent in the second charge stage. Side notes - Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100. - This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise. -\* according to https://batteryuniversity.com/article/bu-409-charging-lithium-ion assuming similar characteristics and readouts from pin `D30` approximate charge voltage \ No newline at end of file +\* according to https://batteryuniversity.com/article/bu-409-charging-lithium-ion assuming similar characteristics and readouts from pin `D30` approximate charge voltage diff --git a/apps/chargent/boot.js b/apps/chargent/boot.js index c8fd4f930..42c384711 100644 --- a/apps/chargent/boot.js +++ b/apps/chargent/boot.js @@ -24,7 +24,8 @@ lim = sum / cnt; require('Storage').writeJSON('chargent.json', {limit: lim}); } - require('notify').show({id: 'chargent', title: 'Fully charged'}); + const onHide = () => { if(id) id = clearInterval(id) }; + require('notify').show({id: 'chargent', title: 'Charged', onHide }); // TODO ? customizable Bangle.buzz(500); setTimeout(() => Bangle.buzz(500), 1000); diff --git a/apps/chargent/metadata.json b/apps/chargent/metadata.json index d43493ada..75366ff59 100644 --- a/apps/chargent/metadata.json +++ b/apps/chargent/metadata.json @@ -1,6 +1,6 @@ { "id": "chargent", "name": "Charge Gently", - "version": "0.05", + "version": "0.07", "description": "When charging, reminds you to disconnect the watch to prolong battery life.", "icon": "icon.png", "type": "bootloader", diff --git a/apps/chess/ChangeLog b/apps/chess/ChangeLog index fb08248ff..1fb70549c 100644 --- a/apps/chess/ChangeLog +++ b/apps/chess/ChangeLog @@ -1,2 +1,5 @@ 0.01: New App! 0.02: Bugfixes +0.03: Use Bangle.setBacklight() +0.04: Add option to buzz after computer move +0.05: Minor code improvements diff --git a/apps/chess/app.js b/apps/chess/app.js index 3d584b261..19802083d 100644 --- a/apps/chess/app.js +++ b/apps/chess/app.js @@ -6,18 +6,18 @@ Bangle.loadWidgets(); // load before first appRect call const FIELD_WIDTH = Bangle.appRect.w/8; const FIELD_HEIGHT = Bangle.appRect.h/8; const SETTINGS_FILE = "chess.json"; -const DEFAULT_TIMEOUT = Bangle.getOptions().lockTimeout; const ICON_SIZE=45; -const ICON_BISHOP = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA=")); -const ICON_PAWN = require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA=")); -const ICON_KING = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA==")); -const ICON_QUEEN = require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA=")); -const ICON_ROOK = require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA=")); -const ICON_KNIGHT = require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA")); +const get_icon_bishop = () => require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA=")); +const get_icon_pawn = () => require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA=")); +const get_icon_king = () => require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA==")); +const get_icon_queen = () => require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA=")); +const get_icon_rook = () => require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA=")); +const get_icon_knight = () => require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA")); const settings = Object.assign({ state: engine.P4_INITIAL_BOARD, computer_level: 0, // default to "stupid" which is the fastest + buzz: false, // Buzz when computer move is done }, require("Storage").readJSON(SETTINGS_FILE,1) || {}); const ovr = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,2,{msb:true}); @@ -57,22 +57,22 @@ const drawPiece = (buf, x, y, piece) => { switch(piece & ~0x1) { case engine.P4_PAWN: - icon = ICON_PAWN; + icon = get_icon_pawn(); break; case engine.P4_BISHOP: - icon = ICON_BISHOP; + icon = get_icon_bishop(); break; case engine.P4_KING: - icon = ICON_KING; + icon = get_icon_king(); break; case engine.P4_QUEEN: - icon = ICON_QUEEN; + icon = get_icon_queen(); break; case engine.P4_ROOK: - icon = ICON_ROOK; + icon = get_icon_rook(); break; case engine.P4_KNIGHT: - icon = ICON_KNIGHT; + icon = get_icon_knight(); break; } @@ -178,7 +178,7 @@ const move = (from,to,cbok) => { }; const showMessage = (msg) => { - g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10); + g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10).flip(); }; // Run @@ -192,6 +192,7 @@ Bangle.drawWidgets(); // drag selected field Bangle.on('drag', (ev) => { + if (showmenu) return; const newx = curfield[0]+ev.dx; const newy = curfield[1]+ev.dy; if (newx >= 0 && newx <= 7*FIELD_WIDTH) { @@ -223,32 +224,31 @@ Bangle.on('touch', (button, xy) => { showMessage(/*LANG*/"Moving.."); const posFrom = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT); const posTo = idx2Pos(colTo/FIELD_WIDTH, rowTo/FIELD_HEIGHT); - setTimeout(() => { - const cb = () => { - // human move ok, update - drawBoard(); - drawSelectedField(); - if (!finished) { - // do computer move - Bangle.setLCDTimeout(0.1); // this can take some time, turn off to save power - showMessage(/*LANG*/"Calculating.."); - setTimeout(() => { - const compMove = state.findmove(settings.computer_level+1); - const result = move(compMove[0], compMove[1]); - if (result.ok) { - writeSettings(); - } - Bangle.setLCDPower(true); - Bangle.setLocked(false); - Bangle.setLCDTimeout(DEFAULT_TIMEOUT/1000); // restore - if (!showmenu) { - showAlert(result.string); - } - }, 200); // execute after display update + const cb = () => { + // human move ok, update + drawBoard(); + drawSelectedField(); + if (!finished) { + // do computer move + Bangle.setBacklight(false); // this can take some time, turn off to save power + showMessage(/*LANG*/"Calculating.."); + const compMove = state.findmove(settings.computer_level+1); + const result = move(compMove[0], compMove[1]); + if (result.ok) { + writeSettings(); } - }; - move(posFrom, posTo,cb); - }, 200); // execute after display update + Bangle.setLCDPower(true); + Bangle.setLocked(false); + Bangle.setBacklight(true); + if (settings.buzz) { + Bangle.buzz(500); + } + if (!showmenu) { + showAlert(result.string); + } + } + }; + move(posFrom, posTo,cb); } // piece_sel === 0 startfield[0] = startfield[1] = undefined; piece_sel = 0; @@ -277,7 +277,9 @@ setWatch(() => { E.showMenu({ "" : { title : /*LANG*/"Chess settings" }, "< Back" : () => closeMenu(), + /*LANG*/"Exit" : () => load(), /*LANG*/"New Game" : () => { + finished = false; state = engine.p4_fen2state(engine.P4_INITIAL_BOARD); writeSettings(); closeMenu(); @@ -296,6 +298,12 @@ setWatch(() => { writeSettings(); } }, - /*LANG*/"Exit" : () => load(), + /*LANG*/'Buzz on next turn': { + value: !!settings.buzz, + onchange: v => { + settings.buzz = v; + writeSettings(); + } + }, }); }, BTN, { repeat: true, edge: "falling" }); diff --git a/apps/chess/engine.js b/apps/chess/engine.js index f67ecc456..c30e2587d 100644 --- a/apps/chess/engine.js +++ b/apps/chess/engine.js @@ -14,7 +14,7 @@ if (this.imports !== undefined && this.printerr !== undefined){//seed or gjs p4_log = function(){ var args = Array.prototype.slice.call(arguments); - printerr(args.join(', ')); + this.printerr(args.join(', ')); }; } else if (this.console === undefined){//MSIE diff --git a/apps/chess/metadata.json b/apps/chess/metadata.json index 3c2ea69ac..4f810886b 100644 --- a/apps/chess/metadata.json +++ b/apps/chess/metadata.json @@ -2,7 +2,7 @@ "id": "chess", "name": "Chess", "shortName": "Chess", - "version": "0.02", + "version": "0.05", "description": "Chess game based on the [p4wn engine](https://p4wn.sourceforge.net/). Drag on the touchscreen to move the green cursor onto a piece, select it with a single touch and drag the now red cursor around. Release the piece with another touch to finish the move. The button opens a menu.", "icon": "app.png", "tags": "game", diff --git a/apps/chimer/ChangeLog b/apps/chimer/ChangeLog index 6c6f5312e..7af6c18ea 100644 --- a/apps/chimer/ChangeLog +++ b/apps/chimer/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fixed some sleep bugs. Added a sleep mode toggle 0.03: Reduce busy-loop and code 0.04: Separate buzz-time and sleep-time +0.05: Minor code improvements diff --git a/apps/chimer/metadata.json b/apps/chimer/metadata.json index cfa0da00f..9d2d0a698 100644 --- a/apps/chimer/metadata.json +++ b/apps/chimer/metadata.json @@ -1,7 +1,7 @@ { "id": "chimer", "name": "Chimer", - "version": "0.04", + "version": "0.05", "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Repeat Chime up to 3 times \n - Set hours to disable chime", "icon": "widget.png", "type": "widget", diff --git a/apps/chimer/widget.js b/apps/chimer/widget.js index 3b7de9d7a..d27ecd78f 100644 --- a/apps/chimer/widget.js +++ b/apps/chimer/widget.js @@ -53,9 +53,7 @@ function check() { const now = new Date(), h = now.getHours(), - m = now.getMinutes(), - s = now.getSeconds(), - ms = now.getMilliseconds(); + m = now.getMinutes(); if (settings.sleep && ( h > settings.end || (h >= settings.end && m !== 0) || diff --git a/apps/chronlog/ChangeLog b/apps/chronlog/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/chronlog/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/chronlog/README.md b/apps/chronlog/README.md new file mode 100644 index 000000000..7c542cf73 --- /dev/null +++ b/apps/chronlog/README.md @@ -0,0 +1,53 @@ +# Chrono Logger + +Record times active on a task, course, work or anything really. + +**Disclaimer:** No one is responsible for any loss of data you recorded with this app. If you run into problems please report as advised under **Requests** below. + +With time on your side and a little help from your friends - you'll surely triumph over Lavos in the end! + +![dump](dump.png) ![dump1](dump1.png) ![dump2](dump2.png) ![dump3](dump3.png) ![dump4](dump4.png) ![dump5](dump5.png) ![dump6](dump6.png) + + +## Usage + +Click the large green button to log the start of your activity. Click the now red button again to log that you stopped. + +## Features + +- Saves to file on every toggling of the active state. + - csv file contents looks like: + ``` + 1,Start,2024-03-02T15:18:09 GMT+0200 + 2,Note,Critical hit! + 3,Stop,2024-03-02T15:19:17 GMT+0200 + ``` +- Add annotations to the log. +- Create and switch between multiple logs. +- Sync log files to an Android device through Gadgetbridge (Needs pending code changes to Gadgetbridge). +- App state is restored when you start the app again. + +## Controls + +- Large button to toggle active state. +- Menu icon to access additional functionality. +- Hardware button exits menus, closes the app on the main screen. + +## TODO and notes + +- Delete individual tasks/logs through the app? +- Reset everything through the app? +- Scan for chronlog storage files that somehow no longer have tasks associated with it? +- Complete the Gadgetbridge side of things for sync. +- Sync to iOS? +- Inspect log files through the app, similarly to Recorder app? +- Changes to Android file system permissions makes it not always trivial to access the synced files. + + +## Requests + +Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions. + +## Creator + +[thyttan](https://github.com/thyttan) diff --git a/apps/chronlog/app-icon.js b/apps/chronlog/app-icon.js new file mode 100644 index 000000000..dc25e4b5b --- /dev/null +++ b/apps/chronlog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///gElq3X0ELJf4AiitAAYMBqgKEgNVrgEBmtVCAQABgtVr/Agf1qtQEQlpq6QB6tpEgkVywLDywLEq2uyoLB6wEBBZAECBYda32lBYIECBZ9W3wjDAgILPquWqoACAgILEtILDAgKOEAAyQCRwIAGSAUVBY6ECBZYGD7WnAoYLF9WrBYupAoWq1QECtQLBtWdBYt21QLC1LfBBYVfA4ILBlWq1f9rWVv/q1WoBYMKCgOvTYP6AoOgBYMCAoIAFwCQCBY6nDGAIAEFwQkIEQZVCBQZRCAAcGBYeQBYoYDCwwYECw5KC0gKIAH4APA=")) diff --git a/apps/chronlog/app.js b/apps/chronlog/app.js new file mode 100644 index 000000000..827ca14e1 --- /dev/null +++ b/apps/chronlog/app.js @@ -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(); +} diff --git a/apps/chronlog/app.png b/apps/chronlog/app.png new file mode 100644 index 000000000..c21a147ea Binary files /dev/null and b/apps/chronlog/app.png differ diff --git a/apps/chronlog/dump.png b/apps/chronlog/dump.png new file mode 100644 index 000000000..0c40b190e Binary files /dev/null and b/apps/chronlog/dump.png differ diff --git a/apps/chronlog/dump1.png b/apps/chronlog/dump1.png new file mode 100644 index 000000000..04f625f04 Binary files /dev/null and b/apps/chronlog/dump1.png differ diff --git a/apps/chronlog/dump2.png b/apps/chronlog/dump2.png new file mode 100644 index 000000000..be0791659 Binary files /dev/null and b/apps/chronlog/dump2.png differ diff --git a/apps/chronlog/dump3.png b/apps/chronlog/dump3.png new file mode 100644 index 000000000..eeeba525f Binary files /dev/null and b/apps/chronlog/dump3.png differ diff --git a/apps/chronlog/dump4.png b/apps/chronlog/dump4.png new file mode 100644 index 000000000..b1bd51669 Binary files /dev/null and b/apps/chronlog/dump4.png differ diff --git a/apps/chronlog/dump5.png b/apps/chronlog/dump5.png new file mode 100644 index 000000000..debf919cc Binary files /dev/null and b/apps/chronlog/dump5.png differ diff --git a/apps/chronlog/dump6.png b/apps/chronlog/dump6.png new file mode 100644 index 000000000..29a06b68f Binary files /dev/null and b/apps/chronlog/dump6.png differ diff --git a/apps/chronlog/metadata.json b/apps/chronlog/metadata.json new file mode 100644 index 000000000..8ed618b27 --- /dev/null +++ b/apps/chronlog/metadata.json @@ -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} + ] +} diff --git a/apps/chrono/ChangeLog b/apps/chrono/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/chrono/ChangeLog +++ b/apps/chrono/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/chrono/chrono.js b/apps/chrono/chrono.js index cd50b8a22..5b9e6eda9 100644 --- a/apps/chrono/chrono.js +++ b/apps/chrono/chrono.js @@ -69,5 +69,5 @@ Bangle.on('touch', function (button) { started = !started; }); -var interval = setInterval(countDown, 1000); +setInterval(countDown, 1000); drawInterface(); \ No newline at end of file diff --git a/apps/chrono/metadata.json b/apps/chrono/metadata.json index 59fc1dbeb..aa8a68b20 100644 --- a/apps/chrono/metadata.json +++ b/apps/chrono/metadata.json @@ -2,7 +2,7 @@ "id": "chrono", "name": "Chrono", "shortName": "Chrono", - "version": "0.01", + "version": "0.02", "description": "Single click BTN1 to add 5 minutes. Single click BTN2 to add 30 seconds. Single click BTN3 to add 5 seconds. Tap to pause or play to timer. Double click BTN1 to reset. When timer finishes the watch vibrates.", "icon": "chrono.png", "tags": "tool", diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index 9838ae409..4e10bfe49 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -42,3 +42,4 @@ 0.23: Setting circles colours per clkinfo and not position 0.24: Using suggested color from clock_info if set as default and available 0.25: Use clock_info module as an app +0.26: Minor code improvements diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 6f76ff013..9be308bb3 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -11,7 +11,6 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) { }; { -let clock_info = require("clock_info"); let locale = require("locale"); let storage = require("Storage"); @@ -26,7 +25,7 @@ const showWidgets = settings.showWidgets || false; const circleCount = settings.circleCount || 3; const showBigWeather = settings.showBigWeather || false; -let now = Math.round(new Date().getTime() / 1000); +//let now = Math.round(new Date().getTime() / 1000); // layout values: let colorFg = g.theme.dark ? '#fff' : '#000'; @@ -87,7 +86,7 @@ let draw = function() { g.setFontAlign(-1, -1); g.drawString(locale.time(new Date(), 1), 2, h1 + 6); } - now = Math.round(new Date().getTime() / 1000); + //now = Math.round(new Date().getTime() / 1000); // date & dow g.setFontRobotoRegular21(); @@ -171,6 +170,7 @@ let getCircleIconColor = function(index, color, percent) { } } +/* let drawEmpty = function(img, w, color) { drawGauge(w, h3, 0, color); drawInnerCircleAndTriangle(w); @@ -179,6 +179,7 @@ let drawEmpty = function(img, w, color) { g.setColor(getGradientColor(color, 0)) .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24}); } +*/ let drawCircle = function(index, item, data, clkmenu) { var w = circlePosX[index-1]; diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index f96a7872c..713b237ac 100644 --- a/apps/circlesclock/metadata.json +++ b/apps/circlesclock/metadata.json @@ -1,7 +1,7 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.25", + "version": "0.26", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style", "icon": "app.png", "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}], diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 63a2b0f93..714b48f2e 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -30,7 +30,6 @@ }, /*LANG*/'show widgets': { value: !!settings.showWidgets, - format: () => (settings.showWidgets ? 'Yes' : 'No'), onchange: x => save('showWidgets', x), }, /*LANG*/'update interval': { @@ -45,7 +44,6 @@ }, /*LANG*/'show big weather': { value: !!settings.showBigWeather, - format: () => (settings.showBigWeather ? 'Yes' : 'No'), onchange: x => save('showBigWeather', x), }, /*LANG*/'colorize icons': ()=>showCircleMenus() @@ -83,12 +81,11 @@ }; for(var circleId=1; circleId<=4; ++circleId) { const circleName = "circle" + circleId; - const colorKey = circleName + "color"; + //const colorKey = circleName + "color"; const colorizeIconKey = circleName + "colorizeIcon"; menu[/*LANG*/'circle ' + circleId] = { value: settings[colorizeIconKey] || false, - format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'), - onchange: x => save(colorizeIconKey, x), + onchange: x => save(colorizeIconKey, x), }; } E.showMenu(menu); diff --git a/apps/clickms/ChangeLog b/apps/clickms/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/clickms/ChangeLog +++ b/apps/clickms/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/clickms/click-master.js b/apps/clickms/click-master.js index 55027e733..a5b3d1a7e 100644 --- a/apps/clickms/click-master.js +++ b/apps/clickms/click-master.js @@ -19,7 +19,7 @@ function drawPlayers() { g.clear(1); g.setFont("6x8",2); var l = [{name:ME,cnt:mycounter}]; - for (p of players) l.push(p); + for (const p of players) l.push(p); l.sort((a,b)=>a.cnt-b.cnt); var y=0; l.forEach(player=>{ diff --git a/apps/clickms/metadata.json b/apps/clickms/metadata.json index baa8c9563..c07f83dcd 100644 --- a/apps/clickms/metadata.json +++ b/apps/clickms/metadata.json @@ -1,7 +1,7 @@ { "id": "clickms", "name": "Click Master", - "version": "0.01", + "version": "0.02", "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", "icon": "click-master.png", "tags": "game", diff --git a/apps/cliclockJS2Enhanced/ChangeLog b/apps/cliclockJS2Enhanced/ChangeLog index f4d146d5f..7e0b4fd69 100644 --- a/apps/cliclockJS2Enhanced/ChangeLog +++ b/apps/cliclockJS2Enhanced/ChangeLog @@ -1,3 +1,4 @@ 0.01: Submitted to App Loader 0.02: Removed unneded code, added HID controlls thanks to t0m1o1 for his code :p 0.03: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) +0.04: Minor code improvements diff --git a/apps/cliclockJS2Enhanced/app.js b/apps/cliclockJS2Enhanced/app.js index b6172b497..b99ed2542 100644 --- a/apps/cliclockJS2Enhanced/app.js +++ b/apps/cliclockJS2Enhanced/app.js @@ -148,7 +148,7 @@ g.clear(); Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); -var click = setInterval(updateTime, 1000); +/*var click =*/ setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ drawAll(); // why do we redraw here?? diff --git a/apps/cliclockJS2Enhanced/metadata.json b/apps/cliclockJS2Enhanced/metadata.json index f428650a7..dcbf5da63 100644 --- a/apps/cliclockJS2Enhanced/metadata.json +++ b/apps/cliclockJS2Enhanced/metadata.json @@ -2,7 +2,7 @@ "id": "cliclockJS2Enhanced", "name": "Commandline-Clock JS2 Enhanced", "shortName": "CLI-Clock JS2", - "version": "0.03", + "version": "0.04", "description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design. Also added HID media controlls, just swipe on the clock face to controll the media! Gadgetbride support coming soon(hopefully) Thanks to t0m1o1 for media controls!", "icon": "app.png", "screenshots": [{"url":"screengrab.png"}], diff --git a/apps/clicompleteclk/ChangeLog b/apps/clicompleteclk/ChangeLog index 50c84593e..436549f8a 100644 --- a/apps/clicompleteclk/ChangeLog +++ b/apps/clicompleteclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New clock! 0.02: Load steps from Health Tracking app (if installed) 0.03: ... +0.04: Minor code improvements diff --git a/apps/clicompleteclk/app.js b/apps/clicompleteclk/app.js index a39b37e58..7472907e1 100644 --- a/apps/clicompleteclk/app.js +++ b/apps/clicompleteclk/app.js @@ -17,8 +17,8 @@ const textColorRed = g.theme.dark ? "#FF0000" : "#FF0000"; let hrtValue; let hrtValueIsOld = false; -let localTempValue; -let weatherTempString; +//let localTempValue; +//let weatherTempString; let lastHeartRateRowIndex; let lastStepsRowIndex; let i = 2; @@ -114,7 +114,7 @@ function drawWeather() { const currentWeather = weatherJson.weather; const weatherTempValue = locale.temp(currentWeather.temp-273.15); - weatherTempString = weatherTempValue; + //weatherTempString = weatherTempValue; writeLineTopic("WTHR", i); writeLine(currentWeather.txt,i); i++; diff --git a/apps/clicompleteclk/metadata.json b/apps/clicompleteclk/metadata.json index 8753c3c37..4d4f7e5ef 100644 --- a/apps/clicompleteclk/metadata.json +++ b/apps/clicompleteclk/metadata.json @@ -2,7 +2,7 @@ "id": "clicompleteclk", "name": "CLI complete clock", "shortName":"CLI cmplt clock", - "version":"0.03", + "version": "0.04", "description": "Command line styled clock with lots of information", "icon": "app.png", "allow_emulator": true, diff --git a/apps/clicompleteclk/settings.js b/apps/clicompleteclk/settings.js index 2df20ed3e..0213ead6e 100644 --- a/apps/clicompleteclk/settings.js +++ b/apps/clicompleteclk/settings.js @@ -9,7 +9,6 @@ '': { 'title': 'CLI complete clk' }, 'Show battery': { value: "battery" in settings ? settings.battery : false, - format: () => (settings.battery ? 'Yes' : 'No'), onchange: () => { settings.battery = !settings.battery; save('battery', settings.battery); @@ -27,7 +26,6 @@ }, 'Show weather': { value: "weather" in settings ? settings.weather : false, - format: () => (settings.weather ? 'Yes' : 'No'), onchange: () => { settings.weather = !settings.weather; save('weather', settings.weather); @@ -35,7 +33,6 @@ }, 'Show steps': { value: "steps" in settings ? settings.steps : false, - format: () => (settings.steps ? 'Yes' : 'No'), onchange: () => { settings.steps = !settings.steps; save('steps', settings.steps); @@ -43,7 +40,6 @@ }, 'Show heartrate': { value: "heartrate" in settings ? settings.heartrate : false, - format: () => (settings.heartrate ? 'Yes' : 'No'), onchange: () => { settings.heartrate = !settings.heartrate; save('heartrate', settings.heartrate); diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog index 68249b622..83bd2eb39 100644 --- a/apps/cliock/ChangeLog +++ b/apps/cliock/ChangeLog @@ -8,3 +8,4 @@ 0.14: Fix BTN1 (fix #853) Add light/dark theme support 0.15: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) +0.16: Minor code improvements diff --git a/apps/cliock/app.js b/apps/cliock/app.js index d9271bf15..c1b3a3106 100644 --- a/apps/cliock/app.js +++ b/apps/cliock/app.js @@ -186,7 +186,7 @@ g.clear(); Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); -var click = setInterval(updateTime, 1000); +/*var click =*/ setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ if (btn<0) changeInfoMode(); diff --git a/apps/cliock/metadata.json b/apps/cliock/metadata.json index 2df48892e..ff50e3869 100644 --- a/apps/cliock/metadata.json +++ b/apps/cliock/metadata.json @@ -2,7 +2,7 @@ "id": "cliock", "name": "Commandline-Clock", "shortName": "CLI-Clock", - "version": "0.15", + "version": "0.16", "description": "Simple CLI-Styled Clock", "icon": "app.png", "screenshots": [{"url":"screenshot_cli.png"}], diff --git a/apps/clkinfocal/ChangeLog b/apps/clkinfocal/ChangeLog index 5560f00bc..ccb73b648 100644 --- a/apps/clkinfocal/ChangeLog +++ b/apps/clkinfocal/ChangeLog @@ -1 +1,5 @@ 0.01: New App! +0.02: added settings options to change date format +0.03: Remove un-needed font requirement, now outputs transparent image +0.04: Fix image after 0.03 regression +0.05: Remove duplicated day in calendar when format setting is 'dd MMM' \ No newline at end of file diff --git a/apps/clkinfocal/clkinfo.js b/apps/clkinfocal/clkinfo.js index a7949cda4..dc93ddd0e 100644 --- a/apps/clkinfocal/clkinfo.js +++ b/apps/clkinfocal/clkinfo.js @@ -1,5 +1,18 @@ (function() { - require("Font4x8Numeric").add(Graphics); + var settings = require("Storage").readJSON("clkinfocal.json",1)||{}; + settings.fmt = settings.fmt||"DDD"; + + var getDateString = function(dt) { + switch(settings.fmt) { + case "dd MMM": + return require("locale").month(dt,1).toUpperCase(); + case "DDD dd": + return require("locale").dow(dt,1).toUpperCase() + ' ' + dt.getDate(); + default: // DDD + return require("locale").dow(dt,1).toUpperCase(); + } + }; + return { name: "Bangle", items: [ @@ -7,10 +20,11 @@ get : () => { let d = new Date(); let g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.transparent = 0; 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(), + text : getDateString(d), img : g.asImage("string") }; }, diff --git a/apps/clkinfocal/metadata.json b/apps/clkinfocal/metadata.json index 1d14c3b59..71dc811e9 100644 --- a/apps/clkinfocal/metadata.json +++ b/apps/clkinfocal/metadata.json @@ -1,13 +1,15 @@ { "id": "clkinfocal", "name": "Calendar Clockinfo", - "version":"0.01", - "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday", + "version":"0.05", + "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday. There is also a settings menu to select the format of the text", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clkinfo", "tags": "clkinfo,calendar", "supports" : ["BANGLEJS2"], "storage": [ - {"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"} - ] + {"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"}, + {"name":"clkinfocal.settings.js","url":"settings.js"} + ], + "data": [{"name":"clkinfocal.json"}] } diff --git a/apps/clkinfocal/settings.js b/apps/clkinfocal/settings.js new file mode 100644 index 000000000..6fe8f2817 --- /dev/null +++ b/apps/clkinfocal/settings.js @@ -0,0 +1,37 @@ +(function(back) { + const SETTINGS_FILE = "clkinfocal.json"; + + // initialize with default settings... + let s = {'fmt': 0}; + + // and overwrite them with any saved values + // this way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + const saved = settings || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + function save() { + settings = s; + storage.write(SETTINGS_FILE, settings); + } + + var date_options = ["DDD","DDD dd","dd MMM"]; + + E.showMenu({ + '': { 'title': 'Cal Clkinfo' }, + '< Back': back, + 'Format': { + value: 0 | date_options.indexOf(s.fmt), + min: 0, max: 2, + format: v => date_options[v], + onchange: v => { + s.fmt = date_options[v]; + save(); + }, + } + }); + +}); diff --git a/apps/clkinfoclk/ChangeLog b/apps/clkinfoclk/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/clkinfoclk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/clkinfoclk/app.png b/apps/clkinfoclk/app.png new file mode 100644 index 000000000..cf057046b Binary files /dev/null and b/apps/clkinfoclk/app.png differ diff --git a/apps/clkinfoclk/clkinfo.js b/apps/clkinfoclk/clkinfo.js new file mode 100644 index 000000000..f98edb5e7 --- /dev/null +++ b/apps/clkinfoclk/clkinfo.js @@ -0,0 +1,27 @@ +(function() { + return { + name: "Bangle", + items: [ + { name : "Clock", + get : () => { + return { + text : require("locale").time(new Date(),1), + img : atob("FhaBAAAAAAPwAD/wA8DwHADgYMGDAwMMDAxgMBmAwGYDAZgOBmAcGYA4YwBjDAAMGABgcAOA8DwA/8AA/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; + } + } + ] + }; +}) diff --git a/apps/clkinfoclk/metadata.json b/apps/clkinfoclk/metadata.json new file mode 100644 index 000000000..8d676d0e0 --- /dev/null +++ b/apps/clkinfoclk/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfoclk", + "name": "Clockinfo Clock", + "version":"0.01", + "description": "This displays a clock *inside* a ClockInfo. This can be really handy for the [Clock Info Widget](https://banglejs.com/apps/?id=widclkinfo) where you might want the option to show a clock in the top bar of a non-clock app.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfoclk.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfoclk/screenshot.png b/apps/clkinfoclk/screenshot.png new file mode 100644 index 000000000..00f4c0c9a Binary files /dev/null and b/apps/clkinfoclk/screenshot.png differ diff --git a/apps/clkinfogps/clkinfo.js b/apps/clkinfogps/clkinfo.js index ed2fc721a..7db9bbdae 100644 --- a/apps/clkinfogps/clkinfo.js +++ b/apps/clkinfogps/clkinfo.js @@ -91,7 +91,7 @@ }; var info = { - name: "Gps", + name: "GPS", items: [ { name: "gridref", diff --git a/apps/clkinfogps/geotools.js b/apps/clkinfogps/geotools.js index d251c1f95..2f25c8453 100644 --- a/apps/clkinfogps/geotools.js +++ b/apps/clkinfogps/geotools.js @@ -1,5 +1,5 @@ /** - * + * * A module of Geo functions for use with gps fixes * * let geo = require("geotools"); @@ -71,7 +71,7 @@ OsGridRef.latLongToOsGrid = function(point) { * */ function to_map_ref(digits, easting, northing) { - if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing + if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); let e = easting; let n = northing; @@ -108,7 +108,7 @@ function to_map_ref(digits, easting, northing) { } /** - * + * * Module exports section, example code below * * let geo = require("geotools"); diff --git a/apps/clkinfogpsspeed/ChangeLog b/apps/clkinfogpsspeed/ChangeLog new file mode 100644 index 000000000..78ba28f3b --- /dev/null +++ b/apps/clkinfogpsspeed/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock Info! diff --git a/apps/clkinfogpsspeed/clkinfo.js b/apps/clkinfogpsspeed/clkinfo.js new file mode 100644 index 000000000..a2c1f51c1 --- /dev/null +++ b/apps/clkinfogpsspeed/clkinfo.js @@ -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! \ No newline at end of file diff --git a/apps/clkinfogpsspeed/icon.png b/apps/clkinfogpsspeed/icon.png new file mode 100644 index 000000000..10186f13f Binary files /dev/null and b/apps/clkinfogpsspeed/icon.png differ diff --git a/apps/clkinfogpsspeed/metadata.json b/apps/clkinfogpsspeed/metadata.json new file mode 100644 index 000000000..7fceeb7b8 --- /dev/null +++ b/apps/clkinfogpsspeed/metadata.json @@ -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"} + ] +} diff --git a/apps/clkinfosec/ChangeLog b/apps/clkinfosec/ChangeLog new file mode 100644 index 000000000..11fefd24b --- /dev/null +++ b/apps/clkinfosec/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Remove 's' after seconds (on some clocks this looks like '5') \ No newline at end of file diff --git a/apps/clkinfosec/app.png b/apps/clkinfosec/app.png new file mode 100644 index 000000000..ed79cd884 Binary files /dev/null and b/apps/clkinfosec/app.png differ diff --git a/apps/clkinfosec/clkinfo.js b/apps/clkinfosec/clkinfo.js new file mode 100644 index 000000000..d407228db --- /dev/null +++ b/apps/clkinfosec/clkinfo.js @@ -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; + } + } + ] + }; +}) \ No newline at end of file diff --git a/apps/clkinfosec/metadata.json b/apps/clkinfosec/metadata.json new file mode 100644 index 000000000..aaf96e9bb --- /dev/null +++ b/apps/clkinfosec/metadata.json @@ -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"} + ] +} diff --git a/apps/clkinfosec/screenshot.png b/apps/clkinfosec/screenshot.png new file mode 100644 index 000000000..bb054e3a4 Binary files /dev/null and b/apps/clkinfosec/screenshot.png differ diff --git a/apps/clkinfosunrise/ChangeLog b/apps/clkinfosunrise/ChangeLog index 86e7a7fa8..a89e38715 100644 --- a/apps/clkinfosunrise/ChangeLog +++ b/apps/clkinfosunrise/ChangeLog @@ -2,3 +2,5 @@ 0.02: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps Add a 'time' clockinfo that also displays a percentage of day left 0.03: Change 3rd mode to show the time to next sunrise/sunset time (not actual time) +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/clkinfosunrise/clkinfo.js b/apps/clkinfosunrise/clkinfo.js index 22c507f34..69df208f4 100644 --- a/apps/clkinfosunrise/clkinfo.js +++ b/apps/clkinfosunrise/clkinfo.js @@ -49,7 +49,8 @@ let day = true; let d = date.getTime(); let dayLength = sunset.getTime()-sunrise.getTime(); - let timeUntil, timeTotal; + let timePast; + let timeTotal; if (d < sunrise.getTime()) { day = false; // early morning timePast = sunrise.getTime()-d; diff --git a/apps/clkinfosunrise/metadata.json b/apps/clkinfosunrise/metadata.json index 7bcbb289b..723f1f0a5 100644 --- a/apps/clkinfosunrise/metadata.json +++ b/apps/clkinfosunrise/metadata.json @@ -1,6 +1,6 @@ { "id": "clkinfosunrise", "name": "Sunrise Clockinfo", - "version":"0.03", + "version": "0.05", "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays sunrise and sunset based on the location from the 'My Location' app", "icon": "app.png", "type": "clkinfo", diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog index 870808eff..8276321ac 100644 --- a/apps/clock_info/ChangeLog +++ b/apps/clock_info/ChangeLog @@ -6,3 +6,8 @@ 0.05: Reported image for battery is now transparent (2v18+) 0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing 0.07: Developer tweak: clkinfo load errors are emitted +0.08: Pass options to show(), hide() and run(), and add focus() and blur() item methods +0.09: Save clkinfo settings on kill and remove +0.10: Fix focus bug when changing focus between two clock infos +0.11: Prepend swipe listener if possible +0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle \ No newline at end of file diff --git a/apps/clock_info/README.md b/apps/clock_info/README.md index 7e1a3d637..031f89121 100644 --- a/apps/clock_info/README.md +++ b/apps/clock_info/README.md @@ -70,10 +70,12 @@ Note that each item is an object with: } ``` -* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get' -* `item.hide` : called when item should be hidden. Disables updates. +* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get'. Passed the clockinfo options (same as what's returned from `addInteractive`). +* `item.hide` : called when item should be hidden. Disables updates. Passed the clockinfo options. * `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show') * `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user. +* `item.focus` : called when the item is focussed (the user has tapped on it). Passed the clockinfo options. +* `item.blur` : called when the item is unfocussed (the user has tapped elsewhere, the screen has locked, etc). Passed the clockinfo options. See the bottom of `lib.js` for example usage... diff --git a/apps/clock_info/lib.js b/apps/clock_info/lib.js index e6c9eb27f..a9ca7de31 100644 --- a/apps/clock_info/lib.js +++ b/apps/clock_info/lib.js @@ -224,6 +224,13 @@ exports.addInteractive = function(menu, options) { options.menuB = b; } } + const save = () => { + // save the currently showing clock_info + const settings = exports.loadSettings(); + settings.apps[appName] = {a:options.menuA, b:options.menuB}; + require("Storage").writeJSON("clock_info.json",settings); + }; + E.on("kill", save); if (options.menuA===undefined) options.menuA = 0; if (options.menuB===undefined) options.menuB = Math.min(exports.loadCount, menu[options.menuA].items.length)-1; @@ -234,7 +241,7 @@ exports.addInteractive = function(menu, options) { options.redrawHandler = ()=>drawItem(itm); itm.on('redraw', options.redrawHandler); itm.uses = (0|itm.uses)+1; - if (itm.uses==1) itm.show(); + if (itm.uses==1) itm.show(options); itm.emit("redraw"); } function menuHideItem(itm) { @@ -242,7 +249,7 @@ exports.addInteractive = function(menu, options) { delete options.redrawHandler; itm.uses--; if (!itm.uses) - itm.hide(); + itm.hide(options); } // handling for swipe between menu items function swipeHandler(lr,ud){ @@ -276,46 +283,51 @@ exports.addInteractive = function(menu, options) { oldMenuItem.removeAllListeners("draw"); menuShowItem(menu[options.menuA].items[options.menuB]); } - // save the currently showing clock_info - let settings = exports.loadSettings(); - settings.apps[appName] = {a:options.menuA,b:options.menuB}; - require("Storage").writeJSON("clock_info.json",settings); // On 2v18+ firmware we can stop other event handlers from being executed since we handled this E.stopEventPropagation&&E.stopEventPropagation(); } - Bangle.on("swipe",swipeHandler); + if (Bangle.prependListener) {Bangle.prependListener("swipe",swipeHandler);} else {Bangle.on("swipe",swipeHandler);} + const blur = () => { + options.focus=false; + Bangle.CLKINFO_FOCUS--; + const itm = menu[options.menuA].items[options.menuB]; + let redraw = true; + if (itm.blur && itm.blur(options) === false) + redraw = false; + if (redraw) options.redraw(); + }; + const focus = () => { + let redraw = true; + Bangle.CLKINFO_FOCUS = (0 | Bangle.CLKINFO_FOCUS) + 1; + if (!options.focus) { + options.focus=true; + const itm = menu[options.menuA].items[options.menuB]; + if (itm.focus && itm.focus(options) === false) + redraw = false; + } + if (redraw) options.redraw(); + }; let touchHandler, lockHandler; if (options.x!==undefined && options.y!==undefined && options.w && options.h) { touchHandler = function(_,e) { if (e.x(options.x+options.w) || e.y>(options.y+options.h)) { - if (options.focus) { - options.focus=false; - delete Bangle.CLKINFO_FOCUS; - options.redraw(); - } + if (options.focus) + blur(); return; // outside area } if (!options.focus) { - options.focus=true; // if not focussed, set focus - Bangle.CLKINFO_FOCUS=true; - options.redraw(); + focus(); } else if (menu[options.menuA].items[options.menuB].run) { Bangle.buzz(100, 0.7); - menu[options.menuA].items[options.menuB].run(); // allow tap on an item to run it (eg home assistant) - } else { - options.focus=true; - Bangle.CLKINFO_FOCUS=true; + menu[options.menuA].items[options.menuB].run(options); // allow tap on an item to run it (eg home assistant) } }; Bangle.on("touch",touchHandler); if (settings.defocusOnLock) { lockHandler = function() { - if(options.focus) { - options.focus=false; - delete Bangle.CLKINFO_FOCUS; - options.redraw(); - } + if(options.focus) + blur(); }; Bangle.on("lock", lockHandler); } @@ -324,10 +336,12 @@ exports.addInteractive = function(menu, options) { menuShowItem(menu[options.menuA].items[options.menuB]); // return an object with info that can be used to remove the info options.remove = function() { + save(); + E.removeListener("kill", save); Bangle.removeListener("swipe",swipeHandler); if (touchHandler) Bangle.removeListener("touch",touchHandler); if (lockHandler) Bangle.removeListener("lock", lockHandler); - delete Bangle.CLKINFO_FOCUS; + Bangle.CLKINFO_FOCUS--; menuHideItem(menu[options.menuA].items[options.menuB]); exports.loadCount--; delete exports.clockInfos[options.index]; @@ -352,10 +366,51 @@ exports.addInteractive = function(menu, options) { return true; }; + if (options.focus) focus(); delete settings; // don't keep settings in RAM - save space return options; }; +/* clockinfos usually return a 24x24 image. This draws that image but +recolors it such that it is transparent, with the middle of the image as background +and the image itself as foreground. options is passed to g.drawImage */ +exports.drawFilledImage = function(img,x,y,options) { + if (!img) return; + if (!g.floodFill/*2v18+*/) return g.drawImage(img,x,y,options); + let gfx = exports.imgGfx; + if (!gfx) { + gfx = exports.imgGfx = Graphics.createArrayBuffer(26, 26, 2, {msb:true}); + gfx.transparent = 3; + gfx.palette = new Uint16Array([g.theme.bg, g.theme.fg, g.toColor("#888"), g.toColor("#888")]); + } + /* img is (usually) a black and white transparent image. But we really would like the bits in + the middle of it to be white. So what we do is we draw a slightly bigger rectangle in white, + draw the image, and then flood-fill the rectangle back to the background color. floodFill + was only added in 2v18 so we have to check for it and fallback if not. */ + gfx.clear(1).setColor(1).drawImage(img, 1,1).floodFill(0,0,3); + var scale = (options && options.scale) || 1; + return g.drawImage(gfx, x-scale,y-scale,options); +}; + +/* clockinfos usually return a 24x24 image. This creates a 26x26 gfx of the image but +recolors it such that it is transparent, with the middle and border of the image as background +and the image itself as foreground. options is passed to g.drawImage */ +exports.drawBorderedImage = function(img,x,y,options) { + if (!img) return; + if (!g.floodFill/*2v18+*/) return g.drawImage(img,x,y,options); + let gfx = exports.imgGfxB; + if (!gfx) { + gfx = exports.imgGfxB = Graphics.createArrayBuffer(28, 28, 2, {msb:true}); + gfx.transparent = 3; + gfx.palette = new Uint16Array([g.theme.bg, g.theme.fg, g.theme.bg/*border*/, g.toColor("#888")]); + } + gfx.clear(1).setColor(2).drawImage(img, 1,1).drawImage(img, 3,1).drawImage(img, 1,3).drawImage(img, 3,3); // border + gfx.setColor(1).drawImage(img, 2,2); // main image + gfx.floodFill(27,27,3); // flood fill edge to transparent + var o = ((options && options.scale) || 1)*2; + return g.drawImage(gfx, x-o,y-o,options); +}; + // Code for testing (plots all elements from first list) /* g.clear(); diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json index 993f112e7..3d47c5062 100644 --- a/apps/clock_info/metadata.json +++ b/apps/clock_info/metadata.json @@ -1,11 +1,11 @@ { "id": "clock_info", "name": "Clock Info Module", "shortName": "Clock Info", - "version":"0.07", + "version":"0.12", "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", "icon": "app.png", "type": "module", - "tags": "clkinfo", + "tags": "clkinfo,clockinfo", "supports" : ["BANGLEJS2"], "provides_modules" : ["clock_info"], "readme": "README.md", diff --git a/apps/clockbg/ChangeLog b/apps/clockbg/ChangeLog new file mode 100644 index 000000000..026dc1aa0 --- /dev/null +++ b/apps/clockbg/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Moved settings into 'Settings->Apps' +0.03: Add 'Squares' option for random squares background \ No newline at end of file diff --git a/apps/clockbg/README.md b/apps/clockbg/README.md new file mode 100644 index 000000000..14bbeb7a7 --- /dev/null +++ b/apps/clockbg/README.md @@ -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 \ No newline at end of file diff --git a/apps/clockbg/app.png b/apps/clockbg/app.png new file mode 100644 index 000000000..21e39a4e5 Binary files /dev/null and b/apps/clockbg/app.png differ diff --git a/apps/clockbg/img/README.md b/apps/clockbg/img/README.md new file mode 100644 index 000000000..17846e6b8 --- /dev/null +++ b/apps/clockbg/img/README.md @@ -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) + + + + diff --git a/apps/clockbg/img/ai_eye.jpeg b/apps/clockbg/img/ai_eye.jpeg new file mode 100644 index 000000000..e41d40457 Binary files /dev/null and b/apps/clockbg/img/ai_eye.jpeg differ diff --git a/apps/clockbg/img/ai_flow.jpeg b/apps/clockbg/img/ai_flow.jpeg new file mode 100644 index 000000000..f30f89843 Binary files /dev/null and b/apps/clockbg/img/ai_flow.jpeg differ diff --git a/apps/clockbg/img/ai_hero.jpeg b/apps/clockbg/img/ai_hero.jpeg new file mode 100644 index 000000000..e0bad0895 Binary files /dev/null and b/apps/clockbg/img/ai_hero.jpeg differ diff --git a/apps/clockbg/img/ai_horse.jpeg b/apps/clockbg/img/ai_horse.jpeg new file mode 100644 index 000000000..3b7da61f1 Binary files /dev/null and b/apps/clockbg/img/ai_horse.jpeg differ diff --git a/apps/clockbg/img/ai_robot.jpeg b/apps/clockbg/img/ai_robot.jpeg new file mode 100644 index 000000000..a493a5c6a Binary files /dev/null and b/apps/clockbg/img/ai_robot.jpeg differ diff --git a/apps/clockbg/img/ai_rock.jpeg b/apps/clockbg/img/ai_rock.jpeg new file mode 100644 index 000000000..18cbe63b2 Binary files /dev/null and b/apps/clockbg/img/ai_rock.jpeg differ diff --git a/apps/clockbg/img/icons8-australia-480.png b/apps/clockbg/img/icons8-australia-480.png new file mode 100644 index 000000000..3bb1ac09b Binary files /dev/null and b/apps/clockbg/img/icons8-australia-480.png differ diff --git a/apps/clockbg/img/icons8-austria-480.png b/apps/clockbg/img/icons8-austria-480.png new file mode 100644 index 000000000..5431bbd7a Binary files /dev/null and b/apps/clockbg/img/icons8-austria-480.png differ diff --git a/apps/clockbg/img/icons8-belgium-480.png b/apps/clockbg/img/icons8-belgium-480.png new file mode 100644 index 000000000..f0c6485f9 Binary files /dev/null and b/apps/clockbg/img/icons8-belgium-480.png differ diff --git a/apps/clockbg/img/icons8-brazil-480.png b/apps/clockbg/img/icons8-brazil-480.png new file mode 100644 index 000000000..505af26ca Binary files /dev/null and b/apps/clockbg/img/icons8-brazil-480.png differ diff --git a/apps/clockbg/img/icons8-canada-480.png b/apps/clockbg/img/icons8-canada-480.png new file mode 100644 index 000000000..62181468f Binary files /dev/null and b/apps/clockbg/img/icons8-canada-480.png differ diff --git a/apps/clockbg/img/icons8-china-480.png b/apps/clockbg/img/icons8-china-480.png new file mode 100644 index 000000000..ba56ccc1e Binary files /dev/null and b/apps/clockbg/img/icons8-china-480.png differ diff --git a/apps/clockbg/img/icons8-denmark-480.png b/apps/clockbg/img/icons8-denmark-480.png new file mode 100644 index 000000000..716e15f98 Binary files /dev/null and b/apps/clockbg/img/icons8-denmark-480.png differ diff --git a/apps/clockbg/img/icons8-england-480.png b/apps/clockbg/img/icons8-england-480.png new file mode 100644 index 000000000..2343521f3 Binary files /dev/null and b/apps/clockbg/img/icons8-england-480.png differ diff --git a/apps/clockbg/img/icons8-flag-of-europe-480.png b/apps/clockbg/img/icons8-flag-of-europe-480.png new file mode 100644 index 000000000..616d5fe7a Binary files /dev/null and b/apps/clockbg/img/icons8-flag-of-europe-480.png differ diff --git a/apps/clockbg/img/icons8-france-480.png b/apps/clockbg/img/icons8-france-480.png new file mode 100644 index 000000000..09a24c8b1 Binary files /dev/null and b/apps/clockbg/img/icons8-france-480.png differ diff --git a/apps/clockbg/img/icons8-germany-480.png b/apps/clockbg/img/icons8-germany-480.png new file mode 100644 index 000000000..dd2b317bb Binary files /dev/null and b/apps/clockbg/img/icons8-germany-480.png differ diff --git a/apps/clockbg/img/icons8-great-britain-480.png b/apps/clockbg/img/icons8-great-britain-480.png new file mode 100644 index 000000000..f89add414 Binary files /dev/null and b/apps/clockbg/img/icons8-great-britain-480.png differ diff --git a/apps/clockbg/img/icons8-greece-480.png b/apps/clockbg/img/icons8-greece-480.png new file mode 100644 index 000000000..eb823b4bc Binary files /dev/null and b/apps/clockbg/img/icons8-greece-480.png differ diff --git a/apps/clockbg/img/icons8-hungary-480.png b/apps/clockbg/img/icons8-hungary-480.png new file mode 100644 index 000000000..4097e88d3 Binary files /dev/null and b/apps/clockbg/img/icons8-hungary-480.png differ diff --git a/apps/clockbg/img/icons8-italy-480.png b/apps/clockbg/img/icons8-italy-480.png new file mode 100644 index 000000000..82b1b710e Binary files /dev/null and b/apps/clockbg/img/icons8-italy-480.png differ diff --git a/apps/clockbg/img/icons8-lgbt-flag-480.png b/apps/clockbg/img/icons8-lgbt-flag-480.png new file mode 100644 index 000000000..2e8bbebb1 Binary files /dev/null and b/apps/clockbg/img/icons8-lgbt-flag-480.png differ diff --git a/apps/clockbg/img/icons8-netherlands-480.png b/apps/clockbg/img/icons8-netherlands-480.png new file mode 100644 index 000000000..4ea397e27 Binary files /dev/null and b/apps/clockbg/img/icons8-netherlands-480.png differ diff --git a/apps/clockbg/img/icons8-new-zealand-480.png b/apps/clockbg/img/icons8-new-zealand-480.png new file mode 100644 index 000000000..e21fdc574 Binary files /dev/null and b/apps/clockbg/img/icons8-new-zealand-480.png differ diff --git a/apps/clockbg/img/icons8-norway-480.png b/apps/clockbg/img/icons8-norway-480.png new file mode 100644 index 000000000..a57c0f7fb Binary files /dev/null and b/apps/clockbg/img/icons8-norway-480.png differ diff --git a/apps/clockbg/img/icons8-scotland-480.png b/apps/clockbg/img/icons8-scotland-480.png new file mode 100644 index 000000000..20f08cfbb Binary files /dev/null and b/apps/clockbg/img/icons8-scotland-480.png differ diff --git a/apps/clockbg/img/icons8-spain-480.png b/apps/clockbg/img/icons8-spain-480.png new file mode 100644 index 000000000..17fed3360 Binary files /dev/null and b/apps/clockbg/img/icons8-spain-480.png differ diff --git a/apps/clockbg/img/icons8-sweden-480.png b/apps/clockbg/img/icons8-sweden-480.png new file mode 100644 index 000000000..99299e93a Binary files /dev/null and b/apps/clockbg/img/icons8-sweden-480.png differ diff --git a/apps/clockbg/img/icons8-switzerland-480.png b/apps/clockbg/img/icons8-switzerland-480.png new file mode 100644 index 000000000..cde8c6ff0 Binary files /dev/null and b/apps/clockbg/img/icons8-switzerland-480.png differ diff --git a/apps/clockbg/img/icons8-ukraine-480.png b/apps/clockbg/img/icons8-ukraine-480.png new file mode 100644 index 000000000..718695b38 Binary files /dev/null and b/apps/clockbg/img/icons8-ukraine-480.png differ diff --git a/apps/clockbg/img/icons8-usa-480.png b/apps/clockbg/img/icons8-usa-480.png new file mode 100644 index 000000000..a7867f070 Binary files /dev/null and b/apps/clockbg/img/icons8-usa-480.png differ diff --git a/apps/clockbg/img/icons8-wales-480.png b/apps/clockbg/img/icons8-wales-480.png new file mode 100644 index 000000000..58233db34 Binary files /dev/null and b/apps/clockbg/img/icons8-wales-480.png differ diff --git a/apps/clockbg/interface.html b/apps/clockbg/interface.html new file mode 100644 index 000000000..c92600b1b --- /dev/null +++ b/apps/clockbg/interface.html @@ -0,0 +1,202 @@ + + + + + + +

Upload an image:

+
+

If you'd like to contribute images you can add them on GitHub!

+
Preview:
+
+
+ +
+ +
+ + +

Click

+ + + + + + + + \ No newline at end of file diff --git a/apps/clockbg/lib.js b/apps/clockbg/lib.js new file mode 100644 index 000000000..c9b1fb1d2 --- /dev/null +++ b/apps/clockbg/lib.js @@ -0,0 +1,37 @@ +let settings = Object.assign({ + style : "randomcolor", + colors : ["#F00","#0F0","#00F"] +},require("Storage").readJSON("clockbg.json")||{}); +if (settings.style=="image") + settings.img = require("Storage").read(settings.fn); +else if (settings.style=="randomcolor") { + settings.style = "color"; + let n = (0|(Math.random()*settings.colors.length)) % settings.colors.length; + settings.color = settings.colors[n]; + delete settings.colors; +} else if (settings.style=="squares") { + settings.style = "image"; + let bpp = (settings.colors.length>4)?4:2; + let bg = Graphics.createArrayBuffer(11,11,bpp,{msb:true}); + E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256); // random pixels + bg.palette = new Uint16Array(1<g.toColor(c))); + settings.img = bg.asImage("string"); + settings.imgOpt = {scale:16}; + delete settings.colors; +} + +// Fill a rectangle with the current background style, rect = {x,y,w,h} +// eg require("clockbg").fillRect({x:10,y:10,w:50,h:50}) +// require("clockbg").fillRect(Bangle.appRect) +exports.fillRect = function(rect,y,x2,y2) { + if ("object"!=typeof rect) rect = {x:rect,y:y,w:1+x2-rect,h:1+y2-y}; + if (settings.img) { + g.setClipRect(rect.x, rect.y, rect.x+rect.w-1, rect.y+rect.h-1).drawImage(settings.img,0,0,settings.imgOpt).setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + } else if (settings.style == "color") { + g.setBgColor(settings.color).clearRect(rect); + } else { + console.log("clockbg: No background set!"); + g.setBgColor(g.theme.bg).clearRect(rect); + } +}; \ No newline at end of file diff --git a/apps/clockbg/metadata.json b/apps/clockbg/metadata.json new file mode 100644 index 000000000..2221e99bd --- /dev/null +++ b/apps/clockbg/metadata.json @@ -0,0 +1,21 @@ +{ "id": "clockbg", + "name": "Clock Backgrounds", + "shortName":"Backgrounds", + "version": "0.03", + "description": "Library that allows clocks to include a custom background (generated on demand or uploaded).", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}], + "type": "module", + "readme": "README.md", + "provides_modules" : ["clockbg"], + "tags": "module,background", + "supports" : ["BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name":"clockbg","url":"lib.js"}, + {"name":"clockbg.settings.js","url":"settings.js"} + ], "data": [ + {"wildcard":"clockbg.bg*.img"}, + {"name":"clockbg.json"} + ] +} diff --git a/apps/clockbg/screenshot.png b/apps/clockbg/screenshot.png new file mode 100644 index 000000000..f9d395e74 Binary files /dev/null and b/apps/clockbg/screenshot.png differ diff --git a/apps/clockbg/screenshot2.png b/apps/clockbg/screenshot2.png new file mode 100644 index 000000000..819e8ca87 Binary files /dev/null and b/apps/clockbg/screenshot2.png differ diff --git a/apps/clockbg/settings.js b/apps/clockbg/settings.js new file mode 100644 index 000000000..c39017262 --- /dev/null +++ b/apps/clockbg/settings.js @@ -0,0 +1,122 @@ +(function(back) { +let settings = Object.assign({ + style : "randomcolor", + colors : ["#F00","#0F0","#00F"] +},require("Storage").readJSON("clockbg.json")||{}); + +function saveSettings() { + if (settings.style!="image") + delete settings.fn; + if (settings.style!="color") + delete settings.color; + if (settings.style!="randomcolor" && settings.style!="squares") + delete settings.colors; + require("Storage").writeJSON("clockbg.json", settings); +} + +function getColorsImage(cols) { + var bpp = 1; + if (cols.length>4) bpp=4; + else if (cols.length>2) bpp=2; + var w = (cols.length>8)?8:16; + var b = Graphics.createArrayBuffer(w*cols.length,16,bpp); + b.palette = new Uint16Array(1<{ + b.setColor(i).fillRect(i*w,0,i*w+w-1,15); + b.palette[i] = g.toColor(c); + }); + return "\0"+b.asImage("string"); +} + +function showModeMenu() { + E.showMenu({ + "" : {title:/*LANG*/"Background", back:showMainMenu}, + /*LANG*/"Solid Color" : function() { + var cols = ["#F00","#0F0","#FF0", + "#00F","#F0F","#0FF", + "#000","#888","#fff",]; + var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}}; + cols.forEach(col => { + menu["-"+getColorsImage([col])] = () => { + settings.style = "color"; + settings.color = col; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + }, + /*LANG*/"Random Color" : function() { + var cols = [ + ["#F00","#0F0","#FF0","#00F","#F0F","#0FF"], + ["#F00","#0F0","#00F"], + // Please add some more! + ]; + var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}}; + cols.forEach(col => { + menu[getColorsImage(col)] = () => { + settings.style = "randomcolor"; + settings.colors = col; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + }, + /*LANG*/"Image" : function() { + let images = require("Storage").list(/clockbg\..*\.img/); + if (images.length) { + var menu = {"":{title:/*LANG*/"Images", back:showModeMenu}}; + images.forEach(im => { + menu[im.slice(8,-4)] = () => { + settings.style = "image"; + settings.fn = im; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + } else { + E.showAlert("Please use App Loader to upload images").then(showModeMenu); + } + }, + /*LANG*/"Squares" : function() { + /* + a = new Array(16); + a.fill(0); + print(a.map((n,i)=>E.HSBtoRGB(0 + i/16,1,1,24).toString(16).padStart(6,0).replace(/(.).(.).(.)./,"\"#$1$2$3\"")).join(",")) + */ + var cols = [ // list of color palettes used as possible square colours - either 4 or 16 entries + ["#00f","#05f","#0bf","#0fd","#0f7","#0f1","#3f0","#9f0","#ff0","#f90","#f30","#f01","#f07","#f0d","#b0f","#50f"], + ["#0FF","#0CC","#088","#044"], + ["#FFF","#FBB","#F66","#F44"], + ["#FFF","#BBB","#666","#000"] + // Please add some more! + ]; + var menu = {"":{title:/*LANG*/"Squares", back:showModeMenu}}; + cols.forEach(col => { + menu[getColorsImage(col)] = () => { + settings.style = "squares"; + settings.colors = col; + console.log(settings); + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + } + }); +} + +function showMainMenu() { + E.showMenu({ + "" : {title:/*LANG*/"Clock Background", back:back}, + /*LANG*/"Mode" : { + value : settings.style, + onchange : showModeMenu + } + }); +} + +showMainMenu(); +}) \ No newline at end of file diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 5657bf26d..6780313ce 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -5,4 +5,6 @@ 0.05: Improved colors (connected vs disconnected) 0.06: Tell clock widgets to hide. 0.07: Convert Yes/No On/Off in settings to checkboxes -0.08: Fixed typo in settings.js for DRAGDOWN to make option work \ No newline at end of file +0.08: Fixed typo in settings.js for DRAGDOWN to make option work +0.09: You can now back out of the calendar using the button +0.10: Fix linter warnings diff --git a/apps/clockcal/README.md b/apps/clockcal/README.md index d30205be0..bc05081ad 100644 --- a/apps/clockcal/README.md +++ b/apps/clockcal/README.md @@ -7,23 +7,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu |:--:|:-| |![locked screen](screenshot.png)|locked: triggers only one minimal update/min| |![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds| -|![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)| +|![big calendar](screenshot3.png)|swipe up for big calendar
⬆️/⬇️ to scroll
⬅️/➡️ to exit| ## Configurable Features - Number of calendar rows (weeks) -- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included) +- Buzz on connect/disconnect (feel free to disable and use a widget) - Clock Mode (24h/12h). (No am/pm indicator) - First day of the week - Red Saturday/Sunday - Swipe/Drag gestures to launch features or apps. -## Auto detects your message/music apps: -- swiping down will search your files for an app with the string "message" in its filename and launch it. (configurable) -- swiping right will search your files for an app with the string "music" in its filename and launch it. (configurable) +## Integrated swipe launcher: (Configure in Settings) +- ⬇️ (down) will search your files for an app with the string "**message**" +- ➡️ (right) will search your files for an app with the string "**music**" +- ⬅️ (left) will search your files for an app with the string "**agenda**" +- ⬆️ (up) will show the **internal full calendar** ## Feedback -The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings. -So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues +If something isn't working, please tell me: https://github.com/Stuff-etc/BangleApps/issues (I moved my github repo) ## Planned features: - Internal lightweight music control, because switching apps has a loading time. diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js index 58ddd7ef5..185f2adea 100644 --- a/apps/clockcal/app.js +++ b/apps/clockcal/app.js @@ -24,15 +24,25 @@ const DEBUG = false; var state = "watch"; var monthOffset = 0; +// FIXME: These variables should maybe be defined inside relevant functions below. The linter complained they were not defined (i.e. they were added to global scope if I understand correctly). +let dayInterval; +let secondInterval; +let minuteInterval; +let newmonth; +let bottomrightY; +let bottomrightX; +let rMonth; +let dimSeconds; + /* * Calendar features */ function drawFullCalendar(monthOffset) { - addMonths = function (_d, _am) { - var ay = 0, m = _d.getMonth(), y = _d.getFullYear(); + const addMonths = function (_d, _am) { + let ay = 0, m = _d.getMonth(), y = _d.getFullYear(); while ((m + _am) > 11) { ay++; _am -= 12; } while ((m + _am) < 0) { ay--; _am += 12; } - n = new Date(_d.getTime()); + let n = new Date(_d.getTime()); n.setMonth(m + _am); n.setFullYear(y + ay); return n; @@ -45,10 +55,10 @@ function drawFullCalendar(monthOffset) { if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); - d = addMonths(Date(), monthOffset); - tdy = Date().getDate() + "." + Date().getMonth(); + var d = addMonths(Date(), monthOffset); + let tdy = Date().getDate() + "." + Date().getMonth(); newmonth = false; - c_y = 0; + let c_y = 0; g.reset(); g.setBgColor(0); g.clear(); @@ -60,8 +70,8 @@ function drawFullCalendar(monthOffset) { rD.setDate(rD.getDate() - dow); var rDate = rD.getDate(); bottomrightY = c_y - 3; - clrsun = s.REDSUN ? '#f00' : '#fff'; - clrsat = s.REDSUN ? '#f00' : '#fff'; + let clrsun = s.REDSUN ? '#f00' : '#fff'; + let clrsat = s.REDSUN ? '#f00' : '#fff'; var fg = [clrsun, '#fff', '#fff', '#fff', '#fff', '#fff', clrsat]; for (var y = 1; y <= 11; y++) { bottomrightY += CELL_H; @@ -90,7 +100,7 @@ function caldrawMonth(rDate, c, m, rD) { g.setColor(c); g.setFont("Vector", 18); g.setFontAlign(-1, 1, 1); - drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : ""; + let drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : ""; g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1); newmonth = false; } @@ -124,7 +134,7 @@ function drawMinutes() { var d = new Date(); var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' '); var minutes = d.getMinutes().toString().padStart(2, '0'); - var textColor = NRF.getSecurityStatus().connected ? '#99f' : '#fff'; + var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00'; var size = 50; var clock_x = (w - 20) / 2; if (dimSeconds) { @@ -156,7 +166,7 @@ function drawSeconds() { } function drawWatch() { - if (DEBUG) console.log("CALENDAR"); + if (DEBUG) console.log("DRAWWATCH"); monthOffset = 0; state = "watch"; var d = new Date(); @@ -197,6 +207,7 @@ function drawWatch() { if (DEBUG) console.log("Next Day:" + (nextday / 3600)); if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); dayInterval = setTimeout(drawWatch, nextday * 1000); + if (DEBUG) console.log("ended DRAWWATCH. next refresh in " + nextday + "s"); } function BTevent() { @@ -211,8 +222,12 @@ function action(a) { g.reset(); if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); if (DEBUG) console.log("action:" + a); + state = "unknown"; + console.log("state -> unknown"); + let l; switch (a) { case "[ignore]": + drawWatch(); break; case "[calend.]": drawFullCalendar(); @@ -229,6 +244,12 @@ function action(a) { load(l[0]); } else E.showAlert("Message app not found", "Not found").then(drawWatch); break; + case "[AI:agenda]": + l = require("Storage").list(RegExp("agenda.*app.js")); + if (l.length > 0) { + load(l[0]); + } else E.showAlert("Agenda app not found", "Not found").then(drawWatch); + break; default: l = require("Storage").list(RegExp(a + ".app.js")); if (l.length > 0) { @@ -276,7 +297,6 @@ function input(dir) { drawWatch(); } break; - } } @@ -309,3 +329,10 @@ NRF.on('disconnect', BTevent); dimSeconds = Bangle.isLocked(); drawWatch(); +setWatch(function() { + if (state == "watch") { + Bangle.showLauncher() + } else if (state == "calendar") { + drawWatch(); + } +}, BTN1, {repeat:true, edge:"falling"}); diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index 998115827..b84b08575 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.08", + "version": "0.10", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index a406f3cf7..ea613f5c0 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -1,6 +1,6 @@ (function (back) { var FILE = "clockcal.json"; - defaults={ + const defaults={ CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually MODE24: true, //24h mode vs 12h mode @@ -9,19 +9,19 @@ REDSAT: true, // Use red color for saturday? DRAGDOWN: "[AI:messg]", DRAGRIGHT: "[AI:music]", - DRAGLEFT: "[ignore]", + DRAGLEFT: "[AI:agenda]", DRAGUP: "[calend.]" }; - settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); + let settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); - actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"]; + let actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]","[AI:agenda]"]; require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js",""))); function writeSettings() { require('Storage').writeJSON(FILE, settings); } - menu = { + const menu = { "": { "title": "Clock & Calendar" }, "< Back": () => back(), 'Buzz(dis)conn.?': { diff --git a/apps/clockswitch/README.md b/apps/clockswitch/README.md new file mode 100644 index 000000000..74fa82b2f --- /dev/null +++ b/apps/clockswitch/README.md @@ -0,0 +1,14 @@ +# Clock Switcher + +This switches the default clock. +The idea is that you can use this app in combination with e.g. the +[Pattern Launcher](?q=ptlaunch) as a quick toggle, instead of navigating through +the settings menu. + +## Usage + +Load the app to switch to your next installed clock. + +## Creator + +Richard de Boer (rigrig) diff --git a/apps/clockswitch/app.js b/apps/clockswitch/app.js new file mode 100644 index 000000000..db738eb56 --- /dev/null +++ b/apps/clockswitch/app.js @@ -0,0 +1,23 @@ +const storage = require('Storage'); +const clocks = storage.list(/\.info$/) + .map(app => { + const a=storage.readJSON(app, 1); + return (a && a.type == "clock") ? a : undefined; + }) + .filter(app => app) // filter out any undefined apps + .sort((a, b) => a.sortorder - b.sortorder) + .map(app => app.src); +if (clocks.length<1) { + E.showAlert(/*LANG*/"No clocks found!", "Clock Switcher") + .then(load); +} else if (clocks.length<2) { + E.showAlert(/*LANG*/"Nothing to do:\nOnly one clock installed!", "Clock Switcher") + .then(load); +} else { + let settings = storage.readJSON('setting.json',true)||{clock:null}; + const old = clocks.indexOf(settings.clock), + next = (old+1)%clocks.length; + settings.clock = clocks[next]; + storage.writeJSON('setting.json', settings); + setTimeout(load, 100); // storage.writeJSON needs some time to complete +} diff --git a/apps/clockswitch/icon.js b/apps/clockswitch/icon.js new file mode 100644 index 000000000..8a85e4da5 --- /dev/null +++ b/apps/clockswitch/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AClhCyoAYsIwuF4IwtF4Qxqw2GF4mG1YsqAAeF1eyAAIteFhAvHGLeGwouLR4IuEGDJcGwooBAweH6/X6wwGGKtbKownB640C1gGCAAQwZLgotDF4WG6wuFMZAuVw2yEgqLCABIuD1eGF6eGExYwLw4bCF1BuCDgWFdaGFRgwAJlb0HJogvPdQoAKq0AlYJG1YwDRr+sgEAL4wABwxgNF4ZeSqwLIMAYvNwpebAAOFSBgMCw7sQLxSQORwZLKLw4OLSBlbBgWyLznX2RfPLqBeM6/WcQYvZldbrYvN64jDF7rRNF7qPDGBqPLd6YxDGBTvQPpowQ1YvLGAeHF54wDlYMIwwvPwovQGAIuJ6+FdxSQF1YwRABKONF4mGF7aONAANbMDpeDRxRgFsOyFy+yP4gvLMAiRX6yNDwouMGDYuELxyRGwySS2QuUMAr0SdQguSGA+G1gtMLgguUGAQxFwuH1aWE2QsBwoQEFyzEHAB+FFzAwCMQoALFrRiRwwtefI5mCQwIslAH4A/AFw")) diff --git a/apps/clockswitch/icon.png b/apps/clockswitch/icon.png new file mode 100644 index 000000000..ac80cd84d Binary files /dev/null and b/apps/clockswitch/icon.png differ diff --git a/apps/clockswitch/metadata.json b/apps/clockswitch/metadata.json new file mode 100644 index 000000000..f13c4829e --- /dev/null +++ b/apps/clockswitch/metadata.json @@ -0,0 +1,14 @@ +{ "id": "clockswitch", + "name": "Clock Switcher", + "shortName":"Switch Clock", + "version":"0.01", + "description": "Switch to the next installed clock", + "icon": "icon.png", + "tags": "tool", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"clockswitch.app.js","url":"app.js"}, + {"name":"clockswitch.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/color_catalog/ChangeLog b/apps/color_catalog/ChangeLog index b79d0c85b..d1d409730 100644 --- a/apps/color_catalog/ChangeLog +++ b/apps/color_catalog/ChangeLog @@ -1 +1,3 @@ 0.01: 1st ver,RGB565 and RGB888 colors in a common UI/UX +0.02: Minor code improvements +0.03: Minor code improvements diff --git a/apps/color_catalog/app.js b/apps/color_catalog/app.js index 58951d1c6..b2f39c7a7 100644 --- a/apps/color_catalog/app.js +++ b/apps/color_catalog/app.js @@ -11,7 +11,7 @@ var v_model=process.env.BOARD; console.log("device="+v_model); var x_max_screen=g.getWidth();//240; - var y_max_screen=g.getHeight(); //240; + //var y_max_screen=g.getHeight(); //240; var y_wg_bottom=g.getHeight()-25; var y_wg_top=25; if (v_model=='BANGLEJS') { @@ -20,7 +20,7 @@ var v_model=process.env.BOARD; var y_btn2=124; //harcoded for bangle.js cuz it is not the half of } else x_max_usable_area=240; - var contador=1; + //var contador=1; var cont_items=0; var cont_row=0; var v_boxes_row=4; @@ -31,26 +31,26 @@ var v_model=process.env.BOARD; var v_font1size=11; var v_fontsize=13; var v_color_b_area='#111111';//black - var v_color_b_area2=0x5AEB;//Dark + //var v_color_b_area2=0x5AEB;//Dark var v_color_text='#FB0E01'; var v_color_statictxt='#e56e06'; //orange RGB format rrggbb //RGB565 requires only 16 (5+6+5) bits/2 bytes - var a_colors_str= Array('White RGB565 0x','Orange','DarkGreen','Yellow', + var a_colors_str= ['White RGB565 0x','Orange','DarkGreen','Yellow', 'Maroon','Blue','green','Purple', 'cyan','olive','DarkCyan','DarkGrey', 'Navy','Red','Magenta','GreenYellow', 'Blush RGB888','pure red','Orange','Grey green', 'D. grey','Almond','Amber','Bone', 'Canary','Aero blue','Camel','Baby pink', - 'Y.Corn','Cultured','Eigengrau','Citrine'); - var a_colors= Array(0xFFFF,0xFD20,0x03E0,0xFFE0, + 'Y.Corn','Cultured','Eigengrau','Citrine']; + var a_colors= [0xFFFF,0xFD20,0x03E0,0xFFE0, 0x7800,0x001F,0x07E0,0x780F, 0x07FF,0x7BE0,0x03EF,0x7BEF, 0x000F,0xF800,0xF81F,0xAFE5, '#DE5D83','#FB0E01','#E56E06','#7E795C', '#404040','#EFDECD','#FFBF00','#E3DAC9', '#FFFF99','#C0E8D5','#C19A6B','#F4C2C2', - '#FBEC5D','#F5F5F5','#16161D','#E4D00A'); + '#FBEC5D','#F5F5F5','#16161D','#E4D00A']; var v_color_lines=0xFFFF; //White hex format diff --git a/apps/color_catalog/metadata.json b/apps/color_catalog/metadata.json index 3146a146f..4d49308ef 100644 --- a/apps/color_catalog/metadata.json +++ b/apps/color_catalog/metadata.json @@ -2,7 +2,7 @@ "id": "color_catalog", "name": "Colors Catalog", "shortName": "Colors Catalog", - "version": "0.01", + "version": "0.03", "description": "Displays RGB565 and RGB888 colors, its name and code in screen.", "icon": "app.png", "tags": "Color,input,buttons,touch,UI", diff --git a/apps/colorful_clock/ChangeLog b/apps/colorful_clock/ChangeLog index 54ee389e3..e38a7c5a5 100644 --- a/apps/colorful_clock/ChangeLog +++ b/apps/colorful_clock/ChangeLog @@ -1,3 +1,4 @@ ... 0.03: First update with ChangeLog Added 0.04: Tell clock widgets to hide. +0.05: Minor code improvements diff --git a/apps/colorful_clock/app.js b/apps/colorful_clock/app.js index ba6272e9b..b58892311 100644 --- a/apps/colorful_clock/app.js +++ b/apps/colorful_clock/app.js @@ -120,7 +120,6 @@ let twoPi = 2*Math.PI; let Pi = Math.PI; - let halfPi = Math.PI/2; let sin = Math.sin, cos = Math.cos; diff --git a/apps/colorful_clock/metadata.json b/apps/colorful_clock/metadata.json index 237acf81c..9e77e12c5 100644 --- a/apps/colorful_clock/metadata.json +++ b/apps/colorful_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "colorful_clock", "name": "Colorful Analog Clock", "shortName":"Colorful Clock", - "version":"0.04", + "version": "0.05", "description": "a colorful analog clock", "icon": "app-icon.png", "type": "clock", diff --git a/apps/colorwheel/app.js b/apps/colorwheel/app.js index 7874c3f54..e8367d329 100644 --- a/apps/colorwheel/app.js +++ b/apps/colorwheel/app.js @@ -64,13 +64,14 @@ switch (true) { case (Radius > outerRadius): Color = '#000000'; break; case (Radius < innerRadius): Color = '#FFFFFF'; break; - default: + default: { let Phi = Math.atan2(dy,dx) + halfPi; if (Phi < 0) { Phi += twoPi; } if (Phi > twoPi) { Phi -= twoPi; } let Index = Math.floor(12*Phi/twoPi); Color = ColorList[Index]; + } } g.setColor(1,1,1); g.fillCircle(CenterX,CenterY, innerRadius); diff --git a/apps/coloursdemo/ChangeLog b/apps/coloursdemo/ChangeLog new file mode 100644 index 000000000..d44ed23f0 --- /dev/null +++ b/apps/coloursdemo/ChangeLog @@ -0,0 +1 @@ +1.00: first release diff --git a/apps/coloursdemo/README.md b/apps/coloursdemo/README.md new file mode 100644 index 000000000..b0fdc6f6b --- /dev/null +++ b/apps/coloursdemo/README.md @@ -0,0 +1,22 @@ +# Colours Demo + +This is a simple app to demonstrate colours on a Bangle 2. + +The colours are "optimised" for the Bangle 2's 3-bit display. They only include values which use either the full, half or no primary RGB colour, which should reduce the artifacts due to dithering (the exception are light and dark grey). + +![](screenshot.png) + +Use this app for choosing colours for your own project, and copy the colour definitions from the source code. + + +## Use colours in other projects + +Copy-and-paste the colour constants to be used in your own app from `coloursdemo.app.js`. They are sandwiched between the "BEGIN" and "END" comments at the beginning of the file. + +With the constants available in your own code, you can for example set the foreground colour to yellow with: + + g.setColor(COLOUR_YELLOW); + +This works for any graphics call requiring a colour value (like `g.setBgColor()`). + + diff --git a/apps/coloursdemo/coloursdemo-icon.js b/apps/coloursdemo/coloursdemo-icon.js new file mode 100644 index 000000000..8c58b0b19 --- /dev/null +++ b/apps/coloursdemo/coloursdemo-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AC8sAAYqpmVdr2Irwvklkzq4qBx4ADxAvDM0EyxAABFwgABF4k5rsyGTksF5MzBwdjAAVdnIzCF69dF5FdEYUyF4YADGQSPVF5LwCRwIvHAAIvVllXF5DwCRwgAFNobwbxFeEISOIAAMzF6zwCsgqBDoMsmUzWQMzF5MyeC4lBEwM5nNAsgABGgMyX5JeWF4IsBFYYADnIvBHgJmBrouDBYIvZnIvHLwIABnBvCMwSOXeAQvImU4F4QADMwReXF5csFwwxDF7IlCYAqOEF44uYF5MzF5ReZR4LwBF4qOKnAvalgvBYAk6RxYvaeAs6EYK+lMAZOBlgtBAQS+jF4QoBSQQjBGRKOcF4YjCMgM4AAIyCBoaOcF4YwCAYIvCGQxeceAQvDGoIvFGQYveSAguJF8iOHAAYueF4iOqeAksRyz8CAAzwNR1RgDMQZeIADJ0JqwmCGQoFB0gAEq2A5wAG0ky54AFrowGFQVXAAIyGmVWF8VWF4QyGlmAF8QsDLYIyFFwovbGAIuDSoqOHF8CJCF4aOHF7q/CqyVEAoIuGF7hgEAAiOIF7xhDYgiOHF7oxDXwLyCRxAvfGAYAhF5QA/AH4AEA")) diff --git a/apps/coloursdemo/coloursdemo.app.js b/apps/coloursdemo/coloursdemo.app.js new file mode 100644 index 000000000..85066bd31 --- /dev/null +++ b/apps/coloursdemo/coloursdemo.app.js @@ -0,0 +1,128 @@ +/* + * Demonstrate colours + */ + + +// BEGIN colour constants +const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0) +const COLOUR_DARK_GREY = 0x4208; // same as: g.setColor(0.25, 0.25, 0.25) +const COLOUR_GREY = 0x8410; // same as: g.setColor(0.5, 0.5, 0.5) +const COLOUR_LIGHT_GREY = 0xc618; // same as: g.setColor(0.75, 0.75, 0.75) +const COLOUR_WHITE = 0xffff; // same as: g.setColor(1, 1, 1) + +const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0) +const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 0) +const COLOUR_BLUE = 0x001f; // same as: g.setColor(0, 0, 1) +const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +const COLOUR_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1) +const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1) + +const COLOUR_LIGHT_RED = 0xfc10; // same as: g.setColor(1, 0.5, 0.5) +const COLOUR_LIGHT_GREEN = 0x87f0; // same as: g.setColor(0.5, 1, 0.5) +const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1) +const COLOUR_LIGHT_YELLOW = 0xfff0; // same as: g.setColor(1, 1, 0.5) +const COLOUR_LIGHT_MAGENTA = 0xfc1f; // same as: g.setColor(1, 0.5, 1) +const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1) + +const COLOUR_DARK_RED = 0x8000; // same as: g.setColor(0.5, 0, 0) +const COLOUR_DARK_GREEN = 0x0400; // same as: g.setColor(0, 0.5, 0) +const COLOUR_DARK_BLUE = 0x0010; // same as: g.setColor(0, 0, 0.5) +const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0) +const COLOUR_DARK_MAGENTA = 0x8010; // same as: g.setColor(0.5, 0, 0.5) +const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5) + +const COLOUR_PINK = 0xf810; // same as: g.setColor(1, 0, 0.5) +const COLOUR_LIMEGREEN = 0x87e0; // same as: g.setColor(0.5, 1, 0) +const COLOUR_ROYALBLUE = 0x041f; // same as: g.setColor(0, 0.5, 1) +const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) +const COLOUR_INDIGO = 0x801f; // same as: g.setColor(0.5, 0, 1) +const COLOUR_TURQUOISE = 0x07f0; // same as: g.setColor(0, 1, 0.5) +// END colour constants + + +// array of colours to be demoed: +// [ colour value, label colour, label ] +const demo = [ + [ COLOUR_LIGHT_RED, COLOUR_BLACK, 'LIGHT RED' ], + [ COLOUR_RED, COLOUR_WHITE, 'RED' ], + [ COLOUR_DARK_RED, COLOUR_WHITE, 'DARK RED' ], + + [ COLOUR_LIGHT_YELLOW, COLOUR_BLACK, 'LIGHT YELLOW' ], + [ COLOUR_YELLOW, COLOUR_BLACK, 'YELLOW' ], + [ COLOUR_DARK_YELLOW, COLOUR_WHITE, 'DARK YELLOW' ], + + [ COLOUR_LIGHT_GREEN, COLOUR_BLACK, 'LIGHT GREEN' ], + [ COLOUR_GREEN, COLOUR_BLACK, 'GREEN' ], + [ COLOUR_DARK_GREEN, COLOUR_WHITE, 'DARK GREEN' ], + + [ COLOUR_LIGHT_CYAN, COLOUR_BLACK, 'LIGHT CYAN' ], + [ COLOUR_CYAN, COLOUR_BLACK, 'CYAN' ], + [ COLOUR_DARK_CYAN, COLOUR_WHITE, 'DARK CYAN' ], + + [ COLOUR_LIGHT_BLUE, COLOUR_BLACK, 'LIGHT BLUE' ], + [ COLOUR_BLUE, COLOUR_WHITE, 'BLUE' ], + [ COLOUR_DARK_BLUE, COLOUR_WHITE, 'DARK BLUE' ], + + [ COLOUR_LIGHT_MAGENTA, COLOUR_BLACK, 'LIGHT MAGENTA' ], + [ COLOUR_MAGENTA, COLOUR_WHITE, 'MAGENTA' ], + [ COLOUR_DARK_MAGENTA, COLOUR_WHITE, 'DARK MAGENTA' ], + + [ COLOUR_LIMEGREEN, COLOUR_BLACK, 'LIMEGREEN' ], + [ COLOUR_TURQUOISE, COLOUR_BLACK, 'TURQUOISE' ], + [ COLOUR_ROYALBLUE, COLOUR_WHITE, 'ROYALBLUE' ], + + [ COLOUR_ORANGE, COLOUR_BLACK, 'ORANGE' ], + [ COLOUR_PINK, COLOUR_WHITE, 'PINK' ], + [ COLOUR_INDIGO, COLOUR_WHITE, 'INDIGO' ], + + [ COLOUR_LIGHT_GREY, COLOUR_BLACK, 'LIGHT GREY' ], + [ COLOUR_GREY, COLOUR_BLACK, 'GREY' ], + [ COLOUR_DARK_GREY, COLOUR_WHITE, 'DARK GREY' ], + + [ COLOUR_WHITE, COLOUR_BLACK, 'WHITE' ], + [ COLOUR_BLACK, COLOUR_WHITE, 'BLACK' ], +]; + +const columns = 3; +const rows = 10; + + +// initialise +g.clear(true); +g.setFont('6x8').setFontAlign(-1, -1); + +// calc some values required to draw the grid +const colWidth = Math.floor(g.getWidth() / columns); +const rowHeight = Math.floor(g.getHeight() / rows); +const xStart = Math.floor((g.getWidth() - (columns * colWidth)) / 2); +var x = xStart; +var y = Math.floor((g.getHeight() - (rows * rowHeight)) / 2); + +// loop through the colours to be demoed +for (var idx in demo) { + var colour = demo[idx][0]; + var labelColour = demo[idx][1]; + var label = demo[idx][2]; + + // draw coloured box + g.setColor(colour).fillRect(x, y, x + colWidth - 1, y + rowHeight - 1); + + // label it + g.setColor(labelColour).drawString(g.wrapString(label, colWidth).join("\n"), x, y); + + x += colWidth; + if ((x + colWidth) >= g.getWidth()) { + x = xStart; + y += rowHeight; + } +} + +// there's an "unused" box left - cross it out +g.setColor(COLOUR_RED); +g.drawLine(x, y, x + colWidth - 1, y + rowHeight - 1); +g.drawLine(x, y + rowHeight - 1, x + colWidth - 1, y); + + +// exit on button press +setWatch(e => { Bangle.showClock(); }, BTN1); + diff --git a/apps/coloursdemo/coloursdemo.png b/apps/coloursdemo/coloursdemo.png new file mode 100644 index 000000000..305ccc9b8 Binary files /dev/null and b/apps/coloursdemo/coloursdemo.png differ diff --git a/apps/coloursdemo/metadata.json b/apps/coloursdemo/metadata.json new file mode 100644 index 000000000..8c34d7ea8 --- /dev/null +++ b/apps/coloursdemo/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "coloursdemo", + "name": "Colours Demo", + "shortName":"Colours Demo", + "version":"1.00", + "description": "Simple app to demonstrate colours", + "icon": "coloursdemo.png", + "screenshots": [{ "url": "screenshot.png" }], + "type": "app", + "tags": "tool", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name":"coloursdemo.app.js", "url":"coloursdemo.app.js" }, + { "name":"coloursdemo.img", "url":"coloursdemo-icon.js", "evaluate":true } + ] +} diff --git a/apps/coloursdemo/screenshot.png b/apps/coloursdemo/screenshot.png new file mode 100644 index 000000000..350011b3f Binary files /dev/null and b/apps/coloursdemo/screenshot.png differ diff --git a/apps/configurable_clock/ChangeLog b/apps/configurable_clock/ChangeLog index 9d55c1a91..59708756a 100644 --- a/apps/configurable_clock/ChangeLog +++ b/apps/configurable_clock/ChangeLog @@ -1,3 +1,4 @@ ... 0.02: First update with ChangeLog Added 0.03: Tell clock widgets to hide. +0.04: Minor code improvements diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js index 45c86c7e9..4192954ae 100644 --- a/apps/configurable_clock/app.js +++ b/apps/configurable_clock/app.js @@ -748,7 +748,6 @@ let twoPi = 2*Math.PI, deg2rad = Math.PI/180; let Pi = Math.PI; - let halfPi = Math.PI/2; let sin = Math.sin, cos = Math.cos; @@ -894,7 +893,7 @@ g.setFontAlign(-1,0); g.drawString('9', CenterX-outerRadius,CenterY); break; - case '1-12': + case '1-12': { let innerRadius = outerRadius * 0.9 - 10; let dark = g.theme.dark; @@ -942,6 +941,7 @@ g.drawString(i == 0 ? '12' : '' + i, x,y); } + } } let now = new Date(); diff --git a/apps/configurable_clock/metadata.json b/apps/configurable_clock/metadata.json index 687a5b212..246a5dc21 100644 --- a/apps/configurable_clock/metadata.json +++ b/apps/configurable_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "configurable_clock", "name": "Configurable Analog Clock", "shortName":"Configurable Clock", - "version":"0.03", + "version": "0.04", "description": "an analog clock with several kinds of faces, hands and colors to choose from", "icon": "app-icon.png", "type": "clock", diff --git a/apps/contacts/ChangeLog b/apps/contacts/ChangeLog new file mode 100644 index 000000000..a0be5eaf6 --- /dev/null +++ b/apps/contacts/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Minor code improvements +0.03: Minor code improvements +0.04: Allow calling contacts from the app, Refactoring diff --git a/apps/contacts/README.md b/apps/contacts/README.md new file mode 100644 index 000000000..1bfc99c8e --- /dev/null +++ b/apps/contacts/README.md @@ -0,0 +1,29 @@ +# Contacts + +This app provides a common way to set up the `contacts.json` file. + +## Contacts JSON file + +When the app is loaded from the app loader, a file named +`contacts.json` is loaded along with the javascript etc. The file +has the following contents: + +``` +[ + { + "name":"NONE" + }, + { + "name":"First Last", + "number":"123456789", + } +] +``` + +## Contacts Editor + +Clicking on the download icon of `Contents` in the app loader invokes +the contact editor. The editor downloads and displays the current +`contacts.json` file. Clicking the `Edit` button beside an entry +causes the entry to be deleted from the list and displayed in the edit +boxes. It can be restored - by clicking the `Add` button. \ No newline at end of file diff --git a/apps/contacts/app-icon.js b/apps/contacts/app-icon.js new file mode 100644 index 000000000..3012be8d8 --- /dev/null +++ b/apps/contacts/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBkmSpIC/AVsJCJ+AQaCZBCOeACKGQLKGQBA0ggARPJ4IRsYo0ggR9IoAIGiRiIpEECJsAiACBBYoRGpEAI4JBFI47CBLIRlDHYJrGYQIRCwQICL4MQOgx9GboUSeQ4RFwAFBiSGHCIo4CiVIWZyPICP4RaRIQROgARHdIwICoIIFkDpGBAKqHgGACI0AyVIggIDoEEMQ1ICINJCIj4CfwIREBwUgQYYOCfYoFDJQKDFCIopEO4RoDKAqJHRhAC/ATA=")) diff --git a/apps/contacts/app.png b/apps/contacts/app.png new file mode 100644 index 000000000..147dcc61a Binary files /dev/null and b/apps/contacts/app.png differ diff --git a/apps/contacts/contacts.app.js b/apps/contacts/contacts.app.js new file mode 100644 index 000000000..34e40927a --- /dev/null +++ b/apps/contacts/contacts.app.js @@ -0,0 +1,170 @@ +/* contacts.js */ + +var Layout = require("Layout"); + +var wp = require('Storage').readJSON("contacts.json", true) || []; + +function writeContacts() { + require('Storage').writeJSON("contacts.json", wp); +} + +function callNumber (number) { + Bluetooth.println(JSON.stringify({ + t:"intent", + target:"activity", + action:"android.intent.action.CALL", + flags:["FLAG_ACTIVITY_NEW_TASK"], + categories:["android.intent.category.DEFAULT"], + data: 'tel:' + number, + })) + +} + +function mainMenu() { + var menu = { + "< Back" : Bangle.load + }; + if (!wp.length) { + menu['No Contacts'] = () => {}; + } else { + for (const e of wp) { + const closureE = e; + menu[e.name] = () => showContact(closureE); + } + } + menu["Add"] = addContact; + menu["Remove"] = removeContact; + g.clear(); + E.showMenu(menu); +} + +function showContact(i) { + g.clear(); + (new Layout ({ + type:"v", + c: [ + {type:"txt", font:"10%", pad:1, fillx:1, filly:1, label: i["name"] + "\n" + i["number"]}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "Call", cb: l => callNumber(i['number'])}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "Back to list", cb: mainMenu} + ], + lazy:true + })).render(); +} + +function showNumpad() { + return new Promise((resolve, reject) => { + let number = '' + E.showMenu(); + function addDigit(digit) { + number += digit; + Bangle.buzz(20); + update(); + } + function removeDigit() { + number = number.slice(0, -1); + Bangle.buzz(20); + update(); + } + function update() { + g.reset(); + g.clearRect(0,0,g.getWidth(),23); + g.setFont("Vector:24").setFontAlign(1,0).drawString(number, g.getWidth(),12); + } + const ds="12%"; + const digitBtn = (digit) => ({type:"btn", font:ds, width:58, label:digit, cb:l=>{addDigit(digit);}}); + var numPad = new Layout ({ + type:"v", c: [{ + type:"v", c: [ + {type:"", height:24}, + {type:"h",filly:1, c: [digitBtn("1"), digitBtn("2"), digitBtn("3")]}, + {type:"h",filly:1, c: [digitBtn("4"), digitBtn("5"), digitBtn("6")]}, + {type:"h",filly:1, c: [digitBtn("7"), digitBtn("8"), digitBtn("9")]}, + {type:"h",filly:1, c: [ + {type:"btn", font:ds, width:58, label:"C", cb: removeDigit}, + digitBtn('0'), + {type:"btn", font:ds, width:58, id:"OK", label:"OK", cb: l => resolve(number)} + ]} + ]} + ], lazy:true}); + g.clear(); + numPad.render(); + update(); + }); +} + +function removeContact() { + var menu = { + "" : {title : "Select Contact"}, + "< Back" : mainMenu + }; + if (wp.length===0) Object.assign(menu, {"No Contacts":""}); + else { + wp.forEach((val, card) => { + const name = wp[card].name; + menu[name]=()=>{ + E.showMenu(); + var confirmRemove = new Layout ( + {type:"v", c: [ + {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:"Delete"}, + {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:name}, + {type:"h", c: [ + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{ + wp.splice(card, 1); + writeContacts(); + mainMenu(); + }}, + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}} + ]} + ], lazy:true}); + g.clear(); + confirmRemove.render(); + }; + }); + } + E.showMenu(menu); +} + + +function addNewContact(name) { + g.clear(); + showNumpad().then((number) => { + wp.push({name: name, number: number}); + writeContacts(); + mainMenu(); + }) + + + +} + +function tryAddContact(name) { + if (wp.filter((e) => e.name === name).length) { + E.showMenu(); + var alreadyExists = new Layout ( + {type:"v", c: [ + {type:"txt", font:Math.min(15,100/name.length)+"%", pad:1, fillx:1, filly:1, label:name}, + {type:"txt", font:"12%", pad:1, fillx:1, filly:1, label:"already exists."}, + {type:"h", c: [ + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "REPLACE", cb:l=>{ addNewContact(name); }}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "CANCEL", cb:l=>{mainMenu();}} + ]} + ], lazy:true}); + g.clear(); + alreadyExists.render(); + return; + } + addNewContact(name); +} + +function addContact() { + require("textinput").input({text:""}).then(name => { + if (name !== "") { + tryAddContact(name); + } else + mainMenu(); + }); +} + +g.reset(); +Bangle.setUI(); +mainMenu(); diff --git a/apps/contacts/contacts.json b/apps/contacts/contacts.json new file mode 100644 index 000000000..40afa27dd --- /dev/null +++ b/apps/contacts/contacts.json @@ -0,0 +1,6 @@ +[ + { + "name":"EU emergency", + "number":"112" + } +] diff --git a/apps/contacts/interface.html b/apps/contacts/interface.html new file mode 100644 index 000000000..4a533bff7 --- /dev/null +++ b/apps/contacts/interface.html @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + +

Contacts v.2

+
+
+ + + +
+
+
+ + + + + + + + + + +
NameNumber
+
+

Add a new contact

+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + + + + + + + diff --git a/apps/contacts/metadata.json b/apps/contacts/metadata.json new file mode 100644 index 000000000..50354f74f --- /dev/null +++ b/apps/contacts/metadata.json @@ -0,0 +1,19 @@ +{ "id": "contacts", + "name": "Contacts", + "version": "0.04", + "description": "Provides means of storing user contacts, viewing/editing them on device and from the App loader", + "icon": "app.png", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "interface": "interface.html", + "dependencies": {"textinput":"type"}, + "storage": [ + {"name":"contacts.app.js","url":"contacts.app.js"}, + {"name":"contacts.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"contacts.json","url":"contacts.json"} + ] +} diff --git a/apps/contourclock/ChangeLog b/apps/contourclock/ChangeLog index d13b16702..6078b4ff3 100644 --- a/apps/contourclock/ChangeLog +++ b/apps/contourclock/ChangeLog @@ -10,3 +10,5 @@ 0.28: More config options for cleaner look, enabled fast loading 0.29: Fixed a bug that would leave old font files in storage. 0.30: Added options to show widgets and date on twist and tap. New fonts. +0.31: Bugfix, no more freeze. +0.32: Minor code improvements diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js index 2e1c25469..18b44b19d 100644 --- a/apps/contourclock/app.js +++ b/apps/contourclock/app.js @@ -2,8 +2,8 @@ let drawTimeout; let extrasTimeout; let onLock; - let onTap; - let onTwist; + //let onTap; + //let onTwist; let settings = require('Storage').readJSON("contourclock.json", true) || {}; if (settings.fontIndex == undefined) { settings.fontIndex = 0; @@ -34,9 +34,12 @@ extrasTimeout = undefined; hideExtras(); }, 5000); + extrasShown = false; }; let drawExtras = function() { //draw date, day of the week and widgets let date = new Date(); + g.reset(); + g.clearRect(0, 138, g.getWidth() - 1, 176); g.setFont("Teletext10x18Ascii").setFontAlign(0, 1); if (settings.weekday) g.drawString(require("locale").dow(date).toUpperCase(), g.getWidth() / 2, g.getHeight() - 18); if (settings.date) g.drawString(require('locale').date(date, 1), g.getWidth() / 2, g.getHeight()); @@ -45,22 +48,22 @@ }; let hideExtras = function() { if (extrasTimeout) clearTimeout(extrasTimeout); - delete extrasTimeout; + extrasTimeout = undefined; //NEW + g.reset(); g.clearRect(0, 138, g.getWidth() - 1, 176); require("widget_utils").hide(); - extrasShown = false; + extrasShown = false; ///NEW }; let draw = function() { - let date = new Date(); - g.reset(); - if (extrasShown) drawExtras(); - else hideExtras(); - require('contourclock').drawClock(settings.fontIndex); - if (drawTimeout) clearTimeout(drawTimeout); + if (drawTimeout) clearTimeout(drawTimeout); //NEW drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); }, 60000 - (Date.now() % 60000)); + g.reset(); + if (extrasShown) drawExtras(); + else hideExtras(); + require('contourclock').drawClock(settings.fontIndex); }; if (settings.hideWhenLocked) { onLock = locked => { @@ -84,8 +87,8 @@ Bangle.removeListener('twist', showExtras); if (drawTimeout) clearTimeout(drawTimeout); if (extrasTimeout) clearTimeout(extrasTimeout); - delete drawTimeout; - delete extrasTimeout; + drawTimeout = undefined; + extrasTimeout = undefined; if (settings.hideWhenLocked) require("widget_utils").show(); g.reset(); g.clear(); @@ -94,7 +97,7 @@ g.clear(); if (settings.widgets) { Bangle.loadWidgets(); - Bangle.drawWidgets(); + setTimeout(Bangle.drawWidgets,0); //NEW } draw(); } diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index ca5ee114f..61c0f5643 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -1,7 +1,7 @@ { "id": "contourclock", "name": "Contour Clock", "shortName" : "Contour Clock", - "version":"0.30", + "version": "0.32", "icon": "app.png", "readme": "README.md", "description": "A Minimalist clockface with large Digits.", diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog index 7386bbc35..30c775a49 100644 --- a/apps/coretemp/ChangeLog +++ b/apps/coretemp/ChangeLog @@ -2,3 +2,4 @@ 0.02: Cleanup interface and add settings, widget, add skin temp reporting. 0.03: Move code for recording to this app 0.04: Use default Bangle formatter for booleans +0.05: Minor code improvements diff --git a/apps/coretemp/coretemp.js b/apps/coretemp/coretemp.js index 7cbbe3577..0337891e1 100644 --- a/apps/coretemp/coretemp.js +++ b/apps/coretemp/coretemp.js @@ -1,6 +1,6 @@ // Simply listen for core events and show data -var btm = g.getHeight() - 1; +//var btm = g.getHeight() - 1; var px = g.getWidth() / 2; // Dark or light logo diff --git a/apps/coretemp/metadata.json b/apps/coretemp/metadata.json index 87cb42722..2b7de0bf0 100644 --- a/apps/coretemp/metadata.json +++ b/apps/coretemp/metadata.json @@ -1,7 +1,7 @@ { "id": "coretemp", "name": "CoreTemp", - "version": "0.04", + "version": "0.05", "description": "Display CoreTemp device sensor data", "icon": "coretemp.png", "type": "app", diff --git a/apps/counter/ChangeLog b/apps/counter/ChangeLog index 8402b3467..950c892dc 100644 --- a/apps/counter/ChangeLog +++ b/apps/counter/ChangeLog @@ -2,3 +2,4 @@ 0.02: Added decrement and touch functions 0.03: Set color - ensures widgets don't end up coloring the counter's text 0.04: Adopted for BangleJS 2 +0.05: Support translations diff --git a/apps/counter/counter.js b/apps/counter/counter.js index 0054ada6d..29413f600 100644 --- a/apps/counter/counter.js +++ b/apps/counter/counter.js @@ -95,9 +95,9 @@ if (BANGLEJS2) { g.clear(1).setFont("6x8"); g.setBgColor(g.theme.bg).setColor(g.theme.fg); if (BANGLEJS2) { - g.drawString('Swipe up to increase\nSwipe down to decrease\nPress button to reset.', x, 100 + y); + g.drawString([/*LANG*/"Swipe up to increase", /*LANG*/"Swipe down to decrease", /*LANG*/"Press button to reset"].join("\n"), x, 100 + y); } else { - g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', x, 100 + y); + g.drawString([/*LANG*/"Tap right or BTN1 to increase", /*LANG*/"Tap left or BTN3 to decrease", /*LANG*/"Press BTN2 to reset"].join("\n"), x, 100 + y); } Bangle.loadWidgets(); diff --git a/apps/counter/metadata.json b/apps/counter/metadata.json index daba58d39..827caa9ec 100644 --- a/apps/counter/metadata.json +++ b/apps/counter/metadata.json @@ -1,7 +1,7 @@ { "id": "counter", "name": "Counter", - "version": "0.04", + "version": "0.05", "description": "Simple counter", "icon": "counter_icon.png", "tags": "tool", diff --git a/apps/counter2/ChangeLog b/apps/counter2/ChangeLog new file mode 100644 index 000000000..58eacf613 --- /dev/null +++ b/apps/counter2/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Added Settings & readme +0.03: Fix lint warnings +0.04: Fix lint warnings diff --git a/apps/counter2/README.md b/apps/counter2/README.md new file mode 100644 index 000000000..d57844aae --- /dev/null +++ b/apps/counter2/README.md @@ -0,0 +1,24 @@ +# Counter2 by Michael + +I needed an HP/XP-Tracker for a game, so i made one. +The counter state gets saved. Best to use this with pattern launcher or ClockCal + +- Colored Background Mode +- ![color bg](https://stuff-etc.github.io/BangleApps/apps/counter2/counter2-screenshot.png) +- Colored Text Mode +- ![color text](https://stuff-etc.github.io/BangleApps/apps/counter2/counter2dark-screenshot.png) + +## Howto + - Tap top side or swipe up to increase counter + - Tap bottom side or swipe down to decrease counter + - Hold (600ms) to reset to default value (configurable) + - Press button to exit + +## Configurable Features +- Default value Counter 1 +- Default value Counter 2 +- Buzz on interact +- Colored Text/Background + +## Feedback +If something isn't working, please tell me: https://github.com/Stuff-etc/BangleApps/issues diff --git a/apps/counter2/app-icon.js b/apps/counter2/app-icon.js new file mode 100644 index 000000000..fda8d1e21 --- /dev/null +++ b/apps/counter2/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcAyVJkgCFAwwCBAgd5CI+eCI2T/IRH/wR7n//AAPyCIdPBAX8CKpr/CLTpSCOipB8gRFXoPJCIknCJAIBOoYRCagLNCa4f8Q4gREI4tP8mT/41HCKJHFGoQRG+QKBLI4RHLIx9CCJ7zBGpxZCPoyhQYpIIBYor7kCP4R8YoX/WY69DAIM/BAT+BdIYICeYQRTGqKP/CNIA==")) \ No newline at end of file diff --git a/apps/counter2/app.js b/apps/counter2/app.js new file mode 100644 index 000000000..42b59cf5d --- /dev/null +++ b/apps/counter2/app.js @@ -0,0 +1,95 @@ +Bangle.loadWidgets(); + +var s = Object.assign({ + counter0:10, + counter1:20, + max0:15, + max1:25, + buzz: true, + colortext: true, +}, require('Storage').readJSON("counter2.json", true) || {}); + +const f1 = (s.colortext) ? "#f00" : "#fff"; +const f2 = (s.colortext) ? "#00f" : "#fff"; +const b1 = (s.colortext) ? g.theme.bg : "#f00"; +const b2 = (s.colortext) ? g.theme.bg : "#00f"; + +var drag; + +const screenwidth = g.getWidth(); +const screenheight = g.getHeight(); +const halfwidth = screenwidth / 2; +const halfheight = screenheight / 2; + +const counter = []; +counter[0] = s.counter0; +counter[1] = s.counter1; +const defaults = []; +defaults[0] = s.max0; +defaults[1] = s.max1; + +function saveSettings() { + s.counter0 = counter[0]; + s.counter1 = counter[1]; + s.max0 = defaults[0]; + s.max1 = defaults[1]; + require('Storage').writeJSON("counter2.json", s); +} + +let ignoreonce = false; +var dragtimeout; + +function updateScreen() { + g.setBgColor(b1); + g.clearRect(0, 0, halfwidth, screenheight); + g.setBgColor(b2); + g.clearRect(halfwidth, 0, screenwidth, screenheight); + g.setFont("Vector", 60).setFontAlign(0, 0); + g.setColor(f1); + g.drawString(Math.floor(counter[0]), halfwidth * 0.5, halfheight); + g.setColor(f2); + g.drawString(Math.floor(counter[1]), halfwidth * 1.5, halfheight); + saveSettings(); + if (s.buzz) Bangle.buzz(50,.5); + Bangle.drawWidgets(); +} + +Bangle.on("drag", e => { + const c = (e.x < halfwidth) ? 0 : 1; + if (!drag) { + if (ignoreonce) { + ignoreonce = false; + return; + } + drag = { x: e.x, y: e.y }; + dragtimeout = setTimeout(function () { resetcounter(c); }, 600); //if dragging for 500ms, reset counter + } + else if (drag && !e.b) { // released + let adjust = 0; + const dx = e.x - drag.x, dy = e.y - drag.y; + if (Math.abs(dy) > Math.abs(dx) + 30) { + adjust = (dy > 0) ? -1 : 1; + } else { + adjust = (e.y > halfwidth) ? -1 : 1; + } + counter[c] += adjust; + updateScreen(); + drag = undefined; + clearTimeout(dragtimeout); + } +}); + +function resetcounter(which) { + counter[which] = defaults[which]; + console.log("resetting counter ", which); + updateScreen(); + drag = undefined; + ignoreonce = true; +} + + +updateScreen(); + +setWatch(function() { + load(); +}, BTN1, {repeat:true, edge:"falling"}); diff --git a/apps/counter2/counter2-icon.png b/apps/counter2/counter2-icon.png new file mode 100644 index 000000000..c16e9c0c7 Binary files /dev/null and b/apps/counter2/counter2-icon.png differ diff --git a/apps/counter2/counter2-screenshot.png b/apps/counter2/counter2-screenshot.png new file mode 100644 index 000000000..0864acb64 Binary files /dev/null and b/apps/counter2/counter2-screenshot.png differ diff --git a/apps/counter2/counter2dark-screenshot.png b/apps/counter2/counter2dark-screenshot.png new file mode 100644 index 000000000..2f0fd07c1 Binary files /dev/null and b/apps/counter2/counter2dark-screenshot.png differ diff --git a/apps/counter2/metadata.json b/apps/counter2/metadata.json new file mode 100644 index 000000000..400abf267 --- /dev/null +++ b/apps/counter2/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "counter2", + "name": "Counter2", + "version": "0.04", + "description": "Dual Counter", + "readme":"README.md", + "icon": "counter2-icon.png", + "tags": "tool", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"counter2-screenshot.png"},{"url":"counter2dark-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"counter2.app.js","url":"app.js"}, + {"name":"counter2.settings.js","url":"settings.js"}, + {"name":"counter2.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"counter2.json"}] +} diff --git a/apps/counter2/settings.js b/apps/counter2/settings.js new file mode 100644 index 000000000..b38df1824 --- /dev/null +++ b/apps/counter2/settings.js @@ -0,0 +1,55 @@ +(function (back) { + var FILE = "counter2.json"; + const defaults={ + counter0:12, + counter1:0, + max0:12, + max1:0, + buzz: true, + colortext: true, + }; + const settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + const menu = { + "": { "title": "Counter2" }, + "< Back": () => back(), + 'Default C1': { + value: settings[0], + min: -99, max: 99, + onchange: v => { + settings.max0 = v; + writeSettings(); + } + }, + 'Default C2': { + value: settings[2], + min: -99, max: 99, + onchange: v => { + settings.max1 = v; + writeSettings(); + } + }, + 'Color': { + value: settings.colortext, + format: v => v?"Text":"Backg", + onchange: v => { + settings.colortext = v; + console.log("Color",v); + writeSettings(); + } + }, + 'Vibrate': { + value: settings.buzz, + onchange: v => { + settings.buzz = v; + writeSettings(); + } + } + }; + // Show the menu + E.showMenu(menu); +}); diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index 5264e8d42..30bfdd560 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -7,3 +7,8 @@ Improve connection code 0.07: Make Bangle.js 2 compatible 0.08: Convert Yes/No On/Off in settings to checkboxes +0.09: Automatically reconnect on error +0.10: Fix cscsensor when using coospoo sensor that supports crank *and* wheel +0.11: Update to use blecsc library +0.12: Fix regression reporting cadence (reported per second when should be per minute) (fix #3434) +0.13: Fix total distance calculation \ No newline at end of file diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js index 4ebe7d57e..6a8ca8b0f 100644 --- a/apps/cscsensor/cscsensor.app.js +++ b/apps/cscsensor/cscsensor.app.js @@ -1,8 +1,3 @@ -var device; -var gatt; -var service; -var characteristic; - const SETTINGS_FILE = 'cscsensor.json'; const storage = require('Storage'); const W = g.getWidth(); @@ -17,12 +12,10 @@ class CSCSensor { constructor() { this.movingTime = 0; this.lastTime = 0; - this.lastBangleTime = Date.now(); this.lastRevs = -1; this.settings = storage.readJSON(SETTINGS_FILE, 1) || {}; this.settings.totaldist = this.settings.totaldist || 0; this.totaldist = this.settings.totaldist; - this.wheelCirc = (this.settings.wheelcirc || 2230)/25.4; this.speedFailed = 0; this.speed = 0; this.maxSpeed = 0; @@ -34,8 +27,6 @@ class CSCSensor { this.distFactor = this.qMetric ? 1.609344 : 1; this.screenInit = true; this.batteryLevel = -1; - this.lastCrankTime = 0; - this.lastCrankRevs = 0; this.showCadence = false; this.cadence = 0; } @@ -63,10 +54,6 @@ class CSCSensor { } } - updateBatteryLevel(event) { - if (event.target.uuid == "0x2a19") this.setBatteryLevel(event.target.value.getUint8(0)); - } - drawBatteryIcon() { g.setColor(1, 1, 1).drawRect(10*W/240, yStart+0.029167*H, 20*W/240, yStart+0.1125*H) .fillRect(14*W/240, yStart+0.020833*H, 16*W/240, yStart+0.029167*H) @@ -81,7 +68,7 @@ class CSCSensor { } updateScreenRevs() { - var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0; + var dist = this.distFactor*(this.lastRevs-this.lastRevsStart) * csc.settings.circum/*mm*/ / 1000000; var ddist = Math.round(100*dist)/100; var tdist = Math.round(this.distFactor*this.totaldist*10)/10; var dspeed = Math.round(10*this.distFactor*this.speed)/10; @@ -157,113 +144,38 @@ class CSCSensor { } } - updateSensor(event) { - var qChanged = false; - if (event.target.uuid == "0x2a5b") { - if (event.target.value.getUint8(0, true) & 0x2) { - // crank revolution - if enabled - const crankRevs = event.target.value.getUint16(1, true); - const crankTime = event.target.value.getUint16(3, true); - if (crankTime > this.lastCrankTime) { - this.cadence = (crankRevs-this.lastCrankRevs)/(crankTime-this.lastCrankTime)*(60*1024); - qChanged = true; - } - this.lastCrankRevs = crankRevs; - this.lastCrankTime = crankTime; - } else { - // wheel revolution - var wheelRevs = event.target.value.getUint32(1, true); - var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); - if (dRevs>0) { - qChanged = true; - this.totaldist += dRevs*this.wheelCirc/63360.0; - if ((this.totaldist-this.settings.totaldist)>0.1) { - this.settings.totaldist = this.totaldist; - storage.writeJSON(SETTINGS_FILE, this.settings); - } - } - this.lastRevs = wheelRevs; - if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; - var wheelTime = event.target.value.getUint16(5, true); - var dT = (wheelTime-this.lastTime)/1024; - var dBT = (Date.now()-this.lastBangleTime)/1000; - this.lastBangleTime = Date.now(); - if (dT<0) dT+=64; - if (Math.abs(dT-dBT)>3) dT = dBT; - this.lastTime = wheelTime; - this.speed = this.lastSpeed; - if (dRevs>0 && dT>0) { - this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; - this.speedFailed = 0; - this.movingTime += dT; - } else if (!this.showCadence) { - this.speedFailed++; - qChanged = false; - if (this.speedFailed>3) { - this.speed = 0; - qChanged = (this.lastSpeed>0); - } - } - this.lastSpeed = this.speed; - if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; - } - } - if (qChanged) this.updateScreen(); - } } var mySensor = new CSCSensor(); -function getSensorBatteryLevel(gatt) { - gatt.getPrimaryService("180f").then(function(s) { - return s.getCharacteristic("2a19"); - }).then(function(c) { - c.on('characteristicvaluechanged', (event)=>mySensor.updateBatteryLevel(event)); - return c.startNotifications(); - }); -} +var csc = require("blecsc").getInstance(); +csc.on("data", e => { + mySensor.totaldist += e.wr * csc.settings.circum/*mm*/ / 1000000; // finally in km + mySensor.lastRevs = e.cwr; + if (mySensor.lastRevsStart<0) mySensor.lastRevsStart = e.cwr; + mySensor.speed = e.kph; + mySensor.movingTime += e.wdt; + if (mySensor.speed>mySensor.maxSpeed && (mySensor.movingTime>3 || mySensor.speed<20) && mySensor.speed<50) + mySensor.maxSpeed = mySensor.speed; + mySensor.cadence = e.crps*60; + mySensor.updateScreen(); + mySensor.updateScreen(); +}); -function connection_setup() { - mySensor.screenInit = true; - E.showMessage("Scanning for CSC sensor..."); - NRF.requestDevice({ filters: [{services:["1816"]}]}).then(function(d) { - device = d; - E.showMessage("Found device"); - return device.gatt.connect(); - }).then(function(ga) { - gatt = ga; - E.showMessage("Connected"); - return gatt.getPrimaryService("1816"); - }).then(function(s) { - service = s; - return service.getCharacteristic("2a5b"); - }).then(function(c) { - characteristic = c; - characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); - return characteristic.startNotifications(); - }).then(function() { - console.log("Done!"); - g.reset().clearRect(Bangle.appRect).flip(); - getSensorBatteryLevel(gatt); - mySensor.updateScreen(); - }).catch(function(e) { - E.showMessage(e.toString(), "ERROR"); - console.log(e); - }); -} - -connection_setup(); +csc.on("status", txt => { + //print("->", txt); + E.showMessage(txt); +}); E.on('kill',()=>{ - if (gatt!=undefined) gatt.disconnect(); + csc.stop(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); }); -NRF.on('disconnect', connection_setup); // restart if disconnected Bangle.setUI("updown", d=>{ if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } - else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); } - else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } + else if (!d) { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } }); Bangle.loadWidgets(); Bangle.drawWidgets(); +csc.start(); // start a connection \ No newline at end of file diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json index d7c3add53..0029c4b82 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -2,10 +2,11 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.08", + "version": "0.13", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth,bike,cycle,bicycle", + "dependencies" : { "blecsc":"module" }, "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ diff --git a/apps/ctrlpad/ChangeLog b/apps/ctrlpad/ChangeLog new file mode 100644 index 000000000..d8c477701 --- /dev/null +++ b/apps/ctrlpad/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app - forked from widhid +0.02: Minor code improvements diff --git a/apps/ctrlpad/README.md b/apps/ctrlpad/README.md new file mode 100644 index 000000000..492957fe7 --- /dev/null +++ b/apps/ctrlpad/README.md @@ -0,0 +1,24 @@ +# Description + +A control pad app to provide fast access to common functions, such as bluetooth power, HRM and Do Not Disturb. +By dragging from the top of the watch, you have this control without leaving your current app (e.g. on a run, bike ride or just watching the clock). + +The app is designed to not conflict with other gestures - when the control pad is visible, it'll prevent propagation of events past it (touch, drag and swipe specifically). When the control pad is hidden, it'll ignore touch, drag and swipe events with the exception of an event dragging from the top 40 pixels of the screen. + + +# Usage + +Swipe down to enable and observe the overlay being dragged in. Swipe up on the overlay to hide it again. Then tap on a given button to trigger it. + +Requires espruino firmware > 2v17 to avoid event handler clashes. + + +# Setup / Technical details + +The control pad disables drag and touch event handlers while active, preventing other apps from interfering. + + +# Todo + +- Handle rotated screen (`g.setRotation(...)`) +- Handle notifications (sharing of `setLCDOverlay`) diff --git a/apps/ctrlpad/icon.js b/apps/ctrlpad/icon.js new file mode 100644 index 000000000..1e139312b --- /dev/null +++ b/apps/ctrlpad/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lcshAzwp9WlgAXldWp8rp5bIq1drwAdq0rFI1XBodXAC4rErorEFIlWLAOCAC2IxGCFY9WA4VWCAQAbJgavBlanCqwodFYpWBp4pCKbwACQYVQfoJUUruBD4dXBQeBBQZWCQIIqBq9dFSNXD4eBFQldDwgqBq4qDP4xEBqwKHFS6qFwVWQ4OsAgYqhAoOtAAYsBFUAbBFImI1uBDIgqQq4qJqwpEwIGCKwgqEroKEFQhsBFRNPwIACVIIECp4qHq16CAKATCAIACqwFEFQxIB6/XRoZVQABwqHLgQqiQAWAQBAqeD4IEDVaLRBABAqJq4qJq5VdwIqKQDwqWQBtXqoUDFQmBCAI2DKq+BvXX6wxCFQb6B6/XEAYqXrurD4N6CoIqDwOBBQIDBQCY1FJQOs1hVIBQgqLwQAFKwwgBVZAKFQDAlCCYYqEBQoqaq4qJrtdFTzJCFX4qoS4gqmCwYqewQqFQIIqhq9XEoNPp4qCQKOBCQeCPQgKEKAdWlYEBrpWSABtWKgNelcAQIdXFbxQBEYQqBgErrpXDq+CADBIBKYRUCAAKCBFYQsCADAoDrzTBFQRWBlZfCADp9BFIgACp4tCq4AYqxMCFAwAEBhgAWA==")) diff --git a/apps/ctrlpad/icon.js.png b/apps/ctrlpad/icon.js.png new file mode 100644 index 000000000..295b31f81 Binary files /dev/null and b/apps/ctrlpad/icon.js.png differ diff --git a/apps/ctrlpad/icon.png b/apps/ctrlpad/icon.png new file mode 100644 index 000000000..39634ea4d Binary files /dev/null and b/apps/ctrlpad/icon.png differ diff --git a/apps/ctrlpad/main.js b/apps/ctrlpad/main.js new file mode 100644 index 000000000..93f2864f7 --- /dev/null +++ b/apps/ctrlpad/main.js @@ -0,0 +1,314 @@ +(function () { + if (!Bangle.prependListener) { + Bangle.prependListener = function (evt, listener) { + var handlers = Bangle["#on".concat(evt)]; + if (!handlers) { + Bangle.on(evt, listener); + } + else { + if (typeof handlers === "function") { + Bangle.on(evt, listener); + } + Bangle["#on".concat(evt)] = [listener].concat(handlers.filter(function (f) { return f !== listener; })); + } + }; + } + var Overlay = (function () { + function Overlay() { + this.width = g.getWidth() - 10 * 2; + this.height = g.getHeight() - 24 - 10; + this.g2 = Graphics.createArrayBuffer(this.width, this.height, 4, { msb: true }); + this.renderG2(); + } + Overlay.prototype.setBottom = function (bottom) { + var g2 = this.g2; + var y = bottom - this.height; + Bangle.setLCDOverlay(g2, 10, y - 10); + }; + Overlay.prototype.hide = function () { + Bangle.setLCDOverlay(); + }; + Overlay.prototype.renderG2 = function () { + this.g2 + .reset() + .setColor(g.theme.bg) + .fillRect(0, 0, this.width, this.height) + .setColor(colour.on.bg) + .drawRect(0, 0, this.width - 1, this.height - 1) + .drawRect(1, 1, this.width - 2, this.height - 2); + }; + return Overlay; + }()); + var colour = { + on: { + fg: "#fff", + bg: "#00a", + }, + off: { + fg: "#000", + bg: "#bbb", + }, + }; + var Controls = (function () { + function Controls(g, controls) { + var height = g.getHeight(); + var centreY = height / 2; + var circleGapY = 30; + var width = g.getWidth(); + this.controls = [ + { x: width / 4 - 10, y: centreY - circleGapY }, + { x: width / 2, y: centreY - circleGapY }, + { x: width * 3 / 4 + 10, y: centreY - circleGapY }, + { x: width / 3, y: centreY + circleGapY }, + { x: width * 2 / 3, y: centreY + circleGapY }, + ].map(function (xy, i) { + var ctrl = xy; + var from = controls[i]; + ctrl.text = from.text; + ctrl.cb = from.cb; + Object.assign(ctrl, from.cb(false) ? colour.on : colour.off); + return ctrl; + }); + } + Controls.prototype.draw = function (g, single) { + g + .setFontAlign(0, 0) + .setFont("4x6:3"); + for (var _i = 0, _a = single ? [single] : this.controls; _i < _a.length; _i++) { + var ctrl = _a[_i]; + g + .setColor(ctrl.bg) + .fillCircle(ctrl.x, ctrl.y, 23) + .setColor(ctrl.fg) + .drawString(ctrl.text, ctrl.x, ctrl.y); + } + }; + Controls.prototype.hitTest = function (x, y) { + var dist = Infinity; + var closest; + for (var _i = 0, _a = this.controls; _i < _a.length; _i++) { + var ctrl = _a[_i]; + var dx = x - ctrl.x; + var dy = y - ctrl.y; + var d = Math.sqrt(dx * dx + dy * dy); + if (d < dist) { + dist = d; + closest = ctrl; + } + } + return dist < 30 ? closest : undefined; + }; + return Controls; + }()); + var state = 0; + var startY = 0; + var startedUpDrag = false; + var upDragAnim; + var ui; + var touchDown = false; + var initUI = function () { + if (ui) + return; + var controls = [ + { + text: "BLE", + cb: function (tap) { + var on = NRF.getSecurityStatus().advertising; + if (tap) { + if (on) + NRF.sleep(); + else + NRF.wake(); + } + return on !== tap; + } + }, + { + text: "DnD", + cb: function (tap) { + var on; + if ((on = !!origBuzz)) { + if (tap) { + Bangle.buzz = origBuzz; + origBuzz = undefined; + } + } + else { + if (tap) { + origBuzz = Bangle.buzz; + Bangle.buzz = function () { return Promise.resolve(); }; + setTimeout(function () { + if (!origBuzz) + return; + Bangle.buzz = origBuzz; + origBuzz = undefined; + }, 1000 * 60 * 10); + } + } + return on !== tap; + } + }, + { + text: "HRM", + cb: function (tap) { + var _a; + var id = "widhid"; + var hrm = (_a = Bangle._PWR) === null || _a === void 0 ? void 0 : _a.HRM; + var off = !hrm || hrm.indexOf(id) === -1; + if (off) { + if (tap) + Bangle.setHRMPower(1, id); + } + else if (tap) { + Bangle.setHRMPower(0, id); + } + return !off !== tap; + } + }, + { + text: "clk", + cb: function (tap) { + if (tap) + Bangle.showClock(), terminateUI(); + return true; + }, + }, + { + text: "lch", + cb: function (tap) { + if (tap) + Bangle.showLauncher(), terminateUI(); + return true; + }, + }, + ]; + var overlay = new Overlay(); + ui = { + overlay: overlay, + ctrls: new Controls(overlay.g2, controls), + }; + ui.ctrls.draw(ui.overlay.g2); + }; + var terminateUI = function () { + state = 0; + ui === null || ui === void 0 ? void 0 : ui.overlay.hide(); + ui = undefined; + }; + var onSwipe = function () { + var _a; + switch (state) { + case 0: + case 2: + return; + case 1: + case 3: + (_a = E.stopEventPropagation) === null || _a === void 0 ? void 0 : _a.call(E); + } + }; + Bangle.prependListener('swipe', onSwipe); + var onDrag = (function (e) { + var _a, _b, _c; + var dragDistance = 30; + if (e.b === 0) + touchDown = startedUpDrag = false; + switch (state) { + case 2: + if (e.b === 0) + state = 0; + break; + case 0: + if (e.b && !touchDown) { + if (e.y <= 40) { + state = 1; + startY = e.y; + (_a = E.stopEventPropagation) === null || _a === void 0 ? void 0 : _a.call(E); + } + else { + state = 2; + } + } + break; + case 1: + if (e.b === 0) { + if (e.y > startY + dragDistance) { + initUI(); + state = 3; + startY = 0; + Bangle.prependListener("touch", onTouch); + Bangle.buzz(20); + ui.overlay.setBottom(g.getHeight()); + } + else { + terminateUI(); + break; + } + } + else { + var dragOffset = 32; + initUI(); + ui.overlay.setBottom(e.y - dragOffset); + } + (_b = E.stopEventPropagation) === null || _b === void 0 ? void 0 : _b.call(E); + break; + case 3: + (_c = E.stopEventPropagation) === null || _c === void 0 ? void 0 : _c.call(E); + if (e.b) { + if (!touchDown) { + startY = e.y; + } + else if (startY) { + var dist = Math.max(0, startY - e.y); + if (startedUpDrag || (startedUpDrag = dist > 10)) + ui.overlay.setBottom(g.getHeight() - dist); + } + } + else if (e.b === 0) { + if ((startY - e.y) > dragDistance) { + var bottom_1 = g.getHeight() - Math.max(0, startY - e.y); + if (upDragAnim) + clearInterval(upDragAnim); + upDragAnim = setInterval(function () { + if (!ui || bottom_1 <= 0) { + clearInterval(upDragAnim); + upDragAnim = undefined; + terminateUI(); + return; + } + ui.overlay.setBottom(bottom_1); + bottom_1 -= 30; + }, 50); + Bangle.removeListener("touch", onTouch); + state = 0; + } + else { + ui.overlay.setBottom(g.getHeight()); + } + } + break; + } + if (e.b) + touchDown = true; + }); + var onTouch = (function (_btn, xy) { + var _a; + if (!ui || !xy) + return; + var top = g.getHeight() - ui.overlay.height; + var left = (g.getWidth() - ui.overlay.width) / 2; + var ctrl = ui.ctrls.hitTest(xy.x - left, xy.y - top); + if (ctrl) { + onCtrlTap(ctrl, ui); + (_a = E.stopEventPropagation) === null || _a === void 0 ? void 0 : _a.call(E); + } + }); + var origBuzz; + var onCtrlTap = function (ctrl, ui) { + Bangle.buzz(20); + var col = ctrl.cb(true) ? colour.on : colour.off; + ctrl.fg = col.fg; + ctrl.bg = col.bg; + ui.ctrls.draw(ui.overlay.g2, ctrl); + }; + Bangle.prependListener("drag", onDrag); + Bangle.on("lock", terminateUI); +})(); diff --git a/apps/ctrlpad/main.ts b/apps/ctrlpad/main.ts new file mode 100644 index 000000000..5faac60fa --- /dev/null +++ b/apps/ctrlpad/main.ts @@ -0,0 +1,418 @@ +(() => { + if(!Bangle.prependListener){ + type Event = T extends `#on${infer Evt}` ? Evt : never; + + Bangle.prependListener = function( + evt: Event, + listener: () => void + ){ + // move our drag to the start of the event listener array + const handlers = (Bangle as BangleEvents)[`#on${evt}`] + + if(!handlers){ + Bangle.on(evt as any, listener); + }else{ + if(typeof handlers === "function"){ + // get Bangle to convert to array + Bangle.on(evt as any, listener); + } + + // shuffle array + (Bangle as BangleEvents)[`#on${evt}`] = [listener as any].concat( + (handlers as Array).filter((f: unknown) => f !== listener) + ); + } + }; + } + + class Overlay { + g2: Graphics; + width: number; + height: number; + + constructor() { + // x padding: 10 each side + // y top: 24, y bottom: 10 + this.width = g.getWidth() - 10 * 2; + this.height = g.getHeight() - 24 - 10; + + this.g2 = Graphics.createArrayBuffer( + this.width, + this.height, + /*bpp*/4, + { msb: true } + ); + + this.renderG2(); + } + + setBottom(bottom: number): void { + const { g2 } = this; + const y = bottom - this.height; + + Bangle.setLCDOverlay(g2, 10, y - 10); + } + + hide(): void { + Bangle.setLCDOverlay(); + } + + renderG2(): void { + this.g2 + .reset() + .setColor(g.theme.bg) + .fillRect(0, 0, this.width, this.height) + .setColor(colour.on.bg) + .drawRect(0, 0, this.width - 1, this.height - 1) + .drawRect(1, 1, this.width - 2, this.height - 2); + } + } + + type ControlCallback = (tap: boolean) => boolean | number; + type Control = { + x: number, + y: number, + fg: ColorResolvable, + bg: ColorResolvable, + text: string, + cb: ControlCallback, + }; + + const colour = { + on: { + fg: "#fff", + bg: "#00a", + }, + off: { + fg: "#000", + bg: "#bbb", + }, + } as const; + + type FiveOf = [X, X, X, X, X]; + type ControlTemplate = { text: string, cb: ControlCallback }; + + class Controls { + controls: FiveOf; + + constructor(g: Graphics, controls: FiveOf) { + // const connected = NRF.getSecurityStatus().connected; + // if (0&&connected) { + // // TODO + // return [ + // { text: "<", cb: hid.next }, + // { text: "@", cb: hid.toggle }, + // { text: ">", cb: hid.prev }, + // { text: "-", cb: hid.down }, + // { text: "+", cb: hid.up }, + // ]; + // } + + const height = g.getHeight(); + const centreY = height / 2; + const circleGapY = 30; + const width = g.getWidth(); + + this.controls = [ + { x: width / 4 - 10, y: centreY - circleGapY }, + { x: width / 2, y: centreY - circleGapY }, + { x: width * 3/4 + 10, y: centreY - circleGapY }, + { x: width / 3, y: centreY + circleGapY }, + { x: width * 2/3, y: centreY + circleGapY }, + ].map((xy, i) => { + const ctrl = xy as Control; + const from = controls[i]!; + ctrl.text = from.text; + ctrl.cb = from.cb; + Object.assign(ctrl, from.cb(false) ? colour.on : colour.off); + return ctrl; + }) as FiveOf; + } + + draw(g: Graphics, single?: Control): void { + g + .setFontAlign(0, 0) + .setFont("4x6:3" as any); + + for(const ctrl of single ? [single] : this.controls){ + g + .setColor(ctrl.bg) + .fillCircle(ctrl.x, ctrl.y, 23) + .setColor(ctrl.fg) + .drawString(ctrl.text, ctrl.x, ctrl.y); + } + } + + hitTest(x: number, y: number): Control | undefined { + let dist = Infinity; + let closest; + + for(const ctrl of this.controls){ + const dx = x-ctrl.x; + const dy = y-ctrl.y; + const d = Math.sqrt(dx*dx + dy*dy); + if(d < dist){ + dist = d; + closest = ctrl; + } + } + + return dist < 30 ? closest : undefined; + } + } + + const enum State { + Idle, + TopDrag, + IgnoreCurrent, + Active, + } + type UI = { overlay: Overlay, ctrls: Controls }; + let state = State.Idle; + let startY = 0; + let startedUpDrag = false; + let upDragAnim: IntervalId | undefined; + let ui: undefined | UI; + let touchDown = false; + + const initUI = () => { + if (ui) return; + + const controls: FiveOf = [ + { + text: "BLE", + cb: tap => { + const on = NRF.getSecurityStatus().advertising; + if(tap){ + if(on) NRF.sleep(); + else NRF.wake(); + } + return on !== tap; // on ^ tap + } + }, + { + text: "DnD", + cb: tap => { + let on; + if((on = !!origBuzz)){ + if(tap){ + Bangle.buzz = origBuzz; + origBuzz = undefined; + } + }else{ + if(tap){ + origBuzz = Bangle.buzz; + Bangle.buzz = () => Promise.resolve(); + setTimeout(() => { + if(!origBuzz) return; + Bangle.buzz = origBuzz; + origBuzz = undefined; + }, 1000 * 60 * 10); + } + } + return on !== tap; // on ^ tap + } + }, + { + text: "HRM", + cb: tap => { + const id = "widhid"; + const hrm = (Bangle as any)._PWR?.HRM as undefined | Array ; + const off = !hrm || hrm.indexOf(id) === -1; + if(off){ + if(tap) + Bangle.setHRMPower(1, id); + }else if(tap){ + Bangle.setHRMPower(0, id); + } + return !off !== tap; // on ^ tap + } + }, + { + text: "clk", + cb: tap => { + if (tap) Bangle.showClock(), terminateUI(); + return true; + }, + }, + { + text: "lch", + cb: tap => { + if (tap) Bangle.showLauncher(), terminateUI(); + return true; + }, + }, + ]; + + const overlay = new Overlay(); + ui = { + overlay, + ctrls: new Controls(overlay.g2, controls), + }; + ui.ctrls.draw(ui.overlay.g2); + }; + + const terminateUI = () => { + state = State.Idle; + ui?.overlay.hide(); + ui = undefined; + }; + + const onSwipe = () => { + switch (state) { + case State.Idle: + case State.IgnoreCurrent: + return; + + case State.TopDrag: + case State.Active: + E.stopEventPropagation?.(); + } + }; + Bangle.prependListener('swipe', onSwipe); + + const onDrag = (e => { + const dragDistance = 30; + + if (e.b === 0) touchDown = startedUpDrag = false; + + switch (state) { + case State.IgnoreCurrent: + if(e.b === 0) + state = State.Idle; + break; + + case State.Idle: + if(e.b && !touchDown){ // no need to check Bangle.CLKINFO_FOCUS + if(e.y <= 40){ + state = State.TopDrag + startY = e.y; + E.stopEventPropagation?.(); + //console.log(" topdrag detected, starting @ " + startY); + }else{ + //console.log(" ignoring this drag (too low @ " + e.y + ")"); + state = State.IgnoreCurrent; + } + } + break; + + case State.TopDrag: + if(e.b === 0){ + //console.log("topdrag stopped, distance: " + (e.y - startY)); + if(e.y > startY + dragDistance){ + //console.log("activating"); + initUI(); + state = State.Active; + startY = 0; + Bangle.prependListener("touch", onTouch); + Bangle.buzz(20); + ui!.overlay.setBottom(g.getHeight()); + }else{ + //console.log("returning to idle"); + terminateUI(); + break; // skip stopEventPropagation + } + }else{ + // partial drag, show UI feedback: + const dragOffset = 32; + + initUI(); + ui!.overlay.setBottom(e.y - dragOffset); + } + E.stopEventPropagation?.(); + break; + + case State.Active: + //console.log("stolen drag handling, do whatever here"); + E.stopEventPropagation?.(); + if(e.b){ + if(!touchDown){ + startY = e.y; + }else if(startY){ + const dist = Math.max(0, startY - e.y); + + if (startedUpDrag || (startedUpDrag = dist > 10)) // ignore small drags + ui!.overlay.setBottom(g.getHeight() - dist); + } + }else if(e.b === 0){ + if((startY - e.y) > dragDistance){ + let bottom = g.getHeight() - Math.max(0, startY - e.y); + + if (upDragAnim) clearInterval(upDragAnim); + upDragAnim = setInterval(() => { + if (!ui || bottom <= 0) { + clearInterval(upDragAnim!); + upDragAnim = undefined; + terminateUI(); + return; + } + ui.overlay.setBottom(bottom); + bottom -= 30; + }, 50) + + Bangle.removeListener("touch", onTouch); + state = State.Idle; + }else{ + ui!.overlay.setBottom(g.getHeight()); + } + } + break; + } + if(e.b) touchDown = true; + }) satisfies DragCallback; + + const onTouch = ((_btn, xy) => { + if(!ui || !xy) return; + + const top = g.getHeight() - ui.overlay.height; // assumed anchored to bottom + const left = (g.getWidth() - ui.overlay.width) / 2; // more assumptions + + const ctrl = ui.ctrls.hitTest(xy.x - left, xy.y - top); + if(ctrl){ + onCtrlTap(ctrl, ui); + E.stopEventPropagation?.(); + } + }) satisfies TouchCallback; + + let origBuzz: undefined | (() => Promise); + const onCtrlTap = (ctrl: Control, ui: UI) => { + Bangle.buzz(20); + + const col = ctrl.cb(true) ? colour.on : colour.off; + ctrl.fg = col.fg; + ctrl.bg = col.bg; + //console.log("hit on " + ctrl.text + ", col: " + ctrl.fg); + + ui.ctrls.draw(ui.overlay.g2, ctrl); + }; + + Bangle.prependListener("drag", onDrag); + Bangle.on("lock", terminateUI); + + + /* + const settings = require("Storage").readJSON("setting.json", true) as Settings || ({ HID: false } as Settings); + const haveMedia = settings.HID === "kbmedia"; + // @ts-ignore + delete settings; + + const sendHid = (code: number) => { + try{ + NRF.sendHIDReport( + [1, code], + () => NRF.sendHIDReport([1, 0]), + ); + }catch(e){ + console.log("sendHIDReport:", e); + } + }; + + const hid = haveMedia ? { + next: () => sendHid(0x01), + prev: () => sendHid(0x02), + toggle: () => sendHid(0x10), + up: () => sendHid(0x40), + down: () => sendHid(0x80), + } : null; + */ +})() diff --git a/apps/ctrlpad/metadata.json b/apps/ctrlpad/metadata.json new file mode 100644 index 000000000..273dcdd7f --- /dev/null +++ b/apps/ctrlpad/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "ctrlpad", + "name": "Control Panel", + "shortName": "ctrlpad", + "version": "0.02", + "description": "Fast access (via a downward swipe) to common functions, such as bluetooth/HRM power and Do Not Disturb", + "icon": "icon.png", + "readme": "README.md", + "type": "bootloader", + "tags": "bluetooth", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"ctrlpad.boot.js","url":"main.js"}, + {"name":"ctrlpad.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/cycling/ChangeLog b/apps/cycling/ChangeLog index ec66c5568..9fec754fc 100644 --- a/apps/cycling/ChangeLog +++ b/apps/cycling/ChangeLog @@ -1 +1,3 @@ 0.01: Initial version +0.02: Minor code improvements +0.03: Move blecsc library into its own app so it can be shared (and fix some issues) \ No newline at end of file diff --git a/apps/cycling/README.md b/apps/cycling/README.md index 7ba8ee224..485537293 100644 --- a/apps/cycling/README.md +++ b/apps/cycling/README.md @@ -1,4 +1,5 @@ # Cycling + > Displays data from a BLE Cycling Speed and Cadence sensor. *This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.* @@ -27,8 +28,5 @@ Inside the Cycling app, use button / tap screen to: ## TODO * Sensor battery status * Implement crank events / show cadence -* Bangle.js 1 compatibility * Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike) -## Development -There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage. diff --git a/apps/cycling/blecsc-emu.js b/apps/cycling/blecsc-emu.js deleted file mode 100644 index ca5058545..000000000 --- a/apps/cycling/blecsc-emu.js +++ /dev/null @@ -1,111 +0,0 @@ -// UUID of the Bluetooth CSC Service -const SERVICE_UUID = "1816"; -// UUID of the CSC measurement characteristic -const MEASUREMENT_UUID = "2a5b"; - -// Wheel revolution present bit mask -const FLAGS_WREV_BM = 0x01; -// Crank revolution present bit mask -const FLAGS_CREV_BM = 0x02; - -/** - * Fake BLECSC implementation for the emulator, where it's hard to test - * with actual hardware. Generates "random" wheel events (no crank). - * - * To upload as a module, paste the entire file in the console using this - * command: require("Storage").write("blecsc-emu",``); - */ -class BLECSCEmulator { - constructor() { - this.timeout = undefined; - this.interval = 500; - this.ccr = 0; - this.lwt = 0; - this.handlers = { - // value - // disconnect - // wheelEvent - // crankEvent - }; - } - - getDeviceAddress() { - return 'fa:ke:00:de:vi:ce'; - } - - /** - * Callback for the GATT characteristicvaluechanged event. - * Consumers must not call this method! - */ - onValue(event) { - // Not interested in non-CSC characteristics - if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; - - // Notify the generic 'value' handler - if (this.handlers.value) this.handlers.value(event); - - const flags = event.target.value.getUint8(0, true); - // Notify the 'wheelEvent' handler - if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ - cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions - lwet: event.target.value.getUint16(5, true), // last wheel event time - }); - - // Notify the 'crankEvent' handler - if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ - ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions - lcet: event.target.value.getUint16(9, true), // last crank event time - }); - } - - /** - * Register an event handler. - * - * @param {string} event value|disconnect - * @param {function} handler handler function that receives the event as its first argument - */ - on(event, handler) { - this.handlers[event] = handler; - } - - fakeEvent() { - this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20)); - this.lwt = (this.lwt + this.interval) % 0x10000; - this.ccr++; - - var buffer = new ArrayBuffer(8); - var view = new DataView(buffer); - view.setUint8(0, 0x01); // Wheel revolution data present bit - view.setUint32(1, this.ccr, true); // Cumulative crank revolutions - view.setUint16(5, this.lwt, true); // Last wheel event time - - this.onValue({ - target: { - uuid: "0x2a5b", - value: view, - }, - }); - - this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); - } - - /** - * Find and connect to a device which exposes the CSC service. - * - * @return {Promise} - */ - connect() { - this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); - return Promise.resolve(true); - } - - /** - * Disconnect the device. - */ - disconnect() { - if (!this.timeout) return; - clearTimeout(this.timeout); - } -} - -exports = BLECSCEmulator; diff --git a/apps/cycling/blecsc.js b/apps/cycling/blecsc.js deleted file mode 100644 index 7a47108e5..000000000 --- a/apps/cycling/blecsc.js +++ /dev/null @@ -1,150 +0,0 @@ -const SERVICE_UUID = "1816"; -// UUID of the CSC measurement characteristic -const MEASUREMENT_UUID = "2a5b"; - -// Wheel revolution present bit mask -const FLAGS_WREV_BM = 0x01; -// Crank revolution present bit mask -const FLAGS_CREV_BM = 0x02; - -/** - * This class 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 - * - \`wheelEvent\` - the peripharial sends a notification containing wheel event data - * - \`crankEvent\` - the peripharial sends a notification containing crank event data - * - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event) - * - \`disconnect\` - the peripherial ends the connection or the connection is lost - * - * Each event can only have one handler. Any call to \`on()\` will - * replace a previously registered handler for the same event. - */ -class BLECSC { - constructor() { - this.device = undefined; - this.ccInterval = undefined; - this.gatt = undefined; - this.handlers = { - // wheelEvent - // crankEvent - // value - // disconnect - }; - } - - getDeviceAddress() { - if (!this.device || !this.device.id) - return '00:00:00:00:00:00'; - return this.device.id.split(" ")[0]; - } - - checkConnection() { - if (!this.device) - console.log("no device"); - // else - // console.log("rssi: " + this.device.rssi); - } - - /** - * Callback for the GATT characteristicvaluechanged event. - * Consumers must not call this method! - */ - onValue(event) { - // Not interested in non-CSC characteristics - if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; - - // Notify the generic 'value' handler - if (this.handlers.value) this.handlers.value(event); - - const flags = event.target.value.getUint8(0, true); - // Notify the 'wheelEvent' handler - if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ - cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions - lwet: event.target.value.getUint16(5, true), // last wheel event time - }); - - // Notify the 'crankEvent' handler - if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ - ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions - lcet: event.target.value.getUint16(9, true), // last crank event time - }); - } - - /** - * Callback for the NRF disconnect event. - * Consumers must not call this method! - */ - onDisconnect(event) { - console.log("disconnected"); - if (this.ccInterval) - clearInterval(this.ccInterval); - - if (!this.handlers.disconnect) return; - this.handlers.disconnect(event); - } - - /** - * Register an event handler. - * - * @param {string} event wheelEvent|crankEvent|value|disconnect - * @param {function} handler function that will receive the event as its first argument - */ - on(event, handler) { - this.handlers[event] = handler; - } - - /** - * Find and connect to a device which exposes the CSC service. - * - * @return {Promise} - */ - connect() { - // Register handler for the disconnect event to be passed throug - NRF.on('disconnect', this.onDisconnect.bind(this)); - - // Find a device, then get the CSC Service and subscribe to - // notifications on the CSC Measurement characteristic. - // NRF.setLowPowerConnection(true); - return NRF.requestDevice({ - timeout: 5000, - filters: [{ services: [SERVICE_UUID] }], - }).then(device => { - this.device = device; - this.device.on('gattserverdisconnected', this.onDisconnect.bind(this)); - this.ccInterval = setInterval(this.checkConnection.bind(this), 2000); - return device.gatt.connect(); - }).then(gatt => { - this.gatt = gatt; - return gatt.getPrimaryService(SERVICE_UUID); - }).then(service => { - return service.getCharacteristic(MEASUREMENT_UUID); - }).then(characteristic => { - characteristic.on('characteristicvaluechanged', this.onValue.bind(this)); - return characteristic.startNotifications(); - }); - } - - /** - * Disconnect the device. - */ - disconnect() { - if (this.ccInterval) - clearInterval(this.ccInterval); - - if (!this.gatt) return; - try { - this.gatt.disconnect(); - } catch { - // - } - } -} - -exports = BLECSC; diff --git a/apps/cycling/cycling.app.js b/apps/cycling/cycling.app.js index 268284a29..7261d3519 100644 --- a/apps/cycling/cycling.app.js +++ b/apps/cycling/cycling.app.js @@ -23,7 +23,6 @@ class CSCSensor { // CSC runtime variables this.movingTime = 0; // unit: s this.lastBangleTime = Date.now(); // unit: ms - this.lwet = 0; // last wheel event time (unit: s/1024) this.cwr = -1; // cumulative wheel revolutions this.cwrTrip = 0; // wheel revolutions since trip start this.speed = 0; // unit: m/s @@ -84,7 +83,7 @@ class CSCSensor { console.log("Trying to connect to BLE CSC"); // Hook up events - this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this)); + this.blecsc.on('data', this.onWheelEvent.bind(this)); this.blecsc.on('disconnect', this.onDisconnect.bind(this)); // Scan for BLE device and connect @@ -171,20 +170,11 @@ class CSCSensor { // Increment the trip revolutions counter this.cwrTrip += dRevs; - // Calculate time delta since last wheel event - var dT = (event.lwet - this.lwet)/1024; - var now = Date.now(); - var dBT = (now-this.lastBangleTime)/1000; - this.lastBangleTime = now; - if (dT<0) dT+=64; // wheel event time wraps every 64s - if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this - this.lwet = event.lwet; - // Recalculate current speed - if (dRevs>0 && dT>0) { - this.speed = dRevs * this.wheelCirc / dT; + if (dRevs>0 ) { + this.speed = event.wrps * this.wheelCirc; this.speedFailed = 0; - this.movingTime += dT; + this.movingTime += event.wdt; } else { this.speedFailed++; if (this.speedFailed>3) { @@ -429,15 +419,7 @@ class CSCDisplay { } } -var BLECSC; -if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") { - // Emulator - BLECSC = require("blecsc-emu"); -} else { - // Actual hardware - BLECSC = require("blecsc"); -} -var blecsc = new BLECSC(); +var blecsc = require("blecsc").getInstance(); var display = new CSCDisplay(); var sensor = new CSCSensor(blecsc, display); diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json index caf93eda3..51e51b409 100644 --- a/apps/cycling/metadata.json +++ b/apps/cycling/metadata.json @@ -2,16 +2,16 @@ "id": "cycling", "name": "Bangle Cycling", "shortName": "Cycling", - "version": "0.01", + "version": "0.03", "description": "Display live values from a BLE CSC sensor", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth", + "dependencies" : { "blecsc":"module" }, "supports": ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"cycling.app.js","url":"cycling.app.js"}, {"name":"cycling.settings.js","url":"settings.js"}, - {"name":"blecsc","url":"blecsc.js"}, {"name":"cycling.img","url":"cycling.icon.js","evaluate": true} ], "data": [ diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index 751164c07..3d8ef0472 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -8,3 +8,7 @@ 0.08: fix idle timer always getting set to true 0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.10: Use widget_utils. +0.11: Minor code improvements +0.12: Added setting to change Battery estimate to hours +0.13: Fixed Battery estimate Default to percentage and improved setting string +0.14: Use `power_usage` module diff --git a/apps/daisy/README.md b/apps/daisy/README.md index 491ed697f..5599d313c 100644 --- a/apps/daisy/README.md +++ b/apps/daisy/README.md @@ -10,7 +10,7 @@ Forum](http://forum.espruino.com/microcosms/1424/) * Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel) * Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer -* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate) +* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate, Battery Estimate) * The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle * The heart value is displayed in RED if the confidence value is less than 50% * NOTE: The heart rate monitor of Bangle JS 2 is not very accurate when moving about. @@ -20,6 +20,7 @@ See [#1248](https://github.com/espruino/BangleApps/issues/1248) [MyLocation](https://banglejs.com/apps/?id=mylocation) * The screen is updated every minute to save battery power * Uses the [BloggerSansLight](https://www.1001fonts.com/rounded-fonts.html?page=3) font, which if free for commercial use +* You need to run >2V22 to show the battery estimate in hours ## Future Development * Use mini icons in the information line rather that text diff --git a/apps/daisy/app.js b/apps/daisy/app.js index 3b3975105..4aa7eb132 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -1,6 +1,4 @@ var SunCalc = require("suncalc"); // from modules folder -const storage = require('Storage'); -const locale = require("locale"); const widget_utils = require('widget_utils'); const SETTINGS_FILE = "daisy.json"; const LOCATION_FILE = "mylocation.json"; @@ -85,6 +83,7 @@ function loadSettings() { settings.gy = settings.gy||'#020'; settings.fg = settings.fg||'#0f0'; settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); + settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours); assignPalettes(); } @@ -114,13 +113,39 @@ function updateSunRiseSunSet(now, lat, lon, line){ sunSet = extractTime(times.sunset); } +function batteryString(){ + let stringToInsert; + if (settings.batt_hours) { + var batt_usage = require("power_usage").get().hrsLeft; + let rounded; + if (batt_usage > 24) { + var days = Math.floor(batt_usage/24); + var hours = Math.round((batt_usage/24 - days) * 24); + stringToInsert = '\n' + days + ((days < 2) ? 'd' : 'ds') + ' ' + hours + ((hours < 2) ? 'h' : 'hs'); + } + else if (batt_usage > 9) { + rounded = Math.round(200000/E.getPowerUsage().total * 10) / 10; + } + else { + rounded = Math.round(200000/E.getPowerUsage().total * 100) / 100; + } + if (batt_usage < 24) { + stringToInsert = '\n' + rounded + ' ' + ((batt_usage < 2) ? 'h' : 'hs'); + } + } + else{ + stringToInsert = ' ' + E.getBattery() + '%'; + } + return 'BATTERY' + stringToInsert; +} + const infoData = { ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, ID_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} }, ID_SR: { calc: () => 'SUNRISE ' + sunRise }, ID_SS: { calc: () => 'SUNSET ' + sunSet }, ID_STEP: { calc: () => 'STEPS ' + getSteps() }, - ID_BATT: { calc: () => 'BATTERY ' + E.getBattery() + '%' }, + ID_BATT: { calc: batteryString}, ID_HRM: { calc: () => hrmCurrent } }; @@ -196,9 +221,9 @@ function draw() { function drawClock() { var date = new Date(); - var timeStr = require("locale").time(date,1); + //var timeStr = require("locale").time(date,1); var da = date.toString().split(" "); - var time = da[4].substr(0,5); + //var time = da[4].substr(0,5); var hh = da[4].substr(0,2); var mm = da[4].substr(3,2); var steps = getSteps(); diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 471f8e56f..178cd8b20 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.10", + "version": "0.14", "dependencies": {"mylocation":"app"}, "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "icon": "app.png", diff --git a/apps/daisy/settings.js b/apps/daisy/settings.js index 6397a81f4..c0a2ffeea 100644 --- a/apps/daisy/settings.js +++ b/apps/daisy/settings.js @@ -5,7 +5,8 @@ let s = {'gy' : '#020', 'fg' : '#0f0', 'color': 'Green', - 'check_idle' : true}; + 'check_idle' : true, + 'batt_hours' : false}; // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -45,6 +46,14 @@ s.idle_check = v; save(); }, + }, + 'Expected Battery Life In Days Not Percentage': { + value: !!s.batt_hours, + onchange: v => { + s.batt_hours = v; + save(); + }, } }); }) + diff --git a/apps/dane_tcr/ChangeLog b/apps/dane_tcr/ChangeLog index 69424b1f4..05ef79052 100644 --- a/apps/dane_tcr/ChangeLog +++ b/apps/dane_tcr/ChangeLog @@ -6,3 +6,4 @@ 0.06: remove app image as it is unused 0.07: Bump version number for change to apps.json causing 404 on upload 0.08: Use default Bangle formatter for booleans +0.09: Minor code improvements diff --git a/apps/dane_tcr/app.js b/apps/dane_tcr/app.js index ce75c55cb..ce8c98025 100644 --- a/apps/dane_tcr/app.js +++ b/apps/dane_tcr/app.js @@ -1,11 +1,6 @@ var d = require("dane_arwes"); var Arwes = d.default(); -const yOffset = 23; -const width = g.getWidth(); -const height = g.getHeight(); -const xyCenter = width / 2 + 4; - const Storage = require("Storage"); const filename = 'dane_tcr.json'; let settings = Storage.readJSON(filename,1) || { diff --git a/apps/dane_tcr/metadata.json b/apps/dane_tcr/metadata.json index 5527c846d..c6a649f0e 100644 --- a/apps/dane_tcr/metadata.json +++ b/apps/dane_tcr/metadata.json @@ -2,7 +2,7 @@ "id": "dane_tcr", "name": "DANE Touch Launcher", "shortName": "DANE Toucher", - "version": "0.08", + "version": "0.09", "description": "Touch enable left to right launcher in the style of the DANE Watchface", "icon": "app.png", "type": "launch", diff --git a/apps/datetime_picker/ChangeLog b/apps/datetime_picker/ChangeLog new file mode 100644 index 000000000..ef4afacd0 --- /dev/null +++ b/apps/datetime_picker/ChangeLog @@ -0,0 +1 @@ +0.01: New drag/swipe date time picker, e.g. for use with dated events alarms diff --git a/apps/datetime_picker/README.md b/apps/datetime_picker/README.md new file mode 100644 index 000000000..f602d44e1 --- /dev/null +++ b/apps/datetime_picker/README.md @@ -0,0 +1,36 @@ +# App Name + +Datetime Picker allows to swipe along the bars to select date and time elements, e.g. for the datetime of Events in the Alarm App. + +Screenshot: ![datetime with swipe controls](screenshot.png) + +## Controls + +Swipe to increase or decrease date and time elements. Press button or go back to select shown datetime. + +![datetime with numbered swipe controls](screenshot2.png) + +1. Year: swipe up to increase, down to decrease +2. Month: swipe right to increase, left to decrease +3. Day: swipe up to increase, down to decrease +4. Week: swipe up to increase week (same day next week), down to decrease (same day previous week) +5. Weekday: swipe right to increase, left to decrease (basically the same effect as 3, but with a focus on the weekday) +6. Hour: swipe right to increase, left to decrease +7. Minutes: swipe right to increase, left to decrease +8. 15 minutes: 00, 15, 30 or 45 minutes; swipe up to increase, down to decrease; wrap-around i.e. goes back to 00 after increasing from 45 + +## How to use it in code + +Sample code which would show a prompt with the number of days and hours between now and the selected datetime: + + require("datetimeinput").input().then(result => { + E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() { + load(); + }); + }); + +To set the initial value, pass a Date object named _datetime_, e.g. for today at 9:30 : + + var datetime = new Date(); + datetime.setHours(9, 30); + require("datetimeinput").input({datetime}).then(... \ No newline at end of file diff --git a/apps/datetime_picker/app-icon.js b/apps/datetime_picker/app-icon.js new file mode 100644 index 000000000..89250ff58 --- /dev/null +++ b/apps/datetime_picker/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AAnfAgf9z4FD/AFE/gFECIoFB98+tv+voFB//C/99z3Z7+J84XC3/7DpAFhKYP3AgP3AoPAOQMD/v/84LB+Z2FABiDKPoqJFKaWe/P/9Pznuf+wKB/29z+2//uTYOeTYPtRMxZKQaPAh6hBnEBwEGAoMYgHf9+/dwP5A==")) diff --git a/apps/datetime_picker/app.js b/apps/datetime_picker/app.js new file mode 100644 index 000000000..7bc66f6c5 --- /dev/null +++ b/apps/datetime_picker/app.js @@ -0,0 +1,5 @@ +require("datetimeinput").input().then(result => { + E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() { + load(); + }); +}); diff --git a/apps/datetime_picker/app.png b/apps/datetime_picker/app.png new file mode 100644 index 000000000..b7cb4b46b Binary files /dev/null and b/apps/datetime_picker/app.png differ diff --git a/apps/datetime_picker/lib.js b/apps/datetime_picker/lib.js new file mode 100644 index 000000000..c3e51ae4d --- /dev/null +++ b/apps/datetime_picker/lib.js @@ -0,0 +1,145 @@ +exports.input = function(options) { + options = options||{}; + var selectedDate; + if (options.datetime instanceof Date) { + selectedDate = new Date(options.datetime.getTime()); + } else { + selectedDate = new Date(); + selectedDate.setMinutes(0); + selectedDate.setSeconds(0); + selectedDate.setMilliseconds(0); + selectedDate.setHours(selectedDate.getHours() + 1); + } + + var R; + var tip = {w: 12, h: 10}; + var arrowRectArray; + var dragging = null; + var startPos = null; + var dateAtDragStart = null; + var SELECTEDFONT = '6x8:2'; + + function drawDateTime() { + g.clearRect(R.x+tip.w,R.y,R.x2-tip.w,R.y+40); + g.clearRect(R.x+tip.w,R.y2-60,R.x2-tip.w,R.y2-40); + + g.setFont(SELECTEDFONT).setColor(g.theme.fg).setFontAlign(-1, -1, 0); + var dateUtils = require('date_utils'); + g.drawString(selectedDate.getFullYear(), R.x+tip.w+10, R.y+15) + .drawString(dateUtils.month(selectedDate.getMonth()+1,1), R.x+tip.w+65, R.y+15) + .drawString(selectedDate.getDate(), R.x2-tip.w-40, R.y+15) + .drawString(`${dateUtils.dow(selectedDate.getDay(), 1)} ${selectedDate.toLocalISOString().slice(11,16)}`, R.x+tip.w+10, R.y2-60); + } + + let dragHandler = function(event) { + "ram"; + + if (event.b) { + if (dragging === null) { + // determine which component we are affecting + var rect = arrowRectArray.find(rect => rect.y2 + ? (event.y >= rect.y && event.y <= rect.y2 && event.x >= rect.x - 10 && event.x <= rect.x + tip.w + 10) + : (event.x >= rect.x && event.x <= rect.x2 && event.y >= rect.y - tip.w - 5 && event.y <= rect.y + 5)); + if (rect) { + dragging = rect; + startPos = dragging.y2 ? event.y : event.x; + dateAtDragStart = selectedDate; + } + } + + if (dragging) { + dragging.swipe(dragging.y2 ? startPos - event.y : event.x - startPos); + drawDateTime(); + } + } else { + dateAtDragStart = null; + dragging = null; + startPos = null; + } + }; + + let catchSwipe = ()=>{ + E.stopEventPropagation&&E.stopEventPropagation(); + }; + + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe listener if it was added with `Bangle.prependListener()` (fw2v19 and up). + g.clearRect(Bangle.appRect); + resolve(selectedDate); + }, + drag: dragHandler + }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. + + R = Bangle.appRect; + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + function drawArrow(rect) { + if(rect.x2) { + g.fillRect(rect.x + tip.h, rect.y - tip.w + 4, rect.x2 - tip.h, rect.y - 4) + .fillPoly([rect.x + tip.h, rect.y, rect.x + tip.h, rect.y - tip.w, rect.x, rect.y - (tip.w / 2)]) + .fillPoly([rect.x2-tip.h, rect.y, rect.x2 - tip.h, rect.y - tip.w, rect.x2, rect.y - (tip.w / 2)]); + } else { + g.fillRect(rect.x + 4, rect.y + tip.h, rect.x + tip.w - 4, rect.y2 - tip.h) + .fillPoly([rect.x, rect.y + tip.h, rect.x + tip.w, rect.y + tip.h, rect.x + (tip.w / 2), rect.y]) + .fillPoly([rect.x, rect.y2 - tip.h, rect.x + tip.w, rect.y2 - tip.h, rect.x + (tip.w / 2), rect.y2]); + } + + } + + var yearArrowRect = {x: R.x, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setFullYear(dateAtDragStart.getFullYear() + Math.floor(d/10)); + if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0); + }}; + + var weekArrowRect = {x: R.x, y: yearArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + (Math.floor(d/10) * 7)); + }}; + + var dayArrowRect = {x: R.x2 - tip.w, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10)); + }}; + + var fifteenMinutesArrowRect = {x: R.x2 - tip.w, y: dayArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMinutes((((dateAtDragStart.getMinutes() - (dateAtDragStart.getMinutes() % 15) + (Math.floor(d/14) * 15)) % 60) + 60) % 60); + }}; + + var weekdayArrowRect = {x: R.x, y: R.y2, x2: (R.x2 - R.x) * 0.3 - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10)); + }}; + + var hourArrowRect = {x: weekdayArrowRect.x2 + 5, y: R.y2, x2: weekdayArrowRect.x2 + (R.x2 - R.x) * 0.38, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setHours((((dateAtDragStart.getHours() + Math.floor(d/10)) % 24) + 24) % 24); + }}; + + var minutesArrowRect = {x: hourArrowRect.x2 + 5, y: R.y2, x2: R.x2, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMinutes((((dateAtDragStart.getMinutes() + Math.floor(d/7)) % 60) + 60) % 60); + }}; + + var monthArrowRect = {x: (R.x2 - R.x) * 0.2, y: R.y2 / 2 + 5, x2: (R.x2 - R.x) * 0.8, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMonth(dateAtDragStart.getMonth() + Math.floor(d/10)); + if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0); + }}; + + arrowRectArray = [yearArrowRect, weekArrowRect, dayArrowRect, fifteenMinutesArrowRect, + weekdayArrowRect, hourArrowRect, minutesArrowRect, monthArrowRect]; + + drawDateTime(); + arrowRectArray.forEach(drawArrow); + }); +}; diff --git a/apps/datetime_picker/metadata.json b/apps/datetime_picker/metadata.json new file mode 100644 index 000000000..173e21020 --- /dev/null +++ b/apps/datetime_picker/metadata.json @@ -0,0 +1,17 @@ +{ "id": "datetime_picker", + "name": "Datetime picker", + "shortName":"Datetime picker", + "version":"0.01", + "description": "A library that allows to pick a date and time by swiping.", + "icon":"app.png", + "type":"module", + "tags":"datetimeinput", + "supports" : ["BANGLEJS2"], + "provides_modules" : ["datetimeinput"], + "readme": "README.md", + "screenshots" : [ { "url":"screenshot.png" } ], + "storage": [ + {"name":"datetimeinput","url":"lib.js"}, + {"name":"datetime_picker.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/datetime_picker/screenshot.png b/apps/datetime_picker/screenshot.png new file mode 100644 index 000000000..6e14af8be Binary files /dev/null and b/apps/datetime_picker/screenshot.png differ diff --git a/apps/datetime_picker/screenshot2.png b/apps/datetime_picker/screenshot2.png new file mode 100644 index 000000000..9a1f9d048 Binary files /dev/null and b/apps/datetime_picker/screenshot2.png differ diff --git a/apps/dclock/clock-dev.js b/apps/dclock/clock-dev.js index d2c3893d5..914234060 100644 --- a/apps/dclock/clock-dev.js +++ b/apps/dclock/clock-dev.js @@ -70,7 +70,7 @@ function drawSimpleClock() { var dom = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate(); //Days since full moon - var knownnew = new Date(2020,02,24,09,28,0); + var knownnew = new Date(2020,2,24,9,28,0); // Get millisecond difference and divide down to cycles var cycles = (d.getTime()-knownnew.getTime())/1000/60/60/24/29.53; diff --git a/apps/dedreckon/ChangeLog b/apps/dedreckon/ChangeLog new file mode 100644 index 000000000..263d4078d --- /dev/null +++ b/apps/dedreckon/ChangeLog @@ -0,0 +1 @@ +0.01: attempt to import diff --git a/apps/dedreckon/README.md b/apps/dedreckon/README.md new file mode 100644 index 000000000..706c7f191 --- /dev/null +++ b/apps/dedreckon/README.md @@ -0,0 +1,20 @@ +# Ded Reckon + +Dead Reckoning using compass and step counter. + +This allows logging track using "dead reckoning" -- that's logging +angles from compass and distances from step counter. You need to mark +turns, and point watch to direction of the turn. Simultaneously, it +tries to log positions using GPS. You can use it to calibrate your +step length by comparing GPS and step counter data. It can also get +pretty accurate recording of track walked in right circumstances. + +Tap bottom part of the screen to select display (text or map for +now). Point watch to new direction, then tap top left part of screen +to indicate a turn. + +Map shows blue line for track from dead reckonging, and green line for +track from GPS. + +You probably want magnav installed (and calibrated) for useful +results, as it provides library with better compass. \ No newline at end of file diff --git a/apps/dedreckon/app-icon.js b/apps/dedreckon/app-icon.js new file mode 100644 index 000000000..39b72f00b --- /dev/null +++ b/apps/dedreckon/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AFsAFtoADF1wwqF4wwhEI5goGGIjFYN4wFF1KbHGUolIMc4lGSdIwJd9DstAH7FrBywwgad4veDwojJBIIvcFwIACGBYICGDYvEGBYvdFwqyLL8i+LF7oxFRxgveGAQ0EF5IwfMY4vpL5AFLAEYv/F8owoE44vrAY4vmAQIEEF85dGGE0AE4gvoFwpmHd0oINAH4A/AH4AvA")) diff --git a/apps/dedreckon/app.png b/apps/dedreckon/app.png new file mode 100644 index 000000000..db3fcfb88 Binary files /dev/null and b/apps/dedreckon/app.png differ diff --git a/apps/dedreckon/dedreckon.app.js b/apps/dedreckon/dedreckon.app.js new file mode 100644 index 000000000..449bf9c1b --- /dev/null +++ b/apps/dedreckon/dedreckon.app.js @@ -0,0 +1,442 @@ +/* Ded Reckon */ +/* eslint-disable no-unused-vars */ + +/* fmt library v0.1.3 */ +let fmt = { + icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3", + icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + + /* 0 .. DD.ddddd + 1 .. DD MM.mmm' + 2 .. DD MM'ss" + */ + geo_mode : 1, + + init: function() {}, + fmtDist: function(km) { + if (km >= 1.0) return km.toFixed(1) + this.icon_km; + return (km*1000).toFixed(0) + this.icon_m; + }, + fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); }, + fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; }, + draw_dot : 1, + add0: function(i) { + if (i > 9) { + return ""+i; + } else { + return "0"+i; + } + }, + fmtTOD: function(now) { + this.draw_dot = !this.draw_dot; + let dot = ":"; + if (!this.draw_dot) + dot = "."; + return now.getHours() + dot + this.add0(now.getMinutes()); + }, + fmtNow: function() { return this.fmtTOD(new Date()); }, + fmtTimeDiff: function(d) { + if (d < 180) + return ""+d.toFixed(0); + d = d/60; + return ""+d.toFixed(0)+"m"; + }, + fmtAngle: function(x) { + switch (this.geo_mode) { + case 0: + return "" + x; + case 1: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + return "" + d + " " + m.toFixed(3) + "'"; + } + case 2: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + let mf = Math.floor(m); + let s = m - mf; + s = s*60; + return "" + d + " " + mf + "'" + s.toFixed(0) + '"'; + } + } + return "bad mode?"; + }, + fmtPos: function(pos) { + let x = pos.lat; + let c = "N"; + if (x<0) { + c = "S"; + x = -x; + } + let s = c+this.fmtAngle(x) + "\n"; + c = "E"; + if (x<0) { + c = "W"; + x = -x; + } + return s + c + this.fmtAngle(x); + }, + fmtFix: function(fix, t) { + if (fix && fix.fix && fix.lat) { + return this.fmtSpeed(fix.speed) + " " + + this.fmtAlt(fix.alt); + } else { + return "N/FIX " + this.fmtTimeDiff(t); + } + }, + fmtSpeed: function(kph) { + return kph.toFixed(1) + this.icon_kph; + }, +}; + +/* gps library v0.1.1 */ +let gps = { + emulator: -1, + init: function(x) { + this.emulator = (process.env.BOARD=="EMSCRIPTEN" + || process.env.BOARD=="EMSCRIPTEN2")?1:0; + }, + state: {}, + on_gps: function(f) { + let fix = this.getGPSFix(); + f(fix); + + /* + "lat": number, // Latitude in degrees + "lon": number, // Longitude in degrees + "alt": number, // altitude in M + "speed": number, // Speed in kph + "course": number, // Course in degrees + "time": Date, // Current Time (or undefined if not known) + "satellites": 7, // Number of satellites + "fix": 1 // NMEA Fix state - 0 is no fix + "hdop": number, // Horizontal Dilution of Precision + */ + this.state.timeout = setTimeout(this.on_gps, 1000, f); + }, + off_gps: function() { + clearTimeout(this.state.timeout); + }, + getGPSFix: function() { + if (!this.emulator) + return Bangle.getGPSFix(); + let fix = {}; + fix.fix = 1; + fix.lat = 50; + fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */ + fix.alt = 200; + fix.speed = 5; + fix.course = 30; + fix.time = Date(); + fix.satellites = 5; + fix.hdop = 12; + return fix; + }, + gps_start : -1, + start_gps: function() { + Bangle.setGPSPower(1, "libgps"); + this.gps_start = getTime(); + }, + stop_gps: function() { + Bangle.setGPSPower(0, "libgps"); + }, +}; + +/* ui library 0.1 */ +let ui = { + display: 0, + numScreens: 2, + drawMsg: function(msg) { + g.reset().setFont("Vector", 35) + .setColor(1,1,1) + .fillRect(0, this.wi, 176, 176) + .setColor(0,0,0) + .drawString(msg, 5, 30); + }, + drawBusy: function() { + this.drawMsg("\n.oO busy"); + }, + nextScreen: function() { + print("nextS"); + this.display = this.display + 1; + if (this.display == this.numScreens) + this.display = 0; + this.drawBusy(); + }, + prevScreen: function() { + print("prevS"); + this.display = this.display - 1; + if (this.display < 0) + this.display = this.numScreens - 1; + this.drawBusy(); +}, + onSwipe: function(dir) { + this.nextScreen(); +}, + h: 176, + w: 176, + wi: 32, + last_b: 0, + touchHandler: function(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + if (d.b != 1 || this.last_b != 0) { + this.last_b = d.b; + return; + } + + print("touch", x, y, this.h, this.w); + + /* + if ((xthis.h/2) && (ythis.w/2)) { + print("prev"); + this.prevScreen(); + } + if ((x>this.h/2) && (y>this.w/2)) { + print("next"); + this.nextScreen(); + } + }, + init: function() { + } +}; + +var last_steps = Bangle.getStepCount(), last_time = getTime(), speed = 0, step_phase = 0; + +var mpstep = 0.719 * 1.15; + +function updateSteps() { + if (step_phase ++ > 9) { + step_phase =0; + let steps = Bangle.getStepCount(); + let time = getTime(); + + speed = 3.6 * mpstep * ((steps-last_steps) / (time-last_time)); + last_steps = steps; + last_time = time; + } + return "" + fmt.fmtSpeed(speed) + " " + step_phase + "\n" + fmt.fmtDist(log_dist/1000) + " " + fmt.fmtDist(log_last/1000); +} + +/* compensated compass */ +var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; +const tiltfixread = require("magnav").tiltfixread; +var heading; + + +var cancel_gps = false; + +function drawStats() { + let fix = gps.getGPSFix(); + + let msg = fmt.fmtFix(fix, getTime() - gps.gps_start); + + msg += "\n" + fmt.fmtDist(gps_dist/1000) + " " + fmt.fmtDist(gps_last/1000) + "\n" + updateSteps(); + let c = Bangle.getCompass(); + if (c) msg += "\n" + c.heading.toFixed(0) + "/" + heading.toFixed(0) + "deg " + log.length + "\n"; + + g.reset().clear().setFont("Vector", 31) + .setColor(1,1,1) + .fillRect(0, 24, 176, 100) + .setColor(0,0,0) + .drawString(msg, 3, 25); +} + +function updateGps() { + if (cancel_gps) + return; + heading = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + if (ui.display == 0) { + setTimeout(updateGps, 1000); + drawLog(); + drawStats(); + } + if (ui.display == 1) { + setTimeout(updateGps, 1000); + drawLog(); + } +} + +function stopGps() { + cancel_gps=true; + gps.stop_gps(); +} + +var log = [], log_dist = 0, gps_dist = 0; +var log_last = 0, gps_last = 0; + +function logEntry() { + let e = {}; + e.time = getTime(); + e.fix = gps.getGPSFix(); + e.steps = Bangle.getStepCount(); + if (0) { + let c = Bangle.getCompass(); + if (c) + e.dir = c.heading; + else + e.dir = -1; + } else { + e.dir = heading; + } + return e; +} + +function onTurn() { + let e = logEntry(); + log.push(e); +} + +function radians(a) { return a*Math.PI/180; } +function degrees(a) { return a*180/Math.PI; } +// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km +// https://www.movable-type.co.uk/scripts/latlong.html +// (Equirectangular approximation) +function calcDistance(a,b) { + var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.sqrt(x*x + y*y) * 6371000; +} + +var dn, de; +function initConv(fix) { + let n = { lat: fix.lat+1, lon: fix.lon }; + let e = { lat: fix.lat, lon: fix.lon+1 }; + + dn = calcDistance(fix, n); + de = calcDistance(fix, e); + print("conversion is ", dn, 108000, de, 50000); +} +function toM(start, fix) { + return { x: (fix.lon - start.lon) * de, y: (fix.lat - start.lat) * dn }; +} +var mpp = 4; +function toPix(q) { + let p = { x: q.x, y: q.y }; + p.x /= mpp; /* 10 m / pix */ + p.y /= -mpp; + p.x += 85; + p.y += 85; + return p; +} + +function drawLog() { + let here = logEntry(); + if (!here.fix.lat) { + here.fix.lat = 50; + here.fix.lon = 14; + } + initConv(here.fix); + log.push(here); + let l = log; + log_dist = 0; + log_last = -1; + gps_last = -1; + + g.reset().clear(); + g.setColor(0, 0, 1); + let last = { x: 0, y: 0 }; + for (let i = l.length - 2; i >= 0; i--) { + let next = {}; + let m = (l[i+1].steps - l[i].steps) * mpstep; + let dir = radians(180 + l[i].dir); + next.x = last.x + m * Math.sin(dir); + next.y = last.y + m * Math.cos(dir); + print(dir, m, last, next); + let lp = toPix(last); + let np = toPix(next); + g.drawLine(lp.x, lp.y, np.x, np.y); + g.drawCircle(np.x, np.y, 3); + last = next; + if (log_last == -1) + log_last = m; + log_dist += m; + } + g.setColor(0, 1, 0); + last = { x: 0, y: 0 }; + gps_dist = 0; + for (let i = l.length - 2; i >= 0; i--) { + let fix = l[i].fix; + if (fix.fix && fix.lat) { + let next = toM(here.fix, fix); + let lp = toPix(last); + let np = toPix(next); + let d = Math.sqrt((next.x-last.x)*(next.x-last.x)+(next.y-last.y)*(next.y-last.y)); + if (gps_last == -1) + gps_last = d; + gps_dist += d; + g.drawLine(lp.x, lp.y, np.x, np.y); + g.drawCircle(np.x, np.y, 3); + last = next; + } + } + log.pop(); +} + +function testPaint() { + let pos = gps.getGPSFix(); + log = []; + let e = { fix: pos, steps: 100, dir: 0 }; + log.push(e); + e = { fix: pos, steps: 200, dir: 90 }; + log.push(e); + e = { fix: pos, steps: 300, dir: 0 }; + log.push(e); + print(log, log.length, log[0], log[1]); + drawLog(); +} + +function touchHandler(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + if (d.b != 1 || ui.last_b != 0) { + ui.last_b = d.b; + return; + } + + + if ((xui.h/2) && (y ui.onSwipe(s), + clock : 0 +}); + +if (0) + testPaint(); +if (1) { + g.reset(); + updateGps(); +} diff --git a/apps/dedreckon/metadata.json b/apps/dedreckon/metadata.json new file mode 100644 index 000000000..79bf8868e --- /dev/null +++ b/apps/dedreckon/metadata.json @@ -0,0 +1,13 @@ +{ "id": "dedreckon", + "name": "Ded Reckon", + "version": "0.01", + "description": "Dead Reckoning using compass and step counter", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "outdoors", + "storage": [ + {"name":"dedreckon.app.js","url":"dedreckon.app.js"}, + {"name":"dedreckon.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/delaylock/ChangeLog b/apps/delaylock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/delaylock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/delaylock/README.md b/apps/delaylock/README.md new file mode 100644 index 000000000..da2ef3cda --- /dev/null +++ b/apps/delaylock/README.md @@ -0,0 +1,23 @@ +# Delayed Locking + +Delay the locking of the touchscreen to 5 seconds after the backlight turns off. Giving you the chance to interact with the watch without having to press the hardware button again. + +## Usage + +Just install and the behavior is tweaked at boot time. + +## Features + +- respects the LCD Timeout and Brightness as configured in the settings app. + +## Requests + +Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions. + +## Creator + +thyttan + +## Acknowledgements + +Inspired by the conversation between Gordon Williams and user156427 linked here: https://forum.espruino.com/conversations/392219/ diff --git a/apps/delaylock/app.png b/apps/delaylock/app.png new file mode 100644 index 000000000..7bdce945d Binary files /dev/null and b/apps/delaylock/app.png differ diff --git a/apps/delaylock/boot.js b/apps/delaylock/boot.js new file mode 100644 index 000000000..87dcbf186 --- /dev/null +++ b/apps/delaylock/boot.js @@ -0,0 +1,21 @@ +{ + let backlightTimeout = Bangle.getOptions().backlightTimeout; + let brightness = require("Storage").readJSON("setting.json", true); + brightness = brightness?brightness.brightness:1; + + Bangle.setOptions({ + backlightTimeout: backlightTimeout, + lockTimeout: backlightTimeout+5000 + }); + + let turnLightsOn = (_,numOrObj)=>{ + if (!Bangle.isBacklightOn()) { + Bangle.setLCDPower(brightness); + if (typeof numOrObj !== "number") E.stopEventPropagation(); // Touches will not be passed on to other listeners, but swipes will. + } + }; + + setWatch(turnLightsOn, BTN1, { repeat: true, edge: 'rising' }); + Bangle.prependListener("swipe", turnLightsOn); + Bangle.prependListener("touch", turnLightsOn); +} diff --git a/apps/delaylock/metadata.json b/apps/delaylock/metadata.json new file mode 100644 index 000000000..dff4d9219 --- /dev/null +++ b/apps/delaylock/metadata.json @@ -0,0 +1,13 @@ +{ "id": "delaylock", + "name": "Delayed Locking", + "version":"0.01", + "description": "Delay the locking of the screen to 5 seconds after the backlight turns off.", + "icon": "app.png", + "tags": "settings, configuration, backlight, touchscreen, screen", + "type": "bootloader", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"delaylock.boot.js","url":"boot.js"} + ] +} diff --git a/apps/devstopwatch/ChangeLog b/apps/devstopwatch/ChangeLog index 7e90e061e..11567d141 100644 --- a/apps/devstopwatch/ChangeLog +++ b/apps/devstopwatch/ChangeLog @@ -5,4 +5,6 @@ realigned quick n dirty screen positions help adjusted to fit bangle1 & bangle2 screen-size with widgets fixed bangle2 colors for chrono and last lap highlight - added screen for bangle2 and a small README \ No newline at end of file + added screen for bangle2 and a small README +0.05: Minor code improvements +0.06: Minor code improvements diff --git a/apps/devstopwatch/app.js b/apps/devstopwatch/app.js index d2a4b1117..747573c0c 100644 --- a/apps/devstopwatch/app.js +++ b/apps/devstopwatch/app.js @@ -12,7 +12,7 @@ const FONT = '6x8'; const CHRONO = '/* C H R O N O */'; -var reset = false; +//var reset = false; var currentLap = ''; var chronoInterval; @@ -43,7 +43,7 @@ Bangle.setUI("clockupdown", btn=>{ function resetChrono() { state.laps = [EMPTY_H, EMPTY_H, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP]; state.started = false; - reset = true; + //reset = true; state.currentLapIndex = 1; currentLap = ''; @@ -61,11 +61,11 @@ function chronometer() { state.whenStarted = rightNow; state.whenStartedTotal = rightNow; state.started = true; - reset = false; + //reset = false; } currentLap = calculateLap(state.whenStarted); - total = calculateLap(state.whenStartedTotal); + const total = calculateLap(state.whenStartedTotal); state.laps[0] = total; state.laps[1] = currentLap; @@ -123,7 +123,7 @@ function printChrono() { g.setColor(g.theme.fg); let suffix = ' '; if (state.currentLapIndex === i) { - let suffix = '*'; + let suffix = '*'; //TODO: Should `let` be removed here? if (process.env.HWVERSION==2) g.setColor("#0ee"); else g.setColor("#f70"); } diff --git a/apps/devstopwatch/metadata.json b/apps/devstopwatch/metadata.json index c4b6c7a67..f8e3fe106 100644 --- a/apps/devstopwatch/metadata.json +++ b/apps/devstopwatch/metadata.json @@ -2,7 +2,7 @@ "id": "devstopwatch", "name": "Dev Stopwatch", "shortName": "Dev Stopwatch", - "version": "0.04", + "version": "0.06", "description": "Stopwatch with 5 laps supported (cyclically replaced)", "icon": "app.png", "tags": "stopwatch,chrono,timer,chronometer", diff --git a/apps/diceroll/ChangeLog b/apps/diceroll/ChangeLog index 89dff4011..284e78368 100644 --- a/apps/diceroll/ChangeLog +++ b/apps/diceroll/ChangeLog @@ -1 +1,2 @@ -0.01: App created \ No newline at end of file +0.01: App created +0.02: Minor code improvements diff --git a/apps/diceroll/app.js b/apps/diceroll/app.js index d514ce92f..61a3d9917 100644 --- a/apps/diceroll/app.js +++ b/apps/diceroll/app.js @@ -105,4 +105,4 @@ function main() { Bangle.setLCDPower(1); } -var interval = setInterval(main, 300); \ No newline at end of file +setInterval(main, 300); \ No newline at end of file diff --git a/apps/diceroll/metadata.json b/apps/diceroll/metadata.json index 81a2f8bfd..256ad8a80 100644 --- a/apps/diceroll/metadata.json +++ b/apps/diceroll/metadata.json @@ -2,7 +2,7 @@ "name": "Dice-n-Roll", "shortName":"Dice-n-Roll", "icon": "app.png", - "version":"0.01", + "version": "0.02", "description": "A dice app with a few different dice.", "screenshots": [{"url":"diceroll_screenshot.png"}], "tags": "game", diff --git a/apps/diract/ChangeLog b/apps/diract/ChangeLog index 34fc73a76..272d01ab8 100644 --- a/apps/diract/ChangeLog +++ b/apps/diract/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Tweaked proximity identification settings +0.03: Minor code improvements diff --git a/apps/diract/diract.js b/apps/diract/diract.js index 69f0a88e4..d4effca89 100644 --- a/apps/diract/diract.js +++ b/apps/diract/diract.js @@ -74,6 +74,7 @@ let digestTime = new Uint8Array([ 0, 0, 0 ]); let numberOfDigestPages = 0; let sensorData = [ 0x82, 0x08, 0x3f ]; let cyclicCount = 0; +let encodedBattery = 0; let lastDigestTime = Math.round(getTime()); let lastResetTime = Math.round(getTime()); let isExciterPresent = false; @@ -517,7 +518,7 @@ function updateSensorData() { encodedBattery = encodeBatteryPercentage(); } - encodedAcceleration = encodeAcceleration(); + let encodedAcceleration = encodeAcceleration(); sensorData[0] = ((encodedAcceleration.x << 2) & 0xfc) | ((encodedAcceleration.y >> 4) & 0x3f); diff --git a/apps/diract/metadata.json b/apps/diract/metadata.json index af9406e91..2b6cd810e 100644 --- a/apps/diract/metadata.json +++ b/apps/diract/metadata.json @@ -2,7 +2,7 @@ "id": "diract", "name": "DirAct", "shortName": "DirAct", - "version": "0.02", + "version": "0.03", "description": "Proximity interaction detection.", "icon": "diract.png", "type": "app", diff --git a/apps/distortclk/ChangeLog b/apps/distortclk/ChangeLog index 4c7291526..11be002af 100644 --- a/apps/distortclk/ChangeLog +++ b/apps/distortclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New face! 0.02: Improved clock +0.03: Minor code improvements diff --git a/apps/distortclk/app.js b/apps/distortclk/app.js index a9fdd1ef2..715899fbb 100644 --- a/apps/distortclk/app.js +++ b/apps/distortclk/app.js @@ -26,7 +26,7 @@ function time() { var d = new Date(); var day = d.getDate(); var time = require("locale").time(d,1); - var date = require("locale").date(d); + //var date = require("locale").date(d); var mo = require("date_utils").month(d.getMonth()+1,0); g.setFontAlign(0,0); diff --git a/apps/distortclk/metadata.json b/apps/distortclk/metadata.json index 125dac590..cd1bf9d4d 100644 --- a/apps/distortclk/metadata.json +++ b/apps/distortclk/metadata.json @@ -2,7 +2,7 @@ "id": "distortclk", "name": "Distort Clock", "shortName":"Distort Clock", - "version": "0.02", + "version": "0.03", "description": "A clockface", "icon": "app.png", "type": "clock", diff --git a/apps/dotclock/ChangeLog b/apps/dotclock/ChangeLog index 563db87e7..cb2e8bd49 100644 --- a/apps/dotclock/ChangeLog +++ b/apps/dotclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Based on the Analog Clock app, minimal dot 0.02: Remove hardcoded hour buzz (you can install widchime if you miss it) 0.03: Use setUI, adjust for themes and different size screens +0.04: Minor code improvements diff --git a/apps/dotclock/clock-dot.js b/apps/dotclock/clock-dot.js index 66255d1b4..0127cd488 100644 --- a/apps/dotclock/clock-dot.js +++ b/apps/dotclock/clock-dot.js @@ -1,6 +1,5 @@ const big = g.getWidth()>200; const locale = require('locale'); -const p = Math.PI / 2; const pRad = Math.PI / 180; let timer = null; let currentDate = new Date(); diff --git a/apps/dotclock/metadata.json b/apps/dotclock/metadata.json index 396e63917..e8d7415fd 100644 --- a/apps/dotclock/metadata.json +++ b/apps/dotclock/metadata.json @@ -1,7 +1,7 @@ { "id": "dotclock", "name": "Dot Clock", - "version": "0.03", + "version": "0.04", "description": "A Minimal Dot Analog Clock", "icon": "clock-dot.png", "type": "clock", diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog index 77d82eff9..dc73a3c23 100644 --- a/apps/doztime/ChangeLog +++ b/apps/doztime/ChangeLog @@ -5,3 +5,5 @@ 0.05: extraneous comments and code removed display improved now supports Adjust Clock widget, if installed +0.06: Minor code improvements +0.07: Bangle2: Shift the position of one line on the screen diff --git a/apps/doztime/app-bangle1.js b/apps/doztime/app-bangle1.js index 38c5acbac..a176ef270 100644 --- a/apps/doztime/app-bangle1.js +++ b/apps/doztime/app-bangle1.js @@ -164,7 +164,7 @@ function drawTime() x = 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); let msg = "00000"+Math.floor(x).toString(12); - let time = msg.substr(-5,3)+"."+msg.substr(-2); + let time = msg.substr(-5,3)+"."+msg.substr(-2); //TODO: should `time` and `wait` have been defined outside the if block? let wait = 347*(1-(x%1)); timeDef = time6; } else { @@ -210,8 +210,8 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); // Functions for weather mode - TODO -function drawWeather() {} -function modeWeather() {} +//function drawWeather() {} +//function modeWeather() {} // Start time on twist Bangle.on('twist', function() { @@ -223,9 +223,8 @@ function fixTime() { Bangle.on("GPS",function cb(g) { Bangle.setGPSPower(0,"time"); Bangle.removeListener("GPS",cb); - if (!g.time || (g.time.getFullYear()<2000) || - (g.time.getFullYear()>2200)) { - } else { + if (g.time && (g.time.getFullYear()>=2000) && + (g.time.getFullYear()<=2200)) { // We have a GPS time. Set time setTime(g.time.getTime()/1000); } diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js index 8a315118f..9d1bb26c8 100644 --- a/apps/doztime/app-bangle2.js +++ b/apps/doztime/app-bangle2.js @@ -16,11 +16,12 @@ const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; const timeColour = "#ffffff"; const dateColours = ["#ff0000","#ff8000","#ffff00","#00ff00","#0080ff","#ff00ff","#ffffff"]; const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line -const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line +const calen7 = {"size":26,"pt0":[42-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30 const baseYear = 11584; const baseDate = Date(2020,11,21); // month values run from 0 to 11 +let calenDef = calen10; let accum = new Date(baseDate.getTime()); let sequence = []; let timeActiveUntil; @@ -125,7 +126,7 @@ function formatDate(res,dateFormat){ } function writeDozTime(text,def){ - let pts = def.pts; + //let pts = def.pts; let x=def.pt0[0]; let y=def.pt0[1]; g_t.clear(); @@ -139,9 +140,9 @@ function writeDozTime(text,def){ } } function writeDozDate(text,def,colour){ - + dateColour = colour; - let pts = def.pts; + //let pts = def.pts; let x=def.pt0[0]; let y=def.pt0[1]; g_d.clear(); @@ -169,20 +170,22 @@ function drawTime() let date = ""; let timeDef; let x = 0; + let time; + let wait; dt.setDate(dt.getDate()); if(addTimeDigit){ x = 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); let msg = "00000"+Math.floor(x).toString(12); - let time = msg.substr(-5,3)+"."+msg.substr(-2); - let wait = 347*(1-(x%1)); + time = msg.substr(-5,3)+"."+msg.substr(-2); //TODO: should `time` and `wait` have been defined outside the if block? + wait = 347*(1-(x%1)); timeDef = time6; } else { x = 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds(); let msg = "0000"+Math.floor(x).toString(12); - let time = msg.substr(-4,3)+"."+msg.substr(-1); - let wait = 4167*(1-(x%1)); + time = msg.substr(-4,3)+"."+msg.substr(-1); + wait = 4167*(1-(x%1)); timeDef = time5; } if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day diff --git a/apps/doztime/metadata.json b/apps/doztime/metadata.json index a05bf1470..407b474ca 100644 --- a/apps/doztime/metadata.json +++ b/apps/doztime/metadata.json @@ -2,7 +2,7 @@ "id": "doztime", "name": "Dozenal Digital Time", "shortName": "Dozenal Digital", - "version": "0.05", + "version": "0.07", "description": "A dozenal Holocene calendar and dozenal diurnal digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index d147a623b..77cc63c98 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -7,3 +7,5 @@ 0.07: Settings for display colors 0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting edge 2v18 ones), allowing compatability with the Back Swipe app. +0.09: Fix colors settings, where color was stored as string instead of the expected int. +0.10: Fix touch region for letters diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 78ef11bd4..2e40f3a77 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -107,7 +107,7 @@ exports.input = function(options) { "ram"; // ABCDEFGHIJKLMNOPQRSTUVWXYZ // Choose character by draging along red rectangle at bottom of screen - if (event.y >= ( (R.y+R.h) - 12 )) { + if (event.y >= ( (R.y+R.h) - 26 )) { // Translate x-position to character if (event.x < ABCPADDING) { abcHL = 0; } else if (event.x >= 176-ABCPADDING) { abcHL = 25; } @@ -139,7 +139,7 @@ exports.input = function(options) { // 12345678901234567890 // Choose number or puctuation by draging on green rectangle - else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) { + else if ((event.y < ( (R.y+R.h) - 26 )) && (event.y > ( (R.y+R.h) - 52 ))) { // Translate x-position to character if (event.x < NUMPADDING) { numHL = 0; } else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; } diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index 5c52d9389..c4596d7bd 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -1,6 +1,6 @@ { "id": "dragboard", "name": "Dragboard", - "version":"0.08", + "version":"0.10", "description": "A library for text input via swiping keyboard", "icon": "app.png", "type":"textinput", diff --git a/apps/dragboard/settings.js b/apps/dragboard/settings.js index a53914869..59a13c443 100644 --- a/apps/dragboard/settings.js +++ b/apps/dragboard/settings.js @@ -21,7 +21,7 @@ value: settings[key] == color, onchange: () => { if (color >= 0) { - settings[key] = color; + settings[key] = parseInt(color); } else { delete settings[key]; } diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog index bca1ca7c4..3f36dc4a6 100644 --- a/apps/draguboard/ChangeLog +++ b/apps/draguboard/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App based on dragboard, but with a U shaped drag area 0.02: Catch and discard swipe events on fw2v19 and up (as well as some cutting edge 2v18 ones), allowing compatability with the Back Swipe app. +0.03: Fix "Uncaught Error: Unhandled promise rejection: ReferenceError: "dragHandlerDB" is not defined" diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js index 6c63668a9..57093de3f 100644 --- a/apps/draguboard/lib.js +++ b/apps/draguboard/lib.js @@ -148,7 +148,7 @@ exports.input = function(options) { g.clearRect(Bangle.appRect); resolve(text); }, - drag: dragHandlerDB + drag: dragHandlerUB }); Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json index 620f39f71..2f395f8a8 100644 --- a/apps/draguboard/metadata.json +++ b/apps/draguboard/metadata.json @@ -1,6 +1,6 @@ { "id": "draguboard", "name": "DragUboard", - "version":"0.02", + "version":"0.03", "description": "A library for text input via swiping U-shaped keyboard.", "icon": "app.png", "type":"textinput", diff --git a/apps/draguboard/settings.js b/apps/draguboard/settings.js index c94ebee70..ff4ede637 100644 --- a/apps/draguboard/settings.js +++ b/apps/draguboard/settings.js @@ -21,7 +21,7 @@ value: settings[key] == color, onchange: () => { if (color >= 0) { - settings[key] = color; + settings[key] = parseInt(color); } else { delete settings[key]; } diff --git a/apps/drained/ChangeLog b/apps/drained/ChangeLog index c7fd27981..3767ad71e 100644 --- a/apps/drained/ChangeLog +++ b/apps/drained/ChangeLog @@ -2,3 +2,6 @@ 0.02: Allow boot exceptions, e.g. to load DST 0.03: Permit exceptions to load in low-power mode, e.g. daylight saving time. Also avoid polluting global scope. +0.04: Enhance menu: enable bluetooth, visit settings & visit recovery +0.05: Enhance menu: permit toggling bluetooth +0.06: Display clock in green when charging, with "charging" text diff --git a/apps/drained/app.js b/apps/drained/app.js index e27fcb1d1..deafe7d68 100644 --- a/apps/drained/app.js +++ b/apps/drained/app.js @@ -37,14 +37,19 @@ var draw = function () { require("locale").dow(date, 0).toUpperCase(); var x2 = x + 6; var y2 = y + 66; + var charging = Bangle.isCharging(); g.reset() .clearRect(Bangle.appRect) .setFont("Vector", 55) .setFontAlign(0, 0) + .setColor(charging ? "#0f0" : g.theme.fg) .drawString(timeStr, x, y) .setFont("Vector", 24) - .drawString(dateStr, x2, y2) - .drawString("".concat(E.getBattery(), "%"), x2, y2 + 48); + .drawString(dateStr, x2, y2); + if (charging) + g.drawString("charging: ".concat(E.getBattery(), "%"), x2, y2 + 48); + else + g.drawString("".concat(E.getBattery(), "%"), x2, y2 + 48); if (nextDraw) clearTimeout(nextDraw); nextDraw = setTimeout(function () { @@ -53,6 +58,21 @@ var draw = function () { }, 60000 - (date.getTime() % 60000)); }; var reload = function () { + var showMenu = function () { + var menu = { + "Restore to full power": drainedRestore, + }; + if (NRF.getSecurityStatus().advertising) + menu["Disable BLE"] = function () { NRF.sleep(); showMenu(); }; + else + menu["Enable BLE"] = function () { NRF.wake(); showMenu(); }; + menu["Settings"] = function () { return load("setting.app.js"); }; + menu["Recovery"] = function () { return Bangle.showRecoveryMenu(); }; + menu["Exit menu"] = reload; + if (nextDraw) + clearTimeout(nextDraw); + E.showMenu(menu); + }; Bangle.setUI({ mode: "custom", remove: function () { @@ -60,16 +80,7 @@ var reload = function () { clearTimeout(nextDraw); nextDraw = undefined; }, - btn: function () { - E.showPrompt("Restore watch to full power?").then(function (v) { - if (v) { - drainedRestore(); - } - else { - reload(); - } - }); - } + btn: showMenu }); Bangle.CLOCK = 1; g.clear(); @@ -90,8 +101,10 @@ function drainedRestore() { load(); } var checkCharge = function () { - if (E.getBattery() < restore) + if (E.getBattery() < restore) { + draw(); return; + } drainedRestore(); }; if (Bangle.isCharging()) diff --git a/apps/drained/app.ts b/apps/drained/app.ts index f4d33bc44..a779a8660 100644 --- a/apps/drained/app.ts +++ b/apps/drained/app.ts @@ -1,7 +1,7 @@ const app = "drained"; // from boot.js -declare var drainedInterval: IntervalId | undefined; +declare let drainedInterval: IntervalId | undefined; if(typeof drainedInterval !== "undefined") drainedInterval = clearInterval(drainedInterval) as undefined; @@ -54,15 +54,21 @@ const draw = () => { require("locale").dow(date, 0).toUpperCase(); const x2 = x + 6; const y2 = y + 66; + const charging = Bangle.isCharging(); g.reset() .clearRect(Bangle.appRect) .setFont("Vector", 55) .setFontAlign(0, 0) + .setColor(charging ? "#0f0" : g.theme.fg) .drawString(timeStr, x, y) .setFont("Vector", 24) - .drawString(dateStr, x2, y2) - .drawString(`${E.getBattery()}%`, x2, y2 + 48); + .drawString(dateStr, x2, y2); + + if(charging) + g.drawString(`charging: ${E.getBattery()}%`, x2, y2 + 48); + else + g.drawString(`${E.getBattery()}%`, x2, y2 + 48); if(nextDraw) clearTimeout(nextDraw); nextDraw = setTimeout(() => { @@ -72,21 +78,31 @@ const draw = () => { }; const reload = () => { + const showMenu = () => { + const menu: { [k: string]: () => void } = { + "Restore to full power": drainedRestore, + }; + + if (NRF.getSecurityStatus().advertising) + menu["Disable BLE"] = () => { NRF.sleep(); showMenu(); }; + else + menu["Enable BLE"] = () => { NRF.wake(); showMenu(); }; + + menu["Settings"] = () => load("setting.app.js"); + menu["Recovery"] = () => Bangle.showRecoveryMenu(); + menu["Exit menu"] = reload; + + if(nextDraw) clearTimeout(nextDraw); + E.showMenu(menu); + }; + Bangle.setUI({ mode: "custom", remove: () => { if (nextDraw) clearTimeout(nextDraw); nextDraw = undefined; }, - btn: () => { - E.showPrompt("Restore watch to full power?").then(v => { - if(v){ - drainedRestore(); - }else{ - reload(); - } - }) - } + btn: showMenu }); Bangle.CLOCK=1; @@ -115,7 +131,10 @@ function drainedRestore() { // "public", to allow users to call } const checkCharge = () => { - if(E.getBattery() < restore) return; + if(E.getBattery() < restore) { + draw(); + return; + } drainedRestore(); }; diff --git a/apps/drained/metadata.json b/apps/drained/metadata.json index 6dfdac78d..a5389a91b 100644 --- a/apps/drained/metadata.json +++ b/apps/drained/metadata.json @@ -1,12 +1,10 @@ { "id": "drained", "name": "Drained", - "version": "0.03", + "version": "0.06", "description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals", "readme": "README.md", "icon": "icon.png", - "type": "clock", - "tags": "clock", "supports": ["BANGLEJS2"], "allow_emulator": true, "storage": [ diff --git a/apps/drinkcounter/ChangeLog b/apps/drinkcounter/ChangeLog index d8d174c4c..0541d11de 100644 --- a/apps/drinkcounter/ChangeLog +++ b/apps/drinkcounter/ChangeLog @@ -1,4 +1,5 @@ 0.10: Initial release - still work in progress 0.15: Added settings and calculations 0.20: Added status saving -0.25: Adopted for Bangle.js 1 - kind of \ No newline at end of file +0.25: Adopted for Bangle.js 1 - kind of +0.26: Minor code improvements diff --git a/apps/drinkcounter/app.js b/apps/drinkcounter/app.js index 323d9fb41..b231930d7 100644 --- a/apps/drinkcounter/app.js +++ b/apps/drinkcounter/app.js @@ -22,7 +22,7 @@ const maxDrinks = 2; // 3 drinks var firstDrinkTime = null; var firstDrinkTimeTime = null; -var confBeerSize; +//var confBeerSize; var confSex; var confWeight; var confWeightUnit; @@ -97,7 +97,7 @@ function loadMySettings() { function def (value, def) {return value !== undefined ? value : def;} var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - confBeerSize = def(settings.beerSize, "0.3L"); + //confBeerSize = def(settings.beerSize, "0.3L"); confSex = def(settings.sex, "male"); confWeight = def(settings.weight, 80); confWeightUnit = def(settings.weightUnit, "Kilo"); diff --git a/apps/drinkcounter/metadata.json b/apps/drinkcounter/metadata.json index 2b8d7fe71..315a5845b 100644 --- a/apps/drinkcounter/metadata.json +++ b/apps/drinkcounter/metadata.json @@ -2,7 +2,7 @@ "id": "drinkcounter", "name": "Drink Counter", "shortName": "Drink Counter", - "version": "0.25", + "version": "0.26", "description": "Counts drinks you had for science. Calculates blood alcohol content (BAC)", "allow_emulator":true, "icon": "drinkcounter.png", diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 6c096f45b..5cac5770e 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -29,3 +29,4 @@ immediately follows the correct theme. when moving pages. Add caching for faster startups. 0.23: Bangle 1: Fix issue with missing icons, added touch screen interactions 0.24: Add buzz-on-interaction setting +0.25: Minor code improvements diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index a3ddd2538..2108910fc 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -42,7 +42,7 @@ let Npages = Math.ceil(Napps/4); let maxPage = Npages-1; let selected = -1; - let oldselected = -1; + //let oldselected = -1; let page = 0; const XOFF = 24; const YOFF = 30; @@ -104,7 +104,7 @@ let swipeListenerDt = function(dirLeftRight, dirUpDown){ updateTimeoutToClock(); selected = -1; - oldselected=-1; + //oldselected=-1; if(settings.swipeExit && dirLeftRight==1) Bangle.showClock(); if (dirUpDown==-1||dirLeftRight==-1){ ++page; if (page>maxPage) page=0; diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 5e25b61fb..bac0ed369 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.24", + "version": "0.25", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/dutchclock/ChangeLog b/apps/dutchclock/ChangeLog new file mode 100644 index 000000000..8efcb9edb --- /dev/null +++ b/apps/dutchclock/ChangeLog @@ -0,0 +1 @@ +0.20: First release \ No newline at end of file diff --git a/apps/dutchclock/README.md b/apps/dutchclock/README.md new file mode 100644 index 000000000..787bcce1b --- /dev/null +++ b/apps/dutchclock/README.md @@ -0,0 +1,22 @@ +# Dutch Clock +This clock shows the time, in words, the way a Dutch person might respond when asked what time it is. Useful when learning Dutch and/or pretending to know Dutch. + +Dedicated to my wife, who will sometimes insist I tell her exactly what time it says on the watch and not just an approximation. + +## Options +- Three modes: + - exact time ("zeven voor half zes / twee voor tien") + - approximate time, rounded to the nearest 5-minute mark ("bijna vijf voor half zes / tegen tienen") (the default) + - hybrid mode, rounded when close to the quarter marks and exact otherwise ("zeven voor half zes / tegen tienen") +- Option to turn top widgets on/off (on by default) +- Option to show digital time at the bottom (off by default) +- Option to show the date at the bottom (on by default) + +The app respects top and bottom widgets, but it gets a bit crowded when you add the time/date and you also have bottom widgets turned on. + +When you turn widgets off, you can still see the top widgets by swiping down from the top. + +## Screenshots +![](screenshotbangle1-2.png) +![](screenshotbangle2.png) +![](screenshotbangle1.png) \ No newline at end of file diff --git a/apps/dutchclock/app-icon.js b/apps/dutchclock/app-icon.js new file mode 100644 index 000000000..7d6e655e8 --- /dev/null +++ b/apps/dutchclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AE0/Ao/4sccAoX79NtAofttIFD8dsAof3t1/GZ397oGE/YLE6IFDloFE1vbAoeNAondAon/z4FE356U/nNxhZC/drlpLDscNAoX4ue9C4f3L4oAKt4FEQ4qxE/0skIGDtg7DAoNtAocsAogAX94POA")) \ No newline at end of file diff --git a/apps/dutchclock/app.js b/apps/dutchclock/app.js new file mode 100644 index 000000000..588692a2b --- /dev/null +++ b/apps/dutchclock/app.js @@ -0,0 +1,260 @@ +// Load libraries +const storage = require("Storage"); +const locale = require('locale'); +const widget_utils = require('widget_utils'); + +// Define constants +const DATETIME_SPACING_HEIGHT = 5; +const TIME_HEIGHT = 8; +const DATE_HEIGHT = 8; +const BOTTOM_SPACING = 2; + +const MINS_IN_HOUR = 60; +const MINS_IN_DAY = 24 * MINS_IN_HOUR; + +const VARIANT_EXACT = 'exact'; +const VARIANT_APPROXIMATE = 'approximate'; +const VARIANT_HYBRID = 'hybrid'; + +const DEFAULTS_FILE = "dutchclock.default.json"; +const SETTINGS_FILE = "dutchclock.json"; + +// Load settings +const settings = Object.assign( + storage.readJSON(DEFAULTS_FILE, true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} +); + +// Define global variables +const textBox = {}; +let date, mins; + +// Define functions +function initialize() { + // Reset the state of the graphics library + g.clear(true); + + // Tell Bangle this is a clock + Bangle.setUI("clock"); + + // Load widgets + Bangle.loadWidgets(); + + // Show widgets, or not + if (settings.showWidgets) { + Bangle.drawWidgets(); + } else { + widget_utils.swipeOn(); + } + + const dateTimeHeight = (settings.showDate || settings.showTime ? DATETIME_SPACING_HEIGHT : 0) + + (settings.showDate ? DATE_HEIGHT : 0) + + (settings.showTime ? TIME_HEIGHT : 0); + + Object.assign(textBox, { + x: Bangle.appRect.x + Bangle.appRect.w / 2, + y: Bangle.appRect.y + (Bangle.appRect.h - dateTimeHeight) / 2, + w: Bangle.appRect.w - 2, + h: Bangle.appRect.h - dateTimeHeight + }); + + // draw immediately at first + tick(); + + // now check every second + let secondInterval = setInterval(tick, 1000); + + // Stop updates when LCD is off, restart when on + Bangle.on('lcdPower',on=>{ + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + if (on) { + secondInterval = setInterval(tick, 1000); + draw(); // draw immediately + } + }); +} + +function tick() { + date = new Date(); + const m = (date.getHours() * MINS_IN_HOUR + date.getMinutes()) % MINS_IN_DAY; + + if (m !== mins) { + mins = m; + draw(); + } +} + +function draw() { + // work out how to display the current time + const timeLines = getTimeLines(mins); + const bottomLines = getBottomLines(); + + g.reset().clearRect(Bangle.appRect); + + // draw the current time (4x size 7 segment) + setFont(timeLines); + + g.setFontAlign(0,0); // align center top + g.drawString(timeLines.join("\n"), textBox.x, textBox.y, false); + + if (bottomLines.length) { + // draw the time and/or date, in a normal font + g.setFont("6x8"); + g.setFontAlign(0,1); // align center bottom + // pad the date - this clears the background if the date were to change length + g.drawString(bottomLines.join('\n'), Bangle.appRect.w / 2, Bangle.appRect.y2 - BOTTOM_SPACING, false); + } +} + +function setFont(timeLines) { + const size = textBox.h / timeLines.length; + + g.setFont("Vector", size); + + let width = g.stringWidth(timeLines.join('\n')); + + if (width > textBox.w) { + g.setFont("Vector", Math.floor(size * (textBox.w / width))); + } +} + +function getBottomLines() { + const lines = []; + + if (settings.showTime) { + lines.push(locale.time(date, 1)); + } + + if (settings.showDate) { + lines.push(locale.date(date)); + } + + return lines; + } + +function getTimeLines(m) { + switch (settings.variant) { + case VARIANT_EXACT: + return getExactTimeLines(m); + case VARIANT_APPROXIMATE: + return getApproximateTimeLines(m); + case VARIANT_HYBRID: + return distanceFromNearest(15)(m) < 3 + ? getApproximateTimeLines(m) + : getExactTimeLines(m); + default: + console.warn(`Error in settings: unknown variant "${settings.variant}"`); + return getExactTimeLines(m); + } +} + +function getExactTimeLines(m) { + if (m === 0) { + return ['middernacht']; + } + + const hour = getHour(m); + const minutes = getMinutes(hour.offset); + + const lines = minutes.concat(hour.lines); + if (lines.length === 1) { + lines.push('uur'); + } + + return lines; +} + +function getApproximateTimeLines(m) { + const roundMinutes = getRoundMinutes(m); + + const lines = getExactTimeLines(roundMinutes.minutes); + + return addApproximateDescription(lines, roundMinutes.offset); +} + +function getHour(minutes) { + const hours = ['twaalf', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf']; + + const h = Math.floor(minutes / MINS_IN_HOUR), m = minutes % MINS_IN_HOUR; + + if (m <= 15) { + return {lines: [hours[h % 12]], offset: m}; + } + + if (m > 15 && m < 45) { + return { + lines: ['half', hours[(h + 1) % 12]], + offset: m - (MINS_IN_HOUR / 2) + }; + } + + return {lines: [hours[(h + 1) % 12]], offset: m - MINS_IN_HOUR}; +} + +function getMinutes(m) { + const minutes = ['', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf', 'twaalf', 'dertien', 'veertien', 'kwart']; + + if (m === 0) { + return []; + } + + return [minutes[Math.abs(m)], m > 0 ? 'over' : 'voor']; +} + +function getRoundMinutes(m) { + const nearest = roundTo(5)(m); + + return { + minutes: nearest % MINS_IN_DAY, + offset: m - nearest + }; +} + +function addApproximateDescription(lines, offset) { + if (offset === 0) { + return lines; + } + + if (lines.length === 1 || lines[1] === 'uur') { + const singular = lines[0]; + const plural = getPlural(singular); + return { + '-2': ['tegen', plural], + '-1': ['iets voor', singular], + '1': ['iets na', plural], + '2': ['even na', plural] + }[`${offset}`]; + } + + return { + '-2': ['bijna'].concat(lines), + '-1': ['rond'].concat(lines), + '1': ['iets na'].concat(lines), + '2': lines.concat(['geweest']) + }[`${offset}`]; +} + +function getPlural(h) { + return { + middernacht: 'middernacht', + een: 'enen', + twee: 'tweeën', + drie: 'drieën', + vijf: 'vijven', + zes: 'zessen', + elf: 'elven', + twaalf: 'twaalven' + }[h] || `${h}en`; +} + +function distanceFromNearest(x) { + return n => Math.abs(n - roundTo(x)(n)); +} + +function roundTo(x) { + return n => Math.round(n / x) * x; +} + +// Let's go +initialize(); \ No newline at end of file diff --git a/apps/dutchclock/app.png b/apps/dutchclock/app.png new file mode 100644 index 000000000..94d35b0c5 Binary files /dev/null and b/apps/dutchclock/app.png differ diff --git a/apps/dutchclock/default.json b/apps/dutchclock/default.json new file mode 100644 index 000000000..cfe5d34a4 --- /dev/null +++ b/apps/dutchclock/default.json @@ -0,0 +1,6 @@ +{ + "variant": "approximate", + "showWidgets": true, + "showTime": false, + "showDate": true +} \ No newline at end of file diff --git a/apps/dutchclock/metadata.json b/apps/dutchclock/metadata.json new file mode 100644 index 000000000..d336023f8 --- /dev/null +++ b/apps/dutchclock/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "dutchclock", + "name": "Dutch Clock", + "shortName":"Dutch Clock", + "icon": "app.png", + "version":"0.20", + "description": "A clock that displays the time the way a Dutch person would respond when asked what time it is.", + "type": "clock", + "tags": "clock,dutch,text", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "screenshots": [ + {"url":"screenshotbangle1-2.png"}, + {"url":"screenshotbangle2.png"}, + {"url":"screenshotbangle1.png"} + ], + "storage": [ + {"name":"dutchclock.app.js","url":"app.js"}, + {"name":"dutchclock.settings.js","url":"settings.js"}, + {"name":"dutchclock.default.json","url":"default.json"}, + {"name":"dutchclock.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"dutchclock.json"} + ], + "readme":"README.md" +} + \ No newline at end of file diff --git a/apps/dutchclock/screenshotbangle1-2.png b/apps/dutchclock/screenshotbangle1-2.png new file mode 100644 index 000000000..08bf31939 Binary files /dev/null and b/apps/dutchclock/screenshotbangle1-2.png differ diff --git a/apps/dutchclock/screenshotbangle1.png b/apps/dutchclock/screenshotbangle1.png new file mode 100644 index 000000000..49ba895f4 Binary files /dev/null and b/apps/dutchclock/screenshotbangle1.png differ diff --git a/apps/dutchclock/screenshotbangle2.png b/apps/dutchclock/screenshotbangle2.png new file mode 100644 index 000000000..48b3fd501 Binary files /dev/null and b/apps/dutchclock/screenshotbangle2.png differ diff --git a/apps/dutchclock/settings.js b/apps/dutchclock/settings.js new file mode 100644 index 000000000..146df5395 --- /dev/null +++ b/apps/dutchclock/settings.js @@ -0,0 +1,73 @@ +(function(back) { + const storage = require("Storage"); + + const VARIANT_EXACT = 'exact'; + const VARIANT_APPROXIMATE = 'approximate'; + const VARIANT_HYBRID = 'hybrid'; + + const DEFAULTS_FILE = "dutchclock.default.json"; + const SETTINGS_FILE = "dutchclock.json"; + + // Load settings + const settings = Object.assign( + storage.readJSON(DEFAULTS_FILE, true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function writeSettings() { + require('Storage').writeJSON(SETTINGS_FILE, settings); + } + + function writeSetting(setting, value) { + settings[setting] = value; + writeSettings(); + } + + function writeOption(setting, value) { + writeSetting(setting, value); + showMainMenu(); + } + + function getOption(label, setting, value) { + return { + title: label, + value: settings[setting] === value, + onchange: () => { + writeOption(setting, value); + } + }; + } + + // Show the menu + function showMainMenu() { + const mainMenu = [ + getOption('Exact', 'variant', VARIANT_EXACT), + getOption('Approximate', 'variant', VARIANT_APPROXIMATE), + getOption('Hybrid', 'variant', VARIANT_HYBRID), + { + title: 'Show widgets?', + value: settings.showWidgets, + onchange: v => writeSetting('showWidgets', v) + }, + { + title: 'Show time?', + value: settings.showTime, + onchange: v => writeSetting('showTime', v) + }, + { + title: 'Show date?', + value: settings.showDate, + onchange: v => writeSetting('showDate', v) + } + ]; + + mainMenu[""] = { + title : "Dutch Clock", + back: back + }; + + E.showMenu(mainMenu); + } + + showMainMenu(); + }) \ No newline at end of file diff --git a/apps/dwm-clock/ChangeLog b/apps/dwm-clock/ChangeLog new file mode 100644 index 000000000..7727f3cc4 --- /dev/null +++ b/apps/dwm-clock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Minor code improvements diff --git a/apps/dwm-clock/README.md b/apps/dwm-clock/README.md new file mode 100644 index 000000000..619cdccd0 --- /dev/null +++ b/apps/dwm-clock/README.md @@ -0,0 +1,7 @@ +A clock with a daylight world map + +The function for the daylight graph is a crude approximation for an equirectangular projection of a circle on a sphere. + +You can change the longitudinal map offset by swiping the map sideways. For saving the changes to the file dwm-clock.json press the top left quarter of the screen. To discard changes press the top right quarter. + +If you are interested in changing the vector font to another one, please do. diff --git a/apps/dwm-clock/app-icon.js b/apps/dwm-clock/app-icon.js new file mode 100644 index 000000000..d3e9182e2 --- /dev/null +++ b/apps/dwm-clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/AoPg/8fAYM5sGt4FakFjsFKgHGv9rsEpoHOkGl0ExsHvj0AkQAugHyAYMv/4ECkX/BwfwAogACgQDCl8PBQnwBQZ3BC4opDAAg0BGIP/+CVCAoPykAiBiIABjOZBwIMB/8PUYQAJ4AYCiuZzLLQCwUYgEEgGZuAXQ5bEBqMAgeTChaHCiMc2cRmFTBQMJ0AXMl8Rj0QqALEyAwL+QXBGAIKFhJHOjcMglz4EMjew4IwJWILVBiMWPQNOstABgMBsxFJkAvCiNsmc+AgPqSIJIIe4KPEBAMG9FstQFCJA/y/4WBC4PNBo0IhQXICwUijy3FglggtKjIXMBYsKwAaBjPwRIICBIgYXJgGFgHBiMvZ4QtCgQXLhXOSgOfQ4TTCAoR3CuAYGhgXBySiBCgIvB+AXEisA8AXEgIXBiUvkUvLwP/AoIXDAATtCAAPhC4QOBL4Mvh//SAYXHhwHCjIPC+Hwh8CkCTCC4cWFwoXEJAIUCPgPyC4/RBAeSLIZiCAYIvIgO0C45LCI5WLR4RIEAAMgIwLaBC4cbCwMIigXI/4lCbQIXDiLVCjdhMA7NEO4kRmEACogXF+AXDgQOEiEGCwrBFC5MeXoYXHl7uBDQJHFABKoFYQQXOYIzXFABUSC4rXFABTZFC6JIHC6EZDAwXPJI4PIilBAgdEAAOZz//UwIXCj3r3YABxGwcgVLwDqDgF5zOQgEPj3QBYgANhOZzMwCyQA/ABYA=")) diff --git a/apps/dwm-clock/app.js b/apps/dwm-clock/app.js new file mode 100644 index 000000000..6d9bd3767 --- /dev/null +++ b/apps/dwm-clock/app.js @@ -0,0 +1,224 @@ +// daylight world map clock +// equirectangular projected map and approximated daylight graph + +// load font for timezone, weekday and day in month +require("FontDennis8").add(Graphics); + +const W = g.getWidth(); +const H = g.getHeight(); + +const TZOFFSET = new Date().getTimezoneOffset(); + +const UTCSTRING = ((TZOFFSET > 0 ? "-" : "+") + + ("0" + Math.floor(Math.abs(TZOFFSET) / 60)).slice(-2)) + + (TZOFFSET % 60 ? Math.abs(TZOFFSET) % 60 : ""); + +function getMap() { + return { + width: 176, height: 88, bpp: 1, + transparent: 1, + buffer: require("heatshrink").decompress(atob("/4A/AA0Av+Ag4UQwBhDn//1//8///AUI3MAhAUBgIQBh4LC/kfCg34rmAngVD/1/CYICBA4IAF8EOwF/+AVCAAXj//AA4PjDQIVDgkQj/4gBtEx+EGgXwCoJ8Bv+8geQgIVE4P/553Egf/nwFCgUE4H8gBqB/0AhLxHggFE+E8gJoBDIIAI5wFE4F8h/4v5FBABA2BAAUf7n+VYXgoAVNn/Dv+fCoPACo8MEQPHHAUf4DuB//58FgCgsHeoWfMgUDConw4AVFh/wXIRDDwBWC8jfBFY3xaAa5DYYXkKw8D+YVDHAcXAwKuIgIUDSIIJCsYVKeAIVHj5fGNogVHgN/AwPyEgPhCokZCo40D8E0wcwTYhsECoY0D8H2hEACocBCoqnCKwQVB/nICokJ+4VL/RGBQQkdw4VESQTwCDgIVBNgkeEQaSEQQReC4QrEhwUECoUECooAFVwoABgF+CoY+DAYZAFAAOgv4VGoFgCpXwGIoABkEHDQUvCo9zD4YVE4EIgIUGCoNnZwYVCiEP8E8hYVH/kHII0Qj/wvkP94WH4IVGhE/MQMH54VH+IVGKYIJBgfnCo/98IVFcYP5/9HMYbdGn7FFv/4/9vCpH/4DmC4AVCD4P/n4VKUoXgCwQ2Cz42CCpX//BtCCoMeCpJTBZgcAgYFCjElCpA7BEIQVBZoeYp4sICoIQCIIJzC/+Mp+DCpJSC/kAj4KC5/f4GfK5AVIeYPgNpIVEIIf/6f/v6ZHPwYVG//7V5BtDCoMOEof+jYVH8AVFhgLD/EZCo6UBCokYBYa2BCp04G4oVJNAX+gF4XYqDHCoKqCCoIrDAoL9DCowfCB4N9CorMDCooPEfowVMB4IVPeAQABwIVPeAQABw4LEg/ANo/wTAQAI8E//YVS+F//IIGGg4AFCo7OHAAf+v/jCowqM//HAwvhCpuPOwwVNAAwrOAA3xCqhtOAH4AfW4wAN/0/A4sP//AgFygYVH/V/AwlwgE8gAACDYIAF9ArC+uACAUgCocAHIn8k/gj4FBCgYAGBoXwgEYDof+ChMAJ4PmAwcBDgIUKgANBJIkZ/0cCpYrBIAIADzkwChQ5B/tgBAh7FNpANMAGg=")) + }; +} + +const YOFFSET = H - getMap().height; + +// map offset in degree +// -180 to 180 / default: 0 +function getLongitudeOffset() { + return require("Storage").readJSON("dwm-clock.json", 1) || {"lon": 0}; +} + +function drawMap() { + g.setBgColor(0, 0, 0); + + // does not flip on it's own, but there is a draw function after that does + g.drawImages([{ + x: -lonOffset * W / 360, + y: YOFFSET, + image: getMap(), + scale: 1, + rotate: 0, + center: false, + repeat: true, + nobounds: false + }], { + x: 0, + y: YOFFSET, + width: getMap().width, + height: getMap().height + }); +} + +function drawDaylightMap() { + // number of xy points, < 40 looks very skewed around solstice + const STEPS = 40; + const YFACTOR = getMap().height / 2; + const YOFF = H / 2 + YFACTOR; + var graph = []; + + // progress of day, float 0 to 1 + var dayOffset = (now.getHours() + (now.getMinutes() + TZOFFSET) / 60) / 24; + + // sun position modifier + var sunPosMod; + + var solarNoon = require("suncalc").getTimes(now, 0, 0, 0).solarNoon; + + var altitude = require("suncalc").getPosition(solarNoon, 0, 0).altitude; + + // this is trial and error. no thought went into this + sunPosMod = Math.pow(altitude - 0.08, 8); + + // switch sign on equinox + // this is an approximation + if (require("suncalc").getPosition(solarNoon, 0, 0).azimuth < -1) { + sunPosMod = -sunPosMod; + } + + for (var x = 0; x < (STEPS + 1) / STEPS; x += 1 / STEPS) { + // this is an approximation instead of projecting a circle onto a sphere + // y = arctan(sin(x) * n) + var y = Math.atan(Math.sin(2 * Math.PI * x + dayOffset * 2 * Math.PI + // user defined map offset fixed offset + // v v + + 2 * Math.PI * lonOffset / 360 - Math.PI / 2) * sunPosMod) + * (2 / Math.PI); + // ^ + // factor keeps y <= 1 + + graph.push(x * W, y * YFACTOR + YOFF); + } + + // day area, yellow + g.setColor(0.8, 0.8, 0.3); + g.fillRect(0, YOFFSET, W, H); + + // night area, blue + g.setColor(0, 0, 0.5); + // switch on equinox + if (sunPosMod < 0) { + g.fillPoly([0, H - 1].concat(graph, W - 1, H - 1)); + } else { + g.fillPoly([0, YOFFSET].concat(graph, W, YOFFSET)); + } + + drawMap(); + + // day-night line, white + g.setColor(1, 1, 1); + g.drawPoly(graph, false); +} + +function drawClock() { + // clock area + g.clearRect(0, YOFFSET, W, 24); + + // clock text + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.setFont("Vector", 58); + // with the vector font this leaves 26px above the text + g.drawString(require("locale").time(now, 1), W / 2, 24 - 2); + + + // timezone text + g.setFontAlign(-1, 1); + g.setFont("6x8", 2); + g.drawString("UTC" + UTCSTRING, 3, YOFFSET); + + + // day text + g.setFontAlign(1, 1); + g.setFont("Dennis8", 2); + g.drawString(require("locale").dow(now, 1) + " " + now.getDate(), + W - 1, YOFFSET); +} + +function renderScreen() { + now = new Date(); + + drawClock(); + drawDaylightMap(); +} + +function renderAndQueue() { + /*timeoutID =*/ setTimeout(renderAndQueue, 60000 - (Date.now() % 60000)); + renderScreen(); +} + +g.reset().clearRect(Bangle.appRect); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.setBgColor(0, 0, 0); + +var now = new Date(); + +// map offsets +var defLonOffset = getLongitudeOffset().lon; +var lonOffset = defLonOffset; + +//var timeoutID; +var timeoutIDTouch; + +Bangle.on('drag', function(touch) { + + if (timeoutIDTouch) { + clearTimeout(timeoutIDTouch); + } + + // return after not touching for 5 seconds + timeoutIDTouch = setTimeout(renderAndQueue, 5 * 1000); + + // touch map + if (touch.y >= YOFFSET) { + lonOffset -= touch.dx * 360 / W; + + // wrap map offset + if (lonOffset < -180) { + lonOffset += 360; + } else if (lonOffset >= 180) { + lonOffset -= 360; + } + + // snap to 0° longitude + if (lonOffset > -5 && lonOffset < 5) { + lonOffset = 0; + } + + lonOffset = Math.round(lonOffset); + + // clock area + g.clearRect(0, YOFFSET, W, 24); + + // text + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.setFont("Dennis8", 2); + // could not get ° (degree sign) to render + g.drawString("select lon offset\n< tap: save\nreset: tap >\n" + + lonOffset + " degree", W / 2, 24); + + drawDaylightMap(); + + // touch clock, left side, save offset + } else if (touch.x < W / 2) { + if (defLonOffset != lonOffset) { + require("Storage").writeJSON("dwm-clock.json", {"lon": lonOffset}); + defLonOffset = lonOffset; + } + + renderScreen(); + + // touch clock, right side, reset offset + } else { + lonOffset = defLonOffset; + renderScreen(); + } +}); + +renderAndQueue(); diff --git a/apps/dwm-clock/app.png b/apps/dwm-clock/app.png new file mode 100644 index 000000000..cf9a16fbf Binary files /dev/null and b/apps/dwm-clock/app.png differ diff --git a/apps/dwm-clock/metadata.json b/apps/dwm-clock/metadata.json new file mode 100644 index 000000000..2a03c396c --- /dev/null +++ b/apps/dwm-clock/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "dwm-clock", + "name": "Daylight World Map Clock", + "shortName": "DWM Clock", + "version": "0.02", + "description": "A clock with a daylight world map", + "readme":"README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"dwm-clock.app.js","url":"app.js"}, + {"name":"dwm-clock.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"dwm-clock.json"}] +} diff --git a/apps/dwm-clock/screenshot.png b/apps/dwm-clock/screenshot.png new file mode 100644 index 000000000..a153c655d Binary files /dev/null and b/apps/dwm-clock/screenshot.png differ diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog index b96d7207d..72bb39ab1 100644 --- a/apps/edgeclk/ChangeLog +++ b/apps/edgeclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial release. 0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps. 0.03: Added option to display weather. +0.04: Added option to display live updates of step count. diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md index 90f6443fc..51747780f 100644 --- a/apps/edgeclk/README.md +++ b/apps/edgeclk/README.md @@ -20,12 +20,13 @@ The appearance is highly configurable. In the settings menu you can: - Set the daily step goal. - En- or disable the individual progress bars. - Set if your week should start with Monday or Sunday (for week progress bar). +- Toggle live step count updates.* -*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well. +*) Hiding seconds and leaving live steps off should further reduce power consumption as the draw interval is prolonged as well. The clock implements Fast Loading for faster switching to and fro. ## Contributors - [tinxx](https://github.com/tinxx) - [peerdavid](https://github.com/peerdavid) - \ No newline at end of file + diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index f9d5f803b..79310c3da 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -14,6 +14,7 @@ weekBar: true, mondayFirst: true, dayBar: true, + liveSteps: false, }, require('Storage').readJSON('edgeclk.settings.json', true) || {}); /* Runtime Variables @@ -279,6 +280,9 @@ drawLower(); }; + const onStep = function () { + drawSteps(); + } /* Lifecycle Functions ------------------------------------------------------------------------------*/ @@ -298,6 +302,9 @@ // Charging event signals when charging status changes: Bangle.on('charging', onCharging); + + // Continously update step count when they happen: + if (settings.redrawOnStep) Bangle.on('step', onStep); }; const deregisterEvents = function () { @@ -306,6 +313,7 @@ Bangle.removeListener('health', onHealth); Bangle.removeListener('lock', onLock); Bangle.removeListener('charging', onCharging); + if (settings.redrawOnStep) Bangle.removeListener('step', onStep); }; const startTimers = function () { diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json index 0d53cd008..ef97b314b 100644 --- a/apps/edgeclk/metadata.json +++ b/apps/edgeclk/metadata.json @@ -2,7 +2,7 @@ "id": "edgeclk", "name": "Edge Clock", "shortName": "Edge Clock", - "version": "0.03", + "version": "0.04", "description": "Crisp clock with perfect readability.", "readme": "README.md", "icon": "app.png", diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js index 6f38e774c..81a7acc5b 100644 --- a/apps/edgeclk/settings.js +++ b/apps/edgeclk/settings.js @@ -14,6 +14,7 @@ weekBar: true, mondayFirst: true, dayBar: true, + redrawOnStep: false, }; const saved_settings = storage.readJSON(SETTINGS_FILE, true); @@ -121,5 +122,12 @@ save(); }, }, + 'Live steps': { + value: settings.redrawOnStep, + onchange: () => { + settings.redrawOnStep = !settings.redrawOnStep; + save(); + }, + }, }); }) diff --git a/apps/edisonsball/ChangeLog b/apps/edisonsball/ChangeLog index b71b8bb0d..c871dbe41 100644 --- a/apps/edisonsball/ChangeLog +++ b/apps/edisonsball/ChangeLog @@ -1,2 +1,4 @@ 0.01: Initial version 0.02: Added BangleJS Two +0.03: Minor code improvements +0.04: Minor code improvements diff --git a/apps/edisonsball/app.js b/apps/edisonsball/app.js index 2aa317829..39b764dfe 100644 --- a/apps/edisonsball/app.js +++ b/apps/edisonsball/app.js @@ -104,10 +104,10 @@ function getStandardDeviation (array) { } function checkHR() { - var bpm = currentBPM, isCurrent = true; + var bpm = currentBPM; //isCurrent = true; if (bpm===undefined) { bpm = lastBPM; - isCurrent = false; + //isCurrent = false; } if (bpm===undefined || bpm < lower_limit_BPM || bpm > upper_limit_BPM) bpm = "--"; @@ -118,8 +118,8 @@ function checkHR() { if(HR_samples.length == 5){ g.clear(); - average_HR = average(HR_samples).toFixed(0); - stdev_HR = getStandardDeviation (HR_samples).toFixed(1); + let average_HR = average(HR_samples).toFixed(0); + let stdev_HR = getStandardDeviation (HR_samples).toFixed(1); if (ISBANGLEJS1) { g.drawString("HR: " + average_HR, 120,100); diff --git a/apps/edisonsball/metadata.json b/apps/edisonsball/metadata.json index dfeb4451e..8526c7926 100644 --- a/apps/edisonsball/metadata.json +++ b/apps/edisonsball/metadata.json @@ -2,7 +2,7 @@ "id": "edisonsball", "name": "Edison's Ball", "shortName": "Edison's Ball", - "version": "0.02", + "version": "0.04", "description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness", "icon": "app-icon.png", "tags": "sleep,hyponagogia,quick,nap", diff --git a/apps/elapsed_t/ChangeLog b/apps/elapsed_t/ChangeLog new file mode 100644 index 000000000..26fbf5ff0 --- /dev/null +++ b/apps/elapsed_t/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Handle AM/PM time in the "set target" menu. Add yesterday/today/tomorrow when showing target date to improve readability. +0.03: Add option to set clock as default, handle DST in day/month/year mode +0.04: Use new pickers from the more_pickers library, add settings to display seconds never/unlocked/always diff --git a/apps/elapsed_t/README.md b/apps/elapsed_t/README.md new file mode 100644 index 000000000..9e361be59 --- /dev/null +++ b/apps/elapsed_t/README.md @@ -0,0 +1,30 @@ +# Elapsed Time Clock +A clock that calculates the time difference between now (in blue/cyan) and any given target date (in red/orange). + +The results is show in years, months, days, hours, minutes, seconds. The seconds can be shown: +- always +- when the watch is unlocked +- never. + +The time difference is positive if the target date is in the past and negative if it is in the future. + +![Screenshot 1](screenshot1.png) +![Screenshot 2](screenshot2.png) +![Screenshot 3](screenshot3.png) +![Screenshot 4](screenshot4.png) + +# Settings +## Time and date formats: +- time can be shown in 24h or in AM/PM format +- date can be shown in DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD format + +## Display years and months +You can select if the difference is shown with years, months and days, or just days. + +# TODO +- add the option to set an alarm to the target date +- add an offset to said alarm (e.g. x hours/days... before/after) + +# Author + +paul-arg [github](https://github.com/paul-arg) \ No newline at end of file diff --git a/apps/elapsed_t/app-icon.js b/apps/elapsed_t/app-icon.js new file mode 100644 index 000000000..0e9a434fc --- /dev/null +++ b/apps/elapsed_t/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcA/4A/AH8kyVJARAQE/YRLn4RD/IRT5cs2QCEEbQgFAQYjIrMlAQwjR5JHIsv2pNkz3JsgjKl/yEAO/I5l/+REBz/7I5f/EYf/I5Vf//2rNlz//8gjJAgIjE/hHIy7xEAAQjIDoIAG+RHHCA///wjHCJIjHMoI1HEY+zCI6zJv4dCFIX9R5PPR4vsEZNJCILXC/77JyXLn4jD/b7KpMnI4fZBARHHpcsEYW2AQIjKARBHIDoICECJIjRpZKCAQYjbCMH/CJVLCAgA/AHYA==")) diff --git a/apps/elapsed_t/app.js b/apps/elapsed_t/app.js new file mode 100644 index 000000000..910ff85f3 --- /dev/null +++ b/apps/elapsed_t/app.js @@ -0,0 +1,537 @@ +const APP_NAME = "elapsed_t"; + +//const COLOUR_BLACK = 0x0; +//const COLOUR_DARK_GREY = 0x4208; // same as: g.setColor(0.25, 0.25, 0.25) +const COLOUR_GREY = 0x8410; // same as: g.setColor(0.5, 0.5, 0.5) +const COLOUR_LIGHT_GREY = 0xc618; // same as: g.setColor(0.75, 0.75, 0.75) +const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0) +const COLOUR_BLUE = 0x001f; // same as: g.setColor(0, 0, 1) +//const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +//const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1) +//const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0) +//const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5) +const COLOUR_CYAN = "#00FFFF"; +const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) + +const SCREEN_WIDTH = g.getWidth(); +const SCREEN_HEIGHT = g.getHeight(); +const BIG_FONT_SIZE = 38; +const SMALL_FONT_SIZE = 22; + +var arrowFont = atob("BwA4AcAOAHADgBwA4McfOf3e/+P+D+A+AOA="); // contains only the > character + +var now = new Date(); + +var settings = Object.assign({ + // default values + displaySeconds: 1, + displayMonthsYears: true, + dateFormat: 0, + time24: true +}, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + +function writeSettings() { + require('Storage').writeJSON(APP_NAME + ".settings.json", settings); +} + +if (typeof settings.displaySeconds === 'boolean') { + settings.displaySeconds = 1; + writeSettings(); +} + +var data = Object.assign({ + // default values + target: { + isSet: false, + Y: now.getFullYear(), + M: now.getMonth() + 1, // Month is zero-based, so add 1 + D: now.getDate(), + h: now.getHours(), + m: now.getMinutes(), + s: 0 + } +}, require('Storage').readJSON(APP_NAME + ".data.json", true) || {}); + +function writeData() { + require('Storage').writeJSON(APP_NAME + ".data.json", data); +} + +let inMenu = false; + +Bangle.on('touch', function (zone, e) { + if (!inMenu && e.y > 24) { + if (drawTimeout) clearTimeout(drawTimeout); + showMainMenu(); + inMenu = true; + } +}); + +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +function formatDateTime(date, dateFormat, time24, showSeconds) { + var formattedDateTime = { + date: "", + time: "" + }; + + var DD = pad2(date.getDate()); + var MM = pad2(date.getMonth() + 1); // Month is zero-based + var YYYY = date.getFullYear(); + var h = date.getHours(); + var hh = pad2(date.getHours()); + var mm = pad2(date.getMinutes()); + var ss = pad2(date.getSeconds()); + + switch (dateFormat) { + case 0: + formattedDateTime.date = `${DD}/${MM}/${YYYY}`; + break; + + case 1: + formattedDateTime.date = `${MM}/${DD}/${YYYY}`; + break; + + case 2: + formattedDateTime.date = `${YYYY}-${MM}-${DD}`; + break; + + default: + formattedDateTime.date = `${YYYY}-${MM}-${DD}`; + break; + } + + if (time24) { + formattedDateTime.time = `${hh}:${mm}${showSeconds ? `:${ss}` : ''}`; + } else { + var ampm = (h >= 12 ? 'PM' : 'AM'); + var h_ampm = h % 12; + h_ampm = (h_ampm == 0 ? 12 : h_ampm); + formattedDateTime.time = `${h_ampm}:${mm}${showSeconds ? `:${ss}` : ''} ${ampm}`; + } + + return formattedDateTime; +} + +function formatHourToAMPM(h) { + var ampm = (h >= 12 ? 'PM' : 'AM'); + var h_ampm = h % 12; + h_ampm = (h_ampm == 0 ? 12 : h_ampm); + return `${h_ampm}\n${ampm}`; +} + +function getDatePickerObject() { + switch (settings.dateFormat) { + case 0: + return { + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: data.target.D, + min_1: 1, max_1: 31, step_1: 1, wrap_1: true, + + value_2: data.target.M, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: data.target.Y, + min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.D = v_1; data.target.M = v_2; data.target.Y = v_3; setTarget(true); } + }; + + case 1: + return { + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: data.target.M, + min_1: 1, max_1: 12, step_1: 1, wrap_1: true, + + value_2: data.target.D, + min_2: 1, max_2: 31, step_2: 1, wrap_2: true, + + value_3: data.target.Y, + min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.M = v_1; data.target.D = v_2; data.target.Y = v_3; setTarget(true); } + }; + + case 2: + return { + back: showMainMenu, + title: "Date", + separator_1: "-", + separator_2: "-", + + value_1: data.target.Y, + min_1: 1900, max_1: 2100, step_1: 1, wrap_1: true, + + value_2: data.target.M, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: data.target.D, + min_3: 1, max_3: 31, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.Y = v_1; data.target.M = v_2; data.target.D = v_3; setTarget(true); } + }; + } +} + +function getTimePickerObject() { + var timePickerObject = { + back: showMainMenu, + title: "Time", + separator_1: ":", + separator_2: ":", + + value_1: data.target.h, + min_1: 0, max_1: 23, step_1: 1, wrap_1: true, + + value_2: data.target.m, + min_2: 0, max_2: 59, step_2: 1, wrap_2: true, + + value_3: data.target.s, + min_3: 0, max_3: 59, step_3: 1, wrap_3: true, + + format_2: function (v_2) { return (pad2(v_2)); }, + format_3: function (v_3) { return (pad2(v_3)); }, + onchange: function (v_1, v_2, v_3) { data.target.h = v_1; data.target.m = v_2; data.target.s = v_3; setTarget(true); }, + }; + + if (settings.time24) { + timePickerObject.format_1 = function (v_1) { return (pad2(v_1)); }; + } else { + timePickerObject.format_1 = function (v_1) { return (formatHourToAMPM(v_1)); }; + } + + return timePickerObject; +} + +function showMainMenu() { + E.showMenu({ + "": { + "title": "Set target", + back: function () { + E.showMenu(); + Bangle.setUI("clock"); + inMenu = false; + draw(); + } + }, + 'Date': { + value: formatDateTime(target, settings.dateFormat, settings.time24, true).date, + onchange: function () { require("more_pickers").triplePicker(getDatePickerObject()); } + }, + 'Time': { + value: formatDateTime(target, settings.dateFormat, settings.time24, true).time, + onchange: function () { require("more_pickers").triplePicker(getTimePickerObject()); } + }, + 'Reset': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(false); + draw(); + }, + 'Set clock as default': function () { + setClockAsDefault(); + E.showAlert("Elapsed Time was set as default").then(function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + draw(); + }); + } + }); +} + +function setClockAsDefault() { + let storage = require('Storage'); + let settings = storage.readJSON('setting.json', true) || { clock: null }; + settings.clock = "elapsed_t.app.js"; + storage.writeJSON('setting.json', settings); +} + +function setTarget(set) { + if (set) { + target = new Date( + data.target.Y, + data.target.M - 1, + data.target.D, + data.target.h, + data.target.m, + data.target.s + ); + data.target.isSet = true; + } else { + target = new Date(); + target.setSeconds(0); + Object.assign( + data, + { + target: { + isSet: false, + Y: target.getFullYear(), + M: target.getMonth() + 1, // Month is zero-based, so add 1 + D: target.getDate(), + h: target.getHours(), + m: target.getMinutes(), + s: 0 + } + } + ); + } + + writeData(); +} + +var target; +setTarget(data.target.isSet); + +var drawTimeout; +var temp_displaySeconds; +var queueMillis; + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + var delay = queueMillis - (Date.now() % queueMillis); + if (queueMillis == 60000 && signIsNegative()) { + delay += 1000; + } + + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw(); + }, delay); +} + +function updateQueueMillisAndDraw(displaySeconds) { + temp_displaySeconds = displaySeconds; + if (displaySeconds) { + queueMillis = 1000; + } else { + queueMillis = 60000; + } + draw(); +} + +Bangle.on('lock', function (on, reason) { + if (inMenu || settings.displaySeconds == 0 || settings.displaySeconds == 2) { // if already in a menu, or always/never show seconds, nothing to do + return; + } + + if (on) { // screen is locked + updateQueueMillisAndDraw(false); + } else { // screen is unlocked + updateQueueMillisAndDraw(true); + } +}); + +function signIsNegative() { + var now = new Date(); + return (now < target); +} + +function diffToTarget() { + var diff = { + sign: "+", + Y: "0", + M: "0", + D: "0", + hh: "00", + mm: "00", + ss: "00" + }; + + if (!data.target.isSet) { + return (diff); + } + + var now = new Date(); + diff.sign = now < target ? '-' : '+'; + + if (settings.displayMonthsYears) { + var start; + var end; + + if (now > target) { + start = new Date(target.getTime()); + end = new Date(now.getTime()); + } else { + start = new Date(now.getTime()); + end = new Date(target.getTime()); + } + + // Adjust for DST + end.setMinutes(end.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset()); + + diff.Y = end.getFullYear() - start.getFullYear(); + diff.M = end.getMonth() - start.getMonth(); + diff.D = end.getDate() - start.getDate(); + diff.hh = end.getHours() - start.getHours(); + diff.mm = end.getMinutes() - start.getMinutes(); + diff.ss = end.getSeconds() - start.getSeconds(); + + // Adjust negative differences + if (diff.ss < 0) { + diff.ss += 60; + diff.mm--; + } + if (diff.mm < 0) { + diff.mm += 60; + diff.hh--; + } + if (diff.hh < 0) { + diff.hh += 24; + diff.D--; + } + if (diff.D < 0) { + var lastMonthDays = new Date(end.getFullYear(), end.getMonth(), 0).getDate(); + diff.D += lastMonthDays; + diff.M--; + } + if (diff.M < 0) { + diff.M += 12; + diff.Y--; + } + + } else { + var timeDifference = target - now; + timeDifference = Math.abs(timeDifference); + + // Calculate days, hours, minutes, and seconds + diff.D = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + diff.hh = Math.floor((timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + diff.mm = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60)); + diff.ss = Math.floor((timeDifference % (1000 * 60)) / 1000); + } + + // add zero padding + diff.hh = pad2(diff.hh); + diff.mm = pad2(diff.mm); + diff.ss = pad2(diff.ss); + + return diff; +} + +function draw() { + var now = new Date(); + var nowFormatted = formatDateTime(now, settings.dateFormat, settings.time24, temp_displaySeconds); + var targetFormatted = formatDateTime(target, settings.dateFormat, settings.time24, true); + var diff = diffToTarget(); + + const nowY = now.getFullYear(); + const nowM = now.getMonth(); + const nowD = now.getDate(); + + const targetY = target.getFullYear(); + const targetM = target.getMonth(); + const targetD = target.getDate(); + + var diffYMD; + if (settings.displayMonthsYears) + diffYMD = `${diff.sign}${diff.Y}Y ${diff.M}M ${diff.D}D`; + else + diffYMD = `${diff.sign}${diff.D}D`; + + var diff_hhmm = `${diff.hh}:${diff.mm}`; + + g.clearRect(0, 24, SCREEN_WIDTH, SCREEN_HEIGHT); + //console.log("drawing"); + + let y = 24; //Bangle.getAppRect().y; + + // draw current date + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_CYAN : COLOUR_BLUE); + g.drawString(nowFormatted.date, 4, y); + y += SMALL_FONT_SIZE; + + // draw current time + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_CYAN : COLOUR_BLUE); + g.drawString(nowFormatted.time, 4, y); + y += SMALL_FONT_SIZE; + + // draw arrow + g.setFontCustom(arrowFont, 62, 16, 13).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + g.drawString(">", 4, y + 3); + + if (data.target.isSet) { + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + + if (nowY == targetY && nowM == targetM && nowD == targetD) { + // today + g.drawString("TODAY", 4 + 16 + 6, y); + } else if (nowY == targetY && nowM == targetM && nowD - targetD == 1) { + // yesterday + g.drawString("YESTERDAY", 4 + 16 + 6, y); + } else if (nowY == targetY && nowM == targetM && targetD - nowD == 1) { + // tomorrow + g.drawString("TOMORROW", 4 + 16 + 6, y); + } else { + // general case + // draw target date + g.drawString(targetFormatted.date, 4 + 16 + 6, y); + } + + y += SMALL_FONT_SIZE; + + // draw target time + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + g.drawString(targetFormatted.time, 4, y); + y += SMALL_FONT_SIZE + 4; + + } else { + // draw NOT SET + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + g.drawString("NOT SET", 4 + 16 + 6, y); + y += 2 * SMALL_FONT_SIZE + 4; + } + + // draw separator + g.setColor(g.theme.fg); + g.drawLine(0, y - 4, SCREEN_WIDTH, y - 4); + + // draw diffYMD + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(0, -1).setColor(g.theme.fg); + g.drawString(diffYMD, SCREEN_WIDTH / 2, y); + y += SMALL_FONT_SIZE; + + // draw diff_hhmm + g.setFont("Vector", BIG_FONT_SIZE).setFontAlign(0, -1).setColor(g.theme.fg); + g.drawString(diff_hhmm, SCREEN_WIDTH / 2, y); + + // draw diff_ss + if (temp_displaySeconds) { + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_GREY); + g.drawString(diff.ss, SCREEN_WIDTH / 2 + 52, y + 13); + } + + queueDraw(); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setUI("clock"); + +switch (settings.displaySeconds) { + case 0: // never + updateQueueMillisAndDraw(false); + break; + case 1: // unlocked + updateQueueMillisAndDraw(Bangle.isBacklightOn()); + break; + case 2: // always + updateQueueMillisAndDraw(true); + break; +} diff --git a/apps/elapsed_t/app.png b/apps/elapsed_t/app.png new file mode 100644 index 000000000..c2cac4fa1 Binary files /dev/null and b/apps/elapsed_t/app.png differ diff --git a/apps/elapsed_t/metadata.json b/apps/elapsed_t/metadata.json new file mode 100644 index 000000000..2515e0e79 --- /dev/null +++ b/apps/elapsed_t/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "elapsed_t", + "name": "Elapsed Time Clock", + "shortName": "Elapsed Time", + "type": "clock", + "version":"0.04", + "description": "A clock that calculates the time difference between now and any given target date.", + "tags": "clock,tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"elapsed_t.app.js","url":"app.js"}, + {"name":"elapsed_t.settings.js","url":"settings.js"}, + {"name":"elapsed_t.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"elapsed_t.data.json"}], + "icon": "app.png", + "readme": "README.md", + "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }, { "url": "screenshot3.png" }, { "url": "screenshot4.png" }], + "allow_emulator":true +} diff --git a/apps/elapsed_t/screenshot1.png b/apps/elapsed_t/screenshot1.png new file mode 100644 index 000000000..d15a5a9ae Binary files /dev/null and b/apps/elapsed_t/screenshot1.png differ diff --git a/apps/elapsed_t/screenshot2.png b/apps/elapsed_t/screenshot2.png new file mode 100644 index 000000000..00ad8aa36 Binary files /dev/null and b/apps/elapsed_t/screenshot2.png differ diff --git a/apps/elapsed_t/screenshot3.png b/apps/elapsed_t/screenshot3.png new file mode 100644 index 000000000..8ca6212f6 Binary files /dev/null and b/apps/elapsed_t/screenshot3.png differ diff --git a/apps/elapsed_t/screenshot4.png b/apps/elapsed_t/screenshot4.png new file mode 100644 index 000000000..e2a10ab62 Binary files /dev/null and b/apps/elapsed_t/screenshot4.png differ diff --git a/apps/elapsed_t/settings.js b/apps/elapsed_t/settings.js new file mode 100644 index 000000000..4726516d5 --- /dev/null +++ b/apps/elapsed_t/settings.js @@ -0,0 +1,63 @@ +(function(back) { + var APP_NAME = "elapsed_t"; + var FILE = APP_NAME + ".settings.json"; + // Load settings + var settings = Object.assign({ + // default values + displaySeconds: 1, + displayMonthsYears: true, + dateFormat: 0, + time24: true + }, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + if (typeof settings.displaySeconds === 'boolean') { + settings.displaySeconds = 1; + writeSettings(); + } + + var dateFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"]; + var displaySecondsFormats = ["Never", "Unlocked", "Always"]; + + // Show the menu + E.showMenu({ + "" : { "title" : "Elapsed Time" }, + "< Back" : () => back(), + 'Show\nseconds': { + value: settings.displaySeconds, + min: 0, max: 2, wrap: true, + onchange: v => { + settings.displaySeconds = v; + writeSettings(); + }, + format: function (v) {return displaySecondsFormats[v];} + }, + 'Show months/\nyears': { + value: !!settings.displayMonthsYears, + onchange: v => { + settings.displayMonthsYears = v; + writeSettings(); + } + }, + 'Time format': { + value: !!settings.time24, + onchange: v => { + settings.time24 = v; + writeSettings(); + }, + format: function (v) {return v ? "24h" : "AM/PM";} + }, + 'Date format': { + value: settings.dateFormat, + min: 0, max: 2, wrap: true, + onchange: v => { + settings.dateFormat = v; + writeSettings(); + }, + format: function (v) {return dateFormats[v];} + } + }); +}) diff --git a/apps/encourageclk/ChangeLog b/apps/encourageclk/ChangeLog index 83c07f784..9cb404008 100644 --- a/apps/encourageclk/ChangeLog +++ b/apps/encourageclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New face :) 0.02: code improvements 0.03: code improvments to queuedraw and draw +0.04: Minor code improvements diff --git a/apps/encourageclk/app.js b/apps/encourageclk/app.js index a78a788ba..1f19cc314 100644 --- a/apps/encourageclk/app.js +++ b/apps/encourageclk/app.js @@ -3,7 +3,6 @@ require("FontHaxorNarrow7x17").add(Graphics); require("FontDylex7x13").add(Graphics); -const storage = require('Storage'); const locale = require("locale"); const dateutil = require("date_utils"); const currentFont=g.getFont(); @@ -52,7 +51,7 @@ function queueDraw() { function draw() { var time = locale.time(d, 1); - var date = locale.date(d); + //var date = locale.date(d); var mo = dateutil.month(d.getMonth() + 1, 1); g.drawImage(bgimg,0,offset); //bg diff --git a/apps/encourageclk/metadata.json b/apps/encourageclk/metadata.json index 4e5d630cf..f3816c9de 100644 --- a/apps/encourageclk/metadata.json +++ b/apps/encourageclk/metadata.json @@ -2,7 +2,7 @@ "id": "encourageclk", "name": "Encouragement & Positivity Clock", "shortName":"Encouragement Clock", - "version": "0.03", + "version": "0.04", "description": "Tap on the watch for a note of encouragement", "icon": "app.png", "type": "clock", diff --git a/apps/espruinoctrl/ChangeLog b/apps/espruinoctrl/ChangeLog index 819ae56cb..522cba63e 100644 --- a/apps/espruinoctrl/ChangeLog +++ b/apps/espruinoctrl/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Disable not existing BTN3 on Bangle.js 2, set maximum transmit power +0.03: Now use BTN2 on Bangle.js 1, and on Bangle.js 2 use the middle button to return to the menu \ No newline at end of file diff --git a/apps/espruinoctrl/README.md b/apps/espruinoctrl/README.md index 7b2e434e7..59c96b0de 100644 --- a/apps/espruinoctrl/README.md +++ b/apps/espruinoctrl/README.md @@ -14,7 +14,8 @@ with 4 options: with this address will be connected to directly. If not specified a menu showing available Espruino devices is popped up. * **RX** - If checked, the app will display any data received from the -device being connected to. Use this if you want to print data - eg: `print(E.getBattery())` +device being connected to (waiting 500ms after the last data before disconnecting). +Use this if you want to print data - eg: `print(E.getBattery())` When done, click 'Upload'. Your changes will be saved to local storage so they'll be remembered next time you upload from the same device. @@ -25,4 +26,9 @@ Simply load the app and you'll see a menu with the menu items you defined. Select one and you'll be able to connect to the device and send the command. -If a command should wait for a response then +The Bangle will connect to the device, send the command, and if: + +* `RX` isn't set it will disconnect immediately and return to the menu +* `RX` is set it will listen for a response and write it to the screen, before +disconnecting after 500ms of inactivity. To return to the menu after this, press the button. + diff --git a/apps/espruinoctrl/custom.html b/apps/espruinoctrl/custom.html index 2329ad214..27ef1eb53 100644 --- a/apps/espruinoctrl/custom.html +++ b/apps/espruinoctrl/custom.html @@ -194,16 +194,14 @@ function sendCommandRX(device, text, callback) { function done() { Terminal.println("\\n============\\n Disconnected"); device.disconnect(); - if (global.BTN3 !== undefined) { - setTimeout(function() { - setWatch(function() { - if (callback) callback(); - resolve(); - }, BTN3); - g.reset().setFont("6x8",2).setFontAlign(0,0,1); - g.drawString("Back", g.getWidth()-10, g.getHeight()-50); - }, 200); - } + setTimeout(function() { + setWatch(function() { + if (callback) callback(); + resolve(); + }, (process.env.HWVERSION==2) ? BTN1 : BTN2); + g.reset().setFont("6x8",2).setFontAlign(0,0,1); + g.drawString("Back", g.getWidth()-10, g.getHeight()/2); + }, 200); } device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) { service = s; diff --git a/apps/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json index 9308b4a46..4f5fa01c8 100644 --- a/apps/espruinoctrl/metadata.json +++ b/apps/espruinoctrl/metadata.json @@ -2,7 +2,7 @@ "id": "espruinoctrl", "name": "Espruino Control", "shortName": "Espruino Ctrl", - "version": "0.02", + "version": "0.03", "description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!", "icon": "app.png", "tags": "tool,bluetooth", diff --git a/apps/espruinoprog/custom.html b/apps/espruinoprog/custom.html index a12189707..c6c51ca3e 100644 --- a/apps/espruinoprog/custom.html +++ b/apps/espruinoprog/custom.html @@ -128,38 +128,36 @@ LED.set();NRF.sleep();`); posteditor.on("change", editorChanged); document.getElementById("upload").addEventListener("click", function() { - if (!hasWarnings()) { - var precode = preeditor.getValue(); - var jscode = jseditor.getValue(); - var postcode = posteditor.getValue(); - var namePrefix = document.getElementById("nameprefix").value; - localStorage.setItem(LS_PRECODE, precode); - localStorage.setItem(LS_JSCODE, jscode); - localStorage.setItem(LS_POSTCODE, postcode); - localStorage.setItem(LS_NAMEPREFIX, namePrefix); + var precode = preeditor.getValue(); + var jscode = jseditor.getValue(); + var postcode = posteditor.getValue(); + var namePrefix = document.getElementById("nameprefix").value; + localStorage.setItem(LS_PRECODE, precode); + localStorage.setItem(LS_JSCODE, jscode); + localStorage.setItem(LS_POSTCODE, postcode); + localStorage.setItem(LS_NAMEPREFIX, namePrefix); - // force version - as long as we're above 1v96 we get the ability to upload to different storage files - var ENV = Espruino.Core.Env.getData(); - ENV.VERSION_MAJOR = 2; - ENV.VERSION_MINOR = 0; - // Now compile - Espruino.transform(jscode, { - SET_TIME_ON_WRITE : false, // time would just be out of date - SAVE_ON_SEND : 1, // save to flash - LOAD_STORAGE_FILE : 0, // do not load from storage after saving - // PRETOKENISE : true, - // MINIFICATION_LEVEL : "ESPRIMA", // maybe? - }).then(content => { - sendCustomizedApp({ - storage: [{ name: "espruinoprog.json", content: JSON.stringify({ - namePrefix : namePrefix, - pre : Espruino.Core.CodeWriter.reformatCode(precode), - code : Espruino.Core.CodeWriter.reformatCode(content), - post : Espruino.Core.CodeWriter.reformatCode(postcode) - })}] - }); + // force version - as long as we're above 1v96 we get the ability to upload to different storage files + var ENV = Espruino.Core.Env.getData(); + ENV.VERSION_MAJOR = 2; + ENV.VERSION_MINOR = 0; + // Now compile + Espruino.transform(jscode, { + SET_TIME_ON_WRITE : false, // time would just be out of date + SAVE_ON_SEND : 1, // save to flash + LOAD_STORAGE_FILE : 0, // do not load from storage after saving + // PRETOKENISE : true, + // MINIFICATION_LEVEL : "ESPRIMA", // maybe? + }).then(content => { + sendCustomizedApp({ + storage: [{ name: "espruinoprog.json", content: JSON.stringify({ + namePrefix : namePrefix, + pre : Espruino.Core.CodeWriter.reformatCode(precode), + code : Espruino.Core.CodeWriter.reformatCode(content), + post : Espruino.Core.CodeWriter.reformatCode(postcode) + })}] }); - } + }); }); document.getElementById("setdefault").addEventListener("click", function(e) { e.preventDefault(); diff --git a/apps/espruinoterm/ChangeLog b/apps/espruinoterm/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/espruinoterm/ChangeLog +++ b/apps/espruinoterm/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/espruinoterm/app.js b/apps/espruinoterm/app.js index 348190db4..1253b253a 100644 --- a/apps/espruinoterm/app.js +++ b/apps/espruinoterm/app.js @@ -7,7 +7,7 @@ var R = Bangle.appRect; var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true}); var termVisible = false; termg.setFont("6x8"); -term = require("VT100").connect(termg, { +let term = require("VT100").connect(termg, { charWidth : 6, charHeight : 8 }); diff --git a/apps/espruinoterm/interface.html b/apps/espruinoterm/interface.html index 660b3a86c..6ff7b7da5 100644 --- a/apps/espruinoterm/interface.html +++ b/apps/espruinoterm/interface.html @@ -83,11 +83,8 @@ function onInit() { Util.showModal("Loading..."); - Util.readStorage("espruinoterm.json", function(j) { + Util.readStorageJSON("espruinoterm.json", function(options) { Util.hideModal(); - try { - options = JSON.parse(j); - } catch (e) {} if (!Array.isArray(options)) setDefaults(); refresh(); }); diff --git a/apps/espruinoterm/metadata.json b/apps/espruinoterm/metadata.json index 25e6183e1..d967e0e1a 100644 --- a/apps/espruinoterm/metadata.json +++ b/apps/espruinoterm/metadata.json @@ -2,7 +2,7 @@ "id": "espruinoterm", "name": "Espruino Terminal", "shortName": "Espruino Term", - "version": "0.01", + "version": "0.02", "description": "Send commands to other Espruino devices via the Bluetooth UART interface, and see the result on a VT100 terminal. Customisable commands!", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/f9lander/ChangeLog b/apps/f9lander/ChangeLog index b5a33bd2e..8aed5d989 100644 --- a/apps/f9lander/ChangeLog +++ b/apps/f9lander/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Add lightning 0.03: Convert Yes/No On/Off in settings to checkboxes +0.04: Minor code improvements diff --git a/apps/f9lander/app.js b/apps/f9lander/app.js index 2f17a5bd5..d195a7c67 100644 --- a/apps/f9lander/app.js +++ b/apps/f9lander/app.js @@ -45,7 +45,7 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2, var exploded = false; var nExplosions = 0; -var landed = false; +//var landed = false; var lightning = 0; var settings = require("Storage").readJSON('f9settings.json', 1) || {}; diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json index 5a3887c9e..868b70f71 100644 --- a/apps/f9lander/metadata.json +++ b/apps/f9lander/metadata.json @@ -1,7 +1,7 @@ { "id": "f9lander", "name": "Falcon9 Lander", "shortName":"F9lander", - "version":"0.03", + "version": "0.04", "description": "Land a rocket booster", "icon": "f9lander.png", "screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }], diff --git a/apps/fallout_clock/.gitignore b/apps/fallout_clock/.gitignore new file mode 100644 index 000000000..e5f9ba937 --- /dev/null +++ b/apps/fallout_clock/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +res/ + +fallout_clock.code-workspace + +package.json +package-lock.json diff --git a/apps/fallout_clock/ChangeLog b/apps/fallout_clock/ChangeLog new file mode 100644 index 000000000..ee9876b1a --- /dev/null +++ b/apps/fallout_clock/ChangeLog @@ -0,0 +1,5 @@ +0.10: (20240125) Basic Working Clock. +0.11: (20240125) Widgets Added. Improved Interval Loop. +0.12: (20240221) Fix: Month Reporting Wrong. +0.20: (20240223) Created as a Package. +0.21: (20240223) Added StandardJS and NPM. diff --git a/apps/fallout_clock/LICENSE b/apps/fallout_clock/LICENSE new file mode 100644 index 000000000..d9d472761 --- /dev/null +++ b/apps/fallout_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Zachary D. Skelton + +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. diff --git a/apps/fallout_clock/README.md b/apps/fallout_clock/README.md new file mode 100644 index 000000000..b48e7e762 --- /dev/null +++ b/apps/fallout_clock/README.md @@ -0,0 +1,29 @@ +# Fallout Clock + +Inspired by the aesthetic of the Fallout series, this clock face looks to emulate the color and feel of a PipBoy. + +![clockface](./res/screenshot.png) + +## Usage + +You can also go into Settings, and choose it as the default clock under **Select Clock**. + +## Planned Features: +- Display Steps as Health +- Display Heartrate +- Brighter Color when the backlight is not on. +- Configurable Settings + +## Controls + +Zero Settings, Zero Configuration. Install and add as your clockface. + +## Requests + +To request new features, add [an issue](https://github.com/zskelton/fallout_clock/issues). + +## Creator + +Zachary D. Skelton \ +[Skelton Networks](https://skeltonnetworks.com)\ +[Github](https://github.com/zskelton) \ No newline at end of file diff --git a/apps/fallout_clock/app-icon.js b/apps/fallout_clock/app-icon.js new file mode 100644 index 000000000..c84c6fb48 --- /dev/null +++ b/apps/fallout_clock/app-icon.js @@ -0,0 +1 @@ +atob("MDDDAb88//9u/1r/1/YZrgAAit4kkkkkkkkkkAAVIkkkkkkkkkkkkkkkkkkAAAARJJIkkkkkkkkkkkkkkkAAAACJJJJUkkkkkkkkkkkkkAAAAARJJJJAAkkkkkkkkkkkAAAAACpJJJKgAAkkkkkkkkkgAAAAAVJJJJIAAAEkkkkkkkkAAAAACpJfpJUAAAAkkkkkkkgAAAAABJf/9JAAAAAEkkkkkkAAAAAARJdf/+gAAAAAkkkkkgAAAAAC//dL//gAAAAAEkkkkAAAAYADpJJL//8AAAAAAkkkkAAAD8AdJJJL///gAAAAAkkkgAAADr/pJJL////0AAAAAEkkgAAABJJL/pfb////gAAAAAkkAAAADpJeu22X////4AAAAAkkAAAADpL1tttuf/7f8AAAAAkgAAAAb/+ttttuSSS7/AAAAAEgAAAAdeyttttySSSb/gAAAAEgAAAC9eWtttuaySSf/2SSSSkgAAAVfxtttttyySX//9JJJQAAAAAJetttttttyST//9JJJUAAAABJeOaNyNutySW//9JJKgAAAARJdu6N1tvRySS3/JJJUAAAACJJVuVu1tzRyST2/JJKAAAAAVJL1ttyttuNuSWW7pJKgAAACpJLxtt6NtttuSS27pJUAAAAVJJLxtt6ttttuSWT9JKgAAAAJJJLxttzNtttuSST9JIAAAAiJJJL1ttttt2NuSSS9JUAAAA2222212xtty3RySSS9KgAAAEgAAAAZ6OW2tu1ySST9QAAAAEgAAAAaW1ttu2VySSXKAAAAAEkAAAACtu221ttySbdKgAAAAEkAAAADNty1ttuST9JUAAAAAkkAAAAAVty1ttuSXpKAAAAAAkkgAAAACtttttyT9JIAAAAAEkkkAAAAARttttyfdJUAAAAAEkkkAAAAACtttuSzJKgAAAAAkkkkgAAAAAWtuSSfpQAAAAAEkkkkkAAAAADa2yT9JAAAAAAkkkkkkgAAAAD7e3/pKgAAAAAkkkkkkkAAAAVL/9JJUAAAAAEkkkkkkkgAAARJJJJKAAAAAEkkkkkkkkkAAAJJJJJIAAAAAkkkkkkkkkkkACpJJJJUAAAAEkkkkkkkkkkkgCJJJJKgAAAEkkkkkkkkkkkkklJJJJQAAAEkkkkkkkkkkkkkkkkpJJAAEkkkkkkkkk=") diff --git a/apps/fallout_clock/clock.js b/apps/fallout_clock/clock.js new file mode 100644 index 000000000..56bb68a3a --- /dev/null +++ b/apps/fallout_clock/clock.js @@ -0,0 +1,141 @@ +/* global Bangle, Graphics, g */ + +// NAME: Fallout Clock (Bangle.js 2) +// DOCS: https://www.espruino.com/ReferenceBANGLEJS2 +// AUTHOR: Zachary D. Skelton +// VERSION: 0.1.0 (24JAN2024) - Creating [ Maj.Min.Bug ] REF: https://semver.org/ +// LICENSE: MIT License (2024) [ https://opensource.org/licenses/MIT ] + +/* THEME COLORS */ +// Dark Full - #000000 - (0,0.00,0) +// Dark Half - #002f00 - (0,0.18,0) +// Dark Zero - #005f00 - (0,0.37,0) +// Light Zero - #008e00 - (0,0.55,0) +// Light Half - #00bf00 - (0,0.75,0) +// Light Full - #00ee00 - (0,0.93,0) + +/* FONTS */ +// Font: Good Time Rg - https://www.dafont.com/good-times.font +// Large = 50px +Graphics.prototype.setLargeFont = function () { + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAABAAAAAAAB8AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAB8AAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAADwAAAAAAD8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAH/8AAAAAH/8AAAAAH/4AAAAAH/4AAAAAH/4AAAAAH/4AAAAAP/4AAAAAP/wAAAAAP/wAAAAAP/wAAAAAP/wAAAAAH/wAAAAAB/wAAAAAAfgAAAAAAHgAAAAAABgAAAAAAAAAAAAAAAAAAOAAAAAAB//AAAAAB//8AAAAB///wAAAA////AAAAf///4AAAP////AAAH////4AAD/+B//AAB/8AD/wAAf8AAP+AAP+AAB/gAD/AAAP8AA/gAAB/AAf4AAAf4AH8AAAD+AB/AAAA/gAfwAAAP4AH8AAAD+AB/AAAA/gAfwAAAP4AH8AAAD+AB/AAAA/gAfwAAAP4AH8AAAD+AB/gAAB/gAP4AAAfwAD/AAAP8AA/4AAH+AAH/AAD/gAB/8AD/wAAP/4H/8AAB////+AAAP////AAAB////gAAAP///wAAAB///wAAAAH//wAAAAAf/wAAAAAAOAAAAAAAAAAAAAfgAAAAAAH8AAAAAAB/AAAAAAAfwAAAAAAH8AAAAAAB/AAAAAAAfwAAAAAAH+AAAAAAB/wAAAAAAf/////wAD/////8AA//////AAH/////wAA/////8AAH/////AAAf////wAAA////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAH8AA//8AB/AA///AAfwAf//wAH8AH//8AB/AD///AAfwA///wAH8Af//8AB/AH8B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwD+AfwAH+B/AH8AB///wB/AAf//8AfwAD///AH8AA///gB/AAH//wAfwAA//4AH8AAH/8AB/AAAf8AAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AB/AAAB/AAfwAAAfwAH8AAAH8AB/AAAB/AAfwAAAfwAH8AAAH8AB/AAAB/AAfwAAAfwAH8AfAH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8A/gH8AB/gP4D/AAf8H/A/wAH///8/8AA/////+AAP/////gAB/////4AAf/8//8AAD/+P/+AAAf/B//AAAA/AH/AAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///AAAAB///8AAAAf///gAAAH///8AAAB////gAAAf///4AAAH////AAAAAAD/wAAAAAAP8AAAAAAB/AAAAAAAfwAAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAH/////8AB//////AAf/////wAH/////8AB//////AAf/////wAH/////8AAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/AAAAAAAAAAAAAAAAAAAAH///AD8AB///4B/AAf//+AfwAH///gH8AB///4B/AAf//+AfwAH///gH8AB///4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH8D/AAfwB/h/wAH8Af//8AB/AD//+AAfwA///gAH8AH//wAB/AA//4AAfgAH/8AAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAB//4AAAAB///wAAAB////AAAA////4AAAf////AAAP////4AAH/////AAB//fv/wAA/8H4f+AAP+B+D/gAH/AfgP8AB/gH4D/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+A/wAH8Afwf8AB/AH///AAfwA///gAH8AP//4AB/AD//8AAfwAf/+AAH8AD//AAAAAAP/gAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAB/AAAABAAfwAAAAwAH8AAAAcAB/AAAAfAAfwAAAPwAH8AAAH8AB/AAAD/AAfwAAD/wAH8AAB/8AB/AAA//AAfwAAf/gAH8AAP/gAB/AAP/wAAfwAH/4AAH8AD/8AAB/AB/8AAAfwB/+AAAH8A//AAAB/Af/gAAAfwP/gAAAH8P/wAAAB/H/4AAAAfz/8AAAAH9/8AAAAB//+AAAAAf//AAAAAH//gAAAAB//gAAAAAf/wAAAAAH/4AAAAAB/8AAAAAAf8AAAAAAH+AAAAAAB/AAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAD/gP/gAAB/+H/8AAA//z//gAAf////8AAP/////gAD/////4AB//////AAf+P/B/wAH+A/gP8AB/AP4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8A/gH8AB/gP8D/AAf+P/h/wAH/////8AA/////+AAP/////gAB/////wAAP/8//4AAB/+H/8AAAH+A/+AAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAH/4AAAAAH//gAAAAD//8AAAAB///AD8AA///4B/AAP///AfwAH///wH8AB/4f8B/AAf4B/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH+APwP8AB/wD8D/AAP+A/B/gAD/wPx/4AAf/j9/+AAH/////AAA/////gAAH////4AAA////8AAAH///8AAAAf//+AAAAA//8AAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAEAAAAD4AHwAAAB/AD+AAAAfwA/gAAAH8AP4AAAB/AD+AAAAfwA/gAAAD4AHwAAAAMAAYAAAAAAAAAAAAAAAAA'), + 46, + atob('DRYqEykpKiwsJi0rDQ=='), + 50 | 65536 + ) + return this +} + +// Medium = 16px () +Graphics.prototype.setMediumFont = function () { + this.setFontCustom( + atob('AAAAAAAADwAAAB8AAAAPAAAAGwAAAb8AAB/9AAH/kAAv+QAA/4AAAPQAAAACvkAAH//wAD///AC/Qf4A/AA/APQAHwDwAA8A9AAfAPwAPwC+Qf4AP//8AB//9AAG/4AAoAAAAPAAAAD4AAAA////AL///wAv//8AAAAAAPAL/wDwH/8A8D//APA9DwDwPA8A8DwPAPA8DwDwPA8A9DwPAP78DwD/+A8AL+APAPAADwDwAA8A8BQPAPA8DwDwPA8A8DwPAPA8DwDwPA8A9DwfAP7/vwD///8AP+v8AAUBUACqqQAA//9AAP//wABVW8AAAAPAAAADwAAAA8AAAAPAAAADwAD///8A////AP///wAAA8AAAAKAAAAAAAD//A8A//wPAP/8DwDwPA8A8DwPAPA8DwDwPA8A8DwPAPA8DwDwPR8A8D+/APAv/gCgC/gAC//gAD///AC///4A/Tx/APg8LwDwPA8A8DwPAPA8DwDwPA8A8D0fAPA/vwDwL/4AoAv4AAAAQABQAAAA8AAHAPAAHwDwAL8A8AP/APAf+ADwf9AA8v9AAP/4AAD/4AAA/0AAAP0AAAAAAEAAL9v4AL///gD//78A+H0vAPA8DwDwPA8A8DwPAPA8DwDwPA8A9D0fAP7/vwD///8AP9v8AC/4AAC//g8A/r8PAPQfDwDwDw8A8A8PAPAPDwDwDw8A+A8fAP5PfwC///4AL//8AAb/4AAAAAAAAAAAAAA8DwAAfB8AADwPAA=='), + 46, + atob('BAcNBg0NDg4ODA4OBA=='), + 16 | 131072 + ) + return this +} + +/* VARIABLES */ +// Const +const H = g.getHeight() +const W = g.getWidth() +// Mutable +let timer = null + +/* UTILITY FUNCTIONS */ +// Return String of Current Time +function getCurrentTime () { + try { + const d = new Date() + const h = d.getHours() + const m = d.getMinutes() + return `${h}:${m.toString().padStart(2, 0)}` + } catch (e) { + console.log(e) + return '0:00' + } +} + +// Return String of Current Date +function getCurrentDate () { + try { + const d = new Date() + const year = d.getFullYear() + const month = d.getMonth() + const day = d.getDate() + const display = `${month + 1}.${day.toString().padStart(2, 0)}.${year}` + return display + } catch (e) { + console.log(e) + return '0.0.0000' + } +} + +// Set A New Draw for the Next Minute +function setNextDraw () { + console.log('tick') + // Clear Timeout + if (timer) { + clearInterval(timer) + } + // Calculate time until next minute + const d = new Date() + const s = d.getSeconds() + const ms = d.getMilliseconds() + const delay = 60000 - (s * 1000) - ms + // Set Timeout + timer = setInterval(draw, delay) +} + +function draw () { + // Reset Variables + g.reset() + // Set Background Color + g.setBgColor(0, 0, 0) + // Draw Background + g.setColor(0, 0, 0) + g.fillRect(0, 0, W, H) + // Set Font for Time + g.setColor(0, 0.93, 0) + g.setLargeFont() + g.setFontAlign(0, 0) + // Draw Time + const time = getCurrentTime() + g.drawString(time, W / 2, H / 2, true /* clear background */) + // Set Font for Date + g.setColor(0, 0.75, 0) + g.setMediumFont() + g.setFontAlign(0, 1) + // Draw Date + const dateStr = getCurrentDate() + g.drawString(dateStr, W / 2, H - 45, true) + // Draw Border + g.setColor(0, 0.93, 0) + g.drawLine(5, 36, W - 5, 36) + g.drawLine(5, H - 9, W - 5, H - 9) + g.setColor(0, 0.18, 0) + g.fillRect(0, 27, W, 32) + g.fillRect(0, H, W, H - 5) + // Draw Widgets + Bangle.drawWidgets() + // Schedule Next Draw + setNextDraw() +} + +/* MAIN LOOP */ +function main () { + // Clear Screen + g.clear() + // Set as Clock to Enable Launcher Screen on BTN1 + Bangle.setUI('clock') + // Load Widgets + Bangle.loadWidgets() + // Draw Clock + draw() +} + +/* BOOT CODE */ +main() diff --git a/apps/fallout_clock/icon.png b/apps/fallout_clock/icon.png new file mode 100644 index 000000000..fc9bc1fdc Binary files /dev/null and b/apps/fallout_clock/icon.png differ diff --git a/apps/fallout_clock/metadata.json b/apps/fallout_clock/metadata.json new file mode 100644 index 000000000..20861411a --- /dev/null +++ b/apps/fallout_clock/metadata.json @@ -0,0 +1,18 @@ +{ + "id":"fallout_clock", + "name":"Fallout Clock", + "version":"0.21", + "description":"A simple clock for the Fallout fan", + "icon":"icon.png", + "type":"clock", + "tags": "clock,fallout,green,retro", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"fallout_clock.app.js", "url":"clock.js"}, + {"name":"fallout_clock.img", "url":"app-icon.js", "evaluate":true} + ], + "screenshots": [ + {"url":"./screenshot.png", "name":"Fallout Clock Screenshot"} + ] +} diff --git a/apps/fallout_clock/res/fallout_icon.png b/apps/fallout_clock/res/fallout_icon.png new file mode 100644 index 000000000..fc9bc1fdc Binary files /dev/null and b/apps/fallout_clock/res/fallout_icon.png differ diff --git a/apps/fallout_clock/res/good times rg.otf b/apps/fallout_clock/res/good times rg.otf new file mode 100644 index 000000000..53c181cca Binary files /dev/null and b/apps/fallout_clock/res/good times rg.otf differ diff --git a/apps/fallout_clock/res/screenshot.png b/apps/fallout_clock/res/screenshot.png new file mode 100644 index 000000000..253554b72 Binary files /dev/null and b/apps/fallout_clock/res/screenshot.png differ diff --git a/apps/fallout_clock/screenshot.png b/apps/fallout_clock/screenshot.png new file mode 100644 index 000000000..253554b72 Binary files /dev/null and b/apps/fallout_clock/screenshot.png differ diff --git a/apps/fastload/README.md b/apps/fastload/README.md index d82e13461..f7fab4933 100644 --- a/apps/fastload/README.md +++ b/apps/fastload/README.md @@ -1,11 +1,17 @@ +#### ⚠️EXPERIMENTAL⚠️ + # Fastload Utils -*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app. +Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app. This allows fast loading of all apps with two conditions: * Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded. * Current app can be removed completely from RAM. +#### ⚠️ KNOWN ISSUES ⚠️ + +* Fastload currently does not play nice with the automatic reload option of the apploader. App installs and upgrades are unreliable since the fastload causes code to run after reset and interfere with the upload process. + ## Settings * Activate app history and navigate back through recent apps instead of immediately loading the clock face @@ -17,7 +23,7 @@ This allows fast loading of all apps with two conditions: ## App history * Long press of hardware button clears the app history and loads the clock face -* Installing the 'Fast Reset' app allows doing fastloads directly to the clock face by pressing the hardware button for one second. Useful if there are many apps in the history and the user want to access the clock quickly. +* Installing the 'Fast Reset' app allows doing fastloads directly to the clock face by pressing the hardware button just a little longer than a click. Useful if there are many apps in the history and the user want to access the clock quickly. ## Technical infos diff --git a/apps/fastreset/ChangeLog b/apps/fastreset/ChangeLog index 5560f00bc..eec108328 100644 --- a/apps/fastreset/ChangeLog +++ b/apps/fastreset/ChangeLog @@ -1 +1,5 @@ 0.01: New App! +0.02: Shorten the timeout before executing to 250 ms. +0.03: Add inner timeout of 150 ms so user has more time to release the button + before clock ui is initialized and adds it's button watch for going to + launcher. diff --git a/apps/fastreset/README.md b/apps/fastreset/README.md index 381d80cf5..b23023f4a 100644 --- a/apps/fastreset/README.md +++ b/apps/fastreset/README.md @@ -1,6 +1,6 @@ # Fast Reset -Reset the watch by holding the hardware button for half a second. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. +Reset the watch to the clock face by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. Fast Reset was developed with the app history feature of 'Fastload Utils' in mind. If many apps are in the history stack, the user may want a fast way to exit directly to the clock face without using the firmwares reset function. @@ -12,11 +12,11 @@ Just install and it will run as boot code. If 'Fastload Utils' is installed fastloading will be used when possible. Otherwise a standard `load(.bootcde)` is used. -If the hardware button is held for longer the standard reset functionality of the firmware is executed as well (total 1.5 seconds). And eventually the watchdog will be kicked. +If the hardware button is held for longer the standard reset functionality of the firmware is executed as well. And eventually the watchdog will be kicked. ## Controls -Hold the hardware button for half a second to feel the buzz, loading the clock face. +Press the hardware button just a little longer than a click to feel the buzz, loading the clock face. ## Requests diff --git a/apps/fastreset/boot.js b/apps/fastreset/boot.js index 681a5ddb7..5d1fd50b1 100644 --- a/apps/fastreset/boot.js +++ b/apps/fastreset/boot.js @@ -1,5 +1,5 @@ {let buzzTimeout; -setWatch((e)=>{ - if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);Bangle.showClock();}, 500); +setWatch((e)=>{ + if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);setTimeout(Bangle.showClock,150);}, 250); if (!e.state && buzzTimeout) clearTimeout(buzzTimeout);}, -BTN,{repeat:true, edge:'both' });} +BTN,{repeat:true,edge:'both'});} diff --git a/apps/fastreset/metadata.json b/apps/fastreset/metadata.json index 098e0eeb1..ccd5e1ce4 100644 --- a/apps/fastreset/metadata.json +++ b/apps/fastreset/metadata.json @@ -1,12 +1,12 @@ { "id": "fastreset", "name": "Fast Reset", "shortName":"Fast Reset", - "version":"0.01", - "description": "Reset the watch by holding the hardware button for half a second. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", + "version":"0.03", + "description": "Reset the watch to the clock face by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", "icon": "app.png", "type": "bootloader", "tags": "system", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"fastreset.boot.js","url":"boot.js"} diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog index 7e7307c59..35fa366a4 100644 --- a/apps/fclock/ChangeLog +++ b/apps/fclock/ChangeLog @@ -1,3 +1,5 @@ 0.01: First published version of app 0.02: Move to Bangle.setUI to launcher support 0.03: Tell clock widgets to hide. +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js index 838a5578d..52607b9fc 100644 --- a/apps/fclock/fclock.app.js +++ b/apps/fclock/fclock.app.js @@ -2,7 +2,7 @@ var minutes; var seconds; var hours; var date; -var first = true; +//var first = true; var locale = require('locale'); var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; @@ -86,7 +86,7 @@ const drawSec = function (sections, color) { const drawClock = function () { - currentTime = new Date(); + const currentTime = new Date(); //Get date as a string date = dateStr(currentTime); @@ -163,12 +163,10 @@ const drawHR = function () { } if (grow) { - color = settings.hr.color; - g.setColor(color); + g.setColor(settings.hr.color); g.fillCircle(settings.hr.x, settings.hr.y, size); } else { - color = "#000000"; - g.setColor(color); + g.setColor("#000000"); g.drawCircle(settings.hr.x, settings.hr.y, size); } }; diff --git a/apps/fclock/metadata.json b/apps/fclock/metadata.json index dffb197a2..3491be0e2 100644 --- a/apps/fclock/metadata.json +++ b/apps/fclock/metadata.json @@ -2,7 +2,7 @@ "id": "fclock", "name": "fclock", "shortName": "F Clock", - "version": "0.03", + "version": "0.05", "description": "Simple design of a digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/ffcniftyapp/ChangeLog b/apps/ffcniftyapp/ChangeLog new file mode 100644 index 000000000..ef797827e --- /dev/null +++ b/apps/ffcniftyapp/ChangeLog @@ -0,0 +1,4 @@ +0.01: New Clock Nifty A ++ >> adding more information on the right side of the clock +0.02: Fix weather icon for languages other than English + + diff --git a/apps/ffcniftyapp/README.md b/apps/ffcniftyapp/README.md new file mode 100644 index 000000000..d6795840e --- /dev/null +++ b/apps/ffcniftyapp/README.md @@ -0,0 +1,13 @@ +# Nifty-A ++ Clock + +This is the clock: + +![](screenshot_niftyapp.png) + +The week number (ISO8601) can be turned off in settings (default is `On`) +Weather and Steps can be also turned off in settings. + +![](screenshot_settings_niftyapp.png) + +Based on the # Nifty-A Clock by @alessandrococco + diff --git a/apps/ffcniftyapp/app-icon.js b/apps/ffcniftyapp/app-icon.js new file mode 100644 index 000000000..f0a2393b1 --- /dev/null +++ b/apps/ffcniftyapp/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A5gX/+AGEn//mIWLgP/C4gGCAAMgC5UvC4sDC4YICkIhBgMQiEBE4Uxn4XDj//iEAn/yA4ICBgUikEikYXBBAIXEn/xJYURAYMygERkQHBiYLBKYIXF+AVDC4czgUSmIXBCQgED+ZeBR4YXBLYICDC5CPGC4IAIC40zmaPDC4MSLQQXK+ayCR4QXCiRoEC44ECh4bCC4MTiTDBC6ZHOC5B3NLYcvC4kBgL5BAAUikT+BfIIrB/8ykf/eYQXBkUTI4cBW4YQCgQGDmAXDkJfEC46GBAoJKCR4geCAAMRAAZRDAoIODO4UBPRIAJR5QXWgKNCTApNDC5Mv/6/DAwR3GAAyHCC4anJIo3/+bvEa4Uia4oXHkEvC4cvIgUf+YXKHYIvEAgcPC5QSGC5UBSwYXJLYQXFkUhgABBC5Ef/4mBl4XEmETmIXKgaXBmYCBC4cTkMxiQXJS4IACL4p3MgESCwJHFR5oxCiB3FkERC5cSToQXFmUyiAZFR48Bn7zCAQMjkfykQkBN4n/XgKPBAAQgCUQIfBUwYXHFgIGCdI4XDmYADmIIEkAWJAH4A4A==")) \ No newline at end of file diff --git a/apps/ffcniftyapp/app.js b/apps/ffcniftyapp/app.js new file mode 100644 index 000000000..5ca48c2f1 --- /dev/null +++ b/apps/ffcniftyapp/app.js @@ -0,0 +1,210 @@ +const w = require("weather"); +const locale = require("locale"); + +// Weather icons from https://icons8.com/icon/set/weather/color +function getSun() { + return require("heatshrink").decompress(atob("kEggILIgOAAZkDAYPAgeBwPAgIFBBgPhw4TBp/yAYMcnADBnEcAYMwhgDBsEGgE/AYP8AYYLDCYgbDEYYrD8fHIwI7CIYZLDL54AHA==")); +} +function getPartSun() { + return require("heatshrink").decompress(atob("kcjwIVSgOAAgUwAYUGAYVgBoQHBkAIBocIDIX4CIcOAYMYg/wgECgODgE8oFAmEDxEYgYZBgQLBGYNAg/ggcYgANBAIIxBsPAG4MYsAIBoQ3ChAQCgI4BHYUEBgUADIIPBh///4GBv//8Cda")); +} +//function getPartRain() { +// return require("heatshrink").decompress(atob("kEggIHEmADJjEwsEAjkw8EAh0B4EAg35wEAgP+CYMDwv8AYMDBAP2g8HgH+g0DBYMMgPwAYX8gOMEwMG3kAg8OvgSBjg2BgcYGQIcBAY5CBg0Av//HAM///4MYgNBEIMOCoUMDoUAnBwGkEA")); +//} +function getCloud() { + return require("heatshrink").decompress(atob("kEggIfcj+AAYM/8ADBuFwAYPAmADCCAMBwEf8ADBhFwg4aBnEPAYMYjAVBhgDDDoQDHCYc4jwDB+EP///FYIDBMTgA==")); +} +function getSnow() { + return require("heatshrink").decompress(atob("kEggITQj/AAYM98ADBsEwAYPAjADCj+AgOAj/gAYMIuEHwEAjEPAYQVChk4AYQhCAYcYBYQTDnEPgEB+EH///IAQACE4IAB8EICIPghwDB4EeBYNAjgDBg8EAYQYCg4bCgZuFA==")); +} +function getRain() { + return require("heatshrink").decompress(atob("kEggIPMh+AAYM/8ADBuFwAYPgmADB4EbAYOAj/ggOAhnwg4aBnAeCjEcCIMMjADCDoQDHjAPCnAXCuEP///8EDAYJECAAXBwkAgPDhwDBwUMgEEhkggEOjFgFgMQLYQAOA==")); +} +function getStorm() { + return require("heatshrink").decompress(atob("kcjwIROgfwAYMB44ICsEwAYMYgYQCgAICoEHCwMYgFDwEHCYfgEAMA4AIBmAXCgUGFIVAwADBhEQFIQtCGwNggPgjAVBngCBv8Oj+AgfjwYpCGAIABn4kBgOBBAVwjBHBD4IdBgYNBGwUAkCdbA=")); +} +// err icon - https://icons8.com/icons/set/error +function getErr() { + return require("heatshrink").decompress(atob("kEggILIgOAAYsD4ADBg/gAYMGsADBhkwAYsYjADCjgDBmEMAYNxxwDBsOGAYPBwYDEgOBwOAgYDB4EDHYPAgwDBsADDhgDBFIcwjAHBjE4AYMcmADBhhNCKIcG/4AGOw4A==")); +} +//function getDummy() { +// return require("heatshrink").decompress(atob("gMBwMAwA")); +//} + + + + +/** +Choose weather icon to display based on condition. +Based on function from the Bangle weather app so it should handle all of the conditions +sent from gadget bridge. +*/ +function chooseIcon(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm") || + condition.includes("squalls") || + condition.includes("tornado")) return getStorm; + else if (condition.includes("freezing") || condition.includes("snow") || + condition.includes("sleet")) { + return getSnow; + } + else if (condition.includes("drizzle") || + condition.includes("shower") || + condition.includes("rain")) return getRain; + else if (condition.includes("clear")) return getSun; + else if (condition.includes("clouds")) return getCloud; + else if (condition.includes("few clouds") || + condition.includes("scattered clouds") || + condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("overcast") || + condition.includes("partly cloudy") || + condition.includes("ash")) { + return getPartSun; + } else return getErr; +} + +/* +* Choose weather icon to display based on weather conditition code +* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 +*/ +function chooseIconByCode(code) { + const codeGroup = Math.round(code / 100); + switch (codeGroup) { + case 2: return getStorm; + case 3: return getRain; + case 5: + switch (code) { + case 511: return getSnow; + default: return getRain; + } + case 6: return getSnow; + case 7: return getPartSun; + case 8: + switch (code) { + case 800: return getSun; + case 804: return getCloud; + default: return getPartSun; + } + default: return getCloud; + } +} + +/*function condenseWeather(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm") || + condition.includes("squalls") || + condition.includes("tornado")) return "storm"; + if (condition.includes("freezing") || condition.includes("snow") || + condition.includes("sleet")) { + return "snow"; + } + if (condition.includes("drizzle") || + condition.includes("shower") || + condition.includes("rain")) return "rain"; + if (condition.includes("clear")) return "clear"; + if (condition.includes("clouds")) return "clouds"; + if (condition.includes("few clouds") || + condition.includes("scattered clouds") || + condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("overcast") || + condition.includes("partly cloudy") || + condition.includes("ash")) { + return "scattered"; + } else { return "N/A"; } + return "N/A"; +} +*/ +// copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 +function ISO8601_week_no(date) { + var tdt = new Date(date.valueOf()); + var dayn = (date.getDay() + 6) % 7; + tdt.setDate(tdt.getDate() - dayn + 3); + var firstThursday = tdt.valueOf(); + tdt.setMonth(0, 1); + if (tdt.getDay() !== 4) { + tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); + } + return 1 + Math.ceil((firstThursday - tdt) / 604800000); +} + +function format(value) { + return ("0" + value).substr(-2); +} + +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + init: function () { + const appRect = Bangle.appRect; + + this.viewport = { + width: appRect.w, + height: appRect.h + }; + + this.center = { + x: this.viewport.width / 2, + y: Math.round((this.viewport.height / 2) + appRect.y) + }; + + this.scale = g.getWidth() / this.viewport.width; + this.centerTimeScaleX = this.center.x + 32 * this.scale; + this.centerDatesScaleX = this.center.x + 40 * this.scale; + }, + draw: function (date) { + const hour = date.getHours() - (this.is12Hour && date.getHours() > 12 ? 12 : 0); + const month = date.getMonth() + 1; + // const monthName = require("date_utils").month(month, 1); + // const dayName = require("date_utils").dow(date.getDay(), 1); + let steps = Bangle.getHealthStatus("day").steps; + let curr = (w.get() === undefined ? "no data" : w.get()); // Get weather from weather app. + //let cWea =(curr === "no data" ? "no data" : curr.txt); + let cTemp= (curr === "no data" ? 273 : curr.temp); + // const temp = locale.temp(curr.temp - 273.15).match(/^(\D*\d*)(.*)$/); + + let w_icon = getErr; + if (locale.name === "en" || locale.name === "en_GB" || locale.name === "en_US") { + w_icon = chooseIcon(curr.txt === undefined ? "no data" : curr.txt); + } else { + // cannot use condition string to determine icon if language is not English; use weather code instead + const code = curr.code || -1; + if (code > 0) { + w_icon = chooseIconByCode(curr.code); + } + } + + g.setFontAlign(1, 0).setFont("Vector", 90 * this.scale); + g.drawString(format(hour), this.centerTimeScaleX, this.center.y - 31 * this.scale); + g.drawString(format(date.getMinutes()), this.centerTimeScaleX, this.center.y + 46 * this.scale); + + g.fillRect(this.center.x + 30 * this.scale, this.center.y - 72 * this.scale, this.center.x + 32 * this.scale, this.center.y + 74 * this.scale); + + g.setFontAlign(-1, 0).setFont("Vector", 16 * this.scale); + g.drawString(format(date.getDate()), this.centerDatesScaleX, this.center.y - 62 * this.scale); //26 + g.drawString("." + format(month) + ".", this.centerDatesScaleX + 20, this.center.y - 62 * this.scale); //44 + g.drawString(date.getFullYear(date), this.centerDatesScaleX, this.center.y - 44 * this.scale); //62 + if (this.showWeekNum) + g.drawString("CW" + format(ISO8601_week_no(date)), this.centerDatesScaleX, this.center.y + -26 * this.scale); //15 + // print(w_icon()); + if (this.showWeather) { + g.drawImage(w_icon(), this.centerDatesScaleX, this.center.y - 8 * this.scale); + // g.drawString(condenseWeather(curr.txt), this.centerDatesScaleX, this.center.y + 24 * this.scale); + g.drawString((cTemp === undefined ? 273 : cTemp ) - 273 + "°C", this.centerDatesScaleX, this.center.y + 44 * this.scale); //48 + + } + if (this.showSteps) + g.drawString(steps, this.centerDatesScaleX, this.center.y + 66 * this.scale); + + }, + settingsFile: "ffcniftyapp.json" +}); +clock.start(); diff --git a/apps/ffcniftyapp/app.png b/apps/ffcniftyapp/app.png new file mode 100644 index 000000000..1cd8a49b7 Binary files /dev/null and b/apps/ffcniftyapp/app.png differ diff --git a/apps/ffcniftyapp/metadata.json b/apps/ffcniftyapp/metadata.json new file mode 100644 index 000000000..6f368160b --- /dev/null +++ b/apps/ffcniftyapp/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "ffcniftyapp", + "name": "Nifty-A Clock ++", + "version": "0.02", + "description": "A nifty clock with time and date and more", + "dependencies": {"weather":"app"}, + "icon": "app.png", + "screenshots": [{"url":"screenshot_niftyapp.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"ffcniftyapp.app.js","url":"app.js"}, + {"name":"ffcniftyapp.img","url":"app-icon.js","evaluate":true}, + {"name":"ffcniftyapp.settings.js","url":"settings.js"} + ], + "data": [{"name":"ffcniftyapp.json"}] +} diff --git a/apps/ffcniftyapp/screenshot_niftyapp.png b/apps/ffcniftyapp/screenshot_niftyapp.png new file mode 100644 index 000000000..2428523d0 Binary files /dev/null and b/apps/ffcniftyapp/screenshot_niftyapp.png differ diff --git a/apps/ffcniftyapp/screenshot_settings_niftyapp.png b/apps/ffcniftyapp/screenshot_settings_niftyapp.png new file mode 100644 index 000000000..0bc9cc72c Binary files /dev/null and b/apps/ffcniftyapp/screenshot_settings_niftyapp.png differ diff --git a/apps/ffcniftyapp/settings.js b/apps/ffcniftyapp/settings.js new file mode 100644 index 000000000..a1f09e454 --- /dev/null +++ b/apps/ffcniftyapp/settings.js @@ -0,0 +1,35 @@ + +(function (back) { + var DEFAULTS = { + 'showWeekNum': false, + 'showWeather': false, + 'showSteps': false, + }; + let settings = require('Storage').readJSON("ffcniftyapp.json", 1) || DEFAULTS; + E.showMenu({ + + "": { "title": "Nifty-A Clock ++" }, + "< Back": () => back(), + /*LANG*/"Show Week Number": { + value: settings.showWeekNum, + onchange: v => { + settings.showWeekNum = v; + require("Storage").writeJSON("ffcniftyapp.json", settings); + } + }, + /*LANG*/"Show Weather": { + value: settings.showWeather, + onchange: w => { + settings.showWeather = w; + require("Storage").writeJSON("ffcniftyapp.json", settings); + } + }, + /*LANG*/"Show Steps": { + value: settings.showSteps, + onchange: z => { + settings.showSteps = z; + require("Storage").writeJSON("ffcniftyapp.json", settings); + } + } + }); +}) diff --git a/apps/fileman/ChangeLog b/apps/fileman/ChangeLog index f5af86229..cc1456b31 100644 --- a/apps/fileman/ChangeLog +++ b/apps/fileman/ChangeLog @@ -1,3 +1,4 @@ 0.01: New app! 0.02: Improve handling of large amounts of files (fix #579) 0.03: Update RegExp use (Was using backreference instead of character code) + diff --git a/apps/fileman/manage_files.html b/apps/fileman/manage_files.html new file mode 100644 index 000000000..30726a869 --- /dev/null +++ b/apps/fileman/manage_files.html @@ -0,0 +1,101 @@ + + + + + + + + +
+ + + +
Stats
{{s[0]}}{{s[1]}}
+

Files

+
+ + +
+ + + + + + + + + +
Filenameshowdelete
{{file}}
+
+ +
+
+ + + + diff --git a/apps/fileman/metadata.json b/apps/fileman/metadata.json index f5589e396..52f2fd06d 100644 --- a/apps/fileman/metadata.json +++ b/apps/fileman/metadata.json @@ -7,6 +7,7 @@ "icon": "icons8-filing-cabinet-48.png", "tags": "tools", "supports": ["BANGLEJS","BANGLEJS2"], + "interface": "manage_files.html", "readme": "README.md", "storage": [ {"name":"fileman.app.js","url":"fileman.app.js"}, diff --git a/apps/flashcards/ChangeLog b/apps/flashcards/ChangeLog index 6f9ed7196..4c0434f5b 100644 --- a/apps/flashcards/ChangeLog +++ b/apps/flashcards/ChangeLog @@ -2,4 +2,5 @@ 1.10: Download cards data from Trello public board 1.20: Configuration instructions added and card layout optimized 1.30: Font size can be changed in Settings -1.31: Fix for fast-loading support \ No newline at end of file +1.31: Fix for fast-loading support +1.32: Minor code improvements diff --git a/apps/flashcards/app.js b/apps/flashcards/app.js index d2118f8cb..43dde213e 100644 --- a/apps/flashcards/app.js +++ b/apps/flashcards/app.js @@ -39,7 +39,7 @@ while (str.length > maxLength) { let found = false; // Inserts new line at first whitespace of the line - for (i = maxLength - 1; i > 0; i--) { + for (let i = maxLength - 1; i > 0; i--) { if (str.charAt(i)==' ') { res = res + [str.slice(0, i), "\n"].join(''); str = str.slice(i + 1); diff --git a/apps/flashcards/metadata.json b/apps/flashcards/metadata.json index 096e7e918..dee6a9e3a 100644 --- a/apps/flashcards/metadata.json +++ b/apps/flashcards/metadata.json @@ -2,7 +2,7 @@ "id": "flashcards", "name": "Flash Cards", "shortName": "Flash Cards", - "version": "1.31", + "version": "1.32", "description": "Flash cards based on public Trello board", "readme":"README.md", "screenshots" : [ { "url":"screenshot.png" }], diff --git a/apps/flashcount/ChangeLog b/apps/flashcount/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/flashcount/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/flashcount/app-icon.js b/apps/flashcount/app-icon.js new file mode 100644 index 000000000..e1cf5fb54 --- /dev/null +++ b/apps/flashcount/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///x3ygHo8H1Jf8AgILLoALVgoLHggLCqAgJioLIqgLDGQsBqtAioOBqoYFqtUAIMVBY9VqwCBDIIAECYILCHowLBrWVBZFlrWWyptGgtq1WqJYI7GrQLCFxGrBYJHEBQNV1Wv9IEBEocFKIOq//qJAIZEAoNq3/+1QMBHoYYBrQLB1J4GitaEYZfGtfvBYJ3HtWr9WlNY0V1Nr1WlC4xIBrWmBZWVrJGFcYILBZY4LBoILIgoNBEILvHDIQ5BBY4IBBYMBMAwLBBA4LPBRMAKAoLRiALWAGw=")) \ No newline at end of file diff --git a/apps/flashcount/app.js b/apps/flashcount/app.js new file mode 100644 index 000000000..e3f925b27 --- /dev/null +++ b/apps/flashcount/app.js @@ -0,0 +1,59 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); +E.showMessage("Loading..."); +Bangle.setOptions({hrmPollInterval:5}); +Bangle.setHRMPower(1); + +function drawCounter() { + g.reset().clearRect(0,24,175,90); + //g.drawRect(0,24,175,90); + g.setFontAlign(0,0).setFontVector(60); + g.drawString(count, 88, 60); +} + +function hadPulse() { + count++; + drawCounter(); + g.setColor("#f00").fillCircle(156,156,20); + setTimeout(function() { + g.setColor(g.theme.bg).fillCircle(156,156,20); + }, 500); +} + +if (parseFloat(process.env.VERSION.replace("v","0"))<2019) { + E.showMessage("You need at least firmware 2v19","Error"); +} else if (Bangle.hrmRd(0)!=33) { // wrong sensor - probably VC31 from original bangle.js 2 + E.showMessage("This Bangle.js doesn't have a VC31B HRM sensor","Error"); +} else { + Bangle.setOptions({hrmGreenAdjust:false, hrmWearDetect:false, hrmPushEnv:true}); + Bangle.hrmWr(0x10, 197&0xF8 | 4); // just SLOT2 + Bangle.hrmWr(0x16, 0); // force env to be used as fast as possible + + var samples = 0, samplesHi = 0; + var count = 0; + { + let last = 0; + Bangle.on('HRM-env',v => { + if (v) { + if (!last) hadPulse(); + samplesHi++; + } + last = v; + samples++; + }); + } + + drawCounter(); + setInterval(function() { + g.reset().clearRect(0,90,175,130); + g.setFontAlign(0,0).setFont("6x8:2"); + g.drawString(samples+" sps", 88, 100); + if (samplesHi*5 > samples) { + g.setBgColor("#f00").setColor("#fff"); + g.clearRect(0,110,175,130).drawString("TOO LIGHT",88,120); + } + samples=0; + samplesHi=0; + Bangle.setLCDPower(1); // force LCD on! + }, 1000); +} \ No newline at end of file diff --git a/apps/flashcount/app.png b/apps/flashcount/app.png new file mode 100644 index 000000000..379b9d381 Binary files /dev/null and b/apps/flashcount/app.png differ diff --git a/apps/flashcount/metadata.json b/apps/flashcount/metadata.json new file mode 100644 index 000000000..1c0f785fd --- /dev/null +++ b/apps/flashcount/metadata.json @@ -0,0 +1,13 @@ +{ "id": "flashcount", + "name": "Flash Counter", + "shortName":"FlashCount", + "version":"0.01", + "description": "Count flashes/pulses of light using the heart rate monitor. Requires a VC31B HRM sensor, which should be in most watches except those produced for the original KickStarter campaign.", + "icon": "app.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"flashcount.app.js","url":"app.js"}, + {"name":"flashcount.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/flightdash/ChangeLog b/apps/flightdash/ChangeLog new file mode 100644 index 000000000..b0e36941a --- /dev/null +++ b/apps/flightdash/ChangeLog @@ -0,0 +1,2 @@ +1.00: initial release +1.01: Minor code improvements diff --git a/apps/flightdash/README.md b/apps/flightdash/README.md new file mode 100644 index 000000000..07b753178 --- /dev/null +++ b/apps/flightdash/README.md @@ -0,0 +1,76 @@ +# Flight Dashboard + +Shows basic flight and navigation instruments. + +![](screenshot.png) + +Basic flight data includes: + +- Ground speed +- Track +- Altimeter +- VSI +- Local time + +You can also set a destination to get nav guidance: + +- Distance from destination +- Bearing to destination +- Estimated Time En-route (minutes and seconds) +- Estimated Time of Arrival (in UTC) + +The speed/distance and altitude units are configurable. + +Altitude data can be derived from GPS or the Bangle's barometer. + + +## DISCLAIMER + +Remember to Aviate - Navigate - Communicate! Do NOT get distracted by your +gadgets, keep your eyes looking outside and do NOT rely on this app for actual +navigation! + + +## Usage + +After installing the app, use the "interface" page (floppy disk icon) in the +App Loader to filter and upload a list of airports (to be used as navigation +destinations). Due to memory constraints, only up to about 500 airports can be +stored on the Bangle itself (recommended is around 100 - 150 airports max.). + +Then, on the Bangle, access the Flight-Dash settings, either through the +Settings app (Settings -> Apps -> Flight-Dash) or a tap anywhere in the +Flight-Dash app itself. The following settings are available: + +- **Nav Dest.**: Choose the navigation destination: + - Nearest airports (from the uploaded list) + - Search the uploaded list of airports + - User waypoints (which can be set/edited through the settings) + - Nearest airports (queried online through AVWX - requires Internet connection at the time) +- **Speed** and **Altitude**: Set the preferred units of measurements. +- **Use Baro**: If enabled, altitude information is derived from the Bangle's barometer (instead of using GPS altitude). + +If the barometer is used for altitude information, the current QNH value is +also displayed. It can be adjusted by swiping up/down in the app. + +To query the nearest airports online through AVWX, you have to install - and +configure - the [avwx](?id=avwx) module. + +The app requires a text input method (to set user waypoint names, and search +for airports), and if not already installed will automatically install the +default "textinput" app as a dependency. + + +## Hint + +Under the bearing "band", the current nav destination is displayed. Next to +that, you'll also find the cardinal direction you are approaching **from**. +This can be useful for inbound radio calls. Together with the distance, the +current altitude and the ETA, you have all the information required to make +radio calls like a pro! + + +## Author + +Flaparoo [github](https://github.com/flaparoo) + diff --git a/apps/flightdash/flightdash-icon.js b/apps/flightdash/flightdash-icon.js new file mode 100644 index 000000000..3a2e2757c --- /dev/null +++ b/apps/flightdash/flightdash-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4")) diff --git a/apps/flightdash/flightdash.app.js b/apps/flightdash/flightdash.app.js new file mode 100644 index 000000000..ac0146210 --- /dev/null +++ b/apps/flightdash/flightdash.app.js @@ -0,0 +1,527 @@ +/* + * Flight Dashboard - Bangle.js + */ + +const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0) +const COLOUR_WHITE = 0xffff; // same as: g.setColor(1, 1, 1) +const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 0) +const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +const COLOUR_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1) +const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1) +const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1) + +const APP_NAME = 'flightdash'; + +const horizontalCenter = g.getWidth() / 2; +//const verticalCenter = g.getHeight() / 2; + +const dataFontHeight = 22; +const secondaryFontHeight = 18; +const labelFontHeight = 12; + + +//globals +var settings = {}; + +//var updateInterval; + +var speed = '-'; var speedPrev = -1; +var track = '-'; var trackPrev = -1; +var lat = 0; var lon = 0; +var distance = '-'; var distancePrev = -1; +var bearing = '-'; var bearingPrev = -1; +var relativeBearing = 0; var relativeBearingPrev = -1; +var fromCardinal = '-'; +var ETAdate = new Date(); +var ETA = '-'; var ETAPrev = ''; + +var QNH = Math.round(Bangle.getOptions().seaLevelPressure); var QNHPrev = -1; + +var altitude = '-'; var altitudePrev = -1; + +var VSI = '-'; var VSIPrev = -1; +var VSIraw = 0; +var VSIprevTimestamp = Date.now(); +var VSIprevAltitude; +var VSIsamples = 0; var VSIsamplesCount = 0; + +var speedUnit = 'N/A'; +var distanceUnit = 'N/A'; +var altUnit = 'N/A'; + + +// date object to time string in format (HH:MM[:SS]) +function timeStr(date, seconds) { + let timeStr = date.getHours().toString(); + if (timeStr.length == 1) timeStr = '0' + timeStr; + let minutes = date.getMinutes().toString(); + if (minutes.length == 1) minutes = '0' + minutes; + timeStr += ':' + minutes; + if (seconds) { + let seconds = date.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + timeStr += ':' + seconds; + } + return timeStr; +} + +// add thousands separator to number +function addThousandSeparator(n) { + let s = n.toString(); + if (s.length > 3) { + return s.substr(0, s.length - 3) + ',' + s.substr(s.length - 3, 3); + } else { + return s; + } +} + + +// update VSI +function updateVSI(alt) { + VSIsamples += alt; VSIsamplesCount += 1; + let VSInewTimestamp = Date.now(); + if (VSIprevTimestamp + 1000 <= VSInewTimestamp) { // update VSI every 1 second + let VSInewAltitude = VSIsamples / VSIsamplesCount; + if (VSIprevAltitude) { + let VSIinterval = (VSInewTimestamp - VSIprevTimestamp) / 1000; + VSIraw = (VSInewAltitude - VSIprevAltitude) * 60 / VSIinterval; // extrapolate to change / minute + } + VSIprevTimestamp = VSInewTimestamp; + VSIprevAltitude = VSInewAltitude; + VSIsamples = 0; VSIsamplesCount = 0; + } + + VSI = Math.floor(VSIraw / 10) * 10; // "smooth" VSI value + if (settings.altimeterUnits == 0) { // Feet + VSI = Math.round(VSI * 3.28084); + } // nothing else required since VSI is already in meters ("smoothed") + + if (VSI > 9999) VSI = 9999; + else if (VSI < -9999) VSI = -9999; +} + +// update GPS-derived information +function updateGPS(fix) { + if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return; + + speed = 'N/A'; + if (settings.speedUnits == 0) { // Knots + speed = Math.round(fix.speed * 0.539957); + } else if (settings.speedUnits == 1) { // km/h + speed = Math.round(fix.speed); + } else if (settings.speedUnits == 2) { // MPH + speed = Math.round(fix.speed * 0.621371); + } + if (speed > 9999) speed = 9999; + + if (! settings.useBaro) { // use GPS altitude + altitude = 'N/A'; + if (settings.altimeterUnits == 0) { // Feet + altitude = Math.round(fix.alt * 3.28084); + } else if (settings.altimeterUnits == 1) { // Meters + altitude = Math.round(fix.alt); + } + if (altitude > 99999) altitude = 99999; + + updateVSI(fix.alt); + } + + track = Math.round(fix.course); + if (isNaN(track)) track = '-'; + else if (track < 10) track = '00'+track; + else if (track < 100) track = '0'+track; + + lat = fix.lat; + lon = fix.lon; + + // calculation from https://www.movable-type.co.uk/scripts/latlong.html + const latRad1 = lat * Math.PI/180; + const latRad2 = settings.destLat * Math.PI/180; + const lonRad1 = lon * Math.PI/180; + const lonRad2 = settings.destLon * Math.PI/180; + + // distance (using "Equirectangular approximation") + let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2); + let y = (latRad2 - latRad1); + let distanceNumber = Math.sqrt(x*x + y*y) * 6371; // in km - 6371 = mean Earth radius + if (settings.speedUnits == 0) { // NM + distanceNumber = distanceNumber * 0.539957; + } else if (settings.speedUnits == 2) { // miles + distanceNumber = distanceNumber * 0.621371; + } + if (distanceNumber > 99.9) { + distance = '>100'; + } else { + distance = (Math.round(distanceNumber * 10) / 10).toString(); + if (! distance.includes('.')) + distance += '.0'; + } + + // bearing + y = Math.sin(lonRad2 - lonRad1) * Math.cos(latRad2); + x = Math.cos(latRad1) * Math.sin(latRad2) - + Math.sin(latRad1) * Math.cos(latRad2) * Math.cos(lonRad2 - lonRad1); + let nonNormalisedBearing = Math.atan2(y, x); + bearing = Math.round((nonNormalisedBearing * 180 / Math.PI + 360) % 360); + + if (bearing > 337 || bearing < 23) { + fromCardinal = 'S'; + } else if (bearing < 68) { + fromCardinal = 'SW'; + } else if (bearing < 113) { + fromCardinal = 'W'; + } else if (bearing < 158) { + fromCardinal = 'NW'; + } else if (bearing < 203) { + fromCardinal = 'N'; + } else if (bearing < 248) { + fromCardinal = 'NE'; + } else if (bearing < 293) { + fromCardinal = 'E'; + } else{ + fromCardinal = 'SE'; + } + + if (bearing < 10) bearing = '00'+bearing; + else if (bearing < 100) bearing = '0'+bearing; + + relativeBearing = parseInt(bearing) - parseInt(track); + if (isNaN(relativeBearing)) relativeBearing = 0; + if (relativeBearing > 180) relativeBearing -= 360; + else if (relativeBearing < -180) relativeBearing += 360; + + // ETA + if (speed) { + let ETE = distanceNumber * 3600 / speed; + let now = new Date(); + ETAdate = new Date(now + (now.getTimezoneOffset() * 1000 * 60) + ETE*1000); + if (ETE < 86400) { + ETA = timeStr(ETAdate, false); + } else { + ETA = '>24h'; + } + } else { + ETAdate = new Date(); + ETA = '-'; + } +} + + +// update barometric information +function updatePressure(e) { + altitude = 'N/A'; + if (settings.altimeterUnits == 0) { // Feet + altitude = Math.round(e.altitude * 3.28084); + } else if (settings.altimeterUnits == 1) { // Meters + altitude = Math.round(e.altitude); // altitude is given in meters + } + if (altitude > 99999) altitude = 99999; + + updateVSI(e.altitude); +} + + +// (re-)draw all read-outs +function draw(initial) { + + g.setBgColor(COLOUR_BLACK); + + // speed + if (speed != speedPrev || initial) { + g.setFontAlign(-1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_GREEN); + g.clearRect(0, 0, 55, dataFontHeight); + g.drawString(speed.toString(), 0, 0, false); + if (initial) { + g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(speedUnit, 0, dataFontHeight, false); + } + speedPrev = speed; + } + + + // distance + if (distance != distancePrev || initial) { + g.setFontAlign(1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE); + g.clearRect(g.getWidth() - 58, 0, g.getWidth(), dataFontHeight); + g.drawString(distance, g.getWidth(), 0, false); + if (initial) { + g.setFontAlign(1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(distanceUnit, g.getWidth(), dataFontHeight, false); + } + distancePrev = distance; + } + + + // track (+ static track/bearing content) + let trackY = 18; + let destInfoY = trackY + 53; + if (track != trackPrev || initial) { + g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE); + g.clearRect(horizontalCenter - 29, trackY, horizontalCenter + 28, trackY + dataFontHeight); + g.drawString(track.toString() + "\xB0", horizontalCenter + 3, trackY, false); + if (initial) { + let y = trackY + dataFontHeight + 1; + g.setColor(COLOUR_YELLOW); + g.drawRect(horizontalCenter - 30, trackY - 3, horizontalCenter + 29, y); + g.drawLine(0, y, g.getWidth(), y); + y += dataFontHeight + 5; + g.drawLine(0, y, g.getWidth(), y); + + g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA); + g.drawString(settings.destID, horizontalCenter, destInfoY, false); + } + trackPrev = track; + } + + + // bearing + if (bearing != bearingPrev || relativeBearing != relativeBearingPrev || initial) { + let bearingY = trackY + 27; + + g.clearRect(0, bearingY, g.getWidth(), bearingY + dataFontHeight); + + g.setColor(COLOUR_YELLOW); + for (let i = Math.floor(relativeBearing * 2.5) % 25; i <= g.getWidth(); i += 25) { + g.drawLine(i, bearingY + 3, i, bearingY + 16); + } + + let bearingX = horizontalCenter + relativeBearing * 2.5; + if (bearingX > g.getWidth() - 26) bearingX = g.getWidth() - 26; + else if (bearingX < 26) bearingX = 26; + g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_MAGENTA); + g.drawString(bearing.toString() + "\xB0", bearingX + 3, bearingY, false); + + g.clearRect(horizontalCenter + 42, destInfoY, horizontalCenter + 69, destInfoY + secondaryFontHeight); + g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA); + g.drawString(fromCardinal, horizontalCenter + 42, destInfoY, false); + if (initial) { + g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(' from', horizontalCenter, destInfoY, false); + } + + bearingPrev = bearing; + relativeBearingPrev = relativeBearing; + } + + + let row3y = g.getHeight() - 48; + + // QNH + if (settings.useBaro) { + if (QNH != QNHPrev || initial) { + let QNHy = row3y - secondaryFontHeight - 2; + g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.clearRect(horizontalCenter - 29, QNHy - secondaryFontHeight, horizontalCenter + 22, QNHy); + g.drawString(QNH.toString(), horizontalCenter - 3, QNHy, false); + if (initial) { + g.setFontAlign(0, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('QNH', horizontalCenter - 3, QNHy, false); + } + QNHPrev = QNH; + } + } + + + // VSI + if (VSI != VSIPrev || initial) { + g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.clearRect(0, row3y - secondaryFontHeight, 51, row3y); + g.drawString(VSI.toString(), 0, row3y, false); + if (initial) { + g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(altUnit + '/min', 0, row3y - secondaryFontHeight, false); + } + + let VSIarrowX = 6; + let VSIarrowY = row3y - 42; + g.clearRect(VSIarrowX - 7, VSIarrowY - 10, VSIarrowX + 6, VSIarrowY + 10); + g.setColor(COLOUR_WHITE); + if (VSIraw > 30) { // climbing + g.fillRect(VSIarrowX - 1, VSIarrowY, VSIarrowX + 1, VSIarrowY + 10); + g.fillPoly([ VSIarrowX , VSIarrowY - 11, + VSIarrowX + 7, VSIarrowY, + VSIarrowX - 7, VSIarrowY]); + } else if (VSIraw < -30) { // descending + g.fillRect(VSIarrowX - 1, VSIarrowY - 10, VSIarrowX + 1, VSIarrowY); + g.fillPoly([ VSIarrowX , VSIarrowY + 11, + VSIarrowX + 7, VSIarrowY, + VSIarrowX - 7, VSIarrowY ]); + } + } + + + // altitude + if (altitude != altitudePrev || initial) { + g.setFontAlign(1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.clearRect(g.getWidth() - 65, row3y - secondaryFontHeight, g.getWidth(), row3y); + g.drawString(addThousandSeparator(altitude), g.getWidth(), row3y, false); + if (initial) { + g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(altUnit, g.getWidth(), row3y - secondaryFontHeight, false); + } + altitudePrev = altitude; + } + + + // time + let now = new Date(); + let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60)); + g.setFontAlign(-1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_LIGHT_BLUE); + let timeStrMetrics = g.stringMetrics(timeStr(now, false)); + g.drawString(timeStr(now, false), 0, g.getHeight(), true); + + let seconds = now.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight); + g.drawString(seconds, timeStrMetrics.width + 2, g.getHeight() - 1, true); + + if (initial) { + g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('LOCAL', 0, g.getHeight() - dataFontHeight, false); + } + + + // ETE + let ETEy = g.getHeight() - dataFontHeight; + let ETE = '-'; + if (ETA != '-') { + let ETEseconds = Math.floor((ETAdate - nowUTC) / 1000); + if (ETEseconds < 0) ETEseconds = 0; + ETE = ETEseconds % 60; + if (ETE < 10) ETE = '0' + ETE; + ETE = Math.floor(ETEseconds / 60) + ':' + ETE; + if (ETE.length > 6) ETE = '>999m'; + } + g.clearRect(horizontalCenter - 35, ETEy - secondaryFontHeight, horizontalCenter + 29, ETEy); + g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.drawString(ETE, horizontalCenter - 3, ETEy, false); + if (initial) { + g.setFontAlign(0, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('ETE', horizontalCenter - 3, ETEy - secondaryFontHeight, false); + } + + + // ETA + if (ETA != ETAPrev || initial) { + g.clearRect(g.getWidth() - 63, g.getHeight() - dataFontHeight, g.getWidth(), g.getHeight()); + g.setFontAlign(1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE); + g.drawString(ETA, g.getWidth(), g.getHeight(), false); + if (initial) { + g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('UTC ETA', g.getWidth(), g.getHeight() - dataFontHeight, false); + } + ETAPrev = ETA; + } +} + + +function handleSwipes(directionLR, directionUD) { + if (directionUD == -1) { // up -> increase QNH + QNH = Math.round(Bangle.getOptions().seaLevelPressure); + QNH++; + Bangle.setOptions({'seaLevelPressure': QNH}); + } else if (directionUD == 1) { // down -> decrease QNH + QNH = Math.round(Bangle.getOptions().seaLevelPressure); + QNH--; + Bangle.setOptions({'seaLevelPressure': QNH}); + } +} + +function handleTouch(button, xy) { + if ('handled' in xy && xy.handled) return; + Bangle.removeListener('touch', handleTouch); + if (settings.useBaro) { + Bangle.removeListener('swipe', handleSwipes); + } + + // any touch -> show settings + clearInterval(updateTimeInterval); + Bangle.setGPSPower(false, APP_NAME); + if (settings.useBaro) + Bangle.setBarometerPower(false, APP_NAME); + + eval(require("Storage").read(APP_NAME+'.settings.js'))( () => { + E.showMenu(); + // "clear" values potentially affected by a settings change + speed = '-'; distance = '-'; + altitude = '-'; VSI = '-'; + // re-launch + start(); + }); +} + + +/* + * main + */ +function start() { + + // read in the settings + settings = Object.assign({ + useBaro: false, + speedUnits: 0, // KTS + altimeterUnits: 0, // FT + destID: 'KOSH', + destLat: 43.9844, + destLon: -88.5570, + }, require('Storage').readJSON(APP_NAME+'.json', true) || {}); + + // set units + if (settings.speedUnits == 0) { // Knots + speedUnit = 'KTS'; + distanceUnit = 'NM'; + } else if (settings.speedUnits == 1) { // km/h + speedUnit = 'KPH'; + distanceUnit = 'KM'; + } else if (settings.speedUnits == 2) { // MPH + speedUnit = 'MPH'; + distanceUnit = 'SM'; + } + + if (settings.altimeterUnits == 0) { // Feet + altUnit = 'FT'; + } else if (settings.altimeterUnits == 1) { // Meters + altUnit = 'M'; + } + + // initialise + g.reset(); + g.setBgColor(COLOUR_BLACK); + g.clear(); + + // draw incl. static components + draw(true); + + // enable timeout/interval and sensors + setTimeout(function() { + draw(); + updateTimeInterval = setInterval(draw, 1000); + }, 1000 - (Date.now() % 1000)); + + Bangle.setGPSPower(true, APP_NAME); + Bangle.on('GPS', updateGPS); + + if (settings.useBaro) { + Bangle.setBarometerPower(true, APP_NAME); + Bangle.on('pressure', updatePressure); + } + + // handle interaction + if (settings.useBaro) { + Bangle.on('swipe', handleSwipes); + } + Bangle.on('touch', handleTouch); + setWatch(e => { Bangle.showClock(); }, BTN1); // exit on button press +} + +start(); + + +/* +// TMP for testing: +//settings.speedUnits = 1; +//settings.altimeterUnits = 1; +QNH = 1013; +updateGPS({"fix":1,"speed":228,"alt":3763,"course":329,"lat":36.0182,"lon":-75.6713}); +updatePressure({"altitude":3700}); +*/ diff --git a/apps/flightdash/flightdash.png b/apps/flightdash/flightdash.png new file mode 100644 index 000000000..8230bc0c1 Binary files /dev/null and b/apps/flightdash/flightdash.png differ diff --git a/apps/flightdash/flightdash.settings.js b/apps/flightdash/flightdash.settings.js new file mode 100644 index 000000000..cd1ecdac6 --- /dev/null +++ b/apps/flightdash/flightdash.settings.js @@ -0,0 +1,327 @@ +(function(back) { + const APP_NAME = 'flightdash'; + const FILE = APP_NAME+'.json'; + + // if the avwx module is available, include an extra menu item to query nearest airports via AVWX + var avwx; + try { + avwx = require('avwx'); + } catch (error) { + // avwx module not installed + } + + // Load settings + var settings = Object.assign({ + useBaro: false, + speedUnits: 0, // KTS + altimeterUnits: 0, // FT + destID: 'KOSH', + destLat: 43.9844, + destLon: -88.5570, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // update the nav destination + function updateNavDest(destID, destLat, destLon) { + settings.destID = destID.replace(/[\W]+/g, '').slice(0, 7); + settings.destLat = parseFloat(destLat); + settings.destLon = parseFloat(destLon); + writeSettings(); + createDestMainMenu(); + } + + var airports; // cache list of airports + function readAirportsList(empty_cb) { + if (airports) { // airport list has already been read in + return true; + } + airports = require('Storage').readJSON(APP_NAME+'.airports.json', true); + if (! airports) { + E.showPrompt('No airports stored - download from the Bangle Apps Loader!', + {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + empty_cb(); + }); + return false; + } + return true; + } + + // use GPS fix + var afterGPSfixMenu = 'destNearest'; + function getLatLon(fix) { + if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return; + Bangle.setGPSPower(false, APP_NAME+'-settings'); + Bangle.removeListener('GPS', getLatLon); + switch (afterGPSfixMenu) { + case 'destNearest': + loadNearest(fix.lat, fix.lon); + break; + case 'createUserWaypoint': + { + if (!('userWaypoints' in settings)) + settings.userWaypoints = []; + let newIdx = settings.userWaypoints.length; + settings.userWaypoints[newIdx] = { + 'ID': 'USER'+(newIdx + 1), + 'lat': fix.lat, + 'lon': fix.lon, + }; + writeSettings(); + showUserWaypoints(); + break; + } + case 'destAVWX': + // the free ("hobby") account of AVWX is limited to 10 nearest stations + avwx.request('station/near/'+fix.lat+','+fix.lon, 'n=10&airport=true&reporting=false', data => { + loadAVWX(data); + }, error => { + console.log(error); + E.showPrompt('AVWX query failed: '+error, {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + createDestMainMenu(); + }); + }); + break; + default: + back(); + } + } + + // find nearest airports + function loadNearest(lat, lon) { + if (! readAirportsList(createDestMainMenu)) + return; + + const latRad1 = lat * Math.PI/180; + const lonRad1 = lon * Math.PI/180; + for (let i = 0; i < airports.length; i++) { + const latRad2 = airports[i].la * Math.PI/180; + const lonRad2 = airports[i].lo * Math.PI/180; + let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2); + let y = (latRad2 - latRad1); + airports[i].distance = Math.sqrt(x*x + y*y) * 6371; + } + let nearest = airports.sort((a, b) => a.distance - b.distance).slice(0, 14); + + let destNearest = { + '' : { 'title' : 'Nearest' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in nearest) { + let airport = nearest[i]; + destNearest[airport.i+' - '+airport.n] = + () => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo); + } + + E.showMenu(destNearest); + } + + // process the data returned by AVWX + function loadAVWX(data) { + let AVWXairports = JSON.parse(data.resp); + + let destAVWX = { + '' : { 'title' : 'Nearest (AVWX)' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in AVWXairports) { + let airport = AVWXairports[i].station; + let airport_id = ( airport.icao ? airport.icao : airport.gps ); + destAVWX[airport_id+' - '+airport.name] = + () => setTimeout(updateNavDest, 10, airport_id, airport.latitude, airport.longitude); + } + + E.showMenu(destAVWX); + } + + // individual user waypoint menu + function showUserWaypoint(idx) { + let wayptID = settings.userWaypoints[idx].ID; + let wayptLat = settings.userWaypoints[idx].lat; + let wayptLon = settings.userWaypoints[idx].lon; + let destUser = { + '' : { 'title' : wayptID }, + '< Back' : () => showUserWaypoints(), + }; + destUser['Set as Dest.'] = + () => setTimeout(updateNavDest, 10, wayptID, wayptLat, wayptLon); + destUser['Edit ID'] = function() { + require('textinput').input({text: wayptID}).then(result => { + if (result) { + if (result.length > 7) { + console.log('test'); + E.showPrompt('ID is too long!\n(max. 7 chars)', + {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + showUserWaypoint(idx); + }); + } else { + settings.userWaypoints[idx].ID = result; + writeSettings(); + showUserWaypoint(idx); + } + } else { + showUserWaypoint(idx); + } + }); + }; + destUser['Delete'] = function() { + E.showPrompt('Delete user waypoint '+wayptID+'?', + {'title': 'Flight-Dash'}).then((v) => { + if (v) { + settings.userWaypoints.splice(idx, 1); + writeSettings(); + showUserWaypoints(); + } else { + showUserWaypoint(idx); + } + }); + }; + + E.showMenu(destUser); + } + + // user waypoints menu + function showUserWaypoints() { + let destUser = { + '' : { 'title' : 'User Waypoints' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in settings.userWaypoints) { + let waypt = settings.userWaypoints[i]; + let idx = i; + destUser[waypt.ID] = + () => setTimeout(showUserWaypoint, 10, idx); + } + destUser['Create New'] = function() { + E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'}); + afterGPSfixMenu = 'createUserWaypoint'; + Bangle.setGPSPower(true, APP_NAME+'-settings'); + Bangle.on('GPS', getLatLon); + }; + + E.showMenu(destUser); + } + + // destination main menu + function createDestMainMenu() { + let destMainMenu = { + '' : { 'title' : 'Nav Dest.' }, + '< Back' : () => E.showMenu(mainMenu), + }; + destMainMenu['Is: '+settings.destID] = {}; + destMainMenu['Nearest'] = function() { + E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'}); + afterGPSfixMenu = 'destNearest'; + Bangle.setGPSPower(true, APP_NAME+'-settings'); + Bangle.on('GPS', getLatLon); + }; + destMainMenu['Search'] = function() { + require('textinput').input({text: ''}).then(result => { + if (result) { + if (! readAirportsList(createDestMainMenu)) + return; + + result = result.toUpperCase(); + let matches = []; + let tooManyFound = false; + for (let i in airports) { + if (airports[i].i.toUpperCase().includes(result) || + airports[i].n.toUpperCase().includes(result)) { + matches.push(airports[i]); + if (matches.length >= 15) { + tooManyFound = true; + break; + } + } + } + if (! matches.length) { + E.showPrompt('No airports found!', {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + createDestMainMenu(); + }); + return; + } + + let destSearch = { + '' : { 'title' : 'Search Results' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in matches) { + let airport = matches[i]; + destSearch[airport.i+' - '+airport.n] = + () => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo); + } + if (tooManyFound) { + destSearch['More than 15 airports found!'] = {}; + } + + E.showMenu(destSearch); + } else { + createDestMainMenu(); + } + }); + }; + destMainMenu['User waypts'] = function() { showUserWaypoints(); }; + if (avwx) { + destMainMenu['Nearest (AVWX)'] = function() { + E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'}); + afterGPSfixMenu = 'destAVWX'; + Bangle.setGPSPower(true, APP_NAME+'-settings'); + Bangle.on('GPS', getLatLon); + }; + } + E.showMenu(destMainMenu); + } + + // main menu + mainMenu = { + '' : { 'title' : 'Flight-Dash' }, + '< Back' : () => { + Bangle.setGPSPower(false, APP_NAME+'-settings'); + Bangle.removeListener('GPS', getLatLon); + back(); + }, + 'Nav Dest.': () => createDestMainMenu(), + 'Speed': { + value: parseInt(settings.speedUnits) || 0, + min: 0, + max: 2, + format: v => { + switch (v) { + case 0: return 'Knots'; + case 1: return 'km/h'; + case 2: return 'MPH'; + } + }, + onchange: v => { + settings.speedUnits = v; + writeSettings(); + } + }, + 'Altitude': { + value: parseInt(settings.altimeterUnits) || 0, + min: 0, + max: 1, + format: v => { + switch (v) { + case 0: return 'Feet'; + case 1: return 'Meters'; + } + }, + onchange: v => { + settings.altimeterUnits = v; + writeSettings(); + } + }, + 'Use Baro': { + value: !!settings.useBaro, // !! converts undefined to false + onchange: v => { + settings.useBaro = v; + writeSettings(); + } + }, + }; + + E.showMenu(mainMenu); +}) diff --git a/apps/flightdash/interface.html b/apps/flightdash/interface.html new file mode 100644 index 000000000..d0f57f316 --- /dev/null +++ b/apps/flightdash/interface.html @@ -0,0 +1,186 @@ + + + + + + + + +

You can upload a list of airports, which can then be used as the + navigation destinations in the Flight-Dash. It is recommended to only + upload up to 100 - 150 airports max. Due to memory contraints on the + Bangle, no more than 500 airports can be uploaded.

+ +

The database of airports is based on OurAirports. + +

Filter Airports

+
+ + nm of + + / + + +
+ This is using a simple lat/lon "block" - and + not within a proper radius around the given lat/lon position. An easy + way to find a lat/lon pair is to search for an airport based on ident + or name, and then use the found coordinates. +
+
+

- or -

+

+ + +

+

- or -

+

+ + +

+

Only 1 of the above filters is applied, with higher up in the list taking precedence.

+
+ + +
+ Use the + ISO-3166 2-letter code, + eg. "AU" +
+
+ +

+ + +

+ +
+ +

Results:

+

+
+ + + + + + + diff --git a/apps/flightdash/jquery-csv.min.js b/apps/flightdash/jquery-csv.min.js new file mode 100644 index 000000000..cbaefa6b8 --- /dev/null +++ b/apps/flightdash/jquery-csv.min.js @@ -0,0 +1 @@ +RegExp.escape=function(r){return r.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")},function(){"use strict";var p;(p="undefined"!=typeof jQuery&&jQuery?jQuery:{}).csv={defaults:{separator:",",delimiter:'"',headers:!0},hooks:{castToScalar:function(r,e){if(isNaN(r))return r;if(/\./.test(r))return parseFloat(r);var a=parseInt(r);return isNaN(a)?null:a}},parsers:{parse:function(r,e){var a=e.separator,t=e.delimiter;e.state.rowNum||(e.state.rowNum=1),e.state.colNum||(e.state.colNum=1);var o=[],s=[],n=0,i="",l=!1;function u(){if(n=0,i="",e.start&&e.state.rowNum=e.end&&(l=!0),e.state.rowNum++,e.state.colNum=1}function c(){if(void 0===e.onParseValue)s.push(i);else if(e.headers&&1===e.state.rowNum)s.push(i);else{var r=e.onParseValue(i,e.state);!1!==r&&s.push(r)}i="",n=0,e.state.colNum++}var f=RegExp.escape(a),d=RegExp.escape(t),m=/(D|S|\r\n|\n|\r|[^DS\r\n]+)/,p=m.source;return p=(p=p.replace(/S/g,f)).replace(/D/g,d),m=new RegExp(p,"gm"),r.replace(m,function(r){if(!l)switch(n){case 0:if(r===a){i+="",c();break}if(r===t){n=1;break}if(/^(\r\n|\n|\r)$/.test(r)){c(),u();break}i+=r,n=3;break;case 1:if(r===t){n=2;break}i+=r,n=1;break;case 2:if(r===t){i+=r,n=1;break}if(r===a){c();break}if(/^(\r\n|\n|\r)$/.test(r)){c(),u();break}throw Error("CSVDataError: Illegal State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");case 3:if(r===a){c();break}if(/^(\r\n|\n|\r)$/.test(r)){c(),u();break}if(r===t)throw Error("CSVDataError: Illegal Quote [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");throw Error("CSVDataError: Illegal Data [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");default:throw Error("CSVDataError: Unknown State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]")}}),0!==s.length&&(c(),u()),o},splitLines:function(r,a){if(r){var t=(a=a||{}).separator||p.csv.defaults.separator,o=a.delimiter||p.csv.defaults.delimiter;a.state=a.state||{},a.state.rowNum||(a.state.rowNum=1);var e=[],s=0,n="",i=!1,l=RegExp.escape(t),u=RegExp.escape(o),c=/(D|S|\n|\r|[^DS\r\n]+)/,f=c.source;return f=(f=f.replace(/S/g,l)).replace(/D/g,u),c=new RegExp(f,"gm"),r.replace(c,function(r){if(!i)switch(s){case 0:if(r===t){n+=r,s=0;break}if(r===o){n+=r,s=1;break}if("\n"===r){d();break}if(/^\r$/.test(r))break;n+=r,s=3;break;case 1:if(r===o){n+=r,s=2;break}n+=r,s=1;break;case 2:var e=n.substr(n.length-1);if(r===o&&e===o){n+=r,s=1;break}if(r===t){n+=r,s=0;break}if("\n"===r){d();break}if("\r"===r)break;throw Error("CSVDataError: Illegal state [Row:"+a.state.rowNum+"]");case 3:if(r===t){n+=r,s=0;break}if("\n"===r){d();break}if("\r"===r)break;if(r===o)throw Error("CSVDataError: Illegal quote [Row:"+a.state.rowNum+"]");throw Error("CSVDataError: Illegal state [Row:"+a.state.rowNum+"]");default:throw Error("CSVDataError: Unknown state [Row:"+a.state.rowNum+"]")}}),""!==n&&d(),e}function d(){if(s=0,a.start&&a.state.rowNum=a.end&&(i=!0),a.state.rowNum++}},parseEntry:function(r,e){var a=e.separator,t=e.delimiter;e.state.rowNum||(e.state.rowNum=1),e.state.colNum||(e.state.colNum=1);var o=[],s=0,n="";function i(){if(void 0===e.onParseValue)o.push(n);else{var r=e.onParseValue(n,e.state);!1!==r&&o.push(r)}n="",s=0,e.state.colNum++}if(!e.match){var l=RegExp.escape(a),u=RegExp.escape(t),c=/(D|S|\n|\r|[^DS\r\n]+)/.source;c=(c=c.replace(/S/g,l)).replace(/D/g,u),e.match=new RegExp(c,"gm")}return r.replace(e.match,function(r){switch(s){case 0:if(r===a){n+="",i();break}if(r===t){s=1;break}if("\n"===r||"\r"===r)break;n+=r,s=3;break;case 1:if(r===t){s=2;break}n+=r,s=1;break;case 2:if(r===t){n+=r,s=1;break}if(r===a){i();break}if("\n"===r||"\r"===r)break;throw Error("CSVDataError: Illegal State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");case 3:if(r===a){i();break}if("\n"===r||"\r"===r)break;if(r===t)throw Error("CSVDataError: Illegal Quote [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");throw Error("CSVDataError: Illegal Data [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");default:throw Error("CSVDataError: Unknown State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]")}}),i(),o}},helpers:{collectPropertyNames:function(r){var e=[],a=[],t=[];for(e in r)for(a in r[e])r[e].hasOwnProperty(a)&&t.indexOf(a)<0&&"function"!=typeof r[e][a]&&t.push(a);return t}},toArray:function(r,e,a){if(void 0!==e&&"function"==typeof e){if(void 0!==a)return console.error("You cannot 3 arguments with the 2nd argument being a function");a=e,e={}}e=void 0!==e?e:{};var t={};t.callback=void 0!==a&&"function"==typeof a&&a,t.separator="separator"in e?e.separator:p.csv.defaults.separator,t.delimiter="delimiter"in e?e.delimiter:p.csv.defaults.delimiter;var o=void 0!==e.state?e.state:{};e={delimiter:t.delimiter,separator:t.separator,onParseEntry:e.onParseEntry,onParseValue:e.onParseValue,state:o};var s=p.csv.parsers.parseEntry(r,e);if(!t.callback)return s;t.callback("",s)},toArrays:function(r,e,a){if(void 0!==e&&"function"==typeof e){if(void 0!==a)return console.error("You cannot 3 arguments with the 2nd argument being a function");a=e,e={}}e=void 0!==e?e:{};var t={};t.callback=void 0!==a&&"function"==typeof a&&a,t.separator="separator"in e?e.separator:p.csv.defaults.separator,t.delimiter="delimiter"in e?e.delimiter:p.csv.defaults.delimiter;var o=[];if(void 0!==(e={delimiter:t.delimiter,separator:t.separator,onPreParse:e.onPreParse,onParseEntry:e.onParseEntry,onParseValue:e.onParseValue,onPostParse:e.onPostParse,start:e.start,end:e.end,state:{rowNum:1,colNum:1}}).onPreParse&&(r=e.onPreParse(r,e.state)),o=p.csv.parsers.parse(r,e),void 0!==e.onPostParse&&(o=e.onPostParse(o,e.state)),!t.callback)return o;t.callback("",o)},toObjects:function(r,e,a){if(void 0!==e&&"function"==typeof e){if(void 0!==a)return console.error("You cannot 3 arguments with the 2nd argument being a function");a=e,e={}}e=void 0!==e?e:{};var t={};t.callback=void 0!==a&&"function"==typeof a&&a,t.separator="separator"in e?e.separator:p.csv.defaults.separator,t.delimiter="delimiter"in e?e.delimiter:p.csv.defaults.delimiter,t.headers="headers"in e?e.headers:p.csv.defaults.headers,e.start="start"in e?e.start:1,t.headers&&e.start++,e.end&&t.headers&&e.end++;var o,s=[];e={delimiter:t.delimiter,separator:t.separator,onPreParse:e.onPreParse,onParseEntry:e.onParseEntry,onParseValue:e.onParseValue,onPostParse:e.onPostParse,start:e.start,end:e.end,state:{rowNum:1,colNum:1},match:!1,transform:e.transform};var n={delimiter:t.delimiter,separator:t.separator,start:1,end:1,state:{rowNum:1,colNum:1},headers:!0};void 0!==e.onPreParse&&(r=e.onPreParse(r,e.state));var i=p.csv.parsers.splitLines(r,n),l=p.csv.toArray(i[0],n);o=p.csv.parsers.splitLines(r,e),e.state.colNum=1,e.state.rowNum=l?2:1;for(var u=0,c=o.length;u { let aJson = storage.readJSON(a, false) as AppInfo; let bJson = storage.readJSON(b, false) as AppInfo; - var n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!); + const n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!); if (n) return n; // do sortorder first if (aJson.name < bJson.name) return -1; if (aJson.name > bJson.name) return 1; diff --git a/apps/folderlaunch/interface.html b/apps/folderlaunch/interface.html new file mode 100644 index 000000000..24d3f448d --- /dev/null +++ b/apps/folderlaunch/interface.html @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + +

Folder Launcher

+
+
+ + + +
+
+
+ + + + + + + + + + +
AppFolderMove to
+
+

Add a new folder

+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + diff --git a/apps/folderlaunch/metadata.json b/apps/folderlaunch/metadata.json index 0cf83abb4..9853c7daf 100644 --- a/apps/folderlaunch/metadata.json +++ b/apps/folderlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "folderlaunch", "name": "Folder launcher", - "version": "0.03", + "version": "0.04", "description": "Launcher that allows you to put your apps into folders", "icon": "icon.png", "type": "launch", @@ -10,6 +10,7 @@ "BANGLEJS2" ], "readme": "README.md", + "interface": "interface.html", "storage": [ { "name": "folderlaunch.app.js", diff --git a/apps/folderlaunch/types.d.ts b/apps/folderlaunch/types.d.ts index 86c0465e5..a6f927cd0 100644 --- a/apps/folderlaunch/types.d.ts +++ b/apps/folderlaunch/types.d.ts @@ -11,7 +11,7 @@ type Config = { showClocks: boolean, // Whether clocks are shown showLaunchers: boolean, // Whether launchers are shown disableVibration: boolean, // Whether vibration is disabled - hidden: Array, // IDs of apps to explicitly hide + hidden: Array, // IDs of apps to explicitly hide display: { rows: number, // Display an X by X grid of apps icon: boolean, // Whether to show icons diff --git a/apps/followtherecipe/ChangeLog b/apps/followtherecipe/ChangeLog new file mode 100644 index 000000000..68ca62642 --- /dev/null +++ b/apps/followtherecipe/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App +0.02: Minor code improvements diff --git a/apps/followtherecipe/README.md b/apps/followtherecipe/README.md new file mode 100644 index 000000000..ce80c680d --- /dev/null +++ b/apps/followtherecipe/README.md @@ -0,0 +1,32 @@ +# Follow the recipe +A simple app using Gadgetbridge internet access to fetch a recipe and follow it step by step. + +For now, if you are connected to Gadgetbridge, it display a random recipe whenever you restart the app. +Else, a stored recipe is displayed. +You can go to the next screen via tab right and go the previous screen via tab left. + +You can choose a recipe via the App Loader: +Select the recipe then click on "Save recipe onto BangleJs". + +![](screenshot1.png) + +Make sure that you allowed 'Internet Access' via the Gadgetbridge app before using Follow The Recipe. + +If you run the app via web IDE, connect your Banglejs via Gadgetbridge app then in the web IDE connect via Android. +For more informations, [see the documentation about Gadgetbridge](https://www.espruino.com/Gadgetbridge) + +TO-DOs: + +- [X] Display random recipe on start +- [ ] Choose between some recipe previously saved or random on start +- [ ] Edit the recipe and save it to BangleJs +- [ ] improve GUI (color, fonts, ...) + +## Contributors + +Written by [Mel-Levesque](https://github.com/Mel-Levesque) + +## Thanks To + +- Design taken from the [Info application](https://github.com/espruino/BangleApps/tree/master/apps/info) by [David Peer](https://github.com/peerdavid) +- App icon from [icons8.com](https://icons8.com) diff --git a/apps/followtherecipe/app-icon.js b/apps/followtherecipe/app-icon.js new file mode 100644 index 000000000..95b75991b --- /dev/null +++ b/apps/followtherecipe/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA////e3uUt+cVEjEK0ALJlWqAAv4BYelqoAEBa/61ALRrQDCBY9q1ILCLYQLD0EKHZFawECAgILGFIYvHwAFBgQLGqwLDyoLGSwYLBI4gLFHYojFI4wdCMAJHGtEghEpBY9YkWIkoLNR4oLEHYwLMHYILJAoILIrWq1SzIBZYjE/gXKBYwAEEYwAEC67LGHQIABZY4jWF9FXBZVfBZX/BYmv/4AEBZ8KKIYACwALCACwA==")) diff --git a/apps/followtherecipe/app.js b/apps/followtherecipe/app.js new file mode 100644 index 000000000..056632e27 --- /dev/null +++ b/apps/followtherecipe/app.js @@ -0,0 +1,226 @@ +const settings = require("Storage").readJSON("followtherecipe.json"); +//const locale = require('locale'); +var W = g.getWidth(), H = g.getHeight(); +var screen = 0; + +let maxLenghtHorizontal = 16; +let maxLenghtvertical = 6; + +let uri = "https://www.themealdb.com/api/json/v1/1/random.php"; + +var colors = {0: "#70f", 1:"#70d", 2: "#70g", 3: "#20f", 4: "#30f"}; + +var screens = []; + +function drawData(name, value, y){ + g.drawString(name, 10, y); + g.drawString(value, 100, y); +} + +function drawInfo() { + g.reset().clearRect(Bangle.appRect); + var h=18, y = h; + + // Header + g.drawLine(0,25,W,25); + g.drawLine(0,26,W,26); + + // Info body depending on screen + g.setFont("Vector",15).setFontAlign(-1,-1).setColor("#0ff"); + screens[screen].items.forEach(function (item, index){ + g.setColor(colors[index]); + drawData(item.name, item.fun, y+=h); + }); + + // Bottom + g.setColor(g.theme.fg); + g.drawLine(0,H-h-3,W,H-h-3); + g.drawLine(0,H-h-2,W,H-h-2); + g.setFont("Vector",h-2).setFontAlign(-1,-1); + g.drawString(screens[screen].name, 2, H-h+2); + g.setFont("Vector",h-2).setFontAlign(1,-1); + g.drawString((screen+1) + "/" + screens.length, W, H-h+2); +} + +// Change page if user touch the left or the right of the screen +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.3); + var right = g.getWidth() - left; + var isLeft = e.x < left; + var isRight = e.x > right; + + if(isRight){ + screen = (screen + 1) % screens.length; + } + + if(isLeft){ + screen -= 1; + screen = screen < 0 ? screens.length-1 : screen; + } + + Bangle.buzz(40, 0.6); + drawInfo(); +}); + +function infoIngredients(ingredients, measures){ + let combinedList = []; + let listOfString = []; + let lineBreaks = 0; + + // Iterate through the arrays and combine the ingredients and measures + for (let i = 0; i < ingredients.length; i++) { + const combinedString = `${ingredients[i]}: ${measures[i]}`; + lineBreaks += 1; + // Check if the line is more than 16 characters + if (combinedString.length > maxLenghtHorizontal) { + // Add line break and update lineBreaks counter + combinedList.push(`${ingredients[i]}:\n${measures[i]}`); + lineBreaks += 1; + } else { + // Add to the combinedList array + combinedList.push(combinedString); + } + + // Check the total line breaks + if (lineBreaks >= maxLenghtvertical) { + const resultString = combinedList.join('\n'); + listOfString.push(resultString); + combinedList = []; + lineBreaks = 0; + } + if(i == ingredients.length){ + listOfString.push(combinedList.join('\n')); + } + } + + for(let i = 0; i < listOfString.length; i++){ + let screen = { + name: "Ingredients", + items: [ + {name: listOfString[i], fun: ""}, + ] + }; + screens.push(screen); + } +} + +// Format instructions to display on screen +function infoInstructions(instructionsString){ + let item = []; + let chunkSize = 22; + //remove all space line and other to avoid problem with text + instructionsString = instructionsString.replace(/[\n\r]/g, ''); + + for (let i = 0; i < instructionsString.length; i += chunkSize) { + const chunk = instructionsString.substring(i, i + chunkSize).trim(); + item.push({ name: chunk, fun: "" }); + + if (item.length === maxLenghtvertical) { + let screen = { + name: "Instructions", + items: item, + }; + screens.push(screen); + item = []; + } + } + + if (item.length > 0) { + let screen = { + name: "Instructions", + items: item, + }; + screens.push(screen); + } +} + + +// Get json format and parse it into Strings +function getRecipeData(data) { + let mealName = data.strMeal; + let category = data.strCategory; + let area = data.strArea; + let instructions = data.strInstructions; + const ingredients = []; + const measures = []; + for (let i = 1; i <= 20; i++) { + const ingredient = data["strIngredient" + i]; + const measure = data["strMeasure" + i]; + if (ingredient && ingredient.trim() !== "") { + ingredients.push(ingredient); + if (measure && measure.trim() !== ""){ + measures.push(measure); + }else{ + measures.push("¯\\_(ツ)_/¯"); + } + } else { // If no more ingredients are found + screens = [ + { + name: "General", + items: [ + {name: mealName, fun: ""}, + {name: "", fun: ""}, + {name: "Category", fun: category}, + {name: "", fun: ""}, + {name: "Area: ", fun: area}, + ] + } + ]; + infoIngredients(ingredients, measures); + infoInstructions(instructions); + drawInfo(); + break; + } + } +} + +function jsonData(){ + let json = '{"meals":[{"idMeal":"52771","strMeal":"Spicy Arrabiata Penne","strDrinkAlternate":null,"strCategory":"Vegetarian","strArea":"Italian","strInstructions":"Bring a large pot of water to a boil. Add kosher salt to the boiling water, then add the pasta. Cook according to the package instructions, about 9 minutes.\\r\\nIn a large skillet over medium-high heat, add the olive oil and heat until the oil starts to shimmer. Add the garlic and cook, stirring, until fragrant, 1 to 2 minutes. Add the chopped tomatoes, red chile flakes, Italian seasoning and salt and pepper to taste. Bring to a boil and cook for 5 minutes. Remove from the heat and add the chopped basil.\\r\\nDrain the pasta and add it to the sauce. Garnish with Parmigiano-Reggiano flakes and more basil and serve warm.","strMealThumb":"https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg","strTags":"Pasta,Curry","strYoutube":"https://www.youtube.com/watch?v=1IszT_guI08","strIngredient1":"penne rigate","strIngredient2":"olive oil","strIngredient3":"garlic","strIngredient4":"chopped tomatoes","strIngredient5":"red chile flakes","strIngredient6":"italian seasoning","strIngredient7":"basil","strIngredient8":"Parmigiano-Reggiano","strIngredient9":"","strIngredient10":"","strIngredient11":"","strIngredient12":"","strIngredient13":"","strIngredient14":"","strIngredient15":"","strIngredient16":null,"strIngredient17":null,"strIngredient18":null,"strIngredient19":null,"strIngredient20":null,"strMeasure1":"1 pound","strMeasure2":"1/4 cup","strMeasure3":"3 cloves","strMeasure4":"1 tin ","strMeasure5":"1/2 teaspoon","strMeasure6":"1/2 teaspoon","strMeasure7":"6 leaves","strMeasure8":"spinkling","strMeasure9":"","strMeasure10":"","strMeasure11":"","strMeasure12":"","strMeasure13":"","strMeasure14":"","strMeasure15":"","strMeasure16":null,"strMeasure17":null,"strMeasure18":null,"strMeasure19":null,"strMeasure20":null,"strSource":null,"strImageSource":null,"strCreativeCommonsConfirmed":null,"dateModified":null}]}'; + if(settings != null){ + json = JSON.stringify({ meals: [settings] }); + } + const obj = JSON.parse(json); + + getRecipeData(obj.meals[0]); +} + +function initData(retryCount) { + if (!Bangle.http) { + console.log("No http method found"); + jsonData(); + return; + } + jsonData(); + Bangle.http(uri, { timeout: 1000 }) + .then(event => { + try { + const obj = JSON.parse(event.resp); + + if (obj.meals && obj.meals.length > 0) { + getRecipeData(obj.meals[0]); + } else { + console.log("Invalid JSON structure: meals array is missing or empty"); + } + } catch (error) { + console.log("JSON Parse Error: " + error.message); + } + }) + .catch(e => { + console.log("Request Error:", e); + if (e === "Timeout" && retryCount > 0) { + setTimeout(() => initData(retryCount - 1), 1000); // Optional: Add a delay before retrying + }else{ + jsonData(); + } + }); +} + +initData(3); + + +Bangle.on('lock', function(isLocked) { + drawInfo(); +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/followtherecipe/icon.png b/apps/followtherecipe/icon.png new file mode 100644 index 000000000..a2a94bb88 Binary files /dev/null and b/apps/followtherecipe/icon.png differ diff --git a/apps/followtherecipe/interface.html b/apps/followtherecipe/interface.html new file mode 100644 index 000000000..19e1edd10 --- /dev/null +++ b/apps/followtherecipe/interface.html @@ -0,0 +1,146 @@ + + + + + + +

Choose your recipe

+

+ +

Recipe to be imported to BangleJs: -

+ +

+

+ +

+ +
+ +
+ + + + diff --git a/apps/followtherecipe/metadata.json b/apps/followtherecipe/metadata.json new file mode 100644 index 000000000..0c1de0817 --- /dev/null +++ b/apps/followtherecipe/metadata.json @@ -0,0 +1,30 @@ +{ + "id": "followtherecipe", + "name": "Follow The Recipe", + "shortName":"FTR", + "icon": "icon.png", + "version": "0.02", + "description": "Follow The Recipe (FTR) is a bangle.js app to follow a recipe step by step", + "type": "app", + "tags": "tool, tools, cook", + "supports": [ + "BANGLEJS2" + ], + "allow_emulator": true, + "interface": "interface.html", + "readme": "README.md", + "data": [ + {"name":"followtherecipe.json"} + ], + "storage": [ + { + "name": "followtherecipe.app.js", + "url": "app.js" + }, + { + "name": "followtherecipe.img", + "url": "app-icon.js", + "evaluate": true + } + ] +} \ No newline at end of file diff --git a/apps/followtherecipe/screenshot1.png b/apps/followtherecipe/screenshot1.png new file mode 100644 index 000000000..6b4438e46 Binary files /dev/null and b/apps/followtherecipe/screenshot1.png differ diff --git a/apps/fontall/ChangeLog b/apps/fontall/ChangeLog new file mode 100644 index 000000000..1ba08319d --- /dev/null +++ b/apps/fontall/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Corrected formatting of punctuation \ No newline at end of file diff --git a/apps/fontall/README.md b/apps/fontall/README.md new file mode 100644 index 000000000..9b53058d9 --- /dev/null +++ b/apps/fontall/README.md @@ -0,0 +1,22 @@ +# Fonts (all languages) + +This library provides an international font that can be used to display messages. + +The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html). +All characters from Unicode codepoint 32 up until codepoint 65535 (U+FFFF) are included here, +which should be enough for most languages. + +**The font is 2MB and takes a while to upload** - if you don't require all the languages +it provides, consider installing another Font library like [extended fonts](https://banglejs.com/apps/?id=fontsext) +that contains just the characters you need instead. + +## Usage + +See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference) +for more information on fonts. + + +## Recreating font.pbf + +* Go to `bin` directory +* Run `./font_creator.js "All" ../apps/fontall/font.pbf` \ No newline at end of file diff --git a/apps/fontall/app.png b/apps/fontall/app.png new file mode 100644 index 000000000..e029647b7 Binary files /dev/null and b/apps/fontall/app.png differ diff --git a/apps/fontall/boot.js b/apps/fontall/boot.js new file mode 100644 index 000000000..07b99570e --- /dev/null +++ b/apps/fontall/boot.js @@ -0,0 +1 @@ +Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontall.pbf")); }; \ No newline at end of file diff --git a/apps/fontall/font.pbf b/apps/fontall/font.pbf new file mode 100644 index 000000000..02345589e Binary files /dev/null and b/apps/fontall/font.pbf differ diff --git a/apps/fontall/lib.js b/apps/fontall/lib.js new file mode 100644 index 000000000..8d5caf366 --- /dev/null +++ b/apps/fontall/lib.js @@ -0,0 +1,3 @@ +exports.getFont = (options) => { + return "Intl"; // placeholder for now - see https://github.com/espruino/BangleApps/issues/3109 +}; \ No newline at end of file diff --git a/apps/fontall/metadata.json b/apps/fontall/metadata.json new file mode 100644 index 000000000..9f9683714 --- /dev/null +++ b/apps/fontall/metadata.json @@ -0,0 +1,16 @@ +{ "id": "fontall", + "name": "Fonts (all languages)", + "version":"0.02", + "description": "Installs a font containing over 50,000 Unifont characters for Chinese, Japanese, Korean, Russian, and more. **Requires 2MB storage**", + "icon": "app.png", + "tags": "font,fonts,language", + "type": "module", + "provides_modules" : ["font"], + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"font","url":"lib.js"}, + {"name":"fontall.boot.js","url":"boot.js"}, + {"name":"fontall.pbf","url":"font.pbf"} + ] +} diff --git a/apps/fontext/ChangeLog b/apps/fontext/ChangeLog new file mode 100644 index 000000000..689986121 --- /dev/null +++ b/apps/fontext/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Corrected formatting of punctuation +0.03: Extend range slightly to include Ukrainian fonts diff --git a/apps/fontext/README.md b/apps/fontext/README.md new file mode 100644 index 000000000..f8f99f3a4 --- /dev/null +++ b/apps/fontext/README.md @@ -0,0 +1,25 @@ +# Fonts (extended) + +This library provides an international font that can be used to display messages. + +The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html). +All characters from Unicode codepoint 32 up until codepoint 1103 (U+044F) are included here, +which should be enough for [around 90% of languages](https://arxiv.org/pdf/1801.07779.pdf#page=5) +but **not** Chinese/Japanese/Korean. + +The font is 20kb so is far more sensible than the [2MB all regions](https://banglejs.com/apps/?id=fontsall) font +if you don't require non-latin languages. + + +https://arxiv.org/pdf/1801.07779.pdf#page=5 + +## Usage + +See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference) +for more information on fonts. + + +## Recreating font.pbf + +* Go to `bin` directory +* Run `./font_creator.js "Extended" ../apps/fontext/font.pbf` \ No newline at end of file diff --git a/apps/fontext/app.png b/apps/fontext/app.png new file mode 100644 index 000000000..e029647b7 Binary files /dev/null and b/apps/fontext/app.png differ diff --git a/apps/fontext/boot.js b/apps/fontext/boot.js new file mode 100644 index 000000000..e52483e16 --- /dev/null +++ b/apps/fontext/boot.js @@ -0,0 +1 @@ +Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontext.pbf")); }; \ No newline at end of file diff --git a/apps/fontext/font.pbf b/apps/fontext/font.pbf new file mode 100644 index 000000000..05c46fcd0 Binary files /dev/null and b/apps/fontext/font.pbf differ diff --git a/apps/fontext/lib.js b/apps/fontext/lib.js new file mode 100644 index 000000000..8d5caf366 --- /dev/null +++ b/apps/fontext/lib.js @@ -0,0 +1,3 @@ +exports.getFont = (options) => { + return "Intl"; // placeholder for now - see https://github.com/espruino/BangleApps/issues/3109 +}; \ No newline at end of file diff --git a/apps/fontext/metadata.json b/apps/fontext/metadata.json new file mode 100644 index 000000000..c39da573a --- /dev/null +++ b/apps/fontext/metadata.json @@ -0,0 +1,17 @@ +{ "id": "fontext", + "name": "Fonts (150+ languages)", + "version":"0.03", + "description": "Installs a font containing 1000 Unifont characters, which should handle the majority of non-Chinese/Japanese/Korean languages (only 20kb)", + "icon": "app.png", + "tags": "font,fonts,language", + "type": "module", + "provides_modules" : ["font"], + "default" : true, + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"font","url":"lib.js"}, + {"name":"fontext.boot.js","url":"boot.js"}, + {"name":"fontext.pbf","url":"font.pbf"} + ] +} diff --git a/apps/fontkorean/ChangeLog b/apps/fontkorean/ChangeLog new file mode 100644 index 000000000..e595c00da --- /dev/null +++ b/apps/fontkorean/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Corrected formatting of punctuation \ No newline at end of file diff --git a/apps/fontkorean/README.md b/apps/fontkorean/README.md new file mode 100644 index 000000000..6d9eecd45 --- /dev/null +++ b/apps/fontkorean/README.md @@ -0,0 +1,17 @@ +# Fonts (Korean) + +This library provides an Korean font that can be used to display messages. + +The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html). +Korean characters from Unicode codepoint 32-255, 0x1100-0x11FF, 0x3130-0x318F, 0xA960-0xA97F + +## Usage + +See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference) +for more information on fonts. + + +## Recreating fontkorean.pbf + +* Go to `bin` directory +* Run `./font_creator.js "Korean" ../apps/fontkorean/font.pbf` diff --git a/apps/fontkorean/app.png b/apps/fontkorean/app.png new file mode 100644 index 000000000..a4b02ea3a Binary files /dev/null and b/apps/fontkorean/app.png differ diff --git a/apps/fontkorean/boot.js b/apps/fontkorean/boot.js new file mode 100644 index 000000000..5f3a24433 --- /dev/null +++ b/apps/fontkorean/boot.js @@ -0,0 +1 @@ +Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontkorean.pbf")); }; \ No newline at end of file diff --git a/apps/fontkorean/font.pbf b/apps/fontkorean/font.pbf new file mode 100644 index 000000000..8a9e44051 Binary files /dev/null and b/apps/fontkorean/font.pbf differ diff --git a/apps/fontkorean/lib.js b/apps/fontkorean/lib.js new file mode 100644 index 000000000..bcb3b4bca --- /dev/null +++ b/apps/fontkorean/lib.js @@ -0,0 +1,3 @@ +exports.getFont = (options) => { + return "Intl"; // placeholder for now - see https://github.com/espruino/BangleApps/issues/3109 +}; \ No newline at end of file diff --git a/apps/fontkorean/metadata.json b/apps/fontkorean/metadata.json new file mode 100644 index 000000000..2ed0c7545 --- /dev/null +++ b/apps/fontkorean/metadata.json @@ -0,0 +1,16 @@ +{ "id": "fontkorean", + "name": "Fonts (Korean)", + "version":"0.02", + "description": "Installs a font data, Unifont characters for Korean **Requires 420 KB storage**", + "icon": "app.png", + "tags": "font,fonts,language", + "type": "module", + "provides_modules" : ["font"], + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"font","url":"lib.js"}, + {"name":"fontkorean.boot.js","url":"boot.js"}, + {"name":"fontkorean.pbf","url":"font.pbf"} + ] +} diff --git a/apps/forge/ChangeLog b/apps/forge/ChangeLog new file mode 100644 index 000000000..ade90fc36 --- /dev/null +++ b/apps/forge/ChangeLog @@ -0,0 +1,3 @@ +0.01: attempt to import +0.02: Make it possible for Fastload Utils to fastload into this app. +0.03: Minor code improvements diff --git a/apps/forge/README.md b/apps/forge/README.md new file mode 100644 index 000000000..5a9d95f99 --- /dev/null +++ b/apps/forge/README.md @@ -0,0 +1,8 @@ +# App Forge + +This should help with your hacks. Sometimes, you want to work on an +application, you'd want to use the stable version, but you'd also want +to use latest development version. + +Well, this makes it easy. Just save your development version as +a.name.js, and you should be able to run it from the menu system. diff --git a/apps/forge/app-icon.js b/apps/forge/app-icon.js new file mode 100644 index 000000000..c7c3959f0 --- /dev/null +++ b/apps/forge/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIhe/AEDgOKAocDwgFDgUEAokKAokTAohDEg0hgEgAoMEoMIoAFCgME4AFCwUCwAFBgoeChEAg8GAoMYEYMECIM4AoMMgFAuEAhv4gkg+EAhPghExAoIACg4FEh4FEj4FEn56Ev/8iAFC///CQUBAoPgQoQFBLYUHAoJbCh4FBFwf//wuD//8Fwf/GoYuNAoUGGggMCGgQeCbIl+Aol8Aol4Aoh2EgFgf5kAA")) diff --git a/apps/forge/app.png b/apps/forge/app.png new file mode 100644 index 000000000..8d4c44b24 Binary files /dev/null and b/apps/forge/app.png differ diff --git a/apps/forge/forge.app.js b/apps/forge/forge.app.js new file mode 100644 index 000000000..5169a04e9 --- /dev/null +++ b/apps/forge/forge.app.js @@ -0,0 +1,34 @@ +// App Forge + +"Bangle.loadWidgets()"; // Facilitates fastloading to this app via Fastload Utils, while still not loading widgets on standard `load` calls. + +const st = require('Storage'); + +let l = /^a\..*\.js$/; +//l = /.*\.js/; +l = st.list(l, {sf:false}); + +print(l); + +function on_load(x) { + print("Loading", x); + Bangle.buzz(50, 1); // Won't happen because load() is quicker + g.reset().clear() + .setFont("Vector", 40) + .drawString("Loading", 0, 30) + .drawString(x, 0, 80); + g.flip(); + load(x); +} + +var menu = { + "< Back" : Bangle.load +}; +if (l.length==0) Object.assign(menu, {"No apps":""}); +else for (let id in l) { + let i = id; + menu[l[id]]=()=>{ on_load(l[i]); }; +} + +g.clear(); +E.showMenu(menu); diff --git a/apps/forge/metadata.json b/apps/forge/metadata.json new file mode 100644 index 000000000..43baca169 --- /dev/null +++ b/apps/forge/metadata.json @@ -0,0 +1,13 @@ +{ "id": "forge", + "name": "App Forge", + "version": "0.03", + "description": "Easy way to run development versions of your apps", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "tool", + "storage": [ + {"name":"forge.app.js","url":"forge.app.js"}, + {"name":"forge.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/ftclock/mkFourTwentyTz.js b/apps/ftclock/mkFourTwentyTz.js index 4e7829aa3..7c6d25995 100644 --- a/apps/ftclock/mkFourTwentyTz.js +++ b/apps/ftclock/mkFourTwentyTz.js @@ -1,3 +1,5 @@ +/* This file is designed to be run on the desktop, not Bangle.js */ +/* eslint-env node */ let fs = require('fs'); let csv = require('csv'); @@ -30,7 +32,7 @@ fs.createReadStream(__dirname+'/country.csv') } else { country = countries[r[1]]; // e.g. United States } - zone = zones[r[0]] || { "name": `${city}, ${country}` }; + let zone = zones[r[0]] || { "name": `${city}, ${country}` }; let starttime = parseInt(r[3] || "0"), // Bugger. They're feeding us blanks for UTC now offs = parseInt(r[4]); if (offs<0) { @@ -43,15 +45,15 @@ fs.createReadStream(__dirname+'/country.csv') zones[r[0]] = zone; }) .on('end', () => { - for (z in zones) { - zone = zones[z]; + for (let z in zones) { + let zone = zones[z]; if (zone.offs%60) continue; // One a dem funky timezones. Ignore. - zonelist = offsdict[zone.offs] || []; + let zonelist = offsdict[zone.offs] || []; zonelist.push(zone.name); offsdict[zone.offs] = zonelist; } - offsets = []; - for (o in offsdict) { + let offsets = []; + for (let o in offsdict) { offsets.unshift(parseInt(o)); } fs.open("fourTwentyTz.js","w", (err, fd) => { @@ -67,7 +69,7 @@ fs.createReadStream(__dirname+'/country.csv') fs.write(fd, ";\n", handleWrite); fs.write(fd, "exports.timezones = function(offs) {\n", handleWrite); fs.write(fd, " switch (offs) {\n", handleWrite); - for (i=0; i=R.w) { - slideX=R.w; - stop = true; + let draw = function() { + time_string = getTimeString(new Date()).replace('*', ''); + //print(time_string); + if (time_string != time_string_old) { + g.setFont('Vector', R.h/text_scale).setFontAlign(0, 0); + if (settings.animate) { + animate(3); + } else { + quickDraw(); + } } - //draw shifted new time - g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2); - g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2); - if (stop) { - time_string_old = time_string; - clearInterval(animInterval); - animInterval=undefined; - time_string_old_wrapped = time_string_new_wrapped; - } - print(Math.round((getTime() - time_start)*1000)) - }, 30); -}; + queueDraw(timeout); + }; -g.clear(); -loadSettings(); - -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ - if (on) { - draw(); // draw immediately, queue redraw - } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - } -}); - -Bangle.setUI({ - mode : 'clock', - remove : function() { - // Called to unload all of the clock app - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; + let animate = function(step) { if (animInterval) clearInterval(animInterval); - animInterval = undefined; - require('widget_utils').show(); // re-show widgets + let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n"); + let slideX = 0; + //don't let pulling the drawer change y + let text_y = R.y + R.h/2; + animInterval = setInterval(function() { + //blank old time + g.setColor(g.theme.bg); + g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, text_y); + g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, text_y); + g.setColor(g.theme.fg); + slideX += step; + let stop = false; + if (slideX>=R.w) { + slideX=R.w; + stop = true; + } + //draw shifted new time + g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, text_y); + g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, text_y); + if (stop) { + time_string_old = time_string; + clearInterval(animInterval); + animInterval=undefined; + time_string_old_wrapped = time_string_new_wrapped; + } + //print(Math.round((getTime() - time_start)*1000)); + }, 30); + }; + + let quickDraw = function() { + let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n"); + g.setColor(g.theme.bg); + g.drawString(time_string_old_wrapped, R.x + R.w/2, R.y + R.h/2); + g.setColor(g.theme.fg); + g.drawString(time_string_new_wrapped, R.x + R.w/2, R.y + R.h/2); + time_string_old_wrapped = time_string_new_wrapped; + }; + + g.clear(); + loadSettings(); + + // Stop updates when LCD is off, restart when on + Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } + }); + + Bangle.setUI({ + mode : 'clock', + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + if (animInterval) clearInterval(animInterval); + animInterval = undefined; + require('widget_utils').show(); // re-show widgets + } + }); + + Bangle.loadWidgets(); + if (settings.showWidgets) { + Bangle.drawWidgets(); + } else { + require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe } -}); -Bangle.loadWidgets(); -if (settings.showWidgets) { - Bangle.drawWidgets(); -} else { - require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe + let R = Bangle.appRect; + draw(); } - -R = Bangle.appRect; -draw(); -} \ No newline at end of file diff --git a/apps/fuzzyw/fuzzyw.settings.js b/apps/fuzzyw/fuzzyw.settings.js index 8feb30bfb..2cb693a84 100644 --- a/apps/fuzzyw/fuzzyw.settings.js +++ b/apps/fuzzyw/fuzzyw.settings.js @@ -1,33 +1,39 @@ (function(back) { - const SETTINGS_FILE = "fuzzy.settings.json"; + const SETTINGS_FILE = "fuzzyw.settings.json"; // initialize with default settings... - let s = {'showWidgets': false} + let s = {'showWidgets': false, 'animate': true}; // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings - const storage = require('Storage') + const storage = require('Storage'); let settings = storage.readJSON(SETTINGS_FILE, 1) || s; - const saved = settings || {} + const saved = settings || {}; for (const key in saved) { - s[key] = saved[key] + s[key] = saved[key]; } function save() { - settings = s - storage.write(SETTINGS_FILE, settings) + settings = s; + storage.write(SETTINGS_FILE, settings); } E.showMenu({ '': { 'title': 'Fuzzy Word Clock' }, '< Back': back, 'Show Widgets': { - value: settings.showWidgets, - format: () => (settings.showWidgets ? 'Yes' : 'No'), + value: s.showWidgets, onchange: () => { - settings.showWidgets = !settings.showWidgets; + s.showWidgets = !s.showWidgets; save(); } }, + 'Animate': { + value: s.animate, + onchange: () => { + s.animate = !s.animate; + save(); + } + } }); }) diff --git a/apps/fuzzyw/metadata.json b/apps/fuzzyw/metadata.json index 97f060866..110526712 100644 --- a/apps/fuzzyw/metadata.json +++ b/apps/fuzzyw/metadata.json @@ -2,7 +2,7 @@ "id":"fuzzyw", "name":"Fuzzy Text Clock", "shortName": "Fuzzy Text", - "version": "0.04", + "version": "0.05", "description": "An imprecise clock for when you're not in a rush", "readme": "README.md", "icon":"fuzzyw.png", diff --git a/apps/fwupdate/bootloader_espruino_2v20_banglejs2.hex b/apps/fwupdate/bootloader_espruino_2v20_banglejs2.hex new file mode 100644 index 000000000..d64a9ffd4 --- /dev/null +++ b/apps/fwupdate/bootloader_espruino_2v20_banglejs2.hex @@ -0,0 +1,1536 @@ +:02000002F0000C +:10700000F0FF032039730F0061730F0063730F00EB +:1070100065730F0067730F0069730F0000000000B5 +:1070200000000000000000000000000021BB0F0075 +:107030006D730F00000000006F730F0071730F007D +:1070400073730F0073730F0073730F0073730F006C +:1070500073730F0073730F0073730F0073730F005C +:1070600073730F0073730F0073730F0073730F004C +:1070700073730F0073730F0073730F0073730F003C +:1070800009BB0F0055BD0F0073730F0073730F0022 +:1070900073730F0073730F0091BB0F0073730F00B6 +:1070A00073730F0073730F0073730F0073730F000C +:1070B00073730F0073730F000000000000000000E6 +:1070C00073730F0073730F0073730F0073730F00EC +:1070D00073730F0073730F0073730F0073730F00DC +:1070E00073730F0073730F0073730F0000000000C1 +:1070F0000000000073730F000000000073730F00A6 +:10710000000000000000000000000000000000007F +:10711000000000000000000000000000000000006F +:10712000000000000000000000000000000000005F +:10713000000000000000000000000000000000004F +:10714000000000000000000000000000000000003F +:10715000000000000000000000000000000000002F +:10716000000000000000000000000000000000001F +:10717000000000000000000000000000000000000F +:1071800000000000000000000000000000000000FF +:1071900000000000000000000000000000000000EF +:1071A00000000000000000000000000000000000DF +:1071B00000000000000000000000000000000000CF +:1071C00000000000000000000000000000000000BF +:1071D00000000000000000000000000000000000AF +:1071E000000000000000000000000000000000009F +:1071F000000000000000000000000000000000008F +:1072000010B5054C237833B9044B13B10448AFF3E0 +:1072100000800123237010BD585900200000000099 +:107220003CCD0F0008B5034B1BB103490348AFF336 +:10723000008008BD000000005C5900203CCD0F001C +:10724000154B002B08BF134B9D46A3F5803A002138 +:107250008B460F461348144A121A00F063F80F4B7E +:10726000002B00D098470E4B002B00D098470020F1 +:10727000002104000D000D48002802D00C48AFF397 +:10728000008000F02BF82000290004F06DFD00F0D4 +:1072900011F800BF00000800F0FF0320000000000C +:1072A0000000000058590020D47B0020000000009E +:1072B0000000000008B5074B044613B10021AFF3EE +:1072C0000080054B1868836A03B19847204600F098 +:1072D00031F800BF0000000034C70F0070B50D4D3D +:1072E0000D4C641BA4100026A64209D10B4D0C4C7A +:1072F00005F014FA641BA4100026A64205D170BD47 +:1073000055F8043B98470136EEE755F8043B98479B +:107310000136F2E73859002038590020385900204A +:107320003C59002002440346934200D1704703F8C1 +:10733000011BF9E7FEE700BF0649074A074B9B1A06 +:1073400003DD043BC858D050FBDC04F03DFCFFF7E4 +:1073500077FF0000A0CD0F00B8570020585900203B +:10736000FEE7FEE7FEE7FEE7FEE7FEE7FEE7FEE7F5 +:10737000FEE7FEE72DE9F047194C07461D4620467B +:10738000184B63608846914601F096FBA24620BBED +:10739000154E2660FFB1B8F1000F08D1DAF80430BD +:1073A0002968DA6891420FD248F2145015E06368F8 +:1073B00042465B6839462046984770B92368B34215 +:1073C000ECD048F2025008E02A4649469B6804483F +:1073D000BDE8F047184748F21050BDE8F08700BFFD +:1073E000C0740020ACCA0F006E52464870B5EFF36F +:1073F000108672B60D4A94680123A4B993600C4BB1 +:107400000C4DD3F8801029401160C3F88050D3F898 +:10741000841051604FF0FF32C3F88420047006B925 +:1074200062B6002070BD0370F9E700BF50780020FD +:1074300000E100E0FC06FFBD38B5074C05462068BA +:1074400018B1294601F030FB18B923682B6025607C +:1074500038BD2468F3E700BF8061002010B5064BFB +:10746000002253E8001F01EA020043E8000494F000 +:10747000000FF6D110BD00BF907400200A4AD2F868 +:1074800000347BB109480023D2F80815D940C90758 +:1074900048BF03F5C07103F1010348BF42F8210062 +:1074A000072BF1D1704700BF000001403546526EF6 +:1074B000024B1B780BB1FFF7E1BF70474878002003 +:1074C000014B01221A7070479474002000B10047EC +:1074D000704741DF70470000BFF34F8F0549064BEF +:1074E000CA6802F4E0621343CB60BFF34F8F00BF62 +:1074F000FDE700BF00ED00E00400FA0508B5FFF766 +:10750000EBFF43681A44426011B101399A4201D13C +:107510000120704713F8010B01F8010FF6E7000096 +:107520005B689A68054B9A4207D1D0E90123044968 +:1075300001320A60034A013B1360704700C90F0023 +:10754000407500203C75002043F204031A68084B84 +:107550009A4201BF43F20803186800F6FF7020F456 +:107560007F600CBF20F00F004FF48050704700BFC9 +:10757000DBE5B15108B5FFF7E7FF054B9B69184400 +:1075800000F6FF7020F47F6020F00F0008BD00BF00 +:1075900060780020F8B5094E094CA41B0746E408A2 +:1075A00000250436A54201D10020F8BD56F835303B +:1075B000384698470028F8D10135F3E740CD0F0051 +:1075C00048CD0F0029DF7047014B06201B68184784 +:1075D0006C75002018DF704743690BB1C06818470D +:1075E000704769B10B78022B0CD1022804D0032814 +:1075F0000CBF11200820704703230B700020704738 +:107600000E2070470820704738B1054B0360054BCA +:10761000436001230372002070470E20704700BFB3 +:1076200095810F00E3750F0013DF704766DF704729 +:1076300062DF7047B5DF7047B1DF7047B0DF70477A +:10764000AEDF70477FB50023CDF80A30ADF80E30BD +:107650000A4BADF806109B89ADF8083001238DF870 +:107660000A300DF10603CDE90430054B02A9188854 +:10767000FFF7E6FF07B05DF804FB00BF387200209B +:107680002A59002010B5044686B01122002101A815 +:10769000FFF748FE2378082B0BD1364A364913886A +:1076A0000988013B9BB21380D1B1CBB90323118070 +:1076B0002370627823788DF805306021012A8DF8D7 +:1076C00004108DF806200DD00B2A11D12B4B002170 +:1076D0001A788DF807201970042101A8FFF7B2FF6E +:1076E00006B010BD062B05D0082B29D0032B27D0C0 +:1076F0000321F2E7E1688DF80710D4E90123080AB5 +:107700008DF80800080C090E8DF80A10110A8DF882 +:107710000B208DF80C10110C120E8DF80E201A0A89 +:107720008DF80F308DF810201A0C1B0E8DF80D10EF +:107730008DF809008DF811208DF812300F21CCE75B +:10774000D4E90123110A8DF807208DF80810110CD7 +:10775000120E8DF80A201A0A8DF80B308DF80C20C5 +:107760001A0C1B0E8DF809108DF80D208DF80E30B7 +:107770000B21B2E70277002000770020647500201B +:10778000ADDF7047AADF7047A8DF704790DF704712 +:107790008FDF704786DF70477FDF70477DDF704780 +:1077A0007CDF70477ADF704776DF704775DF7047A0 +:1077B00074DF704773DF704772DF704730B518228F +:1077C0008DB000210DEB0200FFF7ACFD28230893DC +:1077D0001623ADF806300023CDF80E30CDF8123068 +:1077E000ADF816301A4B02930222062109251A70B1 +:1077F00099701A71032159220124D9705A710DF11F +:107800000601FE2258198DF818408DF82D40ADF86C +:107810000C505C709A71FFF7C1FFB8B9BDF8062033 +:107820001D721119D971BDF80C301A44094BADF80D +:107830000C2002A906AA1846FFF7BEFF30B9187837 +:10784000FFF7B6FF21461878FFF7B4FF0DB030BD43 +:1078500064720020285900206DDF70476CDF70478C +:10786000062810B5044602D10020FFF793FE044B12 +:107870001B681BB12046BDE81040184710BD00BF73 +:107880002478002010B5012203680446984710B1FF +:10789000A368013BA36010BD10B50B689A684B68E4 +:1078A0000024C0E90243C0E9002294782344036124 +:1078B00092F90340234443614B68036101F10C03D7 +:1078C0004361537803F0C003802B04BF043101618E +:1078D00010BD2DE9F347002580460F462E462C4665 +:1078E000D8F8083023B30DF107014046FFF7CAFF6F +:1078F000F0B19DF807C00CF07F025FFA82F9A4F1A5 +:107900002003C4F1200109FA03F329FA01F109FA6D +:1079100004F20B431CF0800F45EA020546EA030619 +:1079200004F1070402D1C7E9005602E0462CD7D182 +:10793000002002B0BDE8F087F7B50F46694616464D +:10794000FFF7C7FFB0B1DDE90013002501F0010426 +:107950004A0842EAC37254EA05014FEA530314BFCE +:10796000DD431D463B7914BFD4431446082B03D195 +:10797000C6E9004503B0F0BD042B08D134602246AF +:10798000D317AB4206BFA24201200020F2E7022B30 +:1079900003D123B233801AB2F2E7012B03D163B2D1 +:1079A00033705AB2ECE70020E4E773B50C46694641 +:1079B0001646FFF78EFF68B12379082B0CD1DDE95D +:1079C0000045C6E90045DDE90023AB4206BFA242FF +:1079D0000120002002B070BD042B04D1009B336055 +:1079E0001C460025EFE7022B4FF0000004D1009B5E +:1079F00033809CB20546E6E7012BEBD1009B337048 +:107A000003F0FF04F6E737B50D4669461446FFF765 +:107A100060FF30B12A79082ADDE9003103D1C4E9D9 +:107A2000003103B030BD042A18464FEAE37108D193 +:107A300023601A460B468B4206BF8242012000207B +:107A4000EFE7022A04D11BB223801AB2D317F2E760 +:107A5000012A03D15BB223705AB2F7E70020E0E7B6 +:107A60002DE9F34183680746884623B9002630464E +:107A700002B0BDE8F0810DF10701FFF703FF0646F4 +:107A80000028F3D09DF80740220616D504F07F04A5 +:107A90000725BB68002BE9D00DF107013846FFF739 +:107AA000F1FE0028E2D09DF8072002F07F03AB40F2 +:107AB0001C43130605F1070502D4C8F80040D6E7B9 +:107AC000232DE6D1D2E773B50D4601A90446FFF791 +:107AD000C7FF064618B90026304602B070BD94E8CC +:107AE0000F0085E80F0001998A42F4D3A960A368CA +:107AF0005B1AA360F0E7000030B5044685B0154678 +:107B0000A9B90268114B9A4211D0102D06D82A4605 +:107B100069462046FFF7F0FF05B030BD10226946E8 +:107B20002046FFF7E9FF0028F6D0103DEDE7A368F7 +:107B3000AB4201D20020EFE723682A46204698474F +:107B40000028F7D0A3685D1BA560E5E703750F006B +:107B5000F7B50E4601A907461546FFF781FFA8B104 +:107B6000019A0123D3184FF0000404D2717801F078 +:107B7000C001802901D1204608E031799942FAD329 +:107B800029463846FFF7B8FF019BEC5403B0F0BD1F +:107B900073B50D4601A906461446FFF761FF90B183 +:107BA000019AFF2A11D86B7803F0C003802B02F1F1 +:107BB00001010AD02B798B4207D32146304601F8C8 +:107BC000012BFFF799FF02B070BD0020FBE713B552 +:107BD000144669460822FFF78FFF18B1DDE900233C +:107BE000C4E9002302B010BD13B5144604220DEB06 +:107BF0000201FFF781FF08B1019B236002B010BDB5 +:107C000013B5044605291FD8DFE801F0030F151E40 +:107C10001E1C012201A92046FFF76EFF18B19DF935 +:107C20000430002BF5DB02B010BD082200212046F5 +:107C3000FFF762FFF7E701A9FFF712FF0028F2D074 +:107C4000019AF3E70422F1E70020ECE710B543685E +:107C50001A787AB103F10C0242601A7B62B90168AA +:107C6000C368C0E901128C782344036191F90310C1 +:107C70000B444361104620E059781A7901F0300432 +:107C8000302C1BD15C7B04F03004302C1ED1997851 +:107C90000269521A02610022597811F0300F02BFB6 +:107CA000816801318160997B114402690A44026153 +:107CB00093F90F301A444261012010BD01F0F00425 +:107CC000202C03D1597911FB02F2E5E701F0C00144 +:107CD000802908BF0422DFE7013A024401398242C9 +:107CE00000D2704711F8013F02F80139F7E74BDF86 +:107CF000704761DF704769DF704760DF704770472A +:107D000011DF704710DF70470F4B30B50124C3F807 +:107D10000445BFF36F8FBFF34F8F01EB8202401A10 +:107D2000914207D10022C3F80425BFF36F8FBFF340 +:107D30004F8F30BD441851F8045B2560D3F80044E0 +:107D4000002CFBD0ECE700BF00E001400A4B022210 +:107D5000C3F80425BFF36F8FBFF34F8FC3F8080537 +:107D6000D3F80024002AFBD00022C3F80425BFF377 +:107D70006F8FBFF34F8F704700E00140054B1B68CA +:107D8000082B01BF044B1868B0FA80F0400918BFF7 +:107D9000002070473001001034010010436843B1E7 +:107DA000026891680B4443605268934204BF002309 +:107DB000436070477FB50C4D0C4C0D4E01A8FFF78A +:107DC00096FF78B9B44218BF204602950390039BF2 +:107DD000002BF3D00198D3E90021904702A8FFF7C8 +:107DE000DDFFF4E704B070BD18CD0F0090CD0F009B +:107DF00098CD0F0030B5104DC7B04FF486740DF11B +:107E0000020103A8ADF80240FFF773FF88B90B4BDE +:107E10000B4A0195934208BF03460293029B002B35 +:107E2000EDD003A8D3E90021904701A8FFF7B6FFE2 +:107E3000F4E747B030BD00BF00CD0F0068CD0F00A4 +:107E400070CD0F00024B1B780BB1FFF7D3BF70470B +:107E50002078002013B50A4B00930A4A0A4B93423C +:107E600008BF002304460193019B0BB902B010BD6B +:107E70002046D3E9002190476846FFF78FFFF3E7DC +:107E800030CD0F0078CD0F0070CD0F0013B50C4B27 +:107E900000930C4A0C4B934208BF00230446019305 +:107EA000019808B902B010BDD0E9003120469847CA +:107EB00018B16846FFF772FFF2E71120F2E700BF42 +:107EC0000CCD0F0090CD0F0088CD0F00034B187A1A +:107ED0000122B0FA80F05A7440097047787400208B +:107EE000024B187A003018BF0120704778740020C8 +:107EF00000207047002070470846704708467047CA +:107F00000D4B10B5012143600C4B53E8002F42EAA2 +:107F1000010043E8000494F0000FF6D14AB9084983 +:107F200009781974074B08499A60C3E90012074A97 +:107F3000DA60002010BD00BFC8CB0F0078740020AD +:107F4000667500206074002084720020DC011C0033 +:107F500070B50568AB6886B004460E4673B114224E +:107F600000216846FFF7DEF9A3680593237901969F +:107F700033B1012B0ED0AB686846984706B070BD90 +:107F800001238DF8003023690293E368039363694A +:107F90000493F0E702238DF80030E3681B0302939B +:107FA000F5E728DF70470000024B1868003018BF63 +:107FB0000120704790740020F0B515461F4687B029 +:107FC00004460E46144B012253E8001F41EA02000A +:107FD00043E8000C9CF0000FF6D131B111200CE009 +:107FE00006EB0130FFF7B2FE01318D42F8D1FFF709 +:107FF00035FAA368002413B9184607B0F0BD022271 +:1080000068468DF80020CDE90146CDE90345059786 +:1080100098472046F1E700BF907400202DE9F04317 +:10802000174687B0814688461E460125134A52E806 +:10803000004F44EA050342E8003191F0000FF6D109 +:10804000CCB9B20839464046FFF75EFEFFF706FAA4 +:10805000D9F808201AB9104607B0BDE8F0830E9B86 +:108060008DF800506846CDE90148CDE903760593C7 +:1080700090472046F0E71120EEE700BF9074002003 +:1080800008B5FFF7EBF9002008BD0000014B436085 +:1080900000207047C8CB0F0013B550E8023F1FFA0D +:1080A000A3F4B0F806C0644502BFBFF32F8F0022CF +:1080B00011E0B0F80EC06444B0F80CC0644528BFAD +:1080C000A4EB0C04C3EA044440E8024CBCF1000FEA +:1080D000E3D14FF001028DF803200193019B9DF83D +:1080E00003000B6002B010BD13B550E8013F9CB215 +:1080F000B0F80EC06444B0F80CC0644528BFA4EBCF +:108100000C04B0F808C0644502BFBFF32F8F0022F3 +:1081100008E0C4EA030440E8014CBCF1000FE4D1DC +:108120004FF001028DF803200193019B9DF803009D +:108130000B6002B010BD50E8022FC2EA224240E8B4 +:108140000223002BF7D1704750E8012FC2EA024208 +:1081500040E80123002BF7D17047000030B53AB159 +:108160001368DB430A4D0144884204D1D84330BD33 +:108170004FF0FF33F6E710F8012B5340082213F0BD +:10818000010418BF2C46013A84EA5303F7D1EBE708 +:108190002083B8ED70B504460D4601B30B78042B6F +:1081A00004D8DFE803F01E05031E1E00112070BD79 +:1081B0000D4ED6F89C3190B1013312D10022182116 +:1081C0000430FFF7CBFF07222060214606F5CE7072 +:1081D000FFF7F8F90028EAD102232B70E7E70E2019 +:1081E000E5E70820E3E700BF00F00F0010B5084BFB +:1081F0009A685AB150B9EFF3108172B6054A1C68FB +:1082000014605C685460986001B962B6002010BDCB +:108210005078002000E100E013B5002304460DF182 +:1082200007008DF80730FFF7E1F8074A1368591C7B +:108230001160064AA41AE4091C709DF80700FFF7B4 +:10824000D5FF02B010BD00BF846100208C6100200A +:10825000FFF7E2BFF7B51B4F3B7873B31A4E01200F +:108260003070FFF713FE11284FF0000527D00220D1 +:10827000FFF7F0FD0DF107008DF80750FFF7B6F896 +:10828000FFF73EFD04469DF807003D70FFF7AEFF87 +:1082900054B90E4B34709A684AB11A6822F48002BD +:1082A0001A600320FFF7D6FD204603B0F0BD084B4F +:1082B0004FF48002C3F88020F3E70824F4E72C464B +:1082C000F2E700BF66750020657500205078002039 +:1082D00000E100E038B514490A6812F0010321D02A +:1082E000124B98421ED0124B18884FF6FF739842DB +:1082F00013D042F004020A601321FFF755FA90B937 +:108300000C4DC82445F001054FF47A40A847013CC4 +:10831000FAD1BDE83840FFF79DBF074B1878FFF74B +:1083200047FAF6E7002038BD8C74002040CD0F00DE +:108330002A590020B0C80F002859002073B51F4EDD +:108340003478002C38D11E4D012320462B70FFF7C6 +:108350009DFD112829D02046FFF77CFD0DF1070077 +:108360008DF80740FFF742F816491748FFF7CAFC97 +:108370000446B0FA80F0400930709DF80700FFF71E +:1083800035FF94B9114A2C7092F816335B09012B12 +:1083900008D9042B06D00E4B996849B11A6842F4EB +:1083A00080021A600120FFF755FD204602B070BD23 +:1083B0004FF480031360F5E70824F6E766750020A4 +:1083C00065750020FF7C0F00F0C70F0000E100E0A2 +:1083D0005078002038B5831E012B52D8294C237ABF +:1083E000002B4ED0022B09D1032833D0264B00227C +:1083F0001B68E2601A7942B1012A25D0637C002B08 +:1084000031D1BDE8384000F049B8D3E905128D1AE2 +:10841000142D94BF521914329A619142EED1022860 +:108420004FF0000308BF19462372174B18BF0D21E8 +:108430001868FFF78DFD154B1A885B889A42DDD1CD +:108440001348FFF778FED9E71A6901321A615B69B0 +:108450009A42E3E7E3680133082B01D8E360CDE7F4 +:108460000023E360DBE70B4B1B7853B10A4B1B780F +:108470001BB1BDE83840FFF7EDBEBDE83840FFF75F +:108480005DBF38BD787400207075002038750020FD +:10849000607400206575002066750020F8B5294CD1 +:1084A000294D237A83B9294F294E39463046FFF7A3 +:1084B000F3FD18B9286000236360F8BD7A8833683B +:1084C00013442B60002BF6D0022323722B681A79F9 +:1084D00092B1012A26D018460321FFF739FD002367 +:1084E00023726360194B1A885B889A42E5D1184859 +:1084F000BDE8F840FFF71FBED3E90520121A142A81 +:1085000028BF1422D3E903159208012A014438BF79 +:1085100001222844FFF756F850B11128DBD101237E +:108520002372CAE7D3E903021044FFF73AFDF3E7E9 +:10853000237C002BC1D10146BDE8F8400220FFF7A3 +:1085400049BF00BF78740020707500203875002086 +:108550006074002000F0FD03012B08D1044B0022C1 +:108560005A74421E504250411874FFF797BF70472B +:10857000787400200A4930B40B1D012253E8000F23 +:1085800040EA020443E8004595F0000FF6D120B917 +:108590004B7C13B930BCFFF781BF30BC704700BFC4 +:1085A0007874002073B5154C06460D46204601A987 +:1085B000FFF79AFDF0B1BDF804302268D11819D048 +:1085C000D654BDF80420BDF806304D609A420FD154 +:1085D0002046FFF7B9FD0A4B93F9043001215A09EF +:1085E00003F01F0301FA03F34032064941F8223039 +:1085F000002000E0042002B070BD00BFD077002052 +:108600002C59002000E100E0094B99420DD8094B9C +:1086100002611B68D3F804350B444360836803B1DF +:10862000816001460020FFF7BDBF0720704700BFF3 +:1086300017FCFF002C590020F8B5434F3B78044647 +:108640000D46164613BB4FF08043D3F81824D103D0 +:1086500037D53E4B3E4A1A600022C3E901223D4A0B +:10866000DA603D4A3D4953793D4841F823003D4891 +:1086700000EBC3031B7963B33B4A3C4BD36000213F +:1086800091603B4AD360012393603B7000232375C4 +:1086900021460120FFF786FF002D54D04FF40047FC +:1086A0004FF4FA7000214FF47A720023E7FB0501C2 +:1086B00003F0B2FE324601462046BDE8F840FFF71F +:1086C000A3BF2C4A01211160D3F81824D203FBD593 +:1086D000BFE71168C1F8083592F90430002BD9B210 +:1086E00028DADFF898C001F00F0EF4444FF0E00EE6 +:1086F0008CF818E05B099B0003F160434FF0010C1C +:1087000003F5614301F01F010CFA01F1C3F8801178 +:108710001960537900EBC30140F83320136881F8E6 +:1087200004C00222C3F84423C3F80423124B83F885 +:1087300000C0A1E703F1604C0CF5614C4FF0E00E76 +:108740008CF800E3D6E7F8BD7C610020D0770020EC +:1087500078770020580008002C590020B8740020B9 +:108760006D880F00307200204C720020CD740F0015 +:108770003078002008000040B5740020FCEC00E0D8 +:1087800070B5134DD5F80034EBB1D5F80445114B55 +:10879000640904EB440449F63F46B4428CBF00220E +:1087A00001221A70FEF76AFEB44203D9D5F80C35DF +:1087B000DB0704D5084A0948A108FFF73DFF084B2D +:1087C0004FF480321A60074B01221A7070BD00BF4F +:1087D0000000014048780020B1740F00307800207C +:1087E00000E100E0945D0020C31E10B50446042B98 +:1087F0000AD8DFE803F0030309101000074A08480D +:1088000047F23051FFF718FF064B1B682BB120468B +:10881000BDE810401847FEF75FFE10BDFD740F0065 +:108820004C7200202878002038B50F4B1D78044684 +:108830009DB1B8B1037D03F0FF0573B1D0E90330FA +:108840009847A3685BB1227D02F0FF053AB16268E8 +:10885000134463602046FEF7EFFD284638BD00252F +:108860002575FAE70546F8E7B57400202DE9FF47BE +:108870000368D3F804210646002A7ED13368D3F872 +:1088800040216AB10025C3F84051D3F840317F4CF4 +:108890000293029B23681BB12068FFF7C5FF256088 +:1088A0007B4C03A92046FFF7F7FB002840F08880A7 +:1088B000DFF8E481754CC146D8F80030002B00F099 +:1088C000D2805A6823685B68216851B12168002909 +:1088D00050D09A424ED223681B7D13B12068FEF718 +:1088E000ABFDD9F8007017B13B68C9F80030DFF86C +:1088F000B4A17D68DAF800304FF48032C3F8082361 +:10890000C3F848230022C3F84021D3F84021039242 +:1089100025F07F45039AD3F84025C3F84055D3F896 +:108920000435D31A23F07F43012B05D1594B4FF463 +:10893000046043F001039847DAF800204FF48033D5 +:10894000C2F84433D2F80435ED1A290225F07F43EA +:1089500040F1908043F07F4313F57A7F00F38D80E0 +:108960004FF48033C2F80433236813B9336801220B +:108970001A60276004B0BDE8F0870022C3F8042124 +:10898000D3F80431414A0193019B13682BB113685A +:108990005B68126823F07F4353603F4B1C68002CD8 +:1089A0003FF46CAF6368B3F1807F04D22046FFF7D9 +:1089B0003BFF2468F3E7636823F07F436360F8E7D5 +:1089C000BDF80E302268314F334D1344002B3FF475 +:1089D0006FAF1A78012A34D04AB1022A1CD1002282 +:1089E0002B68002B4DD0196829601A75F8E75A6872 +:1089F000117D89B9012111755A6830685168D0F824 +:108A00000405091A2548814282BF516821F07F413F +:108A100051605868FEF710FDBDF80C20BDF80E300F +:108A20009A4202D11A48FFF786FB03A91848FFF7BC +:108A300033FB00283FF43CAFBDF80E3022681344EE +:108A4000C4E75A683B689A4204BF00233B602B6826 +:108A50002946002BE0D09A4202D113680B60DBE775 +:108A600019461B68F5E72368002B83D13368012280 +:108A70005A607FE7022B3FF773AF3846FFF7D4FE0B +:108A80001AE7074B01221A70C6E700BF4C78002096 +:108A9000D0770020B0C80F008061002017FCFF00D5 +:108AA000B57400202C59002040684B6898428CBF58 +:108AB000002001207047000010F1080F30B51CD0D5 +:108AC0000F4A104900240025C0E91445C0E91612D8 +:108AD0000D4C0E4A0E4DC0E918420E4AC0E91A521A +:108AE0000D4A026702F17452A2F523020023A2F696 +:108AF000924283644267184630BD0E20FCE700BFF7 +:108B000085AE67BB67E6096A72F36E3C3AF54FA51E +:108B10007F520E518C68059BABD9831F2DE9F04F16 +:108B2000C9B008AA01F1400514464B780E781B0421 +:108B300043EA0663CE7833438E78043143EA062352 +:108B40008D4244F8043BF0D11024916B16464FEA55 +:108B5000F14383EA714383EA9123716A366852F8DC +:108B6000045F31440B444FEAB54181EAF51181EAD3 +:108B7000D50101340B44402CD363E6D1D0E914E392 +:108B80000093836D0193C36D0293036E0393436E51 +:108B90000493836E0593C36E0693DDE9057BDDE9DF +:108BA000031CDDE901592B4B009E079374464FF0DF +:108BB0000008079B53F8042B07934FEAF12383EA3D +:108BC000B11383EA716A08AB53F828301A440AEBF0 +:108BD000020327EA010A01EA0C028AEA02021A44A5 +:108BE0004FEA743A86EA05035A448AEAB40A06EA66 +:108BF000050B234083EA0B038AEAB45A08F1010803 +:108C00009A44B8F1400F02EB0903BB465244A9460F +:108C100019D172440265009A22444265019A324495 +:108C20008265029A2A44C265039A1A44049B02662A +:108C30000B444366059B63448366069B3B44C366C3 +:108C400049B0BDE8F08F674635468C462646194642 +:108C50001446AEE7C4CA0F002DE9F04110F1080731 +:108C6000044622D012B90020BDE8F081E9B10D46DA +:108C70008E184FF00008B542F5D0A36C15F8011B13 +:108C8000E2180133402B1172A364F4D139463846FF +:108C9000FFF744FFD4E9143213F5007342F10002E8 +:108CA000C4E91432C4F84880E5E70E20DCE7000090 +:108CB00000487047F4C70F0000EB81010023884291 +:108CC00000D3704751F8042D43EA52030B60D307D9 +:108CD000F5E710B5013A52B2130601D5002010BDD8 +:108CE00050F8224051F822309C4205D802F1FF3260 +:108CF000F2D24FF0FF30F2E70120F0E710B5002389 +:108D00005CB2A24200DC10BD51F8234040F8234081 +:108D10000133F5E7013910B549B2041D0A06CBB29B +:108D200004D4013954F82120002AF7D001335BB272 +:108D300073B103F18042013A50F82220002022B999 +:108D4000013B00EB431318B210BD52080130F6E7A7 +:108D50001846F9E74A11012350F8220001F01F01DB +:108D600003FA01F10840704710B500231A465CB2BF +:108D7000A14203DCB2FA82F0400910BD50F8234052 +:108D800001332243F3E7002310B51C465AB2914247 +:108D900000DC10BD40F823400133F7E7F8B50E467C +:108DA000D11C48BF911D144641F387010546671E3B +:108DB000FFF7E9FF00214AB2A24201F1010100DB05 +:108DC000F8BDBB1A23F00300B25C03F00303DB0021 +:108DD00002FA03F32A581A432A50ECE72DE9F04F20 +:108DE000A5B01D4604ABCDE90231814629461046A7 +:108DF0001746FFF78FFFC0EB851004B2002C264604 +:108E0000B8BF04F11F06634246F3471603F01F0381 +:108E100004F01F0814A858BFC3F100083146FFF73B +:108E2000B2FFB8F1000F30B22DDD14AB002203EB1E +:108E30008000C8F1200C13469D4219D82FB27B0048 +:108E4000019314AB03EB870705F1FF3824AB012630 +:108E500003EB8808002C54DA24AB03EB86062A4681 +:108E600056F8881C4846FFF749FF25B0BDE8F08F4B +:108E700057F8236006FA08F10A4340F8042B01333F +:108E800026FA0CF2D8E714AB2A46394603EB8000E9 +:108E9000FFF734FFD2E70FFA82FE5AF8883C53F806 +:108EA0002EB024AB03EB8202013052F8403C0B445D +:108EB000BBEB030334BF012200229B4518BF1146C0 +:108EC0005CF8882C42F82E30019B42B29A42E2DBD9 +:108ED000731A5E42294614A85E41FFF7EDFE24ABEB +:108EE00003EB8503294653F8402C58F8403C38469C +:108EF00043EAC273013C48F8403C24B2FFF7DCFE71 +:108F0000A8E724AB0020C6F1010C014603EB860A5A +:108F100003EB8C0CD8E72DE9F041013B00249B00CA +:108F200025462646A4465FF0000E03E05FEA0C0EDD +:108F3000BEEB030EBCEB0E0752F8078051F80E7023 +:108F4000A7FB0878E41955EB080556F100061EF159 +:108F5000040E9E4501DCE645ECDD40F80C402C4655 +:108F6000354600261CF1040C9C45DCDDBCEB430FB0 +:108F7000DCDD40F80C40BDE8F08130B51C4691B016 +:108F8000054693F900306846FFF7C5FFD4F8B030C6 +:108F900069462846984711B030BD13460A46FFF788 +:108FA000ECBFF0B5154689B01C4607460E461A467A +:108FB00029466846FFF7F1FF23466A463946384698 +:108FC000FFF7DBFF23462A4669466846FFF7D5FFD1 +:108FD00023466A4631463046FFF7CFFF09B0F0BD61 +:108FE000C3F10803DB0070B50133002403A500BF03 +:108FF0005B1920C940CAAD1B20C0184720C940CA10 +:10900000B54120C020C940CAB54120C020C940CACE +:10901000B54120C020C940CAB54120C020C940CABE +:10902000B54120C020C940CAB54120C020C940CAAE +:10903000B54120C06441B4FA84F0400970BD00BF5E +:10904000C3F10803DB0070B50133002403A500BFA2 +:109050005B1920C940CAAD1920C0184720C940CAB1 +:10906000754120C020C940CA754120C020C940CAEE +:10907000754120C020C940CA754120C020C940CADE +:10908000754120C020C940CA754120C020C940CACE +:10909000754120C06441204670BD00BFF0B5082274 +:1090A00089B00C460646FFF729FECB6A03930B6B8B +:1090B00004934B6B05938B6B0693CB6B0793002745 +:1090C00008236A4669466846CDE901770097FFF7AD +:1090D000B7FF082305466A4631463046FFF7B0FF22 +:1090E000236B0393636B0493A36B0593E36B0544BA +:1090F000CDE906376A46082369466846FFF7A0FFB0 +:10910000082305446A4631463046FFF799FF236A33 +:109110000093636A0193A36A0293A36B0693E36BC4 +:109120000793054408236A4631463046CDE9047763 +:109130000397FFF785FF636A0093A36AA26B01930D +:10914000E36A02930492636BE26B0393CDE9052318 +:10915000236A0793054408236A4631463046FFF7E1 +:109160006FFFE36A0093236B0193636B0293236A9F +:109170000693A36A0793054408236A46314630469E +:10918000CDE904770397FFF72BFF236B0093636B05 +:109190000193A36B0293E36B0393636A0693E36A01 +:1091A00007932D1A08236A4631463046CDE90477E5 +:1091B000FFF716FF636B0093A36B0193E36B0293BE +:1091C000236A0393636A0493A36ACDE90537236B8B +:1091D00007932D1A08236A4631463046FFF700FFF1 +:1091E000A36B0093E36BCDE90137636A0393A36A32 +:1091F0000493E36ACDE90537636B07932D1A0823BF +:109200006A4631463046FFF7EBFE2D1A0D4C0FD45F +:1092100035B9082231462046FFF75BFD01280FD003 +:109220000823224631463046FFF7DAFE2D1AEFE7D3 +:109230000823224631463046FFF702FF2D18F7D4A7 +:1092400009B0F0BDF8C70F00F0B591B00E461C2272 +:109250000021074609A8FEF765F801241C22002119 +:1092600001A80894FEF75EF8314608AA11F9045BDC +:1092700000942B461046FFF7E3FE294608A8FFF7A7 +:1092800049FD013804B2012C06DC2A4669463846FD +:10929000FFF734FD11B0F0BD694668463246FFF76E +:1092A0007CFE214608A8FFF755FD28B133463A4613 +:1092B00069466846FFF761FE013C24B2E3E770B5FA +:1092C000036813F001050646144605D013460A4606 +:1092D0000146FFF7B5FE054621463046FFF7ECFC98 +:1092E00045B104F18042013A56F8223043F0004380 +:1092F00046F8223070BDF0B50F46A1B006461946BB +:10930000384615461C46FFF72FFD20B13046FFF7C3 +:109310003AFD21B0F0BD224639466846FFF7EEFC23 +:10932000294608A8FFF7EAFC10A82146FFF72BFD05 +:10933000012318A81093FFF726FD224608A96846C6 +:10934000FFF7C7FC28B9224610A93046FFF7D6FC24 +:10935000DFE7009BDA0709D421466846FFF7ACFC3B +:109360002246294610A8FFF7AAFFE6E7089BDB077D +:1093700007D4214608A8FFF79FFC2246294618A8D3 +:10938000F1E7002823461CDD08AA69466846FFF776 +:1093900027FE21466846FFF78FFC224618A910A831 +:1093A000FFF797FC002805DA10A923462A4608464D +:1093B000FFF746FE10A9234618AA0846FFF710FE3D +:1093C000CEE708A96A460846FFF70AFE214608A824 +:1093D000FFF772FC224610A918A8FFF77AFC0028B4 +:1093E00005DA18A923462A460846FFF729FE18A9D8 +:1093F000234610AA0846FFF7F3FDBEE770B59DF9B6 +:1094000010401E4623460546FFF7EAFD38B12346C5 +:10941000324629462846BDE87040FFF711BE70BDB0 +:109420002DE9F0438BB01446129D15F9046B00969C +:109430001F46804689462B460246214602A8FFF772 +:10944000DDFF02A9129A0846FFF7A7FD129B02AAA8 +:1094500041464046FFF791FD129B02AA2146204655 +:10946000FFF78BFD2B464A46394638460096FFF7F4 +:10947000C5FF129A394602A8FFF78FFD02A92B46B5 +:10948000424608460096FFF7B9FF02A92B4622463E +:1094900008460096FFF7B2FF2B464246214620467B +:1094A0000096FFF7ABFF129B224649464846FFF75E +:1094B00064FD2B4602AA414620460096FFF79EFF18 +:1094C000129B224639463846FFF757FD2B464A463F +:1094D000394638460096FFF791FF324602A92046EA +:1094E000FFF70CFC0BB0BDE8F08300002DE9F04F56 +:1094F000C34CC44B22689A42ADF5137D064640F03A +:10950000D58163685A79202A40F0D3819B68DFF8BF +:10951000008398476468BC4994F805B0B34528BFF8 +:10952000B34605465A460AA8FEF7D6FB6679B74CFD +:109530003246214662A8FEF7CFFB62ABA11998190B +:109540003246FEF7C9FBB5F9023095F9004013F138 +:109550001F0948BF03F13E0949F3471909F1FF36D6 +:109560002AAA002342F8263052AF92AA02EB8602C2 +:1095700047F8263042F8E03C95F90120414672A8B0 +:10958000FFF70CFCA30072AA02EB030A95F9012075 +:10959000049308EB02015046FFF700FC95F9012007 +:1095A00062A93846FFF7FAFB95F9012062AB9918DA +:1095B0005AA8FFF7F3FB21463846FFF7D5FB28B141 +:1095C00048F242500DF5137DBDE8F08F5AA8FFF721 +:1095D000CBFB03900028F3D105F124034A4639461A +:1095E00018460293FFF775FB0128E9D14A465AA9AC +:1095F0000298FFF76EFB0128E2D1029A4B465AA966 +:1096000022A812AFFFF777FE039B47F82630B5F983 +:10961000023013F11F0848BF03F13E08DE1D48BFAA +:1096200003F10E06F6105E454FEA681828BF5E4645 +:109630004FFA88FB59463846FFF7A5FB32460AA980 +:10964000FFF7ACFBB5F90230F6009E4214D9F61ACA +:1096500007EB88080022C6F120004745C0F0C5800E +:109660005A4639460298FFF734FB012804D05B467E +:10967000029A3846FFF7B4FC0DF5027822AE39465F +:1096800040464B463246FFF746FC414638464B467D +:10969000029AFFF7A3FB40464B46324652A9FFF71A +:1096A0003AFC4B464146029A1AA8FFF797FB22461E +:1096B000404672A9FFF722FB049B08EB030B5146BF +:1096C000584605F1440AFFF719FB51463AA8FFF73F +:1096D00015FB049B42A80AEB0301FFF70FFB2B1DB0 +:1096E00041463AAA304600940593FFF787FE5B4651 +:1096F000424642A93AA80095FFF792FE23462A1D4A +:1097000031463046FFF7F7FD3246594640462B4674 +:10971000FFF747FC0023CDE9063A494672AB3846CD +:10972000CDE90838FFF7F6FA494680461AA8FFF750 +:10973000F1FA8045B8BF80461FFA88F808F1FF3A71 +:109740000FFA8AFA51463846FFF704FB5146071CC8 +:109750001AA818BF0127FFF7FDFA002814BF02233B +:1097600000231F430DF1180B0DF1A80A5BF82770B9 +:10977000224639465046FFF7C1FA049B0DF1C80B4B +:10978000F9185846FFF7BAFA21463046FFF7FBFAB8 +:1097900001233360A8F102080FFA88F8B8F1000F2E +:1097A00038DA2346059A31463046FFF7A4FD2B46AA +:1097B000324659465046FFF7F4FB4A465146029856 +:1097C000FFF787FA012804D04B46029A5046FFF76C +:1097D00007FC013C64B252AA23065DD5039B002B13 +:1097E00048F2425008BF0020ECE658F8041D21FA68 +:1097F00006F31343C8F8003001FA00F22DE700BF6A +:10980000047700207052464D44750020E077002018 +:109810000C7700202B463246D5F8A47059465046A6 +:10982000B847414612A8FFF795FA4146071C1AA807 +:1098300018BF0127FFF78EFA002814BF0223002368 +:109840001F4306AB53F827701FB3224639463AA888 +:10985000FFF754FA049B42A8F918FFF74FFA2B467A +:10986000324642A93AA8FFF79CFB059B00943AAA0E +:1098700051464AA8FFF7C2FD5B46524642A93AA8A4 +:109880000095FFF7CDFD2B464AAA31463046FFF73B +:1098900074FB08F1FF387FE752F824105AF824309F +:1098A0004B40039919430391013C95E748F240501E +:1098B00088E648F2115085E670B59DF910401E46C5 +:1098C00023460546FFF7BCFB30B922462946304601 +:1098D000FFF7FFF9012807D02346324629462846DC +:1098E000BDE87040FFF77CBB70BD2DE9F041154627 +:1098F0008AB0044688461C22002103A8FDF712FD09 +:109900002E1D95F9007003232A46414620460293F6 +:10991000FFF743FB334602AA214620460097FFF794 +:109920006DFD2B46424621462046FFF726FB009759 +:10993000334605F1840221462046FFF7BDFF0AB0F9 +:10994000BDE8F0812DE9F04793F9006092B004463C +:1099500089461046314615469846FFF705FA002815 +:1099600040F09180424649460AA8FFF716FB43465D +:109970000AAA214602A8FFF700FB0AA908464246A8 +:10998000FFF70BFB43462A4649464846FFF7F5FAE0 +:1099900008F10407424629462846FFF7FEFA3B46EF +:1099A0002A46214620460096FFF786FF3B462A4678 +:1099B000294628460096FFF77FFF3B462A46214668 +:1099C00028460096FFF71AFD43462A4621462046C0 +:1099D000FFF7D3FA3B462246214628460096FFF77A +:1099E0006BFF3B4600962A4621462046FFF764FF60 +:1099F0002368DB074AD533463A4621462046FFF71F +:109A00001FFB314682462046FFF756F906F1804398 +:109A1000013B54F8230040EACA7044F82300424650 +:109A200021462846FFF7B9FA3B4602AA29462846AE +:109A30000096FFF7E3FC3B4602AA2946284600961B +:109A4000FFF7DCFC02A908463B462A460096FFF7D2 +:109A5000D5FC434602AA21462046FFF78EFA0AAA01 +:109A60003B46104621460096FFF7C8FC3246294681 +:109A70002046FFF743F949462846FFF73FF90AA970 +:109A80004846FFF73BF912B0BDE8F0873146204663 +:109A9000FFF712F9C3E70000024B800143F0010316 +:109AA000184700BFB0C80F0007B5AB238DF80730CB +:109AB00001210DF10700044B98473220FFF7ECFF1E +:109AC00003B05DF804FB00BF5358002038B50D46C5 +:109AD0000A4B014604462022284698472868431C22 +:109AE00009D020F07F4020342044B0F1C14F8CBF1A +:109AF0000020012038BD0020FCE700BFA558002051 +:109B00004FF0A043D3F810056FEA504000F0010079 +:109B10007047000008B5FFF7F3FF0B4A60B910600B +:109B20000A4B0B4AC3F8002640F6CD4100220948F3 +:109B3000BDE80840FEF768BD136801330A2B1360C7 +:109B4000EEDDFDF7C9FC00BF5C780020000001409D +:109B50003546526E60770020012359B91F2886BF11 +:109B600000F01F00094A4FF0A04203FA00F0C2F8CB +:109B70000C0570471F2886BF00F01F00034A4FF0F6 +:109B8000A04203FA00F0C2F8080570470003005035 +:109B900010B50446FFF7E0FF1F2C86BF04F01F043A +:109BA000044B4FF0A04304F5E074032243F8242053 +:109BB00010BD00BF000300502DE9F34101210E202C +:109BC000FFF7E6FF01210F20FFF7E2FF0121102040 +:109BD000FFF7DEFF4FF0A0430C22C3F834276420C8 +:109BE000FFF75AFF224C234F6623012101A88DF86D +:109BF0000430A0479923012101A88DF80430A04723 +:109C00003220FFF749FFFFF74FFFFFF74DFFFFF748 +:109C10004BFFFFF749FF042540F2E9364FF00608F5 +:109C2000013E13D10A20FFF737FF0123022101A8CB +:109C3000ADF80430A0470A20FFF72EFF013D11D1F7 +:109C40000D48FFF729FF02B0BDE8F081B847830750 +:109C5000E8D401A801218DF80480A0470A20FFF76D +:109C60001BFFDDE7B84710F01C0FD5D1E8E700BFB8 +:109C700053580020E3580020A086010070B50F4A19 +:109C80000F4C07234FF080614FF00065216000BF4B +:109C900000BF00BF00BF40FA03F6F6074CBF1560D7 +:109CA000256000BF00BF00BF00BF116000BF00BF44 +:109CB00000BF00BF13F1FF33E8D270BD08050050AC +:109CC0000C0500502DE9F84F294D2A4F01210520A0 +:109CD000FFF742FF2C684FF00B083B689C421FDDEA +:109CE0000020FFF7CBFF0020FFF7C8FF00211B205B +:109CF000FFF732FF00211A20FFF74AFF002105205D +:109D0000FFF72AFF1C4B1978062081F0010119701A +:109D1000FFF722FFB0232B6000233B60BDE8F88FE4 +:109D2000164B4FEA440A012608FB043B9020FFF73C +:109D3000A5FF0AEB0600FFF7A1FF4FF000094FEA6D +:109D4000E90209F007011BF802200A4112F0010F95 +:109D50000CBFFF20002009F10109FFF78FFFB9F1C7 +:109D6000580FECD1022E01D10134B6E70226DDE70F +:109D7000D07B002034590020955D0020965D0020A6 +:109D80000B234B4310B503EBE003094C00F0070233 +:109D900001209040E25C1043E054064B1A688A426E +:109DA000C8BF1960044B1A688A42B8BF196010BD59 +:109DB000965D0020D07B00203459002073B500252B +:109DC00004460DF107000E468DF80750FDF70EFB17 +:109DD000204B214A18781178CBB2204909888B4250 +:109DE00038BF0133C0B238BFDDB2A8422AD01578DF +:109DF0001378DBB2994286BF0133DBB200231370C4 +:109E00009DF80700FEF7F2F94FF6FF739D421ED052 +:109E1000134B1B6803EBC50243F83560ACB1114B23 +:109E200011491B88096805FB031504F1180354F850 +:109E3000041B45F8041B9C42F9D118239380002091 +:109E400002B070BD4FF6FF75DAE79480F7E70420A3 +:109E5000F6E700BF5C7700204C7700205A7700209F +:109E600054770020587700205077002070B5254B9C +:109E70004FF0FF324FF00051C3F88020C3F880212B +:109E8000C3F88420C3F884214FF4805388B00B605A +:109E9000EFF30580196841F204031C68002383F383 +:109EA000148883F3108883F3118883F31388C0F335 +:109EB000080028B981F308889646204708B070BD8D +:109EC000CDE90033CDE90233CDE904324FF08073A0 +:109ED0000793A1F1200306946D461C4608AE2A465E +:109EE00003CAB24220606160154604F10804F6D14D +:109EF00083F308884FF0FF339E466FF00603184740 +:109F0000DCE700BF00E100E0F0B503881F2B8BB059 +:109F1000044600F0D88026D8112B53D006D8012B48 +:109F200000F0E080102B3FD00BB0F0BD142B00F000 +:109F30005E811A2B31D0132BF6D10222ADF81020FE +:109F400002AA0592AD4A1488002304AA42F605210C +:109F50002046ADF81230FDF713FC85211A46204645 +:109F6000FDF71AFCE0E7512B00F0C2800ED8232B3E +:109F700000F0A180502B36D0212BD5D1002304A98D +:109F80008088ADF81030FDF703FCCDE7552B77D076 +:109F9000562B00F09D80522BC6D100231A4619463D +:109FA000A088FDF749FBBFE7944B82881A80944A4A +:109FB00012681C460AB10120904792492088FDF79B +:109FC000F5FBB1E78D4B4FF6FF721A808E4B1B6885 +:109FD0005C0701D4FDF7F2FB894B1B68002BA3D073 +:109FE00002209847A0E7894EC288B3889A429BD145 +:109FF000002504A88DF81050FDF7F8F98449854A2A +:10A00000086890420ED9471E0F6010F8013C824D3F +:10A0100005EBC31502F12103DB1B0A79DBB29A427F +:10A0200038BF0B719DF81000FEF7E0F8002D3FF4EB +:10A030007BAF228A04F11201284602F0D1F91822DE +:10A04000002104A8FDF76EF908238DF81030734B3A +:10A050000693734B0596CDE90735238AADF8243076 +:10A06000704B1B88172B03D82846FEF7D5F85BE703 +:10A070006D4904A8FFF7A2FE00283FF455AFF3E7AF +:10A08000C188822915D8CB1E98070DD0CB1F9BB253 +:10A09000013B5A4202F0030203F0030301F1FF31D6 +:10A0A00058BF5342C91A89B2544B1888FDF7C2FAF7 +:10A0B0003AE78321F8E7002204A98088CDE9042249 +:10A0C000FDF764FB30E74D4B00F10801188876E797 +:10A0D0008379002B7FF428AF484B13211888FDF7B4 +:10A0E00063FB21E7454B00211888FDF79FFA1BE72A +:10A0F0008379002B3FF418AF022B7FF415AF43494F +:10A1000000898A8990420D467FF40EAFA27B012A16 +:10A110007FF40AAF0022CDE90422ADF808308DF8B3 +:10A12000103001AB01200393344B8DF81600208AC8 +:10A13000ADF818001B88608AADF81A0004F114000D +:10A140000790ADF80A20098A02AA1846FDF718FB05 +:10A1500060B99DF80420D20708D440F2FD1204A98A +:10A160001846ADF81420FDF769FADDE6002204A9CF +:10A170001846ADF81420FDF761FA014600287FF477 +:10A18000D3AE182204A8FDF7CDF8237D234A8DF81D +:10A190001030022B0595069220D0062B0ED0012BF5 +:10A1A0000ED12248FDF7F6F9214B1A88214B1A806F +:10A1B000637D0893D4F81630099301E0637D08931A +:10A1C000069B002B3FF4B0AE164B1B88172B7FF677 +:10A1D000ABAE154904A8FFF7F1FDA5E6A27D637DAE +:10A1E00043EA0223124A1380124A1380E8E700234D +:10A1F0001A4619468088FDF7CDFA95E62A590020BF +:10A2000068750020D4CB0F008C74002038720020B9 +:10A21000846100200C7200208C61002085760F0084 +:10A2200051820F005877002071B70F0040CD0F000A +:10A23000007700200277002008B510461A4602F089 +:10A24000CFF8002008BD08B510461A4602F0C8F83D +:10A25000002008BD30B5114B1148DA6A5C6A9D6C6C +:10A26000AC2A02BF5B6BED18E41A85B02246294682 +:10A2700002F0C4F880B10B4BD3F800340BB1FDF7FA +:10A28000FDF8102200216846FDF74CF8A408684646 +:10A2900001950294FDF79EF905B030BD607800206D +:10A2A00000700F0000000140F7B5184D1849064630 +:10A2B0004FF4DC72284602F0A1F828B916B9002044 +:10A2C00003B0F0BDB047FAE7124C114923680122F0 +:10A2D0001F6920460023B8470246A8B95821281D07 +:10A2E000FDF73CFF294628604FF4DC720A4802F073 +:10A2F00077F823680096DD68074A05494FF4DC7358 +:10A300002046A8470028DAD00320D9E7607800204B +:10A3100000F00F0040590020187A00202DE9F04F7E +:10A3200002F6FF7585B02B0B0293284B1B780E1A93 +:10A330000746884614464FEA163B0BB9FEF720FA4B +:10A34000BBF1080F28BF4FF0080B4FEA0B33DFF8C3 +:10A35000809003930CB9224633E0029B1C489B4536 +:10A3600028BF9A46D9F8003038BF039E1B6938BF12 +:10A37000DA461D4652464FF00003394628BF2646AE +:10A38000A8470246E8B9D9F800100090CA68F31C43 +:10A39000154623F00303424639464846A84702467D +:10A3A00078B9029B0B4AA3EB0A030293136BA41B1D +:10A3B0003744B0441E441663FFF776FF0246002878 +:10A3C000C8D0104605B0BDE8F08F00BF945D0020F6 +:10A3D000405900206078002008B5164B42F2040274 +:10A3E000996C885802F1A34202F5471202F2D75243 +:10A3F00090421AD1D3E90C02904218D0B1F5805F97 +:10A4000004D10020BDE80840FFF74EBF121A02F643 +:10A41000FF7222F47F62014422F00F0200F58050A7 +:10A42000FFF77CFF0028ECD008BD0320FCE70020EC +:10A43000FAE700BF60780020034A0023036043600E +:10A4400083601363704700BF607800202DE9F04FF0 +:10A45000AB4D2B68DB0791B000F15081A94BAA4AA4 +:10A460001860AA4B1A602122013AD2B2FF2A40F0AA +:10A47000408100221A7109A802230492CDE90A2220 +:10A480000C920993FDF7A6F80446002840F02D81B0 +:10A490009F48FDF7C9F80446002840F02681FDF7E3 +:10A4A0004DFF0446002840F0208106210123CDE91C +:10A4B0000900984A0B90ADF82810202009A98DF8C2 +:10A4C00024308DF826300492FDF715FC4FF481728C +:10A4D000CDE9094409A9ADF826204020049A8DF859 +:10A4E00025300B94FDF707FC8322CDE9094409A927 +:10A4F000ADF826202320049A8DF824300B94FDF724 +:10A50000FAFB049A0B94184609A9CDE909448DF881 +:10A510002430FDF7F0FB4FF4B062099209A9049AC8 +:10A52000A120CDE90A44FDF7E6FB049A0B9409A9A2 +:10A53000A020CDE90944FDF7DEFB04A8FDF7DDFB13 +:10A540000446002840F0D180734A18211370024657 +:10A550007248FDF703FE724AD2F89C31984212D13C +:10A56000704C02F5CE7302F5DA7218685968264607 +:10A5700003C6083393423446F7D1186830602B681D +:10A5800043F002032B60112309A88DF81030FDF76A +:10A5900063F90446002840F0A8809DF8253009A8FA +:10A5A00001338DF82530FDF759F90446002840F0B5 +:10A5B0009C80D5F8009019F0020F15BF59495A49EF +:10A5C0008A69072218BF043192B204A8FDF7E8F89F +:10A5D0000446002840F089805448FDF7E3F804461B +:10A5E000002840F08280524B524D4FF6FF724FF0E0 +:10A5F000010A1A8002A94FF659632A4650468DF87F +:10A600000AA0ADF80830FDF7BFF8044600286CD169 +:10A61000A91C4948FDF70CF80446002865D10146FD +:10A620001C2209A8FCF77EFE9DF82430ADF80440FA +:10A6300043F004038DF824300223ADF80C30AB78DE +:10A640008DF80E3004F0F80343F003038DF8063064 +:10A650008023CDE90644ADF81C302B4601AE4FF007 +:10A66000110B03AF04AA09A933F8040B08948DF861 +:10A6700005B0CDE90476FDF785F8A0460446A0BBF9 +:10A6800001461C2209A8FCF74DFE9DF82430ADF8C8 +:10A69000044023F0180343F018038DF82430AB78FE +:10A6A0008DF80E301723CDE90644ADF81C302B464B +:10A6B00008F0E80848F0130804AA09A933F80C0BBD +:10A6C0000894ADF80CA08DF805B08DF80680CDE9A2 +:10A6D0000476FDF757F8044638B9FDF76FF80446DD +:10A6E00018B9074A49EA0A031360204611B0BDE8C9 +:10A6F000F08F1968481C18600A70B5E60024F4E76A +:10A700008C740020687500200C7200208461002089 +:10A7100000700F00B857002020780020A0F10F0033 +:10A7200000F00F0060610020E8C70F00D4CB0F00DD +:10A730002A59002038720020D8C70F0030B542686F +:10A74000527802F00F01082987B003460FD1036940 +:10A750001C6800250CB907B030BD2146257368463A +:10A76000FDF79AF86846FFF7E9FFA468F2E712F0F0 +:10A77000C00120D102F0300010280ED15A691170AA +:10A780005A6818695378946803F00F03072B09D1AE +:10A790000146204600F017F8DDE79006F0D55B692A +:10A7A0001970D8E71279214614B101F019FED2E7E9 +:10A7B000FCF7B8FDCFE78029CDD1006900219206D2 +:10A7C0000160C8D5EBE77FB50023CDE9023183787E +:10A7D000194490F903300491194403780591CDE9A7 +:10A7E00000003BB16846FFF7A9FF6846FDF72EFA67 +:10A7F0000028F7D107B05DF804FB00002DE9F04315 +:10A8000055686B7813F0C00489B006468C4617462D +:10A8100003D0402C6AD000240AE003F00F025E4906 +:10A8200003F03003102B51F8228020D004D8CBB194 +:10A83000204609B0BDE8F083202B1ED0302BF7D185 +:10A840007B6929781970072A0AD17B6838691A79D7 +:10A850002146FCF767FD7B6839699868FFF7B3FF0D +:10A860003A6940E03A692946C0470446E0E77B6917 +:10A8700001221A703A697968F6E7BCF1020FD7F83D +:10A88000149024D1042A22D804A9FDF71CF9044607 +:10A890000028C0D0069B2BB1796899F800304A791E +:10A8A0009A4205D8059B7360069B002BC0D0B2E787 +:10A8B00008793A6900FB032204A8C04728B199F837 +:10A8C0000030013389F80030E4E70446EAE799F8FC +:10A8D00000306A799A429ED93A692979581C89F8D8 +:10A8E000000001FB032279683046BDE7D2F81080F2 +:10A8F000454655F8043B13B9FDF782F9B5E702293F +:10A9000014D104A9FDF7DFF8002884D0D8F800306E +:10A9100079682A4604A89847044600283FF47BAF8C +:10A92000069B002BF2D1059B736081E701292BD098 +:10A930004CB24CB105297FF46EAF04220DEB02013D +:10A94000FDF7DAF8042424E00DF1040901340B2C9E +:10A950003FF461AF012249463046FDF7CDF80028AB +:10A960003FF459AF19F9013B002BEFDB0B4B04937C +:10A9700001ABCDE9053400230793D8F8003079689E +:10A980002A4604A8984770E7082201A9FDF7B4F801 +:10A9900008240028EAD13EE7B4C70F0003750F0072 +:10A9A0002DE9F04F8B788FB00025CDE904521A4481 +:10A9B00091F903300692CDE90055CDE902111A4410 +:10A9C000044607924FF00108A3682BBBDDE903346E +:10A9D00002A85D78FDF73AF90028F7D115F0300F9D +:10A9E00003D1039B1B7803B10134002C65D06309AC +:10A9F000002293425BD10EAA02EB830304F01F04F2 +:10AA00004FF0FF30C4F1200420FA04F453F8380C5E +:10AA1000231A5842584108E008A92046FDF720F8BB +:10AA200030B9A368002BD1D000200FB0BDE8F08F63 +:10AA3000089EDDF80CA0002EC8D0F70806F0070627 +:10AA4000039B1A78974204D15B7803F00F02082A1F +:10AA500058D102A8FDF7FAF8DDF80C90CA45EFD1FD +:10AA6000BD420DD8039A537803F00F03082B29D069 +:10AA700002A8FDF7EBF8039B9945F3D14FF0FF35A2 +:10AA800031462046FDF7BCF800289DD1CCE79B68F5 +:10AA90001B789F4233D1494608A8FCF7FDFE89F890 +:10AAA0000C8008AA31462046FFF7A8FE25E05DF895 +:10AAB00022100131B8D101329BE70120B5E79A4558 +:10AAC000DED081E71578BD42DAD8069BD4F808A01D +:10AAD000D3F80090A368B9F1000FF0D09A457FF445 +:10AAE00073AFD9F80030D3F800B0BBF1000FCED06F +:10AAF00033463A4649462046D847002894D0D9F8EC +:10AB00000890E7E713F0300F0FD1049B3F2B0CD8D0 +:10AB10005A090EA901EB820203F01F0352F8381CF8 +:10AB200008FA03F30B4342F8383CD4F80C90B9F11F +:10AB3000000F04D002AB324639462046C84702AA6D +:10AB400031462046FFF75AFE9EE7000070B50E4BD7 +:10AB50000E4C9A6B0E4E0F4D0F4921605C33C4E9C9 +:10AB600001320E4B0E4AE360002313600D4A31465A +:10AB700028461360FFF727FE324629462046BDE8E7 +:10AB80007040FFF70DBF00BF60780020F076002016 +:10AB900074750020A0C90F0003750F0021750F0008 +:10ABA000407500203C7500202DE9F7437F49D1E92D +:10ABB0000E329A420E4640F0F2807D4B1A7899464A +:10ABC00052B1FCF7D7FC7B4B4A6A18607A4B1A608B +:10ABD000012003B0BDE8F083FFF7B8FF002834D0B0 +:10ABE000764B93F89C5019460DB91B786BB3724B9A +:10ABF000704F00223A601A6098464DB1704D91F83E +:10AC00003831A5F199042022019225B91320E0E7FB +:10AC10002B466C4CF7E70BBB6B496C486C4A0968D8 +:10AC2000006801ABFCF7A6FBB0B92B78402B13D121 +:10AC3000684A6B1C413553F8041B42F8041BAB42B5 +:10AC4000F9D10198FEF752FC08B10520C1E794F84C +:10AC500058302BB90F20BCE70A20BAE71620B8E716 +:10AC600094F859100329F5D894F87130032B40F06B +:10AC70009880237B002BEDD02369342B40F0938008 +:10AC800043F20402237D1068534A90423FD113B32C +:10AC9000A26902B304F11400002243F20C0CD5B2F5 +:10ACA0009D4201D3122094E7BCF8005050F804EF05 +:10ACB000AE4502F10102F2D12379002B2DD00129FA +:10ACC000A36894BFB268F2689A428CBF0023012344 +:10ACD0002BBB10207DE799B9012BEDD9A269002A81 +:10ACE000EAD143F20C0204F1140015880A4650F828 +:10ACF00004CFAC45E0D00132D2B29A42F7D1D1E7CD +:10AD000011F1FF3318BF0123002BD5D1CAE713B1CE +:10AD1000A369002BC6D12379F6E701299AD1012333 +:10AD200089F8003011BB94F86830002B33D0E56E01 +:10AD3000002D8FD0C8F80050FCF71CFC2749081ADA +:10AD4000854207D9FCF700FC081A854221D824481F +:10AD5000FFF772FB002301212248C6E90431FFF707 +:10AD60006BFB7562FCF706FC386031E711F00105FA +:10AD700003D094F85A5005B1E56D8B07D8D594F8F7 +:10AD80006030002BD4D0184B626EC3F57E239A42FC +:10AD900003D904201DE70D46EFE71544C8E7082056 +:10ADA00017E7142015E7112013E700BF6078002093 +:10ADB0002C780020747400207074002074750020BA +:10ADC000AD760020787500203C750020407500208D +:10ADD00044750020E0770020DBE5B15100400F0012 +:10ADE000787800208478002000700F00F0B585B0DE +:10ADF0000E468C68694607461546FCF764FE88B126 +:10AE0000B06878B1737803F03003202B03D1294662 +:10AE10002046FFF7D8FC2A4621466846FFF7C0FDCA +:10AE2000019B7B6005B0F0BD2DE9F74F184D8346BF +:10AE30000E46284601A991469A46FDF755F920B3DA +:10AE4000BDF80470D5F8008018EB07041DD0182257 +:10AE50000021201DFCF766FA0123237148F807B092 +:10AE6000BDF80420BDF80630C4F808A0360B9A429D +:10AE7000E660C4F8149002D12846FDF765F9FDF7A5 +:10AE800079FB002003B0BDE8F08F0420FAE700BF93 +:10AE9000607400202DE9F74F174D83468A462846F7 +:10AEA00001A991469846FDF71FF910B3BDF804605B +:10AEB0002F68BC191DD018220021201DFCF732FA82 +:10AEC0000023237147F806B00C9BA360BDF8042053 +:10AED000BDF80630C4F814809A42C4E9039A02D13E +:10AEE0002846FDF731F9FDF745FB002003B0BDE82A +:10AEF000F08F0420FAE700BF6074002007B5142229 +:10AF000000211148FCF70EFA104B53E8021FD888B5 +:10AF1000B1EB314F4FF0000217BF89B240EA004059 +:10AF200041EA004004E05968B1EB314F08BF01220B +:10AF300043E802010029E8D18DF807209DF8073089 +:10AF4000002003B05DF804FB7874002060740020DA +:10AF5000F8B513681F2B04460D46164663D910F149 +:10AF6000080763D0002961D0836CC118372B4FF0DC +:10AF7000800003F10102087245D80933234404F12B +:10AF8000400200219A423BD1D4E91423A06C08214D +:10AF9000E1FB0023104612BA62641A0A194684F8CB +:10AFA000433084F842201A0C1B0EC4E9140184F8C3 +:10AFB0004030394684F841203846FDF7AFFD691E20 +:10AFC0000023626FDA4001F8012F226FDA400A7124 +:10AFD000E26EDA400A72A26EDA400A73626EDA40FA +:10AFE0000A74226EDA400A75E26DDA400A76A26DC2 +:10AFF000DA400833202B0A77E3D133600020F8BD14 +:10B0000003F8011BBEE7E31D00213F2A08D939469A +:10B010003846FDF783FD38220021FCF783F9B3E7BA +:10B0200001329954F1E748F21450E8E70E20E6E7C0 +:10B0300008B54FF4727200210848FCF773F9084A0A +:10B0400002231360074A1360074B00221A60074B64 +:10B05000AF221A60BDE80840FEF734BE965D0020BE +:10B060002059002024590020D07B00203459002092 +:10B070002DE9F04FDFF81881DFF8189185B0471EF1 +:10B0800017F8016F56B93D4B1A683D4B1B689A4241 +:10B0900071DC05B0BDE8F04FFEF714BE2E2ED8F8D7 +:10B0A0000040D9F8005059D02B2E59D0602E01D92C +:10B0B000203EF6B2303E5E2E32D8052396FBF3F3E7 +:10B0C00003EB8303F31A03EB4302F61A2D4B0092B2 +:10B0D00003EB46066B1D0193631C0293A31CAA4657 +:10B0E000039336F8022B009B42FA03FB1BF0040F7C +:10B0F00003D051462046FEF743FE1BF0020F03D05B +:10B1000051460298FEF73CFE1BF0010F03D051465A +:10B110000398FEF735FE019B0AF1010A9A45E0D13A +:10B120003B780A2B1ED1AB1D532BC9F80030A7DD8D +:10B1300015494FF45C72A1F1580001F051F9582201 +:10B1400000211248FCF7EEF80C4B00221A600C4B61 +:10B15000023D5722C9F800501A6091E75C26A9E722 +:10B160004026A7E70D2B0BBF02230434C8F800309C +:10B17000C8F8004084E705B0BDE8F08FD07B002020 +:10B18000345900205AC70F00EE5D002006610020F0 +:10B19000205900202459002008B5FFF769FF024814 +:10B1A000BDE80840FFF764BFC4CB0F0008B507280F +:10B1B0001CD8DFE800F0041517191B131B130C48EB +:10B1C0000C4BC3600022012340F6CD418360FDF7A4 +:10B1D0001BFA0948BDE80840FFF7DEBF0748F9E75A +:10B1E0000748F7E70748F5E70748F3E708BD00BF5A +:10B1F00060770020159B0F0079C90F0087C90F00E9 +:10B2000090C90F008DC90F0098C90F001FB547F6F0 +:10B2100030020023CDE901230DF10604CDF80B30F7 +:10B220001C2220FA02F101F00F01CBB20929D4BF90 +:10B2300030333733043A04F8013B131DF1D10023B6 +:10B2400001A88DF80E30FFF713FF04B010BD000009 +:10B250002DE9F04FADF5137D01460446102206A8F6 +:10B26000AC4DA847069B013347D0079B002B44D029 +:10B27000A948FFF791FF0698FFF7C8FFA748FFF717 +:10B280008BFF0798FFF7C2FFA548FFF785FF0898D7 +:10B29000FFF7BCFFA348FFF77FFF0998FFF7B6FF52 +:10B2A000A148FFF779FF069BB3F5772F25D09F487C +:10B2B000FFF772FF04F110060023079F0393B146C6 +:10B2C000002F1EDC089A039B9A4234D09848FFF75F +:10B2D00063FF9848FFF760FF0398FFF797FF9648D2 +:10B2E000FFF75AFF9448FFF757FF0023934A94490A +:10B2F00094485293529B934217DD0DF5137DBDE8A0 +:10B30000F08FB7F5807FB846A8BF4FF480784946E4 +:10B31000424612A8A84703AA414612A8FCF71EFFFE +:10B32000C1440390A7EB0807CAE7C1F80006529B87 +:10B3300001335293DEE78448FFF72EFF8348FFF77F +:10B340002BFFDDE906A9B34652ABB9F1000F01D1DD +:10B3500001272FE0B9F5807FC84628BF4FF48078D9 +:10B36000184642465946A84752AB184600271AF8D5 +:10B37000072010F801CB62450AEB070167D074483B +:10B38000CDE90021FFF774FE01990846FFF73EFF63 +:10B390006948FFF701FF52ABD85DFFF737FF6D48F3 +:10B3A000FFF766FE009A1046FFF730FF6248FFF78E +:10B3B000F3FE00276848DFF8B881FFF7EDFE0623AB +:10B3C00001210AA88DF828301C3CC04700230493B3 +:10B3D000049BB3F57A7F42DB02238DF828301C22D0 +:10B3E000230C002164BA0BA88DF82930ADF82A404F +:10B3F000FBF798FF20210AA8C047584CA047C30775 +:10B40000FCD4002F40F081805548FFF7C5FE554819 +:10B41000FFF7C2FEDDE9068472B6534BDFF820C1A8 +:10B42000DFF820E1CDF80080A146224604EB0801B8 +:10B430000220002A16DCDFF830A14746B9F1000FE0 +:10B440002CDC4444009BA34259D1FCF745F801375A +:10B45000B8458CD1C344C244A9EB080975E7049BE5 +:10B460000133B4E7A1EB020AC3F80405D3F800B432 +:10B47000BBF1000FFAD0C3F808A5D3F800A4BAF1C5 +:10B48000000FFAD0C3F80475D3F800A4BAF1000F86 +:10B49000FAD0A2F58052CCF800E6CAE7B9F5807F71 +:10B4A000CB46A8BF4FF4807B5A46314652A8A847E6 +:10B4B0003B4652AA012000215F44CAF80405DAF88D +:10B4C00000C4BCF1000FFAD052F804CBC3F800C09E +:10B4D000DAF800C4BCF1000FFAD0CAF80415DAF8A3 +:10B4E00000C4BCF1000FFAD004339F42E5D1144BE5 +:10B4F000144AC3F800265E44A9EB0B099EE7009AA4 +:10B5000012F8013B009205939CE71848FFF744FEB0 +:10B51000F3E600BFA5580020F4C90F0001CA0F00D0 +:10B5200007CA0F000DCA0F0012CA0F001BCA0F0076 +:10B5300027CA0F0034CA0F00C6CB0F003F4B4C0088 +:10B54000000001403546526E41CA0F0048CA0F0044 +:10B5500053CA0F005CCA0F0060CA0F00E3580020F6 +:10B560006FCA0F0083CA0F0000E001408FCA0F00AE +:10B57000535800202DE9F0436D4D6E4C89B00746BD +:10B580000026641B1423B4FBF3F4A6422AD118222C +:10B59000002102A8FBF7C6FE04238DF80830664C94 +:10B5A000664BD3F80080236D434540F0BC80644B6C +:10B5B0001E68644B93F89C2003F1A4050833002A0D +:10B5C00008BF1D464FF0200901AB5F4ACDF804903B +:10B5D00041463046FBF7CEFEC8B117255B48FEF763 +:10B5E0002BFF28E02B6813B901361435CDE7DB6952 +:10B5F000284698470028F7D0554B1B88172B03D9AE +:10B6000054493846FEF7DAFB09B0BDE8F0834A46F4 +:10B6100005F16F014C4800F0F1FE0028DDD195F8EE +:10B6200055305BBB012223626B68E262A360A36CAE +:10B63000C4E909830125202200214748FBF772FE57 +:10B64000F422FF214548FBF76DFE0A2D4FF0000361 +:10B65000236388BF424BA66484BF0B3D1D70414CE1 +:10B6600088BF0B258DF80950D7E9011302A8984728 +:10B67000002CC9D02046FEF717FEC5E703F00201F3 +:10B68000DB074BD542F20403F05803F1A34303F563 +:10B69000471303F2D753984243D1AB6D03F5FF52E2 +:10B6A00003F6FF731F32C3F30B03D31A42F20802EF +:10B6B000B2589A4235D343F204031B6883421BD12C +:10B6C0004FF440531B78142B89BF43F2140300221C +:10B6D0001A68254B88BFB2FBF3F206F500531B78BE +:10B6E000142B89BF42F214030023F3581E4888BF6D +:10B6F000B3FBF0F39A420CD099B11C48FEF79CFEC4 +:10B70000AC23E362AB6D6363002990D06B68E360A8 +:10B710008DE7002914BFAC23A523F2E7AA23E36237 +:10B72000F2E705255AE701238DF8093000249BE74D +:10B730004059002054590020607800207074002087 +:10B7400074740020747500209574002084780020A3 +:10B750005877002075B50F0098780020BC7800203D +:10B7600064750020C9750F0040420F0078780020F2 +:10B77000F0B5044689B0162200210DF10A00FBF74E +:10B78000D1FD23788DF8083001228DF809200C2B8B +:10B7900000F2B381DFE813F00D0079000F007900AB +:10B7A0007900B1017900700079006E001C002F0053 +:10B7B00074008DF80C20D4E9011302A898479DF875 +:10B7C0000930012B00F08E81A64B05201B6888E113 +:10B7D0004FF08053D3F800210392D3F80421049250 +:10B7E000D3F80C2192020692D3F81021920205920E +:10B7F0001B690793DFE743F204039B491D680A6A4C +:10B800009A48854214BF01230223012A227C08BFE3 +:10B81000013352B902238DF80C30CB680493944B5A +:10B820000593C3F57E230693C5E7012A12D185420D +:10B8300010D14FF440531B78142B8ABF43F21403EA +:10B8400000231B6804934FF48053059343F20803CD +:10B850001B68E8E79A4209D201238DF80C308B6807 +:10B860000493FBF771FE05908B69DCE7FF238DF8ED +:10B870000C30A0E7237CFAE7238AADF80C309AE776 +:10B88000784B07201B68984795E7062B794A01D02B +:10B89000012B01D1216911701178012904D00229ED +:10B8A00079D000231370C0E0013B072B83D801A29D +:10B8B00052F823F0D5B80F00B7B70F008BB90F00BF +:10B8C00059B90F00B7B70F0085B90F00B7B70F0010 +:10B8D00009B90F00634B03201B6898476669002E67 +:10B8E00077D0B6F5807F00F283805F4D624B0021F8 +:10B8F0001C2205F13C0019702963FBF713FDAE63B0 +:10B9000001238DF8093056E7574DA68AE86BAB6BDB +:10B9100032189A421ED827695C303246394628448C +:10B9200000F05EFDEB6B05F14002334431463846D2 +:10B93000EB63FCF713FC012328648DF80930EB6BF3 +:10B9400003932B6C0493E368002B3FF434AF20691E +:10B95000984730E70323F0E7FFF726F90A2804D9D0 +:10B96000464B0B3818700B23CBE701288DF80900E4 +:10B970007FF421AF0020FEF797FC00283FF41BAFB7 +:10B980000A23BEE74FF480730593374BDA6B1B6CC9 +:10B99000039204930FE7013B072B3FF60CAF01A284 +:10B9A00052F823F0C5B90F00B7B70F00B1BA0F00B6 +:10B9B000BDBA0F00B7B70F00E9BA0F00B7B70F0055 +:10B9C0002FBA0F002C4B1B780BB9082399E762693B +:10B9D0000AB9032395E7C2F30B0333B1224B28497D +:10B9E0005B6D096813448B42F3D1B2F5805F01D9D6 +:10B9F000042386E71C4B2248596D00685518854280 +:10BA0000E3D85A64DA6C196519639A641D481E4BB1 +:10BA100062691E68036802F6FF721D69120B00233B +:10BA20003144A84700283FF4C6AE052369E7124B0E +:10BA30001B78002BC9D00C4DA38AD5E91421521ACA +:10BA4000696C1A448A42C4D80F4A0E481768296B99 +:10BA50000268E6680096D66822693944B047A8B102 +:10BA6000E36874E76C75002060780020DBE5B15175 +:10BA700000700F001C5900202C78002064750020F5 +:10BA8000707400204059002074740020A28A2B6B2F +:10BA900013442B63A68A2B6D1E442E65A18A206950 +:10BAA00005F14802FCF75AFBA864CDE9036082E681 +:10BAB000134B9A6C1B6D049203937CE6104BD3E9F5 +:10BAC0001402596C821A8A427FF47FAF9A6C5865CF +:10BAD000002120465964DA64FFF74CFD094B1B68CE +:10BAE0000420984709B0F0BD054B9A6C1B6D039379 +:10BAF0004FF48053049205935DE6022301E700BFF3 +:10BB0000607800206C750020044B002282B01A601F +:10BB10001B680193019B02B0704700BF0001014008 +:10BB20001EF0040F0CBFEFF30880EFF3098000F064 +:10BB300001B800BF70B5144B144A0C21D21AB2FBE5 +:10BB4000F1F2816911F8025C044635B900690021FF +:10BB5000914205D1012323600EE04FF0FF30F6E75C +:10BB60001E68AE420CD1461C07D19D68D4E9000185 +:10BB7000D4E90223A847206070BD5E688642F4D0F5 +:10BB800001310C33E4E700BF5CCD0F0068CD0F003E +:10BB900007B50A4B00930A4A0A4B934208BF002399 +:10BBA0000193019B13B903B05DF804FBD3E90020B6 +:10BBB00090476846FCF7F2F8F3E700BF24CD0F008A +:10BBC00088CD0F0078CD0F0008B54FF080430022DC +:10BBD000C3F80C21C3F81021C3F838254FF0805268 +:10BBE00003F54043D2F80414C3F82015D2F8081422 +:10BBF000C3F82415D2F80C14C3F82815D2F8101481 +:10BC0000C3F82C15D2F81414C3F83015D2F8181450 +:10BC1000C3F83415D2F81C14C3F84015D2F8201418 +:10BC2000C3F84415D2F82414C3F84815D2F82814E0 +:10BC3000C3F84C15D2F82C14C3F85015D2F83014B0 +:10BC4000C3F85415D2F83414C3F86015D2F8381478 +:10BC5000C3F86415D2F83C14C3F86815D2F8401440 +:10BC6000C3F86C15D2F84424C3F87025FCF786F8A5 +:10BC700010B12E4B2E4A1A60FCF780F818B12D4BEC +:10BC8000FB22C3F81825FCF779F848B12A492B4B59 +:10BC90000A681B6822F00F0203F00F0313430B60C6 +:10BCA000FCF76CF818B1264B4FF400721A604FF095 +:10BCB0008043D3F80024D20744BF6FF00102C3F8D9 +:10BCC0000024204AD2F8883043F47003C2F8883048 +:10BCD000BFF34F8FBFF36F8F4FF01023D3F80C32A9 +:10BCE000DB071ED5184B0122C3F80425D3F8002426 +:10BCF000002AFBD04FF01022D2F80C3223F00103BF +:10BD0000C2F80C32104B1A46D3F800140029FBD0AD +:10BD10000021C3F80415D2F80034002BFBD0FBF748 +:10BD2000DBFB0A4B0A4A1A6008BD00BF8C56004074 +:10BD30004881030000F00040E40E0040580200106B +:10BD40004096024000ED00E000E00140185900205C +:10BD50000090D003024B034A1B681068184700BFCD +:10BD6000B8740020307200202DE9F04F4FF0A0434E +:10BD70000C22C3F844278BB000211320FDF708FFE5 +:10BD80004FF08043D3F8005415F00B0F45D115F058 +:10BD90000F0407D14FF47A764FF47A70FDF77CFEEA +:10BDA000013EF9D1FDF7ACFEB8BBB4BB214608207B +:10BDB000FDF7EEFE0720FDF7EBFEFDF7FDFEB923D4 +:10BDC00001218DF8083002A8A44B98470220FDF706 +:10BDD00063FE01211320FDF7DBFEFDF791FE014616 +:10BDE0000028FAD11320FDF7B7FE4FF0A0439C4A7C +:10BDF000C3F84427D3F8442722F44032C3F8442739 +:10BE0000D3F8442742F44032C3F844274FF080432C +:10BE10000122DA67C3F80025FEE700210520FDF7BF +:10BE2000B7FE01211A20FDF7B3FE01211B20FDF70B +:10BE3000AFFE01210720FDF7ABFE01210620FDF733 +:10BE4000A7FE01210820FDF7A3FE864B8648874CFC +:10BE500043F001039847FFF7EBF8FFF7E9F8E9072C +:10BE600002D52046FFF798F96D106278631C002A0E +:10BE700072D15B780234002BF1D14FF080434FF048 +:10BE8000FF32C3F800247A48FFF786F9FDF738FE41 +:10BE9000044668B97748FFF77FF9FDF78DFE02A9E0 +:10BEA0002046FDF713FE002858D17348FFF774F9B8 +:10BEB0007248FDF7F1FD724B724A1A60724A734B79 +:10BEC000136000211046FCF7E1F8064600920028B6 +:10BED00040F0BE806E4C6F494FF4DC72204600F09B +:10BEE0007FFA25686A1C00F0A58032465821201D83 +:10BEF000FCF734F9854240F09D80E36AA52B00F001 +:10BF0000948100F28980012B00F0D480FDF7F8FDC8 +:10BF1000236A0546002B40F08F815F48FFF73CF90C +:10BF2000002D00F0B7815D485D4EFFF7A1F84FF49A +:10BF30002065FDF7E5FD002800F0A281002D00F04E +:10BF40007E8140F2E730FDF7A7FD6B0602D1304657 +:10BF5000FFF78EF8013DECE71C4686E74CF25235C0 +:10BF6000A046504E504F53E09DF80C302E2B2AD156 +:10BF70009DF80D30662B26D19DF80E30692B22D10D +:10BF80009DF80F30722B1ED19DF810306D2B1AD1F9 +:10BF90009DF81130772B16D19DF81230612B12D1FC +:10BFA0009DF81330722B0ED19DF81430652B0AD1F9 +:10BFB0009DF815303BB93D48FFF7EEF808F1200039 +:10BFC000FFF746F974E7029C24F07F44233444448D +:10BFD00024F00309C145FFF468AF09F12003B3F170 +:10BFE000C14F3FF662AF02A94846FDF76FFD70B939 +:10BFF000344004F580593C4414F1C14F7FF655AFED +:10C0000002A94846FDF762FD00283FF44EAFC8463E +:10C01000013DA9D1264849E7AA2B00F00A81AC2BA3 +:10C020007FF474AFFEF7D8F9002840F00081FEF7E6 +:10C0300011F9FCE04FF4DC7200211548FBF772F9AE +:10C04000012300206360FEF72FF900283FF455AF6D +:10C05000FDF70CFF00200BB0BDE8F08F5358002017 +:10C060000C000300B0C80F0000C4090038C70F005F +:10C07000DCCB0F00E5CB0F00F3CB0F0020A10700B6 +:10C0800028780020ADB10F004059002080CC0F006F +:10C090006078002000F00F001BCC0F0029CC0F00AF +:10C0A00066CC0F0000F0FFFF0010809F0CCC0F004B +:10C0B000FDCB0F00A16C656AFBF746FA814240F0A8 +:10C0C0003981FBF741FA00222946FCF747F8A36AB9 +:10C0D000984202BF0123C4E90703A56100259A48DD +:10C0E000FEF7AAF9994B9A4800221A70FEF7DCF87D +:10C0F00001460028ACD1002D00F0CF80236A012B2F +:10C1000040F0CB80E56945B1FBF71EFA0A46A1690C +:10C11000FCF724F8854240F0C08042F210768D4B47 +:10C120001B780BB9FCF72CFB8B4B8C4FDFF8AC82E8 +:10C130003B608B488B4D8C4903F58473C8F8003005 +:10C14000002303702B7018230B80884B20221A8049 +:10C150004FF080429A46D2F81CC501901CF0B10FF6 +:10C1600018BFD2F81C3582481CBF03F04E03C2F83A +:10C170001C3589467F4A3146FCF75EFA7E4B7F4A82 +:10C180001A600020FBF76CFB7D4E7E4AB61AF6085B +:10C190004FF0000B5E457C4940F08180009A7B4B5C +:10C1A0007B4813600021FBF7ABFE00287FF450AF03 +:10C1B000A36B002B7ED10023764E774A3370774BEA +:10C1C0009B1ADC08764B012119704FF0000B5C457F +:10C1D0007AD102221A70734C734900232360734B87 +:10C1E0006360202204F10800FBF776FD20227049ED +:10C1F00004F12800FBF770FD6E4B23603378002BB1 +:10C200006CD16D4B604A1A606C4B6D4C00221A70F9 +:10C21000D4F800340BB1FBF731F9019B2A781B7875 +:10C220009A4260D1FBF755F9F2E7FEF7D5F8012500 +:10C2300055E7FEF70FF851E700287FF474AE614828 +:10C24000FEF7AAFF6048FDF727FC5D4B01214FF484 +:10C250002032C3F80C15C3F80425D3F808250A4387 +:10C26000C3F80825594A1960C3F80026D4F85C3190 +:10C27000D4F89C211340013351D1FDF7F7FDE7E6D7 +:10C28000002DDCD0FEF7D4FE5148FEF785FF514863 +:10C29000FEF782FF47F2305641E7FBF71DF952F8EF +:10C2A0003B300846984700287FF4D2AE0BF1010BD3 +:10C2B000344A6FE7FEF74AFC00283FF47CAF636A1C +:10C2C000003318BF012377E752F83B108847002856 +:10C2D00081D10BF1010B324B2F4A78E7FEF764FC5A +:10C2E00001288ED0B4E62B783A68B9F80060D8F807 +:10C2F0000000DBB219B202EBC10C03FB060052F8DE +:10C300003120BCF8041090472B78BAF80020DBB23B +:10C310009A4286BF0133DBB200232B707DE74FF4D6 +:10C320007F20FBF713FD57222B494FF47F20FBF7AB +:10C33000EBFCA2E7236BEA1A19441844FDF7EEFF61 +:10C3400000287FF4CBAEBCE68478002094740020F3 +:10C35000C1740F00945D00207459002054770020B0 +:10C360004C7700205C770020587700205A77002017 +:10C370004C720020FD740F0024780020E9870F0024 +:10C3800048CD0F0040CD0F0061780F00A0CC0F000A +:10C39000405900202C78002040CD0F0040CD0F00E8 +:10C3A0002178002004770020C0CC0F00A0CA0F0025 +:10C3B000E0CC0F007052464D6C7500206475002073 +:10C3C0000000014072CC0F00A08601003546526E7D +:10C3D000DCCB0F0068CC0F00607800205077002085 +:10C3E0000A44914200F1FF3300D1704710B511F8B3 +:10C3F000014B03F8014F9142F9D110BD30B501391D +:10C400000024A24201D1002005E0035D01340D5D4E +:10C41000AB42F6D0581B30BD53B94AB9002908BF0A +:10C4200000281CBF4FF0FF314FF0FF3000F074B90F +:10C43000ADF1080C6DE904CE00F006F8DDF804E07B +:10C44000DDE9022304B070472DE9F047089E044659 +:10C450008846002B40F085808A42154648D9B2FABA +:10C4600082F24AB1C2F1200701FA02F320FA07F77B +:10C47000954047EA030894404FEA154E230CB8FB59 +:10C48000FEF71FFA85FC0EFB178843EA084307FBFB +:10C490000CF1994209D9EB1807F1FF3080F0E380E5 +:10C4A000994240F2E080023F2B445B1AA4B2B3FBF6 +:10C4B000FEF00EFB103344EA034400FB0CFCA445E1 +:10C4C00009D92C1900F1FF3380F0CB80A44540F24C +:10C4D000C88002382C4440EA0740A4EB0C04002733 +:10C4E0001EB1D4400023C6E900433946BDE8F087B9 +:10C4F000002A53D0B2FA82F2002A40F0B680491BDB +:10C500004FEA154E1FFA85F80127B1FBFEFC230CFC +:10C510000EFB1C1143EA014308FB0CF1994207D9B9 +:10C52000EB180CF1FF3002D2994200F2EC80844605 +:10C53000591AA3B2B1FBFEF00EFB101443EA0444F7 +:10C5400008FB00F8A04507D92C1900F1FF3302D2EF +:10C55000A04500F2DC801846A4EB080440EA0C4039 +:10C56000BEE78B4208D9002E78D00027C6E900012B +:10C5700038463946BDE8F087B3FA83F77FB98B4276 +:10C5800002D3824200F2BD80841A61EB03030120D2 +:10C590009846002EA9D0C6E90048A6E7FFDEC7F1FD +:10C5A0002005BB4022FA05FC4CEA030C01FA07F413 +:10C5B00020FA05F821FA05F34FEA1C4E48EA040474 +:10C5C000B3FBFEF9210C0EFB19331FFA8CF841EA7C +:10C5D000034309FB08FA9A4502FA07F200FA07F149 +:10C5E0000BD91CEB030309F1FF3080F088809A45DA +:10C5F00040F28580A9F102096344A3EB0A03A4B2C7 +:10C60000B3FBFEF00EFB103344EA034400FB08F8D2 +:10C61000A04508D91CEB040400F1FF336BD2A04500 +:10C6200069D90238644440EA0940A4EB0804A0FB3D +:10C6300002894C45C6464B4654D351D0002E69D092 +:10C64000B1EB0E0264EB030404FA05F522FA07F3DA +:10C65000FC401D43C6E90054002746E737463046F4 +:10C6600043E7184637E707461FE7C2F1200320FAE1 +:10C6700003F7954001FA02F021FA03F34FEA154E51 +:10C680003843010CB3FBFEF71FFA85F80EFB173396 +:10C6900041EA034107FB08F38B4204FA02F407D98D +:10C6A000691807F1FF3C28D28B4226D9023F294462 +:10C6B000CB1A81B2B3FBFEF00EFB103341EA03410B +:10C6C00000FB08F38B4207D9691800F1FF3C10D238 +:10C6D0008B420ED902382944C91A40EA074714E7A9 +:10C6E0004145ABD2B8EB020E69EB0C0201381346A0 +:10C6F000A4E76046F0E7184695E76746D8E7814625 +:10C700007BE7384645E7ACF1020C2B4410E70238D2 +:10C710002C4421E73746E8E6704700BFF8B500BF74 +:10C72000F8BC08BC9E467047F8B500BFF8BC08BC12 +:10C730009E467047B857002050494E005741544319 +:10C7400048444F47005357205245534554004C4FDF +:10C75000434B5550004F46460000D75D7552957CBF +:10C760001513FF1DFF7F645ABF7EA91ABF7E40408C +:10C77000922E0011922E6040823C554BF94D504B49 +:10C78000423DFE572559F57B255B3E577F594A7937 +:10C790008A794A59775F9E656D5BAD6B2D5B155746 +:10C7A0007B5B545B527B5175D655ED076D0492044B +:10C7B0001505D527077A0F00AB790F0039790F00DF +:10C7C000E97B0F00CF7B0F00917B0F00517B0F00A7 +:10C7D000EDAD0F000000000050EADA308883B89F0A +:10C7E000604F15F30000C98E44667554617267008E +:10C7F0000010020008200001FFFFFFFFFFFFFFFF06 +:10C80000FFFFFFFF0000000000000000000000002C +:10C8100001000000FFFFFFFF512563FCC2CAB9F30E +:10C82000849E17A7ADFAE6BCFFFFFFFFFFFFFFFFE7 +:10C8300000000000FFFFFFFF96C298D84539A1F421 +:10C84000A033EB2D817D0377F240A463E5E6BCF8CD +:10C8500047422CE1F2D1176BF551BF376840B6CB98 +:10C86000CE5E316B5733CE2B169E0F7C4AEBE78E94 +:10C870009B7F1AFEE242E34F4B60D2273E3CCE3B09 +:10C88000F6B053CCB0061D65BC86987655BDEBB3AB +:10C89000E7933AAAD835C65A45990F0049920F0036 +:10C8A000EB980F009D900F000000000000000000BA +:10C8B0000338FDD870470000011101FF010000009E +:10C8C00000000000021702FE9400000000C90F00E3 +:10C8D0000000000000000000000000000101000056 +:10C8E000010000000000000002050000210000001F +:10C8F0000000000000000000000000000000000038 +:10C90000011104FC0400000000000000021104FCFE +:10C910000400000000000000032104FC04100000DB +:10C9200000000000041101FF0100000000000000F1 +:10C93000051102FE0400000000000000061104FCC6 +:10C940000400000000000000071104FC04000000C7 +:10C9500000000000081701FF22000000DCC80F00E3 +:10C96000091001FF0100000078C90F00000000005D +:10C9700000000000000000000042544E31203D2025 +:10C980005245424F4F54004552524F520044495372 +:10C99000434F4E4E45435400535441525445440076 +:10C9A000011704FC98000000B8C80F00021704FC2F +:10C9B000DC000000C4C90F000000000000000000FF +:10C9C000000000000107000098000000B8C80F0038 +:10C9D000020100000100000000000000030500004B +:10C9E0004100000000000000000000000000000006 +:10C9F00000000000464C4153482048454144455200 +:10CA0000002041444452002053495A4500204352DB +:10CA100043002056455253494F4E00435243205441 +:10CA20004553542E2E2E00435243204D49534D4121 +:10CA3000544348004E4F5420464C415348494E47BA +:10CA400000435243204F4B0054455354494E472E08 +:10CA50002E2E0044494646204154200020562000F6 +:10CA600052454D4F5645204845414445522E0046BB +:10CA700049524D5741524520444946464552454E3C +:10CA8000542E00464C415348494E472E2E2E00420C +:10CA9000494E415259204D4154434845532E0000C0 +:10CAA0002800480000204000B18C0F00B98A0F0018 +:10CAB000598C0F0051AF0F002000000078000000DB +:10CAC00000000000982F8A4291443771CFFBC0B517 +:10CAD000A5DBB5E95BC25639F111F159A4823F9249 +:10CAE000D55E1CAB98AA07D8015B8312BE853124A2 +:10CAF000C37D0C55745DBE72FEB1DE80A706DC9B63 +:10CB000074F19BC1C1699BE48647BEEFC69DC10F0E +:10CB1000CCA10C246F2CE92DAA84744ADCA9B05C4A +:10CB2000DA88F97652513E986DC631A8C82703B00D +:10CB3000C77F59BFF30BE0C64791A7D55163CA061B +:10CB400067292914850AB72738211B2EFC6D2C4D27 +:10CB5000130D385354730A65BB0A6A762EC9C28115 +:10CB6000852C7292A1E8BFA24B661AA8708B4BC2AB +:10CB7000A3516CC719E892D1240699D685350EF4D5 +:10CB800070A06A1016C1A419086C371E4C7748278C +:10CB9000B5BCB034B30C1C394AAAD84E4FCA9C5B02 +:10CBA000F36F2E68EE828F746F63A5781478C88453 +:10CBB0000802C78CFAFFBE90EB6C50A4F7A3F9BE35 +:10CBC000F27871C60D0A0000001000000400000099 +:10CBD000010000000600180000005802424C2032FC +:10CBE0007632300A00434845434B2053544F52415C +:10CBF0004745004E4F204E455720465700544F4F53 +:10CC0000204D414E592046494C455300464F554E04 +:10CC100044204649524D574152450042414E4B3007 +:10CC200020494E56414C49440052454C45415345DC +:10CC30002042544E3120464F52204446550D0A4260 +:10CC4000544E3120544F20424F4F540D0A0D0A3C90 +:10CC500020202020202020202020202020202020D4 +:10CC60002020203E0D003D004446552053544152A3 +:10CC700054000D0A424F4F54494E472E2E2E0000AD +:10CC80008D800F0081800F0039A20F001D800F00E2 +:10CC9000B97F0F00F97E0F00F17E0F00A97F0F0012 +:10CCA000017F0F00FDAE0F0047A20F0095AE0F00F1 +:10CCB00029AE0F00FD7E0F00F57E0F00E17E0F0014 +:10CCC0004901B952C5EF41DE27476A5527B31CE732 +:10CCD0002D1ECB0FA00C3792BE6AB2E99B240395A0 +:10CCE0006593690B5EF09CFDA5C02CEEF1ECA5A14F +:10CCF000470A9B3F814C5FD407D8A31404C609D3CD +:10CD000068CD0F0070CD0F000800000088CD0F0027 +:10CD100090CD0F000800000090CD0F0098CD0F00BF +:10CD20000800000078CD0F0088CD0F00080000003B +:10CD300070CD0F0078CD0F0008000000000000004B +:08CD40004DA40F00D5820F0085 +:10CD4800846100200C7200202D7200208C6100206C +:04CD58008000000057 +:0CCD5C00000000000300000009760F003A +:08CD6800099F0F00000000000C +:08CD700055850F0000000000D2 +:10CD7800457E0F0000000000B57D0F000000000098 +:08CD8800CD7E0F000000000049 +:08CD9000D5830F000000000034 +:08CD9800A8A4FF7F01000000C8 +:10CDA0000000000000000000000000000000000083 +:10CDB0000000000000000000000000000000000073 +:10CDC0000000000000000000000000000000000063 +:10CDD0000000000000000000000000000000000053 +:10CDE0000000000000000000000000000000000043 +:10CDF0000000000000000000000000000000000033 +:10CE0000F0B501444FF0A0434FF400444FF4803597 +:10CE100010F8016B072246FA02F7FF074CBFC3F870 +:10CE20000845C3F80C4512F1FF32C3F80855C3F8A2 +:10CE30000C55F0D28142EBD1F0BD38B54FF0A04493 +:10CE40004FF48045C4F80C55FFF7DAFFC4F80855D5 +:10CE500038BD4FF0A0434FF4004270B50144C3F811 +:10CE60000C254FF4803608240022C3F80865D3F857 +:10CE70001055C3F80C65C5F34035013C45EA420244 +:10CE8000F3D100F8012B8142EDD170BDF7B503233A +:10CE90008DF804304FF0A0440B0C49BAADF80610E1 +:10CEA0004FF48045042117468DF805300646C4F836 +:10CEB0000C550DEB0100FFF7A3FF39463046FFF795 +:10CEC000C8FFC4F8085503B0F0BD37B54FF0A04413 +:10CED00005234FF480458DF807300DF10700C4F8A5 +:10CEE0000C550121FFF78CFF0DF107000121FFF721 +:10CEF000B0FFC4F808559DF8070003B030BD00002E +:10CF00000090D003010000000200000002000000B9 +:10CF1000FF00FFFF00100140110004005700000057 +:08CF200025720F0001720F00E1 +:10CF28000000000000000000D9750F00001000008C +:04CF380000001000E5 +:020000020000FC +:020000041000EA +:0410140000700F0059 +:0410180000E00F00E5 +:04000003F00073395D +:00000001FF diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 6dcaad035..606e59d89 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -3,7 +3,7 @@ -

This tool allows you to update the firmware on Bangle.js 2 devices +

This tool allows you to update the firmware on Bangle.js 2 devices from within the App Loader.

@@ -12,11 +12,12 @@ see the Bangle.js 1 instructions

    -

    Your current firmware version is unknown and DFU is unknown

    +

    Your current firmware version is unknown and DFU is unknown. + The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.