mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+224
-1
@@ -84,7 +84,8 @@ type
|
||||
rfADCOverload, // ADC overload (рендер индикатора)
|
||||
rfBeaconLock, // QO-100 beacon lock: статус/коррекция
|
||||
rfDevice, // подключённое устройство сменилось
|
||||
rfDeviceList // список discovered устройств обновился
|
||||
rfDeviceList, // список discovered устройств обновился
|
||||
rfPanFreq // центр доп. пана уехал (ретюн DDC извне)
|
||||
);
|
||||
|
||||
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
|
||||
@@ -162,6 +163,12 @@ type
|
||||
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
|
||||
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
||||
FMSQLevel: Integer; // порог squelch 0..100
|
||||
// Зеркало DSP-состояния слайса (движок его не отдаёт обратно): нужно UI
|
||||
// флага и CAT-геттерам. Пишется только через SetSliceDSP.
|
||||
NRMode: Integer; // 0=выкл, 1=NR, 2=NR2 (как у главного)
|
||||
NBMode: Integer; // 0=выкл, 1=NB, 2=NB2
|
||||
SNB: Boolean;
|
||||
ANF: Boolean;
|
||||
DeviceIndex: Integer; // PortAudio-индекс вывода (-1 = default)
|
||||
DevName: string;
|
||||
Audio: TAudioOutput; // владеет; свой поток вывода
|
||||
@@ -330,6 +337,10 @@ type
|
||||
FTwoTone: Boolean; // 2TON активен
|
||||
FSplitTxB: Boolean;
|
||||
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX
|
||||
// Auto TX слайса (индекс = СЛОТ слайса, не Id): PTT по CAT-порту слайса сам
|
||||
// назначает его TX-источником. Флаг живёт на слоте, а не на слайсе, потому
|
||||
// что настройка привязана к букве (B..G) и переживает удаление слайса.
|
||||
FSliceAutoTx: array[0..MAX_SLICES-1] of Boolean;
|
||||
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
|
||||
FDisplayDuplex: Boolean;
|
||||
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
|
||||
@@ -563,6 +574,9 @@ type
|
||||
procedure ApplyBandDSP(const B: TBandSettings);
|
||||
// Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы.
|
||||
function FilterBWFor(M, Idx: Integer): Integer;
|
||||
// Обратное отображение: ближайший индекс пресета к полосе BW (для CAT/UI,
|
||||
// где полоса задана в Гц, а протокол оперирует индексом).
|
||||
function FilterIdxFor(M, BW: Integer): Integer;
|
||||
|
||||
// ---- XVTR (трансвертер) ----
|
||||
// ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть.
|
||||
@@ -644,6 +658,7 @@ type
|
||||
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
|
||||
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
|
||||
class function AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
|
||||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||||
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
|
||||
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
||||
@@ -675,9 +690,30 @@ type
|
||||
function ActiveMicInDevName: string;
|
||||
function SliceCount: Integer;
|
||||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||
// Слот ↔ слайс. Слот 0..MAX_SLICES-1 = буква B..G и не зависит от того,
|
||||
// создан ли слайс сейчас: на слот вешаются настройки (CAT-порт, Auto TX).
|
||||
class function SliceSlotLetter(Slot: Integer): Char;
|
||||
function SliceIdBySlot(Slot: Integer): Integer; // 0 = слот пуст
|
||||
function SliceSlotOf(Id: Integer): Integer; // -1 = нет такого слайса
|
||||
// Auto TX слайса (см. FSliceAutoTx). Геттер по слоту — для UI настроек,
|
||||
// по Id — для флага слайса и CAT.
|
||||
function SliceSlotAutoTx(Slot: Integer): Boolean;
|
||||
procedure SetSliceSlotAutoTx(Slot: Integer; On_: Boolean);
|
||||
function SliceAutoTx(Id: Integer): Boolean;
|
||||
// Заявка на передачу ОТ ИМЕНИ слайса (CAT-порт слайса). Единственная точка,
|
||||
// где внешняя PTT может сменить TX-источник, поэтому здесь же — защита
|
||||
// «в эфире одновременно только один слайс». Выполнять в потоке контроллера.
|
||||
procedure RequestSliceTx(SliceId: Integer; On_: Boolean);
|
||||
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
||||
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
|
||||
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
|
||||
// Границы ВКЛЮЧЁННОГО сейчас диапазона (трансвертер — его FreqBegin/FreqEnd,
|
||||
// иначе band-план бэкенда). False = диапазон неизвестен.
|
||||
function CurrentBandEdges(out LoHz, HiHz: Double): Boolean;
|
||||
// Перестроить слайс на TargetHz В ПРЕДЕЛАХ текущего диапазона: если частота
|
||||
// вне захваченной полосы пана — двигаем окно DDC. False = отказ (другой
|
||||
// диапазон либо окно не подвинуть, не оглушив главный приёмник).
|
||||
function TuneSliceInBand(Id: Integer; TargetHz: Double): Boolean;
|
||||
|
||||
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
||||
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр в ВИДИМЫХ
|
||||
@@ -1592,6 +1628,72 @@ begin
|
||||
Result := (TargetHz >= Lo) and (TargetHz <= Hi);
|
||||
end;
|
||||
|
||||
function TRadioController.CurrentBandEdges(out LoHz, HiHz: Double): Boolean;
|
||||
begin
|
||||
LoHz := 0; HiHz := 0;
|
||||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||||
begin
|
||||
// Трансвертер: диапазон — его собственный (2 м = 144..146 и т.п.).
|
||||
LoHz := FXvtrSettings.Entries[FCurrentXvtr].FreqBegin;
|
||||
HiHz := FXvtrSettings.Entries[FCurrentXvtr].FreqEnd;
|
||||
Result := HiHz > LoHz;
|
||||
end
|
||||
else
|
||||
Result := BandEdges(FCurrentBand, IsPluto, LoHz, HiHz);
|
||||
end;
|
||||
|
||||
function TRadioController.TuneSliceInBand(Id: Integer; TargetHz: Double): Boolean;
|
||||
// Свобода внешней программы: она двигает слайс куда угодно ВНУТРИ включённого
|
||||
// диапазона (2 м на трансвертере: 144.174 при окне на 145.5 — можно), но сменить
|
||||
// диапазон (7 МГц) — нет: это решение оператора, а не управляющего ПО.
|
||||
//
|
||||
// Если цель вне захваченной полосы — окно DDC переезжает центром на цель:
|
||||
// • доп. пан (свой DDC) — свободно;
|
||||
// • пан 0 — только если главный приёмник останется внутри нового окна
|
||||
// (одно железное окно на двоих; иначе главный оглох бы).
|
||||
var
|
||||
idx, PanId: Integer;
|
||||
Lo, Hi, Half, Dist, NewCenter, VisLo, VisHi: Double;
|
||||
begin
|
||||
Result := False;
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
if not CurrentBandEdges(Lo, Hi) then Exit;
|
||||
if (TargetHz < Lo) or (TargetHz > Hi) then Exit; // другой диапазон — отказ
|
||||
|
||||
PanId := FSlices[idx].PanId;
|
||||
if SliceFitsCapture(TargetHz, PanId) then
|
||||
begin
|
||||
SetSliceTarget(Id, TargetHz);
|
||||
Exit(True);
|
||||
end;
|
||||
|
||||
if PanId > 0 then
|
||||
begin
|
||||
if not PanDDCActive(PanId) then Exit;
|
||||
VisibleFreqBounds(VisLo, VisHi);
|
||||
NewCenter := EnsureRange(TargetHz, VisLo, VisHi);
|
||||
SetPanDDCFreq(PanId, NewCenter);
|
||||
SetSliceTarget(Id, TargetHz);
|
||||
Changed(rfPanFreq); // UI: шапка/зум-бар/флаги пана под новый центр
|
||||
Result := True;
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Пан 0: центр общий с главным приёмником. Ставим его посередине между
|
||||
// целью и активным VFO — тогда в окно попадают оба; не помещаются —
|
||||
// отказываем (перестраивать главный ради слайса нельзя).
|
||||
Half := FSampleRate * 0.475;
|
||||
Dist := Abs(TargetHz - ActiveVfoHz);
|
||||
if Dist > Half * 2 * 0.95 then Exit;
|
||||
NewCenter := (TargetHz + ActiveVfoHz) / 2;
|
||||
SetCenter(NewCenter); // несёт Changed(rfCenterFreq) + сдвиги слайсов
|
||||
SetSliceTarget(Id, TargetHz);
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ ---- Панадаптеры на аппаратных DDC (этап 3.1) ---- }
|
||||
|
||||
procedure TRadioController.ResetPanDDCMaps;
|
||||
@@ -1958,6 +2060,10 @@ begin
|
||||
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
|
||||
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
|
||||
FSlices[slot].FMSQLevel := 40;
|
||||
FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным
|
||||
FSlices[slot].NBMode := 0;
|
||||
FSlices[slot].SNB := False;
|
||||
FSlices[slot].ANF := False;
|
||||
FSlices[slot].DeviceIndex := DevIndex;
|
||||
FSlices[slot].DevName := DevName;
|
||||
FSlices[slot].Audio := AO;
|
||||
@@ -2115,6 +2221,10 @@ var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].NRMode := NRMode;
|
||||
FSlices[idx].NBMode := NBMode;
|
||||
FSlices[idx].SNB := SNBOn;
|
||||
FSlices[idx].ANF := ANFOn;
|
||||
if Assigned(FDSPEngine) then
|
||||
begin
|
||||
FDSPEngine.SetSliceNRMode(Id, NRMode);
|
||||
@@ -2383,6 +2493,86 @@ begin
|
||||
if Result then S := FSlices[idx];
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Слоты слайсов: настройки (CAT-порт, Auto TX) висят на слоте/букве, а слайс
|
||||
// на слоте может появляться и исчезать.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class function TRadioController.SliceSlotLetter(Slot: Integer): Char;
|
||||
begin
|
||||
// Слайс A — главный приёмник, поэтому слоты нумеруются с 'B' (как AddSlice).
|
||||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit('?');
|
||||
Result := Chr(Ord('B') + Slot);
|
||||
end;
|
||||
|
||||
function TRadioController.SliceIdBySlot(Slot: Integer): Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
|
||||
if FSlices[Slot].Active then Result := FSlices[Slot].Id;
|
||||
end;
|
||||
|
||||
function TRadioController.SliceSlotOf(Id: Integer): Integer;
|
||||
begin
|
||||
Result := FindSliceIndex(Id);
|
||||
end;
|
||||
|
||||
function TRadioController.SliceSlotAutoTx(Slot: Integer): Boolean;
|
||||
begin
|
||||
Result := (Slot >= 0) and (Slot < MAX_SLICES) and FSliceAutoTx[Slot];
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceSlotAutoTx(Slot: Integer; On_: Boolean);
|
||||
// Без Changed(): единственный вызывающий — TCATAdapter.ApplySettings, а он
|
||||
// работает и на старте (до постройки виджетов), и на connect. Перерисовку
|
||||
// бейджей делает UI-путь настроек (MainForm.OnSliceCATSettingsChange).
|
||||
begin
|
||||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
|
||||
FSliceAutoTx[Slot] := On_;
|
||||
end;
|
||||
|
||||
function TRadioController.SliceAutoTx(Id: Integer): Boolean;
|
||||
begin
|
||||
Result := SliceSlotAutoTx(FindSliceIndex(Id));
|
||||
end;
|
||||
|
||||
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean);
|
||||
// PTT, пришедшая по CAT-порту слайса. Аналог «Auto Switch TX Slice» в SmartSDR
|
||||
// CAT: с Auto TX слайс сам становится TX-источником, без него — передаёт,
|
||||
// только если оператор уже выбрал его источником (бейдж TX на флаге).
|
||||
// Выполняется в потоке контроллера (CAT маршалит через Invoke), поэтому
|
||||
// проверка «уже в эфире» и смена источника атомарны относительно GUI.
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(SliceId);
|
||||
if idx < 0 then Exit;
|
||||
|
||||
if not On_ then
|
||||
begin
|
||||
// Отпускание PTT снимает только СВОЮ передачу: RX; на порту слайса C не
|
||||
// должен обрывать эфир слайса B.
|
||||
if (FTxSliceId = SliceId) and (FTransmitting or FTuning) then
|
||||
begin
|
||||
if FTuning then SetTune(False) else SetMOX(False);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Передатчик один: пока кто-то в эфире (другой слайс, главный VFO, кнопка
|
||||
// MOX/TUNE, аппаратная PTT) — чужую передачу не перехватываем и источник не
|
||||
// переключаем. Своя же передача уже идёт, повторный TX; — no-op.
|
||||
if FTransmitting or FTuning then Exit;
|
||||
if FSlices[idx].Mode = MODE_DMR then Exit; // AMBE-передатчика нет
|
||||
|
||||
if FTxSliceId <> SliceId then
|
||||
begin
|
||||
if not FSliceAutoTx[idx] then Exit; // без Auto TX слайс сам себя не назначает
|
||||
SetTxSlice(SliceId);
|
||||
if FTxSliceId <> SliceId then Exit; // SetTxSlice отказал (DMR/нет слайса)
|
||||
end;
|
||||
SetMOX(True);
|
||||
end;
|
||||
|
||||
// Снимок кадра для web-зеркала: протокол фиксирован на 1024 точки, поэтому
|
||||
// при большем разрешении анализатора децимируем max-детектором (не обрезаем).
|
||||
procedure SnapshotPixels(const Pixels: array of Single; Count: Integer;
|
||||
@@ -2966,6 +3156,27 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.FilterIdxFor(M, BW: Integer): Integer;
|
||||
// Обратно к FilterBWFor: индекс пресета с минимальным отклонением от BW.
|
||||
var
|
||||
i, Cnt, D, Best: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
case M of
|
||||
MODE_FM: Cnt := FILT_FM_COUNT;
|
||||
MODE_WFM: Cnt := FILT_WFM_COUNT;
|
||||
MODE_FMRAW: Cnt := FILT_RAW_COUNT;
|
||||
MODE_DMR: Exit;
|
||||
else Cnt := FILT_COUNT;
|
||||
end;
|
||||
Best := MaxInt;
|
||||
for i := 0 to Cnt - 1 do
|
||||
begin
|
||||
D := Abs(FilterBWFor(M, i) - BW);
|
||||
if D < Best then begin Best := D; Result := i; end;
|
||||
end;
|
||||
end;
|
||||
|
||||
// =====================================================================
|
||||
// Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
|
||||
// =====================================================================
|
||||
@@ -3744,6 +3955,18 @@ begin
|
||||
Result := AGCModes[M];
|
||||
end;
|
||||
|
||||
class function TRadioController.AGCModeToUI(A: TWDSPAGCMode): Integer;
|
||||
// Обратно к AGCModeFromUI: 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF (шкала UI и CAT).
|
||||
begin
|
||||
case A of
|
||||
agcFast: Result := 0;
|
||||
agcSlow: Result := 2;
|
||||
agcLong: Result := 3;
|
||||
agcOff: Result := 4;
|
||||
else Result := 1; // agcMedium
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyAGCToEngine;
|
||||
begin
|
||||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
||||
|
||||
Reference in New Issue
Block a user