{
  Copyright (C)
  2026 - Uladzimir Karpenka, EW8BAK

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
}

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;
    FSyncStr:  string;
    // Какое поле TX-настроек правит SyncTXField (значение — в FSyncInt/FSyncBool).
    FSyncTXFld: (txfMicGain, txfFiltLow, txfFiltHigh, txfCompOn, txfCompGain,
                 txfEQOn, txfTUNLevel, txfFMDev);
    FSyncEQ:    TCATEQGains;

    // ── Геттеры (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);
    function  GetCWSpeed: Integer;
    procedure SetCWSpeed(V: Integer);
    function  GetCWKeyerMode: Integer;
    procedure SetCWKeyerMode(V: Integer);
    procedure SendCWText(const S: string);
    function  GetCWTextBusy: Boolean;
    procedure SyncCWSpeed;
    procedure SyncCWKeyerMode;
    procedure SyncCWText;
    // ── Телеграф: тон, break-in, сайдтон ──
    function  GetCWPitch: Integer;
    procedure SetCWPitch(V: Integer);
    function  GetCWBreakIn: Boolean;
    procedure SetCWBreakIn(V: Boolean);
    function  GetCWHangTime: Integer;
    procedure SetCWHangTime(V: Integer);
    function  GetCWMonitor: Boolean;
    procedure SetCWMonitor(V: Boolean);
    procedure SyncCWPitch;
    procedure SyncCWBreakIn;
    procedure SyncCWHangTime;
    procedure SyncCWMonitor;
    // ── TX-тракт (активный TX-профиль + вкладка Transmit) ──
    function  GetMicGain: Integer;
    procedure SetMicGain(V: Integer);
    function  GetTXFilterLow: Integer;
    procedure SetTXFilterLow(V: Integer);
    function  GetTXFilterHigh: Integer;
    procedure SetTXFilterHigh(V: Integer);
    function  GetCompOn: Boolean;
    procedure SetCompOn(V: Boolean);
    function  GetCompGain: Integer;
    procedure SetCompGain(V: Integer);
    function  GetTXEQOn: Boolean;
    procedure SetTXEQOn(V: Boolean);
    function  GetTXEQ(out Gains: TCATEQGains): Integer;
    procedure SetTXEQ(NumBands: Integer; const Gains: TCATEQGains);
    function  GetTUNLevel: Integer;
    procedure SetTUNLevel(V: Integer);
    function  GetFMDeviation: Integer;
    procedure SetFMDeviation(V: Integer);
    function  GetTXProfile: Integer;
    procedure SetTXProfile(V: Integer);
    function  GetTXProfileCount: Integer;
    function  GetMonitorOn: Boolean;
    procedure SetMonitorOn(V: Boolean);
    function  GetMonitorLevel: Integer;
    procedure SetMonitorLevel(V: Integer);
    function  GetTuning: Boolean;
    procedure SetTuning(V: Boolean);
    function  GetTwoTone: Boolean;
    procedure SetTwoTone(V: Boolean);
    function  GetPureSignal: Boolean;
    procedure SetPureSignal(V: Boolean);
    procedure DoPSSingleCal;
    // ── Приёмник и сервис ──
    function  GetMuted: Boolean;
    procedure SetMuted(V: Boolean);
    function  GetAtten: Integer;
    procedure SetAtten(V: Integer);
    function  GetSupplyVolts: Double;
    procedure DoVfoSwap;
    function  GetMemoryCount: Integer;
    procedure SendCWMacro(V: Integer);
    procedure SyncMuted;
    procedure SyncAtten;
    procedure SyncVfoSwap;
    procedure SyncCWMacro;
    // Правка одного поля TX-настроек: копия записи, поле, SetTXSettings. Так же
    // это делает вкладка Transmit — она тоже отдаёт запись целиком.
    procedure SyncTXField;
    procedure SyncTXEQ;
    procedure SyncTXProfile;
    procedure SyncMonitorOn;
    procedure SyncMonitorLevel;
    procedure SyncTuning;
    procedure SyncTwoTone;
    procedure SyncPureSignal;
    procedure SyncPSSingleCal;
    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;
  // Телеграф: скорость/режим кейера и программная передача текста (KY/ZZKY).
  Ctx.GetCWSpeed      := @GetCWSpeed;
  Ctx.SetCWSpeed      := @SetCWSpeed;
  Ctx.GetCWKeyerMode  := @GetCWKeyerMode;
  Ctx.SetCWKeyerMode  := @SetCWKeyerMode;
  Ctx.SendCWText      := @SendCWText;
  Ctx.CWTextBusy      := @GetCWTextBusy;
  // Телеграф: тон (ZZCL), break-in и его hang-time (ZZCB/ZZCD), сайдтон (ZZCM).
  Ctx.GetCWPitch      := @GetCWPitch;
  Ctx.SetCWPitch      := @SetCWPitch;
  Ctx.GetCWBreakIn    := @GetCWBreakIn;
  Ctx.SetCWBreakIn    := @SetCWBreakIn;
  Ctx.GetCWHangTime   := @GetCWHangTime;
  Ctx.SetCWHangTime   := @SetCWHangTime;
  Ctx.GetCWMonitor    := @GetCWMonitor;
  Ctx.SetCWMonitor    := @SetCWMonitor;
  // TX-тракт: усиление микрофона, кромки TX-фильтра, компрессор, эквалайзер,
  // TUN/2TON/PS и выбор TX-профиля.
  Ctx.GetMicGain      := @GetMicGain;
  Ctx.SetMicGain      := @SetMicGain;
  Ctx.GetTXFilterLow  := @GetTXFilterLow;
  Ctx.SetTXFilterLow  := @SetTXFilterLow;
  Ctx.GetTXFilterHigh := @GetTXFilterHigh;
  Ctx.SetTXFilterHigh := @SetTXFilterHigh;
  Ctx.GetCompOn       := @GetCompOn;
  Ctx.SetCompOn       := @SetCompOn;
  Ctx.GetCompGain     := @GetCompGain;
  Ctx.SetCompGain     := @SetCompGain;
  Ctx.GetTXEQOn       := @GetTXEQOn;
  Ctx.SetTXEQOn       := @SetTXEQOn;
  Ctx.GetTXEQ         := @GetTXEQ;
  Ctx.SetTXEQ         := @SetTXEQ;
  Ctx.GetTUNLevel     := @GetTUNLevel;
  Ctx.SetTUNLevel     := @SetTUNLevel;
  Ctx.GetFMDeviation  := @GetFMDeviation;
  Ctx.SetFMDeviation  := @SetFMDeviation;
  Ctx.GetTXProfile      := @GetTXProfile;
  Ctx.SetTXProfile      := @SetTXProfile;
  Ctx.GetTXProfileCount := @GetTXProfileCount;
  Ctx.GetMonitorOn    := @GetMonitorOn;
  Ctx.SetMonitorOn    := @SetMonitorOn;
  Ctx.GetMonitorLevel := @GetMonitorLevel;
  Ctx.SetMonitorLevel := @SetMonitorLevel;
  Ctx.GetTuning       := @GetTuning;
  Ctx.SetTuning       := @SetTuning;
  Ctx.GetTwoTone      := @GetTwoTone;
  Ctx.SetTwoTone      := @SetTwoTone;
  Ctx.GetPureSignal   := @GetPureSignal;
  Ctx.SetPureSignal   := @SetPureSignal;
  Ctx.DoPSSingleCal   := @DoPSSingleCal;
  // Приёмник и сервис: MUT (ZZMA), шаговый аттенюатор (ZZRX), напряжение
  // питания (ZZRV), обмен VFO (ZZVS), счётчик памяти (ZZMV), CW-макрос (ZZKM).
  Ctx.GetMuted        := @GetMuted;
  Ctx.SetMuted        := @SetMuted;
  Ctx.GetAtten        := @GetAtten;
  Ctx.SetAtten        := @SetAtten;
  Ctx.GetSupplyVolts  := @GetSupplyVolts;
  Ctx.DoVfoSwap       := @DoVfoSwap;
  Ctx.GetMemoryCount  := @GetMemoryCount;
  Ctx.SendCWMacro     := @SendCWMacro;
  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;

