Commit graph

12 commits

Author SHA1 Message Date
GlassOnTin
a45986f7a4 Add watchdog, serial file download, and bootloader-mode flash workflow
Watchdog: 30s ESP32 task watchdog auto-resets on lockup. Fed every
main loop iteration. Disabled during deep sleep (CPU off).

File download: 'D' debug command streams file contents by index over
serial. screenshot.py 'dl <index>' downloads to local file.

Flash workflow no longer needs BOOT+RST:
  screenshot.py z  →  esptool --before no_reset write_flash
2026-03-31 14:32:51 +01:00
GlassOnTin
d1daf8ca3e Add serial reset and bootloader commands, no more BOOT+RST
Two new debug commands:
- 'X' (reset): ESP.restart() for clean reboot
- 'Z' (bootloader): sets RTC_CNTL_FORCE_DOWNLOAD_BOOT and restarts,
  putting the ESP32-S3 directly into download mode for esptool

Workflow: screenshot.py z → esptool write_flash (no BOOT+RST)

Also adds reset/bootloader subcommands to screenshot.py.
2026-03-31 14:06:47 +01:00
GlassOnTin
5a0a0df0ec Confirmed: T-Beam Supreme RX hardware failure, not firmware
Tested with both our fork AND upstream RNode firmware — neither
can receive LoRa packets on this T-Beam Supreme unit. TX works
(14KB transmitted via rnsd), noise floor measured (-86 dBm),
airtime detected from watch beacon, but RX_DONE interrupt never
fires. Decoded packets = 0 in all tests.

The watch beacon firmware is verified correct:
- IFAC crypto: all 4 tests PASS (Python round-trip verified)
- Packet format: byte-for-byte match with Python IFAC apply
- IFAC key: stored correctly in NVS, matches computed value

To verify the beacon end-to-end, a working RNode receiver is
needed (different T-Beam unit, Heltec, or the GL.inet's RNode
brought within range).
2026-03-29 16:20:51 +01:00
GlassOnTin
1672a65475 Add IFAC crypto test vectors — all 4 tests pass
Firmware crypto test (debug command 'C') validates IfacAuth.h
against test vectors generated by scripts/test_ifac.py from
Reticulum's Python implementation:

  pk:   Ed25519 keypair from seed      PASS
  sig:  Ed25519 detached signature     PASS
  hkdf: HKDF-SHA256 mask generation    PASS
  ifac: Full IFAC packet wrapping      PASS

The IFAC crypto implementation matches Reticulum exactly.
The beacon reception failure is NOT a crypto bug — likely
an IFAC key provisioning issue (NVS storage mismatch) or
a beacon packet format issue before IFAC is applied.

Run: ./scripts/screenshot.py crypto
Generate vectors: python3 scripts/test_ifac.py

Also: guarded profiling variables for non-T-Watch builds
so T-Beam Supreme and other targets compile cleanly.
2026-03-29 13:16:17 +01:00
GlassOnTin
6d3ef4441b Add provisioned radio profile baseline
T-Watch provisioned via rnodeconf with patched T-Watch product
code (0xEC). Radio online at 868MHz, GPS active.

Key finding: radio SPI takes only 6µs/loop when online and idle
(vs 99ms when stopRadio() was called every loop). Main loop
stable at 289µs. Full frame 83ms (37ms flush + 45ms render).

rnodeconf requires T-Watch product code (0xEC) and model (0xDA)
to be added to the RNS Utilities package. Signing key generated
locally for self-signed provisioning.
2026-03-29 11:27:36 +01:00
GlassOnTin
4564eb9980 Save profile baseline, add --save option to profile command
Baseline profile (scripts/profile_baseline.json) captured after
beacon/stopRadio loop fix. Use for regression comparison:
  ./scripts/screenshot.py profile --save scripts/profile_latest.json

Key baseline numbers:
  Idle frame: 914µs, Full frame: 80ms, Data update: 2ms,
  Per-tile nav: 75ms, Main loop: 331µs
2026-03-29 11:00:06 +01:00
GlassOnTin
5a1f8eb8f4 Add serial-triggered performance profile test
Debug command 'P' runs a standardized 5-test benchmark:
1. Idle frame (nothing dirty) — measures LVGL overhead
2. Full frame invalidation + render — measures render + flush
3. Data update cycle — measures label formatting cost
4. Navigate all 5 tiles — measures tile transition cost
5. 10-frame burst — measures sustained frame rate

Results reported as JSON with build timestamp for regression
tracking. Python tool: ./scripts/screenshot.py profile

Baseline (Mar 29 2026, hwcdc, unprovisioned):
  Idle: 914µs, Full: 80ms (37ms flush + 42ms render),
  Data update: 2ms, Per-tile: 75ms, Per-frame: 80ms,
  Main loop: 331µs idle
2026-03-29 10:58:50 +01:00
GlassOnTin
926330253a Fix 950ms main loop bottleneck: beacon and radio init on every iteration
Root cause found via loop profiling: two functions called every loop
iteration on unprovisioned devices consumed 950ms:

