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
+447
View File
@@ -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.