diff --git a/BoardUtils.pas b/BoardUtils.pas
index 1a6759a..d35a361 100644
--- a/BoardUtils.pas
+++ b/BoardUtils.pas
@@ -24,6 +24,8 @@ function FreqToBandIdx(Hz: Double; Pluto: Boolean = False): Integer;
function BandName(Idx: Integer; Pluto: Boolean = False): string;
// Частота по умолчанию при входе в диапазон (если нет сохранённой/в диапазоне).
function BandDefaultFreq(Idx: Integer; Pluto: Boolean = False): Double;
+// Границы диапазона по плану. False = слот пуст/индекс вне плана (Lo=Hi=0).
+function BandEdges(Idx: Integer; Pluto: Boolean; out LoHz, HiHz: Double): Boolean;
// Полный band-план активного бэкенда (имена + дефолтные частоты).
function BuildBandPlan(Pluto: Boolean): TBandPlanArray;
@@ -91,6 +93,16 @@ begin
else Result := 'AD936x SDR';
end;
+function BandEdges(Idx: Integer; Pluto: Boolean; out LoHz, HiHz: Double): Boolean;
+begin
+ LoHz := 0; HiHz := 0;
+ Result := (Idx >= 0) and (Idx < BAND_COUNT);
+ if not Result then Exit;
+ if Pluto then begin LoHz := VU_LO[Idx]; HiHz := VU_HI[Idx]; end
+ else begin LoHz := HF_LO[Idx]; HiHz := HF_HI[Idx]; end;
+ Result := HiHz > LoHz; // пустой 12-й слот HF-плана
+end;
+
function FreqToBandIdx(Hz: Double; Pluto: Boolean): Integer;
var
i: Integer;
diff --git a/CATAdapter.pas b/CATAdapter.pas
index 7682c29..ecc6014 100644
--- a/CATAdapter.pas
+++ b/CATAdapter.pas
@@ -20,8 +20,8 @@ interface
uses
Classes, SysUtils,
- RadioController, CATEngine, CATSerial, CATTcp, Settings, WDSPEngine,
- AndromedaPanel;
+ RadioController, CATEngine, CATSerial, CATTcp, CATSliceEndpoint, Settings,
+ WDSPEngine, AndromedaPanel;
type
TCATAdapter = class
@@ -30,6 +30,9 @@ type
FEngine: TCATEngine;
FSerial: TCATSerialManager;
FTcp: TCATTcpServer;
+ // CAT дополнительных слайсов: свой движок+TCP-порт на слот (B..). Порты
+ // главного (serial + FTcp) остаются за главным приёмником — слайс A.
+ FSliceEps: array[0..CAT_SLICE_SLOTS-1] of TCATSliceEndpoint;
FPanel: TAndromedaPanel; // передняя панель Andromeda/G2 (если включена)
// Параметры команды для маршалинга в поток контроллера (через Invoke).
FSyncFreq: Double;
@@ -130,6 +133,7 @@ type
procedure SyncFMStep;
procedure SyncAGCTop;
procedure SyncCTun;
+ procedure ApplySliceSettings(const G: TGlobalSettings); // CAT-порты слайсов
public
constructor Create(AController: TRadioController);
destructor Destroy; override;
@@ -142,6 +146,7 @@ implementation
constructor TCATAdapter.Create(AController: TRadioController);
var
Ctx: TCATContext;
+ i: Integer;
begin
inherited Create;
FController := AController;
@@ -211,13 +216,18 @@ begin
FSerial := TCATSerialManager.Create(FEngine);
FTcp := TCATTcpServer.Create(FEngine);
+ for i := 0 to CAT_SLICE_SLOTS - 1 do
+ FSliceEps[i] := TCATSliceEndpoint.Create(FController, i);
+
// Передняя панель: отправляет наружу через Andromeda-порт, слушает состояние.
FPanel := TAndromedaPanel.Create(FController, @FSerial.SendToAndromeda);
FController.AddStateListener(@FPanel.OnControllerState);
end;
destructor TCATAdapter.Destroy;
+var i: Integer;
begin
+ for i := 0 to CAT_SLICE_SLOTS - 1 do FreeAndNil(FSliceEps[i]);
if Assigned(FTcp) then begin FTcp.Stop; FreeAndNil(FTcp); end;
if Assigned(FSerial) then begin FSerial.StopAll; FreeAndNil(FSerial); end;
FreeAndNil(FEngine);
@@ -253,6 +263,28 @@ begin
FTcp.Port := G.CATTcpPort;
FTcp.Start;
end;
+ ApplySliceSettings(G);
+end;
+
+procedure TCATAdapter.ApplySliceSettings(const G: TGlobalSettings);
+// CAT слайсов: Auto TX — в контроллер (его читают TX-логика и флаг слайса),
+// порт — в свой эндпоинт. Совпадающие порты не поднимаем: второй bind всё
+// равно упадёт, а так поведение предсказуемо (выигрывает слот меньшей буквы).
+var
+ i, j: Integer;
+ Enable: Boolean;
+begin
+ for i := 0 to CAT_SLICE_SLOTS - 1 do
+ begin
+ FController.SetSliceSlotAutoTx(i, G.CATSliceAutoTX[i]);
+ Enable := G.CATSliceEnabled[i] and (G.CATSlicePort[i] > 0);
+ if Enable and G.CATTcpEnabled and (G.CATSlicePort[i] = G.CATTcpPort) then
+ Enable := False;
+ for j := 0 to i - 1 do
+ if G.CATSliceEnabled[j] and (G.CATSlicePort[j] = G.CATSlicePort[i]) then
+ Enable := False;
+ FSliceEps[i].Apply(Enable, G.CATSlicePort[i]);
+ end;
end;
// ── Геттеры ──────────────────────────────────────────────────────────────────
diff --git a/CATEngine.pas b/CATEngine.pas
index 2a1ccbe..7993e69 100644
--- a/CATEngine.pas
+++ b/CATEngine.pas
@@ -106,6 +106,12 @@ type
SetAGCTop: TCATSetIntProc; // AGC-T порог, dB
GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл
SetCTun: TCATSetBoolProc;
+ // Кромки фильтра в Гц (ZZFL/ZZFH). Главный приёмник настраивается пресетами
+ // (callback'и nil → команды отвечают дефолтом режима), слайс — кромками.
+ GetFilterLow: TCATGetIntFunc;
+ GetFilterHigh: TCATGetIntFunc;
+ SetFilterLow: TCATSetIntProc;
+ SetFilterHigh: TCATSetIntProc;
// Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS)
OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO
OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг
@@ -119,6 +125,7 @@ type
FLock: TCriticalSection;
function Pad(V: Int64; W: Integer): string;
+ function SignedPad(V: Int64; W: Integer): string; // '+nnn' / '-nnn'
function FreqToStr(Hz: Double): string;
function StrToFreq(const S: string): Double;
function ModeToK(M: Integer): Integer;
@@ -363,6 +370,13 @@ begin
if Length(Result) > W then Result := Copy(Result, Length(Result) - W + 1, W);
end;
+function TCATEngine.SignedPad(V: Int64; W: Integer): string;
+// Знаковое поле фиксированной ширины (ZZAR/ZZFL/ZZFH): знак + W цифр.
+begin
+ if V >= 0 then Result := '+' + Pad(V, W)
+ else Result := '-' + Pad(-V, W);
+end;
+
function TCATEngine.FreqToStr(Hz: Double): string;
begin
Result := Pad(Round(Hz), 11);
@@ -1447,9 +1461,7 @@ begin
if Assigned(FCtx.SetAGCTop) then FCtx.SetAGCTop(v);
Result := '';
end else if Length(s) = 0 then begin
- v := SafeGetAGCTop;
- if v >= 0 then Result := '+' + Pad(v, 3)
- else Result := '-' + Pad(-v, 3);
+ Result := SignedPad(SafeGetAGCTop, 3);
end else
Result := CAT_ERROR;
end;
@@ -1653,26 +1665,29 @@ function TCATEngine.ZZFI(const s: string): string;
begin Result := ZZBI(s); end;
function TCATEngine.ZZFL(const s: string): string;
-// ZZFL: filter low cut (signed Hz, e.g. "-150")
-// STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs
+// ZZFL: filter low cut (signed Hz, e.g. "-0150"). Работает, если контекст умеет
+// кромки (слайс-CAT); у главного приёмника фильтр индексный — заглушка.
begin
- if Length(s) >= 4 then
- Result := '' // accept but ignore — STUB
- else if Length(s) = 0 then begin
- if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '-0500'
+ if Length(s) >= 4 then begin
+ if Assigned(FCtx.SetFilterLow) then FCtx.SetFilterLow(StrToIntDef(s, 0));
+ Result := '';
+ end else if Length(s) = 0 then begin
+ if Assigned(FCtx.GetFilterLow) then Result := SignedPad(FCtx.GetFilterLow, 4)
+ else if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '-0500'
else Result := '-1500';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZFH(const s: string): string;
-// ZZFH: filter high cut (signed Hz)
-// STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs
+// ZZFH: filter high cut (signed Hz) — см. ZZFL.
begin
- if Length(s) >= 4 then
- Result := '' // accept but ignore — STUB
- else if Length(s) = 0 then begin
- if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '+0500'
+ if Length(s) >= 4 then begin
+ if Assigned(FCtx.SetFilterHigh) then FCtx.SetFilterHigh(StrToIntDef(s, 0));
+ Result := '';
+ end else if Length(s) = 0 then begin
+ if Assigned(FCtx.GetFilterHigh) then Result := SignedPad(FCtx.GetFilterHigh, 4)
+ else if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '+0500'
else Result := '+3000';
end else
Result := CAT_ERROR;
diff --git a/CATSliceEndpoint.pas b/CATSliceEndpoint.pas
new file mode 100644
index 0000000..2c2ec41
--- /dev/null
+++ b/CATSliceEndpoint.pas
@@ -0,0 +1,571 @@
+unit CATSliceEndpoint;
+
+// -----------------------------------------------------------------------------
+// TCATSliceEndpoint — CAT-порт ОДНОГО дополнительного слайса (B, C, …).
+//
+// Протокол не дублируется: движок TCATEngine работает поверх плоской записи
+// callback'ов TCATContext, поэтому «CAT слайса» — это тот же движок с другим
+// контекстом, где геттеры/сеттеры смотрят в FSlices[слот], а не в главный RX.
+// Транспорт — тот же TCATTcpServer, просто на своём порту.
+//
+// TCATAdapter ──┬── главный контекст ── TCATEngine ── serial + TCP 19090
+// └── TCATSliceEndpoint ── TCATEngine ── TCP 19091 (слайс B)
+// TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) …
+//
+// Эталон поведения — SmartSDR CAT: порт привязан к слайсу, а флаг Auto TX
+// («Auto Switch TX Slice») разрешает команде TX самой сделать этот слайс
+// источником передачи. Вся защита «в эфире одновременно только один слайс»
+// живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера).
+//
+// Привязка — к СЛОТУ (букве), а не к Id слайса: слайс на слоте может быть
+// создан/удалён в любой момент, порт при этом остаётся поднятым. Когда слайса
+// нет, эндпоинт отвечает «выключенным радио» (PS0) и игнорирует команды.
+// -----------------------------------------------------------------------------
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, Math, SyncObjs,
+ RadioController, CATEngine, CATTcp, Settings, WDSPEngine, RadioModes;
+
+const
+ // Слотов слайс-CAT = потолок слайсов движка (буквы B..). Настройки хранят
+ // ровно столько же полей — иначе персист разъедется с рантаймом.
+ CAT_SLICE_SLOTS = MAX_SLICES;
+{$IF CAT_SLICE_SLOTS <> CFG_CAT_SLICE_COUNT}
+ {$ERROR CFG_CAT_SLICE_COUNT must match WDSPEngine.MAX_SLICES}
+{$IFEND}
+
+type
+ TCATSliceEndpoint = class
+ private
+ FController: TRadioController;
+ FSlot: Integer; // 0 = слайс B
+ FEngine: TCATEngine;
+ FTcp: TCATTcpServer;
+ FPort: Integer;
+ FEnabled: Boolean;
+ // Теневой VFO B: своего второго VFO у слайса нет, но управляющее ПО любит
+ // его читать/писать (split-режимы). Храним значение, эфир им не двигаем.
+ FShadowVfoB: Double;
+ // Параметры команды для маршалинга в поток контроллера (Invoke). Один
+ // эндпоинт может обслуживать несколько TCP-клиентов, поэтому «положить
+ // параметр + вызвать Invoke» защищено локом.
+ FCmdLock: TCriticalSection;
+ FSyncFreq: Double;
+ FSyncInt: Integer;
+ FSyncBool: Boolean;
+
+ function GetLetter: Char;
+ function SliceId: Integer; // 0 = слайса на слоте нет
+ function Slice(out S: TCtrlSlice): Boolean;
+ function SliceBW(const S: TCtrlSlice): Integer; // ширина фильтра под режим
+
+ // ── Геттеры (CAT-поток, read-only) ──
+ function GetVfoA: Double;
+ function GetVfoB: Double;
+ function GetMode: Integer;
+ function GetFilterIdx: Integer;
+ function GetFilterBW: Integer;
+ function GetFilterLow: Integer;
+ function GetFilterHigh: Integer;
+ function GetAGCMode: Integer;
+ function GetVolume: Integer;
+ function GetDriveLevel: Integer;
+ function GetNRMode: Integer;
+ function GetNBMode: Integer;
+ function GetSNB: Boolean;
+ function GetANF: Boolean;
+ function GetTX: Boolean;
+ function GetRunning: Boolean;
+ function GetSMeter: Double;
+ function GetBand: Integer;
+ function GetSquelchOn: Boolean;
+ function GetSquelchLevel: Integer;
+
+ // ── Сеттеры (CAT-поток → Invoke в поток контроллера) ──
+ procedure SetVfoA(V: Double);
+ procedure SetVfoB(V: Double);
+ procedure SetMode(V: Integer);
+ procedure SetFilterIdx(V: Integer);
+ procedure SetFilterLow(V: Integer);
+ procedure SetFilterHigh(V: Integer);
+ procedure SetAGCMode(V: Integer);
+ procedure SetVolume(V: Integer);
+ procedure SetDriveLevel(V: Integer);
+ procedure SetNRMode(V: Integer);
+ procedure SetNBMode(V: Integer);
+ procedure SetSNB(V: Boolean);
+ procedure SetANF(V: Boolean);
+ procedure SetTransmitting(V: Boolean);
+ procedure SetSquelchOn(V: Boolean);
+ procedure SetSquelchLevel(V: Integer);
+ procedure DoTuneUp;
+ procedure DoTuneDown;
+
+ // ── Sync-методы (поток контроллера) ──
+ procedure SyncVfoA;
+ procedure SyncMode;
+ procedure SyncFilterIdx;
+ procedure SyncFilterLow;
+ procedure SyncFilterHigh;
+ procedure SyncAGC;
+ procedure SyncVolume;
+ procedure SyncDrive;
+ procedure SyncNR;
+ procedure SyncNB;
+ procedure SyncSNB;
+ procedure SyncANF;
+ procedure SyncTx;
+ procedure SyncSquelchOn;
+ procedure SyncSquelchLevel;
+ procedure SyncTuneBy;
+
+ // Параметр команды + Invoke под локом: между «положить» и «исполнить»
+ // не должен вклиниться второй клиент этого же порта.
+ procedure CmdFreq(V: Double; P: TThreadMethod);
+ procedure CmdInt (V: Integer; P: TThreadMethod);
+ procedure CmdBool(V: Boolean; P: TThreadMethod);
+ procedure ApplyDSP(NR, NB: Integer; SNBOn, ANFOn: Boolean);
+ public
+ constructor Create(AController: TRadioController; ASlot: Integer);
+ destructor Destroy; override;
+ // Поднять/опустить TCP-порт эндпоинта. Перезапуск только при реальном
+ // изменении — иначе правка чужой строки настроек рвала бы живые сессии.
+ procedure Apply(AEnabled: Boolean; APort: Integer);
+ property Slot: Integer read FSlot;
+ property Port: Integer read FPort;
+ property Enabled: Boolean read FEnabled;
+ property Letter: Char read GetLetter;
+ end;
+
+implementation
+
+constructor TCATSliceEndpoint.Create(AController: TRadioController; ASlot: Integer);
+var
+ Ctx: TCATContext;
+begin
+ inherited Create;
+ FController := AController;
+ FSlot := ASlot;
+ FPort := 0;
+ FEnabled := False;
+ FShadowVfoB := 0;
+ FCmdLock := TCriticalSection.Create;
+
+ FillChar(Ctx, SizeOf(Ctx), 0);
+ // Слайс-скоуп.
+ Ctx.GetVfoA := @GetVfoA;
+ Ctx.GetVfoB := @GetVfoB;
+ Ctx.GetMode := @GetMode;
+ Ctx.GetFilterIdx := @GetFilterIdx;
+ Ctx.GetFilterBW := @GetFilterBW;
+ Ctx.GetFilterLow := @GetFilterLow;
+ Ctx.GetFilterHigh := @GetFilterHigh;
+ Ctx.GetAGCMode := @GetAGCMode;
+ Ctx.GetVolume := @GetVolume;
+ Ctx.GetNRMode := @GetNRMode;
+ Ctx.GetNBMode := @GetNBMode;
+ Ctx.GetSNBEnabled := @GetSNB;
+ Ctx.GetANFEnabled := @GetANF;
+ Ctx.GetTransmitting := @GetTX;
+ Ctx.GetRunning := @GetRunning;
+ Ctx.GetSMeter := @GetSMeter;
+ Ctx.GetSquelchOn := @GetSquelchOn;
+ Ctx.GetSquelchLevel := @GetSquelchLevel;
+ Ctx.SetVfoA := @SetVfoA;
+ Ctx.SetVfoB := @SetVfoB;
+ Ctx.SetMode := @SetMode;
+ Ctx.SetFilterIdx := @SetFilterIdx;
+ Ctx.SetFilterLow := @SetFilterLow;
+ Ctx.SetFilterHigh := @SetFilterHigh;
+ Ctx.SetAGCMode := @SetAGCMode;
+ Ctx.SetVolume := @SetVolume;
+ Ctx.SetNRMode := @SetNRMode;
+ Ctx.SetNBMode := @SetNBMode;
+ Ctx.SetSNBEnabled := @SetSNB;
+ Ctx.SetANFEnabled := @SetANF;
+ Ctx.SetTransmitting := @SetTransmitting;
+ Ctx.SetSquelchOn := @SetSquelchOn;
+ Ctx.SetSquelchLevel := @SetSquelchLevel;
+ Ctx.DoTuneUp := @DoTuneUp;
+ Ctx.DoTuneDown := @DoTuneDown;
+ // Глобальные (общие с главным) параметры: передатчик один на всё радио.
+ Ctx.GetDriveLevel := @GetDriveLevel;
+ Ctx.SetDriveLevel := @SetDriveLevel;
+ Ctx.GetCurrentBand := @GetBand;
+ // Осознанно НЕ проброшены: split/VFO-переключение (у слайса нет второго VFO),
+ // смена диапазона (диапазон — свойство пана слайса), CTUN/AGC-T/CTCSS/репитер
+ // (настройки главного тракта). Движок ответит на них валидной заглушкой.
+
+ FEngine := TCATEngine.Create(Ctx);
+ FTcp := TCATTcpServer.Create(FEngine);
+end;
+
+destructor TCATSliceEndpoint.Destroy;
+begin
+ if Assigned(FTcp) then begin FTcp.Stop; FreeAndNil(FTcp); end;
+ FreeAndNil(FEngine);
+ FreeAndNil(FCmdLock);
+ inherited Destroy;
+end;
+
+function TCATSliceEndpoint.GetLetter: Char;
+begin
+ Result := TRadioController.SliceSlotLetter(FSlot);
+end;
+
+procedure TCATSliceEndpoint.Apply(AEnabled: Boolean; APort: Integer);
+begin
+ APort := EnsureRange(APort, 1, 65535);
+ if (AEnabled = FEnabled) and (APort = FPort) and (FTcp.Running = AEnabled) then Exit;
+ FTcp.Stop;
+ FEnabled := AEnabled;
+ FPort := APort;
+ if FEnabled then
+ begin
+ FTcp.Port := FPort;
+ FTcp.Start;
+ end;
+end;
+
+// ── Доступ к слайсу слота ────────────────────────────────────────────────────
+
+function TCATSliceEndpoint.SliceId: Integer;
+begin
+ Result := FController.SliceIdBySlot(FSlot);
+end;
+
+function TCATSliceEndpoint.Slice(out S: TCtrlSlice): Boolean;
+var Id: Integer;
+begin
+ Id := SliceId;
+ Result := (Id > 0) and FController.GetSlice(Id, S);
+end;
+
+function TCATSliceEndpoint.SliceBW(const S: TCtrlSlice): Integer;
+// Как в UI-флаге: у односторонних режимов полоса = «своя» кромка.
+begin
+ case S.Mode of
+ MODE_LSB, MODE_DIGL: Result := -S.FilterLo;
+ MODE_USB, MODE_DIGU: Result := S.FilterHi;
+ else Result := S.FilterHi - S.FilterLo;
+ end;
+end;
+
+// ── Геттеры ──────────────────────────────────────────────────────────────────
+
+function TCATSliceEndpoint.GetVfoA: Double;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.TargetHz else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetVfoB: Double;
+begin
+ Result := FShadowVfoB;
+ if Result <= 0 then Result := GetVfoA;
+end;
+
+function TCATSliceEndpoint.GetMode: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.Mode else Result := MODE_USB;
+end;
+
+function TCATSliceEndpoint.GetFilterIdx: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := FController.FilterIdxFor(S.Mode, SliceBW(S))
+ else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetFilterBW: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := SliceBW(S) else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetFilterLow: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.FilterLo else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetFilterHigh: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.FilterHi else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetAGCMode: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := TRadioController.AGCModeToUI(S.AGC)
+ else Result := 1;
+end;
+
+function TCATSliceEndpoint.GetVolume: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := EnsureRange(Round(S.Volume * 100), 0, 100)
+ else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetDriveLevel: Integer;
+begin Result := FController.FDrivePercent; end;
+
+function TCATSliceEndpoint.GetNRMode: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.NRMode else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetNBMode: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.NBMode else Result := 0;
+end;
+
+function TCATSliceEndpoint.GetSNB: Boolean;
+var S: TCtrlSlice;
+begin
+ Result := Slice(S) and S.SNB;
+end;
+
+function TCATSliceEndpoint.GetANF: Boolean;
+var S: TCtrlSlice;
+begin
+ Result := Slice(S) and S.ANF;
+end;
+
+function TCATSliceEndpoint.GetTX: Boolean;
+// «Передаю» для этого порта = передаёт именно ЭТОТ слайс. Чужой эфир на
+// CAT-порту слайса виден как приём (иначе управляющее ПО решит, что его PTT
+// прошла, и будет ждать несуществующую передачу).
+var Id: Integer;
+begin
+ Id := SliceId;
+ Result := (Id > 0) and FController.FTransmitting and (FController.FTxSliceId = Id);
+end;
+
+function TCATSliceEndpoint.GetRunning: Boolean;
+begin
+ Result := (SliceId > 0) and FController.FRunning;
+end;
+
+function TCATSliceEndpoint.GetSMeter: Double;
+var Id: Integer;
+begin
+ Id := SliceId;
+ if Id > 0 then Result := FController.SliceSMeter(Id) else Result := -140;
+end;
+
+function TCATSliceEndpoint.GetBand: Integer;
+begin Result := FController.FCurrentBand; end;
+
+function TCATSliceEndpoint.GetSquelchOn: Boolean;
+var S: TCtrlSlice;
+begin
+ Result := Slice(S) and S.FMSQOn;
+end;
+
+function TCATSliceEndpoint.GetSquelchLevel: Integer;
+var S: TCtrlSlice;
+begin
+ if Slice(S) then Result := S.FMSQLevel else Result := 0;
+end;
+
+// ── Сеттеры (маршалинг в поток контроллера) ──────────────────────────────────
+
+procedure TCATSliceEndpoint.CmdFreq(V: Double; P: TThreadMethod);
+begin
+ FCmdLock.Acquire;
+ try FSyncFreq := V; FController.Invoke(P); finally FCmdLock.Release; end;
+end;
+
+procedure TCATSliceEndpoint.CmdInt(V: Integer; P: TThreadMethod);
+begin
+ FCmdLock.Acquire;
+ try FSyncInt := V; FController.Invoke(P); finally FCmdLock.Release; end;
+end;
+
+procedure TCATSliceEndpoint.CmdBool(V: Boolean; P: TThreadMethod);
+begin
+ FCmdLock.Acquire;
+ try FSyncBool := V; FController.Invoke(P); finally FCmdLock.Release; end;
+end;
+
+procedure TCATSliceEndpoint.SetVfoA(V: Double);
+begin CmdFreq(V, @SyncVfoA); end;
+
+procedure TCATSliceEndpoint.SyncVfoA;
+// Свобода в пределах ВКЛЮЧЁННОГО диапазона: за захваченную полосу окно DDC
+// переедет само (TuneSliceInBand), за границы диапазона — команда отбрасывается.
+var Id: Integer;
+begin
+ Id := SliceId;
+ if Id > 0 then FController.TuneSliceInBand(Id, FSyncFreq);
+end;
+
+procedure TCATSliceEndpoint.SetVfoB(V: Double);
+begin FShadowVfoB := V; end; // теневой VFO: эфир не трогаем
+
+procedure TCATSliceEndpoint.SetMode(V: Integer);
+begin CmdInt(V, @SyncMode); end;
+
+procedure TCATSliceEndpoint.SyncMode;
+var Id: Integer;
+begin
+ Id := SliceId;
+ if (Id > 0) and (FSyncInt >= MODE_MIN) and (FSyncInt <= MODE_MAX) then
+ FController.SetSliceMode(Id, FSyncInt);
+end;
+
+procedure TCATSliceEndpoint.SetFilterIdx(V: Integer);
+begin CmdInt(V, @SyncFilterIdx); end;
+
+procedure TCATSliceEndpoint.SyncFilterIdx;
+var Id: Integer; S: TCtrlSlice;
+begin
+ Id := SliceId;
+ if (Id > 0) and FController.GetSlice(Id, S) then
+ FController.SetSliceModeBW(Id, S.Mode, FController.FilterBWFor(S.Mode, FSyncInt));
+end;
+
+procedure TCATSliceEndpoint.SetFilterLow(V: Integer);
+begin CmdInt(V, @SyncFilterLow); end;
+
+procedure TCATSliceEndpoint.SyncFilterLow;
+var Id: Integer; S: TCtrlSlice;
+begin
+ Id := SliceId;
+ if (Id > 0) and FController.GetSlice(Id, S) and (FSyncInt < S.FilterHi) then
+ FController.SetSliceFilter(Id, FSyncInt, S.FilterHi);
+end;
+
+procedure TCATSliceEndpoint.SetFilterHigh(V: Integer);
+begin CmdInt(V, @SyncFilterHigh); end;
+
+procedure TCATSliceEndpoint.SyncFilterHigh;
+var Id: Integer; S: TCtrlSlice;
+begin
+ Id := SliceId;
+ if (Id > 0) and FController.GetSlice(Id, S) and (FSyncInt > S.FilterLo) then
+ FController.SetSliceFilter(Id, S.FilterLo, FSyncInt);
+end;
+
+procedure TCATSliceEndpoint.SetAGCMode(V: Integer);
+begin CmdInt(V, @SyncAGC); end;
+
+procedure TCATSliceEndpoint.SyncAGC;
+var Id: Integer;
+begin
+ Id := SliceId;
+ if Id > 0 then
+ FController.SetSliceAGCMode(Id, TRadioController.AGCModeFromUI(FSyncInt));
+end;
+
+procedure TCATSliceEndpoint.SetVolume(V: Integer);
+begin CmdInt(V, @SyncVolume); end;
+
+procedure TCATSliceEndpoint.SyncVolume;
+var Id: Integer;
+begin
+ Id := SliceId;
+ if Id > 0 then
+ FController.SetSliceVolume(Id, EnsureRange(FSyncInt, 0, 100) / 100.0);
+end;
+
+procedure TCATSliceEndpoint.SetDriveLevel(V: Integer);
+begin CmdInt(V, @SyncDrive); end;
+
+procedure TCATSliceEndpoint.SyncDrive;
+begin FController.SetDrive(FSyncInt); end;
+
+procedure TCATSliceEndpoint.ApplyDSP(NR, NB: Integer; SNBOn, ANFOn: Boolean);
+// SetSliceDSP принимает весь набор сразу — недостающее берём из состояния слайса.
+var Id: Integer;
+begin
+ Id := SliceId;
+ if Id > 0 then FController.SetSliceDSP(Id, NR, NB, SNBOn, ANFOn);
+end;
+
+procedure TCATSliceEndpoint.SetNRMode(V: Integer);
+begin CmdInt(V, @SyncNR); end;
+
+procedure TCATSliceEndpoint.SyncNR;
+begin ApplyDSP(FSyncInt, GetNBMode, GetSNB, GetANF); end;
+
+procedure TCATSliceEndpoint.SetNBMode(V: Integer);
+begin CmdInt(V, @SyncNB); end;
+
+procedure TCATSliceEndpoint.SyncNB;
+begin ApplyDSP(GetNRMode, FSyncInt, GetSNB, GetANF); end;
+
+procedure TCATSliceEndpoint.SetSNB(V: Boolean);
+begin CmdBool(V, @SyncSNB); end;
+
+procedure TCATSliceEndpoint.SyncSNB;
+begin ApplyDSP(GetNRMode, GetNBMode, FSyncBool, GetANF); end;
+
+procedure TCATSliceEndpoint.SetANF(V: Boolean);
+begin CmdBool(V, @SyncANF); end;
+
+procedure TCATSliceEndpoint.SyncANF;
+begin ApplyDSP(GetNRMode, GetNBMode, GetSNB, FSyncBool); end;
+
+procedure TCATSliceEndpoint.SetTransmitting(V: Boolean);
+begin CmdBool(V, @SyncTx); end;
+
+procedure TCATSliceEndpoint.SyncTx;
+// Вся логика Auto TX и защита «передаёт только один» — в контроллере.
+var Id: Integer;
+begin
+ Id := SliceId;
+ if Id > 0 then FController.RequestSliceTx(Id, FSyncBool);
+end;
+
+procedure TCATSliceEndpoint.SetSquelchOn(V: Boolean);
+begin CmdBool(V, @SyncSquelchOn); end;
+
+procedure TCATSliceEndpoint.SyncSquelchOn;
+var Id: Integer; S: TCtrlSlice;
+begin
+ Id := SliceId;
+ if (Id > 0) and FController.GetSlice(Id, S) then
+ FController.SetSliceFMSquelch(Id, FSyncBool, S.FMSQLevel);
+end;
+
+procedure TCATSliceEndpoint.SetSquelchLevel(V: Integer);
+begin CmdInt(V, @SyncSquelchLevel); end;
+
+procedure TCATSliceEndpoint.SyncSquelchLevel;
+var Id: Integer; S: TCtrlSlice; Lvl: Integer;
+begin
+ Id := SliceId;
+ if (Id > 0) and FController.GetSlice(Id, S) then
+ begin
+ Lvl := EnsureRange(FSyncInt, 0, 100);
+ // Как у главного тракта: ненулевой порог включает шумоподавитель.
+ FController.SetSliceFMSquelch(Id, S.FMSQOn or (Lvl > 0), Lvl);
+ end;
+end;
+
+procedure TCATSliceEndpoint.DoTuneUp;
+begin CmdInt( 10, @SyncTuneBy); end;
+
+procedure TCATSliceEndpoint.DoTuneDown;
+begin CmdInt(-10, @SyncTuneBy); end;
+
+procedure TCATSliceEndpoint.SyncTuneBy;
+var Id: Integer; S: TCtrlSlice;
+begin
+ Id := SliceId;
+ if (Id > 0) and FController.GetSlice(Id, S) then
+ FController.TuneSliceInBand(Id, S.TargetHz + FSyncInt);
+end;
+
+end.
diff --git a/MainForm.pas b/MainForm.pas
index 7f8e4ca..bf16323 100644
--- a/MainForm.pas
+++ b/MainForm.pas
@@ -235,6 +235,10 @@ type
// --- CAT ---
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
+ // Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
+ // адаптеры (CAT/web), создаваемые по ходу FormCreate, могут дёрнуть
+ // Changed() раньше, чем появятся кнопки/панадаптер.
+ FUIReady: Boolean;
// Временные поля для передачи параметров в Synchronize-методы
FWebSyncFreq: Double;
FWebSyncInt: Integer;
@@ -574,6 +578,10 @@ type
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer);
+ procedure OnSliceCATSettingsChange(
+ const Enabled: array of Boolean;
+ const Port: array of Integer;
+ const AutoTX: array of Boolean);
procedure OnAlexSettingsChange(const A: TAlexSettings);
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
procedure PushXvtrToWeb;
@@ -1102,6 +1110,8 @@ begin
PushBandsToWeb;
FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0);
FCATLastGlobal.CATTcpPort := 19090;
+ for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
+ FCATLastGlobal.CATSlicePort[i] := CFG_CAT_SLICE_PORT0 + i; // 19091, 19092, …
for i := 0 to 3 do
begin
FCATLastGlobal.CATSerialBaud[i] := 9600;
@@ -1266,6 +1276,7 @@ begin
{$IFDEF WINDOWS}
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
{$ENDIF}
+ FUIReady := True; // с этого момента состояние контроллера можно рендерить
end;
procedure TMainForm.FormDestroy(Sender: TObject);
@@ -1329,6 +1340,7 @@ var
i: Integer;
SavedFilter, SavedFilterBW: Integer;
begin
+ if not FUIReady then Exit; // виджетов ещё нет — рисовать нечего
FSyncingFromController := True;
try
case Field of
@@ -1703,6 +1715,20 @@ begin
end;
end;
+ rfPanFreq:
+ begin
+ // Центр доп. пана переехал не из UI (CAT слайса перестроил его за
+ // пределы захваченной полосы) — переложить флаги/шапку/зум-бар.
+ for i := 0 to MAX_PANS - 1 do
+ if FPans[i] <> nil then
+ begin
+ SyncPanZoomUI(FPans[i]);
+ FPans[i].LayoutFlags;
+ FPans[i].PushAllSliceFlagStates;
+ MarkPanDirty(FPans[i]);
+ end;
+ UpdatePanHeaders;
+ end;
rfDeviceList:
begin
// Discovery обновил список устройств (или завершился без них).
@@ -8449,6 +8475,7 @@ begin
SF.OnAnt936xChange := OnAnt936xChange;
SF.OnVHFCalChange := OnVHFCalSettingsChange;
SF.OnCATChange := OnCATSettingsChange;
+ SF.OnSliceCATChange := OnSliceCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange;
@@ -8486,6 +8513,10 @@ begin
FCATLastGlobal.CATSerialAndromeda,
FCATLastGlobal.CATTcpEnabled,
FCATLastGlobal.CATTcpPort);
+ SF.LoadSliceCATSettings(
+ FCATLastGlobal.CATSliceEnabled,
+ FCATLastGlobal.CATSlicePort,
+ FCATLastGlobal.CATSliceAutoTX);
// Перечисляем PA устройства
SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn);
SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill,
@@ -8588,4 +8619,29 @@ begin
FController.FSettings.Save;
end;
+procedure TMainForm.OnSliceCATSettingsChange(
+ const Enabled: array of Boolean;
+ const Port: array of Integer;
+ const AutoTX: array of Boolean);
+// CAT дополнительных слайсов. Хранится там же, где остальные CAT-настройки
+// (корневая секция + device-blob), применяется тем же ApplySettings: адаптер
+// поднимет/опустит порты эндпоинтов и положит Auto TX в контроллер.
+var i: Integer;
+begin
+ for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
+ begin
+ if i <= High(Enabled) then FCATLastGlobal.CATSliceEnabled[i] := Enabled[i];
+ if i <= High(Port) then FCATLastGlobal.CATSlicePort[i] := Port[i];
+ if i <= High(AutoTX) then FCATLastGlobal.CATSliceAutoTX[i] := AutoTX[i];
+ end;
+ FController.FLoadedGlobal.CATSliceEnabled := FCATLastGlobal.CATSliceEnabled;
+ FController.FLoadedGlobal.CATSlicePort := FCATLastGlobal.CATSlicePort;
+ FController.FLoadedGlobal.CATSliceAutoTX := FCATLastGlobal.CATSliceAutoTX;
+ FCATAdapter.ApplySettings(FCATLastGlobal);
+ FController.FSettings.SaveCATSettings(FCATLastGlobal);
+ FController.SaveGlobalSettings;
+ FController.FSettings.Save;
+ RefreshTxIndicators; // бейджи AutoTX на флагах слайсов — сразу
+end;
+
end.
diff --git a/PanafallPanel.pas b/PanafallPanel.pas
index adaebc4..ee21ac2 100644
--- a/PanafallPanel.pas
+++ b/PanafallPanel.pas
@@ -1339,12 +1339,15 @@ begin
else BW := S.FilterHi - S.FilterLo;
end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
- O.SetDSPState(0, 0, False, False, FController.FAGCMode);
+ O.SetDSPState(S.NRMode, S.NBMode, S.SNB, S.ANF,
+ TRadioController.AGCModeToUI(S.AGC));
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
// TxSel: слайс — источник передачи; Tx: передача идёт именно с него.
O.SetExtState(Round(S.Volume * 100), S.Muted, False,
FController.FTransmitting and (FController.FTxSliceId = SliceId),
FController.FTxSliceId = SliceId);
+ // Auto TX (настройка CAT-порта слайса) — бейдж TX подписан AutoTX.
+ O.SetAutoTxState(FController.SliceAutoTx(SliceId));
O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM
// Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then
diff --git a/RadioController.pas b/RadioController.pas
index 62a428d..3771a25 100644
--- a/RadioController.pas
+++ b/RadioController.pas
@@ -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
diff --git a/Settings.pas b/Settings.pas
index 33108f2..dee4e04 100644
--- a/Settings.pas
+++ b/Settings.pas
@@ -16,6 +16,8 @@ const
CFG_MODE_MIN = RadioModes.MODE_MIN;
CFG_MODE_MAX = RadioModes.MODE_MAX;
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
+ CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES
+ CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090)
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на
// отдельной странице, не в общей XVTR-таблице)
@@ -448,9 +450,16 @@ type
CATSerialStopBits: array[0..3] of Integer; // 1 или 2
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2
- // TCP CAT сервер (один, многоклиентский)
+ // TCP CAT сервер (один, многоклиентский) — главный приёмник (слайс A)
CATTcpEnabled: Boolean;
CATTcpPort: Integer; // порт, по умолчанию 19090
+ // Слайс-CAT: свой TCP-порт на каждый ДОПОЛНИТЕЛЬНЫЙ слайс (B, C, …).
+ // Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT.
+ CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
+ CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, …
+ // Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по
+ // CAT-порту слайса сама назначает этот слайс источником передачи.
+ CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
end;
TSettingsManager = class
@@ -652,6 +661,11 @@ begin
end;
G.CATTcpEnabled := False;
G.CATTcpPort := 19090;
+ for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin
+ G.CATSliceEnabled[i] := False;
+ G.CATSlicePort[i] := CFG_CAT_SLICE_PORT0 + i;
+ G.CATSliceAutoTX[i] := False;
+ end;
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
G.DitherEnabled := True;
G.RandomEnabled := True;
@@ -905,6 +919,11 @@ begin
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False);
end;
+ for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin
+ G.CATSliceEnabled[i] := JB(GObj,'cat_slice_en_' +IntToStr(i), False);
+ G.CATSlicePort[i] := JI(GObj,'cat_slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i);
+ G.CATSliceAutoTX[i] := JB(GObj,'cat_slice_autotx_'+IntToStr(i), False);
+ end;
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
// ADC settings
@@ -1039,6 +1058,11 @@ begin
JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]);
JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]);
end;
+ for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin
+ JW(O,'cat_slice_en_' +IntToStr(i), G.CATSliceEnabled[i]);
+ JW(O,'cat_slice_port_' +IntToStr(i), G.CATSlicePort[i]);
+ JW(O,'cat_slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]);
+ end;
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
JW(O,'cat_tcp_port',G.CATTcpPort);
// ADC settings
@@ -1200,6 +1224,12 @@ begin
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]);
end;
+ for i := 0 to CFG_CAT_SLICE_COUNT-1 do
+ begin
+ JW(O, 'slice_en_' +IntToStr(i), G.CATSliceEnabled[i]);
+ JW(O, 'slice_port_' +IntToStr(i), G.CATSlicePort[i]);
+ JW(O, 'slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]);
+ end;
end;
procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings);
@@ -1223,6 +1253,12 @@ begin
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False);
end;
+ for i := 0 to CFG_CAT_SLICE_COUNT-1 do
+ begin
+ G.CATSliceEnabled[i] := JB(O, 'slice_en_' +IntToStr(i), False);
+ G.CATSlicePort[i] := JI(O, 'slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i);
+ G.CATSliceAutoTX[i] := JB(O, 'slice_autotx_'+IntToStr(i), False);
+ end;
end;
// ---------------------------------------------------------------------------
diff --git a/SettingsForm.pas b/SettingsForm.pas
index 452c33b..3dab9b2 100644
--- a/SettingsForm.pas
+++ b/SettingsForm.pas
@@ -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
// ---------------------------------------------------------------------------
diff --git a/VfoOverlay.pas b/VfoOverlay.pas
index 3bc84fc..07ec639 100644
--- a/VfoOverlay.pas
+++ b/VfoOverlay.pas
@@ -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;
diff --git a/doc/CAT_SLICES.md b/doc/CAT_SLICES.md
new file mode 100644
index 0000000..316471f
--- /dev/null
+++ b/doc/CAT_SLICES.md
@@ -0,0 +1,160 @@
+# CAT дополнительных слайсов (Slice CAT)
+
+Ветка: `feature/slice-cat`. Дата: 2026-07-25.
+
+Внешняя программа (WSJT-X, FLDIGI, лог) управляет **отдельным слайсом** EWSDR
+как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
+(19090) остаются за главным приёмником (слайс A) и не тронуты.
+
+Эталон поведения — **SmartSDR CAT** (FlexRadio): порт привязан к слайсу
+(«VFO A Slice»), а флаг **Auto TX** — это их «Auto Switch TX Slice»:
+
+> When enabled Auto Switch TX Slice automatically switches the Transmit Slice to
+> the VFO A Slice selected above when a CAT TX command is sent on the com port…
+> If this option is not enabled, the operator will have to manually designate the
+> Transmit Slice.
+
+---
+
+## 1. Архитектура
+
+Протокол **не дублируется**. `TCATEngine` — чистый парсер/формировщик поверх
+плоской записи callback'ов `TCATContext`, `TCATTcpServer` — транспорт поверх
+движка. Значит «CAT слайса» = **тот же движок с другим контекстом**:
+
+```
+TCATAdapter ──┬── главный контекст ──── TCATEngine ── serial ×4 + TCP 19090 (слайс A)
+ ├── TCATSliceEndpoint ─── TCATEngine ── TCP 19091 (слайс B)
+ ├── TCATSliceEndpoint ─── TCATEngine ── TCP 19092 (слайс C)
+ └── … по слоту на букву (B..G, потолок = WDSPEngine.MAX_SLICES)
+```
+
+| Файл | Что добавлено |
+|---|---|
+| `CATSliceEndpoint.pas` (новый) | `TCATSliceEndpoint` — контекст поверх `FSlices[слот]`, свой движок + свой TCP-сервер. Константа `CAT_SLICE_SLOTS`. |
+| `CATAdapter.pas` | Владеет массивом эндпоинтов; `ApplySliceSettings` поднимает/опускает порты и кладёт Auto TX в контроллер. |
+| `CATEngine.pas` | `ZZFL`/`ZZFH` перестали быть заглушками: новые callback'и `Get/SetFilterLow/High` (у главного nil → прежний дефолт режима). Хелпер `SignedPad`. |
+| `RadioController.pas` | `RequestSliceTx`, `FSliceAutoTx[слот]`, слот-хелперы, `FilterIdxFor`, `AGCModeToUI`, зеркало DSP-состояния слайса в `TCtrlSlice`. |
+| `Settings.pas` | `CATSliceEnabled/CATSlicePort/CATSliceAutoTX[0..5]`, персист в корневой секции `cat` и в device-blob. |
+| `SettingsForm.pas` | Вкладка **Slices**. |
+| `VfoOverlay.pas`, `PanafallPanel.pas` | Бейдж `AutoTX` вместо `TX` на флаге слайса. |
+
+**Привязка к слоту, а не к Id слайса.** Слот 0..5 = буква B..G. Слайс на слоте
+создаётся/удаляется в любой момент, порт при этом остаётся поднятым: пока слайса
+нет, эндпоинт отвечает «радио выключено» (`PS0;`) и игнорирует команды. Настройка
+(порт, Auto TX) переживает удаление слайса.
+
+**Потокобезопасность** — как у главного адаптера: геттеры читают состояние в
+CAT-потоке, сеттеры кладут параметр под локом эндпоинта и маршалятся в поток
+контроллера через `Invoke` (GUI = `TThread.Synchronize`).
+
+---
+
+## 2. Auto TX и защита передачи
+
+Вся логика — в **одной** точке: `TRadioController.RequestSliceTx(SliceId, On_)`,
+исполняется в потоке контроллера, поэтому проверка «уже в эфире» и смена
+TX-источника атомарны относительно GUI.
+
+`TX;` / `ZZTX1;` на порту слайса:
+
+1. Слайса на слоте нет → игнор.
+2. **Кто-то уже передаёт** (другой слайс, главный VFO, кнопка MOX/TUNE,
+ аппаратная PTT) → игнор. Перехвата эфира нет никогда — передаёт всегда
+ только один слайс.
+3. Режим слайса `DMR` → игнор (AMBE-передатчика нет).
+4. Слайс не выбран TX-источником:
+ * **Auto TX включён** → `SetTxSlice(слайс)`, затем `SetMOX(True)`;
+ * **Auto TX выключен** → игнор (оператор назначает источник руками —
+ бейдж `TX` на флаге слайса).
+5. Слайс уже TX-источник → просто `SetMOX(True)`.
+
+`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
+слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
+
+Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе (как в
+SmartSDR: Transmit Slice переключён, а не одолжен). Бейдж на флаге это показывает.
+
+`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
+слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
+прошла.
+
+---
+
+## 3. Что поддерживает CAT-порт слайса
+
+| Команды | Маппинг на слайс |
+|---|---|
+| `FA` `ZZFA` (VFO A) | `TuneSliceInBand` — свободно в пределах включённого диапазона, см. §3.1 |
+| `FB` `ZZFB` (VFO B) | теневое значение: читается/пишется, эфир не двигает (второго VFO у слайса нет) |
+| `MD` `ZZMD` `ZZMA` | `SetSliceMode` |
+| `FW` `SH` `SL` `ZZFI` (индекс фильтра) | `FilterIdxFor`/`FilterBWFor` ↔ `SetSliceModeBW` |
+| `ZZFL` `ZZFH` (кромки, Гц) | `FilterLo/FilterHi` ↔ `SetSliceFilter` |
+| `GT` `ZZGT` (AGC) | `AGC` ↔ `SetSliceAGCMode` (`AGCModeToUI`/`FromUI`) |
+| `AG` `ZZAG` (громкость) | `Volume` ↔ `SetSliceVolume` |
+| `ZZNR` `ZZNB` `ZZNS` `ZZNT` (DSP) | `SetSliceDSP` (состояние зеркалится в `TCtrlSlice`) |
+| `SQ` `ZZSQ` `ZZSO` (squelch) | `FMSQOn/FMSQLevel` ↔ `SetSliceFMSquelch` |
+| `SM` `ZZSM` | `SliceSMeter` |
+| `TX` `RX` `ZZTX` | `RequestSliceTx` (см. §2) |
+| `PS` | слайс существует **и** радио запущено |
+| `IF` `ID` | составной статус/идентификатор по данным слайса |
+| `UP` `DN` `RU` `RD` | шаг ±10 Гц по частоте слайса |
+| `PC` `ZZPC` (мощность), `ZZBS` (диапазон, read) | глобальные — передатчик один на всё радио |
+
+### 3.1 Частота слайса: свобода в пределах диапазона
+
+`TRadioController.TuneSliceInBand(Id, TargetHz)` — единственная точка перестройки
+слайса из CAT:
+
+1. **Другой диапазон → отказ.** Границы берутся у включённого сейчас диапазона:
+ активный трансвертер → его `FreqBegin/FreqEnd` (2 м = 144…146 МГц), иначе
+ band-план бэкенда (`BoardUtils.BandEdges`). Стоим на 2 м, программа просит
+ 145.500 → 144.174 — можно; 7.074 МГц — команда отбрасывается. Смена диапазона
+ остаётся решением оператора.
+2. Цель внутри захваченной полосы (`SliceFitsCapture`) → просто `SetSliceTarget`.
+3. Цель внутри диапазона, но вне окна DDC → **окно переезжает центром на цель**:
+ * слайс на дополнительном пане (свой DDC) — свободно (`SetPanDDCFreq` +
+ `Changed(rfPanFreq)`, UI перекладывает флаги/шапку/зум-бар);
+ * слайс на пане 0 — окно общее с главным приёмником, поэтому центр ставится
+ ПОСЕРЕДИНЕ между целью и активным VFO: так в окно попадают оба. Если они
+ дальше, чем окно (`|цель − VFO| > 0.95·полоса`), команда отбрасывается —
+ перестраивать главный приёмник ради слайса нельзя.
+
+Практический вывод: чтобы внешняя программа гуляла по всему диапазону, слайс
+лучше держать на своём пане (дополнительный DDC). На пане 0 её свобода —
+в пределах общего окна вокруг главного VFO.
+
+**Осознанно не проброшено** (движок ответит валидной заглушкой): split/`FR`/`FT`
+(второго VFO нет), смена диапазона `BU`/`BD`/`ZZBS`-set (диапазон — свойство пана,
+на котором живёт слайс), CTUN, AGC-T, CTCSS/репитер/FM-шаг (настройки главного
+тракта), Andromeda (панель одна и висит на главном).
+
+---
+
+## 4. Настройки
+
+Вкладка **Slices**: на каждый слайс B..G — `Enable CAT TCP server`, `Port`
+(по умолчанию 19091, 19092, …) и `Auto TX`.
+
+* Хранятся вместе с остальным CAT: корневая секция `cat` (`slice_en_N`,
+ `slice_port_N`, `slice_autotx_N`) + device-blob (`cat_slice_*`), не привязаны
+ к MAC — доступны до подключения железа.
+* Порт, совпадающий с основным TCP-портом или с портом слайса меньшей буквы,
+ **не поднимается** (иначе второй `bind` просто упал бы молча).
+* Правка одной строки не рвёт живые сессии остальных: эндпоинт перезапускается
+ только при реальном изменении своего enable/порта.
+
+---
+
+## 5. Проверка
+
+```
+# слайс B на 19091
+printf 'ZZFA;' | nc -q1 127.0.0.1 19091 # частота слайса
+printf 'ZZMD;' | nc -q1 127.0.0.1 19091 # режим
+printf 'TX;' | nc -q1 127.0.0.1 19091 # PTT (нужен Auto TX либо выбранный TX-слайс)
+printf 'RX;' | nc -q1 127.0.0.1 19091
+```
+
+Статус: собрано (`lazbuild --ws=qt6` + `build-ewsdrd.sh`), **на эфире и на экране
+не проверено**.
diff --git a/doc/CAT_STATUS.md b/doc/CAT_STATUS.md
index c243a72..ec2c214 100644
--- a/doc/CAT_STATUS.md
+++ b/doc/CAT_STATUS.md
@@ -188,6 +188,10 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
## 5. Транспорты
+> CAT **дополнительных слайсов** (свой TCP-порт на слайс B..G + Auto TX) вынесен
+> в отдельный документ: [`CAT_SLICES.md`](CAT_SLICES.md). Тот же движок, другой
+> контекст; транспорты ниже принадлежат главному приёмнику (слайс A).
+
- **Serial** (`CATSerial.pas`): до 4 портов, параметры (порт/скорость/биты/
чётность/стоп) из `TGlobalSettings.CATSerial*`, применяются
`TCATAdapter.ApplySettings`. На текущей платформе сборки может быть
diff --git a/ewsdr.lpi b/ewsdr.lpi
index 4792265..20314b0 100644
--- a/ewsdr.lpi
+++ b/ewsdr.lpi
@@ -17,9 +17,9 @@
-
+
-
+
@@ -137,6 +137,8 @@
+
+
@@ -358,6 +360,10 @@
+
+
+
+