// ---- Телеграф (KS/KY/ZZKM/ZZKS/ZZKY) --------------------------------------
// Правки настроек и запуск передачи маршалим в поток контроллера, как всё
// остальное в адаптере: SetCWSettings трогает движок и пишет конфиг.

function TCATAdapter.GetCWSpeed: Integer;
begin Result := FController.CWSettings.Speed; end;

procedure TCATAdapter.SetCWSpeed(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWSpeed); end;

procedure TCATAdapter.SyncCWSpeed;
var C: TCWSettings;
begin
  C := FController.CWSettings;
  C.Speed := FSyncInt;
  FController.SetCWSettings(C);
end;

function TCATAdapter.GetCWKeyerMode: Integer;
begin Result := FController.CWSettings.KeyerMode; end;

procedure TCATAdapter.SetCWKeyerMode(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWKeyerMode); end;

procedure TCATAdapter.SyncCWKeyerMode;
var C: TCWSettings;
begin
  C := FController.CWSettings;
  C.KeyerMode := FSyncInt;
  FController.SetCWSettings(C);
end;

procedure TCATAdapter.SendCWText(const S: string);
begin FSyncStr := S; FController.Invoke(@SyncCWText); end;

procedure TCATAdapter.SyncCWText;
begin FController.CWXSend(FSyncStr); end;

