mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Merge feature/slice-cat: CAT-порт на слайс + Auto TX, аудио in/out на слот слайса
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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`), **на эфире и на экране
|
||||
не проверено**.
|
||||
@@ -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`. На текущей платформе сборки может быть
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user