Files
ewsdr/CATAdapter.pas
T
ew8bakandClaude Opus 5 25521a2d10 feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица
(Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB
своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые
кромки умели и раньше — главный приёмник настраивался только пресетами.

Модель (Settings/RadioController):
- TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW
  и FILT_*_NAMES (переехали в Settings — из них строится таблица).
- FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges),
  поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты.
- ApplyFilterSlot — единственная точка «слот → состояние тракта».
- SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением,
  пресеты нельзя испортить случайным движением мыши или энкодера.
- Персист: секция "filters" под MAC, пишутся только режимы, отличные от
  заводских; секция режима сносится целиком, иначе возврат слота к
  заводскому оставлял бы старый ключ.
- Диапазон помнит только выбранный слот: кромки живут в таблице, второго
  источника истины у VAR нет.

UI:
- 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы.
- FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу
  PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева
  слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси.
- Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично
  (не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране
  уже 12px, иначе захват съедал бы клик-тюн.

Отрисовка:
- Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его
  кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было
  преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH)
  рисовался не там, где звучал.
- На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по
  приёмной ширине — TX-полоса от RX не зависит (модель Thetis).
- CalcFilterBandXFor больше не держит свою копию таблицы знака боковой:
  единственная таблица теперь FilterEdgesFromBW.