function TCATAdapter.GetCWTextBusy: Boolean;
begin Result := FController.CWXBusy; end;

function TCATAdapter.GetCWPitch: Integer;
begin Result := FController.CWSettings.Pitch; end;

procedure TCATAdapter.SetCWPitch(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWPitch); end;

procedure TCATAdapter.SyncCWPitch;
var C: TCWSettings;
begin
  C := FController.CWSettings;
  C.Pitch := FSyncInt;
  FController.SetCWSettings(C);
end;

function TCATAdapter.GetCWBreakIn: Boolean;
begin Result := FController.CWSettings.BreakIn; end;

procedure TCATAdapter.SetCWBreakIn(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncCWBreakIn); end;

procedure TCATAdapter.SyncCWBreakIn;
var C: TCWSettings;
begin
  C := FController.CWSettings;
  C.BreakIn := FSyncBool;
  FController.SetCWSettings(C);
end;

function TCATAdapter.GetCWHangTime: Integer;
begin Result := FController.CWSettings.HangTimeMS; end;

procedure TCATAdapter.SetCWHangTime(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWHangTime); end;

procedure TCATAdapter.SyncCWHangTime;
var C: TCWSettings;
begin
  C := FController.CWSettings;
  C.HangTimeMS := FSyncInt;
  FController.SetCWSettings(C);
end;

function TCATAdapter.GetCWMonitor: Boolean;
// Сайдтонов два, и они не взаимозаменяемы: аппаратный звучит в наушниках
// трансивера, программный — в звуковой карте PC. ZZCM правит тот, что отвечает
// за текущий источник манипуляции, иначе команда молча промахивалась бы мимо.
begin
  if FController.CWLocalSource then Result := FController.CWSettings.SidetoneSW
  else                              Result := FController.CWSettings.SidetoneHW;
end;

procedure TCATAdapter.SetCWMonitor(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncCWMonitor); end;

procedure TCATAdapter.SyncCWMonitor;
var C: TCWSettings;
begin
  C := FController.CWSettings;
  if FController.CWLocalSource then C.SidetoneSW := FSyncBool
  else                              C.SidetoneHW := FSyncBool;
  FController.SetCWSettings(C);
end;

