mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +00:00
fix RX leak after TX→RX: drop IQ during TX in non-DUP, Windows post-TX hold
В 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 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,7 @@ type
|
|||||||
procedure SendFullHP;
|
procedure SendFullHP;
|
||||||
procedure SendDDCAudio(const LeftRight: array of SmallInt);
|
procedure SendDDCAudio(const LeftRight: array of SmallInt);
|
||||||
procedure SendDUCIQ(const IData, QData: array of Integer);
|
procedure SendDUCIQ(const IData, QData: array of Integer);
|
||||||
|
procedure ClearDUCIQQueue;
|
||||||
|
|
||||||
property Connected: Boolean read FConnected;
|
property Connected: Boolean read FConnected;
|
||||||
property Running: Boolean read FRunning;
|
property Running: Boolean read FRunning;
|
||||||
@@ -493,6 +494,8 @@ begin
|
|||||||
|
|
||||||
WaitUntil(NextDueUs);
|
WaitUntil(NextDueUs);
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
|
if not FNet.FIsTransmitting then
|
||||||
|
Continue;
|
||||||
|
|
||||||
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
|
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
|
||||||
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
|
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
|
||||||
@@ -519,6 +522,8 @@ begin
|
|||||||
|
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
DrainVirtualFIFO;
|
DrainVirtualFIFO;
|
||||||
|
if not FNet.FIsTransmitting then
|
||||||
|
Continue;
|
||||||
|
|
||||||
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
|
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
|
||||||
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
|
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
|
||||||
@@ -646,6 +651,11 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure THPSDRNetwork.ClearDUCIQQueue;
|
||||||
|
begin
|
||||||
|
ResetDUCIQQueue;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure THPSDRNetwork.EnqueueDUCIQ(const Pkt: TDUCIQPacket);
|
procedure THPSDRNetwork.EnqueueDUCIQ(const Pkt: TDUCIQPacket);
|
||||||
begin
|
begin
|
||||||
FDUCIQLock.Enter;
|
FDUCIQLock.Enter;
|
||||||
|
|||||||
+22
-1
@@ -4468,24 +4468,45 @@ begin
|
|||||||
FSpecView.TXOverlay := FTransmitting;
|
FSpecView.TXOverlay := FTransmitting;
|
||||||
// Перерисовываем сетку спектра под активный тракт (Thetis-style).
|
// Перерисовываем сетку спектра под активный тракт (Thetis-style).
|
||||||
ApplySpecViewGridFromState;
|
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
|
if FWDSPReady then
|
||||||
begin
|
begin
|
||||||
|
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX,
|
||||||
|
// чтобы TX leakage не накапливался в RXA pipeline и FFT-истории
|
||||||
|
// RX-анализатора. В DUP RX-тракт работает как обычно.
|
||||||
|
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
||||||
FDSPEngine.SetTXRun(FTransmitting);
|
FDSPEngine.SetTXRun(FTransmitting);
|
||||||
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
||||||
begin
|
begin
|
||||||
FDSPEngine.FlushRX;
|
FDSPEngine.FlushRX;
|
||||||
if Assigned(FAudioOut) then
|
if Assigned(FAudioOut) then
|
||||||
FAudioOut.Clear;
|
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;
|
end;
|
||||||
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
|
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
|
||||||
// источник на RX, чтобы видеть приём во время передачи.
|
// источник на RX, чтобы видеть приём во время передачи.
|
||||||
if FTransmitting and FDisplayDuplex then
|
if FTransmitting and FDisplayDuplex then
|
||||||
FDSPEngine.SetDisplaySourceTX(False);
|
FDSPEngine.SetDisplaySourceTX(False);
|
||||||
end;
|
end;
|
||||||
if FNetwork.Connected and FNetwork.Running then
|
if (not FTransmitting) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
|
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
|
||||||
FTransmitting, True, True);
|
FTransmitting, True, True);
|
||||||
|
FNetwork.ClearDUCIQQueue;
|
||||||
FNetwork.SendFullHP;
|
FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+52
-2
@@ -216,6 +216,18 @@ type
|
|||||||
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
|
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
|
||||||
FTXThread: TThread;
|
FTXThread: TThread;
|
||||||
FTXActive: Boolean;
|
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
|
FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID
|
||||||
FTXMicSource: TTXMicSource;
|
FTXMicSource: TTXMicSource;
|
||||||
FOnPullMic: TPullMicSamplesFunc;
|
FOnPullMic: TPullMicSamplesFunc;
|
||||||
@@ -325,6 +337,12 @@ type
|
|||||||
DataOffset: Integer;
|
DataOffset: Integer;
|
||||||
IQPairs: Integer);
|
IQPairs: Integer);
|
||||||
procedure FlushRX;
|
procedure FlushRX;
|
||||||
|
// Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это
|
||||||
|
// на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio
|
||||||
|
// буфер удерживает «хвост» с TX leakage из радио, который AudioOut.Clear
|
||||||
|
// не достаёт. На Linux буфер маленький и проблемы нет, поэтому метод
|
||||||
|
// не вызывается.
|
||||||
|
procedure BeginPostTXMute(DurationMs: Integer);
|
||||||
|
|
||||||
// RX управление
|
// RX управление
|
||||||
procedure SetMode(Mode: Integer);
|
procedure SetMode(Mode: Integer);
|
||||||
@@ -432,6 +450,10 @@ type
|
|||||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||||||
property TXActive: Boolean read FTXActive;
|
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).
|
// Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive).
|
||||||
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
|
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
|
||||||
property ActiveDisplayID: Integer read FActiveDisplayID;
|
property ActiveDisplayID: Integer read FActiveDisplayID;
|
||||||
@@ -741,6 +763,8 @@ begin
|
|||||||
FTXMicHead := 0;
|
FTXMicHead := 0;
|
||||||
FTXMicTail := 0;
|
FTXMicTail := 0;
|
||||||
FTXActive := False;
|
FTXActive := False;
|
||||||
|
FKeepRXDuringTX := False;
|
||||||
|
FPostTXMuteUntil := 0;
|
||||||
FActiveDisplayID := RX_DISP_ID;
|
FActiveDisplayID := RX_DISP_ID;
|
||||||
FTXMicSource := txmsRadio;
|
FTXMicSource := txmsRadio;
|
||||||
FOnPullMic := nil;
|
FOnPullMic := nil;
|
||||||
@@ -1124,14 +1148,34 @@ begin
|
|||||||
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
|
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
|
||||||
end;
|
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);
|
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
|
var
|
||||||
i, Pos: Integer;
|
i, Pos: Integer;
|
||||||
IR, QR: LongInt;
|
IR, QR: LongInt;
|
||||||
const
|
const
|
||||||
SCALE = 1.0 / 8388608.0;
|
SCALE = 1.0 / 8388608.0;
|
||||||
begin
|
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
|
for i := 0 to Item.IQPairs - 1 do
|
||||||
begin
|
begin
|
||||||
Pos := i * 6;
|
Pos := i * 6;
|
||||||
@@ -1217,7 +1261,13 @@ begin
|
|||||||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||||||
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
||||||
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
// иначе оператор слышит свой эфир через 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
|
begin
|
||||||
for i := 0 to FAudioBufSize - 1 do
|
for i := 0 to FAudioBufSize - 1 do
|
||||||
begin
|
begin
|
||||||
|
|||||||
Reference in New Issue
Block a user