diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..27d3e08
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,28 @@
+# .github/workflows/build.yml
+name: build
+on:
+ push:
+ branches: [main, 'dev/**']
+ pull_request:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+ - name: Cache PlatformIO
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/pip
+ ~/.platformio/.cache
+ key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
+ restore-keys: ${{ runner.os }}-pio-
+ - name: Install PlatformIO
+ run: pip install platformio
+ - name: Build
+ run: pio run
diff --git a/.gitignore b/.gitignore
index eb17851..9e0918c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
include/version.h
/net-commander
docs
-data
\ No newline at end of file
+data
+test
\ No newline at end of file
diff --git a/README.md b/README.md
index d6acfbd..2bc93c0 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,30 @@
# CYD — Robot Study Companion firmware
-Animated robot face with on-screen menus for the [Cheap Yellow Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) (ESP32-2432S028R), part of the Robot Study Companion (RSC) project. Built on Grobot_Animations and TFT_eSPI, controlled over UART, configured at runtime, persistent across reboots.
+[](https://opensource.org/licenses/Apache-2.0)
+[](https://github.com/RobotStudyCompanion/CYD/tags)
+[](https://github.com/RobotStudyCompanion/CYD/actions)
+
+
+
+Animated robot face with on-screen touch menus for the [Cheap Yellow Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) (ESP32-2432S028R), part of the Robot Study Companion (RSC) project. Acts both as a standalone animated companion and as a serial-controlled control surface for a host device (volume / mute / power / reboot). Built on Grobot_Animations, TFT_eSPI, and GUIslice; configured at runtime over UART, persistent across reboots.
---
## Features
- **Animated eyes** via [Grobot_Animations](https://github.com/tanmaywankar/Grobot_Animations) — spring physics, 10 preset moods, fine-tunable per-eye in real time
-- **Two-mode UI** — FACE for the eye animation, MENU for on-screen settings; long-press toggles between them
-- **Schema-driven menus** — declarative `MenuScreen` / `MenuItem` arrays in flash, with PUSH / INVOKE / BACK action kinds; menu actions invoke through the same dispatch table as serial commands
-- **Runtime schema push** — `menu_begin` / `menu_screen` / `menu_item` / `menu_end` over UART loads a session-only menu without reflashing
-- **UART command surface** — ~40 commands, dispatch-table parser, auto-generated `help` and `status`
-- **NVS persistence with write debounce** — settable values survive reboot; writes batched after 5s of quiet to spare flash wear
+- **Two-mode UI** — FACE for the eye animation, MENU for on-screen controls; long-press toggles into menu
+- **GUIslice touch UI** built with the GUIslice Builder — main page with sliders and toggle icons, burger menu popup, power popup with destructive-action confirm dialog
+- **Host control surface** — volume / mute / mic / power / reboot dispatched to host service over UART as `host_*` messages
+- **UART command surface** — ~30 commands, dispatch-table parser, auto-generated `help` and `status`
+- **NVS persistence with write debounce** — settable values survive reboot; writes batched after 5 s of quiet to spare flash wear
- **LDR-driven auto-brightness** with continuous linear mapping and output-side smoothing
+- **Theme command** — `theme:light` / `theme:dark` preset pairs for background and eye colour; arbitrary colours still settable via `bg_colour` / `eye_colour`
+- **Idle timeout** — menu auto-returns to face after configurable inactivity; disabled while touch debug is on
- **Random idle animation** — periodic cycling between IDLE1/2/3 moods when enabled
-- **Touch event primitives** — TAP / LONG_PRESS state machine with drag-cancel
-- **HUD overlay** (FPS counter), pause/resume, manual blink, canvas look-at, asymmetric eyes
+- **Touch event primitives** — TAP / LONG_PRESS state machine with drag-cancel; debug logging on state transitions (low-spam)
+- **Live stats overlay** in the burger menu — firmware version, uptime, free heap; refreshes at 1 Hz while open
+- **HUD overlay** (Grobot FPS counter), pause/resume, manual blink, canvas look-at, asymmetric eyes
- **Compile-time debug gates** for touch and LDR diagnostics
- **Git-describe versioning** baked at build time
@@ -25,7 +34,7 @@ Animated robot face with on-screen menus for the [Cheap Yellow Display](https://
- **Board:** ESP32-2432S028R (CYD original variant)
- **MCU:** ESP32-WROOM, no PSRAM, ~302 KB heap free at boot
-- **Display:** ILI9341, 320×240 landscape, SPI at 55 MHz
+- **Display:** ILI9341, 320×240 landscape, SPI at 55 MHz, mounted at rotation 3 (180° flipped for the enclosure)
- **Touch:** XPT2046 resistive on its own bitbanged bus (avoids HSPI collision)
- **LDR:** GPIO 34 (after reflow on this variant — most CYDs ship without a working LDR)
- **RGB LED:** GPIO 4 (R), 16 (G), 17 (B) — active low
@@ -39,7 +48,7 @@ Animated robot face with on-screen menus for the [Cheap Yellow Display](https://
| TFT SCK | 14 | |
| TFT MOSI | 13 | |
| TFT MISO | 12 | |
-| TFT Backlight | 21 | LEDC PWM, 5 kHz, 8-bit |
+| TFT Backlight | 21 | LEDC PWM channel 0, 5 kHz, 8-bit |
| Touch CS | 33 | |
| Touch IRQ | 36 | |
| LDR | 34 | ADC1, after hardware reflow |
@@ -49,13 +58,15 @@ Animated robot face with on-screen menus for the [Cheap Yellow Display](https://
## Software stack
-| Component | Source | Pinned version |
+| Component | Source | Version (tested) |
|-----------|--------|----------------|
| PlatformIO platform | espressif32 | 6.4.0 |
| Arduino core | arduino-esp32 | 2.0.11 |
| Display driver | [bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) | 2.5.43 |
| Eye animations | [tanmaywankar/Grobot_Animations](https://github.com/tanmaywankar/Grobot_Animations) | 0.1.3 |
| Touch driver | [hexeguitar/CYD28_Touchscreen](https://github.com/hexeguitar/CYD28_Touchscreen) | bitbang fork |
+| UI framework | [ImpulseAdventure/GUIslice](https://github.com/ImpulseAdventure/GUIslice) | 0.17.2 |
+| UI layout | GUIslice Builder | 0.17.b41 |
| NVS | Built-in `Preferences` | — |
---
@@ -67,22 +78,23 @@ src/
main.cpp Pure orchestration — print*, init*, service*
Config.cpp Dispatch-table UART parser, NVS load/save with debounce
FaceRenderer.cpp Grobot wrapper, splash, mood/colour, pause/resume, idle cycling
- DisplayInit.cpp TFT init, backlight PWM (LEDC channel 0)
- UIManager.cpp Modes (FACE/MENU), screen stack, hit-testing, render
- MenuSchema.cpp Default menu tables + runtime push parser
+ DisplayInit.cpp TFT init, backlight PWM (LEDC channel 0), reattach helper
+ UIManager.cpp FACE/MENU modes, long-press transitions, idle timeout, stats tick
+ MenuCallbacks.cpp GUIslice Builder output — button/slider callbacks, icon swap, stats refresh
+ TouchHandler.cpp initTouch, raw + event-style polls (TAP / LONG_PRESS), state-transition debug logging
DebugOverlay.cpp Touch dots, edge detection, diag helpers
LdrSensor.cpp LDR sampling + continuous auto-brightness
- TouchHandler.cpp initTouch, raw + event-style polls (TAP/LONG_PRESS)
LED_Solution.cpp RGB LED control
include/
+ test_GSLC.h GUIslice Builder generated — element creation, page layout, image refs
... corresponding headers + version.h (auto-generated, gitignored)
scripts/
- version.py git describe -> include/version.h, pre-build hook
+ version.py git describe → include/version.h, pre-build hook
```
-The `Config` struct is the single source of truth for stateful settings. UART setters mark the config dirty; `serviceNvs()` flushes to NVS after a 5s debounce window (or immediately before reboot). `UIManager` owns mode and screen-stack state; menu actions invoke through the same dispatch table that handles typed serial commands, so adding a new UART command automatically makes it menu-capable.
+The `Config` struct is the single source of truth for stateful settings. UART setters and on-screen toggles both mark the config dirty; `serviceNvs()` flushes to NVS after a 5 s debounce window. `UIManager` owns mode and the menu lifecycle; `MenuCallbacks` (the GUIslice Builder output, hand-extended with state tracking and icon swap logic) handles all on-screen widget callbacks and routes through the same `findCommand()` dispatch table that handles typed serial input.
```mermaid
graph TB
@@ -91,6 +103,7 @@ graph TB
TFT[TFT display]
LDR[LDR]
NVS[(NVS)]
+ HOST[Host service]
UART <-->|commands| Config
TS --> TouchHandler
@@ -98,27 +111,87 @@ graph TB
Config <-->|persist| NVS
Config[Config
dispatch + NVS debounce]
- UIManager[UIManager
modes / stack / render]
- MenuSchema[MenuSchema
defaults + runtime push]
- TouchHandler[TouchHandler
TAP/LONG_PRESS]
+ UIManager[UIManager
modes / idle timeout / stats tick]
+ MenuCallbacks[MenuCallbacks
GUIslice widgets / icon swap]
+ TouchHandler[TouchHandler
TAP / LONG_PRESS]
FaceRenderer[FaceRenderer
Grobot + splash + idle]
DisplayInit[DisplayInit
TFT + backlight PWM]
LdrSensor[LdrSensor
auto-brightness]
TouchHandler -->|events| UIManager
- UIManager --> MenuSchema
- UIManager -.INVOKE.-> Config
+ UIManager -->|gslc_Update| MenuCallbacks
+ MenuCallbacks -.findCommand.-> Config
+ MenuCallbacks -->|host_*| HOST
+ MenuCallbacks -->|setBacklight| DisplayInit
Config -.dispatch.-> FaceRenderer
Config -.dispatch.-> UIManager
Config -.dispatch.-> DisplayInit
- UIManager -->|menu render| TFT
+ MenuCallbacks -->|menu render| TFT
FaceRenderer -->|eye animation| TFT
DisplayInit -->|init / PWM| TFT
LdrSensor -.brightness.-> DisplayInit
```
-The crucial connection is `UIManager -.INVOKE.-> Config`: menu actions and typed serial commands both flow through `findCommand()` and the dispatch table, so the menu surface automatically tracks any new UART command without separate wiring.
+Adding a new UART command automatically makes it accessible from on-screen widgets, since menu callbacks invoke through the same dispatch table.
+
+---
+
+## On-screen UI
+
+Three GUIslice pages plus a confirm popup, navigated entirely by touch.
+
+### Main page (`E_PG_MAIN`)
+
+Default screen after long-pressing into menu mode.
+
+| Element | Position | Action |
+|---|---|---|
+| Power button | top-left | Opens power popup |
+| Burger menu button | top-right | Opens burger menu popup |
+| RSC logo | centre-left | Returns to FACE mode |
+| Back arrow | right-middle | Returns to FACE mode |
+| Volume slider | right column | Posts `host_vol:NN` to UART (throttled to ~10 Hz) |
+| Brightness slider | middle column | Sets local backlight via LEDC; broadcasts `bright:NN` (throttled) |
+| Volume mute icon | below volume slider | Posts `host_mute`; icon swaps mute / low / loud based on slider level |
+| Mic icon | below volume mute | Posts `host_mic`; icon swaps active / muted |
+| Auto-brightness icon | below brightness slider | Toggles `auto_bright`; icon swaps manual / auto |
+
+Sliders are inverted: top = max, bottom = min (matches the 180°-flipped enclosure mount).
+
+### Burger menu (`E_PG_BURGER_MENU`)
+
+Settings popup.
+
+| Element | Action |
+|---|---|
+| Debug toggle | Flips `touch_debug` (also disables idle timeout while on) |
+| Theme toggle | Flips `theme:light` / `theme:dark` |
+| Stats text | Firmware version (tag + commits, `+` suffix if dirty), uptime, free heap — refreshes 1 Hz while open |
+| Back arrow | Close popup |
+
+### Power popup (`E_PG_PWR`)
+
+2×2 grid for power actions, split by a horizontal line.
+
+| Row | Left | Right |
+|---|---|---|
+| Raspberry Pi | Poweroff (destructive → confirm) | Reboot (recoverable) |
+| Front panel (CYD) | Reset (destructive, wipes NVS → confirm) | Reboot (recoverable) |
+
+Plus a back arrow to dismiss. Destructive actions route through a reusable confirm popup (`E_PG_POPUP_CONFIRM`) with Yes / Cancel; recoverable actions fire immediately.
+
+### Mode transitions
+
+| From | To | Trigger |
+|---|---|---|
+| FACE | MENU | Long-press (~1 s) anywhere |
+| FACE | MENU | `mode:MENU` over serial |
+| MENU | FACE | Tap RSC logo or back arrow on main page |
+| MENU | FACE | Idle timeout (default 10 s, configurable via `menu_timeout`) |
+| MENU | FACE | `mode:FACE` over serial |
+
+Idle timeout is suspended while `touch_debug:on` — useful when debugging touch coordinates without the menu closing on you.
---
@@ -132,34 +205,36 @@ All commands travel over USB serial at **115200 baud**, newline-terminated.
Type `help` for the full list. `status` prints all current settable values.
+Boolean values accept any of: `on`/`off`, `true`/`false`, `yes`/`no`, `1`/`0` (case-insensitive).
+
### Selected commands
| Command | Form | Example | Notes |
|---------|------|---------|-------|
| `mood` | setter+getter | `mood:HAPPY` | NEUTRAL, HAPPY, ANGRY, SAD, EXCITED, ANNOYED, QUESTIONING, IDLE1-3 |
-| `mood_cycle` | toggle | `mood_cycle:on` | Auto-cycle through moods every 5–8s |
-| `idle_anim` | toggle | `idle_anim:on` | Random IDLE1/2/3 every 5–15s when `mood_cycle` is off |
+| `mood_cycle` | toggle | `mood_cycle:on` | Auto-cycle through moods every 5–8 s |
+| `idle_anim` | toggle | `idle_anim:on` | Random IDLE1/2/3 every 5–15 s when `mood_cycle` is off |
+| `theme` | setter+getter | `theme:dark` | `light` (white bg / black eyes) or `dark` (black bg / white eyes) |
| `face` | setter+getter | `face:0,50,0,30,45` | Symmetric custom mood: topH,botH,tilt,pR,r |
| `face_l` / `face_r` | setter+getter | `face_l:0,30,0,30,45` | Per-eye asymmetric override |
| `look` | setter+getter | `look:30,-20` | Canvas offset (eye gaze direction) |
| `blink` | action | `blink` | Manual blink |
| `hud` | toggle | `hud:on` | FPS overlay (Grobot built-in) |
-| `eye_colour` / `bg_colour` | setters | `eye_colour:00FF00` | RGB565, 6 hex digits |
-| `bright` | setter+getter | `bright:50` | Manual backlight 0–100% |
+| `eye_colour` / `bg_colour` | setters | `eye_colour:00FF00` | RGB565, 6 hex digits; aliases `eye_color` / `bg_color` |
+| `bright` | setter+getter | `bright:50` | Manual backlight 1–100 % |
| `auto_bright` | toggle | `auto_bright:on` | LDR-driven |
-| `bright_light` / `bright_dark` | setters | `bright_dark:1` | Auto-brightness endpoints (1–100%) |
-| `pause` / `resume` | actions | — | Freeze/unfreeze face renderer |
+| `bright_light` / `bright_dark` | setters | `bright_dark:1` | Auto-brightness endpoints (0–100 %) |
+| `menu_timeout` | setter+getter | `menu_timeout:30` | Idle timeout in seconds (0 = disabled) |
+| `touch_debug` | toggle | `touch_debug:on` | State-transition debug logging on touch events |
+| `touch_dots` | toggle | `touch_dots:on` | Visual touch dots on screen |
+| `pause` / `resume` | actions | — | Freeze / unfreeze face renderer |
| `clear` | action | — | Wipe screen with background colour |
| `splash` | action | — | Re-show "RSC-CYD" splash |
| `tap` | action | `tap:160,120,1500` | Inject synthetic touch (debugging) |
| `led` | setter | `led:cyan` | RGB LED: off/on/red/green/blue/white/yellow/cyan/magenta or `r,g,b` |
| `mode` | setter+getter | `mode:MENU` | FACE / MENU; long-press is the touch-side equivalent |
-| `menu` | action | `menu` | Print current screen + items |
-| `menu_back` | action | — | Pop one menu level |
-| `menu_select` | setter | `menu_select:0` | Select item by 0-based index |
-| `menu_begin` / `menu_end` | actions | — | Start / finalise a runtime schema load |
-| `menu_screen` | setter | `menu_screen:Custom` | Add a screen during runtime load |
-| `menu_item` | setter | `menu_item:invoke,Brighter,bright:75` | `kind,label[,payload]` (kind = push/invoke/back) |
+| `menu` | action | `menu` | Print current menu state |
+| `menu_back` | action | — | Exit menu (returns to FACE) |
| `mem` / `uptime` / `version` | actions | — | Diagnostics |
| `ldr` / `light` / `lux` | actions | — | Light sensor readouts |
| `reboot` / `reset` | actions | — | Soft reboot / NVS wipe + reboot |
@@ -169,59 +244,26 @@ Type `help` for the full list. `status` prints all current settable values.
```bash
# From a host machine over USB serial:
echo "mood:HAPPY" > /dev/ttyUSB0
+echo "theme:dark" > /dev/ttyUSB0
echo "auto_bright:on" > /dev/ttyUSB0
echo "status" > /dev/ttyUSB0
```
-```text
-# Push a runtime menu over UART (overrides the compiled-in default for the session):
-menu_begin
-menu_screen:Custom
-menu_item:push,Sub,1
-menu_item:invoke,Bright max,bright:100
-menu_item:back,Resume,
-menu_screen:Sub
-menu_item:invoke,Blink,blink
-menu_item:back,Back,
-menu_end
-```
-
---
-## Menus
-
-Two top-level UI modes:
-
-- **FACE** — animated eyes, splash, idle cycling, etc.
-- **MENU** — on-screen menu, navigated by touch.
-
-**Enter:** long-press (~1s) on the screen, or send `mode:MENU`.
-**Navigate:** tap an item row to fire its action; tap a `BACK` row to pop one level.
-**Exit:** popping past the root returns to FACE automatically, or long-press anywhere to exit immediately.
+## Host serial contract
-### Action kinds
+When the user interacts with on-screen widgets whose state lives on the host (volume, mute, mic, power), the firmware emits short uppercase-namespaced messages over UART. A host service is expected to consume these.
-Each `MenuItem` carries an `ActionKind`:
+| Message | Trigger | Notes |
+|---|---|---|
+| `host_vol:NN` | Volume slider drag | NN in 0–100; throttled to ~10 Hz during drag |
+| `host_mute` | Volume mute icon tap | Idempotent toggle request; CYD does not track audio mute state |
+| `host_mic` | Mic icon tap | Idempotent toggle request; CYD does not track mic state |
+| `host_reboot` | Pi Reboot menu button | Recoverable; no confirmation needed |
+| `host_poweroff` | Pi Poweroff menu button | Confirmed through the Yes/Cancel popup before emission |
-- **`PUSH`** — navigate into a sub-screen. Payload is a `MenuScreen*` (compiled-in) or a 0-based screen index (runtime-pushed schema).
-- **`INVOKE`** — fire a UART command string (e.g. `"bright:75"`, `"blink"`). Routed through the same `findCommand()` path that handles typed serial input — no separate code paths.
-- **`BACK`** — pop one screen off the stack.
-
-### Default menu
-
-Compiled into firmware (`src/MenuSchema.cpp`):
-
-```
-Menu
-├── Brightness ▶ (25% / 50% / 75% / 100% / Back)
-├── Mood ▶ (Neutral / Happy / Sad / Angry / Back)
-├── Blink
-└── Resume
-```
-
-### Runtime schema push
-
-`menu_begin` … `menu_end` loads a session-only schema that replaces the active root for the rest of the session; reboot reverts to the compiled default. Bounds: 8 screens, 8 items per screen, ~1 KB pooled string storage. Items reference push targets by 0-based screen index in the schema being loaded.
+The CYD does not parse these messages back, so a host that echoes them won't loop.
---
@@ -236,6 +278,27 @@ pio device monitor
`pio device monitor` opens a serial console at the right baud. Type `help` once it boots.
+### CI build status (optional)
+
+The build shield in the header references a GitHub Actions workflow. A minimal workflow that runs `pio run` on push:
+
+```yaml
+# .github/workflows/build.yml
+name: build
+on: [push, pull_request]
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with: { python-version: '3.x' }
+ - run: pip install platformio
+ - run: pio run
+```
+
+Once committed under `.github/workflows/build.yml`, the shield will reflect the latest run.
+
---
## Debug builds
@@ -250,21 +313,26 @@ build_flags =
Uncomment to enable per-module Serial output. Defaults to silent for production builds.
-For symbolised crash backtraces (instead of bare hex addresses), also uncomment:
+Runtime debug is also available without rebuilding:
-```ini
-monitor_filters = esp32_exception_decoder
+```bash
+echo "touch_debug:on" > /dev/ttyUSB0 # state-transition logging on touch
+echo "touch_dots:on" > /dev/ttyUSB0 # visual touch dots on screen
```
+Symbolised crash backtraces (instead of bare hex addresses) are enabled by default via `monitor_filters = esp32_exception_decoder` in `platformio.ini`. Comment it out if you want raw addresses.
+
---
## Versioning
-Tags follow [SemVer](https://semver.org). The pre-build script `scripts/version.py` runs `git describe` and writes the result to `include/version.h`, so every binary reports its provenance over the `version` UART command:
+Tags follow [SemVer](https://semver.org). The pre-build script `scripts/version.py` runs `git describe` and writes the result to `include/version.h`, so every binary reports its provenance over the `version` UART command and on the in-menu stats overlay:
+
+- Tagged commit: `Firmware: v0.3.0`
+- Untagged commit: `Firmware: v0.3.0-3-gabc123`
+- Uncommitted changes: `Firmware: v0.3.0-3-gabc123-dirty`
-- Tagged commit: `Firmware: v0.1.0`
-- Untagged commit: `Firmware: v0.1.0-3-gabc123`
-- Uncommitted changes: `Firmware: v0.1.0-3-gabc123-dirty`
+The on-screen stats overlay shortens the version to the tag + commits-since portion and appends a `+` if the working tree was dirty at build.
---
@@ -279,4 +347,5 @@ Apache 2.0 — see [LICENSE](LICENSE).
- **Cheap Yellow Display** community, especially [witnessmenow](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) for the platform reference
- **[Tanmay Wankar](https://github.com/tanmaywankar)** for Grobot_Animations
- **[Bodmer](https://github.com/Bodmer)** for TFT_eSPI
-- **[hexeguitar](https://github.com/hexeguitar)** for the CYD touchscreen fork and the [LDR hardware mod reference](https://github.com/hexeguitar/ESP32_TFT_PIO#ldr)
\ No newline at end of file
+- **[hexeguitar](https://github.com/hexeguitar)** for the CYD touchscreen fork and the [LDR hardware mod reference](https://github.com/hexeguitar/ESP32_TFT_PIO#ldr)
+- **[ImpulseAdventure](https://github.com/ImpulseAdventure)** for GUIslice and the GUIslice Builder layout tool
\ No newline at end of file
diff --git a/docs/chat_note.md b/docs/chat_note.md
deleted file mode 100644
index bb32148..0000000
--- a/docs/chat_note.md
+++ /dev/null
@@ -1,127 +0,0 @@
-=== CYD firmware — context for new chat ===
-
-Repo: https://github.com/RobotStudyCompanion/CYD
-Branches:
- main branch-protected, integration target
- dev/mbz my branch (current work)
- dev/gio student's parallel Grobot test
-Licence: Apache 2.0
-
-=== Hardware ===
-ESP32-2432S028R (CYD original variant)
-- ESP32 WROOM, no PSRAM, ~302 KB heap free at boot, 107 KB largest block
-- ILI9341 320x240, SPI on (DC=2, CS=15, SCK=14, MOSI=13, MISO=12), BL=21
-- XPT2046 resistive touch (independent bus, bitbang)
-- RGB LED on GPIO 4/16/17 (active low)
-- LDR: investigated, none of GPIO 32/33/34/35/36/39 show light-responsive
- behaviour. Floating-input signatures on most pins suggest this variant
- doesn't have an LDR connected to any standard ADC1 pin. Either a clone
- without LDR, or wired non-standardly. Hardware investigation needed
- (visual / multimeter) before further software trial. External LDR on a
- free GPIO is a fallback if light sensing genuinely needed.
-
-=== Software stack ===
-- platform = espressif32 6.4.0 (Arduino-ESP32 2.0.11)
-- bodmer/TFT_eSPI 2.5.43 (ILI9341_2_DRIVER, 55 MHz SPI)
-- tanmaywankar/Grobot_Animations 0.1.3, 320x140 sprite (~90 KB at RGB565)
-- hexeguitar/CYD28_Touchscreen (bitbang fork, avoids HSPI collision)
-- bitbank2/JPEGDEC (kept for future serial-JPEG, currently unused)
-- Built-in Preferences for NVS persistence
-- Git-describe versioning via PlatformIO extra_script
-
-=== Source layout ===
-include/
- Config.h shared config struct + initConfig/serviceConfig
- DebugOverlay.h touch dots + memory/uptime/ldr/version helpers
- DisplayInit.h TFT_eSPI init + setBacklight (PWM on GPIO 21)
- FaceRenderer.h Grobot wrapper, setEyeColour/setBackgroundColour/setMood
- LED_Solution.h setLedColor(r,g,b) for RGB LED
- MjpegClass.h JPEG decoder wrapper (unused, kept for future)
- TouchHandler.h initTouch + getTouchPoint (raw)
- version.h AUTO-GENERATED, gitignored
-src/
- Config.cpp dispatch-table command parser, NVS load/save
- DebugOverlay.cpp dot rendering + edge detection + diag prints
- DisplayInit.cpp display init + backlight PWM (LEDC channel 0)
- FaceRenderer.cpp Grobot wrapper, splash, mood/colour application
- LED_Solution.cpp
- TouchHandler.cpp
- main.cpp pure orchestration (~30 lines)
-scripts/
- version.py git describe -> include/version.h pre-build hook
-
-=== Architecture ===
-- Config struct is single source of truth for stateful settings
- (touchDebug, moodAutoCycle, showTouchDots, eyeColour, bgColour, mood, brightness)
-- Config command parser uses dispatch table (Command struct with set/get
- function pointers); auto-generates help and status output from the table
-- FaceRenderer reads from config; setters update config and rebuild eyes/sprite
-- DebugOverlay owns all visual debug + serial diagnostic helpers
-- DisplayInit owns BL pin via LEDC (channel 0, 5 kHz, 8-bit PWM)
-- All persistent settings stored in NVS namespace "rsc", saved on every change
-- main.cpp = pure orchestration: printVersion, printMemoryReport, init*, service*
-
-=== UART command grammar ===
-- Setter form: key:value e.g. mood:HAPPY, eye_colour:00FF00, bright:128
-- Getter form: key? e.g. mood?, bright?
-- Plain form: key acts as getter for settable, action for plain
-- 22 commands total. Type 'help' on serial monitor for the list.
-- All settable values persisted to NVS automatically on change.
-
-=== Key build flags (platformio.ini) ===
-USER_SETUP_LOADED=1, ILI9341_2_DRIVER=1
-TFT_MISO=12 TFT_MOSI=13 TFT_SCLK=14 TFT_CS=15 TFT_DC=2 TFT_RST=-1 TFT_BL=21
-TFT_BACKLIGHT_ON=HIGH, SPI_FREQUENCY=55000000
-LOAD_GLCD=1 LOAD_FONT2=1 LOAD_FONT4=1 LOAD_GFXFF=1 SMOOTH_FONT=1
-extra_scripts = pre:scripts/version.py
-monitor_echo = yes
-
-=== Done across the whole journey ===
-- Track 1 quick fixes (HSPI swap, halt removal, debounce, debug gating)
-- Migration Arduino_GFX -> TFT_eSPI + Grobot_Animations
-- Sprite resize 120 -> 140 px tall (crisper eyes within memory budget)
-- Renames: AnimationPlayer -> DisplayInit, TouchOverlay -> DebugOverlay
-- DebugOverlay extracted: dots, edge detection, mem/uptime/ldr/version helpers
-- Config layer: NVS-persisted struct + non-blocking serial parser
-- Dispatch-table refactor: 22 commands, auto-generated help/status
-- Backlight PWM, LED control, tap injection, splash/clear actions
-- Version system: git-describe -> version.h, monitor echo enabled
-
-=== Open / next ===
-- LDR: probably absent on this variant; the 'ldr' command currently always
- returns 0 (or noise). Either remove the command or hook to an external
- sensor on a free GPIO. Defer until light-sensing actually needed.- PR dev/mbz to main; tag v0.1.0 (version string auto-upgrades from hash to semver)
-- Coordinate with Gio on Grobot before merging anything that touches FaceRenderer
-- Track 3: UIManager (modes FACE/MENU/TEXT), long-press menu trigger,
- UART 'text:...' command for MODE_TEXT, PNG via bitbank2/PNGdec,
- larger fonts via Adafruit GFX free fonts, mode-aware loop()
-- Future: light-driven auto-brightness once LDR pin is sorted
-- Future: status/loading screens (sprite + static background pattern from whiteboard)
-- Future: serial-JPEG path (re-activate MjpegClass for 'image:' command)
-- Minor: showSplash() blocks 800ms; non-blocking version wanted for Track 3
-- Minor: 'clear' command is partial in eye band (sprite repaints over)
-- Expose Grobot's fine-grained controls over UART:
- face:topH,botH,tilt,pR,r custom mood (symmetric)
- face_l:... / face_r:... asymmetric per-eye
- look:x,y canvas offset (lookAt)
- blink manual blink trigger
- hud:on|off FPS overlay toggle
- All map to existing Grobot public API; ~50-60 lines via dispatch table.
- Unblocks host-side tooling (e.g. live-control extension).
-
-=== Tooling / host-side ===
-- VS Code / Codium extension idea: live remote control of CYD over serial,
- panel UI sends UART commands, reads back via getters. Lives in separate repo.
- Bottleneck is firmware API surface, addressed by face-control commands above.
-- Tanmay's GrobotAnimator (pre-alpha) at tanmaywankar.github.io/GrobotAnimator
- — could contribute back to it as a richer offline simulator, separate effort
-- Future fork of Grobot to expose eyeGap/blinkTime/spring constants as overridable
- via #ifndef guards; submit as PR upstream rather than maintaining a private fork
-- Future RPi/laptop counterpart controller: speaks the UART grammar, drives the
- CYD in production. Existing 22 commands + face controls cover the surface.
-
-=== User preferences (carry forward) ===
-British English, active voice. Brief diff points (file + location + change),
-not full file rewrites where avoidable. Ask before heavy refactor / token-
-burny work. Working within hardware limitations — no PSRAM expansion unless
-forced. Like the rhythm of one fix at a time with checkpoints.
\ No newline at end of file
diff --git a/docs/notes.md b/docs/notes.md
index 513e425..89268d6 100644
--- a/docs/notes.md
+++ b/docs/notes.md
@@ -1,128 +1,47 @@
-# RSC CYD firmware — Track 1 handover
+## continuing dev for RSC CYD firmware, v0.3.0 in progress, branch dev/mbz
-End-of-session notes for the next chat. Branch: `dev/mbz`.
+* GUISlice based Builder-side UI design well underway.
+ * E_PG_MAIN laid out: burger (top-right), help (?), RSC logo (centre-left), two sliders (volume + brightness) with toggle icons below (audio mute, auto-bright A-in-sun), plus power (top-left), back-arrow (right middle), mic (bottom-right).
+* Icon pipeline partly automated (lives at ~/Documents/RSC/CYDisplays_resources/image2c-linux-x64-3.02/resources/):
+ * icon_prep.sh — bash + ImageMagick, PNG → resize → auto-invert → threshold → bilevel → transparent. Flags: -s WxH, -i auto|on|off, -c (emit C array).
+ * pbm_to_gslc_c.py — Python helper, byte-identical to Image2C output except for one cosmetic last-line trailing-whitespace pad. Functionally identical.
+ * All ~12 icons in 1bpp monochrome, white-on-transparent.
+* Design discipline locked to black-and-white for future e-ink compatibility + flash savings.
-## What we set out to do
+### Open queue, priority-ordered:
-Track 1 quick fixes against student's `dev/gio` branch. Original list had ten items focused on robustness (HSPI collision, missing-folder halts, dead debounce, etc.) of the existing MJPEG-from-SD animation pipeline.
+1. Create four popup pages in Builder — E_PG_POPUP_MENU (from burger), E_PG_POPUP_HELP (?), E_PG_POPUP_STATUS (RSC logo), E_PG_POPUP_PWR (power confirm "[Yes]/[Cancel]").
+2. Wire main-page triggers via each button's Popup Page Enum property.
+3. Convert three icons to Togglable Image Buttons — mic (mic_on ↔ mute_on), audio (volume_loud/volume_low (whether slider above/below 50% maybe?) ↔ volume_mute (backend send to alsamixer via daemon service to host - mutes RSC; no daemon/host side app yet)), auto-bright (with/without A (invokes onboard LDR to auto adjust brightness, works - needs plumbing after CYD UI deployed...)). Set Toggle?=true. Manually update Image Select Extern (Builder doesn't propagate?)... new to this...
+4. Clarify the back-arrow's role on main page: exits menu to face-mode (Grobot animated faces, MIT); retains position for popup submenus as a 'back'.
-## What we actually did
+(DONE, test pending once face built) Phase B firmware-side integration — main.cpp refactor for FACE/MENU mode switching, GUIslice init wiring, touch routing branch, Start/Face overlay via direct TFT_eSPI (not GUIslice). Builder output gets pasted into project via four surgical edits already documented.
-The scope shifted mid-session. SD card was lost and the user decided not to architect around SD/MJPEG playback — declared obsolete. The MJPEG pipeline came out wholesale; an orbiting-dot placeholder went in instead. Track 1 effectively reduced to: HSPI fix, display rotation, UART timeout, plus a structural cleanup of the animation player.
+(PENDING, after phase B validated...) Phase C — dispatch table wiring in button callbacks via findCommand(). Toggles read state via gslc_ElemXTogglebtnGetState().
-Decisions reached during the session:
+### Filed for later:
-- **Touch driver swapped** to `hexeguitar/CYD28_Touchscreen` (MIT) for the bitbang HSPI fix. We considered `TheNitek/XPT2046_Bitbang_Arduino_Library` first but rejected on licence grounds — GPL-3.0 conflicts with this repo's Apache 2.0.
-- **MJPEG/SD pipeline deleted**, not commented. `AnimationPlayer.cpp` slimmed down to a thin display init wrapper.
-- **Sprite-driven face is the architectural direction.** The original UART command set (`caring`/`surprised`/`pride` → SD folder switch) is dead. Future protocol will drive procedural face rendering with parameters, eventually steered by a small LM.
-- **Stay on Arduino_GFX for now**, switch to TFT_eSPI in the next chat. Reasoning: Grobot, RandomNerd, every CYD tutorial, and most face-rendering prior art uses TFT_eSPI; switching aligns us with the ecosystem before we build anything real on top.
-- **PSRAM mod is a "nice to have"**, not a blocker. Stock CYD has no PSRAM (confirmed by runtime check — see Diagnostics below).
-- **Module swap (ESP32 → ESP32-S3) considered and rejected.** S3 modules share rough footprint with WROOM-32 but differ in GPIO assignments, strapping pins, USB pin positions, and U4 PSRAM routing. A swap amounts to a partial board redesign, not a drop-in mod. For future S3 capability, source CYD variants that ship with S3 already on board (Sunton makes these). Existing 5 CYDs stay on classic ESP32 + U4 PSRAM mod.
-- **Flicker on the placeholder dot is acceptable.** Burning canvas-buffering work into Arduino_GFX is throwaway code given the upcoming library swap.
+* Power-off semantic for USB-powered CYD (deep-sleep + wake-on-touch, or just blank display). ...actually the power popup menu i want it to show host ctrl (RPi) e.g. reboot/shutdown; and cyd ctrl, e.g. reset (loads defaults, wipes NVS)/reboot (reloads as is)
+* Status popup dynamic redraw — per-element gslc_ElemSetRedraw for live fields (uptime, mem). Uses the format() callback pattern from v0.2.1. ...thought is that user taps the big RSC icon in base menu and that pops up a bunch of stats, e.g. fw version, uptime, then host side stuff like loaded model, or some usage facts...
+* Submenu nav within popups if needed later; for now single-level popups only.
+* Host-side Codium extension (face simulator + menu authoring + serial bridge).
+* TEXT mode design pass.
+* Grobot mouth (fork vs rewrite, defer).
+* we dont have a BMS or much PMIC stuff onboard but later might add e.g. voltages per rail in status maybe... maybe battery % (progress bar)
-## File state at end of session
+### Stuff to remember:
-```
-src/
-├── AnimationPlayer.cpp — slim display init, calls FaceRenderer
-├── FaceRenderer.cpp — splash + orbiting dot (placeholder)
-├── FaceRenderer.h
-├── TouchHandler.cpp — bitbang touch via CYD28_TouchscreenR
-├── TouchHandler.h
-├── LED_Solution.cpp — unchanged
-├── LED_Solution.h
-└── main.cpp — splash → idle dot → touch + serial concurrent
-archive/
-└── AnimationPlayer.cpp — original MJPEG/SD code, kept for reference
-```
+Dispatch table is the spine. Every UI surface, UART command, menu tap routes through findCommand(). The durable contract. Communicates with host service (pending separately) which would reside on the RPi in the RSC... the CYD talks to RPi via Serial and the dispatch table (config.cpp) is the API we can work with, extend, adjust etc.
+UART grammar key:value, key?, plain key is the wire contract.
+Wired-only project (USB to RPi/laptop). No WiFi/BT/OTA.
+CYD: ESP32-2432S028R, 320×240 ILI9341 SPI 55 MHz, XPT2046 resistive touch bitbanged, ~300 KB heap, 4 MB flash.
+platformio.ini: TFT_eSPI pinned 2.5.43. GUIslice via lib_deps. -DGSLC_FEATURE_COMPOUND=1 required for keypads. -DTOUCH_DEBUG commented for clean builds.
+Audio routes through host running alsamixer over serial; CYD is the control surface, not the audio path.
+Touch targets ≥40 px, 48 px ideal (i put icons as 40x40px, maybe ok). Sliders preferred over keypads for continuous values.
+Builder generates .ino + _GSLC.h — paste-into-project workflow, not platformio template.
+No Base Page needed (single-pane with overlays). (technically this is phase B and mostly done, pending tests...)
+Every popup must have a dismiss path (Close button or similar). (that's the same back icon thingy we have on base menu - i want to keep that exact same location for each menu)
-`platformio.ini` adds `build_flags = -DBOARD_HAS_PSRAM`. Harmless on stock board (PSRAM detection just returns false), required for when the mod lands.
+### Preferences carried forward:
-## Diagnostics from final flash
-
-```
-PSRAM found: no
-Heap free: 302 KB
-Largest heap block: 107 KB
-```
-
-107 KB largest contiguous block bounds any future canvas to roughly 230×230 RGB565 (~106 KB) or smaller. A 200×120 face-region canvas (48 KB) sits comfortably within budget without PSRAM.
-
-## Track 1 line-by-line status
-
-| # | Original fix | Status |
-|---|---|---|
-| 1 | HSPI bus collision — bitbang touch | **Done** |
-| 2 | Missing-folder halt | **Moot** — SD code removed |
-| 3 | `used_to_be_setup` halts | **Moot** — function rewritten |
-| 4 | Dead debounce | **Moot** — playback function removed |
-| 5 | Rename `used_to_be_setup` → `initAnimationPlayer` | **Done** as part of rewrite |
-| 6 | Dead `static File mjpegFile` | **Moot** — file removed |
-| 7 | `mjpegFileSizes[]` decision | **Moot** — file removed |
-| 8 | `gfx->setRotation(1)` | **Done** |
-| 9 | Wrap touch debug prints in `#ifdef TOUCH_DEBUG` | **Pending** — kept live for smoke testing |
-| 10 | `Serial.setTimeout(50)` | **Done** |
-
-## What we know works
-
-Display init, splash render, touch reads, RGB LED writes, and serial I/O all run concurrently without HSPI contention. The original symptom (touch reads corrupting display SPI) is fixed and verified.
-
-## What's queued for the next chat
-
-Next chat (call it Track 1.5 — library swap):
-
-1. Switch `Arduino_GFX_Library` → `TFT_eSPI`. Lift the known-good `User_Setup.h` from RandomNerd's CYD tutorial; verify display + touch + LED still work concurrently.
-2. Resolve fix #9 (debug-print gating) once we know what the new touch-coord pipeline looks like.
-3. Drop the orbiting dot, replace with a minimal `TFT_eSprite`-backed face primitive (two procedural eyes, no mouth yet). Steal architecture from `tanmaywankar/Grobot_Animations`, don't depend on it directly — Grobot's emotion-string API is too coarse for the LM-tuning vision.
-4. Add the serial `tune ` command for live parameter editing.
-
-Track 2 (non-blocking animation refactor from the original brief): now moot — there's no MJPEG pipeline left to refactor.
-
-Track 3 begins after Track 1.5: `UIManager` with mode enum, `Rect::contains()` hotspot helper, long-press detection, mode-aware `loop()` switch, UART `text:...` command for transient text mode. Stays roughly as originally scoped.
-
-## Hardware to procure
-
-For the 5-unit RSC prototype run, BoM for the U4 PSRAM mod per hexeguitar's [CYD mod doc](https://github.com/hexeguitar/ESP32_TFT_PIO):
-
-| Qty | Part | Digi-Key P/N | Notes |
-|-----|------|--------------|-------|
-| 5 | Adafruit 4677 PSRAM (rebranded ESP-PSRAM64H) | `1528-4677-ND` | 8 Mbit chip, ESP32 maps 4 MB usable. SOP-8, hand-solderable. |
-| 5 | 1206 red LED, ~620 nm, Vf ~2V | search MFR P/N `LTST-C150KRKT` (Lite-On) or equivalent | Replaces red channel of removed RGB LED, drives off GPIO4 via existing R17. |
-
-Total approx. €10–12 + VAT for both items × 5. Bundles into the existing Digi-Key cart cleanly.
-
-### Why this part choice
-
-- **Adafruit 4677 over the AP Memory branded `APS6404L-3SQR-SN`:** identical silicon, identical pinout (verified across both datasheets), MIT-rebranded by Adafruit. Digi-Key stocks the Adafruit version; AP Memory direct goes through Mouser. Single-supplier sourcing wins.
-- **4 MB usable over 2 MB (`APS1604M-3SQR-SN`):** the 2 MB part isn't stocked at Digi-Key in any recognisable form. The 4 MB part is the same price and the same effort, so capacity is essentially free. ESP32's classic memory controller caps PSRAM mapping at 4 MB regardless of chip size, so the 8 Mbit chip's "wasted" 4 MB is moot.
-- **`-3SQR-SN` suffix is mandatory.** The `-1SQR` variant is 1.8V and won't work in the CYD. The USON-8 (`-ZR` suffix) is a different package.
-
-### Mod sequence (per hexeguitar)
-
-1. Cut CS trace and SCK trace at marked points on the back of the board.
-2. Solder PSRAM chip onto U4 footprint.
-3. Remove existing RGB LED (sacrifices green and blue channels — RSC doesn't need them).
-4. Solder 1206 red LED across middle pads of the removed RGB LED footprint (now driven by GPIO4 via existing R17).
-5. Add fine enamel wire (magnet wire, ~30 AWG) jumpers from PSRAM CS and SCK pads to the freed-up RGB LED pads as shown in the mod photo. Existing R16/R20 (1kΩ) become the pull-ups for SCK/CS — no new resistors needed.
-
-### Caveats
-
-- PSRAM uses GPIO16 and GPIO17 — the green and blue RGB LED channels. Mod sacrifices those LED colours. RSC accepts red-only LED.
-- Hand-solderable with iron + flux (1.27 mm pitch gull-wing leads, no hot-air required).
-- Magnet wire isn't a Digi-Key item — usually scavenged from transformer salvage, or order separately as "magnet wire 30 AWG" from any electronics supplier.
-
-## Reference material worth keeping
-
-- Grobot Animator (live web tuner for parameter design): https://tanmaywankar.github.io/GrobotAnimator/
-- Grobot library source: https://github.com/tanmaywankar/Grobot_Animations
-- Hexeguitar CYD reference project + mod docs: https://github.com/hexeguitar/ESP32_TFT_PIO
-- ThomasHelman CYD face (band-compositor flicker trick): https://github.com/ThomasHelman123/Animated-Robot-Face-on-ESP32-CYD-with-Floating-Heart-Eye-Animations
-- Random Nerd CYD page (User_Setup.h for TFT_eSPI swap): https://randomnerdtutorials.com/cheap-yellow-display-esp32-2432s028r/
-- CYD community hub: https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display
-
-## Open questions for next session
-
-- Once on TFT_eSPI, is the flicker actually solved by `TFT_eSprite` alone, or does the band-compositor pattern from ThomasHelman matter?
-- Serial protocol shape — `tune eye_radius 12` (text) vs binary frames? Text is friendlier for the LM and for hand-debugging; binary scales better. Pick one before writing the parser.
-- `FaceState` schema — what parameters does the LM actually need to control? Lift Grobot's list (eye height, slope, radius, offset, pupil position) as v0, extend later.
-- Sprite module question (raised but not resolved): if face primitives are procedural, the only sprites we need are decorative accents (logos, status icons). Probably not worth a dedicated module yet.
\ No newline at end of file
+British English, active voice. Brief diff points (file + location + change), not full file rewrites. Ask before token-burny work. Save tokens by default. One-fix-at-a-time rhythm with checkpoints. Omit time/effort estimates entirely. Don't invent API specifics — verify from docs or examples. Don't pre-conclude on architectural decisions; surface options with trade-offs (prefer tabular representations over long prose).
\ No newline at end of file
diff --git a/include/Config.h b/include/Config.h
index c079dfc..f63db76 100644
--- a/include/Config.h
+++ b/include/Config.h
@@ -1,19 +1,24 @@
#pragma once
#include
+enum ThemeMode : uint8_t { THEME_DARK = 0, THEME_LIGHT = 1 };
+
struct Config {
- bool touchDebug = false;
- bool moodAutoCycle = true;
- bool showTouchDots = true;
- uint16_t eyeColour = 0xFFFF; // RGB565 (TFT_WHITE)
- uint16_t bgColour = 0x0000; // RGB565 (TFT_BLACK)
- char mood[16] = "NEUTRAL"; // null-terminated, max 15 chars
- uint8_t brightness = 100; // 0-100 backlight % (converts to PWM 0=off, 255=full)
- bool hudOn = false;
- bool autoBright = false;
- uint8_t brightLight = 50; // target % in bright rooms
- uint8_t brightDark = 1; // target % in dark rooms
- bool idleAnim = false;
+ bool touchDebug = false;
+ bool moodAutoCycle = true;
+ bool showTouchDots = false;
+ uint16_t eyeColour = 0xFFFF; // RGB565 (TFT_WHITE)
+ uint16_t bgColour = 0x0000; // RGB565 (TFT_BLACK)
+ char mood[16] = "NEUTRAL"; // null-terminated, max 15 chars
+ uint8_t brightness = 100; // 0-100 backlight % (converts to PWM 0=off, 255=full)
+ bool hudOn = false;
+ bool autoBright = false;
+ uint8_t brightLight = 50; // target % in bright rooms
+ uint8_t brightDark = 1; // target % in dark rooms
+ bool idleAnim = false;
+ uint16_t menuTimeoutSec = 10; // auto-return to face; 0 = disabled
+ ThemeMode theme = THEME_DARK;
+ String (*format)(); // optional: short live-value string for menus, e.g. "75%"
};
extern Config config;
@@ -26,7 +31,9 @@ struct Command {
void (*set)(const String& val);
void (*get)();
const char* help;
+ String (*format)();
};
+
const Command* findCommand(const String& name);
void serviceNvs();
-void markDirty(); // for setters
+void markDirty(); // for setters
\ No newline at end of file
diff --git a/include/DisplayInit.h b/include/DisplayInit.h
index eade745..d28a871 100644
--- a/include/DisplayInit.h
+++ b/include/DisplayInit.h
@@ -5,5 +5,5 @@
void initDisplay();
void setBacklight(uint8_t duty); // 0-255, called by 'bright:' command
-
+void reattachBacklight(); // call after GUIslice re-inits TFT_eSPI
#endif
\ No newline at end of file
diff --git a/include/MenuApp.h b/include/MenuApp.h
new file mode 100644
index 0000000..a026417
--- /dev/null
+++ b/include/MenuApp.h
@@ -0,0 +1,16 @@
+#pragma once
+#include "GUIslice.h"
+
+// GUIslice gui state lives in MenuCallbacks.cpp (single TU defining test_GSLC.h's globals)
+extern gslc_tsGui m_gui;
+
+// Generated by Builder; defined in test_GSLC.h
+extern void InitGUIslice_gen();
+
+
+extern void initMenuIcons();
+// Page IDs — must match enum order in test_GSLC.h.
+// Update when redesigning menus in Builder.
+static const int16_t MENU_PG_ROOT = 0; // E_PG_MAIN
+
+extern void refreshStatsText();
\ No newline at end of file
diff --git a/include/MenuSchema.h b/include/MenuSchema.h
deleted file mode 100644
index 217ebf2..0000000
--- a/include/MenuSchema.h
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma once
-#include
-
-enum ActionKind : uint8_t { ACT_PUSH, ACT_INVOKE, ACT_BACK };
-
-struct MenuScreen;
-struct MenuItem {
- const char* label;
- ActionKind kind;
- const void* payload; // MenuScreen* or const char*
-};
-struct MenuScreen {
- const char* title;
- const MenuItem* items;
- uint8_t count;
-};
-
-extern const MenuScreen ROOT_MENU;
-extern const MenuScreen MOOD_MENU;
-extern const MenuScreen BRIGHTNESS_MENU;
-
-const MenuScreen* getActiveRoot();
-void resetRuntimeSchema();
-bool runtimeBegin();
-bool runtimeAddScreen(const char* title);
-bool runtimeAddItem(ActionKind kind, const char* label, const char* payload);
-bool runtimeEnd();
\ No newline at end of file
diff --git a/include/TouchHandler.h b/include/TouchHandler.h
index 4a41369..741bec7 100644
--- a/include/TouchHandler.h
+++ b/include/TouchHandler.h
@@ -2,6 +2,7 @@
#define TOUCHHANDLER_H
#include
+#include
void initTouch();
bool getTouchPoint(uint16_t &x, uint16_t &y, uint16_t &z);
@@ -14,4 +15,34 @@ struct TouchEvent {
bool pollTouchEvent(TouchEvent& out);
bool peekLastTouch(uint16_t& x, uint16_t& y, uint16_t& z);
+
+// GUIslice adapter — bridges this touch layer to GUIslice's plugin interface.
+// Instantiate in main.cpp, register via gslc_InitTouchHandler().
+class CydTouchHandler : public TouchHandler {
+public:
+ void begin() override {} // no-op; hardware init done in initTouch()
+
+ THPoint getPoint() override {
+ uint16_t x, y, z;
+ bool touched = peekLastTouch(x, y, z);
+ if (touched) {
+ if (!inTouch_) { inTouch_ = true; x_ = x; y_ = y; }
+ else { x_ = (x_ * 7 + x) / 8; y_ = (y_ * 7 + y) / 8; }
+ releaseHoldoff_ = 3;
+ return THPoint(x_, y_, z ? z : 100);
+ }
+ if (releaseHoldoff_ > 0) {
+ releaseHoldoff_--;
+ return THPoint(x_, y_, 100);
+ }
+ inTouch_ = false;
+ return THPoint(0, 0, 0);
+ }
+
+private:
+ int16_t x_ = 0, y_ = 0;
+ bool inTouch_ = false;
+ uint8_t releaseHoldoff_ = 0;
+};
+
#endif
\ No newline at end of file
diff --git a/include/UIManager.h b/include/UIManager.h
index 623894f..a6a2e74 100644
--- a/include/UIManager.h
+++ b/include/UIManager.h
@@ -1,6 +1,5 @@
#pragma once
#include
-#include "MenuSchema.h"
enum UIMode : uint8_t { MODE_FACE, MODE_MENU };
@@ -10,7 +9,11 @@ void serviceUI();
UIMode getUIMode();
void setUIMode(UIMode m);
-void menuBack();
-bool menuSelect(uint8_t idx);
+// Kept for dispatch-table compatibility (Config.cpp's cmdMenu*)
+void menuBack(); // exits to FACE mode
+bool menuSelect(uint8_t idx); // deprecated, no-op
void printMode();
-void printMenuState();
\ No newline at end of file
+void printMenuState();
+
+void cmdMenuBack(); // calls menuBack() + prints OK
+void cmdMenuState(); // calls printMenuState()
\ No newline at end of file
diff --git a/include/test_GSLC.h b/include/test_GSLC.h
new file mode 100644
index 0000000..e6e888e
--- /dev/null
+++ b/include/test_GSLC.h
@@ -0,0 +1,405 @@
+//
+// FILE: [test_GSLC.h]
+// Created by GUIslice Builder version: [0.17.b41]
+//
+// GUIslice Builder Generated GUI Framework File
+//
+// For the latest guides, updates and support view:
+// https://github.com/ImpulseAdventure/GUIslice
+//
+//
+
+#ifndef _GUISLICE_GEN_H
+#define _GUISLICE_GEN_H
+
+// ------------------------------------------------
+// Headers to include
+// ------------------------------------------------
+#include "GUIslice.h"
+#include "GUIslice_drv.h"
+
+// Include any extended elements
+//
+// Include extended elements
+#include "elem/XSlider.h"
+#include "elem/XToggleImgbtn.h"
+#include "elem/XTogglebtn.h"
+//
+
+// ------------------------------------------------
+// Headers and Defines for fonts
+// Note that font files are located within the Adafruit-GFX library folder:
+// ------------------------------------------------
+//
+#if !defined(DRV_DISP_TFT_ESPI)
+ #error E_PROJECT_OPTIONS tab->Graphics Library should be Adafruit_GFX
+#endif
+#include
+//
+
+// ------------------------------------------------
+// Defines for resources
+// ------------------------------------------------
+//
+extern "C" const unsigned short auto_bright_40x40px[] PROGMEM;
+extern "C" const unsigned short back_40x40px[] PROGMEM;
+extern "C" const unsigned short burger_icon_40x40px[] PROGMEM;
+extern "C" const unsigned short mic_on_icon_40x40px[] PROGMEM;
+extern "C" const unsigned short power_icon_40x40px[] PROGMEM;
+extern "C" const unsigned short rsc_130x130[] PROGMEM;
+extern "C" const unsigned short volume_loud_icon2_40x40px[] PROGMEM;
+//
+
+// ------------------------------------------------
+// Enumerations for pages, elements, fonts, images
+// ------------------------------------------------
+//
+enum {E_PG_MAIN,E_PG_BURGER_MENU,E_PG_PWR,E_PG_POPUP_CONFIRM};
+enum {E_DRAW_LINE1,E_DRAW_LINE2,E_DRAW_LINE3,E_ELEM_BOX1
+ ,E_ELEM_BOX_CONFIRM,E_ELEM_BTN10,E_ELEM_BTN11,E_ELEM_BTN5
+ ,E_ELEM_BTN6,E_ELEM_BTN_CANCEL,E_ELEM_BTN_MENU_CLOSE
+ ,E_ELEM_BTN_YES,E_ELEM_IMAGEBTN5,E_ELEM_IMAGEBTN_BACK
+ ,E_ELEM_IMAGEBTN_BRIGHTNESS,E_ELEM_IMAGEBTN_BURGER
+ ,E_ELEM_IMAGEBTN_CONFIRM_BACK,E_ELEM_IMAGEBTN_MIC
+ ,E_ELEM_IMAGEBTN_PWR,E_ELEM_IMAGEBTN_PWR_CLOSE
+ ,E_ELEM_IMAGEBTN_VOLUME,E_ELEM_MENU_BOX,E_ELEM_PWR_BOX
+ ,E_ELEM_SLIDER2,E_ELEM_SLIDER3,E_ELEM_TEXT10,E_ELEM_TEXT9
+ ,E_ELEM_TEXT_CONFIRM,E_ELEM_TEXT_DEBUG_TOGGLE,E_ELEM_TEXT_STATS
+ ,E_ELEM_TEXT_THEME,E_ELEM_TOGGLE_DEBUG,E_ELEM_TOGGLE_THEME};
+// Must use separate enum for fonts with MAX_FONT at end to use gslc_FontSet.
+enum {E_BUILTIN10X16,E_BUILTIN15X24,MAX_FONT};
+//
+
+// ------------------------------------------------
+// Instantiate the GUI
+// ------------------------------------------------
+
+// ------------------------------------------------
+// Define the maximum number of elements and pages
+// ------------------------------------------------
+//
+#define MAX_PAGE 4
+
+#define MAX_ELEM_PG_MAIN 10 // # Elems total on page
+#define MAX_ELEM_PG_MAIN_RAM MAX_ELEM_PG_MAIN // # Elems in RAM
+
+#define MAX_ELEM_PG_BURGER_MENU 11 // # Elems total on page
+#define MAX_ELEM_PG_BURGER_MENU_RAM MAX_ELEM_PG_BURGER_MENU // # Elems in RAM
+
+#define MAX_ELEM_PG_PWR 9 // # Elems total on page
+#define MAX_ELEM_PG_PWR_RAM MAX_ELEM_PG_PWR // # Elems in RAM
+
+#define MAX_ELEM_PG_POPUP_CONFIRM 5 // # Elems total on page
+#define MAX_ELEM_PG_POPUP_CONFIRM_RAM MAX_ELEM_PG_POPUP_CONFIRM // # Elems in RAM
+//
+
+// ------------------------------------------------
+// Create element storage
+// ------------------------------------------------
+gslc_tsGui m_gui;
+gslc_tsDriver m_drv;
+gslc_tsFont m_asFont[MAX_FONT];
+gslc_tsPage m_asPage[MAX_PAGE];
+
+//
+gslc_tsElem m_asPage1Elem[MAX_ELEM_PG_MAIN_RAM];
+gslc_tsElemRef m_asPage1ElemRef[MAX_ELEM_PG_MAIN];
+gslc_tsElem m_asPopup5Elem[MAX_ELEM_PG_BURGER_MENU_RAM];
+gslc_tsElemRef m_asPopup5ElemRef[MAX_ELEM_PG_BURGER_MENU];
+gslc_tsElem m_asPopup6Elem[MAX_ELEM_PG_PWR_RAM];
+gslc_tsElemRef m_asPopup6ElemRef[MAX_ELEM_PG_PWR];
+gslc_tsElem m_asPopup7Elem[MAX_ELEM_PG_POPUP_CONFIRM_RAM];
+gslc_tsElemRef m_asPopup7ElemRef[MAX_ELEM_PG_POPUP_CONFIRM];
+gslc_tsXSlider m_sXSlider2;
+gslc_tsXToggleImgbtn m_sToggleImg32;
+gslc_tsXToggleImgbtn m_sToggleImg29;
+gslc_tsXSlider m_sXSlider3;
+gslc_tsXTogglebtn m_asXToggle10;
+gslc_tsXTogglebtn m_asXToggle7;
+
+#define MAX_STR 100
+
+//
+
+// ------------------------------------------------
+// Program Globals
+// ------------------------------------------------
+
+// Element References for direct access
+//
+extern gslc_tsElemRef* m_pElemOutTxt11;
+extern gslc_tsElemRef* m_pElemOutTxt8;
+extern gslc_tsElemRef* m_pElemSlider2;
+extern gslc_tsElemRef* m_pElemSlider2_3;
+extern gslc_tsElemRef* m_pElemToggle2_7;
+extern gslc_tsElemRef* m_pElemToggle2_7_10;
+extern gslc_tsElemRef* m_pElemToggleImg29;
+extern gslc_tsElemRef* m_pElemToggleImg32;
+//
+
+// Define debug message function
+static int16_t DebugOut(char ch);
+
+// ------------------------------------------------
+// Callback Methods
+// ------------------------------------------------
+bool CbBtnCommon(void* pvGui,void *pvElemRef,gslc_teTouch eTouch,int16_t nX,int16_t nY);
+bool CbCheckbox(void* pvGui, void* pvElemRef, int16_t nSelId, bool bState);
+bool CbDrawScanner(void* pvGui,void* pvElemRef,gslc_teRedrawType eRedraw);
+bool CbKeypad(void* pvGui, void *pvElemRef, int16_t nState, void* pvData);
+bool CbListbox(void* pvGui, void* pvElemRef, int16_t nSelId);
+bool CbSlidePos(void* pvGui,void* pvElemRef,int16_t nPos);
+bool CbSpinner(void* pvGui, void *pvElemRef, int16_t nState, void* pvData);
+bool CbTickScanner(void* pvGui,void* pvScope);
+
+// ------------------------------------------------
+// Create page elements
+// ------------------------------------------------
+void InitGUIslice_gen()
+{
+ gslc_tsElemRef* pElemRef = NULL;
+
+ if (!gslc_Init(&m_gui,&m_drv,m_asPage,MAX_PAGE,m_asFont,MAX_FONT)) { return; }
+
+ // ------------------------------------------------
+ // Load Fonts
+ // ------------------------------------------------
+//
+ if (!gslc_FontSet(&m_gui,E_BUILTIN10X16,GSLC_FONTREF_PTR,NULL,2)) { return; }
+ if (!gslc_FontSet(&m_gui,E_BUILTIN15X24,GSLC_FONTREF_PTR,NULL,3)) { return; }
+//
+
+//
+ gslc_PageAdd(&m_gui,E_PG_MAIN,m_asPage1Elem,MAX_ELEM_PG_MAIN_RAM,m_asPage1ElemRef,MAX_ELEM_PG_MAIN);
+ gslc_PageAdd(&m_gui,E_PG_BURGER_MENU,m_asPopup5Elem,MAX_ELEM_PG_BURGER_MENU_RAM,m_asPopup5ElemRef,MAX_ELEM_PG_BURGER_MENU);
+ gslc_PageAdd(&m_gui,E_PG_PWR,m_asPopup6Elem,MAX_ELEM_PG_PWR_RAM,m_asPopup6ElemRef,MAX_ELEM_PG_PWR);
+ gslc_PageAdd(&m_gui,E_PG_POPUP_CONFIRM,m_asPopup7Elem,MAX_ELEM_PG_POPUP_CONFIRM_RAM,m_asPopup7ElemRef,MAX_ELEM_PG_POPUP_CONFIRM);
+
+ // NOTE: The current page defaults to the first page added. Here we explicitly
+ // ensure that the main page is the correct page no matter the add order.
+ gslc_SetPageCur(&m_gui,E_PG_MAIN);
+
+ // Set Background to a flat color
+ gslc_SetBkgndColor(&m_gui,GSLC_COL_BLACK);
+
+ // -----------------------------------
+ // PAGE: E_PG_MAIN
+
+
+ // Create E_ELEM_MENU_BOX box
+ pElemRef = gslc_ElemCreateBox(&m_gui,E_ELEM_MENU_BOX,E_PG_MAIN,(gslc_tsRect){0,0,320,240});
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_BLACK,GSLC_COL_BLACK,GSLC_COL_BLACK);
+
+ // Create E_ELEM_IMAGEBTN_BACK button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_BACK,E_PG_MAIN,(gslc_tsRect){270,100,40,40},
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // Create slider E_ELEM_SLIDER2
+ pElemRef = gslc_ElemXSliderCreate(&m_gui,E_ELEM_SLIDER2,E_PG_MAIN,&m_sXSlider2,
+ (gslc_tsRect){210,10,40,180},0,100,0,10,true);
+ gslc_ElemXSliderSetStyle(&m_gui,pElemRef,true,GSLC_COL_WHITE,5,10,GSLC_COL_WHITE);
+ gslc_ElemXSliderSetPosFunc(&m_gui,pElemRef,&CbSlidePos);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ m_pElemSlider2 = pElemRef;
+
+ // Create E_ELEM_IMAGEBTN_VOLUME button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_VOLUME,E_PG_MAIN,(gslc_tsRect){210,190,40,40},
+ gslc_GetImageFromProg((const unsigned char*)volume_loud_icon2_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)volume_loud_icon2_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+ m_pElemToggleImg32 = pElemRef;
+
+ // Create E_ELEM_IMAGEBTN_MIC button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_MIC,E_PG_MAIN,(gslc_tsRect){270,190,40,40},
+ gslc_GetImageFromProg((const unsigned char*)mic_on_icon_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)mic_on_icon_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+ m_pElemToggleImg29 = pElemRef;
+
+ // Create slider E_ELEM_SLIDER3
+ pElemRef = gslc_ElemXSliderCreate(&m_gui,E_ELEM_SLIDER3,E_PG_MAIN,&m_sXSlider3,
+ (gslc_tsRect){150,10,40,180},0,100,0,10,true);
+ gslc_ElemXSliderSetStyle(&m_gui,pElemRef,true,GSLC_COL_WHITE,5,10,GSLC_COL_WHITE);
+ gslc_ElemXSliderSetPosFunc(&m_gui,pElemRef,&CbSlidePos);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ m_pElemSlider2_3 = pElemRef;
+
+ // Create E_ELEM_IMAGEBTN_BRIGHTNESS button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_BRIGHTNESS,E_PG_MAIN,(gslc_tsRect){150,190,40,40},
+ gslc_GetImageFromProg((const unsigned char*)auto_bright_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)auto_bright_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // Create E_ELEM_IMAGEBTN5 button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN5,E_PG_MAIN,(gslc_tsRect){10,70,130,130},
+ gslc_GetImageFromProg((const unsigned char*)rsc_130x130,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)rsc_130x130,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+ gslc_ElemSetFillEn(&m_gui,pElemRef,false);
+
+ // Create E_ELEM_IMAGEBTN_BURGER button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_BURGER,E_PG_MAIN,(gslc_tsRect){270,10,40,40},
+ gslc_GetImageFromProg((const unsigned char*)burger_icon_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)burger_icon_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // Create E_ELEM_IMAGEBTN_PWR button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_PWR,E_PG_MAIN,(gslc_tsRect){10,10,40,40},
+ gslc_GetImageFromProg((const unsigned char*)power_icon_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)power_icon_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // -----------------------------------
+ // PAGE: E_PG_BURGER_MENU
+
+
+ // Create E_ELEM_BOX1 box
+ pElemRef = gslc_ElemCreateBox(&m_gui,E_ELEM_BOX1,E_PG_BURGER_MENU,(gslc_tsRect){10,10,300,220});
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+
+ // Create E_ELEM_BTN_MENU_CLOSE button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_BTN_MENU_CLOSE,E_PG_BURGER_MENU,(gslc_tsRect){270,100,40,40},
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // Create toggle button E_ELEM_TOGGLE_THEME
+ pElemRef = gslc_ElemXTogglebtnCreate(&m_gui,E_ELEM_TOGGLE_THEME,E_PG_BURGER_MENU,&m_asXToggle10,
+ (gslc_tsRect){180,20,60,30},GSLC_COL_WHITE,GSLC_COL_WHITE,GSLC_COL_BLACK,
+ true,false,&CbBtnCommon);
+ m_pElemToggle2_7_10 = pElemRef;
+
+ // Create E_ELEM_TEXT_THEME text label
+ pElemRef = gslc_ElemCreateTxt(&m_gui,E_ELEM_TEXT_THEME,E_PG_BURGER_MENU,(gslc_tsRect){240,20,60,30},
+ (char*)"Theme",0,E_BUILTIN10X16);
+ gslc_ElemSetTxtCol(&m_gui,pElemRef,GSLC_COL_WHITE);
+
+ // Create toggle button E_ELEM_TOGGLE_DEBUG
+ pElemRef = gslc_ElemXTogglebtnCreate(&m_gui,E_ELEM_TOGGLE_DEBUG,E_PG_BURGER_MENU,&m_asXToggle7,
+ (gslc_tsRect){20,20,60,30},GSLC_COL_WHITE,GSLC_COL_WHITE,GSLC_COL_BLACK,
+ true,false,&CbBtnCommon);
+ m_pElemToggle2_7 = pElemRef;
+
+ // Create E_ELEM_TEXT_DEBUG_TOGGLE text label
+ pElemRef = gslc_ElemCreateTxt(&m_gui,E_ELEM_TEXT_DEBUG_TOGGLE,E_PG_BURGER_MENU,(gslc_tsRect){80,20,60,30},
+ (char*)"Debug",0,E_BUILTIN10X16);
+ gslc_ElemSetTxtCol(&m_gui,pElemRef,GSLC_COL_WHITE);
+
+ // Create E_ELEM_TEXT_STATS runtime modifiable text
+ static char m_sDisplayText8[121] = "Stats";
+ pElemRef = gslc_ElemCreateTxt(&m_gui,E_ELEM_TEXT_STATS,E_PG_BURGER_MENU,(gslc_tsRect){20,70,240,30},
+ (char*)m_sDisplayText8,121,E_BUILTIN10X16);
+ gslc_ElemSetFrameEn(&m_gui,pElemRef,true);
+ gslc_ElemSetTxtCol(&m_gui,pElemRef,GSLC_COL_WHITE);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ m_pElemOutTxt8 = pElemRef;
+
+ // Create E_DRAW_LINE1 line
+ pElemRef = gslc_ElemCreateLine(&m_gui,E_DRAW_LINE1,E_PG_BURGER_MENU,10,60,310,60);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_BLACK,GSLC_COL_WHITE,GSLC_COL_WHITE);
+
+ // Create E_DRAW_LINE2 line
+ pElemRef = gslc_ElemCreateLine(&m_gui,E_DRAW_LINE2,E_PG_BURGER_MENU,160,10,160,60);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_BLACK,GSLC_COL_WHITE,GSLC_COL_WHITE);
+
+ // -----------------------------------
+ // PAGE: E_PG_PWR
+
+
+ // Create E_ELEM_PWR_BOX box
+ pElemRef = gslc_ElemCreateBox(&m_gui,E_ELEM_PWR_BOX,E_PG_PWR,(gslc_tsRect){10,10,300,220});
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+
+ // Create E_ELEM_IMAGEBTN_PWR_CLOSE button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_PWR_CLOSE,E_PG_PWR,(gslc_tsRect){270,100,40,40},
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // create E_ELEM_BTN5 button with text label
+ pElemRef = gslc_ElemCreateBtnTxt(&m_gui,E_ELEM_BTN5,E_PG_PWR,
+ (gslc_tsRect){20,20,110,40},(char*)"Poweroff",0,E_BUILTIN10X16,&CbBtnCommon);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+
+ // create E_ELEM_BTN6 button with text label
+ pElemRef = gslc_ElemCreateBtnTxt(&m_gui,E_ELEM_BTN6,E_PG_PWR,
+ (gslc_tsRect){190,20,110,40},(char*)"Reboot",0,E_BUILTIN10X16,&CbBtnCommon);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+
+ // Create E_DRAW_LINE3 line
+ pElemRef = gslc_ElemCreateLine(&m_gui,E_DRAW_LINE3,E_PG_PWR,10,120,270,120);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_BLACK,GSLC_COL_GRAY_LT2,GSLC_COL_GRAY_LT2);
+
+ // create E_ELEM_BTN10 button with text label
+ pElemRef = gslc_ElemCreateBtnTxt(&m_gui,E_ELEM_BTN10,E_PG_PWR,
+ (gslc_tsRect){20,180,110,40},(char*)"Reset",0,E_BUILTIN10X16,&CbBtnCommon);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+
+ // create E_ELEM_BTN11 button with text label
+ pElemRef = gslc_ElemCreateBtnTxt(&m_gui,E_ELEM_BTN11,E_PG_PWR,
+ (gslc_tsRect){190,180,110,40},(char*)"Reboot",0,E_BUILTIN10X16,&CbBtnCommon);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+
+ // Create E_ELEM_TEXT9 text label
+ pElemRef = gslc_ElemCreateTxt(&m_gui,E_ELEM_TEXT9,E_PG_PWR,(gslc_tsRect){50,70,216,24},
+ (char*)"Raspberry Pi",0,E_BUILTIN15X24);
+ gslc_ElemSetTxtCol(&m_gui,pElemRef,GSLC_COL_WHITE);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+
+ // Create E_ELEM_TEXT10 text label
+ pElemRef = gslc_ElemCreateTxt(&m_gui,E_ELEM_TEXT10,E_PG_PWR,(gslc_tsRect){50,150,198,24},
+ (char*)"Front panel",0,E_BUILTIN15X24);
+ gslc_ElemSetTxtCol(&m_gui,pElemRef,GSLC_COL_WHITE);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+
+ // -----------------------------------
+ // PAGE: E_PG_POPUP_CONFIRM
+
+
+ // Create E_ELEM_BOX_CONFIRM box
+ pElemRef = gslc_ElemCreateBox(&m_gui,E_ELEM_BOX_CONFIRM,E_PG_POPUP_CONFIRM,(gslc_tsRect){10,50,300,130});
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+
+ // Create E_ELEM_IMAGEBTN_CONFIRM_BACK button with image label
+ pElemRef = gslc_ElemCreateBtnImg(&m_gui,E_ELEM_IMAGEBTN_CONFIRM_BACK,E_PG_POPUP_CONFIRM,(gslc_tsRect){270,100,40,40},
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ gslc_GetImageFromProg((const unsigned char*)back_40x40px,GSLC_IMGREF_FMT_RAW1),
+ &CbBtnCommon);
+
+ // Create E_ELEM_TEXT_CONFIRM runtime modifiable text
+ static char m_sDisplayText11[51] = "Message";
+ pElemRef = gslc_ElemCreateTxt(&m_gui,E_ELEM_TEXT_CONFIRM,E_PG_POPUP_CONFIRM,(gslc_tsRect){20,60,240,60},
+ (char*)m_sDisplayText11,51,E_BUILTIN10X16);
+ gslc_ElemSetTxtAlign(&m_gui,pElemRef,GSLC_ALIGN_MID_MID);
+ gslc_ElemSetTxtCol(&m_gui,pElemRef,GSLC_COL_WHITE);
+ m_pElemOutTxt11 = pElemRef;
+
+ // create E_ELEM_BTN_YES button with text label
+ pElemRef = gslc_ElemCreateBtnTxt(&m_gui,E_ELEM_BTN_YES,E_PG_POPUP_CONFIRM,
+ (gslc_tsRect){30,130,90,30},(char*)"Yes",0,E_BUILTIN10X16,&CbBtnCommon);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+ gslc_ElemSetFillEn(&m_gui,pElemRef,false);
+
+ // create E_ELEM_BTN_CANCEL button with text label
+ pElemRef = gslc_ElemCreateBtnTxt(&m_gui,E_ELEM_BTN_CANCEL,E_PG_POPUP_CONFIRM,
+ (gslc_tsRect){160,130,90,30},(char*)"Cancel",0,E_BUILTIN10X16,&CbBtnCommon);
+ gslc_ElemSetCol(&m_gui,pElemRef,GSLC_COL_WHITE,GSLC_COL_BLACK,GSLC_COL_BLACK);
+ gslc_ElemSetRoundEn(&m_gui, pElemRef, true);
+ gslc_ElemSetFillEn(&m_gui,pElemRef,false);
+//
+
+//
+ gslc_GuiRotate(&m_gui, 1);
+//
+
+}
+
+#endif // end _GUISLICE_GEN_H
diff --git a/platformio.ini b/platformio.ini
index de1cbd1..a3c67d0 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -1,11 +1,15 @@
[env:Esp32dev]
platform = espressif32
+; build_src_filter = +
board = esp32dev
framework = arduino
extra_scripts = pre:scripts/version.py
monitor_speed = 115200
monitor_echo = yes
-monitor_filters = esp32_exception_decoder
+monitor_filters =
+ -esp32_exception_decoder
+ -time
+lib_ldf_mode = deep
build_flags =
-DUSER_SETUP_LOADED=1
-DILI9341_2_DRIVER=1
@@ -29,9 +33,28 @@ build_flags =
-DSPI_TOUCH_FREQUENCY=2500000
; -DLDR_DEBUG
; -DTOUCH_DEBUG
+ ; --- GUIslice demo ---
+ -DUSER_CONFIG_LOADED=1
+ -DDRV_DISP_TFT_ESPI
+ -DDRV_TOUCH_HANDLER
+ -DDEBUG_ERR=1
+ -DGSLC_USE_PROGMEM=0
+ -DGSLC_USE_FLOAT=0
+ -DGSLC_LOCAL_STR=0
+ -DGSLC_LOCAL_STR_LEN=30
+ -DGSLC_TOUCH_MAX_EVT=1
+ -DGSLC_CLIP_EN=1
+ -DGSLC_BMP_TRANS_EN=1
+ -DGSLC_BMP_TRANS_RGB=0xFF,0x00,0xFF
+ -DGSLC_FEATURE_COMPOUND=1
+ -DGSLC_FEATURE_XTEXTBOX_EMBED=0
+ -DGSLC_FEATURE_INPUT=0
+ -DGSLC_SD_EN=0
+ -DGSLC_DEV_TOUCH=\"\"
+ -DGSLC_ROTATE=1
lib_deps =
- ; moononournation/GFX Library for Arduino@1.4.7
bodmer/TFT_eSPI
https://github.com/tanmaywankar/Grobot_Animations.git
- bitbank2/JPEGDEC
- https://github.com/hexeguitar/CYD28_Touchscreen.git
\ No newline at end of file
+ ; bitbank2/JPEGDEC
+ https://github.com/hexeguitar/CYD28_Touchscreen.git
+ https://github.com/ImpulseAdventure/GUIslice.git
\ No newline at end of file
diff --git a/src/Config.cpp b/src/Config.cpp
index 9495141..c4da0e0 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -8,7 +8,6 @@
#include
#include "LdrSensor.h"
#include "UIManager.h"
-#include "MenuSchema.h"
extern TFT_eSPI tft;
@@ -65,6 +64,8 @@ static void saveConfig() {
prefs.putUChar("brDark", config.brightDark);
prefs.putBool("hudOn", config.hudOn);
prefs.putBool("idleAnim", config.idleAnim);
+ prefs.putUShort("menuTo", config.menuTimeoutSec);
+ prefs.putUChar("theme", (uint8_t)config.theme);
prefs.end();
}
@@ -82,10 +83,12 @@ void initConfig() {
config.autoBright = prefs.getBool ("autoBright", config.autoBright);
config.brightLight = prefs.getUChar("brLight", config.brightLight);
config.brightDark = prefs.getUChar("brDark", config.brightDark);
- config.idleAnim = prefs.getBool("idleAnim", config.idleAnim);
- String savedMood = prefs.getString("mood", String(config.mood));
+ config.idleAnim = prefs.getBool("idleAnim", config.idleAnim);
+ String savedMood = prefs.getString("mood", String(config.mood));
+ config.menuTimeoutSec = prefs.getUShort("menuTo", config.menuTimeoutSec);
strncpy(config.mood, savedMood.c_str(), sizeof(config.mood) - 1);
config.mood[sizeof(config.mood) - 1] = '\0';
+ config.theme = (ThemeMode)prefs.getUChar("theme", (uint8_t)config.theme);
prefs.end();
}
@@ -157,6 +160,16 @@ static void cmdSetBgColour(const String &val) {
if (!parseColour(val, c)) { Serial.println("ERR: bad colour"); return; }
setBackgroundColour(c); markDirty(); Serial.println("OK");
}
+static void cmdSetTheme(const String& val) {
+ String v = val; v.toLowerCase();
+ uint16_t bg, eye;
+ if (v == "light") { bg = 0xFFFF; eye = 0x0000; config.theme = THEME_LIGHT; }
+ else if (v == "dark") { bg = 0x0000; eye = 0xFFFF; config.theme = THEME_DARK; }
+ else { Serial.println("ERR: light or dark"); return; }
+ config.bgColour = bg; setBackgroundColour(bg);
+ config.eyeColour = eye; setEyeColour(eye);
+ markDirty(); Serial.println("OK");
+}
static void cmdSetMood(const String &val) {
String upper = val; upper.toUpperCase();
if (!isKnownMood(upper)) { Serial.printf("ERR: unknown mood '%s'\n", val.c_str()); return; }
@@ -185,6 +198,17 @@ static void cmdSetBrightDark(const String &val) {
if (v < 0 || v > 100) { Serial.println("ERR: 0-100"); return; }
config.brightDark = (uint8_t)v; markDirty(); Serial.println("OK");
}
+static void cmdSetMenuTimeout(const String& val) {
+ int v = val.toInt();
+ if (v < 0 || v > 300) { Serial.println("ERR: 0-300"); return; }
+ config.menuTimeoutSec = (uint16_t)v; markDirty(); Serial.println("OK");
+}
+static void cmdGetMenuTimeout() {
+ Serial.printf("menu_timeout: %u\n", config.menuTimeoutSec);
+}
+static void cmdGetTheme() {
+ Serial.printf("theme: %s\n", config.theme == THEME_DARK ? "dark" : "light");
+}
static void cmdSetMode(const String& val) {
String u = val; u.toUpperCase();
if (u == "FACE") { setUIMode(MODE_FACE); Serial.println("OK"); }
@@ -267,40 +291,6 @@ static void cmdGetFaceR() {
}
static void cmdGetMode() { printMode(); }
-static void cmdMenuSelect(const String& val) {
- if (!menuSelect((uint8_t)val.toInt())) Serial.println("ERR: select failed");
-}
-static void cmdMenuBack() { menuBack(); Serial.println("OK"); }
-static void cmdMenuState() { printMenuState(); }
-
-static void cmdMenuBegin() {
- runtimeBegin();
- Serial.println("OK: schema load started");
-}
-static void cmdMenuScreen(const String& val) {
- if (runtimeAddScreen(val.c_str())) Serial.println("OK");
- else Serial.println("ERR: addScreen failed");
-}
-static void cmdMenuItem(const String& val) {
- int c1 = val.indexOf(',');
- if (c1 < 0) { Serial.println("ERR: need kind,label[,payload]"); return; }
- int c2 = val.indexOf(',', c1 + 1);
- String kindStr = val.substring(0, c1); kindStr.toLowerCase();
- String label = (c2 < 0) ? val.substring(c1 + 1) : val.substring(c1 + 1, c2);
- String payload = (c2 < 0) ? String() : val.substring(c2 + 1);
- ActionKind k;
- if (kindStr == "push") k = ACT_PUSH;
- else if (kindStr == "invoke") k = ACT_INVOKE;
- else if (kindStr == "back") k = ACT_BACK;
- else { Serial.println("ERR: kind must be push|invoke|back"); return; }
- if (runtimeAddItem(k, label.c_str(), payload.c_str())) Serial.println("OK");
- else Serial.println("ERR: addItem failed");
-}
-static void cmdMenuEnd() {
- if (runtimeEnd()) Serial.println("OK: schema active");
- else Serial.println("ERR: runtimeEnd failed (bad PUSH index?)");
-}
-
// ============ Getter handlers ============
static void cmdGetTouchDebug() { Serial.printf("touch_debug: %s\n", config.touchDebug ? "on" : "off"); }
static void cmdGetMoodCycle() { Serial.printf("mood_cycle: %s\n", config.moodAutoCycle ? "on" : "off"); }
@@ -313,6 +303,15 @@ static void cmdGetAutoBright() { Serial.printf("auto_bright: %s\n", config.au
static void cmdGetBrightLight(){ Serial.printf("bright_light: %u%%\n", config.brightLight); }
static void cmdGetBrightDark() { Serial.printf("bright_dark: %u%%\n", config.brightDark); }
static void cmdGetVersion() { Serial.printf("version: %s\n", FW_VERSION); }
+static String fmtBright() { return String(config.brightness) + "%"; }
+static String fmtMood() { return String(config.mood); }
+static String fmtUptime() {
+ uint32_t s = millis() / 1000;
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%um %us", s / 60, s % 60);
+ return String(buf);
+}
+static String fmtMem() { return String(ESP.getFreeHeap() / 1024) + "K"; }
// ============ Action handlers (no value, no get/set distinction) ============
static void cmdReset() {
@@ -357,27 +356,21 @@ static const Command commands[] = {
{"bg_colour", cmdSetBgColour, cmdGetBgColour, "RRGGBB hex"},
{"bg_color", cmdSetBgColour, cmdGetBgColour, nullptr}, // alias, hidden
{"idle_anim", cmdSetIdleAnim, cmdGetIdleAnim, "on|off random idle moods"},
- {"mood", cmdSetMood, cmdGetMood, "NEUTRAL|HAPPY|ANGRY|SAD|EXCITED|ANNOYED|QUESTIONING|IDLE1-3"},
- {"bright", cmdSetBright, cmdGetBright, "0-100 backlight %"},
+ {"theme", cmdSetTheme, cmdGetTheme, "light | dark (preset bg+eye; use bg_colour/eye_colour for arbitrary)"},
{"auto_bright", cmdSetAutoBright, cmdGetAutoBright, "on|off LDR-driven brightness"},
{"bright_light", cmdSetBrightLight, cmdGetBrightLight, "1-100 target % when bright"},
{"bright_dark", cmdSetBrightDark, cmdGetBrightDark, "1-100 target % when dark"},
+ {"menu_timeout", cmdSetMenuTimeout, cmdGetMenuTimeout, "0-300 seconds (0=disabled)"},
{"look", cmdSetLook, cmdGetLook, "x,y canvas offset"},
{"hud", cmdSetHud, cmdGetHud, "on|off Grobot HUD overlay"},
{"face", cmdSetFace, cmdGetFace, "topH,botH,tilt,pR,r — symmetric custom mood (floats)"},
{"face_l", cmdSetFaceL, cmdGetFaceL, "topH,botH,tilt,pR,r — left eye only"},
{"face_r", cmdSetFaceR, cmdGetFaceR, "topH,botH,tilt,pR,r — right eye only"},
{"mode", cmdSetMode, cmdGetMode, "FACE|MENU"},
- {"menu_begin", nullptr, cmdMenuBegin, "start runtime schema load"},
- {"menu_screen", cmdMenuScreen, nullptr, "title add screen"},
- {"menu_item", cmdMenuItem, nullptr, "kind,label,payload add item"},
- {"menu_end", nullptr, cmdMenuEnd, "finalise + activate runtime schema"},
// Set-only commands
{"led", cmdSetLed, nullptr, "off|on|red|green|blue|white|yellow|cyan|magenta or r,g,b"},
{"tap", cmdSetTap, nullptr, "x,y[,z] inject touch"},
- {"menu_select", cmdMenuSelect, nullptr, "n select item by index"},
-
// Plain actions (and queries with no setter)
{"menu_back", nullptr, cmdMenuBack, "pop one level"},
@@ -385,8 +378,6 @@ static const Command commands[] = {
{"help", nullptr, cmdHelp, "this message"},
{"status", nullptr, cmdStatus, "print current config"},
{"version", nullptr, cmdGetVersion, "firmware version"},
- {"mem", nullptr, cmdMem, "memory snapshot"},
- {"uptime", nullptr, cmdUptime, "time since boot"},
{"blink", nullptr, cmdBlink, "trigger one blink"},
{"ldr", nullptr, cmdLdr, "light sensor reading"},
{"light", nullptr, cmdGetLight, "ambient brightness 0-100%"},
@@ -397,6 +388,10 @@ static const Command commands[] = {
{"resume", nullptr, cmdResume, "resume face renderer"},
{"reboot", nullptr, cmdReboot, "soft reboot (config preserved)"},
{"reset", nullptr, cmdReset, "clear NVS, reboot to defaults"},
+ {"mood", cmdSetMood, cmdGetMood, "NEUTRAL|HAPPY|ANGRY|SAD|EXCITED|ANNOYED|QUESTIONING|IDLE1-3", fmtMood},
+ {"bright", cmdSetBright, cmdGetBright, "0-100 backlight %", fmtBright},
+ {"uptime", nullptr, cmdUptime, "time since boot", fmtUptime},
+ {"mem", nullptr, cmdMem, "memory snapshot", fmtMem},
};
static const size_t COMMAND_COUNT = sizeof(commands) / sizeof(commands[0]);
diff --git a/src/DisplayInit.cpp b/src/DisplayInit.cpp
index 09aacc6..b5bab00 100644
--- a/src/DisplayInit.cpp
+++ b/src/DisplayInit.cpp
@@ -31,4 +31,9 @@ void setBacklight(uint8_t pct) {
uint8_t duty = (pct * 255) / 100;
ledcWrite(BL_LEDC_CHANNEL, duty);
config.brightness = pct;
+}
+
+void reattachBacklight() {
+ ledcAttachPin(BL_PIN, BL_LEDC_CHANNEL);
+ setBacklight(config.brightness);
}
\ No newline at end of file
diff --git a/src/FaceRenderer.cpp b/src/FaceRenderer.cpp
index 98c49ca..33523aa 100644
--- a/src/FaceRenderer.cpp
+++ b/src/FaceRenderer.cpp
@@ -44,6 +44,7 @@ void initFaceRenderer(TFT_eSPI *tft)
void showSplash()
{
if (!_tft) return;
+ _tft->setRotation(3); // re-orientation
_tft->fillScreen(config.bgColour);
_tft->setTextColor(config.eyeColour);
_tft->setTextSize(3);
@@ -74,7 +75,7 @@ void serviceFaceRenderer()
} else {
_nextIdleSwitch = 0;
}
- if (_paused) return; // <-- new
+ if (_paused) return;
if (config.moodAutoCycle) _eyes->moodSwitch(true);
_eyes->renderEmotions(*_canvas);
if (_hudOn) _eyes->HUD(*_tft);
diff --git a/src/MenuCallbacks.cpp b/src/MenuCallbacks.cpp
new file mode 100644
index 0000000..3cf1d71
--- /dev/null
+++ b/src/MenuCallbacks.cpp
@@ -0,0 +1,290 @@
+#include "test_GSLC.h"
+#include "UIManager.h"
+#include "Config.h"
+#include "DisplayInit.h" // setBacklight
+#include "version.h"
+
+
+// ------------------------------------------------
+// Program Globals
+// ------------------------------------------------
+
+// Save some element references for direct access
+//
+gslc_tsElemRef* m_pElemOutTxt11 = NULL;
+gslc_tsElemRef* m_pElemOutTxt8 = NULL;
+gslc_tsElemRef* m_pElemSlider2 = NULL;
+gslc_tsElemRef* m_pElemSlider2_3 = NULL;
+gslc_tsElemRef* m_pElemToggle2_7 = NULL;
+gslc_tsElemRef* m_pElemToggle2_7_10= NULL;
+gslc_tsElemRef* m_pElemToggleImg29= NULL;
+gslc_tsElemRef* m_pElemToggleImg32= NULL;
+
+extern "C" const unsigned short volume_low_icon2_40x40px[] PROGMEM;
+extern "C" const unsigned short volume_mute_icon2_40x40px[] PROGMEM;
+extern "C" const unsigned short mute_on_icon_40x40px[] PROGMEM;
+extern "C" const unsigned short bright_40x40px[] PROGMEM;
+
+//
+
+// Confirm popup: pending command stashed by destructive buttons, executed by YES
+#define PENDING_CMD_MAX 24
+
+static char m_acPendingCmd[PENDING_CMD_MAX] = {0};
+static bool _muted = false;
+static bool _micMuted = false;
+static int16_t _volumeLevel = 50;
+static const void* _lastVolIcon = nullptr;
+
+// Cached refs — populated by initMenuIcons()
+static gslc_tsElemRef* _refVolume = nullptr;
+static gslc_tsElemRef* _refMic = nullptr;
+static gslc_tsElemRef* _refBright = nullptr;
+
+
+static gslc_tsElemRef* _refStatsUp = nullptr;
+static gslc_tsElemRef* _refStatsHeap = nullptr;
+static char _bufStatsUp[24] = "";
+static char _bufStatsHeap[24] = "";
+
+// Routes host_* commands to Serial; everything else to the local dispatch table
+static void executePending() {
+ if (strncmp(m_acPendingCmd, "host_", 5) == 0) {
+ Serial.println(m_acPendingCmd); // host service consumes via its serial listener
+ } else {
+ const Command* cmd = findCommand(String(m_acPendingCmd));
+ if (cmd && cmd->get) cmd->get();
+ else Serial.printf("ERR: no handler for '%s'\n", m_acPendingCmd);
+ }
+}
+
+
+static void applyIcon(gslc_tsElemRef* ref, const void* iconArr) {
+ if (!ref) return;
+ gslc_tsImgRef img = gslc_GetImageFromProg((const unsigned char*)iconArr, GSLC_IMGREF_FMT_RAW1);
+ gslc_ElemSetImage(&m_gui, ref, img, img);
+}
+
+static void refreshVolumeIcon() {
+ const void* iconArr;
+ if (_muted) iconArr = volume_mute_icon2_40x40px;
+ else if (_volumeLevel > 50) iconArr = volume_loud_icon2_40x40px;
+ else iconArr = volume_low_icon2_40x40px;
+ if (iconArr == _lastVolIcon) return;
+ _lastVolIcon = iconArr;
+ applyIcon(_refVolume, iconArr);
+}
+
+void initMenuIcons() {
+ _refVolume = gslc_PageFindElemById(&m_gui, E_PG_MAIN, E_ELEM_IMAGEBTN_VOLUME);
+ _refMic = gslc_PageFindElemById(&m_gui, E_PG_MAIN, E_ELEM_IMAGEBTN_MIC);
+ _refBright = gslc_PageFindElemById(&m_gui, E_PG_MAIN, E_ELEM_IMAGEBTN_BRIGHTNESS);
+ refreshVolumeIcon();
+ applyIcon(_refMic, mic_on_icon_40x40px);
+ applyIcon(_refBright, config.autoBright ? auto_bright_40x40px : bright_40x40px);
+ // Programmatically add two more stats lines on the burger menu page
+ _refStatsUp = gslc_ElemCreateTxt(&m_gui, GSLC_ID_AUTO, E_PG_BURGER_MENU,
+ (gslc_tsRect){20, 110, 240, 30}, _bufStatsUp, sizeof(_bufStatsUp), E_BUILTIN10X16);
+ gslc_ElemSetTxtCol(&m_gui, _refStatsUp, GSLC_COL_WHITE);
+ gslc_ElemSetCol(&m_gui, _refStatsUp, GSLC_COL_BLACK, GSLC_COL_BLACK, GSLC_COL_BLACK);
+
+ _refStatsHeap = gslc_ElemCreateTxt(&m_gui, GSLC_ID_AUTO, E_PG_BURGER_MENU,
+ (gslc_tsRect){20, 150, 240, 30}, _bufStatsHeap, sizeof(_bufStatsHeap), E_BUILTIN10X16);
+ gslc_ElemSetTxtCol(&m_gui, _refStatsHeap, GSLC_COL_WHITE);
+ gslc_ElemSetCol(&m_gui, _refStatsHeap, GSLC_COL_BLACK, GSLC_COL_BLACK, GSLC_COL_BLACK);
+}
+
+void refreshStatsText() {
+ if (!m_pElemOutTxt8 || !_refStatsUp || !_refStatsHeap) return;
+
+ // Shorten FW_VERSION: keep "v-", drop "-g-dirty"; mark dirty with "+"
+ char shortVer[20];
+ const char* ver = FW_VERSION;
+ const char* hashMark = strstr(ver, "-g");
+ int keepLen = hashMark ? (int)(hashMark - ver) : (int)strlen(ver);
+ if (keepLen > (int)sizeof(shortVer) - 2) keepLen = sizeof(shortVer) - 2;
+ memcpy(shortVer, ver, keepLen);
+ shortVer[keepLen] = '\0';
+ bool dirty = strstr(ver, "-dirty") != nullptr;
+ if (dirty && keepLen + 1 < (int)sizeof(shortVer)) {
+ shortVer[keepLen] = '+';
+ shortVer[keepLen + 1] = '\0';
+ }
+
+ static char buf[40];
+ uint32_t s = millis() / 1000;
+ uint32_t h = s / 3600;
+ uint32_t mn = (s % 3600) / 60;
+ uint32_t sc = s % 60;
+
+ snprintf(buf, sizeof(buf), "fw %s", shortVer);
+ gslc_ElemSetTxtStr(&m_gui, m_pElemOutTxt8, buf);
+
+ snprintf(buf, sizeof(buf), "up %02u:%02u:%02u", (unsigned)h, (unsigned)mn, (unsigned)sc);
+ gslc_ElemSetTxtStr(&m_gui, _refStatsUp, buf);
+
+ snprintf(buf, sizeof(buf), "heap %u", (unsigned)ESP.getFreeHeap());
+ gslc_ElemSetTxtStr(&m_gui, _refStatsHeap, buf);
+}
+
+// ------------------------------------------------
+// Callback Methods
+// ------------------------------------------------
+// Common Button callback
+bool CbBtnCommon(void* pvGui,void *pvElemRef,gslc_teTouch eTouch,int16_t nX,int16_t nY)
+{
+ // Typecast the parameters to match the GUI and element types
+ gslc_tsGui* pGui = (gslc_tsGui*)(pvGui);
+ gslc_tsElemRef* pElemRef = (gslc_tsElemRef*)(pvElemRef);
+ gslc_tsElem* pElem = gslc_GetElemFromRef(pGui,pElemRef);
+
+ if ( eTouch == GSLC_TOUCH_UP_IN ) {
+ // From the element's ID we can determine which button was pressed.
+ switch (pElem->nId) {
+//