Merge feature/slice-cat: CAT-порт на слайс + Auto TX, аудио in/out на слот слайса

This commit is contained in:
2026-07-29 12:31:02 +03:00
13 changed files with 1631 additions and 37 deletions
+12
View File
@@ -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;
+34 -2
View File
@@ -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;
// ── Геттеры ──────────────────────────────────────────────────────────────────
+30 -15
View File
@@ -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;
+571
View File
@@ -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.
+105
View File
@@ -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,18 @@ 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;
const OutDev: array of string;
const InDev: array of string);
// Выбор устройства во флаге слайса = устройство его слота: зеркалим в
// конфиг, чтобы вкладка Slices показывала то же самое.
procedure StoreSliceSlotAudio(SliceId: Integer);
// Аудио слотов из конфига → контроллер (старт и смена устройства): слайсы
// восстанавливаются позже и должны встать на свои звуковухи.
procedure ApplySliceSlotAudio;
procedure OnAlexSettingsChange(const A: TAlexSettings);
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
procedure PushXvtrToWeb;
@@ -1102,6 +1118,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;
@@ -1117,6 +1135,7 @@ begin
FLightTheme := FController.FSettings.LoadTheme;
FCATAdapter := TCATAdapter.Create(FController);
FCATAdapter.ApplySettings(FCATLastGlobal);
ApplySliceSlotAudio;
FSMeterPeak := -130;
FSMeterMin := -130;
FSMeterAvg := -130;
@@ -1266,6 +1285,7 @@ begin
{$IFDEF WINDOWS}
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
{$ENDIF}
FUIReady := True; // с этого момента состояние контроллера можно рендерить
end;
procedure TMainForm.FormDestroy(Sender: TObject);
@@ -1329,6 +1349,7 @@ var
i: Integer;
SavedFilter, SavedFilterBW: Integer;
begin
if not FUIReady then Exit; // виджетов ещё нет — рисовать нечего
FSyncingFromController := True;
try
case Field of
@@ -1667,6 +1688,7 @@ begin
// возвращаем живое значение в persist-буфер, чтобы blob нёс актуальную тему.
FController.FLoadedGlobal.LightTheme := FLightTheme;
FCATAdapter.ApplySettings(FController.FLoadedGlobal);
ApplySliceSlotAudio;
RelabelBands; // HF ↔ VHF/UHF подписи под тип бэкенда
ApplyBackendCapsToUI; // пресеты sample rate + гейтинг полей
RebuildXvtrButtons;
@@ -1703,6 +1725,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 обновил список устройств (или завершился без них).
@@ -5808,6 +5844,7 @@ begin
if CurSliceId <> 0 then
begin
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
StoreSliceSlotAudio(CurSliceId); // вкладка Slices показывает то же устройство
PushFlagStateAllPans(CurSliceId); // обновить подсветку текущего устройства слайса
Exit;
end;
@@ -5822,6 +5859,7 @@ begin
if CurSliceId <> 0 then
begin
FController.SetSliceInDevice(CurSliceId, DevIndex, DevName);
StoreSliceSlotAudio(CurSliceId);
PushFlagStateAllPans(CurSliceId);
Exit;
end;
@@ -8449,6 +8487,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 +8525,12 @@ begin
FCATLastGlobal.CATSerialAndromeda,
FCATLastGlobal.CATTcpEnabled,
FCATLastGlobal.CATTcpPort);
SF.LoadSliceCATSettings(
FCATLastGlobal.CATSliceEnabled,
FCATLastGlobal.CATSlicePort,
FCATLastGlobal.CATSliceAutoTX,
FCATLastGlobal.CATSliceOutDev,
FCATLastGlobal.CATSliceInDev);
// Перечисляем PA устройства
SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn);
SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill,
@@ -8588,4 +8633,64 @@ begin
FController.FSettings.Save;
end;
procedure TMainForm.OnSliceCATSettingsChange(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean;
const OutDev: array of string;
const InDev: array of string);
// 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];
if i <= High(OutDev) then FCATLastGlobal.CATSliceOutDev[i] := OutDev[i];
if i <= High(InDev) then FCATLastGlobal.CATSliceInDev[i] := InDev[i];
// Аудио слота — сразу в контроллер: слайс, стоящий на слоте, переезжает
// на новое устройство, следующий получит его при создании.
FController.SetSliceSlotAudio(i, FCATLastGlobal.CATSliceOutDev[i],
FCATLastGlobal.CATSliceInDev[i]);
end;
FController.FLoadedGlobal.CATSliceEnabled := FCATLastGlobal.CATSliceEnabled;
FController.FLoadedGlobal.CATSlicePort := FCATLastGlobal.CATSlicePort;
FController.FLoadedGlobal.CATSliceAutoTX := FCATLastGlobal.CATSliceAutoTX;
FController.FLoadedGlobal.CATSliceOutDev := FCATLastGlobal.CATSliceOutDev;
FController.FLoadedGlobal.CATSliceInDev := FCATLastGlobal.CATSliceInDev;
FCATAdapter.ApplySettings(FCATLastGlobal);
FController.FSettings.SaveCATSettings(FCATLastGlobal);
FController.SaveGlobalSettings;
FController.FSettings.Save;
RefreshTxIndicators; // бейджи AutoTX на флагах слайсов — сразу
for i := 0 to MAX_PANS - 1 do // подсветка текущего устройства во флагах
if FPans[i] <> nil then FPans[i].PushAllSliceFlagStates;
end;
procedure TMainForm.ApplySliceSlotAudio;
var i: Integer;
begin
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
FController.SetSliceSlotAudio(i, FCATLastGlobal.CATSliceOutDev[i],
FCATLastGlobal.CATSliceInDev[i]);
end;
procedure TMainForm.StoreSliceSlotAudio(SliceId: Integer);
// Слот слайса и его устройства уже обновлены контроллером (SetSliceDevice/
// SetSliceInDevice) — здесь только зеркалим их в конфиг и сохраняем.
var Slot: Integer;
begin
Slot := FController.SliceSlotOf(SliceId);
if (Slot < 0) or (Slot >= CFG_CAT_SLICE_COUNT) then Exit;
FCATLastGlobal.CATSliceOutDev[Slot] := FController.SliceSlotOutDev(Slot);
FCATLastGlobal.CATSliceInDev[Slot] := FController.SliceSlotInDev(Slot);
FController.FLoadedGlobal.CATSliceOutDev := FCATLastGlobal.CATSliceOutDev;
FController.FLoadedGlobal.CATSliceInDev := FCATLastGlobal.CATSliceInDev;
FController.FSettings.SaveCATSettings(FCATLastGlobal);
FController.FSettings.Save;
end;
end.
+4 -1
View File
@@ -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
+303 -4
View File
@@ -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,15 @@ 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;
// Аудио-устройства слота (буквы B..G): '' = общий выход приложения / общий
// вход. Это НЕ шаблон, а привязка слота: применяется к живому слайсу сразу
// и достаётся новому слайсу, который встанет на этот слот.
FSliceOutDev: array[0..MAX_SLICES-1] of string;
FSliceInDev: array[0..MAX_SLICES-1] of string;
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
FDisplayDuplex: Boolean;
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
@@ -563,6 +579,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 +663,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 +695,35 @@ 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;
// Аудио-устройства слота. Сеттер применяет их к слайсу, который стоит на
// слоте прямо сейчас (имя → PA-индекс текущей сессии); '' = общий.
function SliceSlotOutDev(Slot: Integer): string;
function SliceSlotInDev(Slot: Integer): string;
procedure SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
// Заявка на передачу ОТ ИМЕНИ слайса (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 +1638,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;
@@ -1916,6 +2028,8 @@ function TRadioController.AddSlice(TargetHz: Double;
DevIndex: Integer; const DevName: string; APanId: Integer): Integer;
var
i, slot, Id: Integer;
DevIndex_: Integer;
DevName_: string;
AO: TAudioOutput;
begin
Result := -1;
@@ -1930,6 +2044,20 @@ begin
if not FSlices[i].Active then begin slot := i; Break; end;
if slot < 0 then Exit; // нет свободных слотов
// Устройство слота (настройка вкладки Slices) сильнее того, что попросил
// вызывающий: слот — это буква флага, и звук у неё всегда один и тот же.
if FSliceOutDev[slot] <> '' then
begin
DevName_ := FSliceOutDev[slot];
DevIndex_ := -1;
if Assigned(FAudioOut) then DevIndex_ := FAudioOut.FindDeviceByName(DevName_);
end
else
begin
DevName_ := DevName;
DevIndex_ := DevIndex;
end;
Id := FNextSliceId;
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
TargetHz - PanCenterHz(APanId), Vol, AGC,
@@ -1939,7 +2067,7 @@ begin
// Свой аудио-выход слайса на выбранной звуковухе.
AO := TAudioOutput.Create(48000);
AO.OutputBufferSize := FSettings.LoadAudioBufferSize;
AO.DeviceIndex := DevIndex;
AO.DeviceIndex := DevIndex_;
if FLocalAudio then AO.Open;
FSlices[slot].Active := True;
@@ -1958,8 +2086,12 @@ begin
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
FSlices[slot].FMSQLevel := 40;
FSlices[slot].DeviceIndex := DevIndex;
FSlices[slot].DevName := DevName;
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;
// Вход слайса по умолчанию общий (FAudioIn); свой ставится SetSliceInDevice.
FSlices[slot].InDeviceIndex := -1;
@@ -1969,6 +2101,11 @@ begin
if Mode = MODE_DMR then
FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id);
// Вход слота (настройка вкладки Slices) — тем же правилом, что и выход.
if (FSliceInDev[slot] <> '') and Assigned(FAudioIn) then
SetSliceInDeviceIdx(slot, FAudioIn.FindDeviceByName(FSliceInDev[slot]),
FSliceInDev[slot]);
Result := Id;
Changed(rfDevice);
end;
@@ -2115,6 +2252,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);
@@ -2216,6 +2357,7 @@ var idx: Integer;
begin
idx := FindSliceIndex(Id);
if idx < 0 then Exit;
FSliceOutDev[idx] := DevName; // выбор во флаге = устройство слота (одна истина)
FSlices[idx].DeviceIndex := DevIndex;
FSlices[idx].DevName := DevName;
if FSlices[idx].Audio <> nil then
@@ -2309,6 +2451,7 @@ procedure TRadioController.SetSliceInDeviceIdx(Idx, DevIndex: Integer;
begin
if (Idx < 0) or (Idx >= MAX_SLICES) or (not FSlices[Idx].Active) then Exit;
if DevName = '' then DevIndex := -1;
FSliceInDev[Idx] := DevName; // выбор во флаге = вход слота (одна истина)
ReleaseMicInput(FSlices[Idx].AudioIn);
FSlices[Idx].InDeviceIndex := DevIndex;
FSlices[Idx].InDevName := DevName;
@@ -2383,6 +2526,129 @@ 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;
function TRadioController.SliceSlotOutDev(Slot: Integer): string;
begin
if (Slot >= 0) and (Slot < MAX_SLICES) then Result := FSliceOutDev[Slot]
else Result := '';
end;
function TRadioController.SliceSlotInDev(Slot: Integer): string;
begin
if (Slot >= 0) and (Slot < MAX_SLICES) then Result := FSliceInDev[Slot]
else Result := '';
end;
procedure TRadioController.SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
// Настройка = устройство слота, поэтому применяем сразу: слайс, который стоит
// на слоте, переезжает на новое устройство (как выбор во вкладке AUD флага).
var Id: Integer;
begin
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
Id := SliceIdBySlot(Slot);
if FSliceOutDev[Slot] <> OutDev then
begin
FSliceOutDev[Slot] := OutDev;
if Id > 0 then
begin
if (OutDev <> '') and Assigned(FAudioOut) then
SetSliceDevice(Id, FAudioOut.FindDeviceByName(OutDev), OutDev)
else
SetSliceDevice(Id, -1, ''); // общий выход приложения
end;
end;
if FSliceInDev[Slot] <> InDev then
begin
FSliceInDev[Slot] := InDev;
if Id > 0 then
begin
if (InDev <> '') and Assigned(FAudioIn) then
SetSliceInDevice(Id, FAudioIn.FindDeviceByName(InDev), InDev)
else
SetSliceInDevice(Id, -1, ''); // общий вход приложения
end;
end;
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 +3232,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 +4031,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
+52 -1
View File
@@ -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,21 @@ 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;
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
// приложения / общий вход). Имена, а не индексы: порядок перечисления
// устройств меняется от запуска к запуску.
CATSliceOutDev: array[0..CFG_CAT_SLICE_COUNT-1] of string;
CATSliceInDev: array[0..CFG_CAT_SLICE_COUNT-1] of string;
end;
TSettingsManager = class
@@ -652,6 +666,13 @@ 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;
G.CATSliceOutDev[i] := '';
G.CATSliceInDev[i] := '';
end;
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
G.DitherEnabled := True;
G.RandomEnabled := True;
@@ -905,6 +926,13 @@ 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);
G.CATSliceOutDev[i] := JS(GObj,'cat_slice_out_' +IntToStr(i), '');
G.CATSliceInDev[i] := JS(GObj,'cat_slice_in_' +IntToStr(i), '');
end;
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
// ADC settings
@@ -1039,6 +1067,13 @@ 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]);
JWS(O,'cat_slice_out_' +IntToStr(i), G.CATSliceOutDev[i]);
JWS(O,'cat_slice_in_' +IntToStr(i), G.CATSliceInDev[i]);
end;
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
JW(O,'cat_tcp_port',G.CATTcpPort);
// ADC settings
@@ -1200,6 +1235,14 @@ 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]);
JWS(O,'slice_out_' +IntToStr(i), G.CATSliceOutDev[i]);
JWS(O,'slice_in_' +IntToStr(i), G.CATSliceInDev[i]);
end;
end;
procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings);
@@ -1223,6 +1266,14 @@ 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);
G.CATSliceOutDev[i] := JS(O, 'slice_out_' +IntToStr(i), '');
G.CATSliceInDev[i] := JS(O, 'slice_in_' +IntToStr(i), '');
end;
end;
// ---------------------------------------------------------------------------
+304 -3
View File
@@ -76,6 +76,13 @@ 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;
const OutDev: array of string;
const InDev: array of string) of object;
TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
@@ -92,6 +99,7 @@ type
FPagePA: TScrollBox;
FPageAdvanced: TScrollBox;
FPageCAT: TScrollBox;
FPageSlices: TScrollBox;
FPageTransmit: TScrollBox;
FNavAudio: TFlatButton;
FNavDisplay: TFlatButton;
@@ -100,6 +108,7 @@ type
FNavPA: TFlatButton;
FNavAdvanced: TFlatButton;
FNavCAT: TFlatButton;
FNavSlices: TFlatButton;
FNavTransmit: TFlatButton;
// ---- Audio tab controls ----
@@ -335,6 +344,14 @@ 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;
// Аудио слота: списки — общие с главными комбо (RefreshAudioDevices),
// первый пункт «Default»/«None» = общий выход/вход приложения.
FSliceOutDev: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatComboBox;
FSliceInDev: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatComboBox;
// ---- Advanced tab controls ----
FChkDither: TFlatCheckBox;
@@ -350,6 +367,7 @@ type
// ---- Callbacks ----
FOnCATChange: TOnCATChange;
FOnSliceCATChange: TOnSliceCATChange;
FOnPAChange: TOnPASettingsChange;
FOnPlutoTxChange: TOnPlutoTxChange;
FOnAnt936xChange: TOnAnt936xChange;
@@ -393,6 +411,7 @@ type
procedure CollectCalFromUI;
procedure OnCalAnyChange(Sender: TObject);
procedure BuildCATTab;
procedure BuildSlicesTab;
procedure BuildTransmitTab;
procedure BuildAlexTab;
procedure BuildAdvancedTab;
@@ -459,6 +478,10 @@ type
procedure OnCATSerialChange(Sender: TObject);
procedure OnCATTcpChange(Sender: TObject);
procedure FireCATChange;
procedure OnSliceCATCtrlChange(Sender: TObject);
procedure SelectDevInCombo(Cmb: TFlatComboBox; const DevName: string);
procedure FillSliceDevCombos(AudioOut: TAudioOutput; AudioIn: TAudioInput);
procedure FireSliceCATChange;
procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange;
@@ -508,6 +531,13 @@ 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;
const OutDev: array of string;
const InDev: array of string);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
@@ -526,6 +556,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;
@@ -571,6 +602,9 @@ const
BTN_H = 24;
GRP_TITLE_H = 22; // высота заголовка в group-панелях (title label + divider)
// Первый пункт комбо аудио слайса = «как у приложения» (пустое имя в конфиге).
SLICE_DEV_DEFAULT = 'Default (app output)';
SLICE_DEV_NONE = 'None (app input)';
CAT_BAUD_VALS: array[0..7] of Integer = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200);
CAT_BAUD_NAMES: array[0..7] of string = ('1200','2400','4800','9600','19200','38400','57600','115200');
@@ -781,9 +815,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 +835,7 @@ begin
FPageCalib := MakeScrollPage;
FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage;
FPageSlices := MakeScrollPage;
FPageAlex := MakeScrollPage;
FPageXvtr := MakeScrollPage;
FPageQO := MakeScrollPage;
@@ -816,6 +852,7 @@ begin
BuildCalibrationTab;
BuildAdvancedTab;
BuildCATTab;
BuildSlicesTab;
BuildAlexTab;
BuildXvtrTab;
BuildQOTab;
@@ -849,6 +886,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 +903,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 +917,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;
@@ -2817,6 +2857,54 @@ begin
FLoading := False;
Names.Free;
end;
FillSliceDevCombos(AudioOut, AudioIn);
end;
procedure TSettingsForm.FillSliceDevCombos(AudioOut: TAudioOutput; AudioIn: TAudioInput);
// Списки устройств в комбо вкладки Slices — те же, что у главных комбо. Текущий
// выбор держим по ИМЕНИ: порядок перечисления PortAudio между запусками плавает.
var
Names: TStringList;
Idx: array[0..63] of Integer;
Cnt, i, k: Integer;
Cur: string;
begin
if FSliceOutDev[0] = nil then Exit; // вкладка ещё не построена
Names := TStringList.Create;
FLoading := True;
try
if AudioOut <> nil then
begin
Cnt := 0; Names.Clear;
AudioOut.EnumOutputDevices(Names, Idx, Cnt);
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
Cur := FSliceOutDev[i].Text;
if FSliceOutDev[i].ItemIndex = 0 then Cur := '';
FSliceOutDev[i].Items.Clear;
FSliceOutDev[i].Items.Add(SLICE_DEV_DEFAULT);
for k := 0 to Cnt - 1 do FSliceOutDev[i].Items.Add(Names[k]);
SelectDevInCombo(FSliceOutDev[i], Cur);
end;
end;
if AudioIn <> nil then
begin
Cnt := 0; Names.Clear;
AudioIn.EnumInputDevices(Names, Idx, Cnt);
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
Cur := FSliceInDev[i].Text;
if FSliceInDev[i].ItemIndex = 0 then Cur := '';
FSliceInDev[i].Items.Clear;
FSliceInDev[i].Items.Add(SLICE_DEV_NONE);
for k := 0 to Cnt - 1 do FSliceInDev[i].Items.Add(Names[k]);
SelectDevInCombo(FSliceInDev[i], Cur);
end;
end;
finally
FLoading := False;
Names.Free;
end;
end;
// ---------------------------------------------------------------------------
@@ -3522,6 +3610,219 @@ begin
end;
end;
// ---------------------------------------------------------------------------
// BuildSlicesTab — CAT дополнительных слайсов (B..): свой TCP-порт на слайс.
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildSlicesTab;
const
MARGIN = 22;
PAD = 18;
LW = 120;
CX = PAD + LW + 8;
GRP_W = 380;
GRP_H = 240;
GRP_GAP = 16;
CMB_W = 210;
R1 = 42;
STEP = 32;
var
i, col, row, gx, gy: Integer;
Grp: TPanel;
Chk: TFlatCheckBox;
Spin: TFlatSpinEdit;
Cmb: TFlatComboBox;
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);
// Аудио слота: не шаблон, а привязка буквы — применяется к слайсу, который
// стоит на слоте сейчас, и достаётся следующему.
MakeLbl(Grp, 'Audio out:', PAD, R1 + 3 * STEP + 26, LW);
Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP + 20, CMB_W, OnSliceCATCtrlChange);
Cmb.Tag := i;
Cmb.Items.Add(SLICE_DEV_DEFAULT);
Cmb.ItemIndex := 0;
FSliceOutDev[i] := Cmb;
MakeLbl(Grp, 'Audio in:', PAD, R1 + 4 * STEP + 26, LW);
Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP + 20, CMB_W, OnSliceCATCtrlChange);
Cmb.Tag := i;
Cmb.Items.Add(SLICE_DEV_NONE);
Cmb.ItemIndex := 0;
FSliceInDev[i] := Cmb;
end;
gy := 96 + ((CFG_CAT_SLICE_COUNT + 1) div 2) * (GRP_H + GRP_GAP);
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Audio out/in belong to the slice letter: the setting moves the '
+ 'slice sitting on that slot right away and the next one inherits it.';
SetBounds(MARGIN, gy, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
Inc(gy, 18);
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;
OutD, InD: array[0..CFG_CAT_SLICE_COUNT-1] of string;
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;
// Первый пункт списка — «общий выход/вход приложения» (пустое имя).
if FSliceOutDev[i].ItemIndex <= 0 then OutD[i] := ''
else OutD[i] := FSliceOutDev[i].Text;
if FSliceInDev[i].ItemIndex <= 0 then InD[i] := ''
else InD[i] := FSliceInDev[i].Text;
end;
FOnSliceCATChange(En, Prt, Auto, OutD, InD);
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;
const OutDev: array of string;
const InDev: array of string);
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];
if i <= High(OutDev) then SelectDevInCombo(FSliceOutDev[i], OutDev[i]);
if i <= High(InDev) then SelectDevInCombo(FSliceInDev[i], InDev[i]);
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.SelectDevInCombo(Cmb: TFlatComboBox; const DevName: string);
// Выбор устройства по ИМЕНИ. Сохранённого устройства может не быть в системе
// (кабель не поднят, гарнитура не воткнута) — тогда добавляем имя в список,
// чтобы привязка слота не потерялась молча при первом же открытии настроек.
var i: Integer;
begin
if Cmb = nil then Exit;
if DevName = '' then begin Cmb.ItemIndex := 0; Exit; end;
i := Cmb.Items.IndexOf(DevName);
if i < 0 then i := Cmb.Items.Add(DevName);
Cmb.ItemIndex := i;
end;
// ---------------------------------------------------------------------------
// BuildAlexTab
// ---------------------------------------------------------------------------
+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;
+180
View File
@@ -0,0 +1,180 @@
# 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`, `Audio out`, `Audio in`.
### 4.1 Аудио слота
`Audio out`/`Audio in` — это **устройство слота (буквы)**, а не шаблон:
* смена настройки применяется немедленно к слайсу, который стоит на слоте
сейчас (`TRadioController.SetSliceSlotAudio`);
* слайс, который встанет на слот позже (создание/восстановление из персиста),
получает то же устройство — оно сильнее того, что попросил вызывающий
`AddSlice`;
* выбор во вкладке `AUD` флага слайса пишется обратно в настройку слота
(`SetSliceDevice`/`SetSliceInDevice``MainForm.StoreSliceSlotAudio`), так
что вкладка всегда показывает реальное устройство — одна истина, два места
правки;
* пусто (`Default (app output)` / `None (app input)`) — как раньше: общий выход
приложения и общий вход.
Хранятся **имена** PortAudio-устройств, а не индексы: порядок перечисления между
запусками плавает. Имя отсутствующего сейчас устройства (кабель не поднят) не
теряется — комбо добавляет его в список и держит выбранным.
* Хранятся вместе с остальным 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`), **на эфире и на экране
не проверено**.
+4
View File
@@ -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`. На текущей платформе сборки может быть
+8 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/>
<BuildNr Value="285"/>
<BuildNr Value="286"/>
</VersionInfo>
<MacroValues Count="117">
<MacroValues Count="119">
<Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -137,6 +137,8 @@
<Macro115 Name="LCLWidgetType" Value="qt6"/>
<Macro116 Name="LCLWidgetType" Value="qt6"/>
<Macro117 Name="LCLWidgetType" Value="qt6"/>
<Macro118 Name="LCLWidgetType" Value="qt6"/>
<Macro119 Name="LCLWidgetType" Value="qt6"/>
</MacroValues>
<BuildModes>
<Item Name="Debug" Default="True"/>
@@ -358,6 +360,10 @@
<Filename Value="CATTcp.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="CATSliceEndpoint.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="SpectrumView.pas"/>
<IsPartOfProject Value="True"/>