Phase 1: extract TRadioController (state + engine ownership)

Decouple MainForm from CAT/Web by introducing a headless core,
TRadioController, as the single home for radio state and engine objects.
This is groundwork for swappable UIs, a daemon (Web-only) mode, and a
future hardware control panel — each becomes a frontend/adapter over the
same controller API.

RadioController.pas (new):
- Radio state + engine objects (Network/DSP/Audio/Settings/Channels) as
  public fields named exactly as in MainForm, so wiring is mechanical.
- Command API: absolute Set* plus relative *By/Up/Down (for encoders).
- OnStateChanged(field) notifications and GetSnapshot for frontend sync.
- Invoke/OnInvoke (TUIInvokeEvent) thread-marshalling seam (GUI uses
  TThread.Synchronize; daemon supplies its own).
- Commands are scaffolding for now (update field + fire event); engine
  logic stays in MainForm and migrates in later phases (TODO-wiring).

MainForm.pas:
- Owns FController; creates it first in FormCreate, frees it last in
  FormDestroy. Engines are still created/freed by MainForm into the
  controller's fields (ownership move is a later phase).
- ~1400 FXxx references redirected to FController.FXxx (implementation
  only); dead field declarations removed from the interface.

No behavioral change — pure state relocation. Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-04 10:43:01 +03:00
co-authored by Claude Opus 4.8
parent 261b3190b8
commit 63eb2c701f
3 changed files with 1920 additions and 1473 deletions
+1425 -1472
View File
File diff suppressed because it is too large Load Diff
+494
View File
@@ -0,0 +1,494 @@
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;
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);
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;
FDriveLevel: Byte;
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;
// ====================================================================
// КОМАНДЫ — единый 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 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; 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.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 := FDriveLevel; 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;
// =====================================================================
// Команды (Фаза 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; Changed(rfMode); { TODO wiring: ApplyModeFilter } end;
procedure TRadioController.SetFilterIdx(Idx: Integer);
begin FFilter := Idx; Changed(rfFilter); { TODO wiring } end;
procedure TRadioController.SetFilterBW(BWHz: Integer);
begin FFilterBW := BWHz; Changed(rfFilterBW); { TODO wiring } end;
procedure TRadioController.SetAGCMode(M: Integer);
begin FAGCMode := M; Changed(rfAGCMode); { TODO wiring: FDSPEngine.SetAGC } end;
procedure TRadioController.SetAGCTop(DB: Integer);
begin FAGCTop := DB; Changed(rfAGCTop); { TODO wiring } end;
procedure TRadioController.AGCTopBy(Delta: Integer);
begin SetAGCTop(FAGCTop + Delta); end;
procedure TRadioController.SetVolume(V: Integer);
begin FVolume := V; Changed(rfVolume); { TODO wiring: FDSPEngine.SetVolume } end;
procedure TRadioController.VolumeBy(Delta: Integer);
begin SetVolume(FVolume + Delta); end;
procedure TRadioController.SetMute(On_: Boolean);
begin FMuted := On_; Changed(rfMute); { TODO wiring } end;
procedure TRadioController.ToggleMute;
begin SetMute(not FMuted); end;
procedure TRadioController.SetDrive(V: Integer);
begin FDriveLevel := Byte(V); Changed(rfDrive); { TODO wiring } end;
procedure TRadioController.DriveBy(Delta: Integer);
begin SetDrive(FDriveLevel + Delta); end;
procedure TRadioController.SetAtten(Idx: Integer);
begin FAtten := Idx; Changed(rfAtten); { TODO wiring } 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);
begin FSampleRate := Hz; Changed(rfSampleRate); { TODO wiring: RecreateDSPEngine } end;
procedure TRadioController.SetCTun(On_: Boolean);
begin FCTun := On_; Changed(rfCTun); { TODO wiring } 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 FNRMode := Mode; Changed(rfNR); { TODO wiring: FDSPEngine.SetNRMode } end;
procedure TRadioController.SetNB(Mode: Integer);
begin FNBMode := Mode; Changed(rfNB); { TODO wiring } end;
procedure TRadioController.SetSNB(On_: Boolean);
begin FSNB := On_; Changed(rfSNB); { TODO wiring } end;
procedure TRadioController.SetANF(On_: Boolean);
begin FANF := On_; Changed(rfANF); { TODO wiring } 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 FFMStepIdx := Idx; Changed(rfFMStepIdx); { TODO wiring } end;
procedure TRadioController.SetFMCTCSS(On_: Boolean);
begin FFMCTCSSOn := On_; Changed(rfFMCTCSS); { TODO wiring } end;
procedure TRadioController.SetFMCTCSSTone(Idx: Integer);
begin FFMCTCSSToneIdx := Idx; Changed(rfFMCTCSSTone); { TODO wiring } end;
procedure TRadioController.SetFMSquelch(On_: Boolean);
begin FFMSQOn := On_; Changed(rfFMSQ); { TODO wiring } end;
procedure TRadioController.SetFMSquelchLevel(V: Integer);
begin FFMSQLevel := V; Changed(rfFMSQLevel); { TODO wiring } end;
procedure TRadioController.SetFMRpt(Dir: Integer);
begin FFMRptDir := Dir; Changed(rfFMRpt); { TODO wiring } 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.
+1 -1
View File
@@ -18,7 +18,7 @@ uses
Forms, LazOpenGLContext, Forms, LazOpenGLContext,
MainForm, AudioOutput, DeviceForm, FlatButton, FreqDisplay, VfoOverlay, MainForm, AudioOutput, DeviceForm, FlatButton, FreqDisplay, VfoOverlay,
WDSP, WDSPEngine, WebPageHtml, WebServer, WebUtils, WinFirewall, WsClient, WDSP, WDSPEngine, WebPageHtml, WebServer, WebUtils, WinFirewall, WsClient,
Settings; Settings, RadioController;
{$R *.res} {$R *.res}