// ── TX-тракт ─────────────────────────────────────────────────────────────────
// Геттеры читают живые TX-настройки контроллера (они же — активный профиль),
// сеттеры кладут значение в scratch и правят одно поле в потоке контроллера.

function TCATAdapter.GetMicGain: Integer;
begin Result := Round(FController.FTXSettings.MicGainDB); end;

procedure TCATAdapter.SetMicGain(V: Integer);
begin FSyncTXFld := txfMicGain; FSyncInt := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetTXFilterLow: Integer;
begin Result := FController.FTXSettings.FilterLow; end;

procedure TCATAdapter.SetTXFilterLow(V: Integer);
begin FSyncTXFld := txfFiltLow; FSyncInt := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetTXFilterHigh: Integer;
begin Result := FController.FTXSettings.FilterHigh; end;

procedure TCATAdapter.SetTXFilterHigh(V: Integer);
begin FSyncTXFld := txfFiltHigh; FSyncInt := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetCompOn: Boolean;
begin Result := FController.FTXSettings.CompressorOn; end;

procedure TCATAdapter.SetCompOn(V: Boolean);
begin FSyncTXFld := txfCompOn; FSyncBool := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetCompGain: Integer;
begin Result := Round(FController.FTXSettings.CompressorGain); end;

procedure TCATAdapter.SetCompGain(V: Integer);
begin FSyncTXFld := txfCompGain; FSyncInt := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetTXEQOn: Boolean;
begin Result := FController.FTXSettings.EQOn; end;

procedure TCATAdapter.SetTXEQOn(V: Boolean);
begin FSyncTXFld := txfEQOn; FSyncBool := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetTXEQ(out Gains: TCATEQGains): Integer;
var i: Integer;
begin
  for i := 0 to High(Gains) do Gains[i] := Round(FController.FTXSettings.EQGains[i]);
  Result := FController.FTXSettings.EQNumBands;
end;

procedure TCATAdapter.SetTXEQ(NumBands: Integer; const Gains: TCATEQGains);
begin
  FSyncInt := NumBands;
  FSyncEQ  := Gains;
  FController.Invoke(@SyncTXEQ);
end;

procedure TCATAdapter.SyncTXEQ;
// Частоты полос команда не несёт (их в формате CAT попросту нет) — трогаем
// только усиления и число полос, сетка остаётся той, что настроена в UI.
var
  T: TTXSettings;
  i: Integer;
begin
  T := FController.FTXSettings;
  T.EQNumBands := FSyncInt;
  for i := 0 to High(FSyncEQ) do
    if i <= FSyncInt then T.EQGains[i] := FSyncEQ[i];
  FController.SetTXSettings(T);
end;

function TCATAdapter.GetTUNLevel: Integer;
begin Result := FController.FTXSettings.TUNLevel; end;

procedure TCATAdapter.SetTUNLevel(V: Integer);
begin FSyncTXFld := txfTUNLevel; FSyncInt := V; FController.Invoke(@SyncTXField); end;

function TCATAdapter.GetFMDeviation: Integer;
begin Result := Round(FController.FTXSettings.FMDeviation); end;

procedure TCATAdapter.SetFMDeviation(V: Integer);
begin FSyncTXFld := txfFMDev; FSyncInt := V; FController.Invoke(@SyncTXField); end;

procedure TCATAdapter.SyncTXField;
var T: TTXSettings;
begin
  T := FController.FTXSettings;
  case FSyncTXFld of
    txfMicGain:   T.MicGainDB       := FSyncInt;
    txfFiltLow:   T.FilterLow       := FSyncInt;
    txfFiltHigh:  T.FilterHigh      := FSyncInt;
    txfCompOn:    T.CompressorOn    := FSyncBool;
    txfCompGain:  T.CompressorGain  := FSyncInt;
    txfEQOn:      T.EQOn            := FSyncBool;
    txfTUNLevel:  T.TUNLevel        := FSyncInt;
    txfFMDev:     T.FMDeviation     := FSyncInt;
  end;
  FController.SetTXSettings(T);
