Merge feature/puresignal: PureSignal-предыскажение для openHPSDR P2 (Orion MkII, паритет с Thetis)

This commit is contained in:
2026-07-18 10:44:01 +03:00
22 changed files with 2287 additions and 303 deletions
+156 -79
View File
@@ -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
Row := PByte(Target.ScanLine[Y]);
if Row = nil then Continue;
Inc(Row, RR.Left * 4);
for X := RR.Left to RR.Right - 1 do
for Y := Max(0, Y1) to Min(BOX_H, Y2) - 1 do
begin
I := (Y * BoxW + Max(0, X1)) * 4;
for X := Max(0, X1) to Min(BoxW, X2) - 1 do
begin
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}
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);
MaxC := Max(Src[1], Max(Src[2], Src[3]));
{$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);
MaxC := Max(Src[0], Max(Src[1], Src[2]));
{$ENDIF}
Inc(Row, 4);
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
Target.EndUpdate(False);
TxtB.EndUpdate(False);
end;
end;
procedure DrawAlertOverlay(Target: TBitmap; C: TCanvas; W, H: Integer;
const Title, Detail: string);
const
PAD = 12;
INNER_X = 12;
ACCENT_W = 4;
var
R, AccentR: TRect;
TW, DW, BoxW, BoxH: Integer;
begin
if (Target = nil) or (C = nil) then Exit;
if (W < 220) or (H < 60) then Exit;
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;
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);
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); // акцент
BoxW := Max(TW, DW) + INNER_X * 2 + ACCENT_W + 8;
BoxH := 42;
R := Rect(W - PAD - BoxW, PAD, W - PAD, PAD + BoxH);
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
B.Free;
end;
end;
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);
procedure BlendAlertImage(const Img: TAlertImage; Base: PByte;
BPL, RawW, RawH, X, Y: Integer);
var
SX, SY, DX, DY, I, A, InvA: Integer;
P: PByte;
begin
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.
+3 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+14 -14
View File
@@ -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;
+8 -5
View File
@@ -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);
+5 -5
View File
@@ -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
+86 -7
View File
@@ -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;
+221 -18
View File
@@ -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
MOXAllowed := FController.FRunning and
(FController.ActiveTXMode <> MODE_DMR);
BtnMOX.Enabled := Allowed;
BtnTUN.Enabled := Allowed;
// 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]
+453
View File
@@ -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.
+21
View File
@@ -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.
+501 -24
View File
@@ -74,6 +74,7 @@ type
rfCenterFreq, rfSpan, rfSampleRate, rfZoom,
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
rfRunning, rfConnected, rfTransmitting, rfTuning,
rfPureSignal, rfTwoTone, // PureSignal вкл/статус; 2TON вкл/выкл
rfNR, rfNB, rfSNB, rfANF,
rfWfAGC, rfWfNF, rfDither, rfRandom,
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
@@ -289,6 +290,22 @@ type
FRunning: Boolean;
FTransmitting: Boolean;
FTuning: Boolean;
// ---- PureSignal ----
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
FPSSingleCal: Boolean; // запрошена одиночная калибровка
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick)
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
FPSAASaveSingle: Integer;
FPSAADeltaDB: Integer;
FPSInfo: array[0..15] of Integer; // кэш GetPSInfo (UI-поток)
FPSInfoPrev: array[0..15] of Integer;
FPSMaxTX: Double; // кэш GetPSMaxTX
// Feedback-тракт вооружён (сеть DDC0/1 + pscc в движке). Пишет UI-поток
// (PureSignalTick/PSSetFeedbackPath), читает сетевой (OnDDCIQ).
FPSFeedbackOn: Boolean;
FTwoTone: Boolean; // 2TON активен
FSplitTxB: Boolean;
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
@@ -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,9 +2547,13 @@ 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
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.
@@ -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;
+30
View File
@@ -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.
+90 -7
View File
@@ -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);
+185 -31
View File
@@ -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 (2f1f2, 2f2f1). Обновляет 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;
// ────────────────────────────────────────────────────────────────────────────
+105 -26
View File
@@ -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);
+39 -29
View File
@@ -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 := '';
+2
View File
@@ -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
+334 -26
View File
@@ -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);
+1 -1
View File
@@ -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;
+15 -12
View File
@@ -193,7 +193,7 @@ begin
'<select id="modGrpSel">' +
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
'<option value="fm">FM</option><option value="am">AM</option>' +
'<option value="digi">DIGI</option><option value="dmr">DMR</option>' +
'<option value="digi">DIGI</option><option value="dmr">DMR</option><option value="raw">FM RAW</option>' +
'</select>' +
'<select id="stepSel">' +
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
@@ -299,14 +299,15 @@ begin
'function updRfAgc(){const b=q("rfagcBtn");if(!b)return;b.style.display=cur.rf_agc_visible?"":"none";b.textContent="RF AGC "+rfagcModeName(cur.rx_gain_mode);if(rfagcEls)rfagcRefresh();}' +
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM","DMR"];' +
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,10],am:[6,7],digi:[8,9],dmr:[11,11]};' +
'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am","digi","digi","fm","dmr"];' +
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM","DMR","FM RAW"];' +
'const M_LSB=MODE_N.indexOf("LSB"),M_USB=MODE_N.indexOf("USB"),M_DSB=MODE_N.indexOf("DSB"),M_CWL=MODE_N.indexOf("CWL"),M_CWU=MODE_N.indexOf("CWU"),M_FM=MODE_N.indexOf("FM"),M_AM=MODE_N.indexOf("AM"),M_SAM=MODE_N.indexOf("SAM"),M_DIGU=MODE_N.indexOf("DIGU"),M_DIGL=MODE_N.indexOf("DIGL"),M_WFM=MODE_N.indexOf("WFM"),M_DMR=MODE_N.indexOf("DMR"),M_FMRAW=MODE_N.indexOf("FM RAW");' +
'const GRP_MAP={ssb:[M_LSB,M_USB],cw:[M_CWL,M_CWU],fm:[M_FM,M_WFM],am:[M_AM,M_SAM],digi:[M_DIGU,M_DIGL],dmr:[M_DMR,M_DMR],raw:[M_FMRAW,M_FMRAW]};' +
'const MODE_TO_GRP=[];Object.entries(GRP_MAP).forEach(([g,ms])=>ms.forEach(m=>MODE_TO_GRP[m]=g));MODE_TO_GRP[M_DSB]="ssb";' +
'function modGrpFromMode(m){return MODE_TO_GRP[m]||"ssb";}' +
'function modGrp(){return q("modGrpSel").value||"ssb";}' +
'function modTglTarget(){const grp=GRP_MAP[modGrp()];const m=cur.mode||0;return(m===grp[0])?grp[1]:grp[0];}' +
'function updModTglLabel(){const b=q("modTglBtn");if(!b)return;const grp=GRP_MAP[modGrp()];const m=cur.mode||0;const show=(m===grp[0]||m===grp[1])?m:grp[0];b.textContent=MODE_N[show];}' +
'function syncModGrp(m){const g=MODE_TO_GRP[m]||"ssb";const s=q("modGrpSel");if(s&&s.value!==g)s.value=g;const f=q("fnpBtn");if(f)f.style.display=(m===11)?"none":"";updModTglLabel();}' +
'function syncModGrp(m){const g=MODE_TO_GRP[m]||"ssb";const s=q("modGrpSel");if(s&&s.value!==g)s.value=g;const f=q("fnpBtn");if(f)f.style.display=(m===M_DMR)?"none":"";updModTglLabel();}' +
'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' +
'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' +
'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
@@ -320,6 +321,7 @@ begin
'fm:["NFM","FM"],' +
'wfm:["200k","160k","120k"],' +
'dmr:["12.5k"],' +
'raw:["6.25k","10k","12.5k","20k","25k"],' +
'digi:["3.0k","2.7k","2.4k","2.1k","1.8k","1.5k","1.2k","900","600","300"]};' +
'var FNP_BW={ssb:[5000,4400,3800,3300,2900,2700,2400,2100,1800,1000],' +
'dsb:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' +
@@ -328,13 +330,14 @@ begin
'fm:[11000,16000],' +
'wfm:[200000,160000,120000],' +
'dmr:[12500],' +
'raw:[6250,10000,12500,20000,25000],' +
'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' +
'var fnpOv=null;' +
'function fnpGrp(){var m=cur.mode|0;return(m===3||m===4)?"cw":(m===5)?"fm":(m===10)?"wfm":(m===11)?"dmr":(m===8||m===9)?"digi":(m===2)?"dsb":(m===6||m===7)?"am":"ssb";}' +
'function fnpGrp(){var m=cur.mode|0;return(m===M_CWL||m===M_CWU)?"cw":(m===M_FM)?"fm":(m===M_WFM)?"wfm":(m===M_DMR)?"dmr":(m===M_FMRAW)?"raw":(m===M_DIGU||m===M_DIGL)?"digi":(m===M_DSB)?"dsb":(m===M_AM||m===M_SAM)?"am":"ssb";}' +
'function closeFnp(){if(fnpOv){fnpOv.remove();fnpOv=null;}document.removeEventListener("click",fnpOutside);}' +
'function fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
'function showFnp(){' +
'if((cur.mode|0)===11)return;' +
'if((cur.mode|0)===M_DMR)return;' +
'if(fnpOv){closeFnp();return;}' +
'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' +
'fnpOv=document.createElement("div");' +
@@ -791,7 +794,7 @@ begin
'const nowTx=!!cur.transmitting;if(nowTx!==prevTx){prevTx=nowTx;if(nowTx)startTXMic();else stopTXMic();}}' +
'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' +
'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===0)return[-bw,-100];if(m===1)return[100,bw];return[-bw/2,bw/2];}' +
'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===M_LSB)return[-bw,-100];if(m===M_USB)return[100,bw];return[-bw/2,bw/2];}' +
'function xToHz(x,w){return localCenter+((x/Math.max(1,w))-.5)*localSpan;}' +
'function hzToX(f,w){return((f-localCenter)/localSpan+.5)*w;}' +
@@ -817,7 +820,7 @@ begin
'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' +
'const st=localCenter-localSpan/2;' +
'const isFM=(cur.mode===5||cur.mode===11),fmStep=FM_STEP_HZ[fmStepIdx];' +
'const isFM=(cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW),fmStep=FM_STEP_HZ[fmStepIdx];' +
'if(isFM&&fmStep>0){' +
'const pxPerStep=w*fmStep/localSpan;' +
'const labelMult=Math.max(1,Math.ceil((rctx.measureText("000.000").width+6)/pxPerStep));' +
@@ -832,7 +835,7 @@ begin
'function drawSpec(bins){const w=specCv.clientWidth,h=specCv.clientHeight;sctx.fillStyle="#050a05";sctx.fillRect(0,0,w,h);' +
'for(let i=1;i<5;i++){const y=i*h/5;sctx.strokeStyle="#121e12";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(0,y);sctx.lineTo(w,y);sctx.stroke();}' +
'if((cur.mode===5||cur.mode===11)&&FM_STEP_HZ[fmStepIdx]>0){' +
'if((cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW)&&FM_STEP_HZ[fmStepIdx]>0){' +
'const fmSt=FM_STEP_HZ[fmStepIdx],st2=localCenter-localSpan/2;' +
'const pxSt=w*fmSt/localSpan,gm=Math.max(1,Math.ceil(4/pxSt));' +
'const fn=Math.ceil(st2/fmSt);' +
@@ -875,7 +878,7 @@ begin
'if(smCv.width!==(sw*dpr|0)||smCv.height!==(sh*dpr|0)){smCv.width=sw*dpr|0;smCv.height=sh*dpr|0;mctx.setTransform(dpr,0,0,dpr,0,0);}' +
'if(wfBuf.width!==wfCv.clientWidth||wfBuf.height!==wfCv.clientHeight){wfBuf.width=wfCv.clientWidth;wfBuf.height=wfCv.clientHeight;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,wfBuf.width,wfBuf.height);}}' +
'function syncStepSel(){const isFM=(cur.mode===5||cur.mode===11);const sel=q("stepSel");if(!sel)return;' +
'function syncStepSel(){const isFM=(cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW);const sel=q("stepSel");if(!sel)return;' +
'if(isFM&&!inFMStep){lastHFStep=+sel.value||100;sel.innerHTML="";' +
'FM_STEP_LBL.forEach((l,i)=>{const o=document.createElement("option");o.value=FM_STEP_HZ[i];o.textContent=l;sel.appendChild(o);});' +
'sel.value=FM_STEP_HZ[fmStepIdx];' +
@@ -937,7 +940,7 @@ begin
'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' +
// клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули.
// Спектр остаётся на месте, сервер пришлёт center_hz через ~200ms и всё перецентрируется.
'const snap=((cur.mode===5||cur.mode===11)&&inFMStep)?FM_STEP_HZ[fmStepIdx]:100;' +
'const snap=((cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW)&&inFMStep)?FM_STEP_HZ[fmStepIdx]:100;' +
'const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/snap)*snap,vfoMin(),vfoMax());' +
'setActiveVfoHz(nf);' +
'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' +
+5 -5
View File
@@ -69,7 +69,7 @@ interface
uses
Classes, SysUtils, Math,
WebUtils, WsClient, WebPageHtml
WebUtils, WsClient, WebPageHtml, RadioModes
{$IFDEF WINDOWS}, Windows, WinSock2{$ELSE}, BaseUnix, Sockets{$ENDIF},
SyncObjs; // ← после платформенных юнитов: исключает конфликт идентификатора Create
@@ -560,7 +560,7 @@ begin
FStateLock := TCriticalSection.Create;
// Начальные значения состояния
FFreq := 14200000;
FMode := 1;
FMode := MODE_USB;
FFMStepIdx := 3; // 25 kHz default
FFilterBW:= 2700;
FAGCMode := 1;
@@ -1717,8 +1717,8 @@ end;
function TWebServer.BuildStateJson: string;
const
MODE_N: array[0..11] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR');
MODE_N: array[RadioModes.MODE_MIN..RadioModes.MODE_MAX] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FM RAW');
var
FS: TFormatSettings;
XvtrJson, DevJson, RateJson, BandJson: string;
@@ -1790,7 +1790,7 @@ begin
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
'"rx_mute_visible":%s,"rx_mute_on":%s}',
[FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod Length(MODE_N)],
FMode, MODE_N[EnsureRange(FMode, MODE_MIN, MODE_MAX)],
FFilterIdx, FFilterBW,
FAGCMode, FAGCTop,
FSpanHz, FCenterHz, FVolume,
+3 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/>
<BuildNr Value="275"/>
<BuildNr Value="276"/>
</VersionInfo>
<MacroValues Count="105">
<MacroValues Count="106">
<Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -125,6 +125,7 @@
<Macro103 Name="LCLWidgetType" Value="qt6"/>
<Macro104 Name="LCLWidgetType" Value="qt6"/>
<Macro105 Name="LCLWidgetType" Value="qt6"/>
<Macro106 Name="LCLWidgetType" Value="qt6"/>
</MacroValues>
<BuildModes>
<Item Name="Debug" Default="True"/>