feat(cat): CAT-порт на каждый слайс + Auto TX (эталон SmartSDR CAT)

Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой TCP-порт. COM-порты и 19090 остаются за главным приёмником
(слайс A) и не тронуты.

Протокол не дублируется: TCATEngine — чистый парсер поверх записи
callback'ов TCATContext, TCATTcpServer — транспорт поверх движка, поэтому
слайс-CAT = тот же движок с другим контекстом. Новый TCATSliceEndpoint
(движок + TCP-сервер на слот), массивом владеет TCATAdapter. Привязка к
СЛОТУ (буква B..G), не к Id: порт живёт, даже когда слайса нет (PS0).

Auto TX = «Auto Switch TX Slice» из SmartSDR CAT. Вся логика в одной точке
— TRadioController.RequestSliceTx, исполняется в потоке контроллера:
  • пока кто-то уже в эфире (любой источник) — заявка игнорируется,
    перехвата передачи нет никогда;
  • без Auto TX слайс передаёт, только если уже выбран TX-источником;
  • RX; снимает только СВОЮ передачу;
  • TX-источник после отпускания остаётся на слайсе (как в SmartSDR).
На флаге слайса бейдж TX → AutoTX.

Частота слайса — TuneSliceInBand: свобода в пределах ВКЛЮЧЁННОГО диапазона
(трансвертер → его FreqBegin/FreqEnd, иначе band-план), другой диапазон —
отказ. Вне захваченной полосы окно DDC переезжает: доп. пан — центром на
цель (rfPanFreq → UI перекладывает флаги/шапку/зум-бар), пан 0 — центр
посередине между целью и главным VFO и только если оба влезают (общее
железное окно, главный приёмник не оглушаем).

Настройки: вкладка Slices (enable/порт 19091../Auto TX на слайс), персист
в секции cat + device-blob.

Попутно:
  • ZZFL/ZZFH больше не заглушки — новые callback'и кромок фильтра
    (у главного nil → прежний дефолт режима), хелпер SignedPad;
  • FilterIdxFor, AGCModeToUI, BoardUtils.BandEdges;
  • DSP-состояние слайса (NR/NB/SNB/ANF) зеркалится в TCtrlSlice — флаг
    слайса рисовал нули и AGC главного;
  • FUIReady в TMainForm.OnControllerState: адаптеры, создаваемые по ходу
    FormCreate, больше не могут уронить старт рендером до постройки
    виджетов (на этом падал Auto TX из настроек).

