diff --git a/apps/bmoface/ChangeLog b/apps/bmoface/ChangeLog
new file mode 100644
index 0000000000..c503e7b873
--- /dev/null
+++ b/apps/bmoface/ChangeLog
@@ -0,0 +1,14 @@
+0.0.01: Initial release with BMO character
+0.0.02: Added Finn and Jake characters
+0.0.03: Added settings menu for character selection
+0.0.04: Added temperature unit toggle (C/F)
+0.0.05: Fixed settings menu crash, added charging status indicators
+0.0.06: Fixed "Invalid Settings!" error with proper settings file handling
+0.0.07: Added character randomizer feature with multiple intervals
+0.0.08: Fixed lock screen character variable error, separated lock screen logic
+0.0.09: Improved lock screen character-specific drawing and positioning
+0.0.1: Major release with all core features complete
+0.11: Code refactoring and lock screen improvements
+
+## Attribution
+Based on the Advanced Casio Clock by dotgreg (https://github.com/dotgreg/advCasioBangleClock)
\ No newline at end of file
diff --git a/apps/bmoface/README.md b/apps/bmoface/README.md
new file mode 100644
index 0000000000..13a46d8f3b
--- /dev/null
+++ b/apps/bmoface/README.md
@@ -0,0 +1,53 @@
+BMO Face
+
+A playful Bangle.js watchface inspired by BMO from Adventure Time. Features three selectable characters (BMO, Finn, Jake) with dynamic expressions based on watch state.
+
+Features
+- **Three Characters**: BMO (green), Finn (blue), Jake (yellow)
+- **Dynamic Expressions**:
+ - Normal face when unlocked
+ - Sleeping face (`-_-`) when locked
+ - Lightning bolt eyes when charging
+- **Information Display**:
+ - Time (top-center) using `7x11Numeric7Seg` font
+ - Temperature (upper-left) with C/F toggle
+ - Steps (bottom-right)
+ - Heart rate (above steps)
+- **Settings Menu**:
+ - Character selection (BMO, Finn, Jake)
+ - Temperature unit toggle (Celsius/Fahrenheit)
+ - Character randomizer (Off, 5min, 10min, 30min, On Wake)
+- **Lock Screen**: Light gray background with character-specific sleeping expressions
+- **Charging Indicator**: Lightning bolt eyes for all characters
+
+Character Details
+- **BMO**: Green background, black circular eyes, complex layered mouth, dark teal borders
+- **Finn**: Light blue background, flesh-colored face, white hood with ears, simple curved smile
+- **Jake**: Yellow background, white eyes with black outlines, horizontal pointed jowls, oval nose
+
+Testing Commands
+Use in emulator console:
+```javascript
+// Test lock state
+Bangle.setLocked(true);
+Bangle.setLocked(false);
+
+// Test charging state
+Bangle.setCharging(true);
+Bangle.setCharging(false);
+
+// Test character randomizer
+Bangle.emit("lock"); // Triggers "On Wake" randomizer
+```
+
+Installation
+Upload via Bangle.js App Loader or manually install the files in the `bmoface` folder.
+
+## Attribution
+
+**Character Inspiration**: BMO, Finn, and Jake from Adventure Time (Cartoon Network)
+
+**Code Base**: Based on the Advanced Casio Clock by [dotgreg](https://github.com/dotgreg/advCasioBangleClock)
+- Original template: [Advanced Casio Clock](https://github.com/dotgreg/advCasioBangleClock)
+- Creator: [dotgreg](https://github.com/dotgreg)
+
diff --git a/apps/bmoface/app-icon.js b/apps/bmoface/app-icon.js
new file mode 100644
index 0000000000..b69774dd79
--- /dev/null
+++ b/apps/bmoface/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgJC/AD8f/F4vE/wEf/l8vF/4Ef/18/H/8Een18/PzAoMeAoPh+Eenl+/PA+AdBv/7Aom/AoX+u4FCEYIFEjwFEh9zj4LDuEd8I7C+EdHYIjB+Ec/5NB/gFE/AFBn6k/ADQA=="))
\ No newline at end of file
diff --git a/apps/bmoface/app.js b/apps/bmoface/app.js
new file mode 100644
index 0000000000..088a9b2b29
--- /dev/null
+++ b/apps/bmoface/app.js
@@ -0,0 +1,552 @@
+const storage = require('Storage');
+
+// Try to load fonts, but don't fail if they're not available (emulator compatibility)
+try { require("Font6x12").add(Graphics); } catch(e) {}
+try { require("Font8x12").add(Graphics); } catch(e) {}
+try { require("Font7x11Numeric7Seg").add(Graphics); } catch(e) {}
+
+function bigThenSmall(big, small, x, y) {
+ g.setFont("6x8", 2);
+ g.drawString(big, x, y);
+ x += g.stringWidth(big);
+ g.setFont("6x8", 2);
+ g.drawString(small, x, y);
+}
+
+function getBackgroundImage() {
+ // Cartoon face background - we'll create this
+ return null; // Placeholder for now
+}
+
+function drawSmileShape(x, y, width, height, thickness) {
+ // New approach: stamp small circles along an ellipse arc to get
+ // naturally rounded ends (no polygons changed elsewhere)
+ var startAngle = Math.PI / 5;
+ var endAngle = (4 * Math.PI) / 5;
+ var step = Math.PI / 40; // small, keeps change minimal
+ var rx = width/1.57; // match previous horizontal scale
+ var ry = height/2;
+ var r = Math.max(1, thickness/2);
+ for (var a=startAngle; a<=endAngle; a+=step) {
+ var px = x + rx * Math.cos(a);
+ var py = y + ry * Math.sin(a);
+ g.fillCircle(px, py, r);
+ }
+}
+
+function drawLightningBolt(x, y, width, height) {
+ // Draw lightning bolt using two opposing acute triangles
+ // x, y = center point
+ // width = how wide the bolt is
+ // height = how tall the bolt is
+ g.setColor(0x000000);
+
+ var halfWidth = width / 2.5;
+ var halfHeight = height / 1;
+
+ // Upper triangle (pointing down-right)
+ var upperTriangle = [
+ x, y - halfHeight, // Top center point
+ x + halfWidth, y, // Right middle point
+ x - halfWidth/2, y + halfHeight/2 // Left lower point
+ ];
+ g.fillPoly(upperTriangle);
+
+ // Lower triangle (pointing up-left)
+ var lowerTriangle = [
+ x, y + halfHeight, // Bottom center point
+ x - halfWidth, y, // Left middle point
+ x + halfWidth/2, y - halfHeight/2 // Right upper point
+ ];
+ g.fillPoly(lowerTriangle);
+}
+
+function drawFinnFace() {
+ var isCharging = Bangle.isCharging();
+
+ // White hood ears
+ g.setColor(0xFFFFFF);
+ g.fillCircle(30, 20, 22); // Left ear (x, y, radius)
+ g.fillCircle(140, 20, 22); // Right ear (x, y, radius)
+
+ // White hood behind face
+ g.setColor(0xFFFFFF); // White
+ g.fillCircle(85, 82, 85); // Hood circle (x, y, radius)
+
+ // Finn's face (flesh colored circle)
+ g.setColor(0.95, 0.8, 0.7); // Flesh color
+ g.fillEllipse(150, 100, 20, 10); // Face circle (x, y, radius)
+
+ // Outlines
+ g.setColor(0x000000);
+ g.drawEllipse(150, 100, 20, 10); // Face outline
+ g.drawCircle(85, 85, 85); // Hood outline
+
+ // White squared bottom for hood (behind everything)
+ g.setColor(0xFFFFFF); // White
+ g.fillRect(2, 102, 168, 180); // Squared hood bottom (x1, y1, x2, y2)
+
+ if (isCharging) {
+ // Lightning bolt eyes when charging
+ drawLightningBolt(32, 55, 12, 20); // Left lightning bolt
+ drawLightningBolt(139, 55, 12, 20); // Right lightning bolt
+ } else {
+ // Normal circular eyes
+ g.setColor(0x000000);
+ g.fillCircle(35, 55, 10); // Left eye (x, y, radius)
+ g.fillCircle(135, 55, 10); // Right eye (x, y, radius)
+ }
+
+ // Curved smile using arc
+ g.setColor(0x000000);
+ // Draw curved smile: center at (85, 100), radius 20, from 0.2*PI to 0.8*PI
+ var smilePoints = [];
+ for (var angle = 0.2 * Math.PI; angle <= 0.8 * Math.PI; angle += 0.1) {
+ var x = 85 + 20 * Math.cos(angle);
+ var y = 60 + 20 * Math.sin(angle);
+ smilePoints.push(x, y);
+ }
+ g.drawPoly(smilePoints);
+}
+
+function drawBMOFace() {
+ var isCharging = Bangle.isCharging();
+
+ if (isCharging) {
+ // Lightning bolt eyes when charging
+ drawLightningBolt(32, 55, 12, 20); // Left lightning bolt (x, y, width, height)
+ drawLightningBolt(139, 55, 12, 20); // Right lightning bolt (x, y, width, height)
+ } else {
+ // Normal circular eyes
+ g.setColor(0x000000);
+ g.fillCircle(32, 55, 10); // Left eye - moved up and left
+ g.fillCircle(139, 55, 10); // Right eye - moved up and left
+ }
+
+ // BMO mouth structure - all elements follow the same calculated curve
+ // Black mouth outline
+ g.setColor(0x424242);
+ drawSmileShape(85, 86, 40, 20, 29); // Black smile outline
+
+ // Inside of mouth (dark green)
+ g.setColor(0x225c27); // Dark green
+ drawSmileShape(85, 85, 43, 20, 20); // Dark green inside smile
+
+ // Tongue (medium green)
+ g.setColor(0x474747); // Medium green
+ drawSmileShape(85, 99, 40, 10, 6); // Green tongue smile
+
+ // Curved white tooth line (smile)
+ g.setColor(0xFFFFFF);
+ drawSmileShape(85, 80, 50, 12, 4); // White tooth line smile
+}
+
+function drawJakeFace() {
+ var isCharging = Bangle.isCharging();
+
+ // Black circles behind Jake's eyes
+ g.setColor(0x000000);
+ g.fillCircle(45, 63, 30); // Left black eye background (x, y, radius)
+ g.fillCircle(115, 63, 30); // Right black eye background (x, y, radius)
+
+ // Jake's white eyes on top of black circles
+ g.setColor(0xFFFFFF); // White
+ g.fillCircle(50, 60, 25); // Left eye (x, y, radius)
+ g.fillCircle(120, 60, 25); // Right eye (x, y, radius)
+
+ // Eye outlines
+ g.setColor(0x000000);
+ g.drawCircle(50, 60, 25); // Left eye outline
+ g.drawCircle(120, 60, 25); // Right eye outline
+
+ if (isCharging) {
+ // Lightning bolt eyes when charging (inside the white circles)
+ drawLightningBolt(50, 60, 8, 15); // Left lightning bolt (x, y, width, height)
+ drawLightningBolt(120, 60, 8, 15); // Right lightning bolt (x, y, width, height)
+ }
+
+ // Jake's jowls - horizontal pointed oval (like an eye shape)
+ g.setColor(0xFFFF00); // Yellow
+ g.fillEllipse(130, 140, 45, 65); // Main jowl oval (center x, center y, width, height)
+
+ // Jowl outline
+ g.setColor(0x000000);
+ g.drawEllipse(130, 120, 45, 65); // Main jowl outline (center x, center y, width, height)
+ g.drawEllipse(45, 130, 70, 77); // Left droop outline
+ g.drawEllipse(105, 130, 130, 77); // Right droop outline
+
+ g.setColor(0xFFFF00);
+ g.fillEllipse(47, 125, 68, 75); // Inner left droop oval (center x, center y, width, height)
+ g.fillEllipse(107, 125, 128, 75); // Inner right droop oval (center x, center y, width, height)
+
+ // Black horizontal oval nose
+ g.setColor(0x000000);
+ g.fillEllipse(107, 105, 68, 80); // Nose oval (center x, center y, width, height)
+}
+ // g.fillEllipse(105, 105, 68, 80); // Nose oval (center x, center y, width, height)
+
+function drawCartoonFace() {
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ var character = settings.character || "BMO";
+
+ if (character === "Finn") {
+ drawFinnFace();
+ } else if (character === "Jake") {
+ drawJakeFace();
+ } else {
+ drawBMOFace(); // Default BMO face
+ }
+}
+
+// Global variables for randomizer
+var randomizerTimeout = null;
+var currentCharacter = null;
+
+// schedule a draw for the next minute
+var drawTimeout;
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+function clearIntervals() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ if (randomizerTimeout) {
+ clearTimeout(randomizerTimeout);
+ randomizerTimeout = null;
+ }
+}
+
+// Start character randomizer
+function startCharacterRandomizer() {
+ clearIntervals();
+
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ var interval = settings.randomizerInterval || 0;
+
+ if (interval === 0) return; // Off
+ if (interval === 4) return; // On Wake - handled in lock event
+
+ var intervals = [0, 5, 10, 30]; // minutes
+ var intervalMs = intervals[interval] * 60 * 1000;
+
+ if (intervalMs > 0) {
+ randomizerTimeout = setTimeout(function() {
+ cycleCharacter();
+ startCharacterRandomizer(); // Restart timer
+ }, intervalMs);
+ }
+}
+
+// Cycle to next character
+function cycleCharacter() {
+ var characters = ["BMO", "Finn", "Jake"];
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ var currentIndex = characters.indexOf(currentCharacter || settings.character || "BMO");
+ var nextIndex = (currentIndex + 1) % characters.length;
+ currentCharacter = characters[nextIndex];
+
+ // Update settings
+ settings.character = currentCharacter;
+ require("Storage").writeJSON("bmoface.settings.json", settings);
+
+ // Redraw
+ if (Bangle.isLocked()) {
+ drawLockedScreen();
+ } else {
+ draw();
+ }
+}
+
+function drawClock() {
+ g.setFont("7x11Numeric7Seg", 3);
+ g.setColor(0, 0, 0); // Black text directly on green background
+ // Top-center time
+ var t = require("locale").time(new Date(), 1);
+ var tx = (g.getWidth() - g.stringWidth(t)) / 2;
+ g.drawString(t, tx, 8);
+ g.setFont("6x8", 2);
+ g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 140);
+ g.setFont("6x8", 2);
+ g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 77, 126);
+ g.setFont("6x8", 2);
+ const time = new Date().getDate();
+ g.drawString(time < 10 ? "0" + time : time, 78, 145);
+}
+
+function drawBattery() {
+ bigThenSmall(E.getBattery(), "%", 146, 8);
+}
+
+function getTemperature(){
+ try {
+ var temperature = E.getTemperature();
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ var useFahrenheit = settings.tempUnit === "F";
+
+ if (useFahrenheit) {
+ temperature = (temperature * 9/5) + 32;
+ return Math.round(temperature) + "F";
+ } else {
+ var formatted = require("locale").temp(temperature).replace(/[^\d-]/g, '');
+ return formatted;
+ }
+ } catch(ex) {
+ print(ex)
+ return "--"
+ }
+}
+
+function getSteps() {
+ var steps = Bangle.getHealthStatus("day").steps;
+ steps = Math.round(steps/1000);
+ return steps + "k";
+}
+
+function drawBorders() {
+ // Top border - thin dark teal/green line
+ g.setColor(0.1, 0.4, 0.3); // Dark teal/green
+ g.fillRect(0, 0, g.getWidth(), 6);
+
+ // Bottom border - thicker bar (no progress indicator)
+ g.fillRect(0, g.getHeight() - 8, g.getWidth(), g.getHeight());
+}
+
+function draw() {
+ queueDraw();
+
+ // Clear to character-appropriate background
+ g.clear();
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ var character = settings.character || "BMO";
+
+ if (character === "Finn") {
+ g.setColor(0.1, 0.3, 1.0); // Light blue for Finn
+ } else if (character === "Jake") {
+ g.setColor(1.0, 1.0, 0.0); // Yellow for Jake
+ } else {
+ g.setColor(0.35, 0.78, 0.45); // Light green for BMO
+ }
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+
+ // Draw borders (only for BMO, not Finn or Jake)
+ if (character === "BMO") {
+ drawBorders();
+ }
+
+ // Draw cartoon face
+ drawCartoonFace();
+
+ // Draw AdvCasio information like the original
+ g.setColor(0x000000); // Black text
+
+ g.setFontAlign(-1,-1);
+ g.setFont("6x8", 2);
+ // Temperature - upper left
+ g.drawString(getTemperature(), 6, 6);
+
+ // Steps - bottom right
+ var stepsStr = getSteps();
+ var sx = g.getWidth() - g.stringWidth(stepsStr) - 6;
+ var sy = g.getHeight() - g.getFontHeight() - 6;
+ g.drawString(stepsStr, sx, sy);
+
+ // Heart rate just above steps
+ var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm;
+ var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--";
+ var hx = g.getWidth() - g.stringWidth(hrStr) - 6;
+ var hy = sy - g.getFontHeight() - 2;
+ g.drawString(hrStr, hx, hy);
+
+ g.setFontAlign(-1,-1);
+ drawClock();
+ drawBattery();
+
+ // Hide widgets
+ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+}
+
+// Draw BMO's locked face
+function drawBMOLockedFace() {
+ // BMO horizontal mouth slit
+ g.setColor(0x000000);
+ g.fillRect(60, 90, 120, 83);
+}
+
+// Draw Finn's locked face
+function drawFinnLockedFace() {
+ // Black outlines on top
+ g.setColor(0x000000);
+ g.drawEllipse(150, 100, 20, 10); // Face outline
+ g.drawCircle(85, 85, 85); // Hood outline
+
+ // Gray hood bottom rectangle (same as white one but gray)
+ g.setColor(0.8, 0.8, 0.8); // Same gray as lock screen background
+ g.fillRect(2, 102, 168, 180); // Squared hood bottom (x1, y1, x2, y2)
+
+ // Finn's shorter horizontal mouth slit
+ g.setColor(0x000000);
+ g.fillRect(70, 85, 105, 83);
+}
+
+// Draw Jake's locked face
+function drawJakeLockedFace() {
+ // Black jowl outlines on top
+ g.setColor(0x000000);
+ g.drawEllipse(130, 120, 45, 65); // Main jowl outline
+
+ g.setColor(0.8, 0.8, 0.8); // Same gray as lock screen background
+ g.fillEllipse(130, 140, 45, 65); // Main jowl oval
+
+ g.setColor(0x000000);
+ g.drawEllipse(45, 130, 70, 77); // Left droop outline
+ g.drawEllipse(105, 130, 130, 77); // Right droop outline
+ g.setColor(0.8, 0.8, 0.8); // Same gray as lock screen background
+ g.fillEllipse(47, 125, 68, 75); // Inner left droop oval
+ g.fillEllipse(107, 125, 128, 75); // Inner right droop oval
+
+ // Jake's upside-down V mouth (^) - two intersecting lines, centered on nose
+ g.setColor(0x000000);
+ g.drawLine(65, 120, 85, 85); // Left line: bottom-left to apex
+ g.drawLine(105, 120, 85, 85); // Right line: bottom-right to apex
+
+ // Black horizontal oval nose
+ g.setColor(0x000000);
+ g.fillEllipse(95, 95, 75, 80); // Nose oval (center x, center y, width, height)
+}
+
+// Draw the sleeping overlay version when locked
+function drawLockedScreen() {
+ // Light gray background like LCD
+ g.clear();
+ g.setColor(0.8, 0.8, 0.8);
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+
+ // Get character setting first
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ var character = settings.character || "BMO";
+
+ // Draw borders only for BMO
+ if (character === "BMO") {
+ drawBorders();
+ }
+
+ // Schedule next update for time refresh
+ queueDraw();
+
+ var isCharging = Bangle.isCharging();
+
+ if (isCharging) {
+ // Lightning bolt eyes when charging (even when locked)
+ if (character === "Jake") {
+ // Jake's lightning bolts in white eye circles
+ g.setColor(0xFFFFFF); // White eye background
+ g.fillCircle(50, 60, 25); // Left eye
+ g.fillCircle(120, 60, 25); // Right eye
+ g.setColor(0x000000);
+ g.drawCircle(50, 60, 25); // Left eye outline
+ g.drawCircle(120, 60, 25); // Right eye outline
+ drawLightningBolt(50, 60, 8, 15); // Left lightning bolt
+ drawLightningBolt(120, 60, 8, 15); // Right lightning bolt
+ } else {
+ // BMO/Finn lightning bolts
+ drawLightningBolt(32, 55, 12, 20); // Left lightning bolt (x, y, width, height)
+ drawLightningBolt(139, 55, 12, 20); // Right lightning bolt (x, y, width, height)
+ }
+ } else {
+ // Sleeping face: horizontal slits
+ g.setColor(0x000000);
+ if (character === "Jake") {
+ // Jake's sleeping eyes in white circles
+ g.setColor(0xFFFFFF); // White eye background
+ g.fillCircle(50, 60, 25); // Left eye
+ g.fillCircle(120, 60, 25); // Right eye
+ g.setColor(0x000000);
+ g.drawCircle(50, 60, 25); // Left eye outline
+ g.drawCircle(120, 60, 25); // Right eye outline
+ // Horizontal slits inside the white circles
+ g.fillRect(30, 60, 70, 63); // left slit
+ g.fillRect(100, 60, 140, 63); // right slit
+ } else {
+ // BMO/Finn sleeping eyes
+ g.fillRect(22, 55, 42, 58); // left slit: y fixed by height of 3 px
+ g.fillRect(129, 55, 149, 58); // right slit
+ }
+}
+
+ // Draw character-specific locked faces
+ if (character === "Finn") {
+ drawFinnLockedFace();
+ } else if (character === "Jake") {
+ drawJakeLockedFace();
+ } else {
+ drawBMOLockedFace();
+ }
+
+ // Redraw information in black at same positions
+ g.setColor(0x000000);
+ g.setFontAlign(-1,-1);
+ g.setFont("6x8", 2);
+ g.drawString(getTemperature(), 6, 6);
+
+ var stepsStr = getSteps();
+ var sx = g.getWidth() - g.stringWidth(stepsStr) - 6;
+ var sy = g.getHeight() - g.getFontHeight() - 6;
+ g.drawString(stepsStr, sx, sy);
+
+ var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm;
+ var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--";
+ var hx = g.getWidth() - g.stringWidth(hrStr) - 6;
+ var hy = sy - g.getFontHeight() - 2;
+ g.drawString(hrStr, hx, hy);
+
+ g.setFontAlign(-1,-1);
+ drawClock();
+ drawBattery();
+
+ // Keep widgets hidden
+ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+}
+
+Bangle.on("lcdPower", (on) => {
+ if (on) {
+ draw();
+ } else {
+ clearIntervals();
+ }
+});
+
+Bangle.on("lock", (locked) => {
+ clearIntervals();
+ if (locked) {
+ drawLockedScreen();
+ } else {
+ // Check if "On Wake" randomizer is enabled
+ var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+ if (settings.randomizerInterval === 4) {
+ cycleCharacter();
+ }
+ draw();
+ startCharacterRandomizer();
+ }
+});
+
+Bangle.setUI("clock");
+
+// Load widgets, but don't show them
+Bangle.loadWidgets();
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
+
+// Initialize current character from settings
+var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {};
+currentCharacter = settings.character || "BMO";
+
+// Start character randomizer
+startCharacterRandomizer();
+
+g.clear();
+draw();
\ No newline at end of file
diff --git a/apps/bmoface/app.png b/apps/bmoface/app.png
new file mode 100644
index 0000000000..cb52e9f1b7
Binary files /dev/null and b/apps/bmoface/app.png differ
diff --git a/apps/bmoface/metadata.json b/apps/bmoface/metadata.json
new file mode 100644
index 0000000000..5dbda351b9
--- /dev/null
+++ b/apps/bmoface/metadata.json
@@ -0,0 +1,27 @@
+{
+ "id": "bmoface",
+ "name": "BMO Face",
+ "shortName": "BMO",
+ "version": "0.11",
+ "description": "A watch face inspired by BMO that shows time, temp, steps and HR. Sleeps to -_- when locked.",
+ "icon": "app.png",
+ "tags": "clock",
+ "type": "clock",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "allow_emulator": true,
+ "readme": "README.md",
+ "screenshots": [
+ {"url": "screenshot1.png"},
+ {"url": "screenshot2.png"},
+ {"url": "screenshot3.png"},
+ {"url": "screenshot4.png"}
+ ],
+ "storage": [
+ { "name": "bmoface.app.js", "url": "app.js" },
+ { "name": "bmoface.img", "url": "app-icon.js", "evaluate": true },
+ { "name": "bmoface.settings.js", "url": "settings.js" }
+ ],
+ "data": [
+ { "name": "bmoface.settings.json" }
+ ]
+}
\ No newline at end of file
diff --git a/apps/bmoface/screenshot1.png b/apps/bmoface/screenshot1.png
new file mode 100644
index 0000000000..b12ff07ad2
Binary files /dev/null and b/apps/bmoface/screenshot1.png differ
diff --git a/apps/bmoface/screenshot2.png b/apps/bmoface/screenshot2.png
new file mode 100644
index 0000000000..5834256113
Binary files /dev/null and b/apps/bmoface/screenshot2.png differ
diff --git a/apps/bmoface/screenshot3.png b/apps/bmoface/screenshot3.png
new file mode 100644
index 0000000000..f1cbc68721
Binary files /dev/null and b/apps/bmoface/screenshot3.png differ
diff --git a/apps/bmoface/screenshot4.png b/apps/bmoface/screenshot4.png
new file mode 100644
index 0000000000..f2ba4fd1d3
Binary files /dev/null and b/apps/bmoface/screenshot4.png differ
diff --git a/apps/bmoface/settings.js b/apps/bmoface/settings.js
new file mode 100644
index 0000000000..c2341d0934
--- /dev/null
+++ b/apps/bmoface/settings.js
@@ -0,0 +1,47 @@
+(function(back) {
+ var FILE = "bmoface.settings.json";
+
+ // Load settings with proper defaults
+ var settings = Object.assign({
+ character: "BMO",
+ tempUnit: "F",
+ randomizerInterval: 0 // 0=off, 1=5min, 2=10min, 3=30min
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "BMO Face Settings" },
+ "< Back" : back,
+ 'Character': {
+ value: 0 | ["BMO", "Finn", "Jake"].indexOf(settings.character),
+ min: 0, max: 2,
+ format: v => ["BMO", "Finn", "Jake"][v],
+ onchange: v => {
+ settings.character = ["BMO", "Finn", "Jake"][v];
+ writeSettings();
+ }
+ },
+ 'Temperature Unit': {
+ value: settings.tempUnit === "F" ? 1 : 0,
+ min: 0, max: 1,
+ format: v => v ? "Fahrenheit" : "Celsius",
+ onchange: v => {
+ settings.tempUnit = v ? "F" : "C";
+ writeSettings();
+ }
+ },
+ 'Character Randomizer': {
+ value: settings.randomizerInterval,
+ min: 0, max: 4,
+ format: v => ["Off", "5 min", "10 min", "30 min", "On Wake"][v],
+ onchange: v => {
+ settings.randomizerInterval = v;
+ writeSettings();
+ }
+ }
+ });
+})(back)
\ No newline at end of file
diff --git a/apps/doomguy/ChangeLog b/apps/doomguy/ChangeLog
new file mode 100644
index 0000000000..f5332531a2
--- /dev/null
+++ b/apps/doomguy/ChangeLog
@@ -0,0 +1,13 @@
+0.01: Initial Doomguy watch face
+0.02: Added animated Doomguy face that changes with battery level
+0.03: Face animation - Doomguy looks left, right, and center
+0.04: Added heart icon in lower right corner
+0.05: Added yellow lightning bolt charging indicators in lower left
+0.06: Added "BATT" label above battery percentage in white text
+0.07: Changed date text color to yellow
+0.08: Optimized memory usage with 4-bit color sprites and heatshrink compression
+0.09: Added interactive tap feature - tap Doomguy's face to flash yellow and show damage, daily hit counter with persistent storage
+0.11: Added temperature unit settings (Fahrenheit/Celsius toggle)
+
+## Attribution
+Based on the Advanced Casio Clock by dotgreg (https://github.com/dotgreg/advCasioBangleClock)
diff --git a/apps/doomguy/README.md b/apps/doomguy/README.md
new file mode 100644
index 0000000000..ddcc9010e8
--- /dev/null
+++ b/apps/doomguy/README.md
@@ -0,0 +1,78 @@
+# Doomguy Clock
+
+
+
+A DOOM-inspired watch face featuring the iconic Doomguy face that reacts to your battery level!
+
+## Features
+
+### Dynamic Doomguy Face
+- **Battery-Reactive**: Doomguy's face changes based on your battery level:
+ - 80-100%: Normal face
+ - 60-80%: Slightly damaged
+ - 40-60%: More damaged
+ - 20-40%: Heavily damaged
+ - 0-20%: Critical damage
+ - **Charging**: God mode (invincibility face)!
+
+### Face Animation
+- Doomguy periodically looks left, right, and center
+- Animation occurs every 2-3 seconds when unlocked
+- Pauses when watch is locked to save battery
+
+### HUD Display
+- **Time**: Large red digital time display
+- **Date**: Yellow date text (day of week, month, day)
+- **Battery**: Shows percentage with "BATT" label
+- **Heart Rate**: Current BPM displayed with heart icon
+- **Steps**: Daily step count
+- **Temperature**: Current watch temperature
+- **Charging Indicator**: Yellow lightning bolts appear when charging
+
+### Visual Elements
+- **Heart Icon**: Red heart in lower right corner
+- **Lightning Bolts**: Two yellow triangles in lower left when charging
+- **Gray HUD Panels**: Left (battery) and right (stats) panels
+- **Hit Counter**: Displays daily tap count
+
+## Interactive Features
+
+### Tap to Hit
+- **Tap Doomguy's Face**: Interactive hit counter
+ - Screen flashes yellow twice
+ - Face shows damage reaction (damaged2_center)
+ - Hit counter increments
+ - Counter resets automatically each day
+ - Hit count persists through app restarts
+
+## Controls
+
+- **Tap Face**: Trigger hit animation and increment daily counter
+- **Swipe Down**: Show widgets
+- **Lock Watch**: Pauses face animation to save battery
+- **Unlock Watch**: Resumes face animation
+
+## Technical Details
+
+- Uses 4-bit color depth for memory efficiency
+- Heatshrink compression for sprite storage
+- 16 different face sprites (5 damage levels × 3 directions + 1 god mode)
+- Each sprite: 60×60 pixels
+- Optimized for Bangle.js 2
+
+## Memory Optimization
+
+This watch face uses advanced memory optimization techniques:
+- Sprites stored as compressed 4-bit palette images
+- On-demand decompression
+- Efficient animation timer management
+- Persistent storage for daily hit counter
+- Minimal memory footprint for smooth operation
+
+## Credits
+
+**Character Inspiration**: Inspired by the classic DOOM game's status bar face by id Software.
+
+**Code Base**: Based on the Advanced Casio Clock by [dotgreg](https://github.com/dotgreg/advCasioBangleClock)
+- Original template: [Advanced Casio Clock](https://github.com/dotgreg/advCasioBangleClock)
+- Creator: [dotgreg](https://github.com/dotgreg)
diff --git a/apps/doomguy/app-icon.js b/apps/doomguy/app-icon.js
new file mode 100644
index 0000000000..400739092e
--- /dev/null
+++ b/apps/doomguy/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4n/80Zpsb+9XylVBoNNlexoksjdMjdzquUrtPomTqvGiMvom0rtEAC1RiIAVC7EWswAEtwOFjYNFswXBieqAAOvAYWxC4lj1/61X///6nIXC2CjErU2CwdZnNwBomWC5EHzWWHwWTsDJFC5MAgw+BAAP7cIwXKABgXX1IX5gu7AAO2RwwXL8UiAAYXQhwWEkUgC54uFGA4XCjQXEFwwwHC4UZ1wuLkUvfYk7C4MV0dQFxQwF2dhC4MR9WVBANSC5JhChxGBC4UbnYJBCxUi4EAh+VC4cRC6JGBC/4XL1YX/C4sVC6EKC4kZ1wXB+QXK8EAg2rC4dq2AXBh4WJlwNBgFj8IXBi02BAQwKFwIABg0+C4MayAXDGBAuDAAOmC4RGCGBQuDC4kTC4owGFwoXKGAwuFC4cfC4wwElYLFL5QANI5QXPF64Xfgu7AAQjHC5Xj1Wq0cznJ3Qh2pqMRiMVseXC5OWs1r3dm3djyoWBAAMW1RNDAAJHDHoJBBAAIJBAAhNEmejmwXBB4oAQC64="))
\ No newline at end of file
diff --git a/apps/doomguy/app.js b/apps/doomguy/app.js
new file mode 100644
index 0000000000..7698c6b20d
--- /dev/null
+++ b/apps/doomguy/app.js
@@ -0,0 +1,499 @@
+const storage = require('Storage');
+
+require("Font6x12").add(Graphics);
+require("Font8x12").add(Graphics);
+require("Font7x11Numeric7Seg").add(Graphics);
+
+// Load settings from storage
+function loadSettings() {
+ return storage.readJSON("doomguy.settings.json", true) || {
+ faceMetric: "battery",
+ tempUnit: "C"
+ };
+}
+
+// Reset hit counter
+function resetHitCounter() {
+ hitCount = 0;
+ saveHitCounter();
+}
+
+function bigThenSmall(big, small, x, y) {
+ g.setFont("7x11Numeric7Seg", 2);
+ g.drawString(big, x, y);
+ x += g.stringWidth(big);
+ g.setFont("8x12");
+ g.drawString(small, x, y);
+}
+
+
+// schedule a draw for the next minute
+var drawTimeout;
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+function clearIntervals() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ stopFaceAnimation();
+}
+
+function drawClock() {
+ g.setFont("7x11Numeric7Seg", 3);
+ g.setColor(1, 0, 0);
+ g.drawString(require("locale").time(new Date(), 1), 70, 10);
+ g.setFont("8x12", 2);
+ // Yellow color for date
+ g.setColor(1, 1, 0);
+ g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 8, 8);
+ g.setFont("8x12");
+ const time = new Date().getDate();
+ g.drawString(time < 10 ? "0" + time : time, 50, 8);
+ g.setFont("8x12", 1);
+ g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 8, 30);
+ g.setFont("8x12", 2);
+}
+
+function drawBattery() {
+ // Draw "Batt" label in white
+ g.setFont("6x8", 2);
+ g.setColor(1, 1, 1);
+ g.drawString("BATT", 5, 95);
+
+ // Draw battery percentage in red
+ g.setColor(1, 0, 0);
+ g.setFont("8x12", 3);
+ bigThenSmall(E.getBattery(), "%", 8, 120);
+
+ // Draw lightning bolt (two yellow triangles) for charging indicator
+ if (Bangle.isCharging()) {
+ g.setColor(1, 1, 0); // Yellow
+ // First triangle (left bolt)
+ g.fillPoly([
+ 20, 155, // Top vertex
+ 5, 155, // Bottom left vertex
+ 20, 165 // Bottom right vertex
+ ]);
+ // Second triangle (right bolt)
+ g.fillPoly([
+ 20, 148, // Top vertex
+ 20, 160, // Bottom left vertex
+ 35, 160 // Bottom right vertex
+ ]);
+ }
+}
+
+
+function getTemperature(){
+ try {
+ var temperature = E.getTemperature();
+ var settings = require("Storage").readJSON("doomguy.settings.json", 1) || {};
+ var useFahrenheit = settings.tempUnit === "F";
+
+ if (useFahrenheit) {
+ temperature = (temperature * 9/5) + 32;
+ return Math.round(temperature) + "F";
+ } else {
+ var formatted = require("locale").temp(temperature).replace(/[^\d-]/g, '');
+ return formatted || "--";
+ }
+ } catch(ex) {
+ print(ex)
+ return "--"
+ }
+}
+
+function getSteps() {
+ var steps = Bangle.getHealthStatus("day").steps;
+ steps = Math.round(steps/1000);
+ return steps + "k";
+}
+
+
+// Doomguy face sprites - stored as base64 strings (NOT decompressed yet!)
+// We'll decompress ONE at a time when drawing to save memory
+var doomguySprites = {
+ normal_center: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AG91ioABoNFAIVRqNXDqNxqlEAAtUisXDZ9yGINBDAMtnstklEHgVRDp0RqI1CDYYACotFisSDptRK4VNDgtBHgUCDpsEig8BDYNNDwUUBINRgodNOwNUoo2CDgR1DqEWPBsBCYSwEiJzBAAMBgsAahlQDocUAIVVDgRYBiEVg47OorRBoNVoMUqhaBHwMFeRtQGIIaCAINUXQRBBJAQ7PHII2BLIIAEHwNQO5w0BZQNEAgNCDoaZCHZxSDAANCpYeEHgNnDpZYBqlC6XSd4O0olNkmyBIJ3OgtkilDDoPS6gcConT6fUT4IdMiCUBpsz6Y8BHoND6c9nohBaJrgBoktmYWBmgeB6Q6BAwKWBHZtUik9mfjDAIZBIIMzmZDBO5lyqg7BAAJ5BLwIBBloDBWYVSm4dJuhKBoruBppYBkhyBkb4BYAIBBo4dKFgNEotRiNVIIImBqrdCSoMUDpYNBJoMBiK5CC4IFBiRABiNEHZgWBkQdCisj6gbBgMiMINBLJd1KQIvBnapBonUmhjCDgNCBQNXHZRSCok9EAIDBnoDBpYIBpaZBi4dKilC2fSZ4LOBdwIFCoj2BoizMHYI1Bnstmcz6YeBAwQdCiqzMpoTDAAXjAgZfCHZd3cINC6g2CAQIjBAoRbBoJ2KAAKzBOYR0CGwMtMQSeCWRQABWYU0GwPUAgLLB6c+EIMkoIdMu8UVAU9mqvCoK3EoQcMu9EklDGYIaBDwVEPAM0Dp9UOQPTAQIbCoXT7sz6lLogdNig7BDwNDPQNDewQ7SO4KsCeYT2D6ktiQdNudEltDolUWgNNLgTRCDht3uVEoYaBAAPU6geC6SzBDp9UppvBAIK3BmgFBOwNFDp13uhQBKINCHAIkDkgcPLQZRBLgS7C6SxOAAZPBWoI3BAQQ6Bo4dRu61CZ4LQBSQIcTu8VOYLwBHgVEi4dTeYTuDoIbUAAk3DTIA/AH4AKA="))
+ },
+ normal_left: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRAANUigBBAwVXDaFykNUogAB6QDColRqUnDp91HANEoXTofTD4NBHgNQDh8RDgfS6g7DokVisXDptwqlBC4ckAYUUio7BPJ1xqIVBDYlBqlULIMBg6wPoodBHAdFiIKBgsFsoeNgprBLYglCBIMBso8OiAWBN4KOBSAUVBQMFgNRDpocBqlFDQLMBLodFAwKWOLINBR4IBBDgZgBIAQ7NB4KPBK4TWED4I7BeBo7BC4lE2lCLYg7PZ4ND6VElskoXU2gEBkiFBaJwdB6QABGoICC6fT6lBLJztBoU9mYeCklD6c9nskLJ8BR4JZB6YeB6lNEgM9mieBDptVitQgtEGoIABmZYBmZZBO5osBO4NFgtDG4QDBltViLcCDptBosRgFEmndHQQLBDYNRqg7PqNQgEEaANQDYILCAQIdLNANEoMAkURgsViIiBgMiktEigdMBwJ2BqMioMBqhxBEIMikgqBO5hnBojRCIANEltBOgM9olCFQIdLosUDYPSDYIYBmYECoVCBQJ3NHIPTpskofUWQQZBpr1BoKzMHYPS6YTBnv9mYeBAoLVDDpl0SoM9HoIWCAQI+EosXDpdySoQVBAAQbCHgMtWgIdPodDDINEPQJ1Bmc9DgI7Nu9xCoMj6fUWASxBn09mlEo4cMPALpBGYR9BGoJhBmYlBDp11lstN4Q0BMAPS6g7B6dXDp1CoSqBVoS4BbARZQuNNpoWCLgLyBAwU9WRoACkkkVQVNSwaBBBAIcODoNNCgQeBD4a7CDp8loVDRwM9ZoKbEoIdPu43BNwIAEDwMkDiDxCGwJbCXQK4BoQdRu8UC4IABLAQjBdpw8Fig3BmczHINEqYcSu9ykXSHgctkUnDqaYCAAdHDaoA/AH4A/AEoA="))
+ },
+ normal_right: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRAANUigBCisVi4dSitBogADoNFoNXDaF1isUDQUtlskAgI8BisnDhtyDgdD6fSHoY8BqMSDptwqNFCwPS6g6DokUqNVqAdNuNUCgNCO4o5BqNQiAdNkpZBD4KUFitFiEQqwdNiBZBSoYcBojYCgsBgsCDpkFU4MUDIIBBV4QABHQNRgIdMGISqBDYMUIINUDoI9CiIdMgNRqgBBDgZeDHwRZNFwKUBipzBTIp7DO5yRDAAW0W4kQLJpLBGgjyBkkkAQIeBqqzPoVC6QbBHIPS6ktlskosRLJqQBonT6fUHgM9HAO0ns9HaAdBCgIABDYIGDAoI7PqNEmcz6fSAARCBMAKWBHZsBolNnszHoYABmlFQgLRNolVipMBptDHoMz6U0oD1BHYMVo4dLiNEqNVig+BofTpodBQYQgBDph2BojyBisBqlNisAgAaBSoQdMiqJBkUhiBwBoNRgIHBQgNEigdLoNUVAMimkQgNRDoVCkRIDDpYVBCAND6VUKQNNklUij1DLJlFigbB6QTBki1BAgImB6gBBHZhZBV4MtGQPSWYPUmhFDO4MXHZdEls9dQMzd4XTnoADmhZMuSVB6YAE8YkBAoRkCDhQdBigPBLYIYCAQhIBLIIdLu6tBHgIWB6lCoYkBLYT8BqQdMkI7BPAQiBSQR9BLAUnDpl0GYMzc4L1CKYPdn0tEgIcMu91oaWC6XUDoR0BMINNkodNuNNoTGBZIRfBAAb5BHZ1CWQM9OQIDBAQQjCoIdNPARYC7qtBnpzBofS6SUNDoR3BOgdNoh7BEwNEm4dOuIYBNwJzE6VNklFDhwABVYMtPAPUXAPToZEBo4dQigVBaIoGCi4dQu9UDwJ7BoYcBHSaXCaYQAEoixPS4o3B78zWATsPAA0kkiYBmkkK6YdESgZXUAH4A/AFY"))
+ },
+ damaged1_center: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AG91ioABoNFAIVRqMnDiEUiNEAAVD6gDBosVqNBDp44Cok9AAMkmgeBisUiocOqNUCoNNlskAgI8CqgpBg4dNgsUilEoXSppcCBAIcBgNXLBwxBDwIADilUSoNRqFnDhlwCIJ2BDwIDBG4MRBQMFgMFLRlxCQNVip6BR4LPBA4JYBssQi47MqDtCDQVRZwIACHYNQPBg7CogBBagMFgjQCqMViFRLJh3BqhXBHYKRCAwI/BIYR3Pc4VFSwNRgK2CLwTwNGYUkC4QBB6LWDLgJZNSQLtEAAIjCDoUQSpl1ioSBloYED4hgBSp9C6c9kgfDls9ndEPAKVOolDnoABHAVL6YlBmg7RpszmYXBnYbC+YdBolQO5g7CkgYCHYM0EgPdDgNFiIdLuhoBoLSBLYMkpofBIIL4BihKBDpbDBU4MVqtRqNBoNVqrsBFALgBDpY5CoMRiMQqo6BiERgMSXQIsBLJkUkkSiEhiEBls0qESgMjPAItBShoADAoPT6RFCAAZZMJIMtWAQABeQYABoXTUoIdLGoKvBlpuBDgMz6QcD2S2BSplE2buBdQIDB8czEwIjBntBLJdyDoIYBHAQBCA4IJCltEq4dKu91NoQVCHQXTEAJ7CoMXDpdxCANDDgM0PYM0ps9nzQBWQI7MukUV4Y0BkiZBEoPT6g7Bo4dLu47CKgNFL4XBohaBmlCDht3qkkWIIyBAAVS6g7DDp0U6kj6ZTBdgXCWAMzPgKUMHYh3CLgPSAwfUkgcNPAQ5B6g8C6W0oXS6ZBBDqA0BOwdMAYSVBDqEUGYNCSAPdKwMtlsk6lBDp7oBCwM0OYU0K4KbBk4dPLQLJDolUEgMzDoIcQu90N4JVBkQCBno6Tu9yWoQACWINNkk3DqJ5CHoR4CokXDiR6DZgVCOiQdHAAVFDq4A/AH4A/AAo"))
+ },
+ damaged1_left: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRqNUigBCitBqNXDqUVokUogADotBDqN1isVDgdD6QDBihFBHKA6BAAM9mlNkgGCDwI9Pgo6DHAQACPANFi47QHgQ4DoNBotFiodOgsBWQQfBqKUCEwMQqMFg46OLQVBDoNUiIlBAAUBPBo7BspbCoNQiFFWIQ6BqBaNiFQHQNFKwIgBgNULQUVsJZNgo6BqiPBOga6BBIJ5BLJsQCwdFkgDBKwLvCEgI7OKILqDAgIfBBAbSOOwVEoQfDAAodOgMFDRMkegQdNNwVC6cz6QbCoW0mc9kh3PLANEls9ns0LoQFBnstoKzORYNBHwPT6m9mm+mcz6fSLJtyZAYfBKwIZBolNA4IoBgQdLulFLIIBBHoc9WAUVqkVgodMcoVRgMRC4NE6gjCEgICBDpo5BisDmcRghUBiNQgMSHwQdLuNBSoUSDoNRotEqsQqNCkg/BDpd1DgMtkk7EANUoXSAYNLmh7BLJsRokj6RyBeYYEEmlFDpg7B6fToVEofS6czAYIKC6h3MuiUBok9AAMzAAIFCno7BkhZMu6NBloTBAAfzHAJXBIoMXDpjRCoQXCHoICC6aBCq4dMiqMCkczOQNEV4J6BLANBDpo8BZYI7BWwgeBPANHDht3ilNkgzCDYVRTwU0Dp9UklNVgRXBolblsjBAKUNaYTuBLIJyB6bzB6fdHYIcODoMtCwIACns7obwDDqFEprwBO4NFqiVCIAIdPDwJZBofUH4I3BofS6U3DqI8CHoPzXAIfBnocQeIUkRoJZBLAK7BkgdSu54CHoNNdwM064dTogdBnpYCEQMnDqZ5BeQTxB6VHDiY9DHIR7BDiwdBHoPS6gdYAH4A/AH4AiA"))
+ },
+ damaged1_right: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AH4AWqIABotBAIUVisXDaF1CgNEAAtBEwVHDp1xqNUigaCoQCBoo9SqFFDYo7CLgUQDhtygNBLA46CAAJYQDwMkHgVBqgaBHYU3LBqUCO4dESQQABgsVgIdMiASBio9BqMEohWDgNRgNQdhouBqMUKIMBqg8Eq1VSxsFKwIBBdIYGBSoQ6BDpp2Cog3BK4YjBHwUFgo7PSoY3BAgL4BilRHZ7rED4YHDaINWSptUdQIAGeoJaDDpg1CoYbEEgO0PwazOoktns9GoKRB2k9mfTHgQ7NZIIVBmYeB2kkps9IIdFHZsEonTAAIdBH4M9mhXBfIQ7MqNBqgTColNmfSKgI7DoNRo7QLWYKJBaoNC6fUokRAwI7CogdLqNFiguBGoNUoklisRoERIoQdMG4UiiEAgIkBokAiczkAkBAAMXHZdUilCklFPQcViUybAMkoNFHZcUC4U7LYNEoaUD2b0BEgI7LqsUoaQCpstZ4NNHIL6BaoKzNdwQ4B6dDmfTmfUXAPTa4JZMqNSofSCgIACnofBAgU9a4IdLu6mCDAIVB+c9DQQbBoQNBDhaWBOgIWCkczmgjCmlCQQIdNHYMkCwMk6iTCPIPSDgK+BDplyqnSVIQADps+EoIjBDhgdBoVNGYNEL4NFoktIYNEBIIdOrskO4QzBpkjWYQhBoIdOigbBlqRB6UkdYIdC6UnDpt3RoIACdgPbd4ZYBm4dOujvBSIKsBotNPYIICHZ9xZgSvB6nTEAIIDDp91OIJZCZoIAClskoNHDpxaBCwTvDoZ2CDiA8BoQWBpocBXAM0olCDiB4CHgSxBaAJ5BqYdRu6sBKgMzEIKTBoIcSPIQ9CK4R0RDoskoXSAIIdXAH4A/AH4AGA="))
+ },
+ damaged2_center: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AH4AWusVitBooBCqNRq4dSuNUogAEqkVi4cRGQNEilEoQcBoI8CqI5QDgIABns9lskHoZbQOoIVBpocBDwg9Bg4dOuFUK4Mk6fT6XSDgVFiodOuFRDgJZCohZEilRqCYNuMFgiyCSgQcFHh1wq0VHgVND4JZBDgIABgIdNusBgodBoI7DDYQ7BqC0NuEQCYNUoNEgLXBSQIABgNWHZ4dBVYIaCoNUAYJ3CSptwGIRxBDYIjBO4ZIBHZ0FSoQ7BigeBXQJDBeB47CDYLMCEIJ8BAAIMBHZw6BDIMkDAQcDAgJZOuBRCAAokBpacCDpsgSYNEkfT2g9D2k0mlBqEDDplFHYXTns9loeB2XT6fUPAMEDphpBHQMzmfj6Q5BnwGBmlEFYI7NdQNVonUmdNkfSmfUopGBUYIdMoAdCqtD6ZdBps9mlRAAYdMcIMViIwBog1BoICBDgZZOilQqkAgtQiNRqoEBgtAQgJZNoNFZQMRilVHAIfBoJCBih6BHZocBoUkXAIYBTwUiBYIhBDplxokiZoNEltDmfSAwc9olXDpl1iisBCgMz6YABAgIgBoVBDpo7BAAMzDgICBEIb4BosXDpl3kgdBnpUBDwIaBoc9BYIcOu8lNoMzEANNn0tqlNmfUklHDpwvBofT6lEkY6B4hDBmlEDqdCVYNNpcUTgM0ppZQoSNB6h2CoS7BAANCWRoACHYXS2jVBldC6QHBok3DqDmBplVokUqnEkizCDh93iiNBkm0d4Q+BofUoIdQujsB6VC7ocBki6CDiF3uTwBGwJTBoidBoYdSHgQ3BTIL2DWKC1FVwLSBTgNCDiaXC6c9SoUkHShbD6XUdaQA/AH4A/AFQA="))
+ },
+ damaged2_left: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AG91isVoNFAIVRqNXDqVxCwNUqlEAAIfBDqVQHQMUqlBDoQ+BBIMnHKNFDQQAFqNQDp1wGAIVBoVEls0AYNEigLBSaFRGgVNmlNnpcDHZ1wiFWWII7B6fUkhACLINRg46NgsBqkUCwMtknSHQUUilQWxo7BiEVZQIZCHYIcBqg7RssBoLQEDIIADDpo7Bd4NUHoKPBa4KwBAAMFi47NgMVgIxBiNEisQAoNVDoMQHZwcBgsUoo9Ba4NRL4JZCSpo7BgoZBK4J1BdQK6BLQQ7PsrvCSoY9BqlEXoI7PJoIaCZoIhEEQI7PLAVCDAYAEHZ52BGgNNlskDYkrDoI7NoDJBNwMj6fS6QbBoW0ns0orwNVgKtBqlNns9HgW0n0z6VBgJaMJYNUosQilDHgWy8cz6c9osVHZkUSgNBopcBmYfCnvSosBawI7MqhZBF4MVHgPUodEmk0qNVqI7NaAUViFQCwLvBotE6IpBHZx2BolAgEEoofCisAgFEqsBDptFGgIABiklLYNEIYMRoSCBoI7NKAIABkKbBLIQDBawR3NulEkk9mgUBpstmdE6QGBlskHRgdBilNknSc4LqBdgUyegPRo4dMuqVBHoUzmYCBEIU9LQJYMAANxKwUz6lMoYbBA4RcBDhp4CNoIdBolD8fS4lDQINFDp11DoMjGwIdBAYVE6YlBDp13ok06ZQBnstVwPEokzmlBDqHUG4PUKYM92lCnoHBk4dQkgdB7dLSAM03oHB6U3Dp4eBcwNEqlFotMO4JfBHaF3qQVBkm0oY4BoXSHYIdRilNSQUzmc9AAIGBDqLTBSYLKBAAVDMIIcQAAN0LQXUD4IcBXoIdSutNkfSkczawUko4dSPIc9O4VEoQcTagVC6avBOqg8ESgckoIdWAH4A/AH4AFA"))
+ },
+ damaged2_right: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHEikkiAQskDqdVitFotEAQNBA4IbRusVitBogADqIACq4cOuURDQgADoI+BqMnHR45CoVD6QcFqMHDp0UDgXS6ktlskD4cVi4dNiNVDwU9DYNCDgkVgQdNJgJxC6XSpo6CogcBiFQDpsEilUig3CAAUUitRqFRgodNCQNFijtBG4bRDgsBDpsBCQNUiskoKuCAAVQLJ9Rqo1Bio5BgoFCAAVRiAdNCwJYCCoIYBqixBHoVWO5wdBoLLBqlRgNULgLRCO5w7BqgcBV4NEbIcVEAJ3OOgQACEIIfBXAkVHZ0RojuFAAdFoI7OiA2BAA8kMQJZOZQVC6c9mgYBIIO0AwJaCojuMVYND6cz6XUkgcB6XT6SbDHZgOBnvz6YXBpdNn0koEEHYKcBHZgABHoUzH4NDKwL4CqNUWhjDCqsUps9ofUAYKSBqoCBqNFHZxrCoK4BoNAgsAFQRaBHZrSBoEAXAMUiEFokAgC5BSptUqg5CGIVFTwNLiJ3BoNFLJikCokhNwJZCqkRegQGBHZl3ilCcoIABWoIECbYMtEAIcMLQITBkXTnoABls9mi5B6Y7BDppZC789mffmYAC6XUBYNVDpt1KoM9CgMznzwBppdDo4dNukUK4JtB6YZCEoM0AgMXDpx3BCgVNEIPESgQ7QuNUSIY9B6kUEIM0oQ7PO4PT6SrBEIMrpazBmlNoQcNHYNEkfT7ZyBHoKVBAgNCLBx4C6XSovFosU4m0ki7CDh4dBOoNEoVDoR5B6RiBDqVC6VLlczmfT6dNXYNBDqLSC6hTBAALQCDiAABWgTvBDgLWCSaAACqg1CLAJ1B6dEDqd3igeBO4JeBolXDiZ6CG4IAB6gbVTAZ4BaIIdXAH4A/AH4AGA="))
+ },
+ damaged3_center: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AEdyCidRqNUoICBokVAAIbRuoUBilBolFogACEYNXDp8hGwQADkgiBqlFiMnDp0FiocF6Q6BokUHiBXBDYVD6QcBHQNRPYMHLCVC6XT6YeCop4BqSUPK4ctmYdB6lBTgUQDptwqITBoVNnoACkgbBaiFxqh3BD4NDLIjZBHZ5ZBNoIXBHgMtHQIABqo7QVAQ9Bmi0CHgMUiJ3RAAI7CHINC6jSBHYMFHZ9RqsUpZVCEAURBYNQHZ1QiDTEHYZ1BgMBHZ0FspZCPYchoJkCHZ11sISBqjTBoIDBoruBiALBSp9QG4LTBAgYcDHZw5CDANRHQJ8BHQIKCiFCDhd0F4NEEAVFEANUAQIDBX4IdNCoQ0Bio7CSwKxBH4MRDpoZBWAQABOoNUpdD6XSHYNEDplQilC6SsCqjOBok9AAIFBiTuMLINDCYKxCokEonTAAJGBqhZNC4JaEAoQACHYKVPoq0BqFVisRooZBqT3BXgIdLuQdBoOyqsAZgIfBLwNbIoUiDpd3FwIAB2lLqqzBrkcpckLQQcMu52BloSBCoPTmfSogKBoRZBDpoZB6ckGYMjV4QeBoVNkg7PqVN+c9mY6BmYEE6kVDpt3GIIVB6fULIQCBnphBq4dOqlEoZVBAQM+klNns9MQMXHZ4dBns8TQIZBpYHBBYNHDpypBGgNEqo8BotU7s0XoJZPukUoStBqI1BqtUkfUQYM3Dp11oI7BknFqkUqtLpstkh2PHgVC2m0qNVqPB4lLoVFDiA8BKoJWBPoMcim0OwIdRuNEpYACldEDgMkHaTxCnpYBqtVoVE6KTPeQvTplCoScBkjsPDo0k4Wynu02XUDqqXBqlNmlFokTDijUCrkU6jRBDiy2BqPCoNciIdXAH4A/AH4AFA"))
+ },
+ damaged3_left: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/ADV1isUqkVoNFoNRqIcSqNUolEoICDosVisXDqEUDQQADHgIBBq5XQDYs9ktEEwNVHh91igaCoVElpaCotEisQk4dNuJ1CoXTAAIjCoKbBiA7PKAIXBoYeDPwMUqNVgIdNio0Cls9AARbBoq0BgNQDpsEO4dNmY6B6SVCqNQiDtOKAIBBknT6XSoYkBd4MQqI7ORQI9B2dNlpYB6VUoNQqMFHZxsBWYPUaQNC6VLaQI7BO50BqNRqlNklNKwMkolVLAVhHZwABDAQAC6jQCqEVLJ0FgAdBAANBihbBSgTQBWZ0QCYIdCTIVBZ4QeBqqzOqx3BDAVVDoNUDgLSCO6EUHAIECWAIABPIMFm4cMgsUio4BG4JeBdgMVgghCoQdLCII3CdAI0BTYJeBMQICBDplRDoKSBDQMEHoLVDAoQ7NKQMkoAEBjslitLls0DoNEmlSShtFF4NFiCXBSIM9noeBBQI7PC4NBqodBAAVC6RbCHZo5BZAgdDPAY7OUwMAgtVgsQiIGBplFW4JACDpd3HYMFpbmBLINR4JiCSwQcMHgNEqvCDoIjBilMpdR4g7CDprnCiJOB6cz6kkE4MjSwMVDpsRoUznsjDgPTAAQIBklFoIdNolNmYaDAYM9A4IdBoIdOulDC4IABmfj6hdB7skolFk4dNuNEDQPTogiBPwM9+XUWYNHHZwVBKAIdCSAJeBWQQ7OuoVC6lVps9agNC6Q7Bi4cNLIUtmjyBa4MVrdLIYMUq4dQonS2nBqMVrkUA4NEqI7PPANC2g7BgsVivEpYnBigcPHge0qm0AAMbMQMlDiC0Cog1BoVCpgFDDqN3ZwXF4NUqtTaAJ1QDwg9Cmg5BltHDid3kh4BoTNBDwMnDql1olU3stpkUoIcUAAVVqlCqNXDi93kQABoRXVAH4A/AH4AJA"))
+ },
+ damaged3_right: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHNRqNUoNUogCBi4aRuQbBqNEilEAAVBolRDqFxioaBHAIABkgCBIIIdQisVoo4DonT6VFHgNXDp8FKoktmg7CPINBDx8VOoJWCoQ6B6R5DTB1wSYtNns9loHBqNFo6UOgsVLAc9mY8BAwNFilAHaBPBDwc9kk9aQQdOusBZwRaBoY6BptCA4MVHZ7RBCYNEpaTBpq0B2g7BqI7PRQKXBkgaBoR3CDgIdOHYNlHoKyBPQQ8CBIMQHaEFikUOQLzCWQVRqA7OiFQHYQABAYdBFAI7PLAI9BDoa3BitFLAJ3QDgNVZQI2BbANRisFO51yF4IABiNUigGCbANRiAGBiQdLuinCoNVilUOwIBBH4IIBIQIdNcQIWCqNBqhaCTwUQiIdLuIaBCoNEWwNRoVRilLTgVAHZl1oKRBok0HgVNitUoXT6QrBig7MHINUlszKYMQC4MVofTns0opZMuiTBJ4VEZQMReYaCBSp4ACH4MBqFQA4VVFQNFLJgdBHYJxB4MAgJ5BqFVqEAbIIdMHgbnCpjtDqPCooFBDhgdBWQMtkpvBokkmlEKoJHBoodNuoYBnskGYOzV4M9MQKYCDptxG4PTmcz2fkmc9AIUz2hZOu4vBd4I3Bls+mfdmYmB6lHDpx3BoQVCoZZBmgGB6dEDp7nCkYVBPgKVBlohBokXDpyvCHANFqlD6iaBMAMkq5ZQilNnodBps0ovEEoQ7PuVUqlC6VEqsUqtRoW9lsVDp48BitLpdMitRitcQISUPAAMVoIVBoNVitFoO02ktHSF3uKXBHgNbpcrpg7CDqI8BZYKSBolFqOyPwIcRu8iZwXEiJcBlskoQdSHgRbCnu06Y6TPIlULgIiBq4dUuUiolboXUqsyk4dUAANRqqVBqobWAH4A/AH4AG"))
+ },
+ critical_center: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRqNUoICCosViobRuoUBilBolFolEAgIjBq4dPuI2BDIIACkghCisXK6I1CAAXSDgMULgMHDp0FigaCoXSoZaCD4JaQOoIdC6YAB6gFBqNBoNSSh46DoYdB6RgCqlRqAdNuFRVwVNnszDwgLBiKyOqhZBoJ2BDYVNSoJZBHZxZBCQJaCDoKXDqNViruPNoI9BnstAAJ/CFIJ3QAANUKgI+Com0ioABgI7PAANEkQdCpskolFDoMQHZ1QCQMUHYYcBLAIcBLJ1xgtgHYIABoJ7BaAQACWZ1hVANUZYI/BDgJYBqFRHZxLBgsRc4QiBolAWAJ3PugQBoI7Big2BiNUA4IiBHYNCDplQiNEgMVqIdBgpABgsFqo7BoYdMiFUqgxBDoT0Bio7CLgNEDpjtCqsFLwMFgIdBgtUpgIBLJo7BcwLICEAMUqPBqlKIYMUaBgOBoXBqsQLYUEitFqVLFIMSLJqwBWYI7CogABBQLzBM4LRNVYNMiNQVgK6BEoVBHYJ3NGAISBqEAHgNQioFBrY5BoodMu4bBoSUCKgQDBaIJdBoIcMu8UokkovBCoMkmXUbINUoQdPHYRsBolD6cz6dE4PBBQNFHZ1UnsrkfTnocBAQOzoc9kkVDpt3KoO+2c9HYPz6XT6W9mlEo4dOPAU9nstmc+mhaBoR7Bq47QppQBAYQHB2nCAYMXDp10olLofUqhaBoscDYIdTltNpi1BnsVqnB4XSoR3PuIwBpcVdQNRqtRDoJ2QHgdUpdVqtcqvB2gnBDiA8C4W0qPKokcXgPCqIdRutEC4PE2my2gDBigdSu8V4mzoNEplUawPBDiV3uUUWgNLa4VEWB4AFGwNLoQeBptLWKJ5FqPEHYNCDioABktcjdbqjOSag1R4LuBWCYA/AH4A/ABQ"))
+ },
+ critical_left: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/ADN1isVoNFilUilRAAIcRCYNUolEoICBooEBqsVi4dPisUDQQADilFIQIdPK4IbFmgeDMYMnDptxK4VEklC6XUDgJ6BisQDp11HAfTAAJaDqlRqBaNuBYBig6Bls9AAMkS4I7BgNQHRsUDgNEps9mfT6XSSgRZBio7NOwlDDgVNA4KUBqMAShrtCHoJ2B6VEobYBoNRgo7PoJ5BWINNPINEpdUHYMQsA7OHgMUpdElrTBMIUVqC0BWZwABDYMtTIUk6QoBWYMFWZsBHYQbBOYNC6kkilVgNViA7ORAJTCDoICCHYNQiDvOgNlDoTpBH4LtEAQI7NiAxBDALKBAgIcCqoCBqw7NC4J4CDIJABEIIcBioMBm4dLJgIVBijzBEQVBSINVqgCBoQdLcINFAQNRoFVgsUbQUVigpBDpgQBolVLgVQDAQ5BotBqjZBDpYTBolQDQLWBLIJzB4NMikdmlSHZsVoo8BD4R/B4NLmjbBoo7NJYKqCWQSzBrhkBokUoJ3NC4NcWwUBDANEFAQABio7NB4MAiu0gpaBiEAqu0oI7BSpt1HAKOBokReIdVLIQABDhd3uIPBZIMVqgYBEoNVDwcUDpl1HANFPQMtmc9nskrkbofSPgI7Ooc7oez6fTmYCB2VDEINFoodMukUCYM9n09mfjDoPS2YdBoNBDpgeBCwIACHQPUklE7s0olUk4dNLQJXBRgPTGwNE2lC6lEoIdOuh4B6fbSwIdCpdEAYNFHZ9D6k9oNUVoNFrlFog7BiodOuozCdINUAYNR4PCMINHDhp3COAPBqsViodC2h2QAANElgeBDQIbB4m0IANCDh93ihvBokbLoO1oIkBokSDqCnB2m04lCDIIFBqlBi4dQu8Vok0oPEqnEoi6BqIcReIVE5dC2VLpsk4KTQAAVyDoNElZYBkiTSagu1D4KbBoNXDql3otV4nE4PBDapbCkQADDq4A/AH4A/AAw="))
+ },
+ critical_right: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A/ADVxoNBokkilCokUigdTksRqkUD4IABikVDiVRqIcBqgcColFokVi4cRCoIADlskH4VRk4dOgNRDQR0BDgPSolBqkVDp9Rqo5DpodBklEPwQdOuEQiqOBK4Uz6fULAQBBDpt1godBLAU9AAJ4DqkQHZ0VcwJYCno7B6YGBoKhBHZyVBGQNBoQeBSwTSCLJw7BO4UUps06fSO4O0ilRoA7QoqMBoXSpskPgIABFQI7Pso9CdYJ7CaQJYBqA7PWgMUqgYBAAIDBSgIABO6BbBDYSaBAANFilQaJ11g0BqFUOIcUpcUHYILBHZ5NBDINBooDBolRqsQqqVOupZBD4NUGwVVWAQABLJt0RIQCBqlUHYK4BqMFHYUXDpgxColQisRoAXBii8BipbBq4dNGQNEqpWBoNQIAJhBBANRDplxRgIBBqFQiNQqsBqkBrkV4IdNujrDoo0BKgJ9BotFiIEBDpo5BAAPBKgMQaIVEqlV2qVOopZCjkUgJYBEoVViKjBWZ1BDwNLosAgKPBqG0iEAqhZNWgYwBDQJ+Be4VVqgrBDhh4ESoIiCkgkBqsUoNFDpt1C4M7SwIEBmc9loGB4ghBDptxG4PTmdDnoABmYGB2kzkhZOu60BlobCDgIDClsz6lDDpyzBolD6YCCHYPSpYCBk4dOqiWB2k9SQI5BolLIoMkDp8UYwNNkfEqnT6hEBpkjogcOu7NCnlNoNUls0qvE4lNaBwABirrCpdV4NRqtViscklHDp4eCWwNBqodBjkcpXSDqcsjdMiFRovB4W1ogdRuMVHYOx4my2lEjksoQcQDwVF2jRBAAO12QDBDqVyqtD6XE2tMijxBHad3iK2Dkm06VMDiZaBqtE5dEO4KTSAAtEawXBm4cWaoNV4PB4ocXAH4A/AH4AjA="))
+ },
+ godMode: {
+ width : 60, height : 60, bpp : 4,
+ transparent : 7,
+ buffer : require("heatshrink").decompress(atob("u4A/AH4A8qIABqkUAIUVAAIbQuVCisUogABloDCosVkUXDp1wio5CoXT6VCDoI8CDp11io0Cps9AYUkAYNBqoeOuEUAAJWBkg5CDYIcCgIdNutRolUogbDoiWBToNWiAdNgJ1CDYYcBXQVRgFVgLOQDgdBaAcQqMBqAdMgoTBigZCijNBAAVQiABBDpguBNwJbBAIJeCoqWBJANVHZw6CoMVoqcBAASXCsAdMJwNBoggBDYiYBBIMVqI7ODAgABpabEqMFHZwbEoVLeYMkewS2BHZ8tnsklstolNknS7c9olRaJpzBDoU9HAUkogGBlskJQI7Pnsz6fULgXTAAI7BLJw7CmYdBGwIACA4JiBopZNgodB78+KYQBBEYPfDoL5BDhbDBqkUitUN4JZCps9otBotFNAIdLLITPCeYzRCDptRqgUDAoVBignDoNRDpgSCp//OgNEndE6nv/80BgIPBDpQrColDn8+OgJ6Bp0j+bRBQ4QdKDgVEp3eG4NCeQXepoLBaQMRDpVFilDofSCYLsCAQMkoXSofUiJZMqlL6bMDnvj6YkBogGBmlFSptD6fTHIQcBAYYAB6lBDpd3ZQMtDghZBLYS7BklBDhd3aIQ9B8ez6ffEYRcDoodMHYKuBKoIEBXYRhCklEWRYABuIVGkixBIQKgCo4dMug2BKQPSIIIABqhhBIYIdOutC6hvBOAadCBANEmkXLKIYBLQK6CoY7B6g7OLISuCGwLQDEgMlHZp4BptD6gyB6XULoVNDoI6NDoSrBOoYADoYjBDp54CGQJVDpstlp9BLBwABKAKQBZgPeAQJDB6VCDh4dBHIMtH4NBLAXTLCAeDGwYBCDgNEDiN3kirBlsjZ4kiDqV3iiXBeIM9mhdBDid3uq2CDgSwRAAlwiruEoIdVAH4A/AH4AkA"))
+ }
+ }
+
+// Animation state
+var faceDirection = "center"; // current direction: "center", "left", or "right"
+var faceAnimationTimer;
+
+// Hit counter state
+var hitCount = 0;
+var hitCountDate = "";
+
+// Get today's date as a simple string (YYYY-MM-DD)
+function getTodayString() {
+ var d = new Date();
+ return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDate();
+}
+
+// Load hit counter from storage
+function loadHitCounter() {
+ var today = getTodayString();
+ var stored = storage.readJSON("doomguy.hits.json", true) || {};
+
+ if (stored.date === today) {
+ // Same day, restore the count
+ hitCount = stored.count || 0;
+ } else {
+ // New day, reset counter
+ hitCount = 0;
+ saveHitCounter();
+ }
+ hitCountDate = today;
+}
+
+// Save hit counter to storage
+function saveHitCounter() {
+ var today = getTodayString();
+ storage.writeJSON("doomguy.hits.json", {
+ date: today,
+ count: hitCount
+ });
+}
+
+function animateFace() {
+ // Randomly look left or right occasionally
+ var rand = Math.random();
+ if (rand < 0.1) {
+ faceDirection = "left";
+ } else if (rand < 0.2) {
+ faceDirection = "right";
+ } else {
+ faceDirection = "center";
+ }
+}
+
+// Helper functions for different metrics
+function getBatteryLevel() {
+ return E.getBattery();
+}
+
+function getHeartRateLevel() {
+ var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm;
+ if (!hr || !isFinite(hr)) return 0;
+ // Normalize heart rate to 0-100 scale (inverted - lower HR = more damage)
+ // Assuming normal range is 60-180 bpm
+ var normalized = Math.max(0, Math.min(100, ((hr - 60) / 120) * 100));
+ return 100 - normalized; // Invert the scale
+}
+
+function getTemperatureLevel() {
+ try {
+ var temp = E.getTemperature();
+ var settings = loadSettings();
+ var useFahrenheit = settings.tempUnit === "F";
+
+ if (useFahrenheit) {
+ temp = (temp * 9/5) + 32;
+ }
+
+ // Normalize temperature to 0-100 scale (inverted - extreme temps = more damage)
+ // Assuming normal range is 20-40°C (68-104°F)
+ var minTemp = useFahrenheit ? 68 : 20;
+ var maxTemp = useFahrenheit ? 104 : 40;
+ var normalized = Math.max(0, Math.min(100, ((temp - minTemp) / (maxTemp - minTemp)) * 100));
+
+ // Invert so that extreme temperatures (both high and low) cause more damage
+ // Normal temp (around 50% of range) = healthy, extremes = damaged
+ var distanceFromCenter = Math.abs(normalized - 50);
+ return distanceFromCenter * 2; // Scale to 0-100
+ } catch(ex) {
+ return 50; // Default middle value
+ }
+}
+
+function getStepsLevel() {
+ var steps = Bangle.getHealthStatus("day").steps;
+ var stepGoal = 10000; // Default step goal
+ return Math.min(100, (steps / stepGoal) * 100);
+}
+
+function getHitsLevel() {
+ // Normalize hit count to 0-100 scale
+ // More hits = more damage (inverted scale)
+ return Math.max(0, 100 - Math.min(100, hitCount * 2));
+}
+
+function getMetricValue() {
+ var settings = loadSettings();
+ switch(settings.faceMetric) {
+ case "battery": return getBatteryLevel();
+ case "heartrate": return getHeartRateLevel();
+ case "temperature": return getTemperatureLevel();
+ case "steps": return getStepsLevel();
+ case "hits": return getHitsLevel();
+ default: return getBatteryLevel();
+ }
+}
+
+function drawDoomguyFace() {
+ var isCharging = Bangle.isCharging();
+ var faceImage;
+
+ // God mode when charging
+ if (isCharging) {
+ faceImage = doomguySprites.godMode;
+ } else {
+ // Select face based on selected metric
+ var metricValue = getMetricValue();
+ var spriteKey;
+
+ if (metricValue > 80) spriteKey = "normal";
+ else if (metricValue > 60) spriteKey = "damaged1";
+ else if (metricValue > 40) spriteKey = "damaged2";
+ else if (metricValue > 20) spriteKey = "damaged3";
+ else spriteKey = "critical";
+
+ faceImage = doomguySprites[spriteKey + "_" + faceDirection];
+ }
+ // Draw the face in the middle section (between the HUD panels)
+ // Adjust x, y coordinates to center the face
+ g.drawImage(faceImage, 60, 105);
+}
+
+function startFaceAnimation() {
+ if (faceAnimationTimer) clearInterval(faceAnimationTimer);
+ // Animate face every 2-3 seconds
+ faceAnimationTimer = setInterval(function() {
+ animateFace();
+ draw();
+ }, 2500);
+}
+
+function stopFaceAnimation() {
+ if (faceAnimationTimer) clearInterval(faceAnimationTimer);
+ faceAnimationTimer = undefined;
+}
+
+function flashYellow() {
+ // Flash between two background colors twice
+ var flashCount = 0;
+ var flashInterval = setInterval(function() {
+ if (flashCount % 2 === 0) {
+ // First color flash - red background
+ g.setBgColor(1, 0, 0); // Red background
+ g.clear();
+ // Draw damaged2_center face during flash
+ g.drawImage(doomguySprites.damaged2_center, 60, 105);
+ } else {
+ // Second color flash - yellow background
+ g.setBgColor(1, 1, 0); // Yellow background
+ g.clear();
+ // Draw damaged2_center face during flash
+ g.drawImage(doomguySprites.damaged2_center, 60, 105);
+ }
+ flashCount++;
+ if (flashCount >= 4) { // 2 flashes = 4 toggles
+ clearInterval(flashInterval);
+ draw(); // Ensure we end on normal display
+ }
+ }, 100); // Flash every 100ms
+}
+
+function onFaceTap() {
+ // Check if we've moved to a new day
+ var today = getTodayString();
+ if (hitCountDate !== today) {
+ hitCount = 0;
+ hitCountDate = today;
+ }
+
+ hitCount++;
+ saveHitCounter();
+ flashYellow();
+}
+
+function drawHeart(x, y, size) {
+ // Draw a heart using filled circles and triangle
+ g.setColor(1, 0, 0); // Red
+
+ // Left circle (top-left lobe)
+ g.fillCircle(x - 5, y, 5);
+
+ // Right circle (top-right lobe)
+ g.fillCircle(x + 5, y, 5);
+
+ // Bottom triangle (point of heart)
+ g.fillPoly([
+ x - 8, y, // Left top corner
+ x + 8, y, // Right top corner
+ x, y + 16 // Bottom point
+ ]);
+}
+
+function drawHUD() {
+ // Left section - Battery area
+ g.setColor(0.4, 0.4, 0.4); // Solid gray for battery section
+ g.fillRect(0, 95, 50, 176);
+
+ // Right section - Heart rate area
+ g.setColor(0.4, 0.4, 0.4); // Solid gray for BPM section
+ g.fillRect(180, 95, 130, 176);
+ // Connecting line across the top
+ g.setColor(0.3, 0.3, 0.3);
+ g.fillRect(0, 95, g.getWidth(), 93);
+
+ // Draw heart in lower right corner
+ drawHeart(150, 155, 16);
+
+ // Draw hit counter
+ g.setFont("8x12", 1);
+ g.setColor(1, 1, 1); // White
+ g.drawString("Hit:" + hitCount, 60, 98);
+}
+
+function draw() {
+ queueDraw();
+
+ g.clear(1);
+ g.setColor(0, 0, 0);
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+
+ // Draw HUD panel first
+ drawHUD();
+
+ // Draw Doomguy face
+ drawDoomguyFace();
+
+ g.setFontAlign(1,1);
+ g.setFont("8x12", 2);
+ g.setColor(1, 0, 0);
+ var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm;
+ var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--";
+ g.drawString(hrStr, 165, 140);
+ g.drawString(getSteps(), 170, 85);
+ g.drawString(getTemperature(), 35, 85);
+
+ g.setFontAlign(-1,-1);
+ drawClock();
+ drawBattery();
+
+ // Hide widgets
+ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+}
+
+Bangle.on("lcdPower", (on) => {
+ if (on) {
+ draw();
+ } else {
+ clearIntervals();
+ }
+});
+
+
+Bangle.on("lock", (locked) => {
+ clearIntervals();
+ draw();
+ if (!locked) {
+ startFaceAnimation();
+ }
+});
+
+Bangle.setUI("clock");
+
+// Set up touch handler for double-tap on Doomguy face
+Bangle.on('touch', function(button, xy) {
+ // Check if tap is within Doomguy face area (60x60 sprite at position 60, 105)
+ if (xy && xy.x >= 60 && xy.x <= 120 && xy.y >= 105 && xy.y <= 165) {
+ onFaceTap();
+ }
+});
+
+// Load hit counter from storage
+loadHitCounter();
+
+// Load widgets, but don't show them
+Bangle.loadWidgets();
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
+g.clear(1);
+draw();
+startFaceAnimation(); // Start the face animation
diff --git a/apps/doomguy/app.png b/apps/doomguy/app.png
new file mode 100644
index 0000000000..9fc952f77a
Binary files /dev/null and b/apps/doomguy/app.png differ
diff --git a/apps/doomguy/data.json b/apps/doomguy/data.json
new file mode 100644
index 0000000000..0fcf7dd7dd
--- /dev/null
+++ b/apps/doomguy/data.json
@@ -0,0 +1 @@
+{"tasks":"", "weather":[]};
diff --git a/apps/doomguy/doomguy.settings.js b/apps/doomguy/doomguy.settings.js
new file mode 100644
index 0000000000..94b3648fa4
--- /dev/null
+++ b/apps/doomguy/doomguy.settings.js
@@ -0,0 +1,66 @@
+(function(back) {
+ var FILE = "doomguy.settings.json";
+
+ // Load settings with proper defaults
+ var settings = Object.assign({
+ faceMetric: "battery", // Default to battery
+ tempUnit: "F" // Default to Fahrenheit
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ function showSettingsMenu() {
+ E.showMenu({
+ "" : { "title" : "Doomguy Settings" },
+ "< Back" : back,
+ 'Face Metric': {
+ value: ["battery", "heartrate", "temperature", "steps", "hits"].indexOf(settings.faceMetric),
+ min: 0, max: 4,
+ format: function(v) {
+ var options = ["Battery", "Heart Rate", "Temperature", "Steps", "Hit Counter"];
+ return options[v];
+ },
+ onchange: function(v) {
+ var options = ["battery", "heartrate", "temperature", "steps", "hits"];
+ settings.faceMetric = options[v];
+ writeSettings();
+ }
+ },
+ 'Temperature Unit': {
+ value: settings.tempUnit === "F" ? 1 : 0,
+ min: 0, max: 1,
+ format: function(v) { return v ? "Fahrenheit" : "Celsius"; },
+ onchange: function(v) {
+ settings.tempUnit = v ? "F" : "C";
+ writeSettings();
+ }
+ },
+ 'Reset Hit Counter': {
+ value: false,
+ onchange: function() {
+ // Reset hit counter
+ require('Storage').writeJSON("doomguy.hits.json", {
+ date: new Date().getFullYear() + "-" + new Date().getMonth() + "-" + new Date().getDate(),
+ count: 0
+ });
+ // Show brief confirmation message
+ g.clear();
+ g.setFont("6x8", 2);
+ g.setColor(0, 1, 0); // Green color
+ g.drawString("Hit Counter", 20, 50);
+ g.drawString("Reset!", 20, 80);
+ g.flip();
+ // Auto-return to settings after 1 second
+ setTimeout(function() {
+ showSettingsMenu();
+ }, 1000);
+ }
+ }
+ });
+ }
+
+ // Show the menu
+ showSettingsMenu();
+})(back)
diff --git a/apps/doomguy/metadata.json b/apps/doomguy/metadata.json
new file mode 100644
index 0000000000..c965894c10
--- /dev/null
+++ b/apps/doomguy/metadata.json
@@ -0,0 +1,23 @@
+{ "id": "doomguy",
+ "name": "Doomguy Clock",
+ "shortName":"Doomguy",
+ "version":"0.11",
+ "description": "DOOM-inspired watch face with animated Doomguy face that reacts to battery level. Interactive tap feature lets you hit Doomguy with yellow flash effects and damage reactions. Features daily hit counter, battery-reactive faces, animated glances, heart rate, steps, temperature, and charging indicators.",
+ "icon": "app.png",
+ "tags": "clock,retro,doom",
+ "type": "clock",
+ "screenshots": [
+ { "url": "screenshot01.png" }
+ ],
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator":true,
+ "storage": [
+ {"name":"doomguy.app.js","url":"app.js"},
+ {"name":"doomguy.img","url":"app-icon.js","evaluate":true},
+ {"name":"doomguy.settings.js","url":"doomguy.settings.js"}
+ ],
+ "data": [
+ { "name": "doomguy.settings.json" }
+ ]
+}
diff --git a/apps/doomguy/screenshot01.png b/apps/doomguy/screenshot01.png
new file mode 100644
index 0000000000..f834a3935c
Binary files /dev/null and b/apps/doomguy/screenshot01.png differ
diff --git a/apps/meseeks/ChangeLog b/apps/meseeks/ChangeLog
new file mode 100644
index 0000000000..632228d829
--- /dev/null
+++ b/apps/meseeks/ChangeLog
@@ -0,0 +1,5 @@
+0.0.1: Initial release with Mr Meeseeks character faces
+0.0.2: Added battery-dependent aging spots overlay, tap-to-cycle faces, temperature in Fahrenheit
+
+## Attribution
+Based on the Advanced Casio Clock by dotgreg (https://github.com/dotgreg/advCasioBangleClock)
\ No newline at end of file
diff --git a/apps/meseeks/README.md b/apps/meseeks/README.md
new file mode 100644
index 0000000000..344e6e0c64
--- /dev/null
+++ b/apps/meseeks/README.md
@@ -0,0 +1,59 @@
+# Mr Meeseeks Clock
+
+A Rick and Morty inspired watch face featuring Mr Meeseeks with multiple expressions and battery-dependent aging effects!
+
+## Features
+
+### Dynamic Mr Meeseeks Faces
+- **12 Different Expressions**: Various Mr Meeseeks faces to cycle through
+- **Tap to Cycle**: Tap anywhere on the face to cycle through expressions
+- **Swipe/BTN1 Fallback**: Alternative controls for devices with touch issues
+
+### Battery-Dependent Aging
+- **Aging Spots**: Blue spots appear on screen as battery decreases
+- **Progressive Aging**:
+ - 100% battery: No spots
+ - 80% battery: Few spots
+ - 50% battery: Moderate spots
+ - 20% battery: Many spots
+- **Transparent Overlay**: Spots are drawn over background but under the face
+
+### Information Display
+- **Time**: Large digital time display
+- **Date**: Current date
+- **Battery**: Battery percentage with charging indicator
+- **Heart Rate**: Current BPM
+- **Steps**: Daily step count
+- **Temperature**: Current watch temperature in Fahrenheit
+
+### Visual Elements
+- **Transparent Sprites**: Proper transparency handling for clean appearance
+- **Stippled Spots**: Light stipple pattern for semi-transparent aging effect
+- **Charging State**: Different behavior when charging (no aging spots, no face cycling)
+
+## Controls
+
+- **Tap Face**: Cycle through Mr Meeseeks expressions
+- **Swipe**: Alternative face cycling (if tap doesn't work)
+- **BTN1**: Physical button fallback for face cycling
+- **Swipe Down**: Show widgets
+
+## Technical Details
+
+- Uses raw Image Object format for optimal transparency
+- 4-bit color depth with custom palettes
+- Cached spot generation to prevent flicker
+- Multiple input methods for maximum compatibility
+- Optimized for Bangle.js 2
+
+## Attribution
+
+**Character Inspiration**: Mr Meeseeks from Rick and Morty (Adult Swim)
+
+**Code Base**: Based on the Advanced Casio Clock by [dotgreg](https://github.com/dotgreg/advCasioBangleClock)
+- Original template: [Advanced Casio Clock](https://github.com/dotgreg/advCasioBangleClock)
+- Creator: [dotgreg](https://github.com/dotgreg)
+
+## Installation
+
+Upload via Bangle.js App Loader or manually install the files in the `meseeks` folder.
\ No newline at end of file
diff --git a/apps/meseeks/app-icon.js b/apps/meseeks/app-icon.js
new file mode 100644
index 0000000000..400739092e
--- /dev/null
+++ b/apps/meseeks/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4n/80Zpsb+9XylVBoNNlexoksjdMjdzquUrtPomTqvGiMvom0rtEAC1RiIAVC7EWswAEtwOFjYNFswXBieqAAOvAYWxC4lj1/61X///6nIXC2CjErU2CwdZnNwBomWC5EHzWWHwWTsDJFC5MAgw+BAAP7cIwXKABgXX1IX5gu7AAO2RwwXL8UiAAYXQhwWEkUgC54uFGA4XCjQXEFwwwHC4UZ1wuLkUvfYk7C4MV0dQFxQwF2dhC4MR9WVBANSC5JhChxGBC4UbnYJBCxUi4EAh+VC4cRC6JGBC/4XL1YX/C4sVC6EKC4kZ1wXB+QXK8EAg2rC4dq2AXBh4WJlwNBgFj8IXBi02BAQwKFwIABg0+C4MayAXDGBAuDAAOmC4RGCGBQuDC4kTC4owGFwoXKGAwuFC4cfC4wwElYLFL5QANI5QXPF64Xfgu7AAQjHC5Xj1Wq0cznJ3Qh2pqMRiMVseXC5OWs1r3dm3djyoWBAAMW1RNDAAJHDHoJBBAAIJBAAhNEmejmwXBB4oAQC64="))
\ No newline at end of file
diff --git a/apps/meseeks/app.js b/apps/meseeks/app.js
new file mode 100644
index 0000000000..70a16ce64a
--- /dev/null
+++ b/apps/meseeks/app.js
@@ -0,0 +1,385 @@
+const storage = require('Storage');
+
+require("Font6x12").add(Graphics);
+require("Font8x12").add(Graphics);
+require("Font7x11Numeric7Seg").add(Graphics);
+
+// Meeseeks face sprites - stored as base64 strings (decompressed on demand to save memory)
+var meeseeksSprites = {
+ face1: {
+ width : 117, height : 104, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24579,24580,22531,65399,192,1,22532,22530,47112,39904,20482,0,9,45952,8]),
+ buffer : require("heatshrink").decompress(atob("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"))
+ },
+ face2: {
+ width : 115, height : 111, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24579,65534,65532,65502,6368,24580,22531,10,0,22532,29280,27232,1,9,65435]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4AShGABzY4bxAOOHMwoRxBJNHFKDPVMoAF93gHEMOLyntHMMOESpzhHCw5B6DjzVolQHGznfHDI5BOTo4Zfy4A/AH4A/AH4A/AH4A/AH4A/AC8FqvuGFuIwAHFzuZzve6A3q93e9vtqA4ErNd7vdHNVd71d9vdHIfd7kMzPZHNXu647BNII5C93VgcFrKtBHFEO6sCkA5BF4UO7uyn+1OQPuHE8NcINEguZHIfd65yBzOd7vgHM/e6twhdZyrcChvdqvFrNZOVXd6tXM4IBBOQXdrOZzJCBOVPd7ItByudOQMJyo3BHQOdWoQAlh3dzNdGQRyC9vZG4JBBHFAvBN4I4DuAJCAwIICPYKsoOQNdUQIvDPgOZ7PdqA4oF4IADyAJD7OdBAI4qgGV7wvBUItVyvVHFatB73ubVIA/AH4A/AH4A/AH4Ak7Xu93qxXgGl0I93d73e9Xa9XuAoIGBG1MFzvt9Xn7Vau+q09XAYOt1vt92KG8vd9ta7QzB1QCBvQEBu45BBAVe9vpN0Xe7vXFwIAKvV6u+nu9d73QHD9d9uuNIIuBNwI2Fi8XBgIOCvWq9Xl0AdCxBwZrqaBGAenFoURiMXuIDBiK1CuMXHIOlfAIeBhA5UCgcN87fCHAWqUIUXGoQACi0R0IFCHoNn1vtHKoTEryoBOIl2s6kBGwhzCu1204MBuOqsN60vlqAlBwBxVvvnVQZmCszaCHQKjBHYS5DAQQKBjWl1pfC8A4U9ulHAOnMoUWUAI+COAg4CW4OmAAOqCgI5B7oiBxw4RQoXacQOqtRwDHIMWGgVxWwdxi12BgNms1hCgTHB6pgFABkOQgVeDQJbBjQlCsMRAIMWsMXHAayBB4IQCG4QGB1Wt6DmRHAdd7WniOhiNnEYSrBHIMRi45BVQZBBJQoUBs967zmCOR/uHgSpBFQOhuwhBE4IACGggzBWII3DAAQGBixyBLwQCCOR8FcYOni9h0NntQuCVAI4GGwSCCB4KECsOn1XlqAoBOR7kDOQNxGAOhOAyrBu43DAAMXuNxuzzBiI/BjQ5B9pyRVYRyBMgMXs9htUas2mdgQ3Bq5xEAAIVBAAMaJANmjV6coJyRhBJCrQhCi2h0JbBix5BLoN1urvBAAll0ulRYI0B01qvWncqeOAQMN844B1TiBs9xMYet9Xe72qGwWGs2OBAPt9XaHYI5B1Wu7QlBxByS891HAVh042CNwItB92gDY8O13q9xGC1Q+B7AoCwByRgvVu9xbgLdBu9a7pbBGxAAFHQPtxwDB9xFCVZ5yDh3au7MBG4NVrvu9QdOAAeKxHqEYUNHB68E7zLC1Wt7vu1w3SW4/VCSCsCgCMB13tNwPQG7I4B9ISQhDBCgGoxHu1DeOS5rnDHKC+QESOIxyOTHMWIxDAV7wwYKD0O93tRSY4CwCLffgIAB9A4zHYY4JhyiHHEgALhAxFAwI3uNYYACx3uHGQ6Dxw30AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ADg"))
+ },
+ face3: {
+ width : 125, height : 99, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,22531,65400,0,24579,160,24580,8,4,32,9,27264,5,3,2,1]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AE1QMLhQ9vhWgBi4+xRJi7wHuIyL1S6wHxb4xGYQ+Ig49yHxN3uA9yHwN3YYw90AAN8G4aDBXOZ2EHAbBHAG8FznM4A0wrF3fI2Z/3u9NVHl0H5vu7vdqAJDz3vmc1zI+ugvdmc5jM1HwcFzPzjcgqfhH1uezORgUFqo+ChOZyebkUr9uAHtnN8eZ2Ui3+VBIWPzMzBIO9xg9shnunMxOQPfOQUM/OTn2wh3o5R8t+eZrcAqvYuAJBx2ZjNVrPzw+gPlg9BysVr+e44JBhB8BnP5if9Plvfyo+BrMV9C7CnOZ/+TmeT7g9shB8BAAOVn2FBIMJzOf+Mzmf94A+s5nemOZOYPdBIUJ/48BzM+6A9sgF878z+czz1QBIUF/8f/P+6I9tgHI9/+9+VHoZ9B9uI5i5tAAXN7vY6uwBIkF896GMmqBhcH1VgN1mMvlwEkMK07GVquM5GNMcWn5HMH6cN7nMiCij1Hs5nFCqPMu64iAAkM5HZCaHc448mAAVdxARPw/MAwnhrewP0d3CB0HPYcKvHpqoABqA+hg96Jxw9DhnM6e7rY/Cqo+ghQ+OXIcM5EfiEikEA3dV7GDQCtVr2IxHI5obDHx14CYUMwPhyOykUiBANR2sVyuYHaED72VzOf90x/uINIcHuDKM4A9C4PUoNbHoIAB2OwgEL2uexmnEJlc7/5yu7DAMAgsfjmM+AGBu+gDhd3AQMB7thoMRzY9CgpDDYIPt8f5qoACrYBCAAJ3BHYO12AXCkFRyMRxCpCGAQAJ1RLC5sRAAQ5BkGxrMSQQcLiEL3Y2B/46DrNZyo6BgEgqsCCwW7mgkB7w+Bg/HHpUK1S6CiMWskRqO73cVLwImDkG1gUgCgO7nYQBAAUbgA4DTIcLmI9BjuIfRw9Cg/Is1molBjMxnKBDAAW1qBDDipJDkUl2QEClcxXYUrjMRokRjHAFwN6Pha6CvkRs1kDIMRTQORicbModRHAZDEIgNbBYMClezysbgULiqiBoMRjmQNgIxCPhcBvB8DfoYAB3cCE4M1QQcLnJ2DkW5IgUg2bYBrexqIcBsJ8B7wxCHxR8DhmBDINGHwszre72bhBiA9Cma7E3a1BkEAHoMRqsxma6BAAUf+AxBOAR8LgHICwMUswbCihCBiczicfNQMbgGxzMVYwg1Bje7zI3DyJ7DiMZ/J8RgPNW4sRokUAYMTFQcZyqpC2A9B2g0ByJ1FAAUWAYU+qB8OXYw+FojBFipEDyMx3dBqJzCHoy6F9ZwFXZmdicZmOTFAQABoI+FiL9BAAWTiYTEaYRVCJgOTIAMe6IsBg58LJQmNHwMV2cZHIZ+DAYKLGIYg9ECIIgBqNZBIMYXQL5MBgNwAYMF5sz2EA3InDFIjAEIZA9EokZje7hexrPY4B8CHpcK04EChHT2EikGxVAQCBFQJCBHwMUGIY+IAAdbgUikW1jI9ChF6HpQ+BBocFzYcBkEViuZiatCGog9HAwSJBiczyIABHoUg2ucFYXMHpaKFHwlVisbitTGYfxYgZEJiZXBrNV2Q9DqoqCviACHxYOD5nVXgMryoiBhcZFoPhmOZHIWRzJwBHoI6BBIMx2EghZYBHo/Iwo9MJoNwAgUMitbgG5MAULqI3Bre7irCBmOV2tRyK1CmJEBiAWBlYbChdV6AtDHpwQB1QFDqNV2NbEwIjBysZqEigGxyNRJoML2ZDByOxqoJBCwUrrZ6CqAlBhWIfBoACvWnAocKvtV2EAE4O7ysQgQFB2qLBAoI+BjMbgA1CSYQWBzdVJ4IkCvHoHp8AtWgAwkFEAI/CjJrDkG5OIkVnY4DRgIPBhcz+p6CcIN8fgYAVbQIAB3cFOoQ4CnJxDleRBYe1gUA3dVnvhEIcHvnAHrAACHwM1zNb3YIC2NQkAFC2uwHwJUCXoOZiofFhQ8bAAUJ+KBCAANbytbAAZKBrYLBmfzyufGrwAK1Vq5v+73YxuI73l8uNxndxHI+vwHdIA/AH4A/AH4A/ABWqHvugHv4A5u49wu9wBREKHuMK1WqBQ8MHuLtCH42q0/Md+mn096u93IgTEJAFv8vg+BvS3yAH4A/AH4A/AH4A/AH4A/ADYA="))
+ },
+ face4: {
+ width : 106, height : 85, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,2,65534,65533,65340,23008,65437,37664,8416,5,31264,0,14528,9,32,12736]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A2zIAB8AqmjPu9GQBAkBj3hjMe+EA6EN6AzghOR93uzoIDhGR9GO8MR98A7sJGkORjG+90RSoY9BqEP30Rzw0BNYJogiNQhhqB9wJDjkCkGLiMRGgQBBADUNyENgHZFQMw9BrBiByCjlCkG4jERiHd6A0eDwPZ35fB9w0BwA0CjeghmL3eLGgIVCGjZTCzMbuyeB3Y0D3e+3g9C32wNMGdNIMRxnO8O+3ewBoMRjcb33rboJnBGkOZiJhB92O8LTCBIXr2Ph8I0g6A0EGYPuFQINCyIKBwMRiMAGQPQab2QhI0CAAPoBoUJH4IzC94wbGg3QGgMbGYPhjAODzI1BBgPwGj6JBGwKfCAAMZwANDhPZzMZjgzgXwiVBAAQPGzPMGcKkGGZAA/AH4A/AH4A/AH4A/ADndGmnQGn4Anho00alsBGmcJyDUFTFo0zNIcNAQOZGmBmCGmXQgEbT2OQh2RGmEJzOJGmMNzOZiA0xzgztGgnd6AztMoI007o0CGdwxEGmWQhudGmHZzPZaV41DTuIA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AhA=="))
+ },
+ face5: {
+ width : 111, height : 99, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24579,65503,22531,61343,7,24580,6,160,47112,9,5,8,10,4,3]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/hva1va7vQGt8K1Xa03atpsy6EM1nK1Rvw7sK4t3u915Xd7o3vqu+mAGDOEIiB1XK5gMI7SuGG8EN1orB1XM1otG6wWH5nATsmtLwjVKqo3kSyPsb8BwF6EFCBq4FG8SiJQAhulVIYOMhQ2mMB5uh7poFNxrZj1WtAwesAgeAhGP/EwA4UOgHMHEOq0oECgtwAgUIxGO9e73wHB/fwhmgG8MG096GIP4GwmL/2+u9Vvd3vBuj2tT3d84o2Bh++v42B/83rnF4tVq8AhlQGr4uB/3v/d8FIMAutX32DweACQcD0EN6AzchkA6EI90///4mAMD4uwCw7uB7o2cguw8sA90EgeIx4WOqBue98IuH+8EgnGIxwWNGwJudh+A/cO/8wGwOP8AND1QWH46legH4h+z///xAAB/wMDTRHFIJJuW+HnnA2D/+wG5d8uEKbjsAu8M1mI/H4GwP/hVwG4d/Cgl1qEH0A2dgBWB4Y3C//vhlXBocHu93mEAgvF4EAAIIAghGPNgP7gFaBgus1Wq5lcqEKNr4AEvfv/EA5jOH7vaqEAhWtGsUN7oECrRgLgoRDAEHd1kAh98GxfKNsY3C4u7rg2L1SxHAD0Mq+wBxfaGsoA/AH4AJxGAEL8IxGO3YAF2973/4nAUG/3AGjna4/vxGI+f/n//AAQGC/GP/dc6A3E2+gNDM3utVrc+n8zweDAYIAGBQP//d8GIcHq43XMwP+907nc3mc4mcxGo44Dmfz9ZxDgtVU50NGo+InE/MwM2scziMTGxQAC8c//0wN4V1N5sN7oFDg/o/CaEsw2BmI3BAII6N/+4EIMMqo2N7QFDrfvTgIAFm02iIABHQIACG5foEQMK542LhXMAgV13fj+Y1GNwNhGYY4BcYoHBi1mG4eDEgPauA3Ldgf+9wiETQQrBsY2BUYIACBoKsBV4cWAwcz9/wEoPV2A2Ku9QVAPvNYhhCAYM2G4MWG4oADG4R8Ei1jn3gFQNXGxUOIYMNn0/NooADi0xAAM2NIYAEmxDBAILqCs0+/0wE4NbG5Xub4N7NgpZCNoUTsYHBAgNhAAJBCAgMWG4axCeYP+UwSZBABGrdQNfwY3DAAJkBmw3BAQQmBIgJzDNgTaCJgJBBYAU/3orBqo2Jht1gEHGweDnEzcQgACLwIIGJAI2BNgI3CCQI3B/3QgEFNxXXboPoxH4////GIAAWPHgKwDIoQADAwJyBAQI1BW4MxHoM//YrBuo6BNxFXgEIxH//273w3DAYP/+Z0DHoUxs0TweBIIQxBOAgCBnBfBg9dNxPc2wDBhE6sALEgeD/++9e3m6vCi2ZzOWyJ5DGIQABsw7BiymBD4O1NxMA4oLKAAXG6t79/zwNpGwIAByM4VAIwCAAsTn/ggELNxUFvg3NgGqq/v/9mG4mWVALWCUoQCDn+ObphDButeG5qsBre1G47cGHgc4NwMH5olKhHr9167o4Nhd80tWHAeWnDeCAAIzBHQU4xAXBrlgG5e7uvKVJw4Bnk8OIbfBmcRicxmMzm02mcz/GAgGsExsNNpwTE6+2G4SmCNwQ6Bs1jmf/+EAhVaE6IAQ4fHm1py0YG4szs07/7cBgFVG0S9ButT4ymCU4JuCAoJtCgF3qA3jgEM4ezmeDwYyBAAWD/+DB4W7G0gABr3/FwI1DnA2B2A2CrY2mVII3Bx+IAAX4/8wGwXFHYQAmweO/w6BAQKjCAAPF0A2oAAXj2dz2YIEq/AG1axI242Y/A2b9bbYr3wGzNcq/gDbHvDLEOqtcqAcYh6mXhXM5mq0CKZxyJXGwPdfDUA9w3WhnAGrY3CuAfdAC0I/dVG+kAuu3wAHEhVV9A3s4u/+9XAAN+89XrSoux+3ut3u+72utVOEM1ut7vQcmoA/AH4A/AH4A/AH4A/ADIA="))
+ },
+ face6: {
+ width : 108, height : 72, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,8,65501,29248,27296,25184,12512,224,10464,32,0,8416,4576,6624,7,9]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH3u92q9w0whw0BzOa0AKE32uIII1m9Hq1W73OZBIcKBIOu12wGsuu1erqEFzNwBIWaGgOo3WpGkkLMAOmgUgGomu1OamEG3Rskh2q1WoGoWrBIepzIJB1XgGsmuGwNQg2a9wJC9WpzA1BzXvNc2q1eZUoIJBhWqzepoEGvQ1lNYLOBGoOoH4eZzN7vOZGskK9RqBZ4OZyAJD3IzBy41lMII2BGgOauAJBhOaGQI2BzPwGscA3yiBzOe1IIChp0CzWp13gGsiYB9WZ9wqE3w+B1Xu1w0kAAPu1W59w/GOgOuiI1mgGL5gIGx2X13uiA1nABPdxYzxAEf/+AecDqkP///KmXuGjxWCGyMO9zBgh8eCJ8HGkIAB8KOPhPgGsTaBap40jGu0f+DoNIp4AViLHNGsIhBM4URIp4ZETbX/K4fhCp0edghPDM6nx8IZD3ygNDAQvEGwI3RGQIADBQnuiAcPbAwfBiMecZIwEGQyPCj6FRMg/u8I4BAII7BA4MeA4P/jxeJh77ENh5+KiPxGgIACLZvxap4ADgJ/RRhpqTNgSjHACoeWh71HDtpscDjJsbDbMPbLoA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACo"))
+ },
+ face7: {
+ width : 132, height : 97, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24579,65433,65400,65367,7,1,128,22530,8,22531,3,2,35552,0,192]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4AuyAMLhINMAE2ZIP40NJxhBzQWh3MzJB1PBTE0G5bE2XZSC3PQI5GJJAAxrJCEJA7H4IAKC5ABH733vG+kG83/BAuP5m83lvIGUL4xBBuoJE3lmAANrIOV+93ut3FqAICgu292AgHrrKCx933vsA9hBD3ZBBwUi7dVIOFc8/n+Ui7nFIIfOs5BC2uQIN++u9+6RBB9ZBD41nYoOM2qODAFi6Bv3AkGPvY3CIINuv2Hs3LIOFct3uu+v+y7D3dm93nv3utmwIOHO8/n+/sIIYJB9wACthAvPIPOw9392H3YJD21mAANu5hBy8/u9F83gJChe2tnMs1s3RBwgvGXQXr34KDu3mQQNrqBBwgHGXANmG4v/9m8514IGMAg+73e28wKF/dV3HwIOUA2tVr430AH4A/AH4A/AH4A/AH4A/ADEF/4ACs/Gs93u/m5e13fL34MB+A+srdb3+P7/f7v3vvdAAX3tvd7HfKAd3H00PFYWPOIvX+EgAgMC7+AkAGBhv97vf73m94hEhOQHKtVqAGE3fvx/d6EAx+NmU0ocgx99ocikUt739oUiknf+kikEN7H+LImZzJCUhOVCwcP23svp1Bkct//96lEmVNvHu6A3B7H3xxRBhuPwEiohMB/7NCIS8FIAt3xsI/p2BkgEBQgI1B72Hx197vY/+IvHo/H+x/9olEkbKBCgJCEqtbIKOZrhGD2+NgUI/EAmkN7+IxH3x4/BAoPux2PAgOIBAWI9CIBbYJGBmBCBrQpC/byFABeVCQe7wEykUPOYPdx/4GQV4vAECxGHxBCCwJFD8+Px/3xtDRIMN2ugNYVZIB8JyoECg1tgT2BFYOP9w6DAAeq1AECwMa1Wh1WKBQRSC/8EolDkGG36zDegbEMCAfM9/QkjEBHYh1BxWo1QABIIWKHgIADiMaSImNQYMIvFs+ByCIKADCgvI9H47BAFxERAAQ/DHIMYY4JDDIIKPDxH4aYP4vG72BBRhJBDze6YAPnEgMYIQ4yB9C3BOwPo93oJAOqIwIBB0INCIAP4BgNbGIWbIKMJrQmBHYiFFAAOO92OHYJFB7vdx9+w4ABBQTaBAAhBCYwJBQSYUFqpBExSDDu9+PoV47vY+4EB6kikUNx6MB/oDBIYWOB4OHaQW7wAxBzRBRhYaCQYhqBvA8BO4WNoc07vnx8DkciknY//dgkN7w/BSYLQDAANc4AxBypBS3WpPwYAD9HQklA73v6dEokwx99oczmcjnv4gSJBhH3w+IxuH9hAC0uL2EAhT3CABcFIImq0Oq12HP4LDBIAJ+BhvogckmUih/wkdCIQMg/sCmdDJoP46lEpuPxWhjTFB0BzEIJlZIIXKIIOK0/o++ONQIpBokikH/gQYCh/9DwcN/4DBI4Pf/oWBokI/CDC2q1FABcOIIe11WoX4QuB+9/ggpBoEAx8AFoP/AAP3/93v/vAQONJoWAIIUt/HB0u7qAMBYhwcBKoUI3eqxGOxp9CHAMDmh2C/+73/4/Hf7+PAIX42tV24PB6/wgFEocA/GHxm12CCBIJ8A4pWCqu7xF4X4R9B//dPYQABMh3Xs1nv/9/vf+/M5egYgRBQ+pBChdb5lnX4MA7vfHgP9/5vBACELxlvK4W724aD/OQDp8FqoEChlbraxBEgX4HySQOtITRyqECTYOpq6oBXpwAThNbuAURQgJCDAEsJqtZCydVrewIE2ZyryDACW1IYL/ZyA/JAAQlXrdV3a3YGhJAaAAMFqtQXC6DJAGxB/TgSC/rbdXADkJBRMLQWsJzIKI2uQIOxCH+qC1IQWVIQsF2tQIOyFCqp9CzOVQW6FEqqHBzObIHLACrJABZQoA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AxA=="))
+ },
+ face8: {
+ width : 109, height : 92, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24580,65470,65468,24579,22531,0,33440,6304,22532,47112,29408,6272,8,6368,25024]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4Ag7vQFlfAA41mtts8A0n91t5ttGo2Gs1sGtHms1m6xjD7vO/1ss1oGs3O91/GwPdBAXmtcDhAJBszWmt0Cl9m5qaCg3WgUj9lus1gGskN43gkUMtnG6AJBs1rmcM4xsBVoYAh61ogZsBFoIsBhtm9+1GgNttpsls1o//mFgKjCGQIHB6wJB4A2kGAIuBw2GtqjBH4IACOoPONk3GAANoTIY9DHAQ1kgHNsxtBG4LPDtptD7p1BUco2BAAKiCgEMawIABbA48fhieBFgVgVoY2CNY/dBQQ2chw2C6wsFFYNtyAVH91t7o3UYZHdGwPgBQwHHC4o2RhoTBGxHZQ64hIGpLxfGwg1QF7vN7vcAwduPjwAM5PcAAPZ6wiChwmNhoOChud7udyAzShHM7ts6vJzNc5ojDGxihD7vd7ICB43uGh4aB5vF5OVrOZyuZ5raQBoUNzIACy3Ws2O9ozMQANZNAOZqtUog4B6xsPGoXuso2DyvJ41mOgPc5ngCwi0B7PZ6ouBAANUotJAgOdUZ4MCh1sqqHCqtFrOVzmcfwXWAIPWttt5mdJAQ2CoNBjI7C5jLFNhnd4o2BquVKoNZLgeZ5IAFBQYAByMUikRiORHAJoC7psOhvVqo2CiMUio2DOwQ8EGgoyBoICDjPNbKXZGoKJBogCBpI1DOwQOBHIo0BoJqBGYIYBimcNgY1LNgtEoiLBAAIfBoMVyuVMwlBrMZMoJnCimRCYIFByttNiXdqtUNYI2DomRqJdBymUTAjQCiNRoIBBAAcV5vAEoI1MIYUJ7lFGgSlCfgVByheBAwQAGpIMBrJrCotWGoSiMUYnFqhrDKoVRqlUqtFotUO4YRBisUIQJ4BO4UV5hoCURpEDww2BNIInCotJqmUqIABHINEFgIACOYUZymRC4MZrlsGqDaE4ooBAAVJqriBAQNEIIJyBWAQCBUgRNDqudGoaiNIonc5OVGAIeBGYQAEGoIrBF4Q1CJINBBwPN5hcFGyFdtosCK4TVBHYIAEBYVZGwLfCokVGoOQGqQQBGwSFB4vEMYJgBGwIEBFYJwGU4QKBzOdGqoREhilBzOZyKSBFoOqAAOkotVAII+Ca4MUpOc7gwEho1QAAvN7vZ6KTBGogABGgJtCNQVEzNt5vVGCxzH7udytcGoNKGwdV4rfE5nM63NqA1dQwOd7rfBoJsE0lcAAQzBAAXAGjw3Es3Nb4YABzmWGQfc4ozhAAkOOAOdAAbmB7nc8AzmAH4A/AH4AphvQG2ndGv41rUWg11hpr/Gv6ZZ7oGGNdovBGAQECTmAADsDUxUwwA/AH4A/AH4A/AH4A/AH4A/AH4A/ADIA="))
+ },
+ face9: {
+ width : 113, height : 101, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,0,65534,65469,24580,31392,29344,65404,3,32,4,9,27232,1,5,8]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AHW72ANLhf/3Y2mE5/7HEu7+ARPhaBNACsP/43QHEkO/aEU8A3fLYIWU92AG+sAzy+RG8kAz7jdG7EA/Y4cG7MADLIddKTQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ARhe73YJGhOa8A3qhFP93u+AJEzOZzWNG9OZzsR7vY0AJD9o4BzPYOVEIzPArIuF/Wc5g4BxQ4nh/p48jrMdxyqC32pg81HAOuOE/q6kzkuRiOvIIXo4cjhOZzGJG83+7OQuFJFwKfC9WJgU8OAPd6DgmxuZygtBzOOBIMOBILhCzuZOEwqB7I3CzJwCGQIAEyA3laYOROIZwChJwBjI4DOE0IxIsDxXwBIOe1p6DxA3mgGa7MdzudxCoCgHu1o3C7HQHE+ZGoPY7BmE91NOIPa0A3nHAOq1WozgJE/2oN4J5DAE0O///oAJFpztBBIwA/AH4A/AH4A/AH4A/AFu0G2sP//6xQ3zhuupWI1w3yhOU6lE6mIG+WZogAB6mNHGPpG4VExHgG+Hqyg4DxXwG98O0g3DomOOGGNG4naVOEJpJw27o3EpugG98Kwg3DpNIOGHoOAlJOGEOxo3ExRww9RwEpGAG98JOAmY1JwwhvZN4eOgEL2A4uzOUptEptK0EA3Y1s/Y4CztNpGKcAO7OFkLMwWYzPd1VAVF5mDzOZxRBCPQQ4vAAn+G9qpDG4ugHGvu8A3tHAP4Awn/+A3ugG+1IECh/vG+EA13u/e73/vU95sD/4ACG2LjC3e72A3zAH4A/AH4A/AH4A/AH4A/ADgA="))
+ },
+ face10: {
+ width : 156, height : 116, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,10,53856,6304,9,11,41664,65533,1,51712,25280,0,2,8,29344,7]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHcwIH4AKh8zmfUGFn/GAJ/Z4c+8czJlcEmfjGDMNnOZzOeTdnpGAPjTi8PDgVuJtnmzOWyZNayxNvzMun4bWhhNC9xNty2Sz3DDa2jn2Wy3gJtmmtOZ8bpWgczmfunxNt11u90zdK0zn3pnJpQqoNMqAdOh3u92e8czTSuZAAQVPJpkFJqAyDmbrTnfpDIWeCp5AMJqEKtIzCsacTnZnDJqCcMexqbDJoeWnhNS4eZyUpJqaPJgpNQTYmWn5NVkUiJqMBJpNBdB5NFzPsJqXTCwKbBtwXRSBENoobQJoaBBdKddn3p93uTaLfBSI8EoBNRs3u9Oe8czJqUEmZOBJ4IYSqpOFgtUDSKbC90zmcwGiUA8c59OZTaRGBdYgFBUY7pO8ZMUgEzJgOWtwYTiMQUJJNRyc6JqnfJoOZJqkEJoYAC//9JqeemZNUgfmyycBCqP/4YIFh88xk4HB5NCyWTJqsI31ps3jJiE85n8Fws/nG7AAOwJp3uTYNjmBNUgGDmYABCZ5MB5k73nAKwfzxez804/5rO4cz8czJq0M+YZQgf85cz90755WE2cwu8DBIYALP4RMWACcPTIM2IYPMJofInZNC5lAHdIAR/nMwZDBueM6AJBnGIxE+u9rxhN8//I3ZDBsZNDnnD5HDnU7BIYA5+fD3czn0z3nwJoXMxnMxGzBIYA5nmLnezme73BDC/+7BIOz3e8wBN8ne4nE7xZNDh+D3YABneImBN7hnIxGI5E8xhND5G7TYOI5hM7IYPMxHDAQJDE5nIxgIB+ZN8gHf3eMnG4wamE/nM5nPJnpDBxeM5E7mAKE5//5jxCAHsz//8JgoA/AAsPIH4A/AH4A/AH4A/AH4A/AGUFqpB/ABlVqBB/Tn4AaoicvPzkBoLM/ABkU7qa/dZtFJlUBqANL6kUogAFiIgJ6ghMTU/0///5/RiIAEilBiMVqNRiNEC4kEipMogp4H5n//pNB/4XJqtRUQf/NganJJj6aGqtE7pJJABNRqtVNoUEdU6aFhv0bAIhXEAS/IAD5EE3n8xhyiAEMPooECxn8mDAjAEMNiAECqIlfJs9NEslVJkpNlqqamgFUEcUFTU7DkTVB3BTkMFJlAAB//wFdIAhh//IP4AMhvfTqrgrABVP/8QCydVTu1BHClFen4ALh/xIP4AL//QIP6aL+BB/ABfdJv4ALqsRIP5FFqBM/ABUFqNEond/tEoJTEAH4AC//9JoPUoBF/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AlA="))
+ },
+ face11: {
+ width : 109, height : 111, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24579,65271,24580,22531,7,35552,47112,2240,65431,65434,39776,22532,43872,65335,35712]),
+ buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AGVVBpkFqA0kgtVE5tVIppqXGpxGQGqwSRG0IiTG0LGUgo2ffio2ehA1VGwIXWGro2erAYXUjlVDjA2bDbSjcAH4A/AH4A/AH4A/AH4A/AGNVqtQFlOIxAHFhFYGwNVGk8IGoJjFhGFBII4BGtOIF4IIEowKCG00FxGAsg2BwqqDtWgIATbmquA1RmBFgdYxeq0g2orGE1RjFOoJsEUcpoBxuqtDQEAgOEcgZslFguIFgI/BAAZsnrAtENgg2EbMwtFbIQJBOwVVGsosCGooJBwp0DxA1kGw7jEBQLYmFgQ2CMwIJFA4I1ngGFrAACO4tVquAGs8AhFYqo1FAH4A/AH4A/AH4ABrGIAAY0uxA1BwmEww2BwuAGlUIF4OAh2ZzPqtA4CG9NVTYOGl3p9Xq82A0EGG1NVoEK1GAkUil0gxEJzOQotcUE1YgGq0FIsUu93gpGGlWqhGFqo1kguIoHqkFoxEdgFmxFEoEig2EplVUseFxmIpfWpGEpAABwlEG4IEBpnMrA2iGoNUxGEFwIABGgIABGgIABpnI4o1hrHFiItBwIxCAAgLConM5GFGsFVqgqEAA1BigNBiczolVGsGM5nBFQIxEHocRmMUiIOBoo2fa4PM4YpBTwo2CikTodDpjgCqA1dxA1BGwIvBAA0Uikcjk8jhEBpnFGzsIa4NMGpMxNIPDAQUzJIKjdgtUwjRBiY0GiczNAIADikx4dMGztY4g1BZoJrHFwPBNgIBBmkxmlVUblV5lEMgQ2FAwMzmdDGwQABmMzolFGzEFAQWFoJkDGwgrBGoQ2DAIPBidFUbEFJ4VYUIIwDNQY2CibZEikUBoNEpBsYJ4UFwiiDbQjWBiMzGQI3Bmg6CocRphsdLAQzBogACA4bbBAAMUmkUoKABmnFNjguDSAQABHwLfBG4LWCoNDoPB4JsBwpSCbLQ0BijdBmYDCHwVBUwJsECgMUNgVVN6psDwIsCpEzu93OgKYBFgIxBmkzA4IACIoIcCNjNUNgVEi41Bug2DNgKkBjg4BmlENwVIDgIfDGysFwgrBGod3pA2CbgJsBmJsDGwMUpBsBgpvCUa2IFoNEo41CHYLaDNgMUjgABNQSiBovAbK5sChA2CoI1Cu4nBAAdMpjZBNAI9BBINIrhsYUYlIwI1DumBFIOEAANE5kzmc0dYQ1BGQQ1WC4dYwg1EuJsBxHFxFYrFU5nM4PBiJ5CqqiBKgRsVbQeEi5sEwlYqtcqlV4o3CWYI1BpFYQ4RsXbQjYEuNIwtIPwlVrHFcIWExBoCGq4YBGwVYoY1CmfFqmACQtcqoyBpGIxANCwqiWJ4kFSgMzmfMqvICZHFOANVxAHCGrI2EEgIoBrgVMF4iICGzZeCDKlUGrL0YQQJraGwQ3VCwI1cD6QRBAAY0cRgY4OCAZqeAAeILMAA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFg="))
+ },
+ face12: {
+ width : 136, height : 135, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,24579,24580,22531,65400,8,65270,22532,65369,22530,47112,9,11,20482,0,10]),
+ buffer : require("heatshrink").decompress(atob("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"))
+ },
+ charge: {
+ width : 176, height : 176, bpp : 4,
+ transparent : 0,
+ palette : new Uint16Array([65535,65535,0,19967,2040,61309,2041,17919,44373,2048,19935,40607,2008,38559,17918,2009]),
+ buffer : require("heatshrink").decompress(atob("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"))
+ }
+ }
+
+// Current face state
+var currentFace = "face1";
+// Ordered list of faces for cycling
+var faceNumbers = [
+ "face1", "face2", "face3", "face4", "face5", "face6",
+ "face7", "face8", "face9", "face10", "face11", "face12"
+];
+
+// Get a random Meeseeks face
+function getRandomFace() {
+ return faceNumbers[Math.floor(Math.random() * faceNumbers.length)];
+}
+
+// Set a new random face
+function setRandomFace() {
+ currentFace = getRandomFace();
+}
+
+// Cycle to the next face in order
+function setNextFace() {
+ var idx = faceNumbers.indexOf(currentFace);
+ if (idx < 0) idx = 0;
+ currentFace = faceNumbers[(idx + 1) % faceNumbers.length];
+}
+
+// Decompress sprite on demand to save memory
+function getSprite(spriteName) {
+ var sprite = meeseeksSprites[spriteName];
+ if (!sprite.buffer) {
+ // Support legacy heatshrink-compressed base64 (sprite.data)
+ if (sprite.data) {
+ sprite.buffer = require("heatshrink").decompress(atob(sprite.data));
+ // Support raw base64 pixel data from Espruino Image Converter (Image Object, no compression)
+ } else if (sprite.raw) {
+ sprite.buffer = E.toArrayBuffer(atob(sprite.raw));
+ }
+ }
+ return sprite;
+}
+
+// Draw the Meeseeks face
+function drawMeeseeksFace() {
+ var isCharging = Bangle.isCharging();
+ var faceImage;
+
+ // Show charge face when charging
+ if (isCharging) {
+ faceImage = getSprite("charge");
+ } else {
+ faceImage = getSprite(currentFace);
+ }
+
+ // Draw the face centered on screen based on actual sprite dimensions
+ var centerX = (g.getWidth() - faceImage.width) / 2;
+ var centerY = (g.getHeight() - faceImage.height) / 2;
+ g.drawImage(faceImage, centerX, centerY, {transparent: faceImage.transparent});
+}
+
+// ------- Aging spots overlay (more as battery goes down) -------
+var spotCache = { batteryBucket : -1, w:0, h:0, spots : [] };
+
+function randomBetween(min, max) {
+ return Math.floor(min + Math.random() * (max - min + 1));
+}
+
+function computeSpots(centerX, centerY, width, height, count) {
+ var spots = [];
+ var margin = 6; // keep a small inset from edges
+ for (var i = 0; i < count; i++) {
+ var x = randomBetween(centerX + margin, centerX + width - margin);
+ var y = randomBetween(centerY + margin, centerY + height - margin);
+ var baseR = randomBetween(1, 3); // base radius
+ var blobs = randomBetween(1, 3); // draw 1-3 small blobs around base
+ var blobList = [];
+ for (var b = 0; b < blobs; b++) {
+ var ox = randomBetween(-2, 2);
+ var oy = randomBetween(-2, 2);
+ var r = Math.max(1, baseR + randomBetween(-1, 1));
+ blobList.push({ x:x+ox, y:y+oy, r:r });
+ }
+ spots.push(blobList);
+ }
+ return spots;
+}
+
+// Lightweight stipple fill to simulate translucency
+function fillCircleStipple(cx, cy, r, spacing, phase) {
+ if (r <= 0) return;
+ var rr = r*r;
+ for (var dy = -r; dy <= r; dy++) {
+ var y = cy + dy;
+ var dxMax = Math.floor(Math.sqrt(rr - dy*dy));
+ for (var dx = -dxMax; dx <= dxMax; dx++) {
+ var x = cx + dx;
+ if (((dx + dy + phase) % spacing) === 0) g.setPixel(x, y);
+ }
+ }
+}
+
+function drawSpotsOverlay() {
+ if (Bangle.isCharging()) return; // no aging while charging
+ var w = g.getWidth();
+ var h = g.getHeight();
+ var centerX = 0;
+ var centerY = 0;
+ var b = E.getBattery();
+ // Bucketize to reduce flicker/regen
+ var bucket = Math.max(0, Math.min(20, Math.floor((100 - b) / 5))); // 0..20
+ var maxSpots = 60; // many at low battery
+ var spotsWanted = Math.round(maxSpots * Math.min(1, Math.max(0, (100 - b) / 80))); // 100->0, 20->~max
+
+ if (spotCache.batteryBucket !== bucket || spotCache.w !== w || spotCache.h !== h) {
+ spotCache.batteryBucket = bucket;
+ spotCache.w = w;
+ spotCache.h = h;
+ spotCache.spots = computeSpots(centerX, centerY, w, h, spotsWanted);
+ }
+ // Color: dev blue; outlines + stipple for quasi-transparency
+ g.setColor(0, 0, 0.5);
+ for (var i = 0; i < spotCache.spots.length; i++) {
+ var cluster = spotCache.spots[i];
+ for (var j = 0; j < cluster.length; j++) {
+ var c = cluster[j];
+ g.drawCircle(c.x, c.y, c.r);
+ // sparse interior points; spacing 3 gives light fill
+ fillCircleStipple(c.x, c.y, c.r-1, 3, (i*7 + j*3) % 3);
+ }
+ }
+}
+
+function bigThenSmall(big, small, x, y) {
+ g.setFont("7x11Numeric7Seg", 2);
+ g.drawString(big, x, y);
+ x += g.stringWidth(big);
+ g.setFont("8x12");
+ g.drawString(small, x, y);
+}
+
+
+
+
+// schedule a draw for the next minute
+var drawTimeout;
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+function clearIntervals() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+}
+
+function drawClock() {
+ g.setFont("7x11Numeric7Seg", 3);
+ g.setColor(1, 1, 1);
+ g.drawString(require("locale").time(new Date(), 1), 75, 135);
+ g.setFont("8x12", 2);
+ g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 8, 145);
+ g.setFont("8x12");
+ g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 8, 130);
+ g.setFont("8x12", 2);
+ const time = new Date().getDate();
+ g.drawString(time < 10 ? "0" + time : time, 28, 120);
+}
+
+function drawBattery() {
+ bigThenSmall(E.getBattery(), "%", 10, 10);
+}
+
+
+function getTemperature(){
+ try {
+ var temperature = E.getTemperature();
+ if (!temperature || !isFinite(temperature)) return "--";
+ // Show Fahrenheit
+ var f = (temperature * 9/5) + 32;
+ return Math.round(f) + "F";
+
+ } catch(ex) {
+ print(ex)
+ return "--"
+ }
+}
+
+function getSteps() {
+ var steps = Bangle.getHealthStatus("day").steps;
+ steps = Math.round(steps/1000);
+ return steps + "k";
+}
+
+
+function draw() {
+ queueDraw();
+
+ g.clear(1);
+ g.setColor(0, 0.7, 1);
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+
+ // Spots should be above background but below the face
+ drawSpotsOverlay();
+ // Draw the Meeseeks face on top of spots
+ drawMeeseeksFace();
+
+ // Foreground metrics
+ g.setFontAlign(0,-1);
+ g.setFont("8x12", 2);
+ g.setColor(1, 1, 1);
+ g.drawString(getTemperature(), 20, 40);
+ var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm;
+ var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--";
+ g.drawString(hrStr, 15, 70);
+ g.drawString(getSteps(), 160, 10);
+
+ g.setFontAlign(-1,-1);
+ drawClock();
+ drawBattery();
+
+ // Hide widgets
+ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+}
+
+Bangle.on("lcdPower", (on) => {
+ if (on) {
+ // Set a random face when waking up
+ setRandomFace();
+ draw();
+ } else {
+ clearIntervals();
+ }
+});
+
+
+Bangle.on("lock", (locked) => {
+ clearIntervals();
+ draw();
+});
+
+Bangle.setUI("clock");
+
+// Touch debouncing to prevent rapid face changes
+var lastTouchTime = 0;
+var touchDebounceMs = 500; // 500ms debounce
+
+// Independent touch listener so it's not overridden by UI helpers
+Bangle.on('touch', function(button, xy) {
+ var now = Date.now();
+
+ // Debounce rapid touches
+ if (now - lastTouchTime < touchDebounceMs) {
+ return;
+ }
+
+ // Get current sprite to check its dimensions
+ var currentSprite = Bangle.isCharging() ? getSprite("charge") : getSprite(currentFace);
+ var centerX = (g.getWidth() - currentSprite.width) / 2;
+ var centerY = (g.getHeight() - currentSprite.height) / 2;
+
+ // Define touch zones - exclude top area for widget bar access
+ var widgetBarHeight = 40; // Reserve top 40px for widget bar gestures
+ var faceTouchMargin = 20; // Reduce touch area to center of face
+
+ // Check if tap is within the center area of Meeseeks face (excluding top for widget bar)
+ if (xy && xy.x >= centerX + faceTouchMargin && xy.x <= centerX + currentSprite.width - faceTouchMargin &&
+ xy.y >= centerY + faceTouchMargin && xy.y <= centerY + currentSprite.height - faceTouchMargin &&
+ xy.y > widgetBarHeight) { // Exclude top area for widget bar access
+ // Only change face if not charging
+ if (!Bangle.isCharging()) {
+ lastTouchTime = now;
+ setNextFace();
+ draw();
+ }
+ }
+});
+
+// Fallbacks for devices/firmware without touch delivery
+if (typeof BTN1 !== 'undefined') {
+ setWatch(function() {
+ if (!Bangle.isCharging()) { setNextFace(); draw(); }
+ }, BTN1, {repeat:true, edge:'falling'});
+}
+
+Bangle.on('swipe', function(LR, UD) {
+ if (!Bangle.isCharging()) { setNextFace(); draw(); }
+});
+
+// Load widgets, but don't show them
+Bangle.loadWidgets();
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
+g.clear(1);
+// Set initial random face
+setRandomFace();
+draw();
diff --git a/apps/meseeks/app.png b/apps/meseeks/app.png
new file mode 100644
index 0000000000..e54ea5cc6c
Binary files /dev/null and b/apps/meseeks/app.png differ
diff --git a/apps/meseeks/data.json b/apps/meseeks/data.json
new file mode 100644
index 0000000000..0fcf7dd7dd
--- /dev/null
+++ b/apps/meseeks/data.json
@@ -0,0 +1 @@
+{"tasks":"", "weather":[]};
diff --git a/apps/meseeks/metadata.json b/apps/meseeks/metadata.json
new file mode 100644
index 0000000000..4f404d0605
--- /dev/null
+++ b/apps/meseeks/metadata.json
@@ -0,0 +1,17 @@
+{ "id": "meseeks",
+ "name": "Mr Meeseeks",
+ "shortName":"MeSeeks",
+ "version":"0.0.2",
+ "description": "Mr Meeseeks clock with random faces and battery-dependent freckles overlay.",
+ "icon": "app.png",
+ "tags": "clock",
+ "type": "clock",
+ "supports" : ["BANGLEJS", "BANGLEJS2"],
+ "screenshots": [{"url":"screenshot01.png"}, {"url":"screenshot02.png"}, {"url":"screenshot03.png"}],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"meseeks.app.js","url":"app.js"},
+ {"name":"meseeks.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/meseeks/screenshot01.png b/apps/meseeks/screenshot01.png
new file mode 100644
index 0000000000..9c23d3e23f
Binary files /dev/null and b/apps/meseeks/screenshot01.png differ
diff --git a/apps/meseeks/screenshot02.png b/apps/meseeks/screenshot02.png
new file mode 100644
index 0000000000..6665f949cd
Binary files /dev/null and b/apps/meseeks/screenshot02.png differ
diff --git a/apps/meseeks/screenshot03.png b/apps/meseeks/screenshot03.png
new file mode 100644
index 0000000000..93c5fd6e9c
Binary files /dev/null and b/apps/meseeks/screenshot03.png differ
diff --git a/apps/stardateclock_wbin/ChangeLog b/apps/stardateclock_wbin/ChangeLog
new file mode 100644
index 0000000000..09d8d2afb9
--- /dev/null
+++ b/apps/stardateclock_wbin/ChangeLog
@@ -0,0 +1,5 @@
+0.01: Initial release on the app repository for Bangle.js 1 and 2
+0.02: Fixed let/const usage while using firmware version >=2v14
+0.03: Tell clock widgets to hide.
+0.04: Minor code improvements
+0.08: Added binary time display (two rows of buttons for hours and minutes in binary). Planned update to stardate format (YYYYMMDD.hhmm.ss).
diff --git a/apps/stardateclock_wbin/README.md b/apps/stardateclock_wbin/README.md
new file mode 100644
index 0000000000..21ffa46e2e
--- /dev/null
+++ b/apps/stardateclock_wbin/README.md
@@ -0,0 +1,13 @@
+# Stardate Clock with Binary Time
+
+A clock face displaying a stardate (in the format YYYYMMDD.hhmm.ss) along with a "standard" digital/analog clock and a binary time display in LCARS design.
+
+This version is a variant of the original Stardate Clock by Robert Kaiser , with binary time features added. The binary time is shown as two rows of buttons at the bottom of the display: the top row represents the hours (in 8, 4, 2, 1 binary), and the bottom row represents the minutes (in 8, 4, 2, 1 binary). Each button is colored to indicate whether its bit is set, and the value is shown on the button.
+
+The LCARS design has been made popular by various Star Trek shows. Credits for the original LCARS designs go to Michael Okuda, copyrights are owned by Paramount Global, usage of that type of design is permitted freely for non-profit use cases.
+
+The stardate concept used leans on the shows released from the late 80s onward by using 1000 units per Earth year, but this version displays the stardate in the format YYYYMMDD.hhmm.ss for clarity and familiarity.
+
+The clock face supports Bangle.js 1 and 2 with some compromises (e.g. the colors will look best on Bangle.js 1, the font sizes will look best on Bangle.js 2).
+
+Any tap on the display while unlocked switches the "standard" Earth-style clock between digital and analog display.
diff --git a/apps/stardateclock_wbin/app-icon.js b/apps/stardateclock_wbin/app-icon.js
new file mode 100644
index 0000000000..d38013a985
--- /dev/null
+++ b/apps/stardateclock_wbin/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgkiAA0gDRwX/C/4X/C5MO9wBDgnkAIMAAQQXKAItehwECAQIXK8gBEIQIeDC5YAF8EAAIIECC48hE4oYCAogXIkQvHDIgCBiQXHiCPFAIaaCgECJBChDAIsOU4RIJbJwwIDIVEABIYBMJAXOAC8SmYAHmHdABJfBCxAXNCpEyRxoWVgETC46+OkYXHRpxGWC5EwBQMBkQDBiK4DKQMBiAXKNQMggQ2CgI7CkcgC5UjicwkYXCgUxmakBC5kCmERC4MiAoMjgMTC50AC4KYCkcAgYXRPgJFBC6YABgYEBC6iQBC6cRgMgL6ikBR4IXOiR3EX4IXPAgTXDBgIXNgUiiClCAAMikIKBC5YAMC64AXogAGoAX/C6w"))
\ No newline at end of file
diff --git a/apps/stardateclock_wbin/app.js b/apps/stardateclock_wbin/app.js
new file mode 100644
index 0000000000..a64ea673b1
--- /dev/null
+++ b/apps/stardateclock_wbin/app.js
@@ -0,0 +1,1363 @@
+// Stardate clock face, by L.Storm, 2025
+
+let redrawClock = true;
+
+// note: Bangle.js 1 has 240x240x16, 2 has 176x176x3 screen
+const bpp = g.getBPP ? g.getBPP() : 16;
+
+// --- Quiet Mode State ---
+let quietModeActive = false;
+let initialSettings = {}; // For LCD brightness, timeout, wake settings from Bangle.getOptions()
+let originalQuietModeValue; // To store the very first quiet mode value from setting.json
+
+// (Phone weather removed; we'll use onboard sensors only)
+
+// --- Power/Charging State ---
+let isCharging = (typeof Bangle !== "undefined" && typeof Bangle.isCharging === "function") ? Bangle.isCharging() : false;
+if (typeof Bangle !== "undefined" && typeof Bangle.on === "function") {
+ Bangle.on('charging', function(on) {
+ isCharging = !!on;
+ // Start/stop flashing and redraw bar immediately on charge state change
+ if (isCharging) startChargingFlash(); else stopChargingFlash();
+ });
+}
+// --- End Power/Charging State ---
+
+// --- Charging Flash Effect ---
+let chargingFlashIntervalId;
+let chargingFlashBright = false;
+const chargingBlueBright = "#66CCFF";
+const chargingBlueDim = "#0044FF";
+
+function startChargingFlash() {
+ if (chargingFlashIntervalId) return;
+ chargingFlashBright = true;
+ chargingFlashIntervalId = setInterval(function() {
+ chargingFlashBright = !chargingFlashBright;
+ drawBatteryBar(E.getBattery());
+ }, 600);
+ drawBatteryBar(E.getBattery());
+}
+
+function stopChargingFlash() {
+ if (chargingFlashIntervalId) {
+ clearInterval(chargingFlashIntervalId);
+ chargingFlashIntervalId = undefined;
+ }
+ chargingFlashBright = false;
+ drawBatteryBar(E.getBattery());
+}
+// --- End Charging Flash Effect ---
+
+// (Phone message listener removed)
+
+function ensureInitialSettingsCaptured() {
+ if (Object.keys(initialSettings).length === 0) {
+ initialSettings = Bangle.getOptions();
+ // console.log("Initial Bangle.getOptions() captured: " + JSON.stringify(initialSettings));
+ }
+}
+
+function toggleQuietMode() {
+ ensureInitialSettingsCaptured(); // For LCD, timeout, wake settings
+
+ let appSettings = require("Storage").readJSON("setting.json", 1) || {};
+
+ // Capture the very original system quiet mode state ONCE
+ if (originalQuietModeValue === undefined) {
+ originalQuietModeValue = appSettings.quiet !== undefined ? appSettings.quiet : 0;
+ // console.log("Original setting.json quiet value captured: " + originalQuietModeValue);
+ }
+
+ quietModeActive = !quietModeActive; // This remains our app-level toggle state for UI
+
+ if (quietModeActive) {
+ appSettings.quiet = 1; // Set system quiet mode to: notifications silent
+ console.log("ACTIVATING quiet mode. setting.json.quiet will be: 1");
+
+ Bangle.setLCDBrightness(0.1);
+ Bangle.setLCDTimeout(10);
+ Bangle.setOptions({ wakeOnTwist: false, wakeOnTouch: false});
+ Bangle.buzz(100, 0.5);
+ } else {
+ appSettings.quiet = originalQuietModeValue; // Restore to the initially captured system value
+ console.log("DEACTIVATING quiet mode. setting.json.quiet will be: " + appSettings.quiet);
+
+ Bangle.setLCDBrightness(initialSettings.lcdBrightness);
+ Bangle.setLCDTimeout(initialSettings.lcdTimeout);
+ Bangle.setOptions({ wakeOnTwist: initialSettings.wakeOnTwist, wakeOnTouch: initialSettings.wakeOnTouch});
+ Bangle.buzz(200, 0.7);
+ }
+
+ require("Storage").writeJSON("setting.json", appSettings);
+ // load(); // Removing load() to prevent emulator freeze. Settings will apply as OS reads setting.json.
+
+ redrawClock = true;
+ // Ensure immediate UI update for the Q button color etc.
+ if (typeof drawClockInterface === "function") drawClockInterface();
+ if (typeof updateStardate === "function") updateStardate(); // Keep other updates if they don't cause issues
+ if (typeof updateConventionalTime === "function") updateConventionalTime();
+ drawStepCounter(); // Draw initial step counter
+ drawWeatherInfo(); // Draw initial weather info
+}
+
+// Load fonts
+Graphics.prototype.setFontAntonio27 = function(scale) {
+ // Actual height 23 (23 - 1)
+ g.setFontCustom(atob("AAAAAAGAAAAwAAAGAAAAwAAAGAAAAwAAAAAAAAAAAAAAAAAADAAAA4AAAHAAAAAAAAAAAAAAAAAAAAAA4AAB/AAD/4AH/4AP/wAf/gAD/AAAeAAAAAAAAAAAAA///AP//+D///4eAAPDgAA4cAAHD///4P//+A///gAAAAAAAAAAAAAAYAAAHAAAA4AAAOAAAD///4f///D///4AAAAAAAAAAAAAAAAAAAAAAA/gD4P8B/D/g/4cAfzDgP4Yf/8DD/+AYP/ADAGAAAAAAAAAAAAHwD8B+AfwfwD/DgMA4cDgHDgeA4f///B/3/wH8P8AAAAAAAAAAAAOAAAPwAAP+AAP/wAf8OAf4BwD///4f///D///4AABwAAAGAAAAAAAAAAAAAAD/4Pwf/h/D/4P4cMAHDjgA4cf//Dh//4cH/8AAAAAAAAAAAAAAH//8B///wf///Dg4A4cHAHDg4A4f3//B+f/wHh/8AAAAAAAAAAAAAAcAAADgAA4cAD/DgH/4cH//Dv/4Af/gAD/gAAfAAADgAAAAAAAAAAAAH4f8B///wf///Dg8A4cDAHDg8A4f///B///wH8/8AAAAAAAAAAAAAAH/h4B/+Pwf/5/DgHA4cA4HDgHA4f///B///wH//8AAAAAAAAAAAAAAAAAAAHgeAA8DwAHgeAAAAAAAAAA"), 45, atob("CQcKDAsMDAwMDAwMDAc="), 27+(scale<<8)+(1<<16));
+};
+Graphics.prototype.setFontAntonio42 = function(scale) {
+ // Actual height 36 (36 - 1)
+ g.setFontCustom(atob("AAAAAAAAAAAAAAAAAcAAAAAAcAAAAAAcAAAAAAcAAAAAAcAAAAAAcAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAHgAAAAAHgAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAfgAAAAH/gAAAB//gAAAf//gAAH//4AAB//+AAAf//gAAH//4AAAf/+AAAAf/gAAAAf4AAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAA////gAH////+AP/////Af/////gf/////gfAAAAPgeAAAAHgeAAAAHgfAAAAPgf/////gf/////gP/////AH////+AB////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAB4AAAAAB4AAAAADwAAAAAHwAAAAAP/////gf/////gf/////gf/////gf/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAPgH/8AD/gP/8AP/gP/8A//gf/8B//gfAAH/ngeAAf+HgeAB/4HgfAH/gHgf//+AHgP//4AHgH//wAHgD/+AAHgAPgAAAAAAAAAAAAAAAAAAAAAAAAAA+AAfwAH+AAf+AP+AAf/AP+AAf/Af+AAf/gfADwAPgeADwAHgeADwAHgfAH4APgf///h/gf/////AP/+///AH/+f/+AB/4H/4AAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAA/gAAAAH/gAAAB//gAAAP//gAAB//HgAAf/wHgAD/8AHgAf/AAHgAf/////gf/////gf/////gf/////gf/////gAAAAHgAAAAAHgAAAAAHAAAAAAAAAAAAAAAAAAAAAAAf//gP8Af//gP+Af//gP/Af//gP/gf/+AAfgeB8AAHgeB4AAHgeB8AAHgeB////geB////geA////AeAf//+AAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///gAD////8AH/////AP/////Af/////gfAPgAfgeAPAAHgeAPAAHgeAPAAHgf+PgAPgf+P///gP+H///AH+H//+AB+B//8AAAAD8AAAAAAAAAAAAAAAAAAAAAAAeAAAAAAeAAAAAAeAAAAPgeAAAP/geAAD//geAA///geAH///geB///+AeP//4AAe//8AAAf//AAAAf/wAAAAf+AAAAAfwAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAB/wH/4AH/8f/+AP/////Af/////gf/////geAH4APgeADgAHgeADgAHgeAHwAHgf/////gf/////gP/////AH/8//+AB/wH/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//gPgAH//4P+AP//8P/Af//+P/AfwB+P/geAAeAPgeAAeAHgeAAeAHgfAAeAPgf/////gP/////AP/////AH////8AA////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4APgAAH4AfgAAH4AfgAAH4AfgAAH4AfgAAD4APgAAAAAAAAAAAAAAA="), 45, atob("DgsPEhESEhISEhISEgo="), 42+(scale<<8)+(1<<16));
+};
+const fontName = "Antonio27";
+const fontNameLarge = "Antonio42";
+const fontSize = 1;
+const fontSizeLarge = 1;
+const fontHeightLarge = 42 * fontSizeLarge;
+const vectorFontStardateSizeB2 = 16; // For Bangle.js 2 stardate display
+
+// LCARS dimensions
+let baseUnit1 = 5;
+let baseUnit2 = 3;
+let baseUnit3 = 10;
+if (g.getWidth() < 200) { // Bangle.js 2
+ baseUnit1 = 3;
+ baseUnit2 = 2;
+ baseUnit3 = 7;
+}
+
+//const widgetsHeight = 24;
+const sbarWid = baseUnit3 * 5;
+const hbarHt = baseUnit1;
+const outRad = baseUnit1 * 5;
+const inRad = outRad - hbarHt;
+const gap = baseUnit2;
+const divisionPos = baseUnit3 * 8;
+const sbarGapPos = baseUnit3 * 15;
+const lowerTop = divisionPos+gap+1;
+
+// Star Trek famously premiered on Thursday, September 8, 1966, at 8:30 p.m.
+// See http://www.startrek.com/article/what-if-the-original-star-trek-had-debuted-on-friday-nights
+const gSDBase = new Date("September 8, 1966 20:30:00 EST");
+const sdatePosBottom = divisionPos - hbarHt - 1;
+let sdatePosRight = g.getWidth() - baseUnit2 - 10; // Default, will be adjusted
+const sdateDecimals = 1;
+const secondsPerYear = 86400 * 365.2425;
+const sdateDecFactor = Math.pow(10, sdateDecimals);
+
+const clockAreaLeft = sbarWid + inRad / 2;
+const clockAreaTop = lowerTop + hbarHt + inRad / 2;
+const clockWid = g.getWidth() - clockAreaLeft;
+const clockHt = g.getHeight() - clockAreaTop;
+
+const ctimePosTop = clockAreaTop + baseUnit1 * 5;
+const ctimePosCenter = clockAreaLeft + clockWid / 2;
+const cdatePosTop = ctimePosTop + fontHeightLarge;
+const cdatePosCenter = clockAreaLeft + clockWid / 2;
+
+const clockCtrX = Math.floor(clockAreaLeft + clockWid / 2);
+const clockCtrY = Math.floor(clockAreaTop + clockHt / 2);
+const analogRad = Math.floor(Math.min(clockWid, clockHt) / 2);
+
+const analogMainLineLength = baseUnit1 * 2;
+const analogSubLineLength = baseUnit1;
+
+const analogHourHandLength = analogRad / 2;
+const analogMinuteHandLength = analogRad - analogMainLineLength / 2;
+
+const colorBg = "#000000";
+const colorTime = "#9C9CFF";
+const colorDate = "#A09090";
+const colorStardate = "#FFCF00";
+// On low-bpp devices (Bangle.js 2), use basic colors for analog clock.
+const colorHours = bpp > 3 ? "#9C9CFF" : "#00FF00";
+const colorSeconds = bpp > 3 ? "#E7ADE7" : "#FFFF00";
+const colorHands = bpp > 3 ? "#A09090" : "#00FFFF";
+const colorLCARSGray = "#A09090";
+const colorLCARSOrange = "#FF9F00";
+const colorLCARSPink = "#E7ADE7";
+const colorLCARSPurple = "#A06060";
+const colorLCARSBrown = "#C09070";
+// More colors: teal #008484, yellow FFCF00, purple #6050B0
+const colorLCARSDaySegment = "#00A0FF"; // Blue for day segments
+const colorLCARSDayHighlight = "#FFFF00"; // Yellow for day segment highlight
+
+// --- Separator Bar Config (between sensor panel and binary buttons) ---
+// Minimal knobs: set height and move it up/down. That's it.
+const separatorBarConfig = {
+ color: colorLCARSOrange, // Fill color (same LCARS orange)
+ cornerRadius: 4, // Rounded corner radius
+ height: (g.getWidth() < 200) ? 16 : 8, // Bar thickness
+ gapToButtons: 2, // Gap between bar and TOP of the hour buttons
+ verticalOffset: -5, // Additional Y shift: negative = move up, positive = move down
+ leftInsetFromSensorLeft: 55, // Left inset from content-left edge
+ rightInsetFromRightEdge: 2 // Right inset from content-right edge
+};
+
+// Global variables for storing dimensions of the last drawn stardate for accurate clearing
+let lastSDateStringWidth = 0;
+let lastSDateStringHeight = 0;
+let lastSDateActualPosX = 0;
+let lastSDateFinalPosY = 0;
+
+// Global for day of week display
+let highlightedDayIndex = -1; // 0=Sun, 6=Sat
+// Bounds for the quiet-mode grid area (2x2 buttons) to detect touch
+let quietToggleAreaBounds = null; // {x1,y1,x2,y2}
+let timerToggleAreaBounds = null; // bounds for timer button
+let backButtonBounds = null; // bounds for timer back button
+let showTimer = false;
+let timerStartMs = 0;
+let timerIntervalId;
+let timerMode = "setup"; // 'setup' | 'running' | 'paused' | 'finished'
+let timerSetMs = 5 * 60 * 1000; // default 5 minutes
+let timerRemainingMs = timerSetMs;
+let timerEndTimeMs = 0;
+let startPauseButtonBounds = null;
+let resetButtonBounds = null;
+let zeroButtonBounds = null;
+let minPlusBounds = null, minMinusBounds = null, secPlusBounds = null, secMinusBounds = null;
+
+// --- Barometer (Internal Pressure) - Periodic Sampling ---
+let internalPressureHpa = null;
+let barometerSampleInterval = null;
+let lastBarometerUpdate = 0;
+
+// --- Temperature Sensor - Periodic Sampling ---
+let internalTemperatureC = null;
+let temperatureSampleInterval = null;
+let lastTemperatureUpdate = 0;
+
+function enableBarometerPower(on) {
+ if (typeof Bangle !== "undefined" && typeof Bangle.setBarometerPower === "function") {
+ Bangle.setBarometerPower(on ? 1 : 0, "stardateclock");
+ }
+}
+
+function sampleBarometer() {
+ if (typeof Bangle !== "undefined" && typeof Bangle.setBarometerPower === "function") {
+ enableBarometerPower(true);
+ // Sample for 2 seconds then turn off
+ setTimeout(function() {
+ enableBarometerPower(false);
+ }, 2000);
+ }
+}
+
+// Sample barometer every 5 minutes instead of continuous
+function startBarometerSampling() {
+ if (barometerSampleInterval) return;
+ sampleBarometer(); // Initial sample
+ barometerSampleInterval = setInterval(sampleBarometer, 300000); // 5 minutes
+}
+
+function stopBarometerSampling() {
+ if (barometerSampleInterval) {
+ clearInterval(barometerSampleInterval);
+ barometerSampleInterval = null;
+ }
+ enableBarometerPower(false);
+}
+
+function enableTemperaturePower(on) {
+ if (typeof Bangle !== "undefined" && typeof Bangle.setBarometerPower === "function") {
+ Bangle.setBarometerPower(on ? 1 : 0, "stardateclock");
+ }
+}
+
+function sampleTemperature() {
+ if (typeof Bangle !== "undefined" && typeof Bangle.setBarometerPower === "function") {
+ enableTemperaturePower(true);
+ // Sample for 2 seconds then turn off
+ setTimeout(function() {
+ enableTemperaturePower(false);
+ }, 2000);
+ }
+}
+
+// Sample temperature every 5 minutes instead of continuous
+function startTemperatureSampling() {
+ if (temperatureSampleInterval) return;
+ sampleTemperature(); // Initial sample
+ temperatureSampleInterval = setInterval(sampleTemperature, 300000); // 5 minutes
+}
+
+function stopTemperatureSampling() {
+ if (temperatureSampleInterval) {
+ clearInterval(temperatureSampleInterval);
+ temperatureSampleInterval = null;
+ }
+ enableTemperaturePower(false);
+}
+
+if (typeof Bangle !== "undefined" && typeof Bangle.on === "function") {
+ if (typeof Bangle.setBarometerPower === "function") {
+ Bangle.on('pressure', function(e) {
+ if (!e) return;
+ let p = e.pressure;
+ // If units are Pa (e.g. ~101325), convert to hPa; if already hPa (~1013), keep
+ if (p > 2000) p = p / 100;
+ internalPressureHpa = p;
+ lastBarometerUpdate = Date.now();
+ drawWeatherInfo();
+ });
+ }
+
+ // Temperature sensor listener
+ Bangle.on('temperature', function(e) {
+ if (!e) return;
+ internalTemperatureC = e.temperature;
+ lastTemperatureUpdate = Date.now();
+ drawWeatherInfo();
+ });
+}
+// --- End Barometer ---
+
+// --- GPS Time Sync ---
+let gpsTimeSyncInProgress = false;
+let gpsTimeSyncTimeoutId;
+function startGPSTimeSync() {
+ if (gpsTimeSyncInProgress) return;
+ if (!(typeof Bangle !== "undefined" && typeof Bangle.setGPSPower === "function")) return;
+ gpsTimeSyncInProgress = true;
+ function onGPSFix(fix) {
+ if (fix && fix.time !== undefined) {
+ setTime(fix.time.getTime()/1000);
+ // Stop listening and power down GPS after sync
+ Bangle.removeListener('GPS', onGPSFix);
+ Bangle.setGPSPower(0, "stardateclock");
+ gpsTimeSyncInProgress = false;
+ if (gpsTimeSyncTimeoutId) { clearTimeout(gpsTimeSyncTimeoutId); gpsTimeSyncTimeoutId = undefined; }
+ }
+ }
+ Bangle.on('GPS', onGPSFix);
+ Bangle.setGPSPower(1, "stardateclock");
+ // Fallback timeout to turn GPS off if no fix/time comes through
+ gpsTimeSyncTimeoutId = setTimeout(function() {
+ if (gpsTimeSyncInProgress) {
+ Bangle.removeListener('GPS', onGPSFix);
+ Bangle.setGPSPower(0, "stardateclock");
+ gpsTimeSyncInProgress = false;
+ }
+ }, 120000); // 2 minutes
+}
+// --- End GPS Time Sync ---
+
+// Global variables to track what needs updating
+let lastStardateString = "";
+let lastBinaryHours = -1;
+let lastBinaryMinutes = -1;
+let lastDay = -1;
+let lastBatteryLevel = -1;
+let lastSteps = -1;
+
+function updateStardate() {
+ const curDate = new Date();
+ const year = curDate.getFullYear();
+ const month = (curDate.getMonth() + 1).toString().padStart(2, '0');
+ const dayOfMonth = curDate.getDate().toString().padStart(2, '0');
+ const hours = curDate.getHours().toString().padStart(2, '0');
+ const minutes = curDate.getMinutes().toString().padStart(2, '0');
+ const sdatestring = `${year}${month}${dayOfMonth}.${hours}${minutes}`;
+
+ // Only redraw stardate if it actually changed
+ if (sdatestring !== lastStardateString || redrawClock) {
+ let currentFontIsVector = false;
+ let currentSDatePosRight = sdatePosRight;
+ let currentSDatePosBottomAdjust;
+
+ // Clear previous stardate area using stored dimensions
+ if (lastSDateStringWidth > 0) {
+ g.setColor(colorBg);
+ g.fillRect(
+ lastSDateActualPosX - 1,
+ lastSDateFinalPosY - lastSDateStringHeight - 1,
+ lastSDateActualPosX + lastSDateStringWidth + 1,
+ lastSDateFinalPosY + 1
+ );
+ }
+
+ // Set font and determine positioning for the current draw
+ if (g.getWidth() < 200) {
+ g.setFont("Vector", vectorFontStardateSizeB2);
+ currentFontIsVector = true;
+ currentSDatePosRight = g.getWidth() - 3;
+ currentSDatePosBottomAdjust = Math.round(vectorFontStardateSizeB2 / 4);
+ } else {
+ g.setFont(fontName, fontSize);
+ currentSDatePosBottomAdjust = 0;
+ }
+
+ // Calculate and store dimensions for the *current* stardate
+ lastSDateStringWidth = g.stringWidth(sdatestring);
+ lastSDateStringHeight = g.getFontHeight();
+ lastSDateActualPosX = currentSDatePosRight - lastSDateStringWidth;
+ lastSDateFinalPosY = sdatePosBottom - currentSDatePosBottomAdjust;
+
+ // Draw the current stardate
+ g.setColor(colorStardate);
+ if (currentFontIsVector) {
+ g.setFontAlign(-1, -1, 0);
+ g.drawString(sdatestring, lastSDateActualPosX, lastSDateFinalPosY - lastSDateStringHeight);
+ } else {
+ g.setFontAlign(-1, 1, 0);
+ g.drawString(sdatestring, lastSDateActualPosX, lastSDateFinalPosY);
+ }
+
+ lastStardateString = sdatestring;
+ }
+}
+
+function updateConventionalTime() {
+ const curDate = new Date();
+ const day = curDate.getDay();
+ const currentBattery = E.getBattery();
+ const steps = Bangle.getHealthStatus("day").steps;
+
+ // Only update day segments if day changed
+ if (redrawClock || day !== lastDay) {
+ drawDaySegmentsBar(day);
+ lastDay = day;
+ }
+
+ // Only update battery bar if battery level changed
+ if (redrawClock || currentBattery !== lastBatteryLevel) {
+ drawBatteryBar(currentBattery);
+ lastBatteryLevel = currentBattery;
+ }
+
+ // Only update binary time if hours/minutes changed
+ let hoursForBinary = curDate.getHours();
+ if (hoursForBinary === 0) {
+ hoursForBinary = 12;
+ } else if (hoursForBinary > 12) {
+ hoursForBinary -= 12;
+ }
+ const minutes = curDate.getMinutes();
+
+ if (redrawClock || hoursForBinary !== lastBinaryHours || minutes !== lastBinaryMinutes) {
+ drawBinaryTimeButtons(hoursForBinary, minutes);
+ lastBinaryHours = hoursForBinary;
+ lastBinaryMinutes = minutes;
+ }
+
+ // Steps now drawn inside drawWeatherInfo panel
+ if (redrawClock || steps !== lastSteps) {
+ lastSteps = steps;
+ }
+
+ // Always draw sensor panel last so it stays on top
+ drawWeatherInfo();
+}
+
+// Single consolidated update function - replaces dual timers
+function updateClock() {
+ updateStardate();
+ updateConventionalTime();
+
+ // Schedule next update
+ if (redrawClock) {
+ const curDate = new Date();
+ const msToNextSecond = 1000 - curDate.getMilliseconds();
+ setTimeout(updateClock, msToNextSecond);
+ }
+}
+
+function drawBinaryTimeButtons(hours, minutes) {
+ const r = Bangle.appRect;
+ if (!r) {
+ console.log("drawBinaryTimeButtons: Bangle.appRect not ready yet.");
+ return;
+ }
+
+ const buttonCount = 4;
+ const buttonLabels = [8, 4, 2, 1];
+ const buttonLabelsMinutes = [32, 16, 8, 4, 2, 1];
+ const buttonCountMinutes = buttonLabelsMinutes.length;
+
+ const buttonGap = 4; // Default gap, might be overridden by dynamic calculation
+ const buttonHeight = 28;
+ const buttonHeightSmall = 22;
+ const buttonWidth = 33;
+ const buttonWidthSmall = 18; // B2 minute button width (was 18)
+ const marginBottom = 4;
+
+ let actualHourButtonWidth = buttonWidth;
+ let actualHourButtonHeight = buttonHeight;
+ let actualMinuteButtonWidth = buttonWidthSmall;
+ let actualMinuteButtonHeight = buttonHeightSmall;
+ let actualButtonGap = buttonGap;
+ const cornerRadius = 8;
+ let contentAreaX_offset_b2 = 0; // Original B2 offset for LCARS alignment
+ let buttonAreaStartShift = 0; // New: How much to shift the button area left
+
+ if (g.getWidth() < 200) { // Bangle.js 2 specific dimensions
+ actualHourButtonWidth = 28;
+ actualHourButtonHeight = 18;
+ actualMinuteButtonWidth = 21; // Keep at 18px for B2 minutes for now
+ actualMinuteButtonHeight = 18;
+ actualButtonGap = 2; // Default gap for B2 if not enough space for dynamic
+ contentAreaX_offset_b2 = 4; // Original shift for LCARS alignment
+ buttonAreaStartShift = -10; // <<<< REDUCED SHIFT to -15px for B2
+ }
+
+ const minuteButtonsTopY = r.y2 - marginBottom - actualMinuteButtonHeight + 1;
+ const hourButtonsTopY = minuteButtonsTopY - actualButtonGap - actualHourButtonHeight;
+
+ // Original contentAreaX based on LCARS geometry
+ let baseContentAreaX = sbarWid + outRad + gap + 1;
+ if (g.getWidth() < 200) {
+ baseContentAreaX -= contentAreaX_offset_b2;
+ }
+
+ // New effective start for buttons, incorporating the shift
+ const effectiveButtonAreaStartX = baseContentAreaX + buttonAreaStartShift;
+
+ // Right edge for button area
+ const buttonAreaEndX = r.x2; // Use the full app rect right edge
+
+ // Effective width for buttons to span and for gap calculation
+ const effectiveButtonAreaWidth = buttonAreaEndX - effectiveButtonAreaStartX;
+
+ // --- Hour Buttons ---
+ const totalFixedHourButtonWidth = buttonCount * actualHourButtonWidth;
+ let hourGap = actualButtonGap;
+ if (buttonCount > 1) {
+ const totalHourGapSpace = effectiveButtonAreaWidth - totalFixedHourButtonWidth;
+ hourGap = Math.max(1, Math.floor(totalHourGapSpace / (buttonCount - 1)));
+ }
+ const currentEffectiveHourRowWidth = totalFixedHourButtonWidth + (hourGap * (buttonCount - 1));
+ const xStartHours = effectiveButtonAreaStartX + Math.max(0, Math.floor((effectiveButtonAreaWidth - currentEffectiveHourRowWidth) / 2));
+
+ // --- Minute Buttons ---
+ const totalFixedMinuteButtonWidth = buttonCountMinutes * actualMinuteButtonWidth;
+ let minuteGap = actualButtonGap;
+ if (buttonCountMinutes > 1) {
+ const totalMinuteGapSpace = effectiveButtonAreaWidth - totalFixedMinuteButtonWidth;
+ minuteGap = Math.max(1, Math.floor(totalMinuteGapSpace / (buttonCountMinutes - 1)));
+ }
+ const currentEffectiveMinuteRowWidth = totalFixedMinuteButtonWidth + (minuteGap * (buttonCountMinutes - 1));
+ const xStartMinutes = effectiveButtonAreaStartX + Math.max(0, Math.floor((effectiveButtonAreaWidth - currentEffectiveMinuteRowWidth) / 2));
+
+ // Draw the separator bar between sensor panel and the binary buttons
+ drawSeparatorBar(hourButtonsTopY, buttonAreaEndX);
+
+ // Clear the specific button area (now potentially wider and further left)
+ g.setColor(colorBg);
+ g.fillRect(effectiveButtonAreaStartX, hourButtonsTopY, buttonAreaEndX, r.y2); // Clear from new start to app edge right, full button height area
+
+ const colorText = "#000000";
+ const TEMP_DRAW_ALL_BUTTONS = false; // <<<< REVERTED: Was true for layout, now false for actual time
+
+ // Draw hour buttons
+ g.setFont("6x8", 2);
+ for (let i = 0; i < buttonCount; i++) {
+ const bit = buttonLabels[i];
+ const isSet = (hours & bit) !== 0;
+ if (TEMP_DRAW_ALL_BUTTONS || isSet) {
+ const x = xStartHours + i * (actualHourButtonWidth + hourGap); // Use dynamic hourGap
+ drawRoundedButton(x, hourButtonsTopY, actualHourButtonWidth, actualHourButtonHeight, cornerRadius, "#FFCF00");
+ g.setColor(colorText);
+ g.setFontAlign(0,0);
+ g.drawString(bit.toString(), x + actualHourButtonWidth / 2, hourButtonsTopY + actualHourButtonHeight / 2);
+ }
+ }
+
+ // Draw minute buttons
+ g.setFont("6x8", 2);
+ for (let i = 0; i < buttonCountMinutes; i++) {
+ const bit = buttonLabelsMinutes[i];
+ const isSet = (minutes & bit) !== 0;
+ if (TEMP_DRAW_ALL_BUTTONS || isSet) {
+ const x = xStartMinutes + i * (actualMinuteButtonWidth + minuteGap); // Use dynamic minuteGap
+ drawRoundedButton(x, minuteButtonsTopY, actualMinuteButtonWidth, actualMinuteButtonHeight, cornerRadius, "#2A4FFF");
+ g.setColor(colorText);
+ g.setFontAlign(0,0);
+ g.drawString(bit.toString(), x + actualMinuteButtonWidth / 2, minuteButtonsTopY + actualMinuteButtonHeight / 2);
+ }
+ }
+}
+
+// Draws the orange rounded rectangle separator between the sensor panel and binary buttons.
+// The bar spans from the content's left (just right of LCARS border) to the app area right,
+// with configurable insets and height controlled by separatorBarConfig above.
+function drawSeparatorBar(hourButtonsTopY, areaRightX) {
+ const appRect = Bangle.appRect;
+ if (!appRect) return;
+ const sensorLeftX = sbarWid + outRad + gap + 1;
+ // Position the bar directly relative to the button row for clarity:
+ // y = top of hour buttons - configurable gap - bar height + optional verticalOffset
+ const x = sensorLeftX + separatorBarConfig.leftInsetFromSensorLeft;
+ const y = (hourButtonsTopY - separatorBarConfig.gapToButtons - separatorBarConfig.height) + separatorBarConfig.verticalOffset;
+ const rightInset = separatorBarConfig.rightInsetFromRightEdge;
+ const w = Math.max(0, (areaRightX - rightInset) - x);
+ const h = Math.max(1, separatorBarConfig.height);
+
+ if (w <= 0 || h <= 0) return;
+ drawRoundedButton(x, y, w, h, separatorBarConfig.cornerRadius, separatorBarConfig.color);
+}
+
+function drawDigitalClock(curDate) {
+ // This function is now called from updateConventionalTime() with proper change detection
+ // Just ensure background is set
+ g.setBgColor(colorBg);
+}
+
+function drawLine(x1, y1, x2, y2, color) {
+ g.setColor(color);
+ // On high-bpp devices, use anti-aliasing. Low-bpp (Bangle.js 2) doesn't clear nicely with AA.
+ if (bpp > 3 && g.drawLineAA) {
+ g.drawLineAA(x1, y1, x2, y2);
+ } else {
+ g.drawLine(x1, y1, x2, y2);
+ }
+}
+
+function clearLine(x1, y1, x2, y2) {
+ drawLine(x1, y1, x2, y2, colorBg);
+}
+
+function drawDaySegmentsBar(currentDay) {
+ const barX1 = sbarWid + outRad + gap + 1;
+ const barY1 = divisionPos - hbarHt;
+ // Respect the right-hand margin defined by baseUnit2 for consistency
+ const barX2 = g.getWidth() - (g.getWidth() < 200 ? baseUnit2 + 1 : 0); // B2 has baseUnit2 margin, B1 might go to edge.
+ const barY2 = divisionPos;
+ const barHeight = hbarHt; //barY2 - barY1;
+
+ // Clear the area for the day segments
+ g.setColor(colorBg);
+ g.fillRect(barX1, barY1, barX2, barY2);
+
+ const numSegments = 7;
+ const segmentGap = 1;
+ const totalBarWidth = barX2 - barX1;
+ const totalGapWidth = (numSegments - 1) * segmentGap;
+ const segmentWidth = Math.floor((totalBarWidth - totalGapWidth) / numSegments);
+
+ if (segmentWidth <=0) return; // Not enough space to draw
+
+ for (let i = 0; i < numSegments; i++) {
+ const segX = barX1 + i * (segmentWidth + segmentGap);
+ const color = (i === currentDay) ? colorLCARSDayHighlight : colorLCARSDaySegment;
+ g.setColor(color);
+ g.fillRect(segX, barY1, segX + segmentWidth - 1, barY2); // Corrected for inclusive x2
+ }
+ highlightedDayIndex = currentDay;
+}
+
+// Helper function to draw the battery bar
+function drawBatteryBar(batteryPercent) {
+ const barX1 = sbarWid + outRad + gap + 1;
+ const barY1 = lowerTop;
+ const barX2 = g.getWidth() - (g.getWidth() < 200 ? baseUnit2 + 1 : 0); // Respect B2 right margin
+ const barHeight = hbarHt;
+ const totalBarWidth = barX2 - barX1;
+
+ if (totalBarWidth <=0) return; // Not enough space
+
+ // Draw red background bar (full width)
+ g.setColor("#FF0000");
+ g.fillRect(barX1, barY1, barX2, barY1 + barHeight - 1);
+
+ // Calculate width of the yellow foreground bar
+ const yellowWidth = Math.floor((batteryPercent / 100) * totalBarWidth);
+
+ // Draw yellow foreground bar
+ if (yellowWidth > 0) {
+ // When charging, draw the foreground bar in blue instead of yellow/orange
+ const charging = (typeof Bangle !== "undefined" && typeof Bangle.isCharging === "function") ? Bangle.isCharging() : isCharging;
+ if (charging) {
+ const useColor = chargingFlashBright ? chargingBlueBright : chargingBlueDim;
+ g.setColor(useColor);
+ } else {
+ g.setColor(colorLCARSOrange);
+ }
+ g.fillRect(barX1, barY1, barX1 + yellowWidth - 1, barY1 + barHeight - 1);
+ }
+ // Draw a thin white vertical marker crossing the day-segment bar and battery bar.
+ // Always render: if no pressure reading yet, pin at the leftmost edge.
+ let arrowX;
+ if (internalPressureHpa !== null && isFinite(internalPressureHpa)) {
+ const p = Math.max(940, Math.min(1060, internalPressureHpa));
+ const pMin = 980, pMax = 1040; // typical sea-level range
+ let t = (p - pMin) / (pMax - pMin);
+ if (!isFinite(t)) t = 0.5;
+ t = Math.max(0, Math.min(1, t));
+ arrowX = Math.round(barX1 + t * totalBarWidth);
+ } else {
+ arrowX = barX1; // fallback: keep arrow visible at the leftmost position
+ }
+ const lineYTop = divisionPos - hbarHt - 2; // extend 2px above day segments
+ const lineYBottom = barY1 + barHeight - 1 + 2; // extend 2px below battery bar
+ g.setColor("#FFFFFF");
+ // Draw as 1px or 2px wide line for visibility on both devices
+ g.drawLine(arrowX, lineYTop, arrowX, lineYBottom);
+ if (bpp > 3) g.drawLine(arrowX+1, lineYTop, arrowX+1, lineYBottom);
+ lastBatteryLevel = batteryPercent;
+}
+
+// Helper function to draw a filled rounded rectangle
+function drawRoundedButton(x, y, w, h, r, color) {
+ g.setColor(color);
+ // Ensure radius is not too large for the dimensions
+ r = Math.min(r, w/2, h/2);
+ if (r < 0) r = 0;
+
+ // Central cross shape (two overlapping rectangles)
+ g.fillRect(x + r, y, x + w - 1 - r, y + h - 1); // Horizontal part
+ g.fillRect(x, y + r, x + w - 1, y + h - 1 - r); // Vertical part
+
+ // Corner circles (centers are r pixels from the true corners)
+ g.fillCircle(x + r, y + r, r); // Top-left
+ g.fillCircle(x + w - 1 - r, y + r, r); // Top-right
+ g.fillCircle(x + r, y + h - 1 - r, r); // Bottom-left
+ g.fillCircle(x + w - 1 - r, y + h - 1 - r, r); // Bottom-right
+}
+
+// Function to draw the step counter
+function drawStepCounter() {
+ const r = Bangle.appRect;
+ if (!r) {
+ console.log("drawStepCounter: Bangle.appRect not ready yet.");
+ return;
+ }
+
+ const steps = Bangle.getHealthStatus("day").steps;
+
+ // Position above the binary buttons, below the quiet mode button
+ let buttonWidth = (g.getWidth() < 200) ? 28 : 34;
+ let buttonHeight = (g.getWidth() < 200) ? 22 : 28;
+ let buttonGap = (g.getWidth() < 200) ? 4 : 5;
+ let weatherLineHeightEstimate = (g.getWidth() < 200) ? 15 : 17;
+ const areaLeft = sbarWid + 5;
+ const areaTop = (lowerTop + hbarHt + gap + 5) + weatherLineHeightEstimate + buttonGap;
+ const quietModeHeight = 2*buttonHeight + buttonGap;
+
+ // Calculate position above binary buttons
+ let estTimeBtnAreaHeight = (g.getWidth() < 200) ? 45 : 55;
+ const binaryButtonsTopY = r.y2 - estTimeBtnAreaHeight;
+ const stepCounterY = binaryButtonsTopY - 15; // 15px above binary buttons
+
+ const x = areaLeft; // Align with left edge
+ const labelText = "Step";
+ const valueText = steps + "" ;
+
+ g.setFontAlign(-1, -1, 0); // Align top-left
+ g.setColor("#FFFFFF"); // White text to match the color scheme
+
+ let valueFontSize = (g.getWidth() < 200) ? 14 : 16; // Appropriate size for horizontal display
+ let stepsLabelFontSize = (g.getWidth() < 200) ? 12 : 14; // Base size for "Steps" label
+
+ g.setFont("4x6", 2); // Use 4x6 font with 2x scaling for compact display
+ g.setColor("#000000"); // Black color for "Steps" label
+ // Draw "Steps" label on the left border (positioned on the LCARS border)
+ g.drawString(labelText, 3, stepCounterY + 2); // Move to left border (x=3)
+
+ g.setFont("Vector", valueFontSize);
+ g.setColor("#FFFFFF"); // White color for step numbers
+ // Draw step count (value) to the right of "Steps", left-aligned
+ g.drawString(valueText, x + g.stringWidth(labelText) + 5, stepCounterY);
+}
+
+// Function to draw a Do Not Disturb icon (circle with line through it)
+function drawDoNotDisturbIcon(x, y, size) {
+ g.setColor("#000000"); // Black color for the icon
+
+ const radius = Math.floor(size / 2);
+
+ // Draw circle outline
+ g.drawCircle(x, y, radius);
+
+ // Draw diagonal line through the circle
+ const lineLength = Math.floor(radius * 1.2);
+ const lineOffset = Math.floor(lineLength / 2);
+
+ // Draw line from top-left to bottom-right
+ g.drawLine(x - lineOffset, y - lineOffset, x + lineOffset, y + lineOffset);
+}
+
+// Function to draw a speaker icon
+function drawSpeakerIcon(x, y, size, isMuted, iconColor) {
+ g.setColor(iconColor);
+
+ // Speaker base (rectangle)
+ // Making base slightly indented and not full height of icon box
+ const baseWidth = Math.max(2, Math.floor(size / 2.5));
+ const baseHeight = Math.max(3, Math.floor(size / 1.8));
+ const baseX = x + Math.floor(size * 0.1); // Indent base slightly
+ const baseY = y + Math.floor((size - baseHeight) / 2);
+ g.fillRect(baseX, baseY, baseX + baseWidth - 1, baseY + baseHeight - 1);
+
+ // Speaker horn (triangle pointing right)
+ const hornTipX = baseX + baseWidth; // Where base ends
+ const hornRightExtent = x + size - 1 - Math.floor(size * 0.1); // Indent right tip
+ const hornMidY = y + Math.floor(size / 2);
+ g.fillPoly([
+ hornTipX, baseY + Math.floor(baseHeight * 0.2), // Top-left of horn (connected to base)
+ hornTipX, baseY + baseHeight - 1 - Math.floor(baseHeight * 0.2), // Bottom-left of horn (connected to base)
+ hornRightExtent, hornMidY // Rightmost point of horn
+ ]);
+
+ if (isMuted) {
+ // Draw a diagonal slash using a filled polygon for thickness
+ const slashThicknessRatio = 0.18; // Relative thickness of the slash
+ const slashIndentRatio = 0.05; // How much the slash is inset from edges
+
+ // Points for the slash polygon (top-left to bottom-right orientation)
+ const p1x = x + Math.floor(size * slashIndentRatio);
+ const p1y = y + Math.floor(size * (slashIndentRatio + slashThicknessRatio));
+
+ const p2x = x + Math.floor(size * (slashIndentRatio + slashThicknessRatio));
+ const p2y = y + Math.floor(size * slashIndentRatio);
+
+ const p3x = x + size - 1 - Math.floor(size * slashIndentRatio);
+ const p3y = y + size - 1 - Math.floor(size * (slashIndentRatio + slashThicknessRatio));
+
+ const p4x = x + size - 1 - Math.floor(size * (slashIndentRatio + slashThicknessRatio));
+ const p4y = y + size - 1 - Math.floor(size * slashIndentRatio);
+
+ g.fillPoly([p1x,p1y, p2x,p2y, p4x,p4y, p3x,p3y]); // Order for fillPoly needs to be sequential vertices
+ // Corrected order for a diagonal band from top-left towards bottom-right:
+ // Top-left inner, top-left outer, bottom-right outer, bottom-right inner
+ g.fillPoly([
+ x + Math.floor(size*0.15), y + Math.floor(size*0.05), // Top-left of band's "upper edge"
+ x + Math.floor(size*0.05), y + Math.floor(size*0.15), // Top-left of band's "lower edge"
+ x + size -1 - Math.floor(size*0.05), y + size -1 - Math.floor(size*0.15), // Bottom-right of band's "lower edge"
+ x + size -1 - Math.floor(size*0.15), y + size -1 - Math.floor(size*0.05) // Bottom-right of band's "upper edge"
+ ]);
+ }
+}
+
+// --- Function to draw Weather Information ---
+function drawWeatherInfo() {
+ const r = Bangle.appRect;
+ if (!r) {
+ // console.log("drawWeatherInfo: Bangle.appRect not ready yet.");
+ return;
+ }
+
+ // If timer overlay is shown, draw it here and return
+ if (showTimer) {
+ drawTimerOverlay();
+ return;
+ }
+
+ // Define the drawing area for sensor info (rendered after other elements)
+ const weatherAreaX1 = sbarWid + outRad + gap + 10;
+ const weatherAreaY1 = lowerTop + hbarHt + gap + 2; // start a bit higher to fit more lines
+ const weatherAreaX2 = g.getWidth() - 3;
+ // reserve safe space for binary buttons to avoid overlap
+ let estimatedTimeButtonAreaHeight = (g.getWidth() < 200) ? 45 : 55;
+ const weatherAreaY2 = r.y2 - estimatedTimeButtonAreaHeight - 10;
+
+ // Font settings
+ let weatherFontSize = (g.getWidth() < 200) ? 12 : 16; // smaller on B2 to fit 5 lines
+ let weatherLineHeight = (g.getWidth() < 200) ? 13 : 17;
+
+ // Compute quiet-mode button bounds and clamp panel to the right of it
+ const qbWidth = (g.getWidth() < 200) ? 28 : 34;
+ const qbXStart = sbarWid + 5;
+ const qbRight = qbXStart + qbWidth + 1;
+ const gapBetween = 6;
+ const timerRight = qbXStart + qbWidth + gapBetween + qbWidth + 1;
+ const panelX1 = Math.max(weatherAreaX1, Math.max(qbRight, timerRight) + 4);
+
+ // Clear the sensor/timer panel area from the true content-left edge (just right of LCARS border)
+ const sensorLeftX = sbarWid + outRad + gap + 1;
+ g.setColor(colorBg);
+ g.fillRect(sensorLeftX, weatherAreaY1, weatherAreaX2, weatherAreaY2);
+
+ g.setFont("Vector", weatherFontSize);
+ g.setColor("#FFFFFF");
+ g.setFontAlign(1, -1, 0); // right-aligned columns for right side
+
+ let currentY = weatherAreaY1;
+ // Two-column layout: left column for Press/Alt~, right for Temp/HR/Steps
+ const midX = Math.floor((sensorLeftX + weatherAreaX2) / 2);
+ const leftColX = sensorLeftX; // left edge of left column (left-aligned)
+ const rightColX = weatherAreaX2; // right edge of right column
+ const lineMargin = 1; // tighter packing to fit all lines
+
+ // Helper: barometric altitude estimate (ISA approximation)
+ function pressureToAltitudeMeters(pHpa) {
+ if (!pHpa || pHpa <= 0) return undefined;
+ return 44330 * (1 - Math.pow(pHpa / 1013.25, 0.1903));
+ }
+
+ // Right-aligned list of onboard readings (always show with placeholders)
+ // Temperature (right column)
+ if ((currentY + weatherLineHeight) <= weatherAreaY2) {
+ const tempStr = (internalTemperatureC !== null && isFinite(internalTemperatureC))
+ ? `${Math.round(internalTemperatureC * 9/5 + 32)}°F (${Math.round(internalTemperatureC)}°C)`
+ : "--";
+ g.drawString(`Temp: ${tempStr}`, rightColX, currentY);
+ currentY += weatherLineHeight + lineMargin;
+ }
+ // Heart Rate (right column)
+ if ((currentY + weatherLineHeight) <= weatherAreaY2) {
+ const hr = (Bangle.getHealthStatus && (Bangle.getHealthStatus().bpm || (Bangle.getHealthStatus("last")||{}).bpm)) || undefined;
+ const hrStr = (hr && isFinite(hr)) ? `${Math.round(hr)} bpm` : "--";
+ g.drawString(`HR: ${hrStr}`, rightColX, currentY);
+ currentY += weatherLineHeight + lineMargin;
+ }
+ // Steps (right column)
+ if ((currentY + weatherLineHeight) <= weatherAreaY2) {
+ const steps = (Bangle.getHealthStatus && Bangle.getHealthStatus("day").steps);
+ const stepsStr = (typeof steps === "number") ? `${steps}` : "--";
+ g.drawString(`Steps: ${stepsStr}`, rightColX, currentY);
+ currentY += weatherLineHeight + lineMargin;
+ }
+ // Left column: Press and Alt~ start at content left edge (right of LCARS border)
+ // Draw text at a fixed left edge (sensorLeftX) and let feature buttons overdraw on top
+ g.setFontAlign(-1, -1, 0);
+ let leftY = weatherAreaY1;
+ // Show Altimeter first, then Pressure (with tight label→value spacing)
+ if ((leftY + weatherLineHeight) <= weatherAreaY2) {
+ const altM = pressureToAltitudeMeters(internalPressureHpa);
+ const altStr = (altM !== undefined && isFinite(altM)) ? `${Math.round(altM)} m` : "--";
+ const label = "Alt~:";
+ const gapPx = 1; // tighter than a normal space
+ g.drawString(label, sensorLeftX, leftY);
+ g.drawString(altStr, sensorLeftX + g.stringWidth(label) + gapPx, leftY);
+ leftY += weatherLineHeight + lineMargin;
+ }
+ if ((leftY + weatherLineHeight) <= weatherAreaY2) {
+ const pressStr = (internalPressureHpa !== null && isFinite(internalPressureHpa))
+ ? `${Math.round(internalPressureHpa)} hPa`
+ : "--";
+ const label = "Press:";
+ const gapPx = 1;
+ g.drawString(label, sensorLeftX, leftY);
+ g.drawString(pressStr, sensorLeftX + g.stringWidth(label) + gapPx, leftY);
+ leftY += weatherLineHeight + lineMargin;
+ }
+ g.setFontAlign(-1, -1, 0);
+ // Ensure feature buttons are on top
+ drawFeatureButtons();
+
+ // Finally, draw the separator on top of the sensor area
+ // Recompute the hour button row top Y using the same sizing rules
+ const marginBottom_sep = 4;
+ const actualHourButtonHeight_sep = (g.getWidth() < 200) ? 18 : 28;
+ const actualMinuteButtonHeight_sep = (g.getWidth() < 200) ? 21 : 22;
+ const actualButtonGap_sep = (g.getWidth() < 200) ? 2 : 4;
+ const minuteButtonsTopY_sep = r.y2 - marginBottom_sep - actualMinuteButtonHeight_sep + 1;
+ const hourButtonsTopY_sep = minuteButtonsTopY_sep - actualButtonGap_sep - actualHourButtonHeight_sep;
+ drawSeparatorBar(hourButtonsTopY_sep, r.x2);
+}
+// --- End Function to draw Weather Information ---
+
+// --- Timer overlay ---
+function toggleTimerOverlay() {
+ showTimer = !showTimer;
+ if (showTimer) {
+ timerMode = "setup";
+ timerRemainingMs = timerSetMs;
+ if (timerIntervalId) clearInterval(timerIntervalId);
+ timerIntervalId = setInterval(()=>{ drawTimerOverlay(); }, 200);
+ } else {
+ if (timerIntervalId) clearInterval(timerIntervalId);
+ timerIntervalId = undefined;
+ // Clear ALL timer UI bounds so nothing lingers
+ backButtonBounds = null;
+ startPauseButtonBounds = null;
+ resetButtonBounds = null;
+ zeroButtonBounds = null;
+ minPlusBounds = null;
+ minMinusBounds = null;
+ secPlusBounds = null;
+ secMinusBounds = null;
+ // Fully redraw interface to clear any overlay artifacts (including back button)
+ drawClockInterface();
+ }
+}
+
+function drawTimerOverlay() {
+ const r = Bangle.appRect;
+ if (!r) return;
+ const panelTop = lowerTop + hbarHt + gap + 2;
+ // Make overlay reach down to just before the binary buttons
+ const marginBottom_sep = 4;
+ const actualHourButtonHeight_sep = (g.getWidth() < 200) ? 18 : 28;
+ const actualMinuteButtonHeight_sep = (g.getWidth() < 200) ? 21 : 22;
+ const actualButtonGap_sep = (g.getWidth() < 200) ? 2 : 4;
+ const minuteButtonsTopY_sep = r.y2 - marginBottom_sep - actualMinuteButtonHeight_sep + 1;
+ const hourButtonsTopY_sep = minuteButtonsTopY_sep - actualButtonGap_sep - actualHourButtonHeight_sep;
+ const panelBottom = hourButtonsTopY_sep - 2;
+ // Extend overlay left to cover the quiet/timer buttons and any sensor text
+ // while keeping the back button (at x≈3) visible outside the overlay.
+ const panelLeft = sbarWid + 1;
+ const panelRight = g.getWidth() - 3;
+
+ // Clear area
+ g.setColor(colorBg);
+ g.fillRect(panelLeft, panelTop, panelRight, panelBottom);
+
+ // Back button on left border
+ const backSize = (g.getWidth() < 200) ? 18 : 24;
+ const backX1 = 3;
+ const backY1 = panelTop + 4;
+ g.setColor(colorLCARSGray);
+ drawRoundedButton(backX1, backY1, backSize, backSize, 6, colorLCARSGray);
+ g.setColor("#000000");
+ g.setFont("6x8", 2);
+ g.setFontAlign(0,0);
+ g.drawString("<", backX1 + Math.floor(backSize/2), backY1 + Math.floor(backSize/2));
+ backButtonBounds = { x1:backX1, y1:backY1, x2:backX1+backSize, y2:backY1+backSize };
+
+ // Timer UI
+ const cx = Math.floor((panelLeft + panelRight)/2);
+ const cy = Math.floor((panelTop + panelBottom)/2);
+ g.setFontAlign(0,0);
+ g.setColor("#FFFFFF");
+ const bigFont = (g.getWidth() < 200) ? 22 : 28;
+ g.setFont("Vector", bigFont);
+
+ // Update remaining time if running
+ if (timerMode === "running") {
+ const now = Date.now();
+ timerRemainingMs = Math.max(0, timerEndTimeMs - now);
+ if (timerRemainingMs <= 0) {
+ timerMode = "finished";
+ Bangle.buzz(500);
+ }
+ }
+
+ // Format remaining or set time for display
+ const dispMs = (timerMode === "setup") ? timerSetMs : timerRemainingMs;
+ const totalSec = Math.floor(dispMs/1000);
+ const ss = (totalSec % 60).toString().padStart(2,'0');
+ const mm = (Math.floor(totalSec/60) % 60).toString().padStart(2,'0');
+ const hh = Math.floor(totalSec/3600);
+ const timeStr = (hh>0) ? `${hh}:${mm}:${ss}` : `${mm}:${ss}`;
+ const timeStrWidth = g.stringWidth(timeStr);
+ const timeStrX1 = cx - Math.floor(timeStrWidth/2);
+ g.drawString(timeStr, cx, cy);
+
+ // Controls
+ const btnW = (g.getWidth() < 200) ? 20 : 40;
+ const btnH = (g.getWidth() < 200) ? 20 : 40;
+ const gapY = 2;
+ const smallGap = 2; // tighter spacing between Reset/Zero
+
+ // Button layout: Start/Pause on left; Reset and Zero stacked closely on right
+ const leftX = panelLeft + 6;
+ const rightX = panelRight - 6 - btnW;
+ const spBtnH = btnH + ((g.getWidth() < 200) ? 6 : 8); // make Start/Pause taller
+ const spY = cy - Math.floor(spBtnH/2); // center aligned with time string
+ // Right-side stack (Reset above Zero) placed close together and centered
+ const rzTotalH = (spBtnH * 2) + smallGap;
+ const rzTop = cy - Math.floor(rzTotalH/2);
+ const rsY = rzTop;
+ const zY = rsY + spBtnH + smallGap;
+
+ // Start/Pause/Done control (icon-only)
+ const spX = leftX;
+ drawRoundedButton(spX, spY, btnW, spBtnH, 6, colorLCARSGray);
+ g.setColor("#000000");
+ // Icons centered inside button
+ const spCX = spX + Math.floor(btnW/2);
+ const spCY = spY + Math.floor(spBtnH/2);
+ if (timerMode === "running") {
+ // Pause: two bars
+ const barW = Math.max(2, Math.floor(btnW*0.15));
+ const barH1 = Math.floor(spBtnH*0.6);
+ const gapBars = Math.max(2, Math.floor(btnW*0.12));
+ const y1 = spCY - Math.floor(barH1/2);
+ g.fillRect(spCX - gapBars - barW, y1, spCX - gapBars - 1, y1 + barH1);
+ g.fillRect(spCX + gapBars + 1, y1, spCX + gapBars + barW, y1 + barH1);
+ } else if (timerMode === "paused" || timerMode === "setup") {
+ // Play: right-pointing triangle
+ const triW = Math.floor(btnW*0.45), triH = Math.floor(spBtnH*0.55);
+ g.fillPoly([ spCX - Math.floor(triW/4), spCY - Math.floor(triH/2), spCX - Math.floor(triW/4), spCY + Math.floor(triH/2), spCX + Math.floor(triW/2), spCY ]);
+ } else { // finished
+ // Check mark
+ const size = Math.floor(Math.min(btnW, spBtnH)*0.5);
+ const x0 = spCX - Math.floor(size/2);
+ const y0 = spCY - Math.floor(size/4);
+ g.drawLine(x0, y0, x0 + Math.floor(size*0.3), y0 + Math.floor(size*0.3));
+ g.drawLine(x0 + Math.floor(size*0.3), y0 + Math.floor(size*0.3), x0 + size, y0 - Math.floor(size*0.3));
+ }
+ startPauseButtonBounds = { x1:spX, y1:spY, x2:spX+btnW, y2:spY+spBtnH };
+
+ // Reset button (restore to set time) - circular arrow icon
+ const rsX = rightX;
+ drawRoundedButton(rsX, rsY, btnW, spBtnH, 6, colorLCARSGray);
+ g.setColor("#000000");
+ const rcx = rsX + Math.floor(btnW/2);
+ const rcy = rsY + Math.floor(spBtnH/2);
+ const rr = Math.floor(Math.min(btnW, spBtnH)*0.35);
+ g.drawCircle(rcx, rcy, rr);
+ // Arrow head at top-right of circle
+ const ahx = rcx + Math.floor(rr*0.7);
+ const ahy = rcy - Math.floor(rr*0.7);
+ g.fillPoly([ ahx, ahy, ahx + 5, ahy, ahx, ahy + 5 ]);
+ resetButtonBounds = { x1:rsX, y1:rsY, x2:rsX+btnW, y2:rsY+spBtnH };
+
+ // Zero button (set remaining and set time to 0:00) - zero icon
+ const zX = rightX;
+ drawRoundedButton(zX, zY, btnW, spBtnH, 6, colorLCARSGray);
+ g.setColor("#000000");
+ g.setFont("6x8", 2);
+ g.setFontAlign(0,0);
+ g.drawString("0", zX + Math.floor(btnW/2), zY + Math.floor(spBtnH/2));
+ zeroButtonBounds = { x1:zX, y1:zY, x2:zX+btnW, y2:zY+spBtnH };
+
+ // Setup mode increment/decrement controls
+ if (timerMode === "setup") {
+ // Triangle-only controls positioned directly above/below the digits
+ const triW = Math.max(10, Math.floor(bigFont*0.8));
+ const triH = Math.max(8, Math.floor(bigFont*0.65));
+ const fontH = g.getFontHeight();
+ const gapTri = Math.max(14, Math.floor(fontH*0.5)); // increase separation further
+ const upY = cy - Math.floor(fontH/2) - gapTri;
+ const dnY = cy + Math.floor(fontH/2) + gapTri;
+
+ // Compute centers for MM and SS from actual rendered string widths
+ const mmStr = mm, ssStr = ss;
+ const mmW = g.stringWidth(mmStr);
+ const colonW = g.stringWidth(":");
+ const ssW = g.stringWidth(ssStr);
+ const mmCX = timeStrX1 + Math.floor(mmW/2);
+ const ssCX = timeStrX1 + mmW + colonW + Math.floor(ssW/2);
+
+ g.setColor("#FFFFFF");
+ // Minutes up/down triangles
+ g.fillPoly([ mmCX, upY - Math.floor(triH/2), mmCX - Math.floor(triW/2), upY + Math.floor(triH/2), mmCX + Math.floor(triW/2), upY + Math.floor(triH/2) ]);
+ minPlusBounds = { x1:mmCX - Math.floor(triW/2), y1:upY - Math.floor(triH/2), x2:mmCX + Math.floor(triW/2), y2:upY + Math.floor(triH/2) };
+ g.fillPoly([ mmCX, dnY + Math.floor(triH/2), mmCX - Math.floor(triW/2), dnY - Math.floor(triH/2), mmCX + Math.floor(triW/2), dnY - Math.floor(triH/2) ]);
+ minMinusBounds = { x1:mmCX - Math.floor(triW/2), y1:dnY - Math.floor(triH/2), x2:mmCX + Math.floor(triW/2), y2:dnY + Math.floor(triH/2) };
+
+ // Seconds up/down triangles
+ g.fillPoly([ ssCX, upY - Math.floor(triH/2), ssCX - Math.floor(triW/2), upY + Math.floor(triH/2), ssCX + Math.floor(triW/2), upY + Math.floor(triH/2) ]);
+ secPlusBounds = { x1:ssCX - Math.floor(triW/2), y1:upY - Math.floor(triH/2), x2:ssCX + Math.floor(triW/2), y2:upY + Math.floor(triH/2) };
+ g.fillPoly([ ssCX, dnY + Math.floor(triH/2), ssCX - Math.floor(triW/2), dnY - Math.floor(triH/2), ssCX + Math.floor(triW/2), dnY - Math.floor(triH/2) ]);
+ secMinusBounds = { x1:ssCX - Math.floor(triW/2), y1:dnY - Math.floor(triH/2), x2:ssCX + Math.floor(triW/2), y2:dnY + Math.floor(triH/2) };
+ }
+}
+// --- End Timer overlay ---
+
+// --- Function to draw Feature Buttons ---
+function drawFeatureButtons() {
+ const r = Bangle.appRect;
+ if (!r) return;
+
+ // Single button for Quiet Mode toggle
+ let buttonWidth = (g.getWidth() < 200) ? 28 : 34;
+ let buttonHeight = (g.getWidth() < 200) ? 22 : 28;
+ let buttonCornerRadius = 6;
+
+ let weatherLineHeightEstimate = (g.getWidth() < 200) ? 15 : 17;
+ const areaLeft = sbarWid + 5;
+ const areaTop = (lowerTop + hbarHt + gap + 5) + weatherLineHeightEstimate + 18; // move down to free space for two columns
+
+ let estTimeBtnAreaHeight = (g.getWidth() < 200) ? 45 : 55;
+ const maxYForFeatureButtons = r.y2 - estTimeBtnAreaHeight - 5;
+
+ // Ensure button fits in area
+ if (areaTop + buttonHeight > maxYForFeatureButtons) {
+ buttonHeight = maxYForFeatureButtons - areaTop;
+ }
+
+ const xStart = areaLeft;
+ const yStart = areaTop;
+ // Gray as main color, red when quiet mode is active
+ const color = quietModeActive ? "#FF0000" : colorLCARSGray;
+
+ // Clear the area before drawing
+ g.setColor(colorBg);
+ g.fillRect(xStart - 1, yStart - 1, xStart + buttonWidth + 1, yStart + buttonHeight + 1);
+
+ // Draw single button
+ drawRoundedButton(xStart, yStart, buttonWidth, buttonHeight, buttonCornerRadius, color);
+
+ // Draw Do Not Disturb icon when quiet mode is active
+ if (quietModeActive) {
+ drawDoNotDisturbIcon(xStart + buttonWidth/2, yStart + buttonHeight/2, Math.min(buttonWidth, buttonHeight) * 0.6);
+ }
+
+ // Save touch bounds for quiet toggle
+ quietToggleAreaBounds = { x1:xStart, y1:yStart, x2:xStart+buttonWidth, y2:yStart+buttonHeight };
+
+ // Draw second button (Timer) to the right of quiet button
+ const gapBetween = 6;
+ const tX = xStart + buttonWidth + gapBetween;
+ const tY = yStart;
+ drawRoundedButton(tX, tY, buttonWidth, buttonHeight, buttonCornerRadius, colorLCARSGray);
+ // Hourglass icon
+ g.setColor("#000000");
+ const hgSize = Math.floor(Math.min(buttonWidth, buttonHeight) * 0.6);
+ const hgCx = tX + Math.floor(buttonWidth/2);
+ const hgCy = tY + Math.floor(buttonHeight/2);
+ const half = Math.floor(hgSize/2);
+ const topY = hgCy - half + 1;
+ const botY = hgCy + half - 1;
+ // Draw top triangle (sand chamber)
+ g.fillPoly([
+ hgCx - half, topY,
+ hgCx + half, topY,
+ hgCx, hgCy
+ ]);
+ // Draw bottom triangle
+ g.fillPoly([
+ hgCx - half, botY,
+ hgCx + half, botY,
+ hgCx, hgCy
+ ]);
+ // Neck (thin band)
+ g.fillRect(hgCx - 1, hgCy - 2, hgCx + 1, hgCy + 2);
+ timerToggleAreaBounds = { x1:tX, y1:tY, x2:tX+buttonWidth, y2:tY+buttonHeight };
+}
+// --- End Function to draw Feature Buttons ---
+
+function drawClockInterface() {
+ ensureInitialSettingsCaptured();
+g.setBgColor(colorBg);
+g.clear();
+
+ // Draw LCARS borders (this includes the quiet mode icon logic now)
+ // ... (existing LCARS drawing code from drawClockInterface)
+ let currentUpperLeftBorderColor = quietModeActive ? colorLCARSGray : colorLCARSOrange;
+ g.setColor(currentUpperLeftBorderColor);
+g.fillCircle(outRad, divisionPos - outRad, outRad);
+g.fillRect(outRad, divisionPos - outRad, sbarWid + inRad, divisionPos);
+ g.fillRect(outRad, divisionPos - hbarHt, sbarWid + outRad, divisionPos);
+ g.fillRect(0, 0, sbarWid, divisionPos - outRad);
+ g.setColor(colorBg);
+g.fillCircle(sbarWid + inRad + 1, divisionPos - hbarHt - inRad - 1, inRad);
+g.fillRect(sbarWid + 1, divisionPos - outRad * 2, sbarWid + outRad, divisionPos - hbarHt - inRad);
+g.setColor(colorLCARSPurple);
+g.fillRect(sbarWid + outRad + gap + 1, divisionPos - hbarHt, g.getWidth(), divisionPos);
+g.setColor(colorLCARSPink);
+g.fillCircle(outRad, lowerTop + outRad, outRad);
+g.fillRect(outRad, lowerTop, sbarWid + inRad, lowerTop + outRad);
+ g.fillRect(outRad, lowerTop, sbarWid + outRad, lowerTop + hbarHt);
+ g.fillRect(0, lowerTop + outRad, sbarWid, sbarGapPos);
+ g.setColor(colorBg);
+g.fillCircle(sbarWid + inRad + 1, lowerTop + hbarHt + inRad + 1, inRad);
+g.fillRect(sbarWid + 1, lowerTop + hbarHt + inRad, sbarWid + outRad, lowerTop + outRad * 2);
+g.setColor(colorLCARSBrown);
+g.fillRect(0, sbarGapPos + gap + 1, sbarWid, g.getHeight());
+
+ // End LCARS borders draw (quiet icon removed; toggle moved to 2x2 grid)
+
+ drawDaySegmentsBar(new Date().getDay());
+ drawBatteryBar(E.getBattery());
+ drawDigitalClock(new Date());
+ // Steps now shown in the sensor panel
+ drawWeatherInfo();
+ drawFeatureButtons(); // Draw 2x2 quiet mode toggle grid
+
+ redrawClock = true;
+ updateClock(); // Use consolidated update function
+ // Kick off GPS time sync opportunistically
+ startGPSTimeSync();
+ // Start periodic barometer and temperature sampling
+ startBarometerSampling();
+ startTemperatureSampling();
+}
+
+// Clear the screen once, at startup.
+g.setBgColor(colorBg);
+g.clear();
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// clean app variables not defined with var/let/const
+if (global.intervalRefSec) {
+ clearInterval(intervalRefSec);
+ delete global.intervalRefSec;
+}
+
+// Start the clock
+drawClockInterface();
+
+// If already charging at startup, begin flashing effect
+if (isCharging) startChargingFlash();
+
+// Setup touch listener for Quiet Mode toggle on new 2x2 grid
+Bangle.on('touch', function(button, xy) {
+ if (quietToggleAreaBounds) {
+ if (xy.x >= quietToggleAreaBounds.x1 && xy.x <= quietToggleAreaBounds.x2 &&
+ xy.y >= quietToggleAreaBounds.y1 && xy.y <= quietToggleAreaBounds.y2) {
+ toggleQuietMode();
+ // Redraw the 2x2 grid to reflect new state color
+ drawFeatureButtons();
+ return;
+ }
+ }
+ if (timerToggleAreaBounds) {
+ if (xy.x >= timerToggleAreaBounds.x1 && xy.x <= timerToggleAreaBounds.x2 &&
+ xy.y >= timerToggleAreaBounds.y1 && xy.y <= timerToggleAreaBounds.y2) {
+ toggleTimerOverlay();
+ return;
+ }
+ }
+ if (showTimer) {
+ // Back
+ if (backButtonBounds && xy.x >= backButtonBounds.x1 && xy.x <= backButtonBounds.x2 && xy.y >= backButtonBounds.y1 && xy.y <= backButtonBounds.y2) {
+ toggleTimerOverlay();
+ return;
+ }
+ // Start/Pause / Done
+ if (startPauseButtonBounds && xy.x >= startPauseButtonBounds.x1 && xy.x <= startPauseButtonBounds.x2 && xy.y >= startPauseButtonBounds.y1 && xy.y <= startPauseButtonBounds.y2) {
+ if (timerMode === "setup" || timerMode === "paused") {
+ // Start
+ timerRemainingMs = (timerMode === "setup") ? timerSetMs : timerRemainingMs;
+ timerEndTimeMs = Date.now() + timerRemainingMs;
+ timerMode = "running";
+ } else if (timerMode === "running") {
+ // Pause
+ timerRemainingMs = Math.max(0, timerEndTimeMs - Date.now());
+ timerMode = "paused";
+ } else if (timerMode === "finished") {
+ // Dismiss buzzed state; reset to setup
+ timerMode = "setup";
+ timerRemainingMs = timerSetMs;
+ }
+ return;
+ }
+ // Reset
+ if (resetButtonBounds && xy.x >= resetButtonBounds.x1 && xy.x <= resetButtonBounds.x2 && xy.y >= resetButtonBounds.y1 && xy.y <= resetButtonBounds.y2) {
+ timerRemainingMs = timerSetMs;
+ if (timerMode === "running") timerEndTimeMs = Date.now() + timerRemainingMs;
+ return;
+ }
+ // Zero
+ if (zeroButtonBounds && xy.x >= zeroButtonBounds.x1 && xy.x <= zeroButtonBounds.x2 && xy.y >= zeroButtonBounds.y1 && xy.y <= zeroButtonBounds.y2) {
+ timerSetMs = 0; timerRemainingMs = 0;
+ if (timerMode === "running") timerEndTimeMs = Date.now();
+ return;
+ }
+ // Setup increments
+ if (timerMode === "setup") {
+ if (minPlusBounds && xy.x >= minPlusBounds.x1 && xy.x <= minPlusBounds.x2 && xy.y >= minPlusBounds.y1 && xy.y <= minPlusBounds.y2) { timerSetMs += 60*1000; timerRemainingMs = timerSetMs; return; }
+ if (minMinusBounds && xy.x >= minMinusBounds.x1 && xy.x <= minMinusBounds.x2 && xy.y >= minMinusBounds.y1 && xy.y <= minMinusBounds.y2) { timerSetMs = Math.max(0, timerSetMs - 60*1000); timerRemainingMs = timerSetMs; return; }
+ if (secPlusBounds && xy.x >= secPlusBounds.x1 && xy.x <= secPlusBounds.x2 && xy.y >= secPlusBounds.y1 && xy.y <= secPlusBounds.y2) { timerSetMs = Math.min(99*60*1000+59*1000, timerSetMs + 1000); timerRemainingMs = timerSetMs; return; }
+ if (secMinusBounds && xy.x >= secMinusBounds.x1 && xy.x <= secMinusBounds.x2 && xy.y >= secMinusBounds.y1 && xy.y <= secMinusBounds.y2) { timerSetMs = Math.max(0, timerSetMs - 1000); timerRemainingMs = timerSetMs; return; }
+ }
+ }
+});
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower', on => {
+ if (on) {
+ redrawClock = true;
+ // Draw immediately to kick things off.
+ updateClock();
+ // Bangle.drawWidgets(); // Widgets should redraw themselves if needed on lcdPower
+ if (isCharging) startChargingFlash();
+ } else {
+ redrawClock = false;
+ stopChargingFlash();
+ stopBarometerSampling(); // Stop barometer when LCD off
+ stopTemperatureSampling(); // Stop temperature sampling when LCD off
+ }
+});
diff --git a/apps/stardateclock_wbin/app.png b/apps/stardateclock_wbin/app.png
new file mode 100644
index 0000000000..202b3b868a
Binary files /dev/null and b/apps/stardateclock_wbin/app.png differ
diff --git a/apps/stardateclock_wbin/metadata.json b/apps/stardateclock_wbin/metadata.json
new file mode 100644
index 0000000000..e4700322e9
--- /dev/null
+++ b/apps/stardateclock_wbin/metadata.json
@@ -0,0 +1,23 @@
+{
+ "id": "stardateclock_wbin",
+ "name": "Stardate Clock with Binary Time",
+ "shortName": "BinaryStardate",
+ "description": "A clock displaying a stardate along with a Binary clock in LCARS design",
+ "version": "0.08",
+ "icon": "app.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "allow_emulator": true,
+ "readme": "README.md",
+ "storage": [
+ { "name": "stardateclock_wbin.app.js", "url": "app.js" },
+ { "name": "stardateclock_wbin.img", "url": "app-icon.js", "evaluate": true }
+ ],
+ "screenshots": [
+ { "url": "screenshot1.png" },
+ { "url": "screenshot2.png" },
+ { "url": "screenshot3.png" },
+ { "url": "screenshot4.png" }
+ ]
+}
diff --git a/apps/stardateclock_wbin/screenshot1.png b/apps/stardateclock_wbin/screenshot1.png
new file mode 100644
index 0000000000..f82e00195f
Binary files /dev/null and b/apps/stardateclock_wbin/screenshot1.png differ
diff --git a/apps/stardateclock_wbin/screenshot2.png b/apps/stardateclock_wbin/screenshot2.png
new file mode 100644
index 0000000000..421b2e5663
Binary files /dev/null and b/apps/stardateclock_wbin/screenshot2.png differ
diff --git a/apps/stardateclock_wbin/screenshot3.png b/apps/stardateclock_wbin/screenshot3.png
new file mode 100644
index 0000000000..9f7939357d
Binary files /dev/null and b/apps/stardateclock_wbin/screenshot3.png differ
diff --git a/apps/stardateclock_wbin/screenshot4.png b/apps/stardateclock_wbin/screenshot4.png
new file mode 100644
index 0000000000..ffa598bc61
Binary files /dev/null and b/apps/stardateclock_wbin/screenshot4.png differ