Files
ewsdr/RadioController.pas
T
Uladzimir KarpenkaandClaude Opus 4.8 7b40eae174 Phase 5: add OnAfterTune hook so all tune paths run channel orchestration
Channel orchestration (deactivate-on-tune-away, auto-CTCSS/pre-channel-drive
restore) was glued to MainForm.ApplyVfoA, so only desktop/web-freq tuning ran it.
Add TRadioController.OnAfterTune, fired at the very end of SetVfoA (after the
rfVfoA render returns — outside its guard, no re-entrancy), wired to
ChannelController.OnVfoTuned.

Now every SetVfoA caller (desktop, web, CAT, encoder TuneActiveBy, SetActiveVfo,
channel apply) is uniformly channel-aware and headless-compatible. ApplyVfoA
becomes a thin delegate. Minor improvement: encoder tune-away now deactivates an
active channel like desktop; SetActiveVfo/channel-apply are no-ops (freq matches).

Foundation for moving RestoreBand's retune into the controller (Band command).

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

2349 lines
99 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit RadioController;
{
EWSDR — Radio Controller (headless ядро)
Назначение
----------
TRadioController — «мозг» трансивера БЕЗ привязки к UI/LCL-формам. Владеет
состоянием радио и движками (сеть HPSDR, WDSP, аудио, настройки, каналы) и
предоставляет единый интерфейс управления для ЛЮБОГО фронтенда: GUI, Web, CAT,
аппаратная панель Arduino.
• Команды (SetMode, SetVfoA, TuneActiveBy, StartStop, …) — единая точка входа.
Абсолютные (Set*) удобны UI/Web; относительные (*By/Up/Down) — энкодерам.
• GetSnapshot → TRadioSnapshot — весь стейт одной записью (синхронизация
нового фронтенда при подключении: LED/дисплеи панели, веб-клиент).
• OnStateChanged(Field) — гранулярное уведомление об изменении поля; все
фронтенды остаются синхронны, кто бы ни инициировал изменение.
Маршалинг потоков
-----------------
Команды из сетевых/serial/web-потоков идут через OnInvoke (GUI = обёртка над
TThread.Synchronize; демон = своя очередь/прямой вызов), чтобы вызовы движка
шли из одного потока без зависимости от GUI-цикла LCL.
СТАТУС МИГРАЦИИ (Фаза 1)
------------------------
Состояние и движки вынесены сюда как ПУБЛИЧНЫЕ ПОЛЯ с теми же именами, что были
в TMainForm (FVfoA, FNetwork, FDSPEngine, …), чтобы проводка MainForm свелась к
механической замене FXxx -> FController.FXxx без изменения поведения. Логика
операций (ApplyModeFilter, BtnStartStopClick, ApplyVfoA, …) пока остаётся в
MainForm; команды контроллера обновляют поле + шлют событие и будут наполнены
логикой на следующих фазах (помечено TODO-wiring). Поля станут приватными,
когда MainForm перестанет писать в них напрямую.
ВЛАДЕНИЕ ДВИЖКАМИ (Фаза 1): объекты создаёт и освобождает MainForm (в
FormCreate/FormDestroy), сохраняя в поля контроллера. Перенос создания в
Create/Destroy контроллера — отдельная фаза (нужен для режима демона).
FPC 3.2+ / режим Delphi. Без inline var и анонимных процедур.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math,
HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput, AudioInput,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
type
// Поле, которое изменилось — для гранулярных уведомлений наружу.
TRadioField = (
rfVfoA, rfVfoB, rfActiveVfo,
rfMode, rfFilter, rfFilterBW,
rfAGCMode, rfAGCTop,
rfVolume, rfMute, rfDrive, rfAtten,
rfBand, rfXvtr,
rfCenterFreq, rfSpan, rfSampleRate,
rfCTun, rfDuplex,
rfRunning, rfConnected, rfTransmitting, rfTuning,
rfNR, rfNB, rfSNB, rfANF,
rfWfAGC, rfWfNF, rfDither, rfRandom,
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
rfFMStep, rfFMStepIdx, rfFMRpt,
rfChannel,
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
rfADCOverload, // ADC overload (рендер индикатора)
rfDevice, // подключённое устройство сменилось
rfDeviceList // список discovered устройств обновился
);
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
// Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize).
TUIInvokeEvent = procedure(M: TThreadMethod) of object;
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
TRadioSnapshot = record
VfoA, VfoB, CenterFreq, SpanHz: Double;
SampleRate: Integer;
ActiveVfo: Integer;
Mode, Filter, FilterBW: Integer;
AGCMode, AGCTop: Integer;
Volume, Drive, Atten: Integer;
Band, Xvtr: Integer;
Muted, CTun, Duplex: Boolean;
Running, Connected, Transmitting, Tuning: Boolean;
NRMode, NBMode: Integer;
SNB, ANF: Boolean;
WfAGC, WfNF: Boolean;
FMStepOn: Boolean;
FMStepIdx: Integer;
FMCTCSSOn: Boolean;
FMCTCSSToneIdx: Integer;
FMSQOn: Boolean;
FMSQLevel: Integer;
FMRptDir: Integer;
SMeter, FwdW, SWR, SupplyV, SupplyA: Double;
PLLLock: Boolean;
end;
{ TRadioController }
TRadioController = class
private
FOnInvoke: TUIInvokeEvent;
FOnStateChanged: TRadioStateEvent;
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
procedure Changed(Field: TRadioField);
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
function ActiveVfoHz: Double; // частота активного VFO (A или B)
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
public
// ============================================================
// ПУБЛИЧНЫЕ ПОЛЯ (миграционные — имена совпадают с TMainForm).
// Внешний чистый доступ для адаптеров — через команды / GetSnapshot /
// OnStateChanged. Прямая запись извне — временная мера на время Фазы 1.
// ============================================================
// ---- Движки (владение: см. шапку юнита) ----
FNetwork: THPSDRNetwork;
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FAudioIn: TAudioInput;
FSettings: TSettingsManager;
FChannelStore: TChannelStore;
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
FWDSPReady: Boolean;
// ---- Частоты / режим / фильтр / AGC ----
FVfoA, FVfoB: Double;
FActiveVfo: Integer;
FMode: Integer;
FFilter: Integer;
FFilterBW: Integer;
FAGCMode: Integer;
FAGCTop: Integer;
FCTun: Boolean;
FCenterFreq: Double;
FSpanHz: Double;
FSampleRate: Integer;
FCurrentBand: Integer;
FCurrentXvtr: Integer;
// ---- Аудио / TX ----
FVolume: Integer;
FMuted: Boolean;
FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины)
FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte)
FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада)
FAtten: Integer;
FPAMaxPower: Double;
FRunning: Boolean;
FTransmitting: Boolean;
FTuning: Boolean;
FSplitTxB: Boolean;
FDisplayDuplex: Boolean;
FHWPTTActive: Boolean;
FHWPTTStartedTX: Boolean;
FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента)
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
FSendAudioToRadio: Boolean;
FAudioOutDevName: string;
FAudioInDevName: string;
FDevConnected: Boolean;
FDiscoverNoneFound: Boolean; // discovery завершился без устройств (для рендера)
// ---- DSP-фильтры шума / водопад ----
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FDitherEnabled: Boolean;
FRandomEnabled: Boolean;
// ---- Display state (persist в TGlobalSettings; рендерится FSpecView/Wideband) ----
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FShowWideband: Boolean;
FWidebandFill: Boolean;
FSpectrumFill: Boolean;
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
FSpecRefLevel: Double;
FSpecRange: Double;
FSpecGridStep: Double;
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
FTXSpecRange: Double;
FTXSpecGridStep: Double;
// ---- FM ----
FFMDeviation: Double;
FFMCTCSSOn: Boolean;
FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean;
FFMSQLevel: Integer;
FFMStepOn: Boolean;
FFMStepIdx: Integer;
FFMRptDir: Integer;
FFMRptOffsetHz: Double;
FFMRptAutoActive: Boolean;
FFMCTCSSAutoActive: Boolean;
// ---- Конфиги / устройство (per-MAC) ----
FTXSettings: TTXSettings;
FAlexSettings: TAlexSettings;
FXvtrSettings: TXvtrSettings;
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double;
FDevMAC: array[0..5] of Byte;
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
FLoadedGlobal: TGlobalSettings;
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
FLastPLLLock: Boolean;
// ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ----
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
FRXStartTime: QWord; // GetTickCount64 момента START
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index
FDDCLastSeq: array[0..6] of LongWord;
FDDCSeqValid: array[0..6] of Boolean;
FSeqErrorCount: LongWord;
FLastSeqErrorDDC: Integer;
FLastSeqErrorDelta: Int64;
FSeqOkStreak: Integer;
FADCOverloadActive: Boolean; // ADC overload (рендерится по rfADCOverload)
// ---- TX DUC-очередь (накопление IQ от WDSP в пакеты по 240, OnTXIQ) ----
FDUCPendingI: array[0..239] of Integer;
FDUCPendingQ: array[0..239] of Integer;
FDUCPendingCount: Integer;
// Pending-значения декода HP-статуса (сетевой поток → ApplyHPStatus через
// Invoke; Synchronize блокирующий и последовательный, гонок нет).
FHPpFwdW, FHPpSWR, FHPpSupplyV, FHPpSupplyA: Double;
FHPpPLL, FHPpHWPTT: Boolean;
FHPpOverload: Byte;
// Pending discovered-устройство (сетевой поток discovery → ApplyDeviceFound
// через Invoke; Synchronize блокирующий — устройства приходят по одному).
FPendingFoundDev: THPSDRDevice;
FPendingFoundEntry: string;
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 OnMicPacket(const Data: TMicDataPacket);
function PullSoundCardMic(MaxN: Integer): Integer;
procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
procedure OnHPStatus(const Status: THighPriorityStatus);
procedure ApplyHPStatus; // маршалится из OnHPStatus (сглаживание/overload/HWPTT)
procedure OnTXIQ(const Buf: array of Double; Count: Integer);
procedure OnDeviceFound(const Dev: THPSDRDevice); // сетевой поток discovery
procedure ApplyDeviceFound; // маршалится из OnDeviceFound (store + rfDeviceList)
procedure DiscoverFinishedNone; // маршалится из discover-потока: устройств нет
// Маршалинг команды в поток контроллера (GUI: TThread.Synchronize).
procedure Invoke(M: TThreadMethod);
// Снимок всего состояния (для синхронизации нового фронтенда).
function GetSnapshot: TRadioSnapshot;
// Частотные хелперы — чистые вычисления над состоянием, общие для всех
// фронтендов и сетевого слоя (Vfo/Mode/Band/CTun/TX/FM-Rpt).
// XvtrTranslate — переводит видимую частоту в частоту IF-тракта с учётом
// активного трансвертора (LOOffset/LOError).
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
function XvtrTranslate(VisibleHz: Double): Double;
function ActiveTXFreqHz: Double;
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
procedure ApplyModeFilter;
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
function CalcDriveByte: Byte;
// Источник микрофона по умолчанию для MOX-кнопки/Web-TX: звуковая карта
// если настроена, иначе HW mic-стрим трансивера. HW PTT → txmsRadio в SetMOX.
function DefaultMicSource: TTXMicSource;
// ---- TX/DSP-цепь (чистые над состоянием: читают F*/FTXSettings, пишут в движок) ----
function BuildMicLineSelectByte: Byte; // DUC Specific byte 50 (mic/line биты)
procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP
procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
// MakeBandSettings — снимок текущего состояния в запись диапазона.
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
// RestoreBand — загружает состояние диапазона, применяет к DSP, шлёт события
// рендера. Финальную перестройку VFO A делает вызывающий (UI: ApplyVfoA).
function MakeBandSettings: TBandSettings;
procedure SaveCurrentBand;
procedure RestoreBand(BandIdx: Integer);
// Глобальные настройки устройства (persist по MAC). BuildGlobalSettings
// собирает радио-поля из своего состояния, а НЕ-радио поля (тема/FPS/digits/
// CAT) переносит сквозь из FLoadedGlobal — контроллер их не трактует, только
// round-trip в файл. SaveGlobalSettings пишет блоб (гейт по FDevConnected).
function BuildGlobalSettings: TGlobalSettings;
procedure SaveGlobalSettings;
// Применяет DSP-настройки диапазона (mode/filter/AGC/CTUN/FM) + шлёт события
// рендера. НЕ трогает частоты VFO/центр — это переиспользуется как RestoreBand
// (с загрузкой VFO), так и SetActiveVfo (bandstack: смена бэнда без сброса VFO).
procedure ApplyBandDSP(const B: TBandSettings);
// Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы.
function FilterBWFor(M, Idx: Integer): Integer;
// ---- XVTR (трансвертер) ----
// ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть.
// ActivateXvtr — вход в слот: state+DSP (через ApplyBandDSP) + VFO/центр + сеть;
// рендер через rfXvtr/rfBand(XVTR-aware)/rfActiveVfo. UI: wideband+web.
// DeactivateXvtr — сохраняет слот (SaveCurrentBand) и выходит; HF-возврат
// (RestoreBand + финальная перестройка VFO) делает UI-обёртка.
procedure ApplyXvtrToNetwork;
procedure ActivateXvtr(Idx: Integer);
procedure DeactivateXvtr;
// Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX).
// Вызывается командами смены частоты/центра и UI после правки drive.
procedure PushNetworkState;
// ====================================================================
// КОМАНДЫ — единый API для GUI / Web / CAT / Arduino.
// Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring.
// ====================================================================
// Частоты / VFO
procedure SetVfoA(Hz: Double);
procedure SetVfoB(Hz: Double);
procedure SetActiveVfo(Idx: Integer);
procedure TuneActiveBy(DeltaHz: Double); // энкодер VFO
procedure SetCenter(Hz: Double);
procedure VfoSwap;
procedure VfoACopyB;
procedure VfoBCopyA;
// Режим / фильтр / AGC
procedure SetMode(M: Integer);
procedure SetFilterIdx(Idx: Integer);
procedure SetFilterBW(BWHz: Integer);
procedure SetAGCMode(M: Integer);
procedure SetAGCTop(DB: Integer);
procedure AGCTopBy(Delta: Integer);
// Громкость / mute / drive / аттенюатор
procedure SetVolume(V: Integer);
procedure VolumeBy(Delta: Integer);
procedure SetMute(On_: Boolean);
procedure ToggleMute;
procedure SetDrive(V: Integer);
procedure DriveBy(Delta: Integer);
procedure SetAtten(Idx: Integer);
// Диапазоны / трансвертеры / спан
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);
// Отображение / приём
procedure SetCTun(On_: Boolean);
procedure SetDuplex(On_: Boolean);
procedure SetWfAGC(On_: Boolean);
procedure SetWfNF(On_: Boolean);
// DSP-фильтры шума
procedure SetNR(Mode: Integer);
procedure SetNB(Mode: Integer);
procedure SetSNB(On_: Boolean);
procedure SetANF(On_: Boolean);
// Передача
procedure SetMOX(On_: Boolean);
procedure SetTune(On_: Boolean); // TUN
// FM
procedure SetFMStep(Idx: Integer);
procedure SetFMCTCSS(On_: Boolean);
procedure SetFMCTCSSTone(Idx: Integer);
procedure SetFMSquelch(On_: Boolean);
procedure SetFMSquelchLevel(V: Integer);
procedure SetFMRpt(Dir: Integer); // явное направление; снимает авто-флаг
procedure SetFMRptOffset(Hz: Double); // сдвиг репитера; применяет, если RPT вкл
procedure UpdateRptAutoState; // авто-MINUS для 2 м под FVfoA
// Каналы: применение/деактивация каналов вынесено в TChannelController
// (отдельный юнит-адаптер поверх контроллера), оркеструет команды ниже.
// Жизненный цикл движка / устройства
procedure StartStop;
procedure SetRun(On_: Boolean);
procedure StartRunning(SpectrumWidth: Integer); // Run=1 bring-up: General/DDC/DUC + DSP restore
procedure StopRunning; // Run=0: graceful-стоп RX-потока + сброс TX/телеметрии
function ConnectDevice(const Dev: THPSDRDevice): Boolean; // Connect + load-by-MAC; рендер через rfDevice
procedure Discover;
// ---- События наружу ----
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
end;
implementation
constructor TRadioController.Create;
begin
inherited Create;
FSettings := TSettingsManager.Create; // владелец настроек (GUI и демон)
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90;
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FCurrentBand := 5; FCurrentXvtr := -1;
FPAMaxPower := 100.0;
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
FFMStepOn := True;
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
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);
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
// формы / при подключении устройства).
FAudioOut := TAudioOutput.Create(48000);
FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize;
FAudioIn := TAudioInput.Create(48000);
end;
procedure TRadioController.FreeEngines;
begin
if Assigned(FNetwork) then
begin
if FNetwork.Connected then FNetwork.Disconnect;
FreeAndNil(FNetwork);
end;
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end;
end;
destructor TRadioController.Destroy;
begin
FreeEngines;
FreeAndNil(FDeviceStore);
FreeAndNil(FSettings);
inherited Destroy;
end;
procedure TRadioController.Changed(Field: TRadioField);
begin
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
end;
procedure TRadioController.PushNetworkState;
// Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние
// MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг.
begin
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
procedure TRadioController.OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
// Сетевой поток: трекинг seq + подача IQ активного DDC в WDSP. UI не трогаем —
// статус рисует таймер по полям ниже. Backend-агностичен (Pluto синтезирует
// тот же TDDCIQPacket).
var
SamplesPerFrame: Integer;
Seq, ExpectedSeq: LongWord;
Delta: Int64;
begin
Inc(FRXPacketCount);
FRXLastPktTime := GetTickCount64;
Seq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
if (DDCIndex >= Low(FDDCLastSeq)) and (DDCIndex <= High(FDDCLastSeq)) then
begin
if Seq <> 0 then
begin
ExpectedSeq := FDDCLastSeq[DDCIndex] + 1;
if FDDCSeqValid[DDCIndex] and (Seq <> ExpectedSeq) then
begin
Inc(FSeqErrorCount);
FLastSeqErrorDDC := DDCIndex;
Delta := Int64(Seq) - Int64(ExpectedSeq);
FLastSeqErrorDelta := Delta;
FSeqOkStreak := 0;
end
else if FDDCSeqValid[DDCIndex] then
begin
if FSeqErrorCount > 0 then
begin
Inc(FSeqOkStreak);
if FSeqOkStreak >= 20 then
begin
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
end;
end;
end;
end;
FDDCLastSeq[DDCIndex] := Seq;
FDDCSeqValid[DDCIndex] := True;
end;
// Подаём IQ в DSP — только с активного DDC
if (DDCIndex = FActiveDDC) and FWDSPReady then
begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
end;
// Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path)
FLastDDCSeq := Seq;
FLastDDCIndex := DDCIndex;
end;
procedure TRadioController.OnTXIQ(const Buf: array of Double; Count: Integer);
// WDSP TXA отдаёт IQ-пары @ TX rate. Копим хвост между вызовами и шлём только
// полные DUC-пакеты по 240 (зануление хвоста давало 47 Гц гул). Клампим в
// 24-bit, чтобы пики компрессора/EQ (чуть >1.0) не оборачивались через wrap.
// Backend-агностично: Pluto-backend в SendDUCIQ переведёт 24-bit→int16.
function ScaleIQ24(V: Double): Integer;
var S: Double;
begin
S := V * 8388607.0;
if S > 8388607.0 then Result := 8388607
else if S < -8388608.0 then Result := -8388608
else Result := Round(S);
end;
var
i: Integer;
begin
if not (Assigned(FNetwork) and FNetwork.Connected) then Exit;
for i := 0 to Count - 1 do
begin
FDUCPendingI[FDUCPendingCount] := ScaleIQ24(Buf[i * 2]);
FDUCPendingQ[FDUCPendingCount] := ScaleIQ24(Buf[i * 2 + 1]);
Inc(FDUCPendingCount);
if FDUCPendingCount >= 240 then
begin
FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ);
FDUCPendingCount := 0;
end;
end;
end;
procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus);
// Сетевой поток: декод HP-статуса (board-specific supply/power/SWR) → pending →
// маршалинг в ApplyHPStatus (сглаживание + overload + HWPTT уже на потоке UI/ядра).
const
SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем
SWR_CAP = 9.9;
var
FwdPwr, RevPwr: Word;
SupplyV, SupplyA, FwdW, SWRV, Rho: Double;
IsTx: Boolean;
begin
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
SupplyV := -1.0;
SupplyA := -1.0;
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis).
// Прочие платы — в SupplyVolts-байтах, без тока.
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
begin
if FNetwork.Device.BoardType = 5 then
SupplyV := ADCToSupplyVolts(
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
FNetwork.Device.BoardType)
else
SupplyV := ADCToSupplyVolts(
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
FNetwork.Device.BoardType);
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
SupplyA := ADCToSupplyCurrent(
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
FNetwork.Device.BoardType);
end;
FwdW := ADCToWatts100(FwdPwr);
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
// SWR = (1+ρ)/(1-ρ), ρ = adc_r/adc_f; только при значимой fwd-мощности.
if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then
begin
Rho := RevPwr / FwdPwr;
if Rho >= 0.99 then
SWRV := SWR_CAP
else
begin
SWRV := (1.0 + Rho) / (1.0 - Rho);
if SWRV > SWR_CAP then SWRV := SWR_CAP;
if SWRV < 1.0 then SWRV := 1.0;
end;
end else
SWRV := 1.0;
if not IsTx then FwdW := 0;
FHPpFwdW := FwdW;
FHPpSWR := SWRV;
FHPpSupplyV := SupplyV;
FHPpSupplyA := SupplyA;
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload;
Invoke(ApplyHPStatus);
end;
procedure TRadioController.ApplyHPStatus;
// Маршалится из OnHPStatus. PEP-style баллистика (мгновенная атака, медленный
// спад) для FwdW/SWR; EMA для supply. + overload-событие + HWPTT-фронт → SetMOX.
const
DECAY_FWD = 0.02;
DECAY_SWR = 0.015;
ALPHA_SUPPLY = 0.05;
begin
if FHPpFwdW <= 0 then
FLastFwdW := 0
else if FHPpFwdW > FLastFwdW then
FLastFwdW := FHPpFwdW // attack: instant
else
FLastFwdW := DECAY_FWD * FHPpFwdW + (1.0 - DECAY_FWD) * FLastFwdW;
if FHPpSWR <= 1.0 then
FLastSWR := 1.0
else if FHPpSWR > FLastSWR then
FLastSWR := FHPpSWR // attack: instant
else
FLastSWR := DECAY_SWR * FHPpSWR + (1.0 - DECAY_SWR) * FLastSWR;
if FHPpSupplyV >= 0 then
begin
if FLastSupplyV < 0 then
FLastSupplyV := FHPpSupplyV // первое значение — без сглаживания
else
FLastSupplyV := ALPHA_SUPPLY * FHPpSupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV;
end else
FLastSupplyV := -1.0;
if FHPpSupplyA >= 0 then
begin
if FLastSupplyA < 0 then
FLastSupplyA := FHPpSupplyA
else
FLastSupplyA := ALPHA_SUPPLY * FHPpSupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA;
end else
FLastSupplyA := -1.0;
FLastPLLLock := FHPpPLL;
// ADC overload — индикатор рисует UI по rfADCOverload (только на изменении).
if FADCOverloadActive <> (FHPpOverload <> 0) then
begin
FADCOverloadActive := FHPpOverload <> 0;
Changed(rfADCOverload);
end;
// Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить
// короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если
// его включил именно HWPTT.
if FHPpHWPTT <> FHWPTTActive then
begin
FHWPTTActive := FHPpHWPTT;
if not FTuning then
begin
if FHPpHWPTT then
begin
if not FTransmitting then
begin
SetMOX(True);
FHWPTTStartedTX := True;
end;
end
else
begin
if FHWPTTStartedTX then
begin
FHWPTTStartedTX := False;
SetMOX(False);
end;
end;
end;
end;
end;
procedure TRadioController.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.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
Result := VisibleHz
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
end;
function TRadioController.ActiveTXFreqHz: Double;
begin
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
// за активным RX (TX-A горит, но передача шла на B при активном B).
if FSplitTxB then Result := FVfoB
else Result := FVfoA;
if FMode = MODE_FM then
case FFMRptDir of
RPT_MINUS: Result := Result - FFMRptOffsetHz;
RPT_PLUS: Result := Result + FFMRptOffsetHz;
end;
end;
procedure TRadioController.ApplyModeFilter;
var
Lo, Hi, Half: Integer;
begin
Half := FFilterBW div 2;
case FMode of
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
1: begin Lo := 100; Hi := FFilterBW; end; // USB
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
end;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(Lo, Hi);
if FMode = MODE_FM then
begin
FDSPEngine.SetRXFMDeviation(FFMDeviation);
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
end
else
FDSPEngine.SetFMSquelch(False, 0);
end;
end;
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
procedure TRadioController.ApplyModeDefaults;
begin
case FMode of
MODE_FM:
begin
if FFilter > 1 then FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
0, 1: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
2: begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
3, 4: begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end;
end;
end;
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
begin
case M of
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, 1)];
0, 1: Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
2: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
3, 4: Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
end;
end;
// =====================================================================
// Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
// =====================================================================
function TRadioController.CalcDriveByte: Byte;
// Thetis-совместимая формула:
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
var
Cal: Double;
Pos: Integer;
XvtrActive: Boolean;
pwr_W: Double;
target_dBm: Double;
target_volts: Double;
audio_vol: Double;
begin
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
if XvtrActive then
Cal := FVHFBandCal[FCurrentXvtr]
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 38.8;
if FTuning then
begin
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100)
else
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
end
else
begin
if XvtrActive then
Pos := Round(FDrivePercent * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
else
Pos := FDrivePercent;
end;
if Pos <= 0 then begin Result := 0; Exit; end;
pwr_W := Pos / 100.0 * FPAMaxPower;
target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal;
target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05);
audio_vol := Min(target_volts / 0.8, 1.0);
Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
end;
procedure TRadioController.SanitizeFilterForMode;
// Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и
// синхронизируем FFilterBW/FFMDeviation. Чистая (state) часть UpdateFilterButtons.
begin
if FMode = MODE_FM then
begin
if FFilter > 1 then FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
end;
function TRadioController.MakeBandSettings: TBandSettings;
begin
Result.VfoA := FVfoA;
Result.VfoB := FVfoB;
Result.Mode := FMode;
Result.FilterIdx := FFilter;
Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode;
Result.AGCTop := FAGCTop;
Result.CTun := FCTun;
Result.SpanHz := FSpanHz;
Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel;
Result.CTCSSOn := FFMCTCSSOn;
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMStepOn := FFMStepOn;
Result.FMStepIdx := FFMStepIdx;
Result.FMRptDir := FFMRptDir;
Result.FMRptOffsetHz := FFMRptOffsetHz;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
procedure TRadioController.SaveCurrentBand;
// Сохраняет текущее состояние диапазона. В XVTR-режиме пишем в XVTR-слот
// (HF band-cache не трогаем — там сохранён HF-диапазон до активации XVTR).
var
i: Integer;
B: TBandSettings;
begin
if not FDevConnected then Exit;
if FCurrentXvtr >= 0 then
begin
if (FCurrentXvtr < CFG_XVTR_COUNT) then
begin
i := FCurrentXvtr;
FXvtrSettings.Entries[i].LastFreq := FVfoA;
FXvtrSettings.Entries[i].LastFreqB := FVfoB;
FXvtrSettings.Entries[i].LastMode := FMode;
FXvtrSettings.Entries[i].LastFilterIdx := FFilter;
FXvtrSettings.Entries[i].LastFMSQOn := FFMSQOn;
FXvtrSettings.Entries[i].LastFMSQLevel := FFMSQLevel;
FXvtrSettings.Entries[i].LastCTCSSOn := FFMCTCSSOn;
FXvtrSettings.Entries[i].LastCTCSSToneIdx := FFMCTCSSToneIdx;
FXvtrSettings.Entries[i].LastFMStepOn := FFMStepOn;
FXvtrSettings.Entries[i].LastFMStepIdx := FFMStepIdx;
FXvtrSettings.Entries[i].LastFMRptDir := FFMRptDir;
FXvtrSettings.Entries[i].LastFMRptOffsetHz := FFMRptOffsetHz;
FXvtrSettings.Entries[i].LastCTun := FCTun;
FXvtrSettings.Entries[i].LastAGCMode := FAGCMode;
FXvtrSettings.Entries[i].LastAGCTop := FAGCTop;
FXvtrSettings.Entries[i].LastCTCSSAutoActive := FFMCTCSSAutoActive;
FXvtrSettings.Entries[i].LastFMRptAutoActive := FFMRptAutoActive;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
end;
// Не сохраняем, если VFO ушёл в VHF/UHF (например, после канала на 2м без
// XVTR) — иначе HF-кэш диапазона перезапишется VHF-частотой.
if FVfoA > 61000000 then Exit;
B := MakeBandSettings;
// Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш
// ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая
// частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда.
if FreqToBandIdx(FVfoA) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA;
if FreqToBandIdx(FVfoB) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB;
FBandCache[FCurrentBand] := B;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end;
function TRadioController.BuildGlobalSettings: TGlobalSettings;
// Радио-поля — из живого состояния; тема/FPS/digits/CAT — сквозь из FLoadedGlobal
// (контроллер их не трактует). Сидируем загруженным блобом, поверх — радио.
var i: Integer;
begin
Result := FLoadedGlobal; // несёт не-радио поля (тема/FPS/digits/CAT) round-trip
Result.Volume := FVolume;
Result.DriveLevel := FDrivePercent;
Result.PAMaxPower := FPAMaxPower;
for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := FNRMode;
Result.NBMode := FNBMode;
Result.SNBEnabled := FSNB;
Result.ANFEnabled := FANF;
Result.AGCSlope := 0;
Result.AGCHangThreshold := 100;
Result.WfAGCEnabled := FWfAGCEnabled;
Result.WfNFEnabled := FWfNFEnabled;
Result.DitherEnabled := FDitherEnabled;
Result.RandomEnabled := FRandomEnabled;
Result.LastBand := FCurrentBand;
Result.LastXvtr := FCurrentXvtr;
Result.SampleRate := FSampleRate;
if FWDSPReady then
begin
Result.FFTSize := FDSPEngine.FFTSize;
Result.WindowType := FDSPEngine.WindowType;
Result.SpecDetector := FDSPEngine.SpecDetector;
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
Result.WfDetector := FDSPEngine.WfDetector;
Result.WfAvgMode := FDSPEngine.WfAvgMode;
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
end
else
begin
Result.FFTSize := 131072;
Result.WindowType := 2;
Result.SpecDetector := 0;
Result.SpecAvgMode := 3;
Result.SpecAvgTimeMS := 30.0;
Result.WfDetector := 0;
Result.WfAvgMode := 3;
Result.WfAvgTimeMS := 120.0;
end;
Result.WfManualHigh := FWfManualHigh;
Result.WfManualLow := FWfManualLow;
Result.WfAGCOffset := FWfAGCOffset;
Result.SpecRefLevel := FSpecRefLevel;
Result.SpecRange := FSpecRange;
Result.SpecGridStep := FSpecGridStep;
Result.AudioSampleRate := FAudioOut.SampleRate;
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.ShowWideband := FShowWideband;
Result.WidebandFill := FWidebandFill;
Result.SpectrumFill := FSpectrumFill;
Result.DisplayDuplex := FDisplayDuplex;
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
Result.SendAudioToRadio := FSendAudioToRadio;
end;
procedure TRadioController.SaveGlobalSettings;
begin
if FDevConnected then FSettings.SaveGlobal(FDevMAC, BuildGlobalSettings);
end;
procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
begin
// --- Режим ---
FMode := B.Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
FFilter := B.FilterIdx;
FFilterBW := B.FilterBW;
SanitizeFilterForMode;
ApplyModeFilter;
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
FAGCMode := B.AGCMode;
FAGCTop := B.AGCTop;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine;
// --- FM squelch / CTCSS / step / repeater ---
FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel;
FFMCTCSSOn := B.CTCSSOn;
FFMCTCSSToneIdx := B.CTCSSToneIdx;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
end;
FFMStepOn := B.FMStepOn;
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
FFMRptDir := B.FMRptDir;
FFMRptOffsetHz := B.FMRptOffsetHz;
if FFMRptOffsetHz <= 0 then FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA);
// --- CTUN ---
FCTun := B.CTun;
if FWDSPReady and Assigned(FDSPEngine) and not FCTun then FDSPEngine.SetShift(0.0);
// --- Span (sample rate глобальный, не per-band — лишь ресинк) ---
FSpanHz := FSampleRate;
// --- Рендер: единичные события (UI рисует через OnControllerState) ---
Changed(rfBand);
Changed(rfMode); // rfMode-рендер зовёт UpdateFilterButtons (FM-панели)
Changed(rfFilter);
Changed(rfAGCMode);
Changed(rfAGCTop);
Changed(rfFMSQ);
Changed(rfFMSQLevel);
Changed(rfFMCTCSS);
Changed(rfFMCTCSSTone);
Changed(rfFMStep); // кнопка вкл/выкл STEP
Changed(rfFMStepIdx); // дропдаун шага + web + grid
Changed(rfFMRpt);
Changed(rfCTun);
end;
procedure TRadioController.RestoreBand(BandIdx: Integer);
// Загружает состояние диапазона из кэша, применяет к DSP и шлёт события рендера.
// Финальную перестройку VFO A (с канальной оркестрацией) делает вызывающий
// UI-путь через ApplyVfoA(FVfoA); здесь FVfoA лишь выставляется.
var B: TBandSettings;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
B := FBandCache[BandIdx];
FCurrentBand := BandIdx;
ApplyBandDSP(B);
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
FCenterFreq := B.VfoA;
FVfoB := B.VfoB;
FVfoA := B.VfoA;
FDriveLevel := CalcDriveByte;
Changed(rfVfoB);
// rfVfoA НЕ шлём: финальную перестройку делает ApplyVfoA в UI-обёртке.
end;
function TRadioController.ActiveVfoHz: Double;
begin
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
end;
procedure TRadioController.ApplyTuneCore(VfoHz: Double);
// Общее ядро перестройки приёмника на частоту АКТИВНОГО VFO (A или B).
// CTUN OFF: DDC = VfoHz, сдвиг 0.
// CTUN ON: DDC фиксирован, демодулятор сдвигаем на VfoHz-FCenterFreq;
// если полоса фильтра вышла за край дисплея — прокручиваем центр.
// Ставит FCenterFreq / shift / FLastVfoScrolled.
var
Offset, FiltLo, FiltHi, HalfSpan: Double;
begin
FLastVfoScrolled := False;
if not FCTun then
begin
FCenterFreq := VfoHz;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
end
else
begin
Offset := VfoHz - FCenterFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
HalfSpan := FSpanHz / 2;
case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
begin
if FiltHi > HalfSpan then
FCenterFreq := VfoHz - HalfSpan * 0.5
else
FCenterFreq := VfoHz + HalfSpan * 0.5;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(VfoHz - FCenterFreq);
FLastVfoScrolled := True;
end;
end;
end;
procedure TRadioController.SetVfoA(Hz: Double);
// Перестройка VFO A: XVTR-клэмп, CTUN ядро, детект диапазона, FM авто-RPT,
// проталкивание в сеть. Рендер — в UI через OnControllerState(rfVfoA/rfBand).
// Канальную оркестрацию (авто-CTCSS, pre-channel drive) делает UI-путь.
var
NewFreq: Int64;
BandIdx: Integer;
begin
NewFreq := Round(Hz);
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
end;
FVfoA := NewFreq;
ApplyTuneCore(FVfoA);
// Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band,
// FreqToBandIdx вернёт -1 для VHF/UHF).
if FCurrentXvtr < 0 then
begin
BandIdx := FreqToBandIdx(FVfoA);
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.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);
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);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin
SaveCurrentBand; // сохранить покидаемый бэнд
FCurrentBand := NewBand;
ApplyBandDSP(FBandCache[NewBand]); // mode/filter/AGC/CTUN/FM + рендер
FCenterFreq := ActiveFreq; // центр на активную (CTUN центрируется тут)
FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда
NeedPush := True; // DDC-центр сменился → толкнуть в сеть
end;
end;
if FCTun then
begin
HalfSpan := FSpanHz / 2;
Offset := ActiveFreq - FCenterFreq;
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
if FreqVisible then
begin
// Частота видна — просто сдвигаем демодулятор.
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
end
else
begin
// Частота за окном — сбрасываем CTUN и центрируемся на ней.
FCTun := False;
FBandCache[FCurrentBand].CTun := False;
FCenterFreq := ActiveFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
NeedPush := True;
Changed(rfCTun);
end;
end
else
begin
FCenterFreq := ActiveFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
NeedPush := True;
end;
if NeedPush then PushNetworkState;
Changed(rfActiveVfo);
end;
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
begin
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
else SetVfoB(FVfoB + DeltaHz);
end;
procedure TRadioController.SetCenter(Hz: Double);
// CTUN-скролл окна просмотра: двигаем DDC-центр БЕЗ смены VFO. Демодулятор
// сдвигаем = активный VFO − новый центр; полный HP-кадр в радио. Рендер
// (ruler/спектр) — в UI через OnControllerState(rfCenterFreq).
// Точки входа: десктопный CTUN-drag и web (SyncWebCenter).
begin
FCenterFreq := Hz;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
PushNetworkState;
Changed(rfCenterFreq);
end;
procedure TRadioController.VfoSwap;
// Меняем VFO A/B местами и перестраиваем приёмник на новую частоту активного
// VFO (центр=частота, shift 0 — CTUN-оффсет после свопа теряет смысл).
var
T: Double;
ActiveFreq: Double;
BandIdx: Integer;
begin
T := FVfoA; FVfoA := FVfoB; FVfoB := T;
ActiveFreq := ActiveVfoHz;
FCenterFreq := ActiveFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
BandIdx := FreqToBandIdx(ActiveFreq);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
FCurrentBand := BandIdx;
Changed(rfBand);
end;
PushNetworkState;
Changed(rfVfoA);
Changed(rfVfoB);
end;
procedure TRadioController.VfoACopyB; // A>B: частота A копируется в B
begin
SetVfoB(FVfoA); // сохраняет B:=A, перестраивает если B активен, рендер rfVfoB
end;
procedure TRadioController.VfoBCopyA; // B>A: частота B копируется в A
begin
FVfoA := FVfoB;
if FActiveVfo = 0 then SetVfoA(FVfoA) // A активен — перестраиваем приёмник
else Changed(rfVfoA); // A неактивен — только рендер дисплея
end;
procedure TRadioController.SetMode(M: Integer);
begin
FMode := M;
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
FBandCache[FCurrentBand].Mode := FMode;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetMode(FMode);
// Если TUN активен — пересчитываем знак частоты под новый режим
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
end;
ApplyModeFilter;
Changed(rfMode);
end;
procedure TRadioController.SetFilterIdx(Idx: Integer);
begin
FFilter := Idx;
case FMode of
0, 1: FFilterBW := FILT_SSB_BW[Idx];
2: FFilterBW := FILT_DSB_BW[Idx];
3, 4: FFilterBW := FILT_CW_BW[Idx];
MODE_FM:
begin
FFilterBW := FILT_FM_BW[Idx];
FFMDeviation := FILT_FM_DEV[Idx];
end;
else FFilterBW := FILT_AM_BW[Idx];
end;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
ApplyModeFilter;
Changed(rfFilter);
end;
procedure TRadioController.SetFilterBW(BWHz: Integer);
// Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а
// FFilter подбираем ближайшим пресетом — только для подсветки кнопки.
var
BW_arr: array[0..FILT_COUNT-1] of Integer;
best, dist, i: Integer;
begin
FFilterBW := BWHz;
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
case FMode of
0, 1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
3, 4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;
for i := 0 to FILT_COUNT-1 do
if Abs(BW_arr[i] - BWHz) < dist then
begin
dist := Abs(BW_arr[i] - BWHz);
best := i;
end;
FFilter := best;
if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best];
FBandCache[FCurrentBand].FilterBW := FFilterBW;
ApplyModeFilter;
Changed(rfFilter);
end;
procedure TRadioController.ApplyAGCToEngine;
const
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для
// всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
begin
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
FDSPEngine.SetAGC(AGCModes[FAGCMode], 50.0);
end;
procedure TRadioController.SetAGCMode(M: Integer);
begin
if (M < 0) or (M > 4) then Exit;
FAGCMode := M;
ApplyAGCToEngine;
FBandCache[FCurrentBand].AGCMode := FAGCMode;
Changed(rfAGCMode);
end;
procedure TRadioController.SetAGCTop(DB: Integer);
begin
if DB < 20 then DB := 20 else if DB > 120 then DB := 120;
FAGCTop := DB;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine; // ре-применяем режим с новым уровнем
FBandCache[FCurrentBand].AGCTop := FAGCTop;
Changed(rfAGCTop);
end;
procedure TRadioController.AGCTopBy(Delta: Integer);
begin SetAGCTop(FAGCTop + Delta); end;
procedure TRadioController.SetVolume(V: Integer);
begin
if V < 0 then V := 0 else if V > 100 then V := 100;
FVolume := V;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(FVolume / 100.0);
Changed(rfVolume);
end;
procedure TRadioController.VolumeBy(Delta: Integer);
begin SetVolume(FVolume + Delta); end;
procedure TRadioController.SetMute(On_: Boolean);
begin
FMuted := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMute(FMuted);
Changed(rfMute);
end;
procedure TRadioController.ToggleMute;
begin SetMute(not FMuted); end;
procedure TRadioController.SetDrive(V: Integer);
begin
if V < 0 then V := 0 else if V > 100 then V := 100;
FDrivePercent := V;
FDriveLevel := CalcDriveByte;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
PushNetworkState; // протолкнуть новый drive в радио (если подключено+Running)
Changed(rfDrive);
end;
procedure TRadioController.DriveBy(Delta: Integer);
begin SetDrive(FDrivePercent + Delta); 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);
begin FCurrentBand := Idx; Changed(rfBand); { TODO wiring: RestoreBand } end;
procedure TRadioController.BandUp;
begin if FCurrentBand < CFG_BAND_COUNT - 1 then SetBand(FCurrentBand + 1); end;
procedure TRadioController.BandDown;
begin if FCurrentBand > 0 then SetBand(FCurrentBand - 1); end;
procedure TRadioController.ApplyXvtrToNetwork;
// Передаёт XVTR-режим в сеть (enable bit + T/R relay suppression + RX ant).
var
En, DisablePA: Boolean;
RxAnt: Byte;
begin
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
En := True;
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
end
else
begin
En := False; DisablePA := False; RxAnt := 0;
end;
if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.SetXvtrMode(En, DisablePA, RxAnt);
end;
procedure TRadioController.ActivateXvtr(Idx: Integer);
// Вход в XVTR-слот: state+DSP через общий ApplyBandDSP + VFO/центр/сеть.
// Рендер: rfXvtr (XVTR-кнопки), rfBand (XVTR-aware гасит HF), rfActiveVfo
// (дисплеи A/B + полный перерисов спектра). UI-обёртка добавляет wideband+web.
var
E: TXvtrEntry;
B: TBandSettings;
Vis: Double;
begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
FCurrentXvtr := Idx;
E := FXvtrSettings.Entries[Idx];
// Начальная частота: LastFreq если в диапазоне, иначе середина.
Vis := E.LastFreq;
if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
ApplyXvtrToNetwork; // XVTR enable bit + DisablePA + RX ant
// Авто-флаги канала (не входят в TBandSettings/ApplyBandDSP).
FFMCTCSSAutoActive := E.LastCTCSSAutoActive;
FFMRptAutoActive := E.LastFMRptAutoActive;
// DSP-настройки слота — через общий путь Band (mode/filter/AGC/CTUN/FM + рендер).
B.Mode := E.LastMode;
B.FilterIdx := E.LastFilterIdx;
B.FilterBW := FilterBWFor(E.LastMode, E.LastFilterIdx);
B.AGCMode := E.LastAGCMode;
B.AGCTop := E.LastAGCTop;
B.CTun := E.LastCTun;
B.SpanHz := FSampleRate;
B.FMSQOn := E.LastFMSQOn;
B.FMSQLevel := E.LastFMSQLevel;
B.CTCSSOn := E.LastCTCSSOn;
B.CTCSSToneIdx := E.LastCTCSSToneIdx;
B.FMStepOn := E.LastFMStepOn;
B.FMStepIdx := E.LastFMStepIdx;
B.FMRptDir := E.LastFMRptDir;
B.FMRptOffsetHz := E.LastFMRptOffsetHz;
B.VfoA := Vis;
B.VfoB := FVfoB;
ApplyBandDSP(B);
// VFO: B — своя сохранённая частота слота (клэмп в диапазон); A = visible.
FVfoB := EnsureRange(E.LastFreqB, E.FreqBegin, E.FreqEnd);
FVfoA := Vis;
FCenterFreq := Vis;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
FDriveLevel := CalcDriveByte;
PushNetworkState;
Changed(rfXvtr);
Changed(rfActiveVfo);
end;
procedure TRadioController.DeactivateXvtr;
// Сохраняет текущий XVTR-слот и выходит в HF. Сам HF-возврат (RestoreBand +
// финальная перестройка активного VFO) делает UI-обёртка — там же ResetWfAvgBuf.
begin
if FCurrentXvtr < 0 then Exit;
SaveCurrentBand; // XVTR-ветка: пишет весь слот + SaveXvtr
if FDevConnected then FSettings.Save;
FCurrentXvtr := -1;
ApplyXvtrToNetwork; // снять XVTR enable bit
Changed(rfXvtr); // погасить XVTR-кнопки
end;
procedure TRadioController.SetXvtrBand(Idx: Integer);
// Единая точка для GUI/Web/CAT: Idx>=0 — войти в слот, Idx<0 — выйти в HF.
// ВНИМАНИЕ: HF-возврат при выходе делает UI-обёртка DeactivateXvtr (RestoreBand).
begin
if Idx >= 0 then ActivateXvtr(Idx)
else DeactivateXvtr;
end;
procedure TRadioController.SetCurrentBandIdx(NewBand: Integer);
// Лёгкая смена HF-диапазона без полного restore: фиксируем индекс диапазона
// (для drive-кал и подсветки кнопок) и пересчитываем drive-байт. Используется
// канальным адаптером при переходе канала на другой HF-диапазон.
begin
FCurrentBand := NewBand;
FDriveLevel := CalcDriveByte;
Changed(rfBand);
end;
procedure TRadioController.SetSpan(Hz: Integer);
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
procedure TRadioController.SetSampleRate(Hz: Integer);
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в
// трансивер и полное восстановление DSP-состояния. Команда сама персистит
// (sample rate глобальный); рендер overlay/AGC-линий — в OnControllerState.
begin
if (Hz <= 0) or (Hz = FSampleRate) then Exit;
FSampleRate := Hz;
FSpanHz := Hz;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
// 1. Останавливаем WDSP и меняем rate (Close + FlushQueue + Open).
if FWDSPReady and Assigned(FDSPEngine) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(Hz);
end;
// 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate).
if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.ConfigureDDCs(1, Hz div 1000, 0, FDitherEnabled, FRandomEnabled);
// 3. Восстанавливаем DSP-состояние (ChangeSampleRate пересоздал канал).
if Assigned(FDSPEngine) then
begin
FWDSPReady := FDSPEngine.Initialized;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
FDSPEngine.SetNRMode(FNRMode);
FDSPEngine.SetNBMode(FNBMode);
FDSPEngine.SetSNB(FSNB);
FDSPEngine.SetANF(FANF);
FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine; // единый маппинг (раньше здесь был ошибочный каст индекса)
if Assigned(FNetwork) and FNetwork.Connected then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
end;
end;
SaveGlobalSettings; // sample rate глобальный — персистим (гейт по FDevConnected внутри)
Changed(rfSampleRate);
end;
procedure TRadioController.SetCTun(On_: Boolean);
begin
FCTun := On_;
FBandCache[FCurrentBand].CTun := FCTun;
if not FCTun then
begin
// Выключили CTUN: центрируемся на АКТИВНОМ VFO, shift=0. (Включение CTUN не
// трогает движок — FCenterFreq остаётся как есть, спектр не прыгает.)
FCenterFreq := ActiveVfoHz;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
PushNetworkState;
end;
Changed(rfCTun);
end;
procedure TRadioController.SetDuplex(On_: Boolean);
begin FDisplayDuplex := On_; Changed(rfDuplex); { TODO wiring } end;
procedure TRadioController.SetWfAGC(On_: Boolean);
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
procedure TRadioController.SetWfNF(On_: Boolean);
begin FWfNFEnabled := On_; Changed(rfWfNF); end;
procedure TRadioController.SetNR(Mode: Integer);
begin
if Mode < 0 then Mode := 0 else if Mode > 4 then Mode := 4;
FNRMode := Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNRMode(FNRMode);
Changed(rfNR);
end;
procedure TRadioController.SetNB(Mode: Integer);
begin
if Mode < 0 then Mode := 0 else if Mode > 2 then Mode := 2;
FNBMode := Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNBMode(FNBMode);
Changed(rfNB);
end;
procedure TRadioController.SetSNB(On_: Boolean);
begin
FSNB := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetSNB(FSNB);
Changed(rfSNB);
end;
procedure TRadioController.SetANF(On_: Boolean);
begin
FANF := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetANF(FANF);
Changed(rfANF);
end;
function TRadioController.DefaultMicSource: TTXMicSource;
begin
if FAudioInDevName <> '' then Result := txmsSoundCard
else Result := txmsRadio;
end;
function TRadioController.BuildMicLineSelectByte: Byte;
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
// bit 0 — Line In (1 = line-in вместо mic-in)
// bit 1 — Mic Boost (+20 dB); не ставим когда Line In
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
// bit 3 — Tip/Ring (0=Tip, 1=Ring; Orion/OrionMkII)
// bit 4 — Orion mic bias (1 = enable +5V bias)
// bit 5 — Saturn XLR/balanced (не используем)
begin
Result := 0;
if FTXSettings.MicLineIn then
Result := Result or $01 // bit 0: Line In
else if FTXSettings.MicBoost then
Result := Result or $02; // bit 1: Boost только при Mic In
if not FTXSettings.MicPTTEnabled then Result := Result or $04; // bit 2: инверсия
if FTXSettings.MicTipRing then Result := Result or $08; // bit 3: Ring
if FTXSettings.MicBias then Result := Result or $10; // bit 4: bias
end;
procedure TRadioController.ApplyNoiseFilterButtonsToDSP;
begin
if not FWDSPReady then Exit;
FDSPEngine.SetNRMode(FNRMode);
FDSPEngine.SetNBMode(FNBMode);
FDSPEngine.SetSNB(FSNB);
FDSPEngine.SetANF(FANF);
end;
procedure TRadioController.SendDUCSpecificFromSettings;
var
DUCPkt: TDUCSpecificPacket;
RateKsps: Word;
begin
if not (FNetwork.Connected and FNetwork.Running) then Exit;
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
RateKsps := FDSPEngine.TXSampleRate div 1000;
DUCPkt.NumDACs := 1;
DUCPkt.SidetoneLevel := 50;
DUCPkt.SidetoneFreqHi := Hi(600);
DUCPkt.SidetoneFreqLo := Lo(600);
DUCPkt.KeyerSpeed := 20;
DUCPkt.KeyerWeight := 50;
DUCPkt.DUC0RateHi := Hi(RateKsps);
DUCPkt.DUC0RateLo := Lo(RateKsps);
DUCPkt.DUC0Bits := 24;
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
FNetwork.SendDUCSpecific(DUCPkt);
end;
procedure TRadioController.ApplyTXSettingsToDSP;
begin
if not FWDSPReady then Exit;
// Mic source — выставляем значение по умолчанию для MOX-кнопки;
// при реальном PTT SetMOX скорректирует источник.
FDSPEngine.SetTXMicSource(DefaultMicSource);
FDSPEngine.SetMicGain(FTXSettings.MicGainDB);
FDSPEngine.SetTXFilterFull(FTXSettings.FilterLow, FTXSettings.FilterHigh,
FTXSettings.FilterNC, FTXSettings.FilterMP,
FTXSettings.FilterWindow);
FDSPEngine.SetTXCompressor(FTXSettings.CompressorOn, FTXSettings.CompressorGain);
FDSPEngine.SetTXLeveler(FTXSettings.LevelerOn, FTXSettings.LevelerTop,
FTXSettings.LevelerDecay);
FDSPEngine.SetTXALC(FTXSettings.ALCOn, FTXSettings.ALCMaxGain,
FTXSettings.ALCDecay);
FDSPEngine.SetTXPhaseRot(FTXSettings.PhaseRotOn, FTXSettings.PhaseRotStages,
FTXSettings.PhaseRotFreq);
FDSPEngine.SetTXEQ(FTXSettings.EQOn, FTXSettings.EQNumBands,
FTXSettings.EQGains, FTXSettings.EQFreqs);
FDSPEngine.SetTXAMCarrierLevel(FTXSettings.AMCarrierLevel);
FDSPEngine.SetTXFMParams(FTXSettings.FMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetTXCTCSS(FTXSettings.CTCSSOn, FTXSettings.CTCSSFreq);
// TX display
FDSPEngine.SetTXFFTParams(FTXSettings.TXFFTSize, FTXSettings.TXWindowType);
FDSPEngine.SetTXSpectrumDisplay(FTXSettings.TXSpecDetector,
FTXSettings.TXSpecAvgMode,
FTXSettings.TXSpecAvgTimeMS);
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
FTXSettings.TXWfAvgMode,
FTXSettings.TXWfAvgTimeMS);
end;
procedure TRadioController.SetMOX(On_: Boolean);
var
WasTransmitting: Boolean;
begin
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then
Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
Exit;
WasTransmitting := FTransmitting;
FTransmitting := On_;
if On_ then FDriveLevel := CalcDriveByte;
FDUCPendingCount := 0; // сброс TX DUC-очереди на смене TX-состояния
// Сетевая PTT-последовательность TX-on: сначала реле (Alex bit27=1, drive) без
// PTT, затем PTT=1 — переключаем реле раньше передачи.
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.ClearDUCIQQueue;
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, True, True, True);
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
end;
if FWDSPReady and Assigned(FDSPEngine) then
begin
// Mic source автоматически по источнику TX:
// Web TX → txmsWeb; HW PTT → txmsRadio; MOX/CAT → DefaultMicSource.
if FTransmitting then
begin
if FWebClientActive then
begin
FWebMicActive := True;
FDSPEngine.SetTXMicSource(txmsWeb);
end
else if FHWPTTActive then
FDSPEngine.SetTXMicSource(txmsRadio)
else
FDSPEngine.SetTXMicSource(DefaultMicSource);
end
else if FWebMicActive then
begin
FWebMicActive := False;
FDSPEngine.SetTXMicSource(DefaultMicSource);
end;
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
FDSPEngine.SetTXRun(FTransmitting);
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin
FDSPEngine.FlushRX;
if Assigned(FAudioOut) then
FAudioOut.Clear;
{$IFDEF WINDOWS}
// Windows: на TX→RX дропаем И RX-IQ, И аудио-выход — radio FPGA TX-buffer +
// PA slew-down дотравливают «хвост» (на Windows тянется 200500мс).
FDSPEngine.BeginPostTXMute(250);
{$ENDIF}
end;
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
// источник на RX, чтобы видеть приём во время передачи.
if FTransmitting and FDisplayDuplex then
FDSPEngine.SetDisplaySourceTX(False);
end;
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, False, True, True);
FNetwork.ClearDUCIQQueue;
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
end;
Changed(rfTransmitting);
end;
procedure TRadioController.SetTune(On_: Boolean);
begin
if On_ = FTuning then Exit;
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit;
if On_ then
begin
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
FTuning := True;
FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень)
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0);
if not FTransmitting then SetMOX(True); // PTT + run TXA-канал
end
else
begin
if FTransmitting then SetMOX(False);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXTone(False, 0, 0);
FTuning := False;
FDriveLevel := CalcDriveByte; // вернуть обычный drive%
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
Changed(rfTuning);
end;
procedure TRadioController.SetFMStep(Idx: Integer);
begin
if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4
FFMStepIdx := Idx;
Changed(rfFMStepIdx);
end;
procedure TRadioController.SetFMCTCSS(On_: Boolean);
begin
FFMCTCSSOn := On_;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
Changed(rfFMCTCSS);
end;
procedure TRadioController.SetFMCTCSSTone(Idx: Integer);
begin
if Idx < 0 then Idx := 0 else if Idx > CTCSS_COUNT - 1 then Idx := CTCSS_COUNT - 1;
FFMCTCSSToneIdx := Idx;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
Changed(rfFMCTCSSTone);
end;
procedure TRadioController.SetFMSquelch(On_: Boolean);
begin
FFMSQOn := On_;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
Changed(rfFMSQ);
end;
procedure TRadioController.SetFMSquelchLevel(V: Integer);
begin
FFMSQLevel := V;
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
Changed(rfFMSQLevel);
end;
procedure TRadioController.SetFMRpt(Dir: Integer);
begin
FFMRptDir := Dir;
FFMRptAutoActive := False; // явная установка направления — ручное действие
PushNetworkState; // TX-сдвиг применяется через ActiveTXFreqHz
Changed(rfFMRpt);
end;
procedure TRadioController.SetFMRptOffset(Hz: Double);
begin
if Hz <= 0.0 then Exit;
FFMRptOffsetHz := Hz;
if FFMRptDir <> RPT_NONE then PushNetworkState;
Changed(rfFMRpt);
end;
procedure TRadioController.UpdateRptAutoState;
// Авто-включение MINUS-сдвига в 2-метровом репитерном сегменте и авто-снятие
// при уходе из него (только если MINUS был выставлен автоматически).
begin
if RptAutoDir(FVfoA) = RPT_MINUS then
begin
if FFMRptDir = RPT_NONE then
begin
FFMRptDir := RPT_MINUS;
FFMRptAutoActive := True;
Changed(rfFMRpt);
end;
end
else
begin
if (FFMRptDir = RPT_MINUS) and FFMRptAutoActive then
begin
FFMRptDir := RPT_NONE;
FFMRptAutoActive := False;
Changed(rfFMRpt);
end;
end;
end;
procedure TRadioController.StartStop;
begin SetRun(not FRunning); end;
procedure TRadioController.SetRun(On_: Boolean);
begin FRunning := On_; Changed(rfRunning); { TODO wiring: логика BtnStartStopClick } end;
procedure TRadioController.StartRunning(SpectrumWidth: Integer);
// «Go-live» последовательность Run=1: General packet, wideband/DDC-конфиг,
// синхронизация WDSP sample rate, восстановление DSP (mode/volume/filter/NR/AGC),
// Run=1 → seq-сброс → DDC/DUC Specific + TX-цепь. Рендер run-кнопки/sleep — UI
// через OnControllerState(rfRunning); статус-бар/FSpecView.AGCTop/resize — обёртка.
var
GenPkt: TGeneralPacket;
begin
// General packet
FillChar(GenPkt, SizeOf(GenPkt), 0);
GenPkt.Command := CMD_GENERAL;
GenPkt.Flags37 := $08;
GenPkt.Flags38 := $01;
GenPkt.PAConfig := $01;
GenPkt.WBPort[0] := (PORT_WIDEBAND_ADC0 shr 8) and $FF;
GenPkt.WBPort[1] := PORT_WIDEBAND_ADC0 and $FF;
GenPkt.WBEnable := IfThen(FShowWideband, 1, 0);
GenPkt.WBSamplesPerPkt[0] := 512 shr 8;
GenPkt.WBSamplesPerPkt[1] := 512 and $FF;
GenPkt.WBSampleSize := 16;
GenPkt.WBUpdateRate := 70;
GenPkt.WBPacketsPerFrame := 32;
if FNetwork.Device.BoardType = 5 then
GenPkt.AlexEnable := $03
else
GenPkt.AlexEnable := $01;
FNetwork.SendGeneralPacket(GenPkt);
FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32);
if FNetwork.Device.BoardType in [3, 4, 5] then
FActiveDDC := 2
else
FActiveDDC := 0;
// Синхронизируем samplerate WDSP если нужно
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(FSampleRate);
FWDSPReady := FDSPEngine.Initialized;
end;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
ApplyNoiseFilterButtonsToDSP;
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True);
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
FRunning := True;
FRXStartTime := GetTickCount64;
FRXLastPktTime := 0;
FRXPacketCount := 0;
FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0);
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
// Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1).
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
SendDUCSpecificFromSettings;
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP;
ApplyModeFilter;
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
if FWDSPReady then
FDSPEngine.UpdateAGCLines(SpectrumWidth); // ещё не получили ни одного пакета
Changed(rfRunning); // UI: caption STOP / MOX.Enabled / InhibitSleep
end;
procedure TRadioController.StopRunning;
// Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся
// Connected — Disconnect отдельно (UI-обёртка делает это после persistence).
// Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting.
begin
if FRunning then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False;
FTransmitting := False;
if FWDSPReady then FDSPEngine.SetTXRun(False);
Changed(rfTransmitting); // MOX-кнопка off
end;
FRXPacketCount := 0;
FActiveDDC := 0;
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
end;
function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean;
// Подключение + загрузка per-device настроек по MAC в поля контроллера + конфиг
// движка (DSP display, audio). Возвращает False, если Connect не удался (UI
// покажет сообщение по FNetwork.LastError). Рендер ВСЕХ device-виджетов — UI
// через OnControllerState(rfDevice). Перестройку диапазона/VFO (RestoreBand/
// XVTR/ActiveVfo) делает UI-обёртка ПОСЛЕ (там финальная перестройка через
// ApplyVfoA + wideband/web-extras). FLoadedGlobal хранит загруженный G_Settings
// для рендера полей, которых нет в контроллере (FPS/FreqMhzDigits/CAT).
var
i: Integer;
begin
Result := FNetwork.Connect(Dev);
if not Result then Exit;
// --- Загружаем настройки устройства по MAC ---
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache);
// TX/Alex/XVTR per-device
FSettings.LoadTX(FDevMAC, FTXSettings);
FSettings.LoadAlex(FDevMAC, FAlexSettings);
FNetwork.SetAlexConfig(FAlexSettings);
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
FCurrentXvtr := -1;
ApplyXvtrToNetwork;
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
FTXSpecRange := FTXSettings.TXSpecRange;
if FTXSettings.TXSpecGridStep > 0 then
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
FVolume := FLoadedGlobal.Volume;
FActiveVfo := FLoadedGlobal.ActiveVfo;
FPAMaxPower := FLoadedGlobal.PAMaxPower;
for i := 0 to CFG_BAND_COUNT - 1 do
FPABandCal[i] := FLoadedGlobal.PABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do
FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i];
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
FCurrentBand := FLoadedGlobal.LastBand;
FDriveLevel := CalcDriveByte;
// SampleRate — глобальный, до RestoreBand
if FLoadedGlobal.SampleRate > 0 then
begin
FSampleRate := FLoadedGlobal.SampleRate;
FSpanHz := FSampleRate;
end;
FNRMode := EnsureRange(FLoadedGlobal.NRMode, 0, 4);
FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2);
FSNB := FLoadedGlobal.SNBEnabled;
FANF := FLoadedGlobal.ANFEnabled;
FWfAGCEnabled := FLoadedGlobal.WfAGCEnabled;
FWfNFEnabled := FLoadedGlobal.WfNFEnabled;
FDitherEnabled := FLoadedGlobal.DitherEnabled;
FRandomEnabled := FLoadedGlobal.RandomEnabled;
FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio;
FWfManualHigh := FLoadedGlobal.WfManualHigh;
FWfManualLow := FLoadedGlobal.WfManualLow;
FWfAGCOffset := FLoadedGlobal.WfAGCOffset;
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
FShowWideband := FLoadedGlobal.ShowWideband;
FWidebandFill := FLoadedGlobal.WidebandFill;
FSpectrumFill := FLoadedGlobal.SpectrumFill;
FDisplayDuplex := FLoadedGlobal.DisplayDuplex;
// Настройки дисплея/сетки + DSP display + аудио-устройства
if FLoadedGlobal.FFTSize > 0 then
begin
FSpecRefLevel := FLoadedGlobal.SpecRefLevel;
FSpecRange := FLoadedGlobal.SpecRange;
if FLoadedGlobal.SpecGridStep > 0 then FSpecGridStep := FLoadedGlobal.SpecGridStep;
if FWDSPReady then
begin
FDSPEngine.SetFFTParams(FLoadedGlobal.FFTSize, FLoadedGlobal.WindowType);
FDSPEngine.SetSpectrumDisplay(FLoadedGlobal.SpecDetector, FLoadedGlobal.SpecAvgMode,
FLoadedGlobal.SpecAvgTimeMS);
FDSPEngine.SetWaterfallDisplay(FLoadedGlobal.WfDetector, FLoadedGlobal.WfAvgMode,
FLoadedGlobal.WfAvgTimeMS);
end;
// Аудио устройство вывода
FAudioOut.Close;
if FLoadedGlobal.AudioSampleRate > 0 then
FAudioOut.SampleRate := FLoadedGlobal.AudioSampleRate;
if FLoadedGlobal.AudioOutDevice <> '' then
begin
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(FLoadedGlobal.AudioOutDevice);
FAudioOutDevName := FLoadedGlobal.AudioOutDevice;
end
else
begin
FAudioOut.DeviceIndex := -1;
FAudioOutDevName := '';
end;
FAudioOut.Open;
// Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT —
// здесь только открываем устройство.
if FLoadedGlobal.AudioInDevice <> '' then
begin
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice);
FAudioInDevName := FLoadedGlobal.AudioInDevice;
FAudioIn.Open;
end
else
begin
FAudioIn.DeviceIndex := -1;
FAudioInDevName := '';
end;
end;
Changed(rfDevice); // UI: полная синхронизация device-виджетов из состояния
end;
// ---------------------------------------------------------------------------
// 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;
var
Devs: THPSDRDeviceArray;
begin
Devs := FCtrl.FNetwork.Discover(2000);
// Найденные устройства сигналятся колбэком OnDeviceFound во время Discover().
if Length(Devs) = 0 then
FCtrl.Invoke(FCtrl.DiscoverFinishedNone);
end;
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
// Сетевой поток: строим строку для списка → pending → маршалинг в ApplyDeviceFound.
var
BoardName: string;
begin
BoardName := BoardTypeName(Dev.BoardType);
FPendingFoundDev := Dev;
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
[Dev.IPAddress, BoardName,
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);
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.