mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
Паритет с Thetis-Enhanced (Det. Cal. / Volts&Amps правки): - TCalibration в TGlobalSettings (JSON cal_*): модель детектора Alex-моста, per-band множитель FWD/REV 0.5..2.0, множитель напряжения, Voff/Sens датчика тока, офсет S-метра. - AlexDetToWatts: константы Thetis computeAlexFwdPower/computeRefPower по моделям (ANAN-100 / 200D / 7000DLE-Anvelina Pro 3 / 8000DLE-Orion MkII, 6m-спецкейс REV). Дефолт = прежняя формула ANAN-100 (поведение не меняется). - OnHPStatus: FWD и REV через модельную формулу x множитель банда; КСВ = (1+p)/(1-p), p=sqrt(RevW/FwdW) (Thetis) вместо отношения сырых ADC; напряжение x SupplyVoltCal; ток с калибруемыми Voff/Sens. - WDSPEngine.SMeterCalDB: офсет в GetSMeterDBm/GetSliceSMeter (главный S-метр, slice-флаги, web). - Выбор модели в комбо подставляет модельные дефолты датчика тока (Anvelina Pro 3/7000DLE: 340 мВ / 88 мВ/А — Thetis GetDefaultVoltCalibration). - Фиксы: гард FLoading при постройке вкладки (SetItemIndex стреляет OnChange на программном присвоении -> AV на nil-контролах), FPageCalib/FNavCalib в списках SelectPage (вкладка была невидимой). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5493 lines
249 KiB
ObjectPascal
5493 lines
249 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, DMRDecoder,
|
||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||
|
||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||
// Допустимые rate пан-DDC — полная сетка железа Caps.RatePresets (кратные
|
||
// 48 кГц — требование движка), см. PanRateAllowed. Дефолт/фолбэк — 384 кГц.
|
||
|
||
type
|
||
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
||
TBeaconState = (bsIdle, // лок выключен
|
||
bsSearch, // включён, маяк ещё не подтверждён (низкий prominence)
|
||
bsLock); // маяк уверенно виден и сопровождается
|
||
|
||
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
||
TRadioField = (
|
||
rfVfoA, rfVfoB, rfActiveVfo, rfVfoLock,
|
||
rfMode, rfFilter, rfFilterBW,
|
||
rfAGCMode, rfAGCTop,
|
||
rfRxGain, // Pluto: hw-gain (auto/manual dB)
|
||
rfVolume, rfMute, rfDrive, rfAtten,
|
||
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
|
||
rfCenterFreq, rfSpan, rfSampleRate, rfZoom,
|
||
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
|
||
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
||
rfPureSignal, rfTwoTone, // PureSignal вкл/статус; 2TON вкл/выкл
|
||
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 = class;
|
||
|
||
// Event adapter: every DMR slice owns a decoder, while its asynchronous
|
||
// audio callback still has to retain the originating logical slice id.
|
||
TDMRSliceDecoder = class
|
||
private
|
||
FOwner: TRadioController;
|
||
FSliceId: Integer;
|
||
FDecoder: TDMRDecoder;
|
||
procedure AudioReady(const PCM: array of Single; Count: Integer);
|
||
public
|
||
constructor Create(AOwner: TRadioController; ASliceId: Integer);
|
||
destructor Destroy; override;
|
||
procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
|
||
procedure GetStatus(out S: TDMRStatus);
|
||
end;
|
||
|
||
// Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом
|
||
// (в TWDSPEngine) + собственный аудио-выход со своей звуковухой.
|
||
TCtrlSlice = record
|
||
Active: Boolean;
|
||
Id: Integer; // = id слайса в TWDSPEngine
|
||
Letter: Char; // буква флага ('A'..'F'), пока для UI
|
||
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
|
||
TargetHz: Double; // абсолютная частота слайса
|
||
Mode: Integer;
|
||
FilterLo: Integer;
|
||
FilterHi: Integer;
|
||
AGC: TWDSPAGCMode;
|
||
Volume: Double; // 0..1
|
||
Muted: Boolean;
|
||
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
|
||
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
||
FMSQLevel: Integer; // порог squelch 0..100
|
||
DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
|
||
DevName: string;
|
||
Audio: TAudioOutput; // владеет; свой поток вывода
|
||
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
|
||
end;
|
||
|
||
{ TRadioController }
|
||
TRadioController = class
|
||
private
|
||
FOnInvoke: TUIInvokeEvent;
|
||
FOnStateChanged: TRadioStateEvent;
|
||
FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm)
|
||
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
||
FOnBeforeStop: TThreadMethod; // хук перед гашением панов в StopRunning (персист панов)
|
||
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
||
FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False
|
||
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
||
FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
|
||
procedure Changed(Field: TRadioField);
|
||
// --- Мультислайсы ---
|
||
function FindSliceIndex(Id: Integer): Integer; // -1 если нет
|
||
procedure OnSliceAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
procedure OnSliceDemodAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
procedure OnSliceDMRAudioReady(SliceId: Integer;
|
||
const PCM: array of Single; Count: Integer);
|
||
function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче?
|
||
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
||
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
||
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
||
procedure PanZoomStepTo(PanId: Integer; NewZ: Double); // зум с якорем на центре окна
|
||
function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq)
|
||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
||
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
||
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;
|
||
|
||
// ---- Мультислайсы (софт-fan-out) ----
|
||
// Слайс = доп. приёмник (свой WDSP-канал в движке) + своя звуковуха.
|
||
// TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq.
|
||
FSlices: array[0..MAX_SLICES-1] of TCtrlSlice;
|
||
FNextSliceId: Integer; // монотонный генератор id слайсов
|
||
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;
|
||
FZoomFactor: Double; // зум спектра/водопада 0.05..1.0 (1.0 = без зума)
|
||
FPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
||
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
|
||
|
||
// ---- DMR 4FSK receive decoder ----
|
||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||
|
||
// ---- Аудио / TX ----
|
||
FVolume: Integer; // громкость RX
|
||
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
|
||
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;
|
||
// ---- PureSignal ----
|
||
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
|
||
FPSSingleCal: Boolean; // запрошена одиночная калибровка
|
||
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
|
||
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick)
|
||
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
|
||
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
|
||
FPSAASaveSingle: Integer;
|
||
FPSAADeltaDB: Integer;
|
||
FPSInfo: array[0..15] of Integer; // кэш GetPSInfo (UI-поток)
|
||
FPSInfoPrev: array[0..15] of Integer;
|
||
FPSMaxTX: Double; // кэш GetPSMaxTX
|
||
// Feedback-тракт вооружён (сеть DDC0/1 + pscc в движке). Пишет UI-поток
|
||
// (PureSignalTick/PSSetFeedbackPath), читает сетевой (OnDDCIQ).
|
||
FPSFeedbackOn: Boolean;
|
||
FTwoTone: Boolean; // 2TON активен
|
||
FSplitTxB: Boolean;
|
||
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX
|
||
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
|
||
FDisplayDuplex: Boolean;
|
||
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
|
||
// False — слышим свой downlink со спутника при передаче. Кнопка RX MUTE.
|
||
FRxMuteOnTx: Boolean;
|
||
FHWPTTActive: Boolean;
|
||
FHWPTTStartedTX: Boolean;
|
||
FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента)
|
||
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
|
||
FSendAudioToRadio: Boolean;
|
||
FAudioOutDevName: string;
|
||
FAudioInDevName: string;
|
||
FDevConnected: Boolean;
|
||
FDiscoverNoneFound: Boolean; // discovery завершился без устройств (для рендера)
|
||
|
||
// ---- DSP-фильтры шума / водопад ----
|
||
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
|
||
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FDitherEnabled: Boolean;
|
||
FRandomEnabled: Boolean;
|
||
|
||
// ---- Display state (persist в TGlobalSettings; рендерится FSpecView/Wideband) ----
|
||
FShowSpectrum: Boolean;
|
||
FShowWaterfall: Boolean;
|
||
FShowWideband: Boolean;
|
||
FWidebandFill: Boolean;
|
||
FSpectrumFill: Boolean;
|
||
FWfManualHigh: Double;
|
||
FWfManualLow: Double;
|
||
FWfAGCOffset: Double;
|
||
FWfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
||
FWfGamma: Double; // контраст палитры водопада
|
||
FDisplayPixels: Integer; // потолок точек анализатора
|
||
FWfSpeedDiv: Integer; // делитель скорости водопада (1/2/4/8, max-hold)
|
||
FSpecMSAA: Integer; // MSAA GL-спектра (1=off, 2, 4, 8)
|
||
FSpecRefLevel: Double;
|
||
FSpecRange: Double;
|
||
FSpecGridStep: Double;
|
||
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
|
||
FTXSpecRange: Double;
|
||
FTXSpecGridStep: Double;
|
||
|
||
// ---- FM ----
|
||
FFMDeviation: Double;
|
||
FRawFMDeviation: 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;
|
||
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
|
||
FAnt936x: TAD936xAnt;
|
||
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;
|
||
// Калибровки (детектор мощности/КСВ, напряжение/ток, S-метр) — вкладка
|
||
// Calibration. Читается сетевым потоком в OnHPStatus (double-поля, torn
|
||
// read безопасен как у FPABandCal); менять через SetCalibration.
|
||
FCal: TCalibration;
|
||
FDevMAC: array[0..5] of Byte;
|
||
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
|
||
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
|
||
FLoadedGlobal: TGlobalSettings;
|
||
|
||
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
|
||
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
|
||
FLastPLLLock: Boolean;
|
||
// Pluto-телеметрия (нет HP-status): температура трансивера °C / Zynq SoC °C /
|
||
// RSSI приёмника дБ. Опрашиваются ServicePlutoTelemetry с UI-таймера;
|
||
// TELEMETRY_NONE = нет данных.
|
||
FLastPlutoTempC, FLastPlutoSocTempC, FLastPlutoRSSIdB: Double;
|
||
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
|
||
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
|
||
|
||
// ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ----
|
||
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
|
||
FRXStartTime: QWord; // GetTickCount64 момента START
|
||
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
|
||
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
|
||
// ---- Панадаптеры на аппаратных DDC (этап 3.1) ----
|
||
// PanId 1..MAX_PANS-1 → hw DDC index (-1 = пан не создан); пан 0 = главный
|
||
// тракт (FActiveDDC). FDDCPanMap — обратный маппинг DDC→PanId для роутинга
|
||
// IQ в OnDDCIQ (пишется под FSliceLock-дисциплиной: только UI-поток).
|
||
FPanDDCIdx: array[1..MAX_PANS-1] of Integer;
|
||
FPanFreqHz: array[1..MAX_PANS-1] of Double;
|
||
FPanRateKHz: array[1..MAX_PANS-1] of Word;
|
||
FPanADC: array[1..MAX_PANS-1] of Byte; // ADC-источник пана (3.4)
|
||
FPanZoomF: array[1..MAX_PANS-1] of Double; // зум пана 0..1 (0 = весь span)
|
||
FPanPanSl: array[1..MAX_PANS-1] of Double; // положение окна пана 0..1
|
||
FMainADCSrc: Byte; // ADC главного DDC (пан 0)
|
||
FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
|
||
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 OnDemodAudioReady(const Left, Right: array of Single; Count: Integer);
|
||
procedure OnDMRAudioReady(const PCM: 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;
|
||
// Режим передающего тракта: слайс-источник TX (FTxSliceId) либо главный FMode.
|
||
function ActiveTXMode: Integer;
|
||
// Диапазон для калибровки драйва/антенны TX (банд TX-слайса при кросс-банде).
|
||
function TxBandIdx: Integer;
|
||
// Выбор слайса-источника передачи (0 = главный VFO). Эксклюзивно.
|
||
procedure SetTxSlice(SliceId: Integer);
|
||
property TxSliceId: Integer read FTxSliceId;
|
||
// Складывает коррекцию маяка 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
|
||
function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
|
||
|
||
// ---- Управление диапазонами (состояние + 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 SetRawFMDeviation(Hz: 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);
|
||
// True — сейчас звучит self-monitor даунлинка на передаче (TX + DUP + RX MUTE
|
||
// off). В этом контексте SetVolume/слайдер правят FTxMonVolume, иначе FVolume.
|
||
function MonitoringTX: Boolean;
|
||
// Активная громкость под текущий контекст (RX или TX-монитор) — то, что видит
|
||
// слайдер (rfVolume) и получает движок.
|
||
function ActiveVolume: Integer;
|
||
// Применить активную громкость к движку + синхронизировать слайдер (rfVolume).
|
||
// Зовётся на T/R-переходах и смене RX MUTE / DUP.
|
||
procedure ApplyActiveVolume;
|
||
procedure SetMute(On_: Boolean);
|
||
procedure ToggleMute;
|
||
procedure SetDrive(V: Integer);
|
||
procedure DriveBy(Delta: Integer);
|
||
procedure SetAtten(Idx: Integer);
|
||
|
||
// ---- Мультислайсы (софт-fan-out) ----
|
||
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
|
||
// на устройстве DevIndex (-1=default). Возвращает Id слайса или -1 при отказе
|
||
// (нет слотов / TargetHz вне полосы пана). DevName для персиста/UI.
|
||
// APanId>0 — слайс внутри доп. DDC-пана (тот должен быть создан AddPanDDC).
|
||
function AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer;
|
||
AGC: TWDSPAGCMode; Vol: Double;
|
||
DevIndex: Integer; const DevName: string;
|
||
APanId: Integer = 0): Integer;
|
||
procedure RemoveSlice(Id: Integer);
|
||
procedure SetSliceTarget(Id: Integer; TargetHz: Double);
|
||
procedure SetSliceMode(Id, Mode: Integer);
|
||
procedure SetSliceFilter(Id, Low, High: Integer);
|
||
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
|
||
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
|
||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
|
||
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
||
procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX
|
||
function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана
|
||
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
||
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
|
||
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
|
||
function SliceCount: Integer;
|
||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
||
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
|
||
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
|
||
|
||
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
||
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр в ВИДИМЫХ
|
||
// Гц (как FVfoA/FCenterFreq: при активном XVTR транслируется в IF только
|
||
// на границе с железом, XvtrTranslate), RateKHz ∈ PanRateAllowed (иначе
|
||
// 384), ADCSource — АЦП платы. False = нет слота/бэкенд не умеет/не Running.
|
||
function AddPanDDC(PanId: Integer; FreqHz: Double; RateKHz: Word = 384;
|
||
ADCSource: Byte = 0): Boolean;
|
||
procedure RemovePanDDC(PanId: Integer);
|
||
procedure SetPanDDCFreq(PanId: Integer; FreqHz: Double); // ретюн (HP-пакет)
|
||
// Допустимое окно ВИДИМОЙ частоты пана: активный XVTR — его диапазон,
|
||
// иначе пределы железа (UI-кламп drag/тюна панов N).
|
||
procedure VisibleFreqBounds(out LoHz, HiHz: Double);
|
||
// Вход/выход/перекалибровка XVTR при живых панах: клампит видимые частоты
|
||
// панов в новое окно, перешивает freq-слова DDC (XvtrTranslate), слайсы
|
||
// панов — к краям их полос захвата.
|
||
procedure RefreshPanDDCs;
|
||
// Rate ∈ сетке бэкенда (Caps.RatePresets) и кратен 48 кГц (движок).
|
||
function PanRateAllowed(RateKHz: Word): Boolean;
|
||
// Смена rate живого пана: движок переоткрывает аккумулятор/анализатор/слайсы,
|
||
// DDC реконфигурируется; слайсы вне новой полосы клампятся к её краям.
|
||
function SetPanDDCRate(PanId: Integer; RateKHz: Word): Boolean;
|
||
// Переезд пана на другой аппаратный DDC (пан/слайсы/движок не пересоздаются,
|
||
// меняется только hw-индекс): бывает, что конкретный DDC платы ведёт себя
|
||
// хуже соседей — тогда пан можно просто увести на другой.
|
||
function MovePanDDC(PanId, NewDDC: Integer): Boolean;
|
||
function PanDDCIndex(PanId: Integer): Integer; // hw-индекс DDC пана
|
||
function MaxDDCIndex: Integer; // последний DDC железа
|
||
function DDCFree(DDCIdx: Integer): Boolean; // индекс свободен?
|
||
function PanDDCActive(PanId: Integer): Boolean;
|
||
function PanDDCFreq(PanId: Integer): Double;
|
||
function PanDDCRateKHz(PanId: Integer): Word;
|
||
// ADC-источник пана (3.4). PanId=0 = главный DDC (реконфиг ConfigureDDCs),
|
||
// N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1].
|
||
// Антенные разъёмы AD936x: настройки (вкладка Antenna) + применение текущего
|
||
// контекста (диапазон | активный трансвертер) к железу.
|
||
procedure SetAnt936x(const A: TAD936xAnt);
|
||
procedure ApplyAntPorts;
|
||
property Ant936x: TAD936xAnt read FAnt936x;
|
||
function PanADC(PanId: Integer): Byte;
|
||
function SetPanADC(PanId: Integer; ADC: Byte): Boolean;
|
||
// ---- Зум/пан доп. панов (PanId=0 делегирует главному зуму) ----
|
||
// Видимое окно пана — та же формула, что span-clip его анализатора.
|
||
procedure GetPanViewWindow(PanId: Integer; out ACenter, ASpan: Double);
|
||
procedure SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
|
||
procedure PanZoomIn(PanId: Integer); // шаг по лестнице, якорь = центр окна
|
||
procedure PanZoomOut(PanId: Integer);
|
||
procedure PanZoomDefault(PanId: Integer);
|
||
function PanZoomFactor(PanId: Integer): Double;
|
||
function PanPanSlider(PanId: Integer): Double;
|
||
|
||
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
|
||
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
|
||
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
|
||
procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning
|
||
procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель)
|
||
procedure SetCalibration(const C: TCalibration); // вкладка Calibration → FCal + офсет S-метра в DSP
|
||
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);
|
||
|
||
// ---- Zoom/pan спектра и водопада ----
|
||
// GetViewWindow — единая истина «видимого окна» (центр+span в Гц) для всех
|
||
// оверлеев и маппинга мыши: при зуме отдаёт окно из WDSP-анализатора, иначе
|
||
// базовые FCenterFreq/FSpanHz.
|
||
procedure GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||
procedure ApplyZoom(AZoom, APan: Double); // задать зум+пан, сохранить в band, Changed(rfZoom)
|
||
procedure ZoomIn; // шаг увеличения (якорь — маркер активного VFO)
|
||
procedure ZoomOut; // шаг уменьшения
|
||
procedure ZoomDefault; // сброс в 1.0 (весь span)
|
||
function ZoomFactor: Double;
|
||
function PanSlider: Double;
|
||
|
||
// 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;
|
||
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
|
||
|
||
// Отображение / приём
|
||
procedure SetCTun(On_: Boolean);
|
||
procedure SetDuplex(On_: Boolean);
|
||
procedure SetRxMuteOnTx(On_: Boolean); // QO-100 self-monitor (кнопка RX MUTE)
|
||
procedure ApplyRxMuteToEngine; // FRxMuteOnTx → FDSPEngine.MonitorRXDuringTX
|
||
procedure SetWfAGC(On_: Boolean);
|
||
procedure SetWfNF(On_: Boolean);
|
||
|
||
// DSP-фильтры шума
|
||
procedure SetNR(Mode: Integer);
|
||
procedure SetNB(Mode: Integer);
|
||
procedure SetSNB(On_: Boolean);
|
||
procedure SetANF(On_: Boolean);
|
||
|
||
// Передача
|
||
procedure SetMOX(On_: Boolean);
|
||
procedure SetTune(On_: Boolean); // TUN
|
||
|
||
// ---- PureSignal ----
|
||
// Доступность PS у активного бэкенда/платы (Caps.HasPureSignal).
|
||
function PSAvailable: Boolean;
|
||
// Вкл/выкл авто-калибровки PureSignal (кнопка PS). Вооружает feedback-тракт
|
||
// (DDC0/1 на TX) и WDSP calcc automode; выкл = reset коррекции.
|
||
procedure SetPureSignal(On_: Boolean);
|
||
procedure PSSingleCal; // одиночная калибровка
|
||
procedure PSReset; // выключить и сбросить коррекцию
|
||
// Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis
|
||
// PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX.
|
||
procedure PureSignalTick;
|
||
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
|
||
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
|
||
procedure PSSetFeedbackPath(En: Boolean);
|
||
// Статус для UI (кэш последнего PureSignalTick)
|
||
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
|
||
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
|
||
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
|
||
// замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis).
|
||
function PSFBTarget: Integer;
|
||
function PSCalCount: Integer; // info[5]
|
||
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
|
||
function PSMaxTXVal: Double; // GetPSMaxTX (пик TX для SetPk)
|
||
function PSSingleCalActive: Boolean; // идёт одиночная калибровка
|
||
|
||
// 2TON: двухтональник (PostGen two-tone) + MOX — тест IMD / проверка PS.
|
||
procedure SetTwoTone(On_: Boolean);
|
||
|
||
procedure SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A)
|
||
procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO
|
||
|
||
// FM
|
||
procedure SetFMStep(Idx: Integer);
|
||
procedure SetFMCTCSS(On_: Boolean);
|
||
procedure SetFMCTCSSTone(Idx: Integer);
|
||
procedure SetFMSquelch(On_: Boolean);
|
||
procedure SetFMSquelchLevel(V: Integer);
|
||
procedure SetFMRpt(Dir: Integer); // явное направление; снимает авто-флаг
|
||
procedure SetFMRptOffset(Hz: Double); // сдвиг репитера; применяет, если RPT вкл
|
||
procedure UpdateRptAutoState; // авто-MINUS для 2 м под FVfoA
|
||
|
||
// Каналы: применение/деактивация каналов вынесено в TChannelController
|
||
// (отдельный юнит-адаптер поверх контроллера), оркеструет команды ниже.
|
||
|
||
// Жизненный цикл движка / устройства
|
||
procedure StartStop;
|
||
procedure SetRun(On_: Boolean);
|
||
procedure StartRunning(SpectrumWidth: Integer); // Run=1 bring-up: General/DDC/DUC + DSP restore
|
||
procedure StopRunning; // Run=0: graceful-стоп RX-потока + сброс TX/телеметрии
|
||
function ConnectDevice(const Dev: THPSDRDevice): Boolean; // Connect + load-by-MAC; рендер через rfDevice
|
||
function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live
|
||
procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless)
|
||
// По IP/URI ищет устройство в store (discovered→saved), восстанавливая
|
||
// Kind/URI/Serial/BoardType/MAC. Нужно для Pluto (открывается по URI, выбор
|
||
// бэкенда — по Kind). Не найдено → простой HPSDR по IP. Общий путь GUI+демон.
|
||
function ResolveDevice(const IP: string): THPSDRDevice;
|
||
// Сохраняет устройство по введённому адресу. Схема URI ('ip:'/'usb:'/'local:')
|
||
// → Pluto (AddSavedPluto), иначе HPSDR-IP. Общий путь web-dev_add для GUI и
|
||
// демона (десктоп-диалог сохраняет Pluto своим путём через AddSavedPluto).
|
||
procedure AddSavedDevice(const AName, AAddr: string);
|
||
procedure Discover;
|
||
// Гарантирует, что FNetwork — бэкенд нужного типа (HPSDR/Pluto): при
|
||
// несовпадении пересоздаёт его, перенося колбэки данных. Вызывается из
|
||
// ConnectDevice по Dev.Kind. Discovery использует транзиентные экземпляры.
|
||
procedure EnsureBackend(Kind: TBackendKind);
|
||
// Возможности текущего бэкенда (для гейтинга UI / пресетов sample rate).
|
||
function BackendCaps: TBackendCaps;
|
||
// Единый источник пресетов sample-rate для UI (desktop overlay + web).
|
||
// Берётся из caps текущего бэкенда (HPSDR 48k..1536k / Pluto 576k..5760k).
|
||
function SampleRatePresets: TBackendRateArray;
|
||
// Единый источник band-плана для UI (HF / VHF-UHF по типу бэкенда).
|
||
function BandPlan: TBandPlanArray;
|
||
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
|
||
function IsPluto: Boolean;
|
||
// True — активен QO-100 (Pluto + full-duplex XVTR-слот); гейт UI beacon.
|
||
function InQO100: Boolean;
|
||
// True — включён режим DUP (дисплей+RX держатся на TX). Гейт активности
|
||
// кнопки RX MUTE (self-monitor) на ЛЮБОМ бэкенде — без DUP звук на TX не идёт.
|
||
function DisplayDuplex: Boolean;
|
||
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
|
||
// HPSDR → тип платы; Pluto/LibreSDR → hw_model + серийник (если есть).
|
||
function BoardDisplayName: string;
|
||
// Опрос аппаратной телеметрии Pluto (температура/RSSI) с троттлингом — зовётся
|
||
// с UI-таймера (MeterTimer @10 Гц), как ServiceBeaconLock. No-op для HPSDR.
|
||
// Результат — в FLastPlutoTempC/FLastPlutoRSSIdB (UI читает и форматирует).
|
||
procedure ServicePlutoTelemetry;
|
||
// Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд
|
||
// перед Discover (сохранённые Pluto + ручной ввод IP).
|
||
procedure SetPlutoProbeURIs(const URIs: array of string);
|
||
// Сидирует probe-список из сохранённых Pluto (для бэкенд-агностичного Discover
|
||
// без UI-диалога — демон). GUI добавляет ручной IP поверх в BuildProbeURIs.
|
||
procedure SeedPlutoProbeFromSaved;
|
||
|
||
// ---- События наружу ----
|
||
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
||
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
||
// Доп. наблюдатели состояния (помимо единственного OnStateChanged, который
|
||
// занимает MainForm). Нужны CAT/Andromeda для push индикаторов панели.
|
||
procedure AddStateListener(L: TRadioStateEvent);
|
||
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
|
||
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
||
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
||
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
|
||
property OnBeforeStop: TThreadMethod read FOnBeforeStop write FOnBeforeStop;
|
||
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
|
||
|
||
const
|
||
// Амплитуда TUN-тона (WDSP PostGen). НЕ 1.0: на резком старте тона фильтры
|
||
// TXA-тракта дают переходный выброс ~×1.18 (замерено: peak=1.176 при
|
||
// mag=1.0) поверх полной шкалы → жёсткий кламп 24-бит → сплэттер на старте
|
||
// TUN. 0.83×1.18≈0.98 — выброс остаётся в шкале. Мощность TUN задаёт drive
|
||
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
|
||
TUN_TONE_MAG = 0.83;
|
||
|
||
// Состояния командной state machine PureSignal (порт Thetis eCMDState;
|
||
// Restore-ветка (7) не реализована — Save/Restore коррекции нет).
|
||
PS_ST_OFF = 0;
|
||
PS_ST_TURNON_AUTO = 1;
|
||
PS_ST_AUTOCAL = 2;
|
||
PS_ST_TURNON_SINGLE = 3;
|
||
PS_ST_SINGLECAL = 4;
|
||
PS_ST_STAYON = 5;
|
||
PS_ST_TURNOFF = 6;
|
||
|
||
constructor TDMRSliceDecoder.Create(AOwner: TRadioController;
|
||
ASliceId: Integer);
|
||
begin
|
||
inherited Create;
|
||
FOwner := AOwner;
|
||
FSliceId := ASliceId;
|
||
FDecoder := TDMRDecoder.Create;
|
||
FDecoder.SetInverted(True);
|
||
FDecoder.OnAudio := AudioReady;
|
||
FDecoder.SetEnabled(True);
|
||
end;
|
||
|
||
destructor TDMRSliceDecoder.Destroy;
|
||
begin
|
||
if Assigned(FDecoder) then
|
||
begin
|
||
FDecoder.SetEnabled(False);
|
||
FDecoder.OnAudio := nil;
|
||
FreeAndNil(FDecoder);
|
||
end;
|
||
FOwner := nil;
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TDMRSliceDecoder.FeedAudio(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
begin
|
||
if Assigned(FDecoder) then FDecoder.FeedAudio(Left, Right, Count);
|
||
end;
|
||
|
||
procedure TDMRSliceDecoder.GetStatus(out S: TDMRStatus);
|
||
begin
|
||
if Assigned(FDecoder) then FDecoder.GetStatus(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
procedure TDMRSliceDecoder.AudioReady(const PCM: array of Single;
|
||
Count: Integer);
|
||
begin
|
||
if Assigned(FOwner) then FOwner.OnSliceDMRAudioReady(FSliceId, PCM, Count);
|
||
end;
|
||
|
||
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 := MODE_USB; FFilter := 5; FFilterBW := 2700;
|
||
FAGCMode := 1; FAGCTop := 90;
|
||
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
|
||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||
FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||
FNextSliceId := 1; // мультислайсы: генератор id
|
||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||
ResetPanDDCMaps; // паны: все слоты свободны
|
||
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||
FCurrentBand := 5; FCurrentXvtr := -1;
|
||
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
||
FPAMaxPower := 100.0;
|
||
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
|
||
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
||
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
|
||
FWfSpeedDiv := 1; FSpecMSAA := 4;
|
||
FFMStepOn := True;
|
||
FRawFMDeviation := RAW_FM_DEV_DEFAULT;
|
||
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
|
||
TSettingsManager.DefaultCalibration(FCal);
|
||
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
||
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
|
||
FLastPlutoRSSIdB := TELEMETRY_NONE;
|
||
FPlutoTeleTick := 0; FPlutoTempBad := 0;
|
||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||
end;
|
||
|
||
procedure TRadioController.CreateEngines(ASampleRate: Integer);
|
||
// Создаёт движки в полях контроллера. Колбэки (OnAudio/OnSpectrum/OnHPStatus…)
|
||
// навешивает вызывающий после возврата — они указывают на UI/демон.
|
||
begin
|
||
FNetwork := THPSDRNetwork.Create;
|
||
|
||
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
|
||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||
FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady;
|
||
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||
FDSPEngine.SMeterCalDB := FCal.SMeterOffsetDB;
|
||
|
||
// 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);
|
||
|
||
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
|
||
FDMRDec := TDMRDecoder.Create;
|
||
// WDSP's FM discriminator polarity on the EWSDR RX path is opposite to the
|
||
// DMR dibit convention: captured handheld MS voice sync appears as MS data
|
||
// without this inversion (the two sync words are complements).
|
||
FDMRDec.SetInverted(True);
|
||
FDMRDec.OnAudio := OnDMRAudioReady;
|
||
|
||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||
// формы / при подключении устройства).
|
||
FAudioOut := TAudioOutput.Create(48000);
|
||
FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize;
|
||
FAudioIn := TAudioInput.Create(48000);
|
||
end;
|
||
|
||
procedure TRadioController.FreeEngines;
|
||
var i: Integer;
|
||
begin
|
||
// Stop new slice callbacks before their decoder/audio destinations vanish.
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.OnSliceAudio := nil;
|
||
FDSPEngine.OnSliceDemodAudio := nil;
|
||
end;
|
||
// Stop the DMR worker before destroying any of its audio destinations.
|
||
if Assigned(FDMRDec) then
|
||
begin
|
||
FDMRDec.SetEnabled(False);
|
||
FDMRDec.OnAudio := nil;
|
||
FreeAndNil(FDMRDec);
|
||
end;
|
||
if Assigned(FNetwork) then
|
||
begin
|
||
if FNetwork.Connected then FNetwork.Disconnect;
|
||
FreeAndNil(FNetwork);
|
||
end;
|
||
// RemoveSlice takes the DSP slice lock. Besides closing the WDSP channel it
|
||
// waits out a callback that might already have entered before we detached it.
|
||
if Assigned(FDSPEngine) then
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then FDSPEngine.RemoveSlice(FSlices[i].Id);
|
||
// Слайсы: каналы уже закрыты, останавливаем декодеры и аудио.
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then
|
||
begin
|
||
FreeAndNil(FSlices[i].DMR);
|
||
if FSlices[i].Audio <> nil then
|
||
begin
|
||
FSlices[i].Audio.Close;
|
||
FreeAndNil(FSlices[i].Audio);
|
||
end;
|
||
FSlices[i].Active := False;
|
||
end;
|
||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetBeaconDecoder(nil);
|
||
FDSPEngine.Close;
|
||
FreeAndNil(FDSPEngine);
|
||
end;
|
||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||
end;
|
||
|
||
destructor TRadioController.Destroy;
|
||
begin
|
||
FreeEngines;
|
||
FreeAndNil(FDeviceStore);
|
||
FreeAndNil(FSettings);
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TRadioController.Changed(Field: TRadioField);
|
||
var i: Integer;
|
||
begin
|
||
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
|
||
for i := 0 to High(FStateListeners) do
|
||
if Assigned(FStateListeners[i]) then FStateListeners[i](Self, Field);
|
||
end;
|
||
|
||
procedure TRadioController.AddStateListener(L: TRadioStateEvent);
|
||
begin
|
||
if not Assigned(L) then Exit;
|
||
SetLength(FStateListeners, Length(FStateListeners) + 1);
|
||
FStateListeners[High(FStateListeners)] := L;
|
||
end;
|
||
|
||
procedure TRadioController.PushNetworkState;
|
||
// Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние
|
||
// MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг.
|
||
begin
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
||
FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
|
||
// Сетевой поток: трекинг seq + подача IQ активного DDC в WDSP. UI не трогаем —
|
||
// статус рисует таймер по полям ниже. Backend-агностичен (Pluto синтезирует
|
||
// тот же TDDCIQPacket).
|
||
var
|
||
SamplesPerFrame: Integer;
|
||
Seq, ExpectedSeq: LongWord;
|
||
Delta: Int64;
|
||
begin
|
||
Inc(FRXPacketCount);
|
||
FRXLastPktTime := GetTickCount64;
|
||
Seq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
|
||
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
|
||
|
||
if (DDCIndex >= Low(FDDCLastSeq)) and (DDCIndex <= High(FDDCLastSeq)) then
|
||
begin
|
||
if Seq <> 0 then
|
||
begin
|
||
ExpectedSeq := FDDCLastSeq[DDCIndex] + 1;
|
||
if FDDCSeqValid[DDCIndex] and (Seq <> ExpectedSeq) then
|
||
begin
|
||
Inc(FSeqErrorCount);
|
||
FLastSeqErrorDDC := DDCIndex;
|
||
Delta := Int64(Seq) - Int64(ExpectedSeq);
|
||
FLastSeqErrorDelta := Delta;
|
||
FSeqOkStreak := 0;
|
||
end
|
||
else if FDDCSeqValid[DDCIndex] then
|
||
begin
|
||
if FSeqErrorCount > 0 then
|
||
begin
|
||
Inc(FSeqOkStreak);
|
||
if FSeqOkStreak >= 20 then
|
||
begin
|
||
FSeqErrorCount := 0;
|
||
FLastSeqErrorDDC := -1;
|
||
FLastSeqErrorDelta := 0;
|
||
FSeqOkStreak := 0;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
FDDCLastSeq[DDCIndex] := Seq;
|
||
FDDCSeqValid[DDCIndex] := True;
|
||
end;
|
||
|
||
// Подаём IQ в DSP: активный DDC → главный тракт, пан-DDC → его пан (3.2),
|
||
// DDC0 при вооружённом PureSignal на TX → feedback в pscc (DDC0 несёт
|
||
// interleaved пары DDC0+DDC1; на PS-платах главный DDC ≥ 2, конфликта нет).
|
||
if FWDSPReady then
|
||
begin
|
||
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
|
||
Integer(Data.SamplesPerFrame[1]);
|
||
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
|
||
if (DDCIndex = 0) and FPSFeedbackOn and (FActiveDDC <> 0) then
|
||
begin
|
||
if FTransmitting then
|
||
FDSPEngine.PushPSFeedback(Data.IQData, 0, SamplesPerFrame);
|
||
end
|
||
else if DDCIndex = FActiveDDC then
|
||
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
|
||
else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS)
|
||
and (FDDCPanMap[DDCIndex] > 0) then
|
||
FDSPEngine.PushPanIQ(FDDCPanMap[DDCIndex], 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;
|
||
BAND_6M = 10; // индекс 6m в HF-плане (160m..6m)
|
||
var
|
||
FwdPwr, RevPwr: Word;
|
||
SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double;
|
||
Bi: Integer;
|
||
Is6m, 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 SupplyV >= 0 then SupplyV := SupplyV * FCal.SupplyVoltCal;
|
||
|
||
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
|
||
SupplyA := ADCToSupplyCurrent(
|
||
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
|
||
FNetwork.Device.BoardType,
|
||
FCal.AmpVoffMV, FCal.AmpSensMVA);
|
||
end;
|
||
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
|
||
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
|
||
// (как Thetis PABandCalMult).
|
||
Bi := TxBandIdx;
|
||
Is6m := Bi = BAND_6M;
|
||
DetK := 1.0;
|
||
if (Bi >= 0) and (Bi < CFG_BAND_COUNT) and (FCal.PADetCal[Bi] > 0) then
|
||
DetK := FCal.PADetCal[Bi];
|
||
FwdW := AlexDetToWatts(FwdPwr, FCal.PwrDetModel, False, Is6m) * DetK;
|
||
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
|
||
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
|
||
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
||
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
|
||
// fwd-мощности.
|
||
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
|
||
begin
|
||
Rho := Sqrt(RevW / FwdW);
|
||
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
|
||
// Decoded DMR PCM will use this route later. Until then suppress raw 4FSK;
|
||
// the independent pre-volume tap is handled by OnDemodAudioReady.
|
||
if FMode in [MODE_DMR, MODE_FMRAW] then Exit;
|
||
|
||
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.OnDemodAudioReady(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
// DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring.
|
||
begin
|
||
if FMode = MODE_DMR then
|
||
begin
|
||
if not Assigned(FDMRDec) then Exit;
|
||
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
|
||
FDMRDec.FeedAudio(Left, Right, Count);
|
||
end
|
||
else if FMode = MODE_FMRAW then
|
||
begin
|
||
// FM RAW decoder audio deliberately bypasses speaker volume and mute.
|
||
if FLocalAudio and Assigned(FAudioOut) then
|
||
FAudioOut.Write(Left, Right, Count);
|
||
end
|
||
else if Assigned(FDMRDec) and FDMRDec.Enabled then
|
||
FDMRDec.SetEnabled(False);
|
||
end;
|
||
|
||
procedure TRadioController.OnDMRAudioReady(const PCM: array of Single;
|
||
Count: Integer);
|
||
// DMR worker thread, 8 kHz mono from mbelib. Linear 6x interpolation brings
|
||
// it to the common 48 kHz output route used by local, web and radio speakers.
|
||
var
|
||
Left, Right: array of Single;
|
||
i, Phase, OutPos, N: Integer;
|
||
A, B, V, Gain: Single;
|
||
begin
|
||
if (FMode <> MODE_DMR) or FMuted or (Count <= 0) then Exit;
|
||
N := Min(Count, Length(PCM));
|
||
SetLength(Left, N * 6);
|
||
SetLength(Right, N * 6);
|
||
Gain := FVolume / 100.0;
|
||
OutPos := 0;
|
||
for i := 0 to N - 1 do
|
||
begin
|
||
A := PCM[i];
|
||
if i + 1 < N then B := PCM[i + 1] else B := A;
|
||
for Phase := 0 to 5 do
|
||
begin
|
||
V := (A + (B - A) * (Phase / 6.0)) * Gain;
|
||
Left[OutPos] := V;
|
||
Right[OutPos] := V;
|
||
Inc(OutPos);
|
||
end;
|
||
end;
|
||
if FSendAudioToRadio and Assigned(FNetwork) then
|
||
FNetwork.SendSpeakerAudio(Left, Right, OutPos);
|
||
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, OutPos) then Exit;
|
||
if FLocalAudio and Assigned(FAudioOut) then FAudioOut.Write(Left, Right, OutPos);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Мультислайсы (софт-fan-out)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TRadioController.FindSliceIndex(Id: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
|
||
end;
|
||
|
||
// DSP-поток: аудио готового слайса → его собственный выход. Слайс — локальный
|
||
// монитор (в web/на радио не уходит), поэтому только на свою звуковуху.
|
||
procedure TRadioController.OnSliceAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
if not FLocalAudio then Exit;
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx < 0 then Exit;
|
||
if FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW] then Exit;
|
||
if SliceMutedByTx(idx) then Exit;
|
||
if FSlices[idx].Audio <> nil then
|
||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||
end;
|
||
|
||
// Слайс глушится на передаче? В full-duplex (Pluto/QO-100) RX-тракт живёт и во
|
||
// время TX, поэтому иначе оператор слышит сам себя на КАЖДОМ слайсе. У главного
|
||
// приёмника за это отвечает FRxMuteOnTx (кнопка RX MUTE), у слайса — свой флаг.
|
||
function TRadioController.SliceMutedByTx(Idx: Integer): Boolean;
|
||
begin
|
||
Result := FTransmitting and FSlices[Idx].RxMuteOnTx;
|
||
end;
|
||
|
||
// DSP thread: flat, volume-independent discriminator of the corresponding
|
||
// WDSP slice channel. FeedAudio only copies it into that decoder's ring.
|
||
procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx < 0 then Exit;
|
||
if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then
|
||
FSlices[idx].DMR.FeedAudio(Left, Right, Count);
|
||
if (FSlices[idx].Mode = MODE_FMRAW) and FLocalAudio and
|
||
Assigned(FSlices[idx].Audio) then
|
||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||
end;
|
||
|
||
// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output.
|
||
procedure TRadioController.OnSliceDMRAudioReady(SliceId: Integer;
|
||
const PCM: array of Single; Count: Integer);
|
||
var
|
||
Left, Right: array of Single;
|
||
idx, i, Phase, OutPos, N: Integer;
|
||
A, B, V, Gain: Single;
|
||
begin
|
||
if not FLocalAudio or (Count <= 0) then Exit;
|
||
idx := FindSliceIndex(SliceId);
|
||
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or
|
||
FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit;
|
||
if SliceMutedByTx(idx) then Exit;
|
||
N := Min(Count, Length(PCM));
|
||
SetLength(Left, N * 6);
|
||
SetLength(Right, N * 6);
|
||
Gain := FSlices[idx].Volume;
|
||
OutPos := 0;
|
||
for i := 0 to N - 1 do
|
||
begin
|
||
A := PCM[i];
|
||
if i + 1 < N then B := PCM[i + 1] else B := A;
|
||
for Phase := 0 to 5 do
|
||
begin
|
||
V := (A + (B - A) * (Phase / 6.0)) * Gain;
|
||
Left[OutPos] := V;
|
||
Right[OutPos] := V;
|
||
Inc(OutPos);
|
||
end;
|
||
end;
|
||
FSlices[idx].Audio.Write(Left, Right, OutPos);
|
||
end;
|
||
|
||
procedure TRadioController.RefreshSliceShifts;
|
||
// Смена центра ГЛАВНОГО захвата: пересчитываем только слайсы пана 0 —
|
||
// слайсы доп. панов привязаны к центрам своих DDC (SetPanDDCFreq).
|
||
var i: Integer;
|
||
begin
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = 0) then
|
||
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FCenterFreq);
|
||
end;
|
||
|
||
function TRadioController.PanCenterHz(PanId: Integer): Double;
|
||
begin
|
||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
|
||
Result := FPanFreqHz[PanId]
|
||
else
|
||
Result := FCenterFreq;
|
||
end;
|
||
|
||
procedure TRadioController.CapturedSpan(out LoHz, HiHz: Double; PanId: Integer);
|
||
// Полезная полоса ±47.5% от захвата (запас на краях фильтра/анти-алиасинга).
|
||
var Half, Rate: Double;
|
||
begin
|
||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
|
||
Rate := FPanRateKHz[PanId] * 1000.0
|
||
else
|
||
Rate := FSampleRate;
|
||
Half := Rate * 0.475;
|
||
LoHz := PanCenterHz(PanId) - Half;
|
||
HiHz := PanCenterHz(PanId) + Half;
|
||
end;
|
||
|
||
function TRadioController.SliceFitsCapture(TargetHz: Double; PanId: Integer): Boolean;
|
||
var Lo, Hi: Double;
|
||
begin
|
||
CapturedSpan(Lo, Hi, PanId);
|
||
Result := (TargetHz >= Lo) and (TargetHz <= Hi);
|
||
end;
|
||
|
||
{ ---- Панадаптеры на аппаратных DDC (этап 3.1) ---- }
|
||
|
||
procedure TRadioController.ResetPanDDCMaps;
|
||
var i: Integer;
|
||
begin
|
||
for i := 1 to MAX_PANS - 1 do
|
||
begin
|
||
FPanDDCIdx[i] := -1;
|
||
FPanFreqHz[i] := 0;
|
||
FPanRateKHz[i] := 0;
|
||
FPanADC[i] := 0;
|
||
FPanZoomF[i] := 0.0;
|
||
FPanPanSl[i] := 0.5;
|
||
end;
|
||
for i := 0 to MAX_DDCS - 1 do
|
||
FDDCPanMap[i] := -1;
|
||
end;
|
||
|
||
function TRadioController.PanRateAllowed(RateKHz: Word): Boolean;
|
||
// Полная сетка железа (Caps.RatePresets) минус некратные 48 кГц: движок
|
||
// требует целого отношения rate пана к аудио-rate (блок/WDSP-каналы).
|
||
var i: Integer;
|
||
begin
|
||
Result := False;
|
||
if not Assigned(FNetwork) then Exit;
|
||
if (RateKHz = 0) or ((Integer(RateKHz) * 1000) mod 48000 <> 0) then Exit;
|
||
for i := 0 to High(FNetwork.Caps.RatePresets) do
|
||
if FNetwork.Caps.RatePresets[i] = Integer(RateKHz) * 1000 then Exit(True);
|
||
end;
|
||
|
||
function TRadioController.AddPanDDC(PanId: Integer; FreqHz: Double;
|
||
RateKHz: Word; ADCSource: Byte): Boolean;
|
||
var
|
||
i, ddc: Integer;
|
||
begin
|
||
Result := False;
|
||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||
if FPanDDCIdx[PanId] >= 0 then Exit; // уже создан
|
||
if not (Assigned(FNetwork) and FRunning) then Exit;
|
||
if PanId >= FNetwork.Caps.MaxPans then Exit; // бэкенд не умеет (Pluto=1)
|
||
|
||
if not PanRateAllowed(RateKHz) then RateKHz := 384;
|
||
|
||
// hw-индекс: следующий свободный после главного DDC (слот-стабильности не
|
||
// нужно — hw-индекс скрыт за PanId, наружу торчит только PanId).
|
||
ddc := -1;
|
||
for i := FActiveDDC + 1 to FActiveDDC + MAX_PANS - 1 do
|
||
if (i < MAX_DDCS) and (FDDCPanMap[i] < 0) then begin ddc := i; Break; end;
|
||
if ddc < 0 then Exit;
|
||
|
||
// Движок: аккумулятор + analyzer пана (до включения DDC — к приходу первого
|
||
// пакета пан уже готов; PushPanIQ без пана просто дропает).
|
||
if FWDSPReady and not FDSPEngine.AddPan(PanId, Integer(RateKHz) * 1000) then
|
||
Exit;
|
||
|
||
FPanDDCIdx[PanId] := ddc;
|
||
FPanFreqHz[PanId] := FreqHz;
|
||
FPanRateKHz[PanId] := RateKHz;
|
||
FPanADC[PanId] := ADCSource;
|
||
FPanZoomF[PanId] := 0.0;
|
||
FPanPanSl[PanId] := 0.5;
|
||
FDDCPanMap[ddc] := PanId;
|
||
// В железо — IF-частота: FPanFreqHz хранит видимую (как FCenterFreq).
|
||
FNetwork.SetPanDDC(ddc, True, XvtrTranslate(FreqHz), RateKHz, ADCSource);
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.SetPanDDCRate(PanId: Integer; RateKHz: Word): Boolean;
|
||
var
|
||
i: Integer;
|
||
LoHz, HiHz: Double;
|
||
begin
|
||
Result := False;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
if not PanRateAllowed(RateKHz) then Exit;
|
||
if FPanRateKHz[PanId] = RateKHz then Exit(True);
|
||
// Движок первым: под FSliceLock пересобирает аккумулятор/анализатор и
|
||
// переоткрывает слайс-каналы; провалился — состояние не трогаем.
|
||
if FWDSPReady and not FDSPEngine.SetPanRate(PanId, Integer(RateKHz) * 1000) then
|
||
Exit;
|
||
FPanRateKHz[PanId] := RateKHz;
|
||
// DDC железа: реконфиг rate (RebuildDDCSpecific + freq-слово, IF при XVTR).
|
||
if Assigned(FNetwork) then
|
||
FNetwork.SetPanDDC(FPanDDCIdx[PanId], True, XvtrTranslate(FPanFreqHz[PanId]),
|
||
RateKHz, FPanADC[PanId]);
|
||
// Слайсы, выпавшие из сузившейся полосы, — к её краям (абс. TargetHz + NCO).
|
||
CapturedSpan(LoHz, HiHz, PanId);
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
begin
|
||
if FSlices[i].TargetHz < LoHz then SetSliceTarget(FSlices[i].Id, LoHz);
|
||
if FSlices[i].TargetHz > HiHz then SetSliceTarget(FSlices[i].Id, HiHz);
|
||
end;
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.MovePanDDC(PanId, NewDDC: Integer): Boolean;
|
||
// Тот же пан (движок/анализатор/слайсы не трогаем) — только другой аппаратный
|
||
// DDC: старый гасим, новый включаем с теми же freq/rate/ADC. Короткий разрыв IQ
|
||
// на время реконфига — норма.
|
||
begin
|
||
Result := False;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
if not Assigned(FNetwork) then Exit;
|
||
if (NewDDC <= FActiveDDC) or (NewDDC > MaxDDCIndex) then Exit;
|
||
if FPanDDCIdx[PanId] = NewDDC then Exit(True);
|
||
if FDDCPanMap[NewDDC] >= 0 then Exit; // занят другим паном
|
||
|
||
FNetwork.SetPanDDC(FPanDDCIdx[PanId], False, 0, 0, 0);
|
||
FDDCPanMap[FPanDDCIdx[PanId]] := -1;
|
||
// Seq-трекинг обоих индексов начинаем заново (новый поток — новый счётчик).
|
||
if FPanDDCIdx[PanId] <= High(FDDCSeqValid) then
|
||
FDDCSeqValid[FPanDDCIdx[PanId]] := False;
|
||
if NewDDC <= High(FDDCSeqValid) then
|
||
FDDCSeqValid[NewDDC] := False;
|
||
|
||
FPanDDCIdx[PanId] := NewDDC;
|
||
FDDCPanMap[NewDDC] := PanId;
|
||
FNetwork.SetPanDDC(NewDDC, True, XvtrTranslate(FPanFreqHz[PanId]),
|
||
FPanRateKHz[PanId], FPanADC[PanId]);
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.PanDDCIndex(PanId: Integer): Integer;
|
||
begin
|
||
if (PanId >= 1) and (PanId < MAX_PANS) then Result := FPanDDCIdx[PanId]
|
||
else if PanId = 0 then Result := FActiveDDC
|
||
else Result := -1;
|
||
end;
|
||
|
||
function TRadioController.MaxDDCIndex: Integer;
|
||
// Железо openHPSDR (Orion/Saturn) поднимает DDC0..DDC7 — DDC8 и выше молчат
|
||
// (проверено на плате: enable-бит ставится, поток не идёт).
|
||
begin
|
||
Result := 7;
|
||
end;
|
||
|
||
function TRadioController.DDCFree(DDCIdx: Integer): Boolean;
|
||
begin
|
||
Result := (DDCIdx > FActiveDDC) and (DDCIdx <= MaxDDCIndex)
|
||
and (FDDCPanMap[DDCIdx] < 0);
|
||
end;
|
||
|
||
procedure TRadioController.RemovePanDDC(PanId: Integer);
|
||
var ddc, i: Integer;
|
||
begin
|
||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||
ddc := FPanDDCIdx[PanId];
|
||
if ddc < 0 then Exit;
|
||
// Сначала снимаем роутинг (OnDDCIQ в сетевом потоке перестаёт брать пакеты
|
||
// этого DDC), потом слайсы пана (их звуковухи), потом движок и DDC в железе.
|
||
FDDCPanMap[ddc] := -1;
|
||
FPanDDCIdx[PanId] := -1;
|
||
FPanFreqHz[PanId] := 0;
|
||
FPanRateKHz[PanId] := 0;
|
||
FPanADC[PanId] := 0;
|
||
FPanZoomF[PanId] := 0.0;
|
||
FPanPanSl[PanId] := 0.5;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
RemoveSlice(FSlices[i].Id);
|
||
if FWDSPReady then FDSPEngine.RemovePan(PanId);
|
||
if Assigned(FNetwork) then FNetwork.SetPanDDC(ddc, False, 0, 0, 0);
|
||
end;
|
||
|
||
procedure TRadioController.SetPanDDCFreq(PanId: Integer; FreqHz: Double);
|
||
var i: Integer;
|
||
begin
|
||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||
if FPanDDCIdx[PanId] < 0 then Exit;
|
||
FPanFreqHz[PanId] := FreqHz; // видимая; в железо — IF при активном XVTR
|
||
if Assigned(FNetwork) then
|
||
FNetwork.SetPanDDCFreq(FPanDDCIdx[PanId], XvtrTranslate(FreqHz));
|
||
// Слайсы пана держат абсолютный TargetHz — обновляем их NCO-сдвиги
|
||
// относительно нового центра (аналог RefreshSliceShifts главного).
|
||
if Assigned(FDSPEngine) then
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FreqHz);
|
||
end;
|
||
|
||
procedure TRadioController.VisibleFreqBounds(out LoHz, HiHz: Double);
|
||
begin
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
// Активный XVTR: видимые частоты живут в его диапазоне, в железо уходит
|
||
// IF (XvtrTranslate) — клампить видимую частоту пределами АЦП нельзя.
|
||
LoHz := FXvtrSettings.Entries[FCurrentXvtr].FreqBegin;
|
||
HiHz := FXvtrSettings.Entries[FCurrentXvtr].FreqEnd;
|
||
end
|
||
else if Assigned(FNetwork) then
|
||
begin
|
||
LoHz := FNetwork.Caps.MinFreqHz;
|
||
HiHz := FNetwork.Caps.MaxFreqHz;
|
||
end
|
||
else
|
||
begin
|
||
LoHz := 0;
|
||
HiHz := 61440000;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.RefreshPanDDCs;
|
||
var
|
||
p, i: Integer;
|
||
LoV, HiV, LoHz, HiHz: Double;
|
||
begin
|
||
VisibleFreqBounds(LoV, HiV);
|
||
for p := 1 to MAX_PANS - 1 do
|
||
if PanDDCActive(p) then
|
||
begin
|
||
// Видимый центр пана — в новое допустимое окно, freq-слово DDC — через
|
||
// свежую трансляцию (вход/выход XVTR, правка LO/LOError).
|
||
FPanFreqHz[p] := EnsureRange(FPanFreqHz[p], LoV, HiV);
|
||
if Assigned(FNetwork) then
|
||
FNetwork.SetPanDDCFreq(FPanDDCIdx[p], XvtrTranslate(FPanFreqHz[p]));
|
||
// Слайсы держат абсолютный видимый TargetHz: выпавшие из полосы захвата
|
||
// пана — к её краям (SetSliceTarget обновит NCO), остальные — свежий NCO.
|
||
CapturedSpan(LoHz, HiHz, p);
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = p) then
|
||
begin
|
||
if FSlices[i].TargetHz < LoHz then SetSliceTarget(FSlices[i].Id, LoHz)
|
||
else if FSlices[i].TargetHz > HiHz then SetSliceTarget(FSlices[i].Id, HiHz)
|
||
else if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetSliceShift(FSlices[i].Id,
|
||
FSlices[i].TargetHz - FPanFreqHz[p]);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.PanDDCActive(PanId: Integer): Boolean;
|
||
begin
|
||
Result := (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0);
|
||
end;
|
||
|
||
function TRadioController.PanDDCFreq(PanId: Integer): Double;
|
||
begin
|
||
if PanDDCActive(PanId) then Result := FPanFreqHz[PanId]
|
||
else Result := 0;
|
||
end;
|
||
|
||
function TRadioController.PanDDCRateKHz(PanId: Integer): Word;
|
||
begin
|
||
if PanDDCActive(PanId) then Result := FPanRateKHz[PanId]
|
||
else Result := 0;
|
||
end;
|
||
|
||
procedure TRadioController.SetAnt936x(const A: TAD936xAnt);
|
||
// Новая таблица «диапазон → разъём» из настроек: сохранить + применить текущий.
|
||
begin
|
||
FAnt936x := A;
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
||
FSettings.Save;
|
||
end;
|
||
ApplyAntPorts;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyAntPorts;
|
||
// Антенна — свойство диапазона (как RxAnt/TxAnt у Alex): при смене диапазона или
|
||
// входе/выходе из трансвертера включаем разъёмы, выбранные для этого контекста.
|
||
// RX-разъём берём по диапазону ПРИЁМА, TX — по диапазону ПЕРЕДАЧИ: с мультислайс-TX
|
||
// (или split) передача может идти не на частоте центра приёма.
|
||
var
|
||
Rx, Tx: Byte;
|
||
BiRx, BiTx: Integer;
|
||
begin
|
||
if not (Assigned(FNetwork) and FNetwork.Connected) then Exit;
|
||
if not FNetwork.Caps.HasAntSwitch then Exit;
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
begin
|
||
Rx := FAnt936x.XvtrRxAnt[FCurrentXvtr];
|
||
Tx := FAnt936x.XvtrTxAnt[FCurrentXvtr];
|
||
end
|
||
else
|
||
begin
|
||
// Диапазоны — по ВИДИМЫМ частотам (VU-план AD936x).
|
||
BiRx := FreqToBandIdx(FCenterFreq, IsPluto);
|
||
if (BiRx < 0) or (BiRx >= CFG_BAND_COUNT) then Exit; // вне плана — не трогаем
|
||
Rx := FAnt936x.RxAnt[BiRx];
|
||
BiTx := FreqToBandIdx(ActiveTXFreqHz, IsPluto);
|
||
if (BiTx < 0) or (BiTx >= CFG_BAND_COUNT) then BiTx := BiRx; // TX вне плана — как RX
|
||
Tx := FAnt936x.TxAnt[BiTx];
|
||
end;
|
||
FNetwork.SetAntPorts(Rx, Tx);
|
||
end;
|
||
|
||
function TRadioController.PanADC(PanId: Integer): Byte;
|
||
begin
|
||
if PanId = 0 then Result := FMainADCSrc
|
||
else if PanDDCActive(PanId) then Result := FPanADC[PanId]
|
||
else Result := 0;
|
||
end;
|
||
|
||
function TRadioController.SetPanADC(PanId: Integer; ADC: Byte): Boolean;
|
||
begin
|
||
Result := False;
|
||
if not (Assigned(FNetwork) and FRunning) then Exit;
|
||
if ADC >= FNetwork.Caps.NumADCs then Exit;
|
||
if PanId = 0 then
|
||
begin
|
||
// Главный DDC: тот же ConfigureDDCs, что при смене rate (кэш+RebuildDDCSpecific).
|
||
if FMainADCSrc = ADC then Exit(True);
|
||
FMainADCSrc := ADC;
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, ADC,
|
||
FDitherEnabled, FRandomEnabled);
|
||
Exit(True);
|
||
end;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
if FPanADC[PanId] = ADC then Exit(True);
|
||
FPanADC[PanId] := ADC;
|
||
FNetwork.SetPanDDC(FPanDDCIdx[PanId], True, XvtrTranslate(FPanFreqHz[PanId]),
|
||
FPanRateKHz[PanId], ADC);
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.AddSlice(TargetHz: Double;
|
||
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
|
||
DevIndex: Integer; const DevName: string; APanId: Integer): Integer;
|
||
var
|
||
i, slot, Id: Integer;
|
||
AO: TAudioOutput;
|
||
begin
|
||
Result := -1;
|
||
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
|
||
if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан
|
||
if not SliceFitsCapture(TargetHz, APanId) then Exit; // вне полосы пана
|
||
|
||
slot := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if not FSlices[i].Active then begin slot := i; Break; end;
|
||
if slot < 0 then Exit; // нет свободных слотов
|
||
|
||
Id := FNextSliceId;
|
||
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
|
||
TargetHz - PanCenterHz(APanId), Vol, AGC,
|
||
APanId) then Exit;
|
||
Inc(FNextSliceId);
|
||
|
||
// Свой аудио-выход слайса на выбранной звуковухе.
|
||
AO := TAudioOutput.Create(48000);
|
||
AO.OutputBufferSize := FSettings.LoadAudioBufferSize;
|
||
AO.DeviceIndex := DevIndex;
|
||
if FLocalAudio then AO.Open;
|
||
|
||
FSlices[slot].Active := True;
|
||
FSlices[slot].Id := Id;
|
||
// Буква флага. Слайс A — это главный приёмник (не в FSlices[]), поэтому доп.
|
||
// слайсы нумеруются с 'B' (slot 0 → B, 1 → C, …), иначе получалось два «A».
|
||
FSlices[slot].Letter := Chr(Ord('B') + slot);
|
||
FSlices[slot].PanId := APanId;
|
||
FSlices[slot].TargetHz := TargetHz;
|
||
FSlices[slot].Mode := Mode;
|
||
FSlices[slot].FilterLo := FilterLo;
|
||
FSlices[slot].FilterHi := FilterHi;
|
||
FSlices[slot].AGC := AGC;
|
||
FSlices[slot].Volume := Vol;
|
||
FSlices[slot].Muted := False;
|
||
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
|
||
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
|
||
FSlices[slot].FMSQLevel := 40;
|
||
FSlices[slot].DeviceIndex := DevIndex;
|
||
FSlices[slot].DevName := DevName;
|
||
FSlices[slot].Audio := AO;
|
||
FSlices[slot].DMR := nil;
|
||
if Mode = MODE_DMR then
|
||
FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id);
|
||
|
||
Result := Id;
|
||
Changed(rfDevice);
|
||
end;
|
||
|
||
procedure TRadioController.RemoveSlice(Id: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
// Удаляемый слайс был источником TX — вернуть передачу на главный VFO.
|
||
if FTxSliceId = Id then SetTxSlice(0);
|
||
if Assigned(FDSPEngine) then FDSPEngine.RemoveSlice(Id);
|
||
FreeAndNil(FSlices[idx].DMR);
|
||
if FSlices[idx].Audio <> nil then
|
||
begin
|
||
FSlices[idx].Audio.Close;
|
||
FreeAndNil(FSlices[idx].Audio);
|
||
end;
|
||
FSlices[idx].Active := False;
|
||
Changed(rfDevice);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceTarget(Id: Integer; TargetHz: Double);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].TargetHz := TargetHz;
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetSliceShift(Id, TargetHz - PanCenterHz(FSlices[idx].PanId));
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
|
||
var idx, OldMode: Integer;
|
||
begin
|
||
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
OldMode := FSlices[idx].Mode;
|
||
if (Mode = MODE_DMR) and not Assigned(FSlices[idx].DMR) then
|
||
FSlices[idx].DMR := TDMRSliceDecoder.Create(Self, Id);
|
||
FSlices[idx].Mode := Mode;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode);
|
||
// DMR is receive-only. If this slice used to be the TX source, hand TX back
|
||
// to the main receiver as soon as it enters DMR. FM RAW supports flat TX.
|
||
if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0);
|
||
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
|
||
FreeAndNil(FSlices[idx].DMR);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].FilterLo := Low;
|
||
FSlices[idx].FilterHi := High;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceFilter(Id, Low, High);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].AGC := AGC;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceAGC(Id, AGC);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceVolume(Id: Integer; Vol: Double);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].Volume := Vol;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceVolume(Id, Vol);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceMute(Id: Integer; Mute: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].Muted := Mute;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceRxMuteOnTx(Id: Integer; On_: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].RxMuteOnTx := On_;
|
||
end;
|
||
|
||
// «Главный» слайс пана = созданный на нём первым. Id монотонно растёт
|
||
// (FNextSliceId), поэтому первый = слайс с минимальным Id. 0 = слайсов нет.
|
||
function TRadioController.PanPrimarySliceId(PanId: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) and
|
||
((Result = 0) or (FSlices[i].Id < Result)) then
|
||
Result := FSlices[i].Id;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer;
|
||
SNBOn, ANFOn: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetSliceNRMode(Id, NRMode);
|
||
FDSPEngine.SetSliceNBMode(Id, NBMode);
|
||
FDSPEngine.SetSliceSNB(Id, SNBOn);
|
||
FDSPEngine.SetSliceANF(Id, ANFOn);
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].FMSQOn := On_;
|
||
FSlices[idx].FMSQLevel := Level;
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetSliceFMSquelch(Id, On_, Level);
|
||
end;
|
||
|
||
function TRadioController.SliceSMeter(Id: Integer): Double;
|
||
begin
|
||
if Assigned(FDSPEngine) then Result := FDSPEngine.GetSliceSMeter(Id)
|
||
else Result := -130.0;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer;
|
||
const DevName: string);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].DeviceIndex := DevIndex;
|
||
FSlices[idx].DevName := DevName;
|
||
if FSlices[idx].Audio <> nil then
|
||
begin
|
||
FSlices[idx].Audio.Close;
|
||
FSlices[idx].Audio.DeviceIndex := DevIndex;
|
||
if FLocalAudio then FSlices[idx].Audio.Open;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.SliceCount: Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then Inc(Result);
|
||
end;
|
||
|
||
function TRadioController.GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
Result := idx >= 0;
|
||
if Result then S := FSlices[idx];
|
||
end;
|
||
|
||
// Снимок кадра для web-зеркала: протокол фиксирован на 1024 точки, поэтому
|
||
// при большем разрешении анализатора децимируем max-детектором (не обрезаем).
|
||
procedure SnapshotPixels(const Pixels: array of Single; Count: Integer;
|
||
var Dst: array of Single; out DstCount: Integer);
|
||
var
|
||
i, j, S0, S1, N: Integer;
|
||
V: Single;
|
||
begin
|
||
N := Length(Dst);
|
||
if Count > Length(Pixels) then Count := Length(Pixels);
|
||
if Count <= N then
|
||
begin
|
||
for i := 0 to Count - 1 do Dst[i] := Pixels[i];
|
||
DstCount := Count;
|
||
Exit;
|
||
end;
|
||
for i := 0 to N - 1 do
|
||
begin
|
||
S0 := Int64(i) * Count div N;
|
||
S1 := (Int64(i) + 1) * Count div N - 1;
|
||
V := Pixels[S0];
|
||
for j := S0 + 1 to S1 do
|
||
if Pixels[j] > V then V := Pixels[j];
|
||
Dst[i] := V;
|
||
end;
|
||
DstCount := N;
|
||
end;
|
||
|
||
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
||
// (UI рисует FSpecView). Децимации у спектра нет.
|
||
begin
|
||
SnapshotPixels(Pixels, Count, FSpectrumBuf, FSpectrumBufCount);
|
||
if Assigned(FOnSpectrumData) then FOnSpectrumData(Pixels, Count);
|
||
end;
|
||
|
||
procedure TRadioController.OnWaterfallReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
// DSP-поток: web-зеркало получает КАЖДЫЙ кадр; децимация рендера водопада —
|
||
// забота фронтенда (OnWaterfallData).
|
||
begin
|
||
SnapshotPixels(Pixels, Count, FWaterfallBuf, FWaterfallBufCount);
|
||
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;
|
||
var S: TCtrlSlice;
|
||
begin
|
||
// Мультислайс-TX: если выбран слайс-источник — передаём на его частоте
|
||
// (слайсы не несут repeater-конфиг, FM-сдвиг не применяем).
|
||
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then
|
||
Exit(S.TargetHz);
|
||
// 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;
|
||
|
||
function TRadioController.ActiveTXMode: Integer;
|
||
var S: TCtrlSlice;
|
||
begin
|
||
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then Result := S.Mode
|
||
else Result := FMode;
|
||
end;
|
||
|
||
function TRadioController.TxBandIdx: Integer;
|
||
begin
|
||
// При TX на слайсе другого диапазона (только КВ) калибровка/антенна — по
|
||
// банду TX-частоты, иначе главный банд.
|
||
Result := FCurrentBand;
|
||
if (FTxSliceId > 0) and (FCurrentXvtr < 0) then
|
||
begin
|
||
Result := FreqToBandIdx(ActiveTXFreqHz, IsPluto);
|
||
if (Result < 0) or (Result >= CFG_BAND_COUNT) then Result := FCurrentBand;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetTxSlice(SliceId: Integer);
|
||
var S: TCtrlSlice;
|
||
begin
|
||
if SliceId < 0 then SliceId := 0;
|
||
// Слайс-источник должен существовать (0 = главный всегда валиден).
|
||
if SliceId > 0 then
|
||
begin
|
||
if not GetSlice(SliceId, S) then Exit;
|
||
// EWSDR has no DMR transmitter/AMBE encoder.
|
||
if S.Mode = MODE_DMR then Exit;
|
||
end;
|
||
if FTxSliceId = SliceId then Exit;
|
||
// Selecting main A while it is in DMR is allowed as an RX selection, but it
|
||
// cannot inherit an active carrier from a previously selected TX slice.
|
||
if (SliceId = 0) and (FMode = MODE_DMR) then
|
||
begin
|
||
if FTuning then SetTune(False)
|
||
else if FTransmitting then SetMOX(False);
|
||
end;
|
||
FTxSliceId := SliceId;
|
||
// Передающий тракт под режим нового источника (idle-канал, применится к PTT).
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
|
||
ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса
|
||
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
|
||
Changed(rfTransmitting); // UI: перерисовать TX-бейджи флагов/оверлей
|
||
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;
|
||
|
||
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
|
||
begin
|
||
Result := Assigned(FDMRDec);
|
||
if Result then FDMRDec.GetStatus(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
function TRadioController.GetSliceDMRStatus(SliceId: Integer;
|
||
out S: TDMRStatus): Boolean;
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
Idx := FindSliceIndex(SliceId);
|
||
Result := (Idx >= 0) and (FSlices[Idx].Mode = MODE_DMR) and
|
||
Assigned(FSlices[Idx].DMR);
|
||
if Result then FSlices[Idx].DMR.GetStatus(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||
var Half: Integer;
|
||
begin
|
||
Half := BW div 2;
|
||
case Mode of
|
||
MODE_LSB: begin Lo := -BW; Hi := -100; end;
|
||
MODE_USB: begin Lo := 100; Hi := BW; end;
|
||
// DIGI: полоса от нуля — поднесущие цифровых видов начинаются низко,
|
||
// срез на 100 Гц как в SSB съел бы их.
|
||
MODE_DIGL: begin Lo := -BW; Hi := 0; end;
|
||
MODE_DIGU: begin Lo := 0; Hi := BW; end;
|
||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM/WFM
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
|
||
var Lo, Hi: Integer;
|
||
begin
|
||
// DMR has one fixed RF channel width; callers select only the mode.
|
||
if Mode = MODE_DMR then BW := FILT_DMR_BW;
|
||
FilterEdgesFor(Mode, BW, Lo, Hi);
|
||
SetSliceMode(Id, Mode);
|
||
SetSliceFilter(Id, Lo, Hi);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyModeFilter;
|
||
var
|
||
Lo, Hi: Integer;
|
||
begin
|
||
FilterEdgesFor(FMode, FFilterBW, Lo, Hi);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetFilter(Lo, Hi);
|
||
FDSPEngine.SetTXFilter(Lo, Hi);
|
||
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then
|
||
begin
|
||
if FMode = MODE_FMRAW then
|
||
FDSPEngine.SetRawFMDeviation(FRawFMDeviation)
|
||
else
|
||
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
||
if FMode <> MODE_FMRAW then
|
||
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||
// A squelch would remove sync preambles; DMR always receives open.
|
||
FDSPEngine.SetFMSquelch((FMode = MODE_FM) and 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;
|
||
MODE_WFM:
|
||
begin
|
||
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
|
||
FFilterBW := FILT_WFM_BW[FFilter];
|
||
end;
|
||
MODE_DMR:
|
||
begin
|
||
FFilter := 0;
|
||
FFilterBW := FILT_DMR_BW;
|
||
FFMDeviation := DMR_FM_DEV;
|
||
end;
|
||
MODE_FMRAW:
|
||
begin
|
||
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
|
||
FFilterBW := FILT_RAW_BW[FFilter];
|
||
end;
|
||
MODE_LSB, MODE_USB:
|
||
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
||
MODE_DIGU, MODE_DIGL:
|
||
begin FFilter := FILT_DIGI_DEF; FFilterBW := FILT_DIGI_BW[FFilter]; end;
|
||
MODE_DSB:
|
||
begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
|
||
MODE_CWL, MODE_CWU:
|
||
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, FILT_FM_COUNT - 1)];
|
||
MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
|
||
MODE_DMR: Result := FILT_DMR_BW;
|
||
MODE_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)];
|
||
MODE_LSB, MODE_USB:
|
||
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||
MODE_DIGU, MODE_DIGL:
|
||
Result := FILT_DIGI_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||
MODE_DSB: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||
MODE_CWL, MODE_CWU:
|
||
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 (TxBandIdx >= 0) and (TxBandIdx < CFG_BAND_COUNT) then
|
||
Cal := FPABandCal[TxBandIdx] // банд TX-слайса при кросс-банд TX
|
||
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;
|
||
if FMode = MODE_DMR then
|
||
begin
|
||
FFilter := 0;
|
||
FFilterBW := FILT_DMR_BW;
|
||
FFMDeviation := DMR_FM_DEV;
|
||
end;
|
||
if FMode = MODE_FMRAW then
|
||
begin
|
||
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
|
||
FFilterBW := FILT_RAW_BW[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.ZoomFactor := FZoomFactor;
|
||
Result.PanSlider := FPanSlider;
|
||
Result.FMSQOn := FFMSQOn;
|
||
Result.FMSQLevel := FFMSQLevel;
|
||
Result.CTCSSOn := FFMCTCSSOn;
|
||
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
|
||
Result.FMStepOn := FFMStepOn;
|
||
Result.FMStepIdx := FFMStepIdx;
|
||
Result.FMRptDir := FFMRptDir;
|
||
Result.FMRptOffsetHz := FFMRptOffsetHz;
|
||
// Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100).
|
||
Result.RxGainMode := FRxGainMode;
|
||
Result.RxGainDb := FRxGainDb;
|
||
// 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;
|
||
FXvtrSettings.Entries[i].LastZoomFactor := FZoomFactor;
|
||
FXvtrSettings.Entries[i].LastPanSlider := FPanSlider;
|
||
FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode;
|
||
FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb;
|
||
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.TxMonVolume := FTxMonVolume;
|
||
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.Cal := FCal;
|
||
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.WfPalette := FWfPalette;
|
||
Result.WfGamma := FWfGamma;
|
||
Result.DisplayPixels := FDisplayPixels;
|
||
Result.WfSpeedDiv := FWfSpeedDiv;
|
||
Result.SpecMSAA := FSpecMSAA;
|
||
Result.SpecRefLevel := FSpecRefLevel;
|
||
Result.SpecRange := FSpecRange;
|
||
Result.SpecGridStep := FSpecGridStep;
|
||
Result.AudioSampleRate := FAudioOut.SampleRate;
|
||
Result.ShowSpectrum := FShowSpectrum;
|
||
Result.ShowWaterfall := FShowWaterfall;
|
||
Result.ShowWideband := FShowWideband;
|
||
Result.WidebandFill := FWidebandFill;
|
||
Result.SpectrumFill := FSpectrumFill;
|
||
Result.DisplayDuplex := FDisplayDuplex;
|
||
Result.RxMuteOnTx := FRxMuteOnTx;
|
||
Result.AudioOutDevice := FAudioOutDevName;
|
||
Result.AudioInDevice := FAudioInDevName;
|
||
Result.SendAudioToRadio := FSendAudioToRadio;
|
||
Result.RawFMDeviation := Round(FRawFMDeviation);
|
||
end;
|
||
|
||
procedure TRadioController.SaveGlobalSettings;
|
||
begin
|
||
if FDevConnected then FSettings.SaveGlobal(FDevMAC, BuildGlobalSettings);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
|
||
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
|
||
begin
|
||
// --- Режим ---
|
||
FMode := EnsureRange(B.Mode, MODE_MIN, MODE_MAX);
|
||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||
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;
|
||
|
||
// --- Аппаратный AGC AD936x (Pluto/LibreSDR): режим + manual-гейн диапазона.
|
||
// У openHPSDR SetRxGain — no-op, поля просто ездят с диапазоном.
|
||
FRxGainMode := EnsureRange(B.RxGainMode, 0, 3);
|
||
FRxGainDb := EnsureRange(B.RxGainDb, 0, 73);
|
||
ApplyRxGainToBackend;
|
||
Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок
|
||
|
||
// --- 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);
|
||
if FMode in [MODE_DMR, MODE_FMRAW] then
|
||
begin
|
||
FFMStepOn := True;
|
||
FFMStepIdx := 1; // DMR default raster: 12.5 kHz
|
||
end;
|
||
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);
|
||
|
||
// --- Zoom/pan диапазона: восстанавливаем и переармируем анализатор ---
|
||
FZoomFactor := EnsureRange(B.ZoomFactor, 0.0, 1.0);
|
||
FPanSlider := EnsureRange(B.PanSlider, 0.0, 1.0);
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||
Changed(rfZoom);
|
||
|
||
// --- 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);
|
||
ApplyAntPorts; // AD936x: разъёмы, выбранные для этого диапазона
|
||
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
|
||
MODE_LSB: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
|
||
MODE_USB: 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;
|
||
|
||
RefreshSliceShifts; // центр мог сместиться — держим слайсы на их TargetHz
|
||
if NeedPush then PushNetworkState;
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
|
||
begin
|
||
if FVfoLock then Exit; // VFO заблокирован — перестройку игнорируем
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
|
||
else SetVfoB(FVfoB + DeltaHz);
|
||
end;
|
||
|
||
procedure TRadioController.SetCenter(Hz: Double);
|
||
// CTUN-скролл окна просмотра: двигаем DDC-центр БЕЗ смены VFO. Демодулятор
|
||
// сдвигаем = активный VFO − новый центр; полный HP-кадр в радио. Рендер
|
||
// (ruler/спектр) — в UI через OnControllerState(rfCenterFreq).
|
||
// Точки входа: десктопный CTUN-drag и web (SyncWebCenter).
|
||
begin
|
||
FCenterFreq := Hz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
|
||
RefreshSliceShifts; // слайсы привязаны к абсолютной частоте — сдвинуть NCO
|
||
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);
|
||
RefreshSliceShifts;
|
||
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
|
||
M := EnsureRange(M, MODE_MIN, MODE_MAX);
|
||
FMode := M;
|
||
// DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP
|
||
// controls enabled so the operator can select another raster afterwards.
|
||
if FMode in [MODE_DMR, MODE_FMRAW] then
|
||
begin
|
||
FFMStepOn := True;
|
||
if FMode = MODE_DMR then FFMStepIdx := 1; // 12.5 kHz
|
||
Changed(rfFMStep);
|
||
Changed(rfFMStepIdx);
|
||
end;
|
||
// Entering receive-only DMR on the selected main TX source immediately
|
||
// releases an existing carrier. SetTune(False) also releases MOX.
|
||
if ActiveTXMode = MODE_DMR then
|
||
begin
|
||
if FTuning then SetTune(False)
|
||
else if FTransmitting then SetMOX(False);
|
||
end;
|
||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
||
FBandCache[FCurrentBand].Mode := FMode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
// SetMode configures the main RX/TX pair; a selected slice may still be
|
||
// the actual TX source and therefore owns the transmitter mode.
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// A live mode change must also switch away from web/HW microphone when
|
||
// entering FM RAW; normally the modem is connected through PortAudio.
|
||
if FTransmitting and (ActiveTXMode = MODE_FMRAW) then
|
||
begin
|
||
FWebMicActive := False;
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
if Assigned(FAudioIn) and FAudioIn.IsOpen then FAudioIn.Flush;
|
||
end;
|
||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||
end;
|
||
ApplyModeFilter;
|
||
Changed(rfMode);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||
begin
|
||
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
|
||
// Idx может прийти из CAT/web без проверки; FM/WFM-таблицы короче остальных.
|
||
case FMode of
|
||
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
|
||
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
|
||
MODE_DMR: Idx := 0;
|
||
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
|
||
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
||
end;
|
||
FFilter := Idx;
|
||
case FMode of
|
||
MODE_LSB, MODE_USB: FFilterBW := FILT_SSB_BW[Idx];
|
||
MODE_DIGU, MODE_DIGL: FFilterBW := FILT_DIGI_BW[Idx];
|
||
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
|
||
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
|
||
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
|
||
MODE_DMR: FFilterBW := FILT_DMR_BW;
|
||
MODE_FMRAW: FFilterBW := FILT_RAW_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
|
||
if FMode = MODE_DMR then BWHz := FILT_DMR_BW;
|
||
FFilterBW := BWHz;
|
||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
|
||
case FMode of
|
||
MODE_LSB, MODE_USB:
|
||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
||
MODE_DIGU, MODE_DIGL:
|
||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DIGI_BW[i];
|
||
MODE_DSB: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
|
||
MODE_CWL, MODE_CWU:
|
||
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];
|
||
MODE_WFM: for i := 0 to FILT_WFM_COUNT-1 do BW_arr[i] := FILT_WFM_BW[i];
|
||
MODE_DMR: BW_arr[0] := FILT_DMR_BW;
|
||
MODE_FMRAW: for i := 0 to FILT_RAW_COUNT-1 do BW_arr[i] := FILT_RAW_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.SetRawFMDeviation(Hz: Integer);
|
||
begin
|
||
FRawFMDeviation := EnsureRange(Hz, RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
|
||
SaveGlobalSettings;
|
||
Changed(rfFMDeviation);
|
||
end;
|
||
|
||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
|
||
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
|
||
const
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
begin
|
||
if (M < 0) or (M > 4) then M := 1;
|
||
Result := AGCModes[M];
|
||
end;
|
||
|
||
procedure TRadioController.ApplyAGCToEngine;
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
||
FDSPEngine.SetAGC(AGCModeFromUI(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;
|
||
|
||
function TRadioController.MonitoringTX: Boolean;
|
||
begin
|
||
// Звук даунлинка на передаче идёт только в full-duplex (DUP) при выключенном
|
||
// RX MUTE — тот же гейт, что в движке (FKeepRXDuringTX and FMonitorRXDuringTX).
|
||
Result := FTransmitting and FDisplayDuplex and (not FRxMuteOnTx);
|
||
end;
|
||
|
||
function TRadioController.ActiveVolume: Integer;
|
||
begin
|
||
if MonitoringTX then Result := FTxMonVolume else Result := FVolume;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyActiveVolume;
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetVolume(ActiveVolume / 100.0);
|
||
Changed(rfVolume); // слайдер/оверлей ← ActiveVolume
|
||
end;
|
||
|
||
procedure TRadioController.SetVolume(V: Integer);
|
||
begin
|
||
if V < 0 then V := 0 else if V > 100 then V := 100;
|
||
// Правим ту громкость, что сейчас активна: на TX-мониторе — FTxMonVolume,
|
||
// иначе FVolume. Обе персистятся; слайдер редактирует активную.
|
||
if MonitoringTX then FTxMonVolume := V else FVolume := V;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(ActiveVolume / 100.0);
|
||
Changed(rfVolume);
|
||
end;
|
||
|
||
procedure TRadioController.VolumeBy(Delta: Integer);
|
||
begin SetVolume(ActiveVolume + 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.SetCalibration(const C: TCalibration);
|
||
begin
|
||
FCal := C;
|
||
// Санитизация: битый конфиг не должен дать деление на ноль/абсурдные ватты.
|
||
FCal.PwrDetModel := EnsureRange(FCal.PwrDetModel, 0, 3);
|
||
if FCal.AmpSensMVA <= 0 then FCal.AmpSensMVA := 120.0;
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SMeterCalDB := FCal.SMeterOffsetDB;
|
||
SaveGlobalSettings; // персист (гейт по FDevConnected внутри)
|
||
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;
|
||
// Пустой слот плана (HF: 12-й, зарезервирован под 12-местный план Pluto) —
|
||
// не диапазон, игнорируем (защита CAT/web/BandUp).
|
||
if BandName(Idx, IsPluto) = '' 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.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x)
|
||
B.RxGainDb := E.LastRxGainDb;
|
||
B.VfoA := Vis;
|
||
B.VfoB := FVfoB;
|
||
ApplyBandDSP(B);
|
||
|
||
// Zoom/pan слота: восстанавливаем и переармируем анализатор.
|
||
FZoomFactor := EnsureRange(E.LastZoomFactor, 0.0, 1.0);
|
||
FPanSlider := EnsureRange(E.LastPanSlider, 0.0, 1.0);
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||
Changed(rfZoom);
|
||
|
||
// 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;
|
||
RefreshPanDDCs; // живые паны — в диапазон XVTR, DDC — на IF
|
||
ApplyAntPorts; // AD936x: разъёмы, выбранные для этого трансвертера
|
||
|
||
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
|
||
RefreshPanDDCs; // живые паны — обратно в пределы железа
|
||
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;
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// Zoom / pan спектра и водопада
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
function TRadioController.ZoomFactor: Double;
|
||
begin Result := FZoomFactor; end;
|
||
|
||
function TRadioController.PanSlider: Double;
|
||
begin Result := FPanSlider; end;
|
||
|
||
procedure ViewWindowFor(BaseC, BaseS, AZoom, APan: Double;
|
||
out ACenter, ASpan: Double);
|
||
// Видимое окно (центр+span, Гц) с учётом зума. Формула совпадает с расчётом
|
||
// span-clip в WDSPEngine.Apply{RX,Pan}AnalyzerSettings (bw=BaseS, clp=0),
|
||
// поэтому оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
|
||
var
|
||
FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double;
|
||
begin
|
||
if (AZoom <= 0.0) or (BaseS <= 0.0) then
|
||
begin
|
||
ACenter := BaseC;
|
||
ASpan := BaseS;
|
||
Exit;
|
||
end;
|
||
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 0.0, 1.0) + 1.0);
|
||
FVis := 1.0 - OneMinusFVis; // доля span после зума
|
||
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0; // ZOOM_LIMIT
|
||
VisSpan := FVis * BaseS;
|
||
LowEdgeOff := -BaseS / 2.0 + EnsureRange(APan, 0.0, 1.0) * OneMinusFVis * BaseS;
|
||
CenterOff := LowEdgeOff + VisSpan / 2.0;
|
||
ACenter := BaseC + CenterOff;
|
||
ASpan := VisSpan;
|
||
end;
|
||
|
||
procedure TRadioController.GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||
begin
|
||
ViewWindowFor(FCenterFreq, FSpanHz, FZoomFactor, FPanSlider, ACenter, ASpan);
|
||
end;
|
||
|
||
function TRadioController.AnchoredPan(AZoom: Double): Double;
|
||
// Положение пана, при котором видимое окно центрировано на маркере активного
|
||
// VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер
|
||
// перестаёт быть строго по центру — окно упирается в край).
|
||
var OneMinusFVis, U: Double;
|
||
begin
|
||
if (AZoom <= 0.0) or (FSpanHz <= 0.0) then Exit(0.5);
|
||
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 0.0, 1.0) + 1.0);
|
||
if OneMinusFVis <= 1e-9 then Exit(0.5);
|
||
U := (ActiveVfoHz - FCenterFreq) / FSpanHz; // нормализованный сдвиг маркера
|
||
Result := EnsureRange(0.5 + U / OneMinusFVis, 0.0, 1.0);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyZoom(AZoom, APan: Double);
|
||
begin
|
||
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
|
||
FPanSlider := EnsureRange(APan, 0.0, 1.0);
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||
// Кэшируем в активный слот (XVTR-слот при активном трансверторе, иначе HF-band);
|
||
// персист на диск — в SaveCurrentBand при смене/выходе.
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
if FCurrentXvtr < CFG_XVTR_COUNT then
|
||
begin
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastZoomFactor := FZoomFactor;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastPanSlider := FPanSlider;
|
||
end;
|
||
end
|
||
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||
begin
|
||
FBandCache[FCurrentBand].ZoomFactor := FZoomFactor;
|
||
FBandCache[FCurrentBand].PanSlider := FPanSlider;
|
||
end;
|
||
Changed(rfZoom);
|
||
end;
|
||
|
||
// Лестница кратностей увеличения: мелкий шаг в начале (1.0→1.2→1.45…), крупнее
|
||
// к концу. Кнопки [+]/[-] шагают по ней; ZoomFactor (0..1) хранит соответствующий
|
||
// z. Перевод кратность↔z — инверсия формулы анализатора (см. ApplyRXAnalyzerSettings).
|
||
const
|
||
ZOOM_MAG_LADDER: array[0..16] of Double =
|
||
(1.0, 1.1, 1.15, 1.2, 1.45, 1.75, 2.1, 2.5, 3.0, 3.7, 4.7, 6.2, 8.5, 12.0, 20.0, 40.0, 100.0);
|
||
|
||
function ZoomMagToZ(M: Double): Double;
|
||
begin
|
||
if M <= 1.0 then Exit(0.0);
|
||
// M = 1/(1-0.99*log10(9z+1)) ⇒ z = (10^((1-1/M)/0.99) - 1)/9
|
||
Result := EnsureRange((Power(10.0, (1.0 - 1.0 / M) / 0.99) - 1.0) / 9.0, 0.0, 1.0);
|
||
end;
|
||
|
||
function ZoomZToMag(Z: Double): Double;
|
||
begin
|
||
Result := 1.0 / (1.0 - 0.99 * Log10(9.0 * EnsureRange(Z, 0.0, 1.0) + 1.0));
|
||
end;
|
||
|
||
procedure TRadioController.ZoomIn;
|
||
var CurMag, Z: Double; i: Integer;
|
||
begin
|
||
CurMag := ZoomZToMag(FZoomFactor);
|
||
for i := 0 to High(ZOOM_MAG_LADDER) do
|
||
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
|
||
begin
|
||
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||
ApplyZoom(Z, AnchoredPan(Z));
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ZoomOut;
|
||
var CurMag, Z: Double; i: Integer;
|
||
begin
|
||
CurMag := ZoomZToMag(FZoomFactor);
|
||
for i := High(ZOOM_MAG_LADDER) downto 0 do
|
||
if ZOOM_MAG_LADDER[i] < CurMag / 1.001 then
|
||
begin
|
||
if ZOOM_MAG_LADDER[i] <= 1.0 then ApplyZoom(0.0, 0.5)
|
||
else
|
||
begin
|
||
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||
ApplyZoom(Z, AnchoredPan(Z));
|
||
end;
|
||
Exit;
|
||
end;
|
||
ApplyZoom(0.0, 0.5);
|
||
end;
|
||
|
||
procedure TRadioController.ZoomDefault;
|
||
begin
|
||
ApplyZoom(0.0, 0.5);
|
||
end;
|
||
|
||
{ ---- Зум/пан доп. панов (PanId=0 делегирует главному) ---- }
|
||
|
||
procedure TRadioController.GetPanViewWindow(PanId: Integer;
|
||
out ACenter, ASpan: Double);
|
||
begin
|
||
if PanId = 0 then
|
||
begin
|
||
GetViewWindow(ACenter, ASpan);
|
||
Exit;
|
||
end;
|
||
ACenter := 0; ASpan := 0;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
ViewWindowFor(FPanFreqHz[PanId], FPanRateKHz[PanId] * 1000.0,
|
||
FPanZoomF[PanId], FPanPanSl[PanId], ACenter, ASpan);
|
||
end;
|
||
|
||
procedure TRadioController.SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
|
||
// Зум пана — состояние сессии пана (не персистится с бандом, в отличие от
|
||
// главного ApplyZoom): пан живёт до STOP.
|
||
begin
|
||
if PanId = 0 then
|
||
begin
|
||
ApplyZoom(AZoom, APan);
|
||
Exit;
|
||
end;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
FPanZoomF[PanId] := EnsureRange(AZoom, 0.0, 1.0);
|
||
FPanPanSl[PanId] := EnsureRange(APan, 0.0, 1.0);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetPanZoomPan(PanId, FPanZoomF[PanId], FPanPanSl[PanId]);
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomStepTo(PanId: Integer; NewZ: Double);
|
||
// Ставит новый зум, удерживая ЦЕНТР текущего видимого окна (у пана нет VFO-якоря
|
||
// главного AnchoredPan; фиксация центра — естественный аналог).
|
||
var
|
||
C, S, BaseC, BaseS, U, OneMinus, FVis, NewPan: Double;
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
GetPanViewWindow(PanId, C, S);
|
||
BaseC := FPanFreqHz[PanId];
|
||
BaseS := FPanRateKHz[PanId] * 1000.0;
|
||
if BaseS <= 0 then Exit;
|
||
NewZ := EnsureRange(NewZ, 0.0, 1.0);
|
||
// Инверсия ViewWindowFor: U = CenterOff/BaseS = −0.5 + Pan·OneMinus + FVis/2.
|
||
U := (C - BaseC) / BaseS;
|
||
OneMinus := 0.99 * Log10(9.0 * NewZ + 1.0);
|
||
if OneMinus <= 1e-9 then
|
||
NewPan := 0.5
|
||
else
|
||
begin
|
||
FVis := 1.0 - OneMinus;
|
||
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0;
|
||
NewPan := EnsureRange((U + 0.5 - FVis / 2.0) / OneMinus, 0.0, 1.0);
|
||
end;
|
||
SetPanZoomPan(PanId, NewZ, NewPan);
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomIn(PanId: Integer);
|
||
var CurMag: Double; i: Integer;
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
CurMag := ZoomZToMag(FPanZoomF[PanId]);
|
||
for i := 0 to High(ZOOM_MAG_LADDER) do
|
||
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
|
||
begin
|
||
PanZoomStepTo(PanId, ZoomMagToZ(ZOOM_MAG_LADDER[i]));
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomOut(PanId: Integer);
|
||
var CurMag: Double; i: Integer;
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
CurMag := ZoomZToMag(FPanZoomF[PanId]);
|
||
for i := High(ZOOM_MAG_LADDER) downto 0 do
|
||
if ZOOM_MAG_LADDER[i] < CurMag / 1.001 then
|
||
begin
|
||
if ZOOM_MAG_LADDER[i] <= 1.0 then SetPanZoomPan(PanId, 0.0, 0.5)
|
||
else PanZoomStepTo(PanId, ZoomMagToZ(ZOOM_MAG_LADDER[i]));
|
||
Exit;
|
||
end;
|
||
SetPanZoomPan(PanId, 0.0, 0.5);
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomDefault(PanId: Integer);
|
||
begin
|
||
SetPanZoomPan(PanId, 0.0, 0.5);
|
||
end;
|
||
|
||
function TRadioController.PanZoomFactor(PanId: Integer): Double;
|
||
begin
|
||
if PanId = 0 then Result := FZoomFactor
|
||
else if PanDDCActive(PanId) then Result := FPanZoomF[PanId]
|
||
else Result := 0.0;
|
||
end;
|
||
|
||
function TRadioController.PanPanSlider(PanId: Integer): Double;
|
||
begin
|
||
if PanId = 0 then Result := FPanSlider
|
||
else if PanDDCActive(PanId) then Result := FPanPanSl[PanId]
|
||
else Result := 0.5;
|
||
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. Меняем rate главного тракта. Хирургически: движок гейтит и дропает
|
||
// только main-пакеты, DSP-поток/анализаторы/паны N живут без пауз —
|
||
// поэтому FWDSPReady НЕ сбрасываем (иначе OnDDCIQ перестал бы кормить
|
||
// паны на всё время пересборки и их водопады замирали).
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.ChangeSampleRate(Hz);
|
||
|
||
// 2. Новый rate трансиверу. Короткий хвост пакетов старого rate, дошедших
|
||
// до переключения радио, попадает в новый канал — транзиент в единицы мс
|
||
// (то же окно было и при старой схеме: FWDSPReady поднимался до того, как
|
||
// радио реально применяло DDC Specific).
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
FNetwork.ConfigureDDCs(1, Hz div 1000, FMainADCSrc,
|
||
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;
|
||
ApplyActiveVolume; // смена DUP на передаче может включить/выключить TX-монитор
|
||
Changed(rfDuplex);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyRxMuteToEngine;
|
||
begin
|
||
// Монитор имеет смысл только в full-duplex (KeepRXDuringTX); сам гейт в движке
|
||
// это учитывает. Здесь просто транслируем инверсию флага RX MUTE.
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.MonitorRXDuringTX := not FRxMuteOnTx;
|
||
end;
|
||
|
||
procedure TRadioController.SetRxMuteOnTx(On_: Boolean);
|
||
begin
|
||
FRxMuteOnTx := On_;
|
||
ApplyRxMuteToEngine;
|
||
ApplyActiveVolume; // на передаче тумблер монитора меняет активную громкость
|
||
Changed(rfRxMuteOnTx);
|
||
end;
|
||
|
||
procedure TRadioController.SetWfAGC(On_: Boolean);
|
||
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
|
||
|
||
procedure TRadioController.SetWfNF(On_: Boolean);
|
||
begin FWfNFEnabled := On_; Changed(rfWfNF); end;
|
||
|
||
procedure TRadioController.SetNR(Mode: Integer);
|
||
begin
|
||
if Mode < 0 then Mode := 0 else if Mode > 4 then Mode := 4;
|
||
FNRMode := Mode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNRMode(FNRMode);
|
||
Changed(rfNR);
|
||
end;
|
||
|
||
procedure TRadioController.SetNB(Mode: Integer);
|
||
begin
|
||
if Mode < 0 then Mode := 0 else if Mode > 2 then Mode := 2;
|
||
FNBMode := Mode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNBMode(FNBMode);
|
||
Changed(rfNB);
|
||
end;
|
||
|
||
procedure TRadioController.SetSNB(On_: Boolean);
|
||
begin
|
||
FSNB := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetSNB(FSNB);
|
||
Changed(rfSNB);
|
||
end;
|
||
|
||
procedure TRadioController.SetANF(On_: Boolean);
|
||
begin
|
||
FANF := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetANF(FANF);
|
||
Changed(rfANF);
|
||
end;
|
||
|
||
function TRadioController.DefaultMicSource: TTXMicSource;
|
||
begin
|
||
if FAudioInDevName <> '' then Result := txmsSoundCard
|
||
else Result := txmsRadio;
|
||
end;
|
||
|
||
function TRadioController.BuildMicLineSelectByte: Byte;
|
||
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
|
||
// bit 0 — Line In (1 = line-in вместо mic-in)
|
||
// bit 1 — Mic Boost (+20 dB); не ставим когда Line In
|
||
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
|
||
// bit 3 — Tip/Ring (0=Tip, 1=Ring; Orion/OrionMkII)
|
||
// bit 4 — Orion mic bias (1 = enable +5V bias)
|
||
// bit 5 — Saturn XLR/balanced (не используем)
|
||
begin
|
||
Result := 0;
|
||
if FTXSettings.MicLineIn then
|
||
Result := Result or $01 // bit 0: Line In
|
||
else if FTXSettings.MicBoost then
|
||
Result := Result or $02; // bit 1: Boost только при Mic In
|
||
if not FTXSettings.MicPTTEnabled then Result := Result or $04; // bit 2: инверсия
|
||
if FTXSettings.MicTipRing then Result := Result or $08; // bit 3: Ring
|
||
if FTXSettings.MicBias then Result := Result or $10; // bit 4: bias
|
||
end;
|
||
|
||
procedure TRadioController.ApplyNoiseFilterButtonsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
FDSPEngine.SetNRMode(FNRMode);
|
||
FDSPEngine.SetNBMode(FNBMode);
|
||
FDSPEngine.SetSNB(FSNB);
|
||
FDSPEngine.SetANF(FANF);
|
||
end;
|
||
|
||
procedure TRadioController.SendDUCSpecificFromSettings;
|
||
var
|
||
DUCPkt: TDUCSpecificPacket;
|
||
RateKsps: Word;
|
||
begin
|
||
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
||
RateKsps := FDSPEngine.TXSampleRate div 1000;
|
||
DUCPkt.NumDACs := 1;
|
||
DUCPkt.SidetoneLevel := 50;
|
||
DUCPkt.SidetoneFreqHi := Hi(600);
|
||
DUCPkt.SidetoneFreqLo := Lo(600);
|
||
DUCPkt.KeyerSpeed := 20;
|
||
DUCPkt.KeyerWeight := 50;
|
||
DUCPkt.DUC0RateHi := Hi(RateKsps);
|
||
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
||
DUCPkt.DUC0Bits := 24;
|
||
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
||
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
|
||
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
|
||
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
|
||
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
|
||
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
|
||
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
|
||
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
|
||
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
|
||
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
|
||
FNetwork.SendDUCSpecific(DUCPkt);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyTXSettingsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2:
|
||
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
|
||
// имеет, поэтому при смене бэкенда явно выключаем его.
|
||
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
|
||
// 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);
|
||
// Reapply the mode overlay last: FM RAW must stay flat even when the user
|
||
// edits and saves ordinary voice-chain settings while this mode is active.
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// PureSignal-параметры calcc (mox/loop/tx delay, ptol, pin/map/stbl, SetPk)
|
||
ApplyPSSettingsToDSP;
|
||
end;
|
||
|
||
procedure TRadioController.SetMOX(On_: Boolean);
|
||
var
|
||
WasTransmitting: Boolean;
|
||
begin
|
||
// DMR support is receive-only. This guard also covers CAT, web and hardware
|
||
// PTT. FM RAW is a flat external-modem transmit path.
|
||
if On_ and (ActiveTXMode = MODE_DMR) then Exit;
|
||
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
|
||
if On_ and FDevConnected and (FCurrentXvtr < 0)
|
||
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 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_;
|
||
// 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник.
|
||
if (not On_) and FTwoTone then
|
||
begin
|
||
FTwoTone := False;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTwoTone(False, 0, 0);
|
||
Changed(rfTwoTone);
|
||
end;
|
||
if On_ then
|
||
begin
|
||
FDriveLevel := CalcDriveByte;
|
||
ApplyAntPorts; // AD936x: страховка — верный разъём до выхода в эфир
|
||
end;
|
||
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 после переключения реле
|
||
// PureSignal: перевести feedback-DDC0/1 в TX-конфигурацию (частоты = TX,
|
||
// sync, Alex bit18). No-op если PS не вооружён (гейт внутри бэкенда).
|
||
FNetwork.SetPureSignalTX(True);
|
||
// Pre-roll: THROTTLE (2000, порог FPGA — DAC не стартует с пустого буфера)
|
||
// + ДВА WDSP-блока (2×2048) нулевых пар подушкой в очередь. Раньше было 3
|
||
// блока: обоснование «2 не хватало — щелчок через ~0.5с» теперь под вопросом,
|
||
// т.к. тот щелчок по времени (0.5с, не ~21мс) похож не на underrun подушки, а
|
||
// на bfo-глитч тона (устранён коммитом f2d0519). Снижено до 2 блоков ради
|
||
// латентности старта TX (~10мс). Если под нагрузкой DUP / быстром пере-PTT
|
||
// вернётся щелчок РОВНО на старте — это реальный underrun, вернуть 8144.
|
||
FNetwork.PrimeDUCIQ(6096);
|
||
end;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
// Mic source автоматически по источнику TX. FM RAW always uses the
|
||
// configured default input (normally a virtual PortAudio cable), even if
|
||
// a web client is connected or hardware PTT keyed the transmitter.
|
||
if FTransmitting then
|
||
begin
|
||
if ActiveTXMode = MODE_FMRAW then
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource)
|
||
else 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;
|
||
// Сброс бэклога sound-card микрофона на RX→TX: пока шёл приём, ринг
|
||
// PortAudio наполнился до упора (~200мс). Без сброса весь этот «старый»
|
||
// звук бёрстом уходит в тракт: стоячая задержка ~190мс в DUC-очереди
|
||
// + в эфир передаётся аудио, записанное ДО нажатия PTT (на TUN оно
|
||
// складывается с тоном поверх полной шкалы → клип-всплеск).
|
||
if FTransmitting and (not WasTransmitting) and Assigned(FAudioIn)
|
||
and FAudioIn.IsOpen then
|
||
FAudioIn.Flush;
|
||
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
|
||
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
|
||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
||
// Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог
|
||
// оставить главный режим). Ставим перед стартом TX-потока.
|
||
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
|
||
FDSPEngine.PSSetMox(FTransmitting);
|
||
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
|
||
// PureSignal: вернуть DDC0/1 в RX-конфигурацию (убрать feedback-DDC).
|
||
FNetwork.SetPureSignalTX(False);
|
||
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 на приёме
|
||
ApplyActiveVolume; // T/R: переключаем громкость RX ↔ TX-монитор (если monitoring)
|
||
Changed(rfTransmitting);
|
||
end;
|
||
|
||
procedure TRadioController.SetTune(On_: Boolean);
|
||
begin
|
||
if On_ = FTuning then Exit;
|
||
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
|
||
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
|
||
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
|
||
if On_ and FDevConnected and (FCurrentXvtr < 0)
|
||
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then Exit;
|
||
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit;
|
||
if On_ and FTwoTone then SetTwoTone(False); // TUN поверх 2TON — гасим тона
|
||
if On_ then
|
||
begin
|
||
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||
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;
|
||
|
||
// ===========================================================================
|
||
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
|
||
// ===========================================================================
|
||
|
||
function TRadioController.PSAvailable: Boolean;
|
||
begin
|
||
Result := FDevConnected and BackendCaps.HasPureSignal;
|
||
end;
|
||
|
||
procedure TRadioController.PSSetFeedbackPath(En: Boolean);
|
||
// Единая точка вкл/выкл feedback-тракта: сеть (DDC0/1 на TX) + движок (pscc).
|
||
// Намеренно БЕЗ раннего выхода при том же значении: движок сбрасывает armed
|
||
// при Close (ChangeSampleRate), сеть гейтится внутри — повторный вызов с тем
|
||
// же значением дёшев и восстанавливает рассинхрон.
|
||
begin
|
||
FPSFeedbackOn := En;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetPureSignalArmed(En);
|
||
if Assigned(FNetwork) then
|
||
begin
|
||
FNetwork.SetPureSignal(En);
|
||
// Включение/выключение посреди передачи — сразу перестроить DDC.
|
||
if FTransmitting then FNetwork.SetPureSignalTX(En);
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetPureSignal(On_: Boolean);
|
||
// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение —
|
||
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
|
||
begin
|
||
if On_ = FPSEnabled then Exit;
|
||
if On_ and not PSAvailable then Exit;
|
||
FPSEnabled := On_;
|
||
if On_ then
|
||
begin
|
||
ApplyPSSettingsToDSP; // параметры calcc до старта калибровки
|
||
FPSResetReq := False;
|
||
end
|
||
else
|
||
FPSResetReq := True;
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
procedure TRadioController.PSSingleCal;
|
||
// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается.
|
||
begin
|
||
if not PSAvailable then Exit;
|
||
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
|
||
ApplyPSSettingsToDSP;
|
||
FPSEnabled := False;
|
||
FPSSingleCal := True;
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
procedure TRadioController.PSReset;
|
||
begin
|
||
FPSEnabled := False;
|
||
FPSSingleCal := False;
|
||
FPSResetReq := True;
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
function TRadioController.PSFBTarget: Integer;
|
||
begin
|
||
Result := FTXSettings.PSFBTarget;
|
||
if Result <= 0 then
|
||
begin
|
||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
|
||
else
|
||
Result := 152; // классическая цель Thetis
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.PSOutlierSigmaEff: Double;
|
||
begin
|
||
Result := FTXSettings.PSOutlierSigma;
|
||
if Result < 0 then
|
||
begin
|
||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||
Result := 5.0 // Orion MkII: дефолт Thetis-Enhanced eu2av
|
||
else
|
||
Result := 0.0; // выключен
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyPSSettingsToDSP;
|
||
var
|
||
Ints, Spi: Integer;
|
||
begin
|
||
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||
case FTXSettings.PSIntsIdx of
|
||
1: begin Ints := 8; Spi := 512; end;
|
||
2: begin Ints := 4; Spi := 1024; end;
|
||
else begin Ints := 16; Spi := 256; end;
|
||
end;
|
||
FDSPEngine.PSSetParams(FTXSettings.PSMoxDelay, FTXSettings.PSLoopDelay,
|
||
FTXSettings.PSTXDelayNs, FTXSettings.PSRelaxPtol,
|
||
FTXSettings.PSPin, FTXSettings.PSMap,
|
||
FTXSettings.PSStabilize, Ints, Spi,
|
||
FTXSettings.PSHWPeak, PSOutlierSigmaEff);
|
||
// Правка частот двухтональника при активном 2TON — применяем живьём.
|
||
if FTwoTone then
|
||
FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2);
|
||
end;
|
||
|
||
function TRadioController.PSFeedbackLevel: Integer;
|
||
begin
|
||
Result := FPSInfo[4];
|
||
end;
|
||
|
||
function TRadioController.PSCalCount: Integer;
|
||
begin
|
||
Result := FPSInfo[5];
|
||
end;
|
||
|
||
function TRadioController.PSCorrecting: Boolean;
|
||
begin
|
||
Result := FPSInfo[14] = 1;
|
||
end;
|
||
|
||
function TRadioController.PSMaxTXVal: Double;
|
||
begin
|
||
Result := FPSMaxTX;
|
||
end;
|
||
|
||
function TRadioController.PSSingleCalActive: Boolean;
|
||
begin
|
||
Result := FPSSingleCal
|
||
or (FPSCmdState in [PS_ST_TURNON_SINGLE, PS_ST_SINGLECAL]);
|
||
end;
|
||
|
||
procedure TRadioController.PureSignalTick;
|
||
// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis
|
||
// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы)
|
||
// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback.
|
||
var
|
||
NewAtt: Integer;
|
||
FBTarget: Integer;
|
||
ddB: Double;
|
||
CalAttemptsChanged: Boolean;
|
||
begin
|
||
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||
if not PSAvailable then
|
||
begin
|
||
// Устройство сменилось/отключилось: гарантированно всё погасить.
|
||
if FPSFeedbackOn then PSSetFeedbackPath(False);
|
||
FPSCmdState := PS_ST_OFF;
|
||
Exit;
|
||
end;
|
||
// Полный простой (PS ни разу не включался / выключен и сброшен) — не дёргаем
|
||
// WDSP каждые 100 мс впустую.
|
||
if (FPSCmdState = PS_ST_OFF) and not (FPSEnabled or FPSSingleCal
|
||
or FPSResetReq or FPSFeedbackOn) then Exit;
|
||
|
||
// Опрос статуса WDSP calcc
|
||
Move(FPSInfo[0], FPSInfoPrev[0], SizeOf(FPSInfo));
|
||
FDSPEngine.PSGetInfo(FPSInfo);
|
||
FPSMaxTX := FDSPEngine.PSGetMaxTX;
|
||
|
||
// --- Командная state machine ---
|
||
case FPSCmdState of
|
||
PS_ST_OFF:
|
||
begin
|
||
FDSPEngine.PSControl(1, 0, 0, 0);
|
||
PSSetFeedbackPath(False);
|
||
FPSResetReq := False;
|
||
if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE;
|
||
end;
|
||
PS_ST_TURNON_AUTO:
|
||
begin
|
||
FDSPEngine.PSControl(1, 0, 1, 0);
|
||
PSSetFeedbackPath(True);
|
||
FPSCmdState := PS_ST_AUTOCAL;
|
||
end;
|
||
PS_ST_AUTOCAL:
|
||
begin
|
||
PSSetFeedbackPath(True); // ре-синк (движок мог переоткрыться)
|
||
if FPSResetReq or (not FPSEnabled) then
|
||
FPSCmdState := PS_ST_TURNOFF
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE;
|
||
end;
|
||
PS_ST_TURNON_SINGLE:
|
||
begin
|
||
FDSPEngine.PSControl(1, 1, 0, 0);
|
||
PSSetFeedbackPath(True);
|
||
FPSCmdState := PS_ST_SINGLECAL;
|
||
end;
|
||
PS_ST_SINGLECAL:
|
||
begin
|
||
PSSetFeedbackPath(True); // ре-синк (движок мог переоткрыться)
|
||
FPSSingleCal := False;
|
||
if FPSResetReq then
|
||
FPSCmdState := PS_ST_TURNOFF
|
||
else if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if PSCorrecting then
|
||
FPSCmdState := PS_ST_STAYON;
|
||
end;
|
||
PS_ST_STAYON:
|
||
begin
|
||
// Коррекция применяется, калибровка остановлена, feedback не нужен.
|
||
PSSetFeedbackPath(False);
|
||
if FPSResetReq then
|
||
FPSCmdState := PS_ST_TURNOFF
|
||
else if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE;
|
||
end;
|
||
PS_ST_TURNOFF:
|
||
begin
|
||
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
|
||
// не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF).
|
||
FDSPEngine.PSControl(1, 0, 0, 0);
|
||
PSSetFeedbackPath(True);
|
||
FPSResetReq := False;
|
||
if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE
|
||
else if (not PSCorrecting) and (FPSInfo[15] = 0) then // 0 = LRESET
|
||
FPSCmdState := PS_ST_OFF;
|
||
end;
|
||
end;
|
||
|
||
// --- Auto-attenuate (Thetis-Enhanced eu2av timer2code) ---
|
||
// По завершении очередной попытки калибровки: если feedback слишком горячий
|
||
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
|
||
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
|
||
// фидбек-АЦП интермодулирует сам, и PS корректирует по искажённому сигналу).
|
||
FBTarget := PSFBTarget;
|
||
case FPSAAState of
|
||
0: begin
|
||
CalAttemptsChanged := FPSInfo[5] <> FPSInfoPrev[5];
|
||
if FTXSettings.PSAutoAtten and CalAttemptsChanged
|
||
and ((FPSInfo[4] > Round(FBTarget * 1.5)) or
|
||
((FPSInfo[4] < Round(FBTarget * 0.7)) and (FTXSettings.AttOnTX > 0))) then
|
||
begin
|
||
FPSAAState := 1;
|
||
if (FPSInfo[4] > 0) and (FPSInfo[4] <= 256) then
|
||
begin
|
||
ddB := 20.0 * Log10(FPSInfo[4] / FBTarget);
|
||
if ddB < -100.0 then ddB := -100.0;
|
||
if ddB > +100.0 then ddB := +100.0;
|
||
end
|
||
else
|
||
ddB := 31.1;
|
||
FPSAADeltaDB := Round(ddB);
|
||
FPSAASaveAuto := Ord(FPSCmdState = PS_ST_AUTOCAL);
|
||
FPSAASaveSingle := Ord(FPSCmdState = PS_ST_SINGLECAL);
|
||
FDSPEngine.PSControl(1, 0, 0, 0); // всё стоп + reset на время смены
|
||
end;
|
||
end;
|
||
1: begin
|
||
FPSAAState := 2;
|
||
NewAtt := EnsureRange(FTXSettings.AttOnTX + FPSAADeltaDB, 0, 31);
|
||
if NewAtt <> FTXSettings.AttOnTX then
|
||
begin
|
||
FTXSettings.AttOnTX := NewAtt;
|
||
SendDUCSpecificFromSettings; // применить атт. немедленно
|
||
Changed(rfPureSignal); // UI: показать новый ATT
|
||
end;
|
||
end;
|
||
2: begin
|
||
FPSAAState := 0;
|
||
FDSPEngine.PSControl(0, FPSAASaveSingle, FPSAASaveAuto, 0);
|
||
end;
|
||
end;
|
||
|
||
// UI-статус (feedback-лампа/счётчик) обновляем при изменении info
|
||
if not CompareMem(@FPSInfo[0], @FPSInfoPrev[0], SizeOf(FPSInfo)) then
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
procedure TRadioController.SetTwoTone(On_: Boolean);
|
||
// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный
|
||
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
|
||
begin
|
||
if On_ = FTwoTone then Exit;
|
||
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
|
||
if On_ and FTuning then Exit; // не поверх TUN-тона
|
||
// Safety: те же блокировки, что SetMOX/SetTune.
|
||
if On_ and FDevConnected and (FCurrentXvtr < 0)
|
||
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 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.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2);
|
||
FTwoTone := True;
|
||
if not FTransmitting then SetMOX(True);
|
||
end
|
||
else
|
||
begin
|
||
FTwoTone := False;
|
||
if FTransmitting then SetMOX(False);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTwoTone(False, 0, 0);
|
||
end;
|
||
Changed(rfTwoTone);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMStep(Idx: Integer);
|
||
begin
|
||
if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4
|
||
FFMStepIdx := Idx;
|
||
Changed(rfFMStepIdx);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMCTCSS(On_: Boolean);
|
||
begin
|
||
FFMCTCSSOn := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
Changed(rfFMCTCSS);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMCTCSSTone(Idx: Integer);
|
||
begin
|
||
if Idx < 0 then Idx := 0 else if Idx > CTCSS_COUNT - 1 then Idx := CTCSS_COUNT - 1;
|
||
FFMCTCSSToneIdx := Idx;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
Changed(rfFMCTCSSTone);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMSquelch(On_: Boolean);
|
||
begin
|
||
FFMSQOn := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
Changed(rfFMSQ);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMSquelchLevel(V: Integer);
|
||
begin
|
||
FFMSQLevel := V;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
Changed(rfFMSQLevel);
|
||
end;
|
||
|
||
procedure TRadioController.SetSplit(TxOnB: Boolean);
|
||
// Split: TX-VFO = B (TxOnB) либо A. TX-частота берётся из ActiveTXFreqHz
|
||
// (он смотрит на FSplitTxB), поэтому достаточно протолкнуть HP-кадр в сеть.
|
||
// Рендер VFO-дисплеев и подсветки TX-A/TX-B кнопок — через rfActiveVfo.
|
||
begin
|
||
if FSplitTxB = TxOnB then Exit;
|
||
FSplitTxB := TxOnB;
|
||
ApplyAntPorts; // AD936x: TX-разъём — по диапазону нового TX-VFO
|
||
PushNetworkState;
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoLock(On_: Boolean);
|
||
// VFO Lock: пока включён, относительная перестройка (TuneActiveBy — энкодер VFO
|
||
// панели, CAT UP/DN) игнорируется. Подсветка — индикатор aiVFOLock (#11).
|
||
begin
|
||
if FVfoLock = On_ then Exit;
|
||
FVfoLock := On_;
|
||
Changed(rfVfoLock);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMRpt(Dir: Integer);
|
||
begin
|
||
FFMRptDir := Dir;
|
||
FFMRptAutoActive := False; // явная установка направления — ручное действие
|
||
PushNetworkState; // TX-сдвиг применяется через ActiveTXFreqHz
|
||
Changed(rfFMRpt);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMRptOffset(Hz: Double);
|
||
begin
|
||
if Hz <= 0.0 then Exit;
|
||
FFMRptOffsetHz := Hz;
|
||
if FFMRptDir <> RPT_NONE then PushNetworkState;
|
||
Changed(rfFMRpt);
|
||
end;
|
||
|
||
procedure TRadioController.UpdateRptAutoState;
|
||
// Авто-включение MINUS-сдвига в 2-метровом репитерном сегменте и авто-снятие
|
||
// при уходе из него (только если MINUS был выставлен автоматически).
|
||
begin
|
||
if RptAutoDir(FVfoA) = RPT_MINUS then
|
||
begin
|
||
if FFMRptDir = RPT_NONE then
|
||
begin
|
||
FFMRptDir := RPT_MINUS;
|
||
FFMRptAutoActive := True;
|
||
Changed(rfFMRpt);
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
if (FFMRptDir = RPT_MINUS) and FFMRptAutoActive then
|
||
begin
|
||
FFMRptDir := RPT_NONE;
|
||
FFMRptAutoActive := False;
|
||
Changed(rfFMRpt);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.StartStop;
|
||
begin SetRun(not FRunning); end;
|
||
|
||
procedure TRadioController.SetRun(On_: Boolean);
|
||
begin FRunning := On_; Changed(rfRunning); { TODO wiring: логика BtnStartStopClick } end;
|
||
|
||
procedure TRadioController.StartRunning(SpectrumWidth: Integer);
|
||
// «Go-live» последовательность Run=1: General packet, wideband/DDC-конфиг,
|
||
// синхронизация WDSP sample rate, восстановление DSP (mode/volume/filter/NR/AGC),
|
||
// Run=1 → seq-сброс → DDC/DUC Specific + TX-цепь. Рендер run-кнопки/sleep — UI
|
||
// через OnControllerState(rfRunning); статус-бар/FSpecView.AGCTop/resize — обёртка.
|
||
var
|
||
GenPkt: TGeneralPacket;
|
||
begin
|
||
// General packet
|
||
FillChar(GenPkt, SizeOf(GenPkt), 0);
|
||
GenPkt.Command := CMD_GENERAL;
|
||
GenPkt.Flags37 := $08;
|
||
GenPkt.Flags38 := $01;
|
||
GenPkt.PAConfig := $01;
|
||
GenPkt.WBPort[0] := (PORT_WIDEBAND_ADC0 shr 8) and $FF;
|
||
GenPkt.WBPort[1] := PORT_WIDEBAND_ADC0 and $FF;
|
||
GenPkt.WBEnable := IfThen(FShowWideband, 1, 0);
|
||
GenPkt.WBSamplesPerPkt[0] := 512 shr 8;
|
||
GenPkt.WBSamplesPerPkt[1] := 512 and $FF;
|
||
GenPkt.WBSampleSize := 16;
|
||
GenPkt.WBUpdateRate := 70;
|
||
GenPkt.WBPacketsPerFrame := 32;
|
||
if FNetwork.Device.BoardType = 5 then
|
||
GenPkt.AlexEnable := $03
|
||
else
|
||
GenPkt.AlexEnable := $01;
|
||
FNetwork.SendGeneralPacket(GenPkt);
|
||
FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32);
|
||
|
||
// ANGELIA=3, ORION=4, ORION2=5, SATURN=10: DDC0/1 — PureSignal, юзерский с 2
|
||
// (тот же список, что DDCBaseIndex бэкенда; Saturn раньше был пропущен).
|
||
if FNetwork.Device.BoardType in [3, 4, 5, 10] then
|
||
FActiveDDC := 2
|
||
else
|
||
FActiveDDC := 0;
|
||
ResetPanDDCMaps; // пул пан-DDC заново от нового FActiveDDC
|
||
|
||
// Синхронизируем 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, FMainADCSrc,
|
||
FDitherEnabled, FRandomEnabled);
|
||
// Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1).
|
||
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
|
||
// RX hw-gain в бэкенд (Pluto). HPSDR — no-op.
|
||
ApplyRxGainToBackend;
|
||
// Антенные разъёмы AD936x для текущего диапазона/трансвертера. HPSDR — no-op.
|
||
ApplyAntPorts;
|
||
// QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено.
|
||
ApplyRxMuteToEngine;
|
||
SendDUCSpecificFromSettings;
|
||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||
ApplyTXSettingsToDSP;
|
||
ApplyModeFilter;
|
||
|
||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||
if FWDSPReady then
|
||
FDSPEngine.UpdateAGCLines(SpectrumWidth); // ещё не получили ни одного пакета
|
||
|
||
Changed(rfRunning); // UI: caption STOP / MOX.Enabled / InhibitSleep
|
||
end;
|
||
|
||
procedure TRadioController.StopRunning;
|
||
// Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся
|
||
// Connected — Disconnect отдельно (UI-обёртка делает это после persistence).
|
||
// Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting.
|
||
var
|
||
p: Integer;
|
||
begin
|
||
if FRunning then
|
||
begin
|
||
// Персист панов/слайсов ДО их гашения (UI-снимок живого состояния).
|
||
if Assigned(FOnBeforeStop) then FOnBeforeStop;
|
||
// Паны на доп. DDC живут только внутри Run-сессии — гасим до Run=0
|
||
// (3.3 UI пересоздаст по персисту при следующем старте).
|
||
for p := 1 to MAX_PANS - 1 do
|
||
RemovePanDDC(p);
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), 0, False, True, True);
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
// Стоп при активном TX/TUN: хвост IQ в DUC-очереди не должен пережить
|
||
// остановку (иначе при следующем старте уйдёт в эфир старый тон/голос),
|
||
// а TUN-состояние (PostGen-тон) — вернуться в выключенное.
|
||
FNetwork.ClearDUCIQQueue;
|
||
if FTuning then
|
||
begin
|
||
FTuning := False;
|
||
if FWDSPReady then FDSPEngine.SetTXTone(False, 0, 0);
|
||
Changed(rfTuning); // UI: TUN-кнопка off
|
||
end;
|
||
if FTwoTone then
|
||
begin
|
||
FTwoTone := False;
|
||
if FWDSPReady then FDSPEngine.SetTXTwoTone(False, 0, 0);
|
||
Changed(rfTwoTone); // UI: 2TON-кнопка off
|
||
end;
|
||
// PureSignal: погасить калибровку и feedback-тракт (без сброса коррекции —
|
||
// сеть уже останавливается; state machine стартует заново при след. Run).
|
||
if FPSEnabled or FPSSingleCal or FPSFeedbackOn then
|
||
begin
|
||
FPSEnabled := False;
|
||
FPSSingleCal := False;
|
||
FPSResetReq := False;
|
||
FPSCmdState := PS_ST_OFF;
|
||
FPSAAState := 0;
|
||
PSSetFeedbackPath(False);
|
||
if FWDSPReady then FDSPEngine.PSControl(1, 0, 0, 0);
|
||
Changed(rfPureSignal);
|
||
end;
|
||
if FWDSPReady then FDSPEngine.SetTXRun(False);
|
||
Changed(rfTransmitting); // MOX-кнопка off
|
||
end;
|
||
FRXPacketCount := 0;
|
||
FActiveDDC := 0;
|
||
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
|
||
FLastPlutoRSSIdB := TELEMETRY_NONE;
|
||
FPlutoTeleTick := 0; FPlutoTempBad := 0;
|
||
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
|
||
end;
|
||
|
||
function TRadioController.ResolveDevice(const IP: string): THPSDRDevice;
|
||
// discovered→saved по IP/URI: восстанавливает Kind/URI/Serial/BoardType/MAC,
|
||
// иначе простой HPSDR по IP. См. одноимённую обёртку в GUI (TMainForm).
|
||
var
|
||
i: Integer;
|
||
dd: TDiscoveredDevice;
|
||
sd: TSavedDevice;
|
||
begin
|
||
FillChar(Result, SizeOf(Result), 0);
|
||
Result.Kind := bkHPSDR;
|
||
Result.IPAddress := IP;
|
||
|
||
i := FDeviceStore.FindDiscoveredByIP(IP);
|
||
if i >= 0 then
|
||
begin
|
||
dd := FDeviceStore.Discovered(i);
|
||
Result.Kind := dd.Kind;
|
||
Result.URI := dd.URI;
|
||
Result.Serial := dd.Serial;
|
||
Result.BoardType := dd.BoardType;
|
||
Move(dd.MAC[0], Result.MAC[0], 6);
|
||
Exit;
|
||
end;
|
||
|
||
for i := 0 to FDeviceStore.SavedCount - 1 do
|
||
begin
|
||
sd := FDeviceStore.Saved(i);
|
||
if SameText(sd.IPAddress, IP) or SameText(sd.URI, IP) then
|
||
begin
|
||
Result.Kind := sd.Kind;
|
||
Result.URI := sd.URI;
|
||
Result.Serial := sd.Serial;
|
||
Result.BoardType := sd.BoardType;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.AddSavedDevice(const AName, AAddr: string);
|
||
// web-dev_add: Pluto по схеме URI ('ip:'/'usb:'/'local:'), иначе HPSDR-IP.
|
||
// Serial при ручном добавлении неизвестен (допишется при connect/probe).
|
||
var
|
||
A: string;
|
||
begin
|
||
A := Trim(AAddr);
|
||
if A = '' then Exit;
|
||
if (Pos('ip:', A) = 1) or (Pos('usb:', A) = 1) or (Pos('local:', A) = 1) then
|
||
FDeviceStore.AddSavedPluto(AName, A, '')
|
||
else
|
||
FDeviceStore.AddSaved(AName, A, 0);
|
||
end;
|
||
|
||
function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean;
|
||
// Подключение + загрузка per-device настроек по MAC в поля контроллера + конфиг
|
||
// движка (DSP display, audio). Возвращает False, если Connect не удался (UI
|
||
// покажет сообщение по FNetwork.LastError). Рендер ВСЕХ device-виджетов — UI
|
||
// через OnControllerState(rfDevice). Перестройку диапазона/VFO (RestoreBand/
|
||
// XVTR/ActiveVfo) делает UI-обёртка ПОСЛЕ (там финальная перестройка через
|
||
// ApplyVfoA + wideband/web-extras). FLoadedGlobal хранит загруженный G_Settings
|
||
// для рендера полей, которых нет в контроллере (FPS/FreqMhzDigits/CAT).
|
||
var
|
||
i: Integer;
|
||
begin
|
||
// Подбираем бэкенд под тип устройства (HPSDR/Pluto) до подключения.
|
||
EnsureBackend(Dev.Kind);
|
||
Result := FNetwork.Connect(Dev);
|
||
if not Result then Exit;
|
||
|
||
// --- Загружаем настройки устройства по MAC ---
|
||
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
|
||
FDevConnected := True;
|
||
FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache);
|
||
// TX/Alex/XVTR per-device
|
||
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||
FSettings.LoadAlex(FDevMAC, FAlexSettings);
|
||
FSettings.LoadAnt936x(FDevMAC, FAnt936x);
|
||
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;
|
||
FTxMonVolume := EnsureRange(FLoadedGlobal.TxMonVolume, 0, 100);
|
||
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];
|
||
SetCalibration(FLoadedGlobal.Cal);
|
||
// 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;
|
||
FRawFMDeviation := EnsureRange(FLoadedGlobal.RawFMDeviation,
|
||
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
|
||
FWfManualHigh := FLoadedGlobal.WfManualHigh;
|
||
FWfManualLow := FLoadedGlobal.WfManualLow;
|
||
FWfAGCOffset := FLoadedGlobal.WfAGCOffset;
|
||
FWfPalette := EnsureRange(FLoadedGlobal.WfPalette, 0, 2);
|
||
FWfGamma := EnsureRange(FLoadedGlobal.WfGamma, 0.3, 3.0);
|
||
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
||
FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8);
|
||
FSpecMSAA := EnsureRange(FLoadedGlobal.SpecMSAA, 1, 8);
|
||
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||
FShowWideband := FLoadedGlobal.ShowWideband;
|
||
FWidebandFill := FLoadedGlobal.WidebandFill;
|
||
FSpectrumFill := FLoadedGlobal.SpectrumFill;
|
||
FDisplayDuplex := FLoadedGlobal.DisplayDuplex;
|
||
FRxMuteOnTx := FLoadedGlobal.RxMuteOnTx;
|
||
// Настройки дисплея/сетки + DSP display + аудио-устройства
|
||
if FLoadedGlobal.FFTSize > 0 then
|
||
begin
|
||
FSpecRefLevel := FLoadedGlobal.SpecRefLevel;
|
||
FSpecRange := FLoadedGlobal.SpecRange;
|
||
if FLoadedGlobal.SpecGridStep > 0 then FSpecGridStep := FLoadedGlobal.SpecGridStep;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetFFTParams(FLoadedGlobal.FFTSize, FLoadedGlobal.WindowType);
|
||
FDSPEngine.SetSpectrumDisplay(FLoadedGlobal.SpecDetector, FLoadedGlobal.SpecAvgMode,
|
||
FLoadedGlobal.SpecAvgTimeMS);
|
||
FDSPEngine.SetWaterfallDisplay(FLoadedGlobal.WfDetector, FLoadedGlobal.WfAvgMode,
|
||
FLoadedGlobal.WfAvgTimeMS);
|
||
end;
|
||
// Локальная звуковая карта (динамик RX / микрофон TX). В демоне FLocalAudio=
|
||
// False: карту не открываем (иначе headless-хост хватает ALSA-устройство и
|
||
// играет звук). Web-аудио идёт через OnAudioConsume и от FAudioOut не зависит.
|
||
if FLocalAudio then
|
||
begin
|
||
// Аудио устройство вывода
|
||
FAudioOut.Close;
|
||
if FLoadedGlobal.AudioSampleRate > 0 then
|
||
FAudioOut.SampleRate := FLoadedGlobal.AudioSampleRate;
|
||
if FLoadedGlobal.AudioOutDevice <> '' then
|
||
begin
|
||
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(FLoadedGlobal.AudioOutDevice);
|
||
FAudioOutDevName := FLoadedGlobal.AudioOutDevice;
|
||
end
|
||
else
|
||
begin
|
||
FAudioOut.DeviceIndex := -1;
|
||
FAudioOutDevName := '';
|
||
end;
|
||
FAudioOut.Open;
|
||
// Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT —
|
||
// здесь только открываем устройство.
|
||
if FLoadedGlobal.AudioInDevice <> '' then
|
||
begin
|
||
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice);
|
||
FAudioInDevName := FLoadedGlobal.AudioInDevice;
|
||
FAudioIn.Open;
|
||
end
|
||
else
|
||
begin
|
||
FAudioIn.DeviceIndex := -1;
|
||
FAudioInDevName := '';
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
Changed(rfDevice); // UI: полная синхронизация device-виджетов из состояния
|
||
end;
|
||
|
||
function TRadioController.ConnectAndRun(const Dev: THPSDRDevice;
|
||
SpectrumWidth: Integer): Boolean;
|
||
// Headless-оркестрация подключения: Connect+load-by-MAC (ConnectDevice) →
|
||
// перестройка диапазона/XVTR/активного VFO → go-live (StartRunning). Рендер всех
|
||
// device/band/run-виджетов идёт через события (rfDevice/rfBandRestore/rfXvtr/
|
||
// rfActiveVfo/rfRunning). UI-обёртка добавляет лишь статус-строки/AGCTop/resize.
|
||
// Возвращает False, если Connect не удался (тогда bring-up не делается).
|
||
begin
|
||
Result := ConnectDevice(Dev);
|
||
if not Result then Exit;
|
||
|
||
// Перестройка диапазона — команда несёт финальную перестройку по активному VFO.
|
||
RestoreBand(FCurrentBand);
|
||
|
||
// Pluto/VHF: HF-частоты вне диапазона. Если восстановленный VFO ниже минимума
|
||
// бэкенда — ставим дефолт VHF (временно 100 МГц, пока нет VHF band-плана).
|
||
with FNetwork.Caps do
|
||
if (Kind = bkPluto) and (FVfoA < MinFreqHz) then
|
||
begin
|
||
FVfoB := 100000000;
|
||
FCenterFreq := 100000000;
|
||
SetVfoA(100000000); // перестроит дисплей + LO
|
||
end;
|
||
|
||
// Восстанавливаем XVTR-режим, если он был активен при выходе. FCurrentBand уже
|
||
// загружен (HF-диапазон до XVTR) — ActivateXvtr только выставит FVfoA/FCurrentXvtr.
|
||
if (FLoadedGlobal.LastXvtr >= 0) and (FLoadedGlobal.LastXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FLoadedGlobal.LastXvtr].Enabled then
|
||
ActivateXvtr(FLoadedGlobal.LastXvtr);
|
||
|
||
// Активный VFO загружен из настроек; RestoreBand/ActivateXvtr строят только
|
||
// VFO A. Если активен B — перестраиваем приёмник И подсветку на него. Для
|
||
// активного A подсветку освежает UI-обёртка (чистый рендер).
|
||
if FActiveVfo = 1 then
|
||
SetActiveVfo(1);
|
||
|
||
// Go-live Run=1 (General/DDC/DUC + DSP restore); рендер — через rfRunning.
|
||
StartRunning(SpectrumWidth);
|
||
end;
|
||
|
||
procedure TRadioController.StopAndDisconnect;
|
||
// Headless STOP+teardown: graceful Run=0 (StopRunning) + персист настроек
|
||
// устройства/диапазона/глобала + разрыв сети. Персист идёт ДО Disconnect (saves
|
||
// читают живое состояние). Рендер run-кнопки/MOX/sleep — через StopRunning→
|
||
// rfRunning; UI-обёртка добавляет очистку спектра и offline-статус.
|
||
begin
|
||
StopRunning;
|
||
// Сохраняем текущую частоту/настройки чтобы следующий START восстановил их.
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
SaveGlobalSettings;
|
||
FSettings.SaveAlex(FDevMAC, FAlexSettings);
|
||
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
||
// XVTR LastFreq → актуальная видимая частота в момент остановки.
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
end;
|
||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||
FSettings.Save;
|
||
FNetwork.Disconnect;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Discovery — поиск устройств в фоновом потоке. Найденные приходят через
|
||
// FNetwork.OnDeviceFound (=OnDeviceFound) и копятся в FDeviceStore.Discovered;
|
||
// по завершении без устройств — DiscoverFinishedNone. UI слушает rfDeviceList.
|
||
// ---------------------------------------------------------------------------
|
||
type
|
||
TDiscoverThread = class(TThread)
|
||
private
|
||
FCtrl: TRadioController;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(ACtrl: TRadioController);
|
||
end;
|
||
|
||
constructor TDiscoverThread.Create(ACtrl: TRadioController);
|
||
begin
|
||
FCtrl := ACtrl;
|
||
FreeOnTerminate := True;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TDiscoverThread.Execute;
|
||
// Сканируем ОБА бэкенда транзиентными экземплярами (независимо от FNetwork —
|
||
// он может быть уже подключён/другого типа). Найденные сигналятся колбэком
|
||
// OnDeviceFound во время Discover() и копятся в store.
|
||
var
|
||
Found: Integer;
|
||
HP: THPSDRNetwork;
|
||
Pluto: TPlutoBackend;
|
||
PDev: TRadioDevice;
|
||
i: Integer;
|
||
begin
|
||
Found := 0;
|
||
|
||
// openHPSDR (UDP broadcast)
|
||
HP := THPSDRNetwork.Create;
|
||
try
|
||
HP.OnDeviceFound := FCtrl.OnDeviceFound;
|
||
Found := Found + Length(HP.Discover(2000));
|
||
finally
|
||
HP.Free;
|
||
end;
|
||
|
||
// Pluto (libiio scan + прямой probe сетевых URI) — если libiio доступна.
|
||
if IIOLoad then
|
||
begin
|
||
Pluto := TPlutoBackend.Create;
|
||
try
|
||
Pluto.OnDeviceFound := FCtrl.OnDeviceFound;
|
||
// USB scan
|
||
Found := Found + Length(Pluto.Discover(1500));
|
||
// Прямой probe сетевых адресов (mDNS недоступен) — из списка фронтенда.
|
||
for i := 0 to High(FCtrl.FPlutoProbeURIs) do
|
||
if Pluto.ProbeURI(FCtrl.FPlutoProbeURIs[i], PDev) then
|
||
Inc(Found);
|
||
finally
|
||
Pluto.Free;
|
||
end;
|
||
end;
|
||
|
||
if Found = 0 then
|
||
FCtrl.Invoke(FCtrl.DiscoverFinishedNone);
|
||
end;
|
||
|
||
procedure TRadioController.EnsureBackend(Kind: TBackendKind);
|
||
// Пересоздаёт FNetwork нужного класса, перенося колбэки данных. No-op если тип
|
||
// уже верный. Старый бэкенд отключается и освобождается.
|
||
var
|
||
NewB: TRadioBackend;
|
||
begin
|
||
if (FNetwork <> nil) and (FNetwork.Caps.Kind = Kind) then Exit;
|
||
|
||
case Kind of
|
||
bkPluto: NewB := TPlutoBackend.Create;
|
||
else NewB := THPSDRNetwork.Create;
|
||
end;
|
||
|
||
if FNetwork <> nil then
|
||
begin
|
||
if FNetwork.Connected then FNetwork.Disconnect;
|
||
NewB.OnDeviceFound := FNetwork.OnDeviceFound;
|
||
NewB.OnDDCIQ := FNetwork.OnDDCIQ;
|
||
NewB.OnMicPacket := FNetwork.OnMicPacket;
|
||
NewB.OnHPStatus := FNetwork.OnHPStatus;
|
||
NewB.OnWideband := FNetwork.OnWideband;
|
||
FreeAndNil(FNetwork);
|
||
end;
|
||
FNetwork := NewB;
|
||
end;
|
||
|
||
function TRadioController.BackendCaps: TBackendCaps;
|
||
begin
|
||
if FNetwork <> nil then Result := FNetwork.Caps
|
||
else FillChar(Result, SizeOf(Result), 0);
|
||
end;
|
||
|
||
function TRadioController.SampleRatePresets: TBackendRateArray;
|
||
begin
|
||
Result := BackendCaps.RatePresets;
|
||
end;
|
||
|
||
function TRadioController.BandPlan: TBandPlanArray;
|
||
begin
|
||
Result := BuildBandPlan(IsPluto);
|
||
end;
|
||
|
||
function TRadioController.IsPluto: Boolean;
|
||
begin
|
||
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
|
||
end;
|
||
|
||
function TRadioController.InQO100: Boolean;
|
||
// True — активен full-duplex транспондер (QO-100): Pluto + выбран XVTR-слот с
|
||
// флагом FullDuplexTx. Гейт для UI-элементов, осмысленных только в этом режиме
|
||
// (beacon-lock, RX MUTE self-monitor).
|
||
begin
|
||
Result := IsPluto and (FCurrentXvtr >= 0)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx;
|
||
end;
|
||
|
||
function TRadioController.DisplayDuplex: Boolean;
|
||
begin
|
||
Result := FDisplayDuplex;
|
||
end;
|
||
|
||
function TRadioController.BoardDisplayName: string;
|
||
var
|
||
Dev: THPSDRDevice;
|
||
begin
|
||
if FNetwork = nil then Exit('');
|
||
Dev := FNetwork.Device;
|
||
if Dev.Kind = bkPluto then
|
||
begin
|
||
Result := PlutoModelName(Dev.Model);
|
||
// Серийник дописываем, только если это реальный hw_serial, а не URI-fallback
|
||
// ('usb:1.5.5' / 'ip:…'), который PlutoBackend кладёт при отсутствии серийника.
|
||
if (Dev.Serial <> '') and (Pos('usb:', Dev.Serial) = 0)
|
||
and (Pos('ip:', Dev.Serial) = 0) then
|
||
Result := Result + ' SN:' + Dev.Serial;
|
||
end
|
||
else
|
||
Result := BoardTypeName(Dev.BoardType);
|
||
end;
|
||
|
||
procedure TRadioController.ServicePlutoTelemetry;
|
||
const
|
||
POLL_MS = 500; // опрос ~2 Гц: чтение attr — USB round-trip, чаще ни к чему
|
||
TEMP_MIN_C = 0.0; // работающий AD936x всегда теплее окружающей (само-нагрев)
|
||
TEMP_MAX_C = 120.0; // абсолютный потолок (датчик исправен)
|
||
TEMP_STEP_C = 12.0; // макс. физичный шаг между опросами (тепловая инерция чипа)
|
||
TEMP_FORCE = 3; // принять «выброс», если держится N раз подряд (не залипаем)
|
||
var
|
||
t, s, r: Double;
|
||
now_: QWord;
|
||
begin
|
||
if (FNetwork = nil) or (not FRunning) or (FNetwork.Caps.Kind <> bkPluto) then Exit;
|
||
now_ := GetTickCount64;
|
||
if (FPlutoTeleTick <> 0) and (now_ - FPlutoTeleTick < POLL_MS) then Exit;
|
||
FPlutoTeleTick := now_;
|
||
if not FNetwork.ReadTelemetry(t, s, r) then Exit;
|
||
|
||
// Температура: датчик AD9361 иногда отдаёт мусор (мид-конверсия / контеншн на
|
||
// USB-контексте с RX-потоком) — наблюдалось редкое -15 °C. Фильтруем: жёсткое
|
||
// окно правдоподобия + slew-limit по тепловой инерции; устойчивый «выброс»
|
||
// (реальный скачок) всё же принимаем через TEMP_FORCE опросов, чтобы не залипнуть.
|
||
if t > TELEMETRY_NONE then
|
||
begin
|
||
if (t >= TEMP_MIN_C) and (t <= TEMP_MAX_C) then
|
||
begin
|
||
if (FLastPlutoTempC <= TELEMETRY_NONE) or (Abs(t - FLastPlutoTempC) <= TEMP_STEP_C) then
|
||
begin
|
||
FLastPlutoTempC := t; FPlutoTempBad := 0;
|
||
end
|
||
else
|
||
begin
|
||
Inc(FPlutoTempBad);
|
||
if FPlutoTempBad >= TEMP_FORCE then
|
||
begin FLastPlutoTempC := t; FPlutoTempBad := 0; end;
|
||
end;
|
||
end;
|
||
// вне окна правдоподобия — молча отбрасываем, держим прошлое валидное значение
|
||
end;
|
||
|
||
// Температура Zynq SoC (xadc): стабильный локальный датчик (не подвержен мусору
|
||
// мид-конверсии AD9361) — достаточно окна правдоподобия без slew-limit.
|
||
if (s > TELEMETRY_NONE) and (s >= TEMP_MIN_C) and (s <= TEMP_MAX_C) then
|
||
FLastPlutoSocTempC := s;
|
||
|
||
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
|
||
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
|
||
|
||
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
|
||
end;
|
||
|
||
procedure TRadioController.SetPlutoProbeURIs(const URIs: array of string);
|
||
var i: Integer;
|
||
begin
|
||
SetLength(FPlutoProbeURIs, Length(URIs));
|
||
for i := 0 to High(URIs) do FPlutoProbeURIs[i] := URIs[i];
|
||
end;
|
||
|
||
procedure TRadioController.SeedPlutoProbeFromSaved;
|
||
// Заполняет probe-список URI всех сохранённых Pluto (mDNS нет → сетевой Pluto
|
||
// находится только прямым probe его URI). Демон зовёт это перед Discover; GUI
|
||
// добавляет ещё и ручной IP из диалога. Дедупликация по URI.
|
||
var
|
||
URIs: array of string;
|
||
i, k: Integer;
|
||
U: string;
|
||
begin
|
||
SetLength(URIs, 0);
|
||
for i := 0 to FDeviceStore.SavedCount - 1 do
|
||
if FDeviceStore.Saved(i).Kind = bkPluto then
|
||
begin
|
||
U := FDeviceStore.Saved(i).URI;
|
||
if U = '' then Continue;
|
||
for k := 0 to High(URIs) do
|
||
if SameText(URIs[k], U) then begin U := ''; Break; end;
|
||
if U = '' then Continue;
|
||
SetLength(URIs, Length(URIs) + 1);
|
||
URIs[High(URIs)] := U;
|
||
end;
|
||
SetPlutoProbeURIs(URIs);
|
||
end;
|
||
|
||
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
|
||
// Сетевой/scan-поток: строим строку для списка → pending → маршалинг в
|
||
// ApplyDeviceFound. Строка зависит от типа бэкенда (HPSDR vs Pluto).
|
||
begin
|
||
FPendingFoundDev := Dev;
|
||
if Dev.Kind = bkPluto then
|
||
FPendingFoundEntry := Format('PlutoSDR %s', [Dev.URI])
|
||
else
|
||
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
|
||
[Dev.IPAddress, BoardTypeName(Dev.BoardType),
|
||
Dev.FirmwareVersion, Dev.NumDDCs]);
|
||
Invoke(ApplyDeviceFound);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyDeviceFound;
|
||
// Маршалится из OnDeviceFound (Synchronize). Дописываем в store и уведомляем UI.
|
||
begin
|
||
FDiscoverNoneFound := False;
|
||
FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry,
|
||
FPendingFoundDev.BoardType, FPendingFoundDev.MAC,
|
||
FPendingFoundDev.Kind, FPendingFoundDev.URI,
|
||
FPendingFoundDev.Serial);
|
||
Changed(rfDeviceList);
|
||
end;
|
||
|
||
procedure TRadioController.DiscoverFinishedNone;
|
||
// Маршалится из discover-потока, когда устройств не найдено.
|
||
begin
|
||
FDiscoverNoneFound := True;
|
||
Changed(rfDeviceList);
|
||
end;
|
||
|
||
procedure TRadioController.Discover;
|
||
// Старт поиска: чистим прошлый список, сбрасываем direct-IP, запускаем поток.
|
||
begin
|
||
FDeviceStore.ClearDiscovered;
|
||
FDiscoverNoneFound := False;
|
||
FNetwork.DirectIP := '';
|
||
Changed(rfDeviceList);
|
||
TDiscoverThread.Create(Self);
|
||
end;
|
||
|
||
end.
|