-
If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x bootloader, the Firmware Update
- will fail with a message about the bootloader version. If so, please click here to update to bootloader 2v12 and then click the 'Upload' button that appears.
+
If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
+ will fail with a message about the DFU version. If so, please click here to update to DFU 2v12 and then click the 'Upload' button that appears.
The currently available Espruino firmware releases are:
To update, click a link above and then click the 'Upload' button that appears.
-
Advanced ▼
+
+
What is DFU? ▼
+
+
What is DFU?
+
DFU stands for Device Firmware Update . This is the first
+ bit of code that runs when Bangle.js starts, and it is able to update the
+ Bangle.js firmware. Normally you would update firmware via this Firmware
+ Updater app, but if for some reason Bangle.js will not boot, you can
+ always use DFU to to the update manually .
+
DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
+ with the Bootloader app in the app loader (which prepares Bangle.js for running apps).
+
+
+
Advanced ▼
+
Advanced
Firmware updates via this tool work differently to the NRF Connect method mentioned on
the Bangle.js 2 page . Firmware
- is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies
+ is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
the new firmware into internal Storage.
In addition to the links above, you can upload a hex or zip file directly below. This file should be an .app_hex
- file, *not* the normal .hex
(as that contains the bootloader as well).
+ file, *not* the normal
.hex
(as that contains the DFU as well).
DANGER! No verification is performed on uploaded ZIP or HEX files - you could
- potentially overwrite your bootloader with the wrong binary and brick your Bangle.
+ potentially overwrite your DFU with the wrong binary and brick your Bangle.
Upload
@@ -73,7 +87,7 @@ function onInit(device) {
document.getElementById("fw-ok").style = "";
}
Puck.eval("E.CRC32(E.memoryArea(0xF7000,0x7000))", crc => {
- console.log("Bootloader CRC = "+crc);
+ console.log("DFU CRC = "+crc);
var version = `unknown (CRC ${crc})`;
var ok = true;
if (crc==1339551013) { version = "2v10.219"; ok = false; }
@@ -299,8 +313,8 @@ function createJS_app(binary, startAddress, endAddress) {
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
- hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 2v10.219 needs update"); load();}\n`;
- hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("BOOTLOADER 2v10.236 needs update"); load();}\n`;
+ hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("DFU 2v10.219 needs update"); load();}\n`;
+ hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("DFU 2v10.236 needs update"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
hexJS += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 2048;
@@ -320,7 +334,7 @@ function createJS_app(binary, startAddress, endAddress) {
function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary);
console.log("CRC 0x"+crc.toString(16));
- hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("BOOTLOADER UP TO DATE!"); load();}\n`;
+ hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("DFU UP TO DATE!"); load();}\n`;
hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`;
var CHUNKSIZE = 1024;
for (var i=0;i
{ drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this
+}
+
+let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu
+let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason
+
+let angle = 0; // Store the angle of rotation
+let image; // Cache the image here because we access it in multiple places
+
+function drawMenu() {
+ Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu
+ Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that
+ Bangle.setLCDBrightness(backlightSetting); // Restore backlight
+ image = undefined; // Delete the image from memory
+
+ E.showMenu(imageMenu);
+}
+
+function drawImage(fileName) {
+ E.showMenu(); // Remove the menu to prevent it from breaking things
+ setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger)
+ Bangle.setOptions({ // Disable display power saving while showing the image
+ lockTimeout: 0,
+ lcdPowerTimeout: 0,
+ backlightTimeout: 0
+ });
+ Bangle.setLCDBrightness(1); // Full brightness
+
+ image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this
+ g.clear().reset().drawImage(image, 88, 88, { rotate: angle });
+}
+
+setWatch(info => {
+ if (image) {
+ if (angle == 0) angle = Math.PI;
+ else angle = 0;
+ Bangle.buzz();
+
+ g.clear().reset().drawImage(image, 88, 88, { rotate: angle })
+ }
+}, BTN1, { repeat: true });
+
+// We don't load the widgets because there is no reasonable way to unload them
+drawMenu();
\ No newline at end of file
diff --git a/apps/gallery/icon.js b/apps/gallery/icon.js
new file mode 100644
index 000000000..11fee53eb
--- /dev/null
+++ b/apps/gallery/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw"))
\ No newline at end of file
diff --git a/apps/gallery/icon.png b/apps/gallery/icon.png
new file mode 100644
index 000000000..71835e93d
Binary files /dev/null and b/apps/gallery/icon.png differ
diff --git a/apps/gallery/interface.html b/apps/gallery/interface.html
new file mode 100644
index 000000000..f309270ca
--- /dev/null
+++ b/apps/gallery/interface.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+ Existing Images
+
+
+ Convert & Upload Images
+
+
+ Use Compression?
+ Transparency to Color
+ Transparency?
+ Inverted?
+ Crop?
+ Diffusion:
+
+ Brightness:
+
+ Contrast:
+
+ Colours:
+
+
+
+ Upload
+
+
+
+
diff --git a/apps/gallery/metadata.json b/apps/gallery/metadata.json
new file mode 100644
index 000000000..0dc8d1613
--- /dev/null
+++ b/apps/gallery/metadata.json
@@ -0,0 +1,27 @@
+{
+ "id": "gallery",
+ "name": "Gallery",
+ "version": "0.02",
+ "description": "A gallery that lets you view images uploaded with the IDE (see README)",
+ "readme": "README.md",
+ "icon": "icon.png",
+ "type": "app",
+ "tags": "tools",
+ "supports": [
+ "BANGLEJS2",
+ "BANGLEJS"
+ ],
+ "allow_emulator": true,
+ "interface": "interface.html",
+ "storage": [
+ {
+ "name": "gallery.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "gallery.img",
+ "url": "icon.js",
+ "evaluate": true
+ }
+ ]
+}
diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog
new file mode 100644
index 000000000..3b0d62009
--- /dev/null
+++ b/apps/gipy/ChangeLog
@@ -0,0 +1,65 @@
+0.01: Initial code
+
+0.05:
+ * We now buzz before reaching a waypoint.
+ * Display is only updated when not locked.
+ * We detect leaving path and finding path again.
+ * We display remaining distance to next point.
+
+0.06:
+ * Special display for points with steep turns.
+ * Buzz on points with steep turns and unlock.
+ * Losing gps is now displayed.
+
+0.07:
+ * We now use orientation to detect current segment
+ when segments overlap going in both directions.
+ * File format is now versioned.
+
+0.08:
+ * Don't use gps course anymore but figure it from previous positions.
+ * Bugfix: path colors are back.
+ * Always buzz when reaching waypoint even if unlocked.
+
+0.09:
+ * We now display interest points.
+ * Menu to choose which file to load.
+
+0.10:
+ * Display performances enhancement.
+ * Waypoints information is embedded in file and extracted from comments on
+ points.
+ * Bugfix in map display (last segment was missing + wrong colors).
+ * Waypoint detections using OSM + sharp angles
+ * New algorith for direction detection
+
+0.11:
+ * Better fonts (more free space, still readable).
+ * Display direction to nearest point when lost.
+ * Display average speed.
+ * Turn off gps when locked and between points
+
+0.12:
+ * Bugfix in speed computation.
+ * Bugfix in current segment detection.
+ * Bugfix : lost direction.
+ * Larger fonts.
+ * Detecting next point correctly when going back.
+
+0.13:
+ * Bugfix in lost direction.
+ * Buzzing 100m ahead instead of 50m.
+ * Detect sharp turns.
+ * Display instant speed.
+ * New instant speed algorithm.
+ * Bugfix for remaining distance when going back.
+
+0.14:
+ * Detect starting distance to compute a good average speed.
+ * Settings
+ * Account for breaks in average speed.
+
+0.15:
+ * Record traveled distance to get a good average speed.
+ * Breaks (low speed) will not count in average speed.
+ * Bugfix in average speed.
diff --git a/apps/gipy/README.md b/apps/gipy/README.md
new file mode 100644
index 000000000..6c9b87c23
--- /dev/null
+++ b/apps/gipy/README.md
@@ -0,0 +1,109 @@
+# Gipy
+
+Gipy allows you to follow gpx traces on your watch.
+
+
+
+
+It is for now meant for bicycling and not hiking
+(it uses your movement to figure out your orientation
+and walking is too slow).
+
+It is untested on Banglejs1. If you can try it, you would be welcome.
+
+This software is not perfect but surprisingly useful.
+
+## Features
+
+It provides the following features :
+
+- display the path with current position from gps
+- detects and buzzes if you leave the path
+- buzzes before sharp turns
+- buzzes before nodes with comments
+(for example when you need to turn in https://mapstogpx.com/)
+- display instant / average speed
+- display distance to next node
+- display additional data from openstreetmap :
+ - water points
+ - toilets
+ - artwork
+ - bakeries
+
+optionally it can also:
+
+- try to turn off gps between crossroads to save battery
+
+## Usage
+
+### Preparing the file
+
+You first need to have a trace file in *gpx* format.
+Usually I download from [komoot](https://www.komoot.com/) or I export
+from google maps using [mapstogpx](https://mapstogpx.com/).
+
+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.
+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.
+This will request additional information from openstreetmap.
+Your path will be displayed in svg.
+
+### Starting Gipy
+
+Once you start gipy you will have a menu for selecting your trace (if more than one).
+Choose the one you want and here you go :
+
+
+
+On your screen you can see :
+
+- yourself (the big black dot)
+- the path (the top of the screen is in front of you)
+- if needed a projection of yourself on the path (small black dot)
+- extremities of segments as white dots
+- turning points as doubled white dots
+- some text on the left (from top to bottom) :
+ * current time
+ * left distance till end of current segment
+ * distance from start of path / path length
+ * average speed / instant speed
+- interest points from openstreetmap as color dots :
+ * red : bakery
+ * deep blue : water point
+ * cyan : toilets (often doubles as water point)
+ * 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
+- a black segment extending from you when you are lost, indicating the rough direction of where to go
+
+### Settings
+
+Few settings for now (feel free to suggest me more) :
+
+- keep gps alive : if turned off, will try to save battery by turning the gps off on long segments
+- max speed : used to compute how long to turn the gps off
+
+### Caveats
+
+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.
+- 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.
+- battery saving by turning off gps is not very well tested (disabled by default).
+- buzzing does not always work: when there is a high load on the watch, the buzzes might just never happen :-(.
+- buzzes are not strong enough to be always easily noticed.
+- be careful when **GOING DOWNHILL AT VERY HIGH SPEED**. I already missed a few turning points and by the time I realized it,
+I had to go back uphill by quite a distance.
+
+## Creator
+
+Feel free to give me feedback : is it useful for you ? what other features would you like ?
+
+frederic.wagner@imag.fr
diff --git a/apps/gipy/TODO b/apps/gipy/TODO
new file mode 100644
index 000000000..53c3530e2
--- /dev/null
+++ b/apps/gipy/TODO
@@ -0,0 +1,25 @@
+
+* bugs
+
+- when exactly on turn, distance to next point is still often 50m
+ -----> it does not buzz very often on turns
+
+- when going backwards we have a tendencing to get a wrong current_segment
+
+* 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 ?
+- dynamic map rescale
+- display scale (100m)
+
+- compress path ?
+
+* misc
+
+- code is becoming messy
diff --git a/apps/gipy/app-icon.js b/apps/gipy/app-icon.js
new file mode 100644
index 000000000..0fc51609f
--- /dev/null
+++ b/apps/gipy/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBiIA/AE8VqoAGCy1RiN3CyYuBi93uIXJIBV3AAIuMBY4XjQ5YXPRAIAEOwIABPBC4LF54wGF6IwFC5jWGIwxIJC4xJFgDuJJAxJFC6TEIJBzEHGCIYPGA5JQC44YPGBBJKY4gwRfQL4DGCL4GGCAXPGAxGBAAJIMGAwWCGCoWGC55HHJB5HIC8pGDSChfXC5AWIL5ynOC45GJC4h3IIyYwCFxwADgB1SC44uSC4guSAH4Ab"))
diff --git a/apps/gipy/app.js b/apps/gipy/app.js
new file mode 100644
index 000000000..ae82e5dfb
--- /dev/null
+++ b/apps/gipy/app.js
@@ -0,0 +1,766 @@
+let simulated = false;
+let file_version = 3;
+let code_key = 47490;
+
+var settings = Object.assign(
+ {
+ keep_gps_alive: true,
+ max_speed: 35,
+ },
+ require("Storage").readJSON("gipy.json", true) || {}
+);
+
+let interests_colors = [
+ 0xf800, // Bakery, red
+ 0x001f, // DrinkingWater, blue
+ 0x07ff, // Toilets, cyan
+ 0x07e0, // Artwork, green
+];
+
+function binary_search(array, x) {
+ let start = 0,
+ end = array.length - 1;
+
+ while (start <= end) {
+ let mid = Math.floor((start + end) / 2);
+ if (array[mid] < x) start = mid + 1;
+ else end = mid - 1;
+ }
+ return start;
+}
+
+class Status {
+ constructor(path) {
+ this.path = path;
+ this.on_path = false; // are we on the path or lost ?
+ this.position = null; // where we are
+ this.adjusted_cos_direction = null; // cos of where we look at
+ this.adjusted_sin_direction = null; // 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.paused_time = 0.0; // how long did we stop (stops don't count in avg speed)
+ this.paused_since = getTime();
+
+ 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 = this.paused_since; // time we start
+ this.advanced_distance = 0.0;
+ this.gps_coordinates_counter = 0; // how many coordinates did we receive
+ this.old_points = [];
+ this.old_times = [];
+ }
+ 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.
+ this.gps_coordinates_counter += 1;
+ let now = getTime();
+ this.old_points.push(position);
+ this.old_times.push(now);
+
+ if (this.old_points.length == 1) {
+ return null;
+ }
+
+ let last_point = this.old_points[this.old_points.length - 1];
+ let oldest_point = this.old_points[0];
+
+ // every 7 points we count the distance
+ if (this.gps_coordinates_counter % 7 == 0) {
+ let distance = last_point.distance(oldest_point);
+ if (distance < 150.0) {
+ // to avoid gps glitches
+ this.advanced_distance += distance;
+ }
+ }
+
+ if (this.old_points.length == 8) {
+ let p1 = this.old_points[0]
+ .plus(this.old_points[1])
+ .plus(this.old_points[2])
+ .plus(this.old_points[3])
+ .times(1 / 4);
+ let p2 = this.old_points[4]
+ .plus(this.old_points[5])
+ .plus(this.old_points[6])
+ .plus(this.old_points[7])
+ .times(1 / 4);
+ let t1 = (this.old_times[1] + this.old_times[2]) / 2;
+ let t2 = (this.old_times[5] + this.old_times[6]) / 2;
+ this.instant_speed = p1.distance(p2) / (t2 - t1);
+ this.old_points.shift();
+ this.old_times.shift();
+ } else {
+ this.instant_speed =
+ oldest_point.distance(last_point) / (now - this.old_times[0]);
+
+ // update paused time if we are too slow
+ if (this.instant_speed < 2) {
+ if (this.paused_since === null) {
+ this.paused_since = now;
+ }
+ } else {
+ if (this.paused_since !== null) {
+ this.paused_time += now - this.paused_since;
+ this.paused_since = null;
+ }
+ }
+ }
+ // 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) {
+ let direction = this.new_position_reached(new_position);
+ if (direction === null) {
+ if (maybe_direction === null) {
+ return;
+ } else {
+ direction = maybe_direction;
+ }
+ }
+
+ this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0);
+ this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0);
+ cos_direction = Math.cos(direction);
+ sin_direction = Math.sin(direction);
+ this.position = new_position;
+
+ // detect segment we are on now
+ let res = this.path.nearest_segment(
+ this.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)) {
+ // it did not work, try anywhere
+ res = this.path.nearest_segment(
+ this.position,
+ 0,
+ this.path.len - 1,
+ cos_direction,
+ sin_direction
+ );
+ orientation = res[0];
+ next_segment = res[1];
+ }
+ // 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) {
+ Bangle.buzz();
+ setTimeout(() => Bangle.buzz(), 500);
+ setTimeout(() => Bangle.buzz(), 1000);
+ setTimeout(() => Bangle.buzz(), 1500);
+ if (Bangle.isLocked()) {
+ Bangle.setLocked(false);
+ }
+ }
+ }
+ // re-display
+ this.display(orientation);
+ }
+ remaining_distance(orientation) {
+ let remaining_in_correct_orientation =
+ this.remaining_distances[this.current_segment + 1] +
+ this.position.distance(this.path.point(this.current_segment + 1));
+
+ if (orientation == 0) {
+ return remaining_in_correct_orientation;
+ } else {
+ return this.remaining_distances[0] - remaining_in_correct_orientation;
+ }
+ }
+ is_lost(segment) {
+ let distance_to_nearest = this.position.distance_to_segment(
+ this.path.point(segment),
+ this.path.point(segment + 1)
+ );
+ return distance_to_nearest > 50;
+ }
+ display(orientation) {
+ g.clear();
+ this.display_map();
+
+ this.display_interest_points();
+ this.display_stats(orientation);
+ Bangle.drawWidgets();
+ }
+ display_interest_points() {
+ // this is the algorithm in case we have a lot of interest points
+ // let's draw all points for 5 segments centered on current one
+ let starting_group = Math.floor(Math.max(this.current_segment - 2, 0) / 3);
+ let ending_group = Math.floor(
+ Math.min(this.current_segment + 2, this.path.len - 2) / 3
+ );
+ let starting_bucket = binary_search(
+ this.path.interests_starts,
+ starting_group
+ );
+ let ending_bucket = binary_search(
+ this.path.interests_starts,
+ ending_group + 0.5
+ );
+ // we have 5 points per bucket
+ let end_index = Math.min(
+ this.path.interests_types.length - 1,
+ ending_bucket * 5
+ );
+ for (let i = starting_bucket * 5; i <= end_index; i++) {
+ let index = this.path.interests_on_path[i];
+ let interest_point = this.path.interest_point(index);
+ let color = this.path.interest_color(i);
+ let c = interest_point.coordinates(
+ this.position,
+ this.adjusted_cos_direction,
+ this.adjusted_sin_direction
+ );
+ g.setColor(color).fillCircle(c[0], c[1], 5);
+ }
+ }
+ display_stats(orientation) {
+ let remaining_distance = this.remaining_distance(orientation);
+ let rounded_distance = Math.round(remaining_distance / 100) / 10;
+ let total = Math.round(this.remaining_distances[0] / 100) / 10;
+ let now = new Date();
+ let minutes = now.getMinutes().toString();
+ if (minutes.length < 2) {
+ minutes = "0" + minutes;
+ }
+ let hours = now.getHours().toString();
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .setColor(g.theme.fg)
+ .drawString(hours + ":" + minutes, 0, 30);
+
+ g.setFont("6x8:2").drawString(
+ "" + this.distance_to_next_point + "m",
+ 0,
+ g.getHeight() - 49
+ );
+
+ let point_time = this.old_times[this.old_times.length - 1];
+ let done_in = point_time - this.starting_time - this.paused_time;
+ let 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 (in." + approximate_instant_speed + ")",
+ 0,
+ g.getHeight() - 15
+ );
+
+ g.setFont("6x8:2").drawString(
+ "" + rounded_distance + "/" + total,
+ 0,
+ g.getHeight() - 32
+ );
+
+ 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_map() {
+ // 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 from 400ms to 200ms
+ let start = Math.max(this.current_segment - 4, 0);
+ let end = Math.min(this.current_segment + 6, this.path.len);
+ let pos = this.position;
+ let cos = this.adjusted_cos_direction;
+ let sin = this.adjusted_sin_direction;
+ let points = this.path.points;
+ let cx = pos.lon;
+ let cy = pos.lat;
+ let half_width = g.getWidth() / 2;
+ let half_height = g.getHeight() / 2;
+ let previous_x = null;
+ let previous_y = null;
+ for (let i = start; i < end; i++) {
+ let tx = (points[2 * i] - cx) * 40000.0;
+ let ty = (points[2 * i + 1] - cy) * 40000.0;
+ 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);
+ if (previous_x !== null) {
+ if (i == this.current_segment + 1) {
+ g.setColor(0.0, 1.0, 0.0);
+ } else {
+ g.setColor(1.0, 0.0, 0.0);
+ }
+ g.drawLine(previous_x, previous_y, x, y);
+
+ if (this.path.is_waypoint(i - 1)) {
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 6);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 5);
+ }
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 4);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 3);
+ }
+
+ previous_x = x;
+ previous_y = y;
+ }
+
+ if (this.path.is_waypoint(end - 1)) {
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 6);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 5);
+ }
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 4);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 3);
+
+ // now display ourselves
+ g.setColor(g.theme.fgH);
+ g.fillCircle(half_width, half_height, 5);
+
+ // display old points for direction debug
+ // for (let i = 0; i < this.old_points.length; i++) {
+ // let tx = (this.old_points[i].lon - cx) * 40000.0;
+ // let ty = (this.old_points[i].lat - cy) * 40000.0;
+ // 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((i + 1) / 4.0, 0.0, 0.0);
+ // g.fillCircle(x, y, 3);
+ // }
+
+ // display current-segment's projection for debug
+ let projection = pos.closest_segment_point(
+ this.path.point(this.current_segment),
+ this.path.point(this.current_segment + 1)
+ );
+
+ let tx = (projection.lon - cx) * 40000.0;
+ let ty = (projection.lat - cy) * 40000.0;
+ 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.fg);
+ g.fillCircle(x, y, 4);
+
+ // display direction to next point if lost
+ if (!this.on_path) {
+ let next_point = this.path.point(this.current_segment + 1);
+ let diff = next_point.minus(this.position);
+ let angle = Math.atan2(diff.lat, diff.lon);
+ let tx = Math.cos(angle) * 50.0;
+ let ty = Math.sin(angle) * 50.0;
+ 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);
+ }
+ }
+}
+
+function load_gpc(filename) {
+ let buffer = require("Storage").readArrayBuffer(filename);
+ let offset = 0;
+
+ // header
+ let header = Uint16Array(buffer, offset, 5);
+ offset += 5 * 2;
+ let key = header[0];
+ let version = header[1];
+ let points_number = header[2];
+ if (key != code_key || version > file_version) {
+ E.showMessage("Invalid gpc file");
+ load();
+ }
+
+ // path points
+ let points = Float64Array(buffer, offset, points_number * 2);
+ offset += 8 * points_number * 2;
+
+ // path waypoints
+ let waypoints_len = Math.ceil(points_number / 8.0);
+ let waypoints = Uint8Array(buffer, offset, waypoints_len);
+ offset += waypoints_len;
+
+ // interest points
+ let interests_number = header[3];
+ let interests_coordinates = Float64Array(
+ buffer,
+ offset,
+ interests_number * 2
+ );
+ offset += 8 * interests_number * 2;
+ let interests_types = Uint8Array(buffer, offset, interests_number);
+ offset += interests_number;
+
+ // interests on path
+ let interests_on_path_number = header[4];
+ let interests_on_path = Uint16Array(buffer, offset, interests_on_path_number);
+ offset += 2 * interests_on_path_number;
+ let starts_length = Math.ceil(interests_on_path_number / 5.0);
+ let interests_starts = Uint16Array(buffer, offset, starts_length);
+ offset += 2 * starts_length;
+
+ return [
+ points,
+ waypoints,
+ interests_coordinates,
+ interests_types,
+ interests_on_path,
+ interests_starts,
+ ];
+}
+
+class Path {
+ constructor(arrays) {
+ this.points = arrays[0];
+ this.waypoints = arrays[1];
+ this.interests_coordinates = arrays[2];
+ this.interests_types = arrays[3];
+ this.interests_on_path = arrays[4];
+ this.interests_starts = arrays[5];
+ }
+
+ 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;
+ }
+
+ // execute op on all segments.
+ // start is index of first wanted segment
+ // end is 1 after index of last wanted segment
+ on_segments(op, start, end) {
+ let previous_point = null;
+ for (let i = start; i < end + 1; i++) {
+ let point = new Point(this.points[2 * i], this.points[2 * i + 1]);
+ if (previous_point !== null) {
+ op(previous_point, point, i);
+ }
+ previous_point = point;
+ }
+ }
+
+ // 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);
+ }
+
+ interest_point(index) {
+ let lon = this.interests_coordinates[2 * index];
+ let lat = this.interests_coordinates[2 * index + 1];
+ return new Point(lon, lat);
+ }
+
+ interest_color(index) {
+ return interests_colors[this.interests_types[index]];
+ }
+
+ // 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];
+ this.on_segments(
+ function (p1, p2, i) {
+ // we use the dot product to figure out if oriented correctly
+ // let distance = point.fake_distance_to_segment(p1, p2);
+
+ let projection = point.closest_segment_point(p1, p2);
+ let distance = point.fake_distance(projection);
+
+ // let d = projection.minus(point).times(40000.0);
+ // let rotated_x = d.lon * acos - d.lat * asin;
+ // let rotated_y = d.lon * asin + d.lat * acos;
+ // let x = g.getWidth() / 2 - Math.round(rotated_x); // x is inverted
+ // let y = g.getHeight() / 2 + Math.round(rotated_y);
+ //
+ let diff = p2.minus(p1);
+ let dot = cos_direction * diff.lon + sin_direction * diff.lat;
+ let orientation = +(dot < 0); // index 0 is good orientation
+ // g.setColor(0.0, 0.0 + orientation, 1.0 - orientation).fillCircle(
+ // x,
+ // y,
+ // 10
+ // );
+ if (distance <= mins[orientation]) {
+ mins[orientation] = distance;
+ indices[orientation] = i - 1;
+ }
+ },
+ start,
+ end
+ );
+ // by default correct orientation (0) wins
+ // but if other one is really closer, return other one
+ if (mins[1] < mins[0] / 10.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) {
+ let translated = this.minus(current_position).times(40000.0);
+ 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),
+ ];
+ }
+ 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 d = this.minus(other_point);
+ return d.lon * d.lon + d.lat * d.lat;
+ }
+ 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));
+ }
+ 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 l2 = v.length_squared(w); // 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));
+ return v.plus(w.minus(v).times(t)); // Projection falls on the segment
+ }
+ distance_to_segment(v, w) {
+ let projection = this.closest_segment_point(v, w);
+ return this.distance(projection);
+ }
+ fake_distance_to_segment(v, w) {
+ let projection = this.closest_segment_point(v, w);
+ return this.fake_distance(projection);
+ }
+}
+
+Bangle.loadWidgets();
+
+let fake_gps_point = 0.0;
+function simulate_gps(status) {
+ if (fake_gps_point > status.path.len - 1) {
+ return;
+ }
+ let point_index = Math.floor(fake_gps_point);
+ if (point_index >= status.path.len) {
+ return;
+ }
+ //let p1 = status.path.point(0);
+ //let n = status.path.len;
+ //let p2 = status.path.point(n - 1);
+ let p1 = status.path.point(point_index);
+ 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));
+ let old_pos = status.position;
+
+ fake_gps_point += 0.05; // advance simulation
+ status.update_position(pos, null);
+}
+
+function drawMenu() {
+ const menu = {
+ "": { title: "choose trace" },
+ };
+ var files = require("Storage").list(".gpc");
+ 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);
+
+ // let path = new Path(load_gpx("test.gpx"));
+ let path = new Path(load_gpc(fn));
+ let status = new Status(path);
+
+ if (simulated) {
+ status.position = new Point(status.path.point(0));
+ setInterval(simulate_gps, 500, status);
+ } else {
+ // let's display start while waiting for gps signal
+ let p1 = status.path.point(0);
+ let p2 = status.path.point(1);
+ let diff = p2.minus(p1);
+ let direction = Math.atan2(diff.lat, diff.lon);
+ Bangle.setLocked(false);
+ status.update_position(p1, direction);
+
+ 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) {
+ status.update_position(new Point(data.lon, data.lat), null);
+ }
+ let gps_status_color;
+ if (frame % 2 == 0 || valid_coordinates) {
+ gps_status_color = g.theme.bg;
+ } else {
+ gps_status_color = g.theme.fg;
+ }
+ 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 = require("Storage").list(".gpc");
+if (files.length <= 1) {
+ if (files.length == 0) {
+ load();
+ } else {
+ start(files[0]);
+ }
+} else {
+ drawMenu();
+}
diff --git a/apps/gipy/gipy.png b/apps/gipy/gipy.png
new file mode 100644
index 000000000..e9e472f5c
Binary files /dev/null and b/apps/gipy/gipy.png differ
diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html
new file mode 100644
index 000000000..a1c405ed7
--- /dev/null
+++ b/apps/gipy/interface.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+ Please select a gpx file to be converted to gpc and loaded.
+
+
+ gpx file :
+
+ gpc filename : .gpc (max 24 characters)
+
+
+ fetch interests from openstreetmap
+
+
+ nice tags could be :
+ shop/bicycle, amenity/bank, shop/supermarket, leisure/picnic_table, tourism/information, amenity/pharmacy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json
new file mode 100644
index 000000000..2d06a7c2d
--- /dev/null
+++ b/apps/gipy/metadata.json
@@ -0,0 +1,23 @@
+{
+ "id": "gipy",
+ "name": "Gipy",
+ "shortName": "Gipy",
+ "version": "0.15",
+ "description": "Follow gpx files",
+ "allow_emulator":false,
+ "icon": "gipy.png",
+ "type": "app",
+ "tags": "tool,outdoors,gps",
+ "screenshots": [],
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "interface": "interface.html",
+ "storage": [
+ {"name":"gipy.app.js","url":"app.js"},
+ {"name":"gipy.settings.js","url":"settings.js"},
+ {"name":"gipy.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name":"gipy.json"}
+ ]
+}
diff --git a/apps/gipy/pkg/gpconv.d.ts b/apps/gipy/pkg/gpconv.d.ts
new file mode 100644
index 000000000..ecffa7b69
--- /dev/null
+++ b/apps/gipy/pkg/gpconv.d.ts
@@ -0,0 +1,75 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_gpc(gpcsvg: GpcSvg): Uint8Array;
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_svg(gpcsvg: GpcSvg): Uint8Array;
+/**
+* @param {string} input_str
+* @returns {Promise}
+*/
+export function convert_gpx_strings_no_osm(input_str: string): Promise;
+/**
+* @param {string} input_str
+* @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 convert_gpx_strings(input_str: string, key1: string, value1: string, key2: string, value2: string, key3: string, value3: string, key4: string, value4: string): Promise;
+/**
+*/
+export class GpcSvg {
+ free(): void;
+}
+
+export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
+
+export interface InitOutput {
+ readonly memory: WebAssembly.Memory;
+ readonly __wbg_gpcsvg_free: (a: number) => void;
+ readonly get_gpc: (a: number, b: number) => void;
+ readonly get_svg: (a: number, b: number) => void;
+ readonly convert_gpx_strings_no_osm: (a: number, b: number) => number;
+ readonly convert_gpx_strings: (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, r: 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__h0601691a32604cdd: (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__h25ed812378167476: (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/gpconv.js b/apps/gipy/pkg/gpconv.js
new file mode 100644
index 000000000..97b37e340
--- /dev/null
+++ b/apps/gipy/pkg/gpconv.js
@@ -0,0 +1,645 @@
+
+let wasm;
+
+const heap = new Array(32).fill(undefined);
+
+heap.push(undefined, null, true, false);
+
+function getObject(idx) { return heap[idx]; }
+
+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;
+}
+
+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;
+}
+
+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__h0601691a32604cdd(arg0, arg1, addHeapObject(arg2));
+}
+
+function _assertClass(instance, klass) {
+ if (!(instance instanceof klass)) {
+ throw new Error(`expected instance of ${klass.name}`);
+ }
+ return instance.ptr;
+}
+
+function getArrayU8FromWasm0(ptr, len) {
+ return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
+}
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_gpc(gpcsvg) {
+ try {
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+ _assertClass(gpcsvg, GpcSvg);
+ wasm.get_gpc(retptr, gpcsvg.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 {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_svg(gpcsvg) {
+ try {
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+ _assertClass(gpcsvg, GpcSvg);
+ wasm.get_svg(retptr, gpcsvg.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 {string} input_str
+* @returns {Promise}
+*/
+export function convert_gpx_strings_no_osm(input_str) {
+ const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ret = wasm.convert_gpx_strings_no_osm(ptr0, len0);
+ return takeObject(ret);
+}
+
+/**
+* @param {string} input_str
+* @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 convert_gpx_strings(input_str, key1, value1, key2, value2, key3, value3, key4, value4) {
+ const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passStringToWasm0(key1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ptr2 = passStringToWasm0(value1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len2 = WASM_VECTOR_LEN;
+ const ptr3 = passStringToWasm0(key2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len3 = WASM_VECTOR_LEN;
+ const ptr4 = passStringToWasm0(value2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len4 = WASM_VECTOR_LEN;
+ const ptr5 = passStringToWasm0(key3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len5 = WASM_VECTOR_LEN;
+ const ptr6 = passStringToWasm0(value3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len6 = WASM_VECTOR_LEN;
+ const ptr7 = passStringToWasm0(key4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len7 = WASM_VECTOR_LEN;
+ const ptr8 = passStringToWasm0(value4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len8 = WASM_VECTOR_LEN;
+ const ret = wasm.convert_gpx_strings(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6, ptr7, len7, ptr8, len8);
+ return takeObject(ret);
+}
+
+function handleError(f, args) {
+ try {
+ return f.apply(this, args);
+ } catch (e) {
+ wasm.__wbindgen_exn_store(addHeapObject(e));
+ }
+}
+function __wbg_adapter_69(arg0, arg1, arg2, arg3) {
+ wasm.wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
+}
+
+/**
+*/
+export class GpcSvg {
+
+ static __wrap(ptr) {
+ const obj = Object.create(GpcSvg.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_gpcsvg_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_object_drop_ref = function(arg0) {
+ takeObject(arg0);
+ };
+ imports.wbg.__wbg_gpcsvg_new = function(arg0) {
+ const ret = GpcSvg.__wrap(arg0);
+ return addHeapObject(ret);
+ };
+ 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_string_new = function(arg0, arg1) {
+ const ret = getStringFromWasm0(arg0, arg1);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
+ const ret = getObject(arg0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_fetch_386f87a3ebf5003c = function(arg0) {
+ const ret = fetch(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ 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.__wbg_fetch_749a56934f95c96c = function(arg0, arg1) {
+ const ret = getObject(arg0).fetch(getObject(arg1));
+ return addHeapObject(ret);
+ };
+ 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_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.__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_69(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_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) {
+ const ret = JSON.stringify(getObject(arg0));
+ return addHeapObject(ret);
+ }, arguments) };
+ 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.__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_wrapper947 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 147, __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;
+ 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('gpconv_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/gpconv_bg.wasm b/apps/gipy/pkg/gpconv_bg.wasm
new file mode 100644
index 000000000..edeb4eb59
Binary files /dev/null and b/apps/gipy/pkg/gpconv_bg.wasm differ
diff --git a/apps/gipy/pkg/gpconv_bg.wasm.d.ts b/apps/gipy/pkg/gpconv_bg.wasm.d.ts
new file mode 100644
index 000000000..6bc5d3719
--- /dev/null
+++ b/apps/gipy/pkg/gpconv_bg.wasm.d.ts
@@ -0,0 +1,16 @@
+/* tslint:disable */
+/* eslint-disable */
+export const memory: WebAssembly.Memory;
+export function __wbg_gpcsvg_free(a: number): void;
+export function get_gpc(a: number, b: number): void;
+export function get_svg(a: number, b: number): void;
+export function convert_gpx_strings_no_osm(a: number, b: number): number;
+export function convert_gpx_strings(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, r: number): number;
+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__h0601691a32604cdd(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__h25ed812378167476(a: number, b: number, c: number, d: number): void;
diff --git a/apps/gipy/pkg/package.json b/apps/gipy/pkg/package.json
new file mode 100644
index 000000000..dee41f5cc
--- /dev/null
+++ b/apps/gipy/pkg/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "gpconv",
+ "version": "0.1.0",
+ "files": [
+ "gpconv_bg.wasm",
+ "gpconv.js",
+ "gpconv.d.ts"
+ ],
+ "module": "gpconv.js",
+ "types": "gpconv.d.ts",
+ "sideEffects": false
+}
\ No newline at end of file
diff --git a/apps/gipy/screenshot1.png b/apps/gipy/screenshot1.png
new file mode 100644
index 000000000..c7c45fa3b
Binary files /dev/null and b/apps/gipy/screenshot1.png differ
diff --git a/apps/gipy/screenshot2.png b/apps/gipy/screenshot2.png
new file mode 100644
index 000000000..ed61eb795
Binary files /dev/null and b/apps/gipy/screenshot2.png differ
diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js
new file mode 100644
index 000000000..af9cbef22
--- /dev/null
+++ b/apps/gipy/settings.js
@@ -0,0 +1,38 @@
+(function (back) {
+ var FILE = "gipy.json";
+ // Load settings
+ var settings = Object.assign(
+ {
+ keep_gps_alive: false,
+ max_speed: 35,
+ },
+ require("Storage").readJSON(FILE, true) || {}
+ );
+
+ function writeSettings() {
+ require("Storage").writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "": { title: "Gipy" },
+ "< Back": () => back(),
+ "keep gps alive": {
+ value: !!settings.keep_gps_alive, // !! converts undefined to false
+ format: (v) => (v ? "Yes" : "No"),
+ onchange: (v) => {
+ settings.keep_gps_alive = v;
+ writeSettings();
+ },
+ },
+ "max speed": {
+ value: 35 | settings.max_speed, // 0| converts undefined to 0
+ min: 0,
+ max: 130,
+ onchange: (v) => {
+ settings.max_speed = v;
+ writeSettings();
+ },
+ },
+ });
+});
diff --git a/apps/gpsautotime/settings.js b/apps/gpsautotime/settings.js
index be6e3bbec..34a6364fe 100644
--- a/apps/gpsautotime/settings.js
+++ b/apps/gpsautotime/settings.js
@@ -13,7 +13,7 @@
E.showMenu({
"" : { "title" : "GPS auto time" },
"< Back" : () => back(),
- 'Show Widgets': {
+ 'Show Widget': {
value: !!settings.show,
onchange: v => {
settings.show = v;
diff --git a/apps/gpsautotime/widget.js b/apps/gpsautotime/widget.js
index a21c14619..14d6fe140 100644
--- a/apps/gpsautotime/widget.js
+++ b/apps/gpsautotime/widget.js
@@ -9,7 +9,7 @@
delete settings;
Bangle.on('GPS',function(fix) {
- if (fix.fix) {
+ if (fix.fix && fix.time) {
var curTime = fix.time.getTime()/1000;
setTime(curTime);
lastTimeSet = curTime;
diff --git a/apps/gpsinfo/metadata.json b/apps/gpsinfo/metadata.json
index 60bd90c03..002febd86 100644
--- a/apps/gpsinfo/metadata.json
+++ b/apps/gpsinfo/metadata.json
@@ -5,7 +5,7 @@
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
- "tags": "gps",
+ "tags": "gps,outdoors",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"gpsinfo.app.js","url":"gps-info.js"},
diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog
index 2f1a9ce45..c36c23c72 100644
--- a/apps/gpstrek/ChangeLog
+++ b/apps/gpstrek/ChangeLog
@@ -1,2 +1,15 @@
0.01: New App!
0.02: Make selection of background activity more explicit
+0.03: Fix listener for accel always active
+ Use custom UI with swipes instead of leftright
+0.04: Fix compass heading
+0.05: Added adjustment for Bangle.js magnetometer heading fix
+0.06: Fix waypoint menu always selecting last waypoint
+ Fix widget adding listeners more than once
+0.07: Show checkered flag for target markers
+ Single waypoints are now shown in the compass view
+0.08: Better handle state in widget
+ Slightly faster drawing by doing some caching
+ Reconstruct battery voltage by using calibrated batFullVoltage
+ Averaging for smoothing compass headings
+ Save state if route or waypoint has been chosen
diff --git a/apps/gpstrek/README.md b/apps/gpstrek/README.md
index 439b7497a..c55f5a8bf 100644
--- a/apps/gpstrek/README.md
+++ b/apps/gpstrek/README.md
@@ -10,9 +10,12 @@ Tapping or button to switch to the next information display, swipe right for the
Choose either a route or a waypoint as basis for the display.
-After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way.
+After this selection and availability of a GPS fix the compass will show a checkered flag for your destination and a green dot for possibly available waypoints on the way.
Waypoints are shown with name if available and distance to waypoint.
+As long as no GPS signal is available the compass shows the heading from the build in magnetometer. When a GPS fix becomes available, the compass display shows the GPS course. This can be differentiated by the display of bubble levels on top and sides of the compass.
+If they are on display, the source is the magnetometer and you should keep the bangle level. There is currently no tilt compensation for the compass display.
+
### Route
Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh)
diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js
index 0c3579d4b..b3ec79fd2 100644
--- a/apps/gpstrek/app.js
+++ b/apps/gpstrek/app.js
@@ -1,48 +1,69 @@
+
+{ //run in own scope for fast switch
const STORAGE = require("Storage");
-const showWidgets = true;
-let numberOfSlices=4;
+const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
+
+let init = function(){
+ global.screen = 1;
+ global.drawTimeout = undefined;
+ global.lastDrawnScreen = 0;
+ global.firstDraw = true;
+ global.slices = [];
+ global.maxScreens = 1;
+ global.scheduleDraw = false;
-if (showWidgets){
Bangle.loadWidgets();
-}
+ WIDGETS.gpstrek.start(false);
+ if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 3;
+};
-let state = WIDGETS.gpstrek.getState();
-WIDGETS.gpstrek.start(false);
+let cleanup = function(){
+ if (global.drawTimeout) clearTimeout(global.drawTimeout);
+ delete global.screen;
+ delete global.drawTimeout;
+ delete global.lastDrawnScreen;
+ delete global.firstDraw;
+ delete global.slices;
+ delete global.maxScreens;
+};
-function parseNumber(toParse){
+init();
+scheduleDraw = true;
+
+let parseNumber = function(toParse){
if (toParse.includes(".")) return parseFloat(toParse);
return parseFloat("" + toParse + ".0");
-}
+};
-function parseWaypoint(filename, offset, result){
+let parseWaypoint = function(filename, offset, result){
result.lat = parseNumber(STORAGE.read(filename, offset, 11));
result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12));
return offset + 12;
-}
+};
-function parseWaypointWithElevation(filename, offset, result){
+let parseWaypointWithElevation = function (filename, offset, result){
offset = parseWaypoint(filename, offset, result);
result.alt = parseNumber(STORAGE.read(filename, offset, 6));
return offset + 6;
-}
+};
-function parseWaypointWithName(filename, offset, result){
+let parseWaypointWithName = function(filename, offset, result){
offset = parseWaypoint(filename, offset, result);
return parseName(filename, offset, result);
-}
+};
-function parseName(filename, offset, result){
+let parseName = function(filename, offset, result){
let nameLength = STORAGE.read(filename, offset, 2) - 0;
result.name = STORAGE.read(filename, offset += 2, nameLength);
return offset + nameLength;
-}
+};
-function parseWaypointWithElevationAndName(filename, offset, result){
+let parseWaypointWithElevationAndName = function(filename, offset, result){
offset = parseWaypointWithElevation(filename, offset, result);
return parseName(filename, offset, result);
-}
+};
-function getEntry(filename, offset, result){
+let getEntry = function(filename, offset, result){
result.fileOffset = offset;
let type = STORAGE.read(filename, offset++, 1);
if (type == "") return -1;
@@ -68,12 +89,12 @@ function getEntry(filename, offset, result){
result.fileLength = offset - result.fileOffset;
//print(result);
return offset;
-}
+};
const labels = ["N","NE","E","SE","S","SW","W","NW"];
const loc = require("locale");
-function matchFontSize(graphics, text, height, width){
+let matchFontSize = function(graphics, text, height, width){
graphics.setFontVector(height);
let metrics;
let size = 1;
@@ -81,13 +102,19 @@ function matchFontSize(graphics, text, height, width){
size -= 0.05;
graphics.setFont("Vector",Math.floor(height*size));
}
-}
+};
-function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){
+let getDoubleLineSlice = function(title1,title2,provider1,provider2,refreshTime){
let lastDrawn = Date.now() - Math.random()*refreshTime;
+ let lastValue1 = 0;
+ let lastValue2 = 0;
return {
refresh: function (){
- return Date.now() - lastDrawn > (Bangle.isLocked()?(refreshTime?refreshTime:5000):(refreshTime?refreshTime*2:10000));
+ let bigChange1 = (Math.abs(lastValue1 - provider1()) > 1);
+ let bigChange2 = (Math.abs(lastValue2 - provider2()) > 1);
+ let refresh = (Bangle.isLocked()?(refreshTime?refreshTime*5:10000):(refreshTime?refreshTime*2:1000));
+ let old = (Date.now() - lastDrawn) > refresh;
+ return (bigChange1 || bigChange2) && old;
},
draw: function (graphics, x, y, height, width){
lastDrawn = Date.now();
@@ -95,29 +122,29 @@ function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){
if (typeof title2 == "function") title2 = title2();
graphics.clearRect(x,y,x+width,y+height);
- let value = provider1();
- matchFontSize(graphics, title1 + value, Math.floor(height*0.5), width);
+ lastValue1 = provider1();
+ matchFontSize(graphics, title1 + lastValue1, Math.floor(height*0.5), width);
graphics.setFontAlign(-1,-1);
graphics.drawString(title1, x+2, y);
graphics.setFontAlign(1,-1);
- graphics.drawString(value, x+width, y);
+ graphics.drawString(lastValue1, x+width, y);
- value = provider2();
- matchFontSize(graphics, title2 + value, Math.floor(height*0.5), width);
+ lastValue2 = provider2();
+ matchFontSize(graphics, title2 + lastValue2, Math.floor(height*0.5), width);
graphics.setFontAlign(-1,-1);
graphics.drawString(title2, x+2, y+(height*0.5));
graphics.setFontAlign(1,-1);
- graphics.drawString(value, x+width, y+(height*0.5));
+ graphics.drawString(lastValue2, x+width, y+(height*0.5));
}
};
-}
+};
-function getTargetSlice(targetDataSource){
+let getTargetSlice = function(targetDataSource){
let nameIndex = 0;
let lastDrawn = Date.now() - Math.random()*3000;
return {
refresh: function (){
- return Date.now() - lastDrawn > (Bangle.isLocked()?10000:3000);
+ return Date.now() - lastDrawn > (Bangle.isLocked()?3000:10000);
},
draw: function (graphics, x, y, height, width){
lastDrawn = Date.now();
@@ -174,9 +201,9 @@ function getTargetSlice(targetDataSource){
}
}
};
-}
+};
-function drawCompass(graphics, x, y, height, width, increment, start){
+let drawCompass = function(graphics, x, y, height, width, increment, start){
graphics.setFont12x20();
graphics.setFontAlign(0,-1);
graphics.setColor(graphics.theme.fg);
@@ -197,14 +224,19 @@ function drawCompass(graphics, x, y, height, width, increment, start){
xpos+=increment*15;
if (xpos > width + 20) break;
}
-}
+};
-function getCompassSlice(compassDataSource){
+let getCompassSlice = function(compassDataSource){
let lastDrawn = Date.now() - Math.random()*2000;
+ let lastDrawnValue = 0;
const buffers = 4;
let buf = [];
return {
- refresh : function (){return Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true;},
+ refresh : function (){
+ let bigChange = (Math.abs(lastDrawnValue - compassDataSource.getCourse()) > 2);
+ let old = (Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true);
+ return bigChange && old;
+ },
draw: function (graphics, x,y,height,width){
lastDrawn = Date.now();
const max = 180;
@@ -212,12 +244,14 @@ function getCompassSlice(compassDataSource){
graphics.clearRect(x,y,x+width,y+height);
- var start = compassDataSource.getCourse() - 90;
- if (isNaN(compassDataSource.getCourse())) start = -90;
+ lastDrawnValue = compassDataSource.getCourse();
+
+ var start = lastDrawnValue - 90;
+ if (isNaN(lastDrawnValue)) start = -90;
if (start<0) start+=360;
start = start % 360;
- if (state.acc && compassDataSource.getCourseType() == "MAG"){
+ if (WIDGETS.gpstrek.getState().acc && compassDataSource.getCourseType() == "MAG"){
drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start);
} else {
drawCompass(graphics,0,y,height,width,increment,start);
@@ -226,7 +260,8 @@ function getCompassSlice(compassDataSource){
if (compassDataSource.getPoints){
for (let p of compassDataSource.getPoints()){
- var bpos = p.bearing - compassDataSource.getCourse();
+ g.reset();
+ var bpos = p.bearing - lastDrawnValue;
if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360;
bpos+=120;
@@ -239,12 +274,19 @@ function getCompassSlice(compassDataSource){
} else {
bpos=Math.round(bpos*increment);
}
- graphics.setColor(p.color);
- graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
+ if (p.color){
+ graphics.setColor(p.color);
+ }
+ if (p.icon){
+ graphics.drawImage(p.icon, bpos,y+height-12, {rotate:0,scale:2});
+ } else {
+ graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
+ }
}
}
if (compassDataSource.getMarkers){
for (let m of compassDataSource.getMarkers()){
+ g.reset();
g.setColor(m.fillcolor);
let mpos = m.xpos * width;
if (m.xpos < 0.05) mpos = Math.floor(width*0.05);
@@ -257,9 +299,9 @@ function getCompassSlice(compassDataSource){
graphics.setColor(g.theme.fg);
graphics.fillRect(x,y,Math.floor(width*0.05),y+height);
graphics.fillRect(Math.ceil(width*0.95),y,width,y+height);
- if (state.acc && compassDataSource.getCourseType() == "MAG") {
- let xh = E.clip(width*0.5-height/2+(((state.acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2);
- let yh = E.clip(y+(((state.acc.y+1)/2)*height),y,y+height);
+ if (WIDGETS.gpstrek.getState().acc && compassDataSource.getCourseType() == "MAG") {
+ let xh = E.clip(width*0.5-height/2+(((WIDGETS.gpstrek.getState().acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2);
+ let yh = E.clip(y+(((WIDGETS.gpstrek.getState().acc.y+1)/2)*height),y,y+height);
graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05));
@@ -281,56 +323,68 @@ function getCompassSlice(compassDataSource){
graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height);
}
};
-}
+};
-function radians(a) {
+let radians = function(a) {
return a*Math.PI/180;
-}
+};
-function degrees(a) {
- var d = a*180/Math.PI;
+let degrees = function(a) {
+ let d = a*180/Math.PI;
return (d+360)%360;
-}
+};
-function bearing(a,b){
+let bearing = function(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
- var delta = radians(b.lon-a.lon);
- var alat = radians(a.lat);
- var blat = radians(b.lat);
- var y = Math.sin(delta) * Math.cos(blat);
- var x = Math.cos(alat)*Math.sin(blat) -
+ let delta = radians(b.lon-a.lon);
+ let alat = radians(a.lat);
+ let blat = radians(b.lat);
+ let y = Math.sin(delta) * Math.cos(blat);
+ let x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
-}
+};
-function distance(a,b){
+let distance = function(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
- var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
- var y = radians(b.lat-a.lat);
+ let x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
+ let y = radians(b.lat-a.lat);
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
-}
+};
-function triangle (x, y, width, height){
+let getAveragedCompass = function(){
+ return Math.round(WIDGETS.gpstrek.getState().avgComp);
+};
+
+let triangle = function(x, y, width, height){
return [
Math.round(x),Math.round(y),
Math.round(x+width * 0.5), Math.round(y+height),
Math.round(x-width * 0.5), Math.round(y+height)
];
-}
+};
-function setButtons(){
- Bangle.setUI("leftright", (dir)=>{
- if (dir < 0) {
- nextScreen();
- } else if (dir > 0) {
- switchMenu();
- } else {
- nextScreen();
- }
- });
-}
+let onSwipe = function(dir){
+ if (dir < 0) {
+ nextScreen();
+ } else if (dir > 0) {
+ switchMenu();
+ } else {
+ nextScreen();
+ }
+};
-function getApproxFileSize(name){
+let setButtons = function(){
+ let options = {
+ mode: "custom",
+ swipe: onSwipe,
+ btn: nextScreen,
+ touch: nextScreen
+ };
+ Bangle.setUI(options);
+};
+
+let getApproxFileSize = function(name){
let currentStart = STORAGE.getStats().totalBytes;
let currentSize = 0;
for (let i = currentStart; i > 500; i/=2){
@@ -344,9 +398,9 @@ function getApproxFileSize(name){
currentSize += currentDiff;
}
return currentSize;
-}
+};
-function parseRouteData(filename, progressMonitor){
+let parseRouteData = function(filename, progressMonitor){
let routeInfo = {};
routeInfo.filename = filename;
@@ -392,40 +446,40 @@ function parseRouteData(filename, progressMonitor){
set(routeInfo, 0);
return routeInfo;
-}
+};
-function hasPrev(route){
+let hasPrev = function(route){
if (route.mirror) return route.index < (route.count - 1);
return route.index > 0;
-}
+};
-function hasNext(route){
+let hasNext = function(route){
if (route.mirror) return route.index > 0;
return route.index < (route.count - 1);
-}
+};
-function next(route){
+let next = function(route){
if (!hasNext(route)) return;
if (route.mirror) set(route, --route.index);
if (!route.mirror) set(route, ++route.index);
-}
+};
-function set(route, index){
+let set = function(route, index){
route.currentWaypoint = {};
route.index = index;
getEntry(route.filename, route.refs[index], route.currentWaypoint);
-}
+};
-function prev(route){
+let prev = function(route){
if (!hasPrev(route)) return;
if (route.mirror) set(route, ++route.index);
if (!route.mirror) set(route, --route.index);
-}
+};
let lastMirror;
let cachedLast;
-function getLast(route){
+let getLast = function(route){
let wp = {};
if (lastMirror != route.mirror){
if (route.mirror) getEntry(route.filename, route.refs[0], wp);
@@ -434,14 +488,14 @@ function getLast(route){
cachedLast = wp;
}
return cachedLast;
-}
+};
-function removeMenu(){
+let removeMenu = function(){
E.showMenu();
switchNav();
-}
+};
-function showProgress(progress, title, max){
+let showProgress = function(progress, title, max){
//print("Progress",progress,max)
let message = title? title: "Loading";
if (max){
@@ -452,32 +506,31 @@ function showProgress(progress, title, max){
for (let i = dots; i < 4; i++) message += " ";
}
E.showMessage(message);
-}
+};
-function handleLoading(c){
+let handleLoading = function(c){
E.showMenu();
- state.route = parseRouteData(c, showProgress);
- state.waypoint = null;
+ WIDGETS.gpstrek.getState().route = parseRouteData(c, showProgress);
+ WIDGETS.gpstrek.getState().waypoint = null;
+ WIDGETS.gpstrek.getState().route.mirror = false;
removeMenu();
- state.route.mirror = false;
-}
+};
-function showRouteSelector (){
+let showRouteSelector = function(){
var menu = {
"" : {
back : showRouteMenu,
}
};
- for (let c of STORAGE.list((/\.trf$/))){
- let file = c;
- menu[file] = ()=>{handleLoading(file);};
- }
+ STORAGE.list(/\.trf$/).forEach((file)=>{
+ menu[file] = ()=>{handleLoading(file);};
+ });
E.showMenu(menu);
-}
+};
-function showRouteMenu(){
+let showRouteMenu = function(){
var menu = {
"" : {
"title" : "Route",
@@ -486,48 +539,48 @@ function showRouteMenu(){
"Select file" : showRouteSelector
};
- if (state.route){
+ if (WIDGETS.gpstrek.getState().route){
menu.Mirror = {
- value: state && state.route && !!state.route.mirror || false,
+ value: WIDGETS.gpstrek.getState() && WIDGETS.gpstrek.getState().route && !!WIDGETS.gpstrek.getState().route.mirror || false,
onchange: v=>{
- state.route.mirror = v;
+ WIDGETS.gpstrek.getState().route.mirror = v;
}
};
menu['Select closest waypoint'] = function () {
- if (state.currentPos && state.currentPos.lat){
- setClosestWaypoint(state.route, null, showProgress); removeMenu();
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat){
+ setClosestWaypoint(WIDGETS.gpstrek.getState().route, null, showProgress); removeMenu();
} else {
E.showAlert("No position").then(()=>{E.showMenu(menu);});
}
};
menu['Select closest waypoint (not visited)'] = function () {
- if (state.currentPos && state.currentPos.lat){
- setClosestWaypoint(state.route, state.route.index, showProgress); removeMenu();
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat){
+ setClosestWaypoint(WIDGETS.gpstrek.getState().route, WIDGETS.gpstrek.getState().route.index, showProgress); removeMenu();
} else {
E.showAlert("No position").then(()=>{E.showMenu(menu);});
}
};
menu['Select waypoint'] = {
- value : state.route.index,
- min:1,max:state.route.count,step:1,
- onchange : v => { set(state.route, v-1); }
+ value : WIDGETS.gpstrek.getState().route.index,
+ min:1,max:WIDGETS.gpstrek.getState().route.count,step:1,
+ onchange : v => { set(WIDGETS.gpstrek.getState().route, v-1); }
};
menu['Select waypoint as current position'] = function (){
- state.currentPos.lat = state.route.currentWaypoint.lat;
- state.currentPos.lon = state.route.currentWaypoint.lon;
- state.currentPos.alt = state.route.currentWaypoint.alt;
+ WIDGETS.gpstrek.getState().currentPos.lat = WIDGETS.gpstrek.getState().route.currentWaypoint.lat;
+ WIDGETS.gpstrek.getState().currentPos.lon = WIDGETS.gpstrek.getState().route.currentWaypoint.lon;
+ WIDGETS.gpstrek.getState().currentPos.alt = WIDGETS.gpstrek.getState().route.currentWaypoint.alt;
removeMenu();
};
}
- if (state.route && hasPrev(state.route))
- menu['Previous waypoint'] = function() { prev(state.route); removeMenu(); };
- if (state.route && hasNext(state.route))
- menu['Next waypoint'] = function() { next(state.route); removeMenu(); };
+ if (WIDGETS.gpstrek.getState().route && hasPrev(WIDGETS.gpstrek.getState().route))
+ menu['Previous waypoint'] = function() { prev(WIDGETS.gpstrek.getState().route); removeMenu(); };
+ if (WIDGETS.gpstrek.getState().route && hasNext(WIDGETS.gpstrek.getState().route))
+ menu['Next waypoint'] = function() { next(WIDGETS.gpstrek.getState().route); removeMenu(); };
E.showMenu(menu);
-}
+};
-function showWaypointSelector(){
+let showWaypointSelector = function(){
let waypoints = require("waypoints").load();
var menu = {
"" : {
@@ -535,43 +588,43 @@ function showWaypointSelector(){
}
};
- for (let c in waypoints){
+ waypoints.forEach((wp,c)=>{
menu[waypoints[c].name] = function (){
- state.waypoint = waypoints[c];
- state.waypointIndex = c;
- state.route = null;
+ WIDGETS.gpstrek.getState().waypoint = waypoints[c];
+ WIDGETS.gpstrek.getState().waypointIndex = c;
+ WIDGETS.gpstrek.getState().route = null;
removeMenu();
};
- }
+ });
E.showMenu(menu);
-}
+};
-function showCalibrationMenu(){
+let showCalibrationMenu = function(){
let menu = {
"" : {
"title" : "Calibration",
back : showMenu,
},
"Barometer (GPS)" : ()=>{
- if (!state.currentPos || isNaN(state.currentPos.alt)){
+ if (!WIDGETS.gpstrek.getState().currentPos || isNaN(WIDGETS.gpstrek.getState().currentPos.alt)){
E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);});
} else {
- state.calibAltDiff = state.altitude - state.currentPos.alt;
- E.showAlert("Calibrated Altitude Difference: " + state.calibAltDiff.toFixed(0)).then(()=>{removeMenu();});
+ WIDGETS.gpstrek.getState().calibAltDiff = WIDGETS.gpstrek.getState().altitude - WIDGETS.gpstrek.getState().currentPos.alt;
+ E.showAlert("Calibrated Altitude Difference: " + WIDGETS.gpstrek.getState().calibAltDiff.toFixed(0)).then(()=>{removeMenu();});
}
},
"Barometer (Manual)" : {
- value : Math.round(state.currentPos && (state.currentPos.alt != undefined && !isNaN(state.currentPos.alt)) ? state.currentPos.alt: state.altitude),
+ value : Math.round(WIDGETS.gpstrek.getState().currentPos && (WIDGETS.gpstrek.getState().currentPos.alt != undefined && !isNaN(WIDGETS.gpstrek.getState().currentPos.alt)) ? WIDGETS.gpstrek.getState().currentPos.alt: WIDGETS.gpstrek.getState().altitude),
min:-2000,max: 10000,step:1,
- onchange : v => { state.calibAltDiff = state.altitude - v; }
+ onchange : v => { WIDGETS.gpstrek.getState().calibAltDiff = WIDGETS.gpstrek.getState().altitude - v; }
},
"Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();},
};
E.showMenu(menu);
-}
+};
-function showWaypointMenu(){
+let showWaypointMenu = function(){
let menu = {
"" : {
"title" : "Waypoint",
@@ -580,21 +633,21 @@ function showWaypointMenu(){
"Select waypoint" : showWaypointSelector,
};
E.showMenu(menu);
-}
+};
-function showBackgroundMenu(){
+let showBackgroundMenu = function(){
let menu = {
"" : {
"title" : "Background",
back : showMenu,
},
- "Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {E.showMenu(mainmenu);}});},
- "Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {E.showMenu(mainmenu);}});},
+ "Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{showMenu();});},
+ "Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{showMenu();});},
};
E.showMenu(menu);
-}
+};
-function showMenu(){
+let showMenu = function(){
var mainmenu = {
"" : {
"title" : "Main",
@@ -604,50 +657,55 @@ function showMenu(){
"Waypoint" : showWaypointMenu,
"Background" : showBackgroundMenu,
"Calibration": showCalibrationMenu,
- "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}});},
- "Slices" : {
- value : numberOfSlices,
+ "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 => { setNumberOfSlices(v); }
+ onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
},
};
E.showMenu(mainmenu);
-}
+};
-let scheduleDraw = true;
-function switchMenu(){
- screen = 0;
- scheduleDraw = false;
- showMenu();
-}
+let switchMenu = function(){
+ stopDrawing();
+ showMenu();
+};
-function drawInTimeout(){
- setTimeout(()=>{
+let stopDrawing = function(){
+ if (drawTimeout) clearTimeout(drawTimeout);
+ scheduleDraw = false;
+};
+
+let drawInTimeout = function(){
+ if (global.drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(()=>{
+ drawTimeout = undefined;
draw();
- if (scheduleDraw)
- setTimeout(drawInTimeout, 0);
- },0);
-}
+ },50);
+};
-function switchNav(){
+let switchNav = function(){
if (!screen) screen = 1;
setButtons();
scheduleDraw = true;
+ firstDraw = true;
drawInTimeout();
-}
+};
-function nextScreen(){
+let nextScreen = function(){
screen++;
if (screen > maxScreens){
screen = 1;
}
-}
+ drawInTimeout();
+};
-function setClosestWaypoint(route, startindex, progress){
- if (startindex >= state.route.count) startindex = state.route.count - 1;
- if (!state.currentPos.lat){
+let setClosestWaypoint = function(route, startindex, progress){
+ if (startindex >= WIDGETS.gpstrek.getState().route.count) startindex = WIDGETS.gpstrek.getState().route.count - 1;
+ if (!WIDGETS.gpstrek.getState().currentPos.lat){
set(route, startindex);
return;
}
@@ -657,7 +715,7 @@ function setClosestWaypoint(route, startindex, progress){
if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count);
let wp = {};
getEntry(route.filename, route.refs[i], wp);
- let curDist = distance(state.currentPos, wp);
+ let curDist = distance(WIDGETS.gpstrek.getState().currentPos, wp);
if (curDist < minDist){
minDist = curDist;
minIndex = i;
@@ -666,25 +724,28 @@ function setClosestWaypoint(route, startindex, progress){
}
}
set(route, minIndex);
-}
+};
-let screen = 1;
+const finishIcon = atob("CggB//meZmeZ+Z5n/w==");
const compassSliceData = {
getCourseType: function(){
- return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG";
+ return (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.course) ? "GPS" : "MAG";
},
getCourse: function (){
- if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course;
- return state.compassHeading?state.compassHeading:undefined;
+ if(compassSliceData.getCourseType() == "GPS") return WIDGETS.gpstrek.getState().currentPos.course;
+ return getAveragedCompass();
},
getPoints: function (){
let points = [];
- if (state.currentPos && state.currentPos.lon && state.route && state.route.currentWaypoint){
- points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"});
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.currentWaypoint){
+ points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, WIDGETS.gpstrek.getState().route.currentWaypoint), color:"#0f0"});
}
- if (state.currentPos && state.currentPos.lon && state.route){
- points.push({bearing:bearing(state.currentPos, getLast(state.route)), color:"#00f"});
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().route){
+ points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, getLast(WIDGETS.gpstrek.getState().route)), icon: finishIcon});
+ }
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().waypoint){
+ points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, WIDGETS.gpstrek.getState().waypoint), icon: finishIcon});
}
return points;
},
@@ -696,79 +757,74 @@ const compassSliceData = {
const waypointData = {
icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"),
getProgress: function() {
- return (state.route.index + 1) + "/" + state.route.count;
+ return (WIDGETS.gpstrek.getState().route.index + 1) + "/" + WIDGETS.gpstrek.getState().route.count;
},
getTarget: function (){
- if (distance(state.currentPos,state.route.currentWaypoint) < 30 && hasNext(state.route)){
- next(state.route);
+ if (distance(WIDGETS.gpstrek.getState().currentPos,WIDGETS.gpstrek.getState().route.currentWaypoint) < 30 && hasNext(WIDGETS.gpstrek.getState().route)){
+ next(WIDGETS.gpstrek.getState().route);
Bangle.buzz(1000);
}
- return state.route.currentWaypoint;
+ return WIDGETS.gpstrek.getState().route.currentWaypoint;
},
getStart: function (){
- return state.currentPos;
+ return WIDGETS.gpstrek.getState().currentPos;
}
};
const finishData = {
icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="),
getTarget: function (){
- if (state.route) return getLast(state.route);
- if (state.waypoint) return state.waypoint;
+ if (WIDGETS.gpstrek.getState().route) return getLast(WIDGETS.gpstrek.getState().route);
+ if (WIDGETS.gpstrek.getState().waypoint) return WIDGETS.gpstrek.getState().waypoint;
},
getStart: function (){
- return state.currentPos;
+ return WIDGETS.gpstrek.getState().currentPos;
}
};
-let sliceHeight;
-function setNumberOfSlices(number){
- numberOfSlices = number;
- sliceHeight = Math.floor((g.getHeight()-(showWidgets?24:0))/numberOfSlices);
-}
-
-let slices = [];
-let maxScreens = 1;
-setNumberOfSlices(3);
+let getSliceHeight = function(number){
+ return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices);
+};
let compassSlice = getCompassSlice(compassSliceData);
let waypointSlice = getTargetSlice(waypointData);
let finishSlice = getTargetSlice(finishData);
let eleSlice = getDoubleLineSlice("Up","Down",()=>{
- return loc.distance(state.up,3) + "/" + (state.route ? loc.distance(state.route.up,3):"---");
+ return loc.distance(WIDGETS.gpstrek.getState().up,3) + "/" + (WIDGETS.gpstrek.getState().route ? loc.distance(WIDGETS.gpstrek.getState().route.up,3):"---");
},()=>{
- return loc.distance(state.down,3) + "/" + (state.route ? loc.distance(state.route.down,3): "---");
+ return loc.distance(WIDGETS.gpstrek.getState().down,3) + "/" + (WIDGETS.gpstrek.getState().route ? loc.distance(WIDGETS.gpstrek.getState().route.down,3): "---");
});
let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{
let speed = 0;
- if (state.currentPos && state.currentPos.speed) speed = state.currentPos.speed;
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.speed) speed = WIDGETS.gpstrek.getState().currentPos.speed;
return loc.speed(speed,2);
},()=>{
let alt = Infinity;
- if (!isNaN(state.altitude)){
- alt = isNaN(state.calibAltDiff) ? state.altitude : (state.altitude - state.calibAltDiff);
+ if (!isNaN(WIDGETS.gpstrek.getState().altitude)){
+ alt = isNaN(WIDGETS.gpstrek.getState().calibAltDiff) ? WIDGETS.gpstrek.getState().altitude : (WIDGETS.gpstrek.getState().altitude - WIDGETS.gpstrek.getState().calibAltDiff);
}
- if (state.currentPos && state.currentPos.alt) alt = state.currentPos.alt;
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.alt) alt = WIDGETS.gpstrek.getState().currentPos.alt;
+ if (isNaN(alt)) return "---";
return loc.distance(alt,3);
});
let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{
- return (state.compassHeading?Math.round(state.compassHeading):"---") + "°";
+ return getAveragedCompass() + "°";
},()=>{
let course = "---°";
- if (state.currentPos && state.currentPos.course) course = state.currentPos.course + "°";
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.course) course = WIDGETS.gpstrek.getState().currentPos.course + "°";
return course;
},200);
let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{
- return state.bpm;
+ return WIDGETS.gpstrek.getState().bpm || "---";
},()=>{
- return state.steps;
+ return !isNaN(WIDGETS.gpstrek.getState().steps)? WIDGETS.gpstrek.getState().steps: "---";
});
let system2Slice = getDoubleLineSlice("Bat","",()=>{
- return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + NRF.getBattery().toFixed(2) + "V";
+ return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + (analogRead(D3)*4.2/BAT_FULL).toFixed(2) + "V";
},()=>{
return "";
});
@@ -780,17 +836,17 @@ let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{
return (STORAGE.getFree()/1024).toFixed(0)+"kB";
});
-function updateSlices(){
+let updateSlices = function(){
slices = [];
slices.push(compassSlice);
- if (state.currentPos && state.currentPos.lat && state.route && state.route.currentWaypoint && state.route.index < state.route.count - 1) {
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat && WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.currentWaypoint && WIDGETS.gpstrek.getState().route.index < WIDGETS.gpstrek.getState().route.count - 1) {
slices.push(waypointSlice);
}
- if (state.currentPos && state.currentPos.lat && (state.route || state.waypoint)) {
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat && (WIDGETS.gpstrek.getState().route || WIDGETS.gpstrek.getState().waypoint)) {
slices.push(finishSlice);
}
- if ((state.route && state.route.down !== undefined) || state.down != undefined) {
+ if ((WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.down !== undefined) || WIDGETS.gpstrek.getState().down != undefined) {
slices.push(eleSlice);
}
slices.push(statusSlice);
@@ -798,42 +854,44 @@ function updateSlices(){
slices.push(healthSlice);
slices.push(systemSlice);
slices.push(system2Slice);
- maxScreens = Math.ceil(slices.length/numberOfSlices);
-}
+ maxScreens = Math.ceil(slices.length/WIDGETS.gpstrek.getState().numberOfSlices);
+};
-function clear() {
- g.clearRect(0,(showWidgets ? 24 : 0), g.getWidth(),g.getHeight());
-}
-let lastDrawnScreen;
-let firstDraw = true;
+let clear = function() {
+ g.clearRect(Bangle.appRect);
+};
-function draw(){
- if (!screen) return;
- let ypos = showWidgets ? 24 : 0;
+let draw = function(){
+ if (!global.screen) return;
+ let ypos = Bangle.appRect.y;
- let firstSlice = (screen-1)*numberOfSlices;
+ let firstSlice = (screen-1)*WIDGETS.gpstrek.getState().numberOfSlices;
updateSlices();
let force = lastDrawnScreen != screen || firstDraw;
if (force){
clear();
- if (showWidgets){
- Bangle.drawWidgets();
- }
}
+ if (firstDraw) Bangle.drawWidgets();
lastDrawnScreen = screen;
- for (let slice of slices.slice(firstSlice,firstSlice + numberOfSlices)) {
+ let sliceHeight = getSliceHeight();
+ for (let slice of slices.slice(firstSlice,firstSlice + WIDGETS.gpstrek.getState().numberOfSlices)) {
g.reset();
if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth());
ypos += sliceHeight+1;
g.drawLine(0,ypos-1,g.getWidth(),ypos-1);
}
+
+ if (scheduleDraw){
+ drawInTimeout();
+ }
firstDraw = false;
-}
+};
switchNav();
-g.clear();
+clear();
+}
diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json
index 67f71566e..3e27a3247 100644
--- a/apps/gpstrek/metadata.json
+++ b/apps/gpstrek/metadata.json
@@ -1,7 +1,7 @@
{
"id": "gpstrek",
"name": "GPS Trekking",
- "version": "0.02",
+ "version": "0.08",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png",
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],
diff --git a/apps/gpstrek/screen2.png b/apps/gpstrek/screen2.png
index 12cd65975..9a6e14e06 100644
Binary files a/apps/gpstrek/screen2.png and b/apps/gpstrek/screen2.png differ
diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js
index 8d9831e06..363ade8ee 100644
--- a/apps/gpstrek/widget.js
+++ b/apps/gpstrek/widget.js
@@ -1,6 +1,28 @@
(() => {
+const SAMPLES=5;
+function initState(){
+ //cleanup volatile state here
+ state = {};
+ state.compassSamples = new Array(SAMPLES).fill(0);
+ state.lastSample = 0;
+ state.sampleIndex = 0;
+ state.currentPos={};
+ state.steps = 0;
+ state.calibAltDiff = 0;
+ state.numberOfSlices = 3;
+ state.steps = 0;
+ state.up = 0;
+ state.down = 0;
+ state.saved = 0;
+ state.avgComp = 0;
+}
+
const STORAGE=require('Storage');
-let state = STORAGE.readJSON("gpstrek.state.json")||{};
+let state = STORAGE.readJSON("gpstrek.state.json");
+if (!state) {
+ state = {};
+ initState();
+}
let bgChanged = false;
function saveState(){
@@ -8,12 +30,13 @@ function saveState(){
STORAGE.writeJSON("gpstrek.state.json", state);
}
-E.on("kill",()=>{
- if (bgChanged){
+function onKill(){
+ if (bgChanged || state.route || state.waypoint){
saveState();
}
-});
+}
+E.on("kill", onKill);
function onPulse(e){
state.bpm = e.bpm;
@@ -23,31 +46,47 @@ function onGPS(fix) {
if(fix.fix) state.currentPos = fix;
}
-Bangle.on('accel', function(e) {
- state.acc = e;
-});
+let radians = function(a) {
+ return a*Math.PI/180;
+};
-function onMag(e) {
- if (!state.compassHeading) state.compassHeading = e.heading;
-
- //if (a+180)mod 360 == b then
- //return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction)
-//else
- //return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) )
-
- /*
- let average;
- let a = radians(compassHeading);
- let b = radians(e.heading);
- if ((a+180) % 360 == b){
- average = ((a+b)/2 % 360); //can add 180 depending on rotation
- } else {
- average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) );
+let degrees = function(a) {
+ let d = a*180/Math.PI;
+ return (d+360)%360;
+};
+
+function average(samples){
+ let s = 0;
+ let c = 0;
+ for (let h of samples){
+ s += Math.sin(radians(h));
+ c += Math.cos(radians(h));
+ }
+ s /= samples.length;
+ c /= samples.length;
+ let result = degrees(Math.atan(s/c));
+
+ if (c < 0) result += 180;
+ if (s < 0 && c > 0) result += 360;
+
+ result%=360;
+ return result;
+}
+
+function onMag(e) {
+ if (!isNaN(e.heading)){
+ if (Bangle.isLocked() || (Bangle.getGPSFix() && Bangle.getGPSFix().lon))
+ state.avgComp = e.heading;
+ else {
+ state.compassSamples[state.sampleIndex++] = e.heading;
+ state.lastSample = Date.now();
+ if (state.sampleIndex > SAMPLES - 1){
+ state.sampleIndex = 0;
+ let avg = average(state.compassSamples);
+ state.avgComp = average([state.avgComp,avg]);
+ }
+ }
}
- print("Angle",compassHeading,e.heading, average);
- compassHeading = (compassHeading + degrees(average)) % 360;
- */
- state.compassHeading = Math.round(e.heading);
}
function onStep(e) {
@@ -73,12 +112,33 @@ function onPressure(e) {
}
}
+function onAcc (e){
+ state.acc = e;
+}
+
+function update(){
+ if (state.active){
+ start(false);
+ }
+ if (state.active == !(WIDGETS.gpstrek.width)) {
+ if(WIDGETS.gpstrek) WIDGETS.gpstrek.width = state.active?24:0;
+ Bangle.drawWidgets();
+ }
+}
+
function start(bg){
+ Bangle.removeListener('GPS', onGPS);
+ Bangle.removeListener("HRM", onPulse);
+ Bangle.removeListener("mag", onMag);
+ Bangle.removeListener("step", onStep);
+ Bangle.removeListener("pressure", onPressure);
+ Bangle.removeListener('accel', onAcc);
Bangle.on('GPS', onGPS);
Bangle.on("HRM", onPulse);
Bangle.on("mag", onMag);
Bangle.on("step", onStep);
Bangle.on("pressure", onPressure);
+ Bangle.on('accel', onAcc);
Bangle.setGPSPower(1, "gpstrek");
Bangle.setHRMPower(1, "gpstrek");
@@ -87,31 +147,30 @@ function start(bg){
if (bg){
if (!state.active) bgChanged = true;
state.active = true;
+ update();
saveState();
}
- Bangle.drawWidgets();
}
function stop(bg){
if (bg){
if (state.active) bgChanged = true;
state.active = false;
- saveState();
+ } else if (!state.active) {
+ Bangle.setGPSPower(0, "gpstrek");
+ Bangle.setHRMPower(0, "gpstrek");
+ Bangle.setCompassPower(0, "gpstrek");
+ Bangle.setBarometerPower(0, "gpstrek");
+ Bangle.removeListener('GPS', onGPS);
+ Bangle.removeListener("HRM", onPulse);
+ Bangle.removeListener("mag", onMag);
+ Bangle.removeListener("step", onStep);
+ Bangle.removeListener("pressure", onPressure);
+ Bangle.removeListener('accel', onAcc);
+ E.removeListener("kill", onKill);
}
- Bangle.drawWidgets();
-}
-
-function initState(){
- //cleanup volatile state here
- state.currentPos={};
- state.steps = Bangle.getStepCount();
- state.calibAltDiff = 0;
- state.up = 0;
- state.down = 0;
-}
-
-if (state.saved && state.saved < Date.now() - 60000){
- initState();
+ update();
+ saveState();
}
if (state.active){
@@ -123,11 +182,15 @@ WIDGETS["gpstrek"]={
width:state.active?24:0,
resetState: initState,
getState: function() {
+ if (state.saved && Date.now() - state.saved > 60000 || !state){
+ initState();
+ }
return state;
},
start:start,
stop:stop,
draw:function() {
+ update();
if (state.active){
g.reset();
g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y);
diff --git a/apps/groceryaug/ChangeLog b/apps/groceryaug/ChangeLog
new file mode 100644
index 000000000..906046782
--- /dev/null
+++ b/apps/groceryaug/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Refactor code to store grocery list in separate file
diff --git a/apps/groceryaug/README.md b/apps/groceryaug/README.md
new file mode 100644
index 000000000..aa1e62beb
--- /dev/null
+++ b/apps/groceryaug/README.md
@@ -0,0 +1,6 @@
+Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.
+
+Uses this API to do the OCR: https://rapidapi.com/serendi/api/pen-to-print-handwriting-ocr
+With a free account you get 100 API calls a month.
+
+
diff --git a/apps/groceryaug/app.js b/apps/groceryaug/app.js
new file mode 100644
index 000000000..00408abba
--- /dev/null
+++ b/apps/groceryaug/app.js
@@ -0,0 +1,25 @@
+var filename = 'grocery_list_aug.json';
+var settings = require("Storage").readJSON(filename,1)|| { products: [] };
+
+function updateSettings() {
+ require("Storage").writeJSON(filename, settings);
+ Bangle.buzz();
+}
+
+
+const mainMenu = settings.products.reduce(function(m, p, i){
+const name = p.name;
+ m[name] = {
+ value: p.ok,
+ format: v => v?'[x]':'[ ]',
+ onchange: v => {
+ settings.products[i].ok = v;
+ updateSettings();
+ }
+ };
+ return m;
+}, {
+ '': { 'title': 'Grocery list' }
+});
+mainMenu['< Back'] = ()=>{load();};
+E.showMenu(mainMenu);
diff --git a/apps/groceryaug/groceryaug-icon.js b/apps/groceryaug/groceryaug-icon.js
new file mode 100644
index 000000000..33b649647
--- /dev/null
+++ b/apps/groceryaug/groceryaug-icon.js
@@ -0,0 +1 @@
+E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAiAAMAADAAAwAAMAAiAAMAAAAAAAA/8zP/Mz/zM/8z/zM/8zP/Mz/zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA/////////////MzMzMzMzP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAARE////////////////////////zERAAARE////////////////////////zERAAERE////////////////////////zEREAERE////////////////////////zEREAAREzMzMzMzMzMzMzMzMzMzMzMzMzERAAABEREREREREREREREREREREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))
diff --git a/apps/groceryaug/groceryaug.html b/apps/groceryaug/groceryaug.html
new file mode 100644
index 000000000..6ed07df62
--- /dev/null
+++ b/apps/groceryaug/groceryaug.html
@@ -0,0 +1,145 @@
+
+
+
+
+
+ Enter/change API key
+
+
+
+
save API key
+
API key saved!
+
If you don't have an API key, you can create one here . You get 100 API calls a month for free.
+
+
+
+ Products
+
+
+ Upload
+
+
+
+
+
+
+
+
diff --git a/apps/groceryaug/groceryaug.png b/apps/groceryaug/groceryaug.png
new file mode 100644
index 000000000..895a6bbca
Binary files /dev/null and b/apps/groceryaug/groceryaug.png differ
diff --git a/apps/groceryaug/groceryaug_preview.gif b/apps/groceryaug/groceryaug_preview.gif
new file mode 100644
index 000000000..9b099f86e
Binary files /dev/null and b/apps/groceryaug/groceryaug_preview.gif differ
diff --git a/apps/groceryaug/metadata.json b/apps/groceryaug/metadata.json
new file mode 100644
index 000000000..13f377584
--- /dev/null
+++ b/apps/groceryaug/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "groceryaug",
+ "name": "Grocery Augmented",
+ "version": "0.02",
+ "description": "Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.",
+ "icon": "groceryaug.png",
+ "readme":"README.md",
+ "type": "app",
+ "tags": "tool,outdoors,shopping,list",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "custom": "groceryaug.html",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"groceryaug.app.js","url":"app.js"},
+ {"name":"groceryaug.img","url":"groceryaug-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json
index 052e82fe0..fad052544 100644
--- a/apps/ha/metadata.json
+++ b/apps/ha/metadata.json
@@ -5,7 +5,7 @@
"description": "Integrates your BangleJS into HomeAssistant.",
"icon": "ha.png",
"type": "app",
- "tags": "tool",
+ "tags": "tool,clkinfo",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"custom": "custom.html",
diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog
index 289c7ac2d..e2eb18be3 100644
--- a/apps/hcclock/ChangeLog
+++ b/apps/hcclock/ChangeLog
@@ -1,4 +1,5 @@
0.01: Base code
0.02: Saved settings when switching color scheme
0.03: Added Button 3 opening messages (if app is installed)
-0.04: Use `messages` library to check for new messages
\ No newline at end of file
+0.04: Use `messages` library to check for new messages
+0.05: Use `messages` library to open message GUI
\ No newline at end of file
diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js
index 9558c052b..f12a4733e 100644
--- a/apps/hcclock/hcclock.app.js
+++ b/apps/hcclock/hcclock.app.js
@@ -234,7 +234,7 @@ function handleMessages()
{
if(!hasMessages()) return;
E.showMessage("Loading Messages...");
- load("messages.app.js");
+ require("messages").openGUI();
}
function hasMessages()
diff --git a/apps/hcclock/metadata.json b/apps/hcclock/metadata.json
index b8f8c14b9..407114e25 100644
--- a/apps/hcclock/metadata.json
+++ b/apps/hcclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "hcclock",
"name": "Hi-Contrast Clock",
- "version": "0.04",
+ "version": "0.05",
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
"icon": "hcclock-icon.png",
"type": "clock",
diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog
index 62d93e606..fc8f2c950 100644
--- a/apps/health/ChangeLog
+++ b/apps/health/ChangeLog
@@ -14,3 +14,5 @@
0.13: Add support for internationalization
0.14: Move settings
0.15: Fix charts (fix #1366)
+0.16: Code tidyup, add back button in top left of health app graphs
+0.17: Add automatic translation of bar chart labels
diff --git a/apps/health/app.js b/apps/health/app.js
index c0a40bd93..844dd7241 100644
--- a/apps/health/app.js
+++ b/apps/health/app.js
@@ -1,6 +1,4 @@
function menuMain() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title: /*LANG*/"Health Tracking" },
/*LANG*/"< Back": () => load(),
@@ -12,8 +10,6 @@ function menuMain() {
}
function menuStepCount() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title:/*LANG*/"Steps" },
/*LANG*/"< Back": () => menuMain(),
@@ -23,8 +19,6 @@ function menuStepCount() {
}
function menuMovement() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title:/*LANG*/"Movement" },
/*LANG*/"< Back": () => menuMain(),
@@ -34,8 +28,6 @@ function menuMovement() {
}
function menuHRM() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title:/*LANG*/"Heart Rate" },
/*LANG*/"< Back": () => menuMain(),
@@ -48,22 +40,16 @@ function stepsPerHour() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuStepCount);
- barChart("HOUR", data);
+ barChart(/*LANG*/"HOUR", data);
}
function stepsPerDay() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuStepCount);
- barChart("DAY", data);
+ barChart(/*LANG*/"DAY", data);
}
function hrmPerHour() {
@@ -75,11 +61,8 @@ function hrmPerHour() {
if (h.bpm) cnt[h.hr]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuHRM);
- barChart("HOUR", data);
+ barChart(/*LANG*/"HOUR", data);
}
function hrmPerDay() {
@@ -91,37 +74,27 @@ function hrmPerDay() {
if (h.bpm) cnt[h.day]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuHRM);
- barChart("DAY", data);
+ barChart(/*LANG*/"DAY", data);
}
function movementPerHour() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuMovement);
- barChart("HOUR", data);
+ barChart(/*LANG*/"HOUR", data);
}
function movementPerDay() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuMovement);
- barChart("DAY", data);
+ barChart(/*LANG*/"DAY", data);
}
// Bar Chart Code
-
const w = g.getWidth();
const h = g.getHeight();
@@ -130,13 +103,10 @@ var chart_index;
var chart_max_datum;
var chart_label;
var chart_data;
-var swipe_enabled = false;
-var btn;
// find the max value in the array, using a loop due to array size
function max(arr) {
var m = -Infinity;
-
for(var i=0; i< arr.length; i++)
if(arr[i] > m) m = arr[i];
return m;
@@ -145,10 +115,8 @@ function max(arr) {
// find the end of the data, the array might be for 31 days but only have 2 days of data in it
function get_data_length(arr) {
var nlen = arr.length;
-
for(var i = arr.length - 1; i > 0 && arr[i] == 0; i--)
nlen--;
-
return nlen;
}
@@ -167,15 +135,11 @@ function drawBarChart() {
const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre
var bar_top;
var bar;
-
- g.setColor(g.theme.bg);
- g.fillRect(0,24,w,h);
+ g.reset().clearRect(0,24,w,h);
for (bar = 1; bar < 10; bar++) {
if (bar == 5) {
- g.setFont('6x8', 2);
- g.setFontAlign(0,-1);
- g.setColor(g.theme.fg);
+ g.setFont('6x8', 2).setFontAlign(0,-1).setColor(g.theme.fg);
g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
g.setColor("#00f");
} else {
@@ -189,45 +153,26 @@ function drawBarChart() {
bar_top = bar_bot;
g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
- g.setColor(g.theme.fg);
- g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
+ g.setColor(g.theme.fg).drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
}
}
-function next_bar() {
- chart_index = Math.min(data_len - 5, chart_index + 1);
-}
-
-function prev_bar() {
- // HOUR data starts at index 0, DAY data starts at index 1
- chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1);
-}
-
-Bangle.on('swipe', dir => {
- if (!swipe_enabled) return;
- if (dir == 1) prev_bar(); else next_bar();
- drawBarChart();
-});
-
-// use setWatch() as Bangle.setUI("updown",..) interacts with swipes
function setButton(fn) {
- // cancel callback, otherwise a slight up down movement will show the E.showMenu()
- Bangle.setUI("updown", undefined);
-
- if (process.env.HWVERSION == 1)
- btn = setWatch(fn, BTN2);
- else
- btn = setWatch(fn, BTN1);
-}
-
-function clearButton() {
- if (btn !== undefined) {
- clearWatch(btn);
- btn = undefined;
- }
+ Bangle.setUI({mode:"custom",
+ back:fn,
+ swipe:(lr,ud) => {
+ if (lr == 1) {
+ // HOUR data starts at index 0, DAY data starts at index 1
+ chart_index = Math.max((chart_label == /*LANG*/"DAY") ? -3 : -4, chart_index - 1);
+ } else if (lr<0) {
+ chart_index = Math.min(data_len - 5, chart_index + 1);
+ } else {
+ return fn();
+ }
+ drawBarChart();
+ }});
}
Bangle.loadWidgets();
Bangle.drawWidgets();
-
menuMain();
diff --git a/apps/health/metadata.json b/apps/health/metadata.json
index a038f67b5..d4eab1f38 100644
--- a/apps/health/metadata.json
+++ b/apps/health/metadata.json
@@ -1,7 +1,7 @@
{
"id": "health",
"name": "Health Tracking",
- "version": "0.15",
+ "version": "0.17",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",
diff --git a/apps/henkinen/ChangeLog b/apps/henkinen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/henkinen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/henkinen/README.md b/apps/henkinen/README.md
new file mode 100644
index 000000000..e17e86121
--- /dev/null
+++ b/apps/henkinen/README.md
@@ -0,0 +1,7 @@
+# Henkinen
+
+By Jukio Kallio
+
+A tiny app helping you to breath and relax.
+
+
diff --git a/apps/henkinen/app-icon.js b/apps/henkinen/app-icon.js
new file mode 100644
index 000000000..7c82a375d
--- /dev/null
+++ b/apps/henkinen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXH+cykc/C6UhgMSkMQiQXKBQsgiYFDmMCMBIIEmAWEDAUDC5nzBwogDMYgXHBoohJC4wuJEQwXG+ALDmUQgMjEYcPC5MhAYXxgAACj4ICVYYXGIwXzCwYABHAUwC5HyEwXwC4pEC+MvC4/xEoUQC4sBHIQlCC4vwIxBIEGYQXFmJKCC45ECfQQXIRoiRGC5EiOxB4EBwQXdI653XU67XX+QJCPAwrC+JKCC4v/gZIIHIUwCAQXGkIDCSIg4C/8SC5PwEwX/mUQgMjAwXzJQQXH+ZICAA8wEYYXGBgoAEEQoXHGBIhFC44OBcgQADmIgFC5H/kAYEmMCBooXDp4KFkMBiUhiCjDAAX0C5RjBmUjPo4XMABQXEMAwALCwgwRFwowRCwwwPFw4xOCpIArA"))
diff --git a/apps/henkinen/app.js b/apps/henkinen/app.js
new file mode 100644
index 000000000..d7c7bd5ed
--- /dev/null
+++ b/apps/henkinen/app.js
@@ -0,0 +1,127 @@
+// Henkinen
+//
+// Bangle.js 2 breathing helper
+// by Jukio Kallio
+// www.jukiokallio.com
+
+require("FontHaxorNarrow7x17").add(Graphics);
+
+// settings
+const breath = {
+ theme: "default",
+ x:0, y:0, w:0, h:0,
+ size: 60,
+
+ bgcolor: g.theme.bg,
+ incolor: g.theme.fg,
+ keepcolor: g.theme.fg,
+ outcolor: g.theme.fg,
+
+ font: "HaxorNarrow7x17", fontsize: 1,
+ textcolor: g.theme.fg,
+ texty: 18,
+
+ in: 4000,
+ keep: 7000,
+ out: 8000
+};
+
+// set some additional settings
+breath.w = g.getWidth(); // size of the background
+breath.h = g.getHeight();
+breath.x = breath.w * 0.5; // position of the circles
+breath.y = breath.h * 0.45;
+breath.texty = breath.y + breath.size + breath.texty; // text position
+
+var wait = 100; // wait time, normally a minute
+var time = 0; // for time keeping
+
+
+// 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();
+ }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+ // make date object
+ var date = new Date();
+
+ // update current time
+ time += wait - (Date.now() % wait);
+ if (time > breath.in + breath.keep + breath.out) time = 0; // reset time
+
+ // Reset the state of the graphics library
+ g.reset();
+
+ // Clear the area where we want to draw the time
+ g.setColor(breath.bgcolor);
+ g.fillRect(0, 0, breath.w, breath.h);
+
+ // calculate circle size
+ var circle = 0;
+ if (time < breath.in) {
+ // breath in
+ circle = time / breath.in;
+ g.setColor(breath.incolor);
+
+ } else if (time < breath.in + breath.keep) {
+ // keep breath
+ circle = 1;
+ g.setColor(breath.keepcolor);
+
+ } else if (time < breath.in + breath.keep + breath.out) {
+ // breath out
+ circle = ((breath.in + breath.keep + breath.out) - time) / breath.out;
+ g.setColor(breath.outcolor);
+
+ }
+
+ // draw breath circle
+ g.fillCircle(breath.x, breath.y, breath.size * circle);
+
+ // breath area
+ g.setColor(breath.textcolor);
+ g.drawCircle(breath.x, breath.y, breath.size);
+
+ // draw text
+ g.setFontAlign(0,0).setFont(breath.font, breath.fontsize).setColor(breath.textcolor);
+
+ if (time < breath.in) {
+ // breath in
+ g.drawString("Breath in", breath.x, breath.texty);
+
+ } else if (time < breath.in + breath.keep) {
+ // keep breath
+ g.drawString("Keep it in", breath.x, breath.texty);
+
+ } else if (time < breath.in + breath.keep + breath.out) {
+ // breath out
+ g.drawString("Breath out", breath.x, breath.texty);
+
+ }
+
+ // queue draw
+ queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// keep LCD on
+Bangle.setLCDPower(1);
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/henkinen/app.png b/apps/henkinen/app.png
new file mode 100644
index 000000000..575ecbcd4
Binary files /dev/null and b/apps/henkinen/app.png differ
diff --git a/apps/henkinen/metadata.json b/apps/henkinen/metadata.json
new file mode 100644
index 000000000..1f1bb77fc
--- /dev/null
+++ b/apps/henkinen/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "henkinen",
+ "name": "Henkinen - Tiny Breathing Helper",
+ "shortName":"Henkinen",
+ "version":"0.01",
+ "description": "A tiny app helping you to breath and relax.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot1.png"}],
+ "tags": "outdoors",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"henkinen.app.js","url":"app.js"},
+ {"name":"henkinen.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/henkinen/screenshot1.png b/apps/henkinen/screenshot1.png
new file mode 100644
index 000000000..938494673
Binary files /dev/null and b/apps/henkinen/screenshot1.png differ
diff --git a/apps/hrmmar/README.md b/apps/hrmmar/README.md
new file mode 100644
index 000000000..ff90d9156
--- /dev/null
+++ b/apps/hrmmar/README.md
@@ -0,0 +1,11 @@
+# HRM Motion Artifacts removal
+
+Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these.
+
+## Settings
+
+* **MA removal**
+
+Select the algorithm to Remove Motion artifacts:
+ - None: (default) No Motion Artifact removal.
+ - fft elim: (*experimental*) Remove Motion Artifacts by cutting out the frequencies from the HRM frequency spectrum that are noisy in acceleration spectrum. Under motion this can report a heart rate that is closer to the real one but will fail if motion frequency and heart rate overlap.
diff --git a/apps/hrmmar/app.png b/apps/hrmmar/app.png
new file mode 100644
index 000000000..9db19be37
Binary files /dev/null and b/apps/hrmmar/app.png differ
diff --git a/apps/hrmmar/boot.js b/apps/hrmmar/boot.js
new file mode 100644
index 000000000..52d88c313
--- /dev/null
+++ b/apps/hrmmar/boot.js
@@ -0,0 +1,40 @@
+{
+ let bpm_corrected; // result of algorithm
+
+ const updateHrm = (bpm) => {
+ bpm_corrected = bpm;
+ };
+
+ Bangle.on('HRM', (hrm) => {
+ if (bpm_corrected > 0) {
+ // replace bpm data in event
+ hrm.bpm_orig = hrm.bpm;
+ hrm.confidence_orig = hrm.confidence;
+ hrm.bpm = bpm_corrected;
+ hrm.confidence = 0;
+ }
+ });
+
+ let run = () => {
+ const settings = Object.assign({
+ mAremoval: 0
+ }, require("Storage").readJSON("hrmmar.json", true) || {});
+
+ // select motion artifact removal algorithm
+ switch(settings.mAremoval) {
+ case 1:
+ require("hrmfftelim").run(settings, updateHrm);
+ break;
+ }
+ }
+
+ // override setHRMPower so we can run our code on HRM enable
+ const oldSetHRMPower = Bangle.setHRMPower;
+ Bangle.setHRMPower = function(on, id) {
+ if (on && run !== undefined) {
+ run();
+ run = undefined; // Make sure we run only once
+ }
+ return oldSetHRMPower(on, id);
+ };
+}
diff --git a/apps/hrmmar/fftelim.js b/apps/hrmmar/fftelim.js
new file mode 100644
index 000000000..98b7f33ad
--- /dev/null
+++ b/apps/hrmmar/fftelim.js
@@ -0,0 +1,190 @@
+exports.run = (settings, updateHrm) => {
+ const SAMPLE_RATE = 12.5;
+ const NUM_POINTS = 256; // fft size
+ const ACC_PEAKS = 2; // remove this number of ACC peaks
+
+ // ringbuffers
+ const hrmvalues = new Int16Array(8*SAMPLE_RATE);
+ const accvalues = new Int16Array(8*SAMPLE_RATE);
+ // fft buffers
+ const hrmfftbuf = new Int16Array(NUM_POINTS);
+ const accfftbuf = new Int16Array(NUM_POINTS);
+ let BPM_est_1 = 0;
+ let BPM_est_2 = 0;
+
+ let hrmdata;
+ let idx=0, wraps=0;
+
+ // init settings
+ Bangle.setOptions({hrmPollInterval: 40, powerSave: false}); // hrm=25Hz
+ Bangle.setPollInterval(80); // 12.5Hz
+
+ calcfft = (values, idx, normalize, fftbuf) => {
+ fftbuf.fill(0);
+ let i_out=0;
+ let avg = 0;
+ if (normalize) {
+ const sum = values.reduce((a, b) => a + b, 0);
+ avg = sum/values.length;
+ }
+ // sort ringbuffer to fft buffer
+ for(let i_in=idx; i_in {
+ let maxVal = -Number.MAX_VALUE;
+ let maxIdx = 0;
+
+ values.forEach((value,i) => {
+ if (value > maxVal) {
+ maxVal = value;
+ maxIdx = i;
+ }
+ });
+ return {idx: maxIdx, val: maxVal};
+ };
+
+ getSign = (value) => {
+ return value < 0 ? -1 : 1;
+ };
+
+ // idx in fft buffer to frequency
+ getFftFreq = (idx, rate, size) => {
+ return idx*rate/(size-1);
+ };
+
+ // frequency to idx in fft buffer
+ getFftIdx = (freq, rate, size) => {
+ return Math.round(freq*(size-1)/rate);
+ };
+
+ calc2ndDeriative = (values) => {
+ const result = new Int16Array(values.length-2);
+ for(let i=1; i {
+ // fft
+ const ppg_fft = calcfft(hrmvalues, idx, true, hrmfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
+ const acc_fft = calcfft(accvalues, idx, false, accfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
+
+ // remove spectrum that have peaks in acc fft from ppg fft
+ const accGlobalMax = getMax(acc_fft);
+ const acc2nddiff = calc2ndDeriative(acc_fft); // calculate second derivative
+ for(let iClean=0; iClean < ACC_PEAKS; iClean++) {
+ // get max peak in ACC
+ const accMax = getMax(acc_fft);
+
+ if (accMax.val >= 10 && accMax.val/accGlobalMax.val > 0.75) {
+ // set all values in PPG FFT to zero until second derivative of ACC has zero crossing
+ for (let k = accMax.idx-1; k>=0; k--) {
+ ppg_fft[k] = 0;
+ acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
+ if (k-2 > 0 && getSign(acc2nddiff[k-1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
+ break;
+ }
+ }
+ // set all values in PPG FFT to zero until second derivative of ACC has zero crossing
+ for (let k = accMax.idx; k < acc_fft.length-1; k++) {
+ ppg_fft[k] = 0;
+ acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
+ if (k-2 >= 0 && getSign(acc2nddiff[k+1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
+ break;
+ }
+ }
+ }
+ }
+
+ // bpm result is maximum peak in PPG fft
+ const hrRangeMax = getMax(ppg_fft.subarray(rangeIdx[0], rangeIdx[1]));
+ const hrTotalMax = getMax(ppg_fft);
+ const maxDiff = hrTotalMax.val/hrRangeMax.val;
+ let idxMaxPPG = hrRangeMax.idx+rangeIdx[0]; // offset range limit
+
+ if ((maxDiff > 3 && idxMaxPPG != hrTotalMax.idx) || hrRangeMax.val === 0) { // prevent tracking from loosing the real heart rate by checking the full spectrum
+ if (hrTotalMax.idx > idxMaxPPG) {
+ idxMaxPPG = idxMaxPPG+Math.ceil(6/freqStep); // step 6 BPM up into the direction of max peak
+ } else {
+ idxMaxPPG = idxMaxPPG-Math.ceil(2/freqStep); // step 2 BPM down into the direction of max peak
+ }
+ }
+
+ idxMaxPPG = idxMaxPPG + minFreqIdx;
+ const BPM_est_0 = getFftFreq(idxMaxPPG, SAMPLE_RATE, NUM_POINTS)*60;
+
+ // smooth with moving average
+ let BPM_est_res;
+ if (BPM_est_2 > 0) {
+ BPM_est_res = 0.9*BPM_est_0 + 0.05*BPM_est_1 + 0.05*BPM_est_2;
+ } else {
+ BPM_est_res = BPM_est_0;
+ }
+
+ return BPM_est_res.toFixed(1);
+ };
+
+ Bangle.on('HRM-raw', (hrm) => {
+ hrmdata = hrm;
+ });
+
+ Bangle.on('accel', (acc) => {
+ if (hrmdata !== undefined) {
+ hrmvalues[idx] = hrmdata.filt;
+ accvalues[idx] = acc.x*1000 + acc.y*1000 + acc.z*1000;
+ idx++;
+ if (idx >= 8*SAMPLE_RATE) {
+ idx = 0;
+ wraps++;
+ }
+
+ if (idx % (SAMPLE_RATE*2) == 0) { // every two seconds
+ if (wraps === 0) { // use rate of firmware until hrmvalues buffer is filled
+ updateHrm(undefined);
+ BPM_est_2 = BPM_est_1;
+ BPM_est_1 = hrmdata.bpm;
+ } else {
+ let bpm_result;
+ if (hrmdata.confidence >= 90) { // display firmware value if good
+ bpm_result = hrmdata.bpm;
+ updateHrm(undefined);
+ } else {
+ bpm_result = calculate(idx);
+ bpm_corrected = bpm_result;
+ updateHrm(bpm_result);
+ }
+ BPM_est_2 = BPM_est_1;
+ BPM_est_1 = bpm_result;
+
+ // set search range of next BPM
+ const est_res_idx = getFftIdx(bpm_result/60, SAMPLE_RATE, NUM_POINTS)-minFreqIdx;
+ rangeIdx = [est_res_idx-maxBpmDiffIdxDown, est_res_idx+maxBpmDiffIdxUp];
+ if (rangeIdx[0] < 0) {
+ rangeIdx[0] = 0;
+ }
+ if (rangeIdx[1] > maxFreqIdx-minFreqIdx) {
+ rangeIdx[1] = maxFreqIdx-minFreqIdx;
+ }
+ }
+ }
+ }
+ });
+};
diff --git a/apps/hrmmar/metadata.json b/apps/hrmmar/metadata.json
new file mode 100644
index 000000000..232ff64a7
--- /dev/null
+++ b/apps/hrmmar/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "hrmmar",
+ "name": "HRM Motion Artifacts removal",
+ "shortName":"HRM MA removal",
+ "icon": "app.png",
+ "version":"0.01",
+ "description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data.",
+ "type": "bootloader",
+ "tags": "health",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"hrmmar.boot.js","url":"boot.js"},
+ {"name":"hrmfftelim","url":"fftelim.js"},
+ {"name":"hrmmar.settings.js","url":"settings.js"}
+ ],
+ "data": [{"name":"hrmmar.json"}]
+}
diff --git a/apps/hrmmar/settings.js b/apps/hrmmar/settings.js
new file mode 100644
index 000000000..3c6e62c91
--- /dev/null
+++ b/apps/hrmmar/settings.js
@@ -0,0 +1,26 @@
+(function(back) {
+ var FILE = "hrmmar.json";
+ // Load settings
+ var settings = Object.assign({
+ mAremoval: 0,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "HRM MA removal" },
+ "< Back" : () => back(),
+ 'MA removal': {
+ value: settings.mAremoval,
+ min: 0, max: 1,
+ format: v => ["None", "fft elim."][v],
+ onchange: v => {
+ settings.mAremoval = v;
+ writeSettings();
+ }
+ },
+ });
+})
diff --git a/apps/hworldclock/ChangeLog b/apps/hworldclock/ChangeLog
index a4bd84390..8c1517842 100644
--- a/apps/hworldclock/ChangeLog
+++ b/apps/hworldclock/ChangeLog
@@ -7,3 +7,5 @@
0.21: Add Settings
0.22: Use default Bangle formatter for booleans
0.23: Added note to configure position in "my location" if not done yet. Small fixes.
+0.24: Added fast load
+0.25: Minor code optimization
diff --git a/apps/hworldclock/app.js b/apps/hworldclock/app.js
index a0fb4cd20..c80b712da 100644
--- a/apps/hworldclock/app.js
+++ b/apps/hworldclock/app.js
@@ -1,3 +1,5 @@
+{ // must be inside our own scope here so that when we are unloaded everything disappears
+
// ------- Settings file
const SETTINGSFILE = "hworldclock.json";
var secondsMode;
@@ -153,15 +155,15 @@ function updatePos() {
function drawSeconds() {
// get date
- var d = new Date();
- var da = d.toString().split(" ");
+ let d = new Date();
+ let da = d.toString().split(" ");
// default draw styles
g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0);
// draw time
- var time = da[4].split(":");
- var seconds = time[2];
+ let time = da[4].split(":");
+ let seconds = time[2];
g.setFont("5x9Numeric7Seg",primaryTimeFontSize - 3);
if (g.theme.dark) {
@@ -184,15 +186,15 @@ function drawSeconds() {
function draw() {
// get date
- var d = new Date();
- var da = d.toString().split(" ");
+ let d = new Date();
+ let da = d.toString().split(" ");
// default draw styles
g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0);
// draw time
- var time = da[4].split(":");
- var hours = time[0],
+ let time = da[4].split(":");
+ let hours = time[0],
minutes = time[1];
@@ -223,7 +225,7 @@ function draw() {
// am / PM ?
if (_12hour){
//do 12 hour stuff
- //var ampm = require("locale").medidian(new Date()); Not working
+ //let ampm = require("locale").medidian(new Date()); Not working
g.setFont("Vector", 17);
g.drawString(ampm, xyCenterSeconds, yAmPm, true);
}
@@ -232,14 +234,14 @@ function draw() {
// draw Day, name of month, Date
//DATE
- var localDate = require("locale").date(new Date(), 1);
+ let localDate = require("locale").date(new Date(), 1);
localDate = localDate.substring(0, localDate.length - 5);
g.setFont("Vector", 17);
g.drawString(require("locale").dow(new Date(), 1).toUpperCase() + ", " + localDate, xyCenter, yposDate, true);
g.setFont(font, primaryDateFontSize);
// set gmt to UTC+0
- var gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000);
+ let gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000);
// Loop through offset(s) and render
offsets.forEach((offset, index) => {
@@ -249,7 +251,7 @@ function draw() {
if (offsets.length === 1) {
- var date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)];
+ let date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)];
// For a single secondary timezone, draw it bigger and drop time zone to second line
const xOffset = 30;
g.setFont(font, secondaryTimeFontSize).drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true);
@@ -295,8 +297,18 @@ g.clear();
// Init the settings of the app
loadMySettings();
-// Show launcher when button pressed
-Bangle.setUI("clock");
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
+ if (PosInterval) clearInterval(PosInterval);
+ PosInterval = undefined;
+ if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
+ drawTimeoutSeconds = undefined;
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }});
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -307,7 +319,7 @@ draw();
if (!Bangle.isLocked()) { // Initial state
if (showSunInfo) {
- if (PosInterval != 0) clearInterval(PosInterval);
+ if (PosInterval != 0 && typeof PosInterval != 'undefined') clearInterval(PosInterval);
PosInterval = setInterval(updatePos, 60*10E3); // refesh every 10 mins
updatePos();
}
@@ -333,7 +345,7 @@ if (!Bangle.isLocked()) { // Initial state
drawTimeout = undefined;
if (showSunInfo) {
- if (PosInterval != 0) clearInterval(PosInterval);
+ if (PosInterval != 0 && typeof PosInterval != 'undefined') clearInterval(PosInterval);
PosInterval = setInterval(updatePos, 60*60E3); // refesh every 60 mins
updatePos();
}
@@ -378,4 +390,5 @@ Bangle.on('lock',on=>{
}
draw(); // draw immediately, queue redraw
}
- });
\ No newline at end of file
+ });
+}
\ No newline at end of file
diff --git a/apps/hworldclock/metadata.json b/apps/hworldclock/metadata.json
index 653cfc59c..e26599373 100644
--- a/apps/hworldclock/metadata.json
+++ b/apps/hworldclock/metadata.json
@@ -2,7 +2,7 @@
"id": "hworldclock",
"name": "Hanks World Clock",
"shortName": "Hanks World Clock",
- "version": "0.23",
+ "version": "0.25",
"description": "Current time zone plus up to three others",
"allow_emulator":true,
"icon": "app.png",
diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog
index afea3263a..b03599ae6 100644
--- a/apps/iconlaunch/ChangeLog
+++ b/apps/iconlaunch/ChangeLog
@@ -4,3 +4,17 @@
0.04: Support new fast app switching
0.05: Allow to directly eval apps instead of loading
0.06: Cache apps for faster start
+0.07: Read app icons on demand
+ Add swipe-to-exit
+0.08: Only use fast loading for switching to clock to prevent problems in full screen apps
+0.09: Remove fast load option since clocks containing Bangle.loadWidgets are now always normally loaded
+0.10: changed the launch.json file name in iconlaunch.json ( launch.cache.json -> iconlaunch.cache.json)
+ used Object.assing for the settings
+ fix cache not deleted when "showClocks" options is changed
+ added timeOut to return to the clock
+0.11: Cleanup timeout when changing to clock
+ Reset timeout on swipe and drag
+0.12: Use Bangle.load and Bangle.showClock
+0.13: Fix automatic switch to clock
+0.14: Revert use of Bangle.load to classic load calls since widgets would
+still be loaded when they weren't supposed to.
diff --git a/apps/iconlaunch/README.md b/apps/iconlaunch/README.md
index 0b67494ce..0d36fdeb4 100644
--- a/apps/iconlaunch/README.md
+++ b/apps/iconlaunch/README.md
@@ -10,7 +10,3 @@ This launcher shows 9 apps per screen, making it much faster to navigate versus
## Technical note
The app uses `E.showScroller`'s code in the app but not the function itself because `E.showScroller` doesn't report the position of a press to the select function.
-
-### Fastload option
-
-Fastload clears up the memory used by the launcher and directly evals the code of the app to load. This means if widgets are loaded (fullscreen option) it is possible that widgets stay loaded in apps not expecting that and the widgets may draw over the app.
diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js
index 0f20efa67..ccc39f3bb 100644
--- a/apps/iconlaunch/app.js
+++ b/apps/iconlaunch/app.js
@@ -1,31 +1,35 @@
{
const s = require("Storage");
- const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false };
+ const settings = Object.assign({
+ showClocks: true,
+ fullscreen: false,
+ direct: false,
+ oneClickExit: false,
+ swipeExit: false,
+ timeOut:"Off"
+ }, s.readJSON("iconlaunch.json", true) || {});
+
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}
- let launchCache = s.readJSON("launch.cache.json", true)||{};
- let launchHash = require("Storage").hash(/\.info/);
+ let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
+ let launchHash = s.hash(/\.info/);
if (launchCache.hash!=launchHash) {
launchCache = {
hash : launchHash,
apps : s.list(/\.info$/)
- .map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};})
+ .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)=>{
- var n=(0|a.sortorder)-(0|b.sortorder);
+ let n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.nameb.name) return 1;
return 0;
}) };
- s.writeJSON("launch.cache.json", launchCache);
+ s.writeJSON("iconlaunch.cache.json", launchCache);
}
- let apps = launchCache.apps;
- apps.forEach((app) => {
- if (app.icon) app.icon = s.read(app.icon);
- });
let scroll = 0;
let selectedItem = -1;
const R = Bangle.appRect;
@@ -37,12 +41,13 @@
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 (!apps[i]) break;
+ if (!launchCache.apps[i]) break;
x += whitespace;
- if (!apps[i].icon) {
+ if (!launchCache.apps[i].icon) {
g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2);
} else {
- g.drawImage(apps[i].icon, x + r.x, r.y);
+ 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 (selectedItem == i) {
g.drawRect(
@@ -57,7 +62,7 @@
drawText(itemI);
};
let drawItemAuto = function(i) {
- var y = idxToY(i);
+ let y = idxToY(i);
g.reset().setClipRect(R.x, y, R.x2, y + itemSize);
drawItem(i, {
x: R.x,
@@ -69,7 +74,7 @@
};
let lastIsDown = false;
let drawText = function(i) {
- const selectedApp = apps[selectedItem];
+ const selectedApp = launchCache.apps[selectedItem];
const idy = (selectedItem - (selectedItem % 3)) / 3;
if (!selectedApp || i != idy) return;
const appY = idxToY(idy) + iconSize / 2;
@@ -87,17 +92,17 @@
let selectItem = function(id, e) {
const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1);
const appId = id * appsN + iconN;
- if( settings.direct && apps[appId])
+ if( settings.direct && launchCache.apps[appId])
{
- loadApp(apps[appId].src);
+ load(launchCache.apps[appId].src);
return;
}
- if (appId == selectedItem && apps[appId]) {
- const app = apps[appId];
+ if (appId == selectedItem && launchCache.apps[appId]) {
+ const app = launchCache.apps[appId];
if (!app.src || s.read(app.src) === undefined) {
E.showMessage( /*LANG*/ "App Source\nNot found");
} else {
- loadApp(app.src);
+ load(app.src);
}
}
selectedItem = appId;
@@ -112,9 +117,9 @@
let drawItems = function() {
g.reset().clearRect(R.x, R.y, R.x2, R.y2);
g.setClipRect(R.x, R.y, R.x2, R.y2);
- var a = YtoIdx(R.y);
- var b = Math.min(YtoIdx(R.y2), 99);
- for (var i = a; i <= b; i++)
+ let a = YtoIdx(R.y);
+ let b = Math.min(YtoIdx(R.y2), 99);
+ for (let i = a; i <= b; i++)
drawItem(i, {
x: R.x,
y: idxToY(i),
@@ -125,8 +130,9 @@
};
drawItems();
g.flip();
- const itemsN = Math.ceil(apps.length / appsN);
+ const itemsN = Math.ceil(launchCache.apps.length / appsN);
let onDrag = function(e) {
+ updateTimeout();
g.setColor(g.theme.fg);
g.setBgColor(g.theme.bg);
let dy = e.dy;
@@ -171,45 +177,32 @@
}
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
};
- Bangle.setUI({
+ let mode = {
mode: "custom",
drag: onDrag,
touch: (_, e) => {
if (e.y < R.y - 4) return;
- var i = YtoIdx(e.y);
+ updateTimeout();
+ let i = YtoIdx(e.y);
selectItem(i, e);
},
- });
- const returnToClock = function() {
- loadApp(".bootcde");
- };
- let watch;
- let loadApp;
- if (settings.fastload){
- loadApp = function(name) {
- Bangle.setUI();
- if (watch) clearWatch(watch);
- apps = [];
- delete drawItemAuto;
- delete drawText;
- delete selectItem;
- delete onDrag;
- delete drawItems;
- delete drawItem;
- delete returnToClock;
- delete idxToY;
- delete YtoIdx;
- delete settings;
- setTimeout(eval, 0, s.read(name));
- return;
- };
- } else {
- loadApp = function(name) {
- load(name);
+ swipe: (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } },
+ btn: _=> { if (settings.oneClickExit) Bangle.showClock(); },
+ remove: function() {
+ if (timeout) clearTimeout(timeout);
}
- }
-
- if (settings.oneClickExit) {
- watch = setWatch(returnToClock, BTN1);
- }
+ };
+
+ let timeout;
+ const updateTimeout = function(){
+ if (settings.timeOut!="Off"){
+ let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(Bangle.showClock,time*1000);
+ }
+ };
+
+ updateTimeout();
+
+ Bangle.setUI(mode);
}
diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json
index d544da73e..155e7bd9b 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.06",
+ "version": "0.14",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",
@@ -12,6 +12,7 @@
{ "name": "iconlaunch.app.js", "url": "app.js" },
{ "name": "iconlaunch.settings.js", "url": "settings.js" }
],
+ "data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
"readme": "README.md"
}
diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js
index 449a1c096..f4c0599f7 100644
--- a/apps/iconlaunch/settings.js
+++ b/apps/iconlaunch/settings.js
@@ -1,21 +1,29 @@
// make sure to enclose the function in parentheses
(function(back) {
+ const s = require("Storage");
let settings = Object.assign({
showClocks: true,
- fullscreen: false
- }, require("Storage").readJSON("launch.json", true) || {});
+ fullscreen: false,
+ direct: false,
+ oneClickExit: false,
+ swipeExit: false,
+ timeOut:"Off"
+ }, s.readJSON("iconlaunch.json", true) || {});
- let fonts = g.getFonts();
function save(key, value) {
settings[key] = value;
- require("Storage").write("launch.json",settings);
+ s.write("iconlaunch.json",settings);
}
+ const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
const appMenu = {
"": { "title": /*LANG*/"Launcher" },
/*LANG*/"< Back": back,
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
- onchange: (m) => { save("showClocks", m) }
+ onchange: (m) => {
+ save("showClocks", m);
+ s.erase("iconlaunch.cache.json"); //delete the cache app list
+ }
},
/*LANG*/"Fullscreen": {
value: settings.fullscreen == true,
@@ -29,10 +37,18 @@
value: settings.oneClickExit == true,
onchange: (m) => { save("oneClickExit", m) }
},
- /*LANG*/"Fastload": {
- value: settings.fastload == true,
- onchange: (m) => { save("fastload", m) }
- }
+ /*LANG*/"Swipe exit": {
+ value: settings.swipeExit == true,
+ onchange: m => { save("swipeExit", m) }
+ },
+ /*LANG*/'Time Out': {
+ value: timeOutChoices.indexOf(settings.timeOut),
+ min: 0, max: timeOutChoices.length-1,
+ format: v => timeOutChoices[v],
+ onchange: m => {
+ save("timeOut", timeOutChoices[m]);
+ }
+ },
};
E.showMenu(appMenu);
});
diff --git a/apps/imageclock/ChangeLog b/apps/imageclock/ChangeLog
index f81bbf185..df7bfd0de 100644
--- a/apps/imageclock/ChangeLog
+++ b/apps/imageclock/ChangeLog
@@ -12,3 +12,9 @@
0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on
0.11: Additional option in customizer to force drawing directly
Fix some problems in handling timeouts
+0.12: Use widget_utils module
+ Fix colorsetting in promises in generated code
+ Some performance improvements by caching lookups
+ Activate UI after first draw is complete to prevent drawing over launcher
+0.13: Use widget_utils swipeOn()
+ Allows minification by combining all but picture data into one file
diff --git a/apps/imageclock/app.js b/apps/imageclock/app.js
index ff3f5a62d..90c6163cd 100644
--- a/apps/imageclock/app.js
+++ b/apps/imageclock/app.js
@@ -1,7 +1,12 @@
-let unlockedDrawInterval = [];
-let lockedDrawInterval = [];
-let showWidgets = false;
-let firstDraw = true;
+let s = {};
+// unlocked draw intervals
+s.udi = [];
+// locked draw intervals
+s.ldi = [];
+// full draw
+s.fd = true;
+// performance log
+s.pl = {};
{
let x = g.getWidth()/2;
@@ -21,12 +26,10 @@ let firstDraw = true;
let precompiledJs = eval(require("Storage").read("imageclock.draw.js"));
let settings = require('Storage').readJSON("imageclock.json", true) || {};
- let performanceLog = {};
-
let startPerfLog = () => {};
let endPerfLog = () => {};
Bangle.printPerfLog = () => {print("Deactivated");};
- Bangle.resetPerfLog = () => {performanceLog = {};};
+ Bangle.resetPerfLog = () => {s.pl = {};};
let colormap={
"#000":0,
@@ -64,35 +67,37 @@ let firstDraw = true;
if (settings.perflog){
startPerfLog = function(name){
let time = getTime();
- if (!performanceLog.start) performanceLog.start={};
- performanceLog.start[name] = time;
+ if (!s.pl.start) s.pl.start={};
+ s.pl.start[name] = time;
};
- endPerfLog = function (name){
+ endPerfLog = function (name, once){
let time = getTime();
- if (!performanceLog.last) performanceLog.last={};
- let duration = time - performanceLog.start[name];
- performanceLog.last[name] = duration;
- if (!performanceLog.cum) performanceLog.cum={};
- if (!performanceLog.cum[name]) performanceLog.cum[name] = 0;
- performanceLog.cum[name] += duration;
- if (!performanceLog.count) performanceLog.count={};
- if (!performanceLog.count[name]) performanceLog.count[name] = 0;
- performanceLog.count[name]++;
+ if (!s.pl.start[name]) return;
+ if (!s.pl.last) s.pl.last={};
+ let duration = time - s.pl.start[name];
+ s.pl.last[name] = duration;
+ if (!s.pl.cum) s.pl.cum={};
+ if (!s.pl.cum[name]) s.pl.cum[name] = 0;
+ s.pl.cum[name] += duration;
+ if (!s.pl.count) s.pl.count={};
+ if (!s.pl.count[name]) s.pl.count[name] = 0;
+ s.pl.count[name]++;
+ if (once){s.pl.start[name] = undefined}
};
Bangle.printPerfLog = function(){
let result = "";
let keys = [];
- for (let c in performanceLog.cum){
+ for (let c in s.pl.cum){
keys.push(c);
}
keys.sort();
for (let k of keys){
- print(k, "last:", (performanceLog.last[k] * 1000).toFixed(0), "average:", (performanceLog.cum[k]/performanceLog.count[k]*1000).toFixed(0), "count:", performanceLog.count[k], "total:", (performanceLog.cum[k] * 1000).toFixed(0));
+ print(k, "last:", (s.pl.last[k] * 1000).toFixed(0), "average:", (s.pl.cum[k]/s.pl.count[k]*1000).toFixed(0), "count:", s.pl.count[k], "total:", (s.pl.cum[k] * 1000).toFixed(0));
}
};
}
-
+ startPerfLog("fullDraw");
startPerfLog("loadFunctions");
let delayTimeouts = {};
@@ -202,27 +207,39 @@ let firstDraw = true;
let firstDigitY = element.Y;
let imageIndex = element.ImageIndex ? element.ImageIndex : 0;
- let firstImage;
- if (imageIndex){
- firstImage = getByPath(resources, [], "" + (0 + imageIndex));
- } else {
- firstImage = getByPath(resources, element.ImagePath, 0);
+ let firstImage = element.cachedFirstImage;
+ if (!firstImage && !element.cachedFirstImageMissing){
+ if (imageIndex){
+ firstImage = getByPath(resources, [], "" + (0 + imageIndex));
+ } else {
+ firstImage = getByPath(resources, element.ImagePath, 0);
+ }
+ element.cachedFirstImage = firstImage;
+ if (!firstImage) element.cachedFirstImageMissing = true;
}
- let minusImage;
- if (imageIndexMinus){
- minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
- } else {
- minusImage = getByPath(resources, element.ImagePath, "minus");
+ let minusImage = element.cachedMinusImage;
+ if (!minusImage && !element.cachedMinusImageMissing){
+ if (imageIndexMinus){
+ minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
+ } else {
+ minusImage = getByPath(resources, element.ImagePath, "minus");
+ }
+ element.cachedMinusImage = minusImage;
+ if (!minusImage) element.cachedMinusImageMissing = true;
}
- let unitImage;
+ let unitImage = element.cachedUnitImage;
//print("Get image for unit", imageIndexUnit);
- if (imageIndexUnit !== undefined){
- unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
- //print("Unit image is", unitImage);
- } else if (element.Unit){
- unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
+ if (!unitImage && !element.cachedUnitImageMissing){
+ if (imageIndexUnit !== undefined){
+ unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
+ //print("Unit image is", unitImage);
+ } else if (element.Unit){
+ unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
+ }
+ unitImage = element.cachedUnitImage;
+ if (!unitImage) element.cachedUnitImageMissing = true;
}
let numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing);
@@ -292,14 +309,7 @@ let firstDraw = true;
if (resource){
prepareImg(resource);
//print("lastElem", typeof resource)
- if (resource) {
- element.cachedImage[cacheKey] = resource;
- //print("cache res ",typeof element.cachedImage[cacheKey]);
- } else {
- element.cachedImage[cacheKey] = null;
- //print("cache null",typeof element.cachedImage[cacheKey]);
- //print("Could not create image from", resource);
- }
+ element.cachedImage[cacheKey] = resource;
} else {
//print("Could not get resource from", element, lastElem);
}
@@ -604,18 +614,22 @@ let firstDraw = true;
promise.then(()=>{
let currentDrawingTime = Date.now();
- if (showWidgets && global.WIDGETS){
- //print("Draw widgets");
- restoreWidgetDraw();
- Bangle.drawWidgets();
- g.setColor(g.theme.fg);
- g.drawLine(0,24,g.getWidth(),24);
- }
lastDrawTime = Date.now() - start;
isDrawing=false;
- firstDraw=false;
+ s.fd=false;
requestRefresh = false;
endPerfLog("initialDraw");
+ endPerfLog("fullDraw", true);
+
+ if (!Bangle.uiRemove){
+ setUi();
+ let orig = Bangle.drawWidgets;
+ Bangle.drawWidgets = ()=>{};
+ Bangle.loadWidgets();
+ Bangle.drawWidgets = orig;
+ require("widget_utils").swipeOn();
+ Bangle.drawWidgets();
+ }
}).catch((e)=>{
print("Error during drawing", e);
});
@@ -699,16 +713,16 @@ let firstDraw = true;
let handleLock = function(isLocked, forceRedraw){
//print("isLocked", Bangle.isLocked());
- for (let i of unlockedDrawInterval){
+ for (let i of s.udi){
//print("Clearing unlocked", i);
clearInterval(i);
}
- for (let i of lockedDrawInterval){
+ for (let i of s.ldi){
//print("Clearing locked", i);
clearInterval(i);
}
- unlockedDrawInterval = [];
- lockedDrawInterval = [];
+ s.udi = [];
+ s.ldi = [];
if (!isLocked){
if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){
@@ -724,7 +738,7 @@ let firstDraw = true;
initialDraw(watchfaceResources, watchface);
},unlockedRedraw, (v)=>{
//print("New matched unlocked interval", v);
- unlockedDrawInterval.push(v);
+ s.udi.push(v);
}, lastDrawTime);
if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock");
if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock');
@@ -742,54 +756,13 @@ let firstDraw = true;
initialDraw(watchfaceResources, watchface);
},lockedRedraw, (v)=>{
//print("New matched locked interval", v);
- lockedDrawInterval.push(v);
+ s.ldi.push(v);
}, lastDrawTime);
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
}
};
-
- let showWidgetsChanged = false;
- let currentDragDistance = 0;
-
- let restoreWidgetDraw = function(){
- if (global.WIDGETS) {
- for (let w in global.WIDGETS) {
- let wd = global.WIDGETS[w];
- wd.draw = originalWidgetDraw[w];
- wd.area = originalWidgetArea[w];
- }
- }
- };
-
- let handleDrag = function(e){
- //print("handleDrag");
- currentDragDistance += e.dy;
- if (Math.abs(currentDragDistance) < 10) return;
- dragDown = currentDragDistance > 0;
- currentDragDistance = 0;
- if (!showWidgets && dragDown){
- //print("Enable widgets");
- restoreWidgetDraw();
- showWidgetsChanged = true;
- }
- if (showWidgets && !dragDown){
- //print("Disable widgets");
- clearWidgetsDraw();
- firstDraw = true;
- showWidgetsChanged = true;
- }
- if (showWidgetsChanged){
- showWidgetsChanged = false;
- //print("Draw after widget change");
- showWidgets = dragDown;
- initialDraw();
- }
- };
-
- Bangle.on('drag', handleDrag);
-
if (!events || events.includes("pressure")){
Bangle.on('pressure', handlePressure);
try{
@@ -808,69 +781,43 @@ let firstDraw = true;
if (!events || events.includes("charging")) {
Bangle.on('charging', handleCharging);
}
-
- let originalWidgetDraw = {};
- let originalWidgetArea = {};
-
- let clearWidgetsDraw = function(){
- //print("Clear widget draw calls");
- if (global.WIDGETS) {
- originalWidgetDraw = {};
- originalWidgetArea = {};
- for (let w in global.WIDGETS) {
- let wd = global.WIDGETS[w];
- originalWidgetDraw[w] = wd.draw;
- originalWidgetArea[w] = wd.area;
- wd.draw = () => {};
- wd.area = "";
- }
- }
- }
handleLock(Bangle.isLocked(), true);
- Bangle.setUI({
- mode : "clock",
- remove : function() {
- //print("remove calls");
- // Called to unload all of the clock app
- Bangle.setHRMPower(0, "imageclock");
- Bangle.setBarometerPower(0, 'imageclock');
+ let setUi = function(){
+ Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ //print("remove calls");
+ // Called to unload all of the clock app
+ Bangle.setHRMPower(0, "imageclock");
+ Bangle.setBarometerPower(0, 'imageclock');
- Bangle.removeListener('drag', handleDrag);
- Bangle.removeListener('lock', handleLock);
- Bangle.removeListener('charging', handleCharging);
- Bangle.removeListener('HRM', handleHrm);
- Bangle.removeListener('pressure', handlePressure);
+ Bangle.removeListener('lock', handleLock);
+ Bangle.removeListener('charging', handleCharging);
+ Bangle.removeListener('HRM', handleHrm);
+ Bangle.removeListener('pressure', handlePressure);
- if (deferredTimout) clearTimeout(deferredTimout);
- if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
- if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
+ if (deferredTimout) clearTimeout(deferredTimout);
+ if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
+ if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
- for (let i of unlockedDrawInterval){
- //print("Clearing unlocked", i);
- clearInterval(i);
+ for (let i of global.s.udi){
+ //print("Clearing unlocked", i);
+ clearInterval(i);
+ }
+ for (let i of global.s.ldi){
+ //print("Clearing locked", i);
+ clearInterval(i);
+ }
+
+ delete Bangle.printPerfLog;
+ if (settings.perflog){
+ delete Bangle.resetPerfLog;
+ }
+ cleanupDelays();
+ require("widget_utils").show();
}
- delete unlockedDrawInterval;
- for (let i of lockedDrawInterval){
- //print("Clearing locked", i);
- clearInterval(i);
- }
- delete lockedDrawInterval;
- delete showWidgets;
- delete firstDraw;
-
- delete Bangle.printPerfLog;
- if (settings.perflog){
- delete Bangle.resetPerfLog;
- delete performanceLog;
- }
-
- cleanupDelays();
- restoreWidgetDraw();
- }
- });
-
- Bangle.loadWidgets();
- clearWidgetsDraw();
+ });
+ }
}
diff --git a/apps/imageclock/custom.html b/apps/imageclock/custom.html
index e595b51ca..784e6cbdd 100644
--- a/apps/imageclock/custom.html
+++ b/apps/imageclock/custom.html
@@ -4,8 +4,8 @@
-
-
+
+
@@ -25,6 +25,8 @@
Wrap draw calls in timeouts (Slower, more RAM use, better interactivity)
Force use of direct drawing (Even faster, but will produce visible artifacts on not optimized watch faces)
+
+ Do not create combined app flle (slower but more flexible for debugging, incompatible with minification)
Add debug prints to generated code
@@ -32,7 +34,7 @@
Select watchface folder:
or
Select watchface zip file:
-
+
Upload to watch
Save resources file
Save face file
@@ -55,15 +57,15 @@
var expectedFiles = 0;
var rootZip = new JSZip();
var resourcesZip = rootZip.folder("resources");
-
+
function isNativeFormat(){
return document.getElementById("useNative").checked;
}
-
+
function addDebug(){
return document.getElementById("debugprints").checked;
}
-
+
function convertAmazfitTime(time){
var result = {};
if (time.Hours){
@@ -88,7 +90,7 @@
}
return result;
}
-
+
function convertAmazfitDate(date){
var result = {};
if (date.MonthAndDay.Separate.Day) result.Day = convertAmazfitNumber(date.MonthAndDay.Separate.Day, "Day");
@@ -98,11 +100,11 @@
}
return result;
}
-
+
var filesToMove={};
-
+
var zipChangePromise = Promise.resolve();
-
+
function performFileChanges(){
var promise = Promise.resolve();
//rename all files to just numbers without leading zeroes
@@ -111,7 +113,7 @@
var tmp = resultJson[c];
delete resultJson[c];
resultJson[Number(c)] = tmp;
-
+
async function modZip(c){
console.log("Async modification of ", c)
var fileRegex = new RegExp(c + ".*");
@@ -120,27 +122,27 @@
console.log("Filedata is", fileData);
var extension = resourcesZip.file(fileRegex)[0].name.match(/\.[^.]*$/);
var newName = Number(c) + extension;
-
+
console.log("Renaming to", newName);
resourcesZip.remove(c + extension);
resourcesZip.file(newName, fileData);
}
promise = promise.then(modZip(c));
-
+
}
-
-
+
+
console.log("File moves:", filesToMove);
-
+
for (var c in filesToMove){
var tmp = resultJson[c];
console.log("Handle filemove", c, filesToMove[c], tmp);
-
+
var element = resultJson;
var path = filesToMove[c];
-
-
+
+
async function modZip(c){
console.log("Async modification of ", c)
var fileRegex = new RegExp(c + ".*");
@@ -149,13 +151,13 @@
console.log("Filedata is", fileData);
var extension = resourcesZip.file(fileRegex)[0].name.match(/\.[^.]*$/);
var newName = Number(c) + extension;
-
+
console.log("Copying to", newName);
resourcesZip.file(filesToMove[c].join("/") + extension, fileData);
}
promise = promise.then(modZip(c));
-
-
+
+
for (var i = 0; i< path.length; i++){
if (!element[path[i]]) element[path[i]] = {};
if (i == path.length - 1){
@@ -164,7 +166,7 @@
element = element[path[i]];
}
}
-
+
}
promise.then(()=>{
document.getElementById('btnUpload').disabled = true;
@@ -172,7 +174,7 @@
console.log("After moves", resultJson);
return promise;
};
-
+
function convertAmazfitMultistate(multistate, value, minValue, maxValue){
var result = {
MultiState: {
@@ -188,18 +190,18 @@
if (multistate.ImageIndexOff) filesToMove[multistate.ImageIndexOff] = ["status", value, "off"];
return result;
}
-
+
function convertAmazfitStatus(status){
var result = {};
-
+
if (status.Alarm) result.Alarm = convertAmazfitMultistate(status.Alarm,"Alarm");
if (status.Bluetooth) result.Bluetooth = convertAmazfitMultistate(status.Bluetooth,"Bluetooth");
if (status.DoNotDisturb) result.DoNotDisturb = convertAmazfitMultistate(status.DoNotDisturb,"Notifications");
if (status.Lock) result.Lock = convertAmazfitMultistate(status.Lock,"Lock");
-
+
return result;
}
-
+
function convertAmazfitNumber(element, value, minValue, maxValue){
var number = {};
var result = {
@@ -233,10 +235,10 @@
if (maxValue !== undefined) number.MinValue = minValue;
return result;
}
-
+
function moveWeatherIcons(icon){
filesToMove[icon.ImageIndex + 0] = ["weather", "fallback"];
-
+
// Light clouds
filesToMove[icon.ImageIndex + 1] = ["weather", 801];
// Cloudy, possible rain
@@ -282,7 +284,7 @@
// Very heavy shower
filesToMove[icon.ImageIndex + 22] = ["weather", 531];
}
-
+
function convertAmazfitTemperature(temp){
var result = {};
result = convertAmazfitNumber(temp.Number, "WeatherTemperature");
@@ -294,15 +296,15 @@
}
return result;
}
-
+
function convertAmazfitWeather(weather){
var result = {};
-
+
if (weather.Temperature && weather.Temperature.Current){
if (!result.Temperature) result.Temperature = {};
result.Temperature.Current = convertAmazfitTemperature(weather.Temperature.Current);
}
-
+
if (weather.Temperature && weather.Temperature.Today){
if (!result.Temperature) result.Temperature = {};
if (weather.Temperature.Today.Separate){
@@ -327,10 +329,10 @@
}
return result;
}
-
+
function convertAmazfitActivity(activity){
var result = {};
-
+
if (activity.Steps){
result.Steps = convertAmazfitNumber(activity.Steps, "Steps");
}
@@ -339,7 +341,7 @@
}
return result;
}
-
+
function convertAmazfitScale(scale, value, minValue, maxValue){
var result = {};
result.Scale = {
@@ -356,10 +358,10 @@
Y: c.Y
});
}
-
+
return result;
}
-
+
function convertAmazfitStepsProgress(steps){
var result = {};
if (steps.GoalImage){
@@ -378,7 +380,7 @@
}
return result;
}
-
+
function convertAmazfitBattery(battery){
var result = {};
if (battery.Scale){
@@ -389,7 +391,7 @@
}
return result;
}
-
+
function convertAmazfitImage(image){
var result = {
Image: {
@@ -401,11 +403,11 @@
};
return result;
}
-
+
function convertAmazfitColor(color){
return "#" + color.substring(2);
}
-
+
function convertAmazfitHand(hand, rotationValue, minRotationValue, maxRotationValue){
var result = {
Filled: !hand.OnlyBorder,
@@ -418,18 +420,18 @@
MaxRotationValue: maxRotationValue,
MinRotationValue: minRotationValue
};
-
+
result.Vertices = []
for (var c of hand.Shape){
result.Vertices.push(c);
}
return { Poly: result };
}
-
+
function convertAmazfitAnalog(analog, face){
var result = {
};
-
+
if (analog.Hours){
result.Hours = {};
result.Hours.Hand = convertAmazfitHand(analog.Hours, "Hour12Analog", 0, 12);
@@ -464,14 +466,14 @@
}
return result;
}
-
+
function restructureAmazfitFormat(dataString){
console.log("Amazfit data:", dataString);
-
-
+
+
var json = JSON.parse(dataString);
faceJson = json;
-
+
var result = {};
result.Properties = {};
@@ -479,8 +481,8 @@
result.Properties.Redraw.Unlocked = 60000;
result.Properties.Redraw.Locked = 60000;
result.Properties.Redraw.Clear = true;
-
-
+
+
if (json.Background){
result.Background = json.Background;
result.Background.Image.ImagePath = [];
@@ -491,32 +493,32 @@
result.Time = convertAmazfitTime(json.Time);
if (json.AnalogDialFace) result.Time.Plane = 1;
}
-
+
if (json.Date){
result.Date = convertAmazfitDate(json.Date);
if (json.AnalogDialFace) result.Date.Plane = 1;
}
-
+
if (json.Status){
result.Status = convertAmazfitStatus(json.Status);
if (json.AnalogDialFace) result.Status.Plane = 1;
}
-
+
if (json.Weather){
result.Weather = convertAmazfitWeather(json.Weather);
if (json.AnalogDialFace) result.Weather.Plane = 1;
}
-
+
if (json.Activity){
result.Activity = convertAmazfitActivity(json.Activity);
if (json.AnalogDialFace) result.Activity.Plane = 1;
}
-
+
if (json.StepsProgress){
result.StepsProgress = convertAmazfitStepsProgress(json.StepsProgress);
if (json.AnalogDialFace) result.StepsProgress.Plane = 1;
}
-
+
if (json.Battery){
result.Battery = convertAmazfitBattery(json.Battery);
if (json.AnalogDialFace) result.Battery.Plane = 1;
@@ -529,7 +531,7 @@
return result;
}
-
+
function parseFaceJson(jsonString){
if (isNativeFormat()){
return JSON.parse(jsonString);
@@ -537,7 +539,7 @@
return restructureAmazfitFormat(jsonString);
}
}
-
+
function combineProperty(name, source, target){
if (source[name] && target[name]){
if (Array.isArray(target[name])){
@@ -556,7 +558,7 @@
if (typeof element == "string" || typeof element == "number") return [];
for (var c in element){
var next = element[c];
-
+
combineProperty("X",element,next);
combineProperty("Y",element,next);
combineProperty("Width",element,next);
@@ -571,7 +573,7 @@
combineProperty("MaxRotationValue",element,next);
if (typeof element.Plane == "number") next.Plane = element.Plane;
next.Layer = element.Layer ? (element.Layer) : "" + c;
-
+
if (["MultiState","Image","CodedImage","Number","Circle","Poly","Rect","Scale"].includes(c)){
result.push({type:c, value: next});
} else {
@@ -580,12 +582,12 @@
}
return result;
}
-
+
function convertToCode(elements, properties, wrapInTimeouts, forceUseOrigPlane){
var code = "(function (wr, wf) {\n";
code += "var lc;\n";
code += "var p = Promise.resolve();\n";
-
+
//get mapped by layer
var counter = 0;
var planes = {};
@@ -606,20 +608,20 @@
}
if (!planeNumbers.includes(0)) planeNumbers.push(0);
planeNumbers.sort().reverse();
-
+
console.log("Found planes", planes, "with numbers", planeNumbers)
-
+
code += "p0 = g;\n";
-
+
for (var planeIndex = 0; planeIndex < planeNumbers.length; planeIndex++){
var layers = planes[planeNumbers[planeIndex]];
var plane = planeNumbers[planeIndex];
-
+
var lastSetColor;
var lastSetBgColor;
-
+
if (plane != 0) code += "if (!p" + plane + ") p" + plane + " = Graphics.createArrayBuffer(g.getWidth(),g.getHeight(),4,{msb:true});\n";
-
+
if (properties.Redraw && properties.Redraw.Clear){
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
@@ -632,31 +634,31 @@
code += 'endPerfLog("initialDraw_g.clear");'+ "\n";
code += "});\n";
}
-
+
var previousPlane = plane + 1;
if (previousPlane < planeNumbers.length){
code += "p = p.then(()=>{\n";
-
+
if (addDebug()) code += 'print("Copying of plane ' + previousPlane + ' to display");'+"\n";
//code += "g.drawImage(p" + i + ".asImage());";
code += "p0.drawImage({width: p" + previousPlane + ".getWidth(), height: p" + previousPlane + ".getHeight(), bpp: p" + previousPlane + ".getBPP(), buffer: p" + previousPlane + ".buffer, palette: palette});\n";
code += "});\n";
}
-
+
console.log("Got layers", layers);
for (var layername in layers){
var layerElements = layers[layername];
-
+
console.log("Layer elements", layername, layerElements);
//code for whole layer
-
+
if (addDebug()) code += 'print("Starting layer ' + layername + '");' + "\n";
-
+
var checkForLayerChange = false;
var checkcode = "";
-
+
if (!(properties.Redraw && properties.Redraw.Clear)){
- checkcode = 'firstDraw';
+ checkcode = 's.fd';
for (var i = 0; i< layerElements.length; i++){
var layerElement = layerElements[i];
var referencedElement = elements[layerElements[i].index];
@@ -664,38 +666,38 @@
console.log("Check for change:", layerElement, referencedElement);
if (layerElement.element.Value){
if (elementType == "MultiState" && layerElement.element.Value) {
- checkcode += '| isChangedMultistate(wf.Collapsed[' + layerElement.index + '].value)';
+ checkcode += '| isChangedMultistate(wf.c[' + layerElement.index + '].value)';
} else {
- checkcode += '| isChangedNumber(wf.Collapsed[' + layerElement.index + '].value)';
+ checkcode += '| isChangedNumber(wf.c[' + layerElement.index + '].value)';
}
checkForLayerChange = true;
}
}
}
-
-
+
+
//code for elements
for (var i = 0; i< layerElements.length; i++){
var elementIndex = layerElements[i].index;
var c = elements[elementIndex];
console.log("convert to code", c);
-
+
var condition = "";
if (checkcode.length > 0 && checkForLayerChange){
if (condition.length > 0) condition += " && ";
condition = '(' + checkcode + ')';
}
-
+
if (c.value.HideOn && c.value.HideOn.includes("Lock")){
if (condition.length > 0) condition += " && ";
condition = '!Bangle.isLocked()';
}
-
+
if (c.value.Type == "Once"){
if (condition.length > 0) condition += " && ";
- condition += "firstDraw";
+ condition += "s.fd";
}
-
+
var planeName = "p" + plane;
var colorsetting = "";
if (c.value.ForegroundColor && lastSetColor != c.value.ForegroundColor){
@@ -712,28 +714,28 @@
else
colorsetting += planeName + ".setBgColor(\"" + c.value.BackgroundColor + "\");\n";
}
-
+
if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
- code += "" + colorsetting;
code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
+ code += "" + colorsetting;
if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
- code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n";
+ code += "draw" + c.type + "(" + planeName + ", wr, wf.c[" + elementIndex + "].value);\n";
code += "});\n";
code += (condition.length > 0 ? "}\n" : "");
}
}
console.log("Current plane is", plane);
-
-
+
+
}
-
+
code += "return p;})";
console.log("Code:", code);
return code
@@ -742,14 +744,14 @@
function postProcess(){
moveData(resultJson);
console.log("Created data file", resourceDataString, resourceDataOffset, resultJson);
-
+
var properties = faceJson.Properties;
- faceJson = { Properties: properties, Collapsed: collapseTree(faceJson,{X:0,Y:0})};
+ faceJson = { Properties: properties, c: collapseTree(faceJson,{X:0,Y:0})};
console.log("After collapsing", faceJson);
- precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked);
+ precompiledJs = convertToCode(faceJson.c, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked);
console.log("After precompiling", precompiledJs);
}
-
+
function convertJsToJson(imgstr){
var E = {};
E.toArrayBuffer = (s)=>s;
@@ -768,7 +770,7 @@
function imageLoaded() {
var options = {};
-
+
options.diffusion = infoJson.diffusion ? infoJson.diffusion : "none";
options.compression = false;
options.alphaToColor = false;
@@ -779,12 +781,12 @@
options.contrast = 0;
options.mode = infoJson.color ? infoJson.color : "1bit";
options.output = "object";
-
+
console.log("Loaded image has path", this.path);
var jsonPath = this.path.split("/");
-
+
var forcedTransparentColorMatch = jsonPath[jsonPath.length-1].match(/.*\.t([^.]+)\..*/)
-
+
var forcedTransparentColor;
if (jsonPath[jsonPath.length-1].includes(".t.")){
options.transparent = true;
@@ -792,13 +794,13 @@
options.transparent = false;
forcedTransparentColor = forcedTransparentColorMatch[1];
}
-
-
+
+
console.log("image has transparency", options.transparent);
console.log("image has forced transparent color", forcedTransparentColor);
jsonPath[jsonPath.length-1] = jsonPath[jsonPath.length-1].replace(/([^.]*)\..*/, "$1");
console.log("Loaded image has json path", jsonPath);
-
+
var canvas = document.getElementById("canvas")
canvas.width = this.width*2;
canvas.height = this.height;
@@ -819,7 +821,7 @@
imgstr = imageconverter.RGBAtoString(rgba, options);
var outputImageData = new ImageData(options.rgbaOut, options.width, options.height);
ctx.putImageData(outputImageData,this.width,0);
-
+
imgstr = convertJsToJson(imgstr);
// checkerboard for transparency on original image
@@ -827,9 +829,9 @@
imageconverter.RGBAtoCheckerboard(imageData.data, {width:this.width,height:this.height});
ctx.putImageData(imageData,0,0);
-
+
var currentElement = resultJson;
-
+
for (var i = 0; i < jsonPath.length; i++){
if (i == jsonPath.length - 1){
var resultingObject = JSON.parse(imgstr);
@@ -841,18 +843,18 @@
currentElement = currentElement[jsonPath[i]];
}
}
-
+
handledFiles++;
console.log("Expected:", expectedFiles, " handled:", handledFiles);
-
+
if (handledFiles == expectedFiles){
if (!isNativeFormat()) {
performFileChanges().then(()=>{
postProcess();
-
+
rootZip.file("face.json", JSON.stringify(faceJson, null, 2));
rootZip.file("info.json", JSON.stringify(infoJson, null, 2));
-
+
document.getElementById('btnSave').disabled = false;
document.getElementById('btnSaveFace').disabled = false;
document.getElementById('btnSaveZip').disabled = false;
@@ -860,21 +862,21 @@
});
} else {
postProcess();
-
+
document.getElementById('btnSave').disabled = false;
document.getElementById('btnSaveFace').disabled = false;
document.getElementById('btnUpload').disabled = false;
}
}
}
-
+
function handleWatchFace(infoFile, faceFile, resourceFiles){
if (isNativeFormat()){
var reader = new FileReader();
reader.path = infoFile.webkitRelativePath;
reader.onload = function(event) {
infoJson = JSON.parse(reader.result);
-
+
handleFaceJson(faceFile, resourceFiles);
};
reader.readAsText(infoFile);
@@ -883,18 +885,18 @@
handleFaceJson(faceFile, resourceFiles);
}
}
-
+
function handleFaceJson(faceFile, resourceFiles){
var reader = new FileReader();
reader.path = faceFile.webkitRelativePath;
reader.onload = function(event) {
faceJson = parseFaceJson(reader.result);
-
+
handleResourceFiles(resourceFiles);
};
reader.readAsText(faceFile);
}
-
+
function handleResourceFiles(files){
for (var current of files){
console.log('Handle resource file ', current);
@@ -917,25 +919,25 @@
reader.readAsDataURL(current);
}
}
-
+
function handleFileSelect(event) {
handledFiles = 0;
expectedFiles = undefined;
-
+
document.getElementById('btnSave').disabled = true;
document.getElementById('btnSaveZip').disabled = true;
document.getElementById('btnSaveFace').disabled = true;
document.getElementById('btnUpload').disabled = true;
-
+
console.log("File select event", event);
if (event.target.files.length == 0) return;
result = "";
resultJson= {};
-
+
var resourceFiles = [];
var faceFile;
var infoFile;
-
+
for (var current of event.target.files){
console.log('Handle file ', current);
if (isNativeFormat()){
@@ -970,10 +972,10 @@
}
}
handleWatchFace(infoFile, faceFile, resourceFiles);
-
+
};
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
-
+
function moveData(json){
console.log("MoveData for", json);
for (var k in json){
@@ -997,11 +999,11 @@
}
}
}
-
+
document.getElementById("timeoutwrap").addEventListener("click", function() {
document.getElementById("forceOrigPlane").disabled = !document.getElementById("timeoutwrap").checked;
});
-
+
document.getElementById("btnSave").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson));
@@ -1010,25 +1012,48 @@
h.click();
});
document.getElementById("btnUpload").addEventListener("click", function() {
-
+
+ console.log("Fetching app");
+ fetch('app.js').then((r) => {
+ console.log("Got response", r);
+ return r.text();
+ }
+ ).then((imageclockSrc) => {
+ console.log("Got src", imageclockSrc)
+
+ if (!document.getElementById('separateFiles').checked){
+ if (precompiledJs.length > 0){
+ const replacementString = 'eval(require("Storage").read("imageclock.draw.js"))';
+ console.log("Can replace:", imageclockSrc.includes(replacementString));
+ imageclockSrc = imageclockSrc.replace(replacementString, precompiledJs);
+ }
+ imageclockSrc = imageclockSrc.replace('require("Storage").readJSON("imageclock.face.json")', JSON.stringify(faceJson));
+ imageclockSrc = imageclockSrc.replace('require("Storage").readJSON("imageclock.resources.json")', JSON.stringify(resultJson));
+ }
var appDef = {
id : "imageclock",
storage:[
- {name:"imageclock.app.js", url:"app.js"},
- {name:"imageclock.resources.json", content: JSON.stringify(resultJson)},
{name:"imageclock.img", url:"app-icon.js", evaluate:true},
]
};
+ if (document.getElementById('separateFiles').checked){
+ appDef.storage.push({name:"imageclock.app.js", url:"app.js"});
+ if (precompiledJs.length > 0){
+ appDef.storage.push({name:"imageclock.draw.js", content:precompiledJs});
+ }
+ appDef.storage.push({name:"imageclock.face.json", content: JSON.stringify(faceJson)});
+ appDef.storage.push({name:"imageclock.resources.json", content: JSON.stringify(resultJson)});
+ } else {
+ appDef.storage.push({name:"imageclock.app.js", url:"pleaseminifycontent.js", content:imageclockSrc});
+ }
if (resourceDataString.length > 0){
appDef.storage.push({name:"imageclock.resources.data", content: resourceDataString});
}
- appDef.storage.push({name:"imageclock.draw.js", content: precompiledJs.length > 0 ? precompiledJs : "//empty"});
- appDef.storage.push({name:"imageclock.face.json", content: JSON.stringify(faceJson)});
-
console.log("Uploading app:", appDef);
sendCustomizedApp(appDef);
+ });
});
-
+
function handleZipSelect(evt) {
@@ -1040,18 +1065,18 @@
document.getElementById('btnSaveZip').disabled = true;
document.getElementById('btnUpload').disabled = true;
JSZip.loadAsync(f).then(function(zip) {
-
+
console.log("Zip loaded", zip);
result = "";
resultJson= {};
-
+
var resourceFiles = [];
-
+
var promise = zip.file("face.json").async("string").then((data)=>{
console.log("face.json data", data);
faceJson = parseFaceJson(data);
});
-
+
if (isNativeFormat()){
promise = promise.then(zip.file("info.json").async("string").then((data)=>{
console.log("info.json data", data);
@@ -1062,12 +1087,12 @@
"color": "3bit",
"transparent": true
};
-
+
}
-
+
zip.folder("resources").forEach(function (relativePath, file){
console.log("iterating over", relativePath);
-
+
if (!file.dir){
expectedFiles++;
promise = promise.then(file.async("blob").then(function (blob) {
@@ -1083,10 +1108,10 @@
reader.readAsDataURL(blob);
}));
}
-
+
});
-
-
+
+
}, function (e) {
console.log("Error reading " + f.name + ": " + e.message);
});
@@ -1095,11 +1120,11 @@
console.log("Zip select event", evt);
var files = evt.target.files;
-
+
if (files.length > 1){
alert("Only one file allowed");
}
-
+
handleFile(files[0]);
}
@@ -1113,7 +1138,7 @@
});
}
-
+
document.getElementById("btnSaveFace").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(faceJson));
@@ -1121,14 +1146,14 @@
h.download = "face.json";
h.click();
});
-
+
document.getElementById('zipLoader').addEventListener('change', handleZipSelect, false);
document.getElementById('btnSaveZip').addEventListener('click', handleZipExport, false);
document.getElementById('btnSave').disabled = true;
document.getElementById('btnSaveFace').disabled = true;
document.getElementById('btnSaveZip').disabled = true;
document.getElementById('btnUpload').disabled = true;
-
+
diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json
index e068b9fa7..b291ab01e 100644
--- a/apps/imageclock/metadata.json
+++ b/apps/imageclock/metadata.json
@@ -2,7 +2,7 @@
"id": "imageclock",
"name": "Imageclock",
"shortName": "Imageclock",
- "version": "0.11",
+ "version": "0.13",
"type": "clock",
"description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.",
"icon": "app.png",
diff --git a/apps/imgclock/custom.html b/apps/imgclock/custom.html
index 1d8e06c07..68d059b80 100644
--- a/apps/imgclock/custom.html
+++ b/apps/imgclock/custom.html
@@ -10,7 +10,7 @@
-
+
+
+