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.