Merge branch 'espruino:master' into Weather-Feels-Like-Updates

master
RKBoss6 2025-08-01 18:47:52 -04:00 committed by GitHub
commit 76c3801f89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
123 changed files with 1698 additions and 449 deletions

2
apps/backlite/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app! (settings, boot.js).
0.02: Fix settings defaulting brightness to 0

20
apps/backlite/README.md Normal file
View File

@ -0,0 +1,20 @@
# BackLite
### This app needs the latest settings app update (v 0.80), to ensure that setting the brightness to `0` does not default to `1`.
BackLite is an app which greatly conserves battery life by only turning the backlight on when you long press the button from a locked state.
Modern watches have a dedicated button to turn the backlight on, so as not to waste battery in an already light environment. This app recreates that functionality for the Bangle.js, which only has one button.
#### Warning: This app overwrites the LCD brightness setting in `Bangle.js LCD settings`. If it is changed, the app will basically lose functionality. It auto-fixes itself every boot, so if you change the brightness, just reboot :)
# Usage
When you unlock with a press of the button, or any other way you unlock the watch, the backlight will not turn on, as most of the time you are able to read it, due to the transreflective display on the Bangle.js 2.
If you press and hold the button to unlock the watch (for around half a second), the backlight will turn on for 5 seconds - just enough to see what you need to see. After that, it will turn off again.
Some apps like `Light Switch Widget` will prevent this app from working properly.
# Settings
`Brightness` - The LCD brightness when unlocked with a long press.
# Creator
RKBoss6
TODO: Add a setting for long press time, or light duration

36
apps/backlite/boot.js Normal file
View File

@ -0,0 +1,36 @@
{
let getSettings = function(){
return Object.assign({
// default values
brightness: 0.3,
}, require('Storage').readJSON("BackLite.settings.json", true) || {});
};
//Set LCD to zero every reboot
let s = require("Storage").readJSON("setting.json", 1) || {};
s.brightness = 0;
require("Storage").writeJSON("setting.json", s);
//remove large settings object from memory
delete s;
const longPressTime=400; //(ms)
Bangle.on('lock', function(isLocked) {
Bangle.setLCDBrightness(0);
if (!isLocked) {
// Just unlocked — give a short delay and check if BTN1 is still pressed
setTimeout(() => {
if (digitalRead(BTN1)) {
//set brightness until. locked.
Bangle.setLCDBrightness(getSettings().brightness);
} else {
Bangle.setLCDBrightness(0);
}
}, longPressTime); // Slight delay to allow unlock to settle
}
});
}

BIN
apps/backlite/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,17 @@
{
"id": "backlite",
"name": "BackLite",
"version": "0.02",
"description": "Conserves battery life by turning the backlight on only on a long press of the button from a locked state. **Requires the latest settings update (v0.80)**",
"icon": "icon.png",
"type": "bootloader",
"tags": "system",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"backlite.boot.js","url":"boot.js"},
{"name":"backlite.settings.js","url":"settings.js"}
],
"data": [{"name":"BackLite.settings.json"}]
}

25
apps/backlite/settings.js Normal file
View File

@ -0,0 +1,25 @@
(function(back) {
var FILE = "BackLite.settings.json";
var settings = require("Storage").readJSON(FILE, 1) || {};
if (!isFinite(settings.brightness)) settings.brightness = 0.3;
function writeSettings() {
require("Storage").writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "BackLite" },
"< Back": back, // fallback if run standalone
"Brightness": {
value: settings.brightness,
min: 0.1, max: 1,
step: 0.1,
onchange: v => {
settings.brightness = v;
writeSettings();
}
},
});
})

4
apps/bbreaker/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: It works somehow, early version for testers and feedback :)
0.02: Changed almost all code with Frederic version of Pong and adjusted to be a BrickBreaker!, still Alpha
0.03: Rewrote the whole thing to have less code and better graphics, now it works.
0.04: Rewrote part of the code to coupe with the flickering and added better logic to handle the graphics.

28
apps/bbreaker/README.md Normal file
View File

@ -0,0 +1,28 @@
# BrickBreaker
A simple BreakOut clone for the Banglejs
![Screenshot](bbreaker.png)
## Usage
![Screenshot](playing.png)
Buttons 1 and 3 to move the BrickBreaker!
Button 2 to pause and start the game again.
## Disclaimer
This game was created to learn JS and how to interact with Banglejs, meaning that it may not be perfect :).
Built with love with base on the tutorial: 2D breakout game using pure JavaScript
https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript
Started on 2020 but rewrote all in 2025 and this is the version I got without having issues with Memory Exhaustion.
And yes, for Bangle 1, old school!
## Creator
Israel Ochoa <isuraeru at gmail.com>

View File

@ -0,0 +1 @@
atob("MDCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQAAAAAAAAAAAAAAVQAAAAAAAAAAAAAFWUAAAAAAAAAAAABVuUAAAAAAAAAAAAVa/lAAAAAAAAAAABWv/lQAAAAAAAAAAVb/+VAAAAAAAAAABW//lUAAAAAAAAAABX/6VAAAAAAAAAAAAW/lUAAAAAAAAAAAAFpVAAAAAAAAAAAAAFVQAAAAAAAAAAAAABUAAAFVVVVQVVQAAAQAAAFVVVVQVVQAAAAAAAFv//5QW5QAAAAAAAG///9QW9QAAAAAAAG///9QW9QAAAAAAAFqqqpQWpQAAAAAAAFVVVVQVVQAAAAAAAFVVVVQVVQAAAAAAAAAAAAAAAAAAAABVVVVQFVVVVQAAAAFVVVVUVVVVVQAAAAFqqqqUWqqqpQAAAAFv//+UW///9QAAAAFv//+UW///9QAAAAFv//+UWv//5QAAAAFVVVVUVVVVVQAAAABVVVVUFVVVVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVVVUFVVVVQVVQABaqqqUFqqqpQWpQABv//+UF///9QW5QABv//+UG///9QW5QABv//+UG///9QW5QABaqqqUFqqqpQWlQABVVVVUFVVVVQVVQAAVVVVABVVVVABUAAAAAAAAAAAAAAAAAA==")

293
apps/bbreaker/app.js Normal file
View File

@ -0,0 +1,293 @@
(function () {
var BALL_RADIUS = 3;
var PADDLE_WIDTH = 26;
var PADDLE_HEIGHT = 6;
var BRICK_ROWS = 2;
var BRICK_HEIGHT = 8;
var BRICK_PADDING = 4;
var BRICK_OFFSET_TOP = 60;
var BRICK_OFFSET_LEFT = 2;
var SPEED_MULTIPLIER = 1.1;
var PADDLE_SPEED = 12;
var ball, paddle, interval;
var bricks = [];
var BRICK_WIDTH, BRICK_COLS;
var score = 0;
var level = 1;
var highScore = 0;
var paused = false;
var gameOver = false;
var lives = 3;
var paddleMove = 0;
var storage = require("Storage");
function loadHighScore() {
var saved = storage.readJSON("breakout_highscore.json", 1);
highScore = saved && saved.highScore ? saved.highScore : 0;
}
function saveHighScore() {
if (score > highScore) {
highScore = score;
storage.writeJSON("breakout_highscore.json", { highScore });
}
}
function initBricks() {
bricks = [];
for (var i = 0; i < BRICK_ROWS * BRICK_COLS; i++) {
bricks.push(1);
}
}
function initGame() {
var screenWidth = g.getWidth();
BRICK_COLS = Math.min(5, Math.floor((screenWidth - BRICK_OFFSET_LEFT + BRICK_PADDING) / (15 + BRICK_PADDING)));
BRICK_WIDTH = Math.floor((screenWidth - BRICK_OFFSET_LEFT - (BRICK_COLS - 1) * BRICK_PADDING) / BRICK_COLS);
ball = {
x: screenWidth / 2,
y: g.getHeight() - 40,
dx: (Math.random() > 0.5 ? 1 : -1) * 3,
dy: -3,
radius: BALL_RADIUS,
prevPos: null
};
paddle = {
x: screenWidth / 2 - PADDLE_WIDTH / 2,
y: g.getHeight() - 20,
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
prevPos: null
};
lives = 3;
score = 0;
level = 1;
gameOver = false;
paused = false;
loadHighScore();
initBricks();
}
function drawLives() {
var heartSize = 6;
var spacing = 2;
var startX = g.getWidth() - (lives * (heartSize + spacing)) - 2;
var y = 32;
g.setColor(1, 0, 0);
for (var i = 0; i < lives; i++) {
var x = startX + i * (heartSize + spacing);
g.fillPoly([x + 3, y, x + 6, y + 3, x + 3, y + 6, x, y + 3], true);
}
}
function drawBricks() {
g.setColor(0, 1, 0);
for (var i = 0; i < bricks.length; i++) {
if (bricks[i]) {
var c = i % BRICK_COLS;
var r = Math.floor(i / BRICK_COLS);
var brickX = BRICK_OFFSET_LEFT + c * (BRICK_WIDTH + BRICK_PADDING);
var brickY = BRICK_OFFSET_TOP + r * (BRICK_HEIGHT + BRICK_PADDING);
g.fillRect(brickX, brickY, brickX + BRICK_WIDTH - 1, brickY + BRICK_HEIGHT - 1);
}
}
}
function drawBall() {
g.setColor(1, 1, 1);
g.fillCircle(ball.x, ball.y, ball.radius);
g.setColor(0.7, 0.7, 0.7);
g.fillCircle(ball.x - 0.5, ball.y - 0.5, ball.radius - 1);
}
function drawPaddle() {
g.setColor(0, 1, 1);
g.fillRect(paddle.x, paddle.y, paddle.x + paddle.width - 1, paddle.y + paddle.height - 1);
}
function drawHUD() {
g.setColor(0, 0, 0).fillRect(0, 0, g.getWidth(), BRICK_OFFSET_TOP - 1);
g.setColor(1, 1, 1);
g.setFont("6x8", 1);
g.setFontAlign(-1, -1);
g.drawString("Score: " + score, 2, 22);
g.setFontAlign(0, -1);
g.drawString("High: " + highScore, g.getWidth() / 2, 22);
g.setFontAlign(1, -1);
g.drawString("Lvl: " + level, g.getWidth() - 2, 22);
drawLives();
if (paused) {
g.setFontAlign(0, 0);
g.drawString("PAUSED", g.getWidth() / 2, g.getHeight() / 2);
}
}
function draw() {
if (paddle.prevPos) {
g.setColor(0, 0, 0).fillRect(paddle.prevPos.x - 1, paddle.prevPos.y - 1, paddle.prevPos.x + paddle.width + 1, paddle.prevPos.y + paddle.height + 1);
}
if (ball.prevPos) {
g.setColor(0, 0, 0).fillCircle(ball.prevPos.x, ball.prevPos.y, ball.radius + 1);
}
drawHUD();
drawBall();
drawPaddle();
g.flip();
ball.prevPos = { x: ball.x, y: ball.y };
paddle.prevPos = { x: paddle.x, y: paddle.y, width: paddle.width, height: paddle.height };
}
function showGameOver() {
g.clear();
g.setFont("6x8", 2);
g.setFontAlign(0, 0);
g.setColor(1, 0, 0);
g.drawString("GAME OVER", g.getWidth() / 2, g.getHeight() / 2 - 20);
g.setFont("6x8", 1);
g.setColor(1, 1, 1);
g.drawString("Score: " + score, g.getWidth() / 2, g.getHeight() / 2);
g.drawString("High: " + highScore, g.getWidth() / 2, g.getHeight() / 2 + 12);
g.drawString("BTN2 = Restart", g.getWidth() / 2, g.getHeight() / 2 + 28);
g.flip();
}
function collisionDetection() {
for (var i = 0; i < bricks.length; i++) {
if (bricks[i]) {
var c = i % BRICK_COLS;
var r = Math.floor(i / BRICK_COLS);
var brickX = BRICK_OFFSET_LEFT + c * (BRICK_WIDTH + BRICK_PADDING);
var brickY = BRICK_OFFSET_TOP + r * (BRICK_HEIGHT + BRICK_PADDING);
if (ball.x + ball.radius > brickX && ball.x - ball.radius < brickX + BRICK_WIDTH && ball.y + ball.radius > brickY && ball.y - ball.radius < brickY + BRICK_HEIGHT) {
ball.dy = -ball.dy;
bricks[i] = 0;
score += 10;
g.setColor(0, 0, 0).fillRect(brickX, brickY, brickX + BRICK_WIDTH - 1, brickY + BRICK_HEIGHT - 1);
break;
}
}
}
}
function allBricksCleared() {
for (var i = 0; i < bricks.length; i++) {
if (bricks[i]) return false;
}
return true;
}
function resetAfterLifeLost() {
clearInterval(interval);
interval = undefined;
ball.x = g.getWidth() / 2;
ball.y = g.getHeight() - 40;
ball.dx = (Math.random() > 0.5 ? 1 : -1) * 3;
ball.dy = -3;
paddle.x = g.getWidth() / 2 - PADDLE_WIDTH / 2;
ball.prevPos = null;
paddle.prevPos = null;
g.clear();
drawBricks();
draw();
setTimeout(() => { interval = setInterval(update, 50); }, 1000);
}
function update() {
if (paused || gameOver) return;
if (paddleMove) {
paddle.x += paddleMove * PADDLE_SPEED;
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > g.getWidth()) {
paddle.x = g.getWidth() - paddle.width;
}
}
ball.x += ball.dx;
ball.y += ball.dy;
if (ball.x + ball.radius > g.getWidth() || ball.x - ball.radius < 0) {
ball.dx = -ball.dx;
}
if (ball.y - ball.radius < 0) {
ball.dy = -ball.dy;
}
if (ball.y + ball.radius >= paddle.y && ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
ball.dy = -ball.dy;
ball.y = paddle.y - ball.radius;
}
if (ball.y + ball.radius > g.getHeight()) {
lives--;
if (lives > 0) {
resetAfterLifeLost();
return;
} else {
clearInterval(interval);
interval = undefined;
saveHighScore();
gameOver = true;
setTimeout(showGameOver, 50);
return;
}
}
collisionDetection();
if (allBricksCleared()) {
level++;
ball.dx = (Math.random() > 0.5 ? 1 : -1) * Math.abs(ball.dx) * SPEED_MULTIPLIER;
ball.dy *= SPEED_MULTIPLIER;
initBricks();
ball.prevPos = null;
paddle.prevPos = null;
g.clear();
drawBricks();
}
draw();
}
function showGetReady(callback) {
g.clear();
g.setFont("6x8", 2);
g.setFontAlign(0, 0);
g.setColor(1, 1, 0);
g.drawString("GET READY!", g.getWidth() / 2, g.getHeight() / 2);
g.flip();
setTimeout(() => {
g.clear();
drawBricks();
draw();
callback();
}, 1500);
}
function startGame() {
initGame();
showGetReady(() => {
interval = setInterval(update, 50);
});
}
setWatch(() => {
if (gameOver) {
startGame();
return;
}
paused = !paused;
if (paused) {
drawHUD();
g.flip();
} else {
g.clear();
drawBricks();
draw();
}
}, BTN2, { repeat: true, edge: "rising" });
setWatch(() => { paddleMove = -1; }, BTN1, { repeat: true, edge: "rising" });
setWatch(() => { paddleMove = 0; }, BTN1, { repeat: true, edge: "falling" });
setWatch(() => { paddleMove = 1; }, BTN3, { repeat: true, edge: "rising" });
setWatch(() => { paddleMove = 0; }, BTN3, { repeat: true, edge: "falling" });
startGame();
})();

