From 9f208006aa77aa4a5a41c1041a5b574ef20c6673 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Sat, 9 May 2026 13:39:45 +0300 Subject: [PATCH] =?UTF-8?q?fix=20RX=20leak=20after=20TX=E2=86=92RX:=20drop?= =?UTF-8?q?=20IQ=20during=20TX=20in=20non-DUP,=20Windows=20post-TX=20hold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В non-DUP при MOX/TUN на возврате слышен и виден на водопаде хвост своего сигнала. Во время TX RX-IQ оседал в RXA pipeline / FFT-истории RX-анализатора и «выстреливал» при открытии гейта. - WDSPEngine: KeepRXDuringTX (DUP-флаг). В non-DUP TX PushIQItemToDSP дропает пакеты на входе DSP-потока — RXA и анализатор не кормятся. - Windows-only post-TX hold (250 мс): дропаем И входящие RX-IQ, И аудио в окне после MOX-off, чтобы скрыть «хвост» от FPGA TX-buffer/PA slew-down радио. На Linux код-путь не активируется (буфер OS мелкий). - Network: ClearDUCIQQueue + IsTransmitting guard в DUC sender thread, чтобы недопереданные TX-IQ пакеты не уходили после MOX-off. Co-Authored-By: Claude Opus 4.7 --- HPSDRNetwork.pas | 10 +++++++++ MainForm.pas | 23 ++++++++++++++++++++- WDSPEngine.pas | 54 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index efd5b2b..1ac61e2 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -193,6 +193,7 @@ type procedure SendFullHP; procedure SendDDCAudio(const LeftRight: array of SmallInt); procedure SendDUCIQ(const IData, QData: array of Integer); + procedure ClearDUCIQQueue; property Connected: Boolean read FConnected; property Running: Boolean read FRunning; @@ -493,6 +494,8 @@ begin WaitUntil(NextDueUs); if Terminated then Break; + if not FNet.FIsTransmitting then + Continue; FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, @@ -519,6 +522,8 @@ begin if Terminated then Break; DrainVirtualFIFO; + if not FNet.FIsTransmitting then + Continue; FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, @@ -646,6 +651,11 @@ begin end; end; +procedure THPSDRNetwork.ClearDUCIQQueue; +begin + ResetDUCIQQueue; +end; + procedure THPSDRNetwork.EnqueueDUCIQ(const Pkt: TDUCIQPacket); begin FDUCIQLock.Enter; diff --git a/MainForm.pas b/MainForm.pas index b45cab4..ce72ce0 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -4468,24 +4468,45 @@ begin FSpecView.TXOverlay := FTransmitting; // Перерисовываем сетку спектра под активный тракт (Thetis-style). ApplySpecViewGridFromState; + if FTransmitting and FNetwork.Connected and FNetwork.Running then + begin + FNetwork.ClearDUCIQQueue; + FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + True, True, True); + FNetwork.SendFullHP; + end; if FWDSPReady then begin + // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, + // чтобы TX leakage не накапливался в RXA pipeline и FFT-истории + // RX-анализатора. В DUP RX-тракт работает как обычно. + FDSPEngine.KeepRXDuringTX := FDisplayDuplex; FDSPEngine.SetTXRun(FTransmitting); if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then begin FDSPEngine.FlushRX; if Assigned(FAudioOut) then FAudioOut.Clear; + {$IFDEF WINDOWS} + // Windows: на TX→RX дропаем И RX-IQ пакеты, И аудио-выход на это окно. + // Источник «хвоста» — radio FPGA TX-buffer + PA slew-down: после MOX-off + // радио дотравливает буфер передатчика, RX1 ловит это как leak, и WDSP + // отдаёт это и в водопад, и в звук. На Linux наблюдается окно <30мс, + // на Windows может тянуться 200–500мс. Подобрать значение можно здесь + // без перекомпиляции engine. На Linux код-путь не активируется. + FDSPEngine.BeginPostTXMute(250); + {$ENDIF} end; // SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем // источник на RX, чтобы видеть приём во время передачи. if FTransmitting and FDisplayDuplex then FDSPEngine.SetDisplaySourceTX(False); end; - if FNetwork.Connected and FNetwork.Running then + if (not FTransmitting) and FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.ClearDUCIQQueue; FNetwork.SendFullHP; end; end; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 63648cb..4ddc564 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -216,6 +216,18 @@ type FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы FTXThread: TThread; FTXActive: Boolean; + // DUP-флаг от UI. True — RX-анализатор и RXA-канал продолжают работать + // во время TX (full-duplex). False — RX-IQ пакеты дропаются на входе DSP + // потока и не кормят RXA/анализатор. Это не трогает SetChannelState + // (мы выяснили на практике что стоп/старт RXA добавляет задержки на + // Windows без видимого выигрыша). + FKeepRXDuringTX: Boolean; + // Дедлайн (GetTickCount64), до которого RX-аудио не пишется в FOnAudio. + // Используется на Windows для маскировки «хвоста» TX leakage (radio + // FPGA TX-buffer + PA slew-down → RX1 IQ → audio) после MOX-off. + // На Linux выставляется в 0 (UI не зовёт BeginPostTXMute) — поведение + // не меняется. + FPostTXMuteUntil: QWord; FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID FTXMicSource: TTXMicSource; FOnPullMic: TPullMicSamplesFunc; @@ -325,6 +337,12 @@ type DataOffset: Integer; IQPairs: Integer); procedure FlushRX; + // Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это + // на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio + // буфер удерживает «хвост» с TX leakage из радио, который AudioOut.Clear + // не достаёт. На Linux буфер маленький и проблемы нет, поэтому метод + // не вызывается. + procedure BeginPostTXMute(DurationMs: Integer); // RX управление procedure SetMode(Mode: Integer); @@ -432,6 +450,10 @@ type property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ; property TXActive: Boolean read FTXActive; + // Включает full-duplex поведение RX-тракта: RXA не останавливается на TX, + // RX-анализатор продолжает получать IQ. Должен ставиться UI ДО SetTXRun. + property KeepRXDuringTX: Boolean read FKeepRXDuringTX + write FKeepRXDuringTX; // Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive). // SpectrumView/Waterfall должны читать пиксели именно из этого ID. property ActiveDisplayID: Integer read FActiveDisplayID; @@ -741,6 +763,8 @@ begin FTXMicHead := 0; FTXMicTail := 0; FTXActive := False; + FKeepRXDuringTX := False; + FPostTXMuteUntil := 0; FActiveDisplayID := RX_DISP_ID; FTXMicSource := txmsRadio; FOnPullMic := nil; @@ -1124,14 +1148,34 @@ begin if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0); end; +procedure TWDSPEngine.BeginPostTXMute(DurationMs: Integer); +begin + if DurationMs <= 0 then + FPostTXMuteUntil := 0 + else + FPostTXMuteUntil := GetTickCount64 + QWord(DurationMs); +end; + procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem); -// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize +// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize. +// В non-DUP TX (FTXActive and not FKeepRXDuringTX) полностью пропускаем +// IQ-пакет: не кормим RX-анализатор и не накапливаем для fexchange0. +// Иначе TX leakage из DDC оседает в RXA pipeline / RX_DISP_ID FFT-истории +// и звучит/виден на водопаде первые секунды после возврата на RX. var i, Pos: Integer; IR, QR: LongInt; const SCALE = 1.0 / 8388608.0; begin + if FTXActive and not FKeepRXDuringTX then Exit; + // Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта. + // Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не + // кормится), и аудио (RXA не запускается, FOnAudio не вызывается). + // Радио продолжает дотравливать FPGA TX-buffer + PA slew-down первые + // ~50–200мс после MOX-off; этот хвост попадает в RX1 как leak. Дропаем + // его на входе — single-source-of-truth. + if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit; for i := 0 to Item.IQPairs - 1 do begin Pos := i * 6; @@ -1217,7 +1261,13 @@ begin // Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1). // При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX), // иначе оператор слышит свой эфир через RX-цепь и получает feedback. - if Assigned(FOnAudio) and not FMuted and not FTXActive then + // Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage + // на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка + // через unsigned-сравнение, GetTickCount64 не зовётся когда дедлайн = 0. + if (FPostTXMuteUntil <> 0) and (GetTickCount64 >= FPostTXMuteUntil) then + FPostTXMuteUntil := 0; + if Assigned(FOnAudio) and not FMuted and not FTXActive + and (FPostTXMuteUntil = 0) then begin for i := 0 to FAudioBufSize - 1 do begin