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; FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента) FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb FSendAudioToRadio: Boolean; FAudioOutDevName: string; FAudioInDevName: string; FDevConnected: Boolean; // ---- DSP-фильтры шума / водопад ---- FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag) FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag) FWfAGCEnabled: Boolean; FWfNFEnabled: Boolean; FDitherEnabled: Boolean; FRandomEnabled: Boolean; // ---- FM ---- FFMDeviation: Double; FFMCTCSSOn: Boolean; FFMCTCSSToneIdx: Integer; FFMSQOn: Boolean; FFMSQLevel: Integer; FFMStepOn: Boolean; FFMStepIdx: Integer; FFMRptDir: Integer; FFMRptOffsetHz: Double; FFMRptAutoActive: Boolean; FFMCTCSSAutoActive: Boolean; // ---- Конфиги / устройство (per-MAC) ---- FTXSettings: TTXSettings; FAlexSettings: TAlexSettings; FXvtrSettings: TXvtrSettings; FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; FPABandCal: array[0..CFG_BAND_COUNT-1] of Double; FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; FDevMAC: array[0..5] of Byte; // ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ---- FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double; FLastPLLLock: Boolean; // ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ---- FRXPacketCount: LongWord; // счётчик принятых IQ пакетов FRXStartTime: QWord; // GetTickCount64 момента START FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета FActiveDDC: Integer; // DDC index для текущей платы (0 или 2) FLastDDCSeq: LongWord; // последний seq (из сетевого потока) FLastDDCIndex: Integer; // последний DDC index FDDCLastSeq: array[0..6] of LongWord; FDDCSeqValid: array[0..6] of Boolean; FSeqErrorCount: LongWord; FLastSeqErrorDDC: Integer; FLastSeqErrorDelta: Int64; FSeqOkStreak: Integer; constructor Create; destructor Destroy; override; // Lifecycle движков: создаёт FNetwork/FDSPEngine/FAudioOut/FAudioIn с // заданным sample rate (вызывающий уже определил его из настроек). Колбэки // движков навешивает вызывающий (GUI: методы MainForm; демон: свои). Парный // FreeEngines закрывает и освобождает их; вызывается из Destroy. procedure CreateEngines(ASampleRate: Integer); procedure FreeEngines; // ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ---- // Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine // вызывающим (GUI или демон) после CreateEngines. procedure OnMicPacket(const Data: TMicDataPacket); function PullSoundCardMic(MaxN: Integer): Integer; procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket); // Маршалинг команды в поток контроллера (GUI: TThread.Synchronize). procedure Invoke(M: TThreadMethod); // Снимок всего состояния (для синхронизации нового фронтенда). function GetSnapshot: TRadioSnapshot; // Частотные хелперы — чистые вычисления над состоянием, общие для всех // фронтендов и сетевого слоя (Vfo/Mode/Band/CTun/TX/FM-Rpt). // XvtrTranslate — переводит видимую частоту в частоту IF-тракта с учётом // активного трансвертора (LOOffset/LOError). // ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг). function XvtrTranslate(VisibleHz: Double): Double; function ActiveTXFreqHz: Double; // Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр, // FM-девиация, squelch). Вызывается командами и UI-путями смены режима. procedure ApplyModeFilter; // Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния // (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием. function CalcDriveByte: Byte; // Источник микрофона по умолчанию для MOX-кнопки/Web-TX: звуковая карта // если настроена, иначе HW mic-стрим трансивера. HW PTT → txmsRadio в SetMOX. function DefaultMicSource: TTXMicSource; // ---- Управление диапазонами (состояние + DSP; рендер через события) ---- // MakeBandSettings — снимок текущего состояния в запись диапазона. // SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот). // RestoreBand — загружает состояние диапазона, применяет к DSP, шлёт события // рендера. Финальную перестройку VFO A делает вызывающий (UI: ApplyVfoA). function MakeBandSettings: TBandSettings; procedure SaveCurrentBand; procedure RestoreBand(BandIdx: Integer); // Применяет DSP-настройки диапазона (mode/filter/AGC/CTUN/FM) + шлёт события // рендера. НЕ трогает частоты VFO/центр — это переиспользуется как RestoreBand // (с загрузкой VFO), так и SetActiveVfo (bandstack: смена бэнда без сброса VFO). procedure ApplyBandDSP(const B: TBandSettings); // Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы. function FilterBWFor(M, Idx: Integer): Integer; // ---- XVTR (трансвертер) ---- // ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть. // ActivateXvtr — вход в слот: state+DSP (через ApplyBandDSP) + VFO/центр + сеть; // рендер через rfXvtr/rfBand(XVTR-aware)/rfActiveVfo. UI: wideband+web. // DeactivateXvtr — сохраняет слот (SaveCurrentBand) и выходит; HF-возврат // (RestoreBand + финальная перестройка VFO) делает UI-обёртка. procedure ApplyXvtrToNetwork; procedure ActivateXvtr(Idx: Integer); procedure DeactivateXvtr; // Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX). // Вызывается командами смены частоты/центра и UI после правки drive. procedure PushNetworkState; // ==================================================================== // КОМАНДЫ — единый API для GUI / Web / CAT / Arduino. // Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring. // ==================================================================== // Частоты / VFO procedure SetVfoA(Hz: Double); procedure SetVfoB(Hz: Double); procedure SetActiveVfo(Idx: Integer); procedure TuneActiveBy(DeltaHz: Double); // энкодер VFO procedure SetCenter(Hz: Double); procedure VfoSwap; procedure VfoACopyB; procedure VfoBCopyA; // Режим / фильтр / AGC procedure SetMode(M: Integer); procedure SetFilterIdx(Idx: Integer); procedure SetFilterBW(BWHz: Integer); procedure SetAGCMode(M: Integer); procedure SetAGCTop(DB: Integer); procedure AGCTopBy(Delta: Integer); // Громкость / mute / drive / аттенюатор procedure SetVolume(V: Integer); procedure VolumeBy(Delta: Integer); procedure SetMute(On_: Boolean); procedure ToggleMute; procedure SetDrive(V: Integer); procedure DriveBy(Delta: Integer); procedure SetAtten(Idx: Integer); // Диапазоны / трансвертеры / спан procedure SetBand(Idx: Integer); procedure BandUp; procedure BandDown; procedure SetXvtrBand(Idx: Integer); // -1 = выход в HF procedure SetSpan(Hz: Integer); procedure SetSampleRate(Hz: Integer); // Отображение / приём procedure SetCTun(On_: Boolean); procedure SetDuplex(On_: Boolean); procedure SetWfAGC(On_: Boolean); procedure SetWfNF(On_: Boolean); // DSP-фильтры шума procedure SetNR(Mode: Integer); procedure SetNB(Mode: Integer); procedure SetSNB(On_: Boolean); procedure SetANF(On_: Boolean); // Передача procedure SetMOX(On_: Boolean); procedure SetTune(On_: Boolean); // TUN // FM procedure SetFMStep(Idx: Integer); procedure SetFMCTCSS(On_: Boolean); procedure SetFMCTCSSTone(Idx: Integer); procedure SetFMSquelch(On_: Boolean); procedure SetFMSquelchLevel(V: Integer); procedure SetFMRpt(Dir: Integer); // явное направление; снимает авто-флаг procedure SetFMRptOffset(Hz: Double); // сдвиг репитера; применяет, если RPT вкл procedure UpdateRptAutoState; // авто-MINUS для 2 м под FVfoA // Каналы procedure ApplyChannel(Idx: Integer); // Жизненный цикл движка / устройства procedure StartStop; procedure SetRun(On_: Boolean); procedure ConnectDevice(const Dev: THPSDRDevice); procedure Discover; // ---- События наружу ---- property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke; property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged; end; implementation constructor TRadioController.Create; begin inherited Create; FSettings := TSettingsManager.Create; // владелец настроек (GUI и демон) // Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны). FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0; FMode := 1; FFilter := 5; FFilterBW := 2700; FAGCMode := 1; FAGCTop := 90; FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0; FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000; FCurrentBand := 5; FCurrentXvtr := -1; FPAMaxPower := 100.0; FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True; FFMStepOn := True; FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FillChar(FDevMAC, SizeOf(FDevMAC), 0); end; procedure TRadioController.CreateEngines(ASampleRate: Integer); // Создаёт движки в полях контроллера. Колбэки (OnAudio/OnSpectrum/OnHPStatus…) // навешивает вызывающий после возврата — они указывают на UI/демон. begin FNetwork := THPSDRNetwork.Create; // BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший // буфер вдвое сокращает построение downsampler в OpenChannel RXA. FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); // Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа // формы / при подключении устройства). FAudioOut := TAudioOutput.Create(48000); FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize; FAudioIn := TAudioInput.Create(48000); end; procedure TRadioController.FreeEngines; begin if Assigned(FNetwork) then begin if FNetwork.Connected then FNetwork.Disconnect; FreeAndNil(FNetwork); end; if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end; if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end; if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end; end; destructor TRadioController.Destroy; begin FreeEngines; FreeAndNil(FSettings); inherited Destroy; end; procedure TRadioController.Changed(Field: TRadioField); begin if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field); end; procedure TRadioController.PushNetworkState; // Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние // MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг. begin if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; procedure TRadioController.OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket); // Сетевой поток: трекинг seq + подача IQ активного DDC в WDSP. UI не трогаем — // статус рисует таймер по полям ниже. Backend-агностичен (Pluto синтезирует // тот же TDDCIQPacket). var SamplesPerFrame: Integer; Seq, ExpectedSeq: LongWord; Delta: Int64; begin Inc(FRXPacketCount); FRXLastPktTime := GetTickCount64; Seq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16) or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]); if (DDCIndex >= Low(FDDCLastSeq)) and (DDCIndex <= High(FDDCLastSeq)) then begin if Seq <> 0 then begin ExpectedSeq := FDDCLastSeq[DDCIndex] + 1; if FDDCSeqValid[DDCIndex] and (Seq <> ExpectedSeq) then begin Inc(FSeqErrorCount); FLastSeqErrorDDC := DDCIndex; Delta := Int64(Seq) - Int64(ExpectedSeq); FLastSeqErrorDelta := Delta; FSeqOkStreak := 0; end else if FDDCSeqValid[DDCIndex] then begin if FSeqErrorCount > 0 then begin Inc(FSeqOkStreak); if FSeqOkStreak >= 20 then begin FSeqErrorCount := 0; FLastSeqErrorDDC := -1; FLastSeqErrorDelta := 0; FSeqOkStreak := 0; end; end; end; end; FDDCLastSeq[DDCIndex] := Seq; FDDCSeqValid[DDCIndex] := True; end; // Подаём IQ в DSP — только с активного DDC if (DDCIndex = FActiveDDC) and FWDSPReady then begin SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or Integer(Data.SamplesPerFrame[1]); if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame); end; // Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path) FLastDDCSeq := Seq; FLastDDCIndex := DDCIndex; end; procedure TRadioController.OnMicPacket(const Data: TMicDataPacket); // HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы // игнорируются (mic берётся со звуковой карты). begin if FTransmitting and FWDSPReady and (FDSPEngine.TXMicSource = txmsRadio) then FDSPEngine.PushTXMicSamples16(Data.Samples, 64); end; function TRadioController.PullSoundCardMic(MaxN: Integer): Integer; // Колбэк TTXDSPThread при TXMicSource=txmsSoundCard. Тянем только реально // доступные сэмплы из FAudioIn (иначе пушили бы тишину в mic-цепь WDSP), // пачкой в локальный буфер — экономим будилки семафора. var i, n: Integer; Buf: array[0..2047] of Double; begin Result := 0; if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit; n := FAudioIn.Available; if n > MaxN then n := MaxN; if n > Length(Buf) then n := Length(Buf); for i := 0 to n - 1 do Buf[i] := FAudioIn.ReadSample; if n > 0 then FDSPEngine.PushTXMicSamplesD(Buf, n); Result := n; end; procedure TRadioController.Invoke(M: TThreadMethod); begin if Assigned(FOnInvoke) then FOnInvoke(M) else if Assigned(M) then M(); // нет хозяина-маршалера — прямой вызов end; function TRadioController.GetSnapshot: TRadioSnapshot; begin FillChar(Result, SizeOf(Result), 0); Result.VfoA := FVfoA; Result.VfoB := FVfoB; Result.CenterFreq := FCenterFreq; Result.SpanHz := FSpanHz; Result.SampleRate := FSampleRate; Result.ActiveVfo := FActiveVfo; Result.Mode := FMode; Result.Filter := FFilter; Result.FilterBW := FFilterBW; Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop; Result.Volume := FVolume; Result.Drive := FDrivePercent; Result.Atten := FAtten; Result.Band := FCurrentBand; Result.Xvtr := FCurrentXvtr; Result.Muted := FMuted; Result.CTun := FCTun; Result.Duplex := FDisplayDuplex; Result.Running := FRunning; Result.Connected := FDevConnected; Result.Transmitting := FTransmitting; Result.Tuning := FTuning; Result.NRMode := FNRMode; Result.NBMode := FNBMode; Result.SNB := FSNB; Result.ANF := FANF; Result.WfAGC := FWfAGCEnabled; Result.WfNF := FWfNFEnabled; Result.FMStepOn := FFMStepOn; Result.FMStepIdx := FFMStepIdx; Result.FMCTCSSOn := FFMCTCSSOn; Result.FMCTCSSToneIdx := FFMCTCSSToneIdx; Result.FMSQOn := FFMSQOn; Result.FMSQLevel := FFMSQLevel; Result.FMRptDir := FFMRptDir; Result.SMeter := FLastSMeter; Result.FwdW := FLastFwdW; Result.SWR := FLastSWR; Result.SupplyV := FLastSupplyV; Result.SupplyA := FLastSupplyA; Result.PLLLock := FLastPLLLock; end; function TRadioController.XvtrTranslate(VisibleHz: Double): Double; begin Result := VisibleHz; if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit; if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit; // f_IF = f_visible - LOOffset + LOError Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].LOOffset + FXvtrSettings.Entries[FCurrentXvtr].LOError; end; function TRadioController.ActiveTXFreqHz: Double; begin // TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B), // независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал // за активным RX (TX-A горит, но передача шла на B при активном B). if FSplitTxB then Result := FVfoB else Result := FVfoA; if FMode = MODE_FM then case FFMRptDir of RPT_MINUS: Result := Result - FFMRptOffsetHz; RPT_PLUS: Result := Result + FFMRptOffsetHz; end; end; procedure TRadioController.ApplyModeFilter; var Lo, Hi, Half: Integer; begin Half := FFilterBW div 2; case FMode of 0: begin Lo := -FFilterBW; Hi := -100; end; // LSB 1: begin Lo := 100; Hi := FFilterBW; end; // USB else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM end; if FWDSPReady and Assigned(FDSPEngine) then begin FDSPEngine.SetFilter(Lo, Hi); FDSPEngine.SetTXFilter(Lo, Hi); if FMode = MODE_FM then begin FDSPEngine.SetRXFMDeviation(FFMDeviation); FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut, FTXSettings.FMHighCut, FTXSettings.FMEmphPosition); FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel); end else FDSPEngine.SetFMSquelch(False, 0); end; end; // Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode. // Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была // корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт. procedure TRadioController.ApplyModeDefaults; begin case FMode of MODE_FM: begin if FFilter > 1 then FFilter := FILT_FM_DEF; FFilterBW := FILT_FM_BW[FFilter]; FFMDeviation := FILT_FM_DEV[FFilter]; end; 0, 1: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end; 2: begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end; 3, 4: begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end; else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end; end; end; function TRadioController.FilterBWFor(M, Idx: Integer): Integer; // Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons, // но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore. begin case M of MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, 1)]; 0, 1: Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; 2: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; 3, 4: Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; end; end; // ===================================================================== // Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm. // ===================================================================== function TRadioController.CalcDriveByte: Byte; // Thetis-совместимая формула: // drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 ) // target_volts = sqrt( 10^(target_dBm/10) * 0.05 ) // target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower var Cal: Double; Pos: Integer; XvtrActive: Boolean; pwr_W: Double; target_dBm: Double; target_volts: Double; audio_vol: Double; begin XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].Enabled; if XvtrActive then Cal := FVHFBandCal[FCurrentXvtr] else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then Cal := FPABandCal[FCurrentBand] else Cal := 38.8; if FTuning then begin if XvtrActive and FXvtrSettings.UseXVTRTunePower then Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100) else Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100); end else begin if XvtrActive then Pos := Round(FDrivePercent * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0) else Pos := FDrivePercent; end; if Pos <= 0 then begin Result := 0; Exit; end; pwr_W := Pos / 100.0 * FPAMaxPower; target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal; target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05); audio_vol := Min(target_volts / 0.8, 1.0); Result := Min(Round(audio_vol * 1.02 * 255.0), 255); end; procedure TRadioController.SanitizeFilterForMode; // Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и // синхронизируем FFilterBW/FFMDeviation. Чистая (state) часть UpdateFilterButtons. begin if FMode = MODE_FM then begin if FFilter > 1 then FFilter := FILT_FM_DEF; FFilterBW := FILT_FM_BW[FFilter]; FFMDeviation := FILT_FM_DEV[FFilter]; end; end; function TRadioController.MakeBandSettings: TBandSettings; begin Result.VfoA := FVfoA; Result.VfoB := FVfoB; Result.Mode := FMode; Result.FilterIdx := FFilter; Result.FilterBW := FFilterBW; Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop; Result.CTun := FCTun; Result.SpanHz := FSpanHz; Result.FMSQOn := FFMSQOn; Result.FMSQLevel := FFMSQLevel; Result.CTCSSOn := FFMCTCSSOn; Result.CTCSSToneIdx := FFMCTCSSToneIdx; Result.FMStepOn := FFMStepOn; Result.FMStepIdx := FFMStepIdx; Result.FMRptDir := FFMRptDir; Result.FMRptOffsetHz := FFMRptOffsetHz; // Waterfall AGC/NF — глобальные (не диапазонные) end; procedure TRadioController.SaveCurrentBand; // Сохраняет текущее состояние диапазона. В XVTR-режиме пишем в XVTR-слот // (HF band-cache не трогаем — там сохранён HF-диапазон до активации XVTR). var i: Integer; B: TBandSettings; begin if not FDevConnected then Exit; if FCurrentXvtr >= 0 then begin if (FCurrentXvtr < CFG_XVTR_COUNT) then begin i := FCurrentXvtr; FXvtrSettings.Entries[i].LastFreq := FVfoA; FXvtrSettings.Entries[i].LastFreqB := FVfoB; FXvtrSettings.Entries[i].LastMode := FMode; FXvtrSettings.Entries[i].LastFilterIdx := FFilter; FXvtrSettings.Entries[i].LastFMSQOn := FFMSQOn; FXvtrSettings.Entries[i].LastFMSQLevel := FFMSQLevel; FXvtrSettings.Entries[i].LastCTCSSOn := FFMCTCSSOn; FXvtrSettings.Entries[i].LastCTCSSToneIdx := FFMCTCSSToneIdx; FXvtrSettings.Entries[i].LastFMStepOn := FFMStepOn; FXvtrSettings.Entries[i].LastFMStepIdx := FFMStepIdx; FXvtrSettings.Entries[i].LastFMRptDir := FFMRptDir; FXvtrSettings.Entries[i].LastFMRptOffsetHz := FFMRptOffsetHz; FXvtrSettings.Entries[i].LastCTun := FCTun; FXvtrSettings.Entries[i].LastAGCMode := FAGCMode; FXvtrSettings.Entries[i].LastAGCTop := FAGCTop; FXvtrSettings.Entries[i].LastCTCSSAutoActive := FFMCTCSSAutoActive; FXvtrSettings.Entries[i].LastFMRptAutoActive := FFMRptAutoActive; end; FSettings.SaveXvtr(FDevMAC, FXvtrSettings); Exit; end; // Не сохраняем, если VFO ушёл в VHF/UHF (например, после канала на 2м без // XVTR) — иначе HF-кэш диапазона перезапишется VHF-частотой. if FVfoA > 61000000 then Exit; B := MakeBandSettings; // Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш // ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая // частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда. if FreqToBandIdx(FVfoA) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA; if FreqToBandIdx(FVfoB) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB; FBandCache[FCurrentBand] := B; FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]); end; procedure TRadioController.ApplyBandDSP(const B: TBandSettings); // DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает. begin // --- Режим --- FMode := B.Mode; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode); // --- Фильтр (санитизация FM до применения к DSP — как в оригинале) --- FFilter := B.FilterIdx; FFilterBW := B.FilterBW; SanitizeFilterForMode; ApplyModeFilter; // --- AGC (единый маппинг — исправляет каст-баг RestoreBand) --- FAGCMode := B.AGCMode; FAGCTop := B.AGCTop; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop); ApplyAGCToEngine; // --- FM squelch / CTCSS / step / repeater --- FFMSQOn := B.FMSQOn; FFMSQLevel := B.FMSQLevel; FFMCTCSSOn := B.CTCSSOn; FFMCTCSSToneIdx := B.CTCSSToneIdx; if FWDSPReady and Assigned(FDSPEngine) then begin FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]); FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel); end; FFMStepOn := B.FMStepOn; FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3); FFMRptDir := B.FMRptDir; FFMRptOffsetHz := B.FMRptOffsetHz; if FFMRptOffsetHz <= 0 then FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA); // --- CTUN --- FCTun := B.CTun; if FWDSPReady and Assigned(FDSPEngine) and not FCTun then FDSPEngine.SetShift(0.0); // --- Span (sample rate глобальный, не per-band — лишь ресинк) --- FSpanHz := FSampleRate; // --- Рендер: единичные события (UI рисует через OnControllerState) --- Changed(rfBand); Changed(rfMode); // rfMode-рендер зовёт UpdateFilterButtons (FM-панели) Changed(rfFilter); Changed(rfAGCMode); Changed(rfAGCTop); Changed(rfFMSQ); Changed(rfFMSQLevel); Changed(rfFMCTCSS); Changed(rfFMCTCSSTone); Changed(rfFMStep); // кнопка вкл/выкл STEP Changed(rfFMStepIdx); // дропдаун шага + web + grid Changed(rfFMRpt); Changed(rfCTun); end; procedure TRadioController.RestoreBand(BandIdx: Integer); // Загружает состояние диапазона из кэша, применяет к DSP и шлёт события рендера. // Финальную перестройку VFO A (с канальной оркестрацией) делает вызывающий // UI-путь через ApplyVfoA(FVfoA); здесь FVfoA лишь выставляется. var B: TBandSettings; begin if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit; B := FBandCache[BandIdx]; FCurrentBand := BandIdx; ApplyBandDSP(B); // --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) --- FCenterFreq := B.VfoA; FVfoB := B.VfoB; FVfoA := B.VfoA; FDriveLevel := CalcDriveByte; Changed(rfVfoB); // rfVfoA НЕ шлём: финальную перестройку делает ApplyVfoA в UI-обёртке. end; function TRadioController.ActiveVfoHz: Double; begin if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; end; procedure TRadioController.ApplyTuneCore(VfoHz: Double); // Общее ядро перестройки приёмника на частоту АКТИВНОГО VFO (A или B). // CTUN OFF: DDC = VfoHz, сдвиг 0. // CTUN ON: DDC фиксирован, демодулятор сдвигаем на VfoHz-FCenterFreq; // если полоса фильтра вышла за край дисплея — прокручиваем центр. // Ставит FCenterFreq / shift / FLastVfoScrolled. var Offset, FiltLo, FiltHi, HalfSpan: Double; begin FLastVfoScrolled := False; if not FCTun then begin FCenterFreq := VfoHz; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); end else begin Offset := VfoHz - FCenterFreq; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); HalfSpan := FSpanHz / 2; case FMode of 0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; 1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end; end; if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then begin if FiltHi > HalfSpan then FCenterFreq := VfoHz - HalfSpan * 0.5 else FCenterFreq := VfoHz + HalfSpan * 0.5; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(VfoHz - FCenterFreq); FLastVfoScrolled := True; end; end; end; procedure TRadioController.SetVfoA(Hz: Double); // Перестройка VFO A: XVTR-клэмп, CTUN ядро, детект диапазона, FM авто-RPT, // проталкивание в сеть. Рендер — в UI через OnControllerState(rfVfoA/rfBand). // Канальную оркестрацию (авто-CTCSS, pre-channel drive) делает UI-путь. var NewFreq: Int64; BandIdx: Integer; begin NewFreq := Round(Hz); // Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].Enabled then begin if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin); if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd); end; FVfoA := NewFreq; ApplyTuneCore(FVfoA); // Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band, // FreqToBandIdx вернёт -1 для VHF/UHF). if FCurrentXvtr < 0 then begin BandIdx := FreqToBandIdx(FVfoA); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin FCurrentBand := BandIdx; FDriveLevel := CalcDriveByte; Changed(rfBand); end; end; // Авто-MINUS в 2 м репитерном сегменте (само снимает при уходе из сегмента). if FMode = MODE_FM then UpdateRptAutoState; PushNetworkState; Changed(rfVfoA); end; procedure TRadioController.SetVfoB(Hz: Double); // VFO B всегда сохраняется. Если B активен — перестраиваем приёмник через // ApplyTuneCore (с учётом CTUN), детектим диапазон и толкаем в сеть. Если B // неактивен (split, слушаем A) — только сохраняем частоту (для TX). // Рендер FreqDispB/спектра — в UI через OnControllerState(rfVfoB). var BandIdx: Integer; NewFreq: Int64; begin NewFreq := Round(Hz); // Клэмп в пределах XVTR-band (как в SetVfoA) — VFO B тоже не уходит за край. if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].Enabled then begin if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin); if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd); end; FVfoB := NewFreq; if FActiveVfo = 1 then begin ApplyTuneCore(FVfoB); if FCurrentXvtr < 0 then begin BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin FCurrentBand := BandIdx; FDriveLevel := CalcDriveByte; Changed(rfBand); end; end; PushNetworkState; end; Changed(rfVfoB); end; procedure TRadioController.SetActiveVfo(Idx: Integer); // Переключение активного (RX) VFO с перестройкой приёмника на его частоту. // Bandstack: если активируемый VFO стоит на ДРУГОМ ham-бэнде — сохраняем // покидаемый бэнд и восстанавливаем DSP-настройки нового (mode/filter/AGC/CTUN/ // FM). Частоты обоих VFO при этом НЕ трогаем — каждый VFO несёт свой диапазон. // CTUN (того же бэнда): если частота нового VFO видна в окне — только shift // (DDC/сеть не трогаем); иначе CTUN сбрасывается и центрируемся на частоте. // Рендер (дисплеи/спектр/band/CTUN-кнопка) — в UI через OnControllerState. var ActiveFreq: Double; HalfSpan, Offset: Double; FreqVisible, NeedPush: Boolean; NewBand: Integer; begin FActiveVfo := Idx; ActiveFreq := ActiveVfoHz; NeedPush := False; // --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд --- if FCurrentXvtr < 0 then begin NewBand := FreqToBandIdx(ActiveFreq); if (NewBand >= 0) and (NewBand <> FCurrentBand) then begin SaveCurrentBand; // сохранить покидаемый бэнд FCurrentBand := NewBand; ApplyBandDSP(FBandCache[NewBand]); // mode/filter/AGC/CTUN/FM + рендер FCenterFreq := ActiveFreq; // центр на активную (CTUN центрируется тут) FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда NeedPush := True; // DDC-центр сменился → толкнуть в сеть end; end; if FCTun then begin HalfSpan := FSpanHz / 2; Offset := ActiveFreq - FCenterFreq; FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan); if FreqVisible then begin // Частота видна — просто сдвигаем демодулятор. if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); end else begin // Частота за окном — сбрасываем CTUN и центрируемся на ней. FCTun := False; FBandCache[FCurrentBand].CTun := False; FCenterFreq := ActiveFreq; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); NeedPush := True; Changed(rfCTun); end; end else begin FCenterFreq := ActiveFreq; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); NeedPush := True; end; if NeedPush then PushNetworkState; Changed(rfActiveVfo); end; procedure TRadioController.TuneActiveBy(DeltaHz: Double); begin if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz) else SetVfoB(FVfoB + DeltaHz); end; procedure TRadioController.SetCenter(Hz: Double); // CTUN-скролл окна просмотра: двигаем DDC-центр БЕЗ смены VFO. Демодулятор // сдвигаем = активный VFO − новый центр; полный HP-кадр в радио. Рендер // (ruler/спектр) — в UI через OnControllerState(rfCenterFreq). // Точки входа: десктопный CTUN-drag и web (SyncWebCenter). begin FCenterFreq := Hz; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq); PushNetworkState; Changed(rfCenterFreq); end; procedure TRadioController.VfoSwap; // Меняем VFO A/B местами и перестраиваем приёмник на новую частоту активного // VFO (центр=частота, shift 0 — CTUN-оффсет после свопа теряет смысл). var T: Double; ActiveFreq: Double; BandIdx: Integer; begin T := FVfoA; FVfoA := FVfoB; FVfoB := T; ActiveFreq := ActiveVfoHz; FCenterFreq := ActiveFreq; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); BandIdx := FreqToBandIdx(ActiveFreq); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin FCurrentBand := BandIdx; Changed(rfBand); end; PushNetworkState; Changed(rfVfoA); Changed(rfVfoB); end; procedure TRadioController.VfoACopyB; // A>B: частота A копируется в B begin SetVfoB(FVfoA); // сохраняет B:=A, перестраивает если B активен, рендер rfVfoB end; procedure TRadioController.VfoBCopyA; // B>A: частота B копируется в A begin FVfoA := FVfoB; if FActiveVfo = 0 then SetVfoA(FVfoA) // A активен — перестраиваем приёмник else Changed(rfVfoA); // A неактивен — только рендер дисплея end; procedure TRadioController.SetMode(M: Integer); begin FMode := M; ApplyModeDefaults; // дефолтный фильтр/девиация под режим FBandCache[FCurrentBand].Mode := FMode; if FWDSPReady and Assigned(FDSPEngine) then begin FDSPEngine.SetMode(FMode); // Если TUN активен — пересчитываем знак частоты под новый режим if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0); end; ApplyModeFilter; Changed(rfMode); end; procedure TRadioController.SetFilterIdx(Idx: Integer); begin FFilter := Idx; case FMode of 0, 1: FFilterBW := FILT_SSB_BW[Idx]; 2: FFilterBW := FILT_DSB_BW[Idx]; 3, 4: FFilterBW := FILT_CW_BW[Idx]; MODE_FM: begin FFilterBW := FILT_FM_BW[Idx]; FFMDeviation := FILT_FM_DEV[Idx]; end; else FFilterBW := FILT_AM_BW[Idx]; end; FBandCache[FCurrentBand].FilterBW := FFilterBW; ApplyModeFilter; Changed(rfFilter); end; procedure TRadioController.SetFilterBW(BWHz: Integer); // Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а // FFilter подбираем ближайшим пресетом — только для подсветки кнопки. var BW_arr: array[0..FILT_COUNT-1] of Integer; best, dist, i: Integer; begin FFilterBW := BWHz; for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt; case FMode of 0, 1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i]; 2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i]; 3, 4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i]; MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i]; else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i]; end; best := 0; dist := MaxInt; for i := 0 to FILT_COUNT-1 do if Abs(BW_arr[i] - BWHz) < dist then begin dist := Abs(BW_arr[i] - BWHz); best := i; end; FFilter := best; if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best]; FBandCache[FCurrentBand].FilterBW := FFilterBW; ApplyModeFilter; Changed(rfFilter); end; procedure TRadioController.ApplyAGCToEngine; const // UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для // всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum). AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff); begin if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then FDSPEngine.SetAGC(AGCModes[FAGCMode], 50.0); end; procedure TRadioController.SetAGCMode(M: Integer); begin if (M < 0) or (M > 4) then Exit; FAGCMode := M; ApplyAGCToEngine; FBandCache[FCurrentBand].AGCMode := FAGCMode; Changed(rfAGCMode); end; procedure TRadioController.SetAGCTop(DB: Integer); begin if DB < 20 then DB := 20 else if DB > 120 then DB := 120; FAGCTop := DB; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop); ApplyAGCToEngine; // ре-применяем режим с новым уровнем FBandCache[FCurrentBand].AGCTop := FAGCTop; Changed(rfAGCTop); end; procedure TRadioController.AGCTopBy(Delta: Integer); begin SetAGCTop(FAGCTop + Delta); end; procedure TRadioController.SetVolume(V: Integer); begin if V < 0 then V := 0 else if V > 100 then V := 100; FVolume := V; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(FVolume / 100.0); Changed(rfVolume); end; procedure TRadioController.VolumeBy(Delta: Integer); begin SetVolume(FVolume + Delta); end; procedure TRadioController.SetMute(On_: Boolean); begin FMuted := On_; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMute(FMuted); Changed(rfMute); end; procedure TRadioController.ToggleMute; begin SetMute(not FMuted); end; procedure TRadioController.SetDrive(V: Integer); begin FDrivePercent := V; Changed(rfDrive); { TODO wiring: CalcDriveByte + WDSP } end; procedure TRadioController.DriveBy(Delta: Integer); begin SetDrive(FDrivePercent + Delta); end; procedure TRadioController.SetAtten(Idx: Integer); begin if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2; FAtten := Idx; if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10); Changed(rfAtten); end; procedure TRadioController.SetBand(Idx: Integer); begin FCurrentBand := Idx; Changed(rfBand); { TODO wiring: RestoreBand } end; procedure TRadioController.BandUp; begin if FCurrentBand < CFG_BAND_COUNT - 1 then SetBand(FCurrentBand + 1); end; procedure TRadioController.BandDown; begin if FCurrentBand > 0 then SetBand(FCurrentBand - 1); end; procedure TRadioController.ApplyXvtrToNetwork; // Передаёт XVTR-режим в сеть (enable bit + T/R relay suppression + RX ant). var En, DisablePA: Boolean; RxAnt: Byte; begin if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].Enabled then begin En := True; DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA; RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna; end else begin En := False; DisablePA := False; RxAnt := 0; end; if Assigned(FNetwork) and FNetwork.Connected then FNetwork.SetXvtrMode(En, DisablePA, RxAnt); end; procedure TRadioController.ActivateXvtr(Idx: Integer); // Вход в XVTR-слот: state+DSP через общий ApplyBandDSP + VFO/центр/сеть. // Рендер: rfXvtr (XVTR-кнопки), rfBand (XVTR-aware гасит HF), rfActiveVfo // (дисплеи A/B + полный перерисов спектра). UI-обёртка добавляет wideband+web. var E: TXvtrEntry; B: TBandSettings; Vis: Double; begin if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; if not FXvtrSettings.Entries[Idx].Enabled then Exit; FCurrentXvtr := Idx; E := FXvtrSettings.Entries[Idx]; // Начальная частота: LastFreq если в диапазоне, иначе середина. Vis := E.LastFreq; if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then Vis := (E.FreqBegin + E.FreqEnd) / 2.0; ApplyXvtrToNetwork; // XVTR enable bit + DisablePA + RX ant // Авто-флаги канала (не входят в TBandSettings/ApplyBandDSP). FFMCTCSSAutoActive := E.LastCTCSSAutoActive; FFMRptAutoActive := E.LastFMRptAutoActive; // DSP-настройки слота — через общий путь Band (mode/filter/AGC/CTUN/FM + рендер). B.Mode := E.LastMode; B.FilterIdx := E.LastFilterIdx; B.FilterBW := FilterBWFor(E.LastMode, E.LastFilterIdx); B.AGCMode := E.LastAGCMode; B.AGCTop := E.LastAGCTop; B.CTun := E.LastCTun; B.SpanHz := FSampleRate; B.FMSQOn := E.LastFMSQOn; B.FMSQLevel := E.LastFMSQLevel; B.CTCSSOn := E.LastCTCSSOn; B.CTCSSToneIdx := E.LastCTCSSToneIdx; B.FMStepOn := E.LastFMStepOn; B.FMStepIdx := E.LastFMStepIdx; B.FMRptDir := E.LastFMRptDir; B.FMRptOffsetHz := E.LastFMRptOffsetHz; B.VfoA := Vis; B.VfoB := FVfoB; ApplyBandDSP(B); // VFO: B — своя сохранённая частота слота (клэмп в диапазон); A = visible. FVfoB := EnsureRange(E.LastFreqB, E.FreqBegin, E.FreqEnd); FVfoA := Vis; FCenterFreq := Vis; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); FDriveLevel := CalcDriveByte; PushNetworkState; Changed(rfXvtr); Changed(rfActiveVfo); end; procedure TRadioController.DeactivateXvtr; // Сохраняет текущий XVTR-слот и выходит в HF. Сам HF-возврат (RestoreBand + // финальная перестройка активного VFO) делает UI-обёртка — там же ResetWfAvgBuf. begin if FCurrentXvtr < 0 then Exit; SaveCurrentBand; // XVTR-ветка: пишет весь слот + SaveXvtr if FDevConnected then FSettings.Save; FCurrentXvtr := -1; ApplyXvtrToNetwork; // снять XVTR enable bit Changed(rfXvtr); // погасить XVTR-кнопки end; procedure TRadioController.SetXvtrBand(Idx: Integer); // Единая точка для GUI/Web/CAT: Idx>=0 — войти в слот, Idx<0 — выйти в HF. // ВНИМАНИЕ: HF-возврат при выходе делает UI-обёртка DeactivateXvtr (RestoreBand). begin if Idx >= 0 then ActivateXvtr(Idx) else DeactivateXvtr; end; procedure TRadioController.SetSpan(Hz: Integer); begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end; procedure TRadioController.SetSampleRate(Hz: Integer); // Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate. // Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в // трансивер и полное восстановление DSP-состояния. Сохранение в global // settings + рендер overlay/AGC-линий остаются в UI (OnControllerState). begin if (Hz <= 0) or (Hz = FSampleRate) then Exit; FSampleRate := Hz; FSpanHz := Hz; FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости // 1. Останавливаем WDSP и меняем rate (Close + FlushQueue + Open). if FWDSPReady and Assigned(FDSPEngine) then begin FWDSPReady := False; FDSPEngine.ChangeSampleRate(Hz); end; // 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate). if Assigned(FNetwork) and FNetwork.Connected then FNetwork.ConfigureDDCs(1, Hz div 1000, 0, FDitherEnabled, FRandomEnabled); // 3. Восстанавливаем DSP-состояние (ChangeSampleRate пересоздал канал). if Assigned(FDSPEngine) then begin FWDSPReady := FDSPEngine.Initialized; if FWDSPReady then begin FDSPEngine.SetMode(FMode); FDSPEngine.SetVolume(FVolume / 100.0); ApplyModeFilter; FDSPEngine.SetNRMode(FNRMode); FDSPEngine.SetNBMode(FNBMode); FDSPEngine.SetSNB(FSNB); FDSPEngine.SetANF(FANF); FDSPEngine.SetAGCTop(FAGCTop); ApplyAGCToEngine; // единый маппинг (раньше здесь был ошибочный каст индекса) if Assigned(FNetwork) and FNetwork.Connected then FDSPEngine.SetShift(FVfoA - FCenterFreq); end; end; Changed(rfSampleRate); end; procedure TRadioController.SetCTun(On_: Boolean); begin FCTun := On_; FBandCache[FCurrentBand].CTun := FCTun; if not FCTun then begin // Выключили CTUN: центрируемся на АКТИВНОМ VFO, shift=0. (Включение CTUN не // трогает движок — FCenterFreq остаётся как есть, спектр не прыгает.) FCenterFreq := ActiveVfoHz; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); PushNetworkState; end; Changed(rfCTun); end; procedure TRadioController.SetDuplex(On_: Boolean); begin FDisplayDuplex := On_; Changed(rfDuplex); { TODO wiring } end; procedure TRadioController.SetWfAGC(On_: Boolean); begin FWfAGCEnabled := On_; Changed(rfWfAGC); end; procedure TRadioController.SetWfNF(On_: Boolean); begin FWfNFEnabled := On_; Changed(rfWfNF); end; procedure TRadioController.SetNR(Mode: Integer); begin if Mode < 0 then Mode := 0 else if Mode > 4 then Mode := 4; FNRMode := Mode; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNRMode(FNRMode); Changed(rfNR); end; procedure TRadioController.SetNB(Mode: Integer); begin if Mode < 0 then Mode := 0 else if Mode > 2 then Mode := 2; FNBMode := Mode; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNBMode(FNBMode); Changed(rfNB); end; procedure TRadioController.SetSNB(On_: Boolean); begin FSNB := On_; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetSNB(FSNB); Changed(rfSNB); end; procedure TRadioController.SetANF(On_: Boolean); begin FANF := On_; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetANF(FANF); Changed(rfANF); end; function TRadioController.DefaultMicSource: TTXMicSource; begin if FAudioInDevName <> '' then Result := txmsSoundCard else Result := txmsRadio; end; procedure TRadioController.SetMOX(On_: Boolean); var WasTransmitting: Boolean; begin // Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере. if On_ and FDevConnected and (FCurrentXvtr < 0) and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then Exit; if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; WasTransmitting := FTransmitting; FTransmitting := On_; if On_ then FDriveLevel := CalcDriveByte; // Сетевая PTT-последовательность TX-on: сначала реле (Alex bit27=1, drive) без // PTT, затем PTT=1 — переключаем реле раньше передачи. if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin FNetwork.ClearDUCIQQueue; FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, True, True, True); FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле end; if FWDSPReady and Assigned(FDSPEngine) then begin // Mic source автоматически по источнику TX: // Web TX → txmsWeb; HW PTT → txmsRadio; MOX/CAT → DefaultMicSource. if FTransmitting then begin if FWebClientActive then begin FWebMicActive := True; FDSPEngine.SetTXMicSource(txmsWeb); end else if FHWPTTActive then FDSPEngine.SetTXMicSource(txmsRadio) else FDSPEngine.SetTXMicSource(DefaultMicSource); end else if FWebMicActive then begin FWebMicActive := False; FDSPEngine.SetTXMicSource(DefaultMicSource); end; // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы // TX leakage не накапливался в RXA pipeline / FFT-истории анализатора. FDSPEngine.KeepRXDuringTX := FDisplayDuplex; FDSPEngine.SetTXRun(FTransmitting); if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then begin FDSPEngine.FlushRX; if Assigned(FAudioOut) then FAudioOut.Clear; {$IFDEF WINDOWS} // Windows: на TX→RX дропаем И RX-IQ, И аудио-выход — radio FPGA TX-buffer + // PA slew-down дотравливают «хвост» (на Windows тянется 200–500мс). FDSPEngine.BeginPostTXMute(250); {$ENDIF} end; // SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем // источник на RX, чтобы видеть приём во время передачи. if FTransmitting and FDisplayDuplex then FDSPEngine.SetDisplaySourceTX(False); end; // Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX. if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True); FNetwork.ClearDUCIQQueue; FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX) end; Changed(rfTransmitting); end; procedure TRadioController.SetTune(On_: Boolean); begin if On_ = FTuning then Exit; // Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон, // он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR. if On_ and FDevConnected and (FCurrentXvtr < 0) and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then Exit; if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; if On_ then begin // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0); FTuning := True; FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень) if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0); if not FTransmitting then SetMOX(True); // PTT + run TXA-канал end else begin if FTransmitting then SetMOX(False); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetTXTone(False, 0, 0); FTuning := False; FDriveLevel := CalcDriveByte; // вернуть обычный drive% if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetDriveLevel(FDrivePercent / 100.0); if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; Changed(rfTuning); end; procedure TRadioController.SetFMStep(Idx: Integer); begin if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4 FFMStepIdx := Idx; Changed(rfFMStepIdx); end; procedure TRadioController.SetFMCTCSS(On_: Boolean); begin FFMCTCSSOn := On_; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]); Changed(rfFMCTCSS); end; procedure TRadioController.SetFMCTCSSTone(Idx: Integer); begin if Idx < 0 then Idx := 0 else if Idx > CTCSS_COUNT - 1 then Idx := CTCSS_COUNT - 1; FFMCTCSSToneIdx := Idx; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]); Changed(rfFMCTCSSTone); end; procedure TRadioController.SetFMSquelch(On_: Boolean); begin FFMSQOn := On_; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel); Changed(rfFMSQ); end; procedure TRadioController.SetFMSquelchLevel(V: Integer); begin FFMSQLevel := V; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel); Changed(rfFMSQLevel); end; procedure TRadioController.SetFMRpt(Dir: Integer); begin FFMRptDir := Dir; FFMRptAutoActive := False; // явная установка направления — ручное действие PushNetworkState; // TX-сдвиг применяется через ActiveTXFreqHz Changed(rfFMRpt); end; procedure TRadioController.SetFMRptOffset(Hz: Double); begin if Hz <= 0.0 then Exit; FFMRptOffsetHz := Hz; if FFMRptDir <> RPT_NONE then PushNetworkState; Changed(rfFMRpt); end; procedure TRadioController.UpdateRptAutoState; // Авто-включение MINUS-сдвига в 2-метровом репитерном сегменте и авто-снятие // при уходе из него (только если MINUS был выставлен автоматически). begin if RptAutoDir(FVfoA) = RPT_MINUS then begin if FFMRptDir = RPT_NONE then begin FFMRptDir := RPT_MINUS; FFMRptAutoActive := True; Changed(rfFMRpt); end; end else begin if (FFMRptDir = RPT_MINUS) and FFMRptAutoActive then begin FFMRptDir := RPT_NONE; FFMRptAutoActive := False; Changed(rfFMRpt); end; end; end; procedure TRadioController.ApplyChannel(Idx: Integer); begin Changed(rfChannel); { TODO wiring } end; procedure TRadioController.StartStop; begin SetRun(not FRunning); end; procedure TRadioController.SetRun(On_: Boolean); begin FRunning := On_; Changed(rfRunning); { TODO wiring: логика BtnStartStopClick } end; procedure TRadioController.ConnectDevice(const Dev: THPSDRDevice); begin { TODO wiring: DoConnectDevice } Changed(rfDevice); end; procedure TRadioController.Discover; begin { TODO wiring: FNetwork.Discover } end; end.