diff --git a/AlertOverlay.pas b/AlertOverlay.pas index afcfe87..1103131 100644 --- a/AlertOverlay.pas +++ b/AlertOverlay.pas @@ -1,7 +1,15 @@ unit AlertOverlay; { - Reusable overlays for spectrum-like bitmap surfaces. + Общая плашка-алерт для CPU- и GL-рендеров спектра. + + Единственный источник внешнего вида: BuildAlertImage строит готовый + RGBA-образ (straight alpha, попиксельная) ОДИН раз — Canvas используется + только здесь, на собственном маленьком битмапе. Дальше каждый рендер + использует образ по-своему без пер-кадрового жора CPU: + - GL: загружает как текстуру (один раз) и рисует квадом; + - CPU: блендит кэшированный образ в raw-кадр (BlendAlertImage, + ~10 тыс. пикселей — без Canvas и синков режима доступа Qt6). } {$IFDEF FPC} @@ -11,105 +19,174 @@ unit AlertOverlay; interface uses - Graphics, Types, Math; + Classes, Graphics, Types, Math; -procedure DrawAlertOverlay(Target: TBitmap; C: TCanvas; W, H: Integer; - const Title, Detail: string); +const + ALERT_PAD = 12; // отступ плашки от правого/верхнего края спектра + +type + TAlertImage = record + W, H: Integer; + RGBA: array of Byte; // W*H*4, порядок R,G,B,A; straight alpha + end; + +// Построить образ плашки (полупрозрачный красный фон, рамка, акцент-полоса, +// заголовок + деталь). Вызывать один раз и кэшировать результат. +procedure BuildAlertImage(const Title, Detail: string; out Img: TAlertImage); + +// Смешать образ в raw-битмап кадра (32bpp, BGRA / ARGB на Darwin) по +// straight-alpha. (X,Y) — левый верхний угол плашки; клиппинг внутри. +procedure BlendAlertImage(const Img: TAlertImage; Base: PByte; + BPL, RawW, RawH, X, Y: Integer); implementation -procedure BlendRect(Target: TBitmap; const Rct: TRect; R, G, B, Alpha: Byte); +procedure BuildAlertImage(const Title, Detail: string; out Img: TAlertImage); +const + INNER_X = 12; ACCENT_W = 4; BOX_H = 42; var - Y, X, InvA: Integer; - Row: PByte; - RR: TRect; -begin - if (Target = nil) or (Alpha = 0) then Exit; - RR := Rct; - if RR.Left < 0 then RR.Left := 0; - if RR.Top < 0 then RR.Top := 0; - if RR.Right > Target.Width then RR.Right := Target.Width; - if RR.Bottom > Target.Height then RR.Bottom := Target.Height; - if (RR.Right <= RR.Left) or (RR.Bottom <= RR.Top) then Exit; + B: TBitmap; + TitleW, DetailW, BoxW: Integer; - InvA := 255 - Alpha; - Target.BeginUpdate(False); - try - for Y := RR.Top to RR.Bottom - 1 do + procedure FillBuf(X1, Y1, X2, Y2: Integer; R, G, Bl, A: Byte); // [X1..X2) + var X, Y, I: Integer; + begin + for Y := Max(0, Y1) to Min(BOX_H, Y2) - 1 do begin - Row := PByte(Target.ScanLine[Y]); - if Row = nil then Continue; - Inc(Row, RR.Left * 4); - for X := RR.Left to RR.Right - 1 do + I := (Y * BoxW + Max(0, X1)) * 4; + for X := Max(0, X1) to Min(BoxW, X2) - 1 do begin -{$IFDEF DARWIN} - Row[1] := Byte((Alpha * R + InvA * Row[1]) div 255); - Row[2] := Byte((Alpha * G + InvA * Row[2]) div 255); - Row[3] := Byte((Alpha * B + InvA * Row[3]) div 255); -{$ELSE} - Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255); - Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255); - Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255); -{$ENDIF} - Inc(Row, 4); + Img.RGBA[I] := R; Img.RGBA[I+1] := G; + Img.RGBA[I+2] := Bl; Img.RGBA[I+3] := A; + Inc(I, 4); end; end; + end; + + procedure BlendText(TxtB: TBitmap); + // Композит текстового слоя (белым на чёрном) в образ: покрытие = max(R,G,B), + // цвет по строке (заголовок/деталь), сложение по straight-alpha. + var + X, Y, I, MaxC: Integer; + Src: PByte; + CR, CG, CB: Byte; + AT, AB, AO: Double; + begin + TxtB.BeginUpdate(False); + try + for Y := 0 to BOX_H - 1 do + begin + Src := PByte(TxtB.ScanLine[Y]); + I := Y * BoxW * 4; + for X := 0 to BoxW - 1 do + begin +{$IFDEF DARWIN} + MaxC := Max(Src[1], Max(Src[2], Src[3])); +{$ELSE} + MaxC := Max(Src[0], Max(Src[1], Src[2])); +{$ENDIF} + if MaxC > 0 then + begin + if Y < 24 then begin CR := $FF; CG := $88; CB := $88; end // $008888FF + else begin CR := $E8; CG := $C8; CB := $C8; end; // $00C8C8E8 + AT := MaxC / 255.0; + AB := Img.RGBA[I+3] / 255.0 * (1.0 - AT); + AO := AT + AB; + Img.RGBA[I] := Round((CR * AT + Img.RGBA[I] * AB) / AO); + Img.RGBA[I+1] := Round((CG * AT + Img.RGBA[I+1] * AB) / AO); + Img.RGBA[I+2] := Round((CB * AT + Img.RGBA[I+2] * AB) / AO); + Img.RGBA[I+3] := Round(AO * 255.0); + end; + Inc(Src, 4); + Inc(I, 4); + end; + end; + finally + TxtB.EndUpdate(False); + end; + end; + +begin + Img.W := 0; Img.H := 0; Img.RGBA := nil; + B := TBitmap.Create; + try + B.PixelFormat := pf32bit; + B.SetSize(8, 8); + B.Canvas.Font.Name := 'Courier New'; + B.Canvas.Font.Style := [fsBold]; + B.Canvas.Font.Size := 9; + TitleW := B.Canvas.TextWidth(Title); + B.Canvas.Font.Style := []; + B.Canvas.Font.Size := 7; + DetailW := B.Canvas.TextWidth(Detail); + BoxW := Max(TitleW, DetailW) + INNER_X * 2 + ACCENT_W + 8; + + SetLength(Img.RGBA, BoxW * BOX_H * 4); + Img.W := BoxW; + Img.H := BOX_H; + FillBuf(0, 0, BoxW, BOX_H, $A8, $10, $16, 118); // фон + FillBuf(0, 0, BoxW, 1, $C8, $38, $38, 255); // рамка + FillBuf(0, BOX_H - 1, BoxW, BOX_H, $C8, $38, $38, 255); + FillBuf(0, 0, 1, BOX_H, $C8, $38, $38, 255); + FillBuf(BoxW - 1, 0, BoxW, BOX_H, $C8, $38, $38, 255); + FillBuf(7, 7, 7 + ACCENT_W, BOX_H - 7, $F0, $28, $28, 255); // акцент + + B.SetSize(BoxW, BOX_H); + B.Canvas.Brush.Color := clBlack; + B.Canvas.Brush.Style := bsSolid; + B.Canvas.FillRect(Rect(0, 0, BoxW, BOX_H)); + B.Canvas.Brush.Style := bsClear; + B.Canvas.Font.Name := 'Courier New'; + B.Canvas.Font.Color := clWhite; + B.Canvas.Font.Style := [fsBold]; + B.Canvas.Font.Size := 9; + B.Canvas.TextOut(INNER_X + ACCENT_W + 8, 7, Title); + B.Canvas.Font.Style := []; + B.Canvas.Font.Size := 7; + B.Canvas.TextOut(INNER_X + ACCENT_W + 8, 25, Detail); + BlendText(B); finally - Target.EndUpdate(False); + B.Free; end; end; -procedure DrawAlertOverlay(Target: TBitmap; C: TCanvas; W, H: Integer; - const Title, Detail: string); -const - PAD = 12; - INNER_X = 12; - ACCENT_W = 4; +procedure BlendAlertImage(const Img: TAlertImage; Base: PByte; + BPL, RawW, RawH, X, Y: Integer); var - R, AccentR: TRect; - TW, DW, BoxW, BoxH: Integer; + SX, SY, DX, DY, I, A, InvA: Integer; + P: PByte; begin - if (Target = nil) or (C = nil) then Exit; - if (W < 220) or (H < 60) then Exit; - - C.Font.Name := 'Courier New'; - C.Font.Style := [fsBold]; - C.Font.Size := 9; - TW := C.TextWidth(Title); - C.Font.Style := []; - C.Font.Size := 7; - DW := C.TextWidth(Detail); - - BoxW := Max(TW, DW) + INNER_X * 2 + ACCENT_W + 8; - BoxH := 42; - R := Rect(W - PAD - BoxW, PAD, W - PAD, PAD + BoxH); - - BlendRect(Target, R, $A8, $10, $16, 118); - BlendRect(Target, Rect(R.Left, R.Top, R.Right, R.Top + 1), $FF, $68, $68, 80); - - C.Brush.Style := bsClear; - C.Pen.Style := psSolid; - C.Pen.Width := 1; - C.Pen.Color := TColor($003838C8); - C.Rectangle(R); - - AccentR := Rect(R.Left + 7, R.Top + 7, R.Left + 7 + ACCENT_W, R.Bottom - 7); - C.Brush.Style := bsSolid; - C.Brush.Color := TColor($002828F0); - C.Pen.Style := psClear; - C.FillRect(AccentR); - - C.Brush.Style := bsClear; - C.Font.Name := 'Courier New'; - C.Font.Style := [fsBold]; - C.Font.Size := 9; - C.Font.Color := TColor($008888FF); - C.TextOut(R.Left + INNER_X + ACCENT_W + 8, R.Top + 7, Title); - - C.Font.Style := []; - C.Font.Size := 7; - C.Font.Color := TColor($00C8C8E8); - C.TextOut(R.Left + INNER_X + ACCENT_W + 8, R.Top + 25, Detail); + if (Img.W <= 0) or (Base = nil) then Exit; + for SY := 0 to Img.H - 1 do + begin + DY := Y + SY; + if (DY < 0) or (DY >= RawH) then Continue; + I := SY * Img.W * 4; + for SX := 0 to Img.W - 1 do + begin + DX := X + SX; + A := Img.RGBA[I + 3]; + if (DX >= 0) and (DX < RawW) and (A > 0) then + begin + InvA := 255 - A; + P := Base + DY * BPL + DX * 4; +{$IFDEF DARWIN} + // ARGB: byte0=A, byte1=R, byte2=G, byte3=B + P[1] := Byte((Img.RGBA[I] * A + P[1] * InvA) div 255); + P[2] := Byte((Img.RGBA[I+1] * A + P[2] * InvA) div 255); + P[3] := Byte((Img.RGBA[I+2] * A + P[3] * InvA) div 255); + P[0] := $FF; +{$ELSE} + // BGRA: byte0=B, byte1=G, byte2=R + P[0] := Byte((Img.RGBA[I+2] * A + P[0] * InvA) div 255); + P[1] := Byte((Img.RGBA[I+1] * A + P[1] * InvA) div 255); + P[2] := Byte((Img.RGBA[I] * A + P[2] * InvA) div 255); + P[3] := $FF; +{$ENDIF} + end; + Inc(I, 4); + end; + end; end; end. diff --git a/AndromedaPanel.pas b/AndromedaPanel.pas index f90382c..73066ad 100644 --- a/AndromedaPanel.pas +++ b/AndromedaPanel.pas @@ -24,12 +24,13 @@ unit AndromedaPanel; interface uses - SysUtils, RadioController, AndromedaMap; + SysUtils, RadioController, AndromedaMap, RadioModes; const AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT) AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра - MODE_COUNT_CYCLE = 8; // MODE_LSB..MODE_SAM (0..7) для перебора режимов + // Аппаратная панель намеренно перебирает только LSB..SAM. + MODE_COUNT_CYCLE = RadioModes.MODE_SAM - RadioModes.MODE_LSB + 1; type TAndromedaSend = procedure(const S: string) of object; diff --git a/CATAdapter.pas b/CATAdapter.pas index ca6ed52..7682c29 100644 --- a/CATAdapter.pas +++ b/CATAdapter.pas @@ -307,7 +307,7 @@ begin FSyncInt := V; FController.Invoke(@SyncMode); end; procedure TCATAdapter.SyncMode; begin - if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit; + if (FSyncInt < MODE_MIN) or (FSyncInt > MODE_MAX) then Exit; FController.SetMode(FSyncInt); end; diff --git a/CATEngine.pas b/CATEngine.pas index 7b9fc44..2a1ccbe 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -24,19 +24,14 @@ unit CATEngine; interface uses - Classes, SysUtils, Math, SyncObjs; + Classes, SysUtils, Math, SyncObjs, RadioModes; const CAT_ERROR = '?;'; CAT_EXEC_ERROR = 'E;'; CAT_ID_NUM = '019'; // TS-2000 compatible - // Mode mapping ewsdr → CAT (зеркало MODE_* из WDSPEngine) - MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2; - MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5; - MODE_AM = 6; MODE_SAM = 7; - MODE_DIGU = 8; MODE_DIGL = 9; MODE_WFM = 10; MODE_DMR = 11; - CAT_MODE_COUNT = 12; + CAT_MODE_COUNT = RadioModes.MODE_COUNT; // FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны // совпадать: контроллер хранит FFMRptDir в этих же кодах). @@ -392,6 +387,7 @@ begin MODE_FM: Result := 4; MODE_WFM: Result := 4; // treated as FM MODE_DMR: Result := 4; // DMR RF transport is narrow FM + MODE_FMRAW: Result := 4; // raw discriminator is still FM transport MODE_AM: Result := 5; MODE_SAM: Result := 5; // treated as AM MODE_DIGU: Result := 6; // Kenwood FSK @@ -1793,20 +1789,24 @@ end; function TCATEngine.ZZML: string; // ZZML: mode list (returns all mode names) -begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR '; end; +begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end; function TCATEngine.ZZMN(const s: string): string; // ZZMN: mode name — returns mode name for given mode number. -// Индекс режима теперь двузначный (0..10), но односимвольный запрос +// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос // продолжаем принимать — старые клиенты шлют одну цифру. var k: Integer; names: array[0..CAT_MODE_COUNT-1] of string; begin - names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL'; - names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM'; - names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM'; names[11]:='DMR'; + names[MODE_LSB]:='LSB'; names[MODE_USB]:='USB'; + names[MODE_DSB]:='DSB'; names[MODE_CWL]:='CWL'; + names[MODE_CWU]:='CWU'; names[MODE_FM]:='FM'; + names[MODE_AM]:='AM'; names[MODE_SAM]:='SAM'; + names[MODE_DIGU]:='DIGU'; names[MODE_DIGL]:='DIGL'; + names[MODE_WFM]:='WFM'; names[MODE_DMR]:='DMR'; + names[MODE_FMRAW]:='FM RAW'; if (Length(s) = 1) or (Length(s) = 2) then begin - k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1); + k := EnsureRange(StrToIntDef(s, MODE_MIN), MODE_MIN, MODE_MAX); Result := names[k]; end else Result := CAT_ERROR; end; @@ -1819,7 +1819,7 @@ begin end; function TCATEngine.ZZMV: string; -// ZZMV: mode value — returns current mode index (ewsdr 0-7) +// ZZMV: mode value — returns current ewsdr mode index (MODE_MIN..MODE_MAX) begin Result := Pad(SafeGetMode, 2); end; function TCATEngine.ZZMY: string; diff --git a/ChannelStore.pas b/ChannelStore.pas index 09c5db9..cd8dd61 100644 --- a/ChannelStore.pas +++ b/ChannelStore.pas @@ -8,11 +8,13 @@ unit ChannelStore; interface uses - SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils; + SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils, + RadioModes; const CHANNELS_FILE = 'channels.json'; - CHANNEL_MODE_MAX = 11; // LSB..DMR + CHANNEL_MODE_MIN = RadioModes.MODE_MIN; + CHANNEL_MODE_MAX = RadioModes.MODE_MAX; type TChannel = record @@ -20,7 +22,7 @@ type Name: string; RXFreq: Double; // Hz TXFreq: Double; // Hz - Mode: Integer; // 0=LSB ... 10=WFM 11=DMR + Mode: Integer; // RadioModes.MODE_* persisted index RptDir: Integer; // 0=none 1=minus 2=plus RptOffsetHz: Double; // Hz CTCSSOn: Boolean; @@ -59,7 +61,7 @@ begin Ch.Name := 'New Channel'; Ch.RXFreq := 145500000.0; Ch.TXFreq := 145500000.0; - Ch.Mode := 5; // FM + Ch.Mode := MODE_FM; Ch.RptDir := 0; Ch.RptOffsetHz := 600000.0; Ch.CTCSSOn := False; @@ -207,7 +209,8 @@ begin Ch.Name := JS(O, 'name', 'Channel'); Ch.RXFreq := JD(O, 'rx_freq', 145500000.0); Ch.TXFreq := JD(O, 'tx_freq', 145500000.0); - Ch.Mode := EnsureRange(JI(O, 'mode', 5), 0, CHANNEL_MODE_MAX); + Ch.Mode := EnsureRange(JI(O, 'mode', MODE_FM), + CHANNEL_MODE_MIN, CHANNEL_MODE_MAX); Ch.RptDir := EnsureRange(JI(O, 'rpt_dir', 0), 0, 2); Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0); Ch.CTCSSOn := JB(O, 'ctcss_on', False); diff --git a/ChannelsForm.pas b/ChannelsForm.pas index 2293564..5d84547 100644 --- a/ChannelsForm.pas +++ b/ChannelsForm.pas @@ -14,13 +14,13 @@ uses Forms, Controls, Graphics, StdCtrls, ExtCtrls, FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatSpinEdit, FlatFloatSpinEdit, FlatListBox, - AppTheme, ChannelStore; + AppTheme, ChannelStore, RadioModes; const // Копии из MainForm: нужны для выпадающих списков - CH_MODE_COUNT = 12; + CH_MODE_COUNT = RadioModes.MODE_COUNT; CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string = - ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); + ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FM RAW'); CH_CTCSS_COUNT = 38; CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = ( @@ -459,7 +459,7 @@ begin FEdName.Text := Ch.Name; FSpinRXFreq.Value := Ch.RXFreq / 1000000.0; FSpinTXFreq.Value := Ch.TXFreq / 1000000.0; - FCmbMode.ItemIndex := EnsureRange(Ch.Mode, 0, CH_MODE_COUNT - 1); + FCmbMode.ItemIndex := EnsureRange(Ch.Mode, MODE_MIN, MODE_MAX); StyleBtn(FBtnRptNone, Ch.RptDir = 0); StyleBtn(FBtnRptMinus, Ch.RptDir = 1); StyleBtn(FBtnRptPlus, Ch.RptDir = 2); @@ -487,7 +487,7 @@ begin Ch.Name := FEdName.Text; Ch.RXFreq := FSpinRXFreq.Value * 1000000.0; Ch.TXFreq := FSpinTXFreq.Value * 1000000.0; - Ch.Mode := EnsureRange(FCmbMode.ItemIndex, 0, CH_MODE_COUNT - 1); + Ch.Mode := EnsureRange(FCmbMode.ItemIndex, MODE_MIN, MODE_MAX); // RptDir берётся из кнопок (Active) if FBtnRptMinus.Active then Ch.RptDir := 1 else if FBtnRptPlus.Active then Ch.RptDir := 2 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..ea5de28 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -33,10 +33,11 @@ uses WebServer, WebAdapter, CATAdapter, SpectrumView, SpectrumViewOpengl, - PanZoomBar, PanafallPanel, PanDisplayPopup, FlatPopupMenu, + PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu, WidebandView, StatusBar, PlatformUtils, + RadioModes, WinFirewall, BoardUtils, WisdomBuilder, UISync, FlatEdit, FMRepeater, @@ -67,7 +68,6 @@ const WDSP_WISDOM_FILE = 'wdspWisdom00'; BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11) - MODE_COUNT = 12; // Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*) // перенесены в Settings.pas — нужны TRadioController. Здесь остаются только // UI-подписи кнопок (NAMES). @@ -83,6 +83,8 @@ const ('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300'); FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM'); FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k'); + FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string = + ('6.25k', '10k', '12.5k', '20k', '25k'); // CTCSS tones (38 standard tones, no None — toggle via CTCSS button) // CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController). @@ -105,8 +107,8 @@ const '160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m',''); MODE_NAMES: array[0..MODE_COUNT-1] of string = ( - 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); - MODE_ROWS = 3; // 4 колонки; последний ряд неполный + 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FMRAW'); + MODE_ROWS = 4; // 4 колонки; последний ряд неполный SMETER_WIDTH_RATIO = 0.32; SMETER_MARGIN = 3; @@ -299,6 +301,9 @@ type BtnFMSQ: TFlatButton; TrkFMSQ: TFlatSlider; LblFMSQ: TLabel; + PanelRawDev: TPanel; + TrkRawDev: TFlatSlider; + LblRawDev: TLabel; PanelFMCTCSS: TPanel; BtnFMCTCSS: TFlatButton; BtnFMCTCSSTone: TFlatButton; @@ -337,6 +342,9 @@ type TrkDrive: TFlatSlider; BtnMOX: TFlatButton; BtnTUN: TFlatButton; + BtnPS: TFlatButton; // PureSignal: ЛКМ = вкл/выкл, ПКМ = поповер настроек + Btn2Ton: TFlatButton; // двухтональник (IMD/проверка PS) + FPSPopup: TObject; // TPureSignalPopup (cast при использовании) PanelRXBlock: TPanel; BtnChannels: TFlatButton; @@ -460,9 +468,18 @@ 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 UpdateIMDMarkers; // 2TON: маркеры пиков/IMD3 на спектре procedure UpdateFilterButtons; procedure BtnFMSQClick(Sender: TObject); procedure TrkFMSQChange(Sender: TObject); + procedure TrkRawDevChange(Sender: TObject); procedure ApplyFMSquelch; procedure BtnFMCTCSSClick(Sender: TObject); procedure BtnFMCTCSSToneClick(Sender: TObject); @@ -920,7 +937,7 @@ begin FController.FVfoA := 14200000; FController.FVfoB := 7100000; FController.FActiveVfo := 0; - FController.FMode := 1; + FController.FMode := MODE_USB; FController.FFilter := 5; FController.FAGCMode := 1; // MEDIUM FController.FAGCTop := 90; // −90 dBm @@ -1366,9 +1383,12 @@ begin ApplySpecViewGridFromState; // сетка спектра под активный тракт RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт) UpdateTXControlAvailability; + UpdateIMDMarkers; // 2TON: маркеры IMD на TX-спектре end; // (DUC-очередь сбрасывает SetMOX) rfTuning: StyleButton(BtnTUN, FController.FTuning); + rfPureSignal, rfTwoTone: + UpdatePSButtons; // подсветка PS/2TON + обновление статусов поповера rfRunning: // Run-состояние: кнопка START/STOP, доступность MOX, запрет сна. // Текст offline-статуса контекстно-зависим (Disconnected / Connection @@ -1472,6 +1492,12 @@ begin for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter); SyncSpecViewFreq; end; + rfFMDeviation: + if (TrkRawDev <> nil) and (LblRawDev <> nil) then + begin + TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50; + LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]); + end; rfAGCMode: for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); rfVolume: @@ -1512,6 +1538,7 @@ begin // подсвечивал → визуал расходился с состоянием. if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex); UpdateRxMuteButton; // активность RX MUTE зависит от DUP + UpdateIMDMarkers; // IMD-маркеры 2TON живут только в DUP // На передаче переключаем источник пикселей и сбрасываем буферы, чтобы // старые TX/RX bin'ы не оставались на следующем кадре. if FController.FTransmitting then @@ -1538,7 +1565,7 @@ begin begin if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx); if FWebServer <> nil then FWebServer.FMStepIdx := FController.FFMStepIdx; - if (FController.FMode in [MODE_FM, MODE_DMR]) and + if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and (FSpecView <> nil) then FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]; end; @@ -1952,6 +1979,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', @@ -2210,6 +2245,35 @@ begin Inc(Y, 76); + // FM RAW discriminator deviation. Slider units are 50 Hz: fine enough for + // decoder calibration, while the full 0.5..10 kHz range remains usable. + PanelRawDev := TPanel.Create(Self); + PanelRawDev.Parent := PanelLeft; + PanelRawDev.SetBounds(0, Y, LEFT_W, 50); + PanelRawDev.BevelOuter := bvNone; + PanelRawDev.Visible := False; + MakeLbl(PanelRawDev, 'DEVIATION', 4, 2); + TrkRawDev := TFlatSlider.Create(Self); + TrkRawDev.Parent := PanelRawDev; + TrkRawDev.Left := 5; + TrkRawDev.Top := 22; + TrkRawDev.Width := LEFT_W - 68; + TrkRawDev.Height := 14; + TrkRawDev.Min := RAW_FM_DEV_MIN div 50; + TrkRawDev.Max := RAW_FM_DEV_MAX div 50; + TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50; + TrkRawDev.OnChange := TrkRawDevChange; + LblRawDev := TLabel.Create(Self); + LblRawDev.Parent := PanelRawDev; + LblRawDev.Left := LEFT_W - 61; + LblRawDev.Top := 20; + LblRawDev.Width := 57; + LblRawDev.Alignment := taRightJustify; + LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]); + LblRawDev.Font.Color := CLR_TEXT; + LblRawDev.Font.Name := 'Courier New'; + LblRawDev.Font.Size := 8; + // FM Squelch panel (visible only in FM mode, above CTCSS) PanelFMSQ := TPanel.Create(Self); PanelFMSQ.Parent := PanelLeft; @@ -2867,6 +2931,7 @@ begin DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons); DP(PanelBands); DP(PanelMode); DP(PanelFilter); if PanelFMSQ <> nil then DP(PanelFMSQ); + if PanelRawDev <> nil then DP(PanelRawDev); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMStep <> nil then DP(PanelFMStep); if PanelFMRpt <> nil then DP(PanelFMRpt); @@ -3242,7 +3307,7 @@ begin FSpecView.SpanHz := ViewSpan; FSpecView.Mode := FController.FMode; FSpecView.FilterBW := FController.FFilterBW; - if FController.FMode in [MODE_FM, MODE_DMR] then + if FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx] else FSpecView.FMGridStepHz := 0; @@ -3291,6 +3356,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; @@ -3527,13 +3595,59 @@ begin end; procedure TMainForm.UpdateTXControlAvailability; -var Allowed: Boolean; +var MOXAllowed, TuneAllowed: Boolean; begin if (BtnMOX = nil) or (BtnTUN = nil) or (FController = nil) then Exit; - Allowed := FController.FRunning and - (FController.ActiveTXMode <> MODE_DMR); - BtnMOX.Enabled := Allowed; - BtnTUN.Enabled := Allowed; + MOXAllowed := FController.FRunning and + (FController.ActiveTXMode <> MODE_DMR); + // FM RAW transmits external PCM only; the speech-path tune generator is not + // useful there and remains guarded in the controller as well. + TuneAllowed := MOXAllowed and (FController.ActiveTXMode <> MODE_FMRAW); + BtnMOX.Enabled := MOXAllowed; + BtnTUN.Enabled := TuneAllowed; + 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 + not (FController.ActiveTXMode in [MODE_DMR, MODE_FMRAW]); + 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; + UpdateIMDMarkers; +end; + +procedure TMainForm.UpdateIMDMarkers; +// 2TON: маркеры пиков тонов и продуктов IMD3. Только в DUP — дисплей остаётся +// на RX и показывает СВОЙ сигнал после PA (настоящие плечи IMD); цифровой +// TX-спектр продуктов PA не содержит, там замер бессмыслен. Абсолютные +// частоты = TX ± аудио-оффсеты тонов (знак как у генератора: для нижней +// боковой тона лежат НИЖЕ несущей). Замер/отрисовка — в SpectrumView. +var + Sgn: Double; +begin + if (FSpecView = nil) or (FController = nil) then Exit; + if (FController.ActiveTXMode = MODE_LSB) or + (FController.ActiveTXMode = MODE_CWL) or + (FController.ActiveTXMode = MODE_DIGL) then + Sgn := -1.0 + else + Sgn := 1.0; + FSpecView.SetIMDMarkers( + FController.FTwoTone and FController.FTransmitting + and FController.FDisplayDuplex, + ActiveTXFreqHz + Sgn * FController.FTXSettings.PSTTFreq1, + ActiveTXFreqHz + Sgn * FController.FTXSettings.PSTTFreq2); end; procedure TMainForm.UpdateStatusField3; @@ -4226,6 +4340,7 @@ begin EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); PanelFMRpt.Visible := True; end; + if PanelRawDev <> nil then PanelRawDev.Visible := False; RelayoutBelowBands; Exit; end; @@ -4237,7 +4352,7 @@ begin if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False; if PanelFMStep <> nil then begin - if FController.FMode = MODE_DMR then + if FController.FMode in [MODE_DMR, MODE_FMRAW] then begin StyleButton(BtnFMStep, FController.FFMStepOn); if FStepDropDown <> nil then @@ -4251,6 +4366,7 @@ begin end; end; if PanelFMRpt <> nil then PanelFMRpt.Visible := False; + if PanelRawDev <> nil then PanelRawDev.Visible := False; if FController.FMode = MODE_WFM then begin @@ -4273,6 +4389,33 @@ begin Exit; end; + if FController.FMode = MODE_FMRAW then + begin + CloseCTCSSPopup; + if FController.FFilter > FILT_RAW_COUNT - 1 then + FController.FFilter := FILT_RAW_DEF; + FController.FFilterBW := FILT_RAW_BW[FController.FFilter]; + for i := 0 to FILT_RAW_COUNT - 1 do + begin + BtnFilter[i].Caption := FILT_RAW_NAMES[i]; + BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_RAW_COUNT, i); + BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_RAW_COUNT, i); + BtnFilter[i].Top := 16; + BtnFilter[i].Visible := True; + StyleButton(BtnFilter[i], i = FController.FFilter); + end; + for i := FILT_RAW_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False; + PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4; + if PanelRawDev <> nil then + begin + TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50; + LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]); + PanelRawDev.Visible := True; + end; + RelayoutBelowBands; + Exit; + end; + if FController.FMode = MODE_DMR then begin CloseCTCSSPopup; @@ -4481,6 +4624,54 @@ 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 + live-обновление +// частот IMD-маркеров (если правили тона при активном 2TON). +begin + if FController.FDevConnected then + begin + FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); + FController.FSettings.Save; + end; + UpdateIMDMarkers; +end; + // --------------------------------------------------------------------------- // Клик/драг по спектру/водопаду // --------------------------------------------------------------------------- @@ -4499,7 +4690,7 @@ var ViewCenter, ViewSpan: Double; begin if PanelWidth <= 0 then Exit; - if (FController.FMode in [MODE_FM, MODE_DMR]) and + if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and FController.FFMStepOn then StepHz := FM_STEP_HZ[FController.FFMStepIdx] else @@ -4524,7 +4715,7 @@ begin if (FWidebandView = nil) or (not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit; - if (FController.FMode in [MODE_FM, MODE_DMR]) and + if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and FController.FFMStepOn then StepHz := FM_STEP_HZ[FController.FFMStepIdx] else @@ -5006,6 +5197,14 @@ begin SaveCurrentBand; end; +procedure TMainForm.TrkRawDevChange(Sender: TObject); +var Hz: Integer; +begin + if FSyncingFromController then Exit; + Hz := TrkRawDev.Position * 50; + FController.SetRawFMDeviation(Hz); +end; + procedure TMainForm.CloseCTCSSPopup; begin if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup; @@ -6095,8 +6294,8 @@ begin Id := P.FirstSliceId; StepHz := 100; if ((Id <> 0) and FController.GetSlice(Id, S) and - (S.Mode = MODE_DMR)) or - ((Id = 0) and (FController.FMode = MODE_DMR)) then + (S.Mode in [MODE_DMR, MODE_FMRAW])) or + ((Id = 0) and (FController.FMode in [MODE_DMR, MODE_FMRAW])) then StepHz := 12500; Hz := Round(VC + (X / W - 0.5) * VS); Hz := ((Hz + StepHz div 2) div StepHz) * StepHz; @@ -7355,6 +7554,10 @@ begin PanelFilter.Top := Y; Y := Y + PanelFilter.Height + 2; end; + if (PanelRawDev <> nil) and PanelRawDev.Visible then + begin + PanelRawDev.Top := Y; Y := Y + PanelRawDev.Height + 2; + end; if (PanelFMStep <> nil) and PanelFMStep.Visible then begin PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2; @@ -7631,7 +7834,7 @@ begin end; if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then begin - if (Sl.Mode = MODE_DMR) and + if (Sl.Mode in [MODE_DMR, MODE_FMRAW]) and not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := 12500 else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000 else if ssCtrl in Shift then Step := 1000 @@ -7655,7 +7858,7 @@ begin Handled := True; Exit; end; - if (FController.FMode in [MODE_FM, MODE_DMR]) and + if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and FController.FFMStepOn and not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := FM_STEP_HZ[FController.FFMStepIdx] diff --git a/PureSignalPopup.pas b/PureSignalPopup.pas new file mode 100644 index 0000000..3a8f546 --- /dev/null +++ b/PureSignalPopup.pas @@ -0,0 +1,453 @@ +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 — пороги от цели PSFBTarget (Thetis-Enhanced eu2av): + // >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет + // сигнала. 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, FBTarget: 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; + FBTarget := FController.PSFBTarget; + if FController.FPSFeedbackOn then + begin + if FB > Round(FBTarget * 1.5) then LampClr := CLR_FB_HOT + else if FB >= Round(FBTarget * 0.7) then + begin + if FB <= Round(FBTarget * 1.3) then LampClr := CLR_FB_OK + else LampClr := CLR_FB_LOW // 1.3..1.5×: желтовато + end + else if FB >= Round(FBTarget * 0.5) 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/%d', [FB, FBTarget]); + + 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..b43a867 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) @@ -337,6 +354,7 @@ type // ---- FM ---- FFMDeviation: Double; + FRawFMDeviation: Double; FFMCTCSSOn: Boolean; FFMCTCSSToneIdx: Integer; FFMSQOn: Boolean; @@ -489,6 +507,7 @@ type procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP + function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате) // ---- Управление диапазонами (состояние + DSP; рендер через события) ---- // MakeBandSettings — снимок текущего состояния в запись диапазона. @@ -546,6 +565,7 @@ type procedure SetMode(M: Integer); procedure SetFilterIdx(Idx: Integer); procedure SetFilterBW(BWHz: Integer); + procedure SetRawFMDeviation(Hz: Integer); procedure SetAGCMode(M: Integer); procedure SetAGCTop(DB: Integer); procedure AGCTopBy(Delta: Integer); @@ -721,6 +741,35 @@ 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], оптимум ~PSFBTarget + // Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по + // плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC, + // замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis). + function PSFBTarget: Integer; + 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 +864,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 @@ -864,7 +923,7 @@ begin FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web) // Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны). FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0; - FMode := 1; FFilter := 5; FFilterBW := 2700; + FMode := MODE_USB; FFilter := 5; FFilterBW := 2700; FAGCMode := 1; FAGCTop := 90; FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) @@ -882,6 +941,7 @@ begin FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096; FWfSpeedDiv := 1; FSpecMSAA := 4; FFMStepOn := True; + FRawFMDeviation := RAW_FM_DEV_DEFAULT; FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE; @@ -1065,13 +1125,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 @@ -1257,7 +1324,7 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single; begin // Decoded DMR PCM will use this route later. Until then suppress raw 4FSK; // the independent pre-volume tap is handled by OnDemodAudioReady. - if FMode = MODE_DMR then Exit; + if FMode in [MODE_DMR, MODE_FMRAW] then Exit; if FSendAudioToRadio then FNetwork.SendSpeakerAudio(Left, Right, Count); @@ -1273,13 +1340,19 @@ procedure TRadioController.OnDemodAudioReady(const Left, Right: array of Single; Count: Integer); // DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring. begin - if not Assigned(FDMRDec) then Exit; if FMode = MODE_DMR then begin + if not Assigned(FDMRDec) then Exit; if not FDMRDec.Enabled then FDMRDec.SetEnabled(True); FDMRDec.FeedAudio(Left, Right, Count); end - else if FDMRDec.Enabled then + else if FMode = MODE_FMRAW then + begin + // FM RAW decoder audio deliberately bypasses speaker volume and mute. + if FLocalAudio and Assigned(FAudioOut) then + FAudioOut.Write(Left, Right, Count); + end + else if Assigned(FDMRDec) and FDMRDec.Enabled then FDMRDec.SetEnabled(False); end; @@ -1337,7 +1410,7 @@ begin if not FLocalAudio then Exit; idx := FindSliceIndex(SliceId); if idx < 0 then Exit; - if FSlices[idx].Mode = MODE_DMR then Exit; + if FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW] then Exit; if SliceMutedByTx(idx) then Exit; if FSlices[idx].Audio <> nil then FSlices[idx].Audio.Write(Left, Right, Count); @@ -1358,9 +1431,12 @@ procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer; var idx: Integer; begin idx := FindSliceIndex(SliceId); - if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) then Exit; - if Assigned(FSlices[idx].DMR) then + if idx < 0 then Exit; + if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then FSlices[idx].DMR.FeedAudio(Left, Right, Count); + if (FSlices[idx].Mode = MODE_FMRAW) and FLocalAudio and + Assigned(FSlices[idx].Audio) then + FSlices[idx].Audio.Write(Left, Right, Count); end; // DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output. @@ -1762,6 +1838,7 @@ var AO: TAudioOutput; begin Result := -1; + Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX); if not Assigned(FDSPEngine) then Exit; if (APanId < 0) or (APanId >= MAX_PANS) then Exit; if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан @@ -1842,6 +1919,7 @@ end; procedure TRadioController.SetSliceMode(Id, Mode: Integer); var idx, OldMode: Integer; begin + Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX); idx := FindSliceIndex(Id); if idx < 0 then Exit; OldMode := FSlices[idx].Mode; @@ -1850,7 +1928,7 @@ begin FSlices[idx].Mode := Mode; if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode); // DMR is receive-only. If this slice used to be the TX source, hand TX back - // to the main receiver as soon as it enters DMR. + // to the main receiver as soon as it enters DMR. FM RAW supports flat TX. if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0); if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then FreeAndNil(FSlices[idx].DMR); @@ -2160,8 +2238,7 @@ begin if SliceId > 0 then begin if not GetSlice(SliceId, S) then Exit; - // EWSDR has no DMR transmitter/AMBE encoder. Do not accidentally use the - // analog WFM transmitter for a receive-only DMR slice. + // EWSDR has no DMR transmitter/AMBE encoder. if S.Mode = MODE_DMR then Exit; end; if FTxSliceId = SliceId then Exit; @@ -2470,11 +2547,15 @@ begin begin FDSPEngine.SetFilter(Lo, Hi); FDSPEngine.SetTXFilter(Lo, Hi); - if (FMode = MODE_FM) or (FMode = MODE_DMR) then + if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then begin - FDSPEngine.SetRXFMDeviation(FFMDeviation); - FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut, - FTXSettings.FMHighCut, FTXSettings.FMEmphPosition); + if FMode = MODE_FMRAW then + FDSPEngine.SetRawFMDeviation(FRawFMDeviation) + else + FDSPEngine.SetRXFMDeviation(FFMDeviation); + if FMode <> MODE_FMRAW then + FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut, + FTXSettings.FMHighCut, FTXSettings.FMEmphPosition); // A squelch would remove sync preambles; DMR always receives open. FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel); end @@ -2506,6 +2587,11 @@ begin FFilterBW := FILT_DMR_BW; FFMDeviation := DMR_FM_DEV; end; + MODE_FMRAW: + begin + if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF; + FFilterBW := FILT_RAW_BW[FFilter]; + end; MODE_LSB, MODE_USB: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end; MODE_DIGU, MODE_DIGL: @@ -2526,6 +2612,7 @@ begin MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)]; MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)]; MODE_DMR: Result := FILT_DMR_BW; + MODE_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)]; MODE_LSB, MODE_USB: Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; MODE_DIGU, MODE_DIGL: @@ -2604,6 +2691,11 @@ begin FFilterBW := FILT_DMR_BW; FFMDeviation := DMR_FM_DEV; end; + if FMode = MODE_FMRAW then + begin + if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF; + FFilterBW := FILT_RAW_BW[FFilter]; + end; end; function TRadioController.MakeBandSettings: TBandSettings; @@ -2762,6 +2854,7 @@ begin Result.AudioOutDevice := FAudioOutDevName; Result.AudioInDevice := FAudioInDevName; Result.SendAudioToRadio := FSendAudioToRadio; + Result.RawFMDeviation := Round(FRawFMDeviation); end; procedure TRadioController.SaveGlobalSettings; @@ -2773,7 +2866,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings); // DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает. begin // --- Режим --- - FMode := B.Mode; + FMode := EnsureRange(B.Mode, MODE_MIN, MODE_MAX); if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode); @@ -2808,7 +2901,7 @@ begin end; FFMStepOn := B.FMStepOn; FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3); - if FMode = MODE_DMR then + if FMode in [MODE_DMR, MODE_FMRAW] then begin FFMStepOn := True; FFMStepIdx := 1; // DMR default raster: 12.5 kHz @@ -2915,8 +3008,8 @@ begin if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); HalfSpan := FSpanHz / 2; case FMode of - 0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; - 1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; + MODE_LSB: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; + MODE_USB: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end; end; if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then @@ -3169,13 +3262,14 @@ end; procedure TRadioController.SetMode(M: Integer); begin + M := EnsureRange(M, MODE_MIN, MODE_MAX); FMode := M; // DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP // controls enabled so the operator can select another raster afterwards. - if FMode = MODE_DMR then + if FMode in [MODE_DMR, MODE_FMRAW] then begin FFMStepOn := True; - FFMStepIdx := 1; // 12.5 kHz + if FMode = MODE_DMR then FFMStepIdx := 1; // 12.5 kHz Changed(rfFMStep); Changed(rfFMStepIdx); end; @@ -3192,6 +3286,17 @@ begin if FWDSPReady and Assigned(FDSPEngine) then begin FDSPEngine.SetMode(FMode); + // SetMode configures the main RX/TX pair; a selected slice may still be + // the actual TX source and therefore owns the transmitter mode. + FDSPEngine.SetTXMode(ActiveTXMode); + // A live mode change must also switch away from web/HW microphone when + // entering FM RAW; normally the modem is connected through PortAudio. + if FTransmitting and (ActiveTXMode = MODE_FMRAW) then + begin + FWebMicActive := False; + FDSPEngine.SetTXMicSource(DefaultMicSource); + if Assigned(FAudioIn) and FAudioIn.IsOpen then FAudioIn.Flush; + end; // Если TUN активен — пересчитываем знак частоты под новый режим if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); end; @@ -3207,6 +3312,7 @@ begin MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1); MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1); MODE_DMR: Idx := 0; + MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1); else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1); end; FFilter := Idx; @@ -3217,6 +3323,7 @@ begin MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx]; MODE_WFM: FFilterBW := FILT_WFM_BW[Idx]; MODE_DMR: FFilterBW := FILT_DMR_BW; + MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx]; MODE_FM: begin FFilterBW := FILT_FM_BW[Idx]; @@ -3250,6 +3357,7 @@ begin MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i]; MODE_WFM: for i := 0 to FILT_WFM_COUNT-1 do BW_arr[i] := FILT_WFM_BW[i]; MODE_DMR: BW_arr[0] := FILT_DMR_BW; + MODE_FMRAW: for i := 0 to FILT_RAW_COUNT-1 do BW_arr[i] := FILT_RAW_BW[i]; else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i]; end; best := 0; dist := MaxInt; @@ -3266,6 +3374,15 @@ begin Changed(rfFilter); end; +procedure TRadioController.SetRawFMDeviation(Hz: Integer); +begin + FRawFMDeviation := EnsureRange(Hz, RAW_FM_DEV_MIN, RAW_FM_DEV_MAX); + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetRawFMDeviation(FRawFMDeviation); + SaveGlobalSettings; + Changed(rfFMDeviation); +end; + // UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех // фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum). class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode; @@ -4035,6 +4152,10 @@ end; procedure TRadioController.ApplyTXSettingsToDSP; begin if not FWDSPReady then Exit; + // Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2: + // он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не + // имеет, поэтому при смене бэкенда явно выключаем его. + FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR); // Mic source — выставляем значение по умолчанию для MOX-кнопки; // при реальном PTT SetMOX скорректирует источник. FDSPEngine.SetTXMicSource(DefaultMicSource); @@ -4063,6 +4184,11 @@ begin FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector, FTXSettings.TXWfAvgMode, FTXSettings.TXWfAvgTimeMS); + // Reapply the mode overlay last: FM RAW must stay flat even when the user + // edits and saves ordinary voice-chain settings while this mode is active. + FDSPEngine.SetTXMode(ActiveTXMode); + // PureSignal-параметры calcc (mox/loop/tx delay, ptol, pin/map/stbl, SetPk) + ApplyPSSettingsToDSP; end; procedure TRadioController.SetMOX(On_: Boolean); @@ -4070,7 +4196,7 @@ var WasTransmitting: Boolean; begin // DMR support is receive-only. This guard also covers CAT, web and hardware - // PTT. A selected non-DMR TX slice remains valid while main A receives DMR. + // PTT. FM RAW is a flat external-modem transmit path. if On_ and (ActiveTXMode = MODE_DMR) then Exit; // Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере. if On_ and FDevConnected and (FCurrentXvtr < 0) @@ -4081,6 +4207,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 +4230,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с» теперь под вопросом, @@ -4107,11 +4244,14 @@ begin end; if FWDSPReady and Assigned(FDSPEngine) then begin - // Mic source автоматически по источнику TX: - // Web TX → txmsWeb; HW PTT → txmsRadio; MOX/CAT → DefaultMicSource. + // Mic source автоматически по источнику TX. FM RAW always uses the + // configured default input (normally a virtual PortAudio cable), even if + // a web client is connected or hardware PTT keyed the transmitter. if FTransmitting then begin - if FWebClientActive then + if ActiveTXMode = MODE_FMRAW then + FDSPEngine.SetTXMicSource(DefaultMicSource) + else if FWebClientActive then begin FWebMicActive := True; FDSPEngine.SetTXMicSource(txmsWeb); @@ -4140,6 +4280,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 +4303,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; @@ -4174,13 +4318,14 @@ end; procedure TRadioController.SetTune(On_: Boolean); begin if On_ = FTuning then Exit; - if On_ and (ActiveTXMode = MODE_DMR) then Exit; + if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit; // Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон, // он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR. 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_ and FTwoTone then SetTwoTone(False); // TUN поверх 2TON — гасим тона if On_ then begin // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. @@ -4210,6 +4355,315 @@ 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; + +function TRadioController.PSFBTarget: Integer; +begin + Result := FTXSettings.PSFBTarget; + if Result <= 0 then + begin + if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then + Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD + else + Result := 152; // классическая цель Thetis + end; +end; + +function TRadioController.PSOutlierSigmaEff: Double; +begin + Result := FTXSettings.PSOutlierSigma; + if Result < 0 then + begin + if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then + Result := 5.0 // Orion MkII: дефолт Thetis-Enhanced eu2av + else + Result := 0.0; // выключен + end; +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, PSOutlierSigmaEff); + // Правка частот двухтональника при активном 2TON — применяем живьём. + if FTwoTone then + FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2); +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; + FBTarget: 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-Enhanced eu2av timer2code) --- + // По завершении очередной попытки калибровки: если feedback слишком горячий + // (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX + // так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход + // фидбек-АЦП интермодулирует сам, и PS корректирует по искажённому сигналу). + FBTarget := PSFBTarget; + case FPSAAState of + 0: begin + CalAttemptsChanged := FPSInfo[5] <> FPSInfoPrev[5]; + if FTXSettings.PSAutoAtten and CalAttemptsChanged + and ((FPSInfo[4] > Round(FBTarget * 1.5)) or + ((FPSInfo[4] < Round(FBTarget * 0.7)) and (FTXSettings.AttOnTX > 0))) then + begin + FPSAAState := 1; + if (FPSInfo[4] > 0) and (FPSInfo[4] <= 256) then + begin + ddB := 20.0 * Log10(FPSInfo[4] / FBTarget); + 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 in [MODE_DMR, MODE_FMRAW]) 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 +4895,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; @@ -4577,6 +5050,10 @@ begin FDitherEnabled := FLoadedGlobal.DitherEnabled; FRandomEnabled := FLoadedGlobal.RandomEnabled; FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio; + FRawFMDeviation := EnsureRange(FLoadedGlobal.RawFMDeviation, + RAW_FM_DEV_MIN, RAW_FM_DEV_MAX); + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetRawFMDeviation(FRawFMDeviation); FWfManualHigh := FLoadedGlobal.WfManualHigh; FWfManualLow := FLoadedGlobal.WfManualLow; FWfAGCOffset := FLoadedGlobal.WfAGCOffset; diff --git a/RadioModes.pas b/RadioModes.pas new file mode 100644 index 0000000..4099bbc --- /dev/null +++ b/RadioModes.pas @@ -0,0 +1,30 @@ +unit RadioModes; + +{$mode objfpc}{$H+} + +// Canonical persisted mode indices. Append only: these values are stored in +// settings.json/channels.json and are exposed through CAT and the web API. +interface + +const + MODE_LSB = 0; + MODE_USB = 1; + MODE_DSB = 2; + MODE_CWL = 3; + MODE_CWU = 4; + MODE_FM = 5; + MODE_AM = 6; + MODE_SAM = 7; + MODE_DIGU = 8; + MODE_DIGL = 9; + MODE_WFM = 10; + MODE_DMR = 11; + MODE_FMRAW = 12; + + MODE_MIN = MODE_LSB; + MODE_MAX = MODE_FMRAW; + MODE_COUNT = MODE_MAX - MODE_MIN + 1; + +implementation + +end. diff --git a/Settings.pas b/Settings.pas index 6de595a..0011af8 100644 --- a/Settings.pas +++ b/Settings.pas @@ -6,13 +6,15 @@ unit Settings; } {$mode objfpc}{$H+} interface -uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils; +uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils, + RadioModes; const SETTINGS_FILE = 'hpsdr_settings.json'; CFG_BAND_COUNT = 12; CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) - CFG_MODE_MAX = 11; // LSB..DMR; keep persisted-mode validation current + CFG_MODE_MIN = RadioModes.MODE_MIN; + CFG_MODE_MAX = RadioModes.MODE_MAX; CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 // (split-LO транспондер; настраивается на @@ -61,6 +63,16 @@ const FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000); FILT_WFM_DEF = 1; // 160k — влезает и в 192k + // FM RAW: narrow, flat discriminator output for external digital decoders. + // These are RF channel widths; baseband stays flat at 20..7000 Hz. + FILT_RAW_COUNT = 5; + FILT_RAW_BW: array[0..FILT_RAW_COUNT-1] of Integer = + (6250, 10000, 12500, 20000, 25000); + FILT_RAW_DEF = 2; // 12.5 kHz + RAW_FM_DEV_DEFAULT = 2500; + RAW_FM_DEV_MIN = 500; + RAW_FM_DEV_MAX = 10000; + // DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz. FILT_DMR_BW = 12500; DMR_FM_DEV = 2500; @@ -209,6 +221,25 @@ 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) + // Калибровка фидбек-тракта (Thetis-Enhanced eu2av): на Orion MkII цель 152 + // перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ. + PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате + // (Orion MkII → 22, иначе 152) + PSOutlierSigma: Double; // σ outlier-фильтра calcc; <0 = авто по плате + // (Orion MkII → 5.0, иначе выкл.), 0 = выкл. end; // Web-сервер — глобальные настройки (не привязаны к устройству, секция "web"). @@ -346,6 +377,7 @@ type AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000) AudioInDevice: string; // PortAudio input device name for TX mic ('' = none) SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker) + RawFMDeviation: Integer; // Hz, flat RAW FM discriminator scale // --- Visibility settings --- ShowSpectrum: Boolean; // True = draw spectrum on main form ShowWaterfall: Boolean; // True = draw waterfall on main form @@ -553,6 +585,7 @@ begin G.AudioSampleRate := 48000; G.AudioInDevice := ''; G.SendAudioToRadio := False; + G.RawFMDeviation := RAW_FM_DEV_DEFAULT; G.ShowSpectrum := True; G.ShowWaterfall := True; G.ShowWideband := False; @@ -783,6 +816,9 @@ begin G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000); G.AudioInDevice := JS(GObj,'audio_in_device',''); G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False); + G.RawFMDeviation := EnsureRange( + JI(GObj,'raw_fm_deviation',RAW_FM_DEV_DEFAULT), + RAW_FM_DEV_MIN, RAW_FM_DEV_MAX); // Visibility settings G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True); @@ -827,7 +863,7 @@ begin BObj := GetBandObj(DevObj, i); Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA); Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB); - Bands[i].Mode := JI(BObj,'mode', Bands[i].Mode); + Bands[i].Mode := EnsureRange(JI(BObj,'mode', Bands[i].Mode), CFG_MODE_MIN, CFG_MODE_MAX); Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx); Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW); Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode); @@ -895,6 +931,7 @@ begin JW(O,'audio_sample_rate',G.AudioSampleRate); JWS(O,'audio_in_device',G.AudioInDevice); JW(O,'send_audio_to_radio',G.SendAudioToRadio); + JW(O,'raw_fm_deviation',G.RawFMDeviation); // Visibility settings JW(O,'show_spectrum',G.ShowSpectrum); JW(O,'show_waterfall',G.ShowWaterfall); @@ -960,7 +997,7 @@ begin O := GetBandObj(GetDevObj(MacStr), BandIdx); B.VfoA := JD(O,'vfo_a', B.VfoA); B.VfoB := JD(O,'vfo_b', B.VfoB); - B.Mode := JI(O,'mode', B.Mode); + B.Mode := EnsureRange(JI(O,'mode', B.Mode), CFG_MODE_MIN, CFG_MODE_MAX); B.FilterIdx := JI(O,'filter_idx', B.FilterIdx); B.FilterBW := JI(O,'filter_bw', B.FilterBW); B.AGCMode := JI(O,'agc_mode', B.AGCMode); @@ -1170,6 +1207,21 @@ 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; + T.PSFBTarget := 0; // авто по плате + T.PSOutlierSigma := -1.0; // авто по плате end; function TSettingsManager.LoadTX(const MAC: array of Byte; @@ -1236,6 +1288,21 @@ 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); + T.PSFBTarget := EnsureRange(JI(O,'ps_fb_target', T.PSFBTarget), 0, 256); + T.PSOutlierSigma := JD(O,'ps_outlier_sigma', T.PSOutlierSigma); end; procedure TSettingsManager.SaveTX(const MAC: array of Byte; @@ -1294,6 +1361,21 @@ 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); + JW(O,'ps_fb_target', T.PSFBTarget); + JW(O,'ps_outlier_sigma',T.PSOutlierSigma); end; class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings); @@ -1439,7 +1521,7 @@ begin X.Entries[i].RXAntenna := 0; X.Entries[i].LastFreq := 0.0; X.Entries[i].LastFreqB := 0.0; - X.Entries[i].LastMode := 1; // USB — VHF SSB calling default + X.Entries[i].LastMode := MODE_USB; // VHF SSB calling default X.Entries[i].LastFilterIdx := 0; // NFM X.Entries[i].LastFMSQOn := False; X.Entries[i].LastFMSQLevel := 30; @@ -1520,7 +1602,7 @@ begin X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3); X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq); X.Entries[i].LastFreqB := JD(EObj, 'last_freq_b', X.Entries[i].LastFreqB); - X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), 0, CFG_MODE_MAX); + X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), CFG_MODE_MIN, CFG_MODE_MAX); X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9); X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn); X.Entries[i].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100); @@ -1856,7 +1938,8 @@ begin n := Length(P.Pans[id].Slices); SetLength(P.Pans[id].Slices, n+1); P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0); - P.Pans[id].Slices[n].Mode := JI(SliceObj, 'mode', 0); + P.Pans[id].Slices[n].Mode := EnsureRange( + JI(SliceObj, 'mode', MODE_LSB), CFG_MODE_MIN, CFG_MODE_MAX); P.Pans[id].Slices[n].FilterLo := JI(SliceObj, 'filter_lo', -1500); P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500); P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1); diff --git a/SpectrumView.pas b/SpectrumView.pas index 838689f..28484cc 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -21,7 +21,7 @@ uses Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math, AppTheme, AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, - WaterfallView, SMeterView, RulerView; + WaterfallView, SMeterView, RulerView, RadioModes; const SV_CLR_BG = TColor($00101010); @@ -36,6 +36,16 @@ type LastUse: Int64; end; + // Маркер 2TON/IMD-измерения на спектре: кружок на пике продукта + подпись. + // Все маркеры подписываются абсолютным уровнем шкалы (dBm); относительная + // величина IMD3 в dBc — в сводке слева от полосы фильтра. + TIMDMarker = record + FreqHz: Double; // абсолютная частота продукта + LevelDB: Double; // измеренный пик (шкала дисплея) + Caption: string; + IsTone: Boolean; // True = основной тон, False = IMD-продукт + end; + TSpectrumView = class protected // ── Off-screen bitmaps ──────────────────────────────────────────────────── @@ -84,6 +94,7 @@ type FTXOverlay: Boolean; // ── ADC overload overlay ───────────────────────────────────────────────── FADCOverloadVisible: Boolean; + FADCAlertImg: TAlertImage; // кэш образа плашки (BuildAlertImage, 1 раз) FSampleRateOverlay: TSampleRateOverlay; FVfoOverlay: TVfoOverlay; FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm) @@ -99,6 +110,11 @@ type FBeaconDecActive: Boolean; FBeaconDecFreq: Double; FBeaconDecHalf: Double; // полуширина фильтра, Гц + // ── 2TON/IMD-маркеры (пики тонов и продуктов IMD3 при двухтональнике) ──── + FIMDActive: Boolean; + FIMDTone1Hz: Double; // абс. частоты тонов (TX freq + аудио-оффсет) + FIMDTone2Hz: Double; + FIMDSummary: string; // сводка «IMD3 −NN dBc» (обновляет ComputeIMDMarkers) // ── Spectrum buffer ─────────────────────────────────────────────────────── FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single; FSpectrumBufCount: Integer; @@ -149,10 +165,16 @@ type procedure DrawSliceFilterLinesRaw(W, H: Integer); procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor); procedure DrawBeaconMarkersRaw(W, H: Integer); + // 2TON/IMD: замер пиков по TX-буферу (общий для CPU/GL рендеров). + // Возвращает число валидных маркеров в M (0 если измерять нечего): + // [0..1] тона, [2..3] IMD3 (2f1−f2, 2f2−f1). Обновляет FIMDSummary. + function ComputeIMDMarkers(var M: array of TIMDMarker): Integer; + procedure DrawIMDMarkersRaw(W, H: Integer; DBmax, InvRange: Double); + procedure RawCircle(CX, CY, R: Integer; Color: TColor); procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); procedure FillBandRaw(X1, X2, H: Integer; Color: TColor); procedure CopyGridToSpectrum(W, H: Integer); - procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer); + procedure DrawADCOverloadRaw(W, H: Integer); procedure CalcFilterBandX(VfoFreq: Double; W: Integer; out X1, X2, VfoX: Integer); procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer; @@ -244,6 +266,12 @@ type procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double); procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double); + // 2TON/IMD-маркеры: кружки с уровнями на пиках тонов и продуктов IMD3 + // (замер линейности/PureSignal). Включает MainForm при 2TON+TX+DUP: + // дисплей на RX показывает СВОЙ сигнал после PA — настоящие IMD-плечи + // (на цифровом TX-спектре продуктов PA нет, там замер бессмыслен). + procedure SetIMDMarkers(Active: Boolean; Tone1Hz, Tone2Hz: Double); + // ── S-метр ──────────────────────────────────────────────────────────────── property LastSMeter: Double read GetLastSMeter write SetLastSMeter; property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak; @@ -653,6 +681,142 @@ begin end; end; +procedure TSpectrumView.SetIMDMarkers(Active: Boolean; Tone1Hz, Tone2Hz: Double); +begin + FIMDActive := Active; + FIMDTone1Hz := Tone1Hz; + FIMDTone2Hz := Tone2Hz; + if not Active then FIMDSummary := ''; +end; + +function TSpectrumView.ComputeIMDMarkers(var M: array of TIMDMarker): Integer; +// Пики меряются по FSpectrumBuf. Основной сценарий — DUP: дисплей на RX +// (TXMode=False), буфер покрывает видимое окно FCenterFreq ± FSpanHz/2, и мы +// видим СВОЙ сигнал после PA (реальные IMD-плечи). При TXMode буфер покрывает +// FTXFreq ± FTXSpanHz/2 (цифровой TX-тракт — оставлено для общности). +// Окно поиска ±250 Гц вокруг ожидаемой частоты — ловим пик независимо от +// джиттера FFT. +var + SrcCount, i: Integer; + ToneAvg, MapLo, MapSpan: Double; + + function PeakDB(FreqHz: Double): Double; + var C, Win, k, i1, i2: Integer; + begin + Result := -999.0; + C := Round((FreqHz - MapLo) / MapSpan * (SrcCount - 1)); + // Ожидаемая частота вне окна буфера (зум/узкий span) — маркер невалиден, + // иначе намеряли бы уровень краевого бина. + if (C < 0) or (C > SrcCount - 1) then Exit; + Win := Max(2, Round(250.0 * (SrcCount - 1) / MapSpan)); + i1 := Max(0, C - Win); + i2 := Min(SrcCount - 1, C + Win); + for k := i1 to i2 do + if FSpectrumBuf[k] > Result then Result := FSpectrumBuf[k]; + end; + +begin + Result := 0; + FIMDSummary := ''; + if not FIMDActive then Exit; + if (FSpectrumBufCount < 2) or (Length(M) < 4) then Exit; + if FTXMode then + begin + if FTXSpanHz <= 0 then Exit; + MapLo := FTXFreq - FTXSpanHz * 0.5; + MapSpan := FTXSpanHz; + end + else + begin + if FSpanHz <= 0 then Exit; + MapLo := FCenterFreq - FSpanHz * 0.5; + MapSpan := FSpanHz; + end; + SrcCount := EnsureRange(FSpectrumBufCount, 2, WF_MAX_PIXELS); + + M[0].FreqHz := FIMDTone1Hz; M[0].IsTone := True; + M[1].FreqHz := FIMDTone2Hz; M[1].IsTone := True; + M[2].FreqHz := 2 * FIMDTone1Hz - FIMDTone2Hz; M[2].IsTone := False; + M[3].FreqHz := 2 * FIMDTone2Hz - FIMDTone1Hz; M[3].IsTone := False; + for i := 0 to 3 do + M[i].LevelDB := PeakDB(M[i].FreqHz); + + ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5; + // Все маркеры — абсолютный уровень шкалы (dBm, как в Thetis); относительная + // величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы. + for i := 0 to 3 do + M[i].Caption := Format('%.0f', [M[i].LevelDB]); + // Сводка только когда оба тона и хотя бы один продукт в окне буфера + if (M[0].LevelDB > -500) and (M[1].LevelDB > -500) and + (Max(M[2].LevelDB, M[3].LevelDB) > -500) then + FIMDSummary := Format('IMD3 %.0f dBc', + [Max(M[2].LevelDB, M[3].LevelDB) - ToneAvg]); + Result := 4; +end; + +procedure TSpectrumView.RawCircle(CX, CY, R: Integer; Color: TColor); +// Залитый кружок радиуса R: по строкам, ширина из уравнения окружности. +var + DY, HalfW, Y, X1, X2, K: Integer; + Px: LongWord; + P: PLongWord; +begin + if FRawBase = nil then Exit; + Px := RawPack(Color); + for DY := -R to R do + begin + Y := CY + DY; + if (Y < 0) or (Y >= FRawH) then Continue; + HalfW := Round(Sqrt(R * R - DY * DY)); + X1 := Max(0, CX - HalfW); + X2 := Min(FRawW - 1, CX + HalfW); + if X2 < X1 then Continue; + P := PLongWord(FRawBase + Y * FRawBPL + X1 * 4); + for K := X1 to X2 do begin P^ := Px; Inc(P); end; + end; +end; + +procedure TSpectrumView.DrawIMDMarkersRaw(W, H: Integer; DBmax, InvRange: Double); +const + CLR_TONE = TColor($0050FF50); // зелёный — основные тона + CLR_IMD = TColor($000A8CFF); // оранжевый — продукты IMD3 +var + M: array[0..3] of TIMDMarker; + N, i, X, Y, TY, TW, BX1, BX2, BVX: Integer; + Col: TColor; +begin + if FSpanHz <= 0 then Exit; + N := ComputeIMDMarkers(M); + if N = 0 then Exit; + for i := 0 to N - 1 do + begin + if M[i].LevelDB < -500 then Continue; // частота вне окна буфера + X := Round((M[i].FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); + if (X < 0) or (X >= W) then Continue; + Y := Max(0, Min(H - 1, Round((DBmax - M[i].LevelDB) * InvRange * H))); + // Все маркеры зелёные (dBm-замеры); оранжевый — только вычисленная + // сводка dBc, чтобы цвета не путались. + Col := CLR_TONE; + RawCircle(X, Y, 4, Col); + // Подпись по центру над своим кружком; у верхней кромки — под ним. + TW := RawTextWidth(M[i].Caption, 7, True); + TY := Y - 16; + if TY < 2 then TY := Y + 8; + RawText(Max(2, Min(W - TW - 2, X - TW div 2)), TY, + M[i].Caption, Col, 7, True, clNone); + end; + // Сводка слева от полосы фильтра, чтобы не прятаться за SampleRateOverlay + // в левом верхнем углу и сразу бросаться в глаза рядом с сигналом. + if FIMDSummary <> '' then + begin + TW := RawTextWidth(FIMDSummary, 11, True); + if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, BX1, BX2, BVX) + else CalcFilterBandX(FVfoB, W, BX1, BX2, BVX); + X := Max(2, Min(W - TW - 2, BX1 - TW - 8)); + RawText(X, 6, FIMDSummary, CLR_IMD, 11, True, clNone); + end; +end; + // ════════════════════════════════════════════════════════════════════════════ // Full-raw примитивы кадра (валидны только между RawBegin/RawEnd) // ════════════════════════════════════════════════════════════════════════════ @@ -1077,10 +1241,18 @@ begin {$ENDIF} end; -procedure TSpectrumView.DrawADCOverloadOverlay(C: TCanvas; W, H: Integer); +procedure TSpectrumView.DrawADCOverloadRaw(W, H: Integer); +// Плашка ADC OVERLOAD: общий с GL-рендером образ (AlertOverlay.BuildAlertImage, +// строится один раз и кэшируется), в кадре — только straight-alpha бленд +// внутри RawBegin/RawEnd: без Canvas (дорогой raw↔canvas синк Qt6) и без +// пофреймового восстановления альфы всего битмапа. begin if not FADCOverloadVisible then Exit; - DrawAlertOverlay(FSpectrumBitmap, C, W, H, 'ADC OVERLOAD', 'Input clipping detected'); + if (W < 220) or (H < 60) or (FRawBase = nil) then Exit; + if FADCAlertImg.W = 0 then + BuildAlertImage('ADC OVERLOAD', 'Input clipping detected', FADCAlertImg); + BlendAlertImage(FADCAlertImg, FRawBase, FRawBPL, FRawW, FRawH, + W - ALERT_PAD - FADCAlertImg.W, ALERT_PAD); end; procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer; @@ -1098,10 +1270,10 @@ begin Half := BW / 2; VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W); case Mode of - 0: begin Lo_Hz := -BW; Hi_Hz := -100; end; // LSB - 1: begin Lo_Hz := 100; Hi_Hz := BW; end; // USB - 9: begin Lo_Hz := -BW; Hi_Hz := 0; end; // DIGL — полоса от нуля - 8: begin Lo_Hz := 0; Hi_Hz := BW; end; // DIGU + MODE_LSB: begin Lo_Hz := -BW; Hi_Hz := -100; end; + MODE_USB: begin Lo_Hz := 100; Hi_Hz := BW; end; + MODE_DIGL: begin Lo_Hz := -BW; Hi_Hz := 0; end; // полоса от нуля + MODE_DIGU: begin Lo_Hz := 0; Hi_Hz := BW; end; else begin Lo_Hz := -Half; Hi_Hz := Half; end; end; X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W))); @@ -1546,6 +1718,9 @@ begin if FMarkerActive then DrawMarkerLineRaw(W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H); + // 2TON: кружки с уровнями на пиках тонов/IMD3 (активирует MainForm при + // 2TON+TX+DUP — реальный сигнал после PA виден на RX-спектре) + if FIMDActive then DrawIMDMarkersRaw(W, H, DBmax, InvRange); // Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/ @@ -1566,33 +1741,12 @@ begin for i := 0 to FSliceOverlays.Count - 1 do TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H); + DrawADCOverloadRaw(W, H); + finally RawEnd; end; - // ADC overload — редкий canvas-путь (DrawAlertOverlay). Ломает альфу и - // режим доступа только в кадрах, где реально виден; после его canvas- - // операций восстанавливаем непрозрачность (пофреймовый OR-проход для - // обычных кадров удалён — Canvas на битмапе кадра больше не бывает). - if FADCOverloadVisible then - begin - DrawADCOverloadOverlay(C, W, H); -{$IFNDEF DARWIN} - FSpectrumBitmap.BeginUpdate(False); - for i := 0 to H - 1 do - begin - RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]); - if RowLW = nil then Continue; - for Yp := 0 to W - 1 do - begin - RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3 - Inc(RowLW); - end; - end; - FSpectrumBitmap.EndUpdate(False); -{$ENDIF} - end; - end; // ──────────────────────────────────────────────────────────────────────────── diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index c1c6220..98d11a4 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -17,7 +17,7 @@ interface uses Classes, SysUtils, Graphics, Controls, Math, Types, OpenGLContextEx, GL, - AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, + AppTheme, AlertOverlay, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallView, WaterfallViewOpengl; type @@ -57,6 +57,9 @@ type FLastAGCHangY: Integer; FLastMarkerText: string; FLastMarkerX: Integer; + // 2TON/IMD: текстуры подписей маркеров (0..3 = кружки, 4 = сводка IMD3) + FIMDLabelTex: array[0..4] of TGLTextureCache; + FIMDLastText: array[0..4] of string; FLastADCVisible: Boolean; FLastSampleX: Integer; FLastSampleY: Integer; @@ -90,6 +93,8 @@ type procedure DrawSpectrumCurve(W, H: Integer; DBmax, InvRange: Double); procedure DrawMarker(W, H: Integer); procedure DrawBeaconMarkersGL(W, H: Integer); + procedure DrawCircleGL(CX, CY: Integer; R: Single; C: TColor); + procedure DrawIMDMarkersGL(W, H: Integer; DBmax, InvRange: Double); procedure DrawADCOverlay(W, H: Integer); procedure DrawCachedOverlays(W, H: Integer); procedure DrawSliceOverlays(W, H: Integer); @@ -170,6 +175,7 @@ begin DeleteTexture(FBandOverlayTex); for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex); for i := 0 to High(FLetterTex) do DeleteTexture(FLetterTex[i]); + for i := 0 to High(FIMDLabelTex) do DeleteTexture(FIMDLabelTex[i]); end; SetLength(FSliceTex, 0); inherited Destroy; @@ -451,6 +457,11 @@ begin Zap(FBandOverlayTex); for i := 0 to High(FSliceTex) do Zap(FSliceTex[i].Tex); for i := 0 to High(FLetterTex) do Zap(FLetterTex[i]); + for i := 0 to High(FIMDLabelTex) do + begin + Zap(FIMDLabelTex[i]); + FIMDLastText[i] := ''; + end; FLastAGCText := ''; FLastMarkerText := ''; FGridDirty := True; @@ -803,38 +814,103 @@ begin end; end; -procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer); +procedure TSpectrumViewOpenGL.DrawCircleGL(CX, CY: Integer; R: Single; C: TColor); +// Залитый кружок (triangle fan, 20 сегментов) — маркер пика 2TON/IMD. var - B: TBitmap; + CR, CG, CB: GLFloat; + a: Integer; + Ang: Single; +begin + ColorToRGB(C, CR, CG, CB); + glColor4f(CR, CG, CB, 1); + glBegin(GL_TRIANGLE_FAN); + glVertex2f(CX + 0.5, CY + 0.5); + for a := 0 to 20 do + begin + Ang := a * 2 * Pi / 20; + glVertex2f(CX + 0.5 + R * Cos(Ang), CY + 0.5 + R * Sin(Ang)); + end; + glEnd; +end; + +procedure TSpectrumViewOpenGL.DrawIMDMarkersGL(W, H: Integer; + DBmax, InvRange: Double); +// GL-вариант DrawIMDMarkersRaw: замер общий (ComputeIMDMarkers), подписи — +// текстуры с кэшем по строке (перезаливка только при смене значения). +const + CLR_TONE = TColor($0050FF50); // зелёный — основные тона + CLR_IMD = TColor($000A8CFF); // оранжевый — продукты IMD3 +var + M: array[0..3] of TIMDMarker; + N, i, X, Y, TX, TY, BX1, BX2, BVX: Integer; + Col: TColor; +begin + if FSpanHz <= 0 then Exit; + N := ComputeIMDMarkers(M); + if N = 0 then Exit; + for i := 0 to N - 1 do + begin + if M[i].LevelDB < -500 then Continue; // частота вне окна буфера + X := Round((M[i].FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); + if (X < 0) or (X >= W) then Continue; + Y := Max(0, Min(H - 1, Round((DBmax - M[i].LevelDB) * InvRange * H))); + // Все маркеры зелёные (dBm-замеры); оранжевый — только вычисленная + // сводка dBc, чтобы цвета не путались. + Col := CLR_TONE; + DrawCircleGL(X, Y, 4.0, Col); + if (M[i].Caption <> FIMDLastText[i]) or FIMDLabelTex[i].Dirty then + begin + UploadText(FIMDLabelTex[i], M[i].Caption, Col, 7, True); + FIMDLastText[i] := M[i].Caption; + end; + // Подпись по центру над своим кружком; у верхней кромки — под ним. + TY := Y - 16; + if TY < 2 then TY := Y + 8; + TX := Max(2, Min(W - FIMDLabelTex[i].W - 2, X - FIMDLabelTex[i].W div 2)); + DrawTexture(FIMDLabelTex[i], TX, TY); + end; + // Сводка слева от полосы фильтра, чтобы не прятаться за SampleRateOverlay + // в левом верхнем углу и сразу бросаться в глаза рядом с сигналом. + if FIMDSummary <> '' then + begin + if (FIMDSummary <> FIMDLastText[4]) or FIMDLabelTex[4].Dirty then + begin + UploadText(FIMDLabelTex[4], FIMDSummary, CLR_IMD, 11, True); + FIMDLastText[4] := FIMDSummary; + end; + if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, BX1, BX2, BVX) + else CalcFilterBandX(FVfoB, W, BX1, BX2, BVX); + TX := Max(2, Min(W - FIMDLabelTex[4].W - 2, + BX1 - FIMDLabelTex[4].W - 8)); + DrawTexture(FIMDLabelTex[4], TX, 6); + end; +end; + +procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer); +// Тот же образ, что CPU-путь (AlertOverlay.BuildAlertImage — единственный +// источник внешнего вида): грузится текстурой ОДИН раз при появлении плашки +// (кэш FADCOverlayTex), в кадре — только готовый DrawTexture. +var + Img: TAlertImage; begin if not FADCOverloadVisible then Exit; + if (W < 220) or (H < 60) then Exit; // как CPU-путь if (not FLastADCVisible) or FADCOverlayTex.Dirty then begin - B := TBitmap.Create; - try - B.PixelFormat := pf32bit; - B.SetSize(210, 38); - B.Canvas.Brush.Color := TColor($00202020); - B.Canvas.FillRect(Rect(0, 0, B.Width, B.Height)); - B.Canvas.Pen.Color := TColor($000030C0); - B.Canvas.Brush.Style := bsClear; - B.Canvas.Rectangle(0, 0, B.Width, B.Height); - B.Canvas.Font.Name := 'Courier New'; - B.Canvas.Font.Size := 10; - B.Canvas.Font.Style := [fsBold]; - B.Canvas.Font.Color := TColor($000030C0); - B.Canvas.TextOut(12, 5, 'ADC OVERLOAD'); - B.Canvas.Font.Size := 7; - B.Canvas.Font.Style := []; - B.Canvas.Font.Color := TColor($00D0D0D0); - B.Canvas.TextOut(12, 21, 'Input clipping detected'); - UploadBitmap(FADCOverlayTex, B, False, 232); - finally - B.Free; - end; + BuildAlertImage('ADC OVERLOAD', 'Input clipping detected', Img); + if FADCOverlayTex.Tex = 0 then glGenTextures(1, @FADCOverlayTex.Tex); + FADCOverlayTex.W := Img.W; + FADCOverlayTex.H := Img.H; + glBindTexture(GL_TEXTURE_2D, FADCOverlayTex.Tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, Img.W, Img.H, 0, GL_RGBA, + GL_UNSIGNED_BYTE, @Img.RGBA[0]); + glBindTexture(GL_TEXTURE_2D, 0); + FADCOverlayTex.Dirty := False; end; FLastADCVisible := True; - DrawTexture(FADCOverlayTex, (W - FADCOverlayTex.W) div 2, 8); + DrawTexture(FADCOverlayTex, W - FADCOverlayTex.W - ALERT_PAD, ALERT_PAD); end; procedure TSpectrumViewOpenGL.DrawCachedOverlays(W, H: Integer); @@ -1016,6 +1092,9 @@ begin DrawSpectrumCurve(W, H, DBmax, InvRange); DrawMarker(W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersGL(W, H); + // 2TON: кружки с уровнями на пиках тонов/IMD3 (активирует MainForm при + // 2TON+TX+DUP — реальный сигнал после PA виден на RX-спектре) + if FIMDActive then DrawIMDMarkersGL(W, H, DBmax, InvRange); DrawADCOverlay(W, H); DrawCachedOverlays(W, H); diff --git a/VfoOverlay.pas b/VfoOverlay.pas index 5177417..d7f1827 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -17,18 +17,22 @@ unit VfoOverlay; interface uses - Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme; + Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme, + RadioModes; const VFO_OVERLAY_ALPHA = 210; OVL_W = 280; // ширина карточки // Индексы совпадают с MODE_* контроллера (WDSPEngine.pas). - OVL_MODE_COUNT = 12; + OVL_MODE_COUNT = RadioModes.MODE_COUNT; + OVL_MODE_FM = RadioModes.MODE_FM; + OVL_MODE_DMR = RadioModes.MODE_DMR; + OVL_MODE_FMRAW = RadioModes.MODE_FMRAW; OVL_MODE_COLS = 4; OVL_MODE_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS; OVL_MODE_NAMES: array[0..OVL_MODE_COUNT-1] of string = ( - 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); + 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FMRAW'); // Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для // бейджа-карточки во флаге и для буквы на полосе фильтра в спектре. @@ -307,6 +311,8 @@ const WFM_LBL: array[0..2] of string = ('200K', '160K', '120K'); DIGI_BW: array[0..5] of Integer = (300, 600, 1200, 1800, 2400, 3000); DIGI_LBL: array[0..5] of string = ('300','600','1.2K','1.8K','2.4K','3.0K'); + RAW_BW: array[0..4] of Integer = (6250,10000,12500,20000,25000); + RAW_LBL: array[0..4] of string = ('6.25K','10K','12.5K','20K','25K'); DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73); @@ -400,13 +406,14 @@ end; function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer; begin case ModeIdx of - 0,1: Result := SSB_BW[FilterIdx]; - 2: Result := DSB_BW[FilterIdx]; - 3,4: Result := CW_BW[FilterIdx]; - 5: Result := FM_BW[FilterIdx]; - 8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля - 10: Result := WFM_BW[FilterIdx]; - 11: Result := 12500; + MODE_LSB, MODE_USB: Result := SSB_BW[FilterIdx]; + MODE_DSB: Result := DSB_BW[FilterIdx]; + MODE_CWL, MODE_CWU: Result := CW_BW[FilterIdx]; + OVL_MODE_FM: Result := FM_BW[FilterIdx]; + MODE_DIGU, MODE_DIGL: Result := DIGI_BW[FilterIdx]; // полоса от нуля + MODE_WFM: Result := WFM_BW[FilterIdx]; + OVL_MODE_DMR: Result := 12500; + OVL_MODE_FMRAW: Result := RAW_BW[FilterIdx]; else Result := AM_BW[FilterIdx]; end; end; @@ -414,13 +421,14 @@ end; function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string; begin case ModeIdx of - 0,1: Result := SSB_LBL[FilterIdx]; - 2: Result := DSB_LBL[FilterIdx]; - 3,4: Result := CW_LBL[FilterIdx]; - 5: Result := FM_LBL[FilterIdx]; - 8,9: Result := DIGI_LBL[FilterIdx]; - 10: Result := WFM_LBL[FilterIdx]; - 11: Result := '12.5k'; + MODE_LSB, MODE_USB: Result := SSB_LBL[FilterIdx]; + MODE_DSB: Result := DSB_LBL[FilterIdx]; + MODE_CWL, MODE_CWU: Result := CW_LBL[FilterIdx]; + OVL_MODE_FM: Result := FM_LBL[FilterIdx]; + MODE_DIGU, MODE_DIGL: Result := DIGI_LBL[FilterIdx]; + MODE_WFM: Result := WFM_LBL[FilterIdx]; + OVL_MODE_DMR: Result := '12.5k'; + OVL_MODE_FMRAW: Result := RAW_LBL[FilterIdx]; else Result := AM_LBL[FilterIdx]; end; end; @@ -428,9 +436,10 @@ end; function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer; begin case ModeIdx of - 5: Result := 2; // FM: NFM/FM - 10: Result := 3; // WFM: 200K/160K/120K - 11: Result := 0; // DMR: fixed 12.5 kHz, no redundant selector + OVL_MODE_FM: Result := 2; // FM: NFM/FM + MODE_WFM: Result := 3; // WFM: 200K/160K/120K + OVL_MODE_DMR: Result := 0; // DMR: fixed 12.5 kHz, no redundant selector + OVL_MODE_FMRAW: Result := 5; // FM RAW decoder channel widths else Result := 6; end; end; @@ -438,7 +447,7 @@ end; function TVfoOverlay.TxSelectionAllowed: Boolean; begin // Main A remains the normal TX source. Additional DMR slices are RX-only. - Result := FIsMain or (FMode <> 11); + Result := FIsMain or not (FMode in [OVL_MODE_DMR, OVL_MODE_FMRAW]); end; { ---- Lifecycle ---- } @@ -447,7 +456,7 @@ constructor TVfoOverlay.Create(AOwner: TComponent); begin inherited Create(AOwner); ControlStyle := ControlStyle + [csOpaque]; - FMode := 1; + FMode := MODE_USB; FFilterBW := 2700; FVfoHz := 14200000; FSMeterDB := -121; @@ -457,6 +466,7 @@ begin FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000; FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000; FModeFilter[9] := 3000; FModeFilter[10] := 160000; + FModeFilter[11] := 12500; FModeFilter[12] := 12500; FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False; @@ -594,7 +604,7 @@ end; function TVfoOverlay.IsFM: Boolean; begin - Result := FMode = 5; // индекс FM в OVL_MODE_NAMES (= MODE_FM контроллера) + Result := FMode = OVL_MODE_FM; end; function TVfoOverlay.PanelContentH: Integer; @@ -1039,7 +1049,7 @@ begin // Краткий статус дополнительного DMR-слайса. Он обновляется только при // устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру // перезаливаться на каждом burst. - if (not FIsMain) and (FMode = 11) and (FDMRText <> '') then + if (not FIsMain) and (FMode = OVL_MODE_DMR) and (FDMRText <> '') then begin DMRStr := FDMRText; DMRX := X + 22 + C.TextWidth(BWStr) + 8; @@ -1276,7 +1286,7 @@ begin begin FModeFilter[FMode] := FFilterBW; FMode := NewMode; - if FMode = 11 then FFilterBW := 12500 + if FMode = OVL_MODE_DMR then FFilterBW := 12500 else FFilterBW := FModeFilter[FMode]; Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3) RequestInvalidate; @@ -1449,19 +1459,19 @@ var OldMode: Integer; begin OldMode := FMode; - FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode)); + FMode := EnsureRange(AMode, MODE_MIN, MODE_MAX); FFilterBW := ABW; FModeFilter[FMode] := ABW; FVfoHz := AVfoHz; FSMeterDB := ASMeterDB; - if FMode <> 11 then + if FMode <> OVL_MODE_DMR then begin FDMRText := ''; FDMRSynced := False; FDMRCandidateText := ''; FDMRCandidateSince := 0; end - else if OldMode <> 11 then + else if OldMode <> OVL_MODE_DMR then begin FDMRText := 'DMR...'; FDMRSynced := False; @@ -1555,7 +1565,7 @@ procedure TVfoOverlay.UpdateDMRStatus(const AText: string; Synced: Boolean); var NowTick, HoldMs: QWord; begin - if FMode <> 11 then Exit; + if FMode <> OVL_MODE_DMR then Exit; if (FDMRText = AText) and (FDMRSynced = Synced) then begin FDMRCandidateText := ''; diff --git a/WDSP.pas b/WDSP.pas index 21e57c7..c47992d 100644 --- a/WDSP.pas +++ b/WDSP.pas @@ -281,6 +281,8 @@ procedure SetPSPinMode(channel: Integer; pin: Integer); cdecl; external WDSP_LIB procedure SetPSMapMode(channel: Integer; map: Integer); cdecl; external WDSP_LIB; procedure SetPSStabilize(channel: Integer; stbl: Integer); cdecl; external WDSP_LIB; procedure SetPSIntsAndSpi(channel: Integer; ints: Integer; spi: Integer); cdecl; external WDSP_LIB; +// eu2av: медианный (MAD) outlier-фильтр перед кубическим фитом calcc; 0 = выкл. +procedure SetPSOutlierSigma(channel: Integer; sigma: Double); cdecl; external WDSP_LIB; // --------------------------------------------------------------------------- // cblock.c diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 6a20289..8a79bfa 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -25,7 +25,7 @@ interface uses Classes, SysUtils, Math, SyncObjs, - BeaconDecoder, + BeaconDecoder, RadioModes, WDSP; const @@ -49,18 +49,22 @@ const // Наши индексы режимов (совпадают с кнопками MainForm). // Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json, // band-cache, CAT (ZZMD/ZZMV) и web-API. - MODE_LSB = 0; - MODE_USB = 1; - MODE_DSB = 2; - MODE_CWL = 3; - MODE_CWU = 4; - MODE_FM = 5; - MODE_AM = 6; - MODE_SAM = 7; - MODE_DIGU = 8; - MODE_DIGL = 9; - MODE_WFM = 10; - MODE_DMR = 11; // DMR receive: flat WDSP discriminator -> DMRDecoder + MODE_LSB = RadioModes.MODE_LSB; + MODE_USB = RadioModes.MODE_USB; + MODE_DSB = RadioModes.MODE_DSB; + MODE_CWL = RadioModes.MODE_CWL; + MODE_CWU = RadioModes.MODE_CWU; + MODE_FM = RadioModes.MODE_FM; + MODE_AM = RadioModes.MODE_AM; + MODE_SAM = RadioModes.MODE_SAM; + MODE_DIGU = RadioModes.MODE_DIGU; + MODE_DIGL = RadioModes.MODE_DIGL; + MODE_WFM = RadioModes.MODE_WFM; + MODE_DMR = RadioModes.MODE_DMR; + MODE_FMRAW = RadioModes.MODE_FMRAW; + MODE_MIN = RadioModes.MODE_MIN; + MODE_MAX = RadioModes.MODE_MAX; + MODE_COUNT = RadioModes.MODE_COUNT; // Минимальная длина FIR у WDSP: create_* поднимают fircore с // nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1, @@ -375,6 +379,32 @@ 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; + FPSOutlierSigma: Double; // eu2av MAD-фильтр перед фитом; 0 = выкл. // Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении. // Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS. @@ -418,6 +448,8 @@ type FTXWfAvgTimeMS: Double; FMode: Integer; + FTXMode: Integer; // may differ from RX when a slice is TX source + FRawFMDeviation: Double; FSMeter: Double; FFilterLow: Integer; FFilterHigh: Integer; @@ -454,7 +486,11 @@ type procedure ApplyDSPRate(Mode: Integer); procedure ApplyRXWFMToChan(Chan: Integer); procedure ApplyRXDMRToChan(Chan: Integer); + procedure ApplyRXRawToChan(Chan: Integer); procedure ApplyWFMSettings; + // Mode-dependent TX overlay. FM RAW uses the flat WFM modulator and + // bypasses every speech processor without overwriting the saved TX setup. + procedure ApplyTXModeSettings(Mode: Integer); // --- Мультислайсы --- function FindSliceIdx(Id: Integer): Integer; // -1 если нет function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан) @@ -501,6 +537,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 @@ -589,6 +626,7 @@ type procedure SetSNB(Enable: Boolean); procedure SetANF(Enable: Boolean); procedure SetRXFMDeviation(Deviation: Double); + procedure SetRawFMDeviation(Deviation: Double); procedure SetFMSquelch(Enable: Boolean; Level: Integer); procedure SetVolume(Vol: Double); procedure SetMute(Mute: Boolean); @@ -606,6 +644,33 @@ 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); + // Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX. + // Для openHPSDR P2 всегда включён; для остальных бэкендов выключен. + procedure SetTXCFIR(Enabled: Boolean); + + // ---- 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; + OutlierSigma: Double); // Расширенное TX управление (применяются мгновенно если канал открыт) procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer); @@ -942,6 +1007,7 @@ begin // de-emphasis. Those filters collapse the four DMR discriminator levels. // WFMD is a plain quadrature discriminator and permits flat data audio. MODE_DMR: Result := WDSP_RX_WFM; + MODE_FMRAW: Result := WDSP_RX_WFM; else Result := WDSP_USB; end; end; @@ -949,7 +1015,9 @@ end; function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer; // txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM. begin - if Mode = MODE_WFM then Result := WDSP_TX_WFM + // The narrow FM modulator has mandatory speech pre-emphasis. FM RAW uses + // wfmmod even at NFM deviation because that path has an explicit flat mode. + if Mode in [MODE_WFM, MODE_FMRAW] then Result := WDSP_TX_WFM else if Mode = MODE_DMR then Result := WDSP_FM else Result := ModeToWDSP(Mode); end; @@ -1073,6 +1141,15 @@ begin SetRXAWFMDeemphRun (Chan, 0); end; +procedure TWDSPEngine.ApplyRXRawToChan(Chan: Integer); +begin + // Decoder-grade discriminator: no broadcast de-emphasis or limiter. + SetRXAWFMDeviation (Chan, FRawFMDeviation); + SetRXAWFMAFFilter (Chan, 20.0, 7000.0); + SetRXAWFMDeemphRun (Chan, 0); + SetRXAWFMLimRun (Chan, 0); +end; + procedure TWDSPEngine.ApplyWFMSettings; begin ApplyRXWFMToChan(RXA_CHAN); @@ -1082,6 +1159,36 @@ begin SetTXAWFMPreEmphTau (TXA_CHAN, WFM_TAU_DE_US); end; +procedure TWDSPEngine.ApplyTXModeSettings(Mode: Integer); +begin + if not FInitialized then Exit; + if Mode = MODE_FMRAW then + begin + // Decoder/modem-grade FM input. A full-scale PCM peak produces the + // selected peak deviation. Keep only a broad linear-phase AF bandpass; + // APRS and other external modems must not see voice EQ, dynamics or emphasis. + SetTXAWFMDeviation (TXA_CHAN, FRawFMDeviation); + SetTXAWFMAFFreqs (TXA_CHAN, 20.0, 7000.0); + SetTXAWFMPreEmphRun (TXA_CHAN, 0); + SetTXABandpassFreqs (TXA_CHAN, 20.0, 7000.0); + SetTXABandpassRun (TXA_CHAN, 1); + SetTXAPanelGain1 (TXA_CHAN, 1.0); + SetTXACompressorRun (TXA_CHAN, 0); + SetTXALevelerSt (TXA_CHAN, 0); + SetTXAALCSt (TXA_CHAN, 0); + SetTXAPHROTRun (TXA_CHAN, 0); + SetTXAEQRun (TXA_CHAN, 0); + SetTXACTCSSRun (TXA_CHAN, 0); + end + else + begin + // Restore the user's normal voice/data chain after leaving FM RAW. + SetTXABandpassRun(TXA_CHAN, 1); + ApplyTXChainSettings; + if Mode = MODE_WFM then ApplyWFMSettings; + end; +end; + function TWDSPEngine.ClampToNyquist(Hz, Rate: Integer): Integer; var Limit: Integer; begin @@ -1104,6 +1211,7 @@ begin MODE_DIGL: SetFilter(-3000, 0); MODE_WFM: SetFilter(-80000, 80000); MODE_DMR: SetFilter(-6250, 6250); + MODE_FMRAW: SetFilter(-6250, 6250); end; end; @@ -1129,6 +1237,7 @@ begin FInitialized := False; FAnalyzerOpen := False; FMode := MODE_USB; + FTXMode := MODE_USB; FFilterLow := 100; FFilterHigh := 2400; FAGCMode := agcMedium; @@ -1197,7 +1306,21 @@ 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) + FPSOutlierSigma := 0.0; // выкл. до ApplyPSSettingsToDSP (авто по плате) FMonitorRXDuringTX := False; FPostTXMuteUntil := 0; FActiveDisplayID := RX_DISP_ID; @@ -1244,6 +1367,7 @@ begin FTXEQFreqs[10] := 3500; FTXAMCarrier := 0.5; FTXFMDeviation := 5000.0; + FRawFMDeviation := 2500.0; FTXFMLowCut := 300; FTXFMHighCut := 3000; FTXFMEmphPos := 1; @@ -1607,7 +1731,8 @@ begin // изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок). ); - SetTXAMode(TXA_CHAN, ModeToWDSPTX(FMode)); + FTXMode := FMode; + SetTXAMode(TXA_CHAN, ModeToWDSPTX(FTXMode)); SetTXAPanelRun(TXA_CHAN, 1); // PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I. SetTXAPanelSelect(TXA_CHAN, 2); @@ -1661,6 +1786,10 @@ var begin if not FInitialized then Exit; + // PureSignal: снять подачу feedback ДО закрытия TXA — pscc зовётся из + // сетевого потока напрямую и не должен попасть в закрываемый канал. + FPSArmed := False; + // Останавливаем DSP поток перед закрытием WDSP каналов if Assigned(FDSPThread) then begin @@ -2246,6 +2375,7 @@ begin SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan); if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan); + if FSlices[Idx].Mode = MODE_FMRAW then ApplyRXRawToChan(Chan); SetRXAShiftRun(Chan, 1); SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), @@ -2258,7 +2388,7 @@ begin ApplyANFToChan(Chan, FSlices[Idx].ANFOn); ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel); // Decoder input must not disappear or change scale with speaker volume. - if FSlices[Idx].Mode = MODE_DMR then + if FSlices[Idx].Mode in [MODE_DMR, MODE_FMRAW] then SetRXAPanelGain1(Chan, 1.0) else SetRXAPanelGain1(Chan, FSlices[Idx].Volume); @@ -2308,7 +2438,7 @@ begin if not (FSlices[s].Active and FSlices[s].Opened) then Continue; if FSlices[s].PanId <> PanId then Continue; // Muting a DMR speaker must not make its decoder lose synchronization. - if FSlices[s].Muted and (FSlices[s].Mode <> MODE_DMR) then Continue; + if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW]) then Continue; // Свежая копия входного IQ — fexchange0 портит вход in-place. for i := 0 to N - 1 do begin @@ -2324,12 +2454,12 @@ begin FSliceOutL[k] := FSliceOut[k * 2]; FSliceOutR[k] := FSliceOut[k * 2 + 1]; end; - if FSlices[s].Mode = MODE_DMR then + if FSlices[s].Mode in [MODE_DMR, MODE_FMRAW] then begin if Assigned(FOnSliceDemodAudio) then FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize); - Continue; // never route the raw 4FSK discriminator to the speaker + Continue; // decoder-grade signal uses the independent pre-volume route end; if Assigned(FOnSliceAudio) then begin @@ -2550,7 +2680,8 @@ begin SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode)); if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan); if Mode = MODE_DMR then ApplyRXDMRToChan(FSlices[idx].Chan); - if Mode = MODE_DMR then + if Mode = MODE_FMRAW then ApplyRXRawToChan(FSlices[idx].Chan); + if Mode in [MODE_DMR, MODE_FMRAW] then SetRXAPanelGain1(FSlices[idx].Chan, 1.0) else SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume); @@ -2599,7 +2730,7 @@ begin try idx := FindSliceIdx(Id); if idx < 0 then Exit; FSlices[idx].Volume := Vol; - if FSlices[idx].Opened and (FSlices[idx].Mode <> MODE_DMR) then + if FSlices[idx].Opened and not (FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW]) then SetRXAPanelGain1(FSlices[idx].Chan, Vol); finally FSliceLock.Leave; @@ -2732,6 +2863,7 @@ end; procedure TWDSPEngine.SetMode(Mode: Integer); begin FMode := Mode; + FTXMode := Mode; if not FInitialized then Exit; // Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd), // а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate. @@ -2744,7 +2876,9 @@ begin SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION); SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0); SetRXAWFMDeemphRun (RXA_CHAN, 0); - end; + end + else if Mode = MODE_FMRAW then ApplyRXRawToChan(RXA_CHAN); + ApplyTXModeSettings(FTXMode); // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI end; @@ -2980,12 +3114,32 @@ end; procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double); begin if not FInitialized then Exit; - if FMode = MODE_DMR then + if FMode = MODE_FMRAW then SetRawFMDeviation(Deviation) + else if FMode = MODE_DMR then SetRXAWFMDeviation(RXA_CHAN, Deviation) else SetRXAFMDeviation(RXA_CHAN, Deviation); end; +procedure TWDSPEngine.SetRawFMDeviation(Deviation: Double); +var i: Integer; +begin + FRawFMDeviation := EnsureRange(Deviation, 500.0, 10000.0); + if not FInitialized then Exit; + if FMode = MODE_FMRAW then SetRXAWFMDeviation(RXA_CHAN, FRawFMDeviation); + if FTXMode = MODE_FMRAW then SetTXAWFMDeviation(TXA_CHAN, FRawFMDeviation); + // One calibration value applies to every active FM RAW decoder output. + FSliceLock.Enter; + try + for i := 0 to MAX_SLICES - 1 do + if FSlices[i].Active and FSlices[i].Opened and + (FSlices[i].Mode = MODE_FMRAW) then + SetRXAWFMDeviation(FSlices[i].Chan, FRawFMDeviation); + finally + FSliceLock.Leave; + end; +end; + procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer); var Threshold: Double; begin @@ -3020,13 +3174,18 @@ end; procedure TWDSPEngine.SetTXMode(Mode: Integer); begin + FTXMode := Mode; if not FInitialized then Exit; - SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode)); + SetTXAMode(TXA_CHAN, ModeToWDSPTX(FTXMode)); + ApplyTXModeSettings(FTXMode); end; procedure TWDSPEngine.SetTXFilter(Low, High: Integer); begin if not FInitialized then Exit; + // In FM RAW this is an RF-channel-width update mirrored from the RX UI, + // not an audio passband request. The flat modem AF passband is fixed above. + if FTXMode = MODE_FMRAW then Exit; SetTXABandpassFreqs(TXA_CHAN, Low, High); end; @@ -3139,6 +3298,41 @@ 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.SetTXCFIR(Enabled: Boolean); +begin + if not FInitialized then Exit; + SetTXACFIRRun(TXA_CHAN, Ord(Enabled)); +end; + procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean); begin // Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID @@ -3179,9 +3373,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 +3529,120 @@ 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); + SetPSOutlierSigma(TXA_CHAN, FPSOutlierSigma); +end; + +procedure TWDSPEngine.PSSetParams(MoxDelay, LoopDelay: Double; TXDelayNs: Integer; + RelaxPtol: Boolean; Pin, Map, Stabilize: Boolean; Ints, Spi: Integer; + HWPeak: Double; OutlierSigma: 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; + if OutlierSigma >= 0 then FPSOutlierSigma := OutlierSigma; + 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/WebAdapter.pas b/WebAdapter.pas index c827ca2..d92b5ce 100644 --- a/WebAdapter.pas +++ b/WebAdapter.pas @@ -145,7 +145,7 @@ begin FSyncInt := Mode; FController.Invoke(@SyncMode); end; procedure TWebAdapter.SyncMode; begin - if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit; + if (FSyncInt < MODE_MIN) or (FSyncInt > MODE_MAX) then Exit; FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode end; diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 900bca1..360d598 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -193,7 +193,7 @@ begin '' + '