Files
ewsdr/RadioController.pas
T
ew8bakandClaude Opus 4.8 d08fc17056 feat: Pluto TX path — attenuation-based power, full-duplex IQ stream
TX path for Pluto/AD936x (RX was already working):
- PlutoBackend: continuous full-duplex TX stream (cf-ad9361-dds-core-lpc)
  with FIFO + push thread; SendDUCIQ upsamples WDSP 192k -> device rate
  (integer, linear interp), 24->16 bit, Q inverted; zeros on underrun.
- Power via output attenuator (ADI-recommended): SetTxAtten writes
  hardwaregain on voltage0(out). Controller maps drive% -> dB
  (PlutoTxAttForDrive/ApplyPlutoTxAtten); -89.75 dB on RX (~off),
  ceiling PlutoTxMaxAttDb on 100% drive.
- Configurable max-att (default -10 dB, ADI-linear) for external amp:
  persisted in global settings, UI spin on PA page, routed through
  controller SetPlutoTxMaxAtt (UI stays decoupled from logic).
- RadioBackend: virtual SetTxAtten (no-op for HPSDR).
- Update doc/PLUTO_INTEGRATION_PLAN.md: phases 3/4/5 + risks.

Builds clean (headless + qt6 GUI). Not yet verified on hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:27:49 +03:00

