{
  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 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) …
//
// Порт привязан к слайсу, а флаг Auto TX разрешает команде 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.
