diff --git a/README.md b/README.md index 708a0a9..8c7c7e1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,110 @@ +# GuppyScreen — ke-advanced-3d-bedmesh branch + +> **This is a custom branch** for the **Creality Ender-3 V3 KE**, adding an advanced interactive 3D bed mesh visualization. + +## Fork lineage + +``` +ballaswag/guppyscreen (original) + └── probielodan/guppyscreen (KE-focused fork, adds features below) + └── coreflake1/guppyscreen ← this repo + └── ke-advanced-3d-bedmesh ← this branch (adds 3D bed mesh) +``` + +### What probielodan added over ballaswag + +- **Major UI rework** — prompt panel, sensor/slider containers, file panel, homing panel, print panel, and more, all reworked for reliability and the KE's display +- **Better print status** — improved information display during a print +- **Auto-reload file list** — file list refreshes automatically +- **Absolute positioning display** — shows absolute axis coordinates +- **Invert Z direction** option +- **WiFi disconnect and forget-network buttons** +- **Chamber temperature** shown on the print screen +- **Default extruder temperatures** configurable +- **Android support dropped** (the KE runs on a dedicated MIPS/ARM SoC, not Android) + +### What this branch adds on top + +- 3D feature cherry-picked from [prestonbrown/guppyscreen](https://github.com/prestonbrown/guppyscreen) (commit `bced7f7`) + +## What's different on this branch + +- Real-time 3D rendered mesh surface with colour-gradient height map +- Interactive rotation, zoom (Z-scale and FOV), and pan via touch or mouse +- Toggle between 2D and 3D views with a single button +- Multitouch gesture support + +**Changed files vs base:** +- `src/bedmesh_panel.cpp` — full 3D rendering engine +- `src/bedmesh_panel.h` — new structs (`Point3D`, `Vertex3D`, `Quad3D`) and method declarations +- `src/button_container.cpp` — added `get_label()` and `set_text()` helpers +- `src/button_container.h` — corresponding declarations + +## Building from source + +### 1. Clone with submodules + +```bash +git clone --recurse-submodules https://github.com/coreflake1/guppyscreen.git +cd guppyscreen +git checkout ke-advanced-3d-bedmesh +``` + +### 2. Build bundled libraries + +```bash +make libhv.a +make libspdlog.a +make wpaclient +``` + +### 3a. Simulator build (Linux desktop, for testing) + +Install SDL2 dev headers: +```bash +# Fedora / Nobara +sudo dnf install sdl2-compat-devel + +# Debian / Ubuntu / Raspbian +sudo apt install libsdl2-dev +``` + +Build: +```bash +make -j$(nproc) \ + CFLAGS="-O3 -g0 -MD -MP -I./ -I./lvgl/ -I/usr/include/SDL2 -D_GNU_SOURCE=1 -Wno-incompatible-pointer-types" \ + LDFLAGS="-lm -Llibhv/lib -Lspdlog/build -l:libhv.a -latomic -lpthread -Lwpa_supplicant/wpa_supplicant/ -l:libwpa_client.a -lstdc++fs -l:libspdlog.a" +``` + +> The `-Wno-incompatible-pointer-types` flag is required on GCC 14+ due to a pre-existing upstream +> strictness issue in `lv_touch_calibration`. It is unrelated to this branch's changes. + +Binary: `build/bin/guppyscreen` + +### 3b. Cross-compile for Ender-3 V3 KE (MIPS target hardware) + +The Ender-3 V3 KE runs an **Ingenic XBurst2 X2000 SoC (MIPS, mipsel little-endian, kernel 4.4.94)**. +The `ballaswag/guppydev` Docker image contains the required `mipsel-linux-` musl toolchain. + +Use the provided script (handles library rebuilds automatically): + +```bash +bash scripts/build-mips.sh +``` + +Binary: `build/bin/guppyscreen` — verified as `ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked`. + +> **Note**: `installer-deb.sh` targets aarch64/systemd/Debian and will immediately exit with +> `"Found arch mips / Terminating"` on this printer. The correct installer for the KE is +> `installer.sh` (K1/MIPS/BusyBox init path). + +**Package a release tarball** (creates `guppyscreen-smallscreen.tar.gz`): +```bash +GUPPYSCREEN_VERSION=0.1.0-ke-bedmesh GUPPY_THEME=blue bash scripts/release.sh guppyscreen-smallscreen +``` + +--- + # Guppy Screen for Klipper Guppy Screen is a touch UI for Klipper using APIs exposed by Moonraker. It builds on LVGL as a standalone executable, has no dependency on any display servers such as X/Wayland. @@ -20,45 +127,62 @@ Guppy Screen is a touch UI for Klipper using APIs exposed by Moonraker. It build

