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
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
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>
Local patch via patch_filestore.py for upstream attermann/microStore
issue: \`finalize_compaction()\` and \`clear()\` both call
\`_filesystem.remove()\` on segment files without first closing
\`active_file\`. On filesystems that don't auto-close FDs on unlink
(LittleFS / FAT) the descriptor leaks. Over enough compaction cycles
on pyxis the path-store eventually can't open new files.
Both \`open_segment()\` and \`rotate_segment_if_needed()\` already
close \`active_file\` before reopening — the unlink paths just
forgot. Added \`if (active_file) active_file.close();\` at the top
of each.
Validated: 2-round LXMF soak post-patch, 8 pass / 2 fail, identical
to pre-patch baseline (the 2 fails are the known propagation timing
flake unrelated to FD handling). Patch applies cleanly and re-applies
on every build via the pre-build hook.
Will be reverted once the upstream lands the same fix; tracked in
the vault TODO at "80 Assistant/Memory/pyxis/upstream_patches.md".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-LXST 14s call at ULBW (Codec2-700C) was timing out T:CALL_QOS /
T:CALL_STATS responses ~10-15s in. Pyxis itself was still processing
audio fine; the host's serial reader was just overrun by debug-level
prints from three sources, all firing per-packet during voice traffic:
1. TCPClientInterface: per-frame "[TCP] Reading X bytes" / "[TCP]
First bytes: ..." / "[HDLC] Frame #N: ..." / "[TCP] Processing
frame" / 5s "[TCP] connected= ..." were unconditional Serial.printf.
Now gated behind `RNS::loglevel() >= LOG_DEBUG` and the snprintf
work skipped when it'd be discarded.
2. i2s_capture.cpp: "[CAP] rate=" fired every 2s regardless of
activity. Now only emits when ringDrops > 0 OR runningPeak > 1000
(something happened worth noting). Counters still update — only
the print is gated.
3. microStore upstream: "[ustore] get: key not found in index" fires
on every path-store miss, which RNS hits constantly during a call.
patch_filestore.py was already a registered pre-build script for
diagnostic patches; reactivate it (was commented out in
platformio.ini) and add a silence patch as the always-on default.
Diagnostic exists()/put() patches gated behind PYXIS_FILESTORE_DIAG=1
so they're easy to bring back when investigating path-store drift
without touching the script each time.
After this, the ULBW real-LXST call validator returns PASS with full
final stats (pyxis_tx=34 rx=119 decode_ok=151 decode_fail=0
pyxis_rms=4410). 1600bps/3200bps profiles still hit serial-timeout
patterns under sustained TX — likely CPU saturation in the main
loop, separate from this fix; tracked in #75 followup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a USB-CDC serial command interface gated behind PYXIS_TEST_HOOKS
that lets a host-side harness drive pyxis end-to-end without UI taps.
Built specifically to run /tmp/tdeck_harness.py and prove LXMF DIRECT,
OPPORTUNISTIC, and PROPAGATED delivery against a Mac-side echo bot
over the Mac's rnsd + lxmd.
Commands (all newline-terminated, replies T:OK or T:ERR):
T:DEST — pyxis's delivery dest hash (hex)
T:ID — pyxis's identity hash (hex)
T:ANN — force an announce
T:PATHS — count + dump in-memory path table
T:HASPATH <hex> — Transport::has_path + in-memory check
T:RECALL <hex> — Identity::recall_app_data hex
T:SEND <hex> <text> — outbound DIRECT LXMessage
T:SENDOPP <hex> <text> — outbound OPPORTUNISTIC LXMessage
T:SENDPROP <hex> <text> — outbound PROPAGATED LXMessage
T:SETPROP <hex> <stamp_cost> — set outbound propagation node
T:SYNCPROP — request_messages_from_propagation_node
T:SYNCSTATE — current PR_* sync state
T:STATE <msg_hash> — LXMessage state for a tracked send
T:RX — drain inbound RX ring
T:RXCLR — clear RX ring
Build-flag side:
-DPYXIS_TEST_HOOKS — gates all of the above
-DPYXIS_TEST_TCP_HOST="..." — hard-overrides NVS tcp_host so the
harness's rnsd is the only target
-DPYXIS_TEST_TCP_PORT=... — same for tcp_port
Also: replaces `lib_extra_dirs = deps/microReticulum` with an explicit
`file://~/repos/microReticulum` lib_dep. lib_extra_dirs
caused PIO to compile microReticulum twice (once through the extra
dir, once through microLXMF's transitive auto-fetch), producing two
copies of `Transport::_path_store` in BSS. Different translation
units linked against different statics, so put() and exists() landed
in different in-memory indexes. Symptom: `T:HASPATH` returned 0 even
when the previous announce's `[ustore] put: wrote key` log line was
visible. Single source path → single static → consistent reads.
`patch_filestore.py` is committed but commented out in extra_scripts
— used during diagnostic when the dual-static issue was being
triaged. Easy to re-arm if FileStore put/exists drift recurs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>