mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
The TX IQ handler (batch WDSP TXA output into 240-sample DUC packets, 24-bit clamp, FNetwork.SendDUCIQ) moves into TRadioController.OnTXIQ, along with the FDUCPendingI/Q/Count queue fields. The DUC-queue reset on a TX-state change moves from the rfTransmitting render into SetMOX itself (the render does not run headless). Both DSP-callback wiring sites (FormCreate and RecreateDSPEngine) now point at FController.OnTXIQ. Backend-agnostic: a Pluto backend converts 24-bit->int16 in SendDUCIQ. Pre-existing gap noted: RecreateDSPEngine never re-wires OnPullMicSamples, so sound-card mic is lost after a sample-rate change (unchanged here). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1834 lines
75 KiB
ObjectPascal
1834 lines
75 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,
|
||
WDSPEngine, AudioOutput, AudioInput,
|
||
Settings, ChannelStore, FMRepeater, BoardUtils;
|
||
|
||
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 // подключённое устройство сменилось
|
||
);
|
||
|
||
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;
|
||
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;
|
||
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;
|
||
|
||
// ---- DSP-фильтры шума / водопад ----
|
||
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
|
||
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FDitherEnabled: Boolean;
|
||
FRandomEnabled: Boolean;
|
||
|
||
// ---- 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;
|
||
|
||
// ---- Телеметрия (наполняется в Фазе 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;
|
||
|
||
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);
|
||
|
||
// Маршалинг команды в поток контроллера (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;
|
||
|
||
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
|
||
// MakeBandSettings — снимок текущего состояния в запись диапазона.
|
||
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
|
||
// RestoreBand — загружает состояние диапазона, применяет к DSP, шлёт события
|
||
// рендера. Финальную перестройку VFO A делает вызывающий (UI: ApplyVfoA).
|
||
function MakeBandSettings: TBandSettings;
|
||
procedure SaveCurrentBand;
|
||
procedure RestoreBand(BandIdx: Integer);
|
||
// Применяет 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 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
|
||
|
||
// Каналы
|
||
procedure ApplyChannel(Idx: Integer);
|
||
|
||
// Жизненный цикл движка / устройства
|
||
procedure StartStop;
|
||
procedure SetRun(On_: Boolean);
|
||
procedure ConnectDevice(const Dev: THPSDRDevice);
|
||
procedure Discover;
|
||
|
||
// ---- События наружу ----
|
||
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
||
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
||
end;
|
||
|
||
implementation
|
||
|
||
constructor TRadioController.Create;
|
||
begin
|
||
inherited Create;
|
||
FSettings := TSettingsManager.Create; // владелец настроек (GUI и демон)
|
||
// Дефолты (дублируют 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(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;
|
||
|
||
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);
|
||
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 FDrivePercent := V; Changed(rfDrive); { TODO wiring: CalcDriveByte + WDSP } 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.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-состояния. Сохранение в global
|
||
// settings + рендер overlay/AGC-линий остаются в UI (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;
|
||
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;
|
||
|
||
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 тянется 200–500мс).
|
||
FDSPEngine.BeginPostTXMute(250);
|
||
{$ENDIF}
|
||
end;
|
||
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
|
||
// источник на RX, чтобы видеть приём во время передачи.
|
||
if FTransmitting and FDisplayDuplex then
|
||
FDSPEngine.SetDisplaySourceTX(False);
|
||
end;
|
||
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
|
||
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), 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.ApplyChannel(Idx: Integer);
|
||
begin Changed(rfChannel); { TODO wiring } 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.ConnectDevice(const Dev: THPSDRDevice);
|
||
begin { TODO wiring: DoConnectDevice } Changed(rfDevice); end;
|
||
|
||
procedure TRadioController.Discover;
|
||
begin { TODO wiring: FNetwork.Discover } end;
|
||
|
||
end.
|