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
- 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
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
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
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
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
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
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
VoiceFilterChain::applyHighPass collapsed to a constant-gain
multiplier instead of a real high-pass response. The inner loop
read `samples[prevIdx]` as "previous input" — but that slot held
y[n-1] (output) since the previous iteration had overwritten it.
Substituting that into the formula
y[n] = α(y[n-1] + x[n] - x[n-1])
with x[n-1] := y[n-1] gave
y[n] = α(y[n-1] + x[n] - y[n-1]) = α · x[n]
i.e. just a fixed gain ≈ 0.81 at 300Hz cutoff / 8kHz. DC offsets
sailed through; the chain only kept signal levels reasonable
because the AGC stage downstream pulled the residual toward target.
The same bug exists in upstream LXST-kt's
native_audio_filters.cpp (filed as LXST-kt#13).
Fix: walk per-channel with explicit `xPrev` / `yPrev` variables so
input history isn't clobbered by the output write. Per-chunk save
of `lastInputs[ch]` now stores the actual last input, not the last
output, so the first sample of each new chunk uses the correct
x[n-1].
Test `dc_offset_attenuated_by_hpf` updated to assert tail RMS
< 0.01 (was < 0.5 — accommodating the broken behavior). Pass.
End-to-end acoustic test (Mac speaker → T-Deck mic): pyxis_rms
went up from 4378 → 5835, bot_rms from 1194 → 1517 — DC offset
removal lets clean signal through better.
Pyxis-side fix only. LXST-kt should pick up the same fix
upstream — see project_lxst_hpf_filter_bug.md in the vault.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile review feedback on PR #21:
ACCEPT:
- test_patch_nimble.py:151 (P1) — replace dead `if False else True`
ternary with a real assertion that "already applied" is absent on
the first run.
- test_patch_nimble.py:247 (P1) — invoke the shim subprocess via
`sys.executable` instead of hardcoded `/usr/bin/python3` so CI's
setup-python interpreter is used consistently.
- workflows/test.yml:50 (P2) — include hash of
deps/microReticulum/platformio.ini in PlatformIO cache key so the
cache invalidates when dependencies change.
MODIFY (narrowed):
- test_ring_buffers.cpp:209 (P2) — keep both `write(data, 0)` and
`write(data, -1)` assertions, but add a comment clarifying that
EncodedRingBuffer::write() takes signed `int length` (not size_t),
so -1 hits the `length <= 0` branch — same as 0. Greptile's
premise (size_t wrap to SIZE_MAX) does not apply to this codebase.
The two assertions lock the contract in case the param is ever
migrated to size_t.
REJECT (silently — no public reply per agent policy):
- test_audio_filters.cpp:237 (P1) — VoiceFilterChain::process()
takes `numSamples = frames * channels` per the documented
contract in audio_filters.h:33-40, and the implementation does
`numFrames = numSamples / channels_` (audio_filters.cpp:63). The
multichannel test correctly passes `(int)samples.size() = 8000`
(4000 frames * 2 channels). No out-of-bounds read occurs.
Standalone C++ tests of pyxis-unique code (BLE fragmenter/reassembler,
peer manager, GATT op queue, LXST ring buffers, audio filters, HDLC
framing) plus Python tests of the patch_nimble.py build script.
Each C++ test is compiled directly by clang++/g++ with shims in
tests/native/ (Bytes.h, Log.h, Utilities/OS.h) so pyxis sources can build
without microReticulum's full Arduino/MsgPack dep tree. A pytest wrapper
per test compiles, runs, and parses the summary line — the whole suite
is one command: `pytest tests/build_scripts tests/native -v`.
Total: 13 pytest tests, ~72 underlying C++ assertions, 3.4s.
Surfaced an HPF-formula bug in lxst_audio (mirrored upstream in
LXST-kt/native_audio_filters.cpp) — filed as LXST-kt#13 and tracked
in the corresponding test with a TODO link.
CI workflow runs the pyxis pytest suite plus the clean-passing
microReticulum native17 unit tests (94/114 of the existing fork
test/* suites) on push and PR.
PCM_RING_FRAMES 16→50 (320ms→1000ms capacity) and
PREBUFFER_FRAMES 3→15 (60ms→300ms prebuffer) to match
LXST-kt's buffering strategy. Interop test suite confirms
zero underruns with ±100ms jitter at these settings.
Also adds tests/interop/ with 48 Python tests verifying
wire format, codec round-trip, and pipeline compatibility
between Pyxis, Python LXST, and LXST-kt implementations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>