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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:56:36 +03:00
co-authored by Claude Opus 5
parent 8611898ad1
commit 2d15da0462
13 changed files with 1357 additions and 34 deletions
+12
View File
@@ -24,6 +24,8 @@ function FreqToBandIdx(Hz: Double; Pluto: Boolean = False): Integer;
function BandName(Idx: Integer; Pluto: Boolean = False): string; function BandName(Idx: Integer; Pluto: Boolean = False): string;
// Частота по умолчанию при входе в диапазон (если нет сохранённой/в диапазоне). // Частота по умолчанию при входе в диапазон (если нет сохранённой/в диапазоне).
function BandDefaultFreq(Idx: Integer; Pluto: Boolean = False): Double; function BandDefaultFreq(Idx: Integer; Pluto: Boolean = False): Double;
// Границы диапазона по плану. False = слот пуст/индекс вне плана (Lo=Hi=0).
function BandEdges(Idx: Integer; Pluto: Boolean; out LoHz, HiHz: Double): Boolean;
// Полный band-план активного бэкенда (имена + дефолтные частоты). // Полный band-план активного бэкенда (имена + дефолтные частоты).
function BuildBandPlan(Pluto: Boolean): TBandPlanArray; function BuildBandPlan(Pluto: Boolean): TBandPlanArray;
@@ -91,6 +93,16 @@ begin
else Result := 'AD936x SDR'; else Result := 'AD936x SDR';
end; 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; function FreqToBandIdx(Hz: Double; Pluto: Boolean): Integer;
var var
i: Integer; i: Integer;
+34 -2
View File
@@ -20,8 +20,8 @@ interface
uses uses
Classes, SysUtils, Classes, SysUtils,
RadioController, CATEngine, CATSerial, CATTcp, Settings, WDSPEngine, RadioController, CATEngine, CATSerial, CATTcp, CATSliceEndpoint, Settings,
AndromedaPanel; WDSPEngine, AndromedaPanel;
type type
TCATAdapter = class TCATAdapter = class
@@ -30,6 +30,9 @@ type
FEngine: TCATEngine; FEngine: TCATEngine;
FSerial: TCATSerialManager; FSerial: TCATSerialManager;
FTcp: TCATTcpServer; FTcp: TCATTcpServer;
// CAT дополнительных слайсов: свой движок+TCP-порт на слот (B..). Порты
// главного (serial + FTcp) остаются за главным приёмником — слайс A.
FSliceEps: array[0..CAT_SLICE_SLOTS-1] of TCATSliceEndpoint;
FPanel: TAndromedaPanel; // передняя панель Andromeda/G2 (если включена) FPanel: TAndromedaPanel; // передняя панель Andromeda/G2 (если включена)
// Параметры команды для маршалинга в поток контроллера (через Invoke). // Параметры команды для маршалинга в поток контроллера (через Invoke).
FSyncFreq: Double; FSyncFreq: Double;
@@ -130,6 +133,7 @@ type
procedure SyncFMStep; procedure SyncFMStep;
procedure SyncAGCTop; procedure SyncAGCTop;
procedure SyncCTun; procedure SyncCTun;
procedure ApplySliceSettings(const G: TGlobalSettings); // CAT-порты слайсов
public public
constructor Create(AController: TRadioController); constructor Create(AController: TRadioController);
destructor Destroy; override; destructor Destroy; override;
@@ -142,6 +146,7 @@ implementation
constructor TCATAdapter.Create(AController: TRadioController); constructor TCATAdapter.Create(AController: TRadioController);
var var
Ctx: TCATContext; Ctx: TCATContext;
i: Integer;
begin begin
inherited Create; inherited Create;
FController := AController; FController := AController;
@@ -211,13 +216,18 @@ begin
FSerial := TCATSerialManager.Create(FEngine); FSerial := TCATSerialManager.Create(FEngine);
FTcp := TCATTcpServer.Create(FEngine); FTcp := TCATTcpServer.Create(FEngine);
for i := 0 to CAT_SLICE_SLOTS - 1 do
FSliceEps[i] := TCATSliceEndpoint.Create(FController, i);
// Передняя панель: отправляет наружу через Andromeda-порт, слушает состояние. // Передняя панель: отправляет наружу через Andromeda-порт, слушает состояние.
FPanel := TAndromedaPanel.Create(FController, @FSerial.SendToAndromeda); FPanel := TAndromedaPanel.Create(FController, @FSerial.SendToAndromeda);
FController.AddStateListener(@FPanel.OnControllerState); FController.AddStateListener(@FPanel.OnControllerState);
end; end;
destructor TCATAdapter.Destroy; destructor TCATAdapter.Destroy;
var i: Integer;
begin 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(FTcp) then begin FTcp.Stop; FreeAndNil(FTcp); end;
if Assigned(FSerial) then begin FSerial.StopAll; FreeAndNil(FSerial); end; if Assigned(FSerial) then begin FSerial.StopAll; FreeAndNil(FSerial); end;
FreeAndNil(FEngine); FreeAndNil(FEngine);
@@ -253,6 +263,28 @@ begin
FTcp.Port := G.CATTcpPort; FTcp.Port := G.CATTcpPort;
FTcp.Start; FTcp.Start;
end; 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; end;
// ── Геттеры ────────────────────────────────────────────────────────────────── // ── Геттеры ──────────────────────────────────────────────────────────────────
+30 -15
View File
@@ -106,6 +106,12 @@ type
SetAGCTop: TCATSetIntProc; // AGC-T порог, dB SetAGCTop: TCATSetIntProc; // AGC-T порог, dB
GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл
SetCTun: TCATSetBoolProc; SetCTun: TCATSetBoolProc;
// Кромки фильтра в Гц (ZZFL/ZZFH). Главный приёмник настраивается пресетами
// (callback'и nil → команды отвечают дефолтом режима), слайс — кромками.
GetFilterLow: TCATGetIntFunc;
GetFilterHigh: TCATGetIntFunc;
SetFilterLow: TCATSetIntProc;
SetFilterHigh: TCATSetIntProc;
// Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS) // Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS)
OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO
OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг
@@ -119,6 +125,7 @@ type
FLock: TCriticalSection; FLock: TCriticalSection;
function Pad(V: Int64; W: Integer): string; function Pad(V: Int64; W: Integer): string;
function SignedPad(V: Int64; W: Integer): string; // '+nnn' / '-nnn'
function FreqToStr(Hz: Double): string; function FreqToStr(Hz: Double): string;
function StrToFreq(const S: string): Double; function StrToFreq(const S: string): Double;
function ModeToK(M: Integer): Integer; function ModeToK(M: Integer): Integer;
@@ -363,6 +370,13 @@ begin
if Length(Result) > W then Result := Copy(Result, Length(Result) - W + 1, W); if Length(Result) > W then Result := Copy(Result, Length(Result) - W + 1, W);
end; 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; function TCATEngine.FreqToStr(Hz: Double): string;
begin begin
Result := Pad(Round(Hz), 11); Result := Pad(Round(Hz), 11);
@@ -1447,9 +1461,7 @@ begin
if Assigned(FCtx.SetAGCTop) then FCtx.SetAGCTop(v); if Assigned(FCtx.SetAGCTop) then FCtx.SetAGCTop(v);
Result := ''; Result := '';
end else if Length(s) = 0 then begin end else if Length(s) = 0 then begin
v := SafeGetAGCTop; Result := SignedPad(SafeGetAGCTop, 3);
if v >= 0 then Result := '+' + Pad(v, 3)
else Result := '-' + Pad(-v, 3);
end else end else
Result := CAT_ERROR; Result := CAT_ERROR;
end; end;
@@ -1653,26 +1665,29 @@ function TCATEngine.ZZFI(const s: string): string;
begin Result := ZZBI(s); end; begin Result := ZZBI(s); end;
function TCATEngine.ZZFL(const s: string): string; function TCATEngine.ZZFL(const s: string): string;
// ZZFL: filter low cut (signed Hz, e.g. "-150") // ZZFL: filter low cut (signed Hz, e.g. "-0150"). Работает, если контекст умеет
// STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs // кромки (слайс-CAT); у главного приёмника фильтр индексный — заглушка.
begin begin
if Length(s) >= 4 then if Length(s) >= 4 then begin
Result := '' // accept but ignore — STUB if Assigned(FCtx.SetFilterLow) then FCtx.SetFilterLow(StrToIntDef(s, 0));
else if Length(s) = 0 then begin Result := '';
if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '-0500' 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'; else Result := '-1500';
end else end else
Result := CAT_ERROR; Result := CAT_ERROR;
end; end;
function TCATEngine.ZZFH(const s: string): string; function TCATEngine.ZZFH(const s: string): string;
// ZZFH: filter high cut (signed Hz) // ZZFH: filter high cut (signed Hz) — см. ZZFL.
// STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs
begin begin
if Length(s) >= 4 then if Length(s) >= 4 then begin
Result := '' // accept but ignore — STUB if Assigned(FCtx.SetFilterHigh) then FCtx.SetFilterHigh(StrToIntDef(s, 0));
else if Length(s) = 0 then begin Result := '';
if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '+0500' 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'; else Result := '+3000';
end else end else
Result := CAT_ERROR; 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.
+56
View File
@@ -235,6 +235,10 @@ type
// --- CAT --- // --- CAT ---
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст) FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер) FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
// адаптеры (CAT/web), создаваемые по ходу FormCreate, могут дёрнуть
// Changed() раньше, чем появятся кнопки/панадаптер.
FUIReady: Boolean;
// Временные поля для передачи параметров в Synchronize-методы // Временные поля для передачи параметров в Synchronize-методы
FWebSyncFreq: Double; FWebSyncFreq: Double;
FWebSyncInt: Integer; FWebSyncInt: Integer;
@@ -574,6 +578,10 @@ type
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
const SerAndromeda: array of Boolean; const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer); 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 OnAlexSettingsChange(const A: TAlexSettings);
procedure OnXvtrSettingsChange(const X: TXvtrSettings); procedure OnXvtrSettingsChange(const X: TXvtrSettings);
procedure PushXvtrToWeb; procedure PushXvtrToWeb;
@@ -1102,6 +1110,8 @@ begin
PushBandsToWeb; PushBandsToWeb;
FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0); FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0);
FCATLastGlobal.CATTcpPort := 19090; 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 for i := 0 to 3 do
begin begin
FCATLastGlobal.CATSerialBaud[i] := 9600; FCATLastGlobal.CATSerialBaud[i] := 9600;
@@ -1266,6 +1276,7 @@ begin
{$IFDEF WINDOWS} {$IFDEF WINDOWS}
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver'); FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
{$ENDIF} {$ENDIF}
FUIReady := True; // с этого момента состояние контроллера можно рендерить
end; end;
procedure TMainForm.FormDestroy(Sender: TObject); procedure TMainForm.FormDestroy(Sender: TObject);
@@ -1329,6 +1340,7 @@ var
i: Integer; i: Integer;
SavedFilter, SavedFilterBW: Integer; SavedFilter, SavedFilterBW: Integer;
begin begin
if not FUIReady then Exit; // виджетов ещё нет — рисовать нечего
FSyncingFromController := True; FSyncingFromController := True;
try try
case Field of case Field of
@@ -1703,6 +1715,20 @@ begin
end; end;
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: rfDeviceList:
begin begin
// Discovery обновил список устройств (или завершился без них). // Discovery обновил список устройств (или завершился без них).
@@ -8449,6 +8475,7 @@ begin
SF.OnAnt936xChange := OnAnt936xChange; SF.OnAnt936xChange := OnAnt936xChange;
SF.OnVHFCalChange := OnVHFCalSettingsChange; SF.OnVHFCalChange := OnVHFCalSettingsChange;
SF.OnCATChange := OnCATSettingsChange; SF.OnCATChange := OnCATSettingsChange;
SF.OnSliceCATChange := OnSliceCATSettingsChange;
SF.OnThemeChange := SetLightTheme; SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits; SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange; SF.OnTXChange := OnTXSettingsChange;
@@ -8486,6 +8513,10 @@ begin
FCATLastGlobal.CATSerialAndromeda, FCATLastGlobal.CATSerialAndromeda,
FCATLastGlobal.CATTcpEnabled, FCATLastGlobal.CATTcpEnabled,
FCATLastGlobal.CATTcpPort); FCATLastGlobal.CATTcpPort);
SF.LoadSliceCATSettings(
FCATLastGlobal.CATSliceEnabled,
FCATLastGlobal.CATSlicePort,
FCATLastGlobal.CATSliceAutoTX);
// Перечисляем PA устройства // Перечисляем PA устройства
SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn); SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn);
SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill, SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill,
@@ -8588,4 +8619,29 @@ begin
FController.FSettings.Save; FController.FSettings.Save;
end; 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. end.
+4 -1
View File
@@ -1339,12 +1339,15 @@ begin
else BW := S.FilterHi - S.FilterLo; else BW := S.FilterHi - S.FilterLo;
end; end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId)); 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); O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
// TxSel: слайс — источник передачи; Tx: передача идёт именно с него. // TxSel: слайс — источник передачи; Tx: передача идёт именно с него.
O.SetExtState(Round(S.Volume * 100), S.Muted, False, O.SetExtState(Round(S.Volume * 100), S.Muted, False,
FController.FTransmitting and (FController.FTxSliceId = SliceId), FController.FTransmitting and (FController.FTxSliceId = SliceId),
FController.FTxSliceId = SliceId); FController.FTxSliceId = SliceId);
// Auto TX (настройка CAT-порта слайса) — бейдж TX подписан AutoTX.
O.SetAutoTxState(FController.SliceAutoTx(SliceId));
O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM
// Список аудио-устройств (общий PA-список) + текущее устройство слайса. // Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then if Assigned(FController.FAudioOut) then
+224 -1
View File
@@ -84,7 +84,8 @@ type
rfADCOverload, // ADC overload (рендер индикатора) rfADCOverload, // ADC overload (рендер индикатора)
rfBeaconLock, // QO-100 beacon lock: статус/коррекция rfBeaconLock, // QO-100 beacon lock: статус/коррекция
rfDevice, // подключённое устройство сменилось rfDevice, // подключённое устройство сменилось
rfDeviceList // список discovered устройств обновился rfDeviceList, // список discovered устройств обновился
rfPanFreq // центр доп. пана уехал (ретюн DDC извне)
); );
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object; TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
@@ -162,6 +163,12 @@ type
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM) FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
FMSQLevel: Integer; // порог squelch 0..100 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) DeviceIndex: Integer; // PortAudio-индекс вывода (-1 = default)
DevName: string; DevName: string;
Audio: TAudioOutput; // владеет; свой поток вывода Audio: TAudioOutput; // владеет; свой поток вывода
@@ -330,6 +337,10 @@ type
FTwoTone: Boolean; // 2TON активен FTwoTone: Boolean; // 2TON активен
FSplitTxB: Boolean; FSplitTxB: Boolean;
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX 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) FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
FDisplayDuplex: Boolean; FDisplayDuplex: Boolean;
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.), // QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
@@ -563,6 +574,9 @@ type
procedure ApplyBandDSP(const B: TBandSettings); procedure ApplyBandDSP(const B: TBandSettings);
// Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы. // Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы.
function FilterBWFor(M, Idx: Integer): Integer; function FilterBWFor(M, Idx: Integer): Integer;
// Обратное отображение: ближайший индекс пресета к полосе BW (для CAT/UI,
// где полоса задана в Гц, а протокол оперирует индексом).
function FilterIdxFor(M, BW: Integer): Integer;
// ---- XVTR (трансвертер) ---- // ---- XVTR (трансвертер) ----
// ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть. // ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть.
@@ -644,6 +658,7 @@ type
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer); class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
class function AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode); procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1 procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
procedure SetSliceMute(Id: Integer; Mute: Boolean); procedure SetSliceMute(Id: Integer; Mute: Boolean);
@@ -675,9 +690,30 @@ type
function ActiveMicInDevName: string; function ActiveMicInDevName: string;
function SliceCount: Integer; function SliceCount: Integer;
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean; 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 = главный), куда можно ставить слайс. // Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0); procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean; 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) ---- // ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр в ВИДИМЫХ // Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр в ВИДИМЫХ
@@ -1592,6 +1628,72 @@ begin
Result := (TargetHz >= Lo) and (TargetHz <= Hi); Result := (TargetHz >= Lo) and (TargetHz <= Hi);
end; 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) ---- } { ---- Панадаптеры на аппаратных DDC (этап 3.1) ---- }
procedure TRadioController.ResetPanDDCMaps; procedure TRadioController.ResetPanDDCMaps;
@@ -1958,6 +2060,10 @@ begin
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
FSlices[slot].FMSQLevel := 40; 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].DeviceIndex := DevIndex;
FSlices[slot].DevName := DevName; FSlices[slot].DevName := DevName;
FSlices[slot].Audio := AO; FSlices[slot].Audio := AO;
@@ -2115,6 +2221,10 @@ var idx: Integer;
begin begin
idx := FindSliceIndex(Id); idx := FindSliceIndex(Id);
if idx < 0 then Exit; 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 if Assigned(FDSPEngine) then
begin begin
FDSPEngine.SetSliceNRMode(Id, NRMode); FDSPEngine.SetSliceNRMode(Id, NRMode);
@@ -2383,6 +2493,86 @@ begin
if Result then S := FSlices[idx]; if Result then S := FSlices[idx];
end; 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 точки, поэтому // Снимок кадра для web-зеркала: протокол фиксирован на 1024 точки, поэтому
// при большем разрешении анализатора децимируем max-детектором (не обрезаем). // при большем разрешении анализатора децимируем max-детектором (не обрезаем).
procedure SnapshotPixels(const Pixels: array of Single; Count: Integer; procedure SnapshotPixels(const Pixels: array of Single; Count: Integer;
@@ -2966,6 +3156,27 @@ begin
end; end;
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. // Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
// ===================================================================== // =====================================================================
@@ -3744,6 +3955,18 @@ begin
Result := AGCModes[M]; Result := AGCModes[M];
end; 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; procedure TRadioController.ApplyAGCToEngine;
begin begin
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
+37 -1
View File
@@ -16,6 +16,8 @@ const
CFG_MODE_MIN = RadioModes.MODE_MIN; CFG_MODE_MIN = RadioModes.MODE_MIN;
CFG_MODE_MAX = RadioModes.MODE_MAX; CFG_MODE_MAX = RadioModes.MODE_MAX;
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS 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 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
// отдельной странице, не в общей XVTR-таблице) // отдельной странице, не в общей XVTR-таблице)
@@ -448,9 +450,16 @@ type
CATSerialStopBits: array[0..3] of Integer; // 1 или 2 CATSerialStopBits: array[0..3] of Integer; // 1 или 2
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2 CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2
// TCP CAT сервер (один, многоклиентский) // TCP CAT сервер (один, многоклиентский) — главный приёмник (слайс A)
CATTcpEnabled: Boolean; CATTcpEnabled: Boolean;
CATTcpPort: Integer; // порт, по умолчанию 19090 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; end;
TSettingsManager = class TSettingsManager = class
@@ -652,6 +661,11 @@ begin
end; end;
G.CATTcpEnabled := False; G.CATTcpEnabled := False;
G.CATTcpPort := 19090; 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 // ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
G.DitherEnabled := True; G.DitherEnabled := True;
G.RandomEnabled := True; G.RandomEnabled := True;
@@ -905,6 +919,11 @@ begin
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0); G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False); G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False);
end; 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.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090); G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
// ADC settings // ADC settings
@@ -1039,6 +1058,11 @@ begin
JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]); JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]);
JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]); JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]);
end; 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_enabled',G.CATTcpEnabled);
JW(O,'cat_tcp_port',G.CATTcpPort); JW(O,'cat_tcp_port',G.CATTcpPort);
// ADC settings // ADC settings
@@ -1200,6 +1224,12 @@ begin
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]); JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]); JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]);
end; 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; end;
procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings); procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings);
@@ -1223,6 +1253,12 @@ begin
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0); G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False); G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False);
end; 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; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+193 -3
View File
@@ -76,6 +76,11 @@ type
const SerParity: array of Integer; const SerParity: array of Integer;
const SerAndromeda: array of Boolean; const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer) of object; 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; TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) of object; TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
@@ -92,6 +97,7 @@ type
FPagePA: TScrollBox; FPagePA: TScrollBox;
FPageAdvanced: TScrollBox; FPageAdvanced: TScrollBox;
FPageCAT: TScrollBox; FPageCAT: TScrollBox;
FPageSlices: TScrollBox;
FPageTransmit: TScrollBox; FPageTransmit: TScrollBox;
FNavAudio: TFlatButton; FNavAudio: TFlatButton;
FNavDisplay: TFlatButton; FNavDisplay: TFlatButton;
@@ -100,6 +106,7 @@ type
FNavPA: TFlatButton; FNavPA: TFlatButton;
FNavAdvanced: TFlatButton; FNavAdvanced: TFlatButton;
FNavCAT: TFlatButton; FNavCAT: TFlatButton;
FNavSlices: TFlatButton;
FNavTransmit: TFlatButton; FNavTransmit: TFlatButton;
// ---- Audio tab controls ---- // ---- Audio tab controls ----
@@ -335,6 +342,10 @@ type
FCATSerialAndromeda:array[0..3] of TFlatCheckBox; FCATSerialAndromeda:array[0..3] of TFlatCheckBox;
FCATTcpEn: TFlatCheckBox; FCATTcpEn: TFlatCheckBox;
FCATTcpPort: TFlatSpinEdit; 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 ---- // ---- Advanced tab controls ----
FChkDither: TFlatCheckBox; FChkDither: TFlatCheckBox;
@@ -350,6 +361,7 @@ type
// ---- Callbacks ---- // ---- Callbacks ----
FOnCATChange: TOnCATChange; FOnCATChange: TOnCATChange;
FOnSliceCATChange: TOnSliceCATChange;
FOnPAChange: TOnPASettingsChange; FOnPAChange: TOnPASettingsChange;
FOnPlutoTxChange: TOnPlutoTxChange; FOnPlutoTxChange: TOnPlutoTxChange;
FOnAnt936xChange: TOnAnt936xChange; FOnAnt936xChange: TOnAnt936xChange;
@@ -393,6 +405,7 @@ type
procedure CollectCalFromUI; procedure CollectCalFromUI;
procedure OnCalAnyChange(Sender: TObject); procedure OnCalAnyChange(Sender: TObject);
procedure BuildCATTab; procedure BuildCATTab;
procedure BuildSlicesTab;
procedure BuildTransmitTab; procedure BuildTransmitTab;
procedure BuildAlexTab; procedure BuildAlexTab;
procedure BuildAdvancedTab; procedure BuildAdvancedTab;
@@ -459,6 +472,8 @@ type
procedure OnCATSerialChange(Sender: TObject); procedure OnCATSerialChange(Sender: TObject);
procedure OnCATTcpChange(Sender: TObject); procedure OnCATTcpChange(Sender: TObject);
procedure FireCATChange; procedure FireCATChange;
procedure OnSliceCATCtrlChange(Sender: TObject);
procedure FireSliceCATChange;
procedure BtnCloseClick(Sender: TObject); procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange; procedure FireDisplayChange;
@@ -508,6 +523,11 @@ type
// Таблица разъёмов + имена/видимость слотов трансвертеров. // Таблица разъёмов + имена/видимость слотов трансвертеров.
procedure LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings); procedure LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
procedure LoadVHFCalSettings(const VHFCal: array of Double); 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( procedure LoadCATSettings(
const SerEnabled: array of Boolean; const SerEnabled: array of Boolean;
const SerPort: array of string; const SerPort: array of string;
@@ -526,6 +546,7 @@ type
const BindAddr, User, Pass: string); const BindAddr, User, Pass: string);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange; property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnSliceCATChange: TOnSliceCATChange read FOnSliceCATChange write FOnSliceCATChange;
property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange; property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange;
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange; property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
@@ -781,9 +802,10 @@ begin
FNavCalib := MakeNavButton('Calibration', 290); FNavCalib := MakeNavButton('Calibration', 290);
FNavAdvanced := MakeNavButton('Advanced', 326); FNavAdvanced := MakeNavButton('Advanced', 326);
FNavCAT := MakeNavButton('CAT', 362); FNavCAT := MakeNavButton('CAT', 362);
FNavAlex := MakeNavButton('Antenna / Alex', 398); FNavSlices := MakeNavButton('Slices', 398);
FNavXvtr := MakeNavButton('Transverter', 434); FNavAlex := MakeNavButton('Antenna / Alex', 434);
FNavQO := MakeNavButton('QO-100', 470); FNavXvtr := MakeNavButton('Transverter', 470);
FNavQO := MakeNavButton('QO-100', 506);
FContentPanel := TPanel.Create(Self); FContentPanel := TPanel.Create(Self);
FContentPanel.Parent := Self; FContentPanel.Parent := Self;
@@ -800,6 +822,7 @@ begin
FPageCalib := MakeScrollPage; FPageCalib := MakeScrollPage;
FPageAdvanced := MakeScrollPage; FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage; FPageCAT := MakeScrollPage;
FPageSlices := MakeScrollPage;
FPageAlex := MakeScrollPage; FPageAlex := MakeScrollPage;
FPageXvtr := MakeScrollPage; FPageXvtr := MakeScrollPage;
FPageQO := MakeScrollPage; FPageQO := MakeScrollPage;
@@ -816,6 +839,7 @@ begin
BuildCalibrationTab; BuildCalibrationTab;
BuildAdvancedTab; BuildAdvancedTab;
BuildCATTab; BuildCATTab;
BuildSlicesTab;
BuildAlexTab; BuildAlexTab;
BuildXvtrTab; BuildXvtrTab;
BuildQOTab; BuildQOTab;
@@ -849,6 +873,7 @@ begin
else if Sender = FNavCalib then SelectPage(FPageCalib, FNavCalib) else if Sender = FNavCalib then SelectPage(FPageCalib, FNavCalib)
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT) 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 = FNavAlex then SelectPage(FPageAlex, FNavAlex)
else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr) else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr)
else if Sender = FNavQO then SelectPage(FPageQO, FNavQO); else if Sender = FNavQO then SelectPage(FPageQO, FNavQO);
@@ -865,6 +890,7 @@ begin
FPageCalib.Visible := APage = FPageCalib; FPageCalib.Visible := APage = FPageCalib;
FPageAdvanced.Visible := APage = FPageAdvanced; FPageAdvanced.Visible := APage = FPageAdvanced;
FPageCAT.Visible := APage = FPageCAT; FPageCAT.Visible := APage = FPageCAT;
FPageSlices.Visible := APage = FPageSlices;
FPageAlex.Visible := APage = FPageAlex; FPageAlex.Visible := APage = FPageAlex;
FPageXvtr.Visible := APage = FPageXvtr; FPageXvtr.Visible := APage = FPageXvtr;
FPageQO.Visible := APage = FPageQO; FPageQO.Visible := APage = FPageQO;
@@ -878,6 +904,7 @@ begin
FNavCalib.Active := ANav = FNavCalib; FNavCalib.Active := ANav = FNavCalib;
FNavAdvanced.Active := ANav = FNavAdvanced; FNavAdvanced.Active := ANav = FNavAdvanced;
FNavCAT.Active := ANav = FNavCAT; FNavCAT.Active := ANav = FNavCAT;
FNavSlices.Active := ANav = FNavSlices;
FNavAlex.Active := ANav = FNavAlex; FNavAlex.Active := ANav = FNavAlex;
FNavXvtr.Active := ANav = FNavXvtr; FNavXvtr.Active := ANav = FNavXvtr;
FNavQO.Active := ANav = FNavQO; FNavQO.Active := ANav = FNavQO;
@@ -3522,6 +3549,169 @@ begin
end; end;
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 // BuildAlexTab
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+24 -9
View File
@@ -82,6 +82,7 @@ type
FTx: Boolean; // передача идёт (красный) FTx: Boolean; // передача идёт (красный)
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный) FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True) FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True)
FAutoTx: Boolean; // Auto TX: PTT по CAT-порту слайса сама берёт передачу
FSlice: Char; // буква слайса ('A') FSlice: Char; // буква слайса ('A')
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
FDMRText: string; // стабильный краткий статус рядом с полосой FDMRText: string; // стабильный краткий статус рядом с полосой
@@ -205,6 +206,8 @@ type
procedure SetSquelchState(AOn: Boolean; ALevel: Integer); procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean); procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX) 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; procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
const CurName: string); const CurName: string);
// Список устройств ВВОДА + текущий выбор ('' = общий вход приложения). // Список устройств ВВОДА + текущий выбор ('' = общий вход приложения).
@@ -1091,8 +1094,8 @@ end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer); procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var var
FreqStr, SStr, BWStr, DMRStr: string; FreqStr, SStr, BWStr, DMRStr, TxStr: string;
CurY, X, RW, i, DMRX, DMRRight, TxRight: Integer; CurY, X, RW, i, DMRX, DMRRight, TxRight, TxW: Integer;
R: TRect; R: TRect;
BarL, BarW, MeterX: Integer; BarL, BarW, MeterX: Integer;
begin begin
@@ -1117,6 +1120,11 @@ begin
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть). // Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
// Она крайняя справа, TX и RXM выстраиваются левее неё. // Она крайняя справа, TX и RXM выстраиваются левее неё.
TxRight := W - PAD; 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 if not FIsMain then
begin begin
R := Rect(W - PAD - 14, CurY, W - PAD, CurY + HDR_BADGE_H); R := Rect(W - PAD - 14, CurY, W - PAD, CurY + HDR_BADGE_H);
@@ -1126,7 +1134,7 @@ begin
// RXM — приём слайса на СВОЕЙ передаче. Подсвечен (умолч.) = глушим, т.е. // RXM — приём слайса на СВОЕЙ передаче. Подсвечен (умолч.) = глушим, т.е.
// себя не слышим; погашен = self-monitor (свой downlink через транспондер). // себя не слышим; погашен = self-monitor (свой downlink через транспондер).
// У главного приёмника то же самое делает кнопка RX MUTE на левой панели. // У главного приёмника то же самое делает кнопка 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) if FRxMuteTx then DrawBadge(C, R, 'RXM', CLR_ACCENT, CLR_BADGE_TXT)
else DrawBadge(C, R, 'RXM', CLR_BTN_NORM, CLR_DIM); else DrawBadge(C, R, 'RXM', CLR_BTN_NORM, CLR_DIM);
RegisterHit(R, HIT_RXMUTE); RegisterHit(R, HIT_RXMUTE);
@@ -1144,7 +1152,7 @@ begin
begin begin
DMRStr := FDMRText; DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8; 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 while (Length(DMRStr) > 1) and
(C.TextWidth(DMRStr) > DMRRight - DMRX) do (C.TextWidth(DMRStr) > DMRRight - DMRX) do
SetLength(DMRStr, Length(DMRStr) - 1); SetLength(DMRStr, Length(DMRStr) - 1);
@@ -1153,12 +1161,12 @@ begin
C.TextOut(DMRX, CurY + 1, DMRStr); C.TextOut(DMRX, CurY + 1, DMRStr);
end; end;
// TX badge: a DMR slice is visibly disabled and has no hit target. // 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 if not TxSelectionAllowed then
DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM) DrawBadge(C, R, TxStr, CLR_CARD_BG, CLR_DIM)
else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite) else if FTx then DrawBadge(C, R, TxStr, CLR_TX_ON, clWhite)
else if FTxSel then DrawBadge(C, R, 'TX', CLR_TX_SEL, CLR_BADGE_TXT) else if FTxSel then DrawBadge(C, R, TxStr, CLR_TX_SEL, CLR_BADGE_TXT)
else DrawBadge(C, R, 'TX', CLR_BTN_NORM, CLR_DIM); else DrawBadge(C, R, TxStr, CLR_BTN_NORM, CLR_DIM);
if TxSelectionAllowed then RegisterHit(R, HIT_TX); if TxSelectionAllowed then RegisterHit(R, HIT_TX);
// SPLIT (левее TX) — кликабельно; только у главного: split = TX на VFO B, // SPLIT (левее TX) — кликабельно; только у главного: split = TX на VFO B,
// у слайса своего B-VFO нет (кросс-частотный TX делается TX-бейджем слайса). // у слайса своего B-VFO нет (кросс-частотный TX делается TX-бейджем слайса).
@@ -1653,6 +1661,13 @@ begin
RequestInvalidate; RequestInvalidate;
end; 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); procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
begin begin
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit; if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;
+160
View File
@@ -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`), **на эфире и на экране
не проверено**.
+4
View File
@@ -188,6 +188,10 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
## 5. Транспорты ## 5. Транспорты
> CAT **дополнительных слайсов** (свой TCP-порт на слайс B..G + Auto TX) вынесен
> в отдельный документ: [`CAT_SLICES.md`](CAT_SLICES.md). Тот же движок, другой
> контекст; транспорты ниже принадлежат главному приёмнику (слайс A).
- **Serial** (`CATSerial.pas`): до 4 портов, параметры (порт/скорость/биты/ - **Serial** (`CATSerial.pas`): до 4 портов, параметры (порт/скорость/биты/
чётность/стоп) из `TGlobalSettings.CATSerial*`, применяются чётность/стоп) из `TGlobalSettings.CATSerial*`, применяются
`TCATAdapter.ApplySettings`. На текущей платформе сборки может быть `TCATAdapter.ApplySettings`. На текущей платформе сборки может быть
+8 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="285"/> <BuildNr Value="286"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="117"> <MacroValues Count="119">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -137,6 +137,8 @@
<Macro115 Name="LCLWidgetType" Value="qt6"/> <Macro115 Name="LCLWidgetType" Value="qt6"/>
<Macro116 Name="LCLWidgetType" Value="qt6"/> <Macro116 Name="LCLWidgetType" Value="qt6"/>
<Macro117 Name="LCLWidgetType" Value="qt6"/> <Macro117 Name="LCLWidgetType" Value="qt6"/>
<Macro118 Name="LCLWidgetType" Value="qt6"/>
<Macro119 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>
@@ -358,6 +360,10 @@
<Filename Value="CATTcp.pas"/> <Filename Value="CATTcp.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
</Unit> </Unit>
<Unit>
<Filename Value="CATSliceEndpoint.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit> <Unit>
<Filename Value="SpectrumView.pas"/> <Filename Value="SpectrumView.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>