mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой TCP-порт. COM-порты и 19090 остаются за главным приёмником
(слайс A) и не тронуты.
Протокол не дублируется: TCATEngine — чистый парсер поверх записи
callback'ов TCATContext, TCATTcpServer — транспорт поверх движка, поэтому
слайс-CAT = тот же движок с другим контекстом. Новый TCATSliceEndpoint
(движок + TCP-сервер на слот), массивом владеет TCATAdapter. Привязка к
СЛОТУ (буква B..G), не к Id: порт живёт, даже когда слайса нет (PS0).
Auto TX = «Auto Switch TX Slice» из SmartSDR CAT. Вся логика в одной точке
— TRadioController.RequestSliceTx, исполняется в потоке контроллера:
• пока кто-то уже в эфире (любой источник) — заявка игнорируется,
перехвата передачи нет никогда;
• без Auto TX слайс передаёт, только если уже выбран TX-источником;
• RX; снимает только СВОЮ передачу;
• TX-источник после отпускания остаётся на слайсе (как в SmartSDR).
На флаге слайса бейдж TX → AutoTX.
Частота слайса — TuneSliceInBand: свобода в пределах ВКЛЮЧЁННОГО диапазона
(трансвертер → его FreqBegin/FreqEnd, иначе band-план), другой диапазон —
отказ. Вне захваченной полосы окно DDC переезжает: доп. пан — центром на
цель (rfPanFreq → UI перекладывает флаги/шапку/зум-бар), пан 0 — центр
посередине между целью и главным VFO и только если оба влезают (общее
железное окно, главный приёмник не оглушаем).
Настройки: вкладка Slices (enable/порт 19091../Auto TX на слайс), персист
в секции cat + device-blob.
Попутно:
• ZZFL/ZZFH больше не заглушки — новые callback'и кромок фильтра
(у главного nil → прежний дефолт режима), хелпер SignedPad;
• FilterIdxFor, AGCModeToUI, BoardUtils.BandEdges;
• DSP-состояние слайса (NR/NB/SNB/ANF) зеркалится в TCtrlSlice — флаг
слайса рисовал нули и AGC главного;
• FUIReady в TMainForm.OnControllerState: адаптеры, создаваемые по ходу
FormCreate, больше не могут уронить старт рендером до постройки
виджетов (на этом падал Auto TX из настроек).
Проверено вживую: все порты 19091..19096 слушают, PS;/ZZFA;/MD;/ID;/SM0;/
ZZTX; отвечают; старт со всеми включёнными слайсами чист под gdb.
Документ — doc/CAT_SLICES.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
572 lines
20 KiB
ObjectPascal
572 lines
20 KiB
ObjectPascal
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.
|