Merge branch 'espruino:master' into iosWeatherUpdates

master
RKBoss6 2025-07-01 09:05:06 -04:00 committed by GitHub
commit 2d8b1e8218
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 692 additions and 80 deletions

View File

@ -16,3 +16,4 @@
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

View File

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

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.18",
"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

@ -34,4 +34,7 @@
0.30: Minor code improvements 0.30: Minor code improvements
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)

View File

@ -41,23 +41,23 @@ 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
@ -66,23 +66,24 @@ exports.getDecoder = function(fileContents) {
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, 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

@ -2,7 +2,7 @@
"id": "health", "id": "health",
"name": "Health Tracking", "name": "Health Tracking",
"shortName": "Health", "shortName": "Health",
"version": "0.32", "version": "0.34",
"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",
"tags": "tool,system,health", "tags": "tool,system,health",

View File

@ -2,6 +2,8 @@
Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these. Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these.
**WARNING:** On Bangle.js 2 this has been found to make heart rate readings [substantially less accurate in some cases](https://github.com/orgs/espruino/discussions/7738#discussioncomment-13594093) (the HRM already has built in motion artefact removal).
## Settings ## Settings
* **MA removal** * **MA removal**

View File

@ -4,7 +4,7 @@
"shortName":"HRM MA removal", "shortName":"HRM MA removal",
"icon": "app.png", "icon": "app.png",
"version":"0.02", "version":"0.02",
"description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data.", "description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data. **WARNING:** On Bangle.js 2 this has been found to make heart rate readings substantially less accurate in some cases.",
"type": "bootloader", "type": "bootloader",
"tags": "health", "tags": "health",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],

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

3
apps/modclock/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
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

26
apps/modclock/README.md Normal file
View File

@ -0,0 +1,26 @@
# Modern Clock
A beautifully simple, modern clock with three Clock Infos, and a clean UI. Fast-Loads.
![](Scr1.png)
## Features
* Has 3 Clock Infos, that are individually changeable.
* Has an inline Clock Info, next to the date, for quick, glanceable data.
* Low battery consumption.
* Uses locale for time and date.
* 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.
## Creator
RKBoss6
Github: https://github.com/RKBoss6

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4kA///00x3u+pPK8+xxXCxHCn+/xO6tO6o0ZjFt+sjw0xudkhHCMf4A/AH4A/ADECkUiuUnBAe7AAm85gAK5YWChYRMAA2wC4Xq1QAQ13gC4cRAA3RBA8RC4/dCYfdAoIADjQXJjvRCYMdAIIFBjuqBAMaD4Ws4AvG6NB6NNGQMdilEokdAQNJjseC40d6lU7FdC4VImc2ptGmc1poXFjoYBC4PZqgXCouEouNo1E+sajxfELAIXIpoXBAYQXHF4U/+2EC4YVBzoXBpIXBL4oYB6lTmwXBSQNIC4lFO4LXIRIJHBUwIXCptIC4NKI5AqB7GU6OIC4ITBR4IXGvYvEC4IvBC4NDn/zxtDskzU4IXGa4JhBwjzC7MztAvBxE4jscF5EROoJfCAoJRCEgMa1wXFFwQYCAYQbCBgQDBC4aPFGAYGFAga/F1Xd1QCD1vRAoXdjUaAoWsC4e85nB5gCD5kc1kRBAXBiMcAoJHCgW7AAu8AYYdCAggWBAAMiCwvLB4YAF5YWDAAMMCBAAG0AXKNIRrChfsBZAXGBQoABC5QA/AH4A/AFgA=="))

251
apps/modclock/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/modclock/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,20 @@
{
"id": "modclock",
"name": "Modern Clock",
"shortName":"Modern Clk",
"icon": "icon.png",
"version":"0.03",
"description": "A modern, simple clock, with three ClockInfos and Fast Loading",
"type":"clock",
"tags": "clock,clkinfo",
"supports": ["BANGLEJS2"],
"screenshots" : [ { "url":"Scr1.png" },
{ "url":"Scr2.png" } ],
"dependencies" : { "clock_info":"module"},
"allow_emulator":true,
"readme":"README.md",
"storage": [
{"name":"modclock.app.js","url":"app.js"},
{"name":"modclock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -8,11 +8,20 @@ This is Bangle.js's main settings menu:
* **Alerts** - Set how Bangle.js alerts you (including Quiet mode) * **Alerts** - Set how Bangle.js alerts you (including Quiet mode)
* **Utils** - Utilities, including resetting settings * **Utils** - Utilities, including resetting settings
**New Users:** these are some settings you'll probably want to change right away:
* **Calibrate LCD** Make sure that the LCD touchscreen responds to touches where you expect them to
* **Locale** Set whether you want 12 hour time, and what day of the week the week starts on.
See below for options under each heading: See below for options under each heading:
## Apps - App-specific settings
This is where you adjust settings for an individual app. (eg. Health app: Adjust how often heart rate tracking should fire.)
## System - System settings ## System - System settings
* **Theme** Adjust the colour scheme * **Theme** Adjust the colour scheme. Choose between light mode, dark mode, or a custom theme. To adjust themes in more detail you can also use the [Theme Switcher App](https://banglejs.com/apps/?id=themesetter)
* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below. * **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below.
* **Locale** set time zone, the time format (12/24h, for supported clocks) and the first day of the week * **Locale** set time zone, the time format (12/24h, for supported clocks) and the first day of the week
* **Clock** if you have more than one clock face, select the default one * **Clock** if you have more than one clock face, select the default one
@ -44,9 +53,9 @@ See below for options under each heading:
* **Rotation** allows you to rotate (or mirror) what's displayed on the screen, eg. for left-handed wearers (needs 2v16 or 2v15 cutting edge firmware to work reliably) * **Rotation** allows you to rotate (or mirror) what's displayed on the screen, eg. for left-handed wearers (needs 2v16 or 2v15 cutting edge firmware to work reliably)
* **Wake on X** should the given activity wake up the Bangle.js LCD? * **Wake on X** should the given activity wake up the Bangle.js LCD?
* On Bangle.js 2 when locked the touchscreen is turned off to save power. Because of this, * On Bangle.js 2 when locked the touchscreen is turned off to save power. Because of this,
`Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js (we recently renamed the menu item to `Wake on Tap`). `Wake on Tap` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js.
* **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement. * **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement.
* **Calibrate** on Bangle.js 2, pop up a screen allowing you to calibrate the touchscreen (calibration only works on 2v16 or 2v15 cutting edge builds) * **Calibrate** on Bangle.js 2, pop up a screen allowing you to calibrate the touchscreen, ensuring your touches are mapped to the right place on the screen. (Highly reccomended for new users!)
## Locale ## Locale

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

@ -23,4 +23,5 @@
0.24: Redraw clock_info on update and provide color field for condition 0.24: Redraw clock_info on update and provide color field for condition
0.25: Added monochrome parameter to drawIcon in lib 0.25: Added monochrome parameter to drawIcon in lib
0.26: Expose update function (for use by iOS integration) 0.26: Expose update function (for use by iOS integration)
0.27: Add UV index display 0.27: Add UV index display
0.28: Fix UV positioning, hide when 0

View File

@ -7,8 +7,11 @@ It also adds a ClockInfo list to Bangle.js.
You can view the full report through the app: You can view the full report through the app:
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## iOS Setup ## iOS Setup
Use the iOS shortcut [here](https://www.icloud.com/shortcuts/dbf7159200d945179e0938c15e64f102). The shortcut uses Apple Weather for weather updates, and sends a notification, which is read by Bangle.js. To push weather every hour, or interval, you will need to create a shortcut automation for every time you want to push the weather.
Use the iOS shortcut [here](https://www.icloud.com/shortcuts/73be0ce1076446f3bdc45a5707de5c4d). The shortcut uses Apple Weather for weather updates, and sends a notification, which is read by Bangle.js. To push weather every hour, or interval, you will need to create a shortcut automation for every time you want to push the weather.
## Android Setup ## Android Setup
1. Install [Gadgetbridge for Android](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) on your phone. 1. Install [Gadgetbridge for Android](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) on your phone.
@ -51,8 +54,8 @@ When you first load QuickWeather, it will take you through the setup process. Yo
**Note:** at one time, the Weather Notification app also worked with Gadgetbridge. However, many users are reporting it's no longer seeing the OpenWeatherMap API key as valid. The app has not received any updates since August of 2020, and may be unmaintained. **Note:** at one time, the Weather Notification app also worked with Gadgetbridge. However, many users are reporting it's no longer seeing the OpenWeatherMap API key as valid. The app has not received any updates since August of 2020, and may be unmaintained.
## Clock Infos ## Clock Infos
Tap on any clockInfo when focused to directly open the weather app. Tap on any clockInfo when focused to directly open the weather app.
Adds: Adds:
* Condition ClockInfo with condition icon * Condition ClockInfo with condition icon
@ -60,6 +63,7 @@ Adds:
* Wind speed ClockInfo. * Wind speed ClockInfo.
* Chance of rain ClockInfo. * Chance of rain ClockInfo.
* Temperature ClockInfo without condition icon. * Temperature ClockInfo without condition icon.
## Settings ## Settings
* Expiration timespan can be set after which the local weather data is considered as invalid * Expiration timespan can be set after which the local weather data is considered as invalid

View File

@ -9,38 +9,9 @@ Bangle.loadWidgets();
var layout = new Layout({type:"v", bgCol: g.theme.bg, c: [ var layout = new Layout({type:"v", bgCol: g.theme.bg, c: [
{filly: 1}, {filly: 1},
{type: "h", filly: 0, c: [ {type: "h", filly: 0, c: [
{type: "v", width: g.getWidth()/2, c: [ // Vertical container for icon + UV {type: "v", width: g.getWidth()/2, c: [ // Vertical container for icon
{type: "custom", fillx: 1, height: g.getHeight()/2 - 30, valign: -1, txt: "unknown", id: "icon", {type: "custom", fillx: 1, height: g.getHeight()/2 - 30, valign: -1, txt: "unknown", id: "icon",
render: l => weather.drawIcon(l, l.x+l.w/2, l.y+l.h/2, l.w/2-10)}, render: l => weather.drawIcon(l, l.x+l.w/2, l.y+l.h/2, l.w/2-5)},
{type: "custom", fillx: 1, height: 20, id: "uvDisplay",
render: l => {
if (!current || current.uv === undefined) return;
const uv = Math.min(parseInt(current.uv), 11); // Cap at 11
// UV color thresholds: [max_value, color] based on WHO standards
const colors = [[2,"#0F0"], [5,"#FF0"], [7,"#F80"], [10,"#F00"], [11,"#F0F"]];
const color = colors.find(c => uv <= c[0])[1];
// Setup and measure label
g.setFont("6x8").setFontAlign(-1, 0);
const label = "UV: ";
const labelW = g.stringWidth(label);
// Calculate centered position (4px block + 1px spacing) * blocks - last spacing
const totalW = labelW + uv * 5 - (uv > 0 ? 1 : 0);
const x = l.x + (l.w - totalW) / 2;
const y = l.y + l.h;
// Draw label
g.setColor(g.theme.fg).drawString(label, x, y);
// Draw UV blocks
g.setColor(color);
for (let i = 0; i < uv; i++) {
g.fillRect(x + labelW + i * 5, y - 3, x + labelW + i * 5 + 3, y + 3);
}
}
},
]}, ]},
{type: "v", fillx: 1, c: [ {type: "v", fillx: 1, c: [
{type: "h", pad: 2, c: [ {type: "h", pad: 2, c: [
@ -50,12 +21,43 @@ var layout = new Layout({type:"v", bgCol: g.theme.bg, c: [
{filly: 1}, {filly: 1},
{type: "txt", font: "6x8", pad: 2, halign: 1, label: /*LANG*/"Humidity"}, {type: "txt", font: "6x8", pad: 2, halign: 1, label: /*LANG*/"Humidity"},
{type: "txt", font: "9%", pad: 2, halign: 1, id: "hum", label: "000%"}, {type: "txt", font: "9%", pad: 2, halign: 1, id: "hum", label: "000%"},
{filly: 1}, {type: "txt", font: "6x8", pad: [2, 2, 2, 2], halign: -1, label: /*LANG*/"Wind"},
{type: "txt", font: "6x8", pad: 2, halign: -1, label: /*LANG*/"Wind"}, {type: "h", pad: [0, 2, 2, 2], halign: -1, c: [
{type: "h", halign: -1, c: [
{type: "txt", font: "9%", pad: 2, id: "wind", label: "00"}, {type: "txt", font: "9%", pad: 2, id: "wind", label: "00"},
{type: "txt", font: "6x8", pad: 2, valign: -1, id: "windUnit", label: "km/h"}, {type: "txt", font: "6x8", pad: 2, valign: -1, id: "windUnit", label: "km/h"},
]}, ]},
{type: "custom", fillx: 1, height: 15, id: "uvDisplay",
render: l => {
if (!current || current.uv === undefined || current.uv === 0) return;
const uv = Math.min(parseInt(current.uv), 11); // Cap at 11
// UV color thresholds: [max_value, color] based on WHO standards
const colors = [[2,"#0F0"], [5,"#FF0"], [7,"#F80"], [10,"#F00"], [11,"#F0F"]];
const color = colors.find(c => uv <= c[0])[1];
const blockH = 8, blockW = 3;
// Draw UV title and blocks on same line
g.setFont("6x8").setFontAlign(-1, 0);
const label = "UV";
const labelW = g.stringWidth(label);
const x = l.x + 2;
const y = l.y + l.h / 2;
// Draw title
g.setColor(g.theme.fg).drawString(label, x, y);
// Draw UV blocks after title
g.setColor(color);
for (let i = 0; i < uv; i++) {
const blockX = x + labelW + 4 + i * (blockW + 2);
g.fillRect(blockX, y - blockH/2, blockX + blockW, y + blockW/2);
}
// Reset graphics state to prevent interference
g.reset();
}
},
]}, ]},
]}, ]},
{filly: 1}, {filly: 1},
@ -91,6 +93,7 @@ function draw() {
layout.loc.label = current.loc; layout.loc.label = current.loc;
layout.updateTime.label = `${formatDuration(Date.now() - current.time)} ago`; // How to autotranslate this and similar? layout.updateTime.label = `${formatDuration(Date.now() - current.time)} ago`; // How to autotranslate this and similar?
layout.update(); layout.update();
layout.forgetLazyState();
layout.render(); layout.render();
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "weather", "id": "weather",
"name": "Weather", "name": "Weather",
"version": "0.27", "version": "0.28",
"description": "Show Gadgetbridge/iOS weather report", "description": "Show Gadgetbridge/iOS weather report",
"icon": "icon.png", "icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],

View File

@ -13,3 +13,4 @@
0.09: Match draw() API e.g. to allow wid_edit to alter this widget 0.09: Match draw() API e.g. to allow wid_edit to alter this widget
0.10: Change 4x5 font to 6x8, teletext is now default font 0.10: Change 4x5 font to 6x8, teletext is now default font
0.11: Bugfix: handle changes in alarms (e.g. done without a load, such as via fastload) 0.11: Bugfix: handle changes in alarms (e.g. done without a load, such as via fastload)
0.12: Redraw when screen turns on or watch is unlocked

View File

@ -2,7 +2,7 @@
"id": "widalarmeta", "id": "widalarmeta",
"name": "Alarm & Timer ETA", "name": "Alarm & Timer ETA",
"shortName": "Alarm ETA", "shortName": "Alarm ETA",
"version": "0.11", "version": "0.12",
"description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).", "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",

View File

@ -133,5 +133,7 @@
}; };
Bangle.on("alarmReload", () => WIDGETS["widalarmeta"].reload()); Bangle.on("alarmReload", () => WIDGETS["widalarmeta"].reload());
Bangle.on("lock", () => WIDGETS["widalarmeta"].draw(WIDGETS["widalarmeta"]))
Bangle.on("lcdPower", () => WIDGETS["widalarmeta"].draw(WIDGETS["widalarmeta"]))
} }
})(); })();

2
core

@ -1 +1 @@
Subproject commit 0916756932699d626555171ce8e0a2989b151c89 Subproject commit f17383aeed940690db16f58dcfec5a91b7890391