unit MainForm; { OpenHPSDR Transceiver - Main Form Lazarus / FPC 3.2+ (нет inline var, нет анонимных процедур) ИСПРАВЛЕНИЯ: - (Web NR/NB/ANF fix) BtnNRClick, BtnNBClick, BtnANFClick переписаны без использования «Sender as TFlatButton» — теперь обращаются напрямую к BtnNR / BtnNB / BtnANF. Это устраняет EAccessViolation / EInvalidCast при вызове из SyncWebNR/SyncWebNB/SyncWebANF через TThread.Synchronize, где Sender передавался как nil. Исключение в UI-потоке приводило к разрыву WebSocket-соединения и «отключению» веб-клиента. - SyncWebNR / SyncWebNB / SyncWebANF: передают реальную кнопку (BtnNR / BtnNB / BtnANF) вместо nil — защита на случай изменения обработчиков. } {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface uses Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, OpenGLContext, LCLIntf, LCLType, GraphType, HPSDRProtocol, HPSDRNetwork, WDSP, WDSPEngine, AudioOutput, AudioInput, Settings, WebServer, CATEngine, CATSerial, CATTcp, SpectrumView, SpectrumViewOpengl, WidebandView, StatusBar, PlatformUtils, WinFirewall, BoardUtils, WisdomBuilder, UISync, FlatEdit, FMRepeater, ChannelStore, ChannelsForm, PowerInhibit, RadioController; const CLR_BG = TColor($00101010); CLR_PANEL = TColor($00181818); CLR_BORDER = TColor($00303030); CLR_FREQ = TColor($0000FF00); CLR_FREQ_DIM = TColor($00007800); CLR_AMBER = TColor($0000AAFF); CLR_ACTIVE = TColor($000C3010); CLR_ACTIVE_BDR = TColor($00387838); CLR_INACTIVE = TColor($001A1A1A); CLR_TEXT = TColor($00CCCCCC); CLR_TEXTDIM = TColor($00666666); CLR_METER_ON = TColor($0000CC44); CLR_METER_OVR = TColor($000000CC); CLR_SPECTRUM = TColor($0044FF44); WDSP_WISDOM_FILE = 'wdspWisdom00'; BAND_COUNT = 11; MODE_COUNT = 8; FILT_COUNT = 10; // Группы фильтров по типу модуляции // SSB: LSB/USB FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string = ('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k'); FILT_SSB_BW: array[0..FILT_COUNT-1] of Integer = (5000, 4400, 3800, 3300, 2900, 2700, 2400, 2100, 1800, 1000); FILT_SSB_DEF = 5; // 2.7k // CW: CWL/CWU FILT_CW_NAMES: array[0..FILT_COUNT-1] of string = ('1.0k','800','600','500','400','250','150','100','50','25'); FILT_CW_BW: array[0..FILT_COUNT-1] of Integer = (1000, 800, 600, 500, 400, 250, 150, 100, 50, 25); FILT_CW_DEF = 3; // 500 // DSB FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string = ('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k'); FILT_DSB_BW: array[0..FILT_COUNT-1] of Integer = (16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400); FILT_DSB_DEF = 3; // 8.0k // AM/SAM FILT_AM_NAMES: array[0..FILT_COUNT-1] of string = ('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k'); FILT_AM_BW: array[0..FILT_COUNT-1] of Integer = (20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000); FILT_AM_DEF = 4; // 10k // FM: NFM (2.5 kHz deviation / 11 kHz BW) and FM (5.0 kHz deviation / 16 kHz BW) FILT_FM_COUNT = 2; FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM'); FILT_FM_BW: array[0..1] of Integer = (11000, 16000); FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0); FILT_FM_DEF = 0; // NFM // CTCSS tones (38 standard tones, no None — toggle via CTCSS button) // CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController). CTCSS_NAMES: array[0..37] of string = ( '67.0', '71.9', '74.4', '77.0', '79.7', '82.5', '85.4', '88.5', '91.5', '94.8', '97.4', '100.0', '103.5', '107.2', '110.9', '114.8', '118.8', '123.0', '127.3', '131.8', '136.5', '141.3', '146.2', '151.4', '156.7', '162.2', '167.9', '173.8', '179.9', '186.2', '192.8', '203.5', '210.7', '218.1', '225.7', '233.6', '241.8', '250.3'); // FM Step tuning FM_STEP_COUNT = 4; FM_STEP_HZ: array[0..3] of Integer = (6250, 12500, 20000, 25000); FM_STEP_NAMES: array[0..3] of string = ('6.25k', '12.5k', '20k', '25k'); FM_STEP_DEF = 3; // 25 kHz BAND_NAMES: array[0..BAND_COUNT-1] of string = ( '160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); BAND_FREQ: array[0..BAND_COUNT-1] of Double = ( 1900000, 3750000, 5357000, 7100000, 10125000, 14200000, 18120000, 21200000, 24940000, 28500000, 50150000); MODE_NAMES: array[0..MODE_COUNT-1] of string = ( 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM'); SMETER_WIDTH_RATIO = 0.32; SMETER_MARGIN = 3; TOP_SMETER_GAP = 8; TOP_VFO_FONT = 25; LEFT_PANEL_BTN_PAD = 3; LEFT_PANEL_BTN_GAP = 2; type TDeviceItem = record Dev: THPSDRDevice; Display: string; end; { TMainForm } TMainForm = class(TForm) private // ---- Headless ядро (состояние + движки). Фаза 1 миграции: см. // RadioController.pas. Поля состояния/движков теперь живут в // FController; обращения идут как FController.FXxx. ---- FController: TRadioController; // True пока OnControllerState обновляет виджеты — чтобы программное // изменение слайдера/кнопки не вызывало повторно команду из OnChange. FSyncingFromController: Boolean; // ---- Network ---- FDevices: array of TDeviceItem; FDeviceCount: Integer; FDeviceDialog: TDeviceDialog; FPendingIP: string; // IP устройства для подключения FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра FPanelHidden: Boolean; // True = левая панель скрыта FLeftPanelW: Integer; // ширина панели до скрытия (DPI-safe restore) FPendingBoardType: Integer; // BoardType устройства для подключения // ---- UI-local state (мышь на спектре/водопаде) ---- FSpecDrag: Boolean; FSpecDragX0: Integer; // X при нажатии FSpecDragFreq: Double; // FCenterFreq при нажатии FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад // DUC IQ packetizer: TXA отдаёт блок FTXOutBufSize пар, который // не обязан быть кратен 240 (DUC packet). Хвост переносим в следующий // вызов OnTXIQReady, иначе зануление давало 47 Гц гул. FDUCPendingI: array[0..239] of Integer; FDUCPendingQ: array[0..239] of Integer; FDUCPendingCount: Integer; FLastSMeter: Double; FSMeterPeak: Double; // верхняя граница светлой зоны FSMeterMin: Double; // нижняя граница светлой зоны FSMeterAvg: Double; // сглаженное среднее (EMA) 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; FLastFwdW: Double; FLastSWR: Double; // Эти значения приходят с HP Status пакетами (50..200 раз/с). UI обновляется // из MeterTimerTick (10 Гц), чтобы цифры/полоски не дёргались. FLastSupplyV: Double; FLastSupplyA: Double; FLastPLLLock: Boolean; // ---- Spectrum / Waterfall view ---- FSpecView: TSpectrumView; FWidebandView: TWidebandView; FUseOpenGLSpectrum: Boolean; FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора FSpectrumHeight: Integer; FWaterfallHeight:Integer; FShowSpectrum: Boolean; FShowWaterfall: Boolean; FShowWideband: Boolean; FWidebandFill: Boolean; FSpectrumFill: Boolean; FDisplayFPS: Integer; FWaterfallDirty: Boolean; FWidebandDirty: Boolean; FPreChannelPower: Integer; // мощность до применения канала (-1 = не сохранена) FWfManualHigh: Double; FWfManualLow: Double; FWfAGCOffset: Double; // Spectrum display settings (for settings save/load) FSpecRefLevel: Double; FSpecRange: Double; FSpecGridStep: Double; // TX-specific grid (Thetis-style: при TX рисуем спектр в своих границах). // Применяются к FSpecView когда FTransmitting=True; иначе используются RX-значения. FTXSpecRefLevel: Double; FTXSpecRange: Double; FTXSpecGridStep: Double; // DSP data buffers (copy of last DSP output, for WebServer.PushSpectrum) FSpectrumBuf: array[0..1023] of Single; FWaterfallBuf: array[0..1023] of Single; FSpectrumBufCount: Integer; FWaterfallBufCount:Integer; // Waterfall frame timing FWaterfallFrameInterval: Integer; FWaterfallFrameCounter: Integer; FAgcLineCounter: Integer; // ---- Splitter ---- FSplitterDrag: Boolean; FSplitterDragY0: Integer; FSplitterSH0: Integer; FSplitterRatio: Double; // --- Settings --- FWebServer: TWebServer; FWebEnabled: Boolean; FWebPort: Integer; FWebBindAddr: string; FWebUser: string; FWebPass: string; FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb // --- CAT --- FCATEngine: TCATEngine; FCATSerial: TCATSerialManager; FCATTcp: TCATTcpServer; FCATSyncFreq: Double; FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (для сохранения) // Временные поля для передачи параметров в Synchronize-методы FWebSyncFreq: Double; FWebSyncInt: Integer; FWebSyncBool: Boolean; FWebSyncM: TThreadMethod; FSettingsForm: TObject; // TSettingsForm (cast при использовании) FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP FLightTheme: Boolean; // True = светлая тема FFreqMhzDigits: Integer; // 3=999MHz, 4=9.999GHz, 5=99.999GHz // ---- Timers ---- FMeterTimer: TTimer; FSpectrumTimer: TTimer; FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации FRestoreL, FRestoreT: Integer; // позиция для отложенного восстановления FHasPendingRestore: Boolean; // ---- Top panel (toolbar + VFO group + S-meter) ---- PanelToolbar: TPanel; BtnDiscover: TFlatButton; BtnStartStop: TFlatButton; BtnSettings: TFlatButton; // ---- Left panel ---- PanelLeft: TPanel; PanelVfoA: TPanel; LblVfoALabel: TLabel; FreqDispA: TFreqDisplay; BtnTopVfoASelect: TFlatButton; BtnTopVfoATX: TFlatButton; PanelVfoB: TPanel; LblVfoBLabel: TLabel; FreqDispB: TFreqDisplay; BtnTopVfoBSelect: TFlatButton; BtnTopVfoBTX: TFlatButton; PanelVfoButtons: TPanel; BtnVfoSwap: TFlatButton; BtnVfoACopyB: TFlatButton; BtnVfoBCopyA: TFlatButton; PanelTopVfoGroup: TPanel; PanelTopVfoA: TPanel; PanelTopVfoB: TPanel; PanelBands: TPanel; BtnBand: array[0..BAND_COUNT-1] of TFlatButton; // Динамические кнопки XVTR (создаются/удаляются по мере того, // как пользователь enabled/disabled слоты в Settings → Transverter). BtnXvtrBand: array[0..CFG_XVTR_COUNT-1] of TFlatButton; PanelMode: TPanel; BtnMode: array[0..MODE_COUNT-1] of TFlatButton; PanelFilter: TPanel; BtnFilter: array[0..FILT_COUNT-1] of TFlatButton; BtnCTun: TFlatButton; BtnDUP: TFlatButton; PanelFMSQ: TPanel; BtnFMSQ: TFlatButton; TrkFMSQ: TFlatSlider; LblFMSQ: TLabel; PanelFMCTCSS: TPanel; BtnFMCTCSS: TFlatButton; BtnFMCTCSSTone: TFlatButton; FCTCSSDropDown: TFlatDropDown; PanelFMStep: TPanel; BtnFMStep: TFlatButton; BtnFMStepSel: TFlatButton; FStepDropDown: TFlatDropDown; PanelFMRpt: TPanel; BtnFMRptMinus: TFlatButton; BtnFMRptPlus: TFlatButton; EdFMRptOffset: TFlatEdit; PanelAGC: TPanel; BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF LblAGCTop: TLabel; // показывает значение уровня TrkAGC: TFlatSlider; TrkVolume: TFlatSlider; BtnNR: TFlatButton; BtnNB: TFlatButton; BtnSNB: TFlatButton; BtnANF: TFlatButton; BtnMute: TFlatButton; PanelSMeterRight: TPanel; PbSMeterRight: TPaintBox; PanelTX: TPanel; LblDrv: TLabel; TrkDrive: TFlatSlider; BtnMOX: TFlatButton; BtnTUN: TFlatButton; PanelRXBlock: TPanel; BtnChannels: TFlatButton; FChannelsDropDown: TFlatDropDown; FChannelsForm: TObject; // TChannelsForm (cast при использовании) FActiveChannelIdx: Integer; // -1 = нет активного канала // ---- Right panel ---- PanelRight: TPanel; PbSpectrum: TControl; PbWideband: TControl; PbWidebandRuler: TPaintBox; PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад PbWaterfall: TControl; // ---- Status bar ---- StatusPanel: TMainStatusBar; // ---- Helpers ---- procedure DoInvoke(M: TThreadMethod); // маршалинг команд контроллера в UI-поток procedure OnControllerState(Sender: TObject; Field: TRadioField); // контроллер → UI procedure BuildUI; function LeftPanelButtonLeft(PanelWidth, ColCount, ColIndex: Integer): Integer; function LeftPanelButtonWidth(PanelWidth, ColCount, ColIndex: Integer): Integer; procedure ApplyDarkTheme; procedure SetLightTheme(V: Boolean); procedure StyleButton(B: TFlatButton; Active: Boolean = False); procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False); procedure SetStatusText(Index: Integer; const Text: string); procedure SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean = False); procedure ApplyStatusTheme(const T: TAppTheme); procedure PositionSampleRateOverlay; procedure OnSampleRateOverlayInvalidate(Sender: TObject); procedure OnSampleRateSelect(SampleRate: Integer); procedure OnSampleRateHidePanel; procedure UpdateVfoDisplay; function FormatFreq(Hz: Double): string; procedure LayoutTopVfoBlock; procedure ResizeSMeter; procedure ResizeSpectrumPanels; function UpdateWidebandFrequencyView: Boolean; procedure InvalidateGridCache; procedure SyncSpecViewFreq; procedure RecreateDSPEngine(ASampleRate: Integer); function EnsureWDSPWisdom: Boolean; // Network callbacks procedure OnDeviceFound(const Dev: THPSDRDevice); procedure OnHPStatusCB(const Status: THighPriorityStatus); procedure OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket); procedure OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt; Count: Integer); procedure OnWDSPOpenDone(Success: Boolean); procedure DoConnectDevice(const Dev: THPSDRDevice); procedure OnMicPacketCB(const Data: TMicDataPacket); // DSP/Audio callbacks (вызываются из рабочих потоков) procedure OnAudioReady(const Left, Right: array of Single; Count: Integer); procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer); procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer); // Public UI update (called from sync objects) procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string); procedure DoUpdateStatus(FwdW, SWRV, SupplyV, SupplyA: Double; PLLLock, HWPTT: Boolean; ADCOverload: Byte); procedure UpdateTXMeters; procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord); procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord); // Event handlers procedure BtnDiscoverClick(Sender: TObject); procedure BtnDiscoverFromDialog(Sender: TObject); procedure BtnStartStopClick(Sender: TObject); procedure FreqDispAChanged(Sender: TObject; NewFreq: Int64); procedure ApplyVfoA(NewFreq: Int64); procedure FreqDispAClick(Sender: TObject); procedure FreqDispBClick(Sender: TObject); procedure BtnTopVfoASelectClick(Sender: TObject); procedure BtnTopVfoATXClick(Sender: TObject); procedure BtnTopVfoBSelectClick(Sender: TObject); procedure BtnTopVfoBTXClick(Sender: TObject); procedure ActivateVfo(Idx: Integer); function ActiveVfoFreq: Int64; // Настройки procedure SaveCurrentBand; procedure RestoreBand(BandIdx: Integer); procedure SaveAllAndExit; procedure RestoreWindowBounds; procedure SaveWindowBounds; function MakeGlobalSettings: TGlobalSettings; function MakeBandSettings: TBandSettings; procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64); procedure BtnBandClick(Sender: TObject); procedure ApplyModeFilter; procedure BtnModeClick(Sender: TObject); procedure BtnFilterClick(Sender: TObject); procedure BtnCTunClick(Sender: TObject); procedure BtnDUPClick(Sender: TObject); procedure ApplyDUP(Active: Boolean); procedure BtnTUNClick(Sender: TObject); procedure ApplyTUN(Active: Boolean); procedure UpdateFilterButtons; procedure BtnFMSQClick(Sender: TObject); procedure TrkFMSQChange(Sender: TObject); procedure ApplyFMSquelch; procedure BtnFMCTCSSClick(Sender: TObject); procedure BtnFMCTCSSToneClick(Sender: TObject); procedure SetFMCTCSSTone(Idx: Integer); procedure CloseCTCSSPopup; procedure BtnFMRptMinusClick(Sender: TObject); procedure BtnFMRptPlusClick(Sender: TObject); procedure EdFMRptOffsetChange(Sender: TObject); procedure ApplyFMRpt; procedure UpdateRptAutoState; procedure SetFMStep(Idx: Integer); procedure CloseFMStepPopup; procedure BtnFMStepClick(Sender: TObject); procedure BtnFMStepSelClick(Sender: TObject); procedure OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer); procedure OnStepDropDownSelect(Sender: TObject; Idx: Integer); procedure BtnChannelsClick(Sender: TObject); procedure BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer); procedure RefreshChannelsDropDown; procedure OnChannelStoreChanged(Sender: TObject); procedure ApplyChannel(const Ch: TChannel); procedure CheckChannelActive; procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PbSpectrumMouseLeave(Sender: TObject); procedure PbWidebandMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer); procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer); procedure DoWidebandClick(PixelX: Integer; PanelWidth: Integer); procedure BtnVfoSwapClick(Sender: TObject); procedure BtnVfoACopyBClick(Sender: TObject); procedure BtnVfoBCopyAClick(Sender: TObject); procedure BtnMOXClick(Sender: TObject); procedure BtnMuteClick(Sender: TObject); procedure UpdateNRButton; procedure UpdateNBButton; procedure UpdateSNBButton; procedure UpdateANFButton; procedure ApplyNoiseFilterButtonsToDSP; procedure BtnNRClick(Sender: TObject); procedure BtnNBClick(Sender: TObject); procedure BtnSNBClick(Sender: TObject); procedure BtnANFClick(Sender: TObject); // Веб-интерфейс: callbacks от TWebServer procedure WebOnFreq(Hz: Double); procedure WebOnMode(Mode: Integer); procedure WebOnFilter(BW: Integer); procedure WebOnAGC(Mode: Integer); procedure WebOnAGCTop(DB: Integer); procedure WebOnBand(Idx: Integer); procedure WebOnSpan(Hz: Integer); procedure WebOnVolume(V: Integer); procedure WebOnWfAGC(On_: Boolean); procedure WebOnWfNF(On_: Boolean); procedure WebOnRun(On_: Boolean); procedure WebOnMute(On_: Boolean); procedure WebOnCtun(On_: Boolean); procedure WebOnNR(Mode: Integer); procedure WebOnNB(Mode: Integer); procedure WebOnSNB(On_: Boolean); procedure WebOnANF(On_: Boolean); procedure WebOnFreqB(Hz: Double); procedure WebOnActiveVfo(Idx: Integer); procedure WebOnCenter(Hz: Double); procedure WebOnMOX(On_: Boolean); procedure WebOnDrive(V: Integer); procedure WebOnFreqA(Hz: Double); procedure WebOnAttn(Idx: Integer); procedure WebOnTun(On_: Boolean); procedure WebOnFMStep(Idx: Integer); procedure WebOnMic(Samples: PSingle; Count: Integer); procedure WebOnXvtrBand(Idx: Integer); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; procedure SyncWebMode; procedure SyncWebFilter; procedure SyncWebAGC; procedure SyncWebAGCTop; procedure SyncWebBand; procedure SyncWebXvtrBand; procedure SyncWebSpan; procedure SyncWebVolume; procedure SyncWebWfAGC; procedure SyncWebWfNF; procedure SyncWebRun; procedure SyncWebMute; procedure SyncWebCtun; procedure SyncWebNR; procedure SyncWebNB; procedure SyncWebSNB; procedure SyncWebANF; procedure SyncWebFreqB; procedure SyncWebActiveVfo; procedure SyncWebCenter; procedure SyncWebMOX; procedure SyncWebDrive; procedure SyncWebFreqA; procedure SyncWebAttn; procedure SyncWebTun; procedure SyncWebFMStep; // --- CAT callbacks ------------------------------------------------------- function CATGetVfoA: Double; function CATGetVfoB: Double; function CATGetMode: Integer; function CATGetActiveVfo: Integer; function CATGetAGCMode: Integer; function CATGetVolume: Integer; function CATGetDriveLevel: Integer; function CATGetFilterIdx: Integer; function CATGetFilterBW: Integer; function CATGetNRMode: Integer; function CATGetNBMode: Integer; function CATGetSNB: Boolean; function CATGetANF: Boolean; function CATGetTX: Boolean; function CATGetRunning: Boolean; function CATGetSMeter: Double; function CATGetBand: Integer; procedure CATSetVfoA(V: Double); procedure CATSetVfoB(V: Double); procedure CATSetFilterIdx(V: Integer); procedure CATDoBandUp; procedure CATDoBandDown; procedure CATDoTuneUp; procedure CATDoTuneDown; procedure SyncCATVfoA; procedure SyncCATVfoB; procedure SyncCATBandUp; procedure SyncCATBandDown; procedure SyncCATTuneUp; procedure SyncCATTuneDown; procedure InitCATEngine; procedure CATApplySettings(const G: TGlobalSettings); procedure OnCATSettingsChange( const SerEnabled: array of Boolean; const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer); procedure OnAlexSettingsChange(const A: TAlexSettings); procedure OnXvtrSettingsChange(const X: TXvtrSettings); procedure PushXvtrToWeb; // XVTR helpers function FindXvtrIdxForFreq(FreqHz: Double): Integer; function XvtrTranslate(VisibleHz: Double): Double; procedure ApplyXvtrToNetwork; procedure RebuildXvtrButtons; procedure RelayoutBelowBands; procedure BtnXvtrBandClick(Sender: TObject); procedure ActivateXvtrBand(Idx: Integer); procedure DeactivateXvtr; // ------------------------------------------------------------------------- procedure BtnSettingsClick(Sender: TObject); procedure PositionVfoOverlay; procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer); procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean); procedure OnVfoOverlayAGCChange(AGCMode: Integer); procedure OnVfoOverlayInvalidate(Sender: TObject); procedure PbSpectrumDblClick(Sender: TObject); procedure TrkDriveChange(Sender: TObject); procedure TrkVolumeChange(Sender: TObject); function CalcDriveByte: Byte; function ActiveTXFreqHz: Double; function DefaultMicSource: TTXMicSource; procedure ApplyMOX(Active: Boolean); procedure OnTXIQReady(const Buf: array of Double; Count: Integer); procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); procedure OnVHFCalSettingsChange(const VHFCal: array of Double); procedure OnTXSettingsChange(const T: TTXSettings); procedure BtnAGCModeClick(Sender: TObject); procedure TrkAGCChange(Sender: TObject); procedure MeterTimerTick(Sender: TObject); procedure SpectrumTimerTick(Sender: TObject); procedure RightPanelResize(Sender: TObject); procedure AfterShowTick(Sender: TObject); // Splitter handlers procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); public // Методы для SettingsForm — немедленное применение настроек procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer; SpecAvgTimeMS: Double); procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double); procedure ApplyWfAGCNF(WfAGC, WfNF: Boolean); procedure ApplyADCSettings(Dither, Random: Boolean); procedure ApplyWebSettings(Enabled: Boolean; Port: Integer; const BindAddr, User, Pass: string); procedure ApplyGridParams(RefLevel, Range, GridStep: Double); // Пушит активные grid-параметры (RX или TX в зависимости от FTransmitting) // в FSpecView и сбрасывает кэш сетки. Вызывается при смене RX↔TX и при // правке любого из соответствующих полей (FSpec*, FTXSpec*). procedure ApplySpecViewGridFromState; procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string); procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); procedure ApplyAudioBufferSize(BufferSize: Integer); procedure ApplySendAudioToRadio(Enabled: Boolean); // TX settings: применить FTXSettings к WDSP и переслать DUC Specific // (mic-биты Boost/Bias/LineIn/PTT попадают в byte 50 DUCSpecific). procedure ApplyTXSettingsToDSP; procedure SendDUCSpecificFromSettings; function PullSoundCardMic(MaxN: Integer): Integer; function BuildMicLineSelectByte: Byte; procedure ApplyVisibility(ShowSpectrum, ShowWaterfall, ShowWideband, WidebandFill, SpectrumFill: Boolean); procedure ApplyFPS(FPS: Integer); procedure ApplyFreqMhzDigits(Digits: Integer); published // Обработчики событий формы — должны быть в published для RTTI/LFM procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormClose(Sender: TObject; var CloseAction: TCloseAction); procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); end; var MainForm: TMainForm; implementation uses SettingsForm; {$R *.lfm} function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount, ColIndex: Integer): Integer; var InnerW, BaseW, Extra: Integer; begin if ColCount <= 0 then Exit(0); InnerW := PanelWidth - 2 * LEFT_PANEL_BTN_PAD - (ColCount - 1) * LEFT_PANEL_BTN_GAP; BaseW := InnerW div ColCount; Extra := InnerW mod ColCount; Result := BaseW; if ColIndex < Extra then Inc(Result); end; function TMainForm.LeftPanelButtonLeft(PanelWidth, ColCount, ColIndex: Integer): Integer; var i: Integer; begin Result := LEFT_PANEL_BTN_PAD; for i := 0 to ColIndex - 1 do Inc(Result, LeftPanelButtonWidth(PanelWidth, ColCount, i) + LEFT_PANEL_BTN_GAP); end; // =========================================================================== // Settings helpers // =========================================================================== procedure TMainForm.RestoreWindowBounds; var L, T, Wd, Ht, SavedDPI, CurDPI: Integer; Maximized: Boolean; R, WA: TRect; Mon: TMonitor; begin FController.FSettings.LoadWindowBounds(L, T, Wd, Ht, SavedDPI, Maximized); if (Wd > 400) and (Ht > 300) then begin CurDPI := CurrentScreenDPI; if SavedDPI <= 0 then SavedDPI := CurDPI; if SavedDPI <> CurDPI then begin // Позицию не масштабируем: на LCLWin32 координаты физические и не зависят // от DPI-фактора; на Qt6/GTK/Cocoa — логические (device-independent), тоже // инвариантны. Выход за рабочую область поймает EnsureRange ниже. // // Размер масштабируем, чтобы окно выглядело одинаково при смене DPI: // LCLWin32: физические пиксели → Wd × CurDPI/SavedDPI // Qt6/GTK/Cocoa: логические пиксели → Wd × SavedDPI/CurDPI (обратное) {$IFDEF LCLWin32} Wd := MulDiv(Wd, CurDPI, SavedDPI); Ht := MulDiv(Ht, CurDPI, SavedDPI); {$ELSE} Wd := MulDiv(Wd, SavedDPI, CurDPI); Ht := MulDiv(Ht, SavedDPI, CurDPI); {$ENDIF} end; // Минимальный размер: // LCLWin32 — в физических пикселях, масштабируется с DPI. // Qt6/GTK/Cocoa — в логических пикселях, device-independent, фиксированный. {$IFDEF LCLWin32} Wd := Max(MulDiv(900, CurDPI, 96), Wd); Ht := Max(MulDiv(560, CurDPI, 96), Ht); {$ELSE} Wd := Max(900, Wd); Ht := Max(560, Ht); {$ENDIF} R := Bounds(L, T, Wd, Ht); Mon := Screen.MonitorFromRect(R); if Mon <> nil then WA := Mon.WorkareaRect else WA := Screen.PrimaryMonitor.WorkareaRect; if Wd > WA.Right - WA.Left then Wd := WA.Right - WA.Left; if Ht > WA.Bottom - WA.Top then Ht := WA.Bottom - WA.Top; L := EnsureRange(L, WA.Left, Max(WA.Left, WA.Right - Wd)); T := EnsureRange(T, WA.Top, Max(WA.Top, WA.Bottom - Ht)); SetBounds(L, T, Wd, Ht); if Maximized then WindowState := wsMaximized else begin // На Qt6/X11 frame extents ещё не известны Qt во время FormCreate, // поэтому move() может поставить контент туда, куда мы хотели рамку. // Повторно применим позицию в AfterShowTick, когда WM уже декорировал окно. FRestoreL := L; FRestoreT := T; FHasPendingRestore := True; end; end; end; procedure TMainForm.SaveWindowBounds; var L, T, Wd, Ht, DPI: Integer; Maximized: Boolean; begin Maximized := WindowState = wsMaximized; if WindowState = wsNormal then begin L := Left; T := Top; Wd := Width; Ht := Height; end else begin L := RestoredLeft; T := RestoredTop; Wd := RestoredWidth; Ht := RestoredHeight; end; // RestoredLeft/Top могут вернуть мусор на Qt6 при максимизации; // ставим разумные запасные значения if (L < -32000) or (T < -32000) then begin L := 80; T := 80; end; if (Wd <= 400) or (Ht <= 300) then Exit; DPI := CurrentScreenDPI; FController.FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized); end; function TMainForm.MakeBandSettings: TBandSettings; begin Result.VfoA := FController.FVfoA; Result.VfoB := FController.FVfoB; Result.Mode := FController.FMode; Result.FilterIdx := FController.FFilter; Result.FilterBW := FController.FFilterBW; Result.AGCMode := FController.FAGCMode; Result.AGCTop := FController.FAGCTop; Result.CTun := FController.FCTun; Result.SpanHz := FController.FSpanHz; Result.FMSQOn := FController.FFMSQOn; Result.FMSQLevel := FController.FFMSQLevel; Result.CTCSSOn := FController.FFMCTCSSOn; Result.CTCSSToneIdx := FController.FFMCTCSSToneIdx; Result.FMStepOn := FController.FFMStepOn; Result.FMStepIdx := FController.FFMStepIdx; Result.FMRptDir := FController.FFMRptDir; Result.FMRptOffsetHz := FController.FFMRptOffsetHz; // Waterfall AGC/NF — глобальные (не диапазонные) end; function TMainForm.MakeGlobalSettings: TGlobalSettings; var i: Integer; begin Result.Volume := FController.FVolume; Result.DriveLevel := FController.FDrivePercent; Result.PAMaxPower := FController.FPAMaxPower; for i := 0 to BAND_COUNT - 1 do Result.PABandCal[i] := FController.FPABandCal[i]; for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FController.FVHFBandCal[i]; Result.ActiveVfo := FController.FActiveVfo; Result.NRMode := FController.FNRMode; Result.NBMode := FController.FNBMode; Result.SNBEnabled := FController.FSNB; Result.ANFEnabled := FController.FANF; Result.AGCSlope := 0; Result.AGCHangThreshold := 100; Result.WfAGCEnabled := FController.FWfAGCEnabled; Result.WfNFEnabled := FController.FWfNFEnabled; Result.DitherEnabled := FController.FDitherEnabled; Result.RandomEnabled := FController.FRandomEnabled; Result.LastBand := FController.FCurrentBand; Result.LastXvtr := FController.FCurrentXvtr; Result.SampleRate := FController.FSampleRate; // Display settings if FController.FWDSPReady then begin Result.FFTSize := FController.FDSPEngine.FFTSize; Result.WindowType := FController.FDSPEngine.WindowType; Result.SpecDetector := FController.FDSPEngine.SpecDetector; Result.SpecAvgMode := FController.FDSPEngine.SpecAvgMode; Result.SpecAvgTimeMS := FController.FDSPEngine.SpecAvgTimeMS; Result.WfDetector := FController.FDSPEngine.WfDetector; Result.WfAvgMode := FController.FDSPEngine.WfAvgMode; Result.WfAvgTimeMS := FController.FDSPEngine.WfAvgTimeMS; end else begin Result.FFTSize := 131072; Result.WindowType := 2; Result.SpecDetector := 0; Result.SpecAvgMode := 3; Result.SpecAvgTimeMS := 30.0; Result.WfDetector := 0; Result.WfAvgMode := 3; Result.WfAvgTimeMS := 120.0; end; Result.WfManualHigh := FWfManualHigh; Result.WfManualLow := FWfManualLow; Result.WfAGCOffset := FWfAGCOffset; Result.SpecRefLevel := FSpecRefLevel; Result.SpecRange := FSpecRange; Result.SpecGridStep := FSpecGridStep; Result.AudioSampleRate := FController.FAudioOut.SampleRate; Result.ShowSpectrum := FShowSpectrum; Result.ShowWaterfall := FShowWaterfall; Result.ShowWideband := FShowWideband; Result.WidebandFill := FWidebandFill; Result.SpectrumFill := FSpectrumFill; Result.DisplayDuplex := FController.FDisplayDuplex; Result.DisplayFPS := FDisplayFPS; Result.LightTheme := FLightTheme; Result.FreqMhzDigits := FFreqMhzDigits; // Audio device names Result.AudioOutDevice := FController.FAudioOutDevName; Result.AudioInDevice := FController.FAudioInDevName; Result.SendAudioToRadio := FController.FSendAudioToRadio; // CAT settings — carried from the last loaded/saved device config Result.CATSerialEnabled := FCATLastGlobal.CATSerialEnabled; Result.CATSerialPort := FCATLastGlobal.CATSerialPort; Result.CATSerialBaud := FCATLastGlobal.CATSerialBaud; Result.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits; Result.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits; Result.CATSerialParity := FCATLastGlobal.CATSerialParity; Result.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled; Result.CATTcpPort := FCATLastGlobal.CATTcpPort; end; procedure TMainForm.SaveCurrentBand; begin if not FController.FDevConnected then Exit; // В XVTR-режиме НЕ трогаем HF band-cache: FController.FBandCache[FController.FCurrentBand] // относится к HF-диапазону, на котором пользователь был ДО активации // трансвертера, и FController.FVfoA сейчас на XVTR-частоте (144.x, 432.x и т.д.). // Запись в FController.FBandCache[FController.FCurrentBand] стерла бы корректное HF-состояние. // Вместо этого сохраняем LastFreq в XVTR-настройках. if FController.FCurrentXvtr >= 0 then begin if (FController.FCurrentXvtr < CFG_XVTR_COUNT) then begin FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastMode := FController.FMode; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFilterIdx := FController.FFilter; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQOn := FController.FFMSQOn; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQLevel := FController.FFMSQLevel; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSOn := FController.FFMCTCSSOn; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSToneIdx := FController.FFMCTCSSToneIdx; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMStepOn := FController.FFMStepOn; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMStepIdx := FController.FFMStepIdx; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptDir := FController.FFMRptDir; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptOffsetHz := FController.FFMRptOffsetHz; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTun := FController.FCTun; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastAGCMode := FController.FAGCMode; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastAGCTop := FController.FAGCTop; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSAutoActive := FController.FFMCTCSSAutoActive; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptAutoActive := FController.FFMRptAutoActive; end; FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings); Exit; end; // Не сохраняем, если VFO ушёл в VHF/UHF область (например, после применения // канала на 2м без XVTR) — иначе HF-кэш диапазона будет перезаписан VHF-частотой. if FController.FVfoA > 61000000 then Exit; FController.FBandCache[FController.FCurrentBand] := MakeBandSettings; FController.FSettings.SaveBand(FController.FDevMAC, FController.FCurrentBand, FController.FBandCache[FController.FCurrentBand]); end; procedure TMainForm.RestoreBand(BandIdx: Integer); var B: TBandSettings; i: Integer; begin if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit; B := FController.FBandCache[BandIdx]; // --- Кнопки диапазонов --- for i := 0 to BAND_COUNT - 1 do StyleButton(BtnBand[i], i = BandIdx); // Сбрасываем waterfall thresholds при смене диапазона FSpecView.ResetWfAvgBuf; // --- Режим: сначала устанавливаем FController.FMode, потом кнопки и WDSP --- FController.FMode := B.Mode; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FController.FMode); if FController.FWDSPReady then FController.FDSPEngine.SetMode(FController.FMode); // --- Фильтр --- FController.FFilter := B.FilterIdx; FController.FFilterBW := B.FilterBW; UpdateFilterButtons; // обновляет кнопки фильтра ApplyModeFilter; // применяет Lo/Hi в WDSP с учётом нового FController.FMode // --- AGC --- FController.FAGCMode := B.AGCMode; FController.FAGCTop := B.AGCTop; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); TrkAGC.Position := FController.FAGCTop; LblAGCTop.Caption := Format('%ddB', [FController.FAGCTop]); if FController.FWDSPReady then begin FController.FDSPEngine.SetAGCTop(FController.FAGCTop); FController.FDSPEngine.SetAGC(TWDSPAGCMode(FController.FAGCMode), 50.0); end; FSpecView.AGCTop := FController.FAGCTop; // --- FM Squelch + CTCSS --- FController.FFMSQOn := B.FMSQOn; FController.FFMSQLevel := B.FMSQLevel; // UI update happens inside UpdateFilterButtons (called above) when mode=FM FController.FFMCTCSSOn := B.CTCSSOn; if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FController.FFMCTCSSOn); SetFMCTCSSTone(B.CTCSSToneIdx); ApplyFMSquelch; FController.FFMStepOn := B.FMStepOn; SetFMStep(B.FMStepIdx); if BtnFMStep <> nil then StyleButton(BtnFMStep, FController.FFMStepOn); FController.FFMRptDir := B.FMRptDir; FController.FFMRptOffsetHz := B.FMRptOffsetHz; if FController.FFMRptOffsetHz <= 0 then FController.FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA); if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS); if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS); if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); // --- CTUN --- FController.FCTun := B.CTun; StyleButton(BtnCTun, FController.FCTun); if FController.FWDSPReady and not FController.FCTun then FController.FDSPEngine.SetShift(0.0); // --- Sample Rate / Span --- // SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона. FController.FSpanHz := FController.FSampleRate; if Assigned(FSampleRateOverlay) then FSampleRateOverlay.SetCurrentRate(FController.FSampleRate); // --- VFO A: центрируем на новой частоте, сбрасываем shift --- FController.FCenterFreq := B.VfoA; // DDC = центр диапазона FController.FVfoB := B.VfoB; FreqDispB.Frequency := Round(FController.FVfoB); // Пересчитываем drive byte для нового диапазона перед отправкой в сеть FController.FDriveLevel := CalcDriveByte; // ApplyVfoA обновит сеть, FreqDisp, shift, перерисует ApplyVfoA(Round(B.VfoA)); // Смена диапазона — дискретное событие (не wheel/drag), поэтому // перерисовываем Ruler синхронно. ApplyVfoA откладывает рендер на таймер // (FSpectrumDirty), что для оффлайна/без RUN не перерисовывает линейку. FSpecView.InvalidateRulerCache; if PbRuler <> nil then PbRuler.Invalidate; end; procedure TMainForm.SaveAllAndExit; begin if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); if FController.FDevConnected then FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); // Запомнить LastFreq XVTR для следующего запуска if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) then FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings); FController.FSettings.Save; end; end; procedure TMainForm.FormCreate(Sender: TObject); var bi_, StartupRate, i: Integer; WebCfg: TWebSettings; StartupVfoA, StartupVfoB: Double; begin SetExceptionMask(GetExceptionMask + [exZeroDivide, exInvalidOp]); // Headless ядро: владеет состоянием и движками. Создаём первым — все // присваивания FController.FXxx ниже зависят от него. Движки создаются/ // освобождаются здесь же (в полях FController) — Фаза 1 миграции. FController := TRadioController.Create; FController.OnInvoke := DoInvoke; FController.OnStateChanged := OnControllerState; FController.FVfoA := 14200000; FController.FVfoB := 7100000; FController.FActiveVfo := 0; FController.FMode := 1; FController.FFilter := 5; FController.FAGCMode := 1; // MEDIUM FController.FAGCTop := 90; // −90 dBm // default: зависит от режима, будет сброшен в UpdateFilterButtons FController.FCTun := False; FController.FFilterBW := 2700; FSpecDrag := False; FController.FDriveLevel := 0; FController.FPAMaxPower := 100.0; for i := 0 to BAND_COUNT - 1 do FController.FPABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i]; for i := 0 to CFG_XVTR_COUNT - 1 do FController.FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT; FController.FRunning := False; FController.FTransmitting := False; FController.FSplitTxB := False; FDUCPendingCount := 0; FController.FDisplayDuplex := False; FController.FTuning := False; FController.FMuted := False; FController.FVolume := 70; FController.FCenterFreq := FController.FVfoA; FController.FSpanHz := 192000; FController.FSampleRate := 192000; FController.FCurrentBand := 5; // 20m по умолчанию // Waterfall normalization FWfManualHigh := -80.0; FWfManualLow := -130.0; FWfAGCOffset := 0.0; FController.FWfAGCEnabled := True; FController.FWfNFEnabled := False; FController.FDitherEnabled := True; FController.FRandomEnabled := True; FController.FFMDeviation := FILT_FM_DEV[FILT_FM_DEF]; FController.FFMCTCSSOn := False; FController.FFMCTCSSToneIdx := 0; FController.FFMSQOn := False; FController.FFMSQLevel := 30; FController.FFMStepOn := True; FController.FFMStepIdx := FM_STEP_DEF; FController.FFMRptDir := RPT_NONE; FController.FFMRptOffsetHz := RPT_DEFAULT_2M; FController.FFMRptAutoActive := False; FController.FFMCTCSSAutoActive := False; FPreChannelPower := -1; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FWaterfallFrameInterval := 2; FWaterfallFrameCounter := 0; FController.FDevConnected := False; // Spectrum grid settings defaults FSpecRefLevel := -20.0; FSpecRange := 110.0; FSpecGridStep := 10.0; // TX grid defaults (синхронно с TSettingsManager.DefaultTX) FTXSpecRefLevel := 0.0; FTXSpecRange := 80.0; FTXSpecGridStep := 10.0; FSettingsForm := nil; FChannelsForm := nil; FActiveChannelIdx := -1; FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0); // Инициализируем кэш диапазонов умолчаниями for bi_ := 0 to CFG_BAND_COUNT - 1 do TSettingsManager.DefaultBand(bi_, FController.FBandCache[bi_]); FController.FCurrentBand := 5; // Загружаем JSON настройки FController.FSettings := TSettingsManager.Create; FController.FSettings.Load; // Загружаем список каналов FController.FChannelStore := TChannelStore.Create(CHANNELS_FILE); FController.FChannelStore.Load; if FController.FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then begin FController.FVfoA := StartupVfoA; FController.FVfoB := StartupVfoB; FController.FCenterFreq := FController.FVfoA; if StartupRate > 0 then begin FController.FSampleRate := StartupRate; FController.FSpanHz := StartupRate; end; end; // Восстанавливаем размер/позицию окна при старте (до подключения устройства) RestoreWindowBounds; // S-метр позиционируем после рестора размера окна // (будет пересчитан в первом тике SpectrumTimerTick) FLastSMeter := -130; // Загружаем настройки веб-сервера из JSON и создаём сервер FController.FSettings.LoadWebSettings(WebCfg); FWebEnabled := WebCfg.Enabled; FWebPort := WebCfg.Port; FWebBindAddr := WebCfg.BindAddr; FWebUser := WebCfg.User; FWebPass := WebCfg.Pass; FWebServer := TWebServer.Create(FWebUser, FWebPass, Word(FWebPort), FWebBindAddr); FWebServer.OnFreq := WebOnFreq; FWebServer.OnMode := WebOnMode; FWebServer.OnFilter := WebOnFilter; FWebServer.OnAGC := WebOnAGC; FWebServer.OnAGCTop := WebOnAGCTop; FWebServer.OnBand := WebOnBand; FWebServer.OnSpan := WebOnSpan; FWebServer.OnVolume := WebOnVolume; FWebServer.OnWfAGC := WebOnWfAGC; FWebServer.OnWfNF := WebOnWfNF; FWebServer.OnRun := WebOnRun; FWebServer.OnMute := WebOnMute; FWebServer.OnCtun := WebOnCtun; FWebServer.OnNR := WebOnNR; FWebServer.OnNB := WebOnNB; FWebServer.OnSNB := WebOnSNB; FWebServer.OnANF := WebOnANF; FWebServer.OnFreqB := WebOnFreqB; FWebServer.OnActiveVfo := WebOnActiveVfo; FWebServer.OnCenter := WebOnCenter; FWebServer.OnMOX := WebOnMOX; FWebServer.OnDrive := WebOnDrive; FWebServer.OnFreqA := WebOnFreqA; FWebServer.OnAttn := WebOnAttn; FWebServer.OnTun := WebOnTun; FWebServer.OnFMStep := WebOnFMStep; FWebServer.OnWebMic := WebOnMic; FWebServer.OnXvtrBand := WebOnXvtrBand; if FWebEnabled then FWebServer.Start; PushXvtrToWeb; FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0); FCATLastGlobal.CATTcpPort := 19090; for i := 0 to 3 do begin FCATLastGlobal.CATSerialBaud[i] := 9600; FCATLastGlobal.CATSerialDataBits[i] := 8; FCATLastGlobal.CATSerialStopBits[i] := 1; {$IFDEF MSWINDOWS} FCATLastGlobal.CATSerialPort[i] := 'COM' + IntToStr(i + 1); {$ELSE} FCATLastGlobal.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i); {$ENDIF} end; FController.FSettings.LoadCATSettings(FCATLastGlobal); FLightTheme := FController.FSettings.LoadTheme; InitCATEngine; CATApplySettings(FCATLastGlobal); FSMeterPeak := -130; FSMeterMin := -130; FSMeterAvg := -130; FRXPacketCount := 0; FActiveDDC := 0; FLastDDCSeq := 0; FLastDDCIndex := 0; FillChar(FDDCLastSeq, SizeOf(FDDCLastSeq), 0); FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0); FSeqErrorCount := 0; FLastSeqErrorDDC := -1; FLastSeqErrorDelta:= 0; FSeqOkStreak := 0; FLastFwdW := 0; FLastSWR := 1; FLastSupplyV:= -1; FLastSupplyA:= -1; FLastPLLLock:= False; FDeviceCount := 0; FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; FVfoOverlay := TVfoOverlay.Create(Self); FVfoOverlay.OnSelect := OnModeFilterSelect; FVfoOverlay.OnDSPChange := OnVfoOverlayDSPChange; FVfoOverlay.OnAGCChange := OnVfoOverlayAGCChange; FVfoOverlay.OnInvalidate := OnVfoOverlayInvalidate; FVfoOverlay.Visible := False; FPanelHidden := False; FShowSpectrum := True; FShowWaterfall := True; FShowWideband := False; FWidebandFill := False; FSpectrumFill := True; FDisplayFPS := 60; FFreqMhzDigits := 3; FSplitterRatio := 0.40; FSplitterDrag := False; FUseOpenGLSpectrum := HasOpenGLSpectrumSwitch; if FUseOpenGLSpectrum and (not QtPlatformAllowsOpenGLControls) then begin MessageDlg('OpenGL rendering disabled', 'OpenGL rendering is not available in the current Qt Wayland mode.' + LineEnding + LineEnding + 'The Lazarus/LCL Qt6 OpenGL control used by EWSDR requires an X11 display handle, ' + 'but the native Wayland backend does not provide one. Using it here can cause an access violation during startup.' + LineEnding + LineEnding + 'To run EWSDR with OpenGL rendering, start it through XWayland:' + LineEnding + 'QT_QPA_PLATFORM=xcb ./ewsdr --opengl' + LineEnding + LineEnding + 'OpenGL rendering will be disabled for this session.', mtWarning, [mbOK], 0); FUseOpenGLSpectrum := False; end; if FUseOpenGLSpectrum then FWaterfallFrameInterval := 1; if FUseOpenGLSpectrum then FSpecView := TSpectrumViewOpenGL.Create else FSpecView := TSpectrumView.Create; FSpecView.VfoOverlay := FVfoOverlay; FSpecView.VfoA := FController.FVfoA; FSpecView.VfoB := FController.FVfoB; FSpecView.ActiveVfo := FController.FActiveVfo; FSpecView.CenterFreq := FController.FCenterFreq; FSpecView.SpanHz := FController.FSpanHz; FSpecView.Mode := FController.FMode; FSpecView.FilterBW := FController.FFilterBW; FSpecView.AGCTop := FController.FAGCTop; FSpecView.WfAGCEnabled := FController.FWfAGCEnabled; FSpecView.WfNFEnabled := FController.FWfNFEnabled; FSpecView.WfManualHigh := FWfManualHigh; FSpecView.WfManualLow := FWfManualLow; FSpecView.WfAGCOffset := FWfAGCOffset; FSpecView.SpecRefLevel := FSpecRefLevel; FSpecView.SpecRange := FSpecRange; FSpecView.SpecGridStep := FSpecGridStep; // TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k) FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE; FSpecView.WfFrameInterval := FWaterfallFrameInterval; FSpecView.PAMaxPower := FController.FPAMaxPower; FWidebandView := TWidebandView.Create; FController.FNetwork := THPSDRNetwork.Create; FController.FNetwork.OnDeviceFound := OnDeviceFound; FController.FNetwork.OnHPStatus := OnHPStatusCB; FController.FNetwork.OnDDCIQ := OnDDCIQCB; FController.FNetwork.OnMicPacket := OnMicPacketCB; FController.FNetwork.OnWideband := OnWidebandCB; // Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано). TSettingsManager.DefaultTX(FController.FTXSettings); TSettingsManager.DefaultAlex(FController.FAlexSettings); TSettingsManager.DefaultXvtr(FController.FXvtrSettings); FController.FCurrentXvtr := -1; BuildUI; ResizeSMeter; ApplyDarkTheme; UpdateVfoDisplay; UpdateFilterButtons; // DSP Engine — создаём объект, Open вызовется при нажатии START // (FController.FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек) FController.FWDSPReady := False; // BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz. // Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA // (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR. FController.FDSPEngine := TWDSPEngine.Create(FController.FSampleRate, 48000, 512); FController.FDSPEngine.OnAudio := OnAudioReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnTXIQ := OnTXIQReady; // Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым // тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается. FController.FDSPEngine.OnPullMicSamples := PullSoundCardMic; // Audio output/input — создаём объекты сейчас, открываем после показа формы // (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC) FController.FAudioOut := TAudioOutput.Create(48000); FController.FAudioOut.OutputBufferSize := FController.FSettings.LoadAudioBufferSize; FController.FAudioIn := TAudioInput.Create(48000); FMeterTimer := TTimer.Create(Self); FMeterTimer.Interval := 100; FMeterTimer.OnTimer := MeterTimerTick; FMeterTimer.Enabled := True; FSpectrumTimer := TTimer.Create(Self); FSpectrumTimer.Interval := 50; FSpectrumTimer.OnTimer := SpectrumTimerTick; FSpectrumTimer.Enabled := True; // Однократный таймер — инициализация аудио после показа формы FAfterShowTimer := TTimer.Create(Self); FAfterShowTimer.Interval := 200; FAfterShowTimer.OnTimer := AfterShowTick; FAfterShowTimer.Enabled := True; OnMouseWheel := FormMouseWheel; // Проверяем и при необходимости добавляем исключение в Windows Firewall // (UDP входящий трафик для HPSDR Protocol 2) {$IFDEF WINDOWS} FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver'); {$ENDIF} end; procedure TMainForm.FormDestroy(Sender: TObject); begin FMeterTimer.Enabled := False; FSpectrumTimer.Enabled := False; if FController.FNetwork.Running then FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0); FController.FNetwork.Disconnect; FController.FNetwork.Free; FController.FAudioOut.Close; FController.FAudioOut.Free; FController.FAudioIn.Close; FController.FAudioIn.Free; FController.FDSPEngine.Close; FController.FDSPEngine.Free; FreeAndNil(FSpecView); FreeAndNil(FWidebandView); // Сохраняем настройки при закрытии if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); if FController.FDevConnected then FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); end; // Размер окна сохраняем всегда (не зависит от подключения) FController.FSettings.SaveStartupPreview(FController.FVfoA, FController.FVfoB, FController.FSampleRate); FController.FSettings.Save; FController.FSettings.Free; FreeAndNil(FController.FChannelStore); FWebServer.Stop; FWebServer.Free; if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end; if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end; FreeAndNil(FCATEngine); // Движки (поля FController) уже освобождены выше. // Освобождаем само ядро последним. FreeAndNil(FController); end; // Маршалинг команд контроллера в UI-поток (фронтенды зовут FController.Invoke). // В режиме демона хозяин подставит другой механизм (очередь/прямой вызов). procedure TMainForm.DoInvoke(M: TThreadMethod); begin TThread.Synchronize(nil, M); end; // Контроллер изменил состояние — обновляем соответствующие виджеты. // Выполняется в UI-потоке (команды контроллера вызываются из UI-потока либо // маршалятся адаптерами). FSyncingFromController защищает от рекурсии, когда // программное изменение виджета триггерит его OnChange. procedure TMainForm.OnControllerState(Sender: TObject; Field: TRadioField); var i: Integer; begin FSyncingFromController := True; try case Field of rfMute: begin StyleButton(BtnMute, FController.FMuted); if FController.FMuted then BtnMute.Caption := 'UNMUTE' else BtnMute.Caption := 'MUTE'; BtnMute.Invalidate; end; rfNR: UpdateNRButton; rfNB: UpdateNBButton; rfSNB: UpdateSNBButton; rfANF: UpdateANFButton; rfAGCMode: for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); rfVolume: TrkVolume.Position := FController.FVolume; rfAGCTop: begin TrkAGC.Position := FController.FAGCTop; LblAGCTop.Caption := Format('%ddB', [FController.FAGCTop]); FSpecView.AGCTop := FController.FAGCTop; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; end; rfCTun: begin StyleButton(BtnCTun, FController.FCTun); FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; FSpecView.DrawWaterfall; PbSpectrum.Invalidate; PbWaterfall.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; rfWfAGC: FSpecView.WfAGCEnabled := FController.FWfAGCEnabled; rfWfNF: FSpecView.WfNFEnabled := FController.FWfNFEnabled; rfFMStepIdx: begin if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx); if FWebServer <> nil then FWebServer.FMStepIdx := FController.FFMStepIdx; if (FController.FMode = MODE_FM) and (FSpecView <> nil) then FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]; end; rfFMSQ: begin StyleButton(BtnFMSQ, FController.FFMSQOn); BtnFMSQ.Repaint; end; rfFMSQLevel: begin TrkFMSQ.Position := FController.FFMSQLevel; if LblFMSQ <> nil then LblFMSQ.Caption := IntToStr(FController.FFMSQLevel); end; rfFMCTCSS: begin StyleButton(BtnFMCTCSS, FController.FFMCTCSSOn); BtnFMCTCSS.Repaint; end; rfFMCTCSSTone: if FCTCSSDropDown <> nil then FCTCSSDropDown.SetItemIndex(FController.FFMCTCSSToneIdx); end; finally FSyncingFromController := False; end; end; procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction); begin // Сохраняем позицию здесь — окно ещё полностью видимо и стабильно. // В FormDestroy Qt-виджет может быть уже в нестабильном состоянии. SaveWindowBounds; // Останавливаем трансивер как при нажатии STOP if FController.FNetwork.Connected then begin if FController.FRunning then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True); FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0); FController.FRunning := False; UninhibitSleep; FController.FTransmitting := False; if FController.FWDSPReady then FController.FDSPEngine.SetTXRun(False); end; FController.FNetwork.Disconnect; end; CloseAction := caFree; end; // =========================================================================== // Build UI (без анонимных процедур и inline var) // =========================================================================== procedure TMainForm.BuildUI; const BTN_H = 26; BTN_SM = 24; LEFT_W = 232; TOP_VFO_W = 244; TOP_VFO_FREQ_H = 47; var i, X, Y, W: Integer; B: TFlatButton; SepLine: TPanel; function MakeBtn(Parent: TWinControl; const Cap: string; ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton; begin Result := TFlatButton.Create(Self); Result.Parent := Parent; Result.Caption := Cap; Result.Left := ALeft; Result.Top := ATop; Result.Width := AW; Result.Height := AH; Result.OnClick := Handler; StyleButton(Result, False); end; procedure MakeLbl(Parent: TWinControl; const Cap: string; ALeft, ATop: Integer); var L: TLabel; begin L := TLabel.Create(Self); L.Parent := Parent; L.Caption := Cap; L.Left := ALeft; L.Top := ATop; L.Font.Color := CLR_TEXTDIM; L.Font.Name := 'Courier New'; L.Font.Size := 7; end; begin // ---- Toolbar ---- PanelToolbar := TPanel.Create(Self); PanelToolbar.Parent := Self; PanelToolbar.Align := alTop; PanelToolbar.Height := 72; PanelToolbar.BevelOuter := bvNone; PanelToolbar.Color := CLR_PANEL; X := 4; BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick); BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick); BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick); // DISCOVER — синеватый акцент BtnDiscover.ClrNorm := TColor($00101828); BtnDiscover.ClrHot := TColor($00182038); BtnDiscover.ClrActive := TColor($00182038); BtnDiscover.ClrBorder := TColor($00304860); BtnDiscover.ClrText := TColor($0090B8D0); BtnDiscover.ClrTextAct := TColor($00B0D0F0); // START/STOP — зелёный тон в обоих состояниях BtnStartStop.ClrNorm := TColor($00101810); BtnStartStop.ClrBorder := TColor($00304830); BtnStartStop.ClrText := TColor($0090C090); // Линия-разделитель внутри PanelToolbar, прижата к его нижнему краю SepLine := TPanel.Create(Self); SepLine.Parent := PanelToolbar; SepLine.BevelOuter := bvNone; SepLine.Color := TColor($00606060); SepLine.SetBounds(16, PanelToolbar.Height - 10, PanelToolbar.Width - 32, 2); SepLine.Anchors := [akLeft, akRight, akBottom]; // ---- Status bar ---- StatusPanel := TMainStatusBar.Create(Self); StatusPanel.Parent := Self; // ---- Left panel ---- PanelLeft := TPanel.Create(Self); PanelLeft.Parent := Self; PanelLeft.Align := alLeft; PanelLeft.Width := LEFT_W; PanelLeft.BevelOuter := bvNone; Y := 0; // Compact top VFO group PanelTopVfoGroup := TPanel.Create(Self); PanelTopVfoGroup.Parent := PanelToolbar; PanelTopVfoGroup.SetBounds(0, 0, TOP_VFO_W * 2 + 64, 60); PanelTopVfoGroup.BevelOuter := bvNone; PanelTopVfoGroup.Color := TColor($00181818); // VFO A PanelTopVfoA := TPanel.Create(Self); PanelTopVfoA.Parent := PanelTopVfoGroup; PanelTopVfoA.SetBounds(0, 0, TOP_VFO_W, 60); PanelTopVfoA.BevelOuter := bvNone; PanelTopVfoA.Color := CLR_PANEL; PanelVfoA := TPanel.Create(Self); PanelVfoA.Parent := PanelTopVfoA; PanelVfoA.SetBounds(0, 0, TOP_VFO_W, TOP_VFO_FREQ_H); PanelVfoA.BevelOuter := bvNone; BtnTopVfoASelect := MakeBtn(PanelVfoA, 'A', 0, 5, 20, 18, BtnTopVfoASelectClick); BtnTopVfoASelect.Font.Name := 'Courier New'; BtnTopVfoASelect.Font.Size := 7; BtnTopVfoASelect.Font.Bold := True; BtnTopVfoASelect.ClrNorm := TColor($00303030); BtnTopVfoASelect.ClrHot := TColor($00404040); BtnTopVfoASelect.ClrActive := TColor($00404040); BtnTopVfoASelect.ClrBorder := TColor($00505050); BtnTopVfoASelect.ClrText := CLR_TEXTDIM; BtnTopVfoASelect.ClrTextAct := CLR_TEXT; BtnTopVfoATX := MakeBtn(PanelVfoA, 'TX', 0, 25, 20, 18, BtnTopVfoATXClick); BtnTopVfoATX.Font.Name := 'Courier New'; BtnTopVfoATX.Font.Size := 7; BtnTopVfoATX.Font.Bold := True; BtnTopVfoATX.ClrNorm := TColor($00303030); BtnTopVfoATX.ClrHot := TColor($00404040); BtnTopVfoATX.ClrActive := TColor($00404040); BtnTopVfoATX.ClrBorder := TColor($00505050); BtnTopVfoATX.ClrText := CLR_TEXTDIM; BtnTopVfoATX.ClrTextAct := CLR_TEXT; LblVfoALabel := TLabel.Create(Self); LblVfoALabel.Parent := PanelVfoA; LblVfoALabel.Caption := 'VFO-A'; LblVfoALabel.Left := 0; LblVfoALabel.Top := 0; LblVfoALabel.Font.Name := 'Courier New'; LblVfoALabel.Font.Size := 7; LblVfoALabel.Font.Color := CLR_TEXTDIM; LblVfoALabel.Visible := False; FreqDispA := TFreqDisplay.Create(Self); FreqDispA.Parent := PanelVfoA; FreqDispA.SetBounds(20, -2, TOP_VFO_W - 20, TOP_VFO_FREQ_H); FreqDispA.FontSize := TOP_VFO_FONT; FreqDispA.FontName := 'Courier New'; FreqDispA.Frequency := Round(FController.FVfoA); FreqDispA.ColorNormal := CLR_FREQ; FreqDispA.ColorHover := TColor($0040DDFF); FreqDispA.ColorDim := CLR_TEXTDIM; FreqDispA.MinMhzDigits := 3; FreqDispA.DimLeadingZeros := True; FreqDispA.CenterText := False; FreqDispA.OnChange := FreqDispAChanged; FreqDispA.OnClick := FreqDispAClick; // VFO B PanelTopVfoB := TPanel.Create(Self); PanelTopVfoB.Parent := PanelTopVfoGroup; PanelTopVfoB.SetBounds(0, 0, TOP_VFO_W, 60); PanelTopVfoB.BevelOuter := bvNone; PanelTopVfoB.Color := CLR_PANEL; PanelVfoB := TPanel.Create(Self); PanelVfoB.Parent := PanelTopVfoB; PanelVfoB.SetBounds(0, 0, TOP_VFO_W, TOP_VFO_FREQ_H); PanelVfoB.BevelOuter := bvNone; BtnTopVfoBSelect := MakeBtn(PanelVfoB, 'B', 0, 5, 20, 18, BtnTopVfoBSelectClick); BtnTopVfoBSelect.Font.Name := 'Courier New'; BtnTopVfoBSelect.Font.Size := 7; BtnTopVfoBSelect.Font.Bold := True; BtnTopVfoBSelect.ClrNorm := TColor($00303030); BtnTopVfoBSelect.ClrHot := TColor($00404040); BtnTopVfoBSelect.ClrActive := TColor($00404040); BtnTopVfoBSelect.ClrBorder := TColor($00505050); BtnTopVfoBSelect.ClrText := CLR_TEXTDIM; BtnTopVfoBSelect.ClrTextAct := CLR_TEXT; BtnTopVfoBTX := MakeBtn(PanelVfoB, 'TX', 0, 25, 20, 18, BtnTopVfoBTXClick); BtnTopVfoBTX.Font.Name := 'Courier New'; BtnTopVfoBTX.Font.Size := 7; BtnTopVfoBTX.Font.Bold := True; BtnTopVfoBTX.ClrNorm := TColor($00303030); BtnTopVfoBTX.ClrHot := TColor($00404040); BtnTopVfoBTX.ClrActive := TColor($00404040); BtnTopVfoBTX.ClrBorder := TColor($00505050); BtnTopVfoBTX.ClrText := CLR_TEXTDIM; BtnTopVfoBTX.ClrTextAct := CLR_TEXT; LblVfoBLabel := TLabel.Create(Self); LblVfoBLabel.Parent := PanelVfoB; LblVfoBLabel.Caption := 'VFO-B'; LblVfoBLabel.Left := 0; LblVfoBLabel.Top := 0; LblVfoBLabel.Font.Name := 'Courier New'; LblVfoBLabel.Font.Size := 7; LblVfoBLabel.Font.Color := CLR_TEXTDIM; LblVfoBLabel.Visible := False; FreqDispB := TFreqDisplay.Create(Self); FreqDispB.Parent := PanelVfoB; FreqDispB.SetBounds(20, -2, TOP_VFO_W - 20, TOP_VFO_FREQ_H); FreqDispB.FontSize := TOP_VFO_FONT; FreqDispB.FontName := 'Courier New'; FreqDispB.Frequency := Round(FController.FVfoB); FreqDispB.ColorNormal := CLR_FREQ_DIM; FreqDispB.ColorHover := TColor($0040DDFF); FreqDispB.ColorDim := CLR_TEXTDIM; FreqDispB.MinMhzDigits := 3; FreqDispB.DimLeadingZeros := True; FreqDispB.CenterText := False; FreqDispB.OnChange := FreqDispBChanged; FreqDispB.OnClick := FreqDispBClick; // VFO transfer buttons — горизонтальный ряд по центру PanelVfoButtons := TPanel.Create(Self); PanelVfoButtons.Parent := PanelTopVfoGroup; PanelVfoButtons.SetBounds(0, 0, 140, 20); PanelVfoButtons.BevelOuter := bvNone; PanelVfoButtons.Color := TColor($00181818); BtnVfoACopyB := MakeBtn(PanelVfoButtons, 'A>B', 0, 0, 44, 20, BtnVfoACopyBClick); BtnVfoSwap := MakeBtn(PanelVfoButtons, 'A<>B', 0, 0, 44, 20, BtnVfoSwapClick); BtnVfoBCopyA := MakeBtn(PanelVfoButtons, 'B>A', 0, 0, 44, 20, BtnVfoBCopyAClick); // Янтарный стиль, шрифт под размер кнопок StyleSpanButton(BtnVfoACopyB, False); StyleSpanButton(BtnVfoSwap, False); StyleSpanButton(BtnVfoBCopyA, False); BtnVfoACopyB.Font.Size := 7; BtnVfoSwap.Font.Size := 7; BtnVfoBCopyA.Font.Size := 7; LayoutTopVfoBlock; // TX controls PanelTX := TPanel.Create(Self); PanelTX.Parent := PanelLeft; PanelTX.SetBounds(0, Y, LEFT_W, 96); PanelTX.BevelOuter := bvNone; MakeLbl(PanelTX, 'TX', 4, 2); BtnMOX := MakeBtn(PanelTX, 'MOX', LeftPanelButtonLeft(LEFT_W, 4, 0), 16, LeftPanelButtonWidth(LEFT_W, 4, 0), BTN_SM, BtnMOXClick); BtnTUN := MakeBtn(PanelTX, 'TUN', LeftPanelButtonLeft(LEFT_W, 4, 1), 16, LeftPanelButtonWidth(LEFT_W, 4, 1), BTN_SM, BtnTUNClick); BtnDUP := MakeBtn(PanelTX, 'DUP', LeftPanelButtonLeft(LEFT_W, 4, 0), 44, LeftPanelButtonWidth(LEFT_W, 4, 0), BTN_SM, BtnDUPClick); BtnDUP.Tag := 0; StyleButton(BtnDUP, FController.FDisplayDuplex); LblDrv := TLabel.Create(Self); LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 77; LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM; LblDrv.Font.Name := 'Courier New'; LblDrv.Font.Size := 7; TrkDrive := TFlatSlider.Create(Self); TrkDrive.Parent := PanelTX; TrkDrive.Left := 34; TrkDrive.Top := 74; TrkDrive.Width := 150; TrkDrive.Height := 16; TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50; TrkDrive.OnChange := TrkDriveChange; Inc(Y, 100); // RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE PanelRXBlock := TPanel.Create(Self); PanelRXBlock.Parent := PanelLeft; PanelRXBlock.SetBounds(0, Y, LEFT_W, 96); PanelRXBlock.BevelOuter := bvNone; MakeLbl(PanelRXBlock, 'RX', 4, 2); BtnCTun := MakeBtn(PanelRXBlock, 'CTUN', LeftPanelButtonLeft(LEFT_W, 3, 0), 16, LeftPanelButtonWidth(LEFT_W, 3, 0), BTN_H, BtnCTunClick); BtnCTun.Tag := 0; StyleButton(BtnCTun, FController.FCTun); BtnChannels := MakeBtn(PanelRXBlock, 'Channel', LeftPanelButtonLeft(LEFT_W, 3, 1), 16, LeftPanelButtonWidth(LEFT_W, 3, 1), BTN_H, BtnChannelsClick); BtnChannels.OnMouseDown := BtnChannelsMouseDown; StyleButton(BtnChannels, False); FChannelsDropDown := TFlatDropDown.Create(BtnChannels, PanelLeft, 1, BTN_SM); FChannelsDropDown.OnSelect := OnChannelsDropDownSelect; RefreshChannelsDropDown; MakeLbl(PanelRXBlock, 'VOL', 4, 50); TrkVolume := TFlatSlider.Create(Self); TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46; TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 16; TrkVolume.Min := 0; TrkVolume.Max := 100; TrkVolume.Position := FController.FVolume; TrkVolume.OnChange := TrkVolumeChange; BtnNR := MakeBtn(PanelRXBlock, 'NR', LeftPanelButtonLeft(LEFT_W, 5, 0), 70, LeftPanelButtonWidth(LEFT_W, 5, 0), BTN_SM, BtnNRClick); BtnNB := MakeBtn(PanelRXBlock, 'NB', LeftPanelButtonLeft(LEFT_W, 5, 1), 70, LeftPanelButtonWidth(LEFT_W, 5, 1), BTN_SM, BtnNBClick); BtnSNB := MakeBtn(PanelRXBlock, 'SNB', LeftPanelButtonLeft(LEFT_W, 5, 2), 70, LeftPanelButtonWidth(LEFT_W, 5, 2), BTN_SM, BtnSNBClick); BtnANF := MakeBtn(PanelRXBlock, 'ANF', LeftPanelButtonLeft(LEFT_W, 5, 3), 70, LeftPanelButtonWidth(LEFT_W, 5, 3), BTN_SM, BtnANFClick); BtnMute := MakeBtn(PanelRXBlock, 'MUTE', LeftPanelButtonLeft(LEFT_W, 5, 4), 70, LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnMuteClick); Inc(Y, 100); // AGC controls PanelAGC := TPanel.Create(Self); PanelAGC.Parent := PanelLeft; PanelAGC.SetBounds(0, Y, LEFT_W, 70); PanelAGC.BevelOuter := bvNone; // AGC mode — 5 кнопок в ряд MakeLbl(PanelAGC, 'AGC', 4, 2); BtnAGCMode[0] := MakeBtn(PanelAGC, 'FAST', LeftPanelButtonLeft(LEFT_W, 5, 0), 16, LeftPanelButtonWidth(LEFT_W, 5, 0), BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag := 0; BtnAGCMode[1] := MakeBtn(PanelAGC, 'MED', LeftPanelButtonLeft(LEFT_W, 5, 1), 16, LeftPanelButtonWidth(LEFT_W, 5, 1), BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag := 1; BtnAGCMode[2] := MakeBtn(PanelAGC, 'SLOW', LeftPanelButtonLeft(LEFT_W, 5, 2), 16, LeftPanelButtonWidth(LEFT_W, 5, 2), BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag := 2; BtnAGCMode[3] := MakeBtn(PanelAGC, 'LONG', LeftPanelButtonLeft(LEFT_W, 5, 3), 16, LeftPanelButtonWidth(LEFT_W, 5, 3), BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag := 3; BtnAGCMode[4] := MakeBtn(PanelAGC, 'OFF', LeftPanelButtonLeft(LEFT_W, 5, 4), 16, LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag := 4; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); // AGC level slider MakeLbl(PanelAGC, 'THRESH', 4, 44); TrkAGC := TFlatSlider.Create(Self); TrkAGC.Parent := PanelAGC; TrkAGC.Left := 56; TrkAGC.Top := 40; TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 16; TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → −20..-120 dBm TrkAGC.Position := FController.FAGCTop; TrkAGC.Reversed := True; TrkAGC.OnChange := TrkAGCChange; LblAGCTop := TLabel.Create(Self); LblAGCTop.Parent := PanelAGC; LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 44; LblAGCTop.Caption := Format('-%ddB', [FController.FAGCTop]); LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New'; LblAGCTop.Font.Size := 7; Inc(Y, 74); // Band selector. Высота резервирует 2 дополнительных ряда под XVTR-кнопки // (создаются динамически в RebuildXvtrButtons). В каждом ряду до 6 кнопок, // итого до 12 XVTR-кнопок (наш CFG_XVTR_COUNT = 8 умещается с запасом). PanelBands := TPanel.Create(Self); PanelBands.Parent := PanelLeft; PanelBands.SetBounds(0, Y, LEFT_W, 74); PanelBands.BevelOuter := bvNone; MakeLbl(PanelBands, 'BAND', 4, 2); for i := 0 to BAND_COUNT - 1 do begin B := MakeBtn(PanelBands, BAND_NAMES[i], LeftPanelButtonLeft(LEFT_W, 6, i mod 6), 16 + (i div 6) * 27, LeftPanelButtonWidth(LEFT_W, 6, i mod 6), BTN_SM, BtnBandClick); B.Tag := i; BtnBand[i] := B; end; // XVTR кнопки создаются позже (в RebuildXvtrButtons), их Tag = -(idx+1) for i := 0 to CFG_XVTR_COUNT - 1 do BtnXvtrBand[i] := nil; Inc(Y, 76); // Mode selector PanelMode := TPanel.Create(Self); PanelMode.Parent := PanelLeft; PanelMode.SetBounds(0, Y, LEFT_W, 74); PanelMode.BevelOuter := bvNone; MakeLbl(PanelMode, 'MODE', 4, 2); for i := 0 to MODE_COUNT - 1 do begin B := MakeBtn(PanelMode, MODE_NAMES[i], LeftPanelButtonLeft(LEFT_W, 4, i mod 4), 16 + (i div 4) * 27, LeftPanelButtonWidth(LEFT_W, 4, i mod 4), BTN_SM, BtnModeClick); B.Tag := i; BtnMode[i] := B; StyleButton(B, i = FController.FMode); end; Inc(Y, 76); // Filter selector (10 кнопок в 2 ряда по 5) PanelFilter := TPanel.Create(Self); PanelFilter.Parent := PanelLeft; PanelFilter.SetBounds(0, Y, LEFT_W, 74); PanelFilter.BevelOuter := bvNone; MakeLbl(PanelFilter, 'FILTER', 4, 2); for i := 0 to FILT_COUNT - 1 do begin B := MakeBtn(PanelFilter, '---', LeftPanelButtonLeft(LEFT_W, 5, i mod 5), 16 + (i div 5) * 27, LeftPanelButtonWidth(LEFT_W, 5, i mod 5), BTN_SM, BtnFilterClick); B.Tag := i; BtnFilter[i] := B; end; Inc(Y, 76); // FM Squelch panel (visible only in FM mode, above CTCSS) PanelFMSQ := TPanel.Create(Self); PanelFMSQ.Parent := PanelLeft; PanelFMSQ.SetBounds(0, Y, LEFT_W, 50); PanelFMSQ.BevelOuter := bvNone; PanelFMSQ.Visible := False; MakeLbl(PanelFMSQ, 'SQL', 4, 2); W := LeftPanelButtonWidth(LEFT_W, 4, 0); BtnFMSQ := MakeBtn(PanelFMSQ, 'SQL', LeftPanelButtonLeft(LEFT_W, 4, 0), 16, W, BTN_SM, BtnFMSQClick); StyleButton(BtnFMSQ, False); // Slider: vertically centred with button text (button top=16, h=24 → center=28) // Reserve 28px on the right for the numeric label TrkFMSQ := TFlatSlider.Create(Self); TrkFMSQ.Parent := PanelFMSQ; TrkFMSQ.Left := BtnFMSQ.Left + BtnFMSQ.Width + 3; TrkFMSQ.Top := 21; // 28 - 14/2 = 21 TrkFMSQ.Width := LEFT_W - TrkFMSQ.Left - 30 - 4; // leave 30px for label + margins TrkFMSQ.Height := 14; TrkFMSQ.Min := 0; TrkFMSQ.Max := 100; TrkFMSQ.Position := FController.FFMSQLevel; TrkFMSQ.OnChange := TrkFMSQChange; // Numeric value label to the right of the slider LblFMSQ := TLabel.Create(Self); LblFMSQ.Parent := PanelFMSQ; LblFMSQ.Left := TrkFMSQ.Left + TrkFMSQ.Width + 3; LblFMSQ.Top := 21; LblFMSQ.Width := 26; LblFMSQ.Caption := IntToStr(FController.FFMSQLevel); LblFMSQ.Font.Color := CLR_TEXT; LblFMSQ.Font.Name := 'Courier New'; LblFMSQ.Font.Size := 8; // FM CTCSS panel (visible only when FM mode is active) PanelFMCTCSS := TPanel.Create(Self); PanelFMCTCSS.Parent := PanelLeft; PanelFMCTCSS.SetBounds(0, Y, LEFT_W, 50); PanelFMCTCSS.BevelOuter := bvNone; PanelFMCTCSS.Visible := False; MakeLbl(PanelFMCTCSS, 'CTCSS', 4, 2); BtnFMCTCSS := MakeBtn(PanelFMCTCSS, 'CTCSS', LeftPanelButtonLeft(LEFT_W, 3, 0), 16, LeftPanelButtonWidth(LEFT_W, 3, 0), BTN_H, BtnFMCTCSSClick); StyleButton(BtnFMCTCSS, False); BtnFMCTCSSTone := MakeBtn(PanelFMCTCSS, CTCSS_NAMES[0], LeftPanelButtonLeft(LEFT_W, 3, 1), 16, LEFT_W - LEFT_PANEL_BTN_PAD - LeftPanelButtonLeft(LEFT_W, 3, 1), BTN_H, BtnFMCTCSSToneClick); StyleButton(BtnFMCTCSSTone, False); FCTCSSDropDown := TFlatDropDown.Create(BtnFMCTCSSTone, PanelLeft, 4, BTN_SM); FCTCSSDropDown.SetItems(CTCSS_NAMES); FCTCSSDropDown.SetItemIndex(FController.FFMCTCSSToneIdx); FCTCSSDropDown.OnSelect := OnCTCSSDropDownSelect; // FM Step panel (visible only in FM mode, between Filter and SQL) PanelFMStep := TPanel.Create(Self); PanelFMStep.Parent := PanelLeft; PanelFMStep.SetBounds(0, Y, LEFT_W, 50); PanelFMStep.BevelOuter := bvNone; PanelFMStep.Visible := False; MakeLbl(PanelFMStep, 'STEP', 4, 2); BtnFMStep := MakeBtn(PanelFMStep, 'STEP', LeftPanelButtonLeft(LEFT_W, 3, 0), 16, LeftPanelButtonWidth(LEFT_W, 3, 0), BTN_H, BtnFMStepClick); StyleButton(BtnFMStep, FController.FFMStepOn); BtnFMStepSel := MakeBtn(PanelFMStep, FM_STEP_NAMES[FController.FFMStepIdx], LeftPanelButtonLeft(LEFT_W, 3, 1), 16, LEFT_W - LEFT_PANEL_BTN_PAD - LeftPanelButtonLeft(LEFT_W, 3, 1), BTN_H, BtnFMStepSelClick); StyleButton(BtnFMStepSel, False); FStepDropDown := TFlatDropDown.Create(BtnFMStepSel, PanelLeft, FM_STEP_COUNT, BTN_SM); FStepDropDown.SetItems(FM_STEP_NAMES); FStepDropDown.SetItemIndex(FController.FFMStepIdx); FStepDropDown.OnSelect := OnStepDropDownSelect; // FM Repeater offset panel (visible only in FM mode, below CTCSS) PanelFMRpt := TPanel.Create(Self); PanelFMRpt.Parent := PanelLeft; PanelFMRpt.SetBounds(0, Y, LEFT_W, 50); PanelFMRpt.BevelOuter := bvNone; PanelFMRpt.Visible := False; MakeLbl(PanelFMRpt, 'RPT', 4, 2); BtnFMRptMinus := MakeBtn(PanelFMRpt, '-', LeftPanelButtonLeft(LEFT_W, 5, 0), 16, LeftPanelButtonWidth(LEFT_W, 5, 0), BTN_H, BtnFMRptMinusClick); StyleButton(BtnFMRptMinus, False); BtnFMRptPlus := MakeBtn(PanelFMRpt, '+', LeftPanelButtonLeft(LEFT_W, 5, 4), 16, LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_H, BtnFMRptPlusClick); StyleButton(BtnFMRptPlus, False); W := LeftPanelButtonLeft(LEFT_W, 5, 1); EdFMRptOffset := TFlatEdit.Create(Self); EdFMRptOffset.Parent := PanelFMRpt; EdFMRptOffset.Left := W; EdFMRptOffset.Top := 18; EdFMRptOffset.Width := LeftPanelButtonLeft(LEFT_W, 5, 4) - W - LEFT_PANEL_BTN_GAP; EdFMRptOffset.Height := BTN_H - 4; EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); EdFMRptOffset.OnChange := EdFMRptOffsetChange; // ---- Right panel ---- PanelRight := TPanel.Create(Self); PanelRight.Parent := Self; PanelRight.Align := alClient; PanelRight.BevelOuter := bvNone; PanelRight.DoubleBuffered := True; PanelRight.OnResize := RightPanelResize; FSampleRateOverlay := TSampleRateOverlay.Create(Self); FSampleRateOverlay.OnSpanSelect := OnSampleRateSelect; FSampleRateOverlay.OnHidePanel := OnSampleRateHidePanel; FSampleRateOverlay.OnInvalidate := OnSampleRateOverlayInvalidate; FSampleRateOverlay.SetCurrentRate(FController.FSampleRate); FSpecView.SampleRateOverlay := FSampleRateOverlay; // S-метр — внутри PanelToolbar, справа PanelSMeterRight := TPanel.Create(Self); PanelSMeterRight.Parent := PanelToolbar; PanelSMeterRight.BevelOuter := bvNone; PanelSMeterRight.Color := CLR_PANEL; PbSMeterRight := TPaintBox.Create(Self); PbSMeterRight.Parent := PanelSMeterRight; PbSMeterRight.Align := alClient; PbSMeterRight.OnPaint := FSpecView.PaintSMeterRight; PbSMeterRight.Color := CLR_PANEL; if FUseOpenGLSpectrum then begin PbWideband := TOpenGLControl.Create(Self); TOpenGLControl(PbWideband).AutoResizeViewport := False; TOpenGLControl(PbWideband).DoubleBuffered := True; TOpenGLControl(PbWideband).OnPaint := FWidebandView.Paint; TOpenGLControl(PbWideband).OnMouseDown := PbWidebandMouseDown; end else begin PbWideband := TPaintBox.Create(Self); TPaintBox(PbWideband).OnPaint := FWidebandView.Paint; TPaintBox(PbWideband).OnMouseDown := PbWidebandMouseDown; end; PbWideband.Parent := PanelRight; PbWideband.Visible := False; PbWidebandRuler := TPaintBox.Create(Self); PbWidebandRuler.Parent := PanelRight; PbWidebandRuler.OnPaint := FWidebandView.PaintRuler; PbWidebandRuler.Cursor := crDefault; PbWidebandRuler.Visible := False; if FUseOpenGLSpectrum then begin PbSpectrum := TOpenGLControl.Create(Self); TOpenGLControl(PbSpectrum).AutoResizeViewport := False; TOpenGLControl(PbSpectrum).DoubleBuffered := True; TOpenGLControl(PbSpectrum).OnPaint := FSpecView.PaintSpectrum; TOpenGLControl(PbSpectrum).OnMouseDown := PbSpectrumMouseDown; TOpenGLControl(PbSpectrum).OnDblClick := PbSpectrumDblClick; TOpenGLControl(PbSpectrum).OnMouseMove := PbSpectrumMouseMove; TOpenGLControl(PbSpectrum).OnMouseUp := PbSpectrumMouseUp; TOpenGLControl(PbSpectrum).OnMouseLeave := PbSpectrumMouseLeave; TSpectrumViewOpenGL(FSpecView).AttachControl(TOpenGLControl(PbSpectrum)); end else begin PbSpectrum := TPaintBox.Create(Self); TPaintBox(PbSpectrum).OnPaint := FSpecView.PaintSpectrum; TPaintBox(PbSpectrum).OnMouseDown := PbSpectrumMouseDown; TPaintBox(PbSpectrum).OnDblClick := PbSpectrumDblClick; TPaintBox(PbSpectrum).OnMouseMove := PbSpectrumMouseMove; TPaintBox(PbSpectrum).OnMouseUp := PbSpectrumMouseUp; TPaintBox(PbSpectrum).OnMouseLeave := PbSpectrumMouseLeave; end; PbSpectrum.Parent := PanelRight; PbRuler := TPaintBox.Create(Self); PbRuler.Parent := PanelRight; PbRuler.OnPaint := FSpecView.PaintRuler; PbRuler.Cursor := crDefault; FSpecView.PbRuler := PbRuler; // ---- Splitter между спектром+линейкой и водопадом ---- PanelSplitter := TPanel.Create(Self); PanelSplitter.Parent := PanelRight; PanelSplitter.BevelOuter := bvNone; PanelSplitter.Color := TColor($00303030); PanelSplitter.Cursor := crVSplit; PanelSplitter.Height := 5; PanelSplitter.OnMouseDown := SplitterMouseDown; PanelSplitter.OnMouseMove := SplitterMouseMove; PanelSplitter.OnMouseUp := SplitterMouseUp; if FUseOpenGLSpectrum then begin PbWaterfall := TOpenGLControl.Create(Self); TOpenGLControl(PbWaterfall).AutoResizeViewport := False; TOpenGLControl(PbWaterfall).DoubleBuffered := True; TOpenGLControl(PbWaterfall).OnPaint := FSpecView.PaintWaterfall; TOpenGLControl(PbWaterfall).OnMouseDown := PbWaterfallMouseDown; TOpenGLControl(PbWaterfall).OnMouseMove := PbWaterfallMouseMove; TOpenGLControl(PbWaterfall).OnMouseUp := PbWaterfallMouseUp; TSpectrumViewOpenGL(FSpecView).AttachWaterfallControl(TOpenGLControl(PbWaterfall)); end else begin PbWaterfall := TPaintBox.Create(Self); TPaintBox(PbWaterfall).OnPaint := FSpecView.PaintWaterfall; TPaintBox(PbWaterfall).OnMouseDown := PbWaterfallMouseDown; TPaintBox(PbWaterfall).OnMouseMove := PbWaterfallMouseMove; TPaintBox(PbWaterfall).OnMouseUp := PbWaterfallMouseUp; end; PbWaterfall.Parent := PanelRight; // Initial layout ResizeSpectrumPanels; end; // =========================================================================== // Splitter — перетаскивание границы спектр/водопад // Обновление размеров происходит только при отпускании мыши (MouseUp), // чтобы исключить фризы во время перетаскивания. // Во время перетаскивания рисуем только призрак-линию на PanelSplitter. // =========================================================================== procedure TMainForm.SplitterMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var P: TPoint; begin if Button <> mbLeft then Exit; FSplitterDrag := True; FSplitterSH0 := FSpectrumHeight; // Y в координатах PanelRight P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y))); FSplitterDragY0 := P.Y; PanelSplitter.Color := TColor($005050A0); // подсветка при захвате {$IFDEF WINDOWS} SetCapture(PanelSplitter.Handle); {$ENDIF} end; procedure TMainForm.SplitterMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var P: TPoint; DeltaY: Integer; NewSH: Integer; TopOff, AvailH: Integer; RULER_H: Integer; const SPLITTER_H = 5; MIN_SH = 60; MIN_WH = 40; begin RULER_H := MulDiv(18, Screen.PixelsPerInch, 96); if not FSplitterDrag then Exit; P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y))); DeltaY := P.Y - FSplitterDragY0; TopOff := 0; AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H; if AvailH <= 0 then Exit; NewSH := FSplitterSH0 + DeltaY; if NewSH < MIN_SH then NewSH := MIN_SH; if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH; // Только двигаем сплиттер визуально — без пересчёта битмапов PanelSplitter.Top := TopOff + NewSH + RULER_H; PbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H; PbWaterfall.Height := AvailH - NewSH; end; procedure TMainForm.SplitterMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var P: TPoint; DeltaY: Integer; NewSH: Integer; TopOff, AvailH: Integer; RULER_H: Integer; const SPLITTER_H = 5; MIN_SH = 60; MIN_WH = 40; begin RULER_H := MulDiv(18, Screen.PixelsPerInch, 96); if not FSplitterDrag then Exit; FSplitterDrag := False; {$IFDEF WINDOWS} ReleaseCapture; {$ENDIF} PanelSplitter.Color := TColor($00303030); // обычный цвет P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y))); DeltaY := P.Y - FSplitterDragY0; TopOff := 0; AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H; if AvailH <= 0 then Exit; NewSH := FSplitterSH0 + DeltaY; if NewSH < MIN_SH then NewSH := MIN_SH; if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH; // Сохраняем новое соотношение и делаем полный пересчёт с перерисовкой FSplitterRatio := NewSH / AvailH; ResizeSpectrumPanels; end; // =========================================================================== // Resize handler (normal method instead of anonymous procedure) // =========================================================================== procedure TMainForm.RightPanelResize(Sender: TObject); begin ResizeSpectrumPanels; end; procedure TMainForm.LayoutTopVfoBlock; const MARGIN = 4; TOP_OFFSET = 2; TOP_VFO_FREQ_H = 47; VFO_BTN_FREQ_GAP = 4; VFO_FREQ_LEFT_PAD = 2; VFO_FREQ_RIGHT_PAD = 8; XFER_BTN_W = 44; XFER_BTN_H = 20; XFER_BTN_GAP = 4; XFER_PANEL_W = XFER_BTN_W * 3 + XFER_BTN_GAP * 2; // = 140 XFER_SIDE_GAP = 14; // минимальный зазор между VFO и кнопками трансфера SETTINGS_GAP = 8; MAX_SPREAD = 100; // максимальный дополнительный отступ с каждой стороны при растяжении // VFO-B получает левый отступ = правому паддингу VFO-A для симметрии зазоров VFO_B_LEFT_PAD = VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD; // = 10 var BlockH, FreqTop, CenterY, SafeLeft, SafeRight, GroupLeft, GroupTop: Integer; RightW, SMW, SMLeft, AvailW, MinGroupW, ExtraPerSide, DynGap: Integer; TopVfoAW, TopVfoBW, NeedFreqAW, NeedFreqBW, FreqTextW, GroupW: Integer; XferX, XferY: Integer; begin if (PanelToolbar = nil) or (PanelTopVfoGroup = nil) or (PanelTopVfoA = nil) or (PanelTopVfoB = nil) then Exit; Canvas.Font.Name := FreqDispB.FontName; Canvas.Font.Size := FreqDispB.FontSize; Canvas.Font.Style := [fsBold]; FreqDispA.Left := BtnTopVfoASelect.Width + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD; FreqDispB.Left := VFO_B_LEFT_PAD + BtnTopVfoBSelect.Width + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD; FreqTextW := Canvas.TextWidth(StringOfChar('0', Max(3, FFreqMhzDigits)) + '.000.000'); NeedFreqAW := FreqDispA.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD; NeedFreqBW := FreqDispB.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD; TopVfoAW := NeedFreqAW; TopVfoBW := NeedFreqBW; // Минимальная ширина компактной группы MinGroupW := TopVfoAW + XFER_SIDE_GAP + XFER_PANEL_W + XFER_SIDE_GAP + TopVfoBW; SafeLeft := MARGIN; if BtnSettings <> nil then SafeLeft := BtnSettings.Left + BtnSettings.Width + SETTINGS_GAP; SafeRight := ClientWidth - MARGIN; if PanelLeft <> nil then begin RightW := ClientWidth - PanelLeft.Width; if RightW > 200 then begin SMW := Round(RightW * SMETER_WIDTH_RATIO); SMLeft := ClientWidth - SMW - SMETER_MARGIN; SafeRight := Min(SafeRight, SMLeft - TOP_SMETER_GAP); end; end; AvailW := SafeRight - SafeLeft; if AvailW < MinGroupW then begin PanelTopVfoGroup.Visible := False; Exit; end; // Динамический зазор: растёт с шириной окна, но не больше MAX_SPREAD ExtraPerSide := Min((AvailW - MinGroupW) div 2, MAX_SPREAD); DynGap := XFER_SIDE_GAP + ExtraPerSide; GroupW := TopVfoAW + DynGap + XFER_PANEL_W + DynGap + TopVfoBW; BlockH := PanelToolbar.Height - (MARGIN * 2) - 8; if BlockH < 56 then BlockH := 56; // Центрируем группу в доступном пространстве GroupLeft := SafeLeft + (AvailW - GroupW) div 2; GroupTop := TOP_OFFSET; CenterY := PanelToolbar.Height div 2 - GroupTop; PanelTopVfoGroup.SetBounds(GroupLeft, GroupTop, GroupW, BlockH); PanelTopVfoGroup.Visible := True; // VFO-A: левый край группы PanelTopVfoA.SetBounds(0, 0, TopVfoAW, BlockH); PanelTopVfoA.Visible := True; PanelVfoA.SetBounds(0, 0, TopVfoAW, BlockH); // A выше оси, TX ниже; 1px зазор с каждой стороны BtnTopVfoASelect.SetBounds(0, CenterY - BtnTopVfoASelect.Height - 1, BtnTopVfoASelect.Width, BtnTopVfoASelect.Height); BtnTopVfoATX.SetBounds(0, CenterY + 1, BtnTopVfoATX.Width, BtnTopVfoATX.Height); FreqTop := CenterY - (TOP_VFO_FREQ_H div 2); FreqDispA.SetBounds(FreqDispA.Left, FreqTop, FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H); // Кнопки трансфера: горизонтальный ряд, вертикально по оси XferX := TopVfoAW + DynGap; XferY := CenterY - (XFER_BTN_H div 2); PanelVfoButtons.SetBounds(XferX, XferY, XFER_PANEL_W, XFER_BTN_H); BtnVfoACopyB.SetBounds(0, 0, XFER_BTN_W, XFER_BTN_H); BtnVfoSwap.SetBounds(XFER_BTN_W + XFER_BTN_GAP, 0, XFER_BTN_W, XFER_BTN_H); BtnVfoBCopyA.SetBounds((XFER_BTN_W + XFER_BTN_GAP) * 2, 0, XFER_BTN_W, XFER_BTN_H); // VFO-B: правый край группы PanelTopVfoB.SetBounds(TopVfoAW + DynGap + XFER_PANEL_W + DynGap, 0, TopVfoBW, BlockH); PanelTopVfoB.Visible := True; PanelVfoB.SetBounds(0, 0, TopVfoBW, BlockH); BtnTopVfoBSelect.SetBounds(VFO_B_LEFT_PAD, CenterY - BtnTopVfoBSelect.Height - 1, BtnTopVfoBSelect.Width, BtnTopVfoBSelect.Height); BtnTopVfoBTX.SetBounds(VFO_B_LEFT_PAD, CenterY + 1, BtnTopVfoBTX.Width, BtnTopVfoBTX.Height); FreqTop := CenterY - (TOP_VFO_FREQ_H div 2); FreqDispB.SetBounds(FreqDispB.Left, FreqTop, FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H); end; procedure TMainForm.ResizeSMeter; // PanelSMeterRight на главной форме (Parent=Self). // По Y: от самого верха формы до низа тулбара. var FW: Integer; RightW: Integer; // ширина PanelRight SMW: Integer; // ширина S-метра SMLeft: Integer; SMTop: Integer; SMBot: Integer; begin if (PanelSMeterRight = nil) or (PanelToolbar = nil) or (PanelLeft = nil) then Exit; LayoutTopVfoBlock; FW := ClientWidth; // Ширина зоны спектра (PanelRight) RightW := FW - PanelLeft.Width; if RightW < 200 then Exit; // Ширина S-метра = 32% от PanelRight SMW := Round(RightW * SMETER_WIDTH_RATIO); // X: прижат к правому краю PanelToolbar SMLeft := FW - SMW - SMETER_MARGIN; SMTop := SMETER_MARGIN; SMBot := PanelToolbar.Height - SMETER_MARGIN - 9; PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop); end; function TMainForm.UpdateWidebandFrequencyView: Boolean; var E: TXvtrEntry; SrcStart, SrcEnd: Double; MarkerChanged, ViewChanged: Boolean; begin Result := False; if FWidebandView = nil then Exit; if FController.FActiveVfo = 0 then MarkerChanged := FWidebandView.SetMarkerHz(FController.FVfoA) else MarkerChanged := FWidebandView.SetMarkerHz(FController.FVfoB); if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then begin E := FController.FXvtrSettings.Entries[FController.FCurrentXvtr]; SrcStart := E.FreqBegin - E.LOOffset + E.LOError; SrcEnd := E.FreqEnd - E.LOOffset + E.LOError; ViewChanged := FWidebandView.SetFrequencyView(E.FreqBegin, E.FreqEnd, SrcStart, SrcEnd); end else ViewChanged := FWidebandView.SetFrequencyView(0.0, 61440000.0, 0.0, 61440000.0); Result := MarkerChanged or ViewChanged; end; procedure TMainForm.ResizeSpectrumPanels; const SPLITTER_H = 5; WB_H = 118; MIN_SH = 60; // минимальная высота спектра MIN_WH = 40; // минимальная высота водопада var RW, RH, SH, WH, TopOff, WideH, WideSpecH: Integer; AvailH: Integer; RULER_H: Integer; begin RULER_H := MulDiv(18, Screen.PixelsPerInch, 96); if PanelRight = nil then Exit; if Assigned(FSpecView) then SyncSpecViewFreq; RW := PanelRight.ClientWidth; RH := PanelRight.ClientHeight; UpdateWidebandFrequencyView; WideH := 0; WideSpecH := 0; if FShowWideband then WideH := Min(MulDiv(WB_H, Screen.PixelsPerInch, 96), Max(40, RH div 2)); if WideH > 0 then WideSpecH := Max(1, WideH - RULER_H); TopOff := WideH; if PbWideband <> nil then begin PbWideband.SetBounds(0, 0, RW, WideSpecH); PbWideband.Visible := FShowWideband and (WideSpecH > 0); if FShowWideband and (RW > 0) and (WideSpecH > 0) then begin FWidebandView.SetBitmapSize(RW, WideSpecH); PbWideband.Invalidate; end; end; if PbWidebandRuler <> nil then begin PbWidebandRuler.SetBounds(0, WideSpecH, RW, IfThen(FShowWideband and (WideH > 0), RULER_H, 0)); PbWidebandRuler.Visible := FShowWideband and (WideH > 0); if FShowWideband and (RW > 0) and (WideH > 0) then begin FWidebandView.SetRulerSize(RW, RULER_H); PbWidebandRuler.Invalidate; end; end; // S-метр позиционируется отдельно в ResizeSMeter ResizeSMeter; // ---- Оба скрыты ---- if (not FShowSpectrum) and (not FShowWaterfall) then begin PbSpectrum.Visible := False; PbRuler.Visible := False; PanelSplitter.Visible := False; PbWaterfall.Visible := False; FSpectrumWidth := RW; FSpectrumHeight := 0; FWaterfallHeight := 0; PositionSampleRateOverlay; Exit; end; // ---- Только спектр (водопад скрыт) ---- if FShowSpectrum and (not FShowWaterfall) then begin AvailH := RH - TopOff - RULER_H; if AvailH < MIN_SH then AvailH := MIN_SH; SH := AvailH; WH := 0; PbSpectrum.SetBounds(0, TopOff, RW, SH); PbSpectrum.Visible := True; PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H); PbRuler.Visible := True; PanelSplitter.Visible := False; PbWaterfall.Visible := False; PositionSampleRateOverlay; FSpectrumWidth := RW; FSpectrumHeight := SH; FWaterfallHeight := 0; if RW > 0 then begin FSpecView.SetSpectrumBitmapSize(RW, SH); FSpecView.SetWaterfallBitmapSize(1, 1); FSpecView.SetRulerSize(RW, RULER_H); FSpecView.DrawSpectrum; PbSpectrum.Invalidate; PbRuler.Invalidate; end; Exit; end; // ---- Только водопад (спектр скрыт): линейка под водопадом ---- if (not FShowSpectrum) and FShowWaterfall then begin AvailH := RH - TopOff - RULER_H; if AvailH < MIN_WH then AvailH := MIN_WH; SH := 0; WH := AvailH; PbSpectrum.Visible := False; PanelSplitter.Visible := False; PbWaterfall.SetBounds(0, TopOff, RW, WH); PbWaterfall.Visible := True; PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H); PbRuler.Visible := True; PositionSampleRateOverlay; FSpectrumWidth := RW; FSpectrumHeight := 0; FWaterfallHeight := WH; if RW > 0 then begin FSpecView.SetSpectrumBitmapSize(1, 1); FSpecView.SetWaterfallBitmapSize(RW, WH); FSpecView.SetRulerSize(RW, RULER_H); FSpecView.DrawWaterfall; PbWaterfall.Invalidate; PbRuler.Invalidate; end; Exit; end; // ---- Оба видимы: стандартный режим со сплиттером ---- PbSpectrum.Visible := True; PbRuler.Visible := True; PanelSplitter.Visible := True; PbWaterfall.Visible := True; AvailH := RH - TopOff - RULER_H - SPLITTER_H; if AvailH < (MIN_SH + MIN_WH) then Exit; // Ограничиваем соотношение, чтобы каждая зона имела минимальный размер if FSplitterRatio < MIN_SH / AvailH then FSplitterRatio := MIN_SH / AvailH; if FSplitterRatio > 1.0 - MIN_WH / AvailH then FSplitterRatio := 1.0 - MIN_WH / AvailH; SH := Round(AvailH * FSplitterRatio); WH := AvailH - SH; if WH < MIN_WH then WH := MIN_WH; // Спектр PbSpectrum.SetBounds(0, TopOff, RW, SH); // Линейка частот — всегда прижата к низу спектра PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H); // Сплиттер — между линейкой и водопадом PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H); // Водопад — под сплиттером PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH); PositionSampleRateOverlay; // Обновляем переменные размеров FSpectrumWidth := RW; FSpectrumHeight := SH; FWaterfallHeight := WH; // Пересоздаём bitmap точно под новый размер if RW > 0 then begin if SH > 0 then FSpecView.SetSpectrumBitmapSize(RW, SH); if WH > 0 then FSpecView.SetWaterfallBitmapSize(RW, WH); FSpecView.SetRulerSize(RW, RULER_H); FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; PbWaterfall.Invalidate; end; end; // =========================================================================== // Dark Theme // =========================================================================== procedure TMainForm.ApplyDarkTheme; var T: TAppTheme; i: Integer; procedure DP(P: TPanel); begin P.Color := T.Panel; P.Font.Color := T.Text; end; procedure DL(L: TLabel; Dim: Boolean = True); begin if Dim then L.Font.Color := T.TextDim else L.Font.Color := T.Text; end; procedure DS(S: TFlatSlider); begin S.SetThemeColors(T.SliderBG, T.SliderTrackEmpty, T.SliderTrackFill, T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr); end; begin if FLightTheme then T := LightTheme else T := DarkTheme; Color := T.BG; Font.Color := T.Text; DP(PanelToolbar); DP(PanelLeft); DP(PanelTopVfoGroup); DP(PanelTopVfoA); DP(PanelTopVfoB); DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons); DP(PanelBands); DP(PanelMode); DP(PanelFilter); if PanelFMSQ <> nil then DP(PanelFMSQ); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMStep <> nil then DP(PanelFMStep); if PanelFMRpt <> nil then DP(PanelFMRpt); if EdFMRptOffset <> nil then EdFMRptOffset.SetAppTheme(T); DP(PanelRXBlock); DP(PanelAGC); DP(PanelTX); DP(PanelRight); if PbSpectrum is TPaintBox then TPaintBox(PbSpectrum).Color := T.BG; if PbWaterfall is TPaintBox then TPaintBox(PbWaterfall).Color := T.BG; if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel; if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel; ApplyStatusTheme(T); // Метки DL(LblAGCTop, False); DL(LblDrv); if LblFMSQ <> nil then DL(LblFMSQ, False); // Кнопки тулбара BtnDiscover.ClrNorm := T.TbDiscoverNorm; BtnDiscover.ClrHot := T.TbDiscoverHot; BtnDiscover.ClrActive := T.TbDiscoverHot; BtnDiscover.ClrBorder := T.TbDiscoverBorder; BtnDiscover.ClrText := T.TbDiscoverText; BtnDiscover.ClrTextAct := T.TbDiscoverText; BtnDiscover.Invalidate; BtnStartStop.ClrNorm := T.TbStartNorm; BtnStartStop.ClrBorder := T.TbStartBorder; BtnStartStop.ClrText := T.TbStartText; BtnStartStop.Invalidate; // Обычные кнопки — все массивы for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active); for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active); for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active); for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active); StyleButton(BtnCTun, BtnCTun.Active); StyleButton(BtnDUP, BtnDUP.Active); if BtnFMSQ <> nil then StyleButton(BtnFMSQ, BtnFMSQ.Active); if TrkFMSQ <> nil then DS(TrkFMSQ); if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, BtnFMCTCSS.Active); if BtnFMCTCSSTone <> nil then StyleButton(BtnFMCTCSSTone, False); if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, BtnFMRptMinus.Active); if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active); if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel); if FStepDropDown <> nil then FStepDropDown.ApplyStyle(StyleButton, T.Panel); if BtnChannels <> nil then StyleButton(BtnChannels, False); if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel); StyleButton(BtnNR, BtnNR.Active); StyleButton(BtnNB, BtnNB.Active); StyleButton(BtnSNB, BtnSNB.Active); StyleButton(BtnANF, BtnANF.Active); StyleButton(BtnMute, BtnMute.Active); StyleButton(BtnMOX, BtnMOX.Active); StyleButton(BtnTUN, BtnTUN.Active); // VFO-copy StyleSpanButton(BtnVfoACopyB, False); StyleSpanButton(BtnVfoSwap, False); StyleSpanButton(BtnVfoBCopyA, False); // Ползунки DS(TrkAGC); DS(TrkVolume); DS(TrkDrive); // Спектр FSpecView.SetTheme(T); if Assigned(FWidebandView) then FWidebandView.SetTheme(T); // VFO display — вызывает UpdateVfoDisplay, который использует тему UpdateVfoDisplay; end; procedure TMainForm.SetStatusText(Index: Integer; const Text: string); begin if StatusPanel = nil then Exit; StatusPanel.SetStatusText(Index, Text); end; procedure TMainForm.SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean); begin SetStatusText(2, ConnText); SetStatusText(3, 'Supply --'); SetStatusText(4, 'RX idle'); SetStatusText(5, 'TX idle'); SetStatusText(6, 'SEQ --'); SetStatusText(7, 'PLL --'); if FSpecView <> nil then FSpecView.ADCOverloadVisible := False; if PbSpectrum <> nil then PbSpectrum.Invalidate; FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0); FSeqErrorCount := 0; FLastSeqErrorDDC := -1; FLastSeqErrorDelta := 0; FSeqOkStreak := 0; if ClearDevice then begin SetStatusText(0, 'Board --'); SetStatusText(1, 'IP --'); end; end; procedure TMainForm.ApplyStatusTheme(const T: TAppTheme); begin if StatusPanel = nil then Exit; StatusPanel.ApplyTheme(T); end; procedure TMainForm.SetLightTheme(V: Boolean); begin if FLightTheme = V then Exit; FLightTheme := V; ApplyDarkTheme; FController.FSettings.SaveTheme(V); FController.FSettings.Save; if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then TSettingsForm(FSettingsForm).LoadLightTheme(V); end; procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean); var T: TAppTheme; begin if FLightTheme then T := LightTheme else T := DarkTheme; B.Active := Active; B.ClrNorm := T.BtnNorm; B.ClrActive := T.BtnActive; B.ClrHot := T.BtnHot; if Active then B.ClrBorder := T.BtnBorderActive else B.ClrBorder := T.BtnBorderNorm; B.ClrText := T.BtnText; B.ClrTextAct := T.BtnTextActive; B.Font.Name := 'Courier New'; B.Font.Size := 8; end; procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean); var T: TAppTheme; begin if FLightTheme then T := LightTheme else T := DarkTheme; B.Active := Active; B.ClrNorm := T.SpanNorm; B.ClrActive := T.SpanActive; B.ClrHot := T.SpanActive; B.ClrBorder := T.SpanBorder; B.ClrText := T.SpanText; B.ClrTextAct := T.SpanText; B.Font.Name := 'Courier New'; B.Font.Size := 7; end; procedure TMainForm.PositionSampleRateOverlay; const MARGIN_X = 34; MARGIN_Y = 4; begin if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit; FSampleRateOverlay.SetBounds( MARGIN_X, MARGIN_Y, FSampleRateOverlay.Width, FSampleRateOverlay.Height); end; procedure TMainForm.OnSampleRateOverlayInvalidate(Sender: TObject); begin FSpectrumDirty := True; if Assigned(FSpecView) then begin FSpecView.SpectrumDirty := True; FSpecView.InvalidateOverlayCache; end; if Assigned(PbSpectrum) then PbSpectrum.Invalidate; end; // =========================================================================== // VFO // =========================================================================== function TMainForm.FormatFreq(Hz: Double): string; var Mhz: Int64; KHz, Rest: Integer; begin Mhz := Trunc(Hz / 1000000); KHz := Trunc((Hz - Mhz * 1000000) / 1000); Rest := Trunc(Hz) mod 1000; Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); end; procedure TMainForm.UpdateVfoDisplay; const TOP_VFO_FREQ_H = 47; var T: TAppTheme; begin if FLightTheme then T := LightTheme else T := DarkTheme; FreqDispA.Frequency := Round(FController.FVfoA); FreqDispB.Frequency := Round(FController.FVfoB); FreqDispA.FontSize := TOP_VFO_FONT; FreqDispB.FontSize := TOP_VFO_FONT; FreqDispA.Height := TOP_VFO_FREQ_H; FreqDispB.Height := TOP_VFO_FREQ_H; FreqDispA.ColorHover := T.FreqHover; FreqDispA.ColorHoverBG := T.FreqHoverBG; FreqDispA.ColorDim := T.FreqDimDot; FreqDispB.ColorHover := T.FreqHover; FreqDispB.ColorHoverBG := T.FreqHoverBG; FreqDispB.ColorDim := T.FreqDimDot; if FController.FActiveVfo = 0 then begin FreqDispA.ColorNormal := T.FreqActive; FreqDispB.ColorNormal := T.FreqDim; LblVfoALabel.Font.Color := T.FreqActive; LblVfoBLabel.Font.Color := T.TextDim; if BtnTopVfoASelect <> nil then begin BtnTopVfoASelect.ClrNorm := T.VfoSelActNorm; BtnTopVfoASelect.ClrActive := T.VfoSelActNorm; BtnTopVfoASelect.ClrHot := T.VfoSelActNorm; BtnTopVfoASelect.ClrBorder := T.VfoSelActBorder; BtnTopVfoASelect.ClrText := T.VfoSelActText; BtnTopVfoASelect.ClrTextAct := T.VfoSelActTextAct; BtnTopVfoASelect.Active := True; BtnTopVfoASelect.Invalidate; end; if BtnTopVfoBSelect <> nil then begin BtnTopVfoBSelect.ClrNorm := T.VfoSelInaNorm; BtnTopVfoBSelect.ClrActive := T.VfoSelInaNorm; BtnTopVfoBSelect.ClrHot := T.VfoSelInaNorm; BtnTopVfoBSelect.ClrBorder := T.VfoSelInaBorder; BtnTopVfoBSelect.ClrText := T.VfoSelInaText; BtnTopVfoBSelect.ClrTextAct := T.VfoSelInaTextAct; BtnTopVfoBSelect.Active := False; BtnTopVfoBSelect.Invalidate; end; end else begin FreqDispA.ColorNormal := T.FreqDim; FreqDispB.ColorNormal := T.FreqActive; LblVfoALabel.Font.Color := T.TextDim; LblVfoBLabel.Font.Color := T.FreqActive; if BtnTopVfoASelect <> nil then begin BtnTopVfoASelect.ClrNorm := T.VfoSelInaNorm; BtnTopVfoASelect.ClrActive := T.VfoSelInaNorm; BtnTopVfoASelect.ClrHot := T.VfoSelInaNorm; BtnTopVfoASelect.ClrBorder := T.VfoSelInaBorder; BtnTopVfoASelect.ClrText := T.VfoSelInaText; BtnTopVfoASelect.ClrTextAct := T.VfoSelInaTextAct; BtnTopVfoASelect.Active := False; BtnTopVfoASelect.Invalidate; end; if BtnTopVfoBSelect <> nil then begin BtnTopVfoBSelect.ClrNorm := T.VfoSelActNorm; BtnTopVfoBSelect.ClrActive := T.VfoSelActNorm; BtnTopVfoBSelect.ClrHot := T.VfoSelActNorm; BtnTopVfoBSelect.ClrBorder := T.VfoSelActBorder; BtnTopVfoBSelect.ClrText := T.VfoSelActText; BtnTopVfoBSelect.ClrTextAct := T.VfoSelActTextAct; BtnTopVfoBSelect.Active := True; BtnTopVfoBSelect.Invalidate; end; end; if BtnTopVfoATX <> nil then begin if not FController.FSplitTxB then begin BtnTopVfoATX.ClrNorm := T.VfoTxActNorm; BtnTopVfoATX.ClrHot := T.VfoTxActNorm; BtnTopVfoATX.ClrActive := T.VfoTxActNorm; BtnTopVfoATX.ClrBorder := T.VfoTxActBorder; BtnTopVfoATX.ClrText := T.VfoTxActText; BtnTopVfoATX.ClrTextAct := T.VfoTxActText; BtnTopVfoATX.Active := True; end else begin BtnTopVfoATX.ClrNorm := T.VfoTxInaNorm; BtnTopVfoATX.ClrHot := T.VfoTxInaNorm; BtnTopVfoATX.ClrActive := T.VfoTxInaNorm; BtnTopVfoATX.ClrBorder := T.VfoTxInaBorder; BtnTopVfoATX.ClrText := T.VfoTxInaText; BtnTopVfoATX.ClrTextAct := T.VfoTxInaText; BtnTopVfoATX.Active := False; end; BtnTopVfoATX.Invalidate; end; if BtnTopVfoBTX <> nil then begin if FController.FSplitTxB then begin BtnTopVfoBTX.ClrNorm := T.VfoTxActNorm; BtnTopVfoBTX.ClrHot := T.VfoTxActNorm; BtnTopVfoBTX.ClrActive := T.VfoTxActNorm; BtnTopVfoBTX.ClrBorder := T.VfoTxActBorder; BtnTopVfoBTX.ClrText := T.VfoTxActText; BtnTopVfoBTX.ClrTextAct := T.VfoTxActText; BtnTopVfoBTX.Active := True; end else begin BtnTopVfoBTX.ClrNorm := T.VfoTxInaNorm; BtnTopVfoBTX.ClrHot := T.VfoTxInaNorm; BtnTopVfoBTX.ClrActive := T.VfoTxInaNorm; BtnTopVfoBTX.ClrBorder := T.VfoTxInaBorder; BtnTopVfoBTX.ClrText := T.VfoTxInaText; BtnTopVfoBTX.ClrTextAct := T.VfoTxInaText; BtnTopVfoBTX.Active := False; end; BtnTopVfoBTX.Invalidate; end; FreqDispA.Invalidate; FreqDispB.Invalidate; LblVfoALabel.Invalidate; LblVfoBLabel.Invalidate; end; // =========================================================================== // Rendering helpers — delegated to TSpectrumView // =========================================================================== procedure TMainForm.InvalidateGridCache; begin FSpecView.InvalidateGridCache; end; procedure TMainForm.SyncSpecViewFreq; begin FSpecView.VfoA := FController.FVfoA; FSpecView.VfoB := FController.FVfoB; FSpecView.ActiveVfo := FController.FActiveVfo; FSpecView.CenterFreq := FController.FCenterFreq; FSpecView.SpanHz := FController.FSpanHz; FSpecView.Mode := FController.FMode; FSpecView.FilterBW := FController.FFilterBW; if FController.FMode = MODE_FM then FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx] else FSpecView.FMGridStepHz := 0; // TX overlay следует за активной TX-частотой (учитывает split). При CTUN // спектр TX будет отрисован на VFO, а не в центре дисплея. FSpecView.TXFreq := ActiveTXFreqHz; // Какой VFO привязан к TX (для split-полос фильтра): 1=B при FController.FSplitTxB, иначе FController.FActiveVfo. if FController.FSplitTxB then FSpecView.TXVfoIndex := 1 else FSpecView.TXVfoIndex := FController.FActiveVfo; if UpdateWidebandFrequencyView and FShowWideband and (PbWideband <> nil) then PbWideband.Invalidate; end; // =========================================================================== // Timers // =========================================================================== procedure TMainForm.MeterTimerTick(Sender: TObject); const AVG_ALPHA = 0.25; // EMA для сглаженного среднего уровня ZONE_DB = 6.0; // полуширина зоны вокруг среднего (дБ) // Адаптивный alpha: базовый + ускорение при большой разнице PEAK_BASE = 0.06; // базовая скорость пика MIN_BASE = 0.06; // базовая скорость минимума ACCEL_FACTOR = 0.018; // ускорение на каждый dB разницы MAX_ALPHA = 0.85; // максимальная скорость (не мгновенно) var PeakTarget, MinTarget: Double; PeakDiff, MinDiff: Double; PeakAlpha, MinAlpha: Double; begin if not FController.FRunning then begin if PbSMeterRight <> nil then PbSMeterRight.Invalidate; Exit; end; // Сглаженное среднее (EMA) FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FLastSMeter * AVG_ALPHA; // Цели для Peak и Min PeakTarget := Max(FLastSMeter, FSMeterAvg + ZONE_DB); MinTarget := Min(FLastSMeter, FSMeterAvg - ZONE_DB); // Адаптивный alpha: чем дальше от цели — тем быстрее догоняем PeakDiff := Abs(PeakTarget - FSMeterPeak); MinDiff := Abs(MinTarget - FSMeterMin); PeakAlpha := Min(MAX_ALPHA, PEAK_BASE + PeakDiff * ACCEL_FACTOR); MinAlpha := Min(MAX_ALPHA, MIN_BASE + MinDiff * ACCEL_FACTOR); FSMeterPeak := FSMeterPeak + PeakAlpha * (PeakTarget - FSMeterPeak); FSMeterMin := FSMeterMin + MinAlpha * (MinTarget - FSMeterMin); FSpecView.LastSMeter := FLastSMeter; FSpecView.SMeterPeak := FSMeterPeak; FSpecView.SMeterMin := FSMeterMin; FSpecView.LastFwdW := FLastFwdW; FSpecView.LastSWR := FLastSWR; FSpecView.Transmitting := FController.FTransmitting; if PbSMeterRight <> nil then PbSMeterRight.Invalidate; // PWR/SWR метки и бары обновляем здесь (10 Гц), а не в DoUpdateStatus — // HP Status пакеты идут ~150 раз/с, при таком rate цифры дёргаются. UpdateTXMeters; if FController.FWDSPReady then begin if FController.FTransmitting then begin if FController.FDSPEngine.TXMicSource = txmsSoundCard then SetStatusText(5, 'TX sound card') else SetStatusText(5, 'TX radio mic'); end else SetStatusText(5, 'TX idle'); end; end; procedure TMainForm.SpectrumTimerTick(Sender: TObject); var NowTick: QWord; ElapsedTick: QWord; WebStatusText, WebBoardText, WebIPText, WebSupplyText: string; WebPLLText, WebRXText, WebTXText, WebSeqText: string; begin // Синхронизируем размеры если размер панели изменился (после ресайза). // Важно: проверяем только видимые панели — когда спектр скрыт, // PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0, // что вызвало бы сброс буфера водопада на каждом тике. if (FSpectrumWidth = 0) or (FShowWideband and (PbWideband.Width <> FSpectrumWidth)) or (FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or (PbSpectrum.Height <> FSpectrumHeight))) or (FShowWaterfall and not FShowSpectrum and ((PbWaterfall.Width <> FSpectrumWidth) or (PbWaterfall.Height <> FWaterfallHeight))) then ResizeSpectrumPanels; if FSpectrumWidth <= 0 then Exit; SyncSpecViewFreq; // Проверка таймаута — трансивер не отвечает if FController.FRunning and FController.FNetwork.Running then begin NowTick := GetTickCount64; ElapsedTick := NowTick - FRXStartTime; // Ждём первый пакет 5 секунд после старта if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then begin BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); FController.FRunning := False; UninhibitSleep; FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0); SetRadioOfflineStatus('Hardware timeout'); BtnMOX.Enabled := False; Exit; end; // После первого пакета — следим чтобы поток не прерывался более 3 сек if (FRXLastPktTime > 0) and ((NowTick - FRXLastPktTime) > 3000) then begin BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); FController.FRunning := False; UninhibitSleep; FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0); SetRadioOfflineStatus('Connection lost'); BtnMOX.Enabled := False; Exit; end; end; if FController.FRunning then begin // Обновляем статус из таймера — без Synchronize в сетевом потоке. SetStatusText(4, 'RX running'); if FSeqErrorCount > 0 then SetStatusText(6, Format('SEQ ERR DDC%d', [FLastSeqErrorDDC])) else SetStatusText(6, 'SEQ OK'); if FController.FWDSPReady then begin FLastSMeter := FController.FDSPEngine.GetSMeterDBm; FController.FDSPEngine.SetSpectrumWidth(FSpectrumWidth); Inc(FAgcLineCounter); if FAgcLineCounter >= 6 then begin FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth); FSpecView.AGCThresh := FController.FDSPEngine.AGCThresh; FSpecView.AGCHangLevel := FController.FDSPEngine.AGCHangLevel; FAgcLineCounter := 0; end; end; FSpecView.WDSPReady := FController.FWDSPReady; // Обновляем оверлей если видим if Assigned(FVfoOverlay) and FVfoOverlay.Visible then begin FVfoOverlay.UpdateSMeter(FLastSMeter); FVfoOverlay.UpdateVfo(FController.FVfoA); PositionVfoOverlay; end; // Панорама и водопад теперь рисуются независимо по факту новых кадров. // Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще. if FSpectrumDirty then begin FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if FSpecView.NeedsRulerRedraw then PbRuler.Invalidate; FSpectrumDirty := False; end; if FWaterfallDirty then begin FSpecView.DrawWaterfall; PbWaterfall.Invalidate; FWaterfallDirty := False; end; if FWidebandDirty and FShowWideband then begin PbWideband.Invalidate; FWidebandDirty := False; end; end else begin // Не подключены — показываем чистый экран (без анимации) if not FController.FNetwork.Connected then begin // При первом старте показываем демо-спектр if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then begin FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight); FSpecView.ResetSpectrumBuf; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; end; end else begin // Подключены, но не запущены — статичный спектр (шум) FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FSpectrumDirty := False; end; end; // Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем. if (FSpectrumDirty or FWaterfallDirty) and (not FController.FRunning) then begin if FSpectrumDirty then FSpecView.DrawSpectrum; if FWaterfallDirty then FSpecView.DrawWaterfall; if FSpectrumDirty then PbSpectrum.Invalidate; if FSpecView.NeedsRulerRedraw then PbRuler.Invalidate; if FWaterfallDirty then PbWaterfall.Invalidate; FWaterfallDirty := False; FSpectrumDirty := False; end; // Пушим текущее состояние в веб-клиенты (если есть) if Assigned(FWebServer) then begin if FController.FNetwork.Connected then begin if FController.FRunning then WebStatusText := 'Running' else WebStatusText := 'Connected'; WebBoardText := 'Board: ' + BoardTypeName(FController.FNetwork.Device.BoardType); WebIPText := 'IP: ' + FController.FNetwork.Device.IPAddress; end else begin WebStatusText := 'Disconnected'; WebBoardText := 'Board --'; WebIPText := 'IP --'; end; if not FController.FRunning then begin WebSupplyText := 'Supply --'; WebPLLText := 'PLL --'; end else begin if FLastPLLLock then WebPLLText := 'PLL OK' else WebPLLText := 'PLL?'; if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then WebSupplyText := 'Supply n/a' else if FLastSupplyV >= 0 then begin if FLastSupplyA >= 0 then WebSupplyText := Format('Supply %.1fV %.1fA', [FLastSupplyV, FLastSupplyA]) else WebSupplyText := Format('Supply %.1fV', [FLastSupplyV]); end else WebSupplyText := 'Supply --'; end; if FController.FRunning then WebRXText := 'RX running' else WebRXText := 'RX idle'; if FController.FTuning then WebTXText := 'TX tune' else if FController.FTransmitting then WebTXText := 'TX active' else WebTXText := 'TX idle'; if FSeqErrorCount > 0 then WebSeqText := Format('SEQ ERR DDC%d', [FLastSeqErrorDDC]) else if FController.FRunning then WebSeqText := 'SEQ OK' else WebSeqText := 'SEQ --'; FWebServer.PushSpectrum( FSpectrumBuf, 1024, FWaterfallBuf, FLastSMeter, FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop, FController.FSpanHz, FController.FVolume, FController.FWfAGCEnabled, FController.FWfNFEnabled, FController.FCurrentBand, FController.FRunning and FController.FNetwork.Connected, FController.FRunning, FController.FMuted, FController.FCTun, FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FCenterFreq, FController.FFilter, FController.FVfoB, FController.FActiveVfo, FController.FTransmitting, FController.FDrivePercent, FController.FAtten, FController.FTuning, FController.FDisplayDuplex, FLastFwdW, FLastSWR, FController.FPAMaxPower, WebStatusText, WebBoardText, WebIPText, WebSupplyText, WebPLLText, WebRXText, WebTXText, WebSeqText); end; end; // =========================================================================== // Network callbacks → sync helpers // =========================================================================== procedure TMainForm.OnDeviceFound(const Dev: THPSDRDevice); var BoardName, Entry: string; Sync: TDeviceFoundSync; M: TThreadMethod; begin BoardName := BoardTypeName(Dev.BoardType); Entry := Format('%s %s FW:%d DDC:%d', [Dev.IPAddress, BoardName, Dev.FirmwareVersion, Dev.NumDDCs]); Sync := TDeviceFoundSync.Create(DoAddDevice, Dev, Entry); try M := Sync.Execute; TThread.Synchronize(nil, M); finally Sync.Free; end; end; procedure TMainForm.DoAddDevice(const Dev: THPSDRDevice; const Entry: string); var Idx: Integer; begin Idx := Length(FDevices); SetLength(FDevices, Idx + 1); FDevices[Idx].Dev := Dev; FDevices[Idx].Display := Entry; Inc(FDeviceCount); if Assigned(FDeviceDialog) then FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType); SetStatusText(2, Format('Found: %s', [Dev.IPAddress])); end; procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus); const // Порог fwd-мощности (W), ниже которого SWR не вычисляем — там всё ADC-шум. SWR_MIN_FWD_W = 1.0; // Верхняя граница SWR (выше — это всё равно «беда»). SWR_CAP = 9.9; var ExcPwr, FwdPwr, RevPwr: Word; SupplyV, SupplyA, FwdW, SWRV, Rho: Double; IsTx: Boolean; Sync: TStatusUISync; M: TThreadMethod; begin ExcPwr := (Status.ExciterPwr0Hi shl 8) or Status.ExciterPwr0Lo; FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; SupplyV := -1.0; SupplyA := -1.0; // Board 5 (Orion MkII): voltage on UserADC0 (bytes 57-58), // current on UserADC1 (bytes 55-56) — matching Thetis behaviour. // Other supported boards report voltage in SupplyVolts bytes and no current. if BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then begin if FController.FNetwork.Device.BoardType = 5 then SupplyV := ADCToSupplyVolts( (Status.UserADC0Hi shl 8) or Status.UserADC0Lo, FController.FNetwork.Device.BoardType) else SupplyV := ADCToSupplyVolts( (Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo, FController.FNetwork.Device.BoardType); if BoardSupportsSupplyCurrent(FController.FNetwork.Device.BoardType) then SupplyA := ADCToSupplyCurrent( (Status.UserADC1Hi shl 8) or Status.UserADC1Lo, FController.FNetwork.Device.BoardType); end; // FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick — // ExciterPwr это мощность TX, не уровень принятого сигнала. FwdW := ADCToWatts100(FwdPwr); // FController.FTransmitting — наш программный флаг (MOX/TUN/HW-PTT через ApplyMOX). // HPS_PTT-бит в StatusBits — это вход аппаратного PTT (footswitch/mic), // а не «трансивер сейчас передаёт», поэтому только на него полагаться нельзя. IsTx := FController.FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0); // SWR: правильная формула (1+ρ)/(1-ρ), считаем только когда есть значимая // прямая мощность — иначе ADC-шум RevPwr даёт случайные «прыжки» SWR. // Не на передаче — SWR не показываем (фиксируем 1.0), Fwd принудительно 0. if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then begin // ρ = sqrt(Pr/Pf). Поскольку P=V²/R и V пропорционально ADC, то // sqrt(Pr/Pf) = adc_r/adc_f напрямую — лишнего sqrt не нужно. Rho := RevPwr / FwdPwr; if Rho >= 0.99 then SWRV := SWR_CAP else begin SWRV := (1.0 + Rho) / (1.0 - Rho); if SWRV > SWR_CAP then SWRV := SWR_CAP; if SWRV < 1.0 then SWRV := 1.0; end; end else SWRV := 1.0; if not IsTx then FwdW := 0; Sync := TStatusUISync.Create(DoUpdateStatus, FwdW, SWRV, SupplyV, SupplyA, (Status.StatusBits and HPS_PLL_LOCKED) <> 0, (Status.StatusBits and HPS_PTT) <> 0, Status.ADCOverload); try M := Sync.Execute; TThread.Synchronize(nil, M); finally Sync.Free; end; end; procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV, SupplyA: Double; PLLLock, HWPTT: Boolean; ADCOverload: Byte); const // Thetis/piHPSDR PEP-style баллистика для FwdW и SWR: атака мгновенная // (peak-hold), спад медленный — стрелка «висит» на пике и плавно опускается. // Это маскирует огибающую SSB-голоса, которая на ADC прыгает на 10-20 дБ // внутри слога. На rate ~150 пакетов/с эти α дают τ_decay ≈ 350 мс / 500 мс. DECAY_FWD = 0.02; DECAY_SWR = 0.015; // Напряжение питания меняется медленно — обычное симметричное EMA. ALPHA_SUPPLY = 0.05; begin // Запоминаем значения; вывод в UI делает MeterTimerTick на 10 Гц. // Сама обработка здесь идёт на UI-потоке через Synchronize. // FwdW=0 / SWRV=1 — это «не на передаче»: снапим без decay, // чтобы метры моментально падали при отпускании PTT. if FwdW <= 0 then FLastFwdW := 0 else if FwdW > FLastFwdW then FLastFwdW := FwdW // attack: instant else FLastFwdW := DECAY_FWD * FwdW + (1.0 - DECAY_FWD) * FLastFwdW; if SWRV <= 1.0 then FLastSWR := 1.0 else if SWRV > FLastSWR then FLastSWR := SWRV // attack: instant else FLastSWR := DECAY_SWR * SWRV + (1.0 - DECAY_SWR) * FLastSWR; if SupplyV >= 0 then begin if FLastSupplyV < 0 then FLastSupplyV := SupplyV // первое значение — без сглаживания else FLastSupplyV := ALPHA_SUPPLY * SupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV; end else FLastSupplyV := -1.0; if SupplyA >= 0 then begin if FLastSupplyA < 0 then FLastSupplyA := SupplyA else FLastSupplyA := ALPHA_SUPPLY * SupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA; end else FLastSupplyA := -1.0; FLastPLLLock := PLLLock; if (FSpecView <> nil) and (FSpecView.ADCOverloadVisible <> (ADCOverload <> 0)) then begin FSpecView.ADCOverloadVisible := ADCOverload <> 0; if PbSpectrum <> nil then PbSpectrum.Invalidate; end; // Hardware PTT (foot switch / mic PTT) — обнаружение фронта (нужно делать // на rate HP Status, а не таймера, чтобы не пропустить короткое нажатие). if HWPTT <> FController.FHWPTTActive then begin FController.FHWPTTActive := HWPTT; if not FController.FTuning then begin if HWPTT then begin // Press: не перекрываем ручной MOX; запоминаем что мы включили TX. if not BtnMOX.Active then begin ApplyMOX(True); FController.FHWPTTStartedTX := True; end; end else begin // Release: выключаем TX только если он был включён через HWPTT. // Нельзя проверять BtnMOX.Active — ApplyMOX(True) сам его выставляет. if FController.FHWPTTStartedTX then begin FController.FHWPTTStartedTX := False; ApplyMOX(False); end; end; end; end; end; procedure TMainForm.UpdateTXMeters; // Вызывается из MeterTimerTick (10 Гц). Использует FLastFwdW/FLastSWR/ // FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с) // в DoUpdateStatus. begin if not FController.FRunning then begin SetStatusText(3, 'Supply --'); SetStatusText(7, 'PLL --'); Exit; end; if FLastPLLLock then SetStatusText(7, 'PLL OK') else SetStatusText(7, 'PLL?'); if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then begin SetStatusText(3, 'Supply n/a'); Exit; end; if FLastSupplyV >= 0 then begin if FLastSupplyA >= 0 then SetStatusText(3, Format('Supply %.1fV %.1fA', [FLastSupplyV, FLastSupplyA])) else SetStatusText(3, Format('Supply %.1fV', [FLastSupplyV])); end else SetStatusText(3, 'Supply --'); end; procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket); var SamplesPerFrame: Integer; Seq, ExpectedSeq: LongWord; Delta: Int64; begin // Вызывается из сетевого потока — UI не трогаем напрямую // Счётчик принятых пакетов 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; // 1. Подаём IQ данные в DSP — только с активного DDC if (DDCIndex = FActiveDDC) and FController.FWDSPReady then begin SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or Integer(Data.SamplesPerFrame[1]); if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback FController.FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame); end; // 2. Запоминаем последний seq — статус обновит таймер (без Synchronize в горячем пути) FLastDDCSeq := Seq; FLastDDCIndex := DDCIndex; end; procedure TMainForm.OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt; Count: Integer); begin if (not FShowWideband) or (FWidebandView = nil) then Exit; FWidebandView.SetSamples(Samples, Count); FWidebandDirty := True; end; procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord); begin SetStatusText(4, 'RX running'); end; procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket); begin // HW mic-стрим уходит в WDSP только если выбран источник Radio. // Иначе сэмплы игнорируются (mic берётся со звуковой карты). if FController.FTransmitting and FController.FWDSPReady and (FController.FDSPEngine.TXMicSource = txmsRadio) then FController.FDSPEngine.PushTXMicSamples16(Data.Samples, 64); end; // --------------------------------------------------------------------------- // TX settings — применение к WDSP, сборка DUC Specific, sound-card pull // --------------------------------------------------------------------------- function TMainForm.BuildMicLineSelectByte: Byte; // openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50: // bit 0 — Line In (1 = line-in вместо mic-in) // bit 1 — Mic Boost (+20 dB); не ставим когда Line In // bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!) // bit 3 — Tip/Ring (0=Tip, 1=Ring; Orion/OrionMkII) // bit 4 — Orion mic bias (1 = enable +5V bias) // bit 5 — Saturn XLR/balanced (не используем) begin Result := 0; if FController.FTXSettings.MicLineIn then Result := Result or $01 // bit 0: Line In else if FController.FTXSettings.MicBoost then Result := Result or $02; // bit 1: Boost только при Mic In if not FController.FTXSettings.MicPTTEnabled then Result := Result or $04; // bit 2: инверсия if FController.FTXSettings.MicTipRing then Result := Result or $08; // bit 3: Ring if FController.FTXSettings.MicBias then Result := Result or $10; // bit 4: bias end; procedure TMainForm.SendDUCSpecificFromSettings; var DUCPkt: TDUCSpecificPacket; RateKsps: Word; begin if not (FController.FNetwork.Connected and FController.FNetwork.Running) then Exit; FillChar(DUCPkt, SizeOf(DUCPkt), 0); // DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX). RateKsps := FController.FDSPEngine.TXSampleRate div 1000; DUCPkt.NumDACs := 1; DUCPkt.SidetoneLevel := 50; DUCPkt.SidetoneFreqHi := Hi(600); DUCPkt.SidetoneFreqLo := Lo(600); DUCPkt.KeyerSpeed := 20; DUCPkt.KeyerWeight := 50; DUCPkt.DUC0RateHi := Hi(RateKsps); DUCPkt.DUC0RateLo := Lo(RateKsps); DUCPkt.DUC0Bits := 24; DUCPkt.MicLineSelect := BuildMicLineSelectByte; // Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis). // При Mic In значение игнорируется железом, но шлём корректное для Line In. DUCPkt.LineInGain := Byte(EnsureRange(Round((FController.FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31)); // ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection). // Дублируем значение во все три ADC-байта: на однопотоковых платах активен только // ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2. DUCPkt.StepAtten0 := Byte(EnsureRange(FController.FTXSettings.AttOnTX, 0, 31)); DUCPkt.StepAtten1 := DUCPkt.StepAtten0; DUCPkt.StepAtten2 := DUCPkt.StepAtten0; FController.FNetwork.SendDUCSpecific(DUCPkt); end; function TMainForm.DefaultMicSource: TTXMicSource; // Mic source для MOX-кнопки и Web-TX: если настроена звуковая карта — берём с неё, // иначе используем HW mic-стрим от трансивера. // HW PTT всегда переключает на txmsRadio непосредственно в ApplyMOX. begin if FController.FAudioInDevName <> '' then Result := txmsSoundCard else Result := txmsRadio; end; procedure TMainForm.ApplyTXSettingsToDSP; begin if not FController.FWDSPReady then Exit; // Mic source — выставляем значение по умолчанию для MOX-кнопки; // при реальном PTT ApplyMOX скорректирует источник. FController.FDSPEngine.SetTXMicSource(DefaultMicSource); FController.FDSPEngine.SetMicGain(FController.FTXSettings.MicGainDB); FController.FDSPEngine.SetTXFilterFull(FController.FTXSettings.FilterLow, FController.FTXSettings.FilterHigh, FController.FTXSettings.FilterNC, FController.FTXSettings.FilterMP, FController.FTXSettings.FilterWindow); FController.FDSPEngine.SetTXCompressor(FController.FTXSettings.CompressorOn, FController.FTXSettings.CompressorGain); FController.FDSPEngine.SetTXLeveler(FController.FTXSettings.LevelerOn, FController.FTXSettings.LevelerTop, FController.FTXSettings.LevelerDecay); FController.FDSPEngine.SetTXALC(FController.FTXSettings.ALCOn, FController.FTXSettings.ALCMaxGain, FController.FTXSettings.ALCDecay); FController.FDSPEngine.SetTXPhaseRot(FController.FTXSettings.PhaseRotOn, FController.FTXSettings.PhaseRotStages, FController.FTXSettings.PhaseRotFreq); FController.FDSPEngine.SetTXEQ(FController.FTXSettings.EQOn, FController.FTXSettings.EQNumBands, FController.FTXSettings.EQGains, FController.FTXSettings.EQFreqs); FController.FDSPEngine.SetTXAMCarrierLevel(FController.FTXSettings.AMCarrierLevel); FController.FDSPEngine.SetTXFMParams(FController.FTXSettings.FMDeviation, FController.FTXSettings.FMLowCut, FController.FTXSettings.FMHighCut, FController.FTXSettings.FMEmphPosition); FController.FDSPEngine.SetTXCTCSS(FController.FTXSettings.CTCSSOn, FController.FTXSettings.CTCSSFreq); // TX display FController.FDSPEngine.SetTXFFTParams(FController.FTXSettings.TXFFTSize, FController.FTXSettings.TXWindowType); FController.FDSPEngine.SetTXSpectrumDisplay(FController.FTXSettings.TXSpecDetector, FController.FTXSettings.TXSpecAvgMode, FController.FTXSettings.TXSpecAvgTimeMS); FController.FDSPEngine.SetTXWaterfallDisplay(FController.FTXSettings.TXWfDetector, FController.FTXSettings.TXWfAvgMode, FController.FTXSettings.TXWfAvgTimeMS); end; function TMainForm.PullSoundCardMic(MaxN: Integer): Integer; // Колбэк, который TTXDSPThread зовёт при TXMicSource=txmsSoundCard. // Тянем только реально доступные сэмплы из FController.FAudioIn — иначе мы пушили бы // нули и mic-цепь в WDSP получала бы тишину вместо реального сигнала. // Читаем пачкой в локальный буфер и пушим одним вызовом — экономим // будилки семафора и предотвращаем подмену TX-блока тишиной. var i, n: Integer; Buf: array[0..2047] of Double; begin Result := 0; if (FController.FAudioIn = nil) or (not FController.FAudioIn.IsOpen) then Exit; n := FController.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] := FController.FAudioIn.ReadSample; if n > 0 then FController.FDSPEngine.PushTXMicSamplesD(Buf, n); Result := n; end; // =========================================================================== // Button handlers // =========================================================================== { Поток открытия — отдельный класс, без анонимных процедур } // Открываем WDSP в фоновом потоке чтобы не блокировать UI при нажатии START type TWDSPOpenThread = class(TThread) private FForm: TMainForm; FResult: Boolean; procedure SyncDone; protected procedure Execute; override; public constructor Create(AForm: TMainForm); end; constructor TWDSPOpenThread.Create(AForm: TMainForm); begin inherited Create(False); FForm := AForm; FResult := False; FreeOnTerminate := True; end; procedure TWDSPOpenThread.Execute; var M: TThreadMethod; begin try FResult := FForm.FController.FDSPEngine.Open; except FResult := False; end; M := SyncDone; TThread.Synchronize(nil, M); end; procedure TWDSPOpenThread.SyncDone; begin FForm.OnWDSPOpenDone(FResult); end; type TDiscoverThread = class(TThread) private FNet: THPSDRNetwork; FForm: TMainForm; protected procedure Execute; override; public constructor Create(ANet: THPSDRNetwork; AForm: TMainForm); end; constructor TDiscoverThread.Create(ANet: THPSDRNetwork; AForm: TMainForm); begin FNet := ANet; FForm := AForm; FreeOnTerminate := True; inherited Create(False); end; procedure TDiscoverThread.Execute; var Devs: THPSDRDeviceArray; Sync: TDDCSeqSync; M: TThreadMethod; begin Devs := FNet.Discover(2000); if Length(Devs) = 0 then begin // DDCIdx = -1 is the sentinel for "no device found" Sync := TDDCSeqSync.Create(FForm.DoUpdateDDCSeqOrNoDevice, -1, 0); try M := Sync.Execute; TThread.Synchronize(nil, M); finally Sync.Free; end; end; // Devices found are signalled via OnDeviceFound callback during Discover() end; { Перегруженный DoUpdateDDCSeq принимает -1 как «нет устройств» } procedure TMainForm.DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord); begin if DDCIdx = -1 then begin if Assigned(FDeviceDialog) then begin FDeviceDialog.ClearDiscovered; FDeviceDialog.AddDiscovered('', '-- no device found --'); end; SetStatusText(2, 'No hardware found'); end else SetStatusText(4, 'RX running'); end; procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer); begin if ASampleRate <= 0 then Exit; if Assigned(FController.FDSPEngine) then begin FController.FDSPEngine.Close; FreeAndNil(FController.FDSPEngine); end; FController.FWDSPReady := False; FController.FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); FController.FDSPEngine.OnAudio := OnAudioReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnTXIQ := OnTXIQReady; end; function TMainForm.EnsureWDSPWisdom: Boolean; var WisdomDir: string; WisdomPath: string; DirA: AnsiString; BuildThread: TWisdomBuildThread; Dlg: TWisdomProgressDialog; begin Result := True; if not Assigned(@WDSPwisdom) then Exit; WisdomDir := GetAppCfgDir; WisdomPath := WisdomDir + WDSP_WISDOM_FILE; if FileExists(WisdomPath) then begin // По рекомендации WDSP вызываем функцию на каждом старте: // если файл уже есть, библиотека просто импортирует wisdom. DirA := AnsiString(WisdomDir); WDSPwisdom(PAnsiChar(DirA)); Exit; end; BtnStartStop.Enabled := False; SetStatusText(2, 'Creating WDSP wisdom...'); BuildThread := TWisdomBuildThread.Create(WisdomDir); try if FLightTheme then Dlg := TWisdomProgressDialog.Create(Self, BuildThread, LightTheme) else Dlg := TWisdomProgressDialog.Create(Self, BuildThread, DarkTheme); try Dlg.ShowModal; finally Dlg.Free; end; BuildThread.WaitFor; if BuildThread.ErrorText <> '' then begin ShowMessage('Failed to create WDSP wisdom.' + LineEnding + BuildThread.ErrorText); Result := False; end else if not FileExists(WisdomPath) then begin ShowMessage('WDSP wisdom was not created.' + LineEnding + 'Expected file: ' + WisdomPath); Result := False; end; finally BuildThread.Free; BtnStartStop.Enabled := True; end; if Result then SetStatusText(2, 'DSP ready') else SetStatusText(2, 'WDSP wisdom missing'); end; procedure TMainForm.BtnDiscoverClick(Sender: TObject); begin // Открываем диалог выбора устройства if not Assigned(FDeviceDialog) then FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; if FLightTheme then FDeviceDialog.SetTheme(LightTheme) else FDeviceDialog.SetTheme(DarkTheme); FDeviceDialog.ClearResult; // сбрасываем старый выбор перед открытием FDeviceDialog.ShowModal; // Если пользователь нажал CONNECT — результат хранится в DialogResult, // START его заберёт и использует с приоритетом над autostart end; procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject); // Запускается когда пользователь нажимает DISCOVER внутри диалога begin SetLength(FDevices, 0); FDeviceCount := 0; SetStatusText(2, 'Discovering...'); FController.FNetwork.DirectIP := ''; TDiscoverThread.Create(FController.FNetwork, Self); end; procedure TMainForm.BtnStartStopClick(Sender: TObject); var Idx, i: Integer; Dev: THPSDRDevice; GenPkt: TGeneralPacket; G_Settings: TGlobalSettings; PreG: TGlobalSettings; PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings; PreloadRate: Integer; FoundByIP: Boolean; AutoIP: string; begin if FController.FNetwork.Connected then begin // --- STOP --- if FController.FRunning then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True); FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0); FController.FRunning := False; UninhibitSleep; FController.FTransmitting := False; if FController.FWDSPReady then FController.FDSPEngine.SetTXRun(False); StyleButton(BtnMOX, False); end; FRXPacketCount := 0; FActiveDDC := 0; // Сохраняем текущую частоту/настройки чтобы следующий START восстановил их if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); FController.FSettings.SaveAlex(FController.FDevMAC, FController.FAlexSettings); // XVTR LastFreq → актуальная видимая частота в момент остановки if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) then FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings); end; FController.FSettings.SaveStartupPreview(FController.FVfoA, FController.FVfoB, FController.FSampleRate); FController.FSettings.Save; FSpecView.ResetSpectrumBuf; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FController.FNetwork.Disconnect; BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); SetRadioOfflineStatus('Disconnected', True); BtnMOX.Enabled := False; Exit; end; // --- START --- AutoIP := ''; FPendingBoardType := 0; // Явный выбор через CONNECT в диалоге имеет приоритет над autostart if Assigned(FDeviceDialog) and FDeviceDialog.DialogResult.Accepted then begin FPendingIP := FDeviceDialog.DialogResult.IPAddress; FPendingBoardType := FDeviceDialog.GetSavedBoardType( FDeviceDialog.DialogResult.SavedIdx); FDeviceDialog.ClearResult; // consumed — следующий START снова спросит end else begin if Assigned(FDeviceDialog) then AutoIP := FDeviceDialog.GetAutoStartIP; if AutoIP <> '' then begin FPendingIP := AutoIP; FPendingBoardType := FDeviceDialog.GetAutoStartBoardType; end else begin if not Assigned(FDeviceDialog) then FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; if FLightTheme then FDeviceDialog.SetTheme(LightTheme) else FDeviceDialog.SetTheme(DarkTheme); if FDeviceDialog.ShowModal <> mrOk then Exit; if not FDeviceDialog.DialogResult.Accepted then Exit; FPendingIP := FDeviceDialog.DialogResult.IPAddress; FPendingBoardType := FDeviceDialog.GetSavedBoardType( FDeviceDialog.DialogResult.SavedIdx); FDeviceDialog.ClearResult; end; end; // Создаём запись устройства из IP + BoardType из диалога если есть FillChar(Dev, SizeOf(Dev), 0); Dev.IPAddress := FPendingIP; Dev.BoardType := FPendingBoardType; // Подбираем sample rate до открытия WDSP, чтобы не делать двойной Open: // 1) Open на 192k, 2) сразу ChangeSampleRate+Open ещё раз. // Если устройство уже было обнаружено (есть MAC) и есть сохранённый профиль, // создаём WDSPEngine сразу с нужной частотой. PreloadRate := FController.FSampleRate; FoundByIP := False; for i := 0 to High(FDevices) do if SameText(FDevices[i].Dev.IPAddress, Dev.IPAddress) then begin FoundByIP := True; if FController.FSettings.LoadDevice(FDevices[i].Dev.MAC, PreG, PreBands) and (PreG.SampleRate > 0) then PreloadRate := PreG.SampleRate; Break; end; if FoundByIP and (PreloadRate > 0) then begin FController.FSampleRate := PreloadRate; FController.FSpanHz := PreloadRate; if (not FController.FWDSPReady) and (Assigned(FController.FDSPEngine)) and (FController.FDSPEngine.SampleRate <> PreloadRate) then RecreateDSPEngine(PreloadRate); end; // --- Открываем WDSP асинхронно чтобы не блокировать UI --- FPendingDev := Dev; BtnStartStop.Enabled := False; SetStatusText(2, 'Opening DSP...'); if FController.FWDSPReady then // WDSP уже открыт — сразу подключаемся DoConnectDevice(Dev) else // Открываем в фоновом потоке TWDSPOpenThread.Create(Self); end; procedure TMainForm.OnWDSPOpenDone(Success: Boolean); begin // Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке FController.FWDSPReady := Success; if not Success then begin SetStatusText(2, 'DSP failed'); BtnStartStop.Enabled := True; BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); ShowMessage('WDSP not loaded.' + LineEnding + IfThen(FController.FDSPEngine.LastError <> '', FController.FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding + 'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).'); Exit; end; DoConnectDevice(FPendingDev); end; procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice); var GenPkt: TGeneralPacket; G_Settings: TGlobalSettings; i: Integer; begin BtnStartStop.Enabled := True; if not FController.FNetwork.Connect(Dev) then begin ShowMessage('Failed to connect to ' + Dev.IPAddress + IfThen(FController.FNetwork.LastError <> '', ': ' + FController.FNetwork.LastError, '')); Exit; end; // --- Загружаем настройки устройства по MAC --- Move(FController.FNetwork.Device.MAC[0], FController.FDevMAC[0], 6); FController.FDevConnected := True; FController.FSettings.LoadDevice(FController.FDevMAC, G_Settings, FController.FBandCache); FCATLastGlobal := G_Settings; CATApplySettings(G_Settings); // TX-настройки per-device — отдельная JSON-секция "tx" FController.FSettings.LoadTX(FController.FDevMAC, FController.FTXSettings); // Alex-настройки per-device (антенны/маршрутизация) FController.FSettings.LoadAlex(FController.FDevMAC, FController.FAlexSettings); FController.FNetwork.SetAlexConfig(FController.FAlexSettings); // XVTR-настройки per-device (трансвертеры) FController.FSettings.LoadXvtr(FController.FDevMAC, FController.FXvtrSettings); FController.FCurrentXvtr := -1; RebuildXvtrButtons; ApplyXvtrToNetwork; PushXvtrToWeb; FTXSpecRefLevel := FController.FTXSettings.TXSpecRefLevel; FTXSpecRange := FController.FTXSettings.TXSpecRange; if FController.FTXSettings.TXSpecGridStep > 0 then FTXSpecGridStep := FController.FTXSettings.TXSpecGridStep; FController.FVolume := G_Settings.Volume; FController.FActiveVfo := G_Settings.ActiveVfo; // PA settings FController.FPAMaxPower := G_Settings.PAMaxPower; for i := 0 to BAND_COUNT - 1 do FController.FPABandCal[i] := G_Settings.PABandCal[i]; for i := 0 to CFG_XVTR_COUNT - 1 do FController.FVHFBandCal[i] := G_Settings.VHFBandCal[i]; // Slider position (0..100) and calibrated drive byte TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100); FController.FCurrentBand := G_Settings.LastBand; FController.FDriveLevel := CalcDriveByte; // SampleRate — глобальный, загружаем до RestoreBand if G_Settings.SampleRate > 0 then begin FController.FSampleRate := G_Settings.SampleRate; FController.FSpanHz := FController.FSampleRate; if Assigned(FSampleRateOverlay) then FSampleRateOverlay.SetCurrentRate(FController.FSampleRate); end; FController.FNRMode := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton; FController.FNBMode := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton; FController.FSNB := G_Settings.SNBEnabled; UpdateSNBButton; FController.FANF := G_Settings.ANFEnabled; UpdateANFButton; FController.FWfAGCEnabled := G_Settings.WfAGCEnabled; FController.FWfNFEnabled := G_Settings.WfNFEnabled; FController.FDitherEnabled := G_Settings.DitherEnabled; FController.FRandomEnabled := G_Settings.RandomEnabled; FController.FSendAudioToRadio := G_Settings.SendAudioToRadio; FWfManualHigh := G_Settings.WfManualHigh; FWfManualLow := G_Settings.WfManualLow; FWfAGCOffset := G_Settings.WfAGCOffset; FSpecView.WfAGCEnabled := FController.FWfAGCEnabled; FSpecView.WfNFEnabled := FController.FWfNFEnabled; FSpecView.WfManualHigh := FWfManualHigh; FSpecView.WfManualLow := FWfManualLow; FSpecView.WfAGCOffset := FWfAGCOffset; FSpecView.ResetWfAvgBuf; FShowSpectrum := G_Settings.ShowSpectrum; FShowWaterfall := G_Settings.ShowWaterfall; FShowWideband := G_Settings.ShowWideband; FWidebandFill := G_Settings.WidebandFill; if Assigned(FWidebandView) then FWidebandView.SetFillSpectrum(FWidebandFill); FSpectrumFill := G_Settings.SpectrumFill; FSpecView.FillSpectrum := FSpectrumFill; FController.FDisplayDuplex := G_Settings.DisplayDuplex; if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex); if G_Settings.DisplayFPS > 0 then ApplyFPS(G_Settings.DisplayFPS); ApplyFreqMhzDigits(EnsureRange(G_Settings.FreqMhzDigits, 3, 5)); // Настройки дисплея и сетки if G_Settings.FFTSize > 0 then begin FSpecRefLevel := G_Settings.SpecRefLevel; FSpecRange := G_Settings.SpecRange; if G_Settings.SpecGridStep > 0 then FSpecGridStep := G_Settings.SpecGridStep; FSpecView.SpecRefLevel := FSpecRefLevel; FSpecView.SpecRange := FSpecRange; FSpecView.SpecGridStep := FSpecGridStep; InvalidateGridCache; if FController.FWDSPReady then begin FController.FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType); FController.FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode, G_Settings.SpecAvgTimeMS); FController.FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode, G_Settings.WfAvgTimeMS); end; // Аудио устройство вывода FController.FAudioOut.Close; if G_Settings.AudioSampleRate > 0 then FController.FAudioOut.SampleRate := G_Settings.AudioSampleRate; if G_Settings.AudioOutDevice <> '' then begin FController.FAudioOut.DeviceIndex := FController.FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice); FController.FAudioOutDevName := G_Settings.AudioOutDevice; end else begin FController.FAudioOut.DeviceIndex := -1; FController.FAudioOutDevName := ''; end; FController.FAudioOut.Open; // Аудио устройство ввода (TX mic). MicSource (Radio/SoundCard) определяется // автоматически в ApplyMOX по источнику PTT — здесь только открываем устройство. if G_Settings.AudioInDevice <> '' then begin FController.FAudioIn.DeviceIndex := FController.FAudioIn.FindDeviceByName(G_Settings.AudioInDevice); FController.FAudioInDevName := G_Settings.AudioInDevice; FController.FAudioIn.Open; end else begin FController.FAudioIn.DeviceIndex := -1; FController.FAudioInDevName := ''; end; end; RestoreBand(FController.FCurrentBand); // Восстанавливаем XVTR-режим, если он был активен при выходе. // FController.FCurrentBand уже загружен (HF-диапазон, на который пользователь зашёл // до XVTR), его FController.FBandCache не пострадает — ActivateXvtrBand только // выставит FController.FVfoA на XVTR-частоту и FController.FCurrentXvtr. if (G_Settings.LastXvtr >= 0) and (G_Settings.LastXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[G_Settings.LastXvtr].Enabled then ActivateXvtrBand(G_Settings.LastXvtr); SetStatusText(1, 'IP: ' + FController.FNetwork.Device.IPAddress); SetStatusText(0, 'Board: ' + BoardTypeName(FController.FNetwork.Device.BoardType)); // General packet FillChar(GenPkt, SizeOf(GenPkt), 0); GenPkt.Command := CMD_GENERAL; GenPkt.Flags37 := $08; GenPkt.Flags38 := $01; GenPkt.PAConfig := $01; GenPkt.WBPort[0] := (PORT_WIDEBAND_ADC0 shr 8) and $FF; GenPkt.WBPort[1] := PORT_WIDEBAND_ADC0 and $FF; GenPkt.WBEnable := IfThen(FShowWideband, 1, 0); GenPkt.WBSamplesPerPkt[0] := 512 shr 8; GenPkt.WBSamplesPerPkt[1] := 512 and $FF; GenPkt.WBSampleSize := 16; GenPkt.WBUpdateRate := 70; GenPkt.WBPacketsPerFrame := 32; if FController.FNetwork.Device.BoardType = 5 then GenPkt.AlexEnable := $03 else GenPkt.AlexEnable := $01; FController.FNetwork.SendGeneralPacket(GenPkt); FController.FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32); if FController.FNetwork.Device.BoardType in [3, 4, 5] then FActiveDDC := 2 else FActiveDDC := 0; // Синхронизируем samplerate WDSP если нужно if FController.FWDSPReady and (FController.FDSPEngine.SampleRate <> FController.FSampleRate) then begin FController.FWDSPReady := False; FController.FDSPEngine.ChangeSampleRate(FController.FSampleRate); FController.FWDSPReady := FController.FDSPEngine.Initialized; end; if FController.FWDSPReady then begin FController.FDSPEngine.SetMode(FController.FMode); FController.FDSPEngine.SetVolume(FController.FVolume / 100.0); ApplyModeFilter; ApplyNoiseFilterButtonsToDSP; // ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки FController.FDSPEngine.SetAGCTop(FController.FAGCTop); FController.FDSPEngine.SetAGC(TWDSPAGCMode(FController.FAGCMode), 50.0); end; FSpecView.AGCTop := FController.FAGCTop; // Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread // (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1. // DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты. FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, False, True, True); FController.FNetwork.SetRunAndFreq(True, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), FController.FDriveLevel); FController.FRunning := True; InhibitSleep; FRXStartTime := GetTickCount64; FRXLastPktTime := 0; FRXPacketCount := 0; FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0); FSeqErrorCount := 0; FLastSeqErrorDDC := -1; FLastSeqErrorDelta := 0; FSeqOkStreak := 0; SetStatusText(6, 'SEQ OK'); // DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах. // DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FController.FTXSettings. FController.FNetwork.ConfigureDDCs(1, FController.FSampleRate div 1000, 0, FController.FDitherEnabled, FController.FRandomEnabled); // Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1). FController.FNetwork.SetSpeakerAudio(FController.FSendAudioToRadio); SendDUCSpecificFromSettings; // Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...). ApplyTXSettingsToDSP; ApplyModeFilter; // Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов if FController.FWDSPReady then FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета BtnStartStop.Caption := 'STOP'; StyleButton(BtnStartStop, True); SetStatusText(2, 'Running'); BtnMOX.Enabled := True; ResizeSpectrumPanels; end; // --------------------------------------------------------------------------- // ActivateVfo — переключаем активный VFO и перестраиваем приёмник. // Idx=0 → VFO-A активен, Idx=1 → VFO-B активен. // Перестройка всегда происходит на частоту активного VFO (не на оффсет). // CTUN при этом сбрасывается чтобы не было путаницы с оффсетом. // --------------------------------------------------------------------------- // ActiveVfoFreq — частота активного VFO // --------------------------------------------------------------------------- function TMainForm.ActiveVfoFreq: Int64; begin if FController.FActiveVfo = 0 then Result := Round(FController.FVfoA) else Result := Round(FController.FVfoB); end; // --------------------------------------------------------------------------- // ActivateVfo — переключаем активный VFO и перестраиваем приёмник. // Idx=0 → VFO-A активен, Idx=1 → VFO-B активен. // Перестройка на частоту выбранного VFO. CTUN не трогаем. // --------------------------------------------------------------------------- procedure TMainForm.ActivateVfo(Idx: Integer); var ActiveFreq: Int64; NewBand: Integer; Offset: Double; HalfSpan: Double; FreqVisible: Boolean; begin FController.FActiveVfo := Idx; // Частота нового активного VFO if FController.FActiveVfo = 0 then ActiveFreq := Round(FController.FVfoA) else ActiveFreq := Round(FController.FVfoB); // Обновляем визуальный дисплей UpdateVfoDisplay; if FController.FCTun then begin // CTUN включён: проверяем, видна ли новая частота на текущем спектре HalfSpan := FController.FSpanHz / 2; Offset := ActiveFreq - FController.FCenterFreq; FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan); if FreqVisible then begin // Частота видна в окне — просто меняем shift, DDC не трогаем if FController.FWDSPReady then FController.FDSPEngine.SetShift(Offset); // Сеть не трогаем — DDC (FController.FCenterFreq) остаётся прежним end else begin // Частота за пределами спектра — центрируем окно и сбрасываем CTUN FController.FCTun := False; StyleButton(BtnCTun, False); FController.FBandCache[FController.FCurrentBand].CTun := False; FController.FCenterFreq := ActiveFreq; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; end else begin // CTUN выключен: перестраиваем DDC на новую частоту FController.FCenterFreq := ActiveFreq; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; // Переключаем кнопку диапазона по новой частоте NewBand := FreqToBandIdx(ActiveFreq); if (NewBand >= 0) and (NewBand <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := NewBand; StyleButton(BtnBand[FController.FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FSpecView.DrawWaterfall; PbWaterfall.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; procedure TMainForm.FreqDispAClick(Sender: TObject); begin // Клик на VFO-A → активируем A (перестройка на FController.FVfoA) if FController.FActiveVfo <> 0 then ActivateVfo(0); end; procedure TMainForm.FreqDispBClick(Sender: TObject); begin // Клик на VFO-B → активируем B (перестройка на FController.FVfoB) if FController.FActiveVfo <> 1 then ActivateVfo(1); end; procedure TMainForm.BtnTopVfoASelectClick(Sender: TObject); begin if FController.FActiveVfo <> 0 then ActivateVfo(0); end; procedure TMainForm.BtnTopVfoATXClick(Sender: TObject); begin FController.FSplitTxB := False; UpdateVfoDisplay; if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; procedure TMainForm.BtnTopVfoBSelectClick(Sender: TObject); begin if FController.FActiveVfo <> 1 then ActivateVfo(1); end; procedure TMainForm.BtnTopVfoBTXClick(Sender: TObject); begin FController.FSplitTxB := True; if FController.FSplitTxB and (FController.FActiveVfo <> 0) then ActivateVfo(0) else UpdateVfoDisplay; if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64); begin if FController.FActiveVfo = 0 then ApplyVfoA(NewFreq) else FController.FVfoA := NewFreq; // просто сохраняем, приёмник не перестраиваем end; procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64); var BandIdx: Integer; begin FController.FVfoB := NewFreq; if FController.FActiveVfo = 1 then begin // VFO-B активен: перестраиваем приёмник FController.FCenterFreq := FController.FVfoB; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; // Обновляем диапазон BandIdx := FreqToBandIdx(FController.FVfoB); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); FController.FDriveLevel := CalcDriveByte; end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; end; // --------------------------------------------------------------------------- // UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим // --------------------------------------------------------------------------- procedure TMainForm.UpdateFilterButtons; var i: Integer; Names: array[0..FILT_COUNT-1] of string; DefIdx: Integer; begin if FController.FMode = MODE_FM then begin // FM: show only NFM and WFM buttons, resize them to fill the row CloseCTCSSPopup; if FController.FFilter > 1 then FController.FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is FController.FFilterBW := FILT_FM_BW[FController.FFilter]; FController.FFMDeviation := FILT_FM_DEV[FController.FFilter]; BtnFilter[0].Caption := FILT_FM_NAMES[0]; BtnFilter[0].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 0); BtnFilter[0].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 0); BtnFilter[0].Top := 16; BtnFilter[0].Visible := True; StyleButton(BtnFilter[0], FController.FFilter = 0); BtnFilter[1].Caption := FILT_FM_NAMES[1]; BtnFilter[1].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 1); BtnFilter[1].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 1); BtnFilter[1].Top := 16; BtnFilter[1].Visible := True; StyleButton(BtnFilter[1], FController.FFilter = 1); for i := 2 to FILT_COUNT - 1 do BtnFilter[i].Visible := False; // Shrink panel to fit label + 1 button row so SQL/CTCSS sit right below PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4; if PanelFMSQ <> nil then begin PanelFMSQ.Visible := True; StyleButton(BtnFMSQ, FController.FFMSQOn); TrkFMSQ.Position := FController.FFMSQLevel; LblFMSQ.Caption := IntToStr(FController.FFMSQLevel); end; if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := True; if PanelFMStep <> nil then begin StyleButton(BtnFMStep, FController.FFMStepOn); if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx); PanelFMStep.Visible := True; end; if PanelFMRpt <> nil then begin StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS); StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS); EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); PanelFMRpt.Visible := True; end; RelayoutBelowBands; Exit; end; // Non-FM: restore full 2-row height and 5-column layout, hide FM panels PanelFilter.Height := 74; if PanelFMSQ <> nil then PanelFMSQ.Visible := False; if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False; if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end; if PanelFMRpt <> nil then PanelFMRpt.Visible := False; for i := 0 to FILT_COUNT - 1 do begin BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5); BtnFilter[i].Top := 16 + (i div 5) * 27; BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, 5, i mod 5); BtnFilter[i].Visible := True; end; case FController.FMode of 0, 1: begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i]; DefIdx := FILT_SSB_DEF; end; 2: begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i]; DefIdx := FILT_DSB_DEF; end; 3, 4: begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i]; DefIdx := FILT_CW_DEF; end; else begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i]; DefIdx := FILT_AM_DEF; end; end; FController.FFilter := DefIdx; case FController.FMode of 0, 1: FController.FFilterBW := FILT_SSB_BW[DefIdx]; 2: FController.FFilterBW := FILT_DSB_BW[DefIdx]; 3, 4: FController.FFilterBW := FILT_CW_BW[DefIdx]; else FController.FFilterBW := FILT_AM_BW[DefIdx]; end; for i := 0 to FILT_COUNT-1 do begin BtnFilter[i].Caption := Names[i]; StyleButton(BtnFilter[i], i = FController.FFilter); end; RelayoutBelowBands; end; // --------------------------------------------------------------------------- // ApplyVfoA — единая точка смены частоты VFO A // // CTUN ВЫКЛ (классический режим): // FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре // // CTUN ВКЛ (piHPSDR/Thetis режим): // FController.FCenterFreq статичен — спектр/водопад не двигаются // FController.FVfoA движется по дисплею (маркер гуляет) // Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея // (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband // hits the edge of the display") // --------------------------------------------------------------------------- procedure TMainForm.ApplyVfoA(NewFreq: Int64); var Offset: Double; FiltLo: Double; FiltHi: Double; HalfSpan: Double; Scrolled: Boolean; BandIdx: Integer; begin // Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then begin if NewFreq < FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin then NewFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin); if NewFreq > FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd then NewFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd); end; FController.FVfoA := NewFreq; Scrolled := False; if not FController.FCTun then begin // ---- CTUN OFF ---- // DDC = VFO, сдвига нет FController.FCenterFreq := FController.FVfoA; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); end else begin // ---- CTUN ON ---- // DDC (FController.FCenterFreq) стоит на месте. // SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы // демодулятор принимал сигнал на FController.FVfoA, а не на FController.FCenterFreq. // Shift = FController.FVfoA - FController.FCenterFreq (в Гц) Offset := FController.FVfoA - FController.FCenterFreq; if FController.FWDSPReady then FController.FDSPEngine.SetShift(Offset); // Проверяем не вышла ли полоса фильтра за край дисплея HalfSpan := FController.FSpanHz / 2; case FController.FMode of 0: begin FiltLo := Offset - FController.FFilterBW; FiltHi := Offset - 100; end; 1: begin FiltLo := Offset + 100; FiltHi := Offset + FController.FFilterBW; end; else begin FiltLo := Offset - FController.FFilterBW/2; FiltHi := Offset + FController.FFilterBW/2; end; end; if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then begin // Полоса вышла за край → прокручиваем центр if FiltHi > HalfSpan then FController.FCenterFreq := FController.FVfoA - HalfSpan * 0.5 else FController.FCenterFreq := FController.FVfoA + HalfSpan * 0.5; // Пересчитываем сдвиг после прокрутки if FController.FWDSPReady then FController.FDSPEngine.SetShift(FController.FVfoA - FController.FCenterFreq); // DDC перестраивается на новый FController.FCenterFreq Scrolled := True; end; end; // Обновляем VFO дисплей FreqDispA.Frequency := Round(FController.FVfoA); // Обновляем кнопку диапазона если частота перешла в другой диапазон. // В XVTR-режиме HF band-кнопки не подсвечиваем (мы клэмпим VFO внутри // XVTR-диапазона — FreqToBandIdx всегда вернёт -1 для VHF/UHF). if FController.FCurrentXvtr < 0 then begin BandIdx := FreqToBandIdx(FController.FVfoA); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); FController.FDriveLevel := CalcDriveByte; end; end; // Если нет активного канала, но CTCSS/RPT были установлены каналом (авто-флаги), // сбрасываем их при первом же изменении частоты if FActiveChannelIdx < 0 then begin if FController.FFMCTCSSAutoActive then begin FController.FFMCTCSSAutoActive := False; FController.FFMCTCSSOn := False; if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, False); if FController.FWDSPReady then FController.FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FController.FFMCTCSSToneIdx]); end; if FController.FFMRptAutoActive then begin FController.FFMRptDir := RPT_NONE; FController.FFMRptAutoActive := False; if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False); if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False); end; if FPreChannelPower >= 0 then begin TrkDrive.Position := FPreChannelPower; FPreChannelPower := -1; FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FDrivePercent / 100.0); end; end; // Auto-enable repeater minus for 2 m repeater input segment if FController.FMode = MODE_FM then UpdateRptAutoState; // DDC: передаём FController.FCenterFreq (при CTUN OFF = FController.FVfoA, при CTUN ON = фиксирован) if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; // Не рисуем синхронно из wheel/drag path: частые события мыши иначе // забивают UI-поток и мешают аудио. Таймер подхватит ближайший кадр. SyncSpecViewFreq; FSpectrumDirty := True; PbSpectrum.Invalidate; if Scrolled then begin FWaterfallDirty := True; PbWaterfall.Invalidate; end; CheckChannelActive; end; // --------------------------------------------------------------------------- // CTUN toggle // --------------------------------------------------------------------------- procedure TMainForm.BtnCTunClick(Sender: TObject); begin // Логика (band cache, center/shift, сеть) — в контроллере; рендер — в // OnControllerState(rfCTun). FController.SetCTun(not FController.FCTun); end; // --------------------------------------------------------------------------- // DUP toggle (Thetis-style local display mode — без аппаратной команды) // --------------------------------------------------------------------------- procedure TMainForm.ApplyDUP(Active: Boolean); begin FController.FDisplayDuplex := Active; StyleButton(BtnDUP, FController.FDisplayDuplex); // Если сейчас идёт передача — переключаем источник пикселей и сбрасываем // буферы, чтобы старые TX/RX bin'ы не оставались на следующем кадре. if FController.FTransmitting then begin if FController.FWDSPReady then FController.FDSPEngine.SetDisplaySourceTX(not FController.FDisplayDuplex); FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex; FSpecView.ResetSpectrumBuf; FSpecView.ResetWfAvgBuf; ApplySpecViewGridFromState; FSpecView.DrawSpectrum; FSpecView.DrawWaterfall; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; end; procedure TMainForm.BtnDUPClick(Sender: TObject); begin ApplyDUP(not FController.FDisplayDuplex); end; // --------------------------------------------------------------------------- // TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive // --------------------------------------------------------------------------- procedure TMainForm.ApplyTUN(Active: Boolean); begin if Active = FController.FTuning then Exit; // Safety: те же блокировки что в ApplyMOX. TUN — это TX через WDSP PostGen, // он тоже должен молчать на бэндах с DoNotTx или на RX-only XVTR. if Active and FController.FDevConnected and (FController.FCurrentXvtr < 0) and FController.FAlexSettings.DoNotTx[EnsureRange(FController.FCurrentBand, 0, 10)] then Exit; if Active and (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].RXOnly then Exit; if Active then begin // Включаем тон ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном if FController.FWDSPReady then FController.FDSPEngine.SetTXTone(True, FController.FTXSettings.TUNFreq, 1.0); FController.FTuning := True; StyleButton(BtnTUN, True); // Drive байт пересчитываем (CalcDriveByte увидит FController.FTuning=True) FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0); // Активируем TX (то же что MOX — PTT, run TXA-канал) if not FController.FTransmitting then ApplyMOX(True); end else begin // Выключаем TX, потом тон, потом восстанавливаем drive if FController.FTransmitting then ApplyMOX(False); if FController.FWDSPReady then FController.FDSPEngine.SetTXTone(False, 0, 0); FController.FTuning := False; StyleButton(BtnTUN, False); FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FDrivePercent / 100.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; end; procedure TMainForm.BtnTUNClick(Sender: TObject); begin ApplyTUN(not FController.FTuning); end; // --------------------------------------------------------------------------- // Клик/драг по спектру/водопаду // --------------------------------------------------------------------------- // Перевод X пикселя в частоту function PixelToFreq(PixelX, PanelWidth: Integer; CenterFreq, SpanHz: Double): Double; begin Result := CenterFreq + (PixelX / PanelWidth - 0.5) * SpanHz; end; procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer); var ClickFreq: Int64; StepHz: Int64; BandIdx: Integer; begin if PanelWidth <= 0 then Exit; if (FController.FMode = MODE_FM) and FController.FFMStepOn then StepHz := FM_STEP_HZ[FController.FFMStepIdx] else StepHz := 100; ClickFreq := Round(FController.FCenterFreq + (PixelX / PanelWidth - 0.5) * FController.FSpanHz); ClickFreq := ((ClickFreq + StepHz div 2) div StepHz) * StepHz; if FController.FActiveVfo = 0 then ApplyVfoA(ClickFreq) else begin FController.FVfoB := ClickFreq; FreqDispB.Frequency := Round(FController.FVfoB); FController.FCenterFreq := ClickFreq; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; BandIdx := FreqToBandIdx(FController.FVfoB); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; end; procedure TMainForm.DoWidebandClick(PixelX: Integer; PanelWidth: Integer); var FreqHz: Double; ClickFreq: Int64; StepHz: Int64; BandIdx: Integer; WasCTun: Boolean; begin if (FWidebandView = nil) or (not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit; if (FController.FMode = MODE_FM) and FController.FFMStepOn then StepHz := FM_STEP_HZ[FController.FFMStepIdx] else StepHz := 100; ClickFreq := Round(FreqHz); ClickFreq := ((ClickFreq + StepHz div 2) div StepHz) * StepHz; if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then begin if ClickFreq < FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin then ClickFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin); if ClickFreq > FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd then ClickFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd); end; if FController.FCurrentXvtr < 0 then begin BandIdx := FreqToBandIdx(ClickFreq); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.Save; end; FController.FCurrentBand := BandIdx; RestoreBand(FController.FCurrentBand); end; end; if FController.FActiveVfo = 0 then begin WasCTun := FController.FCTun; FController.FCTun := False; ApplyVfoA(ClickFreq); FController.FCTun := WasCTun; if WasCTun and FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); end else begin FController.FVfoB := ClickFreq; FreqDispB.Frequency := Round(FController.FVfoB); FController.FCenterFreq := ClickFreq; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; if FController.FCurrentXvtr < 0 then begin BandIdx := FreqToBandIdx(FController.FVfoB); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin if FController.FCurrentBand >= 0 then StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; end; UpdateVfoDisplay; SyncSpecViewFreq; FSpectrumDirty := True; PbSpectrum.Invalidate; end; FSpecView.InvalidateRulerCache; if PbRuler <> nil then PbRuler.Invalidate; FWaterfallDirty := True; if PbWaterfall <> nil then PbWaterfall.Invalidate; if UpdateWidebandFrequencyView and (PbWideband <> nil) then PbWideband.Invalidate; end; procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer); var dPix: Integer; dFreq: Double; begin if PanelWidth <= 0 then Exit; dPix := PixelX - FSpecDragX0; dFreq := dPix / PanelWidth * FController.FSpanHz; if FController.FCTun then begin // CTUN ON: drag двигает окно просмотра (FController.FCenterFreq/DDC) // VFO остаётся, shift обновляется = VFO - новый FController.FCenterFreq FController.FCenterFreq := FSpecDragFreq - dFreq; if FController.FWDSPReady then begin if FController.FActiveVfo = 0 then FController.FDSPEngine.SetShift(FController.FVfoA - FController.FCenterFreq) else FController.FDSPEngine.SetShift(FController.FVfoB - FController.FCenterFreq); end; if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; FSpectrumDirty := True; end else begin // CTUN OFF: drag двигает VFO, FController.FCenterFreq следует через Apply if FController.FActiveVfo = 0 then ApplyVfoA(Round(FSpecDragFreq - dFreq)) else begin FController.FVfoB := FSpecDragFreq - dFreq; FreqDispB.Frequency := Round(FController.FVfoB); FController.FCenterFreq := FController.FVfoB; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; FSpectrumDirty := True; end; end; end; // Спектр procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Assigned(FVfoOverlay) and FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit; if Assigned(FSampleRateOverlay) and FSampleRateOverlay.HandleMouseDown(Button, X, Y) then Exit; if Button = mbLeft then begin // Левая кнопка: сбрасываем маркер если активен if FSpecView.MarkerActive then begin FSpecView.MarkerActive := False; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; FSpecDrag := True; FSpecDragX0 := X; if FController.FCTun then FSpecDragFreq := FController.FCenterFreq else begin if FController.FActiveVfo = 0 then FSpecDragFreq := FController.FVfoA else FSpecDragFreq := FController.FVfoB; end; PbSpectrum.Cursor := crSizeWE; end else if Button = mbRight then begin // Правая кнопка: toggle маркера if FSpecView.MarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FSpecView.MarkerX) < 8) then begin // Клик рядом с текущим маркером — убираем FSpecView.MarkerActive := False; end else begin // Ставим маркер в новое место FSpecView.MarkerActive := True; FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000); end; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; end; procedure TMainForm.PbWidebandMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then DoWidebandClick(X, TControl(Sender).Width); end; procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if Assigned(FSampleRateOverlay) then begin FSampleRateOverlay.HandleMouseMove(X, Y); end; if Assigned(FVfoOverlay) then FVfoOverlay.HandleMouseMove(X, Y); if not FSpecDrag then begin if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or (Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then PbSpectrum.Cursor := crHandPoint else PbSpectrum.Cursor := crDefault; end; if FSpecDrag and (ssLeft in Shift) then DoSpectrumDrag(X, PbSpectrum.Width); // Маркер следует за курсором — обновление произойдёт в ближайшем тике таймера (50ms) if FSpecView.MarkerActive then begin FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000); FSpectrumDirty := True; end; end; procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin if (Abs(X - FSpecDragX0) < 4) then DoSpectrumClick(X, PbSpectrum.Width); // это был клик, не драг FSpecDrag := False; PbSpectrum.Cursor := crDefault; end; end; procedure TMainForm.PbSpectrumMouseLeave(Sender: TObject); begin if Assigned(FSampleRateOverlay) then FSampleRateOverlay.HandleMouseLeave; if Assigned(FVfoOverlay) then FVfoOverlay.HandleMouseLeave; if not FSpecDrag then PbSpectrum.Cursor := crDefault; end; // Водопад (аналогично) procedure TMainForm.PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin if FSpecView.MarkerActive then begin FSpecView.MarkerActive := False; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; FSpecDrag := True; FSpecDragX0 := X; if FController.FCTun then FSpecDragFreq := FController.FCenterFreq else begin if FController.FActiveVfo = 0 then FSpecDragFreq := FController.FVfoA else FSpecDragFreq := FController.FVfoB; end; PbWaterfall.Cursor := crSizeWE; end else if Button = mbRight then begin if FSpecView.MarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FSpecView.MarkerX) < 8) then FSpecView.MarkerActive := False else begin FSpecView.MarkerActive := True; FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000); end; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; end; procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if FSpecDrag and (ssLeft in Shift) then DoSpectrumDrag(X, PbWaterfall.Width); if FSpecView.MarkerActive then begin FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000); FSpectrumDirty := True; end; end; procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin if (Abs(X - FSpecDragX0) < 4) then DoSpectrumClick(X, PbWaterfall.Width); FSpecDrag := False; PbWaterfall.Cursor := crDefault; end; end; procedure TMainForm.BtnBandClick(Sender: TObject); var NewBand: Integer; begin NewBand := (Sender as TFlatButton).Tag; if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit; // Если в XVTR-режиме — DeactivateXvtr вернёт HF-состояние (RestoreBand // на сохранённом FController.FCurrentBand). Если NewBand совпадает с восстановленным // FController.FCurrentBand, дальше делать ничего не нужно. if FController.FCurrentXvtr >= 0 then begin DeactivateXvtr; if NewBand = FController.FCurrentBand then Exit; end else if NewBand = FController.FCurrentBand then Exit; if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.Save; end; FController.FCurrentBand := NewBand; RestoreBand(FController.FCurrentBand); end; procedure TMainForm.ApplyModeFilter; var Lo, Hi, Half: Integer; begin Half := FController.FFilterBW div 2; case FController.FMode of 0: begin Lo := -FController.FFilterBW; Hi := -100; end; // LSB 1: begin Lo := 100; Hi := FController.FFilterBW; end; // USB 2: begin Lo := -Half; Hi := Half; end; // DSB 3: begin Lo := -Half; Hi := Half; end; // CWL 4: begin Lo := -Half; Hi := Half; end; // CWU 5: begin Lo := -Half; Hi := Half; end; // FM 6: begin Lo := -Half; Hi := Half; end; // AM 7: begin Lo := -Half; Hi := Half; end; // SAM else Lo := -Half; Hi := Half; end; if FController.FWDSPReady then begin FController.FDSPEngine.SetFilter(Lo, Hi); FController.FDSPEngine.SetTXFilter(Lo, Hi); if FController.FMode = MODE_FM then begin FController.FDSPEngine.SetRXFMDeviation(FController.FFMDeviation); FController.FDSPEngine.SetTXFMParams(FController.FFMDeviation, FController.FTXSettings.FMLowCut, FController.FTXSettings.FMHighCut, FController.FTXSettings.FMEmphPosition); FController.FDSPEngine.SetFMSquelch(FController.FFMSQOn, FController.FFMSQLevel); end else FController.FDSPEngine.SetFMSquelch(False, 0); end; end; procedure TMainForm.BtnModeClick(Sender: TObject); var i, N: Integer; begin N := (Sender as TFlatButton).Tag; FController.FMode := N; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FController.FMode); if FController.FWDSPReady then begin FController.FDSPEngine.SetMode(N); // Если TUN активен — пересчитываем знак частоты под новый режим if FController.FTuning then FController.FDSPEngine.SetTXTone(True, FController.FTXSettings.TUNFreq, 1.0); end; UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим ApplyModeFilter; SyncSpecViewFreq; // FSpecView.Mode/FilterBW — иначе полоса рисуется под старый режим end; procedure TMainForm.BtnFilterClick(Sender: TObject); var i, N: Integer; begin N := (Sender as TFlatButton).Tag; FController.FFilter := N; case FController.FMode of 0, 1: FController.FFilterBW := FILT_SSB_BW[N]; 2: FController.FFilterBW := FILT_DSB_BW[N]; 3, 4: FController.FFilterBW := FILT_CW_BW[N]; 5: begin FController.FFilterBW := FILT_FM_BW[N]; FController.FFMDeviation := FILT_FM_DEV[N]; end; else FController.FFilterBW := FILT_AM_BW[N]; end; for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter); ApplyModeFilter; SyncSpecViewFreq; end; // --------------------------------------------------------------------------- // FM CTCSS helpers // --------------------------------------------------------------------------- procedure TMainForm.ApplyFMSquelch; begin if FController.FWDSPReady then FController.FDSPEngine.SetFMSquelch(FController.FFMSQOn and (FController.FMode = MODE_FM), FController.FFMSQLevel); end; procedure TMainForm.BtnFMSQClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetFMSquelch(not FController.FFMSQOn); // DSP + OnControllerState SaveCurrentBand; end; procedure TMainForm.TrkFMSQChange(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetFMSquelchLevel(TrkFMSQ.Position); SaveCurrentBand; end; procedure TMainForm.CloseCTCSSPopup; begin if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup; CloseFMStepPopup; if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup; end; procedure TMainForm.BtnFMRptMinusClick(Sender: TObject); begin if FController.FFMRptDir = RPT_MINUS then FController.FFMRptDir := RPT_NONE else FController.FFMRptDir := RPT_MINUS; FController.FFMRptAutoActive := False; // ручное действие снимает авто-флаг StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS); StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS); BtnFMRptMinus.Repaint; BtnFMRptPlus.Repaint; ApplyFMRpt; SaveCurrentBand; end; procedure TMainForm.BtnFMRptPlusClick(Sender: TObject); begin if FController.FFMRptDir = RPT_PLUS then FController.FFMRptDir := RPT_NONE else FController.FFMRptDir := RPT_PLUS; FController.FFMRptAutoActive := False; // ручное действие снимает авто-флаг StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS); StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS); BtnFMRptMinus.Repaint; BtnFMRptPlus.Repaint; ApplyFMRpt; SaveCurrentBand; end; procedure TMainForm.EdFMRptOffsetChange(Sender: TObject); var Hz: Double; begin Hz := RptParseOffset(EdFMRptOffset.Text); if Hz > 0.0 then begin FController.FFMRptOffsetHz := Hz; if FController.FFMRptDir <> RPT_NONE then begin ApplyFMRpt; SaveCurrentBand; end; end; end; procedure TMainForm.ApplyFMRpt; begin if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; procedure TMainForm.UpdateRptAutoState; var InRange: Boolean; begin InRange := RptAutoDir(FController.FVfoA) = RPT_MINUS; if InRange then begin // Auto-enable minus only when currently off. if FController.FFMRptDir = RPT_NONE then begin FController.FFMRptDir := RPT_MINUS; FController.FFMRptAutoActive := True; if BtnFMRptMinus <> nil then begin StyleButton(BtnFMRptMinus, True); BtnFMRptMinus.Repaint; end; end; end else begin // Auto-disable only if this minus was set automatically. if (FController.FFMRptDir = RPT_MINUS) and FController.FFMRptAutoActive then begin FController.FFMRptDir := RPT_NONE; FController.FFMRptAutoActive := False; if BtnFMRptMinus <> nil then begin StyleButton(BtnFMRptMinus, False); BtnFMRptMinus.Repaint; end; end; end; end; procedure TMainForm.CloseFMStepPopup; begin if FStepDropDown <> nil then FStepDropDown.ClosePopup; if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup; end; procedure TMainForm.SetFMStep(Idx: Integer); begin // Тонкая обёртка: команда контроллера обновит UI/web/спецвью через // OnControllerState(rfFMStepIdx). Сохранена для существующих вызовов // (RestoreBand, dropdown-select, web-sync, XVTR-restore). FController.SetFMStep(Idx); end; procedure TMainForm.BtnFMStepClick(Sender: TObject); begin FController.FFMStepOn := not FController.FFMStepOn; StyleButton(BtnFMStep, FController.FFMStepOn); BtnFMStep.Repaint; SaveCurrentBand; end; procedure TMainForm.BtnFMStepSelClick(Sender: TObject); begin FCTCSSDropDown.ClosePopup; FStepDropDown.Toggle; end; procedure TMainForm.OnStepDropDownSelect(Sender: TObject; Idx: Integer); begin SetFMStep(Idx); SaveCurrentBand; end; // --------------------------------------------------------------------------- // Channels dropdown / store // --------------------------------------------------------------------------- procedure TMainForm.RefreshChannelsDropDown; var names: array of string; i: Integer; begin if (FController.FChannelStore = nil) or (FChannelsDropDown = nil) then Exit; SetLength(names, FController.FChannelStore.Count); for i := 0 to FController.FChannelStore.Count - 1 do names[i] := FController.FChannelStore.GetChannel(i).Name; FChannelsDropDown.SetItems(names); if (FActiveChannelIdx >= 0) and (FActiveChannelIdx < FController.FChannelStore.Count) then FChannelsDropDown.SetItemIndex(FActiveChannelIdx) else if BtnChannels <> nil then BtnChannels.Caption := 'Channel'; end; procedure TMainForm.OnChannelStoreChanged(Sender: TObject); begin RefreshChannelsDropDown; end; procedure TMainForm.BtnChannelsClick(Sender: TObject); begin if FController.FChannelStore.Count = 0 then Exit; FCTCSSDropDown.ClosePopup; FStepDropDown.ClosePopup; FChannelsDropDown.Toggle; end; procedure TMainForm.BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Def: TChannel; begin if Button <> mbRight then Exit; FChannelsDropDown.ClosePopup; if FChannelsForm = nil then FChannelsForm := TChannelsForm.Create(Self, FController.FChannelStore, OnChannelStoreChanged); // Передаём текущее состояние радио как значения по умолчанию для нового канала TChannelStore.DefaultChannel(Def); Def.RXFreq := FController.FVfoA; Def.TXFreq := ActiveTXFreqHz; Def.Mode := FController.FMode; Def.RptDir := FController.FFMRptDir; Def.RptOffsetHz := FController.FFMRptOffsetHz; Def.CTCSSOn := FController.FFMCTCSSOn; Def.CTCSSToneIdx := FController.FFMCTCSSToneIdx; Def.Power := 100; Def.Name := ''; TChannelsForm(FChannelsForm).SetCurrentDefaults(Def); TChannelsForm(FChannelsForm).ApplyTheme(DarkTheme); TChannelsForm(FChannelsForm).Show; end; procedure TMainForm.OnChannelsDropDownSelect(Sender: TObject; Idx: Integer); var Ch: TChannel; begin if (FController.FChannelStore = nil) or (Idx < 0) or (Idx >= FController.FChannelStore.Count) then Exit; Ch := FController.FChannelStore.GetChannel(Idx); FActiveChannelIdx := Idx; StyleButton(BtnChannels, True); BtnChannels.Caption := Ch.Name; BtnChannels.Repaint; ApplyChannel(Ch); end; procedure TMainForm.ApplyChannel(const Ch: TChannel); var i, NewBand, XvtrIdx: Integer; begin // Сохраняем текущее состояние до любых изменений SaveCurrentBand; // Ищем подходящий XVTR-слот по частоте канала XvtrIdx := -1; for i := 0 to CFG_XVTR_COUNT - 1 do if FController.FXvtrSettings.Entries[i].Enabled and (Ch.RXFreq >= FController.FXvtrSettings.Entries[i].FreqBegin) and (Ch.RXFreq <= FController.FXvtrSettings.Entries[i].FreqEnd) then begin XvtrIdx := i; Break; end; NewBand := -1; if XvtrIdx >= 0 then begin // Канал на трансвертерном диапазоне — активируем XVTR если не активен if FController.FCurrentXvtr <> XvtrIdx then ActivateXvtrBand(XvtrIdx); end else begin // Канал на HF (или неизвестная частота) // Если были на XVTR — выходим из него, иначе ApplyVfoA заклэмпит частоту if FController.FCurrentXvtr >= 0 then DeactivateXvtr; NewBand := FreqToBandIdx(Ch.RXFreq); if (NewBand >= 0) and (NewBand <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := NewBand; StyleButton(BtnBand[FController.FCurrentBand], True); FController.FDriveLevel := CalcDriveByte; end; end; // Режим (после ActivateXvtrBand/DeactivateXvtr FController.FMode обновился — сравниваем снова) if FController.FMode <> Ch.Mode then begin FController.FMode := Ch.Mode; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FController.FMode); if FController.FWDSPReady then begin FController.FDSPEngine.SetMode(FController.FMode); if FController.FTuning then FController.FDSPEngine.SetTXTone(True, FController.FTXSettings.TUNFreq, 1.0); end; UpdateFilterButtons; ApplyModeFilter; SyncSpecViewFreq; end; // FM-параметры if FController.FMode = MODE_FM then begin FController.FFMCTCSSOn := Ch.CTCSSOn; FController.FFMCTCSSAutoActive := Ch.CTCSSOn; // CTCSS из канала — авто, сбросится при уходе с частоты SetFMCTCSSTone(Ch.CTCSSToneIdx); if BtnFMCTCSS <> nil then begin StyleButton(BtnFMCTCSS, FController.FFMCTCSSOn); BtnFMCTCSS.Repaint; end; if FController.FWDSPReady then FController.FDSPEngine.SetTXCTCSS(FController.FFMCTCSSOn, CTCSS_TONES[FController.FFMCTCSSToneIdx]); FController.FFMRptDir := Ch.RptDir; FController.FFMRptOffsetHz := Ch.RptOffsetHz; FController.FFMRptAutoActive := Ch.RptDir <> RPT_NONE; // RPT из канала — авто, сбросится при уходе if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS); if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS); if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); end; // Мощность из канала. // TrkDrive.Position := ... синхронно вызывает TrkDriveChange → FPreChannelPower := -1, // поэтому сохраняем оригинал в локальную переменную и восстанавливаем после. if Ch.Power >= 0 then begin i := FController.FDrivePercent; // запомнить до срабатывания OnChange TrkDrive.Position := EnsureRange(Ch.Power, 0, 100); // TrkDriveChange обновит FController.FDriveLevel и WDSP if FPreChannelPower < 0 then FPreChannelPower := i; // установить после (TrkDriveChange уже сбросил в -1) end; // Частота VFO A ApplyVfoA(Round(Ch.RXFreq)); // Сохраняем состояние if XvtrIdx >= 0 then SaveCurrentBand // сохраняем в XVTR-слот (CTCSS/RPT/Mode из канала) else if NewBand >= 0 then SaveCurrentBand; // сохраняем в HF band-кэш end; procedure TMainForm.CheckChannelActive; var Ch: TChannel; begin if FActiveChannelIdx < 0 then Exit; if (FController.FChannelStore = nil) or (FActiveChannelIdx >= FController.FChannelStore.Count) then begin FActiveChannelIdx := -1; Exit; end; Ch := FController.FChannelStore.GetChannel(FActiveChannelIdx); if Round(FController.FVfoA) <> Round(Ch.RXFreq) then begin FActiveChannelIdx := -1; StyleButton(BtnChannels, False); BtnChannels.Caption := 'Channel'; BtnChannels.Repaint; // Сбрасываем CTCSS если он был включён каналом (не вручную) if FController.FFMCTCSSAutoActive then begin FController.FFMCTCSSAutoActive := False; FController.FFMCTCSSOn := False; if BtnFMCTCSS <> nil then begin StyleButton(BtnFMCTCSS, False); BtnFMCTCSS.Repaint; end; if FController.FWDSPReady then FController.FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FController.FFMCTCSSToneIdx]); end; // Сбрасываем RPT если он был включён каналом (не вручную), затем // даём UpdateRptAutoState переоценить авто-MINUS для текущей частоты if FController.FFMRptAutoActive then begin FController.FFMRptDir := RPT_NONE; FController.FFMRptAutoActive := False; if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False); if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False); UpdateRptAutoState; end; // Восстанавливаем мощность которая была до применения канала if FPreChannelPower >= 0 then begin TrkDrive.Position := FPreChannelPower; FPreChannelPower := -1; FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FDrivePercent / 100.0); end; end; end; procedure TMainForm.SetFMCTCSSTone(Idx: Integer); begin // Тонкая обёртка: команда применяет к DSP, OnControllerState(rfFMCTCSSTone) // обновит выпадающий список. Сохранена для вызовов из restore/channel/XVTR. FController.SetFMCTCSSTone(Idx); end; procedure TMainForm.BtnFMCTCSSClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.FFMCTCSSAutoActive := False; // ручное действие снимает авто-флаг FController.SetFMCTCSS(not FController.FFMCTCSSOn); end; procedure TMainForm.BtnFMCTCSSToneClick(Sender: TObject); begin FStepDropDown.ClosePopup; FCTCSSDropDown.Toggle; end; procedure TMainForm.OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer); begin SetFMCTCSSTone(Idx); end; procedure TMainForm.BtnVfoSwapClick(Sender: TObject); var Tmp: Double; BandIdx: Integer; ActiveFreq: Int64; begin // Меняем частоты местами Tmp := FController.FVfoA; FController.FVfoA := FController.FVfoB; FController.FVfoB := Tmp; // Определяем новую частоту активного VFO if FController.FActiveVfo = 0 then ActiveFreq := Round(FController.FVfoA) else ActiveFreq := Round(FController.FVfoB); // Перестраиваем приёмник FController.FCenterFreq := ActiveFreq; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; // Обновляем кнопку диапазона BandIdx := FreqToBandIdx(ActiveFreq); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; FSpecView.InvalidateRulerCache; UpdateVfoDisplay; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; procedure TMainForm.BtnVfoACopyBClick(Sender: TObject); begin // A>B: копируем частоту A в B (VFO-B получает частоту VFO-A) FController.FVfoB := FController.FVfoA; if FController.FActiveVfo = 1 then begin // B активен — перестраиваем приёмник на новую частоту B FController.FCenterFreq := Round(FController.FVfoB); if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; FreqDispB.Frequency := Round(FController.FVfoB); UpdateVfoDisplay; end; procedure TMainForm.BtnVfoBCopyAClick(Sender: TObject); begin // B>A: копируем частоту B в A (VFO-A получает частоту VFO-B) FController.FVfoA := FController.FVfoB; if FController.FActiveVfo = 0 then ApplyVfoA(Round(FController.FVfoA)) // A активен — перестраиваем приёмник else begin FreqDispA.Frequency := Round(FController.FVfoA); UpdateVfoDisplay; end; end; function TMainForm.ActiveTXFreqHz: Double; begin // Логика перенесена в контроллер; обёртка для существующих вызовов в UI. Result := FController.ActiveTXFreqHz; end; procedure TMainForm.ApplyMOX(Active: Boolean); var WasTransmitting: Boolean; begin // Safety: block TX on bands marked DoNotTx in Alex config if Active and FController.FDevConnected and (FController.FCurrentXvtr < 0) and FController.FAlexSettings.DoNotTx[EnsureRange(FController.FCurrentBand, 0, 10)] then Exit; // Safety: block TX on RX-only transverter if Active and (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].RXOnly then Exit; WasTransmitting := FController.FTransmitting; FController.FTransmitting := Active; if Active then FController.FDriveLevel := CalcDriveByte; FDUCPendingCount := 0; if FController.FTransmitting then begin BtnMOX.ClrNorm := TColor($00000044); BtnMOX.ClrText := TColor($000000FF); BtnMOX.ClrTextAct := TColor($000000FF); BtnMOX.Active := True; end else StyleButton(BtnMOX, False); // TX-overlay в SpectrumView: фиксируем актуальную TX-частоту/VFO-индекс и режим FSpecView.TXFreq := ActiveTXFreqHz; if FController.FSplitTxB then FSpecView.TXVfoIndex := 1 else FSpecView.TXVfoIndex := FController.FActiveVfo; // DUP: данные остаются от RX-анализатора (TXMode=False), но TX-полоса // фильтра рисуется поверх через TXOverlay. Без DUP при TX оба флага True. FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex; FSpecView.TXOverlay := FController.FTransmitting; // Перерисовываем сетку спектра под активный тракт (Thetis-style). ApplySpecViewGridFromState; if FController.FTransmitting and FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.ClearDUCIQQueue; FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, True, True, True); FController.FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT FController.FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле end; if FController.FWDSPReady then begin // Mic source — определяем автоматически по источнику TX: // Web TX → txmsWeb // HW PTT (FController.FHWPTTActive при входе в ApplyMOX уже True) → txmsRadio // MOX-кнопка/CAT → sound card если настроена, иначе txmsRadio if FController.FTransmitting then begin if Assigned(FWebServer) and FWebServer.WebClientActive then begin FWebMicActive := True; FController.FDSPEngine.SetTXMicSource(txmsWeb); end else if FController.FHWPTTActive then FController.FDSPEngine.SetTXMicSource(txmsRadio) else FController.FDSPEngine.SetTXMicSource(DefaultMicSource); end else if FWebMicActive then begin FWebMicActive := False; FController.FDSPEngine.SetTXMicSource(DefaultMicSource); end; // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, // чтобы TX leakage не накапливался в RXA pipeline и FFT-истории // RX-анализатора. В DUP RX-тракт работает как обычно. FController.FDSPEngine.KeepRXDuringTX := FController.FDisplayDuplex; FController.FDSPEngine.SetTXRun(FController.FTransmitting); if WasTransmitting and (not FController.FTransmitting) and (not FController.FDisplayDuplex) then begin FController.FDSPEngine.FlushRX; if Assigned(FController.FAudioOut) then FController.FAudioOut.Clear; {$IFDEF WINDOWS} // Windows: на TX→RX дропаем И RX-IQ пакеты, И аудио-выход на это окно. // Источник «хвоста» — radio FPGA TX-buffer + PA slew-down: после MOX-off // радио дотравливает буфер передатчика, RX1 ловит это как leak, и WDSP // отдаёт это и в водопад, и в звук. На Linux наблюдается окно <30мс, // на Windows может тянуться 200–500мс. Подобрать значение можно здесь // без перекомпиляции engine. На Linux код-путь не активируется. FController.FDSPEngine.BeginPostTXMute(250); {$ENDIF} end; // SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем // источник на RX, чтобы видеть приём во время передачи. if FController.FTransmitting and FController.FDisplayDuplex then FController.FDSPEngine.SetDisplaySourceTX(False); end; if (not FController.FTransmitting) and FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, False, True, True); FController.FNetwork.ClearDUCIQQueue; FController.FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX), drive=0 end; end; procedure TMainForm.BtnMOXClick(Sender: TObject); begin // Если идёт tune — клик MOX просто выходит из tune (без перехода на голос). if FController.FTuning then begin ApplyTUN(False); Exit; end; ApplyMOX(not FController.FTransmitting); end; procedure TMainForm.BtnMuteClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.ToggleMute; // логика (движок) + OnControllerState обновит кнопку end; procedure TMainForm.UpdateNRButton; begin case FController.FNRMode of 1: BtnNR.Caption := 'NR'; 2: BtnNR.Caption := 'NR2'; 3: BtnNR.Caption := 'NR3'; 4: BtnNR.Caption := 'NR4'; else BtnNR.Caption := 'NR'; end; StyleButton(BtnNR, FController.FNRMode <> 0); BtnNR.Invalidate; end; procedure TMainForm.UpdateNBButton; begin case FController.FNBMode of 1: BtnNB.Caption := 'NB'; 2: BtnNB.Caption := 'NB2'; else BtnNB.Caption := 'NB'; end; StyleButton(BtnNB, FController.FNBMode <> 0); BtnNB.Invalidate; end; procedure TMainForm.UpdateSNBButton; begin BtnSNB.Caption := 'SNB'; StyleButton(BtnSNB, FController.FSNB); BtnSNB.Invalidate; end; procedure TMainForm.UpdateANFButton; begin BtnANF.Caption := 'ANF'; StyleButton(BtnANF, FController.FANF); BtnANF.Invalidate; end; procedure TMainForm.ApplyNoiseFilterButtonsToDSP; begin if not FController.FWDSPReady then Exit; FController.FDSPEngine.SetNRMode(FController.FNRMode); FController.FDSPEngine.SetNBMode(FController.FNBMode); FController.FDSPEngine.SetSNB(FController.FSNB); FController.FDSPEngine.SetANF(FController.FANF); end; procedure TMainForm.BtnNRClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetNR((FController.FNRMode + 1) mod 5); end; procedure TMainForm.BtnNBClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetNB((FController.FNBMode + 1) mod 3); end; procedure TMainForm.BtnSNBClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetSNB(not FController.FSNB); end; procedure TMainForm.BtnANFClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetANF(not FController.FANF); end; procedure TMainForm.ApplyWfAGCNF(WfAGC, WfNF: Boolean); begin FController.SetWfAGC(WfAGC); FController.SetWfNF(WfNF); end; procedure TMainForm.ApplyADCSettings(Dither, Random: Boolean); begin FController.FDitherEnabled := Dither; FController.FRandomEnabled := Random; if FController.FNetwork.Connected and FController.FRunning then FController.FNetwork.ConfigureDDCs(1, FController.FSampleRate div 1000, 0, FController.FDitherEnabled, FController.FRandomEnabled); if FController.FDevConnected then FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); end; procedure TMainForm.ApplyWebSettings(Enabled: Boolean; Port: Integer; const BindAddr, User, Pass: string); var W: TWebSettings; begin FWebEnabled := Enabled; FWebPort := Port; FWebBindAddr := BindAddr; FWebUser := User; FWebPass := Pass; FWebServer.Reconfigure(User, Pass, BindAddr, Word(Port)); if Enabled then FWebServer.Start; W.Enabled := Enabled; W.Port := Port; W.BindAddr := BindAddr; W.User := User; W.Pass := Pass; FController.FSettings.SaveWebSettings(W); end; procedure TMainForm.OnSampleRateHidePanel; begin FPanelHidden := not FPanelHidden; if FPanelHidden then begin FLeftPanelW := PanelLeft.Width; // сохраняем реальную ширину (учитывает DPI на Win32) PanelLeft.Width := 0; FVfoOverlay.Width := 260; FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FLastSMeter); FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode); FVfoOverlay.Visible := True; PositionVfoOverlay; end else begin PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину FVfoOverlay.HandleMouseLeave; FVfoOverlay.Visible := False; end; if Assigned(FSampleRateOverlay) then FSampleRateOverlay.SetPanelHidden(FPanelHidden); ResizeSpectrumPanels; ResizeSMeter; end; procedure TMainForm.PositionVfoOverlay; var VfoX, FilterEndX, OW, NewLeft, NewTop: Integer; HiHz: Double; begin if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit; OW := FVfoOverlay.Width; if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then begin VfoX := Round((FController.FVfoA - FController.FCenterFreq + FController.FSpanHz / 2) / FController.FSpanHz * PbSpectrum.Width) end else VfoX := PbSpectrum.Width div 2; case FController.FMode of MODE_LSB: HiHz := -100; MODE_USB: HiHz := FController.FFilterBW; else HiHz := FController.FFilterBW / 2; end; if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then FilterEndX := VfoX + Round(HiHz / FController.FSpanHz * PbSpectrum.Width) else FilterEndX := VfoX; FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX)); // Всегда справа от правого края полосы фильтра. NewLeft := Max(4, Min(PbSpectrum.Width - OW - 4, FilterEndX + 6)); NewTop := 16; if (FVfoOverlay.Left <> NewLeft) or (FVfoOverlay.Top <> NewTop) then begin FVfoOverlay.Left := NewLeft; FVfoOverlay.Top := NewTop; OnVfoOverlayInvalidate(FVfoOverlay); end; end; procedure TMainForm.PbSpectrumDblClick(Sender: TObject); begin // Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели) end; procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer); var Idx: Integer; begin FController.FMode := Mode; FController.FFilterBW := FilterBW; if Mode = MODE_FM then begin FController.FFilter := FILT_FM_DEF; for Idx := 0 to FILT_FM_COUNT - 1 do if FILT_FM_BW[Idx] = FilterBW then begin FController.FFilter := Idx; FController.FFMDeviation := FILT_FM_DEV[Idx]; Break; end; end; if FController.FWDSPReady then begin FController.FDSPEngine.SetMode(FController.FMode); ApplyModeFilter; end; SyncSpecViewFreq; if Assigned(FVfoOverlay) and FVfoOverlay.Visible then begin FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FLastSMeter); FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode); PositionVfoOverlay; // перепозиционируем при смене LSB↔USB end; end; procedure TMainForm.OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean); begin // Команды контроллера применяют к DSP и через OnControllerState обновляют кнопки. FController.SetNR(NRMode); FController.SetNB(NBMode); FController.SetSNB(SNBOn); FController.SetANF(ANFOn); end; procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer); begin FController.SetAGCMode(AGCMode); end; procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject); begin FSpectrumDirty := True; if Assigned(FSpecView) then begin FSpecView.SpectrumDirty := True; FSpecView.InvalidateOverlayCache; end; if Assigned(PbSpectrum) then PbSpectrum.Invalidate; end; procedure TMainForm.OnSampleRateSelect(SampleRate: Integer); var NewRate: Integer; DDCRate: Word; begin NewRate := SampleRate; if NewRate = FController.FSampleRate then Exit; FController.FSampleRate := NewRate; FController.FSpanHz := NewRate; DDCRate := NewRate div 1000; if Assigned(FSampleRateOverlay) then FSampleRateOverlay.SetCurrentRate(NewRate); Application.ProcessMessages; FController.FBandCache[FController.FCurrentBand].SpanHz := FController.FSpanHz; // для обратной совместимости // SampleRate — глобальный: сохраняем в global settings if FController.FDevConnected then FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); // 1. Останавливаем WDSP и сбрасываем очередь старых пакетов if FController.FWDSPReady then begin FController.FWDSPReady := False; FController.FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open end; // 2. Отправляем новый rate трансиверу // (после остановки DSP — новые пакеты сразу идут с правильным rate) if FController.FNetwork.Connected then FController.FNetwork.ConfigureDDCs(1, DDCRate, 0, FController.FDitherEnabled, FController.FRandomEnabled); // 3. Восстанавливаем настройки DSP FController.FWDSPReady := FController.FDSPEngine.Initialized; if FController.FWDSPReady then begin FController.FDSPEngine.SetMode(FController.FMode); FController.FDSPEngine.SetVolume(FController.FVolume / 100.0); ApplyModeFilter; ApplyNoiseFilterButtonsToDSP; // ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC FController.FDSPEngine.SetAGCTop(FController.FAGCTop); FController.FDSPEngine.SetAGC(TWDSPAGCMode(FController.FAGCMode), 50.0); FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth); if FController.FNetwork.Connected then FController.FDSPEngine.SetShift(FController.FVfoA - FController.FCenterFreq); end; end; // =========================================================================== // Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока) // Все изменения состояния выполняем через TThread.Synchronize чтобы // не трогать UI и WDSP из чужого потока. // =========================================================================== procedure TMainForm.WebOnFreq(Hz: Double); begin FWebSyncFreq := Hz; FWebSyncM := SyncWebFreq; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebFreq; var BandIdx: Integer; begin // Веб шлёт "freq" для активного VFO if FController.FActiveVfo = 0 then ApplyVfoA(Round(FWebSyncFreq)) else begin FController.FVfoB := FWebSyncFreq; FreqDispB.Frequency := Round(FController.FVfoB); FController.FCenterFreq := FController.FVfoB; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FRunning then FController.FNetwork.SetRunAndFreq(True, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), FController.FDriveLevel); BandIdx := FreqToBandIdx(FController.FVfoB); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; UpdateVfoDisplay; end; end; procedure TMainForm.WebOnMode(Mode: Integer); begin FWebSyncInt := Mode; FWebSyncM := SyncWebMode; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebMode; var i: Integer; begin if (FWebSyncInt < 0) or (FWebSyncInt >= MODE_COUNT) then Exit; FController.FMode := FWebSyncInt; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FController.FMode); if FController.FWDSPReady then FController.FDSPEngine.SetMode(FController.FMode); UpdateFilterButtons; ApplyModeFilter; SyncSpecViewFreq; FController.FBandCache[FController.FCurrentBand].Mode := FController.FMode; end; procedure TMainForm.WebOnFilter(BW: Integer); begin FWebSyncInt := BW; FWebSyncM := SyncWebFilter; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebFilter; var best, dist, i, j: Integer; BW_arr: array[0..FILT_COUNT-1] of Integer; Idx: Integer; begin // Negative value = -(idx+1) means filter index was passed directly if FWebSyncInt < 0 then begin Idx := (-FWebSyncInt) - 1; if (Idx >= 0) and (Idx < FILT_COUNT) then begin FController.FFilter := Idx; case FController.FMode of 0,1: FController.FFilterBW := FILT_SSB_BW[Idx]; 2: FController.FFilterBW := FILT_DSB_BW[Idx]; 3,4: FController.FFilterBW := FILT_CW_BW[Idx]; 5: if Idx < FILT_FM_COUNT then begin FController.FFilterBW := FILT_FM_BW[Idx]; FController.FFMDeviation := FILT_FM_DEV[Idx]; end; else FController.FFilterBW := FILT_AM_BW[Idx]; end; for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FController.FFilter); ApplyModeFilter; SyncSpecViewFreq; FController.FBandCache[FController.FCurrentBand].FilterBW := FController.FFilterBW; end; Exit; end; // Positive value = bandwidth in Hz, find closest filter FController.FFilterBW := FWebSyncInt; for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt; case FController.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]; 5: 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] - FWebSyncInt) < dist then begin dist := Abs(BW_arr[i] - FWebSyncInt); best := i; end; FController.FFilter := best; if FController.FMode = 5 then FController.FFMDeviation := FILT_FM_DEV[best]; for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FController.FFilter); ApplyModeFilter; SyncSpecViewFreq; FController.FBandCache[FController.FCurrentBand].FilterBW := FController.FFilterBW; end; procedure TMainForm.WebOnAGC(Mode: Integer); begin FWebSyncInt := Mode; FWebSyncM := SyncWebAGC; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebAGC; begin FController.SetAGCMode(FWebSyncInt); // единый маппинг (исправляет старый web-баг) end; procedure TMainForm.WebOnAGCTop(DB: Integer); begin FWebSyncInt := DB; FWebSyncM := SyncWebAGCTop; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebAGCTop; begin FController.SetAGCTop(FWebSyncInt); // клампит, применяет, OnControllerState обновит UI end; procedure TMainForm.WebOnBand(Idx: Integer); begin FWebSyncInt := Idx; FWebSyncM := SyncWebBand; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebBand; begin if (FWebSyncInt < 0) or (FWebSyncInt >= BAND_COUNT) then Exit; // Выйти из XVTR при переходе на HF из web. DeactivateXvtr вернёт HF-состояние. if FController.FCurrentXvtr >= 0 then begin DeactivateXvtr; if FWebSyncInt = FController.FCurrentBand then Exit; end else if FWebSyncInt = FController.FCurrentBand then Exit; if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.Save; end; FController.FCurrentBand := FWebSyncInt; RestoreBand(FController.FCurrentBand); end; procedure TMainForm.WebOnXvtrBand(Idx: Integer); begin FWebSyncInt := Idx; FWebSyncM := SyncWebXvtrBand; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebXvtrBand; begin if FWebSyncInt < 0 then begin DeactivateXvtr; Exit; end; if (FWebSyncInt >= CFG_XVTR_COUNT) then Exit; if not FController.FXvtrSettings.Entries[FWebSyncInt].Enabled then Exit; if FWebSyncInt = FController.FCurrentXvtr then Exit; if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.Save; end; ActivateXvtrBand(FWebSyncInt); end; procedure TMainForm.WebOnSpan(Hz: Integer); begin FWebSyncInt := Hz; FWebSyncM := SyncWebSpan; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebSpan; const ValidSpans: array[0..5] of Integer = (48000, 96000, 192000, 384000, 768000, 1536000); var i: Integer; begin for i := 0 to High(ValidSpans) do if ValidSpans[i] = FWebSyncInt then begin OnSampleRateSelect(FWebSyncInt); Exit; end; end; procedure TMainForm.WebOnVolume(V: Integer); begin FWebSyncInt := V; FWebSyncM := SyncWebVolume; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebVolume; begin FController.SetVolume(FWebSyncInt); end; procedure TMainForm.WebOnWfAGC(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebWfAGC; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebWfAGC; begin FController.SetWfAGC(FWebSyncBool); end; procedure TMainForm.WebOnWfNF(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebWfNF; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebWfNF; begin FController.SetWfNF(FWebSyncBool); end; // ── Run ──────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnRun(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebRun; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebRun; begin // Toggle only if state differs if FWebSyncBool <> FController.FRunning then BtnStartStopClick(nil); end; // ── Mute ─────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnMute(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebMute; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebMute; begin FController.SetMute(FWebSyncBool); // идемпотентно; OnControllerState обновит UI end; // ── CTUN ────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnCtun(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebCtun; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebCtun; begin if FWebSyncBool <> FController.FCTun then BtnCTunClick(nil); end; // ── NR ──────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnNR(Mode: Integer); begin FWebSyncInt := Mode; FWebSyncM := SyncWebNR; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebNR; begin FController.SetNR(FWebSyncInt); // клампит и применяет; OnControllerState обновит кнопку end; // ── NB ──────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnNB(Mode: Integer); begin FWebSyncInt := Mode; FWebSyncM := SyncWebNB; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebNB; begin FController.SetNB(FWebSyncInt); // клампит и применяет; OnControllerState обновит кнопку end; procedure TMainForm.WebOnSNB(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebSNB; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebSNB; begin FController.SetSNB(FWebSyncBool); end; // ── ANF ─────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnANF(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebANF; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebANF; begin FController.SetANF(FWebSyncBool); end; // ── VFO-B freq (из веба) ───────────────────────────────────────────────── procedure TMainForm.WebOnFreqB(Hz: Double); begin FWebSyncFreq := Hz; FWebSyncM := SyncWebFreqB; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebFreqB; var BandIdx: Integer; begin FController.FVfoB := FWebSyncFreq; FreqDispB.Frequency := Round(FController.FVfoB); if FController.FActiveVfo = 1 then begin FController.FCenterFreq := FController.FVfoB; if not FController.FCTun then if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FRunning then FController.FNetwork.SetRunAndFreq(True, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), FController.FDriveLevel); BandIdx := FreqToBandIdx(FController.FVfoB); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; UpdateVfoDisplay; end; // ── Активный VFO (из веба) ─────────────────────────────────────────────── procedure TMainForm.WebOnActiveVfo(Idx: Integer); begin FWebSyncInt := Idx; FWebSyncM := SyncWebActiveVfo; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebActiveVfo; begin if (FWebSyncInt < 0) or (FWebSyncInt > 1) then Exit; if FWebSyncInt = FController.FActiveVfo then Exit; ActivateVfo(FWebSyncInt); end; // ── CTUN drag center scroll (из веба) ──────────────────────────────────── procedure TMainForm.WebOnCenter(Hz: Double); begin FWebSyncFreq := Hz; FWebSyncM := SyncWebCenter; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebCenter; begin // Сдвигаем DDC-центр без смены VFO (аналог CTUN-drag в десктопе) FController.FCenterFreq := FWebSyncFreq; if FController.FWDSPReady then begin if FController.FActiveVfo = 0 then FController.FDSPEngine.SetShift(FController.FVfoA - FController.FCenterFreq) else FController.FDSPEngine.SetShift(FController.FVfoB - FController.FCenterFreq); end; if FController.FNetwork.Connected and FController.FNetwork.Running then FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FSpecView.InvalidateRulerCache; FSpectrumDirty := True; end; procedure TMainForm.WebOnMOX(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebMOX; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebMOX; begin ApplyMOX(FWebSyncBool); end; procedure TMainForm.WebOnDrive(V: Integer); begin FWebSyncInt := V; FWebSyncM := SyncWebDrive; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebDrive; begin // Установка Position вызывает TrkDriveChange через OnChange автоматически TrkDrive.Position := FWebSyncInt; end; procedure TMainForm.WebOnFreqA(Hz: Double); begin FWebSyncFreq := Hz; FWebSyncM := SyncWebFreqA; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebFreqA; begin ApplyVfoA(Round(FWebSyncFreq)); end; procedure TMainForm.WebOnAttn(Idx: Integer); begin FWebSyncInt := Idx; FWebSyncM := SyncWebAttn; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebAttn; begin FController.SetAtten(FWebSyncInt); end; procedure TMainForm.WebOnFMStep(Idx: Integer); begin FWebSyncInt := Idx; FWebSyncM := SyncWebFMStep; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebFMStep; begin SetFMStep(Max(0, Min(FM_STEP_COUNT - 1, FWebSyncInt))); SaveCurrentBand; end; procedure TMainForm.WebOnTun(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebTun; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebTun; begin ApplyTUN(FWebSyncBool); end; procedure TMainForm.WebOnMic(Samples: PSingle; Count: Integer); var Buf: array[0..5759] of Double; SP: PSingle; i, n: Integer; begin if not FController.FWDSPReady then Exit; n := Count; if n > Length(Buf) then n := Length(Buf); SP := Samples; for i := 0 to n - 1 do begin Buf[i] := SP^; Inc(SP); end; FController.FDSPEngine.PushTXMicSamplesD(Buf, n); end; procedure TMainForm.TrkDriveChange(Sender: TObject); begin FController.FDrivePercent := TrkDrive.Position; // слайдер — ввод; контроллер — истина FPreChannelPower := -1; // ручное изменение снимает сохранённую мощность канала FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FDrivePercent / 100.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; function TMainForm.CalcDriveByte: Byte; // Thetis-compatible formula: // drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 ) // where: // target_volts = sqrt( 10^((target_dBm)/10) * 0.05 ) [E=sqrt(P*R), R=50, P in mW] // target_dBm = 10*log10(pwr_W * 1000) - Cal // pwr_W = Pos/100 * FController.FPAMaxPower // Cal = per-band PA gain in dB (HF: ~38.8-41.3, VHF default: ~56.2) var Cal: Double; Pos: Integer; XvtrActive: Boolean; pwr_W: Double; target_dBm: Double; target_volts: Double; audio_vol: Double; begin XvtrActive := (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled; if XvtrActive then Cal := FController.FVHFBandCal[FController.FCurrentXvtr] else if (FController.FCurrentBand >= 0) and (FController.FCurrentBand < BAND_COUNT) then Cal := FController.FPABandCal[FController.FCurrentBand] else Cal := 38.8; // Pos — итоговый процент драйва 0..100 if FController.FTuning then begin if XvtrActive and FController.FXvtrSettings.UseXVTRTunePower then Pos := EnsureRange(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].TXPower, 0, 100) else Pos := EnsureRange(FController.FTXSettings.TUNLevel, 0, 100); end else begin if XvtrActive then Pos := Round(FController.FDrivePercent * FController.FXvtrSettings.Entries[FController.FCurrentXvtr].TXPower / 100.0) else Pos := FController.FDrivePercent; end; if Pos <= 0 then begin Result := 0; Exit; end; pwr_W := Pos / 100.0 * FController.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 TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer); // FTXOutBufSize не обязан быть кратен 240, поэтому копим хвост между // вызовами и шлём только полные DUC-пакеты. Иначе зануление хвоста // последнего пакета давало периодический провал на каждом TX-блоке // (≈47 Гц гул при 192k IQ rate). Также клампим в 24-bit чтобы пики // от компрессора/EQ (значение чуть >1.0) не оборачивались через wrap. function ScaleIQ24(V: Double): Integer; var S: Double; begin S := V * 8388607.0; if S > 8388607.0 then Result := 8388607 else if S < -8388608.0 then Result := -8388608 else Result := Round(S); end; var i: Integer; begin if not FController.FNetwork.Connected then Exit; for i := 0 to Count - 1 do begin FDUCPendingI[FDUCPendingCount] := ScaleIQ24(Buf[i * 2]); FDUCPendingQ[FDUCPendingCount] := ScaleIQ24(Buf[i * 2 + 1]); Inc(FDUCPendingCount); if FDUCPendingCount >= 240 then begin FController.FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ); FDUCPendingCount := 0; end; end; end; procedure TMainForm.OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); var i: Integer; begin FController.FPAMaxPower := MaxPower; if FSpecView <> nil then FSpecView.PAMaxPower := MaxPower; for i := 0 to BAND_COUNT - 1 do begin if i <= High(BandCal) then FController.FPABandCal[i] := BandCal[i] else FController.FPABandCal[i] := 100.0; end; FController.FDriveLevel := CalcDriveByte; if FController.FNetwork.Connected and FController.FNetwork.Running then FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); end; procedure TMainForm.OnVHFCalSettingsChange(const VHFCal: array of Double); var i: Integer; begin for i := 0 to CFG_XVTR_COUNT - 1 do begin if i <= High(VHFCal) then FController.FVHFBandCal[i] := VHFCal[i] else FController.FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT; end; FController.FDriveLevel := CalcDriveByte; if FController.FNetwork.Connected and FController.FNetwork.Running then FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); end; procedure TMainForm.OnTXSettingsChange(const T: TTXSettings); // Любая правка в Settings→Transmit летит сюда: обновляем FController.FTXSettings, // перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific. var OldMicByte, NewMicByte: Byte; OldAtt, OldTUNLevel, OldTUNFreq: Integer; GridChanged: Boolean; begin OldMicByte := BuildMicLineSelectByte; OldAtt := FController.FTXSettings.AttOnTX; OldTUNLevel := FController.FTXSettings.TUNLevel; OldTUNFreq := FController.FTXSettings.TUNFreq; GridChanged := (FTXSpecRefLevel <> T.TXSpecRefLevel) or (FTXSpecRange <> T.TXSpecRange) or (FTXSpecGridStep <> T.TXSpecGridStep); FController.FTXSettings := T; NewMicByte := BuildMicLineSelectByte; // TX-grid в момент правки — обновляем рабочие поля и (если на передаче) пушим в FSpecView FTXSpecRefLevel := FController.FTXSettings.TXSpecRefLevel; FTXSpecRange := FController.FTXSettings.TXSpecRange; if FController.FTXSettings.TXSpecGridStep > 0 then FTXSpecGridStep := FController.FTXSettings.TXSpecGridStep; if GridChanged and FController.FTransmitting then ApplySpecViewGridFromState; ApplyTXSettingsToDSP; if (OldMicByte <> NewMicByte) or (OldAtt <> FController.FTXSettings.AttOnTX) then SendDUCSpecificFromSettings; // TUN-параметры live: если идёт tune — обновить тон/drive, иначе просто запомнить. if FController.FTuning then begin if (OldTUNFreq <> FController.FTXSettings.TUNFreq) and FController.FWDSPReady then FController.FDSPEngine.SetTXTone(True, FController.FTXSettings.TUNFreq, 1.0); if OldTUNLevel <> FController.FTXSettings.TUNLevel then begin FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; end; end; // Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json if FController.FDevConnected then begin FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); FController.FSettings.Save; end; end; procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings); begin FController.FAlexSettings := A; if FController.FNetwork.Connected then FController.FNetwork.SetAlexConfig(A); if FController.FDevConnected then begin FController.FSettings.SaveAlex(FController.FDevMAC, A); FController.FSettings.Save; end; end; // =========================================================================== // XVTR (Transverter) integration // =========================================================================== // Ищет XVTR слот, в чьё [FreqBegin..FreqEnd] попадает данная видимая частота. // Возвращает индекс 0..CFG_XVTR_COUNT-1 или -1. function TMainForm.FindXvtrIdxForFreq(FreqHz: Double): Integer; var i: Integer; begin Result := -1; for i := 0 to CFG_XVTR_COUNT - 1 do with FController.FXvtrSettings.Entries[i] do if Enabled and (FreqHz >= FreqBegin) and (FreqHz <= FreqEnd) then Exit(i); end; // Транслирует видимую частоту в IF (то, что слышит/передаёт трансивер). // Если активный XVTR не задан — возвращает входную частоту без изменений. function TMainForm.XvtrTranslate(VisibleHz: Double): Double; begin // Логика перенесена в контроллер; обёртка для существующих вызовов в UI. Result := FController.XvtrTranslate(VisibleHz); end; // Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его // настроек. Управляет XVTR Enable bit, T/R relay suppression. // XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex // tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis. procedure TMainForm.ApplyXvtrToNetwork; var En: Boolean; DisablePA: Boolean; RxAnt: Byte; begin if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then begin En := True; DisablePA := FController.FXvtrSettings.Entries[FController.FCurrentXvtr].DisablePA; RxAnt := FController.FXvtrSettings.Entries[FController.FCurrentXvtr].RXAntenna; end else begin En := False; DisablePA := False; RxAnt := 0; end; if FController.FNetwork.Connected then FController.FNetwork.SetXvtrMode(En, DisablePA, RxAnt); end; // Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые, // создаёт новые для enabled слотов в 3-м и 4-м ряду PanelBands. procedure TMainForm.RelayoutBelowBands; var Y: Integer; begin if PanelBands = nil then Exit; Y := PanelBands.Top + PanelBands.Height + 2; PanelMode.Top := Y; Y := Y + PanelMode.Height + 2; PanelFilter.Top := Y; Y := Y + PanelFilter.Height + 2; if (PanelFMStep <> nil) and PanelFMStep.Visible then begin PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2; end; if (PanelFMSQ <> nil) and PanelFMSQ.Visible then begin PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2; end; if (PanelFMCTCSS <> nil) and PanelFMCTCSS.Visible then begin PanelFMCTCSS.Top := Y; Y := Y + PanelFMCTCSS.Height + 2; end; if (PanelFMRpt <> nil) and PanelFMRpt.Visible then begin PanelFMRpt.Top := Y; Y := Y + PanelFMRpt.Height + 2; end; end; procedure TMainForm.RebuildXvtrButtons; var i, Pos, XvtrRows: Integer; BtnH, RowH, MaxBottom: Integer; B: TFlatButton; Cap: string; begin // Уничтожаем все старые for i := 0 to CFG_XVTR_COUNT - 1 do if BtnXvtrBand[i] <> nil then begin BtnXvtrBand[i].Free; BtnXvtrBand[i] := nil; end; if PanelBands = nil then Exit; // Измеряем фактический bottom последнего ряда обычных кнопок BAND // (уже отмасштабированы LCL при показе формы) MaxBottom := 0; BtnH := 0; for i := 0 to BAND_COUNT - 1 do if BtnBand[i] <> nil then begin if BtnBand[i].Top + BtnBand[i].Height > MaxBottom then MaxBottom := BtnBand[i].Top + BtnBand[i].Height; BtnH := BtnBand[i].Height; end; if BtnH = 0 then BtnH := MulDiv(24, Screen.PixelsPerInch, 96); RowH := BtnH + 3; // шаг ряда: высота кнопки + 3px зазор (как 27=24+3 при 1x) Pos := 0; // позиция среди enabled слотов for i := 0 to CFG_XVTR_COUNT - 1 do if FController.FXvtrSettings.Entries[i].Enabled then begin if Pos >= 12 then Break; // 2 ряда по 6 кнопок Cap := FController.FXvtrSettings.Entries[i].ButtonText; if Cap = '' then Cap := 'X' + IntToStr(i + 1); B := TFlatButton.Create(Self); B.Parent := PanelBands; B.Caption := Cap; B.Left := LeftPanelButtonLeft(PanelBands.Width, 6, Pos mod 6); B.Top := MaxBottom + 4 + (Pos div 6) * RowH; B.Width := LeftPanelButtonWidth(PanelBands.Width, 6, Pos mod 6); B.Height := BtnH; B.OnClick := BtnXvtrBandClick; B.Tag := -(i + 1); // отрицательное значение = XVTR индекс BtnXvtrBand[i] := B; StyleButton(B, i = FController.FCurrentXvtr); Inc(Pos); end; // Динамически подгоняем высоту PanelBands под реальное содержимое if Pos = 0 then XvtrRows := 0 else if Pos <= 6 then XvtrRows := 1 else XvtrRows := 2; if XvtrRows = 0 then PanelBands.Height := MaxBottom + 6 else PanelBands.Height := MaxBottom + 4 + XvtrRows * RowH + 6; RelayoutBelowBands; end; // Обработчик клика по XVTR-кнопке. Tag = -(idx+1). procedure TMainForm.BtnXvtrBandClick(Sender: TObject); var TagVal, Idx: Integer; begin TagVal := (Sender as TFlatButton).Tag; Idx := -TagVal - 1; if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; if not FController.FXvtrSettings.Entries[Idx].Enabled then Exit; if FController.FCurrentXvtr = Idx then Exit; // уже на этом XVTR // Сохраняем текущий band перед сменой (как BtnBandClick) if FController.FDevConnected then begin SaveCurrentBand; FController.FSettings.Save; end; ActivateXvtrBand(Idx); end; // Активирует XVTR-band: устанавливает видимую частоту по LastFreq или // середине [FreqBegin..FreqEnd], применяет network XVTR-режим. procedure TMainForm.ActivateXvtrBand(Idx: Integer); var Vis: Double; i: Integer; E: TXvtrEntry; begin if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; if not FController.FXvtrSettings.Entries[Idx].Enabled then Exit; FController.FCurrentXvtr := Idx; E := FController.FXvtrSettings.Entries[Idx]; // Выбор начальной частоты: LastFreq если в диапазоне, иначе середина Vis := E.LastFreq; if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then Vis := (E.FreqBegin + E.FreqEnd) / 2.0; // Отключаем кнопки HF band for i := 0 to BAND_COUNT - 1 do StyleButton(BtnBand[i], False); // Подсвечиваем активную XVTR-кнопку for i := 0 to CFG_XVTR_COUNT - 1 do if BtnXvtrBand[i] <> nil then StyleButton(BtnXvtrBand[i], i = Idx); // Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant) ApplyXvtrToNetwork; // Восстанавливаем Mode + FM Squelch + CTCSS для этого XVTR-слота FController.FFMSQOn := E.LastFMSQOn; FController.FFMSQLevel := E.LastFMSQLevel; FController.FMode := E.LastMode; FController.FFilter := E.LastFilterIdx; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FController.FMode); if FController.FWDSPReady then FController.FDSPEngine.SetMode(FController.FMode); UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI ApplyModeFilter; FController.FFMCTCSSOn := E.LastCTCSSOn; FController.FFMCTCSSAutoActive := E.LastCTCSSAutoActive; if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FController.FFMCTCSSOn); SetFMCTCSSTone(E.LastCTCSSToneIdx); ApplyFMSquelch; FController.FFMStepOn := E.LastFMStepOn; SetFMStep(E.LastFMStepIdx); if BtnFMStep <> nil then StyleButton(BtnFMStep, FController.FFMStepOn); FController.FFMRptDir := E.LastFMRptDir; FController.FFMRptAutoActive := E.LastFMRptAutoActive; FController.FFMRptOffsetHz := E.LastFMRptOffsetHz; if FController.FFMRptOffsetHz <= 0 then FController.FFMRptOffsetHz := RptDefaultOffsetHz(E.FreqBegin); if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS); if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS); if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); // AGC FController.FAGCMode := E.LastAGCMode; FController.FAGCTop := E.LastAGCTop; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); TrkAGC.Position := FController.FAGCTop; LblAGCTop.Caption := Format('%ddB', [FController.FAGCTop]); if FController.FWDSPReady then begin FController.FDSPEngine.SetAGCTop(FController.FAGCTop); FController.FDSPEngine.SetAGC(TWDSPAGCMode(FController.FAGCMode), 50.0); end; FSpecView.AGCTop := FController.FAGCTop; // CTUN FController.FCTun := E.LastCTun; StyleButton(BtnCTun, FController.FCTun); // VFO на видимую частоту; FController.FCenterFreq тоже visible FController.FVfoA := Vis; FController.FCenterFreq := Vis; FreqDispA.Frequency := Round(FController.FVfoA); if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); // Пересчитываем drive byte для нового XVTR слота и отправляем в сеть FController.FDriveLevel := CalcDriveByte; if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FSpecView.DrawWaterfall; PbWaterfall.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; UpdateWidebandFrequencyView; if PbWideband <> nil then PbWideband.Invalidate; if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate; PushXvtrToWeb; end; // Деактивирует XVTR — возвращает к HF-режиму. // FController.FCurrentBand был сохранён неизменным во время XVTR (см. ActivateXvtrBand, // ApplyVfoA, SaveCurrentBand) — он указывает на HF-диапазон, с которого // пользователь зашёл в трансвертер. RestoreBand вернёт VFO/Mode/Filter/AGC // /CTUN/Span этого диапазона и обновит подсветку HF band-кнопки. procedure TMainForm.DeactivateXvtr; var i: Integer; begin if FController.FCurrentXvtr < 0 then Exit; // Сохраняем полное состояние XVTR-слота (раньше сохранялся только LastFreq) if FController.FCurrentXvtr < CFG_XVTR_COUNT then begin FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastMode := FController.FMode; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFilterIdx := FController.FFilter; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQOn := FController.FFMSQOn; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQLevel := FController.FFMSQLevel; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSOn := FController.FFMCTCSSOn; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSToneIdx := FController.FFMCTCSSToneIdx; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMStepOn := FController.FFMStepOn; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMStepIdx := FController.FFMStepIdx; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptDir := FController.FFMRptDir; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptOffsetHz := FController.FFMRptOffsetHz; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTun := FController.FCTun; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastAGCMode := FController.FAGCMode; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastAGCTop := FController.FAGCTop; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSAutoActive := FController.FFMCTCSSAutoActive; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptAutoActive := FController.FFMRptAutoActive; if FController.FDevConnected then begin FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings); FController.FSettings.Save; end; end; FController.FCurrentXvtr := -1; // Сбрасываем подсветку XVTR-кнопок for i := 0 to CFG_XVTR_COUNT - 1 do if BtnXvtrBand[i] <> nil then StyleButton(BtnXvtrBand[i], False); ApplyXvtrToNetwork; // Восстанавливаем HF band: VFO/Mode/Filter/AGC/CTUN/Span из кэша. // RestoreBand сам обновит подсветку HF band-кнопки и сетевое состояние. if (FController.FCurrentBand >= 0) and (FController.FCurrentBand < CFG_BAND_COUNT) then RestoreBand(FController.FCurrentBand); UpdateWidebandFrequencyView; if PbWideband <> nil then PbWideband.Invalidate; if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate; PushXvtrToWeb; end; procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings); begin FController.FXvtrSettings := X; RebuildXvtrButtons; // Если активный XVTR стал disabled — выйти из XVTR режима if (FController.FCurrentXvtr >= 0) and ((FController.FCurrentXvtr >= CFG_XVTR_COUNT) or (not FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled)) then DeactivateXvtr; // Перепрошить XVTR в сеть (если параметры текущего изменились) ApplyXvtrToNetwork; UpdateWidebandFrequencyView; if PbWideband <> nil then PbWideband.Invalidate; if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate; PushXvtrToWeb; if FController.FDevConnected then begin FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings); FController.FSettings.Save; end; end; procedure TMainForm.PushXvtrToWeb; var Arr: TWebXvtrArray; i, Cnt: Integer; begin if FWebServer = nil then Exit; Cnt := 0; for i := 0 to CFG_XVTR_COUNT - 1 do if FController.FXvtrSettings.Entries[i].Enabled then Inc(Cnt); SetLength(Arr, Cnt); Cnt := 0; for i := 0 to CFG_XVTR_COUNT - 1 do if FController.FXvtrSettings.Entries[i].Enabled then begin Arr[Cnt].Idx := i; Arr[Cnt].Name := FController.FXvtrSettings.Entries[i].ButtonText; Inc(Cnt); end; FWebServer.SetXvtrBands(Arr, FController.FCurrentXvtr); end; procedure TMainForm.TrkVolumeChange(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetVolume(TrkVolume.Position); end; // =========================================================================== // Mouse wheel — перестройка VFO // =========================================================================== procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); // Колёсико на форме/спектре/водопаде — меняет VFO с квантованием // Шаг зависит от модификаторов: Ctrl+Shift=1МГц, Ctrl=1кГц, Shift=10Гц, иначе 100Гц // Квантование: результат выровнен до кратного шагу (как в FreqDisplay) // CTUN логика: через ApplyVfoA var Step: Int64; Delta: Int64; Base: Int64; NewFreq: Int64; begin if (FController.FMode = MODE_FM) and FController.FFMStepOn and not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := FM_STEP_HZ[FController.FFMStepIdx] else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000 else if ssCtrl in Shift then Step := 1000 else if ssShift in Shift then Step := 10 else Step := 100; Delta := 1; if WheelDelta < 0 then Delta := -1; // Квантование (та же логика что в FreqDisplay.ChangeByDigit) Base := (Round(FController.FVfoA) div Step) * Step; if Delta > 0 then NewFreq := Base + Step else begin if Round(FController.FVfoA) = Base then NewFreq := Base - Step else NewFreq := Base; // снэп к нижней границе end; // В XVTR-режиме HF-клэмп не применяем: ApplyVfoA сам ограничит по FreqBegin/FreqEnd if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then NewFreq := Max(0, NewFreq) else NewFreq := Max(30000, Min(60000000, NewFreq)); ApplyVfoA(NewFreq); Handled := True; end; procedure TMainForm.BtnAGCModeClick(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetAGCMode((Sender as TFlatButton).Tag); end; procedure TMainForm.TrkAGCChange(Sender: TObject); begin if FSyncingFromController then Exit; FController.SetAGCTop(TrkAGC.Position); end; // =========================================================================== // DSP/Audio callbacks — вызываются из рабочих потоков // =========================================================================== procedure TMainForm.OnAudioReady(const Left, Right: array of Single; Count: Integer); var MonoBuf: array[0..1023] of Single; i: Integer; begin // Дублируем RX-аудио во встроенный динамик трансивера (DDC Audio, port 1028). if FController.FSendAudioToRadio then FController.FNetwork.SendSpeakerAudio(Left, Right, Count); if Assigned(FWebServer) and FWebServer.WebClientActive then begin for i := 0 to Count - 1 do MonoBuf[i] := (Left[i] + Right[i]) * 0.5; FWebServer.PushAudio(@MonoBuf[0], Count); Exit; end; FController.FAudioOut.Write(Left, Right, Count); end; procedure TMainForm.OnSpectrumReady(const Pixels: array of Single; Count: Integer); var i, N: Integer; begin N := Min(Count, 1024); for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i]; // local copy for WebServer FSpectrumBufCount := N; FSpecView.SetSpectrumData(Pixels, Count); FSpectrumDirty := True; end; procedure TMainForm.OnWaterfallReady(const Pixels: array of Single; Count: Integer); var i, N: Integer; begin N := Min(Count, 1024); for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i]; // local copy for WebServer FWaterfallBufCount := N; Inc(FWaterfallFrameCounter); if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then begin FWaterfallFrameCounter := 0; FSpecView.SetWaterfallData(Pixels, Count); FWaterfallDirty := True; end; end; // =========================================================================== // Settings apply methods (вызываются из TSettingsForm) // =========================================================================== procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer; SpecAvgTimeMS: Double); begin if FController.FWDSPReady then FController.FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS); if FController.FWDSPReady then FController.FDSPEngine.SetFFTParams(FFTSize, WinType); FSpectrumDirty := True; end; procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double); begin if FController.FWDSPReady then FController.FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS); FWfManualHigh := WfHigh; FWfManualLow := WfLow; FWfAGCOffset := WfAGCOffset; FSpecView.WfManualHigh := FWfManualHigh; FSpecView.WfManualLow := FWfManualLow; FSpecView.WfAGCOffset := FWfAGCOffset; FSpecView.ResetWfAvgBuf; FSpectrumDirty := True; end; procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double); begin FSpecRefLevel := RefLevel; FSpecRange := Range; if GridStep > 0 then FSpecGridStep := GridStep; // На передаче FSpecView показывает TX-сетку — RX-правка не должна её перетирать. if not FController.FTransmitting then ApplySpecViewGridFromState; end; procedure TMainForm.ApplySpecViewGridFromState; begin // При DUP во время TX данные приходят от RX-анализатора → используем RX-сетку. if FController.FTransmitting and not FController.FDisplayDuplex then begin FSpecView.SpecRefLevel := FTXSpecRefLevel; FSpecView.SpecRange := FTXSpecRange; FSpecView.SpecGridStep := FTXSpecGridStep; end else begin FSpecView.SpecRefLevel := FSpecRefLevel; FSpecView.SpecRange := FSpecRange; FSpecView.SpecGridStep := FSpecGridStep; end; InvalidateGridCache; FSpectrumDirty := True; end; procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string); begin if FController.FAudioOut.IsOpen then FController.FAudioOut.Close; FController.FAudioOut.DeviceIndex := DevIndex; FController.FAudioOutDevName := DevName; try FController.FAudioOut.Open; except end; end; procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); begin if FController.FAudioIn.IsOpen then FController.FAudioIn.Close; FController.FAudioIn.DeviceIndex := DevIndex; FController.FAudioInDevName := DevName; if DevIndex >= 0 then begin try FController.FAudioIn.Open; except end; end; end; procedure TMainForm.ApplyAudioBufferSize(BufferSize: Integer); var WasOpen: Boolean; begin if BufferSize <= 128 then BufferSize := 128 else if BufferSize <= 256 then BufferSize := 256 else BufferSize := 512; if FController.FAudioOut.OutputBufferSize = BufferSize then Exit; WasOpen := FController.FAudioOut.IsOpen; if WasOpen then FController.FAudioOut.Close; FController.FAudioOut.OutputBufferSize := BufferSize; if WasOpen then begin try FController.FAudioOut.Open; except end; end; FController.FSettings.SaveAudioBufferSize(BufferSize); FController.FSettings.Save; end; procedure TMainForm.ApplySendAudioToRadio(Enabled: Boolean); begin FController.FSendAudioToRadio := Enabled; // Один переключатель: вкл = слать поток + снять мьют динамика, // выкл = замьютить динамик (byte 1400 bit1) и не слать поток. FController.FNetwork.SetSpeakerAudio(Enabled); if FController.FDevConnected then FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); end; procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall, ShowWideband, WidebandFill, SpectrumFill: Boolean); begin FShowSpectrum := ShowSpectrum; FShowWaterfall := ShowWaterfall; FShowWideband := ShowWideband; FWidebandFill := WidebandFill; FSpectrumFill := SpectrumFill; if Assigned(FWidebandView) then FWidebandView.SetFillSpectrum(FWidebandFill); FSpecView.FillSpectrum := FSpectrumFill; if Assigned(FController.FNetwork) and FController.FNetwork.Connected then FController.FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32); ResizeSpectrumPanels; end; procedure TMainForm.ApplyFPS(FPS: Integer); begin if FPS < 1 then FPS := 1; if FPS > 100 then FPS := 100; FDisplayFPS := FPS; FSpectrumTimer.Interval := 1000 div FPS; // CPU path keeps the historical every-other-frame waterfall update. // OpenGL uploads only one texture row per frame, so it can match display FPS. if FUseOpenGLSpectrum then FWaterfallFrameInterval := 1 else FWaterfallFrameInterval := 2; FWaterfallFrameCounter := 0; FWaterfallDirty := True; FSpecView.WfFrameInterval := FWaterfallFrameInterval; if FController.FWDSPReady then FController.FDSPEngine.SetDisplayFPS(FPS); end; procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer); const MaxFreqs: array[3..5] of Int64 = (999999999, 9999999999, 99999999999); begin Digits := EnsureRange(Digits, 3, 5); FFreqMhzDigits := Digits; FreqDispA.MinMhzDigits := Digits; FreqDispB.MinMhzDigits := Digits; FreqDispA.MaxFreq := MaxFreqs[Digits]; FreqDispB.MaxFreq := MaxFreqs[Digits]; if Assigned(FWebServer) then FWebServer.FreqMhzDigits := Digits; LayoutTopVfoBlock; end; procedure TMainForm.BtnSettingsClick(Sender: TObject); var SF: TSettingsForm; begin if not Assigned(FSettingsForm) then begin SF := TSettingsForm.Create(Self); FSettingsForm := SF; SF.OnDisplayChange := ApplyDisplayParams; SF.OnWaterfallChange := ApplyWaterfallParams; SF.OnGridChange := ApplyGridParams; SF.OnAudioDevChange := ApplyAudioDevice; SF.OnAudioInDevChange := ApplyAudioInputDevice; SF.OnAudioBufferChange := ApplyAudioBufferSize; SF.OnSendAudioToRadioChange := ApplySendAudioToRadio; SF.OnVisibilityChange := ApplyVisibility; SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; SF.OnVHFCalChange := OnVHFCalSettingsChange; SF.OnCATChange := OnCATSettingsChange; SF.OnThemeChange := SetLightTheme; SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits; SF.OnTXChange := OnTXSettingsChange; SF.OnAlexChange := OnAlexSettingsChange; SF.OnXvtrChange := OnXvtrSettingsChange; SF.OnWfAGCNFChange := ApplyWfAGCNF; SF.OnADCChange := ApplyADCSettings; SF.OnWebSettingsChange := ApplyWebSettings; end; SF := TSettingsForm(FSettingsForm); SF.LoadTXSettings(FController.FTXSettings, IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); SF.LoadAlexSettings(FController.FAlexSettings, IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); SF.LoadXvtrSettings(FController.FXvtrSettings); SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal); SF.LoadVHFCalSettings(FController.FVHFBandCal); SF.LoadCATSettings( FCATLastGlobal.CATSerialEnabled, FCATLastGlobal.CATSerialPort, FCATLastGlobal.CATSerialBaud, FCATLastGlobal.CATSerialDataBits, FCATLastGlobal.CATSerialStopBits, FCATLastGlobal.CATSerialParity, FCATLastGlobal.CATTcpEnabled, FCATLastGlobal.CATTcpPort); // Перечисляем PA устройства SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn); SF.LoadVisibility(FShowSpectrum, FShowWaterfall, FShowWideband, FWidebandFill, FSpectrumFill); SF.LoadWfAGCNF(FController.FWfAGCEnabled, FController.FWfNFEnabled); SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled); SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass); SF.LoadFPS(FDisplayFPS); SF.LoadLightTheme(FLightTheme); SF.LoadFreqMhzDigits(FFreqMhzDigits); SF.LoadAudioBufferSize(FController.FAudioOut.OutputBufferSize); SF.LoadSendAudioToRadio(FController.FSendAudioToRadio); // Загружаем текущие значения if FController.FWDSPReady then SF.LoadValues( FController.FDSPEngine.FFTSize, FController.FDSPEngine.WindowType, FController.FDSPEngine.SpecDetector, FController.FDSPEngine.SpecAvgMode, FController.FDSPEngine.SpecAvgTimeMS, FController.FDSPEngine.WfDetector, FController.FDSPEngine.WfAvgMode, FController.FDSPEngine.WfAvgTimeMS, FWfManualHigh, FWfManualLow, FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FController.FAudioOutDevName, FController.FAudioInDevName) else SF.LoadValues( 131072, 2, 0, 3, 30.0, 0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FController.FAudioOutDevName, FController.FAudioInDevName); SF.Show; end; procedure TMainForm.AfterShowTick(Sender: TObject); begin // Вызывается один раз через 200 мс после старта формы FAfterShowTimer.Enabled := False; // Повторно применяем сохранённую позицию: к этому моменту WM уже декорировал // окно и Qt знает frame extents, поэтому move() ставит рамку точно. if FHasPendingRestore then begin SetBounds(FRestoreL, FRestoreT, Width, Height); FHasPendingRestore := False; end; try FController.FAudioOut.Open; except end; EnsureWDSPWisdom; end; // =========================================================================== // CAT subsystem // =========================================================================== procedure TMainForm.InitCATEngine; var Ctx: TCATContext; begin FillChar(Ctx, SizeOf(Ctx), 0); Ctx.GetVfoA := CATGetVfoA; Ctx.GetVfoB := CATGetVfoB; Ctx.GetMode := CATGetMode; Ctx.GetActiveVfo := CATGetActiveVfo; Ctx.GetAGCMode := CATGetAGCMode; Ctx.GetVolume := CATGetVolume; Ctx.GetDriveLevel := CATGetDriveLevel; Ctx.GetFilterIdx := CATGetFilterIdx; Ctx.GetFilterBW := CATGetFilterBW; Ctx.GetNRMode := CATGetNRMode; Ctx.GetNBMode := CATGetNBMode; Ctx.GetSNBEnabled := CATGetSNB; Ctx.GetANFEnabled := CATGetANF; Ctx.GetTransmitting := CATGetTX; Ctx.GetRunning := CATGetRunning; Ctx.GetSMeter := CATGetSMeter; Ctx.GetCurrentBand := CATGetBand; Ctx.SetVfoA := CATSetVfoA; Ctx.SetVfoB := CATSetVfoB; Ctx.SetMode := WebOnMode; Ctx.SetActiveVfo := WebOnActiveVfo; Ctx.SetAGCMode := WebOnAGC; Ctx.SetVolume := WebOnVolume; Ctx.SetDriveLevel := WebOnDrive; Ctx.SetFilterIdx := CATSetFilterIdx; Ctx.SetNRMode := WebOnNR; Ctx.SetNBMode := WebOnNB; Ctx.SetSNBEnabled := WebOnSNB; Ctx.SetANFEnabled := WebOnANF; Ctx.SetTransmitting := WebOnMOX; Ctx.DoBandUp := CATDoBandUp; Ctx.DoBandDown := CATDoBandDown; Ctx.DoTuneUp := CATDoTuneUp; Ctx.DoTuneDown := CATDoTuneDown; Ctx.DoBandByIndex := WebOnBand; FCATEngine := TCATEngine.Create(Ctx); FCATSerial := TCATSerialManager.Create(FCATEngine); FCATTcp := TCATTcpServer.Create(FCATEngine); end; procedure TMainForm.CATApplySettings(const G: TGlobalSettings); var Cfgs: array[0..3] of TCATSerialConfig; i: Integer; begin for i := 0 to 3 do begin Cfgs[i].Enabled := G.CATSerialEnabled[i]; Cfgs[i].PortName := G.CATSerialPort[i]; Cfgs[i].BaudRate := G.CATSerialBaud[i]; Cfgs[i].DataBits := G.CATSerialDataBits[i]; Cfgs[i].StopBits := G.CATSerialStopBits[i]; case G.CATSerialParity[i] of 1: Cfgs[i].Parity := cspOdd; 2: Cfgs[i].Parity := cspEven; else Cfgs[i].Parity := cspNone; end; end; FCATSerial.ApplyConfig(Cfgs); FCATTcp.Stop; if G.CATTcpEnabled then begin FCATTcp.Port := G.CATTcpPort; FCATTcp.Start; end; end; procedure TMainForm.OnCATSettingsChange( const SerEnabled: array of Boolean; const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer); var i: Integer; begin for i := 0 to 3 do begin if i <= High(SerEnabled) then FCATLastGlobal.CATSerialEnabled[i] := SerEnabled[i]; if i <= High(SerPort) then FCATLastGlobal.CATSerialPort[i] := SerPort[i]; if i <= High(SerBaud) then FCATLastGlobal.CATSerialBaud[i] := SerBaud[i]; if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i]; if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i]; if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i]; end; FCATLastGlobal.CATTcpEnabled := TcpEnabled; FCATLastGlobal.CATTcpPort := TcpPort; CATApplySettings(FCATLastGlobal); FController.FSettings.SaveCATSettings(FCATLastGlobal); if FController.FDevConnected then FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); FController.FSettings.Save; end; // --- Getters (called from CAT thread — read only, no sync needed) ----------- function TMainForm.CATGetVfoA: Double; begin Result := FController.FVfoA; end; function TMainForm.CATGetVfoB: Double; begin Result := FController.FVfoB; end; function TMainForm.CATGetMode: Integer; begin Result := FController.FMode; end; function TMainForm.CATGetActiveVfo: Integer; begin Result := FController.FActiveVfo; end; function TMainForm.CATGetAGCMode: Integer; begin Result := FController.FAGCMode; end; function TMainForm.CATGetVolume: Integer; begin Result := FController.FVolume; end; function TMainForm.CATGetDriveLevel: Integer;begin Result := FController.FDrivePercent; end; function TMainForm.CATGetFilterIdx: Integer; begin Result := FController.FFilter; end; function TMainForm.CATGetFilterBW: Integer; begin Result := FController.FFilterBW; end; function TMainForm.CATGetNRMode: Integer; begin Result := FController.FNRMode; end; function TMainForm.CATGetNBMode: Integer; begin Result := FController.FNBMode; end; function TMainForm.CATGetSNB: Boolean; begin Result := FController.FSNB; end; function TMainForm.CATGetANF: Boolean; begin Result := FController.FANF; end; function TMainForm.CATGetTX: Boolean; begin Result := FController.FTransmitting; end; function TMainForm.CATGetRunning: Boolean; begin Result := FController.FRunning; end; function TMainForm.CATGetSMeter: Double; begin Result := FLastSMeter; end; function TMainForm.CATGetBand: Integer; begin Result := FController.FCurrentBand; end; // --- Setters ---------------------------------------------------------------- procedure TMainForm.CATSetVfoA(V: Double); begin FCATSyncFreq := V; TThread.Synchronize(nil, SyncCATVfoA); end; procedure TMainForm.CATSetVfoB(V: Double); begin FCATSyncFreq := V; TThread.Synchronize(nil, SyncCATVfoB); end; procedure TMainForm.CATSetFilterIdx(V: Integer); begin // WebOnFilter: negative value encodes 0-based index as -(idx+1) WebOnFilter(-(V + 1)); end; procedure TMainForm.CATDoBandUp; begin TThread.Synchronize(nil, SyncCATBandUp); end; procedure TMainForm.CATDoBandDown; begin TThread.Synchronize(nil, SyncCATBandDown); end; procedure TMainForm.CATDoTuneUp; begin TThread.Synchronize(nil, SyncCATTuneUp); end; procedure TMainForm.CATDoTuneDown; begin TThread.Synchronize(nil, SyncCATTuneDown); end; // --- Sync methods (run in main thread) -------------------------------------- procedure TMainForm.SyncCATVfoA; var BandIdx: Integer; begin FController.FVfoA := FCATSyncFreq; if FController.FActiveVfo = 0 then begin ApplyVfoA(Round(FController.FVfoA)); end else begin // VFO A is not active — just update display and band indicator FreqDispA.Frequency := Round(FController.FVfoA); BandIdx := FreqToBandIdx(FController.FVfoA); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; end; end; procedure TMainForm.SyncCATVfoB; var BandIdx: Integer; begin FController.FVfoB := FCATSyncFreq; FreqDispB.Frequency := Round(FController.FVfoB); if FController.FActiveVfo = 1 then begin FController.FCenterFreq := FController.FVfoB; if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0); if FController.FRunning then FController.FNetwork.SetRunAndFreq(True, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), FController.FDriveLevel); BandIdx := FreqToBandIdx(FController.FVfoB); if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then begin StyleButton(BtnBand[FController.FCurrentBand], False); FController.FCurrentBand := BandIdx; StyleButton(BtnBand[FController.FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; UpdateVfoDisplay; end; end; procedure TMainForm.SyncCATBandUp; begin if FController.FCurrentBand < BAND_COUNT - 1 then WebOnBand(FController.FCurrentBand + 1); end; procedure TMainForm.SyncCATBandDown; begin if FController.FCurrentBand > 0 then WebOnBand(FController.FCurrentBand - 1); end; procedure TMainForm.SyncCATTuneUp; begin if FController.FActiveVfo = 0 then ApplyVfoA(Round(FController.FVfoA) + 10) else begin FCATSyncFreq := FController.FVfoB + 10; SyncCATVfoB; end; end; procedure TMainForm.SyncCATTuneDown; begin if FController.FActiveVfo = 0 then ApplyVfoA(Round(FController.FVfoA) - 10) else begin FCATSyncFreq := FController.FVfoB - 10; SyncCATVfoB; end; end; end.