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, BeaconDecoder, BeaconFEC, Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; type // QO-100 beacon lock — статус контура (для UI/маркеров). TBeaconState = (bsIdle, // лок выключен bsSearch, // включён, маяк ещё не подтверждён (низкий prominence) bsLock); // маяк уверенно виден и сопровождается // Поле, которое изменилось — для гранулярных уведомлений наружу. TRadioField = ( rfVfoA, rfVfoB, rfActiveVfo, rfVfoLock, 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, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle 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; FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm) FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент) FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False 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 (декодер маяка 10489.750 = эталон частоты) ---- // Один концепт: лок = декодер + трим LOError. Декодер точно ведёт несущую, // его измеренный дрейф стекаем в LOError → весь даунлинк стабилен. FBeaconLockOn: Boolean; // лок (=декодер) включён пользователем FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока // (только индикация на кнопке); сама поправка // живёт в LOError активного трансвертера FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6 FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка // (наведение + остаток декодера) — для оранж. маркера FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока) FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR) FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO) FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков) // ---- декодер маяка (BPSK 400 бод, AO-40) — фронтенд+констелляция+бюллетень ---- FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера // (узкий фильтр, ставится кликом); 0 = ещё не наведён FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True // ---- Аудио / 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; FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock) FDisplayDuplex: Boolean; // QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.), // False — слышим свой downlink со спутника при передаче. Кнопка RX MUTE. FRxMuteOnTx: 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 / Zynq SoC °C / // RSSI приёмника дБ. Опрашиваются ServicePlutoTelemetry с UI-таймера; // TELEMETRY_NONE = нет данных. FLastPlutoTempC, FLastPlutoSocTempC, 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; // XvtrTranslateTX — то же для ПЕРЕДАЧИ. Обычный трансвертер: = XvtrTranslate. // QO-100 (FullDuplexTx): отдельный TX-LO (visible-TxLOOffset, без LOError — // uplink-LO не подвержен дрейфу LNB). function XvtrTranslateTX(VisibleHz: Double): Double; function ActiveTXFreqHz: Double; // Складывает коррекцию маяка 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 ApplyWDSPDrive(Pct: Integer); // цифровой drive WDSP: Pluto=1.0 (мощность — атт), HPSDR=Pct/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 по опорному маяку. // QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты). 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-частота маяка (наведение+остаток) function BeaconDecodeFreqHz: Double; // частота наведения (для узкого маркера) procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл function BeaconDecodeInvert: Boolean; function GetBeaconScope(out S: TBeaconScope): Boolean; function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean; // Отображение / приём procedure SetCTun(On_: Boolean); procedure SetDuplex(On_: Boolean); procedure SetRxMuteOnTx(On_: Boolean); // QO-100 self-monitor (кнопка RX MUTE) procedure ApplyRxMuteToEngine; // FRxMuteOnTx → FDSPEngine.MonitorRXDuringTX 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 procedure SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A) procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO // 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) // По IP/URI ищет устройство в store (discovered→saved), восстанавливая // Kind/URI/Serial/BoardType/MAC. Нужно для Pluto (открывается по URI, выбор // бэкенда — по Kind). Не найдено → простой HPSDR по IP. Общий путь GUI+демон. function ResolveDevice(const IP: string): THPSDRDevice; // Сохраняет устройство по введённому адресу. Схема URI ('ip:'/'usb:'/'local:') // → Pluto (AddSavedPluto), иначе HPSDR-IP. Общий путь web-dev_add для GUI и // демона (десктоп-диалог сохраняет Pluto своим путём через AddSavedPluto). procedure AddSavedDevice(const AName, AAddr: string); procedure Discover; // Гарантирует, что FNetwork — бэкенд нужного типа (HPSDR/Pluto): при // несовпадении пересоздаёт его, перенося колбэки данных. Вызывается из // ConnectDevice по Dev.Kind. Discovery использует транзиентные экземпляры. procedure EnsureBackend(Kind: TBackendKind); // Возможности текущего бэкенда (для гейтинга UI / пресетов sample rate). function BackendCaps: TBackendCaps; // Единый источник пресетов sample-rate для UI (desktop overlay + web). // Берётся из caps текущего бэкенда (HPSDR 48k..1536k / Pluto 576k..5760k). function SampleRatePresets: TBackendRateArray; // Единый источник band-плана для UI (HF / VHF-UHF по типу бэкенда). function BandPlan: TBandPlanArray; // True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF). function IsPluto: Boolean; // True — активен QO-100 (Pluto + full-duplex XVTR-слот); гейт UI beacon/RX MUTE. function InQO100: 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); // Сидирует probe-список из сохранённых Pluto (для бэкенд-агностичного Discover // без UI-диалога — демон). GUI добавляет ручной IP поверх в BuildProbeURIs. procedure SeedPlutoProbeFromSaved; // ---- События наружу ---- property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke; property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged; // Доп. наблюдатели состояния (помимо единственного OnStateChanged, который // занимает MainForm). Нужны CAT/Andromeda для push индикаторов панели. procedure AddStateListener(L: TRadioStateEvent); // Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне // рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned), // чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless. property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune; property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume; // Локальный звук ПК (динамик RX / микрофон TX). Демон ставит False: аудио // только через web (OnAudioConsume), локальная звуковая карта не трогается. property LocalAudioEnabled: Boolean read FLocalAudio write FLocalAudio; 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 по умолчанию FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) 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; FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := 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: эталон частоты = декодер (см. ServiceBeaconLock). FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink) FBeaconLockHz := 0.0; FBeaconLockOn := False; // Декодер маяка: фронтенд BPSK 400 бод (тап IQ в движке). FBeaconDec := TBeaconDecoder.Create; // Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4 // и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк). // Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC. FBeaconDecInvert := True; FBeaconDec.SetInvert(FBeaconDecInvert); FDSPEngine.SetBeaconDecoder(FBeaconDec); // 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.SetBeaconDecoder(nil); FDSPEngine.Close; FreeAndNil(FDSPEngine); end; if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec); end; destructor TRadioController.Destroy; begin FreeEngines; FreeAndNil(FDeviceStore); FreeAndNil(FSettings); inherited Destroy; end; procedure TRadioController.Changed(Field: TRadioField); var i: Integer; begin if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field); for i := 0 to High(FStateListeners) do if Assigned(FStateListeners[i]) then FStateListeners[i](Self, Field); end; procedure TRadioController.AddStateListener(L: TRadioStateEvent); begin if not Assigned(L) then Exit; SetLength(FStateListeners, Length(FStateListeners) + 1); FStateListeners[High(FStateListeners)] := L; 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), XvtrTranslateTX(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; // Без локального звука (демон) на этом маршрут заканчивается: если web-клиент // не активен, аудио просто отбрасывается, а не идёт на звуковую карту хоста. if FLocalAudio then 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.XvtrTranslateTX(VisibleHz: Double): Double; begin Result := VisibleHz; if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit; if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit; // QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset), // без LOError — дрейф LNB к передаче отношения не имеет. if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset else // Обычный трансвертер: RX=TX LO (с калибровкой 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 // --- контур коррекции (измерение частоты — от ДЕКОДЕРА, не от display-FFT) --- // Декодер точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload): // суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения. // Это смещение медленно стекаем в LOError активного XVTR → ретюн LO компенсирует // дрейф LNB, весь даунлинк стоит. Внутренний NCO декодера держит маяк // декодированным во время плавного ретюна (наведение едет feed-forward за маяком). BEACON_GAIN = 0.5; // гейн контура: <1 — запас от перелёта (декодерная // оценка точная, но ретюн LO имеет латентность пайплайна) BEACON_DEADBAND_HZ = 20.0; // внутри — коррекции стоп (декодер точнее FFT → дедбэнд уже) BEACON_MAX_STEP_HZ = 400.0; // клам один шаг коррекции (защита от выброса оценки) BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка ретюна LO BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц) // --- знак отображения остатка декодера --- // invert=True (QO-100/LNB) выравнивает частотную ось декодера с дисплеем, поэтому // display_маяка = наведение + (ResidHz+CarFreqHz). Если на эфире контур убегает — // ось перевёрнута, флипнуть на -1.0 (см. лог: e и дрейф должны иметь один знак). BEACON_RESID_SIGN = 1.0; // --- метрика лока (берём из снимка декодера: CarrierLock + SNR) --- BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого procedure TRadioController.SetBeaconRefHz(Hz: Double); begin FBeaconRefHz := Hz; end; procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double); // Пользователь кликнул по маяку → наводим узкий тракт декодера на эту частоту и // переинициализируем петли (RunAcquisition заново оценит несущую). Дальше декодер // сам ведёт маяк своим NCO, а ServiceBeaconLock стекает измеренный дрейф в LOError. begin if not FBeaconLockOn then SetBeaconLock(True); FBeaconDecHz := DisplayHz; FBeaconTrackHz := DisplayHz; if Assigned(FBeaconDec) then begin FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq); FBeaconDec.Reseed; // одноразовый грубый захват частоты заново end; FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции Changed(rfBeaconLock); end; procedure TRadioController.SetBeaconLock(On_: Boolean); // Один концепт: лок = декодер + трим LOError. Включение запускает декодер маяка; // пользователь кликает по маяку (BeaconSeedAtHz) для наведения. Когда декодер // солидно залочен, ServiceBeaconLock стекает его измеренный дрейф в LOError. begin if On_ = FBeaconLockOn then Exit; FBeaconLockOn := On_; if On_ then begin FBeaconCorrCooldown := 0; FBeaconSaveCooldown := 0; FBeaconLocked := False; FBeaconSNRdB := -99.0; FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку) FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку FBeaconTrackHz := FBeaconRefHz; if Assigned(FBeaconDec) then begin FBeaconDec.SetOffsetHz(0); FBeaconDec.SetEnabled(True); // ResetDSP внутри end; end else begin if Assigned(FBeaconDec) then FBeaconDec.SetEnabled(False); FBeaconDecHz := 0.0; PersistBeaconCalibration; // финальный flush LOError на диск end; Changed(rfBeaconLock); end; procedure TRadioController.ServiceBeaconLock; // Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР: // он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его // суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения. // Измеренную АБСОЛЮТНУЮ частоту маяка (наведение + остаток) подтягиваем к опорной, // стекая ошибку в LOError активного XVTR → ретюн LO компенсирует дрейф LNB, весь // даунлинк стоит. Наведение декодера едет feed-forward за маяком (FBeaconDecHz), // так что внутренний NCO держит маяк декодированным сквозь плавный ретюн (нетто // baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock). var S: TBeaconScope; residHz, measured, e, dLock: Double; begin if not FBeaconLockOn then Exit; if not Assigned(FBeaconDec) then Exit; // Держим наведение декодера через АКТУАЛЬНЫЙ FCenterFreq (панорама/CTUN не ломают // снос). До клика (FBeaconDecHz=0) — DC; acquisition попробует поймать у центра. if FBeaconDecHz > 0 then FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq) else FBeaconDec.SetOffsetHz(0); if FCurrentXvtr < 0 then Exit; // трим LOError только в XVTR-режиме if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown); FBeaconDec.GetScope(S); FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB); // Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания). if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False; // Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось // декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN). residHz := BEACON_RESID_SIGN * (S.ResidHz + S.CarFreqHz); if FBeaconDecHz > 0 then begin measured := FBeaconDecHz + residHz; // абсолютная display-частота маяка FBeaconTrackHz := measured; // для оранжевого маркера end else measured := FBeaconTrackHz; // Коррекция: не каждый тик (ретюн LO имеет латентность пайплайна). После коррекции // ждём BEACON_SETTLE_TICKS. Только при солидном локе и после наведения кликом. if FBeaconCorrCooldown > 0 then Dec(FBeaconCorrCooldown) else if FBeaconLocked and (FBeaconDecHz > 0) then begin e := measured - FBeaconRefHz; if Abs(e) > BEACON_DEADBAND_HZ then begin dLock := BEACON_GAIN * e; if dLock > BEACON_MAX_STEP_HZ then dLock := BEACON_MAX_STEP_HZ; if dLock < -BEACON_MAX_STEP_HZ then dLock := -BEACON_MAX_STEP_HZ; ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение // Маяк уедет на −dLock (display); ведём наведение туда же feed-forward, чтобы // baseband декодера не дёрнулся и лок не потерялся. FBeaconDecHz := measured - dLock; FBeaconTrackHz := FBeaconDecHz; FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq); 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 := FBeaconSNRdB; // SNR последнего снимка декодера (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; function TRadioController.BeaconDecodeFreqHz: Double; begin Result := FBeaconDecHz; end; procedure TRadioController.SetBeaconDecodeInvert(On_: Boolean); begin FBeaconDecInvert := On_; if Assigned(FBeaconDec) then FBeaconDec.SetInvert(On_); Changed(rfBeaconLock); end; function TRadioController.BeaconDecodeInvert: Boolean; begin Result := FBeaconDecInvert; end; function TRadioController.GetBeaconScope(out S: TBeaconScope): Boolean; begin Result := Assigned(FBeaconDec); if Result then FBeaconDec.GetScope(S) else FillChar(S, SizeOf(S), 0); end; function TRadioController.GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean; begin Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors); 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.RxMuteOnTx := FRxMuteOnTx; 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 FVfoLock then Exit; // VFO заблокирован — перестройку игнорируем 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; ApplyWDSPDrive(FDrivePercent); 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; if Pct <= 0 then begin Result := TX_OFF; Exit; end; // 0% → TX практически выкл // power-linear: мощность ∝ drive%. dB = потолок + 20·log10(pct/100); 100% → потолок, // 50% → −6 дБ, 10% → −20 дБ. (Раньше dB-линейно от −89.75 — низкие % улетали в −80 дБ.) Result := FPlutoTxMaxAttDb + 20.0 * Log10(Pct / 100.0); 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.ApplyWDSPDrive(Pct: Integer); // На Pluto мощность задаётся ОДИН раз — железной аттенюацией (PlutoTxAttForDrive), // поэтому цифровой уровень WDSP держим на максимуме (1.0), а не режем по drive%. // HPSDR — как раньше, Pct/100 (реальная мощность там — drive-байт в радио). begin if not (FWDSPReady and Assigned(FDSPEngine)) then Exit; if IsPluto then FDSPEngine.SetDriveLevel(1.0) else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0); 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; // QO-100 / split-LO: авто display-duplex (мониторинг своего downlink при TX). if E.FullDuplexTx and not FDisplayDuplex then SetDuplex(True); 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_; // Применяем сразу: при передаче держим RX-IQ в DSP-цепи (full-duplex монитор — // для QO-100 слышим свой downlink через спутник). SetMOX тоже это выставляет, // но здесь — чтобы тумблер DUP действовал и в момент передачи. if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.KeepRXDuringTX := FDisplayDuplex; Changed(rfDuplex); end; procedure TRadioController.ApplyRxMuteToEngine; begin // Монитор имеет смысл только в full-duplex (KeepRXDuringTX); сам гейт в движке // это учитывает. Здесь просто транслируем инверсию флага RX MUTE. if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.MonitorRXDuringTX := not FRxMuteOnTx; end; procedure TRadioController.SetRxMuteOnTx(On_: Boolean); begin FRxMuteOnTx := On_; ApplyRxMuteToEngine; Changed(rfRxMuteOnTx); 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), XvtrTranslateTX(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 тянется 200–500мс). 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), XvtrTranslateTX(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-уровень) ApplyWDSPDrive(FTXSettings.TUNLevel); 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% ApplyWDSPDrive(FDrivePercent); if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(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.SetSplit(TxOnB: Boolean); // Split: TX-VFO = B (TxOnB) либо A. TX-частота берётся из ActiveTXFreqHz // (он смотрит на FSplitTxB), поэтому достаточно протолкнуть HP-кадр в сеть. // Рендер VFO-дисплеев и подсветки TX-A/TX-B кнопок — через rfActiveVfo. begin if FSplitTxB = TxOnB then Exit; FSplitTxB := TxOnB; PushNetworkState; Changed(rfActiveVfo); end; procedure TRadioController.SetVfoLock(On_: Boolean); // VFO Lock: пока включён, относительная перестройка (TuneActiveBy — энкодер VFO // панели, CAT UP/DN) игнорируется. Подсветка — индикатор aiVFOLock (#11). begin if FVfoLock = On_ then Exit; FVfoLock := On_; Changed(rfVfoLock); 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), XvtrTranslateTX(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; // QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено. ApplyRxMuteToEngine; 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), XvtrTranslateTX(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; FLastPlutoSocTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE; FPlutoTeleTick := 0; FPlutoTempBad := 0; Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP) end; function TRadioController.ResolveDevice(const IP: string): THPSDRDevice; // discovered→saved по IP/URI: восстанавливает Kind/URI/Serial/BoardType/MAC, // иначе простой HPSDR по IP. См. одноимённую обёртку в GUI (TMainForm). var i: Integer; dd: TDiscoveredDevice; sd: TSavedDevice; begin FillChar(Result, SizeOf(Result), 0); Result.Kind := bkHPSDR; Result.IPAddress := IP; i := FDeviceStore.FindDiscoveredByIP(IP); if i >= 0 then begin dd := FDeviceStore.Discovered(i); Result.Kind := dd.Kind; Result.URI := dd.URI; Result.Serial := dd.Serial; Result.BoardType := dd.BoardType; Move(dd.MAC[0], Result.MAC[0], 6); Exit; end; for i := 0 to FDeviceStore.SavedCount - 1 do begin sd := FDeviceStore.Saved(i); if SameText(sd.IPAddress, IP) or SameText(sd.URI, IP) then begin Result.Kind := sd.Kind; Result.URI := sd.URI; Result.Serial := sd.Serial; Result.BoardType := sd.BoardType; Exit; end; end; end; procedure TRadioController.AddSavedDevice(const AName, AAddr: string); // web-dev_add: Pluto по схеме URI ('ip:'/'usb:'/'local:'), иначе HPSDR-IP. // Serial при ручном добавлении неизвестен (допишется при connect/probe). var A: string; begin A := Trim(AAddr); if A = '' then Exit; if (Pos('ip:', A) = 1) or (Pos('usb:', A) = 1) or (Pos('local:', A) = 1) then FDeviceStore.AddSavedPluto(AName, A, '') else FDeviceStore.AddSaved(AName, A, 0); 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; FRxMuteOnTx := FLoadedGlobal.RxMuteOnTx; // Настройки дисплея/сетки + 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; // Локальная звуковая карта (динамик RX / микрофон TX). В демоне FLocalAudio= // False: карту не открываем (иначе headless-хост хватает ALSA-устройство и // играет звук). Web-аудио идёт через OnAudioConsume и от FAudioOut не зависит. if FLocalAudio then begin // Аудио устройство вывода 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; 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.SampleRatePresets: TBackendRateArray; begin Result := BackendCaps.RatePresets; end; function TRadioController.BandPlan: TBandPlanArray; begin Result := BuildBandPlan(IsPluto); end; function TRadioController.IsPluto: Boolean; begin Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto); end; function TRadioController.InQO100: Boolean; // True — активен full-duplex транспондер (QO-100): Pluto + выбран XVTR-слот с // флагом FullDuplexTx. Гейт для UI-элементов, осмысленных только в этом режиме // (beacon-lock, RX MUTE self-monitor). begin Result := IsPluto and (FCurrentXvtr >= 0) and FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx; 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, s, 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, s, 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; // Температура Zynq SoC (xadc): стабильный локальный датчик (не подвержен мусору // мид-конверсии AD9361) — достаточно окна правдоподобия без slew-limit. if (s > TELEMETRY_NONE) and (s >= TEMP_MIN_C) and (s <= TEMP_MAX_C) then FLastPlutoSocTempC := s; // 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.SeedPlutoProbeFromSaved; // Заполняет probe-список URI всех сохранённых Pluto (mDNS нет → сетевой Pluto // находится только прямым probe его URI). Демон зовёт это перед Discover; GUI // добавляет ещё и ручной IP из диалога. Дедупликация по URI. var URIs: array of string; i, k: Integer; U: string; begin SetLength(URIs, 0); for i := 0 to FDeviceStore.SavedCount - 1 do if FDeviceStore.Saved(i).Kind = bkPluto then begin U := FDeviceStore.Saved(i).URI; if U = '' then Continue; for k := 0 to High(URIs) do if SameText(URIs[k], U) then begin U := ''; Break; end; if U = '' then Continue; SetLength(URIs, Length(URIs) + 1); URIs[High(URIs)] := U; end; SetPlutoProbeURIs(URIs); 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.