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; 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; // ==================================================================== // КОМАНДЫ — единый 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; 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.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; // ===================================================================== // Команды (Фаза 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); 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 (M < 0) or (M > 4) then Exit; FAGCMode := M; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGC(AGCModes[M], 50.0); FBandCache[FCurrentBand].AGCMode := FAGCMode; Changed(rfAGCMode); 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_; 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 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 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 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.