From 6dbdd4854ba8c00c40b8feb9970f2ae223aaa9db Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Sun, 22 Feb 2026 02:11:13 -0500 Subject: [PATCH] LXST voice TX: link routing fix, frame batching, TX diagnostics - Fix link packet routing in microReticulum to only transmit on the link's attached interface (was flooding all interfaces including LoRa) - Batch up to 8 Codec2 frames per Reticulum packet to reduce per-packet encryption/transport overhead (~3 packets/sec instead of 25) - Add TX diagnostics: hex dump of first 2 packets, per-interval stats with link status, idle/drop warnings - Audio now reaches remote end (TX=325 frames in 12s test call) Co-Authored-By: Claude Opus 4.6 --- deps/microReticulum | 2 +- lib/tdeck_ui/UI/LXMF/UIManager.cpp | 109 +++++++++++++++++++++++------ lib/tdeck_ui/UI/LXMF/UIManager.h | 4 +- 3 files changed, 89 insertions(+), 26 deletions(-) diff --git a/deps/microReticulum b/deps/microReticulum index b6249788..17244c12 160000 --- a/deps/microReticulum +++ b/deps/microReticulum @@ -1 +1 @@ -Subproject commit b624978850eb862338e6e28741c9db0b0218bd7b +Subproject commit 17244c12c6c6f4a87f2374f972c99c86502737bc diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 4718d548..db2b831c 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -888,22 +888,44 @@ void UIManager::call_send_signal(int signal) { DEBUG(buf); } -void UIManager::call_send_audio(const uint8_t* data, int length) { - if (!_call_link || _call_link.status() != Type::Link::ACTIVE) return; +void UIManager::call_send_audio_batch(const uint8_t* batch_data, int batch_len, int frame_count) { + if (!_call_link || _call_link.status() != Type::Link::ACTIVE) { + if (_call_audio_tx_count == 0) { + char dbg[64]; + snprintf(dbg, sizeof(dbg), "LXST: TX drop: link=%p status=%d", + (void*)&_call_link, _call_link ? (int)_call_link.status() : -99); + WARNING(dbg); + } + return; + } - // Msgpack: {0x01: bin8(codec_header + frame_data)} + // Msgpack: {0x01: bin8(codec_header + mode_header + raw_frames...)} + // batch_data = [mode_header] + [raw_frame1] + [raw_frame2] + ... (headers stripped) uint8_t packet_buf[256]; - int total_len = 1 + length; // codec header + frame data + int total_len = 1 + batch_len; // codec_header + batch_data if (total_len > 250 || total_len < 1) return; packet_buf[0] = 0x81; // fixmap(1) packet_buf[1] = 0x01; // key: FIELD_FRAMES packet_buf[2] = 0xC4; // bin8 packet_buf[3] = (uint8_t)total_len; // length - packet_buf[4] = LXST_CODEC_CODEC2; // codec header - memcpy(packet_buf + 5, data, length); + packet_buf[4] = LXST_CODEC_CODEC2; // codec header (0x02) + memcpy(packet_buf + 5, batch_data, batch_len); - Bytes audio_data(packet_buf, 5 + length); + // Hex dump first TX packet for wire format verification + if (_call_audio_tx_count < 2) { + int pkt_len = 5 + batch_len; + char hex[128]; + int pos = 0; + for (int i = 0; i < pkt_len && i < 20 && pos < 120; i++) { + pos += snprintf(hex + pos, 128 - pos, "%02X ", packet_buf[i]); + } + char dbg[196]; + snprintf(dbg, sizeof(dbg), "LXST: TX wire[%d] %d frames: %s", pkt_len, frame_count, hex); + INFO(dbg); + } + + Bytes audio_data(packet_buf, 5 + batch_len); Packet packet(_call_link, audio_data); packet.send(); } @@ -1301,42 +1323,83 @@ void UIManager::call_update() { uint32_t duration_secs = (now - _call_start_ms) / 1000; _call_screen->set_duration(duration_secs); - // Periodic audio stats (every 5 seconds) - if (duration_secs > 0 && duration_secs % 5 == 0 && + // Periodic audio stats (every 2 seconds) + if (duration_secs > 0 && duration_secs % 2 == 0 && now - _call_start_ms > duration_secs * 1000 - 500) { static uint32_t last_stats_sec = 0; if (duration_secs != last_stats_sec) { last_stats_sec = duration_secs; - char dbg[96]; - snprintf(dbg, sizeof(dbg), "LXST: Audio stats: TX=%lu RX=%lu playBuf=%d capAvail=%d state=%d", + char dbg[128]; + snprintf(dbg, sizeof(dbg), "LXST: Audio stats: TX=%lu RX=%lu playBuf=%d capAvail=%d state=%d link=%d", (unsigned long)_call_audio_tx_count, (unsigned long)_call_audio_rx_count, _lxst_audio ? _lxst_audio->playbackFramesBuffered() : -1, _lxst_audio ? _lxst_audio->capturePacketsAvailable() : -1, - _lxst_audio ? (int)_lxst_audio->state() : -1); + _lxst_audio ? (int)_lxst_audio->state() : -1, + _call_link ? (int)_call_link.status() : -99); INFO(dbg); } } } - // Pump TX: read encoded packets from capture and send over link + // Pump TX: batch multiple codec frames into one packet to reduce overhead. + // Each encoded frame from ring buffer = [mode_header(1)] + [raw_codec2(8)] = 9 bytes. + // We batch up to 8 frames per packet: [mode_header] + [8 * raw_codec2] = 65 bytes. + // This matches Columba's batching (~320ms of audio per packet). + static constexpr int TX_BATCH_SIZE = 8; // frames per packet if (_lxst_audio && _lxst_audio->isCapturing()) { - uint8_t encoded_buf[64]; - int encoded_len = 0; - for (int i = 0; i < 8; i++) { - if (_lxst_audio->readEncodedPacket(encoded_buf, sizeof(encoded_buf), &encoded_len)) { - call_send_audio(encoded_buf, encoded_len); + // Send up to 3 batched packets per call_update() cycle + for (int batch = 0; batch < 3; batch++) { + uint8_t batch_buf[128]; // [mode_header] + [N * raw_codec2_data] + int batch_len = 0; + int frame_count = 0; + uint8_t encoded_buf[64]; + int encoded_len = 0; + + for (int i = 0; i < TX_BATCH_SIZE; i++) { + if (!_lxst_audio->readEncodedPacket(encoded_buf, sizeof(encoded_buf), &encoded_len)) { + break; + } + if (encoded_len < 2) continue; // need at least mode_header + 1 byte + + if (frame_count == 0) { + // First frame: keep mode header + data + memcpy(batch_buf, encoded_buf, encoded_len); + batch_len = encoded_len; + } else { + // Subsequent frames: strip mode header, append raw data only + int raw_len = encoded_len - 1; + if (batch_len + raw_len > (int)sizeof(batch_buf)) break; + memcpy(batch_buf + batch_len, encoded_buf + 1, raw_len); + batch_len += raw_len; + } + frame_count++; _call_audio_tx_count++; - if (_call_audio_tx_count <= 3) { - char dbg[64]; - snprintf(dbg, sizeof(dbg), "LXST: TX audio #%lu len=%d", - (unsigned long)_call_audio_tx_count, encoded_len); + } + + if (frame_count > 0) { + call_send_audio_batch(batch_buf, batch_len, frame_count); + if (_call_audio_tx_count <= 16) { + char dbg[80]; + snprintf(dbg, sizeof(dbg), "LXST: TX batch %d frames, %d bytes, total=%lu", + frame_count, batch_len, (unsigned long)_call_audio_tx_count); INFO(dbg); } } else { - break; + break; // no more data } } + } else if (_call_audio_tx_count == 0) { + // Log once why TX is not running + static uint32_t last_tx_warn = 0; + if (now - last_tx_warn > 2000) { + last_tx_warn = now; + char dbg[96]; + snprintf(dbg, sizeof(dbg), "LXST: TX pump idle: audio=%p capturing=%d state=%d", + _lxst_audio, _lxst_audio ? (int)_lxst_audio->isCapturing() : -1, + _lxst_audio ? (int)_lxst_audio->state() : -1); + WARNING(dbg); + } } } } diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.h b/lib/tdeck_ui/UI/LXMF/UIManager.h index eaeac738..80defe41 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.h +++ b/lib/tdeck_ui/UI/LXMF/UIManager.h @@ -294,8 +294,8 @@ private: // Send a signalling byte over the call link void call_send_signal(int signal); - // Send encoded audio packet over the call link - void call_send_audio(const uint8_t* data, int length); + // Send batched encoded audio frames over the call link + void call_send_audio_batch(const uint8_t* batch_data, int batch_len, int frame_count); // Process a single received audio frame (codec_header + data) void call_rx_audio_frame(const uint8_t* frame, size_t frame_len);