BIN
apps/bbreaker/bbreaker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,15 @@
{ "id": "bbreaker",
"name": "BrickBreaker",
"shortName":"BrickBreaker",
"icon": "bbreaker.png",
"version":"0.04",
"description": "A simple BreakOut clone for the Banglejs",
"tags": "game",
"readme": "README.md",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"storage": [
{"name":"bbreaker.app.js","url":"app.js"},
{"name":"bbreaker.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/bbreaker/playing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -5,7 +5,7 @@
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo", "type": "clkinfo",
"tags": "clkinfo", "tags": "clkinfo,clock",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],
"storage": [ "storage": [
{"name":"clkinfoclk.clkinfo.js","url":"clkinfo.js"} {"name":"clkinfoclk.clkinfo.js","url":"clkinfo.js"}

View File

@ -0,0 +1 @@
0.01: New App!

BIN
apps/clkinfodist/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,22 @@
(function() {
let strideLength = (require("Storage").readJSON("myprofile.json",1)||{}).strideLength ?? 0.79,
lastSteps = 0;
function stepUpdateHandler() { distance.emit("redraw"); }
var distance = {
name : "Distance",
get : () => { let v = (Bangle.getHealthStatus("day").steps - lastSteps)*strideLength; return {
text : require("locale").distance(v,1),
img : atob("GBiBAAMAAAeAAA/AAA/AAA/gAA/gwAfh4AfD4APD4AOH4AAH4ADj4AHjwAHhwADgAAACAAAHgAAPAAAHAAgCEBgAGD///BgAGAgAEA==")
};},
run : function() {
lastSteps = (lastSteps>=Bangle.getHealthStatus("day").steps) ? 0 : Bangle.getHealthStatus("day").steps;
this.emit("redraw");
},
show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); },
hide : function() { Bangle.removeListener("step", stepUpdateHandler); }
};
return {
name: "Bangle",
items: [ distance ]
};
})

BIN
apps/clkinfodist/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,14 @@
{ "id": "clkinfodist",
"name": "Clockinfo Distance",
"version":"0.01",
"description": "Uses the 'My Profile' app's Stride Length to calculate distance travelled based on step count. Tap to reset for measuring distances.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo,distance,steps,outdoors,tool",
"dependencies": {"myprofile":"app"},
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfodist.clkinfo.js","url":"clkinfo.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -16,3 +16,6 @@
0.15: Fix error when displaying a category with only one clockinfo (fix #3728) 0.15: Fix error when displaying a category with only one clockinfo (fix #3728)
0.16: Add BLE clkinfo entry 0.16: Add BLE clkinfo entry
0.17: Fix BLE icon alignment and border on some clocks 0.17: Fix BLE icon alignment and border on some clocks
0.18: Tweak BLE icon to add gap and ensure middle of B isn't filled
0.19: Fix Altitude ClockInfo after BLE added
Tapping Altitude now updates the reading

View File

@ -39,22 +39,23 @@ exports.load = function() {
var hrm = 0; var hrm = 0;
var alt = "--"; var alt = "--";
// callbacks (needed for easy removal of listeners) // callbacks (needed for easy removal of listeners)
function batteryUpdateHandler() { bangleItems[0].emit("redraw"); } function batteryUpdateHandler() { bangleItems.find(i=>i.name=="Battery").emit("redraw"); }
function stepUpdateHandler() { bangleItems[1].emit("redraw"); } function stepUpdateHandler() { bangleItems.find(i=>i.name=="Steps").emit("redraw"); }
function hrmUpdateHandler(e) { function hrmUpdateHandler(e) {
if (e && e.confidence>60) hrm = Math.round(e.bpm); if (e && e.confidence>60) hrm = Math.round(e.bpm);
bangleItems[2].emit("redraw"); bangleItems.find(i=>i.name=="HRM").emit("redraw");
} }
function altUpdateHandler() { function altUpdateHandler() {
try { try {
Bangle.getPressure().then(data=>{ Bangle.getPressure().then(data=>{
if (!data) return; if (!data) return;
alt = Math.round(data.altitude) + "m"; alt = Math.round(data.altitude) + "m";
bangleItems[3].emit("redraw"); bangleItems.find(i=>i.name=="Altitude").emit("redraw");
}); });
} catch (e) { } catch (e) {
print("Caught "+e+"\n in function altUpdateHandler in module clock_info"); print("Caught "+e+"\n in function altUpdateHandler in module clock_info");
bangleItems[3].emit('redraw');} bangleItems.find(i=>i.name=="Altitude").emit('redraw');
}
} }
// actual menu // actual menu
var menu = [{ var menu = [{
@ -120,7 +121,6 @@ exports.load = function() {
}, },
}, },
{ name: "BLE", { name: "BLE",
hasRange: false,
isOn: () => { isOn: () => {
const s = NRF.getSecurityStatus(); const s = NRF.getSecurityStatus();
return s.advertising || s.connected; return s.advertising || s.connected;
@ -128,7 +128,8 @@ exports.load = function() {
get: function() { get: function() {
return { return {
text: this.isOn() ? "On" : "Off", text: this.isOn() ? "On" : "Off",
img: atob("GBiBAAAAAAAAAAAYAAAcAAAWAAATAAARgAMRgAGTAADWAAB8AAA4AAA4AAB8AADWAAGTAAMRgAARgAATAAAWAAAcAAAYAAAAAAAAAA==") img: atob("GBiBAAAAAAAAAAAYAAAcAAAWAAATAAARgAMRgAGTAADGAAB8AAA4AAA4AAB8AADGAAGTAAMRgAARgAATAAAWAAAcAAAYAAAAAAAAAA==")
// small gaps added to BLE icon to ensure middle of B isn't filled
}; };
}, },
run: function() { run: function() {
@ -155,6 +156,7 @@ exports.load = function() {
min : 0, max : settings.maxAltitude, min : 0, max : settings.maxAltitude,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==") img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==")
}), }),
run : function() { alt = "--"; this.emit("redraw"); altUpdateHandler(); },
show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); }, show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; }, hide : function() { clearInterval(this.interval); delete this.interval; },
}); });

View File

@ -1,7 +1,7 @@
{ "id": "clock_info", { "id": "clock_info",
"name": "Clock Info Module", "name": "Clock Info Module",
"shortName": "Clock Info", "shortName": "Clock Info",
"version":"0.17", "version":"0.19",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png", "icon": "app.png",
"type": "module", "type": "module",

View File

@ -4,3 +4,4 @@
0.04: Fix lint warnings 0.04: Fix lint warnings
0.05: Fix on not reading counter defaults in Settings 0.05: Fix on not reading counter defaults in Settings
0.06: Added ability to display only one counter and fast-scrolling 0.06: Added ability to display only one counter and fast-scrolling
0.07: Add option to keep unlocked

View File

@ -7,12 +7,19 @@ var s = Object.assign({
fullscreen:true, fullscreen:true,
buzz: true, buzz: true,
colortext: true, colortext: true,
keepunlocked: false,
}, require('Storage').readJSON("counter2.json", true) || {}); }, require('Storage').readJSON("counter2.json", true) || {});
var sGlob = Object.assign({ var sGlob = Object.assign({
timeout: 10, timeout: 10,
}, require('Storage').readJSON("setting.json", true) || {}); }, require('Storage').readJSON("setting.json", true) || {});
const lockTimeout = s.keepunlocked ? 0 : 1000;
if (s.keepunlocked) {
Bangle.setOptions({lockTimeout});
Bangle.setLocked(false);
}
const f1 = (s.colortext) ? "#f00" : "#fff"; const f1 = (s.colortext) ? "#f00" : "#fff";
const f2 = (s.colortext) ? "#00f" : "#fff"; const f2 = (s.colortext) ? "#00f" : "#fff";
const b1 = (s.colortext) ? g.theme.bg : "#f00"; const b1 = (s.colortext) ? g.theme.bg : "#f00";
@ -80,7 +87,7 @@ function updateScreen() {
Bangle.on('lock', e => { Bangle.on('lock', e => {
drag = undefined; drag = undefined;
var timeOutTimer = sGlob.timeout * 1000; var timeOutTimer = sGlob.timeout * 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer}); Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
if (dragtimeout) clearTimeout(dragtimeout); if (dragtimeout) clearTimeout(dragtimeout);
fastupdateoccurring = false; fastupdateoccurring = false;
}); });
@ -102,7 +109,7 @@ Bangle.on("drag", e => {
drag = undefined; drag = undefined;
if (dragtimeout) { if (dragtimeout) {
let timeOutTimer = 1000; let timeOutTimer = 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer}); Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
clearTimeout(dragtimeout); clearTimeout(dragtimeout);
} }
fastupdateoccurring = false; fastupdateoccurring = false;
@ -134,7 +141,7 @@ function resetcounter(which) {
fastupdateoccurring = false; fastupdateoccurring = false;
if (dragtimeout) { if (dragtimeout) {
let timeOutTimer = 1000; let timeOutTimer = 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer}); Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
clearTimeout(dragtimeout); clearTimeout(dragtimeout);
} }
if (which == null) { if (which == null) {
@ -152,7 +159,6 @@ function resetcounter(which) {
ignoreonce = true; ignoreonce = true;
} }
updateScreen(); updateScreen();
setWatch(function() { setWatch(function() {
@ -163,6 +169,6 @@ setWatch(function() {
} }
} }
var timeOutTimer = sGlob.timeout * 1000; var timeOutTimer = sGlob.timeout * 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer}); Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
load(); load();
}, BTN1, {repeat:true, edge:"falling"}); }, BTN1, {repeat:true, edge:"falling"});

View File

@ -1,7 +1,7 @@
{ {
"id": "counter2", "id": "counter2",
"name": "Counter2", "name": "Counter2",
"version": "0.06", "version": "0.07",
"description": "Dual Counter", "description": "Dual Counter",
"readme":"README.md", "readme":"README.md",
"icon": "counter2-icon.png", "icon": "counter2-icon.png",

View File

@ -72,6 +72,13 @@
writeSettings(); writeSettings();
}, },
}; };
appMenu['Keep unlocked'] = {
value: settings.keepunlocked,
onchange: v => {
settings.keepunlocked = v;
writeSettings();
},
};
E.showMenu(appMenu); E.showMenu(appMenu);
} }

View File

