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.
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.
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)
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.
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.
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.
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