feat(lxst): Codec2-700C (ULBW) default profile + T:CALL_ANSWER hook

Adds two profile constants beyond the existing LXST_PROFILE_LBW (0x30,
Codec2-3200): LXST_PROFILE_VLBW (0x20, Codec2-1600) and
LXST_PROFILE_ULBW (0x10, Codec2-700C). Default is now ULBW — a 700C
frame fits comfortably inside an SF7-9 LoRa packet, which is the
target medium for pyxis voice. The previous 3200bps default was 4.5x
larger and unsuitable for the radio path.

Profile is selectable at runtime via T:CALL_PROFILE [hex]. Replaces
five hardcoded LXST_PROFILE_LBW sites: three audio-init paths in
call_process_signal and two profile-negotiation send_signal calls.

Adds T:CALL_ANSWER for harness pyxis-as-callee testing — sets the
same _call_answer_pending flag the UI button does so call_answer()
runs on the main loop in its proper context. Validates against
real LXST.Telephony.Telephone callers.

Validated: pyxis dialed real LXST upstream Telephone bot, negotiated
ULBW end-to-end, reached STATUS_ESTABLISHED, decoded frames cleanly
both directions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
torlando-tech
2026-05-08 19:07:41 -04:00
co-authored by Claude Opus 4.7
parent 6dbbe1bee7
commit 612f44e274
3 changed files with 112 additions and 15 deletions
+32
View File
@@ -1830,6 +1830,17 @@ static void handle_test_hook_command(const String& line) {
ui_manager->test_call_hangup();
Serial.println("T:OK hung_up");
}
else if (cmd == "T:CALL_ANSWER") {
// T:CALL_ANSWER — accept an incoming ring. Only valid when state
// is INCOMING_RINGING. Used by the harness for pyxis-as-callee
// interop tests against real LXST.Telephony.Telephone clients.
if (!ui_manager) { Serial.println("T:ERR no ui_manager"); return; }
if (!ui_manager->test_call_answer()) {
Serial.println("T:ERR not_ringing");
return;
}
Serial.println("T:OK answered");
}
else if (cmd == "T:CALL_STATS") {
// T:CALL_STATS — return audio frame counters for the most recent
// call. tx = frames sent over the wire (encoded by capture path),
@@ -1863,6 +1874,27 @@ static void handle_test_hook_command(const String& line) {
Serial.print(" state=");
Serial.println(ui_manager->test_call_state_name());
}
else if (cmd == "T:CALL_PROFILE") {
// T:CALL_PROFILE [hex] — get/set pyxis's preferred Codec2 profile.
// No arg: print current. With arg: set.
// Valid: 0x10 (ULBW/700C), 0x20 (VLBW/1600), 0x30 (LBW/3200).
if (!ui_manager) { Serial.println("T:ERR no ui_manager"); return; }
if (args.length() == 0) {
int p = ui_manager->test_call_get_profile();
Serial.print("T:OK profile=0x");
if (p < 16) Serial.print("0");
Serial.println(String(p, HEX));
return;
}
int profile = (int)strtol(args.c_str(), nullptr, 0);
if (!ui_manager->test_call_set_profile(profile)) {
Serial.println("T:ERR unknown profile");
return;
}
Serial.print("T:OK profile=0x");
if (profile < 16) Serial.print("0");
Serial.println(String(profile, HEX));
}
else if (cmd == "T:CALL_INJECT") {
// T:CALL_INJECT <on|off> [freq_hz] [amp_pct]
// Replace mic capture with a synthesized sine wave for the