Commit Graph
197 Commits
Author SHA1 Message Date
torlando-agent[bot]andClaude Opus 4.8 7398f82a76 fix(store): raise microStore value cap to 2048 so larger path entries persist
Reticulum path-table entries for destinations with larger announce app_data run
1025-1033 bytes, over microStore's 1024-byte USTORE_MAX_VALUE_LEN default. They
hit "[ustore] put: failed due to excessive data length" and never entered the
path table ("Failed to add destination ... to path table!"), so those peers were
unreachable even though the TCP link was up and receiving their announces -- it
looked like "can't connect to the TCP server." Raise the cap to 2048; this also
sizes TypedStore's read buffer (reads into a vector of USTORE_MAX_VALUE_LEN), so
reads of the larger values don't truncate. Verified on device: zero
excessive-length / failed-add errors, path table populates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 21:03:37 -04:00
TorlandoandGitHub 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
TorlandoandGitHub 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
TorlandoandGitHub 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
TorlandoandGitHub 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
TorlandoandGitHub 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
TorlandoandGitHub 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
torlando-agent[bot] 0c9f0e67a9 fix: actually stage the M-files for the bluedroid drop
Companion to 21b0e96 which deleted BluedroidPlatform.{cpp,h} but
missed staging the three edited files that complete the cleanup:

  - platformio.ini: remove the [env:tdeck-bluedroid] env block.
  - .github/workflows/build-check.yml: drop tdeck-bluedroid from
    the CI matrix.
  - lib/ble_interface/BLEPlatform.cpp: drop the
    USE_BLUEDROID-gated factory branches.

Same `git add` short-arg trap as the eridanus mishap earlier today
— specifying non-existent paths aborts the add before reaching the
M-file paths. Mental-model fix: stage M-files in a separate `git add`
from the staged-deletes.
2026-05-11 09:28:57 -04:00
torlando-agent[bot] 21b0e964bc chore: drop bluedroid BLE backend — NimBLE is canonical
The bluedroid BLE stack hasn't been the runtime path for a while;
NimBLE-Arduino is the canonical backend (lighter heap, more modern
API, what the live ble_interface uses). The bluedroid env+code were
still being maintained as a CI matrix entry, and just started
failing on the current branch — no value to keeping it green.

Changes:
  - platformio.ini: remove the `[env:tdeck-bluedroid]` env block
    entirely (was 159 lines, near-duplicate of [env:tdeck] modulo
    `-DUSE_BLUEDROID`). Also remove the stale comment header that
    used to sit above it.
  - .github/workflows/build-check.yml: drop `tdeck-bluedroid` from
    the build matrix.
  - lib/ble_interface/platforms/BluedroidPlatform.{cpp,h} deleted
    (2 files, ~81 KB / ~2000 LOC of dead code — all gated behind
    `#if defined(USE_BLUEDROID)` which can no longer be defined).
  - lib/ble_interface/BLEPlatform.cpp: drop USE_BLUEDROID-gated
    factory branches (PlatformType::ESP_IDF case + the BluedroidPlatform.h
    include + the "Bluedroid takes priority" detection clause).

Left in place:
  - PlatformType::ESP_IDF enum member in BLETypes.h — dormant value,
    not worth a coordinated removal sweep.
  - USE_BLUEDROID build flag was already absent from [env:tdeck]'s
    flags (this env always used NimBLE in production); just no
    longer ever defined anywhere.

