From b67412299f0af5d8e0dc65f07c958b32249198fc Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 21 May 2023 15:13:39 +0100 Subject: [PATCH 001/242] layout.d.ts: render() takes a RenderedHierarchy (x/y/w/h) --- typescript/types/layout.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/types/layout.d.ts b/typescript/types/layout.d.ts index 8c5706d0b..6694575d9 100644 --- a/typescript/types/layout.d.ts +++ b/typescript/types/layout.d.ts @@ -120,6 +120,6 @@ declare module Layout_ { src: Image | (() => Image), } | { type: "custom", - render: (h: Hierarchy) => void, + render: (h: RenderedHierarchy) => void, }; } From 1c40c44d7ed4c8ce36da2c370991986ece8bc74c Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 5 Jul 2023 15:42:47 +0200 Subject: [PATCH 002/242] Fix for negative coordinates --- apps/gipy/ChangeLog | 5 ++ apps/gipy/TODO | 26 ++++----- apps/gipy/app.js | 94 ++++++++++++++++++--------------- apps/gipy/pkg/gps.d.ts | 4 +- apps/gipy/pkg/gps.js | 38 ++++++------- apps/gipy/pkg/gps_bg.wasm | Bin 748078 -> 748683 bytes apps/gipy/pkg/gps_bg.wasm.d.ts | 4 +- 7 files changed, 90 insertions(+), 81 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 8646ba11a..9e9654fd0 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -87,3 +87,8 @@ * Reduce framerate if locked * Stroke to move around in the map * Fix for missing paths in display + +0.20: + * Large display for instant speed + * Bugfix for negative coordinates + * Disable menu while the map is not loaded diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 266a1c5c9..c94211c1d 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,19 +1,15 @@ ++ disable backlight during day ? ++ put back foot only ways ++ disable bluetooth ++ disable lcd completely ++ try fiddling with jit ++ put back street names ++ put back shortest paths but with points cache this time and jit ++ how to display paths from shortest path ? + + +misc: + use Bangle.project(latlong) -* additional features -- config screen - - are we on foot (and should use compass) - -- we need to buzz 200m before sharp turns (or even better, 30seconds) -(and look at more than next point) - -- display distance to next water/toilet ? -- display scale (100m) - -- compress path ? - -* misc - -- code is becoming messy diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 60e4bb5af..d34cb75fe 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -152,7 +152,7 @@ class Map { color_array[2] / 255, ]; offset += 3; - this.first_tile = Uint32Array(buffer, offset, 2); // absolute tile id of first tile + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile offset += 2 * 4; this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height offset += 2 * 4; @@ -471,7 +471,7 @@ class Map { class Interests { constructor(buffer, offset) { - this.first_tile = Uint32Array(buffer, offset, 2); // absolute tile id of first tile + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile offset += 2 * 4; this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height offset += 2 * 4; @@ -971,12 +971,18 @@ class Status { .drawString( "" + approximate_speed + - "km/h (in." + - approximate_instant_speed + - ")", + "km/h", 0, g.getHeight() - 15 ); + + g.setFont("6x8:3") + .setFontAlign(1, -1, 0) + .drawString( + ""+approximate_instant_speed, + g.getWidth(), + g.getHeight() - 22 + ); } if (this.path === null || this.position === null) { @@ -1374,6 +1380,46 @@ function start_gipy(path, maps, interests) { console.log("starting"); status = new Status(path, maps, interests); + setWatch( + function () { + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { title: "choose action" }, + "Go Backward": { + value: go_backwards, + format: (v) => (v ? "On" : "Off"), + onchange: (v) => { + go_backwards = v; + }, + }, + Zoom: { + value: zoomed, + format: (v) => (v ? "In" : "Out"), + onchange: (v) => { + status.invalidate_caches(); + zoomed = v; + }, + }, + "back to map": function () { + in_menu = false; + E.showMenu(); + g.clear(); + g.flip(); + if (status !== null) { + status.display(); + } + }, + }; + E.showMenu(menu); + }, + BTN1, + { repeat: true } + ); + + if (status.path !== null) { let start = status.path.point(0); status.displayed_position = start; @@ -1455,44 +1501,6 @@ function start_gipy(path, maps, interests) { } } -setWatch( - function () { - if (in_menu) { - return; - } - in_menu = true; - const menu = { - "": { title: "choose action" }, - "Go Backward": { - value: go_backwards, - format: (v) => (v ? "On" : "Off"), - onchange: (v) => { - go_backwards = v; - }, - }, - Zoom: { - value: zoomed, - format: (v) => (v ? "In" : "Out"), - onchange: (v) => { - status.invalidate_caches(); - zoomed = v; - }, - }, - "back to map": function () { - in_menu = false; - E.showMenu(); - g.clear(); - g.flip(); - if (status !== null) { - status.display(); - } - }, - }; - E.showMenu(menu); - }, - BTN1, - { repeat: true } -); let files = s.list(".gps"); if (files.length <= 1) { diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts index 15a90b1e8..c881052f4 100644 --- a/apps/gipy/pkg/gps.d.ts +++ b/apps/gipy/pkg/gps.d.ts @@ -67,11 +67,11 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; readonly __wbindgen_export_2: WebAssembly.Table; - readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1: (a: number, b: number, c: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_exn_store: (a: number) => void; - readonly wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b: (a: number, b: number, c: number, d: number) => void; + readonly wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137: (a: number, b: number, c: number, d: number) => void; } export type SyncInitInput = BufferSource | WebAssembly.Module; diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js index ce9ebe5f8..39c2a6804 100644 --- a/apps/gipy/pkg/gps.js +++ b/apps/gipy/pkg/gps.js @@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_24(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(arg0, arg1, addHeapObject(arg2)); } function _assertClass(instance, klass) { @@ -369,7 +369,7 @@ function handleError(f, args) { } } function __wbg_adapter_84(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); + wasm.wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } /** @@ -460,6 +460,21 @@ function getImports() { const ret = getObject(arg0).fetch(getObject(arg1)); return addHeapObject(ret); }; + imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { + const ret = getObject(arg0).signal; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () { + const ret = new AbortController(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { + getObject(arg0).abort(); + }; + imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { const ret = new Headers(); return addHeapObject(ret); @@ -496,21 +511,6 @@ function getImports() { const ret = getObject(arg0).text(); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { - const ret = getObject(arg0).signal; - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () { - const ret = new AbortController(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { - getObject(arg0).abort(); - }; - imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; imports.wbg.__wbg_new_abda76e883ba8a5f = function() { const ret = new Error(); return addHeapObject(ret); @@ -675,8 +675,8 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2298 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 260, __wbg_adapter_24); + imports.wbg.__wbindgen_closure_wrapper2245 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 267, __wbg_adapter_24); return addHeapObject(ret); }; diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm index 6999cb94673213057a9f2e2d2480063a5033c031..8e0fbc07eb7d7cf8a96dd646305f808eda4e49cc 100644 GIT binary patch delta 135369 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 A$N&HU delta 135283 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 Mk)2sgx-#JZ0Gs6sJpcdz diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts index df9a024fa..b4303ee30 100644 --- a/apps/gipy/pkg/gps_bg.wasm.d.ts +++ b/apps/gipy/pkg/gps_bg.wasm.d.ts @@ -12,8 +12,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe export function __wbindgen_malloc(a: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_exn_store(a: number): void; -export function wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(a: number, b: number, c: number, d: number): void; +export function wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(a: number, b: number, c: number, d: number): void; From fd5cc494c522dc110b6b2f4f64f6bd81490d5a57 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 5 Jul 2023 15:46:45 +0200 Subject: [PATCH 003/242] gipy: readme update --- apps/gipy/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 44a8b9bcd..242282dbf 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -119,4 +119,7 @@ I had to go back uphill by quite a distance. Feel free to give me feedback : is it useful for you ? what other features would you like ? +If you want to raise issues the main repository is [https://github.com/wagnerf42/BangleApps](here) and +the rust code doing the actual map computations is located [https://github.com/wagnerf42/gps](here). + frederic.wagner@imag.fr From d165c822d3a3e485a1f31f47b06b47a2cbc24c7e Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 5 Jul 2023 15:59:15 +0200 Subject: [PATCH 004/242] gipy: forgot to bump metadata up --- apps/gipy/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 8b8c88780..7dd4123f6 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.19", + "version": "0.20", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", From e40cc26240f5303cabd1c6cf1c1c3ec337b60982 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 7 Jul 2023 09:54:55 +0200 Subject: [PATCH 005/242] gipy: new settings + attempts at power saving --- apps/gipy/ChangeLog | 3 + apps/gipy/app.js | 2780 +++++++++++++++++++++-------------------- apps/gipy/settings.js | 71 +- 3 files changed, 1448 insertions(+), 1406 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 9e9654fd0..c2e9a21b4 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -92,3 +92,6 @@ * Large display for instant speed * Bugfix for negative coordinates * Disable menu while the map is not loaded + * Turn screen off while idling to save battery + * New setting : disable buzz on turns + * New setting : turn bluetooth off to save battery diff --git a/apps/gipy/app.js b/apps/gipy/app.js index d34cb75fe..866849efc 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -6,62 +6,63 @@ let zoomed = true; let status; let interests_colors = [ - 0xffff, // Waypoints, white - 0xf800, // Bakery, red - 0x001f, // DrinkingWater, blue - 0x07ff, // Toilets, cyan - 0x07e0, // Artwork, green + 0xffff, // Waypoints, white + 0xf800, // Bakery, red + 0x001f, // DrinkingWater, blue + 0x07ff, // Toilets, cyan + 0x07e0, // Artwork, green ]; let Y_OFFSET = 20; let s = require("Storage"); -var settings = Object.assign( - { - lost_distance: 50, - }, - s.readJSON("gipy.json", true) || {} +var settings = Object.assign({ + lost_distance: 50, + buzz_on_turns: false, + disable_bluetooth: true, + }, + s.readJSON("gipy.json", true) || {} ); let profile_start_times = []; let splashscreen = require("heatshrink").decompress( - atob( - "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" - ) + atob( + "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" + ) ); function start_profiling() { - profile_start_times.push(getTime()); + profile_start_times.push(getTime()); } function end_profiling(label) { - let end_time = getTime(); - let elapsed = end_time - profile_start_times.pop(); - console.log("profile:", label, "took", elapsed); + let end_time = getTime(); + let elapsed = end_time - profile_start_times.pop(); + console.log("profile:", label, "took", elapsed); } // return the index of the largest element of the array which is <= x function binary_search(array, x) { - let start = 0, - end = array.length; + let start = 0, + end = array.length; - while (end - start >= 0) { - let mid = Math.floor((start + end) / 2); - if (array[mid] == x) { - return mid; - } else if (array[mid] < x) { - if (array[mid + 1] > x) { - return mid; - } - start = mid + 1; - } else end = mid - 1; - } - if (array[start] > x) { - return null; - } else { - return start; - } + while (end - start >= 0) { + let mid = Math.floor((start + end) / 2); + if (array[mid] == x) { + return mid; + } else if (array[mid] < x) { + if (array[mid + 1] > x) { + return mid; + } + start = mid + 1; + } else end = mid - 1; + } + if (array[start] > x) { + return null; + } else { + return start; + } } // return a string containing estimated time of arrival. @@ -69,1446 +70,1465 @@ function binary_search(array, x) { // remaining distance in km // hour, minutes is current time function compute_eta(hour, minutes, approximate_speed, remaining_distance) { - if (isNaN(approximate_speed) || approximate_speed < 0.1) { - return ""; - } - let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes - let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); - let eta_minutes = eta_in_minutes % 60; - let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; - if (eta_minutes < 10) { - return eta_hour.toString() + ":0" + eta_minutes; - } else { - return eta_hour.toString() + ":" + eta_minutes; - } + if (isNaN(approximate_speed) || approximate_speed < 0.1) { + return ""; + } + let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes + let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); + let eta_minutes = eta_in_minutes % 60; + let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; + if (eta_minutes < 10) { + return eta_hour.toString() + ":0" + eta_minutes; + } else { + return eta_hour.toString() + ":" + eta_minutes; + } } class TilesOffsets { - constructor(buffer, offset) { - let type_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - this.entry_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; - this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); - offset += 2 * non_empty_tiles_number; - if (type_size == 24) { - this.non_empty_tiles_ends = Uint24Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 3 * non_empty_tiles_number; - } else if (type_size == 16) { - this.non_empty_tiles_ends = Uint16Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 2 * non_empty_tiles_number; - } else { - throw "unknown size"; + constructor(buffer, offset) { + let type_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + this.entry_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; + this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); + offset += 2 * non_empty_tiles_number; + if (type_size == 24) { + this.non_empty_tiles_ends = Uint24Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 3 * non_empty_tiles_number; + } else if (type_size == 16) { + this.non_empty_tiles_ends = Uint16Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 2 * non_empty_tiles_number; + } else { + throw "unknown size"; + } + return [this, offset]; } - return [this, offset]; - } - tile_start_offset(tile_index) { - if (tile_index <= this.non_empty_tiles[0]) { - return 0; - } else { - return this.tile_end_offset(tile_index - 1); + tile_start_offset(tile_index) { + if (tile_index <= this.non_empty_tiles[0]) { + return 0; + } else { + return this.tile_end_offset(tile_index - 1); + } } - } - tile_end_offset(tile_index) { - let me_or_before = binary_search(this.non_empty_tiles, tile_index); - if (me_or_before === null) { - return 0; + tile_end_offset(tile_index) { + let me_or_before = binary_search(this.non_empty_tiles, tile_index); + if (me_or_before === null) { + return 0; + } + if (me_or_before >= this.non_empty_tiles_ends.length) { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * + this.entry_size + ); + } else { + return this.non_empty_tiles_ends[me_or_before] * this.entry_size; + } } - if (me_or_before >= this.non_empty_tiles_ends.length) { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * - this.entry_size - ); - } else { - return this.non_empty_tiles_ends[me_or_before] * this.entry_size; + end_offset() { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * + this.entry_size + ); } - } - end_offset() { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * - this.entry_size - ); - } } class Map { - constructor(buffer, offset, filename) { - this.points_cache = []; // don't refetch points all the time - // header - let color_array = Uint8Array(buffer, offset, 3); - this.color = [ - color_array[0] / 255, - color_array[1] / 255, - color_array[2] / 255, - ]; - offset += 3; - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; + constructor(buffer, offset, filename) { + this.points_cache = []; // don't refetch points all the time + // header + let color_array = Uint8Array(buffer, offset, 3); + this.color = [ + color_array[0] / 255, + color_array[1] / 255, + color_array[2] / 255, + ]; + offset += 3; + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; - // tiles offsets - let res = new TilesOffsets(buffer, offset); - this.tiles_offsets = res[0]; - offset = res[1]; + // tiles offsets + let res = new TilesOffsets(buffer, offset); + this.tiles_offsets = res[0]; + offset = res[1]; - // now, do binary ways - // since the file is so big we'll go line by line - let binary_lines = []; - for (let y = 0; y < this.grid_size[1]; y++) { - let first_tile_start = this.tiles_offsets.tile_start_offset( - y * this.grid_size[0] - ); - let last_tile_end = this.tiles_offsets.tile_start_offset( - (y + 1) * this.grid_size[0] - ); - let size = last_tile_end - first_tile_start; - let string = s.read(filename, offset + first_tile_start, size); - let array = Uint8Array(E.toArrayBuffer(string)); - binary_lines.push(array); - } - this.binary_lines = binary_lines; - offset += this.tiles_offsets.end_offset(); - - return [this, offset]; - - // now do streets data header - // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); - // let streets_header_offset = 0; - // let full_streets_size = Uint32Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 4; - // let blocks_number = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // let labels_string_size = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // offset += streets_header_offset; - - // // continue with main streets labels - // main_streets_labels = s.read(filename, offset, labels_string_size); - // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); - // this.main_streets_labels = main_streets_labels.split(/\n/); - // offset += labels_string_size; - - // // continue with blocks start offsets - // this.blocks_offsets = Uint32Array( - // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) - // ); - // offset += blocks_number * 4; - - // // continue with compressed street blocks - // let encoded_blocks_size = - // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; - // this.compressed_streets = Uint8Array( - // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) - // ); - // offset += encoded_blocks_size; - } - - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - g.setColor(this.color[0], this.color[1], this.color[2]); - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); - - let limit = 1; - if (!zoomed) { - limit = 2; - } - for (let y = tile_y - limit; y <= tile_y + limit; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - limit; x <= tile_x + limit; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - if ( - this.tile_is_on_screen( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ) - ) { -// let colors = [ -// [0, 0, 0], -// [0, 0, 1], -// [0, 1, 0], -// [0, 1, 1], -// [1, 0, 0], -// [1, 0, 1], -// [1, 1, 0], -// [1, 1, 0.5], -// [0.5, 0, 0.5], -// [0, 0.5, 0.5], -// ]; - if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { - this.display_thick_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction + // now, do binary ways + // since the file is so big we'll go line by line + let binary_lines = []; + for (let y = 0; y < this.grid_size[1]; y++) { + let first_tile_start = this.tiles_offsets.tile_start_offset( + y * this.grid_size[0] ); - } else { - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction + let last_tile_end = this.tiles_offsets.tile_start_offset( + (y + 1) * this.grid_size[0] ); - } + let size = last_tile_end - first_tile_start; + let string = s.read(filename, offset + first_tile_start, size); + let array = Uint8Array(E.toArrayBuffer(string)); + binary_lines.push(array); } - } + this.binary_lines = binary_lines; + offset += this.tiles_offsets.end_offset(); + + return [this, offset]; + + // now do streets data header + // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); + // let streets_header_offset = 0; + // let full_streets_size = Uint32Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 4; + // let blocks_number = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // let labels_string_size = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // offset += streets_header_offset; + + // // continue with main streets labels + // main_streets_labels = s.read(filename, offset, labels_string_size); + // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); + // this.main_streets_labels = main_streets_labels.split(/\n/); + // offset += labels_string_size; + + // // continue with blocks start offsets + // this.blocks_offsets = Uint32Array( + // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) + // ); + // offset += blocks_number * 4; + + // // continue with compressed street blocks + // let encoded_blocks_size = + // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; + // this.compressed_streets = Uint8Array( + // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) + // ); + // offset += encoded_blocks_size; } - } - tile_is_on_screen( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let height = g.getHeight(); - let center_x = width / 2; - let center_y = height / 2 + Y_OFFSET; - let side = this.side; - let tile_center_x = (tile_x + 0.5) * side; - let tile_center_y = (tile_y + 0.5) * side; - let scaled_center_x = (tile_center_x - current_x) * scale_factor; - let scaled_center_y = (tile_center_y - current_y) * scale_factor; - let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction; - let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction; - let on_screen_center_x = center_x - rotated_center_x; - let on_screen_center_y = center_y + rotated_center_y; + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + g.setColor(this.color[0], this.color[1], this.color[2]); + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); - let scaled_side = side * scale_factor * Math.sqrt(1/2); - - if (on_screen_center_x + scaled_side <= 0) { - return false; + let limit = 1; + if (!zoomed) { + limit = 2; + } + for (let y = tile_y - limit; y <= tile_y + limit; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - limit; x <= tile_x + limit; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; + } + if ( + this.tile_is_on_screen( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ) + ) { + // let colors = [ + // [0, 0, 0], + // [0, 0, 1], + // [0, 1, 0], + // [0, 1, 1], + // [1, 0, 0], + // [1, 0, 1], + // [1, 1, 0], + // [1, 1, 0.5], + // [0.5, 0, 0.5], + // [0, 0.5, 0.5], + // ]; + if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { + this.display_thick_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ); + } else { + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ); + } + } + } + } } - if (on_screen_center_x - scaled_side >= width) { - return false; + + tile_is_on_screen( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let height = g.getHeight(); + let center_x = width / 2; + let center_y = height / 2 + Y_OFFSET; + let side = this.side; + let tile_center_x = (tile_x + 0.5) * side; + let tile_center_y = (tile_y + 0.5) * side; + let scaled_center_x = (tile_center_x - current_x) * scale_factor; + let scaled_center_y = (tile_center_y - current_y) * scale_factor; + let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction; + let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction; + let on_screen_center_x = center_x - rotated_center_x; + let on_screen_center_y = center_y + rotated_center_y; + + let scaled_side = side * scale_factor * Math.sqrt(1 / 2); + + if (on_screen_center_x + scaled_side <= 0) { + return false; + } + if (on_screen_center_x - scaled_side >= width) { + return false; + } + if (on_screen_center_y + scaled_side <= 0) { + return false; + } + if (on_screen_center_y - scaled_side >= height) { + return false; + } + return true; } - if (on_screen_center_y + scaled_side <= 0) { - return false; + + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let line_start_offset = this.tiles_offsets.tile_start_offset( + tile_y * this.grid_size[0] + ); + let offset = + this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; + let upper_limit = + this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; + + let line = this.binary_lines[tile_y]; + // we need to copy both for correct results and for performances + // let's precompute also. + let cached_tile = new Float64Array(upper_limit - offset); + for (let i = offset; i < upper_limit; i += 2) { + let x = (tile_x + line.buffer[i] / 255) * scaled_side; + let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; + cached_tile[i - offset] = x; + cached_tile[i + 1 - offset] = y; + } + return cached_tile; } - if (on_screen_center_y - scaled_side >= height) { - return false; + + invalidate_caches() { + this.points_cache = []; } - return true; - } - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let line_start_offset = this.tiles_offsets.tile_start_offset( - tile_y * this.grid_size[0] - ); - let offset = - this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; - let upper_limit = - this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; - - let line = this.binary_lines[tile_y]; - // we need to copy both for correct results and for performances - // let's precompute also. - let cached_tile = new Float64Array(upper_limit - offset); - for (let i = offset; i < upper_limit; i += 2) { - let x = (tile_x + line.buffer[i] / 255) * scaled_side; - let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; - cached_tile[i - offset] = x; - cached_tile[i + 1 - offset] = y; + fetch_points(tile_x, tile_y, scaled_side) { + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } + } + if (this.points_cache.length > 40) { + this.points_cache.shift(); + } + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; } - return cached_tile; - } - invalidate_caches() { - this.points_cache = []; - } + display_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + let center_x = g.getWidth() / 2; + let center_y = g.getHeight() / 2 + Y_OFFSET; - fetch_points(tile_x, tile_y, scaled_side) { - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + + for (let i = 0; i < points.length; i += 4) { + let scaled_x = points[i] - scaled_current_x; + let scaled_y = points[i + 1] - scaled_current_y; + let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let final_x = center_x - rotated_x; + let final_y = center_y + rotated_y; + scaled_x = points[i + 2] - scaled_current_x; + scaled_y = points[i + 3] - scaled_current_y; + rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let new_final_x = center_x - rotated_x; + let new_final_y = center_y + rotated_y; + g.drawLine(final_x, final_y, new_final_x, new_final_y); + } } - if (this.points_cache.length > 40) { - this.points_cache.shift(); + + display_thick_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + let center_x = g.getWidth() / 2; + let center_y = g.getHeight() / 2 + Y_OFFSET; + + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + + for (let i = 0; i < points.length; i += 4) { + let scaled_x = points[i] - scaled_current_x; + let scaled_y = points[i + 1] - scaled_current_y; + let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let final_x = center_x - rotated_x; + let final_y = center_y + rotated_y; + scaled_x = points[i + 2] - scaled_current_x; + scaled_y = points[i + 3] - scaled_current_y; + rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let new_final_x = center_x - rotated_x; + let new_final_y = center_y + rotated_y; + + let xdiff = new_final_x - final_x; + let ydiff = new_final_y - final_y; + let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + let ox = (-ydiff / d) * 3; + let oy = (xdiff / d) * 3; + g.fillPoly([ + final_x + ox, + final_y + oy, + new_final_x + ox, + new_final_y + oy, + new_final_x - ox, + new_final_y - oy, + final_x - ox, + final_y - oy, + ]); + } } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; - } - - display_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; - - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - - for (let i = 0; i < points.length; i += 4) { - let scaled_x = points[i] - scaled_current_x; - let scaled_y = points[i + 1] - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = center_x - rotated_x; - let final_y = center_y + rotated_y; - scaled_x = points[i + 2] - scaled_current_x; - scaled_y = points[i + 3] - scaled_current_y; - rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let new_final_x = center_x - rotated_x; - let new_final_y = center_y + rotated_y; - g.drawLine(final_x, final_y, new_final_x, new_final_y); - } - } - - display_thick_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; - - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - - for (let i = 0; i < points.length; i += 4) { - let scaled_x = points[i] - scaled_current_x; - let scaled_y = points[i + 1] - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = center_x - rotated_x; - let final_y = center_y + rotated_y; - scaled_x = points[i + 2] - scaled_current_x; - scaled_y = points[i + 3] - scaled_current_y; - rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let new_final_x = center_x - rotated_x; - let new_final_y = center_y + rotated_y; - - let xdiff = new_final_x - final_x; - let ydiff = new_final_y - final_y; - let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - let ox = (-ydiff / d) * 3; - let oy = (xdiff / d) * 3; - g.fillPoly([ - final_x + ox, - final_y + oy, - new_final_x + ox, - new_final_y + oy, - new_final_x - ox, - new_final_y - oy, - final_x - ox, - final_y - oy, - ]); - } - } } class Interests { - constructor(buffer, offset) { - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; + constructor(buffer, offset) { + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; - let res = new TilesOffsets(buffer, offset); - offset = res[1]; - this.offsets = res[0]; - let end = this.offsets.end_offset(); - this.binary_interests = new Uint8Array(end); - let binary_interests = Uint8Array(buffer, offset, end); - for (let i = 0; i < end; i++) { - this.binary_interests[i] = binary_interests[i]; - } - offset += end; - this.points_cache = []; - return [this, offset]; - } - - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); - for (let y = tile_y - 1; y <= tile_y + 1; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - 1; x <= tile_x + 1; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; + let res = new TilesOffsets(buffer, offset); + offset = res[1]; + this.offsets = res[0]; + let end = this.offsets.end_offset(); + this.binary_interests = new Uint8Array(end); + let binary_interests = Uint8Array(buffer, offset, end); + for (let i = 0; i < end; i++) { + this.binary_interests[i] = binary_interests[i]; } - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } + offset += end; + this.points_cache = []; + return [this, offset]; } - } - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let offset = this.offsets.tile_start_offset(tile_num); - let upper_limit = this.offsets.tile_end_offset(tile_num); - - let tile_interests = []; - for (let i = offset; i < upper_limit; i += 3) { - let interest = this.binary_interests[i]; - let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; - let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; - if (interest >= interests_colors.length) { - throw "bad interest" + interest + "at" + tile_num + "offset" + i; - } - tile_interests.push(interest); - tile_interests.push(x); - tile_interests.push(y); + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + for (let y = tile_y - 1; y <= tile_y + 1; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - 1; x <= tile_x + 1; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; + } + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ); + } + } } - return tile_interests; - } - fetch_points(tile_x, tile_y, scaled_side) { - //TODO: factorize with map ? - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } + + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let offset = this.offsets.tile_start_offset(tile_num); + let upper_limit = this.offsets.tile_end_offset(tile_num); + + let tile_interests = []; + for (let i = offset; i < upper_limit; i += 3) { + let interest = this.binary_interests[i]; + let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; + let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; + if (interest >= interests_colors.length) { + throw "bad interest" + interest + "at" + tile_num + "offset" + i; + } + tile_interests.push(interest); + tile_interests.push(x); + tile_interests.push(y); + } + return tile_interests; } - if (this.points_cache.length > 40) { - this.points_cache.shift(); + fetch_points(tile_x, tile_y, scaled_side) { + //TODO: factorize with map ? + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } + } + if (this.points_cache.length > 40) { + this.points_cache.shift(); + } + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; - } - invalidate_caches() { - this.points_cache = []; - } - display_tile( - tile_x, - tile_y, - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let half_width = width / 2; - let half_height = g.getHeight() / 2 + Y_OFFSET; - let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - - let scaled_current_x = displayed_x * scale_factor; - let scaled_current_y = displayed_y * scale_factor; - - for (let i = 0; i < interests.length; i += 3) { - let type = interests[i]; - let x = interests[i + 1]; - let y = interests[i + 2]; - - let scaled_x = x - scaled_current_x; - let scaled_y = y - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = half_width - rotated_x; - let final_y = half_height + rotated_y; - - let color = interests_colors[type]; - if (type == 0) { - g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); - } - g.setColor(color).fillCircle(final_x, final_y, 5); + invalidate_caches() { + this.points_cache = []; + } + display_tile( + tile_x, + tile_y, + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let half_width = width / 2; + let half_height = g.getHeight() / 2 + Y_OFFSET; + let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + + let scaled_current_x = displayed_x * scale_factor; + let scaled_current_y = displayed_y * scale_factor; + + for (let i = 0; i < interests.length; i += 3) { + let type = interests[i]; + let x = interests[i + 1]; + let y = interests[i + 2]; + + let scaled_x = x - scaled_current_x; + let scaled_y = y - scaled_current_y; + let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let final_x = half_width - rotated_x; + let final_y = half_height + rotated_y; + + let color = interests_colors[type]; + if (type == 0) { + g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); + } + g.setColor(color).fillCircle(final_x, final_y, 5); + } } - } } class Status { - constructor(path, maps, interests) { - this.path = path; - this.maps = maps; - this.interests = interests; - let half_screen_width = g.getWidth() / 2; - let half_screen_height = g.getHeight() / 2; - let half_screen_diagonal = Math.sqrt( - half_screen_width * half_screen_width + - half_screen_height * half_screen_height - ); - this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates - this.on_path = true; // are we on the path or lost ? - this.position = null; // where we are - this.adjusted_cos_direction = 1; // cos of where we look at - this.adjusted_sin_direction = 0; // sin of where we look at - this.current_segment = null; // which segment is closest - this.reaching = null; // which waypoint are we reaching ? - this.distance_to_next_point = null; // how far are we from next point ? - this.projected_point = null; - - if (this.path !== null) { - let r = [0]; - // let's do a reversed prefix computations on all distances: - // loop on all segments in reversed order - let previous_point = null; - for (let i = this.path.len - 1; i >= 0; i--) { - let point = this.path.point(i); - if (previous_point !== null) { - r.unshift(r[0] + point.distance(previous_point)); - } - previous_point = point; - } - this.remaining_distances = r; // how much distance remains at start of each segment - } - this.starting_time = null; // time we start - this.advanced_distance = 0.0; - this.gps_coordinates_counter = 0; // how many coordinates did we receive - this.old_points = []; // record previous points but only when enough distance between them - this.old_times = []; // the corresponding times - } - invalidate_caches() { - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].invalidate_caches(); - } - if (this.interests !== null) { - this.interests.invalidate_caches(); - } - } - new_position_reached(position) { - // we try to figure out direction by looking at previous points - // instead of the gps course which is not very nice. - - let now = getTime(); - - if (this.old_points.length == 0) { - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); - return null; - } else { - let previous_point = this.old_points[this.old_points.length - 1]; - let distance_to_previous = previous_point.distance(position); - // gps signal is noisy but rarely above 5 meters - if (distance_to_previous < 5) { - return null; - } - } - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); - - let oldest_point = this.old_points[0]; - let distance_to_oldest = oldest_point.distance(position); - - // every 3 points we count the distance - if (this.gps_coordinates_counter % 3 == 0) { - if (distance_to_oldest < 150.0) { - // to avoid gps glitches - this.advanced_distance += distance_to_oldest; - } - } - - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - - if (this.old_points.length == 4) { - this.old_points.shift(); - this.old_times.shift(); - } - // let's just take angle of segment between newest point and a point a bit before - let previous_index = this.old_points.length - 3; - if (previous_index < 0) { - previous_index = 0; - } - let diff = position.minus(this.old_points[previous_index]); - let angle = Math.atan2(diff.lat, diff.lon); - return angle; - } - update_position(new_position, maybe_direction, timestamp) { - let direction = this.new_position_reached(new_position); - if (direction === null) { - if (maybe_direction === null) { - return; - } else { - direction = maybe_direction; - } - } - if (in_menu) { - return; - } - - this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); - this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); - this.angle = direction; - let cos_direction = Math.cos(direction); - let sin_direction = Math.sin(direction); - this.position = new_position; - - // we will display position of where we'll be at in a few seconds - // and not where we currently are. - // this is because the display has more than 1sec duration. - this.displayed_position = new Point( - new_position.lon + cos_direction * this.instant_speed * 0.00001, - new_position.lat + sin_direction * this.instant_speed * 0.00001 - ); - - // abort if we are late - // if (timestamp !== null) { - // let elapsed = Date.now() - timestamp; - // if (elapsed > 1000) { - // console.log("we are late"); - // return; - // } - // console.log("we are not late"); - // } - - if (this.path !== null) { - // detect segment we are on now - let res = this.path.nearest_segment( - this.displayed_position, - Math.max(0, this.current_segment - 1), - Math.min(this.current_segment + 2, this.path.len - 1), - cos_direction, - sin_direction - ); - let orientation = res[0]; - let next_segment = res[1]; - - if (this.is_lost(next_segment)) { - // start_profiling(); - // it did not work, try anywhere - res = this.path.nearest_segment( - this.displayed_position, - 0, - this.path.len - 1, - cos_direction, - sin_direction + constructor(path, maps, interests) { + this.path = path; + this.maps = maps; + this.interests = interests; + let half_screen_width = g.getWidth() / 2; + let half_screen_height = g.getHeight() / 2; + let half_screen_diagonal = Math.sqrt( + half_screen_width * half_screen_width + + half_screen_height * half_screen_height ); - orientation = res[0]; - next_segment = res[1]; - // end_profiling("repositioning"); - } - // now check if we strayed away from path or back to it - let lost = this.is_lost(next_segment); - if (this.on_path == lost) { - // if status changes - if (lost) { - Bangle.buzz(); // we lost path - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); + this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates + this.on_path = true; // are we on the path or lost ? + this.position = null; // where we are + this.adjusted_cos_direction = 1; // cos of where we look at + this.adjusted_sin_direction = 0; // sin of where we look at + this.current_segment = null; // which segment is closest + this.reaching = null; // which waypoint are we reaching ? + this.distance_to_next_point = null; // how far are we from next point ? + this.projected_point = null; + + if (this.path !== null) { + let r = [0]; + // let's do a reversed prefix computations on all distances: + // loop on all segments in reversed order + let previous_point = null; + for (let i = this.path.len - 1; i >= 0; i--) { + let point = this.path.point(i); + if (previous_point !== null) { + r.unshift(r[0] + point.distance(previous_point)); + } + previous_point = point; + } + this.remaining_distances = r; // how much distance remains at start of each segment } - this.on_path = !lost; - } - - this.current_segment = next_segment; - - // check if we are nearing the next point on our path and alert the user - let next_point = this.current_segment + (1 - orientation); - this.distance_to_next_point = Math.ceil( - this.position.distance(this.path.point(next_point)) - ); - - // disable gps when far from next point and locked - // if (Bangle.isLocked() && !settings.keep_gps_alive) { - // let time_to_next_point = - // (this.distance_to_next_point * 3.6) / settings.max_speed; - // if (time_to_next_point > 60) { - // Bangle.setGPSPower(false, "gipy"); - // setTimeout(function () { - // Bangle.setGPSPower(true, "gipy"); - // }, time_to_next_point); - // } - // } - if (this.reaching != next_point && this.distance_to_next_point <= 100) { - this.reaching = next_point; - let reaching_waypoint = this.path.is_waypoint(next_point); - if (reaching_waypoint) { - Bangle.buzz(); - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - if (Bangle.isLocked()) { - Bangle.setLocked(false); - } + this.starting_time = null; // time we start + this.advanced_distance = 0.0; + this.gps_coordinates_counter = 0; // how many coordinates did we receive + this.old_points = []; // record previous points but only when enough distance between them + this.old_times = []; // the corresponding times + } + invalidate_caches() { + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].invalidate_caches(); + } + if (this.interests !== null) { + this.interests.invalidate_caches(); } - } } + new_position_reached(position) { + // we try to figure out direction by looking at previous points + // instead of the gps course which is not very nice. - // abort most frames if locked - if (Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) { - return; - } + let now = getTime(); - // re-display - this.display(); - } - display_direction() { - //TODO: go towards point on path at 20 meter - if (this.current_segment === null) { - return; - } - let next_point = this.path.point(this.current_segment + (1 - go_backwards)); - - let distance_to_next_point = Math.ceil( - this.projected_point.distance(next_point) - ); - let towards; - if (distance_to_next_point < 20) { - towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); - } else { - towards = next_point; - } - let diff = towards.minus(this.projected_point); - direction = Math.atan2(diff.lat, diff.lon); - - let full_angle = direction - this.angle; - // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); - // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); - - let scale; - if (zoomed) { - scale = this.scale_factor; - } else { - scale = this.scale_factor / 2; - } - - c = this.projected_point.coordinates( - this.displayed_position, - this.adjusted_cos_direction, - this.adjusted_sin_direction, - scale - ); - - let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); - let cos2 = Math.cos(full_angle + Math.PI / 2); - let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); - let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); - let sin2 = Math.sin(-full_angle - Math.PI / 2); - let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); - g.setColor(0, 1, 0).fillPoly([ - c[0] + cos1 * 15, - c[1] + sin1 * 15, - c[0] + cos2 * 20, - c[1] + sin2 * 20, - c[0] + cos3 * 15, - c[1] + sin3 * 15, - c[0] + cos3 * 10, - c[1] + sin3 * 10, - c[0] + cos2 * 15, - c[1] + sin2 * 15, - c[0] + cos1 * 10, - c[1] + sin1 * 10, - ]); - } - remaining_distance() { - let remaining_in_correct_orientation = - this.remaining_distances[this.current_segment + 1] + - this.position.distance(this.path.point(this.current_segment + 1)); - - if (go_backwards) { - return this.remaining_distances[0] - remaining_in_correct_orientation; - } else { - return remaining_in_correct_orientation; - } - } - // check if we are lost (too far from segment we think we are on) - // if we are adjust scale so that path will still be displayed. - // we do the scale adjustment here to avoid recomputations later on. - is_lost(segment) { - let projection = this.displayed_position.closest_segment_point( - this.path.point(segment), - this.path.point(segment + 1) - ); - this.projected_point = projection; // save this info for display - let distance_to_projection = this.displayed_position.distance(projection); - if (distance_to_projection > settings.lost_distance) { - return true; - } else { - return false; - } - } - display() { - if (displaying || in_menu) { - return; // don't draw on drawings - } - displaying = true; - g.clear(); - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } - - // start_profiling(); - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - // end_profiling("map"); - if (this.interests !== null) { - this.interests.display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - if (this.position !== null) { - this.display_path(); - } - - this.display_direction(); - this.display_stats(); - Bangle.drawWidgets(); - displaying = false; - } - display_stats() { - let now = new Date(); - let minutes = now.getMinutes().toString(); - if (minutes.length < 2) { - minutes = "0" + minutes; - } - let hours = now.getHours().toString(); - - // display the clock - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, 0, 24); - - let approximate_speed; - // display speed (avg and instant) - if (this.old_times.length > 0) { - let point_time = this.old_times[this.old_times.length - 1]; - let done_in = point_time - this.starting_time; - approximate_speed = Math.round( - (this.advanced_distance * 3.6) / done_in - ); - let approximate_instant_speed = Math.round(this.instant_speed * 3.6); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .drawString( - "" + - approximate_speed + - "km/h", - 0, - g.getHeight() - 15 - ); - - g.setFont("6x8:3") - .setFontAlign(1, -1, 0) - .drawString( - ""+approximate_instant_speed, - g.getWidth(), - g.getHeight() - 22 - ); - } - - if (this.path === null || this.position === null) { - return; - } - - let remaining_distance = this.remaining_distance(); - let rounded_distance = Math.round(remaining_distance / 100) / 10; - let total = Math.round(this.remaining_distances[0] / 100) / 10; - // now, distance to next point in meters - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString( - "" + this.distance_to_next_point + "m", - 0, - g.getHeight() - 49 - ); - - let forward_eta = compute_eta( - now.getHours(), - now.getMinutes(), - approximate_speed, - remaining_distance / 1000 - ); - - // now display ETA - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(forward_eta, 0, 42); - - // display distance on path - g.setFont("6x8:2").drawString( - "" + rounded_distance + "/" + total, - 0, - g.getHeight() - 32 - ); - - // display various indicators - if (this.distance_to_next_point <= 100) { - if (this.path.is_waypoint(this.reaching)) { - g.setColor(0.0, 1.0, 0.0) - .setFont("6x15") - .drawString("turn", g.getWidth() - 50, 30); - } - } - if (!this.on_path) { - g.setColor(1.0, 0.0, 0.0) - .setFont("6x15") - .drawString("lost", g.getWidth() - 55, 35); - } - } - display_path() { - // don't display all segments, only those neighbouring current segment - // this is most likely to be the correct display - // while lowering the cost a lot - // - // note that all code is inlined here to speed things up - let cos = this.adjusted_cos_direction; - let sin = this.adjusted_sin_direction; - let displayed_x = this.displayed_position.lon; - let displayed_y = this.displayed_position.lat; - let width = g.getWidth(); - let height = g.getHeight(); - let half_width = width / 2; - let half_height = height / 2 + Y_OFFSET; - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } - - if (this.path !== null) { - // compute coordinate for projection on path - let tx = (this.projected_point.lon - displayed_x) * scale_factor; - let ty = (this.projected_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let projected_x = half_width - Math.round(rotated_x); // x is inverted - let projected_y = half_height + Math.round(rotated_y); - - // display direction to next point if lost - if (!this.on_path) { - let next_point = this.path.point(this.current_segment + 1); - let previous_point = this.path.point(this.current_segment); - let nearest_point; - if ( - previous_point.fake_distance(this.position) < - next_point.fake_distance(this.position) - ) { - nearest_point = previous_point; + if (this.old_points.length == 0) { + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); + return null; } else { - nearest_point = next_point; + let previous_point = this.old_points[this.old_points.length - 1]; + let distance_to_previous = previous_point.distance(position); + // gps signal is noisy but rarely above 5 meters + if (distance_to_previous < 5) { + return null; + } } - let tx = (nearest_point.lon - displayed_x) * scale_factor; - let ty = (nearest_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); - } + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); - // display current-segment's projection - g.setColor(0, 0, 0); - g.fillCircle(projected_x, projected_y, 4); + let oldest_point = this.old_points[0]; + let distance_to_oldest = oldest_point.distance(position); + + // every 3 points we count the distance + if (this.gps_coordinates_counter % 3 == 0) { + if (distance_to_oldest < 150.0) { + // to avoid gps glitches + this.advanced_distance += distance_to_oldest; + } + } + + this.instant_speed = distance_to_oldest / (now - this.old_times[0]); + + if (this.old_points.length == 4) { + this.old_points.shift(); + this.old_times.shift(); + } + // let's just take angle of segment between newest point and a point a bit before + let previous_index = this.old_points.length - 3; + if (previous_index < 0) { + previous_index = 0; + } + let diff = position.minus(this.old_points[previous_index]); + let angle = Math.atan2(diff.lat, diff.lon); + return angle; } + update_position(new_position, maybe_direction, timestamp) { + let direction = this.new_position_reached(new_position); + if (direction === null) { + if (maybe_direction === null) { + return; + } else { + direction = maybe_direction; + } + } + if (in_menu) { + return; + } - // now display ourselves - g.setColor(0, 0, 0); - g.fillCircle(half_width, half_height, 5); - } + this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.angle = direction; + let cos_direction = Math.cos(direction); + let sin_direction = Math.sin(direction); + this.position = new_position; + + // we will display position of where we'll be at in a few seconds + // and not where we currently are. + // this is because the display has more than 1sec duration. + this.displayed_position = new Point( + new_position.lon + cos_direction * this.instant_speed * 0.00001, + new_position.lat + sin_direction * this.instant_speed * 0.00001 + ); + + // abort if we are late + // if (timestamp !== null) { + // let elapsed = Date.now() - timestamp; + // if (elapsed > 1000) { + // console.log("we are late"); + // return; + // } + // console.log("we are not late"); + // } + + if (this.path !== null) { + // detect segment we are on now + let res = this.path.nearest_segment( + this.displayed_position, + Math.max(0, this.current_segment - 1), + Math.min(this.current_segment + 2, this.path.len - 1), + cos_direction, + sin_direction + ); + let orientation = res[0]; + let next_segment = res[1]; + + if (this.is_lost(next_segment)) { + // start_profiling(); + // it did not work, try anywhere + res = this.path.nearest_segment( + this.displayed_position, + 0, + this.path.len - 1, + cos_direction, + sin_direction + ); + orientation = res[0]; + next_segment = res[1]; + // end_profiling("repositioning"); + } + // now check if we strayed away from path or back to it + let lost = this.is_lost(next_segment); + if (this.on_path == lost) { + // if status changes + if (lost) { + Bangle.buzz(); // we lost path + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } + this.on_path = !lost; + } + + this.current_segment = next_segment; + + // check if we are nearing the next point on our path and alert the user + let next_point = this.current_segment + (1 - orientation); + this.distance_to_next_point = Math.ceil( + this.position.distance(this.path.point(next_point)) + ); + + // disable gps when far from next point and locked + // if (Bangle.isLocked() && !settings.keep_gps_alive) { + // let time_to_next_point = + // (this.distance_to_next_point * 3.6) / settings.max_speed; + // if (time_to_next_point > 60) { + // Bangle.setGPSPower(false, "gipy"); + // setTimeout(function () { + // Bangle.setGPSPower(true, "gipy"); + // }, time_to_next_point); + // } + // } + if (this.reaching != next_point && this.distance_to_next_point <= 100) { + this.reaching = next_point; + let reaching_waypoint = this.path.is_waypoint(next_point); + if (reaching_waypoint) { + if (settings.buzz_on_turns) { + Bangle.buzz(); + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } + if (!Bangle.isLCDOn()) { + Bangle.setLCDPower(true); + Bangle.setLocked(false); + } + } + } + } + + // abort most frames if locked + if (Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) { + return; + } + + // re-display + this.display(); + } + display_direction() { + //TODO: go towards point on path at 20 meter + if (this.current_segment === null) { + return; + } + let next_point = this.path.point(this.current_segment + (1 - go_backwards)); + + let distance_to_next_point = Math.ceil( + this.projected_point.distance(next_point) + ); + let towards; + if (distance_to_next_point < 20) { + towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); + } else { + towards = next_point; + } + let diff = towards.minus(this.projected_point); + direction = Math.atan2(diff.lat, diff.lon); + + let full_angle = direction - this.angle; + // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); + // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); + + let scale; + if (zoomed) { + scale = this.scale_factor; + } else { + scale = this.scale_factor / 2; + } + + c = this.projected_point.coordinates( + this.displayed_position, + this.adjusted_cos_direction, + this.adjusted_sin_direction, + scale + ); + + let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); + let cos2 = Math.cos(full_angle + Math.PI / 2); + let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); + let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); + let sin2 = Math.sin(-full_angle - Math.PI / 2); + let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); + g.setColor(0, 1, 0).fillPoly([ + c[0] + cos1 * 15, + c[1] + sin1 * 15, + c[0] + cos2 * 20, + c[1] + sin2 * 20, + c[0] + cos3 * 15, + c[1] + sin3 * 15, + c[0] + cos3 * 10, + c[1] + sin3 * 10, + c[0] + cos2 * 15, + c[1] + sin2 * 15, + c[0] + cos1 * 10, + c[1] + sin1 * 10, + ]); + } + remaining_distance() { + let remaining_in_correct_orientation = + this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)); + + if (go_backwards) { + return this.remaining_distances[0] - remaining_in_correct_orientation; + } else { + return remaining_in_correct_orientation; + } + } + // check if we are lost (too far from segment we think we are on) + // if we are adjust scale so that path will still be displayed. + // we do the scale adjustment here to avoid recomputations later on. + is_lost(segment) { + let projection = this.displayed_position.closest_segment_point( + this.path.point(segment), + this.path.point(segment + 1) + ); + this.projected_point = projection; // save this info for display + let distance_to_projection = this.displayed_position.distance(projection); + if (distance_to_projection > settings.lost_distance) { + return true; + } else { + return false; + } + } + display() { + if (displaying || in_menu) { + return; // don't draw on drawings + } + displaying = true; + g.clear(); + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } + + // start_profiling(); + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + // end_profiling("map"); + if (this.interests !== null) { + this.interests.display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + if (this.position !== null) { + this.display_path(); + } + + this.display_direction(); + this.display_stats(); + Bangle.drawWidgets(); + displaying = false; + } + display_stats() { + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = "0" + minutes; + } + let hours = now.getHours().toString(); + + // display the clock + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, 24); + + let approximate_speed; + // display speed (avg and instant) + if (this.old_times.length > 0) { + let point_time = this.old_times[this.old_times.length - 1]; + let done_in = point_time - this.starting_time; + approximate_speed = Math.round( + (this.advanced_distance * 3.6) / done_in + ); + let approximate_instant_speed = Math.round(this.instant_speed * 3.6); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .drawString( + "" + + approximate_speed + + "km/h", + 0, + g.getHeight() - 15 + ); + + g.setFont("6x8:3") + .setFontAlign(1, -1, 0) + .drawString( + "" + approximate_instant_speed, + g.getWidth(), + g.getHeight() - 22 + ); + } + + if (this.path === null || this.position === null) { + return; + } + + let remaining_distance = this.remaining_distance(); + let rounded_distance = Math.round(remaining_distance / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + // now, distance to next point in meters + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString( + "" + this.distance_to_next_point + "m", + 0, + g.getHeight() - 49 + ); + + let forward_eta = compute_eta( + now.getHours(), + now.getMinutes(), + approximate_speed, + remaining_distance / 1000 + ); + + // now display ETA + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(forward_eta, 0, 42); + + // display distance on path + g.setFont("6x8:2").drawString( + "" + rounded_distance + "/" + total, + 0, + g.getHeight() - 32 + ); + + // display various indicators + if (this.distance_to_next_point <= 100) { + if (this.path.is_waypoint(this.reaching)) { + g.setColor(0.0, 1.0, 0.0) + .setFont("6x15") + .drawString("turn", g.getWidth() - 50, 30); + } + } + if (!this.on_path) { + g.setColor(1.0, 0.0, 0.0) + .setFont("6x15") + .drawString("lost", g.getWidth() - 55, 35); + } + } + display_path() { + // don't display all segments, only those neighbouring current segment + // this is most likely to be the correct display + // while lowering the cost a lot + // + // note that all code is inlined here to speed things up + let cos = this.adjusted_cos_direction; + let sin = this.adjusted_sin_direction; + let displayed_x = this.displayed_position.lon; + let displayed_y = this.displayed_position.lat; + let width = g.getWidth(); + let height = g.getHeight(); + let half_width = width / 2; + let half_height = height / 2 + Y_OFFSET; + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } + + if (this.path !== null) { + // compute coordinate for projection on path + let tx = (this.projected_point.lon - displayed_x) * scale_factor; + let ty = (this.projected_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let projected_x = half_width - Math.round(rotated_x); // x is inverted + let projected_y = half_height + Math.round(rotated_y); + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let previous_point = this.path.point(this.current_segment); + let nearest_point; + if ( + previous_point.fake_distance(this.position) < + next_point.fake_distance(this.position) + ) { + nearest_point = previous_point; + } else { + nearest_point = next_point; + } + let tx = (nearest_point.lon - displayed_x) * scale_factor; + let ty = (nearest_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); + } + + // display current-segment's projection + g.setColor(0, 0, 0); + g.fillCircle(projected_x, projected_y, 4); + } + + // now display ourselves + g.setColor(0, 0, 0); + g.fillCircle(half_width, half_height, 5); + } } function load_gps(filename) { - // let's display splash screen while loading file - g.clear(); - g.drawImage(splashscreen, 0, 0); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(0xf800) - .drawString(filename, 0, g.getHeight() - 30); - g.flip(); + // let's display splash screen while loading file + g.clear(); + g.drawImage(splashscreen, 0, 0); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(0xf800) + .drawString(filename, 0, g.getHeight() - 30); + g.flip(); - let buffer = s.readArrayBuffer(filename); - let file_size = buffer.length; - let offset = 0; + let buffer = s.readArrayBuffer(filename); + let file_size = buffer.length; + let offset = 0; - let path = null; - let maps = []; - let interests = null; - while (offset < file_size) { - let block_type = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - if (block_type == 0) { - // it's a map - console.log("loading map"); - let res = new Map(buffer, offset, filename); - let map = res[0]; - offset = res[1]; - maps.push(map); - } else if (block_type == 2) { - console.log("loading path"); - let res = new Path(buffer, offset); - path = res[0]; - offset = res[1]; - } else if (block_type == 3) { - console.log("loading interests"); - let res = new Interests(buffer, offset); - interests = res[0]; - offset = res[1]; - } else { - console.log("todo : block type", block_type); + let path = null; + let maps = []; + let interests = null; + while (offset < file_size) { + let block_type = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + if (block_type == 0) { + // it's a map + console.log("loading map"); + let res = new Map(buffer, offset, filename); + let map = res[0]; + offset = res[1]; + maps.push(map); + } else if (block_type == 2) { + console.log("loading path"); + let res = new Path(buffer, offset); + path = res[0]; + offset = res[1]; + } else if (block_type == 3) { + console.log("loading interests"); + let res = new Interests(buffer, offset); + interests = res[0]; + offset = res[1]; + } else { + console.log("todo : block type", block_type); + } } - } - // checksum file size - if (offset != file_size) { - console.log("invalid file size", file_size, "expected", offset); - let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; - E.showAlert(msg).then(function () { - E.showAlert(); - start_gipy(path, maps, interests); - }); - } else { - start_gipy(path, maps, interests); - } + // checksum file size + if (offset != file_size) { + console.log("invalid file size", file_size, "expected", offset); + let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; + E.showAlert(msg).then(function() { + E.showAlert(); + start_gipy(path, maps, interests); + }); + } else { + start_gipy(path, maps, interests); + } } class Path { - constructor(buffer, offset) { - // let p = Uint16Array(buffer, offset, 1); - // console.log(p); - let points_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; + constructor(buffer, offset) { + // let p = Uint16Array(buffer, offset, 1); + // console.log(p); + let points_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; - // path points - this.points = Float64Array(buffer, offset, points_number * 2); - offset += 8 * points_number * 2; + // path points + this.points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; - // path waypoints - let waypoints_len = Math.ceil(points_number / 8.0); - this.waypoints = Uint8Array(buffer, offset, waypoints_len); - offset += waypoints_len; + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + this.waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; - return [this, offset]; - } - - is_waypoint(point_index) { - let i = Math.floor(point_index / 8); - let subindex = point_index % 8; - let r = this.waypoints[i] & (1 << subindex); - return r != 0; - } - - // return point at given index - point(index) { - let lon = this.points[2 * index]; - let lat = this.points[2 * index + 1]; - return new Point(lon, lat); - } - - // return index of segment which is nearest from point. - // we need a direction because we need there is an ambiguity - // for overlapping segments which are taken once to go and once to come back. - // (in the other direction). - nearest_segment(point, start, end, cos_direction, sin_direction) { - // we are going to compute two min distances, one for each direction. - let indices = [0, 0]; - let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; - - let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); - for (let i = start + 1; i < end + 1; i++) { - let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); - - let closest_point = point.closest_segment_point(p1, p2); - let distance = point.length_squared(closest_point); - - let dot = - cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); - let orientation = +(dot < 0); // index 0 is good orientation - if (distance <= mins[orientation]) { - mins[orientation] = distance; - indices[orientation] = i - 1; - } - - p1 = p2; + return [this, offset]; } - // by default correct orientation (0) wins - // but if other one is really closer, return other one - if (mins[1] < mins[0] / 100.0) { - return [1, indices[1]]; - } else { - return [0, indices[0]]; + is_waypoint(point_index) { + let i = Math.floor(point_index / 8); + let subindex = point_index % 8; + let r = this.waypoints[i] & (1 << subindex); + return r != 0; + } + + // return point at given index + point(index) { + let lon = this.points[2 * index]; + let lat = this.points[2 * index + 1]; + return new Point(lon, lat); + } + + // return index of segment which is nearest from point. + // we need a direction because we need there is an ambiguity + // for overlapping segments which are taken once to go and once to come back. + // (in the other direction). + nearest_segment(point, start, end, cos_direction, sin_direction) { + // we are going to compute two min distances, one for each direction. + let indices = [0, 0]; + let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; + + let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); + for (let i = start + 1; i < end + 1; i++) { + let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); + + let closest_point = point.closest_segment_point(p1, p2); + let distance = point.length_squared(closest_point); + + let dot = + cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); + let orientation = +(dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; + } + + p1 = p2; + } + + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 100.0) { + return [1, indices[1]]; + } else { + return [0, indices[0]]; + } + } + get len() { + return this.points.length / 2; } - } - get len() { - return this.points.length / 2; - } } class Point { - constructor(lon, lat) { - this.lon = lon; - this.lat = lat; - } - coordinates(current_position, cos_direction, sin_direction, scale_factor) { - let translated = this.minus(current_position).times(scale_factor); - let rotated_x = - translated.lon * cos_direction - translated.lat * sin_direction; - let rotated_y = - translated.lon * sin_direction + translated.lat * cos_direction; - return [ - g.getWidth() / 2 - Math.round(rotated_x), // x is inverted - g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, - ]; - } - minus(other_point) { - let xdiff = this.lon - other_point.lon; - let ydiff = this.lat - other_point.lat; - return new Point(xdiff, ydiff); - } - plus(other_point) { - return new Point(this.lon + other_point.lon, this.lat + other_point.lat); - } - length_squared(other_point) { - let londiff = this.lon - other_point.lon; - let latdiff = this.lat - other_point.lat; - return londiff * londiff + latdiff * latdiff; - } - times(scalar) { - return new Point(this.lon * scalar, this.lat * scalar); - } - dot(other_point) { - return this.lon * other_point.lon + this.lat * other_point.lat; - } - distance(other_point) { - //see https://www.movable-type.co.uk/scripts/latlong.html - const R = 6371e3; // metres - const phi1 = (this.lat * Math.PI) / 180; - const phi2 = (other_point.lat * Math.PI) / 180; - const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; - const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; - - const a = - Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + - Math.cos(phi1) * - Math.cos(phi2) * - Math.sin(deltalambda / 2) * - Math.sin(deltalambda / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; // in meters - } - fake_distance(other_point) { - return Math.sqrt(this.length_squared(other_point)); - } - // return closest point from 'this' on [v,w] segment. - // since this function is critical we inline all code here. - closest_segment_point(v, w) { - // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment - // Return minimum distance between line segment vw and point p - let segment_londiff = w.lon - v.lon; - let segment_latdiff = w.lat - v.lat; - let l2 = - segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt - if (l2 == 0.0) { - return v; // v == w case + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - - // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below - let start_londiff = this.lon - v.lon; - let start_latdiff = this.lat - v.lat; - let t = - (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; - if (t < 0) { - t = 0; - } else { - if (t > 1) { - t = 1; - } + coordinates(current_position, cos_direction, sin_direction, scale_factor) { + let translated = this.minus(current_position).times(scale_factor); + let rotated_x = + translated.lon * cos_direction - translated.lat * sin_direction; + let rotated_y = + translated.lon * sin_direction + translated.lat * cos_direction; + return [ + g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, + ]; + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let londiff = this.lon - other_point.lon; + let latdiff = this.lat - other_point.lat; + return londiff * londiff + latdiff * latdiff; + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + dot(other_point) { + return this.lon * other_point.lon + this.lat * other_point.lat; + } + distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = (this.lat * Math.PI) / 180; + const phi2 = (other_point.lat * Math.PI) / 180; + const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; + const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; + + const a = + Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + + Math.cos(phi1) * + Math.cos(phi2) * + Math.sin(deltalambda / 2) * + Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; // in meters + } + fake_distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + // return closest point from 'this' on [v,w] segment. + // since this function is critical we inline all code here. + closest_segment_point(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let segment_londiff = w.lon - v.lon; + let segment_latdiff = w.lat - v.lat; + let l2 = + segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return v; // v == w case + } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + + // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below + let start_londiff = this.lon - v.lon; + let start_latdiff = this.lat - v.lat; + let t = + (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; + if (t < 0) { + t = 0; + } else { + if (t > 1) { + t = 1; + } + } + let lon = v.lon + segment_londiff * t; + let lat = v.lat + segment_latdiff * t; + return new Point(lon, lat); } - let lon = v.lon + segment_londiff * t; - let lat = v.lat + segment_latdiff * t; - return new Point(lon, lat); - } } let fake_gps_point = 0; + function simulate_gps(status) { - if (status.path === null) { - let map = status.maps[0]; - let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); - let p2 = new Point( - map.start_coordinates[0] + map.side * map.grid_size[0], - map.start_coordinates[1] + map.side * map.grid_size[1] - ); - let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); - if (fake_gps_point < 1) { - fake_gps_point += 0.01; - } - status.update_position(pos, null, null); - } else { - if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { - return; - } - let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len / 2 - 1) { - return; - } - let p1 = status.path.point(2 * point_index); // use these to approximately follow path - let p2 = status.path.point(2 * (point_index + 1)); - //let p1 = status.path.point(point_index); // use these to strictly follow path - //let p2 = status.path.point(point_index + 1); - - let alpha = fake_gps_point - point_index; - let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - - if (go_backwards) { - fake_gps_point -= 0.05; // advance simulation + if (status.path === null) { + let map = status.maps[0]; + let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); + let p2 = new Point( + map.start_coordinates[0] + map.side * map.grid_size[0], + map.start_coordinates[1] + map.side * map.grid_size[1] + ); + let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); + if (fake_gps_point < 1) { + fake_gps_point += 0.01; + } + status.update_position(pos, null, null); } else { - fake_gps_point += 0.05; // advance simulation + if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { + return; + } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len / 2 - 1) { + return; + } + let p1 = status.path.point(2 * point_index); // use these to approximately follow path + let p2 = status.path.point(2 * (point_index + 1)); + //let p1 = status.path.point(point_index); // use these to strictly follow path + //let p2 = status.path.point(point_index + 1); + + let alpha = fake_gps_point - point_index; + let pos = p1.times(1 - alpha).plus(p2.times(alpha)); + + if (go_backwards) { + fake_gps_point -= 0.05; // advance simulation + } else { + fake_gps_point += 0.05; // advance simulation + } + status.update_position(pos, null, null); } - status.update_position(pos, null, null); - } } function drawMenu() { - const menu = { - "": { title: "choose trace" }, - }; - var files = s.list(".gps"); - for (var i = 0; i < files.length; ++i) { - menu[files[i]] = start.bind(null, files[i]); - } - menu["Exit"] = function () { - load(); - }; - E.showMenu(menu); + const menu = { + "": { + title: "choose trace" + }, + }; + var files = s.list(".gps"); + for (var i = 0; i < files.length; ++i) { + menu[files[i]] = start.bind(null, files[i]); + } + menu["Exit"] = function() { + load(); + }; + E.showMenu(menu); } function start(fn) { - E.showMenu(); - console.log("loading", fn); + E.showMenu(); + console.log("loading", fn); - load_gps(fn); + load_gps(fn); } function start_gipy(path, maps, interests) { - console.log("starting"); - status = new Status(path, maps, interests); + console.log("starting"); - setWatch( - function () { - if (in_menu) { - return; - } - in_menu = true; - const menu = { - "": { title: "choose action" }, - "Go Backward": { - value: go_backwards, - format: (v) => (v ? "On" : "Off"), - onchange: (v) => { - go_backwards = v; - }, - }, - Zoom: { - value: zoomed, - format: (v) => (v ? "In" : "Out"), - onchange: (v) => { - status.invalidate_caches(); - zoomed = v; - }, - }, - "back to map": function () { - in_menu = false; - E.showMenu(); - g.clear(); - g.flip(); - if (status !== null) { - status.display(); - } - }, - }; - E.showMenu(menu); - }, - BTN1, - { repeat: true } - ); - - - if (status.path !== null) { - let start = status.path.point(0); - status.displayed_position = start; - } else { - let first_map = maps[0]; - status.displayed_position = new Point( - first_map.start_coordinates[0] + - (first_map.side * first_map.grid_size[0]) / 2, - first_map.start_coordinates[1] + - (first_map.side * first_map.grid_size[1]) / 2); - } - status.display(); - - Bangle.on("stroke", (o) => { - if (in_menu) { - return; - } - // we move display according to stroke - let first_x = o.xy[0]; - let first_y = o.xy[1]; - let last_x = o.xy[o.xy.length - 2]; - let last_y = o.xy[o.xy.length - 1]; - let xdiff = last_x - first_x; - let ydiff = last_y - first_y; - - let c = status.adjusted_cos_direction; - let s = status.adjusted_sin_direction; - let rotated_x = xdiff * c - ydiff * s; - let rotated_y = xdiff * s + ydiff * c; - status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor; - status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor; - status.display(); - }); - - if (simulated) { - status.starting_time = getTime(); - // let's keep the screen on in simulations - Bangle.setLCDTimeout(0); - Bangle.setLCDPower(1); - setInterval(simulate_gps, 500, status); - } else { - Bangle.setLocked(false); - - let frame = 0; - let set_coordinates = function (data) { - frame += 1; - // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere - let valid_coordinates = - !isNaN(data.lat) && - !isNaN(data.lon) && - (data.lat != 0.0 || data.lon != 0.0); - if (valid_coordinates) { - if (status.starting_time === null) { - status.starting_time = getTime(); - Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen - } - status.update_position(new Point(data.lon, data.lat), null, data.time); - } - let gps_status_color; - if (frame % 2 == 0 || valid_coordinates) { - gps_status_color = g.theme.bg; - } else { - gps_status_color = g.theme.fg; - } - if (!in_menu) { - g.setColor(gps_status_color) - .setFont("6x8:2") - .drawString("gps", g.getWidth() - 40, 30); - } - }; - - Bangle.setGPSPower(true, "gipy"); - Bangle.on("GPS", set_coordinates); - Bangle.on("lock", function (on) { - if (!on) { - Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking - } + Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 20000, + lcdPowerTimeout: 30000, + hrmSportMode: 2, }); - } + if (!simulated && settings.disable_bluetooth) { + NRF.sleep(); // disable bluetooth completely + } + + status = new Status(path, maps, interests); + + setWatch( + function() { + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { + title: "choose action" + }, + "Go Backward": { + value: go_backwards, + format: (v) => (v ? "On" : "Off"), + onchange: (v) => { + go_backwards = v; + }, + }, + Zoom: { + value: zoomed, + format: (v) => (v ? "In" : "Out"), + onchange: (v) => { + status.invalidate_caches(); + zoomed = v; + }, + }, + "back to map": function() { + in_menu = false; + E.showMenu(); + g.clear(); + g.flip(); + if (status !== null) { + status.display(); + } + }, + }; + E.showMenu(menu); + }, + BTN1, { + repeat: true + } + ); + + + if (status.path !== null) { + let start = status.path.point(0); + status.displayed_position = start; + } else { + let first_map = maps[0]; + status.displayed_position = new Point( + first_map.start_coordinates[0] + + (first_map.side * first_map.grid_size[0]) / 2, + first_map.start_coordinates[1] + + (first_map.side * first_map.grid_size[1]) / 2); + } + status.display(); + + Bangle.on("stroke", (o) => { + if (in_menu) { + return; + } + // we move display according to stroke + let first_x = o.xy[0]; + let first_y = o.xy[1]; + let last_x = o.xy[o.xy.length - 2]; + let last_y = o.xy[o.xy.length - 1]; + let xdiff = last_x - first_x; + let ydiff = last_y - first_y; + + let c = status.adjusted_cos_direction; + let s = status.adjusted_sin_direction; + let rotated_x = xdiff * c - ydiff * s; + let rotated_y = xdiff * s + ydiff * c; + status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor; + status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor; + status.display(); + }); + + if (simulated) { + status.starting_time = getTime(); + // let's keep the screen on in simulations + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + setInterval(simulate_gps, 500, status); + } else { + Bangle.setLocked(false); + + let frame = 0; + let set_coordinates = function(data) { + frame += 1; + // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere + let valid_coordinates = !isNaN(data.lat) && + !isNaN(data.lon) && + (data.lat != 0.0 || data.lon != 0.0); + if (valid_coordinates) { + if (status.starting_time === null) { + status.starting_time = getTime(); + Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + } + status.update_position(new Point(data.lon, data.lat), null, data.time); + } + let gps_status_color; + if (frame % 2 == 0 || valid_coordinates) { + gps_status_color = g.theme.bg; + } else { + gps_status_color = g.theme.fg; + } + if (!in_menu) { + g.setColor(gps_status_color) + .setFont("6x8:2") + .drawString("gps", g.getWidth() - 40, 30); + } + }; + + Bangle.setGPSPower(true, "gipy"); + Bangle.on("GPS", set_coordinates); + Bangle.on("lock", function(on) { + if (!on) { + Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking + } + }); + } } let files = s.list(".gps"); if (files.length <= 1) { - if (files.length == 0) { - load(); - } else { - start(files[0]); - } + if (files.length == 0) { + load(); + } else { + start(files[0]); + } } else { - drawMenu(); -} + drawMenu(); +} \ No newline at end of file diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index 1f6ae0853..ecbcb267e 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -1,29 +1,48 @@ -(function (back) { - var FILE = "gipy.json"; - // Load settings - var settings = Object.assign( - { - lost_distance: 50, - }, - require("Storage").readJSON(FILE, true) || {} - ); +(function(back) { + var FILE = "gipy.json"; + // Load settings + var settings = Object.assign({ + lost_distance: 50, + buzz_on_turns: false, + disable_bluetooth: true, + }, + require("Storage").readJSON(FILE, true) || {} + ); - function writeSettings() { - require("Storage").writeJSON(FILE, settings); - } + function writeSettings() { + require("Storage").writeJSON(FILE, settings); + } - // Show the menu - E.showMenu({ - "": { title: "Gipy" }, - "< Back": () => back(), - "lost distance": { - value: 50 | settings.lost_distance, // 0| converts undefined to 0 - min: 10, - max: 500, - onchange: (v) => { - settings.max_speed = v; - writeSettings(); - }, - }, - }); + // Show the menu + E.showMenu({ + "": { + title: "Gipy" + }, + "< Back": () => back(), + "buzz on turns": { + value: !!settings.buzz_on_turns, // !! converts undefined to false + format: (v) => (v ? "Yes" : "No"), + onchange: (v) => { + settings.buzz_on_turns = v; + writeSettings(); + } + }, + "disable bluetooth": { + value: !!settings.disable_bluetooth, // !! converts undefined to false + format: (v) => (v ? "Yes" : "No"), + onchange: (v) => { + settings.disable_bluetooth = v; + writeSettings(); + } + }, + "lost distance": { + value: 50 | settings.lost_distance, // 0| converts undefined to 0 + min: 10, + max: 500, + onchange: (v) => { + settings.max_speed = v; + writeSettings(); + }, + }, + }); }); From 9ca06bbfca16aa315da6784bd7e6045fd362ee39 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 8 Jul 2023 11:46:38 +0200 Subject: [PATCH 006/242] gipy : lcd power saving + settings --- apps/gipy/ChangeLog | 3 ++- apps/gipy/README.md | 15 ++++++++++----- apps/gipy/TODO | 3 --- apps/gipy/app.js | 24 +++++++++++++++--------- apps/gipy/settings.js | 9 +++++++++ 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index c2e9a21b4..e8055100a 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -92,6 +92,7 @@ * Large display for instant speed * Bugfix for negative coordinates * Disable menu while the map is not loaded - * Turn screen off while idling to save battery + * Turn screen off while idling to save battery (with setting) * New setting : disable buzz on turns * New setting : turn bluetooth off to save battery + * Color change for lost direction (now purple) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 242282dbf..d75cd5d6f 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -18,8 +18,8 @@ It provides the following features : - display the path with current position from gps - display a local map around you, downloaded from openstreetmap - detects and buzzes if you leave the path -- buzzes before sharp turns -- buzzes before waypoints +- (optional) buzzes before sharp turns +- (optional) buzzes before waypoints (for example when you need to turn in https://mapstogpx.com/) - display instant / average speed - display distance to next point @@ -51,8 +51,8 @@ Your path will be displayed in svg. ### Starting Gipy At start you will have a menu for selecting your trace (if more than one). -Choose the one you want and you will reach the splash screen where you'll wait for the gps signal. -Once you have a signal you will reach the main screen: +Choose the one you want and you will reach the splash screen where you'll wait for the map. +Once the map is loaded you will reach the main screen: ![Screenshot](legend.png) @@ -83,7 +83,7 @@ On your screen you can see: ### Lost If you stray away from path we will rescale the display to continue displaying nearby segments and -display the direction to follow as a black segment. +display the direction to follow as a purple segment. Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you are. On path it just needed to scan a few points ahead and behind. @@ -100,6 +100,10 @@ If you click the button you'll reach a menu where you can currently zoom out to Few settings for now (feel free to suggest me more) : - lost distance : at which distance from path are you considered to be lost ? +- buzz on turns : should the watch buzz when reaching a waypoint ? +- disable bluetooth : turn bluetooth off completely to try to save some power. +- power lcd off : turn lcd off after 30 seconds to save power. the watch will wake up when reaching waypoints +and when you touch the screen. ### Caveats @@ -121,5 +125,6 @@ Feel free to give me feedback : is it useful for you ? what other features would If you want to raise issues the main repository is [https://github.com/wagnerf42/BangleApps](here) and the rust code doing the actual map computations is located [https://github.com/wagnerf42/gps](here). +You can try the cutting edge version at [https://wagnerf42.github.io/BangleApps/](https://wagnerf42.github.io/BangleApps/) frederic.wagner@imag.fr diff --git a/apps/gipy/TODO b/apps/gipy/TODO index c94211c1d..0c25f7d0b 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,8 +1,5 @@ -+ disable backlight during day ? + put back foot only ways -+ disable bluetooth -+ disable lcd completely + try fiddling with jit + put back street names + put back shortest paths but with points cache this time and jit diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 866849efc..a39062aa9 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -20,6 +20,7 @@ var settings = Object.assign({ lost_distance: 50, buzz_on_turns: false, disable_bluetooth: true, + power_lcd_off: true, }, s.readJSON("gipy.json", true) || {} ); @@ -815,7 +816,7 @@ class Status { } // abort most frames if locked - if (Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) { + if (!Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) { return; } @@ -1087,7 +1088,7 @@ class Status { let rotated_y = tx * sin + ty * cos; let x = half_width - Math.round(rotated_x); // x is inverted let y = half_height + Math.round(rotated_y); - g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); + g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); } // display current-segment's projection @@ -1386,12 +1387,17 @@ function start(fn) { function start_gipy(path, maps, interests) { console.log("starting"); - Bangle.setOptions({ - lockTimeout: 10000, - backlightTimeout: 20000, - lcdPowerTimeout: 30000, - hrmSportMode: 2, - }); + if (settings.power_lcd_off) { + Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 20000, + lcdPowerTimeout: 30000, + hrmSportMode: 2, + wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. + wakeOnFaceUp: false, + wakeOnTouch: true, + }); + } if (!simulated && settings.disable_bluetooth) { NRF.sleep(); // disable bluetooth completely } @@ -1531,4 +1537,4 @@ if (files.length <= 1) { } } else { drawMenu(); -} \ No newline at end of file +} diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index ecbcb267e..37b299815 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -5,6 +5,7 @@ lost_distance: 50, buzz_on_turns: false, disable_bluetooth: true, + power_lcd_off: true, }, require("Storage").readJSON(FILE, true) || {} ); @@ -44,5 +45,13 @@ writeSettings(); }, }, + "power lcd off": { + value: !!settings.power_lcd_off, // !! converts undefined to false + format: (v) => (v ? "Yes" : "No"), + onchange: (v) => { + settings.power_lcd_off = v; + writeSettings(); + } + }, }); }); From ed543b871634a593f21743a12b3a13be99d4b70a Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 8 Jul 2023 11:58:18 +0200 Subject: [PATCH 007/242] gipy: checkboxes in settings --- apps/gipy/settings.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index 37b299815..9283f8ab9 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -20,17 +20,15 @@ title: "Gipy" }, "< Back": () => back(), - "buzz on turns": { - value: !!settings.buzz_on_turns, // !! converts undefined to false - format: (v) => (v ? "Yes" : "No"), + /*LANG*/"buzz on turns": { + value: settings.buzz_on_turns == true, onchange: (v) => { settings.buzz_on_turns = v; writeSettings(); } }, - "disable bluetooth": { - value: !!settings.disable_bluetooth, // !! converts undefined to false - format: (v) => (v ? "Yes" : "No"), + /*LANG*/"disable bluetooth": { + value: settings.disable_bluetooth == true, onchange: (v) => { settings.disable_bluetooth = v; writeSettings(); @@ -45,9 +43,8 @@ writeSettings(); }, }, - "power lcd off": { - value: !!settings.power_lcd_off, // !! converts undefined to false - format: (v) => (v ? "Yes" : "No"), + /*LANG*/"power lcd off": { + value: settings.power_lcd_off == true, onchange: (v) => { settings.power_lcd_off = v; writeSettings(); From 9d54ac09ec81abd2c2605d3986ed85c320a2b17d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 11 Jul 2023 11:18:30 +0200 Subject: [PATCH 008/242] gipy: fiddling with powersaving --- apps/gipy/README.md | 6 ++- apps/gipy/TODO | 38 +++++++++++++++++++ apps/gipy/app.js | 86 +++++++++++++++++++++++++++++-------------- apps/gipy/settings.js | 18 +++++++-- 4 files changed, 116 insertions(+), 32 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index d75cd5d6f..fceff20c8 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -93,7 +93,7 @@ The distance to next point displayed corresponds to the length of the black segm ### Menu If you click the button you'll reach a menu where you can currently zoom out to see more of the map -(with a slower refresh rate) and reverse the path direction. +(with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on). ### Settings @@ -102,7 +102,8 @@ Few settings for now (feel free to suggest me more) : - lost distance : at which distance from path are you considered to be lost ? - buzz on turns : should the watch buzz when reaching a waypoint ? - disable bluetooth : turn bluetooth off completely to try to save some power. -- power lcd off : turn lcd off after 30 seconds to save power. the watch will wake up when reaching waypoints +- brightness : how bright should screen be ? (by default 0.5, again saving power) +- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points and when you touch the screen. ### Caveats @@ -111,6 +112,7 @@ It is good to use but you should know : - the gps might take a long time to start initially (see the assisted gps update app). - gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. +- if you adventure in gorges the gps signal will become garbage. - your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** - sometimes the watch will tell you that you are lost but you are in fact on the path. It usually figures again the real gps position after a few minutes. It usually happens when the signal is acquired very fast. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 0c25f7d0b..93f241e44 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,3 +1,41 @@ +*** thoughts on lcd power *** + +so, i tried experimenting with turning the lcd off in order to save power. + +the good news: this saves a lot. i did a 3h ride which usually depletes the battery and I still had +around two more hours to go. + +now the bad news: + +- i had to de-activate the twist detection : you cannot raise your watch to the eyes to turn it on. +that's because with twist detection on all road bumps turn the watch on constantly. +- i tried manual detection like : + +Bangle.on('accel', function(xyz) { + + if (xyz.diff > 0.4 && xyz.mag > 1 && xyz.z < -1.4) { + Bangle.setLCDPower(true); + Bangle.setLocked(false); + } + +}); + +this works nicely when you sit on a chair with a simulated gps signal but does not work so nicely when on the bike. +sometimes it is ok, sometimes you can try 10 times with no success. + +- instead i use screen touch to turn it on. that's a bother since you need two hands but well it could be worth it. +the problem is in the delay: between 1 and 5 seconds before the screen comes back on. + + +my conclusion is that: + +* we should not turn screen off unless we agree to have an unresponsive ui +* we should maybe autowake near segments ends and when lost +* we should play with backlight instead + + +************************** + + put back foot only ways + try fiddling with jit diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a39062aa9..718a2c6f5 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -3,6 +3,7 @@ let displaying = false; let in_menu = false; let go_backwards = false; let zoomed = true; +let powersaving = true; let status; let interests_colors = [ @@ -18,9 +19,10 @@ let s = require("Storage"); var settings = Object.assign({ lost_distance: 50, + brightness: 0.5, buzz_on_turns: false, disable_bluetooth: true, - power_lcd_off: true, + power_lcd_off: false, }, s.readJSON("gipy.json", true) || {} ); @@ -606,6 +608,8 @@ class Interests { class Status { constructor(path, maps, interests) { this.path = path; + this.active = false; // should we have screen on + this.last_activity = getTime(); this.maps = maps; this.interests = interests; let half_screen_width = g.getWidth() / 2; @@ -644,6 +648,32 @@ class Status { this.old_points = []; // record previous points but only when enough distance between them this.old_times = []; // the corresponding times } + activate() { + this.last_activity = getTime(); + if (this.active) { + return; + } else { + this.active = true; + Bangle.setLCDBrightness(settings.brightness); + Bangle.setLocked(false); + if (settings.power_lcd_off) { + Bangle.setLCDPower(true); + } + } + } + check_activity() { + if (!this.active || !powersaving) { + return; + } + if (getTime() - this.last_activity > 30) { + this.active = false; + Bangle.setLCDBrightness(0); + Bangle.setLocked(true); + if (settings.power_lcd_off) { + Bangle.setLCDPower(false); + } + } + } invalidate_caches() { for (let i = 0; i < this.maps.length; i++) { this.maps[i].invalidate_caches(); @@ -713,6 +743,7 @@ class Status { if (in_menu) { return; } + this.check_activity(); // if we don't move or are in menu we should stay on this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); @@ -768,6 +799,7 @@ class Status { // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); if (this.on_path == lost) { + this.activate(); // if status changes if (lost) { Bangle.buzz(); // we lost path @@ -798,6 +830,7 @@ class Status { // } // } if (this.reaching != next_point && this.distance_to_next_point <= 100) { + this.activate(); this.reaching = next_point; let reaching_waypoint = this.path.is_waypoint(next_point); if (reaching_waypoint) { @@ -807,16 +840,12 @@ class Status { setTimeout(() => Bangle.buzz(), 1000); setTimeout(() => Bangle.buzz(), 1500); } - if (!Bangle.isLCDOn()) { - Bangle.setLCDPower(true); - Bangle.setLocked(false); - } } } } - // abort most frames if locked - if (!Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) { + // abort most frames if inactive + if (!this.active && this.gps_coordinates_counter % 5 != 0) { return; } @@ -1353,9 +1382,9 @@ function simulate_gps(status) { let pos = p1.times(1 - alpha).plus(p2.times(alpha)); if (go_backwards) { - fake_gps_point -= 0.05; // advance simulation + fake_gps_point -= 0.01; // advance simulation } else { - fake_gps_point += 0.05; // advance simulation + fake_gps_point += 0.01; // advance simulation } status.update_position(pos, null, null); } @@ -1387,17 +1416,18 @@ function start(fn) { function start_gipy(path, maps, interests) { console.log("starting"); - if (settings.power_lcd_off) { - Bangle.setOptions({ - lockTimeout: 10000, - backlightTimeout: 20000, - lcdPowerTimeout: 30000, - hrmSportMode: 2, - wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. - wakeOnFaceUp: false, - wakeOnTouch: true, - }); - } + // we handle manually the backlight + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 0, + lcdPowerTimeout: 0, + hrmSportMode: 2, + wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. + wakeOnFaceUp: false, + wakeOnTouch: false, + powerSave: false, + }); + Bangle.setPollInterval(4000); // disable accelerometer as much as we can if (!simulated && settings.disable_bluetooth) { NRF.sleep(); // disable bluetooth completely } @@ -1406,6 +1436,7 @@ function start_gipy(path, maps, interests) { setWatch( function() { + status.activate(); if (in_menu) { return; } @@ -1429,6 +1460,12 @@ function start_gipy(path, maps, interests) { zoomed = v; }, }, + /*LANG*/"powersaving": { + value: powersaving, + onchange: (v) => { + powersaving = v; + } + }, "back to map": function() { in_menu = false; E.showMenu(); @@ -1461,6 +1498,7 @@ function start_gipy(path, maps, interests) { status.display(); Bangle.on("stroke", (o) => { + status.activate(); if (in_menu) { return; } @@ -1488,7 +1526,7 @@ function start_gipy(path, maps, interests) { Bangle.setLCDPower(1); setInterval(simulate_gps, 500, status); } else { - Bangle.setLocked(false); + status.activate(); let frame = 0; let set_coordinates = function(data) { @@ -1519,15 +1557,9 @@ function start_gipy(path, maps, interests) { Bangle.setGPSPower(true, "gipy"); Bangle.on("GPS", set_coordinates); - Bangle.on("lock", function(on) { - if (!on) { - Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking - } - }); } } - let files = s.list(".gps"); if (files.length <= 1) { if (files.length == 0) { diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index 9283f8ab9..395b1ac93 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -5,7 +5,8 @@ lost_distance: 50, buzz_on_turns: false, disable_bluetooth: true, - power_lcd_off: true, + brightness: 0.5, + power_lcd_off: false, }, require("Storage").readJSON(FILE, true) || {} ); @@ -35,14 +36,25 @@ } }, "lost distance": { - value: 50 | settings.lost_distance, // 0| converts undefined to 0 + value: settings.lost_distance, min: 10, max: 500, onchange: (v) => { - settings.max_speed = v; + settings.lost_distance = v; writeSettings(); }, }, + "brightness": { + value: settings.brightness, + min: 0, + max: 1, + step: 0.1, + onchange: (v) => { + settings.brightness = v; + writeSettings(); + }, + }, + /*LANG*/"power lcd off": { value: settings.power_lcd_off == true, onchange: (v) => { From 549cd0aadd505cfb84ce31304cb11c07b4e1f0d7 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 14 Jul 2023 08:32:18 +0200 Subject: [PATCH 009/242] gipy: misc --- apps/gipy/TODO | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 93f241e44..294f18952 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -36,6 +36,10 @@ my conclusion is that: ************************** ++ when you walk the direction still has a tendency to shift ++ don't do the new powersave if you walk ? ++ can we turn lcd off without locking ? ++ function Graphics.transformVertices + put back foot only ways + try fiddling with jit From 465539561ff50a230150d51d2e86d4d83060e7bb Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 18 Jul 2023 08:59:37 +0200 Subject: [PATCH 010/242] gipy: improvements to sleep algorithm --- apps/gipy/README.md | 4 +-- apps/gipy/app.js | 78 ++++++++++++++++++++++++++------------------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index fceff20c8..03ca97753 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -103,8 +103,8 @@ Few settings for now (feel free to suggest me more) : - buzz on turns : should the watch buzz when reaching a waypoint ? - disable bluetooth : turn bluetooth off completely to try to save some power. - brightness : how bright should screen be ? (by default 0.5, again saving power) -- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points -and when you touch the screen. +- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points, +when you touch the screen and when speed is below 13km/h. ### Caveats diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 718a2c6f5..85319922a 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -608,6 +608,7 @@ class Interests { class Status { constructor(path, maps, interests) { this.path = path; + this.default_options = true; // do we still have default options ? this.active = false; // should we have screen on this.last_activity = getTime(); this.maps = maps; @@ -668,7 +669,6 @@ class Status { if (getTime() - this.last_activity > 30) { this.active = false; Bangle.setLCDBrightness(0); - Bangle.setLocked(true); if (settings.power_lcd_off) { Bangle.setLCDPower(false); } @@ -698,6 +698,10 @@ class Status { let distance_to_previous = previous_point.distance(position); // gps signal is noisy but rarely above 5 meters if (distance_to_previous < 5) { + // update instant speed and return + let oldest_point = this.old_points[0]; + let distance_to_oldest = oldest_point.distance(position); + this.instant_speed = distance_to_oldest / (now - this.old_times[0]); return null; } } @@ -731,18 +735,47 @@ class Status { let angle = Math.atan2(diff.lat, diff.lon); return angle; } - update_position(new_position, maybe_direction, timestamp) { + update_position(new_position) { let direction = this.new_position_reached(new_position); if (direction === null) { - if (maybe_direction === null) { - return; - } else { - direction = maybe_direction; + if (this.old_points.length > 1) { + this.display(); // re-display because speed has changed } + return; } if (in_menu) { return; } + if (this.instant_speed * 3.6 < 13) { + this.activate(); // if we go too slow turn on, we might be looking for the direction to follow + if (!this.default_options) { + this.default_options = true; + + Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 10000, + wakeOnTwist: true, + powerSave: true, + }); + } + } else { + if (this.default_options) { + this.default_options = false; + + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 0, + lcdPowerTimeout: 0, + hrmSportMode: 2, + wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. + wakeOnFaceUp: false, + wakeOnTouch: true, + powerSave: false, + }); + Bangle.setPollInterval(4000); // disable accelerometer as much as we can + } + + } this.check_activity(); // if we don't move or are in menu we should stay on this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); @@ -760,16 +793,6 @@ class Status { new_position.lat + sin_direction * this.instant_speed * 0.00001 ); - // abort if we are late - // if (timestamp !== null) { - // let elapsed = Date.now() - timestamp; - // if (elapsed > 1000) { - // console.log("we are late"); - // return; - // } - // console.log("we are not late"); - // } - if (this.path !== null) { // detect segment we are on now let res = this.path.nearest_segment( @@ -1364,7 +1387,7 @@ function simulate_gps(status) { if (fake_gps_point < 1) { fake_gps_point += 0.01; } - status.update_position(pos, null, null); + status.update_position(pos); } else { if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { return; @@ -1386,7 +1409,7 @@ function simulate_gps(status) { } else { fake_gps_point += 0.01; // advance simulation } - status.update_position(pos, null, null); + status.update_position(pos); } } @@ -1416,18 +1439,6 @@ function start(fn) { function start_gipy(path, maps, interests) { console.log("starting"); - // we handle manually the backlight - Bangle.setOptions({ - lockTimeout: 0, - backlightTimeout: 0, - lcdPowerTimeout: 0, - hrmSportMode: 2, - wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. - wakeOnFaceUp: false, - wakeOnTouch: false, - powerSave: false, - }); - Bangle.setPollInterval(4000); // disable accelerometer as much as we can if (!simulated && settings.disable_bluetooth) { NRF.sleep(); // disable bluetooth completely } @@ -1460,7 +1471,8 @@ function start_gipy(path, maps, interests) { zoomed = v; }, }, - /*LANG*/"powersaving": { + /*LANG*/ + "powersaving": { value: powersaving, onchange: (v) => { powersaving = v; @@ -1540,7 +1552,7 @@ function start_gipy(path, maps, interests) { status.starting_time = getTime(); Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen } - status.update_position(new Point(data.lon, data.lat), null, data.time); + status.update_position(new Point(data.lon, data.lat)); } let gps_status_color; if (frame % 2 == 0 || valid_coordinates) { @@ -1569,4 +1581,4 @@ if (files.length <= 1) { } } else { drawMenu(); -} +} \ No newline at end of file From d763f63a1920c561ca8f9a36bac6c216dcf0caf8 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Fri, 21 Jul 2023 17:51:31 +0100 Subject: [PATCH 011/242] sched: fix `for` reference in interface --- apps/sched/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index b67029fa2..cd2c9c595 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -334,7 +334,7 @@ function onInit() {

- +
From a560a604235095669b815d483b8d61c525d88b1a Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sat, 22 Jul 2023 09:47:07 +0200 Subject: [PATCH 012/242] openstmap location pointer Replace position marker with location+direction pointer --- apps/openstmap/ChangeLog | 1 + apps/openstmap/app.js | 65 ++++++++++++++++++++++++++++++------ apps/openstmap/metadata.json | 2 +- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 3d08bdf46..ac35122e3 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -26,3 +26,4 @@ If 'Recorder' app installed, add a 'Record' menu item 0.21: Draw a current position marker (Bangle.js 2 only) Enable/Disable previous position marker in new setting "Draw cont. position" +0.22: Replace position marker with location+direction pointer diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 43c747ba7..4069cca4c 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -7,6 +7,10 @@ var hasScrolled = false; var settings = require("Storage").readJSON("openstmap.json",1)||{}; var plotTrack; let checkMapPos = false; // Do we need to check the if the coordinates we have are valid +// Icon for current location+direction: https://icons8.com/icon/13793/north-direction 32x32, 3 Bit + transparency +const imgLoc = Bangle.setLCDOverlay ? require("heatshrink").decompress(atob("kEgwYspgmABacB2wLBm3ABYsN20kyE27ALFtuwBYMG7YKEgdtwMkyUA7dgBYcbsEBBYUDtgLDCIMJBYNADoJLDFIILDGoILDIIMJgECBYMNBYUGBYNIAoICBhuwBYhlFBYJ0BJoo7B23ABZYjLBZhTCBYprFUIoLEHYwLDTYILETYgRCgYOBDorLCBYLLGcYXbNAJNGPQR0DAAwLBIgYAFHYQLUACQA=")) : undefined; +// overlay buffer for current location, image is 32x32, so rotated max is 46x46 +const ovLoc = Bangle.setLCDOverlay ? Graphics.createArrayBuffer(46,46,4,{msb:true}) : undefined; if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) { // restore last view @@ -15,6 +19,9 @@ if (settings.lat !== undefined && settings.lon !== undefined && settings.scale ! m.scale = settings.scale; checkMapPos = true; } +if (settings.dirSrc === undefined) { + settings.dirSrc = 1; // Default=GPS +} // Redraw the whole page function redraw() { @@ -25,9 +32,9 @@ function redraw() { m.lat = m.map.lat; m.lon = m.map.lon; m.scale = m.map.scale; - checkMapPos = false; m.draw(); } + checkMapPos = false; drawPOI(); drawMarker(); drawLocation(); @@ -67,21 +74,28 @@ function drawPOI() { }) } -// Draw the marker for where we are +function isInside(rect, e, w, h) { + return e.x-w/2>=rect.x && e.x+w/2=rect.y && e.y+h/2<=rect.y+rect.h; +} + +// Draw the location & direction marker for where we are function drawMarker() { if (!fix.fix || !settings.drawMarker) return; var p = m.latLonToXY(fix.lat, fix.lon); - g.setColor(1,0,0); - g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); + if (isInside(R, p, 4, 4)) { // avoid drawing over widget area + g.setColor(1,0,0); + g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); + } } -// Draw current location with LCD Overlay (Bangle.js 2 only) +// Draw current location+direction with LCD Overlay (Bangle.js 2 only) function drawLocation() { if (!Bangle.setLCDOverlay) { return; // Overlay not supported } - if (!fix.fix || !mapVisible) { + if (!fix.fix || !mapVisible || settings.dirSrc === 0) { if (this.hasOverlay) { Bangle.setLCDOverlay(); // clear if map is not visible or no fix this.hasOverlay = false; @@ -89,9 +103,20 @@ function drawLocation() { return; } - const icon = require("heatshrink").decompress(atob("jEYwYPMyVJkgHEkgICyAHCgIIDyQIChIIEoAIDC4IIEBwOAgEEyVIBAY4DBD4sGHxBQIMRAIIPpAyCHAYILUJEAiVJkAIFgVJXo5fCABQA==")); // 24x24px var p = m.latLonToXY(fix.lat, fix.lon); - Bangle.setLCDOverlay(icon, p.x-24/2, p.y-24); + + ovLoc.clear(); + if (isInside(R, p, ovLoc.getWidth(), ovLoc.getHeight())) { // avoid drawing over widget area + const angle = settings.dirSrc === 1 ? fix.course : Bangle.getCompass().heading; + if (!isNaN(angle)) { + ovLoc.drawImage(imgLoc, ovLoc.getWidth()/2, ovLoc.getHeight()/2, {rotate: angle*Math.PI/180}); + } + } + Bangle.setLCDOverlay({width:ovLoc.getWidth(), height:ovLoc.getHeight(), + bpp:4, transparent:0, + buffer:ovLoc.buffer + }, p.x-ovLoc.getWidth()/2, p.y-ovLoc.getHeight()/2); + this.hasOverlay = true; } @@ -158,13 +183,31 @@ function showMenu() { value : !!settings.drawMarker, onchange : v => { settings.drawMarker=v; writeSettings(); } }, - /*LANG*/"Center Map": () =>{ + }); + + if (Bangle.setLCDOverlay) { + menu[/*LANG*/"Direction source"] = { + value: settings.dirSrc, + min: 0, max: 2, + format: v => [/*LANG*/"None", /*LANG*/"GPS", /*LANG*/"Compass"][v], + onchange: v => { + settings.dirSrc = v; + writeSettings(); + } + }; + menu[/*LANG*/"Reset compass"] = () => { + Bangle.resetCompass(); + showMap(); + }; + } + + menu[/*LANG*/"Center Map"] = () =>{ m.lat = m.map.lat; m.lon = m.map.lon; m.scale = m.map.scale; showMap(); - } - }); + }; + // If we have the recorder widget, add a menu item to start/stop recording if (WIDGETS.recorder) { menu[/*LANG*/"Record"] = { diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 1b4ab2d7c..2d217cee6 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.21", + "version": "0.22", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "readme": "README.md", "icon": "app.png", From 0d8df8aaebce2b34201e964a52e2d756c1107d1a Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 22 Jul 2023 11:54:52 +0200 Subject: [PATCH 013/242] gipy: removing jit segfaults with the jit so i removed it. perfs are still ok due to the use of transformVertices instead i would have like to have both though :-( my guess is the segfaults are not really jit related but related to the size of the code in memory. --- apps/gipy/ChangeLog | 2 + apps/gipy/TODO | 3 - apps/gipy/app.js | 163 +++++++++++++++++++++----------------------- 3 files changed, 81 insertions(+), 87 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index e8055100a..870ad0fdb 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -95,4 +95,6 @@ * Turn screen off while idling to save battery (with setting) * New setting : disable buzz on turns * New setting : turn bluetooth off to save battery + * New setting : power screen off between points to save battery * Color change for lost direction (now purple) + * Adaptive screen powersaving diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 294f18952..b2a3c7ae1 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -37,9 +37,6 @@ my conclusion is that: ************************** + when you walk the direction still has a tendency to shift -+ don't do the new powersave if you walk ? -+ can we turn lcd off without locking ? -+ function Graphics.transformVertices + put back foot only ways + try fiddling with jit diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 85319922a..9e0dbce24 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -27,23 +27,17 @@ var settings = Object.assign({ s.readJSON("gipy.json", true) || {} ); -let profile_start_times = []; +// let profile_start_times = []; -let splashscreen = require("heatshrink").decompress( - atob( - "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" - ) -); +// function start_profiling() { +// profile_start_times.push(getTime()); +// } -function start_profiling() { - profile_start_times.push(getTime()); -} - -function end_profiling(label) { - let end_time = getTime(); - let elapsed = end_time - profile_start_times.pop(); - console.log("profile:", label, "took", elapsed); -} +// function end_profiling(label) { +// let end_time = getTime(); +// let elapsed = end_time - profile_start_times.pop(); +// console.log("profile:", label, "took", elapsed); +// } // return the index of the largest element of the array which is <= x function binary_search(array, x) { @@ -399,28 +393,20 @@ class Map { cos_direction, sin_direction ) { - "jit"; + // "jit"; let center_x = g.getWidth() / 2; let center_y = g.getHeight() / 2 + Y_OFFSET; let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); let scaled_current_x = current_x * scale_factor; let scaled_current_y = current_y * scale_factor; + let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); + let c = cos_direction; + let s = sin_direction; + let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); - for (let i = 0; i < points.length; i += 4) { - let scaled_x = points[i] - scaled_current_x; - let scaled_y = points[i + 1] - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = center_x - rotated_x; - let final_y = center_y + rotated_y; - scaled_x = points[i + 2] - scaled_current_x; - scaled_y = points[i + 3] - scaled_current_y; - rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let new_final_x = center_x - rotated_x; - let new_final_y = center_y + rotated_y; - g.drawLine(final_x, final_y, new_final_x, new_final_y); + for (let i = 0; i < screen_points.length; i += 4) { + g.drawLine(screen_points[i], screen_points[i + 1], screen_points[i + 2], screen_points[i + 3]); } } @@ -433,26 +419,23 @@ class Map { cos_direction, sin_direction ) { + // "jit"; let center_x = g.getWidth() / 2; let center_y = g.getHeight() / 2 + Y_OFFSET; let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); let scaled_current_x = current_x * scale_factor; let scaled_current_y = current_y * scale_factor; + let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); + let c = cos_direction; + let s = sin_direction; + let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); - for (let i = 0; i < points.length; i += 4) { - let scaled_x = points[i] - scaled_current_x; - let scaled_y = points[i + 1] - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = center_x - rotated_x; - let final_y = center_y + rotated_y; - scaled_x = points[i + 2] - scaled_current_x; - scaled_y = points[i + 3] - scaled_current_y; - rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let new_final_x = center_x - rotated_x; - let new_final_y = center_y + rotated_y; + for (let i = 0; i < screen_points.length; i += 4) { + let final_x = screen_points[i]; + let final_y = screen_points[i + 1]; + let new_final_x = screen_points[i + 2]; + let new_final_y = screen_points[i + 3]; let xdiff = new_final_x - final_x; let ydiff = new_final_y - final_y; @@ -1156,7 +1139,15 @@ class Status { function load_gps(filename) { // let's display splash screen while loading file + + let splashscreen = require("heatshrink").decompress( + atob( + "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" + ) + ); + g.clear(); + g.drawImage(splashscreen, 0, 0); g.setFont("6x8:2") .setFontAlign(-1, -1, 0) @@ -1314,9 +1305,9 @@ class Point { times(scalar) { return new Point(this.lon * scalar, this.lat * scalar); } - dot(other_point) { - return this.lon * other_point.lon + this.lat * other_point.lat; - } + // dot(other_point) { + // return this.lon * other_point.lon + this.lat * other_point.lat; + // } distance(other_point) { //see https://www.movable-type.co.uk/scripts/latlong.html const R = 6371e3; // metres @@ -1375,43 +1366,6 @@ class Point { let fake_gps_point = 0; -function simulate_gps(status) { - if (status.path === null) { - let map = status.maps[0]; - let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); - let p2 = new Point( - map.start_coordinates[0] + map.side * map.grid_size[0], - map.start_coordinates[1] + map.side * map.grid_size[1] - ); - let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); - if (fake_gps_point < 1) { - fake_gps_point += 0.01; - } - status.update_position(pos); - } else { - if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { - return; - } - let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len / 2 - 1) { - return; - } - let p1 = status.path.point(2 * point_index); // use these to approximately follow path - let p2 = status.path.point(2 * (point_index + 1)); - //let p1 = status.path.point(point_index); // use these to strictly follow path - //let p2 = status.path.point(point_index + 1); - - let alpha = fake_gps_point - point_index; - let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - - if (go_backwards) { - fake_gps_point -= 0.01; // advance simulation - } else { - fake_gps_point += 0.01; // advance simulation - } - status.update_position(pos); - } -} function drawMenu() { const menu = { @@ -1536,6 +1490,47 @@ function start_gipy(path, maps, interests) { // let's keep the screen on in simulations Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); + Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + + + function simulate_gps(status) { + if (status.path === null) { + let map = status.maps[0]; + let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); + let p2 = new Point( + map.start_coordinates[0] + map.side * map.grid_size[0], + map.start_coordinates[1] + map.side * map.grid_size[1] + ); + let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); + if (fake_gps_point < 1) { + fake_gps_point += 0.05; + } + status.update_position(pos); + } else { + if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { + return; + } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len / 2 - 1) { + return; + } + let p1 = status.path.point(2 * point_index); // use these to approximately follow path + let p2 = status.path.point(2 * (point_index + 1)); + //let p1 = status.path.point(point_index); // use these to strictly follow path + //let p2 = status.path.point(point_index + 1); + + let alpha = fake_gps_point - point_index; + let pos = p1.times(1 - alpha).plus(p2.times(alpha)); + + if (go_backwards) { + fake_gps_point -= 0.05; // advance simulation + } else { + fake_gps_point += 0.05; // advance simulation + } + status.update_position(pos); + } + } + setInterval(simulate_gps, 500, status); } else { status.activate(); @@ -1550,7 +1545,7 @@ function start_gipy(path, maps, interests) { if (valid_coordinates) { if (status.starting_time === null) { status.starting_time = getTime(); - Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + Bangle.loadWidgets(); // load them even in simulation to eat mem } status.update_position(new Point(data.lon, data.lat)); } From 64238f4ec0e646c41a6f1590e716f02a89946700 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 12:02:34 +0200 Subject: [PATCH 014/242] iconlaunch - Better performance by less array accesses --- apps/iconlaunch/app.js | 43 +++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 9f8cedb0f..4eb4f5721 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -9,7 +9,6 @@ timeOut:"Off" }, s.readJSON("iconlaunch.json", true) || {}); - if (!settings.fullscreen) { Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -19,9 +18,9 @@ let launchCache = s.readJSON("iconlaunch.cache.json", true)||{}; let launchHash = s.hash(/\.info/); if (launchCache.hash!=launchHash) { - launchCache = { - hash : launchHash, - apps : s.list(/\.info$/) + launchCache = { + hash : launchHash, + apps : s.list(/\.info$/) .map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}) .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)) .sort((a,b)=>{ @@ -34,6 +33,17 @@ s.writeJSON("iconlaunch.cache.json", launchCache); } + // cache items + let count = 0; + launchCache.items = []; + for (let c of launchCache.apps){ + let i = Math.floor(count/3); + if (!launchCache.items[i]) + launchCache.items.push([]); + launchCache.items[Math.floor(count/3)].push(c); + count++; + } + let selectedItem = -1; const R = Bangle.appRect; const iconSize = 48; @@ -42,18 +52,26 @@ const itemSize = iconSize + whitespace; let drawItem = function(itemI, r) { + let t = Date.now(); g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); let x = 0; - for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) { - if (!launchCache.apps[i]) break; + let firstApp = itemI * appsN; + let numberOfApps = appsN * (itemI + 1); + let apps = launchCache.items[itemI]; + let i = firstApp - 1; + let selectedApp; + let currentApp; + for (currentApp of apps) { + i++; x += whitespace; - if (!launchCache.apps[i].icon) { + if (!currentApp.icon) { g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2); } else { - if (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon); - g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y); + if (!currentApp.icondata) currentApp.icondata = s.read(currentApp.icon); + g.drawImage(currentApp.icondata, x + r.x, r.y); } if (selectedItem == i) { + selectedApp = currentApp; g.drawRect( x + r.x - 1, r.y - 1, @@ -63,13 +81,12 @@ } x += iconSize; } - drawText(itemI, r.y); + if (selectedApp) drawText(itemI, r.y, selectedApp); }; - let drawText = function(i, appY) { - const selectedApp = launchCache.apps[selectedItem]; + let drawText = function(i, appY, selectedApp) { const idy = (selectedItem - (selectedItem % 3)) / 3; - if (!selectedApp || i != idy) return; + if (i != idy) return; appY = appY + itemSize/2; g.setFontAlign(0, 0, 0); g.setFont("12x20"); From 76425b947b7166f047435ffe07e08b064c5545d6 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 12:24:35 +0200 Subject: [PATCH 015/242] iconlaunch - Render icons for one item at once --- apps/iconlaunch/app.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 4eb4f5721..bb9574e16 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -61,6 +61,8 @@ let i = firstApp - 1; let selectedApp; let currentApp; + let layers=[]; + let selectedRect; for (currentApp of apps) { i++; x += whitespace; @@ -68,19 +70,21 @@ g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2); } else { if (!currentApp.icondata) currentApp.icondata = s.read(currentApp.icon); - g.drawImage(currentApp.icondata, x + r.x, r.y); + layers.push({x:x+r.x,y:r.y,image:currentApp.icondata}); } if (selectedItem == i) { selectedApp = currentApp; - g.drawRect( + selectedRect = [ x + r.x - 1, r.y - 1, x + r.x + iconSize + 1, r.y + iconSize + 1 - ); + ]; } x += iconSize; } + g.drawImages(layers); + if (selectedRect) g.drawRect.apply(null, selectedRect); if (selectedApp) drawText(itemI, r.y, selectedApp); }; From 3adc3f8fba930b8d66be3b11a4d62c9e59f2276d Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 12:32:53 +0200 Subject: [PATCH 016/242] iconlaunch - Precache icons and only clear if needed --- apps/iconlaunch/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index bb9574e16..2beea919d 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -41,6 +41,7 @@ if (!launchCache.items[i]) launchCache.items.push([]); launchCache.items[Math.floor(count/3)].push(c); + c.icondata = s.read(c.icon); count++; } @@ -53,7 +54,6 @@ let drawItem = function(itemI, r) { let t = Date.now(); - g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); let x = 0; let firstApp = itemI * appsN; let numberOfApps = appsN * (itemI + 1); @@ -69,7 +69,6 @@ if (!currentApp.icon) { g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2); } else { - if (!currentApp.icondata) currentApp.icondata = s.read(currentApp.icon); layers.push({x:x+r.x,y:r.y,image:currentApp.icondata}); } if (selectedItem == i) { @@ -83,6 +82,7 @@ } x += iconSize; } + if (selectedRect) g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); g.drawImages(layers); if (selectedRect) g.drawRect.apply(null, selectedRect); if (selectedApp) drawText(itemI, r.y, selectedApp); From f7590cff9f9b637132ed83588b1e79e627a15976 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 13:06:26 +0200 Subject: [PATCH 017/242] iconlaunch - Dedicated icon for apps with missing icons --- apps/iconlaunch/app.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 2beea919d..989c030a1 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -34,6 +34,7 @@ } // cache items + const ICON_MISSING = atob("MDABAAAAAAAAAAAAAAAAAAABAAAAAAADgAAAAAAGwAAAAAAMYAAAAAAYMAAAAAAwGAAAAABgDAAAAADABgAAAAGAAwAAAAMAAYAAAAYP4MAAAAw//GAAABh4/jAAADD4fhgAAGD8PwwAAMD8PwYAAYD8PwMAAwD8PwGABgB4fwDADAAAfgBgGAAAfgAwMAAA+AAYGAAB8AAwDAABwABgBgADgADAAwADAAGAAYADAAMAAMAAAAYAAGAAAAwAADADgBgAABgHwDAAAAwPwGAAAAYP4MAAAAMPwYAAAAGPwwAAAADHhgAAAABgDAAAAAAwGAAAAAAYMAAAAAAMYAAAAAAGwAAAAAADgAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); let count = 0; launchCache.items = []; for (let c of launchCache.apps){ @@ -41,7 +42,10 @@ if (!launchCache.items[i]) launchCache.items.push([]); launchCache.items[Math.floor(count/3)].push(c); - c.icondata = s.read(c.icon); + if (c.icon) + c.icondata = s.read(c.icon); + else + c.icondata = ICON_MISSING; count++; } @@ -55,10 +59,8 @@ let drawItem = function(itemI, r) { let t = Date.now(); let x = 0; - let firstApp = itemI * appsN; - let numberOfApps = appsN * (itemI + 1); let apps = launchCache.items[itemI]; - let i = firstApp - 1; + let i = itemI * appsN; let selectedApp; let currentApp; let layers=[]; @@ -66,11 +68,7 @@ for (currentApp of apps) { i++; x += whitespace; - if (!currentApp.icon) { - g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2); - } else { - layers.push({x:x+r.x,y:r.y,image:currentApp.icondata}); - } + layers.push({x:x+r.x,y:r.y,image:currentApp.icondata}); if (selectedItem == i) { selectedApp = currentApp; selectedRect = [ From c4ae56ed169bfa4f36cd5d9ff95e6dc1ecae34fb Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 13:09:08 +0200 Subject: [PATCH 018/242] iconlaunch - Bump version --- apps/iconlaunch/ChangeLog | 3 ++- apps/iconlaunch/metadata.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog index 8bad496bf..f9d143fca 100644 --- a/apps/iconlaunch/ChangeLog +++ b/apps/iconlaunch/ChangeLog @@ -21,4 +21,5 @@ 0.15: Ensure that we hide widgets if in fullscreen mode (So that widgets are still hidden if launcher is fast-loaded) 0.16: Use firmware provided E.showScroller method -0.17: fix fullscreen with oneClickExit +0.17: fix fullscreen with oneClickExit +0.18: Better performance diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index 35a7907bd..b515a88e7 100644 --- a/apps/iconlaunch/metadata.json +++ b/apps/iconlaunch/metadata.json @@ -2,7 +2,7 @@ "id": "iconlaunch", "name": "Icon Launcher", "shortName" : "Icon launcher", - "version": "0.17", + "version": "0.18", "icon": "app.png", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "tags": "tool,system,launcher", From aaa992ee2b4999271a3cb874c8ba6a81daec5a2e Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 13:20:57 +0200 Subject: [PATCH 019/242] iconlaunch - Use back option instead of translated entry --- apps/iconlaunch/settings.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js index f4c0599f7..3278075e4 100644 --- a/apps/iconlaunch/settings.js +++ b/apps/iconlaunch/settings.js @@ -16,8 +16,7 @@ } const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; const appMenu = { - "": { "title": /*LANG*/"Launcher" }, - /*LANG*/"< Back": back, + "": { "title": /*LANG*/"Launcher", back: back }, /*LANG*/"Show Clocks": { value: settings.showClocks == true, onchange: (m) => { From f4a72b777d0b5178d0de856116aa721383a18669 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 13:22:46 +0200 Subject: [PATCH 020/242] iconlaunch - Remove debug code --- apps/iconlaunch/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 989c030a1..e78cd81d8 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -57,7 +57,6 @@ const itemSize = iconSize + whitespace; let drawItem = function(itemI, r) { - let t = Date.now(); let x = 0; let apps = launchCache.items[itemI]; let i = itemI * appsN; From 91df2dc68e49c139115db797a58af7f969b99788 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 17:34:45 +0200 Subject: [PATCH 021/242] iconlaunch - Fix off by one error when selecting apps --- apps/iconlaunch/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index e78cd81d8..5db324163 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -59,7 +59,7 @@ let drawItem = function(itemI, r) { let x = 0; let apps = launchCache.items[itemI]; - let i = itemI * appsN; + let i = itemI * appsN - 1; let selectedApp; let currentApp; let layers=[]; From ffd967cb634258e6223e0a1f8656f817d98a6196 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 18:04:38 +0200 Subject: [PATCH 022/242] iconlauch - Center icons in items to prevent selection box beeing cut off at top --- apps/iconlaunch/app.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 5db324163..a5f0ee1d6 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -36,12 +36,21 @@ // cache items const ICON_MISSING = atob("MDABAAAAAAAAAAAAAAAAAAABAAAAAAADgAAAAAAGwAAAAAAMYAAAAAAYMAAAAAAwGAAAAABgDAAAAADABgAAAAGAAwAAAAMAAYAAAAYP4MAAAAw//GAAABh4/jAAADD4fhgAAGD8PwwAAMD8PwYAAYD8PwMAAwD8PwGABgB4fwDADAAAfgBgGAAAfgAwMAAA+AAYGAAB8AAwDAABwABgBgADgADAAwADAAGAAYADAAMAAMAAAAYAAGAAAAwAADADgBgAABgHwDAAAAwPwGAAAAYP4MAAAAMPwYAAAAGPwwAAAADHhgAAAABgDAAAAAAwGAAAAAAYMAAAAAAMYAAAAAAGwAAAAAADgAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); let count = 0; + + let selectedItem = -1; + const R = Bangle.appRect; + const iconSize = 48; + const appsN = Math.floor(R.w / iconSize); + const whitespace = Math.floor((R.w - appsN * iconSize) / (appsN + 1)); + const iconYoffset = Math.floor(whitespace/4)-1; + const itemSize = iconSize + whitespace; + launchCache.items = []; for (let c of launchCache.apps){ - let i = Math.floor(count/3); + let i = Math.floor(count/appsN); if (!launchCache.items[i]) launchCache.items.push([]); - launchCache.items[Math.floor(count/3)].push(c); + launchCache.items[Math.floor(count/appsN)].push(c); if (c.icon) c.icondata = s.read(c.icon); else @@ -49,13 +58,6 @@ count++; } - let selectedItem = -1; - const R = Bangle.appRect; - const iconSize = 48; - const appsN = Math.floor(R.w / iconSize); - const whitespace = (R.w - appsN * iconSize) / (appsN + 1); - const itemSize = iconSize + whitespace; - let drawItem = function(itemI, r) { let x = 0; let apps = launchCache.items[itemI]; @@ -67,14 +69,14 @@ for (currentApp of apps) { i++; x += whitespace; - layers.push({x:x+r.x,y:r.y,image:currentApp.icondata}); + layers.push({x:x+r.x,y:r.y + iconYoffset,image:currentApp.icondata}); if (selectedItem == i) { selectedApp = currentApp; selectedRect = [ x + r.x - 1, - r.y - 1, - x + r.x + iconSize + 1, - r.y + iconSize + 1 + r.y + iconYoffset - 1, + x + r.x + iconSize, + r.y + iconYoffset + iconSize ]; } x += iconSize; From 2af98c7347eb37219d03e4e2de056ae0fd04e695 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sat, 22 Jul 2023 20:06:53 +0200 Subject: [PATCH 023/242] openstmap: replace direction icon --- apps/openstmap/app.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 4069cca4c..fe44b6413 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -7,10 +7,13 @@ var hasScrolled = false; var settings = require("Storage").readJSON("openstmap.json",1)||{}; var plotTrack; let checkMapPos = false; // Do we need to check the if the coordinates we have are valid -// Icon for current location+direction: https://icons8.com/icon/13793/north-direction 32x32, 3 Bit + transparency -const imgLoc = Bangle.setLCDOverlay ? require("heatshrink").decompress(atob("kEgwYspgmABacB2wLBm3ABYsN20kyE27ALFtuwBYMG7YKEgdtwMkyUA7dgBYcbsEBBYUDtgLDCIMJBYNADoJLDFIILDGoILDIIMJgECBYMNBYUGBYNIAoICBhuwBYhlFBYJ0BJoo7B23ABZYjLBZhTCBYprFUIoLEHYwLDTYILETYgRCgYOBDorLCBYLLGcYXbNAJNGPQR0DAAwLBIgYAFHYQLUACQA=")) : undefined; -// overlay buffer for current location, image is 32x32, so rotated max is 46x46 -const ovLoc = Bangle.setLCDOverlay ? Graphics.createArrayBuffer(46,46,4,{msb:true}) : undefined; +// Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted +if (Bangle.setLCDOverlay) { + var imgLoc = require("heatshrink").decompress(atob("jEYwINLAQk8AQl+AQn/AQcB/+AAQUD//AAQUH//gAQUP//wAQUf//4j8AvA9IA==")); + // overlay buffer for current location, a bit bigger then image so we can rotate + const ovSize = Math.ceil(Math.sqrt(imgLoc[0]*imgLoc[0]+imgLoc[1]*imgLoc[1])); + var ovLoc = Graphics.createArrayBuffer(ovSize,ovSize,imgLoc[2] & 0x7f,{msb:true}); +} if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) { // restore last view @@ -113,7 +116,8 @@ function drawLocation() { } } Bangle.setLCDOverlay({width:ovLoc.getWidth(), height:ovLoc.getHeight(), - bpp:4, transparent:0, + bpp:ovLoc.getBPP(), transparent:0, + palette:new Uint16Array([0, g.toColor("#00F")]), buffer:ovLoc.buffer }, p.x-ovLoc.getWidth()/2, p.y-ovLoc.getHeight()/2); From 90bd7dcdb7c8c8da4daf1a7929fd75057cfee759 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sat, 22 Jul 2023 20:09:21 +0200 Subject: [PATCH 024/242] openstmap: move comment --- apps/openstmap/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index fe44b6413..8ad3c669d 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -7,8 +7,8 @@ var hasScrolled = false; var settings = require("Storage").readJSON("openstmap.json",1)||{}; var plotTrack; let checkMapPos = false; // Do we need to check the if the coordinates we have are valid -// Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted if (Bangle.setLCDOverlay) { + // Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted var imgLoc = require("heatshrink").decompress(atob("jEYwINLAQk8AQl+AQn/AQcB/+AAQUD//AAQUH//gAQUP//wAQUf//4j8AvA9IA==")); // overlay buffer for current location, a bit bigger then image so we can rotate const ovSize = Math.ceil(Math.sqrt(imgLoc[0]*imgLoc[0]+imgLoc[1]*imgLoc[1])); From ceddc0169714debd5632f29827af6ed1d0da5ae7 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sat, 22 Jul 2023 20:12:18 +0200 Subject: [PATCH 025/242] openstmap: updated Changelog --- apps/openstmap/ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index ac35122e3..b30a871f9 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -26,4 +26,4 @@ If 'Recorder' app installed, add a 'Record' menu item 0.21: Draw a current position marker (Bangle.js 2 only) Enable/Disable previous position marker in new setting "Draw cont. position" -0.22: Replace position marker with location+direction pointer +0.22: Replace position marker with direction arrow From 2f12b9ad6e0511360a7e6849b5246486357f0929 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 21:46:10 +0200 Subject: [PATCH 026/242] kineticscroll - Initial implementation --- apps/kineticscroll/ChangeLog | 1 + apps/kineticscroll/README.md | 7 ++ apps/kineticscroll/app.png | Bin 0 -> 505 bytes apps/kineticscroll/boot.js | 175 +++++++++++++++++++++++++++++++ apps/kineticscroll/metadata.json | 14 +++ 5 files changed, 197 insertions(+) create mode 100644 apps/kineticscroll/ChangeLog create mode 100644 apps/kineticscroll/README.md create mode 100644 apps/kineticscroll/app.png create mode 100644 apps/kineticscroll/boot.js create mode 100644 apps/kineticscroll/metadata.json diff --git a/apps/kineticscroll/ChangeLog b/apps/kineticscroll/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/kineticscroll/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/kineticscroll/README.md b/apps/kineticscroll/README.md new file mode 100644 index 000000000..be451a7f4 --- /dev/null +++ b/apps/kineticscroll/README.md @@ -0,0 +1,7 @@ +# Kinetic scrolling + +This patches the default scroller implementation to use kinetic scrolling. It is based on the original implementation. + +## Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/kineticscroll/app.png b/apps/kineticscroll/app.png new file mode 100644 index 0000000000000000000000000000000000000000..f3184b5c07045352c5bb38d5bf62e20f330e9b58 GIT binary patch literal 505 zcmVKKo=MSs^B&R9)J~J z@q56y1mVhR-@tBG@oRyISB8m9mFQXtPJkt#of4bI6Y`PF$g3YGXY6NwS>m;nm>JJY zZN{8%=GPkB7Ga%~*fjnh73f%bwI*j&pv{@Gg{o)(kFqTYmG==U8x;+BvI%?wec4l0 zt^!v^j{PbcxHobfRnfqmk>fZ=9u*CEa%=5SD_4OVYlBxg1J_0ly|b+JGJ2XLkBSBy z*#HiJb-BJY?%bKg^mm8S*OkI5@Fx97Kohtya-2(SB-i~A4ZIp-w9sec`pFQlq;GHr zwk(nJ+DQ5aXF$^czEaM2wnJZq8goqeh%ap!wN3bnFZDgpl*#%cpM6n2S|-MzyoXeI zNDLiXlQSR2z#Yn?<$JXFXbc=kq9QAi$npNb(9z41Kp$oQIIQVKwJ}i7dKX%Om>B!~ v!7-&SrF*v~_M+QFezWIu=Od9wgu?g(>+wy_CP~wG00000NkvXXu0mjfFumUv literal 0 HcmV?d00001 diff --git a/apps/kineticscroll/boot.js b/apps/kineticscroll/boot.js new file mode 100644 index 000000000..f0bd0af74 --- /dev/null +++ b/apps/kineticscroll/boot.js @@ -0,0 +1,175 @@ +(function() { + E.showScroller = function(options) { + /* options = { + h = height + c = # of items + scroll = initial scroll position + scrollMin = minimum scroll amount (can be negative) + draw = function(idx, rect) + remove = function() + select = function(idx, touch) + } + + returns { + scroll: int // current scroll amount + draw: function() // draw all + drawItem : function(idx) // draw specific item + isActive : function() // is this scroller still active? + } + + */ + if (!options) return Bangle.setUI(); // remove existing handlers + + const MAX_VELOCITY=100; + let scheduledDraw; + let velocity = 0; + let accDy = 0; + let scheduledBrake = setInterval(()=>{velocity*=0.9;}, 50); + let lastDragStart = 0; + let R = Bangle.appRect; + let menuScrollMin = 0|options.scrollMin; + let menuScrollMax = options.h*options.c - R.h; + if (menuScrollMax{ + if (e.y=0) && i { + let dy = velocity; + if (s.scroll - dy > menuScrollMax){ + dy = s.scroll - menuScrollMax; + velocity = 0; + } + if (s.scroll - dy < menuScrollMin){ + dy = s.scroll - menuScrollMin; + velocity = 0; + } + + s.scroll -= dy; + + let oldScroll = rScroll; + rScroll = s.scroll &~1; + let d = oldScroll-rScroll; + + if (Math.abs(velocity) > 0.01) + scheduledDraw = setTimeout(draw,0); + else + scheduledDraw = undefined; + + if (!d) { + return; + } + g.reset().setClipRect(R.x,R.y,R.x2,R.y2).scroll(0,d); + if (d < 0) { + let y = Math.max(R.y2-(1-d), R.y); + g.setClipRect(R.x,y,R.x2,R.y2); + let i = YtoIdx(y); + + for (y = idxToY(i);y < R.y2;y+=options.h) { + options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); + i++; + } + } else { // d>0 + let y = Math.min(R.y+d, R.y2); + g.setClipRect(R.x,R.y,R.x2,y); + let i = YtoIdx(y); + y = idxToY(i); + + for (y = idxToY(i);y > R.y-options.h;y-=options.h) { + options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); + i--; + } + } + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + }; + + const dragHandler = e=>{ + if ((velocity <0 && e.dy>0) || (velocity > 0 && e.dy<0)){ + velocity *= -1; + accDy = 5 * velocity; + } + //velocity += e.dy * (Date.now() - lastDrag); + if (e.b > 0){ + if (!lastDragStart){ + lastDragStart = Date.now(); + velocity = 0; + accDy = 0; + } + accDy += e.dy; + } + velocity = accDy / (Date.now() - lastDragStart) * MAX_VELOCITY; + + if (lastDragStart && e.b == 0){ + accDy = 0; + lastDragStart = 0; + } + + velocity = E.clip(velocity,-MAX_VELOCITY,MAX_VELOCITY); + lastDrag=Date.now(); + if (!scheduledDraw){ + scheduledDraw = setTimeout(draw,0); + } + }; + + let uiOpts = { + mode : "custom", + back : options.back, + remove : ()=>{ + if (scheduledDraw) + clearTimeout(scheduledDraw); + clearInterval(scheduledBrake); + if (options.remove) options.remove(); + }, + drag : dragHandler, + touch : touchHandler + } + + if (options.remove) uiOpts.remove = () => { + if (scheduledDraw) + clearTimeout(scheduledDraw); + clearInterval(scheduledBrake); + if (options.remove) options.remove(); + } + + Bangle.setUI(uiOpts); + + + function idxToY(i) { + return i*options.h + R.y - rScroll; + } + function YtoIdx(y) { + return Math.floor((y + rScroll - R.y)/options.h); + } + + let s = { + scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax), + draw : () => { + g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2); + let a = YtoIdx(R.y); + let b = Math.min(YtoIdx(R.y2),options.c-1); + for (let i=a;i<=b;i++) + options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + }, drawItem : i => { + let y = idxToY(i); + g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2)); + options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + }, isActive : () => Bangle.touchHandler == touchHandler + }; + + let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither) + s.draw(); // draw the full scroller + g.flip(); // force an update now to make this snappier + return s; + }; +})(); diff --git a/apps/kineticscroll/metadata.json b/apps/kineticscroll/metadata.json new file mode 100644 index 000000000..9311ea8d3 --- /dev/null +++ b/apps/kineticscroll/metadata.json @@ -0,0 +1,14 @@ +{ "id": "kineticscroll", + "name": "Kinetic Scroll", + "shortName":"Kinetic Scroll", + "version":"0.01", + "description": "Replacement for the system scroller with kinetic scrolling.", + "icon": "app.png", + "type": "bootloader", + "tags": "system", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"kineticscroll.boot.js","url":"boot.js"} + ] +} From b6459ad26aaf5d43a611ee1ce4d9164976a8ba55 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 22 Jul 2023 23:21:31 +0200 Subject: [PATCH 027/242] iconlaunch - Load N/A icon from image file --- apps/iconlaunch/app.js | 2 +- apps/iconlaunch/metadata.json | 3 ++- apps/iconlaunch/na.img | Bin 0 -> 291 bytes 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 apps/iconlaunch/na.img diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index a5f0ee1d6..8f99fcae7 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -34,7 +34,7 @@ } // cache items - const ICON_MISSING = atob("MDABAAAAAAAAAAAAAAAAAAABAAAAAAADgAAAAAAGwAAAAAAMYAAAAAAYMAAAAAAwGAAAAABgDAAAAADABgAAAAGAAwAAAAMAAYAAAAYP4MAAAAw//GAAABh4/jAAADD4fhgAAGD8PwwAAMD8PwYAAYD8PwMAAwD8PwGABgB4fwDADAAAfgBgGAAAfgAwMAAA+AAYGAAB8AAwDAABwABgBgADgADAAwADAAGAAYADAAMAAMAAAAYAAGAAAAwAADADgBgAABgHwDAAAAwPwGAAAAYP4MAAAAMPwYAAAAGPwwAAAADHhgAAAABgDAAAAAAwGAAAAAAYMAAAAAAMYAAAAAAGwAAAAAADgAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + const ICON_MISSING = s.read("iconlaunch.na.img"); let count = 0; let selectedItem = -1; diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index b515a88e7..e17830ca6 100644 --- a/apps/iconlaunch/metadata.json +++ b/apps/iconlaunch/metadata.json @@ -10,7 +10,8 @@ "supports": ["BANGLEJS2"], "storage": [ { "name": "iconlaunch.app.js", "url": "app.js" }, - { "name": "iconlaunch.settings.js", "url": "settings.js" } + { "name": "iconlaunch.settings.js", "url": "settings.js" }, + { "name": "iconlaunch.na.img", "url": "na.img" } ], "data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}], "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }], diff --git a/apps/iconlaunch/na.img b/apps/iconlaunch/na.img new file mode 100644 index 0000000000000000000000000000000000000000..10f4a8f82e3098850708e4ba4bb60a6fcd7918e8 GIT binary patch literal 291 zcmZ9Iu?<2o3`8A83QeTnW(;qTRWb(EClxI#8BwqXBT)V#LOjbAxVXsno$egfBtI2x zo<1xFNF9z%9kn@*>SzT>GlQ8I`t~yDeGTa64+1@Q1Y>mswt9iKdWODwnnlhRF*dsl zSi2~SLt2yc7Oj#@MiG*jE7Ei;%&@>13zV8C%62l8bu)&MA#e9dw%bFf=hZPXNB+1a P?keWnxcBcXd_jEyDnTgg literal 0 HcmV?d00001 From 9477753cab059f659c2848b42134c92bea860f53 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 10:41:30 +0200 Subject: [PATCH 028/242] messagesoverlay - Remove unused function --- apps/messagesoverlay/lib.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js index 9af14bbe4..d25031c69 100644 --- a/apps/messagesoverlay/lib.js +++ b/apps/messagesoverlay/lib.js @@ -232,13 +232,6 @@ let next = function(ovr) { showMessage(ovr, eventQueue[0]); }; -let showMapMessage = function(ovr, msg) { - ovr.clearRect(2,2,ovr.getWidth()-3,ovr.getHeight()-3); - drawMessage(ovr, { - body: "Not implemented!" - }); -}; - let callBuzzTimer = null; let stopCallBuzz = function() { if (callBuzzTimer) { From 8ce2245538dce7c8ac754642129142f0afe5e070 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 10:42:21 +0200 Subject: [PATCH 029/242] messagesoverlay - Better low memory handling --- apps/messagesoverlay/lib.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js index d25031c69..a90f7b0c1 100644 --- a/apps/messagesoverlay/lib.js +++ b/apps/messagesoverlay/lib.js @@ -1,3 +1,5 @@ +const MIN_FREE_MEM = 1000; +const LOW_MEM = 2000; const ovrx = 10; const ovry = 10; const ovrw = g.getWidth()-2*ovrx; @@ -432,9 +434,14 @@ exports.message = function(type, event) { if(event.handled) return; bpp = 4; - if (process.memory().free < 2000) bpp = 1; + if (process.memory().free < LOW_MEM) bpp = 1; - if (!ovr) { + while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){ + let dropped = eventQueue.pop(); + print("Dropped message because of memory constraints", dropped); + } + + if (!ovr || bpp==1) { ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, { msb: true }); From c6433e828f627ccff447789fdf36b8ff2a671b16 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 11:00:33 +0200 Subject: [PATCH 030/242] messageoverlay - Fix the first overlay buffer beeing kept and reused on unlock --- apps/messagesoverlay/lib.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js index a90f7b0c1..203063104 100644 --- a/apps/messagesoverlay/lib.js +++ b/apps/messagesoverlay/lib.js @@ -30,6 +30,7 @@ let callInProgress = false; let show = function(ovr){ let img = ovr; + LOG("show", img.getBPP()); if (ovr.getBPP() == 1) { img = ovr.asImage(); img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]); @@ -164,8 +165,9 @@ let showMessage = function(ovr, msg) { drawMessage(ovr, msg); }; -let drawBorder = function(ovr) { +let drawBorder = function(img) { LOG("drawBorder", isQuiet()); + if (img) ovr=img; if (Bangle.isLocked()) ovr.setColor(ovr.theme.fgH); else @@ -402,7 +404,7 @@ let main = function(ovr, event) { if (!lockListener) { lockListener = function (){ - drawBorder(ovr); + drawBorder(); }; Bangle.on('lock', lockListener); } From 913c97c0b93e04577fb39224d82b3a8684f5dc9a Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 11:02:48 +0200 Subject: [PATCH 031/242] messagesoverlay - Bump version --- apps/messagesoverlay/ChangeLog | 2 ++ apps/messagesoverlay/metadata.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog index 8ce792783..47aa51107 100644 --- a/apps/messagesoverlay/ChangeLog +++ b/apps/messagesoverlay/ChangeLog @@ -3,3 +3,5 @@ 0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible. 0.04: Use the event mechanism for getting messages 0.05: Fix the overlay keeping the LCD on +0.06: Better low memory handling + Fix first message beeing displayed again on unlock diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json index a043fff64..c16a41f5c 100644 --- a/apps/messagesoverlay/metadata.json +++ b/apps/messagesoverlay/metadata.json @@ -1,7 +1,7 @@ { "id": "messagesoverlay", "name": "Messages Overlay", - "version": "0.05", + "version": "0.06", "description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)", "icon": "app.png", "type": "bootloader", From efaeae09036ce25f6483473ec26001fd7a523a3d Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 11:23:01 +0200 Subject: [PATCH 032/242] messagesoverlay - Explicitly check if buffer needs recreation --- apps/messagesoverlay/lib.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js index 203063104..6767cfbce 100644 --- a/apps/messagesoverlay/lib.js +++ b/apps/messagesoverlay/lib.js @@ -436,14 +436,15 @@ exports.message = function(type, event) { if(event.handled) return; bpp = 4; - if (process.memory().free < LOW_MEM) bpp = 1; + if (process.memory().free < LOW_MEM) + bpp = 1; while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){ let dropped = eventQueue.pop(); print("Dropped message because of memory constraints", dropped); } - if (!ovr || bpp==1) { + if (!ovr || ovr.getBPP() != bpp) { ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, { msb: true }); From 7c874199c4981ab95232e4ddf93d928d1f6e5762 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 11:32:03 +0200 Subject: [PATCH 033/242] kineticscroll - Add minified version for performance --- apps/kineticscroll/boot.min.js | 4 ++++ apps/kineticscroll/metadata.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 apps/kineticscroll/boot.min.js diff --git a/apps/kineticscroll/boot.min.js b/apps/kineticscroll/boot.min.js new file mode 100644 index 000000000..8ab9958cb --- /dev/null +++ b/apps/kineticscroll/boot.min.js @@ -0,0 +1,4 @@ +(function(){E.showScroller=function(c){function n(a){return a*c.h+b.y-k}function p(a){return Math.floor((a+k-b.y)/c.h)}if(!c)return Bangle.setUI();let h,e=0,l=0,t=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,m=0|c.scrollMin,r=c.h*c.c-b.h;r{d.ym||0<=a)&&a{var a=e;f.scroll-a>r&&(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=p(a);for(a=n(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)}},w={mode:"custom",back:c.back,remove:()=>{h&&clearTimeout(h);clearInterval(t);c.remove&&c.remove()},drag:a=> +{if(0>e&&0a.dy)e*=-1,l=5*e;0{h&&clearTimeout(h);clearInterval(t);c.remove&&c.remove()});Bangle.setUI(w);let f={scroll:E.clip(0|c.scroll,m,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=p(b.y);let d=Math.min(p(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:n(a),w:b.w,h:c.h});g.setClipRect(0, +0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=n(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.touchHandler==u},k=f.scroll&-2;f.draw();g.flip();return f}})() \ No newline at end of file diff --git a/apps/kineticscroll/metadata.json b/apps/kineticscroll/metadata.json index 9311ea8d3..022d38291 100644 --- a/apps/kineticscroll/metadata.json +++ b/apps/kineticscroll/metadata.json @@ -9,6 +9,6 @@ "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ - {"name":"kineticscroll.boot.js","url":"boot.js"} + {"name":"kineticscroll.boot.js","url":"boot.min.js"} ] } From a5190d4d860661dec0b3ee3c0ad8e3f8f32a27a7 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 11:53:31 +0200 Subject: [PATCH 034/242] kineticscroll - Fix setting a remove method for every scroller --- apps/kineticscroll/boot.js | 6 ------ apps/kineticscroll/boot.min.js | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/kineticscroll/boot.js b/apps/kineticscroll/boot.js index f0bd0af74..1da4da51c 100644 --- a/apps/kineticscroll/boot.js +++ b/apps/kineticscroll/boot.js @@ -123,12 +123,6 @@ let uiOpts = { mode : "custom", back : options.back, - remove : ()=>{ - if (scheduledDraw) - clearTimeout(scheduledDraw); - clearInterval(scheduledBrake); - if (options.remove) options.remove(); - }, drag : dragHandler, touch : touchHandler } diff --git a/apps/kineticscroll/boot.min.js b/apps/kineticscroll/boot.min.js index 8ab9958cb..2db3098a9 100644 --- a/apps/kineticscroll/boot.min.js +++ b/apps/kineticscroll/boot.min.js @@ -1,4 +1,4 @@ -(function(){E.showScroller=function(c){function n(a){return a*c.h+b.y-k}function p(a){return Math.floor((a+k-b.y)/c.h)}if(!c)return Bangle.setUI();let h,e=0,l=0,t=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,m=0|c.scrollMin,r=c.h*c.c-b.h;r{d.ym||0<=a)&&a{var a=e;f.scroll-a>r&&(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=p(a);for(a=n(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)}},w={mode:"custom",back:c.back,remove:()=>{h&&clearTimeout(h);clearInterval(t);c.remove&&c.remove()},drag:a=> -{if(0>e&&0a.dy)e*=-1,l=5*e;0{h&&clearTimeout(h);clearInterval(t);c.remove&&c.remove()});Bangle.setUI(w);let f={scroll:E.clip(0|c.scroll,m,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=p(b.y);let d=Math.min(p(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:n(a),w:b.w,h:c.h});g.setClipRect(0, -0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=n(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.touchHandler==u},k=f.scroll&-2;f.draw();g.flip();return f}})() \ No newline at end of file +(function(){E.showScroller=function(c){function m(a){return a*c.h+b.y-h}function n(a){return Math.floor((a+h-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,k=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,l=0|c.scrollMin,r=c.h*c.c-b.h;r{d.yl||0<=a)&&a{var a=e;f.scroll-a>r&&(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=n(a);for(a=m(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)}},v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0a.dy)e*=-1,k=5*e;0{p&&clearTimeout(p);clearInterval(w);c.remove&&c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,l,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=n(b.y);let d=Math.min(n(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:m(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=m(a);g.reset().setClipRect(b.x, +Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.touchHandler==t},h=f.scroll&-2;f.draw();g.flip();return f}})() \ No newline at end of file From a07ef21cf92a6c43f8083c8c4c7b53b45a29630d Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 23 Jul 2023 09:39:55 +0100 Subject: [PATCH 035/242] recorder: add clock info --- apps/recorder/clkinfo.js | 37 +++++++++++++++++++++++++++++++++++++ apps/recorder/metadata.json | 1 + 2 files changed, 38 insertions(+) create mode 100644 apps/recorder/clkinfo.js diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js new file mode 100644 index 000000000..a82fd22f9 --- /dev/null +++ b/apps/recorder/clkinfo.js @@ -0,0 +1,37 @@ +(function () { + const recimg = () => + require("heatshrink").decompress(atob("jEYxH+AHHCAAgVQ4fDCwYFCCpotFDQgZJCxYYLCxgYCOJgALFygwHLpphJIyJIFC9O72oXU3m02h3UC4O7U6m7FwhIQIwwwPCxJhMCwSNEDBm83hbBCxQZEDQQUCIhIZIAAO1UAwAzA=")); + + // TODO: deal with dark background - draw image instead? + const pauseimg = () => + require("heatshrink").decompress(atob("jEYxH+AH4Am64ABAxQWLCIYGGC6AHEF9QX/C/4X/C64HEF8YRDAxQA/AEQA=")); + + return { + name: "Recorder", + items: [ + { + name: "Toggle", + get: () => { + const w = typeof WIDGETS !== "undefined" && WIDGETS["recorder"]; + + return w && w.isRecording() ? { + text: "Recording", + short: "rec", + img: recimg(), + } : { + text: "Paused", + short: "paused", + img: pauseimg(), + } + }, + run: () => { + const w = WIDGETS["recorder"]; + Bangle.buzz(); + w.setRecording(!w.isRecording(), { force: "append" }); + }, + show: () => {}, + hide: () => {}, + }, + ], + }; +}); diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index e714abf8d..c04dc1808 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -13,6 +13,7 @@ {"name":"recorder.app.js","url":"app.js"}, {"name":"recorder.img","url":"app-icon.js","evaluate":true}, {"name":"recorder.wid.js","url":"widget.js"}, + {"name":"recorder.clkinfo.js","url":"clkinfo.js"}, {"name":"recorder.settings.js","url":"settings.js"} ], "data": [ From 38c903c3455b9885bebd0f27c195722d84d9f6e2 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 23 Jul 2023 11:35:42 +0100 Subject: [PATCH 036/242] recorder: bump version --- apps/recorder/ChangeLog | 1 + apps/recorder/metadata.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 991b811cb..5f4d1f8b4 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -35,3 +35,4 @@ 0.27: Fix first ever recorded filename being log0 (now all are dated) 0.28: Automatically create new track if the filename is different 0.29: When plotting with OpenStMap scale map to track width & height +0.30: Add clock info for showing and toggling recording state diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index c04dc1808..15ba165d1 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,10 +2,10 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.29", + "version": "0.30", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", - "tags": "tool,outdoors,gps,widget", + "tags": "tool,outdoors,gps,widget,clkinfo", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "interface": "interface.html", From 6e0c985ca8e872e25e6ae2ff3c01de95ca7bfd59 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 23 Jul 2023 11:56:51 +0100 Subject: [PATCH 037/242] recorder: colour pause image based on `g.theme` --- apps/recorder/clkinfo.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js index a82fd22f9..1ddd47067 100644 --- a/apps/recorder/clkinfo.js +++ b/apps/recorder/clkinfo.js @@ -2,9 +2,12 @@ const recimg = () => require("heatshrink").decompress(atob("jEYxH+AHHCAAgVQ4fDCwYFCCpotFDQgZJCxYYLCxgYCOJgALFygwHLpphJIyJIFC9O72oXU3m02h3UC4O7U6m7FwhIQIwwwPCxJhMCwSNEDBm83hbBCxQZEDQQUCIhIZIAAO1UAwAzA=")); - // TODO: deal with dark background - draw image instead? - const pauseimg = () => - require("heatshrink").decompress(atob("jEYxH+AH4Am64ABAxQWLCIYGGC6AHEF9QX/C/4X/C64HEF8YRDAxQA/AEQA=")); + const pauseimg = () => ({ + palette: new Uint16Array([0, g.theme.fg]), + buffer: require("heatshrink").decompress(atob("jEYxH+AH4Am64ABAxQWLCIYGGC6AHEF9QX/C/4X/C64HEF8YRDAxQA/AEQA=")), + width: 16, + height: 20, + }); return { name: "Recorder", From 65cd0a74183b5bd6e3169b2c8265f74cbed4de4c Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 14:00:47 +0200 Subject: [PATCH 038/242] apploader - Additional option to allow updates regardless of version --- android.html | 4 ++++ index.html | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/android.html b/android.html index 48b41e72b..9f8efc23c 100644 --- a/android.html +++ b/android.html @@ -172,6 +172,10 @@ Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this will break many apps) +
diff --git a/index.html b/index.html index 30f660717..4781afa09 100644 --- a/index.html +++ b/index.html @@ -175,6 +175,10 @@ Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this will break many apps) +
From 7a7bcd3e0994d46c44f565813f098960f416761b Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 14:24:38 +0200 Subject: [PATCH 039/242] apploader - Adds button for screenshots --- android.html | 9 +++++---- index.html | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/android.html b/android.html index 48b41e72b..2211d0abd 100644 --- a/android.html +++ b/android.html @@ -135,16 +135,17 @@

Utilities

+ + + +

+

- - - -

Settings

diff --git a/index.html b/index.html index 30f660717..ae587a453 100644 --- a/index.html +++ b/index.html @@ -135,15 +135,17 @@

Utilities

+ + + +

+

- - -

Settings

From 2d5f9a5788a955a501bac942a1b6d556cd63bd8a Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 11:53:31 +0200 Subject: [PATCH 040/242] kineticscroll - No check needed, remove method must always exist at this point --- apps/kineticscroll/boot.js | 2 +- apps/kineticscroll/boot.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/kineticscroll/boot.js b/apps/kineticscroll/boot.js index 1da4da51c..8c44c02ec 100644 --- a/apps/kineticscroll/boot.js +++ b/apps/kineticscroll/boot.js @@ -131,7 +131,7 @@ if (scheduledDraw) clearTimeout(scheduledDraw); clearInterval(scheduledBrake); - if (options.remove) options.remove(); + options.remove(); } Bangle.setUI(uiOpts); diff --git a/apps/kineticscroll/boot.min.js b/apps/kineticscroll/boot.min.js index 2db3098a9..4b549a4f7 100644 --- a/apps/kineticscroll/boot.min.js +++ b/apps/kineticscroll/boot.min.js @@ -1,4 +1,4 @@ (function(){E.showScroller=function(c){function m(a){return a*c.h+b.y-h}function n(a){return Math.floor((a+h-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,k=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,l=0|c.scrollMin,r=c.h*c.c-b.h;r{d.yl||0<=a)&&a{var a=e;f.scroll-a>r&&(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=n(a);for(a=m(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)}},v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0a.dy)e*=-1,k=5*e;0{p&&clearTimeout(p);clearInterval(w);c.remove&&c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,l,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=n(b.y);let d=Math.min(n(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:m(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=m(a);g.reset().setClipRect(b.x, -Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.touchHandler==t},h=f.scroll&-2;f.draw();g.flip();return f}})() \ No newline at end of file +Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.touchHandler==t},h=f.scroll&-2;f.draw();g.flip();return f}})() From e614bdfb51cb35dfd59658f73445de02410ea5e1 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 23 Jul 2023 17:41:48 +0200 Subject: [PATCH 041/242] kineticscroll - Update to latest E.showScroller implementation --- apps/kineticscroll/boot.js | 22 +++++++++++++++++----- apps/kineticscroll/boot.min.js | 8 ++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/kineticscroll/boot.js b/apps/kineticscroll/boot.js index 8c44c02ec..1f1b7923a 100644 --- a/apps/kineticscroll/boot.js +++ b/apps/kineticscroll/boot.js @@ -31,7 +31,7 @@ let menuScrollMax = options.h*options.c - R.h; if (menuScrollMax{ + const touchHandler = (_,e)=>{ if (e.y=0) && i { + + const uiDraw = () => { + g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2); + var a = YtoIdx(R.y); + var b = Math.min(YtoIdx(R.y2),options.c-1); + for (var i=a;i<=b;i++) + options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + } + + const draw = () => { let dy = velocity; if (s.scroll - dy > menuScrollMax){ dy = s.scroll - menuScrollMax; @@ -124,7 +134,8 @@ mode : "custom", back : options.back, drag : dragHandler, - touch : touchHandler + touch : touchHandler, + redraw : uiDraw } if (options.remove) uiOpts.remove = () => { @@ -136,6 +147,7 @@ Bangle.setUI(uiOpts); + function idxToY(i) { return i*options.h + R.y - rScroll; @@ -158,7 +170,7 @@ g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2)); options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); - }, isActive : () => Bangle.touchHandler == touchHandler + }, isActive : () => Bangle.uiRedraw == uiDraw }; let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither) diff --git a/apps/kineticscroll/boot.min.js b/apps/kineticscroll/boot.min.js index 4b549a4f7..42f0afa67 100644 --- a/apps/kineticscroll/boot.min.js +++ b/apps/kineticscroll/boot.min.js @@ -1,4 +1,4 @@ -(function(){E.showScroller=function(c){function m(a){return a*c.h+b.y-h}function n(a){return Math.floor((a+h-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,k=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,l=0|c.scrollMin,r=c.h*c.c-b.h;r{d.yl||0<=a)&&a{var a=e;f.scroll-a>r&&(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=n(a);for(a=m(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)}},v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0a.dy)e*=-1,k=5*e;0{p&&clearTimeout(p);clearInterval(w);c.remove&&c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,l,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=n(b.y);let d=Math.min(n(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:m(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=m(a);g.reset().setClipRect(b.x, -Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.touchHandler==t},h=f.scroll&-2;f.draw();g.flip();return f}})() +(function(){E.showScroller=function(c){function k(a){return a*c.h+b.y-l}function h(a){return Math.floor((a+l-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,m=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,n=0|c.scrollMin,r=c.h*c.c-b.h;r{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);for(var a=h(b.y),d=Math.min(h(b.y2),c.c-1);a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},u=()=>{var a=e;f.scroll-a>r&& +(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=h(a);for(a=k(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()- +1)}};let v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0a.dy)e*=-1,m=5*e;0{if(!(d.yn||0<=a)&&a{p&&clearTimeout(p);clearInterval(w);c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll, +n,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=h(b.y);let d=Math.min(h(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=k(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.uiRedraw==t},l=f.scroll&-2;f.draw();g.flip();return f}})() \ No newline at end of file From 3edc3604e6d242998b6752793ed1903c4f143bb7 Mon Sep 17 00:00:00 2001 From: Joppy Furr Date: Mon, 24 Jul 2023 15:59:21 +1200 Subject: [PATCH 042/242] snepwatch: Snepwatch release 1.00 --- apps/snepwatch/ChangeLog | 1 + apps/snepwatch/README.md | 17 ++ apps/snepwatch/app.js | 242 ++++++++++++++++++++++++++++ apps/snepwatch/app.png | Bin 0 -> 4404 bytes apps/snepwatch/metadata.json | 18 +++ apps/snepwatch/screenshot-dark.png | Bin 0 -> 3057 bytes apps/snepwatch/screenshot-light.png | Bin 0 -> 2749 bytes apps/snepwatch/settings.js | 81 ++++++++++ apps/snepwatch/snepwatch.img | Bin 0 -> 296 bytes 9 files changed, 359 insertions(+) create mode 100644 apps/snepwatch/ChangeLog create mode 100644 apps/snepwatch/README.md create mode 100644 apps/snepwatch/app.js create mode 100644 apps/snepwatch/app.png create mode 100644 apps/snepwatch/metadata.json create mode 100644 apps/snepwatch/screenshot-dark.png create mode 100644 apps/snepwatch/screenshot-light.png create mode 100644 apps/snepwatch/settings.js create mode 100644 apps/snepwatch/snepwatch.img diff --git a/apps/snepwatch/ChangeLog b/apps/snepwatch/ChangeLog new file mode 100644 index 000000000..fa064b2c0 --- /dev/null +++ b/apps/snepwatch/ChangeLog @@ -0,0 +1 @@ +1.00: Initial release diff --git a/apps/snepwatch/README.md b/apps/snepwatch/README.md new file mode 100644 index 000000000..7bf5ec5cd --- /dev/null +++ b/apps/snepwatch/README.md @@ -0,0 +1,17 @@ +# Snepwatch +![](screenshot-dark.png) +![](screenshot-light.png) + +Features: + * This watch face uses the Terminus font. + * Background, digit-outline, and digit-fill colours are all customizable from the settings menu + * Text can be configured to either black or white + * Today's step count is always shown at the bottom + * Heart-rate is shown when we have a valid reading within the last ten seconds + * The heart rate monitor can be activated by another app running in the background (eg, Recorder) + * Or, the heart rate monitor is activated when you unlock the screen (eg, by pressing the button) + * Sometimes a single unlock-period is not long enough to achieve a usable reading, so a second press can be used + * The heart rate monitor will be returned to deactivated when the screen remains locked for 15 seconds. + * Hidden widget-bar. Swipe down to see the widgets. + * "Sneptember" instead of "September", because snow leopards are excellent :3 + * Fast Loading diff --git a/apps/snepwatch/app.js b/apps/snepwatch/app.js new file mode 100644 index 000000000..833285644 --- /dev/null +++ b/apps/snepwatch/app.js @@ -0,0 +1,242 @@ +/* + * SnepWatch - Bangle JS 2 Port + * JoppyFurr 2023 + */ + +{ + Graphics.prototype.setFontTerminus_14 = function (scale) { + g.setFontCustom (atob ("AB/oJhGIZBf4AAAAAgUB/8AQBAAAGDgWCYRiFwQAABhoBiGIYhd4AAABgKBIIhCP/AAAPiiGIYhiGHgAAA/khiGIYhB4AAAgCAIHhiYOAAAAHeiGIYhiF3gAAB4IRhGEYSfwAEAoOjEyMXBQCAA="), 48, 8, 10); + }; + + Graphics.prototype.setFontTerminus_18 = function (scale) { + /* TODO: Strip out unused characters - Eg, encode zero-width */ + g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("AH4A/AH4AnoEHBpcgB4MAwEBgECgEEgEIItf/4UEokQxFBiMCkkEn/wCY0CgMIgU//hHCiAQGh0GkEUoFIwMRgkiiFD4GACQkwmGAiMIBwNEkA7Bh98EoqPBiUAog6BgkQiEgv/4CIvjhFBiGCkMEoUIwkRg/ACQv8IoIODHYVAj43FuC4EgfEjEI4EeOA334RZEHYc9SosD8EIkMQoUgwlBhGCkH/QQoA/AH6VD/+IkDHBkGAoMAwUA//gEq8/+GAiMAkUAokAxEBhkMCIkB//iBwkQgUQhEfG7Ef/0hgFCgGEgMIgUAgip/AAM4gEIgEQQwVH/4HBAFEH/+EgEGcwIDCmEAwDtB4CVERgIBGDoOAcB8P/jgHhEEbYMgiFAkGAnylrnkwwkRhEiiFEkGIoMMvhKFgD+Dh//XYMggFAJVd//AxBgOAhkAjEAAYMEn6XFAH4A/AE8PgMhgVCgmEhMIiUQj/gCIkf/0EgkIhEQiEgkFAoF/EgsH+AOFwGBgMBggSFj/ABwkCgQsBfwQAEv8BiMCkUEokIxERiEeoBvXJQUSkEkoFIwGRgMj/+ACIcB//gFoOAgMAgUALQMHJQwACLIM3/kAhC6bO4I0DhUAkUAwQsBCpBEBx//CwQAagbgBLAUQgEggB3Bv6CEcBh1BAAk//jgIDoJKZQQJMBJQpJFACodCh/+JQbRHUQLwFQ4U/8BKFH4PAgFggEwgOAhkACIoAmJQUggVAgmAhMAiU//AiVO4MEiEEoEFgFEoFBAYMAqCOCgE+gC6BAIkH/Ef8E/wAOBAAI="))), 32, 10, 15); + }; + + Graphics.prototype.setFontDigits = function (scale) { + g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("AEE//4AC4EDAof8Bid8EQMB+ED8/AAwMfgE+BisD8AMEnwIBBisHBAQMCCoYMUh+ABgn4AoIMUTbIMGAH4AhdoKOC8DtBAwTYBBiQHBBgbtCBi1/VAf/84FE/wMSFYZNDAAY4EBmoA7n7PCg/+gf/BQV/8AMSvgLCh+D8/AAwX4nwMWj6OBBgUB+EDBiysBBgasCBiwIDAYMHCoQMTRwP4dIc//+ABiz/8AAbpD//AewcP/gMTdIaOEAAT2EBiLCdBn7pDeQKOC/+D/6bDBiIA/YwbyCAATyDBikH4IMEvkfBi0PwAME/EBBiwDBBgYDCBi0+BAZrB8AMXv7tBAAXnAon+BiQrBAH0HIYP8AgPAJoXAbAIMTvgDCSYPnCASTCBn4M2gE/RQIQDS4QMSf/gAEgJLBeQcfAoRNBBiUD8BwD8E+RwgMSdP4MhUgTpFewYMVAHprCAAZrCBnf//wMDh//WoQMTn/waYn/GYgMRh7GDBgLTDBid/FQQMC/4qBBioA/QoX/wf/OAKSB/AIBOAIMSRwKxDZ4SxCnwM/BmibZBgwA/JoREDJoJRBBiqFDRwkD8CbDBn4MWeQIMEvj6DBh6OCAAOARwQAB/CbDBiAA/AH4ACh/8gEH/wGB/6xBv7YBBiYA/AH4A=="))), 48, 40, 52); + }; + + Graphics.prototype.setFontOutline = function (scale) { + g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("ADMD/4AC/kAoALDkEEAocCBhkH4AGDn2DEgP+h/8jEgC4WAoOCC4UIggMJnwMEgkfgAMDoIIBBgcQBhV8BgkInwMECoYMCEQQMI/AMEiF8BgcBOwQMDQYQMHn/wv59B4IdCVoXgVDEAn7JDVogA/ACEB+AECXwLtBRwUQfQIMCbAIMLSwIMEoImDoL6CBgg4DBg0X4KbD4IkCAAUYKQoMSi4lDEwILEHAhnDBmKoBAGUD/4ECv/goDPChEBgkASAWAiAMLi/ABgX4gODEwUQvkYkCzCglBwQyCiFEBhMB+AMEkH4BgcChEEBgmABhMD8AMEoPwBgasCBggIBBhEHfQIMDwfgBgcICoQMCEQYMGn//Bgd8EwPAF4VAgABBBgKbBAIIMPEwKvCnz/vAAcDbgUP/hLBdIMBW4L2DiECBhh4BBgc+wYmEbgabEAATpEBgibDWpoM/BhrcBPoP/4N8M4cD8D0BogZBTYUEBAKoCBhYmEEQIA1n/waYjyCAAT/CBh59BBgcInwMEwUgBgkiBhX4BgkQvgMDgMEgQMEoAMJgPwBgkg/AMDAYQMEwAMJa4IMEoKFBBgUEiAVDBgIiDBg0H4LaB8H//yQEgEYAokBBiUXEoIAC/z/ygF/G4PAcoKQDbAKdEbAIMSn2D//4BgN8jAQCUAQDBDoKgBBn4MqPoP/4IMBg6oBgfoCASXCDoVAS4IdDBhAdDZYYAzj44B//8c4QACKQIBBeQYMMnwMDn0Ej/4n4mBiFBTYkEiB9DBg18BgkYEwLp/BjWDUQPBvkAcofgCwL2DSQT2CAoQMMgYmBEQIAyLQIADNYQADPAQM0h//9AMCgIBBBgcQgC1CBhibBBglAEwi1BwAzDoEIcAgMEXIIMDiAmCBgI3BHgQMCaYYMGn/wBgYoBcAYoCOgYMBAIIMOg//+D/zAAZ4B/E///8SQMAogLBkCSBggIBOAIMLg54BvgMBn2DEwjPCXwVBAYK+DBn4MoPAJ9B//BQoKKCgfgVDEAEwgiBAGUDG4pEDAAJNBKIIMOPAkAnyFDTYkEiCbDAYIM/Bh18BgkInwMEwT6DBgMgBgzcB/kf/EgvitDAgKbBTIabCUwYMMEwIAB/D/wAH4AG//AgF/8EAiECgEIgMAAIK+BwDYBBiQmGAH4A5"))), 48, 40, 52); + }; + + var snepwatch_tick_timeout; + var snepwatch_hrm_timeout; + var snepwatch_hrm_show_timeout; + var heart_rate = 0; + var heart_rate_time = 0; + + /* Load settings */ + var settings = Object.assign ({ + /* Default Values */ + outline_r: 1, + outline_g: 0, + outline_b: 0, + fill_r: 0.5, + fill_g: 0, + fill_b: 0, + bg_r: 0, + bg_g: 0, + bg_b: 0, + text: 1, + }, require ('Storage').readJSON ("snepwatch.json", true) || {}); + + /* + * Tick once per minute. + */ + let snepwatch_tick_queue = function () { + if (snepwatch_tick_timeout) { + clearTimeout (snepwatch_tick_timeout); + } + + snepwatch_tick_timeout = setTimeout (function () { + snepwatch_tick_timeout = undefined; + snepwatch_tick (); + }, 60000 - (Date.now () % 60000)); + }; + + + /* + * Draw the heart rate sensor reading. + * The reading is only shown if it is from within the last 10 seconds. + * Assumes the Terminus_18 font is already selected. + */ + let draw_heart_rate = function () { + let heart_rate_string = "--"; + let hrm_show = false; + + /* As we are about to show the heart rate, + * previously set timers are considered invalid */ + if (snepwatch_hrm_show_timeout) { + clearTimeout (snepwatch_hrm_show_timeout); + } + + /* Only show the heart rate if the measurement is recent */ + if (heart_rate_time > Date.now () - 10000) { + hrm_show = true; + heart_rate_string = "" + heart_rate; + } + + g.clearRect (17, 160, 88, 175); + g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text); + g.drawString (heart_rate_string, 17, 160); + + /* If the heart rate was shown, check back when the reading + * would become stale so that it can be cleared. */ + if (hrm_show) { + snepwatch_hrm_show_timeout = setTimeout (function () { + snepwatch_hrm_show_timeout = undefined; + draw_heart_rate (); + }, heart_rate_time + 10000 - Date.now ()); + } + }; + + + /* + * Called once per minute. + * + * Updates the time, date, and battery level. + */ + let snepwatch_tick = function () { + /* Data */ + let days = [ "Sun ", "Mon ", "Tue ", "Wed ", "Thu ", "Fri ", "Sat " ]; + let months = [ " Jan", " Feb", " Mar", " Apr", " May", " June", " July", " Aug", " Snep", " Oct", " Nov", " Dec"]; + let date = new Date (); + let charge_level = E.getBattery (); + + /* Clear */ + g.reset (); + g.setBgColor (settings.bg_r, settings.bg_g, settings.bg_b); + g.clear (); + + /* Battery level - Note, '%' is encoded as ':' */ + let battery_text = ((charge_level < 10) ? "0" : "") + charge_level + ":"; + if (charge_level <= 16) { + g.setColor (1, 0, 0); + } else { + g.setColor (0, 0 + settings.text, 1); + } + g.setFont ("Terminus_14"); + g.drawString (battery_text, 2, 2); + + /* Date */ + let day = days [ date.getDay () ]; + let dd = date.getDate (); + dd = ((dd < 10) ? "0" : "") + dd; + let month = months [ date.getMonth () ]; + + let date_text = day + dd + month; + if (date_text.length < 11) { + date_text = " " + date_text; + } + g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text); + g.setFont ("Terminus_18"); + g.drawString (date_text, 65, 2); + + /* Time */ + let hours = date.getHours (); + let minutes = date.getMinutes (); + let time_hh = ((hours < 10) ? "0" : "") + hours; + let time_mm = ((minutes < 10) ? "0" : "") + minutes; + g.setColor (settings.fill_r, settings.fill_g, settings.fill_b); + g.setFont ("Digits"); + g.drawString (time_hh, -2, 60); + g.drawString (":", 71, 55); + g.drawString (time_mm, 98, 60); + + g.setColor (settings.outline_r, settings.outline_g, settings.outline_b); + g.setFont ("Outline"); + g.drawString (time_hh, -2, 60); + g.drawString (":", 71, 55); + g.drawString (time_mm, 98, 60); + + + /* Steps so far for the day */ + let steps = Bangle.getHealthStatus ('day').steps; + let steps_string = "" + steps; + if (steps >= 1000) { + steps_string = steps_string.slice (0, -3) + "," + steps_string.slice (-3); + } + + g.setFont("Terminus_18"); + /* With dark text, use blue for the step symbol. + With light text, use green for the step symbol. */ + g.setColor (0, 0 + settings.text, 1 - settings.text); + g.drawString ("{", 2, 144); /* Arrows */ + g.setColor (1, 0, 0); + g.drawString ("|", 2, 160); /* Heart */ + g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text); + g.drawString (steps_string, 17, 144); + draw_heart_rate (); + + /* Queue up the next tick */ + snepwatch_tick_queue (); + }; + + + /* Callback for when the backlight state changes */ + let display_cb = lock => { + if (lock) { + /* The backlight may not run for long enough to get a good reading. + Wait 15 seconds with the backlight off before disabling the sensor. */ + snepwatch_hrm_timeout = setTimeout (function () { + snepwatch_hrm_timeout = undefined; + Bangle.setHRMPower (false, "snepwatch"); + }, 15000); + } else { + if (snepwatch_hrm_timeout) { + clearTimeout (snepwatch_hrm_timeout); + snepwatch_hrm_timeout = undefined; + } + Bangle.setHRMPower (true, "snepwatch"); + } + }; + + /* Callback for the heart rate monitor */ + let heart_rate_cb = hrm => { + if (hrm.bpm > 0 && hrm.confidence > 50) { + heart_rate = hrm.bpm; + heart_rate_time = Date.now (); + } + + g.setFont("Terminus_18"); + draw_heart_rate (); + }; + + let previous_theme = g.theme; + g.setTheme ( { bg:"#000", fg:"#fff", dark:true } ); + + /* Initial call, will tick once per minute */ + snepwatch_tick (); + Bangle.on ('lock', display_cb); + Bangle.on ('HRM', heart_rate_cb); + + /* Use a swipe to show the widgets */ + Bangle.loadWidgets (); + require ("widget_utils").swipeOn (); + + /* Allow for Fast Loading */ + Bangle.setUI ( { mode:"clock", remove:function () { + if (snepwatch_tick_timeout) { + if (snepwatch_tick_timeout) { + clearTimeout (snepwatch_tick_timeout); + } + if (snepwatch_hrm_timeout) { + clearTimeout (snepwatch_hrm_timeout); + } + if (snepwatch_hrm_show_timeout) { + clearTimeout (snepwatch_hrm_show_timeout); + } + Bangle.removeListener('lcdPower', display_cb); + Bangle.removeListener('HRM', heart_rate_cb); + Bangle.setHRMPower (false, "snepwatch"); + delete Graphics.prototype.setFontTerminus_14; + delete Graphics.prototype.setFontTerminus_18; + delete Graphics.prototype.setFontDigits; + delete Graphics.prototype.setFontOutline; + g.setTheme (previous_theme); + require ("widget_utils").show(); + } + } } ); +} diff --git a/apps/snepwatch/app.png b/apps/snepwatch/app.png new file mode 100644 index 0000000000000000000000000000000000000000..50eb5c388f4f2d4c91d8bb5bb424753b78c1687a GIT binary patch literal 4404 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 literal 0 HcmV?d00001 diff --git a/apps/snepwatch/screenshot-light.png b/apps/snepwatch/screenshot-light.png new file mode 100644 index 0000000000000000000000000000000000000000..074747423b9d19b3e056620007098e299d32c5dd GIT binary patch literal 2749 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 literal 0 HcmV?d00001 diff --git a/apps/snepwatch/settings.js b/apps/snepwatch/settings.js new file mode 100644 index 000000000..57ae94d52 --- /dev/null +++ b/apps/snepwatch/settings.js @@ -0,0 +1,81 @@ +( + function(back) { + var FILE = "snepwatch.json"; + + /* Load settings */ + var settings = Object.assign ({ + /* Default Values */ + outline_r: 1, + outline_g: 0, + outline_b: 0, + fill_r: 0.5, + fill_g: 0, + fill_b: 0, + bg_r: 0, + bg_g: 0, + bg_b: 0, + text: 1 + , + }, require ('Storage').readJSON (FILE, true) || {}); + + function write_settings () { + require ('Storage').writeJSON (FILE, settings); + } + + /* Show the menu */ + var main_menu = { + "" : { "title": "Snepwatch", + back : function() { back (); }}, + "Outline Colour": function () { E.showMenu (outline_menu); }, + "Fill Colour": function () { E.showMenu (fill_menu); }, + "Background Colour": function () { E.showMenu (background_menu); }, + "Text": { value: (settings.text == 1), + format: v => v ? "Light" : "Dark", + onchange: v => { settings.text = v; write_settings (); }}, + }; + + var outline_menu = { + "": { title : "Outline Colour", + back : function() { E.showMenu (main_menu); } }, + "Red": { value: settings.outline_r, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.outline_r = v; write_settings (); }}, + "Green": { value: settings.outline_g, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.outline_g = v; write_settings (); }}, + "Blue": { value: settings.outline_b, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.outline_b = v; write_settings (); }}, + }; + + var fill_menu = { + "" : { title : "Fill Colour", + back : function() { E.showMenu (main_menu); } }, + "Red": { value: settings.fill_r, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.fill_r = v; write_settings (); }}, + "Green": { value: settings.fill_g, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.fill_g = v; write_settings (); }}, + "Blue": { value: settings.fill_b, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.fill_b = v; write_settings (); }}, + }; + + var background_menu = { + "" : { title : "Background Colour", + back : function() { E.showMenu (main_menu); } }, + "Red": { value: settings.bg_r, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.bg_r = v; write_settings (); }}, + "Green": { value: settings.bg_g, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.bg_g = v; write_settings (); }}, + "Blue": { value: settings.bg_b, + min: 0, max: 1, step: 0.5, wrap: true, + onchange: v => { settings.bg_b = v; write_settings (); }}, + }; + + E.showMenu (main_menu); + } +) diff --git a/apps/snepwatch/snepwatch.img b/apps/snepwatch/snepwatch.img new file mode 100644 index 0000000000000000000000000000000000000000..ae0527cbbcabbeea13f1cc9f096f20c538be38f4 GIT binary patch literal 296 zcmb8ou?>VU3jomr=F`1IT^_R`um1h=>S=P4GN;C3dt e{ISfdIl4)=M*R!4*Cds}6Zl_}`jV<$x9SI;lOj6+ literal 0 HcmV?d00001 From 88a4f98ce8f13d79eb800a8eacbdb8b4a9a3bf77 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 24 Jul 2023 09:41:42 +0200 Subject: [PATCH 043/242] gipy: trying to fix the segfaults --- apps/gipy/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 9e0dbce24..071ef8283 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -755,7 +755,7 @@ class Status { wakeOnTouch: true, powerSave: false, }); - Bangle.setPollInterval(4000); // disable accelerometer as much as we can + Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000) } } From 4777d98c1046b70a21c380148898e61ab01055d5 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Mon, 24 Jul 2023 11:33:10 +0200 Subject: [PATCH 044/242] openstmap: Bugfix: Enable Compass if needed --- apps/openstmap/ChangeLog | 1 + apps/openstmap/app.js | 2 ++ apps/openstmap/metadata.json | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index b30a871f9..e684d4e98 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -27,3 +27,4 @@ 0.21: Draw a current position marker (Bangle.js 2 only) Enable/Disable previous position marker in new setting "Draw cont. position" 0.22: Replace position marker with direction arrow +0.23: Bugfix: Enable Compass if needed diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 8ad3c669d..e1a9ea734 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -133,6 +133,7 @@ Bangle.on('GPS',function(f) { } }); Bangle.setGPSPower(1, "app"); +Bangle.setCompassPower(settings.dirSrc === 2, "openstmap"); if (HASWIDGETS) { Bangle.loadWidgets(); @@ -196,6 +197,7 @@ function showMenu() { format: v => [/*LANG*/"None", /*LANG*/"GPS", /*LANG*/"Compass"][v], onchange: v => { settings.dirSrc = v; + Bangle.setCompassPower(settings.dirSrc === 2, "openstmap"); writeSettings(); } }; diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 2d217cee6..9a7527347 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.22", + "version": "0.23", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "readme": "README.md", "icon": "app.png", From 1a932b0618b32865e625889de75e7bc1d4eb9b59 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Mon, 24 Jul 2023 18:24:14 +0200 Subject: [PATCH 045/242] Calendar: Edit holidays on device in settings app: Only refactoring, no changed functionality --- apps/calendar/ChangeLog | 1 + apps/calendar/calendar.js | 48 ++++++------ apps/calendar/metadata.json | 2 +- apps/calendar/settings.js | 151 +++++++++++++++++++++++++++++++++--- 4 files changed, 167 insertions(+), 35 deletions(-) diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index c7902e263..12776867f 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -14,3 +14,4 @@ 0.13: Switch to swipe left/right for month and up/down for year selection Display events for current month on touch 0.14: Add support for holidays +0.15: Edit holidays on device in settings diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index d7c43eb1f..11baf6855 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -75,11 +75,31 @@ function getDowLbls(locale) { } function sameDay(d1, d2) { + "compiled"; return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } +function drawEvent(ev, curDay, x1, y1, x2, y2) { + switch(ev.type) { + 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 + const sliceHeight = height/eventsPerDay; + 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; + case "o": // other + g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1); + break; + } +} + function drawCalendar(date) { g.setBgColor(bgColor); g.clearRect(0, 0, maxX, maxY); @@ -118,7 +138,6 @@ function drawCalendar(date) { true ); - g.setFont("6x8", fontSize); let dowLbls = getDowLbls(require('locale').name); dowLbls.forEach((lbl, i) => { g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); @@ -172,6 +191,7 @@ function drawCalendar(date) { const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth); eventsThisMonth.sort((a,b) => a.date - b.date); let i = 0; + g.setFont("8x12", fontSize); for (y = 0; y < rowN - 1; y++) { for (x = 0; x < colN; x++) { i++; @@ -188,22 +208,7 @@ function drawCalendar(date) { // Display events for this day eventsThisMonth.forEach((ev, idx) => { if (sameDay(ev.date, curDay)) { - switch(ev.type) { - case "e": // alarm/event - const hour = ev.date.getHours() + 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 - const sliceHeight = height/eventsPerDay; - 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; - case "o": // other - g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1); - break; - } + drawEvent(ev, curDay, x1, y1, x2, y2); eventsThisMonth.splice(idx, 1); // this event is no longer needed } @@ -221,17 +226,15 @@ function drawCalendar(date) { ); } - require("Font8x12").add(Graphics); - g.setFont("8x12", fontSize); g.setColor(day < 50 ? fgOtherMonth : fgSameMonth); g.drawString( (day > 50 ? day - 50 : day).toString(), x * colW + colW / 2, headerH + rowH + y * rowH + rowH / 2 ); - } - } -} + } // end for (x = 0; x < colN; x++) + } // end for (y = 0; y < rowN - 1; y++) +} // end function drawCalendar function setUI() { Bangle.setUI({ @@ -279,6 +282,7 @@ function setUI() { }); } +require("Font8x12").add(Graphics); drawCalendar(date); setUI(); // No space for widgets! diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index 44a68d879..bd35c8879 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.14", + "version": "0.15", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js index 54ed50a64..40eca9f68 100644 --- a/apps/calendar/settings.js +++ b/apps/calendar/settings.js @@ -1,5 +1,6 @@ (function (back) { var FILE = "calendar.json"; + const HOLIDAY_FILE = "calendar.days.json"; var settings = require('Storage').readJSON(FILE, true) || {}; if (settings.ndColors === undefined) if (process.env.HWVERSION == 2) { @@ -7,21 +8,147 @@ } else { settings.ndColors = false; } + 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); } - E.showMenu({ - "": { "title": "Calendar" }, - "< Back": () => back(), - 'B2 Colors': { - value: settings.ndColors, - onchange: v => { - settings.ndColors = v; - writeSettings(); - } - }, - }); -}) + function writeHolidays() { + holidays.sort((a,b) => new Date(a.date) - new Date(b.date)); + require('Storage').writeJSON(HOLIDAY_FILE, holidays); + } + function formatDate(d) { + return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0'); + } + + const editdate = (i) => { + const holiday = holidays[i]; + const date = new Date(holiday.date); + const dateStr = require("locale").date(date, 1); + const menu = { + "": { "title" : holiday.name}, + "< Back": () => { + writeHolidays(); + editdates(); + }, + /*LANG*/"Day": { + value: date ? date.getDate() : null, + min: 1, + max: 31, + wrap: true, + onchange: v => { + date.setDate(v); + holiday.date = formatDate(date); + } + }, + /*LANG*/"Month": { + value: date ? date.getMonth() + 1 : null, + format: v => require("date_utils").month(v), + onchange: v => { + date.setMonth((v+11)%12); + holiday.date = formatDate(date); + } + }, + /*LANG*/"Year": { + value: date ? date.getFullYear() : null, + min: 1900, + max: 2100, + onchange: v => { + date.setFullYear(v); + holiday.date = formatDate(date); + } + }, + /*LANG*/"Name": () => { + require("textinput").input({text:holiday.name}).then(result => { + holiday.name = result; + editdate(i); + }); + }, + /*LANG*/"Type": { + value: function() { + switch(holiday.type) { + case 'h': return 0; + case 'o': return 1; + } + return 0; + }(), + min: 0, max: 1, + format: v => [/*LANG*/"Holiday", /*LANG*/"Other"][v], + onchange: v => { + holiday.type = function() { + switch(v) { + case 0: return 'h'; + case 1: return 'o'; + } + }(); + } + }, + /*LANG*/"Repeat": { + value: !!holiday.repeat, + format: v => v ? /*LANG*/"Yearly" : /*LANG*/"Never", + onchange: v => { + holiday.repeat = v ? 'y' : undefined; + } + }, + /*LANG*/"Delete": () => E.showPrompt(/*LANG*/"Delete" + " " + menu[""].title + "?").then(function(v) { + if (v) { + holidays.splice(i, 1); + writeHolidays(); + editdates(); + } else { + editday(i); + } + } + ), + }; + try { + require("textinput"); + } catch(e) { + // textinput not installed + delete menu[/*LANG*/"Name"]; + } + + E.showMenu(menu); + }; + + const editdates = () => { + const menu = holidays.map((holiday,i) => { + const date = new Date(holiday.date); + const dateStr = require("locale").date(date, 1); + return { + title: dateStr + ' ' + holiday.name, + onchange: v => setTimeout(() => editdate(i), 10), + }; + }); + + menu[''] = { 'title': 'Holidays' }; + menu['< Back'] = ()=>settingsmenu(); + E.showMenu(menu); + }; + + const settingsmenu = () => { + E.showMenu({ + "": { "title": "Calendar" }, + "< Back": () => back(), + 'B2 Colors': { + value: settings.ndColors, + onchange: v => { + settings.ndColors = v; + writeSettings(); + } + }, + /*LANG*/"Edit Holidays": () => editdates(), + /*LANG*/"Add Holiday": () => { + holidays.push({ + "date":formatDate(new Date()), + "name":/*LANG*/"New", + "type":'h', + }); + editdate(holidays.length-1); + }, + }); + }; + settingsmenu(); +}) From 34e667b615ecfedcba33f4306e4f90b9d7ae9b70 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Mon, 24 Jul 2023 19:05:24 +0200 Subject: [PATCH 046/242] calendar: adjust strings in front of functions --- apps/calendar/calendar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 11baf6855..0ae852d83 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -75,13 +75,14 @@ function getDowLbls(locale) { } function sameDay(d1, d2) { - "compiled"; + "jit"; return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } function drawEvent(ev, curDay, x1, y1, x2, y2) { + "ram"; switch(ev.type) { case "e": // alarm/event const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0; From b08d35b2d0cd1da339d954a0cdd0df68d6600535 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Mon, 24 Jul 2023 20:06:38 +0200 Subject: [PATCH 047/242] apploader - Update core --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 92769acd6..e9eb97967 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 92769acd60bc31548ff7c635128d4e7ef02b7325 +Subproject commit e9eb97967d948eea85662ee9ea8eba0b44a363e0 From 74bf636da3f6e69d46d822a1d0bf1e3db6ec4e64 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Mon, 24 Jul 2023 20:23:20 +0200 Subject: [PATCH 048/242] apploader - Re-add remote IDE button --- android.html | 1 + 1 file changed, 1 insertion(+) diff --git a/android.html b/android.html index 2211d0abd..2f90c2294 100644 --- a/android.html +++ b/android.html @@ -146,6 +146,7 @@

+

Settings

From 907cea89d8293fd3bd227d230f5767dfa3feae16 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 21 Jul 2023 21:24:08 +0200 Subject: [PATCH 049/242] fastload - Formatting --- apps/fastload/settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js index 66c990df1..1265d5e72 100644 --- a/apps/fastload/settings.js +++ b/apps/fastload/settings.js @@ -52,12 +52,12 @@ } }; - mainmenu['Hide "Fastloading..."'] = { - value: !!settings.hideLoading, - onchange: v => { - writeSettings("hideLoading",v); - } - }; + mainmenu['Hide "Fastloading..."'] = { + value: !!settings.hideLoading, + onchange: v => { + writeSettings("hideLoading",v); + } + }; return mainmenu; } From 4e6827735e461d7f56927cce14b32e332cbe30c4 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 24 Jul 2023 20:39:39 +0100 Subject: [PATCH 050/242] recorder: merge with Bangle clkinfo --- apps/recorder/clkinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js index 1ddd47067..5e09de6f8 100644 --- a/apps/recorder/clkinfo.js +++ b/apps/recorder/clkinfo.js @@ -10,7 +10,7 @@ }); return { - name: "Recorder", + name: "Bangle", items: [ { name: "Toggle", From cfa871e204c54ef9c2ba3be46cef32f9cd60e9e5 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 24 Jul 2023 20:39:46 +0100 Subject: [PATCH 051/242] recorder: handle missing widget --- apps/recorder/clkinfo.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js index 5e09de6f8..a65320a6e 100644 --- a/apps/recorder/clkinfo.js +++ b/apps/recorder/clkinfo.js @@ -11,21 +11,17 @@ return { name: "Bangle", - items: [ + items: WIDGETS["recorder"] ? [ { name: "Toggle", - get: () => { - const w = typeof WIDGETS !== "undefined" && WIDGETS["recorder"]; - - return w && w.isRecording() ? { - text: "Recording", - short: "rec", - img: recimg(), - } : { - text: "Paused", - short: "paused", - img: pauseimg(), - } + get: () => WIDGETS["recorder"].isRecording() ? { + text: "Recording", + short: "rec", + img: recimg(), + } : { + text: "Paused", + short: "paused", + img: pauseimg(), }, run: () => { const w = WIDGETS["recorder"]; @@ -35,6 +31,6 @@ show: () => {}, hide: () => {}, }, - ], + ] : [], }; }); From 9fa01034439532a20bc934b136a20a45211db555 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 24 Jul 2023 20:50:48 +0100 Subject: [PATCH 052/242] recorder: change record & pause icons --- apps/recorder/clkinfo.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js index a65320a6e..acb56dfe9 100644 --- a/apps/recorder/clkinfo.js +++ b/apps/recorder/clkinfo.js @@ -1,13 +1,9 @@ (function () { const recimg = () => - require("heatshrink").decompress(atob("jEYxH+AHHCAAgVQ4fDCwYFCCpotFDQgZJCxYYLCxgYCOJgALFygwHLpphJIyJIFC9O72oXU3m02h3UC4O7U6m7FwhIQIwwwPCxJhMCwSNEDBm83hbBCxQZEDQQUCIhIZIAAO1UAwAzA=")); + atob("GBiBAAAAABwAAD4MAH8eAH8OAH8AAD4QABx8AAD8AAH+AAE+AAM/AAN7wAN4wAB4AAB8AAD8AADOAAHGAAOHAAMDAAIBAAAAAAAAAA=="); - const pauseimg = () => ({ - palette: new Uint16Array([0, g.theme.fg]), - buffer: require("heatshrink").decompress(atob("jEYxH+AH4Am64ABAxQWLCIYGGC6AHEF9QX/C/4X/C64HEF8YRDAxQA/AEQA=")), - width: 16, - height: 20, - }); + const pauseimg = () => + atob("GBiBAAAAAAAAAAAAAAAAAAHDgAPnwAPjwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPjwAPnwAHDgAAAAAAAAAAAAAAAAA=="); return { name: "Bangle", From b63ff956bec655b3b01987e9c3e9d9f558fc9816 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 21 Jul 2023 19:28:32 +0200 Subject: [PATCH 053/242] fastload - Check setting.json and force real load on change --- apps/fastload/README.md | 1 + apps/fastload/boot.js | 12 ++++++++++-- apps/fastload/settings.js | 19 +++++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/fastload/README.md b/apps/fastload/README.md index be4175f55..d82e13461 100644 --- a/apps/fastload/README.md +++ b/apps/fastload/README.md @@ -12,6 +12,7 @@ This allows fast loading of all apps with two conditions: * If Quick Launch is installed it can be excluded from app history * Allows to redirect all loads usually loading the clock to the launcher instead * The "Fastloading..." screen can be switched off +* Enable checking `setting.json` and force a complete load on changes ## App history diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js index c7fc2fd86..baa5a517c 100644 --- a/apps/fastload/boot.js +++ b/apps/fastload/boot.js @@ -19,7 +19,15 @@ let loadingScreen = function(){ let cache = s.readJSON("fastload.cache") || {}; -let checkApp = function(n){ +const SYS_SETTINGS="setting.json"; + +let appFastloadPossible = function(n){ + if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || E.CRC32(SYS_SETTINGS) != cache[SYS_SETTINGS])){ + cache[SYS_SETTINGS] = E.CRC32(SYS_SETTINGS); + s.writeJSON("fastload.cache", cache); + return false; + } + // no widgets, no problem if (!global.WIDGETS) return true; let app = s.read(n); @@ -39,7 +47,7 @@ let slowload = function(n){ }; let fastload = function(n){ - if (!n || checkApp(n)){ + if (!n || appFastloadPossible(n)){ // Bangle.load can call load, to prevent recursion this must be the system load global.load = slowload; Bangle.load(n); diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js index 1265d5e72..15c135fe4 100644 --- a/apps/fastload/settings.js +++ b/apps/fastload/settings.js @@ -52,12 +52,19 @@ } }; - mainmenu['Hide "Fastloading..."'] = { - value: !!settings.hideLoading, - onchange: v => { - writeSettings("hideLoading",v); - } - }; + mainmenu['Hide "Fastloading..."'] = { + value: !!settings.hideLoading, + onchange: v => { + writeSettings("hideLoading",v); + } + }; + + mainmenu['Detect settings changes'] = { + value: !!settings.detectSettingsChange, + onchange: v => { + writeSettings("detectSettingsChange",v); + } + }; return mainmenu; } From e2ba3ca64da7ce5b433e152c3c5887992b7f8897 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Mon, 24 Jul 2023 22:32:01 +0200 Subject: [PATCH 054/242] fastload - Replace E.CRC32 with Storage.hash --- apps/fastload/boot.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js index baa5a517c..4ac8b97e9 100644 --- a/apps/fastload/boot.js +++ b/apps/fastload/boot.js @@ -22,8 +22,8 @@ let cache = s.readJSON("fastload.cache") || {}; const SYS_SETTINGS="setting.json"; let appFastloadPossible = function(n){ - if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || E.CRC32(SYS_SETTINGS) != cache[SYS_SETTINGS])){ - cache[SYS_SETTINGS] = E.CRC32(SYS_SETTINGS); + if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || s.hash(SYS_SETTINGS) != cache[SYS_SETTINGS])){ + cache[SYS_SETTINGS] = s.hash(SYS_SETTINGS); s.writeJSON("fastload.cache", cache); return false; } @@ -31,11 +31,11 @@ let appFastloadPossible = function(n){ // no widgets, no problem if (!global.WIDGETS) return true; let app = s.read(n); - if (cache[n] && E.CRC32(app) == cache[n].crc) + if (cache[n] && s.hash(app) == cache[n].hash) return cache[n].fast; cache[n] = {}; cache[n].fast = app.includes("Bangle.loadWidgets"); - cache[n].crc = E.CRC32(app); + cache[n].hash = s.hash(app); s.writeJSON("fastload.cache", cache); return cache[n].fast; }; From 1d577fe59da81967e53648807cf0d3a27d103a0c Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Mon, 24 Jul 2023 22:35:21 +0200 Subject: [PATCH 055/242] fastload - Bump version --- apps/fastload/ChangeLog | 1 + apps/fastload/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog index 4e68ab2c7..053799a9f 100644 --- a/apps/fastload/ChangeLog +++ b/apps/fastload/ChangeLog @@ -2,3 +2,4 @@ 0.02: Allow redirection of loads to the launcher 0.03: Allow hiding the fastloading info screen 0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app). +0.05: Check for changes in setting.js and force real reload if needed diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json index 954a7d8b4..abd5b3ed5 100644 --- a/apps/fastload/metadata.json +++ b/apps/fastload/metadata.json @@ -1,7 +1,7 @@ { "id": "fastload", "name": "Fastload Utils", "shortName" : "Fastload Utils", - "version": "0.04", + "version": "0.05", "icon": "icon.png", "description": "Enable experimental fastloading for more apps", "type":"bootloader", From 8de261f687b731d771185dcbfc7900c81e8f2d6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 00:20:11 +0000 Subject: [PATCH 056/242] build(deps): bump core from `92769ac` to `e9eb979` Bumps [core](https://github.com/espruino/EspruinoAppLoaderCore) from `92769ac` to `e9eb979`. - [Commits](https://github.com/espruino/EspruinoAppLoaderCore/compare/92769acd60bc31548ff7c635128d4e7ef02b7325...e9eb97967d948eea85662ee9ea8eba0b44a363e0) --- updated-dependencies: - dependency-name: core dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 92769acd6..e9eb97967 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 92769acd60bc31548ff7c635128d4e7ef02b7325 +Subproject commit e9eb97967d948eea85662ee9ea8eba0b44a363e0 From c987ea0056f88a12f92f1644c0d5b8642e9e144f Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 25 Jul 2023 10:37:54 +0100 Subject: [PATCH 057/242] update --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index e9eb97967..2328a68b3 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit e9eb97967d948eea85662ee9ea8eba0b44a363e0 +Subproject commit 2328a68b36a9af63c4d425f6879a62949fd69868 From 09cc9f2fd58aec8314d1fc963d7f9eb7cba501d0 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Tue, 25 Jul 2023 16:30:17 +0200 Subject: [PATCH 058/242] miclock2 maintenance - Register as clock - Implement fast loading --- apps/miclock2/ChangeLog | 1 + apps/miclock2/clock-mixed.js | 157 +++++++++++++++++++---------------- apps/miclock2/metadata.json | 2 +- 3 files changed, 88 insertions(+), 72 deletions(-) diff --git a/apps/miclock2/ChangeLog b/apps/miclock2/ChangeLog index 534332e63..c34ba135c 100644 --- a/apps/miclock2/ChangeLog +++ b/apps/miclock2/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Redraw only when seconds change 0.03: Fix typo in redraw check +0.04: Register as clock and implement fast loading diff --git a/apps/miclock2/clock-mixed.js b/apps/miclock2/clock-mixed.js index bb1537313..4d533b63a 100644 --- a/apps/miclock2/clock-mixed.js +++ b/apps/miclock2/clock-mixed.js @@ -1,29 +1,30 @@ // Code based on the original Mixed Clock +{ /* jshint esversion: 6 */ -var locale = require("locale"); +const locale = require("locale"); const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 }; const Center = { "x": 120, "y": 96 }; const Widths = { hour: 2, minute: 2 }; -var buf = Graphics.createArrayBuffer(240,192,1,{msb:true}); -var lastDate = new Date(); +const buf = Graphics.createArrayBuffer(240,192,1,{msb:true}); +let timeoutId; -function rotatePoint(x, y, d) { - rad = -1 * d / 180 * Math.PI; - var sin = Math.sin(rad); - var cos = Math.cos(rad); - xn = ((Center.x + x * cos - y * sin) + 0.5) | 0; - yn = ((Center.y + x * sin - y * cos) + 0.5) | 0; - p = [xn, yn]; - return p; -} +const rotatePoint = function(x, y, d, center, res) { + "jit"; + const rad = -1 * d / 180 * Math.PI; + const sin = Math.sin(rad); + const cos = Math.cos(rad); + res[0] = ((center.x + x * cos - y * sin) + 0.5) | 0; + res[1] = ((center.y + x * sin - y * cos) + 0.5) | 0; +}; // from https://github.com/espruino/Espruino/issues/1702 -function setLineWidth(x1, y1, x2, y2, lw) { - var dx = x2 - x1; - var dy = y2 - y1; - var d = Math.sqrt(dx * dx + dy * dy); +const setLineWidth = function(x1, y1, x2, y2, lw) { + "ram"; + let dx = x2 - x1; + let dy = y2 - y1; + let d = Math.sqrt(dx * dx + dy * dy); dx = dx * lw / d; dy = dy * lw / d; @@ -44,71 +45,85 @@ function setLineWidth(x1, y1, x2, y2, lw) { x2 - dy, y2 + dx, x1 - dy, y1 + dx ]; -} +}; -function drawMixedClock(force) { - var date = new Date(); - if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() !== lastDate.getSeconds()) { - lastDate = date; - var dateArray = date.toString().split(" "); - var isEn = locale.name.startsWith("en"); - var point = []; - var minute = date.getMinutes(); - var hour = date.getHours(); - var radius; - - g.reset(); - buf.clear(); - - // draw date - buf.setFont("6x8", 2); - buf.setFontAlign(-1, 0); - buf.drawString(locale.dow(date,true) + ' ', 4, 16, true); - buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true); - buf.setFontAlign(1, 0); - buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true); - buf.drawString(dateArray[3], 237, 176, true); +const drawMixedClock = function() { + "ram"; + const date = new Date(); + const dateArray = date.toString().split(" "); + const isEn = locale.name.startsWith("en"); + let point = [0, 0]; + const minute = date.getMinutes(); + const hour = date.getHours(); + let radius; - // draw hour and minute dots - for (i = 0; i < 60; i++) { - radius = (i % 5) ? 2 : 4; - point = rotatePoint(0, Radius.dots, i * 6); - buf.fillCircle(point[0], point[1], radius); - } + g.reset(); + buf.clear(); - // draw digital time - buf.setFont("6x8", 3); - buf.setFontAlign(0, 0); - buf.drawString(dateArray[4], 120, 120, true); + // draw date + buf.setFont("6x8", 2); + buf.setFontAlign(-1, 0); + buf.drawString(locale.dow(date,true) + ' ', 4, 16, true); + buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true); + buf.setFontAlign(1, 0); + buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true); + buf.drawString(dateArray[3], 237, 176, true); - // draw new minute hand - point = rotatePoint(0, Radius.min, minute * 6); - buf.drawLine(Center.x, Center.y, point[0], point[1]); - buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute)); - // draw new hour hand - point = rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0); - buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour)); - - // draw center - buf.fillCircle(Center.x, Center.y, Radius.center); - - g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24); + // draw hour and minute dots + for (i = 0; i < 60; i++) { + radius = (i % 5) ? 2 : 4; + rotatePoint(0, Radius.dots, i * 6, Center, point); + buf.fillCircle(point[0], point[1], radius); } -} -Bangle.on('lcdPower', function(on) { - if (on) - drawMixedClock(true); - Bangle.drawWidgets(); -}); + // draw digital time + buf.setFont("6x8", 3); + buf.setFontAlign(0, 0); + buf.drawString(dateArray[4], 120, 120, true); -setInterval(() => drawMixedClock(true), 30000); // force an update every 30s even screen is off + // draw new minute hand + rotatePoint(0, Radius.min, minute * 6, Center, point); + buf.drawLine(Center.x, Center.y, point[0], point[1]); + buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute)); + // draw new hour hand + rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0, Center, point); + buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour)); + + // draw center + buf.fillCircle(Center.x, Center.y, Radius.center); + + g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24); + + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + const period = (Bangle.isLCDOn() ? 1000 : 60000); // Update every second if display is on else every minute + let timeout = period - (Date.now() % period); + timeoutId = setTimeout(()=>{ + timeoutId = undefined; + drawMixedClock(); + }, timeout); +}; + +const onLCDPower = function(on) { + if (on) { + drawMixedClock(); + Bangle.drawWidgets(); + } +}; +Bangle.on('lcdPower', onLCDPower); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); drawMixedClock(); // immediately draw -setInterval(drawMixedClock, 500); // update twice a second -// Show launcher when middle button pressed after freeing memory first -setWatch(() => {delete buf.buffer; Bangle.showLauncher()}, BTN2, {repeat:false,edge:"falling"}); +Bangle.setUI({mode:"clock", remove:function() { + if (timeoutId !== undefined) { + delete buf.buffer; + clearTimeout(timeoutId); + timeoutId = undefined; + Bangle.removeListener('lcdPower',onLCDPower); + } +}}); +} diff --git a/apps/miclock2/metadata.json b/apps/miclock2/metadata.json index 094d0995a..f177ab4c1 100644 --- a/apps/miclock2/metadata.json +++ b/apps/miclock2/metadata.json @@ -1,7 +1,7 @@ { "id": "miclock2", "name": "Mixed Clock 2", - "version": "0.03", + "version": "0.04", "description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.", "icon": "clock-mixed.png", "type": "clock", From d0551568bb5c8950ced0f2a50645dc4738f6f42f Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Tue, 25 Jul 2023 19:32:14 +0200 Subject: [PATCH 059/242] miclock2: first setui, then widgets --- apps/miclock2/clock-mixed.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/miclock2/clock-mixed.js b/apps/miclock2/clock-mixed.js index 4d533b63a..e6bc0c094 100644 --- a/apps/miclock2/clock-mixed.js +++ b/apps/miclock2/clock-mixed.js @@ -48,7 +48,6 @@ const setLineWidth = function(x1, y1, x2, y2, lw) { }; const drawMixedClock = function() { - "ram"; const date = new Date(); const dateArray = date.toString().split(" "); const isEn = locale.name.startsWith("en"); @@ -113,11 +112,6 @@ const onLCDPower = function(on) { }; Bangle.on('lcdPower', onLCDPower); -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -drawMixedClock(); // immediately draw - Bangle.setUI({mode:"clock", remove:function() { if (timeoutId !== undefined) { delete buf.buffer; @@ -126,4 +120,9 @@ Bangle.setUI({mode:"clock", remove:function() { Bangle.removeListener('lcdPower',onLCDPower); } }}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawMixedClock(); // immediately draw } From 7e0dbbf6f09cf94c90b1080ae0fb0de059ad6c59 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Tue, 25 Jul 2023 19:55:18 +0200 Subject: [PATCH 060/242] iconlaunch - Go back to now faster drawImage --- apps/iconlaunch/app.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 8f99fcae7..92676a33a 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -57,19 +57,17 @@ c.icondata = ICON_MISSING; count++; } - let drawItem = function(itemI, r) { let x = 0; let apps = launchCache.items[itemI]; let i = itemI * appsN - 1; let selectedApp; let currentApp; - let layers=[]; let selectedRect; for (currentApp of apps) { i++; x += whitespace; - layers.push({x:x+r.x,y:r.y + iconYoffset,image:currentApp.icondata}); + g.drawImage(currentApp.icondata,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize); if (selectedItem == i) { selectedApp = currentApp; selectedRect = [ @@ -82,7 +80,6 @@ x += iconSize; } if (selectedRect) g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); - g.drawImages(layers); if (selectedRect) g.drawRect.apply(null, selectedRect); if (selectedApp) drawText(itemI, r.y, selectedApp); }; From 7d82b39a711ea37deace6ddf43a2069f4ea760f2 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Tue, 25 Jul 2023 20:00:26 +0200 Subject: [PATCH 061/242] widclkscrl: New widget A widget that displays the current date & time after unlocking the watch when not showing a fullscreen clock. The information is scrolled by in a two digit field, so this widget is kept tight. --- apps/widclkscrl/metadata.json | 13 +++++++ apps/widclkscrl/widget.js | 64 ++++++++++++++++++++++++++++++++++ apps/widclkscrl/widget.png | Bin 0 -> 422 bytes 3 files changed, 77 insertions(+) create mode 100644 apps/widclkscrl/metadata.json create mode 100644 apps/widclkscrl/widget.js create mode 100644 apps/widclkscrl/widget.png diff --git a/apps/widclkscrl/metadata.json b/apps/widclkscrl/metadata.json new file mode 100644 index 000000000..81221cbe4 --- /dev/null +++ b/apps/widclkscrl/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widclkscrl", + "name": "Scrolling clock widget", + "version": "0.01", + "description": "A widget that displays the current date & time after unlocking the watch when not showing a fullscreen clock. The information is scrolled by in a two digit field, so this widget is kept tight.", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widclkscrl.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widclkscrl/widget.js b/apps/widclkscrl/widget.js new file mode 100644 index 000000000..3f0316d79 --- /dev/null +++ b/apps/widclkscrl/widget.js @@ -0,0 +1,64 @@ +(() => { + const WIDTH = 14; // Width of the text, widget is +2 px wide + const CONTINOUS = false; // Go back & forward or stop after first scroll + require("FontTeletext5x9Ascii").add(Graphics); + + function getDateText() { + const date = new Date(); + const dateStr = require("locale").date(date, 1); + const timeStr = require("locale").time(date, 1); + return ` ${timeStr} ${dateStr} `; + } + + WIDGETS["widclkscrl"]={ + area: "tl", + width: 0, // default hide + pos: 10, + dir: -1, + eventHandlerSet: false, + draw: function() { + if (!this.eventHandlerSet) { + Bangle.on('lock', (on) => { + this.run(!on); + }); + this.eventHandlerSet = true; + } + }, + run: function (on) { + if (!Bangle.CLOCK && on && !this.interval) { + this.text = getDateText(); + this.interval = setInterval(() => { + this.scroll(); + }, 100); + this.width = WIDTH+2; Bangle.drawWidgets(); + } else if (!on && this.interval) { + clearInterval(this.interval); + this.interval = undefined; + this.width = 0; Bangle.drawWidgets(); + delete this.text; + } + }, + scroll: function() { + const buf = Graphics.createArrayBuffer(WIDTH,24,1,{msb:true}).setFont("Teletext5x9Ascii:1x2").setFontAlign(-1, 0); + buf.drawString(this.text, this.pos, 12); + + if (this.dir === 1 && this.pos === 0 || this.dir === -1 && Math.abs(this.pos) === buf.stringWidth(this.text) - WIDTH) { + if (CONTINOUS) { + this.dir*=-1; + this.text = getDateText(); + } else { + this.pos = 0; + this.run(false); + return; + } + } + this.pos+=this.dir; + + g.reset().drawImage({ + width:buf.getWidth(), height:buf.getHeight(), + bpp:buf.getBPP(), + buffer:buf.buffer + }, this.x+1, this.y); + }, + }; +})(); diff --git a/apps/widclkscrl/widget.png b/apps/widclkscrl/widget.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4bc9774ff30d51971c6851bbf846103560d6cc GIT binary patch literal 422 zcmV;X0a^ZuP)o#^qP1I??3}>!~N~_91GZXYMSO|Jgzj2j<#S_EiUD`4LoE_o zTPhZ%7?@UPE@^0OsaOO95<6hfLUX{Mc!Rf)!E2P`LuT+LUX>Un+=($XE{E2ZibW{~ z#>vofIwiVvpO3$NQ)X@~N;UA;h>H{1{vS_dJmM&ra-7VKML7mqAf15$^-)_S&%9bZ z)E3J9?1BcE&^Juzz5%i32Zb8gi@Ks1*n4<;mEA@=DXXE277qXb08j$H0Q#F*L06UM Qy8r+H07*qoM6N<$f~P>OEdT%j literal 0 HcmV?d00001 From a9fff204779201cc21ec08ce47650a3e55c4c8bd Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Tue, 25 Jul 2023 20:43:12 +0200 Subject: [PATCH 062/242] iconlaunch - Use objects instead of arrays --- apps/iconlaunch/app.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 92676a33a..cb43dd79f 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -45,18 +45,20 @@ const iconYoffset = Math.floor(whitespace/4)-1; const itemSize = iconSize + whitespace; - launchCache.items = []; + let t = Date.now(); + launchCache.items = {}; for (let c of launchCache.apps){ let i = Math.floor(count/appsN); if (!launchCache.items[i]) - launchCache.items.push([]); - launchCache.items[Math.floor(count/appsN)].push(c); + launchCache.items[i] = {}; + launchCache.items[i].push(count%3); if (c.icon) c.icondata = s.read(c.icon); else c.icondata = ICON_MISSING; count++; } + let drawItem = function(itemI, r) { let x = 0; let apps = launchCache.items[itemI]; From 94bb93c19e20f9436b4c6436ff3a5ed421f03bdf Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Tue, 25 Jul 2023 21:51:11 +0200 Subject: [PATCH 063/242] iconlaunch - Go back to caching icons on demand but keep presorting apps into items --- apps/iconlaunch/app.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index cb43dd79f..c744759ac 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -45,33 +45,33 @@ const iconYoffset = Math.floor(whitespace/4)-1; const itemSize = iconSize + whitespace; - let t = Date.now(); launchCache.items = {}; for (let c of launchCache.apps){ let i = Math.floor(count/appsN); if (!launchCache.items[i]) launchCache.items[i] = {}; - launchCache.items[i].push(count%3); - if (c.icon) - c.icondata = s.read(c.icon); - else - c.icondata = ICON_MISSING; + launchCache.items[i][(count%3)] = c; count++; } + let texted; let drawItem = function(itemI, r) { - let x = 0; - let apps = launchCache.items[itemI]; + let x = whitespace; let i = itemI * appsN - 1; let selectedApp; - let currentApp; + let c; let selectedRect; - for (currentApp of apps) { + let item = launchCache.items[itemI]; + if (texted == itemI){ + g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); + texted = undefined; + } + for (c of item) { i++; - x += whitespace; - g.drawImage(currentApp.icondata,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize); + let id = c.icondata || (c.iconData = (c.icon ? s.read(c.icon) : ICON_MISSING)); + g.drawImage(id,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize); if (selectedItem == i) { - selectedApp = currentApp; + selectedApp = c; selectedRect = [ x + r.x - 1, r.y + iconYoffset - 1, @@ -79,11 +79,13 @@ r.y + iconYoffset + iconSize ]; } - x += iconSize; + x += iconSize + whitespace; + } + if (selectedRect) { + g.drawRect.apply(null, selectedRect); + drawText(itemI, r.y, selectedApp); + texted=itemI; } - if (selectedRect) g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); - if (selectedRect) g.drawRect.apply(null, selectedRect); - if (selectedApp) drawText(itemI, r.y, selectedApp); }; let drawText = function(i, appY, selectedApp) { From 9ea1fe4dcfcce5f2e31249765def2d779d211bc0 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Tue, 25 Jul 2023 22:38:08 +0200 Subject: [PATCH 064/242] widclkscrl: Draw only in draw method --- apps/widclkscrl/widget.js | 52 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/apps/widclkscrl/widget.js b/apps/widclkscrl/widget.js index 3f0316d79..910108da5 100644 --- a/apps/widclkscrl/widget.js +++ b/apps/widclkscrl/widget.js @@ -16,49 +16,49 @@ pos: 10, dir: -1, eventHandlerSet: false, - draw: function() { + draw: function(_w, scroll) { if (!this.eventHandlerSet) { Bangle.on('lock', (on) => { this.run(!on); }); this.eventHandlerSet = true; } + if (scroll) { + const buf = Graphics.createArrayBuffer(WIDTH,24,1,{msb:true}).setFont("Teletext5x9Ascii:1x2").setFontAlign(-1, 0); + buf.drawString(this.text, this.pos, 12); + + if (this.dir === 1 && this.pos === 0 || this.dir === -1 && Math.abs(this.pos) === buf.stringWidth(this.text) - WIDTH) { + if (CONTINOUS) { + this.dir*=-1; + this.text = getDateText(); + } else { + this.pos = 0; + this.run(false); + return; + } + } + this.pos+=this.dir; + + g.reset().drawImage({ + width:buf.getWidth(), height:buf.getHeight(), + bpp:buf.getBPP(), + buffer:buf.buffer + }, this.x+1, this.y); + } }, run: function (on) { if (!Bangle.CLOCK && on && !this.interval) { this.text = getDateText(); this.interval = setInterval(() => { - this.scroll(); + this.draw(this, true); }, 100); this.width = WIDTH+2; Bangle.drawWidgets(); } else if (!on && this.interval) { clearInterval(this.interval); - this.interval = undefined; - this.width = 0; Bangle.drawWidgets(); + delete this.interval; delete this.text; + this.width = 0; Bangle.drawWidgets(); } }, - scroll: function() { - const buf = Graphics.createArrayBuffer(WIDTH,24,1,{msb:true}).setFont("Teletext5x9Ascii:1x2").setFontAlign(-1, 0); - buf.drawString(this.text, this.pos, 12); - - if (this.dir === 1 && this.pos === 0 || this.dir === -1 && Math.abs(this.pos) === buf.stringWidth(this.text) - WIDTH) { - if (CONTINOUS) { - this.dir*=-1; - this.text = getDateText(); - } else { - this.pos = 0; - this.run(false); - return; - } - } - this.pos+=this.dir; - - g.reset().drawImage({ - width:buf.getWidth(), height:buf.getHeight(), - bpp:buf.getBPP(), - buffer:buf.buffer - }, this.x+1, this.y); - }, }; })(); From 53cb6d8126e18ad46a44ed376f34b54df78f6dc1 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Tue, 25 Jul 2023 22:24:50 +0200 Subject: [PATCH 065/242] iconlaunch - Add jit marker to drawItem and drawText --- apps/iconlaunch/app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index c744759ac..03e743885 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -56,6 +56,7 @@ let texted; let drawItem = function(itemI, r) { + "jit"; let x = whitespace; let i = itemI * appsN - 1; let selectedApp; @@ -89,6 +90,7 @@ }; let drawText = function(i, appY, selectedApp) { + "jit"; const idy = (selectedItem - (selectedItem % 3)) / 3; if (i != idy) return; appY = appY + itemSize/2; From 7625421a51a51ab7a16f55a39e1fde93028a4d1b Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Tue, 25 Jul 2023 23:35:32 +0200 Subject: [PATCH 066/242] widclkscrl: draw without args --- apps/widclkscrl/widget.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/widclkscrl/widget.js b/apps/widclkscrl/widget.js index 910108da5..292d291ac 100644 --- a/apps/widclkscrl/widget.js +++ b/apps/widclkscrl/widget.js @@ -16,14 +16,14 @@ pos: 10, dir: -1, eventHandlerSet: false, - draw: function(_w, scroll) { + draw: function() { if (!this.eventHandlerSet) { Bangle.on('lock', (on) => { this.run(!on); }); this.eventHandlerSet = true; } - if (scroll) { + if (this.text) { const buf = Graphics.createArrayBuffer(WIDTH,24,1,{msb:true}).setFont("Teletext5x9Ascii:1x2").setFontAlign(-1, 0); buf.drawString(this.text, this.pos, 12); @@ -50,7 +50,7 @@ if (!Bangle.CLOCK && on && !this.interval) { this.text = getDateText(); this.interval = setInterval(() => { - this.draw(this, true); + this.draw(); }, 100); this.width = WIDTH+2; Bangle.drawWidgets(); } else if (!on && this.interval) { From e851d1321e646a671cd979e7f8f0a3629319a78e Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 25 Jul 2023 16:40:46 -0500 Subject: [PATCH 067/242] Correct some problems with swipe detection on main clock screen Increase sensitivity so only a small swipe is needed to switch to the appropriate screen. Fix uninitialized variables for calculating swipe length which usually caused first swipe length to register as NaN. --- apps/timerclk/ChangeLog | 1 + apps/timerclk/app.js | 10 +++++----- apps/timerclk/lib.js | 2 +- apps/timerclk/metadata.json | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/timerclk/ChangeLog b/apps/timerclk/ChangeLog index 5a954d58c..46aa52ee1 100644 --- a/apps/timerclk/ChangeLog +++ b/apps/timerclk/ChangeLog @@ -2,3 +2,4 @@ 0.02: Add sunrise/sunset. Fix timer bugs. 0.03: Use default Bangle formatter for booleans 0.04: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps +0.05: Improve responsiveness and detection of swipes on main clock screen diff --git a/apps/timerclk/app.js b/apps/timerclk/app.js index ee30b059a..e489f9844 100644 --- a/apps/timerclk/app.js +++ b/apps/timerclk/app.js @@ -148,23 +148,23 @@ if (process.env.HWVERSION==1) { setWatch(()=>load("timerclk.alarm.js"), BTN3); setWatch(()=>load("timerclk.alarm.js"), BTN1); } else { - var absY, lastX, lastY; + var absY, lastX=0, lastY=0; Bangle.on('drag', e=>{ if (!e.b) { - if (lastX > 50) { // right + if (lastX > 5) { // right if (absY < dragBorder) { // drag over time load("timerclk.timer.js"); }else { // drag over date/dow load("timerclk.alarm.js"); } - } else if (lastX < -50) { // left + } else if (lastX < -5) { // left if (absY < dragBorder) { // drag over time load("timerclk.stopwatch.js"); }else { // drag over date/dow load("timerclk.alarm.js"); } - } else if (lastY > 50) { // down - } else if (lastY < -50) { // up + } else if (lastY > 5) { // down + } else if (lastY < -5) { // up } lastX = 0; lastY = 0; diff --git a/apps/timerclk/lib.js b/apps/timerclk/lib.js index dd3893fa1..47f49736f 100644 --- a/apps/timerclk/lib.js +++ b/apps/timerclk/lib.js @@ -87,7 +87,7 @@ exports.registerControls = function(o) { } } }); - var absX, lastX, lastY; + var absX, lastX=0, lastY=0; Bangle.on('drag', e=>{ if (!e.b) { if (lastX > 40) { // right diff --git a/apps/timerclk/metadata.json b/apps/timerclk/metadata.json index 5bd6bee24..0a6311ac1 100644 --- a/apps/timerclk/metadata.json +++ b/apps/timerclk/metadata.json @@ -2,7 +2,7 @@ "id": "timerclk", "name": "Timer Clock", "shortName":"Timer Clock", - "version":"0.04", + "version":"0.05", "description": "A clock with stopwatches, timers and alarms build in.", "icon": "app-icon.png", "type": "clock", From 63cbd45cc6672b50455b5fb710389bd1783e20e5 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 26 Jul 2023 07:00:31 +0100 Subject: [PATCH 068/242] btadv: add sensor location entry --- apps/btadv/app.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 5e4930865..bc7371fc4 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -43,6 +43,10 @@ const enum BleChar { // HRM = "0x2a37", + // org.bluetooth.characteristic.body_sensor_location + // u8 + SensorLocation = "0x2a38", + // org.bluetooth.characteristic.elevation // s24, meters 0.01 Elevation = "0x2a6c", @@ -84,6 +88,16 @@ type LenFunc = { maxLen: number, } +const enum SensorLocations = { + Other = 0, + Chest = 1, + Wrist = 2, + Finger = 3, + Hand = 4, + EarLobe = 5, + Foot = 6, +} + let acc: undefined | AccelData; let bar: undefined | PressureData; let gps: undefined | GPSFix; @@ -515,12 +529,22 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => { readable: true, notify: true, }; + const os: BleCharAdvert = { + maxLen: 1, + readable: true, + notify: true, + }; + if (hrm) { o.value = encodeHrm(hrm); + os.value = SensorLocations.Wrist; hrm = undefined; } - return { [BleChar.HRM]: o }; + return { + [BleChar.HRM]: o, + [BleChar.SensorLocation]: os, + }; } return {}; From 93f21a51e1f5768ad4c247cd3d3c87d1dbf0c222 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Fri, 14 Jul 2023 16:39:41 +0100 Subject: [PATCH 069/242] opt? --- apps/wid_edit/boot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js index 3cb545a34..60912fd6f 100644 --- a/apps/wid_edit/boot.js +++ b/apps/wid_edit/boot.js @@ -14,7 +14,6 @@ Bangle.loadWidgets = (o => ()=>{ const W = global.WIDGETS; global.WIDGETS = {}; Object.keys(W) - .sort() .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) .forEach(k => global.WIDGETS[k] = W[k]); })(Bangle.loadWidgets); From d5aade8d48b52dab923b423f8190b95852f93e6b Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 26 Jul 2023 07:27:43 +0100 Subject: [PATCH 070/242] wid_edit: bump version --- apps/wid_edit/ChangeLog | 1 + apps/wid_edit/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog index 279fa2438..93f61b814 100644 --- a/apps/wid_edit/ChangeLog +++ b/apps/wid_edit/ChangeLog @@ -3,3 +3,4 @@ Change back entry to menu option Allow changing widgets into all areas, including bottom widget bar 0.03: Fix editing widgets whose draw method takes the widget +0.04: Remove double-sort diff --git a/apps/wid_edit/metadata.json b/apps/wid_edit/metadata.json index e80e45d45..02ab216fe 100644 --- a/apps/wid_edit/metadata.json +++ b/apps/wid_edit/metadata.json @@ -1,6 +1,6 @@ { "id": "wid_edit", - "version": "0.03", + "version": "0.04", "name": "Widget Editor", "icon": "icon.png", "description": "Customize widget locations", From a2d1b9c0fb5fb0adb0adcf833e662c7415870fe0 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Wed, 26 Jul 2023 20:06:42 +0200 Subject: [PATCH 071/242] sensortools - Fix sensortools breaking Bangle.emit with multiple arguments (e.g. "message") --- apps/sensortools/ChangeLog | 1 + apps/sensortools/lib.js | 29 ++++++++++++++++++----------- apps/sensortools/metadata.json | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog index 6d2f5d2b4..92088af2b 100644 --- a/apps/sensortools/ChangeLog +++ b/apps/sensortools/ChangeLog @@ -4,3 +4,4 @@ 0.04: Correct type of time attribute in gps to Date 0.05: Fix gps emulation interpolation Add setting for log output +0.06: Fix sensortools breaking Bangle.emit with multiple arguments (e.g. "message") diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js index 5e1c199c2..fae856108 100644 --- a/apps/sensortools/lib.js +++ b/apps/sensortools/lib.js @@ -20,37 +20,44 @@ exports.enable = () => { Bangle.sensortoolsOrigEmit = Bangle.emit; Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener; + const modifyArguments = function(args, value) { + if (args.length >= 1) + args[0] = value; + return args; + }; + Bangle.on = function(name, callback) { if (onEvents[name]) { log("Redirecting listener for", name, "to", name + "_mod"); - Bangle.sensortoolsOrigOn(name + "_mod", callback); - Bangle.sensortoolsOrigOn(name, (e) => { - log("Redirected event for", name, "to", name + "_mod"); - Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e)); + let origName = name; + Bangle.sensortoolsOrigOn(origName, (e) => { + log("Redirected event for", origName, "to", origName + "_mod"); + Bangle.sensortoolsOrigEmit(origName + "_mod", onEvents[origName](e)); }); + Bangle.sensortoolsOrigOn.apply(this, modifyArguments(arguments, name + "_mod")); } else { log("Pass through on call for", name, callback); - Bangle.sensortoolsOrigOn(name, callback); + Bangle.sensortoolsOrigOn.apply(this, arguments); } }; - Bangle.removeListener = function(name, callback) { + Bangle.removeListener = function(name) { if (onEvents[name]) { log("Removing augmented listener for", name, onEvents[name]); - Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback); + Bangle.sensortoolsOrigRemoveListener.apply(this, modifyArguments(arguments, name + "_mod")); } else { log("Pass through remove listener for", name); - Bangle.sensortoolsOrigRemoveListener(name, callback); + Bangle.sensortoolsOrigRemoveListener.apply(this, arguments); } }; - Bangle.emit = function(name, event) { + Bangle.emit = function(name) { if (onEvents[name]) { log("Augmenting emit call for", name, onEvents[name]); - Bangle.sensortoolsOrigEmit(name + "_mod", event); + Bangle.sensortoolsOrigEmit.apply(this, modifyArguments(arguments, name + "_mod")); } else { log("Pass through emit call for", name); - Bangle.sensortoolsOrigEmit(name, event); + Bangle.sensortoolsOrigEmit.apply(this, arguments); } }; diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json index bffffd090..23749d537 100644 --- a/apps/sensortools/metadata.json +++ b/apps/sensortools/metadata.json @@ -2,7 +2,7 @@ "id": "sensortools", "name": "Sensor tools", "shortName": "Sensor tools", - "version": "0.05", + "version": "0.06", "description": "Tools for testing and debugging apps that use sensor input", "icon": "icon.png", "type": "bootloader", From 31736cb447dc52487d5e3ada1d5862ed78bed322 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 26 Jul 2023 07:15:31 +0100 Subject: [PATCH 072/242] btadv: encode & advertise accelerometer data --- apps/btadv/app.js | 40 ++++++++++++++++++++++++++++++++++---- apps/btadv/app.ts | 49 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/apps/btadv/app.js b/apps/btadv/app.js index 670691fb9..cacba3eb6 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -4,7 +4,12 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); var HRM_MIN_CONFIDENCE_1 = 75; - var services_1 = ["0x180d", "0x181a", "0x1819"]; + var services_1 = [ + "0x180d", + "0x181a", + "0x1819", + "0xE95D0753251D470AA062FA1922DFA9A8", + ]; var acc_1; var bar_1; var gps_1; @@ -215,13 +220,15 @@ ]; }; encodeGpsHeadingOnly_1.maxLen = 17; - var encodeMag_1 = function (data) { + var encodeXYZ = function (data) { var x = toByteArray_1(data.x, 2, true); var y = toByteArray_1(data.y, 2, true); var z = toByteArray_1(data.z, 2, true); return [x[0], x[1], y[0], y[1], z[0], z[1]]; }; - encodeMag_1.maxLen = 6; + encodeXYZ.maxLen = 6; + var encodeMag_1 = encodeXYZ; + var encodeAcc_1 = encodeXYZ; var toByteArray_1 = function (value, numberOfBytes, isSigned) { var byteArray = new Array(numberOfBytes); if (isSigned && (value < 0)) { @@ -251,6 +258,7 @@ case "0x180d": return !!hrm_1; case "0x181a": return !!(bar_1 || mag_1); case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1); + case "0xE95D0753251D470AA062FA1922DFA9A8": return !!acc_1; } }; var serviceToAdvert_1 = function (serv, initial) { @@ -264,11 +272,20 @@ readable: true, notify: true, }; + var os = { + maxLen: 1, + readable: true, + notify: true, + }; if (hrm_1) { o.value = encodeHrm_1(hrm_1); + os.value = [2]; hrm_1 = undefined; } - return _a = {}, _a["0x2a37"] = o, _a; + return _a = {}, + _a["0x2a37"] = o, + _a["0x2a38"] = os, + _a; } return {}; case "0x1819": @@ -331,6 +348,21 @@ } return o; } + case "0xE95D0753251D470AA062FA1922DFA9A8": { + var o = {}; + if (acc_1 || initial) { + o["0xE95DCA4B251D470AA062FA1922DFA9A8"] = { + maxLen: encodeAcc_1.maxLen, + readable: true, + notify: true, + }; + if (acc_1) { + o["0xE95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1); + acc_1 = undefined; + } + } + return o; + } } }; var getBleAdvert_1 = function (map, all) { diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index bc7371fc4..fbce48fe6 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -33,10 +33,17 @@ const enum BleServ { // contains: LocationAndSpeed LocationAndNavigation = "0x1819", - // Acc // none known for this + // org.microbit.service.accelerometer + // contains: Acc + Acc = "0xE95D0753251D470AA062FA1922DFA9A8", } -const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation]; +const services = [ + BleServ.HRM, + BleServ.EnvSensing, + BleServ.LocationAndNavigation, + BleServ.Acc, +]; const enum BleChar { // org.bluetooth.characteristic.heart_rate_measurement @@ -69,6 +76,11 @@ const enum BleChar { // org.bluetooth.characteristic.magnetic_flux_density_3d // s16: x, y, z, tesla (10^-7) MagneticFlux3D = "0x2aa1", + + // org.microbit.characteristic.accelerometer_data + // s16 x3, -1024 .. 1024 + // docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/ + Acc = "0xE95DCA4B251D470AA062FA1922DFA9A8", } type BleCharAdvert = { @@ -88,7 +100,7 @@ type LenFunc = { maxLen: number, } -const enum SensorLocations = { +const enum SensorLocations { Other = 0, Chest = 1, Wrist = 2, @@ -469,14 +481,19 @@ const encodeGpsHeadingOnly: LenFunc = (data: CompassData) => { }; encodeGpsHeadingOnly.maxLen = 17; -const encodeMag: LenFunc = (data: CompassData) => { +type XYZ = { x: number, y: number, z: number }; + +const encodeXYZ: LenFunc = (data: XYZ) => { const x = toByteArray(data.x, 2, true); const y = toByteArray(data.y, 2, true); const z = toByteArray(data.z, 2, true); return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ]; }; -encodeMag.maxLen = 6; +encodeXYZ.maxLen = 6; + +const encodeMag: LenFunc = encodeXYZ; +const encodeAcc: LenFunc = encodeXYZ; const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => { const byteArray: Array = new Array(numberOfBytes); @@ -517,6 +534,7 @@ const haveServiceData = (serv: BleServ): boolean => { case BleServ.HRM: return !!hrm; case BleServ.EnvSensing: return !!(bar || mag); case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag); + case BleServ.Acc: return !!acc; } }; @@ -537,7 +555,7 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => { if (hrm) { o.value = encodeHrm(hrm); - os.value = SensorLocations.Wrist; + os.value = [SensorLocations.Wrist]; hrm = undefined; } @@ -615,6 +633,25 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => { return o; } + + case BleServ.Acc: { + const o: BleServAdvert = {}; + + if (acc || initial) { + o[BleChar.Acc] = { + maxLen: encodeAcc.maxLen, + readable: true, + notify: true, + }; + + if (acc) { + o[BleChar.Acc]!.value = encodeAcc(acc); + acc = undefined; + } + } + + return o; + } } }; From c385d718b509aa9048a75b7302919a8b32fe8b5a Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 26 Jul 2023 07:26:18 +0100 Subject: [PATCH 073/242] btadv: bump version --- apps/btadv/ChangeLog | 1 + apps/btadv/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/btadv/ChangeLog b/apps/btadv/ChangeLog index 1a3bc1757..07e67157c 100644 --- a/apps/btadv/ChangeLog +++ b/apps/btadv/ChangeLog @@ -1 +1,2 @@ 0.01: New app! +0.02: Advertise accelerometer data and sensor location diff --git a/apps/btadv/metadata.json b/apps/btadv/metadata.json index 7028b2a95..efe024a2f 100644 --- a/apps/btadv/metadata.json +++ b/apps/btadv/metadata.json @@ -2,7 +2,7 @@ "id": "btadv", "name": "btadv", "shortName": "btadv", - "version": "0.01", + "version": "0.02", "description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth", "icon": "icon.png", "tags": "health,tool,sensors,bluetooth", From 2024f9267caa36d7ef2337b705d4f5ad100d85fb Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Wed, 26 Jul 2023 23:58:20 +0200 Subject: [PATCH 074/242] taglaunch: Fix remove handler --- apps/taglaunch/ChangeLog | 1 + apps/taglaunch/app.js | 15 +++++++++------ apps/taglaunch/metadata.json | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/taglaunch/ChangeLog b/apps/taglaunch/ChangeLog index 55315bf6e..6c36d39d5 100644 --- a/apps/taglaunch/ChangeLog +++ b/apps/taglaunch/ChangeLog @@ -2,3 +2,4 @@ 0.02: Use Bangle.showClock for changing to clock (Backport from launch) 0.03: Remove app from 'tool' when it has at least one other known tag Add tag 'health' for apps like Heart Rate Monitor +0.04: Fix remove handler diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js index aad61e298..cfe3f7819 100644 --- a/apps/taglaunch/app.js +++ b/apps/taglaunch/app.js @@ -73,6 +73,12 @@ let tagKeys = Object.keys(tags).filter(tag => tag !== "clock" || settings.showCl if (!settings.fullscreen) Bangle.loadWidgets(); +const unload = () => { + // cleanup the timeout to not leave anything behind after being removed from ram + if (lockTimeout) clearTimeout(lockTimeout); + Bangle.removeListener("lock", lockHandler); +}; + let showTagMenu = (tag) => { E.showScroller({ h : 64*scaleval, c : appsByTag[tag].length, @@ -96,7 +102,8 @@ let showTagMenu = (tag) => { load(app.src); } }, - back : showMainMenu + back : showMainMenu, + remove: unload }); }; @@ -118,11 +125,7 @@ let showMainMenu = () => { showTagMenu(tag); }, back : Bangle.showClock, // button press or tap in top left shows clock now - remove : () => { - // cleanup the timeout to not leave anything behind after being removed from ram - if (lockTimeout) clearTimeout(lockTimeout); - Bangle.removeListener("lock", lockHandler); - } + remove : unload }); }; showMainMenu(); diff --git a/apps/taglaunch/metadata.json b/apps/taglaunch/metadata.json index 4f7c295e9..fe48d7492 100644 --- a/apps/taglaunch/metadata.json +++ b/apps/taglaunch/metadata.json @@ -2,7 +2,7 @@ "id": "taglaunch", "name": "Tag Launcher", "shortName": "Taglauncher", - "version": "0.03", + "version": "0.04", "description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.", "readme": "README.md", "icon": "app.png", From 2b973e304289aff7f897625dc4de8451437c794d Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 27 Jul 2023 12:24:21 +0100 Subject: [PATCH 075/242] multitimer: ensure onDrag is registered after all E.show calls --- apps/multitimer/ChangeLog | 2 ++ apps/multitimer/app.js | 7 +++++-- apps/multitimer/metadata.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/multitimer/ChangeLog b/apps/multitimer/ChangeLog index b708a990f..67b0cc014 100644 --- a/apps/multitimer/ChangeLog +++ b/apps/multitimer/ChangeLog @@ -4,3 +4,5 @@ 0.04: Remove copied sched alarm.js & import newer features (oneshot alarms) 0.05: Fix creating new alarms/timers in hardmode 0.06: Support fastloading +0.07: Fix fastloading support - ensure drag handler's restored after + menu display/fastload removes it diff --git a/apps/multitimer/app.js b/apps/multitimer/app.js index 079136431..965a12d26 100644 --- a/apps/multitimer/app.js +++ b/apps/multitimer/app.js @@ -217,6 +217,7 @@ function timerMenu(idx) { } } }); + setUI(); } function editTimer(idx, a) { @@ -376,6 +377,7 @@ function drawSw() { else if (idx > 0 && idx < sw.length+1) swMenu(idx-1); } }); + setUI(); } function swMenu(idx, a) { @@ -499,6 +501,7 @@ function swMenu(idx, a) { } } }); + setUI(); } function drawAlarms() { @@ -539,6 +542,7 @@ function drawAlarms() { else if (idx > 0 && idx < alarms.length+1) editAlarm(idx-1); } }); + setUI(); } function editDOW(dow, onchange) { @@ -672,6 +676,7 @@ function editAlarm(idx, a) { function setUI() { // E.showMenu/E.showScroller/E.showAlert call setUI, so we register onDrag() separately // and tack on uiRemove after the fact to avoid interfering + Bangle.on("drag", onDrag); Bangle.uiRemove = () => { Bangle.removeListener("drag", onDrag); Object.values(timerInt1).forEach(clearTimeout); @@ -703,6 +708,4 @@ function onDrag(e) { } drawTimers(); - -Bangle.on("drag", onDrag); } diff --git a/apps/multitimer/metadata.json b/apps/multitimer/metadata.json index 40d376986..e753d0581 100644 --- a/apps/multitimer/metadata.json +++ b/apps/multitimer/metadata.json @@ -1,7 +1,7 @@ { "id": "multitimer", "name": "Multi Timer", - "version": "0.06", + "version": "0.07", "description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.", "icon": "app.png", "screenshots": [ From f2f04ecd11497deec814d4966d482771779955e7 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Thu, 27 Jul 2023 17:43:06 +0200 Subject: [PATCH 076/242] widshipbell: Bump version as commit 2b6f776 --- apps/widshipbell/ChangeLog | 2 ++ apps/widshipbell/metadata.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 apps/widshipbell/ChangeLog diff --git a/apps/widshipbell/ChangeLog b/apps/widshipbell/ChangeLog new file mode 100644 index 000000000..a26ed96db --- /dev/null +++ b/apps/widshipbell/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called diff --git a/apps/widshipbell/metadata.json b/apps/widshipbell/metadata.json index c130b04ee..1c4a7613e 100644 --- a/apps/widshipbell/metadata.json +++ b/apps/widshipbell/metadata.json @@ -2,7 +2,7 @@ "id": "widshipbell", "name": "Ship's bell Widget", "shortName": "Ship's bell", - "version": "0.01", + "version": "0.02", "description": "A widget that buzzes according to a nautical bell, one strike at 04:30, two strikes at 05:00, up to eight strikes at 08:00 and so on.", "icon": "widget.png", "type": "widget", From cdf3486ab6ec6af786e7020c45800d14a4804d9d Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Thu, 27 Jul 2023 18:26:56 +0200 Subject: [PATCH 077/242] astrocalc: Enable widgets --- apps/astrocalc/ChangeLog | 1 + apps/astrocalc/astrocalc-app.js | 20 +++++++------------- apps/astrocalc/metadata.json | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 11b2d7177..95b9dbaf1 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -2,3 +2,4 @@ 0.02: Store last GPS lock, can be used instead of waiting for new GPS on start 0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.04: Compatibility with Bangle.js 2, get location from My Location +0.05: Enable widgets diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 6629842cf..1963d784e 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -110,7 +110,7 @@ function drawPoints() { } function drawData(title, obj, startX, startY) { - g.clear(); + g.clearRect(Bangle.appRect); drawTitle(title); let xPos, yPos; @@ -154,9 +154,7 @@ function drawMoonPositionPage(gps, title) { drawPoints(); drawPoint(azimuthDegrees, 8, moonColor); - let m = setWatch(() => { - let m = moonIndexPageMenu(gps); - }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); } function drawMoonIlluminationPage(gps, title) { @@ -174,9 +172,7 @@ function drawMoonIlluminationPage(gps, title) { drawData(title, pageData, null, 35); drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2); - let m = setWatch(() => { - let m = moonIndexPageMenu(gps); - }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); } @@ -202,9 +198,7 @@ function drawMoonTimesPage(gps, title) { const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); drawPoint(setAzimuthDegrees, 8, moonColor); - let m = setWatch(() => { - let m = moonIndexPageMenu(gps); - }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); } function drawSunShowPage(gps, key, date) { @@ -233,9 +227,7 @@ function drawSunShowPage(gps, key, date) { // Draw the suns position drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0}); - m = setWatch(() => { - m = sunIndexPageMenu(gps); - }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => sunIndexPageMenu(gps)}); return null; } @@ -314,7 +306,9 @@ function getCenterStringX(str) { function init() { let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + Bangle.loadWidgets(); indexPageMenu(location); + Bangle.drawWidgets(); } let m; diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json index 653c097da..1f4abb356 100644 --- a/apps/astrocalc/metadata.json +++ b/apps/astrocalc/metadata.json @@ -1,7 +1,7 @@ { "id": "astrocalc", "name": "Astrocalc", - "version": "0.04", + "version": "0.05", "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app", "icon": "astrocalc.png", "tags": "app,sun,moon,cycles,tool", From 73218e4180609e1dca8c144180d3952c8367fb28 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 21 May 2023 13:48:52 +0100 Subject: [PATCH 078/242] Regenerate typescript --- typescript/types/main.d.ts | 202 +++++++++++++++++++++++++++++++++---- 1 file changed, 185 insertions(+), 17 deletions(-) diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts index ecf509f70..eda8a9a81 100644 --- a/typescript/types/main.d.ts +++ b/typescript/types/main.d.ts @@ -180,6 +180,7 @@ type SetUIArg = Mode | { mode: Mode, back?: () => void, remove?: () => void, + redraw?: () => void, }; type NRFFilters = { @@ -841,6 +842,16 @@ declare class NRF { */ static on(event: "disconnect", callback: (reason: number) => void): void; + /** + * Called when the Nordic Bluetooth stack (softdevice) generates an error. In pretty + * much all cases an Exception will also have been thrown. + * @param {string} event - The event to listen to. + * @param {(msg: any) => void} callback - A function that is executed when the event occurs. Its arguments are: + * * `msg` The error string + * @url http://www.espruino.com/Reference#l_NRF_error + */ + static on(event: "error", callback: (msg: any) => void): void; + /** * Contains updates on the security of the current Bluetooth link. * See Nordic's `ble_gap_evt_auth_status_t` structure for more information. @@ -964,6 +975,17 @@ declare class NRF { */ static restart(callback?: any): void; + /** + * Delete all data stored for all peers (bonding data used for secure connections). This cannot be done + * while a connection is active, so if there is a connection it will be postponed until everything is disconnected + * (which can be done by calling `NRF.disconnect()` and waiting). + * Booting your device while holding all buttons down together should also have the same effect. + * + * @param {any} [callback] - [optional] A function to be called while the softdevice is uninitialised. Use with caution - accessing console/bluetooth will almost certainly result in a crash. + * @url http://www.espruino.com/Reference#l_NRF_eraseBonds + */ + static eraseBonds(callback?: any): void; + /** * Get this device's default Bluetooth MAC address. * For Puck.js, the last 5 characters of this (e.g. `ee:ff`) are used in the @@ -1222,8 +1244,9 @@ declare class NRF { * hid : new Uint8Array(...), // optional, default is undefined. Enable BLE HID support * uart : true, // optional, default is true. Enable BLE UART support * advertise: [ '180D' ] // optional, list of service UUIDs to advertise - * ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications - * ams : true // optional, Bangle.js-only, enable Apple AMS support for media control + * ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications (see `NRF.ancs*`) + * ams : true // optional, Bangle.js-only, enable Apple AMS support for media control (see `NRF.ams*`) + * cts : true // optional, Bangle.js-only, enable Apple Current Time Service support (see `NRF.ctsGetTime`) * }); * ``` * To enable BLE HID, you must set `hid` to an array which is the BLE report @@ -1747,6 +1770,55 @@ declare class NRF { */ static amsCommand(id: any): void; + /** + * Check if Apple Current Time Service (CTS) is currently active on the BLE connection + * + * @returns {boolean} True if Apple Current Time Service (CTS) has been initialised and is active + * @url http://www.espruino.com/Reference#l_NRF_ctsIsActive + */ + static ctsIsActive(): boolean; + + /** + * Returns time information from the Current Time Service + * (if requested with `NRF.ctsGetTime` and is activated by calling `NRF.setServices(..., {..., cts:true})`) + * ``` + * { + * date : // Date object with the current date + * day : // if known, 0=sun,1=mon (matches JS `Date`) + * reason : [ // reason for the date change + * "external", // External time change + * "manual", // Manual update + * "timezone", // Timezone changed + * "DST", // Daylight savings + * ] + * timezone // if LTI characteristic exists, this is the timezone + * dst // if LTI characteristic exists, this is the dst adjustment + * } + * ``` + * For instance this can be used as follows to update Espruino's time: + * ``` + * E.on('CTS',e=>{ + * setTime(e.date.getTime()/1000); + * }); + * NRF.ctsGetTime(); // also returns a promise with CTS info + * ``` + * @param {string} event - The event to listen to. + * @param {(info: any) => void} callback - A function that is executed when the event occurs. Its arguments are: + * * `info` An object (see below) + * @url http://www.espruino.com/Reference#l_NRF_CTS + */ + static on(event: "CTS", callback: (info: any) => void): void; + + /** + * Read the time from CTS - creates an `NRF.on('CTS', ...)` event as well + * ``` + * NRF.ctsGetTime(); // also returns a promise + * ``` + * @returns {any} A `Promise` that is resolved (or rejected) when time is received + * @url http://www.espruino.com/Reference#l_NRF_ctsGetTime + */ + static ctsGetTime(): Promise; + /** * Search for available devices matching the given filters. Since we have no UI * here, Espruino will pick the FIRST device it finds, or it'll call `catch`. @@ -3538,6 +3610,16 @@ declare class Bangle { */ static on(event: "lcdPower", callback: (on: boolean) => void): void; + /** + * Has the backlight been turned on or off? Can be used to stop tasks that are no + * longer useful if want to see in sun screen only. Also see `Bangle.isBacklightOn()` + * @param {string} event - The event to listen to. + * @param {(on: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are: + * * `on` `true` if backlight is on + * @url http://www.espruino.com/Reference#l_Bangle_backlight + */ + static on(event: "backlight", callback: (on: boolean) => void): void; + /** * Has the screen been locked? Also see `Bangle.isLocked()` * @param {string} event - The event to listen to. @@ -3664,6 +3746,26 @@ declare class Bangle { */ static on(event: "midnight", callback: () => void): void; + /** + * This function can be used to turn Bangle.js's LCD backlight off or on. + * This function resets the Bangle's 'activity timer' (like pressing a button or + * the screen would) so after a time period of inactivity set by + * `Bangle.setOptions({backlightTimeout: X});` the backlight will turn off. + * If you want to keep the backlight on permanently (until apps are changed) you can + * do: + * ``` + * Bangle.setOptions({backlightTimeout: 0}) // turn off the timeout + * Bangle.setBacklight(1); // keep screen on + * ``` + * Of course, the backlight depends on `Bangle.setLCDPower` too, so any lcdPowerTimeout/setLCDTimeout will + * also turn the backlight off. The use case is when you require the backlight timeout + * to be shorter than the power timeout. + * + * @param {boolean} isOn - True if the LCD backlight should be on, false if not + * @url http://www.espruino.com/Reference#l_Bangle_setBacklight + */ + static setBacklight(isOn: boolean): void; + /** * This function can be used to turn Bangle.js's LCD off or on. * This function resets the Bangle's 'activity timer' (like pressing a button or @@ -3805,7 +3907,7 @@ declare class Bangle { static setLCDTimeout(isOn: number): void; /** - * Set how often the watch should poll for new acceleration/gyro data and kick the + * Set how often the watch should poll its sensors (accel/hr/mag) for new data and kick the * Watchdog timer. It isn't recommended that you make this interval much larger * than 1000ms, but values up to 4000ms are allowed. * Calling this will set `Bangle.setOptions({powerSave: false})` - disabling the @@ -3890,6 +3992,13 @@ declare class Bangle { */ static isLCDOn(): boolean; + /** + * Also see the `Bangle.backlight` event + * @returns {boolean} Is the backlight on or not? + * @url http://www.espruino.com/Reference#l_Bangle_isBacklightOn + */ + static isBacklightOn(): boolean; + /** * This function can be used to lock or unlock Bangle.js (e.g. whether buttons and * touchscreen work or not) @@ -4361,7 +4470,8 @@ declare class Bangle { * (function() { * var sui = Bangle.setUI; * Bangle.setUI = function(mode, cb) { - * if (mode!="clock") return sui(mode,cb); + * var m = ("object"==typeof mode) ? mode.mode : mode; + * if (m!="clock") return sui(mode,cb); * sui(); // clear * Bangle.CLOCK=1; * Bangle.swipeHandler = Bangle.showLauncher; @@ -4376,10 +4486,11 @@ declare class Bangle { * mode : "custom", * back : function() {}, // optional - add a 'back' icon in top-left widget area and call this function when it is pressed , also call it when the hardware button is clicked (does not override btn if defined) * remove : function() {}, // optional - add a handler for when the UI should be removed (eg stop any intervals/timers here) - * touch : function(n,e) {}, // optional - handler for 'touch' events - * swipe : function(dir) {}, // optional - handler for 'swipe' events - * drag : function(e) {}, // optional - handler for 'drag' events (Bangle.js 2 only) - * btn : function(n) {}, // optional - handler for 'button' events (n==1 on Bangle.js 2, n==1/2/3 depending on button for Bangle.js 1) + * redraw : function() {}, // optional - add a handler to redraw the UI. Not needed but it can allow widgets/etc to provide other functionality that requires the screen to be redrawn + * touch : function(n,e) {}, // optional - (mode:custom only) handler for 'touch' events + * swipe : function(dir) {}, // optional - (mode:custom only) handler for 'swipe' events + * drag : function(e) {}, // optional - (mode:custom only) handler for 'drag' events (Bangle.js 2 only) + * btn : function(n) {}, // optional - (mode:custom only) handler for 'button' events (n==1 on Bangle.js 2, n==1/2/3 depending on button for Bangle.js 1) * clock : 0 // optional - if set the behavior of 'clock' mode is added (does not override btn if defined) * }); * ``` @@ -4387,7 +4498,7 @@ declare class Bangle { * may choose to just call the `remove` function and then load a new app without resetting Bangle.js. * As a result, **if you specify 'remove' you should make sure you test that after calling `Bangle.setUI()` * without arguments your app is completely unloaded**, otherwise you may end up with memory leaks or - * other issues when switching apps. + * other issues when switching apps. Please see http://www.espruino.com/Bangle.js+Fast+Load for more details on this. * * @param {any} type - The type of UI input: 'updown', 'leftright', 'clock', 'clockupdown' or undefined to cancel. Can also be an object (see below) * @param {any} callback - A function with one argument which is the direction @@ -6872,6 +6983,7 @@ interface DateConstructor { new(): Date; new(value: number | string): Date; new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date; + (arg?: any): string; } interface Date { @@ -8166,7 +8278,9 @@ declare class E { static toFlatString(...args: any[]): string | undefined; /** - * By default, strings in Espruino are standard 8 bit binary strings. + * By default, strings in Espruino are standard 8 bit binary strings + * unless they contain Unicode chars or a `\u####` escape code + * that doesn't map to the range 0..255. * However calling E.asUTF8 will convert one of those strings to * UTF8. * ``` @@ -8177,6 +8291,7 @@ declare class E { * u.length // 1 * u[0] // hamburger emoji * ``` + * **NOTE:** UTF8 is currently only available on Bangle.js devices * * @param {any} str - The string to turn into a UTF8 Unicode String * @returns {any} A String @@ -8184,6 +8299,34 @@ declare class E { */ static asUTF8(str: any): string; + /** + * Given a UTF8 String (see `E.asUTF8`) this returns the underlying representation + * of that String. + * ``` + * E.fromUTF8("\u03C0") == "\xCF\x80" + * ``` + * **NOTE:** UTF8 is currently only available on Bangle.js devices + * + * @param {any} str - The string to check + * @returns {any} A String + * @url http://www.espruino.com/Reference#l_E_fromUTF8 + */ + static fromUTF8(str: any): string; + + /** + * By default, strings in Espruino are standard 8 bit binary strings + * unless they contain Unicode chars or a `\u####` escape code + * that doesn't map to the range 0..255. + * This checks if a String is being treated by Espruino as a UTF8 String + * See `E.asUTF8` to convert to a UTF8 String + * **NOTE:** UTF8 is currently only available on Bangle.js devices + * + * @param {any} str - The string to check + * @returns {boolean} True if the given String is treated as UTF8 by Espruino + * @url http://www.espruino.com/Reference#l_E_isUTF8 + */ + static isUTF8(str: any): boolean; + /** * This creates a Uint8Array from the given arguments. These are handled as * follows: @@ -9120,6 +9263,26 @@ interface Object { */ on(event: any, listener: any): void; + /** + * Register an event listener for this object, for instance `Serial1.addListener('data', function(d) {...})`. + * An alias for `Object.on` + * + * @param {any} event - The name of the event, for instance 'data' + * @param {any} listener - The listener to call when this event is received + * @url http://www.espruino.com/Reference#l_Object_addListener + */ + addListener(event: any, listener: any): void; + + /** + * Register an event listener for this object, for instance `Serial1.addListener('data', function(d) {...})`. + * An alias for `Object.on` + * + * @param {any} event - The name of the event, for instance 'data' + * @param {any} listener - The listener to call when this event is received + * @url http://www.espruino.com/Reference#l_Object_prependListener + */ + prependListener(event: any, listener: any): void; + /** * Call any event listeners that were added to this object with `Object.on`, for * instance `obj.emit('data', 'Foo')`. @@ -9807,21 +9970,22 @@ declare const Promise: PromiseConstructor * This means that while `StorageFile` files exist in the same area as those from * `Storage`, they should be read using `Storage.open` (and not `Storage.read`). * ``` - * f = s.open("foobar","w"); + * f = require("Storage").open("foobar","w"); * f.write("Hell"); * f.write("o World\n"); * f.write("Hello\n"); * f.write("World 2\n"); + * f.write("Hello World 3\n"); * // there's no need to call 'close' * // then - * f = s.open("foobar","r"); + * f = require("Storage").open("foobar","r"); * f.read(13) // "Hello World\nH" * f.read(13) // "ello\nWorld 2\n" * f.read(13) // "Hello World 3" * f.read(13) // "\n" * f.read(13) // undefined * // or - * f = s.open("foobar","r"); + * f = require("Storage").open("foobar","r"); * f.readLine() // "Hello World\n" * f.readLine() // "Hello\n" * f.readLine() // "World 2\n" @@ -10255,6 +10419,7 @@ interface StringConstructor { * @url http://www.espruino.com/Reference#l_String_String */ new(...str: any[]): any; + (arg?: any): string; } interface String { @@ -10499,7 +10664,8 @@ interface RegExpConstructor { * @returns {any} A RegExp object * @url http://www.espruino.com/Reference#l_RegExp_RegExp */ - new(regex: any, flags: any): RegExp; + new(...value: any[]): RegExp; + (value: any): RegExp; } interface RegExp { @@ -10588,7 +10754,8 @@ interface NumberConstructor { * @returns {any} A Number object * @url http://www.espruino.com/Reference#l_Number_Number */ - new(...value: any[]): any; + new(...value: any[]): Number; + (value: any): number; } interface Number { @@ -10728,7 +10895,8 @@ interface BooleanConstructor { * @returns {boolean} A Boolean object * @url http://www.espruino.com/Reference#l_Boolean_Boolean */ - new(value: any): boolean; + new(...value: any[]): Number; + (value: any): boolean; } interface Boolean { @@ -13494,7 +13662,7 @@ declare module "Storage" { * List all files in the flash storage area matching the specified regex (ignores * StorageFiles), and then hash their filenames *and* file locations. * Identical files may have different hashes (e.g. if Storage is compacted and the - * file moves) but the changes of different files having the same hash are + * file moves) but the chances of different files having the same hash are * extremely small. * ``` * // Hash files From 7b7df1741ee96632daf0c6cb996540321301f096 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Thu, 27 Jul 2023 18:35:48 +0200 Subject: [PATCH 079/242] astrocalc: remove unused variable --- apps/astrocalc/astrocalc-app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 1963d784e..2e732c37a 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -11,7 +11,6 @@ const SunCalc = require("suncalc"); // from modules folder const storage = require("Storage"); -const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2 function drawMoon(phase, x, y) { const moonImgFiles = [ From 2ac83661ad1010f86371b3e6b78d162859344ea8 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 27 Jul 2023 18:10:18 +0100 Subject: [PATCH 080/242] recorder: fix WIDGETS check in clkinfo --- apps/recorder/clkinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js index acb56dfe9..162509eb6 100644 --- a/apps/recorder/clkinfo.js +++ b/apps/recorder/clkinfo.js @@ -7,7 +7,7 @@ return { name: "Bangle", - items: WIDGETS["recorder"] ? [ + items: typeof WIDGETS !== "undefined" && WIDGETS["recorder"] ? [ { name: "Toggle", get: () => WIDGETS["recorder"].isRecording() ? { From 8a2a5851be0f7474f56d5f8b0b5cbb2b305c7b44 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 27 Jul 2023 18:10:29 +0100 Subject: [PATCH 081/242] clock_info: display clkinfo load error --- apps/clock_info/ChangeLog | 3 ++- apps/clock_info/lib.js | 2 +- apps/clock_info/metadata.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog index e12b30692..870808eff 100644 --- a/apps/clock_info/ChangeLog +++ b/apps/clock_info/ChangeLog @@ -4,4 +4,5 @@ 0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps eg. when a clockinfo is selected, swipes won't affect swipe-down widgets 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 \ No newline at end of file +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 diff --git a/apps/clock_info/lib.js b/apps/clock_info/lib.js index 9dd975f1e..e6c9eb27f 100644 --- a/apps/clock_info/lib.js +++ b/apps/clock_info/lib.js @@ -141,7 +141,7 @@ exports.load = function() { if(b) b.items = b.items.concat(a.items); else menu = menu.concat(a); } catch(e){ - console.log("Could not load clock info "+E.toJS(fn)); + console.log("Could not load clock info "+E.toJS(fn)+": "+e); } }); diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json index ef9a3effa..993f112e7 100644 --- a/apps/clock_info/metadata.json +++ b/apps/clock_info/metadata.json @@ -1,7 +1,7 @@ { "id": "clock_info", "name": "Clock Info Module", "shortName": "Clock Info", - "version":"0.06", + "version":"0.07", "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", "icon": "app.png", "type": "module", From 7e28e752958b3141d03b00f92a8254ab49420683 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Thu, 27 Jul 2023 21:09:48 +0200 Subject: [PATCH 082/242] widbaroalarm: Compatibility with hideable Widgets --- apps/widbaroalarm/ChangeLog | 1 + apps/widbaroalarm/metadata.json | 2 +- apps/widbaroalarm/widget.js | 18 +++++++----------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog index 3b2ae75c4..e9adf252d 100644 --- a/apps/widbaroalarm/ChangeLog +++ b/apps/widbaroalarm/ChangeLog @@ -8,3 +8,4 @@ Only use valid pressure values 0.06: Fix exception 0.07: Ensure barometer gets turned off after a few readings (isBarometerOn broken in 2v16) +0.08: Compatibility with hideable Widgets diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json index 0a6ddd71e..ba0c02a31 100644 --- a/apps/widbaroalarm/metadata.json +++ b/apps/widbaroalarm/metadata.json @@ -2,7 +2,7 @@ "id": "widbaroalarm", "name": "Barometer Alarm Widget", "shortName": "Barometer Alarm", - "version": "0.07", + "version": "0.08", "description": "A widget that can alarm on when the pressure reaches defined thresholds.", "icon": "widget.png", "type": "widget", diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js index d65a1c09c..c7ca0eda2 100644 --- a/apps/widbaroalarm/widget.js +++ b/apps/widbaroalarm/widget.js @@ -226,7 +226,7 @@ function barometerPressureHandler(e) { medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); if (medianPressure > 0) { turnOff(); - draw(); + WIDGETS.baroalarm.draw(); handlePressureValue(medianPressure); } } @@ -253,13 +253,6 @@ function turnOff() { } function draw() { - if (global.WIDGETS != undefined && typeof global.WIDGETS === "object") { - global.WIDGETS["baroalarm"] = { - width : setting("show") ? 24 : 0, - area : "tr", - draw : draw - }; - } g.reset(); if (this.x == undefined || this.y != 0) @@ -270,9 +263,6 @@ function draw() { if (setting("show")) { g.setFont("6x8", 1).setFontAlign(1, 0); if (medianPressure == undefined) { - // trigger a new check - getPressureValue(); - // lets load last value from log (if available) if (history3.length > 0) { medianPressure = history3[history3.length - 1]["p"]; @@ -297,6 +287,12 @@ function draw() { } } +WIDGETS["baroalarm"] = { + width : setting("show") ? 24 : 0, + area : "tr", + draw : draw +}; + if (interval > 0) { setInterval(getPressureValue, interval * 60000); } From 86f4089b9091440f0959f2143df0710e3f2795b0 Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Thu, 27 Jul 2023 14:51:03 -0500 Subject: [PATCH 083/242] new app added --- apps/lunaclock/ChangeLog.txt | 1 + apps/lunaclock/README.md | 9 +++++ apps/lunaclock/app-icon.json | 1 + apps/lunaclock/app.js | 72 ++++++++++++++++++++++++++++++++++ apps/lunaclock/app.png | Bin 0 -> 2326 bytes apps/lunaclock/metadata.json | 17 ++++++++ apps/lunaclock/screenshot.png | Bin 0 -> 4177 bytes 7 files changed, 100 insertions(+) create mode 100644 apps/lunaclock/ChangeLog.txt create mode 100644 apps/lunaclock/README.md create mode 100644 apps/lunaclock/app-icon.json create mode 100644 apps/lunaclock/app.js create mode 100644 apps/lunaclock/app.png create mode 100644 apps/lunaclock/metadata.json create mode 100644 apps/lunaclock/screenshot.png diff --git a/apps/lunaclock/ChangeLog.txt b/apps/lunaclock/ChangeLog.txt new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/lunaclock/ChangeLog.txt @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/lunaclock/README.md b/apps/lunaclock/README.md new file mode 100644 index 000000000..0b7555b29 --- /dev/null +++ b/apps/lunaclock/README.md @@ -0,0 +1,9 @@ +# Luna Clock + +![](screenshot.png) + +Simple clock face inspired by the moon. + +## Creator + +NovaDawn999 \ No newline at end of file diff --git a/apps/lunaclock/app-icon.json b/apps/lunaclock/app-icon.json new file mode 100644 index 000000000..d17110a5a --- /dev/null +++ b/apps/lunaclock/app-icon.json @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A/AH4AJiIABiAVRiUz/4AB+cyDJ8TCoX/mc/AQMgCxkjCwgAB+YCBDBcDCwYXCn4CC+ZKJgQWEFYQWCA4MyC5EvC5HzA4U/JI4uEIAQWBD4k/+QuKLYZDCDwQFCGAsBIgoBBFIIGBPIf/GAqMFDIQvDHARjBSQp1EFAIpCL4YGEmBGEE4akEIYYIDPIhGBiYwDmTSEgUiAwMiAAMzJAUf+ZgD+cCCwX/iMjCQMxiczmUDSIUvmIXDmIXBmchmcTn8jmcgJAUgC4U/iLXC+ciC4UgCgMyOwMRC4UwiJ2BLAIXDAgJHBCgReCEAQXBiUACgURWoQEBdAYvBCoIXL+TDBiAWCegUgicBC4cTC4IPBkE/OoM/AgK6EBIMQC4ixBH4ISCmUSkEvBoJ3BJAR9CBIMvC4KHCl4YCiESiYXBSAUBWwIRBNgIXEkZbCmLIBRYchgUykMjAILwBgLeBkAhCLAIEBUwK3BiEikUykRJCiEAiQDBAQJYDAgRABiQVBCwcyd4MCiIHCAAMhT4YACCwIXCkUhC4MBiQgDCAQFCkYVCAAhGBC4MBEIQZEFggWHAARrCkQVELYMhMAICBiJFCC4gMBJYI0CCoInCiAlBCooAEDIIUBHwsBFwIXKDAITBQAIECBAIWMJYSBDGAYXNIAaFFACAtELZpUBHoIBBDAIZBBYJ4CEA0TawMyaQQXBmUwAQMgiYKBmIXFgTjEkMgA4QXBcgQbBNCoAEA==")) \ No newline at end of file diff --git a/apps/lunaclock/app.js b/apps/lunaclock/app.js new file mode 100644 index 000000000..e73467bc1 --- /dev/null +++ b/apps/lunaclock/app.js @@ -0,0 +1,72 @@ +var MYIMG = { + width : 176, height : 176, bpp : 4, + buffer : require("heatshrink").decompress(atob("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")) +}; + +//var IMAGEWIDTH = 176; +//var IMAGEHEIGHT = 109; +var IMAGEWIDTH = 176; +var IMAGEHEIGHT = 176; + +Graphics.prototype.setFontcustom = function() { + // Actual height 58 (57 - 0) + return this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('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'))), + 32, + atob("EA4RPC4uLQoZGhwaDxkOKCodHx4pLCMdHCMODx4mHyE0LSgrKiAfLCsZJDImPy4rJC8yHiYoLkEyLiQUFBM="), + 58|65536 + ); +}; + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function draw() { + //clock pos + var x = g.getWidth()/2; + var y = 105; + g.setColor("#000000"); //color of clock text + //draw clock + g.drawImage(MYIMG,0,g.getHeight()-IMAGEHEIGHT, 1); + //get time/date + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date).toUpperCase(); + var dowStr = require("locale").dow(date).toUpperCase(); + // draw time + g.setFontAlign(0,0).setFont("custom"); + g.drawString(timeStr,x,y); + // draw date + g.setColor(1,1,1); + var dateFormatted = dowStr + ", " + dateStr; + g.setFont("6x8"); + g.drawString(dateFormatted,g.getWidth()/2,g.getHeight() - 8); + Bangle.drawWidgets(); + // queue draw in one minute + queueDraw(); +} + + +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); +draw(); +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); + } else { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/lunaclock/app.png b/apps/lunaclock/app.png new file mode 100644 index 0000000000000000000000000000000000000000..314cf9ccab73e8e8614a5c018539669d4ff3ad00 GIT binary patch literal 2326 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 Date: Thu, 27 Jul 2023 14:59:17 -0500 Subject: [PATCH 084/242] rename file --- apps/lunaclock/{app-icon.json => app-icon.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/lunaclock/{app-icon.json => app-icon.js} (100%) diff --git a/apps/lunaclock/app-icon.json b/apps/lunaclock/app-icon.js similarity index 100% rename from apps/lunaclock/app-icon.json rename to apps/lunaclock/app-icon.js From 124e4f240cab3af4a8f4e08c21c04fc735992594 Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Thu, 27 Jul 2023 15:03:33 -0500 Subject: [PATCH 085/242] fix incorrect filename --- apps/lunaclock/{ChangeLog.txt => ChangeLog} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/lunaclock/{ChangeLog.txt => ChangeLog} (100%) diff --git a/apps/lunaclock/ChangeLog.txt b/apps/lunaclock/ChangeLog similarity index 100% rename from apps/lunaclock/ChangeLog.txt rename to apps/lunaclock/ChangeLog From 51838fc13348e4a8305ad397fd143ff0f2b326ca Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Fri, 28 Jul 2023 06:59:17 +0200 Subject: [PATCH 086/242] stopwatch touch: Fast Loading support --- apps/stopwatch/ChangeLog | 1 + apps/stopwatch/metadata.json | 2 +- apps/stopwatch/stopwatch.app.js | 118 +++++++++++++++++--------------- 3 files changed, 64 insertions(+), 57 deletions(-) diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog index c4f382aa9..cb016df1a 100644 --- a/apps/stopwatch/ChangeLog +++ b/apps/stopwatch/ChangeLog @@ -2,3 +2,4 @@ 0.02: Adjust for touch events outside of screen g dimensions 0.03: Do not register as watch, manually start clock on button 0.04: Keep running in background by saving state +0.05: Fast Loading support diff --git a/apps/stopwatch/metadata.json b/apps/stopwatch/metadata.json index bbc2dc181..27cdacb71 100644 --- a/apps/stopwatch/metadata.json +++ b/apps/stopwatch/metadata.json @@ -1,7 +1,7 @@ { "id": "stopwatch", "name": "Stopwatch Touch", - "version": "0.04", + "version": "0.05", "description": "A touch based stop watch for Bangle JS 2", "icon": "stopwatch.png", "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js index d98f06cdd..0d3ec364e 100644 --- a/apps/stopwatch/stopwatch.app.js +++ b/apps/stopwatch/stopwatch.app.js @@ -1,3 +1,4 @@ +{ const CONFIGFILE = "stopwatch.json"; const now = Date.now(); @@ -20,6 +21,7 @@ let timeY = 2*h/5; let displayInterval; let redrawButtons = true; const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size +const origTheme = g.theme; // 24 pixel images, scale to watch // 1 bit optimal, image string, no E.toArrayBuffer() @@ -27,19 +29,19 @@ const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wY const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA="); const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w=="); -function saveState() { +const saveState = function() { config.state.total = tTotal; config.state.start = tStart; config.state.current = tCurrent; config.state.running = running; require("Storage").writeJSON(CONFIGFILE, config); -} +}; -function log_debug(o) { +const log_debug = function(o) { //console.log(o); -} +}; -function timeToText(t) { +const timeToText = function(t) { let hrs = Math.floor(t/3600000); let mins = Math.floor(t/60000)%60; let secs = Math.floor(t/1000)%60; @@ -53,9 +55,9 @@ function timeToText(t) { //log_debug(text); return text; -} +}; -function drawButtons() { +const drawButtons = function() { log_debug("drawButtons()"); if (!running && tCurrent == tTotal) { bigPlayPauseBtn.draw(); @@ -65,11 +67,11 @@ function drawButtons() { } else { bigPlayPauseBtn.draw(); } - - redrawButtons = false; -} -function drawTime() { + redrawButtons = false; +}; + +const drawTime = function() { log_debug("drawTime()"); let Tt = tCurrent-tTotal; let Ttxt = timeToText(Tt); @@ -80,32 +82,32 @@ function drawTime() { g.clearRect(0, timeY - 21, w, timeY + 21); g.setColor(g.theme.fg); g.drawString(Ttxt, w/2, timeY); -} +}; -function draw() { +const draw = function() { let last = tCurrent; if (running) tCurrent = Date.now(); g.setColor(g.theme.fg); if (redrawButtons) drawButtons(); drawTime(); -} +}; -function startTimer() { +const startTimer = function() { log_debug("startTimer()"); draw(); displayInterval = setInterval(draw, 100); -} +}; -function stopTimer() { +const stopTimer = function() { log_debug("stopTimer()"); if (displayInterval) { clearInterval(displayInterval); displayInterval = undefined; } -} +}; // BTN stop start -function stopStart() { +const stopStart = function() { log_debug("stopStart()"); if (running) @@ -127,9 +129,9 @@ function stopStart() { draw(); } saveState(); -} +}; -function setButtonImages() { +const setButtonImages = function() { if (running) { bigPlayPauseBtn.setImage(pause_img); smallPlayPauseBtn.setImage(pause_img); @@ -139,10 +141,10 @@ function setButtonImages() { smallPlayPauseBtn.setImage(play_img); resetBtn.setImage(reset_img); } -} +}; // lap or reset -function lapReset() { +const lapReset = function() { log_debug("lapReset()"); if (!running && tStart != tCurrent) { redrawButtons = true; @@ -152,10 +154,10 @@ function lapReset() { draw(); } saveState(); -} +}; // simple on screen button class -function BUTTON(name,x,y,w,h,c,f,i) { +const BUTTON = function(name,x,y,w,h,c,f,i) { this.name = name; this.x = x; this.y = y; @@ -164,16 +166,16 @@ function BUTTON(name,x,y,w,h,c,f,i) { this.color = c; this.callback = f; this.img = i; -} +}; BUTTON.prototype.setImage = function(i) { this.img = i; -} +}; // if pressed the callback BUTTON.prototype.check = function(x,y) { //console.log(this.name + ":check() x=" + x + " y=" + y +"\n"); - + if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) { log_debug(this.name + ":callback\n"); this.callback(); @@ -197,48 +199,52 @@ BUTTON.prototype.draw = function() { }; -var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img); -var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img); -var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img); +const bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img); +const smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img); +const resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img); bigPlayPauseBtn.setImage(play_img); smallPlayPauseBtn.setImage(play_img); resetBtn.setImage(pause_img); +Bangle.setUI({mode:"custom", btn:() => load(), touch: (button,xy) => { + let x = xy.x; + let y = xy.y; -Bangle.on('touch', function(button, xy) { - var x = xy.x; - var y = xy.y; + // adjust for outside the dimension of the screen + // http://forum.espruino.com/conversations/371867/#comment16406025 + if (y > h) y = h; + if (y < 0) y = 0; + if (x > w) x = w; + if (x < 0) x = 0; - // adjust for outside the dimension of the screen - // http://forum.espruino.com/conversations/371867/#comment16406025 - if (y > h) y = h; - if (y < 0) y = 0; - if (x > w) x = w; - if (x < 0) x = 0; + // not running, and reset + if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return; - // not running, and reset - if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return; + // paused and hit play + if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return; - // paused and hit play - if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return; + // paused and press reset + if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return; - // paused and press reset - if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return; - - // must be running - if (running && bigPlayPauseBtn.check(x, y)) return; -}); + // must be running + if (running && bigPlayPauseBtn.check(x, y)) return; + }, remove: () => { + if (displayInterval) { + clearInterval(displayInterval); + displayInterval = undefined; + } + Bangle.removeListener('lcdPower',onLCDPower); + g.setTheme(origTheme); +}}); // Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ +const onLCDPower = (on) => { if (on) { draw(); // draw immediately, queue redraw - } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; } -}); +}; +Bangle.on('lcdPower',onLCDPower); // Clear the screen once, at startup g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); @@ -254,4 +260,4 @@ if (running) { } else { draw(); } -setWatch(() => load(), BTN, { repeat: false, edge: "falling" }); +} From a5ccaa1c34481750c56003de3f294e31b27acae7 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 28 Jul 2023 07:46:26 +0200 Subject: [PATCH 087/242] sokoban: initial release --- apps/sokoban/ChangeLog | 1 + apps/sokoban/Microban.txt | 1822 ++++++++++++++++++++++++++++++++++++ apps/sokoban/README.md | 20 + apps/sokoban/TODO | 2 + apps/sokoban/app-icon.js | 1 + apps/sokoban/app.js | 464 +++++++++ apps/sokoban/metadata.json | 21 + apps/sokoban/soko.png | Bin 0 -> 3664 bytes apps/sokoban/sokoban.png | Bin 0 -> 863 bytes 9 files changed, 2331 insertions(+) create mode 100644 apps/sokoban/ChangeLog create mode 100644 apps/sokoban/Microban.txt create mode 100644 apps/sokoban/README.md create mode 100644 apps/sokoban/TODO create mode 100644 apps/sokoban/app-icon.js create mode 100644 apps/sokoban/app.js create mode 100644 apps/sokoban/metadata.json create mode 100644 apps/sokoban/soko.png create mode 100644 apps/sokoban/sokoban.png diff --git a/apps/sokoban/ChangeLog b/apps/sokoban/ChangeLog new file mode 100644 index 000000000..9fa2c8172 --- /dev/null +++ b/apps/sokoban/ChangeLog @@ -0,0 +1 @@ +0.01: Initial code diff --git a/apps/sokoban/Microban.txt b/apps/sokoban/Microban.txt new file mode 100644 index 000000000..96146080c --- /dev/null +++ b/apps/sokoban/Microban.txt @@ -0,0 +1,1822 @@ +; 1 + +#### +# .# +# ### +#*@ # +# $ # +# ### +#### + +; 2 + +###### +# # +# #@ # +# $* # +# .* # +# # +###### + +; 3 + + #### +### #### +# $ # +# # #$ # +# . .#@ # +######### + +; 4 + +######## +# # +# .**$@# +# # +##### # + #### + +; 5 + + ####### + # # + # .$. # +## $@$ # +# .$. # +# # +######## + +; 6 + +###### ##### +# ### # +# $$ #@# +# $ #... # +# ######## +##### + +; 7 + +####### +# # +# .$. # +# $.$ # +# .$. # +# $.$ # +# @ # +####### + +; 8 + + ###### + # ..@# + # $$ # + ## ### + # # + # # +#### # +# ## +# # # +# # # +### # + ##### + +; 9 + +##### +#. ## +#@$$ # +## # + ## # + ##.# + ### + +; 10 + + ##### + #. # + #.# # +#######.# # +# @ $ $ $ # +# # # # ### +# # +######### + +; 11 + + ###### + # # + # ##@## +### # $ # +# ..# $ # +# # +# ###### +#### + +; 12 + +##### +# ## +# $ # +## $ #### + ###@. # + # .# # + # # + ####### + +; 13 + +#### +#. ## +#.@ # +#. $# +##$ ### + # $ # + # # + # ### + #### + +; 14 + +####### +# # +# # # # +#. $*@# +# ### +##### + +; 15 + + ### +######@## +# .* # +# # # +#####$# # + # # + ##### + +; 16 + + #### + # #### + # ## +## ## # +#. .# @$## +# # $$ # +# .# # +########## + +; 17 + +##### +# @ # +#...# +#$$$## +# # +# # +###### + +; 18 + +####### +# # +#. . # +# ## ## +# $ # +###$ # + #@ # + # # + #### + +; 19 + +######## +# .. # +# @$$ # +##### ## + # # + # # + # # + #### + +; 20 + +####### +# ### +# @$$..# +#### ## # + # # + # #### + # # + #### + +; 21 + +#### +# #### +# . . # +# $$#@# +## # + ###### + +; 22 + +##### +# ### +#. . # +# # # +## # # + #@$$ # + # # + # ### + #### + +; 23 + +####### +# * # +# # +## # ## + #$@.# + # # + ##### + +; 24 + +# ##### + # # +###$$@# +# ### +# # +# . . # +####### + +; 25 + + #### + # ### + # $$ # +##... # +# @$ # +# ### +##### + +; 26 + + ##### + # @ # + # # +###$ # +# ...# +# $$ # +### # + #### + +; 27 + +###### +# .# +# ## ## +# $$@# +# # # +#. ### +##### + +; 28 + +##### +# # +# @ # +# $$### +##. . # + # # + ###### + +; 29 + + ##### + # ## + # # + ###### # +## #. # +# $ $ @ ## +# ######.# +# # +########## + +; 30 + +#### +# ### +# $$ # +#... # +# @$ # +# ## +##### + +; 31 + + #### + ## # +##@$.## +# $$ # +# . . # +### # + ##### + +; 32 + + #### +## ### +# # +#.**$@# +# ### +## # + #### + +; 33 + +####### +#. # # +# $ # +#. $#@# +# $ # +#. # # +####### + +; 34 + + #### +### #### +# # +#@$***. # +# # +######### + +; 35 + + #### + ## # + #. $# + #.$ # + #.$ # + #.$ # + #. $## + # @# + ## # + ##### + +; 36 + +#### +# ############ +# $ $ $ $ $ @ # +# ..... # +############### + +; 37 + + ### +##### #.# +# ###.# +# $ #.# +# $ $ # +#####@# # + # # + ##### + +; 38 + +########## +# # +# ##.### # +# # $$ . # +# . @$## # +##### # + ###### + +; 39 + +##### +# #### +# # # .# +# $ ### +### #$. # +# #@ # +# # ###### +# # +##### + +; 40 + + ##### + # # +## ## +# $$$ # +# .+. # +####### + +; 41 + +####### +# # +#@$$$ ## +# #...# +## ## + ###### + +; 42 + + #### + # # + #@ # +####$.# +# $.# +# # $.# +# ## +###### + +; 43 + + #### + # @# + # # +###### .# +# $ .# +# $$# .# +# #### +### # + #### + +; 44 'Duh!' + +##### +#@$.# +##### + +; 45 + +###### +#... # +# $ # +# #$## +# $ # +# @ # +###### + +; 46 + + ###### +## # +# ## # +# # $ # +# * .# +## #@## + # # + ##### + +; 47 + + ####### +### # +# $ $ # +# ### ##### +# @ . . # +# ### # +##### ##### + +; 48 + +###### +# @ # +# # ## +# .# ## +# .$$$ # +# .# # +#### # + ##### + +; 49 + +###### +# @ # +# $# # +# $ # +# $ ## +### #### + # # # + #... # + # # + ####### + +; 50 + + #### +### ##### +# $ @..# +# $ # # +### #### # + # # + ######## + +; 51 + +#### +# ### +# ### +# $*@ # +### .# # + # # + ###### + +; 52 + + #### +### @# +# $ # +# *.# +# *.# +# $ # +### # + #### + +; 53 + + ##### +##. .## +# * * # +# # # +# $ $ # +## @ ## + ##### + +; 54 + + ###### + # # + ##### . # +### ###. # +# $ $ . ## +# @$$ # . # +## ##### + ###### + +; 55 + +######## +# @ # # +# # +#####$ # + # ### + ## #$ ..# + ## # ### + #### + +; 56 + +##### +# ### +# $ # +##* . # + # @# + ###### + +; 57 + + #### + # # + #@ # + # # +### #### +# * # +# $ # +#####. # + #### + +; 58 + +#### +# #### +#.*$ # +# .$# # +## @ # + # ## + ##### + +; 59 + +############ +# # +# ####### @## +# # # +# # $ # # +# $$ ##### # +### # # ...# + #### # # + ###### + +; 60 + + ######### + # # +##@##### # +# # # # +# # $.# +# ##$##.# +##$## #.# +# $ #.# +# # ### +######## + +; 61 + +######## +# # +# #### # +# #...@# +# ###$### +# # # +# $$ $ # +#### ## + #.### + ### + +; 62 + + ########## +#### ## # +# $$$....$@# +# ### # +# #### #### +##### + +; 63 + +##### #### +# ##### .# +# $ ######## +### #### .$ @ # + # # # #### # + #### #### ##### + +; 64 + + ###### +## # +# $ # +# $$ # +### .##### + ##.# @ # + #. $ # + #. #### + #### + +; 65 + + ###### + # # + # $ # + ####$ # +## $ $ # +#....# ## +# @ # +## # # + ######## + +; 66 + + ### + #@# + ###$### +## . ## +# # # # +# # # # +# # # # +# # # # +# # # # +## $ $ ## + ##. .## + # # + # # + ##### + +; 67 + +##### +# ## +# # # +#@$*.## +## . # + # $# # + ## # + ##### + +; 68 + + #### + # ###### +## $ # +# .# $ # +# .#$##### +# .@ # +###### + +; 69 + +#### #### +# #### # +# # # # +# # $## +# . .#$ # +#@ ## # $ # +# . # # +########### + +; 70 + +##### +# @ #### +# # +# $ $$ # +##$## # +# #### +# .. # +##.. # + ### # + #### + +; 71 + +########### +# # ### +# $@$ # . .# +# ## ### ## # +# # # # +# # # # # +# ######### # +# # +############# + +; 72 + + #### + ## ##### + # $ @ # + # $# # +#### ##### +# # # +# $ # +# ..# # +# .#### +# ## +#### + +; 73 + +#### +# ##### +# $$ $ # +# # +## ## ## +#...#@# +# ### ## +# # +# # # +######## + +; 74 + + #### + # ####### + #$ @# .# +## #$$ .# +# $ ##..# +# # ##### +### # + ##### + +; 75 + + ####### +## ....## +# ###### +# $ $ @# +### $ $ # + ### # + ###### + +; 76 + + ##### +## # +# ##### +# #.# # +#@ #.# $ # +# #.# ## +# # # +## ##$$# + ## # + # #### + #### + +; 77 + +########## +# @ .... # +# ####$## +## # $ $ # + # $ # + # ###### + ##### + +; 78 + + ####### +## ## +# $ $ # +# $ $ $ # +## ### #### + #@ .....# + ## ### + ####### + +; 79 + + ######### + # # # +## $#$# # +# .$.@ # +# .# # +########## + +; 80 + +#### +# ####### +# . ## .# +# $# .# +## ## # .# + # # # + #### # # + # @$ ### + # $$ # + # # + ###### + +; 81 + + ##### + # # + # . # +## * # +# *## +# @## +## $ # + # # + ##### + +; 82 + +##### +# ### +# . ## +##*#$ # +# .# $ # +# @## ## +# # +####### + +; 83 + +###### +# ## +# $ $ ## +## $$ # + # # # + # ## ## + # . .# + # @. .# + # #### + #### + +; 84 + +######## +# ... # +# ### ## +# # $ # +## #@$ # + # # $ # + # ### ##### + # # + # ### # + ##### ##### + +; 85 + + #### + ####### # + # $ # + # $ $ # + # ######## +## # . # +# # # # +# @ . ## +## # # # + # . # + ####### + +; 86 + + #### + ### ## + ## $ # +## $ # # +# @#$$ # +# .. ### +# ..### +##### + +; 87 + + #### +###### # +# # +# ... .# +##$###### +# $ # +# $### +## $ # + ## @ # + ###### + +; 88 + + #### + # ### # + # # # + # # # # + # #$ #.# + # # # # # + # #$ #.# # + # # # # +####$ #.# # +# @ # # +# # ## # +######## + +; 89 + +########## +# ## # +# $ $@# # +#### # $ # + #.# ## + # #.# $# + # #. # + # #. # + ###### + +; 90 + + ######## + # @ # + # $ $ # +### ## ### +# $..$ # +# .. # +########## + +; 91 + +########### +# .## # +# $$@..$$ # +# ##. # +########### + +; 92 + + #### + # # ##### + # # # # + # ######.# # +#### $ . # +# $$# ###.# # +# # # # # +######### #@ ## + # # + #### + +; 93 + + ######### +## # ## +# # # +# $ # $ # +# *.* # +####.@.#### +# *.* # +# $ # $ # +# # # +## # ## + ######### + +; 94 + +######### +# @ # # +# $ $ # +##$### ## +# ... # +# # # +###### # + #### + +; 95 + +######## +#@ # +# .$$. # +# $..$ # +# $..$ # +# .$$. # +# # +######## + +; 96 + + ###### + # # + # # +##### # +# #.##### +# $@$ # +#####.# # + ## ## ## + # $.# + # ### + ##### + +; 97 + + #### + # ######## +#### $ $.....# +# $ ###### +#@### ### +# $ # +# $ # # +## # # + # # + ###### + +; 98 + +##### +# ## #### +# $ ### .# +# $ $ .# +## $#####.# #### +# $ # # .### # +# # # .# @ # +### # # # + #### ## ## + ####### + +; 99 + + ##### + # # +####### ####### # # +# # # # # +# @ #### # #### +# # ....## #### # +# ##### ## $$ $ $ # +###### # # + # ########## + #### + +; 100 + +####### +# @# # +#.$ # +#. # $## +#.$# # +#. # $ # +# # # +######## + +; 101 'Lockdown' + + ##### + # # + # # ####### + # * # # + ## ## # # + # #* # +### # # # ### +# *#$+ # +# # ## ## +# # * # +####### # # + # # + ##### + +; 102 + +########### +#....# # +# # $$ # +# @ ## # +# ##$ # +###### $ # + # # + ###### + +; 103 + + ##### + # . ## +### $ # +# . $#@# +# #$ . # +# $ ### +## . # + ##### + +; 104 + + ##### +##### # +# $ # +# $#$#@# +### # # + # ... # + ### ## + # # + #### + +; 105 + + #### #### +## ### ## +# # # # +# *. .* # +###$ $### + # @ # +###$ $### +# *. .* # +# # # # +## ### ## + #### #### + +; 106 + + ######## + # # + #@ $ # +## ###$ # +# .....### +# $ $ $ # +###### # # + # # + ##### + +; 107 + +######## +# # +# $*** # +# * * # +# * * # +# ***. # +# @# +######## + +; 108 + +#### ##### +# ### # ## +# # #$ $ # +#..# ##### # # +# @ # $ $ # +#..# ## +## ######### + ##### + +; 109 + + ####### +# # # +# # # # # + # @ $ # +### ### # +# ### # +# $ ##.# +## $ #.# + ## $ .# +# ## $#.# +## ## #.# +### # # +### ##### + +; 110 + + #### + # # + # $#### +###. . # +# $ # $ # +# . .### +####$ # + # @# + #### + +; 111 + +###### +# #### +# ...# +# ...# +###### # + # # # + # $$ ## + # @$ # + # $$ # + ## $# # + # # + ###### + +; 112 + + ##### +## #### +# $$$ # +# # $ # +# $## ## +### #. # + # # # + ##### ### + # # ## + # @....# + # # + # # # + ######## + +; 113 + + ##### + ## # +### # # +# . # +# ## ##### +# . . # ## +# # @ $ ### +#####. # $ # + #### $ # + ## $ ## + # ## + # # + #### + +; 114 + +###### +# ### +# # $ # +# $ @ # +## ## ##### +# #......# +# $ $ $ $ # +## ###### + ##### + +; 115 + + ##### +##### #### +# # # +# #..... # +## ## # ### + #$$@$$$ # + # ### + ####### + +; 116 + + ##### + ### # +####.....# +# @$$$$$ # +# # ## +##### # + ##### + +; 117 + + #### #### + # ### ## + # @ # +##..### # +# # # +#...#$ # # +# ## $$ $ # +# $ ### +#### ### + #### + +; 118 + + ##### +## ## +# $ ## +# $ $ ## +###$# . ## + # # . # + ## ##. # + # @ . ## + # # # + ######## + +; 119 + + ###### + # ## + ## ## # + # $$ # # + # @$ # # + # # # +#### # # +# ... ## +# ## +####### + +; 120 + + #### +####### # +# $ ## +# $##### # +# @# # # +## ##.. # +# # ..#### +# $ ### +# $### +# # +#### + +; 121 + + ###### + # . # +##$.# # +# * # +# ..### +##$ # ##### +## ## # # +# #### # # +# @ $ $ # +## # # + ########## + +; 122 + +##### +# ### +# #$ # +# $ # +# $ $ # +# $# # +# @### +## ######## +# ...# +# # +########..# + #### + +; 123 + +######## +# # +# $ $$ ######## +##### @##. . # + #$ # . # + # #. . ## + #$# ## # # + # # + # ### ## + # # #### + #### + +; 124 + +############## +# # # +# $@$$ # . ..# +## ## ### ## # + # # # # + # # # # # + # ######### # + # # + ############# + +; 125 + + ##### + # ## + # $ # +######## #@## +# . # $ $ # +# $# # +#...##### # +##### ##### + +; 126 + + ########### +##....... # +# $$$$$$$@ # +# # # # ## +# # # # +# ####### +##### + +; 127 + +## #### +#### #### + # $ $. # +## # .$ # +# ##.### +# $ . # +# @ # # +# ###### +#### + +; 128 + + ######### +### # # +# * $ . . # +# $ ## ## +####*# # + # @ ### + # ### + ##### + +; 129 + + ######### +### @ # # +# * $ *.. # +# $ # # +####*# ### + # ## + # ### + ##### + +; 130 + +##### ##### +# ####.. # +# $$$ # +# $# .. # +### @# ## # + # ## # + ########## + +; 131 + +##### +# # +# . # +#.@.### +##.# # +# $ # +# $ # +##$$ # + # ### + # # + #### + +; 132 + +#### +# @### +#.* ##### +#..#$$ $ # +## # + # # ## # + # ##### + ##### + +; 133 + + ####### + # . .### + # . . . # +### #### # +# @$ $ # +# $$ $ # +#### ### + ##### + +; 134 + + #### +######### # +# ## $ # +# $ ## # +### #. .# ## + # #. .#$## + # # # # + # @ $ # + # ####### + #### + +; 135 + +####### +# ##### +# $$#@##..# +# # # +# $ # # # +#### $ ..# + ######## + +; 136 + + ####### + # # +## ###$## +#.$ @ # +# .. #$ # +#.## $ # +# #### +###### + +; 137 + + #### + ## ### +#### # $ # +# #### $ $ # +# ..# #$ # +# # @ ### +## #..# ### + # ## # # + # # + ######## + +; 138 + + #### +### # +# ### +# # . .# +# @ ...#### +# # # # ## +# # $$ # +##### $ $ # + ##$ # ## + # # + ###### + +; 139 + + #### +## #### +# ...# +# ...# +# # ## +# #@ #### #### +##### $ ### # + # ##$ $ # + ### $$ # + # $ ## ### + # ###### + ###### + +; 140 + +######## ##### +# # ### # +# ## $ # +#.# @ ## $ ## +#.# # $ ## +#.# $ ## +#. ## ##### +## # + ###### + +; 141 + + ######## + # # . # + # .*.# + # # * # +####$##.## +# $ # +# $ ## $ # +# @# # +########## + +; 142 + + #### + # # + # #### +###$.$ # +# .@. # +# $.$### +#### # + # # + #### + +; 143 + +#### +# #### +# $ # +# .# # +# $# ## +# . # +#### # + # # + ### ### + # $ # +## #$# ## +# $ @ $ # +# ..#.. # +### ### + ##### + +; 144 + + #### + ### ##### + # $$ # # + # $ . .$$## + # .. #. $ # +### #** . # +# . **# ### +# $ .# .. # +##$$.@. $ # + # # $$ # + ##### ### + #### + +; 145 + + ##### + # @ # + ## ## +###.$$$.### +# $...$ # +# $.#.$ # +# $...$ # +###.$$$.### + ## ## + # # + ##### + +; 146 + + ####### +## . ## +# .$$$. # +# $. .$ # +#.$ @ $.# +# $. .$ # +# .$$$. # +## . ## + ####### + +; 147 + + ##### +######## # +#. . @#.# +# ### # +## $ # # + # $ ##### + # $# # + ## # # + # ## + ##### + +; 148 'from (Original 18)' + +########### +# . # # +# #. @ # +# #..# ####### +## ## $$ $ $ # + ## # + ############# + +; 149 'from (Boxxle 43)' + + #### +## ### +#@$ # +### $ # + # ###### + # $....# + # # #### + ## # # + # $# # + # # + # ### + #### + +; 150 'from (Original 47)' + + #### + ##### # + # $####### +## ## ..# ...# +# $ $$#$ @ # +# ### # +####### # #### + #### + +; 151 'from (Original 47)' + + #### + # # + ### # +## $ # +# # # +# #$$ ###### +# # # .# +# $ @ .# +### ####..# + #### #### + +; 152 + +###### #### +# # # +#.## #$## # +# # # # +#$ # ### # # +# # # # # +# # #### # # # +#. @ $ * . # +############### + +; 153 + +############# +#.# @# # # +#.#$$ # $ # +#.# # $# # +#.# $# # $## +#.# # $# # +#.# $# # $# +#.. # $ # +#.. # # # +############ + +; 154 'Take the long way home.' + + ############################ + # # + # ######################## # + # # # # + # # #################### # # + # # # # # # + # # # ################ # # # + # # # # # # # # + # # # # ############ # # # # + # # # # # # # # # + # # # # # ############ # # # + # # # # # # # # + # # # # ################ # # + # # # # # # +##$# # #################### # +#. @ # # +############################# + +; 155 'The Dungeon' + + ###### #### +#####*# ################# ## +# ### # +# ######## #### ## # +### #### # #### #### ## +#*# # .# # # # # # # +#*# # # # ## # ## ## # +### ### ### # ## # ## ## + # # #*# # # # # + # # ### ##### #### # # + ##### ##### ####### ###### + # # # #**# # +## # # #**# ####### ## # +# ######### # ##### ### +# # # $ #*# +# ######### ### @##### #*# +##### #### #### ###### diff --git a/apps/sokoban/README.md b/apps/sokoban/README.md new file mode 100644 index 000000000..36097d66f --- /dev/null +++ b/apps/sokoban/README.md @@ -0,0 +1,20 @@ +# Sokoban + +Classic Sokoban game. + +Tap screen at bottom/top/left/right to push boxes into their destinations. +Swipe to undo. + +![Screenshot](soko.png) + +You play the yellow disk (rice hat seen from above). +Each level has a set of crates (brown if incorrectly placed or blue if correctly placed) +and a set of placeholders (empty blue squares). Simply push all crates into their placeholders. +Remember you can push but never pull. + +## Creator + +Levels are the [Microban](http://www.abelmartin.com/rj/sokobanJS/Skinner/David%20W.%20Skinner%20-%20Sokoban.htm) levels +by David W. Skinner. + +frederic.wagner@imag.fr diff --git a/apps/sokoban/TODO b/apps/sokoban/TODO new file mode 100644 index 000000000..dcad68d38 --- /dev/null +++ b/apps/sokoban/TODO @@ -0,0 +1,2 @@ +- background +- win screen + final win screen diff --git a/apps/sokoban/app-icon.js b/apps/sokoban/app-icon.js new file mode 100644 index 000000000..e8a1d4b0f --- /dev/null +++ b/apps/sokoban/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMN7oXV7vd6AuVAAIXaAwYAEC75fGC6KPFC58BiMQJxAXLiIABDAcBigXRAAIFDC5pGBAAwvOCQYbDiBfOLwgYBO54qER6QXDexIXJRggXRIxIXpb4wXMLwYvTdIinSC4gYFC5xiIC54YDC54SBIQZHRC4gcFC5hyEC4KPPLIrZGC5IWCLwgXPUApeGC5KfGC6DnGIwwXLB4gXQgI/FAwy/MAH4A/ABgA==")) diff --git a/apps/sokoban/app.js b/apps/sokoban/app.js new file mode 100644 index 000000000..90b24ed11 --- /dev/null +++ b/apps/sokoban/app.js @@ -0,0 +1,464 @@ +// basic shapes +const SPACE = 0; +const WALL = 1; +const PLAYER = 2; +const BOX = 3; +const HOLE = 4; +const FILLED = 5; + +// basic directions +const LEFT = 0; +const UP = 1; +const DOWN = 2; +const RIGHT = 3; + +function go(line, column, direction) { + let destination_line = line; + let destination_column = column; + if (direction == LEFT) { + destination_column -= 1; + } else if (direction == RIGHT) { + destination_column += 1; + } else if (direction == UP) { + destination_line -= 1; + } else { + // direction is down + destination_line += 1; + } + return [destination_line, destination_column]; +} + +Bangle.setOptions({ + lockTimeout: 60000, + backlightTimeout: 60000, +}); + +let s = require("Storage"); + +// parse the levels a bit more to figure offsets delimiting next map. +function next_map_offsets(filename, start_offset) { + let raw_maps = s.readArrayBuffer(filename); + let offsets = []; + // this is a very dumb parser : map starts three chars after the end of a line with a ';' + // and ends two chars before next ';' + let comment_line = true; + for (let i = start_offset; i < raw_maps.length; i++) { + if (raw_maps[i] == 59) { // ';' + if (offsets.length != 0) { + offsets.push(i - 2); + return offsets; + } + comment_line = true; + } else if (raw_maps[i] == 10) { // '\n' + if (comment_line) { + comment_line = false; + offsets.push(i + 3); + } + } + } + return offsets; +} + +let config = s.readJSON("sokoban.json", true); +if (config === undefined) { + let initial_offsets = next_map_offsets("Microban.txt", 0); + config = { + levels_sets: ["Microban.txt"], // all known files containing levels + levels_set: 0, // which set are we using ? + current_maps: [0], // what is current map on each set ? + offsets: [initial_offsets], // known offsets for each levels set (binary positions of maps in each file) + }; + s.writeJSON("sokoban.json", config); +} + +let map = null; +let in_menu = false; +let history = null; // store history to allow undos + + +function load_map(filename, start_offset, end_offset, name) { + console.log("loading map in", filename, "between", start_offset, "and", end_offset); + let raw_map = new Uint8Array(s.readArrayBuffer(filename), start_offset, end_offset - start_offset); + let dimensions = map_dimensions(raw_map); + history = []; + return new Map(dimensions, raw_map, filename, name); +} + +function load_current_map() { + let current_set = config.levels_set; + let offsets = config.offsets[current_set]; + let set_filename = config.levels_sets[current_set]; + let set_name = set_filename.substring(0, set_filename.length - 4); // remove '.txt' + let current_map = config.current_maps[current_set]; + map = load_map(set_filename, offsets[2 * current_map], offsets[2 * current_map + 1], set_name + " " + (current_map + 1)); + map.display(); +} + +function next_map() { + let current_set = config.levels_set; + let current_map = config.current_maps[current_set]; + let offsets = config.offsets[current_set]; + if (2 * (current_map + 1) >= offsets.length) { + // we parse some new offsets + let new_offsets = next_map_offsets(config.levels_sets[current_set], offsets[offsets.length - 1] + 2); // +2 since we need to start at ';' (we did -2 from ';' in previous parser call) + if (new_offsets.length == 0) { + E.showAlert("You Win", "All levels completed").then(function() { + load(); + }); + } else { + config.offsets[current_set].push(new_offsets[0]); + config.offsets[current_set].push(new_offsets[1]); + } + } + config.current_maps[current_set]++; + s.writeJSON("sokoban.json", config); + load_current_map(); +} + +function previous_map() { + let current_set = config.levels_set; + let current_map = config.current_maps[current_set]; + if (current_map > 0) { + current_map--; + config.current_maps[current_set] = current_map; + s.writeJSON("sokoban.json", config); + load_current_map(); + } +} + +function map_dimensions(raw_map) { + let line_start = 0; + let width = 0; + let height = 0; + for (let i = 0; i < raw_map.length; i++) { + if (raw_map[i] == 10) { + height += 1; + let line_width = i - line_start; + if (i > 0 && raw_map[i - 1] == 13) { + line_width -= 1; // remove \r + } + width = Math.max(line_width, width); + line_start = i + 1; + } + } + return [width, height]; +} + +class Map { + constructor(dimensions, raw_map, filename, name) { + this.filename = filename; + this.name = name; + this.width = dimensions[0]; + this.height = dimensions[1]; + this.remaining_holes = 0; + // start by creating an empty map + this.m = []; + for (let i = 0; i < this.height; i++) { + let line = new Uint8Array(this.width); + for (let j = 0; j < this.width; j++) { + line[j] = SPACE; + } + this.m.push(line); + } + // now fill with raw_map's content + let current_line = 0; + let line_start = 0; + for (let i = 0; i < raw_map.length; i++) { + if (raw_map[i] == 32) { + this.m[current_line][i - line_start] = SPACE; + } else if (raw_map[i] == 43) { + // '+' + this.remaining_holes += 1; + this.m[current_line][i - line_start] = HOLE; + this.player_column = i - line_start; + this.player_line = current_line; + } else if (raw_map[i] == 10) { + current_line += 1; + line_start = i + 1; + } else if (raw_map[i] == 35) { + this.m[current_line][i - line_start] = WALL; + } else if (raw_map[i] == 36) { + this.m[current_line][i - line_start] = BOX; + } else if (raw_map[i] == 46) { + this.remaining_holes += 1; + this.m[current_line][i - line_start] = HOLE; + } else if (raw_map[i] == 64) { + this.m[current_line][i - line_start] = SPACE; + this.player_column = i - line_start; + this.player_line = current_line; + } else if (raw_map[i] == 42) { + this.m[current_line][i - line_start] = FILLED; + } else if (raw_map[i] != 13) { + console.log("warning unknown map content", raw_map[i]); + } + } + this.steps = 0; + this.calibrate(); + } + // compute scale + calibrate() { + let r = Bangle.appRect; + let rwidth = 1 + r.x2 - r.x; + let rheight = 1 + r.y2 - r.y; + let cell_width = Math.floor(rwidth / this.width); + let cell_height = Math.floor(rheight / this.height); + let cell_scale = Math.min(cell_width, cell_height); // we want square cells + let real_width = this.width * cell_scale; + let real_height = this.height * cell_scale; + let sx = r.x + Math.ceil((rwidth - real_width) / 2); + let sy = r.y + Math.ceil((rheight - real_height) / 2); + this.sx = sx; + this.sy = sy; + this.cell_scale = cell_scale; + } + undo(direction, pushing) { + this.steps -= 1; + + let previous_position = go(this.player_line, this.player_column, 3 - direction); + let previous_line = previous_position[0]; + let previous_column = previous_position[1]; + + if (pushing) { + // put the box back on current player position + let currently_on = this.m[this.player_line][this.player_column]; + if (currently_on == HOLE) { + this.remaining_holes -= 1; + this.m[this.player_line][this.player_column] = FILLED; + } else { + this.m[this.player_line][this.player_column] = BOX; + } + // now, remove the box from its current position + let current_box_position = go(this.player_line, this.player_column, direction); + let box_line = current_box_position[0]; + let box_column = current_box_position[1]; + let box_on = this.m[box_line][box_column]; + if (box_on == FILLED) { + this.remaining_holes += 1; + this.m[box_line][box_column] = HOLE; + } else { + this.m[box_line][box_column] = SPACE; + } + this.display_cell(box_line, box_column); + } + // cancel player display + this.display_cell(this.player_line, this.player_column); + // re-display player at previous position + this.player_line = previous_line; + this.player_column = previous_column; + this.display_player(); + } + move(direction) { + let destination_position = go(this.player_line, this.player_column, direction); + let destination_line = destination_position[0]; + let destination_column = destination_position[1]; + let destination = this.m[destination_line][destination_column]; + let pushing = false; + if (destination == BOX || destination == SPACE || destination == HOLE || destination == FILLED) { + if (destination == BOX || destination == FILLED) { + pushing = true; + let after_line = 2 * destination_line - this.player_line; + let after_column = 2 * destination_column - this.player_column; + let after = this.m[after_line][after_column]; + let will_remain = SPACE; + if (destination == FILLED) { + will_remain = HOLE; + } + if (after == SPACE) { + if (will_remain == HOLE) { + this.remaining_holes += 1; + } + this.m[destination_line][destination_column] = will_remain; + this.m[after_line][after_column] = BOX; + } else if (after == HOLE) { + this.m[destination_line][destination_column] = will_remain; + this.m[after_line][after_column] = FILLED; + if (will_remain == SPACE) { + this.remaining_holes -= 1; + } + if (this.remaining_holes == 0) { + in_menu = true; + this.steps += 1; + E.showAlert("" + this.steps + "steps", "You Win").then(function() { + in_menu = false; + next_map(); + }); + return; + } + } else { + return; + } + this.display_cell(after_line, after_column); + this.display_cell(destination_line, destination_column); + } + history.push([direction, pushing]); + this.display_cell(this.player_line, this.player_column); + this.steps += 1; + this.player_line = destination_line; + this.player_column = destination_column; + this.display_player(); + // this.display(); + } + } + display_player() { + sx = this.sx; + sy = this.sy; + cell_scale = this.cell_scale; + g.setColor(0.8, 0.8, 0).fillCircle(sx + (0.5 + this.player_column) * cell_scale, sy + (0.5 + this.player_line) * cell_scale, cell_scale / 2 - 1); // -1 because otherwise it overfills + } + display_cell(line, column) { + sx = this.sx; + sy = this.sy; + cell_scale = this.cell_scale; + let shape = this.m[line][column]; + if (shape == WALL) { + if (cell_scale < 10) { + g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale); + } else { + g.setColor(0.5, 0.5, 0.5).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale); + g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 0.35) * cell_scale, sy + (line + 0.45) * cell_scale); + g.fillRect(sx + (column + 0.55) * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.45) * cell_scale); + g.fillRect(sx + column * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 0.65) * cell_scale, sy + (line + 0.95) * cell_scale); + g.fillRect(sx + (column + 0.85) * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.95) * cell_scale); + } + } else if (shape == BOX) { + let border = Math.floor((cell_scale - 2) / 4); + if (border > 0) { + g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border); + } else { + g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + } + } else if (shape == HOLE) { + g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + g.setColor(0, 0, 1).drawRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + } else if (shape == FILLED) { + let border = Math.floor((cell_scale - 2) / 4); + if (border > 0) { + g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border); + } else { + g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border); + + } + } else if (shape == SPACE) { + g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + } + + } + display() { + g.clear(); + for (let line = 0; line < this.height; line++) { + for (let column = 0; column < this.width; column++) { + this.display_cell(line, column); + } + } + this.display_player(); + g.setColor(0, 0, 0).setFont("6x8:2") + .setFontAlign(0, -1, 0) + .drawString(map.name, g.getWidth() / 2, 0); + } +} + + +Bangle.on('touch', function(button, xy) { + if (in_menu) { + return; + } + let half_width = g.getWidth() / 2; + let half_height = g.getHeight() / 2; + let directions_amplitudes = [0, 0, 0, 0]; + directions_amplitudes[LEFT] = half_width - xy.x; + directions_amplitudes[RIGHT] = xy.x - half_width; + directions_amplitudes[UP] = half_height - xy.y; + directions_amplitudes[DOWN] = xy.y - half_height; + + let max_direction; + let second_max_direction; + if (directions_amplitudes[0] > directions_amplitudes[1]) { + max_direction = 0; + second_max_direction = 1; + } else { + max_direction = 1; + second_max_direction = 0; + } + for (let direction = 2; direction < 4; direction++) { + if (directions_amplitudes[direction] > directions_amplitudes[max_direction]) { + second_max_direction = max_direction; + max_direction = direction; + } else if (directions_amplitudes[direction] >= directions_amplitudes[second_max_direction]) { + second_max_direction = direction; + } + } + if (directions_amplitudes[max_direction] - directions_amplitudes[second_max_direction] > 10) { + // if there is little possible confusions between two candidate moves let's move. + // basically we forbid diagonals of 10 pixels wide + map.move(max_direction); + } + +}); + +Bangle.on('swipe', function(directionLR, directionUD) { + if (in_menu) { + return; + } + let last_move = history.pop(); + if (last_move !== undefined) { + map.undo(last_move[0], last_move[1]); + } +}); + +setWatch( + function() { + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { + title: "choose action" + }, + "restart": function() { + E.showMenu(); + load_current_map(); + in_menu = false; + }, + "current map": { + value: config.current_maps[config.levels_set] + 1, + min: 1, + max: config.offsets[config.levels_set].length / 2, + onchange: (v) => { + config.current_maps[config.levels_set] = v - 1; + load_current_map(); + s.writeJSON("sokoban.json", config); + } + }, + "next map": function() { + E.showMenu(); + next_map(); + in_menu = false; + }, + "previous map": function() { + E.showMenu(); + previous_map(); + in_menu = false; + }, + "back to game": function() { + E.showMenu(); + g.clear(); + map.display(); + in_menu = false; + }, + }; + E.showMenu(menu); + }, + BTN1, { + repeat: true + } +); + + +Bangle.setLocked(false); + +current_map = config.current_map; +offsets = config.offsets; +load_current_map(); \ No newline at end of file diff --git a/apps/sokoban/metadata.json b/apps/sokoban/metadata.json new file mode 100644 index 000000000..191cea1e0 --- /dev/null +++ b/apps/sokoban/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "sokoban", + "name": "Sokoban", + "shortName": "Sokoban", + "version": "0.01", + "description": "Classic Sokoban game (microban levels).", + "allow_emulator":false, + "icon": "sokoban.png", + "type": "app", + "tags": "game", + "screenshots": [{"url":"soko.png"}], + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"sokoban.app.js","url":"app.js"}, + {"name":"Microban.txt", "url":"Microban.txt"}, + {"name":"sokoban.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"sokoban.json"} + ] +} diff --git a/apps/sokoban/soko.png b/apps/sokoban/soko.png new file mode 100644 index 0000000000000000000000000000000000000000..5bf0ae772d6f72cd3ddf6bd15fe812dbd9cb1f13 GIT binary patch literal 3664 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 CS<8?B literal 0 HcmV?d00001 diff --git a/apps/sokoban/sokoban.png b/apps/sokoban/sokoban.png new file mode 100644 index 0000000000000000000000000000000000000000..849b92d0114751cc03d97636e077d4e62c849373 GIT binary patch literal 863 zcmV-l1EBngP)-=LruQHqFqP{czKPwG_>nxyEVo|?W% zD^**9pobFjrXzwhd2d@4z4Xw6;>DukrAbAx6+GCg;0FpGL7&#cK z2I;xTBU^xMbTm{A(sPl#EkG`kcYZIb|LgGd1a$uZ)Q9Z>!gW;v@CPCr9o4guiMgk< z@ty;K>e>WILm+8NAWZ|(OGwh*Qof;k1&~NwSS}>L5|VZ60HiTM5}?!y>w7K9^XuX% zIxdj!2}zUJdlTLO2+3$?yk&eAk_e=>OGs`Ll7@w(Y4e<8r_+@Om~$QeQpD&P7hu0m zihtbj;sgNsR&m3qU!ZmZklqwCS|C3#<}|BJO~qqCNbNw;9hEc{S3O&dc}l58AUO<@ z2aVGL%3CEtl*db_Ux8X5)`1JXpH&u4Bg_-($JeG|xCM$PaAKpeYkEg5IBOf9QE+#w`=C~{@!_5}b)t<{oot`wY*Qp=!|V<72i;XJ!AO~s(l%FxyGGV=(;tAO9=FDq6`=7Hue9s50I9#V zwRdST1B`zq%$K^~4NXM>npLI-ke^umct#*6ofdDx^p4qi^D#0|it%Lw0OS!%uzq!u z1_9|}!Fwrfk0$^E`Mt^C$77OqyII^QRwh?_SgP@5189DIA literal 0 HcmV?d00001 From c64b8d2fa6fe498dbb4102dd929ef3148304f7ef Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Fri, 28 Jul 2023 08:00:22 +0200 Subject: [PATCH 088/242] taglaunch: Fix lockhandler --- apps/taglaunch/app.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js index cfe3f7819..eab9f7235 100644 --- a/apps/taglaunch/app.js +++ b/apps/taglaunch/app.js @@ -79,6 +79,17 @@ const unload = () => { Bangle.removeListener("lock", lockHandler); }; +// 10s of inactivity goes back to clock +Bangle.setLocked(false); // unlock initially +let lockTimeout; +let lockHandler = function(locked) { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) { + lockTimeout = setTimeout(Bangle.showClock, 10000); + } +}; + let showTagMenu = (tag) => { E.showScroller({ h : 64*scaleval, c : appsByTag[tag].length, @@ -105,6 +116,7 @@ let showTagMenu = (tag) => { back : showMainMenu, remove: unload }); + Bangle.on("lock", lockHandler); }; let showMainMenu = () => { @@ -127,21 +139,11 @@ let showMainMenu = () => { back : Bangle.showClock, // button press or tap in top left shows clock now remove : unload }); + Bangle.on("lock", lockHandler); }; showMainMenu(); g.flip(); // force a render before widgets have finished drawing -// 10s of inactivity goes back to clock -Bangle.setLocked(false); // unlock initially -let lockTimeout; -let lockHandler = function(locked) { - if (lockTimeout) clearTimeout(lockTimeout); - lockTimeout = undefined; - if (locked) { - lockTimeout = setTimeout(Bangle.showClock, 10000); - } -}; -Bangle.on("lock", lockHandler); if (!settings.fullscreen) // finally draw widgets Bangle.drawWidgets(); } From f96988a53e5b436f2563d948102461958a72ef3b Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Fri, 28 Jul 2023 08:19:28 +0200 Subject: [PATCH 089/242] taglaunch: move icons to functions --- apps/taglaunch/app.js | 19 +++++++++---------- apps/taglaunch/health-icon.js | 1 - apps/taglaunch/metadata.json | 3 +-- 3 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 apps/taglaunch/health-icon.js diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js index eab9f7235..9569cc7bd 100644 --- a/apps/taglaunch/app.js +++ b/apps/taglaunch/app.js @@ -1,15 +1,14 @@ { // must be inside our own scope here so that when we are unloaded everything disappears let s = require("Storage"); -// TODO: Move icons to separate files -// TODO: Allow change sortorder in settings -let tags = {"clock": {name: /*LANG*/"Clocks", icon: atob("MDCEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERESIiIiIREREREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERESIiIiIgz//8ziIiIiIREREREREREREREiIiIg////////ziIiIhERERERERERERIiIiD//////////84iIiERERERERERESIiIg/////8A/////ziIiIRERERERERESIiI//////8A//////+IiIREREREREREiIiP//////8A///////4iIhERERERERIiIg///////8A///////ziIiERERERERIiIP///////8A////////OIiERERERESIiI////////8A////////+IiIRERERESIiD////////8A////////84iIRERERESIiP////////8A/////////4iIRERERESIiP////////8A/////////4iIREREREiIg/////////8A/////////ziIhEREREiIg/////////IAL////////ziIhEREREiIj////////yAAAv////////iIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIj////////yAAAD////////iIhEREREiIg/////////IAABP//////ziIhEREREiIg///////////IAE//////ziIhERERESIiP//////////8gAT/////4iIRERERESIiP///////////yABP////4iIRERERESIiD////////////IAL///84iIRERERESIiI////////////8i////+IiIRERERERIiIP/////////////////OIiERERERERIiIg////////////////ziIiEREREREREiIiP///////////////4iIhERERERERESIiI//////////////+IiIRERERERERESIiIg/////8i/////ziIiIRERERERERERIiIiD////8i////84iIiEREREREREREREiIiIg///8i///ziIiIhERERERERERERESIiIiIgz8i8ziIiIiIREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERERERESIiIiIRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA==")}, - "game": {name: /*LANG*/"Games", sortorder: 1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzIiIiIiIiIiIiIiIiIiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8RMxERABEAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IhERERERAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzL///IiIi////IiIi///xERAAAAAAAAAzL///IiIi////IiIi//8xERAAAAAAAAAzL///IiIi////IiIi//8hEREAAAAAAAAzL///IiIi////IiIi//8REREAAAAAAAAzL///IiIi////IiIi//MREREAAAAAAAAzIiIiIiIiIiIiIiIiIzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMhEREREREAAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, - "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, - "bluetooth": {name: /*LANG*/"Bluetooth", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, - "outdoors": {name: /*LANG*/"Outdoor", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, - "misc": {name: /*LANG*/"Misc", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, - "health": {name: /*LANG*/"Health"}, +// TODO: Allow to change sortorder in settings +let tags = {"clock": {name: /*LANG*/"Clocks", icon: () => atob("MDCEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERESIiIiIREREREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERESIiIiIgz//8ziIiIiIREREREREREREREiIiIg////////ziIiIhERERERERERERIiIiD//////////84iIiERERERERERESIiIg/////8A/////ziIiIRERERERERESIiI//////8A//////+IiIREREREREREiIiP//////8A///////4iIhERERERERIiIg///////8A///////ziIiERERERERIiIP///////8A////////OIiERERERESIiI////////8A////////+IiIRERERESIiD////////8A////////84iIRERERESIiP////////8A/////////4iIRERERESIiP////////8A/////////4iIREREREiIg/////////8A/////////ziIhEREREiIg/////////IAL////////ziIhEREREiIj////////yAAAv////////iIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIj////////yAAAD////////iIhEREREiIg/////////IAABP//////ziIhEREREiIg///////////IAE//////ziIhERERESIiP//////////8gAT/////4iIRERERESIiP///////////yABP////4iIRERERESIiD////////////IAL///84iIRERERESIiI////////////8i////+IiIRERERERIiIP/////////////////OIiERERERERIiIg////////////////ziIiEREREREREiIiP///////////////4iIhERERERERESIiI//////////////+IiIRERERERERESIiIg/////8i/////ziIiIRERERERERERIiIiD////8i////84iIiEREREREREREREiIiIg///8i///ziIiIhERERERERERERESIiIiIgz8i8ziIiIiIREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERERERESIiIiIRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA==")}, + "game": {name: /*LANG*/"Games", sortorder: 1, icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzIiIiIiIiIiIiIiIiIiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8RMxERABEAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IhERERERAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzL///IiIi////IiIi///xERAAAAAAAAAzL///IiIi////IiIi//8xERAAAAAAAAAzL///IiIi////IiIi//8hEREAAAAAAAAzL///IiIi////IiIi//8REREAAAAAAAAzL///IiIi////IiIi//MREREAAAAAAAAzIiIiIiIiIiIiIiIiIzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMhEREREREAAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, + "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, + "bluetooth": {name: /*LANG*/"Bluetooth", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, + "outdoors": {name: /*LANG*/"Outdoor", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, + "misc": {name: /*LANG*/"Misc", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")}, + "health": {name: /*LANG*/"Health", icon: () => require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA=="))}, }; // handle customised launcher @@ -127,7 +126,7 @@ let showMainMenu = () => { g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); g.setFont(font).setFontAlign(-1,0).drawString(tags[tag].name,64*scaleval,r.y+(32*scaleval)); - const img = tags[tag].icon ? tags[tag].icon : s.read("taglaunch." + tag + ".img"); + const img = tags[tag].icon ? tags[tag].icon() : s.read("taglaunch." + tag + ".img"); if (img) { try {g.drawImage(img,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} } diff --git a/apps/taglaunch/health-icon.js b/apps/taglaunch/health-icon.js deleted file mode 100644 index 11b513b72..000000000 --- a/apps/taglaunch/health-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA==")) diff --git a/apps/taglaunch/metadata.json b/apps/taglaunch/metadata.json index fe48d7492..a4fb4ef6c 100644 --- a/apps/taglaunch/metadata.json +++ b/apps/taglaunch/metadata.json @@ -12,8 +12,7 @@ "screenshots": [ {"url":"screenshot.png"} ], "storage": [ {"name":"taglaunch.app.js","url":"app.js"}, - {"name":"taglaunch.settings.js","url":"settings.js"}, - {"name":"taglaunch.health.img","url":"health-icon.js","evaluate":true} + {"name":"taglaunch.settings.js","url":"settings.js"} ], "data": [{"name":"taglaunch.json"},{"name":"taglaunch.cache.json"}] } From 97fa54c2d6131af7c1a231993df6a667c39e9827 Mon Sep 17 00:00:00 2001 From: Simon Sievert Date: Thu, 27 Jul 2023 12:04:24 +0200 Subject: [PATCH 090/242] fastload: fix caching broken since e2ba3ca64 (fastload - Replace E.CRC32 with Storage.hash, 2023-07-24) Storage.hash() expects a regex (or apparently the name as a string) to match files that should be hashed, but was called with the contents of the file --- apps/fastload/ChangeLog | 1 + apps/fastload/boot.js | 7 ++++--- apps/fastload/metadata.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog index 053799a9f..6581e5188 100644 --- a/apps/fastload/ChangeLog +++ b/apps/fastload/ChangeLog @@ -3,3 +3,4 @@ 0.03: Allow hiding the fastloading info screen 0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app). 0.05: Check for changes in setting.js and force real reload if needed +0.06: Fix caching whether an app is fastloadable diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js index 4ac8b97e9..57bc8ea94 100644 --- a/apps/fastload/boot.js +++ b/apps/fastload/boot.js @@ -30,12 +30,13 @@ let appFastloadPossible = function(n){ // no widgets, no problem if (!global.WIDGETS) return true; - let app = s.read(n); - if (cache[n] && s.hash(app) == cache[n].hash) + let hash = s.hash(n); + if (cache[n] && hash == cache[n].hash) return cache[n].fast; + let app = s.read(n); cache[n] = {}; cache[n].fast = app.includes("Bangle.loadWidgets"); - cache[n].hash = s.hash(app); + cache[n].hash = hash; s.writeJSON("fastload.cache", cache); return cache[n].fast; }; diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json index abd5b3ed5..8edd1f95b 100644 --- a/apps/fastload/metadata.json +++ b/apps/fastload/metadata.json @@ -1,7 +1,7 @@ { "id": "fastload", "name": "Fastload Utils", "shortName" : "Fastload Utils", - "version": "0.05", + "version": "0.06", "icon": "icon.png", "description": "Enable experimental fastloading for more apps", "type":"bootloader", From d8c6dbe03b9d06dbf0327a7cdd9a5d1e6d5ae2d7 Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Fri, 28 Jul 2023 21:13:48 -0500 Subject: [PATCH 091/242] stacker: new app --- apps/stacker/ChangeLog | 1 + apps/stacker/README.md | 12 ++++ apps/stacker/app-icon.js | 1 + apps/stacker/app.js | 140 +++++++++++++++++++++++++++++++++++++ apps/stacker/app.png | Bin 0 -> 186 bytes apps/stacker/metadata.json | 14 ++++ 6 files changed, 168 insertions(+) create mode 100644 apps/stacker/ChangeLog create mode 100644 apps/stacker/README.md create mode 100644 apps/stacker/app-icon.js create mode 100644 apps/stacker/app.js create mode 100644 apps/stacker/app.png create mode 100644 apps/stacker/metadata.json diff --git a/apps/stacker/ChangeLog b/apps/stacker/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/stacker/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/stacker/README.md b/apps/stacker/README.md new file mode 100644 index 000000000..5be5b7bee --- /dev/null +++ b/apps/stacker/README.md @@ -0,0 +1,12 @@ +# Stacker + +A simple game of stacking cubes. + + +## Usage + +Press the button to stack! + +## Creator + +NovaDawn999 diff --git a/apps/stacker/app-icon.js b/apps/stacker/app-icon.js new file mode 100644 index 000000000..37130b23f --- /dev/null +++ b/apps/stacker/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIcZh////AAoMIAQNAAq8D//IDrQFFn//DrcH/+IDqx9DAQIADwEAggOBkAF/AuJ9FA==")) \ No newline at end of file diff --git a/apps/stacker/app.js b/apps/stacker/app.js new file mode 100644 index 000000000..bf3be0907 --- /dev/null +++ b/apps/stacker/app.js @@ -0,0 +1,140 @@ +const TICKRATE = 80; +const BLOCK_SIZE = 16; +const BLOCK_COLOR_PRIMARY = "#ff0000"; +const BLOCK_COLOR_SECONDARY = "#ffff00"; +const GAMEBOARD_X = 16; +const GAMEBOARD_WIDTH = g.getWidth() - 16 - BLOCK_SIZE; +const START_X = GAMEBOARD_X; +const START_Y = g.getHeight() - BLOCK_SIZE - 1; +const START_LENGTH = 5; +var startTime = 0; +var length = 5; +var updateTimeout; +var rows = []; +var playing = true; + +class Block { + constructor(x, y, match) { + this.x = x; + this.y = y; + this.match = match; + this.show = true; + } +} + +class Row { + constructor(x, y, size, direction, match) { + this.y = y; + this.size = size; + this.blocks = []; + if (Math.random() > 0.49) { + this.direction = 1; + this.x = BLOCK_SIZE; + } + else { + this.direction = -1; + this.x = g.getWidth() - this.size * BLOCK_SIZE; + } + this.match = match; + for (var i = 0; i < size; i++) { + var b = new Block(this.x + (BLOCK_SIZE * i), this.y, this.match); + this.blocks.push(b); + } + } + update() { + this.x += BLOCK_SIZE * this.direction; + if (this.x + (this.size * BLOCK_SIZE) > GAMEBOARD_X + GAMEBOARD_WIDTH || this.x < GAMEBOARD_X) { + this.direction = -this.direction; + } + for (var i = 0; i < this.size; i++) { + this.blocks[i].x = this.x + BLOCK_SIZE * i; + } + } + draw() { + for (var i = 0; i < this.size; i++) { + if (this.blocks[i].show) { + g.drawRect({x: this.blocks[i].x, y: this.y, w: BLOCK_SIZE, h: BLOCK_SIZE}); + } + } + } +} + +function init() { + Bangle.setLCDPower(1); + g.setTheme({bg:"#000", fg:"#fff", dark:true}).clear(); + setInterval(update, TICKRATE); + setWatch(input_pressed, BTN); + var test = new Row(START_X, START_Y, length, 1, true); + rows.push(test); +} + +function update() { + "ram" + if (playing) { + g.clear(reset); + rows[rows.length - 1].update(); + rows.forEach(row => row.draw()); + } +} + +function resetGame() { + playing = true; + rows = []; + length = START_LENGTH; + var test = new Row(START_X, START_Y, length, 1, true); + rows.push(test); + update(); +} + +function lose() { + print("lose"); + g.clear(reset); + E.showMessage("YOU LOSE!"); + playing = false; + setWatch(resetGame, BTN, {repeat:0,debounce:50,edge:"rising"}); +} + +function win() { + playing = false; + setWatch(resetGame, BTN, {repeat:0,debounce:50,edge:"rising"}); + g.clear(reset); + E.showMessage("YOU WIN!"); +} + +function collapse() { + for (var i = 0; i < rows[rows.length - 1].blocks.length; i++) { + for (var j = 0; j < rows[rows.length -2].blocks.length; j++) { + if (rows[rows.length - 1].blocks[i].x === rows[rows.length - 2].blocks[j].x) { + if (rows[rows.length - 2].blocks[j].match === true) + rows[rows.length - 1].blocks[i].match = true; + } + } + } + for (var y = 0; y < rows[rows.length - 1].blocks.length; y++) { + if (rows[rows.length - 1].blocks[y].match === false) { + length -= 1; + if (length < 1) { + lose(); + playing = false; + } + rows[rows.length - 1].blocks[y].show = false; + } + } +} + +function input_pressed() { + setWatch(input_pressed, BTN); + if (playing) { + if (rows.length > 1) { + collapse(); + if (rows[rows.length - 1].y === -1) { + win(); + } + } + var r = new Row(GAMEBOARD_X + Math.round(length/2) * BLOCK_SIZE, rows[rows.length - 1].y - BLOCK_SIZE, length, 1, false); + rows.push(r); + } +} + +init(); +update(); \ No newline at end of file diff --git a/apps/stacker/app.png b/apps/stacker/app.png new file mode 100644 index 0000000000000000000000000000000000000000..35683688a97baaf421a82f92bf566c211357bce9 GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)I7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1M1^9%x0_p$%|1Z5c|1OZlS>O>_%)r2R7=#&*=dVZs3fg+QIEGl9PF|od z;KC5X$!O6%u}>&qI#1723zY{4jTP1BGMy=nAhn@3{r(=Ks ZgW6NhOX()=H-QE*c)I$ztaD0e0svU#G>8BI literal 0 HcmV?d00001 diff --git a/apps/stacker/metadata.json b/apps/stacker/metadata.json new file mode 100644 index 000000000..e19ef5e11 --- /dev/null +++ b/apps/stacker/metadata.json @@ -0,0 +1,14 @@ +{ "id": "stacker", + "name": "Stacker", + "shortName":"Stacker", + "version":"0.01", + "description": "Game of Stacking", + "icon": "app.png", + "tags": "game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"stacker.app.js","url":"app.js"}, + {"name":"stacker.img","url":"app-icon.js","evaluate":true} + ] +} From 459db44c4ab525426b75897a84b51bfd6a4da4a0 Mon Sep 17 00:00:00 2001 From: stweedo Date: Sat, 29 Jul 2023 18:02:16 -0500 Subject: [PATCH 092/242] Fix capitalization. Improve decimal handling. --- apps/rescalc/ChangeLog | 1 + apps/rescalc/app.js | 131 +++++++++++++++++++------------------ apps/rescalc/metadata.json | 2 +- 3 files changed, 71 insertions(+), 63 deletions(-) diff --git a/apps/rescalc/ChangeLog b/apps/rescalc/ChangeLog index 7b1d6baca..21ff1f6e8 100644 --- a/apps/rescalc/ChangeLog +++ b/apps/rescalc/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fixes colors not matching user input from color menu in some cases, 3 bands are now shown larger, various code improvements. 0.03: Use transparent icon with better visibility on dark backgrounds, new resistor img with darker outlines +0.04: Fix capitalization. Improve decimal handling. diff --git a/apps/rescalc/app.js b/apps/rescalc/app.js index 566809837..4debb6c5f 100644 --- a/apps/rescalc/app.js +++ b/apps/rescalc/app.js @@ -3,19 +3,19 @@ // https://icons8.com/icon/ISAVBnskZod0/resistor let colorData = { - black: { value: 0, multiplier: 1, hex: '#000' }, - brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' }, - red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' }, - orange: { value: 3, multiplier: 1000, hex: '#FF9900' }, - yellow: { value: 4, multiplier: 10000, hex: '#ff0' }, - green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' }, - blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' }, - violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' }, - grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' }, - white: { value: 9, multiplier: 1000000000, hex: '#fff' }, - gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' }, - silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' }, - none: { tolerance: 20 }, + Black: { value: 0, multiplier: 1, hex: '#000' }, + Brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' }, + Red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' }, + Orange: { value: 3, multiplier: 1000, hex: '#FF9900' }, + Yellow: { value: 4, multiplier: 10000, hex: '#ff0' }, + Green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' }, + Blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' }, + Violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' }, + Grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' }, + White: { value: 9, multiplier: 1000000000, hex: '#fff' }, + Gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' }, + Silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' }, + None: { tolerance: 20 }, }; function clearScreen() { // Except Back Button @@ -27,7 +27,7 @@ function colorBandsToResistance(colorBands) { let firstBand = colorBands[0]; let secondBand = colorBands[1]; let multiplierBand = colorBands[2]; - let toleranceBand = colorBands[3] || 'none'; // Add a default value for toleranceBand + let toleranceBand = colorBands[3] || 'None'; // Add a default value for toleranceBand let significantDigits = colorData[firstBand].value * 10 + colorData[secondBand].value; let multiplier = colorData[multiplierBand].multiplier; let resistance = significantDigits * multiplier; @@ -35,57 +35,65 @@ function colorBandsToResistance(colorBands) { return [resistance, tolerance]; } +// Function to get color bands based on resistance and tolerance function resistanceToColorBands(resistance, tolerance) { let firstDigit, secondDigit, multiplier; - if (resistance < 1) { - // The resistance is less than 1, so we need to handle this case specially - let count = 0; - while (resistance < 1) { - resistance *= 10; - count++; - } - // Now, resistance is a whole number and count is how many times we had to multiply by 10 - let resistanceStr = resistance.toString(); - firstDigit = 0; // Set the first band color to be black - secondDigit = Number(resistanceStr.charAt(0)); // Set the second band color to be the significant digit - // Use count to determine the multiplier - multiplier = count === 1 ? 0.1 : 0.01; - } else { - // Convert the resistance to a string so we can manipulate it easily - let resistanceStr = resistance.toString(); - if (resistanceStr.length === 1) { // Check if resistance is a single digit - firstDigit = 0; - secondDigit = Number(resistanceStr.charAt(0)); - multiplier = 1; // Set multiplier to 1 for single digit resistance values + let resistanceStr = resistance.toString(); + let decimalIndex = resistanceStr.indexOf('.'); + + // Handle resistance with decimal + if (decimalIndex !== -1) { + let integerDigits = resistanceStr.substring(0, decimalIndex); + let decimalDigits = resistanceStr.substring(decimalIndex + 1); + let leadingZeros = decimalDigits.match(/^0*/)[0].length; + + // If resistance is less than 1 + if (parseInt(integerDigits) === 0) { + if (leadingZeros === decimalDigits.length - 1) { + // If only one significant digit + firstDigit = 0; + secondDigit = parseInt(decimalDigits.charAt(leadingZeros)); + multiplier = 1 / Math.pow(10, leadingZeros + 1); + } else { + // If more than one significant digit + firstDigit = parseInt(decimalDigits.charAt(leadingZeros)); + secondDigit = parseInt(decimalDigits.charAt(leadingZeros + 1)); + multiplier = 1 / Math.pow(10, leadingZeros + 2); + } } else { - // Extract the first two digits from the resistance value - firstDigit = Number(resistanceStr.charAt(0)); - secondDigit = Number(resistanceStr.charAt(1)); - // Calculate the multiplier by matching it directly with the length of digits - multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1); + // If resistance is greater than 1 + firstDigit = parseInt(integerDigits.charAt(0)); + secondDigit = parseInt(decimalDigits.charAt(0)); + multiplier = 1 / Math.pow(10, decimalDigits.length); } + } else { + // Handle resistance without decimal + firstDigit = resistanceStr.length === 1 ? 0 : parseInt(resistanceStr.charAt(0)); + secondDigit = parseInt(resistanceStr.charAt(resistanceStr.length === 1 ? 0 : 1)); + multiplier = Math.pow(10, resistanceStr.length - 2); } - let firstBandEntry = Object.entries(colorData).find(function (entry) { - return entry[1].value === firstDigit; - }); - let firstBand = firstBandEntry ? firstBandEntry[1].hex : undefined; - let secondBandEntry = Object.entries(colorData).find(function (entry) { - return entry[1].value === secondDigit; - }); - let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined; - let multiplierBandEntry = Object.entries(colorData).find(function (entry) { - return entry[1].multiplier === multiplier; - }); - let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined; - let toleranceBandEntry = Object.entries(colorData).find(function (entry) { - return entry[1].tolerance === tolerance; - }); - let toleranceBand = toleranceBandEntry ? toleranceBandEntry[1].hex : undefined; - let bands = [firstBand, secondBand, multiplierBand]; + + // Generate color bands array + let bands = [ + getBandColor('value', firstDigit), + getBandColor('value', secondDigit), + getBandColor('multiplier', multiplier), + ]; + + // Add tolerance color band if provided + let toleranceBand = getBandColor('tolerance', tolerance); if (toleranceBand) bands.push(toleranceBand); return bands; } +// Helper function to get color band based on property and value +function getBandColor(property, value) { + let entry = Object.entries(colorData).find(function (entry) { + return entry[1][property] === value; + }); + return entry ? entry[1].hex : undefined; +} + function drawResistor(colorBands, tolerance) { let img = require("Storage").read("rescalc-resistor.img"); let resistorBodyWidth = 51; @@ -208,17 +216,17 @@ function drawResistance(resistance, tolerance) { // Populate colorBandMenu with colors from colorData for (let color in colorData) { if (bandNumber === 1 || bandNumber === 2) { - if (color !== 'none' && color !== 'gold' && color !== 'silver') { + if (color !== 'None' && color !== 'Gold' && color !== 'Silver') { (function (color) { - colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () { + colorBandMenu[color] = function () { setBandColor(bandNumber, color); }; })(color); } } else if (bandNumber === 3) { - if (color !== 'none') { + if (color !== 'None') { (function (color) { - colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () { + colorBandMenu[color] = function () { setBandColor(bandNumber, color); }; })(color); @@ -226,7 +234,7 @@ function drawResistance(resistance, tolerance) { } else if (bandNumber === 4) { if (colorData[color].hasOwnProperty('tolerance')) { (function (color) { - colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () { + colorBandMenu[color] = function () { setBandColor(bandNumber, color); }; })(color); @@ -407,7 +415,6 @@ function drawResistance(resistance, tolerance) { }; function showResistanceEntryMenu() { - // Update the 'Ohms' field resistanceEntryMenu['Ohms'].value = settings.resistance; resistanceEntryMenu['Ohms'].format = v => { let multipliedValue = v * (settings.multiplier || 1); diff --git a/apps/rescalc/metadata.json b/apps/rescalc/metadata.json index 473f334d7..cd2b7ea31 100644 --- a/apps/rescalc/metadata.json +++ b/apps/rescalc/metadata.json @@ -3,7 +3,7 @@ "name": "Resistor Calculator", "shortName": "Resistor Calc", "icon": "rescalc.png", - "version":"0.03", + "version":"0.04", "screenshots": [ {"url": "screenshot.png"}, {"url": "screenshot-1.png"}, From b345b1f9b59c4b51b435681043fa4699487b8a4a Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 29 Jul 2023 19:20:36 -0500 Subject: [PATCH 093/242] Save changes to date, not just time, when tapping Confirm on the event edit menu --- apps/alarm/ChangeLog | 1 + apps/alarm/app.js | 2 +- apps/alarm/metadata.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 6f306f61a..f1f8fb40e 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -44,3 +44,4 @@ 0.39: Dated event repeat option 0.40: Use substring of message when it's longer than fits the designated menu entry. 0.41: Fix a menu bug affecting alarms with empty messages. +0.42: Fix date not getting saved in event edit menu when tapping Confirm diff --git a/apps/alarm/app.js b/apps/alarm/app.js index f8ed0322e..d135f184e 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -190,7 +190,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) { }, /*LANG*/"Cancel": () => showMainMenu(), /*LANG*/"Confirm": () => { - prepareAlarmForSave(alarm, alarmIndex, time); + prepareAlarmForSave(alarm, alarmIndex, time, date); saveAndReload(); showMainMenu(); } diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index b986512bc..3c676c217 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,7 +2,7 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.41", + "version": "0.42", "description": "Set alarms and timers on your Bangle", "icon": "app.png", "tags": "tool,alarm", From f384aa244cae7eafee9136b01a3aa814a1b1965e Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 31 Jul 2023 10:49:04 +0100 Subject: [PATCH 094/242] 0.62: Fix whitelist showing as 'on' by default when it's not after 0.59 --- apps/setting/ChangeLog | 1 + apps/setting/metadata.json | 2 +- apps/setting/settings.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index d090add58..942eee11d 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -68,3 +68,4 @@ of 'Select Clock' 0.60: Moved LCD calibration to top of menu, and use 12 taps (not 8) LCD calibration will now error if the calibration is obviously wrong 0.61: Permit temporary bypass of the BLE whitelist +0.62: Fix whitelist showing as 'on' by default when it's not after 0.59 \ No newline at end of file diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index b2b19dd6b..5e0753dd2 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.61", + "version": "0.62", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index d22f28412..87aaba9d1 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -193,7 +193,7 @@ function showBLEMenu() { /*LANG*/'Whitelist': { value: ( - settings.whitelist_disabled ? /*LANG*/"off" : /*LANG*/"on" + (settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"off" : /*LANG*/"on" ) + ( settings.whitelist ? " (" + settings.whitelist.length + ")" From d0579f6689818274221dd7da1fdf5a1689446fca Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 31 Jul 2023 10:49:54 +0100 Subject: [PATCH 095/242] fix test warning --- apps/snepwatch/metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/snepwatch/metadata.json b/apps/snepwatch/metadata.json index bb12618cb..b33f22d11 100644 --- a/apps/snepwatch/metadata.json +++ b/apps/snepwatch/metadata.json @@ -14,5 +14,6 @@ {"name":"snepwatch.app.js","url":"app.js"}, {"name":"snepwatch.settings.js","url":"settings.js"}, {"name":"snepwatch.img","url":"snepwatch.img","evaluate":false} - ] + ], + "data":[{"name":"snepwatch.settings.json"}] } From 5331f7032e393798c519f039ae9b608b6c94f3d1 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Fri, 28 Jul 2023 00:42:29 +0200 Subject: [PATCH 096/242] widanclk: add todays date behind clock hands. --- apps/widanclk/ChangeLog | 1 + apps/widanclk/metadata.json | 4 ++-- apps/widanclk/widget.js | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/widanclk/ChangeLog b/apps/widanclk/ChangeLog index 337288ad2..5a3ba7b14 100644 --- a/apps/widanclk/ChangeLog +++ b/apps/widanclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New app 0.02: Clear between redraws +0.03: Add todays date behind the clock hands. diff --git a/apps/widanclk/metadata.json b/apps/widanclk/metadata.json index cd9347601..39e83a8fe 100644 --- a/apps/widanclk/metadata.json +++ b/apps/widanclk/metadata.json @@ -1,8 +1,8 @@ { "id": "widanclk", "name": "Analog clock widget", - "version": "0.02", - "description": "A simple analog clock widget that appears when not showing a fullscreen clock", + "version": "0.03", + "description": "A simple analog clock widget that appears when not showing a fullscreen clock. Todays date sits behind the clock hands.", "icon": "widget.png", "type": "widget", "tags": "widget,clock", diff --git a/apps/widanclk/widget.js b/apps/widanclk/widget.js index c58f56459..da667d29b 100644 --- a/apps/widanclk/widget.js +++ b/apps/widanclk/widget.js @@ -9,10 +9,15 @@ WIDGETS["wdanclk"]={area:"tl",width:Bangle.CLOCK?0:24,draw:function() { if (!this.width) return; // if size not right, return g.reset(); let d = new Date(); + let dd = d.getDate(); let x=this.x+12, y=this.y+12, ah = (d.getHours()+d.getMinutes()/60)*Math.PI/6, am = d.getMinutes()*Math.PI/30; g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23). + setFont("Vector:16"). + setColor(g.theme.bgH). + drawString(dd,this.x+4+10*(dd<10),this.y+5,true). + setColor(g.theme.fg). drawCircle(x, y, 11). drawLine(x,y, x+Math.sin(ah)*7, y-Math.cos(ah)*7). drawLine(x,y, x+Math.sin(am)*9, y-Math.cos(am)*9); From e3d503eff0c9bf885f0bc0348c5bf79b8124722d Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Mon, 31 Jul 2023 15:10:55 +0200 Subject: [PATCH 097/242] kbmulti:Compat with `backswipe` on newer firmwares --- apps/kbmulti/ChangeLog | 3 +++ apps/kbmulti/lib.js | 5 +++-- apps/kbmulti/metadata.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog index defae902b..28344678a 100644 --- a/apps/kbmulti/ChangeLog +++ b/apps/kbmulti/ChangeLog @@ -4,3 +4,6 @@ 0.04: Allow moving the cursor 0.05: Switch swipe directions for Caps Lock and moving cursor. 0.06: Add ability to auto-lowercase after a capital letter insertion. +0.07: Add compatability with `backswipe` app by using `Bangle.prependListener()` and `E.stopEventPropagation`- requires fw 2v19 or cutting + edge versions of 2v18. Falls back on `Bangle.on()` for backwards + compatability. diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js index 505132040..f979e4473 100644 --- a/apps/kbmulti/lib.js +++ b/apps/kbmulti/lib.js @@ -154,6 +154,7 @@ exports.input = function(options) { displayText(false); } } + E.stopEventPropagation&&E.stopEventPropagation(); } function onHelp(resolve,reject) { @@ -161,7 +162,7 @@ exports.input = function(options) { E.showPrompt( helpMessage, {title: "Help", buttons : {"Ok":true}} ).then(function(v) { - Bangle.on('swipe', onSwipe); + if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);} generateLayout(resolve,reject); layout.render(); }); @@ -208,7 +209,7 @@ exports.input = function(options) { } else { generateLayout(resolve,reject); displayText(false); - Bangle.on('swipe', onSwipe); + if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);} layout.render(); } }); diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json index 0b44b0306..210646a01 100644 --- a/apps/kbmulti/metadata.json +++ b/apps/kbmulti/metadata.json @@ -1,6 +1,6 @@ { "id": "kbmulti", "name": "Multitap keyboard", - "version":"0.06", + "version":"0.07", "description": "A library for text input via multitap/T9 style keypad", "icon": "app.png", "type":"textinput", From fbb38ebe5545c8df6a7519e937b54e526ac8dba3 Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Mon, 31 Jul 2023 10:42:10 -0500 Subject: [PATCH 098/242] stacker updated to 0.2: optimizations --- apps/stacker/ChangeLog | 3 +- apps/stacker/app.js | 89 ++++++++++++++++++-------------------- apps/stacker/metadata.json | 4 +- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/apps/stacker/ChangeLog b/apps/stacker/ChangeLog index 2286a7f70..b0d975391 100644 --- a/apps/stacker/ChangeLog +++ b/apps/stacker/ChangeLog @@ -1 +1,2 @@ -0.01: New App! \ No newline at end of file +0.01: New App! +0.02: Optimizations \ No newline at end of file diff --git a/apps/stacker/app.js b/apps/stacker/app.js index bf3be0907..a486e06cc 100644 --- a/apps/stacker/app.js +++ b/apps/stacker/app.js @@ -1,25 +1,21 @@ -const TICKRATE = 80; +const HARDWARE_VERSION = process.env.HWVERSION; +const BUTTON = HARDWARE_VERSION === 2 ? BTN : BTN2; +const TICKRATE = 69; const BLOCK_SIZE = 16; -const BLOCK_COLOR_PRIMARY = "#ff0000"; -const BLOCK_COLOR_SECONDARY = "#ffff00"; const GAMEBOARD_X = 16; const GAMEBOARD_WIDTH = g.getWidth() - 16 - BLOCK_SIZE; -const START_X = GAMEBOARD_X; const START_Y = g.getHeight() - BLOCK_SIZE - 1; -const START_LENGTH = 5; -var startTime = 0; -var length = 5; +const START_LENGTH = 4; +var length; var updateTimeout; var rows = []; -var playing = true; +var gameState = ""; //win, lose, play -class Block { - constructor(x, y, match) { - this.x = x; - this.y = y; - this.match = match; - this.show = true; - } +function Block (x, y, match) { + this.x = x; + this.y = y; + this.match = match; + this.show = true; } class Row { @@ -59,49 +55,47 @@ class Row { } } + + function init() { Bangle.setLCDPower(1); g.setTheme({bg:"#000", fg:"#fff", dark:true}).clear(); setInterval(update, TICKRATE); - setWatch(input_pressed, BTN); - var test = new Row(START_X, START_Y, length, 1, true); - rows.push(test); + setWatch(handleInput, BUTTON, {repeat:true}); + changeState("play"); } function update() { "ram" - if (playing) { + if (gameState === "play") { g.clear(reset); rows[rows.length - 1].update(); rows.forEach(row => row.draw()); + g.flip(); } } -function resetGame() { - playing = true; - rows = []; - length = START_LENGTH; - var test = new Row(START_X, START_Y, length, 1, true); - rows.push(test); - update(); -} - -function lose() { - print("lose"); +function changeState(gs) { + gameState = gs; g.clear(reset); - E.showMessage("YOU LOSE!"); - playing = false; - setWatch(resetGame, BTN, {repeat:0,debounce:50,edge:"rising"}); -} - -function win() { - playing = false; - setWatch(resetGame, BTN, {repeat:0,debounce:50,edge:"rising"}); - g.clear(reset); - E.showMessage("YOU WIN!"); + switch(gameState) { + case "win": + E.showMessage("YOU WIN!"); + break; + case "lose": + E.showMessage("YOU LOSE!"); + break; + case "play": + rows = []; + length = START_LENGTH; + var first = new Row(GAMEBOARD_X, START_Y, length, 1, true); + rows.push(first); + break; + } } function collapse() { + "ram" for (var i = 0; i < rows[rows.length - 1].blocks.length; i++) { for (var j = 0; j < rows[rows.length -2].blocks.length; j++) { if (rows[rows.length - 1].blocks[i].x === rows[rows.length - 2].blocks[j].x) { @@ -114,21 +108,22 @@ function collapse() { if (rows[rows.length - 1].blocks[y].match === false) { length -= 1; if (length < 1) { - lose(); - playing = false; + changeState("lose"); } rows[rows.length - 1].blocks[y].show = false; } } } -function input_pressed() { - setWatch(input_pressed, BTN); - if (playing) { +function handleInput() { + if (gameState === "win" || gameState === "lose") { + changeState("play"); + } + else { if (rows.length > 1) { collapse(); - if (rows[rows.length - 1].y === -1) { - win(); + if (rows[rows.length - 1].y <= -1) { + changeState("win"); } } var r = new Row(GAMEBOARD_X + Math.round(length/2) * BLOCK_SIZE, rows[rows.length - 1].y - BLOCK_SIZE, length, 1, false); diff --git a/apps/stacker/metadata.json b/apps/stacker/metadata.json index e19ef5e11..abaf49a6d 100644 --- a/apps/stacker/metadata.json +++ b/apps/stacker/metadata.json @@ -1,11 +1,11 @@ { "id": "stacker", "name": "Stacker", "shortName":"Stacker", - "version":"0.01", + "version":"0.02", "description": "Game of Stacking", "icon": "app.png", "tags": "game", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"stacker.app.js","url":"app.js"}, From f4c25b9db3964880386782405fcfd9ba7efb61cc Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Mon, 31 Jul 2023 19:46:44 +0200 Subject: [PATCH 099/242] dragboard: Compat w backswipe on newer firmwares --- apps/dragboard/ChangeLog | 3 +++ apps/dragboard/lib.js | 50 +++++++++++++++++++++--------------- apps/dragboard/metadata.json | 2 +- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index faf3d2d33..86b13f288 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -5,3 +5,6 @@ 0.05: Now scrolls text when string gets longer than screen width. 0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present. 0.07: Settings for display colors +some characters. +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. diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 83aae5f14..66b296ffc 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -103,26 +103,7 @@ exports.input = function(options) { initDraw(); //setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise. - function changeCase(abcHL) { - if (settings.uppercase) return; - g.setColor(BGCOLOR); - g.setFontAlign(-1, -1, 0); - g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); - if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase(); - else ABC = ABC.toUpperCase(); - g.setColor(ABCCOLOR); - g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); - } - return new Promise((resolve,reject) => { - // Interpret touch input - Bangle.setUI({ - mode: 'custom', - back: ()=>{ - Bangle.setUI(); - g.clearRect(Bangle.appRect); - resolve(text); - }, - drag: function(event) { + let dragHandlerDB = function(event) { "ram"; // ABCDEFGHIJKLMNOPQRSTUVWXYZ // Choose character by draging along red rectangle at bottom of screen @@ -243,7 +224,34 @@ exports.input = function(options) { } } } - } + }; + + let catchSwipe = (_,__)=>{ + E.stopEventPropagation&&E.stopEventPropagation(); + }; + + function changeCase(abcHL) { + if (settings.uppercase) return; + g.setColor(BGCOLOR); + g.setFontAlign(-1, -1, 0); + g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); + if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase(); + else ABC = ABC.toUpperCase(); + g.setColor(ABCCOLOR); + g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); + } + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up). + g.clearRect(Bangle.appRect); + resolve(text); + }, + drag: dragHandlerDB, }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. }); }; diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index 58de5153c..5c52d9389 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -1,6 +1,6 @@ { "id": "dragboard", "name": "Dragboard", - "version":"0.07", + "version":"0.08", "description": "A library for text input via swiping keyboard", "icon": "app.png", "type":"textinput", From c59262c4bb24060f0cf02a3be797ce5d8ed76eec Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Mon, 31 Jul 2023 20:21:41 +0200 Subject: [PATCH 100/242] dragboard: autoindent lib.js and fix ChangeLog --- apps/dragboard/ChangeLog | 1 - apps/dragboard/lib.js | 226 +++++++++++++++++++-------------------- 2 files changed, 113 insertions(+), 114 deletions(-) diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index 86b13f288..d147a623b 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -5,6 +5,5 @@ 0.05: Now scrolls text when string gets longer than screen width. 0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present. 0.07: Settings for display colors -some characters. 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. diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 66b296ffc..18b7f2c6f 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -104,127 +104,127 @@ exports.input = function(options) { //setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise. let dragHandlerDB = function(event) { - "ram"; - // ABCDEFGHIJKLMNOPQRSTUVWXYZ - // Choose character by draging along red rectangle at bottom of screen - if (event.y >= ( (R.y+R.h) - 12 )) { - // Translate x-position to character - if (event.x < ABCPADDING) { abcHL = 0; } - else if (event.x >= 176-ABCPADDING) { abcHL = 25; } - else { abcHL = Math.floor((event.x-ABCPADDING)/6); } + "ram"; + // ABCDEFGHIJKLMNOPQRSTUVWXYZ + // Choose character by draging along red rectangle at bottom of screen + if (event.y >= ( (R.y+R.h) - 12 )) { + // Translate x-position to character + if (event.x < ABCPADDING) { abcHL = 0; } + else if (event.x >= 176-ABCPADDING) { abcHL = 25; } + else { abcHL = Math.floor((event.x-ABCPADDING)/6); } - // Datastream for development purposes - //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev)); + // Datastream for development purposes + //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev)); - // Unmark previous character and mark the current one... - // Handling switching between letters and numbers/punctuation - if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + // Unmark previous character and mark the current one... + // Handling switching between letters and numbers/punctuation + if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - if (abcHL != abcHLPrev) { - resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2); - } - // Print string at top of screen - if (event.b == 0) { - text = text + ABC.charAt(abcHL); - updateTopString(); + if (abcHL != abcHLPrev) { + resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2); + } + // Print string at top of screen + if (event.b == 0) { + text = text + ABC.charAt(abcHL); + updateTopString(); - // Autoswitching letter case - if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL); - } - // Update previous character to current one - abcHLPrev = abcHL; - typePrev = 'abc'; + // Autoswitching letter case + if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL); + } + // Update previous character to current one + abcHLPrev = abcHL; + typePrev = 'abc'; + } + + // 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 ))) { + // Translate x-position to character + if (event.x < NUMPADDING) { numHL = 0; } + else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; } + else { numHL = Math.floor((event.x-NUMPADDING)/6); } + + // Datastream for development purposes + //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev)); + + // Unmark previous character and mark the current one... + // Handling switching between letters and numbers/punctuation + if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + + if (numHL != numHLPrev) { + resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4); + } + // Print string at top of screen + if (event.b == 0) { + g.setColor(HLCOLOR); + // Backspace if releasing before list of numbers/punctuation + if (event.x < NUMPADDING) { + // show delete sign + showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4); + delSpaceLast = 1; + text = text.slice(0, -1); + updateTopString(); + //print(text); } - - // 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 ))) { - // Translate x-position to character - if (event.x < NUMPADDING) { numHL = 0; } - else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; } - else { numHL = Math.floor((event.x-NUMPADDING)/6); } - - // Datastream for development purposes - //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev)); - - // Unmark previous character and mark the current one... - // Handling switching between letters and numbers/punctuation - if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - - if (numHL != numHLPrev) { - resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4); - } - // Print string at top of screen - if (event.b == 0) { - g.setColor(HLCOLOR); - // Backspace if releasing before list of numbers/punctuation - if (event.x < NUMPADDING) { - // show delete sign - showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4); - delSpaceLast = 1; - text = text.slice(0, -1); - updateTopString(); - //print(text); - } - // Append space if releasing after list of numbers/punctuation - else if (event.x > (R.x+R.w)-NUMPADDING) { - //show space sign - showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4); - delSpaceLast = 1; - text = text + ' '; - updateTopString(); - //print(text); - } - // Append selected number/punctuation - else { - text = text + NUMHIDDEN.charAt(numHL); - updateTopString(); - - // Autoswitching letter case - if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase(); - } - } - // Update previous character to current one - numHLPrev = numHL; - typePrev = 'num'; + // Append space if releasing after list of numbers/punctuation + else if (event.x > (R.x+R.w)-NUMPADDING) { + //show space sign + showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4); + delSpaceLast = 1; + text = text + ' '; + updateTopString(); + //print(text); } + // Append selected number/punctuation + else { + text = text + NUMHIDDEN.charAt(numHL); + updateTopString(); - // Make a space or backspace by swiping right or left on screen above green rectangle - else if (event.y > 20+4) { - if (event.b == 0) { - g.setColor(HLCOLOR); - if (event.x < (R.x+R.w)/2) { - resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - - // show delete sign - showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4); - delSpaceLast = 1; - - // Backspace and draw string upper right corner - text = text.slice(0, -1); - updateTopString(); - if (text.length==0) changeCase(abcHL); - //print(text, 'undid'); - } - else { - resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - - //show space sign - showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4); - delSpaceLast = 1; - - // Append space and draw string upper right corner - text = text + NUMHIDDEN.charAt(0); - updateTopString(); - //print(text, 'made space'); - } - } + // Autoswitching letter case + if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase(); } - }; + } + // Update previous character to current one + numHLPrev = numHL; + typePrev = 'num'; + } + + // Make a space or backspace by swiping right or left on screen above green rectangle + else if (event.y > 20+4) { + if (event.b == 0) { + g.setColor(HLCOLOR); + if (event.x < (R.x+R.w)/2) { + resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + + // show delete sign + showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4); + delSpaceLast = 1; + + // Backspace and draw string upper right corner + text = text.slice(0, -1); + updateTopString(); + if (text.length==0) changeCase(abcHL); + //print(text, 'undid'); + } + else { + resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + + //show space sign + showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4); + delSpaceLast = 1; + + // Append space and draw string upper right corner + text = text + NUMHIDDEN.charAt(0); + updateTopString(); + //print(text, 'made space'); + } + } + } + }; let catchSwipe = (_,__)=>{ E.stopEventPropagation&&E.stopEventPropagation(); From 92e2f4695cd594a9e69bae6f33229b5d52fe0a6c Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Mon, 31 Jul 2023 20:48:30 +0200 Subject: [PATCH 101/242] draguboard: Compat w backswipe on newer firmwares --- apps/draguboard/ChangeLog | 2 ++ apps/draguboard/lib.js | 66 ++++++++++++++++++++--------------- apps/draguboard/metadata.json | 2 +- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog index a228aab54..bca1ca7c4 100644 --- a/apps/draguboard/ChangeLog +++ b/apps/draguboard/ChangeLog @@ -1 +1,3 @@ 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. diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js index 258f8b02d..4500f523a 100644 --- a/apps/draguboard/lib.js +++ b/apps/draguboard/lib.js @@ -104,45 +104,53 @@ exports.input = function(options) { } } + let dragHandlerUB = function(event) { + "ram"; + + // drag on middle bottom rectangle + if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) { + moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length)); + } + // drag on left or right rectangle + else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) { + moveCharPos(event.x ( (R.y2) - 52 ))) { + moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6); + } + // Make a space or backspace by tapping right or left on screen above green rectangle + else if (event.y > R.y && event.b == 0) { + if (event.x < (R.x2)/2) { + showChars('<-'); + text = text.slice(0, -1); + } else { + //show space sign + showChars('->'); + text += ' '; + } + prevChar = null; + updateTopString(); + } + }; + + 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 lister if it was added with `Bangle.prependListener()` (fw2v19 and up). g.clearRect(Bangle.appRect); resolve(text); }, - drag: function(event) { - "ram"; - - // drag on middle bottom rectangle - if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) { - moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length)); - } - // drag on left or right rectangle - else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) { - moveCharPos(event.x ( (R.y2) - 52 ))) { - moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6); - } - // Make a space or backspace by tapping right or left on screen above green rectangle - else if (event.y > R.y && event.b == 0) { - if (event.x < (R.x2)/2) { - showChars('<-'); - text = text.slice(0, -1); - } else { - //show space sign - showChars('->'); - text += ' '; - } - prevChar = null; - updateTopString(); - } - } + drag: dragHandlerDB }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. R = Bangle.appRect; MIDPADDING = R.x + 35; diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json index dc9b06254..620f39f71 100644 --- a/apps/draguboard/metadata.json +++ b/apps/draguboard/metadata.json @@ -1,6 +1,6 @@ { "id": "draguboard", "name": "DragUboard", - "version":"0.01", + "version":"0.02", "description": "A library for text input via swiping U-shaped keyboard.", "icon": "app.png", "type":"textinput", From 1dc9870c0714e8f831801ac011d6ba40b8b81581 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 21:28:30 +0100 Subject: [PATCH 102/242] btadv: fix long uuid --- apps/btadv/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index fbce48fe6..c832eda62 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -35,7 +35,7 @@ const enum BleServ { // org.microbit.service.accelerometer // contains: Acc - Acc = "0xE95D0753251D470AA062FA1922DFA9A8", + Acc = "E95D0753251D470AA062FA1922DFA9A8", } const services = [ From 1f94a8f26ead8692108730e7feda71cd12fbc5f6 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 21:29:45 +0100 Subject: [PATCH 103/242] btadv: advertise on a cycle for long UUIDs --- apps/btadv/app.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index c832eda62..8aeebd793 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -763,16 +763,35 @@ enableSensors(); // must have fixed services from the start: const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true); - const adServices = Object - .keys(ad) - .map((k: string) => k.replace("0x", "")); - NRF.setServices( ad, { - advertise: adServices, uart: false, }, ); + + if(!(Bangle as any).bleAdvert) + (Bangle as any).bleAdvert = {}; + + const cycle = []; + for(const id in ad){ + const serv = ad[id as BleServ]; + let value; + + // pick the first characteristic to advertise + for(const ch in serv){ + value = serv[ch as BleChar]!.value; + break; + } + + cycle.push({ [id]: value || [] }); + } + + NRF.setAdvertising( + cycle, + { + interval: 100, + } + ); } } From 19c0a9af089a1784ef80c67629219516dfc4fd20 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 21:30:34 +0100 Subject: [PATCH 104/242] btadv: generate JS --- apps/btadv/app.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/btadv/app.js b/apps/btadv/app.js index cacba3eb6..87259e04d 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -1,3 +1,4 @@ +var _a; { var __assign = Object.assign; var Layout_1 = require("Layout"); @@ -8,7 +9,7 @@ "0x180d", "0x181a", "0x1819", - "0xE95D0753251D470AA062FA1922DFA9A8", + "E95D0753251D470AA062FA1922DFA9A8", ]; var acc_1; var bar_1; @@ -258,7 +259,7 @@ case "0x180d": return !!hrm_1; case "0x181a": return !!(bar_1 || mag_1); case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1); - case "0xE95D0753251D470AA062FA1922DFA9A8": return !!acc_1; + case "E95D0753251D470AA062FA1922DFA9A8": return !!acc_1; } }; var serviceToAdvert_1 = function (serv, initial) { @@ -348,7 +349,7 @@ } return o; } - case "0xE95D0753251D470AA062FA1922DFA9A8": { + case "E95D0753251D470AA062FA1922DFA9A8": { var o = {}; if (acc_1 || initial) { o["0xE95DCA4B251D470AA062FA1922DFA9A8"] = { @@ -434,12 +435,23 @@ enableSensors_1(); { var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true); - var adServices = Object - .keys(ad) - .map(function (k) { return k.replace("0x", ""); }); NRF.setServices(ad, { - advertise: adServices, uart: false, }); + if (!Bangle.bleAdvert) + Bangle.bleAdvert = {}; + var cycle = []; + for (var id in ad) { + var serv = ad[id]; + var value = void 0; + for (var ch in serv) { + value = serv[ch].value; + break; + } + cycle.push((_a = {}, _a[id] = value || [], _a)); + } + NRF.setAdvertising(cycle, { + interval: 100, + }); } } From b61e4fb137dcb6858d45cee2ea1deb647b462171 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 21:35:11 +0100 Subject: [PATCH 105/242] btadv: fix long uuid (characteristic) --- apps/btadv/app.js | 4 ++-- apps/btadv/app.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/btadv/app.js b/apps/btadv/app.js index 87259e04d..cb1020035 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -352,13 +352,13 @@ var _a; case "E95D0753251D470AA062FA1922DFA9A8": { var o = {}; if (acc_1 || initial) { - o["0xE95DCA4B251D470AA062FA1922DFA9A8"] = { + o["E95DCA4B251D470AA062FA1922DFA9A8"] = { maxLen: encodeAcc_1.maxLen, readable: true, notify: true, }; if (acc_1) { - o["0xE95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1); + o["E95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1); acc_1 = undefined; } } diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 8aeebd793..77ceb40c1 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -80,7 +80,7 @@ const enum BleChar { // org.microbit.characteristic.accelerometer_data // s16 x3, -1024 .. 1024 // docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/ - Acc = "0xE95DCA4B251D470AA062FA1922DFA9A8", + Acc = "E95DCA4B251D470AA062FA1922DFA9A8", } type BleCharAdvert = { From 701f8c7733437d8ecab8847446ed161eb0951dbb Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 21:40:06 +0100 Subject: [PATCH 106/242] btadv: properly encode acc readings --- apps/btadv/app.js | 13 +++++++++---- apps/btadv/app.ts | 16 ++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/btadv/app.js b/apps/btadv/app.js index cb1020035..94d7f7ad7 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -221,15 +221,20 @@ var _a; ]; }; encodeGpsHeadingOnly_1.maxLen = 17; - var encodeXYZ = function (data) { + var encodeMag_1 = function (data) { var x = toByteArray_1(data.x, 2, true); var y = toByteArray_1(data.y, 2, true); var z = toByteArray_1(data.z, 2, true); return [x[0], x[1], y[0], y[1], z[0], z[1]]; }; - encodeXYZ.maxLen = 6; - var encodeMag_1 = encodeXYZ; - var encodeAcc_1 = encodeXYZ; + encodeMag_1.maxLen = 6; + var encodeAcc_1 = function (data) { + var x = toByteArray_1(data.x * 1000, 2, true); + var y = toByteArray_1(data.y * 1000, 2, true); + var z = toByteArray_1(data.z * 1000, 2, true); + return [x[0], x[1], y[0], y[1], z[0], z[1]]; + }; + encodeAcc_1.maxLen = 6; var toByteArray_1 = function (value, numberOfBytes, isSigned) { var byteArray = new Array(numberOfBytes); if (isSigned && (value < 0)) { diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 77ceb40c1..89685d375 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -481,19 +481,23 @@ const encodeGpsHeadingOnly: LenFunc = (data: CompassData) => { }; encodeGpsHeadingOnly.maxLen = 17; -type XYZ = { x: number, y: number, z: number }; - -const encodeXYZ: LenFunc = (data: XYZ) => { +const encodeMag: LenFunc = (data: CompassData) => { const x = toByteArray(data.x, 2, true); const y = toByteArray(data.y, 2, true); const z = toByteArray(data.z, 2, true); return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ]; }; -encodeXYZ.maxLen = 6; +encodeMag.maxLen = 6; -const encodeMag: LenFunc = encodeXYZ; -const encodeAcc: LenFunc = encodeXYZ; +const encodeAcc: LenFunc = (data: AccelData) => { + const x = toByteArray(data.x * 1000, 2, true); + const y = toByteArray(data.y * 1000, 2, true); + const z = toByteArray(data.z * 1000, 2, true); + + return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ]; +}; +encodeAcc.maxLen = 6; const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => { const byteArray: Array = new Array(numberOfBytes); From ecf85fe9b753aa7073045ec811a09e3f4163b063 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 21:50:52 +0100 Subject: [PATCH 107/242] recorder: always show clkinfo We can't tell whether Bangle.loadWidgets() might be called later, so assume it does, and handle the case where it hasn't been. --- apps/recorder/clkinfo.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js index 162509eb6..b4a9a45b8 100644 --- a/apps/recorder/clkinfo.js +++ b/apps/recorder/clkinfo.js @@ -7,22 +7,28 @@ return { name: "Bangle", - items: typeof WIDGETS !== "undefined" && WIDGETS["recorder"] ? [ + items: require("Storage").readJSON("recorder.json") ? [ { name: "Toggle", - get: () => WIDGETS["recorder"].isRecording() ? { - text: "Recording", - short: "rec", - img: recimg(), - } : { - text: "Paused", - short: "paused", - img: pauseimg(), + get: () => { + const w = WIDGETS && WIDGETS["recorder"]; + + return w && w.isRecording() ? { + text: "Recording", + short: "Rec", + img: recimg(), + } : { + text: w ? "Paused" : "No rec", + short: w ? "Paused" : "No rec", + img: pauseimg(), + }; }, run: () => { - const w = WIDGETS["recorder"]; - Bangle.buzz(); - w.setRecording(!w.isRecording(), { force: "append" }); + const w = WIDGETS && WIDGETS["recorder"]; + if(w){ + Bangle.buzz(); + w.setRecording(!w.isRecording(), { force: "append" }); + } }, show: () => {}, hide: () => {}, From 202313829507abf87bc1e71ce9d14301ecb63d45 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 31 Jul 2023 22:04:20 +0100 Subject: [PATCH 108/242] wid_edit: restore alphabetical sort --- apps/wid_edit/ChangeLog | 1 + apps/wid_edit/boot.js | 1 + apps/wid_edit/metadata.json | 2 +- apps/wid_edit/settings.js | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog index 93f61b814..7d6707467 100644 --- a/apps/wid_edit/ChangeLog +++ b/apps/wid_edit/ChangeLog @@ -4,3 +4,4 @@ Allow changing widgets into all areas, including bottom widget bar 0.03: Fix editing widgets whose draw method takes the widget 0.04: Remove double-sort +0.05: Restore alphabetical sort diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js index 60912fd6f..fe259f97f 100644 --- a/apps/wid_edit/boot.js +++ b/apps/wid_edit/boot.js @@ -14,6 +14,7 @@ Bangle.loadWidgets = (o => ()=>{ const W = global.WIDGETS; global.WIDGETS = {}; Object.keys(W) + .sort() // sort alphabetically. the next sort is stable and preserves this if sortorder matches .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) .forEach(k => global.WIDGETS[k] = W[k]); })(Bangle.loadWidgets); diff --git a/apps/wid_edit/metadata.json b/apps/wid_edit/metadata.json index 02ab216fe..b89640333 100644 --- a/apps/wid_edit/metadata.json +++ b/apps/wid_edit/metadata.json @@ -1,6 +1,6 @@ { "id": "wid_edit", - "version": "0.04", + "version": "0.05", "name": "Widget Editor", "icon": "icon.png", "description": "Customize widget locations", diff --git a/apps/wid_edit/settings.js b/apps/wid_edit/settings.js index be09923f2..a632850d6 100644 --- a/apps/wid_edit/settings.js +++ b/apps/wid_edit/settings.js @@ -27,7 +27,7 @@ let W = global.WIDGETS; global.WIDGETS = {}; Object.keys(W) - .sort() + .sort() // see comment in boot.js .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) .forEach(k => {global.WIDGETS[k] = W[k];}); Bangle.drawWidgets(); From e94f42035637e3b5c604c2c9011fd65a7134ca38 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 1 Aug 2023 13:48:36 -0500 Subject: [PATCH 109/242] Correct calculation of daily summary for movement - Heartrate was being summed from the DB instead of movement itself - The daily average value was inadvertently being divided by 8 relative the hourly data, resulting in very low precision --- apps/health/ChangeLog | 1 + apps/health/boot.js | 12 +++++++++--- apps/health/boot.min.js | 11 ++++++----- apps/health/metadata.json | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 12740959a..9b98e32b7 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -22,3 +22,4 @@ 0.21: Update boot.min.js. 0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435) 0.23: Fix HRM logic +0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written) diff --git a/apps/health/boot.js b/apps/health/boot.js index 62e8b87ab..66b4acda6 100644 --- a/apps/health/boot.js +++ b/apps/health/boot.js @@ -52,7 +52,7 @@ Bangle.on("health", health => { return String.fromCharCode( health.steps>>8,health.steps&255, // 16 bit steps health.bpm, // 8 bit bpm - Math.min(health.movement / 8, 255)); // movement + Math.min(health.movement, 255)); // movement } var rec = getRecordIdx(d); @@ -68,6 +68,12 @@ Bangle.on("health", health => { require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header } var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN); + + // scale down reported movement value in order to fit it within a + // uint8 DB field + health = Object.assign({}, health); + health.movement /= 8; + require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN); if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return; // we're at the end of the day. Read in all of the data for the day and sum it up @@ -82,10 +88,10 @@ Bangle.on("health", health => { var dt = f.substr(recordPos, DB_RECORD_LEN); if (dt!="\xFF\xFF\xFF\xFF") { health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1); - health.movement += dt.charCodeAt(2); - health.movCnt++; var bpm = dt.charCodeAt(2); health.bpm += bpm; + health.movement += dt.charCodeAt(3); + health.movCnt++; if (bpm) health.bpmCnt++; } recordPos -= DB_RECORD_LEN; diff --git a/apps/health/boot.min.js b/apps/health/boot.min.js index 651231195..afda9189a 100644 --- a/apps/health/boot.min.js +++ b/apps/health/boot.min.js @@ -1,5 +1,6 @@ -function l(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateMath.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0, -"health")});90>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0k;k++)e=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=e&&(a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1),a.movement+=e.charCodeAt(2), -a.movCnt++,e=e.charCodeAt(2),a.bpm+=e,e&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}}) +(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){var c=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){return Bangle.setHRMPower(0,"health")},6E4*a);if(1==a){var b=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){Bangle.setHRMPower(0,"health")},6E4)};setTimeout(b,2E5);setTimeout(b,4E5)}};Bangle.on("health",c);Bangle.on("HRM",function(b){90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")}); +90>8,g.steps&255,g.bpm,Math.min(g.movement,255))}var b=new Date(Date.now()-59E4);a&&0n;n++){e=h.substr(m,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var p=e.charCodeAt(2);a.bpm+=p;a.movement+=e.charCodeAt(3);a.movCnt++;p&&a.bpmCnt++}m-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/= +a.movCnt);require("Storage").write(b,c(a),f,17988)}}); +function q(){var a=require("Storage").readJSON("health.json",1)||{},c=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(c=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate Date: Tue, 1 Aug 2023 21:54:18 +0200 Subject: [PATCH 110/242] Allow zooming map in/out by clicking. By simply clicking on screen instead of going to menu to zoom in/out, navigation can be much quicker. --- apps/openstmap/ChangeLog | 3 ++- apps/openstmap/README.md | 1 + apps/openstmap/app.js | 16 ++++++++++++++++ apps/openstmap/metadata.json | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index e684d4e98..7d51a1d0c 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -16,7 +16,7 @@ Support for zooming in on map Satellite count moved to widget bar to leave more room for the map 0.15: Make track drawing an option (default off) -0.16: Draw waypoints, too. +0.16: Draw waypoints, too 0.17: With new Recorder app allow track to be drawn in the background Switch tile layer URL for faster/more reliable map tiles 0.18: Prefer map with highest resolution @@ -28,3 +28,4 @@ Enable/Disable previous position marker in new setting "Draw cont. position" 0.22: Replace position marker with direction arrow 0.23: Bugfix: Enable Compass if needed +0.24: Allow zooming by clicking the screen diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md index bf247c7b7..e0fc30abd 100644 --- a/apps/openstmap/README.md +++ b/apps/openstmap/README.md @@ -29,6 +29,7 @@ and marks the path that you've been travelling (if enabled), and displays waypoints in the watch (if dependencies exist). * Drag on the screen to move the map +* Click bottom left to zoom in, bottom right to zoom out * Press the button to bring up a menu, where you can zoom, go to GPS location, put the map back in its default location, or choose whether to draw the currently recording GPS track (from the `Recorder` app). diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index e1a9ea734..9b53077ab 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -7,6 +7,8 @@ var hasScrolled = false; var settings = require("Storage").readJSON("openstmap.json",1)||{}; var plotTrack; let checkMapPos = false; // Do we need to check the if the coordinates we have are valid +var startDrag = 0; + if (Bangle.setLCDOverlay) { // Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted var imgLoc = require("heatshrink").decompress(atob("jEYwINLAQk8AQl+AQn/AQcB/+AAQUD//AAQUH//gAQUP//wAQUf//4j8AvA9IA==")); @@ -235,6 +237,8 @@ function showMap() { Bangle.setUI({mode:"custom",drag:e=>{ if (plotTrack && plotTrack.stop) plotTrack.stop(); if (e.b) { + if (!startDrag) + startDrag = getTime(); g.setClipRect(R.x,R.y,R.x2,R.y2); g.scroll(e.dx,e.dy); m.scroll(e.dx,e.dy); @@ -242,7 +246,19 @@ function showMap() { hasScrolled = true; drawLocation(); } else if (hasScrolled) { + delta = getTime() - startDrag; + startDrag = 0; hasScrolled = false; + if (delta < 0.2) { + if (e.y > g.getHeight() / 2) { + if (e.x < g.getWidth() / 2) { + m.scale /= 2; + } else { + m.scale *= 2; + } + } + g.reset().clearRect(R); + } redraw(); } }, btn: () => showMenu() }); diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 9a7527347..988a1414d 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.23", + "version": "0.24", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "readme": "README.md", "icon": "app.png", From d7424be12e9de605344c33a704b3f1201d6aaa1b Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Tue, 1 Aug 2023 22:27:18 +0200 Subject: [PATCH 111/242] Sixth sense -- outdoor watch with GPS support. Add initial version of the application. I already use it a lot, as it displays just the data I need. It can log GPS in power-saving mode, which is useful on its own, and also makes GPS ready for use. --- apps/sixths/ChangeLog | 1 + apps/sixths/README.md | 51 ++++ apps/sixths/app-icon.js | 2 + apps/sixths/app.js | 508 ++++++++++++++++++++++++++++++++++++++ apps/sixths/app.png | Bin 0 -> 892 bytes apps/sixths/metadata.json | 13 + 6 files changed, 575 insertions(+) create mode 100644 apps/sixths/ChangeLog create mode 100644 apps/sixths/README.md create mode 100644 apps/sixths/app-icon.js create mode 100644 apps/sixths/app.js create mode 100644 apps/sixths/app.png create mode 100644 apps/sixths/metadata.json diff --git a/apps/sixths/ChangeLog b/apps/sixths/ChangeLog new file mode 100644 index 000000000..263d4078d --- /dev/null +++ b/apps/sixths/ChangeLog @@ -0,0 +1 @@ +0.01: attempt to import diff --git a/apps/sixths/README.md b/apps/sixths/README.md new file mode 100644 index 000000000..e5d76d5ad --- /dev/null +++ b/apps/sixths/README.md @@ -0,0 +1,51 @@ +# Sixth Sense ![](app.png) + +Clock displaying just the right information at the right time. + +Experimental clock face. It aims to display just the right importation +at the right time, with focus on various sensors. Normally, digital +clock, date and step count in kilometers is displayed. + +It saves a lot of logs for debugging and future use. In particular, it +saves battery and step counters all the time, and GPS positions +whenever it is enabled. You may not want to use it if you are secret +agent or journalist in Iran. + +Application can be controled by gestures, making control possible in +challenging conditions such as on horseback. Gestures are based on +morse code, left half of screen is for ".", right half of screen is +for "-". Gesture should always start in the upper half of screen. If +next symbol is same, drag vertically, else drag horizontally. + +Power saving GPS mode is available, suitable for hiking. GPS fix is +acquired once every few minutes, and written into the log. Approximate +distance travelled is displayed. Due to only taking fix every few +minutes, real distance will be usually higher than approximation. + +Useful gestures: + +F -- disable GPS. +G -- enable GPS for 4 hours. +N -- take a note and write it to the log. + +When application detects watch is being worn, it will use vibrations +to communicate back to the user. + +E -- acknowledge, gesture understood. +T -- start of new hour. + +Written by: [Pavel Machek](https://github.com/pavelmachek) + +## Future Development + +I'd like to expand GPS development more, allowing marking of waypoints +and navigating back to them. I'd also like to make power-saving +optional. + +I'd also like to utilize the altimeter more, likely remembering +altitude of home location, automatically correcting for pressure every +night. + +I'd like to make display nicer, and likely more dynamic, displaying +whatever application believes is most important at the time (and +possibly allowing scrolling). \ No newline at end of file diff --git a/apps/sixths/app-icon.js b/apps/sixths/app-icon.js new file mode 100644 index 000000000..c75930b47 --- /dev/null +++ b/apps/sixths/app-icon.js @@ -0,0 +1,2 @@ +require("heatshrink").decompress(atob("mEwgP/AEn3AgfvAonnAon3+/9AoX7+/5CwX7A4IWCB4P7/3/+YZC/gOD/4eDAAIeC/0An4PC+P/4Y3C5E/Cwcgj/+v4WB4EP/+fEoOAg//441BAoQjB84FCDwPvwED/5BB+4FCBYIFBCIJRB/fAAoPPBoIvCn41B+A7EnF//BHCHAODAoXwgF/N4aMCAog1BQoJ0Cv5oCCAO/UAP9AoPP+fv/oOBW4IFBDQP794FB/5BBMwIFBJoItD375eA")) + diff --git a/apps/sixths/app.js b/apps/sixths/app.js new file mode 100644 index 000000000..ce036f79d --- /dev/null +++ b/apps/sixths/app.js @@ -0,0 +1,508 @@ +const W = g.getWidth(); +const H = g.getHeight(); + +var cx = 100; cy = 105; sc = 70; +var buzz = "", msg = ""; +temp = 0; alt = 0; bpm = 0; +var buzz = "", msg = "", inm = "", l = "", note = "(NOTEHERE)"; +var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note + +var gps_on = 0, last_fix = 0, last_restart = 0, last_pause = 0, last_fstart = 0; // utime +var gps_needed = 0, gps_limit = 0; // seconds +var prev_fix = null; +var gps_dist = 0; + +var is_active = false; +var cur_altitude = 0, cur_temperature = 0, alt_adjust = 0; +const rest_altitude = 354; + +function toMorse(x) { + r = ""; + for (var i = 0; i < x.length; i++) { + c = x[i]; + if (c == " ") { + r += " "; + continue; + } + r += asciiToMorse(c) + " "; + } + return r; +} + +function aload(s) { + buzz += toMorse(' E'); + load(s); +} + +function gpsRestart() { + print("gpsRestart"); + Bangle.setGPSPower(1, "sixths"); + last_restart = getTime(); + last_pause = 0; + last_fstart = 0; +} + +function gpsPause() { + print("gpsPause"); + Bangle.setGPSPower(0, "sixths"); + last_restart = 0; + last_pause = getTime(); +} + +function gpsOn() { + gps_on = getTime(); + gps_needed = 1000; + gps_limit = 60*60*4; + last_fix = 0; + prev_fix = null; + gps_dist = 0; + gpsRestart(); +} + +function gpsOff() { + Bangle.setGPSPower(0, "sixths"); + gps_on = 0; +} + +function inputHandler(s) { + print("Ascii: ", s); + if (mode == 1) { + note = note + s; + mode_time = getTime(); + return; + } + switch(s) { + case 'B': + s = ' B'; + bat = E.getBattery(); + if (bat > 45) + s += 'E'; + else + s = s+(bat/5); + buzz += toMorse(s); + break; + case 'F': gpsOff(); break; + case 'G': gpsOn(); break; + case 'L': aload("altimeter.app.js"); break; + case 'N': mode = 1; note = ">"; mode_time = getTime(); break; + case 'O': aload("orloj.app.js"); break; + case 'T': + s = ' T'; + d = new Date(); + s += d.getHours() % 10; + s += add0(d.getMinutes()); + buzz += toMorse(s); + break; + case 'R': aload("run.app.js"); break; + } +} + +const morseDict = { + '.-': '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', + '.----': '1', + '..---': '2', + '...--': '3', + '....-': '4', + '.....': '5', + '----.': '9', + '---..': '8', + '--...': '7', + '-....': '6', + '-----': '0', + }; + +let asciiDict = {}; + +for (let k in morseDict) { + print(k, morseDict[k]); + asciiDict[morseDict[k]] = k; +} + + +function morseToAscii(morse) { + return morseDict[morse]; +} + +function asciiToMorse(char) { + return asciiDict[char]; +} + +function morseHandler() { + inputHandler(morseToAscii(inm)); + inm = ""; + l = ""; +} + +function touchHandler(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + g.setColor(0.25, 0, 0); + g.fillCircle(W-x, W-y, 5); + + if (d.b) { + if (x < W/2 && y < H/2 && l != ".u") { + inm = inm + "."; + l = ".u"; + } + if (x > W/2 && y < H/2 && l != "-u") { + inm = inm + "-"; + l = "-u"; + } + if (x < W/2 && y > H/2 && l != ".d") { + inm = inm + "."; + l = ".d"; + } + if (x > W/2 && y > H/2 && l != "-d") { + inm = inm + "-"; + l = "-d"; + } + + } else + morseHandler(); + + print(inm, "drag:", d); +} + +function add0(i) { + if (i > 9) { + return ""+i; + } else { + return "0"+i; + } +} + +var lastHour = -1, lastMin = -1; + +function logstamp(s) { + logfile.write("utime=" + getTime() + " " + s + "\n"); +} + +function loggps(fix) { + logfile.write(fix.lat + " " + fix.lon + " "); + logstamp(""); +} + +function hourly() { + print("hourly"); + s = ' T'; + if (is_active) + buzz += toMorse(s); + logstamp(""); +} + +function fivemin() { + print("fivemin"); + s = ' B'; + bat = E.getBattery(); + if (bat < 45) { + s = s+(bat/5); + if (is_active) + buzz += toMorse(s); + } + if (0) + Bangle.getPressure().then((x) => { cur_altitude = x.altitude; + cur_temperature = x.temperature; }, + print) + .catch(print); +} + +function every(now) { + if ((mode > 0) && (mode_time - getTime() > 60)) { + if (mode == 1) { + logstamp(">" + note); + } + mode = 0; + } + if (gps_on && getTime() - gps_on > gps_limit) { + Bangle.setGPSPower(0, "sixths"); + gps_on = 0; + } + + if (lastHour != now.getHours()) { + lastHour = now.getHours(); + hourly(); + } + if (lastMin / 5 != now.getMinutes() / 5) { + lastMin = now.getMinutes(); + fivemin(); + } + +} + +// 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) { + function radians(a) { return a*Math.PI/180; } + 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; +} + +function draw() { + g.setColor(1, 1, 1); + g.fillRect(0, 25, W, H); + g.setFont('Vector', 60); + + g.setColor(0, 0, 0); + g.setFontAlign(-1, 1); + let now = new Date(); + g.drawString(now.getHours() + ":" + add0(now.getMinutes()), 10, 90); + + every(now); + + let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps; + + g.setFont('Vector', 26); + + const weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; + + g.drawString(weekday[now.getDay()] + "" + now.getDate() + ". " + km.toFixed(1) + "km", 10, 115); + + if (gps_on) { + if (!last_restart) { + d = (getTime()-last_pause); + if (last_fix) + msg = "PL"+ (getTime()-last_fix).toFixed(0); + else + msg = "PN"+ (getTime()-gps_on).toFixed(0); + + print("gps on, paused ", d, gps_needed); + if (d > gps_needed * 2) { + gpsRestart(); + } + } else { + fix = Bangle.getGPSFix(); + if (fix.fix && fix.lat) { + if (!prev_fix) { + prev_fix = fix; + } + msg = fix.speed.toFixed(1) + " km/h"; + if (!last_fstart) + last_fstart = getTime(); + last_fix = getTime(); + gps_needed = 60; + loggps(fix); + print("GPS FIX", msg); + d = calcDistance(fix, prev_fix); + if (d > 30) { + prev_fix = fix; + gps_dist += d/1000; + } + } else { + if (last_fix) + msg = "L"+ (getTime()-last_fix).toFixed(0); + else + msg = "N"+ (getTime()-gps_on).toFixed(0); + } + + d = (getTime()-last_restart); + d2 = (getTime()-last_fstart); + print("gps on, restarted ", d, gps_needed, d2, fix.lat); + if (d > gps_needed || (last_fstart && d2 > 10)) { + gpsPause(); + gps_needed = gps_needed * 1.5; + print("Pausing, next try", gps_needed); + } + } + msg += " "+gps_dist.toFixed(1)+"km"; + } else { + msg = note; + } + g.drawString(msg, 10, 145); + if (is_active) { + g.drawString("act " + (cur_altitude - alt_adjust).toFixed(0), 10, 175); + } else { + alt_adjust = cur_altitude - rest_altitude; + g.drawString(alt_adjust.toFixed(0) + "m " + cur_temperature.toFixed(1)+"C", 10, 175); + } + queueDraw(); +} + +function draw_all() { + g.setColor(0, 0, 0); + g.fillRect(0, 0, W, H); + g.setFont('Vector', 36); + + g.setColor(1, 1, 1); + g.setFontAlign(-1, 1); + let now = new Date(); + g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40); + + acc = Bangle.getAccel(); + let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y; + let diff = ax * ax + ay * ay + az * az; + diff = diff * 3; + if (diff > 1) + diff = 1; + + co = Bangle.getCompass(); + step = Bangle.getStepCount(); + bat = E.getBattery(); + Bangle.getPressure().then((x) => { alt = x.altitude; temp = x.temperature; }, + print); + + g.setColor(0, 1, 0); + g.drawCircle(cx, cy, sc); + + if (0) { + g.setColor(0, 0.25, 0); + g.fillCircle(cx + sc * acc.x, cy + sc * acc.y, 5); + g.setColor(0, 0, 0.25); + g.fillCircle(cx + sc * acc.x, cy + sc * acc.z, 5); + } + if (0) { + print(co.dx, co.dy, co.dz); + g.setColor(0, 0.25, 0); + g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dy / 1500, 5); + g.setColor(0, 0, 0.25); + g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dz / 400, 5); + } + if (1) { + h = co.heading / 360 * 2 * Math.PI; + g.setColor(0, 0, 0.5); + g.fillCircle(cx + sc * Math.sin(h), cy + sc * Math.cos(h), 5); + } + + g.setColor(1, 1, 1); + + g.setFont('Vector', 22); + g.drawString(now.getDate()+"."+(now.getMonth()+1)+" "+now.getDay(), 3, 60); + g.drawString(msg, 3, 80); + g.drawString("S" + step + " B" + Math.round(bat/10) + (Bangle.isCharging()?"c":""), 3, 100); + g.drawString("A" + Math.round(alt) + " T" + Math.round(temp), 3, 120); + g.drawString("C" + Math.round(co.heading) + " B" + bpm, 3, 140); + + queueDraw(); +} + +function accelTask() { + tm = 100; + acc = Bangle.getAccel(); + en = !Bangle.isLocked(); + if (en && acc.z < -0.95) { + msg = "Level"; + buzz = ".-.."; + tm = 3000; + } + if (en && acc.x < -0.80) { + msg = "Down"; + buzz = "-.."; + tm = 3000; + } + if (en && acc.x > 0.95) { + msg = "Up"; + buzz = "..-"; + tm = 3000; + } + + setTimeout(accelTask, tm); +} + +function buzzTask() { + if (buzz != "") { + now = buzz[0]; + buzz = buzz.substring(1); + dot = 100; + if (now == " ") { + setTimeout(buzzTask, 300); + } else if (now == ".") { + Bangle.buzz(dot, 1); + setTimeout(buzzTask, 2*dot); + } else if (now == "-") { + Bangle.buzz(3*dot, 1); + setTimeout(buzzTask, 4*dot); + } else if (now == "/") { + setTimeout(buzzTask, 6*dot); + } else print("Unknown character -- ", now, buzz); + } else + setTimeout(buzzTask, 60000); +} + +function aliveTask() { + function cmp(s) { + let d = acc[s] - last_acc[s]; + return d < -0.03 || d > 0.03; + } + // HRM seems to detect hand quite nicely + acc = Bangle.getAccel(); + is_active = false; + if (cmp("x") || cmp("y") || cmp("z")) { + print("active"); + is_active = true; + } + last_acc = acc; + + setTimeout(aliveTask, 60000); +} + +var drawTimeout; + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + if (0) // FIXME + next = 60000; + else + next = 1000; + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, next - (Date.now() % next)); + +} + +function start() { + Bangle.on("drag", touchHandler); + if (0) + Bangle.on("accel", accelHandler); + if (0) { + Bangle.setCompassPower(1, "sixths"); + Bangle.setBarometerPower(1, "sixths"); + Bangle.setHRMPower(1, "sixths"); + Bangle.setGPSPower(1, "sixths"); + Bangle.on("HRM", (hrm) => { bpm = hrm.bpm; } ); + } + + draw(); + buzzTask(); + //accelTask(); + + if (1) { + last_acc = Bangle.getAccel(); + aliveTask(); + } +} + +g.reset(); +Bangle.setUI(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +let logfile = require("Storage").open("sixths.egt", "a"); + +start(); diff --git a/apps/sixths/app.png b/apps/sixths/app.png new file mode 100644 index 0000000000000000000000000000000000000000..cf1e13d65f8e5f5b40c80d05aa39859ef860bd75 GIT binary patch literal 892 zcmV-?1B3jDP)EX>4Tx04R}tkv&MmKpe$iTct%R4lO9+kf92K1yK=4twIqhgj%6h2a`*`ph-iL z;^HW{799LotU9+0Yt2!bCVt}afBE>hzEl0u6Z503ls?%w0>9pG10C4=2nH^D}v}p1fvK^%+%*ZF$K@@bq^n3@8Uem``n+SSIL_U@Cn4TOgAjz4dUrd zOXs{#9Aza*AwDM_Gw6cEk6f2se&bwp*v~VgMkYN^93>Wt9V~Y+D;X;B6md*ZHOlvA zT~;`6aaJoe*19KuVK}d?EOVXa5RzEL5=01)Q9~IOScucAkzyiE`w0*KpyLK=+Gne+&b`U7%UF?eAmTZk_;vXW&X}`>PFL=9Bb# zTZg?^`Gp+u90A}uTqVS`gS2<^<*josVk>04VFTPC>>obD%1=?i>H$om}x0Q(&-0L zLNpUU9?_Xj3{3nhMJGCd)ta(0ySE*VlMmg;i#Uq$LgH&@+FM6+bdMtdBChK<{C(SA zmDUymQqwXM{g zY1Qv#-40@cts~h&-$Wi_n)(!`=%IL(MSm#Hi22I*Nz*aS)S@;P3L~LY?@ArHFJ$7Q zC{`|Ob7hVjEmGal$|)k%9EOSp^;VJo^|3*=b@aJAA56IrV0F?HrG$t%senU6kFJ(8 zDOLIAsuPU4RbOwX4ew=qrm!d2T1WjNaE68s#YSYZh7VRv_OkwMyi*UL+V%r--w%+a S4ZZOI0000 Date: Thu, 3 Aug 2023 09:07:17 +0100 Subject: [PATCH 112/242] 0.19: Remove 'jit' keyword as 'for(..of..)' is not supported (fix #2937) --- apps/iconlaunch/ChangeLog | 1 + apps/iconlaunch/app.js | 13 ++++++------- apps/iconlaunch/metadata.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog index f9d143fca..504f747bd 100644 --- a/apps/iconlaunch/ChangeLog +++ b/apps/iconlaunch/ChangeLog @@ -23,3 +23,4 @@ 0.16: Use firmware provided E.showScroller method 0.17: fix fullscreen with oneClickExit 0.18: Better performance +0.19: Remove 'jit' keyword as 'for(..of..)' is not supported (fix #2937) \ No newline at end of file diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 03e743885..f7d5b7bf1 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -56,7 +56,6 @@ let texted; let drawItem = function(itemI, r) { - "jit"; let x = whitespace; let i = itemI * appsN - 1; let selectedApp; @@ -145,21 +144,21 @@ }, btn:Bangle.showClock }; - + //work both the fullscreen and the oneClickExit if( settings.fullscreen && settings.oneClickExit) { - idWatch=setWatch(function(e) { + idWatch=setWatch(function(e) { Bangle.showClock(); }, BTN, {repeat:false, edge:'rising' }); - + } - else if( settings.oneClickExit ) + else if( settings.oneClickExit ) { options.back=Bangle.showClock; } - + let scroller = E.showScroller(options); @@ -174,7 +173,7 @@ }; let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } }; - + Bangle.on("swipe", swipeHandler) Bangle.on("drag", updateTimeout); Bangle.on("touch", updateTimeout); diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index e17830ca6..acf46a431 100644 --- a/apps/iconlaunch/metadata.json +++ b/apps/iconlaunch/metadata.json @@ -2,7 +2,7 @@ "id": "iconlaunch", "name": "Icon Launcher", "shortName" : "Icon launcher", - "version": "0.18", + "version": "0.19", "icon": "app.png", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "tags": "tool,system,launcher", From 752dd852dfb2d95f2a92cf179c72a562333ec8dc Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 2 Aug 2023 00:41:36 +0200 Subject: [PATCH 113/242] kbtouch: Compat w backswipe on newer firmwares --- apps/kbtouch/ChangeLog | 2 ++ apps/kbtouch/lib.js | 3 +++ apps/kbtouch/metadata.json | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/kbtouch/ChangeLog b/apps/kbtouch/ChangeLog index 5bd2159e6..cb8e5cda6 100644 --- a/apps/kbtouch/ChangeLog +++ b/apps/kbtouch/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Introduced settings to customize the layout and functionality of the keyboard. 0.03: Convert Yes/No On/Off in settings to checkboxes +0.04: Catch and discard swipe events on fw2v19 and up (as well as some cutting + edge 2v18 ones), allowing compatability with the Back Swipe app. diff --git a/apps/kbtouch/lib.js b/apps/kbtouch/lib.js index db90440b9..4f064cfc7 100644 --- a/apps/kbtouch/lib.js +++ b/apps/kbtouch/lib.js @@ -161,8 +161,11 @@ function draw() { },back:()=>{ clearInterval(flashInterval); Bangle.setUI(); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up). g.clearRect(Bangle.appRect); resolve(text); }}); + let catchSwipe = ()=>{E.stopEventPropagation&&E.stopEventPropagation();}; + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. }); }; diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json index 31dc8c9a8..349726498 100644 --- a/apps/kbtouch/metadata.json +++ b/apps/kbtouch/metadata.json @@ -1,6 +1,6 @@ { "id": "kbtouch", "name": "Touch keyboard", - "version":"0.03", + "version":"0.04", "description": "A library for text input via onscreen keyboard", "icon": "app.png", "type":"textinput", From 8ba3c68b596f80be3076c74c37b1dcd730a752ce Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 2 Aug 2023 00:15:06 +0200 Subject: [PATCH 114/242] kbswipe: Compat w backswipe on newer firmwares --- apps/kbswipe/ChangeLog | 2 ++ apps/kbswipe/lib.js | 40 ++++++++++++++++++++++++-------------- apps/kbswipe/metadata.json | 2 +- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/kbswipe/ChangeLog b/apps/kbswipe/ChangeLog index 38d71986e..51401deda 100644 --- a/apps/kbswipe/ChangeLog +++ b/apps/kbswipe/ChangeLog @@ -6,3 +6,5 @@ 0.06: Support input of numbers and uppercase characters. 0.07: Support input of symbols. 0.08: Redone patterns a,e,m,w,z. +0.09: Catch and discard swipe events on fw2v19 and up (as well as some cutting + edge 2v18 ones), allowing compatability with the Back Swipe app. diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js index 7d05d7a8e..bed171d72 100644 --- a/apps/kbswipe/lib.js +++ b/apps/kbswipe/lib.js @@ -253,28 +253,38 @@ exports.input = function(options) { }; Bangle.drawWidgets(); + let dragHandlerKB = e=>{ + "ram"; + if (isInside(R, e)) { + if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y); + lastDrag = e.b ? e : 0; + } + } + + let touchHandlerKB = (n,e) => { + if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) { + // touch inside widget + cycleInput(); + } else if (isInside(R, e)) { + // touch inside app area + show(); + } + } + + let catchSwipe = ()=>{ + E.stopEventPropagation&&E.stopEventPropagation(); + }; + return new Promise((resolve,reject) => { - Bangle.setUI({mode:"custom", drag:e=>{ - "ram"; - if (isInside(R, e)) { - if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y); - lastDrag = e.b ? e : 0; - } - },touch:(n,e) => { - if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) { - // touch inside widget - cycleInput(); - } else if (isInside(R, e)) { - // touch inside app area - show(); - } - }, back:()=>{ + Bangle.setUI({mode:"custom", drag:dragHandlerKB, touch:touchHandlerKB, back:()=>{ delete WIDGETS.kbswipe; Bangle.removeListener("stroke", strokeHandler); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up). if (flashInterval) clearInterval(flashInterval); Bangle.setUI(); g.clearRect(Bangle.appRect); resolve(text); }}); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. }); }; diff --git a/apps/kbswipe/metadata.json b/apps/kbswipe/metadata.json index 3f3fbffa3..22e1e1431 100644 --- a/apps/kbswipe/metadata.json +++ b/apps/kbswipe/metadata.json @@ -1,6 +1,6 @@ { "id": "kbswipe", "name": "Swipe keyboard", - "version":"0.08", + "version":"0.09", "description": "A library for text input via PalmOS style swipe gestures (beta!)", "icon": "app.png", "type":"textinput", From 17fc4ccb61a4f5cc6d544b366dcf43fe84102a3c Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Thu, 3 Aug 2023 12:50:23 +0200 Subject: [PATCH 115/242] dragboard: remove arguments to `catchSwipe` --- apps/dragboard/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 18b7f2c6f..78ef11bd4 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -226,7 +226,7 @@ exports.input = function(options) { } }; - let catchSwipe = (_,__)=>{ + let catchSwipe = ()=>{ E.stopEventPropagation&&E.stopEventPropagation(); }; From 7aaea32ebb48abb2e8c6d0292b2c3d8f8020e830 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Thu, 3 Aug 2023 12:52:13 +0200 Subject: [PATCH 116/242] draguboard: remove arguments from `catchSwipe` --- apps/draguboard/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js index 4500f523a..6c63668a9 100644 --- a/apps/draguboard/lib.js +++ b/apps/draguboard/lib.js @@ -134,7 +134,7 @@ exports.input = function(options) { } }; - let catchSwipe = (_,__)=>{ + let catchSwipe = ()=>{ E.stopEventPropagation&&E.stopEventPropagation(); }; From b55d60befd264c75dc656e9a73a2c05ba4c47ba7 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 3 Aug 2023 14:35:18 +0100 Subject: [PATCH 117/242] more robust readfile --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 2328a68b3..b8813ab92 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 2328a68b36a9af63c4d425f6879a62949fd69868 +Subproject commit b8813ab92ceb70fb8ec6a7de6baaec88f6b5026f From c5cf741df1ec694c2649c4eb2f43909c714c407e Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 3 Aug 2023 18:11:45 +0100 Subject: [PATCH 118/242] chargerot: handle missing settings (e.g. first install) --- apps/chargerot/ChangeLog | 1 + apps/chargerot/boot.js | 2 +- apps/chargerot/metadata.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/chargerot/ChangeLog b/apps/chargerot/ChangeLog index 5560f00bc..07029aebd 100644 --- a/apps/chargerot/ChangeLog +++ b/apps/chargerot/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Handle missing settings (e.g. first-install) diff --git a/apps/chargerot/boot.js b/apps/chargerot/boot.js index 0a4361c50..2daeb3d50 100644 --- a/apps/chargerot/boot.js +++ b/apps/chargerot/boot.js @@ -1,5 +1,5 @@ (() => { - const chargingRotation = 0 | require('Storage').readJSON("chargerot.settings.json").rotate; + const chargingRotation = 0 | (require('Storage').readJSON("chargerot.settings.json",1)||{}).rotate; const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate; if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear(); Bangle.on('charging', (charging) => { diff --git a/apps/chargerot/metadata.json b/apps/chargerot/metadata.json index 1b13403d7..8174836be 100644 --- a/apps/chargerot/metadata.json +++ b/apps/chargerot/metadata.json @@ -1,7 +1,7 @@ { "id": "chargerot", "name": "Charge LCD rotation", - "version": "0.01", + "version": "0.02", "description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.", "icon": "icon.png", "tags": "battery", From 31fb8f8d02536309bf552b2780747092a4a547a9 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 4 Aug 2023 09:36:18 +0100 Subject: [PATCH 119/242] health 0.25: lib.read* methods now return correctly scaled movement movement graph in app is now an average, not sum fix 11pm slot for daily HRM --- apps/health/ChangeLog | 3 +++ apps/health/app.js | 16 +++++++++++++--- apps/health/boot.min.js | 11 +++++------ apps/health/lib.js | 6 +++--- apps/health/lib.min.js | 6 +++--- apps/health/metadata.json | 2 +- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 9b98e32b7..da68bc0e7 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -23,3 +23,6 @@ 0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435) 0.23: Fix HRM logic 0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written) +0.25: lib.read* methods now return correctly scaled movement + movement graph in app is now an average, not sum + fix 11pm slot for daily HRM \ No newline at end of file diff --git a/apps/health/app.js b/apps/health/app.js index bd708207b..fdc69dd28 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -59,7 +59,7 @@ function hrmPerHour() { E.showMessage(/*LANG*/"Loading..."); current_selection = "hrmPerHour"; var data = new Uint16Array(24); - var cnt = new Uint8Array(23); + var cnt = new Uint8Array(24); require("health").readDay(new Date(), h=>{ data[h.hr]+=h.bpm; if (h.bpm) cnt[h.hr]++; @@ -87,7 +87,12 @@ function movementPerHour() { E.showMessage(/*LANG*/"Loading..."); current_selection = "movementPerHour"; var data = new Uint16Array(24); - require("health").readDay(new Date(), h=>data[h.hr]+=h.movement); + var cnt = new Uint8Array(24); + require("health").readDay(new Date(), h=>{ + data[h.hr]+=h.movement + cnt[h.hr]++; + }); + data.forEach((d,i)=>data[i] = d/cnt[i]); setButton(menuMovement); barChart(/*LANG*/"HOUR", data); } @@ -96,7 +101,12 @@ function movementPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "movementPerDay"; var data = new Uint16Array(31); - require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement); + var cnt = new Uint8Array(31); + require("health").readDailySummaries(new Date(), h=>{ + data[h.hr]+=h.movement + cnt[h.hr]++; + }); + data.forEach((d,i)=>data[i] = d/cnt[i]); setButton(menuMovement); barChart(/*LANG*/"DAY", data); } diff --git a/apps/health/boot.min.js b/apps/health/boot.min.js index afda9189a..0d1a80f4c 100644 --- a/apps/health/boot.min.js +++ b/apps/health/boot.min.js @@ -1,6 +1,5 @@ -(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){var c=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){return Bangle.setHRMPower(0,"health")},6E4*a);if(1==a){var b=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){Bangle.setHRMPower(0,"health")},6E4)};setTimeout(b,2E5);setTimeout(b,4E5)}};Bangle.on("health",c);Bangle.on("HRM",function(b){90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")}); -90>8,g.steps&255,g.bpm,Math.min(g.movement,255))}var b=new Date(Date.now()-59E4);a&&0n;n++){e=h.substr(m,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var p=e.charCodeAt(2);a.bpm+=p;a.movement+=e.charCodeAt(3);a.movCnt++;p&&a.bpmCnt++}m-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/= -a.movCnt);require("Storage").write(b,c(a),f,17988)}}); -function q(){var a=require("Storage").readJSON("health.json",1)||{},c=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(c=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateBangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",d);Bangle.on("HRM",b=>{90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement,255))}var b=new Date(Date.now()-59E4);a&&0k;k++){e=g.substr(h,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var l=e.charCodeAt(2);a.bpm+=l;a.movement+=e.charCodeAt(3);a.movCnt++; +l&&a.bpmCnt++}h-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}}) \ No newline at end of file diff --git a/apps/health/lib.js b/apps/health/lib.js index 3a52ad59f..7ecbd5bff 100644 --- a/apps/health/lib.js +++ b/apps/health/lib.js @@ -29,7 +29,7 @@ exports.readAllRecords = function(d, cb) { day:day+1, hr : hr, min:m*10, steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), bpm : h.charCodeAt(2), - movement : h.charCodeAt(3) + movement : h.charCodeAt(3)*8 }); } idx += DB_RECORD_LEN; @@ -53,7 +53,7 @@ exports.readDailySummaries = function(d, cb) { day:day+1, steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), bpm : h.charCodeAt(2), - movement : h.charCodeAt(3) + movement : h.charCodeAt(3)*8 }); } idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN; @@ -75,7 +75,7 @@ exports.readDay = function(d, cb) { hr : hr, min:m*10, steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), bpm : h.charCodeAt(2), - movement : h.charCodeAt(3) + movement : h.charCodeAt(3)*8 }); } idx += DB_RECORD_LEN; diff --git a/apps/health/lib.min.js b/apps/health/lib.min.js index 4bdc4c0fb..5d0bed2c2 100644 --- a/apps/health/lib.min.js +++ b/apps/health/lib.min.js @@ -1,3 +1,3 @@ -function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:g.charCodeAt(3)});c+= -4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\u00ff\u00ff\u00ff\u00ff"!=e&&f({hr:d, -min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:e.charCodeAt(3)});a+=4}}} \ No newline at end of file +function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\xff\xff\xff\xff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});c+= +4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\xff\xff\xff\xff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:8*b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\xff\xff\xff\xff"!=e&& +f({hr:d,min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:8*e.charCodeAt(3)});a+=4}}} \ No newline at end of file diff --git a/apps/health/metadata.json b/apps/health/metadata.json index a95f6b19b..5ff3bb3a0 100644 --- a/apps/health/metadata.json +++ b/apps/health/metadata.json @@ -2,7 +2,7 @@ "id": "health", "name": "Health Tracking", "shortName": "Health", - "version": "0.24", + "version": "0.25", "description": "Logs health data and provides an app to view it", "icon": "app.png", "tags": "tool,system,health", From 6210f11149b6623b25946934ae8e5704db8ea232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 4 Aug 2023 23:13:31 +0100 Subject: [PATCH 120/242] android: Send fw and hw on connection --- apps/android/ChangeLog | 3 ++- apps/android/boot.js | 1 + apps/android/metadata.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index d966c1440..8362dd3a9 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -27,4 +27,5 @@ 0.26: Change handling of GPS status to depend on GPS events instead of connection events 0.27: Issue newline before GB commands (solves issue with console.log and ignored commands) 0.28: Navigation messages no longer launch the Maps view unless they're new -0.29: Support for http request xpath return format \ No newline at end of file +0.29: Support for http request xpath return format +0.30: Send firmware and hardware versions on connection diff --git a/apps/android/boot.js b/apps/android/boot.js index 7988c378f..a47edb500 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -253,6 +253,7 @@ Bangle.on("charging", sendBattery); NRF.on("connect", () => setTimeout(function() { sendBattery(); + gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION}); GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process }, 2000)); NRF.on("disconnect", () => { diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 8489570f7..e875c2072 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.29", + "version": "0.30", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", From 9c8da97552e00bc7cc2f19103fe59d8723981450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 6 Aug 2023 00:23:40 +0100 Subject: [PATCH 121/242] android: Allow alarm enable/disable --- apps/android/ChangeLog | 3 ++- apps/android/boot.js | 2 +- apps/android/metadata.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index d966c1440..afb7fee2c 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -27,4 +27,5 @@ 0.26: Change handling of GPS status to depend on GPS events instead of connection events 0.27: Issue newline before GB commands (solves issue with console.log and ignored commands) 0.28: Navigation messages no longer launch the Maps view unless they're new -0.29: Support for http request xpath return format \ No newline at end of file +0.29: Support for http request xpath return format +0.30: Allow alarm enable/disable diff --git a/apps/android/boot.js b/apps/android/boot.js index 7988c378f..2d771a1fa 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -86,7 +86,7 @@ var a = require("sched").newDefaultAlarm(); a.id = "gb"+j; a.appid = "gbalarms"; - a.on = true; + a.on = event.d[j].on !== undefined ? event.d[j].on : true; a.t = event.d[j].h * 3600000 + event.d[j].m * 60000; a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format a.last = last; diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 8489570f7..e875c2072 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.29", + "version": "0.30", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", From f8e0baa7e1b0c8918bb48a0ac07b918b450b1f9b Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 6 Aug 2023 15:53:20 +0200 Subject: [PATCH 122/242] gpstrek - Create settings --- apps/gpstrek/app.js | 42 +++------- apps/gpstrek/default.json | 20 +++++ apps/gpstrek/metadata.json | 7 +- apps/gpstrek/settings.js | 156 +++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 32 deletions(-) create mode 100644 apps/gpstrek/default.json create mode 100644 apps/gpstrek/settings.js diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js index 95db86aaf..21259c522 100644 --- a/apps/gpstrek/app.js +++ b/apps/gpstrek/app.js @@ -7,25 +7,12 @@ const MODE_SLICES = 2; const STORAGE = require("Storage"); const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144; -const SETTINGS = { - mapCompass: true, - mapScale:0.2, //initial value - mapRefresh:1000, //minimum time in ms between refreshs of the map - mapChunkSize: 5, //render this many waypoints at a time - overviewScroll: 30, //scroll this amount on swipe in pixels - overviewScale: 0.02, //initial value - refresh:500, //general refresh interval in ms - refreshLocked:3000, //general refresh interval when Bangle is locked - cacheMinFreeMem:2000, - cacheMaxEntries:0, - minCourseChange: 5, //course change needed in degrees before redrawing the map - minPosChange: 5, //position change needed in pixels before redrawing the map - waypointChangeDist: 50, //distance in m to next waypoint before advancing automatically - queueWaitingTime: 5, // waiting time during processing of task queue items when running with timeouts - autosearch: true, - maxDistForAutosearch: 300, - autosearchLimit: 3 -}; + + +const SETTINGS = Object.assign( + require('Storage').readJSON("gpstrek.default.json", true) || {}, + require('Storage').readJSON("gpstrek.json", true) || {} +); let init = function(){ global.screen = 1; @@ -38,7 +25,6 @@ let init = function(){ Bangle.loadWidgets(); WIDGETS.gpstrek.start(false); - if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 2; if (!WIDGETS.gpstrek.getState().mode) WIDGETS.gpstrek.getState().mode = MODE_MENU; }; @@ -459,7 +445,7 @@ let getMapSlice = function(){ if (!isMapOverview){ drawCurrentPos(); } - if (!isMapOverview && renderInTimeouts){ + if (SETTINGS.mapCompass && !isMapOverview && renderInTimeouts){ drawMapCompass(); } if (renderInTimeouts) drawInterface(); @@ -1254,11 +1240,6 @@ let showMenu = function(){ "Background" : showBackgroundMenu, "Calibration": showCalibrationMenu, "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});}, - "Info rows" : { - value : WIDGETS.gpstrek.getState().numberOfSlices, - min:1,max:6,step:1, - onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; } - }, }; E.showMenu(mainmenu); @@ -1374,7 +1355,7 @@ const finishData = { }; let getSliceHeight = function(number){ - return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices); + return Math.floor(Bangle.appRect.h/SETTINGS.numberOfSlices); }; let compassSlice = getCompassSlice(); @@ -1455,7 +1436,6 @@ let updateRouting = function() { lastSearch = Date.now(); autosearchCounter++; } - let counter = 0; while (hasNext(s.route) && distance(s.currentPos,get(s.route)) < SETTINGS.waypointChangeDist) { next(s.route); minimumDistance = Number.MAX_VALUE; @@ -1479,7 +1459,7 @@ let updateSlices = function(){ slices.push(healthSlice); slices.push(systemSlice); slices.push(system2Slice); - maxSlicePages = Math.ceil(slices.length/s.numberOfSlices); + maxSlicePages = Math.ceil(slices.length/SETTINGS.numberOfSlices); }; let page_slices = 0; @@ -1515,9 +1495,9 @@ let drawSlices = function(){ if (force){ clear(); } - let firstSlice = page_slices*s.numberOfSlices; + let firstSlice = page_slices*SETTINGS.numberOfSlices; let sliceHeight = getSliceHeight(); - let slicesToDraw = slices.slice(firstSlice,firstSlice + s.numberOfSlices); + let slicesToDraw = slices.slice(firstSlice,firstSlice + SETTINGS.numberOfSlices); for (let slice of slicesToDraw) { g.reset(); if (!slice.refresh || slice.refresh() || force) diff --git a/apps/gpstrek/default.json b/apps/gpstrek/default.json new file mode 100644 index 000000000..985efc94e --- /dev/null +++ b/apps/gpstrek/default.json @@ -0,0 +1,20 @@ +{ + "mapCompass": true, + "mapScale":0.5, + "mapRefresh":1000, + "mapChunkSize": 15, + "overviewScroll": 30, + "overviewScale": 0.02, + "refresh":500, + "refreshLocked":3000, + "cacheMinFreeMem":2000, + "cacheMaxEntries":0, + "minCourseChange": 5, + "minPosChange": 5, + "waypointChangeDist": 50, + "queueWaitingTime": 5, + "autosearch": true, + "maxDistForAutosearch": 300, + "autosearchLimit": 3, + "numberOfSlices": 3 +} diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json index 0ec3a8bfe..1f2279fda 100644 --- a/apps/gpstrek/metadata.json +++ b/apps/gpstrek/metadata.json @@ -12,8 +12,13 @@ "interface" : "interface.html", "storage": [ {"name":"gpstrek.app.js","url":"app.js"}, + {"name":"gpstrek.settings.js","url":"settings.js"}, + {"name":"gpstrek.default.json","url":"default.json"}, {"name":"gpstrek.wid.js","url":"widget.js"}, {"name":"gpstrek.img","url":"app-icon.js","evaluate":true} ], - "data": [{"name":"gpstrek.state.json"}] + "data": [ + {"name":"gpstrek.state.json"}, + {"name":"gpstrek.json"} + ] } diff --git a/apps/gpstrek/settings.js b/apps/gpstrek/settings.js new file mode 100644 index 000000000..ebd9c13bb --- /dev/null +++ b/apps/gpstrek/settings.js @@ -0,0 +1,156 @@ +(function(back) { + const FILE="gpstrek.json"; + let settings; + + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = Object.assign( + require('Storage').readJSON("gpstrek.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + + function showMapMenu(){ + var menu = { + '': { 'title': 'Map', back: showMainMenu }, + 'Show compass on map': { + value: !!settings.mapCompass, + onchange: v => { + writeSettings("mapCompass",v); + }, + }, + 'Initial map scale': { + value: settings.mapScale, + min: 0.01,max: 2, step:0.01, + onchange: v => { + writeSettings("mapScale",v); + }, + }, + 'Rendered waypoints': { + value: settings.mapChunkSize, + min: 5,max: 60, step:5, + onchange: v => { + writeSettings("mapChunkSize",v); + } + }, + 'Overview scroll': { + value: settings.overviewScroll, + min: 10,max: 100, step:10, + format: v => v + "px", + onchange: v => { + writeSettings("overviewScroll",v); + } + }, + 'Initial overview scale': { + value: settings.overviewScale, + min: 0.005,max: 0.1, step:0.005, + onchange: v => { + writeSettings("overviewScale",v); + } + } + }; + E.showMenu(menu); + } + + function showRoutingMenu(){ + var menu = { + '': { 'title': 'Routing', back: showMainMenu }, + 'Auto search closest waypoint': { + value: !!settings.autosearch, + onchange: v => { + writeSettings("autosearch",v); + }, + }, + 'Auto search limit': { + value: settings.autosearchLimit, + onchange: v => { + writeSettings("autosearchLimit",v); + }, + }, + 'Waypoint change distance': { + value: settings.waypointChangeDist, + format: v => v + "m", + min: 5,max: 200, step:5, + onchange: v => { + writeSettings("waypointChangeDist",v); + }, + } + }; + E.showMenu(menu); + } + + function showRefreshMenu(){ + var menu = { + '': { 'title': 'Refresh', back: showMainMenu }, + 'Unlocked refresh': { + value: settings.refresh, + format: v => v + "ms", + min: 250,max: 5000, step:250, + onchange: v => { + writeSettings("refresh",v); + } + }, + 'Locked refresh': { + value: settings.refreshLocked, + min: 1000,max: 60000, step:1000, + format: v => v + "ms", + onchange: v => { + writeSettings("refreshLocked",v); + } + }, + 'Minimum refresh': { + value: settings.mapRefresh, + format: v => v + "ms", + min: 250,max: 5000, step:250, + onchange: v => { + writeSettings("mapRefresh",v); + } + }, + 'Minimum course change': { + value: settings.minCourseChange, + min: 0,max: 180, step:1, + format: v => v + "°", + onchange: v => { + writeSettings("minCourseChange",v); + } + }, + 'Minimum position change': { + value: settings.minPosChange, + min: 0,max: 50, step:1, + format: v => v + "px", + onchange: v => { + writeSettings("minPosChange",v); + } + } + }; + E.showMenu(menu); + } + + + function showMainMenu(){ + var mainmenu = { + '': { 'title': 'GPS Trekking', back: back }, + 'Map': showMapMenu, + 'Routing': showRoutingMenu, + 'Refresh': showRefreshMenu, + "Info rows" : { + value : settings.numberOfSlices, + min:1,max:6,step:1, + onchange : v => { + writeSettings("numberOfSlices",v); + } + }, + }; + E.showMenu(mainmenu); + } + + readSettings(); + showMainMenu(); +}) \ No newline at end of file From 62083f34fbd89e37df76e44095c6ab66311418e1 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:33:20 -0400 Subject: [PATCH 123/242] Create usgs app --- apps/usgs/app.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 apps/usgs/app.js diff --git a/apps/usgs/app.js b/apps/usgs/app.js new file mode 100644 index 000000000..244e952df --- /dev/null +++ b/apps/usgs/app.js @@ -0,0 +1,74 @@ +var loc = '03272100'; // Configurable location +var keys = ['Gage', 'Discharge', 'Temperature']; // Configurable values +var shortenedName = {"Gage":"Ga","Discharge":"Dis","Temperature":"Temp"}; +var dataStreams = {}; // Will hold directions to Datastreams and units, with above keys +//var http = require("http"); +function fetchStartup() { + /*http.get( + "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + + loc + + "')/Datastreams?$expand=Observations($orderby=phenomenonTime%20desc;$top=1;$select=result)&$select=unitOfMeasurement,description" +).then(d => handleStartup(d.json().value)); +*/ + handleStartup(JSON.parse(`{"value":[{"unitOfMeasurement":{"name":"V","symbol":"V","definition":""},"description":"DCP battery voltage / USGS-03272100-1964fee2501b43c5b7b807b687319588","Observations":[{"result":"13.4"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1964fee2501b43c5b7b807b687319588')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Cubic Feet per Second","symbol":"ft^3/s","definition":""},"description":"Discharge / USGS-03272100-1db72201226e4f50a94d0b65abc8e7a5","Observations":[{"result":"692"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1db72201226e4f50a94d0b65abc8e7a5')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Feet","symbol":"ft","definition":""},"description":"Gage height / USGS-03272100-56a7245f4b47438cb79c40f0d00605ba","Observations":[{"result":"1.94"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('56a7245f4b47438cb79c40f0d00605ba')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Degrees Centigrade","symbol":"degC","definition":""},"description":"Temperature, water / USGS-03272100-f8e8a724a5c3498ca11bf1a31be5a537","Observations":[{"result":"26.2"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('f8e8a724a5c3498ca11bf1a31be5a537')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"}]}`).value); +} +function handleStartup(data) { + for (var key1 in data) { + desc = data[key1].description; + for (var key2 in keys) { + if (desc.indexOf(keys[key2]) != -1) { + if (data[key1].unitOfMeasurement.symbol === "degC") { + symbol = "F"; + result = (data[key1].Observations[0].result * 9 / 5) + 32; + } else { + symbol = data[key1].unitOfMeasurement.symbol; + result = data[key1].Observations[0].result; + } + dataStreams[keys[key2]] = JSON.parse( + '{"unit":"' + + symbol + + '","value":"' + + result + + '"}' + ); + } + } + } +} + +function displayData() { + num = keys.length; + width = g.getWidth()/(2); + height = g.getHeight()/(num); + g.clear(); + g.setFont("Vector",20); + g.setFontAlign(0,0); + string = ""; + for (var key in keys) { + unit = dataStreams[keys[key]].unit; + value = dataStreams[keys[key]].value; + if (shortenedName[keys[key]]) { + name = shortenedName[keys[key]]; + } else { + name = keys[key]; + } + string += name+": "+value+" "+unit+"\n"; + //g.drawString(name+": "+value+" "+unit, width, height*key+10); + } + E.showMessage(string,"Data"); +} + + +// On startup, grab specific datastream values +//fetchStartup().then(data => handleStartup(data)); +fetchStartup(); + +//toDo Schedule runs? +//toDo update values + +displayData(); + +setWatch(() => { + fetchStartup(); + displayData(); +}, BTN1, {repeat:true}); From f83d3fb4947ec8c233f5d5ec4a35e0c9fdecb7eb Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:34:52 -0400 Subject: [PATCH 124/242] Added icon --- apps/usgs/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/usgs/app-icon.js diff --git a/apps/usgs/app-icon.js b/apps/usgs/app-icon.js new file mode 100644 index 000000000..e98b91bad --- /dev/null +++ b/apps/usgs/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AHeIAAIYYDKkIDAeADFYXDJRECGC0iGCtEkAXPGAkEolAPJ4wEogXMJBIXBSKhGBC6RgCLxoXJLxwXCwc4C4oWMR4UzC65HFIyAXCBAUiAQMCFxheCC40gRpguCC4ouNLwwsCC5QWCIwYXCgQXBIxIuDC6bqDOwwXBIxIuDOw4XDggXICgTtCC4VAC4KQBbI4sEI4oXDggXIdQYXFokCkUgIw4vFAAYXDGANEC6CnBokiC4UBO5JECGAgXBkUkoERC5BgDGAgXEiBIKGAR5DklCC4NBC5AwDCgI0EF4QXJGAbaDDAgXBCxAwCSQgYDwUkOw4wFAAh+EkIXKGAQAJYwIYWfAQA/AHA")) From 6bc8e63ed8d7483cc5c2f98ab94d668679356596 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:36:58 -0400 Subject: [PATCH 125/242] Create metadata.json --- apps/usgs/metadata.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/usgs/metadata.json diff --git a/apps/usgs/metadata.json b/apps/usgs/metadata.json new file mode 100644 index 000000000..7be91438f --- /dev/null +++ b/apps/usgs/metadata.json @@ -0,0 +1,14 @@ +{ "id": "usgs", + "name": "USGS Data fetching app", + "shortName":"USGS", + "version":"0.01", + "description": "App that fetches USGS data from configurable location", + "icon": "app.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"usgs.app.js","url":"app.js"}, + {"name":"usgs.img","url":"app-icon.js","evaluate":true} + ] +} From e858408158e6515acd8c5bce1dd3d11524b39dca Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:37:24 -0400 Subject: [PATCH 126/242] Create README.md --- apps/usgs/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 apps/usgs/README.md diff --git a/apps/usgs/README.md b/apps/usgs/README.md new file mode 100644 index 000000000..eff3919c6 --- /dev/null +++ b/apps/usgs/README.md @@ -0,0 +1,30 @@ +# App Name + +More information on making apps: + +* http://www.espruino.com/Bangle.js+First+App +* http://www.espruino.com/Bangle.js+App+Loader + +Describe the app... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name From 82367d016bf83dbbcf639d303650222e805c8473 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:37:54 -0400 Subject: [PATCH 127/242] Create ChangeLog --- apps/usgs/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/usgs/ChangeLog diff --git a/apps/usgs/ChangeLog b/apps/usgs/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/usgs/ChangeLog @@ -0,0 +1 @@ +0.01: New App! From 3b338197cf41a88d591742995997f9d46fdf9c7d Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:42:10 -0400 Subject: [PATCH 128/242] Add files via upload --- apps/usgs/usgs.img.png | Bin 0 -> 1914 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/usgs/usgs.img.png diff --git a/apps/usgs/usgs.img.png b/apps/usgs/usgs.img.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f46c9a9368d904cd647fa216ee1c57d244c2fb GIT binary patch literal 1914 zcmV-=2Zi{FP)7-GL}S2J9onK`ceh9Hu0w?F zrXcbVY>P22#>H#!BBhr9%KxAD*+h(@C*n4kzz6zKPL9S%S9 z^{%zM-QKMdvw!ksZSVTu@Av~n zksu?()070>&Z&yrOq{PqO5iC<24_1ZB7qiChQ9m?G?+LaEh`Ytreyd>XC$-*4w2Qs z(wG0mtIRxJVNVP743#lJu~EfZ80@d|sEZ^$1(q}s>; ze3+C#Yd1(lvZTm^-Su0-(bI|_rNd=YX|!||b52m%qLhPAkdWZwuB8-P$Fi$VU(=Pp z;v)LK63)?D!qEt?(kb*2BAsRz(I8R+KX-=&DS;K8`SNyeGklxppfa7z#D`_WS524Gt zO+W&B&3u#=(KciBUoLQDr5$`{1hUOXk5gtoN;tYY89d$%v-@6Eomc%U63Puiw74;f z%63ML5(38WyIh$Aiwo)IHrg*_CMDm45HWyPojIWpm-^w6lHfuk0nf+HXvL>3sMJV6 z0^b?G@8!6Qii>ENMBv0Mn!Y&s9$sOd5;o_iepsY};URe3P6p{*1<`AJTF~-OThJqQ z5u=P`Gd~M>y0&aGgG$HKXG%D_uq}9chEH{7rW`!I5nW75aEmeeFK&;f2u%NPk@(ObtYdt}#G52~wW%j)~}8noz7Ue|0g6T$Un z{+2a3N5!JAFsqY4=T)}O*FZm>Qt;U|(c4K0d}~%VpZHLV{`~r=c?k`GC(V5F3iG@^ zb$a8U;Q@|Um;+vv!bU~~!@0(UOY0tMY-{dJR5Hf@3;6iH&<-i~cKW`^44c19o=gTTq#On853*Gd3sj zDmzC|n4c?}G6%-*nNewVxhgWR=yl{t2}C;^5vFg@2;lnP0fCn~^3N;mX`8nQi(hw%wplYO?VXli3N5-Tc8?yS zB{2JyPG%NCJKD>{t#@A6#twF%65f~~ zP?349TeL30uW^GCGypv!FM!*(Amn8f3h_*dADPxUFjS*NGFQ>f9@UeUR)^|L9n@IG>T|s z@C1h6;TCP@uzYaTIo?uJBxH>4F?f9ko=FBWcc<2@yh;>h2YXE7o4lIHP5KUO#RL{@ z$z7*QXoV&p=78I8U5>B0{nn>B!SW_2Sk@7}HJp5;3=YRZu(c4C?`nyWz?yf|w(q)~ zEm8(QWyuk{N3a=*Ry-%~Ba?w*nc?fpc1O{Kwe3kdq(f+0 zX#P=NbzAskWS&Gk1~_{Gunxiz^nM zlkhGX5Bij0IWm$+myyBn zP)dTE(ndl~RIVRL2`<4uET)Z2zx7+c5&xO}4Gm4;FoO{>2><{907*qoM6N<$f)7cL ACIA2c literal 0 HcmV?d00001 From 95c643a2ed59dc4fa4f35cd6b1fab84831fa13f3 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:43:27 -0400 Subject: [PATCH 129/242] Add files via upload --- apps/usgs/usgs.png | Bin 0 -> 1914 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/usgs/usgs.png diff --git a/apps/usgs/usgs.png b/apps/usgs/usgs.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f46c9a9368d904cd647fa216ee1c57d244c2fb GIT binary patch literal 1914 zcmV-=2Zi{FP)7-GL}S2J9onK`ceh9Hu0w?F zrXcbVY>P22#>H#!BBhr9%KxAD*+h(@C*n4kzz6zKPL9S%S9 z^{%zM-QKMdvw!ksZSVTu@Av~n zksu?()070>&Z&yrOq{PqO5iC<24_1ZB7qiChQ9m?G?+LaEh`Ytreyd>XC$-*4w2Qs z(wG0mtIRxJVNVP743#lJu~EfZ80@d|sEZ^$1(q}s>; ze3+C#Yd1(lvZTm^-Su0-(bI|_rNd=YX|!||b52m%qLhPAkdWZwuB8-P$Fi$VU(=Pp z;v)LK63)?D!qEt?(kb*2BAsRz(I8R+KX-=&DS;K8`SNyeGklxppfa7z#D`_WS524Gt zO+W&B&3u#=(KciBUoLQDr5$`{1hUOXk5gtoN;tYY89d$%v-@6Eomc%U63Puiw74;f z%63ML5(38WyIh$Aiwo)IHrg*_CMDm45HWyPojIWpm-^w6lHfuk0nf+HXvL>3sMJV6 z0^b?G@8!6Qii>ENMBv0Mn!Y&s9$sOd5;o_iepsY};URe3P6p{*1<`AJTF~-OThJqQ z5u=P`Gd~M>y0&aGgG$HKXG%D_uq}9chEH{7rW`!I5nW75aEmeeFK&;f2u%NPk@(ObtYdt}#G52~wW%j)~}8noz7Ue|0g6T$Un z{+2a3N5!JAFsqY4=T)}O*FZm>Qt;U|(c4K0d}~%VpZHLV{`~r=c?k`GC(V5F3iG@^ zb$a8U;Q@|Um;+vv!bU~~!@0(UOY0tMY-{dJR5Hf@3;6iH&<-i~cKW`^44c19o=gTTq#On853*Gd3sj zDmzC|n4c?}G6%-*nNewVxhgWR=yl{t2}C;^5vFg@2;lnP0fCn~^3N;mX`8nQi(hw%wplYO?VXli3N5-Tc8?yS zB{2JyPG%NCJKD>{t#@A6#twF%65f~~ zP?349TeL30uW^GCGypv!FM!*(Amn8f3h_*dADPxUFjS*NGFQ>f9@UeUR)^|L9n@IG>T|s z@C1h6;TCP@uzYaTIo?uJBxH>4F?f9ko=FBWcc<2@yh;>h2YXE7o4lIHP5KUO#RL{@ z$z7*QXoV&p=78I8U5>B0{nn>B!SW_2Sk@7}HJp5;3=YRZu(c4C?`nyWz?yf|w(q)~ zEm8(QWyuk{N3a=*Ry-%~Ba?w*nc?fpc1O{Kwe3kdq(f+0 zX#P=NbzAskWS&Gk1~_{Gunxiz^nM zlkhGX5Bij0IWm$+myyBn zP)dTE(ndl~RIVRL2`<4uET)Z2zx7+c5&xO}4Gm4;FoO{>2><{907*qoM6N<$f)7cL ACIA2c literal 0 HcmV?d00001 From 06e030cd71e80dc4befa69ede935d9407aaf22e0 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:43:46 -0400 Subject: [PATCH 130/242] Delete usgs.img.png --- apps/usgs/usgs.img.png | Bin 1914 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/usgs/usgs.img.png diff --git a/apps/usgs/usgs.img.png b/apps/usgs/usgs.img.png deleted file mode 100644 index b5f46c9a9368d904cd647fa216ee1c57d244c2fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1914 zcmV-=2Zi{FP)7-GL}S2J9onK`ceh9Hu0w?F zrXcbVY>P22#>H#!BBhr9%KxAD*+h(@C*n4kzz6zKPL9S%S9 z^{%zM-QKMdvw!ksZSVTu@Av~n zksu?()070>&Z&yrOq{PqO5iC<24_1ZB7qiChQ9m?G?+LaEh`Ytreyd>XC$-*4w2Qs z(wG0mtIRxJVNVP743#lJu~EfZ80@d|sEZ^$1(q}s>; ze3+C#Yd1(lvZTm^-Su0-(bI|_rNd=YX|!||b52m%qLhPAkdWZwuB8-P$Fi$VU(=Pp z;v)LK63)?D!qEt?(kb*2BAsRz(I8R+KX-=&DS;K8`SNyeGklxppfa7z#D`_WS524Gt zO+W&B&3u#=(KciBUoLQDr5$`{1hUOXk5gtoN;tYY89d$%v-@6Eomc%U63Puiw74;f z%63ML5(38WyIh$Aiwo)IHrg*_CMDm45HWyPojIWpm-^w6lHfuk0nf+HXvL>3sMJV6 z0^b?G@8!6Qii>ENMBv0Mn!Y&s9$sOd5;o_iepsY};URe3P6p{*1<`AJTF~-OThJqQ z5u=P`Gd~M>y0&aGgG$HKXG%D_uq}9chEH{7rW`!I5nW75aEmeeFK&;f2u%NPk@(ObtYdt}#G52~wW%j)~}8noz7Ue|0g6T$Un z{+2a3N5!JAFsqY4=T)}O*FZm>Qt;U|(c4K0d}~%VpZHLV{`~r=c?k`GC(V5F3iG@^ zb$a8U;Q@|Um;+vv!bU~~!@0(UOY0tMY-{dJR5Hf@3;6iH&<-i~cKW`^44c19o=gTTq#On853*Gd3sj zDmzC|n4c?}G6%-*nNewVxhgWR=yl{t2}C;^5vFg@2;lnP0fCn~^3N;mX`8nQi(hw%wplYO?VXli3N5-Tc8?yS zB{2JyPG%NCJKD>{t#@A6#twF%65f~~ zP?349TeL30uW^GCGypv!FM!*(Amn8f3h_*dADPxUFjS*NGFQ>f9@UeUR)^|L9n@IG>T|s z@C1h6;TCP@uzYaTIo?uJBxH>4F?f9ko=FBWcc<2@yh;>h2YXE7o4lIHP5KUO#RL{@ z$z7*QXoV&p=78I8U5>B0{nn>B!SW_2Sk@7}HJp5;3=YRZu(c4C?`nyWz?yf|w(q)~ zEm8(QWyuk{N3a=*Ry-%~Ba?w*nc?fpc1O{Kwe3kdq(f+0 zX#P=NbzAskWS&Gk1~_{Gunxiz^nM zlkhGX5Bij0IWm$+myyBn zP)dTE(ndl~RIVRL2`<4uET)Z2zx7+c5&xO}4Gm4;FoO{>2><{907*qoM6N<$f)7cL ACIA2c From f8132e9d1b9e962c4989f2584aeef18297bd1010 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:45:53 -0400 Subject: [PATCH 131/242] Add files via upload --- apps/usgs/app.png | Bin 0 -> 1914 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/usgs/app.png diff --git a/apps/usgs/app.png b/apps/usgs/app.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f46c9a9368d904cd647fa216ee1c57d244c2fb GIT binary patch literal 1914 zcmV-=2Zi{FP)7-GL}S2J9onK`ceh9Hu0w?F zrXcbVY>P22#>H#!BBhr9%KxAD*+h(@C*n4kzz6zKPL9S%S9 z^{%zM-QKMdvw!ksZSVTu@Av~n zksu?()070>&Z&yrOq{PqO5iC<24_1ZB7qiChQ9m?G?+LaEh`Ytreyd>XC$-*4w2Qs z(wG0mtIRxJVNVP743#lJu~EfZ80@d|sEZ^$1(q}s>; ze3+C#Yd1(lvZTm^-Su0-(bI|_rNd=YX|!||b52m%qLhPAkdWZwuB8-P$Fi$VU(=Pp z;v)LK63)?D!qEt?(kb*2BAsRz(I8R+KX-=&DS;K8`SNyeGklxppfa7z#D`_WS524Gt zO+W&B&3u#=(KciBUoLQDr5$`{1hUOXk5gtoN;tYY89d$%v-@6Eomc%U63Puiw74;f z%63ML5(38WyIh$Aiwo)IHrg*_CMDm45HWyPojIWpm-^w6lHfuk0nf+HXvL>3sMJV6 z0^b?G@8!6Qii>ENMBv0Mn!Y&s9$sOd5;o_iepsY};URe3P6p{*1<`AJTF~-OThJqQ z5u=P`Gd~M>y0&aGgG$HKXG%D_uq}9chEH{7rW`!I5nW75aEmeeFK&;f2u%NPk@(ObtYdt}#G52~wW%j)~}8noz7Ue|0g6T$Un z{+2a3N5!JAFsqY4=T)}O*FZm>Qt;U|(c4K0d}~%VpZHLV{`~r=c?k`GC(V5F3iG@^ zb$a8U;Q@|Um;+vv!bU~~!@0(UOY0tMY-{dJR5Hf@3;6iH&<-i~cKW`^44c19o=gTTq#On853*Gd3sj zDmzC|n4c?}G6%-*nNewVxhgWR=yl{t2}C;^5vFg@2;lnP0fCn~^3N;mX`8nQi(hw%wplYO?VXli3N5-Tc8?yS zB{2JyPG%NCJKD>{t#@A6#twF%65f~~ zP?349TeL30uW^GCGypv!FM!*(Amn8f3h_*dADPxUFjS*NGFQ>f9@UeUR)^|L9n@IG>T|s z@C1h6;TCP@uzYaTIo?uJBxH>4F?f9ko=FBWcc<2@yh;>h2YXE7o4lIHP5KUO#RL{@ z$z7*QXoV&p=78I8U5>B0{nn>B!SW_2Sk@7}HJp5;3=YRZu(c4C?`nyWz?yf|w(q)~ zEm8(QWyuk{N3a=*Ry-%~Ba?w*nc?fpc1O{Kwe3kdq(f+0 zX#P=NbzAskWS&Gk1~_{Gunxiz^nM zlkhGX5Bij0IWm$+myyBn zP)dTE(ndl~RIVRL2`<4uET)Z2zx7+c5&xO}4Gm4;FoO{>2><{907*qoM6N<$f)7cL ACIA2c literal 0 HcmV?d00001 From 2e594e47db2b3402f357f90efdf93740805d6963 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:46:13 -0400 Subject: [PATCH 132/242] Delete usgs.png --- apps/usgs/usgs.png | Bin 1914 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/usgs/usgs.png diff --git a/apps/usgs/usgs.png b/apps/usgs/usgs.png deleted file mode 100644 index b5f46c9a9368d904cd647fa216ee1c57d244c2fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1914 zcmV-=2Zi{FP)7-GL}S2J9onK`ceh9Hu0w?F zrXcbVY>P22#>H#!BBhr9%KxAD*+h(@C*n4kzz6zKPL9S%S9 z^{%zM-QKMdvw!ksZSVTu@Av~n zksu?()070>&Z&yrOq{PqO5iC<24_1ZB7qiChQ9m?G?+LaEh`Ytreyd>XC$-*4w2Qs z(wG0mtIRxJVNVP743#lJu~EfZ80@d|sEZ^$1(q}s>; ze3+C#Yd1(lvZTm^-Su0-(bI|_rNd=YX|!||b52m%qLhPAkdWZwuB8-P$Fi$VU(=Pp z;v)LK63)?D!qEt?(kb*2BAsRz(I8R+KX-=&DS;K8`SNyeGklxppfa7z#D`_WS524Gt zO+W&B&3u#=(KciBUoLQDr5$`{1hUOXk5gtoN;tYY89d$%v-@6Eomc%U63Puiw74;f z%63ML5(38WyIh$Aiwo)IHrg*_CMDm45HWyPojIWpm-^w6lHfuk0nf+HXvL>3sMJV6 z0^b?G@8!6Qii>ENMBv0Mn!Y&s9$sOd5;o_iepsY};URe3P6p{*1<`AJTF~-OThJqQ z5u=P`Gd~M>y0&aGgG$HKXG%D_uq}9chEH{7rW`!I5nW75aEmeeFK&;f2u%NPk@(ObtYdt}#G52~wW%j)~}8noz7Ue|0g6T$Un z{+2a3N5!JAFsqY4=T)}O*FZm>Qt;U|(c4K0d}~%VpZHLV{`~r=c?k`GC(V5F3iG@^ zb$a8U;Q@|Um;+vv!bU~~!@0(UOY0tMY-{dJR5Hf@3;6iH&<-i~cKW`^44c19o=gTTq#On853*Gd3sj zDmzC|n4c?}G6%-*nNewVxhgWR=yl{t2}C;^5vFg@2;lnP0fCn~^3N;mX`8nQi(hw%wplYO?VXli3N5-Tc8?yS zB{2JyPG%NCJKD>{t#@A6#twF%65f~~ zP?349TeL30uW^GCGypv!FM!*(Amn8f3h_*dADPxUFjS*NGFQ>f9@UeUR)^|L9n@IG>T|s z@C1h6;TCP@uzYaTIo?uJBxH>4F?f9ko=FBWcc<2@yh;>h2YXE7o4lIHP5KUO#RL{@ z$z7*QXoV&p=78I8U5>B0{nn>B!SW_2Sk@7}HJp5;3=YRZu(c4C?`nyWz?yf|w(q)~ zEm8(QWyuk{N3a=*Ry-%~Ba?w*nc?fpc1O{Kwe3kdq(f+0 zX#P=NbzAskWS&Gk1~_{Gunxiz^nM zlkhGX5Bij0IWm$+myyBn zP)dTE(ndl~RIVRL2`<4uET)Z2zx7+c5&xO}4Gm4;FoO{>2><{907*qoM6N<$f)7cL ACIA2c From e81b35a3472055dcf36268e010715b638f0630be Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:52:15 -0400 Subject: [PATCH 133/242] Update app.js --- apps/usgs/app.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/usgs/app.js b/apps/usgs/app.js index 244e952df..a38603ed8 100644 --- a/apps/usgs/app.js +++ b/apps/usgs/app.js @@ -55,6 +55,12 @@ function displayData() { string += name+": "+value+" "+unit+"\n"; //g.drawString(name+": "+value+" "+unit, width, height*key+10); } + var date = new Date(); + var hours = date.getHours(); + var minutes = date.getMinutes(); + // Format the time as a string + var timeString = hours.toString().padStart(2, "0") + ":" + + minutes.toString().padStart(2, "0"); E.showMessage(string,"Data"); } From 373326265ddb3357d180c530d595fb24b43f831f Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:55:20 -0400 Subject: [PATCH 134/242] Update app.js --- apps/usgs/app.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/usgs/app.js b/apps/usgs/app.js index a38603ed8..148c5687b 100644 --- a/apps/usgs/app.js +++ b/apps/usgs/app.js @@ -10,7 +10,7 @@ function fetchStartup() { "')/Datastreams?$expand=Observations($orderby=phenomenonTime%20desc;$top=1;$select=result)&$select=unitOfMeasurement,description" ).then(d => handleStartup(d.json().value)); */ - handleStartup(JSON.parse(`{"value":[{"unitOfMeasurement":{"name":"V","symbol":"V","definition":""},"description":"DCP battery voltage / USGS-03272100-1964fee2501b43c5b7b807b687319588","Observations":[{"result":"13.4"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1964fee2501b43c5b7b807b687319588')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Cubic Feet per Second","symbol":"ft^3/s","definition":""},"description":"Discharge / USGS-03272100-1db72201226e4f50a94d0b65abc8e7a5","Observations":[{"result":"692"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1db72201226e4f50a94d0b65abc8e7a5')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Feet","symbol":"ft","definition":""},"description":"Gage height / USGS-03272100-56a7245f4b47438cb79c40f0d00605ba","Observations":[{"result":"1.94"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('56a7245f4b47438cb79c40f0d00605ba')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Degrees Centigrade","symbol":"degC","definition":""},"description":"Temperature, water / USGS-03272100-f8e8a724a5c3498ca11bf1a31be5a537","Observations":[{"result":"26.2"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('f8e8a724a5c3498ca11bf1a31be5a537')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"}]}`).value); + handleStartup(JSON.parse(`{"value":[{"unitOfMeasurement":{"name":"V","symbol":"V","definition":""},"description":"DCP battery voltage / USGS-03272100-1964fee2501b43c5b7b807b687319588","Observations":[{"result":"13.4"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1964fee2501b43c5b7b807b687319588')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Cubic Feet per Second","symbol":"ft^3/s","definition":""},"description":"Discharge / USGS-03272100-1db72201226e4f50a94d0b65abc8e7a5","Observations":[{"result":"665"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1db72201226e4f50a94d0b65abc8e7a5')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Feet","symbol":"ft","definition":""},"description":"Gage height / USGS-03272100-56a7245f4b47438cb79c40f0d00605ba","Observations":[{"result":"1.92"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('56a7245f4b47438cb79c40f0d00605ba')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Degrees Centigrade","symbol":"degC","definition":""},"description":"Temperature, water / USGS-03272100-f8e8a724a5c3498ca11bf1a31be5a537","Observations":[{"result":"26.1"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('f8e8a724a5c3498ca11bf1a31be5a537')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"}]}`).value); } function handleStartup(data) { for (var key1 in data) { @@ -34,12 +34,14 @@ function handleStartup(data) { } } } + console.log(dataStreams); } function displayData() { num = keys.length; width = g.getWidth()/(2); height = g.getHeight()/(num); + console.log(g.getHeight()); g.clear(); g.setFont("Vector",20); g.setFontAlign(0,0); @@ -53,7 +55,6 @@ function displayData() { name = keys[key]; } string += name+": "+value+" "+unit+"\n"; - //g.drawString(name+": "+value+" "+unit, width, height*key+10); } var date = new Date(); var hours = date.getHours(); @@ -61,20 +62,17 @@ function displayData() { // Format the time as a string var timeString = hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0"); - E.showMessage(string,"Data"); + E.showMessage(string,timeString); } -// On startup, grab specific datastream values -//fetchStartup().then(data => handleStartup(data)); fetchStartup(); -//toDo Schedule runs? -//toDo update values displayData(); setWatch(() => { + console.log("button"); fetchStartup(); displayData(); }, BTN1, {repeat:true}); From a5073c25145f16ec2881a17a0e8f5f14c2858954 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:56:10 -0400 Subject: [PATCH 135/242] Update app.js --- apps/usgs/app.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/usgs/app.js b/apps/usgs/app.js index 148c5687b..6d01c6a00 100644 --- a/apps/usgs/app.js +++ b/apps/usgs/app.js @@ -38,10 +38,6 @@ function handleStartup(data) { } function displayData() { - num = keys.length; - width = g.getWidth()/(2); - height = g.getHeight()/(num); - console.log(g.getHeight()); g.clear(); g.setFont("Vector",20); g.setFontAlign(0,0); From 56cc982cbc2902347978f324366d00da9da11475 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 6 Aug 2023 20:30:53 +0200 Subject: [PATCH 136/242] gpstrek - Draw direction arrows on route --- apps/gpstrek/app.js | 35 +++++++++++++++++++++++++++-------- apps/gpstrek/default.json | 1 + apps/gpstrek/settings.js | 6 ++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js index 21259c522..eb21498c0 100644 --- a/apps/gpstrek/app.js +++ b/apps/gpstrek/app.js @@ -170,11 +170,6 @@ let getDoubleLineSlice = function(title1,title2,provider1,provider2){ }; }; -const dot = Graphics.createImage(` -XX -XX -`); - const arrow = Graphics.createImage(` X XXX @@ -184,6 +179,14 @@ const arrow = Graphics.createImage(` XXX XXX `); +const thinarrow = Graphics.createImage(` + X + XXX + XX XX + XX XX +XX XX +`); + const cross = Graphics.createImage(` XX XX XX XX @@ -458,7 +461,8 @@ let getMapSlice = function(){ i:startingIndex, poly:[], maxWaypoints: maxWaypoints, - breakLoop: false + breakLoop: false, + dist: 0 }; let drawChunk = function(data){ @@ -469,6 +473,7 @@ let getMapSlice = function(){ let last; let toDraw; let named = []; + let dir = []; for (let j = 0; j < SETTINGS.mapChunkSize; j++){ data.i = data.i + (reverse?-1:1); let p = get(route, data.i); @@ -483,7 +488,17 @@ let getMapSlice = function(){ break; } toDraw = Bangle.project(p); - if (p.name) named.push({i:data.poly.length,n:p.name}); + + if (SETTINGS.mapDirection){ + let lastWp = get(route, data.i - (reverse?-1:1)); + if (lastWp) data.dist+=distance(lastWp,p); + if (!isMapOverview && data.dist > 20/mapScale){ + dir.push({i:data.poly.length,b:require("graphics_utils").degreesToRadians(bearing(lastWp,p)-(reverse?0:180))}); + data.dist=0; + } + } + if (p.name) + named.push({i:data.poly.length,n:p.name}); data.poly.push(startingPoint.x-toDraw.x); data.poly.push((startingPoint.y-toDraw.y)*-1); } @@ -504,7 +519,11 @@ let getMapSlice = function(){ } graphics.drawString(c.n, data.poly[c.i] + 10, data.poly[c.i+1]); } - + + for (let c of dir){ + graphics.drawImage(thinarrow, data.poly[c.i], data.poly[c.i+1], {rotate: c.b}); + } + if (finish) graphics.drawImage(finishIcon, data.poly[data.poly.length - 2] -5, data.poly[data.poly.length - 1] - 4); else if (last) { diff --git a/apps/gpstrek/default.json b/apps/gpstrek/default.json index 985efc94e..aa8d5ecb1 100644 --- a/apps/gpstrek/default.json +++ b/apps/gpstrek/default.json @@ -3,6 +3,7 @@ "mapScale":0.5, "mapRefresh":1000, "mapChunkSize": 15, + "mapDirection": true, "overviewScroll": 30, "overviewScale": 0.02, "refresh":500, diff --git a/apps/gpstrek/settings.js b/apps/gpstrek/settings.js index ebd9c13bb..1510bcba4 100644 --- a/apps/gpstrek/settings.js +++ b/apps/gpstrek/settings.js @@ -54,6 +54,12 @@ onchange: v => { writeSettings("overviewScale",v); } + }, + 'Show direction': { + value: !!settings.mapDirection, + onchange: v => { + writeSettings("mapDirection",v); + } } }; E.showMenu(menu); From 20cfa16a6c8561ed82eb6ce27aa87b638e19b9b5 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 6 Aug 2023 20:49:16 +0200 Subject: [PATCH 137/242] gpstrek - Turn compass off when a fix is available --- apps/gpstrek/widget.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js index 6887486bc..894dbde5a 100644 --- a/apps/gpstrek/widget.js +++ b/apps/gpstrek/widget.js @@ -45,7 +45,12 @@ function onPulse(e){ } function onGPS(fix) { - if(fix.fix) state.currentPos = fix; + if(fix.fix) { + state.currentPos = fix; + Bangle.setCompassPower(0, "gpstrek"); + } else { + Bangle.setCompassPower(1, "gpstrek"); + } } let radians = function(a) { From 811174436d7559e141bc4ce0870887d98d767af9 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 6 Aug 2023 20:56:20 +0200 Subject: [PATCH 138/242] gpstrek - Bump version --- apps/gpstrek/ChangeLog | 5 ++++- apps/gpstrek/metadata.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog index 0d7c06ab4..921000e82 100644 --- a/apps/gpstrek/ChangeLog +++ b/apps/gpstrek/ChangeLog @@ -15,4 +15,7 @@ Save state if route or waypoint has been chosen 0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled 0.10: Adds map view of loaded route - Automatically search for new waypoint if moving away from current target \ No newline at end of file + Automatically search for new waypoint if moving away from current target +0.11: Adds configuration + Draws direction arrows on route + Turn of compass when GPS fix is available \ No newline at end of file diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json index 1f2279fda..ec953e6e7 100644 --- a/apps/gpstrek/metadata.json +++ b/apps/gpstrek/metadata.json @@ -1,7 +1,7 @@ { "id": "gpstrek", "name": "GPS Trekking", - "version": "0.10", + "version": "0.11", "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!", "icon": "icon.png", "screenshots": [{"url":"screenInit.png"},{"url":"screenMenu.png"},{"url":"screenMap.png"},{"url":"screenLost.png"},{"url":"screenOverview.png"},{"url":"screenOverviewScroll.png"},{"url":"screenSlices.png"},{"url":"screenSlices2.png"},{"url":"screenSlices3.png"}], From a6ef1067bf85c7323e584235f51f431bae50b079 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Mon, 7 Aug 2023 08:18:40 +0200 Subject: [PATCH 139/242] gpstrek - Reset compass samples on turning it off --- apps/gpstrek/widget.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js index 894dbde5a..44acdc722 100644 --- a/apps/gpstrek/widget.js +++ b/apps/gpstrek/widget.js @@ -47,7 +47,10 @@ function onPulse(e){ function onGPS(fix) { if(fix.fix) { state.currentPos = fix; - Bangle.setCompassPower(0, "gpstrek"); + if (Bangle.isCompassOn()){ + Bangle.setCompassPower(0, "gpstrek"); + state.compassSamples = new Array(SAMPLES).fill(0) + } } else { Bangle.setCompassPower(1, "gpstrek"); } From dc3a49424e564614d8f680a563e64673200933c5 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 7 Aug 2023 11:31:26 +0100 Subject: [PATCH 140/242] fix duplicate JS files in output --- bin/firmwaremaker_c.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 4bd7fda2a..bd1e1f564 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -103,7 +103,15 @@ Promise.all(APPS.map(appid => { var app = apploader.apps.find(a => a.id==appid); if (!app) throw new Error(`App ${appid} not found`); return apploader.getAppFiles(app).then(files => { - appfiles = appfiles.concat(files); + files.forEach(f => { + var existing = appfiles.find(a=> a.name==f.name); + if (existing) { + if (existing.content !== f.content) + throw new Error(`Duplicate file ${f.name} is different`) + } else { + appfiles.push(f); + } + }); }); })).then(() => { // work out what goes in storage From de1287a685be349ba43d7cc3791d0f6a4d431359 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 7 Aug 2023 12:22:40 +0100 Subject: [PATCH 141/242] allow 8bpp maps on Bangle.js 2 --- apps/openstmap/README.md | 5 +++++ apps/openstmap/interface.html | 22 ++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md index e0fc30abd..d492bae05 100644 --- a/apps/openstmap/README.md +++ b/apps/openstmap/README.md @@ -22,6 +22,11 @@ quality, but uploads faster and takes less space). Bangle.js 2 is limited to 3bp can change settings, move the map around, and click `Get Map` again. * When you're ready, click `Upload` +**Note:** By default on Bangle.js, pre-dithered 3 bpp bitmaps will be uploaded +(which match the screen bit depth). However you can untick the `3 bit` checkbox +to use 8 bit maps, which take up 2.6x more space but look much better when +zoomed in/out. + ## Bangle.js App The Bangle.js app allows you to view a map. It also turns the GPS on diff --git a/apps/openstmap/interface.html b/apps/openstmap/interface.html index 0d9ef3152..9e22c57e4 100644 --- a/apps/openstmap/interface.html +++ b/apps/openstmap/interface.html @@ -124,10 +124,11 @@ TODO: // ---------------------------------------- Run at startup function onInit(device) { if (device && device.info && device.info.g) { - // On 3 bit devices, don't even offer the option. 3 bit is the only way + // On 3 bit devices, 3 bit is the best way + // still allow 8 bit as it makes zoom out much nicer if (device.info.g.bpp==3) { document.getElementById("3bit").checked = true; - document.getElementById("3bitdiv").style = "display:none"; + //document.getElementById("3bitdiv").style = "display:none"; } } @@ -258,15 +259,16 @@ TODO: mode:"3bit", diffusion:"bayer2" }; - /* If in 3 bit mode, go through all the data beforehand and - turn the saturation up to maximum, so when thresholded it - works a lot better */ - var imageData = ctx.getImageData(0,0,width,height); - var dstData = ctx.createImageData(width, height); - var filterOptions = {}; - imageFilterFor3BPP(imageData, dstData, filterOptions); - ctx.putImageData(dstData,0,0); } + /* Go through all the data beforehand and + turn the saturation up to maximum, so if thresholded to 3 bits it + works a lot better */ + var imageData = ctx.getImageData(0,0,width,height); + var dstData = ctx.createImageData(width, height); + var filterOptions = {}; + imageFilterFor3BPP(imageData, dstData, filterOptions); + ctx.putImageData(dstData,0,0); + console.log("Compression options", options); var w = Math.round(width / TILESIZE); var h = Math.round(height / TILESIZE); From 614c003ecaa61acc520ffe4fba7115580b7effb5 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 7 Aug 2023 14:30:30 +0100 Subject: [PATCH 142/242] osm 0.25: Enable scaled image filtering on 2v19+ firmware --- apps/openstmap/ChangeLog | 1 + apps/openstmap/metadata.json | 2 +- apps/openstmap/openstmap.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 7d51a1d0c..32951bda7 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -29,3 +29,4 @@ 0.22: Replace position marker with direction arrow 0.23: Bugfix: Enable Compass if needed 0.24: Allow zooming by clicking the screen +0.25: Enable scaled image filtering on 2v19+ firmware \ No newline at end of file diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 988a1414d..05dcf2709 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.24", + "version": "0.25", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "readme": "README.md", "icon": "app.png", diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 0a8f35f66..0a36b829e 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -52,6 +52,7 @@ exports.draw = function() { if (d!=1) { // if the two are different, add scaling s *= d; o.scale = d; + o.filter = true; // on 2v19+ enables supersampling } //console.log(ix,iy); var tx = 0|(ix/s); From f8af4bcbc9ee5689793b0ba01d1c93565752c6a0 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Mon, 7 Aug 2023 21:45:36 -0400 Subject: [PATCH 143/242] Update metadata.json --- apps/usgs/metadata.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/usgs/metadata.json b/apps/usgs/metadata.json index 7be91438f..9c918332e 100644 --- a/apps/usgs/metadata.json +++ b/apps/usgs/metadata.json @@ -9,6 +9,8 @@ "readme": "README.md", "storage": [ {"name":"usgs.app.js","url":"app.js"}, + {"name":"usgs.settings.js","url":"settings.js"}, {"name":"usgs.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"myapp.json"}] } From a3d2e0a3b5f120b78459547dd6ebfa19b9fa6291 Mon Sep 17 00:00:00 2001 From: Dennis Kueper Date: Tue, 8 Aug 2023 08:05:38 +0200 Subject: [PATCH 144/242] Get time zone from settings for showing the clock --- apps/teatimer/ChangeLog | 1 + apps/teatimer/app.js | 3 ++- apps/teatimer/metadata.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/teatimer/ChangeLog b/apps/teatimer/ChangeLog index db8dd270b..31b495ecf 100644 --- a/apps/teatimer/ChangeLog +++ b/apps/teatimer/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fix issue setting colors after showMessage 0.03: Fix BG/FG Color if e.g. theme background is black +0.04: Get time zone from settings for showing the clock diff --git a/apps/teatimer/app.js b/apps/teatimer/app.js index c394b5e00..fbaed4d62 100644 --- a/apps/teatimer/app.js +++ b/apps/teatimer/app.js @@ -13,7 +13,8 @@ const states = { stop: 32 // timer stopped }; var state = states.start; -E.setTimeZone(1); +let setting = require("Storage").readJSON("setting.json",1); +E.setTimeZone(setting.timezone); // Title showing current time function appTitle() { diff --git a/apps/teatimer/metadata.json b/apps/teatimer/metadata.json index b5cdce92e..a298a0e2b 100644 --- a/apps/teatimer/metadata.json +++ b/apps/teatimer/metadata.json @@ -1,7 +1,7 @@ { "id": "teatimer", "name": "Tea Timer", - "version": "0.03", + "version": "0.04", "description": "A simple timer. You can easyly set up the time.", "icon": "teatimer.png", "type": "app", From 5ab81829a250d32b5f2cae31c4bd8091de13df38 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Tue, 8 Aug 2023 07:28:56 +0100 Subject: [PATCH 145/242] lcdclock: load clockinfo before widgets This matches the behaviour of other clocks, meaning any dependencies between widgets and clock info is consistent See also #2900 --- apps/lcdclock/ChangeLog | 3 ++- apps/lcdclock/app.js | 49 +++++++++++++++++++------------------ apps/lcdclock/metadata.json | 2 +- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/apps/lcdclock/ChangeLog b/apps/lcdclock/ChangeLog index 56ea03c2c..07bb3cdfd 100644 --- a/apps/lcdclock/ChangeLog +++ b/apps/lcdclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Use clock_info module as an app -0.03: clock_info now uses app name to maintain settings specifically for this clock face \ No newline at end of file +0.03: clock_info now uses app name to maintain settings specifically for this clock face +0.04: clock_info is loaded before widgets to match other clocks diff --git a/apps/lcdclock/app.js b/apps/lcdclock/app.js index 3808f46fe..c7789a85f 100644 --- a/apps/lcdclock/app.js +++ b/apps/lcdclock/app.js @@ -30,24 +30,6 @@ let draw = function() { }, 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; - delete Graphics.prototype.setFont7Seg; - // remove info menu - clockInfoMenu.remove(); - delete clockInfoMenu; - clockInfoMenu2.remove(); - delete clockInfoMenu2; - // reset theme - g.setTheme(oldTheme); - }}); -// Load widgets -Bangle.loadWidgets(); var R = Bangle.appRect; R.x+=1; R.y+=1; @@ -57,12 +39,6 @@ R.w-=2; R.h-=2; var midX = R.x+R.w/2; var barY = 80; -// Clear the screen once, at startup -let oldTheme = g.theme; -g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(1); -g.fillRect({x:R.x, y:R.y, w:R.w, h:R.h, r:8}).clearRect(R.x,barY,R.w,barY+1).clearRect(midX,R.y,midX+1,barY); -draw(); -setTimeout(Bangle.drawWidgets,0); let clockInfoDraw = (itm, info, options) => { let texty = options.y+41; @@ -81,4 +57,29 @@ let clockInfoDraw = (itm, info, options) => { let clockInfoItems = require("clock_info").load(); let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x, y:R.y, w:midX-2, h:barY-R.y-2, draw : clockInfoDraw}); let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+2, y:R.y, w:midX-3, h:barY-R.y-2, draw : clockInfoDraw}); + +// 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; + delete Graphics.prototype.setFont7Seg; + // remove info menu + clockInfoMenu.remove(); + delete clockInfoMenu; + clockInfoMenu2.remove(); + delete clockInfoMenu2; + // reset theme + g.setTheme(oldTheme); + }}); +// Load widgets +Bangle.loadWidgets(); +// Clear the screen once, at startup +let oldTheme = g.theme; +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(1); +g.fillRect({x:R.x, y:R.y, w:R.w, h:R.h, r:8}).clearRect(R.x,barY,R.w,barY+1).clearRect(midX,R.y,midX+1,barY); +draw(); +setTimeout(Bangle.drawWidgets,0); } diff --git a/apps/lcdclock/metadata.json b/apps/lcdclock/metadata.json index b144c125e..4a98e8124 100644 --- a/apps/lcdclock/metadata.json +++ b/apps/lcdclock/metadata.json @@ -1,6 +1,6 @@ { "id": "lcdclock", "name": "LCD Clock", - "version":"0.03", + "version":"0.04", "description": "A Casio-style clock, with ClockInfo areas at the top and bottom. Tap them and swipe up/down to toggle between different information", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], From 99b72333e5bc61f48b2e08f96924954ec95a1898 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Tue, 8 Aug 2023 07:57:34 +0100 Subject: [PATCH 146/242] btadv: integration with existing Bangle.bleAdvert --- apps/btadv/app.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 89685d375..d3d720e65 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -774,10 +774,12 @@ enableSensors(); }, ); - if(!(Bangle as any).bleAdvert) - (Bangle as any).bleAdvert = {}; + type BleAdvert = { [key: string]: number[] }; + const bangle2 = Bangle as { + bleAdvert?: BleAdvert | BleAdvert[]; + }; + const cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : []; - const cycle = []; for(const id in ad){ const serv = ad[id as BleServ]; let value; @@ -791,6 +793,8 @@ enableSensors(); cycle.push({ [id]: value || [] }); } + bangle2.bleAdvert = cycle; + NRF.setAdvertising( cycle, { From b86d2bd841db57b348c5f5026f149ce99816f18e Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Tue, 8 Aug 2023 07:49:30 +0100 Subject: [PATCH 147/242] bootgattbat: handle bleAdvert array --- apps/bootgattbat/ChangeLog | 1 + apps/bootgattbat/boot.js | 18 +++++++++++++++++- apps/bootgattbat/metadata.json | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/bootgattbat/ChangeLog b/apps/bootgattbat/ChangeLog index 2a37193a3..df07f6ad0 100644 --- a/apps/bootgattbat/ChangeLog +++ b/apps/bootgattbat/ChangeLog @@ -1 +1,2 @@ 0.01: Initial release. +0.02: Handle the case where other apps have set bleAdvert to an array diff --git a/apps/bootgattbat/boot.js b/apps/bootgattbat/boot.js index d67b766b5..34d9f8d93 100644 --- a/apps/bootgattbat/boot.js +++ b/apps/bootgattbat/boot.js @@ -1,6 +1,22 @@ (() => { function advertiseBattery() { - Bangle.bleAdvert[0x180F] = [E.getBattery()]; + 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); } diff --git a/apps/bootgattbat/metadata.json b/apps/bootgattbat/metadata.json index 95a521f47..f67b4507d 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.01", + "version": "0.02", "description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n", "icon": "bluetooth.png", "type": "bootloader", From 6a4c1f2c273bf56b2ddd71197b32c4d9cc807750 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Tue, 8 Aug 2023 08:14:48 +0100 Subject: [PATCH 148/242] btadv: drop accel button --- apps/btadv/app.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index d3d720e65..1d9501175 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -130,8 +130,7 @@ const settings: BtAdvMap = { mag: false, }; -const idToName: BtAdvMap = { - acc: "Acceleration", +const idToName: BtAdvMap = { bar: "Barometer", gps: "GPS", hrm: "HRM", @@ -223,15 +222,6 @@ const btnLayout = new Layout( { type: "h", c: [ - { - type: "btn", - label: idToName.acc, - id: "acc", - cb: () => {}, - ...btnStyle, - col: colour.on, - btnBorder: colour.on, - }, { type: "btn", label: "Back", From 8cfaf5591edf6f6fcbadec0344c36600f239367d Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Tue, 8 Aug 2023 08:14:57 +0100 Subject: [PATCH 149/242] btadv: generate JS --- apps/btadv/app.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/btadv/app.js b/apps/btadv/app.js index 94d7f7ad7..b72a8127a 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -27,7 +27,6 @@ var _a; mag: false, }; var idToName = { - acc: "Acceleration", bar: "Barometer", gps: "GPS", hrm: "HRM", @@ -75,7 +74,6 @@ var _a; { type: "h", c: [ - __assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour_1.on, btnBorder: colour_1.on }), __assign({ type: "btn", label: "Back", cb: function () { setBtnsShown_1(false); } }, btnStyle), @@ -443,9 +441,8 @@ var _a; NRF.setServices(ad, { uart: false, }); - if (!Bangle.bleAdvert) - Bangle.bleAdvert = {}; - var cycle = []; + var bangle2 = Bangle; + var cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : []; for (var id in ad) { var serv = ad[id]; var value = void 0; @@ -455,6 +452,7 @@ var _a; } cycle.push((_a = {}, _a[id] = value || [], _a)); } + bangle2.bleAdvert = cycle; NRF.setAdvertising(cycle, { interval: 100, }); From 71deca86df601651f0db3146f1799b08f5f3daf9 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Tue, 8 Aug 2023 08:03:32 +0100 Subject: [PATCH 150/242] bthometemp: handle bleAdvert array --- apps/bthometemp/ChangeLog | 1 + apps/bthometemp/app.js | 17 ++++++++++++++++- apps/bthometemp/metadata.json | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/bthometemp/ChangeLog b/apps/bthometemp/ChangeLog index 5560f00bc..480780ec5 100644 --- a/apps/bthometemp/ChangeLog +++ b/apps/bthometemp/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Handle the case where other apps have set bleAdvert to an array diff --git a/apps/bthometemp/app.js b/apps/bthometemp/app.js index 7b55777d1..cf74c7937 100644 --- a/apps/bthometemp/app.js +++ b/apps/bthometemp/app.js @@ -23,7 +23,7 @@ function onTemperature(p) { var temp100 = Math.round(avrTemp*100); var pressure100 = Math.round(avrPressure*100); - Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information + var advert = [ 0x40, /* BTHome Device Information bit 0: "Encryption flag" bit 1-4: "Reserved for future use" bit 5-7: "BTHome Version" */ @@ -37,6 +37,21 @@ function onTemperature(p) { 0x04, // Pressure, 16 bit 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); } diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json index 4bfd08c31..8ffb22c83 100644 --- a/apps/bthometemp/metadata.json +++ b/apps/bthometemp/metadata.json @@ -1,7 +1,7 @@ { "id": "bthometemp", "name": "BTHome Temperature and Pressure", "shortName":"BTHome T", - "version":"0.01", + "version":"0.02", "description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard", "icon": "app.png", "tags": "bthome,bluetooth,temperature", From 90db121cc926b27b55ecee145bbfa280c5124290 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:52:50 -0400 Subject: [PATCH 151/242] Update app.js --- apps/usgs/app.js | 77 ++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/apps/usgs/app.js b/apps/usgs/app.js index 6d01c6a00..e0b515018 100644 --- a/apps/usgs/app.js +++ b/apps/usgs/app.js @@ -1,74 +1,67 @@ -var loc = '03272100'; // Configurable location -var keys = ['Gage', 'Discharge', 'Temperature']; // Configurable values -var shortenedName = {"Gage":"Ga","Discharge":"Dis","Temperature":"Temp"}; var dataStreams = {}; // Will hold directions to Datastreams and units, with above keys -//var http = require("http"); +var FILE = "usgs.json"; + // Load settings + var settings = Object.assign({ + loc: '03272100', + keys: {'Gage height': true, 'Discharge': true, 'Temperature, water': true}, + shortenedName: {"Gage height":"Ga","Discharge":"Dis","Temperature, water":"Temp"}, + tempUnitF: true, + }, require('Storage').readJSON(FILE, true) || {}); function fetchStartup() { - /*http.get( - "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + - loc + - "')/Datastreams?$expand=Observations($orderby=phenomenonTime%20desc;$top=1;$select=result)&$select=unitOfMeasurement,description" -).then(d => handleStartup(d.json().value)); -*/ - handleStartup(JSON.parse(`{"value":[{"unitOfMeasurement":{"name":"V","symbol":"V","definition":""},"description":"DCP battery voltage / USGS-03272100-1964fee2501b43c5b7b807b687319588","Observations":[{"result":"13.4"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1964fee2501b43c5b7b807b687319588')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Cubic Feet per Second","symbol":"ft^3/s","definition":""},"description":"Discharge / USGS-03272100-1db72201226e4f50a94d0b65abc8e7a5","Observations":[{"result":"665"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('1db72201226e4f50a94d0b65abc8e7a5')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Feet","symbol":"ft","definition":""},"description":"Gage height / USGS-03272100-56a7245f4b47438cb79c40f0d00605ba","Observations":[{"result":"1.92"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('56a7245f4b47438cb79c40f0d00605ba')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"},{"unitOfMeasurement":{"name":"Degrees Centigrade","symbol":"degC","definition":""},"description":"Temperature, water / USGS-03272100-f8e8a724a5c3498ca11bf1a31be5a537","Observations":[{"result":"26.1"}],"Observations@iot.nextLink":"https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams('f8e8a724a5c3498ca11bf1a31be5a537')/Observations?$top=1&$skip=1&$select=result&$orderby=phenomenonTime+desc,%40iot.id+asc"}]}`).value); + uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + + settings.loc + + "')/Datastreams?$expand=Observations($orderby=phenomenonTime%20desc;$top=1;$select=result)&$select=unitOfMeasurement,description"; + if (Bangle.http) { + Bangle.http(uri, {timeout:10000}).then(d => handleStartup(JSON.parse(d.resp).value)); + } } function handleStartup(data) { for (var key1 in data) { - desc = data[key1].description; - for (var key2 in keys) { - if (desc.indexOf(keys[key2]) != -1) { - if (data[key1].unitOfMeasurement.symbol === "degC") { - symbol = "F"; - result = (data[key1].Observations[0].result * 9 / 5) + 32; - } else { - symbol = data[key1].unitOfMeasurement.symbol; - result = data[key1].Observations[0].result; - } - dataStreams[keys[key2]] = JSON.parse( + desc = data[key1].description.split(" / ")[0]; + if (settings.keys[desc]) { + if (data[key1].unitOfMeasurement.symbol === "degC" && settings.tempUnitF) { + symbol = "F"; + result = (data[key1].Observations[0].result * 9 / 5) + 32; + } else { + symbol = data[key1].unitOfMeasurement.symbol; + result = data[key1].Observations[0].result; + } + dataStreams[desc] = JSON.parse( '{"unit":"' + symbol + '","value":"' + result + '"}' ); - } } } - console.log(dataStreams); + displayData(dataStreams); } -function displayData() { +function displayData(dataStreams) { g.clear(); g.setFont("Vector",20); g.setFontAlign(0,0); string = ""; - for (var key in keys) { - unit = dataStreams[keys[key]].unit; - value = dataStreams[keys[key]].value; - if (shortenedName[keys[key]]) { - name = shortenedName[keys[key]]; + for (var key in dataStreams) { + unit = dataStreams[key].unit; + value = dataStreams[key].value; + if (settings.shortenedName[key]) { + name = settings.shortenedName[key]; } else { - name = keys[key]; + name = key; } string += name+": "+value+" "+unit+"\n"; } var date = new Date(); var hours = date.getHours(); var minutes = date.getMinutes(); + var seconds = date.getSeconds(); // Format the time as a string var timeString = hours.toString().padStart(2, "0") + ":" + - minutes.toString().padStart(2, "0"); + minutes.toString().padStart(2, "0") + ":" + + seconds.toString().padStart(2,"0"); E.showMessage(string,timeString); } - fetchStartup(); - - -displayData(); - -setWatch(() => { - console.log("button"); - fetchStartup(); - displayData(); -}, BTN1, {repeat:true}); From 3d24af5b7087405c762e5cc86776fc56c80035a0 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:54:31 -0400 Subject: [PATCH 152/242] Create usgs.settings.js --- apps/usgs/usgs.settings.js | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 apps/usgs/usgs.settings.js diff --git a/apps/usgs/usgs.settings.js b/apps/usgs/usgs.settings.js new file mode 100644 index 000000000..bbecfbb9d --- /dev/null +++ b/apps/usgs/usgs.settings.js @@ -0,0 +1,73 @@ +(function(back) { + var submenu = { + "" : { + "title" : "-- DataStreams --" + }, + "< Back" : function() { E.showMenu(menu); }, + }; + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + +function popSubMenu() { + data = getDataStreams(); +} +function popSubMenuData(data) { + console.log(data); + Object.keys(data).forEach(function(k){ + var name = data[k].description.split(" / ")[0]; + console.log(name); + if (!settings.keys[name]) { + // Setting doesn't exist, so we assume it's false + settings.keys[name] = false; + } + submenu[name]= {value:settings.keys[name], onchange: v => { + settings.keys[name] = v; + writeSettings(); + }}; + }); + E.showMenu(submenu); + } + var menu = { + "" : { "title" : "App Name" }, + "< Back" : () => back(), + 'Temp unit': { + value: !!settings.tempUnitF, // !! converts undefined to false + format: v => v?"F":"C", + onchange: v => { + settings.tempUnitF = v; + writeSettings(); + } + }, + 'Location': { + value: settings.loc, + onchange: () => { + setTimeout(() => { + keyboard.input({text:settings.loc}).then(result => { + settings.loc = result; + writeSettings(); + }); + }, 100); + } + }, + "Submenu" : function() { popSubMenu();}, + }; + + var keyboard = "textinput"; + try {keyboard = require(keyboard);} catch(e) {keyboard = null;} + if (!keyboard) delete menu.Location; + + +function getDataStreams() { + uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + + settings.loc + + "')/Datastreams?$select=description"; + if (Bangle.http) { + Bangle.http(uri, {timeout:10000}).then(d => {popSubMenuData(JSON.parse(d.resp).value);}); + } +} + + // Show the menu + E.showMenu(menu); +}); From 6b0d69bd2785eb5640e8ad735dd30bffdf3fd41a Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:56:20 -0400 Subject: [PATCH 153/242] Rename usgs.settings.js to settings.js --- apps/usgs/{usgs.settings.js => settings.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/usgs/{usgs.settings.js => settings.js} (100%) diff --git a/apps/usgs/usgs.settings.js b/apps/usgs/settings.js similarity index 100% rename from apps/usgs/usgs.settings.js rename to apps/usgs/settings.js From 268e6066b66c0934337e9e9fa70ce5c6a11f58cf Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Tue, 8 Aug 2023 10:51:00 -0400 Subject: [PATCH 154/242] Update README.md --- apps/usgs/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/usgs/README.md b/apps/usgs/README.md index eff3919c6..def8f80b4 100644 --- a/apps/usgs/README.md +++ b/apps/usgs/README.md @@ -1,17 +1,18 @@ -# App Name +# USGS Data reporting More information on making apps: * http://www.espruino.com/Bangle.js+First+App * http://www.espruino.com/Bangle.js+App+Loader -Describe the app... - -Add screen shots (if possible) to the app folder and link then into this file with ![](.png) +Simple app that pulls data from internet API regarding water/stream/river conditions. Useful for fishing or other water sports. ## Usage -Describe how to use it +API information can be found here: +https://labs.waterdata.usgs.gov/docs/sensorthings/index.html + +Location can be found ## Features From fb2ffa4957ed980b8a0d2dbf4d4c25a127892f99 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 8 Aug 2023 16:34:09 +0100 Subject: [PATCH 155/242] Ensure escape sequence is double-quoted (hopefully fix issues with openstmap and Gadgetbridge) --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index b8813ab92..5b8b5fdfd 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit b8813ab92ceb70fb8ec6a7de6baaec88f6b5026f +Subproject commit 5b8b5fdfd68164358ecbfbdd0f882404f5e3b0c4 From 57a9dd027f607198e6fd5d2f530f9163560aa168 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 8 Aug 2023 17:14:00 +0100 Subject: [PATCH 156/242] pebblepp 0.04: Ensure we only scale down clockinfo text if it really won't fit --- apps/pebblepp/ChangeLog | 3 ++- apps/pebblepp/app.js | 6 +++--- apps/pebblepp/metadata.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/pebblepp/ChangeLog b/apps/pebblepp/ChangeLog index f3886a42e..83be498bd 100644 --- a/apps/pebblepp/ChangeLog +++ b/apps/pebblepp/ChangeLog @@ -1,4 +1,5 @@ 0.01: First release 0.02: clock_info now uses app name to maintain settings specifically for this clock face ensure clockinfo text is usppercase (font doesn't render lowercase) -0.03: Use smaller font if clock_info test doesn't fit in area \ No newline at end of file +0.03: Use smaller font if clock_info test doesn't fit in area +0.04: Ensure we only scale down clockinfo text if it really won't fit \ No newline at end of file diff --git a/apps/pebblepp/app.js b/apps/pebblepp/app.js index 5121f450e..330d79618 100644 --- a/apps/pebblepp/app.js +++ b/apps/pebblepp/app.js @@ -97,10 +97,10 @@ let clockInfoDraw = (itm, info, options) => { } g.setFontLECO1976Regular22().setFontAlign(0, 0); var txt = info.text.toString().toUpperCase(); - if (g.stringWidth(txt) > options.w-4) // if too big, smaller font + if (g.stringWidth(txt) > options.w) // if too big, smaller font g.setFontLECO1976Regular14(); - if (g.stringWidth(txt) > options.w-4) {// if still too big, split to 2 lines - var l = g.wrapString(txt, options.w-2); + if (g.stringWidth(txt) > options.w) {// if still too big, split to 2 lines + var l = g.wrapString(txt, options.w); txt = l.slice(0,2).join("\n") + (l.length>2)?"...":""; } g.drawString(txt, midx,options.y+options.h-12); // draw the text diff --git a/apps/pebblepp/metadata.json b/apps/pebblepp/metadata.json index 881b558db..e2be44ea0 100644 --- a/apps/pebblepp/metadata.json +++ b/apps/pebblepp/metadata.json @@ -2,7 +2,7 @@ "id": "pebblepp", "name": "Pebble++ Clock", "shortName": "Pebble++", - "version": "0.03", + "version": "0.04", "description": "A pebble style clock (based on the 'Pebble Clock' app) but with two configurable ClockInfo items at the top", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], From 9e1bb92ad76cf2b390243d138a15c47b0756e030 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:05:14 -0400 Subject: [PATCH 157/242] Update README.md --- apps/usgs/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/usgs/README.md b/apps/usgs/README.md index def8f80b4..16d183521 100644 --- a/apps/usgs/README.md +++ b/apps/usgs/README.md @@ -12,20 +12,21 @@ Simple app that pulls data from internet API regarding water/stream/river condit API information can be found here: https://labs.waterdata.usgs.gov/docs/sensorthings/index.html -Location can be found +Location can be found via map here: +https://maps.waterdata.usgs.gov/mapper/ +Find the site you're looking for and note the "site number". This will be the "location" setting. +![image](https://github.com/inhof009/BangleApps/assets/141580984/e16b79ba-a442-480f-b481-b866a10c9c13) + +Keyboard is required to change location setting. ## Features -Name the function +Simple interface will display most current data for the specified location when the app is opened. Settings can change the location, or which data is displayed. ## Controls -Name the buttons and what they are used for - -## Requests - -Name who should be contacted for support/update requests +No controls currently ## Creator -Your name +inhof009 From 54972a9c604a6906d85770522088a0b1d9f57e60 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:07:12 -0400 Subject: [PATCH 158/242] Update README.md --- apps/usgs/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/usgs/README.md b/apps/usgs/README.md index 16d183521..304c1b761 100644 --- a/apps/usgs/README.md +++ b/apps/usgs/README.md @@ -15,6 +15,7 @@ https://labs.waterdata.usgs.gov/docs/sensorthings/index.html Location can be found via map here: https://maps.waterdata.usgs.gov/mapper/ Find the site you're looking for and note the "site number". This will be the "location" setting. + ![image](https://github.com/inhof009/BangleApps/assets/141580984/e16b79ba-a442-480f-b481-b866a10c9c13) Keyboard is required to change location setting. From 24ab25bb534a1a475996ba8abe8a63915a8e39d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Tue, 8 Aug 2023 22:48:30 +0100 Subject: [PATCH 159/242] Ensure progress toast is visible above modal dialogs --- css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/main.css b/css/main.css index 2ea2ab40d..e8330b868 100644 --- a/css/main.css +++ b/css/main.css @@ -54,7 +54,7 @@ a.btn.btn-link.dropdown-toggle { #toastcontainer { position:fixed; bottom:8px;left:0px;right:0px; - z-index: 100; + z-index: 500; } .hero { padding-bottom: 1rem; From 2508fb7374afa4e7d69afc8061d2a1490614f319 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 9 Aug 2023 08:37:20 +0100 Subject: [PATCH 160/242] update description, attempt to fix settings page --- apps/usgs/metadata.json | 6 +++--- apps/usgs/settings.js | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/usgs/metadata.json b/apps/usgs/metadata.json index 9c918332e..80bb5a325 100644 --- a/apps/usgs/metadata.json +++ b/apps/usgs/metadata.json @@ -2,9 +2,9 @@ "name": "USGS Data fetching app", "shortName":"USGS", "version":"0.01", - "description": "App that fetches USGS data from configurable location", + "description": "App that fetches [USGS water data](https://maps.waterdata.usgs.gov/) for a configurable location (requires connection to Android phone)", "icon": "app.png", - "tags": "", + "tags": "outdoors,exercise,http", "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ @@ -12,5 +12,5 @@ {"name":"usgs.settings.js","url":"settings.js"}, {"name":"usgs.img","url":"app-icon.js","evaluate":true} ], - "data": [{"name":"myapp.json"}] + "data": [{"name":"usgs.json"}] } diff --git a/apps/usgs/settings.js b/apps/usgs/settings.js index bbecfbb9d..07abaf88f 100644 --- a/apps/usgs/settings.js +++ b/apps/usgs/settings.js @@ -1,4 +1,12 @@ (function(back) { + var FILE = "usgs.json"; + var settings = Object.assign({ + loc: '03272100', + keys: {'Gage height': true, 'Discharge': true, 'Temperature, water': true}, + shortenedName: {"Gage height":"Ga","Discharge":"Dis","Temperature, water":"Temp"}, + tempUnitF: true, + }, require('Storage').readJSON(FILE, true) || {}); + var submenu = { "" : { "title" : "-- DataStreams --" From 89737153a07ed6bb1ac473b8c51e4d5d9ee948b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Wed, 9 Aug 2023 08:50:49 +0100 Subject: [PATCH 161/242] Implement API for activity fetching --- apps/android/ChangeLog | 3 ++- apps/android/boot.js | 29 ++++++++++++++++++++++++++++- apps/android/metadata.json | 2 +- apps/gbridge/PROTOCOL.md | 31 +++++++++++++++++++++++++++++++ apps/gbridge/widget.js | 2 +- apps/health/ChangeLog | 3 ++- apps/health/lib.js | 31 ++++++++++++++++++++++++++++++- apps/health/metadata.json | 2 +- 8 files changed, 96 insertions(+), 7 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 4587c0911..f2a0c5b3f 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -29,4 +29,5 @@ 0.28: Navigation messages no longer launch the Maps view unless they're new 0.29: Support for http request xpath return format 0.30: Send firmware and hardware versions on connection - Allow alarm enable/disable \ No newline at end of file + Allow alarm enable/disable +0.31: Implement API for activity fetching diff --git a/apps/android/boot.js b/apps/android/boot.js index 018ea7561..54b1cc8f4 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -193,10 +193,37 @@ Bangle.on('HRM',actHRMHandler); actInterval = setInterval(function() { var steps = Bangle.getStepCount(); - gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM }); + gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 }); lastSteps = steps; }, event.int*1000); }, + // {t:"actfetch", ts:long} + "actfetch": function() { + gbSend({t: "actfetch", state: "start"}); + var actCount = 0; + var actCb = function(r) { + // The health lib saves the samples at the start of the 10-minute block + // However, GB expects them at the end of the block, so let's offset them + // here to keep a consistent API in the health lib + var sampleTs = r.date.getTime() + 600000; + if (sampleTs >= event.ts) { + gbSend({ + t: "act", + ts: sampleTs, + stp: r.steps, + hrm: r.bpm, + mov: r.movement + }); + actCount++; + } + } + if (event.ts != 0) { + require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb); + } else { + require("health").readFullDatabase(actCb); + } + gbSend({t: "actfetch", state: "end", count: actCount}); + }, "nav": function() { event.id="nav"; if (event.instr) { diff --git a/apps/android/metadata.json b/apps/android/metadata.json index e875c2072..8d65d32e3 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.30", + "version": "0.31", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", diff --git a/apps/gbridge/PROTOCOL.md b/apps/gbridge/PROTOCOL.md index 7191ca0b1..cb86a1314 100644 --- a/apps/gbridge/PROTOCOL.md +++ b/apps/gbridge/PROTOCOL.md @@ -59,6 +59,26 @@ Send a response to a notification from phone * n can be one of "dismiss", "dismiss all", "open", "mute", "reply" * id, tel and message are optional +## activity data + +```json +{ + "t": "act", + "ts": 1692226800000, + "stp": 26, + "hrm": 70, + "mov": 10, + "rt": 1 +} +``` + +* `ts` is the sample timestamp, in milliseconds since the unix epoch +* `stp` is the number of steps +* `hrm` is heart rate, in bpm +* `mov` is the movement intensity (todo: range?) +* `rt` whether it is a real-time sample + + # Phone -> Watch ## show notification @@ -177,3 +197,14 @@ n is the intensity * hum is the humidity * txt is the weather condition * loc is the location + +## fetch activity data + +```json +{ + "t": "actfetch", + "ts": 1692226800000 +} +``` + +* `ts` is the start timestamp, in milliseconds since the unix epoch diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index 3b5f2c780..6f4b945a5 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -263,7 +263,7 @@ function sendActivity(hrm) { var steps = currentSteps - lastSentSteps; lastSentSteps = currentSteps; - gbSend({ t: "act", stp: steps, hrm:hrm }); + gbSend({ t: "act", stp: steps, hrm:hrm, rt:1 }); } // Battery monitor diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index da68bc0e7..6bc15be83 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -25,4 +25,5 @@ 0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written) 0.25: lib.read* methods now return correctly scaled movement movement graph in app is now an average, not sum - fix 11pm slot for daily HRM \ No newline at end of file + fix 11pm slot for daily HRM +0.26: Implement API for activity fetching diff --git a/apps/health/lib.js b/apps/health/lib.js index 7ecbd5bff..8f3cc800e 100644 --- a/apps/health/lib.js +++ b/apps/health/lib.js @@ -37,7 +37,36 @@ exports.readAllRecords = function(d, cb) { } idx += DB_RECORD_LEN; // +1 because we have an extra record with totals for the end of the day } -} +}; + +// Read the entire database. There is no guarantee that the months are read in order. +exports.readFullDatabase = function(cb) { + require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => { + console.log(val); + var parts = val.split('-'); + var y = parseInt(parts[1]); + var mo = parseInt(parts[2].replace('.raw', '')); + + exports.readAllRecords(new Date(y, mo, 1), (r) => { + r.date = new Date(y, mo, r.day, r.hr, r.min); + cb(r); + }); + }); +}; + +// Read all records per day, until the current time. +// There may be some records for the day of the timestamp previous to the timestamp +exports.readAllRecordsSince = function(d, cb) { + var currentDate = new Date().getTime(); + var di = d; + while (di.getTime() <= currentDate) { + exports.readDay(di, (r) => { + r.date = new Date(di.getFullYear(), di.getMonth(), di.getDate(), r.hr, r.min); + cb(r); + }); + di.setDate(di.getDate() + 1); + } +}; // Read daily summaries from the given month exports.readDailySummaries = function(d, cb) { diff --git a/apps/health/metadata.json b/apps/health/metadata.json index 5ff3bb3a0..10c8268cb 100644 --- a/apps/health/metadata.json +++ b/apps/health/metadata.json @@ -2,7 +2,7 @@ "id": "health", "name": "Health Tracking", "shortName": "Health", - "version": "0.25", + "version": "0.26", "description": "Logs health data and provides an app to view it", "icon": "app.png", "tags": "tool,system,health", From 4ff35f0787823090a7b408231d51e7f89ed57756 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 9 Aug 2023 09:16:33 +0100 Subject: [PATCH 162/242] minor tweaks after #2889 --- apps/android/boot.js | 7 +- apps/gbridge/PROTOCOL.md | 211 +-------------------------------------- 2 files changed, 4 insertions(+), 214 deletions(-) diff --git a/apps/android/boot.js b/apps/android/boot.js index 54b1cc8f4..a8027a67c 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -292,10 +292,9 @@ require("messages").clearAll(); }); setInterval(sendBattery, 10*60*1000); - // Health tracking - Bangle.on('health', health=>{ - if (actInterval===undefined) // if 'realtime' we do it differently - gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); + // Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins + Bangle.on('health', h=>{ + gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement }); }); // Music control Bangle.musicControl = cmd => { diff --git a/apps/gbridge/PROTOCOL.md b/apps/gbridge/PROTOCOL.md index cb86a1314..2147dc049 100644 --- a/apps/gbridge/PROTOCOL.md +++ b/apps/gbridge/PROTOCOL.md @@ -1,210 +1 @@ -# Watch -> Phone - -## show toast - -``` -{ "t": "info", "msg": "message" } -``` - -t can be one of "info", "warn", "error" - -## report battery level - -``` -{ "t": "status", "bat": 30, "volt": 30, "chg": 0 } -``` - -* bat is in range 0 to 100 -* volt is optional and should be greater than 0 -* chg is optional and should be either 0 or 1 to indicate the watch is charging - -## find phone - -``` -{ "t": "findPhone", "n": true } -``` - -n is an boolean and toggles the find phone function - -## control music player - -``` -{ "t": "music", "n": "play" } -``` - -n can be one of "play", "pause", "playpause", "next", "previous", "volumeup", "volumedown", "forward", "rewind" - -## control phone call - -``` -{ "t": "call", "n": "accept"} -``` - -n can be one of "accept", "end", "incoming", "outcoming", "reject", "start", "ignore" - -## react to notifications - -Send a response to a notification from phone - -``` -{ - "t": "notify", - "n": "dismiss", - "id": 2, - "tel": "+491234", - "msg": "message", -} -``` - -* n can be one of "dismiss", "dismiss all", "open", "mute", "reply" -* id, tel and message are optional - -## activity data - -```json -{ - "t": "act", - "ts": 1692226800000, - "stp": 26, - "hrm": 70, - "mov": 10, - "rt": 1 -} -``` - -* `ts` is the sample timestamp, in milliseconds since the unix epoch -* `stp` is the number of steps -* `hrm` is heart rate, in bpm -* `mov` is the movement intensity (todo: range?) -* `rt` whether it is a real-time sample - - -# Phone -> Watch - -## show notification - -``` -{ - "t": "notify", - "id": 2, - "src": "app", - "title": "titel", - "subject": "subject", - "body": "message body", - "sender": "sender", - "tel": "+491234" - } -``` - -## notification deleted - -This event is send when the user skipped a notification - -``` -{ "t": "notify-", "id": 2 } -``` - -## set alarm - -``` -{ - "t": "alarm", - "d": [ - { "h": 13, "m": 37 }, - { "h": 8, "m": 0 } - ] -} -``` - -## call state changed - -``` -{ - "t": "call", - "cmd": "accept", - "name": "name", - "number": "+491234" -} -``` - -cmd can be one of "", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end" - -## music state changed - -``` -{ - "t": "musicstate", - "state": "play", - "position": 40, - "shuffle": 0, - "repeat": 1 -} -``` - -## set music info - -``` -{ - "t": "musicinfo", - "artist": "artist", - "album": "album", - "track": "track", - "dur": 1, - "c": 2, - "n" 3 -} -``` - -* dur is the duration of the track -* c is the track count -* n is the track number - -## find device - -``` -{ - "t": "find", - "n": true -} -``` - -n toggles find device functionality - -## set constant vibration - -``` -{ - "t": "vibrate", - "n": 2 -} -``` - -n is the intensity - -## send weather - -``` -{ - "t": "weather", - "temp": 10, - "hum": 71, - "txt": "condition", - "wind": 13, - "loc": "location" -} -``` - -* hum is the humidity -* txt is the weather condition -* loc is the location - -## fetch activity data - -```json -{ - "t": "actfetch", - "ts": 1692226800000 -} -``` - -* `ts` is the start timestamp, in milliseconds since the unix epoch +For up to date protocol info, please see http://www.espruino.com/Gadgetbridge From 1309e0a457df11f0c4a5c17b9584ceadb684800f Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:10:52 -0400 Subject: [PATCH 163/242] Update settings.js Modified menu names to make more sense --- apps/usgs/settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/usgs/settings.js b/apps/usgs/settings.js index 07abaf88f..1ddde4c60 100644 --- a/apps/usgs/settings.js +++ b/apps/usgs/settings.js @@ -9,7 +9,7 @@ var submenu = { "" : { - "title" : "-- DataStreams --" + "title" : "DataStreams" }, "< Back" : function() { E.showMenu(menu); }, }; @@ -38,7 +38,7 @@ function popSubMenuData(data) { E.showMenu(submenu); } var menu = { - "" : { "title" : "App Name" }, + "" : { "title" : "USGS" }, "< Back" : () => back(), 'Temp unit': { value: !!settings.tempUnitF, // !! converts undefined to false @@ -59,7 +59,7 @@ function popSubMenuData(data) { }, 100); } }, - "Submenu" : function() { popSubMenu();}, + "DataStreams" : function() { popSubMenu();}, }; var keyboard = "textinput"; From 7ae0e8093b4a12303d3548d27c90d1ac4098ae5c Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:34:06 -0400 Subject: [PATCH 164/242] Update app.js Added button interaction. 1 press for refresh data, double press for launcher. --- apps/usgs/app.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/usgs/app.js b/apps/usgs/app.js index e0b515018..e6f2efa1e 100644 --- a/apps/usgs/app.js +++ b/apps/usgs/app.js @@ -63,5 +63,27 @@ function displayData(dataStreams) { seconds.toString().padStart(2,"0"); E.showMessage(string,timeString); } +function handleButton() { + switch(nPress) { + case 1: + fetchStartup(); + break; + case 2: + Bangle.showLauncher(); + break; + default: + Bangle.buzz(50); + } + nPress=0; +} fetchStartup(); + +nPress = 0; +tPress = 0; + +setWatch(() => { + nPress++; + clearTimeout(tPress); + tPress = setTimeout(() => {handleButton();}, 500); + }, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat: true, edge: "rising"}); From 0ed2fd7ad9eb5fbc580a72bbf307482d7d0cf1f2 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:34:38 -0400 Subject: [PATCH 165/242] Update ChangeLog --- apps/usgs/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/usgs/ChangeLog b/apps/usgs/ChangeLog index 5560f00bc..65536966a 100644 --- a/apps/usgs/ChangeLog +++ b/apps/usgs/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Changed menu names, added button interaction. From 0b1b2eee6440ce8642772eb819e16c2eea02dc20 Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:35:09 -0400 Subject: [PATCH 166/242] Update README.md --- apps/usgs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/usgs/README.md b/apps/usgs/README.md index 304c1b761..f8c91f0a6 100644 --- a/apps/usgs/README.md +++ b/apps/usgs/README.md @@ -26,7 +26,7 @@ Simple interface will display most current data for the specified location when ## Controls -No controls currently +Press button (middle on Bangle 1) once to refresh data, twice to return to launcher. ## Creator From 3a5a78ae5acc4c790210b7cf91eb47ec67fbd0cd Mon Sep 17 00:00:00 2001 From: inhof009 <141580984+inhof009@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:36:30 -0400 Subject: [PATCH 167/242] Update metadata.json --- apps/usgs/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/usgs/metadata.json b/apps/usgs/metadata.json index 80bb5a325..6140c59b9 100644 --- a/apps/usgs/metadata.json +++ b/apps/usgs/metadata.json @@ -1,7 +1,7 @@ { "id": "usgs", "name": "USGS Data fetching app", "shortName":"USGS", - "version":"0.01", + "version":"0.02", "description": "App that fetches [USGS water data](https://maps.waterdata.usgs.gov/) for a configurable location (requires connection to Android phone)", "icon": "app.png", "tags": "outdoors,exercise,http", From 6107e9474cc309b3ad2c58ff99a7e63a2e1bae3a Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 9 Aug 2023 15:16:03 +0100 Subject: [PATCH 168/242] fix minified lib after update --- apps/health/lib.min.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/health/lib.min.js b/apps/health/lib.min.js index 5d0bed2c2..e9dfad336 100644 --- a/apps/health/lib.min.js +++ b/apps/health/lib.min.js @@ -1,3 +1,4 @@ -function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\xff\xff\xff\xff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});c+= -4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\xff\xff\xff\xff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:8*b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\xff\xff\xff\xff"!=e&& -f({hr:d,min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:8*e.charCodeAt(3)});a+=4}}} \ No newline at end of file +function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,e){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=8,b=0;31>b;b++){for(var c=0;24>c;c++)for(var f=0;6>f;f++){var g=a.substr(d,4);"\xff\xff\xff\xff"!=g&&e({day:b+1,hr:c,min:10*f,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});d+= +4}d+=4}};exports.readFullDatabase=function(a){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(e=>{console.log(e);e=e.split("-");var d=parseInt(e[1]),b=parseInt(e[2].replace(".raw",""));exports.readAllRecords(new Date(d,b,1),c=>{c.date=new Date(d,b,c.day,c.hr,c.min);a(c)})})};exports.readAllRecordsSince=function(a,e){for(var d=(new Date).getTime();a.getTime()<=d;)exports.readDay(a,b=>{b.date=new Date(a.getFullYear(),a.getMonth(),a.getDate(),b.hr,b.min);e(b)}),a.setDate(a.getDate()+1)}; +exports.readDailySummaries=function(a,e){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=584,b=0;31>b;b++){var c=a.substr(d,4);"\xff\xff\xff\xff"!=c&&e({day:b+1,steps:c.charCodeAt(0)<<8|c.charCodeAt(1),bpm:c.charCodeAt(2),movement:8*c.charCodeAt(3)});d+=580}};exports.readDay=function(a,e){k(a);var d=h(a);d=require("Storage").read(d);if(void 0!==d){a=8+580*(a.getDate()-1);for(var b=0;24>b;b++)for(var c=0;6>c;c++){var f=d.substr(a,4);"\xff\xff\xff\xff"!=f&&e({hr:b,min:10* +c,steps:f.charCodeAt(0)<<8|f.charCodeAt(1),bpm:f.charCodeAt(2),movement:8*f.charCodeAt(3)});a+=4}}} \ No newline at end of file From 747c7e8eb8a7816e7ff672259bd60a4c7279fac0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 00:48:09 +0000 Subject: [PATCH 169/242] build(deps): bump core from `5b8b5fd` to `4f735fc` Bumps [core](https://github.com/espruino/EspruinoAppLoaderCore) from `5b8b5fd` to `4f735fc`. - [Commits](https://github.com/espruino/EspruinoAppLoaderCore/compare/5b8b5fdfd68164358ecbfbdd0f882404f5e3b0c4...4f735fc1c1c0c4e728571f80ad3d5ed04e633d46) --- updated-dependencies: - dependency-name: core dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 5b8b5fdfd..4f735fc1c 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 5b8b5fdfd68164358ecbfbdd0f882404f5e3b0c4 +Subproject commit 4f735fc1c1c0c4e728571f80ad3d5ed04e633d46 From 191c2c99989b59cd60bc550a60de7e9dc3b9b461 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Fri, 11 Aug 2023 07:52:46 +0100 Subject: [PATCH 170/242] bikespeedo: fix locale units setting --- apps/bikespeedo/ChangeLog | 1 + apps/bikespeedo/app.js | 2 +- apps/bikespeedo/metadata.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog index 52459d312..296b6f948 100644 --- a/apps/bikespeedo/ChangeLog +++ b/apps/bikespeedo/ChangeLog @@ -3,3 +3,4 @@ 0.03: Use default Bangle formatter for booleans 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 diff --git a/apps/bikespeedo/app.js b/apps/bikespeedo/app.js index 327f1c754..2310f1656 100644 --- a/apps/bikespeedo/app.js +++ b/apps/bikespeedo/app.js @@ -420,7 +420,7 @@ function updateClock() { // Read settings. let cfg = require('Storage').readJSON('bikespeedo.json',1)||{}; -cfg.spd = !cfg.localeUnits; // Multiplier for speed unit conversions. 0 = use the locale values for speed +cfg.spd = cfg.localeUnits ? 0 : 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed cfg.spd_unit = 'km/h'; // Displayed speed unit cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048') cfg.alt_unit = 'm'; // Displayed altitude units ('feet') diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json index 87c0ed542..4b8ff9d92 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.05", + "version": "0.06", "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources", "icon": "app.png", "screenshots": [{"url":"Screenshot.png"}], From bcc77f5f252f74a7f06d3f1cdd5d2796e9a950b0 Mon Sep 17 00:00:00 2001 From: Tim Kuhlmann Date: Fri, 11 Aug 2023 12:46:34 +0200 Subject: [PATCH 171/242] add edgeclk --- apps/edgeclk/ChangeLog | 1 + apps/edgeclk/README.md | 24 +++ apps/edgeclk/app-icon.js | 1 + apps/edgeclk/app.js | 306 +++++++++++++++++++++++++++++++++++ apps/edgeclk/app.png | Bin 0 -> 4430 bytes apps/edgeclk/metadata.json | 20 +++ apps/edgeclk/screenshot.png | Bin 0 -> 4992 bytes apps/edgeclk/screenshot2.png | Bin 0 -> 5212 bytes apps/edgeclk/screenshot3.png | Bin 0 -> 4936 bytes apps/edgeclk/settings.js | 112 +++++++++++++ 10 files changed, 464 insertions(+) create mode 100644 apps/edgeclk/ChangeLog create mode 100644 apps/edgeclk/README.md create mode 100644 apps/edgeclk/app-icon.js create mode 100644 apps/edgeclk/app.js create mode 100644 apps/edgeclk/app.png create mode 100644 apps/edgeclk/metadata.json create mode 100644 apps/edgeclk/screenshot.png create mode 100644 apps/edgeclk/screenshot2.png create mode 100644 apps/edgeclk/screenshot3.png create mode 100644 apps/edgeclk/settings.js diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/edgeclk/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md new file mode 100644 index 000000000..535a5e9df --- /dev/null +++ b/apps/edgeclk/README.md @@ -0,0 +1,24 @@ +# Edge Clock + +![Screenshot](screenshot.png) +![Screenshot](screenshot2.png) +![Screenshot](screenshot3.png) + +Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability. +It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging. +There are three progress bars that indicate day of the week, time of the day, and daily step goal. +The watch face is monochrome and allows for applying your favorite color scheme. + +The appearance is highly configurable. In the settings menu you can: +- De-/activate a buzz when the charger is connected while the watch face is active. +- Decide if month or day should be displayed first. +- Switch between 24h and 12h clock. +- Hide or display seconds.* +- Show AM/PM in place of the seconds. +- 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). + +*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well. + +The clock implements Fast Loading for faster switching to and fro. diff --git a/apps/edgeclk/app-icon.js b/apps/edgeclk/app-icon.js new file mode 100644 index 000000000..301abc93f --- /dev/null +++ b/apps/edgeclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIJGtgEDkEAn0b3P8uF8kUK1P+sFclQFCuFMkULCIMgvk+GRH/ABQRF5feGwMH7/KjcAmUGqfLzYLBg1fBYU+s1f/QRDofbDog7Xj8P/EH4f+Ao/5w4FBg8MgOB4eOAo8AgIFCDoIFJhk4Aok5DogFEGoIFBGoIdDAozgLOKNwAovv33wg0f/AFFsGGmEGwf4seOmCnBuALCg/YqFv30wgGf7AFEA")) \ No newline at end of file diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js new file mode 100644 index 000000000..dfc847b33 --- /dev/null +++ b/apps/edgeclk/app.js @@ -0,0 +1,306 @@ +{ + /* Configuration + ------------------------------------------------------------------------------*/ + + const settings = Object.assign({ + buzzOnCharge: true, + monthFirst: true, + twentyFourH: true, + showAmPm: false, + showSeconds: true, + stepGoal: 10000, + stepBar: true, + weekBar: true, + mondayFirst: true, + dayBar: true, + }, require('Storage').readJSON('edgeclk.settings.json', true) || {}); + + + /* Runtime Variables + ------------------------------------------------------------------------------*/ + + let startTimeout; + let drawInterval; + + let lcdPower = true; + let charging = Bangle.isCharging(); + + const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA'); + + const iconSize = [19, 26]; + const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA='); + const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA='); + const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA='); + + + /* Draw Functions + ------------------------------------------------------------------------------*/ + + const drawAll = function () { + const date = new Date(); + + drawDate(date); + if (settings.showSeconds) drawSecs(date); + drawTime(date); + drawLower(); + }; + + const drawLower = function (stepsOnlyCount) { + if (charging) { + drawCharge(); + } else { + drawSteps(stepsOnlyCount); + } + }; + + const drawDate = function (date) { + const top = 30; + g.reset(); + + // weekday + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, -1); // left top + g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true); + + // date + g.setFontAlign(1, -1); // right top + // Note: to save space first and last two lines of ASCII are left out. + // That is why '-' is assigned to '\' and ' ' (space) to '_'. + if (settings.monthFirst) { + g.drawString((date.getMonth()+1).toString().padStart(2, '_') + + '\\' + + date.getDate().toString().padStart(2, 0), + g.getWidth(), top + 12, true); + } else { + g.drawString('_' + + date.getDate().toString().padStart(2, 0) + + '\\' + + (date.getMonth()+1).toString(), + g.getWidth(), top + 12, true); + } + + // line/progress bar + if (settings.weekBar) { + let weekday = date.getDay(); + if (settings.mondayFirst) { + if (weekday === 0) { weekday = 7; } + } else { + weekday += 1; + } + drawBar(top, weekday/7); + } else { + drawLine(top); + } + }; + + const drawTime = function (date) { + const top = 72; + g.reset(); + + const h = date.getHours(); + g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9) + g.setFontAlign(-1, -1); // left top + g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0), + 0, top+12, true); + g.setFontAlign(0, -1); // center top + g.drawString(':', g.getWidth()/2, top+12, false); + const m = date.getMinutes(); + g.setFontAlign(1, -1); // right top + g.drawString(m.toString().padStart(2, 0), + g.getWidth(), top+12, true); + + if (settings.showAmPm) { + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true); + } + + if (settings.dayBar) { + drawBar(top, (h*60+m)/1440); + } else { + drawLine(top); + } + }; + + const drawSecs = function (date) { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true); + }; + + const drawSteps = function (onlyCount) { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, 1); // left bottom + + const steps = Bangle.getHealthStatus('day').steps; + g.drawString(steps.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); + + if (onlyCount === true) { + return; + } + + const progress = steps / settings.stepGoal; + if (settings.stepBar) { + drawBar(g.getHeight() - 38, progress); + } else { + drawLine(g.getHeight() - 38); + } + + // icon + if (progress < 1) { + g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]); + } else { + g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]); + } + }; + + const drawCharge = function () { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, 1); // left bottom + + const charge = E.getBattery(); + g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); + + drawBar(g.getHeight() - 38, charge / 100); + g.drawImage(plugIcon, 0, g.getHeight() - 26); + }; + + const drawBar = function (top, progress) { + g.drawRect(0, top, g.getWidth()-1, top + 5); + g.drawRect(1, top+1, g.getWidth()-2, top + 4); + const barLen = progress > 1 ? g.getWidth() : g.getWidth() * progress; + g.drawLine(2, top+2, barLen, top + 2); + g.drawLine(2, top+3, barLen, top + 3); + }; + + const drawLine = function (top) { + const width = g.getWidth(); + g.drawLine(0, top+2, width, top + 2); + g.drawLine(0, top+3, width, top + 3); + }; + + + /* Event Handlers + ------------------------------------------------------------------------------*/ + + const onSecondInterval = function () { + const date = new Date(); + drawSecs(date); + if (date.getSeconds() === 0) { + onMinuteInterval(); + } + }; + + const onMinuteInterval = function () { + const date = new Date(); + drawTime(date); + drawLower(true); + }; + + const onMinuteIntervalStarter = function () { + drawInterval = setInterval(onMinuteInterval, 60000); + startTimeout = null; + onMinuteInterval(); + }; + + const onLcdPower = function (on) { + lcdPower = on; + if (on) { + drawAll(); + startTimers(); + } else { + stopTimers(); + } + }; + + const onMidnight = function () { + if (!lcdPower) return; + drawDate(new Date()); + // Lower part (steps/charge) will be updated every minute. + // However, to save power while on battery only step count will get updated. + // This will update icon and progress bar as well: + if (!charging) drawSteps(); + }; + + const onHealth = function () { + if (!lcdPower || charging) return; + // This will update progress bar and icon: + drawSteps(); + }; + + const onLock = function (locked) { + if (locked) return; + drawLower(); + }; + + const onCharging = function (isCharging) { + charging = isCharging; + if (isCharging && settings.buzzOnCharge) Bangle.buzz(); + if (!lcdPower) return; + drawLower(); + }; + + + /* Lifecycle Functions + ------------------------------------------------------------------------------*/ + + const registerEvents = function () { + // This is for original Bangle.js; version two has always-on display: + Bangle.on('lcdPower', onLcdPower); + + // Midnight event is triggered qhen health data is reset and a new day begins: + Bangle.on('midnight', onMidnight); + + // Health data is published via 10 mins interval: + Bangle.on('health', onHealth); + + // Lock event signals screen (un)lock: + Bangle.on('lock', onLock); + + // Charging event signals when charging status changes: + Bangle.on('charging', onCharging); + }; + + const deregisterEvents = function () { + Bangle.removeListener('lcdPower', onLcdPower); + Bangle.removeListener('midnight', onMidnight); + Bangle.removeListener('health', onHealth); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('charging', onCharging); + }; + + const startTimers = function () { + if (drawInterval) return; + if (settings.showSeconds) { + drawInterval = setInterval( onSecondInterval, 1000); + } else { + startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000); + } + }; + + const stopTimers = function () { + if (startTimeout) clearTimeout(startTimeout); + if (!drawInterval) return; + clearInterval(drawInterval); + drawInterval = null; + }; + + + /* Startup Process + ------------------------------------------------------------------------------*/ + + g.clear(); + drawAll(); + startTimers(); + registerEvents(); + + Bangle.setUI({mode: 'clock', remove: function() { + stopTimers(); + deregisterEvents(); + }}); + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} diff --git a/apps/edgeclk/app.png b/apps/edgeclk/app.png new file mode 100644 index 0000000000000000000000000000000000000000..90fbec3f3d1da6bea37559545ffd2156cd8fce5e GIT binary patch literal 4430 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 literal 0 HcmV?d00001 diff --git a/apps/edgeclk/screenshot2.png b/apps/edgeclk/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..febac2d2ce078e5b21069c5cabed790778351eed GIT binary patch literal 5212 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 HIyLt{#v(H> literal 0 HcmV?d00001 diff --git a/apps/edgeclk/screenshot3.png b/apps/edgeclk/screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..bdad9e1d50e856f072e7c8dbcb582aee1123881f GIT binary patch literal 4936 zcmeHLdsGuw8lMmpM66b+r2-mb*CH~>Ofs1ya|j+vs74J%$O^tHlbK1Fg*==LB=`ik z0zSaj2ekr6ZC6}f?NU8esuU31^;KWRRcg^=J!@%Ie3Vt&9xZ!k0tjb&&K}RM{}ImR ze)HYm{eAcT?sva1tWHUuIxJ*N2n0dHOw)|1psx{HF!%+nX*;qKbmy52lg(s`fdnT& zN&+DXumqf>j~yV9LQ>!>0&OI)1&q+fNCt`hI$%e1+nazL-Hj3EpppP+80afOQvo{+ z^m{;Ki~X_cfh~N3#UE_|BqDnHsU~xZOr<~(g&G6%5tW)ibOegXP&I+62{i)ZOSV5- zk-)Sk`vM3k8P6srrkD~FWp2)8V{;e?@>N#7_v!TJl(3=(%Z1e3)W~Dw6EBtJaWyf4 zN8jB0%DQBBgB4vp-ZZJc(Dl*qmV^;Y2YCnCyCR|@zr3-kxoeTGz3bi==l5?!;17n& z&g^N5dh~Qg*Qs}weQYk)L{`dTJL}u4zVgK{{B&JCUz*<8w$1*#HK$V!r!eF@mduW| zOFCn;d#$(gu(**^N;;y?$RBKMUH#RGny;g(PsToZfE}8%{=iyKOM1r*t_d1mfvT%t zx9~eZB#!^9b>rc%z_zrJ(??k!ez$dZ?aP}vP9IikJZw(9mR!|7iHTjmyKZJv)b2R* z*zZ;}T#i3`%bdOB{h*|jL*MXH$AwJ|#XR&PM!tD6Em8RS_!?sajLUk~=Wv_Nz4T5` zw+cWHrH^zgQ3axOIFx;7c=)6|K+?US-<;vG3eJNaR8_XL`D0B6s|+>3_$`r>u`&+fY|+zJZpO3gBjR@YE(A>PnxDJ7>l!k#d7>%GfhU#@zjYvgP#9J=TL4xCC9i*L6 zx}A1$KoFcrNHH1YDuq9)=t;>Tc^jBukk4eDxxQym3+rIgcv4Uk#q}uIwGKseII32w zpMhpGoCjp0z$q3saYQ&40&E5dOA2`k2t+^F3z5h%B=6!ZE?160-dhrTht1$R(IihA zNuB|uy+zW}QzR!NDgx;hjWD}t)|UU@(8A@B#rcnX8tVb^^F^2cMx`-}{7?SJ99Fza zGMRWO2$J$|!9(UTw75>d_A7L6-ZC<>q8bT9@M1%Xm1uHoB|LGMszTWSWp$A z(h``aTQKE|-=aoK;-sFo!d4BUg)ub+?us5{J4WhIl}$&faE$4{=>K(#-Cvyjw-~1s z%3{x<#wq{f*!_S$breYB_ksEh%Du9;-gj#zRMr>x=}xy77y(cZ1bHrf2jm)%>$wzo zF7QBh4aoIe3OpBhAiMr=a)ms5V`QA*4?Qn=ZLJIk z&LG}oP6}!d4jDRmY&bXQB5(y*XG}FhUB&CaYyc0e$(UgAUFo=TWV$pVtl(SpLD7gh zLqYw!Sr?l!;gY#o6G}F(D%rORUn7gQPM$kCIPVu|<@W6^`Lz*s%;h%3=*16IY^kDt zZFxN>Z~wTHcl7p&vz+k<@6La1N632hvd2x0d%6-v*B>*VQq9HSIhd`~HSUSo6&#t3xwRY`7ay2h$h!RwxQ2(!rs+8!^)? z@RnxbAyU)2WaG{)JD!%Unp8cp{nqh-!A~OYB>frJozPwV86XZ0%h_-%#*}>+3kpZ( z1R$A>7Ydv2oEj9B{*S?9-BE>K|11d&N3Lfz1~$CQwMYhs)-G?n$?>g~PU7r8;~D|O zujj6BlQxeX{$uBi^PP~iEMWd!TVQDI?97&-#Zf`wHGwszPHFi4g|C)Pf3#I1Eq|%( zPxGXqwM~DIL^6NKyp5+qmkk+hMR+D48!G)K5>S4k*>Zj1>De=bZAe$)E;lqdHlp&} zXgOCjuGM3D=+$4KUl|f!^J>PtzdSW*q#wiwg|;rQxm$X3<$?v(Ki+#Nsrci9VKD(U zE2?64^J6EjJ$mhnoA)XMGO*VB`wJvN(#y!c9~zH3iN*rQdgX}H$!$M { + settings.buzzOnCharge = !settings.buzzOnCharge; + save(); + }, + }, + 'Month First': { + value: settings.monthFirst, + onchange: () => { + settings.monthFirst = !settings.monthFirst; + save(); + }, + }, + '24h Clock': { + value: settings.twentyFourH, + onchange: () => { + settings.twentyFourH = !settings.twentyFourH; + save(); + }, + }, + 'Show AM/PM': { + value: settings.showAmPm, + onchange: () => { + settings.showAmPm = !settings.showAmPm; + // TODO can this be visually changed? + if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false; + save(); + }, + }, + 'Show Seconds': { + value: settings.showSeconds, + onchange: () => { + settings.showSeconds = !settings.showSeconds; + // TODO can this be visually changed? + if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false; + save(); + }, + }, + 'Step Goal': { + value: settings.stepGoal, + min: 250, + max: 50000, + step: 250, + onchange: v => { + settings.stepGoal = v; + save(); + } + }, + 'Step Progress': { + value: settings.stepBar, + onchange: () => { + settings.stepBar = !settings.stepBar; + save(); + } + }, + 'Week Progress': { + value: settings.weekBar, + onchange: () => { + settings.weekBar = !settings.weekBar; + save(); + }, + }, + 'Week Start': { + value: settings.mondayFirst, + format: () => settings.mondayFirst ? 'Monday' : 'Sunday', + onchange: () => { + settings.mondayFirst = !settings.mondayFirst; + save(); + }, + }, + 'Day Progress': { + value: settings.dayBar, + onchange: () => { + settings.dayBar = !settings.dayBar; + save(); + }, + }, + }); +}) From bb68b7e9d17afbcf59d43c9e646b1abffdd7eca2 Mon Sep 17 00:00:00 2001 From: Tim Kuhlmann Date: Sat, 12 Aug 2023 10:34:47 +0200 Subject: [PATCH 172/242] edgeclk: fix bar reset and 100k+ steps --- apps/edgeclk/ChangeLog | 1 + apps/edgeclk/app.js | 24 +++++++++++++++--------- apps/edgeclk/metadata.json | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog index 2a37193a3..da75dfbae 100644 --- a/apps/edgeclk/ChangeLog +++ b/apps/edgeclk/ChangeLog @@ -1 +1,2 @@ 0.01: Initial release. +0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps. diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index dfc847b33..9f28e2588 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -135,7 +135,8 @@ g.setFontAlign(-1, 1); // left bottom const steps = Bangle.getHealthStatus('day').steps; - g.drawString(steps.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); + g.drawString((steps < 100000 ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'), + iconSize[0] + 6, g.getHeight() - 1, true); if (onlyCount === true) { return; @@ -169,17 +170,22 @@ }; const drawBar = function (top, progress) { - g.drawRect(0, top, g.getWidth()-1, top + 5); - g.drawRect(1, top+1, g.getWidth()-2, top + 4); - const barLen = progress > 1 ? g.getWidth() : g.getWidth() * progress; - g.drawLine(2, top+2, barLen, top + 2); - g.drawLine(2, top+3, barLen, top + 3); + // draw frame + g.drawRect(0, top, g.getWidth() - 1, top + 5); + g.drawRect(1, top + 1, g.getWidth() - 2, top + 4); + // clear bar area + g.clearRect(2, top + 2, g.getWidth() - 3, top + 3); + // draw bar + const barLen = progress >= 1 ? g.getWidth() : (g.getWidth() - 4) * progress; + if (barLen < 1) return; + g.drawLine(2, top + 2, barLen + 2, top + 2); + g.drawLine(2, top + 3, barLen + 2, top + 3); }; const drawLine = function (top) { const width = g.getWidth(); - g.drawLine(0, top+2, width, top + 2); - g.drawLine(0, top+3, width, top + 3); + g.drawLine(0, top + 2, width, top + 2); + g.drawLine(0, top + 3, width, top + 3); }; @@ -251,7 +257,7 @@ // This is for original Bangle.js; version two has always-on display: Bangle.on('lcdPower', onLcdPower); - // Midnight event is triggered qhen health data is reset and a new day begins: + // Midnight event is triggered when health data is reset and a new day begins: Bangle.on('midnight', onMidnight); // Health data is published via 10 mins interval: diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json index ac12a5f62..3f72be77a 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.01", + "version": "0.02", "description": "Crisp clock with perfect readability.", "readme": "README.md", "icon": "app.png", From 0729baa3acb2069c32c716d6f2d584a5cb3dded9 Mon Sep 17 00:00:00 2001 From: Tim Kuhlmann Date: Sat, 12 Aug 2023 13:50:04 +0200 Subject: [PATCH 173/242] update edgeclk icon --- apps/edgeclk/app-icon.js | 2 +- apps/edgeclk/app.png | Bin 4430 -> 4630 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edgeclk/app-icon.js b/apps/edgeclk/app-icon.js index 301abc93f..b81918b73 100644 --- a/apps/edgeclk/app-icon.js +++ b/apps/edgeclk/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwgIJGtgEDkEAn0b3P8uF8kUK1P+sFclQFCuFMkULCIMgvk+GRH/ABQRF5feGwMH7/KjcAmUGqfLzYLBg1fBYU+s1f/QRDofbDog7Xj8P/EH4f+Ao/5w4FBg8MgOB4eOAo8AgIFCDoIFJhk4Aok5DogFEGoIFBGoIdDAozgLOKNwAovv33wg0f/AFFsGGmEGwf4seOmCnBuALCg/YqFv30wgGf7AFEA")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwgMgBAoFEuADCgP8sAFD/wLE/wXDCIIjFAAv/ABQRF5fegEPgfe5UbgEJgVS5ebBYMyr36BYdC7YXEGq4AFj8f/ED8f+ApHjAoMHjkA8HjxwFIgAFCC4IFJjk4AoodEAogXBAoI1BDoYFGL5Z3XmHv33whkfuAFE/Fgw0whuD/Fjz0wh/fuALCh/Y/Fv30wgOf7AFE")) diff --git a/apps/edgeclk/app.png b/apps/edgeclk/app.png index 90fbec3f3d1da6bea37559545ffd2156cd8fce5e..3a0bbe130d4d0164e02aec84db636f761eb61baf 100644 GIT binary patch delta 784 zcmV+r1MmFKB9~XI@v`<9BUPdP$AR`tvZ-o`UOoIk`xz5!L{Jv z$70pN#aUMeS3wZ`0C9D3Qgo3L|Cbb6#CUMrk9YSTckcjyv&vMnCjqFMWn@xGA(vkj zg0Bc6jDAEgDlt=^6U8(<$Jaf4e7%eFEbnuFjsc}$GQcMgCx4i3Si~E|Gn0S*p<@e*aP zd%Qc;-P^xs+Wq|iSrT%b%+TIP000j@X;fHrSWQeiW3#>ifdV8lV`61uWH~u4IAvxr zEi`6fV=ZAYWil;eH8e0|Wn^PvGh{WBV+5!SI5aReH!w3YH8eLllNSYW3o$ZOF*7m5hGG<~eG&C`0En#D2 zGA&{@H#KEpHZ)^pWnq(12TBVuGE^}$Iy5voGcdEJ2W|)i_g^8VlRyw5lN%5ne+&yO z9&%XB&j0`by-7qtR5;7UlUoWyF$hEhtxM^C=4bZ5mFu&Ri?;b{LkPr!j>fna*04C( zg^h`76tn=72Re-iHD@(uJFFSZp%zFXDzG1RSD?ipT_JwRazMO+i5f5uHXc?TrO|j0 zKPhlt`ug61NQ7R2Urs>OTfn-WUXAxoBli(VxPcNs2O%(lUVH#m;JO#MiGKrs{M4Vm zyQaw8X^5^4z(*SPiv=;k6rbe;Itna6J~PzKgW=;ICh$t2SJ#jN+`t78N}gs^RiUFE#o2f)x%w$)^ zz$=32!2tR&C^O5LlcXd($Jaf4e7%eEEbnuFj(#;~F~BDh$A6e%+Qb{gGn=--d7n7U zin2<4PCQ}K1&JTIuDJZhxzJ>RXNJvmYMwYuEEYOg>0nkgHR5UFh^pz7FJxR+Id5^+ z%2n37Cx2lmr>`t?o#rSKSi}+}h)_^P31!%b(yEhUAw~Of5C2HhFOf?jR|$+93#dSY z-1LM0!S8OZ{D0J>n-q!z-7k*wF$M&7fkw@7zK|*WBJ(`#607($rP*1~@nb#tW3a z?(yzmXK(+WY4!I5DZO%u$$VeQ000hKX;fHrSWQeiW3#>ifdV8rH)T0BV`MiiWi~cp zEi^edWG!N2WMwU4G-6^kI5}cuHa9bqV+5!SH90XjHZ?UjFf}$XlNSYW3otoUF*Z6g zH##&mlad7^3otoUF*Z6gH##&mv%m%P0kf9~U>+n0QV^U)zCHNc`PPzJ8xG?!CH!Hrm>Et zM3I)%Eep~Fk=h$@f~N|bZomU65OZMuZgINY_w9V^`RMxn#|4l+W`V#MzhJot-~de7 z7XX}qh4})^4ekt|&Hn?h#v4Z)Pk?nkb1d_Su0aYV-3D2>fzdeUdHU3GGwdsL+zOCt i{pvRQMlqeh9ry+6XsKn4OQ%Wz0000 Date: Sun, 13 Aug 2023 00:49:32 +0200 Subject: [PATCH 174/242] Add prompt before shutdown --- apps/poweroff/app.js | 18 ++++++++++++++---- apps/poweroff/metadata.json | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js index 303e78d03..98c9471ba 100644 --- a/apps/poweroff/app.js +++ b/apps/poweroff/app.js @@ -1,13 +1,23 @@ g.clear(); -g.setFont("6x8",2).setFontAlign(0,0); +E.showPrompt('Are you sure?', { + title: 'Power off', + buttons: { Yes: true, No: false }, +}).then((confirm) => { + if (!confirm) { + setTimeout(load, 100); + return; + } + + g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; g.drawString("Powering off...", x, y); -setTimeout(function() { - if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); -}, 1000); + setTimeout(function() { + if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); + }, 1000); +}); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/poweroff/metadata.json b/apps/poweroff/metadata.json index 80e71a1d2..673266691 100644 --- a/apps/poweroff/metadata.json +++ b/apps/poweroff/metadata.json @@ -1,7 +1,7 @@ { "id": "poweroff", "name": "Poweroff", "shortName":"Poweroff", -"version":"0.01", +"version":"0.02", "description": "Simple app to power off your Bangle.js", "icon": "app.png", "tags": "tool, poweroff, shutdown", From a1c3d83401972d1efcace8e466d052664184a093 Mon Sep 17 00:00:00 2001 From: StefanBruens Date: Sun, 13 Aug 2023 20:47:33 +0200 Subject: [PATCH 175/242] Add missing gx namespace for kml extensions According to https://developers.google.com/kml/documentation/kmlreference#kml-extension-namespace-and-the-gx-prefix > This namespace URI must be added to the element in any KML file using gx-prefixed elements As the exported kml tracks always contain such elements, add the namespace. --- apps/recorder/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index cc9762d20..24dba9b64 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -30,7 +30,7 @@ function saveKML(track,title) { track = filterGPSCoordinates(track); // Now output KML var kml = ` - + ${track[0].Heartrate!==undefined ? ` From c380bda4dccacdb202b0c32ab818e8efc48f3723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20To=C5=A1i=C4=87?= Date: Mon, 14 Aug 2023 01:23:11 +0200 Subject: [PATCH 176/242] Fix hwid_a_battery_widget background color --- apps/hwid_a_battery_widget/ChangeLog | 1 + apps/hwid_a_battery_widget/metadata.json | 2 +- apps/hwid_a_battery_widget/widget.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/hwid_a_battery_widget/ChangeLog b/apps/hwid_a_battery_widget/ChangeLog index 6c57f97a8..e7cdd2b4b 100644 --- a/apps/hwid_a_battery_widget/ChangeLog +++ b/apps/hwid_a_battery_widget/ChangeLog @@ -7,3 +7,4 @@ 0.07: Fixed position after unlocking 0.08: Handling exceptions 0.09: Add option for showing battery high mark +0.10: Fix background color diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 73dfc7c92..98d7ce2d5 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -3,7 +3,7 @@ "name": "A Battery Widget (with percentage) - Hanks Mod", "shortName":"H Battery Widget", "icon": "widget.png", - "version":"0.09", + "version":"0.10", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index 027535051..79ce9a5ad 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -26,6 +26,7 @@ var y = this.y; if ((typeof x === 'undefined') || (typeof y === 'undefined')) { } else { + g.setBgColor(COLORS.white); g.clearRect(old_x, old_y, old_x + width, old_y + height); const l = E.getBattery(); // debug: Math.floor(Math.random() * 101); From 5dcf0790cc8f58557c56ba510e068d231cd860fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20To=C5=A1i=C4=87?= Date: Mon, 14 Aug 2023 11:54:27 +0200 Subject: [PATCH 177/242] Update changelog --- apps/poweroff/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/poweroff/ChangeLog b/apps/poweroff/ChangeLog index 1a3bc1757..7ec3097bd 100644 --- a/apps/poweroff/ChangeLog +++ b/apps/poweroff/ChangeLog @@ -1 +1,2 @@ 0.01: New app! +0.02: Add prompt before shutdown From 656b8a275c1d737c0a383cd3b6c45b6fa3c5ada5 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 15 Aug 2023 08:21:50 +0100 Subject: [PATCH 178/242] 0.05: fix alignment of clock items caused by 0.04 (fix #2970) Also move var->let, to stop fast load memory leaks --- apps/lcdclock/ChangeLog | 1 + apps/lcdclock/app.js | 32 ++++++++++++++++---------------- apps/lcdclock/metadata.json | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/lcdclock/ChangeLog b/apps/lcdclock/ChangeLog index 07bb3cdfd..e81ee5bc4 100644 --- a/apps/lcdclock/ChangeLog +++ b/apps/lcdclock/ChangeLog @@ -2,3 +2,4 @@ 0.02: Use clock_info module as an app 0.03: clock_info now uses app name to maintain settings specifically for this clock face 0.04: clock_info is loaded before widgets to match other clocks +0.05: fix alignment of clock items caused by 0.04 (fix #2970) \ No newline at end of file diff --git a/apps/lcdclock/app.js b/apps/lcdclock/app.js index c7789a85f..b4eb25b0b 100644 --- a/apps/lcdclock/app.js +++ b/apps/lcdclock/app.js @@ -30,16 +30,6 @@ let draw = function() { }, 60000 - (Date.now() % 60000)); }; -var R = Bangle.appRect; -R.x+=1; -R.y+=1; -R.x2-=1; -R.y2-=1; -R.w-=2; -R.h-=2; -var midX = R.x+R.w/2; -var barY = 80; - let clockInfoDraw = (itm, info, options) => { let texty = options.y+41; g.reset().setFont("7Seg").setColor(g.theme.bg).setBgColor(g.theme.fg); @@ -52,11 +42,7 @@ let clockInfoDraw = (itm, info, options) => { if (title!="Bangle") g.setFontAlign(1,0).drawString(title.toUpperCase(), options.x+options.w-2, options.y+14); if (g.setFont("7Seg:2").stringWidth(text)+8>options.w) g.setFont("7Seg"); g.setFontAlign(0,0).drawString(text, options.x+options.w/2, options.y+40); - }; -let clockInfoItems = require("clock_info").load(); -let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x, y:R.y, w:midX-2, h:barY-R.y-2, draw : clockInfoDraw}); -let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+2, y:R.y, w:midX-3, h:barY-R.y-2, draw : clockInfoDraw}); // Show launcher when middle button pressed Bangle.setUI({ @@ -76,10 +62,24 @@ Bangle.setUI({ }}); // Load widgets Bangle.loadWidgets(); +// Work out sizes +let R = Bangle.appRect; +R.x+=1; +R.y+=1; +R.x2-=1; +R.y2-=1; +R.w-=2; +R.h-=2; +let midX = R.x+R.w/2; +let barY = 80; // Clear the screen once, at startup let oldTheme = g.theme; g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(1); g.fillRect({x:R.x, y:R.y, w:R.w, h:R.h, r:8}).clearRect(R.x,barY,R.w,barY+1).clearRect(midX,R.y,midX+1,barY); draw(); -setTimeout(Bangle.drawWidgets,0); -} +Bangle.drawWidgets(); +// Allocate and draw clockinfos +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x, y:R.y, w:midX-2, h:barY-R.y-2, draw : clockInfoDraw}); +let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+2, y:R.y, w:midX-3, h:barY-R.y-2, draw : clockInfoDraw}); +} \ No newline at end of file diff --git a/apps/lcdclock/metadata.json b/apps/lcdclock/metadata.json index 4a98e8124..83107cf40 100644 --- a/apps/lcdclock/metadata.json +++ b/apps/lcdclock/metadata.json @@ -1,6 +1,6 @@ { "id": "lcdclock", "name": "LCD Clock", - "version":"0.04", + "version":"0.05", "description": "A Casio-style clock, with ClockInfo areas at the top and bottom. Tap them and swipe up/down to toggle between different information", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], From f0304b5b43be5eafac2b9d4497a2c0190a964fb6 Mon Sep 17 00:00:00 2001 From: Dennis Kueper Date: Wed, 16 Aug 2023 08:44:25 +0200 Subject: [PATCH 179/242] Add settings to configure prompt --- apps/poweroff/ChangeLog | 1 + apps/poweroff/app.js | 11 ++++++--- apps/poweroff/metadata.json | 10 +++++--- apps/poweroff/settings.js | 46 +++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 apps/poweroff/settings.js diff --git a/apps/poweroff/ChangeLog b/apps/poweroff/ChangeLog index 7ec3097bd..38e6128f6 100644 --- a/apps/poweroff/ChangeLog +++ b/apps/poweroff/ChangeLog @@ -1,2 +1,3 @@ 0.01: New app! 0.02: Add prompt before shutdown +0.03: Add settings to configure prompt diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js index 98c9471ba..1c5ce59dc 100644 --- a/apps/poweroff/app.js +++ b/apps/poweroff/app.js @@ -1,14 +1,19 @@ g.clear(); -E.showPrompt('Are you sure?', { +let settings = require('Storage').readJSON("poweroff.json", true) || {}; +let showPrompt; +showPrompt = def(settings.showPrompt, true); + +if (showPrompt) { + E.showPrompt('Are you sure?', { title: 'Power off', buttons: { Yes: true, No: false }, -}).then((confirm) => { + }).then((confirm) => { if (!confirm) { setTimeout(load, 100); return; } - +} g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; diff --git a/apps/poweroff/metadata.json b/apps/poweroff/metadata.json index 673266691..254127935 100644 --- a/apps/poweroff/metadata.json +++ b/apps/poweroff/metadata.json @@ -1,7 +1,7 @@ { "id": "poweroff", "name": "Poweroff", "shortName":"Poweroff", -"version":"0.02", +"version":"0.03", "description": "Simple app to power off your Bangle.js", "icon": "app.png", "tags": "tool, poweroff, shutdown", @@ -10,6 +10,10 @@ "allow_emulator": true, "storage": [ {"name":"poweroff.app.js","url":"app.js"}, - {"name":"poweroff.img","url":"app-icon.js","evaluate":true} -] + {"name":"poweroff.img","url":"app-icon.js","evaluate":true}, + {"name":"poweroff.settings.js","url":"settings.js"} +], +"data": [ + {"name":"poweroff.json"} + ] } diff --git a/apps/poweroff/settings.js b/apps/poweroff/settings.js new file mode 100644 index 000000000..b0158855c --- /dev/null +++ b/apps/poweroff/settings.js @@ -0,0 +1,46 @@ +(function(back) { + var FILE = "poweroff.json"; + var settings = Object.assign({ + secondsOnUnlock: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "Poweroff" + }, + "< Back": () => back(), + "Show prompt": { + value: (settings.showPrompt !== undefined ? settings.showPrompt : true), + onchange: v => { + settings.showPrompt = v; + writeSettings(); + } + } + }; + E.showMenu(mainmenu); +}); From c4ced443e087e14e96a0a4c45f3726a6d64b4a04 Mon Sep 17 00:00:00 2001 From: Dennis Kueper Date: Wed, 16 Aug 2023 08:46:54 +0200 Subject: [PATCH 180/242] Fix js --- apps/poweroff/app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js index 1c5ce59dc..019787d0e 100644 --- a/apps/poweroff/app.js +++ b/apps/poweroff/app.js @@ -1,3 +1,4 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears g.clear(); let settings = require('Storage').readJSON("poweroff.json", true) || {}; @@ -26,3 +27,4 @@ if (showPrompt) { Bangle.loadWidgets(); Bangle.drawWidgets(); +} \ No newline at end of file From 0fb770795fbbfd91d5af5f848ea179be960dda44 Mon Sep 17 00:00:00 2001 From: Dennis Kueper Date: Wed, 16 Aug 2023 08:53:17 +0200 Subject: [PATCH 181/242] fix --- apps/poweroff/app.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js index 019787d0e..6e7eee459 100644 --- a/apps/poweroff/app.js +++ b/apps/poweroff/app.js @@ -1,20 +1,22 @@ { // must be inside our own scope here so that when we are unloaded everything disappears g.clear(); +let confirm = false; let settings = require('Storage').readJSON("poweroff.json", true) || {}; let showPrompt; showPrompt = def(settings.showPrompt, true); -if (showPrompt) { - E.showPrompt('Are you sure?', { +E.showPrompt('Are you sure?', { title: 'Power off', buttons: { Yes: true, No: false }, - }).then((confirm) => { +}).then((confirm) => { if (!confirm) { setTimeout(load, 100); return; } -} +}); + +if (showPrompt && confirm) { g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; @@ -23,7 +25,7 @@ if (showPrompt) { setTimeout(function() { if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); }, 1000); -}); +} Bangle.loadWidgets(); Bangle.drawWidgets(); From 8715e0df92517330bec75de95f67f5d48c47dbdb Mon Sep 17 00:00:00 2001 From: Dennis Kueper Date: Wed, 16 Aug 2023 09:06:18 +0200 Subject: [PATCH 182/242] fix --- apps/poweroff/app.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js index 6e7eee459..b179cfb38 100644 --- a/apps/poweroff/app.js +++ b/apps/poweroff/app.js @@ -1,22 +1,25 @@ { // must be inside our own scope here so that when we are unloaded everything disappears g.clear(); -let confirm = false; +let confirmed = false; let settings = require('Storage').readJSON("poweroff.json", true) || {}; let showPrompt; showPrompt = def(settings.showPrompt, true); -E.showPrompt('Are you sure?', { +if (showPrompt) { + E.showPrompt('Are you sure?', { title: 'Power off', buttons: { Yes: true, No: false }, -}).then((confirm) => { + }).then((confirm) => { if (!confirm) { setTimeout(load, 100); + confirmed = true; return; } -}); + }); +} -if (showPrompt && confirm) { +if (!showPrompt || confirmed) { g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; From e1692a4dbcf77cd1ad57512bc09d2988433053fb Mon Sep 17 00:00:00 2001 From: Simon Sievert Date: Wed, 16 Aug 2023 09:01:02 +0200 Subject: [PATCH 183/242] boot, setting: whitelist: try to resolve peer address This uses NRF.resolveAddress() on newer firmwares, to try to resolve "random private resolvable addresses" of peers that connect, before checking the whitelist. --- apps/boot/ChangeLog | 1 + apps/boot/bootupdate.js | 2 +- apps/boot/metadata.json | 2 +- apps/setting/ChangeLog | 3 ++- apps/setting/metadata.json | 2 +- apps/setting/settings.js | 6 ++++++ 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index d7405e763..6349d9213 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -67,3 +67,4 @@ 0.56: Settings.log = 0,1,2,3 for off,display, log, both 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 diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index a12d41e1b..1b11a3f16 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -79,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't 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.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist && !(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; +if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`; if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation // ================================================== FIXING OLDER FIRMWARES if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted. diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 0a4e7e9d1..45f531776 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.58", + "version": "0.59", "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/setting/ChangeLog b/apps/setting/ChangeLog index 942eee11d..af9ef39e9 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -68,4 +68,5 @@ of 'Select Clock' 0.60: Moved LCD calibration to top of menu, and use 12 taps (not 8) LCD calibration will now error if the calibration is obviously wrong 0.61: Permit temporary bypass of the BLE whitelist -0.62: Fix whitelist showing as 'on' by default when it's not after 0.59 \ No newline at end of file +0.62: Fix whitelist showing as 'on' by default when it's not after 0.59 +0.63: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index 5e0753dd2..c94abdd82 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.62", + "version": "0.63", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 87aaba9d1..badcea015 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -383,6 +383,12 @@ function showWhitelistMenu() { NRF.on('connect', function(addr) { if (!settings.whitelist) settings.whitelist=[]; delete settings.whitelist_disabled; + if (NRF.resolveAddress !== undefined) { + let resolvedAddr = NRF.resolveAddress(addr); + if (resolvedAddr !== undefined) { + addr = resolvedAddr + " (resolved)"; + } + } settings.whitelist.push(addr); updateSettings(); NRF.removeAllListeners('connect'); From ef4ee7e478bedf8f3caec8d60c6c35dfbea4c852 Mon Sep 17 00:00:00 2001 From: Dennis Kueper Date: Wed, 16 Aug 2023 09:35:50 +0200 Subject: [PATCH 184/242] Not beautiful but finally working --- apps/poweroff/app.js | 18 ++++++++++++++---- apps/poweroff/settings.js | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js index b179cfb38..6e14b1a44 100644 --- a/apps/poweroff/app.js +++ b/apps/poweroff/app.js @@ -2,24 +2,34 @@ g.clear(); let confirmed = false; +// Helper function default setting +let def = function(value, def) { + return value !== undefined ? value : def; +}; let settings = require('Storage').readJSON("poweroff.json", true) || {}; let showPrompt; showPrompt = def(settings.showPrompt, true); if (showPrompt) { + Bangle.setLocked(false); // handy when debugging via IDE E.showPrompt('Are you sure?', { title: 'Power off', buttons: { Yes: true, No: false }, }).then((confirm) => { if (!confirm) { setTimeout(load, 100); - confirmed = true; return; } - }); -} + g.setFont("6x8",2).setFontAlign(0,0); + var x = g.getWidth()/2; + var y = g.getHeight()/2 + 10; + g.drawString("Powering off...", x, y); -if (!showPrompt || confirmed) { + setTimeout(function() { + if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); + }, 1000); + }); +} else { g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; diff --git a/apps/poweroff/settings.js b/apps/poweroff/settings.js index b0158855c..b22a7918a 100644 --- a/apps/poweroff/settings.js +++ b/apps/poweroff/settings.js @@ -1,7 +1,7 @@ (function(back) { var FILE = "poweroff.json"; var settings = Object.assign({ - secondsOnUnlock: false, + showPrompt: true, }, require('Storage').readJSON(FILE, true) || {}); function writeSettings() { From 8ccbec811c187303bc88d9b8eb8281bcb8ce2898 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 16 Aug 2023 09:43:14 +0100 Subject: [PATCH 185/242] Add compiled c code into espruinotools, and ensure it can be used (fix #2972) --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 4f735fc1c..45e27480d 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 4f735fc1c1c0c4e728571f80ad3d5ed04e633d46 +Subproject commit 45e27480d0100a601eeda2692b7ee9e2032c4054 From f67d9eb85f42d48463bd1207fae5945f72e5bd02 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 16 Aug 2023 09:47:01 +0100 Subject: [PATCH 186/242] Remove 'beta' label from passkey - it's been around for a while and works ok --- apps/setting/ChangeLog | 1 + apps/setting/README.md | 4 ++-- apps/setting/settings.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index af9ef39e9..dbeea10ca 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -70,3 +70,4 @@ of 'Select Clock' 0.61: Permit temporary bypass of the BLE whitelist 0.62: Fix whitelist showing as 'on' by default when it's not after 0.59 0.63: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds + Remove 'beta' label from passkey - it's been around for a while and works ok \ No newline at end of file diff --git a/apps/setting/README.md b/apps/setting/README.md index 2a7f7ee9c..18aa8af46 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -21,7 +21,7 @@ This is Bangle.js's settings menu * **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps? As long as your watch firmware is up to date, Gadgetbridge will work even with `Programmable` set to `Off`. * **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device. * **NOTE:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps. -* **Passkey BETA** allows you to set a passkey that is required to connect and pair to Bangle.js. **Note:** This is Beta and you will almost certainly encounter issues connecting with Web Bluetooth using this option. +* **Passkey** allows you to set a passkey that is required to connect and pair to Bangle.js. * **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app. * **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again. @@ -62,7 +62,7 @@ The exact effects depend on the app. In general the watch will not wake up by i * `Both` Log and display on Bangle's screen * **Compact Storage** Removes deleted/old files from Storage - this will speed up your Bangle.js * **Rewrite Settings** Should not normally be required, but if `.boot0` has been deleted/corrupted (and so no settings are being loaded) this will fix it. -* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours. +* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours. * **Calibrate Battery** If you're finding your battery percentage meter isn't accurate, leave your Bangle.js on charge for at least 3 hours, and then choose this menu option. It will measure the battery voltage when full and will allow Bangle.js to report a more accurate battery percentage. * **Reset Settings** Reset the settings (as set in this app) to defaults. Does not reset settings for other apps. * **Factory Reset** (not available on Bangle.js 1) - wipe **everything** and return to a factory state diff --git a/apps/setting/settings.js b/apps/setting/settings.js index badcea015..f3b963d6e 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -186,7 +186,7 @@ function showBLEMenu() { updateSettings(); } }, - /*LANG*/'Passkey BETA': { + /*LANG*/'Passkey': { value: settings.passkey?settings.passkey:/*LANG*/"none", onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, From ebc95fda6920da806064eefea5ea0da782d261c8 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 17 Aug 2023 09:11:23 +0200 Subject: [PATCH 187/242] sokoban: renamed level file --- apps/sokoban/{Microban.txt => Microban.sok} | 0 apps/sokoban/app.js | 6 +++--- apps/sokoban/metadata.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename apps/sokoban/{Microban.txt => Microban.sok} (100%) diff --git a/apps/sokoban/Microban.txt b/apps/sokoban/Microban.sok similarity index 100% rename from apps/sokoban/Microban.txt rename to apps/sokoban/Microban.sok diff --git a/apps/sokoban/app.js b/apps/sokoban/app.js index 90b24ed11..b7d89d6ba 100644 --- a/apps/sokoban/app.js +++ b/apps/sokoban/app.js @@ -61,9 +61,9 @@ function next_map_offsets(filename, start_offset) { let config = s.readJSON("sokoban.json", true); if (config === undefined) { - let initial_offsets = next_map_offsets("Microban.txt", 0); + let initial_offsets = next_map_offsets("Microban.sok", 0); config = { - levels_sets: ["Microban.txt"], // all known files containing levels + levels_sets: ["Microban.sok"], // all known files containing levels levels_set: 0, // which set are we using ? current_maps: [0], // what is current map on each set ? offsets: [initial_offsets], // known offsets for each levels set (binary positions of maps in each file) @@ -461,4 +461,4 @@ Bangle.setLocked(false); current_map = config.current_map; offsets = config.offsets; -load_current_map(); \ No newline at end of file +load_current_map(); diff --git a/apps/sokoban/metadata.json b/apps/sokoban/metadata.json index 191cea1e0..7a4d5bc50 100644 --- a/apps/sokoban/metadata.json +++ b/apps/sokoban/metadata.json @@ -13,7 +13,7 @@ "readme": "README.md", "storage": [ {"name":"sokoban.app.js","url":"app.js"}, - {"name":"Microban.txt", "url":"Microban.txt"}, + {"name":"Microban.sok", "url":"Microban.sok"}, {"name":"sokoban.img","url":"app-icon.js","evaluate":true} ], "data": [{"name":"sokoban.json"} From 5b515bf2b8ca03c68b48d278acf080671cb54e73 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 17 Aug 2023 21:56:49 +0200 Subject: [PATCH 188/242] Add astronomical clock. The plan is to have an (analog) astronomical clock with a lot of information on single dial. --- apps/orloj/ChangeLog | 1 + apps/orloj/README.md | 25 +++ apps/orloj/app-icon.js | 1 + apps/orloj/app.js | 407 +++++++++++++++++++++++++++++++++++++++ apps/orloj/app.png | Bin 0 -> 756 bytes apps/orloj/metadata.json | 13 ++ 6 files changed, 447 insertions(+) create mode 100644 apps/orloj/ChangeLog create mode 100644 apps/orloj/README.md create mode 100644 apps/orloj/app-icon.js create mode 100644 apps/orloj/app.js create mode 100644 apps/orloj/app.png create mode 100644 apps/orloj/metadata.json diff --git a/apps/orloj/ChangeLog b/apps/orloj/ChangeLog new file mode 100644 index 000000000..263d4078d --- /dev/null +++ b/apps/orloj/ChangeLog @@ -0,0 +1 @@ +0.01: attempt to import diff --git a/apps/orloj/README.md b/apps/orloj/README.md new file mode 100644 index 000000000..4da3f6a98 --- /dev/null +++ b/apps/orloj/README.md @@ -0,0 +1,25 @@ +# Orloj ![](app.png) + +Astronomical clock. + +Written by: [Pavel Machek](https://github.com/pavelmachek) + +The plan is to have an (analog) astronomical clock with a lot of +information on single dial. + +It continuously displays information that can be obtained "cheaply", +that is current time, sunset/sunrise times, battery status and +altitude. One-second updates with useful compass can be activated by +tapping bottom right corner. + +Display is split in three rings. Outside ring is for time-based data +with base of one week, and for non time-based data. Black dot +indicates day of week. Green foot indicates number of steps taken, red +battery symbol indicates remaining charge, black thermometer symbol +represents temperature, and black ruler symbol indicates +altitude. Number in bottom left corner is day of month. + +In the middle ring, hour-based data are displayed. Black dot indicates +current hour, yellow symbols indicate sunset and sunrise, and black +symbols indicate moonset and moonrise. + diff --git a/apps/orloj/app-icon.js b/apps/orloj/app-icon.js new file mode 100644 index 000000000..4663c8266 --- /dev/null +++ b/apps/orloj/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkB/4A+gAXWh4YWh4BCC6vwBxoIJGBYXXHhAUCJBYXXHhAXaVBgkHC7JGMEpDvQC48ACxoXHCx5WPC8BvPC8BgwgAAFC65nQ+AvWAFSjYaiyERUQ7QYGKS4EOyguUC7h4VFoIXUIgbBWAH4A/AH4APA==")) diff --git a/apps/orloj/app.js b/apps/orloj/app.js new file mode 100644 index 000000000..8dd1cd571 --- /dev/null +++ b/apps/orloj/app.js @@ -0,0 +1,407 @@ +const SunCalc = require("suncalc"); // from modules folder + +// ################################################################################ + +let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; +let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; +let outerRadius = Math.min(CenterX,CenterY) * 0.9; + +const lat = 50.1; +const lon = 14.45; + +const h = g.getHeight(); +const w = g.getWidth(); +const sm = 15; +let settings, location, mode = 0; +var altitude, temperature; + +var img_north = Graphics.createImage(` + X + XXX + XXX + X XXX + X XXX + X XXXX + X XXXX +X XXXXX +X XXXXX +XXXXXXXXX +`); + +var img_sunrise = Graphics.createImage(` + XXX + XXXXX +XXXXXXXXX +`); + +var img_moonrise = Graphics.createImage(` + XXX + XX X +XXXXXXXXX +`); + +var img_altitude = Graphics.createImage(` +X X +X X X +XXXXXXXXX +X X X +X X +`); + +var img_temperature = Graphics.createImage(` + XX +XXXXXXXX +X XX +XXXXXXXX + XX +`); + +var img_battery = Graphics.createImage(` +XXXXXXXX +XXX X +XXXX XX +XXXXX X +XXXXXXXX +`); + +var img_step = Graphics.createImage(` + XXX + XX XXXXX +XXX XXXXX +XXX XXXXX + XX XXXX +`); + +var img_sun = Graphics.createImage(` +X X + XXX + XXXXXXX +XXXXXXXXX +XXXXXXXXX +XXXXXXXXX + XXXXXXX + XXX +X X +`); + +var img_moon = Graphics.createImage(` + XXX + XX XXX +X XXXX +X XXX +X XXX +X XXX +X XXXX + X XXX + XXX +`); + +let use_compass = 0; + +function draw() { + drawBorders(); + queueDraw(); +} + +function radA(p) { return p*(Math.PI*2); } +function radD(d) { return d*(h/2); } + +function radX(p, d) { + let a = radA(p); + return h/2 + Math.sin(a)*radD(d); +} + +function radY(p, d) { + let a = radA(p); + return w/2 - Math.cos(a)*radD(d); +} + +function fracHour(d) { + let hour = d.getHours(); + let min = d.getMinutes(); + hour = hour + min/60; + if (hour > 12) + hour -= 12; + return hour; +} + + let HourHandLength = outerRadius * 0.5; + let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2; + + let MinuteHandLength = outerRadius * 0.7; + let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2; + + let SecondHandLength = outerRadius * 0.9; + let SecondHandOffset = 6; + + let twoPi = 2*Math.PI; + let Pi = Math.PI; + let halfPi = Math.PI/2; + + let sin = Math.sin, cos = Math.cos; + + let HourHandPolygon = [ + -halfHourHandWidth,halfHourHandWidth, + -halfHourHandWidth,halfHourHandWidth-HourHandLength, + halfHourHandWidth,halfHourHandWidth-HourHandLength, + halfHourHandWidth,halfHourHandWidth, + ]; + + let MinuteHandPolygon = [ + -halfMinuteHandWidth,halfMinuteHandWidth, + -halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength, + halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength, + halfMinuteHandWidth,halfMinuteHandWidth, + ]; + +/**** drawClockFace ****/ + + function drawClockFace () { + g.setColor(g.theme.fg); + g.setFont('Vector', 22); + + g.setFontAlign(0,-1); + g.drawString('12', CenterX,CenterY-outerRadius); + + g.setFontAlign(1,0); + g.drawString('3', CenterX+outerRadius,CenterY); + + g.setFontAlign(0,1); + g.drawString('6', CenterX,CenterY+outerRadius); + + g.setFontAlign(-1,0); + g.drawString('9', CenterX-outerRadius,CenterY); + } + +/**** transforme polygon ****/ + + let transformedPolygon = new Array(HourHandPolygon.length); + + function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } + } + +/**** draw clock hands ****/ + + function drawClockHands () { + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + let Seconds = now.getSeconds(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + g.setColor(g.theme.fg); + + transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + g.setColor(g.theme.fg2); + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + + g.setFont('Vector', 22); + + g.setFontAlign(-1, 1); + g.drawString(now.getDate(), CenterX-outerRadius,CenterY+outerRadius); + + } + +function drawTimeIcon(time, icon, options) { + let h = fracHour(time); + let x = radX(h/12, 0.7); + let y = radY(h/12, 0.7); + g.drawImage(icon, x,y, options); +} + +function drawOutsideIcon(h, icon, options) { + let x = radX(h, 0.95); + let y = radY(h, 0.95); + g.drawImage(icon, x,y, options); +} + +function drawBorders() { + g.reset(); + g.setColor(0); + g.fillRect(Bangle.appRect); + + g.setColor(-1); + g.fillCircle(w/2, h/2, h/2 - 2); + if (0) { + g.fillCircle(sm+1, sm+1, sm); + g.fillCircle(sm+1, h-sm-1, sm); + g.fillCircle(w-sm-1, h-sm-1, sm); + g.fillCircle(h-sm-1, sm+1, sm); + } + g.setColor(0, 1, 0); + g.drawCircle(h/2, w/2, radD(0.7)); + g.drawCircle(h/2, w/2, radD(0.5)); + + outerRadius = radD(0.7); + drawClockHands(); + + let d = new Date(); + let hour = fracHour(d); + let min = d.getMinutes(); + let day = d.getDay(); + day = day + hour/24; + { + let x = radX(hour/12, 0.7); + let y = radY(hour/12, 0.7); + g.setColor(0, 0, 0); + g.fillCircle(x,y, 5); + } + { + let x = radX(min/60, 0.5); + let y = radY(min/60, 0.5); + g.setColor(0, 0, 0); + g.drawLine(h/2, w/2, x, y); + } + { + let x = radX(hour/12, 0.3); + let y = radY(hour/12, 0.3); + g.setColor(0, 0, 0); + g.drawLine(h/2, w/2, x, y); + } + { + let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps; + let x = radX(km/12 + 0, 0.95); + let y = radY(km/12 + 0, 0.95); + g.setColor(0, 0.7, 0); + g.drawImage(img_step, x,y, { scale: 2, rotate: Math.PI*0.0 } ); + } + { + let bat = E.getBattery(); + let x = radX(bat/100, 0.95); + let y = radY(bat/100, 0.95); + g.setColor(0.7, 0, 0); + g.drawImage(img_battery, x,y, { scale: 2, rotate: Math.PI*0.0 } ); + } + { + d = new Date(); + sun = SunCalc.getTimes(d, lat, lon); + g.setColor(0.5, 0.5, 0); + print("sun", sun); + drawTimeIcon(sun.sunset, img_sunrise, { rotate: Math.PI, scale: 2 }); + drawTimeIcon(sun.sunrise, img_sunrise, { scale: 2 }); + g.setColor(0, 0, 0); + moon = SunCalc.getMoonTimes(d, lat, lon); + print("moon", moon); + drawTimeIcon(moon.set, img_moonrise, { rotate: Math.PI, scale: 2 }); + drawTimeIcon(moon.rise, img_sunrise, { scale: 2 }); + pos = SunCalc.getPosition(d, lat, lon); + print("sun:", pos); + if (pos.altitude > -0.1) { + g.setColor(0.5, 0.5, 0); + az = pos.azimuth; + drawOutsideIcon(az / (2*Math.PI), img_sun, { scale: 2 }); + } + pos = SunCalc.getMoonPosition(d, lat, lon); + print("moon:", pos); + if (pos.altitude > -0.05) { + g.setColor(0, 0, 0); + az = pos.azimuth; + drawOutsideIcon(az / (2*Math.PI), img_moon, { scale: 2 }); + } + } + { + Bangle.getPressure().then((x) => + { altitude = x.altitude; temperature = x.temperature; }, + print); + print(altitude, temperature); + drawOutsideIcon(altitude / 120, img_altitude, { scale: 2 }); + drawOutsideIcon(temperature / 12, img_temperature, { scale: 2 }); + } + if (use_compass) { + let obj = Bangle.getCompass(); + if (obj) { + let h = 360-obj.heading; + let x = radX(h/360, 0.7); + let y = radY(h/360, 0.7); + g.setColor(0, 0, 1); + g.drawImage(img_north, x,y, {scale:2}); + } + } + { + let x = radX(day/7, 0.95); + let y = radY(day/7, 0.95); + g.setColor(0, 0, 0); + g.fillCircle(x,y, 5); + } +} + +function drawEmpty() { + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); +} + +Bangle.on('touch', function(button, xy) { + var x = xy.x; + var y = xy.y; + if (y > h) y = h; + if (y < 0) y = 0; + if (x > w) x = w; + if (x < 0) x = 0; +}); + +// if we get a step then we are not idle +Bangle.on('step', s => { +}); + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + next = 60000; + if (use_compass) next = 250; + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, next - (Date.now() % next)); +} + +// 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("clockupdown", btn=> { + if (btn<0) use_compass = 0; + if (btn>0) use_compass = 1; + Bangle.setCompassPower(use_compass, 'orloj'); + draw(); +}); + +if (use_compass) + Bangle.setCompassPower(true, 'orloj'); +g.clear(); +draw(); + diff --git a/apps/orloj/app.png b/apps/orloj/app.png new file mode 100644 index 0000000000000000000000000000000000000000..fced2ce5e8916058790277ad5af479f9577aa393 GIT binary patch literal 756 zcmVEX>4Tx04R}tkv&MmKpe$iTcsiuhZYoZ2vVIah>CR7DionYs1;guFuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|>f)s6A|?JWDYS_3;J6>}?mh0_0scmXsb<#%plX(p zP9}tGZdC}rB8UNm5yyzcOnpuiQ}7&L_we!cF2=LG&;2?2l)T9RpFljzbi*RvAfDN@ zbk6(45mu5E;&bA0gDyz?$aUG}H_kbWYY7*5n`d(!Ey()lA#h$6Gs(QqkMnX zWrgz=XSGset$XqphVt6VGS_JiBZ);UL4*JqHIz|-g&3_GDJIgipYZSxIew8`GP%lN zwnK>3i zB4UT-bYvpZVa3vr1ydbTg9E0WU1k))&3!hXL#s%Nr6Fd%xUVY0x;rs+P0u8tX_8bl zMAY1f>3^cGY!Qy>y~=bz8*1~H0S)B;XGK^9N#PhO_nl%V$dATN_1eAah(79gD-fT76P$pym0000 Date: Fri, 18 Aug 2023 09:21:09 +0100 Subject: [PATCH 189/242] Update tags to show it's a clock --- apps/orloj/metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/orloj/metadata.json b/apps/orloj/metadata.json index 00c091e35..a59d2e6dd 100644 --- a/apps/orloj/metadata.json +++ b/apps/orloj/metadata.json @@ -5,7 +5,8 @@ "icon": "app.png", "readme": "README.md", "supports" : ["BANGLEJS2"], - "tags": "", + "type": "clock", + "tags": "clock", "storage": [ {"name":"orloj.app.js","url":"app.js"}, {"name":"orloj.img","url":"app-icon.js","evaluate":true} From 918be9e7bbd353da10c0e561a913701c731b8ea9 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 18 Aug 2023 10:06:54 +0100 Subject: [PATCH 190/242] set pretokenise value correctly at startup - fix https://github.com/espruino/EspruinoAppLoaderCore/issues/53 --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 45e27480d..8cf4d0fbf 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 45e27480d0100a601eeda2692b7ee9e2032c4054 +Subproject commit 8cf4d0fbfc310e0d68d616ec779c1888475899a2 From ff8b799427684e9dc456388b072b00acd8e3231c Mon Sep 17 00:00:00 2001 From: David Peer Date: Fri, 18 Aug 2023 17:51:14 +0200 Subject: [PATCH 191/242] Added option to show weather --- apps/edgeclk/ChangeLog | 1 + apps/edgeclk/README.md | 2 ++ apps/edgeclk/app.js | 33 ++++++++++++++++++++++++++++++--- apps/edgeclk/metadata.json | 4 ++-- apps/edgeclk/screenshot4.png | Bin 0 -> 2665 bytes apps/edgeclk/settings.js | 13 +++++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 apps/edgeclk/screenshot4.png diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog index da75dfbae..b96d7207d 100644 --- a/apps/edgeclk/ChangeLog +++ b/apps/edgeclk/ChangeLog @@ -1,2 +1,3 @@ 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. diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md index 535a5e9df..cf2d0dd76 100644 --- a/apps/edgeclk/README.md +++ b/apps/edgeclk/README.md @@ -3,6 +3,7 @@ ![Screenshot](screenshot.png) ![Screenshot](screenshot2.png) ![Screenshot](screenshot3.png) +![Screenshot](screenshot4.png) Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability. It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging. @@ -15,6 +16,7 @@ The appearance is highly configurable. In the settings menu you can: - Switch between 24h and 12h clock. - Hide or display seconds.* - Show AM/PM in place of the seconds. +- Show weather temperature and icon in place of the seconds. - 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). diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index 9f28e2588..5bfa77b09 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -7,7 +7,8 @@ monthFirst: true, twentyFourH: true, showAmPm: false, - showSeconds: true, + showSeconds: false, + showWeather: true, stepGoal: 10000, stepBar: true, weekBar: true, @@ -15,7 +16,6 @@ dayBar: true, }, require('Storage').readJSON('edgeclk.settings.json', true) || {}); - /* Runtime Variables ------------------------------------------------------------------------------*/ @@ -51,6 +51,30 @@ } else { drawSteps(stepsOnlyCount); } + + drawWeather(); + }; + + const drawWeather = function () { + if (!settings.showWeather){ + return; + } + + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + + try{ + const weather = require('weather'); + const w = weather.get(); + let temp = parseInt(w.temp-273.15); + temp = temp < 0 ? '\\' + String(temp*-1) : String(temp); + + g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true); + + weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 15); + } catch(e) { + g.drawString("ERR", g.getWidth()-3, g.getHeight() - 1, true); + } }; const drawDate = function (date) { @@ -135,7 +159,8 @@ g.setFontAlign(-1, 1); // left bottom const steps = Bangle.getHealthStatus('day').steps; - g.drawString((steps < 100000 ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'), + const toKSteps = settings.showWeather ? 1000 : 100000; + g.drawString((steps < toKSteps ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); if (onlyCount === true) { @@ -229,12 +254,14 @@ // However, to save power while on battery only step count will get updated. // This will update icon and progress bar as well: if (!charging) drawSteps(); + drawWeather(); }; const onHealth = function () { if (!lcdPower || charging) return; // This will update progress bar and icon: drawSteps(); + drawWeather(); }; const onLock = function (locked) { diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json index 3f72be77a..0d53cd008 100644 --- a/apps/edgeclk/metadata.json +++ b/apps/edgeclk/metadata.json @@ -2,11 +2,11 @@ "id": "edgeclk", "name": "Edge Clock", "shortName": "Edge Clock", - "version": "0.02", + "version": "0.03", "description": "Crisp clock with perfect readability.", "readme": "README.md", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}], + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}, {"url":"screenshot4.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], diff --git a/apps/edgeclk/screenshot4.png b/apps/edgeclk/screenshot4.png new file mode 100644 index 0000000000000000000000000000000000000000..66ec85c898aa6f87081bad5d75059dbc528cd2fa GIT binary patch literal 2665 zcmYjTc~sKb7k|Kj4r-cdnWUCFQ(;bJrdBScscmY?;v}ieWM-IKiW>%B`?Z=&=2B+n zIA$6em`u3^d|O;9jhb=Ijgitq6wOd^YRRqr7^GH+SU*o!z&B0Hi;f?gn8kQA+p=cT!%yrE8F0ufN zWfpZ+C}3Wi)J1n473)yZa$d;#pcjtp9$9KJkR#X}i%r;cvRH!EhD#&r-ln|wnaa;} ziTM$>K4cT>j{ulVg4Medg-N;7a;$7{mt$+uZC+%P@cD?1BA~-6SYO|kD80FgDa{`T0G%QL;I_B{Pssu6=^gsRe^G11PMK)v@)7Nf7V+85~jBKUd!TP=Yko(TQ1zmN4uk zT?9j6qXrq?ypN5oy6gF=kbW*Vh(A9Ts0w0&iBA;;we}!%SA3#;9?|n8L#xWE-c65`MGAar(d-od#pQ}ev#nO*1!P-`er_nxihF;KL3Z* zce+fdRbtG+AKf{OE&tlyFi6>&7p!~`7jy2YqC#m2^KoZ!gW(nUEd8okdabVgvP{vZ z8>2qp*V)Biu1BWGBm0xt!{x%OvAAC2Y?GnBo-n2w<{aP|euv^ias;)z$&Q@Y?}lrh ze@X5Z_OJd^YSd=y_=R}B*+?1^+DO;y4CBApSgjjcmq(y#LQn}MkVc|A_dSI+7A zd-yr;#={FkpetvcC_@qH9&R7)HLf0MUyOc!Gh>-WYww`tn5pt zlQHCURfX!odD~*EN`SD9cS)D{z5)PyY)kb{^?Tcy@<*Bn8r1nZTzXid?)86BBwIXe zmUP-6^SAI#01ltqYnY*qSQ_n*i_iC;@EIuH$^>&XOS>5qobi0SHrfVTjXc$HLKcU1 z@F4|bHD#~#|CiY`OIf`ja_$y7+Rr$41z1H$jG~S(Y3TRXPVMH9icbO+4346!*W3khHqwpb63-Y+L3TZM=StAkMzsY$04Exl#%J|k9 zfh-YtoDOu>*tl=g^a*X$w;iaJe8d-yC#3P@25d^<(3mVl4jmT zs#f*~*E+apo>snFrNqC|zmqVavMc_8n!OvW5MkTC(^D?gc%H}&Tv2;bWkJ)29NS-np&Ce?sJ)h**>6UeJR@ELJlnnH6Xi(4dfmscJ^pTdRZ zFH~@9cMr*!{Yu^xhk0g0k84PQm5DBV{|<+WKSy`oX-3g}qE|NhCy))Em4An%)#~k9 z_*xnW`qz?}_Qg0+VO_@WudjECygZ{e$5R?SVWe~n7Ki0fsAfCoOtE%_P8X7Al+t! zK7xL2Hr%HC(K9s{3GUp;dd5>v@7@*LSclZp`&Em$7YioOXxwrsgS%&CvDrkWwxAIS zQWB8fgDPvpTYLn%Ml+gY!e$o?VNBgJdEZfK@ zb{33>ceEg@&LbZiu(0R33iN$<0R=qWNYmX%IWo6477bZA5yMx&4__K)UAx7yJsHo) zi)eWu)E_P>*PAdUHZ^t^|4hQ(+PSk|eLF?)D4ft_H$|Wam?^DZ*uQufwTO)gdG6!ojz)-=HnPex7)zRhir{jD10{^8ck+3GzqkVPKEjVK=uYC* zQxbbA>k@)PDlXUMo7c2#15ddPl^I{BB3A^A zyXnD!-{;kL-s|EkHKU7TK{a_HC(AAPr!oB@i+;mji+dv|l?@pMSpo8Vn%)dGFm%il5C;P&RD@3+;`6o&#n)KU`%v(D@Y@*sY zv->T$a^yNEjX3AbO0Trr)%IKhBZ|O&?5ps2Oh^h1R?X30UwLFrxk}49zN&vNdxRKX zN((-wMOql6gZK`qkro^=Ji#p3cw=I7a!6C=M+*%4IGTyLxVO?R=aY>}{dWWh-M#m7 IT&d^(3xcHytN;K2 literal 0 HcmV?d00001 diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js index 205dc5170..d7eff58d5 100644 --- a/apps/edgeclk/settings.js +++ b/apps/edgeclk/settings.js @@ -11,6 +11,7 @@ stepGoal: 10000, stepBar: true, weekBar: true, + showWeather: false, mondayFirst: true, dayBar: true, }; @@ -57,6 +58,7 @@ settings.showAmPm = !settings.showAmPm; // TODO can this be visually changed? if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false; + if (settings.showAmPm && settings.showWeather) settings.showWeather = false; save(); }, }, @@ -66,6 +68,17 @@ settings.showSeconds = !settings.showSeconds; // TODO can this be visually changed? if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false; + if (settings.showSeconds && settings.showWeather) settings.showWeather = false; + save(); + }, + }, + 'Show Weather': { + value: settings.showWeather, + onchange: () => { + settings.showWeather = !settings.showWeather; + // TODO can this be visually changed? + if (settings.showWeather && settings.showAmPm) settings.showAmPm = false; + if (settings.showWeather && settings.showSeconds) settings.showSeconds = false; save(); }, }, From 03b12614c3c0aa07240caa1eb29d5ba64d7e5cbd Mon Sep 17 00:00:00 2001 From: David Peer Date: Fri, 18 Aug 2023 17:53:46 +0200 Subject: [PATCH 192/242] Changed default --- apps/edgeclk/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index 5bfa77b09..45dd0c4c2 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -7,8 +7,8 @@ monthFirst: true, twentyFourH: true, showAmPm: false, - showSeconds: false, - showWeather: true, + showSeconds: true, + showWeather: false, stepGoal: 10000, stepBar: true, weekBar: true, From 817607287c918f0cdfc74817f798406b01f8af57 Mon Sep 17 00:00:00 2001 From: David Peer Date: Fri, 18 Aug 2023 18:35:58 +0200 Subject: [PATCH 193/242] Ensure same default settings in app.js and settings.js --- apps/edgeclk/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js index d7eff58d5..6f38e774c 100644 --- a/apps/edgeclk/settings.js +++ b/apps/edgeclk/settings.js @@ -8,10 +8,10 @@ twentyFourH: true, showAmPm: false, showSeconds: true, + showWeather: false, stepGoal: 10000, stepBar: true, weekBar: true, - showWeather: false, mondayFirst: true, dayBar: true, }; From a335acf17eb83f13fe5efb569ab560abc8e85e2a Mon Sep 17 00:00:00 2001 From: David Peer Date: Fri, 18 Aug 2023 18:37:51 +0200 Subject: [PATCH 194/242] Added contributors --- apps/edgeclk/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md index cf2d0dd76..90f6443fc 100644 --- a/apps/edgeclk/README.md +++ b/apps/edgeclk/README.md @@ -24,3 +24,8 @@ The appearance is highly configurable. In the settings menu you can: *) Hiding seconds 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 From 2d7fdd88c2575915dd95f080d1f211cfa9b2e1af Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Fri, 18 Aug 2023 20:40:24 +0200 Subject: [PATCH 195/242] sched/calendar: Fix timezone handling on ical --- apps/calendar/interface.html | 9 +++++++-- apps/sched/interface.html | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/calendar/interface.html b/apps/calendar/interface.html index 280a96c0b..ea64632f8 100644 --- a/apps/calendar/interface.html +++ b/apps/calendar/interface.html @@ -28,11 +28,16 @@ function readFile(input) { for(let i=0; i { - const jCalData = ICAL.parse(reader.result); + const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data + const jCalData = ICAL.parse(icalText); const comp = new ICAL.Component(jCalData); + const vtz = comp.getFirstSubcomponent('vtimezone'); + const tz = new ICAL.Timezone(vtz); + // Fetch the VEVENT part comp.getAllSubcomponents('vevent').forEach(vevent => { - event = new ICAL.Event(vevent); + const event = new ICAL.Event(vevent); + event.startDate.zone = tz; holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists const holiday = eventToHoliday(event); diff --git a/apps/sched/interface.html b/apps/sched/interface.html index cd2c9c595..53b443371 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -16,14 +16,18 @@ function readFile(input) { for(let i=0; i { - const jCalData = ICAL.parse(reader.result); + const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data + const jCalData = ICAL.parse(icalText); const comp = new ICAL.Component(jCalData); + const vtz = comp.getFirstSubcomponent('vtimezone'); + const tz = new ICAL.Timezone(vtz); + // Fetch the VEVENT part comp.getAllSubcomponents('vevent').forEach(vevent => { event = new ICAL.Event(vevent); const exists = alarms.some(alarm => alarm.id === event.uid); - const alarm = eventToAlarm(event, offsetMinutes*60*1000); + const alarm = eventToAlarm(event, tz, offsetMinutes*60*1000); renderAlarm(alarm, exists); if (exists) { @@ -68,7 +72,8 @@ function getAlarmDefaults() { }; } -function eventToAlarm(event, offsetMs) { +function eventToAlarm(event, tz, offsetMs) { + event.startDate.zone = tz; const dateOrig = event.startDate.toJSDate(); const date = offsetMs ? new Date(dateOrig - offsetMs) : dateOrig; From 3158d945aaba454cf40c5901f96a94bed8fb39d7 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 18 Aug 2023 14:17:53 +0200 Subject: [PATCH 196/242] Add Space Weaver -- vector map application. This really needs more work (as documented in README), but this is already quite useful. Please check app.js file -- I used library for conversion between xyz and lat/lon, so its license applies. It seems to be compatible with bangle apps license. --- apps/spacew/README.md | 43 +++ apps/spacew/app-icon.js | 2 + apps/spacew/app.js | 620 ++++++++++++++++++++++++++++++++++ apps/spacew/app.png | Bin 0 -> 3263 bytes apps/spacew/metadata.json | 13 + apps/spacew/prep/minitar.js | 82 +++++ apps/spacew/prep/prepare.json | 18 + apps/spacew/prep/prepare.sh | 18 + apps/spacew/prep/split.js | 177 ++++++++++ apps/spacew/prep/stats.sh | 22 ++ 10 files changed, 995 insertions(+) create mode 100644 apps/spacew/README.md create mode 100644 apps/spacew/app-icon.js create mode 100644 apps/spacew/app.js create mode 100644 apps/spacew/app.png create mode 100644 apps/spacew/metadata.json create mode 100755 apps/spacew/prep/minitar.js create mode 100644 apps/spacew/prep/prepare.json create mode 100755 apps/spacew/prep/prepare.sh create mode 100755 apps/spacew/prep/split.js create mode 100755 apps/spacew/prep/stats.sh diff --git a/apps/spacew/README.md b/apps/spacew/README.md new file mode 100644 index 000000000..4f2ca3f00 --- /dev/null +++ b/apps/spacew/README.md @@ -0,0 +1,43 @@ +# Space Weaver ![](app.png) + +Vector map + +Written by: [Pavel Machek](https://github.com/pavelmachek) + +Space Weaver is application for displaying vector maps. It is +currently suitable for developers, and more work is needed. + +Maps can be created from openstreetmap extracts. Those are cut using +osmosis, then translated into geojson. Geojson is further processes to +add metadata such as colors, and to split it into xyz tiles, while +keeping geojson format. Tiles are then merged into single file, which +can be uploaded to the filesystem. Index at the end provides locations +of the tiles. + +## Preparing data + +Tools in spacew/prep can be used to prepare data. + +You'll need to edit prepare.sh to point it to suitable osm extract, +and you'll need to select area of interest. Start experiments with +small area. You may want to delete cstocs and provide custom +conversion to ascii. + +Details of which features are visible at what zoom levels can be +configured in split.js. This can greatly affect file sizes. Then +there's "meta.max_zoom = 17" setting, reduce it if file is too big. + +For initial experiments, configure things so that mtar file is around +500KB. (I had troubles with big files, both on hardware and to lesser +extent on simulator. In particular, mtar seemed to be corrupted after +emulator window was closed.) + +## Future Development + +Directories at the end of .mtar should be hashed, not linear searched. + +Geojson is not really suitable as it takes a lot of storage. + +It would be nice to support polygons. + +Web-based tool for preparing maps would be nice. \ No newline at end of file diff --git a/apps/spacew/app-icon.js b/apps/spacew/app-icon.js new file mode 100644 index 000000000..27b6e2662 --- /dev/null +++ b/apps/spacew/app-icon.js @@ -0,0 +1,2 @@ +require("heatshrink").decompress(atob("mEwwkE/8Ql//j//AAUD//yAgILBAAXzBQMxAoMwn4XKBIgXCmAXEh4XF+IJGC4XxAoMgl/zgX/nASBBgPwIoIEBmYBBI4ug1/6hX/zOf+UBEIMP+UC+eZAIPyhP/yAnB0Ef+QXBnM/GgUwh4ECwX/wYvCkIvB+BrBA4JsFAQMiL4gRBA4XxNYIlBBgQGBiJXEBQRnBiYoEiQXFgURT4YAB+QXBS4RTCJoQMBj4gBWQPwN4IKCNgLHDRAIlDEgIxBC4zHBJITACC4gMB+MfAIJCCRIU/GIIGCEoLyCBgQOCgZAEBAL5CC4UvC4oFBMIJ9CCAQMBPwbABKoYMBJ4KpBZQgKBVwnyh/wKoQMBVoUgn4XFmTGEgfxC4QKBCQRKBeAYtBkYXFXYIFBkTfCSgMfIIYbBdwTADJIIEBkYEDAYKyDC4J9DKoSFDiZMDGYKCDkbWEKoUzIQQREHQIFDifzBQYXGIIIMDkDwDN4IXFIIIXBJQMhEQqCCT4IWENoUCC4MvXwTjCiZqBEQIXGNoITBC4LRDEQMDHQbWEAAUDIYPzmabEEQIXDmYXGiUgFAMyLASQDXgPzj7uEQobNB+MxWYsgHQKSBEQqFCUYPwUwgKCHQUvEQqFCkAXCBQ0Qn/xmYXH+IXB+S+ESAUAEQMzHQqFCgEvmS+EBQUBl/wUw4MDmS+ESAcf+ExC44MCmS+ESAcPmAvI/8hh8iNY8wgcwaw4MCh8hNY/wC5kDTwKbHgThGEgsQQZMhdw61CgSmGAAUANRAkCgUTBZEQiRSHHga+HNYUCC5I8BXw4XCgIWJHgJTJ+IXJHAIXB+eTJoIJD+fyC4LABYQWZBQOYC4Mf+eS/85DgIJBxMygAFB+YUBC4YqBkAoBAIM5n4JCAgIvBwYBCNgyDKTRIXM+YXFA=")) + diff --git a/apps/spacew/app.js b/apps/spacew/app.js new file mode 100644 index 000000000..e438799f6 --- /dev/null +++ b/apps/spacew/app.js @@ -0,0 +1,620 @@ +/* original openstmap.js */ + +/* OpenStreetMap plotting module. + +Usage: + +var m = require("openstmap"); +// m.lat/lon are now the center of the loaded map +m.draw(); // draw centered on the middle of the loaded map + +// plot gps position on map +Bangle.on('GPS',function(f) { + if (!f.fix) return; + var p = m.latLonToXY(fix.lat, fix.lon); + g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); +}); + +// recenter and redraw map! +function center() { + m.lat = fix.lat; + m.lon = fix.lon; + m.draw(); +} + +// you can even change the scale - eg 'm/scale *= 2' + +*/ + +var exports = {}; +var m = exports; +m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{ + let map = require("Storage").readJSON(f); + map.center = Bangle.project({lat:map.lat,lon:map.lon}); + return map; +}); +// we base our start position on the middle of the first map +if (m.maps[0] != undefined) { + m.map = m.maps[0]; + m.scale = m.map.scale; // current scale (based on first map) + m.lat = m.map.lat; // position of middle of screen + m.lon = m.map.lon; // position of middle of screen +} else { + m.scale = 20; + m.lat = 50; + m.lon = 14; +} + +exports.draw = function() { + var cx = g.getWidth()/2; + var cy = g.getHeight()/2; + var p = Bangle.project({lat:m.lat,lon:m.lon}); + m.maps.forEach((map,idx) => { + var d = map.scale/m.scale; + var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx; + var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy; + var o = {}; + var s = map.tilesize; + if (d!=1) { // if the two are different, add scaling + s *= d; + o.scale = d; + } + //console.log(ix,iy); + var tx = 0|(ix/s); + var ty = 0|(iy/s); + var ox = (tx*s)-ix; + var oy = (ty*s)-iy; + var img = require("Storage").read(map.fn); + // fix out of range so we don't have to iterate over them + if (tx<0) { + ox+=s*-tx; + tx=0; + } + if (ty<0) { + oy+=s*-ty; + ty=0; + } + var mx = g.getWidth(); + var my = g.getHeight(); + for (var x=ox,ttx=tx; x ac * expansion) && (x = ac * expansion); + (y > ac) && (y = ac); + //(x < 0) && (x = 0); + //(y < 0) && (y = 0); + return [x, y]; +} + +// Convert bbox to xyx bounds +// +// - `bbox` {Number} bbox in the form `[w, s, e, n]`. +// - `zoom` {Number} zoom. +// - `tms_style` {Boolean} whether to compute using tms-style. +// - `srs` {String} projection of input bbox (WGS84|900913). +// - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties. +xyz = function(bbox, zoom, tms_style, srs) { + // If web mercator provided reproject to WGS84. + if (srs === '900913') { + bbox = this.convert(bbox, 'WGS84'); + } + + var ll = [bbox[0], bbox[1]]; // lower left + var ur = [bbox[2], bbox[3]]; // upper right + var px_ll = px(ll, zoom); + var px_ur = px(ur, zoom); + // Y = 0 for XYZ is the top hence minY uses px_ur[1]. + var size = 256; + var x = [ Math.floor(px_ll[0] / size), Math.floor((px_ur[0] - 1) / size) ]; + var y = [ Math.floor(px_ur[1] / size), Math.floor((px_ll[1] - 1) / size) ]; + var bounds = { + minX: Math.min.apply(Math, x) < 0 ? 0 : Math.min.apply(Math, x), + minY: Math.min.apply(Math, y) < 0 ? 0 : Math.min.apply(Math, y), + maxX: Math.max.apply(Math, x), + maxY: Math.max.apply(Math, y) + }; + if (tms_style) { + var tms = { + minY: (Math.pow(2, zoom) - 1) - bounds.maxY, + maxY: (Math.pow(2, zoom) - 1) - bounds.minY + }; + bounds.minY = tms.minY; + bounds.maxY = tms.maxY; + } + return bounds; +}; + +// Convert screen pixel value to lon lat +// +// - `px` {Array} `[x, y]` array of geographic coordinates. +// - `zoom` {Number} zoom level. +function ll(px, zoom) { + var size = 256 * Math.pow(2, zoom); + var bc = (size / 360); + var cc = (size / (2 * Math.PI)); + var zc = size / 2; + var g = (px[1] - zc) / -cc; + var lon = (px[0] - zc) / bc; + var R2D = 180 / Math.PI; + var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI); + return [lon, lat]; +} + +// Convert tile xyz value to bbox of the form `[w, s, e, n]` +// +// - `x` {Number} x (longitude) number. +// - `y` {Number} y (latitude) number. +// - `zoom` {Number} zoom. +// - `tms_style` {Boolean} whether to compute using tms-style. +// - `srs` {String} projection for resulting bbox (WGS84|900913). +// - `return` {Array} bbox array of values in form `[w, s, e, n]`. +bbox = function(x, y, zoom, tms_style, srs) { + var size = 256; + + // Convert xyz into bbox with srs WGS84 + if (tms_style) { + y = (Math.pow(2, zoom) - 1) - y; + } + // Use +y to make sure it's a number to avoid inadvertent concatenation. + var ll_ = [x * size, (+y + 1) * size]; // lower left + // Use +x to make sure it's a number to avoid inadvertent concatenation. + var ur = [(+x + 1) * size, y * size]; // upper right + var bbox = ll(ll_, zoom).concat(ll(ur, zoom)); + + // If web mercator requested reproject to 900913. + if (srs === '900913') { + return this.convert(bbox, '900913'); + } else { + return bbox; + } +}; + +/* original openstmap_app.js */ + +//var m = require("openstmap"); +var HASWIDGETS = true; +var R; +var fix = {}; +var mapVisible = false; +var hasScrolled = false; +var settings = require("Storage").readJSON("openstmap.json",1)||{}; +var points; +var startDrag = 0; + +// Redraw the whole page +function redraw(qual) { + if (1) drawAll(qual); + g.setClipRect(R.x,R.y,R.x2,R.y2); + if (0) m.draw(); + drawPOI(); + drawMarker(); + // if track drawing is enabled... + if (settings.drawTrack) { + if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { + g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later + WIDGETS["gpsrec"].plotTrack(m); + } + if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) { + g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later + WIDGETS["recorder"].plotTrack(m); + } + } + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); +} + +// Draw the POIs +function drawPOI() { + if (1) return; + /* var waypoints = require("waypoints").load(); FIXME */ g.setFont("Vector", 18); + waypoints.forEach((wp, idx) => { + var p = m.latLonToXY(wp.lat, wp.lon); + var sz = 2; + g.setColor(0,0,0); + g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); + g.setColor(0,0,0); + g.drawString(wp.name, p.x, p.y); + print(wp.name); + }) +} + + + +// Draw the marker for where we are +function drawMarker() { + if (!fix.fix) return; + var p = m.latLonToXY(fix.lat, fix.lon); + g.setColor(1,0,0); + g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); +} + +Bangle.on('GPS',function(f) { + fix=f; + if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]); + if (mapVisible) drawMarker(); +}); +Bangle.setGPSPower(1, "app"); + +if (HASWIDGETS) { + Bangle.loadWidgets(); + WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{ + var txt = (0|fix.satellites)+" Sats"; + if (!fix.fix) txt += "\nNO FIX"; + g.reset().setFont("6x8").setFontAlign(0,0) + .drawString(txt,w.x+24,w.y+12); + } + }; + Bangle.drawWidgets(); +} +R = Bangle.appRect; + +function showMap() { + mapVisible = true; + g.reset().clearRect(R); + redraw(0); + emptyMap(); +} + +function emptyMap() { + Bangle.setUI({mode:"custom",drag:e=>{ + if (e.b) { + if (!startDrag) + startDrag = getTime(); + g.setClipRect(R.x,R.y,R.x2,R.y2); + g.scroll(e.dx,e.dy); + m.scroll(e.dx,e.dy); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + hasScrolled = true; + print("Has scrolled"); + } else if (hasScrolled) { + delta = getTime() - startDrag; + startDrag = 0; + hasScrolled = false; + print("Done", delta, e.x, e.y); + qual = 0; + if (delta < 0.2) { + if (e.x < g.getWidth() / 2) { + if (e.y < g.getHeight() / 2) { + m.scale /= 2; + } else { + m.scale *= 2; + } + } else { + if (e.y < g.getHeight() / 2) { + qual = 2; + } else { + qual = 4; + } + } + } + g.reset().clearRect(R); + redraw(qual); + } + }, btn: btn=>{ + mapVisible = false; + var menu = {"":{title:"Map"}, + "< Back": ()=> showMap(), + /*LANG*/"Zoom In": () =>{ + m.scale /= 2; + showMap(); + }, + /*LANG*/"Zoom Out": () =>{ + m.scale *= 2; + showMap(); + }, + /*LANG*/"Draw Track": { + value : !!settings.drawTrack, + onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); } + }, + /*LANG*/"Center Map": () =>{ + m.lat = m.map.lat; + m.lon = m.map.lon; + m.scale = m.map.scale; + showMap(); + }, + /*LANG*/"Benchmark": () =>{ + m.lat = 50.001; + m.lon = 14.759; + m.scale = 2; + g.reset().clearRect(R); + redraw(18); + print("Benchmark done (31 sec)"); + } + }; + if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{ + m.lat = fix.lat; + m.lon = fix.lon; + showMap(); + }; + E.showMenu(menu); + }}); +} + +var gjson = null; + +function readTarFile(tar, f) { + const st = require('Storage'); + json_off = st.read(tar, 0, 16) * 1; + if (isNaN(json_off)) { + print("Don't have archive", tar); + return undefined; + } + while (1) { + json_len = st.read(tar, json_off, 6) * 1; + if (json_len == -1) + break; + json_off += 6; + json = st.read(tar, json_off, json_len); + //print("Have directory, ", json.length, "bytes"); + //print(json); + files = JSON.parse(json); + //print(files); + rec = files[f]; + if (rec) + return st.read(tar, rec.st, rec.si); + json_off += json_len; + } + return undefined; +} + +function loadVector(name) { + var t1 = getTime(); + print(".. Read", name); + //s = require("Storage").read(name); + var s = readTarFile("delme.mtar", name); + if (s == undefined) { + print("Don't have file", name); + return null; + } + var r = JSON.parse(s); + print(".... Read and parse took ", getTime()-t1); + return r; +} + +function drawPoint(a) { + lon = a.geometry.coordinates[0]; + lat = a.geometry.coordinates[1]; + + var p = m.latLonToXY(lat, lon); + var sz = 2; + if (a.properties["marker-color"]) { + g.setColor(a.properties["marker-color"]); + } + if (a.properties.marker_size == "small") + sz = 1; + if (a.properties.marker_size == "large") + sz = 4; + + g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); + g.setColor(0,0,0); + g.setFont("Vector", 18).setFontAlign(-1,-1); + g.drawString(a.properties.name, p.x, p.y); + points ++; +} + +function drawLine(a, qual) { + lon = a.geometry.coordinates[0][0]; + lat = a.geometry.coordinates[0][1]; + i = 1; + step = 1; + len = a.geometry.coordinates.length; + step = step * qual; + var p1 = m.latLonToXY(lat, lon); + if (a.properties.stroke) { + g.setColor(a.properties.stroke); + } + while (i < len) { + lon = a.geometry.coordinates[i][0]; + lat = a.geometry.coordinates[i][1]; + var p2 = m.latLonToXY(lat, lon); + + //print(p1.x, p1.y, p2.x, p2.y); + g.drawLine(p1.x, p1.y, p2.x, p2.y); + if (i == len-1) + break; + i = i + step; + if (i>len) + i = len-1; + points ++; + p1 = p2; + g.flip(); + } +} + +function drawVector(gjson, qual) { + var d = gjson; + points = 0; + var t1 = getTime(); + + for (var a of d.features) { + if (a.type != "Feature") + print("Expecting feature"); + g.setColor(0,0,0); + // marker-size, marker-color, stroke + if (qual < 32 && a.geometry.type == "Point") + drawPoint(a); + if (qual < 8 && a.geometry.type == "LineString") + drawLine(a, qual); + } + print("....", points, "painted in", getTime()-t1, "sec"); +} + +function fname(lon, lat, zoom) { + var bbox = [lon, lat, lon, lat]; + var r = xyz(bbox, 13, false, "WGS84"); + //console.log('fname', r); + return 'z'+zoom+'-'+r.minX+'-'+r.minY+'.json'; +} + +function fnames(zoom) { + var bb = [m.lon, m.lat, m.lon, m.lat]; + var r = xyz(bb, zoom, false, "WGS84"); + while (1) { + var bb2 = bbox(r.minX, r.minY, zoom, false, "WGS84"); + var os = m.latLonToXY(bb2[3], bb2[0]); + if (os.x >= 0) + r.minX -= 1; + else if (os.y >= 0) + r.minY -= 1; + else break; + } + while (1) { + var bb2 = bbox(r.maxX, r.maxY, zoom, false, "WGS84"); + var os = m.latLonToXY(bb2[1], bb2[2]); + if (os.x <= g.getWidth()) + r.maxX += 1; + else if (os.y <= g.getHeight()) + r.maxY += 1; + else break; + } + print(".. paint range", r); + return r; +} + +function log2(x) { return Math.log(x) / Math.log(2); } + +function getZoom(qual) { + var z = 16-Math.round(log2(m.scale)); + z += qual; + z -= 0; + if (z < meta.min_zoom) + return meta.min_zoom; + if (z > meta.max_zoom) + return meta.max_zoom; + return z; +} + +function drawDebug(text, perc) { + g.setClipRect(0,0,R.x2,R.y); + g.reset(); + g.setColor(1,1,1).fillRect(0,0,R.x2,R.y); + g.setColor(1,0,0).fillRect(0,0,R.x2*perc,R.y); + g.setColor(0,0,0).setFont("Vector",15); + g.setFontAlign(0,0) + .drawString(text,80,10); + + g.setClipRect(R.x,R.y,R.x2,R.y2); + g.flip(); +} + +function drawAll(qual) { + var zoom = getZoom(qual); + var t1 = getTime(); + + drawDebug("Zoom "+zoom, 0); + + print("Draw all", m.scale, "->", zoom, "q", qual, "at", m.lat, m.lon); + var r = fnames(zoom); + var tiles = (r.maxY-r.minY+1) * (r.maxY-r.minY+1); + var num = 0; + drawDebug("Zoom "+zoom+" tiles "+tiles, 0); + for (y=r.minY; y<=r.maxY; y++) { + for (x=r.minX; x<=r.maxX; x++) { + + for (cnt=0; cnt<1000; cnt++) { + var n ='z'+zoom+'-'+x+'-'+y+'-'+cnt+'.json'; + var gjson = loadVector(n); + if (!gjson) break; + drawVector(gjson, 1); + } + num++; + drawDebug("Zoom "+zoom+" tiles "+num+"/"+tiles, num/tiles); + } + } + g.flip(); + Bangle.drawWidgets(); + print("Load and paint in", getTime()-t1, "sec"); +} + +function initVector() { + var s = readTarFile("delme.mtar", "meta.json"); + meta = JSON.parse(s); + +} + +function introScreen() { + g.reset().clearRect(R); + g.setColor(0,0,0).setFont("Vector",25); + g.setFontAlign(0,0); + g.drawString("SpaceWeaver", 85,35); + g.setColor(0,0,0).setFont("Vector",18); + g.drawString("Vector maps", 85,55); + g.drawString("Zoom "+meta.min_zoom+".."+meta.max_zoom, 85,75); +} + + +m.scale = 76; +m.lat = 50.001; +m.lon = 14.759; + +initVector(); +introScreen(); +emptyMap(); diff --git a/apps/spacew/app.png b/apps/spacew/app.png new file mode 100644 index 0000000000000000000000000000000000000000..0e52fa31684acc82bb0262f4fa05e31ef56151a3 GIT binary patch literal 3263 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 literal 0 HcmV?d00001 diff --git a/apps/spacew/metadata.json b/apps/spacew/metadata.json new file mode 100644 index 000000000..51bdb35b8 --- /dev/null +++ b/apps/spacew/metadata.json @@ -0,0 +1,13 @@ +{ "id": "spacew", + "name": "Space Weaver", + "version":"0.01", + "description": "Application for displaying vector maps", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "outdoors,gps,osm", + "storage": [ + {"name":"spacew.app.js","url":"app.js"}, + {"name":"spacew.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/spacew/prep/minitar.js b/apps/spacew/prep/minitar.js new file mode 100755 index 000000000..bf59e7a64 --- /dev/null +++ b/apps/spacew/prep/minitar.js @@ -0,0 +1,82 @@ +#!/usr/bin/nodejs + +var pc = 1; +var hack = 0; +const hs = require('./heatshrink.js'); + +if (pc) { + fs = require('fs'); + var print=console.log; +} else { + +} + +print("hello world"); + +function writeDir(json) { + json_str = JSON.stringify(json, "", " "); + dirent = '' + json_str.length; + while (dirent.length < 6) + dirent = dirent + ' '; + return dirent + json_str; +} + +function writeTar(tar, dir) { + var h_len = 16; + var cur = h_len; + files = fs.readdirSync(dir); + data = ''; + var directory = ''; + var json = {}; + for (f of files) { + d = fs.readFileSync(dir+f); + cs = d; + //cs = String.fromCharCode.apply(null, hs.compress(d)) + print("Processing", f, cur, d.length, cs.length); + //if (d.length == 42) continue; + data = data + cs; + var f_rec = {}; + f_rec.st = cur; + var len = d.length; + f_rec.si = len; + cur = cur + len; + json[f] = f_rec; + json_str = JSON.stringify(json, "", " "); + if (json_str.length < 16000) + continue; + directory += writeDir(json); + json = {}; + } + directory += writeDir(json); + directory += '-1 '; + + size = cur; + header = '' + size; + while (header.length < h_len) { + header = header+' '; + } + if (!hack) + fs.writeFileSync(tar, header+data+directory); + else + fs.writeFileSync(tar, directory); +} + +function readTarFile(tar, f) { + const st = require('Storage'); + json_off = st.read(tar, 0, 16) * 1; + print(json_off); + json = st.read(tar, json_off, -1); + //print(json); + files = JSON.parse(json); + //print(files); + rec = files[f]; + return st.read(tar, rec.st, rec.si); +} + +if (pc) + writeTar("delme.mtaz", "delme/"); +else { + print(readTarFile("delme.mtar", "ahoj")); + print(readTarFile("delme.mtar", "nazdar")); +} + diff --git a/apps/spacew/prep/prepare.json b/apps/spacew/prep/prepare.json new file mode 100644 index 000000000..33cb21a3c --- /dev/null +++ b/apps/spacew/prep/prepare.json @@ -0,0 +1,18 @@ +{ + "attributes": { + "type": false, + "id": false, + "version": false, + "changeset": false, + "timestamp": false, + "uid": false, + "user": false, + "way_nodes": false, + }, + "format_options": { + }, + "linear_tags": true, + "area_tags": false, + "exclude_tags": [], + "include_tags": [ "place", "name", "landuse", "highway" ] +} diff --git a/apps/spacew/prep/prepare.sh b/apps/spacew/prep/prepare.sh new file mode 100755 index 000000000..ac1db0019 --- /dev/null +++ b/apps/spacew/prep/prepare.sh @@ -0,0 +1,18 @@ +#!/bin/bash +if [ ".$1" == "-f" ]; then + I=/data/gis/osm/dumps/czech_republic-2023-07-24.osm.pbf + #I=/data/gis/osm/dumps/zernovka.osm.bz2 + O=cr.geojson + rm delme.pbf $O + time osmium extract $I --bbox 14.7,49.9,14.8,50.1 -f pbf -o delme.pbf + time osmium export delme.pbf -c prepare.json -o $O + echo "Converting to ascii" + time cstocs utf8 ascii cr.geojson > cr_ascii.geojson + mv -f cr_ascii.geojson delme.json +fi +rm -r delme/; mkdir delme +./split.js +./minitar.js +ls -lS delme/*.json | head -20 +cat delme/* | wc -c +ls -l delme.mtar diff --git a/apps/spacew/prep/split.js b/apps/spacew/prep/split.js new file mode 100755 index 000000000..f25e8e338 --- /dev/null +++ b/apps/spacew/prep/split.js @@ -0,0 +1,177 @@ +#!/usr/bin/nodejs --max-old-space-size=5500 + +// npm install geojson-vt +// docs: https://github.com/mapbox/geojson-vt +// output format: https://github.com/mapbox/vector-tile-spec/ + +const fs = require('fs'); +const sphm = require('./sphericalmercator.js'); +var split = require('geojson-vt') + +// delme.json needs to be real file, symlink to geojson will not work +console.log("Loading json"); +var gjs = require("./delme.json"); + +function tileToLatLon(x, y, z, x_, y_) { + var [ w, s, e, n ] = merc.bbox(x, y, z); + var lon = (e - w) * (x_ / 4096) + w; + var lat = (n - s) * (1-(y_ / 4096)) + s; + //console.log("to ", lon, lat); + return [ lon, lat ]; +} + +function convGeom(tile, geom) { + var g = []; + for (i = 0; i< geom.length; i++) { + var x = geom[i][0]; + var y = geom[i][1]; + //console.log("Geometry: ", geom, geom.length, "X,y", x, y); + var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); + g.push(pos); + } + return g; +} + +function zoomPoint(tags) { + var z = 99; + + if (tags.place == "city") z = 4; + if (tags.place == "town") z = 8; + if (tags.place == "village") z = 10; + + return z; +} + +function paintPoint(tags) { + var p = {}; + + if (tags.place == "village") p["marker-color"] = "#ff0000"; + + return p; +} + +function zoomWay(tags) { + var z = 99; + + if (tags.highway == "motorway") z = 7; + if (tags.highway == "primary") z = 9; + if (tags.highway == "secondary") z = 13; + if (tags.highway == "tertiary") z = 14; + if (tags.highway == "unclassified") z = 16; + if (tags.highway == "residential") z = 17; + if (tags.highway == "track") z = 17; + if (tags.highway == "path") z = 17; + if (tags.highway == "footway") z = 17; + + return z; +} + +function paintWay(tags) { + var p = {}; + + if (tags.highway == "motorway" || tags.highway == "primary") /* ok */; + if (tags.highway == "secondary" || tags.highway == "tertiary") p.stroke = "#0000ff"; + if (tags.highway == "tertiary" || tags.highway == "unclassified" || tags.highway == "residential") p.stroke = "#00ff00"; + if (tags.highway == "track") p.stroke = "#ff0000"; + if (tags.highway == "path" || tags.highway == "footway") p.stroke = "#800000"; + + return p; +} + +function writeFeatures(name, feat) +{ + var n = {}; + n.type = "FeatureCollection"; + n.features = feat; + + fs.writeFile(name+'.json', JSON.stringify(n), on_error); +} + +function toGjson(name, d, tile) { + var cnt = 0; + var feat = []; + for (var a of d) { + var f = {}; + var zoom = 99; + var p = {}; + f.properties = a.tags; + f.type = "Feature"; + f.geometry = {}; + if (a.type == 1) { + f.geometry.type = "Point"; + f.geometry.coordinates = convGeom(tile, a.geometry)[0]; + zoom = zoomPoint(a.tags); + p = paintPoint(a.tags); + } else if (a.type == 2) { + f.geometry.type = "LineString"; + f.geometry.coordinates = convGeom(tile, a.geometry[0]); + zoom = zoomWay(a.tags); + p = paintWay(a.tags); + } else { + //console.log("Unknown type", a.type); + } + //zoom -= 4; // Produces way nicer map, at expense of space. + if (tile.z < zoom) + continue; + f.properties = Object.assign({}, f.properties, p); + feat.push(f); + var s = JSON.stringify(feat); + if (s.length > 6000) { + console.log("tile too big, splitting", cnt); + writeFeatures(name+'-'+cnt++, feat); + feat = []; + } + } + writeFeatures(name+'-'+cnt, feat); + return n; +} + +function writeTile(name, d, tile) { + toGjson(name, d, tile) +} + +// By default, precomputes up to z30 +var merc = new sphm({ + size: 256, + antimeridian: true +}); + +//console.log(merc.ll([124, 123], 15)); +//console.log(merc.px([17734, 11102], 15)); +//console.log(merc.bbox(17734, 11102, 15)); +//return; + +console.log("Splitting data"); +var meta = {} +meta.min_zoom = 0; +meta.max_zoom = 17; // HERE + // = 16 ... split3 takes > 30 minutes + // = 13 ... 2 minutes +var index = split(gjs, Object.assign({ + maxZoom: meta.max_zoom, + indexMaxZoom: meta.max_zoom, + indexMaxPoints: 0, + tolerance: 30, +}), {}); +console.log("Producing output"); + +var output = {}; + +function on_error(e) { + if (e) { console.log(e); } +} + +var num = 0; +for (const id in index.tiles) { + const tile = index.tiles[id]; + const z = tile.z; + console.log(num++, ":", tile.x, tile.y, z); + var d = index.getTile(z, tile.x, tile.y).features; + //console.log(d); + var n = `delme/z${z}-${tile.x}-${tile.y}` ; + //output[n] = d; + //console.log(n); + writeTile(n, d, tile) +} + +fs.writeFile('delme/meta.json', JSON.stringify(meta), on_error); diff --git a/apps/spacew/prep/stats.sh b/apps/spacew/prep/stats.sh new file mode 100755 index 000000000..6c10ea1b0 --- /dev/null +++ b/apps/spacew/prep/stats.sh @@ -0,0 +1,22 @@ +#!/bin/bash +zoom() { + echo "Zoom $1" + cat delme/z$1-* | wc -c + echo "M..k..." +} + +echo "Total data" +cat delme/* | wc -c +echo "M..k..." +zoom 18 +zoom 17 +zoom 16 +zoom 15 +zoom 14 +zoom 13 +zoom 12 +zoom 11 +zoom 10 +echo "Zoom 1..9" +cat delme/z?-* | wc -c +echo "M..k..." From 1c96a66db92b2694830390afbfdbc166d39f8703 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 18 Aug 2023 22:49:30 +0200 Subject: [PATCH 197/242] Fix tabs-vs-spaces warning, remove extra debugging. --- apps/spacew/prep/minitar.js | 46 ++++++++++------------ apps/spacew/prep/split.js | 78 ++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 69 deletions(-) diff --git a/apps/spacew/prep/minitar.js b/apps/spacew/prep/minitar.js index bf59e7a64..e07c47049 100755 --- a/apps/spacew/prep/minitar.js +++ b/apps/spacew/prep/minitar.js @@ -11,13 +11,11 @@ if (pc) { } -print("hello world"); - function writeDir(json) { json_str = JSON.stringify(json, "", " "); dirent = '' + json_str.length; while (dirent.length < 6) - dirent = dirent + ' '; + dirent = dirent + ' '; return dirent + json_str; } @@ -29,23 +27,23 @@ function writeTar(tar, dir) { var directory = ''; var json = {}; for (f of files) { - d = fs.readFileSync(dir+f); - cs = d; - //cs = String.fromCharCode.apply(null, hs.compress(d)) - print("Processing", f, cur, d.length, cs.length); - //if (d.length == 42) continue; - data = data + cs; - var f_rec = {}; - f_rec.st = cur; - var len = d.length; - f_rec.si = len; - cur = cur + len; - json[f] = f_rec; - json_str = JSON.stringify(json, "", " "); - if (json_str.length < 16000) - continue; - directory += writeDir(json); - json = {}; + d = fs.readFileSync(dir+f); + cs = d; + //cs = String.fromCharCode.apply(null, hs.compress(d)) + print("Processing", f, cur, d.length, cs.length); + //if (d.length == 42) continue; + data = data + cs; + var f_rec = {}; + f_rec.st = cur; + var len = d.length; + f_rec.si = len; + cur = cur + len; + json[f] = f_rec; + json_str = JSON.stringify(json, "", " "); + if (json_str.length < 16000) + continue; + directory += writeDir(json); + json = {}; } directory += writeDir(json); directory += '-1 '; @@ -53,12 +51,12 @@ function writeTar(tar, dir) { size = cur; header = '' + size; while (header.length < h_len) { - header = header+' '; + header = header+' '; } if (!hack) - fs.writeFileSync(tar, header+data+directory); + fs.writeFileSync(tar, header+data+directory); else - fs.writeFileSync(tar, directory); + fs.writeFileSync(tar, directory); } function readTarFile(tar, f) { @@ -66,9 +64,7 @@ function readTarFile(tar, f) { json_off = st.read(tar, 0, 16) * 1; print(json_off); json = st.read(tar, json_off, -1); - //print(json); files = JSON.parse(json); - //print(files); rec = files[f]; return st.read(tar, rec.st, rec.si); } diff --git a/apps/spacew/prep/split.js b/apps/spacew/prep/split.js index f25e8e338..3d6f81b63 100755 --- a/apps/spacew/prep/split.js +++ b/apps/spacew/prep/split.js @@ -16,18 +16,16 @@ function tileToLatLon(x, y, z, x_, y_) { var [ w, s, e, n ] = merc.bbox(x, y, z); var lon = (e - w) * (x_ / 4096) + w; var lat = (n - s) * (1-(y_ / 4096)) + s; - //console.log("to ", lon, lat); return [ lon, lat ]; } function convGeom(tile, geom) { var g = []; for (i = 0; i< geom.length; i++) { - var x = geom[i][0]; - var y = geom[i][1]; - //console.log("Geometry: ", geom, geom.length, "X,y", x, y); - var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); - g.push(pos); + var x = geom[i][0]; + var y = geom[i][1]; + var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); + g.push(pos); } return g; } @@ -91,36 +89,36 @@ function toGjson(name, d, tile) { var cnt = 0; var feat = []; for (var a of d) { - var f = {}; - var zoom = 99; - var p = {}; - f.properties = a.tags; - f.type = "Feature"; - f.geometry = {}; - if (a.type == 1) { - f.geometry.type = "Point"; - f.geometry.coordinates = convGeom(tile, a.geometry)[0]; - zoom = zoomPoint(a.tags); - p = paintPoint(a.tags); - } else if (a.type == 2) { - f.geometry.type = "LineString"; - f.geometry.coordinates = convGeom(tile, a.geometry[0]); - zoom = zoomWay(a.tags); - p = paintWay(a.tags); - } else { - //console.log("Unknown type", a.type); - } - //zoom -= 4; // Produces way nicer map, at expense of space. - if (tile.z < zoom) - continue; - f.properties = Object.assign({}, f.properties, p); - feat.push(f); - var s = JSON.stringify(feat); - if (s.length > 6000) { - console.log("tile too big, splitting", cnt); - writeFeatures(name+'-'+cnt++, feat); - feat = []; - } + var f = {}; + var zoom = 99; + var p = {}; + f.properties = a.tags; + f.type = "Feature"; + f.geometry = {}; + if (a.type == 1) { + f.geometry.type = "Point"; + f.geometry.coordinates = convGeom(tile, a.geometry)[0]; + zoom = zoomPoint(a.tags); + p = paintPoint(a.tags); + } else if (a.type == 2) { + f.geometry.type = "LineString"; + f.geometry.coordinates = convGeom(tile, a.geometry[0]); + zoom = zoomWay(a.tags); + p = paintWay(a.tags); + } else { + //console.log("Unknown type", a.type); + } + //zoom -= 4; // Produces way nicer map, at expense of space. + if (tile.z < zoom) + continue; + f.properties = Object.assign({}, f.properties, p); + feat.push(f); + var s = JSON.stringify(feat); + if (s.length > 6000) { + console.log("tile too big, splitting", cnt); + writeFeatures(name+'-'+cnt++, feat); + feat = []; + } } writeFeatures(name+'-'+cnt, feat); return n; @@ -136,11 +134,6 @@ var merc = new sphm({ antimeridian: true }); -//console.log(merc.ll([124, 123], 15)); -//console.log(merc.px([17734, 11102], 15)); -//console.log(merc.bbox(17734, 11102, 15)); -//return; - console.log("Splitting data"); var meta = {} meta.min_zoom = 0; @@ -167,10 +160,7 @@ for (const id in index.tiles) { const z = tile.z; console.log(num++, ":", tile.x, tile.y, z); var d = index.getTile(z, tile.x, tile.y).features; - //console.log(d); var n = `delme/z${z}-${tile.x}-${tile.y}` ; - //output[n] = d; - //console.log(n); writeTile(n, d, tile) } From 188aaa99f775f79438c2cd9a2edfc8dc134f5b58 Mon Sep 17 00:00:00 2001 From: David Peer Date: Sat, 19 Aug 2023 07:18:53 +0200 Subject: [PATCH 198/242] clear icon area in case weather condition changed --- apps/edgeclk/app.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index 45dd0c4c2..4bb1f4700 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -70,8 +70,11 @@ temp = temp < 0 ? '\\' + String(temp*-1) : String(temp); g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true); + + // clear icon area in case weather condition changed + g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight()); + weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 13); - weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 15); } catch(e) { g.drawString("ERR", g.getWidth()-3, g.getHeight() - 1, true); } From 0cec394aab823e23074566237c71ba15749535e5 Mon Sep 17 00:00:00 2001 From: David Peer Date: Sat, 19 Aug 2023 07:21:12 +0200 Subject: [PATCH 199/242] Minor design change --- apps/edgeclk/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index 4bb1f4700..603dbd921 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -73,7 +73,7 @@ // clear icon area in case weather condition changed g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight()); - weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 13); + weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14); } catch(e) { g.drawString("ERR", g.getWidth()-3, g.getHeight() - 1, true); From 463a107c2809c37adb2882ffc5042e5cc8d92a5e Mon Sep 17 00:00:00 2001 From: David Peer Date: Sat, 19 Aug 2023 07:22:56 +0200 Subject: [PATCH 200/242] Show ? instead of err if weather is unknown. --- apps/edgeclk/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index 603dbd921..f9d5f803b 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -76,7 +76,7 @@ weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14); } catch(e) { - g.drawString("ERR", g.getWidth()-3, g.getHeight() - 1, true); + g.drawString("???", g.getWidth()-3, g.getHeight() - 1, true); } }; From c9ba8997b6917ce1ac1c10f28ca43ac7f9896ef8 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Thu, 17 Aug 2023 13:34:26 +0200 Subject: [PATCH 201/242] messagelist: add `debug()` statements Set `global.DEBUG_MESSAGELIST = true` to enable --- apps/messagelist/app.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js index dfa7e43d4..207c70412 100644 --- a/apps/messagelist/app.js +++ b/apps/messagelist/app.js @@ -24,6 +24,9 @@ RIGHT = 1, LEFT = -1, // swipe directions UP = -1, DOWN = 1; // updown directions const Layout = require("Layout"); + const debug = function() { + if (global.DEBUG_MESSAGELIST) console.log.apply(console, ['messagelist:'].concat(arguments)); + } const settings = () => require("messagegui").settings(); const fontTiny = "6x8"; // fixed size, don't use this for important things @@ -45,6 +48,7 @@ /// List of all our messages let MESSAGES; const saveMessages = function() { + debug('saveMessages()'); const noSave = ["alarm", "call", "music"]; // assume these are outdated once we close the app noSave.forEach(id => remove({id: id})); require("messages").write(MESSAGES @@ -56,6 +60,7 @@ ); }; const uiRemove = function() { + debug('uiRemove()'); if (musicTimeout) clearTimeout(musicTimeout); layout = undefined; Bangle.removeListener("message", onMessage); @@ -85,6 +90,7 @@ } const setUI = function(options, cb) { + debug('setUI(', options, cb?'':cb) delete Bangle.uiRemove; // don't clear out things when switching UI within the app options = Object.assign({mode:"custom", remove: () => uiRemove()}, options); // If options={} assume we still want `remove` to be called when leaving via fast load (so we must have 'mode:custom') @@ -111,6 +117,7 @@ }; const onMessage = function(type, msg) { + debug(`onMessage(${type}`, msg); if (msg.handled) return; msg.handled = true; switch(type) { @@ -135,6 +142,7 @@ Bangle.on("message", onMessage); const onCall = function(msg) { + debug('onCall(', msg); if (msg.t==="remove") { call = undefined; return exitScreen("call"); @@ -145,6 +153,7 @@ showCall(); }; const onAlarm = function(msg) { + debug('onAlarm(', msg); if (msg.t==="remove") { alarm = undefined; return exitScreen("alarm"); @@ -155,6 +164,7 @@ }; let musicTimeout; const onMusic = function(msg) { + debug('onMusic(', msg); const hadMusic = !!music; if (musicTimeout) clearTimeout(musicTimeout); musicTimeout = undefined; @@ -184,6 +194,7 @@ } }; const onMap = function(msg) { + debug('onMap(', msg); const hadMap = !!map; if (msg.t==="remove") { map = undefined; @@ -196,6 +207,7 @@ else if (active==="main" && !hadMap) showMain(); // refresh menu: add "Map" entry }; const onText = function(msg) { + debug('onText(', msg); require("messages").apply(msg, MESSAGES); const mIdx = MESSAGES.findIndex(m => m.id===msg.id); if (!MESSAGES[mIdx]) if (back==="messages") back = undefined; @@ -237,6 +249,7 @@ }; const showMap = function() { + debug('showMap()'); setActive("map"); delete map.new; let m, distance, street, target, eta; @@ -319,6 +332,7 @@ else Bangle.musicControl(action); }; const showMusic = function() { + debug('showMusic()', music); if (active!==music) setActive("music"); if (!music) music = {track: "", artist: "", album: "", state: "pause"}; delete music.new; @@ -442,12 +456,14 @@ let layout; const clearStuff = function() { + debug('clearStuff()'); delete Bangle.appRect; layout = undefined; setUI(); g.reset().clearRect(Bangle.appRect); }; const setActive = function(screen, args) { + debug(`setActive(${screen}`, args); clearStuff(); if (active && screen!==active) back = active; if (screen==="messages") messageNum = args; @@ -476,6 +492,7 @@ } }; const showMain = function() { + debug('showMain()'); setActive("main"); let grid = {"": {title:/*LANG*/"Messages", align: 0, back: load}}; if (call) grid[/*LANG*/"Incoming Call"] = {icon: "Phone", cb: showCall}; @@ -640,6 +657,7 @@ }; const showSettings = function() { + debug('showSettings()'); setActive("settings"); eval(require("Storage").read("messagelist.settings.js"))(() => { setFont(); @@ -647,6 +665,7 @@ }); }; const showCall = function() { + debug('showCall()'); setActive("call"); delete call.new; Bangle.setLocked(false); @@ -722,6 +741,7 @@ }); }; const showAlarm = function() { + debug('showAlarm()'); // dismissing alarms doesn't seem to work, so this is simple */ setActive("alarm"); delete alarm.new; @@ -830,6 +850,7 @@ ); }; const showMessage = function(num, bottom) { + debug(`showMessage(${num}, ${!!bottom})`); if (num<0) num = 0; if (!num) num = 0; // no number: show first if (num>=MESSAGES.length) num = MESSAGES.length-1; @@ -1133,6 +1154,7 @@ * Stop auto-unload timeout and buzzing, remove listeners for this function */ const clearUnreadStuff = function() { + debug('clearUnreadStuff()'); require("messages").stopBuzz(); if (unreadTimeout) clearTimeout(unreadTimeout); unreadTimeout = undefined; @@ -1208,4 +1230,4 @@ // stop buzzing, auto-close timeout on input ["touch", "drag", "swipe"].forEach(l => Bangle.on(l, clearUnreadStuff)); (B2 ? [BTN1] : [BTN1, BTN2, BTN3]).forEach(b => watches.push(setWatch(clearUnreadStuff, b, false))); -} \ No newline at end of file +} From 4473acdbdeb962f1d1947ef9ca003e20cb865a10 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 18 Aug 2023 21:14:43 +0200 Subject: [PATCH 202/242] messagelist: Fix app crashing when new message arrives new Layout(...) calls Bangle.setUI(), causing uiRemove() to be called whenever a new messages arrives, instead of only when the app exits. This fixes that by unsetting Bangle.uiRemove before calling Layout, and restoring it afterwards. --- apps/messagelist/ChangeLog | 4 +++- apps/messagelist/app.js | 25 +++++++++++++++++++------ apps/messagelist/metadata.json | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/messagelist/ChangeLog b/apps/messagelist/ChangeLog index 37854d8ae..899e29cb6 100644 --- a/apps/messagelist/ChangeLog +++ b/apps/messagelist/ChangeLog @@ -1,3 +1,5 @@ 0.01: New app! 0.02: Fix music updates while app is already open -0.03: Fix invalid use of Bangle.setUI \ No newline at end of file +0.03: Fix invalid use of Bangle.setUI +0.04: Fix app crashing when new message arrives + diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js index 207c70412..3c140a0c4 100644 --- a/apps/messagelist/app.js +++ b/apps/messagelist/app.js @@ -96,6 +96,19 @@ // If options={} assume we still want `remove` to be called when leaving via fast load (so we must have 'mode:custom') Bangle.setUI(options, cb); }; + /** + * Same as calling `new Layout(layout, options)`, except Bangle.uiRemove is not called + * @param {object} layout + * @param {object} options + * @returns {Layout} + */ + const makeLayout = function(layout, options) { + const remove = Bangle.uiRemove; + delete Bangle.uiRemove; // don't clear out things when setting up new Layout + const result = new Layout(layout, options); + if (remove) Bangle.uiRemove = remove; + return result; + } const remove = function(msg) { if (msg.id==="call") call = undefined; @@ -267,7 +280,7 @@ } else { target = map.body; } - let layout = new Layout({ + let layout = makeLayout({ type: "v", c: [ {type: "txt", font: fontNormal, label: target, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2}, { @@ -369,7 +382,7 @@ else if (dur) info = dur; else info = {}; - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ @@ -613,7 +626,7 @@ } l.c.push(row); } - layout = new Layout(l, {back: back}); + layout = makeLayout(l, {back: back}); layout.render(); if (B2) { @@ -697,7 +710,7 @@ ]; } - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ @@ -751,7 +764,7 @@ const w = g.getWidth()-48, lines = g.setFont(fontNormal).wrapString(alarm.title, w), title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n"); - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ @@ -1114,7 +1127,7 @@ let imageCol = getImageColor(msg); if (g.setColor(imageCol).getColor()==hBg) imageCol = hCol; - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: hBg, col: hCol, c: [ diff --git a/apps/messagelist/metadata.json b/apps/messagelist/metadata.json index 37fed5795..2f1abe11d 100644 --- a/apps/messagelist/metadata.json +++ b/apps/messagelist/metadata.json @@ -1,7 +1,7 @@ { "id": "messagelist", "name": "Message List", - "version": "0.03", + "version": "0.04", "description": "Display notifications from iOS and Gadgetbridge/Android as a list", "icon": "app.png", "type": "app", From b45bcab1815c7854266858bff2b650aea4c19bd8 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sat, 19 Aug 2023 16:28:04 +0200 Subject: [PATCH 203/242] astrocalc azimuth fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes #2651 azimuth value: 0° is south, so add 180° - Show all values in degrees --- apps/astrocalc/ChangeLog | 1 + apps/astrocalc/astrocalc-app.js | 24 +++++++++++++----------- apps/astrocalc/metadata.json | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 95b9dbaf1..156cf17bf 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -3,3 +3,4 @@ 0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.04: Compatibility with Bangle.js 2, get location from My Location 0.05: Enable widgets +0.06: Fix azimuth (bug #2651), only show degrees diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 2e732c37a..21e9a37ff 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -140,14 +140,15 @@ function drawData(title, obj, startX, startY) { function drawMoonPositionPage(gps, title) { const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0}; + const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north const pageData = { - Azimuth: pos.azimuth.toFixed(2), - Altitude: pos.altitude.toFixed(2), + Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '', + Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '', Distance: `${pos.distance.toFixed(0)} km`, - "Parallactic Ang": pos.parallacticAngle.toFixed(2), + "Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '', }; - const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5); drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20); drawPoints(); @@ -189,12 +190,14 @@ function drawMoonTimesPage(gps, title) { // Draw the moon rise position const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); - const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); + const riseAzimuth = risePos.azimuth + Math.PI; // 0 is south, we want 0 to be north + const riseAzimuthDegrees = parseInt(riseAzimuth * 180 / Math.PI); drawPoint(riseAzimuthDegrees, 8, moonColor); // Draw the moon set position const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); - const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); + const setAzimuth = setPos.azimuth + Math.PI; // 0 is south, we want 0 to be north + const setAzimuthDegrees = parseInt(setAzimuth * 180 / Math.PI); drawPoint(setAzimuthDegrees, 8, moonColor); Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); @@ -207,16 +210,15 @@ function drawSunShowPage(gps, key, date) { const mins = ("0" + date.getMinutes()).substr(-2); const secs = ("0" + date.getMinutes()).substr(-2); const time = `${hrs}:${mins}:${secs}`; + const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north - const azimuth = Number(pos.azimuth.toFixed(2)); - const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); - const altitude = Number(pos.altitude.toFixed(2)); + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + ''; + const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + ''; const pageData = { Time: time, Altitude: altitude, - Azimumth: azimuth, - Degrees: azimuthDegrees + Azimuth: azimuthDegrees, }; drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5); diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json index 1f4abb356..09dc53170 100644 --- a/apps/astrocalc/metadata.json +++ b/apps/astrocalc/metadata.json @@ -1,10 +1,10 @@ { "id": "astrocalc", "name": "Astrocalc", - "version": "0.05", + "version": "0.06", "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app", "icon": "astrocalc.png", - "tags": "app,sun,moon,cycles,tool", + "tags": "app,sun,moon,cycles,tool,outdoors", "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, "dependencies": {"mylocation":"app"}, From 5947397938afd3c05bfcbef4b2301e6f1a95c815 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sat, 19 Aug 2023 16:32:44 +0200 Subject: [PATCH 204/242] astrocalc encoding --- apps/astrocalc/astrocalc-app.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 21e9a37ff..5589a5703 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -143,10 +143,10 @@ function drawMoonPositionPage(gps, title) { const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north const pageData = { - Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '', - Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '', + Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '°', + Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°', Distance: `${pos.distance.toFixed(0)} km`, - "Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '', + "Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '°', }; const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5); @@ -212,8 +212,8 @@ function drawSunShowPage(gps, key, date) { const time = `${hrs}:${mins}:${secs}`; const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north - const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + ''; - const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + ''; + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + '°'; + const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°'; const pageData = { Time: time, From e379e52dd49ee5a7b25f5a09fc565ad2b230d3fb Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 20 Aug 2023 17:34:49 +0200 Subject: [PATCH 205/242] sokoban: more renaming --- apps/sokoban/app.js | 4 ++-- apps/sokoban/metadata.json | 2 +- apps/sokoban/{Microban.sok => sokoban.microban.sok} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename apps/sokoban/{Microban.sok => sokoban.microban.sok} (100%) diff --git a/apps/sokoban/app.js b/apps/sokoban/app.js index b7d89d6ba..890156214 100644 --- a/apps/sokoban/app.js +++ b/apps/sokoban/app.js @@ -61,9 +61,9 @@ function next_map_offsets(filename, start_offset) { let config = s.readJSON("sokoban.json", true); if (config === undefined) { - let initial_offsets = next_map_offsets("Microban.sok", 0); + let initial_offsets = next_map_offsets("sokoban.microban.sok", 0); config = { - levels_sets: ["Microban.sok"], // all known files containing levels + levels_sets: ["sokoban.microban.sok"], // all known files containing levels levels_set: 0, // which set are we using ? current_maps: [0], // what is current map on each set ? offsets: [initial_offsets], // known offsets for each levels set (binary positions of maps in each file) diff --git a/apps/sokoban/metadata.json b/apps/sokoban/metadata.json index 7a4d5bc50..ef4a45f83 100644 --- a/apps/sokoban/metadata.json +++ b/apps/sokoban/metadata.json @@ -13,7 +13,7 @@ "readme": "README.md", "storage": [ {"name":"sokoban.app.js","url":"app.js"}, - {"name":"Microban.sok", "url":"Microban.sok"}, + {"name":"sokoban.microban.sok", "url":"sokoban.microban.sok"}, {"name":"sokoban.img","url":"app-icon.js","evaluate":true} ], "data": [{"name":"sokoban.json"} diff --git a/apps/sokoban/Microban.sok b/apps/sokoban/sokoban.microban.sok similarity index 100% rename from apps/sokoban/Microban.sok rename to apps/sokoban/sokoban.microban.sok From 6b51925109a316507cb9b79cd812d00dad569316 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 21 Aug 2023 15:09:34 +0200 Subject: [PATCH 206/242] gipy: jit is back --- apps/gipy/ChangeLog | 3 +++ apps/gipy/TODO | 12 +++++++++++- apps/gipy/app.js | 34 +++++++++++++++++++--------------- apps/gipy/metadata.json | 2 +- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 870ad0fdb..3ca699a4f 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -98,3 +98,6 @@ * New setting : power screen off between points to save battery * Color change for lost direction (now purple) * Adaptive screen powersaving + +0.21: + * Jit is back for display functions (10% speed increase) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index b2a3c7ae1..614d2dda2 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -36,10 +36,20 @@ my conclusion is that: ************************** +JIT: array declaration in jit is buggy +(especially several declarations) + +************************** + ++ there is still a bug with negative remaining distances (when lost and nearest point is endpoint ?) ++ when lost we still get powersaving ++ try disabling gps for more powersaving + ++ add heights + + when you walk the direction still has a tendency to shift + put back foot only ways -+ try fiddling with jit + put back street names + put back shortest paths but with points cache this time and jit + how to display paths from shortest path ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 071ef8283..3a1ddcc61 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -139,6 +139,19 @@ class TilesOffsets { } } +// this function is not inlined to avoid array declaration in jit +function center_points(points, scaled_current_x, scaled_current_y) { + return g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); +} + +// this function is not inlined to avoid array declaration in jit +function rotate_points(points, c, s) { + let center_x = g.getWidth() / 2; + let center_y = g.getHeight() / 2 + Y_OFFSET; + + return g.transformVertices(points, [-c, s, s, c, center_x, center_y]); +} + class Map { constructor(buffer, offset, filename) { this.points_cache = []; // don't refetch points all the time @@ -393,17 +406,12 @@ class Map { cos_direction, sin_direction ) { - // "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; - + "jit"; let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); let scaled_current_x = current_x * scale_factor; let scaled_current_y = current_y * scale_factor; - let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); - let c = cos_direction; - let s = sin_direction; - let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); + let recentered_points = center_points(points, scaled_current_x, scaled_current_y); + let screen_points = rotate_points(recentered_points, cos_direction, sin_direction); for (let i = 0; i < screen_points.length; i += 4) { g.drawLine(screen_points[i], screen_points[i + 1], screen_points[i + 2], screen_points[i + 3]); @@ -419,17 +427,13 @@ class Map { cos_direction, sin_direction ) { - // "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; + "jit"; let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); let scaled_current_x = current_x * scale_factor; let scaled_current_y = current_y * scale_factor; - let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); - let c = cos_direction; - let s = sin_direction; - let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); + let recentered_points = center_points(points, scaled_current_x, scaled_current_y); + let screen_points = rotate_points(recentered_points, cos_direction, sin_direction); for (let i = 0; i < screen_points.length; i += 4) { let final_x = screen_points[i]; diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 7dd4123f6..3526e1afa 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.20", + "version": "0.21", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", From 3a6eb8b2adc41dd06cdffb6dd7fe9e07c3050493 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 21 Aug 2023 17:02:29 +0200 Subject: [PATCH 207/242] gipy: parsing heights --- apps/gipy/TODO | 3 +++ apps/gipy/app.js | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 614d2dda2..51ca38184 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -46,6 +46,9 @@ JIT: array declaration in jit is buggy + try disabling gps for more powersaving + add heights + -> have two views: zoomed in and zoomed out + ++ remove "lost" indicator and change position point's color instead + when you walk the direction still has a tendency to shift diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 3a1ddcc61..c608d182a 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -593,13 +593,14 @@ class Interests { } class Status { - constructor(path, maps, interests) { + constructor(path, maps, interests, heights) { this.path = path; this.default_options = true; // do we still have default options ? this.active = false; // should we have screen on this.last_activity = getTime(); this.maps = maps; this.interests = interests; + this.heights = heights; let half_screen_width = g.getWidth() / 2; let half_screen_height = g.getHeight() / 2; let half_screen_diagonal = Math.sqrt( @@ -1164,6 +1165,7 @@ function load_gps(filename) { let offset = 0; let path = null; + let heights = null; let maps = []; let interests = null; while (offset < file_size) { @@ -1186,6 +1188,11 @@ function load_gps(filename) { let res = new Interests(buffer, offset); interests = res[0]; offset = res[1]; + } else if (block_type == 4) { + console.log("loading heights"); + let heights_number = path.points.length / 2; + heights = Float64Array(buffer, offset, heights_number); + offset += 8 * heights_number; } else { console.log("todo : block type", block_type); } @@ -1197,10 +1204,10 @@ function load_gps(filename) { let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; E.showAlert(msg).then(function() { E.showAlert(); - start_gipy(path, maps, interests); + start_gipy(path, maps, interests, heights); }); } else { - start_gipy(path, maps, interests); + start_gipy(path, maps, interests, heights); } } @@ -1394,14 +1401,14 @@ function start(fn) { load_gps(fn); } -function start_gipy(path, maps, interests) { +function start_gipy(path, maps, interests, heights) { console.log("starting"); if (!simulated && settings.disable_bluetooth) { NRF.sleep(); // disable bluetooth completely } - status = new Status(path, maps, interests); + status = new Status(path, maps, interests, heights); setWatch( function() { From df46cd643e47f76cc752fac41e60ae8052d20faa Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 22 Aug 2023 14:37:44 +0200 Subject: [PATCH 208/242] gipy: elevation --- apps/gipy/README.md | 7 + apps/gipy/TODO | 3 - apps/gipy/app.js | 3011 +++++++++++++++++--------------- apps/gipy/pkg/gps.d.ts | 48 +- apps/gipy/pkg/gps.js | 735 +------- apps/gipy/pkg/gps_bg.wasm | Bin 748683 -> 743877 bytes apps/gipy/pkg/gps_bg.wasm.d.ts | 5 +- apps/gipy/pkg/package.json | 1 + 8 files changed, 1625 insertions(+), 2185 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 03ca97753..ac65f8c3f 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -28,6 +28,7 @@ It provides the following features : - toilets - artwork - bakeries +- display elevation data if available in the trace ## Usage @@ -95,6 +96,12 @@ The distance to next point displayed corresponds to the length of the black segm If you click the button you'll reach a menu where you can currently zoom out to see more of the map (with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on). +### Elevation + +If you touch the screen you will switch between display modes. +The first one displays the map, the second one the nearby elevation and the last one the elevation +for the whole path. + ### Settings Few settings for now (feel free to suggest me more) : diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 51ca38184..ef1df3dc5 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -45,9 +45,6 @@ JIT: array declaration in jit is buggy + when lost we still get powersaving + try disabling gps for more powersaving -+ add heights - -> have two views: zoomed in and zoomed out - + remove "lost" indicator and change position point's color instead + when you walk the direction still has a tendency to shift diff --git a/apps/gipy/app.js b/apps/gipy/app.js index c608d182a..70c1aafd3 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -7,24 +7,31 @@ let powersaving = true; let status; let interests_colors = [ - 0xffff, // Waypoints, white - 0xf800, // Bakery, red - 0x001f, // DrinkingWater, blue - 0x07ff, // Toilets, cyan - 0x07e0, // Artwork, green + 0xffff, // Waypoints, white + 0xf800, // Bakery, red + 0x001f, // DrinkingWater, blue + 0x07ff, // Toilets, cyan + 0x07e0, // Artwork, green ]; let Y_OFFSET = 20; + +// some constants for screen types +let MAP = 0; +let HEIGHTS_ZOOMED_IN = 1; +let HEIGHTS_FULL = 2; + let s = require("Storage"); -var settings = Object.assign({ - lost_distance: 50, - brightness: 0.5, - buzz_on_turns: false, - disable_bluetooth: true, - power_lcd_off: false, - }, - s.readJSON("gipy.json", true) || {} +var settings = Object.assign( + { + lost_distance: 50, + brightness: 0.5, + buzz_on_turns: false, + disable_bluetooth: true, + power_lcd_off: false, + }, + s.readJSON("gipy.json", true) || {} ); // let profile_start_times = []; @@ -41,25 +48,25 @@ var settings = Object.assign({ // return the index of the largest element of the array which is <= x function binary_search(array, x) { - let start = 0, - end = array.length; + let start = 0, + end = array.length; - while (end - start >= 0) { - let mid = Math.floor((start + end) / 2); - if (array[mid] == x) { - return mid; - } else if (array[mid] < x) { - if (array[mid + 1] > x) { - return mid; - } - start = mid + 1; - } else end = mid - 1; - } - if (array[start] > x) { - return null; - } else { - return start; - } + while (end - start >= 0) { + let mid = Math.floor((start + end) / 2); + if (array[mid] == x) { + return mid; + } else if (array[mid] < x) { + if (array[mid + 1] > x) { + return mid; + } + start = mid + 1; + } else end = mid - 1; + } + if (array[start] > x) { + return null; + } else { + return start; + } } // return a string containing estimated time of arrival. @@ -67,81 +74,88 @@ function binary_search(array, x) { // remaining distance in km // hour, minutes is current time function compute_eta(hour, minutes, approximate_speed, remaining_distance) { - if (isNaN(approximate_speed) || approximate_speed < 0.1) { - return ""; - } - let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes - let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); - let eta_minutes = eta_in_minutes % 60; - let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; - if (eta_minutes < 10) { - return eta_hour.toString() + ":0" + eta_minutes; - } else { - return eta_hour.toString() + ":" + eta_minutes; - } + if (isNaN(approximate_speed) || approximate_speed < 0.1) { + return ""; + } + let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes + let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); + let eta_minutes = eta_in_minutes % 60; + let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; + if (eta_minutes < 10) { + return eta_hour.toString() + ":0" + eta_minutes; + } else { + return eta_hour.toString() + ":" + eta_minutes; + } } class TilesOffsets { - constructor(buffer, offset) { - let type_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - this.entry_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; - this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); - offset += 2 * non_empty_tiles_number; - if (type_size == 24) { - this.non_empty_tiles_ends = Uint24Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 3 * non_empty_tiles_number; - } else if (type_size == 16) { - this.non_empty_tiles_ends = Uint16Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 2 * non_empty_tiles_number; - } else { - throw "unknown size"; - } - return [this, offset]; + constructor(buffer, offset) { + let type_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + this.entry_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; + this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); + offset += 2 * non_empty_tiles_number; + if (type_size == 24) { + this.non_empty_tiles_ends = Uint24Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 3 * non_empty_tiles_number; + } else if (type_size == 16) { + this.non_empty_tiles_ends = Uint16Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 2 * non_empty_tiles_number; + } else { + throw "unknown size"; } - tile_start_offset(tile_index) { - if (tile_index <= this.non_empty_tiles[0]) { - return 0; - } else { - return this.tile_end_offset(tile_index - 1); - } + return [this, offset]; + } + tile_start_offset(tile_index) { + if (tile_index <= this.non_empty_tiles[0]) { + return 0; + } else { + return this.tile_end_offset(tile_index - 1); } - tile_end_offset(tile_index) { - let me_or_before = binary_search(this.non_empty_tiles, tile_index); - if (me_or_before === null) { - return 0; - } - if (me_or_before >= this.non_empty_tiles_ends.length) { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * - this.entry_size - ); - } else { - return this.non_empty_tiles_ends[me_or_before] * this.entry_size; - } + } + tile_end_offset(tile_index) { + let me_or_before = binary_search(this.non_empty_tiles, tile_index); + if (me_or_before === null) { + return 0; } - end_offset() { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * - this.entry_size - ); + if (me_or_before >= this.non_empty_tiles_ends.length) { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * + this.entry_size + ); + } else { + return this.non_empty_tiles_ends[me_or_before] * this.entry_size; } + } + end_offset() { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * + this.entry_size + ); + } } // this function is not inlined to avoid array declaration in jit function center_points(points, scaled_current_x, scaled_current_y) { - return g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); + return g.transformVertices(points, [ + 1, + 0, + 0, + 1, + -scaled_current_x, + -scaled_current_y, + ]); } // this function is not inlined to avoid array declaration in jit @@ -153,1438 +167,1627 @@ function rotate_points(points, c, s) { } class Map { - constructor(buffer, offset, filename) { - this.points_cache = []; // don't refetch points all the time - // header - let color_array = Uint8Array(buffer, offset, 3); - this.color = [ - color_array[0] / 255, - color_array[1] / 255, - color_array[2] / 255, - ]; - offset += 3; - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; + constructor(buffer, offset, filename) { + this.points_cache = []; // don't refetch points all the time + // header + let color_array = Uint8Array(buffer, offset, 3); + this.color = [ + color_array[0] / 255, + color_array[1] / 255, + color_array[2] / 255, + ]; + offset += 3; + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; - // tiles offsets - let res = new TilesOffsets(buffer, offset); - this.tiles_offsets = res[0]; - offset = res[1]; + // tiles offsets + let res = new TilesOffsets(buffer, offset); + this.tiles_offsets = res[0]; + offset = res[1]; - // now, do binary ways - // since the file is so big we'll go line by line - let binary_lines = []; - for (let y = 0; y < this.grid_size[1]; y++) { - let first_tile_start = this.tiles_offsets.tile_start_offset( - y * this.grid_size[0] + // now, do binary ways + // since the file is so big we'll go line by line + let binary_lines = []; + for (let y = 0; y < this.grid_size[1]; y++) { + let first_tile_start = this.tiles_offsets.tile_start_offset( + y * this.grid_size[0] + ); + let last_tile_end = this.tiles_offsets.tile_start_offset( + (y + 1) * this.grid_size[0] + ); + let size = last_tile_end - first_tile_start; + let string = s.read(filename, offset + first_tile_start, size); + let array = Uint8Array(E.toArrayBuffer(string)); + binary_lines.push(array); + } + this.binary_lines = binary_lines; + offset += this.tiles_offsets.end_offset(); + + return [this, offset]; + + // now do streets data header + // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); + // let streets_header_offset = 0; + // let full_streets_size = Uint32Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 4; + // let blocks_number = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // let labels_string_size = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // offset += streets_header_offset; + + // // continue with main streets labels + // main_streets_labels = s.read(filename, offset, labels_string_size); + // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); + // this.main_streets_labels = main_streets_labels.split(/\n/); + // offset += labels_string_size; + + // // continue with blocks start offsets + // this.blocks_offsets = Uint32Array( + // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) + // ); + // offset += blocks_number * 4; + + // // continue with compressed street blocks + // let encoded_blocks_size = + // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; + // this.compressed_streets = Uint8Array( + // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) + // ); + // offset += encoded_blocks_size; + } + + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + g.setColor(this.color[0], this.color[1], this.color[2]); + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + + let limit = 1; + if (!zoomed) { + limit = 2; + } + for (let y = tile_y - limit; y <= tile_y + limit; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - limit; x <= tile_x + limit; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; + } + if ( + this.tile_is_on_screen( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ) + ) { + // let colors = [ + // [0, 0, 0], + // [0, 0, 1], + // [0, 1, 0], + // [0, 1, 1], + // [1, 0, 0], + // [1, 0, 1], + // [1, 1, 0], + // [1, 1, 0.5], + // [0.5, 0, 0.5], + // [0, 0.5, 0.5], + // ]; + if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { + this.display_thick_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction ); - let last_tile_end = this.tiles_offsets.tile_start_offset( - (y + 1) * this.grid_size[0] + } else { + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction ); - let size = last_tile_end - first_tile_start; - let string = s.read(filename, offset + first_tile_start, size); - let array = Uint8Array(E.toArrayBuffer(string)); - binary_lines.push(array); + } } - this.binary_lines = binary_lines; - offset += this.tiles_offsets.end_offset(); - - return [this, offset]; - - // now do streets data header - // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); - // let streets_header_offset = 0; - // let full_streets_size = Uint32Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 4; - // let blocks_number = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // let labels_string_size = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // offset += streets_header_offset; - - // // continue with main streets labels - // main_streets_labels = s.read(filename, offset, labels_string_size); - // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); - // this.main_streets_labels = main_streets_labels.split(/\n/); - // offset += labels_string_size; - - // // continue with blocks start offsets - // this.blocks_offsets = Uint32Array( - // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) - // ); - // offset += blocks_number * 4; - - // // continue with compressed street blocks - // let encoded_blocks_size = - // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; - // this.compressed_streets = Uint8Array( - // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) - // ); - // offset += encoded_blocks_size; + } } + } - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - g.setColor(this.color[0], this.color[1], this.color[2]); - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); + tile_is_on_screen( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let height = g.getHeight(); + let center_x = width / 2; + let center_y = height / 2 + Y_OFFSET; + let side = this.side; + let tile_center_x = (tile_x + 0.5) * side; + let tile_center_y = (tile_y + 0.5) * side; + let scaled_center_x = (tile_center_x - current_x) * scale_factor; + let scaled_center_y = (tile_center_y - current_y) * scale_factor; + let rotated_center_x = + scaled_center_x * cos_direction - scaled_center_y * sin_direction; + let rotated_center_y = + scaled_center_x * sin_direction + scaled_center_y * cos_direction; + let on_screen_center_x = center_x - rotated_center_x; + let on_screen_center_y = center_y + rotated_center_y; - let limit = 1; - if (!zoomed) { - limit = 2; - } - for (let y = tile_y - limit; y <= tile_y + limit; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - limit; x <= tile_x + limit; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - if ( - this.tile_is_on_screen( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ) - ) { - // let colors = [ - // [0, 0, 0], - // [0, 0, 1], - // [0, 1, 0], - // [0, 1, 1], - // [1, 0, 0], - // [1, 0, 1], - // [1, 1, 0], - // [1, 1, 0.5], - // [0.5, 0, 0.5], - // [0, 0.5, 0.5], - // ]; - if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { - this.display_thick_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } else { - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } - } - } - } + let scaled_side = side * scale_factor * Math.sqrt(1 / 2); + + if (on_screen_center_x + scaled_side <= 0) { + return false; } - - tile_is_on_screen( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let height = g.getHeight(); - let center_x = width / 2; - let center_y = height / 2 + Y_OFFSET; - let side = this.side; - let tile_center_x = (tile_x + 0.5) * side; - let tile_center_y = (tile_y + 0.5) * side; - let scaled_center_x = (tile_center_x - current_x) * scale_factor; - let scaled_center_y = (tile_center_y - current_y) * scale_factor; - let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction; - let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction; - let on_screen_center_x = center_x - rotated_center_x; - let on_screen_center_y = center_y + rotated_center_y; - - let scaled_side = side * scale_factor * Math.sqrt(1 / 2); - - if (on_screen_center_x + scaled_side <= 0) { - return false; - } - if (on_screen_center_x - scaled_side >= width) { - return false; - } - if (on_screen_center_y + scaled_side <= 0) { - return false; - } - if (on_screen_center_y - scaled_side >= height) { - return false; - } - return true; + if (on_screen_center_x - scaled_side >= width) { + return false; } - - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let line_start_offset = this.tiles_offsets.tile_start_offset( - tile_y * this.grid_size[0] - ); - let offset = - this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; - let upper_limit = - this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; - - let line = this.binary_lines[tile_y]; - // we need to copy both for correct results and for performances - // let's precompute also. - let cached_tile = new Float64Array(upper_limit - offset); - for (let i = offset; i < upper_limit; i += 2) { - let x = (tile_x + line.buffer[i] / 255) * scaled_side; - let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; - cached_tile[i - offset] = x; - cached_tile[i + 1 - offset] = y; - } - return cached_tile; + if (on_screen_center_y + scaled_side <= 0) { + return false; } - - invalidate_caches() { - this.points_cache = []; + if (on_screen_center_y - scaled_side >= height) { + return false; } + return true; + } - fetch_points(tile_x, tile_y, scaled_side) { - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } - } - if (this.points_cache.length > 40) { - this.points_cache.shift(); - } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let line_start_offset = this.tiles_offsets.tile_start_offset( + tile_y * this.grid_size[0] + ); + let offset = + this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; + let upper_limit = + this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; + + let line = this.binary_lines[tile_y]; + // we need to copy both for correct results and for performances + // let's precompute also. + let cached_tile = new Float64Array(upper_limit - offset); + for (let i = offset; i < upper_limit; i += 2) { + let x = (tile_x + line.buffer[i] / 255) * scaled_side; + let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; + cached_tile[i - offset] = x; + cached_tile[i + 1 - offset] = y; } + return cached_tile; + } - display_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - "jit"; - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - let recentered_points = center_points(points, scaled_current_x, scaled_current_y); - let screen_points = rotate_points(recentered_points, cos_direction, sin_direction); + invalidate_caches() { + this.points_cache = []; + } - for (let i = 0; i < screen_points.length; i += 4) { - g.drawLine(screen_points[i], screen_points[i + 1], screen_points[i + 2], screen_points[i + 3]); - } + fetch_points(tile_x, tile_y, scaled_side) { + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } } - - display_thick_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - "jit"; - - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - let recentered_points = center_points(points, scaled_current_x, scaled_current_y); - let screen_points = rotate_points(recentered_points, cos_direction, sin_direction); - - for (let i = 0; i < screen_points.length; i += 4) { - let final_x = screen_points[i]; - let final_y = screen_points[i + 1]; - let new_final_x = screen_points[i + 2]; - let new_final_y = screen_points[i + 3]; - - let xdiff = new_final_x - final_x; - let ydiff = new_final_y - final_y; - let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - let ox = (-ydiff / d) * 3; - let oy = (xdiff / d) * 3; - g.fillPoly([ - final_x + ox, - final_y + oy, - new_final_x + ox, - new_final_y + oy, - new_final_x - ox, - new_final_y - oy, - final_x - ox, - final_y - oy, - ]); - } + if (this.points_cache.length > 40) { + this.points_cache.shift(); } + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; + } + + display_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + let recentered_points = center_points( + points, + scaled_current_x, + scaled_current_y + ); + let screen_points = rotate_points( + recentered_points, + cos_direction, + sin_direction + ); + + for (let i = 0; i < screen_points.length; i += 4) { + g.drawLine( + screen_points[i], + screen_points[i + 1], + screen_points[i + 2], + screen_points[i + 3] + ); + } + } + + display_thick_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + let recentered_points = center_points( + points, + scaled_current_x, + scaled_current_y + ); + let screen_points = rotate_points( + recentered_points, + cos_direction, + sin_direction + ); + + for (let i = 0; i < screen_points.length; i += 4) { + let final_x = screen_points[i]; + let final_y = screen_points[i + 1]; + let new_final_x = screen_points[i + 2]; + let new_final_y = screen_points[i + 3]; + + let xdiff = new_final_x - final_x; + let ydiff = new_final_y - final_y; + let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + let ox = (-ydiff / d) * 3; + let oy = (xdiff / d) * 3; + g.fillPoly([ + final_x + ox, + final_y + oy, + new_final_x + ox, + new_final_y + oy, + new_final_x - ox, + new_final_y - oy, + final_x - ox, + final_y - oy, + ]); + } + } } class Interests { - constructor(buffer, offset) { - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; + constructor(buffer, offset) { + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; - let res = new TilesOffsets(buffer, offset); - offset = res[1]; - this.offsets = res[0]; - let end = this.offsets.end_offset(); - this.binary_interests = new Uint8Array(end); - let binary_interests = Uint8Array(buffer, offset, end); - for (let i = 0; i < end; i++) { - this.binary_interests[i] = binary_interests[i]; - } - offset += end; - this.points_cache = []; - return [this, offset]; + let res = new TilesOffsets(buffer, offset); + offset = res[1]; + this.offsets = res[0]; + let end = this.offsets.end_offset(); + this.binary_interests = new Uint8Array(end); + let binary_interests = Uint8Array(buffer, offset, end); + for (let i = 0; i < end; i++) { + this.binary_interests[i] = binary_interests[i]; } + offset += end; + this.points_cache = []; + return [this, offset]; + } - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); - for (let y = tile_y - 1; y <= tile_y + 1; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - 1; x <= tile_x + 1; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + for (let y = tile_y - 1; y <= tile_y + 1; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - 1; x <= tile_x + 1; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; } + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ); + } } + } - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let offset = this.offsets.tile_start_offset(tile_num); - let upper_limit = this.offsets.tile_end_offset(tile_num); + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let offset = this.offsets.tile_start_offset(tile_num); + let upper_limit = this.offsets.tile_end_offset(tile_num); - let tile_interests = []; - for (let i = offset; i < upper_limit; i += 3) { - let interest = this.binary_interests[i]; - let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; - let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; - if (interest >= interests_colors.length) { - throw "bad interest" + interest + "at" + tile_num + "offset" + i; - } - tile_interests.push(interest); - tile_interests.push(x); - tile_interests.push(y); - } - return tile_interests; + let tile_interests = []; + for (let i = offset; i < upper_limit; i += 3) { + let interest = this.binary_interests[i]; + let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; + let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; + if (interest >= interests_colors.length) { + throw "bad interest" + interest + "at" + tile_num + "offset" + i; + } + tile_interests.push(interest); + tile_interests.push(x); + tile_interests.push(y); } - fetch_points(tile_x, tile_y, scaled_side) { - //TODO: factorize with map ? - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } - } - if (this.points_cache.length > 40) { - this.points_cache.shift(); - } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; + return tile_interests; + } + fetch_points(tile_x, tile_y, scaled_side) { + //TODO: factorize with map ? + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } } - invalidate_caches() { - this.points_cache = []; + if (this.points_cache.length > 40) { + this.points_cache.shift(); } - display_tile( - tile_x, - tile_y, - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let half_width = width / 2; - let half_height = g.getHeight() / 2 + Y_OFFSET; - let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; + } + invalidate_caches() { + this.points_cache = []; + } + display_tile( + tile_x, + tile_y, + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let half_width = width / 2; + let half_height = g.getHeight() / 2 + Y_OFFSET; + let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = displayed_x * scale_factor; - let scaled_current_y = displayed_y * scale_factor; + let scaled_current_x = displayed_x * scale_factor; + let scaled_current_y = displayed_y * scale_factor; - for (let i = 0; i < interests.length; i += 3) { - let type = interests[i]; - let x = interests[i + 1]; - let y = interests[i + 2]; + for (let i = 0; i < interests.length; i += 3) { + let type = interests[i]; + let x = interests[i + 1]; + let y = interests[i + 2]; - let scaled_x = x - scaled_current_x; - let scaled_y = y - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = half_width - rotated_x; - let final_y = half_height + rotated_y; + let scaled_x = x - scaled_current_x; + let scaled_y = y - scaled_current_y; + let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let final_x = half_width - rotated_x; + let final_y = half_height + rotated_y; - let color = interests_colors[type]; - if (type == 0) { - g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); - } - g.setColor(color).fillCircle(final_x, final_y, 5); - } + let color = interests_colors[type]; + if (type == 0) { + g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); + } + g.setColor(color).fillCircle(final_x, final_y, 5); } + } } class Status { - constructor(path, maps, interests, heights) { - this.path = path; - this.default_options = true; // do we still have default options ? - this.active = false; // should we have screen on - this.last_activity = getTime(); - this.maps = maps; - this.interests = interests; - this.heights = heights; - let half_screen_width = g.getWidth() / 2; - let half_screen_height = g.getHeight() / 2; - let half_screen_diagonal = Math.sqrt( - half_screen_width * half_screen_width + - half_screen_height * half_screen_height - ); - this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates - this.on_path = true; // are we on the path or lost ? - this.position = null; // where we are - this.adjusted_cos_direction = 1; // cos of where we look at - this.adjusted_sin_direction = 0; // sin of where we look at - this.current_segment = null; // which segment is closest - this.reaching = null; // which waypoint are we reaching ? - this.distance_to_next_point = null; // how far are we from next point ? - this.projected_point = null; + constructor(path, maps, interests, heights) { + this.path = path; + this.default_options = true; // do we still have default options ? + this.active = false; // should we have screen on + this.last_activity = getTime(); + this.maps = maps; + this.interests = interests; + this.heights = heights; + this.screen = MAP; + let half_screen_width = g.getWidth() / 2; + let half_screen_height = g.getHeight() / 2; + let half_screen_diagonal = Math.sqrt( + half_screen_width * half_screen_width + + half_screen_height * half_screen_height + ); + this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates + this.on_path = true; // are we on the path or lost ? + this.position = null; // where we are + this.adjusted_cos_direction = 1; // cos of where we look at + this.adjusted_sin_direction = 0; // sin of where we look at + this.current_segment = null; // which segment is closest + this.reaching = null; // which waypoint are we reaching ? + this.distance_to_next_point = null; // how far are we from next point ? + this.projected_point = null; - if (this.path !== null) { - let r = [0]; - // let's do a reversed prefix computations on all distances: - // loop on all segments in reversed order - let previous_point = null; - for (let i = this.path.len - 1; i >= 0; i--) { - let point = this.path.point(i); - if (previous_point !== null) { - r.unshift(r[0] + point.distance(previous_point)); - } - previous_point = point; - } - this.remaining_distances = r; // how much distance remains at start of each segment + if (this.path !== null) { + let r = [0]; + // let's do a reversed prefix computations on all distances: + // loop on all segments in reversed order + let previous_point = null; + for (let i = this.path.len - 1; i >= 0; i--) { + let point = this.path.point(i); + if (previous_point !== null) { + r.unshift(r[0] + point.distance(previous_point)); } - this.starting_time = null; // time we start - this.advanced_distance = 0.0; - this.gps_coordinates_counter = 0; // how many coordinates did we receive - this.old_points = []; // record previous points but only when enough distance between them - this.old_times = []; // the corresponding times + previous_point = point; + } + this.remaining_distances = r; // how much distance remains at start of each segment } - activate() { - this.last_activity = getTime(); - if (this.active) { - return; - } else { - this.active = true; - Bangle.setLCDBrightness(settings.brightness); - Bangle.setLocked(false); - if (settings.power_lcd_off) { - Bangle.setLCDPower(true); - } - } + this.starting_time = null; // time we start + this.advanced_distance = 0.0; + this.gps_coordinates_counter = 0; // how many coordinates did we receive + this.old_points = []; // record previous points but only when enough distance between them + this.old_times = []; // the corresponding times + } + activate() { + this.last_activity = getTime(); + if (this.active) { + return; + } else { + this.active = true; + Bangle.setLCDBrightness(settings.brightness); + Bangle.setLocked(false); + if (settings.power_lcd_off) { + Bangle.setLCDPower(true); + } } - check_activity() { - if (!this.active || !powersaving) { - return; - } - if (getTime() - this.last_activity > 30) { - this.active = false; - Bangle.setLCDBrightness(0); - if (settings.power_lcd_off) { - Bangle.setLCDPower(false); - } - } + } + check_activity() { + if (!this.active || !powersaving) { + return; } - invalidate_caches() { - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].invalidate_caches(); - } - if (this.interests !== null) { - this.interests.invalidate_caches(); - } + if (getTime() - this.last_activity > 30) { + this.active = false; + Bangle.setLCDBrightness(0); + if (settings.power_lcd_off) { + Bangle.setLCDPower(false); + } } - new_position_reached(position) { - // we try to figure out direction by looking at previous points - // instead of the gps course which is not very nice. + } + invalidate_caches() { + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].invalidate_caches(); + } + if (this.interests !== null) { + this.interests.invalidate_caches(); + } + } + new_position_reached(position) { + // we try to figure out direction by looking at previous points + // instead of the gps course which is not very nice. - let now = getTime(); - - if (this.old_points.length == 0) { - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); - return null; - } else { - let previous_point = this.old_points[this.old_points.length - 1]; - let distance_to_previous = previous_point.distance(position); - // gps signal is noisy but rarely above 5 meters - if (distance_to_previous < 5) { - // update instant speed and return - let oldest_point = this.old_points[0]; - let distance_to_oldest = oldest_point.distance(position); - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - return null; - } - } - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); + let now = getTime(); + if (this.old_points.length == 0) { + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); + return null; + } else { + let previous_point = this.old_points[this.old_points.length - 1]; + let distance_to_previous = previous_point.distance(position); + // gps signal is noisy but rarely above 5 meters + if (distance_to_previous < 5) { + // update instant speed and return let oldest_point = this.old_points[0]; let distance_to_oldest = oldest_point.distance(position); - - // every 3 points we count the distance - if (this.gps_coordinates_counter % 3 == 0) { - if (distance_to_oldest < 150.0) { - // to avoid gps glitches - this.advanced_distance += distance_to_oldest; - } - } - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - - if (this.old_points.length == 4) { - this.old_points.shift(); - this.old_times.shift(); - } - // let's just take angle of segment between newest point and a point a bit before - let previous_index = this.old_points.length - 3; - if (previous_index < 0) { - previous_index = 0; - } - let diff = position.minus(this.old_points[previous_index]); - let angle = Math.atan2(diff.lat, diff.lon); - return angle; + return null; + } } - update_position(new_position) { - let direction = this.new_position_reached(new_position); - if (direction === null) { - if (this.old_points.length > 1) { - this.display(); // re-display because speed has changed - } - return; - } - if (in_menu) { - return; - } - if (this.instant_speed * 3.6 < 13) { - this.activate(); // if we go too slow turn on, we might be looking for the direction to follow - if (!this.default_options) { - this.default_options = true; + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); - Bangle.setOptions({ - lockTimeout: 10000, - backlightTimeout: 10000, - wakeOnTwist: true, - powerSave: true, - }); - } - } else { - if (this.default_options) { - this.default_options = false; + let oldest_point = this.old_points[0]; + let distance_to_oldest = oldest_point.distance(position); - Bangle.setOptions({ - lockTimeout: 0, - backlightTimeout: 0, - lcdPowerTimeout: 0, - hrmSportMode: 2, - wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. - wakeOnFaceUp: false, - wakeOnTouch: true, - powerSave: false, - }); - Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000) - } - - } - this.check_activity(); // if we don't move or are in menu we should stay on - - this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); - this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); - this.angle = direction; - let cos_direction = Math.cos(direction); - let sin_direction = Math.sin(direction); - this.position = new_position; - - // we will display position of where we'll be at in a few seconds - // and not where we currently are. - // this is because the display has more than 1sec duration. - this.displayed_position = new Point( - new_position.lon + cos_direction * this.instant_speed * 0.00001, - new_position.lat + sin_direction * this.instant_speed * 0.00001 - ); - - if (this.path !== null) { - // detect segment we are on now - let res = this.path.nearest_segment( - this.displayed_position, - Math.max(0, this.current_segment - 1), - Math.min(this.current_segment + 2, this.path.len - 1), - cos_direction, - sin_direction - ); - let orientation = res[0]; - let next_segment = res[1]; - - if (this.is_lost(next_segment)) { - // start_profiling(); - // it did not work, try anywhere - res = this.path.nearest_segment( - this.displayed_position, - 0, - this.path.len - 1, - cos_direction, - sin_direction - ); - orientation = res[0]; - next_segment = res[1]; - // end_profiling("repositioning"); - } - // now check if we strayed away from path or back to it - let lost = this.is_lost(next_segment); - if (this.on_path == lost) { - this.activate(); - // if status changes - if (lost) { - Bangle.buzz(); // we lost path - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - } - this.on_path = !lost; - } - - this.current_segment = next_segment; - - // check if we are nearing the next point on our path and alert the user - let next_point = this.current_segment + (1 - orientation); - this.distance_to_next_point = Math.ceil( - this.position.distance(this.path.point(next_point)) - ); - - // disable gps when far from next point and locked - // if (Bangle.isLocked() && !settings.keep_gps_alive) { - // let time_to_next_point = - // (this.distance_to_next_point * 3.6) / settings.max_speed; - // if (time_to_next_point > 60) { - // Bangle.setGPSPower(false, "gipy"); - // setTimeout(function () { - // Bangle.setGPSPower(true, "gipy"); - // }, time_to_next_point); - // } - // } - if (this.reaching != next_point && this.distance_to_next_point <= 100) { - this.activate(); - this.reaching = next_point; - let reaching_waypoint = this.path.is_waypoint(next_point); - if (reaching_waypoint) { - if (settings.buzz_on_turns) { - Bangle.buzz(); - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - } - } - } - } - - // abort most frames if inactive - if (!this.active && this.gps_coordinates_counter % 5 != 0) { - return; - } - - // re-display - this.display(); + // every 3 points we count the distance + if (this.gps_coordinates_counter % 3 == 0) { + if (distance_to_oldest < 150.0) { + // to avoid gps glitches + this.advanced_distance += distance_to_oldest; + } } - display_direction() { - //TODO: go towards point on path at 20 meter - if (this.current_segment === null) { - return; - } - let next_point = this.path.point(this.current_segment + (1 - go_backwards)); - let distance_to_next_point = Math.ceil( - this.projected_point.distance(next_point) - ); - let towards; - if (distance_to_next_point < 20) { - towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); - } else { - towards = next_point; - } - let diff = towards.minus(this.projected_point); - direction = Math.atan2(diff.lat, diff.lon); + this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - let full_angle = direction - this.angle; - // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); - // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); - - let scale; - if (zoomed) { - scale = this.scale_factor; - } else { - scale = this.scale_factor / 2; - } - - c = this.projected_point.coordinates( - this.displayed_position, - this.adjusted_cos_direction, - this.adjusted_sin_direction, - scale - ); - - let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); - let cos2 = Math.cos(full_angle + Math.PI / 2); - let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); - let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); - let sin2 = Math.sin(-full_angle - Math.PI / 2); - let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); - g.setColor(0, 1, 0).fillPoly([ - c[0] + cos1 * 15, - c[1] + sin1 * 15, - c[0] + cos2 * 20, - c[1] + sin2 * 20, - c[0] + cos3 * 15, - c[1] + sin3 * 15, - c[0] + cos3 * 10, - c[1] + sin3 * 10, - c[0] + cos2 * 15, - c[1] + sin2 * 15, - c[0] + cos1 * 10, - c[1] + sin1 * 10, - ]); + if (this.old_points.length == 4) { + this.old_points.shift(); + this.old_times.shift(); } - remaining_distance() { - let remaining_in_correct_orientation = - this.remaining_distances[this.current_segment + 1] + - this.position.distance(this.path.point(this.current_segment + 1)); - - if (go_backwards) { - return this.remaining_distances[0] - remaining_in_correct_orientation; - } else { - return remaining_in_correct_orientation; - } + // let's just take angle of segment between newest point and a point a bit before + let previous_index = this.old_points.length - 3; + if (previous_index < 0) { + previous_index = 0; } - // check if we are lost (too far from segment we think we are on) - // if we are adjust scale so that path will still be displayed. - // we do the scale adjustment here to avoid recomputations later on. - is_lost(segment) { - let projection = this.displayed_position.closest_segment_point( - this.path.point(segment), - this.path.point(segment + 1) - ); - this.projected_point = projection; // save this info for display - let distance_to_projection = this.displayed_position.distance(projection); - if (distance_to_projection > settings.lost_distance) { - return true; - } else { - return false; - } + let diff = position.minus(this.old_points[previous_index]); + let angle = Math.atan2(diff.lat, diff.lon); + return angle; + } + update_position(new_position) { + let direction = this.new_position_reached(new_position); + if (direction === null) { + if (this.old_points.length > 1) { + this.display(); // re-display because speed has changed + } + return; } - display() { - if (displaying || in_menu) { - return; // don't draw on drawings - } - displaying = true; - g.clear(); - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } + if (in_menu) { + return; + } + if (this.instant_speed * 3.6 < 13) { + this.activate(); // if we go too slow turn on, we might be looking for the direction to follow + if (!this.default_options) { + this.default_options = true; + Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 10000, + wakeOnTwist: true, + powerSave: true, + }); + } + } else { + if (this.default_options) { + this.default_options = false; + + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 0, + lcdPowerTimeout: 0, + hrmSportMode: 2, + wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. + wakeOnFaceUp: false, + wakeOnTouch: true, + powerSave: false, + }); + Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000) + } + } + this.check_activity(); // if we don't move or are in menu we should stay on + + this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.angle = direction; + let cos_direction = Math.cos(direction); + let sin_direction = Math.sin(direction); + this.position = new_position; + + // we will display position of where we'll be at in a few seconds + // and not where we currently are. + // this is because the display has more than 1sec duration. + this.displayed_position = new Point( + new_position.lon + cos_direction * this.instant_speed * 0.00001, + new_position.lat + sin_direction * this.instant_speed * 0.00001 + ); + + if (this.path !== null) { + // detect segment we are on now + let res = this.path.nearest_segment( + this.displayed_position, + Math.max(0, this.current_segment - 1), + Math.min(this.current_segment + 2, this.path.len - 1), + cos_direction, + sin_direction + ); + let orientation = res[0]; + let next_segment = res[1]; + + if (this.is_lost(next_segment)) { // start_profiling(); - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - // end_profiling("map"); - if (this.interests !== null) { - this.interests.display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - if (this.position !== null) { - this.display_path(); - } - - this.display_direction(); - this.display_stats(); - Bangle.drawWidgets(); - displaying = false; - } - display_stats() { - let now = new Date(); - let minutes = now.getMinutes().toString(); - if (minutes.length < 2) { - minutes = "0" + minutes; - } - let hours = now.getHours().toString(); - - // display the clock - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, 0, 24); - - let approximate_speed; - // display speed (avg and instant) - if (this.old_times.length > 0) { - let point_time = this.old_times[this.old_times.length - 1]; - let done_in = point_time - this.starting_time; - approximate_speed = Math.round( - (this.advanced_distance * 3.6) / done_in - ); - let approximate_instant_speed = Math.round(this.instant_speed * 3.6); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .drawString( - "" + - approximate_speed + - "km/h", - 0, - g.getHeight() - 15 - ); - - g.setFont("6x8:3") - .setFontAlign(1, -1, 0) - .drawString( - "" + approximate_instant_speed, - g.getWidth(), - g.getHeight() - 22 - ); - } - - if (this.path === null || this.position === null) { - return; - } - - let remaining_distance = this.remaining_distance(); - let rounded_distance = Math.round(remaining_distance / 100) / 10; - let total = Math.round(this.remaining_distances[0] / 100) / 10; - // now, distance to next point in meters - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString( - "" + this.distance_to_next_point + "m", - 0, - g.getHeight() - 49 - ); - - let forward_eta = compute_eta( - now.getHours(), - now.getMinutes(), - approximate_speed, - remaining_distance / 1000 + // it did not work, try anywhere + res = this.path.nearest_segment( + this.displayed_position, + 0, + this.path.len - 1, + cos_direction, + sin_direction ); + orientation = res[0]; + next_segment = res[1]; + // end_profiling("repositioning"); + } + // now check if we strayed away from path or back to it + let lost = this.is_lost(next_segment); + if (this.on_path == lost) { + this.activate(); + // if status changes + if (lost) { + Bangle.buzz(); // we lost path + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } + this.on_path = !lost; + } - // now display ETA - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(forward_eta, 0, 42); + this.current_segment = next_segment; - // display distance on path - g.setFont("6x8:2").drawString( - "" + rounded_distance + "/" + total, - 0, - g.getHeight() - 32 + // check if we are nearing the next point on our path and alert the user + let next_point = this.current_segment + (1 - orientation); + this.distance_to_next_point = Math.ceil( + this.position.distance(this.path.point(next_point)) + ); + + // disable gps when far from next point and locked + // if (Bangle.isLocked() && !settings.keep_gps_alive) { + // let time_to_next_point = + // (this.distance_to_next_point * 3.6) / settings.max_speed; + // if (time_to_next_point > 60) { + // Bangle.setGPSPower(false, "gipy"); + // setTimeout(function () { + // Bangle.setGPSPower(true, "gipy"); + // }, time_to_next_point); + // } + // } + if (this.reaching != next_point && this.distance_to_next_point <= 100) { + this.activate(); + this.reaching = next_point; + let reaching_waypoint = this.path.is_waypoint(next_point); + if (reaching_waypoint) { + if (settings.buzz_on_turns) { + Bangle.buzz(); + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } + } + } + } + + // abort most frames if inactive + if (!this.active && this.gps_coordinates_counter % 5 != 0) { + return; + } + + // re-display + this.display(); + } + display_direction() { + //TODO: go towards point on path at 20 meter + if (this.current_segment === null) { + return; + } + let next_point = this.path.point(this.current_segment + (1 - go_backwards)); + + let distance_to_next_point = Math.ceil( + this.projected_point.distance(next_point) + ); + let towards; + if (distance_to_next_point < 20) { + towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); + } else { + towards = next_point; + } + let diff = towards.minus(this.projected_point); + direction = Math.atan2(diff.lat, diff.lon); + + let full_angle = direction - this.angle; + // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); + // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); + + let scale; + if (zoomed) { + scale = this.scale_factor; + } else { + scale = this.scale_factor / 2; + } + + c = this.projected_point.coordinates( + this.displayed_position, + this.adjusted_cos_direction, + this.adjusted_sin_direction, + scale + ); + + let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); + let cos2 = Math.cos(full_angle + Math.PI / 2); + let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); + let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); + let sin2 = Math.sin(-full_angle - Math.PI / 2); + let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); + g.setColor(0, 1, 0).fillPoly([ + c[0] + cos1 * 15, + c[1] + sin1 * 15, + c[0] + cos2 * 20, + c[1] + sin2 * 20, + c[0] + cos3 * 15, + c[1] + sin3 * 15, + c[0] + cos3 * 10, + c[1] + sin3 * 10, + c[0] + cos2 * 15, + c[1] + sin2 * 15, + c[0] + cos1 * 10, + c[1] + sin1 * 10, + ]); + } + remaining_distance() { + let remaining_in_correct_orientation = + this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)); + + if (go_backwards) { + return this.remaining_distances[0] - remaining_in_correct_orientation; + } else { + return remaining_in_correct_orientation; + } + } + // check if we are lost (too far from segment we think we are on) + // if we are adjust scale so that path will still be displayed. + // we do the scale adjustment here to avoid recomputations later on. + is_lost(segment) { + let projection = this.displayed_position.closest_segment_point( + this.path.point(segment), + this.path.point(segment + 1) + ); + this.projected_point = projection; // save this info for display + let distance_to_projection = this.displayed_position.distance(projection); + if (distance_to_projection > settings.lost_distance) { + return true; + } else { + return false; + } + } + display() { + if (displaying || in_menu) { + return; // don't draw on drawings + } + displaying = true; + g.clear(); + if (this.screen == MAP) { + this.display_map(); + } else { + let current_position = 0; + if (this.current_segment !== null) { + current_position = + this.remaining_distances[0] - this.remaining_distance(); + } + if (this.screen == HEIGHTS_FULL) { + this.display_heights(0, current_position, this.remaining_distances[0]); + } else { + // only display 2500m + let start; + if (go_backwards) { + start = Math.max(0, current_position - 2000); + } else { + start = Math.max(0, current_position - 500); + } + let length = Math.min(2500, this.remaining_distances[0] - start); + this.display_heights(start, current_position, length); + } + } + Bangle.drawWidgets(); + displaying = false; + } + display_heights(display_start, current_position, displayed_length) { + let path_length = this.remaining_distances[0]; + let widgets_height = 24; + let graph_width = g.getWidth(); + let graph_height = g.getHeight() - 20 - widgets_height; + + let distance_per_pixel = displayed_length / graph_width; + + let start_point_index = 0; + let end_point_index = this.remaining_distances.length - 1; + for (let i = 0; i < this.remaining_distances.length; i++) { + let point_distance = path_length - this.remaining_distances[i]; + if (point_distance <= display_start) { + start_point_index = i; + } + if (point_distance >= display_start + displayed_length) { + end_point_index = i; + break; + } + } + let max_height = Number.NEGATIVE_INFINITY; + let min_height = Number.POSITIVE_INFINITY; + for (let i = start_point_index; i <= end_point_index; i++) { + let height = this.heights[i]; + max_height = Math.max(max_height, height); + min_height = Math.min(min_height, height); + } + + let displayed_height = max_height - min_height; + let height_per_pixel = displayed_height / graph_height; + // g.setColor(0, 0, 0).drawRect(0, widgets_height, graph_width, graph_height + widgets_height); + + let previous_x = null; + let previous_y = null; + let previous_height = null; + let previous_distance = null; + let current_x; + let current_y; + for (let i = start_point_index; i < end_point_index; i++) { + let point_distance = path_length - this.remaining_distances[i]; + let height = this.heights[i]; + let x = Math.round((point_distance - display_start) / distance_per_pixel); + if (go_backwards) { + x = graph_width - x; + } + let y = + widgets_height + + graph_height - + Math.round((height - min_height) / height_per_pixel); + if (x != previous_x) { + if (previous_x !== null) { + let steepness = + (height - previous_height) / (point_distance - previous_distance); + if (go_backwards) { + steepness *= -1; + } + let color; + if (steepness > 0.15) { + color = "#ff0000"; + } else if (steepness > 0.8) { + color = "#aa0000"; + } else if (steepness > 0.03) { + color = "#ffff00"; + } else if (steepness > -0.03) { + color = "#00ff00"; + } else if (steepness > -0.08) { + color = "#00aa44"; + } else if (steepness > -0.015) { + color = "#0044aa"; + } else { + color = "#0000ff"; + } + g.setColor(color); + g.fillPoly([ + previous_x, + previous_y, + x, + y, + x, + widgets_height + graph_height, + previous_x, + widgets_height + graph_height, + ]); + if ( + current_position >= previous_distance && + current_position < point_distance + ) { + let current_height = + previous_height + + ((current_position - previous_distance) / + (point_distance - previous_distance)) * + (height - previous_height); + current_x = Math.round( + (current_position - display_start) / distance_per_pixel + ); + if (go_backwards) { + current_x = graph_width - current_x; + } + current_y = + widgets_height + + graph_height - + Math.round((current_height - min_height) / height_per_pixel); + } + } + previous_distance = point_distance; + previous_height = height; + previous_x = x; + previous_y = y; + } + } + g.setColor(0, 0, 0); + g.fillCircle(current_x, current_y, 5); + + // display min dist/max dist and min height/max height + g.setColor(g.theme.fg); + g.setFont("6x8:2"); + g.setFontAlign(-1, 1, 0).drawString( + Math.ceil(display_start / 100) / 10, + 0, + g.getHeight() + ); + + g.setFontAlign(1, 1, 0).drawString( + Math.ceil((display_start + displayed_length) / 100) / 10, + g.getWidth(), + g.getHeight() + ); + + g.setFontAlign(1, 1, 0).drawString( + min_height, + g.getWidth(), + widgets_height + graph_height + ); + g.setFontAlign(1, -1, 0).drawString( + max_height, + g.getWidth(), + widgets_height + ); + } + display_map() { + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } + + // start_profiling(); + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + // end_profiling("map"); + if (this.interests !== null) { + this.interests.display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + if (this.position !== null) { + this.display_path(); + } + + this.display_direction(); + this.display_stats(); + } + display_stats() { + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = "0" + minutes; + } + let hours = now.getHours().toString(); + + // display the clock + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, 24); + + let approximate_speed; + // display speed (avg and instant) + if (this.old_times.length > 0) { + let point_time = this.old_times[this.old_times.length - 1]; + let done_in = point_time - this.starting_time; + approximate_speed = Math.round((this.advanced_distance * 3.6) / done_in); + let approximate_instant_speed = Math.round(this.instant_speed * 3.6); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .drawString("" + approximate_speed + "km/h", 0, g.getHeight() - 15); + + g.setFont("6x8:3") + .setFontAlign(1, -1, 0) + .drawString( + "" + approximate_instant_speed, + g.getWidth(), + g.getHeight() - 22 ); - - // display various indicators - if (this.distance_to_next_point <= 100) { - if (this.path.is_waypoint(this.reaching)) { - g.setColor(0.0, 1.0, 0.0) - .setFont("6x15") - .drawString("turn", g.getWidth() - 50, 30); - } - } - if (!this.on_path) { - g.setColor(1.0, 0.0, 0.0) - .setFont("6x15") - .drawString("lost", g.getWidth() - 55, 35); - } } - display_path() { - // don't display all segments, only those neighbouring current segment - // this is most likely to be the correct display - // while lowering the cost a lot - // - // note that all code is inlined here to speed things up - let cos = this.adjusted_cos_direction; - let sin = this.adjusted_sin_direction; - let displayed_x = this.displayed_position.lon; - let displayed_y = this.displayed_position.lat; - let width = g.getWidth(); - let height = g.getHeight(); - let half_width = width / 2; - let half_height = height / 2 + Y_OFFSET; - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } - if (this.path !== null) { - // compute coordinate for projection on path - let tx = (this.projected_point.lon - displayed_x) * scale_factor; - let ty = (this.projected_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let projected_x = half_width - Math.round(rotated_x); // x is inverted - let projected_y = half_height + Math.round(rotated_y); - - // display direction to next point if lost - if (!this.on_path) { - let next_point = this.path.point(this.current_segment + 1); - let previous_point = this.path.point(this.current_segment); - let nearest_point; - if ( - previous_point.fake_distance(this.position) < - next_point.fake_distance(this.position) - ) { - nearest_point = previous_point; - } else { - nearest_point = next_point; - } - let tx = (nearest_point.lon - displayed_x) * scale_factor; - let ty = (nearest_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); - } - - // display current-segment's projection - g.setColor(0, 0, 0); - g.fillCircle(projected_x, projected_y, 4); - } - - // now display ourselves - g.setColor(0, 0, 0); - g.fillCircle(half_width, half_height, 5); + if (this.path === null || this.position === null) { + return; } + + let remaining_distance = this.remaining_distance(); + let rounded_distance = Math.round(remaining_distance / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + // now, distance to next point in meters + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString( + "" + this.distance_to_next_point + "m", + 0, + g.getHeight() - 49 + ); + + let forward_eta = compute_eta( + now.getHours(), + now.getMinutes(), + approximate_speed, + remaining_distance / 1000 + ); + + // now display ETA + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(forward_eta, 0, 42); + + // display distance on path + g.setFont("6x8:2").drawString( + "" + rounded_distance + "/" + total, + 0, + g.getHeight() - 32 + ); + + // display various indicators + if (this.distance_to_next_point <= 100) { + if (this.path.is_waypoint(this.reaching)) { + g.setColor(0.0, 1.0, 0.0) + .setFont("6x15") + .drawString("turn", g.getWidth() - 50, 30); + } + } + if (!this.on_path) { + g.setColor(1.0, 0.0, 0.0) + .setFont("6x15") + .drawString("lost", g.getWidth() - 55, 35); + } + } + display_path() { + // don't display all segments, only those neighbouring current segment + // this is most likely to be the correct display + // while lowering the cost a lot + // + // note that all code is inlined here to speed things up + let cos = this.adjusted_cos_direction; + let sin = this.adjusted_sin_direction; + let displayed_x = this.displayed_position.lon; + let displayed_y = this.displayed_position.lat; + let width = g.getWidth(); + let height = g.getHeight(); + let half_width = width / 2; + let half_height = height / 2 + Y_OFFSET; + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } + + if (this.path !== null) { + // compute coordinate for projection on path + let tx = (this.projected_point.lon - displayed_x) * scale_factor; + let ty = (this.projected_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let projected_x = half_width - Math.round(rotated_x); // x is inverted + let projected_y = half_height + Math.round(rotated_y); + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let previous_point = this.path.point(this.current_segment); + let nearest_point; + if ( + previous_point.fake_distance(this.position) < + next_point.fake_distance(this.position) + ) { + nearest_point = previous_point; + } else { + nearest_point = next_point; + } + let tx = (nearest_point.lon - displayed_x) * scale_factor; + let ty = (nearest_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); + } + + // display current-segment's projection + g.setColor(0, 0, 0); + g.fillCircle(projected_x, projected_y, 4); + } + + // now display ourselves + g.setColor(0, 0, 0); + g.fillCircle(half_width, half_height, 5); + } } function load_gps(filename) { - // let's display splash screen while loading file + // let's display splash screen while loading file - let splashscreen = require("heatshrink").decompress( - atob( - "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" - ) - ); + let splashscreen = require("heatshrink").decompress( + atob( + "2Gwgdly1ZATttAQfZARm2AQXbAREsyXJARmyAQXLAViDgARm2AQVbAR0kyVJAQ2yAQVLARZfBAQSD/ARXZAQVtARnbAQe27aAE5ICClgCMLgICCQEQCCkqDnARb+BAQW2AQyDEARdLAQeyAR3LAQSDXL51v+x9bfAICC7ICM23ZPpD4BAQXJn//7IFCAQ2yAQR6YQZOSQZpBBsiDZARm2AQVbAQSDIAQt///btufTAOyBYL+DARJrBAQSDWLJvvQYNlz/7tiAeEYICBtoCHQZ/+7ds//7tu2pMsyXJlmOnAFDyRoBAQSAWAQUlyVZAQxcBAQX//3ZsjIBWYUtBYN8uPHjqMeAQVbQZ/2QYXbQYNbQwRNBnHjyVLkhNBARvLAQSDLIgNJKZf/+1ZsjIBlmzQwXPjlwg8cux9YtoCD7ICCQZ192yDBIINt2f7tuSvED/0AgeOhMsyXJAQeyAQR6MARElyT+BAQ9lIIL+CsqDF21Ajlx4EAuPBQa4CIQZ0EQYNnAQNt2QCByU48f+nEAh05kuyC4L+DARJ3BAQSDJsmWpICEfwJQEkESoNl2wXByaDB2PAQYPHgEB4cgEYKDc7KDOkmAgMkyCABy3bsuegHjx/4QYM4sk27d/+XJlmSAQpcBAQSAKAQQ1BZAVZkoCHBYNIgEApMgEwcHQYUcgPHEYVv+SDaGQSDNAQZDByUbDQM48eOn/ggCDB23bIIICB/1LC4ICB2QCLPoICEfwNJARA1BAQZEDgEJkkyQAKDB/gCBQYUt+ACB/yDsAQVA8ESrKDC//+nIjB7dt/0bQYNJlmS5ICG2QCCcwQCGGQslAQdZAQ4RDQAPJQYUf//DGQKAB31LQYKeCQbmT//8QZlIQAM4QYkZQYe+raDCC4eyAQVLARaDBAoL4CAQNkz///4FCAQxWCp8AQAKDCjlwU4OCQYcv3yDfIAP/+SDM8EOQYOPCgOAhFl2CDB20bQwIUCfwICMLgICC2XLGQsnIISnDKAVZkoCDpKADAQUSoARBhcs2/Dlm2QbEEiFJggvBeAIAC5KDKpKDF8AIBgEAhMkw3LQYgCIfYICC2QCHCgl/IIf5smWpICIniDELgQdBoEAgVJkqDboMkiVBIAYABQZcjxyDB//4Bw2QRAIIEfAICC5ICM2XJkGSUgIXBIIvkEwklAQdZkiDD4IOBrILDC4UAQbYCBo5BF/iDKkiDB//+LgYCY2QCCpYCCkGCpEkwVPIIv/fwMkAQNkAQuRQYNwBAVZAQRoCRgSDcv5BG+RlLvHjQDHJAQUsAQ6DBhACBn5BG/wpOrMlARZuBAQSDRgEQgMAiJAGAAPJgmQpMEfbQCSpaDDx5BJCgVkAQWWARhoBAQR9SQY0AoEEv5BI/MkiVBPs0sAQfJAQUAQYQ5Bj4CB/hHEExz+BAQT+BARVlAQSDPAAKDJ/8EiFBAQeQQ0gCFkECgEj//HQYUcuPHIIXkwQaHfYICCsgCMrICCQByDFHwQAI/iDFiVBkkSQc3JIIfx46ACAQ1yhEgyUJAQImOrICCkoCLPQICCQZCCKAAXBQYYCFyFJgiGiIIX8QBACD4EgwVIkmCDo1kAQWWARh0BAQR9GQY8H8aDM/CDJiVBkkSQccHQBQCDgGChCGBAQOShImLfYICFfwICKsoCCQYcAQRn+n/8iEBgCGIAQWQQbtPQaMcuSDEwVIkmCEw77BAQVkARlZAQSACAQN/IIM/8f+nCCI8f//H/x0AgkAoCDJiVBkkSQbOT/8AgKANAQiDEAQsJkA1PrICCkoCIz5BBhyDBxyDJAAYOB/iZBAAMBgCGIAQdJgiDUFwKDUjkCQZEIkmCpApCsgCFywCLv9lAoNl//HQYk/P5Hjx4GE+CEDgkAoCDKoMkiQCBPpeT//8AoMnQYSARAQVwH4OAQxMgyUJAQQ7IfwICCrMlz48B+VZngsBgeP/CAIAAaDB8YGD/CEDAAMDMQUQgKJJyFJAQRKGEYK8BhIqCQCQCEgECgEggUIEAX8QwkkwVIHAz7BAQVkAQN/+KqCg4pCOIKDN/0/QwQADwCCCBYIRDoEEgCDHAQMkiQCBJQiABnHggE4VoSDXAQPAgEPKoyDCAQkJkCGFAQdPEYcBFIaAMABsDBA/8gEBgEQgKGIAQNJgmSnCDDhwFDQbICBv5MI5CGFkmCpCACsgCCyImJfAYAOCIPjBA4TI8kAoCDKoMnPQJ9CgeAAQKDdAQMfHgXxBYl+QYYCEhMgyUJngRBgAAHf6R6Cx4FCnALDxyGC/BuCAQVAFoUQgKDEoARF8EOgACBiSDdjlwg4LIpMkhSGHo8cQJEkyRuDABxcBQwaDBMoIFCEYMONwY+BnFL12SoEgoEEgCDCCIfjwE4gYCBhMk2SDeuPAIQKGDFIOSIgICCyCDDwPAQY8SCgXjQaL4FAowAB+EAgYIB9cu3Xrlmy5JECGwIOCDQYCC0gOBCgKAbuB9DAQUAgPHQAgCEkUHP4wABTAplDABaSDPogCDEgMOQwX6r/+QYJrB5csySDCpaAIx06pYUEQbUAAQQABBAPSpF145uFAQOXjkB4ACCC4VIgCVGQYf+n7+FAgYLFMonghyrEh0SpeuyVIkmypEgF4MuQBE49IRB9euQYWyQbUcdw0HNYoCCpFwg8AAQYVDSo6DDKAKDLnAFF8EAfYOAgHj1gjBRIPjlxrDGQOQQBACBnVLl269esQbhrBhMh4BoEw8dNwslDQvAjkBAQKAHQYn4QZHjx4EBL4IJCMokA9ck3ED1xoBlmS8LyB5MgRgSAIAQOkPoIaD2VLlmCQbF0L4ZrLrgUBgCYBAQYABTYgCGPQwAELgX//xfBAQRlCxmS9euyTsCdISABAQKPBQBOOnVJCgKDCC4cgQbEAMpQCDkoaHgPAjkEDRj4C8aGCQY4CGwm48EEMoOscwQFBAQNIkApBhyAInCABTwSbB1waCAoMk2SDVuj1BAQJoLrgXFuEHgFwgUJTxpWDfASADn5iFgYCBgEO2XpLgPL0mSMQOSF4UIkmQTxOOiCYCQYIdBAQUuQYILBPprjBAoMAAQUAMplJkojKuAaNQYoCCQY47BnHgeQPggG69aDENwOChEgwUJCIKDKTAKDCAQKDC5Ms3XIkCDFPQYCE4VcIQIABi8cMptIU5UADRqDHgHj/xiG9JBDiXj0hlB1hrB0mCEAKABkmQDQihDAQQyCPQOyTYIdB1iGBBANIAQMcgLaCgBiIKwtdMpmHDpApBQB4CCeoXhh0QQY+Q9ek3Xr1z+BcYLsDQYKABEYIgBDQYgE9eOiQXCAQI4DQwIIBkmyhYLBgBZBjpZBL4clMQhlQpCAIAQMJQacAgiDBl26L4M6fYO4AoJ3BxgCB126pekL4fJkGChEgyT+FAQvpF4PJOgKDBwR6BUgYCCBwOygB6BVQR9BgVckmXjkAMSIUBQZPSQCKDDl04eoKDDoeu3DmBfYRZBSQLpCQYIdBQYJcBPomP/AFDwm4fYXJkmCpACBHAOy5CPCBAMJCIMJkPCI4VcuESeQcBMqCAJAQNwQCQCCheunT4CoeAiXr1m69MAmSDDcAlLL4MIkGSpb+E8f+AoihBVoXLCgL7C9csDodJAoMLQYZ3DrkAKAkgRIYCLQBICCuiDWPQKDCcYL4BBAaJCBAMsLgWShKDCkmQPQgCG8L7B5aDDAoaDBTwKJC1ytDI4tIL4qPEARMlQBVxDRoCKbQXol2y9JxBpaDBKASJB2TmBQAkgwVJhx9Ex/4QYkQDoVLF4IjFQAXIkizCFgSDGASlcQBICBuAmYpcuJQICCcYRZBL4YIB5MgQYKABQYOSfwvj/wFD8MAPoIgEhICB5L4FQYQRBRIKDaw6AJAQMBVTLRCJQSDCAoTpDPoKDCQAOCDQKAEAQ8LlhxCyRxChCnCliPB1wOBEYI7C5ACBQbCAKjdtwCqZQYZTDAoSDBBYtJLgKDBC4J9F//4AoXbtuwpcuOgIdBfYL4DEwOS9aDBFIOC5ckAQMuQbCAIAQPG7VtmiDbkGy5IFB5KGDAQYIChKDCkm4fwv/Aoc27dp01L0gmCwXr1gjDDoIFB1ytBBwIRCBARZVkqAIAQX2YoMwQbbdB5L1BhJZBboR9BAoSABQYNJhyADAQ2P2xBBw9LPoNIC4KDBOIIvB5B6CAoICBEwIFB9aDWriAJAQRBCnCDgbQJQCwUJlzdCBYWQPov//yDFYoXHof8EwRxBFgJ3CEYOC5KwBQYVLl26SoZWSw6AKAQMB/5KCjsEQbICBLgO65JWBhJWBpbUEd4J6Ex0//6JEoel4BCB48IDoPrkiGBAQa2CWASDBBAQvBSoZWRQBYCBpMF/8DI4NAQCyDEwT4BZwJTBBYJQBl2ShIOBhZ6EfwP/RIk68eBQQKDBgKDCeoPIFgYpBBYIFCQYXLQAPr1iDSQBYCB6VIurFB/04pf0QbFJkGChMsQYOucwRTCBwW4PQgCB//4BAkQYoUcv/CpMMEAOu3QgBwVIF4QpCAoPJAoICB2SGCKB8lQBaDDKYOS/+kWwaDZJQLOCcYLRByVLcAUOQAmPQAoCCEAME3UJZANBDQPJlxxD5AvBQZFIQadIQBgCBF4NIkrCBkkSQDCDE5ZKB9YCBRIJcBLIMDPQv/QY+uPQMEiVBgmyhBrCAQIpBU4R0DPQOCBwY7BBwIIBKBqAMkoCBCgeQpApBQb5oBAQSDBhEg3B6F//+QAmEyCDBTYWyfAL+BFIQgBF4SDCQAIFE126QYQUBQZp0CQZd0y4UCpB9aAQihCKYSJCFIOChEuPQmOn//RIiDB3VJlz+CTYRxBJRCDF1g1B1myRIOCTwKDMpCALQYYUEQcACBdISDBwSMBwVDPQuP/6JEQYfrdgIjC5CDD2QFBF4Wy5ICDQYOu2XrQYKPBQYI1BJpaAMAQVwQchWCAoZKBdgO4PQwCJPQMu3RxCPoyqB5YCCFgeyQYKeBBYNIQZ0lQBoCCuiDkLIRlCJQUIhyAOnHpDoRuBfAZoCQAosEpAUBBAKDB1iDBBYNLkiDJpCAOAQMJPr4CFJoLXCyUIMoMDQBoCB3FL1gdBNwPrEYSGCQAQFDBYaDDAoKPCQYcsQZKAOjskw6AjAQREBQYuAPQ3//AIFoeu3VLAQSDCRIQmB9ekFgSDBGQe6PQKABGQIOCAQQ+DJQ2HQZvXQEwCDIgMJkGCQYL+G//+BAs6QAL1C3TvDQYJoCRIOCpYsBhYIBpEuCga2BfwdLBYUsRIRHEkKALAQXCrqDuhaAEAQM//4IGQYW6QYKABQYQFBQYXLSQMLkgmBBAMIO4UgGoICCQYQjBQZFcQBgCDQE4CBhJWCQYJ3EAQOP/4IGAQKbBL4RlBeQQCCQYR6B9esR4fIBANLQAeCDQOShaDJy6AOQY+CMQaDgAQKDB3CDQiXJO4PJEARiBQwQICNYKDDpYOBC4IRDBAIRCQYYaBQYklQB6DFpCDBQAazDATcIEwICBfY3j//4QY86MQSDDfwREDwXLNYPrPoQUBQASPD1wLDQZMhQaEgwCDEMoiDfpBfBhMOQY3//yMHeQIdDdgZuBPQILBwRrCQwQCB3SDCpcuBAJ9BDQKGCAQJEFQBwCBjt0PRkJQbkIQYMDfYwCJ8JcBcAaDBQARrCQYYICQYnrTwPLQYKGBTYYaCCIOCIgSAOQYbdDQdSAO8eunFBPoKDByTmBQYOkRgIFBEwSDC5MgBYR6B1x3BAQQIBQAXIEASDDy6DPkmHpAXDTwZlGQb24QZ+kyFLOgSDD2RiBPoYmCKYL1DBYSACpcufwQCBSQKDD1hoCw6DPkvXLgiDpPQ3//yDIdgJcBfwVL0h3CyRuCFIiDDAQSYCUIJ9BCIMLQYwaBkqANAQV16S2EMQqJDBY6DWlx6Fn//QAoCCwkyQYJ3BlxfB0iACQZCVDfwYFBpJ9CBwMJRIQRC1gdBQBwCCuAvDO4cgQYgFBQbsLO4uP/6AGAQPhhxWBQYe6QAXJEw4LDOIRNBQYXIQYMIQYYIBBYNLFINIQaEJQYIdCHAaDCAQqDcgZ6F/6DJpYyCLgPrkm6EAiMBQY5TGfwSDB5AOEboaDBQByDDkESQYogCEYYCfO4qCB/CDI8ckiVLC4KDBPoQCBMQPr0gLB1jvCFgcIkGCKYOy5YLBQYQUCQa3CQASDIQECDHn///yAHx069ZWBOIXL1zyDBYO65esAoICBhIUBNwKDCQAKDEDQYgDQbB6jQZ6AGQYfBQYZoBl265JuCkm6PQQFBwUIBYPJBAKJC5MgBwKDCRgKDBSoWCCISDQ6VBL5AsBAoVIQceP/6DKiR6CO4QaBQYQjGQYRHBPoILDQYWCRgVIQYNL126RgOyeQOCQZ50EC4OSWwImCQwaDkQQKAHAQOEEaR9BQYTRGKwOCpaDBhCDBR4SDCBwSDPuAmCwSDCAQQ1DQwSDiQQKDKx0SFjSDFBASDCcwQRDBwIA=" + ) + ); - g.clear(); + g.clear(); - g.drawImage(splashscreen, 0, 0); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(0xf800) - .drawString(filename, 0, g.getHeight() - 30); - g.flip(); + g.drawImage(splashscreen, 0, 0); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(0xf800) + .drawString(filename, 0, g.getHeight() - 30); + g.flip(); - let buffer = s.readArrayBuffer(filename); - let file_size = buffer.length; - let offset = 0; + let buffer = s.readArrayBuffer(filename); + let file_size = buffer.length; + let offset = 0; - let path = null; - let heights = null; - let maps = []; - let interests = null; - while (offset < file_size) { - let block_type = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - if (block_type == 0) { - // it's a map - console.log("loading map"); - let res = new Map(buffer, offset, filename); - let map = res[0]; - offset = res[1]; - maps.push(map); - } else if (block_type == 2) { - console.log("loading path"); - let res = new Path(buffer, offset); - path = res[0]; - offset = res[1]; - } else if (block_type == 3) { - console.log("loading interests"); - let res = new Interests(buffer, offset); - interests = res[0]; - offset = res[1]; - } else if (block_type == 4) { - console.log("loading heights"); - let heights_number = path.points.length / 2; - heights = Float64Array(buffer, offset, heights_number); - offset += 8 * heights_number; - } else { - console.log("todo : block type", block_type); - } - } - - // checksum file size - if (offset != file_size) { - console.log("invalid file size", file_size, "expected", offset); - let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; - E.showAlert(msg).then(function() { - E.showAlert(); - start_gipy(path, maps, interests, heights); - }); + let path = null; + let heights = null; + let maps = []; + let interests = null; + while (offset < file_size) { + let block_type = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + if (block_type == 0) { + // it's a map + console.log("loading map"); + let res = new Map(buffer, offset, filename); + let map = res[0]; + offset = res[1]; + maps.push(map); + } else if (block_type == 2) { + console.log("loading path"); + let res = new Path(buffer, offset); + path = res[0]; + offset = res[1]; + } else if (block_type == 3) { + console.log("loading interests"); + let res = new Interests(buffer, offset); + interests = res[0]; + offset = res[1]; + } else if (block_type == 4) { + console.log("loading heights"); + let heights_number = path.points.length / 2; + heights = Int16Array(buffer, offset, heights_number); + offset += 2 * heights_number; } else { - start_gipy(path, maps, interests, heights); + console.log("todo : block type", block_type); } + } + + // checksum file size + if (offset != file_size) { + console.log("invalid file size", file_size, "expected", offset); + let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; + E.showAlert(msg).then(function () { + E.showAlert(); + start_gipy(path, maps, interests, heights); + }); + } else { + start_gipy(path, maps, interests, heights); + } } class Path { - constructor(buffer, offset) { - // let p = Uint16Array(buffer, offset, 1); - // console.log(p); - let points_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; + constructor(buffer, offset) { + // let p = Uint16Array(buffer, offset, 1); + // console.log(p); + let points_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; - // path points - this.points = Float64Array(buffer, offset, points_number * 2); - offset += 8 * points_number * 2; + // path points + this.points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; - // path waypoints - let waypoints_len = Math.ceil(points_number / 8.0); - this.waypoints = Uint8Array(buffer, offset, waypoints_len); - offset += waypoints_len; + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + this.waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; - return [this, offset]; + return [this, offset]; + } + + is_waypoint(point_index) { + let i = Math.floor(point_index / 8); + let subindex = point_index % 8; + let r = this.waypoints[i] & (1 << subindex); + return r != 0; + } + + // return point at given index + point(index) { + let lon = this.points[2 * index]; + let lat = this.points[2 * index + 1]; + return new Point(lon, lat); + } + + // return index of segment which is nearest from point. + // we need a direction because we need there is an ambiguity + // for overlapping segments which are taken once to go and once to come back. + // (in the other direction). + nearest_segment(point, start, end, cos_direction, sin_direction) { + // we are going to compute two min distances, one for each direction. + let indices = [0, 0]; + let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; + + let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); + for (let i = start + 1; i < end + 1; i++) { + let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); + + let closest_point = point.closest_segment_point(p1, p2); + let distance = point.length_squared(closest_point); + + let dot = + cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); + let orientation = +(dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; + } + + p1 = p2; } - is_waypoint(point_index) { - let i = Math.floor(point_index / 8); - let subindex = point_index % 8; - let r = this.waypoints[i] & (1 << subindex); - return r != 0; - } - - // return point at given index - point(index) { - let lon = this.points[2 * index]; - let lat = this.points[2 * index + 1]; - return new Point(lon, lat); - } - - // return index of segment which is nearest from point. - // we need a direction because we need there is an ambiguity - // for overlapping segments which are taken once to go and once to come back. - // (in the other direction). - nearest_segment(point, start, end, cos_direction, sin_direction) { - // we are going to compute two min distances, one for each direction. - let indices = [0, 0]; - let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; - - let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); - for (let i = start + 1; i < end + 1; i++) { - let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); - - let closest_point = point.closest_segment_point(p1, p2); - let distance = point.length_squared(closest_point); - - let dot = - cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); - let orientation = +(dot < 0); // index 0 is good orientation - if (distance <= mins[orientation]) { - mins[orientation] = distance; - indices[orientation] = i - 1; - } - - p1 = p2; - } - - // by default correct orientation (0) wins - // but if other one is really closer, return other one - if (mins[1] < mins[0] / 100.0) { - return [1, indices[1]]; - } else { - return [0, indices[0]]; - } - } - get len() { - return this.points.length / 2; + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 100.0) { + return [1, indices[1]]; + } else { + return [0, indices[0]]; } + } + get len() { + return this.points.length / 2; + } } class Point { - constructor(lon, lat) { - this.lon = lon; - this.lat = lat; - } - coordinates(current_position, cos_direction, sin_direction, scale_factor) { - let translated = this.minus(current_position).times(scale_factor); - let rotated_x = - translated.lon * cos_direction - translated.lat * sin_direction; - let rotated_y = - translated.lon * sin_direction + translated.lat * cos_direction; - return [ - g.getWidth() / 2 - Math.round(rotated_x), // x is inverted - g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, - ]; - } - minus(other_point) { - let xdiff = this.lon - other_point.lon; - let ydiff = this.lat - other_point.lat; - return new Point(xdiff, ydiff); - } - plus(other_point) { - return new Point(this.lon + other_point.lon, this.lat + other_point.lat); - } - length_squared(other_point) { - let londiff = this.lon - other_point.lon; - let latdiff = this.lat - other_point.lat; - return londiff * londiff + latdiff * latdiff; - } - times(scalar) { - return new Point(this.lon * scalar, this.lat * scalar); - } - // dot(other_point) { - // return this.lon * other_point.lon + this.lat * other_point.lat; - // } - distance(other_point) { - //see https://www.movable-type.co.uk/scripts/latlong.html - const R = 6371e3; // metres - const phi1 = (this.lat * Math.PI) / 180; - const phi2 = (other_point.lat * Math.PI) / 180; - const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; - const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; + } + coordinates(current_position, cos_direction, sin_direction, scale_factor) { + let translated = this.minus(current_position).times(scale_factor); + let rotated_x = + translated.lon * cos_direction - translated.lat * sin_direction; + let rotated_y = + translated.lon * sin_direction + translated.lat * cos_direction; + return [ + g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, + ]; + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let londiff = this.lon - other_point.lon; + let latdiff = this.lat - other_point.lat; + return londiff * londiff + latdiff * latdiff; + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + // dot(other_point) { + // return this.lon * other_point.lon + this.lat * other_point.lat; + // } + distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = (this.lat * Math.PI) / 180; + const phi2 = (other_point.lat * Math.PI) / 180; + const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; + const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; - const a = - Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + - Math.cos(phi1) * - Math.cos(phi2) * - Math.sin(deltalambda / 2) * - Math.sin(deltalambda / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const a = + Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + + Math.cos(phi1) * + Math.cos(phi2) * + Math.sin(deltalambda / 2) * + Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; // in meters + return R * c; // in meters + } + fake_distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + // return closest point from 'this' on [v,w] segment. + // since this function is critical we inline all code here. + closest_segment_point(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let segment_londiff = w.lon - v.lon; + let segment_latdiff = w.lat - v.lat; + let l2 = + segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return v; // v == w case } - fake_distance(other_point) { - return Math.sqrt(this.length_squared(other_point)); - } - // return closest point from 'this' on [v,w] segment. - // since this function is critical we inline all code here. - closest_segment_point(v, w) { - // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment - // Return minimum distance between line segment vw and point p - let segment_londiff = w.lon - v.lon; - let segment_latdiff = w.lat - v.lat; - let l2 = - segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt - if (l2 == 0.0) { - return v; // v == w case - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. - // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below - let start_londiff = this.lon - v.lon; - let start_latdiff = this.lat - v.lat; - let t = - (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; - if (t < 0) { - t = 0; - } else { - if (t > 1) { - t = 1; - } - } - let lon = v.lon + segment_londiff * t; - let lat = v.lat + segment_latdiff * t; - return new Point(lon, lat); + // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below + let start_londiff = this.lon - v.lon; + let start_latdiff = this.lat - v.lat; + let t = + (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; + if (t < 0) { + t = 0; + } else { + if (t > 1) { + t = 1; + } } + let lon = v.lon + segment_londiff * t; + let lat = v.lat + segment_latdiff * t; + return new Point(lon, lat); + } } let fake_gps_point = 0; - function drawMenu() { - const menu = { - "": { - title: "choose trace" - }, - }; - var files = s.list(".gps"); - for (var i = 0; i < files.length; ++i) { - menu[files[i]] = start.bind(null, files[i]); - } - menu["Exit"] = function() { - load(); - }; - E.showMenu(menu); + const menu = { + "": { + title: "choose trace", + }, + }; + var files = s.list(".gps"); + for (var i = 0; i < files.length; ++i) { + menu[files[i]] = start.bind(null, files[i]); + } + menu["Exit"] = function () { + load(); + }; + E.showMenu(menu); } function start(fn) { - E.showMenu(); - console.log("loading", fn); + E.showMenu(); + console.log("loading", fn); - load_gps(fn); + load_gps(fn); } function start_gipy(path, maps, interests, heights) { - console.log("starting"); + console.log("starting"); - if (!simulated && settings.disable_bluetooth) { - NRF.sleep(); // disable bluetooth completely - } + if (!simulated && settings.disable_bluetooth) { + NRF.sleep(); // disable bluetooth completely + } - status = new Status(path, maps, interests, heights); + status = new Status(path, maps, interests, heights); - setWatch( - function() { - status.activate(); - if (in_menu) { - return; - } - in_menu = true; - const menu = { - "": { - title: "choose action" - }, - "Go Backward": { - value: go_backwards, - format: (v) => (v ? "On" : "Off"), - onchange: (v) => { - go_backwards = v; - }, - }, - Zoom: { - value: zoomed, - format: (v) => (v ? "In" : "Out"), - onchange: (v) => { - status.invalidate_caches(); - zoomed = v; - }, - }, - /*LANG*/ - "powersaving": { - value: powersaving, - onchange: (v) => { - powersaving = v; - } - }, - "back to map": function() { - in_menu = false; - E.showMenu(); - g.clear(); - g.flip(); - if (status !== null) { - status.display(); - } - }, - }; - E.showMenu(menu); + setWatch( + function () { + status.activate(); + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { + title: "choose action", }, - BTN1, { - repeat: true - } + "Go Backward": { + value: go_backwards, + format: (v) => (v ? "On" : "Off"), + onchange: (v) => { + go_backwards = v; + }, + }, + Zoom: { + value: zoomed, + format: (v) => (v ? "In" : "Out"), + onchange: (v) => { + status.invalidate_caches(); + zoomed = v; + }, + }, + /*LANG*/ + powersaving: { + value: powersaving, + onchange: (v) => { + powersaving = v; + }, + }, + "back to map": function () { + in_menu = false; + E.showMenu(); + g.clear(); + g.flip(); + if (status !== null) { + status.display(); + } + }, + }; + E.showMenu(menu); + }, + BTN1, + { + repeat: true, + } + ); + + if (status.path !== null) { + let start = status.path.point(0); + status.displayed_position = start; + } else { + let first_map = maps[0]; + status.displayed_position = new Point( + first_map.start_coordinates[0] + + (first_map.side * first_map.grid_size[0]) / 2, + first_map.start_coordinates[1] + + (first_map.side * first_map.grid_size[1]) / 2 ); + } + status.display(); - - if (status.path !== null) { - let start = status.path.point(0); - status.displayed_position = start; - } else { - let first_map = maps[0]; - status.displayed_position = new Point( - first_map.start_coordinates[0] + - (first_map.side * first_map.grid_size[0]) / 2, - first_map.start_coordinates[1] + - (first_map.side * first_map.grid_size[1]) / 2); + Bangle.on("touch", () => { + status.activate(); + if (in_menu) { + return; } + if (status.heights !== null) { + status.screen = (status.screen + 1) % 3; + status.display(); + } + }); + + Bangle.on("stroke", (o) => { + status.activate(); + if (in_menu) { + return; + } + // we move display according to stroke + let first_x = o.xy[0]; + let first_y = o.xy[1]; + let last_x = o.xy[o.xy.length - 2]; + let last_y = o.xy[o.xy.length - 1]; + let xdiff = last_x - first_x; + let ydiff = last_y - first_y; + + let c = status.adjusted_cos_direction; + let s = status.adjusted_sin_direction; + let rotated_x = xdiff * c - ydiff * s; + let rotated_y = xdiff * s + ydiff * c; + status.displayed_position.lon += (1.3 * rotated_x) / status.scale_factor; + status.displayed_position.lat -= (1.3 * rotated_y) / status.scale_factor; status.display(); + }); - Bangle.on("stroke", (o) => { - status.activate(); - if (in_menu) { - return; + if (simulated) { + status.starting_time = getTime(); + // let's keep the screen on in simulations + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + + function simulate_gps(status) { + if (status.path === null) { + let map = status.maps[0]; + let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); + let p2 = new Point( + map.start_coordinates[0] + map.side * map.grid_size[0], + map.start_coordinates[1] + map.side * map.grid_size[1] + ); + let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); + if (fake_gps_point < 1) { + fake_gps_point += 0.05; } - // we move display according to stroke - let first_x = o.xy[0]; - let first_y = o.xy[1]; - let last_x = o.xy[o.xy.length - 2]; - let last_y = o.xy[o.xy.length - 1]; - let xdiff = last_x - first_x; - let ydiff = last_y - first_y; - - let c = status.adjusted_cos_direction; - let s = status.adjusted_sin_direction; - let rotated_x = xdiff * c - ydiff * s; - let rotated_y = xdiff * s + ydiff * c; - status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor; - status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor; - status.display(); - }); - - if (simulated) { - status.starting_time = getTime(); - // let's keep the screen on in simulations - Bangle.setLCDTimeout(0); - Bangle.setLCDPower(1); - Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen - - - function simulate_gps(status) { - if (status.path === null) { - let map = status.maps[0]; - let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); - let p2 = new Point( - map.start_coordinates[0] + map.side * map.grid_size[0], - map.start_coordinates[1] + map.side * map.grid_size[1] - ); - let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); - if (fake_gps_point < 1) { - fake_gps_point += 0.05; - } - status.update_position(pos); - } else { - if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { - return; - } - let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len / 2 - 1) { - return; - } - let p1 = status.path.point(2 * point_index); // use these to approximately follow path - let p2 = status.path.point(2 * (point_index + 1)); - //let p1 = status.path.point(point_index); // use these to strictly follow path - //let p2 = status.path.point(point_index + 1); - - let alpha = fake_gps_point - point_index; - let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - - if (go_backwards) { - fake_gps_point -= 0.05; // advance simulation - } else { - fake_gps_point += 0.05; // advance simulation - } - status.update_position(pos); - } + status.update_position(pos); + } else { + if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { + return; } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len / 2 - 1) { + return; + } + let p1 = status.path.point(2 * point_index); // use these to approximately follow path + let p2 = status.path.point(2 * (point_index + 1)); + //let p1 = status.path.point(point_index); // use these to strictly follow path + //let p2 = status.path.point(point_index + 1); - setInterval(simulate_gps, 500, status); - } else { - status.activate(); + let alpha = fake_gps_point - point_index; + let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - let frame = 0; - let set_coordinates = function(data) { - frame += 1; - // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere - let valid_coordinates = !isNaN(data.lat) && - !isNaN(data.lon) && - (data.lat != 0.0 || data.lon != 0.0); - if (valid_coordinates) { - if (status.starting_time === null) { - status.starting_time = getTime(); - Bangle.loadWidgets(); // load them even in simulation to eat mem - } - status.update_position(new Point(data.lon, data.lat)); - } - let gps_status_color; - if (frame % 2 == 0 || valid_coordinates) { - gps_status_color = g.theme.bg; - } else { - gps_status_color = g.theme.fg; - } - if (!in_menu) { - g.setColor(gps_status_color) - .setFont("6x8:2") - .drawString("gps", g.getWidth() - 40, 30); - } - }; - - Bangle.setGPSPower(true, "gipy"); - Bangle.on("GPS", set_coordinates); + if (go_backwards) { + fake_gps_point -= 0.2; // advance simulation + } else { + fake_gps_point += 0.2; // advance simulation + } + status.update_position(pos); + } } + + setInterval(simulate_gps, 500, status); + } else { + status.activate(); + + let frame = 0; + let set_coordinates = function (data) { + frame += 1; + // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere + let valid_coordinates = + !isNaN(data.lat) && + !isNaN(data.lon) && + (data.lat != 0.0 || data.lon != 0.0); + if (valid_coordinates) { + if (status.starting_time === null) { + status.starting_time = getTime(); + Bangle.loadWidgets(); // load them even in simulation to eat mem + } + status.update_position(new Point(data.lon, data.lat)); + } + let gps_status_color; + if (frame % 2 == 0 || valid_coordinates) { + gps_status_color = g.theme.bg; + } else { + gps_status_color = g.theme.fg; + } + if (!in_menu) { + g.setColor(gps_status_color) + .setFont("6x8:2") + .drawString("gps", g.getWidth() - 40, 30); + } + }; + + Bangle.setGPSPower(true, "gipy"); + Bangle.on("GPS", set_coordinates); + } } let files = s.list(".gps"); if (files.length <= 1) { - if (files.length == 0) { - load(); - } else { - start(files[0]); - } + if (files.length == 0) { + load(); + } else { + start(files[0]); + } } else { - drawMenu(); -} \ No newline at end of file + drawMenu(); +} diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts index c881052f4..3f1c8f372 100644 --- a/apps/gipy/pkg/gps.d.ts +++ b/apps/gipy/pkg/gps.d.ts @@ -12,6 +12,11 @@ export function get_gps_map_svg(gps: Gps): string; export function get_polygon(gps: Gps): Float64Array; /** * @param {Gps} gps +* @returns {boolean} +*/ +export function has_heights(gps: Gps): boolean; +/** +* @param {Gps} gps * @returns {Float64Array} */ export function get_polyline(gps: Gps): Float64Array; @@ -51,46 +56,3 @@ export function gps_from_area(xmin: number, ymin: number, xmax: number, ymax: nu export class Gps { free(): void; } - -export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; - -export interface InitOutput { - readonly memory: WebAssembly.Memory; - readonly __wbg_gps_free: (a: number) => void; - readonly get_gps_map_svg: (a: number, b: number) => void; - readonly get_polygon: (a: number, b: number) => void; - readonly get_polyline: (a: number, b: number) => void; - readonly get_gps_content: (a: number, b: number) => void; - readonly request_map: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => number; - readonly load_gps_from_string: (a: number, b: number) => number; - readonly gps_from_area: (a: number, b: number, c: number, d: number) => number; - readonly __wbindgen_malloc: (a: number) => number; - readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; - readonly __wbindgen_export_2: WebAssembly.Table; - readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1: (a: number, b: number, c: number) => void; - readonly __wbindgen_add_to_stack_pointer: (a: number) => number; - readonly __wbindgen_free: (a: number, b: number) => void; - readonly __wbindgen_exn_store: (a: number) => void; - readonly wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137: (a: number, b: number, c: number, d: number) => void; -} - -export type SyncInitInput = BufferSource | WebAssembly.Module; -/** -* Instantiates the given `module`, which can either be bytes or -* a precompiled `WebAssembly.Module`. -* -* @param {SyncInitInput} module -* -* @returns {InitOutput} -*/ -export function initSync(module: SyncInitInput): InitOutput; - -/** -* If `module_or_path` is {RequestInfo} or {URL}, makes a request and -* for everything else, calls `WebAssembly.instantiate` directly. -* -* @param {InitInput | Promise} module_or_path -* -* @returns {Promise} -*/ -export default function init (module_or_path?: InitInput | Promise): Promise; diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js index 39c2a6804..5c9bfc9bd 100644 --- a/apps/gipy/pkg/gps.js +++ b/apps/gipy/pkg/gps.js @@ -1,733 +1,2 @@ - -let wasm; - -const heap = new Array(32).fill(undefined); - -heap.push(undefined, null, true, false); - -function getObject(idx) { return heap[idx]; } - -let WASM_VECTOR_LEN = 0; - -let cachedUint8Memory0 = new Uint8Array(); - -function getUint8Memory0() { - if (cachedUint8Memory0.byteLength === 0) { - cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8Memory0; -} - -const cachedTextEncoder = new TextEncoder('utf-8'); - -const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' - ? function (arg, view) { - return cachedTextEncoder.encodeInto(arg, view); -} - : function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; -}); - -function passStringToWasm0(arg, malloc, realloc) { - - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length); - getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len); - - const mem = getUint8Memory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3); - const view = getUint8Memory0().subarray(ptr + offset, ptr + len); - const ret = encodeString(arg, view); - - offset += ret.written; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -let cachedInt32Memory0 = new Int32Array(); - -function getInt32Memory0() { - if (cachedInt32Memory0.byteLength === 0) { - cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); - } - return cachedInt32Memory0; -} - -let heap_next = heap.length; - -function dropObject(idx) { - if (idx < 36) return; - heap[idx] = heap_next; - heap_next = idx; -} - -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} - -const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - -function debugString(val) { - // primitive types - const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { - const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } - } - if (type == 'function') { - const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } - } - // objects - if (Array.isArray(val)) { - const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; - } - // Test for built-in - const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); - let className; - if (builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; - } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; -} - -function makeMutClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; - const real = (...args) => { - // First up with a closure we increment the internal reference - // count. This ensures that the Rust closure environment won't - // be deallocated while we're invoking it. - state.cnt++; - const a = state.a; - state.a = 0; - try { - return f(a, state.b, ...args); - } finally { - if (--state.cnt === 0) { - wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); - - } else { - state.a = a; - } - } - }; - real.original = state; - - return real; -} -function __wbg_adapter_24(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(arg0, arg1, addHeapObject(arg2)); -} - -function _assertClass(instance, klass) { - if (!(instance instanceof klass)) { - throw new Error(`expected instance of ${klass.name}`); - } - return instance.ptr; -} -/** -* @param {Gps} gps -* @returns {string} -*/ -export function get_gps_map_svg(gps) { - try { - const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - _assertClass(gps, Gps); - wasm.get_gps_map_svg(retptr, gps.ptr); - var r0 = getInt32Memory0()[retptr / 4 + 0]; - var r1 = getInt32Memory0()[retptr / 4 + 1]; - return getStringFromWasm0(r0, r1); - } finally { - wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_free(r0, r1); - } -} - -let cachedFloat64Memory0 = new Float64Array(); - -function getFloat64Memory0() { - if (cachedFloat64Memory0.byteLength === 0) { - cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer); - } - return cachedFloat64Memory0; -} - -function getArrayF64FromWasm0(ptr, len) { - return getFloat64Memory0().subarray(ptr / 8, ptr / 8 + len); -} -/** -* @param {Gps} gps -* @returns {Float64Array} -*/ -export function get_polygon(gps) { - try { - const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - _assertClass(gps, Gps); - wasm.get_polygon(retptr, gps.ptr); - var r0 = getInt32Memory0()[retptr / 4 + 0]; - var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v0 = getArrayF64FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 8); - return v0; - } finally { - wasm.__wbindgen_add_to_stack_pointer(16); - } -} - -/** -* @param {Gps} gps -* @returns {Float64Array} -*/ -export function get_polyline(gps) { - try { - const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - _assertClass(gps, Gps); - wasm.get_polyline(retptr, gps.ptr); - var r0 = getInt32Memory0()[retptr / 4 + 0]; - var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v0 = getArrayF64FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 8); - return v0; - } finally { - wasm.__wbindgen_add_to_stack_pointer(16); - } -} - -function getArrayU8FromWasm0(ptr, len) { - return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); -} -/** -* @param {Gps} gps -* @returns {Uint8Array} -*/ -export function get_gps_content(gps) { - try { - const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - _assertClass(gps, Gps); - wasm.get_gps_content(retptr, gps.ptr); - var r0 = getInt32Memory0()[retptr / 4 + 0]; - var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v0 = getArrayU8FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 1); - return v0; - } finally { - wasm.__wbindgen_add_to_stack_pointer(16); - } -} - -/** -* @param {Gps} gps -* @param {string} key1 -* @param {string} value1 -* @param {string} key2 -* @param {string} value2 -* @param {string} key3 -* @param {string} value3 -* @param {string} key4 -* @param {string} value4 -* @returns {Promise} -*/ -export function request_map(gps, key1, value1, key2, value2, key3, value3, key4, value4) { - _assertClass(gps, Gps); - const ptr0 = passStringToWasm0(key1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(value1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(key2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len2 = WASM_VECTOR_LEN; - const ptr3 = passStringToWasm0(value2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len3 = WASM_VECTOR_LEN; - const ptr4 = passStringToWasm0(key3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len4 = WASM_VECTOR_LEN; - const ptr5 = passStringToWasm0(value3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len5 = WASM_VECTOR_LEN; - const ptr6 = passStringToWasm0(key4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len6 = WASM_VECTOR_LEN; - const ptr7 = passStringToWasm0(value4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len7 = WASM_VECTOR_LEN; - const ret = wasm.request_map(gps.ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6, ptr7, len7); - return takeObject(ret); -} - -/** -* @param {string} input -* @returns {Gps} -*/ -export function load_gps_from_string(input) { - const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.load_gps_from_string(ptr0, len0); - return Gps.__wrap(ret); -} - -/** -* @param {number} xmin -* @param {number} ymin -* @param {number} xmax -* @param {number} ymax -* @returns {Gps} -*/ -export function gps_from_area(xmin, ymin, xmax, ymax) { - const ret = wasm.gps_from_area(xmin, ymin, xmax, ymax); - return Gps.__wrap(ret); -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - wasm.__wbindgen_exn_store(addHeapObject(e)); - } -} -function __wbg_adapter_84(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); -} - -/** -*/ -export class Gps { - - static __wrap(ptr) { - const obj = Object.create(Gps.prototype); - obj.ptr = ptr; - - return obj; - } - - __destroy_into_raw() { - const ptr = this.ptr; - this.ptr = 0; - - return ptr; - } - - free() { - const ptr = this.__destroy_into_raw(); - wasm.__wbg_gps_free(ptr); - } -} - -async function load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - - } catch (e) { - if (module.headers.get('Content-Type') != 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { - throw e; - } - } - } - - const bytes = await module.arrayBuffer(); - return await WebAssembly.instantiate(bytes, imports); - - } else { - const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - - } else { - return instance; - } - } -} - -function getImports() { - const imports = {}; - imports.wbg = {}; - imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) { - console.log(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbindgen_string_get = function(arg0, arg1) { - const obj = getObject(arg1); - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - const ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; - imports.wbg.__wbg_fetch_57429b87be3dcc33 = function(arg0) { - const ret = fetch(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - const ret = getObject(arg0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_fetch_749a56934f95c96c = function(arg0, arg1) { - const ret = getObject(arg0).fetch(getObject(arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { - const ret = getObject(arg0).signal; - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () { - const ret = new AbortController(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { - getObject(arg0).abort(); - }; - imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { - const ret = new Headers(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_append_de37df908812970d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }, arguments) }; - imports.wbg.__wbg_instanceof_Response_eaa426220848a39e = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof Response; - } catch { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_url_74285ddf2747cb3d = function(arg0, arg1) { - const ret = getObject(arg1).url; - const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_status_c4ef3dd591e63435 = function(arg0) { - const ret = getObject(arg0).status; - return ret; - }; - imports.wbg.__wbg_headers_fd64ad685cf22e5d = function(arg0) { - const ret = getObject(arg0).headers; - return addHeapObject(ret); - }; - imports.wbg.__wbg_text_1169d752cc697903 = function() { return handleError(function (arg0) { - const ret = getObject(arg0).text(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_new_abda76e883ba8a5f = function() { - const ret = new Error(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { - const ret = getObject(arg1).stack; - const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { - try { - console.error(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_free(arg0, arg1); - } - }; - imports.wbg.__wbindgen_cb_drop = function(arg0) { - const obj = takeObject(arg0).original; - if (obj.cnt-- == 1) { - obj.a = 0; - return true; - } - const ret = false; - return ret; - }; - imports.wbg.__wbindgen_is_object = function(arg0) { - const val = getObject(arg0); - const ret = typeof(val) === 'object' && val !== null; - return ret; - }; - imports.wbg.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) { - const ret = new Function(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_next_579e583d33566a86 = function(arg0) { - const ret = getObject(arg0).next; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_is_function = function(arg0) { - const ret = typeof(getObject(arg0)) === 'function'; - return ret; - }; - imports.wbg.__wbg_value_1ccc36bc03462d71 = function(arg0) { - const ret = getObject(arg0).value; - return addHeapObject(ret); - }; - imports.wbg.__wbg_iterator_6f9d4f28845f426c = function() { - const ret = Symbol.iterator; - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_0b9bfdd97583284e = function() { - const ret = new Object(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () { - const ret = self.self; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_window_f2557cc78490aceb = function() { return handleError(function () { - const ret = window.window; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () { - const ret = globalThis.globalThis; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () { - const ret = global.global; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbindgen_is_undefined = function(arg0) { - const ret = getObject(arg0) === undefined; - return ret; - }; - imports.wbg.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg0).call(getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_next_aaef7c8aa5e212ac = function() { return handleError(function (arg0) { - const ret = getObject(arg0).next(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_done_1b73b0672e15f234 = function(arg0) { - const ret = getObject(arg0).done; - return ret; - }; - imports.wbg.__wbg_new_9962f939219f1820 = function(arg0, arg1) { - try { - var state0 = {a: arg0, b: arg1}; - var cb0 = (arg0, arg1) => { - const a = state0.a; - state0.a = 0; - try { - return __wbg_adapter_84(a, state0.b, arg0, arg1); - } finally { - state0.a = a; - } - }; - const ret = new Promise(cb0); - return addHeapObject(ret); - } finally { - state0.a = state0.b = 0; - } - }; - imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) { - const ret = Promise.resolve(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) { - const ret = getObject(arg0).then(getObject(arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) { - const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) { - const ret = getObject(arg0).buffer; - return addHeapObject(ret); - }; - imports.wbg.__wbg_newwithbyteoffsetandlength_d9aa266703cb98be = function(arg0, arg1, arg2) { - const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) { - const ret = new Uint8Array(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_get_765201544a2b6869 = function() { return handleError(function (arg0, arg1) { - const ret = Reflect.get(getObject(arg0), getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_has_8359f114ce042f5a = function() { return handleError(function (arg0, arg1) { - const ret = Reflect.has(getObject(arg0), getObject(arg1)); - return ret; - }, arguments) }; - imports.wbg.__wbg_set_bf3f89b92d5a34bf = function() { return handleError(function (arg0, arg1, arg2) { - const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); - return ret; - }, arguments) }; - imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) { - const ret = JSON.stringify(getObject(arg0)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { - const ret = debugString(getObject(arg1)); - const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbindgen_throw = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbindgen_memory = function() { - const ret = wasm.memory; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper2245 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 267, __wbg_adapter_24); - return addHeapObject(ret); - }; - - return imports; -} - -function initMemory(imports, maybe_memory) { - -} - -function finalizeInit(instance, module) { - wasm = instance.exports; - init.__wbindgen_wasm_module = module; - cachedFloat64Memory0 = new Float64Array(); - cachedInt32Memory0 = new Int32Array(); - cachedUint8Memory0 = new Uint8Array(); - - - return wasm; -} - -function initSync(module) { - const imports = getImports(); - - initMemory(imports); - - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - - const instance = new WebAssembly.Instance(module, imports); - - return finalizeInit(instance, module); -} - -async function init(input) { - if (typeof input === 'undefined') { - input = new URL('gps_bg.wasm', import.meta.url); - } - const imports = getImports(); - - if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { - input = fetch(input); - } - - initMemory(imports); - - const { instance, module } = await load(await input, imports); - - return finalizeInit(instance, module); -} - -export { initSync } -export default init; +import * as wasm from "./gps_bg.wasm"; +export * from "./gps_bg.js"; \ No newline at end of file diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm index 8e0fbc07eb7d7cf8a96dd646305f808eda4e49cc..bbc9ce6a7449547ed257a72d61207453d2938894 100644 GIT binary patch delta 266102 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

TJG^>?hdH_6c@S?gTq2z>y|_+OqblLj;B} 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≧ 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 HW$^z40Wg=I delta 269785 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 LH>yJtsZW|`>zn~@3T*DPr*(nRfOHn23 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 diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts index b4303ee30..3b95ada78 100644 --- a/apps/gipy/pkg/gps_bg.wasm.d.ts +++ b/apps/gipy/pkg/gps_bg.wasm.d.ts @@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory; export function __wbg_gps_free(a: number): void; export function get_gps_map_svg(a: number, b: number): void; export function get_polygon(a: number, b: number): void; +export function has_heights(a: number): number; export function get_polyline(a: number, b: number): void; export function get_gps_content(a: number, b: number): void; export function request_map(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number): number; @@ -12,8 +13,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe export function __wbindgen_malloc(a: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_exn_store(a: number): void; -export function wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(a: number, b: number, c: number, d: number): void; +export function wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(a: number, b: number, c: number, d: number): void; diff --git a/apps/gipy/pkg/package.json b/apps/gipy/pkg/package.json index 0f6ae6a75..57fbc3352 100644 --- a/apps/gipy/pkg/package.json +++ b/apps/gipy/pkg/package.json @@ -4,6 +4,7 @@ "files": [ "gps_bg.wasm", "gps.js", + "gps_bg.js", "gps.d.ts" ], "module": "gps.js", From b9dc5a11ced52b20c1caa9b81a18632933701028 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 22 Aug 2023 14:50:37 +0200 Subject: [PATCH 209/242] gipy: wasm fix --- apps/gipy/ChangeLog | 1 + apps/gipy/pkg/gps.d.ts | 44 +++ apps/gipy/pkg/gps.js | 745 ++++++++++++++++++++++++++++++++++++- apps/gipy/pkg/gps_bg.wasm | Bin 743877 -> 743453 bytes apps/gipy/pkg/package.json | 1 - 5 files changed, 788 insertions(+), 3 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 3ca699a4f..ac67eafda 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -101,3 +101,4 @@ 0.21: * Jit is back for display functions (10% speed increase) + * Store, parse and display elevation data diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts index 3f1c8f372..e4644f74f 100644 --- a/apps/gipy/pkg/gps.d.ts +++ b/apps/gipy/pkg/gps.d.ts @@ -56,3 +56,47 @@ export function gps_from_area(xmin: number, ymin: number, xmax: number, ymax: nu export class Gps { free(): void; } + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_gps_free: (a: number) => void; + readonly get_gps_map_svg: (a: number, b: number) => void; + readonly get_polygon: (a: number, b: number) => void; + readonly has_heights: (a: number) => number; + readonly get_polyline: (a: number, b: number) => void; + readonly get_gps_content: (a: number, b: number) => void; + readonly request_map: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => number; + readonly load_gps_from_string: (a: number, b: number) => number; + readonly gps_from_area: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_malloc: (a: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; + readonly __wbindgen_export_2: WebAssembly.Table; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7: (a: number, b: number, c: number) => void; + readonly __wbindgen_add_to_stack_pointer: (a: number) => number; + readonly __wbindgen_free: (a: number, b: number) => void; + readonly __wbindgen_exn_store: (a: number) => void; + readonly wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027: (a: number, b: number, c: number, d: number) => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {SyncInitInput} module +* +* @returns {InitOutput} +*/ +export function initSync(module: SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {InitInput | Promise} module_or_path +* +* @returns {Promise} +*/ +export default function init (module_or_path?: InitInput | Promise): Promise; diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js index 5c9bfc9bd..563bf6251 100644 --- a/apps/gipy/pkg/gps.js +++ b/apps/gipy/pkg/gps.js @@ -1,2 +1,743 @@ -import * as wasm from "./gps_bg.wasm"; -export * from "./gps_bg.js"; \ No newline at end of file + +let wasm; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let WASM_VECTOR_LEN = 0; + +let cachedUint8Memory0 = new Uint8Array(); + +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +const cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = new Int32Array(); + +function getInt32Memory0() { + if (cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + + } else { + state.a = a; + } + } + }; + real.original = state; + + return real; +} +function __wbg_adapter_24(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(arg0, arg1, addHeapObject(arg2)); +} + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } + return instance.ptr; +} +/** +* @param {Gps} gps +* @returns {string} +*/ +export function get_gps_map_svg(gps) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(gps, Gps); + wasm.get_gps_map_svg(retptr, gps.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } +} + +let cachedFloat64Memory0 = new Float64Array(); + +function getFloat64Memory0() { + if (cachedFloat64Memory0.byteLength === 0) { + cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer); + } + return cachedFloat64Memory0; +} + +function getArrayF64FromWasm0(ptr, len) { + return getFloat64Memory0().subarray(ptr / 8, ptr / 8 + len); +} +/** +* @param {Gps} gps +* @returns {Float64Array} +*/ +export function get_polygon(gps) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(gps, Gps); + wasm.get_polygon(retptr, gps.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayF64FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 8); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** +* @param {Gps} gps +* @returns {boolean} +*/ +export function has_heights(gps) { + _assertClass(gps, Gps); + const ret = wasm.has_heights(gps.ptr); + return ret !== 0; +} + +/** +* @param {Gps} gps +* @returns {Float64Array} +*/ +export function get_polyline(gps) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(gps, Gps); + wasm.get_polyline(retptr, gps.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayF64FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 8); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} +/** +* @param {Gps} gps +* @returns {Uint8Array} +*/ +export function get_gps_content(gps) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(gps, Gps); + wasm.get_gps_content(retptr, gps.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** +* @param {Gps} gps +* @param {string} key1 +* @param {string} value1 +* @param {string} key2 +* @param {string} value2 +* @param {string} key3 +* @param {string} value3 +* @param {string} key4 +* @param {string} value4 +* @returns {Promise} +*/ +export function request_map(gps, key1, value1, key2, value2, key3, value3, key4, value4) { + _assertClass(gps, Gps); + const ptr0 = passStringToWasm0(key1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(value1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passStringToWasm0(key2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len2 = WASM_VECTOR_LEN; + const ptr3 = passStringToWasm0(value2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len3 = WASM_VECTOR_LEN; + const ptr4 = passStringToWasm0(key3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len4 = WASM_VECTOR_LEN; + const ptr5 = passStringToWasm0(value3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len5 = WASM_VECTOR_LEN; + const ptr6 = passStringToWasm0(key4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len6 = WASM_VECTOR_LEN; + const ptr7 = passStringToWasm0(value4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len7 = WASM_VECTOR_LEN; + const ret = wasm.request_map(gps.ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6, ptr7, len7); + return takeObject(ret); +} + +/** +* @param {string} input +* @returns {Gps} +*/ +export function load_gps_from_string(input) { + const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.load_gps_from_string(ptr0, len0); + return Gps.__wrap(ret); +} + +/** +* @param {number} xmin +* @param {number} ymin +* @param {number} xmax +* @param {number} ymax +* @returns {Gps} +*/ +export function gps_from_area(xmin, ymin, xmax, ymax) { + const ret = wasm.gps_from_area(xmin, ymin, xmax, ymax); + return Gps.__wrap(ret); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +function __wbg_adapter_85(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +} + +/** +*/ +export class Gps { + + static __wrap(ptr) { + const obj = Object.create(Gps.prototype); + obj.ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_gps_free(ptr); + } +} + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function getImports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbg_fetch_57429b87be3dcc33 = function(arg0) { + const ret = fetch(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_fetch_749a56934f95c96c = function(arg0, arg1) { + const ret = getObject(arg0).fetch(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { + const ret = getObject(arg0).signal; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () { + const ret = new AbortController(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { + getObject(arg0).abort(); + }; + imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { + const ret = new Headers(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_append_de37df908812970d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbg_instanceof_Response_eaa426220848a39e = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Response; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_url_74285ddf2747cb3d = function(arg0, arg1) { + const ret = getObject(arg1).url; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_status_c4ef3dd591e63435 = function(arg0) { + const ret = getObject(arg0).status; + return ret; + }; + imports.wbg.__wbg_headers_fd64ad685cf22e5d = function(arg0) { + const ret = getObject(arg0).headers; + return addHeapObject(ret); + }; + imports.wbg.__wbg_text_1169d752cc697903 = function() { return handleError(function (arg0) { + const ret = getObject(arg0).text(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_new_abda76e883ba8a5f = function() { + const ret = new Error(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbindgen_is_object = function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_next_579e583d33566a86 = function(arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }; + imports.wbg.__wbg_value_1ccc36bc03462d71 = function(arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); + }; + imports.wbg.__wbg_iterator_6f9d4f28845f426c = function() { + const ret = Symbol.iterator; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_0b9bfdd97583284e = function() { + const ret = new Object(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () { + const ret = self.self; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_window_f2557cc78490aceb = function() { return handleError(function () { + const ret = window.window; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () { + const ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () { + const ret = global.global; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_next_aaef7c8aa5e212ac = function() { return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_done_1b73b0672e15f234 = function(arg0) { + const ret = getObject(arg0).done; + return ret; + }; + imports.wbg.__wbg_new_9962f939219f1820 = function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_85(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) { + const ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) { + const ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) { + const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }; + imports.wbg.__wbg_newwithbyteoffsetandlength_d9aa266703cb98be = function(arg0, arg1, arg2) { + const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_get_765201544a2b6869 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_has_8359f114ce042f5a = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.has(getObject(arg0), getObject(arg1)); + return ret; + }, arguments) }; + imports.wbg.__wbg_set_bf3f89b92d5a34bf = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments) }; + imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) { + const ret = JSON.stringify(getObject(arg0)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper2214 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 268, __wbg_adapter_24); + return addHeapObject(ret); + }; + + return imports; +} + +function initMemory(imports, maybe_memory) { + +} + +function finalizeInit(instance, module) { + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + cachedFloat64Memory0 = new Float64Array(); + cachedInt32Memory0 = new Int32Array(); + cachedUint8Memory0 = new Uint8Array(); + + + return wasm; +} + +function initSync(module) { + const imports = getImports(); + + initMemory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return finalizeInit(instance, module); +} + +async function init(input) { + if (typeof input === 'undefined') { + input = new URL('gps_bg.wasm', import.meta.url); + } + const imports = getImports(); + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + initMemory(imports); + + const { instance, module } = await load(await input, imports); + + return finalizeInit(instance, module); +} + +export { initSync } +export default init; diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm index bbc9ce6a7449547ed257a72d61207453d2938894..7a42fb564e0b20e7bbc6645e7ef0725ff06f8bec 100644 GIT binary patch delta 906 zcmZ8f%Zk)M6z%DBC*2W5#TTRLhvN&RC6B63Wh3rnKonPko1&iSXeN>FG>vN)et=6I z5Ix9Zf!1BTc?qo+iJyno-A8_B%$i>`b)gXxsX} zZCTy1Zu#ATCeOOwt$Nl~?Q&5O_5x2K3gbiy03ZmMoEo1;-65*A=Nc9m#e81A)Hgrh zXk-hl?HN;^y{9KWv6u3Mfqm*yoUIi@6r4EkI-ix7~&9RKF*{#{Ut8Od{;#6%<)i0jYAPY8AiYuI_#M}o%F`dGu2GJmLQNZfq+kA3aBL9 z%<=kLNBym%KKMTk#3JeYFbw0602mW9?`Pj@DraPYQ3Mki1b_)2gc1@n<*sXP_SD^b zQpa_vu^BGtGBvMRKZ7%kZZ(^iD_uU@C?AM}a{fx^`g-lYS-U$5Z3RoAqtI2b6&!`0 OLSMoCymn9hc=QJjUK!~C delta 1363 zcmZ`(J#W)c6m{doPTCHXPpLpcp$k+gv7h~HZ|q1wCRotJ@53goYge{IKp=HMf{7_3 zs>F_jDlsqu0}BH23m8~f`3c}89^QQ_!&%Nd@A%$x@AKhN|I7RS(T&Ez*EM%#`+B@z z5E^gq6lVyT&{4``k&>d!M`=uAQEHm3S)XZQl4LRA0|z;Xx;_=YO$X62!rCmOJA##j z=h;5VMWpEq$7}!nY$_(&lKk1}=SC09#CfUK+MXp@D(3!%tehHpZxs+%^({!;(=S2d znSK!xhpNXXRq;#pXf^vF-8#8(o)FKmeV6md_8gB<#9?yNyG^B>r_Bvm&YNcUf!tE< zUByO1%0mwe-$yj`LpRcvW#NUz_=+-Qy9B$w?FEr=9M`c}gq1RkR_~5{_oNg#qw{HM zAnDVRU{sLBSfFHjzjft6Hl5EgCaHK_64wia>m!bki*e{mq9+_|9Rxc)c6Z9O5W%7=M2fyZmo@t)Yr@+a_J nbft5aJRf#m4m(Q~8Wr>k%?hmwMg_A%yTVMf{9)Kx`F86cBxtWp diff --git a/apps/gipy/pkg/package.json b/apps/gipy/pkg/package.json index 57fbc3352..0f6ae6a75 100644 --- a/apps/gipy/pkg/package.json +++ b/apps/gipy/pkg/package.json @@ -4,7 +4,6 @@ "files": [ "gps_bg.wasm", "gps.js", - "gps_bg.js", "gps.d.ts" ], "module": "gps.js", From e6f30b9dc0df975b188f93792786b1840d889e10 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 22 Aug 2023 16:52:28 +0200 Subject: [PATCH 210/242] gipy: removed 'lost' message --- apps/gipy/ChangeLog | 1 + apps/gipy/README.md | 5 ++--- apps/gipy/TODO | 2 -- apps/gipy/app.js | 17 ++++++++++------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index ac67eafda..a5384c0c8 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -102,3 +102,4 @@ 0.21: * Jit is back for display functions (10% speed increase) * Store, parse and display elevation data + * Removed 'lost' indicator (we now change position to purple when lost) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index ac65f8c3f..dc260fd70 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -79,17 +79,16 @@ On your screen you can see: * green: artwork - a *turn* indicator on the top right when you reach a turning point - a *gps* indicator (blinking) on the top right if you lose gps signal -- a *lost* indicator on the top right if you stray too far away from path ### Lost If you stray away from path we will rescale the display to continue displaying nearby segments and -display the direction to follow as a purple segment. +display the direction to follow as a purple segment. Your main position will also turn to purple. Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you are. On path it just needed to scan a few points ahead and behind. -The distance to next point displayed corresponds to the length of the black segment. +The distance to next point displayed corresponds to the length of the purple segment. ### Menu diff --git a/apps/gipy/TODO b/apps/gipy/TODO index ef1df3dc5..b642b5cf6 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -45,8 +45,6 @@ JIT: array declaration in jit is buggy + when lost we still get powersaving + try disabling gps for more powersaving -+ remove "lost" indicator and change position point's color instead - + when you walk the direction still has a tendency to shift + put back foot only ways diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 70c1aafd3..2aec35429 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1126,7 +1126,11 @@ class Status { previous_y = y; } } - g.setColor(0, 0, 0); + if (this.on_path) { + g.setColor(0, 0, 0); + } else { + g.setColor(1, 0, 1); + } g.fillCircle(current_x, current_y, 5); // display min dist/max dist and min height/max height @@ -1267,11 +1271,6 @@ class Status { .drawString("turn", g.getWidth() - 50, 30); } } - if (!this.on_path) { - g.setColor(1.0, 0.0, 0.0) - .setFont("6x15") - .drawString("lost", g.getWidth() - 55, 35); - } } display_path() { // don't display all segments, only those neighbouring current segment @@ -1329,7 +1328,11 @@ class Status { } // now display ourselves - g.setColor(0, 0, 0); + if (this.on_path) { + g.setColor(0, 0, 0); + } else { + g.setColor(1, 0, 1); + } g.fillCircle(half_width, half_height, 5); } } From 7baa0d21739caeb79a29fec4a31e3ce8069ecbdf Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 22 Aug 2023 17:05:41 +0200 Subject: [PATCH 211/242] gipy: small fixes --- apps/gipy/ChangeLog | 2 ++ apps/gipy/TODO | 2 -- apps/gipy/app.js | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index a5384c0c8..73164dbd3 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -103,3 +103,5 @@ * Jit is back for display functions (10% speed increase) * Store, parse and display elevation data * Removed 'lost' indicator (we now change position to purple when lost) + * Powersaving fix : don't powersave when lost + * Bugfix for negative remaining distance when going backwards diff --git a/apps/gipy/TODO b/apps/gipy/TODO index b642b5cf6..8c767c463 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -41,8 +41,6 @@ JIT: array declaration in jit is buggy ************************** -+ there is still a bug with negative remaining distances (when lost and nearest point is endpoint ?) -+ when lost we still get powersaving + try disabling gps for more powersaving + when you walk the direction still has a tendency to shift diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 2aec35429..0ccdff36b 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -847,7 +847,6 @@ class Status { // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); if (this.on_path == lost) { - this.activate(); // if status changes if (lost) { Bangle.buzz(); // we lost path @@ -857,6 +856,9 @@ class Status { } this.on_path = !lost; } + if (!this.on_path) { + this.activate(); + } this.current_segment = next_segment; @@ -959,14 +961,12 @@ class Status { ]); } remaining_distance() { - let remaining_in_correct_orientation = - this.remaining_distances[this.current_segment + 1] + - this.position.distance(this.path.point(this.current_segment + 1)); - if (go_backwards) { - return this.remaining_distances[0] - remaining_in_correct_orientation; + return this.remaining_distances[0] - this.remaining_distances[this.current_segment] + + this.position.distance(this.path.point(this.current_segment)); } else { - return remaining_in_correct_orientation; + return this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)); } } // check if we are lost (too far from segment we think we are on) From 822ca78504924d9f1c9ba418ca5c9691c9b914a0 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 22 Aug 2023 16:00:50 -0500 Subject: [PATCH 212/242] Fix field name for movementPerDay() --- apps/health/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/health/app.js b/apps/health/app.js index fdc69dd28..eea0a3cce 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -103,8 +103,8 @@ function movementPerDay() { var data = new Uint16Array(31); var cnt = new Uint8Array(31); require("health").readDailySummaries(new Date(), h=>{ - data[h.hr]+=h.movement - cnt[h.hr]++; + data[h.day]+=h.movement + cnt[h.day]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); setButton(menuMovement); From e7ad8097323f4720c57439317be88eecb803d3a7 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 22 Aug 2023 16:38:06 -0500 Subject: [PATCH 213/242] Add missing semicolons (fix warnings in IDE) --- apps/health/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/health/app.js b/apps/health/app.js index eea0a3cce..3b615ff1d 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -89,7 +89,7 @@ function movementPerHour() { var data = new Uint16Array(24); var cnt = new Uint8Array(24); require("health").readDay(new Date(), h=>{ - data[h.hr]+=h.movement + data[h.hr]+=h.movement; cnt[h.hr]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); @@ -103,7 +103,7 @@ function movementPerDay() { var data = new Uint16Array(31); var cnt = new Uint8Array(31); require("health").readDailySummaries(new Date(), h=>{ - data[h.day]+=h.movement + data[h.day]+=h.movement; cnt[h.day]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); From fde449c8c0ef863c15b2565d6360c0f6d939cdd9 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 22 Aug 2023 16:39:35 -0500 Subject: [PATCH 214/242] Bump version number --- apps/health/ChangeLog | 1 + apps/health/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 6bc15be83..489715931 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -27,3 +27,4 @@ movement graph in app is now an average, not sum fix 11pm slot for daily HRM 0.26: Implement API for activity fetching +0.27: Fix typo in daily summary graph code causing graph not to load diff --git a/apps/health/metadata.json b/apps/health/metadata.json index 10c8268cb..10a146bdd 100644 --- a/apps/health/metadata.json +++ b/apps/health/metadata.json @@ -2,7 +2,7 @@ "id": "health", "name": "Health Tracking", "shortName": "Health", - "version": "0.26", + "version": "0.27", "description": "Logs health data and provides an app to view it", "icon": "app.png", "tags": "tool,system,health", From 463bba49968102907baf231bd7a084bf688605d5 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 23 Aug 2023 08:00:05 +0200 Subject: [PATCH 215/242] gipy: min height --- apps/gipy/app.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 0ccdff36b..674771293 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1043,6 +1043,12 @@ class Status { max_height = Math.max(max_height, height); min_height = Math.min(min_height, height); } + // we'll set the displayed height to a minimum value of 100m + // if we don't, then we'll see too much noise + if (max_height - min_height < 100) { + min_height = min_height - 10; + max_height = min_height + 110; + } let displayed_height = max_height - min_height; let height_per_pixel = displayed_height / graph_height; From b8e29f397dd7ced3ab9b544f8c5e4cab9e5d2db1 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 23 Aug 2023 08:29:21 +0200 Subject: [PATCH 216/242] gipy: doc change --- apps/gipy/README.md | 4 ++++ apps/gipy/app.js | 2 +- apps/gipy/heights.png | Bin 0 -> 3627 bytes 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 apps/gipy/heights.png diff --git a/apps/gipy/README.md b/apps/gipy/README.md index dc260fd70..0df008b38 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -101,6 +101,10 @@ If you touch the screen you will switch between display modes. The first one displays the map, the second one the nearby elevation and the last one the elevation for the whole path. +![Screenshot](heights.png) + +Colors correspond to slopes. + ### Settings Few settings for now (feel free to suggest me more) : diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 674771293..4fa51f779 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1082,7 +1082,7 @@ class Status { if (steepness > 0.15) { color = "#ff0000"; } else if (steepness > 0.8) { - color = "#aa0000"; + color = "#ff8000"; } else if (steepness > 0.03) { color = "#ffff00"; } else if (steepness > -0.03) { diff --git a/apps/gipy/heights.png b/apps/gipy/heights.png new file mode 100644 index 0000000000000000000000000000000000000000..07f82511b7d9f731f2cd059123feeb9af62ecc17 GIT binary patch literal 3627 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 Ezcg}uFaQ7m literal 0 HcmV?d00001 From cf0695c3eb5a39fc7ecd5fb77ab909486e1f760d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 24 Aug 2023 10:01:50 +0200 Subject: [PATCH 217/242] gipy : fix for backwards position in heights --- apps/gipy/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 4fa51f779..56137ae58 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -996,8 +996,12 @@ class Status { } else { let current_position = 0; if (this.current_segment !== null) { + if (go_backwards) { + current_position = this.remaining_distance(); + } else { current_position = this.remaining_distances[0] - this.remaining_distance(); + } } if (this.screen == HEIGHTS_FULL) { this.display_heights(0, current_position, this.remaining_distances[0]); From 52ee0825c5a7d3d04aa7d3e1c164888d9df095e4 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 24 Aug 2023 10:56:59 +0200 Subject: [PATCH 218/242] sokoban: small fixes --- apps/sokoban/ChangeLog | 4 ++++ apps/sokoban/app.js | 17 ++++++++++++----- apps/sokoban/metadata.json | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/sokoban/ChangeLog b/apps/sokoban/ChangeLog index 9fa2c8172..f931ec63e 100644 --- a/apps/sokoban/ChangeLog +++ b/apps/sokoban/ChangeLog @@ -1 +1,5 @@ 0.01: Initial code +0.02: + * Fix for last level offsets parsing + * Fix for title display + diff --git a/apps/sokoban/app.js b/apps/sokoban/app.js index 890156214..3915556e3 100644 --- a/apps/sokoban/app.js +++ b/apps/sokoban/app.js @@ -56,6 +56,9 @@ function next_map_offsets(filename, start_offset) { } } } + if (offsets.length == 1) { + offsets.push(raw_maps.length); + } return offsets; } @@ -88,7 +91,7 @@ function load_current_map() { let current_set = config.levels_set; let offsets = config.offsets[current_set]; let set_filename = config.levels_sets[current_set]; - let set_name = set_filename.substring(0, set_filename.length - 4); // remove '.txt' + let set_name = set_filename.substring(8, set_filename.length - 4); // remove '.txt' and 'sokoban.' let current_map = config.current_maps[current_set]; map = load_map(set_filename, offsets[2 * current_map], offsets[2 * current_map + 1], set_name + " " + (current_map + 1)); map.display(); @@ -98,10 +101,12 @@ function next_map() { let current_set = config.levels_set; let current_map = config.current_maps[current_set]; let offsets = config.offsets[current_set]; + let won = false; if (2 * (current_map + 1) >= offsets.length) { // we parse some new offsets let new_offsets = next_map_offsets(config.levels_sets[current_set], offsets[offsets.length - 1] + 2); // +2 since we need to start at ';' (we did -2 from ';' in previous parser call) - if (new_offsets.length == 0) { + if (new_offsets.length != 2) { + won = true; E.showAlert("You Win", "All levels completed").then(function() { load(); }); @@ -110,9 +115,11 @@ function next_map() { config.offsets[current_set].push(new_offsets[1]); } } - config.current_maps[current_set]++; - s.writeJSON("sokoban.json", config); - load_current_map(); + if (!won) { + config.current_maps[current_set]++; + s.writeJSON("sokoban.json", config); + load_current_map(); + } } function previous_map() { diff --git a/apps/sokoban/metadata.json b/apps/sokoban/metadata.json index ef4a45f83..752c17e75 100644 --- a/apps/sokoban/metadata.json +++ b/apps/sokoban/metadata.json @@ -2,7 +2,7 @@ "id": "sokoban", "name": "Sokoban", "shortName": "Sokoban", - "version": "0.01", + "version": "0.02", "description": "Classic Sokoban game (microban levels).", "allow_emulator":false, "icon": "sokoban.png", From f95e1508872efb78effa55324fc3ea1c2b3ee6f5 Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:35:03 -0400 Subject: [PATCH 219/242] Create app.js Added the code for the app. --- apps/bblobface/app.js | 768 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 768 insertions(+) create mode 100644 apps/bblobface/app.js diff --git a/apps/bblobface/app.js b/apps/bblobface/app.js new file mode 100644 index 000000000..579a6bbb4 --- /dev/null +++ b/apps/bblobface/app.js @@ -0,0 +1,768 @@ +{ + // ~~ Variables for clock ~~ + let clockDrawTimeout; + let twelveHourTime = require('Storage').readJSON('setting.json', 1)['12hour']; + let updateSeconds = !Bangle.isLocked(); + let batteryLevel = E.getBattery(); + + // ~~ Variables for game logic ~~ + const NUM_COLORS = 6; + const NUISANCE_COLOR = 7; + let grid = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + let hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]); + let nextQueue = [{pivot: 1, leaf: 1}, {pivot: 1, leaf: 1}]; + let currentPair = {pivot: 0, leaf: 0}; + let dropCoordinates = {pivotX: 2, pivotY: 11, leafX: 2, leafY: 10}; + let pairX = 2; + let pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left + let slotsToCheck = []; + let selectedColors; + let lastChain = 0; + let gameLost = false; + let gamePaused = false; + let midChain = false; + + /* + Sets up a new game. + Must be called once before the first round. + */ + let restartGame = function() { + grid = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]); + currentPair = {pivot: 0, leaf: 0}; + pairX = 2; + pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left + slotsToCheck = []; + gameLost = false; + lastChain = 0; + + //Set up random colors + selectedColors = new Uint8Array([1, 2, 3, 4, 5, 6]); + for (let i = NUM_COLORS - 1; i > 0; i--) { + let swap = selectedColors[i]; + let swapIndex = Math.floor(Math.random() * (i + 1)); + selectedColors[i] = selectedColors[swapIndex]; + selectedColors[swapIndex] = swap; + } + + //Create the first two pairs (Always in the first three colors) + nextQueue[0].pivot = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[0].leaf = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 3)]; + }; + + /* + Readies the next pair and generates a new one for the queue. + */ + let newPair = function() { + currentPair.pivot = nextQueue[0].pivot; + currentPair.leaf = nextQueue[0].leaf; + + nextQueue[0].pivot = nextQueue[1].pivot; + nextQueue[0].leaf = nextQueue[1].leaf; + + nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 4)]; + nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 4)]; + + pairX = 2; + pairOrientation = 0; + + calcDropCoordinates(); + }; + + /* + Calculates the coordinates at which the current pair will be placed when quick dropped. + */ + let calcDropCoordinates = function() { + dropCoordinates.pivotX = pairX; + + //Find Y coordinate of pivot + dropCoordinates.pivotY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX] == 0) { + dropCoordinates.pivotY = i; + break; + } + } + if (dropCoordinates.pivotY == -2 && hiddenRow[pairX] == 0) + dropCoordinates.pivotY = -1; + + //Find coordinates of leaf + if (pairOrientation == 1) { + dropCoordinates.leafX = pairX + 1; + + dropCoordinates.leafY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX + 1] == 0) { + dropCoordinates.leafY = i; + break; + } + } + if (dropCoordinates.leafY == -2 && hiddenRow[pairX + 1] == 0) + dropCoordinates.leafY = -1; + } else if (pairOrientation == 3) { + dropCoordinates.leafX = pairX - 1; + + dropCoordinates.leafY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX - 1] == 0) { + dropCoordinates.leafY = i; + break; + } + } + if (dropCoordinates.leafY == -2 && hiddenRow[pairX - 1] == 0) + dropCoordinates.leafY = -1; + } else if (pairOrientation == 2) { + dropCoordinates.leafX = pairX; + dropCoordinates.leafY = dropCoordinates.pivotY; + dropCoordinates.pivotY--; + } else { + dropCoordinates.leafX = pairX; + dropCoordinates.leafY = dropCoordinates.pivotY - 1; + } + }; + + /* + Moves the current pair a certain number of slots. + */ + let movePair = function(dx) { + pairX += dx; + + if (dx < 0) { + if (pairX < (pairOrientation == 3 ? 1 : 0)) + pairX = (pairOrientation == 3 ? 1 : 0); + } + if (dx > 0) { + if (pairX > (pairOrientation == 1 ? 4 : 5)) + pairX = (pairOrientation == 1 ? 4 : 5); + } + + calcDropCoordinates(); + }; + + /* + Rotates the pair in the given direction around the pivot. + */ + let rotatePair = function(clockwise) { + pairOrientation += (clockwise ? 1 : -1); + if (pairOrientation > 3) + pairOrientation = 0; + if (pairOrientation < 0) + pairOrientation = 3; + + if (pairOrientation == 1 && pairX == 5) + pairX = 4; + if (pairOrientation == 3 && pairX == 0) + pairX = 1; + + calcDropCoordinates(); + }; + + /* + Places the current pair at the drop coordinates. + */ + let quickDrop = function() { + if (dropCoordinates.pivotY == -1) { + hiddenRow[dropCoordinates.pivotX] = currentPair.pivot; + } else if (dropCoordinates.pivotY > -1) { + grid[dropCoordinates.pivotY][dropCoordinates.pivotX] = currentPair.pivot; + } + + if (dropCoordinates.leafY == -1) { + hiddenRow[dropCoordinates.leafX] = currentPair.leaf; + } else if (dropCoordinates.leafY > -1) { + grid[dropCoordinates.leafY][dropCoordinates.leafX] = currentPair.leaf; + } + + currentPair.pivot = 0; + currentPair.leaf = 0; + }; + + /* + Makes all blobs fall to the lowest available slot. + All blobs that fall will be added to slotsToCheck. + */ + let settleBlobs = function() { + for (let x = 0; x < 6; x++) { + let lowestOpen = 11; + for (let y = 11; y >= 0; y--) { + if (grid[y][x] != 0) { + if (y != lowestOpen) { + grid[lowestOpen][x] = grid[y][x]; + grid[y][x] = 0; + addSlotToCheck(x, lowestOpen); + } + lowestOpen--; + } + } + + if (lowestOpen >= 0 && hiddenRow[x] != 0) { + grid[lowestOpen][x] = hiddenRow[x]; + hiddenRow[x] = 0; + addSlotToCheck(x, lowestOpen); + } + } + }; + + /* + Adds a slot to slotsToCheck. This slot will be checked for a pop + next time popAll is called. + */ + let addSlotToCheck = function(x, y) { + slotsToCheck.push({x: x, y: y}); + }; + + /* + Checks for a pop at every slot in slotsToCheck. + Pops at all locations. + */ + let popAll = function() { + let result = {pops: 0}; + while(slotsToCheck.length > 0) { + let coord = slotsToCheck.pop(); + if (grid[coord.y][coord.x] != 0 && grid[coord.y][coord.x] != NUISANCE_COLOR) { + if (checkSlotForPop(coord.x, coord.y)) + result.pops += 1; + } + } + return result; + }; + + /* + Checks a specific slot for a pop. + If there are four or more adjacent blobs of the same color, they are removed. + */ + let checkSlotForPop = function(x, y) { + let toDelete = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + let blobsInClump = 0; + let color = grid[y][x]; + let toCheck = [{x: x, y: y}]; + + //Count every blob in this clump + while (toCheck.length > 0) { + let coord = toCheck.pop(); + if (grid[coord.y][coord.x] == color && toDelete[coord.y][coord.x] == 0) { + blobsInClump++; + toDelete[coord.y][coord.x] = 1; + if (coord.x > 0) toCheck.push({x: coord.x - 1, y: coord.y}); + if (coord.x < 5) toCheck.push({x: coord.x + 1, y: coord.y}); + if (coord.y > 0) toCheck.push({x: coord.x, y: coord.y - 1}); + if (coord.y < 11) toCheck.push({x: coord.x, y: coord.y + 1}); + } + if (grid[coord.y][coord.x] == NUISANCE_COLOR && toDelete[coord.y][coord.x] == 0) + toDelete[coord.y][coord.x] = 1; //For erasing garbage + } + + //If there are at least four blobs in this clump, remove them from the grid and draw a pop. + if (blobsInClump >= 4) { + for (let y = 0; y < 12; y++) { + for (let x = 0; x < 6; x++) { + if (toDelete[y][x] == 1) { + grid[y][x] = 0; + + //Clear the blob out of the slot + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + + //Draw the pop + let colorInfo = getColor(color); + g.setColor(colorInfo.r, colorInfo.g, colorInfo.b); + if (color < NUISANCE_COLOR) { + //A fancy pop for popped colors! + g.drawEllipse((x*18)+36, (y*14)+7, (x*18)+50, (y*14)+21); + g.drawEllipse((x*18)+27, (y*14)-2, (x*18)+59, (y*14)+30); + } else if (color == NUISANCE_COLOR) { + //Nuisance Blobs are simply crossed out. + //TODO: Nuisance Blobs are currently unusued, but also untested. Test before use. + g.drawLine((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + } + } + } + } + return true; + } + return false; + }; + + // Variables for graphics + let oldGhost = {pivotX: 0, pivotY: 0, leafX: 0, leafY: 0}; + + /* + Draws the time on the side. + */ + let drawTime = function(scheduleNext) { + //Change this to alter the y-coordinate of the top edge. + let dy = 25; + + g.setBgColor(0, 0, 0); + g.clearRect(2, dy, 30, dy + 121); + + //Draw the time + let d = new Date(); + let h = d.getHours(), m = d.getMinutes(); + if (twelveHourTime) { + let mer = 'A'; + if (h >= 12) mer = 'P'; + if (h >= 13) h -= 12; + if (h == 0) h = 12; + + g.setColor(1, 1, 1); + g.setFont("Vector", 12); + g.drawString(mer, 23, dy + 63); + } + let hs = h.toString().padStart(2, 0); + let ms = m.toString().padStart(2, 0); + g.setFont("Vector", 24); + g.setColor(1, 0.2, 1); + g.drawString(hs, 3, dy + 21); + g.setColor(0.5, 0.5, 1); + g.drawString(ms, 3, dy + 42); + + //Draw seconds + let s = d.getSeconds(); + if (updateSeconds) { + let ss = s.toString().padStart(2, 0); + g.setFont("Vector", 12); + g.setColor(0.2, 1, 0.2); + g.drawString(ss, 3, dy + 63); + } + + //Draw the date + let dayString = d.getDate().toString(); + let dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; + let dayName = dayNames[d.getDay()]; + let monthNames = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JLY", "AUG", "SEP", "OCT", "NOV", "DEC"]; + let monthName = monthNames[d.getMonth()]; + g.setColor(1, 1, 1); + g.setFont("Vector", 12); + g.drawString(monthName, 3, dy + 84); + g.drawString(dayString, 3, dy + 97); + g.setColor(0.5, 0.5, 0.5); + g.drawString(dayName, 3, dy + 110); + + //Draw battery + if (s == 0) batteryLevel = E.getBattery(); + if (Bangle.isCharging()) { + g.setColor(0, 0, 1); + } else if (batteryLevel <= 15) { + g.setColor(1, 0, 0); + } else { + g.setColor(0, 1, 0); + } + g.drawString(batteryLevel + "%", 3, dy + 1); + + //Schedule the next draw if requested. + if (!scheduleNext) return; + if (clockDrawTimeout) clearTimeout(clockDrawTimeout); + let interval = updateSeconds ? 1000 : 60000; + clockDrawTimeout = setTimeout(function() { + clockDrawTimeout = undefined; + drawTime(true); + }, interval - (Date.now() % interval)); + }; + + /* + Returns a tuple in the format {r, g, b} with the color + of the blob with the given ID. + This saves memory compared to having the colors stored in an array. + */ + let getColor = function(color) { + if (color == 1) + return {r: 1, g: 0, b: 0}; + if (color == 2) + return {r: 0, g: 1, b: 0}; + if (color == 3) + return {r: 0, g: 0, b: 1}; + if (color == 4) + return {r: 1, g: 1, b: 0}; + if (color == 5) + return {r: 1, g: 0, b: 1}; + if (color == 6) + return {r: 0, g: 1, b: 1}; + if (color == 7) + return {r: 0.5, g: 0.5, b: 0.5}; + return {r: 1, g: 1, b: 1}; + }; + + /* + Clears the screen and draws the background. + */ + let drawBackground = function() { + //Background + g.setBgColor(0.5, 0.2, 0.1); + g.clear(); + g.setBgColor(0, 0, 0); + g.clearRect(33, 0, 142, 176); + g.setBgColor(0.5, 0.5, 0.5); + g.clearRect(33, 4, 142, 6); + + //Reset button + g.setBgColor(0.5, 0.5, 0.5); + g.setColor(0, 0, 0); + g.clearRect(143, 150, 175, 175); + g.setFont("Vector", 30); + g.drawString("R", 152, 150); + + //Pause button + g.clearRect(0, 150, 32, 175); + g.fillRect(9, 154, 13, 171); + g.fillRect(18, 154, 22, 171); + }; + + /* + Draws a box under the next queue that displays + the current value of lastChain. + */ + let drawChainCount = function() { + g.setBgColor(0, 0, 0); + g.setColor(1, 0.2, 0.2); + g.setFont("Vector", 23); + g.clearRect(145, 42, 173, 64); + + if (lastChain > 0) { + if (lastChain < 10) g.drawString(lastChain, 154, 44); + if (lastChain >= 10) g.drawString(lastChain, 147, 44); + } + }; + + /* + Draws the blob at the given slot. + */ + let drawBlobAtSlot = function(x, y) { + //If this blob is in the hidden row, clear it out and stop. + if (y < 0) { + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, 0, (x*18)+52, 3); + return; + } + + //First, clear what was in that slot. + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + + let color = grid[y][x]; + + if (color != 0) { + let myColor = getColor(color); + g.setColor(myColor.r, myColor.g, myColor.b); + g.fillEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + g.setColor(1, 1, 1); + g.drawEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + } + }; + + /* + Draws the ghost piece. + clearOld: if the previous location of the ghost piece should be cleared. + */ + let drawGhostPiece = function(clearOld) { + if (clearOld) { + g.setColor(0, 0, 0); + g.fillRect((oldGhost.pivotX*18)+38, (oldGhost.pivotY*14)+8, (oldGhost.pivotX*18)+47, (oldGhost.pivotY*14)+17); + g.fillRect((oldGhost.leafX*18)+38, (oldGhost.leafY*14)+8, (oldGhost.leafX*18)+47, (oldGhost.leafY*14)+17); + } + + let pivotX = dropCoordinates.pivotX; + let pivotY = dropCoordinates.pivotY; + let leafX = dropCoordinates.leafX; + let leafY = dropCoordinates.leafY; + let pivotColor = getColor(currentPair.pivot); + let leafColor = getColor(currentPair.leaf); + + g.setColor(pivotColor.r, pivotColor.g, pivotColor.b); + g.fillRect((pivotX*18)+40, (pivotY*14)+10, (pivotX*18)+45, (pivotY*14)+15); + g.setColor(1, 1, 1); + g.drawRect((pivotX*18)+38, (pivotY*14)+8, (pivotX*18)+47, (pivotY*14)+17); + g.setColor(leafColor.r, leafColor.g, leafColor.b); + g.fillRect((leafX*18)+40, (leafY*14)+10, (leafX*18)+45, (leafY*14)+15); + + oldGhost = {pivotX: pivotX, pivotY: pivotY, leafX: leafX, leafY: leafY}; + }; + + /* + Draws the next queue. + */ + let drawNextQueue = function() { + g.setBgColor(0, 0, 0); + g.clearRect(145, 4, 173, 28); + + let p1 = nextQueue[0].pivot; + let l1 = nextQueue[0].leaf; + let p2 = nextQueue[1].pivot; + let l2 = nextQueue[1].leaf; + let p1C = getColor(p1); + let l1C = getColor(l1); + let p2C = getColor(p2); + let l2C = getColor(l2); + + g.setColor(p1C.r, p1C.g, p1C.b); + g.fillEllipse(146, 17, 157, 28); + g.setColor(l1C.r, l1C.g, l1C.b); + g.fillEllipse(146, 5, 157, 16); + g.setColor(p2C.r, p2C.g, p2C.b); + g.fillEllipse(162, 17, 173, 28); + g.setColor(l2C.r, l2C.g, l2C.b); + g.fillEllipse(162, 5, 173, 16); + + g.setColor(1, 1, 1); + g.drawLine(159, 4, 159, 28); + g.drawEllipse(146, 17, 157, 28); + g.drawEllipse(146, 5, 157, 16); + g.drawEllipse(162, 17, 173, 28); + g.drawEllipse(162, 5, 173, 16); + }; + + /* + Redraws the screen, except for the ghost piece. + */ + let redrawBoard = function() { + drawBackground(); + drawNextQueue(); + drawChainCount(); + drawTime(false); + for (let y = 0; y < 12; y++) { + for (let x = 0; x < 6; x++) { + drawBlobAtSlot(x, y); + } + } + }; + + /* + Toggles the pause screen. + */ + let togglePause = function() { + gamePaused = !gamePaused; + + if (gamePaused) { + g.setBgColor(0.5, 0.2, 0.1); + g.clear(); + drawTime(false); + + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(48, 66, 157, 110); + g.setFont("Vector", 20); + g.drawString("Tap here\nto unpause", 50, 68); + + require("widget_utils").show(); + Bangle.drawWidgets(); + } else { + require("widget_utils").hide(); + + redrawBoard(); + drawGhostPiece(false); + + //Display the loss text if the game is lost. + if (gameLost) { + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(33, 73, 142, 103); + g.setFont("Vector", 20); + g.drawString("You Lose", 43, 80); + } + } + }; + + // ~~ Events ~~ + let dragAmnt = 0; + + let onTouch = (z, e) => { + if (midChain) return; + + if (gamePaused) { + if (e.x >= 40 && e.y >= 58 && e.x <= 165 && e.y <= 118) { + g.setBgColor(1, 1, 1); + g.clearRect(48, 66, 157, 110); + g.flip(); + togglePause(); + } + } else { + //Tap reset button + if (e.x >= 143 && e.y >= 150) { + restartGame(); + newPair(); + redrawBoard(); + drawGhostPiece(false); + g.flip(); + return; + } + + //Tap pause button + if (e.x <= 32 && e.y >= 150) { + togglePause(); + return; + } + + //While playing, rotate pieces. + if (!gameLost && !gamePaused) { + if (e.x < 88) { + rotatePair(false); + drawGhostPiece(true); + } else { + rotatePair(true); + drawGhostPiece(true); + } + } + } + }; + + Bangle.on("touch", onTouch); + + let onDrag = (e) => { + if (gameLost || gamePaused || midChain) return; + + //Do nothing if the user is dragging down so that they don't accidentally move while dropping + if (e.dy >= 5) { + return; + } + + dragAmnt += e.dx; + if (e.b == 0) { + dragAmnt = 0; + } + if (dragAmnt >= 20) { + movePair(Math.floor(dragAmnt / 20)); + drawGhostPiece(true); + dragAmnt = dragAmnt % 20; + } + if (dragAmnt <= -20) { + movePair(Math.ceil(dragAmnt / 20)); + drawGhostPiece(true); + dragAmnt = dragAmnt % 20; + } + }; + + Bangle.on("drag", onDrag); + + let onSwipe = (x, y) => { + if (gameLost || gamePaused || midChain) return; + + if (y > 0) { + let pivotX = dropCoordinates.pivotX; + let pivotY = dropCoordinates.pivotY; + let leafX = dropCoordinates.leafX; + let leafY = dropCoordinates.leafY; + + if (pivotY < -1 && leafY < -1) return; + + quickDrop(); + drawBlobAtSlot(pivotX, pivotY); + drawBlobAtSlot(leafX, leafY); + g.flip(); + + //Check for pops + if (pivotY >= 0) addSlotToCheck(pivotX, pivotY); + if (leafY >= 0) addSlotToCheck(leafX, leafY); + midChain = true; + let currentChain = 0; + while (popAll().pops > 0) { + currentChain++; + lastChain = currentChain; + drawChainCount(); + g.flip(); + settleBlobs(); + redrawBoard(); + g.flip(); + } + + newPair(); + drawNextQueue(); + drawGhostPiece(false); + + //If the top slot of the third column is taken, lose the game. + if (grid[0][2] != 0) { + gameLost = true; + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(33, 73, 142, 103); + g.setFont("Vector", 20); + g.drawString("You Lose", 43, 80); + } + + midChain = false; + } + }; + + Bangle.on("swipe", onSwipe); + + let onLock = on => { + updateSeconds = !on; + drawTime(true); + }; + + Bangle.on('lock', onLock); + + let onCharging = charging => { + drawTime(false); + }; + + Bangle.on('charging', onCharging); + + Bangle.setUI({mode:"clock", remove:function() { + //Remove listeners + Bangle.removeListener("touch", onTouch); + Bangle.removeListener("drag", onDrag); + Bangle.removeListener("swipe", onSwipe); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('charging', onCharging); + + if (clockDrawTimeout) clearTimeout(clockDrawTimeout); + require("widget_utils").show(); + }}); + + g.reset(); + + Bangle.loadWidgets(); + require("widget_utils").hide(); + + drawBackground(); + drawTime(true); + + restartGame(); + + newPair(); + drawGhostPiece(false); + + drawNextQueue(); + drawChainCount(); +} From a109cfd391e109d84a1d6f07d0486fd551fa7ecc Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:35:34 -0400 Subject: [PATCH 220/242] Create ChangeLog Added the changelog. --- apps/bblobface/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/bblobface/ChangeLog diff --git a/apps/bblobface/ChangeLog b/apps/bblobface/ChangeLog new file mode 100644 index 000000000..6a29fdb72 --- /dev/null +++ b/apps/bblobface/ChangeLog @@ -0,0 +1 @@ +1.00: Initial release of Bangle Blobs Clock! From fd367c06dcf7d17ba1352c232b0f7ff266369e70 Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:35:56 -0400 Subject: [PATCH 221/242] Create app-icon.js --- apps/bblobface/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/bblobface/app-icon.js diff --git a/apps/bblobface/app-icon.js b/apps/bblobface/app-icon.js new file mode 100644 index 000000000..e8d9baced --- /dev/null +++ b/apps/bblobface/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA=")) From cfbc148a86da07530c3b320fa11cc105bbd8879d Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:37:42 -0400 Subject: [PATCH 222/242] Create metadata.json --- apps/bblobface/metadata.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/bblobface/metadata.json diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json new file mode 100644 index 000000000..0d50a2424 --- /dev/null +++ b/apps/bblobface/metadata.json @@ -0,0 +1,14 @@ +{ "id": "bblobface", + "name": "Bangle Blobs Clock", + "shortName":"BBClock", + "icon": "app.png", + "version": "1.00", + "description": "A fully featured watch face with a playable game on the side.", + "type": "clock", + "tags": "", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"bblobface.app.js","url":"app.js"}, + {"name":"bblobface.img","url":"app-icon.js","evaluate":true} + ] +} From 8fd2a87342c4143b2e3be930bdfdf81402e40237 Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:38:27 -0400 Subject: [PATCH 223/242] Upload Image Uploaded the png for the icon. --- apps/bblobface/app.png | Bin 0 -> 691 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/bblobface/app.png diff --git a/apps/bblobface/app.png b/apps/bblobface/app.png new file mode 100644 index 0000000000000000000000000000000000000000..2201fa621ee4950521e77d8914e279dffb9d614f GIT binary patch literal 691 zcmV;k0!;mhP)C*2ppRI|6k@Fx>{5a)Y{%9H!mi;NC!rNmH;sRFh1Ik z3_immfIwXb%nT5+y|;S)Mv+`!#7#r}!M!~rer9oGBFu@Ax-zp)*049%n~@ceaC2~_;g%pjT|k?un0Z*WH@(nF_no1dZCEPDG2;=RNO8)AB z-N3#~o~TbAxNNvQ)K>srQmI-4U~smtk6l521XU2vDNz43Lq0(Su~h-neuaGYdj_#h z0ohmXE}atl6qr?P5L*<;eFJ;54?Lm3W`q=w84XvVp56BXF5z1U)LEzpcf1aE?4?pF z$hQitPLn}<0_Kzu}$hUqkp5IPv4Q|#N!yTFL`2{%5LMz9>hB=MiFkU3Tssi3R z`jDNoP*X;qK)M_2eg=lQ--`bLW^m)D(YF+!r68I<@DApJsGIF8Rj9<;Ec6QkNSK>O Ze*m1G3ni=?{K^0T002ovPDHLkV1h2JIyC?Q literal 0 HcmV?d00001 From d9718f4e57337111ba3ac430ca4d2997063df69a Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:45:07 -0400 Subject: [PATCH 224/242] Add tags to metadata.json --- apps/bblobface/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json index 0d50a2424..ad822e15c 100644 --- a/apps/bblobface/metadata.json +++ b/apps/bblobface/metadata.json @@ -5,7 +5,7 @@ "version": "1.00", "description": "A fully featured watch face with a playable game on the side.", "type": "clock", - "tags": "", + "tags": "clock", "game", "supports" : ["BANGLEJS2"], "storage": [ {"name":"bblobface.app.js","url":"app.js"}, From baf720c5bcfc54494db7421284969504099532b1 Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:47:00 -0400 Subject: [PATCH 225/242] Fix metadata.json Attempt to fix the metadata.json --- apps/bblobface/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json index ad822e15c..c9e4acca6 100644 --- a/apps/bblobface/metadata.json +++ b/apps/bblobface/metadata.json @@ -5,7 +5,7 @@ "version": "1.00", "description": "A fully featured watch face with a playable game on the side.", "type": "clock", - "tags": "clock", "game", + "tags": ["clock", "game"], "supports" : ["BANGLEJS2"], "storage": [ {"name":"bblobface.app.js","url":"app.js"}, From 4b925b7fea4c33179745c68250db268f2576c5e1 Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:50:22 -0400 Subject: [PATCH 226/242] Actually fix metadata.json After trying multiple things, I think this will fix the tags. --- apps/bblobface/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json index c9e4acca6..51b5b81f5 100644 --- a/apps/bblobface/metadata.json +++ b/apps/bblobface/metadata.json @@ -5,7 +5,7 @@ "version": "1.00", "description": "A fully featured watch face with a playable game on the side.", "type": "clock", - "tags": ["clock", "game"], + "tags": "clock, game", "supports" : ["BANGLEJS2"], "storage": [ {"name":"bblobface.app.js","url":"app.js"}, From e881ab3f52042855bc885451d89e9dc29baa7aff Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 13:05:54 -0400 Subject: [PATCH 227/242] Screenshots Uploaded a few screenshots for the README. --- apps/bblobface/screenshot1.png | Bin 0 -> 3797 bytes apps/bblobface/screenshot2.png | Bin 0 -> 4808 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/bblobface/screenshot1.png create mode 100644 apps/bblobface/screenshot2.png diff --git a/apps/bblobface/screenshot1.png b/apps/bblobface/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..91650c07a62a25d0c8ff75aeba9a2dbe0d72af18 GIT binary patch literal 3797 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 literal 0 HcmV?d00001 diff --git a/apps/bblobface/screenshot2.png b/apps/bblobface/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..64644965f7cfab5d4c278b85863c7ae8ba943434 GIT binary patch literal 4808 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 Date: Sun, 27 Aug 2023 13:57:51 -0400 Subject: [PATCH 228/242] Create README.md --- apps/bblobface/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 apps/bblobface/README.md diff --git a/apps/bblobface/README.md b/apps/bblobface/README.md new file mode 100644 index 000000000..4c32221f6 --- /dev/null +++ b/apps/bblobface/README.md @@ -0,0 +1,34 @@ +# Bangle Blobs Clock +What if every time you checked the time, you could play a turn of a turn-based puzzle game? +You check the time dozens, maybe hundreds of times per day, and Bangle Blobs Clock wants to add a splash of fun to each of these moments! +Bangle Blobs Clock is a fully featured watch face with a turn-based puzzle game right next to the clock. + +![](screenshot1.png) +![](screenshot2.png) + +## Clock Features +- Hour and minute +- Seconds (only while the screen is unlocked to save power) +- Month, day, and day of week +- Battery percentage. Blue while charging, red when low, green otherwise. +- Respects your 24-hour/12-hour time setting in Locale +- Press the pause button to access your Widgets +- Supports Fast Loading + +## The Game +This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA. +Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall. +If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs! +- Drag left and right to move the pair +- Tap the left or right half of the screen to rotate the pair +- Swipe down to place the pair + +## More Info +If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing + +## Credits +- I'm Pasta Rhythm, computer scientist and aspiring game developer. +- ![nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started. +- ![gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and ![Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2. +- Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects! +- SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games! From 639b26b7b3f3b9f65d23498f36e606a03fa77cfd Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 14:01:48 -0400 Subject: [PATCH 229/242] Edited README Fixed some formatting issues with the README --- apps/bblobface/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/bblobface/README.md b/apps/bblobface/README.md index 4c32221f6..7eab8221d 100644 --- a/apps/bblobface/README.md +++ b/apps/bblobface/README.md @@ -28,7 +28,7 @@ If you're confused about the functionality of the clock or want a better explana ## Credits - I'm Pasta Rhythm, computer scientist and aspiring game developer. -- ![nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started. -- ![gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and ![Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2. +- [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started. +- [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2. - Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects! - SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games! From a46751a3731c226a05053d5a30c679448760440b Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 14:04:48 -0400 Subject: [PATCH 230/242] Add README to metadata --- apps/bblobface/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json index 51b5b81f5..6af247c91 100644 --- a/apps/bblobface/metadata.json +++ b/apps/bblobface/metadata.json @@ -4,6 +4,7 @@ "icon": "app.png", "version": "1.00", "description": "A fully featured watch face with a playable game on the side.", + "readme":"README.md", "type": "clock", "tags": "clock, game", "supports" : ["BANGLEJS2"], From aeca14baa08977d8f2fa467d015d7efe3fe29fb1 Mon Sep 17 00:00:00 2001 From: PastaRhythm <83084413+PastaRhythm@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:34:27 -0400 Subject: [PATCH 231/242] Update README.md Made a quick change to the credits to avoid confusion and make it more readable. --- apps/bblobface/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/bblobface/README.md b/apps/bblobface/README.md index 7eab8221d..54e07e9f8 100644 --- a/apps/bblobface/README.md +++ b/apps/bblobface/README.md @@ -26,9 +26,10 @@ If this causes another pop, it's called a chain! Build a massive chain reaction ## More Info If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing -## Credits -- I'm Pasta Rhythm, computer scientist and aspiring game developer. +## Special Thanks +I'm Pasta Rhythm, computer scientist and aspiring game developer. I would like to say thank you to the people who inspired me while I was making this app: - [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started. - [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2. - Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects! - SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games! +- Compile, the original creators of Puyo Puyo. The company went bankrupt long ago, but the people who worked for them continue to make games. From b75c2751c9882a8c1a21e4d48e1d23a6b0438670 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 29 Aug 2023 17:04:37 +0100 Subject: [PATCH 232/242] Fix daily summaries for 31st of the month - https://github.com/espruino/BangleApps/pull/2986 --- apps/health/ChangeLog | 1 + apps/health/app.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 489715931..02b53c56d 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -28,3 +28,4 @@ fix 11pm slot for daily HRM 0.26: Implement API for activity fetching 0.27: Fix typo in daily summary graph code causing graph not to load + Fix daily summaries for 31st of the month diff --git a/apps/health/app.js b/apps/health/app.js index 3b615ff1d..db21d9243 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -48,7 +48,7 @@ function stepsPerHour() { function stepsPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "stepsPerDay"; - var data = new Uint16Array(31); + var data = new Uint16Array(32); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); setButton(menuStepCount); barChart(/*LANG*/"DAY", data); @@ -72,8 +72,8 @@ function hrmPerHour() { function hrmPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "hrmPerDay"; - var data = new Uint16Array(31); - var cnt = new Uint8Array(31); + var data = new Uint16Array(32); + var cnt = new Uint8Array(32); require("health").readDailySummaries(new Date(), h=>{ data[h.day]+=h.bpm; if (h.bpm) cnt[h.day]++; @@ -100,8 +100,8 @@ function movementPerHour() { function movementPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "movementPerDay"; - var data = new Uint16Array(31); - var cnt = new Uint8Array(31); + var data = new Uint16Array(32); + var cnt = new Uint8Array(32); require("health").readDailySummaries(new Date(), h=>{ data[h.day]+=h.movement; cnt[h.day]++; From 6c4d3f41a59d519bc502f6d1a1792c472586398d Mon Sep 17 00:00:00 2001 From: Niko Komin Date: Tue, 29 Aug 2023 11:32:03 -0700 Subject: [PATCH 233/242] added new app --- apps/nightwatch/ChangeLog | 1 + apps/nightwatch/README.md | 20 +++ apps/nightwatch/metadata.json | 16 +++ apps/nightwatch/nightwatch.app.info | 6 + apps/nightwatch/nightwatch.app.js | 175 +++++++++++++++++++++++++ apps/nightwatch/nightwatch.icon.js | 1 + apps/nightwatch/nightwatch.icon.png | Bin 0 -> 959 bytes apps/nightwatch/nightwatch.info.js | 6 + apps/nightwatch/nightwatch.settings.js | 25 ++++ apps/nightwatch/screenshot.png | Bin 0 -> 3608 bytes apps/nightwatch/screenshot2.png | Bin 0 -> 3795 bytes 11 files changed, 250 insertions(+) create mode 100644 apps/nightwatch/ChangeLog create mode 100644 apps/nightwatch/README.md create mode 100644 apps/nightwatch/metadata.json create mode 100644 apps/nightwatch/nightwatch.app.info create mode 100644 apps/nightwatch/nightwatch.app.js create mode 100644 apps/nightwatch/nightwatch.icon.js create mode 100644 apps/nightwatch/nightwatch.icon.png create mode 100644 apps/nightwatch/nightwatch.info.js create mode 100644 apps/nightwatch/nightwatch.settings.js create mode 100644 apps/nightwatch/screenshot.png create mode 100644 apps/nightwatch/screenshot2.png diff --git a/apps/nightwatch/ChangeLog b/apps/nightwatch/ChangeLog new file mode 100644 index 000000000..dc179ee9d --- /dev/null +++ b/apps/nightwatch/ChangeLog @@ -0,0 +1 @@ +1.0: first working version of App diff --git a/apps/nightwatch/README.md b/apps/nightwatch/README.md new file mode 100644 index 000000000..6d1749c5d --- /dev/null +++ b/apps/nightwatch/README.md @@ -0,0 +1,20 @@ +# The Nightwatch + +Snuggle into your sleeping bag, hang the Bangle on the tent wall +and check the screen before you fall asleep: + +![](screenshot.png) +![](screenshot2.png) + + +Reads temperature and pressure sensor. Shows current, maximal and minimal values +since the start of the app. Also show a graph of the last 20 measures. + +Swipe left/right between values. + +Screen is updated periodically, time step is configurable in settings. + + +# Creator + +[Niko Komin](https://www.laikaundfreunde.de/niko-komin/) diff --git a/apps/nightwatch/metadata.json b/apps/nightwatch/metadata.json new file mode 100644 index 000000000..90e05214e --- /dev/null +++ b/apps/nightwatch/metadata.json @@ -0,0 +1,16 @@ +{ + "id":"nightwatch", + "readme":"README.md", + "name":"The Nightwatch", + "shortName":"Nightwatch", + "supports" : ["BANGLEJS2"], + "icon":"nightwatch.icon.png", + "screenshots" : [ { "url":"screenshot.png","url":"screenshot2.png" } ], + "version":"1.0", + "description":"Logs sensor readings (currently T and p), show min/max and graph.", + "tags": "tools,outdoors", + "storage": [ + {"name":"nightwatch.app.js","url":"nightwatch.app.js"}, + {"name":"nightwatch.img","url":"nightwatch.icon.js","evaluate":true} + ] +} diff --git a/apps/nightwatch/nightwatch.app.info b/apps/nightwatch/nightwatch.app.info new file mode 100644 index 000000000..36345ce26 --- /dev/null +++ b/apps/nightwatch/nightwatch.app.info @@ -0,0 +1,6 @@ +require("Storage").write("nightwatch.info",{ + "id":"nightwatch", + "name":"nightwatch", + "src":"nightwatch.app.js", + "icon":"nightwatch.icon.png" +}); \ No newline at end of file diff --git a/apps/nightwatch/nightwatch.app.js b/apps/nightwatch/nightwatch.app.js new file mode 100644 index 000000000..035307106 --- /dev/null +++ b/apps/nightwatch/nightwatch.app.js @@ -0,0 +1,175 @@ +// PTLOGGER +// MEASURES p AND T PERIODICALLY AND UPDATES MIN & MAX VALS +// DISPLAYS EITHER OF BOTH + + +var settings = Object.assign({ + dt: 5, //time interval in minutes +}, require('Storage').readJSON("nightwatch.json", true) || {}); + +let dt = settings.dt; +delete settings; + +var timerID; + +const highColor = '#35b779';//#6dcd59; +const lowColor = '#eb005c';//#3d4a89;//#482878; +const normColor = '#000000'; +const historyAmnt = 24; + + +const TData = { + ondisplay:true, + unit: '\xB0C', + accuracy: 1, + value : 100, t_value:'0:00', + values : new Array(historyAmnt), + maxval : -100, t_max:'0:00', + minval : 100, t_min:'0:00' +}; + +const PData = { + ondisplay:false, + unit: 'mbar', + accuracy: 0, + value : 0, t_value:'0:00', + values : new Array(historyAmnt), + maxval : 0, t_max:'0:00', + minval : 10000, t_min:'0:00' +}; + +function minMaxString(val,accuracy,unit,time){ + return time+' '+val.toFixed(accuracy)+unit; +// return val.toFixed(accuracy)+unit+'('+time+')'; +} + +function updateScreen() { + // what are we showing right now? + let data; + if (TData.ondisplay){data = TData;} + else {data = PData;} + + // make strings + let valueString = data.value.toFixed(data.accuracy)+data.unit; + let minString = minMaxString(data.minval, data.accuracy, data.unit, data.t_min); + let maxString = minMaxString(data.maxval, data.accuracy, data.unit, data.t_max); + + // LETS PAINT + g.clear(); + g.setFontAlign(0, 0); + + // MINUM AND MAXIMUM VALUES AND TIMES + g.setFont("Vector:18"); + g.setColor(normColor); + g.drawString(maxString, g.getWidth() / 2, 11); + g.drawString(minString, g.getWidth() / 2, g.getHeight() - 11); + + g.setColor(normColor); + + // TIME OF LAST MEASURE AND SIZE OF INTERVAL + g.setFontAlign(-1, 0); + g.drawString(data.t_value, 0, g.getHeight()/2 - 25); + g.setFontAlign(1, 0); + g.drawString('dt='+dt+'min', g.getWidth() , g.getHeight()/2 - 25); + + //////////////////////////////////////////////////////////// + // GRAPH OF MEASUREMENT HISTORY + g.setFont("Vector:16"); + const graphHeight=35; + const graphWidth=g.getWidth()-30; + const graphLocX = 15; + const graphLocY = g.getHeight() - 16 - 18 - graphHeight; + + // DRAW SOME KIND OF AXES + g.setColor(0.4,0.4,0.4); + g.drawRect(graphLocX,graphLocY,graphLocX+graphWidth,graphLocY+graphHeight); + g.drawLine(graphLocX,graphLocY+graphHeight/2,graphLocX+graphWidth,graphLocY+graphHeight/2); + g.drawLine(graphLocX+graphWidth/2,graphLocY,graphLocX+graphWidth/2,graphLocY+graphHeight); + g.drawLine(graphLocX+graphWidth/4,graphLocY,graphLocX+graphWidth/4,graphLocY+graphHeight); + g.drawLine(graphLocX+3*graphWidth/4,graphLocY,graphLocX+3*graphWidth/4,graphLocY+graphHeight); + g.setColor(normColor); + + // DRAW LINE + require("graph").drawLine(g, data.values, { + x:graphLocX, + y:graphLocY, + width:graphWidth, + height:graphHeight + }); + + let graphMax=Math.max.apply(Math,data.values); + let graphMin=Math.min.apply(Math,data.values); + g.setFontAlign(0, 0); + g.setColor(highColor); + g.drawString(graphMax.toFixed(data.accuracy), g.getWidth()/2, g.getHeight() - 16 - 18 - graphHeight); + g.setColor(lowColor); + g.drawString(graphMin.toFixed(data.accuracy), g.getWidth()/2, g.getHeight() - 16 - 18); + g.setColor(normColor); + + let historyLength = (historyAmnt*dt >= 60)?('-'+historyAmnt*dt/60+'h'):('-'+historyAmnt*dt+'"'); + + g.drawString(historyLength,25, g.getHeight() - 16 - 18 - graphHeight/2); + + //////////////////////////////////////////////////////////// + // LAST MEASURE + g.setFontAlign(0, 0); + g.setFont('Vector:36'); + g.drawString(valueString, g.getWidth() / 2, g.getHeight() / 2); + + data.ondisplay = true; +} + +function updateMinMax( data, currentValue ){ + data.values.push(currentValue); + data.values.shift(); + data.value=currentValue; + + let now = new Date(); + data.t_value = now.getHours()+':'+String(now.getMinutes()).padStart(2, '0'); + if (currentValue < data.minval){data.t_min=data.t_value;data.minval = currentValue;} + if (currentValue > data.maxval){data.t_max=data.t_value;data.maxval = currentValue;} +} + +function switchDisplay(){ + if (TData.ondisplay) {TData.ondisplay=false;PData.ondisplay=true;updateScreen();} + else {PData.ondisplay=false;TData.ondisplay=true;updateScreen();} +} + +function settingsPage(){ + Bangle.on('swipe',function (){}); + eval(require("Storage").read("nightwatch.settings.js"))(()=>load()); + Bangle.on('swipe',switchDisplay); + console.log(3); +} + +function handlePressureSensorReading(data) { + updateMinMax(TData,data.temperature); + updateMinMax(PData,data.pressure); +} + +function startup(){ + // testing in emulator + // handlePressureSensorReading({ "temperature": 28.64251302083, "pressure": 1004.66520303803, "altitude": 71.72072902749 }); + // updateScreen(); + + // ON STARTUP: + // fill current reading into data, + // before `updateMinMax` uses it + Bangle.getPressure().then(d=>{TData.value=d.temperature; + TData.values.fill(d.temperature); + PData.value=d.pressure; + PData.values.fill(d.pressure); + handlePressureSensorReading(d); + updateScreen();}); + Bangle.on('swipe',switchDisplay); + + //Bangle.on('tap',settingsPage); + + timerID = setInterval( function() { + Bangle.getPressure().then(d=>{handlePressureSensorReading(d);updateScreen();}); + }, dt * 60000); + +} + +startup(); + diff --git a/apps/nightwatch/nightwatch.icon.js b/apps/nightwatch/nightwatch.icon.js new file mode 100644 index 000000000..19b4623f0 --- /dev/null +++ b/apps/nightwatch/nightwatch.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4MA///ospETUQAgc//gFDv4FF/wFP/4FF/5PCgIFChF/AoWA/1/+YFBx/+g4EBAAPAFAIEBEgUDBQYAN/E/AgQvDDoXHDocH4wFDgf8v4RCDooAMj/4AoZcBcM8DOAgFFgJSDAqQAhA==")) diff --git a/apps/nightwatch/nightwatch.icon.png b/apps/nightwatch/nightwatch.icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3a3282a2f655eba40d0441711ed16c1a809216 GIT binary patch literal 959 zcmV;w13>(VP)Vs(=D#7dY15`A0EmtJSi%|rFP2^IAo;3<%`y2-Hk*Aq zG!sUyF!8R5|M?Pe+la$PZjrFZh~N6J71vcs&xB^di2Y>&4!^*<7p##pOHJN4!)R1B)doOs4)O$NzqyfyNz7`VAe=@cZeeUMZzwUi*mW#r1DS9 zWKx|*+;|)TR!F1X!6H@yJ`j1pi0hAtC;pT)ITR=49*M_IygAf2WyC27+l^cXV6JzO z#|~Z(M64q@FD5^9K_>q3KlfiX;aeY)Q4~EpKy_DF*HdQEeDBd-oJrud3bOk3j|pE% zSPdW@D#1Mq$=8kCD)CG`0v2&Qc^0|VpWV~b^F*i>UN~maLf`V@R{tpCiim%!`)1iE z@_Vl}16KJ^^ehOCU~XsU_)I4AT@gkV>VVl=Pm2kIZb2Tj!zf z&Ci%I(SYR=HjsQJG#WFbx7)JR=u7N{MGG=O-j~l737?mBxw}4hYMSGf;Dg1)ze`%} z*}`CZM9?rr%dhL6_hvRz#I=&(Kl6O=uLX(URaVeASl6A9_-WZ7>h)UiYo0PjTsHAm zQWwP0k%)+ap1KKeuO#tEd0UitzQp4!>fe!JMBH+rp#gMwak`@%Se)*m^hW88KJtCn z2F?py@|wv+Q52ehjwp(z4`%vGku)yX-Tji!hWdpzn#pFfQ~f;1Q^v^m-K`ew@6U+^ zCVNGm^NuC*hh!Fz8x2_LE;x?UXxQ;e9H+DH5Z%X!{KkV8d7&^H@RNjXB+V>(Q{tBe z-{Y+pzb4k5{KCbMT>Nq>^+H2Lv}x036t~xOIz6TTwVCBYDmLfxlF({o{C7c7z##u}Y6&|0giXZnp;a+*uaWygt3vWEKRgbtN-me1 h)GWWa+O!$1+y}#On>2C^{7nD=002ovPDHLkV1lA$%cuYV literal 0 HcmV?d00001 diff --git a/apps/nightwatch/nightwatch.info.js b/apps/nightwatch/nightwatch.info.js new file mode 100644 index 000000000..ccbc8909a --- /dev/null +++ b/apps/nightwatch/nightwatch.info.js @@ -0,0 +1,6 @@ +require("Storage").write("nightwatch.info",{ + "id":"nightwatch", + "name":"The Nightwatch", + "src":"nightwatch.app.js", + "icon":"nightwatch.icon.png" +}); diff --git a/apps/nightwatch/nightwatch.settings.js b/apps/nightwatch/nightwatch.settings.js new file mode 100644 index 000000000..c543b7343 --- /dev/null +++ b/apps/nightwatch/nightwatch.settings.js @@ -0,0 +1,25 @@ +(function(back) { + var FILE = "nightwatch.json"; + // Load settings + var settings = Object.assign({ + dt: 30, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "nightwatch" }, + "< Back" : () => back(), + 'log freq (min)': { + value: 0|settings.dt, // 0| converts undefined to 0 + min: 1, max: 60, + onchange: v => { + settings.dt = v; + writeSettings(); + } + }, + }); +}); diff --git a/apps/nightwatch/screenshot.png b/apps/nightwatch/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..194c91b28c327b6ca06d06515b1dd81197411ed9 GIT binary patch literal 3608 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 literal 0 HcmV?d00001 From 006445c7b298a5d6448e8275f5ab34ea69eb7000 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 00:29:58 +0000 Subject: [PATCH 234/242] build(deps): bump core from `8cf4d0f` to `431a3fb` Bumps [core](https://github.com/espruino/EspruinoAppLoaderCore) from `8cf4d0f` to `431a3fb`. - [Commits](https://github.com/espruino/EspruinoAppLoaderCore/compare/8cf4d0fbfc310e0d68d616ec779c1888475899a2...431a3fb743da5c370729ab748cb2c177e70a345b) --- updated-dependencies: - dependency-name: core dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 8cf4d0fbf..431a3fb74 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 8cf4d0fbfc310e0d68d616ec779c1888475899a2 +Subproject commit 431a3fb743da5c370729ab748cb2c177e70a345b From 08687fb265b64ca7042c44b4d76e16afa8361a47 Mon Sep 17 00:00:00 2001 From: Niko Komin Date: Tue, 29 Aug 2023 20:22:23 -0700 Subject: [PATCH 235/242] bugfix (enable settings page) --- apps/nightwatch/ChangeLog | 1 + apps/nightwatch/metadata.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/nightwatch/ChangeLog b/apps/nightwatch/ChangeLog index dc179ee9d..829c8c887 100644 --- a/apps/nightwatch/ChangeLog +++ b/apps/nightwatch/ChangeLog @@ -1 +1,2 @@ +1.1: bugfix (enable settings page) 1.0: first working version of App diff --git a/apps/nightwatch/metadata.json b/apps/nightwatch/metadata.json index 90e05214e..fc0150051 100644 --- a/apps/nightwatch/metadata.json +++ b/apps/nightwatch/metadata.json @@ -6,11 +6,12 @@ "supports" : ["BANGLEJS2"], "icon":"nightwatch.icon.png", "screenshots" : [ { "url":"screenshot.png","url":"screenshot2.png" } ], - "version":"1.0", + "version":"1.1", "description":"Logs sensor readings (currently T and p), show min/max and graph.", "tags": "tools,outdoors", "storage": [ {"name":"nightwatch.app.js","url":"nightwatch.app.js"}, + {"name":"nightwatch.settings.js","url":"nightwatch.settings.js"}, {"name":"nightwatch.img","url":"nightwatch.icon.js","evaluate":true} ] } From 72f49eeee71a57b49fa27b8c4eaf7ddd0c1f66e3 Mon Sep 17 00:00:00 2001 From: Niko Komin Date: Tue, 29 Aug 2023 21:00:52 -0700 Subject: [PATCH 236/242] bugfix (enable settings page) --- apps/nightwatch/ChangeLog | 2 +- apps/nightwatch/metadata.json | 3 ++- apps/nightwatch/nightwatch.settings.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/nightwatch/ChangeLog b/apps/nightwatch/ChangeLog index 829c8c887..c854e9e5b 100644 --- a/apps/nightwatch/ChangeLog +++ b/apps/nightwatch/ChangeLog @@ -1,2 +1,2 @@ -1.1: bugfix (enable settings page) 1.0: first working version of App +1.1: bugfix (enable settings page) diff --git a/apps/nightwatch/metadata.json b/apps/nightwatch/metadata.json index fc0150051..4de2a0271 100644 --- a/apps/nightwatch/metadata.json +++ b/apps/nightwatch/metadata.json @@ -13,5 +13,6 @@ {"name":"nightwatch.app.js","url":"nightwatch.app.js"}, {"name":"nightwatch.settings.js","url":"nightwatch.settings.js"}, {"name":"nightwatch.img","url":"nightwatch.icon.js","evaluate":true} - ] + ], + "data": [{"name":"nightwatch.json"}] } diff --git a/apps/nightwatch/nightwatch.settings.js b/apps/nightwatch/nightwatch.settings.js index c543b7343..744ebd8dc 100644 --- a/apps/nightwatch/nightwatch.settings.js +++ b/apps/nightwatch/nightwatch.settings.js @@ -2,7 +2,7 @@ var FILE = "nightwatch.json"; // Load settings var settings = Object.assign({ - dt: 30, + dt: 5, }, require('Storage').readJSON(FILE, true) || {}); function writeSettings() { From 1aa7a4d7b50ae4989faa68f14a870a00c7ef8840 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 30 Aug 2023 18:29:36 +0200 Subject: [PATCH 237/242] gipy: powersaving changes + fix for heights --- apps/gipy/ChangeLog | 2 ++ apps/gipy/README.md | 4 +++- apps/gipy/app.js | 18 +++++++++++------- apps/gipy/settings.js | 20 ++++++++++++++++++++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 73164dbd3..09637df1b 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -105,3 +105,5 @@ * Removed 'lost' indicator (we now change position to purple when lost) * Powersaving fix : don't powersave when lost * Bugfix for negative remaining distance when going backwards + * New settings for powersaving + * Adjustments to powersaving algorithm diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 0df008b38..d96461dfe 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -109,9 +109,11 @@ Colors correspond to slopes. Few settings for now (feel free to suggest me more) : -- lost distance : at which distance from path are you considered to be lost ? - buzz on turns : should the watch buzz when reaching a waypoint ? - disable bluetooth : turn bluetooth off completely to try to save some power. +- lost distance : at which distance from path are you considered to be lost ? +- wake-up speed : if you drive below this speed powersaving will disable itself +- active-time : how long (in seconds) the screen should be turned on if activated before going back to sleep. - brightness : how bright should screen be ? (by default 0.5, again saving power) - power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points, when you touch the screen and when speed is below 13km/h. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 56137ae58..46e29c359 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -26,6 +26,8 @@ let s = require("Storage"); var settings = Object.assign( { lost_distance: 50, + wake_up_speed: 13, + active_time: 10, brightness: 0.5, buzz_on_turns: false, disable_bluetooth: true, @@ -692,7 +694,7 @@ class Status { if (!this.active || !powersaving) { return; } - if (getTime() - this.last_activity > 30) { + if (getTime() - this.last_activity > settings.active_time) { this.active = false; Bangle.setLCDBrightness(0); if (settings.power_lcd_off) { @@ -772,13 +774,13 @@ class Status { if (in_menu) { return; } - if (this.instant_speed * 3.6 < 13) { + if (this.instant_speed * 3.6 < settings.wake_up_speed) { this.activate(); // if we go too slow turn on, we might be looking for the direction to follow if (!this.default_options) { this.default_options = true; Bangle.setOptions({ - lockTimeout: 10000, + lockTimeout: 0, backlightTimeout: 10000, wakeOnTwist: true, powerSave: true, @@ -798,7 +800,6 @@ class Status { wakeOnTouch: true, powerSave: false, }); - Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000) } } this.check_activity(); // if we don't move or are in menu we should stay on @@ -879,8 +880,10 @@ class Status { // }, time_to_next_point); // } // } - if (this.reaching != next_point && this.distance_to_next_point <= 100) { + if (this.distance_to_next_point <= 100) { this.activate(); + } + if (this.reaching != next_point && this.distance_to_next_point <= 100) { this.reaching = next_point; let reaching_waypoint = this.path.is_waypoint(next_point); if (reaching_waypoint) { @@ -1029,8 +1032,8 @@ class Status { let distance_per_pixel = displayed_length / graph_width; let start_point_index = 0; - let end_point_index = this.remaining_distances.length - 1; - for (let i = 0; i < this.remaining_distances.length; i++) { + let end_point_index = this.path.len - 1; + for (let i = 0; i < this.path.len; i++) { let point_distance = path_length - this.remaining_distances[i]; if (point_distance <= display_start) { start_point_index = i; @@ -1040,6 +1043,7 @@ class Status { break; } } + end_point_index = Math.min(end_point_index+1, this.path.len -1); let max_height = Number.NEGATIVE_INFINITY; let min_height = Number.POSITIVE_INFINITY; for (let i = start_point_index; i <= end_point_index; i++) { diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index 395b1ac93..1b030f5cd 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -3,6 +3,8 @@ // Load settings var settings = Object.assign({ lost_distance: 50, + wake_up_speed: 13, + active_time: 10, buzz_on_turns: false, disable_bluetooth: true, brightness: 0.5, @@ -44,6 +46,24 @@ writeSettings(); }, }, + "wake-up speed": { + value: settings.wake_up_speed, + min: 0, + max: 30, + onchange: (v) => { + settings.wake_up_speed = v; + writeSettings(); + }, + }, + "active time": { + value: settings.active_time, + min: 5, + max: 60, + onchange: (v) => { + settings.active_time = v; + writeSettings(); + }, + }, "brightness": { value: settings.brightness, min: 0, From c645b3110504d38e9b05ebd7b5ba8384ca486652 Mon Sep 17 00:00:00 2001 From: Niko Komin Date: Wed, 30 Aug 2023 11:06:26 -0700 Subject: [PATCH 238/242] corrected screenshot --- apps/nightwatch/screenshot.png | Bin 3608 -> 3660 bytes apps/nightwatch/screenshot2.png | Bin 3795 -> 3855 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/nightwatch/screenshot.png b/apps/nightwatch/screenshot.png index 194c91b28c327b6ca06d06515b1dd81197411ed9..3f524eac9e1402c8ae66f48f312f8748d41cb628 100644 GIT binary patch literal 3660 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 literal 3608 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 literal 3795 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 From 30de2cc6b2e9bc2f1fa268f1d3352c3f7f3b0723 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 31 Aug 2023 09:32:55 +0200 Subject: [PATCH 239/242] gipy: documentation update --- apps/gipy/README.md | 36 +++++++++++++++++++++++++++++++++--- apps/gipy/metadata.json | 2 +- apps/gipy/shot.png | Bin 0 -> 4703 bytes 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 apps/gipy/shot.png diff --git a/apps/gipy/README.md b/apps/gipy/README.md index d96461dfe..f4c68d027 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -42,7 +42,7 @@ also a nice open source option. Note that *mapstogpx* has a super nice feature in its advanced settings. You can turn on 'next turn info' and be warned by the watch when you need to turn. -Once you have your gpx file you need to convert it to *gpc* which is my custom file format. +Once you have your gpx file you need to convert it to *gps* which is my custom file format. They are smaller than gpx and reduce the number of computations left to be done on the watch. Just click the disk icon and select your gpx file. @@ -82,8 +82,7 @@ On your screen you can see: ### Lost -If you stray away from path we will rescale the display to continue displaying nearby segments and -display the direction to follow as a purple segment. Your main position will also turn to purple. +If you stray away from path we will display the direction to follow as a purple segment. Your main position will also turn to purple. Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you are. On path it just needed to scan a few points ahead and behind. @@ -104,6 +103,11 @@ for the whole path. ![Screenshot](heights.png) Colors correspond to slopes. +Above 15% will be red, above 8% orange, above 3% yellow, below 3% and -3% is green and shades of blue +are for descents. + +You should note that the precision is not very good. The input data is not very precise and you only get the +slopes between path points. Don't expect to see small bumps on the road. ### Settings @@ -118,6 +122,32 @@ Few settings for now (feel free to suggest me more) : - power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points, when you touch the screen and when speed is below 13km/h. +### Powersaving + +Starting with release 0.20 we experiment with power saving. + +There are now two display modes : + +- active : the screen is lit back (default at 50% light but can be configured with the *brightness* setting) +- inactive : by default the screen is not lit but you can also power it off completely (with the *power lcd off* setting) + +The algorithm works in the following ways : + +- some events will *activate* : the display will turn *active* +- if no activation event occur for at least 10 seconds (or *active-time* setting) we switch back to *inactive* + +Activation events are the following : + +- you are near (< 100m) the next point on path +- you are slow (< *wake-up speed* setting (13 km/h by default)) +- you press the button / touch the screen + + +Power saving has been tested on a very long trip with several benefits + +- longer battery life +- waking up near path points will attract your attention more easily when needed + ### Caveats It is good to use but you should know : diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 3526e1afa..d6b5e1405 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -8,7 +8,7 @@ "icon": "gipy.png", "type": "app", "tags": "tool,outdoors,gps", - "screenshots": [{"url":"splash.png"}], + "screenshots": [{"url":"splash.png"}, {"url":"heights.png"}, {"url":"shot.png"}], "supports": ["BANGLEJS2"], "readme": "README.md", "interface": "interface.html", diff --git a/apps/gipy/shot.png b/apps/gipy/shot.png new file mode 100644 index 0000000000000000000000000000000000000000..c2ffea7241be4305f60e338965c3a4f78131cbad GIT binary patch literal 4703 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 hxtE Date: Thu, 31 Aug 2023 10:36:02 +0200 Subject: [PATCH 240/242] gipy: typo fix --- apps/gipy/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index f4c68d027..8539167e1 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -103,7 +103,7 @@ for the whole path. ![Screenshot](heights.png) Colors correspond to slopes. -Above 15% will be red, above 8% orange, above 3% yellow, below 3% and -3% is green and shades of blue +Above 15% will be red, above 8% orange, above 3% yellow, between 3% and -3% is green and shades of blue are for descents. You should note that the precision is not very good. The input data is not very precise and you only get the From 7d636013556d934b9631e4db99a1cdb11c97df22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sarah=20Fjelsted=20Alr=C3=B8e?= Date: Sun, 3 Sep 2023 21:50:54 +0200 Subject: [PATCH 241/242] Added app 'Rebble Agenda' --- apps/rebbleagenda/ChangeLog | 1 + apps/rebbleagenda/README.md | 24 + apps/rebbleagenda/app-icon.js | 1 + apps/rebbleagenda/app.js | 583 ++++++++++++++++++ apps/rebbleagenda/app.png | Bin 0 -> 479 bytes apps/rebbleagenda/metadata.json | 26 + .../screenshot_rebbleagenda_customtheme.png | Bin 0 -> 2878 bytes .../screenshot_rebbleagenda_events.png | Bin 0 -> 2654 bytes .../screenshot_rebbleagenda_sun.png | Bin 0 -> 2430 bytes apps/rebbleagenda/settings.js | 69 +++ 10 files changed, 704 insertions(+) create mode 100644 apps/rebbleagenda/ChangeLog create mode 100644 apps/rebbleagenda/README.md create mode 100644 apps/rebbleagenda/app-icon.js create mode 100644 apps/rebbleagenda/app.js create mode 100644 apps/rebbleagenda/app.png create mode 100644 apps/rebbleagenda/metadata.json create mode 100644 apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png create mode 100644 apps/rebbleagenda/screenshot_rebbleagenda_events.png create mode 100644 apps/rebbleagenda/screenshot_rebbleagenda_sun.png create mode 100644 apps/rebbleagenda/settings.js diff --git a/apps/rebbleagenda/ChangeLog b/apps/rebbleagenda/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/rebbleagenda/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/rebbleagenda/README.md b/apps/rebbleagenda/README.md new file mode 100644 index 000000000..77afd4b48 --- /dev/null +++ b/apps/rebbleagenda/README.md @@ -0,0 +1,24 @@ +# Rebble Agenda + +Agenda app for showing upcoming events in an animated fashion. +Heavily inspired by the inbuilt agenda of the pebble time. +Switch between calendar events by swiping up or down. Click the button to exit. + +![Two events shown using the default light system theme](./screenshot_rebbleagenda_events.png) ![The last event of the agenda shown using a custom red theme](./screenshot_rebbleagenda_customtheme.png) ![An animated sun shows the day of the following events](./screenshot_rebbleagenda_sun.png) + +## Settings + +- *Use system theme* - Use the colors of the system theme. Otherwise use following colors. +- *Accent* - The color of the rightmost accent bar if not following system theme. +- *Background* - The background color to use if not following system theme. +- *Foreground* - The foreground color to use if not following system theme. + +## Notes + +- The weather icon in the top right corner is currently just showing the current weather as provided by [weather](https://github.com/espruino/BangleApps/blob/master/apps/weather/). Closest forecast to be implemented in a future release. +- Events only show as much of their title and description as can be fit on the screen, which is one and four (wrapped) lines respectively. +- Events are loaded from ```android.calendar.json```, which is read in its entirety. If you have a very busy schedule, loading may take a second or two. + +## Creator + +- [Sarah Alrøe](https://github.com/SarahAlroe), August+September 2023 diff --git a/apps/rebbleagenda/app-icon.js b/apps/rebbleagenda/app-icon.js new file mode 100644 index 000000000..2b2773ee7 --- /dev/null +++ b/apps/rebbleagenda/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/ADscjgRhDhgePCKIv1hgAEDoYJFAA4RJFyQvcGBYRGlIdDlIuLCJgvQggdDggvLCJgv/PoOgDgOgR5oRLF6AeBgkEFxgRNF6IAdF/4vpjwAkF/4v/F/4vRjgAEA6Iv/F/4v/F/4RHADov/Q6MMAAgv/F/4v/F/4vJADov/F/4v/F5IwkFxQwjFxgA/AH4A/AH4AZA")) \ No newline at end of file diff --git a/apps/rebbleagenda/app.js b/apps/rebbleagenda/app.js new file mode 100644 index 000000000..3b6eca900 --- /dev/null +++ b/apps/rebbleagenda/app.js @@ -0,0 +1,583 @@ +{ + /* Requires */ + const weather = require('weather'); + require("Font6x12").add(Graphics); + require("Font8x16").add(Graphics); + const SETTINGS_FILE = "rebbleagenda.json"; + const settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {'system':true, 'bg': '#fff','fg': '#000','acc': '#0FF'}; + + /* Layout consts */ + const MARKER_SIZE = 4; + const BORDER_SIZE = 6; + const WIDGET_SIZE = 24; + const PRIMARY_OFFSET = WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE - 20 / 2; + const SECONDARY_OFFSET = g.getHeight() - WIDGET_SIZE - 16 - 20; + const MARKER_POS_UPPER = Uint8Array([g.getWidth() - BORDER_SIZE - MARKER_SIZE, WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE]); + const PIN_SIZE = 10; + const ACCENT_WIDTH = 2 * BORDER_SIZE + 2 * MARKER_SIZE; // �=2r, borders each side. + + const TEXT_COLOR = settings.system?g.theme.fg:settings.fg; + const BG_COLOR = settings.system?g.theme.bg:settings.bg; + const ACCENT_COLOR = settings.system?g.theme.bgH:settings.acc; + const SUN_COLOR_START = 0xF800; + const SUN_COLOR_END = 0xFFE0; + const SUN_FACE = 0x0000; + + /* Animation polygon sets*/ + const CLEAR_POLYS_1 = [ + new Uint8Array([0, 176, 0, 0, 176, 0, 176, 0, 0, 0, 0, 176]), + new Uint8Array([0, 176, 0, 0, 176, 0, 170, 7, 10, 12, 7, 168]), + new Uint8Array([0, 176, 0, 0, 176, 0, 139, 49, 41, 45, 43, 125]), + new Uint8Array([0, 176, 0, 0, 176, 0, 90, 81, 82, 86, 85, 94]), + new Uint8Array([0, 176, 0, 0, 176, 0, 91, 85, 85, 85, 85, 91]) + ]; + + const CLEAR_POLYS_2 = [ + new Uint8Array([0, 176, 176, 176, 176, 0, 176, 0, 176, 176, 0, 176]), + new Uint8Array([0, 176, 176, 176, 176, 0, 170, 7, 162, 161, 7, 168]), + new Uint8Array([0, 176, 176, 176, 176, 0, 139, 49, 130, 126, 43, 125]), + new Uint8Array([0, 176, 176, 176, 176, 0, 90, 81, 95, 89, 85, 94]), + new Uint8Array([0, 176, 176, 176, 176, 0, 91, 85, 91, 91, 85, 91]) + ]; + + const BREATHING_POLYS = [ + new Uint8Array([72, 88, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 84, 88]), + new Uint8Array([63, 88, 64, 73, 78, 73, 78, 73, 78, 73, 78, 73, 92, 73, 93, 88]), + new Uint8Array([60, 88, 56, 76, 78, 60, 78, 60, 78, 60, 78, 60, 100, 76, 96, 88]), + new Uint8Array([56, 88, 50, 78, 64, 54, 78, 54, 78, 54, 92, 54, 106, 78, 100, 88]), + new Uint8Array([53, 88, 47, 80, 52, 53, 78, 41, 78, 41, 104, 53, 109, 80, 103, 88]), + new Uint8Array([50, 88, 43, 81, 43, 51, 63, 32, 92, 32, 113, 51, 113, 81, 106, 88])]; + const SUN_EYE_LEFT_POLY = new Uint8Array([56, 52, 64, 44, 72, 52, 72, 55, 69, 54, 64, 50, 58, 55, 56, 55]); + const SUN_EYE_RIGHT_OFFSET = 30; + const MOUTH_POLY = new Uint8Array([78, 77, 68, 75, 67, 73, 69, 71, 78, 73, 87, 71, 89, 73, 88, 75]); + + /* Animation timings */ + const TIME_CLEAR_ANIM = 400; + const TIME_CLEAR_BREAK = 10; + const TIME_DEFAULT_ANIM = 300; + const TIME_BUMP_ANIM = 200; + const TIME_EXIT_ANIM = 500; + const TIME_EVENT_CHANGE = 150; + const TIME_EVENT_BREAK_IN = 300; + const TIME_EVENT_BREAK_ANIM = 800; + const TIME_EVENT_BREAK_HALT = 500; + const TIME_EVENT_BREAK_OUT = 500; + + /* Utility functions */ + + /** + * Check if two dates occur on the same day + * @param {Date} d1 The first date to compare + * @param {Date} d2 The second date to compare + * @returns {Boolean} The two dates are on the same day + */ + const isSameDay = function (d1, d2) { + return (d1.getDate() == d2.getDate() && d1.getMonth() == d2.getMonth() && d1.getFullYear() == d2.getFullYear()); + }; + + /** + * Apply sinusoidal easing to a value 0-1 + * @param {Number} x Number to ease + * @returns {Number} Ease of x + */ + const ease = function (x) { + "jit"; + return 1 - (Math.cos(Math.PI * x) + 1) / 2; + }; + + /** + * Map from 0-1 to a number interval + * @param {Number} outMin Minimum output number + * @param {Number} outMax Maximum output number + * @param {Number} x Number between 0 and 1 to map from + * @returns {Number} x mapped between min and max + */ + const map = function (outMin, outMax, x) { + "jit"; + return outMin + x * (outMax - outMin); + }; + + /** + * Return [0-1] progress through an interval + * @param {Number} start When the interval was started in ms + * @param {Number} end When the interval is supposed to stop in ms + * @returns {Number} Value between 0 and 1 reflecting progress through interval + */ + const timeProgress = function (start, end) { + "jit"; + const length = end - start; + const delta = Date.now() - start; + return Math.min(Math.max(delta / length, 0), 1); + }; + + /** + * Interpolate between sets of polygon coordinates + * @param {Array} polys An array of arrays, each containing an equally long set of coordinates + * @param {Number} pos Progress through interpolation [0-1] + * @returns {Array} Interpolation between the two closest sets of coordinates + */ + const interpolatePoly = function (polys, pos) { + const span = polys.length - 1; + pos = pos * span; + pos = pos > span ? span : pos; + const upper = polys[Math.ceil(pos)]; + const lower = polys[Math.floor(Math.max(pos - 0.000001, 0))]; + const interp = pos - Math.floor(pos - 0.000001); + return upper.map((up, i) => { + return Math.round(up * interp + lower[i] * (1 - interp)); + }); + }; + + /** + * Repeatedly call callback with progress through an interval of length time + * @param {Function} anim Callback which takes i, animation progress [0-1] + * @param {Number} time How many ms the animation should last + * @returns {void} + */ + const doAnim = function (anim, time) { + const animStart = Date.now(); + const animEnd = animStart + time; + let i = 0; + do { + i = timeProgress(animStart, animEnd); + anim(i); + } while (i < 1); + anim(1); + }; + + /* Screen draw functions */ + + /** + * Draw an event + * @param {Number} index Index in the events array of event to draw + * @param {Number} yOffset Vertical pixel offset of the draw + * @param {Boolean} drawSecondary Should secondary event be drawn if possible? + */ + const drawEvent = function (index, yOffset, drawSecondary) { + g.setColor(TEXT_COLOR); + // Draw the event time + g.setFontAlign(-1, -1, 0); + g.setFont("Vector", 20); + g.drawString(events[index].time, BORDER_SIZE, PRIMARY_OFFSET + yOffset); + + // Draw the event title + g.setFont("8x16"); + g.drawString(events[index].title, BORDER_SIZE, PRIMARY_OFFSET + 20 + yOffset); + + // And the event description + g.setFont("6x12"); + g.drawString(events[index].description, BORDER_SIZE, PRIMARY_OFFSET + 20 + 12 + 2 + yOffset); + + // Draw a secondary event if asked to and exists + if (drawSecondary) { + if (index + 1 < events.length) { + if (events[index].date != events[index + 1].date) { + // If event belongs to another day, draw circle + g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE + yOffset, MARKER_SIZE); + } else { + // Draw event time and title + g.setFont("Vector", 20); + g.drawString(events[index + 1].time, BORDER_SIZE, SECONDARY_OFFSET + yOffset); + g.setFont("8x16"); + g.drawString(events[index + 1].title, BORDER_SIZE, SECONDARY_OFFSET + 20 + yOffset); + } + } else { + // If no more events exist, draw end + g.setFontAlign(0, 1, 0); + g.setFont("Vector", 20); + g.drawString("End", (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - BORDER_SIZE + yOffset); + } + } + }; + + /** + * Draw a two-line caption beneath a figure (Just beneath centre) + * @param {String} first Top string to draw + * @param {String} second Bottom string to draw + * @param {Number} yOffset Vertical pixel offset of the draw + */ + const drawFigureCaption = function (first, second, yOffset) { + g.setFontAlign(0, -1, 0); + g.setFont("Vector", 18); + g.setColor(TEXT_COLOR); + g.drawString(first, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + yOffset); + g.drawString(second, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + 20 + yOffset); + }; + + /** + * Clear the contents area of the default layout + */ + const clearContent = function () { + g.setColor(BG_COLOR); + g.fillRect(0, 0, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight()); + }; + + /** + * Draw the sun figure (above centre, in content area) + * @param {Number} progress Progress through the sun expansion animation, between 0 and 1 + * @param {Number} yOffset Vertical pixel offset of the draw + */ + const drawSun = function (progress, yOffset) { + const p = ease(progress); + const sunColor = progress == 1 ? SUN_COLOR_END : g.blendColor(SUN_COLOR_START, SUN_COLOR_END, p); + g.setColor(sunColor); + g.fillPoly(g.transformVertices(interpolatePoly(BREATHING_POLYS, p), { y: yOffset })); + + if (progress > 0.6) { + const faceP = ease((progress - 0.6) * 2.5); + g.setColor(g.blendColor(sunColor, SUN_FACE, faceP)); + g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { y: map(20, 0, faceP) + yOffset })); + g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { x: SUN_EYE_RIGHT_OFFSET, y: map(20, 0, faceP) + yOffset })); + g.fillPoly(g.transformVertices(MOUTH_POLY, { y: map(10, 0, faceP) + yOffset })); + } + + g.setColor(TEXT_COLOR); + g.fillRect({ + x: map((g.getWidth() - ACCENT_WIDTH) / 2 - MARKER_SIZE, 20, p), + y: map(g.getHeight() / 2 - MARKER_SIZE, g.getHeight() / 2 - MARKER_SIZE / 2, p) + yOffset, + x2: map((g.getWidth() - ACCENT_WIDTH) / 2 + MARKER_SIZE, (g.getWidth() - ACCENT_WIDTH) - 20, p), + y2: map(g.getHeight() / 2 + MARKER_SIZE / 2, g.getHeight() / 2, p) + yOffset + }); + }; + + /* Animation functions */ + + /** + * Animate clearing the screen to accent color with a single dot in the middle + */ + const animClearScreen = function () { + let oldPoly1 = CLEAR_POLYS_1[0]; + let oldPoly2 = CLEAR_POLYS_2[0]; + doAnim(i => { + i = ease(i); + poly1 = interpolatePoly(CLEAR_POLYS_1, i); + poly2 = interpolatePoly(CLEAR_POLYS_2, i); + // Fill in black line + g.setColor(TEXT_COLOR); + g.fillPoly(poly1); + g.fillPoly(poly2); + + // Fill in outer shape + g.setColor(ACCENT_COLOR); + g.fillPoly(oldPoly1); + g.fillPoly(oldPoly2); + g.flip(); + + // Save poly for next loop outer shape + oldPoly1 = poly1; + oldPoly2 = poly2; + }, TIME_CLEAR_ANIM); + + // Draw circle + g.setColor(TEXT_COLOR); + g.fillCircle(g.getWidth() / 2, g.getHeight() / 2, MARKER_SIZE); + g.flip(); + }; + + /** + * Animate from a cleared screen and dot to the default layout + */ + const animDefaultScreen = function () { + doAnim(i => { + // Draw the circle moving into the corner + i = ease(i); + const circleX = map(g.getWidth() / 2, MARKER_POS_UPPER[0], i); + const circleY = map(g.getHeight() / 2, MARKER_POS_UPPER[1], i); + g.setColor(TEXT_COLOR); + g.fillCircle(circleX, circleY, MARKER_SIZE); + + // Move the background poly in from the left + g.setColor(BG_COLOR); + const accentX = map(0, g.getWidth() - ACCENT_WIDTH, i); + g.fillPoly([0, 0, accentX, 0, accentX, MARKER_POS_UPPER[1] - PIN_SIZE, accentX - PIN_SIZE, MARKER_POS_UPPER[1], accentX, MARKER_POS_UPPER[1] + PIN_SIZE, accentX, 176, 0, 176]); + g.flip(); + + // Clear the circle for the next loop + g.setColor(ACCENT_COLOR); + g.fillCircle(circleX, circleY, MARKER_SIZE + 2); + }, TIME_DEFAULT_ANIM); + + // Finish up the circle + const w = weather.get(); + if (w && (w.code || w.txt)) { + doAnim(i => { + weather.drawIcon(w, MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * 2); + g.setColor(TEXT_COLOR); + g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * ease(1 - i)); + g.flip(); + }, 100); + } else { + g.setColor(TEXT_COLOR); + g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE); + } + }; + + /** + * Animate the sun figure expand or shrink fully + * @param {Number} direction Direction in which to animate. +1 = Expand. -1 = Shrink + */ + const animSun = function (direction) { + doAnim(i => { + // Clear and redraw just the sun area + g.setColor(BG_COLOR); + g.fillRect(0, 31, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight() / 2 + 4); + drawSun((direction == 1 ? 0 : 1) + i * direction, 0); + g.flip(); + }, TIME_EVENT_BREAK_ANIM); + }; + + /** + * Animate from centre dot to an event or backwards. Used for entering (forwards) or leaving (backwards) the day-change animation + * @param {Number} index Index of the event to draw animate in or out + * @param {Number} direction Direction of the animation. +1 = Event -> Dot. -1 = Dot -> Event + */ + const animEventToMarker = function (index, direction) { + doAnim(i => { + let ei = direction == 1 ? ease(i) : ease(1 - i); + clearContent(); + drawEvent(index, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ei, false); + g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, map(g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE, g.getHeight() / 2, ei), MARKER_SIZE); + g.flip(); + }, TIME_EVENT_BREAK_IN); + + }; + + /** + * Blit the current contents of content area out of screen, replacing it with something. Currently only for moving stuff upwards. + * @param {Function} thing Callback for the new thing to draw on the screen + * @param {Number} time How long the animation should last + */ + const animBlitToX = function (thing, time) { + let oldI = 0; + doAnim(i => { + // Move stuff out of frame, index into frame + g.blit({ + x1: 0, + y1: 0, + w: g.getWidth() - ACCENT_WIDTH - PIN_SIZE, + h: ease(1 - oldI) * g.getHeight(), + x2: 0, + y2: - (ease(i) - ease(oldI)) * g.getHeight(), + setModified: true + }); + g.setColor(BG_COLOR); + // Only clear where old stuff no longer is + g.fillRect(0, g.getHeight() * (1 - ease(i)), g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight()); + thing(i); + g.flip(); + oldI = i; + }, time); + }; + + /** + * Transition between one event and another, showing a day-change animation if needed + * @param {Number} startIndex The event index that we are animating out of + * @param {Number} endIndex The event index that we are animating into + */ + const animEventTransition = function (startIndex, endIndex) { + if (events[startIndex].date == events[endIndex].date) { + // If both events are within the same day, just scroll from one to the other. + // First determine which event is on top and which direction we are animating in + let topIndex = (startIndex < endIndex) ? startIndex : endIndex; + let botIndex = (startIndex < endIndex) ? endIndex : startIndex; + let direction = (startIndex < endIndex) ? 1 : -1; + let offset = (startIndex < endIndex) ? 0 : 1; + + doAnim(i => { + // Animate the two events moving towards their destinations + clearContent(); + drawEvent(topIndex, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), false); + drawEvent(botIndex, (SECONDARY_OFFSET - PRIMARY_OFFSET) - (SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), true); + g.flip(); + }, TIME_EVENT_CHANGE); + + // Finally, reset contents and redraw for good measure + clearContent(); + drawEvent(endIndex, 0, true); + g.flip(); + } else { + // The events are on different days, trigger day-change animation + if (startIndex < endIndex) { + // Destination is later, Stuff moves upwards + animEventToMarker(startIndex, 1); // The day-end dot moves to center of screen + drawFigureCaption(events[endIndex].weekday, events[endIndex].date, 0); // Caption between sun appears, no need to continuously redraw + animSun(1); // Animate the sun expanding + doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment + animBlitToX(i => { drawEvent(endIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT); // Blit the sun and caption out, replacing with destination event + } else { + // Destination is earlier, content moves downwards + doAnim(i => { + // Can't animBlit, draw sun and figure caption replacing origin event + clearContent(); + drawEvent(startIndex, g.getHeight() * ease(i), true); + drawSun(1, - g.getHeight() * ease(1 - i)); + drawFigureCaption(events[endIndex].weekday, events[endIndex].date, - g.getHeight() * ease(1 - i)); + g.flip(); + }, TIME_EVENT_BREAK_OUT); + doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment + animSun(-1); // Collapse the sun + animEventToMarker(endIndex, -1); // Animate from dot to destination event + } + } + g.flip(); + }; + + /** + * Bump the event because we've reached an end + * @param {Number} index The index of the event which we are currently at (probably last) + * @param {Number} direction Which direction to bump. +1 = content moves down, then up. -1 = content moves up, back down + */ + const animEventBump = function (index, direction) { + doAnim(i => { + clearContent(); + drawEvent(index, Math.sin(Math.PI * i) * 24 * direction, true); + g.flip(); + }, TIME_BUMP_ANIM); + }; + + /** + * Run the exit animation of the application + */ + const animExit = function () { + // First, move out (downwards) the current event + doAnim(i => { + clearContent(); + drawEvent(currentEventIndex, ease(i) * g.getHeight(), true); + g.flip(); + }, TIME_EXIT_ANIM / 3 * 2); + + // Clear the screen leftwards with the accent color + g.setColor(ACCENT_COLOR); + doAnim(i => { + g.fillRect(ease(1 - i) * g.getWidth(), 0, g.getWidth(), g.getHeight()); + g.flip(); + }, TIME_EXIT_ANIM / 3); + }; + + /** + * Animate from empty default screen to the first event to show. + * If the event we're moving to is not later today, show the date first. + */ + const animFirstEvent = function () { + if (!isSameDay(new Date(events[currentEventIndex].timestamp * 1000), new Date())) { + drawFigureCaption(events[currentEventIndex].weekday, events[currentEventIndex].date, 0); + animSun(1); + doAnim(i => { }, TIME_EVENT_BREAK_HALT); + animBlitToX(i => { drawEvent(currentEventIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT, 1); + } else { + drawEvent(currentEventIndex, 0, true); + } + }; + + /* Setup */ + + /* Load events */ + const today = new Date(); + const tomorrow = new Date(); + const yesterday = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + yesterday.setDate(yesterday.getDate() - 1); + g.setFont("6x12"); + const locale = require("locale"); + + let events = (require("Storage").readJSON("android.calendar.json", true) || []).map(event => { + // Title uses 8x16 font, 8 px wide characters. Limit title to fit on a line. + let title = event.title; + if (title.length > (g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) { + title = title.slice(0, ((g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) - 3) + "..."; + } + + // Wrap description to fit four lines of content + let description = g.wrapString(event.description, g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH - PIN_SIZE).slice(0, 4).join("\n"); + + // Set weekday text + let eventDate = new Date(event.timestamp * 1000); + let weekday = locale.dow(eventDate); + if (isSameDay(eventDate, today)) { + weekday = /*LANG*/"Today"; + } else if (isSameDay(eventDate, tomorrow)) { + weekday = /*LANG*/"Tomorrow"; + } else if (isSameDay(eventDate, yesterday)) { + weekday = /*LANG*/"Yesterday"; + } + + return { + timestamp: event.timestamp, + weekday: weekday, + date: locale.date(eventDate, 1), + time: locale.time(eventDate, 1) + locale.meridian(eventDate), + title: title, + description: description + }; + }).sort((a, b) => { return a.timestamp - b.timestamp; }); + + // If no events, add a note. + if (events.length == 0) { + events[0] = { + timestamp: Date.now() / 1000, + weekday: /*LANG*/"Today", + date: require("locale").date(new Date(), 1), + time: require("locale").time(new Date(), 1), + title: /*LANG*/"No events", + description: /*LANG*/"Nothing to do" + }; + } + + // We should start at the first event later than now + let currentEventIndex = events.findIndex((event) => { return event.timestamp * 1000 > Date.now(); }); + if (currentEventIndex == -1) currentEventIndex = 0; // Or just first event if none found + + // Setup the UI with remove to support fast load + Bangle.setUI({ + mode: "custom", + btn: () => { animExit(); Bangle.load(); }, + remove: function () { + require("widget_utils").show(); + delete Graphics.prototype.Font6x12; + delete Graphics.prototype.Font8x16; + Bangle.removeListener('swipe', onSwipe); + }, + }); + + /** + * Callback for swipe gesture. Transitions between adjacent events. + * @param {Number} directionLR Unused. + * @param {Number} directionUD Whether swipe direction is up or down + */ + const onSwipe = function (directionLR, directionUD) { + if (directionUD == -1) { + // Swiping up + if (currentEventIndex + 1 < events.length) { + // Animate to the next event + animEventTransition(currentEventIndex, currentEventIndex + 1); + currentEventIndex += 1; + } else { + // We've hit the end, bump + animEventBump(currentEventIndex, -1); + } + } else if (directionUD == 1) { + //Swiping down + if (currentEventIndex > 0) { + // Animate to the previous event + animEventTransition(currentEventIndex, currentEventIndex - 1); + currentEventIndex -= 1; + } else { + // If swiping earlier than earliest event, exit back to watchface + animExit(); + Bangle.load(); + } + } + }; + + // Ready animations for showing the first event, then register swipe listener for switching events + setTimeout(() => { + animDefaultScreen(); + animFirstEvent(); + Bangle.on('swipe', onSwipe); + }, TIME_CLEAR_ANIM + TIME_CLEAR_BREAK); + animClearScreen(); // Start visible changes by clearing the screen + + // Load and hide widgets to background + Bangle.loadWidgets(); + require("widget_utils").hide(); +} \ No newline at end of file diff --git a/apps/rebbleagenda/app.png b/apps/rebbleagenda/app.png new file mode 100644 index 0000000000000000000000000000000000000000..20715656561dcb8df20a8ebd30c1bba33e7e0335 GIT binary patch literal 479 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 literal 0 HcmV?d00001 diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_events.png b/apps/rebbleagenda/screenshot_rebbleagenda_events.png new file mode 100644 index 0000000000000000000000000000000000000000..c94c0d9c4747b84a0d15d63d77328b5974216b05 GIT binary patch literal 2654 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 { + s.system = v; + save(); + }, + }, + /*LANG*/'Accent': { + value: 0 | color_codes.indexOf(s.acc), + min: 0, max: color_codes.length-1, + format: v => color_options[v], + onchange: v => { + s.acc = color_codes[v]; + save(); + }, + }, + /*LANG*/'Background': { + value: 0 | ground_codes.indexOf(s.bg), + min: 0, max: ground_codes.length-1, + format: v => ground_options[v], + onchange: v => { + s.bg = ground_codes[v]; + save(); + }, + }, + /*LANG*/'Foreground': { + value: 0 | ground_codes.indexOf(s.fg), + min: 0, max: ground_codes.length-1, + format: v => ground_options[v], + onchange: v => { + s.fg = ground_codes[v]; + save(); + }, + } + }); +}); \ No newline at end of file From 75f41f705d02b3964cf5f35d4424e3ad67940703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sarah=20Fjelsted=20Alr=C3=B8e?= Date: Sun, 3 Sep 2023 22:19:41 +0200 Subject: [PATCH 242/242] Fixed icon palette, fixed settings key that breaks on minify, disabled emulator. --- apps/rebbleagenda/app-icon.js | 2 +- apps/rebbleagenda/metadata.json | 1 - apps/rebbleagenda/settings.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/rebbleagenda/app-icon.js b/apps/rebbleagenda/app-icon.js index 2b2773ee7..d432f8179 100644 --- a/apps/rebbleagenda/app-icon.js +++ b/apps/rebbleagenda/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwxH+AH4A/ADscjgRhDhgePCKIv1hgAEDoYJFAA4RJFyQvcGBYRGlIdDlIuLCJgvQggdDggvLCJgv/PoOgDgOgR5oRLF6AeBgkEFxgRNF6IAdF/4vpjwAkF/4v/F/4vRjgAEA6Iv/F/4v/F/4RHADov/Q6MMAAgv/F/4v/F/4vJADov/F/4v/F5IwkFxQwjFxgA/AH4A/AH4AZA")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4v/F6OIAAgHRF/4v/F/4v/CI4AdF/6HR3YAEF/4v/F/4v/F5IAdF/4v/F/4vJGEguKGEYuMAH4A/AH4A/ADIA==")) \ No newline at end of file diff --git a/apps/rebbleagenda/metadata.json b/apps/rebbleagenda/metadata.json index e6f1d0a7e..07227d3bc 100644 --- a/apps/rebbleagenda/metadata.json +++ b/apps/rebbleagenda/metadata.json @@ -13,7 +13,6 @@ "tags": "agenda,tool", "supports" : ["BANGLEJS2"], "readme": "README.md", - "allow_emulator": true, "dependencies" : { "weather":"app" }, "storage": [ {"name":"rebbleagenda.app.js","url":"app.js"}, diff --git a/apps/rebbleagenda/settings.js b/apps/rebbleagenda/settings.js index 87ba11cff..8ed2ceae5 100644 --- a/apps/rebbleagenda/settings.js +++ b/apps/rebbleagenda/settings.js @@ -30,7 +30,7 @@ E.showMenu({ '': { 'title': 'Rebble Agenda' }, - ['< '+/*LANG*/'Back']: back, + /*LANG*/'< Back': back, /*LANG*/'Use system theme': { value: !!s.system, onchange: v => {