Проверено вживую: все порты 19091..19096 слушают, PS;/ZZFA;/MD;/ID;/SM0;/
ZZTX; отвечают; старт со всеми включёнными слайсами чист под gdb.
Документ — doc/CAT_SLICES.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:56:36 +03:00
co-authored by Claude Opus 5
parent 8611898ad1
commit 2d15da0462
13 changed files with 1357 additions and 34 deletions
+193 -3
View File
@@ -76,6 +76,11 @@ type
const SerParity: array of Integer;
const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer) of object;
// CAT дополнительных слайсов: индекс = слот (0 = слайс B).
TOnSliceCATChange = procedure(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean) of object;
TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
@@ -92,6 +97,7 @@ type
FPagePA: TScrollBox;
FPageAdvanced: TScrollBox;
FPageCAT: TScrollBox;
FPageSlices: TScrollBox;
FPageTransmit: TScrollBox;
FNavAudio: TFlatButton;
FNavDisplay: TFlatButton;
@@ -100,6 +106,7 @@ type
FNavPA: TFlatButton;
FNavAdvanced: TFlatButton;
FNavCAT: TFlatButton;
FNavSlices: TFlatButton;
FNavTransmit: TFlatButton;
// ---- Audio tab controls ----
@@ -335,6 +342,10 @@ type
FCATSerialAndromeda:array[0..3] of TFlatCheckBox;
FCATTcpEn: TFlatCheckBox;
FCATTcpPort: TFlatSpinEdit;
// ---- Slices tab controls (CAT доп. слайсов; индекс = слот, 0 = слайс B) ----
FSliceCATEn: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatCheckBox;
FSliceCATPort: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatSpinEdit;
FSliceCATAutoTx: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatCheckBox;
// ---- Advanced tab controls ----
FChkDither: TFlatCheckBox;
@@ -350,6 +361,7 @@ type
// ---- Callbacks ----
FOnCATChange: TOnCATChange;
FOnSliceCATChange: TOnSliceCATChange;
FOnPAChange: TOnPASettingsChange;
FOnPlutoTxChange: TOnPlutoTxChange;
FOnAnt936xChange: TOnAnt936xChange;
@@ -393,6 +405,7 @@ type
procedure CollectCalFromUI;
procedure OnCalAnyChange(Sender: TObject);
procedure BuildCATTab;
procedure BuildSlicesTab;
procedure BuildTransmitTab;
procedure BuildAlexTab;
procedure BuildAdvancedTab;
@@ -459,6 +472,8 @@ type
procedure OnCATSerialChange(Sender: TObject);
procedure OnCATTcpChange(Sender: TObject);
procedure FireCATChange;
procedure OnSliceCATCtrlChange(Sender: TObject);
procedure FireSliceCATChange;
procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange;
@@ -508,6 +523,11 @@ type
// Таблица разъёмов + имена/видимость слотов трансвертеров.
procedure LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
procedure LoadVHFCalSettings(const VHFCal: array of Double);
// Слайс-CAT: enable/порт/AutoTX по слотам (0 = слайс B).
procedure LoadSliceCATSettings(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
@@ -526,6 +546,7 @@ type
const BindAddr, User, Pass: string);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnSliceCATChange: TOnSliceCATChange read FOnSliceCATChange write FOnSliceCATChange;
property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange;
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
@@ -781,9 +802,10 @@ begin
FNavCalib := MakeNavButton('Calibration', 290);
FNavAdvanced := MakeNavButton('Advanced', 326);
FNavCAT := MakeNavButton('CAT', 362);
FNavAlex := MakeNavButton('Antenna / Alex', 398);
FNavXvtr := MakeNavButton('Transverter', 434);
FNavQO := MakeNavButton('QO-100', 470);
FNavSlices := MakeNavButton('Slices', 398);
FNavAlex := MakeNavButton('Antenna / Alex', 434);
FNavXvtr := MakeNavButton('Transverter', 470);
FNavQO := MakeNavButton('QO-100', 506);
FContentPanel := TPanel.Create(Self);
FContentPanel.Parent := Self;
@@ -800,6 +822,7 @@ begin
FPageCalib := MakeScrollPage;
FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage;
FPageSlices := MakeScrollPage;
FPageAlex := MakeScrollPage;
FPageXvtr := MakeScrollPage;
FPageQO := MakeScrollPage;
@@ -816,6 +839,7 @@ begin
BuildCalibrationTab;
BuildAdvancedTab;
BuildCATTab;
BuildSlicesTab;
BuildAlexTab;
BuildXvtrTab;
BuildQOTab;
@@ -849,6 +873,7 @@ begin
else if Sender = FNavCalib then SelectPage(FPageCalib, FNavCalib)
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
else if Sender = FNavSlices then SelectPage(FPageSlices, FNavSlices)
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr)
else if Sender = FNavQO then SelectPage(FPageQO, FNavQO);
@@ -865,6 +890,7 @@ begin
FPageCalib.Visible := APage = FPageCalib;
FPageAdvanced.Visible := APage = FPageAdvanced;
FPageCAT.Visible := APage = FPageCAT;
FPageSlices.Visible := APage = FPageSlices;
FPageAlex.Visible := APage = FPageAlex;
FPageXvtr.Visible := APage = FPageXvtr;
FPageQO.Visible := APage = FPageQO;
@@ -878,6 +904,7 @@ begin
FNavCalib.Active := ANav = FNavCalib;
FNavAdvanced.Active := ANav = FNavAdvanced;
FNavCAT.Active := ANav = FNavCAT;
FNavSlices.Active := ANav = FNavSlices;
FNavAlex.Active := ANav = FNavAlex;
FNavXvtr.Active := ANav = FNavXvtr;
FNavQO.Active := ANav = FNavQO;
@@ -3522,6 +3549,169 @@ begin
end;
end;
// ---------------------------------------------------------------------------
// BuildSlicesTab — CAT дополнительных слайсов (B..): свой TCP-порт на слайс.
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildSlicesTab;
const
MARGIN = 22;
PAD = 18;
LW = 120;
CX = PAD + LW + 8;
GRP_W = 320;
GRP_H = 168;
GRP_GAP = 16;
R1 = 42;
STEP = 32;
var
i, col, row, gx, gy: Integer;
Grp: TPanel;
Chk: TFlatCheckBox;
Spin: TFlatSpinEdit;
begin
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Slices';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'A separate CAT TCP port per additional slice — external software '
+ 'controls its own slice as a standalone transceiver.';
SetBounds(MARGIN, 48, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Slice A is the main receiver — it is configured on the CAT tab '
+ '(serial ports and the main TCP port).';
SetBounds(MARGIN, 66, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
row := i div 2;
col := i mod 2;
gx := MARGIN + col * (GRP_W + GRP_GAP);
gy := 96 + row * (GRP_H + GRP_GAP);
Grp := MakeGroupPanel(FPageSlices, 'Slice ' + Chr(Ord('B') + i), gx, gy, GRP_W, GRP_H);
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Enable CAT TCP server';
Chk.SetBounds(PAD, R1, GRP_W - PAD * 2, 22);
Chk.Font.Color := CLR_TEXT;
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
Chk.Tag := i;
Chk.OnChange := OnSliceCATCtrlChange;
FSliceCATEn[i] := Chk;
MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
Spin := TFlatSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(CX, R1 + STEP, 100, BTN_H + 2);
Spin.Color := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Name := UI_FONT; Spin.Font.Size := 9;
Spin.MinValue := 1; Spin.MaxValue := 65535;
Spin.Value := CFG_CAT_SLICE_PORT0 + i;
Spin.Tag := i;
Spin.OnChange := OnSliceCATCtrlChange;
FSliceCATPort[i] := Spin;
// Auto TX = «Auto Switch TX Slice» из SmartSDR CAT: команда TX по этому
// порту сама делает слайс источником передачи. Пока кто-то уже в эфире,
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
// (в ширину группы), пояснение — строкой ниже.
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Auto TX';
Chk.SetBounds(PAD, R1 + 2 * STEP, GRP_W - PAD * 2, 22);
Chk.Font.Color := CLR_TEXT;
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
Chk.Tag := i;
Chk.OnChange := OnSliceCATCtrlChange;
FSliceCATAutoTx[i] := Chk;
MakeLbl(Grp, 'CAT TX takes transmit to this slice',
PAD + 2, R1 + 2 * STEP + 24, GRP_W - PAD * 2 - 2);
end;
gy := 96 + ((CFG_CAT_SLICE_COUNT + 1) div 2) * (GRP_H + GRP_GAP);
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Only one slice transmits at a time: while anything is already on '
+ 'the air the request is ignored — no take-over.';
SetBounds(MARGIN, gy, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Without Auto TX a slice keys up over CAT only when it is already '
+ 'the transmit source (TX badge on the slice flag).';
SetBounds(MARGIN, gy + 18, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'The port is opened even when the slice does not exist yet — until '
+ 'it is created CAT answers "radio off" (PS0).';
SetBounds(MARGIN, gy + 36, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
end;
procedure TSettingsForm.FireSliceCATChange;
var
En, Auto: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
Prt: array[0..CFG_CAT_SLICE_COUNT-1] of Integer;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnSliceCATChange) then Exit;
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
En[i] := FSliceCATEn[i].Checked;
Prt[i] := FSliceCATPort[i].Value;
Auto[i] := FSliceCATAutoTx[i].Checked;
end;
FOnSliceCATChange(En, Prt, Auto);
end;
procedure TSettingsForm.OnSliceCATCtrlChange(Sender: TObject);
begin FireSliceCATChange; end;
procedure TSettingsForm.LoadSliceCATSettings(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean);
var i: Integer;
begin
FLoading := True;
try
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
if i <= High(Enabled) then FSliceCATEn[i].Checked := Enabled[i];
if (i <= High(Port)) and (Port[i] > 0) then
FSliceCATPort[i].Value := EnsureRange(Port[i], 1, 65535);
if i <= High(AutoTX) then FSliceCATAutoTx[i].Checked := AutoTX[i];
end;
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// BuildAlexTab
// ---------------------------------------------------------------------------