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 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