Commit graph

915 commits

Author SHA1 Message Date
GlassOnTin
fded16c19a Fix bubble level: sub-step spring-damper for stability at 500ms GUI rate
The spring-damper (k=40, c=12) was unstable at dt=0.5s — position
overshot max_r on every frame, got clamped, then overshot again,
keeping the bubble stuck at the ring edge.

Fix: sub-step the physics at 20ms intervals (25 steps per 500ms
frame). The integration is now stable at any GUI update rate.
2026-04-02 11:35:44 +01:00
GlassOnTin
3848b97150 Bubble level: polar non-linear mapping and spring-damper fluid physics
Replaces independent per-axis tanh with polar decomposition:
tilt magnitude → tanh(r*3) non-linearity → project back to cartesian.
This gives circular response at all angles instead of square distortion.

Spring-damper model (k=40, c=12) simulates overdamped viscous fluid:
bubble settles in ~0.3s with no oscillation. Wall collision kills
radial velocity component so bubble slides along the ring edge.
2026-04-02 11:31:07 +01:00
GlassOnTin
3d328d28f4 Add bubble level complication with EMA-filtered accelerometer
Always-on ACCEL_PASSTHROUGH at 10Hz feeds an exponential moving
average filter (α=0.15) for smooth, noise-resistant tilt sensing.

Watch face shows a circular bubble level below the step counter:
- Ring with crosshairs, moving dot shows tilt direction
- Dot colour: green (<3°), amber (<15°), red (>15°)
- Angle readout in degrees below the ring
- Bubble position clamped to ring boundary

Accel mapping: ax/ay normalized to 1g (4096 LSB), inverted for
natural bubble behaviour (tilt left → bubble moves left).
2026-04-02 11:28:00 +01:00
GlassOnTin
7208ce348c Add BHI260 init retry, revert BMM150 firmware (no magnetometer on PCB)
The T-Watch Ultra does not have a BMM150 magnetometer — the BHI260AP
aux I2C pins are tied to VDD1V8 (confirmed from schematic). The
BOSCH_BHI260_AUX_BMM150_GPIO firmware fails to init because there
is no BMM150 to discover.

Reverted to BOSCH_BHI260_GPIO. Added 10s retry interval for BHI260
init failures instead of single-attempt with no recovery.
2026-04-02 10:49:33 +01:00
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
9edf5a953c Add SD card file listing via debug command F
Implements gui_list_files_fn callback to list SD card contents
as JSON via the serial debug protocol. Replaces the stub that
returned "use_log_command".
2026-03-31 14:00:56 +01:00
GlassOnTin
782710e52e Extend sensor logger: tagged CSV, GPS/step/tilt/touch channels, Settings toggle
Refactors IMULogger to a general sensor data logger with tagged CSV format:
  ms,type,d0,d1,...,d8
Types: A=accel, G=gyro, M=mag, S=step, W=wrist_tilt, P=GPS, T=touch

Each sensor pushes its own tagged sample independently instead of
the old combined IMU-only row format. GPS logged at 1Hz when active,
step/tilt events logged on occurrence, touch logged on press.

Adds Data Logger switch to the Settings screen with live sample
count and duration display.

Reorders includes so IMULogger.h is available to sensor callbacks.
2026-03-31 04:22:18 +01:00
GlassOnTin
c3ab57f484 Fix self-provisioning: write INFO_LOCK_BYTE and EEPROM checksum
Self-provisioning wrote 0x00 to ADDR_INFO_LOCK instead of
INFO_LOCK_BYTE (0x73), causing eeprom_lock_set() to fail and
hw_ready to stay false after flash erase.

Also computes and writes the MD5 checksum over the first 11 EEPROM
bytes so eeprom_checksum_valid() passes without rnodeconf.
2026-03-31 02:04:39 +01:00
GlassOnTin
2bf457e2ba Add Settings screen with display timeout, beacon, and GPS controls
Replaces the "Coming soon" placeholder with working LVGL settings:
- Display timeout: slider 5-60s, updates display_blanking_timeout
- Beacon enable: switch, gates beacon_update()
- Beacon interval: roller (10s/30s/1m/5m/10m), replaces #define
- GPS dynamic model: roller (Portable/Stationary/Pedestrian/Automotive),
  sends UBX-CFG-VALSET to MIA-M10Q