3200 lines
147 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit RadioController;
{
EWSDR — Radio Controller (headless ядро)
Назначение
----------
TRadioController — «мозг» трансивера БЕЗ привязки к UI/LCL-формам. Владеет
состоянием радио и движками (сеть HPSDR, WDSP, аудио, настройки, каналы) и
предоставляет единый интерфейс управления для ЛЮБОГО фронтенда: GUI, Web, CAT,
аппаратная панель Arduino.
• Команды (SetMode, SetVfoA, TuneActiveBy, StartStop, …) — единая точка входа.
Абсолютные (Set*) удобны UI/Web; относительные (*By/Up/Down) — энкодерам.
• GetSnapshot → TRadioSnapshot — весь стейт одной записью (синхронизация
нового фронтенда при подключении: LED/дисплеи панели, веб-клиент).
• OnStateChanged(Field) — гранулярное уведомление об изменении поля; все
фронтенды остаются синхронны, кто бы ни инициировал изменение.
Маршалинг потоков
-----------------
Команды из сетевых/serial/web-потоков идут через OnInvoke (GUI = обёртка над
TThread.Synchronize; демон = своя очередь/прямой вызов), чтобы вызовы движка
шли из одного потока без зависимости от GUI-цикла LCL.
СТАТУС МИГРАЦИИ (Фаза 1)
------------------------
Состояние и движки вынесены сюда как ПУБЛИЧНЫЕ ПОЛЯ с теми же именами, что были
в TMainForm (FVfoA, FNetwork, FDSPEngine, …), чтобы проводка MainForm свелась к
механической замене FXxx -> FController.FXxx без изменения поведения. Логика
операций (ApplyModeFilter, BtnStartStopClick, ApplyVfoA, …) пока остаётся в
MainForm; команды контроллера обновляют поле + шлют событие и будут наполнены
логикой на следующих фазах (помечено TODO-wiring). Поля станут приватными,
когда MainForm перестанет писать в них напрямую.
ВЛАДЕНИЕ ДВИЖКАМИ (Фаза 1): объекты создаёт и освобождает MainForm (в
FormCreate/FormDestroy), сохраняя в поля контроллера. Перенос создания в
Create/Destroy контроллера — отдельная фаза (нужен для режима демона).
FPC 3.2+ / режим Delphi. Без inline var и анонимных процедур.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math,
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
WDSPEngine, AudioOutput, AudioInput,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
type
// QO-100 beacon lock — статус контура (для UI/маркеров).
TBeaconState = (bsIdle, // лок выключен
bsSearch, // включён, маяк ещё не подтверждён (низкий prominence)
bsLock); // маяк уверенно виден и сопровождается
// Поле, которое изменилось — для гранулярных уведомлений наружу.
TRadioField = (
rfVfoA, rfVfoB, rfActiveVfo,
rfMode, rfFilter, rfFilterBW,
rfAGCMode, rfAGCTop,
rfRxGain, // Pluto: hw-gain (auto/manual dB)
rfVolume, rfMute, rfDrive, rfAtten,
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
rfCenterFreq, rfSpan, rfSampleRate,
rfCTun, rfDuplex,
rfRunning, rfConnected, rfTransmitting, rfTuning,
rfNR, rfNB, rfSNB, rfANF,
rfWfAGC, rfWfNF, rfDither, rfRandom,
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
rfFMStep, rfFMStepIdx, rfFMRpt,
rfChannel,
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
rfADCOverload, // ADC overload (рендер индикатора)
rfBeaconLock, // QO-100 beacon lock: статус/коррекция
rfDevice, // подключённое устройство сменилось
rfDeviceList // список discovered устройств обновился
);
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
// Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize).
TUIInvokeEvent = procedure(M: TThreadMethod) of object;
// Перехват RX-аудио потребителем (web-адаптер): вернуть True, если аудио
// отправлено клиенту и НЕ должно идти в локальный AudioOut. Вызывается из
// DSP-потока (как сам OnAudioReady) — реализация должна быть thread-safe.
TAudioConsumeEvent = function(const Left, Right: array of Single;
Count: Integer): Boolean of object;
// Сырьё спектра/водопада наружу (фронтенды рисуют). Вызывается из DSP-потока.
TPixelDataEvent = procedure(const Pixels: array of Single; Count: Integer) of object;
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
TRadioSnapshot = record
VfoA, VfoB, CenterFreq, SpanHz: Double;
SampleRate: Integer;
ActiveVfo: Integer;
Mode, Filter, FilterBW: Integer;
AGCMode, AGCTop: Integer;
RxGainMode: Integer;
RxGainDb: Integer;
Volume, Drive, Atten: Integer;
Band, Xvtr: Integer;
Muted, CTun, Duplex: Boolean;
Running, Connected, Transmitting, Tuning: Boolean;
NRMode, NBMode: Integer;
SNB, ANF: Boolean;
WfAGC, WfNF: Boolean;
FMStepOn: Boolean;
FMStepIdx: Integer;
FMCTCSSOn: Boolean;
FMCTCSSToneIdx: Integer;
FMSQOn: Boolean;
FMSQLevel: Integer;
FMRptDir: Integer;
SMeter, FwdW, SWR, SupplyV, SupplyA: Double;
PLLLock: Boolean;
end;
{ TRadioController }
TRadioController = class
private
FOnInvoke: TUIInvokeEvent;
FOnStateChanged: TRadioStateEvent;
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
procedure Changed(Field: TRadioField);
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
function ActiveVfoHz: Double; // частота активного VFO (A или B)
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
public
// ============================================================
// ПУБЛИЧНЫЕ ПОЛЯ (миграционные — имена совпадают с TMainForm).
// Внешний чистый доступ для адаптеров — через команды / GetSnapshot /
// OnStateChanged. Прямая запись извне — временная мера на время Фазы 1.
// ============================================================
// ---- Последний кадр спектра/водопада (пиксели от WDSP) ----
// Снимок для исходящего web-зеркала (PushState читает напрямую). Пишется из
// DSP-потока в OnSpectrumReady/OnWaterfallReady; читается из GUI-таймера —
// та же толерантная гонка, что и раньше (косметика пикселей).
FSpectrumBuf: array[0..1023] of Single;
FWaterfallBuf: array[0..1023] of Single;
FSpectrumBufCount: Integer;
FWaterfallBufCount: Integer;
// ---- Движки (владение: см. шапку юнита) ----
// FNetwork — абстрактный бэкенд железа (TRadioBackend): конкретный класс
// (THPSDRNetwork / TPlutoBackend позже) выбирается при подключении.
FNetwork: TRadioBackend;
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FAudioIn: TAudioInput;
FSettings: TSettingsManager;
FChannelStore: TChannelStore;
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
FWDSPReady: Boolean;
// ---- Частоты / режим / фильтр / AGC ----
FVfoA, FVfoB: Double;
FActiveVfo: Integer;
FMode: Integer;
FFilter: Integer;
FFilterBW: Integer;
FAGCMode: Integer;
FAGCTop: Integer;
FRxGainMode: Integer; // Pluto hw-gain: 0=manual,1=fast,2=slow,3=hybrid
FRxGainDb: Integer; // Pluto manual gain (дБ, 0..73)
FCTun: Boolean;
FCenterFreq: Double;
FSpanHz: Double;
FSampleRate: Integer;
FCurrentBand: Integer;
FCurrentXvtr: Integer;
// ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ----
FBeaconLockOn: Boolean; // лок включён пользователем
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
// (только индикация на кнопке); сама поправка
// живёт в LOError активного трансвертера
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
// FFT-трекинг маяка (робастный, по display-FFT):
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
FBeaconLocked: Boolean; // пик уверенно виден
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
// ---- Аудио / TX ----
FVolume: Integer;
FMuted: Boolean;
FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины)
FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte)
FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада)
FAtten: Integer;
FPAMaxPower: Double;
FPlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100% (потолок, дБ<0)
FRunning: Boolean;
FTransmitting: Boolean;
FTuning: Boolean;
FSplitTxB: Boolean;
FDisplayDuplex: Boolean;
FHWPTTActive: Boolean;
FHWPTTStartedTX: Boolean;
FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента)
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
FSendAudioToRadio: Boolean;
FAudioOutDevName: string;
FAudioInDevName: string;
FDevConnected: Boolean;
FDiscoverNoneFound: Boolean; // discovery завершился без устройств (для рендера)
// ---- DSP-фильтры шума / водопад ----
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FDitherEnabled: Boolean;
FRandomEnabled: Boolean;
// ---- Display state (persist в TGlobalSettings; рендерится FSpecView/Wideband) ----
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FShowWideband: Boolean;
FWidebandFill: Boolean;
FSpectrumFill: Boolean;
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
FSpecRefLevel: Double;
FSpecRange: Double;
FSpecGridStep: Double;
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
FTXSpecRange: Double;
FTXSpecGridStep: Double;
// ---- FM ----
FFMDeviation: Double;
FFMCTCSSOn: Boolean;
FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean;
FFMSQLevel: Integer;
FFMStepOn: Boolean;
FFMStepIdx: Integer;
FFMRptDir: Integer;
FFMRptOffsetHz: Double;
FFMRptAutoActive: Boolean;
FFMCTCSSAutoActive: Boolean;
// ---- Конфиги / устройство (per-MAC) ----
FTXSettings: TTXSettings;
FAlexSettings: TAlexSettings;
FXvtrSettings: TXvtrSettings;
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double;
FDevMAC: array[0..5] of Byte;
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
FLoadedGlobal: TGlobalSettings;
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
FLastPLLLock: Boolean;
// Pluto-телеметрия (нет HP-status): температура чипа °C / RSSI приёмника дБ.
// Опрашиваются ServicePlutoTelemetry с UI-таймера; TELEMETRY_NONE = нет данных.
FLastPlutoTempC, FLastPlutoRSSIdB: Double;
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
// ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ----
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
FRXStartTime: QWord; // GetTickCount64 момента START
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index
FDDCLastSeq: array[0..6] of LongWord;
FDDCSeqValid: array[0..6] of Boolean;
FSeqErrorCount: LongWord;
FLastSeqErrorDDC: Integer;
FLastSeqErrorDelta: Int64;
FSeqOkStreak: Integer;
FADCOverloadActive: Boolean; // ADC overload (рендерится по rfADCOverload)
// ---- TX DUC-очередь (накопление IQ от WDSP в пакеты по 240, OnTXIQ) ----
FDUCPendingI: array[0..239] of Integer;
FDUCPendingQ: array[0..239] of Integer;
FDUCPendingCount: Integer;
// Pending-значения декода HP-статуса (сетевой поток → ApplyHPStatus через
// Invoke; Synchronize блокирующий и последовательный, гонок нет).
FHPpFwdW, FHPpSWR, FHPpSupplyV, FHPpSupplyA: Double;
FHPpPLL, FHPpHWPTT: Boolean;
FHPpOverload: Byte;
// Pending discovered-устройство (сетевой поток discovery → ApplyDeviceFound
// через Invoke; Synchronize блокирующий — устройства приходят по одному).
FPendingFoundDev: THPSDRDevice;
FPendingFoundEntry: string;
// URI для probe сетевых Pluto при discovery (см. SetPlutoProbeURIs).
FPlutoProbeURIs: array of string;
constructor Create;
destructor Destroy; override;
// Lifecycle движков: создаёт FNetwork/FDSPEngine/FAudioOut/FAudioIn с
// заданным sample rate (вызывающий уже определил его из настроек). Колбэки
// движков навешивает вызывающий (GUI: методы MainForm; демон: свои). Парный
// FreeEngines закрывает и освобождает их; вызывается из Destroy.
procedure CreateEngines(ASampleRate: Integer);
procedure FreeEngines;
// ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ----
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
// вызывающим (GUI или демон) после CreateEngines.
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
procedure OnMicPacket(const Data: TMicDataPacket);
function PullSoundCardMic(MaxN: Integer): Integer;
procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
procedure OnHPStatus(const Status: THighPriorityStatus);
procedure ApplyHPStatus; // маршалится из OnHPStatus (сглаживание/overload/HWPTT)
procedure OnTXIQ(const Buf: array of Double; Count: Integer);
procedure OnDeviceFound(const Dev: THPSDRDevice); // сетевой поток discovery
procedure ApplyDeviceFound; // маршалится из OnDeviceFound (store + rfDeviceList)
procedure DiscoverFinishedNone; // маршалится из discover-потока: устройств нет
// Маршалинг команды в поток контроллера (GUI: TThread.Synchronize).
procedure Invoke(M: TThreadMethod);
// Снимок всего состояния (для синхронизации нового фронтенда).
function GetSnapshot: TRadioSnapshot;
// Частотные хелперы — чистые вычисления над состоянием, общие для всех
// фронтендов и сетевого слоя (Vfo/Mode/Band/CTun/TX/FM-Rpt).
// XvtrTranslate — переводит видимую частоту в частоту IF-тракта с учётом
// активного трансвертора (LOOffset/LOError).
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
function XvtrTranslate(VisibleHz: Double): Double;
function ActiveTXFreqHz: Double;
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
// (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/
// несимметричный сосед) или вне span — тогда трек держим на месте.
function MeasureBeaconFFT(ExpectOff, WinHz: Double;
out PeakOff, PromDB: Double): Boolean;
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
procedure ApplyBeaconLOError(dLock: Double);
// Flush LOError на диск при выключении лока (если троттлинг не успел).
procedure PersistBeaconCalibration;
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
procedure ApplyModeFilter;
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
function CalcDriveByte: Byte;
// Источник микрофона по умолчанию для MOX-кнопки/Web-TX: звуковая карта
// если настроена, иначе HW mic-стрим трансивера. HW PTT → txmsRadio в SetMOX.
function DefaultMicSource: TTXMicSource;
// ---- TX/DSP-цепь (чистые над состоянием: читают F*/FTXSettings, пишут в движок) ----
function BuildMicLineSelectByte: Byte; // DUC Specific byte 50 (mic/line биты)
procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP
procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
// MakeBandSettings — снимок текущего состояния в запись диапазона.
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
// RestoreBand — загружает состояние диапазона, применяет к DSP, шлёт события
// рендера. Финальную перестройку VFO A делает вызывающий (UI: ApplyVfoA).
function MakeBandSettings: TBandSettings;
procedure SaveCurrentBand;
procedure RestoreBand(BandIdx: Integer);
// Глобальные настройки устройства (persist по MAC). BuildGlobalSettings
// собирает радио-поля из своего состояния, а НЕ-радио поля (тема/FPS/digits/
// CAT) переносит сквозь из FLoadedGlobal — контроллер их не трактует, только
// round-trip в файл. SaveGlobalSettings пишет блоб (гейт по FDevConnected).
function BuildGlobalSettings: TGlobalSettings;
procedure SaveGlobalSettings;
// Применяет DSP-настройки диапазона (mode/filter/AGC/CTUN/FM) + шлёт события
// рендера. НЕ трогает частоты VFO/центр — это переиспользуется как RestoreBand
// (с загрузкой VFO), так и SetActiveVfo (bandstack: смена бэнда без сброса VFO).
procedure ApplyBandDSP(const B: TBandSettings);
// Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы.
function FilterBWFor(M, Idx: Integer): Integer;
// ---- XVTR (трансвертер) ----
// ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть.
// ActivateXvtr — вход в слот: state+DSP (через ApplyBandDSP) + VFO/центр + сеть;
// рендер через rfXvtr/rfBand(XVTR-aware)/rfActiveVfo. UI: wideband+web.
// DeactivateXvtr — сохраняет слот (SaveCurrentBand) и выходит; HF-возврат
// (RestoreBand + финальная перестройка VFO) делает UI-обёртка.
procedure ApplyXvtrToNetwork;
procedure ActivateXvtr(Idx: Integer);
procedure DeactivateXvtr;
// Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX).
// Вызывается командами смены частоты/центра и UI после правки drive.
procedure PushNetworkState;
// ====================================================================
// КОМАНДЫ — единый API для GUI / Web / CAT / Arduino.
// Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring.
// ====================================================================
// Частоты / VFO
procedure SetVfoA(Hz: Double);
procedure StoreVfoA(Hz: Double); // сохранить частоту A без перестройки (когда активен B)
procedure SetVfoB(Hz: Double);
procedure SetActiveVfo(Idx: Integer);
procedure TuneActiveBy(DeltaHz: Double); // энкодер VFO
procedure SetCenter(Hz: Double);
procedure VfoSwap;
procedure VfoACopyB;
procedure VfoBCopyA;
// Режим / фильтр / AGC
procedure SetMode(M: Integer);
procedure SetFilterIdx(Idx: Integer);
procedure SetFilterBW(BWHz: Integer);
procedure SetAGCMode(M: Integer);
procedure SetAGCTop(DB: Integer);
procedure AGCTopBy(Delta: Integer);
// RX hw-gain (Pluto): режим (0=manual,1=fast,2=slow,3=hybrid) / ручной дБ.
// На HPSDR — no-op в бэкенде.
procedure SetRxGainMode(M: Integer);
procedure SetRxGain(Db: Integer);
procedure RxGainBy(Delta: Integer);
procedure ApplyRxGainToBackend; // FRxGain* → FNetwork.SetRxGain
// Громкость / mute / drive / аттенюатор
procedure SetVolume(V: Integer);
procedure VolumeBy(Delta: Integer);
procedure SetMute(On_: Boolean);
procedure ToggleMute;
procedure SetDrive(V: Integer);
procedure DriveBy(Delta: Integer);
procedure SetAtten(Idx: Integer);
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning
procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель)
// Диапазоны / трансвертеры / спан
procedure SetBand(Idx: Integer);
procedure BandUp;
procedure BandDown;
procedure SetXvtrBand(Idx: Integer); // -1 = выход в HF
procedure SetCurrentBandIdx(NewBand: Integer); // лёгкая смена HF-диапазона (band+drive+рендер), без restore
procedure SetSpan(Hz: Integer);
procedure SetSampleRate(Hz: Integer);
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
procedure SetBeaconLock(On_: Boolean);
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
function BeaconState: TBeaconState;
function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError)
function BeaconSNRdB: Double;
function BeaconLockEnabled: Boolean;
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
// Отображение / приём
procedure SetCTun(On_: Boolean);
procedure SetDuplex(On_: Boolean);
procedure SetWfAGC(On_: Boolean);
procedure SetWfNF(On_: Boolean);
// DSP-фильтры шума
procedure SetNR(Mode: Integer);
procedure SetNB(Mode: Integer);
procedure SetSNB(On_: Boolean);
procedure SetANF(On_: Boolean);
// Передача
procedure SetMOX(On_: Boolean);
procedure SetTune(On_: Boolean); // TUN
// FM
procedure SetFMStep(Idx: Integer);
procedure SetFMCTCSS(On_: Boolean);
procedure SetFMCTCSSTone(Idx: Integer);
procedure SetFMSquelch(On_: Boolean);
procedure SetFMSquelchLevel(V: Integer);
procedure SetFMRpt(Dir: Integer); // явное направление; снимает авто-флаг
procedure SetFMRptOffset(Hz: Double); // сдвиг репитера; применяет, если RPT вкл
procedure UpdateRptAutoState; // авто-MINUS для 2 м под FVfoA
// Каналы: применение/деактивация каналов вынесено в TChannelController
// (отдельный юнит-адаптер поверх контроллера), оркеструет команды ниже.
// Жизненный цикл движка / устройства
procedure StartStop;
procedure SetRun(On_: Boolean);
procedure StartRunning(SpectrumWidth: Integer); // Run=1 bring-up: General/DDC/DUC + DSP restore
procedure StopRunning; // Run=0: graceful-стоп RX-потока + сброс TX/телеметрии
function ConnectDevice(const Dev: THPSDRDevice): Boolean; // Connect + load-by-MAC; рендер через rfDevice
function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live
procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless)
procedure Discover;
// Гарантирует, что FNetwork — бэкенд нужного типа (HPSDR/Pluto): при
// несовпадении пересоздаёт его, перенося колбэки данных. Вызывается из
// ConnectDevice по Dev.Kind. Discovery использует транзиентные экземпляры.
procedure EnsureBackend(Kind: TBackendKind);
// Возможности текущего бэкенда (для гейтинга UI / пресетов sample rate).
function BackendCaps: TBackendCaps;
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
function IsPluto: Boolean;
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
// HPSDR → тип платы; Pluto/LibreSDR → hw_model + серийник (если есть).
function BoardDisplayName: string;
// Опрос аппаратной телеметрии Pluto (температура/RSSI) с троттлингом — зовётся
// с UI-таймера (MeterTimer @10 Гц), как ServiceBeaconLock. No-op для HPSDR.
// Результат — в FLastPlutoTempC/FLastPlutoRSSIdB (UI читает и форматирует).
procedure ServicePlutoTelemetry;
// Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд
// перед Discover (сохранённые Pluto + ручной ввод IP).
procedure SetPlutoProbeURIs(const URIs: array of string);
// ---- События наружу ----
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
property OnSpectrumData: TPixelDataEvent read FOnSpectrumData write FOnSpectrumData;
property OnWaterfallData: TPixelDataEvent read FOnWaterfallData write FOnWaterfallData;
end;
implementation
constructor TRadioController.Create;
begin
inherited Create;
FSettings := TSettingsManager.Create; // владелец настроек (GUI и демон)
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FCurrentBand := 5; FCurrentXvtr := -1;
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
FPAMaxPower := 100.0;
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
FFMStepOn := True;
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE;
FPlutoTeleTick := 0; FPlutoTempBad := 0;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
end;
procedure TRadioController.CreateEngines(ASampleRate: Integer);
// Создаёт движки в полях контроллера. Колбэки (OnAudio/OnSpectrum/OnHPStatus…)
// навешивает вызывающий после возврата — они указывают на UI/демон.
begin
FNetwork := THPSDRNetwork.Create;
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
// QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock).
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
FBeaconLockHz := 0.0;
FBeaconLockOn := False;
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
// формы / при подключении устройства).
FAudioOut := TAudioOutput.Create(48000);
FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize;
FAudioIn := TAudioInput.Create(48000);
end;
procedure TRadioController.FreeEngines;
begin
if Assigned(FNetwork) then
begin
if FNetwork.Connected then FNetwork.Disconnect;
FreeAndNil(FNetwork);
end;
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end;
end;
destructor TRadioController.Destroy;
begin
FreeEngines;
FreeAndNil(FDeviceStore);
FreeAndNil(FSettings);
inherited Destroy;
end;
procedure TRadioController.Changed(Field: TRadioField);
begin
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
end;
procedure TRadioController.PushNetworkState;
// Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние
// MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг.
begin
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
procedure TRadioController.OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
// Сетевой поток: трекинг seq + подача IQ активного DDC в WDSP. UI не трогаем —
// статус рисует таймер по полям ниже. Backend-агностичен (Pluto синтезирует
// тот же TDDCIQPacket).
var
SamplesPerFrame: Integer;
Seq, ExpectedSeq: LongWord;
Delta: Int64;
begin
Inc(FRXPacketCount);
FRXLastPktTime := GetTickCount64;
Seq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
if (DDCIndex >= Low(FDDCLastSeq)) and (DDCIndex <= High(FDDCLastSeq)) then
begin
if Seq <> 0 then
begin
ExpectedSeq := FDDCLastSeq[DDCIndex] + 1;
if FDDCSeqValid[DDCIndex] and (Seq <> ExpectedSeq) then
begin
Inc(FSeqErrorCount);
FLastSeqErrorDDC := DDCIndex;
Delta := Int64(Seq) - Int64(ExpectedSeq);
FLastSeqErrorDelta := Delta;
FSeqOkStreak := 0;
end
else if FDDCSeqValid[DDCIndex] then
begin
if FSeqErrorCount > 0 then
begin
Inc(FSeqOkStreak);
if FSeqOkStreak >= 20 then
begin
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
end;
end;
end;
end;
FDDCLastSeq[DDCIndex] := Seq;
FDDCSeqValid[DDCIndex] := True;
end;
// Подаём IQ в DSP — только с активного DDC
if (DDCIndex = FActiveDDC) and FWDSPReady then
begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
end;
// Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path)
FLastDDCSeq := Seq;
FLastDDCIndex := DDCIndex;
end;
procedure TRadioController.OnTXIQ(const Buf: array of Double; Count: Integer);
// WDSP TXA отдаёт IQ-пары @ TX rate. Копим хвост между вызовами и шлём только
// полные DUC-пакеты по 240 (зануление хвоста давало 47 Гц гул). Клампим в
// 24-bit, чтобы пики компрессора/EQ (чуть >1.0) не оборачивались через wrap.
// Backend-агностично: Pluto-backend в SendDUCIQ переведёт 24-bit→int16.
function ScaleIQ24(V: Double): Integer;
var S: Double;
begin
S := V * 8388607.0;
if S > 8388607.0 then Result := 8388607
else if S < -8388608.0 then Result := -8388608
else Result := Round(S);
end;
var
i: Integer;
begin
if not (Assigned(FNetwork) and FNetwork.Connected) then Exit;
for i := 0 to Count - 1 do
begin
FDUCPendingI[FDUCPendingCount] := ScaleIQ24(Buf[i * 2]);
FDUCPendingQ[FDUCPendingCount] := ScaleIQ24(Buf[i * 2 + 1]);
Inc(FDUCPendingCount);
if FDUCPendingCount >= 240 then
begin
FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ);
FDUCPendingCount := 0;
end;
end;
end;
procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus);
// Сетевой поток: декод HP-статуса (board-specific supply/power/SWR) → pending →
// маршалинг в ApplyHPStatus (сглаживание + overload + HWPTT уже на потоке UI/ядра).
const
SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем
SWR_CAP = 9.9;
var
FwdPwr, RevPwr: Word;
SupplyV, SupplyA, FwdW, SWRV, Rho: Double;
IsTx: Boolean;
begin
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
SupplyV := -1.0;
SupplyA := -1.0;
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis).
// Прочие платы — в SupplyVolts-байтах, без тока.
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
begin
if FNetwork.Device.BoardType = 5 then
SupplyV := ADCToSupplyVolts(
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
FNetwork.Device.BoardType)
else
SupplyV := ADCToSupplyVolts(
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
FNetwork.Device.BoardType);
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
SupplyA := ADCToSupplyCurrent(
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
FNetwork.Device.BoardType);
end;
FwdW := ADCToWatts100(FwdPwr);
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
// SWR = (1+ρ)/(1-ρ), ρ = adc_r/adc_f; только при значимой fwd-мощности.
if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then
begin
Rho := RevPwr / FwdPwr;
if Rho >= 0.99 then
SWRV := SWR_CAP
else
begin
SWRV := (1.0 + Rho) / (1.0 - Rho);
if SWRV > SWR_CAP then SWRV := SWR_CAP;
if SWRV < 1.0 then SWRV := 1.0;
end;
end else
SWRV := 1.0;
if not IsTx then FwdW := 0;
FHPpFwdW := FwdW;
FHPpSWR := SWRV;
FHPpSupplyV := SupplyV;
FHPpSupplyA := SupplyA;
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload;
Invoke(ApplyHPStatus);
end;
procedure TRadioController.ApplyHPStatus;
// Маршалится из OnHPStatus. PEP-style баллистика (мгновенная атака, медленный
// спад) для FwdW/SWR; EMA для supply. + overload-событие + HWPTT-фронт → SetMOX.
const
DECAY_FWD = 0.02;
DECAY_SWR = 0.015;
ALPHA_SUPPLY = 0.05;
begin
if FHPpFwdW <= 0 then
FLastFwdW := 0
else if FHPpFwdW > FLastFwdW then
FLastFwdW := FHPpFwdW // attack: instant
else
FLastFwdW := DECAY_FWD * FHPpFwdW + (1.0 - DECAY_FWD) * FLastFwdW;
if FHPpSWR <= 1.0 then
FLastSWR := 1.0
else if FHPpSWR > FLastSWR then
FLastSWR := FHPpSWR // attack: instant
else
FLastSWR := DECAY_SWR * FHPpSWR + (1.0 - DECAY_SWR) * FLastSWR;
if FHPpSupplyV >= 0 then
begin
if FLastSupplyV < 0 then
FLastSupplyV := FHPpSupplyV // первое значение — без сглаживания
else
FLastSupplyV := ALPHA_SUPPLY * FHPpSupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV;
end else
FLastSupplyV := -1.0;
if FHPpSupplyA >= 0 then
begin
if FLastSupplyA < 0 then
FLastSupplyA := FHPpSupplyA
else
FLastSupplyA := ALPHA_SUPPLY * FHPpSupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA;
end else
FLastSupplyA := -1.0;
FLastPLLLock := FHPpPLL;
// ADC overload — индикатор рисует UI по rfADCOverload (только на изменении).
if FADCOverloadActive <> (FHPpOverload <> 0) then
begin
FADCOverloadActive := FHPpOverload <> 0;
Changed(rfADCOverload);
end;
// Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить
// короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если
// его включил именно HWPTT.
if FHPpHWPTT <> FHWPTTActive then
begin
FHWPTTActive := FHPpHWPTT;
if not FTuning then
begin
if FHPpHWPTT then
begin
if not FTransmitting then
begin
SetMOX(True);
FHWPTTStartedTX := True;
end;
end
else
begin
if FHWPTTStartedTX then
begin
FHWPTTStartedTX := False;
SetMOX(False);
end;
end;
end;
end;
end;
procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
Count: Integer);
// DSP-поток: маршрут RX-аудио. Встроенный динамик трансивера (DDC Audio,
// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио
// (активный клиент); если перехвачено — в локальный AudioOut не пишем.
begin
if FSendAudioToRadio then
FNetwork.SendSpeakerAudio(Left, Right, Count);
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
Exit;
FAudioOut.Write(Left, Right, Count);
end;
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
Count: Integer);
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
// (UI рисует FSpecView). Децимации у спектра нет.
var
i, N: Integer;
begin
N := Count;
if N > Length(FSpectrumBuf) then N := Length(FSpectrumBuf);
for i := 0 to N - 1 do
FSpectrumBuf[i] := Pixels[i];
FSpectrumBufCount := N;
if Assigned(FOnSpectrumData) then FOnSpectrumData(Pixels, Count);
end;
procedure TRadioController.OnWaterfallReady(const Pixels: array of Single;
Count: Integer);
// DSP-поток: web-зеркало получает КАЖДЫЙ кадр; децимация рендера водопада —
// забота фронтенда (OnWaterfallData).
var
i, N: Integer;
begin
N := Count;
if N > Length(FWaterfallBuf) then N := Length(FWaterfallBuf);
for i := 0 to N - 1 do
FWaterfallBuf[i] := Pixels[i];
FWaterfallBufCount := N;
if Assigned(FOnWaterfallData) then FOnWaterfallData(Pixels, Count);
end;
procedure TRadioController.OnMicPacket(const Data: TMicDataPacket);
// HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы
// игнорируются (mic берётся со звуковой карты).
begin
if FTransmitting and FWDSPReady and
(FDSPEngine.TXMicSource = txmsRadio) then
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
end;
function TRadioController.PullSoundCardMic(MaxN: Integer): Integer;
// Колбэк TTXDSPThread при TXMicSource=txmsSoundCard. Тянем только реально
// доступные сэмплы из FAudioIn (иначе пушили бы тишину в mic-цепь WDSP),
// пачкой в локальный буфер — экономим будилки семафора.
var
i, n: Integer;
Buf: array[0..2047] of Double;
begin
Result := 0;
if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit;
n := FAudioIn.Available;
if n > MaxN then n := MaxN;
if n > Length(Buf) then n := Length(Buf);
for i := 0 to n - 1 do
Buf[i] := FAudioIn.ReadSample;
if n > 0 then
FDSPEngine.PushTXMicSamplesD(Buf, n);
Result := n;
end;
procedure TRadioController.Invoke(M: TThreadMethod);
begin
if Assigned(FOnInvoke) then FOnInvoke(M)
else if Assigned(M) then M(); // нет хозяина-маршалера — прямой вызов
end;
function TRadioController.GetSnapshot: TRadioSnapshot;
begin
FillChar(Result, SizeOf(Result), 0);
Result.VfoA := FVfoA; Result.VfoB := FVfoB;
Result.CenterFreq := FCenterFreq; Result.SpanHz := FSpanHz;
Result.SampleRate := FSampleRate; Result.ActiveVfo := FActiveVfo;
Result.Mode := FMode; Result.Filter := FFilter; Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop;
Result.RxGainMode := FRxGainMode; Result.RxGainDb := FRxGainDb;
Result.Volume := FVolume; Result.Drive := FDrivePercent; Result.Atten := FAtten;
Result.Band := FCurrentBand; Result.Xvtr := FCurrentXvtr;
Result.Muted := FMuted; Result.CTun := FCTun; Result.Duplex := FDisplayDuplex;
Result.Running := FRunning; Result.Connected := FDevConnected;
Result.Transmitting := FTransmitting; Result.Tuning := FTuning;
Result.NRMode := FNRMode; Result.NBMode := FNBMode;
Result.SNB := FSNB; Result.ANF := FANF;
Result.WfAGC := FWfAGCEnabled; Result.WfNF := FWfNFEnabled;
Result.FMStepOn := FFMStepOn; Result.FMStepIdx := FFMStepIdx;
Result.FMCTCSSOn := FFMCTCSSOn; Result.FMCTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMSQOn := FFMSQOn; Result.FMSQLevel := FFMSQLevel;
Result.FMRptDir := FFMRptDir;
Result.SMeter := FLastSMeter; Result.FwdW := FLastFwdW; Result.SWR := FLastSWR;
Result.SupplyV := FLastSupplyV; Result.SupplyA := FLastSupplyA;
Result.PLLLock := FLastPLLLock;
end;
function TRadioController.XvtrTranslate(VisibleHz: Double): Double;
begin
Result := VisibleHz;
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
// f_IF = f_visible - LOOffset + LOError.
// QO-100 beacon lock складывает свою коррекцию дрейфа LNB ПРЯМО в LOError
// (живая калибровка): она сразу видна в настройках и сохраняется. Отдельного
// слагаемого нет — иначе двойной счёт.
Result := VisibleHz
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
end;
function TRadioController.ActiveTXFreqHz: Double;
begin
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
// за активным RX (TX-A горит, но передача шла на B при активном B).
if FSplitTxB then Result := FVfoB
else Result := FVfoA;
if FMode = MODE_FM then
case FFMRptDir of
RPT_MINUS: Result := Result - FFMRptOffsetHz;
RPT_PLUS: Result := Result + FFMRptOffsetHz;
end;
end;
// ---------------------------------------------------------------------------
// QO-100 beacon lock
// ---------------------------------------------------------------------------
const
// --- контур коррекции ---
BEACON_GAIN = 0.9; // гейн контура: feed-forward делает шаг точным,
// <1 — запас от перелёта при шумной оценке центроида
BEACON_SEED_GAIN = 0.95; // первая коррекция по клику агрессивнее (1 клик → лок)
BEACON_DEADBAND_HZ = 75.0; // внутри — коррекции стоп (без вечного подкручивания)
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка FFT-пайплайна
BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц)
// --- метрика лока (prominence = пик над шумовым полом окна) ---
BEACON_LOCK_PROM_DB = 6.0; // загорается LOCK выше этого
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: гаснет ниже этого
// --- окна поиска/слежения ---
BEACON_SEARCH_HZ = 6000.0; // ШИРОКОЕ окно захвата (клик/включение/потеря лока)
BEACON_TRACK_HZ = 2000.0; // УЗКИЙ gate слежения: сосед дальше — физически вне окна
BEACON_MAX_SLEW_HZ = 500.0; // дрейф LNB ≪ этого/тик; больший скачок = чужой сигнал → отвергаем
BEACON_TRACK_ALPHA = 0.25; // сглаживание позиции центроида за тик
// --- форма лепестка BPSK-маяка (узнаём маяк, а не «самый громкий пик») ---
BEACON_LOBE_DB = 10.0; // лепесток = пиксели в пределах этого ниже пика
BEACON_LOBE_MIN_HZ = 700.0; // уже → CW-несущая, не BPSK-маяк
BEACON_LOBE_MAX_HZ = 5000.0; // шире → слились два сигнала
BEACON_SYM_MAX = 0.70; // |LR|/(L+R) выше → односторонний сосед, не маяк
BEACON_SHAPE_MAX_HZPP = 600.0; // форму лепестка проверяем лишь если бин ≤ этого
function TRadioController.MeasureBeaconFFT(ExpectOff, WinHz: Double;
out PeakOff, PromDB: Double): Boolean;
// Маяк по display-FFT в окне ±WinHz вокруг ExpectOff (baseband offset; центр =
// пиксель n/2). Центральный маяк — BPSK с ПОДАВЛЕННОЙ несущей: в спектре не
// линия, а широкая симметричная «полка» (~2.4 кГц), peak-finder по ней скачет.
// Позицию берём как POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах
// BEACON_LOBE_DB от пика) — центр симметричной полки = несущая, стабилен.
// PromDB — пик над ШУМОВЫМ ПОЛОМ окна (бины ниже лепестка), не над общим средним:
// иначе на узком gate сам лепесток поднимал бы «средний» и занижал метрику.
// Проверка ФОРМЫ (ширина + симметрия) отсеивает CW-несущую и одностороннего
// соседа: False → лепесток не похож на маяк, вызывающий держит позицию.
var
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
n, i, best, cnt, noiseCnt: Integer;
hzPerPix, lo, hi, off, thr, p, wsum, fsum, noiseSum: Double;
minOff, maxOff, wLeft, wRight, widthHz, floorDb: Double;
bestv: Single;
begin
Result := False; PeakOff := ExpectOff; PromDB := -99.0;
if not FWDSPReady or not Assigned(FDSPEngine) then Exit;
n := 0;
FDSPEngine.GetSpectrumData(pix, n);
if n < 8 then Exit;
hzPerPix := FSpanHz / n;
lo := ExpectOff - WinHz;
hi := ExpectOff + WinHz;
// Проход A — пик в окне.
best := -1; bestv := -1e30; cnt := 0;
for i := 0 to n - 1 do
begin
off := (i - n / 2) * hzPerPix;
if (off < lo) or (off > hi) then Continue;
Inc(cnt);
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
end;
if (best < 0) or (cnt < 4) then Exit;
// Проход B — центроид лепестка + протяжённость + шумовой пол (бины вне лепестка).
thr := bestv - BEACON_LOBE_DB;
wsum := 0; fsum := 0; minOff := 1e30; maxOff := -1e30;
noiseSum := 0; noiseCnt := 0;
for i := 0 to n - 1 do
begin
off := (i - n / 2) * hzPerPix;
if (off < lo) or (off > hi) then Continue;
if pix[i] >= thr then
begin
p := Power(10.0, (pix[i] - thr) / 10.0); // линейная мощность над порогом
wsum := wsum + p;
fsum := fsum + p * off;
if off < minOff then minOff := off;
if off > maxOff then maxOff := off;
end
else
begin
noiseSum := noiseSum + pix[i]; Inc(noiseCnt);
end;
end;
if wsum <= 0 then Exit;
PeakOff := fsum / wsum;
if noiseCnt > 0 then floorDb := noiseSum / noiseCnt
else floorDb := bestv - BEACON_LOBE_DB; // весь gate занят лепестком — fallback
PromDB := bestv - floorDb;
// Форма лепестка — оцениваем ТОЛЬКО при достаточном разрешении (лепесток
// разрешается в несколько бинов). На крупном бине (широкий span) форму не
// отличить от CW → пропускаем проверку, полагаемся на gate+slew в контуре.
if hzPerPix > BEACON_SHAPE_MAX_HZPP then begin Result := True; Exit; end;
// Ширина лепестка (BPSK ~2.4 кГц). Узкий = CW-несущая, широкий = слились два.
widthHz := maxOff - minOff;
if (widthHz < BEACON_LOBE_MIN_HZ) or (widthHz > BEACON_LOBE_MAX_HZ) then Exit;
// Симметрия мощности вокруг центроида — односторонний сосед в окне ломает её.
wLeft := 0; wRight := 0;
for i := 0 to n - 1 do
begin
off := (i - n / 2) * hzPerPix;
if (off < lo) or (off > hi) then Continue;
if pix[i] < thr then Continue;
p := Power(10.0, (pix[i] - thr) / 10.0);
if off < PeakOff then wLeft := wLeft + p else wRight := wRight + p;
end;
if (wLeft + wRight) > 0 then
if Abs(wLeft - wRight) / (wLeft + wRight) > BEACON_SYM_MAX then Exit;
Result := True;
end;
procedure TRadioController.SetBeaconRefHz(Hz: Double);
begin
FBeaconRefHz := Hz;
end;
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик,
// без автопоиска, который в забитой полосе хватает соседей). Так как клик —
// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик
// ставит маяк на опорную за одну перестройку, без многократного «тыкания».
var clickOff, refined, prom, e, dLock: Double;
begin
if not FBeaconLockOn then SetBeaconLock(True);
FBeaconManualSeed := True;
clickOff := DisplayHz - FCenterFreq;
if MeasureBeaconFFT(clickOff, BEACON_SEARCH_HZ, refined, prom) then
begin
FBeaconTrackHz := FCenterFreq + refined;
FBeaconPromDB := prom;
FBeaconLocked := prom >= BEACON_LOCK_PROM_DB;
// deadbeat-коррекция + feed-forward: маяк уедет на −dLock, окно ведём туда же.
e := FBeaconTrackHz - FBeaconRefHz;
dLock := BEACON_SEED_GAIN * e;
ApplyBeaconLOError(dLock); // складываем в LOError + сохраняем
FBeaconTrackHz := FBeaconTrackHz - dLock; // ≈ FBeaconRefHz
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать FFT отразить новый LO
end
else
begin
// маяк под кликом не распознан — просто ставим окно туда, ждём захвата.
FBeaconTrackHz := DisplayHz;
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
end;
end;
procedure TRadioController.SetBeaconLock(On_: Boolean);
var refined, prom: Double;
begin
if On_ = FBeaconLockOn then Exit;
FBeaconLockOn := On_;
if On_ then
begin
FBeaconCorrCooldown := 0;
FBeaconSaveCooldown := 0;
FBeaconManualSeed := False; // авто до первого клика пользователя
FBeaconLocked := False;
FBeaconPromDB := 0.0;
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError уже несёт калибровку)
// Начальная область — у опорной частоты (уточняем пик, если виден).
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, BEACON_SEARCH_HZ, refined, prom) then
FBeaconTrackHz := FCenterFreq + refined
else
FBeaconTrackHz := FBeaconRefHz;
end
else
PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод)
Changed(rfBeaconLock);
end;
procedure TRadioController.ServiceBeaconLock;
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Меряет маяк по
// display-FFT и (после ручного клика) трим LOError так, чтобы маяк стоял на
// опорной частоте → весь downlink стабилен. Коррекция складывается в LOError
// (живая калибровка LNB: видна в настройках, сохраняется).
//
// Устойчивость к соседним сигналам (как в SDR Console):
// • при локе окно УЗКОЕ (±BEACON_TRACK_HZ) — далёкий сосед вне окна;
// • slew-отбраковка: дрейф LNB медленный, резкий скачок центроида = чужой
// сигнал, измерение игнорируем;
// • MeasureBeaconFFT проверяет форму лепестка (узнаём маяк, не «громкий пик»).
// Быстрая сходимость при большой ошибке:
// • feed-forward (FBeaconTrackHz = dLock) — окно едет за маяком сквозь ретюн,
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
var win, pk, prom, cand, e, dLock: Double;
begin
if not FBeaconLockOn then Exit;
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
// Окно — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband через
// актуальный FCenterFreq (сдвиг водопада/центра окно не ломает). Узкое при локе.
if FBeaconLocked then win := BEACON_TRACK_HZ else win := BEACON_SEARCH_HZ;
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, win, pk, prom) then
begin
Changed(rfBeaconLock); // лепесток не виден/не маяк — держим позицию и лок
Exit;
end;
cand := FCenterFreq + pk;
// Slew-отбраковка (только при локе/узком окне): скачок больше дрейфа LNB =
// чужой сигнал в окне → измерение игнорируем, остаёмся на маяке.
if FBeaconLocked and (Abs(cand - FBeaconTrackHz) > BEACON_MAX_SLEW_HZ) then
begin
Changed(rfBeaconLock);
Exit;
end;
// Сопровождение центроида. Во время settle НЕ двигаем — FFT ещё отражает
// старый LO (доверяем feed-forward-предсказанию позиции).
if FBeaconCorrCooldown = 0 then
FBeaconTrackHz := FBeaconTrackHz + BEACON_TRACK_ALPHA * (cand - FBeaconTrackHz);
FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB);
// Гистерезис лока: загорается при LOCK_PROM, гаснет ниже UNLOCK_PROM (без мерцания).
if FBeaconPromDB >= BEACON_LOCK_PROM_DB then FBeaconLocked := True
else if FBeaconPromDB < BEACON_UNLOCK_PROM_DB then FBeaconLocked := False;
// Коррекция: НЕ каждый тик (display-FFT отстаёт от ретюна → перелёт). После
// коррекции ждём BEACON_SETTLE_TICKS. Только после ручного клика (manual seed).
if FBeaconCorrCooldown > 0 then
Dec(FBeaconCorrCooldown)
else if FBeaconLocked and FBeaconManualSeed then
begin
// e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = 1, поэтому
// LOError += e тянет маяк к опорной; feed-forward предсказывает новую
// display-позицию (track = dLock), чтобы окно ехало за маяком.
e := FBeaconTrackHz - FBeaconRefHz;
if Abs(e) > BEACON_DEADBAND_HZ then
begin
dLock := BEACON_GAIN * e;
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
FBeaconTrackHz := FBeaconTrackHz - dLock;
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
end;
end;
Changed(rfBeaconLock);
end;
procedure TRadioController.ApplyBeaconLOError(dLock: Double);
// Складывает коррекцию маяка ПРЯМО в LOError активного трансвертера — это и есть
// живая калибровка дрейфа LNB: сразу видна в настройках, переживает рестарт.
// FBeaconLockHz — лишь сессионный аккумулятор для индикации. Запись на диск
// троттлим (BEACON_SAVE_TICKS): коррекции редки (deadband), но при долгом прогреве
// LNB их много — не молотим диск каждую.
begin
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
FXvtrSettings.Entries[FCurrentXvtr].LOError :=
FXvtrSettings.Entries[FCurrentXvtr].LOError + dLock;
FBeaconLockHz := FBeaconLockHz + dLock; // сколько накрутили за сессию (индикация)
PushNetworkState; // ретюн: XvtrTranslate читает новый LOError
if (FBeaconSaveCooldown = 0) and FDevConnected then
begin
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
FBeaconSaveCooldown := BEACON_SAVE_TICKS;
end;
end;
procedure TRadioController.PersistBeaconCalibration;
// Коррекция уже живёт в LOError (ApplyBeaconLOError). Здесь — финальный flush на
// диск при выключении лока, на случай неотправленной из-за троттлинга.
begin
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
if FDevConnected then FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
FBeaconSaveCooldown := 0;
end;
function TRadioController.BeaconState: TBeaconState;
begin
if not FBeaconLockOn then Exit(bsIdle);
if FBeaconLocked then Result := bsLock else Result := bsSearch;
end;
function TRadioController.BeaconCorrectionHz: Double;
begin
Result := FBeaconLockHz;
end;
function TRadioController.BeaconSNRdB: Double;
begin
Result := FBeaconPromDB; // «высота» пика над шумом окна (dB)
end;
function TRadioController.BeaconLockEnabled: Boolean;
begin
Result := FBeaconLockOn;
end;
function TRadioController.BeaconRefHz: Double;
begin
Result := FBeaconRefHz;
end;
function TRadioController.BeaconTrackedFreqHz: Double;
begin
Result := FBeaconTrackHz;
end;
procedure TRadioController.ApplyModeFilter;
var
Lo, Hi, Half: Integer;
begin
Half := FFilterBW div 2;
case FMode of
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
1: begin Lo := 100; Hi := FFilterBW; end; // USB
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
end;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(Lo, Hi);
if FMode = MODE_FM then
begin
FDSPEngine.SetRXFMDeviation(FFMDeviation);
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
end
else
FDSPEngine.SetFMSquelch(False, 0);
end;
end;
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
procedure TRadioController.ApplyModeDefaults;
begin
case FMode of
MODE_FM:
begin
if FFilter > 1 then FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
0, 1: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
2: begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
3, 4: begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end;
end;
end;
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
begin
case M of
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, 1)];
0, 1: Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
2: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
3, 4: Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
end;
end;
// =====================================================================
// Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
// =====================================================================
function TRadioController.CalcDriveByte: Byte;
// Thetis-совместимая формула:
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
var
Cal: Double;
Pos: Integer;
XvtrActive: Boolean;
pwr_W: Double;
target_dBm: Double;
target_volts: Double;
audio_vol: Double;
begin
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
if XvtrActive then
Cal := FVHFBandCal[FCurrentXvtr]
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 38.8;
if FTuning then
begin
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100)
else
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
end
else
begin
if XvtrActive then
Pos := Round(FDrivePercent * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
else
Pos := FDrivePercent;
end;
if Pos <= 0 then begin Result := 0; Exit; end;
pwr_W := Pos / 100.0 * FPAMaxPower;
target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal;
target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05);
audio_vol := Min(target_volts / 0.8, 1.0);
Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
end;
procedure TRadioController.SanitizeFilterForMode;
// Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и
// синхронизируем FFilterBW/FFMDeviation. Чистая (state) часть UpdateFilterButtons.
begin
if FMode = MODE_FM then
begin
if FFilter > 1 then FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
end;
function TRadioController.MakeBandSettings: TBandSettings;
begin
Result.VfoA := FVfoA;
Result.VfoB := FVfoB;
Result.Mode := FMode;
Result.FilterIdx := FFilter;
Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode;
Result.AGCTop := FAGCTop;
Result.CTun := FCTun;
Result.CenterHz := FCenterFreq; // DDC-центр → восстановим offset CTun при загрузке
Result.SpanHz := FSpanHz;
Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel;
Result.CTCSSOn := FFMCTCSSOn;
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMStepOn := FFMStepOn;
Result.FMStepIdx := FFMStepIdx;
Result.FMRptDir := FFMRptDir;
Result.FMRptOffsetHz := FFMRptOffsetHz;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
procedure TRadioController.SaveCurrentBand;
// Сохраняет текущее состояние диапазона. В XVTR-режиме пишем в XVTR-слот
// (HF band-cache не трогаем — там сохранён HF-диапазон до активации XVTR).
var
i: Integer;
B: TBandSettings;
begin
if not FDevConnected then Exit;
if FCurrentXvtr >= 0 then
begin
if (FCurrentXvtr < CFG_XVTR_COUNT) then
begin
i := FCurrentXvtr;
FXvtrSettings.Entries[i].LastFreq := FVfoA;
FXvtrSettings.Entries[i].LastFreqB := FVfoB;
FXvtrSettings.Entries[i].LastMode := FMode;
FXvtrSettings.Entries[i].LastFilterIdx := FFilter;
FXvtrSettings.Entries[i].LastFMSQOn := FFMSQOn;
FXvtrSettings.Entries[i].LastFMSQLevel := FFMSQLevel;
FXvtrSettings.Entries[i].LastCTCSSOn := FFMCTCSSOn;
FXvtrSettings.Entries[i].LastCTCSSToneIdx := FFMCTCSSToneIdx;
FXvtrSettings.Entries[i].LastFMStepOn := FFMStepOn;
FXvtrSettings.Entries[i].LastFMStepIdx := FFMStepIdx;
FXvtrSettings.Entries[i].LastFMRptDir := FFMRptDir;
FXvtrSettings.Entries[i].LastFMRptOffsetHz := FFMRptOffsetHz;
FXvtrSettings.Entries[i].LastCTun := FCTun;
FXvtrSettings.Entries[i].LastCenterHz := FCenterFreq;
FXvtrSettings.Entries[i].LastAGCMode := FAGCMode;
FXvtrSettings.Entries[i].LastAGCTop := FAGCTop;
FXvtrSettings.Entries[i].LastCTCSSAutoActive := FFMCTCSSAutoActive;
FXvtrSettings.Entries[i].LastFMRptAutoActive := FFMRptAutoActive;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
end;
// HPSDR: приёмник только HF — если VFO ушёл в VHF/UHF (напр. канал на 2м без
// XVTR), не сохраняем, иначе HF-кэш диапазона перезапишется VHF-частотой.
// Pluto: VHF/UHF — нативные диапазоны (VU-план), сохранять обязательно;
// от перезаписи чужим диапазоном защищает FreqToBandIdx-проверка ниже.
if (not IsPluto) and (FVfoA > 61000000) then Exit;
B := MakeBandSettings;
// Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш
// ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая
// частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда.
if FreqToBandIdx(FVfoA, IsPluto) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA;
if FreqToBandIdx(FVfoB, IsPluto) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB;
FBandCache[FCurrentBand] := B;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end;
function TRadioController.BuildGlobalSettings: TGlobalSettings;
// Радио-поля — из живого состояния; тема/FPS/digits/CAT — сквозь из FLoadedGlobal
// (контроллер их не трактует). Сидируем загруженным блобом, поверх — радио.
var i: Integer;
begin
Result := FLoadedGlobal; // несёт не-радио поля (тема/FPS/digits/CAT) round-trip
Result.Volume := FVolume;
Result.DriveLevel := FDrivePercent;
Result.PAMaxPower := FPAMaxPower;
Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb;
for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := FNRMode;
Result.NBMode := FNBMode;
Result.SNBEnabled := FSNB;
Result.ANFEnabled := FANF;
Result.AGCSlope := 0;
Result.AGCHangThreshold := 100;
Result.WfAGCEnabled := FWfAGCEnabled;
Result.WfNFEnabled := FWfNFEnabled;
Result.DitherEnabled := FDitherEnabled;
Result.RandomEnabled := FRandomEnabled;
Result.LastBand := FCurrentBand;
Result.LastXvtr := FCurrentXvtr;
Result.SampleRate := FSampleRate;
Result.RxGainMode := FRxGainMode;
Result.RxGainDb := FRxGainDb;
if FWDSPReady then
begin
Result.FFTSize := FDSPEngine.FFTSize;
Result.WindowType := FDSPEngine.WindowType;
Result.SpecDetector := FDSPEngine.SpecDetector;
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
Result.WfDetector := FDSPEngine.WfDetector;
Result.WfAvgMode := FDSPEngine.WfAvgMode;
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
end
else
begin
Result.FFTSize := 131072;
Result.WindowType := 2;
Result.SpecDetector := 0;
Result.SpecAvgMode := 3;
Result.SpecAvgTimeMS := 30.0;
Result.WfDetector := 0;
Result.WfAvgMode := 3;
Result.WfAvgTimeMS := 120.0;
end;
Result.WfManualHigh := FWfManualHigh;
Result.WfManualLow := FWfManualLow;
Result.WfAGCOffset := FWfAGCOffset;
Result.SpecRefLevel := FSpecRefLevel;
Result.SpecRange := FSpecRange;
Result.SpecGridStep := FSpecGridStep;
Result.AudioSampleRate := FAudioOut.SampleRate;
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.ShowWideband := FShowWideband;
Result.WidebandFill := FWidebandFill;
Result.SpectrumFill := FSpectrumFill;
Result.DisplayDuplex := FDisplayDuplex;
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
Result.SendAudioToRadio := FSendAudioToRadio;
end;
procedure TRadioController.SaveGlobalSettings;
begin
if FDevConnected then FSettings.SaveGlobal(FDevMAC, BuildGlobalSettings);
end;
procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
begin
// --- Режим ---
FMode := B.Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
FFilter := B.FilterIdx;
FFilterBW := B.FilterBW;
SanitizeFilterForMode;
ApplyModeFilter;
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
FAGCMode := B.AGCMode;
FAGCTop := B.AGCTop;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine;
// --- FM squelch / CTCSS / step / repeater ---
FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel;
FFMCTCSSOn := B.CTCSSOn;
FFMCTCSSToneIdx := B.CTCSSToneIdx;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
end;
FFMStepOn := B.FMStepOn;
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
FFMRptDir := B.FMRptDir;
FFMRptOffsetHz := B.FMRptOffsetHz;
if FFMRptOffsetHz <= 0 then FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA);
// --- CTUN ---
FCTun := B.CTun;
if FWDSPReady and Assigned(FDSPEngine) and not FCTun then FDSPEngine.SetShift(0.0);
// --- Span (sample rate глобальный, не per-band — лишь ресинк) ---
FSpanHz := FSampleRate;
// --- Рендер: единичные события (UI рисует через OnControllerState) ---
Changed(rfBand);
Changed(rfMode); // rfMode-рендер зовёт UpdateFilterButtons (FM-панели)
Changed(rfFilter);
Changed(rfAGCMode);
Changed(rfAGCTop);
Changed(rfFMSQ);
Changed(rfFMSQLevel);
Changed(rfFMCTCSS);
Changed(rfFMCTCSSTone);
Changed(rfFMStep); // кнопка вкл/выкл STEP
Changed(rfFMStepIdx); // дропдаун шага + web + grid
Changed(rfFMRpt);
Changed(rfCTun);
end;
procedure TRadioController.RestoreBand(BandIdx: Integer);
// Загружает состояние диапазона из кэша, применяет к DSP, делает финальную
// перестройку приёмника на активный VFO и шлёт события рендера. Полная команда —
// зовётся напрямую desktop/web/channel/connect-restore.
var B: TBandSettings;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
Changed(rfBandRestore); // явная смена диапазона → UI сбросит Wf-avg буфер
B := FBandCache[BandIdx];
FCurrentBand := BandIdx;
// Pluto: кэш диапазона может нести HF-частоту (дефолты Settings). Если VfoA не
// попадает в VHF-слот — берём частоту по умолчанию из плана.
if IsPluto and (FreqToBandIdx(B.VfoA, True) <> BandIdx) then
begin
B.VfoA := BandDefaultFreq(BandIdx, True);
B.VfoB := B.VfoA;
end;
ApplyBandDSP(B);
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
// текущий span — иначе (сменился sample rate / битый конфиг) центрируем на VFO.
// ApplyTuneCore в SetVfoA при необходимости сам прокрутит центр к краю.
if FCTun and (B.CenterHz > 0) and (Abs(B.VfoA - B.CenterHz) < FSpanHz / 2) then
FCenterFreq := B.CenterHz
else
FCenterFreq := B.VfoA;
FVfoB := B.VfoB;
FVfoA := B.VfoA;
FDriveLevel := CalcDriveByte;
Changed(rfVfoB);
// Финальная перестройка приёмника на АКТИВНЫЙ VFO. VFO A — через SetVfoA
// (хук OnAfterTune несёт канальную оркестрацию); VFO B — через SetVfoB.
// Иначе смена диапазона при активном B центрировала бы приёмник на A.
if FActiveVfo = 0 then SetVfoA(FVfoA)
else SetVfoB(FVfoB);
end;
function TRadioController.ActiveVfoHz: Double;
begin
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
end;
procedure TRadioController.ApplyTuneCore(VfoHz: Double);
// Общее ядро перестройки приёмника на частоту АКТИВНОГО VFO (A или B).
// CTUN OFF: DDC = VfoHz, сдвиг 0.
// CTUN ON: DDC фиксирован, демодулятор сдвигаем на VfoHz-FCenterFreq;
// если полоса фильтра вышла за край дисплея — прокручиваем центр.
// Ставит FCenterFreq / shift / FLastVfoScrolled.
var
Offset, FiltLo, FiltHi, HalfSpan: Double;
begin
FLastVfoScrolled := False;
if not FCTun then
begin
FCenterFreq := VfoHz;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
end
else
begin
Offset := VfoHz - FCenterFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
HalfSpan := FSpanHz / 2;
case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
begin
if FiltHi > HalfSpan then
FCenterFreq := VfoHz - HalfSpan * 0.5
else
FCenterFreq := VfoHz + HalfSpan * 0.5;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(VfoHz - FCenterFreq);
FLastVfoScrolled := True;
end;
end;
end;
procedure TRadioController.SetVfoA(Hz: Double);
// Перестройка VFO A: XVTR-клэмп, CTUN ядро, детект диапазона, FM авто-RPT,
// проталкивание в сеть. Рендер — в UI через OnControllerState(rfVfoA/rfBand).
// Канальную оркестрацию (авто-CTCSS, pre-channel drive) делает UI-путь.
var
NewFreq: Int64;
BandIdx: Integer;
begin
NewFreq := Round(Hz);
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
end;
FVfoA := NewFreq;
ApplyTuneCore(FVfoA);
// Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band,
// FreqToBandIdx вернёт -1 для VHF/UHF).
if FCurrentXvtr < 0 then
begin
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
FCurrentBand := BandIdx;
FDriveLevel := CalcDriveByte;
Changed(rfBand);
end;
end;
// Авто-MINUS в 2 м репитерном сегменте (само снимает при уходе из сегмента).
if FMode = MODE_FM then UpdateRptAutoState;
PushNetworkState;
Changed(rfVfoA);
// Канальная оркестрация — ПОСЛЕ рендера rfVfoA (вне его try/finally-гарда), без
// ре-ентранси: OnVfoTuned зовёт команды (SetDrive/SetFMRpt), которым нужен
// чистый верхний уровень OnControllerState.
if Assigned(FOnAfterTune) then FOnAfterTune;
end;
procedure TRadioController.StoreVfoA(Hz: Double);
// Сохранить частоту VFO A БЕЗ перестройки приёмника (когда активен B — слушаем B,
// A только хранится/отображается). Симметрично «неактивной» ветке SetVfoB.
// Рендер FreqDispA — через rfVfoA (спектр он не трогает, т.к. SyncSpecViewFreq
// держит активный VFO B); смена band-кнопки — через rfBand.
var BandIdx: Integer;
begin
FVfoA := Hz;
if FCurrentXvtr < 0 then
begin
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
FCurrentBand := BandIdx;
FDriveLevel := CalcDriveByte;
Changed(rfBand);
end;
end;
Changed(rfVfoA);
end;
procedure TRadioController.SetVfoB(Hz: Double);
// VFO B всегда сохраняется. Если B активен — перестраиваем приёмник через
// ApplyTuneCore (с учётом CTUN), детектим диапазон и толкаем в сеть. Если B
// неактивен (split, слушаем A) — только сохраняем частоту (для TX).
// Рендер FreqDispB/спектра — в UI через OnControllerState(rfVfoB).
var
BandIdx: Integer;
NewFreq: Int64;
begin
NewFreq := Round(Hz);
// Клэмп в пределах XVTR-band (как в SetVfoA) — VFO B тоже не уходит за край.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
end;
FVfoB := NewFreq;
if FActiveVfo = 1 then
begin
ApplyTuneCore(FVfoB);
if FCurrentXvtr < 0 then
begin
BandIdx := FreqToBandIdx(FVfoB, IsPluto);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
FCurrentBand := BandIdx;
FDriveLevel := CalcDriveByte;
Changed(rfBand);
end;
end;
PushNetworkState;
end;
Changed(rfVfoB);
end;
procedure TRadioController.SetActiveVfo(Idx: Integer);
// Переключение активного (RX) VFO с перестройкой приёмника на его частоту.
// Bandstack: если активируемый VFO стоит на ДРУГОМ ham-бэнде — сохраняем
// покидаемый бэнд и восстанавливаем DSP-настройки нового (mode/filter/AGC/CTUN/
// FM). Частоты обоих VFO при этом НЕ трогаем — каждый VFO несёт свой диапазон.
// CTUN (того же бэнда): если частота нового VFO видна в окне — только shift
// (DDC/сеть не трогаем); иначе CTUN сбрасывается и центрируемся на частоте.
// Рендер (дисплеи/спектр/band/CTUN-кнопка) — в UI через OnControllerState.
var
ActiveFreq: Double;
HalfSpan, Offset: Double;
FreqVisible, NeedPush: Boolean;
NewBand: Integer;
begin
FActiveVfo := Idx;
ActiveFreq := ActiveVfoHz;
NeedPush := False;
// --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд ---
if FCurrentXvtr < 0 then
begin
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin
SaveCurrentBand; // сохранить покидаемый бэнд
FCurrentBand := NewBand;
ApplyBandDSP(FBandCache[NewBand]); // mode/filter/AGC/CTUN/FM + рендер
FCenterFreq := ActiveFreq; // центр на активную (CTUN центрируется тут)
FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда
NeedPush := True; // DDC-центр сменился → толкнуть в сеть
end;
end;
if FCTun then
begin
HalfSpan := FSpanHz / 2;
Offset := ActiveFreq - FCenterFreq;
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
if FreqVisible then
begin
// Частота видна — просто сдвигаем демодулятор.
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
end
else
begin
// Частота за окном — сбрасываем CTUN и центрируемся на ней.
FCTun := False;
FBandCache[FCurrentBand].CTun := False;
FCenterFreq := ActiveFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
NeedPush := True;
Changed(rfCTun);
end;
end
else
begin
FCenterFreq := ActiveFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
NeedPush := True;
end;
if NeedPush then PushNetworkState;
Changed(rfActiveVfo);
end;
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
begin
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
else SetVfoB(FVfoB + DeltaHz);
end;
procedure TRadioController.SetCenter(Hz: Double);
// CTUN-скролл окна просмотра: двигаем DDC-центр БЕЗ смены VFO. Демодулятор
// сдвигаем = активный VFO − новый центр; полный HP-кадр в радио. Рендер
// (ruler/спектр) — в UI через OnControllerState(rfCenterFreq).
// Точки входа: десктопный CTUN-drag и web (SyncWebCenter).
begin
FCenterFreq := Hz;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
PushNetworkState;
Changed(rfCenterFreq);
end;
procedure TRadioController.VfoSwap;
// Меняем VFO A/B местами и перестраиваем приёмник на новую частоту активного
// VFO (центр=частота, shift 0 — CTUN-оффсет после свопа теряет смысл).
var
T: Double;
ActiveFreq: Double;
NewBand: Integer;
begin
T := FVfoA; FVfoA := FVfoB; FVfoB := T;
ActiveFreq := ActiveVfoHz;
// Bandstack: если активный после свопа VFO на другом ham-бэнде — сохраняем
// покидаемый бэнд и восстанавливаем DSP нового (mode/filter/AGC/CTUN/FM), как
// SetActiveVfo. Иначе своп на другой диапазон оставлял бы старые mode/фильтр/AGC
// при подсвеченной кнопке нового бэнда (рассинхрон).
if FCurrentXvtr < 0 then
begin
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin
SaveCurrentBand;
FCurrentBand := NewBand;
ApplyBandDSP(FBandCache[NewBand]); // несёт Changed(rfBand) + рендер DSP-полей
FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда
end;
end;
FCenterFreq := ActiveFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
PushNetworkState;
Changed(rfVfoA);
Changed(rfVfoB);
end;
procedure TRadioController.VfoACopyB; // A>B: частота A копируется в B
begin
SetVfoB(FVfoA); // сохраняет B:=A, перестраивает если B активен, рендер rfVfoB
end;
procedure TRadioController.VfoBCopyA; // B>A: частота B копируется в A
begin
FVfoA := FVfoB;
if FActiveVfo = 0 then SetVfoA(FVfoA) // A активен — перестраиваем приёмник
else Changed(rfVfoA); // A неактивен — только рендер дисплея
end;
procedure TRadioController.SetMode(M: Integer);
begin
FMode := M;
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
FBandCache[FCurrentBand].Mode := FMode;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetMode(FMode);
// Если TUN активен — пересчитываем знак частоты под новый режим
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
end;
ApplyModeFilter;
Changed(rfMode);
end;
procedure TRadioController.SetFilterIdx(Idx: Integer);
begin
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
// Idx может прийти из CAT/web без проверки; FM-таблицы всего на 2 элемента.
if FMode = MODE_FM then Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1)
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
FFilter := Idx;
case FMode of
0, 1: FFilterBW := FILT_SSB_BW[Idx];
2: FFilterBW := FILT_DSB_BW[Idx];
3, 4: FFilterBW := FILT_CW_BW[Idx];
MODE_FM:
begin
FFilterBW := FILT_FM_BW[Idx];
FFMDeviation := FILT_FM_DEV[Idx];
end;
else FFilterBW := FILT_AM_BW[Idx];
end;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
ApplyModeFilter;
Changed(rfFilter);
end;
procedure TRadioController.SetFilterBW(BWHz: Integer);
// Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а
// FFilter подбираем ближайшим пресетом — только для подсветки кнопки.
var
BW_arr: array[0..FILT_COUNT-1] of Integer;
best, dist, i: Integer;
begin
FFilterBW := BWHz;
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
case FMode of
0, 1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
3, 4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;
for i := 0 to FILT_COUNT-1 do
if Abs(BW_arr[i] - BWHz) < dist then
begin
dist := Abs(BW_arr[i] - BWHz);
best := i;
end;
FFilter := best;
if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best];
FBandCache[FCurrentBand].FilterBW := FFilterBW;
ApplyModeFilter;
Changed(rfFilter);
end;
procedure TRadioController.ApplyAGCToEngine;
const
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для
// всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
begin
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
FDSPEngine.SetAGC(AGCModes[FAGCMode], 50.0);
end;
procedure TRadioController.SetAGCMode(M: Integer);
begin
if (M < 0) or (M > 4) then Exit;
FAGCMode := M;
ApplyAGCToEngine;
FBandCache[FCurrentBand].AGCMode := FAGCMode;
Changed(rfAGCMode);
end;
procedure TRadioController.SetAGCTop(DB: Integer);
begin
if DB < 20 then DB := 20 else if DB > 120 then DB := 120;
FAGCTop := DB;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine; // ре-применяем режим с новым уровнем
FBandCache[FCurrentBand].AGCTop := FAGCTop;
Changed(rfAGCTop);
end;
procedure TRadioController.AGCTopBy(Delta: Integer);
begin SetAGCTop(FAGCTop + Delta); end;
procedure TRadioController.ApplyRxGainToBackend;
begin
if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.SetRxGain(FRxGainMode, FRxGainDb);
end;
procedure TRadioController.SetRxGainMode(M: Integer);
begin
if M < 0 then M := 0 else if M > 3 then M := 3;
FRxGainMode := M;
ApplyRxGainToBackend;
Changed(rfRxGain);
end;
procedure TRadioController.SetRxGain(Db: Integer);
begin
if Db < 0 then Db := 0 else if Db > 73 then Db := 73;
FRxGainDb := Db;
// Ручная установка усиления подразумевает manual-режим (0).
FRxGainMode := 0;
ApplyRxGainToBackend;
Changed(rfRxGain);
end;
procedure TRadioController.RxGainBy(Delta: Integer);
begin SetRxGain(FRxGainDb + Delta); end;
procedure TRadioController.SetVolume(V: Integer);
begin
if V < 0 then V := 0 else if V > 100 then V := 100;
FVolume := V;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(FVolume / 100.0);
Changed(rfVolume);
end;
procedure TRadioController.VolumeBy(Delta: Integer);
begin SetVolume(FVolume + Delta); end;
procedure TRadioController.SetMute(On_: Boolean);
begin
FMuted := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMute(FMuted);
Changed(rfMute);
end;
procedure TRadioController.ToggleMute;
begin SetMute(not FMuted); end;
procedure TRadioController.SetDrive(V: Integer);
begin
if V < 0 then V := 0 else if V > 100 then V := 100;
FDrivePercent := V;
FDriveLevel := CalcDriveByte;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи
PushNetworkState; // протолкнуть новый drive в радио (если подключено+Running)
Changed(rfDrive);
end;
procedure TRadioController.DriveBy(Delta: Integer);
begin SetDrive(FDrivePercent + Delta); end;
function TRadioController.PlutoTxAttForDrive: Double;
const
TX_OFF = -89.75; // макс. аттенюация AD9361 (0% → практически выкл)
var
Pct: Integer;
begin
// На тюне (TUN) уровень берём из TX-настроек (TUNLevel), иначе из drive%.
if FTuning then Pct := FTXSettings.TUNLevel else Pct := FDrivePercent;
if Pct < 0 then Pct := 0 else if Pct > 100 then Pct := 100;
// dB-линейно: 0% → TX_OFF, 100% → FPlutoTxMaxAttDb (потолок под внешний тракт).
Result := TX_OFF + (Pct / 100.0) * (FPlutoTxMaxAttDb - TX_OFF);
if Result > FPlutoTxMaxAttDb then Result := FPlutoTxMaxAttDb;
if Result < TX_OFF then Result := TX_OFF;
end;
procedure TRadioController.ApplyPlutoTxAtten;
const
TX_OFF = -89.75;
begin
if (FNetwork = nil) or not IsPluto then Exit;
// На передаче (MOX/TUN) — мощность от drive; на приёме — макс. аттенюация (выкл).
if FTransmitting or FTuning then FNetwork.SetTxAtten(PlutoTxAttForDrive)
else FNetwork.SetTxAtten(TX_OFF);
end;
procedure TRadioController.SetPlutoTxMaxAtt(Db: Double);
begin
if Db > 0 then Db := 0 else if Db < -50 then Db := -50;
FPlutoTxMaxAttDb := Db;
ApplyPlutoTxAtten; // применить живьём, если сейчас передаём
SaveGlobalSettings; // персист потолка (per-device)
end;
procedure TRadioController.SetAtten(Idx: Integer);
begin
if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2;
FAtten := Idx;
if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10);
Changed(rfAtten);
end;
procedure TRadioController.SetBand(Idx: Integer);
// Смена HF-диапазона по индексу (web/CAT/BandUp-Down). Выход из XVTR (если активен),
// персист покидаемого, restore нового. Полная команда — не трогает UI.
begin
if (Idx < 0) or (Idx >= CFG_BAND_COUNT) then Exit;
if FCurrentXvtr >= 0 then
begin
DeactivateXvtr; // выход в HF (HF-возврат внутри)
if Idx = FCurrentBand then Exit;
end
else if Idx = FCurrentBand then Exit;
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
FCurrentBand := Idx;
RestoreBand(Idx);
end;
procedure TRadioController.BandUp;
begin if FCurrentBand < CFG_BAND_COUNT - 1 then SetBand(FCurrentBand + 1); end;
procedure TRadioController.BandDown;
begin if FCurrentBand > 0 then SetBand(FCurrentBand - 1); end;
procedure TRadioController.ApplyXvtrToNetwork;
// Передаёт XVTR-режим в сеть (enable bit + T/R relay suppression + RX ant).
var
En, DisablePA: Boolean;
RxAnt: Byte;
begin
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
En := True;
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
end
else
begin
En := False; DisablePA := False; RxAnt := 0;
end;
if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.SetXvtrMode(En, DisablePA, RxAnt);
end;
procedure TRadioController.ActivateXvtr(Idx: Integer);
// Вход в XVTR-слот: state+DSP через общий ApplyBandDSP + VFO/центр/сеть.
// Рендер: rfXvtr (XVTR-кнопки), rfBand (XVTR-aware гасит HF), rfActiveVfo
// (дисплеи A/B + полный перерисов спектра). UI-обёртка добавляет wideband+web.
var
E: TXvtrEntry;
B: TBandSettings;
Vis: Double;
begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
FCurrentXvtr := Idx;
E := FXvtrSettings.Entries[Idx];
// Начальная частота: LastFreq если в диапазоне, иначе середина.
Vis := E.LastFreq;
if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
ApplyXvtrToNetwork; // XVTR enable bit + DisablePA + RX ant
// Авто-флаги канала (не входят в TBandSettings/ApplyBandDSP).
FFMCTCSSAutoActive := E.LastCTCSSAutoActive;
FFMRptAutoActive := E.LastFMRptAutoActive;
// DSP-настройки слота — через общий путь Band (mode/filter/AGC/CTUN/FM + рендер).
B.Mode := E.LastMode;
B.FilterIdx := E.LastFilterIdx;
B.FilterBW := FilterBWFor(E.LastMode, E.LastFilterIdx);
B.AGCMode := E.LastAGCMode;
B.AGCTop := E.LastAGCTop;
B.CTun := E.LastCTun;
B.SpanHz := FSampleRate;
B.FMSQOn := E.LastFMSQOn;
B.FMSQLevel := E.LastFMSQLevel;
B.CTCSSOn := E.LastCTCSSOn;
B.CTCSSToneIdx := E.LastCTCSSToneIdx;
B.FMStepOn := E.LastFMStepOn;
B.FMStepIdx := E.LastFMStepIdx;
B.FMRptDir := E.LastFMRptDir;
B.FMRptOffsetHz := E.LastFMRptOffsetHz;
B.VfoA := Vis;
B.VfoB := FVfoB;
ApplyBandDSP(B);
// VFO: B — своя сохранённая частота слота (клэмп в диапазон); A = visible.
FVfoB := EnsureRange(E.LastFreqB, E.FreqBegin, E.FreqEnd);
FVfoA := Vis;
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
// текущий span — иначе центрируемся на видимой частоте.
if FCTun and (E.LastCenterHz > 0) and (Abs(Vis - E.LastCenterHz) < FSpanHz / 2) then
begin
FCenterFreq := E.LastCenterHz;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Vis - FCenterFreq);
end
else
begin
FCenterFreq := Vis;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
end;
FDriveLevel := CalcDriveByte;
PushNetworkState;
Changed(rfXvtr);
Changed(rfActiveVfo);
end;
procedure TRadioController.DeactivateXvtr;
// Сохраняет текущий XVTR-слот и выходит в HF: HF-возврат (RestoreBand + финальная
// перестройка активного VFO + ResetWfAvgBuf через rfBandRestore) делается здесь.
// Changed(rfXvtr) шлём ПОСЛЕ RestoreBand — чтобы wideband/web-extras в его рендере
// увидели уже восстановленное HF-состояние.
begin
if FCurrentXvtr < 0 then Exit;
SaveCurrentBand; // XVTR-ветка: пишет весь слот + SaveXvtr
if FDevConnected then FSettings.Save;
FCurrentXvtr := -1;
ApplyXvtrToNetwork; // снять XVTR enable bit
if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
RestoreBand(FCurrentBand); // HF-возврат (состояние/DSP/retune)
Changed(rfXvtr); // погасить XVTR-кнопки + wideband/web (rfXvtr-рендер)
end;
procedure TRadioController.SetXvtrBand(Idx: Integer);
// Единая точка для GUI/Web/CAT: Idx>=0 — войти в слот, Idx<0 — выйти в HF.
// ВНИМАНИЕ: HF-возврат при выходе делает UI-обёртка DeactivateXvtr (RestoreBand).
begin
if Idx >= 0 then ActivateXvtr(Idx)
else DeactivateXvtr;
end;
procedure TRadioController.SetCurrentBandIdx(NewBand: Integer);
// Лёгкая смена HF-диапазона без полного restore: фиксируем индекс диапазона
// (для drive-кал и подсветки кнопок) и пересчитываем drive-байт. Используется
// канальным адаптером при переходе канала на другой HF-диапазон.
begin
FCurrentBand := NewBand;
FDriveLevel := CalcDriveByte;
Changed(rfBand);
end;
procedure TRadioController.SetSpan(Hz: Integer);
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
procedure TRadioController.SetSampleRate(Hz: Integer);
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в
// трансивер и полное восстановление DSP-состояния. Команда сама персистит
// (sample rate глобальный); рендер overlay/AGC-линий — в OnControllerState.
begin
if (Hz <= 0) or (Hz = FSampleRate) then Exit;
FSampleRate := Hz;
FSpanHz := Hz;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
// 1. Останавливаем WDSP и меняем rate (Close + FlushQueue + Open).
if FWDSPReady and Assigned(FDSPEngine) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(Hz);
end;
// 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate).
if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.ConfigureDDCs(1, Hz div 1000, 0, FDitherEnabled, FRandomEnabled);
// 3. Восстанавливаем DSP-состояние (ChangeSampleRate пересоздал канал).
if Assigned(FDSPEngine) then
begin
FWDSPReady := FDSPEngine.Initialized;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
FDSPEngine.SetNRMode(FNRMode);
FDSPEngine.SetNBMode(FNBMode);
FDSPEngine.SetSNB(FSNB);
FDSPEngine.SetANF(FANF);
FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine; // единый маппинг (раньше здесь был ошибочный каст индекса)
if Assigned(FNetwork) and FNetwork.Connected then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
end;
end;
SaveGlobalSettings; // sample rate глобальный — персистим (гейт по FDevConnected внутри)
Changed(rfSampleRate);
end;
procedure TRadioController.SetCTun(On_: Boolean);
begin
FCTun := On_;
FBandCache[FCurrentBand].CTun := FCTun;
if not FCTun then
begin
// Выключили CTUN: центрируемся на АКТИВНОМ VFO, shift=0. (Включение CTUN не
// трогает движок — FCenterFreq остаётся как есть, спектр не прыгает.)
FCenterFreq := ActiveVfoHz;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
PushNetworkState;
end;
Changed(rfCTun);
end;
procedure TRadioController.SetDuplex(On_: Boolean);
begin FDisplayDuplex := On_; Changed(rfDuplex); { TODO wiring } end;
procedure TRadioController.SetWfAGC(On_: Boolean);
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
procedure TRadioController.SetWfNF(On_: Boolean);
begin FWfNFEnabled := On_; Changed(rfWfNF); end;
procedure TRadioController.SetNR(Mode: Integer);
begin
if Mode < 0 then Mode := 0 else if Mode > 4 then Mode := 4;
FNRMode := Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNRMode(FNRMode);
Changed(rfNR);
end;
procedure TRadioController.SetNB(Mode: Integer);
begin
if Mode < 0 then Mode := 0 else if Mode > 2 then Mode := 2;
FNBMode := Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNBMode(FNBMode);
Changed(rfNB);
end;
procedure TRadioController.SetSNB(On_: Boolean);
begin
FSNB := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetSNB(FSNB);
Changed(rfSNB);
end;
procedure TRadioController.SetANF(On_: Boolean);
begin
FANF := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetANF(FANF);
Changed(rfANF);
end;
function TRadioController.DefaultMicSource: TTXMicSource;
begin
if FAudioInDevName <> '' then Result := txmsSoundCard
else Result := txmsRadio;
end;
function TRadioController.BuildMicLineSelectByte: Byte;
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
// bit 0 — Line In (1 = line-in вместо mic-in)
// bit 1 — Mic Boost (+20 dB); не ставим когда Line In
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
// bit 3 — Tip/Ring (0=Tip, 1=Ring; Orion/OrionMkII)
// bit 4 — Orion mic bias (1 = enable +5V bias)
// bit 5 — Saturn XLR/balanced (не используем)
begin
Result := 0;
if FTXSettings.MicLineIn then
Result := Result or $01 // bit 0: Line In
else if FTXSettings.MicBoost then
Result := Result or $02; // bit 1: Boost только при Mic In
if not FTXSettings.MicPTTEnabled then Result := Result or $04; // bit 2: инверсия
if FTXSettings.MicTipRing then Result := Result or $08; // bit 3: Ring
if FTXSettings.MicBias then Result := Result or $10; // bit 4: bias
end;
procedure TRadioController.ApplyNoiseFilterButtonsToDSP;
begin
if not FWDSPReady then Exit;
FDSPEngine.SetNRMode(FNRMode);
FDSPEngine.SetNBMode(FNBMode);
FDSPEngine.SetSNB(FSNB);
FDSPEngine.SetANF(FANF);
end;
procedure TRadioController.SendDUCSpecificFromSettings;
var
DUCPkt: TDUCSpecificPacket;
RateKsps: Word;
begin
if not (FNetwork.Connected and FNetwork.Running) then Exit;
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
RateKsps := FDSPEngine.TXSampleRate div 1000;
DUCPkt.NumDACs := 1;
DUCPkt.SidetoneLevel := 50;
DUCPkt.SidetoneFreqHi := Hi(600);
DUCPkt.SidetoneFreqLo := Lo(600);
DUCPkt.KeyerSpeed := 20;
DUCPkt.KeyerWeight := 50;
DUCPkt.DUC0RateHi := Hi(RateKsps);
DUCPkt.DUC0RateLo := Lo(RateKsps);
DUCPkt.DUC0Bits := 24;
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
FNetwork.SendDUCSpecific(DUCPkt);
end;
procedure TRadioController.ApplyTXSettingsToDSP;
begin
if not FWDSPReady then Exit;
// Mic source — выставляем значение по умолчанию для MOX-кнопки;
// при реальном PTT SetMOX скорректирует источник.
FDSPEngine.SetTXMicSource(DefaultMicSource);
FDSPEngine.SetMicGain(FTXSettings.MicGainDB);
FDSPEngine.SetTXFilterFull(FTXSettings.FilterLow, FTXSettings.FilterHigh,
FTXSettings.FilterNC, FTXSettings.FilterMP,
FTXSettings.FilterWindow);
FDSPEngine.SetTXCompressor(FTXSettings.CompressorOn, FTXSettings.CompressorGain);
FDSPEngine.SetTXLeveler(FTXSettings.LevelerOn, FTXSettings.LevelerTop,
FTXSettings.LevelerDecay);
FDSPEngine.SetTXALC(FTXSettings.ALCOn, FTXSettings.ALCMaxGain,
FTXSettings.ALCDecay);
FDSPEngine.SetTXPhaseRot(FTXSettings.PhaseRotOn, FTXSettings.PhaseRotStages,
FTXSettings.PhaseRotFreq);
FDSPEngine.SetTXEQ(FTXSettings.EQOn, FTXSettings.EQNumBands,
FTXSettings.EQGains, FTXSettings.EQFreqs);
FDSPEngine.SetTXAMCarrierLevel(FTXSettings.AMCarrierLevel);
FDSPEngine.SetTXFMParams(FTXSettings.FMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetTXCTCSS(FTXSettings.CTCSSOn, FTXSettings.CTCSSFreq);
// TX display
FDSPEngine.SetTXFFTParams(FTXSettings.TXFFTSize, FTXSettings.TXWindowType);
FDSPEngine.SetTXSpectrumDisplay(FTXSettings.TXSpecDetector,
FTXSettings.TXSpecAvgMode,
FTXSettings.TXSpecAvgTimeMS);
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
FTXSettings.TXWfAvgMode,
FTXSettings.TXWfAvgTimeMS);
end;
procedure TRadioController.SetMOX(On_: Boolean);
var
WasTransmitting: Boolean;
begin
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then
Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
Exit;
WasTransmitting := FTransmitting;
FTransmitting := On_;
if On_ then FDriveLevel := CalcDriveByte;
FDUCPendingCount := 0; // сброс TX DUC-очереди на смене TX-состояния
// Сетевая PTT-последовательность TX-on: сначала реле (Alex bit27=1, drive) без
// PTT, затем PTT=1 — переключаем реле раньше передачи.
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.ClearDUCIQQueue;
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, True, True, True);
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
end;
if FWDSPReady and Assigned(FDSPEngine) then
begin
// Mic source автоматически по источнику TX:
// Web TX → txmsWeb; HW PTT → txmsRadio; MOX/CAT → DefaultMicSource.
if FTransmitting then
begin
if FWebClientActive then
begin
FWebMicActive := True;
FDSPEngine.SetTXMicSource(txmsWeb);
end
else if FHWPTTActive then
FDSPEngine.SetTXMicSource(txmsRadio)
else
FDSPEngine.SetTXMicSource(DefaultMicSource);
end
else if FWebMicActive then
begin
FWebMicActive := False;
FDSPEngine.SetTXMicSource(DefaultMicSource);
end;
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
FDSPEngine.SetTXRun(FTransmitting);
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin
FDSPEngine.FlushRX;
if Assigned(FAudioOut) then
FAudioOut.Clear;
{$IFDEF WINDOWS}
// Windows: на TX→RX дропаем И RX-IQ, И аудио-выход — radio FPGA TX-buffer +
// PA slew-down дотравливают «хвост» (на Windows тянется 200500мс).
FDSPEngine.BeginPostTXMute(250);
{$ENDIF}
end;
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
// источник на RX, чтобы видеть приём во время передачи.
if FTransmitting and FDisplayDuplex then
FDSPEngine.SetDisplaySourceTX(False);
end;
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, False, True, True);
FNetwork.ClearDUCIQQueue;
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
end;
ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме
Changed(rfTransmitting);
end;
procedure TRadioController.SetTune(On_: Boolean);
begin
if On_ = FTuning then Exit;
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit;
if On_ then
begin
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
FTuning := True;
FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень)
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0);
if not FTransmitting then SetMOX(True); // PTT + run TXA-канал
end
else
begin
if FTransmitting then SetMOX(False);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXTone(False, 0, 0);
FTuning := False;
FDriveLevel := CalcDriveByte; // вернуть обычный drive%
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
ApplyPlutoTxAtten; // финальное состояние att по FTuning/FTransmitting (Pluto)
Changed(rfTuning);
end;
procedure TRadioController.SetFMStep(Idx: Integer);
begin
if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4
FFMStepIdx := Idx;
Changed(rfFMStepIdx);
end;
procedure TRadioController.SetFMCTCSS(On_: Boolean);
begin
FFMCTCSSOn := On_;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
Changed(rfFMCTCSS);
end;
procedure TRadioController.SetFMCTCSSTone(Idx: Integer);
begin
if Idx < 0 then Idx := 0 else if Idx > CTCSS_COUNT - 1 then Idx := CTCSS_COUNT - 1;
FFMCTCSSToneIdx := Idx;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
Changed(rfFMCTCSSTone);
end;
procedure TRadioController.SetFMSquelch(On_: Boolean);
begin
FFMSQOn := On_;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
Changed(rfFMSQ);
end;
procedure TRadioController.SetFMSquelchLevel(V: Integer);
begin
FFMSQLevel := V;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
Changed(rfFMSQLevel);
end;
procedure TRadioController.SetFMRpt(Dir: Integer);
begin
FFMRptDir := Dir;
FFMRptAutoActive := False; // явная установка направления — ручное действие
PushNetworkState; // TX-сдвиг применяется через ActiveTXFreqHz
Changed(rfFMRpt);
end;
procedure TRadioController.SetFMRptOffset(Hz: Double);
begin
if Hz <= 0.0 then Exit;
FFMRptOffsetHz := Hz;
if FFMRptDir <> RPT_NONE then PushNetworkState;
Changed(rfFMRpt);
end;
procedure TRadioController.UpdateRptAutoState;
// Авто-включение MINUS-сдвига в 2-метровом репитерном сегменте и авто-снятие
// при уходе из него (только если MINUS был выставлен автоматически).
begin
if RptAutoDir(FVfoA) = RPT_MINUS then
begin
if FFMRptDir = RPT_NONE then
begin
FFMRptDir := RPT_MINUS;
FFMRptAutoActive := True;
Changed(rfFMRpt);
end;
end
else
begin
if (FFMRptDir = RPT_MINUS) and FFMRptAutoActive then
begin
FFMRptDir := RPT_NONE;
FFMRptAutoActive := False;
Changed(rfFMRpt);
end;
end;
end;
procedure TRadioController.StartStop;
begin SetRun(not FRunning); end;
procedure TRadioController.SetRun(On_: Boolean);
begin FRunning := On_; Changed(rfRunning); { TODO wiring: логика BtnStartStopClick } end;
procedure TRadioController.StartRunning(SpectrumWidth: Integer);
// «Go-live» последовательность Run=1: General packet, wideband/DDC-конфиг,
// синхронизация WDSP sample rate, восстановление DSP (mode/volume/filter/NR/AGC),
// Run=1 → seq-сброс → DDC/DUC Specific + TX-цепь. Рендер run-кнопки/sleep — UI
// через OnControllerState(rfRunning); статус-бар/FSpecView.AGCTop/resize — обёртка.
var
GenPkt: TGeneralPacket;
begin
// General packet
FillChar(GenPkt, SizeOf(GenPkt), 0);
GenPkt.Command := CMD_GENERAL;
GenPkt.Flags37 := $08;
GenPkt.Flags38 := $01;
GenPkt.PAConfig := $01;
GenPkt.WBPort[0] := (PORT_WIDEBAND_ADC0 shr 8) and $FF;
GenPkt.WBPort[1] := PORT_WIDEBAND_ADC0 and $FF;
GenPkt.WBEnable := IfThen(FShowWideband, 1, 0);
GenPkt.WBSamplesPerPkt[0] := 512 shr 8;
GenPkt.WBSamplesPerPkt[1] := 512 and $FF;
GenPkt.WBSampleSize := 16;
GenPkt.WBUpdateRate := 70;
GenPkt.WBPacketsPerFrame := 32;
if FNetwork.Device.BoardType = 5 then
GenPkt.AlexEnable := $03
else
GenPkt.AlexEnable := $01;
FNetwork.SendGeneralPacket(GenPkt);
FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32);
if FNetwork.Device.BoardType in [3, 4, 5] then
FActiveDDC := 2
else
FActiveDDC := 0;
// Синхронизируем samplerate WDSP если нужно
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(FSampleRate);
FWDSPReady := FDSPEngine.Initialized;
end;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
ApplyNoiseFilterButtonsToDSP;
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine; // единый маппинг UI-индекс→enum (не прямой каст)
end;
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True);
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
FRunning := True;
FRXStartTime := GetTickCount64;
FRXLastPktTime := 0;
FRXPacketCount := 0;
FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0);
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
// Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1).
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
// RX hw-gain в бэкенд (Pluto). HPSDR — no-op.
ApplyRxGainToBackend;
SendDUCSpecificFromSettings;
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP;
ApplyModeFilter;
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
if FWDSPReady then
FDSPEngine.UpdateAGCLines(SpectrumWidth); // ещё не получили ни одного пакета
Changed(rfRunning); // UI: caption STOP / MOX.Enabled / InhibitSleep
end;
procedure TRadioController.StopRunning;
// Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся
// Connected — Disconnect отдельно (UI-обёртка делает это после persistence).
// Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting.
begin
if FRunning then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False;
FTransmitting := False;
if FWDSPReady then FDSPEngine.SetTXRun(False);
Changed(rfTransmitting); // MOX-кнопка off
end;
FRXPacketCount := 0;
FActiveDDC := 0;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE;
FPlutoTeleTick := 0; FPlutoTempBad := 0;
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
end;
function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean;
// Подключение + загрузка per-device настроек по MAC в поля контроллера + конфиг
// движка (DSP display, audio). Возвращает False, если Connect не удался (UI
// покажет сообщение по FNetwork.LastError). Рендер ВСЕХ device-виджетов — UI
// через OnControllerState(rfDevice). Перестройку диапазона/VFO (RestoreBand/
// XVTR/ActiveVfo) делает UI-обёртка ПОСЛЕ (там финальная перестройка через
// ApplyVfoA + wideband/web-extras). FLoadedGlobal хранит загруженный G_Settings
// для рендера полей, которых нет в контроллере (FPS/FreqMhzDigits/CAT).
var
i: Integer;
begin
// Подбираем бэкенд под тип устройства (HPSDR/Pluto) до подключения.
EnsureBackend(Dev.Kind);
Result := FNetwork.Connect(Dev);
if not Result then Exit;
// --- Загружаем настройки устройства по MAC ---
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache);
// TX/Alex/XVTR per-device
FSettings.LoadTX(FDevMAC, FTXSettings);
FSettings.LoadAlex(FDevMAC, FAlexSettings);
FNetwork.SetAlexConfig(FAlexSettings);
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
FCurrentXvtr := -1;
ApplyXvtrToNetwork;
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
FTXSpecRange := FTXSettings.TXSpecRange;
if FTXSettings.TXSpecGridStep > 0 then
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
FVolume := FLoadedGlobal.Volume;
FActiveVfo := EnsureRange(FLoadedGlobal.ActiveVfo, 0, 1); // битый конфиг не должен дать невалидный VFO
FPAMaxPower := FLoadedGlobal.PAMaxPower;
FPlutoTxMaxAttDb := EnsureRange(FLoadedGlobal.PlutoTxMaxAttDb, -50.0, 0.0);
for i := 0 to CFG_BAND_COUNT - 1 do
FPABandCal[i] := FLoadedGlobal.PABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do
FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i];
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
// Клампим LastBand — иначе битый/старый конфиг → RestoreBand выйдет по гарду,
// а последующие SetMode/SetFilterIdx запишут в FBandCache[невалид] за массив.
FCurrentBand := EnsureRange(FLoadedGlobal.LastBand, 0, CFG_BAND_COUNT - 1);
FDriveLevel := CalcDriveByte;
// SampleRate — глобальный, до RestoreBand
if FLoadedGlobal.SampleRate > 0 then
begin
FSampleRate := FLoadedGlobal.SampleRate;
FSpanHz := FSampleRate;
end;
// Pluto: HPSDR-наследие rate (48..1536k, часто <520k) невалидно. Клампим к
// дефолту бэкенда, если ниже минимума (overlay покажет валидный набор).
with FNetwork.Caps do
if (Kind = bkPluto) and (FSampleRate < MinSampleRate) then
begin
FSampleRate := 1536000;
FSpanHz := FSampleRate;
end;
FRxGainMode := EnsureRange(FLoadedGlobal.RxGainMode, 0, 3);
FRxGainDb := EnsureRange(FLoadedGlobal.RxGainDb, 0, 73);
FNRMode := EnsureRange(FLoadedGlobal.NRMode, 0, 4);
FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2);
FSNB := FLoadedGlobal.SNBEnabled;
FANF := FLoadedGlobal.ANFEnabled;
FWfAGCEnabled := FLoadedGlobal.WfAGCEnabled;
FWfNFEnabled := FLoadedGlobal.WfNFEnabled;
FDitherEnabled := FLoadedGlobal.DitherEnabled;
FRandomEnabled := FLoadedGlobal.RandomEnabled;
FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio;
FWfManualHigh := FLoadedGlobal.WfManualHigh;
FWfManualLow := FLoadedGlobal.WfManualLow;
FWfAGCOffset := FLoadedGlobal.WfAGCOffset;
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
FShowWideband := FLoadedGlobal.ShowWideband;
FWidebandFill := FLoadedGlobal.WidebandFill;
FSpectrumFill := FLoadedGlobal.SpectrumFill;
FDisplayDuplex := FLoadedGlobal.DisplayDuplex;
// Настройки дисплея/сетки + DSP display + аудио-устройства
if FLoadedGlobal.FFTSize > 0 then
begin
FSpecRefLevel := FLoadedGlobal.SpecRefLevel;
FSpecRange := FLoadedGlobal.SpecRange;
if FLoadedGlobal.SpecGridStep > 0 then FSpecGridStep := FLoadedGlobal.SpecGridStep;
if FWDSPReady then
begin
FDSPEngine.SetFFTParams(FLoadedGlobal.FFTSize, FLoadedGlobal.WindowType);
FDSPEngine.SetSpectrumDisplay(FLoadedGlobal.SpecDetector, FLoadedGlobal.SpecAvgMode,
FLoadedGlobal.SpecAvgTimeMS);
FDSPEngine.SetWaterfallDisplay(FLoadedGlobal.WfDetector, FLoadedGlobal.WfAvgMode,
FLoadedGlobal.WfAvgTimeMS);
end;
// Аудио устройство вывода
FAudioOut.Close;
if FLoadedGlobal.AudioSampleRate > 0 then
FAudioOut.SampleRate := FLoadedGlobal.AudioSampleRate;
if FLoadedGlobal.AudioOutDevice <> '' then
begin
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(FLoadedGlobal.AudioOutDevice);
FAudioOutDevName := FLoadedGlobal.AudioOutDevice;
end
else
begin
FAudioOut.DeviceIndex := -1;
FAudioOutDevName := '';
end;
FAudioOut.Open;
// Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT —
// здесь только открываем устройство.
if FLoadedGlobal.AudioInDevice <> '' then
begin
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice);
FAudioInDevName := FLoadedGlobal.AudioInDevice;
FAudioIn.Open;
end
else
begin
FAudioIn.DeviceIndex := -1;
FAudioInDevName := '';
end;
end;
Changed(rfDevice); // UI: полная синхронизация device-виджетов из состояния
end;
function TRadioController.ConnectAndRun(const Dev: THPSDRDevice;
SpectrumWidth: Integer): Boolean;
// Headless-оркестрация подключения: Connect+load-by-MAC (ConnectDevice) →
// перестройка диапазона/XVTR/активного VFO → go-live (StartRunning). Рендер всех
// device/band/run-виджетов идёт через события (rfDevice/rfBandRestore/rfXvtr/
// rfActiveVfo/rfRunning). UI-обёртка добавляет лишь статус-строки/AGCTop/resize.
// Возвращает False, если Connect не удался (тогда bring-up не делается).
begin
Result := ConnectDevice(Dev);
if not Result then Exit;
// Перестройка диапазона — команда несёт финальную перестройку по активному VFO.
RestoreBand(FCurrentBand);
// Pluto/VHF: HF-частоты вне диапазона. Если восстановленный VFO ниже минимума
// бэкенда — ставим дефолт VHF (временно 100 МГц, пока нет VHF band-плана).
with FNetwork.Caps do
if (Kind = bkPluto) and (FVfoA < MinFreqHz) then
begin
FVfoB := 100000000;
FCenterFreq := 100000000;
SetVfoA(100000000); // перестроит дисплей + LO
end;
// Восстанавливаем XVTR-режим, если он был активен при выходе. FCurrentBand уже
// загружен (HF-диапазон до XVTR) — ActivateXvtr только выставит FVfoA/FCurrentXvtr.
if (FLoadedGlobal.LastXvtr >= 0) and (FLoadedGlobal.LastXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FLoadedGlobal.LastXvtr].Enabled then
ActivateXvtr(FLoadedGlobal.LastXvtr);
// Активный VFO загружен из настроек; RestoreBand/ActivateXvtr строят только
// VFO A. Если активен B — перестраиваем приёмник И подсветку на него. Для
// активного A подсветку освежает UI-обёртка (чистый рендер).
if FActiveVfo = 1 then
SetActiveVfo(1);
// Go-live Run=1 (General/DDC/DUC + DSP restore); рендер — через rfRunning.
StartRunning(SpectrumWidth);
end;
procedure TRadioController.StopAndDisconnect;
// Headless STOP+teardown: graceful Run=0 (StopRunning) + персист настроек
// устройства/диапазона/глобала + разрыв сети. Персист идёт ДО Disconnect (saves
// читают живое состояние). Рендер run-кнопки/MOX/sleep — через StopRunning→
// rfRunning; UI-обёртка добавляет очистку спектра и offline-статус.
begin
StopRunning;
// Сохраняем текущую частоту/настройки чтобы следующий START восстановил их.
if FDevConnected then
begin
SaveCurrentBand;
SaveGlobalSettings;
FSettings.SaveAlex(FDevMAC, FAlexSettings);
// XVTR LastFreq → актуальная видимая частота в момент остановки.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
end;
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
FSettings.Save;
FNetwork.Disconnect;
end;
// ---------------------------------------------------------------------------
// Discovery — поиск устройств в фоновом потоке. Найденные приходят через
// FNetwork.OnDeviceFound (=OnDeviceFound) и копятся в FDeviceStore.Discovered;
// по завершении без устройств — DiscoverFinishedNone. UI слушает rfDeviceList.
// ---------------------------------------------------------------------------
type
TDiscoverThread = class(TThread)
private
FCtrl: TRadioController;
protected
procedure Execute; override;
public
constructor Create(ACtrl: TRadioController);
end;
constructor TDiscoverThread.Create(ACtrl: TRadioController);
begin
FCtrl := ACtrl;
FreeOnTerminate := True;
inherited Create(False);
end;
procedure TDiscoverThread.Execute;
// Сканируем ОБА бэкенда транзиентными экземплярами (независимо от FNetwork —
// он может быть уже подключён/другого типа). Найденные сигналятся колбэком
// OnDeviceFound во время Discover() и копятся в store.
var
Found: Integer;
HP: THPSDRNetwork;
Pluto: TPlutoBackend;
PDev: TRadioDevice;
i: Integer;
begin
Found := 0;
// openHPSDR (UDP broadcast)
HP := THPSDRNetwork.Create;
try
HP.OnDeviceFound := FCtrl.OnDeviceFound;
Found := Found + Length(HP.Discover(2000));
finally
HP.Free;
end;
// Pluto (libiio scan + прямой probe сетевых URI) — если libiio доступна.
if IIOLoad then
begin
Pluto := TPlutoBackend.Create;
try
Pluto.OnDeviceFound := FCtrl.OnDeviceFound;
// USB scan
Found := Found + Length(Pluto.Discover(1500));
// Прямой probe сетевых адресов (mDNS недоступен) — из списка фронтенда.
for i := 0 to High(FCtrl.FPlutoProbeURIs) do
if Pluto.ProbeURI(FCtrl.FPlutoProbeURIs[i], PDev) then
Inc(Found);
finally
Pluto.Free;
end;
end;
if Found = 0 then
FCtrl.Invoke(FCtrl.DiscoverFinishedNone);
end;
procedure TRadioController.EnsureBackend(Kind: TBackendKind);
// Пересоздаёт FNetwork нужного класса, перенося колбэки данных. No-op если тип
// уже верный. Старый бэкенд отключается и освобождается.
var
NewB: TRadioBackend;
begin
if (FNetwork <> nil) and (FNetwork.Caps.Kind = Kind) then Exit;
case Kind of
bkPluto: NewB := TPlutoBackend.Create;
else NewB := THPSDRNetwork.Create;
end;
if FNetwork <> nil then
begin
if FNetwork.Connected then FNetwork.Disconnect;
NewB.OnDeviceFound := FNetwork.OnDeviceFound;
NewB.OnDDCIQ := FNetwork.OnDDCIQ;
NewB.OnMicPacket := FNetwork.OnMicPacket;
NewB.OnHPStatus := FNetwork.OnHPStatus;
NewB.OnWideband := FNetwork.OnWideband;
FreeAndNil(FNetwork);
end;
FNetwork := NewB;
end;
function TRadioController.BackendCaps: TBackendCaps;
begin
if FNetwork <> nil then Result := FNetwork.Caps
else FillChar(Result, SizeOf(Result), 0);
end;
function TRadioController.IsPluto: Boolean;
begin
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
end;
function TRadioController.BoardDisplayName: string;
var
Dev: THPSDRDevice;
begin
if FNetwork = nil then Exit('');
Dev := FNetwork.Device;
if Dev.Kind = bkPluto then
begin
Result := PlutoModelName(Dev.Model);
// Серийник дописываем, только если это реальный hw_serial, а не URI-fallback
// ('usb:1.5.5' / 'ip:…'), который PlutoBackend кладёт при отсутствии серийника.
if (Dev.Serial <> '') and (Pos('usb:', Dev.Serial) = 0)
and (Pos('ip:', Dev.Serial) = 0) then
Result := Result + ' SN:' + Dev.Serial;
end
else
Result := BoardTypeName(Dev.BoardType);
end;
procedure TRadioController.ServicePlutoTelemetry;
const
POLL_MS = 500; // опрос ~2 Гц: чтение attr — USB round-trip, чаще ни к чему
TEMP_MIN_C = 0.0; // работающий AD936x всегда теплее окружающей (само-нагрев)
TEMP_MAX_C = 120.0; // абсолютный потолок (датчик исправен)
TEMP_STEP_C = 12.0; // макс. физичный шаг между опросами (тепловая инерция чипа)
TEMP_FORCE = 3; // принять «выброс», если держится N раз подряд (не залипаем)
var
t, r: Double;
now_: QWord;
begin
if (FNetwork = nil) or (not FRunning) or (FNetwork.Caps.Kind <> bkPluto) then Exit;
now_ := GetTickCount64;
if (FPlutoTeleTick <> 0) and (now_ - FPlutoTeleTick < POLL_MS) then Exit;
FPlutoTeleTick := now_;
if not FNetwork.ReadTelemetry(t, r) then Exit;
// Температура: датчик AD9361 иногда отдаёт мусор (мид-конверсия / контеншн на
// USB-контексте с RX-потоком) — наблюдалось редкое -15 °C. Фильтруем: жёсткое
// окно правдоподобия + slew-limit по тепловой инерции; устойчивый «выброс»
// (реальный скачок) всё же принимаем через TEMP_FORCE опросов, чтобы не залипнуть.
if t > TELEMETRY_NONE then
begin
if (t >= TEMP_MIN_C) and (t <= TEMP_MAX_C) then
begin
if (FLastPlutoTempC <= TELEMETRY_NONE) or (Abs(t - FLastPlutoTempC) <= TEMP_STEP_C) then
begin
FLastPlutoTempC := t; FPlutoTempBad := 0;
end
else
begin
Inc(FPlutoTempBad);
if FPlutoTempBad >= TEMP_FORCE then
begin FLastPlutoTempC := t; FPlutoTempBad := 0; end;
end;
end;
// вне окна правдоподобия — молча отбрасываем, держим прошлое валидное значение
end;
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
end;
procedure TRadioController.SetPlutoProbeURIs(const URIs: array of string);
var i: Integer;
begin
SetLength(FPlutoProbeURIs, Length(URIs));
for i := 0 to High(URIs) do FPlutoProbeURIs[i] := URIs[i];
end;
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
// Сетевой/scan-поток: строим строку для списка → pending → маршалинг в
// ApplyDeviceFound. Строка зависит от типа бэкенда (HPSDR vs Pluto).
begin
FPendingFoundDev := Dev;
if Dev.Kind = bkPluto then
FPendingFoundEntry := Format('PlutoSDR %s', [Dev.URI])
else
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
[Dev.IPAddress, BoardTypeName(Dev.BoardType),
Dev.FirmwareVersion, Dev.NumDDCs]);
Invoke(ApplyDeviceFound);
end;
procedure TRadioController.ApplyDeviceFound;
// Маршалится из OnDeviceFound (Synchronize). Дописываем в store и уведомляем UI.
begin
FDiscoverNoneFound := False;
FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry,
FPendingFoundDev.BoardType, FPendingFoundDev.MAC,
FPendingFoundDev.Kind, FPendingFoundDev.URI,
FPendingFoundDev.Serial);
Changed(rfDeviceList);
end;
procedure TRadioController.DiscoverFinishedNone;
// Маршалится из discover-потока, когда устройств не найдено.
begin
FDiscoverNoneFound := True;
Changed(rfDeviceList);
end;
procedure TRadioController.Discover;
// Старт поиска: чистим прошлый список, сбрасываем direct-IP, запускаем поток.
begin
FDeviceStore.ClearDiscovered;
FDiscoverNoneFound := False;
FNetwork.DirectIP := '';
Changed(rfDeviceList);
TDiscoverThread.Create(Self);
end;
end.