## Installation / Update -Run the following interactive script via SSH on your K1/Max/CR-10 SE/Ender 3 V3 KE/Nebula Pad to install Guppy Screen. -#### Material Design Theme -``` -sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/probielodan/guppyscreen/main/scripts/installer.sh)" -``` +> **Hardware**: Ender-3 V3 KE — Ingenic XBurst2 X2000, MIPS (mipsel), kernel 4.4.94. +> This installer targets the K1/MIPS/BusyBox-init path. It is **untested on hardware** as +> of this writing. **Back up your printer config before proceeding.** -#### Z-Bolt Theme (Only for the K1/Max) -``` -sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/probielodan/guppyscreen/main/scripts/installer.sh)" -s zbolt -``` +SSH into your Ender-3 V3 KE and run: -### Install on a PI (Debian/Raspbian) -Tested on a BTT Pad 7. Please install with care and make sure you're okay with resetting your setup if things break. -``` -wget -O - https://raw.githubusercontent.com/probielodan/guppyscreen/main/scripts/installer-deb.sh | bash +```sh +sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/coreflake1/guppyscreen/ke-advanced-3d-bedmesh/scripts/installer.sh)" ``` -### Nightly Builds -#### Material (Nightly) -``` -sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/probielodan/guppyscreen/main/scripts/installer.sh)" -s nightly -``` +> **Do not use `installer-deb.sh`** — it targets aarch64/systemd/Debian and will immediately +> exit with `"Found arch mips / Terminating"` on the Ender-3 V3 KE. +> +> **Do not use probielodan's installer** — it downloads from probielodan's +> releases and does not include the 3D bed mesh feature. + +The installer downloads release `v0.1.0-ke-bedmesh` (pinned tag — not `latest`). + +**What the installer changes on your printer:** + +| Change | Rolled back by uninstall? | +|---|---| +| Extracts `/usr/data/guppyscreen/` | Optional (prompts) | +| Installs `/etc/init.d/S99guppyscreen` | Yes — removed | +| Replaces `S50dropbear` SSH init script¹ | **No** — original saved to `/usr/data/guppyify-backup/` | +| Moves `S12boot_display` to backup (disables boot display) | Partially — restored from backup | +| Backs up + removes `S99start_app` (optional, prompted) | Partially — restored from backup | +| Renames `Monitor` + `display-server` to `.disable` (if Creality kept) | **No** — restore manually | +| Adds `[include GuppyScreen/*.cfg]` to `printer.cfg` (after `gcode_macro` include) | Yes — line removed | +| Creates `printer_data/config/GuppyScreen/` | Yes — directory removed | +| Skips `gcode_shell_command.py` (already present on KE) | — | +| Overwrites `calibrate_shaper_config.py` in Klipper extras | **No** — original saved to backup | +| Symlinks `guppy_module_loader.py`, `guppy_config_helper.py`, `tmcstatus.py` | Yes — removed | +| Replaces `ft2font.so` (matplotlib 2.2.3 confirmed installed on KE) | Partially — original moved to backup | + +> ¹ The KE's stock `S50dropbear` has the `start` call **commented out**, so SSH does not +> auto-start on a clean reboot. The guppy version fixes this — SSH will auto-start after install. + +Backup files are saved to `/usr/data/guppyify-backup/` before any destructive change, +including a copy of `printer.cfg` taken **before** the include line is added. -#### Z-Bolt (Nightly) -``` -sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/probielodan/guppyscreen/main/scripts/installer.sh)" -s zbolt nightly -``` +## Uninstall -#### Raspbian Variant (Nightly) -``` -wget -O - https://raw.githubusercontent.com/probielodan/guppyscreen/main/scripts/installer-deb.sh | bash -s nightly +```sh +sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/coreflake1/guppyscreen/ke-advanced-3d-bedmesh/scripts/installer.sh)" uninstall ``` -## Uninstall -ssh into your K1/Max and run the following command: -``` -/usr/data/guppyscreen/reinstall-creality.sh -``` +Uninstall stops GuppyScreen, removes `/etc/init.d/S99guppyscreen`, restores +`S12boot_display` and `S99start_app` from backup, removes `[include GuppyScreen/*.cfg]` +from `printer.cfg`, removes the GuppyScreen config directory, and removes Klipper symlinks. +Things **not** automatically restored: +- `gcode_shell_command.py` and `calibrate_shaper_config.py` in Klipper extras +- `Monitor` / `display-server` if renamed to `.disable` (restore manually) +- `S50dropbear` (guppy version stays; original is in `/usr/data/guppyify-backup/`) +- Reboot required to restore display services ## Features :white_check_mark: Console/Macro Shell\ diff --git a/debian/guppyconfig.json b/debian/guppyconfig.json index 713337e..9a97567 100644 --- a/debian/guppyconfig.json +++ b/debian/guppyconfig.json @@ -1,6 +1,6 @@ { "default_printer": "default_printer", - "display_rotate": 0, + "display_rotate": 2, "log_path": "/logs/guppyscreen.log", "printers": { "default_printer": { diff --git a/docs/dev-notes.md b/docs/dev-notes.md new file mode 100644 index 0000000..27198da --- /dev/null +++ b/docs/dev-notes.md @@ -0,0 +1,366 @@ +# Developer Notes — ke-advanced-3d-bedmesh + +Accumulated discoveries from building and packaging this branch for the Ender-3 V3 KE. + +## Fork lineage + +``` +ballaswag/guppyscreen (original) + └── probielodan/guppyscreen (KE-focused UI rework) + └── coreflake1/guppyscreen + └── ke-advanced-3d-bedmesh ← this branch +``` + +3D bed mesh panel cherry-picked from prestonbrown/guppyscreen commit `bced7f7`. + +--- + +## Target hardware — Ender-3 V3 KE + +| Property | Value | +|---|---| +| SoC | Ingenic XBurst2 X2000 | +| Architecture | **MIPS (mipsel little-endian)** — NOT aarch64 | +| Kernel | 4.4.94 | +| Screen | 480×272 physical (framebuffer geometry 480×544 — double-buffer, two 272-line halves) | +| libc | /lib/ld-2.29.so (musl 2.29) | +| Init system | /etc/init.d/ BusyBox-style (S##name scripts) | +| systemctl | Present at /usr/bin/systemctl but NOT the primary init | +| Data root | /usr/data/ | +| Printer config | /usr/data/printer_data/config/printer.cfg | + +`systemctl` is present but a shim — the real init is BusyBox `/etc/init.d/`. + +--- + +## Binary + +- **Build script**: `scripts/build-mips.sh` (handles all library rebuilds automatically) +- **Docker image**: `ballaswag/guppydev:latest` +- **Toolchain**: `/toolchains/mips32el--musl--stable-2024.02-1/bin/mipsel-linux-` +- **Flags**: `GUPPY_SMALL_SCREEN=1` required for 480×544 +- **Output**: `build/bin/guppyscreen` +- **Verified**: `ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked` (~6.5 MB) +- **No dynamic deps** — `readelf -d` shows no NEEDED entries (musl static) + +--- + +## Release + +- **Tag**: `v0.1.0-ke-bedmesh` +- **Correct asset**: `guppyscreen-smallscreen.tar.gz` (MIPS) +- **Wrong asset**: `guppyscreen-arm.tar.gz` (aarch64, will not run on KE) +- **Package command**: + ```sh + GUPPYSCREEN_VERSION=0.1.0-ke-bedmesh GUPPY_THEME=blue bash scripts/release.sh guppyscreen-smallscreen + ``` + +--- + +## Installer + +- **Correct installer**: `scripts/installer.sh` (K1/MIPS/BusyBox path) +- **Wrong installer**: `scripts/installer-deb.sh` — immediately exits with + `"Found arch mips / Terminating"` on the KE. Do not use on this printer. + +**Install**: +```sh +sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/coreflake1/guppyscreen/ke-advanced-3d-bedmesh/scripts/installer.sh)" +``` + +**Uninstall**: +```sh +sh -c "$(wget --no-check-certificate -qO - https://raw.githubusercontent.com/coreflake1/guppyscreen/ke-advanced-3d-bedmesh/scripts/installer.sh)" uninstall +``` + +The installer always fetches from the branch URL (not from the tarball), so it is +always the latest version of the script. `PINNED_RELEASE="v0.1.0-ke-bedmesh"` in the +script pins which tarball it downloads. + +--- + +## What installer.sh does (complete) + +1. Checks arch == mips, screen < 800px → selects smallscreen asset +2. Checks `/lib/ld-2.29.so` present +3. Checks Moonraker reachable; queries klipper_path and config_file +4. Downloads tarball → extracts to `/usr/data/` +5. Substitutes ``/`` in `debian/guppyconfig.json` → `/usr/data/guppyscreen/guppyconfig.json` +6. Creates `/usr/data/guppyscreen/thumbnails/` +7. Smoke-tests the binary (runs 1s, checks `ps`) +8. Copies `gcode_shell_command.py` to Klipper extras (if not already present) +9. Creates `printer_data/config/GuppyScreen/scripts/` +10. Copies `scripts/*.cfg` and `*.py` to GuppyScreen config dir +11. **Backs up `printer.cfg`** → `/usr/data/guppyify-backup/printer.cfg.bak` (before modifying) +12. Adds `[include GuppyScreen/*.cfg]` to `printer.cfg` + - If `[include gcode_macro.cfg]` exists: inserts after that line + - Otherwise: **prepends** to the file (KE may not have that line) +13. Symlinks `/tmp/udisk` → gcodes dir `/usb` +14. Backs up (first run only, guarded by absence of `S50dropbear` in backup): + - **Moves** `S12boot_display` → backup (disables boot display init) + - Copies `S50dropbear` → backup + - Copies `S99start_app` → backup (if file exists; KE may not have it) +15. Backs up `ft2font.so` (moves to backup) +16. **Overwrites** `/etc/init.d/S50dropbear` with guppy version + - **Why**: The KE's stock `S50dropbear` has the `start` call inside `case start)` **commented + out** (`# start`). This means `S50dropbear start` is a no-op — SSH does **not** + auto-start on a clean reboot. Guppy's version has the `start` uncommented, so SSH reliably + starts on every boot after install. Currently SSH works on this printer because the + helper-script started dropbear directly (not via the init script). +17. Interactive: disable all Creality services? + - Y → removes `S99start_app` (guarded: only if exists) + - N → renames `Monitor` and `display-server` to `.disable` (guarded: only if exist) +18. Installs `/etc/init.d/S99guppyscreen` +19. Backs up existing `calibrate_shaper_config.py` → backup dir; then overwrites it +20. Symlinks `guppy_module_loader.py`, `guppy_config_helper.py`, `tmcstatus.py` → Klipper extras +21. Replaces `ft2font.cpython-38-mipsel-linux-gnu.so` if matplotlib is installed +22. Symlinks `libeinfo.so.1`, `librc.so.1` → `/lib/` (needed by supervise-daemon) +23. Optionally restarts Klipper +24. Kills Creality processes if chosen +25. Starts `/etc/init.d/S99guppyscreen`; partial rollback if final ps check fails + +--- + +## Backup directory: `/usr/data/guppyify-backup/` + +| File | How saved | Notes | +|---|---|---| +| `printer.cfg.bak` | copied | Taken before include line is added | +| `S50dropbear` | copied | Original Creality SSH init | +| `S12boot_display` | **moved** | Only copy is in backup | +| `S99start_app` | copied | May be absent on KE — guarded | +| `ft2font.cpython-38-mipsel-linux-gnu.so` | **moved** | Only copy is in backup | +| `calibrate_shaper_config.py.bak` | copied | Taken before unconditional overwrite | + +--- + +## What uninstall does NOT restore + +- `gcode_shell_command.py` in Klipper extras (left in place) +- `/etc/init.d/S50dropbear` (guppy version stays; restore manually: `cp /usr/data/guppyify-backup/S50dropbear /etc/init.d/S50dropbear`) +- `Monitor` / `display-server` if renamed to `.disable` (restore manually: `mv /usr/bin/Monitor.disable /usr/bin/Monitor`) +- `libeinfo.so.1`, `librc.so.1` symlinks in `/lib/` +- Reboot required after uninstall to restore display services + +--- + +## guppyconfig.json + +- In tarball at `./guppyscreen/debian/guppyconfig.json` +- Contains placeholders `` and `` +- installer.sh substitutes both before copying to `/usr/data/guppyscreen/guppyconfig.json` +- `guppy_init_script`: `"service guppyscreen"` (invokes the init script via service command) +- `log_path`: resolves to `/usr/data/printer_data/logs/guppyscreen.log` + +--- + +## S99guppyscreen + +- Hardcoded paths: `GUPPY_DIR=/usr/data/guppyscreen`, log at `/usr/data/printer_data/logs/guppyscreen.log` +- Uses `supervise-daemon` from `k1_mods/respawn/` for auto-restart on crash +- Requires `libeinfo.so.1` and `librc.so.1` symlinked in `/lib/` +- `RC_DIR=/run/openrc` created on start (needed by OpenRC supervise-daemon) + +--- + +## Simulator build (Linux desktop) + +```sh +sudo dnf install sdl2-compat-devel # Fedora/Nobara +make libhv.a && make libspdlog.a && make wpaclient +make -j$(nproc) \ + CFLAGS="-O3 -g0 -MD -MP -I./ -I./lvgl/ -I/usr/include/SDL2 -D_GNU_SOURCE=1 -Wno-incompatible-pointer-types" \ + LDFLAGS="-lm -Llibhv/lib -Lspdlog/build -l:libhv.a -latomic -lpthread -Lwpa_supplicant/wpa_supplicant/ -l:libwpa_client.a -lstdc++fs -l:libspdlog.a" +``` + +- Binary needs `/usr/data/printer_data/logs/` to exist or it crashes on startup + (`spdlog_ex: Failed opening file ... for writing`) +- Without a printer connected, websocket reconnects every 2s — this is normal +- Simulator shows 480×800 (not 480×544); smallscreen layout only applies at runtime + when `GUPPY_SMALL_SCREEN=1` is compiled in +- Mouse wheel zoom works in simulator (SIMULATOR flag enables it with 50ms timer) + +--- + +## Confirmed on-device state (verified via SSH 2026-05-25) + +Hostname: `Ender3V3KE-4C14` + +### printer.cfg +- `[include gcode_macro.cfg]` **IS present** (line 8) — installer sed path will work; prepend fallback is unused but kept as insurance +- Config dir confirmed: `/usr/data/printer_data/config/` +- Klipper path confirmed: `/usr/share/klipper` +- Has `[bed_mesh]`, `[input_shaper]`, `[bltouch]`, `[prtouch_v2]`, `[adxl345]` +- Saved 5×5 bed mesh already present in `SAVE_CONFIG` block +- Saved input shaper: X=ei/55.4Hz, Y=ei/37.6Hz + +### Init scripts — all confirmed present +- `S12boot_display` — minimal: just runs `/usr/bin/boot_display display &` +- `S50dropbear` — **start case is COMMENTED OUT** (see S50dropbear section above) +- `S99start_app` — substantial script starting 8 Creality services (master-server, audio-server, wifi-server, app-server, display-server, upgrade-server, web-server, Monitor) + +### Binaries — all confirmed present +- `/usr/bin/Monitor` — 45 KB +- `/usr/bin/display-server` — 6.5 MB +- `/usr/sbin/dropbear` — running as PID 2124 (started by helper-script, not by init.d) + +### Klipper extras — already present +- `gcode_shell_command.py` — already installed (installer will skip, if-not-exists guard) +- `calibrate_shaper_config.py` — already installed (will be backed up then overwritten) +- No guppy modules yet (`guppy_module_loader`, `guppy_config_helper`, `tmcstatus`) + +### Matplotlib +- **Version 2.2.3 installed** → `ft2font.so` replacement will fire +- `ft2font.cpython-38-mipsel-linux-gnu.so` dated **Apr 15 2025** — post-firmware date, + meaning it was already replaced once (likely by helper-script). Our install will back + it up and replace it again with the guppy MIPS version. + +### Helper script +- `/usr/data/helper-script/` is present (KIAUH-equivalent for K1/KE) +- This is what enabled SSH (started dropbear directly; init.d start case is commented out) +- `/usr/data/helper-script-backup/` also present + +--- + +## Display orientation — confirmed fix (2026-05-25) + +The KE screen is physically mounted **upside-down (180° rotated)** inside the chassis. +The kernel display driver compensates by rotating the framebuffer 180°. +GuppyScreen/LVGL must therefore **pre-rotate 180°** so the kernel's rotation cancels out. + +**Fix**: `display_rotate: 2` in `debian/guppyconfig.json` (committed `8bed38d`). + +How it propagates: +- `display_rotate: 2` → `sw_rotate=1`, `rotated=LV_DISP_ROT_180` +- LVGL renders content upside-down into the framebuffer +- Kernel rotates 180° → net 0° on the physical screen + +Debugging history: +- `display_rotate:0` → screen upside-down (LVGL and kernel both unrotated, but hardware is inverted) +- `display_rotate:1` → 90° rotated (wrong axis) +- `display_rotate:2` → correct orientation ✓ +- White screen on restart (without full reboot) was a framebuffer state artefact — full reboot clears it + +`lv_disp_get_physical_hor_res()` returns **480** (not 272) for ROT_180 because a +180° rotation does not swap width/height, unlike 90°/270°. + +--- + +## Font and layout tuning (2026-05-25) + +### LVGL font architecture — critical lesson + +GuppyScreen is compiled with `GUPPY_SMALL_SCREEN=1`. Before the fix, all widget +text rendered at **16 px** (`lv_font_montserrat_16`), which is too large for a +272 px tall screen — labels wrapped, buttons truncated text, table cells were +unreadable. + +**Root cause**: `LV_FONT_DEFAULT` in `lv_conf.h` was unconditionally +`&lv_font_montserrat_16` — no `GUPPY_SMALL_SCREEN` guard existed. + +The `font` parameter passed to `lv_theme_default_init()` in `src/main.cpp` is +almost irrelevant: the LVGL v8 default theme only applies that font to the +checked-state marker of checkbox widgets (`cb_marker_checked`). It does **not** +set a default `text_font` on labels, buttons, tables, or any other widget. Those +all fall back to `LV_FONT_DEFAULT`. + +**Fix** (`lv_conf.h`, line ~379): +```c +// Before: +#define LV_FONT_DEFAULT &lv_font_montserrat_16 + +// After: +#ifdef GUPPY_SMALL_SCREEN + #define LV_FONT_DEFAULT &lv_font_montserrat_12 +#else + #define LV_FONT_DEFAULT &lv_font_montserrat_16 +#endif +``` + +This is a 25% reduction (16 → 12 px) that propagates to every widget which does +not have an explicit `lv_obj_set_style_text_font()` call. + +### Scale factors (reference) + +``` +width_scale = hor_res / 800.0 = 480/800 = 0.600 +height_scale = ver_res / 480.0 = 272/480 = 0.567 +``` + +Widgets with explicit hardcoded fonts (NOT affected by `LV_FONT_DEFAULT`): +| File | Widget | Font | +|---|---|---| +| `console_panel.cpp` | keyboard, send button, log label | `montserrat_16` | +| `macro_item.cpp` | run (play) button | `montserrat_16` | +| `macros_panel.cpp` | keyboard | `montserrat_16` | +| `bedmesh_panel.cpp` | 3D mesh labels | `montserrat_12` / `montserrat_16` | + +### Remaining layout issues (lower priority) + +| Screen | Issue | File | Notes | +|---|---|---|---| +| Temperature chart | Y-axis tick labels overlap at 6 ticks on 272px height | `src/main_panel.cpp` L227 | Reduce `major_cnt` 6→3 in `lv_chart_set_axis_tick`; set `LV_PART_TICKS` font to `montserrat_8` | +| Limits sliders | 4 sliders in COLUMN layout overflow 272px; bottom slider(s) clipped | `src/limits_panel.cpp` | Switch `limit_cont` to 2×2 grid (2 FR cols × 2 FR rows) | +| ~~Bed mesh table~~ | ~~Cells unreadable~~ | `src/bedmesh_panel.cpp` | **Resolved** — table view is now the default; see GUI pass below | + +--- + +## GUI & functional pass (2026-05-26) + +A round of usability fixes driven by on-device review. Verified in the SDL simulator +(`build-sim/`, `GUPPY_SMALL_SCREEN=1`). + +### Print-state safety locks +Panels are now gated by `KUtils::is_printing()` (true when `print_stats/state` is +`printing` or `paused`) so a touch mid-print can't ruin the job. Helpers live in +`src/utils.cpp`: `is_printing()`, `notify_locked()` (modal toast), and +`confirm_if_printing(msg, cb)` (Confirm/Cancel override; runs `cb` immediately when idle). + +- **Block entry + toast** (nothing useful to see mid-print): Homing, Extrude + (`main_panel.cpp`); Bed Mesh, Input Shaper, Belts/Shake, TMC Autotune + (`printertune_panel.cpp`). +- **Confirm-with-override**: Console (per command, `console_panel.cpp`), Macros + (per run, `macro_item.cpp`), Limits & Power (on entry, `printertune_panel.cpp`). +- **Left open** (designed for mid-print use): Fine Tune, Fan, LED, Print Status, file browsing. +- Note: a new-print start was *already* guarded in `print_panel.cpp` — it shows a + "Printing in progress…" dialog instead of starting a second job. (`View Job` / `Queue Job` + in that dialog are unimplemented stubs.) + +### Layout / UX +- **LED ↔ Fine Tune swap**: Fine Tune moved to the home screen (used during prints); + LED moved to the Tune tab. `LedPanel` is owned by `MainPanel` and now passed by + reference into `PrinterTunePanel` (where `FineTunePanel&` used to go). +- **Bed Mesh redesign** (`bedmesh_panel.cpp`): **table view is now the default**. The + 3D view is an opt-in fullscreen mode — pressing "3D View" hides the profile list and + the other controls; the canvas fills the left and a narrow right-side strip holds the + zoom +/- and Back buttons. Back from 3D returns to table view; Back from table exits. + `resize_canvas()` floor lowered for <800px screens (the old 300px min overflowed 272px). +- **Fine Tune readouts** (`finetune_panel.cpp`): Z/PA precision trimmed `{:.5}`→`{:.3f}` + and readout icon zoom 150→100 so long values (e.g. `0.123 mm/s`) no longer collide + with the icon. +- **Belt shake** (`belts_calibration_panel.cpp`): excite-frequency readout moved from + 35px below the slider (clipped by the cramped grid cell) to the top-right of the control. +- **System panel** (`sysinfo_panel.cpp`): Factory Reset moved to the top-right corner + (away from Back / settings rows to avoid accidental presses); network/version text + width-constrained so it can't slide under the button. Factory Reset already had a + "Reset GuppyScreen to default settings?" confirm prompt. +- **Mini print-status popup** (`mini_print_status.cpp`): aligned `TOP_RIGHT` (over the + now-locked Home/Fine-Tune buttons) instead of `TOP_LEFT` where it overlapped the temps. +- **MCU temp default** (`src/config.cpp`): shipped default sensor changed from + `temperature_sensor chamber_temp`/"Chamber" to `temperature_sensor mcu_temp`/"MCU Temp". + Affects fresh installs only — existing on-device `guppyconfig.json` must be edited to + update a printer that already has a config. + +--- + +## KE-specific gotchas (resolved vs remaining) + +- ~~`[include gcode_macro.cfg]` may not be in the KE's printer.cfg~~ **CONFIRMED PRESENT** +- ~~`S99start_app` may not exist on KE firmware~~ **CONFIRMED PRESENT** +- ~~`/usr/bin/Monitor` and `/usr/bin/display-server` may not exist~~ **BOTH CONFIRMED PRESENT** +- ~~Hardware install is untested~~ **CONFIRMED WORKING** — GuppyScreen installed and running +- `installer-deb.sh` is also present in the release tarball (stale, old version + without safety fixes) — it won't be run by the standard install flow but could + confuse someone who tries to run it from the extracted `/usr/data/guppyscreen/` dir +- ft2font.so was already replaced once by helper-script (Apr 2025); our install will + replace it again — the backup step ensures the current version is preserved diff --git a/lv_conf.h b/lv_conf.h index 3b2793d..891bb36 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -376,7 +376,13 @@ extern uint32_t custom_tick_get(void); #define LV_FONT_CUSTOM_DECLARE /*Always set a default font*/ -#define LV_FONT_DEFAULT &lv_font_montserrat_16 +#ifdef GUPPY_SMALL_SCREEN + /* m10 keeps file lists, btnmatrix labels (e.g. "Reload"/"Modified"/"A-Z"), + * and other un-styled labels readable without truncating at 480x272. */ + #define LV_FONT_DEFAULT &lv_font_montserrat_10 +#else + #define LV_FONT_DEFAULT &lv_font_montserrat_16 +#endif /*Enable handling large font and/or fonts with a lot of characters. *The limit depends on the font size, font face and bpp. diff --git a/lv_touch_calibration/lv_tc_screen.c b/lv_touch_calibration/lv_tc_screen.c index 2192db8..216b4ad 100644 --- a/lv_touch_calibration/lv_tc_screen.c +++ b/lv_touch_calibration/lv_tc_screen.c @@ -297,7 +297,7 @@ static void lv_tc_screen_recalibrate_timer(lv_timer_t *timer) { if(timer->repeat_count == 0) { //Restart when timed out - lv_tc_screen_start(tCScreenObj); + lv_tc_screen_start((lv_obj_t *)tCScreenObj); return; } lv_label_set_text_fmt(lv_obj_get_child(tCScreenObj->recalibrateBtnObj, 0), LV_TC_RECALIBRATE_TXT LV_TC_RECALIBRATE_TIMEOUT_FORMAT, (int)timer->repeat_count); @@ -307,5 +307,5 @@ static void lv_tc_screen_start_delay_timer(lv_timer_t *timer) { lv_tc_screen_t *tCScreenObj = (lv_tc_screen_t*)timer->user_data; lv_point_t point = {0, 0}; - lv_tc_screen_step(tCScreenObj, STEP_FIRST, point); + lv_tc_screen_step((lv_obj_t *)tCScreenObj, STEP_FIRST, point); } diff --git a/scripts/build-mips.sh b/scripts/build-mips.sh new file mode 100644 index 0000000..09847ba --- /dev/null +++ b/scripts/build-mips.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Cross-compile guppyscreen for MIPS (Ingenic X2000, K1/KE) +# Run inside ballaswag/guppydev:latest Docker container +set -e + +CROSS_COMPILE="${CROSS_COMPILE:-mipsel-linux-}" +GUPPYSCREEN_VERSION="${GUPPYSCREEN_VERSION:-0.1.0-ke-bedmesh}" +GUPPY_THEME="${GUPPY_THEME:-blue}" +JOBS="$(nproc)" +WORKDIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$WORKDIR" + +echo "=== MIPS build: CROSS_COMPILE=$CROSS_COMPILE ===" + +# Backup aarch64 libraries +cp libhv/lib/libhv.a /tmp/libhv.a.bak +cp spdlog/build/libspdlog.a /tmp/libspdlog.a.bak +cp wpa_supplicant/wpa_supplicant/libwpa_client.a /tmp/libwpa_client.a.bak + +restore_libs() { + echo "=== Restoring original libraries ===" + cp /tmp/libhv.a.bak libhv/lib/libhv.a + cp /tmp/libspdlog.a.bak spdlog/build/libspdlog.a + cp /tmp/libwpa_client.a.bak wpa_supplicant/wpa_supplicant/libwpa_client.a +} +trap restore_libs EXIT + +# --- libhv --- +echo "=== Building libhv for MIPS ===" +mkdir -p libhv/build-mips +cd libhv/build-mips +cmake .. \ + -DCMAKE_C_COMPILER="${CROSS_COMPILE}gcc" \ + -DCMAKE_CXX_COMPILER="${CROSS_COMPILE}g++" \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=mipsel \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DWITH_SSL=OFF \ + 2>&1 | tail -5 +make -j"$JOBS" hv_static 2>&1 | tail -5 +mkdir -p "$WORKDIR/libhv/lib" +cp lib/libhv_static.a "$WORKDIR/libhv/lib/libhv.a" +cd "$WORKDIR" + +# --- spdlog --- +echo "=== Building spdlog for MIPS ===" +mkdir -p spdlog/build-mips +cd spdlog/build-mips +cmake .. \ + -DCMAKE_C_COMPILER="${CROSS_COMPILE}gcc" \ + -DCMAKE_CXX_COMPILER="${CROSS_COMPILE}g++" \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=mipsel \ + -DSPDLOG_BUILD_SHARED=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + 2>&1 | tail -5 +make -j"$JOBS" 2>&1 | tail -5 +cp libspdlog.a "$WORKDIR/spdlog/build/libspdlog.a" +cd "$WORKDIR" + +# --- libwpa_client --- +echo "=== Building libwpa_client for MIPS ===" +cd wpa_supplicant/wpa_supplicant +WPA_CC="${CROSS_COMPILE}gcc" +WPA_CFLAGS="-O2 -D_GNU_SOURCE -DCONFIG_CTRL_IFACE -DCONFIG_CTRL_IFACE_UNIX -I../src -I../src/utils -I../src/common" +$WPA_CC $WPA_CFLAGS -c ../src/common/wpa_ctrl.c -o wpa_ctrl.o +$WPA_CC $WPA_CFLAGS -c ../src/utils/os_unix.c -o os_unix.o +$WPA_CC $WPA_CFLAGS -c ../src/utils/common.c -o common.o +$WPA_CC $WPA_CFLAGS -c ../src/utils/wpa_debug.c -o wpa_debug.o +${CROSS_COMPILE}ar crs libwpa_client.a wpa_ctrl.o os_unix.o common.o wpa_debug.o +cd "$WORKDIR" + +# --- guppyscreen --- +echo "=== Building guppyscreen for MIPS (GUPPY_SMALL_SCREEN=1) ===" +make clean +make -j"$JOBS" \ + CROSS_COMPILE="$CROSS_COMPILE" \ + GUPPY_SMALL_SCREEN=1 \ + GUPPYSCREEN_VERSION="$GUPPYSCREEN_VERSION" \ + GUPPY_THEME="$GUPPY_THEME" + +echo "=== Build complete ===" +file build/bin/guppyscreen diff --git a/scripts/installer-deb.sh b/scripts/installer-deb.sh index c18bf7c..e46e649 100755 --- a/scripts/installer-deb.sh +++ b/scripts/installer-deb.sh @@ -12,6 +12,9 @@ SHAPER_CONFIG=$KLIPPY_EXTRA_DIR/calibrate_shaper_config.py CONFIG_FILE="${HOME}/printer_data/config" PRINTER_DATA_DIR="${HOME}/printer_data" GUPPY_DIR="${HOME}/guppyscreen" +BACKUP_DIR="${HOME}/guppyscreen-backup-$(date +%Y%m%d-%H%M%S)" +PINNED_RELEASE="v0.1.0-ke-bedmesh" +WPA_SVC=/lib/systemd/system/wpa_supplicant.service has_moonraker() { echo "Checking for a working Moonraker" @@ -46,6 +49,18 @@ get_klipper_paths() { } +backup_existing() { + printf "${green}Creating pre-install backup at $BACKUP_DIR ${white}\n" + mkdir -p "$BACKUP_DIR" + [ -f "$CONFIG_DIR/printer.cfg" ] && cp "$CONFIG_DIR/printer.cfg" "$BACKUP_DIR/printer.cfg" + [ -f "$WPA_SVC" ] && cp "$WPA_SVC" "$BACKUP_DIR/wpa_supplicant.service" + [ -f /etc/systemd/system/guppyscreen.service ] && cp /etc/systemd/system/guppyscreen.service "$BACKUP_DIR/guppyscreen.service.bak" + [ -f /etc/systemd/system/disable_blinking_cursor.service ] && cp /etc/systemd/system/disable_blinking_cursor.service "$BACKUP_DIR/disable_blinking_cursor.service.bak" + [ -d "$CONFIG_DIR/GuppyScreen" ] && cp -r "$CONFIG_DIR/GuppyScreen" "$BACKUP_DIR/GuppyScreen" + printf "${green}Backup saved to $BACKUP_DIR ${white}\n" + printf "${yellow}NOTE: To manually restore printer.cfg: cp $BACKUP_DIR/printer.cfg $CONFIG_DIR/printer.cfg ${white}\n" +} + install_services() { sed -i "s||$USER|g" ${HOME}/guppyscreen/debian/guppyscreen.service @@ -98,17 +113,72 @@ restart_services() { service guppyscreen restart } +uninstall_guppy() { + printf "${green}Uninstalling Guppy Screen ${white}\n" + + # Stop and remove services + sudo systemctl stop guppyscreen.service 2>/dev/null + sudo systemctl disable guppyscreen.service 2>/dev/null + sudo systemctl disable disable_blinking_cursor.service 2>/dev/null + sudo rm -f /etc/systemd/system/guppyscreen.service + sudo rm -f /etc/systemd/system/disable_blinking_cursor.service + sudo systemctl daemon-reload + + # Re-enable KlipperScreen if it exists + sudo systemctl enable KlipperScreen.service 2>/dev/null && sudo systemctl start KlipperScreen.service 2>/dev/null + + # Restore wpa_supplicant.service from most recent backup if available + LATEST_BACKUP=$(ls -dt "${HOME}"/guppyscreen-backup-* 2>/dev/null | head -1) + if [ -n "$LATEST_BACKUP" ] && [ -f "$LATEST_BACKUP/wpa_supplicant.service" ]; then + sudo cp "$LATEST_BACKUP/wpa_supplicant.service" "$WPA_SVC" + printf "${green}Restored wpa_supplicant.service from backup ${white}\n" + fi + + # Remove GuppyScreen include from printer.cfg + K1_CONFIG_FILE=$(curl -s localhost:7125/printer/info 2>/dev/null | jq -r .result.config_file 2>/dev/null) + if [ -n "$K1_CONFIG_FILE" ] && [ -f "$K1_CONFIG_FILE" ]; then + sed -i '/\[include GuppyScreen/d' "$K1_CONFIG_FILE" + printf "${green}Removed GuppyScreen include from printer.cfg ${white}\n" + else + printf "${yellow}Could not auto-remove GuppyScreen from printer.cfg. Remove manually: [include GuppyScreen/*.cfg] ${white}\n" + fi + + # Remove GuppyScreen config directory + if [ -n "$K1_CONFIG_FILE" ]; then + CONFIG_DIR=$(dirname "$K1_CONFIG_FILE") + if [ -d "$CONFIG_DIR/GuppyScreen" ]; then + rm -rf "$CONFIG_DIR/GuppyScreen" + printf "${green}Removed $CONFIG_DIR/GuppyScreen ${white}\n" + fi + fi + + printf "${yellow}NOTE: Klipper extras (gcode_shell_command.py, calibrate_shaper_config.py) were NOT removed\n(they may be used by other tools). Remove manually if needed. ${white}\n" + + printf "${yellow}Do you want to remove ~/guppyscreen? (y/n): ${white}" + read confirm + if [ "$confirm" = "y" -o "$confirm" = "Y" ]; then + rm -rf "${HOME}/guppyscreen" + printf "${green}Removed ~/guppyscreen ${white}\n" + fi + printf "${green}GuppyScreen uninstalled. Restart Klipper to apply printer.cfg changes. ${white}\n" +} + ARCH=`uname -m` echo "Found arch $ARCH" if [ "$ARCH" = "aarch64" ]; then - printf "${green}Installing Guppy Screen ${white}\n" + if [ "$1" = "uninstall" ]; then + uninstall_guppy + exit 0 + fi + + printf "${green}Installing Guppy Screen (coreflake1 fork - ke-advanced-3d-bedmesh) ${white}\n" - ASSET_URL="https://github.com/probielodan/guppyscreen/releases/latest/download/guppyscreen-arm.tar.gz" + ASSET_URL="https://github.com/coreflake1/guppyscreen/releases/download/${PINNED_RELEASE}/guppyscreen-arm.tar.gz" if [ "$1" = "nightly" ]; then printf "${yellow}Installing nightly build ${white}\n" - ASSET_URL="https://github.com/probielodan/guppyscreen/releases/download/nightly/guppyscreen-arm.tar.gz" + ASSET_URL="https://github.com/coreflake1/guppyscreen/releases/download/nightly/guppyscreen-arm.tar.gz" fi curl -s -L $ASSET_URL -o /tmp/guppyscreen.tar.gz @@ -116,6 +186,7 @@ if [ "$ARCH" = "aarch64" ]; then has_moonraker get_klipper_paths + backup_existing install_services install_guppy_goodies restart_services diff --git a/scripts/installer.sh b/scripts/installer.sh index b043b71..0417b8b 100755 --- a/scripts/installer.sh +++ b/scripts/installer.sh @@ -10,6 +10,85 @@ K1_GUPPY_DIR=/usr/data/guppyscreen FT2FONT_PATH=/usr/lib/python3.8/site-packages/matplotlib/ft2font.cpython-38-mipsel-linux-gnu.so ASSET_NAME="guppyscreen" +uninstall_guppy() { + printf "${green}=== Uninstalling GuppyScreen ===${white}\n" + + # Get klipper/config paths (same fallback logic as installer) + KLIPPER_PATH_U=$(curl localhost:7125/printer/info 2>/dev/null | jq -r .result.klipper_path 2>/dev/null) + if [ -z "$KLIPPER_PATH_U" ] || [ "$KLIPPER_PATH_U" = "null" ]; then + KLIPPER_PATH_U=/usr/share/klipper + fi + KLIPPY_EXTRA_DIR_U=$KLIPPER_PATH_U/klippy/extras + + K1_CONFIG_FILE_U=$(curl localhost:7125/printer/info 2>/dev/null | jq -r .result.config_file 2>/dev/null) + if [ -z "$K1_CONFIG_FILE_U" ] || [ "$K1_CONFIG_FILE_U" = "null" ]; then + K1_CONFIG_DIR_U=/usr/data/printer_data/config + else + K1_CONFIG_DIR_U=$(dirname "$K1_CONFIG_FILE_U") + fi + + # Stop and remove GuppyScreen + printf "${green}Stopping GuppyScreen...${white}\n" + [ -f /etc/init.d/S99guppyscreen ] && /etc/init.d/S99guppyscreen stop 2>/dev/null + killall -q guppyscreen 2>/dev/null + rm -f /etc/init.d/S99guppyscreen + + # Restore S12boot_display from backup + if [ -f "$BACKUP_DIR/S12boot_display" ]; then + cp "$BACKUP_DIR/S12boot_display" /etc/init.d/S12boot_display + printf "${green}Restored S12boot_display${white}\n" + else + printf "${yellow}No S12boot_display backup found. Restore manually if needed.${white}\n" + fi + + # Restore S99start_app from backup + if [ -f "$BACKUP_DIR/S99start_app" ] && [ ! -f /etc/init.d/S99start_app ]; then + cp "$BACKUP_DIR/S99start_app" /etc/init.d/S99start_app + printf "${green}Restored S99start_app${white}\n" + fi + + # Remove [include GuppyScreen] from printer.cfg + if [ -f "$K1_CONFIG_DIR_U/printer.cfg" ]; then + sed -i '/\[include GuppyScreen/d' "$K1_CONFIG_DIR_U/printer.cfg" + printf "${green}Removed GuppyScreen include from printer.cfg${white}\n" + else + printf "${yellow}printer.cfg not found at $K1_CONFIG_DIR_U. Remove [include GuppyScreen/*.cfg] manually.${white}\n" + fi + + # Remove GuppyScreen config directory + if [ -d "$K1_CONFIG_DIR_U/GuppyScreen" ]; then + rm -rf "$K1_CONFIG_DIR_U/GuppyScreen" + printf "${green}Removed GuppyScreen config directory${white}\n" + fi + + # Remove Klipper symlinks + rm -f "$KLIPPY_EXTRA_DIR_U/guppy_module_loader.py" + rm -f "$KLIPPY_EXTRA_DIR_U/guppy_config_helper.py" + rm -f "$KLIPPY_EXTRA_DIR_U/tmcstatus.py" + printf "${green}Removed Klipper symlinks${white}\n" + + # Optionally remove /usr/data/guppyscreen + printf "${yellow}Remove $K1_GUPPY_DIR (binary + themes + configs)? (y/n): ${white}" + read confirm_rm + echo + if [ "$confirm_rm" = "y" ] || [ "$confirm_rm" = "Y" ]; then + rm -rf "$K1_GUPPY_DIR" + printf "${green}Removed $K1_GUPPY_DIR${white}\n" + fi + + printf "${yellow}NOTE: calibrate_shaper_config.py and gcode_shell_command.py NOT removed (may be shared with Klipper).${white}\n" + printf "${yellow}NOTE: If Monitor/display-server were renamed to .disable, restore manually:${white}\n" + printf "${yellow} mv /usr/bin/Monitor.disable /usr/bin/Monitor${white}\n" + printf "${yellow} mv /usr/bin/display-server.disable /usr/bin/display-server${white}\n" + printf "${yellow}NOTE: Backup files remain at $BACKUP_DIR (ft2font.so, printer.cfg.bak, etc.)${white}\n" + printf "${green}GuppyScreen uninstalled. Reboot printer to restore display services.${white}\n" +} + +if [ "$1" = "uninstall" ]; then + uninstall_guppy + exit 0 +fi + if [ "$1" = "zbolt" ]; then ASSET_NAME="$ASSET_NAME-zbolt" fi @@ -82,12 +161,8 @@ rm -rf /root/.cache wget -q --no-check-certificate https://raw.githubusercontent.com/ballaswag/k1-discovery/main/bin/curl -O /tmp/curl chmod +x /tmp/curl -ASSET_URL="https://github.com/probielodan/guppyscreen/releases/latest/download/$ASSET_NAME.tar.gz" - -if [ "$1" = "nightly" ] || [ "$2" = "nightly" ]; then - printf "${yellow}Installing nightly build ${white}\n" - ASSET_URL="https://github.com/probielodan/guppyscreen/releases/download/nightly/$ASSET_NAME.tar.gz" -fi +PINNED_RELEASE="v0.1.0-ke-bedmesh" +ASSET_URL="https://github.com/coreflake1/guppyscreen/releases/download/${PINNED_RELEASE}/$ASSET_NAME.tar.gz" printf "${green} Downloading asset: $ASSET_NAME.tar.gz ${white}\n" @@ -95,6 +170,14 @@ printf "${green} Downloading asset: $ASSET_NAME.tar.gz ${white}\n" /tmp/curl -s -L $ASSET_URL -o /tmp/guppyscreen.tar.gz tar xf /tmp/guppyscreen.tar.gz -C /usr/data/ +# substitute paths in guppyconfig.json +if [ -f "$K1_GUPPY_DIR/debian/guppyconfig.json" ]; then + PRINTER_DATA_DIR=$(dirname "$K1_CONFIG_DIR") + sed -i "s||$K1_GUPPY_DIR|g; s||$PRINTER_DATA_DIR|g" "$K1_GUPPY_DIR/debian/guppyconfig.json" + cp "$K1_GUPPY_DIR/debian/guppyconfig.json" "$K1_GUPPY_DIR/guppyconfig.json" +fi +mkdir -p "$K1_GUPPY_DIR/thumbnails" + if [ ! -f "$K1_GUPPY_DIR/guppyscreen" ]; then printf "${red}Did not find guppyscreen in $K1_GUPPY_DIR. GuppyScreen must be extracted in $K1_GUPPY_DIR ${white}\n" exit 1 @@ -129,25 +212,35 @@ mkdir -p $K1_CONFIG_DIR/GuppyScreen/scripts cp $K1_GUPPY_DIR/scripts/*.cfg $K1_CONFIG_DIR/GuppyScreen cp $K1_GUPPY_DIR/scripts/*.py $K1_CONFIG_DIR/GuppyScreen/scripts -## includ guppyscreen *.cfg in printer.cfg +## backup printer.cfg before any modification +mkdir -p $BACKUP_DIR +if [ ! -f "$BACKUP_DIR/printer.cfg.bak" ]; then + cp "$K1_CONFIG_DIR/printer.cfg" "$BACKUP_DIR/printer.cfg.bak" 2>/dev/null || true + printf "${green}Backed up printer.cfg to $BACKUP_DIR/printer.cfg.bak ${white}\n" +fi + +## include guppyscreen *.cfg in printer.cfg if grep -q "include GuppyScreen" $K1_CONFIG_DIR/printer.cfg ; then echo "printer.cfg already includes GuppyScreen cfgs" -else - printf "${green}Including guppyscreen cfgs in printer.cfg ${white}\n" +elif grep -q "include gcode_macro" $K1_CONFIG_DIR/printer.cfg ; then + printf "${green}Including guppyscreen cfgs in printer.cfg (after gcode_macro include) ${white}\n" sed -i '/\[include gcode_macro\.cfg\]/a \[include GuppyScreen/*\.cfg\]' $K1_CONFIG_DIR/printer.cfg +else + printf "${green}Including guppyscreen cfgs in printer.cfg (prepended, no gcode_macro include found) ${white}\n" + sed -i '1s;^;\[include GuppyScreen/*\.cfg\]\n;' $K1_CONFIG_DIR/printer.cfg fi ## symlink usb K1_GCODE_DIR=$(dirname "$K1_CONFIG_DIR")/gcodes ln -sf /tmp/udisk $K1_GCODE_DIR/usb -if [ ! -d "$BACKUP_DIR" ]; then +if [ ! -f "$BACKUP_DIR/S50dropbear" ]; then printf "${green} Backing up original K1 files ${white}\n" mkdir -p $BACKUP_DIR - mv /etc/init.d/S12boot_display $BACKUP_DIR + [ -f /etc/init.d/S12boot_display ] && mv /etc/init.d/S12boot_display $BACKUP_DIR cp /etc/init.d/S50dropbear $BACKUP_DIR - cp /etc/init.d/S99start_app $BACKUP_DIR + [ -f /etc/init.d/S99start_app ] && cp /etc/init.d/S99start_app $BACKUP_DIR fi if [ ! -f $BACKUP_DIR/ft2font.cpython-38-mipsel-linux-gnu.so ]; then @@ -168,16 +261,18 @@ echo if [ "$confirm_decreality" = "y" -o "$confirm_decreality" = "Y" ]; then printf "${green}Disabling Creality services ${white}\n" - rm /etc/init.d/S99start_app + [ -f /etc/init.d/S99start_app ] && rm /etc/init.d/S99start_app else - # disables only display-server and Monitor - mv /usr/bin/Monitor /usr/bin/Monitor.disable - mv /usr/bin/display-server /usr/bin/display-server.disable + # disables only display-server and Monitor (guard: may not exist on all firmwares) + [ -f /usr/bin/Monitor ] && mv /usr/bin/Monitor /usr/bin/Monitor.disable + [ -f /usr/bin/display-server ] && mv /usr/bin/display-server /usr/bin/display-server.disable fi printf "${green}Setting up GuppyScreen ${white}\n" cp $K1_GUPPY_DIR/k1_mods/S99guppyscreen /etc/init.d/S99guppyscreen +# backup existing calibrate_shaper_config.py before unconditional overwrite +[ -f "$SHAPER_CONFIG" ] && [ ! -f "$BACKUP_DIR/calibrate_shaper_config.py.bak" ] && cp "$SHAPER_CONFIG" "$BACKUP_DIR/calibrate_shaper_config.py.bak" cp $K1_GUPPY_DIR/k1_mods/calibrate_shaper_config.py $SHAPER_CONFIG ln -sf $K1_GUPPY_DIR/k1_mods/guppy_module_loader.py $KLIPPY_EXTRA_DIR/guppy_module_loader.py diff --git a/scripts/release.sh b/scripts/release.sh index 6815322..dfa82da 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -7,11 +7,15 @@ mkdir -p $RELEASES_DIR ASSET_NAME=$1 "$CROSS_COMPILE"strip ./build/bin/guppyscreen -"$CROSS_COMPILE"strip ./build/bin/kd_graphic_mode cp ./build/bin/guppyscreen $RELEASES_DIR/guppyscreen +if [ -f ./build/bin/kd_graphic_mode ]; then + "$CROSS_COMPILE"strip ./build/bin/kd_graphic_mode + cp ./build/bin/kd_graphic_mode $RELEASES_DIR/debian +fi cp -r ./k1/k1_mods $RELEASES_DIR cp -r ./k1/scripts $RELEASES_DIR cp -r ./themes $RELEASES_DIR +cp ./scripts/installer-deb.sh $RELEASES_DIR cp ./scripts/installer.sh $RELEASES_DIR cp ./scripts/update.sh $RELEASES_DIR if [ -f ./custom_upgrade.sh ]; then @@ -19,7 +23,6 @@ if [ -f ./custom_upgrade.sh ]; then fi cp scripts/reinstall-creality.sh $RELEASES_DIR cp -r ./debian $RELEASES_DIR -cp ./build/bin/kd_graphic_mode $RELEASES_DIR/debian echo "{\"version\": \"$GUPPYSCREEN_VERSION\", \"theme\": \"$GUPPY_THEME\", \"asset_name\": \"$ASSET_NAME.tar.gz\"}" > $RELEASES_DIR/.version diff --git a/src/bedmesh_panel.cpp b/src/bedmesh_panel.cpp index 86f17a1..d8474b2 100644 --- a/src/bedmesh_panel.cpp +++ b/src/bedmesh_panel.cpp @@ -7,6 +7,31 @@ #include #include +#ifndef SIMULATOR +#include +#include +#include +#include + +// Helper macros for bit testing +#define NLONGS(x) (((x) + 8*sizeof(long) - 1) / (8*sizeof(long))) +#define test_bit(nr, addr) (((1UL << ((nr) % (8*sizeof(long)))) & ((addr)[(nr) / (8*sizeof(long))])) != 0) +#endif + +LV_IMG_DECLARE(chart_img); + +// Z zoom limits +const double MIN_Z_SCALE = 0.1; +const double MAX_Z_SCALE = 5.0; + +// FOV/Camera distance limits +const double MIN_CAMERA_DISTANCE = 200.0; +const double MAX_CAMERA_DISTANCE = 800.0; + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + LV_IMG_DECLARE(back); LV_IMG_DECLARE(delete_img); LV_IMG_DECLARE(bedmesh_img); @@ -14,6 +39,7 @@ LV_IMG_DECLARE(sd_img); static lv_color_t color_gradient(double offset); +static lv_color_t color_gradient_enhanced(double offset, double min_val, double max_val); BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) : NotifyConsumer(l) @@ -21,7 +47,14 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) , cont(lv_obj_create(lv_scr_act())) , prompt(lv_obj_create(lv_scr_act())) , top_cont(lv_obj_create(cont)) - , mesh_table(lv_table_create(top_cont)) + , display_cont(lv_obj_create(top_cont)) + , mesh_table(lv_table_create(display_cont)) + , mesh_canvas(lv_canvas_create(display_cont)) + , rotation_cont(nullptr) + , z_zoom_in_btn(lv_btn_create(display_cont)) + , z_zoom_out_btn(lv_btn_create(display_cont)) + , fov_zoom_in_btn(lv_btn_create(display_cont)) + , fov_zoom_out_btn(lv_btn_create(display_cont)) , profile_cont(lv_obj_create(top_cont)) , profile_table(lv_table_create(profile_cont)) , profile_info(lv_table_create(profile_cont)) @@ -29,10 +62,30 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) , save_btn(controls_cont, &sd_img, "Save Profile", &BedMeshPanel::_handle_callback, this) , clear_btn(controls_cont, &delete_img, "Clear Profile", &BedMeshPanel::_handle_callback, this) , calibrate_btn(controls_cont, &bedmesh_img, "Calibrate", &BedMeshPanel::_handle_callback, this) + , toggle_view_btn(controls_cont, &chart_img, "3D View", &BedMeshPanel::_handle_toggle_view, this) , back_btn(controls_cont, &back, "Back", &BedMeshPanel::_handle_callback, this) , msgbox(lv_obj_create(prompt)) , input(lv_textarea_create(msgbox)) , kb(lv_keyboard_create(prompt)) + , current_view_angle(VIEW_ANGLE_Z_DEGREES) + , canvas_draw_buf(nullptr) + , show_3d_view(false) + , z_display_scale(2.4) + , camera_distance(CAMERA_DISTANCE) + , mesh_min_x(0.0) + , mesh_max_x(0.0) + , mesh_min_y(0.0) + , mesh_max_y(0.0) + , is_dragging(false) + , drag_start_x(0) + , last_drag_x(0) + , drag_start_y(0) + , last_drag_y(0) + , current_x_angle(VIEW_ANGLE_X_DEGREES) // Start with the default viewing angle + , has_multitouch(false) + , use_gesture_zoom(false) + , mouse_over_canvas(false) + , scroll_check_timer(nullptr) { lv_obj_move_background(cont); @@ -48,6 +101,13 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) lv_obj_set_flex_grow(top_cont, 1); lv_obj_set_style_pad_all(top_cont, 0, 0); + lv_obj_set_height(display_cont, LV_PCT(100)); + lv_obj_clear_flag(display_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(display_cont, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(display_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_grow(display_cont, 1); + lv_obj_set_style_pad_all(display_cont, 1, 0); // Absolute minimal padding for maximum canvas space + auto screen_width = lv_disp_get_physical_hor_res(NULL); if (screen_width < 800) { lv_obj_set_style_text_font(mesh_table, &lv_font_montserrat_8, LV_STATE_DEFAULT); @@ -56,9 +116,116 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) } // mesh table - lv_obj_set_height(mesh_table, LV_PCT(100)); - lv_obj_clear_flag(mesh_table, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_width(mesh_table, LV_PCT(100)); lv_obj_set_flex_grow(mesh_table, 1); + lv_obj_clear_flag(mesh_table, LV_OBJ_FLAG_SCROLLABLE); + + // Setup 3D canvas - start with larger initial size + auto canvas_width = 400; + auto canvas_height = 400; + + canvas_draw_buf = lv_mem_alloc(LV_CANVAS_BUF_SIZE_TRUE_COLOR(canvas_width, canvas_height)); + lv_canvas_set_buffer(mesh_canvas, canvas_draw_buf, canvas_width, canvas_height, LV_IMG_CF_TRUE_COLOR); + lv_obj_set_size(mesh_canvas, canvas_width, canvas_height); + lv_obj_set_flex_grow(mesh_canvas, 1); + lv_obj_set_align(mesh_canvas, LV_ALIGN_CENTER); + + // Fill canvas with dark slate gray background initially + lv_canvas_fill_bg(mesh_canvas, lv_color_make(40, 40, 40), LV_OPA_COVER); + + spdlog::debug("Initial canvas size: {}x{}", canvas_width, canvas_height); + + // Add resize callback to update canvas size when container changes + lv_obj_add_event_cb(display_cont, [](lv_event_t *e) { + BedMeshPanel *panel = static_cast(lv_event_get_user_data(e)); + panel->resize_canvas(); + }, LV_EVENT_SIZE_CHANGED, this); + + // Create an invisible overlay button on top of canvas to capture input events + // Canvas objects don't receive input events well in LVGL + // Create it as a child of the canvas itself to ensure proper positioning + lv_obj_t *canvas_overlay = lv_btn_create(mesh_canvas); + + // Position it to cover the entire canvas area + lv_obj_set_size(canvas_overlay, LV_PCT(100), LV_PCT(100)); + lv_obj_set_pos(canvas_overlay, 0, 0); + + // Make the overlay invisible but clickable + lv_obj_set_style_bg_opa(canvas_overlay, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_opa(canvas_overlay, LV_OPA_TRANSP, 0); + lv_obj_set_style_shadow_opa(canvas_overlay, LV_OPA_TRANSP, 0); + lv_obj_set_style_outline_opa(canvas_overlay, LV_OPA_TRANSP, 0); + + // Remove any padding/margin + lv_obj_set_style_pad_all(canvas_overlay, 0, 0); + + // Ensure it doesn't interfere with layout + lv_obj_add_flag(canvas_overlay, LV_OBJ_FLAG_FLOATING); + + // Add drag event handler to the overlay button + lv_obj_add_event_cb(canvas_overlay, &BedMeshPanel::_handle_canvas_drag, LV_EVENT_PRESSED, this); + lv_obj_add_event_cb(canvas_overlay, &BedMeshPanel::_handle_canvas_drag, LV_EVENT_PRESSING, this); + lv_obj_add_event_cb(canvas_overlay, &BedMeshPanel::_handle_canvas_drag, LV_EVENT_RELEASED, this); + + // Detect input capabilities and configure zoom behavior +#ifdef SIMULATOR + // In simulation mode, add mouse wheel zoom support via encoder polling + use_gesture_zoom = true; + // Create timer to continuously check for mouse wheel input in 3D view mode + scroll_check_timer = lv_timer_create(&BedMeshPanel::_scroll_check_timer_cb, 50, this); // Check every 50ms + spdlog::debug("Simulator mode detected - mouse wheel zoom enabled (use_gesture_zoom={}) with 50ms timer always active", use_gesture_zoom); +#else + // On real hardware, check for multi-touch support + has_multitouch = detect_multitouch_support(); + if (has_multitouch) { + use_gesture_zoom = true; + lv_obj_add_event_cb(canvas_overlay, &BedMeshPanel::_handle_canvas_gesture, LV_EVENT_GESTURE, this); + spdlog::debug("Multi-touch detected: pinch zoom enabled"); + } else { + spdlog::debug("No multi-touch support: using zoom buttons"); + } +#endif + + + // Create single controls container for all 3D controls + rotation_cont = lv_obj_create(display_cont); + lv_obj_set_size(rotation_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(rotation_cont, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(rotation_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(rotation_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(rotation_cont, 15, 0); // More padding to prevent slider clipping + lv_obj_set_style_pad_column(rotation_cont, 10, 0); // Add spacing between items + + // Move all controls to the single container - Z buttons first, then FOV buttons + lv_obj_set_parent(z_zoom_out_btn, rotation_cont); // Z- (minus) first + lv_obj_set_parent(z_zoom_in_btn, rotation_cont); // Z+ (plus) second + lv_obj_set_parent(fov_zoom_out_btn, rotation_cont); // FOV- (minus) third + lv_obj_set_parent(fov_zoom_in_btn, rotation_cont); // FOV+ (plus) fourth + + + // Setup Z zoom buttons with correct symbols + setup_button_with_label(z_zoom_in_btn, LV_SYMBOL_PLUS, 40, 30, &BedMeshPanel::_handle_z_zoom_in); + setup_button_with_label(z_zoom_out_btn, LV_SYMBOL_MINUS, 40, 30, &BedMeshPanel::_handle_z_zoom_out); + + // Setup FOV zoom buttons with correct symbols (hidden for now, kept for future debugging) + setup_button_with_label(fov_zoom_in_btn, LV_SYMBOL_UP, 40, 30, &BedMeshPanel::_handle_fov_zoom_in); + setup_button_with_label(fov_zoom_out_btn, LV_SYMBOL_DOWN, 40, 30, &BedMeshPanel::_handle_fov_zoom_out); + + // Hide FOV buttons (debugging only) + lv_obj_add_flag(fov_zoom_in_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(fov_zoom_out_btn, LV_OBJ_FLAG_HIDDEN); + + // Hide Z zoom buttons if gesture zoom is available + if (use_gesture_zoom) { + lv_obj_add_flag(z_zoom_in_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(z_zoom_out_btn, LV_OBJ_FLAG_HIDDEN); + spdlog::debug("Z zoom buttons hidden - using gesture zoom"); + } + + // Start in table view by default; 3D is an opt-in fullscreen mode. + lv_obj_clear_flag(mesh_table, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(mesh_canvas, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(rotation_cont, LV_OBJ_FLAG_HIDDEN); // profile container lv_obj_set_height(profile_cont, LV_PCT(100)); @@ -72,9 +239,7 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) lv_obj_add_event_cb(profile_cont, [](lv_event_t *e) { lv_obj_t *profile_cont = lv_event_get_target(e); lv_obj_t *profile_table = static_cast(lv_event_get_user_data(e)); - int profile_cont_width = lv_obj_get_width(profile_cont); - lv_table_set_col_width(profile_table, 0, profile_cont_width * 0.7); lv_table_set_col_width(profile_table, 1, profile_cont_width * 0.15); lv_table_set_col_width(profile_table, 2, profile_cont_width * 0.15); @@ -86,9 +251,7 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) lv_obj_add_event_cb(profile_cont, [](lv_event_t *e) { lv_obj_t *profile_cont = lv_event_get_target(e); lv_obj_t *profile_info = static_cast(lv_event_get_user_data(e)); - int profile_cont_width = lv_obj_get_width(profile_cont); - lv_table_set_col_width(profile_info, 0, profile_cont_width * 0.5); lv_table_set_col_width(profile_info, 1, profile_cont_width * 0.5); }, LV_EVENT_SIZE_CHANGED, profile_info); @@ -107,6 +270,7 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) lv_obj_set_flex_grow(save_btn.get_container(), 1); lv_obj_set_flex_grow(clear_btn.get_container(), 1); lv_obj_set_flex_grow(calibrate_btn.get_container(), 1); + lv_obj_set_flex_grow(toggle_view_btn.get_container(), 1); lv_obj_set_flex_grow(back_btn.get_container(), 1); lv_obj_add_event_cb(mesh_table, &BedMeshPanel::_mesh_draw_cb, LV_EVENT_DRAW_PART_BEGIN, this); @@ -163,6 +327,19 @@ BedMeshPanel::BedMeshPanel(KWebSocketClient &c, std::mutex &l) } BedMeshPanel::~BedMeshPanel() { + if (canvas_draw_buf != nullptr) { + lv_mem_free(canvas_draw_buf); + canvas_draw_buf = nullptr; + } + +#ifdef SIMULATOR + if (scroll_check_timer != nullptr) { + lv_timer_del(scroll_check_timer); + scroll_check_timer = nullptr; + spdlog::debug("Scroll check timer cleaned up"); + } +#endif + if (cont != NULL) { lv_obj_del(cont); cont = NULL; @@ -201,7 +378,6 @@ void BedMeshPanel::refresh_views(json &bm) { save_btn.enable(); refresh_profile_info(active_profile); - lv_obj_clear_flag(mesh_table, LV_OBJ_FLAG_HIDDEN); auto mesh_json = bm["/probed_matrix"_json_pointer]; if (mesh_json.is_null()) { @@ -209,31 +385,83 @@ void BedMeshPanel::refresh_views(json &bm) { } mesh = mesh_json.template get>>(); + spdlog::debug("Loaded mesh data: {}x{} points", mesh.size(), mesh.empty() ? 0 : mesh[0].size()); + + // Draw the 3D mesh + draw_3d_mesh(); + + // Force complete layout update chain to ensure proper table sizing + lv_obj_update_layout(cont); // Update main container first + lv_obj_update_layout(top_cont); // Then top container + lv_obj_update_layout(display_cont); // Then display container + + // Add a small delay to let the layout settle completely + // lv_timer_t* timer = lv_timer_create([](lv_timer_t* timer) { + // BedMeshPanel* panel = static_cast(timer->user_data); + // panel->delayed_table_setup(); + // lv_timer_del(timer); + // }, 10, this); // calculate cell width if (mesh.size() > 0 && mesh[0].size() > 0) { + // Force table to use available height + auto display_height = lv_obj_get_height(display_cont); + auto target_table_height = display_height - 60; // Leave room for controls + lv_obj_set_height(mesh_table, target_table_height); + auto height = lv_obj_get_height(mesh_table); - int cel_height = std::max(1, (int)(height / mesh.size() / 2 - 5)); auto width = lv_obj_get_width(mesh_table); - int col_width = std::max(4, (int)(width / mesh[0].size())); + spdlog::debug("Table dimensions: {}x{} (forced to {}), display_cont height: {}, mesh size: {}x{}", width, height, target_table_height, display_height, mesh[0].size(), mesh.size()); + + // Adjust spacing based on mesh size + bool is_large_mesh = (mesh.size() > 6 || mesh[0].size() > 6); + + int cel_height, col_width; + // Calculate actual row height needed: total height divided by number of rows + // Each row needs space for text + top padding + bottom padding + int available_row_height = height / mesh.size(); + + if (is_large_mesh) { + // Minimal padding for large meshes to fit more data + cel_height = std::max(1, available_row_height / 3); + col_width = std::max(35, (int)(width / mesh[0].size())); + } else { + // More comfortable padding for small meshes + cel_height = std::max(2, available_row_height / 2); + col_width = std::max(4, (int)(width / mesh[0].size())); + } lv_obj_set_style_pad_top(mesh_table, cel_height, LV_PART_ITEMS | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(mesh_table, cel_height, LV_PART_ITEMS | LV_STATE_DEFAULT); + // Reduce left/right padding for large meshes to prevent text wrapping + if (is_large_mesh) { + lv_obj_set_style_pad_left(mesh_table, 1, LV_PART_ITEMS | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(mesh_table, 1, LV_PART_ITEMS | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_pad_left(mesh_table, 3, LV_PART_ITEMS | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(mesh_table, 3, LV_PART_ITEMS | LV_STATE_DEFAULT); + } + lv_table_set_col_cnt(mesh_table, mesh[0].size()); for (int i = 0; i < mesh[0].size(); i++) { lv_table_set_col_width(mesh_table, i, col_width); } } + // Use minimum 10pt font for readability, only drop to 8pt for very large meshes + if (mesh.size() > 9 || mesh[0].size() > 9) { + lv_obj_set_style_text_font(mesh_table, &lv_font_montserrat_8, LV_STATE_DEFAULT); + } else { + lv_obj_set_style_text_font(mesh_table, &lv_font_montserrat_10, LV_STATE_DEFAULT); + } + for (int i = mesh.size() - 1; i >= 0; i--) { auto row = mesh[i]; for (int j = 0; j < row.size(); j++) { - if (mesh.size() < 6 && row.size() < 6) { - lv_table_set_cell_value(mesh_table, row_idx, j, fmt::format("{:.2f}", row[j]).c_str()); - } else { - lv_table_set_cell_value(mesh_table, row_idx, j, ""); - } + // Use shorter format for large meshes to reduce wrapping + const char* format = (mesh.size() > 6 || mesh[0].size() > 6) ? "{:.1f}" : "{:.2f}"; + lv_table_set_cell_value(mesh_table, row_idx, j, fmt::format(format, row[j]).c_str()); } row_idx++; } @@ -301,11 +529,16 @@ void BedMeshPanel::refresh_profile_info(std::string profile) { } std::vector xvalues; - for (auto ¶m : {"min_x", "max_x", "x_count", "mesh_x_pps"}) { - v = mesh_params[json::json_pointer(fmt::format("/{}", param))]; - if (!v.is_null()) { - xvalues.push_back(v.template get()); - } + std::vector x_params = {"min_x", "max_x", "x_count", "mesh_x_pps"}; + extract_mesh_parameter_values(mesh_params, x_params, xvalues); + + v = mesh_params["/min_x"_json_pointer]; + if (!v.is_null()) { + mesh_min_x = v.template get(); + } + v = mesh_params["/max_x"_json_pointer]; + if (!v.is_null()) { + mesh_max_x = v.template get(); } lv_table_set_cell_value(profile_info, rowidx, 0, "X (min, max, count, pps)"); @@ -313,15 +546,20 @@ void BedMeshPanel::refresh_profile_info(std::string profile) { rowidx++; std::vector yvalues; - for (auto ¶m : {"min_y", "max_y", "y_count", "mesh_y_pps"}) { - v = mesh_params[json::json_pointer(fmt::format("/{}", param))]; - if (!v.is_null()) { - yvalues.push_back(v.template get()); - } + std::vector y_params = {"min_y", "max_y", "y_count", "mesh_y_pps"}; + extract_mesh_parameter_values(mesh_params, y_params, yvalues); + + v = mesh_params["/min_y"_json_pointer]; + if (!v.is_null()) { + mesh_min_y = v.template get(); + } + v = mesh_params["/max_y"_json_pointer]; + if (!v.is_null()) { + mesh_max_y = v.template get(); } lv_table_set_cell_value(profile_info, rowidx, 0, "Y (min, max, count, pps)"); - lv_table_set_cell_value(profile_info, rowidx, 1, fmt::format("{}", fmt::join(xvalues, ", ")).c_str()); + lv_table_set_cell_value(profile_info, rowidx, 1, fmt::format("{}", fmt::join(yvalues, ", ")).c_str()); rowidx++; } } @@ -330,6 +568,7 @@ void BedMeshPanel::foreground() { auto bm = State::get_instance()->get_data("/printer_state/bed_mesh"_json_pointer); spdlog::trace("bm {}", bm.dump()); refresh_views(bm); + show_table_view(); lv_obj_move_foreground(cont); } @@ -365,7 +604,12 @@ void BedMeshPanel::handle_callback(lv_event_t *event) { } else if (btn == back_btn.get_container()) { spdlog::trace("back button pressed"); - lv_obj_move_background(cont); + if (show_3d_view) { + // From fullscreen 3D, Back returns to the table view rather than exiting. + show_table_view(); + } else { + lv_obj_move_background(cont); + } } } @@ -464,6 +708,880 @@ void BedMeshPanel::mesh_draw_cb(lv_event_t *e) } } +void BedMeshPanel::resize_canvas() +{ + if (canvas_draw_buf == nullptr) { + return; + } + + // Canvas fills the display container. In fullscreen 3D the zoom controls live + // in the right-side strip, so no bottom reservation is needed here. + auto cont_width = lv_obj_get_width(display_cont); + auto cont_height = lv_obj_get_height(display_cont); + + // Floor must stay below the small-screen height (272) or the canvas overflows. + int min_dim = (lv_disp_get_physical_hor_res(NULL) < 800) ? 100 : 300; + auto canvas_width = std::max(min_dim, cont_width); + auto canvas_height = std::max(min_dim, std::min(cont_height, canvas_width)); + + // Only resize if significantly different to avoid constant redraws + auto current_width = lv_obj_get_width(mesh_canvas); + auto current_height = lv_obj_get_height(mesh_canvas); + + if (abs(canvas_width - current_width) > 10 || abs(canvas_height - current_height) > 10) { + // Free old buffer and create new one + lv_mem_free(canvas_draw_buf); + canvas_draw_buf = lv_mem_alloc(LV_CANVAS_BUF_SIZE_TRUE_COLOR(canvas_width, canvas_height)); + lv_canvas_set_buffer(mesh_canvas, canvas_draw_buf, canvas_width, canvas_height, LV_IMG_CF_TRUE_COLOR); + lv_obj_set_size(mesh_canvas, canvas_width, canvas_height); + + // Fill with dark slate gray background + lv_canvas_fill_bg(mesh_canvas, lv_color_make(40, 40, 40), LV_OPA_COVER); + + spdlog::debug("Resized canvas from {}x{} to {}x{}", current_width, current_height, canvas_width, canvas_height); + + // Redraw with new size + if (show_3d_view && !mesh.empty()) { + draw_3d_mesh(); + } + } +} + +void BedMeshPanel::draw_mesh_surface(const std::vector>& points, + int rows, int cols, int canvas_width, int canvas_height, + double min_z, double max_z) +{ + // Draw surface quads with proper size and connection + for (int row = 0; row < rows - 1; row++) { + for (int col = 0; col < cols - 1; col++) { + // Get the four corner points of the current quad + const auto& p1 = points[row][col]; // bottom-left + const auto& p2 = points[row][col+1]; // bottom-right + const auto& p3 = points[row+1][col]; // top-left + const auto& p4 = points[row+1][col+1]; // top-right + + // Calculate average height for color + double avg_height = (mesh[row][col] + mesh[row][col+1] + + mesh[row+1][col] + mesh[row+1][col+1]) / 4.0; + + lv_color_t surface_color = color_gradient_enhanced(avg_height, min_z, max_z); + + // Create a larger, more connected surface quad + int min_x = std::min({p1.screen_x, p2.screen_x, p3.screen_x, p4.screen_x}); + int max_x = std::max({p1.screen_x, p2.screen_x, p3.screen_x, p4.screen_x}); + int min_y = std::min({p1.screen_y, p2.screen_y, p3.screen_y, p4.screen_y}); + int max_y = std::max({p1.screen_y, p2.screen_y, p3.screen_y, p4.screen_y}); + + // Expand the quad to ensure surface connection + int width = std::max(8, max_x - min_x + 4); // Minimum width for visibility + int height = std::max(8, max_y - min_y + 4); // Minimum height for visibility + + // Center the expanded quad + int center_x = (min_x + max_x) / 2; + int center_y = (min_y + max_y) / 2; + min_x = center_x - width / 2; + min_y = center_y - height / 2; + + // Check bounds + if (min_x >= 0 && min_y >= 0 && + min_x + width < canvas_width && min_y + height < canvas_height) { + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = surface_color; + rect_dsc.bg_opa = LV_OPA_90; + rect_dsc.border_width = 0; + + lv_canvas_draw_rect(mesh_canvas, min_x, min_y, width, height, &rect_dsc); + } + } + } +} + +void BedMeshPanel::draw_mesh_wireframe(const std::vector>& points, + int rows, int cols, int canvas_width, int canvas_height) +{ + lv_draw_line_dsc_t line_dsc; + lv_draw_line_dsc_init(&line_dsc); + line_dsc.color = lv_color_make(80, 80, 80); // Dark gray wireframe + line_dsc.width = 1; + line_dsc.opa = LV_OPA_60; + + // Draw horizontal grid lines + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols - 1; col++) { + const auto& p1 = points[row][col]; + const auto& p2 = points[row][col + 1]; + + // Check bounds + if (p1.screen_x >= 0 && p1.screen_x < canvas_width && p1.screen_y >= 0 && p1.screen_y < canvas_height && + p2.screen_x >= 0 && p2.screen_x < canvas_width && p2.screen_y >= 0 && p2.screen_y < canvas_height) { + lv_point_t line_points[2] = {{p1.screen_x, p1.screen_y}, {p2.screen_x, p2.screen_y}}; + lv_canvas_draw_line(mesh_canvas, line_points, 2, &line_dsc); + } + } + } + + // Draw vertical grid lines + for (int col = 0; col < cols; col++) { + for (int row = 0; row < rows - 1; row++) { + const auto& p1 = points[row][col]; + const auto& p2 = points[row + 1][col]; + + // Check bounds + if (p1.screen_x >= 0 && p1.screen_x < canvas_width && p1.screen_y >= 0 && p1.screen_y < canvas_height && + p2.screen_x >= 0 && p2.screen_x < canvas_width && p2.screen_y >= 0 && p2.screen_y < canvas_height) { + lv_point_t line_points[2] = {{p1.screen_x, p1.screen_y}, {p2.screen_x, p2.screen_y}}; + lv_canvas_draw_line(mesh_canvas, line_points, 2, &line_dsc); + } + } + } +} + +void BedMeshPanel::draw_axes_and_labels(const std::vector>& points, + int rows, int cols, int canvas_width, int canvas_height, + double min_z, double max_z, double z_scale) +{ + // Draw coordinate axes and labels with wireframe style + lv_draw_line_dsc_t axis_dsc; + lv_draw_line_dsc_init(&axis_dsc); + axis_dsc.color = lv_color_make(160, 160, 160); // Slightly darker gray + axis_dsc.width = 1; // Thin lines + axis_dsc.opa = LV_OPA_70; // More visible but still subtle + + lv_draw_label_dsc_t label_dsc; + lv_draw_label_dsc_init(&label_dsc); + label_dsc.color = lv_color_white(); + label_dsc.font = &lv_font_montserrat_10; + + + // Draw Z-axis (vertical) - connect to X/Y axes origin with centered Z range + // Use the mesh corner coordinates for X/Y positioning (same as X/Y axes) + double mesh_origin_x = (0 - cols / 2.0) * MESH_SCALE; + double mesh_origin_y = (0 - rows / 2.0) * MESH_SCALE; + + // Draw wireframe grid for X-Y plane at the same Z level as the bottom of the Z-axis (minimum Z value) + double z_range = max_z - min_z; + double grid_reference_z = (-z_range * Z_AXIS_EXTENSION) * z_scale; // At the bottom of the visualization + spdlog::debug("Drawing grid at Z={}, z_range={}, z_scale={}, (matches Z-axis bottom/min)", grid_reference_z, z_range, z_scale); + + // Draw grid lines parallel to X-axis and Y-axis + draw_grid_lines_parallel_to_axis(rows, false, rows, cols, grid_reference_z, canvas_width, canvas_height, axis_dsc); + draw_grid_lines_parallel_to_axis(cols, true, rows, cols, grid_reference_z, canvas_width, canvas_height, axis_dsc); + + // Draw wireframe grid for X/Z plane (vertical plane at Y edge) + double y_edge = (0 - rows / 2.0) * MESH_SCALE; // Front edge of mesh (minimum Y) + for (int col = 0; col < cols; col++) { + double grid_x = (col - cols / 2.0) * MESH_SCALE; + Point3D start = project_3d_to_2d(grid_x, y_edge, (-z_range * Z_AXIS_EXTENSION) * z_scale, canvas_width, canvas_height); + Point3D end = project_3d_to_2d(grid_x, y_edge, (z_range * Z_AXIS_EXTENSION) * z_scale, canvas_width, canvas_height); + + + lv_point_t grid_line[2] = {{start.screen_x, start.screen_y}, {end.screen_x, end.screen_y}}; + lv_canvas_draw_line(mesh_canvas, grid_line, 2, &axis_dsc); + } + + // Add horizontal lines to complete X/Z plane wireframe with multiple Z divisions + double x_left = (0 - cols / 2.0) * MESH_SCALE; + double x_right = ((cols-1) - cols / 2.0) * MESH_SCALE; + + // Use same number of Z divisions as X/Y grid divisions + int z_divisions = std::max(rows, cols); + double z_min = (-z_range * Z_AXIS_EXTENSION) * z_scale; + double z_max = (z_range * Z_AXIS_EXTENSION) * z_scale; + + for (int z_div = 0; z_div <= z_divisions; z_div++) { + double z_level = z_min + (z_div * (z_max - z_min) / z_divisions); + + Point3D xz_left = project_3d_to_2d(x_left, y_edge, z_level, canvas_width, canvas_height); + Point3D xz_right = project_3d_to_2d(x_right, y_edge, z_level, canvas_width, canvas_height); + lv_point_t xz_line[2] = {{xz_left.screen_x, xz_left.screen_y}, {xz_right.screen_x, xz_right.screen_y}}; + lv_canvas_draw_line(mesh_canvas, xz_line, 2, &axis_dsc); + } + + // Draw wireframe grid for Y/Z plane (vertical plane at X edge) + double x_edge = (0 - cols / 2.0) * MESH_SCALE; // Left edge of mesh (minimum X) + for (int row = 0; row < rows; row++) { + double grid_y = (row - rows / 2.0) * MESH_SCALE; + Point3D start = project_3d_to_2d(x_edge, grid_y, (-z_range * Z_AXIS_EXTENSION) * z_scale, canvas_width, canvas_height); + Point3D end = project_3d_to_2d(x_edge, grid_y, (z_range * Z_AXIS_EXTENSION) * z_scale, canvas_width, canvas_height); + + + lv_point_t grid_line[2] = {{start.screen_x, start.screen_y}, {end.screen_x, end.screen_y}}; + lv_canvas_draw_line(mesh_canvas, grid_line, 2, &axis_dsc); + } + + // Add horizontal lines to complete Y/Z plane wireframe with multiple Z divisions + double y_front = (0 - rows / 2.0) * MESH_SCALE; + double y_back = ((rows-1) - rows / 2.0) * MESH_SCALE; + + for (int z_div = 0; z_div <= z_divisions; z_div++) { + double z_level = z_min + (z_div * (z_max - z_min) / z_divisions); + + Point3D yz_front = project_3d_to_2d(x_edge, y_front, z_level, canvas_width, canvas_height); + Point3D yz_back = project_3d_to_2d(x_edge, y_back, z_level, canvas_width, canvas_height); + lv_point_t yz_line[2] = {{yz_front.screen_x, yz_front.screen_y}, {yz_back.screen_x, yz_back.screen_y}}; + lv_canvas_draw_line(mesh_canvas, yz_line, 2, &axis_dsc); + } + + // Z-axis using extended range for better visibility + Point3D z_bottom = project_3d_to_2d(mesh_origin_x, mesh_origin_y, (-z_range * Z_AXIS_EXTENSION) * z_scale, canvas_width, canvas_height); + Point3D z_top = project_3d_to_2d(mesh_origin_x, mesh_origin_y, (z_range * Z_AXIS_EXTENSION) * z_scale, canvas_width, canvas_height); + + // Check if both points are reasonably within view (with culling margin) + if (z_bottom.screen_x >= -CULLING_MARGIN && z_bottom.screen_x < canvas_width + CULLING_MARGIN && + z_bottom.screen_y >= -CULLING_MARGIN && z_bottom.screen_y < canvas_height + CULLING_MARGIN && + z_top.screen_x >= -CULLING_MARGIN && z_top.screen_x < canvas_width + CULLING_MARGIN && + z_top.screen_y >= -CULLING_MARGIN && z_top.screen_y < canvas_height + CULLING_MARGIN) { + + lv_point_t z_line[2] = {{z_bottom.screen_x, z_bottom.screen_y}, {z_top.screen_x, z_top.screen_y}}; + lv_canvas_draw_line(mesh_canvas, z_line, 2, &axis_dsc); + + // Draw X and Y axis lines + draw_horizontal_axes(mesh_origin_x, mesh_origin_y, grid_reference_z, rows, cols, canvas_width, canvas_height, axis_dsc); + + // Add Z-axis labels along the front left edge (min X, max Y) + lv_draw_label_dsc_t z_label_dsc; + lv_draw_label_dsc_init(&z_label_dsc); + z_label_dsc.color = lv_color_white(); + z_label_dsc.font = &lv_font_montserrat_10; + + lv_draw_label_dsc_t z_axis_label_dsc; + lv_draw_label_dsc_init(&z_axis_label_dsc); + z_axis_label_dsc.color = lv_color_white(); + z_axis_label_dsc.font = &lv_font_montserrat_10; + + double left_x = (0 - cols / 2.0) * MESH_SCALE; + double front_y = ((rows-1) - rows / 2.0) * MESH_SCALE; + + int z_divisions = std::max(rows, cols); + double z_min = (-z_range * Z_AXIS_EXTENSION) * z_scale; + double z_max = (z_range * Z_AXIS_EXTENSION) * z_scale; + + // Draw Z-axis tick values + draw_z_axis_tick_values(left_x, front_y, z_min, z_max, z_divisions, z_scale, canvas_width, canvas_height, z_label_dsc, min_z, max_z); + + // Draw Z-axis label + double middle_z_level = z_min + ((z_divisions/2) * (z_max - z_min) / z_divisions); + draw_axis_label("Z", left_x - TICK_MARK_LENGTH * 6, front_y, middle_z_level, -10, -10, canvas_width, canvas_height, z_axis_label_dsc); + + + + } + + // Add mesh info (left-justified) and Z-range info (right-justified) on same line using smaller font + char mesh_info[64]; + snprintf(mesh_info, sizeof(mesh_info), "Mesh: %dx%d", cols, rows); + + char z_info[64]; + snprintf(z_info, sizeof(z_info), "Z: %.3f to %.3f", min_z, max_z); + + // Use smaller font for both labels + lv_draw_label_dsc_t small_label_dsc; + lv_draw_label_dsc_init(&small_label_dsc); + small_label_dsc.color = lv_color_white(); + small_label_dsc.font = &lv_font_montserrat_10; + + // Left-justified mesh info + lv_area_t mesh_info_area; + if (create_and_check_label_area(mesh_info_area, 10, canvas_height - 25, 100, LABEL_HEIGHT, canvas_width, canvas_height)) { + lv_canvas_draw_text(mesh_canvas, mesh_info_area.x1, mesh_info_area.y1, 100, &small_label_dsc, mesh_info); + } + + // Right-justified Z info + lv_draw_label_dsc_t right_label_dsc; + lv_draw_label_dsc_init(&right_label_dsc); + right_label_dsc.color = lv_color_white(); + right_label_dsc.font = &lv_font_montserrat_10; + +#ifndef CROSS_COMPILE + // Debug info for simulation mode: show current viewing angles at top + char debug_info[128]; + // The Y rotation is fixed at 0° since we only control X (tilt) and Z (spin) rotations + snprintf(debug_info, sizeof(debug_info), "View: X=%.1f° Y=0.0° Z=%.1f°", + current_x_angle, (double)current_view_angle); + + lv_draw_label_dsc_t debug_label_dsc; + lv_draw_label_dsc_init(&debug_label_dsc); + debug_label_dsc.color = lv_color_make(255, 255, 0); // Yellow for visibility + debug_label_dsc.font = &lv_font_montserrat_10; + + lv_area_t debug_area; + if (create_and_check_label_area(debug_area, 10, 10, 200, LABEL_HEIGHT, canvas_width, canvas_height)) { + lv_canvas_draw_text(mesh_canvas, debug_area.x1, debug_area.y1, 200, &debug_label_dsc, debug_info); + } +#endif + right_label_dsc.align = LV_TEXT_ALIGN_RIGHT; + + lv_area_t z_info_area; + if (create_and_check_label_area(z_info_area, canvas_width - 160, canvas_height - 25, 150, LABEL_HEIGHT, canvas_width, canvas_height)) { + lv_canvas_draw_text(mesh_canvas, z_info_area.x1, z_info_area.y1, 150, &right_label_dsc, z_info); + } +} + +// Convert 3D world coordinates to 2D screen coordinates using perspective projection +BedMeshPanel::Point3D BedMeshPanel::project_3d_to_2d(double x, double y, double z, int canvas_width, int canvas_height) +{ + // Camera parameters for perspective projection using dynamic camera distance + double fov_scale = this->fov_scale; + + // Apply user-controlled Z-axis rotation (rotate around vertical axis in the horizontal X/Y plane) + double z_angle = current_view_angle * M_PI / 180.0; + double cos_z = cos(z_angle); + double sin_z = sin(z_angle); + + // Rotate around Z-axis (this rotates in the X/Y horizontal plane) + double rotated_x = x * cos_z - y * sin_z; + double rotated_y = x * sin_z + y * cos_z; + double rotated_z = z * z_display_scale; // Apply Z display scaling + + // Apply dynamic X-axis rotation controlled by user + double x_angle = current_x_angle * M_PI / 180.0; + double cos_x = cos(x_angle); + double sin_x = sin(x_angle); + + double final_x = rotated_x; + double final_y = rotated_y * cos_x + rotated_z * sin_x; // Fixed: + instead of - for Z component + double final_z = rotated_y * sin_x - rotated_z * cos_x; // Fixed: - instead of + for Z component + + // Move camera back to avoid clipping + final_z += this->camera_distance; + + // Perspective projection: project 3D to 2D using similar triangles + // screen_pos = (3d_pos * focal_length) / depth + double perspective_x = (final_x * fov_scale) / final_z; + double perspective_y = (final_y * fov_scale) / final_z; + + // Convert to screen coordinates with offset for gradient band space + // Center mesh in the available space (canvas minus gradient band area) + int effective_width = canvas_width - GRADIENT_BAND_TOTAL_WIDTH; + int screen_x = GRADIENT_BAND_TOTAL_WIDTH + effective_width / 2 + (int)perspective_x; + int screen_y = canvas_height * Z_ORIGIN_VERTICAL_POSITION + (int)perspective_y; + + Point3D result; + result.x = final_x; + result.y = final_y; + result.z = final_z; + result.screen_x = screen_x; + result.screen_y = screen_y; + result.depth = final_z; // Store depth for sorting (larger = further away) + + return result; +} + +// Triangle rasterization using scan-line algorithm for proper 3D polygon filling +void BedMeshPanel::fill_triangle(int x1, int y1, int x2, int y2, int x3, int y3, lv_color_t color, int canvas_width, int canvas_height) +{ + // Sort vertices by Y coordinate (y1 <= y2 <= y3) + if (y1 > y2) { std::swap(x1, x2); std::swap(y1, y2); } + if (y2 > y3) { std::swap(x2, x3); std::swap(y2, y3); } + if (y1 > y2) { std::swap(x1, x2); std::swap(y1, y2); } + + // Skip degenerate triangles + if (y1 == y3) return; + + // For each scanline, interpolate left and right X coordinates + for (int y = std::max(0, y1); y <= std::min(canvas_height - 1, y3); y++) { + int x_left, x_right; + + if (y < y2) { + // Upper half of triangle + if (y2 != y1) { + x_left = x1 + (x2 - x1) * (y - y1) / (y2 - y1); + } else { + x_left = x1; + } + if (y3 != y1) { + x_right = x1 + (x3 - x1) * (y - y1) / (y3 - y1); + } else { + x_right = x1; + } + } else { + // Lower half of triangle + if (y3 != y2) { + x_left = x2 + (x3 - x2) * (y - y2) / (y3 - y2); + } else { + x_left = x2; + } + if (y3 != y1) { + x_right = x1 + (x3 - x1) * (y - y1) / (y3 - y1); + } else { + x_right = x1; + } + } + + // Ensure x_left <= x_right + if (x_left > x_right) std::swap(x_left, x_right); + + // Clamp to canvas bounds and draw horizontal line + x_left = std::max(0, x_left); + x_right = std::min(canvas_width - 1, x_right); + + if (x_left <= x_right) { + // Draw horizontal line using LVGL rect (width 1 for line) + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = color; + rect_dsc.bg_opa = LV_OPA_90; + rect_dsc.border_width = 0; + + int width = x_right - x_left + 1; + if (width > 0) { + lv_canvas_draw_rect(mesh_canvas, x_left, y, width, 1, &rect_dsc); + } + } + } +} + +// Triangle rasterization with gradient interpolation between vertex colors +void BedMeshPanel::fill_triangle_gradient(int x1, int y1, lv_color_t c1, int x2, int y2, lv_color_t c2, int x3, int y3, lv_color_t c3, int canvas_width, int canvas_height) +{ + // Helper struct to keep vertex data together during sorting + struct Vertex { + int x, y; + lv_color_t color; + }; + + // Create vertex array and sort by Y coordinate + Vertex vertices[3] = {{x1, y1, c1}, {x2, y2, c2}, {x3, y3, c3}}; + + // Sort vertices by Y coordinate (bubble sort for 3 elements) + if (vertices[0].y > vertices[1].y) std::swap(vertices[0], vertices[1]); + if (vertices[1].y > vertices[2].y) std::swap(vertices[1], vertices[2]); + if (vertices[0].y > vertices[1].y) std::swap(vertices[0], vertices[1]); + + // Skip degenerate triangles + if (vertices[0].y == vertices[2].y) return; + + // Extract RGB components for interpolation + auto extract_rgb = [](lv_color_t color) -> std::tuple { + return {LV_COLOR_GET_R(color), LV_COLOR_GET_G(color), LV_COLOR_GET_B(color)}; + }; + + auto [r1, g1, b1] = extract_rgb(vertices[0].color); + auto [r2, g2, b2] = extract_rgb(vertices[1].color); + auto [r3, g3, b3] = extract_rgb(vertices[2].color); + + // For each scanline, interpolate left and right X coordinates and colors + for (int y = std::max(0, vertices[0].y); y <= std::min(canvas_height - 1, vertices[2].y); y++) { + int x_left, x_right; + int r_left, g_left, b_left, r_right, g_right, b_right; + + if (y < vertices[1].y) { + // Upper half of triangle: interpolate between v0-v1 and v0-v2 + float t1 = (vertices[1].y != vertices[0].y) ? (float)(y - vertices[0].y) / (vertices[1].y - vertices[0].y) : 0.0f; + float t2 = (vertices[2].y != vertices[0].y) ? (float)(y - vertices[0].y) / (vertices[2].y - vertices[0].y) : 0.0f; + + x_left = vertices[0].x + (int)(t1 * (vertices[1].x - vertices[0].x)); + x_right = vertices[0].x + (int)(t2 * (vertices[2].x - vertices[0].x)); + + r_left = r1 + (int)(t1 * (r2 - r1)); + g_left = g1 + (int)(t1 * (g2 - g1)); + b_left = b1 + (int)(t1 * (b2 - b1)); + + r_right = r1 + (int)(t2 * (r3 - r1)); + g_right = g1 + (int)(t2 * (g3 - g1)); + b_right = b1 + (int)(t2 * (b3 - b1)); + } else { + // Lower half of triangle: interpolate between v1-v2 and v0-v2 + float t1 = (vertices[2].y != vertices[1].y) ? (float)(y - vertices[1].y) / (vertices[2].y - vertices[1].y) : 0.0f; + float t2 = (vertices[2].y != vertices[0].y) ? (float)(y - vertices[0].y) / (vertices[2].y - vertices[0].y) : 0.0f; + + x_left = vertices[1].x + (int)(t1 * (vertices[2].x - vertices[1].x)); + x_right = vertices[0].x + (int)(t2 * (vertices[2].x - vertices[0].x)); + + r_left = r2 + (int)(t1 * (r3 - r2)); + g_left = g2 + (int)(t1 * (g3 - g2)); + b_left = b2 + (int)(t1 * (b3 - b2)); + + r_right = r1 + (int)(t2 * (r3 - r1)); + g_right = g1 + (int)(t2 * (g3 - g1)); + b_right = b1 + (int)(t2 * (b3 - b1)); + } + + // Ensure x_left <= x_right (swap if needed, including colors) + if (x_left > x_right) { + std::swap(x_left, x_right); + std::swap(r_left, r_right); + std::swap(g_left, g_right); + std::swap(b_left, b_right); + } + + // Clamp to canvas bounds + int x_start = std::max(0, x_left); + int x_end = std::min(canvas_width - 1, x_right); + + // Draw horizontal line with approximated gradient using fewer segments + if (x_start <= x_end) { + int line_width = x_end - x_start + 1; + + // For performance, use solid color for very thin lines + if (line_width <= GRADIENT_MIN_LINE_WIDTH) { + // Use average color for short lines + int r = (r_left + r_right) / 2; + int g = (g_left + g_right) / 2; + int b = (b_left + b_right) / 2; + + r = std::max(0, std::min(255, r)); + g = std::max(0, std::min(255, g)); + b = std::max(0, std::min(255, b)); + + lv_color_t avg_color = lv_color_make(r, g, b); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = avg_color; + rect_dsc.bg_opa = LV_OPA_90; + rect_dsc.border_width = 0; + + lv_canvas_draw_rect(mesh_canvas, x_start, y, line_width, 1, &rect_dsc); + } else { + // For longer lines, draw gradient using segments (max GRADIENT_MAX_SEGMENTS for performance) + int segments = std::min(GRADIENT_MAX_SEGMENTS, line_width / 4); + segments = std::max(1, segments); + + for (int seg = 0; seg < segments; seg++) { + int seg_start = x_start + (seg * line_width) / segments; + int seg_end = x_start + ((seg + 1) * line_width) / segments - 1; + int seg_width = seg_end - seg_start + 1; + + if (seg_width <= 0) continue; + + // Interpolate color at segment center + float t = (x_right != x_left) ? (float)(seg_start + seg_width/2 - x_left) / (x_right - x_left) : 0.0f; + + int r = r_left + (int)(t * (r_right - r_left)); + int g = g_left + (int)(t * (g_right - g_left)); + int b = b_left + (int)(t * (b_right - b_left)); + + r = std::max(0, std::min(255, r)); + g = std::max(0, std::min(255, g)); + b = std::max(0, std::min(255, b)); + + lv_color_t seg_color = lv_color_make(r, g, b); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.bg_color = seg_color; + rect_dsc.bg_opa = LV_OPA_90; + rect_dsc.border_width = 0; + + lv_canvas_draw_rect(mesh_canvas, seg_start, y, seg_width, 1, &rect_dsc); + } + } + } + } +} + +// Generate 3D quads from mesh data with proper vertex positioning and colors +std::vector BedMeshPanel::generate_mesh_quads(int rows, int cols, double min_z, double max_z) +{ + std::vector quads; + + // Use the same dynamic Z scaling as draw_3d_mesh with defined constants + double z_range = max_z - min_z; + double z_center = (min_z + max_z) / 2.0; // Center of the data range + double z_scale = DEFAULT_Z_SCALE; + if (z_range > 0.001) { // Avoid division by zero for flat surfaces + z_scale = DEFAULT_Z_TARGET_HEIGHT / z_range; + z_scale = std::max(DEFAULT_Z_MIN_SCALE, std::min(DEFAULT_Z_MAX_SCALE, z_scale)); + } + + // Generate quads for each mesh cell using dynamic scaling + for (int row = 0; row < rows - 1; row++) { + for (int col = 0; col < cols - 1; col++) { + Quad3D quad; + + // Calculate world positions for the four corners of this mesh cell + // Center the mesh around origin for better rotation + double base_x = (col - cols / 2.0) * MESH_SCALE; // Restore original X: col 0 = left (min X) + double base_y = ((rows - 1 - row) - rows / 2.0) * MESH_SCALE; // Invert Y: row 0 = front (min Y) + + // Bottom-left vertex (row, col) - center Z values around zero + quad.vertices[0].x = base_x; + quad.vertices[0].y = base_y; + quad.vertices[0].z = (mesh[row][col] - z_center) * z_scale; + quad.vertices[0].color = color_gradient_enhanced(mesh[row][col], min_z, max_z); + + // Bottom-right vertex (row, col+1) + quad.vertices[1].x = base_x + MESH_SCALE; + quad.vertices[1].y = base_y; + quad.vertices[1].z = (mesh[row][col + 1] - z_center) * z_scale; + quad.vertices[1].color = color_gradient_enhanced(mesh[row][col + 1], min_z, max_z); + + // Top-left vertex (row+1, col) - use inverted Y for row+1 + quad.vertices[2].x = base_x; + quad.vertices[2].y = ((rows - 1 - (row + 1)) - rows / 2.0) * MESH_SCALE; + quad.vertices[2].z = (mesh[row + 1][col] - z_center) * z_scale; + quad.vertices[2].color = color_gradient_enhanced(mesh[row + 1][col], min_z, max_z); + + // Top-right vertex (row+1, col+1) + quad.vertices[3].x = base_x + MESH_SCALE; + quad.vertices[3].y = ((rows - 1 - (row + 1)) - rows / 2.0) * MESH_SCALE; + quad.vertices[3].z = (mesh[row + 1][col + 1] - z_center) * z_scale; + quad.vertices[3].color = color_gradient_enhanced(mesh[row + 1][col + 1], min_z, max_z); + + // Calculate average height for center color (fallback) + double avg_height = (mesh[row][col] + mesh[row][col + 1] + + mesh[row + 1][col] + mesh[row + 1][col + 1]) / 4.0; + quad.center_color = color_gradient_enhanced(avg_height, min_z, max_z); + + // Calculate average depth for sorting (will be updated after projection) + quad.avg_depth = 0; // Will be calculated during projection + + quads.push_back(quad); + } + } + + return quads; +} + +// Sort quads by depth for proper back-to-front rendering (painter's algorithm) +void BedMeshPanel::sort_quads_by_depth(std::vector& quads) +{ + // Sort by average depth - furthest away (largest depth) first + std::sort(quads.begin(), quads.end(), [](const Quad3D& a, const Quad3D& b) { + return a.avg_depth > b.avg_depth; // Render back-to-front + }); +} + +// Render a single 3D quad as two triangles using proper triangle rasterization +void BedMeshPanel::draw_3d_quad(const Quad3D& quad, int canvas_width, int canvas_height) +{ + // Project all vertices to 2D screen coordinates + Point3D projected[4]; + for (int i = 0; i < 4; i++) { + projected[i] = project_3d_to_2d(quad.vertices[i].x, quad.vertices[i].y, quad.vertices[i].z, + canvas_width, canvas_height); + } + + // Simple culling with margin - skip if all vertices are far off-screen + // The margin allows partially visible triangles to be rendered + bool any_visible = false; + for (int i = 0; i < 4; i++) { + if (projected[i].screen_x >= -CULLING_MARGIN && projected[i].screen_x < canvas_width + CULLING_MARGIN && + projected[i].screen_y >= -CULLING_MARGIN && projected[i].screen_y < canvas_height + CULLING_MARGIN) { + any_visible = true; + break; + } + } + + if (!any_visible) return; // Skip if completely off-screen + + // Use solid colors during dragging for performance, gradients when static + if (is_dragging) { + // Fast solid color rendering during drag operations + // Triangle 1: vertices 0, 1, 2 (bottom-left, bottom-right, top-left) + fill_triangle(projected[0].screen_x, projected[0].screen_y, + projected[1].screen_x, projected[1].screen_y, + projected[2].screen_x, projected[2].screen_y, + quad.center_color, canvas_width, canvas_height); + + // Triangle 2: vertices 1, 2, 3 (bottom-right, top-left, top-right) + fill_triangle(projected[1].screen_x, projected[1].screen_y, + projected[2].screen_x, projected[2].screen_y, + projected[3].screen_x, projected[3].screen_y, + quad.center_color, canvas_width, canvas_height); + } else { + // High-quality gradient rendering when not dragging + // Triangle 1: vertices 0, 1, 2 (bottom-left, bottom-right, top-left) + fill_triangle_gradient(projected[0].screen_x, projected[0].screen_y, quad.vertices[0].color, + projected[1].screen_x, projected[1].screen_y, quad.vertices[1].color, + projected[2].screen_x, projected[2].screen_y, quad.vertices[2].color, + canvas_width, canvas_height); + + // Triangle 2: vertices 1, 2, 3 (bottom-right, top-left, top-right) + fill_triangle_gradient(projected[1].screen_x, projected[1].screen_y, quad.vertices[1].color, + projected[2].screen_x, projected[2].screen_y, quad.vertices[2].color, + projected[3].screen_x, projected[3].screen_y, quad.vertices[3].color, + canvas_width, canvas_height); + } +} + +void BedMeshPanel::draw_3d_mesh() +{ + if (mesh.empty()) { + spdlog::debug("Cannot draw 3D mesh: mesh data is empty"); + return; + } + + if (canvas_draw_buf == nullptr) { + spdlog::debug("Cannot draw 3D mesh: canvas buffer is null"); + return; + } + + spdlog::debug("draw_3d_mesh: z_display_scale={}", z_display_scale); + + int canvas_width = lv_obj_get_width(mesh_canvas); + int canvas_height = lv_obj_get_height(mesh_canvas); + int mesh_rows = mesh.size(); + int mesh_cols = mesh[0].size(); + + spdlog::debug("Drawing proper 3D mesh: canvas {}x{}, mesh {}x{}", canvas_width, canvas_height, mesh_rows, mesh_cols); + + // Clear canvas with dark slate gray background + lv_canvas_fill_bg(mesh_canvas, lv_color_make(40, 40, 40), LV_OPA_COVER); + + // Find min/max Z values for color mapping + double min_z = mesh[0][0], max_z = mesh[0][0]; + for (const auto& row : mesh) { + for (double val : row) { + min_z = std::min(min_z, val); + max_z = std::max(max_z, val); + } + } + + // Calculate dynamic Z scaling using defined constants + double z_range = max_z - min_z; + double z_scale = DEFAULT_Z_SCALE; + if (z_range > 0.001) { // Avoid division by zero for flat surfaces + z_scale = DEFAULT_Z_TARGET_HEIGHT / z_range; + z_scale = std::max(DEFAULT_Z_MIN_SCALE, std::min(DEFAULT_Z_MAX_SCALE, z_scale)); + } + + // Calculate dynamic FOV scale to fit mesh in canvas with padding + fov_scale = calculate_dynamic_fov_scale(mesh_rows, mesh_cols, canvas_width, canvas_height); + + spdlog::debug("Bed mesh display: rotation={:.1f}°, Z range=[{:.3f} to {:.3f}]mm (span={:.3f}mm), scale={:.2f}, fov={:.1f}, camera_distance={:.1f}", + VIEW_ANGLE_X_DEGREES, min_z, max_z, z_range, z_scale, fov_scale, camera_distance); + + // Generate 3D quads from mesh data + std::vector quads = generate_mesh_quads(mesh_rows, mesh_cols, min_z, max_z); + + // Project quads to screen coordinates and calculate depths for sorting + for (auto& quad : quads) { + double total_depth = 0; + for (int i = 0; i < 4; i++) { + Point3D projected = project_3d_to_2d(quad.vertices[i].x, quad.vertices[i].y, quad.vertices[i].z, + canvas_width, canvas_height); + total_depth += projected.depth; + } + quad.avg_depth = total_depth / 4.0; // Average depth of all vertices + } + + // Create point grid for wireframe and axes (convert quads back to point grid) + std::vector> point_grid(mesh_rows, std::vector(mesh_cols)); + + // Calculate Z center for consistent centering with quads + double z_center = (min_z + max_z) / 2.0; + + for (int row = 0; row < mesh_rows; row++) { + for (int col = 0; col < mesh_cols; col++) { + double world_x = (col - mesh_cols / 2.0) * MESH_SCALE; // Restore original X: col 0 = left (min X) + double world_y = ((mesh_rows - 1 - row) - mesh_rows / 2.0) * MESH_SCALE; // Invert Y: row 0 = front (min Y) + double world_z = (mesh[row][col] - z_center) * z_scale; // Center Z values around zero + + point_grid[row][col] = project_3d_to_2d(world_x, world_y, world_z, canvas_width, canvas_height); + } + } + + // Draw coordinate axes and labels FIRST (behind the mesh) + draw_axes_and_labels(point_grid, mesh_rows, mesh_cols, canvas_width, canvas_height, min_z, max_z, z_scale); + + // Sort quads by depth (back-to-front rendering for proper overlapping) + sort_quads_by_depth(quads); + + // Render all quads in sorted order (ON TOP of axes/grid) + for (const auto& quad : quads) { + draw_3d_quad(quad, canvas_width, canvas_height); + } + + // Draw wireframe grid overlay following the 3D surface (on top) + draw_mesh_wireframe(point_grid, mesh_rows, mesh_cols, canvas_width, canvas_height); + + // Draw vertical color gradient band on the left side (on top of everything) + draw_color_gradient_band(canvas_width, canvas_height, min_z, max_z); + + // Force canvas redraw + lv_obj_invalidate(mesh_canvas); + spdlog::debug("3D mesh rendering completed with {} quads", quads.size()); +} + +void BedMeshPanel::handle_z_zoom_in(lv_event_t *event) +{ + double old_display_scale = z_display_scale; + + // Z+ = ZOOM IN: increase Z visual height only (not color sensitivity) + z_display_scale *= 1.1; // Increase Z display range by 10% + + if (z_display_scale > MAX_Z_SCALE) z_display_scale = MAX_Z_SCALE; + + spdlog::debug("Z+ ZOOM IN: display_scale {} -> {}", + old_display_scale, z_display_scale); + + // Update button states + lv_obj_clear_state(z_zoom_out_btn, LV_STATE_DISABLED); + if (z_display_scale >= MAX_Z_SCALE) { + lv_obj_add_state(z_zoom_in_btn, LV_STATE_DISABLED); + } + + draw_3d_mesh(); +} + +void BedMeshPanel::handle_z_zoom_out(lv_event_t *event) +{ + double old_display_scale = z_display_scale; + + // Z- = ZOOM OUT: reduce Z visual height only (not color sensitivity) + z_display_scale *= 0.9; // Reduce Z display range by 10% + + if (z_display_scale < MIN_Z_SCALE) z_display_scale = MIN_Z_SCALE; + + spdlog::debug("Z- ZOOM OUT: display_scale {} -> {}", + old_display_scale, z_display_scale); + + // Update button states + lv_obj_clear_state(z_zoom_in_btn, LV_STATE_DISABLED); + if (z_display_scale <= MIN_Z_SCALE) { + lv_obj_add_state(z_zoom_out_btn, LV_STATE_DISABLED); + } + + draw_3d_mesh(); +} + +void BedMeshPanel::handle_fov_zoom_in(lv_event_t *event) +{ + double old_camera_distance = camera_distance; + + // FOV+ = ZOOM IN: decrease camera distance for closer view (tighter FOV) + camera_distance *= 0.9; // Decrease distance by 10% + + if (camera_distance < MIN_CAMERA_DISTANCE) camera_distance = MIN_CAMERA_DISTANCE; + + spdlog::debug("FOV+ ZOOM IN: camera_distance {} -> {} (closer view)", + old_camera_distance, camera_distance); + + // Update button states + lv_obj_clear_state(fov_zoom_out_btn, LV_STATE_DISABLED); + if (camera_distance <= MIN_CAMERA_DISTANCE) { + lv_obj_add_state(fov_zoom_in_btn, LV_STATE_DISABLED); + } + + draw_3d_mesh(); +} + +void BedMeshPanel::handle_fov_zoom_out(lv_event_t *event) +{ + double old_camera_distance = camera_distance; + + // FOV- = ZOOM OUT: increase camera distance for wider view (wider FOV) + camera_distance *= 1.1; // Increase distance by 10% + + if (camera_distance > MAX_CAMERA_DISTANCE) camera_distance = MAX_CAMERA_DISTANCE; + + spdlog::debug("FOV- ZOOM OUT: camera_distance {} -> {} (wider view)", + old_camera_distance, camera_distance); + + // Update button states + lv_obj_clear_state(fov_zoom_in_btn, LV_STATE_DISABLED); + if (camera_distance >= MAX_CAMERA_DISTANCE) { + lv_obj_add_state(fov_zoom_out_btn, LV_STATE_DISABLED); + } + + draw_3d_mesh(); +} + + static lv_color_t color_gradient(double offset) { double red_max = 0.25; @@ -478,3 +1596,709 @@ static lv_color_t color_gradient(double offset) return lv_color_make(255, 255, 255); } + +static lv_color_t color_gradient_enhanced(double offset, double min_val, double max_val) +{ + double range = max_val - min_val; + if (range == 0) { + return lv_color_make(0, 255, 0); // Green for flat surface + } + + // Use a dynamic color scale range based on actual mesh data variation + // This automatically adapts to the data: flat beds get high contrast, varied beds get appropriate scaling + double data_range = range; // Full range of actual mesh data + + // Apply compression factor for enhanced visual contrast + // Smaller factor = more dramatic colors for small variations + double adjusted_range = data_range * BedMeshPanel::COLOR_COMPRESSION_FACTOR; + + // Ensure minimum range to prevent division by zero and ensure visible colors + const double min_color_range = 0.01; // mm + adjusted_range = std::max(adjusted_range, min_color_range); + + // static int call_count = 0; + // call_count++; + // if (call_count <= 5 || call_count % 20 == 0) { // Log first 5 calls, then every 20th + // spdlog::debug("color_gradient_enhanced[{}]: data_range={:.3f}mm, compression_factor={}, adjusted_range={:.3f}mm", + // call_count, data_range, BedMeshPanel::COLOR_COMPRESSION_FACTOR, adjusted_range); + // } + + // Center the data range within our color scale + double data_center = (min_val + max_val) / 2.0; + double color_scale_min = data_center - (adjusted_range / 2.0); + + // Map offset to the adjusted color scale + double normalized = (offset - color_scale_min) / adjusted_range; + normalized = std::max(0.0, std::min(1.0, normalized)); // Clamp to [0,1] + + // Create smooth scientific heat map gradient using continuous interpolation + // Classic "jet" colormap: Purple → Blue → Cyan → Green → Yellow → Orange → Red + // High normalized values = high heights = red, low normalized values = low heights = purple + uint8_t r, g, b; + + // Use smooth continuous functions to eliminate banding + // Each transition point is carefully matched to ensure continuity + + // Clamp to ensure we're in valid range + normalized = std::max(0.0, std::min(1.0, normalized)); + + if (normalized < 0.125) { + // Purple to Blue (0.0 to 0.125) + double t = normalized / 0.125; + r = (uint8_t)(128 * (1.0 - t)); // 128 → 0 + g = (uint8_t)(0 + t * 128); // 0 → 128 + b = (uint8_t)(255); // constant blue + } else if (normalized < 0.375) { + // Blue to Cyan (0.125 to 0.375) + double t = (normalized - 0.125) / 0.25; + r = (uint8_t)(0); // constant 0 + g = (uint8_t)(128 + t * 127); // 128 → 255 + b = (uint8_t)(255); // constant blue + } else if (normalized < 0.625) { + // Cyan to Yellow (0.375 to 0.625) + double t = (normalized - 0.375) / 0.25; + r = (uint8_t)(t * 255); // 0 → 255 + g = (uint8_t)(255); // constant green + b = (uint8_t)(255 * (1.0 - t)); // 255 → 0 + } else if (normalized < 0.875) { + // Yellow to Red (0.625 to 0.875) + double t = (normalized - 0.625) / 0.25; + r = (uint8_t)(255); // constant red + g = (uint8_t)(255 * (1.0 - t)); // 255 → 0 + b = (uint8_t)(0); // constant 0 + } else { + // Deep Red (0.875 to 1.0) - keep it pure red + r = (uint8_t)(255); // constant bright red + g = (uint8_t)(0); // keep green at 0 to avoid pink + b = (uint8_t)(0); // keep blue at 0 to avoid pink + } + + // De-saturate all colors by 35% by blending with gray + const double desaturation = 0.35; + uint8_t gray = (uint8_t)((r + g + b) / 3); // Calculate grayscale value + + r = (uint8_t)(r * (1.0 - desaturation) + gray * desaturation); + g = (uint8_t)(g * (1.0 - desaturation) + gray * desaturation); + b = (uint8_t)(b * (1.0 - desaturation) + gray * desaturation); + + return lv_color_make(r, g, b); +} + +void BedMeshPanel::draw_horizontal_axes(double mesh_origin_x, double mesh_origin_y, double grid_reference_z, + int rows, int cols, int canvas_width, int canvas_height, + const lv_draw_line_dsc_t& axis_dsc) +{ + // Draw X-axis (horizontal line extending in X direction from origin) + double x_axis_start = mesh_origin_x; + double x_axis_end = ((cols-1) - cols / 2.0) * MESH_SCALE; // Right edge of mesh + Point3D x_start = project_3d_to_2d(x_axis_start, mesh_origin_y, grid_reference_z, canvas_width, canvas_height); + Point3D x_end = project_3d_to_2d(x_axis_end, mesh_origin_y, grid_reference_z, canvas_width, canvas_height); + + lv_point_t x_line[2] = {{x_start.screen_x, x_start.screen_y}, {x_end.screen_x, x_end.screen_y}}; + lv_canvas_draw_line(mesh_canvas, x_line, 2, &axis_dsc); + + // Draw Y-axis (horizontal line extending in Y direction from origin) + double y_axis_start = mesh_origin_y; + double y_axis_end = ((rows-1) - rows / 2.0) * MESH_SCALE; // Back edge of mesh + Point3D y_start = project_3d_to_2d(mesh_origin_x, y_axis_start, grid_reference_z, canvas_width, canvas_height); + Point3D y_end = project_3d_to_2d(mesh_origin_x, y_axis_end, grid_reference_z, canvas_width, canvas_height); + + lv_point_t y_line[2] = {{y_start.screen_x, y_start.screen_y}, {y_end.screen_x, y_end.screen_y}}; + lv_canvas_draw_line(mesh_canvas, y_line, 2, &axis_dsc); + + // Add X-axis labels along the front edge (max Y) + lv_draw_label_dsc_t label_dsc; + lv_draw_label_dsc_init(&label_dsc); + label_dsc.color = lv_color_white(); + label_dsc.font = &lv_font_montserrat_10; + + double front_y = ((rows-1) - rows / 2.0) * MESH_SCALE; + + // Draw X-axis tick values + draw_axis_tick_values(cols, true, rows, cols, grid_reference_z, canvas_width, canvas_height, label_dsc); + + // Draw X-axis label + lv_draw_label_dsc_t axis_label_dsc; + lv_draw_label_dsc_init(&axis_label_dsc); + axis_label_dsc.color = lv_color_white(); + axis_label_dsc.font = &lv_font_montserrat_10; + + double middle_x_coord = ((cols/2) - cols / 2.0) * MESH_SCALE; + draw_axis_label("X", middle_x_coord, front_y + TICK_MARK_LENGTH * 6, grid_reference_z, -10, 0, canvas_width, canvas_height, axis_label_dsc); + + // Draw Y-axis tick values + draw_axis_tick_values(rows, false, rows, cols, grid_reference_z, canvas_width, canvas_height, label_dsc); + + // Draw Y-axis label + double right_x = ((cols-1) - cols / 2.0) * MESH_SCALE; + double middle_y_coord = ((rows/2) - rows / 2.0) * MESH_SCALE; + draw_axis_label("Y", right_x + TICK_MARK_LENGTH * 6, middle_y_coord, grid_reference_z, 0, -10, canvas_width, canvas_height, axis_label_dsc); + +} + + +void BedMeshPanel::draw_color_gradient_band(int canvas_width, int canvas_height, double min_z, double max_z) +{ + // Calculate gradient band position and dimensions + int band_x = GRADIENT_BAND_MARGIN; + int band_width = GRADIENT_BAND_WIDTH; + int band_height = (canvas_height * GRADIENT_BAND_HEIGHT_RATIO) / 100; + int band_y = (canvas_height - band_height) / 2; // Center vertically + + // Calculate the same extended color range that color_gradient_enhanced uses + double data_range = max_z - min_z; + double adjusted_range = data_range * COLOR_COMPRESSION_FACTOR; + const double min_color_range = 0.01; // mm (same as in color_gradient_enhanced) + adjusted_range = std::max(adjusted_range, min_color_range); + + // Center the data range within our color scale (same logic as color_gradient_enhanced) + double data_center = (min_z + max_z) / 2.0; + double color_scale_min = data_center - (adjusted_range / 2.0); + double color_scale_max = data_center + (adjusted_range / 2.0); + + // Draw the vertical gradient band pixel by pixel + for (int y = 0; y < band_height; y++) { + // Calculate the Z value this pixel represents using the extended color scale range + // Flip the gradient: top should be high Z (color_scale_max), bottom should be low Z (color_scale_min) + double progress = (double)y / (band_height - 1); + double z_value = color_scale_max - progress * (color_scale_max - color_scale_min); // Top = color_scale_max, bottom = color_scale_min + + // Get the color for this Z value using the same function as the mesh + lv_color_t pixel_color = color_gradient_enhanced(z_value, min_z, max_z); + + // Draw a horizontal line at this Y position across the band width + lv_draw_line_dsc_t line_dsc; + lv_draw_line_dsc_init(&line_dsc); + line_dsc.color = pixel_color; + line_dsc.width = 1; + line_dsc.opa = LV_OPA_COVER; + + lv_point_t line_points[2]; + line_points[0].x = band_x; + line_points[0].y = band_y + y; + line_points[1].x = band_x + band_width - 1; + line_points[1].y = band_y + y; + + lv_canvas_draw_line(mesh_canvas, line_points, 2, &line_dsc); + } + + // Add text labels for min and max values + lv_draw_label_dsc_t label_dsc; + lv_draw_label_dsc_init(&label_dsc); + label_dsc.color = lv_color_white(); + label_dsc.opa = LV_OPA_COVER; + label_dsc.font = &lv_font_montserrat_10; + + // Max value label at the top (showing extended color scale max) + char max_label[16]; + snprintf(max_label, sizeof(max_label), "%.2f", color_scale_max); + lv_area_t max_area; + max_area.x1 = band_x + band_width + 5; + max_area.y1 = band_y - 5; + max_area.x2 = max_area.x1 + 40; + max_area.y2 = max_area.y1 + LABEL_HEIGHT; + + if (max_area.x2 < canvas_width && max_area.y1 >= 0) { + lv_canvas_draw_text(mesh_canvas, max_area.x1, max_area.y1, 40, &label_dsc, max_label); + } + + // Min value label at the bottom (showing extended color scale min) + char min_label[16]; + snprintf(min_label, sizeof(min_label), "%.2f", color_scale_min); + lv_area_t min_area; + min_area.x1 = band_x + band_width + 5; + min_area.y1 = band_y + band_height - LABEL_HEIGHT + 5; + min_area.x2 = min_area.x1 + 40; + min_area.y2 = min_area.y1 + LABEL_HEIGHT; + + if (min_area.x2 < canvas_width && min_area.y2 < canvas_height) { + lv_canvas_draw_text(mesh_canvas, min_area.x1, min_area.y1, 40, &label_dsc, min_label); + } +} + +void BedMeshPanel::setup_button_with_label(lv_obj_t* btn, const char* text, int width, int height, lv_event_cb_t callback) +{ + lv_obj_set_size(btn, width, height); + lv_obj_t *label = lv_label_create(btn); + lv_label_set_text(label, text); + lv_obj_center(label); + lv_obj_add_event_cb(btn, callback, LV_EVENT_CLICKED, this); +} + +void BedMeshPanel::draw_grid_lines_parallel_to_axis(int axis_count, bool is_x_axis, int rows, int cols, double grid_reference_z, int canvas_width, int canvas_height, const lv_draw_line_dsc_t& line_dsc) +{ + for (int i = 0; i < axis_count; i++) { + double start_x, start_y, end_x, end_y; + + if (is_x_axis) { + // Draw lines parallel to X-axis (for each column) + start_x = (i - cols / 2.0) * MESH_SCALE; + start_y = (0 - rows / 2.0) * MESH_SCALE; + end_x = (i - cols / 2.0) * MESH_SCALE; + end_y = ((rows-1) - rows / 2.0) * MESH_SCALE; + } else { + // Draw lines parallel to Y-axis (for each row) + start_x = (0 - cols / 2.0) * MESH_SCALE; + start_y = (i - rows / 2.0) * MESH_SCALE; + end_x = ((cols-1) - cols / 2.0) * MESH_SCALE; + end_y = (i - rows / 2.0) * MESH_SCALE; + } + + Point3D start = project_3d_to_2d(start_x, start_y, grid_reference_z, canvas_width, canvas_height); + Point3D end = project_3d_to_2d(end_x, end_y, grid_reference_z, canvas_width, canvas_height); + + + lv_point_t grid_line[2] = {{start.screen_x, start.screen_y}, {end.screen_x, end.screen_y}}; + lv_canvas_draw_line(mesh_canvas, grid_line, 2, &line_dsc); + } +} + +bool BedMeshPanel::create_and_check_label_area(lv_area_t& area, int x, int y, int width, int height, int canvas_width, int canvas_height) +{ + area.x1 = x; + area.y1 = y; + area.x2 = area.x1 + width; + area.y2 = area.y1 + height; + return (area.x1 >= 0 && area.x2 < canvas_width && area.y1 >= 0 && area.y2 < canvas_height); +} + +void BedMeshPanel::extract_mesh_parameter_values(const json& mesh_params, const std::vector& param_names, std::vector& values) +{ + values.clear(); + for (const auto& param : param_names) { + auto v = mesh_params[json::json_pointer(fmt::format("/{}", param))]; + if (!v.is_null()) { + values.push_back(v.template get()); + } + } +} + +double BedMeshPanel::calculate_dynamic_fov_scale(int mesh_rows, int mesh_cols, int canvas_width, int canvas_height) +{ + // Calculate the 2D bounding box of the mesh in world coordinates + double mesh_world_width = (mesh_cols - 1) * MESH_SCALE; + double mesh_world_height = (mesh_rows - 1) * MESH_SCALE; + + // Account for the rotation by using the diagonal as the conservative bound + double mesh_diagonal = sqrt(mesh_world_width * mesh_world_width + mesh_world_height * mesh_world_height); + + // Calculate available canvas space after padding and gradient band + double effective_canvas_width = canvas_width - GRADIENT_BAND_TOTAL_WIDTH; + double available_width = effective_canvas_width * (1.0 - 2.0 * CANVAS_PADDING_RATIO); + double available_height = canvas_height * (1.0 - 2.0 * CANVAS_PADDING_RATIO); + + // Use the smaller dimension to ensure the mesh fits in both directions + double available_space = std::min(available_width, available_height); + + // Calculate the FOV scale needed to fit the mesh diagonal in the available space + // The formula is: fov_scale = (available_pixels * camera_distance) / world_size + double fov_scale = (available_space * CAMERA_DISTANCE) / mesh_diagonal; + + return fov_scale; +} + +void BedMeshPanel::handle_canvas_drag(lv_event_t *event) +{ + lv_event_code_t code = lv_event_get_code(event); + + // Optional debug logging (disabled for now) + // if (code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING || code == LV_EVENT_RELEASED) { + // spdlog::debug("Canvas drag event: {}", (int)code); + // } + + lv_indev_t *indev = lv_indev_get_act(); + if (indev == nullptr) { + spdlog::debug("No active input device"); + return; + } + + + lv_point_t point; + lv_indev_get_point(indev, &point); + + if (code == LV_EVENT_PRESSED) { + // Start drag + is_dragging = true; + drag_start_x = point.x; + last_drag_x = point.x; + drag_start_y = point.y; + last_drag_y = point.y; + // spdlog::debug("Canvas drag started at x={}, y={}", point.x, point.y); + } + else if (code == LV_EVENT_PRESSING && is_dragging) { + // Continue drag - calculate rotation and tilt based on movement + int current_x = point.x; + int current_y = point.y; + int drag_distance_x = current_x - last_drag_x; + int drag_distance_y = current_y - last_drag_y; + + bool mesh_changed = false; + + // Horizontal drag = Z rotation (around vertical axis) + if (abs(drag_distance_x) > 0) { + // Convert horizontal drag distance to rotation angle + // Scale factor: 1 pixel = 0.5 degrees, reversed for natural feel + double z_angle_change = -drag_distance_x * 0.5; + + // Update Z rotation angle (round to nearest integer) + current_view_angle += (int)round(z_angle_change); + + // Wrap angle to 0-359 degrees + while (current_view_angle < 0) current_view_angle += 360; + while (current_view_angle >= 360) current_view_angle -= 360; + + mesh_changed = true; + } + + // Vertical drag = X tilt (tilt up/down) + if (abs(drag_distance_y) > 0) { + // Convert vertical drag distance to tilt angle + // Scale factor: 1 pixel = 0.5 degrees, natural direction (drag up = tilt up) + double x_angle_change = drag_distance_y * 0.5; + + // Update X tilt angle with limits (-90 to -10 degrees for good viewing) + current_x_angle += x_angle_change; + + // Clamp to reasonable viewing limits + const double MIN_X_ANGLE = -85.0; // Don't go too vertical + const double MAX_X_ANGLE = -10.0; // Don't go too horizontal + current_x_angle = std::max(MIN_X_ANGLE, std::min(MAX_X_ANGLE, current_x_angle)); + + mesh_changed = true; + } + + // Update last position + last_drag_x = current_x; + last_drag_y = current_y; + + // Redraw the mesh if any angle changed + if (mesh_changed) { + draw_3d_mesh(); + } + + // if (abs(drag_distance_x) > 0 || abs(drag_distance_y) > 0) { + // spdlog::debug("Canvas drag: x={}, y={}, z_angle={}, x_angle={:.1f}", + // current_x, current_y, current_view_angle, current_x_angle); + // } + } + else if (code == LV_EVENT_RELEASED) { + // End drag + is_dragging = false; + + // Redraw with high-quality gradients now that dragging has stopped + if (show_3d_view && !mesh.empty()) { + draw_3d_mesh(); + } + // spdlog::debug("Canvas drag ended at z_angle={}, x_angle={:.1f}", current_view_angle, current_x_angle); + } +} + +// Helper function to draw axis tick values for X and Y axes +void BedMeshPanel::draw_axis_tick_values(int count, bool is_x_axis, int rows, int cols, double grid_reference_z, + int canvas_width, int canvas_height, lv_draw_label_dsc_t& label_dsc) { + if (is_dragging) return; // Skip during drag for performance + + double edge_coord = is_x_axis ? ((rows-1) - rows / 2.0) * MESH_SCALE // front_y for X-axis + : ((cols-1) - cols / 2.0) * MESH_SCALE; // right_x for Y-axis + + for (int i = 0; i < count; i += 2) { + double coord, actual_value; + Point3D label_pos; + + if (is_x_axis) { + // X-axis: labels along front edge + coord = (i - cols / 2.0) * MESH_SCALE; + actual_value = mesh_min_x + (i * (mesh_max_x - mesh_min_x) / (cols - 1)); + label_pos = project_3d_to_2d(coord, edge_coord + TICK_MARK_LENGTH * 2, grid_reference_z, canvas_width, canvas_height); + } else { + // Y-axis: labels along right edge + coord = (i - rows / 2.0) * MESH_SCALE; + actual_value = mesh_min_y + (i * (mesh_max_y - mesh_min_y) / (rows - 1)); + label_pos = project_3d_to_2d(edge_coord + TICK_MARK_LENGTH * 2, coord, grid_reference_z, canvas_width, canvas_height); + } + + char label_text[16]; + snprintf(label_text, sizeof(label_text), "%.0f", actual_value); + + lv_area_t label_area; + int offset_x = is_x_axis ? -15 : 0; + int offset_y = is_x_axis ? 0 : -5; + + if (create_and_check_label_area(label_area, label_pos.screen_x + offset_x, label_pos.screen_y + offset_y, 30, LABEL_HEIGHT, canvas_width, canvas_height)) { + lv_canvas_draw_text(mesh_canvas, label_area.x1, label_area.y1, 30, &label_dsc, label_text); + } + } +} + +// Helper function to draw Z-axis tick values +void BedMeshPanel::draw_z_axis_tick_values(double left_x, double front_y, double z_min, double z_max, + int z_divisions, double z_scale, int canvas_width, int canvas_height, + lv_draw_label_dsc_t& label_dsc, double min_z, double max_z) { + if (is_dragging) return; // Skip during drag for performance + + for (int z_div = 0; z_div <= z_divisions; z_div += 2) { + double z_level = z_min + (z_div * (z_max - z_min) / z_divisions); + double actual_z = (z_level / z_scale / Z_AXIS_EXTENSION + (min_z + max_z) / 2.0); + + Point3D label_pos = project_3d_to_2d(left_x - TICK_MARK_LENGTH * 2, front_y, z_level, canvas_width, canvas_height); + + char label_text[16]; + snprintf(label_text, sizeof(label_text), "%.2f", actual_z); + + lv_area_t label_area; + if (create_and_check_label_area(label_area, label_pos.screen_x - 20, label_pos.screen_y - 5, 40, LABEL_HEIGHT, canvas_width, canvas_height)) { + lv_canvas_draw_text(mesh_canvas, label_area.x1, label_area.y1, 40, &label_dsc, label_text); + } + } +} + +// Helper function to draw main axis labels (X, Y, Z) +void BedMeshPanel::draw_axis_label(const char* label_text, double x, double y, double z, int offset_x, int offset_y, + int canvas_width, int canvas_height, lv_draw_label_dsc_t& label_dsc) { + Point3D label_pos = project_3d_to_2d(x, y, z, canvas_width, canvas_height); + + lv_area_t label_area; + if (create_and_check_label_area(label_area, label_pos.screen_x + offset_x, label_pos.screen_y + offset_y, 20, LABEL_HEIGHT, canvas_width, canvas_height)) { + lv_canvas_draw_text(mesh_canvas, label_area.x1, label_area.y1, 20, &label_dsc, label_text); + } +} + +void BedMeshPanel::handle_toggle_view(lv_event_t *event) { + if (lv_event_get_code(event) == LV_EVENT_CLICKED) { + show_3d_fullscreen(); + } +} + +// Table view: mesh values + profile list + all controls along the bottom. +// This is the default state and uses the original vertical layout. +void BedMeshPanel::show_table_view() { + show_3d_view = false; + + // Vertical layout: top area over a bottom controls row. + lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); + + lv_obj_set_width(top_cont, LV_PCT(100)); + lv_obj_set_height(top_cont, LV_SIZE_CONTENT); + + // Zoom controls belong back inside the display area (hidden in table view). + lv_obj_set_parent(rotation_cont, display_cont); + lv_obj_set_flex_flow(rotation_cont, LV_FLEX_FLOW_ROW); + + lv_obj_set_size(controls_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(controls_cont, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(controls_cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_grow(back_btn.get_container(), 1); + + lv_obj_clear_flag(mesh_table, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(mesh_canvas, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(rotation_cont, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(profile_cont, LV_OBJ_FLAG_HIDDEN); + + lv_obj_clear_flag(save_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(clear_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(calibrate_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(toggle_view_btn.get_container(), LV_OBJ_FLAG_HIDDEN); +} + +// Fullscreen 3D: canvas fills the left, a narrow right-side strip holds the +// zoom +/- and Back buttons. The profile list and other controls are hidden. +void BedMeshPanel::show_3d_fullscreen() { + show_3d_view = true; + + // Horizontal layout: [ canvas (grows) ][ control strip ]. + lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW); + + lv_obj_set_width(top_cont, LV_SIZE_CONTENT); // flex_grow fills the width + lv_obj_set_height(top_cont, LV_PCT(100)); + + // Right strip: zoom controls at the top, Back at the bottom. + lv_obj_set_size(controls_cont, LV_SIZE_CONTENT, LV_PCT(100)); + lv_obj_set_flex_flow(controls_cont, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(controls_cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_grow(back_btn.get_container(), 0); + + lv_obj_set_parent(rotation_cont, controls_cont); + lv_obj_set_flex_flow(rotation_cont, LV_FLEX_FLOW_COLUMN); + lv_obj_move_to_index(rotation_cont, 0); + + lv_obj_add_flag(mesh_table, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(mesh_canvas, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(rotation_cont, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(profile_cont, LV_OBJ_FLAG_HIDDEN); + + lv_obj_add_flag(save_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(clear_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(calibrate_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(toggle_view_btn.get_container(), LV_OBJ_FLAG_HIDDEN); + + // Recompute layout so the canvas resize fires against the new (larger) area. + lv_obj_update_layout(cont); + resize_canvas(); + if (!mesh.empty()) { + draw_3d_mesh(); + } +} + +bool BedMeshPanel::detect_multitouch_support() +{ +#ifdef SIMULATOR + return false; // Simulation mode doesn't support multi-touch +#else + // Check if evdev device supports multi-touch + int fd = open("/dev/input/event0", O_RDONLY); + if (fd < 0) { + spdlog::debug("Cannot open input device for multi-touch detection"); + return false; + } + + unsigned long abs_bits[NLONGS(ABS_CNT)] = {0}; + bool has_mt = false; + + if (ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(abs_bits)), abs_bits) >= 0) { + // Check for multi-touch absolute events + has_mt = test_bit(ABS_MT_POSITION_X, abs_bits) && test_bit(ABS_MT_POSITION_Y, abs_bits); + } + + close(fd); + spdlog::debug("Multi-touch support detected: {}", has_mt); + return has_mt; +#endif +} + +void BedMeshPanel::handle_canvas_scroll(lv_event_t *event) +{ + if (!show_3d_view) { + spdlog::debug("handle_canvas_scroll: not in 3D view, ignoring"); + return; + } + +#ifdef SIMULATOR + spdlog::debug("handle_canvas_scroll: checking for encoder input in simulation mode"); + + // In simulation mode, check for encoder (mouse wheel) input + // We need to find the encoder input device and check for changes + lv_indev_t *encoder_indev = nullptr; + lv_indev_t *indev = lv_indev_get_next(nullptr); + int indev_count = 0; + + // Find the encoder input device (mouse wheel) + while (indev) { + lv_indev_type_t type = lv_indev_get_type(indev); + spdlog::debug("Found input device {}: type={}", indev_count, (int)type); + + if (type == LV_INDEV_TYPE_ENCODER) { + encoder_indev = indev; + spdlog::debug("Found encoder device at index {}", indev_count); + break; + } + indev = lv_indev_get_next(indev); + indev_count++; + } + + if (!encoder_indev) { + spdlog::debug("No encoder input device found after checking {} devices", indev_count); + return; + } + + // Check if encoder has new data + lv_indev_data_t data; + encoder_indev->driver->read_cb(encoder_indev->driver, &data); + + spdlog::debug("Encoder data: enc_diff={}, state={}", data.enc_diff, (int)data.state); + + if (data.enc_diff != 0) { + double old_scale = z_display_scale; + spdlog::debug("Mouse wheel detected: enc_diff={}, current_scale={}", data.enc_diff, old_scale); + + if (data.enc_diff > 0) { + // Scroll up = zoom in + z_display_scale *= 1.1; + if (z_display_scale > MAX_Z_SCALE) z_display_scale = MAX_Z_SCALE; + spdlog::debug("Zoom IN: {} -> {} (clamped to max {})", old_scale, z_display_scale, MAX_Z_SCALE); + } else if (data.enc_diff < 0) { + // Scroll down = zoom out + z_display_scale *= 0.9; + if (z_display_scale < MIN_Z_SCALE) z_display_scale = MIN_Z_SCALE; + spdlog::debug("Zoom OUT: {} -> {} (clamped to min {})", old_scale, z_display_scale, MIN_Z_SCALE); + } + + if (old_scale != z_display_scale) { + spdlog::debug("Scale changed, redrawing 3D mesh"); + draw_3d_mesh(); + } else { + spdlog::debug("Scale unchanged (hit limit), no redraw needed"); + } + } else { + spdlog::debug("No encoder movement detected"); + } +#else + spdlog::debug("handle_canvas_scroll: not in simulation mode, ignoring"); +#endif +} + +void BedMeshPanel::scroll_check_timer_callback() +{ +#ifdef SIMULATOR + if (!show_3d_view || !use_gesture_zoom) { + return; + } + + // Don't process scroll events if the panel is not visible + if (!cont || lv_obj_has_flag(cont, LV_OBJ_FLAG_HIDDEN)) { + return; + } + + // Find the encoder input device (mouse wheel) + lv_indev_t *encoder_indev = nullptr; + lv_indev_t *indev = lv_indev_get_next(nullptr); + + while (indev) { + if (lv_indev_get_type(indev) == LV_INDEV_TYPE_ENCODER) { + encoder_indev = indev; + break; + } + indev = lv_indev_get_next(indev); + } + + if (!encoder_indev) return; + + // Check if encoder has new data + lv_indev_data_t data; + encoder_indev->driver->read_cb(encoder_indev->driver, &data); + + if (data.enc_diff != 0) { + double old_scale = z_display_scale; + spdlog::debug("TIMER: Mouse wheel detected: enc_diff={}, current_scale={}", data.enc_diff, old_scale); + + if (data.enc_diff > 0) { + // Positive enc_diff = zoom out + z_display_scale *= 0.9; + if (z_display_scale < MIN_Z_SCALE) z_display_scale = MIN_Z_SCALE; + spdlog::debug("TIMER: Zoom OUT: {} -> {} (clamped to min {})", old_scale, z_display_scale, MIN_Z_SCALE); + } else if (data.enc_diff < 0) { + // Negative enc_diff = zoom in + z_display_scale *= 1.1; + if (z_display_scale > MAX_Z_SCALE) z_display_scale = MAX_Z_SCALE; + spdlog::debug("TIMER: Zoom IN: {} -> {} (clamped to max {})", old_scale, z_display_scale, MAX_Z_SCALE); + } + + if (old_scale != z_display_scale) { + spdlog::debug("TIMER: Scale changed, redrawing 3D mesh"); + draw_3d_mesh(); + } + } +#endif +} + +void BedMeshPanel::handle_canvas_gesture(lv_event_t *event) +{ + if (!show_3d_view) return; // Only zoom in 3D view mode + + lv_indev_t * indev = lv_indev_get_act(); + if (!indev) return; + + // TODO: Implement pinch gesture detection + // This requires LVGL gesture recognition to be enabled + // and proper multi-touch input processing + spdlog::debug("Gesture event detected (pinch zoom not yet implemented)"); +} diff --git a/src/bedmesh_panel.h b/src/bedmesh_panel.h index 23b2d04..436954d 100644 --- a/src/bedmesh_panel.h +++ b/src/bedmesh_panel.h @@ -5,6 +5,7 @@ #include "notify_consumer.h" #include "button_container.h" #include "lvgl/lvgl.h" +#include "spdlog/spdlog.h" #include #include @@ -25,6 +26,133 @@ class BedMeshPanel : public NotifyConsumer { void handle_prompt_cancel(lv_event_t *event); void handle_kb_input(lv_event_t *e); void mesh_draw_cb(lv_event_t *e); + void draw_3d_mesh(); + void resize_canvas(); + void handle_z_zoom_in(lv_event_t *event); + void handle_z_zoom_out(lv_event_t *event); + void handle_fov_zoom_in(lv_event_t *event); + void handle_fov_zoom_out(lv_event_t *event); + void handle_canvas_drag(lv_event_t *event); + void handle_toggle_view(lv_event_t *event); + // Table view is the default; the 3D view is a fullscreen mode (canvas + Back only). + void show_table_view(); + void show_3d_fullscreen(); + void handle_canvas_scroll(lv_event_t *event); + void handle_canvas_gesture(lv_event_t *event); + void scroll_check_timer_callback(); + bool detect_multitouch_support(); + + // Basic 3D point with screen projection for mesh vertices + struct Point3D { + double x, y, z; // 3D world coordinates + int screen_x, screen_y; // 2D screen coordinates after perspective projection + double depth; // Z-depth for sorting (closer objects rendered last) + }; + + // 3D vertex with color information for smooth interpolation + struct Vertex3D { + double x, y, z; // 3D position in world space + lv_color_t color; // Color at this vertex (for gradient blending) + }; + + // 3D quadrilateral representing one mesh cell with depth sorting + struct Quad3D { + Vertex3D vertices[4]; // Four corners of the quad (bottom-left, bottom-right, top-left, top-right) + double avg_depth; // Average depth for back-to-front sorting + lv_color_t center_color; // Fallback color if vertex blending not used + }; + + void draw_mesh_wireframe(const std::vector>& points, + int rows, int cols, int canvas_width, int canvas_height); + void draw_axes_and_labels(const std::vector>& points, + int rows, int cols, int canvas_width, int canvas_height, + double min_z, double max_z, double z_scale); + void draw_mesh_surface(const std::vector>& points, + int rows, int cols, int canvas_width, int canvas_height, + double min_z, double max_z); + + // 3D rendering pipeline functions + Point3D project_3d_to_2d(double x, double y, double z, int canvas_width, int canvas_height); // Perspective projection + void draw_3d_quad(const Quad3D& quad, int canvas_width, int canvas_height); // Render a single 3D quad + std::vector generate_mesh_quads(int rows, int cols, double min_z, double max_z); // Create 3D quads from mesh data + void sort_quads_by_depth(std::vector& quads); // Z-buffer sorting (back to front) + + // Triangle rasterization for proper 3D shapes + void fill_triangle(int x1, int y1, int x2, int y2, int x3, int y3, lv_color_t color, int canvas_width, int canvas_height); + + // Triangle rasterization with gradient interpolation between vertex colors + // Parameters: (x1,y1,c1), (x2,y2,c2), (x3,y3,c3) are the three vertices with positions and colors + void fill_triangle_gradient(int x1, int y1, lv_color_t c1, int x2, int y2, lv_color_t c2, int x3, int y3, lv_color_t c3, int canvas_width, int canvas_height); + + // Color gradient band rendering + void draw_color_gradient_band(int canvas_width, int canvas_height, double min_z, double max_z); + + // Axis drawing helper methods + void draw_horizontal_axes(double mesh_origin_x, double mesh_origin_y, double grid_reference_z, + int rows, int cols, int canvas_width, int canvas_height, + const lv_draw_line_dsc_t& axis_dsc); + + + // Helper methods for reducing code duplication + void setup_button_with_label(lv_obj_t* btn, const char* text, int width, int height, lv_event_cb_t callback); + void draw_grid_lines_parallel_to_axis(int axis_count, bool is_x_axis, int rows, int cols, double grid_reference_z, int canvas_width, int canvas_height, const lv_draw_line_dsc_t& line_dsc); + bool create_and_check_label_area(lv_area_t& area, int x, int y, int width, int height, int canvas_width, int canvas_height); + void extract_mesh_parameter_values(const json& mesh_params, const std::vector& param_names, std::vector& values); + double calculate_dynamic_fov_scale(int mesh_rows, int mesh_cols, int canvas_width, int canvas_height); + + // Axis tick drawing helper functions + void draw_axis_tick_values(int count, bool is_x_axis, int rows, int cols, double grid_reference_z, + int canvas_width, int canvas_height, lv_draw_label_dsc_t& label_dsc); + void draw_z_axis_tick_values(double left_x, double front_y, double z_min, double z_max, + int z_divisions, double z_scale, int canvas_width, int canvas_height, + lv_draw_label_dsc_t& label_dsc, double min_z, double max_z); + void draw_axis_label(const char* label_text, double x, double y, double z, int offset_x, int offset_y, + int canvas_width, int canvas_height, lv_draw_label_dsc_t& label_dsc); + + // Rendering constants + static constexpr int CULLING_MARGIN = 50; // Pixels beyond canvas bounds to include for triangle culling + static constexpr double MESH_SCALE = 50.0; // Base spacing between mesh points in 3D world units + static constexpr double DEFAULT_Z_SCALE = 60.0; // Height amplification factor for dramatic Z-axis visualization (increased for better visibility) + + // Dynamic Z-scaling constants for adaptive height visualization + static constexpr double DEFAULT_Z_TARGET_HEIGHT = 80.0; // Target height range in world units for good visual impact + static constexpr double DEFAULT_Z_MIN_SCALE = 35.0; // Minimum Z-scale factor to prevent flat surfaces + static constexpr double DEFAULT_Z_MAX_SCALE = 120.0; // Maximum Z-scale factor to prevent extreme projections off-screen + + // 3D projection constants for camera positioning and field of view + static constexpr double CAMERA_DISTANCE = 450.0; // Distance of camera from origin (affects perspective strength) + static constexpr double CANVAS_PADDING_RATIO = 0.1; // Padding as ratio of canvas size (0.1 = 10% padding on each side) + static constexpr double VIEW_ANGLE_X_DEGREES = -85.0; // Initial X-axis rotation (-85 degrees) for optimal perspective + static constexpr double VIEW_ANGLE_Z_DEGREES = 10.0; // Initial Z-axis rotation (10 degrees) for optimal perspective + static constexpr double Z_ORIGIN_VERTICAL_POSITION = 0.4; // Position of Z=0 plane in canvas (0.0=top, 0.4=upper center, 1.0=bottom) + + // Z-axis range constants for 3D visualization + static constexpr double Z_AXIS_EXTENSION = 0.75; // How far to extend Z-axis beyond actual data range (as multiplier of z_range) + + // Color gradient constants for enhanced visual contrast + static constexpr double COLOR_COMPRESSION_FACTOR = 0.8; // Fraction of data range used for color mapping (smaller = more dramatic colors) + + // Axis label positioning constants + static constexpr int AXIS_LABEL_OFFSET_X = 5; // Horizontal offset of axis labels from axis line (pixels) + static constexpr int AXIS_LABEL_OFFSET_Y = 20; // Vertical offset of axis labels from axis line (pixels) - increased for better visibility + static constexpr int AXIS_LABEL_MARGIN = 15; // Additional margin for axis labels to avoid canvas edge (pixels) + + // Axis tick mark constants + static constexpr int TICK_MARK_LENGTH = 6; // Length of perpendicular tick marks (pixels) + static constexpr int TICK_LABEL_OFFSET = 12; // Distance of tick labels from axis line (pixels) + + // Label height constant + static constexpr int LABEL_HEIGHT = 15; // Standard height for text labels (pixels) + + // Color gradient band constants + static constexpr int GRADIENT_BAND_WIDTH = 20; // Width of the vertical color gradient band (pixels) + static constexpr int GRADIENT_BAND_MARGIN = 10; // Margin from left edge of canvas (pixels) + static constexpr int GRADIENT_BAND_HEIGHT_RATIO = 80; // Percentage of canvas height to use for gradient band + static constexpr int GRADIENT_BAND_TOTAL_WIDTH = 85; // Total space reserved for gradient band (band + margin + labels) + + // Triangle gradient performance constants + static constexpr int GRADIENT_MAX_SEGMENTS = 6; // Maximum segments per scanline for gradient approximation + static constexpr int GRADIENT_MIN_LINE_WIDTH = 3; // Minimum line width to use gradient (shorter lines use solid color) static void _handle_callback(lv_event_t *event) { BedMeshPanel *panel = (BedMeshPanel*)event->user_data; @@ -56,12 +184,67 @@ class BedMeshPanel : public NotifyConsumer { panel->mesh_draw_cb(e); }; + static void _handle_z_zoom_in(lv_event_t *event) { + spdlog::debug("_handle_z_zoom_in static wrapper called"); + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_z_zoom_in(event); + }; + + static void _handle_z_zoom_out(lv_event_t *event) { + spdlog::debug("_handle_z_zoom_out static wrapper called"); + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_z_zoom_out(event); + }; + + static void _handle_fov_zoom_in(lv_event_t *event) { + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_fov_zoom_in(event); + }; + + static void _handle_fov_zoom_out(lv_event_t *event) { + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_fov_zoom_out(event); + }; + + static void _handle_canvas_drag(lv_event_t *event) { + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_canvas_drag(event); + }; + + static void _handle_canvas_scroll(lv_event_t *event) { + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_canvas_scroll(event); + }; + + static void _handle_canvas_gesture(lv_event_t *event) { + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_canvas_gesture(event); + }; + + static void _scroll_check_timer_cb(lv_timer_t *timer) { + BedMeshPanel *panel = (BedMeshPanel*)timer->user_data; + panel->scroll_check_timer_callback(); + }; + + static void _handle_toggle_view(lv_event_t *event) { + BedMeshPanel *panel = (BedMeshPanel*)event->user_data; + panel->handle_toggle_view(event); + }; + + private: KWebSocketClient &ws; lv_obj_t *cont; lv_obj_t *prompt; lv_obj_t *top_cont; + lv_obj_t *display_cont; lv_obj_t *mesh_table; + lv_obj_t *mesh_canvas; + lv_obj_t *rotation_cont; + lv_obj_t *z_zoom_in_btn; + lv_obj_t *z_zoom_out_btn; + lv_obj_t *fov_zoom_in_btn; + lv_obj_t *fov_zoom_out_btn; lv_obj_t *profile_cont; lv_obj_t *profile_table; lv_obj_t *profile_info; @@ -69,12 +252,35 @@ class BedMeshPanel : public NotifyConsumer { ButtonContainer save_btn; ButtonContainer clear_btn; ButtonContainer calibrate_btn; + ButtonContainer toggle_view_btn; ButtonContainer back_btn; lv_obj_t *msgbox; lv_obj_t *input; lv_obj_t *kb; std::string active_profile; std::vector> mesh; + int current_view_angle; + void *canvas_draw_buf; + bool show_3d_view; + double z_display_scale; + double fov_scale; + double camera_distance; + double mesh_min_x; + double mesh_max_x; + double mesh_min_y; + double mesh_max_y; + + // Touch drag state for rotation control + bool is_dragging; + int drag_start_x; + int last_drag_x; + int drag_start_y; + int last_drag_y; + double current_x_angle; // Current X-axis viewing angle (tilt up/down) + bool has_multitouch; + bool use_gesture_zoom; + bool mouse_over_canvas; + lv_timer_t* scroll_check_timer; }; #endif // __BEDMESH_PANEL_H__ diff --git a/src/belts_calibration_panel.cpp b/src/belts_calibration_panel.cpp index 31619c1..dbf8c96 100644 --- a/src/belts_calibration_panel.cpp +++ b/src/belts_calibration_panel.cpp @@ -77,7 +77,7 @@ BeltsCalibrationPanel::BeltsCalibrationPanel(KWebSocketClient &c, std::mutex &l) lv_obj_add_event_cb(excite_slider, &BeltsCalibrationPanel::_handle_update_slider, LV_EVENT_VALUE_CHANGED, this); - lv_obj_align_to(excite_label, excite_slider, LV_ALIGN_BOTTOM_MID, 0, 35 * hscale); + lv_obj_align(excite_label, LV_ALIGN_TOP_RIGHT, 0, 2 * hscale); lv_label_set_text(excite_label, "1 hz"); lv_dropdown_set_options(excite_dd, fmt::format("{}", fmt::join(axes, "\n")).c_str()); diff --git a/src/button_container.cpp b/src/button_container.cpp index cb9b5d1..6df9637 100644 --- a/src/button_container.cpp +++ b/src/button_container.cpp @@ -65,6 +65,13 @@ ButtonContainer::ButtonContainer(lv_obj_t *parent, lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_color(label, lv_palette_darken(LV_PALETTE_GREY, 1), LV_STATE_DISABLED); + /* On small screens (e.g. 480x272 KE) the theme's default m12 truncates 7-8 char + * button labels like "Spoolman"/"Cooldown"/"Reload"/"Modified". Use m10 globally + * so all panels get readable, non-wrapping labels without each having to override. */ + if (lv_disp_get_physical_hor_res(NULL) <= 480) { + lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0); + } + // lv_obj_align_to(label, btn, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); // lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, 5); // lv_obj_set_style_border_width(btn_cont, 2, 0); @@ -81,6 +88,14 @@ lv_obj_t *ButtonContainer::get_button() { return btn; } +lv_obj_t *ButtonContainer::get_label() { + return label; +} + +void ButtonContainer::set_text(const char *text) { + lv_label_set_text(label, text); +} + void ButtonContainer::disable() { lv_obj_add_state(btn, LV_STATE_DISABLED); lv_obj_add_state(btn_cont, LV_STATE_DISABLED); diff --git a/src/button_container.h b/src/button_container.h index 16e546d..09af3e8 100644 --- a/src/button_container.h +++ b/src/button_container.h @@ -19,11 +19,13 @@ class ButtonContainer { lv_obj_t *get_container(); lv_obj_t *get_button(); + lv_obj_t *get_label(); void disable(); void enable(); void hide(); void set_image(const void *img); + void set_text(const char *text); void handle_callback(lv_event_t *event); diff --git a/src/config.cpp b/src/config.cpp index b8bd873..3d48d2b 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -52,8 +52,8 @@ void Config::init(std::string config_path, const std::string thumbdir) { {"color", "purple"} }, { - {"id", "temperature_sensor chamber_temp"}, - {"display_name", "Chamber"}, + {"id", "temperature_sensor mcu_temp"}, + {"display_name", "MCU Temp"}, {"controllable", false}, {"color", "blue"} } diff --git a/src/console_panel.cpp b/src/console_panel.cpp index 54868b8..e940e95 100644 --- a/src/console_panel.cpp +++ b/src/console_panel.cpp @@ -1,5 +1,6 @@ #include "console_panel.h" #include "state.h" +#include "utils.h" #include "spdlog/spdlog.h" #include @@ -42,7 +43,7 @@ ConsolePanel::ConsolePanel(KWebSocketClient &websocket_client, std::mutex &lock, lv_obj_set_size(input_cont, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_t *send_btn = lv_btn_create(input_cont); - lv_obj_set_style_text_font(send_btn, &lv_font_montserrat_16, LV_STATE_DEFAULT); + lv_obj_set_style_text_font(send_btn, &lv_font_montserrat_10, LV_STATE_DEFAULT); lv_obj_set_width(send_btn, 100); lv_obj_t *send_btn_label = lv_label_create(send_btn); lv_label_set_text(send_btn_label, LV_SYMBOL_NEW_LINE); @@ -50,7 +51,7 @@ ConsolePanel::ConsolePanel(KWebSocketClient &websocket_client, std::mutex &lock, lv_obj_add_event_cb(send_btn , &ConsolePanel::_handle_send_macro, LV_EVENT_CLICKED, this); lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_text_font(kb, &lv_font_montserrat_16, LV_STATE_DEFAULT); + lv_obj_set_style_text_font(kb, &lv_font_montserrat_10, LV_STATE_DEFAULT); lv_obj_add_event_cb(input, &ConsolePanel::_handle_kb_input, LV_EVENT_ALL, this); lv_obj_set_size(macro_list, LV_PCT(40), LV_PCT(100)); @@ -61,7 +62,7 @@ ConsolePanel::ConsolePanel(KWebSocketClient &websocket_client, std::mutex &lock, lv_obj_set_scroll_dir(macro_list, LV_DIR_TOP | LV_DIR_BOTTOM); lv_obj_t *label = lv_label_create(input); - lv_obj_set_style_text_font(label, &lv_font_montserrat_16, LV_STATE_DEFAULT); + lv_obj_set_style_text_font(label, &lv_font_montserrat_10, LV_STATE_DEFAULT); lv_label_set_text(label, " " LV_SYMBOL_CLOSE " "); lv_obj_align(label, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_flag(label, LV_OBJ_FLAG_CLICKABLE); @@ -133,10 +134,14 @@ void ConsolePanel::handle_kb_input(lv_event_t *e) return; } - lv_textarea_add_text(output,"> "); - lv_textarea_add_text(output, cmd); - lv_textarea_add_text(output,"\n"); - ws.gcode_script(cmd); + std::string command(cmd); + KUtils::confirm_if_printing("Printer is printing.\nSend this command anyway?", + [this, command]() { + lv_textarea_add_text(output, "> "); + lv_textarea_add_text(output, command.c_str()); + lv_textarea_add_text(output, "\n"); + ws.gcode_script(command); + }); if (!history.empty()) { const auto &front = history.front(); diff --git a/src/extruder_panel.cpp b/src/extruder_panel.cpp index d59b33e..16f94e6 100644 --- a/src/extruder_panel.cpp +++ b/src/extruder_panel.cpp @@ -71,12 +71,15 @@ ExtruderPanel::ExtruderPanel(KWebSocketClient &websocket_client, lv_obj_set_flex_flow(rightside_btns_cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(rightside_btns_cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_clear_flag(rightside_btns_cont, LV_OBJ_FLAG_SCROLLABLE); + /* Zero padding so the inner buttons (LV_PCT(100)) actually fill the column. */ + lv_obj_set_style_pad_all(rightside_btns_cont, 0, 0); lv_obj_set_size(leftside_btns_cont, LV_PCT(20), LV_PCT(100)); // lv_obj_set_style_pad_row(leftside_btns_cont, 15, 0); lv_obj_set_flex_flow(leftside_btns_cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(leftside_btns_cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_clear_flag(leftside_btns_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(leftside_btns_cont, 0, 0); lv_obj_set_flex_grow(load_btn.get_container(), 1); lv_obj_set_flex_grow(unload_btn.get_container(), 1); @@ -86,11 +89,27 @@ ExtruderPanel::ExtruderPanel(KWebSocketClient &websocket_client, lv_obj_set_flex_grow(retract_btn.get_container(), 1); lv_obj_set_flex_grow(back_btn.get_container(), 1); + /* Stretch each button to fill its flex column. ButtonContainer's default + * 110*width_scale (~66 px on 480-wide) is too narrow for 7-8 char labels + * like "Spoolman" / "Cooldown" / "Extrude" — they truncate or wrap. */ + lv_obj_t *btns[] = { + load_btn.get_container(), unload_btn.get_container(), cooldown_btn.get_container(), + spoolman_btn.get_container(), extrude_btn.get_container(), + retract_btn.get_container(), back_btn.get_container() + }; + for (auto *b : btns) { + lv_obj_set_width(b, lv_pct(100)); + } + /* Note: ButtonContainer applies the small-screen m10 font globally. */ + spoolman_btn.disable(); static lv_coord_t grid_main_row_dsc[] = {LV_GRID_FR(3), LV_GRID_FR(6), LV_GRID_FR(6), LV_GRID_FR(6), LV_GRID_TEMPLATE_LAST}; - static lv_coord_t grid_main_col_dsc[] = {LV_GRID_FR(2), LV_GRID_FR(7), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST}; + /* FR(2)/FR(8)/FR(2): side cols ~80 px (still room for 8-char labels with + * pad_all=0 on the side containers) and middle ~320 px so each of the 7 + * selector pills gets ~45 px — a usable touch target on the KE display. */ + static lv_coord_t grid_main_col_dsc[] = {LV_GRID_FR(2), LV_GRID_FR(8), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST}; lv_obj_clear_flag(panel_cont, LV_OBJ_FLAG_SCROLLABLE); diff --git a/src/file_panel.cpp b/src/file_panel.cpp index 6926457..7174b4a 100644 --- a/src/file_panel.cpp +++ b/src/file_panel.cpp @@ -41,10 +41,12 @@ FilePanel::FilePanel(lv_obj_t *parent) lv_label_set_long_mode(fname_label, LV_LABEL_LONG_SCROLL); lv_obj_set_width(fname_label, LV_PCT(100)); lv_obj_set_style_pad_all(fname_label, 0, 0); + lv_obj_set_style_text_font(fname_label, &lv_font_montserrat_10, 0); // Setup detail label lv_obj_set_width(detail_label, LV_PCT(100)); lv_obj_set_style_pad_all(detail_label, 0, 0); + lv_obj_set_style_text_font(detail_label, &lv_font_montserrat_10, 0); } FilePanel::~FilePanel() { diff --git a/src/finetune_panel.cpp b/src/finetune_panel.cpp index 05d18e1..366a4c9 100644 --- a/src/finetune_panel.cpp +++ b/src/finetune_panel.cpp @@ -39,10 +39,10 @@ FineTunePanel::FineTunePanel(KWebSocketClient &websocket_client, std::mutex &l) {"0.01", "0.05", "0.10", ""}, 0, 30, 15, &FineTunePanel::_handle_callback, this) , multipler_selector(panel_cont, "Multipler Step (%)", {"1", "5", "10", "25", ""}, 0, 40, 15, &FineTunePanel::_handle_callback, this) - , z_offset(values_cont, &home_z, 150, 100, 15, "0.0 mm") - , pa(values_cont, &pa_plus_img, 150, 100, 15, "0.0 mm/s") - , speed_factor(values_cont, &speed_up_img, 150, 100, 15, "100%") - , flow_factor(values_cont, &flow_up_img, 150, 100, 15, "100%") + , z_offset(values_cont, &home_z, 100, 100, 15, "0.0 mm") + , pa(values_cont, &pa_plus_img, 100, 100, 15, "0.0 mm/s") + , speed_factor(values_cont, &speed_up_img, 100, 100, 15, "100%") + , flow_factor(values_cont, &flow_up_img, 100, 100, 15, "100%") { lv_obj_move_background(panel_cont); @@ -103,13 +103,13 @@ void FineTunePanel::foreground() { auto v = State::get_instance()->get_data( "/printer_state/gcode_move/homing_origin/2"_json_pointer); if (!v.is_null()) { - z_offset.update_label(fmt::format("{:.5} mm", v.template get()).c_str()); + z_offset.update_label(fmt::format("{:.3f} mm", v.template get()).c_str()); } v = State::get_instance()->get_data( "/printer_state/extruder/pressure_advance"_json_pointer); if (!v.is_null()) { - pa.update_label(fmt::format("{:.5} mm/s", v.template get()).c_str()); + pa.update_label(fmt::format("{:.3f} mm/s", v.template get()).c_str()); } v = State::get_instance()->get_data( @@ -133,12 +133,12 @@ void FineTunePanel::consume(json &j) { std::lock_guard lock(lv_lock); auto v = j["/params/0/gcode_move/homing_origin/2"_json_pointer]; if (!v.is_null()) { - z_offset.update_label(fmt::format("{:.5} mm", v.template get()).c_str()); + z_offset.update_label(fmt::format("{:.3f} mm", v.template get()).c_str()); } v = j["/params/0/extruder/pressure_advance"_json_pointer]; if (!v.is_null()) { - pa.update_label(fmt::format("{:.5} mm/s", v.template get()).c_str()); + pa.update_label(fmt::format("{:.3f} mm/s", v.template get()).c_str()); } v = j["/params/0/gcode_move/speed_factor"_json_pointer]; diff --git a/src/guppyscreen.cpp b/src/guppyscreen.cpp index 0eb6e93..dfa1955 100644 --- a/src/guppyscreen.cpp +++ b/src/guppyscreen.cpp @@ -113,6 +113,13 @@ GuppyScreen *GuppyScreen::init(std::function hal_i lv_style_init(&style_container); lv_style_set_border_width(&style_container, 0); lv_style_set_radius(&style_container, 0); + /* Force m10 on all lv_obj instances on small screens. Theme cascade alone + * doesn't reach plain lv_label children of lv_btn etc.; setting text_font + * on style_container (applied to every obj via theme_apply_cb) propagates + * via LV_STYLE_PROP_INHERIT to descendant labels too. */ + if (lv_disp_get_physical_hor_res(NULL) <= 480) { + lv_style_set_text_font(&style_container, &lv_font_montserrat_10); + } // lv_style_init(&style_imgbtn_default); // lv_style_set_img_recolor_opa(&style_imgbtn_default, LV_OPA_100); @@ -148,14 +155,21 @@ GuppyScreen *GuppyScreen::init(std::function hal_i conf->get(conf->df() + "moonraker_host"), conf->get(conf->df() + "moonraker_port")); +#ifdef SIMULATOR + /* Don't actually connect in simulator mode — the failing connect's + * disconnected callback re-shows the init overlay over the mock data. */ + spdlog::info("simulator mode — skipping printer websocket connection to {}", ws_url); +#else spdlog::info("connecting to printer at {}", ws_url); gs->connect_ws(ws_url); +#endif } #ifndef OS_ANDROID screen_saver = lv_obj_create(lv_scr_act()); lv_obj_set_size(screen_saver, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(screen_saver, lv_color_black(), 0); lv_obj_set_style_bg_opa(screen_saver, LV_OPA_100, 0); lv_obj_move_background(screen_saver); @@ -185,6 +199,13 @@ GuppyScreen *GuppyScreen::init(std::function hal_i } #endif // OS_ANDROID +#ifdef SIMULATOR + /* No Moonraker in simulator mode — hide the init overlay and fabricate + * sensor data so the home panel renders something visible. */ + gs->init_panel.sim_hide(); + gs->main_panel.sim_setup_mock_data(); +#endif + return gs; } @@ -206,7 +227,9 @@ void GuppyScreen::loop() { if (lv_disp_get_inactive_time(NULL) > display_sleep) { if (!is_sleeping.load()) { spdlog::debug("putting display to sleeping"); - fbdev_blank(); + // fbdev_blank() uses FB_BLANK_POWERDOWN which the Ingenic X2000 + // kernel 4.4.94 DSI driver cannot recover from via FB_BLANK_UNBLANK. + // Use only the LVGL screen_saver (black) for the visual sleep effect. lv_obj_move_foreground(screen_saver); // spdlog::debug("screen saver foreground"); is_sleeping = true; @@ -216,6 +239,7 @@ void GuppyScreen::loop() { spdlog::debug("waking up display"); fbdev_unblank(); lv_obj_move_background(screen_saver); + lv_obj_invalidate(lv_scr_act()); is_sleeping = false; } } diff --git a/src/homing_panel.cpp b/src/homing_panel.cpp index f7a4e9b..775ad76 100644 --- a/src/homing_panel.cpp +++ b/src/homing_panel.cpp @@ -45,7 +45,9 @@ HomingPanel::HomingPanel(KWebSocketClient &websocket_client, std::mutex &lock) lv_obj_set_height(homing_cont, lv_pct(100)); lv_obj_set_width(homing_cont, lv_pct(100)); - static lv_coord_t grid_main_row_dsc[] = {LV_GRID_FR(4), LV_GRID_FR(4), LV_GRID_FR(2), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST}; + /* Tuned for 480x272: buttons get 30% each, selectors 20% each so the + * "Move Speed"/"Move Distance" titles fit above their button matrices. */ + static lv_coord_t grid_main_row_dsc[] = {LV_GRID_FR(3), LV_GRID_FR(3), LV_GRID_FR(2), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST}; static lv_coord_t grid_main_col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; lv_obj_set_grid_dsc_array(homing_cont, grid_main_col_dsc, grid_main_row_dsc); diff --git a/src/init_panel.cpp b/src/init_panel.cpp index 7e44360..7cc33df 100644 --- a/src/init_panel.cpp +++ b/src/init_panel.cpp @@ -135,6 +135,13 @@ void InitPanel::disconnected(KWebSocketClient &ws) { lv_obj_move_foreground(cont); } +#ifdef SIMULATOR +void InitPanel::sim_hide() { + lv_obj_add_flag(cont, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_background(cont); +} +#endif + void InitPanel::set_message(const char *message) { lv_label_set_text(label, message); } diff --git a/src/init_panel.h b/src/init_panel.h index 27e7121..3fb567e 100644 --- a/src/init_panel.h +++ b/src/init_panel.h @@ -17,6 +17,10 @@ class InitPanel { void connected(KWebSocketClient &ws); void disconnected(KWebSocketClient &ws); void set_message(const char *message); +#ifdef SIMULATOR + /* Hide the "Waiting for printer..." overlay in simulator mode. */ + void sim_hide(); +#endif private: lv_obj_t *cont; diff --git a/src/macro_item.cpp b/src/macro_item.cpp index 4d3aa7c..9d3a3e6 100644 --- a/src/macro_item.cpp +++ b/src/macro_item.cpp @@ -1,4 +1,5 @@ #include "macro_item.h" +#include "utils.h" #include "spdlog/spdlog.h" MacroItem::MacroItem(KWebSocketClient &c, @@ -60,7 +61,7 @@ MacroItem::MacroItem(KWebSocketClient &c, lv_obj_t *run_btn = lv_btn_create(top_cont); lv_obj_align(run_btn, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_set_style_text_font(run_btn, &lv_font_montserrat_16, LV_STATE_DEFAULT); + lv_obj_set_style_text_font(run_btn, &lv_font_montserrat_10, LV_STATE_DEFAULT); lv_obj_set_width(run_btn, 80); lv_obj_t *run_btn_label = lv_label_create(run_btn); lv_label_set_text(run_btn_label, LV_SYMBOL_PLAY); @@ -149,8 +150,10 @@ void MacroItem::handle_send_macro(lv_event_t *e) { } } - spdlog::trace("sending macro: {}", fmt::format("{}", fmt::join(kv, " "))); - ws.gcode_script(fmt::format("{}", fmt::join(kv, " "))); + std::string command = fmt::format("{}", fmt::join(kv, " ")); + spdlog::trace("sending macro: {}", command); + KUtils::confirm_if_printing("Printer is printing.\nRun this macro anyway?", + [this, command]() { ws.gcode_script(command); }); } } diff --git a/src/macros_panel.cpp b/src/macros_panel.cpp index bf8d857..5e31464 100644 --- a/src/macros_panel.cpp +++ b/src/macros_panel.cpp @@ -33,7 +33,7 @@ MacrosPanel::MacrosPanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent) lv_obj_set_style_pad_row(top_cont, 0, 0); lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_text_font(kb, &lv_font_montserrat_16, LV_STATE_DEFAULT); + lv_obj_set_style_text_font(kb, &lv_font_montserrat_10, LV_STATE_DEFAULT); } MacrosPanel::~MacrosPanel() diff --git a/src/main.cpp b/src/main.cpp index 4406d9d..c45113e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -85,7 +85,7 @@ static void hal_init(lv_color_t primary, lv_color_t secondary) { spdlog::debug("resolution {} x {}", width, height); lv_disp_t * disp = lv_disp_drv_register(&disp_drv); lv_theme_t * th = height <= 480 - ? lv_theme_default_init(NULL, primary, secondary, true, &lv_font_montserrat_12) + ? lv_theme_default_init(NULL, primary, secondary, true, &lv_font_montserrat_10) : lv_theme_default_init(NULL, primary, secondary, true, &lv_font_montserrat_20); lv_disp_set_theme(disp, th); @@ -142,7 +142,7 @@ static void hal_init(lv_color_t primary, lv_color_t secondary) lv_disp_t * disp = lv_disp_drv_register(&disp_drv); lv_theme_t * th = MONITOR_HOR_RES <= 480 - ? lv_theme_default_init(NULL, primary, secondary, true, &lv_font_montserrat_12) + ? lv_theme_default_init(NULL, primary, secondary, true, &lv_font_montserrat_10) : lv_theme_default_init(NULL, primary, secondary, true, &lv_font_montserrat_16); lv_disp_set_theme(disp, th); diff --git a/src/main_panel.cpp b/src/main_panel.cpp index beacc06..637a03d 100644 --- a/src/main_panel.cpp +++ b/src/main_panel.cpp @@ -1,12 +1,13 @@ #include "main_panel.h" #include "state.h" +#include "utils.h" #include "lvgl/lvgl.h" #include "spdlog/spdlog.h" #include LV_IMG_DECLARE(filament_img); -LV_IMG_DECLARE(light_img); +LV_IMG_DECLARE(fine_tune_img); LV_IMG_DECLARE(move); LV_IMG_DECLARE(print); LV_IMG_DECLARE(extruder); @@ -41,7 +42,7 @@ MainPanel::MainPanel(KWebSocketClient &websocket, , main_cont(lv_obj_create(main_tab)) , print_status_panel(websocket, lock, main_cont) , print_panel(ws, lock, print_status_panel) - , printertune_panel(ws, lock, printertune_tab, print_status_panel.get_finetune_panel()) + , printertune_panel(ws, lock, printertune_tab, led_panel) , numpad(Numpad(main_cont)) , extruder_panel(ws, lock, numpad, sm) , prompt_panel(websocket, lock, main_cont) @@ -51,7 +52,7 @@ MainPanel::MainPanel(KWebSocketClient &websocket, , homing_btn(main_cont, &move, "Homing", &MainPanel::_handle_homing_cb, this) , extrude_btn(main_cont, &filament_img, "Extrude", &MainPanel::_handle_extrude_cb, this) , action_btn(main_cont, &fan, "Fans", &MainPanel::_handle_fanpanel_cb, this) - , led_btn(main_cont, &light_img, "LED", &MainPanel::_handle_ledpanel_cb, this) + , finetune_btn(main_cont, &fine_tune_img, "Fine Tune", &MainPanel::_handle_finetune_cb, this) , print_btn(main_cont, &print, "Print", &MainPanel::_handle_print_cb, this) { lv_style_init(&style); @@ -159,6 +160,10 @@ void MainPanel::handle_homing_cb(lv_event_t *event) { spdlog::trace("clicked homing1"); if (lv_event_get_code(event) == LV_EVENT_CLICKED) { spdlog::trace("clicked homing"); + if (KUtils::is_printing()) { + KUtils::notify_locked(); + return; + } homing_panel.foreground(); } } @@ -166,6 +171,10 @@ void MainPanel::handle_homing_cb(lv_event_t *event) { void MainPanel::handle_extrude_cb(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { spdlog::trace("clicked extruder"); + if (KUtils::is_printing()) { + KUtils::notify_locked(); + return; + } extruder_panel.foreground(); } } @@ -177,10 +186,10 @@ void MainPanel::handle_fanpanel_cb(lv_event_t *event) { } } -void MainPanel::handle_ledpanel_cb(lv_event_t *event) { +void MainPanel::handle_finetune_cb(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { - spdlog::trace("clicked led panel"); - led_panel.foreground(); + spdlog::trace("clicked fine tune"); + print_status_panel.get_finetune_panel().foreground(); } } @@ -195,7 +204,9 @@ void MainPanel::create_main(lv_obj_t *parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_ROW_WRAP); - static lv_coord_t grid_main_row_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; + /* Row 0: sensors (CONTENT-sized). Row 1: chart fills the rest. The + * previous buffer row left a visible whitespace gap between sensors and chart. */ + static lv_coord_t grid_main_row_dsc[] = {LV_GRID_CONTENT, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; static lv_coord_t grid_main_col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; @@ -205,31 +216,63 @@ void MainPanel::create_main(lv_obj_t *parent) lv_obj_set_flex_grow(main_cont, 1); lv_obj_set_grid_dsc_array(main_cont, grid_main_col_dsc, grid_main_row_dsc); - lv_obj_set_grid_cell(homing_btn.get_container(), LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 0, 1); - lv_obj_set_grid_cell(extrude_btn.get_container(), LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 0, 1); - lv_obj_set_grid_cell(action_btn.get_container(), LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 1, 1); - lv_obj_set_grid_cell(led_btn.get_container(), LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 1, 1); - lv_obj_set_grid_cell(print_btn.get_container(), LV_GRID_ALIGN_STRETCH, 2, 2, LV_GRID_ALIGN_STRETCH, 2, 1); - - // lv_obj_clear_flag(temp_cont, LV_OBJ_FLAG_SCROLLABLE); - // lv_obj_set_size(temp_cont, LV_PCT(50), LV_PCT(50)); + // Wrap all buttons in a container spanning cols 2-3 across all 3 rows so + // button heights are uniform and independent of the CONTENT-sized sensor row. + lv_obj_t *btn_wrapper = lv_obj_create(main_cont); + lv_obj_clear_flag(btn_wrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(btn_wrapper, 0, 0); + lv_obj_set_style_border_width(btn_wrapper, 0, 0); + lv_obj_set_style_bg_opa(btn_wrapper, LV_OPA_TRANSP, 0); + lv_obj_set_grid_cell(btn_wrapper, LV_GRID_ALIGN_STRETCH, 2, 2, LV_GRID_ALIGN_STRETCH, 0, 2); + + static lv_coord_t btn_col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; + static lv_coord_t btn_row_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; + lv_obj_set_grid_dsc_array(btn_wrapper, btn_col_dsc, btn_row_dsc); + + lv_obj_set_parent(homing_btn.get_container(), btn_wrapper); + lv_obj_set_parent(extrude_btn.get_container(), btn_wrapper); + lv_obj_set_parent(action_btn.get_container(), btn_wrapper); + lv_obj_set_parent(finetune_btn.get_container(), btn_wrapper); + lv_obj_set_parent(print_btn.get_container(), btn_wrapper); + + lv_obj_set_grid_cell(homing_btn.get_container(), LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 0, 1); + lv_obj_set_grid_cell(extrude_btn.get_container(), LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 0, 1); + lv_obj_set_grid_cell(action_btn.get_container(), LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1); + lv_obj_set_grid_cell(finetune_btn.get_container(), LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 1, 1); + lv_obj_set_grid_cell(print_btn.get_container(), LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_STRETCH, 2, 1); + + /* Disable scrolling on the sensor container — the 5px left border on each + * sensor row was tripping the horizontal scrollbar even though everything + * fits visually. */ + lv_obj_clear_flag(temp_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollbar_mode(temp_cont, LV_SCROLLBAR_MODE_OFF); lv_obj_set_style_pad_all(temp_cont, 0, 0); + /* Zero row gap so sensor rows stack flush — default flex pad_row eats vertical space. */ + lv_obj_set_style_pad_row(temp_cont, 0, 0); lv_obj_set_flex_flow(temp_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_grid_cell(temp_cont, LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_STRETCH, 0, 2); + /* Row 0 is CONTENT-sized — it shrinks to fit only the sensor rows. */ + lv_obj_set_grid_cell(temp_cont, LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_STRETCH, 0, 1); lv_obj_align(temp_chart, LV_ALIGN_CENTER, 0, 0); - lv_obj_set_width(temp_chart, LV_PCT(45));//, LV_PCT(40)); + lv_obj_set_width(temp_chart, LV_PCT(45)); lv_obj_set_style_size(temp_chart, 0, LV_PART_INDICATOR); lv_chart_set_range(temp_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 300); - lv_obj_set_grid_cell(temp_chart, LV_GRID_ALIGN_END, 0, 2, LV_GRID_ALIGN_STRETCH, 2, 1); - lv_chart_set_axis_tick(temp_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 0, 6, 5, true, 50); - - lv_chart_set_div_line_count(temp_chart, 3, 8); - lv_chart_set_point_count(temp_chart, 5000); - lv_chart_set_zoom_x(temp_chart, 5000); - lv_obj_scroll_to_x(temp_chart, LV_COORD_MAX, LV_ANIM_OFF); + /* Chart in row 1, stretches to fill remaining space below the sensor rows. */ + lv_obj_set_grid_cell(temp_chart, LV_GRID_ALIGN_END, 0, 2, LV_GRID_ALIGN_STRETCH, 1, 1); + /* 7 major ticks = labels at 0,50,100,...,300 — 50° steps for easier reading. */ + lv_chart_set_axis_tick(temp_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 0, 7, 5, true, 25); + lv_obj_set_style_text_font(temp_chart, &lv_font_montserrat_8, LV_PART_TICKS); + + /* 5 interior horizontal div lines align with the 50° tick interval (50,100,150,200,250). */ + lv_chart_set_div_line_count(temp_chart, 5, 8); + /* point_count + zoom_x previously 5000 each, which compressed all data into + * the rightmost slice of the chart. With zoom 256 (1x) and a smaller point + * buffer, the pre-populated samples spread across the full chart width. */ + lv_chart_set_point_count(temp_chart, 300); + lv_chart_set_zoom_x(temp_chart, 256); + /* Scroll is set in create_sensors() after sensors are added and layout is stable. */ } void MainPanel::create_sensors(json &temp_sensors) { @@ -277,11 +320,32 @@ void MainPanel::create_sensors(json &temp_sensors) { lv_chart_series_t *temp_series = lv_chart_add_series(temp_chart, color_code, LV_CHART_AXIS_PRIMARY_Y); + lv_chart_set_all_value(temp_chart, temp_series, 0); - sensors.insert({key, std::make_shared(ws, temp_cont, sensor_img, 150, + auto sc = std::make_shared(ws, temp_cont, sensor_img, 150, display_name.c_str(), color_code, controllable, false, numpad, sensor_name, - temp_chart, temp_series, type)}); + temp_chart, temp_series, type); + + /* Controllable sensors (extruder, bed) get a second series rendered as a + * faint horizontal line at the target temperature so the user can see + * actual vs set at a glance. */ + if (controllable) { + lv_color_t target_color = lv_color_mix(color_code, lv_color_white(), 128); + lv_chart_series_t *target_series = + lv_chart_add_series(temp_chart, target_color, LV_CHART_AXIS_PRIMARY_Y); + lv_chart_set_all_value(temp_chart, target_series, 0); + sc->set_target_series(target_series); + } + + sensors.insert({key, sc}); } + + /* Layout is now stable (sensors added to temp_cont, CONTENT row resolved). + * Scroll chart to show the newest data (right end of the ring buffer). */ + /* Force main_cont grid to recalculate: row 0 was LV_GRID_CONTENT but temp_cont + * was empty when create_main() ran, so its height was 0. Now sensors are present. */ + lv_obj_update_layout(main_cont); + lv_obj_scroll_to_x(temp_chart, LV_COORD_MAX, LV_ANIM_OFF); } void MainPanel::create_fans(json &fans) { @@ -297,3 +361,79 @@ void MainPanel::enable_spoolman() { setting_panel.enable_spoolman(); extruder_panel.enable_spoolman(); } + +#ifdef SIMULATOR +#include "config.h" + +void MainPanel::sim_setup_mock_data() { + Config *conf = Config::get_instance(); + json &user_sensors = conf->get_json(conf->df() + "monitored_sensors"); + if (user_sensors.is_null()) { + spdlog::warn("sim_setup_mock_data: no monitored_sensors in config"); + return; + } + + /* Build a display_sensors json keyed by id, matching State::get_display_sensors + * shape — but bypass the Moonraker-dependent extruder/heater/sensor lookups. */ + json display_sensors; + for (auto &s : user_sensors) { + std::string id = s["id"].template get(); + display_sensors[id] = s; + } + create_sensors(display_sensors); + + /* Set initial displayed values. */ + for (auto &kv : sensors) { + const std::string &key = kv.first; + auto &s = kv.second; + if (key.find("extruder") != std::string::npos) { + s->update_value(205); s->update_target(210); + } else if (key.find("heater_bed") != std::string::npos) { + s->update_value(62); s->update_target(65); + } else { + s->update_value(38); + } + } + + /* Pre-populate the chart with ~200 historical points so the graph + * looks alive immediately rather than starting empty. */ + for (int i = 0; i < 200; i++) { + for (auto &kv : sensors) { + const std::string &key = kv.first; + auto &s = kv.second; + int v; + if (key.find("extruder") != std::string::npos) { + v = 200 + (i % 12) - 6; + } else if (key.find("heater_bed") != std::string::npos) { + v = 60 + (i % 6) - 3; + } else { + v = 35 + (i % 8) - 4; + } + s->update_series(v); + } + } + + /* Faster timer (300ms) gives a denser live chart since SIMULATOR mode + * bypasses the 1s throttle in SensorContainer::update_series. */ + lv_timer_t *t = lv_timer_create([](lv_timer_t *timer) { + auto *self = static_cast(timer->user_data); + static int n = 0; + n++; + for (auto &kv : self->sensors) { + const std::string &key = kv.first; + auto &s = kv.second; + int v; + if (key.find("extruder") != std::string::npos) { + v = 205 + (n % 12) - 6; + } else if (key.find("heater_bed") != std::string::npos) { + v = 62 + (n % 6) - 3; + } else { + v = 38 + (n % 8) - 4; + } + s->update_value(v); + s->update_series(v); + } + }, 300, this); + (void)t; +} +#endif diff --git a/src/main_panel.h b/src/main_panel.h index 39f3191..87b2b2c 100644 --- a/src/main_panel.h +++ b/src/main_panel.h @@ -41,10 +41,16 @@ class MainPanel : public NotifyConsumer { void create_sensors(json &temp_sensors); void create_fans(json &temp_fans); void create_leds(json &leds); +#ifdef SIMULATOR + /* Populate sensors directly from config and start a periodic timer that + * pushes mock temperature values, so the home panel is visible without + * a live Moonraker connection. */ + void sim_setup_mock_data(); +#endif void handle_homing_cb(lv_event_t *event); void handle_extrude_cb(lv_event_t *event); void handle_fanpanel_cb(lv_event_t *event); - void handle_ledpanel_cb(lv_event_t *event); + void handle_finetune_cb(lv_event_t *event); void handle_print_cb(lv_event_t *event); lv_obj_t *create_button(lv_obj_t *parent, @@ -72,9 +78,9 @@ class MainPanel : public NotifyConsumer { panel->handle_fanpanel_cb(event); }; - static void _handle_ledpanel_cb(lv_event_t *event) { + static void _handle_finetune_cb(lv_event_t *event) { MainPanel *panel = (MainPanel*)event->user_data; - panel->handle_ledpanel_cb(event); + panel->handle_finetune_cb(event); }; static void _handle_print_cb(lv_event_t *event) { @@ -116,7 +122,7 @@ class MainPanel : public NotifyConsumer { ButtonContainer homing_btn; ButtonContainer extrude_btn; ButtonContainer action_btn; - ButtonContainer led_btn; + ButtonContainer finetune_btn; ButtonContainer print_btn; }; #endif // __MAIN_PANEL_H__ diff --git a/src/mini_print_status.cpp b/src/mini_print_status.cpp index 322e56a..f61bf9b 100644 --- a/src/mini_print_status.cpp +++ b/src/mini_print_status.cpp @@ -34,7 +34,7 @@ MiniPrintStatus::MiniPrintStatus(lv_obj_t *parent, lv_obj_set_style_radius(cont, 4, 0); lv_obj_add_flag(cont, LV_OBJ_FLAG_FLOATING); - lv_obj_align(cont, LV_ALIGN_TOP_LEFT, 0, -14 * scale); + lv_obj_align(cont, LV_ALIGN_TOP_RIGHT, 0, -14 * scale); lv_obj_add_flag(cont, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(cont, cb, LV_EVENT_CLICKED, user_data); diff --git a/src/numpad.cpp b/src/numpad.cpp index 3bdee53..683c6f0 100644 --- a/src/numpad.cpp +++ b/src/numpad.cpp @@ -103,9 +103,16 @@ void Numpad::handle_input(lv_event_t *e) { // } // } -void Numpad::foreground_reset() { - spdlog::trace("resetting foreground"); - lv_textarea_set_text(input, ""); +void Numpad::foreground_reset(int initial) { + spdlog::trace("resetting foreground with initial {}", initial); + if (initial >= 0) { + lv_textarea_set_text(input, fmt::format("{}", initial).c_str()); + /* Place the cursor at the end so the next digit appends; selecting all + * would let a fresh number replace the value but feels surprising. */ + lv_textarea_set_cursor_pos(input, LV_TEXTAREA_CURSOR_LAST); + } else { + lv_textarea_set_text(input, ""); + } lv_obj_clear_flag(edit_cont, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(edit_cont); } diff --git a/src/numpad.h b/src/numpad.h index 92b0a37..4b288db 100644 --- a/src/numpad.h +++ b/src/numpad.h @@ -12,7 +12,9 @@ class Numpad { void set_callback(std::function cb); void handle_input(lv_event_t *event); /* void handle_defocused(lv_event_t *event); */ - void foreground_reset(); + /* initial >= 0 pre-fills the input field (useful for showing current set temp + * so the user can adjust rather than retype). Pass -1 (default) for blank. */ + void foreground_reset(int initial = -1); static void _handle_input(lv_event_t *event) { Numpad *panel = (Numpad*)event->user_data; diff --git a/src/print_panel.cpp b/src/print_panel.cpp index 66b0ae0..55bd4f1 100644 --- a/src/print_panel.cpp +++ b/src/print_panel.cpp @@ -58,16 +58,22 @@ PrintPanel::PrintPanel(KWebSocketClient &websocket, std::mutex &lock, PrintStatu // file view buttons lv_obj_t *label = NULL; + /* Explicit m10 override — theme cascade alone isn't reaching these labels for + * some reason, possibly because they're children of plain lv_btn which carries + * its own default style. Forcing the font here matches the rest of the UI. */ label = lv_label_create(refresh_btn); lv_label_set_text(label, LV_SYMBOL_REFRESH " Reload"); + lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0); lv_obj_center(label); label = lv_label_create(modified_sort_btn); lv_label_set_text(label, LV_SYMBOL_LIST " Modified"); + lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0); lv_obj_center(label); label = lv_label_create(az_sort_btn); lv_label_set_text(label, LV_SYMBOL_LIST " A-Z"); + lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0); lv_obj_center(label); lv_obj_add_event_cb(refresh_btn, &PrintPanel::_handle_btns, LV_EVENT_CLICKED, this); diff --git a/src/printertune_panel.cpp b/src/printertune_panel.cpp index 2c3f15b..5a09158 100644 --- a/src/printertune_panel.cpp +++ b/src/printertune_panel.cpp @@ -1,5 +1,6 @@ #include "printertune_panel.h" #include "state.h" +#include "utils.h" #include "spdlog/spdlog.h" #include @@ -7,7 +8,7 @@ namespace fs = std::experimental::filesystem; LV_IMG_DECLARE(bedmesh_img); -LV_IMG_DECLARE(fine_tune_img); +LV_IMG_DECLARE(light_img); LV_IMG_DECLARE(inputshaper_img); LV_IMG_DECLARE(limit_img); LV_IMG_DECLARE(motor_img); @@ -20,10 +21,10 @@ LV_IMG_DECLARE(power_devices_img); LV_IMG_DECLARE(print); #endif -PrinterTunePanel::PrinterTunePanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent, FineTunePanel &finetune) +PrinterTunePanel::PrinterTunePanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent, LedPanel &led) : cont(lv_obj_create(parent)) , bedmesh_panel(c, l) - , finetune_panel(finetune) + , led_panel(led) , limits_panel(c, l) , inputshaper_panel(c, l) , belts_calibration_panel(c, l) @@ -31,7 +32,7 @@ PrinterTunePanel::PrinterTunePanel(KWebSocketClient &c, std::mutex &l, lv_obj_t , tmc_status_panel(c, l) , power_panel(c, l) , bedmesh_btn(cont, &bedmesh_img, "Bed Mesh", &PrinterTunePanel::_handle_callback, this) - , finetune_btn(cont, &fine_tune_img, "Fine Tune", &PrinterTunePanel::_handle_callback, this) + , led_btn(cont, &light_img, "LED", &PrinterTunePanel::_handle_callback, this) , inputshaper_btn(cont, &inputshaper_img, "Input Shaper", &PrinterTunePanel::_handle_callback, this) #ifndef ZBOLT , belts_calibration_btn(cont, &belts_calibration_img, "Belts/Shake", &PrinterTunePanel::_handle_callback, this) @@ -63,7 +64,7 @@ PrinterTunePanel::PrinterTunePanel(KWebSocketClient &c, std::mutex &l, lv_obj_t // row 1 lv_obj_set_grid_cell(bedmesh_btn.get_container(), LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 1, 1); - lv_obj_set_grid_cell(finetune_btn.get_container(), LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1); + lv_obj_set_grid_cell(led_btn.get_container(), LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1); lv_obj_set_grid_cell(inputshaper_btn.get_container(), LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 1, 1); lv_obj_set_grid_cell(belts_calibration_btn.get_container(), LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 1, 1); @@ -117,30 +118,36 @@ void PrinterTunePanel::handle_callback(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { lv_obj_t *btn = lv_event_get_current_target(event); - if (btn == finetune_btn.get_container()) { - spdlog::trace("tune finetune pressed"); - finetune_panel.foreground(); + if (btn == led_btn.get_container()) { + spdlog::trace("tune led pressed"); + led_panel.foreground(); } else if (btn == bedmesh_btn.get_container()) { spdlog::trace("tune bedmesh pressed"); + if (KUtils::is_printing()) { KUtils::notify_locked(); return; } bedmesh_panel.foreground(); } else if (btn == inputshaper_btn.get_container()) { spdlog::trace("tune inputshaper pressed"); + if (KUtils::is_printing()) { KUtils::notify_locked(); return; } inputshaper_panel.foreground(); } else if (btn == belts_calibration_btn.get_container()) { spdlog::trace("tune belts pressed"); + if (KUtils::is_printing()) { KUtils::notify_locked(); return; } belts_calibration_panel.foreground(); } else if (btn == limits_btn.get_container()) { spdlog::trace("limits pressed"); - limits_panel.foreground(); + KUtils::confirm_if_printing("Printer is printing.\nAdjust limits anyway?", + [this]() { limits_panel.foreground(); }); } else if (btn == tmc_tune_btn.get_container()) { spdlog::trace("tmc auto tune pressed"); + if (KUtils::is_printing()) { KUtils::notify_locked(); return; } tmc_tune_panel.foreground(); } else if (btn == tmc_status_btn.get_container()) { spdlog::trace("tmc metrics pressed"); tmc_status_panel.foreground(); } else if (btn == power_devices_btn.get_container()) { spdlog::trace("power devices pressed"); - power_panel.foreground(); + KUtils::confirm_if_printing("Printer is printing.\nChange power devices anyway?", + [this]() { power_panel.foreground(); }); } } } diff --git a/src/printertune_panel.h b/src/printertune_panel.h index a10ee1b..8d1e4a4 100644 --- a/src/printertune_panel.h +++ b/src/printertune_panel.h @@ -1,7 +1,7 @@ #ifndef __PRINTERTUNE_PANEL_H__ #define __PRINTERTUNE_PANEL_H__ -#include "finetune_panel.h" +#include "led_panel.h" #include "limits_panel.h" #include "bedmesh_panel.h" #include "inputshaper_panel.h" @@ -15,7 +15,7 @@ #include class PrinterTunePanel { public: - PrinterTunePanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent, FineTunePanel &); + PrinterTunePanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent, LedPanel &); ~PrinterTunePanel(); lv_obj_t *get_container(); @@ -32,15 +32,15 @@ class PrinterTunePanel { private: lv_obj_t *cont; BedMeshPanel bedmesh_panel; - FineTunePanel &finetune_panel; + LedPanel &led_panel; LimitsPanel limits_panel; InputShaperPanel inputshaper_panel; BeltsCalibrationPanel belts_calibration_panel; TmcTunePanel tmc_tune_panel; TmcStatusPanel tmc_status_panel; PowerPanel power_panel; - ButtonContainer bedmesh_btn; - ButtonContainer finetune_btn; + ButtonContainer bedmesh_btn; + ButtonContainer led_btn; ButtonContainer inputshaper_btn; ButtonContainer belts_calibration_btn; ButtonContainer limits_btn; diff --git a/src/selector.cpp b/src/selector.cpp index 299e6e2..4716ad1 100644 --- a/src/selector.cpp +++ b/src/selector.cpp @@ -24,15 +24,22 @@ Selector::Selector(lv_obj_t *parent, lv_obj_set_style_pad_all(cont, 0, 0); lv_obj_set_style_pad_row(cont, 0, 0); - auto height = (double)lv_disp_get_physical_ver_res(NULL) * (height_pct / 100.0); - height = height < 50 ? 50 : height; + auto ver_res = lv_disp_get_physical_ver_res(NULL); + bool small_screen = ver_res <= 320; + auto height = (double)ver_res * (height_pct / 100.0); + /* Lower the minimum on compact screens so the title label still fits + * alongside the button matrix in tight grid rows (e.g. 480x272). */ + double min_height = small_screen ? 28.0 : 50.0; + if (height < min_height) height = min_height; lv_obj_set_size(btnm, LV_PCT(100), height); - lv_label_set_text(label, label_text); - lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_text(label, label_text); + lv_obj_set_width(label, LV_PCT(100)); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); - + + /* Theme default is m10 on small screens now — no need to override per-widget here. */ + lv_btnmatrix_set_map(btnm, &map[0]); - lv_obj_set_style_pad_all(btnm, 4, LV_PART_MAIN); + lv_obj_set_style_pad_all(btnm, small_screen ? 2 : 4, LV_PART_MAIN); lv_obj_set_style_outline_width(btnm, 0, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); diff --git a/src/sensor_container.cpp b/src/sensor_container.cpp index d85a2fb..bd9d69f 100644 --- a/src/sensor_container.cpp +++ b/src/sensor_container.cpp @@ -28,6 +28,7 @@ SensorContainer::SensorContainer(KWebSocketClient &c, , id(name) , chart(chart_chart) , series(chart_series) + , target_series(NULL) , last_updated_ts(std::time(nullptr)) , type(type) { @@ -43,31 +44,46 @@ SensorContainer::SensorContainer(KWebSocketClient &c, auto width_scale = (double)lv_disp_get_physical_hor_res(NULL) / 800.0; auto height_scale = (double)lv_disp_get_physical_ver_res(NULL) / 480.0; - lv_obj_set_size(sensor_cont, 330 * width_scale, 60 * height_scale); + /* Tighter row: 48 (was 60) so three sensors + chart fit comfortably at 480x272. */ + lv_obj_set_size(sensor_cont, 330 * width_scale, 48 * height_scale); lv_obj_set_style_pad_all(sensor_cont, 0, 0); + /* Smaller, screen-aware font — on 480x272 the previous theme-default m12 felt oversized. */ + const lv_font_t *row_font = (width_scale < 0.8) ? &lv_font_montserrat_10 : &lv_font_montserrat_14; + lv_img_set_src(sensor_img, img); lv_obj_align(sensor_img, LV_ALIGN_LEFT_MID, 0, 0); lv_label_set_text(sensor_label, text); lv_obj_align_to(sensor_label, sensor_img, LV_ALIGN_OUT_RIGHT_MID, -7 * width_scale, 0); - + lv_obj_set_style_text_font(sensor_label, row_font, 0); + // Clip long display names (e.g. "MCU Temperature") so they don't bleed into the value/target boxes. + lv_obj_set_width(sensor_label, 130 * width_scale); + lv_label_set_long_mode(sensor_label, LV_LABEL_LONG_DOT); + + // Right-anchored block: [value] [/] [target]. Sized so 3-digit values ("210") fit inside + // the bordered target box without wrapping. Targets/values use LV_LABEL_LONG_CLIP to + // suppress wrap even if a future value somehow exceeds the box. lv_label_set_text(value_label, "0"); - lv_obj_set_width(value_label, 55 * width_scale); - lv_obj_align(value_label, LV_ALIGN_RIGHT_MID, -75 * width_scale, 0); + lv_obj_set_width(value_label, 50 * width_scale); + lv_obj_align(value_label, LV_ALIGN_RIGHT_MID, -90 * width_scale, 0); lv_label_set_long_mode(value_label, LV_LABEL_LONG_CLIP); - lv_obj_set_style_pad_all(value_label, 8 * width_scale, 0); + lv_obj_set_style_pad_all(value_label, 2 * width_scale, 0); + lv_obj_set_style_text_font(value_label, row_font, 0); lv_label_set_text(divider_label, "/"); - lv_obj_set_width(divider_label, 50 * width_scale); - lv_obj_align(divider_label, LV_ALIGN_RIGHT_MID, -32 * width_scale, 0); - lv_obj_set_style_pad_all(divider_label, 8 * width_scale, 0); + lv_obj_set_width(divider_label, 12 * width_scale); + lv_obj_align(divider_label, LV_ALIGN_RIGHT_MID, -72 * width_scale, 0); + lv_obj_set_style_pad_all(divider_label, 0, 0); + lv_obj_set_style_text_font(divider_label, row_font, 0); if (show_target || can_edit) { lv_label_set_text(target_label, "0"); lv_obj_set_width(target_label, 60 * width_scale); - lv_obj_align(target_label, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_set_style_pad_all(target_label, 8 * width_scale, 0); + lv_obj_align(target_label, LV_ALIGN_RIGHT_MID, -5 * width_scale, 0); + lv_obj_set_style_pad_all(target_label, 2 * width_scale, 0); + lv_obj_set_style_text_font(target_label, row_font, 0); + lv_label_set_long_mode(target_label, LV_LABEL_LONG_CLIP); } else { lv_obj_add_flag(target_label, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(divider_label, LV_OBJ_FLAG_HIDDEN); @@ -113,6 +129,11 @@ SensorContainer::~SensorContainer() { lv_chart_remove_series(chart, series); series = NULL; } + + if (target_series != NULL && chart != NULL) { + lv_chart_remove_series(chart, target_series); + target_series = NULL; + } } lv_obj_t *SensorContainer::get_sensor() { @@ -123,6 +144,18 @@ void SensorContainer::update_target(int new_target) { if (new_target >= 0) { target = new_target; lv_label_set_text(target_label, fmt::format("{}", new_target).c_str()); + /* Paint the entire target series at the new target value — gives a flat + * horizontal line at that temp so the user can see actual vs set at a glance. */ + if (target_series != NULL && chart != NULL) { + lv_chart_set_all_value(chart, target_series, new_target); + } + } +} + +void SensorContainer::set_target_series(lv_chart_series_t *ts) { + target_series = ts; + if (ts != NULL && chart != NULL && target >= 0) { + lv_chart_set_all_value(chart, ts, target); } } @@ -135,11 +168,16 @@ void SensorContainer::update_value(int new_value) { void SensorContainer::update_series(int v) { if (series != NULL && chart != NULL) { +#ifdef SIMULATOR + /* No throttling in simulator mode — driven entirely by the mock timer. */ + lv_chart_set_next_value(chart, series, v); +#else auto delta = std::time(nullptr) - last_updated_ts; if (delta > 1) { lv_chart_set_next_value(chart, series, v); last_updated_ts = std::time(nullptr); } +#endif } } @@ -160,6 +198,8 @@ void SensorContainer::handle_edit(lv_event_t *e) { } ws.gcode_script(cmd); }); - numpad.foreground_reset(); + /* Pre-fill the numpad with the current target so the user can adjust + * from the existing value instead of retyping from scratch. */ + numpad.foreground_reset(target); } } diff --git a/src/sensor_container.h b/src/sensor_container.h index 31e4a12..87d39a6 100644 --- a/src/sensor_container.h +++ b/src/sensor_container.h @@ -48,6 +48,9 @@ class SensorContainer { void update_target(int new_target); void update_value(int new_value); void update_series(int value); + /* Attach a second chart series that draws a horizontal line at the current + * target temperature. Set once, updated whenever update_target() fires. */ + void set_target_series(lv_chart_series_t *ts); void handle_edit(lv_event_t *event); static void _handle_edit(lv_event_t *event) { @@ -69,6 +72,7 @@ class SensorContainer { std::string id; lv_obj_t *chart; lv_chart_series_t *series; + lv_chart_series_t *target_series; std::time_t last_updated_ts; SensorType type; }; diff --git a/src/sysinfo_panel.cpp b/src/sysinfo_panel.cpp index 2be9268..fd8addf 100644 --- a/src/sysinfo_panel.cpp +++ b/src/sysinfo_panel.cpp @@ -249,11 +249,23 @@ SysInfoPanel::SysInfoPanel() } lv_obj_add_event_cb(def_temp_dd, &SysInfoPanel::_handle_callback, LV_EVENT_VALUE_CHANGED, this); + // Factory Reset lives alone in the top-right corner — kept away from Back and + // the settings rows to avoid accidental presses. lv_obj_add_flag(factory_reset_btn.get_container(), LV_OBJ_FLAG_FLOATING); - lv_obj_align(factory_reset_btn.get_container(), LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_obj_align(factory_reset_btn.get_container(), LV_ALIGN_TOP_RIGHT, 0, 0); lv_obj_add_flag(back_btn.get_container(), LV_OBJ_FLAG_FLOATING); lv_obj_align(back_btn.get_container(), LV_ALIGN_BOTTOM_RIGHT, 0, 0); + + // The network/version text is left-aligned and the Factory Reset button is + // narrow and pinned to the right, so they don't overlap. Reserve only the + // button's width on the right so a very long interface line can't slide under + // it — full vertical height stays available so the version never clips. + lv_obj_update_layout(cont); + lv_obj_set_width(network_label, + lv_obj_get_width(right_cont) + - lv_obj_get_width(factory_reset_btn.get_container()) - 4); + lv_label_set_long_mode(network_label, LV_LABEL_LONG_WRAP); } SysInfoPanel::~SysInfoPanel() { diff --git a/src/utils.cpp b/src/utils.cpp index 2c85d35..c2d2c9c 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -4,6 +4,7 @@ #include "state.h" #include "spdlog/spdlog.h" #include "platform.h" +#include "lvgl/lvgl.h" #include #include @@ -34,6 +35,65 @@ namespace KUtils { return false; } + bool is_printing() { + auto v = State::get_instance() + ->get_data("/printer_state/print_stats/state"_json_pointer); + if (!v.is_null()) { + std::string s = v.template get(); + return s == "printing" || s == "paused"; + } + return false; + } + + // Shared styling so print-lock dialogs match the app's other msgboxes: + // centered body text and a floating, centered button row at the bottom. + static void style_lock_mbox(lv_obj_t *mbox, lv_coord_t btns_pct) { + lv_obj_t *msg = ((lv_msgbox_t *)mbox)->text; + lv_obj_set_style_text_align(msg, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_width(msg, LV_PCT(100)); + lv_obj_center(msg); + + lv_obj_t *btnm = lv_msgbox_get_btns(mbox); + lv_obj_add_flag(btnm, LV_OBJ_FLAG_FLOATING); + lv_obj_align(btnm, LV_ALIGN_BOTTOM_MID, 0, 0); + + auto hscale = (double)lv_disp_get_physical_ver_res(NULL) / 480.0; + lv_obj_set_size(btnm, LV_PCT(btns_pct), 50 * hscale); + lv_obj_set_size(mbox, LV_PCT(70), LV_PCT(45)); + lv_obj_center(mbox); + } + + void notify_locked() { + static const char *btns[] = {"OK", ""}; + lv_obj_t *mbox = lv_msgbox_create(NULL, NULL, + "Unavailable while printing", btns, false); + lv_obj_add_event_cb(mbox, [](lv_event_t *e) { + lv_msgbox_close(lv_obj_get_parent(lv_event_get_target(e))); + }, LV_EVENT_VALUE_CHANGED, NULL); + style_lock_mbox(mbox, 50); + } + + void confirm_if_printing(const std::string &msg, const std::function &cb) { + if (!is_printing()) { + cb(); + return; + } + + auto *heap_cb = new std::function(cb); + static const char *btns[] = {"Confirm", "Cancel", ""}; + lv_obj_t *mbox = lv_msgbox_create(NULL, NULL, msg.c_str(), btns, false); + lv_obj_add_event_cb(mbox, [](lv_event_t *e) { + lv_obj_t *obj = lv_obj_get_parent(lv_event_get_target(e)); + auto *fn = (std::function *)lv_event_get_user_data(e); + if (lv_msgbox_get_active_btn(obj) == 0 && fn != NULL) { + (*fn)(); + } + delete fn; + lv_msgbox_close(obj); + }, LV_EVENT_VALUE_CHANGED, heap_cb); + style_lock_mbox(mbox, 90); + } + bool is_running_local() { Config *conf = Config::get_instance(); std::string df_host = conf->get(conf->df() + "moonraker_host"); diff --git a/src/utils.h b/src/utils.h index 18cc9fd..0330a4f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -12,6 +12,14 @@ using json = nlohmann::json; namespace KUtils { bool is_homed(); + // True while a print job is active (printing or paused). Panels that move axes, + // home, or run calibration must not act in this state. + bool is_printing(); + // Modal "Unavailable while printing" notice (single OK button). + void notify_locked(); + // Runs cb immediately when idle; while printing, shows a Confirm/Cancel modal + // and only runs cb if the user confirms the override. + void confirm_if_printing(const std::string &msg, const std::function &cb); bool is_running_local(); std::string get_root_path(const std::string root_name);