@ -7,6 +7,8 @@
"icon": "icon.png", "icon": "icon.png",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"allow_emulator": true, "allow_emulator": true,
"type": "clock",
"tags": "clock",
"storage": [ "storage": [
{"name":"drained.boot.js","url":"boot.js"}, {"name":"drained.boot.js","url":"boot.js"},
{"name":"drained.app.js","url":"app.js"}, {"name":"drained.app.js","url":"app.js"},

View File

@ -0,0 +1 @@
0.01: initial import

View File

@ -0,0 +1,5 @@
# Elite clock ![](app.png)
Simple binary clock for leet haxorz.
Written by: [Pavel Machek](https://github.com/pavelmachek)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIspiEPgEeAoU/4F/wGAiEAsEA4AFImAHBAolj/gRD4YFEC4UPwEfgAFC4EfF5IpHAp4dC4EQv/A/+AHYJlDnjY/AH4AJ"))

BIN
apps/eliteclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,86 @@
// Bangle.js 2 - Binary Leet ClockZ
/*
bangle.js2: create binary 'leet clock' where the time is shown as text
"leet clockz" with binary 0 being normal character and binary one
being leet translation. Be careful to only update time on minute
boundaries.
So yeah 1337 c10ckZ = certified leetspeak 😎
ChatGPT said:
Alright, heres a hardcore hacker variant of elite clock in full-on aggressive leetspeak:
£|173 ¢|_0¢|<Z
Breakdown:
E £ or 3
L | or £
I 1 or !
T 7 or +
C ¢ or (
O 0
K |< or X
Other extreme variants:
3|173 (|_0(<Z
|!73 ©|0xX
3L1+3 (L0XXZ
*/
const TEXT = "leet\nclockz";
const LEET = {
"l": "1",
"e": "3",
"t": "7",
" ": " ",
"c": "(",
"o": "0",
"k": "X",
"z": "2"
};
// Convert hour (12h) and minute to binary mask
function getBinaryFromTime(d) {
let h = d.getHours() % 12;
if (h === 0) h = 12; // 12-hour format: 0 becomes 12
const m = d.getMinutes();
const bin = ((h << 7) | m).toString(2).padStart(11, '0');
return bin;
}
// Map normal characters to leet if binary mask says so
function getDisplayText(binMask) {
return TEXT.split("").map((ch, i) =>
binMask[i] === '1' ? (LEET[ch] || ch) : ch
).join("");
}
function draw() {
g.reset().clear();
const now = new Date();
const bin = getBinaryFromTime(now);
const txt = getDisplayText(bin);
const w = 0;
g.setFont("Vector", 47).setFontAlign(0,0);
g.drawString(txt, (g.getWidth() - w) / 2, (g.getHeight() - 0) / 2);
}
function scheduleNextDraw() {
const now = new Date();
const msToNextMin = 60000 - (now.getSeconds() * 1000 + now.getMilliseconds());
setTimeout(() => {
draw();
scheduleNextDraw();
}, msToNextMin);
}
// Init
draw();
scheduleNextDraw();
//Bangle.loadWidgets();
//Bangle.drawWidgets();

View File

@ -0,0 +1,14 @@
{ "id": "eliteclock",
"name": "Elite clock",
"version": "0.01",
"description": "Simple binary clock for leet haxorz",
"icon": "app.png",
"readme": "README.md",
"supports" : ["BANGLEJS2"],
"type": "clock",
"tags": "clock",
"storage": [
{"name":"eliteclock.app.js","url":"eliteclock.app.js"},
{"name":"eliteclock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -35,3 +35,10 @@
0.31: Add support for new health format (storing more data) 0.31: Add support for new health format (storing more data)
Added graphs for Temperature and Battery Added graphs for Temperature and Battery
0.32: If getting HRM every 3/10 minutes, don't turn it on if the Bangle is charging or hasn't moved and is face down/up 0.32: If getting HRM every 3/10 minutes, don't turn it on if the Bangle is charging or hasn't moved and is face down/up
0.33: Ensure readAllRecordsSince always includes the current day
Speed improvements (put temporary functions in RAM where possible)
0.34: Fix readFullDatabase (was skipping first month of data)
0.35: Update boot/lib.min.js
0.36: Fix Distance graphs that used '1*' to remove the suffix
0.37: Reduce movement limit for HRM off from 400 to 100
Fix daily summary for movement (was not scaling down correctly)

View File

@ -32,7 +32,7 @@ function menuStepCount() {
} }
function menuDistance() { function menuDistance() {
const distMult = 1*require("locale").distance(myprofile.strideLength, 2); // hackish: this removes the distance suffix, e.g. 'm' const distMult = parseFloat(require("locale").distance(myprofile.strideLength, 2)); // this removes the distance suffix, e.g. 'm'
E.showMenu({ E.showMenu({
"": { title:/*LANG*/"Distance" }, "": { title:/*LANG*/"Distance" },
/*LANG*/"< Back": () => menuStepCount(), /*LANG*/"< Back": () => menuStepCount(),

View File

@ -6,7 +6,7 @@
function startMeasurement() { function startMeasurement() {
// if is charging, or hardly moved and face up/down, don't start HRM // if is charging, or hardly moved and face up/down, don't start HRM
if (Bangle.isCharging() || if (Bangle.isCharging() ||
(Bangle.getHealthStatus("last").movement<400 && Math.abs(Bangle.getAccel().z)>0.99)) return; (Bangle.getHealthStatus("last").movement<100 && Math.abs(Bangle.getAccel().z)>0.99)) return;
// otherwise turn HRM on // otherwise turn HRM on
Bangle.setHRMPower(1, "health"); Bangle.setHRMPower(1, "health");
setTimeout(() => { setTimeout(() => {
@ -81,12 +81,6 @@ Bangle.on("health", health => {
require("Storage").write(fn, "HEALTH2\0", 0, DB_HEADER_LEN + DB_RECORDS_PER_MONTH*inf.r); // header (and allocate full new file) require("Storage").write(fn, "HEALTH2\0", 0, DB_HEADER_LEN + DB_RECORDS_PER_MONTH*inf.r); // header (and allocate full new file)
} }
var recordPos = DB_HEADER_LEN+(rec*inf.r); var recordPos = DB_HEADER_LEN+(rec*inf.r);
// 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, inf.encode(health), recordPos); require("Storage").write(fn, inf.encode(health), recordPos);
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return; 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 // we're at the end of the day. Read in all of the data for the day and sum it up

View File

@ -1,5 +1,5 @@
(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function c(){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",c);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence|| {let a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){let d=function(b){function c(){Bangle.isCharging()||100>Bangle.getHealthStatus("last").movement&&.99<Math.abs(Bangle.getAccel().z)||(Bangle.setHRMPower(1,"health"),setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4*a))}c();1==a&&(setTimeout(c,2E5),setTimeout(c,4E5))};Bangle.on("health",d);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,
c()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(c=>{Object.assign(a,c);c=new Date(Date.now()-59E4);if(a&&0<a.steps){var b=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;b.stepGoalNotification&&0<b.stepGoal&&d>=b.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!b.stepGoalNotificationDate||b.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:b.stepGoal+ "health")});90>Bangle.getHealthStatus().bpmConfidence&&d()}else Bangle.setHRMPower(!!a,"health")}Bangle.on("health",a=>{(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(d=>{Object.assign(a,d);d=new Date(Date.now()-59E4);if(a&&0<a.steps){var b=require("Storage").readJSON("health.json",1)||{},c=Bangle.getHealthStatus("day").steps;b.stepGoalNotification&&0<b.stepGoal&&c>=b.stepGoal&&(c=(new Date(Date.now())).toISOString().split("T")[0],!b.stepGoalNotificationDate||b.stepGoalNotificationDate<
" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),b.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",b))}var g=function(f){return 145*(f.getDate()-1)+6*f.getHours()+(0|6*f.getMinutes()/60)}(c);c=function(f){return"health-"+f.getFullYear()+"-"+(f.getMonth()+1)+".raw"}(c);d=require("Storage").read(c);if(void 0!==d){b=require("health").getDecoder(d);var e=d.substr(8+g*b.r,b.r);if(e!=b.clr){print("HEALTH ERR: Already written!");return}}else b= c)&&(Bangle.buzz(200,.5),require("notify").show({title:b.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),b.stepGoalNotificationDate=c,require("Storage").writeJSON("health.json",b))}var g=function(f){return 145*(f.getDate()-1)+6*f.getHours()+(0|6*f.getMinutes()/60)}(d);d=function(f){return"health-"+f.getFullYear()+"-"+(f.getMonth()+1)+".raw"}(d);c=require("Storage").read(d);if(void 0!==c){b=require("health").getDecoder(c);var e=c.substr(8+g*b.r,
require("health").getDecoder("HEALTH2"),require("Storage").write(c,"HEALTH2\x00",0,8+4495*b.r);var h=8+g*b.r;a=Object.assign({},a);a.movement/=8;require("Storage").write(c,b.encode(a),h);if(143==g%145)if(g=h+b.r,d.substr(g,b.r)!=b.clr)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=d.substr(h,b.r),e!=b.clr&&(e=b.decode(e),a.steps+=e.steps,a.bpm+=e.bpm,a.movement+=e.movement,a.movCnt++,e.bpm&&a.bpmCnt++),h-=b.r;a.bpmCnt&& b.r);if(e!=b.clr){print("HEALTH ERR: Already written!");return}}else b=require("health").getDecoder("HEALTH2"),require("Storage").write(d,"HEALTH2\x00",0,8+4495*b.r);var h=8+g*b.r;require("Storage").write(d,b.encode(a),h);if(143==g%145)if(g=h+b.r,c.substr(g,b.r)!=b.clr)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=c.substr(h,b.r),e!=b.clr&&(e=b.decode(e),a.steps+=e.steps,a.bpm+=e.bpm,a.movement+=e.movement,a.movCnt++,
(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(c,b.encode(a),g)}})}) e.bpm&&a.bpmCnt++),h-=b.r;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(d,b.encode(a),g)}})})

View File

@ -41,48 +41,49 @@ exports.getDecoder = function(fileContents) {
return { return {
r : 10, // record length r : 10, // record length
clr : "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", clr : "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF",
decode : h => { var v = { decode : h => { "ram"; var d = h.charCodeAt.bind(h), v = {
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), steps : (d(0)<<8) | d(1),
bpmMin : h.charCodeAt(2), bpmMin : d(2),
bpmMax : h.charCodeAt(3), bpmMax : d(3),
movement : h.charCodeAt(4)*8, movement : d(4)*8,
battery : h.charCodeAt(5)&127, battery : d(5)&127,
isCharging : !!(h.charCodeAt(5)&128), isCharging : !!(d(5)&128),
temperature : h.charCodeAt(6)/2, // signed? temperature : d(6)/2, // signed?
altitude : ((h.charCodeAt(7)&31)<<8)|h.charCodeAt(8), // signed? altitude : ((d(7)&31)<<8)|d(8), // signed?
activity : exports.ACTIVITY[h.charCodeAt(7)>>5] activity : exports.ACTIVITY[d(7)>>5]
}; };
if (v.temperature>80) v.temperature-=128; if (v.temperature>80) v.temperature-=128;
v.bpm = (v.bpmMin+v.bpmMax)/2; v.bpm = (v.bpmMin+v.bpmMax)/2;
if (v.altitude > 7500) v.altitude-=8192; if (v.altitude > 7500) v.altitude-=8192;
return v; return v;
}, },
encode : health => {var alt=health.altitude&8191;return String.fromCharCode( encode : health => { "ram"; var alt=health.altitude&8191;return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps health.steps>>8,health.steps&255, // 16 bit steps
health.bpmMin || health.bpm, // 8 bit bpm health.bpmMin || health.bpm, // 8 bit bpm
health.bpmMax || health.bpm, // 8 bit bpm health.bpmMax || health.bpm, // 8 bit bpm
Math.min(health.movement, 255), Math.min(health.movement >> 3, 255),
E.getBattery()|(Bangle.isCharging()&&128), E.getBattery()|(Bangle.isCharging()&&128),
0|Math.round(health.temperature*2), 0|Math.round(health.temperature*2),
(alt>>8)|(Math.max(0,exports.ACTIVITY.indexOf(health.activity))<<5),alt&255, (alt>>8)|(Math.max(0,exports.ACTIVITY.indexOf(health.activity))<<5),alt&255,
0 // tbd 0 // tbd
)} );}
}; };
} else { // HEALTH1 } else { // HEALTH1
return { return {
r : 4, // record length r : 4, // record length
clr : "\xFF\xFF\xFF\xFF", clr : "\xFF\xFF\xFF\xFF",
decode : h => ({ decode : h => { "ram"; return {
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2), bpm : h.charCodeAt(2),
bpmMin : h.charCodeAt(2), bpmMin : h.charCodeAt(2),
bpmMax : h.charCodeAt(2), bpmMax : h.charCodeAt(2),
movement : h.charCodeAt(3)*8 movement : h.charCodeAt(3)*8
}), };},
encode : health => String.fromCharCode( encode : health => { "ram"; return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps health.steps>>8,health.steps&255, // 16 bit steps
health.bpm, // 8 bit bpm health.bpm, // 8 bit bpm
Math.min(health.movement, 255)) Math.min(health.movement >> 3, 255));
}
}; };
} }
}; };
@ -111,12 +112,10 @@ exports.readAllRecords = function(d, cb) {
// Read the entire database. There is no guarantee that the months are read in order. // Read the entire database. There is no guarantee that the months are read in order.
exports.readFullDatabase = function(cb) { exports.readFullDatabase = function(cb) {
require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => { require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => {
console.log(val);
var parts = val.split('-'); var parts = val.split('-');
var y = parseInt(parts[1],10); var y = parseInt(parts[1],10);
var mo = parseInt(parts[2].replace('.raw', ''),10); var mo = parseInt(parts[2].replace('.raw', ''),10) - 1;
exports.readAllRecords(new Date(y, mo, 1), (r) => {"ram";
exports.readAllRecords(new Date(y, mo, 1), (r) => {
r.date = new Date(y, mo, r.day, r.hr, r.min); r.date = new Date(y, mo, r.day, r.hr, r.min);
cb(r); cb(r);
}); });
@ -127,9 +126,9 @@ exports.readFullDatabase = function(cb) {
// There may be some records for the day of the timestamp previous to the timestamp // There may be some records for the day of the timestamp previous to the timestamp
exports.readAllRecordsSince = function(d, cb) { exports.readAllRecordsSince = function(d, cb) {
var currentDate = new Date().getTime(); var currentDate = new Date().getTime();
var di = d; var di = new Date(d.toISOString().substr(0,10)); // copy date (ignore time)
while (di.getTime() <= currentDate) { while (di.getTime() <= currentDate) {
exports.readDay(di, (r) => { exports.readDay(di, (r) => {"ram";
r.date = new Date(di.getFullYear(), di.getMonth(), di.getDate(), r.hr, r.min); r.date = new Date(di.getFullYear(), di.getMonth(), di.getDate(), r.hr, r.min);
cb(r); cb(r);
}); });
@ -149,7 +148,7 @@ exports.readDailySummaries = function(d, cb) {
if (h!=inf.clr) cb(Object.assign(inf.decode(h), {day:day+1})); if (h!=inf.clr) cb(Object.assign(inf.decode(h), {day:day+1}));
idx += DB_RECORDS_PER_DAY*inf.r; idx += DB_RECORDS_PER_DAY*inf.r;
} }
} };
// Read all records from the given day // Read all records from the given day
exports.readDay = function(d, cb) { exports.readDay = function(d, cb) {
@ -167,4 +166,4 @@ exports.readDay = function(d, cb) {
idx += inf.r; idx += inf.r;
} }
} }
} };

View File

@ -1,6 +1,6 @@
function k(b){return"health-"+b.getFullYear()+"-"+(b.getMonth()+1)+".raw"}function l(b){return 145*(b.getDate()-1)+6*b.getHours()+(0|6*b.getMinutes()/60)}exports.ACTIVITY="UNKNOWN NOT_WORN WALKING EXERCISE LIGHT_SLEEP DEEP_SLEEP".split(" ");exports.getDecoder=function(b){return"HEALTH2"==b.substr(0,7)?{r:10,clr:"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",decode:a=>{a={steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(3), function k(b){return"health-"+b.getFullYear()+"-"+(b.getMonth()+1)+".raw"}function l(b){return 145*(b.getDate()-1)+6*b.getHours()+(0|6*b.getMinutes()/60)}exports.ACTIVITY="UNKNOWN NOT_WORN WALKING EXERCISE LIGHT_SLEEP DEEP_SLEEP".split(" ");exports.getDecoder=function(b){return"HEALTH2"==b.substr(0,7)?{r:10,clr:"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",decode:a=>{"ram";a=a.charCodeAt.bind(a);a={steps:a(0)<<8|a(1),bpmMin:a(2),bpmMax:a(3),movement:8*a(4),
movement:8*a.charCodeAt(4),battery:a.charCodeAt(5)&127,isCharging:!!(a.charCodeAt(5)&128),temperature:a.charCodeAt(6)/2,altitude:(a.charCodeAt(7)&31)<<8|a.charCodeAt(8),activity:exports.ACTIVITY[a.charCodeAt(7)>>5]};80<a.temperature&&(a.temperature-=128);a.bpm=(a.bpmMin+a.bpmMax)/2;7500<a.altitude&&(a.altitude-=8192);return a},encode:a=>{var c=a.altitude&8191;return String.fromCharCode(a.steps>>8,a.steps&255,a.bpmMin||a.bpm,a.bpmMax||a.bpm,Math.min(a.movement,255),E.getBattery()|(Bangle.isCharging()&& battery:a(5)&127,isCharging:!!(a(5)&128),temperature:a(6)/2,altitude:(a(7)&31)<<8|a(8),activity:exports.ACTIVITY[a(7)>>5]};80<a.temperature&&(a.temperature-=128);a.bpm=(a.bpmMin+a.bpmMax)/2;7500<a.altitude&&(a.altitude-=8192);return a},encode:a=>{"ram";var c=a.altitude&8191;return String.fromCharCode(a.steps>>8,a.steps&255,a.bpmMin||a.bpm,a.bpmMax||a.bpm,Math.min(a.movement>>3,255),E.getBattery()|(Bangle.isCharging()&&128),0|Math.round(2*a.temperature),c>>8|Math.max(0,exports.ACTIVITY.indexOf(a.activity))<<
128),0|Math.round(2*a.temperature),c>>8|Math.max(0,exports.ACTIVITY.indexOf(a.activity))<<5,c&255,0)}}:{r:4,clr:"\xff\xff\xff\xff",decode:a=>({steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpm:a.charCodeAt(2),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(2),movement:8*a.charCodeAt(3)}),encode:a=>String.fromCharCode(a.steps>>8,a.steps&255,a.bpm,Math.min(a.movement,255))}};exports.readAllRecords=function(b,a){b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d={},e=8, 5,c&255,0)}}:{r:4,clr:"\xff\xff\xff\xff",decode:a=>{"ram";return{steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpm:a.charCodeAt(2),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(2),movement:8*a.charCodeAt(3)}},encode:a=>{"ram";return String.fromCharCode(a.steps>>8,a.steps&255,a.bpm,Math.min(a.movement>>3,255))}}};exports.readAllRecords=function(b,a){b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d={},e=8,f=0;31>f;f++){d.day=f+1;for(var g=0;24>g;g++){d.hr=g;for(var h=
f=0;31>f;f++){d.day=f+1;for(var g=0;24>g;g++){d.hr=g;for(var h=0;6>h;h++){d.min=10*h;var m=b.substr(e,c.r);m!=c.clr&&a(Object.assign(c.decode(m),d));e+=c.r}}e+=c.r}};exports.readFullDatabase=function(b){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(a=>{console.log(a);a=a.split("-");var c=parseInt(a[1],10),d=parseInt(a[2].replace(".raw",""),10);exports.readAllRecords(new Date(c,d,1),e=>{e.date=new Date(c,d,e.day,e.hr,e.min);b(e)})})};exports.readAllRecordsSince=function(b,a){for(var c= 0;6>h;h++){d.min=10*h;var m=b.substr(e,c.r);m!=c.clr&&a(Object.assign(c.decode(m),d));e+=c.r}}e+=c.r}};exports.readFullDatabase=function(b){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(a=>{a=a.split("-");var c=parseInt(a[1],10),d=parseInt(a[2].replace(".raw",""),10)-1;exports.readAllRecords(new Date(c,d,1),e=>{"ram";e.date=new Date(c,d,e.day,e.hr,e.min);b(e)})})};exports.readAllRecordsSince=function(b,a){for(var c=(new Date).getTime(),d=new Date(b.toISOString().substr(0,10));d.getTime()<=
(new Date).getTime();b.getTime()<=c;)exports.readDay(b,d=>{d.date=new Date(b.getFullYear(),b.getMonth(),b.getDate(),d.hr,d.min);a(d)}),b.setDate(b.getDate()+1)};exports.readDailySummaries=function(b,a){l(b);b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d=8+144*c.r,e=0;31>e;e++){var f=b.substr(d,c.r);f!=c.clr&&a(Object.assign(c.decode(f),{day:e+1}));d+=145*c.r}};exports.readDay=function(b,a){l(b);var c=k(b);c=require("Storage").read(c);if(void 0!==c){var d=exports.getDecoder(c), c;)exports.readDay(d,e=>{"ram";e.date=new Date(d.getFullYear(),d.getMonth(),d.getDate(),e.hr,e.min);a(e)}),d.setDate(d.getDate()+1)};exports.readDailySummaries=function(b,a){l(b);b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d=8+144*c.r,e=0;31>e;e++){var f=b.substr(d,c.r);f!=c.clr&&a(Object.assign(c.decode(f),{day:e+1}));d+=145*c.r}};exports.readDay=function(b,a){l(b);var c=k(b);c=require("Storage").read(c);if(void 0!==c){var d=exports.getDecoder(c),e={};b=8+145*
e={};b=8+145*d.r*(b.getDate()-1);for(var f=0;24>f;f++){e.hr=f;for(var g=0;6>g;g++){e.min=10*g;var h=c.substr(b,d.r);h!=d.clr&&a(Object.assign(d.decode(h),e));b+=d.r}}}} d.r*(b.getDate()-1);for(var f=0;24>f;f++){e.hr=f;for(var g=0;6>g;g++){e.min=10*g;var h=c.substr(b,d.r);h!=d.clr&&a(Object.assign(d.decode(h),e));b+=d.r}}}}

View File

@ -2,9 +2,10 @@
"id": "health", "id": "health",
"name": "Health Tracking", "name": "Health Tracking",
"shortName": "Health", "shortName": "Health",
"version": "0.32", "version": "0.37",
"description": "Logs health data and provides an app to view it", "description": "Logs health data and provides an app to view it",
"icon": "app.png", "icon": "app.png",
"screenshots" : [ { "url":"screenshot.png" } ],
"tags": "tool,system,health", "tags": "tool,system,health",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",

BIN
apps/health/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -4,6 +4,7 @@
"version": "0.13", "version": "0.13",
"description": "Measure your heart rate and see live sensor data", "description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png", "icon": "heartrate.png",
"screenshots" : [ { "url":"screenshot.png" } ],
"tags": "health", "tags": "health",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [

BIN
apps/hrm/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -16,5 +16,4 @@ It is also possible to load existing icon into editor, using
"load_icon("");" command. At the end of iconbits.app.js file there are "load_icon("");" command. At the end of iconbits.app.js file there are
more utility functions. more utility functions.
Create 48x48 icon in gimp.

BIN
apps/iconbits/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -18,3 +18,4 @@
0.17: Default to passing full UTF8 strings into messages app (which can now process them with an international font) 0.17: Default to passing full UTF8 strings into messages app (which can now process them with an international font)
0.18: Fix UTF8 conversion (check for `font` library, not `fonts`) 0.18: Fix UTF8 conversion (check for `font` library, not `fonts`)
0.19: Convert numeric weather values to int from BangleDumpWeather shortcut 0.19: Convert numeric weather values to int from BangleDumpWeather shortcut
0.20: Add feels-like temperature data field to weather parsing from BangleDumpWeather shortcut.

View File

@ -180,6 +180,7 @@ E.on('notify',msg=>{
let weatherEvent = { let weatherEvent = {
t: "weather", t: "weather",
temp: d.temp, temp: d.temp,
feels: d.feels,
hi: d.hi, hi: d.hi,
lo: d.lo, lo: d.lo,
hum: d.hum, hum: d.hum,
@ -192,7 +193,7 @@ E.on('notify',msg=>{
loc: d.loc loc: d.loc
} }
// Convert string fields to numbers for iOS weather shortcut // Convert string fields to numbers for iOS weather shortcut
const numFields = ['code', 'wdir', 'temp', 'hi', 'lo', 'hum', 'wind', 'uv', 'rain']; const numFields = ['code', 'wdir', 'temp','feels', 'hi', 'lo', 'hum', 'wind', 'uv', 'rain'];
numFields.forEach(field => { numFields.forEach(field => {
if (weatherEvent[field] != null) weatherEvent[field] = +weatherEvent[field]; if (weatherEvent[field] != null) weatherEvent[field] = +weatherEvent[field];
}); });

View File

@ -1,8 +1,8 @@
{ {
"id": "ios", "id": "ios",
"name": "iOS Integration", "name": "iOS Integration",
"version": "0.19", "version": "0.20",
"description": "Display notifications/music/etc from iOS devices", "description": "Display/pull notifications, music, weather, and agenda from iOS devices",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications", "tags": "tool,system,ios,apple,messages,notifications",
"dependencies": {"messages":"module"}, "dependencies": {"messages":"module"},

21
apps/jwalk/README.md Normal file
View File

@ -0,0 +1,21 @@
# Japanese Walking Timer
A simple timer designed to help you manage your walking intervals, whether you're in a relaxed mode or an intense workout!
![](screenshot.png)
## Usage
- The timer starts with a default total duration and interval duration, which can be adjusted in the settings.
- Tap the screen to pause or resume the timer.
- The timer will switch modes between "Relax" and "Intense" at the end of each interval.
- The display shows the current time, the remaining interval time, and the total time left.
## Creator
[Fabian Köll] ([Koell](https://github.com/Koell))
## Icon
[Icon](https://www.koreanwikiproject.com/wiki/images/2/2f/%E8%A1%8C.png)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///A4IDBvvv11zw0xlljjnnJ3USoARP0uICJ+hnOACJ8mkARO9Mn0AGDhP2FQ8FhM9L4nyyc4CI0OpJZBgVN//lkmSsARGnlMPoMH2mSpMkzPQCAsBoViAgMC/WTt2T2giGhUTiBWDm3SU5FQ7yNOgeHum7Ypu+3sB5rFMgP3tEB5MxBg2X//+yAFBOIKhBngcFn8pkmTO4ShFAAUT+cSSQOSpgKDlihCPoN/mIOBCIVvUIsBk//zWStOz////u27QRCheTzEOtVJnV+6070BgGj2a4EL5V39MAgkm2ARGvGbNwMkOgUHknwCAsC43DvAIEg8mGo0Um+yCI0nkARF0O8nQjHCIsFh1gCJ08WwM6rARLgftNAMzCIsDI4te4gDBuYRM/pxCCJoADCI6PHdINDCI0kYo8BqYRHYowRByZ9GCJEDCLXACLVQAoUL+mXCJBrBiARD7clCJNzBIl8pIRIgEuwBGExMmUI4qH9MnYo4AH3MxCB0Ai/oCJ4AY"))

178
apps/jwalk/app.js Normal file
View File

@ -0,0 +1,178 @@
// === Utility Functions ===
function formatTime(seconds) {
let mins = Math.floor(seconds / 60);
let secs = (seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
}
function getTimeStr() {
let d = new Date();
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
function updateCachedLeftTime() {
cachedLeftTime = "Left: " + formatTime(state.remainingTotal);
}
// === Constants ===
const FILE = "jwalk.json";
const DEFAULTS = {
totalDuration: 30,
intervalDuration: 3,
startMode: 0,
modeBuzzerDuration: 1000,
finishBuzzerDuration: 1500,
showClock: 1,
updateWhileLocked: 0
};
// === Settings and State ===
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
let state = {
remainingTotal: settings.totalDuration * 60,
intervalDuration: settings.intervalDuration * 60,
remainingInterval: 0,
intervalEnd: 0,
paused: false,
currentMode: settings.startMode === 1 ? "Intense" : "Relax",
finished: false,
forceDraw: false,
};
let cachedLeftTime = "";
let lastMinuteStr = getTimeStr();
let drawTimerInterval;
// === UI Rendering ===
function drawUI() {
let y = Bangle.appRect.y + 8;
g.reset().setBgColor(g.theme.bg).clearRect(Bangle.appRect);
g.setColor(g.theme.fg);
let displayInterval = state.paused
? state.remainingInterval
: Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
g.setFont("Vector", 40);
g.setFontAlign(0, 0);
g.drawString(formatTime(displayInterval), g.getWidth() / 2, y + 70);
let cy = y + 100;
if (state.paused) {
g.setFont("Vector", 15);
g.drawString("PAUSED", g.getWidth() / 2, cy);
} else {
let cx = g.getWidth() / 2;
g.setColor(g.theme.accent || g.theme.fg2 || g.theme.fg);
if (state.currentMode === "Relax") {
g.fillCircle(cx, cy, 5);
} else {
g.fillPoly([
cx, cy - 6,
cx - 6, cy + 6,
cx + 6, cy + 6
]);
}
g.setColor(g.theme.fg);
}
g.setFont("6x8", 2);
g.setFontAlign(0, -1);
g.drawString(state.currentMode, g.getWidth() / 2, y + 15);
g.drawString(cachedLeftTime, g.getWidth() / 2, cy + 15);
if (settings.showClock) {
g.setFontAlign(1, 0);
g.drawString(lastMinuteStr, g.getWidth() - 4, y);
}
g.flip();
}
// === Workout Logic ===
function toggleMode() {
state.currentMode = state.currentMode === "Relax" ? "Intense" : "Relax";
Bangle.buzz(settings.modeBuzzerDuration);
state.forceDraw = true;
}
function startNextInterval() {
if (state.remainingTotal <= 0) {
finishWorkout();
return;
}
state.remainingInterval = Math.min(state.intervalDuration, state.remainingTotal);
state.remainingTotal -= state.remainingInterval;
updateCachedLeftTime();
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
state.forceDraw = true;
}
function togglePause() {
if (state.finished) return;
if (!state.paused) {
state.remainingInterval = Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
state.paused = true;
} else {
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
state.paused = false;
}
drawUI();
}
function finishWorkout() {
clearInterval(drawTimerInterval);
Bangle.buzz(settings.finishBuzzerDuration);
state.finished = true;
setTimeout(() => {
g.clear();
g.setFont("Vector", 30);
g.setFontAlign(0, 0);
g.drawString("Well done!", g.getWidth() / 2, g.getHeight() / 2);
g.flip();
const exitHandler = () => {
Bangle.removeListener("touch", exitHandler);
Bangle.removeListener("btn1", exitHandler);
load(); // Exit app
};
Bangle.on("touch", exitHandler);
setWatch(exitHandler, BTN1, { repeat: false });
}, 500);
}
// === Timer Tick ===
function tick() {
if (state.finished) return;
const currentMinuteStr = getTimeStr();
if (currentMinuteStr !== lastMinuteStr) {
lastMinuteStr = currentMinuteStr;
state.forceDraw = true;
}
if (!state.paused && (state.intervalEnd - Date.now()) / 1000 <= 0) {
toggleMode();
startNextInterval();
return;
}
if (state.forceDraw || settings.updateWhileLocked || !Bangle.isLocked()) {
drawUI();
state.forceDraw = false;
}
}
// === Initialization ===
Bangle.on("touch", togglePause);
Bangle.loadWidgets();
Bangle.drawWidgets();
updateCachedLeftTime();
startNextInterval();
drawUI();
drawTimerInterval = setInterval(tick, 1000);

BIN
apps/jwalk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

19
apps/jwalk/metadata.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": "jwalk",
"name": "Japanese Walking",
"shortName": "J-Walk",
"icon": "app.png",
"version": "0.01",
"description": "Alternating walk timer: 3 min Relax / 3 min Intense for a set time. Tap to pause/resume. Start mode, interval and total time configurable via Settings.",
"tags": "walk,timer,fitness",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"data": [
{ "name": "jwalk.json" }
],
"storage": [
{ "name": "jwalk.app.js", "url": "app.js" },
{ "name": "jwalk.settings.js", "url": "settings.js" },
{ "name": "jwalk.img", "url": "app-icon.js", "evaluate": true }
]
}

BIN
apps/jwalk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

65
apps/jwalk/settings.js Normal file
View File

@ -0,0 +1,65 @@
(function (back) {
const FILE = "jwalk.json";
const DEFAULTS = {
totalDuration: 30,
intervalDuration: 3,
startMode: 0,
modeBuzzerDuration: 1000,
finishBuzzerDuration: 1500,
showClock: 1,
updateWhileLocked: 0
};
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
function saveSettings() {
require("Storage").writeJSON(FILE, settings);
}
function showSettingsMenu() {
E.showMenu({
'': { title: 'Japanese Walking' },
'< Back': back,
'Total Time (min)': {
value: settings.totalDuration,
min: 10, max: 60, step: 1,
onchange: v => { settings.totalDuration = v; saveSettings(); }
},
'Interval (min)': {
value: settings.intervalDuration,
min: 1, max: 10, step: 1,
onchange: v => { settings.intervalDuration = v; saveSettings(); }
},
'Start Mode': {
value: settings.startMode,
min: 0, max: 1,
format: v => v ? "Intense" : "Relax",
onchange: v => { settings.startMode = v; saveSettings(); }
},
'Display Clock': {
value: settings.showClock,
min: 0, max: 1,
format: v => v ? "Show" : "Hide" ,
onchange: v => { settings.showClock = v; saveSettings(); }
},
'Update UI While Locked': {
value: settings.updateWhileLocked,
min: 0, max: 1,
format: v => v ? "Always" : "On Change",
onchange: v => { settings.updateWhileLocked = v; saveSettings(); }
},
'Mode Buzz (ms)': {
value: settings.modeBuzzerDuration,
min: 0, max: 2000, step: 50,
onchange: v => { settings.modeBuzzerDuration = v; saveSettings(); }
},
'Finish Buzz (ms)': {
value: settings.finishBuzzerDuration,
min: 0, max: 5000, step: 100,
onchange: v => { settings.finishBuzzerDuration = v; saveSettings(); }
},
});
}
showSettingsMenu();
})

View File

@ -117,3 +117,4 @@
Remove workaround for 2v10 (>3 years ago) - assume everyone is on never firmware now Remove workaround for 2v10 (>3 years ago) - assume everyone is on never firmware now
0.86: Default to showing message scroller (with title, bigger icon) 0.86: Default to showing message scroller (with title, bigger icon)
0.87: Make choosing of font size more repeatable 0.87: Make choosing of font size more repeatable
0.88: Adjust padding calculation so messages are spaced out properly even when using international fonts

View File

@ -14,7 +14,7 @@
// a message // a message
require("messages").pushMessage({"t":"add","id":1575479849,"src":"WhatsApp","title":"My Friend","body":"Hey! How's everything going?",reply:1,negative:1}) require("messages").pushMessage({"t":"add","id":1575479849,"src":"WhatsApp","title":"My Friend","body":"Hey! How's everything going?",reply:1,negative:1})
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title":"My Friend","body":"Hey! How's everything going? This is a really really long message that is really so super long you'll have to scroll it lots and lots",positive:1,negative:1}) require("messages").pushMessage({"t":"add","id":1575479850,"src":"Skype","title":"My Friend","body":"Hey! How's everything going? This is a really really long message that is really so super long you'll have to scroll it lots and lots",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":23232,"src":"Skype","title":"Mr. Bobby McBobFace","body":"Boopedy-boop",positive:1,negative:1}) require("messages").pushMessage({"t":"add","id":23232,"src":"Skype","title":"Mr. Bobby McBobFace","body":"Boopedy-boop",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":23233,"src":"Skype","title":"Thyttan test","body":"Nummerplåtsbelysning trodo",positive:1,negative:1}) require("messages").pushMessage({"t":"add","id":23233,"src":"Skype","title":"Thyttan test","body":"Nummerplåtsbelysning trodo",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":23234,"src":"Skype","title":"Thyttan test 2","body":"Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo",positive:1,negative:1}) require("messages").pushMessage({"t":"add","id":23234,"src":"Skype","title":"Thyttan test 2","body":"Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo",positive:1,negative:1})
@ -391,8 +391,6 @@ function showMessage(msgid, persist) {
} }
} }
lines = g.setFont(bodyFont).wrapString(body, w); lines = g.setFont(bodyFont).wrapString(body, w);
if (lines.length<3)
lines.unshift(""); // if less lines, pad them out a bit at the top!
} }
let negHandler,posHandler,rowLeftDraw,rowRightDraw; let negHandler,posHandler,rowLeftDraw,rowRightDraw;
if (msg.negative) { if (msg.negative) {
@ -432,12 +430,14 @@ function showMessage(msgid, persist) {
} }
let fontHeight = g.setFont(bodyFont).getFontHeight(); let fontHeight = g.setFont(bodyFont).getFontHeight();
let lineHeight = (fontHeight>25)?fontHeight:25; let lineHeight = (fontHeight>25)?fontHeight:25;
if (title.includes("\n")) lineHeight=25; // ensure enough room for 2 lines of title in header if (title.includes("\n") && lineHeight<25) lineHeight=25; // ensure enough room for 2 lines of title in header
let linesPerRow = 2; let linesPerRow = 2;
if (fontHeight<17) { if (fontHeight<17) {
lineHeight = 16; lineHeight = 16;
linesPerRow = 3; linesPerRow = 3;
} }
if ((lines.length+4.5)*lineHeight < Bangle.appRect.h)
lines.unshift(""); // if less lines, pad them out a bit at the top!
let rowHeight = lineHeight*linesPerRow; let rowHeight = lineHeight*linesPerRow;
let textLineOffset = -(linesPerRow + ((rowLeftDraw||rowRightDraw)?1:0)); let textLineOffset = -(linesPerRow + ((rowLeftDraw||rowRightDraw)?1:0));
let msgIcon = require("messageicons").getImage(msg); let msgIcon = require("messageicons").getImage(msg);

View File

@ -2,7 +2,7 @@
"id": "messagegui", "id": "messagegui",
"name": "Message UI", "name": "Message UI",
"shortName": "Messages", "shortName": "Messages",
"version": "0.87", "version": "0.88",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android", "description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -1 +1,4 @@
0.01: App Created w/ clockInfos, bold font, date and time. 0.01: App created w/ clockInfos, bold font, date and time.
0.02: Added text truncation for long ClockInfo strings.
0.03: Added small inline Clock Info next to date
0.04: Fix date not clearing

View File

@ -2,18 +2,20 @@
A beautifully simple, modern clock with two Clock Infos, and a clean UI. Fast-Loads. A beautifully simple, modern clock with three Clock Infos, and a clean UI. Fast-Loads.
![](Screenshot1.png) ![](Scr1.png)
## Features ## Features
* Has 2 Clock Infos, that are individually changeable. * Has 3 Clock Infos, that are individually changeable.
* Has an inline Clock Info, next to the date, for quick, glanceable data.
* Low battery consumption. * Low battery consumption.
* Uses locale for time and date. * Uses locale for time and date.
* Bold time font, for quicker readability. * Bold time font, for quicker readability.
* Uses rounded rectangles and a bold font for a simple, clean, modern look.
* Has Fast Loading, for quicker access to launcher. * Has Fast Loading, for quicker access to launcher.

BIN
apps/modclock/Scr1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
apps/modclock/Scr2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -9,6 +9,20 @@ Graphics.prototype.setFontBold = function(scale) {
{ // must be inside our own scope here so that when we are unloaded everything disappears { // must be inside our own scope here so that when we are unloaded everything disappears
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
let drawTimeout; let drawTimeout;
let showInlineClkInfo=true;
let calcStrLength=function(str,maxLength){
//too long
// Example maximum length
var truncatedText = str;
if (str.length > maxLength) {
truncatedText = str.substring(0, maxLength - 3) + "...";
}
return truncatedText;
};
//ROUNDED RECT FUNCTION //ROUNDED RECT FUNCTION
let bRoundedRectangle= function(x1,y1,x2,y2,r) { let bRoundedRectangle= function(x1,y1,x2,y2,r) {
@ -48,7 +62,6 @@ let bRoundedRectangle= function(x1,y1,x2,y2,r) {
}; };
//CLOCK INFO //CLOCK INFO
let clockInfoItems = require("clock_info").load(); let clockInfoItems = require("clock_info").load();
@ -58,7 +71,7 @@ let clockInfoItems = require("clock_info").load();
let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, { let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
// Add the dimensions we're rendering to here - these are used to detect taps on the clock info area // Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
x : 10, y: 100, w: 72, h:70, x : 7, y: 100, w: 76, h:70,
// You can add other information here you want to be passed into 'options' in 'draw' // You can add other information here you want to be passed into 'options' in 'draw'
// This function draws the info // This function draws the info
draw : (itm, info, options) => { draw : (itm, info, options) => {
@ -70,7 +83,7 @@ let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
// indicate focus - we're using a border, but you could change color? // indicate focus - we're using a border, but you could change color?
if (options.focus){ if (options.focus){
// show if focused // show if focused
g.setColor(0,15,255); g.setColor(0,255,15);
bRoundedRectangle(options.x,options.y,options.x+options.w,options.y+options.h,8); bRoundedRectangle(options.x,options.y,options.x+options.w,options.y+options.h,8);
}else{ }else{
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
@ -82,7 +95,7 @@ let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
if (info.img){ if (info.img){
g.drawImage(info.img, midx-12,midy-21); g.drawImage(info.img, midx-12,midy-21);
}// draw the image }// draw the image
g.setFont("Vector",16).setFontAlign(0,1).drawString(info.text, midx,midy+23); // draw the text g.setFont("Vector",16).setFontAlign(0,1).drawString(calcStrLength(info.text,8), midx,midy+23); // draw the text
} }
}); });
@ -91,7 +104,7 @@ let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
//CLOCK INFO RIGHT DIMENSIONS: 97,113, w:66, h: 55 //CLOCK INFO RIGHT DIMENSIONS: 97,113, w:66, h: 55
let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, { let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, {
// Add the dimensions we're rendering to here - these are used to detect taps on the clock info area // Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
x : 94, y: 100, w: 72, h:70, x : 91, y: 100, w: 76, h:70,
// You can add other information here you want to be passed into 'options' in 'draw' // You can add other information here you want to be passed into 'options' in 'draw'
// This function draws the info // This function draws the info
draw : (itm, info, options) => { draw : (itm, info, options) => {
@ -103,7 +116,7 @@ let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, {
// indicate focus - we're using a border, but you could change color? // indicate focus - we're using a border, but you could change color?
if (options.focus){ if (options.focus){
// show if focused // show if focused
g.setColor(0,15,255); g.setColor(0,255,15);
bRoundedRectangle(options.x,options.y,options.x+options.w,options.y+options.h,8); bRoundedRectangle(options.x,options.y,options.x+options.w,options.y+options.h,8);
}else{ }else{
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
@ -115,13 +128,40 @@ let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, {
if (info.img){ if (info.img){
g.drawImage(info.img, midx-12,midy-21); g.drawImage(info.img, midx-12,midy-21);
}// draw the image }// draw the image
g.setFont("Vector",16).setFontAlign(0,1).drawString(info.text, midx,midy+23); // draw the text g.setFont("Vector",16).setFontAlign(0,1).drawString(calcStrLength(info.text,8), midx,midy+23); // draw the text
} }
}); });
let clockInfoMenuInline;
if(showInlineClkInfo){
clockInfoMenuInline = require("clock_info").addInteractive(clockInfoItems, {
// Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
x : g.getWidth()/2+6, y: 69, w: 95, h:25,
// You can add other information here you want to be passed into 'options' in 'draw'
// This function draws the info
draw : (itm, info, options) => {
// itm: the item containing name/hasRange/etc
// info: data returned from itm.get() containing text/img/etc
// options: options passed into addInteractive
// Clear the background
g.reset().clearRect(options.x, options.y, options.x+options.w, options.y+options.h);
// indicate focus - we're using a border, but you could change color?
if (options.focus){
// show if focused
g.setColor(0,255,15);
}
// we're drawing center-aligned here
//var midx = options.x+options.w/2;
var midy=options.y+options.h/2;
if (info.img){
g.drawImage(info.img, options.x+4,midy-7.2,{scale: 0.63});
}// draw the image
g.setFont("Vector",15).setFontAlign(-1,0).drawString(calcStrLength(info.text,6), options.x+22,midy+1); // draw the text
}
});
}
@ -149,13 +189,13 @@ let draw = function() {
var meridianStrWidth=g.stringWidth(meridian); var meridianStrWidth=g.stringWidth(meridian);
var totalStrWidth=meridianStrWidth+padding+clkStrWidth; var totalStrWidth=meridianStrWidth+padding+clkStrWidth;
var startX = ((g.getWidth() - totalStrWidth) / 2)+6; var startX = ((g.getWidth() - totalStrWidth) / 2)+6;
g.clearRect(0,0,g.getWidth(),90); g.clearRect(0,0,g.getWidth(),64);
g.setFontBold(); g.setFontBold();
g.setFontAlign(-1,1); g.setFontAlign(-1,1);
g.drawString(clock, startX, Y+2,true); g.drawString(clock, startX, Y-2);
g.setFont("Vector",20); g.setFont("Vector",20);
g.setFontAlign(-1,1); g.setFontAlign(-1,1);
g.drawString(meridian, startX + clkStrWidth + padding, Y-5, true); g.drawString(meridian, startX + clkStrWidth + padding, Y-9, true);
}else{ }else{
@ -165,15 +205,17 @@ let draw = function() {
} }
// draw the date, in a normal font // draw the date, in a normal font
g.setFont("Vector",18); g.setFont("Vector",16);
g.setFontAlign(0,0); // align center bottom g.setFontAlign(1,0); // align center bottom
// pad the date - this clears the background if the date were to change length // pad the date - this clears the background if the date were to change length
var dateStr = require("locale").dow(new Date(), 1)+", "+ require("locale").month(new Date(), true)+" "+new Date().getDate(); var dateStr = " "+require("locale").dow(new Date(), 1)+", " +new Date().getDate();
g.drawString(" "+dateStr+" ", g.getWidth()/2, Y+9, true /*clear background*/); //print(g.stringHeight(dateStr));
g.drawString(" "+dateStr+" ", g.getWidth()/2+6, Y+9, true /*clear background*/);
Bangle.drawWidgets(); Bangle.drawWidgets();
g.setColor("#ffffff");
// queue next draw // queue next draw
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
@ -196,6 +238,7 @@ Bangle.setUI({
delete Graphics.prototype.setFontBold; delete Graphics.prototype.setFontBold;
clockInfoMenuRight.remove(); clockInfoMenuRight.remove();
clockInfoMenuLeft.remove(); clockInfoMenuLeft.remove();
if(showInlineClkInfo) clockInfoMenuInline.remove();
}}); }});
@ -203,4 +246,6 @@ g.clear();
// Load widgets // Load widgets
Bangle.loadWidgets(); Bangle.loadWidgets();
draw(); draw();
} }

View File

@ -3,13 +3,13 @@
"name": "Modern Clock", "name": "Modern Clock",
"shortName":"Modern Clk", "shortName":"Modern Clk",
"icon": "icon.png", "icon": "icon.png",
"version":"0.01", "version":"0.04",
"description": "A modern, simple clock, with two Clock Infos and Fast Loading", "description": "A modern, simple clock, with three ClockInfos and Fast Loading",
"type":"clock", "type":"clock",
"tags": "clock,clkinfo", "tags": "clock,clkinfo",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"screenshots" : [ { "url":"Screenshot1.png" }, "screenshots" : [ { "url":"Scr1.png" },
{ "url":"Screenshot2.png" } ], { "url":"Scr2.png" } ],
"dependencies" : { "clock_info":"module"}, "dependencies" : { "clock_info":"module"},
"allow_emulator":true, "allow_emulator":true,
"readme":"README.md", "readme":"README.md",

View File

@ -13,3 +13,4 @@
0.10: Use charging state on boot for auto calibration 0.10: Use charging state on boot for auto calibration
Log additional timestamp for trace log Log additional timestamp for trace log
0.11: Minor code improvements 0.11: Minor code improvements
0.12: Round monotonic percentage, rename to stable percentage/voltage

View File

@ -29,6 +29,8 @@
} }
} }
setInterval(save, saveEvery); setInterval(save, saveEvery);
E.on("kill", ()=>{ E.on("kill", ()=>{
@ -75,7 +77,7 @@
})(Bangle[functionName]); })(Bangle[functionName]);
} }
let functions = {};
let wrapDeferred = ((o,t) => (a) => { let wrapDeferred = ((o,t) => (a) => {
if (a == eval || typeof a == "string") { if (a == eval || typeof a == "string") {
return o.apply(this, arguments); return o.apply(this, arguments);
@ -131,19 +133,25 @@
handleCharging(Bangle.isCharging()); handleCharging(Bangle.isCharging());
} }
var savedBatPercent=E.getBattery();
if (settings.forceMonoPercentage){ if (settings.forceMonoPercentage){
var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4; var newPercent =Math.round((E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/6);
var op = E.getBattery;
E.getBattery = function() { E.getBattery = function() {
var current = Math.round((op()+op()+op()+op())/4);
if (Bangle.isCharging() && current > p) p = current; if(Bangle.isCharging()){
if (!Bangle.isCharging() && current < p) p = current; if(newPercent > savedBatPercent)
return p; savedBatPercent = newPercent;
}else{
if(newPercent < savedBatPercent)
savedBatPercent = newPercent;
}
return savedBatPercent;
}; };
} }
if (settings.forceMonoVoltage){ if (settings.forceMonoVoltage){
var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/4; var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/6;
var ov = NRF.getBattery; var ov = NRF.getBattery;
NRF.getBattery = function() { NRF.getBattery = function() {
var current = (ov()+ov()+ov()+ov())/4; var current = (ov()+ov()+ov()+ov())/4;

View File

@ -2,8 +2,8 @@
"id": "powermanager", "id": "powermanager",
"name": "Power Manager", "name": "Power Manager",
"shortName": "Power Manager", "shortName": "Power Manager",
"version": "0.11", "version": "0.12",
"description": "Allow configuration of warnings and thresholds for battery charging and display.", "description": "Allow configuration of warnings for battery charging, stabilization of voltage, stabilization of battery percentage, and battery logging.",
"icon": "app.png", "icon": "app.png",
"type": "bootloader", "type": "bootloader",
"tags": "tool", "tags": "tool",

View File

@ -27,13 +27,13 @@
'Widget': function() { 'Widget': function() {
E.showMenu(submenu_widget); E.showMenu(submenu_widget);
}, },
'Monotonic percentage': { 'Stable Percentage': {
value: !!settings.forceMonoPercentage, value: !!settings.forceMonoPercentage,
onchange: v => { onchange: v => {
writeSettings("forceMonoPercentage", v); writeSettings("forceMonoPercentage", v);
} }
}, },
'Monotonic voltage': { 'Stable Voltage': {
value: !!settings.forceMonoVoltage, value: !!settings.forceMonoVoltage,
onchange: v => { onchange: v => {
writeSettings("forceMonoVoltage", v); writeSettings("forceMonoVoltage", v);

View File

@ -3,3 +3,4 @@
0.03: Add settings to configure prompt 0.03: Add settings to configure prompt
0.04: Minor code improvements 0.04: Minor code improvements
0.05: Comment out unused function in settings.js 0.05: Comment out unused function in settings.js
0.06: Changed to use E.showMessage() instead of g.drawString() to display power off message. Looks better and is more coherent.

View File

@ -19,20 +19,15 @@ if (showPrompt) {
setTimeout(load, 100); setTimeout(load, 100);
return; return;
} }
g.setFont("6x8",2).setFontAlign(0,0);
var x = g.getWidth()/2; E.showMessage("Powering off...");
var y = g.getHeight()/2 + 10;
g.drawString("Powering off...", x, y);
setTimeout(function() { setTimeout(function() {
if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); if (Bangle.softOff) Bangle.softOff(); else Bangle.off();
}, 1000); }, 1000);
}); });
} else { } else {
g.setFont("6x8",2).setFontAlign(0,0); E.showMessage("Powering off...");
var x = g.getWidth()/2;
var y = g.getHeight()/2 + 10;
g.drawString("Powering off...", x, y);
setTimeout(function() { setTimeout(function() {
if (Bangle.softOff) Bangle.softOff(); else Bangle.off(); if (Bangle.softOff) Bangle.softOff(); else Bangle.off();

View File

@ -1,7 +1,7 @@
{ "id": "poweroff", { "id": "poweroff",
"name": "Poweroff", "name": "Poweroff",
"shortName":"Poweroff", "shortName":"Poweroff",
"version": "0.05", "version": "0.06",
"description": "Simple app to power off your Bangle.js", "description": "Simple app to power off your Bangle.js",
"icon": "app.png", "icon": "app.png",
"tags": "tool,poweroff,shutdown", "tags": "tool,poweroff,shutdown",

View File

@ -16,3 +16,4 @@
0.12: Fix bug where settings would behave as if all were set to false 0.12: Fix bug where settings would behave as if all were set to false
0.13: Update to new touch-event handling 0.13: Update to new touch-event handling
0.14: Fix bug in handling changes to `Bangle.appRect` 0.14: Fix bug in handling changes to `Bangle.appRect`
0.15: Workaround bug in 2v26/2v27 firmware

View File

@ -2,7 +2,11 @@ var _a, _b;
var prosettings = (require("Storage").readJSON("promenu.settings.json", true) || {}); var prosettings = (require("Storage").readJSON("promenu.settings.json", true) || {});
(_a = prosettings.naturalScroll) !== null && _a !== void 0 ? _a : (prosettings.naturalScroll = false); (_a = prosettings.naturalScroll) !== null && _a !== void 0 ? _a : (prosettings.naturalScroll = false);
(_b = prosettings.wrapAround) !== null && _b !== void 0 ? _b : (prosettings.wrapAround = true); (_b = prosettings.wrapAround) !== null && _b !== void 0 ? _b : (prosettings.wrapAround = true);
E.showMenu = function (items) { E.showMenu = (function (items) {
if (items == null) {
g.clearRect(Bangle.appRect);
return Bangle.setUI();
}
var RectRnd = function (x1, y1, x2, y2, r) { var RectRnd = function (x1, y1, x2, y2, r) {
var pp = []; var pp = [];
pp.push.apply(pp, g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r])); pp.push.apply(pp, g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r]));
@ -122,7 +126,6 @@ E.showMenu = function (items) {
nameScroll_1 = 0; nameScroll_1 = 0;
}, 300, name, v, item, idx, x, iy); }, 300, name, v, item, idx, x, iy);
} }
g.setColor(g.theme.fg);
iy += fontHeight; iy += fontHeight;
idx++; idx++;
}; };
@ -204,7 +207,10 @@ E.showMenu = function (items) {
else else
l.select(evt); l.select(evt);
}; };
Bangle.setUI({ var touchcb = (function (_button, xy) {
cb(void 0, xy);
});
var uiopts = {
mode: "updown", mode: "updown",
back: back, back: back,
remove: function () { remove: function () {
@ -212,11 +218,20 @@ E.showMenu = function (items) {
if (nameScroller) if (nameScroller)
clearInterval(nameScroller); clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe); Bangle.removeListener("swipe", onSwipe);
if (setUITouch)
Bangle.removeListener("touch", touchcb);
(_a = options.remove) === null || _a === void 0 ? void 0 : _a.call(options); (_a = options.remove) === null || _a === void 0 ? void 0 : _a.call(options);
}, },
touch: (function (_button, xy) { };
cb(void 0, xy); var setUITouch = process.env.VERSION >= "2v26";
}), if (!setUITouch) {
}, cb); uiopts.touch = touchcb;
}
Bangle.setUI(uiopts, cb);
if (setUITouch) {
Bangle.removeListener("touch", Bangle.touchHandler);
delete Bangle.touchHandler;
Bangle.on("touch", touchcb);
}
return l; return l;
}; });

View File

@ -13,7 +13,12 @@ const prosettings = (require("Storage").readJSON("promenu.settings.json", true)
prosettings.naturalScroll ??= false; prosettings.naturalScroll ??= false;
prosettings.wrapAround ??= true; prosettings.wrapAround ??= true;
E.showMenu = (items?: Menu): MenuInstance => { E.showMenu = ((items?: Menu): MenuInstance | void => {
if(items == null){
g.clearRect(Bangle.appRect);
return Bangle.setUI();
}
const RectRnd = (x1: number, y1: number, x2: number, y2: number, r: number) => { const RectRnd = (x1: number, y1: number, x2: number, y2: number, r: number) => {
const pp = []; const pp = [];
pp.push(...g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r])); pp.push(...g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r]));
@ -167,7 +172,6 @@ E.showMenu = (items?: Menu): MenuInstance => {
}, 300, name, v, item, idx, x, iy); }, 300, name, v, item, idx, x, iy);
} }
g.setColor(g.theme.fg);
iy += fontHeight; iy += fontHeight;
idx++; idx++;
} }
@ -252,21 +256,47 @@ E.showMenu = (items?: Menu): MenuInstance => {
else l.select(evt); else l.select(evt);
}; };
Bangle.setUI({ const touchcb = ((_button, xy) => {
mode: "updown",
back,
remove: () => {
if (nameScroller) clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe);
options.remove?.();
},
touch: ((_button, xy) => {
// since we've specified options.touch, // since we've specified options.touch,
// we need to pass through all taps since the default // we need to pass through all taps since the default
// touchHandler isn't installed in setUI // touchHandler isn't installed in setUI
cb(void 0, xy); cb(void 0, xy);
}) satisfies TouchCallback, }) satisfies TouchCallback;
} as SetUIArg<"updown">, cb);
const uiopts = {
mode: "updown",
back: back as () => void,
remove: () => {
if (nameScroller) clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe);
if(setUITouch)
Bangle.removeListener("touch", touchcb);
options.remove?.();
},
} satisfies SetUIArg<"updown">;
// does setUI install its own touch handler?
const setUITouch = process.env.VERSION >= "2v26";
if (!setUITouch) {
// old firmware, we can use its touch handler - no need for workaround
(uiopts as any).touch = touchcb;
}
Bangle.setUI(uiopts, cb);
if(setUITouch){
// new firmware, remove setUI's touch handler and use just our own to
// avoid `cb` drawing the menu (as part of setUI's touch handler)
// followed by us drawing the menu (as part of our touch handler)
//
// work around details:
// - https://github.com/espruino/Espruino/issues/2648
// - https://github.com/orgs/espruino/discussions/7697#discussioncomment-13782299
Bangle.removeListener("touch", (Bangle as any).touchHandler);
delete (Bangle as any).touchHandler;
Bangle.on("touch", touchcb);
}
return l; return l;
}; }) as typeof E.showMenu;

