Commit Graph
98 Commits
Author SHA1 Message Date
torlando-agent[bot] 117d3894bd feat: make maps SD-pack only 2026-08-07 05:26:34 +00:00
torlando-agent[bot] 354dda9cda fix: close final power-cut review 2026-08-07 01:37:13 +00:00
torlando-agent[bot] c57db6b921 fix: make final recovery paths durable 2026-08-07 01:36:50 +00:00
torlando-agent[bot] b3f38b1b8c fix: close final transactional review 2026-08-07 01:36:21 +00:00
torlando-agent[bot] 340c9c240f fix: close map integration safety review 2026-08-07 01:35:35 +00:00
torlando-agent[bot] f55b54eec0 feat: wire safe opt-in map downloads 2026-08-07 01:34:30 +00:00
torlando-agent[bot] fe0d1f7d55 fix: close location durability and router races 2026-08-07 01:27:49 +00:00
torlando-agent[bot] d7a8f17ac8 feat: persist live location state 2026-08-07 01:27:07 +00:00
torlando-agent[bot] a71e19b65c fix: serialize live router admission 2026-08-07 01:26:45 +00:00
torlando-agent[bot] 0a28266b9c fix: harden live location dispatch boundaries 2026-08-07 01:26:12 +00:00
torlando-agent[bot] 9791de8149 build: isolate T-Deck test hooks 2026-08-07 01:25:10 +00:00
torlando-agent[bot] 504afdce70 fix: fail closed on persistence I/O errors 2026-08-07 01:23:28 +00:00
torlando-agent[bot] 45496a49d4 fix(nomadnet): bound path retry and unblock navigation 2026-08-05 17:34:16 +00:00
torlando-agent[bot] d0e1ce273c feat(ui): add app launcher and NomadNet MVP 2026-08-05 00:22:59 +00:00
torlando-agent[bot] 93760af5d5 feat: add current-channel radio activity view 2026-08-04 15:56:56 +00:00
torlando-agent[bot] ae91ccdd92 [verified] fix: restore watchdog panic with bounded polling 2026-08-03 20:48:12 +00:00
Torlando df2fe4542f make Reticulum transport mode opt-in 2026-08-03 17:50:13 +00:00
Torlando e6ee0a57e5 fix: close recovery and file-selection races 2026-08-02 23:30:40 +00:00
Torlando 9c0997befa fix: make firmware updates persistence-safe 2026-08-02 23:06:44 +00:00
torlando-agent[bot] 959f8d8262 build: add hook-free tdeck release environment 2026-08-02 03:00:06 +00:00
torlando-agent[bot] 61872480c9 fix: enforce ULBW-only LXST voice profile 2026-08-02 00:39:45 +00:00
torlando-agent[bot] c2c6ae5641 fix(voice): synchronize call termination and TCP announces 2026-08-01 21:32:01 +00:00
torlando-agent[bot] cfa6fa3afa test: harden LXST contention harness 2026-08-01 14:14:56 +00:00
torlando-agent[bot] 9d5cc32b82 fix: avoid duplicate delivery-state persistence 2026-07-27 04:24:58 +00:00
torlando-agent[bot] 55cd2528dd fix: validate OTA image after UI task starts 2026-07-27 01:51:44 +00:00
torlando-agent[bot] dcc80e14d7 fix: report active firmware and LittleFS storage 2026-07-25 02:01:05 +00:00
torlando-agent[bot] 0ea2be3fda fix: confirm successful OTA boot before restart 2026-07-24 19:21:56 +00:00
torlando-agent[bot] eabc3578b6 fix: preserve persistent data on filesystem mount failure 2026-07-23 02:41:39 +00:00
torlando-agent[bot] b043d1b62a fix: synchronize recorder state reads 2026-07-22 19:38:08 -04:00
torlando-agent[bot] efd3b0bb8f fix: address Greptile safety findings 2026-07-22 19:28:32 -04:00
torlando-agent[bot] 31631b0afd fix: make full-duplex voice fit live device memory 2026-07-22 13:28:23 -04:00
torlando-agent[bot] ae1f474491 fix(input): debounce phantom trackball button events 2026-07-22 11:46:10 -04:00
torlando-agent[bot] 9e151877cc fix(voice): stabilize LXST call lifecycle and add E2E harness 2026-07-22 10:25:45 -04:00
torlando-agent[bot]andClaude Opus 4.8 0281cb5b8e fix(audio): mute loopback speaker during raw-mic recording — the actual "garble" was acoustic feedback
The dominant garble was NOT the mic: T:RAWMIC/recorder ran full-duplex (start_loopback), so
the T-Deck played the decoded loopback round-trip out its OWN speaker, inches from the mic ->
acoustic feedback that built into "oscillating static that gets slowly louder". Gate the
playback ring-write off while the PSRAM recorder is active (i2s_playback.cpp) so raw-mic
captures are feedback-free -> speech becomes intelligible (Whisper recovers ~85% of words).

