diff --git a/MainForm.pas b/MainForm.pas index 69db365..be79697 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -182,6 +182,9 @@ type FRunning: Boolean; FTransmitting: Boolean; FSplitTxB: Boolean; + // DUP: при TX продолжаем показывать RX1-водопад/панораму, а TX-фильтр + // и TX-линии рисуем поверх. Аппаратной командой не является. + FDisplayDuplex: Boolean; FHWPTTActive: Boolean; // True = hardware PTT нажата FMuted: Boolean; FVolume: Integer; @@ -304,6 +307,7 @@ type PanelFilter: TPanel; BtnFilter: array[0..FILT_COUNT-1] of TFlatButton; BtnCTun: TFlatButton; + BtnDUP: TFlatButton; PanelRX: TPanel; BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF @@ -413,6 +417,8 @@ type procedure BtnModeClick(Sender: TObject); procedure BtnFilterClick(Sender: TObject); procedure BtnCTunClick(Sender: TObject); + procedure BtnDUPClick(Sender: TObject); + procedure ApplyDUP(Active: Boolean); procedure UpdateFilterButtons; procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); @@ -863,6 +869,7 @@ begin Result.AudioSampleRate := FAudioOut.SampleRate; Result.ShowSpectrum := FShowSpectrum; Result.ShowWaterfall := FShowWaterfall; + Result.DisplayDuplex := FDisplayDuplex; Result.DisplayFPS := FDisplayFPS; Result.LightTheme := FLightTheme; // Audio device names @@ -987,6 +994,7 @@ begin FRunning := False; FTransmitting := False; FSplitTxB := False; + FDisplayDuplex := False; FMuted := False; FVolume := 70; FCenterFreq := FVfoA; @@ -1541,11 +1549,16 @@ begin Inc(Y, 76); - // CTUN button + // CTUN / DUP buttons (display-related) BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick); BtnCTun.Tag := 0; StyleButton(BtnCTun, FCTun); + BtnDUP := MakeBtn(PanelLeft, 'DUP', + 2 + (LEFT_W div 3) + 0, Y, LEFT_W div 3 - 2, BTN_H, BtnDUPClick); + BtnDUP.Tag := 0; + StyleButton(BtnDUP, FDisplayDuplex); + Inc(Y, BTN_H + 4); // RX controls @@ -2215,6 +2228,7 @@ begin for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active); for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active); StyleButton(BtnCTun, BtnCTun.Active); + StyleButton(BtnDUP, BtnDUP.Active); StyleButton(BtnNR, BtnNR.Active); StyleButton(BtnNB, BtnNB.Active); StyleButton(BtnSNB, BtnSNB.Active); @@ -2910,6 +2924,12 @@ begin DUCPkt.DUC0RateLo := Lo(RateKsps); DUCPkt.DUC0Bits := 24; DUCPkt.MicLineSelect := BuildMicLineSelectByte; + // ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection). + // Дублируем значение во все три ADC-байта: на однопотоковых платах активен только + // ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2. + DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31)); + DUCPkt.StepAtten1 := DUCPkt.StepAtten0; + DUCPkt.StepAtten2 := DUCPkt.StepAtten0; FNetwork.SendDUCSpecific(DUCPkt); end; @@ -3352,6 +3372,8 @@ begin FSpecView.ResetWfAvgBuf; FShowSpectrum := G_Settings.ShowSpectrum; FShowWaterfall := G_Settings.ShowWaterfall; + FDisplayDuplex := G_Settings.DisplayDuplex; + if BtnDUP <> nil then StyleButton(BtnDUP, FDisplayDuplex); if G_Settings.DisplayFPS > 0 then ApplyFPS(G_Settings.DisplayFPS); // Настройки дисплея и сетки @@ -3866,6 +3888,35 @@ begin PbWaterfall.Invalidate; end; +// --------------------------------------------------------------------------- +// DUP toggle (Thetis-style local display mode — без аппаратной команды) +// --------------------------------------------------------------------------- +procedure TMainForm.ApplyDUP(Active: Boolean); +begin + FDisplayDuplex := Active; + StyleButton(BtnDUP, FDisplayDuplex); + // Если сейчас идёт передача — переключаем источник пикселей и сбрасываем + // буферы, чтобы старые TX/RX bin'ы не оставались на следующем кадре. + if FTransmitting then + begin + if FWDSPReady then + FDSPEngine.SetDisplaySourceTX(not FDisplayDuplex); + FSpecView.TXMode := FTransmitting and not FDisplayDuplex; + FSpecView.ResetSpectrumBuf; + FSpecView.ResetWfAvgBuf; + ApplySpecViewGridFromState; + FSpecView.DrawSpectrum; + FSpecView.DrawWaterfall; + PbSpectrum.Invalidate; + PbWaterfall.Invalidate; + end; +end; + +procedure TMainForm.BtnDUPClick(Sender: TObject); +begin + ApplyDUP(not FDisplayDuplex); +end; + // --------------------------------------------------------------------------- // Клик/драг по спектру/водопаду // --------------------------------------------------------------------------- @@ -4261,11 +4312,20 @@ begin FSpecView.TXFreq := ActiveTXFreqHz; if FSplitTxB then FSpecView.TXVfoIndex := 1 else FSpecView.TXVfoIndex := FActiveVfo; - FSpecView.TXMode := FTransmitting; + // DUP: данные остаются от RX-анализатора (TXMode=False), но TX-полоса + // фильтра рисуется поверх через TXOverlay. Без DUP при TX оба флага True. + FSpecView.TXMode := FTransmitting and not FDisplayDuplex; + FSpecView.TXOverlay := FTransmitting; // Перерисовываем сетку спектра под активный тракт (Thetis-style). ApplySpecViewGridFromState; if FWDSPReady then + begin FDSPEngine.SetTXRun(FTransmitting); + // SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем + // источник на RX, чтобы видеть приём во время передачи. + if FTransmitting and FDisplayDuplex then + FDSPEngine.SetDisplaySourceTX(False); + end; if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, @@ -5029,9 +5089,11 @@ procedure TMainForm.OnTXSettingsChange(const T: TTXSettings); // перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific. var OldMicByte, NewMicByte: Byte; + OldAtt: Integer; GridChanged: Boolean; begin OldMicByte := BuildMicLineSelectByte; + OldAtt := FTXSettings.AttOnTX; GridChanged := (FTXSpecRefLevel <> T.TXSpecRefLevel) or (FTXSpecRange <> T.TXSpecRange) or (FTXSpecGridStep <> T.TXSpecGridStep); @@ -5045,7 +5107,7 @@ begin if GridChanged and FTransmitting then ApplySpecViewGridFromState; ApplyTXSettingsToDSP; - if OldMicByte <> NewMicByte then + if (OldMicByte <> NewMicByte) or (OldAtt <> FTXSettings.AttOnTX) then SendDUCSpecificFromSettings; // Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json if FDevConnected then @@ -5226,7 +5288,8 @@ end; procedure TMainForm.ApplySpecViewGridFromState; begin - if FTransmitting then + // При DUP во время TX данные приходят от RX-анализатора → используем RX-сетку. + if FTransmitting and not FDisplayDuplex then begin FSpecView.SpecRefLevel := FTXSpecRefLevel; FSpecView.SpecRange := FTXSpecRange; diff --git a/Settings.pas b/Settings.pas index ee01b36..96508f6 100644 --- a/Settings.pas +++ b/Settings.pas @@ -44,6 +44,9 @@ type MicPTTEnabled: Boolean; // bit2: Mic PTT enable // Mic gain (dB, применяется в WDSP PanelGain1) MicGainDB: Double; // -10..+30 + // ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX). + // DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB. + AttOnTX: Integer; // 0..31 dB // TX bandpass filter FilterLow: Integer; // Hz, для SSB обычно 100 FilterHigh: Integer; // Hz, для SSB обычно 2800 @@ -135,6 +138,7 @@ type // --- Visibility settings --- ShowSpectrum: Boolean; // True = draw spectrum on main form ShowWaterfall: Boolean; // True = draw waterfall on main form + DisplayDuplex: Boolean; // DUP: при TX показывать RX-водопад, TX-фильтр overlay'ем // --- Display FPS --- DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100) // --- Theme --- @@ -280,6 +284,7 @@ begin G.AudioInDevice := ''; G.ShowSpectrum := True; G.ShowWaterfall := True; + G.DisplayDuplex := False; G.DisplayFPS := 60; // CAT defaults for i := 0 to 3 do begin @@ -488,6 +493,7 @@ begin // Visibility settings G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True); + G.DisplayDuplex := JB(GObj,'display_duplex',False); G.DisplayFPS := JI(GObj,'display_fps',60); G.LightTheme := JB(GObj,'light_theme',False); // PA settings @@ -565,6 +571,7 @@ begin // Visibility settings JW(O,'show_spectrum',G.ShowSpectrum); JW(O,'show_waterfall',G.ShowWaterfall); + JW(O,'display_duplex',G.DisplayDuplex); JW(O,'display_fps',G.DisplayFPS); JW(O,'light_theme',G.LightTheme); // PA settings @@ -721,6 +728,8 @@ begin T.MicLineIn := False; T.MicPTTEnabled := False; T.MicGainDB := 10.0; + // ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи) + T.AttOnTX := 31; // SSB-фильтр по умолчанию T.FilterLow := 100; T.FilterHigh := 2800; @@ -794,6 +803,7 @@ begin T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn); T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled); T.MicGainDB := JD(O,'mic_gain_db', T.MicGainDB); + T.AttOnTX := EnsureRange(JI(O,'att_on_tx', T.AttOnTX), 0, 31); T.FilterLow := JI(O,'filter_low', T.FilterLow); T.FilterHigh := JI(O,'filter_high', T.FilterHigh); T.FilterNC := JI(O,'filter_nc', T.FilterNC); @@ -848,6 +858,7 @@ begin JW(O,'mic_line_in', T.MicLineIn); JW(O,'mic_ptt_enabled', T.MicPTTEnabled); JW(O,'mic_gain_db', T.MicGainDB); + JW(O,'att_on_tx', T.AttOnTX); JW(O,'filter_low', T.FilterLow); JW(O,'filter_high', T.FilterHigh); JW(O,'filter_nc', T.FilterNC); diff --git a/SettingsForm.pas b/SettingsForm.pas index 746a7f6..07e23cd 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -122,6 +122,7 @@ type FChkTXMicLineIn: TCheckBox; FChkTXMicPTT: TCheckBox; FEdTXMicGain: TFloatSpinEdit; + FEdTXAttOnTX: TSpinEdit; // Filter FEdTXFiltLow: TSpinEdit; FEdTXFiltHigh: TSpinEdit; @@ -952,6 +953,11 @@ begin MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 3 * STEP + 5, LW); FEdTXMicGain := MkFloat(Grp, CX, R1 + 3 * STEP, 10.0, -20.0, 60.0, 0.5); + // ATT on TX — RX-ADC ослабление во время передачи (DUC Specific bytes 57/58/59). + // Thetis-style default 31 dB. Помещаем на той же строке, что и Mic gain. + MakeLbl(Grp, 'ATT on TX (dB)', PAD + 320, R1 + 3 * STEP + 5, 110); + FEdTXAttOnTX := MkSpin(Grp, PAD + 320 + 120, R1 + 3 * STEP, 0, 31, 31); + // ---- TX bandpass filter ---- Inc(Y, 220 + 12); Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, 700, 200); @@ -1131,6 +1137,7 @@ begin if FChkTXMicLineIn <> nil then FTX.MicLineIn := FChkTXMicLineIn.Checked; if FChkTXMicPTT <> nil then FTX.MicPTTEnabled := FChkTXMicPTT.Checked; if FEdTXMicGain <> nil then FTX.MicGainDB := FEdTXMicGain.Value; + if FEdTXAttOnTX <> nil then FTX.AttOnTX := FEdTXAttOnTX.Value; if FEdTXFiltLow <> nil then FTX.FilterLow := FEdTXFiltLow.Value; if FEdTXFiltHigh <> nil then FTX.FilterHigh := FEdTXFiltHigh.Value; if FCmbTXFiltNC <> nil then @@ -1215,6 +1222,7 @@ begin if FChkTXMicLineIn <> nil then FChkTXMicLineIn.Checked := T.MicLineIn; if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled; if FEdTXMicGain <> nil then FEdTXMicGain.Value := T.MicGainDB; + if FEdTXAttOnTX <> nil then FEdTXAttOnTX.Value := EnsureRange(T.AttOnTX, 0, 31); if FEdTXFiltLow <> nil then FEdTXFiltLow.Value := T.FilterLow; if FEdTXFiltHigh <> nil then FEdTXFiltHigh.Value := T.FilterHigh; if FCmbTXFiltNC <> nil then FCmbTXFiltNC.Text := IntToStr(T.FilterNC); diff --git a/SpectrumView.pas b/SpectrumView.pas index 705de32..d9a49f7 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -89,6 +89,10 @@ type FTXFreq: Double; // центр TX-сигнала (Hz) FTXSpanHz: Double; // ширина TX-окна (192000 для Hermes/Saturn) FTXVfoIndex: Integer; // 0=A, 1=B — какой VFO привязан к TX (для split) + // FTXOverlay — независимый от FTXMode флаг: «нарисовать TX-фильтр поверх + // данных». DUP-режим: FTXMode=False (данные RX), но FTXOverlay=True (TX-полоса + // красным сверху). Без DUP при TX оба флага True. Без TX оба False. + FTXOverlay: Boolean; // ── Marker ──────────────────────────────────────────────────────────────── FMarkerActive: Boolean; @@ -167,6 +171,7 @@ type property TXFreq: Double read FTXFreq write FTXFreq; property TXSpanHz: Double read FTXSpanHz write FTXSpanHz; property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex; + property TXOverlay: Boolean read FTXOverlay write FTXOverlay; // ── Маркер ──────────────────────────────────────────────────────────────── property MarkerActive: Boolean read FMarkerActive write FMarkerActive; @@ -414,6 +419,7 @@ begin FTXFreq := 0.0; FTXSpanHz := 192000.0; FTXVfoIndex := 0; + FTXOverlay := False; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FWfFrameInterval := 2; @@ -774,7 +780,7 @@ begin // (для split-режима TX-полоса рисуется без edges чтобы визуально отличалась). if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX) else CalcFilterBandX(FVfoB, W, X1, X2, VfoX); - if FTXMode then + if FTXOverlay then begin if FTXVfoIndex <> FActiveVfo then begin @@ -881,7 +887,7 @@ begin C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1; C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]); // Split TX: рисуем края + cursor для TX-VFO красным, чтобы было видно где идёт передача - if FTXMode and (FTXVfoIndex <> FActiveVfo) then + if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then begin C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid; C.MoveTo(TXX1, 0); C.LineTo(TXX1, H); diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 77e664b..dfedfc4 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -350,6 +350,10 @@ type procedure SetDriveLevel(Level: Double); procedure SetMicGain(GainDB: Double); procedure SetTXRun(Run: Boolean); + // DUP: переключить ИСТОЧНИК пикселей display'а независимо от TX-аудио + // (SetTXRun меняет и аудио, и источник; этот метод позволяет вернуть + // RX-источник пока TX-аудио продолжает работать). + procedure SetDisplaySourceTX(UseTX: Boolean); // Расширенное TX управление (применяются мгновенно если канал открыт) procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer); @@ -1502,6 +1506,20 @@ begin end; end; +procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean); +begin + // Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID + // независимы, но мы переключаем источник, и старые байты в накапливающем + // буфере на новой стороне могут повести себя как «всплеск», если их не + // обнулить. + FDispPos := 0; + FTXDispPos := 0; + if UseTX then + FActiveDisplayID := TX_DISP_ID + else + FActiveDisplayID := RX_DISP_ID; +end; + procedure TWDSPEngine.ProcessTXBlock; // Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов // через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.