Commit Graph
212 Commits
Author SHA1 Message Date
torlando-agent[bot]andClaude Opus 4.8 0922c0de31 fix(voice): send real batch_len, not hardcoded BATCH_BYTES=82, on the call wire
call_send_audio_batch shipped a fixed 82-byte bin8 payload+length, correct only for 8-B/frame Codec2 (3200/1600). The ULBW default is 700C at 4 B/frame -> a 42-B batch, so every packet carried 40 B of uninitialized stack and a bin8 length that lied (82 vs 42): pyxis<->pyxis decoded to SILENCE (RX 6400 > 5120 output guard -> ring underrun), pyxis->a length-driven peer (Columba/Python) decoded 10 real + 10 stack-noise frames = audible GARBAGE. Use the batch_len the caller already computes. ULBW stays default (LoRa-first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-24 23:19:57 -04:00
Torlando 6f9ebf5570 Merge pull request #38 from torlando-tech/fix/ota-hardening
Build and Deploy Firmware / build-and-deploy (push) Failing after 7m55s
Test / Pyxis pytest suite (build_scripts + native) (push) Successful in 28s
Test / microReticulum native unit tests (PlatformIO native17) (push) Successful in 2m2s
chore(build): unify pre-script libdeps paths on a shared helper + OTA log accuracy
2026-06-23 01:11:55 -04:00
torlando-agent[bot]andClaude Opus 4.8 96d007870c fix(tests): add build_scripts conftest so _build_helpers is importable on CI
The pre-scripts now import their sibling _build_helpers; the build_scripts tests
load those scripts in-process, so the import needs the repo root on sys.path. CI
runs bare `pytest` (which, unlike `python -m pytest` locally, does NOT add the cwd
to sys.path), so the import failed only there. A conftest.py that puts the repo
root on sys.path fixes it for any invocation. (The standalone-exec subprocess test
got its own sys.path line in the prior commit.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-23 00:22:12 -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 41e6eb72a2 Merge pull request #37 from torlando-tech/fix/settings-clock-ota
fix: announce/sync interval units+defaults, live Settings clock, OTA repair
2026-06-22 23:51:28 -04:00
torlando-agent[bot]andClaude Opus 4.8 b2236d5901 fix(settings): correct announce/sync prefs defaults + stale unit comments
Addresses Greptile review on #37. SettingsScreen has its OWN settings load
(separate from main.cpp's app_settings), which still defaulted announce to 60s and
sync to 3600s and overrode the intended defaults -- so the UI still showed the old
values. Set them to 3600s (1h) and 14400s (4h) to match. Also updated two stale
block comments in update_ui_from_settings / update_settings_from_ui that still said
"minutes" after the sync field was promoted to hours.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-22 23:37:07 -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 e18cee7323 Merge pull request #36 from torlando-tech/fix/gps-time-sync-quality-gate
fix(gps): quality-gate time-sync against GPS week-rollover (+ DST, Settings time/HDOP)
2026-06-22 22:52:39 -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 c208e9aaec feat(settings): show current time in GPS status + fix HDOP readout
- Add a live "Time:" line to Settings -> GPS showing the system clock (e.g.
  "Time: 2026-06-22 16:32:12", or "not set" when unsynced), so the GPS time-sync
  can be verified on-device at a glance -- a far-future year or "not set" flags a
  rollover/unsynced clock without needing serial.
- Fix the HDOP readout: it divided by 100 a second time (hdop.hdop() already
  returns the true value), so it always showed ~0.0 (Ideal). Now shows the real
  geometry -- which matters since HDOP is the metric the new sync-gate keys off.

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 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 d47c06e00f Merge pull request #35 from torlando-tech/fix/announce-list-path-table
Test / microReticulum native unit tests (PlatformIO native17) (push) Successful in 2m9s
Test / Pyxis pytest suite (build_scripts + native) (push) Failing after 14m32s
Build and Deploy Firmware / build-and-deploy (push) Failing after 14m44s
fix: announces end-to-end (ratchet name, random_blobs leak, path_table, safe refresh)
2026-06-21 12:32:28 -04:00
torlando-agent[bot]andClaude Opus 4.8 ba7304b41b docs: document the _path_table mirror as an upstream-migration stopgap
Bumps the microReticulum pin to 6054f6ba (a comment-only amend of the mirror
commit) and adds a pin-comment note: both _path_table and _new_path_table are
upstream (attermann/microReticulum is mid-migration to the microStore-backed
table and left path_table()'s in-memory source unpopulated). The mirror is a
stopgap to restore enumeration; the note + the long comment at the mirror site
say to DELETE it once upstream finishes the read-side migration, so a future pin
bump knows why the table can go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-21 12:19:32 -04:00
torlando-agent[bot]andClaude Opus 4.8 834f047200 address greptile: gather all before sort + re-add containers to focus group
- AnnounceListScreen::tick(): drop the pre-sort 64-item gather cap so the sort
  always sees the true newest destinations (the path table is bounded by
  USTORE_DEFAULT_MAX_RECS=400 and items allocate in PSRAM, so the gather is
  bounded); only the render stays capped at MAX_DISPLAY.
- tick() now re-adds the freshly rendered item containers to the focus group and
  focuses the first. The deferred gather meant show() (which adds widgets to the
  group) ran before the containers existed and lv_obj_clean dropped the old ones,
  so trackball navigation couldn't reach the list items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-21 03:11:14 -04:00
torlando-agent[bot]andClaude Opus 4.8 142ff464e8 fix: populate announces end-to-end (ratchet name, blob leak, path-table, safe refresh)
Pins microReticulum to the fork's pyxis-fixes-on-0.4.1 @ b582986 (three 0.4.1-graft
regression fixes) and adds the pyxis-side pieces.

microReticulum fixes (via the pin):
- Identity::recall now accounts for the announce ratchet (matches validate_announce)
  — it read app_data 32 bytes early for ratchet-bearing announces (Sideband/Columba),
  garbling peer display names.
- DestinationEntry caps persisted random_blobs to PERSIST_RANDOM_BLOBS (16); the set
  was never trimmed, growing path entries ~12B/re-announce past microStore's 1024B
  ceiling so put() rejected them (empty path table) — and under a raised ceiling
  bloated the FS until compaction crashed.
- Transport mirrors learned paths into _path_table so path_table() (the UI announce
  list + T:PATHS) reflects them; the microStore migration stopped populating it.

pyxis pieces:
- -DUSTORE_DEFAULT_MAX_RECS=400 caps the live path-store record count (defense-in-depth).
- AnnounceListScreen::refresh() defers to a main-loop tick(): the gather (path-table
  iteration + per-entry recall/app_data) runs off the LVGL lock, serialized with the
  main-loop path-table writes, instead of racing them on the LVGL task and blocking
  past the 5s lock timeout — which hung/crashed on open once the table was non-empty.

Verified on device: display names correct, T:PATHS 0 -> 11, no rejections, no
crashes, announce list opens cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-21 02:58:45 -04:00
Torlando 58e3b6d2ae Merge pull request #33 from torlando-tech/fix/wifi-auto-reconnect
Build and Deploy Firmware / build-and-deploy (push) Failing after 7m49s
Test / Pyxis pytest suite (build_scripts + native) (push) Successful in 25s
Test / microReticulum native unit tests (PlatformIO native17) (push) Successful in 1m58s
fix(wifi): auto-reconnect after the AP drops (was offline until reboot)
2026-06-20 20:44:55 -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 d7518eb187 Merge pull request #30 from torlando-tech/fix/convo-timestamp-clock-race-and-load-perf
fix: correct conversation timestamps (clock race) + ~2x faster message load
2026-06-20 19:27:20 -04:00
Torlando 4bc5186e33 Merge pull request #32 from torlando-tech/fix/chat-load-page-cap
fix(chat): cap chat-open render to avoid LVGL-lock timeout crash
2026-06-20 19:10:33 -04:00
torlando-agent[bot]andClaude Opus 4.8 e1760291f5 fix(chat): use lv_obj_del_async to close the full-message view (greptile)
on_full_message_close() runs from the Close button's own callback, and the button
is a descendant of the modal — so lv_obj_del(modal) freed the button mid-dispatch
(use-after-free). lv_obj_del_async defers the delete until the event completes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 17:22:47 -04:00
Torlando cdbdc4ac61 Merge pull request #31 from torlando-tech/fix/tcp-interface-own-task
Test / Pyxis pytest suite (build_scripts + native) (push) Successful in 24s
Test / microReticulum native unit tests (PlatformIO native17) (push) Successful in 1m52s
Build and Deploy Firmware / build-and-deploy (push) Failing after 14m34s
fix: run TCP connect on its own task — instant UI / screen wake
2026-06-20 17:20:43 -04:00
torlando-agent[bot]andClaude Opus 4.8 22e985087a feat(chat): long-press a message to view its full text (and copy)
Long-press a bubble to open a scrollable full-message view with Copy/Close.
Bubbles render truncated for scroll performance, so the handler recovers the FULL
stored content (row -> hash -> item) for the view -- which also fixes Copy, which
had regressed to copying the truncated label text after the render cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 14:42:02 -04:00
torlando-agent[bot]andClaude Opus 4.8 28c40de3a1 fix(chat): cap rendered bubble text so large messages don't crawl on scroll
A message with multi-KB content (e.g. a large bz2-delivered payload) rendered
untruncated, so LVGL laid it out as a 50+ line wrapped bubble and re-drew the
whole thing while scrolling past it -- crawling the UI. Cap the *displayed* text
to MAX_DISPLAY_CHARS; the full content stays stored. (Decompression is unrelated:
it happens once in Resource::assemble() at receive, content is saved already
decompressed, and load_message_metadata never re-decompresses.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 01:24:14 -04:00
torlando-agent[bot]andClaude Opus 4.8 13b3d7d73a fix(chat): stream older messages on scroll-up too (no synchronous batch)
on_scroll() loaded a full MESSAGES_PER_PAGE batch synchronously under the LVGL
lock, which froze scrolling. Make it trigger the same incremental
tick_background_fill() streaming as the open path instead. _bg_fill_active is now
std::atomic since on_scroll() (LVGL task) sets it while tick_background_fill()
(main loop) reads it; the target is written before the flag for visibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 01:07:51 -04:00
torlando-agent[bot]andClaude Opus 4.8 f8bbfba3e3 feat(chat): render newest messages first, stream the rest in on the main loop
Open a conversation by rendering only the 3 newest messages synchronously (fast),
then stream the rest of the first page in BG_FILL_BATCH (2) at a time from
UIManager::update() via tick_background_fill(). Each step holds the LVGL lock
only briefly, so a large conversation no longer freezes the UI or trips
LVGLLock's 5s timeout (which previously asserted/crashed). Runs on the main loop
rather than a task because MessageStore shares one _json_doc between save and
load and is not safe for concurrent access.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 00:27:54 -04:00
torlando-agent[bot]andClaude Opus 4.8 07c2f42284 fix(chat): cap chat-open render to 10 messages to avoid LVGL-lock timeout crash
ChatScreen::refresh() loads + renders MESSAGES_PER_PAGE messages while the LVGL
mutex is held (the open path runs on the LVGL task's lv_task_handler). On a 32+
message conversation with a memory-pressured heap, 20 LittleFS reads + parses
exceeded LVGLLock's 5s timeout and asserted (crash). Cap to 10 so the under-lock
work stays well under budget; older messages load on scroll.

This is a mitigation. The real fix is to do the message I/O off the LVGL lock
(load lock-free on the main loop, render under the lock) so a large conversation
neither freezes the UI nor risks the timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 00:15:13 -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 2625a9fa1e fix: correct conversation timestamps + ~2x faster message load
Bump microReticulum -> cb71ace and microLXMF -> 3cdde79.

microReticulum (cb71ace): OS::ltime() now uses the monotonic 64-bit esp_timer
instead of a 32-bit millis() rollover counter. The old static low32/high32
counter was not thread-safe -- concurrent ltime() calls from the transport/UI/
BLE tasks raced on `if (new_low32 < low32) high32++` and spuriously inflated
OS::time() by N*49.7 days. Conversation timestamps showed garbage ("Future",
"49w ago", a year-2046 clock). Verified on a T-Deck: the clock now holds at the
correct epoch instead of climbing ~19.7 years within minutes of runtime.

microLXMF (3cdde79): load_message_metadata() drops a redundant LittleFS open per
message (read_file returns 0 on a missing file, so the file_exists probe was
unnecessary) and parses with a JSON field filter that skips the large "packed"
hex blob. Roughly halves conversation-load time on ESP32/LittleFS (~1330ms ->
~670ms for 10 messages).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 20:55:18 -04:00
Torlando 88ec8c2523 Merge pull request #29 from torlando-tech/feat/graft-microreticulum-0.4.1
Build and Deploy Firmware / build-and-deploy (push) Failing after 7m53s
Test / Pyxis pytest suite (build_scripts + native) (push) Successful in 25s
Test / microReticulum native unit tests (PlatformIO native17) (push) Successful in 2m3s
Graft pyxis onto upstream microReticulum 0.4.1
2026-06-19 17:19:05 -04:00
torlando-agent[bot]andClaude Opus 4.8 3718f2702a test(native): add microReticulum/ shim headers for the 0.4.x include layout
The graft namespaced pyxis's BLE/HDLC sources to <microReticulum/Bytes.h> etc.,
but the standalone native unit-test build only had flat shims in tests/native/,
so test_ble_* and test_hdlc failed to compile ("'microReticulum/Bytes.h' file
not found"). Add forwarding shims under tests/native/microReticulum/ that
include the existing flat shims. Production code is unchanged; this only fixes
the test harness for the relocated layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 16:31:43 -04:00
torlando-agent[bot]andClaude Opus 4.8 7f35ce5eed test(harness): bot reads PYXIS_TEST_TCP_PORT for its rnsd target (greptile)
The echo bot hardcoded target_port=4242 in its RNS config, but run_e2e.sh bakes
PYXIS_TEST_TCP_PORT into the firmware's TCP target. With a non-default port the
bot kept dialing 127.0.0.1:4242 and silently failed to join the network
(echobot_announce_dest then timed out with a confusing error). Read the port
from the same env var (default 4242) so the bot and T-Deck share one rnsd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 16:20:46 -04:00
torlando-agent[bot]andClaude Opus 4.8 2ae570f811 test(harness): close serial on early exit, bound PN path wait, guard PIO_PY
Three non-blocking robustness nits Greptile noted in the on-device harness
(none touch firmware):
- tdeck_harness.py: t.close() on every early `return 1` so a failed run frees
  the USB serial device (otherwise the next run fails with "port in use").
- lxmf_echo_bot.py: bound the prop-syncer's path-acquisition loop (~5 min) so it
  can't spin forever when the PN is unreachable; PROPAGATED rounds just skip.
- run_e2e.sh: guard $PIO_PY is non-empty before exec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 16:10:52 -04:00
torlando-agent[bot]andClaude Opus 4.8 f8019dde90 test: fix echo-bot interpreter + drop dead var (greptile)
start_echobot() hardcoded /usr/bin/python3 -- use sys.executable (the harness's
own interpreter, which run_e2e.sh selects and which has rns/lxmf importable via
the bot's repo-path insert), overridable with PYXIS_BOT_PY. Also remove the dead
last_size variable in echobot_log_after().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:59:37 -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
torlando-agent[bot]andClaude Opus 4.8 092fbedc44 test: on-device LXMF e2e harness with bz2-on-receive probe
tests/hardware/ drives a flashed T-Deck through LXMF round-trips against a
Mac-side echo bot over the device's TCP-client -> rnsd link, via the firmware's
-DPYXIS_TEST_HOOKS T: command surface.

- run_e2e.sh: orchestrator -- autodetect serial port + Mac IP, verify rnsd on
  :4242, build+flash with the TCP target baked in, run the harness, assert.
- tdeck_harness.py: resets the device, establishes path/identity with the bot,
  then runs a bz2-on-receive probe + DIRECT/OPPORTUNISTIC (and PROPAGATED when
  PYXIS_PROPAGATION_NODE_HEX is set) round-trips, watching for crash signatures.
- lxmf_echo_bot.py: Mac-side LXMF echo bot; on a BZ2PROBE trigger it replies
  with a ~1.5KB highly-compressible payload so python LXMF sends a bz2-COMPRESSED
  Resource, exercising the receiver's decompress-on-receive path.

Parametrized -- serial port, Mac IP, and propagation-node hash all come from the
environment; nothing deployment-specific is committed.

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
torlando-agent[bot]andClaude Opus 4.8 70d4aa6be9 feat: graft pyxis onto upstream microReticulum 0.4.1
Repins microReticulum + microLXMF onto the upstream-0.4.1 graft and adapts
pyxis to the new src/microReticulum/ layout and 0.4.x APIs. The far-diverged
0.3.0 fork's Resource/Transport/Identity work is subsumed by upstream's
reimplementation; only the still-needed fixes ride on the pinned branches
(PKCS7/HMAC/X25519 crypto -- proven byte-identical to python RNS 1.3.1 --
Packet link-proof callback, Identity short-sig guard, and the bz2 layer +
decompress-on-receive in Resource::assemble()).

Consumer-side changes:
- platformio.ini: pin microReticulum @2f21fee (pyxis-fixes-on-0.4.1) and
  microLXMF @33760d0 (chore/microreticulum-0.4.1-layout); bump microStore
  ceea8f5 -> c5fb69d (0.4.x requires the new BasicFileStore::init API);
  -std=gnu++11 -> gnu++17 (upstream requires C++17).
- Namespace all microReticulum includes (angle + quote) to <microReticulum/...>
  for the relocated layout; shim-local Utilities/Stream.h|Print.h preserved.
- Interface::send_outgoing now returns bool: update TCP/BLE/SX1262/Auto
  overrides with correct success/failure returns.
- SDArchiveFileSystem::init(bool reformatOnFail=true) to match new microStore.
- Static Transport::get_path_table() -> path_table(); instance getter unchanged.
- Remove duplicate shim Cryptography/BZ2 (microReticulum provides it now; keep
  lib/libbz2 as the ESP32 bzlib provider).
- patch_littlefs_paths.py: normalize microStore's LittleFS adapter paths to a
  leading "/" -- ESP32 Arduino LittleFS rejects "./"-prefixed paths, which
  silently broke the path store (no peer paths learned, all messaging blocked).

Validated on T-Deck Plus: builds (RAM 27.5% / Flash 77.7%), boots stable
(no WDT/panic), and a full on-device LXMF e2e (DIRECT + OPPORTUNISTIC +
bz2-compressed-Resource receive) passes 5/5.

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
Torlando 95fe121a35 Merge pull request #26 from torlando-tech/chore/lib-deps-public-urls
Track A/B: microLXMF + microReticulum extraction + public-URL lib_deps
2026-06-19 15:46:46 -04:00
torlando-agent[bot]andClaude Opus 4.8 ed3ec541fd fix: defer ConversationListScreen display-name writes out of the LVGL lock
refresh() called set_display_name() (a microStore/LittleFS write) while the
LVGL lock was held by UIManager::update(). On a cold-boot announce burst,
refresh() writes a name per newly-seen peer, serially stalling the LVGL render
task for the combined I/O time. Mirror the on_message_received fix: accumulate
the write-throughs in _pending_name_writes during refresh(), and flush them at
the top of UIManager::update() before it takes the LVGL lock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 13:14:05 -04:00
torlando-agent[bot]andClaude Opus 4.8 4dc5c89e25 fix: scope LVGL lock to the snapshot in T:SCREENSHOT (not the serial dump)
LVGL_LOCK() was held for the entire base64 serial dump (~18s for a 320x240
frame), blocking the LVGL render task the whole time and tripping the 5s
LVGLLock recursive-take timeout assert in debug builds (crash on every
screenshot). lv_snapshot_take() copies the pixels into its own buffer, so the
lock is only needed for the snapshot itself -- scope it there, dump the copy
unlocked, and re-take the lock only to free the snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 10:53:33 -04:00
torlando-agent[bot]andClaude Opus 4.8 a1c548dbf2 fix: make PYXIS_FILESTORE_DIAG self-contained (define bin_str)
The diagnostic patch injected printf("...%s...", bin_str(key, key_len)) into
FileStore.h but never defined bin_str, so a PYXIS_FILESTORE_DIAG=1 build failed
with an undeclared-identifier error (normal DIAG=0 builds were unaffected).
Inject a standard-C hex-encode helper as a static member alongside the prints
so the diagnostic build is self-contained. Verified: the helper compiles clean
under gnu++11 -Wall -Wextra -Werror and round-trips keys correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 10:39:15 -04:00
torlando-agent[bot]andClaude Opus 4.8 ee059e86ea fix: restart AutoInterface on WiFi reconnect (greploop)
The `!auto_interface_impl` guard meant start_auto_interface() only ran on the
first WiFi-connect edge; on every reconnect the block was skipped, so
AutoInterface kept stale multicast sockets and peers never rediscovered until
reboot. start_auto_interface() is idempotent (its else-if(!online()) branch
rebinds the sockets), so drop the guard and call it on every connect edge.
TCPClientInterface self-reconnects in its own loop(), so only AutoInterface
needed this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 02:16:09 -04:00
torlando-agent[bot]andClaude Opus 4.8 d98b6f0682 chore: remove dead universal_filesystem dep + unused BLE rx counter
Greptile cleanup (greploop): universal_filesystem is no longer included by
main.cpp (migrated to microStore) -- drop the dead lib_dep so a clean build
doesn't pull its removed SPIFFS dependency. _stat_rx_packets_complete was
declared but never incremented or logged -- remove the unfinished counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-18 23:08:24 -04:00