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
+24 -9
View File
@@ -82,6 +82,7 @@ type
FTx: Boolean; // передача идёт (красный)
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True)
FAutoTx: Boolean; // Auto TX: PTT по CAT-порту слайса сама берёт передачу
FSlice: Char; // буква слайса ('A')
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
FDMRText: string; // стабильный краткий статус рядом с полосой
@@ -205,6 +206,8 @@ type
procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX)
// Auto TX слайса (настройка CAT): бейдж TX превращается в AutoTX.
procedure SetAutoTxState(AOn: Boolean);
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
const CurName: string);
// Список устройств ВВОДА + текущий выбор ('' = общий вход приложения).
@@ -1091,8 +1094,8 @@ end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var
FreqStr, SStr, BWStr, DMRStr: string;
CurY, X, RW, i, DMRX, DMRRight, TxRight: Integer;
FreqStr, SStr, BWStr, DMRStr, TxStr: string;
CurY, X, RW, i, DMRX, DMRRight, TxRight, TxW: Integer;
R: TRect;
BarL, BarW, MeterX: Integer;
begin
@@ -1117,6 +1120,11 @@ begin
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
// Она крайняя справа, TX и RXM выстраиваются левее неё.
TxRight := W - PAD;
// Слайс с Auto TX (PTT по своему CAT-порту сама берёт передачу) подписан
// AutoTX — бейдж шире, остальные едут левее.
TxStr := 'TX';
TxW := 26;
if FAutoTx and (not FIsMain) then begin TxStr := 'AutoTX'; TxW := 46; end;
if not FIsMain then
begin
R := Rect(W - PAD - 14, CurY, W - PAD, CurY + HDR_BADGE_H);
@@ -1126,7 +1134,7 @@ begin
// RXM — приём слайса на СВОЕЙ передаче. Подсвечен (умолч.) = глушим, т.е.
// себя не слышим; погашен = self-monitor (свой downlink через транспондер).
// У главного приёмника то же самое делает кнопка RX MUTE на левой панели.
R := Rect(TxRight - 26 - 4 - 32, CurY, TxRight - 26 - 4, CurY + HDR_BADGE_H);
R := Rect(TxRight - TxW - 4 - 32, CurY, TxRight - TxW - 4, CurY + HDR_BADGE_H);
if FRxMuteTx then DrawBadge(C, R, 'RXM', CLR_ACCENT, CLR_BADGE_TXT)
else DrawBadge(C, R, 'RXM', CLR_BTN_NORM, CLR_DIM);
RegisterHit(R, HIT_RXMUTE);
@@ -1144,7 +1152,7 @@ begin
begin
DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8;
DMRRight := TxRight - 26 - 4 - 32 - 5; // до бейджей RXM, TX и ×
DMRRight := TxRight - TxW - 4 - 32 - 5; // до бейджей RXM, TX и ×
while (Length(DMRStr) > 1) and
(C.TextWidth(DMRStr) > DMRRight - DMRX) do
SetLength(DMRStr, Length(DMRStr) - 1);
@@ -1153,12 +1161,12 @@ begin
C.TextOut(DMRX, CurY + 1, DMRStr);
end;
// TX badge: a DMR slice is visibly disabled and has no hit target.
R := Rect(TxRight - 26, CurY, TxRight, CurY + HDR_BADGE_H);
R := Rect(TxRight - TxW, CurY, TxRight, CurY + HDR_BADGE_H);
if not TxSelectionAllowed then
DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM)
else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite)
else if FTxSel then DrawBadge(C, R, 'TX', CLR_TX_SEL, CLR_BADGE_TXT)
else DrawBadge(C, R, 'TX', CLR_BTN_NORM, CLR_DIM);
DrawBadge(C, R, TxStr, CLR_CARD_BG, CLR_DIM)
else if FTx then DrawBadge(C, R, TxStr, CLR_TX_ON, clWhite)
else if FTxSel then DrawBadge(C, R, TxStr, CLR_TX_SEL, CLR_BADGE_TXT)
else DrawBadge(C, R, TxStr, CLR_BTN_NORM, CLR_DIM);
if TxSelectionAllowed then RegisterHit(R, HIT_TX);
// SPLIT (левее TX) — кликабельно; только у главного: split = TX на VFO B,
// у слайса своего B-VFO нет (кросс-частотный TX делается TX-бейджем слайса).
@@ -1653,6 +1661,13 @@ begin
RequestInvalidate;
end;
procedure TVfoOverlay.SetAutoTxState(AOn: Boolean);
begin
if FAutoTx = AOn then Exit;
FAutoTx := AOn;
RequestInvalidate;
end;
procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
begin
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;