View File

@ -1,7 +1,7 @@
{ {
"id": "promenu", "id": "promenu",
"name": "Pro Menu", "name": "Pro Menu",
"version": "0.14", "version": "0.15",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png", "icon": "icon.png",
"type": "bootloader", "type": "bootloader",

View File

@ -88,3 +88,6 @@ of 'Select Clock'
0.77: Save altitude calibration when user exits via reset 0.77: Save altitude calibration when user exits via reset
0.78: Fix menu scroll restore on BangleJS1 0.78: Fix menu scroll restore on BangleJS1
0.79: Ensure that tapping on pressure/altitude doesn't cause a menu to display temporarily 0.79: Ensure that tapping on pressure/altitude doesn't cause a menu to display temporarily
0.80: Add option to set LCD brightness to 0, keep default brightness at 1.
0.81: Ensure 'Privacy' (address randomnisation) is only added for Bangle.js 2,
and add warnings in documentation that it can cause connection issues

View File

@ -45,6 +45,8 @@ This is where you adjust settings for an individual app. (eg. Health app: Adjust
* **Passkey** allows you to set a passkey that is required to connect and pair to Bangle.js. * **Passkey** allows you to set a passkey that is required to connect and pair to Bangle.js.
* **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app. * **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app.
* **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again. * **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again.
* **Privacy** - (Bangle.js 2 only) enables BLE privacy mode (see [NRF.setSecurity](https://www.espruino.com/Reference#l_NRF_setSecurity)). This randomises the Bangle's MAC address and can also
remove advertising of its name. **This can cause connection issues with apps that expect to keep a permanent connection like iOS/Gadgetbridge**
## LCD ## LCD

View File

@ -1,7 +1,7 @@
{ {
"id": "setting", "id": "setting",
"name": "Settings", "name": "Settings",
"version": "0.79", "version": "0.81",
"description": "A menu for setting up Bangle.js", "description": "A menu for setting up Bangle.js",
"icon": "settings.png", "icon": "settings.png",
"tags": "tool,system", "tags": "tool,system",

View File

@ -1,3 +1,4 @@
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
@ -191,7 +192,7 @@ function BLEMenu() {
var hidN = [/*LANG*/"Off", /*LANG*/"Kbrd & Media", /*LANG*/"Kbrd", /*LANG*/"Kbrd & Mouse", /*LANG*/"Joystick"]; var hidN = [/*LANG*/"Off", /*LANG*/"Kbrd & Media", /*LANG*/"Kbrd", /*LANG*/"Kbrd & Mouse", /*LANG*/"Joystick"];
var privacy = [/*LANG*/"Off", /*LANG*/"Show name", /*LANG*/"Hide name"]; var privacy = [/*LANG*/"Off", /*LANG*/"Show name", /*LANG*/"Hide name"];
return { var menu = {
'': { 'title': /*LANG*/'Bluetooth' }, '': { 'title': /*LANG*/'Bluetooth' },
'< Back': ()=>popMenu(mainMenu()), '< Back': ()=>popMenu(mainMenu()),
/*LANG*/'Make Connectable': ()=>makeConnectable(), /*LANG*/'Make Connectable': ()=>makeConnectable(),
@ -209,7 +210,32 @@ function BLEMenu() {
updateSettings(); updateSettings();
} }
}, },
/*LANG*/'Privacy': { /*LANG*/'HID': {
value: Math.max(0,0 | hidV.indexOf(settings.HID)),
min: 0, max: hidN.length-1,
format: v => hidN[v],
onchange: v => {
settings.HID = hidV[v];
updateSettings();
}
},
/*LANG*/'Passkey': {
value: settings.passkey?settings.passkey:/*LANG*/"none",
onchange: () => setTimeout(() => pushMenu(passkeyMenu())) // graphical_menu redraws after the call
},
/*LANG*/'Whitelist': {
value:
(
(settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"Off" : /*LANG*/"On"
) + (
settings.whitelist
? " (" + settings.whitelist.length + ")"
: ""
),
onchange: () => setTimeout(() => pushMenu(whitelistMenu())) // graphical_menu redraws after the call
}
};
if (BANGLEJS2) menu[/*LANG*/'Privacy'] = {
min: 0, max: privacy.length-1, min: 0, max: privacy.length-1,
format: v => privacy[v], format: v => privacy[v],
value: (() => { value: (() => {
@ -234,32 +260,8 @@ function BLEMenu() {
} }
updateSettings(); updateSettings();
} }
},
/*LANG*/'HID': {
value: Math.max(0,0 | hidV.indexOf(settings.HID)),
min: 0, max: hidN.length-1,
format: v => hidN[v],
onchange: v => {
settings.HID = hidV[v];
updateSettings();
}
},
/*LANG*/'Passkey': {
value: settings.passkey?settings.passkey:/*LANG*/"none",
onchange: () => setTimeout(() => pushMenu(passkeyMenu())) // graphical_menu redraws after the call
},
/*LANG*/'Whitelist': {
value:
(
(settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"off" : /*LANG*/"on"
) + (
settings.whitelist
? " (" + settings.whitelist.length + ")"
: ""
),
onchange: () => setTimeout(() => pushMenu(whitelistMenu())) // graphical_menu redraws after the call
}
}; };
return menu;
} }
function showThemeMenu(pop) { function showThemeMenu(pop) {
@ -474,11 +476,11 @@ function LCDMenu() {
Object.assign(lcdMenu, { Object.assign(lcdMenu, {
/*LANG*/'LCD Brightness': { /*LANG*/'LCD Brightness': {
value: settings.brightness, value: settings.brightness,
min: 0.1, min : BANGLEJS2 ? 0 : 0.1,
max: 1, max: 1,
step: 0.1, step: 0.1,
onchange: v => { onchange: v => {
settings.brightness = v || 1; settings.brightness = v ?? 1;
updateSettings(); updateSettings();
Bangle.setLCDBrightness(settings.brightness); Bangle.setLCDBrightness(settings.brightness);
} }

View File

@ -15,3 +15,4 @@
0.17: Minor code improvements 0.17: Minor code improvements
0.18: Add back as a function to prevent translation making it a menu entry 0.18: Add back as a function to prevent translation making it a menu entry
0.19: Write sleep state into health event's .activity field 0.19: Write sleep state into health event's .activity field
0.20: Increase default sleep thresholds, tweak settings to allow higher ones to be chosen

View File

@ -11,8 +11,8 @@ global.sleeplog = {
// threshold settings // threshold settings
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
deepTh: 100, // threshold for deep sleep deepTh: 150, // threshold for deep sleep
lightTh: 200, // threshold for light sleep lightTh: 300, // threshold for light sleep
wearTemp: 19.5, // temperature threshold to count as worn wearTemp: 19.5, // temperature threshold to count as worn
}, require("Storage").readJSON("sleeplog.json", true) || {}) }, require("Storage").readJSON("sleeplog.json", true) || {})
}; };

View File

@ -2,7 +2,7 @@
"id":"sleeplog", "id":"sleeplog",
"name":"Sleep Log", "name":"Sleep Log",
"shortName": "SleepLog", "shortName": "SleepLog",
"version": "0.19", "version": "0.20",
"description": "Log and view your sleeping habits. This app uses built in movement calculations. View data from Bangle, or from the web app.", "description": "Log and view your sleeping habits. This app uses built in movement calculations. View data from Bangle, or from the web app.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -11,8 +11,8 @@
// threshold settings // threshold settings
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
deepTh: 100, // threshold for deep sleep deepTh: 150, // threshold for deep sleep
lightTh: 200, // threshold for light sleep lightTh: 300, // threshold for light sleep
wearTemp: 19.5, // temperature threshold to count as worn wearTemp: 19.5, // temperature threshold to count as worn
// app settings // app settings
breakToD: 12, // [h] time of day when to start/end graphs breakToD: 12, // [h] time of day when to start/end graphs
@ -324,9 +324,9 @@
}, },
/*LANG*/"Deep Sleep": { /*LANG*/"Deep Sleep": {
value: settings.deepTh, value: settings.deepTh,
step: 1, step: 10,
min: 30, min: 30,
max: 200, max: 500,
wrap: true, wrap: true,
noList: true, noList: true,
onchange: v => { onchange: v => {
@ -338,7 +338,7 @@
value: settings.lightTh, value: settings.lightTh,
step: 10, step: 10,
min: 100, min: 100,
max: 400, max: 800,
wrap: true, wrap: true,
noList: true, noList: true,
onchange: v => { onchange: v => {

View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Added pie chart for visualization, tweaked UI.
0.03: Fixed bug with total storage pie chart.

View File

@ -0,0 +1,154 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../core/lib/customize.js"></script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<!-- Toggle Buttons -->
<div class="btn-group" style="margin: 1em; display: flex; justify-content: center;">
<button id="tableButton" class="btn btn-primary">View Table</button>
<button id="pieChartButton" class="btn">View Pie Chart</button>
</div>
<!-- Table View -->
<div id="storageTable"></div>
<!-- Chart View -->
<div id="storagePieChart" style="display: none; flex-direction: column; align-items: center;">
<div id="piechart" style="width: 100%; max-width: 600px; height: 400px;"></div>
<div id="totalStoragePie" style="width: 100%; max-width: 600px; height: 300px;"></div>
</div>
<script>
let globalApps = [];
let storageStats = null;
function onInit(device) {
Util.showModal("Reading Storage...");
Puck.eval(`(()=>{
let getApps = () => require("Storage").list(/\\.info$/).map(appInfoName => {
let appInfo = require("Storage").readJSON(appInfoName,1)||{};
var fileSize = 0, dataSize = 0;
appInfo.files.split(",").forEach(f => fileSize += require("Storage").read(f).length);
var data = (appInfo.data||"").split(";");
function wildcardToRegexp(wc) {
return new RegExp("^"+wc.replaceAll(".","\\\\.").replaceAll("?",".*")+"$");
}
if (data[0]) data[0].split(",").forEach(wc => {
require("Storage").list(wildcardToRegexp(wc), {sf:false}).forEach(f => {
dataSize += require("Storage").read(f).length
});
});
if (data[1]) data[1].split(",").forEach(wc => {
require("Storage").list(wildcardToRegexp(wc), {sf:true}).forEach(f => {
dataSize += require("Storage").open(f,"r").getLength();
});
});
return [appInfo.id, fileSize, dataSize];
});
return [getApps(), require(\"Storage\").getStats()]; })()`, function(result) {
Util.hideModal();
globalApps = result[0].sort((a,b) => (b[1]+b[2]) - (a[1]+a[2]));
storageStats = result[1];
if (globalApps.length === 0) {
document.getElementById("storageTable").innerHTML = "<p>No apps found</p>";
return;
}
drawTable();
});
}
function roundDecimal(num){
return Math.round(num * 10) / 10;
}
function drawTable() {
document.getElementById("storageTable").innerHTML = `
<table class="table table-striped">
<thead>
<tr>
<th>App</th>
<th>Code (kb)</th>
<th>Data (kb)</th>
<th>Total (kb)</th>
</tr>
</thead>
<tbody>
${globalApps.map(app => `
<tr>
<td>${app[0]}</td>
<td>${(app[1]/1000).toFixed(1)}</td>
<td>${(app[2]/1000).toFixed(1)}</td>
<td>${((app[1]+app[2])/1000).toFixed(1)}</td>
</tr>`).join("")}
</tbody>
</table>`;
}
function drawChart() {
if (globalApps.length === 0) return;
// App-specific chart
const chartData = [
['App', 'Total Size (KB)']
].concat(globalApps.map(app => [app[0], roundDecimal((app[1] + app[2])/1000)]));
const data = google.visualization.arrayToDataTable(chartData);
const options = {
title: 'App Storage Breakdown (KBs)',
chartArea: { width: '90%', height: '80%' },
legend: { position: 'bottom' }
};
const chart = new google.visualization.PieChart(document.getElementById('piechart'));
chart.draw(data, options);
// Total storage chart
if (storageStats) {
const usedKB = roundDecimal(storageStats.fileBytes / 1000);
const freeKB = roundDecimal(storageStats.freeBytes / 1000);
const trashKB = roundDecimal(storageStats.trashBytes / 1000);
const totalData = google.visualization.arrayToDataTable([
['Type', 'KB'],
['Used', usedKB],
['Free', freeKB],
['Trash', trashKB],
]);
const totalOptions = {
title: 'Total Storage Usage (KBs)',
chartArea: { width: '90%', height: '80%' },
legend: { position: 'bottom' }
};
const totalChart = new google.visualization.PieChart(document.getElementById('totalStoragePie'));
totalChart.draw(totalData, totalOptions);
}
}
google.charts.load('current', {'packages':['corechart']});
document.getElementById("pieChartButton").addEventListener("click", function () {
document.getElementById("storageTable").style.display = "none";
document.getElementById("storagePieChart").style.display = "flex";
drawChart();
this.classList.add("btn-primary");
document.getElementById("tableButton").classList.remove("btn-primary");
});
document.getElementById("tableButton").addEventListener("click", function () {
document.getElementById("storageTable").style.display = "block";
document.getElementById("storagePieChart").style.display = "none";
drawTable();
this.classList.add("btn-primary");
document.getElementById("pieChartButton").classList.remove("btn-primary");
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,13 @@
{
"id": "storageanalyzer",
"name": "Storage Analyzer",
"version": "0.03",
"description": "Analyzes Bangle.js storage and shows which apps are using storage space",
"icon": "icon.png",
"type": "RAM",
"tags": "tool,storage,flash,memory",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": []
}

View File

@ -6,3 +6,4 @@
0.05: Make the "App source not found" warning less buggy 0.05: Make the "App source not found" warning less buggy
0.06: Fixed a crash if an app has no tags (app.tags is undefined) 0.06: Fixed a crash if an app has no tags (app.tags is undefined)
0.07: Clear cached app list when updating showClocks setting 0.07: Clear cached app list when updating showClocks setting
0.08: Add haptic feedback option when selecting app or category in menu, increase vector size limit in settings

View File

@ -11,6 +11,7 @@ Settings
- `Font` - The font used (`4x6`, `6x8`, `12x20`, `6x15` or `Vector`). Default `12x20`. - `Font` - The font used (`4x6`, `6x8`, `12x20`, `6x15` or `Vector`). Default `12x20`.
- `Vector Font Size` - The size of the font if `Font` is set to `Vector`. Default `10`. - `Vector Font Size` - The size of the font if `Font` is set to `Vector`. Default `10`.
- `Haptic Feedback` - Whether or not to vibrate slightly when selecting an app or category in the launcher. Default `No`.
- `Show Clocks` - If set to `No` then clocks won't appear in the app list. Default `Yes`. - `Show Clocks` - If set to `No` then clocks won't appear in the app list. Default `Yes`.
- `Fullscreen` - If set to `Yes` then widgets won't be loaded. Default `No`. - `Fullscreen` - If set to `Yes` then widgets won't be loaded. Default `No`.
@ -28,3 +29,4 @@ Contributors
- [atjn](https://github.com/atjn) - [atjn](https://github.com/atjn)
- [BlueFox4](https://github.com/BlueFox4) - [BlueFox4](https://github.com/BlueFox4)
- [RKBoss6](https://github.com/RKBoss6)

View File

@ -17,7 +17,8 @@ let vectorval = 20;
let font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; let font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
let settings = Object.assign({ let settings = Object.assign({
showClocks: true, showClocks: true,
fullscreen: false fullscreen: false,
buzz:false
}, s.readJSON("taglaunch.json", true) || {}); }, s.readJSON("taglaunch.json", true) || {});
if ("vectorsize" in settings) if ("vectorsize" in settings)
vectorval = parseInt(settings.vectorsize); vectorval = parseInt(settings.vectorsize);
@ -108,6 +109,7 @@ let showTagMenu = (tag) => {
} }
}, },
select : i => { select : i => {
const loadApp = () => {
let app = appsByTag[tag][i]; let app = appsByTag[tag][i];
if (!app) return; if (!app) return;
if (!app.src || require("Storage").read(app.src)===undefined) { if (!app.src || require("Storage").read(app.src)===undefined) {
@ -117,6 +119,15 @@ let showTagMenu = (tag) => {
} else { } else {
load(app.src); load(app.src);
} }
};
if(settings.buzz){
Bangle.buzz(25);
//let the buzz have effect
setTimeout(loadApp,27);
}else{
loadApp();
}
}, },
back : showMainMenu, back : showMainMenu,
remove: unload remove: unload
@ -138,6 +149,7 @@ let showMainMenu = () => {
} }
}, },
select : i => { select : i => {
if(settings.buzz)Bangle.buzz(25);
let tag = tagKeys[i]; let tag = tagKeys[i];
showTagMenu(tag); showTagMenu(tag);
}, },

View File

@ -2,8 +2,8 @@
"id": "taglaunch", "id": "taglaunch",
"name": "Tag Launcher", "name": "Tag Launcher",
"shortName": "Taglauncher", "shortName": "Taglauncher",
"version": "0.07", "version": "0.08",
"description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.", "description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access from the default launcher.",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",
"type": "launch", "type": "launch",

View File

@ -2,7 +2,8 @@
(function(back) { (function(back) {
let settings = Object.assign({ let settings = Object.assign({
showClocks: true, showClocks: true,
fullscreen: false fullscreen: false,
buzz:false
}, require("Storage").readJSON("taglaunch.json", true) || {}); }, require("Storage").readJSON("taglaunch.json", true) || {});
let fonts = g.getFonts(); let fonts = g.getFonts();
@ -21,9 +22,16 @@
}, },
/*LANG*/"Vector Font Size": { /*LANG*/"Vector Font Size": {
value: settings.vectorsize || 10, value: settings.vectorsize || 10,
min:10, max: 20,step:1,wrap:true, min:10, max: 25,step:1,wrap:true,
onchange: (m) => {save("vectorsize", m)} onchange: (m) => {save("vectorsize", m)}
}, },
/*LANG*/"Haptic Feedback": {
value: settings.buzz == true,
onchange: (m) => {
save("buzz", m);
}
},
/*LANG*/"Show Clocks": { /*LANG*/"Show Clocks": {
value: settings.showClocks == true, value: settings.showClocks == true,
onchange: (m) => { onchange: (m) => {

View File

@ -4,3 +4,4 @@
0.04: Get time zone from settings for showing the clock 0.04: Get time zone from settings for showing the clock
0.05: Minor code improvements 0.05: Minor code improvements
0.06: Adjust format of title, save counter before leaving help screen 0.06: Adjust format of title, save counter before leaving help screen
0.07: Refactor code, fix stuttering timer, add settings menu

View File

@ -3,7 +3,7 @@
A simple timer. You can easily set up the time. The initial time is 2:30 A simple timer. You can easily set up the time. The initial time is 2:30
On the first screen, you can On the first screen, you can
- tap to get help - double tap to get help
- swipe up/down to change the timer by +/- one minute - swipe up/down to change the timer by +/- one minute
- swipe left/right to change the time by +/- 15 seconds - swipe left/right to change the time by +/- 15 seconds
- press Btn1 to start - press Btn1 to start
@ -12,24 +12,31 @@ Press Btn1 again to stop the timer
- when time is up, your Bangle will buzz for 15 seconds - when time is up, your Bangle will buzz for 15 seconds
- and it will count up to 60 seconds and stop after that - and it will count up to 60 seconds and stop after that
## Images The time changes can be adjusted in the settings menu.
_1. Startscreen_
![](TeatimerStart.jpg) ## Images
_1. Start screen_
![](TeatimerStart.png)
Current time is displayed below the Title. Initial time is 2:30. Current time is displayed below the Title. Initial time is 2:30.
_2. Help Screen_ _2. Help Screen_
![](TeatimerHelp.jpg) ![](TeatimerHelp.png)
_3. Tea Timer running_ _3. Tea Timer running_
![](TeatimerRun.jpg) ![](TeatimerRun.png)
Remainig time is shown in big font size. Above the initial time is shown. Remainig time is shown in big font size.
_4. When time is up_ _4. Pause Timer
![](TeatimerUp.jpg) ![](TeatimerPause.png)
While the timer is running, you can pause and unpause it by pressing BTN1.
_5. When time is up_
![](TeatimerUp.png)
When time is up, the watch will buzz for 15 seconds. It will count up to 60 seconds. When time is up, the watch will buzz for 15 seconds. It will count up to 60 seconds.
## Requests ## Requests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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