All settings persist to EEPROM config region (addresses 0x91-0x94)
and reload on boot. Callbacks save immediately on change.
2026-03-31 01:45:12 +01:00
GlassOnTin
0cf5faa01f Fix LXMF beacon: remove beacon_crypto_configured gate, add diagnostics
The LXMF beacon path was gated on both lxmf_identity_configured AND
beacon_crypto_configured. The latter is only set when legacy collector
keys are provisioned via EEPROM — unrelated to LXMF. Removing the
gate allows the LXMF announce + telemetry path to work standalone.

Verified: watch LXMF announce received by Sideband as "RNode GPS
Tracker" via T-Beam Supreme LoRa gateway.

Also adds lxmf_id and bcn_crypto fields to metrics output.
2026-03-30 15:00:43 +01:00
GlassOnTin
c6e2a282ab Fix LoRa RX: remove DC-DC regulator, restore demod reset after false preamble
Root cause: SetRegulatorMode(DC-DC+LDO) in begin() kills LoRa RX on
boards without the DC-DC inductor (T-Beam Supreme, T-Watch Ultra).
TX still works, preamble/RSSI detection works, but RX_DONE never
fires — mimics hardware failure.

Three regressions from 033ddd6 fixed:
- Remove SetRegulatorMode(0x01) from begin() — these boards use
  LDO-only, which is the SX1262 default after reset
- Restore receive() call in dcd() after false preamble timeout —
  upstream fix (3ae8982) we had removed; just clearing IRQ flags
  is insufficient to reset the demodulator chain
- Add lora_receive() after beacon_transmit() — radio was left in
  STDBY after TX, never returning to RX mode

Also reverts getPacketType check in receive(), TCXO-before-calibration,
clear-device-errors, and TX fallback mode changes that were not needed
and caused intermittent RX failures during testing.

Verified: bidirectional LoRa RX between watch and T-Beam Supreme
with IFAC authentication at 868 MHz SF7 BW125K.
2026-03-29 21:05:59 +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
6a43afb019 Force radio restart on CMD_RADIO_STATE, debug LoRa RX failure
CMD_RADIO_STATE 0x01 now does stopRadio() + startRadio() to ensure
clean SX1262 re-initialization when rnsd configures the radio.

LoRa bidirectional test: NEITHER device can receive from the other.
Both detect RF energy (airtime 1.34%) but can't demodulate packets.
This rules out IFAC, provisioning, and firmware issues — the problem
is at the LoRa PHY layer. Both SX1262 chips see preambles but fail
to decode the payload.

Next step: bare-metal LoRa test bypassing all RNS/IFAC/KISS layers.
Send a raw LoRa packet from one SX1262 and verify reception on the
other with minimal firmware to isolate the demodulation failure.
2026-03-29 15:39:04 +01:00
GlassOnTin
82bf97f532 IFAC verified correct end-to-end, receiver not decoding LoRa frame
Python verification of actual captured beacon packet confirms IFAC
is correct: unmask → verify signature → MATCH. The firmware crypto
implementation (IfacAuth.h) produces identical output to Reticulum.

T-Beam Supreme receiver detects RF energy (-74 dBm) but doesn't
decode a valid LoRa packet — the airtime counter shows activity but
no traffic is counted. Likely cause: near-field RF saturation (watch
and T-Beam are cm apart on the same Pi USB hub). The SX1262 receiver
may be overloaded at this close range.

Also: added beacon packet dump command 'B', extended crypto test
with live key verification, added T-Beam Supreme to device_init
signature bypass for development.
2026-03-29 14:02:09 +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
32db2f2bf5 Guard profiling for T-Watch only, fix T-Beam Supreme build
Profiling variables (prof_radio_us etc.) are defined in Gui.h which
is T-Watch only. Wrapped all profiling lines in the main loop with
#if BOARD_MODEL == BOARD_TWATCH_ULT guards so other board targets
(T-Beam Supreme, Heltec, etc.) compile cleanly.

