Files
pyxis/docs/serial_commands.md
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

6.3 KiB

Pyxis serial commands

USB-CDC-driven test/debug command surface, gated behind -DPYXIS_TEST_HOOKS in platformio.ini. All commands and responses live on the same line-oriented serial channel as the regular log stream — host scripts are expected to skip non-T: lines while reading replies.

Convention

  • Request: T:<NAME>[ <args>]\n — sent to pyxis on the USB-CDC port (115200 8N1).
  • Reply: every command emits exactly one of:
    • T:OK [<payload>] — success
    • T:ERR <reason> — failure
  • A few commands stream multiple lines (e.g. T:PATHS, T:RX, T:SCREENSHOT). Each emits T:OK first with a count, then per-item lines, then nothing more until the next request.
  • Hex args are unprefixed (abcdef…) and accepted at full length unless otherwise noted (most are 16 bytes / 32 hex chars for LXMF destination hashes).

If T:ERR unknown cmd <X> comes back, the build either lacks PYXIS_TEST_HOOKS or <X> is misspelled.

Commands

Identity, addresses, paths

Command Args Reply Notes
T:ID T:OK <hex> Local Reticulum identity hash (32 hex).
T:DEST T:OK <hex> LXMF delivery destination hash (16 hex).
T:LXSTDEST T:OK <hex> LXST telephony destination hash. Used to share with the test bot before T:CALL.
T:ANN T:OK announced Force a fresh LXMF delivery announce.
T:ANNLXST T:OK announced Force a fresh LXST telephony announce. Required before pyxis-as-callee tests because the TCP-reconnect path only re-announces LXMF.
T:PATHS T:OK count=N then T:PATH <hex> per row Dump the in-memory path table.
T:HASPATH <hex> T:OK 0/1 mem=0/1 mem_count=N Has-path check + diagnostic split between disk-backed Transport::has_path and the in-memory _path_table.
T:RECALL <hex> T:OK <hex> or T:ERR not recallable Try to resolve <hex> to its identity hash via Identity::recall.
T:HASIDENTITY <hex> T:OK 0/1 Boolean check whether pyxis has a recallable identity for <hex>.

Send / receive

Command Args Reply Notes
T:SEND <hex_dest> <text> T:OK <hash> Send LXMF message via DIRECT (link). Reply is the message hash.
T:SENDOPP <hex_dest> <text> T:OK <hash> Same but OPPORTUNISTIC (single packet).
T:STATE <msg_hash> T:OK <state> Outbound message state: OUTBOUND / SENDING / SENT / DELIVERED / FAILED.
T:RX T:OK count=N then T:RX <from_hash> <text> per message Drain the test-side inbox of received messages (see also test_rx_count).
T:RXCLR T:OK cleared Reset the test-side inbox counters.

Propagation (offline-tolerant delivery)

Command Args Reply Notes
T:SETPROP <hex_dest> <cost> T:OK Configure outbound propagation node hash + LXMF stamp cost.
T:SENDPROP <hex_dest> <text> T:OK <hash> Send PROPAGATED via the configured propagation node.
T:SYNCPROP T:OK Kick off an inbound sync from the configured propagation node.
T:SYNCSTATE T:OK <state> Current PR_* state of the propagation-sync FSM.

Voice / LXST telephony

Command Args Reply Notes
T:CALL <hex_dest> T:OK or T:ERR Initiate outgoing LXST call.
T:CALL_ANSWER T:OK Accept an incoming ring. Only valid in state RING.
T:CALL_HANGUP T:OK Tear down the active call.
T:CALL_STATE T:OK <state> Current call FSM state name.
T:CALL_STATS T:OK … Audio frame counters for the most recent call.
T:CALL_QOS T:OK … Wire-level audio fidelity counters (decoded RMS, frame loss, etc).
T:CALL_PROFILE [hex] T:OK <hex> Get (no arg) or set (hex arg) preferred Codec2 profile. Profiles: 0x10 ULBW (700C), 0x20 VLBW (1600), 0x30 LBW (3200).
T:CALL_INJECT <on|off> [freq_hz] [amp_pct] T:OK inject=<on/off> freq=<f> amp=<a> Replace mic capture with a synthesized sine wave for the active call. Useful for end-to-end audio fidelity checks against a bot that decodes pyxis's audio frames. Defaults: 1000 Hz, amp 0.5.

BLE interface

Command Args Reply Notes
T:BLE on|off T:OK ble=<on/off> Toggle the BLE Mesh interface at runtime AND persist the setting (NVS namespace settings, key ble_en).

UI / docs

Command Args Reply Notes
T:SHOW <screen> T:OK shown <screen> Switch the active LVGL screen. Names: conversation_list (alias home), compose, announces, status, settings, propagation_nodes.
T:SCREENSHOT Multi-line; see below Capture the active screen as RGB565 and base64-dump it.

T:SCREENSHOT wire format

T:SCREENSHOT BEGIN W=320 H=240 FMT=rgb565be BYTES=153600
<base64 line, ≤76 chars>
<base64 line>
…
T:SCREENSHOT END

FMT carries the byte order — rgb565be when LV_COLOR_16_SWAP=1 is set in lib/lv_conf.h (the current default for the ST7789 panel), rgb565le otherwise. The host script (screenshot.py) reads until END, filters out any interleaved log lines (heap heartbeats, BLE stats), validates the byte count matches the header, and writes a PNG.

Throughput: ~205 KB base64 over CDC at 115200 baud → roughly 18s per shot. Bumping baud or compressing on-device (zlib via the vendored bz2 path) is the obvious win if this becomes a hot path.

Host-side helpers

  • screenshot.py — auto-detects the pyxis port, sends T:SCREENSHOT, decodes RGB565, writes a PNG. --port / --out flags. See file header for full usage.

(More host helpers welcome — keep them thin wrappers around T: so the device-side surface stays the source of truth.)

Adding a new command

  1. New else if (cmd == "T:NEWTHING") { … } block under process_test_command in src/main.cpp (gated behind PYXIS_TEST_HOOKS).
  2. Reply with exactly one terminal line — T:OK <payload> on success or T:ERR <reason> on failure. Multi-line streams should emit a T:OK count=N header so the host knows how many follow-up lines to read.
  3. Document the command in this file under the right section, and link from any host script that drives it.