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
+161 -84
View File
@@ -1,7 +1,15 @@
unit AlertOverlay; 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} {$IFDEF FPC}
@@ -11,105 +19,174 @@ unit AlertOverlay;
interface interface
uses uses
Graphics, Types, Math; Classes, Graphics, Types, Math;
procedure DrawAlertOverlay(Target: TBitmap; C: TCanvas; W, H: Integer; const
const Title, Detail: string); 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 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 var
Y, X, InvA: Integer; B: TBitmap;
Row: PByte; TitleW, DetailW, BoxW: Integer;
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;
InvA := 255 - Alpha; procedure FillBuf(X1, Y1, X2, Y2: Integer; R, G, Bl, A: Byte); // [X1..X2)
Target.BeginUpdate(False); var X, Y, I: Integer;
try begin
for Y := RR.Top to RR.Bottom - 1 do for Y := Max(0, Y1) to Min(BOX_H, Y2) - 1 do
begin begin
Row := PByte(Target.ScanLine[Y]); I := (Y * BoxW + Max(0, X1)) * 4;
if Row = nil then Continue; for X := Max(0, X1) to Min(BoxW, X2) - 1 do
Inc(Row, RR.Left * 4);
for X := RR.Left to RR.Right - 1 do
begin begin
{$IFDEF DARWIN} Img.RGBA[I] := R; Img.RGBA[I+1] := G;
Row[1] := Byte((Alpha * R + InvA * Row[1]) div 255); Img.RGBA[I+2] := Bl; Img.RGBA[I+3] := A;
Row[2] := Byte((Alpha * G + InvA * Row[2]) div 255); Inc(I, 4);
Row[3] := Byte((Alpha * B + InvA * Row[3]) div 255);
{$ELSE}
Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255);
Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255);
Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255);
{$ENDIF}
Inc(Row, 4);
end; end;
end; end;
end;
procedure BlendText(TxtB: TBitmap);
// Композит текстового слоя (белым на чёрном) в образ: покрытие = max(R,G,B),
// цвет по строке (заголовок/деталь), сложение по straight-alpha.
var
X, Y, I, MaxC: Integer;
Src: PByte;
CR, CG, CB: Byte;
AT, AB, AO: Double;
begin
TxtB.BeginUpdate(False);
try
for Y := 0 to BOX_H - 1 do
begin
Src := PByte(TxtB.ScanLine[Y]);
I := Y * BoxW * 4;
for X := 0 to BoxW - 1 do
begin
{$IFDEF DARWIN}
MaxC := Max(Src[1], Max(Src[2], Src[3]));
{$ELSE}
MaxC := Max(Src[0], Max(Src[1], Src[2]));
{$ENDIF}
if MaxC > 0 then
begin
if Y < 24 then begin CR := $FF; CG := $88; CB := $88; end // $008888FF
else begin CR := $E8; CG := $C8; CB := $C8; end; // $00C8C8E8
AT := MaxC / 255.0;
AB := Img.RGBA[I+3] / 255.0 * (1.0 - AT);
AO := AT + AB;
Img.RGBA[I] := Round((CR * AT + Img.RGBA[I] * AB) / AO);
Img.RGBA[I+1] := Round((CG * AT + Img.RGBA[I+1] * AB) / AO);
Img.RGBA[I+2] := Round((CB * AT + Img.RGBA[I+2] * AB) / AO);
Img.RGBA[I+3] := Round(AO * 255.0);
end;
Inc(Src, 4);
Inc(I, 4);
end;
end;
finally
TxtB.EndUpdate(False);
end;
end;
begin
Img.W := 0; Img.H := 0; Img.RGBA := nil;
B := TBitmap.Create;
try
B.PixelFormat := pf32bit;
B.SetSize(8, 8);
B.Canvas.Font.Name := 'Courier New';
B.Canvas.Font.Style := [fsBold];
B.Canvas.Font.Size := 9;
TitleW := B.Canvas.TextWidth(Title);
B.Canvas.Font.Style := [];
B.Canvas.Font.Size := 7;
DetailW := B.Canvas.TextWidth(Detail);
BoxW := Max(TitleW, DetailW) + INNER_X * 2 + ACCENT_W + 8;
SetLength(Img.RGBA, BoxW * BOX_H * 4);
Img.W := BoxW;
Img.H := BOX_H;
FillBuf(0, 0, BoxW, BOX_H, $A8, $10, $16, 118); // фон
FillBuf(0, 0, BoxW, 1, $C8, $38, $38, 255); // рамка
FillBuf(0, BOX_H - 1, BoxW, BOX_H, $C8, $38, $38, 255);
FillBuf(0, 0, 1, BOX_H, $C8, $38, $38, 255);
FillBuf(BoxW - 1, 0, BoxW, BOX_H, $C8, $38, $38, 255);
FillBuf(7, 7, 7 + ACCENT_W, BOX_H - 7, $F0, $28, $28, 255); // акцент
B.SetSize(BoxW, BOX_H);
B.Canvas.Brush.Color := clBlack;
B.Canvas.Brush.Style := bsSolid;
B.Canvas.FillRect(Rect(0, 0, BoxW, BOX_H));
B.Canvas.Brush.Style := bsClear;
B.Canvas.Font.Name := 'Courier New';
B.Canvas.Font.Color := clWhite;
B.Canvas.Font.Style := [fsBold];
B.Canvas.Font.Size := 9;
B.Canvas.TextOut(INNER_X + ACCENT_W + 8, 7, Title);
B.Canvas.Font.Style := [];
B.Canvas.Font.Size := 7;
B.Canvas.TextOut(INNER_X + ACCENT_W + 8, 25, Detail);
BlendText(B);
finally finally
Target.EndUpdate(False); B.Free;
end; end;
end; end;
procedure DrawAlertOverlay(Target: TBitmap; C: TCanvas; W, H: Integer; procedure BlendAlertImage(const Img: TAlertImage; Base: PByte;
const Title, Detail: string); BPL, RawW, RawH, X, Y: Integer);
const
PAD = 12;
INNER_X = 12;
ACCENT_W = 4;
var var
R, AccentR: TRect; SX, SY, DX, DY, I, A, InvA: Integer;
TW, DW, BoxW, BoxH: Integer; P: PByte;
begin begin
if (Target = nil) or (C = nil) then Exit; if (Img.W <= 0) or (Base = nil) then Exit;
if (W < 220) or (H < 60) then Exit; for SY := 0 to Img.H - 1 do
begin
C.Font.Name := 'Courier New'; DY := Y + SY;
C.Font.Style := [fsBold]; if (DY < 0) or (DY >= RawH) then Continue;
C.Font.Size := 9; I := SY * Img.W * 4;
TW := C.TextWidth(Title); for SX := 0 to Img.W - 1 do
C.Font.Style := []; begin
C.Font.Size := 7; DX := X + SX;
DW := C.TextWidth(Detail); A := Img.RGBA[I + 3];
if (DX >= 0) and (DX < RawW) and (A > 0) then
BoxW := Max(TW, DW) + INNER_X * 2 + ACCENT_W + 8; begin
BoxH := 42; InvA := 255 - A;
R := Rect(W - PAD - BoxW, PAD, W - PAD, PAD + BoxH); P := Base + DY * BPL + DX * 4;
{$IFDEF DARWIN}
BlendRect(Target, R, $A8, $10, $16, 118); // ARGB: byte0=A, byte1=R, byte2=G, byte3=B
BlendRect(Target, Rect(R.Left, R.Top, R.Right, R.Top + 1), $FF, $68, $68, 80); 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);
C.Brush.Style := bsClear; P[3] := Byte((Img.RGBA[I+2] * A + P[3] * InvA) div 255);
C.Pen.Style := psSolid; P[0] := $FF;
C.Pen.Width := 1; {$ELSE}
C.Pen.Color := TColor($003838C8); // BGRA: byte0=B, byte1=G, byte2=R
C.Rectangle(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);
AccentR := Rect(R.Left + 7, R.Top + 7, R.Left + 7 + ACCENT_W, R.Bottom - 7); P[2] := Byte((Img.RGBA[I] * A + P[2] * InvA) div 255);
C.Brush.Style := bsSolid; P[3] := $FF;
C.Brush.Color := TColor($002828F0); {$ENDIF}
C.Pen.Style := psClear; end;
C.FillRect(AccentR); Inc(I, 4);
end;
C.Brush.Style := bsClear; end;
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);
end; end;
end. end.
+3 -2
View File
@@ -24,12 +24,13 @@ unit AndromedaPanel;
interface interface
uses uses
SysUtils, RadioController, AndromedaMap; SysUtils, RadioController, AndromedaMap, RadioModes;
const const
AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT) AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT)
AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра 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 type
TAndromedaSend = procedure(const S: string) of object; 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; procedure TCATAdapter.SyncMode;
begin begin
if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit; if (FSyncInt < MODE_MIN) or (FSyncInt > MODE_MAX) then Exit;
FController.SetMode(FSyncInt); FController.SetMode(FSyncInt);
end; end;
+14 -14
View File
@@ -24,19 +24,14 @@ unit CATEngine;
interface interface
uses uses
Classes, SysUtils, Math, SyncObjs; Classes, SysUtils, Math, SyncObjs, RadioModes;
const const
CAT_ERROR = '?;'; CAT_ERROR = '?;';
CAT_EXEC_ERROR = 'E;'; CAT_EXEC_ERROR = 'E;';
CAT_ID_NUM = '019'; // TS-2000 compatible CAT_ID_NUM = '019'; // TS-2000 compatible
// Mode mapping ewsdr → CAT (зеркало MODE_* из WDSPEngine) CAT_MODE_COUNT = RadioModes.MODE_COUNT;
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;
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны // FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
// совпадать: контроллер хранит FFMRptDir в этих же кодах). // совпадать: контроллер хранит FFMRptDir в этих же кодах).
@@ -392,6 +387,7 @@ begin
MODE_FM: Result := 4; MODE_FM: Result := 4;
MODE_WFM: Result := 4; // treated as FM MODE_WFM: Result := 4; // treated as FM
MODE_DMR: Result := 4; // DMR RF transport is narrow 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_AM: Result := 5;
MODE_SAM: Result := 5; // treated as AM MODE_SAM: Result := 5; // treated as AM
MODE_DIGU: Result := 6; // Kenwood FSK MODE_DIGU: Result := 6; // Kenwood FSK
@@ -1793,20 +1789,24 @@ end;
function TCATEngine.ZZML: string; function TCATEngine.ZZML: string;
// ZZML: mode list (returns all mode names) // 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; function TCATEngine.ZZMN(const s: string): string;
// ZZMN: mode name — returns mode name for given mode number. // ZZMN: mode name — returns mode name for given mode number.
// Индекс режима теперь двузначный (0..10), но односимвольный запрос // Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
// продолжаем принимать — старые клиенты шлют одну цифру. // продолжаем принимать — старые клиенты шлют одну цифру.
var k: Integer; var k: Integer;
names: array[0..CAT_MODE_COUNT-1] of string; names: array[0..CAT_MODE_COUNT-1] of string;
begin begin
names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL'; names[MODE_LSB]:='LSB'; names[MODE_USB]:='USB';
names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM'; names[MODE_DSB]:='DSB'; names[MODE_CWL]:='CWL';
names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM'; names[11]:='DMR'; 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 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]; Result := names[k];
end else Result := CAT_ERROR; end else Result := CAT_ERROR;
end; end;
@@ -1819,7 +1819,7 @@ begin
end; end;
function TCATEngine.ZZMV: string; 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; begin Result := Pad(SafeGetMode, 2); end;
function TCATEngine.ZZMY: string; function TCATEngine.ZZMY: string;
+8 -5
View File
@@ -8,11 +8,13 @@ unit ChannelStore;
interface interface
uses uses
SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils; SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
RadioModes;
const const
CHANNELS_FILE = 'channels.json'; CHANNELS_FILE = 'channels.json';
CHANNEL_MODE_MAX = 11; // LSB..DMR CHANNEL_MODE_MIN = RadioModes.MODE_MIN;
CHANNEL_MODE_MAX = RadioModes.MODE_MAX;
type type
TChannel = record TChannel = record
@@ -20,7 +22,7 @@ type
Name: string; Name: string;
RXFreq: Double; // Hz RXFreq: Double; // Hz
TXFreq: 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 RptDir: Integer; // 0=none 1=minus 2=plus
RptOffsetHz: Double; // Hz RptOffsetHz: Double; // Hz
CTCSSOn: Boolean; CTCSSOn: Boolean;
@@ -59,7 +61,7 @@ begin
Ch.Name := 'New Channel'; Ch.Name := 'New Channel';
Ch.RXFreq := 145500000.0; Ch.RXFreq := 145500000.0;
Ch.TXFreq := 145500000.0; Ch.TXFreq := 145500000.0;
Ch.Mode := 5; // FM Ch.Mode := MODE_FM;
Ch.RptDir := 0; Ch.RptDir := 0;
Ch.RptOffsetHz := 600000.0; Ch.RptOffsetHz := 600000.0;
Ch.CTCSSOn := False; Ch.CTCSSOn := False;
@@ -207,7 +209,8 @@ begin
Ch.Name := JS(O, 'name', 'Channel'); Ch.Name := JS(O, 'name', 'Channel');
Ch.RXFreq := JD(O, 'rx_freq', 145500000.0); Ch.RXFreq := JD(O, 'rx_freq', 145500000.0);
Ch.TXFreq := JD(O, 'tx_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.RptDir := EnsureRange(JI(O, 'rpt_dir', 0), 0, 2);
Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0); Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0);
Ch.CTCSSOn := JB(O, 'ctcss_on', False); Ch.CTCSSOn := JB(O, 'ctcss_on', False);
+5 -5
View File
@@ -14,13 +14,13 @@ uses
Forms, Controls, Graphics, StdCtrls, ExtCtrls, Forms, Controls, Graphics, StdCtrls, ExtCtrls,
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatButton, FlatCheckBox, FlatComboBox, FlatEdit,
FlatSpinEdit, FlatFloatSpinEdit, FlatListBox, FlatSpinEdit, FlatFloatSpinEdit, FlatListBox,
AppTheme, ChannelStore; AppTheme, ChannelStore, RadioModes;
const const
// Копии из MainForm: нужны для выпадающих списков // Копии из MainForm: нужны для выпадающих списков
CH_MODE_COUNT = 12; CH_MODE_COUNT = RadioModes.MODE_COUNT;
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string = 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_COUNT = 38;
CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = ( CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
@@ -459,7 +459,7 @@ begin
FEdName.Text := Ch.Name; FEdName.Text := Ch.Name;
FSpinRXFreq.Value := Ch.RXFreq / 1000000.0; FSpinRXFreq.Value := Ch.RXFreq / 1000000.0;
FSpinTXFreq.Value := Ch.TXFreq / 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(FBtnRptNone, Ch.RptDir = 0);
StyleBtn(FBtnRptMinus, Ch.RptDir = 1); StyleBtn(FBtnRptMinus, Ch.RptDir = 1);
StyleBtn(FBtnRptPlus, Ch.RptDir = 2); StyleBtn(FBtnRptPlus, Ch.RptDir = 2);
@@ -487,7 +487,7 @@ begin
Ch.Name := FEdName.Text; Ch.Name := FEdName.Text;
Ch.RXFreq := FSpinRXFreq.Value * 1000000.0; Ch.RXFreq := FSpinRXFreq.Value * 1000000.0;
Ch.TXFreq := FSpinTXFreq.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) // RptDir берётся из кнопок (Active)
if FBtnRptMinus.Active then Ch.RptDir := 1 if FBtnRptMinus.Active then Ch.RptDir := 1
else if FBtnRptPlus.Active then Ch.RptDir := 2 else if FBtnRptPlus.Active then Ch.RptDir := 2
+86 -7
View File
@@ -166,6 +166,13 @@ type
FAlexEnabled: Boolean; FAlexEnabled: Boolean;
FAlexConfig: TAlexSettings; // per-band antenna + routing config FAlexConfig: TAlexSettings; // per-band antenna + routing config
FStepAtten: Byte; // ADC0 step attenuator 0-31 dB 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 ---- // ---- XVTR (Transverter) state ----
// FXvtrEnable — True когда активен XVTR-диапазон. Используется как // FXvtrEnable — True когда активен XVTR-диапазон. Используется как
// XvtrActive в CalcAlex0 и для управления byte 1400 bit 0. // XvtrActive в CalcAlex0 и для управления byte 1400 bit 0.
@@ -243,6 +250,9 @@ type
procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double; procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double;
RateKHz: Word; ADCSource: Byte); override; RateKHz: Word; ADCSource: Byte); override;
procedure SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double); 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 SendDUCSpecific(const Pkt: TDUCSpecificPacket); override;
procedure SendHighPriority(const Pkt: THighPriorityPacket); override; procedure SendHighPriority(const Pkt: THighPriorityPacket); override;
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
@@ -693,12 +703,14 @@ begin
// см. doc/SLICES_PLAN.md 3.1). Каждый DDC тюнится независимо. // см. doc/SLICES_PLAN.md 3.1). Каждый DDC тюнится независимо.
Result.MaxPans := 4; Result.MaxPans := 4;
Result.IndependentPanFreq := True; 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). // те же платы, что в RebuildDDCSpecific (Pkt.NumADCs).
if FDevice.BoardType in [4, 5, 10] then if FDevice.BoardType in [3, 4, 5, 10] then
Result.NumADCs := 2 Result.NumADCs := 2
else else
Result.NumADCs := 1; Result.NumADCs := 1;
// PureSignal: нужны свободные DDC0/1 под feedback — платы с DDCBase=2.
Result.HasPureSignal := FDevice.BoardType in [3, 4, 5, 10];
end; end;
constructor THPSDRNetwork.Create; constructor THPSDRNetwork.Create;
@@ -1328,7 +1340,8 @@ function CalcAlex0(RXFreqHz, TXFreqHz: Double;
XvtrRxAnt: Byte = 0; XvtrRxAnt: Byte = 0;
XvtrDisablePA: Boolean = False; XvtrDisablePA: Boolean = False;
XvtrActive: Boolean = False; XvtrActive: Boolean = False;
WidebandBypass: Boolean = False): LongWord; WidebandBypass: Boolean = False;
PureSignalTX: Boolean = False): LongWord;
var var
txf: Double; txf: Double;
BandIdx: Integer; BandIdx: Integer;
@@ -1339,10 +1352,15 @@ begin
// Bit 27 = T/R relay (0=RX, 1=TX). // Bit 27 = T/R relay (0=RX, 1=TX).
// Если XvtrDisablePA=True — НЕ устанавливаем bit 27 (PA остаётся в RX // Если XvtrDisablePA=True — НЕ устанавливаем bit 27 (PA остаётся в RX
// положении, TX-сигнал идёт через XVTR-IF без усиления). // положении, TX-сигнал идёт через XVTR-IF без усиления).
// Bit 18 (PureSignal) не ставим — PureSignal не реализован.
if Transmitting then if Transmitting then
if not XvtrDisablePA then if not XvtrDisablePA then
begin
Result := Result or $08000000; // bit 27: T/R relay 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-частоте. // HPF (ANAN-100/200) или BPF (ANAN-7000/8000/Saturn/G2) — по RX-частоте.
if WidebandBypass and (not Transmitting) then if WidebandBypass and (not Transmitting) then
@@ -1545,8 +1563,8 @@ var
begin begin
FillChar(Pkt, SizeOf(Pkt), 0); FillChar(Pkt, SizeOf(Pkt), 0);
// ANAN-7000/8000/Saturn имеют 2 ADC // ANAN-100D/200D (ANGELIA/ORION), 7000/8000 (ORION2) и Saturn имеют 2 ADC
if FDevice.BoardType in [4, 5, 10] then // ORION=4, ORION2=5, SATURN=10 if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION2=5, SATURN=10
Pkt.NumADCs := 2 Pkt.NumADCs := 2
else else
Pkt.NumADCs := 1; Pkt.NumADCs := 1;
@@ -1568,6 +1586,21 @@ begin
if FPanDDCEnabled[ddc] then if FPanDDCEnabled[ddc] then
AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]); 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); SendDDCSpecific(Pkt);
FCachedDDCSpec := Pkt; FCachedDDCSpec := Pkt;
FCachedDDCValid := True; FCachedDDCValid := True;
@@ -1596,6 +1629,37 @@ begin
if FConnected and FPanDDCEnabled[DDCIdx] then SendFullHP; if FConnected and FPanDDCEnabled[DDCIdx] then SendFullHP;
end; 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); procedure THPSDRNetwork.SendDUCSpecific(const Pkt: TDUCSpecificPacket);
var var
B: TDUCSpecificPacket; B: TDUCSpecificPacket;
@@ -1691,6 +1755,21 @@ begin
Buf[12 + ddc*4] := Ph and $FF; Buf[12 + ddc*4] := Ph and $FF;
end; 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) // DUC TX frequency (bytes 329-332)
Ph := FreqToPhaseWord(FCurrentTXFreq); Ph := FreqToPhaseWord(FCurrentTXFreq);
Buf[329] := (Ph shr 24) and $FF; Buf[329] := (Ph shr 24) and $FF;
@@ -1708,7 +1787,7 @@ begin
Alex0 := CalcAlex0(FCurrentRXFreq, FCurrentTXFreq, Alex0 := CalcAlex0(FCurrentRXFreq, FCurrentTXFreq,
FIsTransmitting, IsOrion2, FAlexConfig, FIsTransmitting, IsOrion2, FAlexConfig,
FXvtrRxAntOverride, FXvtrDisablePA, FXvtrRxAntOverride, FXvtrDisablePA,
FXvtrEnable, FWidebandEnabled); FXvtrEnable, FWidebandEnabled, FPSTXActive);
Buf[1432] := (Alex0 shr 24) and $FF; Buf[1432] := (Alex0 shr 24) and $FF;
Buf[1433] := (Alex0 shr 16) and $FF; Buf[1433] := (Alex0 shr 16) and $FF;
Buf[1434] := (Alex0 shr 8) and $FF; Buf[1434] := (Alex0 shr 8) and $FF;
+222 -19
View File
@@ -33,10 +33,11 @@ uses
WebServer, WebAdapter, WebServer, WebAdapter,
CATAdapter, CATAdapter,
SpectrumView, SpectrumViewOpengl, SpectrumView, SpectrumViewOpengl,
PanZoomBar, PanafallPanel, PanDisplayPopup, FlatPopupMenu, PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu,
WidebandView, WidebandView,
StatusBar, StatusBar,
PlatformUtils, PlatformUtils,
RadioModes,
WinFirewall, WinFirewall,
BoardUtils, WisdomBuilder, UISync, BoardUtils, WisdomBuilder, UISync,
FlatEdit, FMRepeater, FlatEdit, FMRepeater,
@@ -67,7 +68,6 @@ const
WDSP_WISDOM_FILE = 'wdspWisdom00'; WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11) BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
MODE_COUNT = 12;
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*) // Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только // перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
// UI-подписи кнопок (NAMES). // UI-подписи кнопок (NAMES).
@@ -83,6 +83,8 @@ const
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300'); ('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_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k'); 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 tones (38 standard tones, no None — toggle via CTCSS button)
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController). // CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
@@ -105,8 +107,8 @@ const
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m',''); '160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','');
MODE_NAMES: array[0..MODE_COUNT-1] of string = ( MODE_NAMES: array[0..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');
MODE_ROWS = 3; // 4 колонки; последний ряд неполный MODE_ROWS = 4; // 4 колонки; последний ряд неполный
SMETER_WIDTH_RATIO = 0.32; SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3; SMETER_MARGIN = 3;
@@ -299,6 +301,9 @@ type
BtnFMSQ: TFlatButton; BtnFMSQ: TFlatButton;
TrkFMSQ: TFlatSlider; TrkFMSQ: TFlatSlider;
LblFMSQ: TLabel; LblFMSQ: TLabel;
PanelRawDev: TPanel;
TrkRawDev: TFlatSlider;
LblRawDev: TLabel;
PanelFMCTCSS: TPanel; PanelFMCTCSS: TPanel;
BtnFMCTCSS: TFlatButton; BtnFMCTCSS: TFlatButton;
BtnFMCTCSSTone: TFlatButton; BtnFMCTCSSTone: TFlatButton;
@@ -337,6 +342,9 @@ type
TrkDrive: TFlatSlider; TrkDrive: TFlatSlider;
BtnMOX: TFlatButton; BtnMOX: TFlatButton;
BtnTUN: TFlatButton; BtnTUN: TFlatButton;
BtnPS: TFlatButton; // PureSignal: ЛКМ = вкл/выкл, ПКМ = поповер настроек
Btn2Ton: TFlatButton; // двухтональник (IMD/проверка PS)
FPSPopup: TObject; // TPureSignalPopup (cast при использовании)
PanelRXBlock: TPanel; PanelRXBlock: TPanel;
BtnChannels: TFlatButton; BtnChannels: TFlatButton;
@@ -460,9 +468,18 @@ type
procedure UpdateBandPlanOverlay; // вкл/выкл полоски бэндплана QO-100 по InQO100 procedure UpdateBandPlanOverlay; // вкл/выкл полоски бэндплана QO-100 по InQO100
procedure BtnTUNClick(Sender: TObject); procedure BtnTUNClick(Sender: TObject);
procedure ApplyTUN(Active: Boolean); 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 UpdateFilterButtons;
procedure BtnFMSQClick(Sender: TObject); procedure BtnFMSQClick(Sender: TObject);
procedure TrkFMSQChange(Sender: TObject); procedure TrkFMSQChange(Sender: TObject);
procedure TrkRawDevChange(Sender: TObject);
procedure ApplyFMSquelch; procedure ApplyFMSquelch;
procedure BtnFMCTCSSClick(Sender: TObject); procedure BtnFMCTCSSClick(Sender: TObject);
procedure BtnFMCTCSSToneClick(Sender: TObject); procedure BtnFMCTCSSToneClick(Sender: TObject);
@@ -920,7 +937,7 @@ begin
FController.FVfoA := 14200000; FController.FVfoA := 14200000;
FController.FVfoB := 7100000; FController.FVfoB := 7100000;
FController.FActiveVfo := 0; FController.FActiveVfo := 0;
FController.FMode := 1; FController.FMode := MODE_USB;
FController.FFilter := 5; FController.FFilter := 5;
FController.FAGCMode := 1; // MEDIUM FController.FAGCMode := 1; // MEDIUM
FController.FAGCTop := 90; // 90 dBm FController.FAGCTop := 90; // 90 dBm
@@ -1366,9 +1383,12 @@ begin
ApplySpecViewGridFromState; // сетка спектра под активный тракт ApplySpecViewGridFromState; // сетка спектра под активный тракт
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт) RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
UpdateTXControlAvailability; UpdateTXControlAvailability;
UpdateIMDMarkers; // 2TON: маркеры IMD на TX-спектре
end; // (DUC-очередь сбрасывает SetMOX) end; // (DUC-очередь сбрасывает SetMOX)
rfTuning: rfTuning:
StyleButton(BtnTUN, FController.FTuning); StyleButton(BtnTUN, FController.FTuning);
rfPureSignal, rfTwoTone:
UpdatePSButtons; // подсветка PS/2TON + обновление статусов поповера
rfRunning: rfRunning:
// Run-состояние: кнопка START/STOP, доступность MOX, запрет сна. // Run-состояние: кнопка START/STOP, доступность MOX, запрет сна.
// Текст offline-статуса контекстно-зависим (Disconnected / Connection // Текст offline-статуса контекстно-зависим (Disconnected / Connection
@@ -1472,6 +1492,12 @@ begin
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter); for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
SyncSpecViewFreq; SyncSpecViewFreq;
end; 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: rfAGCMode:
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
rfVolume: rfVolume:
@@ -1512,6 +1538,7 @@ begin
// подсвечивал → визуал расходился с состоянием. // подсвечивал → визуал расходился с состоянием.
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex); if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
UpdateRxMuteButton; // активность RX MUTE зависит от DUP UpdateRxMuteButton; // активность RX MUTE зависит от DUP
UpdateIMDMarkers; // IMD-маркеры 2TON живут только в DUP
// На передаче переключаем источник пикселей и сбрасываем буферы, чтобы // На передаче переключаем источник пикселей и сбрасываем буферы, чтобы
// старые TX/RX bin'ы не оставались на следующем кадре. // старые TX/RX bin'ы не оставались на следующем кадре.
if FController.FTransmitting then if FController.FTransmitting then
@@ -1538,7 +1565,7 @@ begin
begin begin
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx); if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx);
if FWebServer <> nil then FWebServer.FMStepIdx := 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 <> nil) then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]; FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx];
end; end;
@@ -1952,6 +1979,14 @@ begin
BtnTUN := MakeBtn(PanelTX, 'TUN', BtnTUN := MakeBtn(PanelTX, 'TUN',
LeftPanelButtonLeft(LEFT_W, 4, 1), 16, LeftPanelButtonLeft(LEFT_W, 4, 1), 16,
LeftPanelButtonWidth(LEFT_W, 4, 1), BTN_SM, BtnTUNClick); 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; UpdateTXControlAvailability;
BtnDUP := MakeBtn(PanelTX, 'DUP', BtnDUP := MakeBtn(PanelTX, 'DUP',
@@ -2210,6 +2245,35 @@ begin
Inc(Y, 76); 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) // FM Squelch panel (visible only in FM mode, above CTCSS)
PanelFMSQ := TPanel.Create(Self); PanelFMSQ := TPanel.Create(Self);
PanelFMSQ.Parent := PanelLeft; PanelFMSQ.Parent := PanelLeft;
@@ -2867,6 +2931,7 @@ begin
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons); DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
DP(PanelBands); DP(PanelMode); DP(PanelFilter); DP(PanelBands); DP(PanelMode); DP(PanelFilter);
if PanelFMSQ <> nil then DP(PanelFMSQ); if PanelFMSQ <> nil then DP(PanelFMSQ);
if PanelRawDev <> nil then DP(PanelRawDev);
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMStep <> nil then DP(PanelFMStep); if PanelFMStep <> nil then DP(PanelFMStep);
if PanelFMRpt <> nil then DP(PanelFMRpt); if PanelFMRpt <> nil then DP(PanelFMRpt);
@@ -3242,7 +3307,7 @@ begin
FSpecView.SpanHz := ViewSpan; FSpecView.SpanHz := ViewSpan;
FSpecView.Mode := FController.FMode; FSpecView.Mode := FController.FMode;
FSpecView.FilterBW := FController.FFilterBW; 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] FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]
else else
FSpecView.FMGridStepHz := 0; FSpecView.FMGridStepHz := 0;
@@ -3291,6 +3356,9 @@ begin
FController.ServiceBeaconLock; FController.ServiceBeaconLock;
// Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR). // Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR).
FController.ServicePlutoTelemetry; FController.ServicePlutoTelemetry;
// PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo
// (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal).
FController.PureSignalTick;
// Сглаженное среднее (EMA) // Сглаженное среднее (EMA)
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA; FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA;
@@ -3527,13 +3595,59 @@ begin
end; end;
procedure TMainForm.UpdateTXControlAvailability; procedure TMainForm.UpdateTXControlAvailability;
var Allowed: Boolean; var MOXAllowed, TuneAllowed: Boolean;
begin begin
if (BtnMOX = nil) or (BtnTUN = nil) or (FController = nil) then Exit; if (BtnMOX = nil) or (BtnTUN = nil) or (FController = nil) then Exit;
Allowed := FController.FRunning and MOXAllowed := FController.FRunning and
(FController.ActiveTXMode <> MODE_DMR); (FController.ActiveTXMode <> MODE_DMR);
BtnMOX.Enabled := Allowed; // FM RAW transmits external PCM only; the speech-path tune generator is not
BtnTUN.Enabled := Allowed; // 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; end;
procedure TMainForm.UpdateStatusField3; procedure TMainForm.UpdateStatusField3;
@@ -4226,6 +4340,7 @@ begin
EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
PanelFMRpt.Visible := True; PanelFMRpt.Visible := True;
end; end;
if PanelRawDev <> nil then PanelRawDev.Visible := False;
RelayoutBelowBands; RelayoutBelowBands;
Exit; Exit;
end; end;
@@ -4237,7 +4352,7 @@ begin
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False; if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then if PanelFMStep <> nil then
begin begin
if FController.FMode = MODE_DMR then if FController.FMode in [MODE_DMR, MODE_FMRAW] then
begin begin
StyleButton(BtnFMStep, FController.FFMStepOn); StyleButton(BtnFMStep, FController.FFMStepOn);
if FStepDropDown <> nil then if FStepDropDown <> nil then
@@ -4251,6 +4366,7 @@ begin
end; end;
end; end;
if PanelFMRpt <> nil then PanelFMRpt.Visible := False; if PanelFMRpt <> nil then PanelFMRpt.Visible := False;
if PanelRawDev <> nil then PanelRawDev.Visible := False;
if FController.FMode = MODE_WFM then if FController.FMode = MODE_WFM then
begin begin
@@ -4273,6 +4389,33 @@ begin
Exit; Exit;
end; 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 if FController.FMode = MODE_DMR then
begin begin
CloseCTCSSPopup; CloseCTCSSPopup;
@@ -4481,6 +4624,54 @@ begin
ApplyTUN(not FController.FTuning); ApplyTUN(not FController.FTuning);
end; 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; ViewCenter, ViewSpan: Double;
begin begin
if PanelWidth <= 0 then Exit; 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 FController.FFMStepOn then
StepHz := FM_STEP_HZ[FController.FFMStepIdx] StepHz := FM_STEP_HZ[FController.FFMStepIdx]
else else
@@ -4524,7 +4715,7 @@ begin
if (FWidebandView = nil) or if (FWidebandView = nil) or
(not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit; (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 FController.FFMStepOn then
StepHz := FM_STEP_HZ[FController.FFMStepIdx] StepHz := FM_STEP_HZ[FController.FFMStepIdx]
else else
@@ -5006,6 +5197,14 @@ begin
SaveCurrentBand; SaveCurrentBand;
end; 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; procedure TMainForm.CloseCTCSSPopup;
begin begin
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup; if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
@@ -6095,8 +6294,8 @@ begin
Id := P.FirstSliceId; Id := P.FirstSliceId;
StepHz := 100; StepHz := 100;
if ((Id <> 0) and FController.GetSlice(Id, S) and if ((Id <> 0) and FController.GetSlice(Id, S) and
(S.Mode = MODE_DMR)) or (S.Mode in [MODE_DMR, MODE_FMRAW])) or
((Id = 0) and (FController.FMode = MODE_DMR)) then ((Id = 0) and (FController.FMode in [MODE_DMR, MODE_FMRAW])) then
StepHz := 12500; StepHz := 12500;
Hz := Round(VC + (X / W - 0.5) * VS); Hz := Round(VC + (X / W - 0.5) * VS);
Hz := ((Hz + StepHz div 2) div StepHz) * StepHz; Hz := ((Hz + StepHz div 2) div StepHz) * StepHz;
@@ -7355,6 +7554,10 @@ begin
PanelFilter.Top := Y; PanelFilter.Top := Y;
Y := Y + PanelFilter.Height + 2; Y := Y + PanelFilter.Height + 2;
end; 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 if (PanelFMStep <> nil) and PanelFMStep.Visible then
begin begin
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2; PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
@@ -7631,7 +7834,7 @@ begin
end; end;
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
begin 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 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) and (ssShift in Shift) then Step := 1000000
else if ssCtrl in Shift then Step := 1000 else if ssCtrl in Shift then Step := 1000
@@ -7655,7 +7858,7 @@ begin
Handled := True; Handled := True;
Exit; Exit;
end; end;
if (FController.FMode in [MODE_FM, MODE_DMR]) and if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn and FController.FFMStepOn and
not ((ssCtrl in Shift) or (ssShift in Shift)) then not ((ssCtrl in Shift) or (ssShift in Shift)) then
Step := FM_STEP_HZ[FController.FFMStepIdx] 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» в настройках; // парами SMA (LibreSDR/AntSDR/Pluto+). Гейтит блок «Antenna» в настройках;
// у openHPSDR антеннами рулит Alex, поэтому False. Достоверно после Connect. // у openHPSDR антеннами рулит Alex, поэтому False. Достоверно после Connect.
HasAntSwitch: Boolean; HasAntSwitch: Boolean;
// PureSignal-предыскажение: openHPSDR P2 платы со свободными DDC0/1 под
// feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON.
// Достоверно после Connect.
HasPureSignal: Boolean;
MinSampleRate: Integer; MinSampleRate: Integer;
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
MinFreqHz: Double; MinFreqHz: Double;
@@ -142,6 +146,14 @@ type
// У бэкендов без переключателя — no-op. // У бэкендов без переключателя — no-op.
procedure SetAntPorts(RxPort, TxPort: Byte); virtual; 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-состояние ---- // ---- Частота / drive / TX-состояние ----
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte = 100); virtual; abstract; DriveLevel: Byte = 100); virtual; abstract;
@@ -306,4 +318,13 @@ procedure TRadioBackend.SetAntPorts(RxPort, TxPort: Byte);
begin begin
end; end;
procedure TRadioBackend.SetPureSignal(Enabled: Boolean);
// PureSignal есть только у openHPSDR P2 (THPSDRNetwork переопределяет).
begin
end;
procedure TRadioBackend.SetPureSignalTX(Active: Boolean);
begin
end;
end. end.
+504 -27
View File
@@ -74,6 +74,7 @@ type
rfCenterFreq, rfSpan, rfSampleRate, rfZoom, rfCenterFreq, rfSpan, rfSampleRate, rfZoom,
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
rfRunning, rfConnected, rfTransmitting, rfTuning, rfRunning, rfConnected, rfTransmitting, rfTuning,
rfPureSignal, rfTwoTone, // PureSignal вкл/статус; 2TON вкл/выкл
rfNR, rfNB, rfSNB, rfANF, rfNR, rfNB, rfSNB, rfANF,
rfWfAGC, rfWfNF, rfDither, rfRandom, rfWfAGC, rfWfNF, rfDither, rfRandom,
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel, rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
@@ -289,6 +290,22 @@ type
FRunning: Boolean; FRunning: Boolean;
FTransmitting: Boolean; FTransmitting: Boolean;
FTuning: 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; FSplitTxB: Boolean;
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock) FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
@@ -337,6 +354,7 @@ type
// ---- FM ---- // ---- FM ----
FFMDeviation: Double; FFMDeviation: Double;
FRawFMDeviation: Double;
FFMCTCSSOn: Boolean; FFMCTCSSOn: Boolean;
FFMCTCSSToneIdx: Integer; FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean; FFMSQOn: Boolean;
@@ -489,6 +507,7 @@ type
procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP
procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
// ---- Управление диапазонами (состояние + DSP; рендер через события) ---- // ---- Управление диапазонами (состояние + DSP; рендер через события) ----
// MakeBandSettings — снимок текущего состояния в запись диапазона. // MakeBandSettings — снимок текущего состояния в запись диапазона.
@@ -546,6 +565,7 @@ type
procedure SetMode(M: Integer); procedure SetMode(M: Integer);
procedure SetFilterIdx(Idx: Integer); procedure SetFilterIdx(Idx: Integer);
procedure SetFilterBW(BWHz: Integer); procedure SetFilterBW(BWHz: Integer);
procedure SetRawFMDeviation(Hz: Integer);
procedure SetAGCMode(M: Integer); procedure SetAGCMode(M: Integer);
procedure SetAGCTop(DB: Integer); procedure SetAGCTop(DB: Integer);
procedure AGCTopBy(Delta: Integer); procedure AGCTopBy(Delta: Integer);
@@ -721,6 +741,35 @@ type
// Передача // Передача
procedure SetMOX(On_: Boolean); procedure SetMOX(On_: Boolean);
procedure SetTune(On_: Boolean); // TUN 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 SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A)
procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO
@@ -815,6 +864,16 @@ const
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel. // (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
TUN_TONE_MAG = 0.83; 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; constructor TDMRSliceDecoder.Create(AOwner: TRadioController;
ASliceId: Integer); ASliceId: Integer);
begin begin
@@ -864,7 +923,7 @@ begin
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web) FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны). // Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0; FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700; FMode := MODE_USB; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90; FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
@@ -882,6 +941,7 @@ begin
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096; FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
FWfSpeedDiv := 1; FSpecMSAA := 4; FWfSpeedDiv := 1; FSpecMSAA := 4;
FFMStepOn := True; FFMStepOn := True;
FRawFMDeviation := RAW_FM_DEV_DEFAULT;
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
@@ -1065,13 +1125,20 @@ begin
FDDCSeqValid[DDCIndex] := True; FDDCSeqValid[DDCIndex] := True;
end; 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 if FWDSPReady then
begin begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]); Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback 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) FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS) else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS)
and (FDDCPanMap[DDCIndex] > 0) then and (FDDCPanMap[DDCIndex] > 0) then
@@ -1257,7 +1324,7 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
begin begin
// Decoded DMR PCM will use this route later. Until then suppress raw 4FSK; // Decoded DMR PCM will use this route later. Until then suppress raw 4FSK;
// the independent pre-volume tap is handled by OnDemodAudioReady. // 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 if FSendAudioToRadio then
FNetwork.SendSpeakerAudio(Left, Right, Count); FNetwork.SendSpeakerAudio(Left, Right, Count);
@@ -1273,13 +1340,19 @@ procedure TRadioController.OnDemodAudioReady(const Left, Right: array of Single;
Count: Integer); Count: Integer);
// DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring. // DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring.
begin begin
if not Assigned(FDMRDec) then Exit;
if FMode = MODE_DMR then if FMode = MODE_DMR then
begin begin
if not Assigned(FDMRDec) then Exit;
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True); if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
FDMRDec.FeedAudio(Left, Right, Count); FDMRDec.FeedAudio(Left, Right, Count);
end 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); FDMRDec.SetEnabled(False);
end; end;
@@ -1337,7 +1410,7 @@ begin
if not FLocalAudio then Exit; if not FLocalAudio then Exit;
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if idx < 0 then Exit; 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 SliceMutedByTx(idx) then Exit;
if FSlices[idx].Audio <> nil then if FSlices[idx].Audio <> nil then
FSlices[idx].Audio.Write(Left, Right, Count); FSlices[idx].Audio.Write(Left, Right, Count);
@@ -1358,9 +1431,12 @@ procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
var idx: Integer; var idx: Integer;
begin begin
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) then Exit; if idx < 0 then Exit;
if Assigned(FSlices[idx].DMR) then if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then
FSlices[idx].DMR.FeedAudio(Left, Right, Count); 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; end;
// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output. // DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output.
@@ -1762,6 +1838,7 @@ var
AO: TAudioOutput; AO: TAudioOutput;
begin begin
Result := -1; Result := -1;
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
if not Assigned(FDSPEngine) then Exit; if not Assigned(FDSPEngine) then Exit;
if (APanId < 0) or (APanId >= MAX_PANS) then Exit; if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан
@@ -1842,6 +1919,7 @@ end;
procedure TRadioController.SetSliceMode(Id, Mode: Integer); procedure TRadioController.SetSliceMode(Id, Mode: Integer);
var idx, OldMode: Integer; var idx, OldMode: Integer;
begin begin
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
idx := FindSliceIndex(Id); idx := FindSliceIndex(Id);
if idx < 0 then Exit; if idx < 0 then Exit;
OldMode := FSlices[idx].Mode; OldMode := FSlices[idx].Mode;
@@ -1850,7 +1928,7 @@ begin
FSlices[idx].Mode := Mode; FSlices[idx].Mode := Mode;
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, 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 // 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 (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0);
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
FreeAndNil(FSlices[idx].DMR); FreeAndNil(FSlices[idx].DMR);
@@ -2160,8 +2238,7 @@ begin
if SliceId > 0 then if SliceId > 0 then
begin begin
if not GetSlice(SliceId, S) then Exit; if not GetSlice(SliceId, S) then Exit;
// EWSDR has no DMR transmitter/AMBE encoder. Do not accidentally use the // EWSDR has no DMR transmitter/AMBE encoder.
// analog WFM transmitter for a receive-only DMR slice.
if S.Mode = MODE_DMR then Exit; if S.Mode = MODE_DMR then Exit;
end; end;
if FTxSliceId = SliceId then Exit; if FTxSliceId = SliceId then Exit;
@@ -2470,11 +2547,15 @@ begin
begin begin
FDSPEngine.SetFilter(Lo, Hi); FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(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 begin
FDSPEngine.SetRXFMDeviation(FFMDeviation); if FMode = MODE_FMRAW then
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut, FDSPEngine.SetRawFMDeviation(FRawFMDeviation)
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition); 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. // A squelch would remove sync preambles; DMR always receives open.
FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel); FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel);
end end
@@ -2506,6 +2587,11 @@ begin
FFilterBW := FILT_DMR_BW; FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV; FFMDeviation := DMR_FM_DEV;
end; 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: MODE_LSB, MODE_USB:
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end; begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
MODE_DIGU, MODE_DIGL: MODE_DIGU, MODE_DIGL:
@@ -2526,6 +2612,7 @@ begin
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)]; 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_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
MODE_DMR: Result := FILT_DMR_BW; MODE_DMR: Result := FILT_DMR_BW;
MODE_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)];
MODE_LSB, MODE_USB: MODE_LSB, MODE_USB:
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DIGU, MODE_DIGL: MODE_DIGU, MODE_DIGL:
@@ -2604,6 +2691,11 @@ begin
FFilterBW := FILT_DMR_BW; FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV; FFMDeviation := DMR_FM_DEV;
end; 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; end;
function TRadioController.MakeBandSettings: TBandSettings; function TRadioController.MakeBandSettings: TBandSettings;
@@ -2762,6 +2854,7 @@ begin
Result.AudioOutDevice := FAudioOutDevName; Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName; Result.AudioInDevice := FAudioInDevName;
Result.SendAudioToRadio := FSendAudioToRadio; Result.SendAudioToRadio := FSendAudioToRadio;
Result.RawFMDeviation := Round(FRawFMDeviation);
end; end;
procedure TRadioController.SaveGlobalSettings; procedure TRadioController.SaveGlobalSettings;
@@ -2773,7 +2866,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает. // DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
begin begin
// --- Режим --- // --- Режим ---
FMode := B.Mode; FMode := EnsureRange(B.Mode, MODE_MIN, MODE_MAX);
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR); if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
@@ -2808,7 +2901,7 @@ begin
end; end;
FFMStepOn := B.FMStepOn; FFMStepOn := B.FMStepOn;
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3); FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
if FMode = MODE_DMR then if FMode in [MODE_DMR, MODE_FMRAW] then
begin begin
FFMStepOn := True; FFMStepOn := True;
FFMStepIdx := 1; // DMR default raster: 12.5 kHz FFMStepIdx := 1; // DMR default raster: 12.5 kHz
@@ -2915,8 +3008,8 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
HalfSpan := FSpanHz / 2; HalfSpan := FSpanHz / 2;
case FMode of case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; MODE_LSB: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; MODE_USB: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end; else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end; end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
@@ -3169,13 +3262,14 @@ end;
procedure TRadioController.SetMode(M: Integer); procedure TRadioController.SetMode(M: Integer);
begin begin
M := EnsureRange(M, MODE_MIN, MODE_MAX);
FMode := M; FMode := M;
// DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP // DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP
// controls enabled so the operator can select another raster afterwards. // controls enabled so the operator can select another raster afterwards.
if FMode = MODE_DMR then if FMode in [MODE_DMR, MODE_FMRAW] then
begin begin
FFMStepOn := True; FFMStepOn := True;
FFMStepIdx := 1; // 12.5 kHz if FMode = MODE_DMR then FFMStepIdx := 1; // 12.5 kHz
Changed(rfFMStep); Changed(rfFMStep);
Changed(rfFMStepIdx); Changed(rfFMStepIdx);
end; end;
@@ -3192,6 +3286,17 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin begin
FDSPEngine.SetMode(FMode); 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 активен — пересчитываем знак частоты под новый режим // Если TUN активен — пересчитываем знак частоты под новый режим
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
end; end;
@@ -3207,6 +3312,7 @@ begin
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1); MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1); MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
MODE_DMR: Idx := 0; MODE_DMR: Idx := 0;
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1); else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
end; end;
FFilter := Idx; FFilter := Idx;
@@ -3217,6 +3323,7 @@ begin
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx]; MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx]; MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
MODE_DMR: FFilterBW := FILT_DMR_BW; MODE_DMR: FFilterBW := FILT_DMR_BW;
MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx];
MODE_FM: MODE_FM:
begin begin
FFilterBW := FILT_FM_BW[Idx]; 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_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_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_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]; else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end; end;
best := 0; dist := MaxInt; best := 0; dist := MaxInt;
@@ -3266,6 +3374,15 @@ begin
Changed(rfFilter); Changed(rfFilter);
end; 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. Единый маппинг для всех // UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum). // фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode; class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
@@ -4035,6 +4152,10 @@ end;
procedure TRadioController.ApplyTXSettingsToDSP; procedure TRadioController.ApplyTXSettingsToDSP;
begin begin
if not FWDSPReady then Exit; if not FWDSPReady then Exit;
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2:
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
// имеет, поэтому при смене бэкенда явно выключаем его.
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
// Mic source — выставляем значение по умолчанию для MOX-кнопки; // Mic source — выставляем значение по умолчанию для MOX-кнопки;
// при реальном PTT SetMOX скорректирует источник. // при реальном PTT SetMOX скорректирует источник.
FDSPEngine.SetTXMicSource(DefaultMicSource); FDSPEngine.SetTXMicSource(DefaultMicSource);
@@ -4063,6 +4184,11 @@ begin
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector, FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
FTXSettings.TXWfAvgMode, FTXSettings.TXWfAvgMode,
FTXSettings.TXWfAvgTimeMS); 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; end;
procedure TRadioController.SetMOX(On_: Boolean); procedure TRadioController.SetMOX(On_: Boolean);
@@ -4070,7 +4196,7 @@ var
WasTransmitting: Boolean; WasTransmitting: Boolean;
begin begin
// DMR support is receive-only. This guard also covers CAT, web and hardware // 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; if On_ and (ActiveTXMode = MODE_DMR) then Exit;
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере. // Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
@@ -4081,6 +4207,14 @@ begin
Exit; Exit;
WasTransmitting := FTransmitting; WasTransmitting := FTransmitting;
FTransmitting := On_; 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 if On_ then
begin begin
FDriveLevel := CalcDriveByte; FDriveLevel := CalcDriveByte;
@@ -4096,6 +4230,9 @@ begin
FDriveLevel, True, True, True); FDriveLevel, True, True, True);
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле 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 не стартует с пустого буфера) // Pre-roll: THROTTLE (2000, порог FPGA — DAC не стартует с пустого буфера)
// + ДВА WDSP-блока (2×2048) нулевых пар подушкой в очередь. Раньше было 3 // + ДВА WDSP-блока (2×2048) нулевых пар подушкой в очередь. Раньше было 3
// блока: обоснование «2 не хватало — щелчок через ~0.5с» теперь под вопросом, // блока: обоснование «2 не хватало — щелчок через ~0.5с» теперь под вопросом,
@@ -4107,11 +4244,14 @@ begin
end; end;
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin begin
// Mic source автоматически по источнику TX: // Mic source автоматически по источнику TX. FM RAW always uses the
// Web TX → txmsWeb; HW PTT → txmsRadio; MOX/CAT → DefaultMicSource. // configured default input (normally a virtual PortAudio cable), even if
// a web client is connected or hardware PTT keyed the transmitter.
if FTransmitting then if FTransmitting then
begin begin
if FWebClientActive then if ActiveTXMode = MODE_FMRAW then
FDSPEngine.SetTXMicSource(DefaultMicSource)
else if FWebClientActive then
begin begin
FWebMicActive := True; FWebMicActive := True;
FDSPEngine.SetTXMicSource(txmsWeb); FDSPEngine.SetTXMicSource(txmsWeb);
@@ -4140,6 +4280,8 @@ begin
// Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог // Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог
// оставить главный режим). Ставим перед стартом TX-потока. // оставить главный режим). Ставим перед стартом TX-потока.
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode); if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
FDSPEngine.PSSetMox(FTransmitting);
FDSPEngine.SetTXRun(FTransmitting); FDSPEngine.SetTXRun(FTransmitting);
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin begin
@@ -4161,6 +4303,8 @@ begin
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin begin
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
// PureSignal: вернуть DDC0/1 в RX-конфигурацию (убрать feedback-DDC).
FNetwork.SetPureSignalTX(False);
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
FDriveLevel, False, True, True); FDriveLevel, False, True, True);
FNetwork.ClearDUCIQQueue; FNetwork.ClearDUCIQQueue;
@@ -4174,13 +4318,14 @@ end;
procedure TRadioController.SetTune(On_: Boolean); procedure TRadioController.SetTune(On_: Boolean);
begin begin
if On_ = FTuning then Exit; 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-тон, // Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR. // он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then Exit; and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit;
if On_ and FTwoTone then SetTwoTone(False); // TUN поверх 2TON — гасим тона
if On_ then if On_ then
begin begin
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
@@ -4210,6 +4355,315 @@ begin
Changed(rfTuning); Changed(rfTuning);
end; 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); procedure TRadioController.SetFMStep(Idx: Integer);
begin begin
if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4 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); if FWDSPReady then FDSPEngine.SetTXTone(False, 0, 0);
Changed(rfTuning); // UI: TUN-кнопка off Changed(rfTuning); // UI: TUN-кнопка off
end; 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); if FWDSPReady then FDSPEngine.SetTXRun(False);
Changed(rfTransmitting); // MOX-кнопка off Changed(rfTransmitting); // MOX-кнопка off
end; end;
@@ -4577,6 +5050,10 @@ begin
FDitherEnabled := FLoadedGlobal.DitherEnabled; FDitherEnabled := FLoadedGlobal.DitherEnabled;
FRandomEnabled := FLoadedGlobal.RandomEnabled; FRandomEnabled := FLoadedGlobal.RandomEnabled;
FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio; 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; FWfManualHigh := FLoadedGlobal.WfManualHigh;
FWfManualLow := FLoadedGlobal.WfManualLow; FWfManualLow := FLoadedGlobal.WfManualLow;
FWfAGCOffset := FLoadedGlobal.WfAGCOffset; 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+} {$mode objfpc}{$H+}
interface interface
uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils; uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
RadioModes;
const const
SETTINGS_FILE = 'hpsdr_settings.json'; SETTINGS_FILE = 'hpsdr_settings.json';
CFG_BAND_COUNT = 12; CFG_BAND_COUNT = 12;
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) 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 CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
@@ -61,6 +63,16 @@ const
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000); FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
FILT_WFM_DEF = 1; // 160k — влезает и в 192k 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. // DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz.
FILT_DMR_BW = 12500; FILT_DMR_BW = 12500;
DMR_FM_DEV = 2500; DMR_FM_DEV = 2500;
@@ -209,6 +221,25 @@ type
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0 TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
TXSpecRange: Double; // dB range, e.g. 80 TXSpecRange: Double; // dB range, e.g. 80
TXSpecGridStep: Double; // grid step in dB, e.g. 10 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; end;
// Web-сервер — глобальные настройки (не привязаны к устройству, секция "web"). // Web-сервер — глобальные настройки (не привязаны к устройству, секция "web").
@@ -346,6 +377,7 @@ type
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000) AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
AudioInDevice: string; // PortAudio input device name for TX mic ('' = none) AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker) SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker)
RawFMDeviation: Integer; // Hz, flat RAW FM discriminator scale
// --- Visibility settings --- // --- Visibility settings ---
ShowSpectrum: Boolean; // True = draw spectrum on main form ShowSpectrum: Boolean; // True = draw spectrum on main form
ShowWaterfall: Boolean; // True = draw waterfall on main form ShowWaterfall: Boolean; // True = draw waterfall on main form
@@ -553,6 +585,7 @@ begin
G.AudioSampleRate := 48000; G.AudioSampleRate := 48000;
G.AudioInDevice := ''; G.AudioInDevice := '';
G.SendAudioToRadio := False; G.SendAudioToRadio := False;
G.RawFMDeviation := RAW_FM_DEV_DEFAULT;
G.ShowSpectrum := True; G.ShowSpectrum := True;
G.ShowWaterfall := True; G.ShowWaterfall := True;
G.ShowWideband := False; G.ShowWideband := False;
@@ -783,6 +816,9 @@ begin
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000); G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
G.AudioInDevice := JS(GObj,'audio_in_device',''); G.AudioInDevice := JS(GObj,'audio_in_device','');
G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False); 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 // Visibility settings
G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowSpectrum := JB(GObj,'show_spectrum',True);
G.ShowWaterfall := JB(GObj,'show_waterfall',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True);
@@ -827,7 +863,7 @@ begin
BObj := GetBandObj(DevObj, i); BObj := GetBandObj(DevObj, i);
Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA); Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA);
Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB); 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].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx);
Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW); Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW);
Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode); Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode);
@@ -895,6 +931,7 @@ begin
JW(O,'audio_sample_rate',G.AudioSampleRate); JW(O,'audio_sample_rate',G.AudioSampleRate);
JWS(O,'audio_in_device',G.AudioInDevice); JWS(O,'audio_in_device',G.AudioInDevice);
JW(O,'send_audio_to_radio',G.SendAudioToRadio); JW(O,'send_audio_to_radio',G.SendAudioToRadio);
JW(O,'raw_fm_deviation',G.RawFMDeviation);
// Visibility settings // Visibility settings
JW(O,'show_spectrum',G.ShowSpectrum); JW(O,'show_spectrum',G.ShowSpectrum);
JW(O,'show_waterfall',G.ShowWaterfall); JW(O,'show_waterfall',G.ShowWaterfall);
@@ -960,7 +997,7 @@ begin
O := GetBandObj(GetDevObj(MacStr), BandIdx); O := GetBandObj(GetDevObj(MacStr), BandIdx);
B.VfoA := JD(O,'vfo_a', B.VfoA); B.VfoA := JD(O,'vfo_a', B.VfoA);
B.VfoB := JD(O,'vfo_b', B.VfoB); 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.FilterIdx := JI(O,'filter_idx', B.FilterIdx);
B.FilterBW := JI(O,'filter_bw', B.FilterBW); B.FilterBW := JI(O,'filter_bw', B.FilterBW);
B.AGCMode := JI(O,'agc_mode', B.AGCMode); B.AGCMode := JI(O,'agc_mode', B.AGCMode);
@@ -1170,6 +1207,21 @@ begin
T.TXSpecRefLevel := 0.0; T.TXSpecRefLevel := 0.0;
T.TXSpecRange := 80.0; T.TXSpecRange := 80.0;
T.TXSpecGridStep := 10.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; end;
function TSettingsManager.LoadTX(const MAC: array of Byte; function TSettingsManager.LoadTX(const MAC: array of Byte;
@@ -1236,6 +1288,21 @@ begin
T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel); T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel);
T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange); T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange);
T.TXSpecGridStep := JD(O,'tx_spec_grid_step', T.TXSpecGridStep); 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; end;
procedure TSettingsManager.SaveTX(const MAC: array of Byte; 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_ref_level', T.TXSpecRefLevel);
JW(O,'tx_spec_range', T.TXSpecRange); JW(O,'tx_spec_range', T.TXSpecRange);
JW(O,'tx_spec_grid_step', T.TXSpecGridStep); 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; end;
class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings); class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings);
@@ -1439,7 +1521,7 @@ begin
X.Entries[i].RXAntenna := 0; X.Entries[i].RXAntenna := 0;
X.Entries[i].LastFreq := 0.0; X.Entries[i].LastFreq := 0.0;
X.Entries[i].LastFreqB := 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].LastFilterIdx := 0; // NFM
X.Entries[i].LastFMSQOn := False; X.Entries[i].LastFMSQOn := False;
X.Entries[i].LastFMSQLevel := 30; 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].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].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].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].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].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); 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); n := Length(P.Pans[id].Slices);
SetLength(P.Pans[id].Slices, n+1); SetLength(P.Pans[id].Slices, n+1);
P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0); 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].FilterLo := JI(SliceObj, 'filter_lo', -1500);
P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500); P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500);
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1); 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, Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
AppTheme, AppTheme,
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
WaterfallView, SMeterView, RulerView; WaterfallView, SMeterView, RulerView, RadioModes;
const const
SV_CLR_BG = TColor($00101010); SV_CLR_BG = TColor($00101010);
@@ -36,6 +36,16 @@ type
LastUse: Int64; LastUse: Int64;
end; end;
// Маркер 2TON/IMD-измерения на спектре: кружок на пике продукта + подпись.
// Все маркеры подписываются абсолютным уровнем шкалы (dBm); относительная
// величина IMD3 в dBc — в сводке слева от полосы фильтра.
TIMDMarker = record
FreqHz: Double; // абсолютная частота продукта
LevelDB: Double; // измеренный пик (шкала дисплея)
Caption: string;
IsTone: Boolean; // True = основной тон, False = IMD-продукт
end;
TSpectrumView = class TSpectrumView = class
protected protected
// ── Off-screen bitmaps ──────────────────────────────────────────────────── // ── Off-screen bitmaps ────────────────────────────────────────────────────
@@ -84,6 +94,7 @@ type
FTXOverlay: Boolean; FTXOverlay: Boolean;
// ── ADC overload overlay ───────────────────────────────────────────────── // ── ADC overload overlay ─────────────────────────────────────────────────
FADCOverloadVisible: Boolean; FADCOverloadVisible: Boolean;
FADCAlertImg: TAlertImage; // кэш образа плашки (BuildAlertImage, 1 раз)
FSampleRateOverlay: TSampleRateOverlay; FSampleRateOverlay: TSampleRateOverlay;
FVfoOverlay: TVfoOverlay; FVfoOverlay: TVfoOverlay;
FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm) FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm)
@@ -99,6 +110,11 @@ type
FBeaconDecActive: Boolean; FBeaconDecActive: Boolean;
FBeaconDecFreq: Double; FBeaconDecFreq: Double;
FBeaconDecHalf: Double; // полуширина фильтра, Гц FBeaconDecHalf: Double; // полуширина фильтра, Гц
// ── 2TON/IMD-маркеры (пики тонов и продуктов IMD3 при двухтональнике) ────
FIMDActive: Boolean;
FIMDTone1Hz: Double; // абс. частоты тонов (TX freq + аудио-оффсет)
FIMDTone2Hz: Double;
FIMDSummary: string; // сводка «IMD3 −NN dBc» (обновляет ComputeIMDMarkers)
// ── Spectrum buffer ─────────────────────────────────────────────────────── // ── Spectrum buffer ───────────────────────────────────────────────────────
FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single; FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single;
FSpectrumBufCount: Integer; FSpectrumBufCount: Integer;
@@ -149,10 +165,16 @@ type
procedure DrawSliceFilterLinesRaw(W, H: Integer); procedure DrawSliceFilterLinesRaw(W, H: Integer);
procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor); procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
procedure DrawBeaconMarkersRaw(W, H: Integer); 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 BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
procedure FillBandRaw(X1, X2, H: Integer; Color: TColor); procedure FillBandRaw(X1, X2, H: Integer; Color: TColor);
procedure CopyGridToSpectrum(W, H: Integer); procedure CopyGridToSpectrum(W, H: Integer);
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer); procedure DrawADCOverloadRaw(W, H: Integer);
procedure CalcFilterBandX(VfoFreq: Double; W: Integer; procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
out X1, X2, VfoX: Integer); out X1, X2, VfoX: Integer);
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer; procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
@@ -244,6 +266,12 @@ type
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double); procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: 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-метр ──────────────────────────────────────────────────────────────── // ── S-метр ────────────────────────────────────────────────────────────────
property LastSMeter: Double read GetLastSMeter write SetLastSMeter; property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak; property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak;
@@ -653,6 +681,142 @@ begin
end; end;
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) // Full-raw примитивы кадра (валидны только между RawBegin/RawEnd)
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
@@ -1077,10 +1241,18 @@ begin
{$ENDIF} {$ENDIF}
end; 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 begin
if not FADCOverloadVisible then Exit; 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; end;
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer; procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
@@ -1098,10 +1270,10 @@ begin
Half := BW / 2; Half := BW / 2;
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W); VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
case Mode of case Mode of
0: begin Lo_Hz := -BW; Hi_Hz := -100; end; // LSB MODE_LSB: begin Lo_Hz := -BW; Hi_Hz := -100; end;
1: begin Lo_Hz := 100; Hi_Hz := BW; end; // USB MODE_USB: begin Lo_Hz := 100; Hi_Hz := BW; end;
9: begin Lo_Hz := -BW; Hi_Hz := 0; end; // DIGL — полоса от нуля MODE_DIGL: begin Lo_Hz := -BW; Hi_Hz := 0; end; // полоса от нуля
8: begin Lo_Hz := 0; Hi_Hz := BW; end; // DIGU MODE_DIGU: begin Lo_Hz := 0; Hi_Hz := BW; end;
else begin Lo_Hz := -Half; Hi_Hz := Half; end; else begin Lo_Hz := -Half; Hi_Hz := Half; end;
end; end;
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W))); X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
@@ -1546,6 +1718,9 @@ begin
if FMarkerActive then DrawMarkerLineRaw(W, H); if FMarkerActive then DrawMarkerLineRaw(W, H);
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(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/ // Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/
@@ -1566,33 +1741,12 @@ begin
for i := 0 to FSliceOverlays.Count - 1 do for i := 0 to FSliceOverlays.Count - 1 do
TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H); TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H);
DrawADCOverloadRaw(W, H);
finally finally
RawEnd; RawEnd;
end; 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; end;
// ──────────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────────
+105 -26
View File
@@ -17,7 +17,7 @@ interface
uses uses
Classes, SysUtils, Graphics, Controls, Math, Types, Classes, SysUtils, Graphics, Controls, Math, Types,
OpenGLContextEx, GL, OpenGLContextEx, GL,
AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, AppTheme, AlertOverlay, SpectrumView, VfoOverlay, BandPlanOverlay,
WaterfallView, WaterfallViewOpengl; WaterfallView, WaterfallViewOpengl;
type type
@@ -57,6 +57,9 @@ type
FLastAGCHangY: Integer; FLastAGCHangY: Integer;
FLastMarkerText: string; FLastMarkerText: string;
FLastMarkerX: Integer; FLastMarkerX: Integer;
// 2TON/IMD: текстуры подписей маркеров (0..3 = кружки, 4 = сводка IMD3)
FIMDLabelTex: array[0..4] of TGLTextureCache;
FIMDLastText: array[0..4] of string;
FLastADCVisible: Boolean; FLastADCVisible: Boolean;
FLastSampleX: Integer; FLastSampleX: Integer;
FLastSampleY: Integer; FLastSampleY: Integer;
@@ -90,6 +93,8 @@ type
procedure DrawSpectrumCurve(W, H: Integer; DBmax, InvRange: Double); procedure DrawSpectrumCurve(W, H: Integer; DBmax, InvRange: Double);
procedure DrawMarker(W, H: Integer); procedure DrawMarker(W, H: Integer);
procedure DrawBeaconMarkersGL(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 DrawADCOverlay(W, H: Integer);
procedure DrawCachedOverlays(W, H: Integer); procedure DrawCachedOverlays(W, H: Integer);
procedure DrawSliceOverlays(W, H: Integer); procedure DrawSliceOverlays(W, H: Integer);
@@ -170,6 +175,7 @@ begin
DeleteTexture(FBandOverlayTex); DeleteTexture(FBandOverlayTex);
for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex); 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(FLetterTex) do DeleteTexture(FLetterTex[i]);
for i := 0 to High(FIMDLabelTex) do DeleteTexture(FIMDLabelTex[i]);
end; end;
SetLength(FSliceTex, 0); SetLength(FSliceTex, 0);
inherited Destroy; inherited Destroy;
@@ -451,6 +457,11 @@ begin
Zap(FBandOverlayTex); Zap(FBandOverlayTex);
for i := 0 to High(FSliceTex) do Zap(FSliceTex[i].Tex); 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(FLetterTex) do Zap(FLetterTex[i]);
for i := 0 to High(FIMDLabelTex) do
begin
Zap(FIMDLabelTex[i]);
FIMDLastText[i] := '';
end;
FLastAGCText := ''; FLastAGCText := '';
FLastMarkerText := ''; FLastMarkerText := '';
FGridDirty := True; FGridDirty := True;
@@ -803,38 +814,103 @@ begin
end; end;
end; end;
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer); procedure TSpectrumViewOpenGL.DrawCircleGL(CX, CY: Integer; R: Single; C: TColor);
// Залитый кружок (triangle fan, 20 сегментов) — маркер пика 2TON/IMD.
var 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 begin
if not FADCOverloadVisible then Exit; if not FADCOverloadVisible then Exit;
if (W < 220) or (H < 60) then Exit; // как CPU-путь
if (not FLastADCVisible) or FADCOverlayTex.Dirty then if (not FLastADCVisible) or FADCOverlayTex.Dirty then
begin begin
B := TBitmap.Create; BuildAlertImage('ADC OVERLOAD', 'Input clipping detected', Img);
try if FADCOverlayTex.Tex = 0 then glGenTextures(1, @FADCOverlayTex.Tex);
B.PixelFormat := pf32bit; FADCOverlayTex.W := Img.W;
B.SetSize(210, 38); FADCOverlayTex.H := Img.H;
B.Canvas.Brush.Color := TColor($00202020); glBindTexture(GL_TEXTURE_2D, FADCOverlayTex.Tex);
B.Canvas.FillRect(Rect(0, 0, B.Width, B.Height)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
B.Canvas.Pen.Color := TColor($000030C0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
B.Canvas.Brush.Style := bsClear; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, Img.W, Img.H, 0, GL_RGBA,
B.Canvas.Rectangle(0, 0, B.Width, B.Height); GL_UNSIGNED_BYTE, @Img.RGBA[0]);
B.Canvas.Font.Name := 'Courier New'; glBindTexture(GL_TEXTURE_2D, 0);
B.Canvas.Font.Size := 10; FADCOverlayTex.Dirty := False;
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;
end; end;
FLastADCVisible := True; FLastADCVisible := True;
DrawTexture(FADCOverlayTex, (W - FADCOverlayTex.W) div 2, 8); DrawTexture(FADCOverlayTex, W - FADCOverlayTex.W - ALERT_PAD, ALERT_PAD);
end; end;
procedure TSpectrumViewOpenGL.DrawCachedOverlays(W, H: Integer); procedure TSpectrumViewOpenGL.DrawCachedOverlays(W, H: Integer);
@@ -1016,6 +1092,9 @@ begin
DrawSpectrumCurve(W, H, DBmax, InvRange); DrawSpectrumCurve(W, H, DBmax, InvRange);
DrawMarker(W, H); DrawMarker(W, H);
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersGL(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); DrawADCOverlay(W, H);
DrawCachedOverlays(W, H); DrawCachedOverlays(W, H);
+39 -29
View File
@@ -17,18 +17,22 @@ unit VfoOverlay;
interface interface
uses uses
Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme; Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme,
RadioModes;
const const
VFO_OVERLAY_ALPHA = 210; VFO_OVERLAY_ALPHA = 210;
OVL_W = 280; // ширина карточки OVL_W = 280; // ширина карточки
// Индексы совпадают с MODE_* контроллера (WDSPEngine.pas). // Индексы совпадают с 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_COLS = 4;
OVL_MODE_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS; 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 = ( 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): одна на букву — общий цвет для // Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре. // бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
@@ -307,6 +311,8 @@ const
WFM_LBL: array[0..2] of string = ('200K', '160K', '120K'); WFM_LBL: array[0..2] of string = ('200K', '160K', '120K');
DIGI_BW: array[0..5] of Integer = (300, 600, 1200, 1800, 2400, 3000); 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'); 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); 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; function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
begin begin
case ModeIdx of case ModeIdx of
0,1: Result := SSB_BW[FilterIdx]; MODE_LSB, MODE_USB: Result := SSB_BW[FilterIdx];
2: Result := DSB_BW[FilterIdx]; MODE_DSB: Result := DSB_BW[FilterIdx];
3,4: Result := CW_BW[FilterIdx]; MODE_CWL, MODE_CWU: Result := CW_BW[FilterIdx];
5: Result := FM_BW[FilterIdx]; OVL_MODE_FM: Result := FM_BW[FilterIdx];
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля MODE_DIGU, MODE_DIGL: Result := DIGI_BW[FilterIdx]; // полоса от нуля
10: Result := WFM_BW[FilterIdx]; MODE_WFM: Result := WFM_BW[FilterIdx];
11: Result := 12500; OVL_MODE_DMR: Result := 12500;
OVL_MODE_FMRAW: Result := RAW_BW[FilterIdx];
else Result := AM_BW[FilterIdx]; else Result := AM_BW[FilterIdx];
end; end;
end; end;
@@ -414,13 +421,14 @@ end;
function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string; function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
begin begin
case ModeIdx of case ModeIdx of
0,1: Result := SSB_LBL[FilterIdx]; MODE_LSB, MODE_USB: Result := SSB_LBL[FilterIdx];
2: Result := DSB_LBL[FilterIdx]; MODE_DSB: Result := DSB_LBL[FilterIdx];
3,4: Result := CW_LBL[FilterIdx]; MODE_CWL, MODE_CWU: Result := CW_LBL[FilterIdx];
5: Result := FM_LBL[FilterIdx]; OVL_MODE_FM: Result := FM_LBL[FilterIdx];
8,9: Result := DIGI_LBL[FilterIdx]; MODE_DIGU, MODE_DIGL: Result := DIGI_LBL[FilterIdx];
10: Result := WFM_LBL[FilterIdx]; MODE_WFM: Result := WFM_LBL[FilterIdx];
11: Result := '12.5k'; OVL_MODE_DMR: Result := '12.5k';
OVL_MODE_FMRAW: Result := RAW_LBL[FilterIdx];
else Result := AM_LBL[FilterIdx]; else Result := AM_LBL[FilterIdx];
end; end;
end; end;
@@ -428,9 +436,10 @@ end;
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer; function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
begin begin
case ModeIdx of case ModeIdx of
5: Result := 2; // FM: NFM/FM OVL_MODE_FM: Result := 2; // FM: NFM/FM
10: Result := 3; // WFM: 200K/160K/120K MODE_WFM: Result := 3; // WFM: 200K/160K/120K
11: Result := 0; // DMR: fixed 12.5 kHz, no redundant selector 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; else Result := 6;
end; end;
end; end;
@@ -438,7 +447,7 @@ end;
function TVfoOverlay.TxSelectionAllowed: Boolean; function TVfoOverlay.TxSelectionAllowed: Boolean;
begin begin
// Main A remains the normal TX source. Additional DMR slices are RX-only. // 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; end;
{ ---- Lifecycle ---- } { ---- Lifecycle ---- }
@@ -447,7 +456,7 @@ constructor TVfoOverlay.Create(AOwner: TComponent);
begin begin
inherited Create(AOwner); inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque]; ControlStyle := ControlStyle + [csOpaque];
FMode := 1; FMode := MODE_USB;
FFilterBW := 2700; FFilterBW := 2700;
FVfoHz := 14200000; FVfoHz := 14200000;
FSMeterDB := -121; FSMeterDB := -121;
@@ -457,6 +466,7 @@ begin
FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000; FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000;
FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000; FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000;
FModeFilter[9] := 3000; FModeFilter[10] := 160000; FModeFilter[9] := 3000; FModeFilter[10] := 160000;
FModeFilter[11] := 12500; FModeFilter[12] := 12500;
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False;
@@ -594,7 +604,7 @@ end;
function TVfoOverlay.IsFM: Boolean; function TVfoOverlay.IsFM: Boolean;
begin begin
Result := FMode = 5; // индекс FM в OVL_MODE_NAMES (= MODE_FM контроллера) Result := FMode = OVL_MODE_FM;
end; end;
function TVfoOverlay.PanelContentH: Integer; function TVfoOverlay.PanelContentH: Integer;
@@ -1039,7 +1049,7 @@ begin
// Краткий статус дополнительного DMR-слайса. Он обновляется только при // Краткий статус дополнительного DMR-слайса. Он обновляется только при
// устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру // устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру
// перезаливаться на каждом burst. // перезаливаться на каждом burst.
if (not FIsMain) and (FMode = 11) and (FDMRText <> '') then if (not FIsMain) and (FMode = OVL_MODE_DMR) and (FDMRText <> '') then
begin begin
DMRStr := FDMRText; DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8; DMRX := X + 22 + C.TextWidth(BWStr) + 8;
@@ -1276,7 +1286,7 @@ begin
begin begin
FModeFilter[FMode] := FFilterBW; FModeFilter[FMode] := FFilterBW;
FMode := NewMode; FMode := NewMode;
if FMode = 11 then FFilterBW := 12500 if FMode = OVL_MODE_DMR then FFilterBW := 12500
else FFilterBW := FModeFilter[FMode]; else FFilterBW := FModeFilter[FMode];
Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3) Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3)
RequestInvalidate; RequestInvalidate;
@@ -1449,19 +1459,19 @@ var
OldMode: Integer; OldMode: Integer;
begin begin
OldMode := FMode; OldMode := FMode;
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode)); FMode := EnsureRange(AMode, MODE_MIN, MODE_MAX);
FFilterBW := ABW; FFilterBW := ABW;
FModeFilter[FMode] := ABW; FModeFilter[FMode] := ABW;
FVfoHz := AVfoHz; FVfoHz := AVfoHz;
FSMeterDB := ASMeterDB; FSMeterDB := ASMeterDB;
if FMode <> 11 then if FMode <> OVL_MODE_DMR then
begin begin
FDMRText := ''; FDMRText := '';
FDMRSynced := False; FDMRSynced := False;
FDMRCandidateText := ''; FDMRCandidateText := '';
FDMRCandidateSince := 0; FDMRCandidateSince := 0;
end end
else if OldMode <> 11 then else if OldMode <> OVL_MODE_DMR then
begin begin
FDMRText := 'DMR...'; FDMRText := 'DMR...';
FDMRSynced := False; FDMRSynced := False;
@@ -1555,7 +1565,7 @@ procedure TVfoOverlay.UpdateDMRStatus(const AText: string; Synced: Boolean);
var var
NowTick, HoldMs: QWord; NowTick, HoldMs: QWord;
begin begin
if FMode <> 11 then Exit; if FMode <> OVL_MODE_DMR then Exit;
if (FDMRText = AText) and (FDMRSynced = Synced) then if (FDMRText = AText) and (FDMRSynced = Synced) then
begin begin
FDMRCandidateText := ''; 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 SetPSMapMode(channel: Integer; map: Integer); cdecl; external WDSP_LIB;
procedure SetPSStabilize(channel: Integer; stbl: 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; 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 // cblock.c
+334 -26
View File
@@ -25,7 +25,7 @@ interface
uses uses
Classes, SysUtils, Math, SyncObjs, Classes, SysUtils, Math, SyncObjs,
BeaconDecoder, BeaconDecoder, RadioModes,
WDSP; WDSP;
const const
@@ -49,18 +49,22 @@ const
// Наши индексы режимов (совпадают с кнопками MainForm). // Наши индексы режимов (совпадают с кнопками MainForm).
// Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json, // Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json,
// band-cache, CAT (ZZMD/ZZMV) и web-API. // band-cache, CAT (ZZMD/ZZMV) и web-API.
MODE_LSB = 0; MODE_LSB = RadioModes.MODE_LSB;
MODE_USB = 1; MODE_USB = RadioModes.MODE_USB;
MODE_DSB = 2; MODE_DSB = RadioModes.MODE_DSB;
MODE_CWL = 3; MODE_CWL = RadioModes.MODE_CWL;
MODE_CWU = 4; MODE_CWU = RadioModes.MODE_CWU;
MODE_FM = 5; MODE_FM = RadioModes.MODE_FM;
MODE_AM = 6; MODE_AM = RadioModes.MODE_AM;
MODE_SAM = 7; MODE_SAM = RadioModes.MODE_SAM;
MODE_DIGU = 8; MODE_DIGU = RadioModes.MODE_DIGU;
MODE_DIGL = 9; MODE_DIGL = RadioModes.MODE_DIGL;
MODE_WFM = 10; MODE_WFM = RadioModes.MODE_WFM;
MODE_DMR = 11; // DMR receive: flat WDSP discriminator -> DMRDecoder 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 с // Минимальная длина FIR у WDSP: create_* поднимают fircore с
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1, // nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
@@ -375,6 +379,32 @@ type
// PostGen стоит ПОСЛЕ ALC — голос из микрофона складывался бы с тоном // PostGen стоит ПОСЛЕ ALC — голос из микрофона складывался бы с тоном
// поверх полной шкалы (тон ~0.98) → клип 24-бит → сплэттер на спектре. // поверх полной шкалы (тон ~0.98) → клип 24-бит → сплэттер на спектре.
FTXToneOn: Boolean; 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) и при изменении. // Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении.
// Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS. // Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS.
@@ -418,6 +448,8 @@ type
FTXWfAvgTimeMS: Double; FTXWfAvgTimeMS: Double;
FMode: Integer; FMode: Integer;
FTXMode: Integer; // may differ from RX when a slice is TX source
FRawFMDeviation: Double;
FSMeter: Double; FSMeter: Double;
FFilterLow: Integer; FFilterLow: Integer;
FFilterHigh: Integer; FFilterHigh: Integer;
@@ -454,7 +486,11 @@ type
procedure ApplyDSPRate(Mode: Integer); procedure ApplyDSPRate(Mode: Integer);
procedure ApplyRXWFMToChan(Chan: Integer); procedure ApplyRXWFMToChan(Chan: Integer);
procedure ApplyRXDMRToChan(Chan: Integer); procedure ApplyRXDMRToChan(Chan: Integer);
procedure ApplyRXRawToChan(Chan: Integer);
procedure ApplyWFMSettings; 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 FindSliceIdx(Id: Integer): Integer; // -1 если нет
function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан) function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан)
@@ -501,6 +537,7 @@ type
procedure ProcessTXBlock; // вызывается из TTXDSPThread procedure ProcessTXBlock; // вызывается из TTXDSPThread
procedure FeedTXDisplaySample(const I, Q: Double); procedure FeedTXDisplaySample(const I, Q: Double);
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
@@ -589,6 +626,7 @@ type
procedure SetSNB(Enable: Boolean); procedure SetSNB(Enable: Boolean);
procedure SetANF(Enable: Boolean); procedure SetANF(Enable: Boolean);
procedure SetRXFMDeviation(Deviation: Double); procedure SetRXFMDeviation(Deviation: Double);
procedure SetRawFMDeviation(Deviation: Double);
procedure SetFMSquelch(Enable: Boolean; Level: Integer); procedure SetFMSquelch(Enable: Boolean; Level: Integer);
procedure SetVolume(Vol: Double); procedure SetVolume(Vol: Double);
procedure SetMute(Mute: Boolean); procedure SetMute(Mute: Boolean);
@@ -606,6 +644,33 @@ type
// TUN: WDSP TXA PostGen — гонит чистый тон в DSP-цепь после всей // TUN: WDSP TXA PostGen — гонит чистый тон в DSP-цепь после всей
// обработки. Mag — амплитуда в [0..1], Freq — Hz (audio). // обработки. Mag — амплитуда в [0..1], Freq — Hz (audio).
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double); 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 управление (применяются мгновенно если канал открыт) // Расширенное TX управление (применяются мгновенно если канал открыт)
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer); procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
@@ -942,6 +1007,7 @@ begin
// de-emphasis. Those filters collapse the four DMR discriminator levels. // de-emphasis. Those filters collapse the four DMR discriminator levels.
// WFMD is a plain quadrature discriminator and permits flat data audio. // WFMD is a plain quadrature discriminator and permits flat data audio.
MODE_DMR: Result := WDSP_RX_WFM; MODE_DMR: Result := WDSP_RX_WFM;
MODE_FMRAW: Result := WDSP_RX_WFM;
else Result := WDSP_USB; else Result := WDSP_USB;
end; end;
end; end;
@@ -949,7 +1015,9 @@ end;
function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer; function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer;
// txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM. // txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM.
begin 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 if Mode = MODE_DMR then Result := WDSP_FM
else Result := ModeToWDSP(Mode); else Result := ModeToWDSP(Mode);
end; end;
@@ -1073,6 +1141,15 @@ begin
SetRXAWFMDeemphRun (Chan, 0); SetRXAWFMDeemphRun (Chan, 0);
end; 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; procedure TWDSPEngine.ApplyWFMSettings;
begin begin
ApplyRXWFMToChan(RXA_CHAN); ApplyRXWFMToChan(RXA_CHAN);
@@ -1082,6 +1159,36 @@ begin
SetTXAWFMPreEmphTau (TXA_CHAN, WFM_TAU_DE_US); SetTXAWFMPreEmphTau (TXA_CHAN, WFM_TAU_DE_US);
end; 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; function TWDSPEngine.ClampToNyquist(Hz, Rate: Integer): Integer;
var Limit: Integer; var Limit: Integer;
begin begin
@@ -1104,6 +1211,7 @@ begin
MODE_DIGL: SetFilter(-3000, 0); MODE_DIGL: SetFilter(-3000, 0);
MODE_WFM: SetFilter(-80000, 80000); MODE_WFM: SetFilter(-80000, 80000);
MODE_DMR: SetFilter(-6250, 6250); MODE_DMR: SetFilter(-6250, 6250);
MODE_FMRAW: SetFilter(-6250, 6250);
end; end;
end; end;
@@ -1129,6 +1237,7 @@ begin
FInitialized := False; FInitialized := False;
FAnalyzerOpen := False; FAnalyzerOpen := False;
FMode := MODE_USB; FMode := MODE_USB;
FTXMode := MODE_USB;
FFilterLow := 100; FFilterLow := 100;
FFilterHigh := 2400; FFilterHigh := 2400;
FAGCMode := agcMedium; FAGCMode := agcMedium;
@@ -1197,7 +1306,21 @@ begin
FTXMicTail := 0; FTXMicTail := 0;
FTXActive := False; FTXActive := False;
FTXToneOn := False; FTXToneOn := False;
FTXTTOn := False;
FKeepRXDuringTX := 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; FMonitorRXDuringTX := False;
FPostTXMuteUntil := 0; FPostTXMuteUntil := 0;
FActiveDisplayID := RX_DISP_ID; FActiveDisplayID := RX_DISP_ID;
@@ -1244,6 +1367,7 @@ begin
FTXEQFreqs[10] := 3500; FTXEQFreqs[10] := 3500;
FTXAMCarrier := 0.5; FTXAMCarrier := 0.5;
FTXFMDeviation := 5000.0; FTXFMDeviation := 5000.0;
FRawFMDeviation := 2500.0;
FTXFMLowCut := 300; FTXFMLowCut := 300;
FTXFMHighCut := 3000; FTXFMHighCut := 3000;
FTXFMEmphPos := 1; FTXFMEmphPos := 1;
@@ -1607,7 +1731,8 @@ begin
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок). // изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
); );
SetTXAMode(TXA_CHAN, ModeToWDSPTX(FMode)); FTXMode := FMode;
SetTXAMode(TXA_CHAN, ModeToWDSPTX(FTXMode));
SetTXAPanelRun(TXA_CHAN, 1); SetTXAPanelRun(TXA_CHAN, 1);
// PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I. // PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I.
SetTXAPanelSelect(TXA_CHAN, 2); SetTXAPanelSelect(TXA_CHAN, 2);
@@ -1661,6 +1786,10 @@ var
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
// PureSignal: снять подачу feedback ДО закрытия TXA — pscc зовётся из
// сетевого потока напрямую и не должен попасть в закрываемый канал.
FPSArmed := False;
// Останавливаем DSP поток перед закрытием WDSP каналов // Останавливаем DSP поток перед закрытием WDSP каналов
if Assigned(FDSPThread) then if Assigned(FDSPThread) then
begin begin
@@ -2246,6 +2375,7 @@ begin
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan); if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan); if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan);
if FSlices[Idx].Mode = MODE_FMRAW then ApplyRXRawToChan(Chan);
SetRXAShiftRun(Chan, 1); SetRXAShiftRun(Chan, 1);
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate),
@@ -2258,7 +2388,7 @@ begin
ApplyANFToChan(Chan, FSlices[Idx].ANFOn); ApplyANFToChan(Chan, FSlices[Idx].ANFOn);
ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel); ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel);
// Decoder input must not disappear or change scale with speaker volume. // 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) SetRXAPanelGain1(Chan, 1.0)
else else
SetRXAPanelGain1(Chan, FSlices[Idx].Volume); SetRXAPanelGain1(Chan, FSlices[Idx].Volume);
@@ -2308,7 +2438,7 @@ begin
if not (FSlices[s].Active and FSlices[s].Opened) then Continue; if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
if FSlices[s].PanId <> PanId then Continue; if FSlices[s].PanId <> PanId then Continue;
// Muting a DMR speaker must not make its decoder lose synchronization. // 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. // Свежая копия входного IQ — fexchange0 портит вход in-place.
for i := 0 to N - 1 do for i := 0 to N - 1 do
begin begin
@@ -2324,12 +2454,12 @@ begin
FSliceOutL[k] := FSliceOut[k * 2]; FSliceOutL[k] := FSliceOut[k * 2];
FSliceOutR[k] := FSliceOut[k * 2 + 1]; FSliceOutR[k] := FSliceOut[k * 2 + 1];
end; end;
if FSlices[s].Mode = MODE_DMR then if FSlices[s].Mode in [MODE_DMR, MODE_FMRAW] then
begin begin
if Assigned(FOnSliceDemodAudio) then if Assigned(FOnSliceDemodAudio) then
FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR,
FAudioBufSize); FAudioBufSize);
Continue; // never route the raw 4FSK discriminator to the speaker Continue; // decoder-grade signal uses the independent pre-volume route
end; end;
if Assigned(FOnSliceAudio) then if Assigned(FOnSliceAudio) then
begin begin
@@ -2550,7 +2680,8 @@ begin
SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode)); SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode));
if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan); if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan);
if Mode = MODE_DMR then ApplyRXDMRToChan(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) SetRXAPanelGain1(FSlices[idx].Chan, 1.0)
else else
SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume); SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume);
@@ -2599,7 +2730,7 @@ begin
try try
idx := FindSliceIdx(Id); if idx < 0 then Exit; idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].Volume := Vol; 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); SetRXAPanelGain1(FSlices[idx].Chan, Vol);
finally finally
FSliceLock.Leave; FSliceLock.Leave;
@@ -2732,6 +2863,7 @@ end;
procedure TWDSPEngine.SetMode(Mode: Integer); procedure TWDSPEngine.SetMode(Mode: Integer);
begin begin
FMode := Mode; FMode := Mode;
FTXMode := Mode;
if not FInitialized then Exit; if not FInitialized then Exit;
// Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd), // Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd),
// а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate. // а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate.
@@ -2744,7 +2876,9 @@ begin
SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION); SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION);
SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0); SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0);
SetRXAWFMDeemphRun (RXA_CHAN, 0); SetRXAWFMDeemphRun (RXA_CHAN, 0);
end; end
else if Mode = MODE_FMRAW then ApplyRXRawToChan(RXA_CHAN);
ApplyTXModeSettings(FTXMode);
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end; end;
@@ -2980,12 +3114,32 @@ end;
procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double); procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double);
begin begin
if not FInitialized then Exit; 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) SetRXAWFMDeviation(RXA_CHAN, Deviation)
else else
SetRXAFMDeviation(RXA_CHAN, Deviation); SetRXAFMDeviation(RXA_CHAN, Deviation);
end; 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); procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer);
var Threshold: Double; var Threshold: Double;
begin begin
@@ -3020,13 +3174,18 @@ end;
procedure TWDSPEngine.SetTXMode(Mode: Integer); procedure TWDSPEngine.SetTXMode(Mode: Integer);
begin begin
FTXMode := Mode;
if not FInitialized then Exit; if not FInitialized then Exit;
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode)); SetTXAMode(TXA_CHAN, ModeToWDSPTX(FTXMode));
ApplyTXModeSettings(FTXMode);
end; end;
procedure TWDSPEngine.SetTXFilter(Low, High: Integer); procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
begin begin
if not FInitialized then Exit; 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); SetTXABandpassFreqs(TXA_CHAN, Low, High);
end; end;
@@ -3139,6 +3298,41 @@ begin
SetTXAPostGenRun(TXA_CHAN, 0); SetTXAPostGenRun(TXA_CHAN, 0);
end; 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); procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean);
begin begin
// Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID // Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID
@@ -3179,9 +3373,9 @@ begin
tail := (tail + 1) and (TX_MIC_RING - 1); tail := (tail + 1) and (TX_MIC_RING - 1);
end; end;
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread) FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
// TUN: ринг потребляем (иначе поток зациклится на «есть полный блок»), // TUN/2TON: ринг потребляем (иначе поток зациклится на «есть полный блок»),
// но в TXA подаём тишину — тон PostGen должен идти без голоса поверх. // но в TXA подаём тишину — тон PostGen должен идти без голоса поверх.
if FTXToneOn then if FTXToneOn or FTXTTOn then
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end else end else
begin begin
@@ -3335,6 +3529,120 @@ begin
// CTCSS // CTCSS
SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq); SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq);
SetTXACTCSSRun(TXA_CHAN, Ord(FTXCTCSSOn)); 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; end;
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer); 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; procedure TWebAdapter.SyncMode;
begin 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 FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode
end; end;
+15 -12
View File
@@ -193,7 +193,7 @@ begin
'<select id="modGrpSel">' + '<select id="modGrpSel">' +
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' + '<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
'<option value="fm">FM</option><option value="am">AM</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>' +
'<select id="stepSel">' + '<select id="stepSel">' +
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' + '<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();}' + '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 MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM","DMR","FM RAW"];' +
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,10],am:[6,7],digi:[8,9],dmr:[11,11]};' + '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 MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am","digi","digi","fm","dmr"];' + '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 modGrpFromMode(m){return MODE_TO_GRP[m]||"ssb";}' +
'function modGrp(){return q("modGrpSel").value||"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 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 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("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("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});};' + 'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
@@ -320,6 +321,7 @@ begin
'fm:["NFM","FM"],' + 'fm:["NFM","FM"],' +
'wfm:["200k","160k","120k"],' + 'wfm:["200k","160k","120k"],' +
'dmr:["12.5k"],' + '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"]};' + '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],' + '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],' + 'dsb:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' +
@@ -328,13 +330,14 @@ begin
'fm:[11000,16000],' + 'fm:[11000,16000],' +
'wfm:[200000,160000,120000],' + 'wfm:[200000,160000,120000],' +
'dmr:[12500],' + 'dmr:[12500],' +
'raw:[6250,10000,12500,20000,25000],' +
'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' + 'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' +
'var fnpOv=null;' + '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 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 fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
'function showFnp(){' + 'function showFnp(){' +
'if((cur.mode|0)===11)return;' + 'if((cur.mode|0)===M_DMR)return;' +
'if(fnpOv){closeFnp();return;}' + 'if(fnpOv){closeFnp();return;}' +
'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' + 'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' +
'fnpOv=document.createElement("div");' + 'fnpOv=document.createElement("div");' +
@@ -791,7 +794,7 @@ begin
'const nowTx=!!cur.transmitting;if(nowTx!==prevTx){prevTx=nowTx;if(nowTx)startTXMic();else stopTXMic();}}' + '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 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 xToHz(x,w){return localCenter+((x/Math.max(1,w))-.5)*localSpan;}' +
'function hzToX(f,w){return((f-localCenter)/localSpan+.5)*w;}' + '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);' + 'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' +
'const st=localCenter-localSpan/2;' + '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){' + 'if(isFM&&fmStep>0){' +
'const pxPerStep=w*fmStep/localSpan;' + 'const pxPerStep=w*fmStep/localSpan;' +
'const labelMult=Math.max(1,Math.ceil((rctx.measureText("000.000").width+6)/pxPerStep));' + '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);' + '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();}' + '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 fmSt=FM_STEP_HZ[fmStepIdx],st2=localCenter-localSpan/2;' +
'const pxSt=w*fmSt/localSpan,gm=Math.max(1,Math.ceil(4/pxSt));' + 'const pxSt=w*fmSt/localSpan,gm=Math.max(1,Math.ceil(4/pxSt));' +
'const fn=Math.ceil(st2/fmSt);' + '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(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);}}' + '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="";' + '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);});' + '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];' + 'sel.value=FM_STEP_HZ[fmStepIdx];' +
@@ -937,7 +940,7 @@ begin
'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' + 'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' +
// клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули. // клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули.
// Спектр остаётся на месте, сервер пришлёт center_hz через ~200ms и всё перецентрируется. // Спектр остаётся на месте, сервер пришлёт 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());' + 'const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/snap)*snap,vfoMin(),vfoMax());' +
'setActiveVfoHz(nf);' + 'setActiveVfoHz(nf);' +
'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=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 uses
Classes, SysUtils, Math, Classes, SysUtils, Math,
WebUtils, WsClient, WebPageHtml WebUtils, WsClient, WebPageHtml, RadioModes
{$IFDEF WINDOWS}, Windows, WinSock2{$ELSE}, BaseUnix, Sockets{$ENDIF}, {$IFDEF WINDOWS}, Windows, WinSock2{$ELSE}, BaseUnix, Sockets{$ENDIF},
SyncObjs; // ← после платформенных юнитов: исключает конфликт идентификатора Create SyncObjs; // ← после платформенных юнитов: исключает конфликт идентификатора Create
@@ -560,7 +560,7 @@ begin
FStateLock := TCriticalSection.Create; FStateLock := TCriticalSection.Create;
// Начальные значения состояния // Начальные значения состояния
FFreq := 14200000; FFreq := 14200000;
FMode := 1; FMode := MODE_USB;
FFMStepIdx := 3; // 25 kHz default FFMStepIdx := 3; // 25 kHz default
FFilterBW:= 2700; FFilterBW:= 2700;
FAGCMode := 1; FAGCMode := 1;
@@ -1717,8 +1717,8 @@ end;
function TWebServer.BuildStateJson: string; function TWebServer.BuildStateJson: string;
const const
MODE_N: array[0..11] of string = MODE_N: array[RadioModes.MODE_MIN..RadioModes.MODE_MAX] 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');
var var
FS: TFormatSettings; FS: TFormatSettings;
XvtrJson, DevJson, RateJson, BandJson: string; XvtrJson, DevJson, RateJson, BandJson: string;
@@ -1790,7 +1790,7 @@ begin
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' + '"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
'"rx_mute_visible":%s,"rx_mute_on":%s}', '"rx_mute_visible":%s,"rx_mute_on":%s}',
[FFreq, FVfoB, FActiveVfo, [FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod Length(MODE_N)], FMode, MODE_N[EnsureRange(FMode, MODE_MIN, MODE_MAX)],
FFilterIdx, FFilterBW, FFilterIdx, FFilterBW,
FAGCMode, FAGCTop, FAGCMode, FAGCTop,
FSpanHz, FCenterHz, FVolume, FSpanHz, FCenterHz, FVolume,
+3 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="275"/> <BuildNr Value="276"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="105"> <MacroValues Count="106">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -125,6 +125,7 @@
<Macro103 Name="LCLWidgetType" Value="qt6"/> <Macro103 Name="LCLWidgetType" Value="qt6"/>
<Macro104 Name="LCLWidgetType" Value="qt6"/> <Macro104 Name="LCLWidgetType" Value="qt6"/>
<Macro105 Name="LCLWidgetType" Value="qt6"/> <Macro105 Name="LCLWidgetType" Value="qt6"/>
<Macro106 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>