Beacon RF link verified: T-Beam Supreme receiver detects watch
beacon transmissions at -74 dBm (same Pi, cm apart). Airtime shows
1.33% during beacon TX. However, Reticulum rejects the packets due
to IFAC signature mismatch — the watch's IfacAuth.h Ed25519/HKDF
implementation produces different signatures than Reticulum's Python
crypto. Needs investigation in the IFAC apply/verify path.
2026-03-29 13:03:09 +01:00
GlassOnTin
d49618a5ab Prevent beacon deep sleep on USB power, add beacon_gate to metrics
Watch no longer enters deep sleep after beacon TX when on external
power (charging or charged). This keeps the display, USB serial,
and debug tools active during development. On battery only, the
sleep/wake beacon cycle operates normally for power conservation.

Added beacon_gate and hw_ready to metrics output for debugging
beacon activation: gate 1=host active, 2=startup delay, 3=radio
offline, 4=no GPS fix, 5=interval wait, 6=beacon sent.

Verified: radio comes online with "RADIO" mode indicator and
amber "---" LoRa complication when provisioned with GPS fix.
Beacon mode activates after 15s no-host timeout.
2026-03-29 11:58:55 +01:00
GlassOnTin
2f693131a3 Add provision-twatch_ultra and TWATCH_PORT to Makefile
New targets:
  make provision-twatch_ultra  — provisions EEPROM + sets firmware hash
  make flash-twatch_ultra-full TWATCH_PORT=/dev/ttyACM0  — full flash

TWATCH_PORT defaults to /dev/ttyACM4, override for remote Pi:
  make provision-twatch_ultra TWATCH_PORT=/dev/ttyACM0
2026-03-29 11:47:36 +01:00
GlassOnTin
cc5bd6b4ee Enable radio on T-Watch: dev signature bypass, self-provisioning
Device.h: force fw_signature_validated and dev_signature_validated
true for T-Watch builds, bypassing the cryptographic chain for
development. Production devices should use proper signing.

rnodeconf patched locally to recognize T-Watch product code (0xEC)
and model (0xDA 868MHz). Provisioning + firmware hash set via:
  rnodeconf --rom --product ec --model da --hwrev 1
  rnodeconf --firmware-hash <hash>

Radio complication confirmed working:
- Shows "OFF" when radio in standby (no host, no GPS fix)
- Will show "---" in amber when radio online but no packets
- Will show RSSI value when packets received
- Mode indicator shows "IDLE" → "BEACON" → "MODEM" based on state
2026-03-29 11:46:45 +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
c3b22f36e8 Self-provisioning, visible-tile-only updates, cleanup dead async code
T-Watch self-provisions EEPROM on first boot (blank flash recovery).
Writes product/model/hwrev so the firmware can initialize without
rnodeconf. Full cryptographic provisioning still requires rnodeconf
with T-Watch product code support (not yet upstream).

GUI updates only format labels for the currently visible tile —
GPS float formatting (%.6f) skipped when GPS screen not shown,
radio/battery labels skipped when not on those screens.

