mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Span ≡ sample rate in this app; OnSampleRateSelect (overlay dropdown) is a fairly self-contained heavy-class item. - TRadioController.SetSampleRate(Hz): sets FSampleRate/FSpanHz + band cache, recreates the WDSP channel (ChangeSampleRate = Close+FlushQueue+Open), pushes the new DDC rate to the radio, and restores full DSP state (mode/volume/filter/NR/NB/SNB/ANF/AGC-top/AGC/shift). - Fixes the same latent AGC cast bug as earlier batches: the inline restore used SetAGC(TWDSPAGCMode(FAGCMode)) (index cast); now goes through the shared ApplyAGCToEngine mapping. MainForm: OnSampleRateSelect is a thin wrapper (global-settings SaveGlobal stays in UI). OnControllerState(rfSampleRate) renders the rate overlay and recomputes AGC display lines (UpdateAGCLines needs UI spectrum width). Dropped the cosmetic ProcessMessages pre-paint. Startup paths that set FSampleRate directly + RecreateDSPEngine left as-is. Builds clean, links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
791 lines
30 KiB
ObjectPascal
791 lines
30 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,
|
|
HPSDRProtocol, HPSDRNetwork,
|
|
WDSPEngine, AudioOutput, AudioInput,
|
|
Settings, ChannelStore, FMRepeater;
|
|
|
|
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, // телеметрия
|
|
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 PushNetworkState; // полный HP-кадр в радио (RX/TX freq+drive)
|
|
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)
|
|
FAtten: Integer;
|
|
FPAMaxPower: Double;
|
|
FRunning: Boolean;
|
|
FTransmitting: Boolean;
|
|
FTuning: Boolean;
|
|
FSplitTxB: Boolean;
|
|
FDisplayDuplex: Boolean;
|
|
FHWPTTActive: Boolean;
|
|
FHWPTTStartedTX: Boolean;
|
|
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;
|
|
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
|
|
// Маршалинг команды в поток контроллера (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;
|
|
|
|
// ====================================================================
|
|
// КОМАНДЫ — единый 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;
|
|
// Дефолты (дублируют 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;
|
|
|
|
destructor TRadioController.Destroy;
|
|
begin
|
|
// Фаза 1: движки освобождает MainForm. Здесь ничего не освобождаем.
|
|
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.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
|
|
if FSplitTxB then Result := FVfoB
|
|
else if FActiveVfo = 0 then Result := FVfoA
|
|
else Result := FVfoB;
|
|
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;
|
|
|
|
// =====================================================================
|
|
// Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
|
|
// =====================================================================
|
|
|
|
procedure TRadioController.SetVfoA(Hz: Double);
|
|
begin FVfoA := Hz; Changed(rfVfoA); { TODO wiring: ApplyVfoA } end;
|
|
|
|
procedure TRadioController.SetVfoB(Hz: Double);
|
|
begin FVfoB := Hz; Changed(rfVfoB); { TODO wiring } end;
|
|
|
|
procedure TRadioController.SetActiveVfo(Idx: Integer);
|
|
begin FActiveVfo := Idx; Changed(rfActiveVfo); { TODO wiring: ActivateVfo } end;
|
|
|
|
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
|
|
begin
|
|
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
|
|
else SetVfoB(FVfoB + DeltaHz);
|
|
end;
|
|
|
|
procedure TRadioController.SetCenter(Hz: Double);
|
|
begin FCenterFreq := Hz; Changed(rfCenterFreq); { TODO wiring } end;
|
|
|
|
procedure TRadioController.VfoSwap;
|
|
var T: Double;
|
|
begin T := FVfoA; FVfoA := FVfoB; FVfoB := T; Changed(rfVfoA); Changed(rfVfoB); { TODO wiring } end;
|
|
|
|
procedure TRadioController.VfoACopyB;
|
|
begin FVfoA := FVfoB; Changed(rfVfoA); { TODO wiring } end;
|
|
|
|
procedure TRadioController.VfoBCopyA;
|
|
begin FVfoB := FVfoA; Changed(rfVfoB); { TODO wiring } 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);
|
|
begin FFilterBW := BWHz; Changed(rfFilterBW); { TODO wiring } 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.SetXvtrBand(Idx: Integer);
|
|
begin FCurrentXvtr := Idx; Changed(rfXvtr); { TODO wiring } 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 := FVfoA;
|
|
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;
|
|
|
|
procedure TRadioController.SetMOX(On_: Boolean);
|
|
begin FTransmitting := On_; Changed(rfTransmitting); { TODO wiring: ApplyMOX } end;
|
|
|
|
procedure TRadioController.SetTune(On_: Boolean);
|
|
begin FTuning := On_; Changed(rfTuning); { TODO wiring: ApplyTUN } 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.
|