diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 330da14b..99e45a44 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -1024,6 +1024,9 @@ static void lxst_breadcrumb(uint8_t step, uint32_t heap) { } void UIManager::call_initiate(const Bytes& peer_hash) { +#ifdef PYXIS_TEST_HOOKS + _test_call_initiate_result = TestCallInitiateResult::FAILED; +#endif { std::string h = peer_hash.toHex().substr(0, 16); INFO(("LXST: Initiating call to " + h + "...").c_str()); @@ -1066,6 +1069,9 @@ void UIManager::call_initiate(const Bytes& peer_hash) { // the definitive ownership check. const uint32_t generation = call_begin_generation(); if (generation == 0) { +#ifdef PYXIS_TEST_HOOKS + _test_call_initiate_result = TestCallInitiateResult::BUSY; +#endif WARNING("LXST: Another call was accepted concurrently"); return; } @@ -1119,6 +1125,10 @@ void UIManager::call_initiate(const Bytes& peer_hash) { _call_timeout_ms = millis() + 10000; } +#ifdef PYXIS_TEST_HOOKS + _test_call_initiate_result = TestCallInitiateResult::STARTED; +#endif + lxst_breadcrumb(7, ESP.getFreeHeap()); } diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.h b/lib/tdeck_ui/UI/LXMF/UIManager.h index 082ff538..ab75311a 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.h +++ b/lib/tdeck_ui/UI/LXMF/UIManager.h @@ -202,8 +202,17 @@ public: * commands defined in main.cpp under PYXIS_TEST_HOOKS. */ - /** Initiate an outgoing call to peer (calls private call_initiate). */ - void test_call_initiate(const RNS::Bytes& peer_hash) { call_initiate(peer_hash); } + enum class TestCallInitiateResult { + FAILED, + BUSY, + STARTED, + }; + + /** Initiate an outgoing call and report its exact admission outcome. */ + TestCallInitiateResult test_call_initiate(const RNS::Bytes& peer_hash) { + call_initiate(peer_hash); + return _test_call_initiate_result; + } /** Hang up the active call on loopTask (calls private call_hangup). */ void test_call_hangup() { call_hangup(); } @@ -406,6 +415,10 @@ private: CallCommandMailbox _call_commands; CallGenerationGuard _call_generation_guard; CallLinkOwnership _call_link_ownership; +#ifdef PYXIS_TEST_HOOKS + TestCallInitiateResult _test_call_initiate_result = + TestCallInitiateResult::FAILED; +#endif uint32_t _call_start_ms; // millis() when call became ACTIVE uint32_t _call_timeout_ms; // millis() deadline for current wait state bool _call_muted; diff --git a/src/main.cpp b/src/main.cpp index b5efeffb..8cd300b6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2175,8 +2175,15 @@ static void handle_test_hook_command(const String& line) { // Serial hooks run on loopTask, outside the LVGL task. The production // call path mutates screens immediately, so hold the same LVGL lock as // other cross-task UI operations. - { LVGL_LOCK(); ui_manager->test_call_initiate(dest_hash); } - Serial.println(String("T:OK calling=") + args); + UI::LXMF::UIManager::TestCallInitiateResult result; + { LVGL_LOCK(); result = ui_manager->test_call_initiate(dest_hash); } + if (result == UI::LXMF::UIManager::TestCallInitiateResult::BUSY) { + Serial.println("T:ERR busy"); + } else if (result == UI::LXMF::UIManager::TestCallInitiateResult::STARTED) { + Serial.println(String("T:OK calling=") + args); + } else { + Serial.println("T:ERR call_failed"); + } } else if (cmd == "T:CALL_STATE") { // T:CALL_STATE — print the current call FSM state name. diff --git a/tools/voice_test/README.md b/tools/voice_test/README.md index 55c3db21..8a23bc8a 100644 --- a/tools/voice_test/README.md +++ b/tools/voice_test/README.md @@ -51,8 +51,9 @@ T-Deck serial hooks to verify: second raw link receives `STATUS_BUSY` and cannot replace it. - A local `T:CALL` request cannot displace an incoming link that is still identifying. -- Closing reserved caller A and then ringing caller B isolates B from a feasible - late action and queued callback drain from A. +- Closing reserved caller A and then ringing caller B proves that closed-link + `identify()` is a no-op and that B remains stable while A's queued callbacks + drain. The harness does not inject a fabricated stale callback. - A non-identifying caller is closed after the 15-second firmware timeout, and a subsequent normal call still rings, answers, reaches `ACTIVE`, exchanges audio in both directions, and hangs up cleanly. @@ -64,6 +65,10 @@ export PYXIS_TEST_TCP_HOST=10.0.0.145 PYXIS_TEST_TCP_PORT=4242 /opt/homebrew/bin/pio run -e tdeck -t upload --upload-port /dev/cu.usbmodem101 ``` +After testing, remove/disable `PYXIS_TEST_HOOKS` and the test TCP overrides and +restore the normal release firmware on the device. Do not leave test-hook +firmware deployed as the normal user build. + Run from the Mac with its Reticulum venv (defaults to the local TCP server on `127.0.0.1:4242`): @@ -80,9 +85,17 @@ On Apple Silicon the harness re-executes itself with `/opt/homebrew/lib` on `DYLD_LIBRARY_PATH`, allowing LXST/PyOgg to load Homebrew `libopus` for incoming calls. -The contention cases use the pinned Reticulum 1.3.8 `Identity`, `Destination`, -and `Link` APIs directly. They require current Pyxis and Sideband LXST announces, -the TCP Reticulum hub, a serial-connected T-Deck running the test-hooks firmware, -and the Mac Reticulum environment shown above. Run the host-native generation -guard tests separately for the deterministic portable stale-callback model; the -raw-link stale-action case is an additional physical integration check. +The contention cases use the ordinary Reticulum `Identity`, `Destination`, and +`Link` APIs directly and print the observed `RNS.__version__` at startup. The +legacy Sideband environment was validated with RNS 1.3.8 for API compatibility; +that is an observation, not a pin or downgrade recommendation. Torlando's +security-patched deployments require RNS 1.3.9 or newer for Luthen. Use the +current security-patched version and do not force an insecure rollback merely +to run this harness. + +The physical run requires current Pyxis and Sideband LXST announces, the TCP +Reticulum hub, a serial-connected T-Deck running the test-hooks firmware, and +the Mac Reticulum environment shown above. Run the host-native generation guard +tests separately for the deterministic portable stale-callback model. The +physical closed-link case proves close/B-redial stability plus callback drain; +it does not inject a stale callback. diff --git a/tools/voice_test/sideband_e2e.py b/tools/voice_test/sideband_e2e.py index 3326cd1d..a2972875 100644 --- a/tools/voice_test/sideband_e2e.py +++ b/tools/voice_test/sideband_e2e.py @@ -127,13 +127,17 @@ Sinks.Backend = FakeSinkBackend class Dev: def __init__(self): self.s = serial.Serial(PORT, 115200, timeout=0.12) - self.crash_trace = False - time.sleep(0.4) - self.s.reset_input_buffer() + try: + self.crash_trace = False + time.sleep(0.4) + self.s.reset_input_buffer() + except BaseException: + self.s.close() + raise def cmd(self, line, timeout=4): self.s.write((line+"\n").encode()); self.s.flush() - deadline = time.time()+timeout; lines=[] - while time.time() Sideband",flush=True) print("TEST1 call",dev.cmd("T:CALL "+side_dest)[0],flush=True) ok,seen=dev.wait_state("ACTIVE",35); print("TEST1 states_to_active",seen,flush=True); assert ok - deadline=time.time()+15 - while not phone.is_in_call and time.time()