Also in this change:
- Recorder stores BOTH TDM channels (main.cpp) for offboard de-interleave; confirmed CH0 is
  the mic, CH1 dead -> de-interleave was always correct.
- MCLK raised to an exact APLL-locked 12.288MHz (es7210.cpp MCLK_DIV_FRE=768 + the
  {12288000,16000} coeff; i2s_capture.cpp use_apll=true/fixed_mclk) -- cleaner than the
  fractional-N 4.096MHz, though the feedback (not jitter) was the real cause.

Residual (under investigation): signal-correlated distortion ("static reacts to speech") +
50Hz mains hum (USB-ground pickup, removed by the existing 300Hz HPF in the call path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-24 21:46:57 -04:00
torlando-agent[bot]andClaude Opus 4.8 bd7b357b96 fix(audio): ES7210 mic gain saturation + heap-starvation reboots + raw-mic diagnostic harness
Confirmed fixes:
- LVGL hybrid-allocator threshold 1024->256 (lib/lv_mem_hybrid.h): moves ~45KB of
  small LVGL objects to PSRAM, fixing internal-heap starvation that made the
  call/loopback pipeline intermittently fail to allocate and reboot mid-call
  (internal free 71->116KB, largest contiguous block 61->106KB).
- ES7210 mic PGA gain 21dB->12dB (lxst_audio.cpp): 21dB saturated the ADC -- an
  rms-7003 noise floor in silence + 0x8000 negative-rail spikes + a spectral-peak
  shift that masqueraded as a "+17% pitch warp". At 12dB idle silence is clean (rms 48).

Diagnostic harness (PYXIS_TEST_HOOKS, test-only): T:RAWMIC[stage], T:REG, and
T:RECORD/T:DUMPREC add a raw-mic UDP tap plus a reliable PSRAM recorder with a
checksummed serial transfer, and T:LOOPBACK wiring, for objective mic-capture
analysis (offboard Whisper STT scoring).

Exploratory / still unresolved: captured speech remains garbled (oscillating static,
likely an ES7210 sigma-delta capture instability -- under investigation). Includes a
16kHz/main-PLL clock path + MICBIAS exploration; the "warp" was the gain artifact, not
a clock issue, so the 16kHz path can later be simplified back to 8kHz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-24 17:21:25 -04:00
torlando-agent[bot]andClaude Opus 4.8 dfc6a14f00 address greptile #38: fix standalone-exec test, drop dead var, clarify docstring, DRY hostname
- test_patch_nimble.py: the refactor made the script import its sibling
  _build_helpers; the standalone-exec test ran it with a temp PROJECT_DIR, so add
  the script's real directory to the shim's sys.path (CI pytest failure fix).
- sync_file_libdeps.py: remove the now-dead ENV_NAME binding.
- _build_helpers.py: reword the docstring (the "never literal env" line conflicted
  with the helper's own "tdeck" fallback) and note inline that "tdeck" is a
  never-hit default (PlatformIO always injects PIOENV).
- main.cpp: DRY the OTA hostname into OTA_HOSTNAME used by both setHostname() and
  the log line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-23 00:11:54 -04:00
torlando-agent[bot]andClaude Opus 4.8 b06b3650ae chore(build): unify pre-script libdeps paths on a shared helper + OTA log
OTA hardening follow-up to the patch_nimble.py hardcoded-"tdeck" bug that broke
tdeck-ota linking (undefined reference to nimble_host_reset_reason):
- Add _build_helpers.env_libdeps_dir(env, *parts): the single per-environment
  libdeps path resolver (.pio/libdeps/<PIOENV>/...). Converted all five pre-scripts
  (patch_nimble/msgpack/filestore/littlefs_paths, sync_file_libdeps) to use it, so
  the env component can no longer be hand-rolled/hardcoded wrong per script.
- "OTA: Ready" log -> "OTA: wireless flash service started (pyxis-tdeck:3232)":
  ArduinoOTA.begin() is void and can't confirm a ready state, so log the target
  instead of claiming readiness we can't verify.

Verified: both tdeck and tdeck-ota build, and the NimBLE patch lands in EACH env's
own libdeps tree (nimble_host_reset_reason count 2 in both).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-23 00:01:17 -04:00
torlando-agent[bot]andClaude Opus 4.8 36d93f965f fix: announce/sync interval units+defaults, live Settings clock, OTA repair
Settings/clock (user-reported):
- Announce interval: default 1h (was 60s); the field is now MINUTES (label
  "Announce Interval (min):", populate /60, read *60).
- Prop sync interval: default 4h (was 1h); the field is now HOURS (label
  "Prop Sync Interval (hrs):", populate /3600, read *3600); log prints hours.
- Settings clock now ticks live: SettingsScreen::tick() (throttled ~1s) is hooked
  into UIManager::update() like the announce-list tick, so the Time/GPS/system
  readouts refresh while the screen is open instead of being a static snapshot.

OTA repair (was broken two ways; diagnosed via an ultracode workflow):
- patch_nimble.py hardcoded the `tdeck` libdeps path, so building any OTHER env
  (tdeck-ota gets its own libdeps tree) never received the NimBLE patch that
  DEFINES nimble_host_reset_reason -> undefined-reference link error. Made it
  env-aware (env.get("PIOENV",...)), matching the four sibling pre-scripts. Also
  restores 3 NimBLE stability patches that were silently missing from OTA builds.
- ArduinoOTA onStart synchronously called TCPClientInterface::stop() (blocks up to
  30s), overrunning espota's ~10s connect-back window -> "No response from device"
  (phase 2), and its un-fed watchdog tripped reboots. Made onStart non-blocking.
- [env:tdeck-ota] upload_command invoked a non-existent tools/espota.py -> switched
  to upload_protocol=espota.
- Do NOT WiFi.setSleep(false): the ESP32 requires WiFi modem-sleep when WiFi+BT
  coexist (aborts at boot otherwise) -- documented inline.

Verified on device: tdeck-ota links; OTA over WiFi completes 100% (Result: OK) and
the device reboots clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-22 23:28:48 -04:00
torlando-agent[bot]andClaude Opus 4.8 27ff17b2ad fix(gps): Arizona no-DST carve-out + raise unsynced-display threshold to 2024
Addresses Greptile review on #36:
- Arizona is inside the Mountain longitude band but doesn't observe DST (and it
  straddles the Mountain/Pacific boundary), so MST7MDT wrongly showed MDT in
  summer. It now gets an explicit MST7 (no-DST) carve-out by lat/lon box before
  the longitude bands. (The DST-observing Navajo Nation is not separately handled.)
- The Settings "Time:" display used a >= 2016 threshold, which let the ESP32's
  ~2016-01-01 first-boot default render as a real date instead of "not set".
  Raised to >= 2024 so an unsynced clock is visibly unsynced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-22 17:08:22 -04:00
torlando-agent[bot]andClaude Opus 4.8 ccee8fe59b fix(gps): gate time-sync on a high-quality fix + correct re-sync + DST
The device intermittently stamped messages ~19 years in the future. Root cause is
a GPS week-number rollover (1024 weeks ~= 19.6y): an un-resolved/cold-start fix
emits a rollover date (e.g. 2046), and sync_time_from_gps accepted any date with
year >= 2024 (no upper bound) and wrote it to the system clock, which microLXMF
then stamps onto every outgoing message.

- Quality+stability gate: set the clock only from a real 3D fix (>= 4 satellites,
  HDOP <= 5) whose calendar date has been stable for >= 3s. Rollover/cold-start
  bogus dates are weak and don't hold steady, so they're rejected. No hardcoded
  future cutoff (which would itself age out). Date stability is tracked off the
  main loop's continuous gps.encode() feed.
- Re-sync fixed: the periodic retry called sync_time_from_gps(0), which returned
  immediately without syncing, and only ran until the first sync. It's now
  genuinely non-blocking (sets the clock iff the current fix passes the gate) and
  keeps retrying, so the clock self-corrects when a fix appears (e.g. basement ->
  window) with no reboot.
- DST + double-mktime (ported from abandoned branch 8d4a047): TZ=UTC0 is set
  before the single mktime() (the old double call corrupted the struct and skewed
  the UTC clock by the DST offset), and continental-US timezones use POSIX strings
  with DST rules so summer shows EDT/CDT/MDT/PDT instead of standard time.

Field-verified: from a window fix it synced to the correct 2026-06-22, no rollover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-22 16:44:20 -04:00
torlando-agent[bot]andClaude Opus 4.8 dd72ad3d41 fix(wifi): don't disturb the boot connect or wear NVS (greptile)
- Backstop ARMS its timer on the first disconnected tick instead of firing
  immediately, so it can't issue a redundant begin() ~1s into boot and reset the
  in-progress association (or setAutoReconnect's own retry). Re-armed on each
  (re)connect so every drop gets a fresh ~15s grace.
- WiFi.persistent(false): begin() no longer writes creds to NVS on every call, so
  the periodic backstop during a long outage doesn't wear flash. Creds are
  already persisted by the app's Preferences store; setAutoReconnect is in-RAM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 20:34:47 -04:00
torlando-agent[bot]andClaude Opus 4.8 72e4d036c2 fix(wifi): auto-reconnect after the AP drops the association
connect_wifi() never enabled auto-reconnect, and the loop's "WiFi reconnect
check" only re-attempted on the manual Settings -> Reconnect button. So a dropped
association left the device offline (no TCP, no AutoInterface, "no connection" in
the status bar) until a reboot. Enable WiFi.setAutoReconnect(true) + persistent,
and add a non-blocking backstop in the main loop that re-issues WiFi.begin()
every ~15s while disconnected -- setAutoReconnect alone doesn't cover every
disconnect reason. Verified the SSID/AP are fine (connects at boot); closes the
stay-offline-until-reboot gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 20:19:45 -04:00
torlando-agent[bot]andClaude Opus 4.8 9922130ead fix(tcp): close stop() teardown UAF window — force-delete task on deadline (greptile)
stop()'s join had a fixed deadline (CONNECT_TIMEOUT_MS + 2s); a slow DNS could
keep the task inside connect() past it, so stop() would free the object while the
task still referenced `this`. Extend the deadline well beyond any connect()+DNS,
and if it still expires, vTaskDelete(_task_handle) the task so it can't touch
`this` after return. (The task's own self-delete path sets _task_done first, so
this branch only runs when it has not self-deleted — no double delete.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 23:31:48 -04:00
torlando-agent[bot]andClaude Opus 4.8 afd374ff3f fix(tcp): publish CONNECTED before _reconnected so the announce isn't dropped (greptile)
Storing _reconnected before _conn_state=CONNECTED left a seq-cst window where the
main loop could observe _reconnected==true while still CONNECTING. check_reconnected()
would then clear the flag and announce on an offline interface (loop() returns
early), so no announce fired once actually connected. Store CONNECTED first; seq-cst
then guarantees _reconnected is only ever observed true on an online interface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 23:02:25 -04:00
torlando-agent[bot]andClaude Opus 4.8 7be3138fc5 fix(tcp): drop redundant _online write in the task to remove a race (greptile)
task_loop() set `_online = true` during the CONNECTING window, which races with
loop()'s `_online = false` on the main loop (plain bool, no synchronizes-with).
It's redundant: the main loop sets `_online = true` when it observes CONNECTED.
Removing it eliminates the race with no behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 22:54:49 -04:00
torlando-agent[bot]andClaude Opus 4.8 4a9e44318c fix(tcp): make _task_running/_task_done atomic (greptile)
Both are written by task_loop() (core 0) and read by stop() (core 1); volatile
gives no cross-core ordering. Use std::atomic<bool> to match the other shared
flags (_conn_state, _reconnected, _last_connect_attempt) and give stop()'s join
a well-defined happens-before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 22:35:53 -04:00
torlando-agent[bot]andClaude Opus 4.8 a1a5104c5b fix(tcp): seed _last_connect_attempt so the initial connect isn't delayed (greptile)
With _last_connect_attempt == 0, task_loop()'s first
`now - _last_connect_attempt >= RECONNECT_WAIT_MS` check only passes once
millis() >= RECONNECT_WAIT_MS, delaying the very first connect up to 15s after
boot. Seed it to millis() - RECONNECT_WAIT_MS in start() so the first attempt
fires immediately (unsigned wraparound keeps it correct when millis() < the wait).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 22:30:11 -04:00
torlando-agent[bot]andClaude Opus 4.8 7148859582 fix(tcp): real task join in stop() + atomic _last_connect_attempt (greptile)
- stop() now waits on a _task_done flag the task sets right before exiting,
  instead of a fixed sleep. Closes a use-after-free window where an in-flight
  connect() overrunning CONNECT_TIMEOUT_MS (slow DNS) could touch `this` after
  ~TCPClientInterface() freed it.
- _last_connect_attempt is now std::atomic<uint32_t> — it's read/written by
  task_loop() (core 0) and handle_disconnect() (core 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 22:23:03 -04:00
torlando-agent[bot]andClaude Opus 4.8 988e42c52c fix: run TCP interface connect on its own task (instant UI / screen wake)
The blocking WiFiClient.connect() ran on the main loop and stalled it for the
lwIP default (~18.5s) when the host was unreachable -- including the DNS lookup --
freezing the UI, so the screen took ~10s to wake. The 2-arg connect() also
ignored CONNECT_TIMEOUT_MS (that only bounds reads).

Move only the blocking connect() to a dedicated FreeRTOS task. read/write/frame
stay on the main loop exactly as before (unchanged low-latency data path -- the
link/Resource timing is untouched). An atomic _conn_state hands _client ownership
between the task (while CONNECTING) and the main loop (while CONNECTED) so they
never touch the socket concurrently. Bound the connect via the 3-arg connect()
and back off retries to 15s.

tests/hardware: wait_for_tcp_link() matched "started", keying on interface
startup rather than the actual connect. With the async connect that let the
harness drive the announce before the link was up, so the device's announce was
lost and the first direct message (bz2-probe) failed. Match "connected to".

Verified on a T-Deck: screen wake instant (connect off the main loop); e2e smoke
5/5 including bz2-on-receive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 22:14:45 -04:00
torlando-agent[bot]andClaude Opus 4.8 f0a78f70e1 fix: derive OS::time() offset from the 64-bit rolling uptime, not millis()
The offset was computed as unix_ms - millis() (32-bit) but ltime() adds it to a
64-bit (high32<<32|low32) counter. Once high32 != 0 (a millis() rollover, or an
erratic screen-off/wake tripping ltime()'s roll-over check) OS::time() was wrong
by high32*49.7 days -- surfacing as garbage "Nw ago" timestamps in the
conversation list. Compute the offset from OS::ltime()-getTimeOffset() at both
the GPS and NTP sync sites; add a diagnostic logging OS::time() after each sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:49:44 -04:00