Управление:
- CAT ZZFL/ZZFH заработали на основном порту (были заглушками).
- Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2
  (было: #4 ширина, #5 не реализован).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:15:33 +03:00

529 lines
21 KiB
ObjectPascal

unit CATAdapter;
// -----------------------------------------------------------------------------
// TCATAdapter — адаптер CAT (Kenwood TS-2000) поверх TRadioController.
//
// Равноправный клиент контроллера наряду с WebAdapter и будущей Arduino-панелью.
// Владеет CAT-движком (TCATEngine) + транспортами (serial/TCP) и строит TCATContext:
// • геттеры читают состояние FController напрямую (CAT-поток, read-only);
// • сеттеры/команды зовут команды контроллера, маршалинг через FController.Invoke
// (GUI = TThread.Synchronize; демон подставит свою очередь).
// Никаких обращений к MainForm/WebAdapter — CAT говорит только с контроллером.
//
// Конфиг serial/TCP применяется снаружи через ApplySettings (UI-настройки CAT
// остаются в MainForm/SettingsForm и зовут ApplySettings при изменении/connect).
// -----------------------------------------------------------------------------
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils,
RadioController, CATEngine, CATSerial, CATTcp, CATSliceEndpoint, Settings,
WDSPEngine, AndromedaPanel;
type
TCATAdapter = class
private
FController: TRadioController;
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;
FSyncInt: Integer;
FSyncBool: Boolean;
// ── Геттеры (CAT-поток, read-only) ──
function GetVfoA: Double;
function GetVfoB: Double;
function GetMode: Integer;
function GetActiveVfo: Integer;
function GetAGCMode: Integer;
function GetVolume: Integer;
function GetDriveLevel: Integer;
function GetFilterIdx: Integer;
function GetFilterBW: Integer;
function GetFilterLow: Integer;
function GetFilterHigh: Integer;
procedure SetFilterLow(V: Integer);
procedure SetFilterHigh(V: 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 GetSplit: Boolean;
function GetSquelchOn: Boolean;
function GetSquelchLevel: Integer;
function GetCTCSSOn: Boolean;
function GetCTCSSTone: Integer;
function GetFMRptDir: Integer;
function GetFMRptOffset: Double;
function GetFMStep: Integer;
function GetAGCTop: Integer;
function GetCTun: Boolean;
// ── Сеттеры/команды (из CAT-потока → Invoke в поток контроллера) ──
procedure SetVfoA(V: Double);
procedure SetVfoB(V: Double);
procedure SetMode(V: Integer);
procedure SetActiveVfo(V: Integer);
procedure SetAGCMode(V: Integer);
procedure SetVolume(V: Integer);
procedure SetDriveLevel(V: Integer);
procedure SetFilterIdx(V: Integer);
procedure SetNRMode(V: Integer);
procedure SetNBMode(V: Integer);
procedure SetSNB(V: Boolean);
procedure SetANF(V: Boolean);
procedure SetTransmitting(V: Boolean);
procedure DoBandUp;
procedure DoBandDown;
procedure DoTuneUp;
procedure DoTuneDown;
procedure DoBandByIndex(Idx: Integer);
procedure SetSplit(V: Boolean);
procedure SetSquelchOn(V: Boolean);
procedure SetSquelchLevel(V: Integer);
procedure SetCTCSSOn(V: Boolean);
procedure SetCTCSSTone(V: Integer);
procedure SetFMRptDir(V: Integer);
procedure SetFMRptOffset(V: Double);
procedure SetFMStep(V: Integer);
procedure SetAGCTop(V: Integer);
procedure SetCTun(V: Boolean);
// ── Andromeda: форвард callback'ов движка в панель (из потока транспорта) ──
procedure PanelVfoStep(V: Integer);
procedure PanelEncoder(Enc, Step: Integer);
procedure PanelButton(Btn: Integer; State, LongPress: Boolean);
procedure PanelVersion(V: Integer);
// ── Sync-методы (выполняются в потоке контроллера) ──
procedure SyncVfoA;
procedure SyncVfoB;
procedure SyncMode;
procedure SyncActiveVfo;
procedure SyncAGC;
procedure SyncVolume;
procedure SyncDrive;
procedure SyncFilterIdx;
procedure SyncNR;
procedure SyncNB;
procedure SyncSNB;
procedure SyncANF;
procedure SyncMOX;
procedure SyncBandUp;
procedure SyncBandDown;
procedure SyncTuneUp;
procedure SyncTuneDown;
procedure SyncBandByIndex;
procedure SyncSplit;
procedure SyncSquelchOn;
procedure SyncSquelchLevel;
procedure SyncCTCSSOn;
procedure SyncCTCSSTone;
procedure SyncFMRptDir;
procedure SyncFMRptOffset;
procedure SyncFMStep;
procedure SyncAGCTop;
procedure SyncCTun;
procedure ApplySliceSettings(const G: TGlobalSettings); // CAT-порты слайсов
public
constructor Create(AController: TRadioController);
destructor Destroy; override;
// Применить serial/TCP-конфиг из глобальных настроек (вызывает UI на change/connect).
procedure ApplySettings(const G: TGlobalSettings);
end;
implementation
constructor TCATAdapter.Create(AController: TRadioController);
var
Ctx: TCATContext;
i: Integer;
begin
inherited Create;
FController := AController;
FillChar(Ctx, SizeOf(Ctx), 0);
Ctx.GetVfoA := @GetVfoA;
Ctx.GetVfoB := @GetVfoB;
Ctx.GetMode := @GetMode;
Ctx.GetActiveVfo := @GetActiveVfo;
Ctx.GetAGCMode := @GetAGCMode;
Ctx.GetVolume := @GetVolume;
Ctx.GetDriveLevel := @GetDriveLevel;
Ctx.GetFilterIdx := @GetFilterIdx;
Ctx.GetFilterBW := @GetFilterBW;
// Кромки фильтра (ZZFL/ZZFH). Раньше у главного приёмника были nil (фильтр
// был индексным) — теперь он хранит те же знаковые кромки, что и слайс.
Ctx.GetFilterLow := @GetFilterLow;
Ctx.GetFilterHigh := @GetFilterHigh;
Ctx.GetNRMode := @GetNRMode;
Ctx.GetNBMode := @GetNBMode;
Ctx.GetSNBEnabled := @GetSNB;
Ctx.GetANFEnabled := @GetANF;
Ctx.GetTransmitting := @GetTX;
Ctx.GetRunning := @GetRunning;
Ctx.GetSMeter := @GetSMeter;
Ctx.GetCurrentBand := @GetBand;
Ctx.SetVfoA := @SetVfoA;
Ctx.SetVfoB := @SetVfoB;
Ctx.SetMode := @SetMode;
Ctx.SetActiveVfo := @SetActiveVfo;
Ctx.SetAGCMode := @SetAGCMode;
Ctx.SetVolume := @SetVolume;
Ctx.SetDriveLevel := @SetDriveLevel;
Ctx.SetFilterIdx := @SetFilterIdx;
Ctx.SetFilterLow := @SetFilterLow;
Ctx.SetFilterHigh := @SetFilterHigh;
Ctx.SetNRMode := @SetNRMode;
Ctx.SetNBMode := @SetNBMode;
Ctx.SetSNBEnabled := @SetSNB;
Ctx.SetANFEnabled := @SetANF;
Ctx.SetTransmitting := @SetTransmitting;
Ctx.DoBandUp := @DoBandUp;
Ctx.DoBandDown := @DoBandDown;
Ctx.DoTuneUp := @DoTuneUp;
Ctx.DoTuneDown := @DoTuneDown;
Ctx.DoBandByIndex := @DoBandByIndex;
Ctx.GetSplit := @GetSplit;
Ctx.SetSplit := @SetSplit;
Ctx.GetSquelchOn := @GetSquelchOn;
Ctx.SetSquelchOn := @SetSquelchOn;
Ctx.GetSquelchLevel := @GetSquelchLevel;
Ctx.SetSquelchLevel := @SetSquelchLevel;
Ctx.GetCTCSSOn := @GetCTCSSOn;
Ctx.SetCTCSSOn := @SetCTCSSOn;
Ctx.GetCTCSSTone := @GetCTCSSTone;
Ctx.SetCTCSSTone := @SetCTCSSTone;
Ctx.GetFMRptDir := @GetFMRptDir;
Ctx.SetFMRptDir := @SetFMRptDir;
Ctx.GetFMRptOffset := @GetFMRptOffset;
Ctx.SetFMRptOffset := @SetFMRptOffset;
Ctx.GetFMStep := @GetFMStep;
Ctx.SetFMStep := @SetFMStep;
Ctx.GetAGCTop := @GetAGCTop;
Ctx.SetAGCTop := @SetAGCTop;
Ctx.GetCTun := @GetCTun;
Ctx.SetCTun := @SetCTun;
Ctx.OnPanelVfoStep := @PanelVfoStep;
Ctx.OnPanelEncoder := @PanelEncoder;
Ctx.OnPanelButton := @PanelButton;
Ctx.OnPanelVersion := @PanelVersion;
FEngine := TCATEngine.Create(Ctx);
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);
FreeAndNil(FPanel);
inherited Destroy;
end;
procedure TCATAdapter.ApplySettings(const G: TGlobalSettings);
var
Cfgs: array[0..3] of TCATSerialConfig;
i: Integer;
begin
for i := 0 to 3 do
begin
Cfgs[i].Enabled := G.CATSerialEnabled[i];
Cfgs[i].PortName := G.CATSerialPort[i];
Cfgs[i].BaudRate := G.CATSerialBaud[i];
Cfgs[i].DataBits := G.CATSerialDataBits[i];
Cfgs[i].StopBits := G.CATSerialStopBits[i];
case G.CATSerialParity[i] of
1: Cfgs[i].Parity := cspOdd;
2: Cfgs[i].Parity := cspEven;
else Cfgs[i].Parity := cspNone;
end;
Cfgs[i].Andromeda := G.CATSerialAndromeda[i];
end;
FSerial.ApplyConfig(Cfgs);
// Если назначен Andromeda-порт — запросить версию панели (рукопожатие ZZZS).
if FSerial.HasAndromeda and Assigned(FPanel) then FPanel.RequestVersion;
FTcp.Stop;
if G.CATTcpEnabled then
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;
// ── Геттеры ──────────────────────────────────────────────────────────────────
function TCATAdapter.GetVfoA: Double; begin Result := FController.FVfoA; end;
function TCATAdapter.GetVfoB: Double; begin Result := FController.FVfoB; end;
function TCATAdapter.GetMode: Integer; begin Result := FController.FMode; end;
function TCATAdapter.GetActiveVfo: Integer; begin Result := FController.FActiveVfo; end;
function TCATAdapter.GetAGCMode: Integer; begin Result := FController.FAGCMode; end;
function TCATAdapter.GetVolume: Integer; begin Result := FController.FVolume; end;
function TCATAdapter.GetDriveLevel: Integer; begin Result := FController.FDrivePercent; end;
function TCATAdapter.GetFilterIdx: Integer; begin Result := FController.FFilter; end;
function TCATAdapter.GetFilterBW: Integer; begin Result := FController.FFilterBW; end;
function TCATAdapter.GetFilterLow: Integer; begin Result := FController.FilterLo; end;
function TCATAdapter.GetFilterHigh: Integer; begin Result := FController.FilterHi; end;
// Кромку меняем по одной — вторую держим текущей (контроллер уводит правку в
// VAR, поэтому пресеты внешняя программа не испортит).
procedure TCATAdapter.SetFilterLow(V: Integer);
begin FController.SetFilterEdges(V, FController.FilterHi); end;
procedure TCATAdapter.SetFilterHigh(V: Integer);
begin FController.SetFilterEdges(FController.FilterLo, V); end;
function TCATAdapter.GetNRMode: Integer; begin Result := FController.FNRMode; end;
function TCATAdapter.GetNBMode: Integer; begin Result := FController.FNBMode; end;
function TCATAdapter.GetSNB: Boolean; begin Result := FController.FSNB; end;
function TCATAdapter.GetANF: Boolean; begin Result := FController.FANF; end;
function TCATAdapter.GetTX: Boolean; begin Result := FController.FTransmitting; end;
function TCATAdapter.GetRunning: Boolean; begin Result := FController.FRunning; end;
function TCATAdapter.GetSMeter: Double; begin Result := FController.FLastSMeter; end;
function TCATAdapter.GetBand: Integer; begin Result := FController.FCurrentBand; end;
function TCATAdapter.GetSplit: Boolean; begin Result := FController.FSplitTxB; end;
function TCATAdapter.GetSquelchOn: Boolean; begin Result := FController.FFMSQOn; end;
function TCATAdapter.GetSquelchLevel: Integer; begin Result := FController.FFMSQLevel; end;
function TCATAdapter.GetCTCSSOn: Boolean; begin Result := FController.FFMCTCSSOn; end;
function TCATAdapter.GetCTCSSTone: Integer; begin Result := FController.FFMCTCSSToneIdx; end;
function TCATAdapter.GetFMRptDir: Integer; begin Result := FController.FFMRptDir; end;
function TCATAdapter.GetFMRptOffset: Double; begin Result := FController.FFMRptOffsetHz; end;
function TCATAdapter.GetFMStep: Integer; begin Result := FController.FFMStepIdx; end;
function TCATAdapter.GetAGCTop: Integer; begin Result := FController.FAGCTop; end;
function TCATAdapter.GetCTun: Boolean; begin Result := FController.FCTun; end;
// ── Сеттеры (маршалинг) ──────────────────────────────────────────────────────
procedure TCATAdapter.SetVfoA(V: Double);
begin FSyncFreq := V; FController.Invoke(@SyncVfoA); end;
procedure TCATAdapter.SyncVfoA;
begin
// A активен — полная перестройка (CTUN/band/сеть + канальная оркестрация через
// OnAfterTune). B активен — только сохраняем/отображаем A (приёмник на B).
if FController.FActiveVfo = 0 then FController.SetVfoA(FSyncFreq)
else FController.StoreVfoA(FSyncFreq);
end;
procedure TCATAdapter.SetVfoB(V: Double);
begin FSyncFreq := V; FController.Invoke(@SyncVfoB); end;
procedure TCATAdapter.SyncVfoB;
begin FController.SetVfoB(FSyncFreq); end;
procedure TCATAdapter.SetMode(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncMode); end;
procedure TCATAdapter.SyncMode;
begin
if (FSyncInt < MODE_MIN) or (FSyncInt > MODE_MAX) then Exit;
FController.SetMode(FSyncInt);
end;
procedure TCATAdapter.SetActiveVfo(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncActiveVfo); end;
procedure TCATAdapter.SyncActiveVfo;
begin FController.SetActiveVfo(FSyncInt); end;
procedure TCATAdapter.SetAGCMode(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncAGC); end;
procedure TCATAdapter.SyncAGC;
begin FController.SetAGCMode(FSyncInt); end;
procedure TCATAdapter.SetVolume(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncVolume); end;
procedure TCATAdapter.SyncVolume;
begin FController.SetVolume(FSyncInt); end;
procedure TCATAdapter.SetDriveLevel(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncDrive); end;
procedure TCATAdapter.SyncDrive;
begin FController.SetDrive(FSyncInt); end;
procedure TCATAdapter.SetFilterIdx(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncFilterIdx); end;
procedure TCATAdapter.SyncFilterIdx;
begin FController.SetFilterIdx(FSyncInt); end; // прямой индекс пресета
procedure TCATAdapter.SetNRMode(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncNR); end;
procedure TCATAdapter.SyncNR;
begin FController.SetNR(FSyncInt); end;
procedure TCATAdapter.SetNBMode(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncNB); end;
procedure TCATAdapter.SyncNB;
begin FController.SetNB(FSyncInt); end;
procedure TCATAdapter.SetSNB(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncSNB); end;
procedure TCATAdapter.SyncSNB;
begin FController.SetSNB(FSyncBool); end;
procedure TCATAdapter.SetANF(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncANF); end;
procedure TCATAdapter.SyncANF;
begin FController.SetANF(FSyncBool); end;
procedure TCATAdapter.SetTransmitting(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncMOX); end;
procedure TCATAdapter.SyncMOX;
begin FController.SetMOX(FSyncBool); end;
procedure TCATAdapter.DoBandUp;
begin FController.Invoke(@SyncBandUp); end;
procedure TCATAdapter.SyncBandUp;
begin FController.BandUp; end;
procedure TCATAdapter.DoBandDown;
begin FController.Invoke(@SyncBandDown); end;
procedure TCATAdapter.SyncBandDown;
begin FController.BandDown; end;
procedure TCATAdapter.DoTuneUp;
begin FController.Invoke(@SyncTuneUp); end;
procedure TCATAdapter.SyncTuneUp;
begin FController.TuneActiveBy(10); end;
procedure TCATAdapter.DoTuneDown;
begin FController.Invoke(@SyncTuneDown); end;
procedure TCATAdapter.SyncTuneDown;
begin FController.TuneActiveBy(-10); end;
procedure TCATAdapter.DoBandByIndex(Idx: Integer);
begin FSyncInt := Idx; FController.Invoke(@SyncBandByIndex); end;
procedure TCATAdapter.SyncBandByIndex;
begin FController.SetBand(FSyncInt); end;
// ── Split / FM (squelch, CTCSS, repeater, step) ───────────────────────────────
procedure TCATAdapter.SetSplit(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncSplit); end;
procedure TCATAdapter.SyncSplit;
begin FController.SetSplit(FSyncBool); end;
procedure TCATAdapter.SetSquelchOn(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncSquelchOn); end;
procedure TCATAdapter.SyncSquelchOn;
begin FController.SetFMSquelch(FSyncBool); end;
procedure TCATAdapter.SetSquelchLevel(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncSquelchLevel); end;
procedure TCATAdapter.SyncSquelchLevel;
begin FController.SetFMSquelchLevel(FSyncInt); end;
procedure TCATAdapter.SetCTCSSOn(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncCTCSSOn); end;
procedure TCATAdapter.SyncCTCSSOn;
begin FController.SetFMCTCSS(FSyncBool); end;
procedure TCATAdapter.SetCTCSSTone(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCTCSSTone); end;
procedure TCATAdapter.SyncCTCSSTone;
begin FController.SetFMCTCSSTone(FSyncInt); end;
procedure TCATAdapter.SetFMRptDir(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncFMRptDir); end;
procedure TCATAdapter.SyncFMRptDir;
begin FController.SetFMRpt(FSyncInt); end;
procedure TCATAdapter.SetFMRptOffset(V: Double);
begin FSyncFreq := V; FController.Invoke(@SyncFMRptOffset); end;
procedure TCATAdapter.SyncFMRptOffset;
begin FController.SetFMRptOffset(FSyncFreq); end;
procedure TCATAdapter.SetFMStep(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncFMStep); end;
procedure TCATAdapter.SyncFMStep;
begin FController.SetFMStep(FSyncInt); end;
procedure TCATAdapter.SetAGCTop(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncAGCTop); end;
procedure TCATAdapter.SyncAGCTop;
begin FController.SetAGCTop(FSyncInt); end;
procedure TCATAdapter.SetCTun(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncCTun); end;
procedure TCATAdapter.SyncCTun;
begin FController.SetCTun(FSyncBool); end;
// ── Andromeda: форвард в панель (сама маршалит действия в поток контроллера) ──
procedure TCATAdapter.PanelVfoStep(V: Integer);
begin if Assigned(FPanel) then FPanel.HandleVfoStep(V); end;
procedure TCATAdapter.PanelEncoder(Enc, Step: Integer);
begin if Assigned(FPanel) then FPanel.HandleEncoder(Enc, Step); end;
procedure TCATAdapter.PanelButton(Btn: Integer; State, LongPress: Boolean);
begin if Assigned(FPanel) then FPanel.HandleButton(Btn, State, LongPress); end;
procedure TCATAdapter.PanelVersion(V: Integer);
begin if Assigned(FPanel) then FPanel.HandleVersion(V); end;
end.