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, FlatButton, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types, LCLIntf, LCLType, GraphType, HPSDRProtocol, HPSDRNetwork, WDSP, WDSPEngine, AudioOutput, AudioInput, Settings, WebServer, CATEngine, CATSerial, CATTcp, SpectrumView; const CLR_BG = TColor($00101010); CLR_PANEL = TColor($00181818); CLR_BORDER = TColor($00303030); CLR_FREQ = TColor($0000FF00); CLR_FREQ_DIM = TColor($00008000); CLR_AMBER = TColor($0000AAFF); CLR_ACTIVE = TColor($00003300); CLR_INACTIVE = TColor($00202020); 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','750','600','500','400','250','100','50','25'); FILT_CW_BW: array[0..FILT_COUNT-1] of Integer = (1000, 800, 750, 600, 500, 400, 250, 100, 50, 25); FILT_CW_DEF = 4; // 500 // AM/SAM/DSB FILT_AM_NAMES: array[0..FILT_COUNT-1] of string = ('16k','12k','10k','8k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k'); FILT_AM_BW: array[0..FILT_COUNT-1] of Integer = (16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400); FILT_AM_DEF = 3; // 8k // FM FILT_FM_NAMES: array[0..FILT_COUNT-1] of string = ('20k','15k','12k','10k','8k','6k','5k','4k','3k','2k'); FILT_FM_BW: array[0..FILT_COUNT-1] of Integer = (20000, 15000, 12000, 10000, 8000, 6000, 5000, 4000, 3000, 2000); FILT_FM_DEF = 0; // 20k 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'); type TDeviceItem = record Dev: THPSDRDevice; Display: string; end; // Синхронизирующий объект для OnDeviceFound TDeviceFoundSync = class private FForm: TObject; // TMainForm, через forward ref FDev: THPSDRDevice; FEntry: string; public constructor Create(AForm: TObject; const D: THPSDRDevice; const E: string); procedure Execute; end; // Синхронизирующий объект для HP Status TStatusUISync = class private FForm: TObject; FFwdW: Double; FSWRV: Double; FSupplyV: Double; FPLLLock: Boolean; FHWPTT: Boolean; public constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean); procedure Execute; end; // Синхронизирующий объект для DDC IQ TDDCSeqSync = class private FForm: TObject; FDDCIdx: Integer; FSeq: LongWord; public constructor Create(AForm: TObject; Idx: Integer; Seq: LongWord); procedure Execute; end; { TMainForm } TMainForm = class(TForm) private // ---- Network ---- FNetwork: THPSDRNetwork; FDevices: array of TDeviceItem; FDeviceCount: Integer; FDeviceDialog: TDeviceDialog; FPendingIP: string; // IP устройства для подключения FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре FPanelHidden: Boolean; // True = левая панель скрыта FPendingBoardType: Integer; // BoardType устройства для подключения // ---- DSP + Audio ---- FDSPEngine: TWDSPEngine; FAudioOut: TAudioOutput; FAudioIn: TAudioInput; FAudioOutDevName: string; // текущее имя выходного устройства FAudioInDevName: string; // текущее имя входного устройства (TX mic) FWDSPReady: Boolean; // True когда WDSP открыт и работает // ---- State ---- FVfoA: Double; FVfoB: Double; FActiveVfo: Integer; FMode: Integer; FFilter: Integer; FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF FAGCTop: Integer; // AGC level dBm, −20..−120 FCTun: Boolean; // Center Tune: True=спектр стоит, маркер двигается FFilterBW: Integer; // текущая полоса фильтра в Гц // мышь на спектре/водопаде FSpecDrag: Boolean; FSpecDragX0: Integer; // X при нажатии FSpecDragFreq: Double; // FCenterFreq при нажатии FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад FDriveLevel: Byte; FPAMaxPower: Double; FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон FRunning: Boolean; FTransmitting: Boolean; FHWPTTActive: Boolean; // True = hardware PTT нажата FMuted: Boolean; FVolume: 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 FLastFwdW: Double; FLastSWR: Double; // ---- Spectrum / Waterfall view ---- FSpecView: TSpectrumView; FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора FSpectrumHeight: Integer; FWaterfallHeight:Integer; FShowSpectrum: Boolean; FShowWaterfall: Boolean; FDisplayFPS: Integer; FWaterfallDirty: Boolean; // Waterfall settings (kept here for MakeGlobalSettings, buttons, settings dialog) FWfAGCEnabled: Boolean; FWfNFEnabled: Boolean; FWfManualHigh: Double; FWfManualLow: Double; FWfAGCOffset: Double; // Spectrum display settings (for settings save/load) FSpecRefLevel: Double; FSpecRange: Double; FSpecGridStep: 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; // ---- Splitter ---- FSplitterDrag: Boolean; FSplitterDragY0: Integer; FSplitterSH0: Integer; FSplitterRatio: Double; // --- Settings --- FSettings: TSettingsManager; FWebServer: TWebServer; // веб-интерфейс (порт 8080) // --- CAT --- FCATEngine: TCATEngine; FCATSerial: TCATSerialManager; FCATTcp: TCATTcpServer; FCATSyncFreq: Double; FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (для сохранения) // Временные поля для передачи параметров в Synchronize-методы FWebSyncFreq: Double; FWebSyncInt: Integer; FWebSyncBool: Boolean; FWebSyncM: TThreadMethod; FCurrentBand: Integer; // текущий активный диапазон 0..10 FSettingsForm: TObject; // TSettingsForm (cast при использовании) FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов FDevConnected: Boolean; // True после первого подключения FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO FSpanHz: Double; FSampleRate: Integer; // текущий DDC sample rate (Hz) // ---- Timers ---- FMeterTimer: TTimer; FSpectrumTimer: TTimer; FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации // ---- Toolbar ---- PanelToolbar: TPanel; BtnDiscover: TFlatButton; BtnStartStop: TFlatButton; // START / STOP (connect+run в одном) BtnSettings: TFlatButton; // открыть диалог настроек // ---- Left panel ---- PanelLeft: TPanel; PanelVfoA: TPanel; LblVfoALabel: TLabel; FreqDispA: TFreqDisplay; PanelVfoB: TPanel; LblVfoBLabel: TLabel; FreqDispB: TFreqDisplay; PanelVfoButtons: TPanel; BtnVfoSwap: TFlatButton; BtnVfoACopyB: TFlatButton; BtnVfoBCopyA: TFlatButton; PanelBands: TPanel; BtnBand: array[0..BAND_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; PanelRX: TPanel; BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF LblAGCTop: TLabel; // показывает значение уровня TrkAGC: TTrackBar; // ползунок уровня AGC TrkVolume: TTrackBar; BtnNR: TFlatButton; BtnNB: TFlatButton; BtnSNB: TFlatButton; BtnANF: TFlatButton; BtnMute: TFlatButton; BtnWfAGC: TFlatButton; BtnWfNF: TFlatButton; PanelSMeterRight: TPanel; PbSMeterRight: TPaintBox; PanelTX: TPanel; LblDrv: TLabel; TrkDrive: TTrackBar; BtnMOX: TFlatButton; PbFwdPower: TPaintBox; PbSWR: TPaintBox; LblFwdPwr: TLabel; LblSWRVal: TLabel; // ---- Right panel ---- PanelRight: TPanel; PanelSpanButtons: TPanel; LblSpan: TLabel; BtnSpan48k: TFlatButton; BtnSpan96k: TFlatButton; BtnSpan192k: TFlatButton; BtnSpan384k: TFlatButton; BtnSpan768k: TFlatButton; BtnSpan1536k: TFlatButton; BtnHidePanel: TFlatButton; // скрыть/показать левую панель PbSpectrum: TPaintBox; PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад PbWaterfall: TPaintBox; // ---- Status bar ---- StatusBar1: TStatusBar; // ---- Helpers ---- procedure BuildUI; procedure ApplyDarkTheme; procedure StyleButton(B: TFlatButton; Active: Boolean = False); procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False); procedure PositionSpanOverlayButtons; procedure UpdateVfoDisplay; function FormatFreq(Hz: Double): string; procedure ResizeSMeter; procedure ResizeSpectrumPanels; 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 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: Double; PLLLock, HWPTT: Boolean); 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 ActivateVfo(Idx: Integer); function ActiveVfoFreq: Int64; // Настройки procedure SaveCurrentBand; procedure RestoreBand(BandIdx: Integer); procedure SaveAllAndExit; procedure RestoreWindowBounds; 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 UpdateFilterButtons; 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 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 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); procedure BtnSpanClick(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); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; procedure SyncWebMode; procedure SyncWebFilter; procedure SyncWebAGC; procedure SyncWebAGCTop; procedure SyncWebBand; 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; // --- 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 BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject); procedure BtnSettingsClick(Sender: TObject); procedure PositionVfoOverlay; procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer); procedure PbSpectrumDblClick(Sender: TObject); procedure TrkDriveChange(Sender: TObject); procedure TrkVolumeChange(Sender: TObject); function CalcDriveByte: Byte; function ActiveTXFreqHz: Double; procedure ApplyMOX(Active: Boolean); procedure OnTXIQReady(const Buf: array of Double; Count: Integer); procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); 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 ApplyGridParams(RefLevel, Range, GridStep: Double); procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string); procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean); procedure ApplyFPS(FPS: 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; TWisdomBuildThread = class(TThread) private FDirectory: string; FError: string; protected procedure Execute; override; public constructor Create(const ADirectory: string); property ErrorText: string read FError; end; TWisdomProgressDialog = class(TForm) private FInfoLabel: TLabel; FProgress: TProgressBar; FTimer: TTimer; FThread: TWisdomBuildThread; FPhase: Integer; procedure TimerTick(Sender: TObject); public constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread); reintroduce; end; var MainForm: TMainForm; implementation uses SettingsForm; {$R *.lfm} constructor TWisdomBuildThread.Create(const ADirectory: string); begin inherited Create(False); FreeOnTerminate := False; FDirectory := ADirectory; FError := ''; end; procedure TWisdomBuildThread.Execute; var DirA: AnsiString; begin try if not Assigned(@WDSPwisdom) then Exit; DirA := AnsiString(FDirectory); WDSPwisdom(PAnsiChar(DirA)); except on E: Exception do FError := E.ClassName + ': ' + E.Message; end; end; constructor TWisdomProgressDialog.Create(AOwner: TComponent; AThread: TWisdomBuildThread); begin inherited CreateNew(AOwner); FThread := AThread; FPhase := 0; Caption := 'WDSP Wisdom'; Width := 520; Height := 140; Position := poScreenCenter; BorderStyle := bsDialog; BorderIcons := []; Color := CLR_BG; Font.Name := 'Courier New'; Font.Size := 9; Font.Color := CLR_TEXT; FInfoLabel := TLabel.Create(Self); FInfoLabel.Parent := Self; FInfoLabel.SetBounds(16, 16, 480, 40); FInfoLabel.AutoSize := False; FInfoLabel.WordWrap := True; FInfoLabel.Font.Color := CLR_TEXT; FInfoLabel.Caption := 'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.'; FProgress := TProgressBar.Create(Self); FProgress.Parent := Self; FProgress.SetBounds(16, 72, 480, 22); FProgress.Min := 0; FProgress.Max := 100; FProgress.Position := 0; FTimer := TTimer.Create(Self); FTimer.Interval := 250; FTimer.OnTimer := TimerTick; FTimer.Enabled := True; end; procedure TWisdomProgressDialog.TimerTick(Sender: TObject); var P: PAnsiChar; S: string; begin FPhase := (FPhase + 7) mod 101; FProgress.Position := FPhase; if Assigned(@wisdom_get_status) then begin P := wisdom_get_status; if P <> nil then begin S := Trim(string(AnsiString(P))); if S <> '' then FInfoLabel.Caption := S; end; end; if Assigned(FThread) and FThread.Finished then begin FTimer.Enabled := False; ModalResult := mrOk; end; end; // =========================================================================== // Общая функция декодирования типа платы — используется везде // =========================================================================== function BoardTypeName(BoardType: Integer): string; begin case BoardType of 1: Result := 'HERMES (ANAN-10/100)'; 2: Result := 'HERMES-E (ANAN-10E/100B)'; 3: Result := 'ANGELIA (ANAN-100D)'; 4: Result := 'ORION (ANAN-200D)'; 5: Result := 'ORION MkII (ANAN-7000/8000)'; 6: Result := 'HERMES-LITE 2'; 10: Result := 'SATURN (G2)'; else Result := Format('Unknown Board #%d', [BoardType]); end; end; // =========================================================================== // Sync helpers // =========================================================================== constructor TDeviceFoundSync.Create(AForm: TObject; const D: THPSDRDevice; const E: string); begin inherited Create; FForm := AForm; FDev := D; FEntry := E; end; procedure TDeviceFoundSync.Execute; begin TMainForm(FForm).DoAddDevice(FDev, FEntry); end; constructor TStatusUISync.Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean); begin inherited Create; FForm := AForm; FFwdW := FW; FSWRV := SW; FSupplyV := SV; FPLLLock := PLL; FHWPTT := HWPTT; end; procedure TStatusUISync.Execute; begin TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT); end; constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord); begin inherited Create; FForm := AForm; FDDCIdx := Idx; FSeq := Seq; end; procedure TDDCSeqSync.Execute; begin TMainForm(FForm).DoUpdateDDCSeqOrNoDevice(FDDCIdx, FSeq); end; // =========================================================================== // FormCreate / FormDestroy // =========================================================================== // =========================================================================== // Settings helpers // =========================================================================== procedure TMainForm.RestoreWindowBounds; var L, T, Wd, Ht: Integer; begin FSettings.LoadWindowBounds(L, T, Wd, Ht); if (Wd > 400) and (Ht > 300) then begin Left := L; Top := T; Width := Wd; Height := Ht; end; end; function TMainForm.MakeBandSettings: TBandSettings; begin Result.VfoA := FVfoA; Result.VfoB := FVfoB; Result.Mode := FMode; Result.FilterIdx := FFilter; Result.FilterBW := FFilterBW; Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop; Result.CTun := FCTun; Result.SpanHz := FSpanHz; // Waterfall AGC/NF — глобальные (не диапазонные) end; function TMainForm.MakeGlobalSettings: TGlobalSettings; var i: Integer; begin Result.Volume := FVolume; Result.DriveLevel := TrkDrive.Position; Result.PAMaxPower := FPAMaxPower; for i := 0 to BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i]; Result.ActiveVfo := FActiveVfo; Result.NRMode := BtnNR.Tag; Result.NBMode := BtnNB.Tag; Result.SNBEnabled := BtnSNB.Tag <> 0; Result.ANFEnabled := BtnANF.Tag <> 0; Result.AGCSlope := 0; Result.AGCHangThreshold := 100; Result.WfAGCEnabled := FWfAGCEnabled; Result.WfNFEnabled := FWfNFEnabled; Result.LastBand := FCurrentBand; Result.SampleRate := FSampleRate; // Display settings if FWDSPReady then begin Result.FFTSize := FDSPEngine.FFTSize; Result.WindowType := FDSPEngine.WindowType; Result.SpecDetector := FDSPEngine.SpecDetector; Result.SpecAvgMode := FDSPEngine.SpecAvgMode; Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS; Result.WfDetector := FDSPEngine.WfDetector; Result.WfAvgMode := FDSPEngine.WfAvgMode; Result.WfAvgTimeMS := 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 := FAudioOut.SampleRate; Result.ShowSpectrum := FShowSpectrum; Result.ShowWaterfall := FShowWaterfall; Result.DisplayFPS := FDisplayFPS; // Audio device names Result.AudioOutDevice := FAudioOutDevName; Result.AudioInDevice := FAudioInDevName; // 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 FDevConnected then Exit; FBandCache[FCurrentBand] := MakeBandSettings; FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]); end; procedure TMainForm.RestoreBand(BandIdx: Integer); var B: TBandSettings; i: Integer; begin if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit; B := FBandCache[BandIdx]; // --- Кнопки диапазонов --- for i := 0 to BAND_COUNT - 1 do StyleButton(BtnBand[i], i = BandIdx); // Сбрасываем waterfall thresholds при смене диапазона FSpecView.ResetWfAvgBuf; // --- Режим: сначала устанавливаем FMode, потом кнопки и WDSP --- FMode := B.Mode; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode); if FWDSPReady then FDSPEngine.SetMode(FMode); // --- Фильтр --- FFilter := B.FilterIdx; FFilterBW := B.FilterBW; UpdateFilterButtons; // обновляет кнопки фильтра ApplyModeFilter; // применяет Lo/Hi в WDSP с учётом нового FMode // --- AGC --- FAGCMode := B.AGCMode; FAGCTop := B.AGCTop; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode); TrkAGC.Position := FAGCTop; LblAGCTop.Caption := Format('%ddB', [FAGCTop]); if FWDSPReady then begin FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); end; FSpecView.AGCTop := FAGCTop; // --- CTUN --- FCTun := B.CTun; StyleButton(BtnCTun, FCTun); if FWDSPReady and not FCTun then FDSPEngine.SetShift(0.0); // --- Sample Rate / Span --- // SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона. // Обновляем только FSpanHz для отображения (берём текущий FSampleRate) FSpanHz := FSampleRate; StyleSpanButton(BtnSpan48k, FSampleRate = 48000); StyleSpanButton(BtnSpan96k, FSampleRate = 96000); StyleSpanButton(BtnSpan192k, FSampleRate = 192000); StyleSpanButton(BtnSpan384k, FSampleRate = 384000); StyleSpanButton(BtnSpan768k, FSampleRate = 768000); StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000); // --- VFO A: центрируем на новой частоте, сбрасываем shift --- FCenterFreq := B.VfoA; // DDC = центр диапазона FVfoB := B.VfoB; FreqDispB.Frequency := Round(FVfoB); // ApplyVfoA обновит сеть, FreqDisp, shift, перерисует ApplyVfoA(Round(B.VfoA)); end; procedure TMainForm.SaveAllAndExit; begin if FDevConnected then begin SaveCurrentBand; FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); FSettings.Save; end; end; procedure TMainForm.FormCreate(Sender: TObject); var bi_, StartupRate, i: Integer; StartupVfoA, StartupVfoB: Double; begin FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0; FMode := 1; FFilter := 5; FAGCMode := 1; // MEDIUM FAGCTop := 90; // −90 dBm // default: зависит от режима, будет сброшен в UpdateFilterButtons FCTun := False; FFilterBW := 2700; FSpecDrag := False; FDriveLevel := 0; FPAMaxPower := 100.0; for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0; FRunning := False; FTransmitting := False; FMuted := False; FVolume := 70; FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000; FCurrentBand := 5; // 20m по умолчанию // Waterfall normalization FWfManualHigh := -80.0; FWfManualLow := -130.0; FWfAGCOffset := 0.0; FWfAGCEnabled := True; FWfNFEnabled := False; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FWaterfallFrameInterval := 2; FWaterfallFrameCounter := 0; FDevConnected := False; // Spectrum grid settings defaults FSpecRefLevel := -20.0; FSpecRange := 110.0; FSpecGridStep := 10.0; FSettingsForm := nil; FillChar(FDevMAC, SizeOf(FDevMAC), 0); // Инициализируем кэш диапазонов умолчаниями for bi_ := 0 to CFG_BAND_COUNT - 1 do TSettingsManager.DefaultBand(bi_, FBandCache[bi_]); FCurrentBand := 5; // Загружаем JSON настройки FSettings := TSettingsManager.Create; FSettings.Load; if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then begin FVfoA := StartupVfoA; FVfoB := StartupVfoB; FCenterFreq := FVfoA; if StartupRate > 0 then begin FSampleRate := StartupRate; FSpanHz := StartupRate; end; end; // Восстанавливаем размер/позицию окна при старте (до подключения устройства) RestoreWindowBounds; // S-метр позиционируем после рестора размера окна // (будет пересчитан в первом тике SpectrumTimerTick) FLastSMeter := -130; // Создаём веб-сервер (логин: admin, пароль: hpsdr — менять здесь) FWebServer := TWebServer.Create('admin', 'hpsdr'); 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.Start; FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0); InitCATEngine; FSMeterPeak := -130; FSMeterMin := -130; FSMeterAvg := -130; FRXPacketCount := 0; FActiveDDC := 0; FLastDDCSeq := 0; FLastDDCIndex := 0; FLastFwdW := 0; FLastSWR := 1; FDeviceCount := 0; FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; FVfoOverlay := TVfoOverlay.Create(Self); FVfoOverlay.OnSelect := OnModeFilterSelect; FVfoOverlay.Visible := False; FPanelHidden := False; FShowSpectrum := True; FShowWaterfall := True; FDisplayFPS := 60; FSplitterRatio := 0.40; FSplitterDrag := False; FSpecView := TSpectrumView.Create; FSpecView.VfoA := FVfoA; FSpecView.VfoB := FVfoB; FSpecView.ActiveVfo := FActiveVfo; FSpecView.CenterFreq := FCenterFreq; FSpecView.SpanHz := FSpanHz; FSpecView.Mode := FMode; FSpecView.FilterBW := FFilterBW; FSpecView.AGCTop := FAGCTop; FSpecView.WfAGCEnabled := FWfAGCEnabled; FSpecView.WfNFEnabled := FWfNFEnabled; FSpecView.WfManualHigh := FWfManualHigh; FSpecView.WfManualLow := FWfManualLow; FSpecView.WfAGCOffset := FWfAGCOffset; FSpecView.SpecRefLevel := FSpecRefLevel; FSpecView.SpecRange := FSpecRange; FSpecView.SpecGridStep := FSpecGridStep; FSpecView.WfFrameInterval := FWaterfallFrameInterval; FSpecView.FillDemoSpectrum; FNetwork := THPSDRNetwork.Create; FNetwork.OnDeviceFound := OnDeviceFound; FNetwork.OnHPStatus := OnHPStatusCB; FNetwork.OnDDCIQ := OnDDCIQCB; FNetwork.OnMicPacket := OnMicPacketCB; BuildUI; StyleSpanButton(BtnSpan48k, FSampleRate = 48000); StyleSpanButton(BtnSpan96k, FSampleRate = 96000); StyleSpanButton(BtnSpan192k, FSampleRate = 192000); StyleSpanButton(BtnSpan384k, FSampleRate = 384000); StyleSpanButton(BtnSpan768k, FSampleRate = 768000); StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000); ResizeSMeter; ApplyDarkTheme; UpdateVfoDisplay; UpdateFilterButtons; // DSP Engine — создаём объект, Open вызовется при нажатии START // (FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек) FWDSPReady := False; // BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz. // Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA // (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR. FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512); FDSPEngine.OnAudio := OnAudioReady; FDSPEngine.OnSpectrum := OnSpectrumReady; FDSPEngine.OnWaterfall := OnWaterfallReady; FDSPEngine.OnTXIQ := OnTXIQReady; // Audio output/input — создаём объекты сейчас, открываем после показа формы // (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC) FAudioOut := TAudioOutput.Create(48000); 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 FNetwork.Running then FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); FNetwork.Disconnect; FNetwork.Free; FAudioOut.Close; FAudioOut.Free; FAudioIn.Close; FAudioIn.Free; FDSPEngine.Close; FDSPEngine.Free; FreeAndNil(FSpecView); // Сохраняем настройки при закрытии if FDevConnected then begin SaveCurrentBand; FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); end; // Размер окна сохраняем всегда (не зависит от подключения) FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate); FSettings.SaveWindowBounds(Left, Top, Width, Height); FSettings.Save; FSettings.Free; 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); end; procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction); begin // Останавливаем трансивер как при нажатии STOP if FNetwork.Connected then begin if FRunning then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, 0, False, True, True); FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); FRunning := False; FTransmitting := False; if FWDSPReady then FDSPEngine.SetTXRun(False); end; FNetwork.Disconnect; end; CloseAction := caFree; end; // =========================================================================== // Build UI (без анонимных процедур и inline var) // =========================================================================== procedure TMainForm.BuildUI; const BTN_H = 26; BTN_SM = 24; LEFT_W = 232; var i, X, Y, W: Integer; B: TFlatButton; 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 := 36; PanelToolbar.BevelOuter := bvNone; 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); // ---- Status bar ---- StatusBar1 := TStatusBar.Create(Self); StatusBar1.Parent := Self; StatusBar1.Align := alBottom; StatusBar1.SimplePanel := False; with StatusBar1.Panels do begin Add; Items[0].Width := 150; Items[0].Text := 'IP: --'; Add; Items[1].Width := 120; Items[1].Text := 'Board: --'; Add; Items[2].Width := 320; Items[2].Text := 'Supply: -- | FWD: -- | SWR: --'; Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...'; Add; Items[4].Width := 200; Items[4].Text := 'Not connected'; end; // ---- Left panel ---- PanelLeft := TPanel.Create(Self); PanelLeft.Parent := Self; PanelLeft.Align := alLeft; PanelLeft.Width := LEFT_W; PanelLeft.BevelOuter := bvNone; Y := 2; // VFO A PanelVfoA := TPanel.Create(Self); PanelVfoA.Parent := PanelLeft; PanelVfoA.SetBounds(0, Y, LEFT_W, 56); PanelVfoA.BevelOuter := bvNone; LblVfoALabel := TLabel.Create(Self); LblVfoALabel.Parent := PanelVfoA; LblVfoALabel.Caption := 'VFO-A'; LblVfoALabel.Left := 4; LblVfoALabel.Top := 2; LblVfoALabel.Font.Name := 'Courier New'; LblVfoALabel.Font.Size := 7; LblVfoALabel.Font.Color := CLR_TEXTDIM; FreqDispA := TFreqDisplay.Create(Self); FreqDispA.Parent := PanelVfoA; FreqDispA.SetBounds(0, 14, LEFT_W, 36); FreqDispA.FontSize := 20; FreqDispA.FontName := 'Courier New'; FreqDispA.Frequency := Round(FVfoA); FreqDispA.ColorNormal := CLR_FREQ; FreqDispA.ColorHover := TColor($0040DDFF); FreqDispA.ColorDim := CLR_TEXTDIM; FreqDispA.OnChange := FreqDispAChanged; FreqDispA.OnClick := FreqDispAClick; Inc(Y, 58); // VFO B PanelVfoB := TPanel.Create(Self); PanelVfoB.Parent := PanelLeft; PanelVfoB.SetBounds(0, Y, LEFT_W, 46); PanelVfoB.BevelOuter := bvNone; LblVfoBLabel := TLabel.Create(Self); LblVfoBLabel.Parent := PanelVfoB; LblVfoBLabel.Caption := 'VFO-B'; LblVfoBLabel.Left := 4; LblVfoBLabel.Top := 2; LblVfoBLabel.Font.Name := 'Courier New'; LblVfoBLabel.Font.Size := 7; LblVfoBLabel.Font.Color := CLR_TEXTDIM; FreqDispB := TFreqDisplay.Create(Self); FreqDispB.Parent := PanelVfoB; FreqDispB.SetBounds(0, 14, LEFT_W, 28); FreqDispB.FontSize := 14; FreqDispB.FontName := 'Courier New'; FreqDispB.Frequency := Round(FVfoB); FreqDispB.ColorNormal := CLR_FREQ_DIM; FreqDispB.ColorHover := TColor($0040DDFF); FreqDispB.ColorDim := CLR_TEXTDIM; FreqDispB.OnChange := FreqDispBChanged; FreqDispB.OnClick := FreqDispBClick; Inc(Y, 50); // VFO buttons PanelVfoButtons := TPanel.Create(Self); PanelVfoButtons.Parent := PanelLeft; PanelVfoButtons.SetBounds(0, Y, LEFT_W, BTN_H + 4); PanelVfoButtons.BevelOuter := bvNone; BtnVfoSwap := MakeBtn(PanelVfoButtons, 'A<>B', 2, 2, 72, BTN_H, BtnVfoSwapClick); BtnVfoACopyB := MakeBtn(PanelVfoButtons, 'A>B', 78, 2, 72, BTN_H, BtnVfoACopyBClick); BtnVfoBCopyA := MakeBtn(PanelVfoButtons, 'B>A', 154, 2, 72, BTN_H, BtnVfoBCopyAClick); Inc(Y, BTN_H + 8); // Band selector PanelBands := TPanel.Create(Self); PanelBands.Parent := PanelLeft; PanelBands.SetBounds(0, Y, LEFT_W, 74); PanelBands.BevelOuter := bvNone; MakeLbl(PanelBands, 'BAND', 4, 2); W := (LEFT_W - 6) div 6; for i := 0 to BAND_COUNT - 1 do begin B := MakeBtn(PanelBands, BAND_NAMES[i], 2 + (i mod 6) * W, 16 + (i div 6) * 27, W - 2, BTN_SM, BtnBandClick); B.Tag := i; BtnBand[i] := B; end; 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); W := (LEFT_W - 6) div 4; for i := 0 to MODE_COUNT - 1 do begin B := MakeBtn(PanelMode, MODE_NAMES[i], 2 + (i mod 4) * W, 16 + (i div 4) * 27, W - 2, BTN_SM, BtnModeClick); B.Tag := i; BtnMode[i] := B; StyleButton(B, i = 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); W := (LEFT_W - 6) div 5; for i := 0 to FILT_COUNT - 1 do begin B := MakeBtn(PanelFilter, '---', 2 + (i mod 5) * W, 16 + (i div 5) * 27, W - 2, BTN_SM, BtnFilterClick); B.Tag := i; BtnFilter[i] := B; end; Inc(Y, 76); // CTUN button BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick); BtnCTun.Tag := 0; StyleButton(BtnCTun, FCTun); Inc(Y, BTN_H + 4); // RX controls PanelRX := TPanel.Create(Self); PanelRX.Parent := PanelLeft; PanelRX.SetBounds(0, Y, LEFT_W, 150); PanelRX.BevelOuter := bvNone; // AGC mode — 5 кнопок в ряд MakeLbl(PanelRX, 'AGC', 4, 4); W := (LEFT_W - 10) div 5; BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 2, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag:=0; BtnAGCMode[1] := MakeBtn(PanelRX,'MED', W+4, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag:=1; BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 2*W+6, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag:=2; BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 3*W+8, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag:=3; BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 4*W+10, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag:=4; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode); // AGC level slider MakeLbl(PanelRX, 'THRESH', 4, 50); TrkAGC := TTrackBar.Create(Self); TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 46; TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 20; TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → −20..-120 dBm TrkAGC.Position := FAGCTop; TrkAGC.TickStyle := tsNone; TrkAGC.Reversed := True; TrkAGC.OnChange := TrkAGCChange; LblAGCTop := TLabel.Create(Self); LblAGCTop.Parent := PanelRX; LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 50; LblAGCTop.Caption := Format('-%ddB', [FAGCTop]); LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New'; LblAGCTop.Font.Size := 7; // VOL MakeLbl(PanelRX, 'VOL', 4, 74); TrkVolume := TTrackBar.Create(Self); TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 70; TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 20; TrkVolume.Min := 0; TrkVolume.Max := 100; TrkVolume.Position := FVolume; TrkVolume.TickStyle := tsNone; TrkVolume.OnChange := TrkVolumeChange; // NR NB SNB ANF MUTE W := (LEFT_W - 10) div 5; BtnNR := MakeBtn(PanelRX, 'NR', 2, 96, W, BTN_SM, BtnNRClick); BtnNB := MakeBtn(PanelRX, 'NB', W+4, 96, W, BTN_SM, BtnNBClick); BtnSNB := MakeBtn(PanelRX, 'SNB', 2*W+6, 96, W, BTN_SM, BtnSNBClick); BtnANF := MakeBtn(PanelRX, 'ANF', 3*W+8, 96, W, BTN_SM, BtnANFClick); BtnMute:= MakeBtn(PanelRX, 'MUTE', 4*W+10, 96, W, BTN_SM, BtnMuteClick); BtnWfAGC := MakeBtn(PanelRX, 'WF AGC', 2, 122, (LEFT_W - 6) div 2, BTN_SM, BtnWfAGCClick); BtnWfNF := MakeBtn(PanelRX, 'WF NF', 4 + (LEFT_W - 6) div 2, 122, (LEFT_W - 6) div 2, BTN_SM, BtnWfNFClick); Inc(Y, 154); // S-Meter Inc(Y, 52); // TX controls PanelTX := TPanel.Create(Self); PanelTX.Parent := PanelLeft; PanelTX.SetBounds(0, Y, LEFT_W, 104); PanelTX.BevelOuter := bvNone; MakeLbl(PanelTX, 'TX', 4, 2); LblDrv := TLabel.Create(Self); LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 17; LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM; LblDrv.Font.Name := 'Courier New'; LblDrv.Font.Size := 7; TrkDrive := TTrackBar.Create(Self); TrkDrive.Parent := PanelTX; TrkDrive.Left := 34; TrkDrive.Top := 14; TrkDrive.Width := 150; TrkDrive.Height := 22; TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50; TrkDrive.TickStyle := tsNone; TrkDrive.OnChange := TrkDriveChange; BtnMOX := MakeBtn(PanelTX, 'MOX', 2, 38, 70, 32, BtnMOXClick); BtnMOX.Font.Size := 12; BtnMOX.Font.Bold := True; LblFwdPwr := TLabel.Create(Self); LblFwdPwr.Parent := PanelTX; LblFwdPwr.Left := 80; LblFwdPwr.Top := 42; LblFwdPwr.Caption := '0W'; LblFwdPwr.Font.Color := CLR_METER_ON; LblFwdPwr.Font.Name := 'Courier New'; LblFwdPwr.Font.Size := 8; LblSWRVal := TLabel.Create(Self); LblSWRVal.Parent := PanelTX; LblSWRVal.Left := 140; LblSWRVal.Top := 42; LblSWRVal.Caption := 'SWR:1.0'; LblSWRVal.Font.Color := CLR_TEXTDIM; LblSWRVal.Font.Name := 'Courier New'; LblSWRVal.Font.Size := 8; MakeLbl(PanelTX, 'PWR', 4, 76); PbFwdPower := TPaintBox.Create(Self); PbFwdPower.Parent := PanelTX; PbFwdPower.SetBounds(34, 74, LEFT_W - 38, 12); PbFwdPower.OnPaint := FSpecView.PaintFwdPower; MakeLbl(PanelTX, 'SWR', 4, 92); PbSWR := TPaintBox.Create(Self); PbSWR.Parent := PanelTX; PbSWR.SetBounds(34, 90, LEFT_W - 38, 12); PbSWR.OnPaint := FSpecView.PaintSWR; // ---- Right panel ---- PanelRight := TPanel.Create(Self); PanelRight.Parent := Self; PanelRight.Align := alClient; PanelRight.BevelOuter := bvNone; PanelRight.OnResize := RightPanelResize; PanelSpanButtons := TPanel.Create(Self); PanelSpanButtons.Parent := PanelRight; PanelSpanButtons.Align := alTop; PanelSpanButtons.Height := 30; PanelSpanButtons.BevelOuter := bvNone; LblSpan := TLabel.Create(Self); LblSpan.Parent := PanelSpanButtons; LblSpan.Left := 4; LblSpan.Top := 8; LblSpan.Caption := 'SPAN:'; LblSpan.Font.Color := CLR_TEXTDIM; LblSpan.Font.Name := 'Courier New'; LblSpan.Font.Size := 7; BtnSpan48k := MakeBtn(PanelSpanButtons, '48k', 50, 3, 46, BTN_SM, BtnSpanClick); BtnSpan96k := MakeBtn(PanelSpanButtons, '96k', 99, 3, 46, BTN_SM, BtnSpanClick); BtnSpan192k := MakeBtn(PanelSpanButtons, '192k', 148, 3, 52, BTN_SM, BtnSpanClick); BtnSpan384k := MakeBtn(PanelSpanButtons, '384k', 203, 3, 52, BTN_SM, BtnSpanClick); BtnSpan768k := MakeBtn(PanelSpanButtons, '768k', 258, 3, 52, BTN_SM, BtnSpanClick); BtnSpan1536k := MakeBtn(PanelSpanButtons, '1536k', 313, 3, 58, BTN_SM, BtnSpanClick); BtnSpan48k.Tag := 48000; BtnSpan96k.Tag := 96000; BtnSpan192k.Tag := 192000; BtnSpan384k.Tag := 384000; BtnSpan768k.Tag := 768000; BtnSpan1536k.Tag := 1536000; StyleSpanButton(BtnSpan48k, False); StyleSpanButton(BtnSpan96k, False); StyleSpanButton(BtnSpan192k, True); StyleSpanButton(BtnSpan384k, False); StyleSpanButton(BtnSpan768k, False); StyleSpanButton(BtnSpan1536k, False); BtnHidePanel := MakeBtn(PanelRight, '◀', 0, 0, 28, 28, BtnHidePanelClick); BtnHidePanel.Font.Size := 10; BtnHidePanel.ClrNorm := TColor($00543F18); BtnHidePanel.ClrHot := TColor($00624B1D); BtnHidePanel.ClrActive := TColor($00624B1D); BtnHidePanel.ClrBorder := TColor($00685624); BtnHidePanel.ClrText := TColor($00D8E6EE); BtnHidePanel.ClrTextAct := TColor($00F0FAFF); BtnHidePanel.BringToFront; // Большой S-метр — на главной форме поверх двух панелей // S-метр: Parent=Self, перекрывает Toolbar+SpanButtons вертикально PanelSMeterRight := TPanel.Create(Self); PanelSMeterRight.Parent := Self; 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; PbSpectrum := TPaintBox.Create(Self); PbSpectrum.Parent := PanelRight; PbSpectrum.OnPaint := FSpecView.PaintSpectrum; PbSpectrum.OnMouseDown := PbSpectrumMouseDown; PbSpectrum.OnDblClick := PbSpectrumDblClick; PbSpectrum.OnMouseMove := PbSpectrumMouseMove; PbSpectrum.OnMouseUp := PbSpectrumMouseUp; 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; PbWaterfall := TPaintBox.Create(Self); PbWaterfall.Parent := PanelRight; PbWaterfall.OnPaint := FSpecView.PaintWaterfall; PbWaterfall.OnMouseDown := PbWaterfallMouseDown; PbWaterfall.OnMouseMove := PbWaterfallMouseMove; PbWaterfall.OnMouseUp := PbWaterfallMouseUp; // SPAN кнопки как оверлей на спектре (в стиле web) BtnSpan48k.Parent := PanelRight; BtnSpan96k.Parent := PanelRight; BtnSpan192k.Parent := PanelRight; BtnSpan384k.Parent := PanelRight; BtnSpan768k.Parent := PanelRight; BtnSpan1536k.Parent := PanelRight; LblSpan.Visible := False; PositionSpanOverlayButtons; // 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; const RULER_H = 18; SPLITTER_H = 5; MIN_SH = 60; MIN_WH = 40; begin if not FSplitterDrag then Exit; P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y))); DeltaY := P.Y - FSplitterDragY0; TopOff := PanelSpanButtons.Height; 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; const RULER_H = 18; SPLITTER_H = 5; MIN_SH = 60; MIN_WH = 40; begin 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 := PanelSpanButtons.Height; 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.ResizeSMeter; // PanelSMeterRight на главной форме (Parent=Self). // По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px). // По X: 40% ширины PanelRight, прижат к правому краю. const MARGIN = 3; var FW, FH: Integer; RightW: Integer; // ширина PanelRight SMW: Integer; // ширина S-метра SMLeft: Integer; SMTop: Integer; SMBot: Integer; begin if (PanelSMeterRight = nil) or (PanelToolbar = nil) or (PanelSpanButtons = nil) or (PanelLeft = nil) then Exit; FW := ClientWidth; // Ширина зоны спектра (PanelRight) RightW := FW - PanelLeft.Width; if RightW < 200 then Exit; // Ширина S-метра = 40% от PanelRight SMW := Round(RightW * 0.40); // X: прижат к правому краю формы SMLeft := FW - SMW - MARGIN; // Y: от верха формы+margin до низа PanelSpanButtons-margin SMTop := MARGIN; SMBot := PanelToolbar.Height + PanelSpanButtons.Height - MARGIN; PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop); PanelSMeterRight.BringToFront; end; procedure TMainForm.ResizeSpectrumPanels; const RULER_H = 18; SPLITTER_H = 5; MIN_SH = 60; // минимальная высота спектра MIN_WH = 40; // минимальная высота водопада var RW, RH, SH, WH, TopOff: Integer; AvailH: Integer; begin if PanelRight = nil then Exit; if Assigned(FSpecView) then SyncSpecViewFreq; RW := PanelRight.ClientWidth; RH := PanelRight.ClientHeight; TopOff := PanelSpanButtons.Height; // 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; PositionSpanOverlayButtons; 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; PositionSpanOverlayButtons; 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; PositionSpanOverlayButtons; 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); PositionSpanOverlayButtons; // Обновляем переменные размеров 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; procedure DP(P: TPanel); begin P.Color := CLR_PANEL; P.Font.Color := CLR_TEXT; end; begin Color := CLR_BG; Font.Color := CLR_TEXT; DP(PanelToolbar); DP(PanelLeft); DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons); DP(PanelBands); DP(PanelMode); DP(PanelFilter); DP(PanelRX); DP(PanelTX); DP(PanelRight); DP(PanelSpanButtons); PbSpectrum.Color := CLR_BG; PbWaterfall.Color := CLR_BG; if PanelSMeterRight <> nil then PanelSMeterRight.Color := CLR_PANEL; if PbSMeterRight <> nil then PbSMeterRight.Color := CLR_PANEL; PbFwdPower.Color := CLR_BG; PbSWR.Color := CLR_BG; end; procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean); begin B.Active := Active; B.ClrNorm := CLR_INACTIVE; B.ClrActive := CLR_ACTIVE; B.ClrHot := TColor($00323232); B.ClrBorder := TColor($00505050); B.ClrText := CLR_TEXT; B.ClrTextAct := TColor($0000FF88); B.Font.Name := 'Courier New'; B.Font.Size := 8; end; procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean); begin B.Active := Active; B.ClrNorm := TColor($00543F18); B.ClrActive := TColor($00624B1D); B.ClrHot := TColor($00624B1D); B.ClrBorder := TColor($00685624); B.ClrText := TColor($00D8E6EE); B.ClrTextAct := TColor($00F0FAFF); B.Font.Name := 'Courier New'; B.Font.Size := 7; end; procedure TMainForm.PositionSpanOverlayButtons; const BTN_TOP = 6; GAP = 3; W48 = 40; W96 = 40; W192 = 44; W384 = 44; W768 = 44; W1536 = 50; HBTN = 18; var X, NeedW, BaseX, BaseY: Integer; begin if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit; NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8; if PbSpectrum.Width < NeedW then begin BtnSpan48k.Visible := False; BtnSpan96k.Visible := False; BtnSpan192k.Visible := False; BtnSpan384k.Visible := False; BtnSpan768k.Visible := False; BtnSpan1536k.Visible := False; Exit; end; BaseX := PbSpectrum.Left; BaseY := PbSpectrum.Top; X := BaseX + 56; BtnSpan48k.SetBounds(X, BaseY + BTN_TOP, W48, HBTN); Inc(X, W48 + GAP); BtnSpan96k.SetBounds(X, BaseY + BTN_TOP, W96, HBTN); Inc(X, W96 + GAP); BtnSpan192k.SetBounds(X, BaseY + BTN_TOP, W192, HBTN); Inc(X, W192 + GAP); BtnSpan384k.SetBounds(X, BaseY + BTN_TOP, W384, HBTN); Inc(X, W384 + GAP); BtnSpan768k.SetBounds(X, BaseY + BTN_TOP, W768, HBTN); Inc(X, W768 + GAP); BtnSpan1536k.SetBounds(X, BaseY + BTN_TOP, W1536, HBTN); if BtnHidePanel <> nil then BtnHidePanel.SetBounds(BaseX, BaseY + BTN_TOP, 28, HBTN); BtnSpan48k.Visible := True; BtnSpan96k.Visible := True; BtnSpan192k.Visible := True; BtnSpan384k.Visible := True; BtnSpan768k.Visible := True; BtnSpan1536k.Visible := True; BtnSpan48k.BringToFront; BtnSpan96k.BringToFront; BtnSpan192k.BringToFront; BtnSpan384k.BringToFront; BtnSpan768k.BringToFront; BtnSpan1536k.BringToFront; if BtnHidePanel <> nil then BtnHidePanel.BringToFront; end; // =========================================================================== // VFO // =========================================================================== function TMainForm.FormatFreq(Hz: Double): string; var Mhz, KHz, Rest: Integer; begin Mhz := Trunc(Hz / 1000000); KHz := Trunc((Hz - Mhz * 1000000) / 1000); Rest := Trunc(Hz) mod 1000; Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]); end; procedure TMainForm.UpdateVfoDisplay; begin FreqDispA.Frequency := Round(FVfoA); FreqDispB.Frequency := Round(FVfoB); if FActiveVfo = 0 then begin // VFO-A активен: яркий зелёный, крупный FreqDispA.ColorNormal := CLR_FREQ; FreqDispA.FontSize := 20; FreqDispB.ColorNormal := CLR_FREQ_DIM; FreqDispB.FontSize := 14; FreqDispA.Height := 36; FreqDispB.Height := 28; LblVfoALabel.Font.Color := CLR_FREQ; LblVfoBLabel.Font.Color := CLR_TEXTDIM; end else begin // VFO-B активен: яркий зелёный, крупный FreqDispA.ColorNormal := CLR_FREQ_DIM; FreqDispA.FontSize := 14; FreqDispB.ColorNormal := CLR_FREQ; FreqDispB.FontSize := 20; FreqDispA.Height := 28; FreqDispB.Height := 36; LblVfoALabel.Font.Color := CLR_TEXTDIM; LblVfoBLabel.Font.Color := CLR_FREQ; 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 := FVfoA; FSpecView.VfoB := FVfoB; FSpecView.ActiveVfo := FActiveVfo; FSpecView.CenterFreq := FCenterFreq; FSpecView.SpanHz := FSpanHz; FSpecView.Mode := FMode; FSpecView.FilterBW := FFilterBW; 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 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; if PbSMeterRight <> nil then PbSMeterRight.Invalidate; end; procedure TMainForm.SpectrumTimerTick(Sender: TObject); var NowTick: QWord; ElapsedTick: QWord; begin // Синхронизируем размеры если размер панели изменился (после ресайза). // Важно: проверяем только видимые панели — когда спектр скрыт, // PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0, // что вызвало бы сброс буфера водопада на каждом тике. if (FSpectrumWidth = 0) 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; ResizeSMeter; if FSpectrumWidth <= 0 then Exit; SyncSpecViewFreq; // Проверка таймаута — трансивер не отвечает if FRunning and FNetwork.Running then begin NowTick := GetTickCount64; ElapsedTick := NowTick - FRXStartTime; // Ждём первый пакет 5 секунд после старта if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then begin BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); FRunning := False; FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН'; BtnMOX.Enabled := False; Exit; end; // После первого пакета — следим чтобы поток не прерывался более 3 сек if (FRXLastPktTime > 0) and ((NowTick - FRXLastPktTime) > 3000) then begin BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); FRunning := False; FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ'; BtnMOX.Enabled := False; Exit; end; end; if FRunning then begin // Обновляем StatusBar из таймера — без Synchronize в сетевом потоке StatusBar1.Panels[3].Text := Format('RX DDC%d | seq=%d | pkts=%d', [FLastDDCIndex, FLastDDCSeq, FRXPacketCount]); if FWDSPReady then begin FLastSMeter := FDSPEngine.GetSMeterDBm; FDSPEngine.SetSpectrumWidth(FSpectrumWidth); FDSPEngine.UpdateAGCLines(FSpectrumWidth); FSpecView.AGCThresh := FDSPEngine.AGCThresh; FSpecView.AGCHangLevel := FDSPEngine.AGCHangLevel; end; FSpecView.WDSPReady := FWDSPReady; // Обновляем оверлей если видим if Assigned(FVfoOverlay) and FVfoOverlay.Visible then begin FVfoOverlay.UpdateSMeter(FLastSMeter); FVfoOverlay.UpdateVfo(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; end else begin // Не подключены — показываем чистый экран (без анимации) if not 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 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 FWebServer.PushSpectrum( FSpectrumBuf, 1024, FWaterfallBuf, FLastSMeter, FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop, FSpanHz, FVolume, FWfAGCEnabled, FWfNFEnabled, FCurrentBand, FRunning and FNetwork.Connected, FRunning, FMuted, FCTun, BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, FCenterFreq, FFilter, FVfoB, FActiveVfo, FTransmitting, TrkDrive.Position); 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(Self, 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); StatusBar1.Panels[4].Text := Format('Found: %s', [Dev.IPAddress]); end; procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus); var ExcPwr, FwdPwr, RevPwr: Word; SupplyV, FwdW, SWRV: Double; 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 := ADCToSupplyVolts((Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo); // FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick — // ExciterPwr это мощность TX, не уровень принятого сигнала. FwdW := ADCToWatts100(FwdPwr); if FwdW > 0 then SWRV := 1 + Sqrt(RevPwr / FwdPwr) else SWRV := 1.0; Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV, (Status.StatusBits and HPS_PLL_LOCKED) <> 0, (Status.StatusBits and HPS_PTT) <> 0); try M := Sync.Execute; TThread.Synchronize(nil, M); finally Sync.Free; end; end; procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean); var PLLStr: string; begin FLastFwdW := FwdW; FLastSWR := SWRV; LblFwdPwr.Caption := Format('%.0fW', [FwdW]); LblSWRVal.Caption := Format('SWR:%.1f', [SWRV]); if PLLLock then PLLStr := 'PLL OK' else PLLStr := 'PLL?'; StatusBar1.Panels[2].Text := Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s', [SupplyV, FwdW, SWRV, PLLStr]); // Hardware PTT (foot switch / mic PTT) — обнаружение фронта if HWPTT <> FHWPTTActive then begin FHWPTTActive := HWPTT; // Hardware PTT включает/выключает передачу только если BtnMOX не нажата if not BtnMOX.Active then ApplyMOX(HWPTT); end; end; procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket); var SamplesPerFrame: Integer; begin // Вызывается из сетевого потока — UI не трогаем напрямую // Счётчик принятых пакетов Inc(FRXPacketCount); FRXLastPktTime := GetTickCount64; // фиксируем время последнего пакета // 1. Подаём IQ данные в DSP — только с активного DDC if (DDCIndex = FActiveDDC) and FWDSPReady then begin SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or Integer(Data.SamplesPerFrame[1]); if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame); end; // 2. Запоминаем последний seq — StatusBar обновит таймер (без Synchronize в горячем пути) FLastDDCSeq := (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]); FLastDDCIndex := DDCIndex; end; procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord); begin StatusBar1.Panels[3].Text := Format('RX DDC%d | seq=%d | pkts=%d', [DDCIdx, Seq, FRXPacketCount]); end; procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket); begin if FTransmitting and FWDSPReady then FDSPEngine.PushTXMicSamples16(Data.Samples, 64); 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.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, -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; StatusBar1.Panels[4].Text := 'No hardware found'; end else StatusBar1.Panels[3].Text := Format('DDC%d seq=%d', [DDCIdx, Seq]); end; procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer); begin if ASampleRate <= 0 then Exit; if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end; FWDSPReady := False; FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); FDSPEngine.OnAudio := OnAudioReady; FDSPEngine.OnSpectrum := OnSpectrumReady; FDSPEngine.OnWaterfall := OnWaterfallReady; 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 := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))); WisdomPath := WisdomDir + WDSP_WISDOM_FILE; if FileExists(WisdomPath) then begin // По рекомендации WDSP вызываем функцию на каждом старте: // если файл уже есть, библиотека просто импортирует wisdom. DirA := AnsiString(WisdomDir); WDSPwisdom(PAnsiChar(DirA)); Exit; end; BtnStartStop.Enabled := False; StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...'; BuildThread := TWisdomBuildThread.Create(WisdomDir); try Dlg := TWisdomProgressDialog.Create(Self, BuildThread); 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 StatusBar1.Panels[3].Text := 'WDSP wisdom ready' else StatusBar1.Panels[3].Text := 'WDSP wisdom missing'; end; procedure TMainForm.BtnDiscoverClick(Sender: TObject); begin // Открываем диалог выбора устройства if not Assigned(FDeviceDialog) then FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; FDeviceDialog.ShowModal; // Результат диалога не обрабатываем здесь — START сам спросит если нужно end; procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject); // Запускается когда пользователь нажимает DISCOVER внутри диалога begin SetLength(FDevices, 0); FDeviceCount := 0; StatusBar1.Panels[4].Text := 'Discovering...'; FNetwork.DirectIP := ''; TDiscoverThread.Create(FNetwork, Self); end; procedure TMainForm.BtnStartStopClick(Sender: TObject); var Idx, i: Integer; Dev: THPSDRDevice; GenPkt: TGeneralPacket; DUCPkt: TDUCSpecificPacket; G_Settings: TGlobalSettings; PreG: TGlobalSettings; PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings; PreloadRate: Integer; FoundByIP: Boolean; AutoIP: string; begin if FNetwork.Connected then begin // --- STOP --- if FRunning then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, 0, False, True, True); FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); FRunning := False; FTransmitting := False; if FWDSPReady then FDSPEngine.SetTXRun(False); StyleButton(BtnMOX, False); end; FRXPacketCount := 0; FActiveDDC := 0; StatusBar1.Panels[3].Text := 'RX: waiting...'; // Сохраняем текущую частоту/настройки чтобы следующий START восстановил их if FDevConnected then begin SaveCurrentBand; FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); FSettings.Save; end; FSpecView.ResetSpectrumBuf; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FNetwork.Disconnect; BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); StatusBar1.Panels[4].Text := 'Not connected'; StatusBar1.Panels[0].Text := 'IP: --'; StatusBar1.Panels[1].Text := 'Board: --'; BtnMOX.Enabled := False; Exit; end; // --- START --- AutoIP := ''; FPendingBoardType := 0; 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 FDeviceDialog.ShowModal <> mrOk then Exit; if not FDeviceDialog.DialogResult.Accepted then Exit; FPendingIP := FDeviceDialog.DialogResult.IPAddress; FPendingBoardType := FDeviceDialog.GetSavedBoardType( FDeviceDialog.DialogResult.SavedIdx); 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 := FSampleRate; FoundByIP := False; for i := 0 to High(FDevices) do if SameText(FDevices[i].Dev.IPAddress, Dev.IPAddress) then begin FoundByIP := True; if 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 FSampleRate := PreloadRate; FSpanHz := PreloadRate; if (not FWDSPReady) and (Assigned(FDSPEngine)) and (FDSPEngine.SampleRate <> PreloadRate) then RecreateDSPEngine(PreloadRate); end; // --- Открываем WDSP асинхронно чтобы не блокировать UI --- FPendingDev := Dev; BtnStartStop.Enabled := False; StatusBar1.Panels[3].Text := 'Opening WDSP...'; if FWDSPReady then // WDSP уже открыт — сразу подключаемся DoConnectDevice(Dev) else // Открываем в фоновом потоке TWDSPOpenThread.Create(Self); end; procedure TMainForm.OnWDSPOpenDone(Success: Boolean); begin // Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке FWDSPReady := Success; if not Success then begin StatusBar1.Panels[3].Text := 'WDSP open failed'; BtnStartStop.Enabled := True; BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); ShowMessage('WDSP not loaded.' + LineEnding + IfThen(FDSPEngine.LastError <> '', 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; DUCPkt: TDUCSpecificPacket; G_Settings: TGlobalSettings; i: Integer; begin BtnStartStop.Enabled := True; if not FNetwork.Connect(Dev) then begin ShowMessage('Failed to connect to ' + Dev.IPAddress + IfThen(FNetwork.LastError <> '', ': ' + FNetwork.LastError, '')); Exit; end; // --- Загружаем настройки устройства по MAC --- Move(FNetwork.Device.MAC[0], FDevMAC[0], 6); FDevConnected := True; FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache); FCATLastGlobal := G_Settings; CATApplySettings(G_Settings); FVolume := G_Settings.Volume; FActiveVfo := G_Settings.ActiveVfo; // PA settings FPAMaxPower := G_Settings.PAMaxPower; for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := G_Settings.PABandCal[i]; // Slider position (0..100) and calibrated drive byte TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100); FDriveLevel := CalcDriveByte; FCurrentBand := G_Settings.LastBand; // SampleRate — глобальный, загружаем до RestoreBand if G_Settings.SampleRate > 0 then begin FSampleRate := G_Settings.SampleRate; FSpanHz := FSampleRate; end; BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton; BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton; BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton; BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton; FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); FWfManualHigh := G_Settings.WfManualHigh; FWfManualLow := G_Settings.WfManualLow; FWfAGCOffset := G_Settings.WfAGCOffset; FSpecView.WfAGCEnabled := FWfAGCEnabled; FSpecView.WfNFEnabled := FWfNFEnabled; FSpecView.WfManualHigh := FWfManualHigh; FSpecView.WfManualLow := FWfManualLow; FSpecView.WfAGCOffset := FWfAGCOffset; FSpecView.ResetWfAvgBuf; FShowSpectrum := G_Settings.ShowSpectrum; FShowWaterfall := G_Settings.ShowWaterfall; if G_Settings.DisplayFPS > 0 then ApplyFPS(G_Settings.DisplayFPS); // Настройки дисплея и сетки 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 FWDSPReady then begin FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType); FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode, G_Settings.SpecAvgTimeMS); FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode, G_Settings.WfAvgTimeMS); end; // Аудио устройство вывода FAudioOut.Close; if G_Settings.AudioSampleRate > 0 then FAudioOut.SampleRate := G_Settings.AudioSampleRate; if G_Settings.AudioOutDevice <> '' then begin FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice); FAudioOutDevName := G_Settings.AudioOutDevice; end else begin FAudioOut.DeviceIndex := -1; FAudioOutDevName := ''; end; FAudioOut.Open; // Аудио устройство ввода (TX mic) if G_Settings.AudioInDevice <> '' then begin FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice); FAudioInDevName := G_Settings.AudioInDevice; FAudioIn.Open; end else begin FAudioIn.DeviceIndex := -1; FAudioInDevName := ''; end; end; RestoreBand(FCurrentBand); StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress; StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType); // General packet FillChar(GenPkt, SizeOf(GenPkt), 0); GenPkt.Command := CMD_GENERAL; GenPkt.Flags37 := $08; GenPkt.Flags38 := $01; GenPkt.PAConfig := $01; if FNetwork.Device.BoardType = 5 then GenPkt.AlexEnable := $03 else GenPkt.AlexEnable := $01; FNetwork.SendGeneralPacket(GenPkt); if FNetwork.Device.BoardType in [3, 4, 5] then FActiveDDC := 2 else FActiveDDC := 0; FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0); // Синхронизируем samplerate WDSP если нужно if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then begin FWDSPReady := False; FDSPEngine.ChangeSampleRate(FSampleRate); FWDSPReady := FDSPEngine.Initialized; end; if FWDSPReady then begin FDSPEngine.SetMode(FMode); FDSPEngine.SetVolume(FVolume / 100.0); ApplyModeFilter; ApplyNoiseFilterButtonsToDSP; // ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); end; FSpecView.AGCTop := FAGCTop; // DUC specific FillChar(DUCPkt, SizeOf(DUCPkt), 0); DUCPkt.NumDACs := 1; DUCPkt.SidetoneLevel := 50; DUCPkt.SidetoneFreqHi := Hi(600); DUCPkt.SidetoneFreqLo := Lo(600); DUCPkt.KeyerSpeed := 20; DUCPkt.KeyerWeight := 50; DUCPkt.DUC0RateHi := Hi(192); DUCPkt.DUC0RateLo := Lo(192); DUCPkt.DUC0Bits := 24; FNetwork.SendDUCSpecific(DUCPkt); FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, False, True, True); FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); FRunning := True; FRXStartTime := GetTickCount64; FRXLastPktTime := 0; // Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов if FWDSPReady then FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета BtnStartStop.Caption := 'STOP'; StyleButton(BtnStartStop, True); StatusBar1.Panels[4].Text := 'RUNNING ' + FormatFreq(FVfoA); BtnMOX.Enabled := True; end; // --------------------------------------------------------------------------- // ActivateVfo — переключаем активный VFO и перестраиваем приёмник. // Idx=0 → VFO-A активен, Idx=1 → VFO-B активен. // Перестройка всегда происходит на частоту активного VFO (не на оффсет). // CTUN при этом сбрасывается чтобы не было путаницы с оффсетом. // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // FreqToBandIdx — возвращает индекс диапазона для частоты Hz, или -1 // --------------------------------------------------------------------------- function FreqToBandIdx(Hz: Double): Integer; const // Границы диапазонов [Low, High] в Гц (приблизительные) BAND_LO: array[0..10] of Double = ( 1800000, 3500000, 5330000, 7000000, 10100000, 14000000, 18068000, 21000000, 24890000, 28000000, 50000000); BAND_HI: array[0..10] of Double = ( 2000000, 4000000, 5410000, 7300000, 10150000, 14350000, 18168000, 21450000, 24990000, 29700000, 54000000); var i: Integer; begin Result := -1; for i := 0 to 10 do if (Hz >= BAND_LO[i]) and (Hz <= BAND_HI[i]) then begin Result := i; Exit; end; end; // --------------------------------------------------------------------------- // ActiveVfoFreq — частота активного VFO // --------------------------------------------------------------------------- function TMainForm.ActiveVfoFreq: Int64; begin if FActiveVfo = 0 then Result := Round(FVfoA) else Result := Round(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 FActiveVfo := Idx; // Частота нового активного VFO if FActiveVfo = 0 then ActiveFreq := Round(FVfoA) else ActiveFreq := Round(FVfoB); // Обновляем визуальный дисплей UpdateVfoDisplay; if FCTun then begin // CTUN включён: проверяем, видна ли новая частота на текущем спектре HalfSpan := FSpanHz / 2; Offset := ActiveFreq - FCenterFreq; FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan); if FreqVisible then begin // Частота видна в окне — просто меняем shift, DDC не трогаем if FWDSPReady then FDSPEngine.SetShift(Offset); // Сеть не трогаем — DDC (FCenterFreq) остаётся прежним end else begin // Частота за пределами спектра — центрируем окно и сбрасываем CTUN FCTun := False; StyleButton(BtnCTun, False); FBandCache[FCurrentBand].CTun := False; FCenterFreq := ActiveFreq; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; end else begin // CTUN выключен: перестраиваем DDC на новую частоту FCenterFreq := ActiveFreq; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; // Переключаем кнопку диапазона по новой частоте NewBand := FreqToBandIdx(ActiveFreq); if (NewBand >= 0) and (NewBand <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := NewBand; StyleButton(BtnBand[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 (перестройка на FVfoA) if FActiveVfo <> 0 then ActivateVfo(0); end; procedure TMainForm.FreqDispBClick(Sender: TObject); begin // Клик на VFO-B → активируем B (перестройка на FVfoB) if FActiveVfo <> 1 then ActivateVfo(1); end; procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64); begin if FActiveVfo = 0 then ApplyVfoA(NewFreq) else FVfoA := NewFreq; // просто сохраняем, приёмник не перестраиваем end; procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64); var BandIdx: Integer; begin FVfoB := NewFreq; if FActiveVfo = 1 then begin // VFO-B активен: перестраиваем приёмник FCenterFreq := FVfoB; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; // Обновляем диапазон BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); 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 case FMode of 0, 1: begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i]; DefIdx := FILT_SSB_DEF; end; 3, 4: begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i]; DefIdx := FILT_CW_DEF; end; 5: begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_FM_NAMES[i]; DefIdx := FILT_FM_DEF; end; else begin for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i]; DefIdx := FILT_AM_DEF; end; end; FFilter := DefIdx; case FMode of 0, 1: FFilterBW := FILT_SSB_BW[DefIdx]; 3, 4: FFilterBW := FILT_CW_BW[DefIdx]; 5: FFilterBW := FILT_FM_BW[DefIdx]; else FFilterBW := FILT_AM_BW[DefIdx]; end; for i := 0 to FILT_COUNT-1 do begin BtnFilter[i].Caption := Names[i]; StyleButton(BtnFilter[i], i = FFilter); end; end; // --------------------------------------------------------------------------- // ApplyVfoA — единая точка смены частоты VFO A // // CTUN ВЫКЛ (классический режим): // FCenterFreq = FVfoA — спектр центрирован на VFO, маркер всегда в центре // // CTUN ВКЛ (piHPSDR/Thetis режим): // FCenterFreq статичен — спектр/водопад не двигаются // 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 FVfoA := NewFreq; Scrolled := False; if not FCTun then begin // ---- CTUN OFF ---- // DDC = VFO, сдвига нет FCenterFreq := FVfoA; if FWDSPReady then FDSPEngine.SetShift(0.0); end else begin // ---- CTUN ON ---- // DDC (FCenterFreq) стоит на месте. // SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы // демодулятор принимал сигнал на FVfoA, а не на FCenterFreq. // Shift = FVfoA - FCenterFreq (в Гц) Offset := FVfoA - FCenterFreq; if FWDSPReady then FDSPEngine.SetShift(Offset); // Проверяем не вышла ли полоса фильтра за край дисплея HalfSpan := FSpanHz / 2; case FMode of 0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; 1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end; end; if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then begin // Полоса вышла за край → прокручиваем центр if FiltHi > HalfSpan then FCenterFreq := FVfoA - HalfSpan * 0.5 else FCenterFreq := FVfoA + HalfSpan * 0.5; // Пересчитываем сдвиг после прокрутки if FWDSPReady then FDSPEngine.SetShift(FVfoA - FCenterFreq); // DDC перестраивается на новый FCenterFreq Scrolled := True; end; end; // Обновляем VFO дисплей FreqDispA.Frequency := Round(FVfoA); // Обновляем кнопку диапазона если частота перешла в другой диапазон BandIdx := FreqToBandIdx(FVfoA); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; // DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован) if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; // Перерисовываем спектр всегда (маркер и полоса двигаются) // Если идёт drag — не вызываем Draw напрямую, таймер подхватит SyncSpecViewFreq; if FSpecDrag then FSpectrumDirty := True else begin FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if Scrolled then begin FSpecView.DrawWaterfall; PbWaterfall.Invalidate; end; end; end; // --------------------------------------------------------------------------- // CTUN toggle // --------------------------------------------------------------------------- procedure TMainForm.BtnCTunClick(Sender: TObject); begin FCTun := not FCTun; StyleButton(BtnCTun, FCTun); FBandCache[FCurrentBand].CTun := FCTun; if not FCTun then begin // Выключили CTUN: центрируемся на VFO, shift=0 FCenterFreq := FVfoA; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end else begin // Включили CTUN: фиксируем текущее положение дисплея // FCenterFreq остаётся как есть — спектр не прыгает end; SyncSpecViewFreq; FSpecView.DrawSpectrum; FSpecView.DrawWaterfall; PbSpectrum.Invalidate; PbWaterfall.Invalidate; 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; StepHz := 100; ClickFreq := Round(FCenterFreq + (PixelX / PanelWidth - 0.5) * FSpanHz); ClickFreq := (ClickFreq div StepHz) * StepHz; if FActiveVfo = 0 then ApplyVfoA(ClickFreq) else begin FVfoB := ClickFreq; FreqDispB.Frequency := Round(FVfoB); FCenterFreq := ClickFreq; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; 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 * FSpanHz; if FCTun then begin // CTUN ON: drag двигает окно просмотра (FCenterFreq/DDC) // VFO остаётся, shift обновляется = VFO - новый FCenterFreq FCenterFreq := FSpecDragFreq - dFreq; if FWDSPReady then begin if FActiveVfo = 0 then FDSPEngine.SetShift(FVfoA - FCenterFreq) else FDSPEngine.SetShift(FVfoB - FCenterFreq); end; if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; FSpectrumDirty := True; end else begin // CTUN OFF: drag двигает VFO, FCenterFreq следует через Apply if FActiveVfo = 0 then ApplyVfoA(Round(FSpecDragFreq - dFreq)) else begin FVfoB := FSpecDragFreq - dFreq; FreqDispB.Frequency := Round(FVfoB); FCenterFreq := FVfoB; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; FSpectrumDirty := True; end; end; end; // Спектр procedure TMainForm.PbSpectrumMouseDown(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 FCTun then FSpecDragFreq := FCenterFreq else begin if FActiveVfo = 0 then FSpecDragFreq := FVfoA else FSpecDragFreq := 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.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin 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.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 FCTun then FSpecDragFreq := FCenterFreq else begin if FActiveVfo = 0 then FSpecDragFreq := FVfoA else FSpecDragFreq := 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; if NewBand = FCurrentBand then Exit; // уже на этом диапазоне // Сохраняем настройки текущего диапазона перед сменой if FDevConnected then begin SaveCurrentBand; FSettings.Save; end; // Переключаем диапазон FCurrentBand := NewBand; // Восстанавливаем настройки нового диапазона RestoreBand(FCurrentBand); end; procedure TMainForm.ApplyModeFilter; var Lo, Hi, Half: Integer; begin Half := FFilterBW div 2; case FMode of 0: begin Lo := -FFilterBW; Hi := -100; end; // LSB: верхний срез -100 Гц 1: begin Lo := 100; Hi := 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 FWDSPReady then FDSPEngine.SetFilter(Lo, Hi); end; procedure TMainForm.BtnModeClick(Sender: TObject); var i, N: Integer; begin N := (Sender as TFlatButton).Tag; FMode := N; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode); if FWDSPReady then FDSPEngine.SetMode(N); UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим ApplyModeFilter; end; procedure TMainForm.BtnFilterClick(Sender: TObject); var i, N: Integer; begin N := (Sender as TFlatButton).Tag; FFilter := N; // Устанавливаем полосу по текущему режиму case FMode of 0, 1: FFilterBW := FILT_SSB_BW[N]; 3, 4: FFilterBW := FILT_CW_BW[N]; 5: FFilterBW := FILT_FM_BW[N]; else FFilterBW := FILT_AM_BW[N]; // DSB, AM, SAM end; for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter); ApplyModeFilter; end; procedure TMainForm.BtnVfoSwapClick(Sender: TObject); var Tmp: Double; BandIdx: Integer; ActiveFreq: Int64; begin // Меняем частоты местами Tmp := FVfoA; FVfoA := FVfoB; FVfoB := Tmp; // Определяем новую частоту активного VFO if FActiveVfo = 0 then ActiveFreq := Round(FVfoA) else ActiveFreq := Round(FVfoB); // Перестраиваем приёмник FCenterFreq := ActiveFreq; if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; // Обновляем кнопку диапазона BandIdx := FreqToBandIdx(ActiveFreq); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[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) FVfoB := FVfoA; if FActiveVfo = 1 then begin // B активен — перестраиваем приёмник на новую частоту B FCenterFreq := Round(FVfoB); if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; FreqDispB.Frequency := Round(FVfoB); UpdateVfoDisplay; end; procedure TMainForm.BtnVfoBCopyAClick(Sender: TObject); begin // B>A: копируем частоту B в A (VFO-A получает частоту VFO-B) FVfoA := FVfoB; if FActiveVfo = 0 then ApplyVfoA(Round(FVfoA)) // A активен — перестраиваем приёмник else begin FreqDispA.Frequency := Round(FVfoA); UpdateVfoDisplay; end; end; function TMainForm.ActiveTXFreqHz: Double; begin if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; end; procedure TMainForm.ApplyMOX(Active: Boolean); begin FTransmitting := Active; if FTransmitting then begin BtnMOX.ClrNorm := TColor($00000044); BtnMOX.ClrText := TColor($000000FF); BtnMOX.ClrTextAct := TColor($000000FF); BtnMOX.Active := True; end else StyleButton(BtnMOX, False); if FWDSPReady then FDSPEngine.SetTXRun(FTransmitting); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; procedure TMainForm.BtnMOXClick(Sender: TObject); begin ApplyMOX(not FTransmitting); end; procedure TMainForm.BtnMuteClick(Sender: TObject); begin FMuted := not FMuted; StyleButton(BtnMute, FMuted); if FMuted then BtnMute.Caption := 'UNMUTE' else BtnMute.Caption := 'MUTE'; if FWDSPReady then FDSPEngine.SetMute(FMuted); end; procedure TMainForm.UpdateNRButton; begin case BtnNR.Tag of 1: BtnNR.Caption := 'NR'; 2: BtnNR.Caption := 'NR2'; 3: BtnNR.Caption := 'NR3'; 4: BtnNR.Caption := 'NR4'; else BtnNR.Caption := 'NR'; end; StyleButton(BtnNR, BtnNR.Tag <> 0); BtnNR.Invalidate; end; procedure TMainForm.UpdateNBButton; begin case BtnNB.Tag of 1: BtnNB.Caption := 'NB'; 2: BtnNB.Caption := 'NB2'; else BtnNB.Caption := 'NB'; end; StyleButton(BtnNB, BtnNB.Tag <> 0); BtnNB.Invalidate; end; procedure TMainForm.UpdateSNBButton; begin BtnSNB.Caption := 'SNB'; StyleButton(BtnSNB, BtnSNB.Tag <> 0); BtnSNB.Invalidate; end; procedure TMainForm.UpdateANFButton; begin BtnANF.Caption := 'ANF'; StyleButton(BtnANF, BtnANF.Tag <> 0); BtnANF.Invalidate; end; procedure TMainForm.ApplyNoiseFilterButtonsToDSP; begin if not FWDSPReady then Exit; FDSPEngine.SetNRMode(BtnNR.Tag); FDSPEngine.SetNBMode(BtnNB.Tag); FDSPEngine.SetSNB(BtnSNB.Tag <> 0); FDSPEngine.SetANF(BtnANF.Tag <> 0); end; procedure TMainForm.BtnNRClick(Sender: TObject); begin BtnNR.Tag := (BtnNR.Tag + 1) mod 5; UpdateNRButton; if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag); end; procedure TMainForm.BtnNBClick(Sender: TObject); begin BtnNB.Tag := (BtnNB.Tag + 1) mod 3; UpdateNBButton; if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag); end; procedure TMainForm.BtnSNBClick(Sender: TObject); begin BtnSNB.Tag := 1 - BtnSNB.Tag; UpdateSNBButton; if FWDSPReady then FDSPEngine.SetSNB(BtnSNB.Tag <> 0); end; procedure TMainForm.BtnANFClick(Sender: TObject); begin BtnANF.Tag := 1 - BtnANF.Tag; UpdateANFButton; if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag <> 0); end; procedure TMainForm.BtnWfAGCClick(Sender: TObject); begin FWfAGCEnabled := not FWfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); FSpecView.WfAGCEnabled := FWfAGCEnabled; end; procedure TMainForm.BtnWfNFClick(Sender: TObject); begin FWfNFEnabled := not FWfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); FSpecView.WfNFEnabled := FWfNFEnabled; end; procedure TMainForm.BtnHidePanelClick(Sender: TObject); begin FPanelHidden := not FPanelHidden; if FPanelHidden then begin PanelLeft.Width := 0; BtnHidePanel.Caption := '▶'; FVfoOverlay.Parent := PanelRight; FVfoOverlay.Width := 260; FVfoOverlay.Height := 130; FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter); FVfoOverlay.Visible := True; FVfoOverlay.BringToFront; PositionVfoOverlay; end else begin PanelLeft.Width := 232; BtnHidePanel.Caption := '◀'; FVfoOverlay.Visible := False; end; ResizeSpectrumPanels; ResizeSMeter; end; procedure TMainForm.PositionVfoOverlay; var VfoX, OW, SpTop: Integer; begin if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit; OW := FVfoOverlay.Width; SpTop := PbSpectrum.Top; // смещение PbSpectrum внутри PanelRight if PbSpectrum.Width > 0 then VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * PbSpectrum.Width) else VfoX := PbSpectrum.Width div 2; // Всегда справа от VFO-линии FVfoOverlay.Left := Min(PanelRight.Width - OW - 4, VfoX + 6); FVfoOverlay.Top := SpTop + 6; end; procedure TMainForm.PbSpectrumDblClick(Sender: TObject); begin // Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели) end; procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer); begin FMode := Mode; FFilterBW := FilterBW; if FWDSPReady then begin FDSPEngine.SetMode(FMode); ApplyModeFilter; end; if Assigned(FVfoOverlay) and FVfoOverlay.Visible then begin FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter); PositionVfoOverlay; // перепозиционируем при смене LSB↔USB end; end; procedure TMainForm.BtnSpanClick(Sender: TObject); var NewRate: Integer; DDCRate: Word; begin NewRate := (Sender as TFlatButton).Tag; if NewRate = FSampleRate then Exit; FSampleRate := NewRate; FSpanHz := NewRate; DDCRate := NewRate div 1000; // Обновляем кнопки сразу — UI отзывчив StyleSpanButton(BtnSpan48k, FSampleRate = 48000); StyleSpanButton(BtnSpan96k, FSampleRate = 96000); StyleSpanButton(BtnSpan192k, FSampleRate = 192000); StyleSpanButton(BtnSpan384k, FSampleRate = 384000); StyleSpanButton(BtnSpan768k, FSampleRate = 768000); StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000); Application.ProcessMessages; FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости // SampleRate — глобальный: сохраняем в global settings if FDevConnected then FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); // 1. Останавливаем WDSP и сбрасываем очередь старых пакетов if FWDSPReady then begin FWDSPReady := False; FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open end; // 2. Отправляем новый rate трансиверу // (после остановки DSP — новые пакеты сразу идут с правильным rate) if FNetwork.Connected then FNetwork.ConfigureDDCs(1, DDCRate, 0); // 3. Восстанавливаем настройки DSP FWDSPReady := FDSPEngine.Initialized; if FWDSPReady then begin FDSPEngine.SetMode(FMode); FDSPEngine.SetVolume(FVolume / 100.0); ApplyModeFilter; ApplyNoiseFilterButtonsToDSP; // ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); FDSPEngine.UpdateAGCLines(FSpectrumWidth); if FNetwork.Connected then FDSPEngine.SetShift(FVfoA - 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 FActiveVfo = 0 then ApplyVfoA(Round(FWebSyncFreq)) else begin FVfoB := FWebSyncFreq; FreqDispB.Frequency := Round(FVfoB); FCenterFreq := FVfoB; if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[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; FMode := FWebSyncInt; for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode); if FWDSPReady then FDSPEngine.SetMode(FMode); UpdateFilterButtons; ApplyModeFilter; FBandCache[FCurrentBand].Mode := 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 FFilter := Idx; case FMode of 0,1: FFilterBW := FILT_SSB_BW[Idx]; 3,4: FFilterBW := FILT_CW_BW[Idx]; 5: FFilterBW := FILT_FM_BW[Idx]; else FFilterBW := FILT_AM_BW[Idx]; end; for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter); ApplyModeFilter; FBandCache[FCurrentBand].FilterBW := FFilterBW; end; Exit; end; // Positive value = bandwidth in Hz, find closest filter FFilterBW := FWebSyncInt; case FMode of 0,1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_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_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; FFilter := best; for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter); ApplyModeFilter; FBandCache[FCurrentBand].FilterBW := FFilterBW; end; procedure TMainForm.WebOnAGC(Mode: Integer); begin FWebSyncInt := Mode; FWebSyncM := SyncWebAGC; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebAGC; var i: Integer; begin if (FWebSyncInt < 0) or (FWebSyncInt > 4) then Exit; FAGCMode := FWebSyncInt; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode); if FWDSPReady then FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); FBandCache[FCurrentBand].AGCMode := FAGCMode; end; procedure TMainForm.WebOnAGCTop(DB: Integer); begin FWebSyncInt := DB; FWebSyncM := SyncWebAGCTop; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebAGCTop; begin FAGCTop := Max(20, Min(120, FWebSyncInt)); TrkAGC.Position := FAGCTop; LblAGCTop.Caption := Format('%ddB', [FAGCTop]); if FWDSPReady then begin FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); end; FBandCache[FCurrentBand].AGCTop := FAGCTop; 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; if FWebSyncInt = FCurrentBand then Exit; if FDevConnected then begin SaveCurrentBand; FSettings.Save; end; FCurrentBand := FWebSyncInt; RestoreBand(FCurrentBand); end; procedure TMainForm.WebOnSpan(Hz: Integer); begin FWebSyncInt := Hz; FWebSyncM := SyncWebSpan; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebSpan; var ValidSpans: array[0..5] of Integer; Found: Boolean; i: Integer; Btn: TFlatButton; begin ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000; ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000; Found := False; for i := 0 to 5 do if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end; if not Found then Exit; if FWebSyncInt = FSampleRate then Exit; Btn := nil; if FWebSyncInt = 48000 then Btn := BtnSpan48k else if FWebSyncInt = 96000 then Btn := BtnSpan96k else if FWebSyncInt = 192000 then Btn := BtnSpan192k else if FWebSyncInt = 384000 then Btn := BtnSpan384k else if FWebSyncInt = 768000 then Btn := BtnSpan768k else if FWebSyncInt = 1536000 then Btn := BtnSpan1536k; if Assigned(Btn) then BtnSpanClick(Btn); end; procedure TMainForm.WebOnVolume(V: Integer); begin FWebSyncInt := V; FWebSyncM := SyncWebVolume; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebVolume; begin FVolume := Max(0, Min(100, FWebSyncInt)); TrkVolume.Position := FVolume; if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0); end; procedure TMainForm.WebOnWfAGC(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebWfAGC; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebWfAGC; begin FWfAGCEnabled := FWebSyncBool; StyleButton(BtnWfAGC, FWfAGCEnabled); FSpecView.WfAGCEnabled := FWfAGCEnabled; end; procedure TMainForm.WebOnWfNF(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebWfNF; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebWfNF; begin FWfNFEnabled := FWebSyncBool; StyleButton(BtnWfNF, FWfNFEnabled); FSpecView.WfNFEnabled := FWfNFEnabled; 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 <> 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 if FWebSyncBool <> FMuted then BtnMuteClick(nil); end; // ── CTUN ────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnCtun(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebCtun; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebCtun; begin if FWebSyncBool <> 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 if FWebSyncInt < 0 then FWebSyncInt := 0; if FWebSyncInt > 4 then FWebSyncInt := 4; if BtnNR.Tag <> FWebSyncInt then begin BtnNR.Tag := FWebSyncInt; UpdateNRButton; if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag); end; end; // ── NB ──────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnNB(Mode: Integer); begin FWebSyncInt := Mode; FWebSyncM := SyncWebNB; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebNB; begin if FWebSyncInt < 0 then FWebSyncInt := 0; if FWebSyncInt > 2 then FWebSyncInt := 2; if BtnNB.Tag <> FWebSyncInt then begin BtnNB.Tag := FWebSyncInt; UpdateNBButton; if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag); end; end; procedure TMainForm.WebOnSNB(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebSNB; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebSNB; begin if FWebSyncBool <> (BtnSNB.Tag <> 0) then BtnSNBClick(BtnSNB); end; // ── ANF ─────────────────────────────────────────────────────────────────── procedure TMainForm.WebOnANF(On_: Boolean); begin FWebSyncBool := On_; FWebSyncM := SyncWebANF; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebANF; begin if FWebSyncBool <> (BtnANF.Tag <> 0) then BtnANFClick(BtnANF); 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 FVfoB := FWebSyncFreq; FreqDispB.Frequency := Round(FVfoB); if FActiveVfo = 1 then begin FCenterFreq := FVfoB; if not FCTun then if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[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 = 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 в десктопе) FCenterFreq := FWebSyncFreq; if FWDSPReady then begin if FActiveVfo = 0 then FDSPEngine.SetShift(FVfoA - FCenterFreq) else FDSPEngine.SetShift(FVfoB - FCenterFreq); end; if FNetwork.Connected and FNetwork.Running then FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, 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.TrkDriveChange(Sender: TObject); begin FDriveLevel := CalcDriveByte; if FWDSPReady then FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0); if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; function TMainForm.CalcDriveByte: Byte; var Cal: Double; begin if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then Cal := FPABandCal[FCurrentBand] else Cal := 100.0; Result := Round(TrkDrive.Position / 100.0 * Cal / 100.0 * 255.0); end; procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer); var IData: array[0..239] of Integer; QData: array[0..239] of Integer; Offset, n, i: Integer; begin if not FNetwork.Connected then Exit; Offset := 0; while Offset < Count do begin FillChar(IData, SizeOf(IData), 0); FillChar(QData, SizeOf(QData), 0); n := Count - Offset; if n > 240 then n := 240; for i := 0 to n - 1 do begin IData[i] := Round(Buf[(Offset + i) * 2] * 8388607.0); QData[i] := Round(Buf[(Offset + i) * 2 + 1] * 8388607.0); end; FNetwork.SendDUCIQ(IData, QData); Inc(Offset, 240); end; end; procedure TMainForm.OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); var i: Integer; begin FPAMaxPower := MaxPower; for i := 0 to BAND_COUNT - 1 do begin if i <= High(BandCal) then FPABandCal[i] := BandCal[i] else FPABandCal[i] := 100.0; end; FDriveLevel := CalcDriveByte; if FNetwork.Connected and FNetwork.Running then FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); end; procedure TMainForm.TrkVolumeChange(Sender: TObject); begin FVolume := TrkVolume.Position; if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0); 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 (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(FVfoA) div Step) * Step; if Delta > 0 then NewFreq := Base + Step else begin if Round(FVfoA) = Base then NewFreq := Base - Step else NewFreq := Base; // снэп к нижней границе end; NewFreq := Max(30000, Min(60000000, NewFreq)); ApplyVfoA(NewFreq); Handled := True; end; procedure TMainForm.BtnAGCModeClick(Sender: TObject); const AGCModes: array[0..4] of TWDSPAGCMode = ( agcFast, agcMedium, agcSlow, agcLong, agcOff); var i, N: Integer; begin N := (Sender as TFlatButton).Tag; FAGCMode := N; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = N); if FWDSPReady then FDSPEngine.SetAGC(AGCModes[N], 50.0); FBandCache[FCurrentBand].AGCMode := FAGCMode; end; procedure TMainForm.TrkAGCChange(Sender: TObject); begin FAGCTop := TrkAGC.Position; LblAGCTop.Caption := Format('%ddB', [FAGCTop]); if FWDSPReady then begin FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); end; FBandCache[FCurrentBand].AGCTop := FAGCTop; FSpecView.AGCTop := FAGCTop; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; 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 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; 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 FWDSPReady then FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS); if FWDSPReady then FDSPEngine.SetFFTParams(FFTSize, WinType); FSpectrumDirty := True; end; procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double); begin if FWDSPReady then 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.SpecRefLevel := FSpecRefLevel; FSpecView.SpecRange := FSpecRange; FSpecView.SpecGridStep := FSpecGridStep; InvalidateGridCache; end; procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string); begin if FAudioOut.IsOpen then FAudioOut.Close; FAudioOut.DeviceIndex := DevIndex; FAudioOutDevName := DevName; try if not FAudioOut.Open then StatusBar1.Panels[3].Text := 'Audio out: ' + FAudioOut.LastError else StatusBar1.Panels[3].Text := 'Audio out: ' + IfThen(DevName = '', 'Default', DevName); except on E: Exception do StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message; end; end; procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); begin if FAudioIn.IsOpen then FAudioIn.Close; FAudioIn.DeviceIndex := DevIndex; FAudioInDevName := DevName; if DevIndex >= 0 then begin try if not FAudioIn.Open then StatusBar1.Panels[3].Text := 'Mic: ' + FAudioIn.LastError else StatusBar1.Panels[3].Text := 'Mic: ' + DevName; except on E: Exception do StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message; end; end else StatusBar1.Panels[3].Text := 'Mic: none'; end; procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean); begin FShowSpectrum := ShowSpectrum; FShowWaterfall := ShowWaterfall; 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; // Thetis по умолчанию обновляет водопад через один display frame. FWaterfallFrameInterval := 2; FWaterfallFrameCounter := 0; FWaterfallDirty := True; FSpecView.WfFrameInterval := FWaterfallFrameInterval; if FWDSPReady then FDSPEngine.SetDisplayFPS(FPS); 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.OnVisibilityChange := ApplyVisibility; SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; SF.OnCATChange := OnCATSettingsChange; end; SF := TSettingsForm(FSettingsForm); SF.LoadPASettings(FPAMaxPower, FPABandCal); SF.LoadCATSettings( FCATLastGlobal.CATSerialEnabled, FCATLastGlobal.CATSerialPort, FCATLastGlobal.CATSerialBaud, FCATLastGlobal.CATSerialDataBits, FCATLastGlobal.CATSerialStopBits, FCATLastGlobal.CATSerialParity, FCATLastGlobal.CATTcpEnabled, FCATLastGlobal.CATTcpPort); // Перечисляем PA устройства SF.RefreshAudioDevices(FAudioOut, FAudioIn); SF.LoadVisibility(FShowSpectrum, FShowWaterfall); SF.LoadFPS(FDisplayFPS); // Загружаем текущие значения if FWDSPReady then SF.LoadValues( FDSPEngine.FFTSize, FDSPEngine.WindowType, FDSPEngine.SpecDetector, FDSPEngine.SpecAvgMode, FDSPEngine.SpecAvgTimeMS, FDSPEngine.WfDetector, FDSPEngine.WfAvgMode, FDSPEngine.WfAvgTimeMS, FWfManualHigh, FWfManualLow, FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FAudioOutDevName, FAudioInDevName) else SF.LoadValues( 131072, 2, 0, 3, 30.0, 0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FAudioOutDevName, FAudioInDevName); SF.Show; end; procedure TMainForm.AfterShowTick(Sender: TObject); begin // Вызывается один раз через 200 мс после старта формы FAfterShowTimer.Enabled := False; // Открываем PortAudio — теперь FPC уже перехватывает сигналы try if not FAudioOut.Open then StatusBar1.Panels[3].Text := 'Audio: ' + FAudioOut.LastError else StatusBar1.Panels[3].Text := 'Audio OK (PortAudio)'; except on E: Exception do StatusBar1.Panels[3].Text := 'Audio: ' + E.Message; 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); if FDevConnected then begin FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); FSettings.Save; end; end; // --- Getters (called from CAT thread — read only, no sync needed) ----------- function TMainForm.CATGetVfoA: Double; begin Result := FVfoA; end; function TMainForm.CATGetVfoB: Double; begin Result := FVfoB; end; function TMainForm.CATGetMode: Integer; begin Result := FMode; end; function TMainForm.CATGetActiveVfo: Integer; begin Result := FActiveVfo; end; function TMainForm.CATGetAGCMode: Integer; begin Result := FAGCMode; end; function TMainForm.CATGetVolume: Integer; begin Result := FVolume; end; function TMainForm.CATGetDriveLevel: Integer;begin Result := TrkDrive.Position; end; function TMainForm.CATGetFilterIdx: Integer; begin Result := FFilter; end; function TMainForm.CATGetFilterBW: Integer; begin Result := FFilterBW; end; function TMainForm.CATGetNRMode: Integer; begin Result := BtnNR.Tag; end; function TMainForm.CATGetNBMode: Integer; begin Result := BtnNB.Tag; end; function TMainForm.CATGetSNB: Boolean; begin Result := BtnSNB.Tag <> 0; end; function TMainForm.CATGetANF: Boolean; begin Result := BtnANF.Tag <> 0; end; function TMainForm.CATGetTX: Boolean; begin Result := FTransmitting; end; function TMainForm.CATGetRunning: Boolean; begin Result := FRunning; end; function TMainForm.CATGetSMeter: Double; begin Result := FLastSMeter; end; function TMainForm.CATGetBand: Integer; begin Result := 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 FVfoA := FCATSyncFreq; if FActiveVfo = 0 then begin ApplyVfoA(Round(FVfoA)); end else begin // VFO A is not active — just update display and band indicator FreqDispA.Frequency := Round(FVfoA); BandIdx := FreqToBandIdx(FVfoA); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; end; end; procedure TMainForm.SyncCATVfoB; var BandIdx: Integer; begin FVfoB := FCATSyncFreq; FreqDispB.Frequency := Round(FVfoB); if FActiveVfo = 1 then begin FCenterFreq := FVfoB; if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; UpdateVfoDisplay; end; end; procedure TMainForm.SyncCATBandUp; begin if FCurrentBand < BAND_COUNT - 1 then WebOnBand(FCurrentBand + 1); end; procedure TMainForm.SyncCATBandDown; begin if FCurrentBand > 0 then WebOnBand(FCurrentBand - 1); end; procedure TMainForm.SyncCATTuneUp; begin if FActiveVfo = 0 then ApplyVfoA(Round(FVfoA) + 10) else begin FCATSyncFreq := FVfoB + 10; SyncCATVfoB; end; end; procedure TMainForm.SyncCATTuneDown; begin if FActiveVfo = 0 then ApplyVfoA(Round(FVfoA) - 10) else begin FCATSyncFreq := FVfoB - 10; SyncCATVfoB; end; end; end.