mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
The headless daemon (ewsdrd) only ever built HPSDR devices, so Pluto could not be used: backend is chosen by Dev.Kind and Pluto opens by URI, neither of which the daemon propagated. Also local audio played on the host and web Discover never probed network Plutos. Shared, backend-agnostic helpers in TRadioController (used by GUI + daemon): - ResolveDevice(IP): discovered->saved lookup restoring Kind/URI/Serial/ BoardType/MAC. MainForm.ResolveDevice now delegates here. - AddSavedDevice(name, addr): web dev_add saves Pluto when addr has a URI scheme (ip:/usb:/local:), else HPSDR. Both web hosts use it. - SeedPlutoProbeFromSaved: seed network-probe URIs from saved Plutos (no mDNS -> a network Pluto is only found by direct URI probe). - LocalAudioEnabled flag (GUI=True): when False the local sound card is neither opened nor written; RX audio goes only to web (OnAudioConsume). DeviceStore.AutoStartDevice(out Dev): full autostart record (Kind/URI/ Serial) so a saved Pluto (URI-addressed, empty IPAddress on USB) can autostart. Daemon (ewsdrd.lpr): LocalAudioEnabled:=False; SyncConnect via ResolveDevice; autostart via AutoStartDevice; SyncDiscover seeds probe URIs then Discover; SyncDevAdd via AddSavedDevice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3300 lines
151 KiB
ObjectPascal
3300 lines
151 KiB
ObjectPascal
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,
|
||
rfMode, rfFilter, rfFilterBW,
|
||
rfAGCMode, rfAGCTop,
|
||
rfRxGain, // Pluto: hw-gain (auto/manual dB)
|
||
rfVolume, rfMute, rfDrive, rfAtten,
|
||
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
|
||
rfCenterFreq, rfSpan, rfSampleRate,
|
||
rfCTun, rfDuplex,
|
||
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
||
rfNR, rfNB, rfSNB, rfANF,
|
||
rfWfAGC, rfWfNF, rfDither, rfRandom,
|
||
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
|
||
rfFMStep, rfFMStepIdx, rfFMRpt,
|
||
rfChannel,
|
||
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
|
||
rfADCOverload, // ADC overload (рендер индикатора)
|
||
rfBeaconLock, // QO-100 beacon lock: статус/коррекция
|
||
rfDevice, // подключённое устройство сменилось
|
||
rfDeviceList // список discovered устройств обновился
|
||
);
|
||
|
||
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
|
||
|
||
// Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize).
|
||
TUIInvokeEvent = procedure(M: TThreadMethod) of object;
|
||
|
||
// Перехват RX-аудио потребителем (web-адаптер): вернуть True, если аудио
|
||
// отправлено клиенту и НЕ должно идти в локальный AudioOut. Вызывается из
|
||
// DSP-потока (как сам OnAudioReady) — реализация должна быть thread-safe.
|
||
TAudioConsumeEvent = function(const Left, Right: array of Single;
|
||
Count: Integer): Boolean of object;
|
||
|
||
// Сырьё спектра/водопада наружу (фронтенды рисуют). Вызывается из DSP-потока.
|
||
TPixelDataEvent = procedure(const Pixels: array of Single; Count: Integer) of object;
|
||
|
||
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
|
||
TRadioSnapshot = record
|
||
VfoA, VfoB, CenterFreq, SpanHz: Double;
|
||
SampleRate: Integer;
|
||
ActiveVfo: Integer;
|
||
Mode, Filter, FilterBW: Integer;
|
||
AGCMode, AGCTop: Integer;
|
||
RxGainMode: Integer;
|
||
RxGainDb: Integer;
|
||
Volume, Drive, Atten: Integer;
|
||
Band, Xvtr: Integer;
|
||
Muted, CTun, Duplex: Boolean;
|
||
Running, Connected, Transmitting, Tuning: Boolean;
|
||
NRMode, NBMode: Integer;
|
||
SNB, ANF: Boolean;
|
||
WfAGC, WfNF: Boolean;
|
||
FMStepOn: Boolean;
|
||
FMStepIdx: Integer;
|
||
FMCTCSSOn: Boolean;
|
||
FMCTCSSToneIdx: Integer;
|
||
FMSQOn: Boolean;
|
||
FMSQLevel: Integer;
|
||
FMRptDir: Integer;
|
||
SMeter, FwdW, SWR, SupplyV, SupplyA: Double;
|
||
PLLLock: Boolean;
|
||
end;
|
||
|
||
{ TRadioController }
|
||
TRadioController = class
|
||
private
|
||
FOnInvoke: TUIInvokeEvent;
|
||
FOnStateChanged: TRadioStateEvent;
|
||
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
||
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
||
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;
|
||
FDisplayDuplex: Boolean;
|
||
FHWPTTActive: Boolean;
|
||
FHWPTTStartedTX: Boolean;
|
||
FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента)
|
||
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
|
||
FSendAudioToRadio: Boolean;
|
||
FAudioOutDevName: string;
|
||
FAudioInDevName: string;
|
||
FDevConnected: Boolean;
|
||
FDiscoverNoneFound: Boolean; // discovery завершился без устройств (для рендера)
|
||
|
||
// ---- DSP-фильтры шума / водопад ----
|
||
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
|
||
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FDitherEnabled: Boolean;
|
||
FRandomEnabled: Boolean;
|
||
|
||
// ---- Display state (persist в TGlobalSettings; рендерится FSpecView/Wideband) ----
|
||
FShowSpectrum: Boolean;
|
||
FShowWaterfall: Boolean;
|
||
FShowWideband: Boolean;
|
||
FWidebandFill: Boolean;
|
||
FSpectrumFill: Boolean;
|
||
FWfManualHigh: Double;
|
||
FWfManualLow: Double;
|
||
FWfAGCOffset: Double;
|
||
FSpecRefLevel: Double;
|
||
FSpecRange: Double;
|
||
FSpecGridStep: Double;
|
||
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
|
||
FTXSpecRange: Double;
|
||
FTXSpecGridStep: Double;
|
||
|
||
// ---- FM ----
|
||
FFMDeviation: Double;
|
||
FFMCTCSSOn: Boolean;
|
||
FFMCTCSSToneIdx: Integer;
|
||
FFMSQOn: Boolean;
|
||
FFMSQLevel: Integer;
|
||
FFMStepOn: Boolean;
|
||
FFMStepIdx: Integer;
|
||
FFMRptDir: Integer;
|
||
FFMRptOffsetHz: Double;
|
||
FFMRptAutoActive: Boolean;
|
||
FFMCTCSSAutoActive: Boolean;
|
||
|
||
// ---- Конфиги / устройство (per-MAC) ----
|
||
FTXSettings: TTXSettings;
|
||
FAlexSettings: TAlexSettings;
|
||
FXvtrSettings: TXvtrSettings;
|
||
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
|
||
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double;
|
||
FDevMAC: array[0..5] of Byte;
|
||
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
|
||
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
|
||
FLoadedGlobal: TGlobalSettings;
|
||
|
||
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
|
||
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
|
||
FLastPLLLock: Boolean;
|
||
// Pluto-телеметрия (нет HP-status): температура чипа °C / RSSI приёмника дБ.
|
||
// Опрашиваются ServicePlutoTelemetry с UI-таймера; TELEMETRY_NONE = нет данных.
|
||
FLastPlutoTempC, FLastPlutoRSSIdB: Double;
|
||
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
|
||
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
|
||
|
||
// ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ----
|
||
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
|
||
FRXStartTime: QWord; // GetTickCount64 момента START
|
||
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
|
||
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
|
||
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
|
||
FLastDDCIndex: Integer; // последний DDC index
|
||
FDDCLastSeq: array[0..6] of LongWord;
|
||
FDDCSeqValid: array[0..6] of Boolean;
|
||
FSeqErrorCount: LongWord;
|
||
FLastSeqErrorDDC: Integer;
|
||
FLastSeqErrorDelta: Int64;
|
||
FSeqOkStreak: Integer;
|
||
FADCOverloadActive: Boolean; // ADC overload (рендерится по rfADCOverload)
|
||
// ---- TX DUC-очередь (накопление IQ от WDSP в пакеты по 240, OnTXIQ) ----
|
||
FDUCPendingI: array[0..239] of Integer;
|
||
FDUCPendingQ: array[0..239] of Integer;
|
||
FDUCPendingCount: Integer;
|
||
// Pending-значения декода HP-статуса (сетевой поток → ApplyHPStatus через
|
||
// Invoke; Synchronize блокирующий и последовательный, гонок нет).
|
||
FHPpFwdW, FHPpSWR, FHPpSupplyV, FHPpSupplyA: Double;
|
||
FHPpPLL, FHPpHWPTT: Boolean;
|
||
FHPpOverload: Byte;
|
||
// Pending discovered-устройство (сетевой поток discovery → ApplyDeviceFound
|
||
// через Invoke; Synchronize блокирующий — устройства приходят по одному).
|
||
FPendingFoundDev: THPSDRDevice;
|
||
FPendingFoundEntry: string;
|
||
// URI для probe сетевых Pluto при discovery (см. SetPlutoProbeURIs).
|
||
FPlutoProbeURIs: array of string;
|
||
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
|
||
// Lifecycle движков: создаёт FNetwork/FDSPEngine/FAudioOut/FAudioIn с
|
||
// заданным sample rate (вызывающий уже определил его из настроек). Колбэки
|
||
// движков навешивает вызывающий (GUI: методы MainForm; демон: свои). Парный
|
||
// FreeEngines закрывает и освобождает их; вызывается из Destroy.
|
||
procedure CreateEngines(ASampleRate: Integer);
|
||
procedure FreeEngines;
|
||
|
||
// ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ----
|
||
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
|
||
// вызывающим (GUI или демон) после CreateEngines.
|
||
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
||
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
|
||
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||
procedure OnMicPacket(const Data: TMicDataPacket);
|
||
function PullSoundCardMic(MaxN: Integer): Integer;
|
||
procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
|
||
procedure OnHPStatus(const Status: THighPriorityStatus);
|
||
procedure ApplyHPStatus; // маршалится из OnHPStatus (сглаживание/overload/HWPTT)
|
||
procedure OnTXIQ(const Buf: array of Double; Count: Integer);
|
||
procedure OnDeviceFound(const Dev: THPSDRDevice); // сетевой поток discovery
|
||
procedure ApplyDeviceFound; // маршалится из OnDeviceFound (store + rfDeviceList)
|
||
procedure DiscoverFinishedNone; // маршалится из discover-потока: устройств нет
|
||
|
||
// Маршалинг команды в поток контроллера (GUI: TThread.Synchronize).
|
||
procedure Invoke(M: TThreadMethod);
|
||
|
||
// Снимок всего состояния (для синхронизации нового фронтенда).
|
||
function GetSnapshot: TRadioSnapshot;
|
||
|
||
// Частотные хелперы — чистые вычисления над состоянием, общие для всех
|
||
// фронтендов и сетевого слоя (Vfo/Mode/Band/CTun/TX/FM-Rpt).
|
||
// XvtrTranslate — переводит видимую частоту в частоту IF-тракта с учётом
|
||
// активного трансвертора (LOOffset/LOError).
|
||
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
||
function XvtrTranslate(VisibleHz: Double): Double;
|
||
// 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 SetWfAGC(On_: Boolean);
|
||
procedure SetWfNF(On_: Boolean);
|
||
|
||
// DSP-фильтры шума
|
||
procedure SetNR(Mode: Integer);
|
||
procedure SetNB(Mode: Integer);
|
||
procedure SetSNB(On_: Boolean);
|
||
procedure SetANF(On_: Boolean);
|
||
|
||
// Передача
|
||
procedure SetMOX(On_: Boolean);
|
||
procedure SetTune(On_: Boolean); // TUN
|
||
|
||
// FM
|
||
procedure SetFMStep(Idx: Integer);
|
||
procedure SetFMCTCSS(On_: Boolean);
|
||
procedure SetFMCTCSSTone(Idx: Integer);
|
||
procedure SetFMSquelch(On_: Boolean);
|
||
procedure SetFMSquelchLevel(V: Integer);
|
||
procedure SetFMRpt(Dir: Integer); // явное направление; снимает авто-флаг
|
||
procedure SetFMRptOffset(Hz: Double); // сдвиг репитера; применяет, если RPT вкл
|
||
procedure UpdateRptAutoState; // авто-MINUS для 2 м под FVfoA
|
||
|
||
// Каналы: применение/деактивация каналов вынесено в TChannelController
|
||
// (отдельный юнит-адаптер поверх контроллера), оркеструет команды ниже.
|
||
|
||
// Жизненный цикл движка / устройства
|
||
procedure StartStop;
|
||
procedure SetRun(On_: Boolean);
|
||
procedure StartRunning(SpectrumWidth: Integer); // Run=1 bring-up: General/DDC/DUC + DSP restore
|
||
procedure StopRunning; // Run=0: graceful-стоп RX-потока + сброс TX/телеметрии
|
||
function ConnectDevice(const Dev: THPSDRDevice): Boolean; // Connect + load-by-MAC; рендер через rfDevice
|
||
function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live
|
||
procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless)
|
||
// По 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;
|
||
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
|
||
function IsPluto: Boolean;
|
||
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
|
||
// HPSDR → тип платы; Pluto/LibreSDR → hw_model + серийник (если есть).
|
||
function BoardDisplayName: string;
|
||
// Опрос аппаратной телеметрии Pluto (температура/RSSI) с троттлингом — зовётся
|
||
// с UI-таймера (MeterTimer @10 Гц), как ServiceBeaconLock. No-op для HPSDR.
|
||
// Результат — в FLastPlutoTempC/FLastPlutoRSSIdB (UI читает и форматирует).
|
||
procedure ServicePlutoTelemetry;
|
||
// Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд
|
||
// перед Discover (сохранённые Pluto + ручной ввод IP).
|
||
procedure SetPlutoProbeURIs(const URIs: array of string);
|
||
// Сидирует probe-список из сохранённых Pluto (для бэкенд-агностичного Discover
|
||
// без UI-диалога — демон). GUI добавляет ручной IP поверх в BuildProbeURIs.
|
||
procedure SeedPlutoProbeFromSaved;
|
||
|
||
// ---- События наружу ----
|
||
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
||
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
||
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
|
||
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
||
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
||
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
|
||
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
|
||
// Локальный звук ПК (динамик 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 по умолчанию
|
||
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; 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);
|
||
begin
|
||
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
|
||
end;
|
||
|
||
procedure TRadioController.PushNetworkState;
|
||
// Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние
|
||
// MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг.
|
||
begin
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), 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.AudioOutDevice := FAudioOutDevName;
|
||
Result.AudioInDevice := FAudioInDevName;
|
||
Result.SendAudioToRadio := FSendAudioToRadio;
|
||
end;
|
||
|
||
procedure TRadioController.SaveGlobalSettings;
|
||
begin
|
||
if FDevConnected then FSettings.SaveGlobal(FDevMAC, BuildGlobalSettings);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
|
||
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
|
||
begin
|
||
// --- Режим ---
|
||
FMode := B.Mode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||
|
||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||
FFilter := B.FilterIdx;
|
||
FFilterBW := B.FilterBW;
|
||
SanitizeFilterForMode;
|
||
ApplyModeFilter;
|
||
|
||
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
|
||
FAGCMode := B.AGCMode;
|
||
FAGCTop := B.AGCTop;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
|
||
ApplyAGCToEngine;
|
||
|
||
// --- FM squelch / CTCSS / step / repeater ---
|
||
FFMSQOn := B.FMSQOn;
|
||
FFMSQLevel := B.FMSQLevel;
|
||
FFMCTCSSOn := B.CTCSSOn;
|
||
FFMCTCSSToneIdx := B.CTCSSToneIdx;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
end;
|
||
FFMStepOn := B.FMStepOn;
|
||
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
|
||
FFMRptDir := B.FMRptDir;
|
||
FFMRptOffsetHz := B.FMRptOffsetHz;
|
||
if FFMRptOffsetHz <= 0 then FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA);
|
||
|
||
// --- CTUN ---
|
||
FCTun := B.CTun;
|
||
if FWDSPReady and Assigned(FDSPEngine) and not FCTun then FDSPEngine.SetShift(0.0);
|
||
|
||
// --- Span (sample rate глобальный, не per-band — лишь ресинк) ---
|
||
FSpanHz := FSampleRate;
|
||
|
||
// --- Рендер: единичные события (UI рисует через OnControllerState) ---
|
||
Changed(rfBand);
|
||
Changed(rfMode); // rfMode-рендер зовёт UpdateFilterButtons (FM-панели)
|
||
Changed(rfFilter);
|
||
Changed(rfAGCMode);
|
||
Changed(rfAGCTop);
|
||
Changed(rfFMSQ);
|
||
Changed(rfFMSQLevel);
|
||
Changed(rfFMCTCSS);
|
||
Changed(rfFMCTCSSTone);
|
||
Changed(rfFMStep); // кнопка вкл/выкл STEP
|
||
Changed(rfFMStepIdx); // дропдаун шага + web + grid
|
||
Changed(rfFMRpt);
|
||
Changed(rfCTun);
|
||
end;
|
||
|
||
procedure TRadioController.RestoreBand(BandIdx: Integer);
|
||
// Загружает состояние диапазона из кэша, применяет к DSP, делает финальную
|
||
// перестройку приёмника на активный VFO и шлёт события рендера. Полная команда —
|
||
// зовётся напрямую desktop/web/channel/connect-restore.
|
||
var B: TBandSettings;
|
||
begin
|
||
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
|
||
Changed(rfBandRestore); // явная смена диапазона → UI сбросит Wf-avg буфер
|
||
B := FBandCache[BandIdx];
|
||
FCurrentBand := BandIdx;
|
||
|
||
// Pluto: кэш диапазона может нести HF-частоту (дефолты Settings). Если VfoA не
|
||
// попадает в VHF-слот — берём частоту по умолчанию из плана.
|
||
if IsPluto and (FreqToBandIdx(B.VfoA, True) <> BandIdx) then
|
||
begin
|
||
B.VfoA := BandDefaultFreq(BandIdx, True);
|
||
B.VfoB := B.VfoA;
|
||
end;
|
||
|
||
ApplyBandDSP(B);
|
||
|
||
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
|
||
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
|
||
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
|
||
// текущий span — иначе (сменился sample rate / битый конфиг) центрируем на VFO.
|
||
// ApplyTuneCore в SetVfoA при необходимости сам прокрутит центр к краю.
|
||
if FCTun and (B.CenterHz > 0) and (Abs(B.VfoA - B.CenterHz) < FSpanHz / 2) then
|
||
FCenterFreq := B.CenterHz
|
||
else
|
||
FCenterFreq := B.VfoA;
|
||
FVfoB := B.VfoB;
|
||
FVfoA := B.VfoA;
|
||
FDriveLevel := CalcDriveByte;
|
||
|
||
Changed(rfVfoB);
|
||
// Финальная перестройка приёмника на АКТИВНЫЙ VFO. VFO A — через SetVfoA
|
||
// (хук OnAfterTune несёт канальную оркестрацию); VFO B — через SetVfoB.
|
||
// Иначе смена диапазона при активном B центрировала бы приёмник на A.
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA)
|
||
else SetVfoB(FVfoB);
|
||
end;
|
||
|
||
function TRadioController.ActiveVfoHz: Double;
|
||
begin
|
||
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyTuneCore(VfoHz: Double);
|
||
// Общее ядро перестройки приёмника на частоту АКТИВНОГО VFO (A или B).
|
||
// CTUN OFF: DDC = VfoHz, сдвиг 0.
|
||
// CTUN ON: DDC фиксирован, демодулятор сдвигаем на VfoHz-FCenterFreq;
|
||
// если полоса фильтра вышла за край дисплея — прокручиваем центр.
|
||
// Ставит FCenterFreq / shift / FLastVfoScrolled.
|
||
var
|
||
Offset, FiltLo, FiltHi, HalfSpan: Double;
|
||
begin
|
||
FLastVfoScrolled := False;
|
||
if not FCTun then
|
||
begin
|
||
FCenterFreq := VfoHz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
end
|
||
else
|
||
begin
|
||
Offset := VfoHz - FCenterFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
|
||
HalfSpan := FSpanHz / 2;
|
||
case FMode of
|
||
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
|
||
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
|
||
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
|
||
end;
|
||
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
|
||
begin
|
||
if FiltHi > HalfSpan then
|
||
FCenterFreq := VfoHz - HalfSpan * 0.5
|
||
else
|
||
FCenterFreq := VfoHz + HalfSpan * 0.5;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(VfoHz - FCenterFreq);
|
||
FLastVfoScrolled := True;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoA(Hz: Double);
|
||
// Перестройка VFO A: XVTR-клэмп, CTUN ядро, детект диапазона, FM авто-RPT,
|
||
// проталкивание в сеть. Рендер — в UI через OnControllerState(rfVfoA/rfBand).
|
||
// Канальную оркестрацию (авто-CTCSS, pre-channel drive) делает UI-путь.
|
||
var
|
||
NewFreq: Int64;
|
||
BandIdx: Integer;
|
||
begin
|
||
NewFreq := Round(Hz);
|
||
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
|
||
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
|
||
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
|
||
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
|
||
end;
|
||
FVfoA := NewFreq;
|
||
ApplyTuneCore(FVfoA);
|
||
|
||
// Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band,
|
||
// FreqToBandIdx вернёт -1 для VHF/UHF).
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
FCurrentBand := BandIdx;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
end;
|
||
|
||
// Авто-MINUS в 2 м репитерном сегменте (само снимает при уходе из сегмента).
|
||
if FMode = MODE_FM then UpdateRptAutoState;
|
||
|
||
PushNetworkState;
|
||
Changed(rfVfoA);
|
||
// Канальная оркестрация — ПОСЛЕ рендера rfVfoA (вне его try/finally-гарда), без
|
||
// ре-ентранси: OnVfoTuned зовёт команды (SetDrive/SetFMRpt), которым нужен
|
||
// чистый верхний уровень OnControllerState.
|
||
if Assigned(FOnAfterTune) then FOnAfterTune;
|
||
end;
|
||
|
||
procedure TRadioController.StoreVfoA(Hz: Double);
|
||
// Сохранить частоту VFO A БЕЗ перестройки приёмника (когда активен B — слушаем B,
|
||
// A только хранится/отображается). Симметрично «неактивной» ветке SetVfoB.
|
||
// Рендер FreqDispA — через rfVfoA (спектр он не трогает, т.к. SyncSpecViewFreq
|
||
// держит активный VFO B); смена band-кнопки — через rfBand.
|
||
var BandIdx: Integer;
|
||
begin
|
||
FVfoA := Hz;
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
FCurrentBand := BandIdx;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
end;
|
||
Changed(rfVfoA);
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoB(Hz: Double);
|
||
// VFO B всегда сохраняется. Если B активен — перестраиваем приёмник через
|
||
// ApplyTuneCore (с учётом CTUN), детектим диапазон и толкаем в сеть. Если B
|
||
// неактивен (split, слушаем A) — только сохраняем частоту (для TX).
|
||
// Рендер FreqDispB/спектра — в UI через OnControllerState(rfVfoB).
|
||
var
|
||
BandIdx: Integer;
|
||
NewFreq: Int64;
|
||
begin
|
||
NewFreq := Round(Hz);
|
||
// Клэмп в пределах XVTR-band (как в SetVfoA) — VFO B тоже не уходит за край.
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
|
||
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
|
||
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
|
||
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
|
||
end;
|
||
FVfoB := NewFreq;
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
ApplyTuneCore(FVfoB);
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoB, IsPluto);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
FCurrentBand := BandIdx;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
end;
|
||
PushNetworkState;
|
||
end;
|
||
Changed(rfVfoB);
|
||
end;
|
||
|
||
procedure TRadioController.SetActiveVfo(Idx: Integer);
|
||
// Переключение активного (RX) VFO с перестройкой приёмника на его частоту.
|
||
// Bandstack: если активируемый VFO стоит на ДРУГОМ ham-бэнде — сохраняем
|
||
// покидаемый бэнд и восстанавливаем DSP-настройки нового (mode/filter/AGC/CTUN/
|
||
// FM). Частоты обоих VFO при этом НЕ трогаем — каждый VFO несёт свой диапазон.
|
||
// CTUN (того же бэнда): если частота нового VFO видна в окне — только shift
|
||
// (DDC/сеть не трогаем); иначе CTUN сбрасывается и центрируемся на частоте.
|
||
// Рендер (дисплеи/спектр/band/CTUN-кнопка) — в UI через OnControllerState.
|
||
var
|
||
ActiveFreq: Double;
|
||
HalfSpan, Offset: Double;
|
||
FreqVisible, NeedPush: Boolean;
|
||
NewBand: Integer;
|
||
begin
|
||
FActiveVfo := Idx;
|
||
ActiveFreq := ActiveVfoHz;
|
||
NeedPush := False;
|
||
|
||
// --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд ---
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
|
||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||
begin
|
||
SaveCurrentBand; // сохранить покидаемый бэнд
|
||
FCurrentBand := NewBand;
|
||
ApplyBandDSP(FBandCache[NewBand]); // mode/filter/AGC/CTUN/FM + рендер
|
||
FCenterFreq := ActiveFreq; // центр на активную (CTUN центрируется тут)
|
||
FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда
|
||
NeedPush := True; // DDC-центр сменился → толкнуть в сеть
|
||
end;
|
||
end;
|
||
|
||
if FCTun then
|
||
begin
|
||
HalfSpan := FSpanHz / 2;
|
||
Offset := ActiveFreq - FCenterFreq;
|
||
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
|
||
if FreqVisible then
|
||
begin
|
||
// Частота видна — просто сдвигаем демодулятор.
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
|
||
end
|
||
else
|
||
begin
|
||
// Частота за окном — сбрасываем CTUN и центрируемся на ней.
|
||
FCTun := False;
|
||
FBandCache[FCurrentBand].CTun := False;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
NeedPush := True;
|
||
Changed(rfCTun);
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
NeedPush := True;
|
||
end;
|
||
|
||
if NeedPush then PushNetworkState;
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
|
||
begin
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
|
||
else SetVfoB(FVfoB + DeltaHz);
|
||
end;
|
||
|
||
procedure TRadioController.SetCenter(Hz: Double);
|
||
// CTUN-скролл окна просмотра: двигаем DDC-центр БЕЗ смены VFO. Демодулятор
|
||
// сдвигаем = активный VFO − новый центр; полный HP-кадр в радио. Рендер
|
||
// (ruler/спектр) — в UI через OnControllerState(rfCenterFreq).
|
||
// Точки входа: десктопный CTUN-drag и web (SyncWebCenter).
|
||
begin
|
||
FCenterFreq := Hz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
|
||
PushNetworkState;
|
||
Changed(rfCenterFreq);
|
||
end;
|
||
|
||
procedure TRadioController.VfoSwap;
|
||
// Меняем VFO A/B местами и перестраиваем приёмник на новую частоту активного
|
||
// VFO (центр=частота, shift 0 — CTUN-оффсет после свопа теряет смысл).
|
||
var
|
||
T: Double;
|
||
ActiveFreq: Double;
|
||
NewBand: Integer;
|
||
begin
|
||
T := FVfoA; FVfoA := FVfoB; FVfoB := T;
|
||
ActiveFreq := ActiveVfoHz;
|
||
// Bandstack: если активный после свопа VFO на другом ham-бэнде — сохраняем
|
||
// покидаемый бэнд и восстанавливаем DSP нового (mode/filter/AGC/CTUN/FM), как
|
||
// SetActiveVfo. Иначе своп на другой диапазон оставлял бы старые mode/фильтр/AGC
|
||
// при подсвеченной кнопке нового бэнда (рассинхрон).
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
|
||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||
begin
|
||
SaveCurrentBand;
|
||
FCurrentBand := NewBand;
|
||
ApplyBandDSP(FBandCache[NewBand]); // несёт Changed(rfBand) + рендер DSP-полей
|
||
FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда
|
||
end;
|
||
end;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
PushNetworkState;
|
||
Changed(rfVfoA);
|
||
Changed(rfVfoB);
|
||
end;
|
||
|
||
procedure TRadioController.VfoACopyB; // A>B: частота A копируется в B
|
||
begin
|
||
SetVfoB(FVfoA); // сохраняет B:=A, перестраивает если B активен, рендер rfVfoB
|
||
end;
|
||
|
||
procedure TRadioController.VfoBCopyA; // B>A: частота B копируется в A
|
||
begin
|
||
FVfoA := FVfoB;
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA) // A активен — перестраиваем приёмник
|
||
else Changed(rfVfoA); // A неактивен — только рендер дисплея
|
||
end;
|
||
|
||
procedure TRadioController.SetMode(M: Integer);
|
||
begin
|
||
FMode := M;
|
||
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
||
FBandCache[FCurrentBand].Mode := FMode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||
end;
|
||
ApplyModeFilter;
|
||
Changed(rfMode);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||
begin
|
||
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
|
||
// Idx может прийти из CAT/web без проверки; FM-таблицы всего на 2 элемента.
|
||
if FMode = MODE_FM then Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1)
|
||
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
||
FFilter := Idx;
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[Idx];
|
||
2: FFilterBW := FILT_DSB_BW[Idx];
|
||
3, 4: FFilterBW := FILT_CW_BW[Idx];
|
||
MODE_FM:
|
||
begin
|
||
FFilterBW := FILT_FM_BW[Idx];
|
||
FFMDeviation := FILT_FM_DEV[Idx];
|
||
end;
|
||
else FFilterBW := FILT_AM_BW[Idx];
|
||
end;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
ApplyModeFilter;
|
||
Changed(rfFilter);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterBW(BWHz: Integer);
|
||
// Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а
|
||
// FFilter подбираем ближайшим пресетом — только для подсветки кнопки.
|
||
var
|
||
BW_arr: array[0..FILT_COUNT-1] of Integer;
|
||
best, dist, i: Integer;
|
||
begin
|
||
FFilterBW := BWHz;
|
||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
|
||
case FMode of
|
||
0, 1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
||
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
|
||
3, 4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
||
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
|
||
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
||
end;
|
||
best := 0; dist := MaxInt;
|
||
for i := 0 to FILT_COUNT-1 do
|
||
if Abs(BW_arr[i] - BWHz) < dist then
|
||
begin
|
||
dist := Abs(BW_arr[i] - BWHz);
|
||
best := i;
|
||
end;
|
||
FFilter := best;
|
||
if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best];
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
ApplyModeFilter;
|
||
Changed(rfFilter);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyAGCToEngine;
|
||
const
|
||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для
|
||
// всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
||
FDSPEngine.SetAGC(AGCModes[FAGCMode], 50.0);
|
||
end;
|
||
|
||
procedure TRadioController.SetAGCMode(M: Integer);
|
||
begin
|
||
if (M < 0) or (M > 4) then Exit;
|
||
FAGCMode := M;
|
||
ApplyAGCToEngine;
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
Changed(rfAGCMode);
|
||
end;
|
||
|
||
procedure TRadioController.SetAGCTop(DB: Integer);
|
||
begin
|
||
if DB < 20 then DB := 20 else if DB > 120 then DB := 120;
|
||
FAGCTop := DB;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
|
||
ApplyAGCToEngine; // ре-применяем режим с новым уровнем
|
||
FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
Changed(rfAGCTop);
|
||
end;
|
||
|
||
procedure TRadioController.AGCTopBy(Delta: Integer);
|
||
begin SetAGCTop(FAGCTop + Delta); end;
|
||
|
||
procedure TRadioController.ApplyRxGainToBackend;
|
||
begin
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
FNetwork.SetRxGain(FRxGainMode, FRxGainDb);
|
||
end;
|
||
|
||
procedure TRadioController.SetRxGainMode(M: Integer);
|
||
begin
|
||
if M < 0 then M := 0 else if M > 3 then M := 3;
|
||
FRxGainMode := M;
|
||
ApplyRxGainToBackend;
|
||
Changed(rfRxGain);
|
||
end;
|
||
|
||
procedure TRadioController.SetRxGain(Db: Integer);
|
||
begin
|
||
if Db < 0 then Db := 0 else if Db > 73 then Db := 73;
|
||
FRxGainDb := Db;
|
||
// Ручная установка усиления подразумевает manual-режим (0).
|
||
FRxGainMode := 0;
|
||
ApplyRxGainToBackend;
|
||
Changed(rfRxGain);
|
||
end;
|
||
|
||
procedure TRadioController.RxGainBy(Delta: Integer);
|
||
begin SetRxGain(FRxGainDb + Delta); end;
|
||
|
||
procedure TRadioController.SetVolume(V: Integer);
|
||
begin
|
||
if V < 0 then V := 0 else if V > 100 then V := 100;
|
||
FVolume := V;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(FVolume / 100.0);
|
||
Changed(rfVolume);
|
||
end;
|
||
|
||
procedure TRadioController.VolumeBy(Delta: Integer);
|
||
begin SetVolume(FVolume + Delta); end;
|
||
|
||
procedure TRadioController.SetMute(On_: Boolean);
|
||
begin
|
||
FMuted := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMute(FMuted);
|
||
Changed(rfMute);
|
||
end;
|
||
|
||
procedure TRadioController.ToggleMute;
|
||
begin SetMute(not FMuted); end;
|
||
|
||
procedure TRadioController.SetDrive(V: Integer);
|
||
begin
|
||
if V < 0 then V := 0 else if V > 100 then V := 100;
|
||
FDrivePercent := V;
|
||
FDriveLevel := CalcDriveByte;
|
||
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.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.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;
|
||
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; 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;
|
||
// Настройки дисплея/сетки + 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.IsPluto: Boolean;
|
||
begin
|
||
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
|
||
end;
|
||
|
||
function TRadioController.BoardDisplayName: string;
|
||
var
|
||
Dev: THPSDRDevice;
|
||
begin
|
||
if FNetwork = nil then Exit('');
|
||
Dev := FNetwork.Device;
|
||
if Dev.Kind = bkPluto then
|
||
begin
|
||
Result := PlutoModelName(Dev.Model);
|
||
// Серийник дописываем, только если это реальный hw_serial, а не URI-fallback
|
||
// ('usb:1.5.5' / 'ip:…'), который PlutoBackend кладёт при отсутствии серийника.
|
||
if (Dev.Serial <> '') and (Pos('usb:', Dev.Serial) = 0)
|
||
and (Pos('ip:', Dev.Serial) = 0) then
|
||
Result := Result + ' SN:' + Dev.Serial;
|
||
end
|
||
else
|
||
Result := BoardTypeName(Dev.BoardType);
|
||
end;
|
||
|
||
procedure TRadioController.ServicePlutoTelemetry;
|
||
const
|
||
POLL_MS = 500; // опрос ~2 Гц: чтение attr — USB round-trip, чаще ни к чему
|
||
TEMP_MIN_C = 0.0; // работающий AD936x всегда теплее окружающей (само-нагрев)
|
||
TEMP_MAX_C = 120.0; // абсолютный потолок (датчик исправен)
|
||
TEMP_STEP_C = 12.0; // макс. физичный шаг между опросами (тепловая инерция чипа)
|
||
TEMP_FORCE = 3; // принять «выброс», если держится N раз подряд (не залипаем)
|
||
var
|
||
t, r: Double;
|
||
now_: QWord;
|
||
begin
|
||
if (FNetwork = nil) or (not FRunning) or (FNetwork.Caps.Kind <> bkPluto) then Exit;
|
||
now_ := GetTickCount64;
|
||
if (FPlutoTeleTick <> 0) and (now_ - FPlutoTeleTick < POLL_MS) then Exit;
|
||
FPlutoTeleTick := now_;
|
||
if not FNetwork.ReadTelemetry(t, r) then Exit;
|
||
|
||
// Температура: датчик AD9361 иногда отдаёт мусор (мид-конверсия / контеншн на
|
||
// USB-контексте с RX-потоком) — наблюдалось редкое -15 °C. Фильтруем: жёсткое
|
||
// окно правдоподобия + slew-limit по тепловой инерции; устойчивый «выброс»
|
||
// (реальный скачок) всё же принимаем через TEMP_FORCE опросов, чтобы не залипнуть.
|
||
if t > TELEMETRY_NONE then
|
||
begin
|
||
if (t >= TEMP_MIN_C) and (t <= TEMP_MAX_C) then
|
||
begin
|
||
if (FLastPlutoTempC <= TELEMETRY_NONE) or (Abs(t - FLastPlutoTempC) <= TEMP_STEP_C) then
|
||
begin
|
||
FLastPlutoTempC := t; FPlutoTempBad := 0;
|
||
end
|
||
else
|
||
begin
|
||
Inc(FPlutoTempBad);
|
||
if FPlutoTempBad >= TEMP_FORCE then
|
||
begin FLastPlutoTempC := t; FPlutoTempBad := 0; end;
|
||
end;
|
||
end;
|
||
// вне окна правдоподобия — молча отбрасываем, держим прошлое валидное значение
|
||
end;
|
||
|
||
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
|
||
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
|
||
|
||
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
|
||
end;
|
||
|
||
procedure TRadioController.SetPlutoProbeURIs(const URIs: array of string);
|
||
var i: Integer;
|
||
begin
|
||
SetLength(FPlutoProbeURIs, Length(URIs));
|
||
for i := 0 to High(URIs) do FPlutoProbeURIs[i] := URIs[i];
|
||
end;
|
||
|
||
procedure TRadioController.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.
|