Merge branch 'espruino:master' into development

master
xxDUxx 2023-09-05 08:23:43 +02:00 committed by GitHub
commit 901f122a05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
275 changed files with 10510 additions and 1254 deletions

View File

@ -135,15 +135,17 @@
<h3>Utilities</h3>
<p>
<button class="btn tooltip" id="settime" data-tooltip="Set the Bangle's time to your Browser's time">Set Bangle.js Time</button>
<button class="btn tooltip" id="screenshot" data-tooltip="Create screenshot">Screenshot</button>
<button class="btn tooltip" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button>
</p><p>
<button class="btn tooltip" id="removeall" data-tooltip="Delete everything, leave it blank">Remove all Apps</button>
<button class="btn tooltip" id="reinstallall" data-tooltip="Re-install every app, leave all data">Reinstall apps</button>
<button class="btn tooltip" id="installdefault" data-tooltip="Delete everything, install default apps">Install default apps</button>
<button class="btn tooltip" id="installfavourite" data-tooltip="Delete everything, install your favourites">Install favourite apps</button>
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
</p><p>
<button class="btn tooltip" id="newGithubIssue" data-tooltip="Create a new issue on GitHub">New issue on GitHub</button>
<button class="btn tooltip" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button>
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
<button class="btn tooltip" id="webideremote" data-tooltip="Enable the Web IDE remote server">Web IDE Remote</button>
</p>
<h3>Settings</h3>
@ -172,6 +174,10 @@
<input type="checkbox" id="settings-minify">
<i class="form-icon"></i> Minify apps before upload (⚠DANGER⚠: Not recommended. Uploads smaller, faster apps but this <b>will</b> break many apps)
</label>
<label class="form-switch">
<input type="checkbox" id="settings-alwaysAllowUpdate">
<i class="form-icon"></i> Always allow to reinstall apps in place regardless of the version
</label>
<button class="btn" id="defaultsettings">Reset to default App Loader settings</button>
</details>
</div>

View File

@ -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

View File

@ -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();
}

View File

@ -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",

View File

@ -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
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

View File

@ -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 => {

View File

@ -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",

View File

@ -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

View File

@ -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;

View File

@ -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"},

1
apps/bblobface/ChangeLog Normal file
View File

@ -0,0 +1 @@
1.00: Initial release of Bangle Blobs Clock!

35
apps/bblobface/README.md Normal file
View File

@ -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.
![](screenshot1.png)
![](screenshot2.png)
## Clock Features
- Hour and minute
- Seconds (only while the screen is unlocked to save power)
- Month, day, and day of week
- Battery percentage. Blue while charging, red when low, green otherwise.
- Respects your 24-hour/12-hour time setting in Locale
- Press the pause button to access your Widgets
- Supports Fast Loading
## The Game
This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA.
Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall.
If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs!
- Drag left and right to move the pair
- Tap the left or right half of the screen to rotate the pair
- Swipe down to place the pair
## More Info
If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing
## 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.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA="))

768
apps/bblobface/app.js Normal file
View File

@ -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();
}

BIN
apps/bblobface/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

View File

@ -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}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -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

View File

@ -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')

View File

@ -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"}],

View File

@ -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

View File

@ -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.

View File

@ -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",

View File

@ -1 +1,2 @@
0.01: Initial release.
0.02: Handle the case where other apps have set bleAdvert to an array

View File

@ -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);
}

View File

@ -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",

View File

@ -1 +1,2 @@
0.01: New app!
0.02: Advertise accelerometer data and sensor location

View File

@ -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,
});
}
}

View File

@ -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
// <see encode function>
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<T> = {
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<boolean> = {
mag: false,
};
const idToName: BtAdvMap<string, true> = {
acc: "Acceleration",
const idToName: BtAdvMap<string> = {
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<CompassData> = (data: CompassData) => {
};
encodeMag.maxLen = 6;
const encodeAcc: LenFunc<AccelData> = (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<number> = 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,
}
);
}
}

View File

