feat(puresignal): PureSignal-предыскажение для openHPSDR P2 + кнопки PS/2TON

Протокол (по 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 11:55:26 +03:00
co-authored by Claude Fable 5
parent a84de32294
commit 3acd569da5
8 changed files with 1284 additions and 14 deletions
+377 -2
View File
@@ -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;