Build verified clean: `pio run -e tdeck` succeeds with the same
27.4% RAM / 79.8% Flash shape.
2026-05-11 09:28:27 -04:00
torlando-agent[bot] d7b308b80b chore(deps): pin microStore to ceea8f5 — match the conformance bridge SHA
The microStore dep was the last unpinned third-party library, leaving
pyxis tracking the moving `master` branch while microReticulum and
microLXMF are SHA-pinned. Pin to the same commit the microLXMF
conformance bridge already validates against (ceea8f5 — "Added SD
filesystem and enhanced Flash filesystem", 2026-04-14). Newer commits
on master introduced a dynamic segment-size refactor (pre-0.1.6) that
hasn't been validated against pyxis's MessageStore / path-store usage
patterns; pinning here freezes the version until that's done.

Verified: fresh `pio run -e tdeck` re-fetches the pinned SHA into
.pio/libdeps/tdeck/microStore (`git log -1` confirms `ceea8f5`),
builds clean — same 27.4% RAM / 79.8% Flash shape.
2026-05-11 01:52:29 -04:00
torlando-agent[bot]andClaude Opus 4.7 d22e1fd186 chore(greptile): iteration 5 — applied 1, rejected 0
SDArchiveFileSystem.h: split listDirectory's SPI bus mutex hold into
per-entry acquire/release cycles. The previous all-or-nothing hold
scaled with archive size; a directory with thousands of entries
could keep the bus locked past LoRa/display's 500 ms acquire timeout
(LoRa TX would fail silently, display would tear, RX FIFO could
overflow under inbound flood). The new pattern records each entry's
name to a std::string, releases the bus, invokes the callback (or
appends), then re-acquires for the next entry. Callback runs outside
the critical section. SD cursor lives in the root File so other-CS
bus users (display CS, LoRa CS) between iterations don't clobber it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:39:25 -04:00
torlando-agent[bot]andClaude Opus 4.7 4c5ca8cbc8 chore(greptile): iteration 4 — applied 1, rejected 0
patch_msgpack.py + patch_filestore.py: read PIOENV instead of
hardcoding "tdeck" in the libdeps path. The hardcode meant the
msgpack public-modifier patch silently no-op'd under tdeck-bluedroid
(and tdeck-ota), making microLXMF's packRawBytes / raw_data /
indices accesses fail to compile. Mirrors the pattern already used
in sync_file_libdeps.py. Resolves the failing tdeck-bluedroid build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:30:50 -04:00
torlando-agent[bot]andClaude Opus 4.7 ec19df9d5e chore(greptile): iteration 3 — applied 1, rejected 0
SDArchiveFileSystem.h: guard release_bus() in FileImpl::close() on
the acquire_bus return value, matching the pattern every other
method already uses. Previously close() (which is also called from
~FileImpl) issued an unconditional xSemaphoreGive even when
acquire_bus(500) timed out, skewing the SPI bus mutex counter on
each over-release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:20:33 -04:00
torlando-agent[bot]andClaude Opus 4.7 a88983aa05 chore(greptile): iteration 2 — applied 1, rejected 0
main.cpp: split on_wifi_connected NTP sync into kick-off +
pump_ntp_sync_if_pending poller. Previously the synchronous
getLocalTime retry loop blocked loopTask for up to 10 s on first
WiFi-associate, stalling RNS packet ingestion / LXMF delivery /
SX1262 RX FIFO drain. Now configTzTime kicks off the SNTP task
non-blocking and the periodic loop probes for completion until
NTP_TIMEOUT_MS elapses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:12:36 -04:00
torlando-agent[bot]andClaude Opus 4.7 3391cb2fb3 chore(greptile): iteration 1 — applied 2, rejected 3
- main.cpp: split T:RX `count=` from ring-buffer index so soak-test
  harness sees true received-message count past TEST_RX_RING=32
- sync_file_libdeps.py: drop hardcoded ~/repos/microReticulum, read
  PYXIS_MICRORETICULUM_DIR env var for opt-in local-override workflow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:02:42 -04:00
torlando-tech a9d79660a4 sec: move PYXIS_TEST_TCP_HOST/PORT to env vars + .env.example
The previous hardcoded LAN IP in platformio.ini identified a specific
deployment (Mac rnsd at a 10.0.0.x address). Move host:port to env
vars read at build time via PIO's ${sysenv.X} interpolation, with
a .env.example template + .env gitignored. Production builds (no
PYXIS_TEST_HOOKS) skip the macros entirely.

Changes:
  - platformio.ini (both env blocks): replace literal IP/port with
    ${sysenv.PYXIS_TEST_TCP_HOST} / ${sysenv.PYXIS_TEST_TCP_PORT}.
    Both quoted so an unset env var expands to an empty string
    literal "" rather than an empty token (the latter would break
    the PORT > 0 numeric compare in main.cpp).
  - src/main.cpp test-hooks block: guard host/port assignment so
    empty values fall back to NVS-stored settings. atoi() handles
    the port string side. Missing env var won't silently brick
    test mode — it just becomes "use whatever's in NVS".
  - .env.example: checked-in template with placeholder values
    (TEST-NET-1 192.0.2.x range, not a real LAN IP).
  - .gitignore: add `.env` so the populated local copy stays out
    of source.

Build: verified clean with env vars UNSET (NVS-fallback path) and
SET (override path); 27.4% RAM, 79.8% Flash in both cases.
2026-05-11 00:06:35 -04:00
torlando-tech a0419fcdb4 deps(codec2): swap vendor for sh123/esp32_codec2@^1.0.7
sh123/esp32_codec2 PR #4 (merged Jan 6 2026) updated the bundled
codec2 to the modern stable line — addresses the v0.9.2-vs-pycodec2-
v1.2.0 interop gap that motivated vendoring back on May 8. With the
version gap closed, the standalone vendor in lib/codec2/ becomes
pure noise: 42 .c source files (~2.9 MB tree) sitting in the repo
when a single lib_deps line plus two build_flags get the same
behaviour.

Changes:
  - platformio.ini lib_deps (both env blocks): `codec2` (vendored)
    → `sh123/esp32_codec2@^1.0.7`. Comment notes the version
    history so future eyes don't re-vendor.
  - platformio.ini build_flags (both env blocks): add
    `-D__EMBEDDED__` and `-DMEMORY_CRITICAL` so codec2's codebook
    tables land in flash (.const) instead of BSS. Moved here from
    lib/codec2/library.json's `build.flags`. Without these, ~127 KB
    of codebook data competes with LVGL's framebuffer in BSS.
  - lib/codec2/ (vendored tree) deleted — 106 files, ~43,000 lines.
    PIO will fetch sh123 into .pio/libdeps/tdeck/esp32_codec2/ on
    first build.

Size impact:
  - RAM: 27.4% → 27.4% (no change; codebooks were already in flash
    via __EMBEDDED__).
  - Flash: 77.0% → 79.8% (+~88 KB). sh123 ships the full codec2
    source (69 .c files) vs the trimmed vendor (42 files): unused
    paths like FreeDV, OFDM, COHPSK, FSK get linked in. Well below
    the 100% partition limit; can revisit with `lib_ignore` if
    flash pressure increases.

This is a real-functional change vs the rest of this PR which is
purely build-config rewiring. Audio fidelity should be unchanged
(sh123 carries the same codec2 SHA range as the vendor), but worth
revalidating with the codec2 fidelity test against pycodec2 before
merging.
2026-05-10 23:40:15 -04:00
torlando-tech adad674f66 chore(deps): switch microLXMF + microReticulum to public github URLs
microLXMF flipped public today; microReticulum's pyxis-fixes-on-0.3.0
fork branch has been public throughout. Both are now reachable
without machine-local clones, so the file:// pin + lib/microLXMF
symlink workarounds can go.

Changes:
  - platformio.ini lib_deps: replace `file://~/repos/microReticulum`
    with `https://github.com/torlando-tech/microReticulum.git#3ee2bd8`
    (the same SHA the conformance bridge fetches).
  - platformio.ini lib_deps: add `https://github.com/torlando-tech/microLXMF.git
    #9876dff` (main HEAD after PR #4) as an explicit lib_dep.
  - lib/microLXMF symlink to ~/repos/microLXMF removed — no longer
    needed; PIO will fetch into .pio/libdeps/tdeck/microLXMF/ on
    first build.
  - platformio.ini build_flags: remove `-Ideps/microReticulum/src`
    from both [env] sections. The hardcoded -I path was shadowing
    PIO's auto-include of .pio/libdeps/tdeck/microReticulum/src/
    (build_flags are searched first), letting the submodule's
    potentially-stale headers win over the freshly-fetched git pin.
    Now headers come from the same SHA as the .cpp source.

Pin-coordination note (in the inline comment): bump both SHAs in
tandem if either upstream changes, AND keep them in sync with
microLXMF/conformance-bridge/CMakeLists.txt's FetchContent_Declare
tag — otherwise the bridge tests against a different microReticulum
than the firmware does.

Build: `pio run -e tdeck` succeeds with the new deps, 27.4% RAM,
77.0% Flash — same shape as before. Verified both deps fetched
from github with `git config --get remote.origin.url` in
.pio/libdeps/tdeck/.
2026-05-10 23:26:45 -04:00
torlando-tech 48382b2f9f feat: T:SCREENSHOT + T:SHOW serial commands + host-side capture
New testing/docs surface for grabbing the active LVGL screen as a
PNG over USB-CDC. Useful both for documentation (round-trip capture
of every public screen via T:SHOW <name> + T:SCREENSHOT) and
automated UI regression tests.

On-device:
  - lib/lv_conf.h — enable LV_USE_SNAPSHOT (~5 KB code; uses PSRAM
    via the existing hybrid allocator so internal RAM is unaffected)
  - src/main.cpp — T:SCREENSHOT handler takes an lv_snapshot_take()
    of lv_scr_act() under LVGL_LOCK(), dumps a delimited base64
    stream over CDC. Inlines a tiny base64 encoder (no new dep).
  - src/main.cpp — T:SHOW <name> dispatches to UIManager::show_*()
    for the six publicly-navigable screens (conversation_list,
    compose, announces, status, settings, propagation_nodes).

Wire format:
  T:SCREENSHOT BEGIN W=320 H=240 FMT=rgb565<be|le> BYTES=153600
  <base64 line, 76 chars>
  ...
  T:SCREENSHOT END

Host side:
  - screenshot.py — auto-detects the pyxis port via T:ID probe,
    sends T:SCREENSHOT, reads until END, filters out interleaved
    log lines (heap heartbeats / BLE stats can splice in
    mid-dump), validates byte count matches header, decodes RGB565
    with the documented byte order, expands channels via 5→8 / 6→8
    high-bit replication, saves PNG. Pillow + pyserial.

Catalog:
  - docs/serial_commands.md — full reference for all T:* commands
    accumulated so far (identity/paths/send/receive, propagation,
    voice, BLE, UI). New commands should land here when added.

Throughput: ~205 KB base64 over CDC at 115200 → ~18 s/shot. Fine
for docs and automated tests, not video. Bumping baud or zlib-
compressing on-device is queued in the doc as future work.
2026-05-10 15:23:39 -04:00
torlando-tech 76ffd29b01 feat(ble): TX/RX fragment + byte counters in BLE heartbeat
Pre-this the 10s heartbeat reported running/scanning/connected/peers
state but nothing about whether data was actually flowing. With the
counters added to BLEInterface and threaded into the heartbeat
snprintf, the line now also surfaces:

  tx_pkt   — outbound RNS packets attempted
  tx_frag  — BLE fragments actually written/notified
  tx_b     — total bytes written
  tx_fail  — platform write/notify returned false
  rx_frag  — BLE fragments handed to the reassembler
  rx_b     — total bytes received

That was enough to root-cause the Columba-side stalls observed
during the BLE end-to-end testing session: pyxis showed connected=1
but tx_pkt frozen, surfacing that the keepalive loop wasn't firing
for a peer whose handshake had completed but identity recording
raced. Cumulative-since-start, no reset; cheap to keep on always.
2026-05-10 15:23:09 -04:00
torlando-techandClaude Opus 4.7 cbac8ed5ca fix(ble): enable NimBLE scan response so service UUID + name fit; add T:BLE hook
Two related changes:

1. NimBLE advertising overflow

   At boot pyxis was logging "NimBLEAdvertisementData: Data length
   exceeded" twice. The 128-bit Reticulum service UUID is 18 bytes
   once you include the AD type+length headers; the device name
   "TD-XXXXXX" is another 9-11 bytes; flags eat 3 bytes. That's
   already over the 31-byte legacy adv-packet limit, so NimBLE was
   silently truncating the advertisement and dropping the service
   UUID. Android Columba's BleScanner filters by ServiceUuid at the
   Android BLE driver layer (ScanFilter.Builder().setServiceUuid),
   so without the UUID in the primary adv data, pyxis was invisible
   to Columba.

   Fix: call enableScanResponse(true) BEFORE addServiceUUID +
   setName. NimBLE then routes the long device name into the
   secondary 31-byte scan-response payload that active scanners
   request, leaving the primary adv data with just flags + the
   service UUID — under budget and visible to the filter.

   Verified: with the fix, Android system Bluetooth reads pyxis's
   name as "TD-46cbcf" and Columba's BleGattServer logs
   "Central connected: FC:69:15:9C:B2:C9" (pyxis as central). The
   connection holds for ~40s before HCI_CONN_TIMEOUT — separate
   issue not addressed here, just the unblock so the link can be
   established at all.

2. T:BLE on|off harness hook

   Mirrors T:CALL_PROFILE / T:ANNLXST / T:LXSTDEST: persists the
   ble_en NVS key and starts/stops the interface live so the LXMF
   harness can flip BLE on/off the same way it drives any other
   subsystem. Idempotent for "already on" / "already off". Useful
   for upcoming pyxis ↔ Android Columba BLE smoke tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:21:08 -04:00
torlando-techandClaude Opus 4.7 2504fefa66 fix(autointerface): explicit mld6_joingroup + LOOP + RX/TX diagnostics
Three changes, motivated by debugging "Sideband + pyxis on the same
WiFi don't hear each other's announces":

1. Always call \`mld6_joingroup_netif()\` in addition to \`setsockopt
   IPV6_JOIN_GROUP\`. On ESP-IDF lwIP, the setsockopt path returns
   success but doesn't reliably push the multicast hash into the
   WiFi MAC filter — incoming multicast frames get silently dropped
   at L2. Calling the netif's mld6 API directly programs the chip
   filter. Joining twice on the netif is refcount-safe.

2. Set IPV6_MULTICAST_LOOP=1 so pyxis receives its own multicast
   echoes. ESP-IDF lwIP defaults this off, which makes upstream's
   "carrier lost / multicast echo timeout" warning fire even on a
   functioning network. With LOOP=1, the initial-echo path actually
   works on isolated test setups too. Logged as DEBUG if the
   platform doesn't support the option.

3. Add a periodic \`AutoInterface: stats announce_tx=N tx_fail=N
   disc_rx=N disc_self=N data_rx=N peers=N\` heartbeat (every 10s).
   Without this it's hard to tell whether pyxis isn't sending,
   isn't receiving, or is sending+receiving but rejecting the
   tokens. Discovery-RX from non-self addresses with bad tokens
   now also logs once with the hex prefix so token-mismatch cases
   are visible (group_id drift, scope-suffix encoding mismatches).
   Added _initial_echo_received update on first self-echo so the
   firewall warning at startup_grace fires correctly.

After this, pyxis's own multicast loopback works (disc_self=N
matches announce_tx=N within 10s). Cross-LAN multicast against
rnsd / Sideband still doesn't make it through, which is an ESP32
WiFi multicast TX limitation — pyxis's frames aren't reaching the
AP. Not a fix here; the diagnostics make the boundary visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:29:33 -04:00