end;

function TCATAdapter.GetTXProfile: Integer;
begin Result := FController.ActiveTXProfile; end;

procedure TCATAdapter.SetTXProfile(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncTXProfile); end;

procedure TCATAdapter.SyncTXProfile;
begin FController.SelectTXProfile(FSyncInt); end;

function TCATAdapter.GetTXProfileCount: Integer;
begin Result := FController.TXProfileCount; end;

function TCATAdapter.GetMonitorOn: Boolean;
begin Result := not FController.FRxMuteOnTx; end;

procedure TCATAdapter.SetMonitorOn(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncMonitorOn); end;

procedure TCATAdapter.SyncMonitorOn;
begin FController.SetRxMuteOnTx(not FSyncBool); end;

function TCATAdapter.GetMonitorLevel: Integer;
begin Result := FController.FTxMonVolume; end;

procedure TCATAdapter.SetMonitorLevel(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncMonitorLevel); end;

procedure TCATAdapter.SyncMonitorLevel;
begin FController.SetTXMonVolume(FSyncInt); end;

function TCATAdapter.GetTuning: Boolean;
begin Result := FController.FTuning; end;

procedure TCATAdapter.SetTuning(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncTuning); end;

procedure TCATAdapter.SyncTuning;
begin FController.SetTune(FSyncBool); end;

function TCATAdapter.GetTwoTone: Boolean;
begin Result := FController.FTwoTone; end;

procedure TCATAdapter.SetTwoTone(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncTwoTone); end;

procedure TCATAdapter.SyncTwoTone;
begin FController.SetTwoTone(FSyncBool); end;

function TCATAdapter.GetPureSignal: Boolean;
begin Result := FController.FPSEnabled; end;

procedure TCATAdapter.SetPureSignal(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncPureSignal); end;

procedure TCATAdapter.SyncPureSignal;
begin FController.SetPureSignal(FSyncBool); end;

procedure TCATAdapter.DoPSSingleCal;
begin FController.Invoke(@SyncPSSingleCal); end;

procedure TCATAdapter.SyncPSSingleCal;
begin FController.PSSingleCal; end;

// ── Приёмник и сервис ────────────────────────────────────────────────────────

function TCATAdapter.GetMuted: Boolean;
begin Result := FController.FMuted; end;

procedure TCATAdapter.SetMuted(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncMuted); end;

procedure TCATAdapter.SyncMuted;
begin FController.SetMute(FSyncBool); end;

function TCATAdapter.GetAtten: Integer;
begin Result := FController.FAtten; end;

procedure TCATAdapter.SetAtten(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncAtten); end;

procedure TCATAdapter.SyncAtten;
begin FController.SetAtten(FSyncInt); end;

function TCATAdapter.GetSupplyVolts: Double;
// Телеметрия платы; -1 означает, что данных ещё нет, и наружу отдаётся ноль.
begin Result := FController.FLastSupplyV; end;

procedure TCATAdapter.DoVfoSwap;
begin FController.Invoke(@SyncVfoSwap); end;

procedure TCATAdapter.SyncVfoSwap;
begin FController.VfoSwap; end;

function TCATAdapter.GetMemoryCount: Integer;
begin
  if Assigned(FController.FChannelStore) then Result := FController.FChannelStore.Count
  else Result := 0;
end;

procedure TCATAdapter.SendCWMacro(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWMacro); end;

procedure TCATAdapter.SyncCWMacro;
// Макрос n (1..9) = ячейка памяти сообщений n. Ячеек восемь (F1..F8): девятая
// и пустые просто ничего не передают — очередь дёргать незачем.
var Idx: Integer;
begin
  Idx := FSyncInt - 1;
  if (Idx < 0) or (Idx >= CW_MSG_COUNT) then Exit;
  if FController.CWSettings.Messages[Idx] = '' then Exit;
  FController.CWXSend(FController.CWSettings.Messages[Idx]);
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.