1. beacon_update() called startRadio() every loop when GPS had fix
   but device wasn't provisioned (hw_ready=false). startRadio() does
   full radio init with SPI commands and delays (~600ms). Fix: gate
   beacon_update() on hw_ready.

2. stopRadio() called LoRa->end() (SPI sleep + SPI.end()) every loop
   in the !hw_ready path (~99ms). Fix: only call when radio_online
   is true (stop once, not repeatedly).

Result: loop time 950ms → 0.3ms (3200x improvement).

Also added:
- Main loop profiling (radio/serial/display/pmu/gps/bt/imu timing)
- Build timestamp in metrics command for version verification
- Visible-tile-only label updates (GPS float formatting skipped
  when GPS screen not shown)
- Reverted to hwcdc USB mode (TinyUSB adds ~900ms/loop overhead)
- USB MSC SD card code preserved but inactive (needs TinyUSB)
2026-03-29 10:52:50 +01:00
GlassOnTin
b594284060 Add IMU data logger to SD card with remote start/stop
Streams BHI260AP accelerometer (50Hz), gyroscope (50Hz), and
magnetometer (25Hz) as timestamped CSV to SD card. Ring buffer
in PSRAM (512 samples) flushed to SD from the main loop.

Remote debug command 'L' toggles logging. Python tool:
  ./scripts/screenshot.py log

CSV format: ms,ax,ay,az,gx,gy,gz,mx,my,mz (raw int16 units)
Measured throughput: ~43Hz actual sample rate.

Also fixed: BHI260AP init no longer gated on hw_ready (radio
provisioning) — IMU sensors should work regardless of radio state.

Foundation for compass calibration (PCA on magnetometer data),
gesture recognition training, and activity classification.
2026-03-28 17:05:33 +00:00
GlassOnTin
2e9f703121 Partial rendering, remote debug protocol, frame metrics
Switched from full-frame to partial rendering — only dirty
rectangles are rendered and flushed. Static watch face flushes
in ~750us vs ~18ms (24x faster). PSRAM usage drops from 824KB
to 197KB for draw buffers.

Remote debug protocol over serial (prefix "RWS" + command byte):
  S — Screenshot (wakes display, forces full redraw, captures)
  T — Touch injection (x, y, duration)
  N — Navigate to tile by column/row
  M — Frame metrics (JSON: flush/render times, memory)
  I — Invalidate (force full redraw)

Python tool (scripts/screenshot.py) supports all commands:
  screenshot, metrics, touch, swipe, navigate, invalidate

Screenshot now works correctly with partial rendering by
keeping the capture flag active across all flush strips and
forcing a full invalidation before capture.

Frame timing instrumentation added to flush callback and
render loop for performance profiling.
2026-03-28 15:04:49 +00:00
GlassOnTin
54618f2f2d Fix custom font rendering, tearing, and scrollbar visibility
Custom 96px font was not rendering because lv_font_conv defaults
to compressed bitmap format (bitmap_format=1) which requires
LV_USE_FONT_COMPRESSED. Regenerated with --no-compress.

Eliminated scroll tearing by switching from partial-strip rendering
to full-frame double buffering (LV_DISPLAY_RENDER_MODE_FULL).
Each buffer is 412KB in PSRAM (824KB total). Removes the need for
a separate byte-swap buffer — using LV_COLOR_FORMAT_RGB565_SWAPPED
so LVGL renders directly in the display's native byte order.

Also: hidden tileview scrollbar, enabled PSRAM in build flags.
2026-03-28 13:59:52 +00:00
GlassOnTin
2c0c9f3d5a Add LVGL watch GUI with tileview navigation and serial screenshot
LVGL 9.5.0 integration replacing direct framebuffer rendering.
Watch face with time, date, status bar (mode/battery), and three
complications (LoRa/GPS/Steps). Tileview swipe navigation between
five screens: watch face, radio status, GPS, messages, settings.
Haptic feedback on screen transitions via DRV2605.

Radio status screen shows frequency, LoRa params, RSSI bar gauge,
channel utilization, BLE state, and packet counts. GPS screen shows
coordinates, fix quality, altitude/speed, and beacon status.

Serial screenshot tool (scripts/screenshot.py) captures display
contents over USB CDC by sending trigger bytes and receiving the
shadow framebuffer as raw RGB565.

Build changes:
- FlashSize=16M in FQBN (bootloader embeds flash size, defaults 4MB)
- PSRAM=enabled for LVGL draw buffers and screenshot shadow buffer
- Custom 8MB app partition (partition_twatch.csv) for 16MB flash
- flash-twatch_ultra-full make target for full bootloader+partition+app flash

Architecture notes in code:
- display_init() must run BEFORE xl9555_init() (display power gate
  defaults high at power-on, reordering causes black screen)
- LVGL draw buffers use separate swap buffer for RGB565 byte-order
  conversion to avoid corrupting LVGL's internal buffer state
- Touch input registered via function pointer to decouple Gui.h from
  touch library include order
2026-03-28 11:41:45 +00:00