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, // телеметрия 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; 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; // Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния // (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием. function CalcDriveByte: Byte; // ---- Управление диапазонами (состояние + 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; // Дефолты (дублируют 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 // 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); begin FCenterFreq := Hz; Changed(rfCenterFreq); { TODO wiring } 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); 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.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; 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.