@@ -172,6 +174,10 @@
Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this will break many apps)
+
diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog
index 6f306f61a..f1f8fb40e 100644
--- a/apps/alarm/ChangeLog
+++ b/apps/alarm/ChangeLog
@@ -44,3 +44,4 @@
0.39: Dated event repeat option
0.40: Use substring of message when it's longer than fits the designated menu entry.
0.41: Fix a menu bug affecting alarms with empty messages.
+0.42: Fix date not getting saved in event edit menu when tapping Confirm
diff --git a/apps/alarm/app.js b/apps/alarm/app.js
index f8ed0322e..d135f184e 100644
--- a/apps/alarm/app.js
+++ b/apps/alarm/app.js
@@ -190,7 +190,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
},
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
- prepareAlarmForSave(alarm, alarmIndex, time);
+ prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload();
showMainMenu();
}
diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json
index b986512bc..3c676c217 100644
--- a/apps/alarm/metadata.json
+++ b/apps/alarm/metadata.json
@@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
- "version": "0.41",
+ "version": "0.42",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm",
diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog
index d966c1440..f2a0c5b3f 100644
--- a/apps/android/ChangeLog
+++ b/apps/android/ChangeLog
@@ -27,4 +27,7 @@
0.26: Change handling of GPS status to depend on GPS events instead of connection events
0.27: Issue newline before GB commands (solves issue with console.log and ignored commands)
0.28: Navigation messages no longer launch the Maps view unless they're new
-0.29: Support for http request xpath return format
\ No newline at end of file
+0.29: Support for http request xpath return format
+0.30: Send firmware and hardware versions on connection
+ Allow alarm enable/disable
+0.31: Implement API for activity fetching
diff --git a/apps/android/boot.js b/apps/android/boot.js
index 7988c378f..a8027a67c 100644
--- a/apps/android/boot.js
+++ b/apps/android/boot.js
@@ -86,7 +86,7 @@
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
a.appid = "gbalarms";
- a.on = true;
+ a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.last = last;
@@ -193,10 +193,37 @@
Bangle.on('HRM',actHRMHandler);
actInterval = setInterval(function() {
var steps = Bangle.getStepCount();
- gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM });
+ gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
lastSteps = steps;
}, event.int*1000);
},
+ // {t:"actfetch", ts:long}
+ "actfetch": function() {
+ gbSend({t: "actfetch", state: "start"});
+ var actCount = 0;
+ var actCb = function(r) {
+ // The health lib saves the samples at the start of the 10-minute block
+ // However, GB expects them at the end of the block, so let's offset them
+ // here to keep a consistent API in the health lib
+ var sampleTs = r.date.getTime() + 600000;
+ if (sampleTs >= event.ts) {
+ gbSend({
+ t: "act",
+ ts: sampleTs,
+ stp: r.steps,
+ hrm: r.bpm,
+ mov: r.movement
+ });
+ actCount++;
+ }
+ }
+ if (event.ts != 0) {
+ require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
+ } else {
+ require("health").readFullDatabase(actCb);
+ }
+ gbSend({t: "actfetch", state: "end", count: actCount});
+ },
"nav": function() {
event.id="nav";
if (event.instr) {
@@ -253,6 +280,7 @@
Bangle.on("charging", sendBattery);
NRF.on("connect", () => setTimeout(function() {
sendBattery();
+ gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
}, 2000));
NRF.on("disconnect", () => {
@@ -264,10 +292,9 @@
require("messages").clearAll();
});
setInterval(sendBattery, 10*60*1000);
- // Health tracking
- Bangle.on('health', health=>{
- if (actInterval===undefined) // if 'realtime' we do it differently
- gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
+ // Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
+ Bangle.on('health', h=>{
+ gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
});
// Music control
Bangle.musicControl = cmd => {
diff --git a/apps/android/metadata.json b/apps/android/metadata.json
index 8489570f7..8d65d32e3 100644
--- a/apps/android/metadata.json
+++ b/apps/android/metadata.json
@@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
- "version": "0.29",
+ "version": "0.31",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",
diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog
index 11b2d7177..156cf17bf 100644
--- a/apps/astrocalc/ChangeLog
+++ b/apps/astrocalc/ChangeLog
@@ -2,3 +2,5 @@
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start
0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
0.04: Compatibility with Bangle.js 2, get location from My Location
+0.05: Enable widgets
+0.06: Fix azimuth (bug #2651), only show degrees
diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js
index 6629842cf..5589a5703 100644
--- a/apps/astrocalc/astrocalc-app.js
+++ b/apps/astrocalc/astrocalc-app.js
@@ -11,7 +11,6 @@
const SunCalc = require("suncalc"); // from modules folder
const storage = require("Storage");
-const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
function drawMoon(phase, x, y) {
const moonImgFiles = [
@@ -110,7 +109,7 @@ function drawPoints() {
}
function drawData(title, obj, startX, startY) {
- g.clear();
+ g.clearRect(Bangle.appRect);
drawTitle(title);
let xPos, yPos;
@@ -141,22 +140,21 @@ function drawData(title, obj, startX, startY) {
function drawMoonPositionPage(gps, title) {
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
+ const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north
const pageData = {
- Azimuth: pos.azimuth.toFixed(2),
- Altitude: pos.altitude.toFixed(2),
+ Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '°',
+ Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°',
Distance: `${pos.distance.toFixed(0)} km`,
- "Parallactic Ang": pos.parallacticAngle.toFixed(2),
+ "Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '°',
};
- const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
+ const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5);
drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20);
drawPoints();
drawPoint(azimuthDegrees, 8, moonColor);
- let m = setWatch(() => {
- let m = moonIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
}
function drawMoonIlluminationPage(gps, title) {
@@ -174,9 +172,7 @@ function drawMoonIlluminationPage(gps, title) {
drawData(title, pageData, null, 35);
drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2);
- let m = setWatch(() => {
- let m = moonIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
}
@@ -194,17 +190,17 @@ function drawMoonTimesPage(gps, title) {
// Draw the moon rise position
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
- const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
+ const riseAzimuth = risePos.azimuth + Math.PI; // 0 is south, we want 0 to be north
+ const riseAzimuthDegrees = parseInt(riseAzimuth * 180 / Math.PI);
drawPoint(riseAzimuthDegrees, 8, moonColor);
// Draw the moon set position
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
- const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
+ const setAzimuth = setPos.azimuth + Math.PI; // 0 is south, we want 0 to be north
+ const setAzimuthDegrees = parseInt(setAzimuth * 180 / Math.PI);
drawPoint(setAzimuthDegrees, 8, moonColor);
- let m = setWatch(() => {
- let m = moonIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
}
function drawSunShowPage(gps, key, date) {
@@ -214,16 +210,15 @@ function drawSunShowPage(gps, key, date) {
const mins = ("0" + date.getMinutes()).substr(-2);
const secs = ("0" + date.getMinutes()).substr(-2);
const time = `${hrs}:${mins}:${secs}`;
+ const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north
- const azimuth = Number(pos.azimuth.toFixed(2));
- const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
- const altitude = Number(pos.altitude.toFixed(2));
+ const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + '°';
+ const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°';
const pageData = {
Time: time,
Altitude: altitude,
- Azimumth: azimuth,
- Degrees: azimuthDegrees
+ Azimuth: azimuthDegrees,
};
drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
@@ -233,9 +228,7 @@ function drawSunShowPage(gps, key, date) {
// Draw the suns position
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0});
- m = setWatch(() => {
- m = sunIndexPageMenu(gps);
- }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
+ Bangle.setUI({mode: "custom", back: () => sunIndexPageMenu(gps)});
return null;
}
@@ -314,7 +307,9 @@ function getCenterStringX(str) {
function init() {
let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
+ Bangle.loadWidgets();
indexPageMenu(location);
+ Bangle.drawWidgets();
}
let m;
diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json
index 653c097da..09dc53170 100644
--- a/apps/astrocalc/metadata.json
+++ b/apps/astrocalc/metadata.json
@@ -1,10 +1,10 @@
{
"id": "astrocalc",
"name": "Astrocalc",
- "version": "0.04",
+ "version": "0.06",
"description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app",
"icon": "astrocalc.png",
- "tags": "app,sun,moon,cycles,tool",
+ "tags": "app,sun,moon,cycles,tool,outdoors",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"dependencies": {"mylocation":"app"},
diff --git a/apps/bblobface/ChangeLog b/apps/bblobface/ChangeLog
new file mode 100644
index 000000000..6a29fdb72
--- /dev/null
+++ b/apps/bblobface/ChangeLog
@@ -0,0 +1 @@
+1.00: Initial release of Bangle Blobs Clock!
diff --git a/apps/bblobface/README.md b/apps/bblobface/README.md
new file mode 100644
index 000000000..54e07e9f8
--- /dev/null
+++ b/apps/bblobface/README.md
@@ -0,0 +1,35 @@
+# Bangle Blobs Clock
+What if every time you checked the time, you could play a turn of a turn-based puzzle game?
+You check the time dozens, maybe hundreds of times per day, and Bangle Blobs Clock wants to add a splash of fun to each of these moments!
+Bangle Blobs Clock is a fully featured watch face with a turn-based puzzle game right next to the clock.
+
+
+
+
+## Clock Features
+- Hour and minute
+- Seconds (only while the screen is unlocked to save power)
+- Month, day, and day of week
+- Battery percentage. Blue while charging, red when low, green otherwise.
+- Respects your 24-hour/12-hour time setting in Locale
+- Press the pause button to access your Widgets
+- Supports Fast Loading
+
+## The Game
+This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA.
+Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall.
+If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs!
+- Drag left and right to move the pair
+- Tap the left or right half of the screen to rotate the pair
+- Swipe down to place the pair
+
+## More Info
+If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing
+
+## Special Thanks
+I'm Pasta Rhythm, computer scientist and aspiring game developer. I would like to say thank you to the people who inspired me while I was making this app:
+- [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started.
+- [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2.
+- Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects!
+- SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games!
+- Compile, the original creators of Puyo Puyo. The company went bankrupt long ago, but the people who worked for them continue to make games.
diff --git a/apps/bblobface/app-icon.js b/apps/bblobface/app-icon.js
new file mode 100644
index 000000000..e8d9baced
--- /dev/null
+++ b/apps/bblobface/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA="))
diff --git a/apps/bblobface/app.js b/apps/bblobface/app.js
new file mode 100644
index 000000000..579a6bbb4
--- /dev/null
+++ b/apps/bblobface/app.js
@@ -0,0 +1,768 @@
+{
+ // ~~ Variables for clock ~~
+ let clockDrawTimeout;
+ let twelveHourTime = require('Storage').readJSON('setting.json', 1)['12hour'];
+ let updateSeconds = !Bangle.isLocked();
+ let batteryLevel = E.getBattery();
+
+ // ~~ Variables for game logic ~~
+ const NUM_COLORS = 6;
+ const NUISANCE_COLOR = 7;
+ let grid = [
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0])
+ ];
+ let hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]);
+ let nextQueue = [{pivot: 1, leaf: 1}, {pivot: 1, leaf: 1}];
+ let currentPair = {pivot: 0, leaf: 0};
+ let dropCoordinates = {pivotX: 2, pivotY: 11, leafX: 2, leafY: 10};
+ let pairX = 2;
+ let pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left
+ let slotsToCheck = [];
+ let selectedColors;
+ let lastChain = 0;
+ let gameLost = false;
+ let gamePaused = false;
+ let midChain = false;
+
+ /*
+ Sets up a new game.
+ Must be called once before the first round.
+ */
+ let restartGame = function() {
+ grid = [
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0])
+ ];
+ hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]);
+ currentPair = {pivot: 0, leaf: 0};
+ pairX = 2;
+ pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left
+ slotsToCheck = [];
+ gameLost = false;
+ lastChain = 0;
+
+ //Set up random colors
+ selectedColors = new Uint8Array([1, 2, 3, 4, 5, 6]);
+ for (let i = NUM_COLORS - 1; i > 0; i--) {
+ let swap = selectedColors[i];
+ let swapIndex = Math.floor(Math.random() * (i + 1));
+ selectedColors[i] = selectedColors[swapIndex];
+ selectedColors[swapIndex] = swap;
+ }
+
+ //Create the first two pairs (Always in the first three colors)
+ nextQueue[0].pivot = selectedColors[Math.floor(Math.random() * 3)];
+ nextQueue[0].leaf = selectedColors[Math.floor(Math.random() * 3)];
+ nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 3)];
+ nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 3)];
+ };
+
+ /*
+ Readies the next pair and generates a new one for the queue.
+ */
+ let newPair = function() {
+ currentPair.pivot = nextQueue[0].pivot;
+ currentPair.leaf = nextQueue[0].leaf;
+
+ nextQueue[0].pivot = nextQueue[1].pivot;
+ nextQueue[0].leaf = nextQueue[1].leaf;
+
+ nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 4)];
+ nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 4)];
+
+ pairX = 2;
+ pairOrientation = 0;
+
+ calcDropCoordinates();
+ };
+
+ /*
+ Calculates the coordinates at which the current pair will be placed when quick dropped.
+ */
+ let calcDropCoordinates = function() {
+ dropCoordinates.pivotX = pairX;
+
+ //Find Y coordinate of pivot
+ dropCoordinates.pivotY = -2;
+ for (let i = 11; i >= 0; i--) {
+ if (grid[i][pairX] == 0) {
+ dropCoordinates.pivotY = i;
+ break;
+ }
+ }
+ if (dropCoordinates.pivotY == -2 && hiddenRow[pairX] == 0)
+ dropCoordinates.pivotY = -1;
+
+ //Find coordinates of leaf
+ if (pairOrientation == 1) {
+ dropCoordinates.leafX = pairX + 1;
+
+ dropCoordinates.leafY = -2;
+ for (let i = 11; i >= 0; i--) {
+ if (grid[i][pairX + 1] == 0) {
+ dropCoordinates.leafY = i;
+ break;
+ }
+ }
+ if (dropCoordinates.leafY == -2 && hiddenRow[pairX + 1] == 0)
+ dropCoordinates.leafY = -1;
+ } else if (pairOrientation == 3) {
+ dropCoordinates.leafX = pairX - 1;
+
+ dropCoordinates.leafY = -2;
+ for (let i = 11; i >= 0; i--) {
+ if (grid[i][pairX - 1] == 0) {
+ dropCoordinates.leafY = i;
+ break;
+ }
+ }
+ if (dropCoordinates.leafY == -2 && hiddenRow[pairX - 1] == 0)
+ dropCoordinates.leafY = -1;
+ } else if (pairOrientation == 2) {
+ dropCoordinates.leafX = pairX;
+ dropCoordinates.leafY = dropCoordinates.pivotY;
+ dropCoordinates.pivotY--;
+ } else {
+ dropCoordinates.leafX = pairX;
+ dropCoordinates.leafY = dropCoordinates.pivotY - 1;
+ }
+ };
+
+ /*
+ Moves the current pair a certain number of slots.
+ */
+ let movePair = function(dx) {
+ pairX += dx;
+
+ if (dx < 0) {
+ if (pairX < (pairOrientation == 3 ? 1 : 0))
+ pairX = (pairOrientation == 3 ? 1 : 0);
+ }
+ if (dx > 0) {
+ if (pairX > (pairOrientation == 1 ? 4 : 5))
+ pairX = (pairOrientation == 1 ? 4 : 5);
+ }
+
+ calcDropCoordinates();
+ };
+
+ /*
+ Rotates the pair in the given direction around the pivot.
+ */
+ let rotatePair = function(clockwise) {
+ pairOrientation += (clockwise ? 1 : -1);
+ if (pairOrientation > 3)
+ pairOrientation = 0;
+ if (pairOrientation < 0)
+ pairOrientation = 3;
+
+ if (pairOrientation == 1 && pairX == 5)
+ pairX = 4;
+ if (pairOrientation == 3 && pairX == 0)
+ pairX = 1;
+
+ calcDropCoordinates();
+ };
+
+ /*
+ Places the current pair at the drop coordinates.
+ */
+ let quickDrop = function() {
+ if (dropCoordinates.pivotY == -1) {
+ hiddenRow[dropCoordinates.pivotX] = currentPair.pivot;
+ } else if (dropCoordinates.pivotY > -1) {
+ grid[dropCoordinates.pivotY][dropCoordinates.pivotX] = currentPair.pivot;
+ }
+
+ if (dropCoordinates.leafY == -1) {
+ hiddenRow[dropCoordinates.leafX] = currentPair.leaf;
+ } else if (dropCoordinates.leafY > -1) {
+ grid[dropCoordinates.leafY][dropCoordinates.leafX] = currentPair.leaf;
+ }
+
+ currentPair.pivot = 0;
+ currentPair.leaf = 0;
+ };
+
+ /*
+ Makes all blobs fall to the lowest available slot.
+ All blobs that fall will be added to slotsToCheck.
+ */
+ let settleBlobs = function() {
+ for (let x = 0; x < 6; x++) {
+ let lowestOpen = 11;
+ for (let y = 11; y >= 0; y--) {
+ if (grid[y][x] != 0) {
+ if (y != lowestOpen) {
+ grid[lowestOpen][x] = grid[y][x];
+ grid[y][x] = 0;
+ addSlotToCheck(x, lowestOpen);
+ }
+ lowestOpen--;
+ }
+ }
+
+ if (lowestOpen >= 0 && hiddenRow[x] != 0) {
+ grid[lowestOpen][x] = hiddenRow[x];
+ hiddenRow[x] = 0;
+ addSlotToCheck(x, lowestOpen);
+ }
+ }
+ };
+
+ /*
+ Adds a slot to slotsToCheck. This slot will be checked for a pop
+ next time popAll is called.
+ */
+ let addSlotToCheck = function(x, y) {
+ slotsToCheck.push({x: x, y: y});
+ };
+
+ /*
+ Checks for a pop at every slot in slotsToCheck.
+ Pops at all locations.
+ */
+ let popAll = function() {
+ let result = {pops: 0};
+ while(slotsToCheck.length > 0) {
+ let coord = slotsToCheck.pop();
+ if (grid[coord.y][coord.x] != 0 && grid[coord.y][coord.x] != NUISANCE_COLOR) {
+ if (checkSlotForPop(coord.x, coord.y))
+ result.pops += 1;
+ }
+ }
+ return result;
+ };
+
+ /*
+ Checks a specific slot for a pop.
+ If there are four or more adjacent blobs of the same color, they are removed.
+ */
+ let checkSlotForPop = function(x, y) {
+ let toDelete = [
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0]),
+ new Uint8Array([0, 0, 0, 0, 0, 0])
+ ];
+ let blobsInClump = 0;
+ let color = grid[y][x];
+ let toCheck = [{x: x, y: y}];
+
+ //Count every blob in this clump
+ while (toCheck.length > 0) {
+ let coord = toCheck.pop();
+ if (grid[coord.y][coord.x] == color && toDelete[coord.y][coord.x] == 0) {
+ blobsInClump++;
+ toDelete[coord.y][coord.x] = 1;
+ if (coord.x > 0) toCheck.push({x: coord.x - 1, y: coord.y});
+ if (coord.x < 5) toCheck.push({x: coord.x + 1, y: coord.y});
+ if (coord.y > 0) toCheck.push({x: coord.x, y: coord.y - 1});
+ if (coord.y < 11) toCheck.push({x: coord.x, y: coord.y + 1});
+ }
+ if (grid[coord.y][coord.x] == NUISANCE_COLOR && toDelete[coord.y][coord.x] == 0)
+ toDelete[coord.y][coord.x] = 1; //For erasing garbage
+ }
+
+ //If there are at least four blobs in this clump, remove them from the grid and draw a pop.
+ if (blobsInClump >= 4) {
+ for (let y = 0; y < 12; y++) {
+ for (let x = 0; x < 6; x++) {
+ if (toDelete[y][x] == 1) {
+ grid[y][x] = 0;
+
+ //Clear the blob out of the slot
+ g.setBgColor(0, 0, 0);
+ g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
+
+ //Draw the pop
+ let colorInfo = getColor(color);
+ g.setColor(colorInfo.r, colorInfo.g, colorInfo.b);
+ if (color < NUISANCE_COLOR) {
+ //A fancy pop for popped colors!
+ g.drawEllipse((x*18)+36, (y*14)+7, (x*18)+50, (y*14)+21);
+ g.drawEllipse((x*18)+27, (y*14)-2, (x*18)+59, (y*14)+30);
+ } else if (color == NUISANCE_COLOR) {
+ //Nuisance Blobs are simply crossed out.
+ //TODO: Nuisance Blobs are currently unusued, but also untested. Test before use.
+ g.drawLine((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
+ }
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ };
+
+ // Variables for graphics
+ let oldGhost = {pivotX: 0, pivotY: 0, leafX: 0, leafY: 0};
+
+ /*
+ Draws the time on the side.
+ */
+ let drawTime = function(scheduleNext) {
+ //Change this to alter the y-coordinate of the top edge.
+ let dy = 25;
+
+ g.setBgColor(0, 0, 0);
+ g.clearRect(2, dy, 30, dy + 121);
+
+ //Draw the time
+ let d = new Date();
+ let h = d.getHours(), m = d.getMinutes();
+ if (twelveHourTime) {
+ let mer = 'A';
+ if (h >= 12) mer = 'P';
+ if (h >= 13) h -= 12;
+ if (h == 0) h = 12;
+
+ g.setColor(1, 1, 1);
+ g.setFont("Vector", 12);
+ g.drawString(mer, 23, dy + 63);
+ }
+ let hs = h.toString().padStart(2, 0);
+ let ms = m.toString().padStart(2, 0);
+ g.setFont("Vector", 24);
+ g.setColor(1, 0.2, 1);
+ g.drawString(hs, 3, dy + 21);
+ g.setColor(0.5, 0.5, 1);
+ g.drawString(ms, 3, dy + 42);
+
+ //Draw seconds
+ let s = d.getSeconds();
+ if (updateSeconds) {
+ let ss = s.toString().padStart(2, 0);
+ g.setFont("Vector", 12);
+ g.setColor(0.2, 1, 0.2);
+ g.drawString(ss, 3, dy + 63);
+ }
+
+ //Draw the date
+ let dayString = d.getDate().toString();
+ let dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
+ let dayName = dayNames[d.getDay()];
+ let monthNames = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JLY", "AUG", "SEP", "OCT", "NOV", "DEC"];
+ let monthName = monthNames[d.getMonth()];
+ g.setColor(1, 1, 1);
+ g.setFont("Vector", 12);
+ g.drawString(monthName, 3, dy + 84);
+ g.drawString(dayString, 3, dy + 97);
+ g.setColor(0.5, 0.5, 0.5);
+ g.drawString(dayName, 3, dy + 110);
+
+ //Draw battery
+ if (s == 0) batteryLevel = E.getBattery();
+ if (Bangle.isCharging()) {
+ g.setColor(0, 0, 1);
+ } else if (batteryLevel <= 15) {
+ g.setColor(1, 0, 0);
+ } else {
+ g.setColor(0, 1, 0);
+ }
+ g.drawString(batteryLevel + "%", 3, dy + 1);
+
+ //Schedule the next draw if requested.
+ if (!scheduleNext) return;
+ if (clockDrawTimeout) clearTimeout(clockDrawTimeout);
+ let interval = updateSeconds ? 1000 : 60000;
+ clockDrawTimeout = setTimeout(function() {
+ clockDrawTimeout = undefined;
+ drawTime(true);
+ }, interval - (Date.now() % interval));
+ };
+
+ /*
+ Returns a tuple in the format {r, g, b} with the color
+ of the blob with the given ID.
+ This saves memory compared to having the colors stored in an array.
+ */
+ let getColor = function(color) {
+ if (color == 1)
+ return {r: 1, g: 0, b: 0};
+ if (color == 2)
+ return {r: 0, g: 1, b: 0};
+ if (color == 3)
+ return {r: 0, g: 0, b: 1};
+ if (color == 4)
+ return {r: 1, g: 1, b: 0};
+ if (color == 5)
+ return {r: 1, g: 0, b: 1};
+ if (color == 6)
+ return {r: 0, g: 1, b: 1};
+ if (color == 7)
+ return {r: 0.5, g: 0.5, b: 0.5};
+ return {r: 1, g: 1, b: 1};
+ };
+
+ /*
+ Clears the screen and draws the background.
+ */
+ let drawBackground = function() {
+ //Background
+ g.setBgColor(0.5, 0.2, 0.1);
+ g.clear();
+ g.setBgColor(0, 0, 0);
+ g.clearRect(33, 0, 142, 176);
+ g.setBgColor(0.5, 0.5, 0.5);
+ g.clearRect(33, 4, 142, 6);
+
+ //Reset button
+ g.setBgColor(0.5, 0.5, 0.5);
+ g.setColor(0, 0, 0);
+ g.clearRect(143, 150, 175, 175);
+ g.setFont("Vector", 30);
+ g.drawString("R", 152, 150);
+
+ //Pause button
+ g.clearRect(0, 150, 32, 175);
+ g.fillRect(9, 154, 13, 171);
+ g.fillRect(18, 154, 22, 171);
+ };
+
+ /*
+ Draws a box under the next queue that displays
+ the current value of lastChain.
+ */
+ let drawChainCount = function() {
+ g.setBgColor(0, 0, 0);
+ g.setColor(1, 0.2, 0.2);
+ g.setFont("Vector", 23);
+ g.clearRect(145, 42, 173, 64);
+
+ if (lastChain > 0) {
+ if (lastChain < 10) g.drawString(lastChain, 154, 44);
+ if (lastChain >= 10) g.drawString(lastChain, 147, 44);
+ }
+ };
+
+ /*
+ Draws the blob at the given slot.
+ */
+ let drawBlobAtSlot = function(x, y) {
+ //If this blob is in the hidden row, clear it out and stop.
+ if (y < 0) {
+ g.setBgColor(0, 0, 0);
+ g.clearRect((x*18)+34, 0, (x*18)+52, 3);
+ return;
+ }
+
+ //First, clear what was in that slot.
+ g.setBgColor(0, 0, 0);
+ g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
+
+ let color = grid[y][x];
+
+ if (color != 0) {
+ let myColor = getColor(color);
+ g.setColor(myColor.r, myColor.g, myColor.b);
+ g.fillEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
+ g.setColor(1, 1, 1);
+ g.drawEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
+ }
+ };
+
+ /*
+ Draws the ghost piece.
+ clearOld: if the previous location of the ghost piece should be cleared.
+ */
+ let drawGhostPiece = function(clearOld) {
+ if (clearOld) {
+ g.setColor(0, 0, 0);
+ g.fillRect((oldGhost.pivotX*18)+38, (oldGhost.pivotY*14)+8, (oldGhost.pivotX*18)+47, (oldGhost.pivotY*14)+17);
+ g.fillRect((oldGhost.leafX*18)+38, (oldGhost.leafY*14)+8, (oldGhost.leafX*18)+47, (oldGhost.leafY*14)+17);
+ }
+
+ let pivotX = dropCoordinates.pivotX;
+ let pivotY = dropCoordinates.pivotY;
+ let leafX = dropCoordinates.leafX;
+ let leafY = dropCoordinates.leafY;
+ let pivotColor = getColor(currentPair.pivot);
+ let leafColor = getColor(currentPair.leaf);
+
+ g.setColor(pivotColor.r, pivotColor.g, pivotColor.b);
+ g.fillRect((pivotX*18)+40, (pivotY*14)+10, (pivotX*18)+45, (pivotY*14)+15);
+ g.setColor(1, 1, 1);
+ g.drawRect((pivotX*18)+38, (pivotY*14)+8, (pivotX*18)+47, (pivotY*14)+17);
+ g.setColor(leafColor.r, leafColor.g, leafColor.b);
+ g.fillRect((leafX*18)+40, (leafY*14)+10, (leafX*18)+45, (leafY*14)+15);
+
+ oldGhost = {pivotX: pivotX, pivotY: pivotY, leafX: leafX, leafY: leafY};
+ };
+
+ /*
+ Draws the next queue.
+ */
+ let drawNextQueue = function() {
+ g.setBgColor(0, 0, 0);
+ g.clearRect(145, 4, 173, 28);
+
+ let p1 = nextQueue[0].pivot;
+ let l1 = nextQueue[0].leaf;
+ let p2 = nextQueue[1].pivot;
+ let l2 = nextQueue[1].leaf;
+ let p1C = getColor(p1);
+ let l1C = getColor(l1);
+ let p2C = getColor(p2);
+ let l2C = getColor(l2);
+
+ g.setColor(p1C.r, p1C.g, p1C.b);
+ g.fillEllipse(146, 17, 157, 28);
+ g.setColor(l1C.r, l1C.g, l1C.b);
+ g.fillEllipse(146, 5, 157, 16);
+ g.setColor(p2C.r, p2C.g, p2C.b);
+ g.fillEllipse(162, 17, 173, 28);
+ g.setColor(l2C.r, l2C.g, l2C.b);
+ g.fillEllipse(162, 5, 173, 16);
+
+ g.setColor(1, 1, 1);
+ g.drawLine(159, 4, 159, 28);
+ g.drawEllipse(146, 17, 157, 28);
+ g.drawEllipse(146, 5, 157, 16);
+ g.drawEllipse(162, 17, 173, 28);
+ g.drawEllipse(162, 5, 173, 16);
+ };
+
+ /*
+ Redraws the screen, except for the ghost piece.
+ */
+ let redrawBoard = function() {
+ drawBackground();
+ drawNextQueue();
+ drawChainCount();
+ drawTime(false);
+ for (let y = 0; y < 12; y++) {
+ for (let x = 0; x < 6; x++) {
+ drawBlobAtSlot(x, y);
+ }
+ }
+ };
+
+ /*
+ Toggles the pause screen.
+ */
+ let togglePause = function() {
+ gamePaused = !gamePaused;
+
+ if (gamePaused) {
+ g.setBgColor(0.5, 0.2, 0.1);
+ g.clear();
+ drawTime(false);
+
+ g.setBgColor(0, 0, 0);
+ g.setColor(1, 1, 1);
+ g.clearRect(48, 66, 157, 110);
+ g.setFont("Vector", 20);
+ g.drawString("Tap here\nto unpause", 50, 68);
+
+ require("widget_utils").show();
+ Bangle.drawWidgets();
+ } else {
+ require("widget_utils").hide();
+
+ redrawBoard();
+ drawGhostPiece(false);
+
+ //Display the loss text if the game is lost.
+ if (gameLost) {
+ g.setBgColor(0, 0, 0);
+ g.setColor(1, 1, 1);
+ g.clearRect(33, 73, 142, 103);
+ g.setFont("Vector", 20);
+ g.drawString("You Lose", 43, 80);
+ }
+ }
+ };
+
+ // ~~ Events ~~
+ let dragAmnt = 0;
+
+ let onTouch = (z, e) => {
+ if (midChain) return;
+
+ if (gamePaused) {
+ if (e.x >= 40 && e.y >= 58 && e.x <= 165 && e.y <= 118) {
+ g.setBgColor(1, 1, 1);
+ g.clearRect(48, 66, 157, 110);
+ g.flip();
+ togglePause();
+ }
+ } else {
+ //Tap reset button
+ if (e.x >= 143 && e.y >= 150) {
+ restartGame();
+ newPair();
+ redrawBoard();
+ drawGhostPiece(false);
+ g.flip();
+ return;
+ }
+
+ //Tap pause button
+ if (e.x <= 32 && e.y >= 150) {
+ togglePause();
+ return;
+ }
+
+ //While playing, rotate pieces.
+ if (!gameLost && !gamePaused) {
+ if (e.x < 88) {
+ rotatePair(false);
+ drawGhostPiece(true);
+ } else {
+ rotatePair(true);
+ drawGhostPiece(true);
+ }
+ }
+ }
+ };
+
+ Bangle.on("touch", onTouch);
+
+ let onDrag = (e) => {
+ if (gameLost || gamePaused || midChain) return;
+
+ //Do nothing if the user is dragging down so that they don't accidentally move while dropping
+ if (e.dy >= 5) {
+ return;
+ }
+
+ dragAmnt += e.dx;
+ if (e.b == 0) {
+ dragAmnt = 0;
+ }
+ if (dragAmnt >= 20) {
+ movePair(Math.floor(dragAmnt / 20));
+ drawGhostPiece(true);
+ dragAmnt = dragAmnt % 20;
+ }
+ if (dragAmnt <= -20) {
+ movePair(Math.ceil(dragAmnt / 20));
+ drawGhostPiece(true);
+ dragAmnt = dragAmnt % 20;
+ }
+ };
+
+ Bangle.on("drag", onDrag);
+
+ let onSwipe = (x, y) => {
+ if (gameLost || gamePaused || midChain) return;
+
+ if (y > 0) {
+ let pivotX = dropCoordinates.pivotX;
+ let pivotY = dropCoordinates.pivotY;
+ let leafX = dropCoordinates.leafX;
+ let leafY = dropCoordinates.leafY;
+
+ if (pivotY < -1 && leafY < -1) return;
+
+ quickDrop();
+ drawBlobAtSlot(pivotX, pivotY);
+ drawBlobAtSlot(leafX, leafY);
+ g.flip();
+
+ //Check for pops
+ if (pivotY >= 0) addSlotToCheck(pivotX, pivotY);
+ if (leafY >= 0) addSlotToCheck(leafX, leafY);
+ midChain = true;
+ let currentChain = 0;
+ while (popAll().pops > 0) {
+ currentChain++;
+ lastChain = currentChain;
+ drawChainCount();
+ g.flip();
+ settleBlobs();
+ redrawBoard();
+ g.flip();
+ }
+
+ newPair();
+ drawNextQueue();
+ drawGhostPiece(false);
+
+ //If the top slot of the third column is taken, lose the game.
+ if (grid[0][2] != 0) {
+ gameLost = true;
+ g.setBgColor(0, 0, 0);
+ g.setColor(1, 1, 1);
+ g.clearRect(33, 73, 142, 103);
+ g.setFont("Vector", 20);
+ g.drawString("You Lose", 43, 80);
+ }
+
+ midChain = false;
+ }
+ };
+
+ Bangle.on("swipe", onSwipe);
+
+ let onLock = on => {
+ updateSeconds = !on;
+ drawTime(true);
+ };
+
+ Bangle.on('lock', onLock);
+
+ let onCharging = charging => {
+ drawTime(false);
+ };
+
+ Bangle.on('charging', onCharging);
+
+ Bangle.setUI({mode:"clock", remove:function() {
+ //Remove listeners
+ Bangle.removeListener("touch", onTouch);
+ Bangle.removeListener("drag", onDrag);
+ Bangle.removeListener("swipe", onSwipe);
+ Bangle.removeListener('lock', onLock);
+ Bangle.removeListener('charging', onCharging);
+
+ if (clockDrawTimeout) clearTimeout(clockDrawTimeout);
+ require("widget_utils").show();
+ }});
+
+ g.reset();
+
+ Bangle.loadWidgets();
+ require("widget_utils").hide();
+
+ drawBackground();
+ drawTime(true);
+
+ restartGame();
+
+ newPair();
+ drawGhostPiece(false);
+
+ drawNextQueue();
+ drawChainCount();
+}
diff --git a/apps/bblobface/app.png b/apps/bblobface/app.png
new file mode 100644
index 000000000..2201fa621
Binary files /dev/null and b/apps/bblobface/app.png differ
diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json
new file mode 100644
index 000000000..6af247c91
--- /dev/null
+++ b/apps/bblobface/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "bblobface",
+ "name": "Bangle Blobs Clock",
+ "shortName":"BBClock",
+ "icon": "app.png",
+ "version": "1.00",
+ "description": "A fully featured watch face with a playable game on the side.",
+ "readme":"README.md",
+ "type": "clock",
+ "tags": "clock, game",
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"bblobface.app.js","url":"app.js"},
+ {"name":"bblobface.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/bblobface/screenshot1.png b/apps/bblobface/screenshot1.png
new file mode 100644
index 000000000..91650c07a
Binary files /dev/null and b/apps/bblobface/screenshot1.png differ
diff --git a/apps/bblobface/screenshot2.png b/apps/bblobface/screenshot2.png
new file mode 100644
index 000000000..64644965f
Binary files /dev/null and b/apps/bblobface/screenshot2.png differ
diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog
index 52459d312..296b6f948 100644
--- a/apps/bikespeedo/ChangeLog
+++ b/apps/bikespeedo/ChangeLog
@@ -3,3 +3,4 @@
0.03: Use default Bangle formatter for booleans
0.04: Add options for units in locale and recording GPS
0.05: Allow toggling of "max" values (screen tap) and recording (button press)
+0.06: Fix local unit setting
diff --git a/apps/bikespeedo/app.js b/apps/bikespeedo/app.js
index 327f1c754..2310f1656 100644
--- a/apps/bikespeedo/app.js
+++ b/apps/bikespeedo/app.js
@@ -420,7 +420,7 @@ function updateClock() {
// Read settings.
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
-cfg.spd = !cfg.localeUnits; // Multiplier for speed unit conversions. 0 = use the locale values for speed
+cfg.spd = cfg.localeUnits ? 0 : 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = 'km/h'; // Displayed speed unit
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json
index 87c0ed542..4b8ff9d92 100644
--- a/apps/bikespeedo/metadata.json
+++ b/apps/bikespeedo/metadata.json
@@ -2,7 +2,7 @@
"id": "bikespeedo",
"name": "Bike Speedometer (beta)",
"shortName": "Bike Speedometer",
- "version": "0.05",
+ "version": "0.06",
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
"icon": "app.png",
"screenshots": [{"url":"Screenshot.png"}],
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index d7405e763..6349d9213 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -67,3 +67,4 @@
0.56: Settings.log = 0,1,2,3 for off,display, log, both
0.57: Handle the whitelist being disabled
0.58: "Make Connectable" temporarily bypasses the whitelist
+0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds
diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js
index a12d41e1b..1b11a3f16 100644
--- a/apps/boot/bootupdate.js
+++ b/apps/boot/bootupdate.js
@@ -79,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
-if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist && !(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
+if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json
index 0a4e7e9d1..45f531776 100644
--- a/apps/boot/metadata.json
+++ b/apps/boot/metadata.json
@@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
- "version": "0.58",
+ "version": "0.59",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
diff --git a/apps/bootgattbat/ChangeLog b/apps/bootgattbat/ChangeLog
index 2a37193a3..df07f6ad0 100644
--- a/apps/bootgattbat/ChangeLog
+++ b/apps/bootgattbat/ChangeLog
@@ -1 +1,2 @@
0.01: Initial release.
+0.02: Handle the case where other apps have set bleAdvert to an array
diff --git a/apps/bootgattbat/boot.js b/apps/bootgattbat/boot.js
index d67b766b5..34d9f8d93 100644
--- a/apps/bootgattbat/boot.js
+++ b/apps/bootgattbat/boot.js
@@ -1,6 +1,22 @@
(() => {
function advertiseBattery() {
- Bangle.bleAdvert[0x180F] = [E.getBattery()];
+ if(Array.isArray(Bangle.bleAdvert)){
+ // ensure we're in the cycle
+ var found = false;
+ for(var ad in Bangle.bleAdvert){
+ if(ad[0x180F]){
+ ad[0x180F] = [E.getBattery()];
+ found = true;
+ break;
+ }
+ }
+ if(!found)
+ Bangle.bleAdvert.push({ 0x180F: [E.getBattery()] });
+ }else{
+ // simple object
+ Bangle.bleAdvert[0x180F] = [E.getBattery()];
+ }
+
NRF.setAdvertising(Bangle.bleAdvert);
}
diff --git a/apps/bootgattbat/metadata.json b/apps/bootgattbat/metadata.json
index 95a521f47..f67b4507d 100644
--- a/apps/bootgattbat/metadata.json
+++ b/apps/bootgattbat/metadata.json
@@ -2,7 +2,7 @@
"id": "bootgattbat",
"name": "BLE GATT Battery Service",
"shortName": "BLE Battery Service",
- "version": "0.01",
+ "version": "0.02",
"description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n",
"icon": "bluetooth.png",
"type": "bootloader",
diff --git a/apps/btadv/ChangeLog b/apps/btadv/ChangeLog
index 1a3bc1757..07e67157c 100644
--- a/apps/btadv/ChangeLog
+++ b/apps/btadv/ChangeLog
@@ -1 +1,2 @@
0.01: New app!
+0.02: Advertise accelerometer data and sensor location
diff --git a/apps/btadv/app.js b/apps/btadv/app.js
index 670691fb9..b72a8127a 100644
--- a/apps/btadv/app.js
+++ b/apps/btadv/app.js
@@ -1,10 +1,16 @@
+var _a;
{
var __assign = Object.assign;
var Layout_1 = require("Layout");
Bangle.loadWidgets();
Bangle.drawWidgets();
var HRM_MIN_CONFIDENCE_1 = 75;
- var services_1 = ["0x180d", "0x181a", "0x1819"];
+ var services_1 = [
+ "0x180d",
+ "0x181a",
+ "0x1819",
+ "E95D0753251D470AA062FA1922DFA9A8",
+ ];
var acc_1;
var bar_1;
var gps_1;
@@ -21,7 +27,6 @@
mag: false,
};
var idToName = {
- acc: "Acceleration",
bar: "Barometer",
gps: "GPS",
hrm: "HRM",
@@ -69,7 +74,6 @@
{
type: "h",
c: [
- __assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour_1.on, btnBorder: colour_1.on }),
__assign({ type: "btn", label: "Back", cb: function () {
setBtnsShown_1(false);
} }, btnStyle),
@@ -222,6 +226,13 @@
return [x[0], x[1], y[0], y[1], z[0], z[1]];
};
encodeMag_1.maxLen = 6;
+ var encodeAcc_1 = function (data) {
+ var x = toByteArray_1(data.x * 1000, 2, true);
+ var y = toByteArray_1(data.y * 1000, 2, true);
+ var z = toByteArray_1(data.z * 1000, 2, true);
+ return [x[0], x[1], y[0], y[1], z[0], z[1]];
+ };
+ encodeAcc_1.maxLen = 6;
var toByteArray_1 = function (value, numberOfBytes, isSigned) {
var byteArray = new Array(numberOfBytes);
if (isSigned && (value < 0)) {
@@ -251,6 +262,7 @@
case "0x180d": return !!hrm_1;
case "0x181a": return !!(bar_1 || mag_1);
case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1);
+ case "E95D0753251D470AA062FA1922DFA9A8": return !!acc_1;
}
};
var serviceToAdvert_1 = function (serv, initial) {
@@ -264,11 +276,20 @@
readable: true,
notify: true,
};
+ var os = {
+ maxLen: 1,
+ readable: true,
+ notify: true,
+ };
if (hrm_1) {
o.value = encodeHrm_1(hrm_1);
+ os.value = [2];
hrm_1 = undefined;
}
- return _a = {}, _a["0x2a37"] = o, _a;
+ return _a = {},
+ _a["0x2a37"] = o,
+ _a["0x2a38"] = os,
+ _a;
}
return {};
case "0x1819":
@@ -331,6 +352,21 @@
}
return o;
}
+ case "E95D0753251D470AA062FA1922DFA9A8": {
+ var o = {};
+ if (acc_1 || initial) {
+ o["E95DCA4B251D470AA062FA1922DFA9A8"] = {
+ maxLen: encodeAcc_1.maxLen,
+ readable: true,
+ notify: true,
+ };
+ if (acc_1) {
+ o["E95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1);
+ acc_1 = undefined;
+ }
+ }
+ return o;
+ }
}
};
var getBleAdvert_1 = function (map, all) {
@@ -402,12 +438,23 @@
enableSensors_1();
{
var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true);
- var adServices = Object
- .keys(ad)
- .map(function (k) { return k.replace("0x", ""); });
NRF.setServices(ad, {
- advertise: adServices,
uart: false,
});
+ var bangle2 = Bangle;
+ var cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
+ for (var id in ad) {
+ var serv = ad[id];
+ var value = void 0;
+ for (var ch in serv) {
+ value = serv[ch].value;
+ break;
+ }
+ cycle.push((_a = {}, _a[id] = value || [], _a));
+ }
+ bangle2.bleAdvert = cycle;
+ NRF.setAdvertising(cycle, {
+ interval: 100,
+ });
}
}
diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts
index 5e4930865..1d9501175 100644
--- a/apps/btadv/app.ts
+++ b/apps/btadv/app.ts
@@ -33,16 +33,27 @@ const enum BleServ {
// contains: LocationAndSpeed
LocationAndNavigation = "0x1819",
- // Acc // none known for this
+ // org.microbit.service.accelerometer
+ // contains: Acc
+ Acc = "E95D0753251D470AA062FA1922DFA9A8",
}
-const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation];
+const services = [
+ BleServ.HRM,
+ BleServ.EnvSensing,
+ BleServ.LocationAndNavigation,
+ BleServ.Acc,
+];
const enum BleChar {
// org.bluetooth.characteristic.heart_rate_measurement
//
HRM = "0x2a37",
+ // org.bluetooth.characteristic.body_sensor_location
+ // u8
+ SensorLocation = "0x2a38",
+
// org.bluetooth.characteristic.elevation
// s24, meters 0.01
Elevation = "0x2a6c",
@@ -65,6 +76,11 @@ const enum BleChar {
// org.bluetooth.characteristic.magnetic_flux_density_3d
// s16: x, y, z, tesla (10^-7)
MagneticFlux3D = "0x2aa1",
+
+ // org.microbit.characteristic.accelerometer_data
+ // s16 x3, -1024 .. 1024
+ // docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/
+ Acc = "E95DCA4B251D470AA062FA1922DFA9A8",
}
type BleCharAdvert = {
@@ -84,6 +100,16 @@ type LenFunc = {
maxLen: number,
}
+const enum SensorLocations {
+ Other = 0,
+ Chest = 1,
+ Wrist = 2,
+ Finger = 3,
+ Hand = 4,
+ EarLobe = 5,
+ Foot = 6,
+}
+
let acc: undefined | AccelData;
let bar: undefined | PressureData;
let gps: undefined | GPSFix;
@@ -104,8 +130,7 @@ const settings: BtAdvMap = {
mag: false,
};
-const idToName: BtAdvMap = {
- acc: "Acceleration",
+const idToName: BtAdvMap = {
bar: "Barometer",
gps: "GPS",
hrm: "HRM",
@@ -197,15 +222,6 @@ const btnLayout = new Layout(
{
type: "h",
c: [
- {
- type: "btn",
- label: idToName.acc,
- id: "acc",
- cb: () => {},
- ...btnStyle,
- col: colour.on,
- btnBorder: colour.on,
- },
{
type: "btn",
label: "Back",
@@ -464,6 +480,15 @@ const encodeMag: LenFunc = (data: CompassData) => {
};
encodeMag.maxLen = 6;
+const encodeAcc: LenFunc = (data: AccelData) => {
+ const x = toByteArray(data.x * 1000, 2, true);
+ const y = toByteArray(data.y * 1000, 2, true);
+ const z = toByteArray(data.z * 1000, 2, true);
+
+ return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
+};
+encodeAcc.maxLen = 6;
+
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
const byteArray: Array = new Array(numberOfBytes);
@@ -503,6 +528,7 @@ const haveServiceData = (serv: BleServ): boolean => {
case BleServ.HRM: return !!hrm;
case BleServ.EnvSensing: return !!(bar || mag);
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
+ case BleServ.Acc: return !!acc;
}
};
@@ -515,12 +541,22 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
readable: true,
notify: true,
};
+ const os: BleCharAdvert = {
+ maxLen: 1,
+ readable: true,
+ notify: true,
+ };
+
if (hrm) {
o.value = encodeHrm(hrm);
+ os.value = [SensorLocations.Wrist];
hrm = undefined;
}
- return { [BleChar.HRM]: o };
+ return {
+ [BleChar.HRM]: o,
+ [BleChar.SensorLocation]: os,
+ };
}
return {};
@@ -591,6 +627,25 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
return o;
}
+
+ case BleServ.Acc: {
+ const o: BleServAdvert = {};
+
+ if (acc || initial) {
+ o[BleChar.Acc] = {
+ maxLen: encodeAcc.maxLen,
+ readable: true,
+ notify: true,
+ };
+
+ if (acc) {
+ o[BleChar.Acc]!.value = encodeAcc(acc);
+ acc = undefined;
+ }
+ }
+
+ return o;
+ }
}
};
@@ -702,16 +757,39 @@ enableSensors();
// must have fixed services from the start:
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
- const adServices = Object
- .keys(ad)
- .map((k: string) => k.replace("0x", ""));
-
NRF.setServices(
ad,
{
- advertise: adServices,
uart: false,
},
);
+
+ type BleAdvert = { [key: string]: number[] };
+ const bangle2 = Bangle as {
+ bleAdvert?: BleAdvert | BleAdvert[];
+ };
+ const cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
+
+ for(const id in ad){
+ const serv = ad[id as BleServ];
+ let value;
+
+ // pick the first characteristic to advertise
+ for(const ch in serv){
+ value = serv[ch as BleChar]!.value;
+ break;
+ }
+
+ cycle.push({ [id]: value || [] });
+ }
+
+ bangle2.bleAdvert = cycle;
+
+ NRF.setAdvertising(
+ cycle,
+ {
+ interval: 100,
+ }
+ );
}
}
diff --git a/apps/btadv/metadata.json b/apps/btadv/metadata.json
index 7028b2a95..efe024a2f 100644
--- a/apps/btadv/metadata.json
+++ b/apps/btadv/metadata.json
@@ -2,7 +2,7 @@
"id": "btadv",
"name": "btadv",
"shortName": "btadv",
- "version": "0.01",
+ "version": "0.02",
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
"icon": "icon.png",
"tags": "health,tool,sensors,bluetooth",
diff --git a/apps/bthometemp/ChangeLog b/apps/bthometemp/ChangeLog
index 5560f00bc..480780ec5 100644
--- a/apps/bthometemp/ChangeLog
+++ b/apps/bthometemp/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Handle the case where other apps have set bleAdvert to an array
diff --git a/apps/bthometemp/app.js b/apps/bthometemp/app.js
index 7b55777d1..cf74c7937 100644
--- a/apps/bthometemp/app.js
+++ b/apps/bthometemp/app.js
@@ -23,7 +23,7 @@ function onTemperature(p) {
var temp100 = Math.round(avrTemp*100);
var pressure100 = Math.round(avrPressure*100);
- Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information
+ var advert = [ 0x40, /* BTHome Device Information
bit 0: "Encryption flag"
bit 1-4: "Reserved for future use"
bit 5-7: "BTHome Version" */
@@ -37,6 +37,21 @@ function onTemperature(p) {
0x04, // Pressure, 16 bit
pressure100&255,(pressure100>>8)&255,pressure100>>16
];
+
+ if(Array.isArray(Bangle.bleAdvert)){
+ var found = false;
+ for(var ad in Bangle.bleAdvert){
+ if(ad[0xFCD2]){
+ ad[0xFCD2] = advert;
+ found = true;
+ break;
+ }
+ }
+ if(!found)
+ Bangle.bleAdvert.push({ 0xFCD2: advert });
+ }else{
+ Bangle.bleAdvert[0xFCD2] = advert;
+ }
NRF.setAdvertising(Bangle.bleAdvert);
}
diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json
index 4bfd08c31..8ffb22c83 100644
--- a/apps/bthometemp/metadata.json
+++ b/apps/bthometemp/metadata.json
@@ -1,7 +1,7 @@
{ "id": "bthometemp",
"name": "BTHome Temperature and Pressure",
"shortName":"BTHome T",
- "version":"0.01",
+ "version":"0.02",
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
"icon": "app.png",
"tags": "bthome,bluetooth,temperature",
diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog
index c7902e263..12776867f 100644
--- a/apps/calendar/ChangeLog
+++ b/apps/calendar/ChangeLog
@@ -14,3 +14,4 @@
0.13: Switch to swipe left/right for month and up/down for year selection
Display events for current month on touch
0.14: Add support for holidays
+0.15: Edit holidays on device in settings
diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js
index d7c43eb1f..0ae852d83 100644
--- a/apps/calendar/calendar.js
+++ b/apps/calendar/calendar.js
@@ -75,11 +75,32 @@ function getDowLbls(locale) {
}
function sameDay(d1, d2) {
+ "jit";
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
+function drawEvent(ev, curDay, x1, y1, x2, y2) {
+ "ram";
+ switch(ev.type) {
+ case "e": // alarm/event
+ const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0;
+ const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
+ const height = (y2-2) - (y1+2); // height of a cell
+ const sliceHeight = height/eventsPerDay;
+ const ystart = (y1+2) + slice*sliceHeight;
+ g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
+ break;
+ case "h": // holiday
+ g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
+ break;
+ case "o": // other
+ g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
+ break;
+ }
+}
+
function drawCalendar(date) {
g.setBgColor(bgColor);
g.clearRect(0, 0, maxX, maxY);
@@ -118,7 +139,6 @@ function drawCalendar(date) {
true
);
- g.setFont("6x8", fontSize);
let dowLbls = getDowLbls(require('locale').name);
dowLbls.forEach((lbl, i) => {
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
@@ -172,6 +192,7 @@ function drawCalendar(date) {
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
eventsThisMonth.sort((a,b) => a.date - b.date);
let i = 0;
+ g.setFont("8x12", fontSize);
for (y = 0; y < rowN - 1; y++) {
for (x = 0; x < colN; x++) {
i++;
@@ -188,22 +209,7 @@ function drawCalendar(date) {
// Display events for this day
eventsThisMonth.forEach((ev, idx) => {
if (sameDay(ev.date, curDay)) {
- switch(ev.type) {
- case "e": // alarm/event
- const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
- const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
- const height = (y2-2) - (y1+2); // height of a cell
- const sliceHeight = height/eventsPerDay;
- const ystart = (y1+2) + slice*sliceHeight;
- g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
- break;
- case "h": // holiday
- g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
- break;
- case "o": // other
- g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
- break;
- }
+ drawEvent(ev, curDay, x1, y1, x2, y2);
eventsThisMonth.splice(idx, 1); // this event is no longer needed
}
@@ -221,17 +227,15 @@ function drawCalendar(date) {
);
}
- require("Font8x12").add(Graphics);
- g.setFont("8x12", fontSize);
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
g.drawString(
(day > 50 ? day - 50 : day).toString(),
x * colW + colW / 2,
headerH + rowH + y * rowH + rowH / 2
);
- }
- }
-}
+ } // end for (x = 0; x < colN; x++)
+ } // end for (y = 0; y < rowN - 1; y++)
+} // end function drawCalendar
function setUI() {
Bangle.setUI({
@@ -279,6 +283,7 @@ function setUI() {
});
}
+require("Font8x12").add(Graphics);
drawCalendar(date);
setUI();
// No space for widgets!
diff --git a/apps/calendar/interface.html b/apps/calendar/interface.html
index 280a96c0b..ea64632f8 100644
--- a/apps/calendar/interface.html
+++ b/apps/calendar/interface.html
@@ -28,11 +28,16 @@ function readFile(input) {
for(let i=0; i {
- const jCalData = ICAL.parse(reader.result);
+ const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data
+ const jCalData = ICAL.parse(icalText);
const comp = new ICAL.Component(jCalData);
+ const vtz = comp.getFirstSubcomponent('vtimezone');
+ const tz = new ICAL.Timezone(vtz);
+
// Fetch the VEVENT part
comp.getAllSubcomponents('vevent').forEach(vevent => {
- event = new ICAL.Event(vevent);
+ const event = new ICAL.Event(vevent);
+ event.startDate.zone = tz;
holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists
const holiday = eventToHoliday(event);
diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json
index 44a68d879..bd35c8879 100644
--- a/apps/calendar/metadata.json
+++ b/apps/calendar/metadata.json
@@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
- "version": "0.14",
+ "version": "0.15",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js
index 54ed50a64..40eca9f68 100644
--- a/apps/calendar/settings.js
+++ b/apps/calendar/settings.js
@@ -1,5 +1,6 @@
(function (back) {
var FILE = "calendar.json";
+ const HOLIDAY_FILE = "calendar.days.json";
var settings = require('Storage').readJSON(FILE, true) || {};
if (settings.ndColors === undefined)
if (process.env.HWVERSION == 2) {
@@ -7,21 +8,147 @@
} else {
settings.ndColors = false;
}
+ const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
- E.showMenu({
- "": { "title": "Calendar" },
- "< Back": () => back(),
- 'B2 Colors': {
- value: settings.ndColors,
- onchange: v => {
- settings.ndColors = v;
- writeSettings();
- }
- },
- });
-})
+ function writeHolidays() {
+ holidays.sort((a,b) => new Date(a.date) - new Date(b.date));
+ require('Storage').writeJSON(HOLIDAY_FILE, holidays);
+ }
+ function formatDate(d) {
+ return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0');
+ }
+
+ const editdate = (i) => {
+ const holiday = holidays[i];
+ const date = new Date(holiday.date);
+ const dateStr = require("locale").date(date, 1);
+ const menu = {
+ "": { "title" : holiday.name},
+ "< Back": () => {
+ writeHolidays();
+ editdates();
+ },
+ /*LANG*/"Day": {
+ value: date ? date.getDate() : null,
+ min: 1,
+ max: 31,
+ wrap: true,
+ onchange: v => {
+ date.setDate(v);
+ holiday.date = formatDate(date);
+ }
+ },
+ /*LANG*/"Month": {
+ value: date ? date.getMonth() + 1 : null,
+ format: v => require("date_utils").month(v),
+ onchange: v => {
+ date.setMonth((v+11)%12);
+ holiday.date = formatDate(date);
+ }
+ },
+ /*LANG*/"Year": {
+ value: date ? date.getFullYear() : null,
+ min: 1900,
+ max: 2100,
+ onchange: v => {
+ date.setFullYear(v);
+ holiday.date = formatDate(date);
+ }
+ },
+ /*LANG*/"Name": () => {
+ require("textinput").input({text:holiday.name}).then(result => {
+ holiday.name = result;
+ editdate(i);
+ });
+ },
+ /*LANG*/"Type": {
+ value: function() {
+ switch(holiday.type) {
+ case 'h': return 0;
+ case 'o': return 1;
+ }
+ return 0;
+ }(),
+ min: 0, max: 1,
+ format: v => [/*LANG*/"Holiday", /*LANG*/"Other"][v],
+ onchange: v => {
+ holiday.type = function() {
+ switch(v) {
+ case 0: return 'h';
+ case 1: return 'o';
+ }
+ }();
+ }
+ },
+ /*LANG*/"Repeat": {
+ value: !!holiday.repeat,
+ format: v => v ? /*LANG*/"Yearly" : /*LANG*/"Never",
+ onchange: v => {
+ holiday.repeat = v ? 'y' : undefined;
+ }
+ },
+ /*LANG*/"Delete": () => E.showPrompt(/*LANG*/"Delete" + " " + menu[""].title + "?").then(function(v) {
+ if (v) {
+ holidays.splice(i, 1);
+ writeHolidays();
+ editdates();
+ } else {
+ editday(i);
+ }
+ }
+ ),
+ };
+ try {
+ require("textinput");
+ } catch(e) {
+ // textinput not installed
+ delete menu[/*LANG*/"Name"];
+ }
+
+ E.showMenu(menu);
+ };
+
+ const editdates = () => {
+ const menu = holidays.map((holiday,i) => {
+ const date = new Date(holiday.date);
+ const dateStr = require("locale").date(date, 1);
+ return {
+ title: dateStr + ' ' + holiday.name,
+ onchange: v => setTimeout(() => editdate(i), 10),
+ };
+ });
+
+ menu[''] = { 'title': 'Holidays' };
+ menu['< Back'] = ()=>settingsmenu();
+ E.showMenu(menu);
+ };
+
+ const settingsmenu = () => {
+ E.showMenu({
+ "": { "title": "Calendar" },
+ "< Back": () => back(),
+ 'B2 Colors': {
+ value: settings.ndColors,
+ onchange: v => {
+ settings.ndColors = v;
+ writeSettings();
+ }
+ },
+ /*LANG*/"Edit Holidays": () => editdates(),
+ /*LANG*/"Add Holiday": () => {
+ holidays.push({
+ "date":formatDate(new Date()),
+ "name":/*LANG*/"New",
+ "type":'h',
+ });
+ editdate(holidays.length-1);
+ },
+ });
+ };
+ settingsmenu();
+})
diff --git a/apps/chargerot/ChangeLog b/apps/chargerot/ChangeLog
index 5560f00bc..07029aebd 100644
--- a/apps/chargerot/ChangeLog
+++ b/apps/chargerot/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Handle missing settings (e.g. first-install)
diff --git a/apps/chargerot/boot.js b/apps/chargerot/boot.js
index 0a4361c50..2daeb3d50 100644
--- a/apps/chargerot/boot.js
+++ b/apps/chargerot/boot.js
@@ -1,5 +1,5 @@
(() => {
- const chargingRotation = 0 | require('Storage').readJSON("chargerot.settings.json").rotate;
+ const chargingRotation = 0 | (require('Storage').readJSON("chargerot.settings.json",1)||{}).rotate;
const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate;
if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear();
Bangle.on('charging', (charging) => {
diff --git a/apps/chargerot/metadata.json b/apps/chargerot/metadata.json
index 1b13403d7..8174836be 100644
--- a/apps/chargerot/metadata.json
+++ b/apps/chargerot/metadata.json
@@ -1,7 +1,7 @@
{
"id": "chargerot",
"name": "Charge LCD rotation",
- "version": "0.01",
+ "version": "0.02",
"description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.",
"icon": "icon.png",
"tags": "battery",
diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog
index e12b30692..870808eff 100644
--- a/apps/clock_info/ChangeLog
+++ b/apps/clock_info/ChangeLog
@@ -4,4 +4,5 @@
0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps
eg. when a clockinfo is selected, swipes won't affect swipe-down widgets
0.05: Reported image for battery is now transparent (2v18+)
-0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
\ No newline at end of file
+0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
+0.07: Developer tweak: clkinfo load errors are emitted
diff --git a/apps/clock_info/lib.js b/apps/clock_info/lib.js
index 9dd975f1e..e6c9eb27f 100644
--- a/apps/clock_info/lib.js
+++ b/apps/clock_info/lib.js
@@ -141,7 +141,7 @@ exports.load = function() {
if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a);
} catch(e){
- console.log("Could not load clock info "+E.toJS(fn));
+ console.log("Could not load clock info "+E.toJS(fn)+": "+e);
}
});
diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json
index ef9a3effa..993f112e7 100644
--- a/apps/clock_info/metadata.json
+++ b/apps/clock_info/metadata.json
@@ -1,7 +1,7 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
- "version":"0.06",
+ "version":"0.07",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",
diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog
index faf3d2d33..d147a623b 100644
--- a/apps/dragboard/ChangeLog
+++ b/apps/dragboard/ChangeLog
@@ -5,3 +5,5 @@
0.05: Now scrolls text when string gets longer than screen width.
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.
0.07: Settings for display colors
+0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js
index 83aae5f14..78ef11bd4 100644
--- a/apps/dragboard/lib.js
+++ b/apps/dragboard/lib.js
@@ -103,6 +103,133 @@ exports.input = function(options) {
initDraw();
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
+ let dragHandlerDB = function(event) {
+ "ram";
+ // ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ // Choose character by draging along red rectangle at bottom of screen
+ if (event.y >= ( (R.y+R.h) - 12 )) {
+ // Translate x-position to character
+ if (event.x < ABCPADDING) { abcHL = 0; }
+ else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
+ else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
+
+ // Datastream for development purposes
+ //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
+
+ // Unmark previous character and mark the current one...
+ // Handling switching between letters and numbers/punctuation
+ if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ if (abcHL != abcHLPrev) {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
+ }
+ // Print string at top of screen
+ if (event.b == 0) {
+ text = text + ABC.charAt(abcHL);
+ updateTopString();
+
+ // Autoswitching letter case
+ if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
+ }
+ // Update previous character to current one
+ abcHLPrev = abcHL;
+ typePrev = 'abc';
+ }
+
+ // 12345678901234567890
+ // Choose number or puctuation by draging on green rectangle
+ else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
+ // Translate x-position to character
+ if (event.x < NUMPADDING) { numHL = 0; }
+ else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
+ else { numHL = Math.floor((event.x-NUMPADDING)/6); }
+
+ // Datastream for development purposes
+ //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
+
+ // Unmark previous character and mark the current one...
+ // Handling switching between letters and numbers/punctuation
+ if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+
+ if (numHL != numHLPrev) {
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+ showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
+ }
+ // Print string at top of screen
+ if (event.b == 0) {
+ g.setColor(HLCOLOR);
+ // Backspace if releasing before list of numbers/punctuation
+ if (event.x < NUMPADDING) {
+ // show delete sign
+ showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
+ delSpaceLast = 1;
+ text = text.slice(0, -1);
+ updateTopString();
+ //print(text);
+ }
+ // Append space if releasing after list of numbers/punctuation
+ else if (event.x > (R.x+R.w)-NUMPADDING) {
+ //show space sign
+ showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
+ delSpaceLast = 1;
+ text = text + ' ';
+ updateTopString();
+ //print(text);
+ }
+ // Append selected number/punctuation
+ else {
+ text = text + NUMHIDDEN.charAt(numHL);
+ updateTopString();
+
+ // Autoswitching letter case
+ if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
+ }
+ }
+ // Update previous character to current one
+ numHLPrev = numHL;
+ typePrev = 'num';
+ }
+
+ // Make a space or backspace by swiping right or left on screen above green rectangle
+ else if (event.y > 20+4) {
+ if (event.b == 0) {
+ g.setColor(HLCOLOR);
+ if (event.x < (R.x+R.w)/2) {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ // show delete sign
+ showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
+ delSpaceLast = 1;
+
+ // Backspace and draw string upper right corner
+ text = text.slice(0, -1);
+ updateTopString();
+ if (text.length==0) changeCase(abcHL);
+ //print(text, 'undid');
+ }
+ else {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ //show space sign
+ showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
+ delSpaceLast = 1;
+
+ // Append space and draw string upper right corner
+ text = text + NUMHIDDEN.charAt(0);
+ updateTopString();
+ //print(text, 'made space');
+ }
+ }
+ }
+ };
+
+ let catchSwipe = ()=>{
+ E.stopEventPropagation&&E.stopEventPropagation();
+ };
+
function changeCase(abcHL) {
if (settings.uppercase) return;
g.setColor(BGCOLOR);
@@ -119,131 +246,12 @@ exports.input = function(options) {
mode: 'custom',
back: ()=>{
Bangle.setUI();
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(text);
},
- drag: function(event) {
- "ram";
- // ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Choose character by draging along red rectangle at bottom of screen
- if (event.y >= ( (R.y+R.h) - 12 )) {
- // Translate x-position to character
- if (event.x < ABCPADDING) { abcHL = 0; }
- else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
- else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
-
- // Datastream for development purposes
- //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
-
- // Unmark previous character and mark the current one...
- // Handling switching between letters and numbers/punctuation
- if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- if (abcHL != abcHLPrev) {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
- }
- // Print string at top of screen
- if (event.b == 0) {
- text = text + ABC.charAt(abcHL);
- updateTopString();
-
- // Autoswitching letter case
- if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
- }
- // Update previous character to current one
- abcHLPrev = abcHL;
- typePrev = 'abc';
- }
-
- // 12345678901234567890
- // Choose number or puctuation by draging on green rectangle
- else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
- // Translate x-position to character
- if (event.x < NUMPADDING) { numHL = 0; }
- else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
- else { numHL = Math.floor((event.x-NUMPADDING)/6); }
-
- // Datastream for development purposes
- //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
-
- // Unmark previous character and mark the current one...
- // Handling switching between letters and numbers/punctuation
- if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
-
- if (numHL != numHLPrev) {
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
- showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
- }
- // Print string at top of screen
- if (event.b == 0) {
- g.setColor(HLCOLOR);
- // Backspace if releasing before list of numbers/punctuation
- if (event.x < NUMPADDING) {
- // show delete sign
- showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
- delSpaceLast = 1;
- text = text.slice(0, -1);
- updateTopString();
- //print(text);
- }
- // Append space if releasing after list of numbers/punctuation
- else if (event.x > (R.x+R.w)-NUMPADDING) {
- //show space sign
- showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
- delSpaceLast = 1;
- text = text + ' ';
- updateTopString();
- //print(text);
- }
- // Append selected number/punctuation
- else {
- text = text + NUMHIDDEN.charAt(numHL);
- updateTopString();
-
- // Autoswitching letter case
- if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
- }
- }
- // Update previous character to current one
- numHLPrev = numHL;
- typePrev = 'num';
- }
-
- // Make a space or backspace by swiping right or left on screen above green rectangle
- else if (event.y > 20+4) {
- if (event.b == 0) {
- g.setColor(HLCOLOR);
- if (event.x < (R.x+R.w)/2) {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- // show delete sign
- showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
- delSpaceLast = 1;
-
- // Backspace and draw string upper right corner
- text = text.slice(0, -1);
- updateTopString();
- if (text.length==0) changeCase(abcHL);
- //print(text, 'undid');
- }
- else {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- //show space sign
- showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
- delSpaceLast = 1;
-
- // Append space and draw string upper right corner
- text = text + NUMHIDDEN.charAt(0);
- updateTopString();
- //print(text, 'made space');
- }
- }
- }
- }
+ drag: dragHandlerDB,
});
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
});
};
diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json
index 58de5153c..5c52d9389 100644
--- a/apps/dragboard/metadata.json
+++ b/apps/dragboard/metadata.json
@@ -1,6 +1,6 @@
{ "id": "dragboard",
"name": "Dragboard",
- "version":"0.07",
+ "version":"0.08",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog
index a228aab54..bca1ca7c4 100644
--- a/apps/draguboard/ChangeLog
+++ b/apps/draguboard/ChangeLog
@@ -1 +1,3 @@
0.01: New App based on dragboard, but with a U shaped drag area
+0.02: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js
index 258f8b02d..6c63668a9 100644
--- a/apps/draguboard/lib.js
+++ b/apps/draguboard/lib.js
@@ -104,45 +104,53 @@ exports.input = function(options) {
}
}
+ let dragHandlerUB = function(event) {
+ "ram";
+
+ // drag on middle bottom rectangle
+ if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
+ moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
+ }
+ // drag on left or right rectangle
+ else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
+ moveCharPos(event.x ( (R.y2) - 52 ))) {
+ moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
+ }
+ // Make a space or backspace by tapping right or left on screen above green rectangle
+ else if (event.y > R.y && event.b == 0) {
+ if (event.x < (R.x2)/2) {
+ showChars('<-');
+ text = text.slice(0, -1);
+ } else {
+ //show space sign
+ showChars('->');
+ text += ' ';
+ }
+ prevChar = null;
+ updateTopString();
+ }
+ };
+
+ let catchSwipe = ()=>{
+ E.stopEventPropagation&&E.stopEventPropagation();
+ };
+
return new Promise((resolve,reject) => {
// Interpret touch input
Bangle.setUI({
mode: 'custom',
back: ()=>{
Bangle.setUI();
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(text);
},
- drag: function(event) {
- "ram";
-
- // drag on middle bottom rectangle
- if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
- moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
- }
- // drag on left or right rectangle
- else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
- moveCharPos(event.x ( (R.y2) - 52 ))) {
- moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
- }
- // Make a space or backspace by tapping right or left on screen above green rectangle
- else if (event.y > R.y && event.b == 0) {
- if (event.x < (R.x2)/2) {
- showChars('<-');
- text = text.slice(0, -1);
- } else {
- //show space sign
- showChars('->');
- text += ' ';
- }
- prevChar = null;
- updateTopString();
- }
- }
+ drag: dragHandlerDB
});
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
R = Bangle.appRect;
MIDPADDING = R.x + 35;
diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json
index dc9b06254..620f39f71 100644
--- a/apps/draguboard/metadata.json
+++ b/apps/draguboard/metadata.json
@@ -1,6 +1,6 @@
{ "id": "draguboard",
"name": "DragUboard",
- "version":"0.01",
+ "version":"0.02",
"description": "A library for text input via swiping U-shaped keyboard.",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog
new file mode 100644
index 000000000..b96d7207d
--- /dev/null
+++ b/apps/edgeclk/ChangeLog
@@ -0,0 +1,3 @@
+0.01: Initial release.
+0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps.
+0.03: Added option to display weather.
diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md
new file mode 100644
index 000000000..90f6443fc
--- /dev/null
+++ b/apps/edgeclk/README.md
@@ -0,0 +1,31 @@
+# Edge Clock
+
+
+
+
+
+
+Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability.
+It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging.
+There are three progress bars that indicate day of the week, time of the day, and daily step goal.
+The watch face is monochrome and allows for applying your favorite color scheme.
+
+The appearance is highly configurable. In the settings menu you can:
+- De-/activate a buzz when the charger is connected while the watch face is active.
+- Decide if month or day should be displayed first.
+- Switch between 24h and 12h clock.
+- Hide or display seconds.*
+- Show AM/PM in place of the seconds.
+- Show weather temperature and icon in place of the seconds.
+- Set the daily step goal.
+- En- or disable the individual progress bars.
+- Set if your week should start with Monday or Sunday (for week progress bar).
+
+*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well.
+
+The clock implements Fast Loading for faster switching to and fro.
+
+## Contributors
+ - [tinxx](https://github.com/tinxx)
+ - [peerdavid](https://github.com/peerdavid)
+
\ No newline at end of file
diff --git a/apps/edgeclk/app-icon.js b/apps/edgeclk/app-icon.js
new file mode 100644
index 000000000..b81918b73
--- /dev/null
+++ b/apps/edgeclk/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgMgBAoFEuADCgP8sAFD/wLE/wXDCIIjFAAv/ABQRF5fegEPgfe5UbgEJgVS5ebBYMyr36BYdC7YXEGq4AFj8f/ED8f+ApHjAoMHjkA8HjxwFIgAFCC4IFJjk4AoodEAogXBAoI1BDoYFGL5Z3XmHv33whkfuAFE/Fgw0whuD/Fjz0wh/fuALCh/Y/Fv30wgOf7AFE"))
diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js
new file mode 100644
index 000000000..f9d5f803b
--- /dev/null
+++ b/apps/edgeclk/app.js
@@ -0,0 +1,342 @@
+{
+ /* Configuration
+ ------------------------------------------------------------------------------*/
+
+ const settings = Object.assign({
+ buzzOnCharge: true,
+ monthFirst: true,
+ twentyFourH: true,
+ showAmPm: false,
+ showSeconds: true,
+ showWeather: false,
+ stepGoal: 10000,
+ stepBar: true,
+ weekBar: true,
+ mondayFirst: true,
+ dayBar: true,
+ }, require('Storage').readJSON('edgeclk.settings.json', true) || {});
+
+ /* Runtime Variables
+ ------------------------------------------------------------------------------*/
+
+ let startTimeout;
+ let drawInterval;
+
+ let lcdPower = true;
+ let charging = Bangle.isCharging();
+
+ const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA');
+
+ const iconSize = [19, 26];
+ const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA=');
+ const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
+ const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
+
+
+ /* Draw Functions
+ ------------------------------------------------------------------------------*/
+
+ const drawAll = function () {
+ const date = new Date();
+
+ drawDate(date);
+ if (settings.showSeconds) drawSecs(date);
+ drawTime(date);
+ drawLower();
+ };
+
+ const drawLower = function (stepsOnlyCount) {
+ if (charging) {
+ drawCharge();
+ } else {
+ drawSteps(stepsOnlyCount);
+ }
+
+ drawWeather();
+ };
+
+ const drawWeather = function () {
+ if (!settings.showWeather){
+ return;
+ }
+
+ g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
+ g.setFontAlign(1, 1); // right bottom
+
+ try{
+ const weather = require('weather');
+ const w = weather.get();
+ let temp = parseInt(w.temp-273.15);
+ temp = temp < 0 ? '\\' + String(temp*-1) : String(temp);
+
+ g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true);
+
+ // clear icon area in case weather condition changed
+ g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight());
+ weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14);
+
+ } catch(e) {
+ g.drawString("???", g.getWidth()-3, g.getHeight() - 1, true);
+ }
+ };
+
+ const drawDate = function (date) {
+ const top = 30;
+ g.reset();
+
+ // weekday
+ g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
+ g.setFontAlign(-1, -1); // left top
+ g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true);
+
+ // date
+ g.setFontAlign(1, -1); // right top
+ // Note: to save space first and last two lines of ASCII are left out.
+ // That is why '-' is assigned to '\' and ' ' (space) to '_'.
+ if (settings.monthFirst) {
+ g.drawString((date.getMonth()+1).toString().padStart(2, '_')
+ + '\\'
+ + date.getDate().toString().padStart(2, 0),
+ g.getWidth(), top + 12, true);
+ } else {
+ g.drawString('_'
+ + date.getDate().toString().padStart(2, 0)
+ + '\\'
+ + (date.getMonth()+1).toString(),
+ g.getWidth(), top + 12, true);
+ }
+
+ // line/progress bar
+ if (settings.weekBar) {
+ let weekday = date.getDay();
+ if (settings.mondayFirst) {
+ if (weekday === 0) { weekday = 7; }
+ } else {
+ weekday += 1;
+ }
+ drawBar(top, weekday/7);
+ } else {
+ drawLine(top);
+ }
+ };
+
+ const drawTime = function (date) {
+ const top = 72;
+ g.reset();
+
+ const h = date.getHours();
+ g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9)
+ g.setFontAlign(-1, -1); // left top
+ g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0),
+ 0, top+12, true);
+ g.setFontAlign(0, -1); // center top
+ g.drawString(':', g.getWidth()/2, top+12, false);
+ const m = date.getMinutes();
+ g.setFontAlign(1, -1); // right top
+ g.drawString(m.toString().padStart(2, 0),
+ g.getWidth(), top+12, true);
+
+ if (settings.showAmPm) {
+ g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
+ g.setFontAlign(1, 1); // right bottom
+ g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true);
+ }
+
+ if (settings.dayBar) {
+ drawBar(top, (h*60+m)/1440);
+ } else {
+ drawLine(top);
+ }
+ };
+
+ const drawSecs = function (date) {
+ g.reset();
+ g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
+ g.setFontAlign(1, 1); // right bottom
+ g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true);
+ };
+
+ const drawSteps = function (onlyCount) {
+ g.reset();
+ g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
+ g.setFontAlign(-1, 1); // left bottom
+
+ const steps = Bangle.getHealthStatus('day').steps;
+ const toKSteps = settings.showWeather ? 1000 : 100000;
+ g.drawString((steps < toKSteps ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'),
+ iconSize[0] + 6, g.getHeight() - 1, true);
+
+ if (onlyCount === true) {
+ return;
+ }
+
+ const progress = steps / settings.stepGoal;
+ if (settings.stepBar) {
+ drawBar(g.getHeight() - 38, progress);
+ } else {
+ drawLine(g.getHeight() - 38);
+ }
+
+ // icon
+ if (progress < 1) {
+ g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]);
+ } else {
+ g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]);
+ }
+ };
+
+ const drawCharge = function () {
+ g.reset();
+ g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
+ g.setFontAlign(-1, 1); // left bottom
+
+ const charge = E.getBattery();
+ g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true);
+
+ drawBar(g.getHeight() - 38, charge / 100);
+ g.drawImage(plugIcon, 0, g.getHeight() - 26);
+ };
+
+ const drawBar = function (top, progress) {
+ // draw frame
+ g.drawRect(0, top, g.getWidth() - 1, top + 5);
+ g.drawRect(1, top + 1, g.getWidth() - 2, top + 4);
+ // clear bar area
+ g.clearRect(2, top + 2, g.getWidth() - 3, top + 3);
+ // draw bar
+ const barLen = progress >= 1 ? g.getWidth() : (g.getWidth() - 4) * progress;
+ if (barLen < 1) return;
+ g.drawLine(2, top + 2, barLen + 2, top + 2);
+ g.drawLine(2, top + 3, barLen + 2, top + 3);
+ };
+
+ const drawLine = function (top) {
+ const width = g.getWidth();
+ g.drawLine(0, top + 2, width, top + 2);
+ g.drawLine(0, top + 3, width, top + 3);
+ };
+
+
+ /* Event Handlers
+ ------------------------------------------------------------------------------*/
+
+ const onSecondInterval = function () {
+ const date = new Date();
+ drawSecs(date);
+ if (date.getSeconds() === 0) {
+ onMinuteInterval();
+ }
+ };
+
+ const onMinuteInterval = function () {
+ const date = new Date();
+ drawTime(date);
+ drawLower(true);
+ };
+
+ const onMinuteIntervalStarter = function () {
+ drawInterval = setInterval(onMinuteInterval, 60000);
+ startTimeout = null;
+ onMinuteInterval();
+ };
+
+ const onLcdPower = function (on) {
+ lcdPower = on;
+ if (on) {
+ drawAll();
+ startTimers();
+ } else {
+ stopTimers();
+ }
+ };
+
+ const onMidnight = function () {
+ if (!lcdPower) return;
+ drawDate(new Date());
+ // Lower part (steps/charge) will be updated every minute.
+ // However, to save power while on battery only step count will get updated.
+ // This will update icon and progress bar as well:
+ if (!charging) drawSteps();
+ drawWeather();
+ };
+
+ const onHealth = function () {
+ if (!lcdPower || charging) return;
+ // This will update progress bar and icon:
+ drawSteps();
+ drawWeather();
+ };
+
+ const onLock = function (locked) {
+ if (locked) return;
+ drawLower();
+ };
+
+ const onCharging = function (isCharging) {
+ charging = isCharging;
+ if (isCharging && settings.buzzOnCharge) Bangle.buzz();
+ if (!lcdPower) return;
+ drawLower();
+ };
+
+
+ /* Lifecycle Functions
+ ------------------------------------------------------------------------------*/
+
+ const registerEvents = function () {
+ // This is for original Bangle.js; version two has always-on display:
+ Bangle.on('lcdPower', onLcdPower);
+
+ // Midnight event is triggered when health data is reset and a new day begins:
+ Bangle.on('midnight', onMidnight);
+
+ // Health data is published via 10 mins interval:
+ Bangle.on('health', onHealth);
+
+ // Lock event signals screen (un)lock:
+ Bangle.on('lock', onLock);
+
+ // Charging event signals when charging status changes:
+ Bangle.on('charging', onCharging);
+ };
+
+ const deregisterEvents = function () {
+ Bangle.removeListener('lcdPower', onLcdPower);
+ Bangle.removeListener('midnight', onMidnight);
+ Bangle.removeListener('health', onHealth);
+ Bangle.removeListener('lock', onLock);
+ Bangle.removeListener('charging', onCharging);
+ };
+
+ const startTimers = function () {
+ if (drawInterval) return;
+ if (settings.showSeconds) {
+ drawInterval = setInterval( onSecondInterval, 1000);
+ } else {
+ startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000);
+ }
+ };
+
+ const stopTimers = function () {
+ if (startTimeout) clearTimeout(startTimeout);
+ if (!drawInterval) return;
+ clearInterval(drawInterval);
+ drawInterval = null;
+ };
+
+
+ /* Startup Process
+ ------------------------------------------------------------------------------*/
+
+ g.clear();
+ drawAll();
+ startTimers();
+ registerEvents();
+
+ Bangle.setUI({mode: 'clock', remove: function() {
+ stopTimers();
+ deregisterEvents();
+ }});
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+}
diff --git a/apps/edgeclk/app.png b/apps/edgeclk/app.png
new file mode 100644
index 000000000..3a0bbe130
Binary files /dev/null and b/apps/edgeclk/app.png differ
diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json
new file mode 100644
index 000000000..0d53cd008
--- /dev/null
+++ b/apps/edgeclk/metadata.json
@@ -0,0 +1,20 @@
+{
+ "id": "edgeclk",
+ "name": "Edge Clock",
+ "shortName": "Edge Clock",
+ "version": "0.03",
+ "description": "Crisp clock with perfect readability.",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}, {"url":"screenshot4.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"edgeclk.app.js", "url": "app.js"},
+ {"name":"edgeclk.settings.js", "url": "settings.js"},
+ {"name":"edgeclk.img", "url": "app-icon.js", "evaluate": true}
+ ],
+ "data": [{"name":"edgeclk.settings.json"}]
+}
diff --git a/apps/edgeclk/screenshot.png b/apps/edgeclk/screenshot.png
new file mode 100644
index 000000000..758dca96b
Binary files /dev/null and b/apps/edgeclk/screenshot.png differ
diff --git a/apps/edgeclk/screenshot2.png b/apps/edgeclk/screenshot2.png
new file mode 100644
index 000000000..febac2d2c
Binary files /dev/null and b/apps/edgeclk/screenshot2.png differ
diff --git a/apps/edgeclk/screenshot3.png b/apps/edgeclk/screenshot3.png
new file mode 100644
index 000000000..bdad9e1d5
Binary files /dev/null and b/apps/edgeclk/screenshot3.png differ
diff --git a/apps/edgeclk/screenshot4.png b/apps/edgeclk/screenshot4.png
new file mode 100644
index 000000000..66ec85c89
Binary files /dev/null and b/apps/edgeclk/screenshot4.png differ
diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js
new file mode 100644
index 000000000..6f38e774c
--- /dev/null
+++ b/apps/edgeclk/settings.js
@@ -0,0 +1,125 @@
+(function(back) {
+ const SETTINGS_FILE = 'edgeclk.settings.json';
+ const storage = require('Storage');
+
+ const settings = {
+ buzzOnCharge: true,
+ monthFirst: true,
+ twentyFourH: true,
+ showAmPm: false,
+ showSeconds: true,
+ showWeather: false,
+ stepGoal: 10000,
+ stepBar: true,
+ weekBar: true,
+ mondayFirst: true,
+ dayBar: true,
+ };
+
+ const saved_settings = storage.readJSON(SETTINGS_FILE, true);
+ if (saved_settings) {
+ for (const key in saved_settings) {
+ if (!settings.hasOwnProperty(key)) continue;
+ settings[key] = saved_settings[key];
+ }
+ }
+
+ let save = function() {
+ storage.write(SETTINGS_FILE, settings);
+ }
+
+ E.showMenu({
+ '': { 'title': 'Edge Clock' },
+ '< Back': back,
+ 'Charge Buzz': {
+ value: settings.buzzOnCharge,
+ onchange: () => {
+ settings.buzzOnCharge = !settings.buzzOnCharge;
+ save();
+ },
+ },
+ 'Month First': {
+ value: settings.monthFirst,
+ onchange: () => {
+ settings.monthFirst = !settings.monthFirst;
+ save();
+ },
+ },
+ '24h Clock': {
+ value: settings.twentyFourH,
+ onchange: () => {
+ settings.twentyFourH = !settings.twentyFourH;
+ save();
+ },
+ },
+ 'Show AM/PM': {
+ value: settings.showAmPm,
+ onchange: () => {
+ settings.showAmPm = !settings.showAmPm;
+ // TODO can this be visually changed?
+ if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false;
+ if (settings.showAmPm && settings.showWeather) settings.showWeather = false;
+ save();
+ },
+ },
+ 'Show Seconds': {
+ value: settings.showSeconds,
+ onchange: () => {
+ settings.showSeconds = !settings.showSeconds;
+ // TODO can this be visually changed?
+ if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false;
+ if (settings.showSeconds && settings.showWeather) settings.showWeather = false;
+ save();
+ },
+ },
+ 'Show Weather': {
+ value: settings.showWeather,
+ onchange: () => {
+ settings.showWeather = !settings.showWeather;
+ // TODO can this be visually changed?
+ if (settings.showWeather && settings.showAmPm) settings.showAmPm = false;
+ if (settings.showWeather && settings.showSeconds) settings.showSeconds = false;
+ save();
+ },
+ },
+ 'Step Goal': {
+ value: settings.stepGoal,
+ min: 250,
+ max: 50000,
+ step: 250,
+ onchange: v => {
+ settings.stepGoal = v;
+ save();
+ }
+ },
+ 'Step Progress': {
+ value: settings.stepBar,
+ onchange: () => {
+ settings.stepBar = !settings.stepBar;
+ save();
+ }
+ },
+ 'Week Progress': {
+ value: settings.weekBar,
+ onchange: () => {
+ settings.weekBar = !settings.weekBar;
+ save();
+ },
+ },
+ 'Week Start': {
+ value: settings.mondayFirst,
+ format: () => settings.mondayFirst ? 'Monday' : 'Sunday',
+ onchange: () => {
+ settings.mondayFirst = !settings.mondayFirst;
+ save();
+ },
+ },
+ 'Day Progress': {
+ value: settings.dayBar,
+ onchange: () => {
+ settings.dayBar = !settings.dayBar;
+ save();
+ },
+ },
+ });
+})
diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog
index 4e68ab2c7..6581e5188 100644
--- a/apps/fastload/ChangeLog
+++ b/apps/fastload/ChangeLog
@@ -2,3 +2,5 @@
0.02: Allow redirection of loads to the launcher
0.03: Allow hiding the fastloading info screen
0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app).
+0.05: Check for changes in setting.js and force real reload if needed
+0.06: Fix caching whether an app is fastloadable
diff --git a/apps/fastload/README.md b/apps/fastload/README.md
index be4175f55..d82e13461 100644
--- a/apps/fastload/README.md
+++ b/apps/fastload/README.md
@@ -12,6 +12,7 @@ This allows fast loading of all apps with two conditions:
* If Quick Launch is installed it can be excluded from app history
* Allows to redirect all loads usually loading the clock to the launcher instead
* The "Fastloading..." screen can be switched off
+* Enable checking `setting.json` and force a complete load on changes
## App history
diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js
index c7fc2fd86..57bc8ea94 100644
--- a/apps/fastload/boot.js
+++ b/apps/fastload/boot.js
@@ -19,15 +19,24 @@ let loadingScreen = function(){
let cache = s.readJSON("fastload.cache") || {};
-let checkApp = function(n){
+const SYS_SETTINGS="setting.json";
+
+let appFastloadPossible = function(n){
+ if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || s.hash(SYS_SETTINGS) != cache[SYS_SETTINGS])){
+ cache[SYS_SETTINGS] = s.hash(SYS_SETTINGS);
+ s.writeJSON("fastload.cache", cache);
+ return false;
+ }
+
// no widgets, no problem
if (!global.WIDGETS) return true;
- let app = s.read(n);
- if (cache[n] && E.CRC32(app) == cache[n].crc)
+ let hash = s.hash(n);
+ if (cache[n] && hash == cache[n].hash)
return cache[n].fast;
+ let app = s.read(n);
cache[n] = {};
cache[n].fast = app.includes("Bangle.loadWidgets");
- cache[n].crc = E.CRC32(app);
+ cache[n].hash = hash;
s.writeJSON("fastload.cache", cache);
return cache[n].fast;
};
@@ -39,7 +48,7 @@ let slowload = function(n){
};
let fastload = function(n){
- if (!n || checkApp(n)){
+ if (!n || appFastloadPossible(n)){
// Bangle.load can call load, to prevent recursion this must be the system load
global.load = slowload;
Bangle.load(n);
diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json
index 954a7d8b4..8edd1f95b 100644
--- a/apps/fastload/metadata.json
+++ b/apps/fastload/metadata.json
@@ -1,7 +1,7 @@
{ "id": "fastload",
"name": "Fastload Utils",
"shortName" : "Fastload Utils",
- "version": "0.04",
+ "version": "0.06",
"icon": "icon.png",
"description": "Enable experimental fastloading for more apps",
"type":"bootloader",
diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js
index 66c990df1..15c135fe4 100644
--- a/apps/fastload/settings.js
+++ b/apps/fastload/settings.js
@@ -59,6 +59,13 @@
}
};
+ mainmenu['Detect settings changes'] = {
+ value: !!settings.detectSettingsChange,
+ onchange: v => {
+ writeSettings("detectSettingsChange",v);
+ }
+ };
+
return mainmenu;
}
diff --git a/apps/gbridge/PROTOCOL.md b/apps/gbridge/PROTOCOL.md
index 7191ca0b1..2147dc049 100644
--- a/apps/gbridge/PROTOCOL.md
+++ b/apps/gbridge/PROTOCOL.md
@@ -1,179 +1 @@
-# Watch -> Phone
-
-## show toast
-
-```
-{ "t": "info", "msg": "message" }
-```
-
-t can be one of "info", "warn", "error"
-
-## report battery level
-
-```
-{ "t": "status", "bat": 30, "volt": 30, "chg": 0 }
-```
-
-* bat is in range 0 to 100
-* volt is optional and should be greater than 0
-* chg is optional and should be either 0 or 1 to indicate the watch is charging
-
-## find phone
-
-```
-{ "t": "findPhone", "n": true }
-```
-
-n is an boolean and toggles the find phone function
-
-## control music player
-
-```
-{ "t": "music", "n": "play" }
-```
-
-n can be one of "play", "pause", "playpause", "next", "previous", "volumeup", "volumedown", "forward", "rewind"
-
-## control phone call
-
-```
-{ "t": "call", "n": "accept"}
-```
-
-n can be one of "accept", "end", "incoming", "outcoming", "reject", "start", "ignore"
-
-## react to notifications
-
-Send a response to a notification from phone
-
-```
-{
- "t": "notify",
- "n": "dismiss",
- "id": 2,
- "tel": "+491234",
- "msg": "message",
-}
-```
-
-* n can be one of "dismiss", "dismiss all", "open", "mute", "reply"
-* id, tel and message are optional
-
-# Phone -> Watch
-
-## show notification
-
-```
-{
- "t": "notify",
- "id": 2,
- "src": "app",
- "title": "titel",
- "subject": "subject",
- "body": "message body",
- "sender": "sender",
- "tel": "+491234"
- }
-```
-
-## notification deleted
-
-This event is send when the user skipped a notification
-
-```
-{ "t": "notify-", "id": 2 }
-```
-
-## set alarm
-
-```
-{
- "t": "alarm",
- "d": [
- { "h": 13, "m": 37 },
- { "h": 8, "m": 0 }
- ]
-}
-```
-
-## call state changed
-
-```
-{
- "t": "call",
- "cmd": "accept",
- "name": "name",
- "number": "+491234"
-}
-```
-
-cmd can be one of "", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end"
-
-## music state changed
-
-```
-{
- "t": "musicstate",
- "state": "play",
- "position": 40,
- "shuffle": 0,
- "repeat": 1
-}
-```
-
-## set music info
-
-```
-{
- "t": "musicinfo",
- "artist": "artist",
- "album": "album",
- "track": "track",
- "dur": 1,
- "c": 2,
- "n" 3
-}
-```
-
-* dur is the duration of the track
-* c is the track count
-* n is the track number
-
-## find device
-
-```
-{
- "t": "find",
- "n": true
-}
-```
-
-n toggles find device functionality
-
-## set constant vibration
-
-```
-{
- "t": "vibrate",
- "n": 2
-}
-```
-
-n is the intensity
-
-## send weather
-
-```
-{
- "t": "weather",
- "temp": 10,
- "hum": 71,
- "txt": "condition",
- "wind": 13,
- "loc": "location"
-}
-```
-
-* hum is the humidity
-* txt is the weather condition
-* loc is the location
+For up to date protocol info, please see http://www.espruino.com/Gadgetbridge
diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js
index 3b5f2c780..6f4b945a5 100644
--- a/apps/gbridge/widget.js
+++ b/apps/gbridge/widget.js
@@ -263,7 +263,7 @@
function sendActivity(hrm) {
var steps = currentSteps - lastSentSteps;
lastSentSteps = currentSteps;
- gbSend({ t: "act", stp: steps, hrm:hrm });
+ gbSend({ t: "act", stp: steps, hrm:hrm, rt:1 });
}
// Battery monitor
diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog
index 8646ba11a..09637df1b 100644
--- a/apps/gipy/ChangeLog
+++ b/apps/gipy/ChangeLog
@@ -87,3 +87,23 @@
* Reduce framerate if locked
* Stroke to move around in the map
* Fix for missing paths in display
+
+0.20:
+ * Large display for instant speed
+ * Bugfix for negative coordinates
+ * Disable menu while the map is not loaded
+ * Turn screen off while idling to save battery (with setting)
+ * New setting : disable buzz on turns
+ * New setting : turn bluetooth off to save battery
+ * New setting : power screen off between points to save battery
+ * Color change for lost direction (now purple)
+ * Adaptive screen powersaving
+
+0.21:
+ * Jit is back for display functions (10% speed increase)
+ * Store, parse and display elevation data
+ * Removed 'lost' indicator (we now change position to purple when lost)
+ * Powersaving fix : don't powersave when lost
+ * Bugfix for negative remaining distance when going backwards
+ * New settings for powersaving
+ * Adjustments to powersaving algorithm
diff --git a/apps/gipy/README.md b/apps/gipy/README.md
index 44a8b9bcd..8539167e1 100644
--- a/apps/gipy/README.md
+++ b/apps/gipy/README.md
@@ -18,8 +18,8 @@ It provides the following features :
- display the path with current position from gps
- display a local map around you, downloaded from openstreetmap
- detects and buzzes if you leave the path
-- buzzes before sharp turns
-- buzzes before waypoints
+- (optional) buzzes before sharp turns
+- (optional) buzzes before waypoints
(for example when you need to turn in https://mapstogpx.com/)
- display instant / average speed
- display distance to next point
@@ -28,6 +28,7 @@ It provides the following features :
- toilets
- artwork
- bakeries
+- display elevation data if available in the trace
## Usage
@@ -41,7 +42,7 @@ also a nice open source option.
Note that *mapstogpx* has a super nice feature in its advanced settings.
You can turn on 'next turn info' and be warned by the watch when you need to turn.
-Once you have your gpx file you need to convert it to *gpc* which is my custom file format.
+Once you have your gpx file you need to convert it to *gps* which is my custom file format.
They are smaller than gpx and reduce the number of computations left to be done on the watch.
Just click the disk icon and select your gpx file.
@@ -51,8 +52,8 @@ Your path will be displayed in svg.
### Starting Gipy
At start you will have a menu for selecting your trace (if more than one).
-Choose the one you want and you will reach the splash screen where you'll wait for the gps signal.
-Once you have a signal you will reach the main screen:
+Choose the one you want and you will reach the splash screen where you'll wait for the map.
+Once the map is loaded you will reach the main screen:

@@ -78,28 +79,74 @@ On your screen you can see:
* green: artwork
- a *turn* indicator on the top right when you reach a turning point
- a *gps* indicator (blinking) on the top right if you lose gps signal
-- a *lost* indicator on the top right if you stray too far away from path
### Lost
-If you stray away from path we will rescale the display to continue displaying nearby segments and
-display the direction to follow as a black segment.
+If you stray away from path we will display the direction to follow as a purple segment. Your main position will also turn to purple.
Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you
are. On path it just needed to scan a few points ahead and behind.
-The distance to next point displayed corresponds to the length of the black segment.
+The distance to next point displayed corresponds to the length of the purple segment.
### Menu
If you click the button you'll reach a menu where you can currently zoom out to see more of the map
-(with a slower refresh rate) and reverse the path direction.
+(with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on).
+
+### Elevation
+
+If you touch the screen you will switch between display modes.
+The first one displays the map, the second one the nearby elevation and the last one the elevation
+for the whole path.
+
+
+
+Colors correspond to slopes.
+Above 15% will be red, above 8% orange, above 3% yellow, between 3% and -3% is green and shades of blue
+are for descents.
+
+You should note that the precision is not very good. The input data is not very precise and you only get the
+slopes between path points. Don't expect to see small bumps on the road.
### Settings
Few settings for now (feel free to suggest me more) :
+- buzz on turns : should the watch buzz when reaching a waypoint ?
+- disable bluetooth : turn bluetooth off completely to try to save some power.
- lost distance : at which distance from path are you considered to be lost ?
+- wake-up speed : if you drive below this speed powersaving will disable itself
+- active-time : how long (in seconds) the screen should be turned on if activated before going back to sleep.
+- brightness : how bright should screen be ? (by default 0.5, again saving power)
+- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points,
+when you touch the screen and when speed is below 13km/h.
+
+### Powersaving
+
+Starting with release 0.20 we experiment with power saving.
+
+There are now two display modes :
+
+- active : the screen is lit back (default at 50% light but can be configured with the *brightness* setting)
+- inactive : by default the screen is not lit but you can also power it off completely (with the *power lcd off* setting)
+
+The algorithm works in the following ways :
+
+- some events will *activate* : the display will turn *active*
+- if no activation event occur for at least 10 seconds (or *active-time* setting) we switch back to *inactive*
+
+Activation events are the following :
+
+- you are near (< 100m) the next point on path
+- you are slow (< *wake-up speed* setting (13 km/h by default))
+- you press the button / touch the screen
+
+
+Power saving has been tested on a very long trip with several benefits
+
+- longer battery life
+- waking up near path points will attract your attention more easily when needed
### Caveats
@@ -107,6 +154,7 @@ It is good to use but you should know :
- the gps might take a long time to start initially (see the assisted gps update app).
- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else.
+- if you adventure in gorges the gps signal will become garbage.
- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY**
- sometimes the watch will tell you that you are lost but you are in fact on the path. It usually figures again
the real gps position after a few minutes. It usually happens when the signal is acquired very fast.
@@ -119,4 +167,8 @@ I had to go back uphill by quite a distance.
Feel free to give me feedback : is it useful for you ? what other features would you like ?
+If you want to raise issues the main repository is [https://github.com/wagnerf42/BangleApps](here) and
+the rust code doing the actual map computations is located [https://github.com/wagnerf42/gps](here).
+You can try the cutting edge version at [https://wagnerf42.github.io/BangleApps/](https://wagnerf42.github.io/BangleApps/)
+
frederic.wagner@imag.fr
diff --git a/apps/gipy/TODO b/apps/gipy/TODO
index 266a1c5c9..8c767c463 100644
--- a/apps/gipy/TODO
+++ b/apps/gipy/TODO
@@ -1,19 +1,57 @@
+*** thoughts on lcd power ***
+so, i tried experimenting with turning the lcd off in order to save power.
+
+the good news: this saves a lot. i did a 3h ride which usually depletes the battery and I still had
+around two more hours to go.
+
+now the bad news:
+
+- i had to de-activate the twist detection : you cannot raise your watch to the eyes to turn it on.
+that's because with twist detection on all road bumps turn the watch on constantly.
+- i tried manual detection like :
+
+Bangle.on('accel', function(xyz) {
+
+ if (xyz.diff > 0.4 && xyz.mag > 1 && xyz.z < -1.4) {
+ Bangle.setLCDPower(true);
+ Bangle.setLocked(false);
+ }
+
+});
+
+this works nicely when you sit on a chair with a simulated gps signal but does not work so nicely when on the bike.
+sometimes it is ok, sometimes you can try 10 times with no success.
+
+- instead i use screen touch to turn it on. that's a bother since you need two hands but well it could be worth it.
+the problem is in the delay: between 1 and 5 seconds before the screen comes back on.
+
+
+my conclusion is that:
+
+* we should not turn screen off unless we agree to have an unresponsive ui
+* we should maybe autowake near segments ends and when lost
+* we should play with backlight instead
+
+
+**************************
+
+JIT: array declaration in jit is buggy
+(especially several declarations)
+
+**************************
+
++ try disabling gps for more powersaving
+
++ when you walk the direction still has a tendency to shift
+
++ put back foot only ways
++ put back street names
++ put back shortest paths but with points cache this time and jit
++ how to display paths from shortest path ?
+
+
+misc:
+ use Bangle.project(latlong)
-* additional features
-- config screen
- - are we on foot (and should use compass)
-
-- we need to buzz 200m before sharp turns (or even better, 30seconds)
-(and look at more than next point)
-
-- display distance to next water/toilet ?
-- display scale (100m)
-
-- compress path ?
-
-* misc
-
-- code is becoming messy
diff --git a/apps/gipy/app.js b/apps/gipy/app.js
index 60e4bb5af..46e29c359 100644
--- a/apps/gipy/app.js
+++ b/apps/gipy/app.js
@@ -3,6 +3,7 @@ let displaying = false;
let in_menu = false;
let go_backwards = false;
let zoomed = true;
+let powersaving = true;
let status;
let interests_colors = [
@@ -14,32 +15,38 @@ let interests_colors = [
];
let Y_OFFSET = 20;
+
+// some constants for screen types
+let MAP = 0;
+let HEIGHTS_ZOOMED_IN = 1;
+let HEIGHTS_FULL = 2;
+
let s = require("Storage");
var settings = Object.assign(
{
lost_distance: 50,
+ wake_up_speed: 13,
+ active_time: 10,
+ brightness: 0.5,
+ buzz_on_turns: false,
+ disable_bluetooth: true,
+ power_lcd_off: false,
},
s.readJSON("gipy.json", true) || {}
);
-let profile_start_times = [];
+// let profile_start_times = [];
-let splashscreen = require("heatshrink").decompress(
- atob(
- "2Gwgdly1ZATttAQfZARm2AQXbAREsyXJARmyAQXLAViDgARm2AQVbAR0kyVJAQ2yAQVLARZfBAQSD/ARXZAQVtARnbAQe27aAE5ICClgCMLgICCQEQCCkqDnARb+BAQW2AQyDEARdLAQeyAR3LAQSDXL51v+x9bfAICC7ICM23ZPpD4BAQXJn//7IFCAQ2yAQR6YQZOSQZpBBsiDZARm2AQVbAQSDIAQt///btufTAOyBYL+DARJrBAQSDWLJvvQYNlz/7tiAeEYICBtoCHQZ/+7ds//7tu2pMsyXJlmOnAFDyRoBAQSAWAQUlyVZAQxcBAQX//3ZsjIBWYUtBYN8uPHjqMeAQVbQZ/2QYXbQYNbQwRNBnHjyVLkhNBARvLAQSDLIgNJKZf/+1ZsjIBlmzQwXPjlwg8cux9YtoCD7ICCQZ192yDBIINt2f7tuSvED/0AgeOhMsyXJAQeyAQR6MARElyT+BAQ9lIIL+CsqDF21Ajlx4EAuPBQa4CIQZ0EQYNnAQNt2QCByU48f+nEAh05kuyC4L+DARJ3BAQSDJsmWpICEfwJQEkESoNl2wXByaDB2PAQYPHgEB4cgEYKDc7KDOkmAgMkyCABy3bsuegHjx/4QYM4sk27d/+XJlmSAQpcBAQSAKAQQ1BZAVZkoCHBYNIgEApMgEwcHQYUcgPHEYVv+SDaGQSDNAQZDByUbDQM48eOn/ggCDB23bIIICB/1LC4ICB2QCLPoICEfwNJARA1BAQZEDgEJkkyQAKDB/gCBQYUt+ACB/yDsAQVA8ESrKDC//+nIjB7dt/0bQYNJlmS5ICG2QCCcwQCGGQslAQdZAQ4RDQAPJQYUf//DGQKAB31LQYKeCQbmT//8QZlIQAM4QYkZQYe+raDCC4eyAQVLARaDBAoL4CAQNkz///4FCAQxWCp8AQAKDCjlwU4OCQYcv3yDfIAP/+SDM8EOQYOPCgOAhFl2CDB20bQwIUCfwICMLgICC2XLGQsnIISnDKAVZkoCDpKADAQUSoARBhcs2/Dlm2QbEEiFJggvBeAIAC5KDKpKDF8AIBgEAhMkw3LQYgCIfYICC2QCHCgl/IIf5smWpICIniDELgQdBoEAgVJkqDboMkiVBIAYABQZcjxyDB//4Bw2QRAIIEfAICC5ICM2XJkGSUgIXBIIvkEwklAQdZkiDD4IOBrILDC4UAQbYCBo5BF/iDKkiDB//+LgYCY2QCCpYCCkGCpEkwVPIIv/fwMkAQNkAQuRQYNwBAVZAQRoCRgSDcv5BG+RlLvHjQDHJAQUsAQ6DBhACBn5BG/wpOrMlARZuBAQSDRgEQgMAiJAGAAPJgmQpMEfbQCSpaDDx5BJCgVkAQWWARhoBAQR9SQY0AoEEv5BI/MkiVBPs0sAQfJAQUAQYQ5Bj4CB/hHEExz+BAQT+BARVlAQSDPAAKDJ/8EiFBAQeQQ0gCFkECgEj//HQYUcuPHIIXkwQaHfYICCsgCMrICCQByDFHwQAI/iDFiVBkkSQc3JIIfx46ACAQ1yhEgyUJAQImOrICCkoCLPQICCQZCCKAAXBQYYCFyFJgiGiIIX8QBACD4EgwVIkmCDo1kAQWWARh0BAQR9GQY8H8aDM/CDJiVBkkSQccHQBQCDgGChCGBAQOShImLfYICFfwICKsoCCQYcAQRn+n/8iEBgCGIAQWQQbtPQaMcuSDEwVIkmCEw77BAQVkARlZAQSACAQN/IIM/8f+nCCI8f//H/x0AgkAoCDJiVBkkSQbOT/8AgKANAQiDEAQsJkA1PrICCkoCIz5BBhyDBxyDJAAYOB/iZBAAMBgCGIAQdJgiDUFwKDUjkCQZEIkmCpApCsgCFywCLv9lAoNl//HQYk/P5Hjx4GE+CEDgkAoCDKoMkiQCBPpeT//8AoMnQYSARAQVwH4OAQxMgyUJAQQ7IfwICCrMlz48B+VZngsBgeP/CAIAAaDB8YGD/CEDAAMDMQUQgKJJyFJAQRKGEYK8BhIqCQCQCEgECgEggUIEAX8QwkkwVIHAz7BAQVkAQN/+KqCg4pCOIKDN/0/QwQADwCCCBYIRDoEEgCDHAQMkiQCBJQiABnHggE4VoSDXAQPAgEPKoyDCAQkJkCGFAQdPEYcBFIaAMABsDBA/8gEBgEQgKGIAQNJgmSnCDDhwFDQbICBv5MI5CGFkmCpCACsgCCyImJfAYAOCIPjBA4TI8kAoCDKoMnPQJ9CgeAAQKDdAQMfHgXxBYl+QYYCEhMgyUJngRBgAAHf6R6Cx4FCnALDxyGC/BuCAQVAFoUQgKDEoARF8EOgACBiSDdjlwg4LIpMkhSGHo8cQJEkyRuDABxcBQwaDBMoIFCEYMONwY+BnFL12SoEgoEEgCDCCIfjwE4gYCBhMk2SDeuPAIQKGDFIOSIgICCyCDDwPAQY8SCgXjQaL4FAowAB+EAgYIB9cu3Xrlmy5JECGwIOCDQYCC0gOBCgKAbuB9DAQUAgPHQAgCEkUHP4wABTAplDABaSDPogCDEgMOQwX6r/+QYJrB5csySDCpaAIx06pYUEQbUAAQQABBAPSpF145uFAQOXjkB4ACCC4VIgCVGQYf+n7+FAgYLFMonghyrEh0SpeuyVIkmypEgF4MuQBE49IRB9euQYWyQbUcdw0HNYoCCpFwg8AAQYVDSo6DDKAKDLnAFF8EAfYOAgHj1gjBRIPjlxrDGQOQQBACBnVLl269esQbhrBhMh4BoEw8dNwslDQvAjkBAQKAHQYn4QZHjx4EBL4IJCMokA9ck3ED1xoBlmS8LyB5MgRgSAIAQOkPoIaD2VLlmCQbF0L4ZrLrgUBgCYBAQYABTYgCGPQwAELgX//xfBAQRlCxmS9euyTsCdISABAQKPBQBOOnVJCgKDCC4cgQbEAMpQCDkoaHgPAjkEDRj4C8aGCQY4CGwm48EEMoOscwQFBAQNIkApBhyAInCABTwSbB1waCAoMk2SDVuj1BAQJoLrgXFuEHgFwgUJTxpWDfASADn5iFgYCBgEO2XpLgPL0mSMQOSF4UIkmQTxOOiCYCQYIdBAQUuQYILBPprjBAoMAAQUAMplJkojKuAaNQYoCCQY47BnHgeQPggG69aDENwOChEgwUJCIKDKTAKDCAQKDC5Ms3XIkCDFPQYCE4VcIQIABi8cMptIU5UADRqDHgHj/xiG9JBDiXj0hlB1hrB0mCEAKABkmQDQihDAQQyCPQOyTYIdB1iGBBANIAQMcgLaCgBiIKwtdMpmHDpApBQB4CCeoXhh0QQY+Q9ek3Xr1z+BcYLsDQYKABEYIgBDQYgE9eOiQXCAQI4DQwIIBkmyhYLBgBZBjpZBL4clMQhlQpCAIAQMJQacAgiDBl26L4M6fYO4AoJ3BxgCB126pekL4fJkGChEgyT+FAQvpF4PJOgKDBwR6BUgYCCBwOygB6BVQR9BgVckmXjkAMSIUBQZPSQCKDDl04eoKDDoeu3DmBfYRZBSQLpCQYIdBQYJcBPomP/AFDwm4fYXJkmCpACBHAOy5CPCBAMJCIMJkPCI4VcuESeQcBMqCAJAQNwQCQCCheunT4CoeAiXr1m69MAmSDDcAlLL4MIkGSpb+E8f+AoihBVoXLCgL7C9csDodJAoMLQYZ3DrkAKAkgRIYCLQBICCuiDWPQKDCcYL4BBAaJCBAMsLgWShKDCkmQPQgCG8L7B5aDDAoaDBTwKJC1ytDI4tIL4qPEARMlQBVxDRoCKbQXol2y9JxBpaDBKASJB2TmBQAkgwVJhx9Ex/4QYkQDoVLF4IjFQAXIkizCFgSDGASlcQBICBuAmYpcuJQICCcYRZBL4YIB5MgQYKABQYOSfwvj/wFD8MAPoIgEhICB5L4FQYQRBRIKDaw6AJAQMBVTLRCJQSDCAoTpDPoKDCQAOCDQKAEAQ8LlhxCyRxChCnCliPB1wOBEYI7C5ACBQbCAKjdtwCqZQYZTDAoSDBBYtJLgKDBC4J9F//4AoXbtuwpcuOgIdBfYL4DEwOS9aDBFIOC5ckAQMuQbCAIAQPG7VtmiDbkGy5IFB5KGDAQYIChKDCkm4fwv/Aoc27dp01L0gmCwXr1gjDDoIFB1ytBBwIRCBARZVkqAIAQX2YoMwQbbdB5L1BhJZBboR9BAoSABQYNJhyADAQ2P2xBBw9LPoNIC4KDBOIIvB5B6CAoICBEwIFB9aDWriAJAQRBCnCDgbQJQCwUJlzdCBYWQPov//yDFYoXHof8EwRxBFgJ3CEYOC5KwBQYVLl26SoZWSw6AKAQMB/5KCjsEQbICBLgO65JWBhJWBpbUEd4J6Ex0//6JEoel4BCB48IDoPrkiGBAQa2CWASDBBAQvBSoZWRQBYCBpMF/8DI4NAQCyDEwT4BZwJTBBYJQBl2ShIOBhZ6EfwP/RIk68eBQQKDBgKDCeoPIFgYpBBYIFCQYXLQAPr1iDSQBYCB6VIurFB/04pf0QbFJkGChMsQYOucwRTCBwW4PQgCB//4BAkQYoUcv/CpMMEAOu3QgBwVIF4QpCAoPJAoICB2SGCKB8lQBaDDKYOS/+kWwaDZJQLOCcYLRByVLcAUOQAmPQAoCCEAME3UJZANBDQPJlxxD5AvBQZFIQadIQBgCBF4NIkrCBkkSQDCDE5ZKB9YCBRIJcBLIMDPQv/QY+uPQMEiVBgmyhBrCAQIpBU4R0DPQOCBwY7BBwIIBKBqAMkoCBCgeQpApBQb5oBAQSDBhEg3B6F//+QAmEyCDBTYWyfAL+BFIQgBF4SDCQAIFE126QYQUBQZp0CQZd0y4UCpB9aAQihCKYSJCFIOChEuPQmOn//RIiDB3VJlz+CTYRxBJRCDF1g1B1myRIOCTwKDMpCALQYYUEQcACBdISDBwSMBwVDPQuP/6JEQYfrdgIjC5CDD2QFBF4Wy5ICDQYOu2XrQYKPBQYI1BJpaAMAQVwQchWCAoZKBdgO4PQwCJPQMu3RxCPoyqB5YCCFgeyQYKeBBYNIQZ0lQBoCCuiDkLIRlCJQUIhyAOnHpDoRuBfAZoCQAosEpAUBBAKDB1iDBBYNLkiDJpCAOAQMJPr4CFJoLXCyUIMoMDQBoCB3FL1gdBNwPrEYSGCQAQFDBYaDDAoKPCQYcsQZKAOjskw6AjAQREBQYuAPQ3//AIFoeu3VLAQSDCRIQmB9ekFgSDBGQe6PQKABGQIOCAQQ+DJQ2HQZvXQEwCDIgMJkGCQYL+G//+BAs6QAL1C3TvDQYJoCRIOCpYsBhYIBpEuCga2BfwdLBYUsRIRHEkKALAQXCrqDuhaAEAQM//4IGQYW6QYKABQYQFBQYXLSQMLkgmBBAMIO4UgGoICCQYQjBQZFcQBgCDQE4CBhJWCQYJ3EAQOP/4IGAQKbBL4RlBeQQCCQYR6B9esR4fIBANLQAeCDQOShaDJy6AOQY+CMQaDgAQKDB3CDQiXJO4PJEARiBQwQICNYKDDpYOBC4IRDBAIRCQYYaBQYklQB6DFpCDBQAazDATcIEwICBfY3j//4QY86MQSDDfwREDwXLNYPrPoQUBQASPD1wLDQZMhQaEgwCDEMoiDfpBfBhMOQY3//yMHeQIdDdgZuBPQILBwRrCQwQCB3SDCpcuBAJ9BDQKGCAQJEFQBwCBjt0PRkJQbkIQYMDfYwCJ8JcBcAaDBQARrCQYYICQYnrTwPLQYKGBTYYaCCIOCIgSAOQYbdDQdSAO8eunFBPoKDByTmBQYOkRgIFBEwSDC5MgBYR6B1x3BAQQIBQAXIEASDDy6DPkmHpAXDTwZlGQb24QZ+kyFLOgSDD2RiBPoYmCKYL1DBYSACpcufwQCBSQKDD1hoCw6DPkvXLgiDpPQ3//yDIdgJcBfwVL0h3CyRuCFIiDDAQSYCUIJ9BCIMLQYwaBkqANAQV16S2EMQqJDBY6DWlx6Fn//QAoCCwkyQYJ3BlxfB0iACQZCVDfwYFBpJ9CBwMJRIQRC1gdBQBwCCuAvDO4cgQYgFBQbsLO4uP/6AGAQPhhxWBQYe6QAXJEw4LDOIRNBQYXIQYMIQYYIBBYNLFINIQaEJQYIdCHAaDCAQqDcgZ6F/6DJpYyCLgPrkm6EAiMBQY5TGfwSDB5AOEboaDBQByDDkESQYogCEYYCfO4qCB/CDI8ckiVLC4KDBPoQCBMQPr0gLB1jvCFgcIkGCKYOy5YLBQYQUCQa3CQASDIQECDHn///yAHx069ZWBOIXL1zyDBYO65esAoICBhIUBNwKDCQAKDEDQYgDQbB6jQZ6AGQYfBQYZoBl265JuCkm6PQQFBwUIBYPJBAKJC5MgBwKDCRgKDBSoWCCISDQ6VBL5AsBAoVIQceP/6DKiR6CO4QaBQYQjGQYRHBPoILDQYWCRgVIQYNL126RgOyeQOCQZ50EC4OSWwImCQwaDkQQKAHAQOEEaR9BQYTRGKwOCpaDBhCDBR4SDCBwSDPuAmCwSDCAQQ1DQwSDiQQKDKx0SFjSDFBASDCcwQRDBwIA="
- )
-);
+// function start_profiling() {
+// profile_start_times.push(getTime());
+// }
-function start_profiling() {
- profile_start_times.push(getTime());
-}
-
-function end_profiling(label) {
- let end_time = getTime();
- let elapsed = end_time - profile_start_times.pop();
- console.log("profile:", label, "took", elapsed);
-}
+// function end_profiling(label) {
+// let end_time = getTime();
+// let elapsed = end_time - profile_start_times.pop();
+// console.log("profile:", label, "took", elapsed);
+// }
// return the index of the largest element of the array which is <= x
function binary_search(array, x) {
@@ -141,6 +148,26 @@ class TilesOffsets {
}
}
+// this function is not inlined to avoid array declaration in jit
+function center_points(points, scaled_current_x, scaled_current_y) {
+ return g.transformVertices(points, [
+ 1,
+ 0,
+ 0,
+ 1,
+ -scaled_current_x,
+ -scaled_current_y,
+ ]);
+}
+
+// this function is not inlined to avoid array declaration in jit
+function rotate_points(points, c, s) {
+ let center_x = g.getWidth() / 2;
+ let center_y = g.getHeight() / 2 + Y_OFFSET;
+
+ return g.transformVertices(points, [-c, s, s, c, center_x, center_y]);
+}
+
class Map {
constructor(buffer, offset, filename) {
this.points_cache = []; // don't refetch points all the time
@@ -152,7 +179,7 @@ class Map {
color_array[2] / 255,
];
offset += 3;
- this.first_tile = Uint32Array(buffer, offset, 2); // absolute tile id of first tile
+ this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile
offset += 2 * 4;
this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height
offset += 2 * 4;
@@ -267,18 +294,18 @@ class Map {
sin_direction
)
) {
-// let colors = [
-// [0, 0, 0],
-// [0, 0, 1],
-// [0, 1, 0],
-// [0, 1, 1],
-// [1, 0, 0],
-// [1, 0, 1],
-// [1, 1, 0],
-// [1, 1, 0.5],
-// [0.5, 0, 0.5],
-// [0, 0.5, 0.5],
-// ];
+ // let colors = [
+ // [0, 0, 0],
+ // [0, 0, 1],
+ // [0, 1, 0],
+ // [0, 1, 1],
+ // [1, 0, 0],
+ // [1, 0, 1],
+ // [1, 1, 0],
+ // [1, 1, 0.5],
+ // [0.5, 0, 0.5],
+ // [0, 0.5, 0.5],
+ // ];
if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) {
this.display_thick_tile(
x,
@@ -323,12 +350,14 @@ class Map {
let tile_center_y = (tile_y + 0.5) * side;
let scaled_center_x = (tile_center_x - current_x) * scale_factor;
let scaled_center_y = (tile_center_y - current_y) * scale_factor;
- let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction;
- let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction;
+ let rotated_center_x =
+ scaled_center_x * cos_direction - scaled_center_y * sin_direction;
+ let rotated_center_y =
+ scaled_center_x * sin_direction + scaled_center_y * cos_direction;
let on_screen_center_x = center_x - rotated_center_x;
let on_screen_center_y = center_y + rotated_center_y;
- let scaled_side = side * scale_factor * Math.sqrt(1/2);
+ let scaled_side = side * scale_factor * Math.sqrt(1 / 2);
if (on_screen_center_x + scaled_side <= 0) {
return false;
@@ -395,28 +424,28 @@ class Map {
cos_direction,
sin_direction
) {
- "jit";
- let center_x = g.getWidth() / 2;
- let center_y = g.getHeight() / 2 + Y_OFFSET;
-
+ "jit";
let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
let scaled_current_x = current_x * scale_factor;
let scaled_current_y = current_y * scale_factor;
+ let recentered_points = center_points(
+ points,
+ scaled_current_x,
+ scaled_current_y
+ );
+ let screen_points = rotate_points(
+ recentered_points,
+ cos_direction,
+ sin_direction
+ );
- for (let i = 0; i < points.length; i += 4) {
- let scaled_x = points[i] - scaled_current_x;
- let scaled_y = points[i + 1] - scaled_current_y;
- let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let final_x = center_x - rotated_x;
- let final_y = center_y + rotated_y;
- scaled_x = points[i + 2] - scaled_current_x;
- scaled_y = points[i + 3] - scaled_current_y;
- rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let new_final_x = center_x - rotated_x;
- let new_final_y = center_y + rotated_y;
- g.drawLine(final_x, final_y, new_final_x, new_final_y);
+ for (let i = 0; i < screen_points.length; i += 4) {
+ g.drawLine(
+ screen_points[i],
+ screen_points[i + 1],
+ screen_points[i + 2],
+ screen_points[i + 3]
+ );
}
}
@@ -429,26 +458,27 @@ class Map {
cos_direction,
sin_direction
) {
- let center_x = g.getWidth() / 2;
- let center_y = g.getHeight() / 2 + Y_OFFSET;
+ "jit";
let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor);
let scaled_current_x = current_x * scale_factor;
let scaled_current_y = current_y * scale_factor;
+ let recentered_points = center_points(
+ points,
+ scaled_current_x,
+ scaled_current_y
+ );
+ let screen_points = rotate_points(
+ recentered_points,
+ cos_direction,
+ sin_direction
+ );
- for (let i = 0; i < points.length; i += 4) {
- let scaled_x = points[i] - scaled_current_x;
- let scaled_y = points[i + 1] - scaled_current_y;
- let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let final_x = center_x - rotated_x;
- let final_y = center_y + rotated_y;
- scaled_x = points[i + 2] - scaled_current_x;
- scaled_y = points[i + 3] - scaled_current_y;
- rotated_x = scaled_x * cos_direction - scaled_y * sin_direction;
- rotated_y = scaled_x * sin_direction + scaled_y * cos_direction;
- let new_final_x = center_x - rotated_x;
- let new_final_y = center_y + rotated_y;
+ for (let i = 0; i < screen_points.length; i += 4) {
+ let final_x = screen_points[i];
+ let final_y = screen_points[i + 1];
+ let new_final_x = screen_points[i + 2];
+ let new_final_y = screen_points[i + 3];
let xdiff = new_final_x - final_x;
let ydiff = new_final_y - final_y;
@@ -471,7 +501,7 @@ class Map {
class Interests {
constructor(buffer, offset) {
- this.first_tile = Uint32Array(buffer, offset, 2); // absolute tile id of first tile
+ this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile
offset += 2 * 4;
this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height
offset += 2 * 4;
@@ -602,10 +632,15 @@ class Interests {
}
class Status {
- constructor(path, maps, interests) {
+ constructor(path, maps, interests, heights) {
this.path = path;
+ this.default_options = true; // do we still have default options ?
+ this.active = false; // should we have screen on
+ this.last_activity = getTime();
this.maps = maps;
this.interests = interests;
+ this.heights = heights;
+ this.screen = MAP;
let half_screen_width = g.getWidth() / 2;
let half_screen_height = g.getHeight() / 2;
let half_screen_diagonal = Math.sqrt(
@@ -642,6 +677,31 @@ class Status {
this.old_points = []; // record previous points but only when enough distance between them
this.old_times = []; // the corresponding times
}
+ activate() {
+ this.last_activity = getTime();
+ if (this.active) {
+ return;
+ } else {
+ this.active = true;
+ Bangle.setLCDBrightness(settings.brightness);
+ Bangle.setLocked(false);
+ if (settings.power_lcd_off) {
+ Bangle.setLCDPower(true);
+ }
+ }
+ }
+ check_activity() {
+ if (!this.active || !powersaving) {
+ return;
+ }
+ if (getTime() - this.last_activity > settings.active_time) {
+ this.active = false;
+ Bangle.setLCDBrightness(0);
+ if (settings.power_lcd_off) {
+ Bangle.setLCDPower(false);
+ }
+ }
+ }
invalidate_caches() {
for (let i = 0; i < this.maps.length; i++) {
this.maps[i].invalidate_caches();
@@ -666,6 +726,10 @@ class Status {
let distance_to_previous = previous_point.distance(position);
// gps signal is noisy but rarely above 5 meters
if (distance_to_previous < 5) {
+ // update instant speed and return
+ let oldest_point = this.old_points[0];
+ let distance_to_oldest = oldest_point.distance(position);
+ this.instant_speed = distance_to_oldest / (now - this.old_times[0]);
return null;
}
}
@@ -699,18 +763,46 @@ class Status {
let angle = Math.atan2(diff.lat, diff.lon);
return angle;
}
- update_position(new_position, maybe_direction, timestamp) {
+ update_position(new_position) {
let direction = this.new_position_reached(new_position);
if (direction === null) {
- if (maybe_direction === null) {
- return;
- } else {
- direction = maybe_direction;
+ if (this.old_points.length > 1) {
+ this.display(); // re-display because speed has changed
}
+ return;
}
if (in_menu) {
return;
}
+ if (this.instant_speed * 3.6 < settings.wake_up_speed) {
+ this.activate(); // if we go too slow turn on, we might be looking for the direction to follow
+ if (!this.default_options) {
+ this.default_options = true;
+
+ Bangle.setOptions({
+ lockTimeout: 0,
+ backlightTimeout: 10000,
+ wakeOnTwist: true,
+ powerSave: true,
+ });
+ }
+ } else {
+ if (this.default_options) {
+ this.default_options = false;
+
+ Bangle.setOptions({
+ lockTimeout: 0,
+ backlightTimeout: 0,
+ lcdPowerTimeout: 0,
+ hrmSportMode: 2,
+ wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds.
+ wakeOnFaceUp: false,
+ wakeOnTouch: true,
+ powerSave: false,
+ });
+ }
+ }
+ this.check_activity(); // if we don't move or are in menu we should stay on
this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0);
this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0);
@@ -727,16 +819,6 @@ class Status {
new_position.lat + sin_direction * this.instant_speed * 0.00001
);
- // abort if we are late
- // if (timestamp !== null) {
- // let elapsed = Date.now() - timestamp;
- // if (elapsed > 1000) {
- // console.log("we are late");
- // return;
- // }
- // console.log("we are not late");
- // }
-
if (this.path !== null) {
// detect segment we are on now
let res = this.path.nearest_segment(
@@ -775,6 +857,9 @@ class Status {
}
this.on_path = !lost;
}
+ if (!this.on_path) {
+ this.activate();
+ }
this.current_segment = next_segment;
@@ -795,23 +880,25 @@ class Status {
// }, time_to_next_point);
// }
// }
+ if (this.distance_to_next_point <= 100) {
+ this.activate();
+ }
if (this.reaching != next_point && this.distance_to_next_point <= 100) {
this.reaching = next_point;
let reaching_waypoint = this.path.is_waypoint(next_point);
if (reaching_waypoint) {
- Bangle.buzz();
- setTimeout(() => Bangle.buzz(), 500);
- setTimeout(() => Bangle.buzz(), 1000);
- setTimeout(() => Bangle.buzz(), 1500);
- if (Bangle.isLocked()) {
- Bangle.setLocked(false);
+ if (settings.buzz_on_turns) {
+ Bangle.buzz();
+ setTimeout(() => Bangle.buzz(), 500);
+ setTimeout(() => Bangle.buzz(), 1000);
+ setTimeout(() => Bangle.buzz(), 1500);
}
}
}
}
- // abort most frames if locked
- if (Bangle.isLocked() && this.gps_coordinates_counter % 5 != 0) {
+ // abort most frames if inactive
+ if (!this.active && this.gps_coordinates_counter % 5 != 0) {
return;
}
@@ -877,14 +964,12 @@ class Status {
]);
}
remaining_distance() {
- let remaining_in_correct_orientation =
- this.remaining_distances[this.current_segment + 1] +
- this.position.distance(this.path.point(this.current_segment + 1));
-
if (go_backwards) {
- return this.remaining_distances[0] - remaining_in_correct_orientation;
+ return this.remaining_distances[0] - this.remaining_distances[this.current_segment] +
+ this.position.distance(this.path.point(this.current_segment));
} else {
- return remaining_in_correct_orientation;
+ return this.remaining_distances[this.current_segment + 1] +
+ this.position.distance(this.path.point(this.current_segment + 1));
}
}
// check if we are lost (too far from segment we think we are on)
@@ -909,6 +994,186 @@ class Status {
}
displaying = true;
g.clear();
+ if (this.screen == MAP) {
+ this.display_map();
+ } else {
+ let current_position = 0;
+ if (this.current_segment !== null) {
+ if (go_backwards) {
+ current_position = this.remaining_distance();
+ } else {
+ current_position =
+ this.remaining_distances[0] - this.remaining_distance();
+ }
+ }
+ if (this.screen == HEIGHTS_FULL) {
+ this.display_heights(0, current_position, this.remaining_distances[0]);
+ } else {
+ // only display 2500m
+ let start;
+ if (go_backwards) {
+ start = Math.max(0, current_position - 2000);
+ } else {
+ start = Math.max(0, current_position - 500);
+ }
+ let length = Math.min(2500, this.remaining_distances[0] - start);
+ this.display_heights(start, current_position, length);
+ }
+ }
+ Bangle.drawWidgets();
+ displaying = false;
+ }
+ display_heights(display_start, current_position, displayed_length) {
+ let path_length = this.remaining_distances[0];
+ let widgets_height = 24;
+ let graph_width = g.getWidth();
+ let graph_height = g.getHeight() - 20 - widgets_height;
+
+ let distance_per_pixel = displayed_length / graph_width;
+
+ let start_point_index = 0;
+ let end_point_index = this.path.len - 1;
+ for (let i = 0; i < this.path.len; i++) {
+ let point_distance = path_length - this.remaining_distances[i];
+ if (point_distance <= display_start) {
+ start_point_index = i;
+ }
+ if (point_distance >= display_start + displayed_length) {
+ end_point_index = i;
+ break;
+ }
+ }
+ end_point_index = Math.min(end_point_index+1, this.path.len -1);
+ let max_height = Number.NEGATIVE_INFINITY;
+ let min_height = Number.POSITIVE_INFINITY;
+ for (let i = start_point_index; i <= end_point_index; i++) {
+ let height = this.heights[i];
+ max_height = Math.max(max_height, height);
+ min_height = Math.min(min_height, height);
+ }
+ // we'll set the displayed height to a minimum value of 100m
+ // if we don't, then we'll see too much noise
+ if (max_height - min_height < 100) {
+ min_height = min_height - 10;
+ max_height = min_height + 110;
+ }
+
+ let displayed_height = max_height - min_height;
+ let height_per_pixel = displayed_height / graph_height;
+ // g.setColor(0, 0, 0).drawRect(0, widgets_height, graph_width, graph_height + widgets_height);
+
+ let previous_x = null;
+ let previous_y = null;
+ let previous_height = null;
+ let previous_distance = null;
+ let current_x;
+ let current_y;
+ for (let i = start_point_index; i < end_point_index; i++) {
+ let point_distance = path_length - this.remaining_distances[i];
+ let height = this.heights[i];
+ let x = Math.round((point_distance - display_start) / distance_per_pixel);
+ if (go_backwards) {
+ x = graph_width - x;
+ }
+ let y =
+ widgets_height +
+ graph_height -
+ Math.round((height - min_height) / height_per_pixel);
+ if (x != previous_x) {
+ if (previous_x !== null) {
+ let steepness =
+ (height - previous_height) / (point_distance - previous_distance);
+ if (go_backwards) {
+ steepness *= -1;
+ }
+ let color;
+ if (steepness > 0.15) {
+ color = "#ff0000";
+ } else if (steepness > 0.8) {
+ color = "#ff8000";
+ } else if (steepness > 0.03) {
+ color = "#ffff00";
+ } else if (steepness > -0.03) {
+ color = "#00ff00";
+ } else if (steepness > -0.08) {
+ color = "#00aa44";
+ } else if (steepness > -0.015) {
+ color = "#0044aa";
+ } else {
+ color = "#0000ff";
+ }
+ g.setColor(color);
+ g.fillPoly([
+ previous_x,
+ previous_y,
+ x,
+ y,
+ x,
+ widgets_height + graph_height,
+ previous_x,
+ widgets_height + graph_height,
+ ]);
+ if (
+ current_position >= previous_distance &&
+ current_position < point_distance
+ ) {
+ let current_height =
+ previous_height +
+ ((current_position - previous_distance) /
+ (point_distance - previous_distance)) *
+ (height - previous_height);
+ current_x = Math.round(
+ (current_position - display_start) / distance_per_pixel
+ );
+ if (go_backwards) {
+ current_x = graph_width - current_x;
+ }
+ current_y =
+ widgets_height +
+ graph_height -
+ Math.round((current_height - min_height) / height_per_pixel);
+ }
+ }
+ previous_distance = point_distance;
+ previous_height = height;
+ previous_x = x;
+ previous_y = y;
+ }
+ }
+ if (this.on_path) {
+ g.setColor(0, 0, 0);
+ } else {
+ g.setColor(1, 0, 1);
+ }
+ g.fillCircle(current_x, current_y, 5);
+
+ // display min dist/max dist and min height/max height
+ g.setColor(g.theme.fg);
+ g.setFont("6x8:2");
+ g.setFontAlign(-1, 1, 0).drawString(
+ Math.ceil(display_start / 100) / 10,
+ 0,
+ g.getHeight()
+ );
+
+ g.setFontAlign(1, 1, 0).drawString(
+ Math.ceil((display_start + displayed_length) / 100) / 10,
+ g.getWidth(),
+ g.getHeight()
+ );
+
+ g.setFontAlign(1, 1, 0).drawString(
+ min_height,
+ g.getWidth(),
+ widgets_height + graph_height
+ );
+ g.setFontAlign(1, -1, 0).drawString(
+ max_height,
+ g.getWidth(),
+ widgets_height
+ );
+ }
+ display_map() {
let scale_factor = this.scale_factor;
if (!zoomed) {
scale_factor /= 2;
@@ -940,8 +1205,6 @@ class Status {
this.display_direction();
this.display_stats();
- Bangle.drawWidgets();
- displaying = false;
}
display_stats() {
let now = new Date();
@@ -962,20 +1225,18 @@ class Status {
if (this.old_times.length > 0) {
let point_time = this.old_times[this.old_times.length - 1];
let done_in = point_time - this.starting_time;
- approximate_speed = Math.round(
- (this.advanced_distance * 3.6) / done_in
- );
+ approximate_speed = Math.round((this.advanced_distance * 3.6) / done_in);
let approximate_instant_speed = Math.round(this.instant_speed * 3.6);
g.setFont("6x8:2")
.setFontAlign(-1, -1, 0)
+ .drawString("" + approximate_speed + "km/h", 0, g.getHeight() - 15);
+
+ g.setFont("6x8:3")
+ .setFontAlign(1, -1, 0)
.drawString(
- "" +
- approximate_speed +
- "km/h (in." +
- approximate_instant_speed +
- ")",
- 0,
- g.getHeight() - 15
+ "" + approximate_instant_speed,
+ g.getWidth(),
+ g.getHeight() - 22
);
}
@@ -1024,11 +1285,6 @@ class Status {
.drawString("turn", g.getWidth() - 50, 30);
}
}
- if (!this.on_path) {
- g.setColor(1.0, 0.0, 0.0)
- .setFont("6x15")
- .drawString("lost", g.getWidth() - 55, 35);
- }
}
display_path() {
// don't display all segments, only those neighbouring current segment
@@ -1077,7 +1333,7 @@ class Status {
let rotated_y = tx * sin + ty * cos;
let x = half_width - Math.round(rotated_x); // x is inverted
let y = half_height + Math.round(rotated_y);
- g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y);
+ g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y);
}
// display current-segment's projection
@@ -1086,14 +1342,26 @@ class Status {
}
// now display ourselves
- g.setColor(0, 0, 0);
+ if (this.on_path) {
+ g.setColor(0, 0, 0);
+ } else {
+ g.setColor(1, 0, 1);
+ }
g.fillCircle(half_width, half_height, 5);
}
}
function load_gps(filename) {
// let's display splash screen while loading file
+
+ let splashscreen = require("heatshrink").decompress(
+ atob(
+ "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"
+ )
+ );
+
g.clear();
+
g.drawImage(splashscreen, 0, 0);
g.setFont("6x8:2")
.setFontAlign(-1, -1, 0)
@@ -1106,6 +1374,7 @@ function load_gps(filename) {
let offset = 0;
let path = null;
+ let heights = null;
let maps = [];
let interests = null;
while (offset < file_size) {
@@ -1128,6 +1397,11 @@ function load_gps(filename) {
let res = new Interests(buffer, offset);
interests = res[0];
offset = res[1];
+ } else if (block_type == 4) {
+ console.log("loading heights");
+ let heights_number = path.points.length / 2;
+ heights = Int16Array(buffer, offset, heights_number);
+ offset += 2 * heights_number;
} else {
console.log("todo : block type", block_type);
}
@@ -1139,10 +1413,10 @@ function load_gps(filename) {
let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset;
E.showAlert(msg).then(function () {
E.showAlert();
- start_gipy(path, maps, interests);
+ start_gipy(path, maps, interests, heights);
});
} else {
- start_gipy(path, maps, interests);
+ start_gipy(path, maps, interests, heights);
}
}
@@ -1251,9 +1525,9 @@ class Point {
times(scalar) {
return new Point(this.lon * scalar, this.lat * scalar);
}
- dot(other_point) {
- return this.lon * other_point.lon + this.lat * other_point.lat;
- }
+ // dot(other_point) {
+ // return this.lon * other_point.lon + this.lat * other_point.lat;
+ // }
distance(other_point) {
//see https://www.movable-type.co.uk/scripts/latlong.html
const R = 6371e3; // metres
@@ -1311,47 +1585,12 @@ class Point {
}
let fake_gps_point = 0;
-function simulate_gps(status) {
- if (status.path === null) {
- let map = status.maps[0];
- let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]);
- let p2 = new Point(
- map.start_coordinates[0] + map.side * map.grid_size[0],
- map.start_coordinates[1] + map.side * map.grid_size[1]
- );
- let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point));
- if (fake_gps_point < 1) {
- fake_gps_point += 0.01;
- }
- status.update_position(pos, null, null);
- } else {
- if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) {
- return;
- }
- let point_index = Math.floor(fake_gps_point);
- if (point_index >= status.path.len / 2 - 1) {
- return;
- }
- let p1 = status.path.point(2 * point_index); // use these to approximately follow path
- let p2 = status.path.point(2 * (point_index + 1));
- //let p1 = status.path.point(point_index); // use these to strictly follow path
- //let p2 = status.path.point(point_index + 1);
-
- let alpha = fake_gps_point - point_index;
- let pos = p1.times(1 - alpha).plus(p2.times(alpha));
-
- if (go_backwards) {
- fake_gps_point -= 0.05; // advance simulation
- } else {
- fake_gps_point += 0.05; // advance simulation
- }
- status.update_position(pos, null, null);
- }
-}
function drawMenu() {
const menu = {
- "": { title: "choose trace" },
+ "": {
+ title: "choose trace",
+ },
};
var files = s.list(".gps");
for (var i = 0; i < files.length; ++i) {
@@ -1370,9 +1609,65 @@ function start(fn) {
load_gps(fn);
}
-function start_gipy(path, maps, interests) {
+function start_gipy(path, maps, interests, heights) {
console.log("starting");
- status = new Status(path, maps, interests);
+
+ if (!simulated && settings.disable_bluetooth) {
+ NRF.sleep(); // disable bluetooth completely
+ }
+
+ status = new Status(path, maps, interests, heights);
+
+ setWatch(
+ function () {
+ status.activate();
+ if (in_menu) {
+ return;
+ }
+ in_menu = true;
+ const menu = {
+ "": {
+ title: "choose action",
+ },
+ "Go Backward": {
+ value: go_backwards,
+ format: (v) => (v ? "On" : "Off"),
+ onchange: (v) => {
+ go_backwards = v;
+ },
+ },
+ Zoom: {
+ value: zoomed,
+ format: (v) => (v ? "In" : "Out"),
+ onchange: (v) => {
+ status.invalidate_caches();
+ zoomed = v;
+ },
+ },
+ /*LANG*/
+ powersaving: {
+ value: powersaving,
+ onchange: (v) => {
+ powersaving = v;
+ },
+ },
+ "back to map": function () {
+ in_menu = false;
+ E.showMenu();
+ g.clear();
+ g.flip();
+ if (status !== null) {
+ status.display();
+ }
+ },
+ };
+ E.showMenu(menu);
+ },
+ BTN1,
+ {
+ repeat: true,
+ }
+ );
if (status.path !== null) {
let start = status.path.point(0);
@@ -1381,13 +1676,26 @@ function start_gipy(path, maps, interests) {
let first_map = maps[0];
status.displayed_position = new Point(
first_map.start_coordinates[0] +
- (first_map.side * first_map.grid_size[0]) / 2,
+ (first_map.side * first_map.grid_size[0]) / 2,
first_map.start_coordinates[1] +
- (first_map.side * first_map.grid_size[1]) / 2);
+ (first_map.side * first_map.grid_size[1]) / 2
+ );
}
status.display();
+ Bangle.on("touch", () => {
+ status.activate();
+ if (in_menu) {
+ return;
+ }
+ if (status.heights !== null) {
+ status.screen = (status.screen + 1) % 3;
+ status.display();
+ }
+ });
+
Bangle.on("stroke", (o) => {
+ status.activate();
if (in_menu) {
return;
}
@@ -1403,8 +1711,8 @@ function start_gipy(path, maps, interests) {
let s = status.adjusted_sin_direction;
let rotated_x = xdiff * c - ydiff * s;
let rotated_y = xdiff * s + ydiff * c;
- status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor;
- status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor;
+ status.displayed_position.lon += (1.3 * rotated_x) / status.scale_factor;
+ status.displayed_position.lat -= (1.3 * rotated_y) / status.scale_factor;
status.display();
});
@@ -1413,9 +1721,49 @@ function start_gipy(path, maps, interests) {
// let's keep the screen on in simulations
Bangle.setLCDTimeout(0);
Bangle.setLCDPower(1);
+ Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen
+
+ function simulate_gps(status) {
+ if (status.path === null) {
+ let map = status.maps[0];
+ let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]);
+ let p2 = new Point(
+ map.start_coordinates[0] + map.side * map.grid_size[0],
+ map.start_coordinates[1] + map.side * map.grid_size[1]
+ );
+ let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point));
+ if (fake_gps_point < 1) {
+ fake_gps_point += 0.05;
+ }
+ status.update_position(pos);
+ } else {
+ if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) {
+ return;
+ }
+ let point_index = Math.floor(fake_gps_point);
+ if (point_index >= status.path.len / 2 - 1) {
+ return;
+ }
+ let p1 = status.path.point(2 * point_index); // use these to approximately follow path
+ let p2 = status.path.point(2 * (point_index + 1));
+ //let p1 = status.path.point(point_index); // use these to strictly follow path
+ //let p2 = status.path.point(point_index + 1);
+
+ let alpha = fake_gps_point - point_index;
+ let pos = p1.times(1 - alpha).plus(p2.times(alpha));
+
+ if (go_backwards) {
+ fake_gps_point -= 0.2; // advance simulation
+ } else {
+ fake_gps_point += 0.2; // advance simulation
+ }
+ status.update_position(pos);
+ }
+ }
+
setInterval(simulate_gps, 500, status);
} else {
- Bangle.setLocked(false);
+ status.activate();
let frame = 0;
let set_coordinates = function (data) {
@@ -1428,9 +1776,9 @@ function start_gipy(path, maps, interests) {
if (valid_coordinates) {
if (status.starting_time === null) {
status.starting_time = getTime();
- Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen
+ Bangle.loadWidgets(); // load them even in simulation to eat mem
}
- status.update_position(new Point(data.lon, data.lat), null, data.time);
+ status.update_position(new Point(data.lon, data.lat));
}
let gps_status_color;
if (frame % 2 == 0 || valid_coordinates) {
@@ -1447,53 +1795,9 @@ function start_gipy(path, maps, interests) {
Bangle.setGPSPower(true, "gipy");
Bangle.on("GPS", set_coordinates);
- Bangle.on("lock", function (on) {
- if (!on) {
- Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking
- }
- });
}
}
-setWatch(
- function () {
- if (in_menu) {
- return;
- }
- in_menu = true;
- const menu = {
- "": { title: "choose action" },
- "Go Backward": {
- value: go_backwards,
- format: (v) => (v ? "On" : "Off"),
- onchange: (v) => {
- go_backwards = v;
- },
- },
- Zoom: {
- value: zoomed,
- format: (v) => (v ? "In" : "Out"),
- onchange: (v) => {
- status.invalidate_caches();
- zoomed = v;
- },
- },
- "back to map": function () {
- in_menu = false;
- E.showMenu();
- g.clear();
- g.flip();
- if (status !== null) {
- status.display();
- }
- },
- };
- E.showMenu(menu);
- },
- BTN1,
- { repeat: true }
-);
-
let files = s.list(".gps");
if (files.length <= 1) {
if (files.length == 0) {
diff --git a/apps/gipy/heights.png b/apps/gipy/heights.png
new file mode 100644
index 000000000..07f82511b
Binary files /dev/null and b/apps/gipy/heights.png differ
diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json
index 8b8c88780..d6b5e1405 100644
--- a/apps/gipy/metadata.json
+++ b/apps/gipy/metadata.json
@@ -2,13 +2,13 @@
"id": "gipy",
"name": "Gipy",
"shortName": "Gipy",
- "version": "0.19",
+ "version": "0.21",
"description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.",
"allow_emulator":false,
"icon": "gipy.png",
"type": "app",
"tags": "tool,outdoors,gps",
- "screenshots": [{"url":"splash.png"}],
+ "screenshots": [{"url":"splash.png"}, {"url":"heights.png"}, {"url":"shot.png"}],
"supports": ["BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts
index 15a90b1e8..e4644f74f 100644
--- a/apps/gipy/pkg/gps.d.ts
+++ b/apps/gipy/pkg/gps.d.ts
@@ -12,6 +12,11 @@ export function get_gps_map_svg(gps: Gps): string;
export function get_polygon(gps: Gps): Float64Array;
/**
* @param {Gps} gps
+* @returns {boolean}
+*/
+export function has_heights(gps: Gps): boolean;
+/**
+* @param {Gps} gps
* @returns {Float64Array}
*/
export function get_polyline(gps: Gps): Float64Array;
@@ -59,6 +64,7 @@ export interface InitOutput {
readonly __wbg_gps_free: (a: number) => void;
readonly get_gps_map_svg: (a: number, b: number) => void;
readonly get_polygon: (a: number, b: number) => void;
+ readonly has_heights: (a: number) => number;
readonly get_polyline: (a: number, b: number) => void;
readonly get_gps_content: (a: number, b: number) => void;
readonly request_map: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => number;
@@ -67,11 +73,11 @@ export interface InitOutput {
readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table;
- readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a: (a: number, b: number, c: number) => void;
+ readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;
- readonly wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b: (a: number, b: number, c: number, d: number) => void;
+ readonly wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027: (a: number, b: number, c: number, d: number) => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js
index ce9ebe5f8..563bf6251 100644
--- a/apps/gipy/pkg/gps.js
+++ b/apps/gipy/pkg/gps.js
@@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_24(arg0, arg1, arg2) {
- wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(arg0, arg1, addHeapObject(arg2));
+ wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(arg0, arg1, addHeapObject(arg2));
}
function _assertClass(instance, klass) {
@@ -263,6 +263,16 @@ export function get_polygon(gps) {
}
}
+/**
+* @param {Gps} gps
+* @returns {boolean}
+*/
+export function has_heights(gps) {
+ _assertClass(gps, Gps);
+ const ret = wasm.has_heights(gps.ptr);
+ return ret !== 0;
+}
+
/**
* @param {Gps} gps
* @returns {Float64Array}
@@ -368,8 +378,8 @@ function handleError(f, args) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
-function __wbg_adapter_84(arg0, arg1, arg2, arg3) {
- wasm.wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
+function __wbg_adapter_85(arg0, arg1, arg2, arg3) {
+ wasm.wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
}
/**
@@ -430,9 +440,6 @@ async function load(module, imports) {
function getImports() {
const imports = {};
imports.wbg = {};
- imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) {
- console.log(getStringFromWasm0(arg0, arg1));
- };
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
@@ -441,6 +448,9 @@ function getImports() {
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
+ imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) {
+ console.log(getStringFromWasm0(arg0, arg1));
+ };
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
@@ -460,6 +470,21 @@ function getImports() {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
+ imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
+ const ret = getObject(arg0).signal;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
+ const ret = new AbortController();
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
+ getObject(arg0).abort();
+ };
imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () {
const ret = new Headers();
return addHeapObject(ret);
@@ -496,21 +521,6 @@ function getImports() {
const ret = getObject(arg0).text();
return addHeapObject(ret);
}, arguments) };
- imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
- const ret = getObject(arg0).signal;
- return addHeapObject(ret);
- };
- imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
- const ret = new AbortController();
- return addHeapObject(ret);
- }, arguments) };
- imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
- getObject(arg0).abort();
- };
- imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
- const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
- return addHeapObject(ret);
- }, arguments) };
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error();
return addHeapObject(ret);
@@ -610,7 +620,7 @@ function getImports() {
const a = state0.a;
state0.a = 0;
try {
- return __wbg_adapter_84(a, state0.b, arg0, arg1);
+ return __wbg_adapter_85(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
@@ -675,8 +685,8 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
- imports.wbg.__wbindgen_closure_wrapper2298 = function(arg0, arg1, arg2) {
- const ret = makeMutClosure(arg0, arg1, 260, __wbg_adapter_24);
+ imports.wbg.__wbindgen_closure_wrapper2214 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 268, __wbg_adapter_24);
return addHeapObject(ret);
};
diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm
index 6999cb946..7a42fb564 100644
Binary files a/apps/gipy/pkg/gps_bg.wasm and b/apps/gipy/pkg/gps_bg.wasm differ
diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts
index df9a024fa..3b95ada78 100644
--- a/apps/gipy/pkg/gps_bg.wasm.d.ts
+++ b/apps/gipy/pkg/gps_bg.wasm.d.ts
@@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory;
export function __wbg_gps_free(a: number): void;
export function get_gps_map_svg(a: number, b: number): void;
export function get_polygon(a: number, b: number): void;
+export function has_heights(a: number): number;
export function get_polyline(a: number, b: number): void;
export function get_gps_content(a: number, b: number): void;
export function request_map(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number): number;
@@ -12,8 +13,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number): number;
export const __wbindgen_export_2: WebAssembly.Table;
-export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(a: number, b: number, c: number): void;
+export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_exn_store(a: number): void;
-export function wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(a: number, b: number, c: number, d: number): void;
+export function wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(a: number, b: number, c: number, d: number): void;
diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js
index 1f6ae0853..1b030f5cd 100644
--- a/apps/gipy/settings.js
+++ b/apps/gipy/settings.js
@@ -1,29 +1,86 @@
-(function (back) {
- var FILE = "gipy.json";
- // Load settings
- var settings = Object.assign(
- {
- lost_distance: 50,
- },
- require("Storage").readJSON(FILE, true) || {}
- );
+(function(back) {
+ var FILE = "gipy.json";
+ // Load settings
+ var settings = Object.assign({
+ lost_distance: 50,
+ wake_up_speed: 13,
+ active_time: 10,
+ buzz_on_turns: false,
+ disable_bluetooth: true,
+ brightness: 0.5,
+ power_lcd_off: false,
+ },
+ require("Storage").readJSON(FILE, true) || {}
+ );
- function writeSettings() {
- require("Storage").writeJSON(FILE, settings);
- }
+ function writeSettings() {
+ require("Storage").writeJSON(FILE, settings);
+ }
- // Show the menu
- E.showMenu({
- "": { title: "Gipy" },
- "< Back": () => back(),
- "lost distance": {
- value: 50 | settings.lost_distance, // 0| converts undefined to 0
- min: 10,
- max: 500,
- onchange: (v) => {
- settings.max_speed = v;
- writeSettings();
- },
- },
- });
+ // Show the menu
+ E.showMenu({
+ "": {
+ title: "Gipy"
+ },
+ "< Back": () => back(),
+ /*LANG*/"buzz on turns": {
+ value: settings.buzz_on_turns == true,
+ onchange: (v) => {
+ settings.buzz_on_turns = v;
+ writeSettings();
+ }
+ },
+ /*LANG*/"disable bluetooth": {
+ value: settings.disable_bluetooth == true,
+ onchange: (v) => {
+ settings.disable_bluetooth = v;
+ writeSettings();
+ }
+ },
+ "lost distance": {
+ value: settings.lost_distance,
+ min: 10,
+ max: 500,
+ onchange: (v) => {
+ settings.lost_distance = v;
+ writeSettings();
+ },
+ },
+ "wake-up speed": {
+ value: settings.wake_up_speed,
+ min: 0,
+ max: 30,
+ onchange: (v) => {
+ settings.wake_up_speed = v;
+ writeSettings();
+ },
+ },
+ "active time": {
+ value: settings.active_time,
+ min: 5,
+ max: 60,
+ onchange: (v) => {
+ settings.active_time = v;
+ writeSettings();
+ },
+ },
+ "brightness": {
+ value: settings.brightness,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ onchange: (v) => {
+ settings.brightness = v;
+ writeSettings();
+ },
+ },
+
+ /*LANG*/"power lcd off": {
+ value: settings.power_lcd_off == true,
+ onchange: (v) => {
+ settings.power_lcd_off = v;
+ writeSettings();
+ }
+ },
+ });
});
diff --git a/apps/gipy/shot.png b/apps/gipy/shot.png
new file mode 100644
index 000000000..c2ffea724
Binary files /dev/null and b/apps/gipy/shot.png differ
diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog
index 0d7c06ab4..921000e82 100644
--- a/apps/gpstrek/ChangeLog
+++ b/apps/gpstrek/ChangeLog
@@ -15,4 +15,7 @@
Save state if route or waypoint has been chosen
0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled
0.10: Adds map view of loaded route
- Automatically search for new waypoint if moving away from current target
\ No newline at end of file
+ Automatically search for new waypoint if moving away from current target
+0.11: Adds configuration
+ Draws direction arrows on route
+ Turn of compass when GPS fix is available
\ No newline at end of file
diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js
index 95db86aaf..eb21498c0 100644
--- a/apps/gpstrek/app.js
+++ b/apps/gpstrek/app.js
@@ -7,25 +7,12 @@ const MODE_SLICES = 2;
const STORAGE = require("Storage");
const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
-const SETTINGS = {
- mapCompass: true,
- mapScale:0.2, //initial value
- mapRefresh:1000, //minimum time in ms between refreshs of the map
- mapChunkSize: 5, //render this many waypoints at a time
- overviewScroll: 30, //scroll this amount on swipe in pixels
- overviewScale: 0.02, //initial value
- refresh:500, //general refresh interval in ms
- refreshLocked:3000, //general refresh interval when Bangle is locked
- cacheMinFreeMem:2000,
- cacheMaxEntries:0,
- minCourseChange: 5, //course change needed in degrees before redrawing the map
- minPosChange: 5, //position change needed in pixels before redrawing the map
- waypointChangeDist: 50, //distance in m to next waypoint before advancing automatically
- queueWaitingTime: 5, // waiting time during processing of task queue items when running with timeouts
- autosearch: true,
- maxDistForAutosearch: 300,
- autosearchLimit: 3
-};
+
+
+const SETTINGS = Object.assign(
+ require('Storage').readJSON("gpstrek.default.json", true) || {},
+ require('Storage').readJSON("gpstrek.json", true) || {}
+);
let init = function(){
global.screen = 1;
@@ -38,7 +25,6 @@ let init = function(){
Bangle.loadWidgets();
WIDGETS.gpstrek.start(false);
- if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 2;
if (!WIDGETS.gpstrek.getState().mode) WIDGETS.gpstrek.getState().mode = MODE_MENU;
};
@@ -184,11 +170,6 @@ let getDoubleLineSlice = function(title1,title2,provider1,provider2){
};
};
-const dot = Graphics.createImage(`
-XX
-XX
-`);
-
const arrow = Graphics.createImage(`
X
XXX
@@ -198,6 +179,14 @@ const arrow = Graphics.createImage(`
XXX XXX
`);
+const thinarrow = Graphics.createImage(`
+ X
+ XXX
+ XX XX
+ XX XX
+XX XX
+`);
+
const cross = Graphics.createImage(`
XX XX
XX XX
@@ -459,7 +448,7 @@ let getMapSlice = function(){
if (!isMapOverview){
drawCurrentPos();
}
- if (!isMapOverview && renderInTimeouts){
+ if (SETTINGS.mapCompass && !isMapOverview && renderInTimeouts){
drawMapCompass();
}
if (renderInTimeouts) drawInterface();
@@ -472,7 +461,8 @@ let getMapSlice = function(){
i:startingIndex,
poly:[],
maxWaypoints: maxWaypoints,
- breakLoop: false
+ breakLoop: false,
+ dist: 0
};
let drawChunk = function(data){
@@ -483,6 +473,7 @@ let getMapSlice = function(){
let last;
let toDraw;
let named = [];
+ let dir = [];
for (let j = 0; j < SETTINGS.mapChunkSize; j++){
data.i = data.i + (reverse?-1:1);
let p = get(route, data.i);
@@ -497,7 +488,17 @@ let getMapSlice = function(){
break;
}
toDraw = Bangle.project(p);
- if (p.name) named.push({i:data.poly.length,n:p.name});
+
+ if (SETTINGS.mapDirection){
+ let lastWp = get(route, data.i - (reverse?-1:1));
+ if (lastWp) data.dist+=distance(lastWp,p);
+ if (!isMapOverview && data.dist > 20/mapScale){
+ dir.push({i:data.poly.length,b:require("graphics_utils").degreesToRadians(bearing(lastWp,p)-(reverse?0:180))});
+ data.dist=0;
+ }
+ }
+ if (p.name)
+ named.push({i:data.poly.length,n:p.name});
data.poly.push(startingPoint.x-toDraw.x);
data.poly.push((startingPoint.y-toDraw.y)*-1);
}
@@ -518,7 +519,11 @@ let getMapSlice = function(){
}
graphics.drawString(c.n, data.poly[c.i] + 10, data.poly[c.i+1]);
}
-
+
+ for (let c of dir){
+ graphics.drawImage(thinarrow, data.poly[c.i], data.poly[c.i+1], {rotate: c.b});
+ }
+
if (finish)
graphics.drawImage(finishIcon, data.poly[data.poly.length - 2] -5, data.poly[data.poly.length - 1] - 4);
else if (last) {
@@ -1254,11 +1259,6 @@ let showMenu = function(){
"Background" : showBackgroundMenu,
"Calibration": showCalibrationMenu,
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});},
- "Info rows" : {
- value : WIDGETS.gpstrek.getState().numberOfSlices,
- min:1,max:6,step:1,
- onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
- },
};
E.showMenu(mainmenu);
@@ -1374,7 +1374,7 @@ const finishData = {
};
let getSliceHeight = function(number){
- return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices);
+ return Math.floor(Bangle.appRect.h/SETTINGS.numberOfSlices);
};
let compassSlice = getCompassSlice();
@@ -1455,7 +1455,6 @@ let updateRouting = function() {
lastSearch = Date.now();
autosearchCounter++;
}
- let counter = 0;
while (hasNext(s.route) && distance(s.currentPos,get(s.route)) < SETTINGS.waypointChangeDist) {
next(s.route);
minimumDistance = Number.MAX_VALUE;
@@ -1479,7 +1478,7 @@ let updateSlices = function(){
slices.push(healthSlice);
slices.push(systemSlice);
slices.push(system2Slice);
- maxSlicePages = Math.ceil(slices.length/s.numberOfSlices);
+ maxSlicePages = Math.ceil(slices.length/SETTINGS.numberOfSlices);
};
let page_slices = 0;
@@ -1515,9 +1514,9 @@ let drawSlices = function(){
if (force){
clear();
}
- let firstSlice = page_slices*s.numberOfSlices;
+ let firstSlice = page_slices*SETTINGS.numberOfSlices;
let sliceHeight = getSliceHeight();
- let slicesToDraw = slices.slice(firstSlice,firstSlice + s.numberOfSlices);
+ let slicesToDraw = slices.slice(firstSlice,firstSlice + SETTINGS.numberOfSlices);
for (let slice of slicesToDraw) {
g.reset();
if (!slice.refresh || slice.refresh() || force)
diff --git a/apps/gpstrek/default.json b/apps/gpstrek/default.json
new file mode 100644
index 000000000..aa8d5ecb1
--- /dev/null
+++ b/apps/gpstrek/default.json
@@ -0,0 +1,21 @@
+{
+ "mapCompass": true,
+ "mapScale":0.5,
+ "mapRefresh":1000,
+ "mapChunkSize": 15,
+ "mapDirection": true,
+ "overviewScroll": 30,
+ "overviewScale": 0.02,
+ "refresh":500,
+ "refreshLocked":3000,
+ "cacheMinFreeMem":2000,
+ "cacheMaxEntries":0,
+ "minCourseChange": 5,
+ "minPosChange": 5,
+ "waypointChangeDist": 50,
+ "queueWaitingTime": 5,
+ "autosearch": true,
+ "maxDistForAutosearch": 300,
+ "autosearchLimit": 3,
+ "numberOfSlices": 3
+}
diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json
index 0ec3a8bfe..ec953e6e7 100644
--- a/apps/gpstrek/metadata.json
+++ b/apps/gpstrek/metadata.json
@@ -1,7 +1,7 @@
{
"id": "gpstrek",
"name": "GPS Trekking",
- "version": "0.10",
+ "version": "0.11",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png",
"screenshots": [{"url":"screenInit.png"},{"url":"screenMenu.png"},{"url":"screenMap.png"},{"url":"screenLost.png"},{"url":"screenOverview.png"},{"url":"screenOverviewScroll.png"},{"url":"screenSlices.png"},{"url":"screenSlices2.png"},{"url":"screenSlices3.png"}],
@@ -12,8 +12,13 @@
"interface" : "interface.html",
"storage": [
{"name":"gpstrek.app.js","url":"app.js"},
+ {"name":"gpstrek.settings.js","url":"settings.js"},
+ {"name":"gpstrek.default.json","url":"default.json"},
{"name":"gpstrek.wid.js","url":"widget.js"},
{"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
],
- "data": [{"name":"gpstrek.state.json"}]
+ "data": [
+ {"name":"gpstrek.state.json"},
+ {"name":"gpstrek.json"}
+ ]
}
diff --git a/apps/gpstrek/settings.js b/apps/gpstrek/settings.js
new file mode 100644
index 000000000..1510bcba4
--- /dev/null
+++ b/apps/gpstrek/settings.js
@@ -0,0 +1,162 @@
+(function(back) {
+ const FILE="gpstrek.json";
+ let settings;
+
+ function writeSettings(key, value) {
+ var s = require('Storage').readJSON(FILE, true) || {};
+ s[key] = value;
+ require('Storage').writeJSON(FILE, s);
+ readSettings();
+ }
+
+ function readSettings(){
+ settings = Object.assign(
+ require('Storage').readJSON("gpstrek.default.json", true) || {},
+ require('Storage').readJSON(FILE, true) || {}
+ );
+ }
+
+
+ function showMapMenu(){
+ var menu = {
+ '': { 'title': 'Map', back: showMainMenu },
+ 'Show compass on map': {
+ value: !!settings.mapCompass,
+ onchange: v => {
+ writeSettings("mapCompass",v);
+ },
+ },
+ 'Initial map scale': {
+ value: settings.mapScale,
+ min: 0.01,max: 2, step:0.01,
+ onchange: v => {
+ writeSettings("mapScale",v);
+ },
+ },
+ 'Rendered waypoints': {
+ value: settings.mapChunkSize,
+ min: 5,max: 60, step:5,
+ onchange: v => {
+ writeSettings("mapChunkSize",v);
+ }
+ },
+ 'Overview scroll': {
+ value: settings.overviewScroll,
+ min: 10,max: 100, step:10,
+ format: v => v + "px",
+ onchange: v => {
+ writeSettings("overviewScroll",v);
+ }
+ },
+ 'Initial overview scale': {
+ value: settings.overviewScale,
+ min: 0.005,max: 0.1, step:0.005,
+ onchange: v => {
+ writeSettings("overviewScale",v);
+ }
+ },
+ 'Show direction': {
+ value: !!settings.mapDirection,
+ onchange: v => {
+ writeSettings("mapDirection",v);
+ }
+ }
+ };
+ E.showMenu(menu);
+ }
+
+ function showRoutingMenu(){
+ var menu = {
+ '': { 'title': 'Routing', back: showMainMenu },
+ 'Auto search closest waypoint': {
+ value: !!settings.autosearch,
+ onchange: v => {
+ writeSettings("autosearch",v);
+ },
+ },
+ 'Auto search limit': {
+ value: settings.autosearchLimit,
+ onchange: v => {
+ writeSettings("autosearchLimit",v);
+ },
+ },
+ 'Waypoint change distance': {
+ value: settings.waypointChangeDist,
+ format: v => v + "m",
+ min: 5,max: 200, step:5,
+ onchange: v => {
+ writeSettings("waypointChangeDist",v);
+ },
+ }
+ };
+ E.showMenu(menu);
+ }
+
+ function showRefreshMenu(){
+ var menu = {
+ '': { 'title': 'Refresh', back: showMainMenu },
+ 'Unlocked refresh': {
+ value: settings.refresh,
+ format: v => v + "ms",
+ min: 250,max: 5000, step:250,
+ onchange: v => {
+ writeSettings("refresh",v);
+ }
+ },
+ 'Locked refresh': {
+ value: settings.refreshLocked,
+ min: 1000,max: 60000, step:1000,
+ format: v => v + "ms",
+ onchange: v => {
+ writeSettings("refreshLocked",v);
+ }
+ },
+ 'Minimum refresh': {
+ value: settings.mapRefresh,
+ format: v => v + "ms",
+ min: 250,max: 5000, step:250,
+ onchange: v => {
+ writeSettings("mapRefresh",v);
+ }
+ },
+ 'Minimum course change': {
+ value: settings.minCourseChange,
+ min: 0,max: 180, step:1,
+ format: v => v + "°",
+ onchange: v => {
+ writeSettings("minCourseChange",v);
+ }
+ },
+ 'Minimum position change': {
+ value: settings.minPosChange,
+ min: 0,max: 50, step:1,
+ format: v => v + "px",
+ onchange: v => {
+ writeSettings("minPosChange",v);
+ }
+ }
+ };
+ E.showMenu(menu);
+ }
+
+
+ function showMainMenu(){
+ var mainmenu = {
+ '': { 'title': 'GPS Trekking', back: back },
+ 'Map': showMapMenu,
+ 'Routing': showRoutingMenu,
+ 'Refresh': showRefreshMenu,
+ "Info rows" : {
+ value : settings.numberOfSlices,
+ min:1,max:6,step:1,
+ onchange : v => {
+ writeSettings("numberOfSlices",v);
+ }
+ },
+ };
+ E.showMenu(mainmenu);
+ }
+
+ readSettings();
+ showMainMenu();
+})
\ No newline at end of file
diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js
index 6887486bc..44acdc722 100644
--- a/apps/gpstrek/widget.js
+++ b/apps/gpstrek/widget.js
@@ -45,7 +45,15 @@ function onPulse(e){
}
function onGPS(fix) {
- if(fix.fix) state.currentPos = fix;
+ if(fix.fix) {
+ state.currentPos = fix;
+ if (Bangle.isCompassOn()){
+ Bangle.setCompassPower(0, "gpstrek");
+ state.compassSamples = new Array(SAMPLES).fill(0)
+ }
+ } else {
+ Bangle.setCompassPower(1, "gpstrek");
+ }
}
let radians = function(a) {
diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog
index 12740959a..02b53c56d 100644
--- a/apps/health/ChangeLog
+++ b/apps/health/ChangeLog
@@ -22,3 +22,10 @@
0.21: Update boot.min.js.
0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435)
0.23: Fix HRM logic
+0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written)
+0.25: lib.read* methods now return correctly scaled movement
+ movement graph in app is now an average, not sum
+ fix 11pm slot for daily HRM
+0.26: Implement API for activity fetching
+0.27: Fix typo in daily summary graph code causing graph not to load
+ Fix daily summaries for 31st of the month
diff --git a/apps/health/app.js b/apps/health/app.js
index bd708207b..db21d9243 100644
--- a/apps/health/app.js
+++ b/apps/health/app.js
@@ -48,7 +48,7 @@ function stepsPerHour() {
function stepsPerDay() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "stepsPerDay";
- var data = new Uint16Array(31);
+ var data = new Uint16Array(32);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
setButton(menuStepCount);
barChart(/*LANG*/"DAY", data);
@@ -59,7 +59,7 @@ function hrmPerHour() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "hrmPerHour";
var data = new Uint16Array(24);
- var cnt = new Uint8Array(23);
+ var cnt = new Uint8Array(24);
require("health").readDay(new Date(), h=>{
data[h.hr]+=h.bpm;
if (h.bpm) cnt[h.hr]++;
@@ -72,8 +72,8 @@ function hrmPerHour() {
function hrmPerDay() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "hrmPerDay";
- var data = new Uint16Array(31);
- var cnt = new Uint8Array(31);
+ var data = new Uint16Array(32);
+ var cnt = new Uint8Array(32);
require("health").readDailySummaries(new Date(), h=>{
data[h.day]+=h.bpm;
if (h.bpm) cnt[h.day]++;
@@ -87,7 +87,12 @@ function movementPerHour() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "movementPerHour";
var data = new Uint16Array(24);
- require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
+ var cnt = new Uint8Array(24);
+ require("health").readDay(new Date(), h=>{
+ data[h.hr]+=h.movement;
+ cnt[h.hr]++;
+ });
+ data.forEach((d,i)=>data[i] = d/cnt[i]);
setButton(menuMovement);
barChart(/*LANG*/"HOUR", data);
}
@@ -95,8 +100,13 @@ function movementPerHour() {
function movementPerDay() {
E.showMessage(/*LANG*/"Loading...");
current_selection = "movementPerDay";
- var data = new Uint16Array(31);
- require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
+ var data = new Uint16Array(32);
+ var cnt = new Uint8Array(32);
+ require("health").readDailySummaries(new Date(), h=>{
+ data[h.day]+=h.movement;
+ cnt[h.day]++;
+ });
+ data.forEach((d,i)=>data[i] = d/cnt[i]);
setButton(menuMovement);
barChart(/*LANG*/"DAY", data);
}
diff --git a/apps/health/boot.js b/apps/health/boot.js
index 62e8b87ab..66b4acda6 100644
--- a/apps/health/boot.js
+++ b/apps/health/boot.js
@@ -52,7 +52,7 @@ Bangle.on("health", health => {
return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps
health.bpm, // 8 bit bpm
- Math.min(health.movement / 8, 255)); // movement
+ Math.min(health.movement, 255)); // movement
}
var rec = getRecordIdx(d);
@@ -68,6 +68,12 @@ Bangle.on("health", health => {
require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header
}
var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN);
+
+ // scale down reported movement value in order to fit it within a
+ // uint8 DB field
+ health = Object.assign({}, health);
+ health.movement /= 8;
+
require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN);
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
// we're at the end of the day. Read in all of the data for the day and sum it up
@@ -82,10 +88,10 @@ Bangle.on("health", health => {
var dt = f.substr(recordPos, DB_RECORD_LEN);
if (dt!="\xFF\xFF\xFF\xFF") {
health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1);
- health.movement += dt.charCodeAt(2);
- health.movCnt++;
var bpm = dt.charCodeAt(2);
health.bpm += bpm;
+ health.movement += dt.charCodeAt(3);
+ health.movCnt++;
if (bpm) health.bpmCnt++;
}
recordPos -= DB_RECORD_LEN;
diff --git a/apps/health/boot.min.js b/apps/health/boot.min.js
index 651231195..0d1a80f4c 100644
--- a/apps/health/boot.min.js
+++ b/apps/health/boot.min.js
@@ -1,5 +1,5 @@
-function l(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateMath.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,
-"health")});90>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0k;k++)e=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=e&&(a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1),a.movement+=e.charCodeAt(2),
-a.movCnt++,e=e.charCodeAt(2),a.bpm+=e,e&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
+function m(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateBangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",d);Bangle.on("HRM",b=>{90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement,255))}var b=new Date(Date.now()-59E4);a&&0k;k++){e=g.substr(h,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var l=e.charCodeAt(2);a.bpm+=l;a.movement+=e.charCodeAt(3);a.movCnt++;
+l&&a.bpmCnt++}h-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
\ No newline at end of file
diff --git a/apps/health/lib.js b/apps/health/lib.js
index 3a52ad59f..8f3cc800e 100644
--- a/apps/health/lib.js
+++ b/apps/health/lib.js
@@ -29,7 +29,7 @@ exports.readAllRecords = function(d, cb) {
day:day+1, hr : hr, min:m*10,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
- movement : h.charCodeAt(3)
+ movement : h.charCodeAt(3)*8
});
}
idx += DB_RECORD_LEN;
@@ -37,7 +37,36 @@ exports.readAllRecords = function(d, cb) {
}
idx += DB_RECORD_LEN; // +1 because we have an extra record with totals for the end of the day
}
-}
+};
+
+// Read the entire database. There is no guarantee that the months are read in order.
+exports.readFullDatabase = function(cb) {
+ require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => {
+ console.log(val);
+ var parts = val.split('-');
+ var y = parseInt(parts[1]);
+ var mo = parseInt(parts[2].replace('.raw', ''));
+
+ exports.readAllRecords(new Date(y, mo, 1), (r) => {
+ r.date = new Date(y, mo, r.day, r.hr, r.min);
+ cb(r);
+ });
+ });
+};
+
+// Read all records per day, until the current time.
+// There may be some records for the day of the timestamp previous to the timestamp
+exports.readAllRecordsSince = function(d, cb) {
+ var currentDate = new Date().getTime();
+ var di = d;
+ while (di.getTime() <= currentDate) {
+ exports.readDay(di, (r) => {
+ r.date = new Date(di.getFullYear(), di.getMonth(), di.getDate(), r.hr, r.min);
+ cb(r);
+ });
+ di.setDate(di.getDate() + 1);
+ }
+};
// Read daily summaries from the given month
exports.readDailySummaries = function(d, cb) {
@@ -53,7 +82,7 @@ exports.readDailySummaries = function(d, cb) {
day:day+1,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
- movement : h.charCodeAt(3)
+ movement : h.charCodeAt(3)*8
});
}
idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN;
@@ -75,7 +104,7 @@ exports.readDay = function(d, cb) {
hr : hr, min:m*10,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
- movement : h.charCodeAt(3)
+ movement : h.charCodeAt(3)*8
});
}
idx += DB_RECORD_LEN;
diff --git a/apps/health/lib.min.js b/apps/health/lib.min.js
index 4bdc4c0fb..e9dfad336 100644
--- a/apps/health/lib.min.js
+++ b/apps/health/lib.min.js
@@ -1,3 +1,4 @@
-function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:g.charCodeAt(3)});c+=
-4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\u00ff\u00ff\u00ff\u00ff"!=e&&f({hr:d,
-min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:e.charCodeAt(3)});a+=4}}}
\ No newline at end of file
+function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,e){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=8,b=0;31>b;b++){for(var c=0;24>c;c++)for(var f=0;6>f;f++){var g=a.substr(d,4);"\xff\xff\xff\xff"!=g&&e({day:b+1,hr:c,min:10*f,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});d+=
+4}d+=4}};exports.readFullDatabase=function(a){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(e=>{console.log(e);e=e.split("-");var d=parseInt(e[1]),b=parseInt(e[2].replace(".raw",""));exports.readAllRecords(new Date(d,b,1),c=>{c.date=new Date(d,b,c.day,c.hr,c.min);a(c)})})};exports.readAllRecordsSince=function(a,e){for(var d=(new Date).getTime();a.getTime()<=d;)exports.readDay(a,b=>{b.date=new Date(a.getFullYear(),a.getMonth(),a.getDate(),b.hr,b.min);e(b)}),a.setDate(a.getDate()+1)};
+exports.readDailySummaries=function(a,e){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=584,b=0;31>b;b++){var c=a.substr(d,4);"\xff\xff\xff\xff"!=c&&e({day:b+1,steps:c.charCodeAt(0)<<8|c.charCodeAt(1),bpm:c.charCodeAt(2),movement:8*c.charCodeAt(3)});d+=580}};exports.readDay=function(a,e){k(a);var d=h(a);d=require("Storage").read(d);if(void 0!==d){a=8+580*(a.getDate()-1);for(var b=0;24>b;b++)for(var c=0;6>c;c++){var f=d.substr(a,4);"\xff\xff\xff\xff"!=f&&e({hr:b,min:10*
+c,steps:f.charCodeAt(0)<<8|f.charCodeAt(1),bpm:f.charCodeAt(2),movement:8*f.charCodeAt(3)});a+=4}}}
\ No newline at end of file
diff --git a/apps/health/metadata.json b/apps/health/metadata.json
index 30e4b4276..10a146bdd 100644
--- a/apps/health/metadata.json
+++ b/apps/health/metadata.json
@@ -2,7 +2,7 @@
"id": "health",
"name": "Health Tracking",
"shortName": "Health",
- "version": "0.23",
+ "version": "0.27",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",
diff --git a/apps/hwid_a_battery_widget/ChangeLog b/apps/hwid_a_battery_widget/ChangeLog
index 6c57f97a8..e7cdd2b4b 100644
--- a/apps/hwid_a_battery_widget/ChangeLog
+++ b/apps/hwid_a_battery_widget/ChangeLog
@@ -7,3 +7,4 @@
0.07: Fixed position after unlocking
0.08: Handling exceptions
0.09: Add option for showing battery high mark
+0.10: Fix background color
diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json
index 73dfc7c92..98d7ce2d5 100644
--- a/apps/hwid_a_battery_widget/metadata.json
+++ b/apps/hwid_a_battery_widget/metadata.json
@@ -3,7 +3,7 @@
"name": "A Battery Widget (with percentage) - Hanks Mod",
"shortName":"H Battery Widget",
"icon": "widget.png",
- "version":"0.09",
+ "version":"0.10",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js
index 027535051..79ce9a5ad 100644
--- a/apps/hwid_a_battery_widget/widget.js
+++ b/apps/hwid_a_battery_widget/widget.js
@@ -26,6 +26,7 @@
var y = this.y;
if ((typeof x === 'undefined') || (typeof y === 'undefined')) {
} else {
+ g.setBgColor(COLORS.white);
g.clearRect(old_x, old_y, old_x + width, old_y + height);
const l = E.getBattery(); // debug: Math.floor(Math.random() * 101);
diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog
index 8bad496bf..504f747bd 100644
--- a/apps/iconlaunch/ChangeLog
+++ b/apps/iconlaunch/ChangeLog
@@ -21,4 +21,6 @@
0.15: Ensure that we hide widgets if in fullscreen mode
(So that widgets are still hidden if launcher is fast-loaded)
0.16: Use firmware provided E.showScroller method
-0.17: fix fullscreen with oneClickExit
+0.17: fix fullscreen with oneClickExit
+0.18: Better performance
+0.19: Remove 'jit' keyword as 'for(..of..)' is not supported (fix #2937)
\ No newline at end of file
diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js
index 9f8cedb0f..f7d5b7bf1 100644
--- a/apps/iconlaunch/app.js
+++ b/apps/iconlaunch/app.js
@@ -9,7 +9,6 @@
timeOut:"Off"
}, s.readJSON("iconlaunch.json", true) || {});
-
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -19,9 +18,9 @@
let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
let launchHash = s.hash(/\.info/);
if (launchCache.hash!=launchHash) {
- launchCache = {
- hash : launchHash,
- apps : s.list(/\.info$/)
+ launchCache = {
+ hash : launchHash,
+ apps : s.list(/\.info$/)
.map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};})
.filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type))
.sort((a,b)=>{
@@ -34,42 +33,65 @@
s.writeJSON("iconlaunch.cache.json", launchCache);
}
+ // cache items
+ const ICON_MISSING = s.read("iconlaunch.na.img");
+ let count = 0;
+
let selectedItem = -1;
const R = Bangle.appRect;
const iconSize = 48;
const appsN = Math.floor(R.w / iconSize);
- const whitespace = (R.w - appsN * iconSize) / (appsN + 1);
+ const whitespace = Math.floor((R.w - appsN * iconSize) / (appsN + 1));
+ const iconYoffset = Math.floor(whitespace/4)-1;
const itemSize = iconSize + whitespace;
+ launchCache.items = {};
+ for (let c of launchCache.apps){
+ let i = Math.floor(count/appsN);
+ if (!launchCache.items[i])
+ launchCache.items[i] = {};
+ launchCache.items[i][(count%3)] = c;
+ count++;
+ }
+
+ let texted;
let drawItem = function(itemI, r) {
- g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
- let x = 0;
- for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) {
- if (!launchCache.apps[i]) break;
- x += whitespace;
- if (!launchCache.apps[i].icon) {
- g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2);
- } else {
- if (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon);
- g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y);
- }
- if (selectedItem == i) {
- g.drawRect(
- x + r.x - 1,
- r.y - 1,
- x + r.x + iconSize + 1,
- r.y + iconSize + 1
- );
- }
- x += iconSize;
+ let x = whitespace;
+ let i = itemI * appsN - 1;
+ let selectedApp;
+ let c;
+ let selectedRect;
+ let item = launchCache.items[itemI];
+ if (texted == itemI){
+ g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
+ texted = undefined;
+ }
+ for (c of item) {
+ i++;
+ let id = c.icondata || (c.iconData = (c.icon ? s.read(c.icon) : ICON_MISSING));
+ g.drawImage(id,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize);
+ if (selectedItem == i) {
+ selectedApp = c;
+ selectedRect = [
+ x + r.x - 1,
+ r.y + iconYoffset - 1,
+ x + r.x + iconSize,
+ r.y + iconYoffset + iconSize
+ ];
+ }
+ x += iconSize + whitespace;
+ }
+ if (selectedRect) {
+ g.drawRect.apply(null, selectedRect);
+ drawText(itemI, r.y, selectedApp);
+ texted=itemI;
}
- drawText(itemI, r.y);
};
- let drawText = function(i, appY) {
- const selectedApp = launchCache.apps[selectedItem];
+ let drawText = function(i, appY, selectedApp) {
+ "jit";
const idy = (selectedItem - (selectedItem % 3)) / 3;
- if (!selectedApp || i != idy) return;
+ if (i != idy) return;
appY = appY + itemSize/2;
g.setFontAlign(0, 0, 0);
g.setFont("12x20");
@@ -122,21 +144,21 @@
},
btn:Bangle.showClock
};
-
+
//work both the fullscreen and the oneClickExit
if( settings.fullscreen && settings.oneClickExit)
{
- idWatch=setWatch(function(e) {
+ idWatch=setWatch(function(e) {
Bangle.showClock();
}, BTN, {repeat:false, edge:'rising' });
-
+
}
- else if( settings.oneClickExit )
+ else if( settings.oneClickExit )
{
options.back=Bangle.showClock;
}
-
+
let scroller = E.showScroller(options);
@@ -151,7 +173,7 @@
};
let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } };
-
+
Bangle.on("swipe", swipeHandler)
Bangle.on("drag", updateTimeout);
Bangle.on("touch", updateTimeout);
diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json
index 35a7907bd..acf46a431 100644
--- a/apps/iconlaunch/metadata.json
+++ b/apps/iconlaunch/metadata.json
@@ -2,7 +2,7 @@
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
- "version": "0.17",
+ "version": "0.19",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",
@@ -10,7 +10,8 @@
"supports": ["BANGLEJS2"],
"storage": [
{ "name": "iconlaunch.app.js", "url": "app.js" },
- { "name": "iconlaunch.settings.js", "url": "settings.js" }
+ { "name": "iconlaunch.settings.js", "url": "settings.js" },
+ { "name": "iconlaunch.na.img", "url": "na.img" }
],
"data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
diff --git a/apps/iconlaunch/na.img b/apps/iconlaunch/na.img
new file mode 100644
index 000000000..10f4a8f82
Binary files /dev/null and b/apps/iconlaunch/na.img differ
diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js
index f4c0599f7..3278075e4 100644
--- a/apps/iconlaunch/settings.js
+++ b/apps/iconlaunch/settings.js
@@ -16,8 +16,7 @@
}
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
const appMenu = {
- "": { "title": /*LANG*/"Launcher" },
- /*LANG*/"< Back": back,
+ "": { "title": /*LANG*/"Launcher", back: back },
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
onchange: (m) => {
diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog
index defae902b..28344678a 100644
--- a/apps/kbmulti/ChangeLog
+++ b/apps/kbmulti/ChangeLog
@@ -4,3 +4,6 @@
0.04: Allow moving the cursor
0.05: Switch swipe directions for Caps Lock and moving cursor.
0.06: Add ability to auto-lowercase after a capital letter insertion.
+0.07: Add compatability with `backswipe` app by using `Bangle.prependListener()` and `E.stopEventPropagation`- requires fw 2v19 or cutting
+ edge versions of 2v18. Falls back on `Bangle.on()` for backwards
+ compatability.
diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js
index 505132040..f979e4473 100644
--- a/apps/kbmulti/lib.js
+++ b/apps/kbmulti/lib.js
@@ -154,6 +154,7 @@ exports.input = function(options) {
displayText(false);
}
}
+ E.stopEventPropagation&&E.stopEventPropagation();
}
function onHelp(resolve,reject) {
@@ -161,7 +162,7 @@ exports.input = function(options) {
E.showPrompt(
helpMessage, {title: "Help", buttons : {"Ok":true}}
).then(function(v) {
- Bangle.on('swipe', onSwipe);
+ if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);}
generateLayout(resolve,reject);
layout.render();
});
@@ -208,7 +209,7 @@ exports.input = function(options) {
} else {
generateLayout(resolve,reject);
displayText(false);
- Bangle.on('swipe', onSwipe);
+ if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);}
layout.render();
}
});
diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json
index 0b44b0306..210646a01 100644
--- a/apps/kbmulti/metadata.json
+++ b/apps/kbmulti/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbmulti",
"name": "Multitap keyboard",
- "version":"0.06",
+ "version":"0.07",
"description": "A library for text input via multitap/T9 style keypad",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kbswipe/ChangeLog b/apps/kbswipe/ChangeLog
index 38d71986e..51401deda 100644
--- a/apps/kbswipe/ChangeLog
+++ b/apps/kbswipe/ChangeLog
@@ -6,3 +6,5 @@
0.06: Support input of numbers and uppercase characters.
0.07: Support input of symbols.
0.08: Redone patterns a,e,m,w,z.
+0.09: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js
index 7d05d7a8e..bed171d72 100644
--- a/apps/kbswipe/lib.js
+++ b/apps/kbswipe/lib.js
@@ -253,28 +253,38 @@ exports.input = function(options) {
};
Bangle.drawWidgets();
+ let dragHandlerKB = e=>{
+ "ram";
+ if (isInside(R, e)) {
+ if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
+ lastDrag = e.b ? e : 0;
+ }
+ }
+
+ let touchHandlerKB = (n,e) => {
+ if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
+ // touch inside widget
+ cycleInput();
+ } else if (isInside(R, e)) {
+ // touch inside app area
+ show();
+ }
+ }
+
+ let catchSwipe = ()=>{
+ E.stopEventPropagation&&E.stopEventPropagation();
+ };
+
return new Promise((resolve,reject) => {
- Bangle.setUI({mode:"custom", drag:e=>{
- "ram";
- if (isInside(R, e)) {
- if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
- lastDrag = e.b ? e : 0;
- }
- },touch:(n,e) => {
- if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
- // touch inside widget
- cycleInput();
- } else if (isInside(R, e)) {
- // touch inside app area
- show();
- }
- }, back:()=>{
+ Bangle.setUI({mode:"custom", drag:dragHandlerKB, touch:touchHandlerKB, back:()=>{
delete WIDGETS.kbswipe;
Bangle.removeListener("stroke", strokeHandler);
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
if (flashInterval) clearInterval(flashInterval);
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
}});
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
});
};
diff --git a/apps/kbswipe/metadata.json b/apps/kbswipe/metadata.json
index 3f3fbffa3..22e1e1431 100644
--- a/apps/kbswipe/metadata.json
+++ b/apps/kbswipe/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbswipe",
"name": "Swipe keyboard",
- "version":"0.08",
+ "version":"0.09",
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kbtouch/ChangeLog b/apps/kbtouch/ChangeLog
index 5bd2159e6..cb8e5cda6 100644
--- a/apps/kbtouch/ChangeLog
+++ b/apps/kbtouch/ChangeLog
@@ -1,3 +1,5 @@
0.01: New App!
0.02: Introduced settings to customize the layout and functionality of the keyboard.
0.03: Convert Yes/No On/Off in settings to checkboxes
+0.04: Catch and discard swipe events on fw2v19 and up (as well as some cutting
+ edge 2v18 ones), allowing compatability with the Back Swipe app.
diff --git a/apps/kbtouch/lib.js b/apps/kbtouch/lib.js
index db90440b9..4f064cfc7 100644
--- a/apps/kbtouch/lib.js
+++ b/apps/kbtouch/lib.js
@@ -161,8 +161,11 @@ function draw() {
},back:()=>{
clearInterval(flashInterval);
Bangle.setUI();
+ Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect);
resolve(text);
}});
+ let catchSwipe = ()=>{E.stopEventPropagation&&E.stopEventPropagation();};
+ Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
});
};
diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json
index 31dc8c9a8..349726498 100644
--- a/apps/kbtouch/metadata.json
+++ b/apps/kbtouch/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbtouch",
"name": "Touch keyboard",
- "version":"0.03",
+ "version":"0.04",
"description": "A library for text input via onscreen keyboard",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kineticscroll/ChangeLog b/apps/kineticscroll/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/kineticscroll/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/kineticscroll/README.md b/apps/kineticscroll/README.md
new file mode 100644
index 000000000..be451a7f4
--- /dev/null
+++ b/apps/kineticscroll/README.md
@@ -0,0 +1,7 @@
+# Kinetic scrolling
+
+This patches the default scroller implementation to use kinetic scrolling. It is based on the original implementation.
+
+## Creator
+
+[halemmerich](https://github.com/halemmerich)
diff --git a/apps/kineticscroll/app.png b/apps/kineticscroll/app.png
new file mode 100644
index 000000000..f3184b5c0
Binary files /dev/null and b/apps/kineticscroll/app.png differ
diff --git a/apps/kineticscroll/boot.js b/apps/kineticscroll/boot.js
new file mode 100644
index 000000000..1f1b7923a
--- /dev/null
+++ b/apps/kineticscroll/boot.js
@@ -0,0 +1,181 @@
+(function() {
+ E.showScroller = function(options) {
+ /* options = {
+ h = height
+ c = # of items
+ scroll = initial scroll position
+ scrollMin = minimum scroll amount (can be negative)
+ draw = function(idx, rect)
+ remove = function()
+ select = function(idx, touch)
+ }
+
+ returns {
+ scroll: int // current scroll amount
+ draw: function() // draw all
+ drawItem : function(idx) // draw specific item
+ isActive : function() // is this scroller still active?
+ }
+
+ */
+ if (!options) return Bangle.setUI(); // remove existing handlers
+
+ const MAX_VELOCITY=100;
+ let scheduledDraw;
+ let velocity = 0;
+ let accDy = 0;
+ let scheduledBrake = setInterval(()=>{velocity*=0.9;}, 50);
+ let lastDragStart = 0;
+ let R = Bangle.appRect;
+ let menuScrollMin = 0|options.scrollMin;
+ let menuScrollMax = options.h*options.c - R.h;
+ if (menuScrollMax{
+ if (e.y=0) && i {
+ g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2);
+ var a = YtoIdx(R.y);
+ var b = Math.min(YtoIdx(R.y2),options.c-1);
+ for (var i=a;i<=b;i++)
+ options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ }
+
+ const draw = () => {
+ let dy = velocity;
+ if (s.scroll - dy > menuScrollMax){
+ dy = s.scroll - menuScrollMax;
+ velocity = 0;
+ }
+ if (s.scroll - dy < menuScrollMin){
+ dy = s.scroll - menuScrollMin;
+ velocity = 0;
+ }
+
+ s.scroll -= dy;
+
+ let oldScroll = rScroll;
+ rScroll = s.scroll &~1;
+ let d = oldScroll-rScroll;
+
+ if (Math.abs(velocity) > 0.01)
+ scheduledDraw = setTimeout(draw,0);
+ else
+ scheduledDraw = undefined;
+
+ if (!d) {
+ return;
+ }
+ g.reset().setClipRect(R.x,R.y,R.x2,R.y2).scroll(0,d);
+ if (d < 0) {
+ let y = Math.max(R.y2-(1-d), R.y);
+ g.setClipRect(R.x,y,R.x2,R.y2);
+ let i = YtoIdx(y);
+
+ for (y = idxToY(i);y < R.y2;y+=options.h) {
+ options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+ i++;
+ }
+ } else { // d>0
+ let y = Math.min(R.y+d, R.y2);
+ g.setClipRect(R.x,R.y,R.x2,y);
+ let i = YtoIdx(y);
+ y = idxToY(i);
+
+ for (y = idxToY(i);y > R.y-options.h;y-=options.h) {
+ options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+ i--;
+ }
+ }
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ };
+
+ const dragHandler = e=>{
+ if ((velocity <0 && e.dy>0) || (velocity > 0 && e.dy<0)){
+ velocity *= -1;
+ accDy = 5 * velocity;
+ }
+ //velocity += e.dy * (Date.now() - lastDrag);
+ if (e.b > 0){
+ if (!lastDragStart){
+ lastDragStart = Date.now();
+ velocity = 0;
+ accDy = 0;
+ }
+ accDy += e.dy;
+ }
+ velocity = accDy / (Date.now() - lastDragStart) * MAX_VELOCITY;
+
+ if (lastDragStart && e.b == 0){
+ accDy = 0;
+ lastDragStart = 0;
+ }
+
+ velocity = E.clip(velocity,-MAX_VELOCITY,MAX_VELOCITY);
+ lastDrag=Date.now();
+ if (!scheduledDraw){
+ scheduledDraw = setTimeout(draw,0);
+ }
+ };
+
+ let uiOpts = {
+ mode : "custom",
+ back : options.back,
+ drag : dragHandler,
+ touch : touchHandler,
+ redraw : uiDraw
+ }
+
+ if (options.remove) uiOpts.remove = () => {
+ if (scheduledDraw)
+ clearTimeout(scheduledDraw);
+ clearInterval(scheduledBrake);
+ options.remove();
+ }
+
+ Bangle.setUI(uiOpts);
+
+
+
+ function idxToY(i) {
+ return i*options.h + R.y - rScroll;
+ }
+ function YtoIdx(y) {
+ return Math.floor((y + rScroll - R.y)/options.h);
+ }
+
+ let s = {
+ scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
+ draw : () => {
+ g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2);
+ let a = YtoIdx(R.y);
+ let b = Math.min(YtoIdx(R.y2),options.c-1);
+ for (let i=a;i<=b;i++)
+ options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ }, drawItem : i => {
+ let y = idxToY(i);
+ g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2));
+ options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ }, isActive : () => Bangle.uiRedraw == uiDraw
+ };
+
+ let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
+ s.draw(); // draw the full scroller
+ g.flip(); // force an update now to make this snappier
+ return s;
+ };
+})();
diff --git a/apps/kineticscroll/boot.min.js b/apps/kineticscroll/boot.min.js
new file mode 100644
index 000000000..42f0afa67
--- /dev/null
+++ b/apps/kineticscroll/boot.min.js
@@ -0,0 +1,4 @@
+(function(){E.showScroller=function(c){function k(a){return a*c.h+b.y-l}function h(a){return Math.floor((a+l-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,m=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,n=0|c.scrollMin,r=c.h*c.c-b.h;r{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);for(var a=h(b.y),d=Math.min(h(b.y2),c.c-1);a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},u=()=>{var a=e;f.scroll-a>r&&
+(a=f.scroll-r,e=0);f.scroll-aa){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=h(a);for(a=k(d);ab.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-
+1)}};let v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0a.dy)e*=-1,m=5*e;0{if(!(d.yn||0<=a)&&a{p&&clearTimeout(p);clearInterval(w);c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,
+n,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=h(b.y);let d=Math.min(h(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=k(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.uiRedraw==t},l=f.scroll&-2;f.draw();g.flip();return f}})()
\ No newline at end of file
diff --git a/apps/kineticscroll/metadata.json b/apps/kineticscroll/metadata.json
new file mode 100644
index 000000000..022d38291
--- /dev/null
+++ b/apps/kineticscroll/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "kineticscroll",
+ "name": "Kinetic Scroll",
+ "shortName":"Kinetic Scroll",
+ "version":"0.01",
+ "description": "Replacement for the system scroller with kinetic scrolling.",
+ "icon": "app.png",
+ "type": "bootloader",
+ "tags": "system",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"kineticscroll.boot.js","url":"boot.min.js"}
+ ]
+}
diff --git a/apps/lcdclock/ChangeLog b/apps/lcdclock/ChangeLog
index 56ea03c2c..e81ee5bc4 100644
--- a/apps/lcdclock/ChangeLog
+++ b/apps/lcdclock/ChangeLog
@@ -1,3 +1,5 @@
0.01: New App!
0.02: Use clock_info module as an app
-0.03: clock_info now uses app name to maintain settings specifically for this clock face
\ No newline at end of file
+0.03: clock_info now uses app name to maintain settings specifically for this clock face
+0.04: clock_info is loaded before widgets to match other clocks
+0.05: fix alignment of clock items caused by 0.04 (fix #2970)
\ No newline at end of file
diff --git a/apps/lcdclock/app.js b/apps/lcdclock/app.js
index 3808f46fe..b4eb25b0b 100644
--- a/apps/lcdclock/app.js
+++ b/apps/lcdclock/app.js
@@ -30,6 +30,20 @@ let draw = function() {
}, 60000 - (Date.now() % 60000));
};
+let clockInfoDraw = (itm, info, options) => {
+ let texty = options.y+41;
+ g.reset().setFont("7Seg").setColor(g.theme.bg).setBgColor(g.theme.fg);
+ if (options.focus) g.setBgColor("#FF0");
+ g.clearRect({x:options.x,y:options.y,w:options.w,h:options.h,r:8});
+
+ if (info.img) g.drawImage(info.img, options.x+2, options.y+2);
+ var title = clockInfoItems[options.menuA].name;
+ var text = info.text.toString().toUpperCase();
+ if (title!="Bangle") g.setFontAlign(1,0).drawString(title.toUpperCase(), options.x+options.w-2, options.y+14);
+ if (g.setFont("7Seg:2").stringWidth(text)+8>options.w) g.setFont("7Seg");
+ g.setFontAlign(0,0).drawString(text, options.x+options.w/2, options.y+40);
+};
+
// Show launcher when middle button pressed
Bangle.setUI({
mode : "clock",
@@ -48,37 +62,24 @@ Bangle.setUI({
}});
// Load widgets
Bangle.loadWidgets();
-var R = Bangle.appRect;
+// Work out sizes
+let R = Bangle.appRect;
R.x+=1;
R.y+=1;
R.x2-=1;
R.y2-=1;
R.w-=2;
R.h-=2;
-var midX = R.x+R.w/2;
-var barY = 80;
+let midX = R.x+R.w/2;
+let barY = 80;
// Clear the screen once, at startup
let oldTheme = g.theme;
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(1);
g.fillRect({x:R.x, y:R.y, w:R.w, h:R.h, r:8}).clearRect(R.x,barY,R.w,barY+1).clearRect(midX,R.y,midX+1,barY);
draw();
-setTimeout(Bangle.drawWidgets,0);
-
-let clockInfoDraw = (itm, info, options) => {
- let texty = options.y+41;
- g.reset().setFont("7Seg").setColor(g.theme.bg).setBgColor(g.theme.fg);
- if (options.focus) g.setBgColor("#FF0");
- g.clearRect({x:options.x,y:options.y,w:options.w,h:options.h,r:8});
-
- if (info.img) g.drawImage(info.img, options.x+2, options.y+2);
- var title = clockInfoItems[options.menuA].name;
- var text = info.text.toString().toUpperCase();
- if (title!="Bangle") g.setFontAlign(1,0).drawString(title.toUpperCase(), options.x+options.w-2, options.y+14);
- if (g.setFont("7Seg:2").stringWidth(text)+8>options.w) g.setFont("7Seg");
- g.setFontAlign(0,0).drawString(text, options.x+options.w/2, options.y+40);
-
-};
+Bangle.drawWidgets();
+// Allocate and draw clockinfos
let clockInfoItems = require("clock_info").load();
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x, y:R.y, w:midX-2, h:barY-R.y-2, draw : clockInfoDraw});
let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+2, y:R.y, w:midX-3, h:barY-R.y-2, draw : clockInfoDraw});
-}
+}
\ No newline at end of file
diff --git a/apps/lcdclock/metadata.json b/apps/lcdclock/metadata.json
index b144c125e..83107cf40 100644
--- a/apps/lcdclock/metadata.json
+++ b/apps/lcdclock/metadata.json
@@ -1,6 +1,6 @@
{ "id": "lcdclock",
"name": "LCD Clock",
- "version":"0.03",
+ "version":"0.05",
"description": "A Casio-style clock, with ClockInfo areas at the top and bottom. Tap them and swipe up/down to toggle between different information",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/lunaclock/ChangeLog b/apps/lunaclock/ChangeLog
new file mode 100644
index 000000000..2286a7f70
--- /dev/null
+++ b/apps/lunaclock/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
\ No newline at end of file
diff --git a/apps/lunaclock/README.md b/apps/lunaclock/README.md
new file mode 100644
index 000000000..0b7555b29
--- /dev/null
+++ b/apps/lunaclock/README.md
@@ -0,0 +1,9 @@
+# Luna Clock
+
+
+
+Simple clock face inspired by the moon.
+
+## Creator
+
+NovaDawn999
\ No newline at end of file
diff --git a/apps/lunaclock/app-icon.js b/apps/lunaclock/app-icon.js
new file mode 100644
index 000000000..d17110a5a
--- /dev/null
+++ b/apps/lunaclock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEIf4A/AH4AJiIABiAVRiUz/4AB+cyDJ8TCoX/mc/AQMgCxkjCwgAB+YCBDBcDCwYXCn4CC+ZKJgQWEFYQWCA4MyC5EvC5HzA4U/JI4uEIAQWBD4k/+QuKLYZDCDwQFCGAsBIgoBBFIIGBPIf/GAqMFDIQvDHARjBSQp1EFAIpCL4YGEmBGEE4akEIYYIDPIhGBiYwDmTSEgUiAwMiAAMzJAUf+ZgD+cCCwX/iMjCQMxiczmUDSIUvmIXDmIXBmchmcTn8jmcgJAUgC4U/iLXC+ciC4UgCgMyOwMRC4UwiJ2BLAIXDAgJHBCgReCEAQXBiUACgURWoQEBdAYvBCoIXL+TDBiAWCegUgicBC4cTC4IPBkE/OoM/AgK6EBIMQC4ixBH4ISCmUSkEvBoJ3BJAR9CBIMvC4KHCl4YCiESiYXBSAUBWwIRBNgIXEkZbCmLIBRYchgUykMjAILwBgLeBkAhCLAIEBUwK3BiEikUykRJCiEAiQDBAQJYDAgRABiQVBCwcyd4MCiIHCAAMhT4YACCwIXCkUhC4MBiQgDCAQFCkYVCAAhGBC4MBEIQZEFggWHAARrCkQVELYMhMAICBiJFCC4gMBJYI0CCoInCiAlBCooAEDIIUBHwsBFwIXKDAITBQAIECBAIWMJYSBDGAYXNIAaFFACAtELZpUBHoIBBDAIZBBYJ4CEA0TawMyaQQXBmUwAQMgiYKBmIXFgTjEkMgA4QXBcgQbBNCoAEA=="))
\ No newline at end of file
diff --git a/apps/lunaclock/app.js b/apps/lunaclock/app.js
new file mode 100644
index 000000000..e73467bc1
--- /dev/null
+++ b/apps/lunaclock/app.js
@@ -0,0 +1,72 @@
+var MYIMG = {
+ width : 176, height : 176, bpp : 4,
+ buffer : require("heatshrink").decompress(atob("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"))
+};
+
+//var IMAGEWIDTH = 176;
+//var IMAGEHEIGHT = 109;
+var IMAGEWIDTH = 176;
+var IMAGEHEIGHT = 176;
+
+Graphics.prototype.setFontcustom = function() {
+ // Actual height 58 (57 - 0)
+ return this.setFontCustom(
+ E.toString(require('heatshrink').decompress(atob('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'))),
+ 32,
+ atob("EA4RPC4uLQoZGhwaDxkOKCodHx4pLCMdHCMODx4mHyE0LSgrKiAfLCsZJDImPy4rJC8yHiYoLkEyLiQUFBM="),
+ 58|65536
+ );
+};
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+function draw() {
+ //clock pos
+ var x = g.getWidth()/2;
+ var y = 105;
+ g.setColor("#000000"); //color of clock text
+ //draw clock
+ g.drawImage(MYIMG,0,g.getHeight()-IMAGEHEIGHT, 1);
+ //get time/date
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var dateStr = require("locale").date(date).toUpperCase();
+ var dowStr = require("locale").dow(date).toUpperCase();
+ // draw time
+ g.setFontAlign(0,0).setFont("custom");
+ g.drawString(timeStr,x,y);
+ // draw date
+ g.setColor(1,1,1);
+ var dateFormatted = dowStr + ", " + dateStr;
+ g.setFont("6x8");
+ g.drawString(dateFormatted,g.getWidth()/2,g.getHeight() - 8);
+ Bangle.drawWidgets();
+ // queue draw in one minute
+ queueDraw();
+}
+
+
+g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
+draw();
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw();
+ } else {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/lunaclock/app.png b/apps/lunaclock/app.png
new file mode 100644
index 000000000..314cf9cca
Binary files /dev/null and b/apps/lunaclock/app.png differ
diff --git a/apps/lunaclock/metadata.json b/apps/lunaclock/metadata.json
new file mode 100644
index 000000000..4e797d8e7
--- /dev/null
+++ b/apps/lunaclock/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "lunaclock",
+ "name": "Luna Clock",
+ "version": "0.01",
+ "description": "Simple clock face inspired by the moon.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"lunaclock.app.js","url":"app.js"},
+ {"name":"lunaclock.img","url":"app-icon.js","evaluate":true}
+ ]
+}
\ No newline at end of file
diff --git a/apps/lunaclock/screenshot.png b/apps/lunaclock/screenshot.png
new file mode 100644
index 000000000..62fd91ecf
Binary files /dev/null and b/apps/lunaclock/screenshot.png differ
diff --git a/apps/messagelist/ChangeLog b/apps/messagelist/ChangeLog
index 37854d8ae..899e29cb6 100644
--- a/apps/messagelist/ChangeLog
+++ b/apps/messagelist/ChangeLog
@@ -1,3 +1,5 @@
0.01: New app!
0.02: Fix music updates while app is already open
-0.03: Fix invalid use of Bangle.setUI
\ No newline at end of file
+0.03: Fix invalid use of Bangle.setUI
+0.04: Fix app crashing when new message arrives
+
diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js
index dfa7e43d4..3c140a0c4 100644
--- a/apps/messagelist/app.js
+++ b/apps/messagelist/app.js
@@ -24,6 +24,9 @@
RIGHT = 1, LEFT = -1, // swipe directions
UP = -1, DOWN = 1; // updown directions
const Layout = require("Layout");
+ const debug = function() {
+ if (global.DEBUG_MESSAGELIST) console.log.apply(console, ['messagelist:'].concat(arguments));
+ }
const settings = () => require("messagegui").settings();
const fontTiny = "6x8"; // fixed size, don't use this for important things
@@ -45,6 +48,7 @@
/// List of all our messages
let MESSAGES;
const saveMessages = function() {
+ debug('saveMessages()');
const noSave = ["alarm", "call", "music"]; // assume these are outdated once we close the app
noSave.forEach(id => remove({id: id}));
require("messages").write(MESSAGES
@@ -56,6 +60,7 @@
);
};
const uiRemove = function() {
+ debug('uiRemove()');
if (musicTimeout) clearTimeout(musicTimeout);
layout = undefined;
Bangle.removeListener("message", onMessage);
@@ -85,11 +90,25 @@
}
const setUI = function(options, cb) {
+ debug('setUI(', options, cb?'':cb)
delete Bangle.uiRemove; // don't clear out things when switching UI within the app
options = Object.assign({mode:"custom", remove: () => uiRemove()}, options);
// If options={} assume we still want `remove` to be called when leaving via fast load (so we must have 'mode:custom')
Bangle.setUI(options, cb);
};
+ /**
+ * Same as calling `new Layout(layout, options)`, except Bangle.uiRemove is not called
+ * @param {object} layout
+ * @param {object} options
+ * @returns {Layout}
+ */
+ const makeLayout = function(layout, options) {
+ const remove = Bangle.uiRemove;
+ delete Bangle.uiRemove; // don't clear out things when setting up new Layout
+ const result = new Layout(layout, options);
+ if (remove) Bangle.uiRemove = remove;
+ return result;
+ }
const remove = function(msg) {
if (msg.id==="call") call = undefined;
@@ -111,6 +130,7 @@
};
const onMessage = function(type, msg) {
+ debug(`onMessage(${type}`, msg);
if (msg.handled) return;
msg.handled = true;
switch(type) {
@@ -135,6 +155,7 @@
Bangle.on("message", onMessage);
const onCall = function(msg) {
+ debug('onCall(', msg);
if (msg.t==="remove") {
call = undefined;
return exitScreen("call");
@@ -145,6 +166,7 @@
showCall();
};
const onAlarm = function(msg) {
+ debug('onAlarm(', msg);
if (msg.t==="remove") {
alarm = undefined;
return exitScreen("alarm");
@@ -155,6 +177,7 @@
};
let musicTimeout;
const onMusic = function(msg) {
+ debug('onMusic(', msg);
const hadMusic = !!music;
if (musicTimeout) clearTimeout(musicTimeout);
musicTimeout = undefined;
@@ -184,6 +207,7 @@
}
};
const onMap = function(msg) {
+ debug('onMap(', msg);
const hadMap = !!map;
if (msg.t==="remove") {
map = undefined;
@@ -196,6 +220,7 @@
else if (active==="main" && !hadMap) showMain(); // refresh menu: add "Map" entry
};
const onText = function(msg) {
+ debug('onText(', msg);
require("messages").apply(msg, MESSAGES);
const mIdx = MESSAGES.findIndex(m => m.id===msg.id);
if (!MESSAGES[mIdx]) if (back==="messages") back = undefined;
@@ -237,6 +262,7 @@
};
const showMap = function() {
+ debug('showMap()');
setActive("map");
delete map.new;
let m, distance, street, target, eta;
@@ -254,7 +280,7 @@
} else {
target = map.body;
}
- let layout = new Layout({
+ let layout = makeLayout({
type: "v", c: [
{type: "txt", font: fontNormal, label: target, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2},
{
@@ -319,6 +345,7 @@
else Bangle.musicControl(action);
};
const showMusic = function() {
+ debug('showMusic()', music);
if (active!==music) setActive("music");
if (!music) music = {track: "", artist: "", album: "", state: "pause"};
delete music.new;
@@ -355,7 +382,7 @@
else if (dur) info = dur;
else info = {};
- layout = new Layout({
+ layout = makeLayout({
type: "v", c: [
{
type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
@@ -442,12 +469,14 @@
let layout;
const clearStuff = function() {
+ debug('clearStuff()');
delete Bangle.appRect;
layout = undefined;
setUI();
g.reset().clearRect(Bangle.appRect);
};
const setActive = function(screen, args) {
+ debug(`setActive(${screen}`, args);
clearStuff();
if (active && screen!==active) back = active;
if (screen==="messages") messageNum = args;
@@ -476,6 +505,7 @@
}
};
const showMain = function() {
+ debug('showMain()');
setActive("main");
let grid = {"": {title:/*LANG*/"Messages", align: 0, back: load}};
if (call) grid[/*LANG*/"Incoming Call"] = {icon: "Phone", cb: showCall};
@@ -596,7 +626,7 @@
}
l.c.push(row);
}
- layout = new Layout(l, {back: back});
+ layout = makeLayout(l, {back: back});
layout.render();
if (B2) {
@@ -640,6 +670,7 @@
};
const showSettings = function() {
+ debug('showSettings()');
setActive("settings");
eval(require("Storage").read("messagelist.settings.js"))(() => {
setFont();
@@ -647,6 +678,7 @@
});
};
const showCall = function() {
+ debug('showCall()');
setActive("call");
delete call.new;
Bangle.setLocked(false);
@@ -678,7 +710,7 @@
];
}
- layout = new Layout({
+ layout = makeLayout({
type: "v", c: [
{
type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
@@ -722,6 +754,7 @@
});
};
const showAlarm = function() {
+ debug('showAlarm()');
// dismissing alarms doesn't seem to work, so this is simple */
setActive("alarm");
delete alarm.new;
@@ -731,7 +764,7 @@
const w = g.getWidth()-48,
lines = g.setFont(fontNormal).wrapString(alarm.title, w),
title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n");
- layout = new Layout({
+ layout = makeLayout({
type: "v", c: [
{
type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
@@ -830,6 +863,7 @@
);
};
const showMessage = function(num, bottom) {
+ debug(`showMessage(${num}, ${!!bottom})`);
if (num<0) num = 0;
if (!num) num = 0; // no number: show first
if (num>=MESSAGES.length) num = MESSAGES.length-1;
@@ -1093,7 +1127,7 @@
let imageCol = getImageColor(msg);
if (g.setColor(imageCol).getColor()==hBg) imageCol = hCol;
- layout = new Layout({
+ layout = makeLayout({
type: "v", c: [
{
type: "h", fillx: 1, bgCol: hBg, col: hCol, c: [
@@ -1133,6 +1167,7 @@
* Stop auto-unload timeout and buzzing, remove listeners for this function
*/
const clearUnreadStuff = function() {
+ debug('clearUnreadStuff()');
require("messages").stopBuzz();
if (unreadTimeout) clearTimeout(unreadTimeout);
unreadTimeout = undefined;
@@ -1208,4 +1243,4 @@
// stop buzzing, auto-close timeout on input
["touch", "drag", "swipe"].forEach(l => Bangle.on(l, clearUnreadStuff));
(B2 ? [BTN1] : [BTN1, BTN2, BTN3]).forEach(b => watches.push(setWatch(clearUnreadStuff, b, false)));
-}
\ No newline at end of file
+}
diff --git a/apps/messagelist/metadata.json b/apps/messagelist/metadata.json
index 37fed5795..2f1abe11d 100644
--- a/apps/messagelist/metadata.json
+++ b/apps/messagelist/metadata.json
@@ -1,7 +1,7 @@
{
"id": "messagelist",
"name": "Message List",
- "version": "0.03",
+ "version": "0.04",
"description": "Display notifications from iOS and Gadgetbridge/Android as a list",
"icon": "app.png",
"type": "app",
diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog
index 8ce792783..47aa51107 100644
--- a/apps/messagesoverlay/ChangeLog
+++ b/apps/messagesoverlay/ChangeLog
@@ -3,3 +3,5 @@
0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible.
0.04: Use the event mechanism for getting messages
0.05: Fix the overlay keeping the LCD on
+0.06: Better low memory handling
+ Fix first message beeing displayed again on unlock
diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js
index 9af14bbe4..6767cfbce 100644
--- a/apps/messagesoverlay/lib.js
+++ b/apps/messagesoverlay/lib.js
@@ -1,3 +1,5 @@
+const MIN_FREE_MEM = 1000;
+const LOW_MEM = 2000;
const ovrx = 10;
const ovry = 10;
const ovrw = g.getWidth()-2*ovrx;
@@ -28,6 +30,7 @@ let callInProgress = false;
let show = function(ovr){
let img = ovr;
+ LOG("show", img.getBPP());
if (ovr.getBPP() == 1) {
img = ovr.asImage();
img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]);
@@ -162,8 +165,9 @@ let showMessage = function(ovr, msg) {
drawMessage(ovr, msg);
};
-let drawBorder = function(ovr) {
+let drawBorder = function(img) {
LOG("drawBorder", isQuiet());
+ if (img) ovr=img;
if (Bangle.isLocked())
ovr.setColor(ovr.theme.fgH);
else
@@ -232,13 +236,6 @@ let next = function(ovr) {
showMessage(ovr, eventQueue[0]);
};
-let showMapMessage = function(ovr, msg) {
- ovr.clearRect(2,2,ovr.getWidth()-3,ovr.getHeight()-3);
- drawMessage(ovr, {
- body: "Not implemented!"
- });
-};
-
let callBuzzTimer = null;
let stopCallBuzz = function() {
if (callBuzzTimer) {
@@ -407,7 +404,7 @@ let main = function(ovr, event) {
if (!lockListener) {
lockListener = function (){
- drawBorder(ovr);
+ drawBorder();
};
Bangle.on('lock', lockListener);
}
@@ -439,9 +436,15 @@ exports.message = function(type, event) {
if(event.handled) return;
bpp = 4;
- if (process.memory().free < 2000) bpp = 1;
+ if (process.memory().free < LOW_MEM)
+ bpp = 1;
- if (!ovr) {
+ while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){
+ let dropped = eventQueue.pop();
+ print("Dropped message because of memory constraints", dropped);
+ }
+
+ if (!ovr || ovr.getBPP() != bpp) {
ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, {
msb: true
});
diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json
index a043fff64..c16a41f5c 100644
--- a/apps/messagesoverlay/metadata.json
+++ b/apps/messagesoverlay/metadata.json
@@ -1,7 +1,7 @@
{
"id": "messagesoverlay",
"name": "Messages Overlay",
- "version": "0.05",
+ "version": "0.06",
"description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)",
"icon": "app.png",
"type": "bootloader",
diff --git a/apps/miclock2/ChangeLog b/apps/miclock2/ChangeLog
index 534332e63..c34ba135c 100644
--- a/apps/miclock2/ChangeLog
+++ b/apps/miclock2/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Redraw only when seconds change
0.03: Fix typo in redraw check
+0.04: Register as clock and implement fast loading
diff --git a/apps/miclock2/clock-mixed.js b/apps/miclock2/clock-mixed.js
index bb1537313..e6bc0c094 100644
--- a/apps/miclock2/clock-mixed.js
+++ b/apps/miclock2/clock-mixed.js
@@ -1,29 +1,30 @@
// Code based on the original Mixed Clock
+{
/* jshint esversion: 6 */
-var locale = require("locale");
+const locale = require("locale");
const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 };
const Center = { "x": 120, "y": 96 };
const Widths = { hour: 2, minute: 2 };
-var buf = Graphics.createArrayBuffer(240,192,1,{msb:true});
-var lastDate = new Date();
+const buf = Graphics.createArrayBuffer(240,192,1,{msb:true});
+let timeoutId;
-function rotatePoint(x, y, d) {
- rad = -1 * d / 180 * Math.PI;
- var sin = Math.sin(rad);
- var cos = Math.cos(rad);
- xn = ((Center.x + x * cos - y * sin) + 0.5) | 0;
- yn = ((Center.y + x * sin - y * cos) + 0.5) | 0;
- p = [xn, yn];
- return p;
-}
+const rotatePoint = function(x, y, d, center, res) {
+ "jit";
+ const rad = -1 * d / 180 * Math.PI;
+ const sin = Math.sin(rad);
+ const cos = Math.cos(rad);
+ res[0] = ((center.x + x * cos - y * sin) + 0.5) | 0;
+ res[1] = ((center.y + x * sin - y * cos) + 0.5) | 0;
+};
// from https://github.com/espruino/Espruino/issues/1702
-function setLineWidth(x1, y1, x2, y2, lw) {
- var dx = x2 - x1;
- var dy = y2 - y1;
- var d = Math.sqrt(dx * dx + dy * dy);
+const setLineWidth = function(x1, y1, x2, y2, lw) {
+ "ram";
+ let dx = x2 - x1;
+ let dy = y2 - y1;
+ let d = Math.sqrt(dx * dx + dy * dy);
dx = dx * lw / d;
dy = dy * lw / d;
@@ -44,71 +45,84 @@ function setLineWidth(x1, y1, x2, y2, lw) {
x2 - dy, y2 + dx,
x1 - dy, y1 + dx
];
-}
+};
-function drawMixedClock(force) {
- var date = new Date();
- if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() !== lastDate.getSeconds()) {
- lastDate = date;
- var dateArray = date.toString().split(" ");
- var isEn = locale.name.startsWith("en");
- var point = [];
- var minute = date.getMinutes();
- var hour = date.getHours();
- var radius;
-
- g.reset();
- buf.clear();
-
- // draw date
- buf.setFont("6x8", 2);
- buf.setFontAlign(-1, 0);
- buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
- buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
- buf.setFontAlign(1, 0);
- buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
- buf.drawString(dateArray[3], 237, 176, true);
+const drawMixedClock = function() {
+ const date = new Date();
+ const dateArray = date.toString().split(" ");
+ const isEn = locale.name.startsWith("en");
+ let point = [0, 0];
+ const minute = date.getMinutes();
+ const hour = date.getHours();
+ let radius;
- // draw hour and minute dots
- for (i = 0; i < 60; i++) {
- radius = (i % 5) ? 2 : 4;
- point = rotatePoint(0, Radius.dots, i * 6);
- buf.fillCircle(point[0], point[1], radius);
- }
+ g.reset();
+ buf.clear();
- // draw digital time
- buf.setFont("6x8", 3);
- buf.setFontAlign(0, 0);
- buf.drawString(dateArray[4], 120, 120, true);
+ // draw date
+ buf.setFont("6x8", 2);
+ buf.setFontAlign(-1, 0);
+ buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
+ buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
+ buf.setFontAlign(1, 0);
+ buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
+ buf.drawString(dateArray[3], 237, 176, true);
- // draw new minute hand
- point = rotatePoint(0, Radius.min, minute * 6);
- buf.drawLine(Center.x, Center.y, point[0], point[1]);
- buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute));
- // draw new hour hand
- point = rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0);
- buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
-
- // draw center
- buf.fillCircle(Center.x, Center.y, Radius.center);
-
- g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
+ // draw hour and minute dots
+ for (i = 0; i < 60; i++) {
+ radius = (i % 5) ? 2 : 4;
+ rotatePoint(0, Radius.dots, i * 6, Center, point);
+ buf.fillCircle(point[0], point[1], radius);
}
-}
-Bangle.on('lcdPower', function(on) {
- if (on)
- drawMixedClock(true);
- Bangle.drawWidgets();
-});
+ // draw digital time
+ buf.setFont("6x8", 3);
+ buf.setFontAlign(0, 0);
+ buf.drawString(dateArray[4], 120, 120, true);
-setInterval(() => drawMixedClock(true), 30000); // force an update every 30s even screen is off
+ // draw new minute hand
+ rotatePoint(0, Radius.min, minute * 6, Center, point);
+ buf.drawLine(Center.x, Center.y, point[0], point[1]);
+ buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute));
+ // draw new hour hand
+ rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0, Center, point);
+ buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
+
+ // draw center
+ buf.fillCircle(Center.x, Center.y, Radius.center);
+
+ g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
+
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ const period = (Bangle.isLCDOn() ? 1000 : 60000); // Update every second if display is on else every minute
+ let timeout = period - (Date.now() % period);
+ timeoutId = setTimeout(()=>{
+ timeoutId = undefined;
+ drawMixedClock();
+ }, timeout);
+};
+
+const onLCDPower = function(on) {
+ if (on) {
+ drawMixedClock();
+ Bangle.drawWidgets();
+ }
+};
+Bangle.on('lcdPower', onLCDPower);
+
+Bangle.setUI({mode:"clock", remove:function() {
+ if (timeoutId !== undefined) {
+ delete buf.buffer;
+ clearTimeout(timeoutId);
+ timeoutId = undefined;
+ Bangle.removeListener('lcdPower',onLCDPower);
+ }
+}});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawMixedClock(); // immediately draw
-setInterval(drawMixedClock, 500); // update twice a second
-
-// Show launcher when middle button pressed after freeing memory first
-setWatch(() => {delete buf.buffer; Bangle.showLauncher()}, BTN2, {repeat:false,edge:"falling"});
+}
diff --git a/apps/miclock2/metadata.json b/apps/miclock2/metadata.json
index 094d0995a..f177ab4c1 100644
--- a/apps/miclock2/metadata.json
+++ b/apps/miclock2/metadata.json
@@ -1,7 +1,7 @@
{
"id": "miclock2",
"name": "Mixed Clock 2",
- "version": "0.03",
+ "version": "0.04",
"description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.",
"icon": "clock-mixed.png",
"type": "clock",
diff --git a/apps/multitimer/ChangeLog b/apps/multitimer/ChangeLog
index b708a990f..67b0cc014 100644
--- a/apps/multitimer/ChangeLog
+++ b/apps/multitimer/ChangeLog
@@ -4,3 +4,5 @@
0.04: Remove copied sched alarm.js & import newer features (oneshot alarms)
0.05: Fix creating new alarms/timers in hardmode
0.06: Support fastloading
+0.07: Fix fastloading support - ensure drag handler's restored after
+ menu display/fastload removes it
diff --git a/apps/multitimer/app.js b/apps/multitimer/app.js
index 079136431..965a12d26 100644
--- a/apps/multitimer/app.js
+++ b/apps/multitimer/app.js
@@ -217,6 +217,7 @@ function timerMenu(idx) {
}
}
});
+ setUI();
}
function editTimer(idx, a) {
@@ -376,6 +377,7 @@ function drawSw() {
else if (idx > 0 && idx < sw.length+1) swMenu(idx-1);
}
});
+ setUI();
}
function swMenu(idx, a) {
@@ -499,6 +501,7 @@ function swMenu(idx, a) {
}
}
});
+ setUI();
}
function drawAlarms() {
@@ -539,6 +542,7 @@ function drawAlarms() {
else if (idx > 0 && idx < alarms.length+1) editAlarm(idx-1);
}
});
+ setUI();
}
function editDOW(dow, onchange) {
@@ -672,6 +676,7 @@ function editAlarm(idx, a) {
function setUI() {
// E.showMenu/E.showScroller/E.showAlert call setUI, so we register onDrag() separately
// and tack on uiRemove after the fact to avoid interfering
+ Bangle.on("drag", onDrag);
Bangle.uiRemove = () => {
Bangle.removeListener("drag", onDrag);
Object.values(timerInt1).forEach(clearTimeout);
@@ -703,6 +708,4 @@ function onDrag(e) {
}
drawTimers();
-
-Bangle.on("drag", onDrag);
}
diff --git a/apps/multitimer/metadata.json b/apps/multitimer/metadata.json
index 40d376986..e753d0581 100644
--- a/apps/multitimer/metadata.json
+++ b/apps/multitimer/metadata.json
@@ -1,7 +1,7 @@
{
"id": "multitimer",
"name": "Multi Timer",
- "version": "0.06",
+ "version": "0.07",
"description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.",
"icon": "app.png",
"screenshots": [
diff --git a/apps/nightwatch/ChangeLog b/apps/nightwatch/ChangeLog
new file mode 100644
index 000000000..c854e9e5b
--- /dev/null
+++ b/apps/nightwatch/ChangeLog
@@ -0,0 +1,2 @@
+1.0: first working version of App
+1.1: bugfix (enable settings page)
diff --git a/apps/nightwatch/README.md b/apps/nightwatch/README.md
new file mode 100644
index 000000000..6d1749c5d
--- /dev/null
+++ b/apps/nightwatch/README.md
@@ -0,0 +1,20 @@
+# The Nightwatch
+
+Snuggle into your sleeping bag, hang the Bangle on the tent wall
+and check the screen before you fall asleep:
+
+
+
+
+
+Reads temperature and pressure sensor. Shows current, maximal and minimal values
+since the start of the app. Also show a graph of the last 20 measures.
+
+Swipe left/right between values.
+
+Screen is updated periodically, time step is configurable in settings.
+
+
+# Creator
+
+[Niko Komin](https://www.laikaundfreunde.de/niko-komin/)
diff --git a/apps/nightwatch/metadata.json b/apps/nightwatch/metadata.json
new file mode 100644
index 000000000..4de2a0271
--- /dev/null
+++ b/apps/nightwatch/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id":"nightwatch",
+ "readme":"README.md",
+ "name":"The Nightwatch",
+ "shortName":"Nightwatch",
+ "supports" : ["BANGLEJS2"],
+ "icon":"nightwatch.icon.png",
+ "screenshots" : [ { "url":"screenshot.png","url":"screenshot2.png" } ],
+ "version":"1.1",
+ "description":"Logs sensor readings (currently T and p), show min/max and graph.",
+ "tags": "tools,outdoors",
+ "storage": [
+ {"name":"nightwatch.app.js","url":"nightwatch.app.js"},
+ {"name":"nightwatch.settings.js","url":"nightwatch.settings.js"},
+ {"name":"nightwatch.img","url":"nightwatch.icon.js","evaluate":true}
+ ],
+ "data": [{"name":"nightwatch.json"}]
+}
diff --git a/apps/nightwatch/nightwatch.app.info b/apps/nightwatch/nightwatch.app.info
new file mode 100644
index 000000000..36345ce26
--- /dev/null
+++ b/apps/nightwatch/nightwatch.app.info
@@ -0,0 +1,6 @@
+require("Storage").write("nightwatch.info",{
+ "id":"nightwatch",
+ "name":"nightwatch",
+ "src":"nightwatch.app.js",
+ "icon":"nightwatch.icon.png"
+});
\ No newline at end of file
diff --git a/apps/nightwatch/nightwatch.app.js b/apps/nightwatch/nightwatch.app.js
new file mode 100644
index 000000000..035307106
--- /dev/null
+++ b/apps/nightwatch/nightwatch.app.js
@@ -0,0 +1,175 @@
+// PTLOGGER
+// MEASURES p AND T PERIODICALLY AND UPDATES MIN & MAX VALS
+// DISPLAYS EITHER OF BOTH
+
+
+var settings = Object.assign({
+ dt: 5, //time interval in minutes
+}, require('Storage').readJSON("nightwatch.json", true) || {});
+
+let dt = settings.dt;
+delete settings;
+
+var timerID;
+
+const highColor = '#35b779';//#6dcd59;
+const lowColor = '#eb005c';//#3d4a89;//#482878;
+const normColor = '#000000';
+const historyAmnt = 24;
+
+
+const TData = {
+ ondisplay:true,
+ unit: '\xB0C',
+ accuracy: 1,
+ value : 100, t_value:'0:00',
+ values : new Array(historyAmnt),
+ maxval : -100, t_max:'0:00',
+ minval : 100, t_min:'0:00'
+};
+
+const PData = {
+ ondisplay:false,
+ unit: 'mbar',
+ accuracy: 0,
+ value : 0, t_value:'0:00',
+ values : new Array(historyAmnt),
+ maxval : 0, t_max:'0:00',
+ minval : 10000, t_min:'0:00'
+};
+
+function minMaxString(val,accuracy,unit,time){
+ return time+' '+val.toFixed(accuracy)+unit;
+// return val.toFixed(accuracy)+unit+'('+time+')';
+}
+
+function updateScreen() {
+ // what are we showing right now?
+ let data;
+ if (TData.ondisplay){data = TData;}
+ else {data = PData;}
+
+ // make strings
+ let valueString = data.value.toFixed(data.accuracy)+data.unit;
+ let minString = minMaxString(data.minval, data.accuracy, data.unit, data.t_min);
+ let maxString = minMaxString(data.maxval, data.accuracy, data.unit, data.t_max);
+
+ // LETS PAINT
+ g.clear();
+ g.setFontAlign(0, 0);
+
+ // MINUM AND MAXIMUM VALUES AND TIMES
+ g.setFont("Vector:18");
+ g.setColor(normColor);
+ g.drawString(maxString, g.getWidth() / 2, 11);
+ g.drawString(minString, g.getWidth() / 2, g.getHeight() - 11);
+
+ g.setColor(normColor);
+
+ // TIME OF LAST MEASURE AND SIZE OF INTERVAL
+ g.setFontAlign(-1, 0);
+ g.drawString(data.t_value, 0, g.getHeight()/2 - 25);
+ g.setFontAlign(1, 0);
+ g.drawString('dt='+dt+'min', g.getWidth() , g.getHeight()/2 - 25);
+
+ ////////////////////////////////////////////////////////////
+ // GRAPH OF MEASUREMENT HISTORY
+ g.setFont("Vector:16");
+ const graphHeight=35;
+ const graphWidth=g.getWidth()-30;
+ const graphLocX = 15;
+ const graphLocY = g.getHeight() - 16 - 18 - graphHeight;
+
+ // DRAW SOME KIND OF AXES
+ g.setColor(0.4,0.4,0.4);
+ g.drawRect(graphLocX,graphLocY,graphLocX+graphWidth,graphLocY+graphHeight);
+ g.drawLine(graphLocX,graphLocY+graphHeight/2,graphLocX+graphWidth,graphLocY+graphHeight/2);
+ g.drawLine(graphLocX+graphWidth/2,graphLocY,graphLocX+graphWidth/2,graphLocY+graphHeight);
+ g.drawLine(graphLocX+graphWidth/4,graphLocY,graphLocX+graphWidth/4,graphLocY+graphHeight);
+ g.drawLine(graphLocX+3*graphWidth/4,graphLocY,graphLocX+3*graphWidth/4,graphLocY+graphHeight);
+ g.setColor(normColor);
+
+ // DRAW LINE
+ require("graph").drawLine(g, data.values, {
+ x:graphLocX,
+ y:graphLocY,
+ width:graphWidth,
+ height:graphHeight
+ });
+
+ let graphMax=Math.max.apply(Math,data.values);
+ let graphMin=Math.min.apply(Math,data.values);
+ g.setFontAlign(0, 0);
+ g.setColor(highColor);
+ g.drawString(graphMax.toFixed(data.accuracy), g.getWidth()/2, g.getHeight() - 16 - 18 - graphHeight);
+ g.setColor(lowColor);
+ g.drawString(graphMin.toFixed(data.accuracy), g.getWidth()/2, g.getHeight() - 16 - 18);
+ g.setColor(normColor);
+
+ let historyLength = (historyAmnt*dt >= 60)?('-'+historyAmnt*dt/60+'h'):('-'+historyAmnt*dt+'"');
+
+ g.drawString(historyLength,25, g.getHeight() - 16 - 18 - graphHeight/2);
+
+ ////////////////////////////////////////////////////////////
+ // LAST MEASURE
+ g.setFontAlign(0, 0);
+ g.setFont('Vector:36');
+ g.drawString(valueString, g.getWidth() / 2, g.getHeight() / 2);
+
+ data.ondisplay = true;
+}
+
+function updateMinMax( data, currentValue ){
+ data.values.push(currentValue);
+ data.values.shift();
+ data.value=currentValue;
+
+ let now = new Date();
+ data.t_value = now.getHours()+':'+String(now.getMinutes()).padStart(2, '0');
+ if (currentValue < data.minval){data.t_min=data.t_value;data.minval = currentValue;}
+ if (currentValue > data.maxval){data.t_max=data.t_value;data.maxval = currentValue;}
+}
+
+function switchDisplay(){
+ if (TData.ondisplay) {TData.ondisplay=false;PData.ondisplay=true;updateScreen();}
+ else {PData.ondisplay=false;TData.ondisplay=true;updateScreen();}
+}
+
+function settingsPage(){
+ Bangle.on('swipe',function (){});
+ eval(require("Storage").read("nightwatch.settings.js"))(()=>load());
+ Bangle.on('swipe',switchDisplay);
+ console.log(3);
+}
+
+function handlePressureSensorReading(data) {
+ updateMinMax(TData,data.temperature);
+ updateMinMax(PData,data.pressure);
+}
+
+function startup(){
+ // testing in emulator
+ // handlePressureSensorReading({ "temperature": 28.64251302083, "pressure": 1004.66520303803, "altitude": 71.72072902749 });
+ // updateScreen();
+
+ // ON STARTUP:
+ // fill current reading into data,
+ // before `updateMinMax` uses it
+ Bangle.getPressure().then(d=>{TData.value=d.temperature;
+ TData.values.fill(d.temperature);
+ PData.value=d.pressure;
+ PData.values.fill(d.pressure);
+ handlePressureSensorReading(d);
+ updateScreen();});
+ Bangle.on('swipe',switchDisplay);
+
+ //Bangle.on('tap',settingsPage);
+
+ timerID = setInterval( function() {
+ Bangle.getPressure().then(d=>{handlePressureSensorReading(d);updateScreen();});
+ }, dt * 60000);
+
+}
+
+startup();
+
diff --git a/apps/nightwatch/nightwatch.icon.js b/apps/nightwatch/nightwatch.icon.js
new file mode 100644
index 000000000..19b4623f0
--- /dev/null
+++ b/apps/nightwatch/nightwatch.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4MA///ospETUQAgc//gFDv4FF/wFP/4FF/5PCgIFChF/AoWA/1/+YFBx/+g4EBAAPAFAIEBEgUDBQYAN/E/AgQvDDoXHDocH4wFDgf8v4RCDooAMj/4AoZcBcM8DOAgFFgJSDAqQAhA=="))
diff --git a/apps/nightwatch/nightwatch.icon.png b/apps/nightwatch/nightwatch.icon.png
new file mode 100644
index 000000000..bf3a3282a
Binary files /dev/null and b/apps/nightwatch/nightwatch.icon.png differ
diff --git a/apps/nightwatch/nightwatch.info.js b/apps/nightwatch/nightwatch.info.js
new file mode 100644
index 000000000..ccbc8909a
--- /dev/null
+++ b/apps/nightwatch/nightwatch.info.js
@@ -0,0 +1,6 @@
+require("Storage").write("nightwatch.info",{
+ "id":"nightwatch",
+ "name":"The Nightwatch",
+ "src":"nightwatch.app.js",
+ "icon":"nightwatch.icon.png"
+});
diff --git a/apps/nightwatch/nightwatch.settings.js b/apps/nightwatch/nightwatch.settings.js
new file mode 100644
index 000000000..744ebd8dc
--- /dev/null
+++ b/apps/nightwatch/nightwatch.settings.js
@@ -0,0 +1,25 @@
+(function(back) {
+ var FILE = "nightwatch.json";
+ // Load settings
+ var settings = Object.assign({
+ dt: 5,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "nightwatch" },
+ "< Back" : () => back(),
+ 'log freq (min)': {
+ value: 0|settings.dt, // 0| converts undefined to 0
+ min: 1, max: 60,
+ onchange: v => {
+ settings.dt = v;
+ writeSettings();
+ }
+ },
+ });
+});
diff --git a/apps/nightwatch/screenshot.png b/apps/nightwatch/screenshot.png
new file mode 100644
index 000000000..3f524eac9
Binary files /dev/null and b/apps/nightwatch/screenshot.png differ
diff --git a/apps/nightwatch/screenshot2.png b/apps/nightwatch/screenshot2.png
new file mode 100644
index 000000000..6fe6faf41
Binary files /dev/null and b/apps/nightwatch/screenshot2.png differ
diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog
index 3d08bdf46..32951bda7 100644
--- a/apps/openstmap/ChangeLog
+++ b/apps/openstmap/ChangeLog
@@ -16,7 +16,7 @@
Support for zooming in on map
Satellite count moved to widget bar to leave more room for the map
0.15: Make track drawing an option (default off)
-0.16: Draw waypoints, too.
+0.16: Draw waypoints, too
0.17: With new Recorder app allow track to be drawn in the background
Switch tile layer URL for faster/more reliable map tiles
0.18: Prefer map with highest resolution
@@ -26,3 +26,7 @@
If 'Recorder' app installed, add a 'Record' menu item
0.21: Draw a current position marker (Bangle.js 2 only)
Enable/Disable previous position marker in new setting "Draw cont. position"
+0.22: Replace position marker with direction arrow
+0.23: Bugfix: Enable Compass if needed
+0.24: Allow zooming by clicking the screen
+0.25: Enable scaled image filtering on 2v19+ firmware
\ No newline at end of file
diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md
index bf247c7b7..d492bae05 100644
--- a/apps/openstmap/README.md
+++ b/apps/openstmap/README.md
@@ -22,6 +22,11 @@ quality, but uploads faster and takes less space). Bangle.js 2 is limited to 3bp
can change settings, move the map around, and click `Get Map` again.
* When you're ready, click `Upload`
+**Note:** By default on Bangle.js, pre-dithered 3 bpp bitmaps will be uploaded
+(which match the screen bit depth). However you can untick the `3 bit` checkbox
+to use 8 bit maps, which take up 2.6x more space but look much better when
+zoomed in/out.
+
## Bangle.js App
The Bangle.js app allows you to view a map. It also turns the GPS on
@@ -29,6 +34,7 @@ and marks the path that you've been travelling (if enabled), and
displays waypoints in the watch (if dependencies exist).
* Drag on the screen to move the map
+* Click bottom left to zoom in, bottom right to zoom out
* Press the button to bring up a menu, where you can zoom, go to GPS location,
put the map back in its default location, or choose whether to draw the currently
recording GPS track (from the `Recorder` app).
diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js
index 43c747ba7..9b53077ab 100644
--- a/apps/openstmap/app.js
+++ b/apps/openstmap/app.js
@@ -7,6 +7,15 @@ var hasScrolled = false;
var settings = require("Storage").readJSON("openstmap.json",1)||{};
var plotTrack;
let checkMapPos = false; // Do we need to check the if the coordinates we have are valid
+var startDrag = 0;
+
+if (Bangle.setLCDOverlay) {
+ // Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted
+ var imgLoc = require("heatshrink").decompress(atob("jEYwINLAQk8AQl+AQn/AQcB/+AAQUD//AAQUH//gAQUP//wAQUf//4j8AvA9IA=="));
+ // overlay buffer for current location, a bit bigger then image so we can rotate
+ const ovSize = Math.ceil(Math.sqrt(imgLoc[0]*imgLoc[0]+imgLoc[1]*imgLoc[1]));
+ var ovLoc = Graphics.createArrayBuffer(ovSize,ovSize,imgLoc[2] & 0x7f,{msb:true});
+}
if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) {
// restore last view
@@ -15,6 +24,9 @@ if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !
m.scale = settings.scale;
checkMapPos = true;
}
+if (settings.dirSrc === undefined) {
+ settings.dirSrc = 1; // Default=GPS
+}
// Redraw the whole page
function redraw() {
@@ -25,9 +37,9 @@ function redraw() {
m.lat = m.map.lat;
m.lon = m.map.lon;
m.scale = m.map.scale;
- checkMapPos = false;
m.draw();
}
+ checkMapPos = false;
drawPOI();
drawMarker();
drawLocation();
@@ -67,21 +79,28 @@ function drawPOI() {
})
}
-// Draw the marker for where we are
+function isInside(rect, e, w, h) {
+ return e.x-w/2>=rect.x && e.x+w/2=rect.y && e.y+h/2<=rect.y+rect.h;
+}
+
+// Draw the location & direction marker for where we are
function drawMarker() {
if (!fix.fix || !settings.drawMarker) return;
var p = m.latLonToXY(fix.lat, fix.lon);
- g.setColor(1,0,0);
- g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
+ if (isInside(R, p, 4, 4)) { // avoid drawing over widget area
+ g.setColor(1,0,0);
+ g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
+ }
}
-// Draw current location with LCD Overlay (Bangle.js 2 only)
+// Draw current location+direction with LCD Overlay (Bangle.js 2 only)
function drawLocation() {
if (!Bangle.setLCDOverlay) {
return; // Overlay not supported
}
- if (!fix.fix || !mapVisible) {
+ if (!fix.fix || !mapVisible || settings.dirSrc === 0) {
if (this.hasOverlay) {
Bangle.setLCDOverlay(); // clear if map is not visible or no fix
this.hasOverlay = false;
@@ -89,9 +108,21 @@ function drawLocation() {
return;
}
- const icon = require("heatshrink").decompress(atob("jEYwYPMyVJkgHEkgICyAHCgIIDyQIChIIEoAIDC4IIEBwOAgEEyVIBAY4DBD4sGHxBQIMRAIIPpAyCHAYILUJEAiVJkAIFgVJXo5fCABQA==")); // 24x24px
var p = m.latLonToXY(fix.lat, fix.lon);
- Bangle.setLCDOverlay(icon, p.x-24/2, p.y-24);
+
+ ovLoc.clear();
+ if (isInside(R, p, ovLoc.getWidth(), ovLoc.getHeight())) { // avoid drawing over widget area
+ const angle = settings.dirSrc === 1 ? fix.course : Bangle.getCompass().heading;
+ if (!isNaN(angle)) {
+ ovLoc.drawImage(imgLoc, ovLoc.getWidth()/2, ovLoc.getHeight()/2, {rotate: angle*Math.PI/180});
+ }
+ }
+ Bangle.setLCDOverlay({width:ovLoc.getWidth(), height:ovLoc.getHeight(),
+ bpp:ovLoc.getBPP(), transparent:0,
+ palette:new Uint16Array([0, g.toColor("#00F")]),
+ buffer:ovLoc.buffer
+ }, p.x-ovLoc.getWidth()/2, p.y-ovLoc.getHeight()/2);
+
this.hasOverlay = true;
}
@@ -104,6 +135,7 @@ Bangle.on('GPS',function(f) {
}
});
Bangle.setGPSPower(1, "app");
+Bangle.setCompassPower(settings.dirSrc === 2, "openstmap");
if (HASWIDGETS) {
Bangle.loadWidgets();
@@ -158,13 +190,32 @@ function showMenu() {
value : !!settings.drawMarker,
onchange : v => { settings.drawMarker=v; writeSettings(); }
},
- /*LANG*/"Center Map": () =>{
+ });
+
+ if (Bangle.setLCDOverlay) {
+ menu[/*LANG*/"Direction source"] = {
+ value: settings.dirSrc,
+ min: 0, max: 2,
+ format: v => [/*LANG*/"None", /*LANG*/"GPS", /*LANG*/"Compass"][v],
+ onchange: v => {
+ settings.dirSrc = v;
+ Bangle.setCompassPower(settings.dirSrc === 2, "openstmap");
+ writeSettings();
+ }
+ };
+ menu[/*LANG*/"Reset compass"] = () => {
+ Bangle.resetCompass();
+ showMap();
+ };
+ }
+
+ menu[/*LANG*/"Center Map"] = () =>{
m.lat = m.map.lat;
m.lon = m.map.lon;
m.scale = m.map.scale;
showMap();
- }
- });
+ };
+
// If we have the recorder widget, add a menu item to start/stop recording
if (WIDGETS.recorder) {
menu[/*LANG*/"Record"] = {
@@ -186,6 +237,8 @@ function showMap() {
Bangle.setUI({mode:"custom",drag:e=>{
if (plotTrack && plotTrack.stop) plotTrack.stop();
if (e.b) {
+ if (!startDrag)
+ startDrag = getTime();
g.setClipRect(R.x,R.y,R.x2,R.y2);
g.scroll(e.dx,e.dy);
m.scroll(e.dx,e.dy);
@@ -193,7 +246,19 @@ function showMap() {
hasScrolled = true;
drawLocation();
} else if (hasScrolled) {
+ delta = getTime() - startDrag;
+ startDrag = 0;
hasScrolled = false;
+ if (delta < 0.2) {
+ if (e.y > g.getHeight() / 2) {
+ if (e.x < g.getWidth() / 2) {
+ m.scale /= 2;
+ } else {
+ m.scale *= 2;
+ }
+ }
+ g.reset().clearRect(R);
+ }
redraw();
}
}, btn: () => showMenu() });
diff --git a/apps/openstmap/interface.html b/apps/openstmap/interface.html
index 0d9ef3152..9e22c57e4 100644
--- a/apps/openstmap/interface.html
+++ b/apps/openstmap/interface.html
@@ -124,10 +124,11 @@ TODO:
// ---------------------------------------- Run at startup
function onInit(device) {
if (device && device.info && device.info.g) {
- // On 3 bit devices, don't even offer the option. 3 bit is the only way
+ // On 3 bit devices, 3 bit is the best way
+ // still allow 8 bit as it makes zoom out much nicer
if (device.info.g.bpp==3) {
document.getElementById("3bit").checked = true;
- document.getElementById("3bitdiv").style = "display:none";
+ //document.getElementById("3bitdiv").style = "display:none";
}
}
@@ -258,15 +259,16 @@ TODO:
mode:"3bit",
diffusion:"bayer2"
};
- /* If in 3 bit mode, go through all the data beforehand and
- turn the saturation up to maximum, so when thresholded it
- works a lot better */
- var imageData = ctx.getImageData(0,0,width,height);
- var dstData = ctx.createImageData(width, height);
- var filterOptions = {};
- imageFilterFor3BPP(imageData, dstData, filterOptions);
- ctx.putImageData(dstData,0,0);
}
+ /* Go through all the data beforehand and
+ turn the saturation up to maximum, so if thresholded to 3 bits it
+ works a lot better */
+ var imageData = ctx.getImageData(0,0,width,height);
+ var dstData = ctx.createImageData(width, height);
+ var filterOptions = {};
+ imageFilterFor3BPP(imageData, dstData, filterOptions);
+ ctx.putImageData(dstData,0,0);
+
console.log("Compression options", options);
var w = Math.round(width / TILESIZE);
var h = Math.round(height / TILESIZE);
diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json
index 1b4ab2d7c..05dcf2709 100644
--- a/apps/openstmap/metadata.json
+++ b/apps/openstmap/metadata.json
@@ -2,7 +2,7 @@
"id": "openstmap",
"name": "OpenStreetMap",
"shortName": "OpenStMap",
- "version": "0.21",
+ "version": "0.25",
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
"readme": "README.md",
"icon": "app.png",
diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js
index 0a8f35f66..0a36b829e 100644
--- a/apps/openstmap/openstmap.js
+++ b/apps/openstmap/openstmap.js
@@ -52,6 +52,7 @@ exports.draw = function() {
if (d!=1) { // if the two are different, add scaling
s *= d;
o.scale = d;
+ o.filter = true; // on 2v19+ enables supersampling
}
//console.log(ix,iy);
var tx = 0|(ix/s);
diff --git a/apps/orloj/ChangeLog b/apps/orloj/ChangeLog
new file mode 100644
index 000000000..263d4078d
--- /dev/null
+++ b/apps/orloj/ChangeLog
@@ -0,0 +1 @@
+0.01: attempt to import
diff --git a/apps/orloj/README.md b/apps/orloj/README.md
new file mode 100644
index 000000000..4da3f6a98
--- /dev/null
+++ b/apps/orloj/README.md
@@ -0,0 +1,25 @@
+# Orloj 
+
+Astronomical clock.
+
+Written by: [Pavel Machek](https://github.com/pavelmachek)
+
+The plan is to have an (analog) astronomical clock with a lot of
+information on single dial.
+
+It continuously displays information that can be obtained "cheaply",
+that is current time, sunset/sunrise times, battery status and
+altitude. One-second updates with useful compass can be activated by
+tapping bottom right corner.
+
+Display is split in three rings. Outside ring is for time-based data
+with base of one week, and for non time-based data. Black dot
+indicates day of week. Green foot indicates number of steps taken, red
+battery symbol indicates remaining charge, black thermometer symbol
+represents temperature, and black ruler symbol indicates
+altitude. Number in bottom left corner is day of month.
+
+In the middle ring, hour-based data are displayed. Black dot indicates
+current hour, yellow symbols indicate sunset and sunrise, and black
+symbols indicate moonset and moonrise.
+
diff --git a/apps/orloj/app-icon.js b/apps/orloj/app-icon.js
new file mode 100644
index 000000000..4663c8266
--- /dev/null
+++ b/apps/orloj/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkB/4A+gAXWh4YWh4BCC6vwBxoIJGBYXXHhAUCJBYXXHhAXaVBgkHC7JGMEpDvQC48ACxoXHCx5WPC8BvPC8BgwgAAFC65nQ+AvWAFSjYaiyERUQ7QYGKS4EOyguUC7h4VFoIXUIgbBWAH4A/AH4APA=="))
diff --git a/apps/orloj/app.js b/apps/orloj/app.js
new file mode 100644
index 000000000..8dd1cd571
--- /dev/null
+++ b/apps/orloj/app.js
@@ -0,0 +1,407 @@
+const SunCalc = require("suncalc"); // from modules folder
+
+// ################################################################################
+
+let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2;
+let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2;
+let outerRadius = Math.min(CenterX,CenterY) * 0.9;
+
+const lat = 50.1;
+const lon = 14.45;
+
+const h = g.getHeight();
+const w = g.getWidth();
+const sm = 15;
+let settings, location, mode = 0;
+var altitude, temperature;
+
+var img_north = Graphics.createImage(`
+ X
+ XXX
+ XXX
+ X XXX
+ X XXX
+ X XXXX
+ X XXXX
+X XXXXX
+X XXXXX
+XXXXXXXXX
+`);
+
+var img_sunrise = Graphics.createImage(`
+ XXX
+ XXXXX
+XXXXXXXXX
+`);
+
+var img_moonrise = Graphics.createImage(`
+ XXX
+ XX X
+XXXXXXXXX
+`);
+
+var img_altitude = Graphics.createImage(`
+X X
+X X X
+XXXXXXXXX
+X X X
+X X
+`);
+
+var img_temperature = Graphics.createImage(`
+ XX
+XXXXXXXX
+X XX
+XXXXXXXX
+ XX
+`);
+
+var img_battery = Graphics.createImage(`
+XXXXXXXX
+XXX X
+XXXX XX
+XXXXX X
+XXXXXXXX
+`);
+
+var img_step = Graphics.createImage(`
+ XXX
+ XX XXXXX
+XXX XXXXX
+XXX XXXXX
+ XX XXXX
+`);
+
+var img_sun = Graphics.createImage(`
+X X
+ XXX
+ XXXXXXX
+XXXXXXXXX
+XXXXXXXXX
+XXXXXXXXX
+ XXXXXXX
+ XXX
+X X
+`);
+
+var img_moon = Graphics.createImage(`
+ XXX
+ XX XXX
+X XXXX
+X XXX
+X XXX
+X XXX
+X XXXX
+ X XXX
+ XXX
+`);
+
+let use_compass = 0;
+
+function draw() {
+ drawBorders();
+ queueDraw();
+}
+
+function radA(p) { return p*(Math.PI*2); }
+function radD(d) { return d*(h/2); }
+
+function radX(p, d) {
+ let a = radA(p);
+ return h/2 + Math.sin(a)*radD(d);
+}
+
+function radY(p, d) {
+ let a = radA(p);
+ return w/2 - Math.cos(a)*radD(d);
+}
+
+function fracHour(d) {
+ let hour = d.getHours();
+ let min = d.getMinutes();
+ hour = hour + min/60;
+ if (hour > 12)
+ hour -= 12;
+ return hour;
+}
+
+ let HourHandLength = outerRadius * 0.5;
+ let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2;
+
+ let MinuteHandLength = outerRadius * 0.7;
+ let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2;
+
+ let SecondHandLength = outerRadius * 0.9;
+ let SecondHandOffset = 6;
+
+ let twoPi = 2*Math.PI;
+ let Pi = Math.PI;
+ let halfPi = Math.PI/2;
+
+ let sin = Math.sin, cos = Math.cos;
+
+ let HourHandPolygon = [
+ -halfHourHandWidth,halfHourHandWidth,
+ -halfHourHandWidth,halfHourHandWidth-HourHandLength,
+ halfHourHandWidth,halfHourHandWidth-HourHandLength,
+ halfHourHandWidth,halfHourHandWidth,
+ ];
+
+ let MinuteHandPolygon = [
+ -halfMinuteHandWidth,halfMinuteHandWidth,
+ -halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
+ halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
+ halfMinuteHandWidth,halfMinuteHandWidth,
+ ];
+
+/**** drawClockFace ****/
+
+ function drawClockFace () {
+ g.setColor(g.theme.fg);
+ g.setFont('Vector', 22);
+
+ g.setFontAlign(0,-1);
+ g.drawString('12', CenterX,CenterY-outerRadius);
+
+ g.setFontAlign(1,0);
+ g.drawString('3', CenterX+outerRadius,CenterY);
+
+ g.setFontAlign(0,1);
+ g.drawString('6', CenterX,CenterY+outerRadius);
+
+ g.setFontAlign(-1,0);
+ g.drawString('9', CenterX-outerRadius,CenterY);
+ }
+
+/**** transforme polygon ****/
+
+ let transformedPolygon = new Array(HourHandPolygon.length);
+
+ function transformPolygon (originalPolygon, OriginX,OriginY, Phi) {
+ let sPhi = sin(Phi), cPhi = cos(Phi), x,y;
+
+ for (let i = 0, l = originalPolygon.length; i < l; i+=2) {
+ x = originalPolygon[i];
+ y = originalPolygon[i+1];
+
+ transformedPolygon[i] = OriginX + x*cPhi + y*sPhi;
+ transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi;
+ }
+ }
+
+/**** draw clock hands ****/
+
+ function drawClockHands () {
+ let now = new Date();
+
+ let Hours = now.getHours() % 12;
+ let Minutes = now.getMinutes();
+ let Seconds = now.getSeconds();
+
+ let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi;
+ let MinutesAngle = (Minutes/60) * twoPi - Pi;
+ let SecondsAngle = (Seconds/60) * twoPi - Pi;
+
+ g.setColor(g.theme.fg);
+
+ transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle);
+ g.fillPoly(transformedPolygon);
+
+ transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle);
+ g.fillPoly(transformedPolygon);
+
+ let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle);
+
+ g.setColor(g.theme.fg2);
+ g.drawLine(
+ CenterX + SecondHandOffset*sPhi,
+ CenterY - SecondHandOffset*cPhi,
+ CenterX - SecondHandLength*sPhi,
+ CenterY + SecondHandLength*cPhi
+ );
+
+ g.setFont('Vector', 22);
+
+ g.setFontAlign(-1, 1);
+ g.drawString(now.getDate(), CenterX-outerRadius,CenterY+outerRadius);
+
+ }
+
+function drawTimeIcon(time, icon, options) {
+ let h = fracHour(time);
+ let x = radX(h/12, 0.7);
+ let y = radY(h/12, 0.7);
+ g.drawImage(icon, x,y, options);
+}
+
+function drawOutsideIcon(h, icon, options) {
+ let x = radX(h, 0.95);
+ let y = radY(h, 0.95);
+ g.drawImage(icon, x,y, options);
+}
+
+function drawBorders() {
+ g.reset();
+ g.setColor(0);
+ g.fillRect(Bangle.appRect);
+
+ g.setColor(-1);
+ g.fillCircle(w/2, h/2, h/2 - 2);
+ if (0) {
+ g.fillCircle(sm+1, sm+1, sm);
+ g.fillCircle(sm+1, h-sm-1, sm);
+ g.fillCircle(w-sm-1, h-sm-1, sm);
+ g.fillCircle(h-sm-1, sm+1, sm);
+ }
+ g.setColor(0, 1, 0);
+ g.drawCircle(h/2, w/2, radD(0.7));
+ g.drawCircle(h/2, w/2, radD(0.5));
+
+ outerRadius = radD(0.7);
+ drawClockHands();
+
+ let d = new Date();
+ let hour = fracHour(d);
+ let min = d.getMinutes();
+ let day = d.getDay();
+ day = day + hour/24;
+ {
+ let x = radX(hour/12, 0.7);
+ let y = radY(hour/12, 0.7);
+ g.setColor(0, 0, 0);
+ g.fillCircle(x,y, 5);
+ }
+ {
+ let x = radX(min/60, 0.5);
+ let y = radY(min/60, 0.5);
+ g.setColor(0, 0, 0);
+ g.drawLine(h/2, w/2, x, y);
+ }
+ {
+ let x = radX(hour/12, 0.3);
+ let y = radY(hour/12, 0.3);
+ g.setColor(0, 0, 0);
+ g.drawLine(h/2, w/2, x, y);
+ }
+ {
+ let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps;
+ let x = radX(km/12 + 0, 0.95);
+ let y = radY(km/12 + 0, 0.95);
+ g.setColor(0, 0.7, 0);
+ g.drawImage(img_step, x,y, { scale: 2, rotate: Math.PI*0.0 } );
+ }
+ {
+ let bat = E.getBattery();
+ let x = radX(bat/100, 0.95);
+ let y = radY(bat/100, 0.95);
+ g.setColor(0.7, 0, 0);
+ g.drawImage(img_battery, x,y, { scale: 2, rotate: Math.PI*0.0 } );
+ }
+ {
+ d = new Date();
+ sun = SunCalc.getTimes(d, lat, lon);
+ g.setColor(0.5, 0.5, 0);
+ print("sun", sun);
+ drawTimeIcon(sun.sunset, img_sunrise, { rotate: Math.PI, scale: 2 });
+ drawTimeIcon(sun.sunrise, img_sunrise, { scale: 2 });
+ g.setColor(0, 0, 0);
+ moon = SunCalc.getMoonTimes(d, lat, lon);
+ print("moon", moon);
+ drawTimeIcon(moon.set, img_moonrise, { rotate: Math.PI, scale: 2 });
+ drawTimeIcon(moon.rise, img_sunrise, { scale: 2 });
+ pos = SunCalc.getPosition(d, lat, lon);
+ print("sun:", pos);
+ if (pos.altitude > -0.1) {
+ g.setColor(0.5, 0.5, 0);
+ az = pos.azimuth;
+ drawOutsideIcon(az / (2*Math.PI), img_sun, { scale: 2 });
+ }
+ pos = SunCalc.getMoonPosition(d, lat, lon);
+ print("moon:", pos);
+ if (pos.altitude > -0.05) {
+ g.setColor(0, 0, 0);
+ az = pos.azimuth;
+ drawOutsideIcon(az / (2*Math.PI), img_moon, { scale: 2 });
+ }
+ }
+ {
+ Bangle.getPressure().then((x) =>
+ { altitude = x.altitude; temperature = x.temperature; },
+ print);
+ print(altitude, temperature);
+ drawOutsideIcon(altitude / 120, img_altitude, { scale: 2 });
+ drawOutsideIcon(temperature / 12, img_temperature, { scale: 2 });
+ }
+ if (use_compass) {
+ let obj = Bangle.getCompass();
+ if (obj) {
+ let h = 360-obj.heading;
+ let x = radX(h/360, 0.7);
+ let y = radY(h/360, 0.7);
+ g.setColor(0, 0, 1);
+ g.drawImage(img_north, x,y, {scale:2});
+ }
+ }
+ {
+ let x = radX(day/7, 0.95);
+ let y = radY(day/7, 0.95);
+ g.setColor(0, 0, 0);
+ g.fillCircle(x,y, 5);
+ }
+}
+
+function drawEmpty() {
+ g.reset();
+ g.setColor(g.theme.bg);
+ g.fillRect(Bangle.appRect);
+}
+
+Bangle.on('touch', function(button, xy) {
+ var x = xy.x;
+ var y = xy.y;
+ if (y > h) y = h;
+ if (y < 0) y = 0;
+ if (x > w) x = w;
+ if (x < 0) x = 0;
+});
+
+// if we get a step then we are not idle
+Bangle.on('step', s => {
+});
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ next = 60000;
+ if (use_compass) next = 250;
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, next - (Date.now() % next));
+}
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+Bangle.setUI("clockupdown", btn=> {
+ if (btn<0) use_compass = 0;
+ if (btn>0) use_compass = 1;
+ Bangle.setCompassPower(use_compass, 'orloj');
+ draw();
+});
+
+if (use_compass)
+ Bangle.setCompassPower(true, 'orloj');
+g.clear();
+draw();
+
diff --git a/apps/orloj/app.png b/apps/orloj/app.png
new file mode 100644
index 000000000..fced2ce5e
Binary files /dev/null and b/apps/orloj/app.png differ
diff --git a/apps/orloj/metadata.json b/apps/orloj/metadata.json
new file mode 100644
index 000000000..a59d2e6dd
--- /dev/null
+++ b/apps/orloj/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "orloj",
+ "name": "Orloj",
+ "version":"0.01",
+ "description": "Astronomical clock",
+ "icon": "app.png",
+ "readme": "README.md",
+ "supports" : ["BANGLEJS2"],
+ "type": "clock",
+ "tags": "clock",
+ "storage": [
+ {"name":"orloj.app.js","url":"app.js"},
+ {"name":"orloj.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/pebblepp/ChangeLog b/apps/pebblepp/ChangeLog
index f3886a42e..83be498bd 100644
--- a/apps/pebblepp/ChangeLog
+++ b/apps/pebblepp/ChangeLog
@@ -1,4 +1,5 @@
0.01: First release
0.02: clock_info now uses app name to maintain settings specifically for this clock face
ensure clockinfo text is usppercase (font doesn't render lowercase)
-0.03: Use smaller font if clock_info test doesn't fit in area
\ No newline at end of file
+0.03: Use smaller font if clock_info test doesn't fit in area
+0.04: Ensure we only scale down clockinfo text if it really won't fit
\ No newline at end of file
diff --git a/apps/pebblepp/app.js b/apps/pebblepp/app.js
index 5121f450e..330d79618 100644
--- a/apps/pebblepp/app.js
+++ b/apps/pebblepp/app.js
@@ -97,10 +97,10 @@ let clockInfoDraw = (itm, info, options) => {
}
g.setFontLECO1976Regular22().setFontAlign(0, 0);
var txt = info.text.toString().toUpperCase();
- if (g.stringWidth(txt) > options.w-4) // if too big, smaller font
+ if (g.stringWidth(txt) > options.w) // if too big, smaller font
g.setFontLECO1976Regular14();
- if (g.stringWidth(txt) > options.w-4) {// if still too big, split to 2 lines
- var l = g.wrapString(txt, options.w-2);
+ if (g.stringWidth(txt) > options.w) {// if still too big, split to 2 lines
+ var l = g.wrapString(txt, options.w);
txt = l.slice(0,2).join("\n") + (l.length>2)?"...":"";
}
g.drawString(txt, midx,options.y+options.h-12); // draw the text
diff --git a/apps/pebblepp/metadata.json b/apps/pebblepp/metadata.json
index 881b558db..e2be44ea0 100644
--- a/apps/pebblepp/metadata.json
+++ b/apps/pebblepp/metadata.json
@@ -2,7 +2,7 @@
"id": "pebblepp",
"name": "Pebble++ Clock",
"shortName": "Pebble++",
- "version": "0.03",
+ "version": "0.04",
"description": "A pebble style clock (based on the 'Pebble Clock' app) but with two configurable ClockInfo items at the top",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/poweroff/ChangeLog b/apps/poweroff/ChangeLog
index 1a3bc1757..38e6128f6 100644
--- a/apps/poweroff/ChangeLog
+++ b/apps/poweroff/ChangeLog
@@ -1 +1,3 @@
0.01: New app!
+0.02: Add prompt before shutdown
+0.03: Add settings to configure prompt
diff --git a/apps/poweroff/app.js b/apps/poweroff/app.js
index 303e78d03..6e14b1a44 100644
--- a/apps/poweroff/app.js
+++ b/apps/poweroff/app.js
@@ -1,13 +1,45 @@
+{ // must be inside our own scope here so that when we are unloaded everything disappears
g.clear();
-g.setFont("6x8",2).setFontAlign(0,0);
+let confirmed = false;
+// Helper function default setting
+let def = function(value, def) {
+ return value !== undefined ? value : def;
+};
+let settings = require('Storage').readJSON("poweroff.json", true) || {};
+let showPrompt;
+showPrompt = def(settings.showPrompt, true);
+
+if (showPrompt) {
+ Bangle.setLocked(false); // handy when debugging via IDE
+ E.showPrompt('Are you sure?', {
+ title: 'Power off',
+ buttons: { Yes: true, No: false },
+ }).then((confirm) => {
+ if (!confirm) {
+ setTimeout(load, 100);
+ return;
+ }
+ g.setFont("6x8",2).setFontAlign(0,0);
var x = g.getWidth()/2;
var y = g.getHeight()/2 + 10;
g.drawString("Powering off...", x, y);
-setTimeout(function() {
- if (Bangle.softOff) Bangle.softOff(); else Bangle.off();
-}, 1000);
+ setTimeout(function() {
+ if (Bangle.softOff) Bangle.softOff(); else Bangle.off();
+ }, 1000);
+ });
+} else {
+ g.setFont("6x8",2).setFontAlign(0,0);
+ var x = g.getWidth()/2;
+ var y = g.getHeight()/2 + 10;
+ g.drawString("Powering off...", x, y);
+
+ setTimeout(function() {
+ if (Bangle.softOff) Bangle.softOff(); else Bangle.off();
+ }, 1000);
+}
Bangle.loadWidgets();
Bangle.drawWidgets();
+}
\ No newline at end of file
diff --git a/apps/poweroff/metadata.json b/apps/poweroff/metadata.json
index 80e71a1d2..254127935 100644
--- a/apps/poweroff/metadata.json
+++ b/apps/poweroff/metadata.json
@@ -1,7 +1,7 @@
{ "id": "poweroff",
"name": "Poweroff",
"shortName":"Poweroff",
-"version":"0.01",
+"version":"0.03",
"description": "Simple app to power off your Bangle.js",
"icon": "app.png",
"tags": "tool, poweroff, shutdown",
@@ -10,6 +10,10 @@
"allow_emulator": true,
"storage": [
{"name":"poweroff.app.js","url":"app.js"},
- {"name":"poweroff.img","url":"app-icon.js","evaluate":true}
-]
+ {"name":"poweroff.img","url":"app-icon.js","evaluate":true},
+ {"name":"poweroff.settings.js","url":"settings.js"}
+],
+"data": [
+ {"name":"poweroff.json"}
+ ]
}
diff --git a/apps/poweroff/settings.js b/apps/poweroff/settings.js
new file mode 100644
index 000000000..b22a7918a
--- /dev/null
+++ b/apps/poweroff/settings.js
@@ -0,0 +1,46 @@
+(function(back) {
+ var FILE = "poweroff.json";
+ var settings = Object.assign({
+ showPrompt: true,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Helper method which uses int-based menu item for set of string values
+ function stringItems(startvalue, writer, values) {
+ return {
+ value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
+ format: v => values[v],
+ min: 0,
+ max: values.length - 1,
+ wrap: true,
+ step: 1,
+ onchange: v => {
+ writer(values[v]);
+ writeSettings();
+ }
+ };
+ }
+
+ // Helper method which breaks string set settings down to local settings object
+ function stringInSettings(name, values) {
+ return stringItems(settings[name], v => settings[name] = v, values);
+ }
+
+ var mainmenu = {
+ "": {
+ "title": "Poweroff"
+ },
+ "< Back": () => back(),
+ "Show prompt": {
+ value: (settings.showPrompt !== undefined ? settings.showPrompt : true),
+ onchange: v => {
+ settings.showPrompt = v;
+ writeSettings();
+ }
+ }
+ };
+ E.showMenu(mainmenu);
+});
diff --git a/apps/rebbleagenda/ChangeLog b/apps/rebbleagenda/ChangeLog
new file mode 100644
index 000000000..ec66c5568
--- /dev/null
+++ b/apps/rebbleagenda/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial version
diff --git a/apps/rebbleagenda/README.md b/apps/rebbleagenda/README.md
new file mode 100644
index 000000000..77afd4b48
--- /dev/null
+++ b/apps/rebbleagenda/README.md
@@ -0,0 +1,24 @@
+# Rebble Agenda
+
+Agenda app for showing upcoming events in an animated fashion.
+Heavily inspired by the inbuilt agenda of the pebble time.
+Switch between calendar events by swiping up or down. Click the button to exit.
+
+  
+
+## Settings
+
+- *Use system theme* - Use the colors of the system theme. Otherwise use following colors.
+- *Accent* - The color of the rightmost accent bar if not following system theme.
+- *Background* - The background color to use if not following system theme.
+- *Foreground* - The foreground color to use if not following system theme.
+
+## Notes
+
+- The weather icon in the top right corner is currently just showing the current weather as provided by [weather](https://github.com/espruino/BangleApps/blob/master/apps/weather/). Closest forecast to be implemented in a future release.
+- Events only show as much of their title and description as can be fit on the screen, which is one and four (wrapped) lines respectively.
+- Events are loaded from ```android.calendar.json```, which is read in its entirety. If you have a very busy schedule, loading may take a second or two.
+
+## Creator
+
+- [Sarah Alrøe](https://github.com/SarahAlroe), August+September 2023
diff --git a/apps/rebbleagenda/app-icon.js b/apps/rebbleagenda/app-icon.js
new file mode 100644
index 000000000..d432f8179
--- /dev/null
+++ b/apps/rebbleagenda/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4v/F6OIAAgHRF/4v/F/4v/CI4AdF/6HR3YAEF/4v/F/4v/F5IAdF/4v/F/4vJGEguKGEYuMAH4A/AH4A/ADIA=="))
\ No newline at end of file
diff --git a/apps/rebbleagenda/app.js b/apps/rebbleagenda/app.js
new file mode 100644
index 000000000..3b6eca900
--- /dev/null
+++ b/apps/rebbleagenda/app.js
@@ -0,0 +1,583 @@
+{
+ /* Requires */
+ const weather = require('weather');
+ require("Font6x12").add(Graphics);
+ require("Font8x16").add(Graphics);
+ const SETTINGS_FILE = "rebbleagenda.json";
+ const settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {'system':true, 'bg': '#fff','fg': '#000','acc': '#0FF'};
+
+ /* Layout consts */
+ const MARKER_SIZE = 4;
+ const BORDER_SIZE = 6;
+ const WIDGET_SIZE = 24;
+ const PRIMARY_OFFSET = WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE - 20 / 2;
+ const SECONDARY_OFFSET = g.getHeight() - WIDGET_SIZE - 16 - 20;
+ const MARKER_POS_UPPER = Uint8Array([g.getWidth() - BORDER_SIZE - MARKER_SIZE, WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE]);
+ const PIN_SIZE = 10;
+ const ACCENT_WIDTH = 2 * BORDER_SIZE + 2 * MARKER_SIZE; // �=2r, borders each side.
+
+ const TEXT_COLOR = settings.system?g.theme.fg:settings.fg;
+ const BG_COLOR = settings.system?g.theme.bg:settings.bg;
+ const ACCENT_COLOR = settings.system?g.theme.bgH:settings.acc;
+ const SUN_COLOR_START = 0xF800;
+ const SUN_COLOR_END = 0xFFE0;
+ const SUN_FACE = 0x0000;
+
+ /* Animation polygon sets*/
+ const CLEAR_POLYS_1 = [
+ new Uint8Array([0, 176, 0, 0, 176, 0, 176, 0, 0, 0, 0, 176]),
+ new Uint8Array([0, 176, 0, 0, 176, 0, 170, 7, 10, 12, 7, 168]),
+ new Uint8Array([0, 176, 0, 0, 176, 0, 139, 49, 41, 45, 43, 125]),
+ new Uint8Array([0, 176, 0, 0, 176, 0, 90, 81, 82, 86, 85, 94]),
+ new Uint8Array([0, 176, 0, 0, 176, 0, 91, 85, 85, 85, 85, 91])
+ ];
+
+ const CLEAR_POLYS_2 = [
+ new Uint8Array([0, 176, 176, 176, 176, 0, 176, 0, 176, 176, 0, 176]),
+ new Uint8Array([0, 176, 176, 176, 176, 0, 170, 7, 162, 161, 7, 168]),
+ new Uint8Array([0, 176, 176, 176, 176, 0, 139, 49, 130, 126, 43, 125]),
+ new Uint8Array([0, 176, 176, 176, 176, 0, 90, 81, 95, 89, 85, 94]),
+ new Uint8Array([0, 176, 176, 176, 176, 0, 91, 85, 91, 91, 85, 91])
+ ];
+
+ const BREATHING_POLYS = [
+ new Uint8Array([72, 88, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 84, 88]),
+ new Uint8Array([63, 88, 64, 73, 78, 73, 78, 73, 78, 73, 78, 73, 92, 73, 93, 88]),
+ new Uint8Array([60, 88, 56, 76, 78, 60, 78, 60, 78, 60, 78, 60, 100, 76, 96, 88]),
+ new Uint8Array([56, 88, 50, 78, 64, 54, 78, 54, 78, 54, 92, 54, 106, 78, 100, 88]),
+ new Uint8Array([53, 88, 47, 80, 52, 53, 78, 41, 78, 41, 104, 53, 109, 80, 103, 88]),
+ new Uint8Array([50, 88, 43, 81, 43, 51, 63, 32, 92, 32, 113, 51, 113, 81, 106, 88])];
+ const SUN_EYE_LEFT_POLY = new Uint8Array([56, 52, 64, 44, 72, 52, 72, 55, 69, 54, 64, 50, 58, 55, 56, 55]);
+ const SUN_EYE_RIGHT_OFFSET = 30;
+ const MOUTH_POLY = new Uint8Array([78, 77, 68, 75, 67, 73, 69, 71, 78, 73, 87, 71, 89, 73, 88, 75]);
+
+ /* Animation timings */
+ const TIME_CLEAR_ANIM = 400;
+ const TIME_CLEAR_BREAK = 10;
+ const TIME_DEFAULT_ANIM = 300;
+ const TIME_BUMP_ANIM = 200;
+ const TIME_EXIT_ANIM = 500;
+ const TIME_EVENT_CHANGE = 150;
+ const TIME_EVENT_BREAK_IN = 300;
+ const TIME_EVENT_BREAK_ANIM = 800;
+ const TIME_EVENT_BREAK_HALT = 500;
+ const TIME_EVENT_BREAK_OUT = 500;
+
+ /* Utility functions */
+
+ /**
+ * Check if two dates occur on the same day
+ * @param {Date} d1 The first date to compare
+ * @param {Date} d2 The second date to compare
+ * @returns {Boolean} The two dates are on the same day
+ */
+ const isSameDay = function (d1, d2) {
+ return (d1.getDate() == d2.getDate() && d1.getMonth() == d2.getMonth() && d1.getFullYear() == d2.getFullYear());
+ };
+
+ /**
+ * Apply sinusoidal easing to a value 0-1
+ * @param {Number} x Number to ease
+ * @returns {Number} Ease of x
+ */
+ const ease = function (x) {
+ "jit";
+ return 1 - (Math.cos(Math.PI * x) + 1) / 2;
+ };
+
+ /**
+ * Map from 0-1 to a number interval
+ * @param {Number} outMin Minimum output number
+ * @param {Number} outMax Maximum output number
+ * @param {Number} x Number between 0 and 1 to map from
+ * @returns {Number} x mapped between min and max
+ */
+ const map = function (outMin, outMax, x) {
+ "jit";
+ return outMin + x * (outMax - outMin);
+ };
+
+ /**
+ * Return [0-1] progress through an interval
+ * @param {Number} start When the interval was started in ms
+ * @param {Number} end When the interval is supposed to stop in ms
+ * @returns {Number} Value between 0 and 1 reflecting progress through interval
+ */
+ const timeProgress = function (start, end) {
+ "jit";
+ const length = end - start;
+ const delta = Date.now() - start;
+ return Math.min(Math.max(delta / length, 0), 1);
+ };
+
+ /**
+ * Interpolate between sets of polygon coordinates
+ * @param {Array} polys An array of arrays, each containing an equally long set of coordinates
+ * @param {Number} pos Progress through interpolation [0-1]
+ * @returns {Array} Interpolation between the two closest sets of coordinates
+ */
+ const interpolatePoly = function (polys, pos) {
+ const span = polys.length - 1;
+ pos = pos * span;
+ pos = pos > span ? span : pos;
+ const upper = polys[Math.ceil(pos)];
+ const lower = polys[Math.floor(Math.max(pos - 0.000001, 0))];
+ const interp = pos - Math.floor(pos - 0.000001);
+ return upper.map((up, i) => {
+ return Math.round(up * interp + lower[i] * (1 - interp));
+ });
+ };
+
+ /**
+ * Repeatedly call callback with progress through an interval of length time
+ * @param {Function} anim Callback which takes i, animation progress [0-1]
+ * @param {Number} time How many ms the animation should last
+ * @returns {void}
+ */
+ const doAnim = function (anim, time) {
+ const animStart = Date.now();
+ const animEnd = animStart + time;
+ let i = 0;
+ do {
+ i = timeProgress(animStart, animEnd);
+ anim(i);
+ } while (i < 1);
+ anim(1);
+ };
+
+ /* Screen draw functions */
+
+ /**
+ * Draw an event
+ * @param {Number} index Index in the events array of event to draw
+ * @param {Number} yOffset Vertical pixel offset of the draw
+ * @param {Boolean} drawSecondary Should secondary event be drawn if possible?
+ */
+ const drawEvent = function (index, yOffset, drawSecondary) {
+ g.setColor(TEXT_COLOR);
+ // Draw the event time
+ g.setFontAlign(-1, -1, 0);
+ g.setFont("Vector", 20);
+ g.drawString(events[index].time, BORDER_SIZE, PRIMARY_OFFSET + yOffset);
+
+ // Draw the event title
+ g.setFont("8x16");
+ g.drawString(events[index].title, BORDER_SIZE, PRIMARY_OFFSET + 20 + yOffset);
+
+ // And the event description
+ g.setFont("6x12");
+ g.drawString(events[index].description, BORDER_SIZE, PRIMARY_OFFSET + 20 + 12 + 2 + yOffset);
+
+ // Draw a secondary event if asked to and exists
+ if (drawSecondary) {
+ if (index + 1 < events.length) {
+ if (events[index].date != events[index + 1].date) {
+ // If event belongs to another day, draw circle
+ g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE + yOffset, MARKER_SIZE);
+ } else {
+ // Draw event time and title
+ g.setFont("Vector", 20);
+ g.drawString(events[index + 1].time, BORDER_SIZE, SECONDARY_OFFSET + yOffset);
+ g.setFont("8x16");
+ g.drawString(events[index + 1].title, BORDER_SIZE, SECONDARY_OFFSET + 20 + yOffset);
+ }
+ } else {
+ // If no more events exist, draw end
+ g.setFontAlign(0, 1, 0);
+ g.setFont("Vector", 20);
+ g.drawString("End", (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - BORDER_SIZE + yOffset);
+ }
+ }
+ };
+
+ /**
+ * Draw a two-line caption beneath a figure (Just beneath centre)
+ * @param {String} first Top string to draw
+ * @param {String} second Bottom string to draw
+ * @param {Number} yOffset Vertical pixel offset of the draw
+ */
+ const drawFigureCaption = function (first, second, yOffset) {
+ g.setFontAlign(0, -1, 0);
+ g.setFont("Vector", 18);
+ g.setColor(TEXT_COLOR);
+ g.drawString(first, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + yOffset);
+ g.drawString(second, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + 20 + yOffset);
+ };
+
+ /**
+ * Clear the contents area of the default layout
+ */
+ const clearContent = function () {
+ g.setColor(BG_COLOR);
+ g.fillRect(0, 0, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight());
+ };
+
+ /**
+ * Draw the sun figure (above centre, in content area)
+ * @param {Number} progress Progress through the sun expansion animation, between 0 and 1
+ * @param {Number} yOffset Vertical pixel offset of the draw
+ */
+ const drawSun = function (progress, yOffset) {
+ const p = ease(progress);
+ const sunColor = progress == 1 ? SUN_COLOR_END : g.blendColor(SUN_COLOR_START, SUN_COLOR_END, p);
+ g.setColor(sunColor);
+ g.fillPoly(g.transformVertices(interpolatePoly(BREATHING_POLYS, p), { y: yOffset }));
+
+ if (progress > 0.6) {
+ const faceP = ease((progress - 0.6) * 2.5);
+ g.setColor(g.blendColor(sunColor, SUN_FACE, faceP));
+ g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { y: map(20, 0, faceP) + yOffset }));
+ g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { x: SUN_EYE_RIGHT_OFFSET, y: map(20, 0, faceP) + yOffset }));
+ g.fillPoly(g.transformVertices(MOUTH_POLY, { y: map(10, 0, faceP) + yOffset }));
+ }
+
+ g.setColor(TEXT_COLOR);
+ g.fillRect({
+ x: map((g.getWidth() - ACCENT_WIDTH) / 2 - MARKER_SIZE, 20, p),
+ y: map(g.getHeight() / 2 - MARKER_SIZE, g.getHeight() / 2 - MARKER_SIZE / 2, p) + yOffset,
+ x2: map((g.getWidth() - ACCENT_WIDTH) / 2 + MARKER_SIZE, (g.getWidth() - ACCENT_WIDTH) - 20, p),
+ y2: map(g.getHeight() / 2 + MARKER_SIZE / 2, g.getHeight() / 2, p) + yOffset
+ });
+ };
+
+ /* Animation functions */
+
+ /**
+ * Animate clearing the screen to accent color with a single dot in the middle
+ */
+ const animClearScreen = function () {
+ let oldPoly1 = CLEAR_POLYS_1[0];
+ let oldPoly2 = CLEAR_POLYS_2[0];
+ doAnim(i => {
+ i = ease(i);
+ poly1 = interpolatePoly(CLEAR_POLYS_1, i);
+ poly2 = interpolatePoly(CLEAR_POLYS_2, i);
+ // Fill in black line
+ g.setColor(TEXT_COLOR);
+ g.fillPoly(poly1);
+ g.fillPoly(poly2);
+
+ // Fill in outer shape
+ g.setColor(ACCENT_COLOR);
+ g.fillPoly(oldPoly1);
+ g.fillPoly(oldPoly2);
+ g.flip();
+
+ // Save poly for next loop outer shape
+ oldPoly1 = poly1;
+ oldPoly2 = poly2;
+ }, TIME_CLEAR_ANIM);
+
+ // Draw circle
+ g.setColor(TEXT_COLOR);
+ g.fillCircle(g.getWidth() / 2, g.getHeight() / 2, MARKER_SIZE);
+ g.flip();
+ };
+
+ /**
+ * Animate from a cleared screen and dot to the default layout
+ */
+ const animDefaultScreen = function () {
+ doAnim(i => {
+ // Draw the circle moving into the corner
+ i = ease(i);
+ const circleX = map(g.getWidth() / 2, MARKER_POS_UPPER[0], i);
+ const circleY = map(g.getHeight() / 2, MARKER_POS_UPPER[1], i);
+ g.setColor(TEXT_COLOR);
+ g.fillCircle(circleX, circleY, MARKER_SIZE);
+
+ // Move the background poly in from the left
+ g.setColor(BG_COLOR);
+ const accentX = map(0, g.getWidth() - ACCENT_WIDTH, i);
+ g.fillPoly([0, 0, accentX, 0, accentX, MARKER_POS_UPPER[1] - PIN_SIZE, accentX - PIN_SIZE, MARKER_POS_UPPER[1], accentX, MARKER_POS_UPPER[1] + PIN_SIZE, accentX, 176, 0, 176]);
+ g.flip();
+
+ // Clear the circle for the next loop
+ g.setColor(ACCENT_COLOR);
+ g.fillCircle(circleX, circleY, MARKER_SIZE + 2);
+ }, TIME_DEFAULT_ANIM);
+
+ // Finish up the circle
+ const w = weather.get();
+ if (w && (w.code || w.txt)) {
+ doAnim(i => {
+ weather.drawIcon(w, MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * 2);
+ g.setColor(TEXT_COLOR);
+ g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * ease(1 - i));
+ g.flip();
+ }, 100);
+ } else {
+ g.setColor(TEXT_COLOR);
+ g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE);
+ }
+ };
+
+ /**
+ * Animate the sun figure expand or shrink fully
+ * @param {Number} direction Direction in which to animate. +1 = Expand. -1 = Shrink
+ */
+ const animSun = function (direction) {
+ doAnim(i => {
+ // Clear and redraw just the sun area
+ g.setColor(BG_COLOR);
+ g.fillRect(0, 31, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight() / 2 + 4);
+ drawSun((direction == 1 ? 0 : 1) + i * direction, 0);
+ g.flip();
+ }, TIME_EVENT_BREAK_ANIM);
+ };
+
+ /**
+ * Animate from centre dot to an event or backwards. Used for entering (forwards) or leaving (backwards) the day-change animation
+ * @param {Number} index Index of the event to draw animate in or out
+ * @param {Number} direction Direction of the animation. +1 = Event -> Dot. -1 = Dot -> Event
+ */
+ const animEventToMarker = function (index, direction) {
+ doAnim(i => {
+ let ei = direction == 1 ? ease(i) : ease(1 - i);
+ clearContent();
+ drawEvent(index, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ei, false);
+ g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, map(g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE, g.getHeight() / 2, ei), MARKER_SIZE);
+ g.flip();
+ }, TIME_EVENT_BREAK_IN);
+
+ };
+
+ /**
+ * Blit the current contents of content area out of screen, replacing it with something. Currently only for moving stuff upwards.
+ * @param {Function} thing Callback for the new thing to draw on the screen
+ * @param {Number} time How long the animation should last
+ */
+ const animBlitToX = function (thing, time) {
+ let oldI = 0;
+ doAnim(i => {
+ // Move stuff out of frame, index into frame
+ g.blit({
+ x1: 0,
+ y1: 0,
+ w: g.getWidth() - ACCENT_WIDTH - PIN_SIZE,
+ h: ease(1 - oldI) * g.getHeight(),
+ x2: 0,
+ y2: - (ease(i) - ease(oldI)) * g.getHeight(),
+ setModified: true
+ });
+ g.setColor(BG_COLOR);
+ // Only clear where old stuff no longer is
+ g.fillRect(0, g.getHeight() * (1 - ease(i)), g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight());
+ thing(i);
+ g.flip();
+ oldI = i;
+ }, time);
+ };
+
+ /**
+ * Transition between one event and another, showing a day-change animation if needed
+ * @param {Number} startIndex The event index that we are animating out of
+ * @param {Number} endIndex The event index that we are animating into
+ */
+ const animEventTransition = function (startIndex, endIndex) {
+ if (events[startIndex].date == events[endIndex].date) {
+ // If both events are within the same day, just scroll from one to the other.
+ // First determine which event is on top and which direction we are animating in
+ let topIndex = (startIndex < endIndex) ? startIndex : endIndex;
+ let botIndex = (startIndex < endIndex) ? endIndex : startIndex;
+ let direction = (startIndex < endIndex) ? 1 : -1;
+ let offset = (startIndex < endIndex) ? 0 : 1;
+
+ doAnim(i => {
+ // Animate the two events moving towards their destinations
+ clearContent();
+ drawEvent(topIndex, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), false);
+ drawEvent(botIndex, (SECONDARY_OFFSET - PRIMARY_OFFSET) - (SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), true);
+ g.flip();
+ }, TIME_EVENT_CHANGE);
+
+ // Finally, reset contents and redraw for good measure
+ clearContent();
+ drawEvent(endIndex, 0, true);
+ g.flip();
+ } else {
+ // The events are on different days, trigger day-change animation
+ if (startIndex < endIndex) {
+ // Destination is later, Stuff moves upwards
+ animEventToMarker(startIndex, 1); // The day-end dot moves to center of screen
+ drawFigureCaption(events[endIndex].weekday, events[endIndex].date, 0); // Caption between sun appears, no need to continuously redraw
+ animSun(1); // Animate the sun expanding
+ doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment
+ animBlitToX(i => { drawEvent(endIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT); // Blit the sun and caption out, replacing with destination event
+ } else {
+ // Destination is earlier, content moves downwards
+ doAnim(i => {
+ // Can't animBlit, draw sun and figure caption replacing origin event
+ clearContent();
+ drawEvent(startIndex, g.getHeight() * ease(i), true);
+ drawSun(1, - g.getHeight() * ease(1 - i));
+ drawFigureCaption(events[endIndex].weekday, events[endIndex].date, - g.getHeight() * ease(1 - i));
+ g.flip();
+ }, TIME_EVENT_BREAK_OUT);
+ doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment
+ animSun(-1); // Collapse the sun
+ animEventToMarker(endIndex, -1); // Animate from dot to destination event
+ }
+ }
+ g.flip();
+ };
+
+ /**
+ * Bump the event because we've reached an end
+ * @param {Number} index The index of the event which we are currently at (probably last)
+ * @param {Number} direction Which direction to bump. +1 = content moves down, then up. -1 = content moves up, back down
+ */
+ const animEventBump = function (index, direction) {
+ doAnim(i => {
+ clearContent();
+ drawEvent(index, Math.sin(Math.PI * i) * 24 * direction, true);
+ g.flip();
+ }, TIME_BUMP_ANIM);
+ };
+
+ /**
+ * Run the exit animation of the application
+ */
+ const animExit = function () {
+ // First, move out (downwards) the current event
+ doAnim(i => {
+ clearContent();
+ drawEvent(currentEventIndex, ease(i) * g.getHeight(), true);
+ g.flip();
+ }, TIME_EXIT_ANIM / 3 * 2);
+
+ // Clear the screen leftwards with the accent color
+ g.setColor(ACCENT_COLOR);
+ doAnim(i => {
+ g.fillRect(ease(1 - i) * g.getWidth(), 0, g.getWidth(), g.getHeight());
+ g.flip();
+ }, TIME_EXIT_ANIM / 3);
+ };
+
+ /**
+ * Animate from empty default screen to the first event to show.
+ * If the event we're moving to is not later today, show the date first.
+ */
+ const animFirstEvent = function () {
+ if (!isSameDay(new Date(events[currentEventIndex].timestamp * 1000), new Date())) {
+ drawFigureCaption(events[currentEventIndex].weekday, events[currentEventIndex].date, 0);
+ animSun(1);
+ doAnim(i => { }, TIME_EVENT_BREAK_HALT);
+ animBlitToX(i => { drawEvent(currentEventIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT, 1);
+ } else {
+ drawEvent(currentEventIndex, 0, true);
+ }
+ };
+
+ /* Setup */
+
+ /* Load events */
+ const today = new Date();
+ const tomorrow = new Date();
+ const yesterday = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ yesterday.setDate(yesterday.getDate() - 1);
+ g.setFont("6x12");
+ const locale = require("locale");
+
+ let events = (require("Storage").readJSON("android.calendar.json", true) || []).map(event => {
+ // Title uses 8x16 font, 8 px wide characters. Limit title to fit on a line.
+ let title = event.title;
+ if (title.length > (g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) {
+ title = title.slice(0, ((g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) - 3) + "...";
+ }
+
+ // Wrap description to fit four lines of content
+ let description = g.wrapString(event.description, g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH - PIN_SIZE).slice(0, 4).join("\n");
+
+ // Set weekday text
+ let eventDate = new Date(event.timestamp * 1000);
+ let weekday = locale.dow(eventDate);
+ if (isSameDay(eventDate, today)) {
+ weekday = /*LANG*/"Today";
+ } else if (isSameDay(eventDate, tomorrow)) {
+ weekday = /*LANG*/"Tomorrow";
+ } else if (isSameDay(eventDate, yesterday)) {
+ weekday = /*LANG*/"Yesterday";
+ }
+
+ return {
+ timestamp: event.timestamp,
+ weekday: weekday,
+ date: locale.date(eventDate, 1),
+ time: locale.time(eventDate, 1) + locale.meridian(eventDate),
+ title: title,
+ description: description
+ };
+ }).sort((a, b) => { return a.timestamp - b.timestamp; });
+
+ // If no events, add a note.
+ if (events.length == 0) {
+ events[0] = {
+ timestamp: Date.now() / 1000,
+ weekday: /*LANG*/"Today",
+ date: require("locale").date(new Date(), 1),
+ time: require("locale").time(new Date(), 1),
+ title: /*LANG*/"No events",
+ description: /*LANG*/"Nothing to do"
+ };
+ }
+
+ // We should start at the first event later than now
+ let currentEventIndex = events.findIndex((event) => { return event.timestamp * 1000 > Date.now(); });
+ if (currentEventIndex == -1) currentEventIndex = 0; // Or just first event if none found
+
+ // Setup the UI with remove to support fast load
+ Bangle.setUI({
+ mode: "custom",
+ btn: () => { animExit(); Bangle.load(); },
+ remove: function () {
+ require("widget_utils").show();
+ delete Graphics.prototype.Font6x12;
+ delete Graphics.prototype.Font8x16;
+ Bangle.removeListener('swipe', onSwipe);
+ },
+ });
+
+ /**
+ * Callback for swipe gesture. Transitions between adjacent events.
+ * @param {Number} directionLR Unused.
+ * @param {Number} directionUD Whether swipe direction is up or down
+ */
+ const onSwipe = function (directionLR, directionUD) {
+ if (directionUD == -1) {
+ // Swiping up
+ if (currentEventIndex + 1 < events.length) {
+ // Animate to the next event
+ animEventTransition(currentEventIndex, currentEventIndex + 1);
+ currentEventIndex += 1;
+ } else {
+ // We've hit the end, bump
+ animEventBump(currentEventIndex, -1);
+ }
+ } else if (directionUD == 1) {
+ //Swiping down
+ if (currentEventIndex > 0) {
+ // Animate to the previous event
+ animEventTransition(currentEventIndex, currentEventIndex - 1);
+ currentEventIndex -= 1;
+ } else {
+ // If swiping earlier than earliest event, exit back to watchface
+ animExit();
+ Bangle.load();
+ }
+ }
+ };
+
+ // Ready animations for showing the first event, then register swipe listener for switching events
+ setTimeout(() => {
+ animDefaultScreen();
+ animFirstEvent();
+ Bangle.on('swipe', onSwipe);
+ }, TIME_CLEAR_ANIM + TIME_CLEAR_BREAK);
+ animClearScreen(); // Start visible changes by clearing the screen
+
+ // Load and hide widgets to background
+ Bangle.loadWidgets();
+ require("widget_utils").hide();
+}
\ No newline at end of file
diff --git a/apps/rebbleagenda/app.png b/apps/rebbleagenda/app.png
new file mode 100644
index 000000000..207156565
Binary files /dev/null and b/apps/rebbleagenda/app.png differ
diff --git a/apps/rebbleagenda/metadata.json b/apps/rebbleagenda/metadata.json
new file mode 100644
index 000000000..07227d3bc
--- /dev/null
+++ b/apps/rebbleagenda/metadata.json
@@ -0,0 +1,25 @@
+{ "id": "rebbleagenda",
+ "name": "Rebble Agenda",
+ "shortName":"Agenda",
+ "version":"0.01",
+ "description": "A pebble-inspired animated agenda",
+ "icon": "app.png",
+ "screenshots" : [
+ { "url":"screenshot_rebbleagenda_events.png" },
+ { "url":"screenshot_rebbleagenda_customtheme.png" },
+ { "url":"screenshot_rebbleagenda_sun.png" }
+ ],
+ "type": "app",
+ "tags": "agenda,tool",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "dependencies" : { "weather":"app" },
+ "storage": [
+ {"name":"rebbleagenda.app.js","url":"app.js"},
+ {"name":"rebbleagenda.settings.js","url":"settings.js"},
+ {"name":"rebbleagenda.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name":"rebbleagenda.json"}
+ ]
+}
diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png b/apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png
new file mode 100644
index 000000000..2d9959a5e
Binary files /dev/null and b/apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png differ
diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_events.png b/apps/rebbleagenda/screenshot_rebbleagenda_events.png
new file mode 100644
index 000000000..c94c0d9c4
Binary files /dev/null and b/apps/rebbleagenda/screenshot_rebbleagenda_events.png differ
diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_sun.png b/apps/rebbleagenda/screenshot_rebbleagenda_sun.png
new file mode 100644
index 000000000..16a63002b
Binary files /dev/null and b/apps/rebbleagenda/screenshot_rebbleagenda_sun.png differ
diff --git a/apps/rebbleagenda/settings.js b/apps/rebbleagenda/settings.js
new file mode 100644
index 000000000..8ed2ceae5
--- /dev/null
+++ b/apps/rebbleagenda/settings.js
@@ -0,0 +1,69 @@
+(function (back) {
+ const SETTINGS_FILE = "rebbleagenda.json";
+
+ // initialize with default settings...
+ let s = {
+ 'system': true,
+ 'bg': "#FFF",
+ 'fg': "#000",
+ 'acc': "#0FF"
+ };
+
+ // ...and overwrite them with any saved values
+ // This way saved values are preserved if a new version adds more settings
+ const storage = require('Storage');
+ let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
+ const saved = settings || {};
+ for (const key in saved) {
+ s[key] = saved[key];
+ }
+
+ const save = function () {
+ settings = s;
+ storage.write(SETTINGS_FILE, settings);
+ };
+
+ const color_options = [/*LANG*/"Red", /*LANG*/"Green", /*LANG*/"Blue", /*LANG*/"Purple", /*LANG*/"Cyan", /*LANG*/"Orange", /*LANG*/"Grey"];
+ const color_codes = ['#F00','#0F0','#00F','#F0F','#0FF','#FF0', "#888"];
+ const ground_options = [/*LANG*/"Black", /*LANG*/"White", /*LANG*/"Dark Blue", /*LANG*/"Dark Red", /*LANG*/"Dark Green", /*LANG*/"Light Blue", /*LANG*/"Light Red", /*LANG*/"Light Green"];
+ const ground_codes = ["#000", "#FFF", "#003", "#300", "#030", "#BBF", "#FBB", "#BFB"];
+
+ E.showMenu({
+ '': { 'title': 'Rebble Agenda' },
+ /*LANG*/'< Back': back,
+ /*LANG*/'Use system theme': {
+ value: !!s.system,
+ onchange: v => {
+ s.system = v;
+ save();
+ },
+ },
+ /*LANG*/'Accent': {
+ value: 0 | color_codes.indexOf(s.acc),
+ min: 0, max: color_codes.length-1,
+ format: v => color_options[v],
+ onchange: v => {
+ s.acc = color_codes[v];
+ save();
+ },
+ },
+ /*LANG*/'Background': {
+ value: 0 | ground_codes.indexOf(s.bg),
+ min: 0, max: ground_codes.length-1,
+ format: v => ground_options[v],
+ onchange: v => {
+ s.bg = ground_codes[v];
+ save();
+ },
+ },
+ /*LANG*/'Foreground': {
+ value: 0 | ground_codes.indexOf(s.fg),
+ min: 0, max: ground_codes.length-1,
+ format: v => ground_options[v],
+ onchange: v => {
+ s.fg = ground_codes[v];
+ save();
+ },
+ }
+ });
+});
\ No newline at end of file
diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog
index 991b811cb..5f4d1f8b4 100644
--- a/apps/recorder/ChangeLog
+++ b/apps/recorder/ChangeLog
@@ -35,3 +35,4 @@
0.27: Fix first ever recorded filename being log0 (now all are dated)
0.28: Automatically create new track if the filename is different
0.29: When plotting with OpenStMap scale map to track width & height
+0.30: Add clock info for showing and toggling recording state
diff --git a/apps/recorder/clkinfo.js b/apps/recorder/clkinfo.js
new file mode 100644
index 000000000..b4a9a45b8
--- /dev/null
+++ b/apps/recorder/clkinfo.js
@@ -0,0 +1,38 @@
+(function () {
+ const recimg = () =>
+ atob("GBiBAAAAABwAAD4MAH8eAH8OAH8AAD4QABx8AAD8AAH+AAE+AAM/AAN7wAN4wAB4AAB8AAD8AADOAAHGAAOHAAMDAAIBAAAAAAAAAA==");
+
+ const pauseimg = () =>
+ atob("GBiBAAAAAAAAAAAAAAAAAAHDgAPnwAPjwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPnwAPjwAPnwAHDgAAAAAAAAAAAAAAAAA==");
+
+ return {
+ name: "Bangle",
+ items: require("Storage").readJSON("recorder.json") ? [
+ {
+ name: "Toggle",
+ get: () => {
+ const w = WIDGETS && WIDGETS["recorder"];
+
+ return w && w.isRecording() ? {
+ text: "Recording",
+ short: "Rec",
+ img: recimg(),
+ } : {
+ text: w ? "Paused" : "No rec",
+ short: w ? "Paused" : "No rec",
+ img: pauseimg(),
+ };
+ },
+ run: () => {
+ const w = WIDGETS && WIDGETS["recorder"];
+ if(w){
+ Bangle.buzz();
+ w.setRecording(!w.isRecording(), { force: "append" });
+ }
+ },
+ show: () => {},
+ hide: () => {},
+ },
+ ] : [],
+ };
+});
diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html
index cc9762d20..24dba9b64 100644
--- a/apps/recorder/interface.html
+++ b/apps/recorder/interface.html
@@ -30,7 +30,7 @@ function saveKML(track,title) {
track = filterGPSCoordinates(track);
// Now output KML
var kml = `
-
+
${track[0].Heartrate!==undefined ? `
diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json
index e714abf8d..15ba165d1 100644
--- a/apps/recorder/metadata.json
+++ b/apps/recorder/metadata.json
@@ -2,10 +2,10 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
- "version": "0.29",
+ "version": "0.30",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
- "tags": "tool,outdoors,gps,widget",
+ "tags": "tool,outdoors,gps,widget,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
@@ -13,6 +13,7 @@
{"name":"recorder.app.js","url":"app.js"},
{"name":"recorder.img","url":"app-icon.js","evaluate":true},
{"name":"recorder.wid.js","url":"widget.js"},
+ {"name":"recorder.clkinfo.js","url":"clkinfo.js"},
{"name":"recorder.settings.js","url":"settings.js"}
],
"data": [
diff --git a/apps/rescalc/ChangeLog b/apps/rescalc/ChangeLog
index 7b1d6baca..21ff1f6e8 100644
--- a/apps/rescalc/ChangeLog
+++ b/apps/rescalc/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Fixes colors not matching user input from color menu in some cases, 3 bands are now shown larger, various code improvements.
0.03: Use transparent icon with better visibility on dark backgrounds, new resistor img with darker outlines
+0.04: Fix capitalization. Improve decimal handling.
diff --git a/apps/rescalc/app.js b/apps/rescalc/app.js
index 566809837..4debb6c5f 100644
--- a/apps/rescalc/app.js
+++ b/apps/rescalc/app.js
@@ -3,19 +3,19 @@
// https://icons8.com/icon/ISAVBnskZod0/resistor
let colorData = {
- black: { value: 0, multiplier: 1, hex: '#000' },
- brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' },
- red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' },
- orange: { value: 3, multiplier: 1000, hex: '#FF9900' },
- yellow: { value: 4, multiplier: 10000, hex: '#ff0' },
- green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' },
- blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' },
- violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' },
- grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' },
- white: { value: 9, multiplier: 1000000000, hex: '#fff' },
- gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' },
- silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' },
- none: { tolerance: 20 },
+ Black: { value: 0, multiplier: 1, hex: '#000' },
+ Brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' },
+ Red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' },
+ Orange: { value: 3, multiplier: 1000, hex: '#FF9900' },
+ Yellow: { value: 4, multiplier: 10000, hex: '#ff0' },
+ Green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' },
+ Blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' },
+ Violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' },
+ Grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' },
+ White: { value: 9, multiplier: 1000000000, hex: '#fff' },
+ Gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' },
+ Silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' },
+ None: { tolerance: 20 },
};
function clearScreen() { // Except Back Button
@@ -27,7 +27,7 @@ function colorBandsToResistance(colorBands) {
let firstBand = colorBands[0];
let secondBand = colorBands[1];
let multiplierBand = colorBands[2];
- let toleranceBand = colorBands[3] || 'none'; // Add a default value for toleranceBand
+ let toleranceBand = colorBands[3] || 'None'; // Add a default value for toleranceBand
let significantDigits = colorData[firstBand].value * 10 + colorData[secondBand].value;
let multiplier = colorData[multiplierBand].multiplier;
let resistance = significantDigits * multiplier;
@@ -35,57 +35,65 @@ function colorBandsToResistance(colorBands) {
return [resistance, tolerance];
}
+// Function to get color bands based on resistance and tolerance
function resistanceToColorBands(resistance, tolerance) {
let firstDigit, secondDigit, multiplier;
- if (resistance < 1) {
- // The resistance is less than 1, so we need to handle this case specially
- let count = 0;
- while (resistance < 1) {
- resistance *= 10;
- count++;
- }
- // Now, resistance is a whole number and count is how many times we had to multiply by 10
- let resistanceStr = resistance.toString();
- firstDigit = 0; // Set the first band color to be black
- secondDigit = Number(resistanceStr.charAt(0)); // Set the second band color to be the significant digit
- // Use count to determine the multiplier
- multiplier = count === 1 ? 0.1 : 0.01;
- } else {
- // Convert the resistance to a string so we can manipulate it easily
- let resistanceStr = resistance.toString();
- if (resistanceStr.length === 1) { // Check if resistance is a single digit
- firstDigit = 0;
- secondDigit = Number(resistanceStr.charAt(0));
- multiplier = 1; // Set multiplier to 1 for single digit resistance values
+ let resistanceStr = resistance.toString();
+ let decimalIndex = resistanceStr.indexOf('.');
+
+ // Handle resistance with decimal
+ if (decimalIndex !== -1) {
+ let integerDigits = resistanceStr.substring(0, decimalIndex);
+ let decimalDigits = resistanceStr.substring(decimalIndex + 1);
+ let leadingZeros = decimalDigits.match(/^0*/)[0].length;
+
+ // If resistance is less than 1
+ if (parseInt(integerDigits) === 0) {
+ if (leadingZeros === decimalDigits.length - 1) {
+ // If only one significant digit
+ firstDigit = 0;
+ secondDigit = parseInt(decimalDigits.charAt(leadingZeros));
+ multiplier = 1 / Math.pow(10, leadingZeros + 1);
+ } else {
+ // If more than one significant digit
+ firstDigit = parseInt(decimalDigits.charAt(leadingZeros));
+ secondDigit = parseInt(decimalDigits.charAt(leadingZeros + 1));
+ multiplier = 1 / Math.pow(10, leadingZeros + 2);
+ }
} else {
- // Extract the first two digits from the resistance value
- firstDigit = Number(resistanceStr.charAt(0));
- secondDigit = Number(resistanceStr.charAt(1));
- // Calculate the multiplier by matching it directly with the length of digits
- multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1);
+ // If resistance is greater than 1
+ firstDigit = parseInt(integerDigits.charAt(0));
+ secondDigit = parseInt(decimalDigits.charAt(0));
+ multiplier = 1 / Math.pow(10, decimalDigits.length);
}
+ } else {
+ // Handle resistance without decimal
+ firstDigit = resistanceStr.length === 1 ? 0 : parseInt(resistanceStr.charAt(0));
+ secondDigit = parseInt(resistanceStr.charAt(resistanceStr.length === 1 ? 0 : 1));
+ multiplier = Math.pow(10, resistanceStr.length - 2);
}
- let firstBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].value === firstDigit;
- });
- let firstBand = firstBandEntry ? firstBandEntry[1].hex : undefined;
- let secondBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].value === secondDigit;
- });
- let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined;
- let multiplierBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].multiplier === multiplier;
- });
- let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined;
- let toleranceBandEntry = Object.entries(colorData).find(function (entry) {
- return entry[1].tolerance === tolerance;
- });
- let toleranceBand = toleranceBandEntry ? toleranceBandEntry[1].hex : undefined;
- let bands = [firstBand, secondBand, multiplierBand];
+
+ // Generate color bands array
+ let bands = [
+ getBandColor('value', firstDigit),
+ getBandColor('value', secondDigit),
+ getBandColor('multiplier', multiplier),
+ ];
+
+ // Add tolerance color band if provided
+ let toleranceBand = getBandColor('tolerance', tolerance);
if (toleranceBand) bands.push(toleranceBand);
return bands;
}
+// Helper function to get color band based on property and value
+function getBandColor(property, value) {
+ let entry = Object.entries(colorData).find(function (entry) {
+ return entry[1][property] === value;
+ });
+ return entry ? entry[1].hex : undefined;
+}
+
function drawResistor(colorBands, tolerance) {
let img = require("Storage").read("rescalc-resistor.img");
let resistorBodyWidth = 51;
@@ -208,17 +216,17 @@ function drawResistance(resistance, tolerance) {
// Populate colorBandMenu with colors from colorData
for (let color in colorData) {
if (bandNumber === 1 || bandNumber === 2) {
- if (color !== 'none' && color !== 'gold' && color !== 'silver') {
+ if (color !== 'None' && color !== 'Gold' && color !== 'Silver') {
(function (color) {
- colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
+ colorBandMenu[color] = function () {
setBandColor(bandNumber, color);
};
})(color);
}
} else if (bandNumber === 3) {
- if (color !== 'none') {
+ if (color !== 'None') {
(function (color) {
- colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
+ colorBandMenu[color] = function () {
setBandColor(bandNumber, color);
};
})(color);
@@ -226,7 +234,7 @@ function drawResistance(resistance, tolerance) {
} else if (bandNumber === 4) {
if (colorData[color].hasOwnProperty('tolerance')) {
(function (color) {
- colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
+ colorBandMenu[color] = function () {
setBandColor(bandNumber, color);
};
})(color);
@@ -407,7 +415,6 @@ function drawResistance(resistance, tolerance) {
};
function showResistanceEntryMenu() {
- // Update the 'Ohms' field
resistanceEntryMenu['Ohms'].value = settings.resistance;
resistanceEntryMenu['Ohms'].format = v => {
let multipliedValue = v * (settings.multiplier || 1);
diff --git a/apps/rescalc/metadata.json b/apps/rescalc/metadata.json
index 473f334d7..cd2b7ea31 100644
--- a/apps/rescalc/metadata.json
+++ b/apps/rescalc/metadata.json
@@ -3,7 +3,7 @@
"name": "Resistor Calculator",
"shortName": "Resistor Calc",
"icon": "rescalc.png",
- "version":"0.03",
+ "version":"0.04",
"screenshots": [
{"url": "screenshot.png"},
{"url": "screenshot-1.png"},
diff --git a/apps/sched/interface.html b/apps/sched/interface.html
index b67029fa2..53b443371 100644
--- a/apps/sched/interface.html
+++ b/apps/sched/interface.html
@@ -16,14 +16,18 @@ function readFile(input) {
for(let i=0; i {
- const jCalData = ICAL.parse(reader.result);
+ const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data
+ const jCalData = ICAL.parse(icalText);
const comp = new ICAL.Component(jCalData);
+ const vtz = comp.getFirstSubcomponent('vtimezone');
+ const tz = new ICAL.Timezone(vtz);
+
// Fetch the VEVENT part
comp.getAllSubcomponents('vevent').forEach(vevent => {
event = new ICAL.Event(vevent);
const exists = alarms.some(alarm => alarm.id === event.uid);
- const alarm = eventToAlarm(event, offsetMinutes*60*1000);
+ const alarm = eventToAlarm(event, tz, offsetMinutes*60*1000);
renderAlarm(alarm, exists);
if (exists) {
@@ -68,7 +72,8 @@ function getAlarmDefaults() {
};
}
-function eventToAlarm(event, offsetMs) {
+function eventToAlarm(event, tz, offsetMs) {
+ event.startDate.zone = tz;
const dateOrig = event.startDate.toJSDate();
const date = offsetMs ? new Date(dateOrig - offsetMs) : dateOrig;
@@ -334,7 +339,7 @@ function onInit() {
-
+
diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog
index 6d2f5d2b4..92088af2b 100644
--- a/apps/sensortools/ChangeLog
+++ b/apps/sensortools/ChangeLog
@@ -4,3 +4,4 @@
0.04: Correct type of time attribute in gps to Date
0.05: Fix gps emulation interpolation
Add setting for log output
+0.06: Fix sensortools breaking Bangle.emit with multiple arguments (e.g. "message")
diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js
index 5e1c199c2..fae856108 100644
--- a/apps/sensortools/lib.js
+++ b/apps/sensortools/lib.js
@@ -20,37 +20,44 @@ exports.enable = () => {
Bangle.sensortoolsOrigEmit = Bangle.emit;
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
+ const modifyArguments = function(args, value) {
+ if (args.length >= 1)
+ args[0] = value;
+ return args;
+ };
+
Bangle.on = function(name, callback) {
if (onEvents[name]) {
log("Redirecting listener for", name, "to", name + "_mod");
- Bangle.sensortoolsOrigOn(name + "_mod", callback);
- Bangle.sensortoolsOrigOn(name, (e) => {
- log("Redirected event for", name, "to", name + "_mod");
- Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
+ let origName = name;
+ Bangle.sensortoolsOrigOn(origName, (e) => {
+ log("Redirected event for", origName, "to", origName + "_mod");
+ Bangle.sensortoolsOrigEmit(origName + "_mod", onEvents[origName](e));
});
+ Bangle.sensortoolsOrigOn.apply(this, modifyArguments(arguments, name + "_mod"));
} else {
log("Pass through on call for", name, callback);
- Bangle.sensortoolsOrigOn(name, callback);
+ Bangle.sensortoolsOrigOn.apply(this, arguments);
}
};
- Bangle.removeListener = function(name, callback) {
+ Bangle.removeListener = function(name) {
if (onEvents[name]) {
log("Removing augmented listener for", name, onEvents[name]);
- Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
+ Bangle.sensortoolsOrigRemoveListener.apply(this, modifyArguments(arguments, name + "_mod"));
} else {
log("Pass through remove listener for", name);
- Bangle.sensortoolsOrigRemoveListener(name, callback);
+ Bangle.sensortoolsOrigRemoveListener.apply(this, arguments);
}
};
- Bangle.emit = function(name, event) {
+ Bangle.emit = function(name) {
if (onEvents[name]) {
log("Augmenting emit call for", name, onEvents[name]);
- Bangle.sensortoolsOrigEmit(name + "_mod", event);
+ Bangle.sensortoolsOrigEmit.apply(this, modifyArguments(arguments, name + "_mod"));
} else {
log("Pass through emit call for", name);
- Bangle.sensortoolsOrigEmit(name, event);
+ Bangle.sensortoolsOrigEmit.apply(this, arguments);
}
};
diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json
index bffffd090..23749d537 100644
--- a/apps/sensortools/metadata.json
+++ b/apps/sensortools/metadata.json
@@ -2,7 +2,7 @@
"id": "sensortools",
"name": "Sensor tools",
"shortName": "Sensor tools",
- "version": "0.05",
+ "version": "0.06",
"description": "Tools for testing and debugging apps that use sensor input",
"icon": "icon.png",
"type": "bootloader",
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index d090add58..dbeea10ca 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -68,3 +68,6 @@ of 'Select Clock'
0.60: Moved LCD calibration to top of menu, and use 12 taps (not 8)
LCD calibration will now error if the calibration is obviously wrong
0.61: Permit temporary bypass of the BLE whitelist
+0.62: Fix whitelist showing as 'on' by default when it's not after 0.59
+0.63: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds
+ Remove 'beta' label from passkey - it's been around for a while and works ok
\ No newline at end of file
diff --git a/apps/setting/README.md b/apps/setting/README.md
index 2a7f7ee9c..18aa8af46 100644
--- a/apps/setting/README.md
+++ b/apps/setting/README.md
@@ -21,7 +21,7 @@ This is Bangle.js's settings menu
* **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps? As long as your watch firmware is up to date, Gadgetbridge will work even with `Programmable` set to `Off`.
* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device.
* **NOTE:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps.
-* **Passkey BETA** allows you to set a passkey that is required to connect and pair to Bangle.js. **Note:** This is Beta and you will almost certainly encounter issues connecting with Web Bluetooth using this option.
+* **Passkey** allows you to set a passkey that is required to connect and pair to Bangle.js.
* **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app.
* **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again.
@@ -62,7 +62,7 @@ The exact effects depend on the app. In general the watch will not wake up by i
* `Both` Log and display on Bangle's screen
* **Compact Storage** Removes deleted/old files from Storage - this will speed up your Bangle.js
* **Rewrite Settings** Should not normally be required, but if `.boot0` has been deleted/corrupted (and so no settings are being loaded) this will fix it.
-* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours.
+* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours.
* **Calibrate Battery** If you're finding your battery percentage meter isn't accurate, leave your Bangle.js on charge for at least 3 hours, and then choose this menu option. It will measure the battery voltage when full and will allow Bangle.js to report a more accurate battery percentage.
* **Reset Settings** Reset the settings (as set in this app) to defaults. Does not reset settings for other apps.
* **Factory Reset** (not available on Bangle.js 1) - wipe **everything** and return to a factory state
diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json
index b2b19dd6b..c94abdd82 100644
--- a/apps/setting/metadata.json
+++ b/apps/setting/metadata.json
@@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
- "version": "0.61",
+ "version": "0.63",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index d22f28412..f3b963d6e 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -186,14 +186,14 @@ function showBLEMenu() {
updateSettings();
}
},
- /*LANG*/'Passkey BETA': {
+ /*LANG*/'Passkey': {
value: settings.passkey?settings.passkey:/*LANG*/"none",
onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call
},
/*LANG*/'Whitelist': {
value:
(
- settings.whitelist_disabled ? /*LANG*/"off" : /*LANG*/"on"
+ (settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"off" : /*LANG*/"on"
) + (
settings.whitelist
? " (" + settings.whitelist.length + ")"
@@ -383,6 +383,12 @@ function showWhitelistMenu() {
NRF.on('connect', function(addr) {
if (!settings.whitelist) settings.whitelist=[];
delete settings.whitelist_disabled;
+ if (NRF.resolveAddress !== undefined) {
+ let resolvedAddr = NRF.resolveAddress(addr);
+ if (resolvedAddr !== undefined) {
+ addr = resolvedAddr + " (resolved)";
+ }
+ }
settings.whitelist.push(addr);
updateSettings();
NRF.removeAllListeners('connect');
diff --git a/apps/sixths/ChangeLog b/apps/sixths/ChangeLog
new file mode 100644
index 000000000..263d4078d
--- /dev/null
+++ b/apps/sixths/ChangeLog
@@ -0,0 +1 @@
+0.01: attempt to import
diff --git a/apps/sixths/README.md b/apps/sixths/README.md
new file mode 100644
index 000000000..e5d76d5ad
--- /dev/null
+++ b/apps/sixths/README.md
@@ -0,0 +1,51 @@
+# Sixth Sense 
+
+Clock displaying just the right information at the right time.
+
+Experimental clock face. It aims to display just the right importation
+at the right time, with focus on various sensors. Normally, digital
+clock, date and step count in kilometers is displayed.
+
+It saves a lot of logs for debugging and future use. In particular, it
+saves battery and step counters all the time, and GPS positions
+whenever it is enabled. You may not want to use it if you are secret
+agent or journalist in Iran.
+
+Application can be controled by gestures, making control possible in
+challenging conditions such as on horseback. Gestures are based on
+morse code, left half of screen is for ".", right half of screen is
+for "-". Gesture should always start in the upper half of screen. If
+next symbol is same, drag vertically, else drag horizontally.
+
+Power saving GPS mode is available, suitable for hiking. GPS fix is
+acquired once every few minutes, and written into the log. Approximate
+distance travelled is displayed. Due to only taking fix every few
+minutes, real distance will be usually higher than approximation.
+
+Useful gestures:
+
+F -- disable GPS.
+G -- enable GPS for 4 hours.
+N -- take a note and write it to the log.
+
+When application detects watch is being worn, it will use vibrations
+to communicate back to the user.
+
+E -- acknowledge, gesture understood.
+T -- start of new hour.
+
+Written by: [Pavel Machek](https://github.com/pavelmachek)
+
+## Future Development
+
+I'd like to expand GPS development more, allowing marking of waypoints
+and navigating back to them. I'd also like to make power-saving
+optional.
+
+I'd also like to utilize the altimeter more, likely remembering
+altitude of home location, automatically correcting for pressure every
+night.
+
+I'd like to make display nicer, and likely more dynamic, displaying
+whatever application believes is most important at the time (and
+possibly allowing scrolling).
\ No newline at end of file
diff --git a/apps/sixths/app-icon.js b/apps/sixths/app-icon.js
new file mode 100644
index 000000000..c75930b47
--- /dev/null
+++ b/apps/sixths/app-icon.js
@@ -0,0 +1,2 @@
+require("heatshrink").decompress(atob("mEwgP/AEn3AgfvAonnAon3+/9AoX7+/5CwX7A4IWCB4P7/3/+YZC/gOD/4eDAAIeC/0An4PC+P/4Y3C5E/Cwcgj/+v4WB4EP/+fEoOAg//441BAoQjB84FCDwPvwED/5BB+4FCBYIFBCIJRB/fAAoPPBoIvCn41B+A7EnF//BHCHAODAoXwgF/N4aMCAog1BQoJ0Cv5oCCAO/UAP9AoPP+fv/oOBW4IFBDQP794FB/5BBMwIFBJoItD375eA"))
+
diff --git a/apps/sixths/app.js b/apps/sixths/app.js
new file mode 100644
index 000000000..ce036f79d
--- /dev/null
+++ b/apps/sixths/app.js
@@ -0,0 +1,508 @@
+const W = g.getWidth();
+const H = g.getHeight();
+
+var cx = 100; cy = 105; sc = 70;
+var buzz = "", msg = "";
+temp = 0; alt = 0; bpm = 0;
+var buzz = "", msg = "", inm = "", l = "", note = "(NOTEHERE)";
+var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note
+
+var gps_on = 0, last_fix = 0, last_restart = 0, last_pause = 0, last_fstart = 0; // utime
+var gps_needed = 0, gps_limit = 0; // seconds
+var prev_fix = null;
+var gps_dist = 0;
+
+var is_active = false;
+var cur_altitude = 0, cur_temperature = 0, alt_adjust = 0;
+const rest_altitude = 354;
+
+function toMorse(x) {
+ r = "";
+ for (var i = 0; i < x.length; i++) {
+ c = x[i];
+ if (c == " ") {
+ r += " ";
+ continue;
+ }
+ r += asciiToMorse(c) + " ";
+ }
+ return r;
+}
+
+function aload(s) {
+ buzz += toMorse(' E');
+ load(s);
+}
+
+function gpsRestart() {
+ print("gpsRestart");
+ Bangle.setGPSPower(1, "sixths");
+ last_restart = getTime();
+ last_pause = 0;
+ last_fstart = 0;
+}
+
+function gpsPause() {
+ print("gpsPause");
+ Bangle.setGPSPower(0, "sixths");
+ last_restart = 0;
+ last_pause = getTime();
+}
+
+function gpsOn() {
+ gps_on = getTime();
+ gps_needed = 1000;
+ gps_limit = 60*60*4;
+ last_fix = 0;
+ prev_fix = null;
+ gps_dist = 0;
+ gpsRestart();
+}
+
+function gpsOff() {
+ Bangle.setGPSPower(0, "sixths");
+ gps_on = 0;
+}
+
+function inputHandler(s) {
+ print("Ascii: ", s);
+ if (mode == 1) {
+ note = note + s;
+ mode_time = getTime();
+ return;
+ }
+ switch(s) {
+ case 'B':
+ s = ' B';
+ bat = E.getBattery();
+ if (bat > 45)
+ s += 'E';
+ else
+ s = s+(bat/5);
+ buzz += toMorse(s);
+ break;
+ case 'F': gpsOff(); break;
+ case 'G': gpsOn(); break;
+ case 'L': aload("altimeter.app.js"); break;
+ case 'N': mode = 1; note = ">"; mode_time = getTime(); break;
+ case 'O': aload("orloj.app.js"); break;
+ case 'T':
+ s = ' T';
+ d = new Date();
+ s += d.getHours() % 10;
+ s += add0(d.getMinutes());
+ buzz += toMorse(s);
+ break;
+ case 'R': aload("run.app.js"); break;
+ }
+}
+
+const morseDict = {
+ '.-': 'A',
+ '-...': 'B',
+ '-.-.': 'C',
+ '-..': 'D',
+ '.': 'E',
+ '..-.': 'F',
+ '--.': 'G',
+ '....': 'H',
+ '..': 'I',
+ '.---': 'J',
+ '-.-': 'K',
+ '.-..': 'L',
+ '--': 'M',
+ '-.': 'N',
+ '---': 'O',
+ '.--.': 'P',
+ '--.-': 'Q',
+ '.-.': 'R',
+ '...': 'S',
+ '-': 'T',
+ '..-': 'U',
+ '...-': 'V',
+ '.--': 'W',
+ '-..-': 'X',
+ '-.--': 'Y',
+ '--..': 'Z',
+ '.----': '1',
+ '..---': '2',
+ '...--': '3',
+ '....-': '4',
+ '.....': '5',
+ '----.': '9',
+ '---..': '8',
+ '--...': '7',
+ '-....': '6',
+ '-----': '0',
+ };
+
+let asciiDict = {};
+
+for (let k in morseDict) {
+ print(k, morseDict[k]);
+ asciiDict[morseDict[k]] = k;
+}
+
+
+function morseToAscii(morse) {
+ return morseDict[morse];
+}
+
+function asciiToMorse(char) {
+ return asciiDict[char];
+}
+
+function morseHandler() {
+ inputHandler(morseToAscii(inm));
+ inm = "";
+ l = "";
+}
+
+function touchHandler(d) {
+ let x = Math.floor(d.x);
+ let y = Math.floor(d.y);
+
+ g.setColor(0.25, 0, 0);
+ g.fillCircle(W-x, W-y, 5);
+
+ if (d.b) {
+ if (x < W/2 && y < H/2 && l != ".u") {
+ inm = inm + ".";
+ l = ".u";
+ }
+ if (x > W/2 && y < H/2 && l != "-u") {
+ inm = inm + "-";
+ l = "-u";
+ }
+ if (x < W/2 && y > H/2 && l != ".d") {
+ inm = inm + ".";
+ l = ".d";
+ }
+ if (x > W/2 && y > H/2 && l != "-d") {
+ inm = inm + "-";
+ l = "-d";
+ }
+
+ } else
+ morseHandler();
+
+ print(inm, "drag:", d);
+}
+
+function add0(i) {
+ if (i > 9) {
+ return ""+i;
+ } else {
+ return "0"+i;
+ }
+}
+
+var lastHour = -1, lastMin = -1;
+
+function logstamp(s) {
+ logfile.write("utime=" + getTime() + " " + s + "\n");
+}
+
+function loggps(fix) {
+ logfile.write(fix.lat + " " + fix.lon + " ");
+ logstamp("");
+}
+
+function hourly() {
+ print("hourly");
+ s = ' T';
+ if (is_active)
+ buzz += toMorse(s);
+ logstamp("");
+}
+
+function fivemin() {
+ print("fivemin");
+ s = ' B';
+ bat = E.getBattery();
+ if (bat < 45) {
+ s = s+(bat/5);
+ if (is_active)
+ buzz += toMorse(s);
+ }
+ if (0)
+ Bangle.getPressure().then((x) => { cur_altitude = x.altitude;
+ cur_temperature = x.temperature; },
+ print)
+ .catch(print);
+}
+
+function every(now) {
+ if ((mode > 0) && (mode_time - getTime() > 60)) {
+ if (mode == 1) {
+ logstamp(">" + note);
+ }
+ mode = 0;
+ }
+ if (gps_on && getTime() - gps_on > gps_limit) {
+ Bangle.setGPSPower(0, "sixths");
+ gps_on = 0;
+ }
+
+ if (lastHour != now.getHours()) {
+ lastHour = now.getHours();
+ hourly();
+ }
+ if (lastMin / 5 != now.getMinutes() / 5) {
+ lastMin = now.getMinutes();
+ fivemin();
+ }
+
+}
+
+// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
+// https://www.movable-type.co.uk/scripts/latlong.html
+// (Equirectangular approximation)
+function calcDistance(a,b) {
+ function radians(a) { return a*Math.PI/180; }
+ var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
+ var y = radians(b.lat-a.lat);
+ return Math.sqrt(x*x + y*y) * 6371000;
+}
+
+function draw() {
+ g.setColor(1, 1, 1);
+ g.fillRect(0, 25, W, H);
+ g.setFont('Vector', 60);
+
+ g.setColor(0, 0, 0);
+ g.setFontAlign(-1, 1);
+ let now = new Date();
+ g.drawString(now.getHours() + ":" + add0(now.getMinutes()), 10, 90);
+
+ every(now);
+
+ let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps;
+
+ g.setFont('Vector', 26);
+
+ const weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
+
+ g.drawString(weekday[now.getDay()] + "" + now.getDate() + ". " + km.toFixed(1) + "km", 10, 115);
+
+ if (gps_on) {
+ if (!last_restart) {
+ d = (getTime()-last_pause);
+ if (last_fix)
+ msg = "PL"+ (getTime()-last_fix).toFixed(0);
+ else
+ msg = "PN"+ (getTime()-gps_on).toFixed(0);
+
+ print("gps on, paused ", d, gps_needed);
+ if (d > gps_needed * 2) {
+ gpsRestart();
+ }
+ } else {
+ fix = Bangle.getGPSFix();
+ if (fix.fix && fix.lat) {
+ if (!prev_fix) {
+ prev_fix = fix;
+ }
+ msg = fix.speed.toFixed(1) + " km/h";
+ if (!last_fstart)
+ last_fstart = getTime();
+ last_fix = getTime();
+ gps_needed = 60;
+ loggps(fix);
+ print("GPS FIX", msg);
+ d = calcDistance(fix, prev_fix);
+ if (d > 30) {
+ prev_fix = fix;
+ gps_dist += d/1000;
+ }
+ } else {
+ if (last_fix)
+ msg = "L"+ (getTime()-last_fix).toFixed(0);
+ else
+ msg = "N"+ (getTime()-gps_on).toFixed(0);
+ }
+
+ d = (getTime()-last_restart);
+ d2 = (getTime()-last_fstart);
+ print("gps on, restarted ", d, gps_needed, d2, fix.lat);
+ if (d > gps_needed || (last_fstart && d2 > 10)) {
+ gpsPause();
+ gps_needed = gps_needed * 1.5;
+ print("Pausing, next try", gps_needed);
+ }
+ }
+ msg += " "+gps_dist.toFixed(1)+"km";
+ } else {
+ msg = note;
+ }
+ g.drawString(msg, 10, 145);
+ if (is_active) {
+ g.drawString("act " + (cur_altitude - alt_adjust).toFixed(0), 10, 175);
+ } else {
+ alt_adjust = cur_altitude - rest_altitude;
+ g.drawString(alt_adjust.toFixed(0) + "m " + cur_temperature.toFixed(1)+"C", 10, 175);
+ }
+ queueDraw();
+}
+
+function draw_all() {
+ g.setColor(0, 0, 0);
+ g.fillRect(0, 0, W, H);
+ g.setFont('Vector', 36);
+
+ g.setColor(1, 1, 1);
+ g.setFontAlign(-1, 1);
+ let now = new Date();
+ g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40);
+
+ acc = Bangle.getAccel();
+ let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y;
+ let diff = ax * ax + ay * ay + az * az;
+ diff = diff * 3;
+ if (diff > 1)
+ diff = 1;
+
+ co = Bangle.getCompass();
+ step = Bangle.getStepCount();
+ bat = E.getBattery();
+ Bangle.getPressure().then((x) => { alt = x.altitude; temp = x.temperature; },
+ print);
+
+ g.setColor(0, 1, 0);
+ g.drawCircle(cx, cy, sc);
+
+ if (0) {
+ g.setColor(0, 0.25, 0);
+ g.fillCircle(cx + sc * acc.x, cy + sc * acc.y, 5);
+ g.setColor(0, 0, 0.25);
+ g.fillCircle(cx + sc * acc.x, cy + sc * acc.z, 5);
+ }
+ if (0) {
+ print(co.dx, co.dy, co.dz);
+ g.setColor(0, 0.25, 0);
+ g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dy / 1500, 5);
+ g.setColor(0, 0, 0.25);
+ g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dz / 400, 5);
+ }
+ if (1) {
+ h = co.heading / 360 * 2 * Math.PI;
+ g.setColor(0, 0, 0.5);
+ g.fillCircle(cx + sc * Math.sin(h), cy + sc * Math.cos(h), 5);
+ }
+
+ g.setColor(1, 1, 1);
+
+ g.setFont('Vector', 22);
+ g.drawString(now.getDate()+"."+(now.getMonth()+1)+" "+now.getDay(), 3, 60);
+ g.drawString(msg, 3, 80);
+ g.drawString("S" + step + " B" + Math.round(bat/10) + (Bangle.isCharging()?"c":""), 3, 100);
+ g.drawString("A" + Math.round(alt) + " T" + Math.round(temp), 3, 120);
+ g.drawString("C" + Math.round(co.heading) + " B" + bpm, 3, 140);
+
+ queueDraw();
+}
+
+function accelTask() {
+ tm = 100;
+ acc = Bangle.getAccel();
+ en = !Bangle.isLocked();
+ if (en && acc.z < -0.95) {
+ msg = "Level";
+ buzz = ".-..";
+ tm = 3000;
+ }
+ if (en && acc.x < -0.80) {
+ msg = "Down";
+ buzz = "-..";
+ tm = 3000;
+ }
+ if (en && acc.x > 0.95) {
+ msg = "Up";
+ buzz = "..-";
+ tm = 3000;
+ }
+
+ setTimeout(accelTask, tm);
+}
+
+function buzzTask() {
+ if (buzz != "") {
+ now = buzz[0];
+ buzz = buzz.substring(1);
+ dot = 100;
+ if (now == " ") {
+ setTimeout(buzzTask, 300);
+ } else if (now == ".") {
+ Bangle.buzz(dot, 1);
+ setTimeout(buzzTask, 2*dot);
+ } else if (now == "-") {
+ Bangle.buzz(3*dot, 1);
+ setTimeout(buzzTask, 4*dot);
+ } else if (now == "/") {
+ setTimeout(buzzTask, 6*dot);
+ } else print("Unknown character -- ", now, buzz);
+ } else
+ setTimeout(buzzTask, 60000);
+}
+
+function aliveTask() {
+ function cmp(s) {
+ let d = acc[s] - last_acc[s];
+ return d < -0.03 || d > 0.03;
+ }
+ // HRM seems to detect hand quite nicely
+ acc = Bangle.getAccel();
+ is_active = false;
+ if (cmp("x") || cmp("y") || cmp("z")) {
+ print("active");
+ is_active = true;
+ }
+ last_acc = acc;
+
+ setTimeout(aliveTask, 60000);
+}
+
+var drawTimeout;
+
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ if (0) // FIXME
+ next = 60000;
+ else
+ next = 1000;
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, next - (Date.now() % next));
+
+}
+
+function start() {
+ Bangle.on("drag", touchHandler);
+ if (0)
+ Bangle.on("accel", accelHandler);
+ if (0) {
+ Bangle.setCompassPower(1, "sixths");
+ Bangle.setBarometerPower(1, "sixths");
+ Bangle.setHRMPower(1, "sixths");
+ Bangle.setGPSPower(1, "sixths");
+ Bangle.on("HRM", (hrm) => { bpm = hrm.bpm; } );
+ }
+
+ draw();
+ buzzTask();
+ //accelTask();
+
+ if (1) {
+ last_acc = Bangle.getAccel();
+ aliveTask();
+ }
+}
+
+g.reset();
+Bangle.setUI();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+let logfile = require("Storage").open("sixths.egt", "a");
+
+start();
diff --git a/apps/sixths/app.png b/apps/sixths/app.png
new file mode 100644
index 000000000..cf1e13d65
Binary files /dev/null and b/apps/sixths/app.png differ
diff --git a/apps/sixths/metadata.json b/apps/sixths/metadata.json
new file mode 100644
index 000000000..ece88348d
--- /dev/null
+++ b/apps/sixths/metadata.json
@@ -0,0 +1,13 @@
+{ "id": "sixths",
+ "name": "Sixth sense",
+ "version":"0.01",
+ "description": "Clock for outdoor use with GPS support",
+ "icon": "app.png",
+ "readme": "README.md",
+ "supports" : ["BANGLEJS2"],
+ "tags": "",
+ "storage": [
+ {"name":"sixths.app.js","url":"app.js"},
+ {"name":"sixths.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/snepwatch/ChangeLog b/apps/snepwatch/ChangeLog
new file mode 100644
index 000000000..fa064b2c0
--- /dev/null
+++ b/apps/snepwatch/ChangeLog
@@ -0,0 +1 @@
+1.00: Initial release
diff --git a/apps/snepwatch/README.md b/apps/snepwatch/README.md
new file mode 100644
index 000000000..7bf5ec5cd
--- /dev/null
+++ b/apps/snepwatch/README.md
@@ -0,0 +1,17 @@
+# Snepwatch
+
+
+
+Features:
+ * This watch face uses the Terminus font.
+ * Background, digit-outline, and digit-fill colours are all customizable from the settings menu
+ * Text can be configured to either black or white
+ * Today's step count is always shown at the bottom
+ * Heart-rate is shown when we have a valid reading within the last ten seconds
+ * The heart rate monitor can be activated by another app running in the background (eg, Recorder)
+ * Or, the heart rate monitor is activated when you unlock the screen (eg, by pressing the button)
+ * Sometimes a single unlock-period is not long enough to achieve a usable reading, so a second press can be used
+ * The heart rate monitor will be returned to deactivated when the screen remains locked for 15 seconds.
+ * Hidden widget-bar. Swipe down to see the widgets.
+ * "Sneptember" instead of "September", because snow leopards are excellent :3
+ * Fast Loading
diff --git a/apps/snepwatch/app.js b/apps/snepwatch/app.js
new file mode 100644
index 000000000..833285644
--- /dev/null
+++ b/apps/snepwatch/app.js
@@ -0,0 +1,242 @@
+/*
+ * SnepWatch - Bangle JS 2 Port
+ * JoppyFurr 2023
+ */
+
+{
+ Graphics.prototype.setFontTerminus_14 = function (scale) {
+ g.setFontCustom (atob ("AB/oJhGIZBf4AAAAAgUB/8AQBAAAGDgWCYRiFwQAABhoBiGIYhd4AAABgKBIIhCP/AAAPiiGIYhiGHgAAA/khiGIYhB4AAAgCAIHhiYOAAAAHeiGIYhiF3gAAB4IRhGEYSfwAEAoOjEyMXBQCAA="), 48, 8, 10);
+ };
+
+ Graphics.prototype.setFontTerminus_18 = function (scale) {
+ /* TODO: Strip out unused characters - Eg, encode zero-width */
+ g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("AH4A/AH4AnoEHBpcgB4MAwEBgECgEEgEIItf/4UEokQxFBiMCkkEn/wCY0CgMIgU//hHCiAQGh0GkEUoFIwMRgkiiFD4GACQkwmGAiMIBwNEkA7Bh98EoqPBiUAog6BgkQiEgv/4CIvjhFBiGCkMEoUIwkRg/ACQv8IoIODHYVAj43FuC4EgfEjEI4EeOA334RZEHYc9SosD8EIkMQoUgwlBhGCkH/QQoA/AH6VD/+IkDHBkGAoMAwUA//gEq8/+GAiMAkUAokAxEBhkMCIkB//iBwkQgUQhEfG7Ef/0hgFCgGEgMIgUAgip/AAM4gEIgEQQwVH/4HBAFEH/+EgEGcwIDCmEAwDtB4CVERgIBGDoOAcB8P/jgHhEEbYMgiFAkGAnylrnkwwkRhEiiFEkGIoMMvhKFgD+Dh//XYMggFAJVd//AxBgOAhkAjEAAYMEn6XFAH4A/AE8PgMhgVCgmEhMIiUQj/gCIkf/0EgkIhEQiEgkFAoF/EgsH+AOFwGBgMBggSFj/ABwkCgQsBfwQAEv8BiMCkUEokIxERiEeoBvXJQUSkEkoFIwGRgMj/+ACIcB//gFoOAgMAgUALQMHJQwACLIM3/kAhC6bO4I0DhUAkUAwQsBCpBEBx//CwQAagbgBLAUQgEggB3Bv6CEcBh1BAAk//jgIDoJKZQQJMBJQpJFACodCh/+JQbRHUQLwFQ4U/8BKFH4PAgFggEwgOAhkACIoAmJQUggVAgmAhMAiU//AiVO4MEiEEoEFgFEoFBAYMAqCOCgE+gC6BAIkH/Ef8E/wAOBAAI="))), 32, 10, 15);
+ };
+
+ Graphics.prototype.setFontDigits = function (scale) {
+ g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("AEE//4AC4EDAof8Bid8EQMB+ED8/AAwMfgE+BisD8AMEnwIBBisHBAQMCCoYMUh+ABgn4AoIMUTbIMGAH4AhdoKOC8DtBAwTYBBiQHBBgbtCBi1/VAf/84FE/wMSFYZNDAAY4EBmoA7n7PCg/+gf/BQV/8AMSvgLCh+D8/AAwX4nwMWj6OBBgUB+EDBiysBBgasCBiwIDAYMHCoQMTRwP4dIc//+ABiz/8AAbpD//AewcP/gMTdIaOEAAT2EBiLCdBn7pDeQKOC/+D/6bDBiIA/YwbyCAATyDBikH4IMEvkfBi0PwAME/EBBiwDBBgYDCBi0+BAZrB8AMXv7tBAAXnAon+BiQrBAH0HIYP8AgPAJoXAbAIMTvgDCSYPnCASTCBn4M2gE/RQIQDS4QMSf/gAEgJLBeQcfAoRNBBiUD8BwD8E+RwgMSdP4MhUgTpFewYMVAHprCAAZrCBnf//wMDh//WoQMTn/waYn/GYgMRh7GDBgLTDBid/FQQMC/4qBBioA/QoX/wf/OAKSB/AIBOAIMSRwKxDZ4SxCnwM/BmibZBgwA/JoREDJoJRBBiqFDRwkD8CbDBn4MWeQIMEvj6DBh6OCAAOARwQAB/CbDBiAA/AH4ACh/8gEH/wGB/6xBv7YBBiYA/AH4A=="))), 48, 40, 52);
+ };
+
+ Graphics.prototype.setFontOutline = function (scale) {
+ g.setFontCustom (E.toString (require ("heatshrink").decompress (atob ("ADMD/4AC/kAoALDkEEAocCBhkH4AGDn2DEgP+h/8jEgC4WAoOCC4UIggMJnwMEgkfgAMDoIIBBgcQBhV8BgkInwMECoYMCEQQMI/AMEiF8BgcBOwQMDQYQMHn/wv59B4IdCVoXgVDEAn7JDVogA/ACEB+AECXwLtBRwUQfQIMCbAIMLSwIMEoImDoL6CBgg4DBg0X4KbD4IkCAAUYKQoMSi4lDEwILEHAhnDBmKoBAGUD/4ECv/goDPChEBgkASAWAiAMLi/ABgX4gODEwUQvkYkCzCglBwQyCiFEBhMB+AMEkH4BgcChEEBgmABhMD8AMEoPwBgasCBggIBBhEHfQIMDwfgBgcICoQMCEQYMGn//Bgd8EwPAF4VAgABBBgKbBAIIMPEwKvCnz/vAAcDbgUP/hLBdIMBW4L2DiECBhh4BBgc+wYmEbgabEAATpEBgibDWpoM/BhrcBPoP/4N8M4cD8D0BogZBTYUEBAKoCBhYmEEQIA1n/waYjyCAAT/CBh59BBgcInwMEwUgBgkiBhX4BgkQvgMDgMEgQMEoAMJgPwBgkg/AMDAYQMEwAMJa4IMEoKFBBgUEiAVDBgIiDBg0H4LaB8H//yQEgEYAokBBiUXEoIAC/z/ygF/G4PAcoKQDbAKdEbAIMSn2D//4BgN8jAQCUAQDBDoKgBBn4MqPoP/4IMBg6oBgfoCASXCDoVAS4IdDBhAdDZYYAzj44B//8c4QACKQIBBeQYMMnwMDn0Ej/4n4mBiFBTYkEiB9DBg18BgkYEwLp/BjWDUQPBvkAcofgCwL2DSQT2CAoQMMgYmBEQIAyLQIADNYQADPAQM0h//9AMCgIBBBgcQgC1CBhibBBglAEwi1BwAzDoEIcAgMEXIIMDiAmCBgI3BHgQMCaYYMGn/wBgYoBcAYoCOgYMBAIIMOg//+D/zAAZ4B/E///8SQMAogLBkCSBggIBOAIMLg54BvgMBn2DEwjPCXwVBAYK+DBn4MoPAJ9B//BQoKKCgfgVDEAEwgiBAGUDG4pEDAAJNBKIIMOPAkAnyFDTYkEiCbDAYIM/Bh18BgkInwMEwT6DBgMgBgzcB/kf/EgvitDAgKbBTIabCUwYMMEwIAB/D/wAH4AG//AgF/8EAiECgEIgMAAIK+BwDYBBiQmGAH4A5"))), 48, 40, 52);
+ };
+
+ var snepwatch_tick_timeout;
+ var snepwatch_hrm_timeout;
+ var snepwatch_hrm_show_timeout;
+ var heart_rate = 0;
+ var heart_rate_time = 0;
+
+ /* Load settings */
+ var settings = Object.assign ({
+ /* Default Values */
+ outline_r: 1,
+ outline_g: 0,
+ outline_b: 0,
+ fill_r: 0.5,
+ fill_g: 0,
+ fill_b: 0,
+ bg_r: 0,
+ bg_g: 0,
+ bg_b: 0,
+ text: 1,
+ }, require ('Storage').readJSON ("snepwatch.json", true) || {});
+
+ /*
+ * Tick once per minute.
+ */
+ let snepwatch_tick_queue = function () {
+ if (snepwatch_tick_timeout) {
+ clearTimeout (snepwatch_tick_timeout);
+ }
+
+ snepwatch_tick_timeout = setTimeout (function () {
+ snepwatch_tick_timeout = undefined;
+ snepwatch_tick ();
+ }, 60000 - (Date.now () % 60000));
+ };
+
+
+ /*
+ * Draw the heart rate sensor reading.
+ * The reading is only shown if it is from within the last 10 seconds.
+ * Assumes the Terminus_18 font is already selected.
+ */
+ let draw_heart_rate = function () {
+ let heart_rate_string = "--";
+ let hrm_show = false;
+
+ /* As we are about to show the heart rate,
+ * previously set timers are considered invalid */
+ if (snepwatch_hrm_show_timeout) {
+ clearTimeout (snepwatch_hrm_show_timeout);
+ }
+
+ /* Only show the heart rate if the measurement is recent */
+ if (heart_rate_time > Date.now () - 10000) {
+ hrm_show = true;
+ heart_rate_string = "" + heart_rate;
+ }
+
+ g.clearRect (17, 160, 88, 175);
+ g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text);
+ g.drawString (heart_rate_string, 17, 160);
+
+ /* If the heart rate was shown, check back when the reading
+ * would become stale so that it can be cleared. */
+ if (hrm_show) {
+ snepwatch_hrm_show_timeout = setTimeout (function () {
+ snepwatch_hrm_show_timeout = undefined;
+ draw_heart_rate ();
+ }, heart_rate_time + 10000 - Date.now ());
+ }
+ };
+
+
+ /*
+ * Called once per minute.
+ *
+ * Updates the time, date, and battery level.
+ */
+ let snepwatch_tick = function () {
+ /* Data */
+ let days = [ "Sun ", "Mon ", "Tue ", "Wed ", "Thu ", "Fri ", "Sat " ];
+ let months = [ " Jan", " Feb", " Mar", " Apr", " May", " June", " July", " Aug", " Snep", " Oct", " Nov", " Dec"];
+ let date = new Date ();
+ let charge_level = E.getBattery ();
+
+ /* Clear */
+ g.reset ();
+ g.setBgColor (settings.bg_r, settings.bg_g, settings.bg_b);
+ g.clear ();
+
+ /* Battery level - Note, '%' is encoded as ':' */
+ let battery_text = ((charge_level < 10) ? "0" : "") + charge_level + ":";
+ if (charge_level <= 16) {
+ g.setColor (1, 0, 0);
+ } else {
+ g.setColor (0, 0 + settings.text, 1);
+ }
+ g.setFont ("Terminus_14");
+ g.drawString (battery_text, 2, 2);
+
+ /* Date */
+ let day = days [ date.getDay () ];
+ let dd = date.getDate ();
+ dd = ((dd < 10) ? "0" : "") + dd;
+ let month = months [ date.getMonth () ];
+
+ let date_text = day + dd + month;
+ if (date_text.length < 11) {
+ date_text = " " + date_text;
+ }
+ g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text);
+ g.setFont ("Terminus_18");
+ g.drawString (date_text, 65, 2);
+
+ /* Time */
+ let hours = date.getHours ();
+ let minutes = date.getMinutes ();
+ let time_hh = ((hours < 10) ? "0" : "") + hours;
+ let time_mm = ((minutes < 10) ? "0" : "") + minutes;
+ g.setColor (settings.fill_r, settings.fill_g, settings.fill_b);
+ g.setFont ("Digits");
+ g.drawString (time_hh, -2, 60);
+ g.drawString (":", 71, 55);
+ g.drawString (time_mm, 98, 60);
+
+ g.setColor (settings.outline_r, settings.outline_g, settings.outline_b);
+ g.setFont ("Outline");
+ g.drawString (time_hh, -2, 60);
+ g.drawString (":", 71, 55);
+ g.drawString (time_mm, 98, 60);
+
+
+ /* Steps so far for the day */
+ let steps = Bangle.getHealthStatus ('day').steps;
+ let steps_string = "" + steps;
+ if (steps >= 1000) {
+ steps_string = steps_string.slice (0, -3) + "," + steps_string.slice (-3);
+ }
+
+ g.setFont("Terminus_18");
+ /* With dark text, use blue for the step symbol.
+ With light text, use green for the step symbol. */
+ g.setColor (0, 0 + settings.text, 1 - settings.text);
+ g.drawString ("{", 2, 144); /* Arrows */
+ g.setColor (1, 0, 0);
+ g.drawString ("|", 2, 160); /* Heart */
+ g.setColor (0 + settings.text, 0 + settings.text, 0 + settings.text);
+ g.drawString (steps_string, 17, 144);
+ draw_heart_rate ();
+
+ /* Queue up the next tick */
+ snepwatch_tick_queue ();
+ };
+
+
+ /* Callback for when the backlight state changes */
+ let display_cb = lock => {
+ if (lock) {
+ /* The backlight may not run for long enough to get a good reading.
+ Wait 15 seconds with the backlight off before disabling the sensor. */
+ snepwatch_hrm_timeout = setTimeout (function () {
+ snepwatch_hrm_timeout = undefined;
+ Bangle.setHRMPower (false, "snepwatch");
+ }, 15000);
+ } else {
+ if (snepwatch_hrm_timeout) {
+ clearTimeout (snepwatch_hrm_timeout);
+ snepwatch_hrm_timeout = undefined;
+ }
+ Bangle.setHRMPower (true, "snepwatch");
+ }
+ };
+
+ /* Callback for the heart rate monitor */
+ let heart_rate_cb = hrm => {
+ if (hrm.bpm > 0 && hrm.confidence > 50) {
+ heart_rate = hrm.bpm;
+ heart_rate_time = Date.now ();
+ }
+
+ g.setFont("Terminus_18");
+ draw_heart_rate ();
+ };
+
+ let previous_theme = g.theme;
+ g.setTheme ( { bg:"#000", fg:"#fff", dark:true } );
+
+ /* Initial call, will tick once per minute */
+ snepwatch_tick ();
+ Bangle.on ('lock', display_cb);
+ Bangle.on ('HRM', heart_rate_cb);
+
+ /* Use a swipe to show the widgets */
+ Bangle.loadWidgets ();
+ require ("widget_utils").swipeOn ();
+
+ /* Allow for Fast Loading */
+ Bangle.setUI ( { mode:"clock", remove:function () {
+ if (snepwatch_tick_timeout) {
+ if (snepwatch_tick_timeout) {
+ clearTimeout (snepwatch_tick_timeout);
+ }
+ if (snepwatch_hrm_timeout) {
+ clearTimeout (snepwatch_hrm_timeout);
+ }
+ if (snepwatch_hrm_show_timeout) {
+ clearTimeout (snepwatch_hrm_show_timeout);
+ }
+ Bangle.removeListener('lcdPower', display_cb);
+ Bangle.removeListener('HRM', heart_rate_cb);
+ Bangle.setHRMPower (false, "snepwatch");
+ delete Graphics.prototype.setFontTerminus_14;
+ delete Graphics.prototype.setFontTerminus_18;
+ delete Graphics.prototype.setFontDigits;
+ delete Graphics.prototype.setFontOutline;
+ g.setTheme (previous_theme);
+ require ("widget_utils").show();
+ }
+ } } );
+}
diff --git a/apps/snepwatch/app.png b/apps/snepwatch/app.png
new file mode 100644
index 000000000..50eb5c388
Binary files /dev/null and b/apps/snepwatch/app.png differ
diff --git a/apps/snepwatch/metadata.json b/apps/snepwatch/metadata.json
new file mode 100644
index 000000000..b33f22d11
--- /dev/null
+++ b/apps/snepwatch/metadata.json
@@ -0,0 +1,19 @@
+{
+ "id": "snepwatch",
+ "name": "Snepwatch",
+ "version": "1.00",
+ "description": "A configurable watch face using the Terminus font",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"snepwatch.app.js","url":"app.js"},
+ {"name":"snepwatch.settings.js","url":"settings.js"},
+ {"name":"snepwatch.img","url":"snepwatch.img","evaluate":false}
+ ],
+ "data":[{"name":"snepwatch.settings.json"}]
+}
diff --git a/apps/snepwatch/screenshot-dark.png b/apps/snepwatch/screenshot-dark.png
new file mode 100644
index 000000000..d9a1ccb51
Binary files /dev/null and b/apps/snepwatch/screenshot-dark.png differ
diff --git a/apps/snepwatch/screenshot-light.png b/apps/snepwatch/screenshot-light.png
new file mode 100644
index 000000000..074747423
Binary files /dev/null and b/apps/snepwatch/screenshot-light.png differ
diff --git a/apps/snepwatch/settings.js b/apps/snepwatch/settings.js
new file mode 100644
index 000000000..57ae94d52
--- /dev/null
+++ b/apps/snepwatch/settings.js
@@ -0,0 +1,81 @@
+(
+ function(back) {
+ var FILE = "snepwatch.json";
+
+ /* Load settings */
+ var settings = Object.assign ({
+ /* Default Values */
+ outline_r: 1,
+ outline_g: 0,
+ outline_b: 0,
+ fill_r: 0.5,
+ fill_g: 0,
+ fill_b: 0,
+ bg_r: 0,
+ bg_g: 0,
+ bg_b: 0,
+ text: 1
+ ,
+ }, require ('Storage').readJSON (FILE, true) || {});
+
+ function write_settings () {
+ require ('Storage').writeJSON (FILE, settings);
+ }
+
+ /* Show the menu */
+ var main_menu = {
+ "" : { "title": "Snepwatch",
+ back : function() { back (); }},
+ "Outline Colour": function () { E.showMenu (outline_menu); },
+ "Fill Colour": function () { E.showMenu (fill_menu); },
+ "Background Colour": function () { E.showMenu (background_menu); },
+ "Text": { value: (settings.text == 1),
+ format: v => v ? "Light" : "Dark",
+ onchange: v => { settings.text = v; write_settings (); }},
+ };
+
+ var outline_menu = {
+ "": { title : "Outline Colour",
+ back : function() { E.showMenu (main_menu); } },
+ "Red": { value: settings.outline_r,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.outline_r = v; write_settings (); }},
+ "Green": { value: settings.outline_g,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.outline_g = v; write_settings (); }},
+ "Blue": { value: settings.outline_b,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.outline_b = v; write_settings (); }},
+ };
+
+ var fill_menu = {
+ "" : { title : "Fill Colour",
+ back : function() { E.showMenu (main_menu); } },
+ "Red": { value: settings.fill_r,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.fill_r = v; write_settings (); }},
+ "Green": { value: settings.fill_g,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.fill_g = v; write_settings (); }},
+ "Blue": { value: settings.fill_b,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.fill_b = v; write_settings (); }},
+ };
+
+ var background_menu = {
+ "" : { title : "Background Colour",
+ back : function() { E.showMenu (main_menu); } },
+ "Red": { value: settings.bg_r,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.bg_r = v; write_settings (); }},
+ "Green": { value: settings.bg_g,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.bg_g = v; write_settings (); }},
+ "Blue": { value: settings.bg_b,
+ min: 0, max: 1, step: 0.5, wrap: true,
+ onchange: v => { settings.bg_b = v; write_settings (); }},
+ };
+
+ E.showMenu (main_menu);
+ }
+)
diff --git a/apps/snepwatch/snepwatch.img b/apps/snepwatch/snepwatch.img
new file mode 100644
index 000000000..ae0527cbb
Binary files /dev/null and b/apps/snepwatch/snepwatch.img differ
diff --git a/apps/sokoban/ChangeLog b/apps/sokoban/ChangeLog
new file mode 100644
index 000000000..f931ec63e
--- /dev/null
+++ b/apps/sokoban/ChangeLog
@@ -0,0 +1,5 @@
+0.01: Initial code
+0.02:
+ * Fix for last level offsets parsing
+ * Fix for title display
+
diff --git a/apps/sokoban/README.md b/apps/sokoban/README.md
new file mode 100644
index 000000000..36097d66f
--- /dev/null
+++ b/apps/sokoban/README.md
@@ -0,0 +1,20 @@
+# Sokoban
+
+Classic Sokoban game.
+
+Tap screen at bottom/top/left/right to push boxes into their destinations.
+Swipe to undo.
+
+
+
+You play the yellow disk (rice hat seen from above).
+Each level has a set of crates (brown if incorrectly placed or blue if correctly placed)
+and a set of placeholders (empty blue squares). Simply push all crates into their placeholders.
+Remember you can push but never pull.
+
+## Creator
+
+Levels are the [Microban](http://www.abelmartin.com/rj/sokobanJS/Skinner/David%20W.%20Skinner%20-%20Sokoban.htm) levels
+by David W. Skinner.
+
+frederic.wagner@imag.fr
diff --git a/apps/sokoban/TODO b/apps/sokoban/TODO
new file mode 100644
index 000000000..dcad68d38
--- /dev/null
+++ b/apps/sokoban/TODO
@@ -0,0 +1,2 @@
+- background
+- win screen + final win screen
diff --git a/apps/sokoban/app-icon.js b/apps/sokoban/app-icon.js
new file mode 100644
index 000000000..e8a1d4b0f
--- /dev/null
+++ b/apps/sokoban/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMN7oXV7vd6AuVAAIXaAwYAEC75fGC6KPFC58BiMQJxAXLiIABDAcBigXRAAIFDC5pGBAAwvOCQYbDiBfOLwgYBO54qER6QXDexIXJRggXRIxIXpb4wXMLwYvTdIinSC4gYFC5xiIC54YDC54SBIQZHRC4gcFC5hyEC4KPPLIrZGC5IWCLwgXPUApeGC5KfGC6DnGIwwXLB4gXQgI/FAwy/MAH4A/ABgA=="))
diff --git a/apps/sokoban/app.js b/apps/sokoban/app.js
new file mode 100644
index 000000000..3915556e3
--- /dev/null
+++ b/apps/sokoban/app.js
@@ -0,0 +1,471 @@
+// basic shapes
+const SPACE = 0;
+const WALL = 1;
+const PLAYER = 2;
+const BOX = 3;
+const HOLE = 4;
+const FILLED = 5;
+
+// basic directions
+const LEFT = 0;
+const UP = 1;
+const DOWN = 2;
+const RIGHT = 3;
+
+function go(line, column, direction) {
+ let destination_line = line;
+ let destination_column = column;
+ if (direction == LEFT) {
+ destination_column -= 1;
+ } else if (direction == RIGHT) {
+ destination_column += 1;
+ } else if (direction == UP) {
+ destination_line -= 1;
+ } else {
+ // direction is down
+ destination_line += 1;
+ }
+ return [destination_line, destination_column];
+}
+
+Bangle.setOptions({
+ lockTimeout: 60000,
+ backlightTimeout: 60000,
+});
+
+let s = require("Storage");
+
+// parse the levels a bit more to figure offsets delimiting next map.
+function next_map_offsets(filename, start_offset) {
+ let raw_maps = s.readArrayBuffer(filename);
+ let offsets = [];
+ // this is a very dumb parser : map starts three chars after the end of a line with a ';'
+ // and ends two chars before next ';'
+ let comment_line = true;
+ for (let i = start_offset; i < raw_maps.length; i++) {
+ if (raw_maps[i] == 59) { // ';'
+ if (offsets.length != 0) {
+ offsets.push(i - 2);
+ return offsets;
+ }
+ comment_line = true;
+ } else if (raw_maps[i] == 10) { // '\n'
+ if (comment_line) {
+ comment_line = false;
+ offsets.push(i + 3);
+ }
+ }
+ }
+ if (offsets.length == 1) {
+ offsets.push(raw_maps.length);
+ }
+ return offsets;
+}
+
+let config = s.readJSON("sokoban.json", true);
+if (config === undefined) {
+ let initial_offsets = next_map_offsets("sokoban.microban.sok", 0);
+ config = {
+ levels_sets: ["sokoban.microban.sok"], // all known files containing levels
+ levels_set: 0, // which set are we using ?
+ current_maps: [0], // what is current map on each set ?
+ offsets: [initial_offsets], // known offsets for each levels set (binary positions of maps in each file)
+ };
+ s.writeJSON("sokoban.json", config);
+}
+
+let map = null;
+let in_menu = false;
+let history = null; // store history to allow undos
+
+
+function load_map(filename, start_offset, end_offset, name) {
+ console.log("loading map in", filename, "between", start_offset, "and", end_offset);
+ let raw_map = new Uint8Array(s.readArrayBuffer(filename), start_offset, end_offset - start_offset);
+ let dimensions = map_dimensions(raw_map);
+ history = [];
+ return new Map(dimensions, raw_map, filename, name);
+}
+
+function load_current_map() {
+ let current_set = config.levels_set;
+ let offsets = config.offsets[current_set];
+ let set_filename = config.levels_sets[current_set];
+ let set_name = set_filename.substring(8, set_filename.length - 4); // remove '.txt' and 'sokoban.'
+ let current_map = config.current_maps[current_set];
+ map = load_map(set_filename, offsets[2 * current_map], offsets[2 * current_map + 1], set_name + " " + (current_map + 1));
+ map.display();
+}
+
+function next_map() {
+ let current_set = config.levels_set;
+ let current_map = config.current_maps[current_set];
+ let offsets = config.offsets[current_set];
+ let won = false;
+ if (2 * (current_map + 1) >= offsets.length) {
+ // we parse some new offsets
+ let new_offsets = next_map_offsets(config.levels_sets[current_set], offsets[offsets.length - 1] + 2); // +2 since we need to start at ';' (we did -2 from ';' in previous parser call)
+ if (new_offsets.length != 2) {
+ won = true;
+ E.showAlert("You Win", "All levels completed").then(function() {
+ load();
+ });
+ } else {
+ config.offsets[current_set].push(new_offsets[0]);
+ config.offsets[current_set].push(new_offsets[1]);
+ }
+ }
+ if (!won) {
+ config.current_maps[current_set]++;
+ s.writeJSON("sokoban.json", config);
+ load_current_map();
+ }
+}
+
+function previous_map() {
+ let current_set = config.levels_set;
+ let current_map = config.current_maps[current_set];
+ if (current_map > 0) {
+ current_map--;
+ config.current_maps[current_set] = current_map;
+ s.writeJSON("sokoban.json", config);
+ load_current_map();
+ }
+}
+
+function map_dimensions(raw_map) {
+ let line_start = 0;
+ let width = 0;
+ let height = 0;
+ for (let i = 0; i < raw_map.length; i++) {
+ if (raw_map[i] == 10) {
+ height += 1;
+ let line_width = i - line_start;
+ if (i > 0 && raw_map[i - 1] == 13) {
+ line_width -= 1; // remove \r
+ }
+ width = Math.max(line_width, width);
+ line_start = i + 1;
+ }
+ }
+ return [width, height];
+}
+
+class Map {
+ constructor(dimensions, raw_map, filename, name) {
+ this.filename = filename;
+ this.name = name;
+ this.width = dimensions[0];
+ this.height = dimensions[1];
+ this.remaining_holes = 0;
+ // start by creating an empty map
+ this.m = [];
+ for (let i = 0; i < this.height; i++) {
+ let line = new Uint8Array(this.width);
+ for (let j = 0; j < this.width; j++) {
+ line[j] = SPACE;
+ }
+ this.m.push(line);
+ }
+ // now fill with raw_map's content
+ let current_line = 0;
+ let line_start = 0;
+ for (let i = 0; i < raw_map.length; i++) {
+ if (raw_map[i] == 32) {
+ this.m[current_line][i - line_start] = SPACE;
+ } else if (raw_map[i] == 43) {
+ // '+'
+ this.remaining_holes += 1;
+ this.m[current_line][i - line_start] = HOLE;
+ this.player_column = i - line_start;
+ this.player_line = current_line;
+ } else if (raw_map[i] == 10) {
+ current_line += 1;
+ line_start = i + 1;
+ } else if (raw_map[i] == 35) {
+ this.m[current_line][i - line_start] = WALL;
+ } else if (raw_map[i] == 36) {
+ this.m[current_line][i - line_start] = BOX;
+ } else if (raw_map[i] == 46) {
+ this.remaining_holes += 1;
+ this.m[current_line][i - line_start] = HOLE;
+ } else if (raw_map[i] == 64) {
+ this.m[current_line][i - line_start] = SPACE;
+ this.player_column = i - line_start;
+ this.player_line = current_line;
+ } else if (raw_map[i] == 42) {
+ this.m[current_line][i - line_start] = FILLED;
+ } else if (raw_map[i] != 13) {
+ console.log("warning unknown map content", raw_map[i]);
+ }
+ }
+ this.steps = 0;
+ this.calibrate();
+ }
+ // compute scale
+ calibrate() {
+ let r = Bangle.appRect;
+ let rwidth = 1 + r.x2 - r.x;
+ let rheight = 1 + r.y2 - r.y;
+ let cell_width = Math.floor(rwidth / this.width);
+ let cell_height = Math.floor(rheight / this.height);
+ let cell_scale = Math.min(cell_width, cell_height); // we want square cells
+ let real_width = this.width * cell_scale;
+ let real_height = this.height * cell_scale;
+ let sx = r.x + Math.ceil((rwidth - real_width) / 2);
+ let sy = r.y + Math.ceil((rheight - real_height) / 2);
+ this.sx = sx;
+ this.sy = sy;
+ this.cell_scale = cell_scale;
+ }
+ undo(direction, pushing) {
+ this.steps -= 1;
+
+ let previous_position = go(this.player_line, this.player_column, 3 - direction);
+ let previous_line = previous_position[0];
+ let previous_column = previous_position[1];
+
+ if (pushing) {
+ // put the box back on current player position
+ let currently_on = this.m[this.player_line][this.player_column];
+ if (currently_on == HOLE) {
+ this.remaining_holes -= 1;
+ this.m[this.player_line][this.player_column] = FILLED;
+ } else {
+ this.m[this.player_line][this.player_column] = BOX;
+ }
+ // now, remove the box from its current position
+ let current_box_position = go(this.player_line, this.player_column, direction);
+ let box_line = current_box_position[0];
+ let box_column = current_box_position[1];
+ let box_on = this.m[box_line][box_column];
+ if (box_on == FILLED) {
+ this.remaining_holes += 1;
+ this.m[box_line][box_column] = HOLE;
+ } else {
+ this.m[box_line][box_column] = SPACE;
+ }
+ this.display_cell(box_line, box_column);
+ }
+ // cancel player display
+ this.display_cell(this.player_line, this.player_column);
+ // re-display player at previous position
+ this.player_line = previous_line;
+ this.player_column = previous_column;
+ this.display_player();
+ }
+ move(direction) {
+ let destination_position = go(this.player_line, this.player_column, direction);
+ let destination_line = destination_position[0];
+ let destination_column = destination_position[1];
+ let destination = this.m[destination_line][destination_column];
+ let pushing = false;
+ if (destination == BOX || destination == SPACE || destination == HOLE || destination == FILLED) {
+ if (destination == BOX || destination == FILLED) {
+ pushing = true;
+ let after_line = 2 * destination_line - this.player_line;
+ let after_column = 2 * destination_column - this.player_column;
+ let after = this.m[after_line][after_column];
+ let will_remain = SPACE;
+ if (destination == FILLED) {
+ will_remain = HOLE;
+ }
+ if (after == SPACE) {
+ if (will_remain == HOLE) {
+ this.remaining_holes += 1;
+ }
+ this.m[destination_line][destination_column] = will_remain;
+ this.m[after_line][after_column] = BOX;
+ } else if (after == HOLE) {
+ this.m[destination_line][destination_column] = will_remain;
+ this.m[after_line][after_column] = FILLED;
+ if (will_remain == SPACE) {
+ this.remaining_holes -= 1;
+ }
+ if (this.remaining_holes == 0) {
+ in_menu = true;
+ this.steps += 1;
+ E.showAlert("" + this.steps + "steps", "You Win").then(function() {
+ in_menu = false;
+ next_map();
+ });
+ return;
+ }
+ } else {
+ return;
+ }
+ this.display_cell(after_line, after_column);
+ this.display_cell(destination_line, destination_column);
+ }
+ history.push([direction, pushing]);
+ this.display_cell(this.player_line, this.player_column);
+ this.steps += 1;
+ this.player_line = destination_line;
+ this.player_column = destination_column;
+ this.display_player();
+ // this.display();
+ }
+ }
+ display_player() {
+ sx = this.sx;
+ sy = this.sy;
+ cell_scale = this.cell_scale;
+ g.setColor(0.8, 0.8, 0).fillCircle(sx + (0.5 + this.player_column) * cell_scale, sy + (0.5 + this.player_line) * cell_scale, cell_scale / 2 - 1); // -1 because otherwise it overfills
+ }
+ display_cell(line, column) {
+ sx = this.sx;
+ sy = this.sy;
+ cell_scale = this.cell_scale;
+ let shape = this.m[line][column];
+ if (shape == WALL) {
+ if (cell_scale < 10) {
+ g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale);
+ } else {
+ g.setColor(0.5, 0.5, 0.5).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale);
+ g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 0.35) * cell_scale, sy + (line + 0.45) * cell_scale);
+ g.fillRect(sx + (column + 0.55) * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.45) * cell_scale);
+ g.fillRect(sx + column * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 0.65) * cell_scale, sy + (line + 0.95) * cell_scale);
+ g.fillRect(sx + (column + 0.85) * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.95) * cell_scale);
+ }
+ } else if (shape == BOX) {
+ let border = Math.floor((cell_scale - 2) / 4);
+ if (border > 0) {
+ g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
+ g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border);
+ } else {
+ g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
+ }
+ } else if (shape == HOLE) {
+ g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
+ g.setColor(0, 0, 1).drawRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
+ } else if (shape == FILLED) {
+ let border = Math.floor((cell_scale - 2) / 4);
+ if (border > 0) {
+ g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
+ g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border);
+ } else {
+ g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border);
+
+ }
+ } else if (shape == SPACE) {
+ g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
+ }
+
+ }
+ display() {
+ g.clear();
+ for (let line = 0; line < this.height; line++) {
+ for (let column = 0; column < this.width; column++) {
+ this.display_cell(line, column);
+ }
+ }
+ this.display_player();
+ g.setColor(0, 0, 0).setFont("6x8:2")
+ .setFontAlign(0, -1, 0)
+ .drawString(map.name, g.getWidth() / 2, 0);
+ }
+}
+
+
+Bangle.on('touch', function(button, xy) {
+ if (in_menu) {
+ return;
+ }
+ let half_width = g.getWidth() / 2;
+ let half_height = g.getHeight() / 2;
+ let directions_amplitudes = [0, 0, 0, 0];
+ directions_amplitudes[LEFT] = half_width - xy.x;
+ directions_amplitudes[RIGHT] = xy.x - half_width;
+ directions_amplitudes[UP] = half_height - xy.y;
+ directions_amplitudes[DOWN] = xy.y - half_height;
+
+ let max_direction;
+ let second_max_direction;
+ if (directions_amplitudes[0] > directions_amplitudes[1]) {
+ max_direction = 0;
+ second_max_direction = 1;
+ } else {
+ max_direction = 1;
+ second_max_direction = 0;
+ }
+ for (let direction = 2; direction < 4; direction++) {
+ if (directions_amplitudes[direction] > directions_amplitudes[max_direction]) {
+ second_max_direction = max_direction;
+ max_direction = direction;
+ } else if (directions_amplitudes[direction] >= directions_amplitudes[second_max_direction]) {
+ second_max_direction = direction;
+ }
+ }
+ if (directions_amplitudes[max_direction] - directions_amplitudes[second_max_direction] > 10) {
+ // if there is little possible confusions between two candidate moves let's move.
+ // basically we forbid diagonals of 10 pixels wide
+ map.move(max_direction);
+ }
+
+});
+
+Bangle.on('swipe', function(directionLR, directionUD) {
+ if (in_menu) {
+ return;
+ }
+ let last_move = history.pop();
+ if (last_move !== undefined) {
+ map.undo(last_move[0], last_move[1]);
+ }
+});
+
+setWatch(
+ function() {
+ if (in_menu) {
+ return;
+ }
+ in_menu = true;
+ const menu = {
+ "": {
+ title: "choose action"
+ },
+ "restart": function() {
+ E.showMenu();
+ load_current_map();
+ in_menu = false;
+ },
+ "current map": {
+ value: config.current_maps[config.levels_set] + 1,
+ min: 1,
+ max: config.offsets[config.levels_set].length / 2,
+ onchange: (v) => {
+ config.current_maps[config.levels_set] = v - 1;
+ load_current_map();
+ s.writeJSON("sokoban.json", config);
+ }
+ },
+ "next map": function() {
+ E.showMenu();
+ next_map();
+ in_menu = false;
+ },
+ "previous map": function() {
+ E.showMenu();
+ previous_map();
+ in_menu = false;
+ },
+ "back to game": function() {
+ E.showMenu();
+ g.clear();
+ map.display();
+ in_menu = false;
+ },
+ };
+ E.showMenu(menu);
+ },
+ BTN1, {
+ repeat: true
+ }
+);
+
+
+Bangle.setLocked(false);
+
+current_map = config.current_map;
+offsets = config.offsets;
+load_current_map();
diff --git a/apps/sokoban/metadata.json b/apps/sokoban/metadata.json
new file mode 100644
index 000000000..752c17e75
--- /dev/null
+++ b/apps/sokoban/metadata.json
@@ -0,0 +1,21 @@
+{
+ "id": "sokoban",
+ "name": "Sokoban",
+ "shortName": "Sokoban",
+ "version": "0.02",
+ "description": "Classic Sokoban game (microban levels).",
+ "allow_emulator":false,
+ "icon": "sokoban.png",
+ "type": "app",
+ "tags": "game",
+ "screenshots": [{"url":"soko.png"}],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"sokoban.app.js","url":"app.js"},
+ {"name":"sokoban.microban.sok", "url":"sokoban.microban.sok"},
+ {"name":"sokoban.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [{"name":"sokoban.json"}
+ ]
+}
diff --git a/apps/sokoban/soko.png b/apps/sokoban/soko.png
new file mode 100644
index 000000000..5bf0ae772
Binary files /dev/null and b/apps/sokoban/soko.png differ
diff --git a/apps/sokoban/sokoban.microban.sok b/apps/sokoban/sokoban.microban.sok
new file mode 100644
index 000000000..96146080c
--- /dev/null
+++ b/apps/sokoban/sokoban.microban.sok
@@ -0,0 +1,1822 @@
+; 1
+
+####
+# .#
+# ###
+#*@ #
+# $ #
+# ###
+####
+
+; 2
+
+######
+# #
+# #@ #
+# $* #
+# .* #
+# #
+######
+
+; 3
+
+ ####
+### ####
+# $ #
+# # #$ #
+# . .#@ #
+#########
+
+; 4
+
+########
+# #
+# .**$@#
+# #
+##### #
+ ####
+
+; 5
+
+ #######
+ # #
+ # .$. #
+## $@$ #
+# .$. #
+# #
+########
+
+; 6
+
+###### #####
+# ### #
+# $$ #@#
+# $ #... #
+# ########
+#####
+
+; 7
+
+#######
+# #
+# .$. #
+# $.$ #
+# .$. #
+# $.$ #
+# @ #
+#######
+
+; 8
+
+ ######
+ # ..@#
+ # $$ #
+ ## ###
+ # #
+ # #
+#### #
+# ##
+# # #
+# # #
+### #
+ #####
+
+; 9
+
+#####
+#. ##
+#@$$ #
+## #
+ ## #
+ ##.#
+ ###
+
+; 10
+
+ #####
+ #. #
+ #.# #
+#######.# #
+# @ $ $ $ #
+# # # # ###
+# #
+#########
+
+; 11
+
+ ######
+ # #
+ # ##@##
+### # $ #
+# ..# $ #
+# #
+# ######
+####
+
+; 12
+
+#####
+# ##
+# $ #
+## $ ####
+ ###@. #
+ # .# #
+ # #
+ #######
+
+; 13
+
+####
+#. ##
+#.@ #
+#. $#
+##$ ###
+ # $ #
+ # #
+ # ###
+ ####
+
+; 14
+
+#######
+# #
+# # # #
+#. $*@#
+# ###
+#####
+
+; 15
+
+ ###
+######@##
+# .* #
+# # #
+#####$# #
+ # #
+ #####
+
+; 16
+
+ ####
+ # ####
+ # ##
+## ## #
+#. .# @$##
+# # $$ #
+# .# #
+##########
+
+; 17
+
+#####
+# @ #
+#...#
+#$$$##
+# #
+# #
+######
+
+; 18
+
+#######
+# #
+#. . #
+# ## ##
+# $ #
+###$ #
+ #@ #
+ # #
+ ####
+
+; 19
+
+########
+# .. #
+# @$$ #
+##### ##
+ # #
+ # #
+ # #
+ ####
+
+; 20
+
+#######
+# ###
+# @$$..#
+#### ## #
+ # #
+ # ####
+ # #
+ ####
+
+; 21
+
+####
+# ####
+# . . #
+# $$#@#
+## #
+ ######
+
+; 22
+
+#####
+# ###
+#. . #
+# # #
+## # #
+ #@$$ #
+ # #
+ # ###
+ ####
+
+; 23
+
+#######
+# * #
+# #
+## # ##
+ #$@.#
+ # #
+ #####
+
+; 24
+
+# #####
+ # #
+###$$@#
+# ###
+# #
+# . . #
+#######
+
+; 25
+
+ ####
+ # ###
+ # $$ #
+##... #
+# @$ #
+# ###
+#####
+
+; 26
+
+ #####
+ # @ #
+ # #
+###$ #
+# ...#
+# $$ #
+### #
+ ####
+
+; 27
+
+######
+# .#
+# ## ##
+# $$@#
+# # #
+#. ###
+#####
+
+; 28
+
+#####
+# #
+# @ #
+# $$###
+##. . #
+ # #
+ ######
+
+; 29
+
+ #####
+ # ##
+ # #
+ ###### #
+## #. #
+# $ $ @ ##
+# ######.#
+# #
+##########
+
+; 30
+
+####
+# ###
+# $$ #
+#... #
+# @$ #
+# ##
+#####
+
+; 31
+
+ ####
+ ## #
+##@$.##
+# $$ #
+# . . #
+### #
+ #####
+
+; 32
+
+ ####
+## ###
+# #
+#.**$@#
+# ###
+## #
+ ####
+
+; 33
+
+#######
+#. # #
+# $ #
+#. $#@#
+# $ #
+#. # #
+#######
+
+; 34
+
+ ####
+### ####
+# #
+#@$***. #
+# #
+#########
+
+; 35
+
+ ####
+ ## #
+ #. $#
+ #.$ #
+ #.$ #
+ #.$ #
+ #. $##
+ # @#
+ ## #
+ #####
+
+; 36
+
+####
+# ############
+# $ $ $ $ $ @ #
+# ..... #
+###############
+
+; 37
+
+ ###
+##### #.#
+# ###.#
+# $ #.#
+# $ $ #
+#####@# #
+ # #
+ #####
+
+; 38
+
+##########
+# #
+# ##.### #
+# # $$ . #
+# . @$## #
+##### #
+ ######
+
+; 39
+
+#####
+# ####
+# # # .#
+# $ ###
+### #$. #
+# #@ #
+# # ######
+# #
+#####
+
+; 40
+
+ #####
+ # #
+## ##
+# $$$ #
+# .+. #
+#######
+
+; 41
+
+#######
+# #
+#@$$$ ##
+# #...#
+## ##
+ ######
+
+; 42
+
+ ####
+ # #
+ #@ #
+####$.#
+# $.#
+# # $.#
+# ##
+######
+
+; 43
+
+ ####
+ # @#
+ # #
+###### .#
+# $ .#
+# $$# .#
+# ####
+### #
+ ####
+
+; 44 'Duh!'
+
+#####
+#@$.#
+#####
+
+; 45
+
+######
+#... #
+# $ #
+# #$##
+# $ #
+# @ #
+######
+
+; 46
+
+ ######
+## #
+# ## #
+# # $ #
+# * .#
+## #@##
+ # #
+ #####
+
+; 47
+
+ #######
+### #
+# $ $ #
+# ### #####
+# @ . . #
+# ### #
+##### #####
+
+; 48
+
+######
+# @ #
+# # ##
+# .# ##
+# .$$$ #
+# .# #
+#### #
+ #####
+
+; 49
+
+######
+# @ #
+# $# #
+# $ #
+# $ ##
+### ####
+ # # #
+ #... #
+ # #
+ #######
+
+; 50
+
+ ####
+### #####
+# $ @..#
+# $ # #
+### #### #
+ # #
+ ########
+
+; 51
+
+####
+# ###
+# ###
+# $*@ #
+### .# #
+ # #
+ ######
+
+; 52
+
+ ####
+### @#
+# $ #
+# *.#
+# *.#
+# $ #
+### #
+ ####
+
+; 53
+
+ #####
+##. .##
+# * * #
+# # #
+# $ $ #
+## @ ##
+ #####
+
+; 54
+
+ ######
+ # #
+ ##### . #
+### ###. #
+# $ $ . ##
+# @$$ # . #
+## #####
+ ######
+
+; 55
+
+########
+# @ # #
+# #
+#####$ #
+ # ###
+ ## #$ ..#
+ ## # ###
+ ####
+
+; 56
+
+#####
+# ###
+# $ #
+##* . #
+ # @#
+ ######
+
+; 57
+
+ ####
+ # #
+ #@ #
+ # #
+### ####
+# * #
+# $ #
+#####. #
+ ####
+
+; 58
+
+####
+# ####
+#.*$ #
+# .$# #
+## @ #
+ # ##
+ #####
+
+; 59
+
+############
+# #
+# ####### @##
+# # #
+# # $ # #
+# $$ ##### #
+### # # ...#
+ #### # #
+ ######
+
+; 60
+
+ #########
+ # #
+##@##### #
+# # # #
+# # $.#
+# ##$##.#
+##$## #.#
+# $ #.#
+# # ###
+########
+
+; 61
+
+########
+# #
+# #### #
+# #...@#
+# ###$###
+# # #
+# $$ $ #
+#### ##
+ #.###
+ ###
+
+; 62
+
+ ##########
+#### ## #
+# $$$....$@#
+# ### #
+# #### ####
+#####
+
+; 63
+
+##### ####
+# ##### .#
+# $ ########
+### #### .$ @ #
+ # # # #### #
+ #### #### #####
+
+; 64
+
+ ######
+## #
+# $ #
+# $$ #
+### .#####
+ ##.# @ #
+ #. $ #
+ #. ####
+ ####
+
+; 65
+
+ ######
+ # #
+ # $ #
+ ####$ #
+## $ $ #
+#....# ##
+# @ #
+## # #
+ ########
+
+; 66
+
+ ###
+ #@#
+ ###$###
+## . ##
+# # # #
+# # # #
+# # # #
+# # # #
+# # # #
+## $ $ ##
+ ##. .##
+ # #
+ # #
+ #####
+
+; 67
+
+#####
+# ##
+# # #
+#@$*.##
+## . #
+ # $# #
+ ## #
+ #####
+
+; 68
+
+ ####
+ # ######
+## $ #
+# .# $ #
+# .#$#####
+# .@ #
+######
+
+; 69
+
+#### ####
+# #### #
+# # # #
+# # $##
+# . .#$ #
+#@ ## # $ #
+# . # #
+###########
+
+; 70
+
+#####
+# @ ####
+# #
+# $ $$ #
+##$## #
+# ####
+# .. #
+##.. #
+ ### #
+ ####
+
+; 71
+
+###########
+# # ###
+# $@$ # . .#
+# ## ### ## #
+# # # #
+# # # # #
+# ######### #
+# #
+#############
+
+; 72
+
+ ####
+ ## #####
+ # $ @ #
+ # $# #
+#### #####
+# # #
+# $ #
+# ..# #
+# .####
+# ##
+####
+
+; 73
+
+####
+# #####
+# $$ $ #
+# #
+## ## ##
+#...#@#
+# ### ##
+# #
+# # #
+########
+
+; 74
+
+ ####
+ # #######
+ #$ @# .#
+## #$$ .#
+# $ ##..#
+# # #####
+### #
+ #####
+
+; 75
+
+ #######
+## ....##
+# ######
+# $ $ @#
+### $ $ #
+ ### #
+ ######
+
+; 76
+
+ #####
+## #
+# #####
+# #.# #
+#@ #.# $ #
+# #.# ##
+# # #
+## ##$$#
+ ## #
+ # ####
+ ####
+
+; 77
+
+##########
+# @ .... #
+# ####$##
+## # $ $ #
+ # $ #
+ # ######
+ #####
+
+; 78
+
+ #######
+## ##
+# $ $ #
+# $ $ $ #
+## ### ####
+ #@ .....#
+ ## ###
+ #######
+
+; 79
+
+ #########
+ # # #
+## $#$# #
+# .$.@ #
+# .# #
+##########
+
+; 80
+
+####
+# #######
+# . ## .#
+# $# .#
+## ## # .#
+ # # #
+ #### # #
+ # @$ ###
+ # $$ #
+ # #
+ ######
+
+; 81
+
+ #####
+ # #
+ # . #
+## * #
+# *##
+# @##
+## $ #
+ # #
+ #####
+
+; 82
+
+#####
+# ###
+# . ##
+##*#$ #
+# .# $ #
+# @## ##
+# #
+#######
+
+; 83
+
+######
+# ##
+# $ $ ##
+## $$ #
+ # # #
+ # ## ##
+ # . .#
+ # @. .#
+ # ####
+ ####
+
+; 84
+
+########
+# ... #
+# ### ##
+# # $ #
+## #@$ #
+ # # $ #
+ # ### #####
+ # #
+ # ### #
+ ##### #####
+
+; 85
+
+ ####
+ ####### #
+ # $ #
+ # $ $ #
+ # ########
+## # . #
+# # # #
+# @ . ##
+## # # #
+ # . #
+ #######
+
+; 86
+
+ ####
+ ### ##
+ ## $ #
+## $ # #
+# @#$$ #
+# .. ###
+# ..###
+#####
+
+; 87
+
+ ####
+###### #
+# #
+# ... .#
+##$######
+# $ #
+# $###
+## $ #
+ ## @ #
+ ######
+
+; 88
+
+ ####
+ # ### #
+ # # #
+ # # # #
+ # #$ #.#
+ # # # # #
+ # #$ #.# #
+ # # # #
+####$ #.# #
+# @ # #
+# # ## #
+########
+
+; 89
+
+##########
+# ## #
+# $ $@# #
+#### # $ #
+ #.# ##
+ # #.# $#
+ # #. #
+ # #. #
+ ######
+
+; 90
+
+ ########
+ # @ #
+ # $ $ #
+### ## ###
+# $..$ #
+# .. #
+##########
+
+; 91
+
+###########
+# .## #
+# $$@..$$ #
+# ##. #
+###########
+
+; 92
+
+ ####
+ # # #####
+ # # # #
+ # ######.# #
+#### $ . #
+# $$# ###.# #
+# # # # #
+######### #@ ##
+ # #
+ ####
+
+; 93
+
+ #########
+## # ##
+# # #
+# $ # $ #
+# *.* #
+####.@.####
+# *.* #
+# $ # $ #
+# # #
+## # ##
+ #########
+
+; 94
+
+#########
+# @ # #
+# $ $ #
+##$### ##
+# ... #
+# # #
+###### #
+ ####
+
+; 95
+
+########
+#@ #
+# .$$. #
+# $..$ #
+# $..$ #
+# .$$. #
+# #
+########
+
+; 96
+
+ ######
+ # #
+ # #
+##### #
+# #.#####
+# $@$ #
+#####.# #
+ ## ## ##
+ # $.#
+ # ###
+ #####
+
+; 97
+
+ ####
+ # ########
+#### $ $.....#
+# $ ######
+#@### ###
+# $ #
+# $ # #
+## # #
+ # #
+ ######
+
+; 98
+
+#####
+# ## ####
+# $ ### .#
+# $ $ .#
+## $#####.# ####
+# $ # # .### #
+# # # .# @ #
+### # # #
+ #### ## ##
+ #######
+
+; 99
+
+ #####
+ # #
+####### ####### # #
+# # # # #
+# @ #### # ####
+# # ....## #### #
+# ##### ## $$ $ $ #
+###### # #
+ # ##########
+ ####
+
+; 100
+
+#######
+# @# #
+#.$ #
+#. # $##
+#.$# #
+#. # $ #
+# # #
+########
+
+; 101 'Lockdown'
+
+ #####
+ # #
+ # # #######
+ # * # #
+ ## ## # #
+ # #* #
+### # # # ###
+# *#$+ #
+# # ## ##
+# # * #
+####### # #
+ # #
+ #####
+
+; 102
+
+###########
+#....# #
+# # $$ #
+# @ ## #
+# ##$ #
+###### $ #
+ # #
+ ######
+
+; 103
+
+ #####
+ # . ##
+### $ #
+# . $#@#
+# #$ . #
+# $ ###
+## . #
+ #####
+
+; 104
+
+ #####
+##### #
+# $ #
+# $#$#@#
+### # #
+ # ... #
+ ### ##
+ # #
+ ####
+
+; 105
+
+ #### ####
+## ### ##
+# # # #
+# *. .* #
+###$ $###
+ # @ #
+###$ $###
+# *. .* #
+# # # #
+## ### ##
+ #### ####
+
+; 106
+
+ ########
+ # #
+ #@ $ #
+## ###$ #
+# .....###
+# $ $ $ #
+###### # #
+ # #
+ #####
+
+; 107
+
+########
+# #
+# $*** #
+# * * #
+# * * #
+# ***. #
+# @#
+########
+
+; 108
+
+#### #####
+# ### # ##
+# # #$ $ #
+#..# ##### # #
+# @ # $ $ #
+#..# ##
+## #########
+ #####
+
+; 109
+
+ #######
+# # #
+# # # # #
+ # @ $ #
+### ### #
+# ### #
+# $ ##.#
+## $ #.#
+ ## $ .#
+# ## $#.#
+## ## #.#
+### # #
+### #####
+
+; 110
+
+ ####
+ # #
+ # $####
+###. . #
+# $ # $ #
+# . .###
+####$ #
+ # @#
+ ####
+
+; 111
+
+######
+# ####
+# ...#
+# ...#
+###### #
+ # # #
+ # $$ ##
+ # @$ #
+ # $$ #
+ ## $# #
+ # #
+ ######
+
+; 112
+
+ #####
+## ####
+# $$$ #
+# # $ #
+# $## ##
+### #. #
+ # # #
+ ##### ###
+ # # ##
+ # @....#
+ # #
+ # # #
+ ########
+
+; 113
+
+ #####
+ ## #
+### # #
+# . #
+# ## #####
+# . . # ##
+# # @ $ ###
+#####. # $ #
+ #### $ #
+ ## $ ##
+ # ##
+ # #
+ ####
+
+; 114
+
+######
+# ###
+# # $ #
+# $ @ #
+## ## #####
+# #......#
+# $ $ $ $ #
+## ######
+ #####
+
+; 115
+
+ #####
+##### ####
+# # #
+# #..... #
+## ## # ###
+ #$$@$$$ #
+ # ###
+ #######
+
+; 116
+
+ #####
+ ### #
+####.....#
+# @$$$$$ #
+# # ##
+##### #
+ #####
+
+; 117
+
+ #### ####
+ # ### ##
+ # @ #
+##..### #
+# # #
+#...#$ # #
+# ## $$ $ #
+# $ ###
+#### ###
+ ####
+
+; 118
+
+ #####
+## ##
+# $ ##
+# $ $ ##
+###$# . ##
+ # # . #
+ ## ##. #
+ # @ . ##
+ # # #
+ ########
+
+; 119
+
+ ######
+ # ##
+ ## ## #
+ # $$ # #
+ # @$ # #
+ # # #
+#### # #
+# ... ##
+# ##
+#######
+
+; 120
+
+ ####
+####### #
+# $ ##
+# $##### #
+# @# # #
+## ##.. #
+# # ..####
+# $ ###
+# $###
+# #
+####
+
+; 121
+
+ ######
+ # . #
+##$.# #
+# * #
+# ..###
+##$ # #####
+## ## # #
+# #### # #
+# @ $ $ #
+## # #
+ ##########
+
+; 122
+
+#####
+# ###
+# #$ #
+# $ #
+# $ $ #
+# $# #
+# @###
+## ########
+# ...#
+# #
+########..#
+ ####
+
+; 123
+
+########
+# #
+# $ $$ ########
+##### @##. . #
+ #$ # . #
+ # #. . ##
+ #$# ## # #
+ # #
+ # ### ##
+ # # ####
+ ####
+
+; 124
+
+##############
+# # #
+# $@$$ # . ..#
+## ## ### ## #
+ # # # #
+ # # # # #
+ # ######### #
+ # #
+ #############
+
+; 125
+
+ #####
+ # ##
+ # $ #
+######## #@##
+# . # $ $ #
+# $# #
+#...##### #
+##### #####
+
+; 126
+
+ ###########
+##....... #
+# $$$$$$$@ #
+# # # # ##
+# # # #
+# #######
+#####
+
+; 127
+
+## ####
+#### ####
+ # $ $. #
+## # .$ #
+# ##.###
+# $ . #
+# @ # #
+# ######
+####
+
+; 128
+
+ #########
+### # #
+# * $ . . #
+# $ ## ##
+####*# #
+ # @ ###
+ # ###
+ #####
+
+; 129
+
+ #########
+### @ # #
+# * $ *.. #
+# $ # #
+####*# ###
+ # ##
+ # ###
+ #####
+
+; 130
+
+##### #####
+# ####.. #
+# $$$ #
+# $# .. #
+### @# ## #
+ # ## #
+ ##########
+
+; 131
+
+#####
+# #
+# . #
+#.@.###
+##.# #
+# $ #
+# $ #
+##$$ #
+ # ###
+ # #
+ ####
+
+; 132
+
+####
+# @###
+#.* #####
+#..#$$ $ #
+## #
+ # # ## #
+ # #####
+ #####
+
+; 133
+
+ #######
+ # . .###
+ # . . . #
+### #### #
+# @$ $ #
+# $$ $ #
+#### ###
+ #####
+
+; 134
+
+ ####
+######### #
+# ## $ #
+# $ ## #
+### #. .# ##
+ # #. .#$##
+ # # # #
+ # @ $ #
+ # #######
+ ####
+
+; 135
+
+#######
+# #####
+# $$#@##..#
+# # #
+# $ # # #
+#### $ ..#
+ ########
+
+; 136
+
+ #######
+ # #
+## ###$##
+#.$ @ #
+# .. #$ #
+#.## $ #
+# ####
+######
+
+; 137
+
+ ####
+ ## ###
+#### # $ #
+# #### $ $ #
+# ..# #$ #
+# # @ ###
+## #..# ###
+ # ## # #
+ # #
+ ########
+
+; 138
+
+ ####
+### #
+# ###
+# # . .#
+# @ ...####
+# # # # ##
+# # $$ #
+##### $ $ #
+ ##$ # ##
+ # #
+ ######
+
+; 139
+
+ ####
+## ####
+# ...#
+# ...#
+# # ##
+# #@ #### ####
+##### $ ### #
+ # ##$ $ #
+ ### $$ #
+ # $ ## ###
+ # ######
+ ######
+
+; 140
+
+######## #####
+# # ### #
+# ## $ #
+#.# @ ## $ ##
+#.# # $ ##
+#.# $ ##
+#. ## #####
+## #
+ ######
+
+; 141
+
+ ########
+ # # . #
+ # .*.#
+ # # * #
+####$##.##
+# $ #
+# $ ## $ #
+# @# #
+##########
+
+; 142
+
+ ####
+ # #
+ # ####
+###$.$ #
+# .@. #
+# $.$###
+#### #
+ # #
+ ####
+
+; 143
+
+####
+# ####
+# $ #
+# .# #
+# $# ##
+# . #
+#### #
+ # #
+ ### ###
+ # $ #
+## #$# ##
+# $ @ $ #
+# ..#.. #
+### ###
+ #####
+
+; 144
+
+ ####
+ ### #####
+ # $$ # #
+ # $ . .$$##
+ # .. #. $ #
+### #** . #
+# . **# ###
+# $ .# .. #
+##$$.@. $ #
+ # # $$ #
+ ##### ###
+ ####
+
+; 145
+
+ #####
+ # @ #
+ ## ##
+###.$$$.###
+# $...$ #
+# $.#.$ #
+# $...$ #
+###.$$$.###
+ ## ##
+ # #
+ #####
+
+; 146
+
+ #######
+## . ##
+# .$$$. #
+# $. .$ #
+#.$ @ $.#
+# $. .$ #
+# .$$$. #
+## . ##
+ #######
+
+; 147
+
+ #####
+######## #
+#. . @#.#
+# ### #
+## $ # #
+ # $ #####
+ # $# #
+ ## # #
+ # ##
+ #####
+
+; 148 'from (Original 18)'
+
+###########
+# . # #
+# #. @ #
+# #..# #######
+## ## $$ $ $ #
+ ## #
+ #############
+
+; 149 'from (Boxxle 43)'
+
+ ####
+## ###
+#@$ #
+### $ #
+ # ######
+ # $....#
+ # # ####
+ ## # #
+ # $# #
+ # #
+ # ###
+ ####
+
+; 150 'from (Original 47)'
+
+ ####
+ ##### #
+ # $#######
+## ## ..# ...#
+# $ $$#$ @ #
+# ### #
+####### # ####
+ ####
+
+; 151 'from (Original 47)'
+
+ ####
+ # #
+ ### #
+## $ #
+# # #
+# #$$ ######
+# # # .#
+# $ @ .#
+### ####..#
+ #### ####
+
+; 152
+
+###### ####
+# # #
+#.## #$## #
+# # # #
+#$ # ### # #
+# # # # #
+# # #### # # #
+#. @ $ * . #
+###############
+
+; 153
+
+#############
+#.# @# # #
+#.#$$ # $ #
+#.# # $# #
+#.# $# # $##
+#.# # $# #
+#.# $# # $#
+#.. # $ #
+#.. # # #
+############
+
+; 154 'Take the long way home.'
+
+ ############################
+ # #
+ # ######################## #
+ # # # #
+ # # #################### # #
+ # # # # # #
+ # # # ################ # # #
+ # # # # # # # #
+ # # # # ############ # # # #
+ # # # # # # # # #
+ # # # # # ############ # # #
+ # # # # # # # #
+ # # # # ################ # #
+ # # # # # #
+##$# # #################### #
+#. @ # #
+#############################
+
+; 155 'The Dungeon'
+
+ ###### ####
+#####*# ################# ##
+# ### #
+# ######## #### ## #
+### #### # #### #### ##
+#*# # .# # # # # # #
+#*# # # # ## # ## ## #
+### ### ### # ## # ## ##
+ # # #*# # # # #
+ # # ### ##### #### # #
+ ##### ##### ####### ######
+ # # # #**# #
+## # # #**# ####### ## #
+# ######### # ##### ###
+# # # $ #*#
+# ######### ### @##### #*#
+##### #### #### ######
diff --git a/apps/sokoban/sokoban.png b/apps/sokoban/sokoban.png
new file mode 100644
index 000000000..849b92d01
Binary files /dev/null and b/apps/sokoban/sokoban.png differ
diff --git a/apps/spacew/README.md b/apps/spacew/README.md
new file mode 100644
index 000000000..4f2ca3f00
--- /dev/null
+++ b/apps/spacew/README.md
@@ -0,0 +1,43 @@
+# Space Weaver 
+
+Vector map
+
+Written by: [Pavel Machek](https://github.com/pavelmachek)
+
+Space Weaver is application for displaying vector maps. It is
+currently suitable for developers, and more work is needed.
+
+Maps can be created from openstreetmap extracts. Those are cut using
+osmosis, then translated into geojson. Geojson is further processes to
+add metadata such as colors, and to split it into xyz tiles, while
+keeping geojson format. Tiles are then merged into single file, which
+can be uploaded to the filesystem. Index at the end provides locations
+of the tiles.
+
+## Preparing data
+
+Tools in spacew/prep can be used to prepare data.
+
+You'll need to edit prepare.sh to point it to suitable osm extract,
+and you'll need to select area of interest. Start experiments with
+small area. You may want to delete cstocs and provide custom
+conversion to ascii.
+
+Details of which features are visible at what zoom levels can be
+configured in split.js. This can greatly affect file sizes. Then
+there's "meta.max_zoom = 17" setting, reduce it if file is too big.
+
+For initial experiments, configure things so that mtar file is around
+500KB. (I had troubles with big files, both on hardware and to lesser
+extent on simulator. In particular, mtar seemed to be corrupted after
+emulator window was closed.)
+
+## Future Development
+
+Directories at the end of .mtar should be hashed, not linear searched.
+
+Geojson is not really suitable as it takes a lot of storage.
+
+It would be nice to support polygons.
+
+Web-based tool for preparing maps would be nice.
\ No newline at end of file
diff --git a/apps/spacew/app-icon.js b/apps/spacew/app-icon.js
new file mode 100644
index 000000000..27b6e2662
--- /dev/null
+++ b/apps/spacew/app-icon.js
@@ -0,0 +1,2 @@
+require("heatshrink").decompress(atob("mEwwkE/8Ql//j//AAUD//yAgILBAAXzBQMxAoMwn4XKBIgXCmAXEh4XF+IJGC4XxAoMgl/zgX/nASBBgPwIoIEBmYBBI4ug1/6hX/zOf+UBEIMP+UC+eZAIPyhP/yAnB0Ef+QXBnM/GgUwh4ECwX/wYvCkIvB+BrBA4JsFAQMiL4gRBA4XxNYIlBBgQGBiJXEBQRnBiYoEiQXFgURT4YAB+QXBS4RTCJoQMBj4gBWQPwN4IKCNgLHDRAIlDEgIxBC4zHBJITACC4gMB+MfAIJCCRIU/GIIGCEoLyCBgQOCgZAEBAL5CC4UvC4oFBMIJ9CCAQMBPwbABKoYMBJ4KpBZQgKBVwnyh/wKoQMBVoUgn4XFmTGEgfxC4QKBCQRKBeAYtBkYXFXYIFBkTfCSgMfIIYbBdwTADJIIEBkYEDAYKyDC4J9DKoSFDiZMDGYKCDkbWEKoUzIQQREHQIFDifzBQYXGIIIMDkDwDN4IXFIIIXBJQMhEQqCCT4IWENoUCC4MvXwTjCiZqBEQIXGNoITBC4LRDEQMDHQbWEAAUDIYPzmabEEQIXDmYXGiUgFAMyLASQDXgPzj7uEQobNB+MxWYsgHQKSBEQqFCUYPwUwgKCHQUvEQqFCkAXCBQ0Qn/xmYXH+IXB+S+ESAUAEQMzHQqFCgEvmS+EBQUBl/wUw4MDmS+ESAcf+ExC44MCmS+ESAcPmAvI/8hh8iNY8wgcwaw4MCh8hNY/wC5kDTwKbHgThGEgsQQZMhdw61CgSmGAAUANRAkCgUTBZEQiRSHHga+HNYUCC5I8BXw4XCgIWJHgJTJ+IXJHAIXB+eTJoIJD+fyC4LABYQWZBQOYC4Mf+eS/85DgIJBxMygAFB+YUBC4YqBkAoBAIM5n4JCAgIvBwYBCNgyDKTRIXM+YXFA="))
+
diff --git a/apps/spacew/app.js b/apps/spacew/app.js
new file mode 100644
index 000000000..e438799f6
--- /dev/null
+++ b/apps/spacew/app.js
@@ -0,0 +1,620 @@
+/* original openstmap.js */
+
+/* OpenStreetMap plotting module.
+
+Usage:
+
+var m = require("openstmap");
+// m.lat/lon are now the center of the loaded map
+m.draw(); // draw centered on the middle of the loaded map
+
+// plot gps position on map
+Bangle.on('GPS',function(f) {
+ if (!f.fix) return;
+ var p = m.latLonToXY(fix.lat, fix.lon);
+ g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
+});
+
+// recenter and redraw map!
+function center() {
+ m.lat = fix.lat;
+ m.lon = fix.lon;
+ m.draw();
+}
+
+// you can even change the scale - eg 'm/scale *= 2'
+
+*/
+
+var exports = {};
+var m = exports;
+m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{
+ let map = require("Storage").readJSON(f);
+ map.center = Bangle.project({lat:map.lat,lon:map.lon});
+ return map;
+});
+// we base our start position on the middle of the first map
+if (m.maps[0] != undefined) {
+ m.map = m.maps[0];
+ m.scale = m.map.scale; // current scale (based on first map)
+ m.lat = m.map.lat; // position of middle of screen
+ m.lon = m.map.lon; // position of middle of screen
+} else {
+ m.scale = 20;
+ m.lat = 50;
+ m.lon = 14;
+}
+
+exports.draw = function() {
+ var cx = g.getWidth()/2;
+ var cy = g.getHeight()/2;
+ var p = Bangle.project({lat:m.lat,lon:m.lon});
+ m.maps.forEach((map,idx) => {
+ var d = map.scale/m.scale;
+ var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
+ var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
+ var o = {};
+ var s = map.tilesize;
+ if (d!=1) { // if the two are different, add scaling
+ s *= d;
+ o.scale = d;
+ }
+ //console.log(ix,iy);
+ var tx = 0|(ix/s);
+ var ty = 0|(iy/s);
+ var ox = (tx*s)-ix;
+ var oy = (ty*s)-iy;
+ var img = require("Storage").read(map.fn);
+ // fix out of range so we don't have to iterate over them
+ if (tx<0) {
+ ox+=s*-tx;
+ tx=0;
+ }
+ if (ty<0) {
+ oy+=s*-ty;
+ ty=0;
+ }
+ var mx = g.getWidth();
+ var my = g.getHeight();
+ for (var x=ox,ttx=tx; x ac * expansion) && (x = ac * expansion);
+ (y > ac) && (y = ac);
+ //(x < 0) && (x = 0);
+ //(y < 0) && (y = 0);
+ return [x, y];
+}
+
+// Convert bbox to xyx bounds
+//
+// - `bbox` {Number} bbox in the form `[w, s, e, n]`.
+// - `zoom` {Number} zoom.
+// - `tms_style` {Boolean} whether to compute using tms-style.
+// - `srs` {String} projection of input bbox (WGS84|900913).
+// - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties.
+xyz = function(bbox, zoom, tms_style, srs) {
+ // If web mercator provided reproject to WGS84.
+ if (srs === '900913') {
+ bbox = this.convert(bbox, 'WGS84');
+ }
+
+ var ll = [bbox[0], bbox[1]]; // lower left
+ var ur = [bbox[2], bbox[3]]; // upper right
+ var px_ll = px(ll, zoom);
+ var px_ur = px(ur, zoom);
+ // Y = 0 for XYZ is the top hence minY uses px_ur[1].
+ var size = 256;
+ var x = [ Math.floor(px_ll[0] / size), Math.floor((px_ur[0] - 1) / size) ];
+ var y = [ Math.floor(px_ur[1] / size), Math.floor((px_ll[1] - 1) / size) ];
+ var bounds = {
+ minX: Math.min.apply(Math, x) < 0 ? 0 : Math.min.apply(Math, x),
+ minY: Math.min.apply(Math, y) < 0 ? 0 : Math.min.apply(Math, y),
+ maxX: Math.max.apply(Math, x),
+ maxY: Math.max.apply(Math, y)
+ };
+ if (tms_style) {
+ var tms = {
+ minY: (Math.pow(2, zoom) - 1) - bounds.maxY,
+ maxY: (Math.pow(2, zoom) - 1) - bounds.minY
+ };
+ bounds.minY = tms.minY;
+ bounds.maxY = tms.maxY;
+ }
+ return bounds;
+};
+
+// Convert screen pixel value to lon lat
+//
+// - `px` {Array} `[x, y]` array of geographic coordinates.
+// - `zoom` {Number} zoom level.
+function ll(px, zoom) {
+ var size = 256 * Math.pow(2, zoom);
+ var bc = (size / 360);
+ var cc = (size / (2 * Math.PI));
+ var zc = size / 2;
+ var g = (px[1] - zc) / -cc;
+ var lon = (px[0] - zc) / bc;
+ var R2D = 180 / Math.PI;
+ var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI);
+ return [lon, lat];
+}
+
+// Convert tile xyz value to bbox of the form `[w, s, e, n]`
+//
+// - `x` {Number} x (longitude) number.
+// - `y` {Number} y (latitude) number.
+// - `zoom` {Number} zoom.
+// - `tms_style` {Boolean} whether to compute using tms-style.
+// - `srs` {String} projection for resulting bbox (WGS84|900913).
+// - `return` {Array} bbox array of values in form `[w, s, e, n]`.
+bbox = function(x, y, zoom, tms_style, srs) {
+ var size = 256;
+
+ // Convert xyz into bbox with srs WGS84
+ if (tms_style) {
+ y = (Math.pow(2, zoom) - 1) - y;
+ }
+ // Use +y to make sure it's a number to avoid inadvertent concatenation.
+ var ll_ = [x * size, (+y + 1) * size]; // lower left
+ // Use +x to make sure it's a number to avoid inadvertent concatenation.
+ var ur = [(+x + 1) * size, y * size]; // upper right
+ var bbox = ll(ll_, zoom).concat(ll(ur, zoom));
+
+ // If web mercator requested reproject to 900913.
+ if (srs === '900913') {
+ return this.convert(bbox, '900913');
+ } else {
+ return bbox;
+ }
+};
+
+/* original openstmap_app.js */
+
+//var m = require("openstmap");
+var HASWIDGETS = true;
+var R;
+var fix = {};
+var mapVisible = false;
+var hasScrolled = false;
+var settings = require("Storage").readJSON("openstmap.json",1)||{};
+var points;
+var startDrag = 0;
+
+// Redraw the whole page
+function redraw(qual) {
+ if (1) drawAll(qual);
+ g.setClipRect(R.x,R.y,R.x2,R.y2);
+ if (0) m.draw();
+ drawPOI();
+ drawMarker();
+ // if track drawing is enabled...
+ if (settings.drawTrack) {
+ if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
+ g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
+ WIDGETS["gpsrec"].plotTrack(m);
+ }
+ if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) {
+ g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
+ WIDGETS["recorder"].plotTrack(m);
+ }
+ }
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+}
+
+// Draw the POIs
+function drawPOI() {
+ if (1) return;
+ /* var waypoints = require("waypoints").load(); FIXME */ g.setFont("Vector", 18);
+ waypoints.forEach((wp, idx) => {
+ var p = m.latLonToXY(wp.lat, wp.lon);
+ var sz = 2;
+ g.setColor(0,0,0);
+ g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz);
+ g.setColor(0,0,0);
+ g.drawString(wp.name, p.x, p.y);
+ print(wp.name);
+ })
+}
+
+
+
+// Draw the marker for where we are
+function drawMarker() {
+ if (!fix.fix) return;
+ var p = m.latLonToXY(fix.lat, fix.lon);
+ g.setColor(1,0,0);
+ g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
+}
+
+Bangle.on('GPS',function(f) {
+ fix=f;
+ if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]);
+ if (mapVisible) drawMarker();
+});
+Bangle.setGPSPower(1, "app");
+
+if (HASWIDGETS) {
+ Bangle.loadWidgets();
+ WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{
+ var txt = (0|fix.satellites)+" Sats";
+ if (!fix.fix) txt += "\nNO FIX";
+ g.reset().setFont("6x8").setFontAlign(0,0)
+ .drawString(txt,w.x+24,w.y+12);
+ }
+ };
+ Bangle.drawWidgets();
+}
+R = Bangle.appRect;
+
+function showMap() {
+ mapVisible = true;
+ g.reset().clearRect(R);
+ redraw(0);
+ emptyMap();
+}
+
+function emptyMap() {
+ Bangle.setUI({mode:"custom",drag:e=>{
+ if (e.b) {
+ if (!startDrag)
+ startDrag = getTime();
+ g.setClipRect(R.x,R.y,R.x2,R.y2);
+ g.scroll(e.dx,e.dy);
+ m.scroll(e.dx,e.dy);
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ hasScrolled = true;
+ print("Has scrolled");
+ } else if (hasScrolled) {
+ delta = getTime() - startDrag;
+ startDrag = 0;
+ hasScrolled = false;
+ print("Done", delta, e.x, e.y);
+ qual = 0;
+ if (delta < 0.2) {
+ if (e.x < g.getWidth() / 2) {
+ if (e.y < g.getHeight() / 2) {
+ m.scale /= 2;
+ } else {
+ m.scale *= 2;
+ }
+ } else {
+ if (e.y < g.getHeight() / 2) {
+ qual = 2;
+ } else {
+ qual = 4;
+ }
+ }
+ }
+ g.reset().clearRect(R);
+ redraw(qual);
+ }
+ }, btn: btn=>{
+ mapVisible = false;
+ var menu = {"":{title:"Map"},
+ "< Back": ()=> showMap(),
+ /*LANG*/"Zoom In": () =>{
+ m.scale /= 2;
+ showMap();
+ },
+ /*LANG*/"Zoom Out": () =>{
+ m.scale *= 2;
+ showMap();
+ },
+ /*LANG*/"Draw Track": {
+ value : !!settings.drawTrack,
+ onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); }
+ },
+ /*LANG*/"Center Map": () =>{
+ m.lat = m.map.lat;
+ m.lon = m.map.lon;
+ m.scale = m.map.scale;
+ showMap();
+ },
+ /*LANG*/"Benchmark": () =>{
+ m.lat = 50.001;
+ m.lon = 14.759;
+ m.scale = 2;
+ g.reset().clearRect(R);
+ redraw(18);
+ print("Benchmark done (31 sec)");
+ }
+ };
+ if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{
+ m.lat = fix.lat;
+ m.lon = fix.lon;
+ showMap();
+ };
+ E.showMenu(menu);
+ }});
+}
+
+var gjson = null;
+
+function readTarFile(tar, f) {
+ const st = require('Storage');
+ json_off = st.read(tar, 0, 16) * 1;
+ if (isNaN(json_off)) {
+ print("Don't have archive", tar);
+ return undefined;
+ }
+ while (1) {
+ json_len = st.read(tar, json_off, 6) * 1;
+ if (json_len == -1)
+ break;
+ json_off += 6;
+ json = st.read(tar, json_off, json_len);
+ //print("Have directory, ", json.length, "bytes");
+ //print(json);
+ files = JSON.parse(json);
+ //print(files);
+ rec = files[f];
+ if (rec)
+ return st.read(tar, rec.st, rec.si);
+ json_off += json_len;
+ }
+ return undefined;
+}
+
+function loadVector(name) {
+ var t1 = getTime();
+ print(".. Read", name);
+ //s = require("Storage").read(name);
+ var s = readTarFile("delme.mtar", name);
+ if (s == undefined) {
+ print("Don't have file", name);
+ return null;
+ }
+ var r = JSON.parse(s);
+ print(".... Read and parse took ", getTime()-t1);
+ return r;
+}
+
+function drawPoint(a) {
+ lon = a.geometry.coordinates[0];
+ lat = a.geometry.coordinates[1];
+
+ var p = m.latLonToXY(lat, lon);
+ var sz = 2;
+ if (a.properties["marker-color"]) {
+ g.setColor(a.properties["marker-color"]);
+ }
+ if (a.properties.marker_size == "small")
+ sz = 1;
+ if (a.properties.marker_size == "large")
+ sz = 4;
+
+ g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz);
+ g.setColor(0,0,0);
+ g.setFont("Vector", 18).setFontAlign(-1,-1);
+ g.drawString(a.properties.name, p.x, p.y);
+ points ++;
+}
+
+function drawLine(a, qual) {
+ lon = a.geometry.coordinates[0][0];
+ lat = a.geometry.coordinates[0][1];
+ i = 1;
+ step = 1;
+ len = a.geometry.coordinates.length;
+ step = step * qual;
+ var p1 = m.latLonToXY(lat, lon);
+ if (a.properties.stroke) {
+ g.setColor(a.properties.stroke);
+ }
+ while (i < len) {
+ lon = a.geometry.coordinates[i][0];
+ lat = a.geometry.coordinates[i][1];
+ var p2 = m.latLonToXY(lat, lon);
+
+ //print(p1.x, p1.y, p2.x, p2.y);
+ g.drawLine(p1.x, p1.y, p2.x, p2.y);
+ if (i == len-1)
+ break;
+ i = i + step;
+ if (i>len)
+ i = len-1;
+ points ++;
+ p1 = p2;
+ g.flip();
+ }
+}
+
+function drawVector(gjson, qual) {
+ var d = gjson;
+ points = 0;
+ var t1 = getTime();
+
+ for (var a of d.features) {
+ if (a.type != "Feature")
+ print("Expecting feature");
+ g.setColor(0,0,0);
+ // marker-size, marker-color, stroke
+ if (qual < 32 && a.geometry.type == "Point")
+ drawPoint(a);
+ if (qual < 8 && a.geometry.type == "LineString")
+ drawLine(a, qual);
+ }
+ print("....", points, "painted in", getTime()-t1, "sec");
+}
+
+function fname(lon, lat, zoom) {
+ var bbox = [lon, lat, lon, lat];
+ var r = xyz(bbox, 13, false, "WGS84");
+ //console.log('fname', r);
+ return 'z'+zoom+'-'+r.minX+'-'+r.minY+'.json';
+}
+
+function fnames(zoom) {
+ var bb = [m.lon, m.lat, m.lon, m.lat];
+ var r = xyz(bb, zoom, false, "WGS84");
+ while (1) {
+ var bb2 = bbox(r.minX, r.minY, zoom, false, "WGS84");
+ var os = m.latLonToXY(bb2[3], bb2[0]);
+ if (os.x >= 0)
+ r.minX -= 1;
+ else if (os.y >= 0)
+ r.minY -= 1;
+ else break;
+ }
+ while (1) {
+ var bb2 = bbox(r.maxX, r.maxY, zoom, false, "WGS84");
+ var os = m.latLonToXY(bb2[1], bb2[2]);
+ if (os.x <= g.getWidth())
+ r.maxX += 1;
+ else if (os.y <= g.getHeight())
+ r.maxY += 1;
+ else break;
+ }
+ print(".. paint range", r);
+ return r;
+}
+
+function log2(x) { return Math.log(x) / Math.log(2); }
+
+function getZoom(qual) {
+ var z = 16-Math.round(log2(m.scale));
+ z += qual;
+ z -= 0;
+ if (z < meta.min_zoom)
+ return meta.min_zoom;
+ if (z > meta.max_zoom)
+ return meta.max_zoom;
+ return z;
+}
+
+function drawDebug(text, perc) {
+ g.setClipRect(0,0,R.x2,R.y);
+ g.reset();
+ g.setColor(1,1,1).fillRect(0,0,R.x2,R.y);
+ g.setColor(1,0,0).fillRect(0,0,R.x2*perc,R.y);
+ g.setColor(0,0,0).setFont("Vector",15);
+ g.setFontAlign(0,0)
+ .drawString(text,80,10);
+
+ g.setClipRect(R.x,R.y,R.x2,R.y2);
+ g.flip();
+}
+
+function drawAll(qual) {
+ var zoom = getZoom(qual);
+ var t1 = getTime();
+
+ drawDebug("Zoom "+zoom, 0);
+
+ print("Draw all", m.scale, "->", zoom, "q", qual, "at", m.lat, m.lon);
+ var r = fnames(zoom);
+ var tiles = (r.maxY-r.minY+1) * (r.maxY-r.minY+1);
+ var num = 0;
+ drawDebug("Zoom "+zoom+" tiles "+tiles, 0);
+ for (y=r.minY; y<=r.maxY; y++) {
+ for (x=r.minX; x<=r.maxX; x++) {
+
+ for (cnt=0; cnt<1000; cnt++) {
+ var n ='z'+zoom+'-'+x+'-'+y+'-'+cnt+'.json';
+ var gjson = loadVector(n);
+ if (!gjson) break;
+ drawVector(gjson, 1);
+ }
+ num++;
+ drawDebug("Zoom "+zoom+" tiles "+num+"/"+tiles, num/tiles);
+ }
+ }
+ g.flip();
+ Bangle.drawWidgets();
+ print("Load and paint in", getTime()-t1, "sec");
+}
+
+function initVector() {
+ var s = readTarFile("delme.mtar", "meta.json");
+ meta = JSON.parse(s);
+
+}
+
+function introScreen() {
+ g.reset().clearRect(R);
+ g.setColor(0,0,0).setFont("Vector",25);
+ g.setFontAlign(0,0);
+ g.drawString("SpaceWeaver", 85,35);
+ g.setColor(0,0,0).setFont("Vector",18);
+ g.drawString("Vector maps", 85,55);
+ g.drawString("Zoom "+meta.min_zoom+".."+meta.max_zoom, 85,75);
+}
+
+
+m.scale = 76;
+m.lat = 50.001;
+m.lon = 14.759;
+
+initVector();
+introScreen();
+emptyMap();
diff --git a/apps/spacew/app.png b/apps/spacew/app.png
new file mode 100644
index 000000000..0e52fa316
Binary files /dev/null and b/apps/spacew/app.png differ
diff --git a/apps/spacew/metadata.json b/apps/spacew/metadata.json
new file mode 100644
index 000000000..51bdb35b8
--- /dev/null
+++ b/apps/spacew/metadata.json
@@ -0,0 +1,13 @@
+{ "id": "spacew",
+ "name": "Space Weaver",
+ "version":"0.01",
+ "description": "Application for displaying vector maps",
+ "icon": "app.png",
+ "readme": "README.md",
+ "supports" : ["BANGLEJS2"],
+ "tags": "outdoors,gps,osm",
+ "storage": [
+ {"name":"spacew.app.js","url":"app.js"},
+ {"name":"spacew.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/spacew/prep/minitar.js b/apps/spacew/prep/minitar.js
new file mode 100755
index 000000000..e07c47049
--- /dev/null
+++ b/apps/spacew/prep/minitar.js
@@ -0,0 +1,78 @@
+#!/usr/bin/nodejs
+
+var pc = 1;
+var hack = 0;
+const hs = require('./heatshrink.js');
+
+if (pc) {
+ fs = require('fs');
+ var print=console.log;
+} else {
+
+}
+
+function writeDir(json) {
+ json_str = JSON.stringify(json, "", " ");
+ dirent = '' + json_str.length;
+ while (dirent.length < 6)
+ dirent = dirent + ' ';
+ return dirent + json_str;
+}
+
+function writeTar(tar, dir) {
+ var h_len = 16;
+ var cur = h_len;
+ files = fs.readdirSync(dir);
+ data = '';
+ var directory = '';
+ var json = {};
+ for (f of files) {
+ d = fs.readFileSync(dir+f);
+ cs = d;
+ //cs = String.fromCharCode.apply(null, hs.compress(d))
+ print("Processing", f, cur, d.length, cs.length);
+ //if (d.length == 42) continue;
+ data = data + cs;
+ var f_rec = {};
+ f_rec.st = cur;
+ var len = d.length;
+ f_rec.si = len;
+ cur = cur + len;
+ json[f] = f_rec;
+ json_str = JSON.stringify(json, "", " ");
+ if (json_str.length < 16000)
+ continue;
+ directory += writeDir(json);
+ json = {};
+ }
+ directory += writeDir(json);
+ directory += '-1 ';
+
+ size = cur;
+ header = '' + size;
+ while (header.length < h_len) {
+ header = header+' ';
+ }
+ if (!hack)
+ fs.writeFileSync(tar, header+data+directory);
+ else
+ fs.writeFileSync(tar, directory);
+}
+
+function readTarFile(tar, f) {
+ const st = require('Storage');
+ json_off = st.read(tar, 0, 16) * 1;
+ print(json_off);
+ json = st.read(tar, json_off, -1);
+ files = JSON.parse(json);
+ rec = files[f];
+ return st.read(tar, rec.st, rec.si);
+}
+
+if (pc)
+ writeTar("delme.mtaz", "delme/");
+else {
+ print(readTarFile("delme.mtar", "ahoj"));
+ print(readTarFile("delme.mtar", "nazdar"));
+}
+
diff --git a/apps/spacew/prep/prepare.json b/apps/spacew/prep/prepare.json
new file mode 100644
index 000000000..33cb21a3c
--- /dev/null
+++ b/apps/spacew/prep/prepare.json
@@ -0,0 +1,18 @@
+{
+ "attributes": {
+ "type": false,
+ "id": false,
+ "version": false,
+ "changeset": false,
+ "timestamp": false,
+ "uid": false,
+ "user": false,
+ "way_nodes": false,
+ },
+ "format_options": {
+ },
+ "linear_tags": true,
+ "area_tags": false,
+ "exclude_tags": [],
+ "include_tags": [ "place", "name", "landuse", "highway" ]
+}
diff --git a/apps/spacew/prep/prepare.sh b/apps/spacew/prep/prepare.sh
new file mode 100755
index 000000000..ac1db0019
--- /dev/null
+++ b/apps/spacew/prep/prepare.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+if [ ".$1" == "-f" ]; then
+ I=/data/gis/osm/dumps/czech_republic-2023-07-24.osm.pbf
+ #I=/data/gis/osm/dumps/zernovka.osm.bz2
+ O=cr.geojson
+ rm delme.pbf $O
+ time osmium extract $I --bbox 14.7,49.9,14.8,50.1 -f pbf -o delme.pbf
+ time osmium export delme.pbf -c prepare.json -o $O
+ echo "Converting to ascii"
+ time cstocs utf8 ascii cr.geojson > cr_ascii.geojson
+ mv -f cr_ascii.geojson delme.json
+fi
+rm -r delme/; mkdir delme
+./split.js
+./minitar.js
+ls -lS delme/*.json | head -20
+cat delme/* | wc -c
+ls -l delme.mtar
diff --git a/apps/spacew/prep/split.js b/apps/spacew/prep/split.js
new file mode 100755
index 000000000..3d6f81b63
--- /dev/null
+++ b/apps/spacew/prep/split.js
@@ -0,0 +1,167 @@
+#!/usr/bin/nodejs --max-old-space-size=5500
+
+// npm install geojson-vt
+// docs: https://github.com/mapbox/geojson-vt
+// output format: https://github.com/mapbox/vector-tile-spec/
+
+const fs = require('fs');
+const sphm = require('./sphericalmercator.js');
+var split = require('geojson-vt')
+
+// delme.json needs to be real file, symlink to geojson will not work
+console.log("Loading json");
+var gjs = require("./delme.json");
+
+function tileToLatLon(x, y, z, x_, y_) {
+ var [ w, s, e, n ] = merc.bbox(x, y, z);
+ var lon = (e - w) * (x_ / 4096) + w;
+ var lat = (n - s) * (1-(y_ / 4096)) + s;
+ return [ lon, lat ];
+}
+
+function convGeom(tile, geom) {
+ var g = [];
+ for (i = 0; i< geom.length; i++) {
+ var x = geom[i][0];
+ var y = geom[i][1];
+ var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y);
+ g.push(pos);
+ }
+ return g;
+}
+
+function zoomPoint(tags) {
+ var z = 99;
+
+ if (tags.place == "city") z = 4;
+ if (tags.place == "town") z = 8;
+ if (tags.place == "village") z = 10;
+
+ return z;
+}
+
+function paintPoint(tags) {
+ var p = {};
+
+ if (tags.place == "village") p["marker-color"] = "#ff0000";
+
+ return p;
+}
+
+function zoomWay(tags) {
+ var z = 99;
+
+ if (tags.highway == "motorway") z = 7;
+ if (tags.highway == "primary") z = 9;
+ if (tags.highway == "secondary") z = 13;
+ if (tags.highway == "tertiary") z = 14;
+ if (tags.highway == "unclassified") z = 16;
+ if (tags.highway == "residential") z = 17;
+ if (tags.highway == "track") z = 17;
+ if (tags.highway == "path") z = 17;
+ if (tags.highway == "footway") z = 17;
+
+ return z;
+}
+
+function paintWay(tags) {
+ var p = {};
+
+ if (tags.highway == "motorway" || tags.highway == "primary") /* ok */;
+ if (tags.highway == "secondary" || tags.highway == "tertiary") p.stroke = "#0000ff";
+ if (tags.highway == "tertiary" || tags.highway == "unclassified" || tags.highway == "residential") p.stroke = "#00ff00";
+ if (tags.highway == "track") p.stroke = "#ff0000";
+ if (tags.highway == "path" || tags.highway == "footway") p.stroke = "#800000";
+
+ return p;
+}
+
+function writeFeatures(name, feat)
+{
+ var n = {};
+ n.type = "FeatureCollection";
+ n.features = feat;
+
+ fs.writeFile(name+'.json', JSON.stringify(n), on_error);
+}
+
+function toGjson(name, d, tile) {
+ var cnt = 0;
+ var feat = [];
+ for (var a of d) {
+ var f = {};
+ var zoom = 99;
+ var p = {};
+ f.properties = a.tags;
+ f.type = "Feature";
+ f.geometry = {};
+ if (a.type == 1) {
+ f.geometry.type = "Point";
+ f.geometry.coordinates = convGeom(tile, a.geometry)[0];
+ zoom = zoomPoint(a.tags);
+ p = paintPoint(a.tags);
+ } else if (a.type == 2) {
+ f.geometry.type = "LineString";
+ f.geometry.coordinates = convGeom(tile, a.geometry[0]);
+ zoom = zoomWay(a.tags);
+ p = paintWay(a.tags);
+ } else {
+ //console.log("Unknown type", a.type);
+ }
+ //zoom -= 4; // Produces way nicer map, at expense of space.
+ if (tile.z < zoom)
+ continue;
+ f.properties = Object.assign({}, f.properties, p);
+ feat.push(f);
+ var s = JSON.stringify(feat);
+ if (s.length > 6000) {
+ console.log("tile too big, splitting", cnt);
+ writeFeatures(name+'-'+cnt++, feat);
+ feat = [];
+ }
+ }
+ writeFeatures(name+'-'+cnt, feat);
+ return n;
+}
+
+function writeTile(name, d, tile) {
+ toGjson(name, d, tile)
+}
+
+// By default, precomputes up to z30
+var merc = new sphm({
+ size: 256,
+ antimeridian: true
+});
+
+console.log("Splitting data");
+var meta = {}
+meta.min_zoom = 0;
+meta.max_zoom = 17; // HERE
+ // = 16 ... split3 takes > 30 minutes
+ // = 13 ... 2 minutes
+var index = split(gjs, Object.assign({
+ maxZoom: meta.max_zoom,
+ indexMaxZoom: meta.max_zoom,
+ indexMaxPoints: 0,
+ tolerance: 30,
+}), {});
+console.log("Producing output");
+
+var output = {};
+
+function on_error(e) {
+ if (e) { console.log(e); }
+}
+
+var num = 0;
+for (const id in index.tiles) {
+ const tile = index.tiles[id];
+ const z = tile.z;
+ console.log(num++, ":", tile.x, tile.y, z);
+ var d = index.getTile(z, tile.x, tile.y).features;
+ var n = `delme/z${z}-${tile.x}-${tile.y}` ;
+ writeTile(n, d, tile)
+}
+
+fs.writeFile('delme/meta.json', JSON.stringify(meta), on_error);
diff --git a/apps/spacew/prep/stats.sh b/apps/spacew/prep/stats.sh
new file mode 100755
index 000000000..6c10ea1b0
--- /dev/null
+++ b/apps/spacew/prep/stats.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+zoom() {
+ echo "Zoom $1"
+ cat delme/z$1-* | wc -c
+ echo "M..k..."
+}
+
+echo "Total data"
+cat delme/* | wc -c
+echo "M..k..."
+zoom 18
+zoom 17
+zoom 16
+zoom 15
+zoom 14
+zoom 13
+zoom 12
+zoom 11
+zoom 10
+echo "Zoom 1..9"
+cat delme/z?-* | wc -c
+echo "M..k..."
diff --git a/apps/stacker/ChangeLog b/apps/stacker/ChangeLog
new file mode 100644
index 000000000..b0d975391
--- /dev/null
+++ b/apps/stacker/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Optimizations
\ No newline at end of file
diff --git a/apps/stacker/README.md b/apps/stacker/README.md
new file mode 100644
index 000000000..5be5b7bee
--- /dev/null
+++ b/apps/stacker/README.md
@@ -0,0 +1,12 @@
+# Stacker
+
+A simple game of stacking cubes.
+
+
+## Usage
+
+Press the button to stack!
+
+## Creator
+
+NovaDawn999
diff --git a/apps/stacker/app-icon.js b/apps/stacker/app-icon.js
new file mode 100644
index 000000000..37130b23f
--- /dev/null
+++ b/apps/stacker/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgIcZh////AAoMIAQNAAq8D//IDrQFFn//DrcH/+IDqx9DAQIADwEAggOBkAF/AuJ9FA=="))
\ No newline at end of file
diff --git a/apps/stacker/app.js b/apps/stacker/app.js
new file mode 100644
index 000000000..a486e06cc
--- /dev/null
+++ b/apps/stacker/app.js
@@ -0,0 +1,135 @@
+const HARDWARE_VERSION = process.env.HWVERSION;
+const BUTTON = HARDWARE_VERSION === 2 ? BTN : BTN2;
+const TICKRATE = 69;
+const BLOCK_SIZE = 16;
+const GAMEBOARD_X = 16;
+const GAMEBOARD_WIDTH = g.getWidth() - 16 - BLOCK_SIZE;
+const START_Y = g.getHeight() - BLOCK_SIZE - 1;
+const START_LENGTH = 4;
+var length;
+var updateTimeout;
+var rows = [];
+var gameState = ""; //win, lose, play
+
+function Block (x, y, match) {
+ this.x = x;
+ this.y = y;
+ this.match = match;
+ this.show = true;
+}
+
+class Row {
+ constructor(x, y, size, direction, match) {
+ this.y = y;
+ this.size = size;
+ this.blocks = [];
+ if (Math.random() > 0.49) {
+ this.direction = 1;
+ this.x = BLOCK_SIZE;
+ }
+ else {
+ this.direction = -1;
+ this.x = g.getWidth() - this.size * BLOCK_SIZE;
+ }
+ this.match = match;
+ for (var i = 0; i < size; i++) {
+ var b = new Block(this.x + (BLOCK_SIZE * i), this.y, this.match);
+ this.blocks.push(b);
+ }
+ }
+ update() {
+ this.x += BLOCK_SIZE * this.direction;
+ if (this.x + (this.size * BLOCK_SIZE) > GAMEBOARD_X + GAMEBOARD_WIDTH || this.x < GAMEBOARD_X) {
+ this.direction = -this.direction;
+ }
+ for (var i = 0; i < this.size; i++) {
+ this.blocks[i].x = this.x + BLOCK_SIZE * i;
+ }
+ }
+ draw() {
+ for (var i = 0; i < this.size; i++) {
+ if (this.blocks[i].show) {
+ g.drawRect({x: this.blocks[i].x, y: this.y, w: BLOCK_SIZE, h: BLOCK_SIZE});
+ }
+ }
+ }
+}
+
+
+
+function init() {
+ Bangle.setLCDPower(1);
+ g.setTheme({bg:"#000", fg:"#fff", dark:true}).clear();
+ setInterval(update, TICKRATE);
+ setWatch(handleInput, BUTTON, {repeat:true});
+ changeState("play");
+}
+
+function update() {
+ "ram"
+ if (gameState === "play") {
+ g.clear(reset);
+ rows[rows.length - 1].update();
+ rows.forEach(row => row.draw());
+ g.flip();
+ }
+}
+
+function changeState(gs) {
+ gameState = gs;
+ g.clear(reset);
+ switch(gameState) {
+ case "win":
+ E.showMessage("YOU WIN!");
+ break;
+ case "lose":
+ E.showMessage("YOU LOSE!");
+ break;
+ case "play":
+ rows = [];
+ length = START_LENGTH;
+ var first = new Row(GAMEBOARD_X, START_Y, length, 1, true);
+ rows.push(first);
+ break;
+ }
+}
+
+function collapse() {
+ "ram"
+ for (var i = 0; i < rows[rows.length - 1].blocks.length; i++) {
+ for (var j = 0; j < rows[rows.length -2].blocks.length; j++) {
+ if (rows[rows.length - 1].blocks[i].x === rows[rows.length - 2].blocks[j].x) {
+ if (rows[rows.length - 2].blocks[j].match === true)
+ rows[rows.length - 1].blocks[i].match = true;
+ }
+ }
+ }
+ for (var y = 0; y < rows[rows.length - 1].blocks.length; y++) {
+ if (rows[rows.length - 1].blocks[y].match === false) {
+ length -= 1;
+ if (length < 1) {
+ changeState("lose");
+ }
+ rows[rows.length - 1].blocks[y].show = false;
+ }
+ }
+}
+
+function handleInput() {
+ if (gameState === "win" || gameState === "lose") {
+ changeState("play");
+ }
+ else {
+ if (rows.length > 1) {
+ collapse();
+ if (rows[rows.length - 1].y <= -1) {
+ changeState("win");
+ }
+ }
+ var r = new Row(GAMEBOARD_X + Math.round(length/2) * BLOCK_SIZE, rows[rows.length - 1].y - BLOCK_SIZE, length, 1, false);
+ rows.push(r);
+ }
+}
+
+init();
+update();
\ No newline at end of file
diff --git a/apps/stacker/app.png b/apps/stacker/app.png
new file mode 100644
index 000000000..35683688a
Binary files /dev/null and b/apps/stacker/app.png differ
diff --git a/apps/stacker/metadata.json b/apps/stacker/metadata.json
new file mode 100644
index 000000000..abaf49a6d
--- /dev/null
+++ b/apps/stacker/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "stacker",
+ "name": "Stacker",
+ "shortName":"Stacker",
+ "version":"0.02",
+ "description": "Game of Stacking",
+ "icon": "app.png",
+ "tags": "game",
+ "supports" : ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"stacker.app.js","url":"app.js"},
+ {"name":"stacker.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog
index c4f382aa9..cb016df1a 100644
--- a/apps/stopwatch/ChangeLog
+++ b/apps/stopwatch/ChangeLog
@@ -2,3 +2,4 @@
0.02: Adjust for touch events outside of screen g dimensions
0.03: Do not register as watch, manually start clock on button
0.04: Keep running in background by saving state
+0.05: Fast Loading support
diff --git a/apps/stopwatch/metadata.json b/apps/stopwatch/metadata.json
index bbc2dc181..27cdacb71 100644
--- a/apps/stopwatch/metadata.json
+++ b/apps/stopwatch/metadata.json
@@ -1,7 +1,7 @@
{
"id": "stopwatch",
"name": "Stopwatch Touch",
- "version": "0.04",
+ "version": "0.05",
"description": "A touch based stop watch for Bangle JS 2",
"icon": "stopwatch.png",
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js
index d98f06cdd..0d3ec364e 100644
--- a/apps/stopwatch/stopwatch.app.js
+++ b/apps/stopwatch/stopwatch.app.js
@@ -1,3 +1,4 @@
+{
const CONFIGFILE = "stopwatch.json";
const now = Date.now();
@@ -20,6 +21,7 @@ let timeY = 2*h/5;
let displayInterval;
let redrawButtons = true;
const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size
+const origTheme = g.theme;
// 24 pixel images, scale to watch
// 1 bit optimal, image string, no E.toArrayBuffer()
@@ -27,19 +29,19 @@ const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wY
const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA=");
const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w==");
-function saveState() {
+const saveState = function() {
config.state.total = tTotal;
config.state.start = tStart;
config.state.current = tCurrent;
config.state.running = running;
require("Storage").writeJSON(CONFIGFILE, config);
-}
+};
-function log_debug(o) {
+const log_debug = function(o) {
//console.log(o);
-}
+};
-function timeToText(t) {
+const timeToText = function(t) {
let hrs = Math.floor(t/3600000);
let mins = Math.floor(t/60000)%60;
let secs = Math.floor(t/1000)%60;
@@ -53,9 +55,9 @@ function timeToText(t) {
//log_debug(text);
return text;
-}
+};
-function drawButtons() {
+const drawButtons = function() {
log_debug("drawButtons()");
if (!running && tCurrent == tTotal) {
bigPlayPauseBtn.draw();
@@ -65,11 +67,11 @@ function drawButtons() {
} else {
bigPlayPauseBtn.draw();
}
-
- redrawButtons = false;
-}
-function drawTime() {
+ redrawButtons = false;
+};
+
+const drawTime = function() {
log_debug("drawTime()");
let Tt = tCurrent-tTotal;
let Ttxt = timeToText(Tt);
@@ -80,32 +82,32 @@ function drawTime() {
g.clearRect(0, timeY - 21, w, timeY + 21);
g.setColor(g.theme.fg);
g.drawString(Ttxt, w/2, timeY);
-}
+};
-function draw() {
+const draw = function() {
let last = tCurrent;
if (running) tCurrent = Date.now();
g.setColor(g.theme.fg);
if (redrawButtons) drawButtons();
drawTime();
-}
+};
-function startTimer() {
+const startTimer = function() {
log_debug("startTimer()");
draw();
displayInterval = setInterval(draw, 100);
-}
+};
-function stopTimer() {
+const stopTimer = function() {
log_debug("stopTimer()");
if (displayInterval) {
clearInterval(displayInterval);
displayInterval = undefined;
}
-}
+};
// BTN stop start
-function stopStart() {
+const stopStart = function() {
log_debug("stopStart()");
if (running)
@@ -127,9 +129,9 @@ function stopStart() {
draw();
}
saveState();
-}
+};
-function setButtonImages() {
+const setButtonImages = function() {
if (running) {
bigPlayPauseBtn.setImage(pause_img);
smallPlayPauseBtn.setImage(pause_img);
@@ -139,10 +141,10 @@ function setButtonImages() {
smallPlayPauseBtn.setImage(play_img);
resetBtn.setImage(reset_img);
}
-}
+};
// lap or reset
-function lapReset() {
+const lapReset = function() {
log_debug("lapReset()");
if (!running && tStart != tCurrent) {
redrawButtons = true;
@@ -152,10 +154,10 @@ function lapReset() {
draw();
}
saveState();
-}
+};
// simple on screen button class
-function BUTTON(name,x,y,w,h,c,f,i) {
+const BUTTON = function(name,x,y,w,h,c,f,i) {
this.name = name;
this.x = x;
this.y = y;
@@ -164,16 +166,16 @@ function BUTTON(name,x,y,w,h,c,f,i) {
this.color = c;
this.callback = f;
this.img = i;
-}
+};
BUTTON.prototype.setImage = function(i) {
this.img = i;
-}
+};
// if pressed the callback
BUTTON.prototype.check = function(x,y) {
//console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
-
+
if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
log_debug(this.name + ":callback\n");
this.callback();
@@ -197,48 +199,52 @@ BUTTON.prototype.draw = function() {
};
-var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
-var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
-var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
+const bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
+const smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
+const resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
bigPlayPauseBtn.setImage(play_img);
smallPlayPauseBtn.setImage(play_img);
resetBtn.setImage(pause_img);
+Bangle.setUI({mode:"custom", btn:() => load(), touch: (button,xy) => {
+ let x = xy.x;
+ let y = xy.y;
-Bangle.on('touch', function(button, xy) {
- var x = xy.x;
- var y = xy.y;
+ // adjust for outside the dimension of the screen
+ // http://forum.espruino.com/conversations/371867/#comment16406025
+ if (y > h) y = h;
+ if (y < 0) y = 0;
+ if (x > w) x = w;
+ if (x < 0) x = 0;
- // adjust for outside the dimension of the screen
- // http://forum.espruino.com/conversations/371867/#comment16406025
- if (y > h) y = h;
- if (y < 0) y = 0;
- if (x > w) x = w;
- if (x < 0) x = 0;
+ // not running, and reset
+ if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return;
- // not running, and reset
- if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return;
+ // paused and hit play
+ if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return;
- // paused and hit play
- if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return;
+ // paused and press reset
+ if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return;
- // paused and press reset
- if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return;
-
- // must be running
- if (running && bigPlayPauseBtn.check(x, y)) return;
-});
+ // must be running
+ if (running && bigPlayPauseBtn.check(x, y)) return;
+ }, remove: () => {
+ if (displayInterval) {
+ clearInterval(displayInterval);
+ displayInterval = undefined;
+ }
+ Bangle.removeListener('lcdPower',onLCDPower);
+ g.setTheme(origTheme);
+}});
// Stop updates when LCD is off, restart when on
-Bangle.on('lcdPower',on=>{
+const onLCDPower = (on) => {
if (on) {
draw(); // draw immediately, queue redraw
- } else { // stop draw timer
- if (drawTimeout) clearTimeout(drawTimeout);
- drawTimeout = undefined;
}
-});
+};
+Bangle.on('lcdPower',onLCDPower);
// Clear the screen once, at startup
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
@@ -254,4 +260,4 @@ if (running) {
} else {
draw();
}
-setWatch(() => load(), BTN, { repeat: false, edge: "falling" });
+}
diff --git a/apps/taglaunch/ChangeLog b/apps/taglaunch/ChangeLog
index 55315bf6e..6c36d39d5 100644
--- a/apps/taglaunch/ChangeLog
+++ b/apps/taglaunch/ChangeLog
@@ -2,3 +2,4 @@
0.02: Use Bangle.showClock for changing to clock (Backport from launch)
0.03: Remove app from 'tool' when it has at least one other known tag
Add tag 'health' for apps like Heart Rate Monitor
+0.04: Fix remove handler
diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js
index aad61e298..9569cc7bd 100644
--- a/apps/taglaunch/app.js
+++ b/apps/taglaunch/app.js
@@ -1,15 +1,14 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
let s = require("Storage");
-// TODO: Move icons to separate files
-// TODO: Allow change sortorder in settings
-let tags = {"clock": {name: /*LANG*/"Clocks", icon: atob("MDCEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERESIiIiIREREREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERESIiIiIgz//8ziIiIiIREREREREREREREiIiIg////////ziIiIhERERERERERERIiIiD//////////84iIiERERERERERESIiIg/////8A/////ziIiIRERERERERESIiI//////8A//////+IiIREREREREREiIiP//////8A///////4iIhERERERERIiIg///////8A///////ziIiERERERERIiIP///////8A////////OIiERERERESIiI////////8A////////+IiIRERERESIiD////////8A////////84iIRERERESIiP////////8A/////////4iIRERERESIiP////////8A/////////4iIREREREiIg/////////8A/////////ziIhEREREiIg/////////IAL////////ziIhEREREiIj////////yAAAv////////iIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIj////////yAAAD////////iIhEREREiIg/////////IAABP//////ziIhEREREiIg///////////IAE//////ziIhERERESIiP//////////8gAT/////4iIRERERESIiP///////////yABP////4iIRERERESIiD////////////IAL///84iIRERERESIiI////////////8i////+IiIRERERERIiIP/////////////////OIiERERERERIiIg////////////////ziIiEREREREREiIiP///////////////4iIhERERERERESIiI//////////////+IiIRERERERERESIiIg/////8i/////ziIiIRERERERERERIiIiD////8i////84iIiEREREREREREREiIiIg///8i///ziIiIhERERERERERERESIiIiIgz8i8ziIiIiIREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERERERESIiIiIRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA==")},
- "game": {name: /*LANG*/"Games", sortorder: 1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzIiIiIiIiIiIiIiIiIiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8RMxERABEAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IhERERERAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzL///IiIi////IiIi///xERAAAAAAAAAzL///IiIi////IiIi//8xERAAAAAAAAAzL///IiIi////IiIi//8hEREAAAAAAAAzL///IiIi////IiIi//8REREAAAAAAAAzL///IiIi////IiIi//MREREAAAAAAAAzIiIiIiIiIiIiIiIiIzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMhEREREREAAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "bluetooth": {name: /*LANG*/"Bluetooth", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "outdoors": {name: /*LANG*/"Outdoor", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "misc": {name: /*LANG*/"Misc", icon: atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
- "health": {name: /*LANG*/"Health"},
+// TODO: Allow to change sortorder in settings
+let tags = {"clock": {name: /*LANG*/"Clocks", icon: () => atob("MDCEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERESIiIiIREREREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERESIiIiIgz//8ziIiIiIREREREREREREREiIiIg////////ziIiIhERERERERERERIiIiD//////////84iIiERERERERERESIiIg/////8A/////ziIiIRERERERERESIiI//////8A//////+IiIREREREREREiIiP//////8A///////4iIhERERERERIiIg///////8A///////ziIiERERERERIiIP///////8A////////OIiERERERESIiI////////8A////////+IiIRERERESIiD////////8A////////84iIRERERESIiP////////8A/////////4iIRERERESIiP////////8A/////////4iIREREREiIg/////////8A/////////ziIhEREREiIg/////////IAL////////ziIhEREREiIj////////yAAAv////////iIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIgiIv/////wCIAP/////yIiiIhEREREiIj////////yAAAD////////iIhEREREiIg/////////IAABP//////ziIhEREREiIg///////////IAE//////ziIhERERESIiP//////////8gAT/////4iIRERERESIiP///////////yABP////4iIRERERESIiD////////////IAL///84iIRERERESIiI////////////8i////+IiIRERERERIiIP/////////////////OIiERERERERIiIg////////////////ziIiEREREREREiIiP///////////////4iIhERERERERESIiI//////////////+IiIRERERERERESIiIg/////8i/////ziIiIRERERERERERIiIiD////8i////84iIiEREREREREREREiIiIg///8i///ziIiIhERERERERERERESIiIiIgz8i8ziIiIiIREREREREREREREREiIiIiIiIiIiIiIhERERERERERERERERESIiIiIiIiIiIiIREREREREREREREREREREiIiIiIiIiIhERERERERERERERERERERERESIiIiIRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA==")},
+ "game": {name: /*LANG*/"Games", sortorder: 1, icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAzIiIiIiIiIiIiIiIiIiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzIiIi////IiIi////IiIiMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi///yMwAAAAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8R/xERABEAAAAAAzL///IiIi////IiIi8RMxERABEAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IREREREREAAAAAAzIiIi////IiIi////IhERERERAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzIiIi////IiIi////IiMzMzMwAAAAAAAzL///IiIi////IiIi///xERAAAAAAAAAzL///IiIi////IiIi//8xERAAAAAAAAAzL///IiIi////IiIi//8hEREAAAAAAAAzL///IiIi////IiIi//8REREAAAAAAAAzL///IiIi////IiIi//MREREAAAAAAAAzIiIiIiIiIiIiIiIiIzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAzMzMzMzMzMzMzMzMzMhEREREREAAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAEREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "tool": {name: /*LANG*/"Tools", sortorder: -1, icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAAAAAAAAAIiIgAAAAAAAAAAADMzMAAAAAAAAAAAAiIiIgAAAAAAAAAAADMzMwAAAAAAAAAAIiIiIAAAAAAAAAAAAAMzMzAAAAAAAAACIiIiAAAAAAAAAAAAAAMzMzMAAAAAAAACIiIgAAAAAAAAAAAAAAAzMzMAAAAAAAAiIiIAAAAAAAAAAAAAAAADMzMwAAAAAAAiIiIAAAAAIgAAAAAAAAAAMzMzAAAAAAAiIiIgAAACIgAAAAAAAAAAADMzMAAAAAAiIiIiAAAiIgAAAAAAAAAAAAMzMwAAAAAiIiIiIAIiIgAAAAAAAAAAAAAzMzAAAAACIiIiIiIiIgAAAAAAAAAAAAADMzMAAAAiIiIiIiIiIAAAAAAAAAAAAAAAMzMwDdQiIiIiIiIiIAAAAAAAAAAAAAAAAzMz3d0iIiIiIiIiAAAAAAAAAAAAAAAAADM93d1CIiIiIiIgAAAAAAAAAAAAAAAAAAPd3d3iIiACIiAAAAAAAAAAAAAAAAAAAA3d3d7kIgAAAAAAAAAAAAAAAAAAAAAAAN3d3e7uQAAAAAAAAAAAAAAAAAAAAAAAAN3d3u7u4AAAAAAAAAAAAAAAAAAAAAAAAC3d7u7u7gAAAAAAAAAAAAAAAAAAAAAAAiJO7u7u7uAAAAAAAAAAAAAAAAAAAAAAIiIiTu7u7u7gAAAAAAAAAAAAAAAAAAACIiIiIu7u7u7uAAAAAAAAAAAAAAAAAAAiIiIiIA7u7u7u4AAAAAAAAAAAAAAAAAIiIiIiAADu7u7u7uAAAAAAAAAAAAAAACIiIiIgAAAO7u7u7u4AAAAAAAAAAAAAAiIiIiIAAAAO7u7u7u7gAAAAAAAAAAAAIiIiIiAAAAAA7u7u7u7uAAAAAAAAAAAiIiIiIgAAAAAADu7u7u7u4AAAAAAAAAIiIiIiIAAAAAAAAO7u7u7u7gAAAAAAAAIiIiIiAAAAAAAAAO7u7u7u7gAAAAAAACIgAiIgAAAAAAAAAA7u7u7u7gAAAAAAACIgAiIAAAAAAAAAAADu7u7u7gAAAAAAACIiIiIAAAAAAAAAAAAO7u7u4AAAAAAAAAIiIiAAAAAAAAAAAAAO7u7uAAAAAAAAAAAiIAAAAAAAAAAAAAAADu7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "bluetooth": {name: /*LANG*/"Bluetooth", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqqgAAAAAAAAAAAAAAAKqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAACqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAACqqqqqqqoAAAAAAAAAAAAAAAAAAAAAAAqqqqqqqqqgAAAAAAAAAAAAAAAAAAAAAKqqoKqqCqqqAAAAAAAAAAAAAAAAAAAACqqqAKqqAKqqoAAAAAAAAAAAAAAAAAAAqqqgAKqqAAqqqgAAAAAAAAAAAAAAAAAKqqoAAKqqAACqqqAAAAAAAAAAAAAAAACqqqAAAKqqAAAKqqoAAAAAAAAAAAAAAAAKqgAAAKqqAACqqqoAAAAAAAAAAAAAAAAAoAAAAKqqAAqqqqAAAAAAAAAAAAAAAAAAAAAAAKqqAKqqqgAAAAAAAAAAAAAAAAAAAAAAAKqqCqqqoAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqqAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqqgAAAAAAAAAAAAAAAAAAAAAAAAAKqqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAKqqqgAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "outdoors": {name: /*LANG*/"Outdoor", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN3d0AAAAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3d3d3d3QAAAAAAAAAAAAAAAAAAAAAADd3e7u7d3QAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAA3d7u7u7u3dAAAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAADd3d7u7u7u3d3QAAAAAAAAAAAAAAAAAAAN3d7u7u7u3d0AAAAAAAAAAAAAAAAAAAAA3d3u7u7u3dAAAAAAAAAAAAAAAAAAAAAADd3u7u7t3QAAAAAAAAAAAAAAAAAAAAAADd3d7u7d3QAMzMwAAAAAAAAO4AAAAAAADd3d3d3d3QAMzMwAAAAAAA7u7gAAAAAADd3d3d3d3QAAzMwAAAAAAO7u7gAAAAAAAAAN3d0AAAAAzMAAAAAAAO7u7gAAAAAAAAAA3dAAAAAAzMAAAAAADu7u4AAAAAAAAAAADQAAAAAAzMAAAAAADu7u4AAAAAAAAAAAAAAAAAAAERAAAAAA7u7uAAAAAAAAAAAAAAAAAAAAERAAAAQO7u7uAAAAAAAAAAAAAAAAAAAAEREAAEAO7u7gAAAAAAAAAAAAAAAAAAABEREN3U3e7u7gAAAAAAAAAAAAAAAAAAARERFN3d3d7u4AAAAAAAAAAAAAAAAAAAARERFEREREREQAAAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAEREREREREREREREREAAAAAAAAAAAAAAAARERERERERERERERAAAAAAAAAAAAAAAAAEREREREREREREREAAAAAAAAAAAAAAAAAERERERERERERERAAAAAAAAAADMzMzMzMxFEERRBEUQRFEETMzMzAAAAADMzMzMzMyFEERRBEUQRFEETMzMzAAAAADMzMzMzMzREREREREREREQzMzMzAAAAADMzMzMzMzJEREREREREREIzMzMzAAAAADMzMzMzMzMkRERERERERCMzMzMzAAAAADMzMzMzMzMzJEREREREIzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAADMzMzMzMzMzMzMzMzMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "misc": {name: /*LANG*/"Misc", icon: () => atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAACIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")},
+ "health": {name: /*LANG*/"Health", icon: () => require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA=="))},
};
// handle customised launcher
@@ -73,6 +72,23 @@ let tagKeys = Object.keys(tags).filter(tag => tag !== "clock" || settings.showCl
if (!settings.fullscreen)
Bangle.loadWidgets();
+const unload = () => {
+ // cleanup the timeout to not leave anything behind after being removed from ram
+ if (lockTimeout) clearTimeout(lockTimeout);
+ Bangle.removeListener("lock", lockHandler);
+};
+
+// 10s of inactivity goes back to clock
+Bangle.setLocked(false); // unlock initially
+let lockTimeout;
+let lockHandler = function(locked) {
+ if (lockTimeout) clearTimeout(lockTimeout);
+ lockTimeout = undefined;
+ if (locked) {
+ lockTimeout = setTimeout(Bangle.showClock, 10000);
+ }
+};
+
let showTagMenu = (tag) => {
E.showScroller({
h : 64*scaleval, c : appsByTag[tag].length,
@@ -96,8 +112,10 @@ let showTagMenu = (tag) => {
load(app.src);
}
},
- back : showMainMenu
+ back : showMainMenu,
+ remove: unload
});
+ Bangle.on("lock", lockHandler);
};
let showMainMenu = () => {
@@ -108,7 +126,7 @@ let showMainMenu = () => {
g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1));
g.setFont(font).setFontAlign(-1,0).drawString(tags[tag].name,64*scaleval,r.y+(32*scaleval));
- const img = tags[tag].icon ? tags[tag].icon : s.read("taglaunch." + tag + ".img");
+ const img = tags[tag].icon ? tags[tag].icon() : s.read("taglaunch." + tag + ".img");
if (img) {
try {g.drawImage(img,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){}
}
@@ -118,27 +136,13 @@ let showMainMenu = () => {
showTagMenu(tag);
},
back : Bangle.showClock, // button press or tap in top left shows clock now
- remove : () => {
- // cleanup the timeout to not leave anything behind after being removed from ram
- if (lockTimeout) clearTimeout(lockTimeout);
- Bangle.removeListener("lock", lockHandler);
- }
+ remove : unload
});
+ Bangle.on("lock", lockHandler);
};
showMainMenu();
g.flip(); // force a render before widgets have finished drawing
-// 10s of inactivity goes back to clock
-Bangle.setLocked(false); // unlock initially
-let lockTimeout;
-let lockHandler = function(locked) {
- if (lockTimeout) clearTimeout(lockTimeout);
- lockTimeout = undefined;
- if (locked) {
- lockTimeout = setTimeout(Bangle.showClock, 10000);
- }
-};
-Bangle.on("lock", lockHandler);
if (!settings.fullscreen) // finally draw widgets
Bangle.drawWidgets();
}
diff --git a/apps/taglaunch/health-icon.js b/apps/taglaunch/health-icon.js
deleted file mode 100644
index 11b513b72..000000000
--- a/apps/taglaunch/health-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwwhC/AEnM5ndABgWGhgXP6AuHC5wwGC/4X/C98z///mYXSn4WBAAPzC6HTCwYABnoXPFwgwGC5QuDAoIwGC5XfC4/9C5pGDC4hIDC8QVDAAYHEC/4XSR97XX6YXHnoXNJAhGGC5gwDFwwXMGAouEC5vdmYWBmbEFC5oAJC/4X/C9sMC5/QC4owCFyYA/ADoA=="))
diff --git a/apps/taglaunch/metadata.json b/apps/taglaunch/metadata.json
index 4f7c295e9..a4fb4ef6c 100644
--- a/apps/taglaunch/metadata.json
+++ b/apps/taglaunch/metadata.json
@@ -2,7 +2,7 @@
"id": "taglaunch",
"name": "Tag Launcher",
"shortName": "Taglauncher",
- "version": "0.03",
+ "version": "0.04",
"description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.",
"readme": "README.md",
"icon": "app.png",
@@ -12,8 +12,7 @@
"screenshots": [ {"url":"screenshot.png"} ],
"storage": [
{"name":"taglaunch.app.js","url":"app.js"},
- {"name":"taglaunch.settings.js","url":"settings.js"},
- {"name":"taglaunch.health.img","url":"health-icon.js","evaluate":true}
+ {"name":"taglaunch.settings.js","url":"settings.js"}
],
"data": [{"name":"taglaunch.json"},{"name":"taglaunch.cache.json"}]
}
diff --git a/apps/teatimer/ChangeLog b/apps/teatimer/ChangeLog
index db8dd270b..31b495ecf 100644
--- a/apps/teatimer/ChangeLog
+++ b/apps/teatimer/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Fix issue setting colors after showMessage
0.03: Fix BG/FG Color if e.g. theme background is black
+0.04: Get time zone from settings for showing the clock
diff --git a/apps/teatimer/app.js b/apps/teatimer/app.js
index c394b5e00..fbaed4d62 100644
--- a/apps/teatimer/app.js
+++ b/apps/teatimer/app.js
@@ -13,7 +13,8 @@ const states = {
stop: 32 // timer stopped
};
var state = states.start;
-E.setTimeZone(1);
+let setting = require("Storage").readJSON("setting.json",1);
+E.setTimeZone(setting.timezone);
// Title showing current time
function appTitle() {
diff --git a/apps/teatimer/metadata.json b/apps/teatimer/metadata.json
index b5cdce92e..a298a0e2b 100644
--- a/apps/teatimer/metadata.json
+++ b/apps/teatimer/metadata.json
@@ -1,7 +1,7 @@
{
"id": "teatimer",
"name": "Tea Timer",
- "version": "0.03",
+ "version": "0.04",
"description": "A simple timer. You can easyly set up the time.",
"icon": "teatimer.png",
"type": "app",
diff --git a/apps/timerclk/ChangeLog b/apps/timerclk/ChangeLog
index 5a954d58c..46aa52ee1 100644
--- a/apps/timerclk/ChangeLog
+++ b/apps/timerclk/ChangeLog
@@ -2,3 +2,4 @@
0.02: Add sunrise/sunset. Fix timer bugs.
0.03: Use default Bangle formatter for booleans
0.04: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
+0.05: Improve responsiveness and detection of swipes on main clock screen
diff --git a/apps/timerclk/app.js b/apps/timerclk/app.js
index ee30b059a..e489f9844 100644
--- a/apps/timerclk/app.js
+++ b/apps/timerclk/app.js
@@ -148,23 +148,23 @@ if (process.env.HWVERSION==1) {
setWatch(()=>load("timerclk.alarm.js"), BTN3);
setWatch(()=>load("timerclk.alarm.js"), BTN1);
} else {
- var absY, lastX, lastY;
+ var absY, lastX=0, lastY=0;
Bangle.on('drag', e=>{
if (!e.b) {
- if (lastX > 50) { // right
+ if (lastX > 5) { // right
if (absY < dragBorder) { // drag over time
load("timerclk.timer.js");
}else { // drag over date/dow
load("timerclk.alarm.js");
}
- } else if (lastX < -50) { // left
+ } else if (lastX < -5) { // left
if (absY < dragBorder) { // drag over time
load("timerclk.stopwatch.js");
}else { // drag over date/dow
load("timerclk.alarm.js");
}
- } else if (lastY > 50) { // down
- } else if (lastY < -50) { // up
+ } else if (lastY > 5) { // down
+ } else if (lastY < -5) { // up
}
lastX = 0;
lastY = 0;
diff --git a/apps/timerclk/lib.js b/apps/timerclk/lib.js
index dd3893fa1..47f49736f 100644
--- a/apps/timerclk/lib.js
+++ b/apps/timerclk/lib.js
@@ -87,7 +87,7 @@ exports.registerControls = function(o) {
}
}
});
- var absX, lastX, lastY;
+ var absX, lastX=0, lastY=0;
Bangle.on('drag', e=>{
if (!e.b) {
if (lastX > 40) { // right
diff --git a/apps/timerclk/metadata.json b/apps/timerclk/metadata.json
index 5bd6bee24..0a6311ac1 100644
--- a/apps/timerclk/metadata.json
+++ b/apps/timerclk/metadata.json
@@ -2,7 +2,7 @@
"id": "timerclk",
"name": "Timer Clock",
"shortName":"Timer Clock",
- "version":"0.04",
+ "version":"0.05",
"description": "A clock with stopwatches, timers and alarms build in.",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/usgs/ChangeLog b/apps/usgs/ChangeLog
new file mode 100644
index 000000000..65536966a
--- /dev/null
+++ b/apps/usgs/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Changed menu names, added button interaction.
diff --git a/apps/usgs/README.md b/apps/usgs/README.md
new file mode 100644
index 000000000..f8c91f0a6
--- /dev/null
+++ b/apps/usgs/README.md
@@ -0,0 +1,33 @@
+# USGS Data reporting
+
+More information on making apps:
+
+* http://www.espruino.com/Bangle.js+First+App
+* http://www.espruino.com/Bangle.js+App+Loader
+
+Simple app that pulls data from internet API regarding water/stream/river conditions. Useful for fishing or other water sports.
+
+## Usage
+
+API information can be found here:
+https://labs.waterdata.usgs.gov/docs/sensorthings/index.html
+
+Location can be found via map here:
+https://maps.waterdata.usgs.gov/mapper/
+Find the site you're looking for and note the "site number". This will be the "location" setting.
+
+
+
+Keyboard is required to change location setting.
+
+## Features
+
+Simple interface will display most current data for the specified location when the app is opened. Settings can change the location, or which data is displayed.
+
+## Controls
+
+Press button (middle on Bangle 1) once to refresh data, twice to return to launcher.
+
+## Creator
+
+inhof009
diff --git a/apps/usgs/app-icon.js b/apps/usgs/app-icon.js
new file mode 100644
index 000000000..e98b91bad
--- /dev/null
+++ b/apps/usgs/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AHeIAAIYYDKkIDAeADFYXDJRECGC0iGCtEkAXPGAkEolAPJ4wEogXMJBIXBSKhGBC6RgCLxoXJLxwXCwc4C4oWMR4UzC65HFIyAXCBAUiAQMCFxheCC40gRpguCC4ouNLwwsCC5QWCIwYXCgQXBIxIuDC6bqDOwwXBIxIuDOw4XDggXICgTtCC4VAC4KQBbI4sEI4oXDggXIdQYXFokCkUgIw4vFAAYXDGANEC6CnBokiC4UBO5JECGAgXBkUkoERC5BgDGAgXEiBIKGAR5DklCC4NBC5AwDCgI0EF4QXJGAbaDDAgXBCxAwCSQgYDwUkOw4wFAAh+EkIXKGAQAJYwIYWfAQA/AHA"))
diff --git a/apps/usgs/app.js b/apps/usgs/app.js
new file mode 100644
index 000000000..e6f2efa1e
--- /dev/null
+++ b/apps/usgs/app.js
@@ -0,0 +1,89 @@
+var dataStreams = {}; // Will hold directions to Datastreams and units, with above keys
+var FILE = "usgs.json";
+ // Load settings
+ var settings = Object.assign({
+ loc: '03272100',
+ keys: {'Gage height': true, 'Discharge': true, 'Temperature, water': true},
+ shortenedName: {"Gage height":"Ga","Discharge":"Dis","Temperature, water":"Temp"},
+ tempUnitF: true,
+ }, require('Storage').readJSON(FILE, true) || {});
+function fetchStartup() {
+ uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" +
+ settings.loc +
+ "')/Datastreams?$expand=Observations($orderby=phenomenonTime%20desc;$top=1;$select=result)&$select=unitOfMeasurement,description";
+ if (Bangle.http) {
+ Bangle.http(uri, {timeout:10000}).then(d => handleStartup(JSON.parse(d.resp).value));
+ }
+}
+function handleStartup(data) {
+ for (var key1 in data) {
+ desc = data[key1].description.split(" / ")[0];
+ if (settings.keys[desc]) {
+ if (data[key1].unitOfMeasurement.symbol === "degC" && settings.tempUnitF) {
+ symbol = "F";
+ result = (data[key1].Observations[0].result * 9 / 5) + 32;
+ } else {
+ symbol = data[key1].unitOfMeasurement.symbol;
+ result = data[key1].Observations[0].result;
+ }
+ dataStreams[desc] = JSON.parse(
+ '{"unit":"' +
+ symbol +
+ '","value":"' +
+ result +
+ '"}'
+ );
+ }
+ }
+ displayData(dataStreams);
+}
+
+function displayData(dataStreams) {
+ g.clear();
+ g.setFont("Vector",20);
+ g.setFontAlign(0,0);
+ string = "";
+ for (var key in dataStreams) {
+ unit = dataStreams[key].unit;
+ value = dataStreams[key].value;
+ if (settings.shortenedName[key]) {
+ name = settings.shortenedName[key];
+ } else {
+ name = key;
+ }
+ string += name+": "+value+" "+unit+"\n";
+ }
+ var date = new Date();
+ var hours = date.getHours();
+ var minutes = date.getMinutes();
+ var seconds = date.getSeconds();
+ // Format the time as a string
+ var timeString = hours.toString().padStart(2, "0") + ":" +
+ minutes.toString().padStart(2, "0") + ":" +
+ seconds.toString().padStart(2,"0");
+ E.showMessage(string,timeString);
+}
+function handleButton() {
+ switch(nPress) {
+ case 1:
+ fetchStartup();
+ break;
+ case 2:
+ Bangle.showLauncher();
+ break;
+ default:
+ Bangle.buzz(50);
+ }
+ nPress=0;
+}
+
+fetchStartup();
+
+nPress = 0;
+tPress = 0;
+
+setWatch(() => {
+ nPress++;
+ clearTimeout(tPress);
+ tPress = setTimeout(() => {handleButton();}, 500);
+ }, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat: true, edge: "rising"});
diff --git a/apps/usgs/app.png b/apps/usgs/app.png
new file mode 100644
index 000000000..b5f46c9a9
Binary files /dev/null and b/apps/usgs/app.png differ
diff --git a/apps/usgs/metadata.json b/apps/usgs/metadata.json
new file mode 100644
index 000000000..6140c59b9
--- /dev/null
+++ b/apps/usgs/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "usgs",
+ "name": "USGS Data fetching app",
+ "shortName":"USGS",
+ "version":"0.02",
+ "description": "App that fetches [USGS water data](https://maps.waterdata.usgs.gov/) for a configurable location (requires connection to Android phone)",
+ "icon": "app.png",
+ "tags": "outdoors,exercise,http",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"usgs.app.js","url":"app.js"},
+ {"name":"usgs.settings.js","url":"settings.js"},
+ {"name":"usgs.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [{"name":"usgs.json"}]
+}
diff --git a/apps/usgs/settings.js b/apps/usgs/settings.js
new file mode 100644
index 000000000..1ddde4c60
--- /dev/null
+++ b/apps/usgs/settings.js
@@ -0,0 +1,81 @@
+(function(back) {
+ var FILE = "usgs.json";
+ var settings = Object.assign({
+ loc: '03272100',
+ keys: {'Gage height': true, 'Discharge': true, 'Temperature, water': true},
+ shortenedName: {"Gage height":"Ga","Discharge":"Dis","Temperature, water":"Temp"},
+ tempUnitF: true,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ var submenu = {
+ "" : {
+ "title" : "DataStreams"
+ },
+ "< Back" : function() { E.showMenu(menu); },
+ };
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+function popSubMenu() {
+ data = getDataStreams();
+}
+function popSubMenuData(data) {
+ console.log(data);
+ Object.keys(data).forEach(function(k){
+ var name = data[k].description.split(" / ")[0];
+ console.log(name);
+ if (!settings.keys[name]) {
+ // Setting doesn't exist, so we assume it's false
+ settings.keys[name] = false;
+ }
+ submenu[name]= {value:settings.keys[name], onchange: v => {
+ settings.keys[name] = v;
+ writeSettings();
+ }};
+ });
+ E.showMenu(submenu);
+ }
+ var menu = {
+ "" : { "title" : "USGS" },
+ "< Back" : () => back(),
+ 'Temp unit': {
+ value: !!settings.tempUnitF, // !! converts undefined to false
+ format: v => v?"F":"C",
+ onchange: v => {
+ settings.tempUnitF = v;
+ writeSettings();
+ }
+ },
+ 'Location': {
+ value: settings.loc,
+ onchange: () => {
+ setTimeout(() => {
+ keyboard.input({text:settings.loc}).then(result => {
+ settings.loc = result;
+ writeSettings();
+ });
+ }, 100);
+ }
+ },
+ "DataStreams" : function() { popSubMenu();},
+ };
+
+ var keyboard = "textinput";
+ try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
+ if (!keyboard) delete menu.Location;
+
+
+function getDataStreams() {
+ uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" +
+ settings.loc +
+ "')/Datastreams?$select=description";
+ if (Bangle.http) {
+ Bangle.http(uri, {timeout:10000}).then(d => {popSubMenuData(JSON.parse(d.resp).value);});
+ }
+}
+
+ // Show the menu
+ E.showMenu(menu);
+});
diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog
index 279fa2438..7d6707467 100644
--- a/apps/wid_edit/ChangeLog
+++ b/apps/wid_edit/ChangeLog
@@ -3,3 +3,5 @@
Change back entry to menu option
Allow changing widgets into all areas, including bottom widget bar
0.03: Fix editing widgets whose draw method takes the widget
+0.04: Remove double-sort
+0.05: Restore alphabetical sort
diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js
index 3cb545a34..fe259f97f 100644
--- a/apps/wid_edit/boot.js
+++ b/apps/wid_edit/boot.js
@@ -14,7 +14,7 @@ Bangle.loadWidgets = (o => ()=>{
const W = global.WIDGETS;
global.WIDGETS = {};
Object.keys(W)
- .sort()
+ .sort() // sort alphabetically. the next sort is stable and preserves this if sortorder matches
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
.forEach(k => global.WIDGETS[k] = W[k]);
})(Bangle.loadWidgets);
diff --git a/apps/wid_edit/metadata.json b/apps/wid_edit/metadata.json
index e80e45d45..b89640333 100644
--- a/apps/wid_edit/metadata.json
+++ b/apps/wid_edit/metadata.json
@@ -1,6 +1,6 @@
{
"id": "wid_edit",
- "version": "0.03",
+ "version": "0.05",
"name": "Widget Editor",
"icon": "icon.png",
"description": "Customize widget locations",
diff --git a/apps/wid_edit/settings.js b/apps/wid_edit/settings.js
index be09923f2..a632850d6 100644
--- a/apps/wid_edit/settings.js
+++ b/apps/wid_edit/settings.js
@@ -27,7 +27,7 @@
let W = global.WIDGETS;
global.WIDGETS = {};
Object.keys(W)
- .sort()
+ .sort() // see comment in boot.js
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
.forEach(k => {global.WIDGETS[k] = W[k];});
Bangle.drawWidgets();
diff --git a/apps/widanclk/ChangeLog b/apps/widanclk/ChangeLog
index 337288ad2..5a3ba7b14 100644
--- a/apps/widanclk/ChangeLog
+++ b/apps/widanclk/ChangeLog
@@ -1,2 +1,3 @@
0.01: New app
0.02: Clear between redraws
+0.03: Add todays date behind the clock hands.
diff --git a/apps/widanclk/metadata.json b/apps/widanclk/metadata.json
index cd9347601..39e83a8fe 100644
--- a/apps/widanclk/metadata.json
+++ b/apps/widanclk/metadata.json
@@ -1,8 +1,8 @@
{
"id": "widanclk",
"name": "Analog clock widget",
- "version": "0.02",
- "description": "A simple analog clock widget that appears when not showing a fullscreen clock",
+ "version": "0.03",
+ "description": "A simple analog clock widget that appears when not showing a fullscreen clock. Todays date sits behind the clock hands.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,clock",
diff --git a/apps/widanclk/widget.js b/apps/widanclk/widget.js
index c58f56459..da667d29b 100644
--- a/apps/widanclk/widget.js
+++ b/apps/widanclk/widget.js
@@ -9,10 +9,15 @@ WIDGETS["wdanclk"]={area:"tl",width:Bangle.CLOCK?0:24,draw:function() {
if (!this.width) return; // if size not right, return
g.reset();
let d = new Date();
+ let dd = d.getDate();
let x=this.x+12, y=this.y+12,
ah = (d.getHours()+d.getMinutes()/60)*Math.PI/6,
am = d.getMinutes()*Math.PI/30;
g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23).
+ setFont("Vector:16").
+ setColor(g.theme.bgH).
+ drawString(dd,this.x+4+10*(dd<10),this.y+5,true).
+ setColor(g.theme.fg).
drawCircle(x, y, 11).
drawLine(x,y, x+Math.sin(ah)*7, y-Math.cos(ah)*7).
drawLine(x,y, x+Math.sin(am)*9, y-Math.cos(am)*9);
diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog
index 3b2ae75c4..e9adf252d 100644
--- a/apps/widbaroalarm/ChangeLog
+++ b/apps/widbaroalarm/ChangeLog
@@ -8,3 +8,4 @@
Only use valid pressure values
0.06: Fix exception
0.07: Ensure barometer gets turned off after a few readings (isBarometerOn broken in 2v16)
+0.08: Compatibility with hideable Widgets
diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json
index 0a6ddd71e..ba0c02a31 100644
--- a/apps/widbaroalarm/metadata.json
+++ b/apps/widbaroalarm/metadata.json
@@ -2,7 +2,7 @@
"id": "widbaroalarm",
"name": "Barometer Alarm Widget",
"shortName": "Barometer Alarm",
- "version": "0.07",
+ "version": "0.08",
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
"icon": "widget.png",
"type": "widget",
diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js
index d65a1c09c..c7ca0eda2 100644
--- a/apps/widbaroalarm/widget.js
+++ b/apps/widbaroalarm/widget.js
@@ -226,7 +226,7 @@ function barometerPressureHandler(e) {
medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9);
if (medianPressure > 0) {
turnOff();
- draw();
+ WIDGETS.baroalarm.draw();
handlePressureValue(medianPressure);
}
}
@@ -253,13 +253,6 @@ function turnOff() {
}
function draw() {
- if (global.WIDGETS != undefined && typeof global.WIDGETS === "object") {
- global.WIDGETS["baroalarm"] = {
- width : setting("show") ? 24 : 0,
- area : "tr",
- draw : draw
- };
- }
g.reset();
if (this.x == undefined || this.y != 0)
@@ -270,9 +263,6 @@ function draw() {
if (setting("show")) {
g.setFont("6x8", 1).setFontAlign(1, 0);
if (medianPressure == undefined) {
- // trigger a new check
- getPressureValue();
-
// lets load last value from log (if available)
if (history3.length > 0) {
medianPressure = history3[history3.length - 1]["p"];
@@ -297,6 +287,12 @@ function draw() {
}
}
+WIDGETS["baroalarm"] = {
+ width : setting("show") ? 24 : 0,
+ area : "tr",
+ draw : draw
+};
+
if (interval > 0) {
setInterval(getPressureValue, interval * 60000);
}
diff --git a/apps/widclkscrl/metadata.json b/apps/widclkscrl/metadata.json
new file mode 100644
index 000000000..81221cbe4
--- /dev/null
+++ b/apps/widclkscrl/metadata.json
@@ -0,0 +1,13 @@
+{
+ "id": "widclkscrl",
+ "name": "Scrolling clock widget",
+ "version": "0.01",
+ "description": "A widget that displays the current date & time after unlocking the watch when not showing a fullscreen clock. The information is scrolled by in a two digit field, so this widget is kept tight.",
+ "icon": "widget.png",
+ "type": "widget",
+ "tags": "widget",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"widclkscrl.wid.js","url":"widget.js"}
+ ]
+}
diff --git a/apps/widclkscrl/widget.js b/apps/widclkscrl/widget.js
new file mode 100644
index 000000000..292d291ac
--- /dev/null
+++ b/apps/widclkscrl/widget.js
@@ -0,0 +1,64 @@
+(() => {
+ const WIDTH = 14; // Width of the text, widget is +2 px wide
+ const CONTINOUS = false; // Go back & forward or stop after first scroll
+ require("FontTeletext5x9Ascii").add(Graphics);
+
+ function getDateText() {
+ const date = new Date();
+ const dateStr = require("locale").date(date, 1);
+ const timeStr = require("locale").time(date, 1);
+ return ` ${timeStr} ${dateStr} `;
+ }
+
+ WIDGETS["widclkscrl"]={
+ area: "tl",
+ width: 0, // default hide
+ pos: 10,
+ dir: -1,
+ eventHandlerSet: false,
+ draw: function() {
+ if (!this.eventHandlerSet) {
+ Bangle.on('lock', (on) => {
+ this.run(!on);
+ });
+ this.eventHandlerSet = true;
+ }
+ if (this.text) {
+ const buf = Graphics.createArrayBuffer(WIDTH,24,1,{msb:true}).setFont("Teletext5x9Ascii:1x2").setFontAlign(-1, 0);
+ buf.drawString(this.text, this.pos, 12);
+
+ if (this.dir === 1 && this.pos === 0 || this.dir === -1 && Math.abs(this.pos) === buf.stringWidth(this.text) - WIDTH) {
+ if (CONTINOUS) {
+ this.dir*=-1;
+ this.text = getDateText();
+ } else {
+ this.pos = 0;
+ this.run(false);
+ return;
+ }
+ }
+ this.pos+=this.dir;
+
+ g.reset().drawImage({
+ width:buf.getWidth(), height:buf.getHeight(),
+ bpp:buf.getBPP(),
+ buffer:buf.buffer
+ }, this.x+1, this.y);
+ }
+ },
+ run: function (on) {
+ if (!Bangle.CLOCK && on && !this.interval) {
+ this.text = getDateText();
+ this.interval = setInterval(() => {
+ this.draw();
+ }, 100);
+ this.width = WIDTH+2; Bangle.drawWidgets();
+ } else if (!on && this.interval) {
+ clearInterval(this.interval);
+ delete this.interval;
+ delete this.text;
+ this.width = 0; Bangle.drawWidgets();
+ }
+ },
+ };
+})();
diff --git a/apps/widclkscrl/widget.png b/apps/widclkscrl/widget.png
new file mode 100644
index 000000000..6b4bc9774
Binary files /dev/null and b/apps/widclkscrl/widget.png differ
diff --git a/apps/widshipbell/ChangeLog b/apps/widshipbell/ChangeLog
new file mode 100644
index 000000000..a26ed96db
--- /dev/null
+++ b/apps/widshipbell/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called
diff --git a/apps/widshipbell/metadata.json b/apps/widshipbell/metadata.json
index c130b04ee..1c4a7613e 100644
--- a/apps/widshipbell/metadata.json
+++ b/apps/widshipbell/metadata.json
@@ -2,7 +2,7 @@
"id": "widshipbell",
"name": "Ship's bell Widget",
"shortName": "Ship's bell",
- "version": "0.01",
+ "version": "0.02",
"description": "A widget that buzzes according to a nautical bell, one strike at 04:30, two strikes at 05:00, up to eight strikes at 08:00 and so on.",
"icon": "widget.png",
"type": "widget",
diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js
index 4bd7fda2a..bd1e1f564 100755
--- a/bin/firmwaremaker_c.js
+++ b/bin/firmwaremaker_c.js
@@ -103,7 +103,15 @@ Promise.all(APPS.map(appid => {
var app = apploader.apps.find(a => a.id==appid);
if (!app) throw new Error(`App ${appid} not found`);
return apploader.getAppFiles(app).then(files => {
- appfiles = appfiles.concat(files);
+ files.forEach(f => {
+ var existing = appfiles.find(a=> a.name==f.name);
+ if (existing) {
+ if (existing.content !== f.content)
+ throw new Error(`Duplicate file ${f.name} is different`)
+ } else {
+ appfiles.push(f);
+ }
+ });
});
})).then(() => {
// work out what goes in storage
diff --git a/core b/core
index 92769acd6..431a3fb74 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 92769acd60bc31548ff7c635128d4e7ef02b7325
+Subproject commit 431a3fb743da5c370729ab748cb2c177e70a345b
diff --git a/css/main.css b/css/main.css
index 2ea2ab40d..e8330b868 100644
--- a/css/main.css
+++ b/css/main.css
@@ -54,7 +54,7 @@ a.btn.btn-link.dropdown-toggle {
#toastcontainer {
position:fixed;
bottom:8px;left:0px;right:0px;
- z-index: 100;
+ z-index: 500;
}
.hero {
padding-bottom: 1rem;
diff --git a/index.html b/index.html
index 30f660717..6c3809343 100644
--- a/index.html
+++ b/index.html
@@ -135,15 +135,17 @@
Utilities
+
+
+
+
+
-
-
-
Settings
@@ -175,6 +177,10 @@
Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this will break many apps)
+
diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts
index ecf509f70..eda8a9a81 100644
--- a/typescript/types/main.d.ts
+++ b/typescript/types/main.d.ts
@@ -180,6 +180,7 @@ type SetUIArg = Mode | {
mode: Mode,
back?: () => void,
remove?: () => void,
+ redraw?: () => void,
};
type NRFFilters = {
@@ -841,6 +842,16 @@ declare class NRF {
*/
static on(event: "disconnect", callback: (reason: number) => void): void;
+ /**
+ * Called when the Nordic Bluetooth stack (softdevice) generates an error. In pretty
+ * much all cases an Exception will also have been thrown.
+ * @param {string} event - The event to listen to.
+ * @param {(msg: any) => void} callback - A function that is executed when the event occurs. Its arguments are:
+ * * `msg` The error string
+ * @url http://www.espruino.com/Reference#l_NRF_error
+ */
+ static on(event: "error", callback: (msg: any) => void): void;
+
/**
* Contains updates on the security of the current Bluetooth link.
* See Nordic's `ble_gap_evt_auth_status_t` structure for more information.
@@ -964,6 +975,17 @@ declare class NRF {
*/
static restart(callback?: any): void;
+ /**
+ * Delete all data stored for all peers (bonding data used for secure connections). This cannot be done
+ * while a connection is active, so if there is a connection it will be postponed until everything is disconnected
+ * (which can be done by calling `NRF.disconnect()` and waiting).
+ * Booting your device while holding all buttons down together should also have the same effect.
+ *
+ * @param {any} [callback] - [optional] A function to be called while the softdevice is uninitialised. Use with caution - accessing console/bluetooth will almost certainly result in a crash.
+ * @url http://www.espruino.com/Reference#l_NRF_eraseBonds
+ */
+ static eraseBonds(callback?: any): void;
+
/**
* Get this device's default Bluetooth MAC address.
* For Puck.js, the last 5 characters of this (e.g. `ee:ff`) are used in the
@@ -1222,8 +1244,9 @@ declare class NRF {
* hid : new Uint8Array(...), // optional, default is undefined. Enable BLE HID support
* uart : true, // optional, default is true. Enable BLE UART support
* advertise: [ '180D' ] // optional, list of service UUIDs to advertise
- * ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications
- * ams : true // optional, Bangle.js-only, enable Apple AMS support for media control
+ * ancs : true, // optional, Bangle.js-only, enable Apple ANCS support for notifications (see `NRF.ancs*`)
+ * ams : true // optional, Bangle.js-only, enable Apple AMS support for media control (see `NRF.ams*`)
+ * cts : true // optional, Bangle.js-only, enable Apple Current Time Service support (see `NRF.ctsGetTime`)
* });
* ```
* To enable BLE HID, you must set `hid` to an array which is the BLE report
@@ -1747,6 +1770,55 @@ declare class NRF {
*/
static amsCommand(id: any): void;
+ /**
+ * Check if Apple Current Time Service (CTS) is currently active on the BLE connection
+ *
+ * @returns {boolean} True if Apple Current Time Service (CTS) has been initialised and is active
+ * @url http://www.espruino.com/Reference#l_NRF_ctsIsActive
+ */
+ static ctsIsActive(): boolean;
+
+ /**
+ * Returns time information from the Current Time Service
+ * (if requested with `NRF.ctsGetTime` and is activated by calling `NRF.setServices(..., {..., cts:true})`)
+ * ```
+ * {
+ * date : // Date object with the current date
+ * day : // if known, 0=sun,1=mon (matches JS `Date`)
+ * reason : [ // reason for the date change
+ * "external", // External time change
+ * "manual", // Manual update
+ * "timezone", // Timezone changed
+ * "DST", // Daylight savings
+ * ]
+ * timezone // if LTI characteristic exists, this is the timezone
+ * dst // if LTI characteristic exists, this is the dst adjustment
+ * }
+ * ```
+ * For instance this can be used as follows to update Espruino's time:
+ * ```
+ * E.on('CTS',e=>{
+ * setTime(e.date.getTime()/1000);
+ * });
+ * NRF.ctsGetTime(); // also returns a promise with CTS info
+ * ```
+ * @param {string} event - The event to listen to.
+ * @param {(info: any) => void} callback - A function that is executed when the event occurs. Its arguments are:
+ * * `info` An object (see below)
+ * @url http://www.espruino.com/Reference#l_NRF_CTS
+ */
+ static on(event: "CTS", callback: (info: any) => void): void;
+
+ /**
+ * Read the time from CTS - creates an `NRF.on('CTS', ...)` event as well
+ * ```
+ * NRF.ctsGetTime(); // also returns a promise
+ * ```
+ * @returns {any} A `Promise` that is resolved (or rejected) when time is received
+ * @url http://www.espruino.com/Reference#l_NRF_ctsGetTime
+ */
+ static ctsGetTime(): Promise;
+
/**
* Search for available devices matching the given filters. Since we have no UI
* here, Espruino will pick the FIRST device it finds, or it'll call `catch`.
@@ -3538,6 +3610,16 @@ declare class Bangle {
*/
static on(event: "lcdPower", callback: (on: boolean) => void): void;
+ /**
+ * Has the backlight been turned on or off? Can be used to stop tasks that are no
+ * longer useful if want to see in sun screen only. Also see `Bangle.isBacklightOn()`
+ * @param {string} event - The event to listen to.
+ * @param {(on: boolean) => void} callback - A function that is executed when the event occurs. Its arguments are:
+ * * `on` `true` if backlight is on
+ * @url http://www.espruino.com/Reference#l_Bangle_backlight
+ */
+ static on(event: "backlight", callback: (on: boolean) => void): void;
+
/**
* Has the screen been locked? Also see `Bangle.isLocked()`
* @param {string} event - The event to listen to.
@@ -3664,6 +3746,26 @@ declare class Bangle {
*/
static on(event: "midnight", callback: () => void): void;
+ /**
+ * This function can be used to turn Bangle.js's LCD backlight off or on.
+ * This function resets the Bangle's 'activity timer' (like pressing a button or
+ * the screen would) so after a time period of inactivity set by
+ * `Bangle.setOptions({backlightTimeout: X});` the backlight will turn off.
+ * If you want to keep the backlight on permanently (until apps are changed) you can
+ * do:
+ * ```
+ * Bangle.setOptions({backlightTimeout: 0}) // turn off the timeout
+ * Bangle.setBacklight(1); // keep screen on
+ * ```
+ * Of course, the backlight depends on `Bangle.setLCDPower` too, so any lcdPowerTimeout/setLCDTimeout will
+ * also turn the backlight off. The use case is when you require the backlight timeout
+ * to be shorter than the power timeout.
+ *
+ * @param {boolean} isOn - True if the LCD backlight should be on, false if not
+ * @url http://www.espruino.com/Reference#l_Bangle_setBacklight
+ */
+ static setBacklight(isOn: boolean): void;
+
/**
* This function can be used to turn Bangle.js's LCD off or on.
* This function resets the Bangle's 'activity timer' (like pressing a button or
@@ -3805,7 +3907,7 @@ declare class Bangle {
static setLCDTimeout(isOn: number): void;
/**
- * Set how often the watch should poll for new acceleration/gyro data and kick the
+ * Set how often the watch should poll its sensors (accel/hr/mag) for new data and kick the
* Watchdog timer. It isn't recommended that you make this interval much larger
* than 1000ms, but values up to 4000ms are allowed.
* Calling this will set `Bangle.setOptions({powerSave: false})` - disabling the
@@ -3890,6 +3992,13 @@ declare class Bangle {
*/
static isLCDOn(): boolean;
+ /**
+ * Also see the `Bangle.backlight` event
+ * @returns {boolean} Is the backlight on or not?
+ * @url http://www.espruino.com/Reference#l_Bangle_isBacklightOn
+ */
+ static isBacklightOn(): boolean;
+
/**
* This function can be used to lock or unlock Bangle.js (e.g. whether buttons and
* touchscreen work or not)
@@ -4361,7 +4470,8 @@ declare class Bangle {
* (function() {
* var sui = Bangle.setUI;
* Bangle.setUI = function(mode, cb) {
- * if (mode!="clock") return sui(mode,cb);
+ * var m = ("object"==typeof mode) ? mode.mode : mode;
+ * if (m!="clock") return sui(mode,cb);
* sui(); // clear
* Bangle.CLOCK=1;
* Bangle.swipeHandler = Bangle.showLauncher;
@@ -4376,10 +4486,11 @@ declare class Bangle {
* mode : "custom",
* back : function() {}, // optional - add a 'back' icon in top-left widget area and call this function when it is pressed , also call it when the hardware button is clicked (does not override btn if defined)
* remove : function() {}, // optional - add a handler for when the UI should be removed (eg stop any intervals/timers here)
- * touch : function(n,e) {}, // optional - handler for 'touch' events
- * swipe : function(dir) {}, // optional - handler for 'swipe' events
- * drag : function(e) {}, // optional - handler for 'drag' events (Bangle.js 2 only)
- * btn : function(n) {}, // optional - handler for 'button' events (n==1 on Bangle.js 2, n==1/2/3 depending on button for Bangle.js 1)
+ * redraw : function() {}, // optional - add a handler to redraw the UI. Not needed but it can allow widgets/etc to provide other functionality that requires the screen to be redrawn
+ * touch : function(n,e) {}, // optional - (mode:custom only) handler for 'touch' events
+ * swipe : function(dir) {}, // optional - (mode:custom only) handler for 'swipe' events
+ * drag : function(e) {}, // optional - (mode:custom only) handler for 'drag' events (Bangle.js 2 only)
+ * btn : function(n) {}, // optional - (mode:custom only) handler for 'button' events (n==1 on Bangle.js 2, n==1/2/3 depending on button for Bangle.js 1)
* clock : 0 // optional - if set the behavior of 'clock' mode is added (does not override btn if defined)
* });
* ```
@@ -4387,7 +4498,7 @@ declare class Bangle {
* may choose to just call the `remove` function and then load a new app without resetting Bangle.js.
* As a result, **if you specify 'remove' you should make sure you test that after calling `Bangle.setUI()`
* without arguments your app is completely unloaded**, otherwise you may end up with memory leaks or
- * other issues when switching apps.
+ * other issues when switching apps. Please see http://www.espruino.com/Bangle.js+Fast+Load for more details on this.
*
* @param {any} type - The type of UI input: 'updown', 'leftright', 'clock', 'clockupdown' or undefined to cancel. Can also be an object (see below)
* @param {any} callback - A function with one argument which is the direction
@@ -6872,6 +6983,7 @@ interface DateConstructor {
new(): Date;
new(value: number | string): Date;
new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date;
+ (arg?: any): string;
}
interface Date {
@@ -8166,7 +8278,9 @@ declare class E {
static toFlatString(...args: any[]): string | undefined;
/**
- * By default, strings in Espruino are standard 8 bit binary strings.
+ * By default, strings in Espruino are standard 8 bit binary strings
+ * unless they contain Unicode chars or a `\u####` escape code
+ * that doesn't map to the range 0..255.
* However calling E.asUTF8 will convert one of those strings to
* UTF8.
* ```
@@ -8177,6 +8291,7 @@ declare class E {
* u.length // 1
* u[0] // hamburger emoji
* ```
+ * **NOTE:** UTF8 is currently only available on Bangle.js devices
*
* @param {any} str - The string to turn into a UTF8 Unicode String
* @returns {any} A String
@@ -8184,6 +8299,34 @@ declare class E {
*/
static asUTF8(str: any): string;
+ /**
+ * Given a UTF8 String (see `E.asUTF8`) this returns the underlying representation
+ * of that String.
+ * ```
+ * E.fromUTF8("\u03C0") == "\xCF\x80"
+ * ```
+ * **NOTE:** UTF8 is currently only available on Bangle.js devices
+ *
+ * @param {any} str - The string to check
+ * @returns {any} A String
+ * @url http://www.espruino.com/Reference#l_E_fromUTF8
+ */
+ static fromUTF8(str: any): string;
+
+ /**
+ * By default, strings in Espruino are standard 8 bit binary strings
+ * unless they contain Unicode chars or a `\u####` escape code
+ * that doesn't map to the range 0..255.
+ * This checks if a String is being treated by Espruino as a UTF8 String
+ * See `E.asUTF8` to convert to a UTF8 String
+ * **NOTE:** UTF8 is currently only available on Bangle.js devices
+ *
+ * @param {any} str - The string to check
+ * @returns {boolean} True if the given String is treated as UTF8 by Espruino
+ * @url http://www.espruino.com/Reference#l_E_isUTF8
+ */
+ static isUTF8(str: any): boolean;
+
/**
* This creates a Uint8Array from the given arguments. These are handled as
* follows:
@@ -9120,6 +9263,26 @@ interface Object {
*/
on(event: any, listener: any): void;
+ /**
+ * Register an event listener for this object, for instance `Serial1.addListener('data', function(d) {...})`.
+ * An alias for `Object.on`
+ *
+ * @param {any} event - The name of the event, for instance 'data'
+ * @param {any} listener - The listener to call when this event is received
+ * @url http://www.espruino.com/Reference#l_Object_addListener
+ */
+ addListener(event: any, listener: any): void;
+
+ /**
+ * Register an event listener for this object, for instance `Serial1.addListener('data', function(d) {...})`.
+ * An alias for `Object.on`
+ *
+ * @param {any} event - The name of the event, for instance 'data'
+ * @param {any} listener - The listener to call when this event is received
+ * @url http://www.espruino.com/Reference#l_Object_prependListener
+ */
+ prependListener(event: any, listener: any): void;
+
/**
* Call any event listeners that were added to this object with `Object.on`, for
* instance `obj.emit('data', 'Foo')`.
@@ -9807,21 +9970,22 @@ declare const Promise: PromiseConstructor
* This means that while `StorageFile` files exist in the same area as those from
* `Storage`, they should be read using `Storage.open` (and not `Storage.read`).
* ```
- * f = s.open("foobar","w");
+ * f = require("Storage").open("foobar","w");
* f.write("Hell");
* f.write("o World\n");
* f.write("Hello\n");
* f.write("World 2\n");
+ * f.write("Hello World 3\n");
* // there's no need to call 'close'
* // then
- * f = s.open("foobar","r");
+ * f = require("Storage").open("foobar","r");
* f.read(13) // "Hello World\nH"
* f.read(13) // "ello\nWorld 2\n"
* f.read(13) // "Hello World 3"
* f.read(13) // "\n"
* f.read(13) // undefined
* // or
- * f = s.open("foobar","r");
+ * f = require("Storage").open("foobar","r");
* f.readLine() // "Hello World\n"
* f.readLine() // "Hello\n"
* f.readLine() // "World 2\n"
@@ -10255,6 +10419,7 @@ interface StringConstructor {
* @url http://www.espruino.com/Reference#l_String_String
*/
new(...str: any[]): any;
+ (arg?: any): string;
}
interface String {
@@ -10499,7 +10664,8 @@ interface RegExpConstructor {
* @returns {any} A RegExp object
* @url http://www.espruino.com/Reference#l_RegExp_RegExp
*/
- new(regex: any, flags: any): RegExp;
+ new(...value: any[]): RegExp;
+ (value: any): RegExp;
}
interface RegExp {
@@ -10588,7 +10754,8 @@ interface NumberConstructor {
* @returns {any} A Number object
* @url http://www.espruino.com/Reference#l_Number_Number
*/
- new(...value: any[]): any;
+ new(...value: any[]): Number;
+ (value: any): number;
}
interface Number {
@@ -10728,7 +10895,8 @@ interface BooleanConstructor {
* @returns {boolean} A Boolean object
* @url http://www.espruino.com/Reference#l_Boolean_Boolean
*/
- new(value: any): boolean;
+ new(...value: any[]): Number;
+ (value: any): boolean;
}
interface Boolean {
@@ -13494,7 +13662,7 @@ declare module "Storage" {
* List all files in the flash storage area matching the specified regex (ignores
* StorageFiles), and then hash their filenames *and* file locations.
* Identical files may have different hashes (e.g. if Storage is compacted and the
- * file moves) but the changes of different files having the same hash are
+ * file moves) but the chances of different files having the same hash are
* extremely small.
* ```
* // Hash files