@ -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",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Handle the case where other apps have set bleAdvert to an array

View File

@ -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);
}

View File

@ -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",

View File

@ -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

View File

@ -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!

View File

@ -28,11 +28,16 @@ function readFile(input) {
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
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);

View File

@ -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"}],

View File

@ -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();
})

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Handle missing settings (e.g. first-install)

View File

@ -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) => {

View File

@ -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",

View File

@ -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
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

View File

@ -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);
}
});

View File

@ -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",

View File

@ -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.

View File

@ -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.
});
};

View File

@ -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",

View File

@ -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.

View File

@ -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<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
}
// drag on top rectangle for number or punctuation
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (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<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
}
// drag on top rectangle for number or punctuation
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (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;

View File

@ -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",

3
apps/edgeclk/ChangeLog Normal file
View File

@ -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.

31
apps/edgeclk/README.md Normal file
View File

@ -0,0 +1,31 @@
# Edge Clock
![Screenshot](screenshot.png)
![Screenshot](screenshot2.png)
![Screenshot](screenshot3.png)
![Screenshot](screenshot4.png)
Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability.
It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging.
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)

1
apps/edgeclk/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgMgBAoFEuADCgP8sAFD/wLE/wXDCIIjFAAv/ABQRF5fegEPgfe5UbgEJgVS5ebBYMyr36BYdC7YXEGq4AFj8f/ED8f+ApHjAoMHjkA8HjxwFIgAFCC4IFJjk4AoodEAogXBAoI1BDoYFGL5Z3XmHv33whkfuAFE/Fgw0whuD/Fjz0wh/fuALCh/Y/Fv30wgOf7AFE"))

342
apps/edgeclk/app.js Normal file
View File

@ -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();
}

BIN
apps/edgeclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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"}]
}

BIN
apps/edgeclk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

125
apps/edgeclk/settings.js Normal file
View File

@ -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();
},
},
});
})

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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",

View File

@ -59,6 +59,13 @@
}
};
mainmenu['Detect settings changes'] = {
value: !!settings.detectSettingsChange,
onchange: v => {
writeSettings("detectSettingsChange",v);
}
};
return mainmenu;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:
![Screenshot](legend.png)
@ -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.
![Screenshot](heights.png)
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

View File

@ -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

File diff suppressed because one or more lines are too long

BIN
apps/gipy/heights.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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",

View File

@ -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;

View File

@ -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);
};

Binary file not shown.

View File

@ -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;

View File

@ -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();
}
},
});
});

BIN
apps/gipy/shot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -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
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

View File

@ -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)

21
apps/gpstrek/default.json Normal file
View File

@ -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
}

View File

@ -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"}
]
}

162
apps/gpstrek/settings.js Normal file
View File

@ -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();
})

View File

@ -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) {

View File

@ -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

View File

@ -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);
}

View File

@ -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;

View File

@ -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>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",
a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){var d=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){return Bangle.setHRMPower(0,"health")},6E4*a);if(1==a){var b=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){Bangle.setHRMPower(0,"health")},6E4)};setTimeout(b,2E5);setTimeout(b,4E5)}};Bangle.on("health",d);Bangle.on("HRM",function(b){90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,
"health")});90<Bangle.getHealthStatus().bpmConfidence||d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",function(a){function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0<a.steps&&l();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+
4*f,4);if("\u00ff\u00ff\u00ff\u00ff"!=e){print("HEALTH ERR: Already written!");return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\u00ff\u00ff\u00ff\u00ff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;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>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",
a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function d(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.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=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence||
d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{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&&0<a.steps&&m();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+4*f,4);if("\xff\xff\xff\xff"!=e){print("HEALTH ERR: Already written!");
return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;a=Object.assign({},a);a.movement/=8;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\xff\xff\xff\xff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;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)}})

View File

@ -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;

View File

@ -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}}}
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}}}

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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);

View File

@ -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)

View File

@ -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);

View File

@ -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" }],

Some files were not shown because too many files have changed in this diff Show More