Removed dead async DMA code from CO5300.h (spi_device_queue_trans
doesn't work on ESP32-S3 SPI3 with this driver version).

Removed SPI mutex from sx126x.cpp (unnecessary with exclusive
SD/LoRa mode switching). SharedSPI.h retained for IMULogger/Gui
SD card access coordination.
2026-03-29 11:17:05 +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
5e19294dea Add shared SPI bus mutex for LoRa + SD card coexistence
SharedSPI.h: FreeRTOS mutex protecting the shared SPI bus (pins
33/34/35) used by LoRa (SX1262), SD card, and future NFC.

sx126x.cpp: All 6 SPI transaction blocks wrapped with
shared_spi_mutex acquire/release. LoRa uses portMAX_DELAY
(always gets access, just delayed by SD if needed).

USBSD.h: USB MSC SD card access uses shared_spi_mutex with
200ms timeout. Deferred init (3s after boot) for SPI bus
readiness. Metrics: sd_ready, sd_reads, sd_fails counters.

IMULogger.h, Gui.h: SD card writes wrapped in shared_spi_mutex.

Status: SD card correctly reports 29.7GB via USB MSC. Reads
work intermittently (~50% success). Root cause: main loop
takes ~700ms per iteration, causing mutex timeout for MSC
callbacks. Needs loop time investigation to achieve reliable
USB mass storage.

The CO5300 display uses a separate SPI3 bus — not affected.
2026-03-28 20:59:52 +00: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
cb367eff5c Enable BHI260AP wrist tilt wake and step counter
Wrist tilt gesture (sensor ID 67): waking the display by raising
the wrist toward you, with a light haptic click. Replaces the need
to touch the screen or press a button for the most common interaction.

Step counter (sensor ID 52): always-on pedometer displayed below
the complications on the watch face. Shows count once walking starts.

Both sensors configured after the deferred BHI260AP firmware upload
(~5s after boot). IMU events processed in the main loop via
bhi260->update(). Wrist tilt triggers display_unblank() directly.

Also added BoschSensorDataHelper.hpp for typed sensor data parsing
and imu_step_count/imu_wrist_tilt globals for cross-module access.
2026-03-28 16:54:08 +00:00
GlassOnTin
9ad16bd8ca Add HDOP-based quality filtering to GPS display
GPS screen: coordinates shown in teal when HDOP<10 (good fix),
grey when poor. Speed suppressed when HDOP>=10 (meaningless
noise from position drift). HDOP text color-coded: green (<2),
teal (<5), amber (<10), red (>=10).

Watch face GPS complication: satellite count color reflects fix
quality — teal (HDOP<5), amber (HDOP<15), grey (poor).

Addresses phantom speed readings during high-HDOP conditions
where position noise causes false velocity reports from the
GPS module's Kalman filter.
2026-03-28 16:42:25 +00:00
GlassOnTin
849d772774 Center GPS coordinates, fix screenshot timing for blanked display
GPS screen: centered title and coordinate text to clear the
display's rounded corners. Previously left-aligned text was
clipped at the top-left corner radius.

Screenshot command: advance LVGL tick by LV_DEF_REFR_PERIOD+1
before calling lv_timer_handler() to ensure the refresh timer
fires. Previously the 1ms tick advance wasn't enough to trigger
a render cycle, resulting in black screenshots.
2026-03-28 16:36:30 +00:00
GlassOnTin
d3416a8dfe Add home button: short-press BOOT returns to watch face
GPIO 0 (BOOT button) now serves as the home/wake button:
- Short press when blanked: wake display
- Short press when on sub-screen: animate back to watch face
- Long press (2s): screenshot to SD (unchanged)

Addresses spurious navigation from USB disconnect noise on
the capacitive touch panel.
2026-03-28 16:15:41 +00:00
GlassOnTin
1e83e6afcd Add battery voltage, charge state, and temperature to GUI
Watch face: replaced Steps complication with battery voltage and
charge state (green when charging/full, red when <15%, white
otherwise). Shows actual voltage (e.g., 4.17V).

Radio status screen: added BATTERY row (voltage + percentage)
and TEMPERATURE row (ESP32-S3 internal temp via PMU sensor).

Status bar continues to show percentage + charging indicator.
2026-03-28 16:12:15 +00:00
GlassOnTin
3137220add Tune scroll feel: low-friction momentum, skip data updates during scroll
Scroll throw friction set to 2% (momentum carries into snap naturally).
Disabled elastic overscroll at tile boundaries.
gui_update_data() skipped during scroll animations to free CPU for
rendering. Added gui_is_scrolling() detection.

Loop timing metrics added: loop_us (interval between gui_update calls),
loop_max_us (worst case). Metrics show 84ms/frame during scroll
(37ms SPI flush + 44ms LVGL render). Release-to-snap latency is
~2 frames (~168ms).

SPI chunk size remains at 16384 pixels (32768 bytes) — larger chunks
cause display failure (DMA/PSRAM bandwidth limits on ESP32-S3).
Async DMA and larger chunks investigated but not viable with current
SPI driver constraints.
2026-03-28 16:06:42 +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
7366a671b4 Optimize display: DMA SPI, on-demand screenshots, faster scroll
Switched all SPI operations from spi_device_polling_transmit (CPU
busy-wait) to spi_device_transmit (DMA with FreeRTOS yield). The
CPU blocks on a semaphore instead of spinning, allowing ISRs to
run during the ~18ms frame transfer.

Removed per-frame 411KB screenshot buffer copy — now only copies
when a screenshot is actually requested via serial trigger.

Shortened tileview scroll snap animation from default ~300ms to
150ms for snappier transitions with fewer intermediate frames.

Async DMA (queue_trans with deferred flush_ready) was investigated
but spi_device_queue_trans conflicts with spi_device_transmit on
the same device — needs further investigation with a fully queued
pipeline (no mixed blocking/queued calls).
2026-03-28 14:54:29 +00:00
GlassOnTin
76a45dfe53 Add 28px Montserrat Bold font, full-frame rendering, clean scrolling
Both custom fonts (96px time, 28px date/complications/sub-screens)
now render correctly using C++ namespace isolation to avoid symbol
collisions when included in the same translation unit.

Switched to LV_DISPLAY_RENDER_MODE_FULL with two 412KB PSRAM
frame buffers for tear-free scrolling. Combined with
LV_COLOR_FORMAT_RGB565_SWAPPED to eliminate byte-swap overhead
in the flush callback entirely.
2026-03-28 14:02:10 +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
GlassOnTin
c31b1908a7 Add SPM1423 PDM microphone driver, fix speaker I2S port
Microphone on I2S_NUM_0 (PDM hardware constraint), speaker moved to
I2S_NUM_1. Both init at boot, shut down before deep sleep.
Mic provides raw audio read and RMS level measurement.
Boot beep disabled (speaker confirmed working).
2026-03-27 19:08:26 +00:00
GlassOnTin
8dd50ac901 Add MAX98357A I2S speaker driver with tone generator
I2S audio output on GPIO 9/10/11 (BCLK/WCLK/DOUT). Sine wave tone
generator with predefined alert sounds: beep, alert, success, error.
Boot beep confirms speaker is working. Speaker shut down before deep
sleep to release I2S driver.
2026-03-27 19:00:48 +00:00
GlassOnTin
deb061943a Add CST9217 touch panel with touch-to-wake display blanking
Touch driver via SensorLib TouchDrvCST92xx on I2C 0x1A with interrupt
on GPIO 12. Touch events unblank the display and reset the blanking
timer. Display blanks after 10 seconds of inactivity.

XL9555 TOUCH_RST now explicitly released at boot. Touch init runs
with explicit I2C_SDA/I2C_SCL pins (same fix as XPowersLib).
2026-03-27 18:48:07 +00:00
GlassOnTin
84c7b886c4 Add BHI260AP sensor hub with deferred init
BHI260AP firmware upload (~260KB at 1MHz I2C) takes ~10 seconds and
blocks the main loop. Moved init from setup() to a deferred check in
the main loop that runs once after 5 seconds when hw_ready is true.
This allows the radio and serial to come up immediately while the
sensor firmware uploads in the background.

Also restores XL9555 DRV_EN and DISP_EN enables at boot (lost during
earlier git checkout/stash operations).
2026-03-27 18:03:33 +00:00
GlassOnTin
9f034e8d0d Add DRV2605 haptic driver with boot and sleep feedback
Minimal I2C driver for the DRV2605 ERM vibration motor. 117 built-in
effects via the ERM effect library. Named constants for watch use
cases (click, bump, alert, buzz, tick, etc.).

Boot: sharp click on startup. Sleep: soft bump before entering deep
sleep. XL9555 DRV_EN and DISP_EN now explicitly enabled at boot.

Updated Makefile upload-twatch_ultra to set firmware hash after JTAG flash.
2026-03-27 17:45:01 +00:00
GlassOnTin
b9319fa761 Fix deep sleep: remove PMU enableSleep, fix GPIO handling, add shared helper
Three critical fixes:
- Remove PMU->enableSleep() which bricked AXP2101 I2C across resets
- Remove I2C pins (GPIO 2/3) from OPEN_DRAIN list — gpio_reset_pin()
  already sets INPUT mode, and OPEN_DRAIN persists in battery-backed
  RTC domain, corrupting the I2C bus
- Use esp_reset_reason() == ESP_RST_DEEPSLEEP for reliable cold boot
  vs timer wake discrimination

Extracted twatch_enter_deep_sleep() shared helper used by both
sleep_now() and beacon_wake_cycle(). Adds co5300_sleep() call
before SPI.end() to properly shut down display controller.

Tested: sleep via long-press, wake via button, radio reconnects,
display re-initialises, I2C bus intact across multiple cycles.
2026-03-27 17:08:05 +00:00
GlassOnTin
504fb08fdc Enable CO5300 AMOLED display with live watch face
Display now shows time (00:00 from reset RTC) and status line with
radio state, battery percentage, GPS satellites, and uptime counter.
Display blanking disabled until button input is implemented.
QSPI display driver confirmed working after I2C pin fix.
2026-03-27 16:56:30 +00:00
GlassOnTin
4ddbe83c51 Add T-Watch Ultra developer notes documenting hard-won lessons
Covers: I2C pin default mismatch (XPowersLib), flash workflow
(esptool vs JTAG), deep sleep PMU caution, display power gate,
GPS/RTC differences from T-Beam Supreme, and I2C bus architecture.
2026-03-27 16:35:28 +00:00
GlassOnTin
999933386b Fix I2C bus failure — XPowersLib was using wrong default SDA/SCL pins
Root cause: XPowersAXP2101(Wire) constructor defaults to SDA=8, SCL=9
(from generic esp32s3 variant pins_arduino.h). Its begin() method then
calls Wire.begin(8, 9), overriding our Wire.begin(3, 2). All I2C
traffic went to the wrong GPIO pins.

Fix: pass explicit I2C_SDA, I2C_SCL to the XPowersAXP2101 constructor
so it uses GPIO 3/2 (the T-Watch Ultra's actual I2C pins).

Also removed I2C diagnostic/recovery code that was debugging the
wrong problem.
2026-03-27 16:31:10 +00:00
GlassOnTin
f024489aee Add T-Watch Ultra PCB reference photos
Three photos from disassembly session:
- PCB overview with back cover removed
- PCB closeup showing component labels
- Battery connector detail

Factory firmware reflashed to verify hardware integrity. I2C bus and
display confirmed working under LilyGo firmware. RNode firmware I2C
init needs debugging — Wire.begin succeeds but no slaves respond.
2026-03-27 15:53:18 +00:00
GlassOnTin
6ad9abe74d Debug display + fix I2C bus bricked by deep sleep GPIO config
Key finding: deep sleep GPIO OPEN_DRAIN configuration persists in the
battery-backed RTC domain, corrupting the I2C bus across reboots.
Wire.begin() returns false after deep sleep has been entered once.

I2C bus recovery attempt added (SCL clock-out + STOP condition) but
Wire (I2C0) peripheral remains stuck. Wire1 (I2C1) works on same pins
but no slave devices respond — bus lines may be held by stuck slaves.

Found from schematic: XL9555 DOES control VC_EN (display VCI power).
Display QSPI pins confirmed correct from schematic (sheet 4).
BHI260AP SensorLib removed (was causing boot failure due to firmware blob size).
Beacon wake cycle still disabled pending I2C fix.

Critical TODO: fix deep sleep to not set I2C pins to OPEN_DRAIN,
or add robust I2C bus recovery in early boot before PMU init.
2026-03-27 13:46:00 +00:00
GlassOnTin
ae02c5c4af Add BHI260AP sensor hub integration for display GPIO expansion
Added SensorLib dependency (v0.3.1) with BHI260AP GPIO firmware
for controlling display power gate, haptic driver, and touch reset
via the sensor's auxiliary GPIO pins.

Fixed beacon wake cycle triggering on cold boot — disabled pending
proper deep sleep vs cold boot discrimination.

Display status: BHI260AP begin() returns false (firmware upload
failing). CO5300 QSPI driver still cannot reach display controller.
Both issues under investigation.
2026-03-27 13:01:20 +00:00