From 3acd569da5984bf5243e0c85072727bb9da031e6 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 16 Jul 2026 11:17:29 +0300 Subject: [PATCH] =?UTF-8?q?feat(puresignal):=20PureSignal-=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D1=8B=D1=81=D0=BA=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B4=D0=BB=D1=8F=20openHPSDR=20P2=20+=20=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BA=D0=B8=20PS/2TON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Протокол (по pihpsdr new_protocol.c): на PTT при PS DDC0<-ADC0 (RX-feedback), DDC1<-ADC2 (TX DAC loopback), 192к/24бит, sync DDC1->DDC0 (byte 1363=0x02, interleaved в потоке DDC0), частоты DDC0/1 = TX в HP-пакете, Alex0 bit18 (мост обратной связи). Caps.HasPureSignal для плат с DDCBase=2 (Angelia/Orion/Orion2/Saturn); попутно NumADCs=2 для Angelia. WDSPEngine: PushPSFeedback (деинтерлив -> pscc(TXA, tx=DDC1, rx=DDC0) из сетевого потока), PS-параметры calcc в кэше с переприменением после пересоздания TXA; SetTXTwoTone (PostGen mode 1, 700/1900, 0.49+0.49, инверсия LSB/CWL/DIGL, мьют mic как TUN). RadioController: state machine команд (порт Thetis PSForm timer1) в PureSignalTick @10Гц + auto-attenuate (AttOnTX по уровню feedback, цель ~152, через DUC Specific); SetPSMox и перестройка feedback-DDC на T/R; маршрутизация DDC0 в pscc из OnDDCIQ; SetTwoTone с гейтами safety. UI: кнопки PS (ЛКМ = поповер настроек, ПКМ = быстрый toggle) и 2TON в блоке TX; поповер TPureSignalPopup (дочерний контрол, Wayland-safe): PS ON/OFF, CAL, RESET, 2TON, лампа/уровень feedback, cals, corr, GetPk/SetPk, MOX/CAL wait, PH#, Auto-atten, Relax ptol, Pin/Map/Stbl, Tint, тона двухтональника. Персист ps_* в tx-секции per-device. Не проверено на железе; Save/Restore коррекции не реализованы. Co-Authored-By: Claude Fable 5 --- HPSDRNetwork.pas | 93 ++++++++- MainForm.pas | 90 ++++++++- PureSignalPopup.pas | 447 ++++++++++++++++++++++++++++++++++++++++++++ RadioBackend.pas | 21 +++ RadioController.pas | 379 ++++++++++++++++++++++++++++++++++++- Settings.pas | 52 ++++++ WDSPEngine.pas | 211 ++++++++++++++++++++- ewsdr.lpi | 5 +- 8 files changed, 1284 insertions(+), 14 deletions(-) create mode 100644 PureSignalPopup.pas diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index b3ffbcb..cc742c7 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -166,6 +166,13 @@ type FAlexEnabled: Boolean; FAlexConfig: TAlexSettings; // per-band antenna + routing config FStepAtten: Byte; // ADC0 step attenuator 0-31 dB + // ---- PureSignal ---- + // FPSEnabled — фича вооружена (кнопка PS). FPSTXActive — активная PTT-фаза: + // RebuildDDCSpecific включает feedback-DDC0 (ADC0) + sync DDC1 (TX DAC), + // SendFullHP ставит частоты DDC0/1 = TX и Alex bit18. Пишутся из потока + // контроллера (дисциплина как у FCurrentRXFreq). + FPSEnabled: Boolean; + FPSTXActive: Boolean; // ---- XVTR (Transverter) state ---- // FXvtrEnable — True когда активен XVTR-диапазон. Используется как // XvtrActive в CalcAlex0 и для управления byte 1400 bit 0. @@ -243,6 +250,9 @@ type procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double; RateKHz: Word; ADCSource: Byte); override; procedure SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double); override; + // PureSignal: вооружение фичи и PTT-фаза (см. TRadioBackend). + procedure SetPureSignal(Enabled: Boolean); override; + procedure SetPureSignalTX(Active: Boolean); override; procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); override; procedure SendHighPriority(const Pkt: THighPriorityPacket); override; procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; @@ -693,12 +703,14 @@ begin // см. doc/SLICES_PLAN.md 3.1). Каждый DDC тюнится независимо. Result.MaxPans := 4; Result.IndependentPanFreq := True; - // Два АЦП у ANAN-100D/200D (ORION), 7000/8000 (ORION2) и Saturn/G2 — + // Два АЦП у ANAN-100D/200D (ANGELIA/ORION), 7000/8000 (ORION2) и Saturn/G2 — // те же платы, что в RebuildDDCSpecific (Pkt.NumADCs). - if FDevice.BoardType in [4, 5, 10] then + if FDevice.BoardType in [3, 4, 5, 10] then Result.NumADCs := 2 else Result.NumADCs := 1; + // PureSignal: нужны свободные DDC0/1 под feedback — платы с DDCBase=2. + Result.HasPureSignal := FDevice.BoardType in [3, 4, 5, 10]; end; constructor THPSDRNetwork.Create; @@ -1328,7 +1340,8 @@ function CalcAlex0(RXFreqHz, TXFreqHz: Double; XvtrRxAnt: Byte = 0; XvtrDisablePA: Boolean = False; XvtrActive: Boolean = False; - WidebandBypass: Boolean = False): LongWord; + WidebandBypass: Boolean = False; + PureSignalTX: Boolean = False): LongWord; var txf: Double; BandIdx: Integer; @@ -1339,10 +1352,15 @@ begin // Bit 27 = T/R relay (0=RX, 1=TX). // Если XvtrDisablePA=True — НЕ устанавливаем bit 27 (PA остаётся в RX // положении, TX-сигнал идёт через XVTR-IF без усиления). - // Bit 18 (PureSignal) не ставим — PureSignal не реализован. if Transmitting then if not XvtrDisablePA then + begin Result := Result or $08000000; // bit 27: T/R relay + // Bit 18 = PureSignal (ALEX_PS_BIT в pihpsdr): на TX подключает + // внутренний мост обратной связи PA к RX-тракту ADC0. + if PureSignalTX then + Result := Result or $00040000; + end; // HPF (ANAN-100/200) или BPF (ANAN-7000/8000/Saturn/G2) — по RX-частоте. if WidebandBypass and (not Transmitting) then @@ -1545,8 +1563,8 @@ var begin FillChar(Pkt, SizeOf(Pkt), 0); - // ANAN-7000/8000/Saturn имеют 2 ADC - if FDevice.BoardType in [4, 5, 10] then // ORION=4, ORION2=5, SATURN=10 + // ANAN-100D/200D (ANGELIA/ORION), 7000/8000 (ORION2) и Saturn имеют 2 ADC + if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION2=5, SATURN=10 Pkt.NumADCs := 2 else Pkt.NumADCs := 1; @@ -1568,6 +1586,21 @@ begin if FPanDDCEnabled[ddc] then AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]); + // PureSignal feedback (как pihpsdr new_protocol_receive_specific): на время + // PTT DDC0 ← ADC0 (RX-feedback с моста PA), DDC1 ← «ADC 2» = TX DAC loopback, + // оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) — + // его сэмплы приходят interleaved внутри потока DDC0 (порт 1035). + // Enable-бит ставится только DDC0. Только платы с DDCBase=2 (DDC0/1 свободны). + if FPSTXActive and (DDCBase = 2) then + begin + AddDDC(0, 192, 0); // DDC0: enable, ADC0, 192 kSPS, 24 bit + Pkt.DDCConfig[1 * 6] := 2; // DDC1: источник = TX DAC (Angelia/Orion) + Pkt.DDCConfig[1 * 6 + 1] := 0; + Pkt.DDCConfig[1 * 6 + 2] := 192; + Pkt.DDCConfig[1 * 6 + 5] := 24; + Pkt.SyncDDC[0] := $02; // sync DDC1 → DDC0 + end; + SendDDCSpecific(Pkt); FCachedDDCSpec := Pkt; FCachedDDCValid := True; @@ -1596,6 +1629,37 @@ begin if FConnected and FPanDDCEnabled[DDCIdx] then SendFullHP; end; +procedure THPSDRNetwork.SetPureSignal(Enabled: Boolean); +begin + // Только платы, где DDC0/1 зарезервированы под feedback (Angelia/Orion/ + // Orion2/Saturn). На прочих (Hermes: DDC0 = главный) PS не включаем. + if Enabled and (DDCBaseIndex <> 2) then Exit; + if FPSEnabled = Enabled then Exit; + FPSEnabled := Enabled; + if not Enabled and FPSTXActive then + begin + // Выключение посреди передачи: убрать feedback-DDC немедленно. + FPSTXActive := False; + if FConnected and FRunning then + begin + RebuildDDCSpecific; + SendFullHP; + end; + end; +end; + +procedure THPSDRNetwork.SetPureSignalTX(Active: Boolean); +begin + Active := Active and FPSEnabled and (DDCBaseIndex = 2); + if FPSTXActive = Active then Exit; + FPSTXActive := Active; + if FConnected and FRunning then + begin + RebuildDDCSpecific; // включить/убрать DDC0 + sync DDC1 + SendFullHP; // частоты DDC0/1 = TX, Alex bit18 + end; +end; + procedure THPSDRNetwork.SendDUCSpecific(const Pkt: TDUCSpecificPacket); var B: TDUCSpecificPacket; @@ -1691,6 +1755,21 @@ begin Buf[12 + ddc*4] := Ph and $FF; end; + // PureSignal: feedback-DDC0/DDC1 следят за TX-частотой (pihpsdr, bytes 9..16). + // DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся. + if FPSTXActive then + begin + Ph := FreqToPhaseWord(FCurrentTXFreq); + Buf[9] := (Ph shr 24) and $FF; + Buf[10] := (Ph shr 16) and $FF; + Buf[11] := (Ph shr 8) and $FF; + Buf[12] := Ph and $FF; + Buf[13] := (Ph shr 24) and $FF; + Buf[14] := (Ph shr 16) and $FF; + Buf[15] := (Ph shr 8) and $FF; + Buf[16] := Ph and $FF; + end; + // DUC TX frequency (bytes 329-332) Ph := FreqToPhaseWord(FCurrentTXFreq); Buf[329] := (Ph shr 24) and $FF; @@ -1708,7 +1787,7 @@ begin Alex0 := CalcAlex0(FCurrentRXFreq, FCurrentTXFreq, FIsTransmitting, IsOrion2, FAlexConfig, FXvtrRxAntOverride, FXvtrDisablePA, - FXvtrEnable, FWidebandEnabled); + FXvtrEnable, FWidebandEnabled, FPSTXActive); Buf[1432] := (Alex0 shr 24) and $FF; Buf[1433] := (Alex0 shr 16) and $FF; Buf[1434] := (Alex0 shr 8) and $FF; diff --git a/MainForm.pas b/MainForm.pas index 009dd21..76f1f6e 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -33,7 +33,7 @@ uses WebServer, WebAdapter, CATAdapter, SpectrumView, SpectrumViewOpengl, - PanZoomBar, PanafallPanel, PanDisplayPopup, FlatPopupMenu, + PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu, WidebandView, StatusBar, PlatformUtils, @@ -337,6 +337,9 @@ type TrkDrive: TFlatSlider; BtnMOX: TFlatButton; BtnTUN: TFlatButton; + BtnPS: TFlatButton; // PureSignal: ЛКМ = вкл/выкл, ПКМ = поповер настроек + Btn2Ton: TFlatButton; // двухтональник (IMD/проверка PS) + FPSPopup: TObject; // TPureSignalPopup (cast при использовании) PanelRXBlock: TPanel; BtnChannels: TFlatButton; @@ -460,6 +463,13 @@ type procedure UpdateBandPlanOverlay; // вкл/выкл полоски бэндплана QO-100 по InQO100 procedure BtnTUNClick(Sender: TObject); procedure ApplyTUN(Active: Boolean); + // PureSignal: ЛКМ по PS = вкл/выкл, ПКМ = поповер настроек. + procedure BtnPSClick(Sender: TObject); + procedure BtnPSMouseDown(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); + procedure Btn2TonClick(Sender: TObject); + procedure OnPSPopupChanged(Sender: TObject); // персист PS-параметров + procedure UpdatePSButtons; // стиль/доступность PS/2TON procedure UpdateFilterButtons; procedure BtnFMSQClick(Sender: TObject); procedure TrkFMSQChange(Sender: TObject); @@ -1369,6 +1379,8 @@ begin end; // (DUC-очередь сбрасывает SetMOX) rfTuning: StyleButton(BtnTUN, FController.FTuning); + rfPureSignal, rfTwoTone: + UpdatePSButtons; // подсветка PS/2TON + обновление статусов поповера rfRunning: // Run-состояние: кнопка START/STOP, доступность MOX, запрет сна. // Текст offline-статуса контекстно-зависим (Disconnected / Connection @@ -1952,6 +1964,14 @@ begin BtnTUN := MakeBtn(PanelTX, 'TUN', LeftPanelButtonLeft(LEFT_W, 4, 1), 16, LeftPanelButtonWidth(LEFT_W, 4, 1), BTN_SM, BtnTUNClick); + // PureSignal: ЛКМ = вкл/выкл, ПКМ = поповер настроек/управления. + BtnPS := MakeBtn(PanelTX, 'PS', + LeftPanelButtonLeft(LEFT_W, 4, 2), 16, + LeftPanelButtonWidth(LEFT_W, 4, 2), BTN_SM, BtnPSClick); + BtnPS.OnMouseDown := BtnPSMouseDown; + Btn2Ton := MakeBtn(PanelTX, '2TON', + LeftPanelButtonLeft(LEFT_W, 4, 3), 16, + LeftPanelButtonWidth(LEFT_W, 4, 3), BTN_SM, Btn2TonClick); UpdateTXControlAvailability; BtnDUP := MakeBtn(PanelTX, 'DUP', @@ -3291,6 +3311,9 @@ begin FController.ServiceBeaconLock; // Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR). FController.ServicePlutoTelemetry; + // PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo + // (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal). + FController.PureSignalTick; // Сглаженное среднее (EMA) FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA; @@ -3534,6 +3557,25 @@ begin (FController.ActiveTXMode <> MODE_DMR); BtnMOX.Enabled := Allowed; BtnTUN.Enabled := Allowed; + UpdatePSButtons; +end; + +procedure TMainForm.UpdatePSButtons; +// PS: доступен на openHPSDR-платах с feedback-DDC (Caps.HasPureSignal) при +// Run. 2TON — обычный TX-тест, доступен как MOX/TUN. +var + PSAllowed, TXAllowed: Boolean; +begin + if (BtnPS = nil) or (Btn2Ton = nil) or (FController = nil) then Exit; + TXAllowed := FController.FRunning and + (FController.ActiveTXMode <> MODE_DMR); + PSAllowed := TXAllowed and FController.PSAvailable; + BtnPS.Enabled := PSAllowed; + Btn2Ton.Enabled := TXAllowed; + StyleButton(BtnPS, FController.FPSEnabled or FController.PSSingleCalActive); + StyleButton(Btn2Ton, FController.FTwoTone); + if (FPSPopup <> nil) and TPureSignalPopup(FPSPopup).Visible then + TPureSignalPopup(FPSPopup).RefreshStatus; end; procedure TMainForm.UpdateStatusField3; @@ -4481,6 +4523,52 @@ begin ApplyTUN(not FController.FTuning); end; +// --------------------------------------------------------------------------- +// PureSignal: кнопка PS (ЛКМ = вкл/выкл, ПКМ = поповер настроек) + 2TON +// --------------------------------------------------------------------------- +procedure TMainForm.BtnPSClick(Sender: TObject); +begin + if not FController.PSAvailable then Exit; + FController.SetPureSignal(not FController.FPSEnabled); // рендер через rfPureSignal +end; + +procedure TMainForm.BtnPSMouseDown(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); +var + Pop: TPureSignalPopup; +begin + if Button <> mbRight then Exit; + if not FController.PSAvailable then Exit; + if FPSPopup = nil then + begin + Pop := TPureSignalPopup.Create(Self); + Pop.OnChanged := OnPSPopupChanged; + FPSPopup := Pop; + end + else + Pop := TPureSignalPopup(FPSPopup); + if Pop.Visible then + Pop.Hide + else + Pop.PopupBelow(FController, BtnPS); +end; + +procedure TMainForm.Btn2TonClick(Sender: TObject); +begin + FController.SetTwoTone(not FController.FTwoTone); // рендер через rfTwoTone +end; + +procedure TMainForm.OnPSPopupChanged(Sender: TObject); +// Правка PS-параметров в поповере: применение к WDSP делает сам поповер +// (ApplyPSSettingsToDSP), здесь — только персист per-device. +begin + if FController.FDevConnected then + begin + FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); + FController.FSettings.Save; + end; +end; + // --------------------------------------------------------------------------- // Клик/драг по спектру/водопаду // --------------------------------------------------------------------------- diff --git a/PureSignalPopup.pas b/PureSignalPopup.pas new file mode 100644 index 0000000..7ba5724 --- /dev/null +++ b/PureSignalPopup.pas @@ -0,0 +1,447 @@ +unit PureSignalPopup; + +{ + TPureSignalPopup — поповер управления и настроек PureSignal, открывается + ПКМ по кнопке PS на левой панели. Управление: PS (авто-калибровка вкл/выкл), + CAL (одиночная калибровка), RESET (сброс коррекции), 2TON. Статус: лампа и + уровень feedback (оптимум ~152), счётчик калибровок, лампа коррекции, + GetPk/SetPk. Параметры calcc (MOX/CAL wait, PH#, Relax ptol, Auto-atten, + Pin/Map/Stbl, Ints, тона 2TON) пишутся в FTXSettings.PS* живьём: + ApplyPSSettingsToDSP + OnChanged (персист в MainForm). + + Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно) — + как TPanDisplayPopup: Wayland не даёт клиенту позиционировать top-level окна. +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Math, Types, Controls, ExtCtrls, StdCtrls, Graphics, Forms, + RadioController, Settings, + FlatButton, FlatComboBox, FlatCheckBox, FlatSpinEdit, FlatFloatSpinEdit; + +type + TPureSignalPopup = class(TCustomControl) + private + FController: TRadioController; // не владеем + FLoading: Boolean; // подавляет Apply при программной заливке + FBg: TPanel; + FTitle: TLabel; + FBtnClose: TFlatButton; + // Управление + FBtnPS: TFlatButton; + FBtnCal: TFlatButton; + FBtnReset: TFlatButton; + FBtn2Ton: TFlatButton; + // Статус + FFbLamp: TPanel; + FLbFbVal: TLabel; + FLbCals: TLabel; + FCorrLamp: TPanel; + FLbGetPk: TLabel; + FLbAtt: TLabel; + // Параметры + FSePk: TFlatFloatSpinEdit; + FBtnDefPk: TFlatButton; + FSeMoxWait: TFlatFloatSpinEdit; + FSeCalWait: TFlatFloatSpinEdit; + FSePhNum: TFlatSpinEdit; + FChkRelax: TFlatCheckBox; + FChkAutoAtt:TFlatCheckBox; + FChkPin: TFlatCheckBox; + FChkMap: TFlatCheckBox; + FChkStbl: TFlatCheckBox; + FCbInts: TFlatComboBox; + FSeTT1: TFlatSpinEdit; + FSeTT2: TFlatSpinEdit; + FOnChanged: TNotifyEvent; + + function MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel; + procedure Apply(Sender: TObject); // контролы → FTXSettings.PS* → WDSP + procedure PSClick(Sender: TObject); + procedure CalClick(Sender: TObject); + procedure ResetClick(Sender: TObject); + procedure TwoToneClick(Sender: TObject); + procedure DefPkClick(Sender: TObject); + procedure CloseClick(Sender: TObject); + protected + procedure Paint; override; + public + constructor Create(AOwner: TComponent); override; + procedure LoadFrom(C: TRadioController); + // Обновление статусных ламп/значений (звать с UI-таймера пока Visible + // и по rfPureSignal/rfTwoTone). + procedure RefreshStatus; + // Показать под контролом-якорем (кнопкой PS) как дочерний контрол формы. + procedure PopupBelow(C: TRadioController; Anchor: TControl); + property OnChanged: TNotifyEvent read FOnChanged write FOnChanged; + end; + +implementation + +const +{$IFDEF WINDOWS} + UI_FONT = 'Segoe UI'; +{$ELSE} + UI_FONT = 'Sans'; +{$ENDIF} + CLR_BG = TColor($00303030); // рамка (1px кайма формы) + CLR_PANEL = TColor($001A1A1A); + CLR_TEXT = TColor($00E0E0E0); + CLR_TEXTDIM = TColor($00888888); + CLR_ACCENT = TColor($0040FF80); + CLR_INPUT = TColor($00FFFFFF); + CLR_INPUT_TEXT = TColor($00202020); + + // Лампа feedback (пороги Thetis: >181 горячо, 129..181 оптимум, 91..128 + // слабовато, <=90 нет сигнала). TColor = BGR. + CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал) + CLR_FB_OK = TColor($0000FF00); // зелёный: оптимум + CLR_FB_LOW = TColor($0000FFFF); // жёлтый: слабовато + CLR_FB_NONE = TColor($000000C0); // красный: нет feedback + CLR_LAMP_OFF= TColor($00101010); + + POP_W = 272; + PAD = 10; + LBLW = 92; + CTLX = 108; + ROWH = 24; + +constructor TPureSignalPopup.Create(AOwner: TComponent); +var + Y: Integer; + + function NextRow: Integer; + begin + Result := Y; + Inc(Y, ROWH + 4); + end; + + function MakeIntSpin(ATop, AMin, AMax, AInc: Integer): TFlatSpinEdit; + begin + Result := TFlatSpinEdit.Create(Self); + Result.Parent := FBg; + Result.SetBounds(CTLX, ATop, POP_W - CTLX - PAD, ROWH - 2); + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Increment := AInc; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := Apply; + end; + + function MakeFloatSpin(ATop: Integer; AMin, AMax, AInc: Double; + ADec: Integer): TFlatFloatSpinEdit; + begin + Result := TFlatFloatSpinEdit.Create(Self); + Result.Parent := FBg; + Result.SetBounds(CTLX, ATop, POP_W - CTLX - PAD, ROWH - 2); + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Increment := AInc; + Result.DecimalPlaces := ADec; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := Apply; + end; + + function MakeChk(const Cap: string; ALeft, ATop, AW: Integer): TFlatCheckBox; + begin + Result := TFlatCheckBox.Create(Self); + Result.Parent := FBg; + Result.SetBounds(ALeft, ATop, AW, ROWH - 4); + Result.Caption := Cap; + Result.Font.Color := CLR_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := Apply; + end; + + function MakeLamp(ALeft, ATop: Integer): TPanel; + begin + Result := TPanel.Create(Self); + Result.Parent := FBg; + Result.SetBounds(ALeft, ATop, 16, 16); + Result.BevelOuter := bvNone; + Result.Color := CLR_LAMP_OFF; + Result.Caption := ''; + end; + +var + BW: Integer; +begin + inherited Create(AOwner); + ControlStyle := ControlStyle + [csOpaque]; + Visible := False; + Color := CLR_BG; + Font.Name := UI_FONT; + Font.Size := 9; + Font.Color := CLR_TEXT; + FLoading := False; + + FBg := TPanel.Create(Self); + FBg.Parent := Self; + FBg.BevelOuter := bvNone; + FBg.Color := CLR_PANEL; + FBg.SetBounds(1, 1, POP_W - 2, 10); + + Y := PAD; + + FTitle := MakeLbl('PureSignal', PAD, NextRow + 2, POP_W - 2 * PAD - 18); + FTitle.Font.Color := CLR_ACCENT; + FTitle.Font.Style := [fsBold]; + FBtnClose := MakeFlatBtn(FBg, '×', POP_W - PAD - 16, PAD, 16, 16, CloseClick); + FBtnClose.ClrText := CLR_TEXTDIM; + FBtnClose.ClrTextAct := CLR_TEXT; + + // ---- Управление: PS / CAL / RESET / 2TON ---- + BW := (POP_W - 2 * PAD - 2 * 6) div 3; + FBtnPS := MakeFlatBtn(FBg, 'PS ON', PAD, Y, BW, ROWH, PSClick); + FBtnCal := MakeFlatBtn(FBg, 'CAL', PAD + BW + 6, Y, BW, ROWH, CalClick); + FBtnReset := MakeFlatBtn(FBg, 'RESET', PAD + 2 * (BW + 6), Y, BW, ROWH, ResetClick); + NextRow; + FBtn2Ton := MakeFlatBtn(FBg, '2TON', PAD, Y, BW, ROWH, TwoToneClick); + // ---- Статус: лампа + FB + cals + corr в одной строке с 2TON ---- + FFbLamp := MakeLamp(PAD + BW + 6, Y + 4); + FLbFbVal := MakeLbl('FB: —', PAD + BW + 28, Y + 4, 68); + FCorrLamp := MakeLamp(POP_W - PAD - 62, Y + 4); + MakeLbl('corr', POP_W - PAD - 42, Y + 4, 36).Font.Color := CLR_TEXTDIM; + NextRow; + FLbCals := MakeLbl('cals: 0', PAD, Y + 2, 90); + FLbCals.Font.Color := CLR_TEXTDIM; + FLbAtt := MakeLbl('TX ATT: — dB', PAD + 96, Y + 2, POP_W - 2 * PAD - 96); + FLbAtt.Font.Color := CLR_TEXTDIM; + Inc(Y, 20); + + MakeLbl('— Calibration —', PAD, Y + 3, POP_W - 2 * PAD).Font.Color := CLR_TEXTDIM; + NextRow; + + // ---- GetPk / SetPk ---- + FLbGetPk := MakeLbl('GetPk: —', PAD, Y + 3, LBLW); + FSePk := MakeFloatSpin(Y, 0.05, 1.00, 0.01, 4); + FSePk.Width := FSePk.Width - 42; + FBtnDefPk := MakeFlatBtn(FBg, 'DEF', POP_W - PAD - 38, Y, 38, ROWH - 2, DefPkClick); + NextRow; + + MakeLbl('MOX wait, s', PAD, Y + 3, LBLW); + FSeMoxWait := MakeFloatSpin(Y, 0.0, 0.9, 0.1, 1); + NextRow; + MakeLbl('CAL wait, s', PAD, Y + 3, LBLW); + FSeCalWait := MakeFloatSpin(Y, 0.0, 10.0, 0.5, 1); + NextRow; + MakeLbl('PH# delay, ns', PAD, Y + 3, LBLW); + FSePhNum := MakeIntSpin(Y, 0, 1000, 10); + NextRow; + + FChkAutoAtt := MakeChk('Auto-attenuate', PAD, Y + 2, 130); + FChkRelax := MakeChk('Relax tolerance', PAD + 134, Y + 2, POP_W - 2 * PAD - 134); + NextRow; + FChkPin := MakeChk('Pin', PAD, Y + 2, 56); + FChkMap := MakeChk('Map', PAD + 62, Y + 2, 56); + FChkStbl := MakeChk('Stbl', PAD + 124, Y + 2, 60); + NextRow; + + MakeLbl('Tint (ints/spi)', PAD, Y + 3, LBLW); + FCbInts := TFlatComboBox.Create(Self); + FCbInts.Parent := FBg; + FCbInts.SetBounds(CTLX, Y, POP_W - CTLX - PAD, ROWH - 2); + FCbInts.Items.Add('16 / 256'); + FCbInts.Items.Add('8 / 512'); + FCbInts.Items.Add('4 / 1024'); + FCbInts.Color := CLR_INPUT; + FCbInts.Font.Color := CLR_INPUT_TEXT; + FCbInts.Font.Name := UI_FONT; + FCbInts.Font.Size := 9; + FCbInts.OnChange := Apply; + NextRow; + + MakeLbl('— Two-tone, Hz —', PAD, Y + 3, POP_W - 2 * PAD).Font.Color := CLR_TEXTDIM; + NextRow; + MakeLbl('F1 / F2', PAD, Y + 3, LBLW); + FSeTT1 := MakeIntSpin(Y, 100, 5000, 100); + FSeTT1.Width := (POP_W - CTLX - PAD - 6) div 2; + FSeTT2 := MakeIntSpin(Y, 100, 5000, 100); + FSeTT2.SetBounds(CTLX + FSeTT1.Width + 6, Y, (POP_W - CTLX - PAD - 6) div 2, ROWH - 2); + NextRow; + + FBg.Height := Y - 1 + PAD; + Width := POP_W; + Height := Y + 1 + PAD; +end; + +procedure TPureSignalPopup.Paint; +begin + Canvas.Brush.Style := bsSolid; + Canvas.Brush.Color := CLR_BG; + Canvas.Pen.Style := psClear; + Canvas.FillRect(0, 0, Width, Height); +end; + +function TPureSignalPopup.MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel; +begin + Result := TLabel.Create(Self); + Result.Parent := FBg; + Result.Caption := Cap; + Result.Left := ALeft; + Result.Top := ATop; + if AW > 0 then Result.Width := AW; + Result.Font.Color := CLR_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; +end; + +procedure TPureSignalPopup.LoadFrom(C: TRadioController); +begin + FController := C; + if C = nil then Exit; + FLoading := True; + try + FSePk.Value := EnsureRange(C.FTXSettings.PSHWPeak, 0.05, 1.00); + FSeMoxWait.Value := EnsureRange(C.FTXSettings.PSMoxDelay, 0.0, 0.9); + FSeCalWait.Value := EnsureRange(C.FTXSettings.PSLoopDelay, 0.0, 10.0); + FSePhNum.Value := EnsureRange(C.FTXSettings.PSTXDelayNs, 0, 1000); + FChkRelax.Checked := C.FTXSettings.PSRelaxPtol; + FChkAutoAtt.Checked := C.FTXSettings.PSAutoAtten; + FChkPin.Checked := C.FTXSettings.PSPin; + FChkMap.Checked := C.FTXSettings.PSMap; + FChkStbl.Checked := C.FTXSettings.PSStabilize; + FCbInts.ItemIndex := EnsureRange(C.FTXSettings.PSIntsIdx, 0, 2); + FSeTT1.Value := EnsureRange(C.FTXSettings.PSTTFreq1, 100, 5000); + FSeTT2.Value := EnsureRange(C.FTXSettings.PSTTFreq2, 100, 5000); + finally + FLoading := False; + end; + RefreshStatus; +end; + +procedure TPureSignalPopup.Apply(Sender: TObject); +begin + if FLoading or (FController = nil) then Exit; + FController.FTXSettings.PSHWPeak := FSePk.Value; + FController.FTXSettings.PSMoxDelay := FSeMoxWait.Value; + FController.FTXSettings.PSLoopDelay := FSeCalWait.Value; + FController.FTXSettings.PSTXDelayNs := FSePhNum.Value; + FController.FTXSettings.PSRelaxPtol := FChkRelax.Checked; + FController.FTXSettings.PSAutoAtten := FChkAutoAtt.Checked; + FController.FTXSettings.PSPin := FChkPin.Checked; + FController.FTXSettings.PSMap := FChkMap.Checked; + FController.FTXSettings.PSStabilize := FChkStbl.Checked; + FController.FTXSettings.PSIntsIdx := EnsureRange(FCbInts.ItemIndex, 0, 2); + FController.FTXSettings.PSTTFreq1 := FSeTT1.Value; + FController.FTXSettings.PSTTFreq2 := FSeTT2.Value; + FController.ApplyPSSettingsToDSP; + if Assigned(FOnChanged) then FOnChanged(Self); +end; + +procedure TPureSignalPopup.PSClick(Sender: TObject); +begin + if FController = nil then Exit; + FController.SetPureSignal(not FController.FPSEnabled); + RefreshStatus; +end; + +procedure TPureSignalPopup.CalClick(Sender: TObject); +begin + if FController = nil then Exit; + FController.PSSingleCal; + RefreshStatus; +end; + +procedure TPureSignalPopup.ResetClick(Sender: TObject); +begin + if FController = nil then Exit; + FController.PSReset; + RefreshStatus; +end; + +procedure TPureSignalPopup.TwoToneClick(Sender: TObject); +begin + if FController = nil then Exit; + FController.SetTwoTone(not FController.FTwoTone); + RefreshStatus; +end; + +procedure TPureSignalPopup.DefPkClick(Sender: TObject); +// Дефолт SetPk для протокола 2 (Thetis clsHardwareSpecific.PSDefaultPeak). +begin + FSePk.Value := 0.2899; // OnChange → Apply +end; + +procedure TPureSignalPopup.CloseClick(Sender: TObject); +begin + Hide; +end; + +procedure TPureSignalPopup.RefreshStatus; +var + FB: Integer; + LampClr: TColor; +begin + if (FController = nil) or not Visible then Exit; + + FBtnPS.Active := FController.FPSEnabled; + if FController.FPSEnabled then FBtnPS.Caption := 'PS ON' + else FBtnPS.Caption := 'PS OFF'; + FBtnCal.Active := FController.PSSingleCalActive; + FBtn2Ton.Active := FController.FTwoTone; + + FB := FController.PSFeedbackLevel; + if FController.FPSFeedbackOn then + begin + if FB > 181 then LampClr := CLR_FB_HOT + else if FB > 128 then LampClr := CLR_FB_OK + else if FB > 90 then LampClr := CLR_FB_LOW + else LampClr := CLR_FB_NONE; + end + else + LampClr := CLR_LAMP_OFF; + if FFbLamp.Color <> LampClr then FFbLamp.Color := LampClr; + FLbFbVal.Caption := Format('FB: %d', [FB]); + + if FController.PSCorrecting then + begin + if FCorrLamp.Color <> CLR_FB_OK then FCorrLamp.Color := CLR_FB_OK; + end + else if FCorrLamp.Color <> CLR_LAMP_OFF then + FCorrLamp.Color := CLR_LAMP_OFF; + + FLbCals.Caption := Format('cals: %d', [FController.PSCalCount]); + FLbAtt.Caption := Format('TX ATT: %d dB', [FController.FTXSettings.AttOnTX]); + FLbGetPk.Caption := Format('GetPk: %.3f', [FController.PSMaxTXVal]); +end; + +procedure TPureSignalPopup.PopupBelow(C: TRadioController; Anchor: TControl); +var + Root: TCustomForm; + L, T: Integer; + Cl: TPoint; +begin + if Anchor = nil then Exit; + Root := GetParentForm(Anchor); + if Root = nil then Exit; + LoadFrom(C); + // Позиция под якорем в КЛИЕНТСКИХ координатах формы (не top-level окно!). + Cl := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, Anchor.Height + 2))); + L := Cl.X; + T := Cl.Y; + if L + Width > Root.ClientWidth then L := Root.ClientWidth - Width; + if L < 0 then L := 0; + if T + Height > Root.ClientHeight then + T := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, 0))).Y - Height; // вверх + if T < 0 then T := 0; + Parent := Root; + SetBounds(L, T, Width, Height); + Visible := True; + BringToFront; + RefreshStatus; +end; + +end. diff --git a/RadioBackend.pas b/RadioBackend.pas index ca045a7..ad73241 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -56,6 +56,10 @@ type // парами SMA (LibreSDR/AntSDR/Pluto+). Гейтит блок «Antenna» в настройках; // у openHPSDR антеннами рулит Alex, поэтому False. Достоверно после Connect. HasAntSwitch: Boolean; + // PureSignal-предыскажение: openHPSDR P2 платы со свободными DDC0/1 под + // feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON. + // Достоверно после Connect. + HasPureSignal: Boolean; MinSampleRate: Integer; RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay MinFreqHz: Double; @@ -142,6 +146,14 @@ type // У бэкендов без переключателя — no-op. procedure SetAntPorts(RxPort, TxPort: Byte); virtual; + // ---- PureSignal (openHPSDR P2, платы с DDC0/1 под feedback) ---- + // SetPureSignal — фича вооружена (кнопка PS в UI). SetPureSignalTX — фаза + // PTT: на True бэкенд включает feedback-DDC0 (+sync DDC1 = TX DAC) на 192к + // и ставит их частоты = TX; на False возвращает RX-конфигурацию. + // База — no-op (Pluto и др. без PS-тракта). + procedure SetPureSignal(Enabled: Boolean); virtual; + procedure SetPureSignalTX(Active: Boolean); virtual; + // ---- Частота / drive / TX-состояние ---- procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; DriveLevel: Byte = 100); virtual; abstract; @@ -306,4 +318,13 @@ procedure TRadioBackend.SetAntPorts(RxPort, TxPort: Byte); begin end; +procedure TRadioBackend.SetPureSignal(Enabled: Boolean); +// PureSignal есть только у openHPSDR P2 (THPSDRNetwork переопределяет). +begin +end; + +procedure TRadioBackend.SetPureSignalTX(Active: Boolean); +begin +end; + end. diff --git a/RadioController.pas b/RadioController.pas index 38c5a59..fd49c48 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -74,6 +74,7 @@ type rfCenterFreq, rfSpan, rfSampleRate, rfZoom, rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle rfRunning, rfConnected, rfTransmitting, rfTuning, + rfPureSignal, rfTwoTone, // PureSignal вкл/статус; 2TON вкл/выкл rfNR, rfNB, rfSNB, rfANF, rfWfAGC, rfWfNF, rfDither, rfRandom, rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel, @@ -289,6 +290,22 @@ type FRunning: Boolean; FTransmitting: Boolean; FTuning: Boolean; + // ---- PureSignal ---- + FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS) + FPSSingleCal: Boolean; // запрошена одиночная калибровка + FPSResetReq: Boolean; // запрошен сброс коррекции (OFF) + FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick) + FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore) + FPSAASaveAuto: Integer; // сохранённый automode на время смены атт. + FPSAASaveSingle: Integer; + FPSAADeltaDB: Integer; + FPSInfo: array[0..15] of Integer; // кэш GetPSInfo (UI-поток) + FPSInfoPrev: array[0..15] of Integer; + FPSMaxTX: Double; // кэш GetPSMaxTX + // Feedback-тракт вооружён (сеть DDC0/1 + pscc в движке). Пишет UI-поток + // (PureSignalTick/PSSetFeedbackPath), читает сетевой (OnDDCIQ). + FPSFeedbackOn: Boolean; + FTwoTone: Boolean; // 2TON активен FSplitTxB: Boolean; FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock) @@ -721,6 +738,31 @@ type // Передача procedure SetMOX(On_: Boolean); procedure SetTune(On_: Boolean); // TUN + + // ---- PureSignal ---- + // Доступность PS у активного бэкенда/платы (Caps.HasPureSignal). + function PSAvailable: Boolean; + // Вкл/выкл авто-калибровки PureSignal (кнопка PS). Вооружает feedback-тракт + // (DDC0/1 на TX) и WDSP calcc automode; выкл = reset коррекции. + procedure SetPureSignal(On_: Boolean); + procedure PSSingleCal; // одиночная калибровка + procedure PSReset; // выключить и сбросить коррекцию + // Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis + // PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX. + procedure PureSignalTick; + procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc + // Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно. + procedure PSSetFeedbackPath(En: Boolean); + // Статус для UI (кэш последнего PureSignalTick) + function PSFeedbackLevel: Integer; // info[4], оптимум ~140-160 + function PSCalCount: Integer; // info[5] + function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется + function PSMaxTXVal: Double; // GetPSMaxTX (пик TX для SetPk) + function PSSingleCalActive: Boolean; // идёт одиночная калибровка + + // 2TON: двухтональник (PostGen two-tone) + MOX — тест IMD / проверка PS. + procedure SetTwoTone(On_: Boolean); + procedure SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A) procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO @@ -815,6 +857,16 @@ const // (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel. TUN_TONE_MAG = 0.83; + // Состояния командной state machine PureSignal (порт Thetis eCMDState; + // Restore-ветка (7) не реализована — Save/Restore коррекции нет). + PS_ST_OFF = 0; + PS_ST_TURNON_AUTO = 1; + PS_ST_AUTOCAL = 2; + PS_ST_TURNON_SINGLE = 3; + PS_ST_SINGLECAL = 4; + PS_ST_STAYON = 5; + PS_ST_TURNOFF = 6; + constructor TDMRSliceDecoder.Create(AOwner: TRadioController; ASliceId: Integer); begin @@ -1065,13 +1117,20 @@ begin FDDCSeqValid[DDCIndex] := True; end; - // Подаём IQ в DSP: активный DDC → главный тракт, пан-DDC → его пан (3.2). + // Подаём IQ в DSP: активный DDC → главный тракт, пан-DDC → его пан (3.2), + // DDC0 при вооружённом PureSignal на TX → feedback в pscc (DDC0 несёт + // interleaved пары DDC0+DDC1; на PS-платах главный DDC ≥ 2, конфликта нет). if FWDSPReady then begin SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or Integer(Data.SamplesPerFrame[1]); if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback - if DDCIndex = FActiveDDC then + if (DDCIndex = 0) and FPSFeedbackOn and (FActiveDDC <> 0) then + begin + if FTransmitting then + FDSPEngine.PushPSFeedback(Data.IQData, 0, SamplesPerFrame); + end + else if DDCIndex = FActiveDDC then FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame) else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS) and (FDDCPanMap[DDCIndex] > 0) then @@ -4063,6 +4122,8 @@ begin FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector, FTXSettings.TXWfAvgMode, FTXSettings.TXWfAvgTimeMS); + // PureSignal-параметры calcc (mox/loop/tx delay, ptol, pin/map/stbl, SetPk) + ApplyPSSettingsToDSP; end; procedure TRadioController.SetMOX(On_: Boolean); @@ -4081,6 +4142,14 @@ begin Exit; WasTransmitting := FTransmitting; FTransmitting := On_; + // 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник. + if (not On_) and FTwoTone then + begin + FTwoTone := False; + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetTXTwoTone(False, 0, 0); + Changed(rfTwoTone); + end; if On_ then begin FDriveLevel := CalcDriveByte; @@ -4096,6 +4165,9 @@ begin FDriveLevel, True, True, True); FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле + // PureSignal: перевести feedback-DDC0/1 в TX-конфигурацию (частоты = TX, + // sync, Alex bit18). No-op если PS не вооружён (гейт внутри бэкенда). + FNetwork.SetPureSignalTX(True); // Pre-roll: THROTTLE (2000, порог FPGA — DAC не стартует с пустого буфера) // + ДВА WDSP-блока (2×2048) нулевых пар подушкой в очередь. Раньше было 3 // блока: обоснование «2 не хватало — щелчок через ~0.5с» теперь под вопросом, @@ -4140,6 +4212,8 @@ begin // Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог // оставить главный режим). Ставим перед стартом TX-потока. if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode); + // PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc). + FDSPEngine.PSSetMox(FTransmitting); FDSPEngine.SetTXRun(FTransmitting); if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then begin @@ -4161,6 +4235,8 @@ begin if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT + // PureSignal: вернуть DDC0/1 в RX-конфигурацию (убрать feedback-DDC). + FNetwork.SetPureSignalTX(False); FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FDriveLevel, False, True, True); FNetwork.ClearDUCIQQueue; @@ -4181,6 +4257,7 @@ begin and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then Exit; if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; + if On_ and FTwoTone then SetTwoTone(False); // TUN поверх 2TON — гасим тона if On_ then begin // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. @@ -4210,6 +4287,285 @@ begin Changed(rfTuning); end; +// =========================================================================== +// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2) +// =========================================================================== + +function TRadioController.PSAvailable: Boolean; +begin + Result := FDevConnected and BackendCaps.HasPureSignal; +end; + +procedure TRadioController.PSSetFeedbackPath(En: Boolean); +// Единая точка вкл/выкл feedback-тракта: сеть (DDC0/1 на TX) + движок (pscc). +// Намеренно БЕЗ раннего выхода при том же значении: движок сбрасывает armed +// при Close (ChangeSampleRate), сеть гейтится внутри — повторный вызов с тем +// же значением дёшев и восстанавливает рассинхрон. +begin + FPSFeedbackOn := En; + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetPureSignalArmed(En); + if Assigned(FNetwork) then + begin + FNetwork.SetPureSignal(En); + // Включение/выключение посреди передачи — сразу перестроить DDC. + if FTransmitting then FNetwork.SetPureSignalTX(En); + end; +end; + +procedure TRadioController.SetPureSignal(On_: Boolean); +// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение — +// через запрос сброса: state machine выполнит reset коррекции и погасит тракт. +begin + if On_ = FPSEnabled then Exit; + if On_ and not PSAvailable then Exit; + FPSEnabled := On_; + if On_ then + begin + ApplyPSSettingsToDSP; // параметры calcc до старта калибровки + FPSResetReq := False; + end + else + FPSResetReq := True; + Changed(rfPureSignal); +end; + +procedure TRadioController.PSSingleCal; +// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается. +begin + if not PSAvailable then Exit; + if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена + ApplyPSSettingsToDSP; + FPSEnabled := False; + FPSSingleCal := True; + Changed(rfPureSignal); +end; + +procedure TRadioController.PSReset; +begin + FPSEnabled := False; + FPSSingleCal := False; + FPSResetReq := True; + Changed(rfPureSignal); +end; + +procedure TRadioController.ApplyPSSettingsToDSP; +var + Ints, Spi: Integer; +begin + if not (FWDSPReady and Assigned(FDSPEngine)) then Exit; + case FTXSettings.PSIntsIdx of + 1: begin Ints := 8; Spi := 512; end; + 2: begin Ints := 4; Spi := 1024; end; + else begin Ints := 16; Spi := 256; end; + end; + FDSPEngine.PSSetParams(FTXSettings.PSMoxDelay, FTXSettings.PSLoopDelay, + FTXSettings.PSTXDelayNs, FTXSettings.PSRelaxPtol, + FTXSettings.PSPin, FTXSettings.PSMap, + FTXSettings.PSStabilize, Ints, Spi, + FTXSettings.PSHWPeak); +end; + +function TRadioController.PSFeedbackLevel: Integer; +begin + Result := FPSInfo[4]; +end; + +function TRadioController.PSCalCount: Integer; +begin + Result := FPSInfo[5]; +end; + +function TRadioController.PSCorrecting: Boolean; +begin + Result := FPSInfo[14] = 1; +end; + +function TRadioController.PSMaxTXVal: Double; +begin + Result := FPSMaxTX; +end; + +function TRadioController.PSSingleCalActive: Boolean; +begin + Result := FPSSingleCal + or (FPSCmdState in [PS_ST_TURNON_SINGLE, PS_ST_SINGLECAL]); +end; + +procedure TRadioController.PureSignalTick; +// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis +// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы) +// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback. +var + NewAtt: Integer; + ddB: Double; + CalAttemptsChanged: Boolean; +begin + if not (FWDSPReady and Assigned(FDSPEngine)) then Exit; + if not PSAvailable then + begin + // Устройство сменилось/отключилось: гарантированно всё погасить. + if FPSFeedbackOn then PSSetFeedbackPath(False); + FPSCmdState := PS_ST_OFF; + Exit; + end; + // Полный простой (PS ни разу не включался / выключен и сброшен) — не дёргаем + // WDSP каждые 100 мс впустую. + if (FPSCmdState = PS_ST_OFF) and not (FPSEnabled or FPSSingleCal + or FPSResetReq or FPSFeedbackOn) then Exit; + + // Опрос статуса WDSP calcc + Move(FPSInfo[0], FPSInfoPrev[0], SizeOf(FPSInfo)); + FDSPEngine.PSGetInfo(FPSInfo); + FPSMaxTX := FDSPEngine.PSGetMaxTX; + + // --- Командная state machine --- + case FPSCmdState of + PS_ST_OFF: + begin + FDSPEngine.PSControl(1, 0, 0, 0); + PSSetFeedbackPath(False); + FPSResetReq := False; + if FPSEnabled then + FPSCmdState := PS_ST_TURNON_AUTO + else if FPSSingleCal then + FPSCmdState := PS_ST_TURNON_SINGLE; + end; + PS_ST_TURNON_AUTO: + begin + FDSPEngine.PSControl(1, 0, 1, 0); + PSSetFeedbackPath(True); + FPSCmdState := PS_ST_AUTOCAL; + end; + PS_ST_AUTOCAL: + begin + PSSetFeedbackPath(True); // ре-синк (движок мог переоткрыться) + if FPSResetReq or (not FPSEnabled) then + FPSCmdState := PS_ST_TURNOFF + else if FPSSingleCal then + FPSCmdState := PS_ST_TURNON_SINGLE; + end; + PS_ST_TURNON_SINGLE: + begin + FDSPEngine.PSControl(1, 1, 0, 0); + PSSetFeedbackPath(True); + FPSCmdState := PS_ST_SINGLECAL; + end; + PS_ST_SINGLECAL: + begin + PSSetFeedbackPath(True); // ре-синк (движок мог переоткрыться) + FPSSingleCal := False; + if FPSResetReq then + FPSCmdState := PS_ST_TURNOFF + else if FPSEnabled then + FPSCmdState := PS_ST_TURNON_AUTO + else if PSCorrecting then + FPSCmdState := PS_ST_STAYON; + end; + PS_ST_STAYON: + begin + // Коррекция применяется, калибровка остановлена, feedback не нужен. + PSSetFeedbackPath(False); + if FPSResetReq then + FPSCmdState := PS_ST_TURNOFF + else if FPSEnabled then + FPSCmdState := PS_ST_TURNON_AUTO + else if FPSSingleCal then + FPSCmdState := PS_ST_TURNON_SINGLE; + end; + PS_ST_TURNOFF: + begin + // Reset исполняется внутри pscc — держим feedback-тракт, пока calcc + // не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF). + FDSPEngine.PSControl(1, 0, 0, 0); + PSSetFeedbackPath(True); + FPSResetReq := False; + if FPSEnabled then + FPSCmdState := PS_ST_TURNON_AUTO + else if FPSSingleCal then + FPSCmdState := PS_ST_TURNON_SINGLE + else if (not PSCorrecting) and (FPSInfo[15] = 0) then // 0 = LRESET + FPSCmdState := PS_ST_OFF; + end; + end; + + // --- Auto-attenuate (Thetis timer2code) --- + // По завершении очередной попытки калибровки: если feedback слишком горячий + // (>181) или слабый при ненулевом атт. (<=128), подстроить AttOnTX так, + // чтобы уровень лёг к оптимуму 152. + case FPSAAState of + 0: begin + CalAttemptsChanged := FPSInfo[5] <> FPSInfoPrev[5]; + if FTXSettings.PSAutoAtten and CalAttemptsChanged + and ((FPSInfo[4] > 181) or + ((FPSInfo[4] <= 128) and (FTXSettings.AttOnTX > 0))) then + begin + FPSAAState := 1; + if (FPSInfo[4] > 0) and (FPSInfo[4] <= 256) then + begin + ddB := 20.0 * Log10(FPSInfo[4] / 152.293); + if ddB < -100.0 then ddB := -100.0; + if ddB > +100.0 then ddB := +100.0; + end + else + ddB := 31.1; + FPSAADeltaDB := Round(ddB); + FPSAASaveAuto := Ord(FPSCmdState = PS_ST_AUTOCAL); + FPSAASaveSingle := Ord(FPSCmdState = PS_ST_SINGLECAL); + FDSPEngine.PSControl(1, 0, 0, 0); // всё стоп + reset на время смены + end; + end; + 1: begin + FPSAAState := 2; + NewAtt := EnsureRange(FTXSettings.AttOnTX + FPSAADeltaDB, 0, 31); + if NewAtt <> FTXSettings.AttOnTX then + begin + FTXSettings.AttOnTX := NewAtt; + SendDUCSpecificFromSettings; // применить атт. немедленно + Changed(rfPureSignal); // UI: показать новый ATT + end; + end; + 2: begin + FPSAAState := 0; + FDSPEngine.PSControl(0, FPSAASaveSingle, FPSAASaveAuto, 0); + end; + end; + + // UI-статус (feedback-лампа/счётчик) обновляем при изменении info + if not CompareMem(@FPSInfo[0], @FPSInfoPrev[0], SizeOf(FPSInfo)) then + Changed(rfPureSignal); +end; + +procedure TRadioController.SetTwoTone(On_: Boolean); +// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный +// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию). +begin + if On_ = FTwoTone then Exit; + if On_ and (ActiveTXMode = MODE_DMR) then Exit; + if On_ and FTuning then Exit; // не поверх TUN-тона + // Safety: те же блокировки, что SetMOX/SetTune. + if On_ and FDevConnected and (FCurrentXvtr < 0) + and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then Exit; + if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) + and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; + if On_ then + begin + // Генератор ДО запуска TX-тракта — первые блоки уже с тонами. + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2); + FTwoTone := True; + if not FTransmitting then SetMOX(True); + end + else + begin + FTwoTone := False; + if FTransmitting then SetMOX(False); + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetTXTwoTone(False, 0, 0); + end; + Changed(rfTwoTone); +end; + procedure TRadioController.SetFMStep(Idx: Integer); begin if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4 @@ -4441,6 +4797,25 @@ begin if FWDSPReady then FDSPEngine.SetTXTone(False, 0, 0); Changed(rfTuning); // UI: TUN-кнопка off end; + if FTwoTone then + begin + FTwoTone := False; + if FWDSPReady then FDSPEngine.SetTXTwoTone(False, 0, 0); + Changed(rfTwoTone); // UI: 2TON-кнопка off + end; + // PureSignal: погасить калибровку и feedback-тракт (без сброса коррекции — + // сеть уже останавливается; state machine стартует заново при след. Run). + if FPSEnabled or FPSSingleCal or FPSFeedbackOn then + begin + FPSEnabled := False; + FPSSingleCal := False; + FPSResetReq := False; + FPSCmdState := PS_ST_OFF; + FPSAAState := 0; + PSSetFeedbackPath(False); + if FWDSPReady then FDSPEngine.PSControl(1, 0, 0, 0); + Changed(rfPureSignal); + end; if FWDSPReady then FDSPEngine.SetTXRun(False); Changed(rfTransmitting); // MOX-кнопка off end; diff --git a/Settings.pas b/Settings.pas index 6de595a..a56e89e 100644 --- a/Settings.pas +++ b/Settings.pas @@ -209,6 +209,19 @@ type TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0 TXSpecRange: Double; // dB range, e.g. 80 TXSpecGridStep: Double; // grid step in dB, e.g. 10 + // PureSignal (WDSP calcc; дефолты = Thetis PSForm, протокол 2) + PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2) + PSLoopDelay: Double; // с — пауза между итерациями калибровки (0) + PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150) + PSRelaxPtol: Boolean; // ослабленный допуск фазы (ptol 0.4 вместо 0.8) + PSAutoAtten: Boolean; // авто-подстройка AttOnTX по уровню feedback + PSPin: Boolean; // pin mode (True) + PSMap: Boolean; // map mode (True) + PSStabilize: Boolean; // stabilize (False) + PSIntsIdx: Integer; // 0=16/256, 1=8/512, 2=4/1024 + PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn) + PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700) + PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900) end; // Web-сервер — глобальные настройки (не привязаны к устройству, секция "web"). @@ -1170,6 +1183,19 @@ begin T.TXSpecRefLevel := 0.0; T.TXSpecRange := 80.0; T.TXSpecGridStep := 10.0; + // PureSignal — дефолты Thetis PSForm (протокол 2) + T.PSMoxDelay := 0.2; + T.PSLoopDelay := 0.0; + T.PSTXDelayNs := 150; + T.PSRelaxPtol := False; + T.PSAutoAtten := True; + T.PSPin := True; + T.PSMap := True; + T.PSStabilize := False; + T.PSIntsIdx := 0; // 16 ints / 256 spi + T.PSHWPeak := 0.2899; + T.PSTTFreq1 := 700; + T.PSTTFreq2 := 1900; end; function TSettingsManager.LoadTX(const MAC: array of Byte; @@ -1236,6 +1262,19 @@ begin T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel); T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange); T.TXSpecGridStep := JD(O,'tx_spec_grid_step', T.TXSpecGridStep); + // PureSignal + T.PSMoxDelay := JD(O,'ps_mox_delay', T.PSMoxDelay); + T.PSLoopDelay := JD(O,'ps_loop_delay', T.PSLoopDelay); + T.PSTXDelayNs := JI(O,'ps_tx_delay_ns', T.PSTXDelayNs); + T.PSRelaxPtol := JB(O,'ps_relax_ptol', T.PSRelaxPtol); + T.PSAutoAtten := JB(O,'ps_auto_atten', T.PSAutoAtten); + T.PSPin := JB(O,'ps_pin', T.PSPin); + T.PSMap := JB(O,'ps_map', T.PSMap); + T.PSStabilize := JB(O,'ps_stabilize', T.PSStabilize); + T.PSIntsIdx := EnsureRange(JI(O,'ps_ints_idx', T.PSIntsIdx), 0, 2); + T.PSHWPeak := JD(O,'ps_hw_peak', T.PSHWPeak); + T.PSTTFreq1 := JI(O,'ps_tt_freq1', T.PSTTFreq1); + T.PSTTFreq2 := JI(O,'ps_tt_freq2', T.PSTTFreq2); end; procedure TSettingsManager.SaveTX(const MAC: array of Byte; @@ -1294,6 +1333,19 @@ begin JW(O,'tx_spec_ref_level', T.TXSpecRefLevel); JW(O,'tx_spec_range', T.TXSpecRange); JW(O,'tx_spec_grid_step', T.TXSpecGridStep); + // PureSignal + JW(O,'ps_mox_delay', T.PSMoxDelay); + JW(O,'ps_loop_delay', T.PSLoopDelay); + JW(O,'ps_tx_delay_ns', T.PSTXDelayNs); + JW(O,'ps_relax_ptol', T.PSRelaxPtol); + JW(O,'ps_auto_atten', T.PSAutoAtten); + JW(O,'ps_pin', T.PSPin); + JW(O,'ps_map', T.PSMap); + JW(O,'ps_stabilize', T.PSStabilize); + JW(O,'ps_ints_idx', T.PSIntsIdx); + JW(O,'ps_hw_peak', T.PSHWPeak); + JW(O,'ps_tt_freq1', T.PSTTFreq1); + JW(O,'ps_tt_freq2', T.PSTTFreq2); end; class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings); diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 6a20289..eea8d62 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -375,6 +375,31 @@ type // PostGen стоит ПОСЛЕ ALC — голос из микрофона складывался бы с тоном // поверх полной шкалы (тон ~0.98) → клип 24-бит → сплэттер на спектре. FTXToneOn: Boolean; + // 2TON активен (PostGen two-tone 700/1900 Гц для IMD/PureSignal-теста): + // mic-вход глушится так же, как при TUN. + FTXTTOn: Boolean; + + // ---- PureSignal ---- + // FPSArmed: PS включён контроллером — PushPSFeedback подаёт feedback в + // pscc (WDSP calcc). Пишется из UI-потока, читается в сетевом. + FPSArmed: Boolean; + // Деинтерливленные буферы feedback-пакета: interleaved I/Q пары каждого + // потока (≤119 пар на поток при 238 парах в пакете; 256 пар с запасом). + // Используются только сетевым потоком. + FPSTxBuf: array[0..511] of Double; // DDC1 = TX DAC (чистый эталон) + FPSRxBuf: array[0..511] of Double; // DDC0 = RX feedback (после PA) + // Кэш PS-параметров: применяются в ApplyTXChainSettings (т.е. и после + // каждого пересоздания TXA-канала в Open/ChangeSampleRate). + FPSMoxDelay: Double; + FPSLoopDelay: Double; + FPSTXDelayNs: Integer; + FPSRelaxPtol: Boolean; + FPSPin: Boolean; + FPSMap: Boolean; + FPSStabilize: Boolean; + FPSInts: Integer; + FPSSpi: Integer; + FPSHWPeak: Double; // Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении. // Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS. @@ -501,6 +526,7 @@ type procedure ProcessTXBlock; // вызывается из TTXDSPThread procedure FeedTXDisplaySample(const I, Q: Double); procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP + procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID @@ -606,6 +632,29 @@ type // TUN: WDSP TXA PostGen — гонит чистый тон в DSP-цепь после всей // обработки. Mag — амплитуда в [0..1], Freq — Hz (audio). procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double); + // 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal). + // Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды + // по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr). + procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double); + + // ---- PureSignal (WDSP calcc на TXA_CHAN) ---- + // Армирование подачи feedback в pscc (кнопка PS). Сама калибровка + // управляется PSControl (state machine в контроллере). + procedure SetPureSignalArmed(Armed: Boolean); + // Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 = + // RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО + // потока (как Thetis/pihpsdr — pscc потокобезопасен относительно + // fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока). + procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer; + TotalPairs: Integer); + // Тонкие врапперы WDSP PS API (все на TXA_CHAN, guard FInitialized): + procedure PSControl(Reset, Mancal, Automode, Turnon: Integer); + procedure PSSetMox(Mox: Boolean); + procedure PSGetInfo(var Info: array of Integer); // 16 int + function PSGetMaxTX: Double; + procedure PSSetParams(MoxDelay, LoopDelay: Double; TXDelayNs: Integer; + RelaxPtol: Boolean; Pin, Map, Stabilize: Boolean; + Ints, Spi: Integer; HWPeak: Double); // Расширенное TX управление (применяются мгновенно если канал открыт) procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer); @@ -1197,7 +1246,20 @@ begin FTXMicTail := 0; FTXActive := False; FTXToneOn := False; + FTXTTOn := False; FKeepRXDuringTX := False; + // PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2) + FPSArmed := False; + FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных + FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки + FPSTXDelayNs := 150; // нс — компенсация задержки TX-тракта + FPSRelaxPtol := False; + FPSPin := True; + FPSMap := True; + FPSStabilize := False; + FPSInts := 16; + FPSSpi := 256; + FPSHWPeak := 0.2899; // протокол 2 (Saturn: 0.6121) FMonitorRXDuringTX := False; FPostTXMuteUntil := 0; FActiveDisplayID := RX_DISP_ID; @@ -1661,6 +1723,10 @@ var begin if not FInitialized then Exit; + // PureSignal: снять подачу feedback ДО закрытия TXA — pscc зовётся из + // сетевого потока напрямую и не должен попасть в закрываемый канал. + FPSArmed := False; + // Останавливаем DSP поток перед закрытием WDSP каналов if Assigned(FDSPThread) then begin @@ -3139,6 +3205,35 @@ begin SetTXAPostGenRun(TXA_CHAN, 0); end; +procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double); +// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal. +// Амплитуды 0.49+0.49 — сумма чуть ниже полной шкалы (как Thetis/pihpsdr). +var + F1, F2: Double; +begin + if not FInitialized then Exit; + FTXTTOn := Enabled; // мьют mic-входа (см. ProcessTXBlock) + if Enabled then + begin + // Для нижней боковой тона должны лежать НИЖЕ несущей. + if (FMode = MODE_LSB) or (FMode = MODE_CWL) or (FMode = MODE_DIGL) then + begin + F1 := -Abs(Freq1); + F2 := -Abs(Freq2); + end + else + begin + F1 := Abs(Freq1); + F2 := Abs(Freq2); + end; + SetTXAPostGenMode(TXA_CHAN, 1); // 1 = two-tone + SetTXAPostGenTTFreq(TXA_CHAN, F1, F2); + SetTXAPostGenTTMag(TXA_CHAN, 0.49, 0.49); + SetTXAPostGenRun(TXA_CHAN, 1); + end else + SetTXAPostGenRun(TXA_CHAN, 0); +end; + procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean); begin // Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID @@ -3179,9 +3274,9 @@ begin tail := (tail + 1) and (TX_MIC_RING - 1); end; FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread) - // TUN: ринг потребляем (иначе поток зациклится на «есть полный блок»), + // TUN/2TON: ринг потребляем (иначе поток зациклится на «есть полный блок»), // но в TXA подаём тишину — тон PostGen должен идти без голоса поверх. - if FTXToneOn then + if FTXToneOn or FTXTTOn then FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); end else begin @@ -3335,6 +3430,118 @@ begin // CTCSS SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq); SetTXACTCSSRun(TXA_CHAN, Ord(FTXCTCSSOn)); + + // PureSignal (calcc живёт внутри TXA-канала — после пересоздания канала + // все его параметры надо прокинуть заново) + ApplyPSParams; +end; + +procedure TWDSPEngine.ApplyPSParams; +begin + if not FInitialized then Exit; + // Feedback приходит с DDC0/1 @ 192 кС/с (фиксированный PS-rate протокола 2) + SetPSFeedbackRate(TXA_CHAN, TX_SAMPLE_RATE); + SetPSMoxDelay(TXA_CHAN, FPSMoxDelay); + SetPSLoopDelay(TXA_CHAN, FPSLoopDelay); + SetPSTXDelay(TXA_CHAN, FPSTXDelayNs * 1.0e-9); + if FPSRelaxPtol then + SetPSPtol(TXA_CHAN, 0.400) + else + SetPSPtol(TXA_CHAN, 0.800); + SetPSPinMode(TXA_CHAN, Ord(FPSPin)); + SetPSMapMode(TXA_CHAN, Ord(FPSMap)); + SetPSStabilize(TXA_CHAN, Ord(FPSStabilize)); + SetPSIntsAndSpi(TXA_CHAN, FPSInts, FPSSpi); + SetPSHWPeak(TXA_CHAN, FPSHWPeak); +end; + +procedure TWDSPEngine.PSSetParams(MoxDelay, LoopDelay: Double; TXDelayNs: Integer; + RelaxPtol: Boolean; Pin, Map, Stabilize: Boolean; Ints, Spi: Integer; + HWPeak: Double); +begin + FPSMoxDelay := MoxDelay; + FPSLoopDelay := LoopDelay; + FPSTXDelayNs := TXDelayNs; + FPSRelaxPtol := RelaxPtol; + FPSPin := Pin; + FPSMap := Map; + FPSStabilize := Stabilize; + if Ints > 0 then FPSInts := Ints; + if Spi > 0 then FPSSpi := Spi; + if HWPeak > 0 then FPSHWPeak := HWPeak; + ApplyPSParams; +end; + +procedure TWDSPEngine.SetPureSignalArmed(Armed: Boolean); +begin + FPSArmed := Armed; +end; + +procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte; + DataOffset: Integer; TotalPairs: Integer); +// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... → +// деинтерливим и кормим WDSP pscc (как pihpsdr process_ps_iq_data): +// DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон). +var + i, n, Pos: Integer; + IR, QR: LongInt; + P: PByte; +const + SCALE = 1.0 / 8388608.0; +begin + if not (FPSArmed and FInitialized) then Exit; + n := TotalPairs div 2; // пар на каждый поток + if n > 256 then n := 256; // ёмкость FPSTx/RxBuf + if n <= 0 then Exit; + if (DataOffset + n * 2 * 6) > Length(Buf) then Exit; + + P := @Buf[DataOffset]; + for i := 0 to n * 2 - 1 do + begin + IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]); + QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]); + Inc(P, 6); + if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000); + if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000); + Pos := i shr 1; // индекс пары в потоке + if (i and 1) = 0 then + begin + FPSRxBuf[Pos * 2] := IR * SCALE; // чётные = DDC0 (RX feedback) + FPSRxBuf[Pos * 2 + 1] := QR * SCALE; + end + else + begin + FPSTxBuf[Pos * 2] := IR * SCALE; // нечётные = DDC1 (TX DAC) + FPSTxBuf[Pos * 2 + 1] := QR * SCALE; + end; + end; + + pscc(TXA_CHAN, n, @FPSTxBuf[0], @FPSRxBuf[0]); +end; + +procedure TWDSPEngine.PSControl(Reset, Mancal, Automode, Turnon: Integer); +begin + if not FInitialized then Exit; + SetPSControl(TXA_CHAN, Reset, Mancal, Automode, Turnon); +end; + +procedure TWDSPEngine.PSSetMox(Mox: Boolean); +begin + if not FInitialized then Exit; + SetPSMox(TXA_CHAN, Ord(Mox)); +end; + +procedure TWDSPEngine.PSGetInfo(var Info: array of Integer); +begin + if (not FInitialized) or (Length(Info) < 16) then Exit; + GetPSInfo(TXA_CHAN, @Info[0]); +end; + +function TWDSPEngine.PSGetMaxTX: Double; +begin + Result := 0.0; + if not FInitialized then Exit; + GetPSMaxTX(TXA_CHAN, @Result); end; procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer); diff --git a/ewsdr.lpi b/ewsdr.lpi index 5e963e2..82176c3 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -125,6 +125,7 @@ +