mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
The headless daemon (ewsdrd) only ever built HPSDR devices, so Pluto could not be used: backend is chosen by Dev.Kind and Pluto opens by URI, neither of which the daemon propagated. Also local audio played on the host and web Discover never probed network Plutos. Shared, backend-agnostic helpers in TRadioController (used by GUI + daemon): - ResolveDevice(IP): discovered->saved lookup restoring Kind/URI/Serial/ BoardType/MAC. MainForm.ResolveDevice now delegates here. - AddSavedDevice(name, addr): web dev_add saves Pluto when addr has a URI scheme (ip:/usb:/local:), else HPSDR. Both web hosts use it. - SeedPlutoProbeFromSaved: seed network-probe URIs from saved Plutos (no mDNS -> a network Pluto is only found by direct URI probe). - LocalAudioEnabled flag (GUI=True): when False the local sound card is neither opened nor written; RX audio goes only to web (OnAudioConsume). DeviceStore.AutoStartDevice(out Dev): full autostart record (Kind/URI/ Serial) so a saved Pluto (URI-addressed, empty IPAddress on USB) can autostart. Daemon (ewsdrd.lpr): LocalAudioEnabled:=False; SyncConnect via ResolveDevice; autostart via AutoStartDevice; SyncDiscover seeds probe URIs then Discover; SyncDevAdd via AddSavedDevice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5888 lines
234 KiB
ObjectPascal
5888 lines
234 KiB
ObjectPascal
unit MainForm;
|
||
|
||
{
|
||
OpenHPSDR Transceiver - Main Form
|
||
Lazarus / FPC 3.2+ (нет inline var, нет анонимных процедур)
|
||
|
||
ИСПРАВЛЕНИЯ:
|
||
- (Web NR/NB/ANF fix) BtnNRClick, BtnNBClick, BtnANFClick переписаны
|
||
без использования «Sender as TFlatButton» — теперь обращаются напрямую
|
||
к BtnNR / BtnNB / BtnANF. Это устраняет EAccessViolation / EInvalidCast
|
||
при вызове из SyncWebNR/SyncWebNB/SyncWebANF через TThread.Synchronize,
|
||
где Sender передавался как nil. Исключение в UI-потоке приводило к
|
||
разрыву WebSocket-соединения и «отключению» веб-клиента.
|
||
- SyncWebNR / SyncWebNB / SyncWebANF: передают реальную кнопку (BtnNR /
|
||
BtnNB / BtnANF) вместо nil — защита на случай изменения обработчиков.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay,
|
||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types,
|
||
OpenGLContext,
|
||
LCLIntf, LCLType, GraphType,
|
||
HPSDRProtocol, HPSDRNetwork,
|
||
WDSP, WDSPEngine, AudioOutput, AudioInput,
|
||
Settings,
|
||
WebServer, WebAdapter,
|
||
CATAdapter,
|
||
SpectrumView, SpectrumViewOpengl,
|
||
WidebandView,
|
||
StatusBar,
|
||
PlatformUtils,
|
||
WinFirewall,
|
||
BoardUtils, WisdomBuilder, UISync,
|
||
FlatEdit, FMRepeater,
|
||
ChannelStore, ChannelsForm, BeaconScopeForm,
|
||
PowerInhibit,
|
||
DeviceStore,
|
||
RadioBackend, PlutoBackend,
|
||
RadioController, ChannelController;
|
||
|
||
const
|
||
CLR_BG = TColor($00101010);
|
||
CLR_PANEL = TColor($00181818);
|
||
CLR_BORDER = TColor($00303030);
|
||
CLR_FREQ = TColor($0000FF00);
|
||
CLR_FREQ_DIM = TColor($00007800);
|
||
CLR_AMBER = TColor($0000AAFF);
|
||
CLR_ACTIVE = TColor($000C3010);
|
||
CLR_ACTIVE_BDR = TColor($00387838);
|
||
CLR_INACTIVE = TColor($001A1A1A);
|
||
CLR_TEXT = TColor($00CCCCCC);
|
||
CLR_TEXTDIM = TColor($00666666);
|
||
CLR_METER_ON = TColor($0000CC44);
|
||
CLR_METER_OVR = TColor($000000CC);
|
||
CLR_SPECTRUM = TColor($0044FF44);
|
||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||
|
||
BAND_COUNT = 11;
|
||
MODE_COUNT = 8;
|
||
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
||
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
||
// UI-подписи кнопок (NAMES).
|
||
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_CW_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('1.0k','800','600','500','400','250','150','100','50','25');
|
||
FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
|
||
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
|
||
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
|
||
|
||
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
|
||
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
|
||
CTCSS_NAMES: array[0..37] of string = (
|
||
'67.0', '71.9', '74.4', '77.0', '79.7', '82.5', '85.4', '88.5',
|
||
'91.5', '94.8', '97.4', '100.0', '103.5', '107.2', '110.9', '114.8',
|
||
'118.8', '123.0', '127.3', '131.8', '136.5', '141.3', '146.2', '151.4',
|
||
'156.7', '162.2', '167.9', '173.8', '179.9', '186.2', '192.8', '203.5',
|
||
'210.7', '218.1', '225.7', '233.6', '241.8', '250.3');
|
||
|
||
// FM Step tuning
|
||
FM_STEP_COUNT = 4;
|
||
FM_STEP_HZ: array[0..3] of Integer = (6250, 12500, 20000, 25000);
|
||
FM_STEP_NAMES: array[0..3] of string = ('6.25k', '12.5k', '20k', '25k');
|
||
FM_STEP_DEF = 3; // 25 kHz
|
||
|
||
BAND_NAMES: array[0..BAND_COUNT-1] of string = (
|
||
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
|
||
BAND_FREQ: array[0..BAND_COUNT-1] of Double = (
|
||
1900000, 3750000, 5357000, 7100000, 10125000,
|
||
14200000, 18120000, 21200000, 24940000, 28500000, 50150000);
|
||
|
||
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
|
||
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
|
||
|
||
SMETER_WIDTH_RATIO = 0.32;
|
||
SMETER_MARGIN = 3;
|
||
TOP_SMETER_GAP = 8;
|
||
TOP_VFO_FONT = 25;
|
||
LEFT_PANEL_BTN_PAD = 3;
|
||
LEFT_PANEL_BTN_GAP = 2;
|
||
|
||
type
|
||
{ TMainForm }
|
||
TMainForm = class(TForm)
|
||
private
|
||
// ---- Headless ядро (состояние + движки). Фаза 1 миграции: см.
|
||
// RadioController.pas. Поля состояния/движков теперь живут в
|
||
// FController; обращения идут как FController.FXxx. ----
|
||
FController: TRadioController;
|
||
// True пока OnControllerState обновляет виджеты — чтобы программное
|
||
// изменение слайдера/кнопки не вызывало повторно команду из OnChange.
|
||
FSyncingFromController: Boolean;
|
||
|
||
// ---- Network ----
|
||
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||
FDeviceDialog: TDeviceDialog;
|
||
FPendingIP: string; // IP устройства для подключения
|
||
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
|
||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||
FPanelHidden: Boolean; // True = левая панель скрыта
|
||
FLeftPanelW: Integer; // ширина панели до скрытия (DPI-safe restore)
|
||
FPendingBoardType: Integer; // BoardType устройства для подключения
|
||
|
||
// ---- UI-local state (мышь на спектре/водопаде) ----
|
||
FSpecDrag: Boolean;
|
||
FSpecDragX0: Integer; // X при нажатии
|
||
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
||
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
|
||
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
||
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
|
||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||
FSMeterMin: Double; // нижняя граница светлой зоны
|
||
FSMeterAvg: Double; // сглаженное среднее (EMA)
|
||
// RX/seq + HP-телеметрия (FController.FLastSMeter/FwdW/SWR/SupplyV/SupplyA/PLLLock)
|
||
// живут в TRadioController.
|
||
|
||
// ---- Spectrum / Waterfall view ----
|
||
FSpecView: TSpectrumView;
|
||
FWidebandView: TWidebandView;
|
||
FUseOpenGLSpectrum: Boolean;
|
||
FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора
|
||
FSpectrumHeight: Integer;
|
||
FWaterfallHeight:Integer;
|
||
FDisplayFPS: Integer;
|
||
FWaterfallDirty: Boolean;
|
||
FWidebandDirty: Boolean;
|
||
// Display state (FShow*/FSpec*/FWf*/FTXSpec*/FController.FWidebandFill/FController.FSpectrumFill)
|
||
// переехало в TRadioController (нужно headless-load в DoConnectDevice).
|
||
// Снимок последнего кадра спектра/водопада для web переехал в TRadioController
|
||
// (FSpectrumBuf/FWaterfallBuf) — нужен headless-демону.
|
||
// Waterfall frame timing
|
||
FWaterfallFrameInterval: Integer;
|
||
FWaterfallFrameCounter: Integer;
|
||
FAgcLineCounter: Integer;
|
||
// ---- Splitter ----
|
||
FSplitterDrag: Boolean;
|
||
FSplitterDragY0: Integer;
|
||
FSplitterSH0: Integer;
|
||
FSplitterRatio: Double;
|
||
// --- Settings ---
|
||
FWebServer: TWebServer;
|
||
FWebAdapter: TWebAdapter; // мост web-сервер ↔ контроллер (вынесенные WebOnXxx)
|
||
FChannelController: TChannelController; // адаптер каналов памяти поверх контроллера
|
||
FWebEnabled: Boolean;
|
||
FWebPort: Integer;
|
||
FWebBindAddr: string;
|
||
FWebUser: string;
|
||
FWebPass: string;
|
||
// --- CAT ---
|
||
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
|
||
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
|
||
// Временные поля для передачи параметров в Synchronize-методы
|
||
FWebSyncFreq: Double;
|
||
FWebSyncInt: Integer;
|
||
FWebSyncBool: Boolean;
|
||
FWebSyncStr1: string; // для web-команд с текстом (IP/имя)
|
||
FWebSyncStr2: string;
|
||
FWebSyncM: TThreadMethod;
|
||
FSettingsForm: TObject; // TSettingsForm (cast при использовании)
|
||
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
|
||
FLightTheme: Boolean; // True = светлая тема
|
||
FFreqMhzDigits: Integer; // 3=999MHz, 4=9.999GHz, 5=99.999GHz
|
||
|
||
// ---- Timers ----
|
||
FMeterTimer: TTimer;
|
||
FSpectrumTimer: TTimer;
|
||
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
|
||
FRestoreL, FRestoreT: Integer; // позиция для отложенного восстановления
|
||
FHasPendingRestore: Boolean;
|
||
|
||
// ---- Top panel (toolbar + VFO group + S-meter) ----
|
||
PanelToolbar: TPanel;
|
||
BtnDiscover: TFlatButton;
|
||
BtnStartStop: TFlatButton;
|
||
BtnSettings: TFlatButton;
|
||
|
||
// ---- Left panel ----
|
||
PanelLeft: TPanel;
|
||
|
||
PanelVfoA: TPanel;
|
||
LblVfoALabel: TLabel;
|
||
FreqDispA: TFreqDisplay;
|
||
BtnTopVfoASelect: TFlatButton;
|
||
BtnTopVfoATX: TFlatButton;
|
||
|
||
PanelVfoB: TPanel;
|
||
LblVfoBLabel: TLabel;
|
||
FreqDispB: TFreqDisplay;
|
||
BtnTopVfoBSelect: TFlatButton;
|
||
BtnTopVfoBTX: TFlatButton;
|
||
|
||
PanelVfoButtons: TPanel;
|
||
BtnVfoSwap: TFlatButton;
|
||
BtnVfoACopyB: TFlatButton;
|
||
BtnVfoBCopyA: TFlatButton;
|
||
PanelTopVfoGroup: TPanel;
|
||
PanelTopVfoA: TPanel;
|
||
PanelTopVfoB: TPanel;
|
||
|
||
PanelBands: TPanel;
|
||
BtnBand: array[0..BAND_COUNT-1] of TFlatButton;
|
||
// Динамические кнопки XVTR (создаются/удаляются по мере того,
|
||
// как пользователь enabled/disabled слоты в Settings → Transverter).
|
||
BtnXvtrBand: array[0..CFG_XVTR_COUNT-1] of TFlatButton;
|
||
|
||
PanelMode: TPanel;
|
||
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
|
||
|
||
PanelFilter: TPanel;
|
||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
||
BtnCTun: TFlatButton;
|
||
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
|
||
BtnDUP: TFlatButton;
|
||
PanelFMSQ: TPanel;
|
||
BtnFMSQ: TFlatButton;
|
||
TrkFMSQ: TFlatSlider;
|
||
LblFMSQ: TLabel;
|
||
PanelFMCTCSS: TPanel;
|
||
BtnFMCTCSS: TFlatButton;
|
||
BtnFMCTCSSTone: TFlatButton;
|
||
FCTCSSDropDown: TFlatDropDown;
|
||
PanelFMStep: TPanel;
|
||
BtnFMStep: TFlatButton;
|
||
BtnFMStepSel: TFlatButton;
|
||
FStepDropDown: TFlatDropDown;
|
||
PanelFMRpt: TPanel;
|
||
BtnFMRptMinus: TFlatButton;
|
||
BtnFMRptPlus: TFlatButton;
|
||
EdFMRptOffset: TFlatEdit;
|
||
|
||
PanelAGC: TPanel;
|
||
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
|
||
LblAGCTop: TLabel; // показывает значение уровня
|
||
TrkAGC: TFlatSlider;
|
||
// RX hw-gain (Pluto) — ОТДЕЛЬНЫЙ блок под AGC (WDSP). Виден только для Pluto.
|
||
PanelPlutoAGC: TPanel;
|
||
BtnPlutoGain: array[0..3] of TFlatButton; // FAST/SLOW/HYB/MAN (Tag=режим)
|
||
TrkRxGain: TFlatSlider; // manual gain 0..73 dB
|
||
LblRxGain: TLabel; // значение усиления
|
||
TrkVolume: TFlatSlider;
|
||
BtnNR: TFlatButton;
|
||
BtnNB: TFlatButton;
|
||
BtnSNB: TFlatButton;
|
||
BtnANF: TFlatButton;
|
||
BtnMute: TFlatButton;
|
||
|
||
PanelSMeterRight: TPanel;
|
||
PbSMeterRight: TPaintBox;
|
||
|
||
PanelTX: TPanel;
|
||
LblDrv: TLabel;
|
||
LblDrvVal: TLabel; // значение drive: % (openHPSDR) или дБ (Pluto)
|
||
TrkDrive: TFlatSlider;
|
||
BtnMOX: TFlatButton;
|
||
BtnTUN: TFlatButton;
|
||
|
||
PanelRXBlock: TPanel;
|
||
BtnChannels: TFlatButton;
|
||
FChannelsDropDown: TFlatDropDown;
|
||
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
|
||
|
||
// ---- Right panel ----
|
||
PanelRight: TPanel;
|
||
PbSpectrum: TControl;
|
||
PbWideband: TControl;
|
||
PbWidebandRuler: TPaintBox;
|
||
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
||
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
||
PbWaterfall: TControl;
|
||
|
||
// ---- Status bar ----
|
||
StatusPanel: TMainStatusBar;
|
||
|
||
// ---- Helpers ----
|
||
procedure DoInvoke(M: TThreadMethod); // маршалинг команд контроллера в UI-поток
|
||
procedure OnControllerState(Sender: TObject; Field: TRadioField); // контроллер → UI
|
||
procedure BuildUI;
|
||
function LeftPanelButtonLeft(PanelWidth, ColCount, ColIndex: Integer): Integer;
|
||
function LeftPanelButtonWidth(PanelWidth, ColCount, ColIndex: Integer): Integer;
|
||
procedure ApplyDarkTheme;
|
||
procedure SetLightTheme(V: Boolean);
|
||
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
|
||
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
|
||
procedure SetStatusText(Index: Integer; const Text: string);
|
||
procedure SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean = False);
|
||
procedure ApplyStatusTheme(const T: TAppTheme);
|
||
procedure PositionSampleRateOverlay;
|
||
procedure OnSampleRateOverlayInvalidate(Sender: TObject);
|
||
procedure OnSampleRateSelect(SampleRate: Integer);
|
||
procedure OnSampleRateHidePanel;
|
||
procedure UpdateVfoDisplay;
|
||
function FormatFreq(Hz: Double): string;
|
||
procedure LayoutTopVfoBlock;
|
||
|
||
procedure ResizeSMeter;
|
||
procedure ResizeSpectrumPanels;
|
||
function UpdateWidebandFrequencyView: Boolean;
|
||
procedure InvalidateGridCache;
|
||
procedure SyncSpecViewFreq;
|
||
procedure RecreateDSPEngine(ASampleRate: Integer);
|
||
function EnsureWDSPWisdom: Boolean;
|
||
|
||
// Network callbacks
|
||
procedure OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt;
|
||
Count: Integer);
|
||
procedure OnWDSPOpenDone(Success: Boolean);
|
||
function ResolveDevice(const IP: string): THPSDRDevice;
|
||
procedure RelabelBands; // подписи band-кнопок под план бэкенда
|
||
procedure ApplyBackendCapsToUI; // sample-rate пресеты + гейтинг полей по Caps
|
||
procedure UpdateRxGainUI; // рендер RX hw-gain (Pluto)
|
||
procedure LayoutLeftPanel(Pluto: Boolean); // показать/скрыть Pluto-блок + reflow
|
||
procedure BtnPlutoGainClick(Sender: TObject);
|
||
procedure DoConnectDevice(const Dev: THPSDRDevice);
|
||
// Старт с уже выбранным устройством (preload rate + WDSP open + connect).
|
||
// Переиспользуется десктоп-диалогом и web-оверлеем — без модального диалога.
|
||
procedure StartWithDevice(const Dev: THPSDRDevice);
|
||
// STOP-teardown: persistence + graceful Run=0 + Disconnect + offline-статус.
|
||
// Общий для десктоп-STOP и web-disconnect.
|
||
procedure DoStopAndDisconnect;
|
||
|
||
// DSP/Audio callbacks (вызываются из рабочих потоков)
|
||
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 UpdateTXMeters;
|
||
procedure UpdateStatusField7; // поле 7: PLL (HPSDR) или beacon (Pluto)
|
||
procedure UpdateStatusField3; // поле 3: Supply (HPSDR) или Temp/RSSI (Pluto)
|
||
function BeaconStatusText: string; // компактный статус QO-100 beacon lock
|
||
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||
|
||
// Event handlers
|
||
procedure BtnDiscoverClick(Sender: TObject);
|
||
procedure BtnDiscoverFromDialog(Sender: TObject);
|
||
procedure BtnStartStopClick(Sender: TObject);
|
||
procedure FreqDispAChanged(Sender: TObject; NewFreq: Int64);
|
||
procedure ApplyVfoA(NewFreq: Int64);
|
||
procedure FreqDispAClick(Sender: TObject);
|
||
procedure FreqDispBClick(Sender: TObject);
|
||
procedure BtnTopVfoASelectClick(Sender: TObject);
|
||
procedure BtnTopVfoATXClick(Sender: TObject);
|
||
procedure BtnTopVfoBSelectClick(Sender: TObject);
|
||
procedure BtnTopVfoBTXClick(Sender: TObject);
|
||
procedure ActivateVfo(Idx: Integer);
|
||
function ActiveVfoFreq: Int64;
|
||
// Настройки
|
||
procedure SaveCurrentBand;
|
||
procedure RestoreBand(BandIdx: Integer);
|
||
procedure SaveAllAndExit;
|
||
procedure RestoreWindowBounds;
|
||
procedure SaveWindowBounds;
|
||
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||
procedure BtnBandClick(Sender: TObject);
|
||
procedure BtnModeClick(Sender: TObject);
|
||
procedure BtnFilterClick(Sender: TObject);
|
||
procedure BtnCTunClick(Sender: TObject);
|
||
procedure BtnBeaconClick(Sender: TObject);
|
||
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure UpdateBeaconButton;
|
||
procedure BtnDUPClick(Sender: TObject);
|
||
procedure ApplyDUP(Active: Boolean);
|
||
procedure BtnTUNClick(Sender: TObject);
|
||
procedure ApplyTUN(Active: Boolean);
|
||
procedure UpdateFilterButtons;
|
||
procedure BtnFMSQClick(Sender: TObject);
|
||
procedure TrkFMSQChange(Sender: TObject);
|
||
procedure ApplyFMSquelch;
|
||
procedure BtnFMCTCSSClick(Sender: TObject);
|
||
procedure BtnFMCTCSSToneClick(Sender: TObject);
|
||
procedure SetFMCTCSSTone(Idx: Integer);
|
||
procedure CloseCTCSSPopup;
|
||
procedure BtnFMRptMinusClick(Sender: TObject);
|
||
procedure BtnFMRptPlusClick(Sender: TObject);
|
||
procedure EdFMRptOffsetChange(Sender: TObject);
|
||
procedure UpdateRptAutoState;
|
||
procedure SetFMStep(Idx: Integer);
|
||
procedure CloseFMStepPopup;
|
||
procedure BtnFMStepClick(Sender: TObject);
|
||
procedure BtnFMStepSelClick(Sender: TObject);
|
||
procedure OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer);
|
||
procedure OnStepDropDownSelect(Sender: TObject; Idx: Integer);
|
||
procedure BtnChannelsClick(Sender: TObject);
|
||
procedure BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
|
||
procedure RefreshChannelsDropDown;
|
||
procedure OnChannelStoreChanged(Sender: TObject);
|
||
procedure OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
|
||
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbSpectrumMouseLeave(Sender: TObject);
|
||
procedure PbWidebandMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||
procedure DoWidebandClick(PixelX: Integer; PanelWidth: Integer);
|
||
procedure BtnVfoSwapClick(Sender: TObject);
|
||
procedure BtnVfoACopyBClick(Sender: TObject);
|
||
procedure BtnVfoBCopyAClick(Sender: TObject);
|
||
procedure BtnMOXClick(Sender: TObject);
|
||
procedure BtnMuteClick(Sender: TObject);
|
||
procedure UpdateNRButton;
|
||
procedure UpdateNBButton;
|
||
procedure UpdateSNBButton;
|
||
procedure UpdateANFButton;
|
||
procedure BtnNRClick(Sender: TObject);
|
||
procedure BtnNBClick(Sender: TObject);
|
||
procedure BtnSNBClick(Sender: TObject);
|
||
procedure BtnANFClick(Sender: TObject);
|
||
// Веб-интерфейс: host-зависимые callbacks от TWebServer (UI-обёртки).
|
||
// Чистые-контроллерные перенесены в TWebAdapter (Mode/Filter/AGC/AGCTop/Volume/
|
||
// WfAGC/WfNF/Mute/NR/NB/SNB/ANF/FreqB/Center/Attn/ActiveVfo/Ctun/MOX/Tun/FMStep/
|
||
// ClientActive). Здесь только то, что дёргает UI-обёртки MainForm.
|
||
procedure WebOnRun(On_: Boolean);
|
||
// Discovery / connect / saved-CRUD из web (маршалятся в UI)
|
||
procedure WebOnConnect(const IP: string);
|
||
procedure WebOnDiscover;
|
||
procedure WebOnDisconnect;
|
||
procedure WebOnDevAdd(const AName, IP: string);
|
||
procedure WebOnDevRemove(Idx: Integer);
|
||
procedure WebOnDevAutoStart(Idx: Integer);
|
||
procedure SyncWebConnect;
|
||
procedure SyncWebDiscover;
|
||
procedure SyncWebDisconnect;
|
||
procedure SyncWebDevAdd;
|
||
procedure SyncWebDevRemove;
|
||
procedure SyncWebDevAutoStart;
|
||
procedure PushDeviceListToWeb; // store → FWebServer.SetDeviceList
|
||
// Synchronize-обёртки (выполняются в UI-потоке) — только host-зависимые
|
||
procedure SyncWebRun;
|
||
// --- CAT settings (UI/persist; контекст+движок — в TCATAdapter) ----------
|
||
procedure OnCATSettingsChange(
|
||
const SerEnabled: array of Boolean;
|
||
const SerPort: array of string;
|
||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||
TcpEnabled: Boolean; TcpPort: Integer);
|
||
procedure OnAlexSettingsChange(const A: TAlexSettings);
|
||
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
|
||
procedure PushXvtrToWeb;
|
||
// XVTR helpers
|
||
function FindXvtrIdxForFreq(FreqHz: Double): Integer;
|
||
function XvtrTranslate(VisibleHz: Double): Double;
|
||
function XvtrTranslateTX(VisibleHz: Double): Double;
|
||
procedure ApplyXvtrToNetwork;
|
||
procedure RebuildXvtrButtons;
|
||
procedure RelayoutBelowBands;
|
||
procedure BtnXvtrBandClick(Sender: TObject);
|
||
procedure ActivateXvtrBand(Idx: Integer);
|
||
procedure DeactivateXvtr;
|
||
// -------------------------------------------------------------------------
|
||
procedure BtnSettingsClick(Sender: TObject);
|
||
procedure PositionVfoOverlay;
|
||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||
procedure OnVfoOverlayAGCChange(AGCMode: Integer);
|
||
procedure OnVfoOverlayInvalidate(Sender: TObject);
|
||
procedure PbSpectrumDblClick(Sender: TObject);
|
||
procedure TrkDriveChange(Sender: TObject);
|
||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||
procedure TrkVolumeChange(Sender: TObject);
|
||
function CalcDriveByte: Byte;
|
||
function ActiveTXFreqHz: Double;
|
||
function DefaultMicSource: TTXMicSource;
|
||
procedure ApplyMOX(Active: Boolean);
|
||
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
|
||
procedure OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
||
procedure OnVHFCalSettingsChange(const VHFCal: array of Double);
|
||
procedure OnTXSettingsChange(const T: TTXSettings);
|
||
procedure BtnAGCModeClick(Sender: TObject);
|
||
procedure TrkAGCChange(Sender: TObject);
|
||
procedure TrkRxGainChange(Sender: TObject);
|
||
// (BtnPlutoGainClick объявлен выше)
|
||
procedure MeterTimerTick(Sender: TObject);
|
||
procedure SpectrumTimerTick(Sender: TObject);
|
||
procedure RightPanelResize(Sender: TObject);
|
||
procedure AfterShowTick(Sender: TObject);
|
||
// Splitter handlers
|
||
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
|
||
public
|
||
// Методы для SettingsForm — немедленное применение настроек
|
||
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||
SpecAvgTimeMS: Double);
|
||
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||
procedure ApplyWfAGCNF(WfAGC, WfNF: Boolean);
|
||
procedure ApplyADCSettings(Dither, Random: Boolean);
|
||
procedure ApplyWebSettings(Enabled: Boolean; Port: Integer;
|
||
const BindAddr, User, Pass: string);
|
||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||
// Пушит активные grid-параметры (RX или TX в зависимости от FTransmitting)
|
||
// в FSpecView и сбрасывает кэш сетки. Вызывается при смене RX↔TX и при
|
||
// правке любого из соответствующих полей (FSpec*, FTXSpec*).
|
||
procedure ApplySpecViewGridFromState;
|
||
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||
procedure ApplyAudioBufferSize(BufferSize: Integer);
|
||
procedure ApplySendAudioToRadio(Enabled: Boolean);
|
||
// TX settings (ApplyTXSettingsToDSP/SendDUCSpecificFromSettings/
|
||
// BuildMicLineSelectByte) переехали в TRadioController.
|
||
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall, ShowWideband,
|
||
WidebandFill, SpectrumFill: Boolean);
|
||
procedure ApplyFPS(FPS: Integer);
|
||
procedure ApplyFreqMhzDigits(Digits: Integer);
|
||
procedure UpdateFreqDisplayMax; // верхняя граница FreqDisplay (XVTR/Pluto/HF)
|
||
|
||
published
|
||
// Обработчики событий формы — должны быть в published для RTTI/LFM
|
||
procedure FormCreate(Sender: TObject);
|
||
procedure FormDestroy(Sender: TObject);
|
||
procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
|
||
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
||
WheelDelta: Integer; MousePos: TPoint;
|
||
var Handled: Boolean);
|
||
end;
|
||
|
||
var
|
||
MainForm: TMainForm;
|
||
|
||
implementation
|
||
|
||
uses SettingsForm;
|
||
|
||
{$R *.lfm}
|
||
|
||
function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount,
|
||
ColIndex: Integer): Integer;
|
||
var
|
||
InnerW, BaseW, Extra: Integer;
|
||
begin
|
||
if ColCount <= 0 then Exit(0);
|
||
InnerW := PanelWidth - 2 * LEFT_PANEL_BTN_PAD -
|
||
(ColCount - 1) * LEFT_PANEL_BTN_GAP;
|
||
BaseW := InnerW div ColCount;
|
||
Extra := InnerW mod ColCount;
|
||
Result := BaseW;
|
||
if ColIndex < Extra then Inc(Result);
|
||
end;
|
||
|
||
function TMainForm.LeftPanelButtonLeft(PanelWidth, ColCount,
|
||
ColIndex: Integer): Integer;
|
||
var
|
||
i: Integer;
|
||
begin
|
||
Result := LEFT_PANEL_BTN_PAD;
|
||
for i := 0 to ColIndex - 1 do
|
||
Inc(Result, LeftPanelButtonWidth(PanelWidth, ColCount, i) +
|
||
LEFT_PANEL_BTN_GAP);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Settings helpers
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.RestoreWindowBounds;
|
||
var
|
||
L, T, Wd, Ht, SavedDPI, CurDPI: Integer;
|
||
Maximized: Boolean;
|
||
R, WA: TRect;
|
||
Mon: TMonitor;
|
||
begin
|
||
FController.FSettings.LoadWindowBounds(L, T, Wd, Ht, SavedDPI, Maximized);
|
||
if (Wd > 400) and (Ht > 300) then
|
||
begin
|
||
CurDPI := CurrentScreenDPI;
|
||
if SavedDPI <= 0 then SavedDPI := CurDPI;
|
||
|
||
if SavedDPI <> CurDPI then
|
||
begin
|
||
// Позицию не масштабируем: на LCLWin32 координаты физические и не зависят
|
||
// от DPI-фактора; на Qt6/GTK/Cocoa — логические (device-independent), тоже
|
||
// инвариантны. Выход за рабочую область поймает EnsureRange ниже.
|
||
//
|
||
// Размер масштабируем, чтобы окно выглядело одинаково при смене DPI:
|
||
// LCLWin32: физические пиксели → Wd × CurDPI/SavedDPI
|
||
// Qt6/GTK/Cocoa: логические пиксели → Wd × SavedDPI/CurDPI (обратное)
|
||
{$IFDEF LCLWin32}
|
||
Wd := MulDiv(Wd, CurDPI, SavedDPI);
|
||
Ht := MulDiv(Ht, CurDPI, SavedDPI);
|
||
{$ELSE}
|
||
Wd := MulDiv(Wd, SavedDPI, CurDPI);
|
||
Ht := MulDiv(Ht, SavedDPI, CurDPI);
|
||
{$ENDIF}
|
||
end;
|
||
|
||
// Минимальный размер:
|
||
// LCLWin32 — в физических пикселях, масштабируется с DPI.
|
||
// Qt6/GTK/Cocoa — в логических пикселях, device-independent, фиксированный.
|
||
{$IFDEF LCLWin32}
|
||
Wd := Max(MulDiv(900, CurDPI, 96), Wd);
|
||
Ht := Max(MulDiv(560, CurDPI, 96), Ht);
|
||
{$ELSE}
|
||
Wd := Max(900, Wd);
|
||
Ht := Max(560, Ht);
|
||
{$ENDIF}
|
||
R := Bounds(L, T, Wd, Ht);
|
||
Mon := Screen.MonitorFromRect(R);
|
||
if Mon <> nil then WA := Mon.WorkareaRect
|
||
else WA := Screen.PrimaryMonitor.WorkareaRect;
|
||
|
||
if Wd > WA.Right - WA.Left then Wd := WA.Right - WA.Left;
|
||
if Ht > WA.Bottom - WA.Top then Ht := WA.Bottom - WA.Top;
|
||
L := EnsureRange(L, WA.Left, Max(WA.Left, WA.Right - Wd));
|
||
T := EnsureRange(T, WA.Top, Max(WA.Top, WA.Bottom - Ht));
|
||
|
||
SetBounds(L, T, Wd, Ht);
|
||
if Maximized then
|
||
WindowState := wsMaximized
|
||
else
|
||
begin
|
||
// На Qt6/X11 frame extents ещё не известны Qt во время FormCreate,
|
||
// поэтому move() может поставить контент туда, куда мы хотели рамку.
|
||
// Повторно применим позицию в AfterShowTick, когда WM уже декорировал окно.
|
||
FRestoreL := L;
|
||
FRestoreT := T;
|
||
FHasPendingRestore := True;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SaveWindowBounds;
|
||
var
|
||
L, T, Wd, Ht, DPI: Integer;
|
||
Maximized: Boolean;
|
||
begin
|
||
Maximized := WindowState = wsMaximized;
|
||
if WindowState = wsNormal then
|
||
begin
|
||
L := Left; T := Top; Wd := Width; Ht := Height;
|
||
end
|
||
else
|
||
begin
|
||
L := RestoredLeft; T := RestoredTop;
|
||
Wd := RestoredWidth; Ht := RestoredHeight;
|
||
end;
|
||
|
||
// RestoredLeft/Top могут вернуть мусор на Qt6 при максимизации;
|
||
// ставим разумные запасные значения
|
||
if (L < -32000) or (T < -32000) then begin L := 80; T := 80; end;
|
||
if (Wd <= 400) or (Ht <= 300) then Exit;
|
||
DPI := CurrentScreenDPI;
|
||
FController.FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized);
|
||
end;
|
||
|
||
procedure TMainForm.SaveCurrentBand;
|
||
begin
|
||
// Логика сохранения (band-cache/XVTR-слот + конфиг) — в контроллере.
|
||
FController.SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.RestoreBand(BandIdx: Integer);
|
||
begin
|
||
// Тонкий делегат: состояние+DSP+финальная перестройка+рендер — в команде
|
||
// контроллера; сброс Wf-avg буфера — в OnControllerState(rfBandRestore).
|
||
FController.RestoreBand(BandIdx);
|
||
end;
|
||
|
||
procedure TMainForm.SaveAllAndExit;
|
||
begin
|
||
if FController.FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FController.SaveGlobalSettings;
|
||
if FController.FDevConnected then
|
||
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
|
||
// Запомнить LastFreq A/B XVTR для следующего запуска
|
||
if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
begin
|
||
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA;
|
||
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreqB := FController.FVfoB;
|
||
end;
|
||
FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings);
|
||
FController.FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.FormCreate(Sender: TObject);
|
||
var
|
||
bi_, StartupRate, i: Integer;
|
||
WebCfg: TWebSettings;
|
||
StartupVfoA, StartupVfoB: Double;
|
||
begin
|
||
SetExceptionMask(GetExceptionMask + [exZeroDivide, exInvalidOp]);
|
||
// Headless ядро: владеет состоянием и движками. Создаём первым — все
|
||
// присваивания FController.FXxx ниже зависят от него. Движки создаются/
|
||
// освобождаются здесь же (в полях FController) — Фаза 1 миграции.
|
||
FController := TRadioController.Create;
|
||
FController.OnInvoke := DoInvoke;
|
||
FController.OnStateChanged := OnControllerState;
|
||
// Адаптер каналов поверх контроллера: оркеструет команды, держит активный
|
||
// канал/pre-channel-drive. Рендер кнопки Channel — через OnActiveChanged;
|
||
// XVTR-вход/выход — через UI-обёртки (wideband/web живут в них).
|
||
FChannelController := TChannelController.Create(FController);
|
||
FChannelController.OnActiveChanged := OnChannelActiveChanged;
|
||
// Канал-оркестрация фаером из контроллера в конце SetVfoA (любой путь тюнинга).
|
||
FController.OnAfterTune := FChannelController.OnVfoTuned;
|
||
FController.FVfoA := 14200000;
|
||
FController.FVfoB := 7100000;
|
||
FController.FActiveVfo := 0;
|
||
FController.FMode := 1;
|
||
FController.FFilter := 5;
|
||
FController.FAGCMode := 1; // MEDIUM
|
||
FController.FAGCTop := 90; // −90 dBm
|
||
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
||
FController.FCTun := False;
|
||
FController.FFilterBW := 2700;
|
||
FSpecDrag := False;
|
||
FController.FDriveLevel := 0;
|
||
FController.FPAMaxPower := 100.0;
|
||
for i := 0 to BAND_COUNT - 1 do FController.FPABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i];
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do FController.FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
|
||
FController.FRunning := False;
|
||
FController.FTransmitting := False;
|
||
FController.FSplitTxB := False;
|
||
FController.FDisplayDuplex := False;
|
||
FController.FTuning := False;
|
||
FController.FMuted := False;
|
||
FController.FVolume := 70;
|
||
FController.FCenterFreq := FController.FVfoA;
|
||
FController.FSpanHz := 192000;
|
||
FController.FSampleRate := 192000;
|
||
FController.FCurrentBand := 5; // 20m по умолчанию
|
||
// Waterfall normalization
|
||
FController.FWfManualHigh := -80.0;
|
||
FController.FWfManualLow := -130.0;
|
||
FController.FWfAGCOffset := 0.0;
|
||
FController.FWfAGCEnabled := True;
|
||
FController.FWfNFEnabled := False;
|
||
FController.FDitherEnabled := True;
|
||
FController.FRandomEnabled := True;
|
||
FController.FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
|
||
FController.FFMCTCSSOn := False;
|
||
FController.FFMCTCSSToneIdx := 0;
|
||
FController.FFMSQOn := False;
|
||
FController.FFMSQLevel := 30;
|
||
FController.FFMStepOn := True;
|
||
FController.FFMStepIdx := FM_STEP_DEF;
|
||
FController.FFMRptDir := RPT_NONE;
|
||
FController.FFMRptOffsetHz := RPT_DEFAULT_2M;
|
||
FController.FFMRptAutoActive := False;
|
||
FController.FFMCTCSSAutoActive := False;
|
||
FWaterfallFrameInterval := 2;
|
||
FWaterfallFrameCounter := 0;
|
||
FController.FDevConnected := False;
|
||
// Spectrum grid settings defaults
|
||
FController.FSpecRefLevel := -20.0;
|
||
FController.FSpecRange := 110.0;
|
||
FController.FSpecGridStep := 10.0;
|
||
// TX grid defaults (синхронно с TSettingsManager.DefaultTX)
|
||
FController.FTXSpecRefLevel := 0.0;
|
||
FController.FTXSpecRange := 80.0;
|
||
FController.FTXSpecGridStep := 10.0;
|
||
FSettingsForm := nil;
|
||
FChannelsForm := nil;
|
||
FBeaconScopeForm := nil;
|
||
FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0);
|
||
|
||
// Инициализируем кэш диапазонов умолчаниями
|
||
for bi_ := 0 to CFG_BAND_COUNT - 1 do
|
||
TSettingsManager.DefaultBand(bi_, FController.FBandCache[bi_]);
|
||
FController.FCurrentBand := 5;
|
||
|
||
// Загружаем JSON настройки (FSettings создаётся в TRadioController.Create)
|
||
FController.FSettings.Load;
|
||
|
||
// Загружаем список каналов
|
||
FController.FChannelStore := TChannelStore.Create(CHANNELS_FILE);
|
||
FController.FChannelStore.Load;
|
||
if FController.FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
|
||
begin
|
||
FController.FVfoA := StartupVfoA;
|
||
FController.FVfoB := StartupVfoB;
|
||
FController.FCenterFreq := FController.FVfoA;
|
||
if StartupRate > 0 then
|
||
begin
|
||
FController.FSampleRate := StartupRate;
|
||
FController.FSpanHz := StartupRate;
|
||
end;
|
||
end;
|
||
// Восстанавливаем размер/позицию окна при старте (до подключения устройства)
|
||
RestoreWindowBounds;
|
||
// S-метр позиционируем после рестора размера окна
|
||
// (будет пересчитан в первом тике SpectrumTimerTick)
|
||
FController.FLastSMeter := -130;
|
||
|
||
// Загружаем настройки веб-сервера из JSON и создаём сервер
|
||
FController.FSettings.LoadWebSettings(WebCfg);
|
||
FWebEnabled := WebCfg.Enabled;
|
||
FWebPort := WebCfg.Port;
|
||
FWebBindAddr := WebCfg.BindAddr;
|
||
FWebUser := WebCfg.User;
|
||
FWebPass := WebCfg.Pass;
|
||
FWebServer := TWebServer.Create(FWebUser, FWebPass, Word(FWebPort), FWebBindAddr);
|
||
// Вынесенные в TWebAdapter чистые-контроллерные команды (host=nil: их хендлеры
|
||
// ещё не нужны хосту; host-зависимые WebOnXxx пока на MainForm — переедут в WA2).
|
||
FWebAdapter := TWebAdapter.Create(FController, FWebServer, nil);
|
||
FWebServer.OnMode := FWebAdapter.OnMode;
|
||
FWebServer.OnFilter := FWebAdapter.OnFilter;
|
||
FWebServer.OnAGC := FWebAdapter.OnAGC;
|
||
FWebServer.OnAGCTop := FWebAdapter.OnAGCTop;
|
||
FWebServer.OnVolume := FWebAdapter.OnVolume;
|
||
FWebServer.OnWfAGC := FWebAdapter.OnWfAGC;
|
||
FWebServer.OnWfNF := FWebAdapter.OnWfNF;
|
||
FWebServer.OnMute := FWebAdapter.OnMute;
|
||
FWebServer.OnNR := FWebAdapter.OnNR;
|
||
FWebServer.OnNB := FWebAdapter.OnNB;
|
||
FWebServer.OnSNB := FWebAdapter.OnSNB;
|
||
FWebServer.OnANF := FWebAdapter.OnANF;
|
||
FWebServer.OnFreqB := FWebAdapter.OnFreqB;
|
||
FWebServer.OnCenter := FWebAdapter.OnCenter;
|
||
FWebServer.OnAttn := FWebAdapter.OnAttn;
|
||
// Web-слой держит FController.FWebClientActive актуальным (выбор mic-source
|
||
// при TX, в т.ч. для HWPTT из контроллера) — больше не зеркалим лениво.
|
||
FWebServer.OnClientActiveChanged := FWebAdapter.OnClientActiveChanged;
|
||
// Host-зависимые (UI-обёртки) и исходящий push — пока на MainForm:
|
||
FWebServer.OnCtun := FWebAdapter.OnCtun;
|
||
FWebServer.OnActiveVfo := FWebAdapter.OnActiveVfo;
|
||
FWebServer.OnMOX := FWebAdapter.OnMOX;
|
||
FWebServer.OnTun := FWebAdapter.OnTun;
|
||
FWebServer.OnFMStep := FWebAdapter.OnFMStep;
|
||
// Host-зависимые (UI-обёртки) — пока на MainForm:
|
||
FWebServer.OnFreq := FWebAdapter.OnFreq;
|
||
FWebServer.OnBand := FWebAdapter.OnBand;
|
||
FWebServer.OnSpan := FWebAdapter.OnSpan;
|
||
FWebServer.OnRun := WebOnRun;
|
||
FWebServer.OnDrive := FWebAdapter.OnDrive;
|
||
FWebServer.OnFreqA := FWebAdapter.OnFreqA;
|
||
FWebServer.OnWebMic := FWebAdapter.OnMic;
|
||
FWebServer.OnXvtrBand := FWebAdapter.OnXvtrBand;
|
||
FWebServer.OnConnect := WebOnConnect;
|
||
FWebServer.OnDiscoverDev := WebOnDiscover;
|
||
FWebServer.OnDisconnectDev := WebOnDisconnect;
|
||
FWebServer.OnDevAdd := WebOnDevAdd;
|
||
FWebServer.OnDevRemove := WebOnDevRemove;
|
||
FWebServer.OnDevAutoStart := WebOnDevAutoStart;
|
||
if FWebEnabled then FWebServer.Start;
|
||
PushXvtrToWeb;
|
||
PushDeviceListToWeb; // saved-устройства сразу видны web-overlay
|
||
FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0);
|
||
FCATLastGlobal.CATTcpPort := 19090;
|
||
for i := 0 to 3 do
|
||
begin
|
||
FCATLastGlobal.CATSerialBaud[i] := 9600;
|
||
FCATLastGlobal.CATSerialDataBits[i] := 8;
|
||
FCATLastGlobal.CATSerialStopBits[i] := 1;
|
||
{$IFDEF MSWINDOWS}
|
||
FCATLastGlobal.CATSerialPort[i] := 'COM' + IntToStr(i + 1);
|
||
{$ELSE}
|
||
FCATLastGlobal.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i);
|
||
{$ENDIF}
|
||
end;
|
||
FController.FSettings.LoadCATSettings(FCATLastGlobal);
|
||
FLightTheme := FController.FSettings.LoadTheme;
|
||
FCATAdapter := TCATAdapter.Create(FController);
|
||
FCATAdapter.ApplySettings(FCATLastGlobal);
|
||
FSMeterPeak := -130;
|
||
FSMeterMin := -130;
|
||
FSMeterAvg := -130;
|
||
FController.FRXPacketCount := 0;
|
||
FController.FActiveDDC := 0;
|
||
FController.FLastDDCSeq := 0;
|
||
FController.FLastDDCIndex := 0;
|
||
FillChar(FController.FDDCLastSeq, SizeOf(FController.FDDCLastSeq), 0);
|
||
FillChar(FController.FDDCSeqValid, SizeOf(FController.FDDCSeqValid), 0);
|
||
FController.FSeqErrorCount := 0;
|
||
FController.FLastSeqErrorDDC := -1;
|
||
FController.FLastSeqErrorDelta:= 0;
|
||
FController.FSeqOkStreak := 0;
|
||
FController.FLastFwdW := 0;
|
||
FController.FLastSWR := 1;
|
||
FController.FLastSupplyV:= -1;
|
||
FController.FLastSupplyA:= -1;
|
||
FController.FLastPLLLock:= False;
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.Store := FController.FDeviceStore;
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
FVfoOverlay := TVfoOverlay.Create(Self);
|
||
FVfoOverlay.OnSelect := OnModeFilterSelect;
|
||
FVfoOverlay.OnDSPChange := OnVfoOverlayDSPChange;
|
||
FVfoOverlay.OnAGCChange := OnVfoOverlayAGCChange;
|
||
FVfoOverlay.OnInvalidate := OnVfoOverlayInvalidate;
|
||
FVfoOverlay.Visible := False;
|
||
FPanelHidden := False;
|
||
FController.FShowSpectrum := True;
|
||
FController.FShowWaterfall := True;
|
||
FController.FShowWideband := False;
|
||
FController.FWidebandFill := False;
|
||
FController.FSpectrumFill := True;
|
||
FDisplayFPS := 60;
|
||
FFreqMhzDigits := 3;
|
||
FSplitterRatio := 0.40;
|
||
FSplitterDrag := False;
|
||
|
||
FUseOpenGLSpectrum := HasOpenGLSpectrumSwitch;
|
||
if FUseOpenGLSpectrum and (not QtPlatformAllowsOpenGLControls) then
|
||
begin
|
||
MessageDlg('OpenGL rendering disabled',
|
||
'OpenGL rendering is not available in the current Qt Wayland mode.' + LineEnding +
|
||
LineEnding +
|
||
'The Lazarus/LCL Qt6 OpenGL control used by EWSDR requires an X11 display handle, ' +
|
||
'but the native Wayland backend does not provide one. Using it here can cause an access violation during startup.' +
|
||
LineEnding +
|
||
LineEnding +
|
||
'To run EWSDR with OpenGL rendering, start it through XWayland:' + LineEnding +
|
||
'QT_QPA_PLATFORM=xcb ./ewsdr --opengl' + LineEnding +
|
||
LineEnding +
|
||
'OpenGL rendering will be disabled for this session.',
|
||
mtWarning, [mbOK], 0);
|
||
FUseOpenGLSpectrum := False;
|
||
end;
|
||
if FUseOpenGLSpectrum then
|
||
FWaterfallFrameInterval := 1;
|
||
if FUseOpenGLSpectrum then
|
||
FSpecView := TSpectrumViewOpenGL.Create
|
||
else
|
||
FSpecView := TSpectrumView.Create;
|
||
FSpecView.VfoOverlay := FVfoOverlay;
|
||
FSpecView.VfoA := FController.FVfoA;
|
||
FSpecView.VfoB := FController.FVfoB;
|
||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||
FSpecView.CenterFreq := FController.FCenterFreq;
|
||
FSpecView.SpanHz := FController.FSpanHz;
|
||
FSpecView.Mode := FController.FMode;
|
||
FSpecView.FilterBW := FController.FFilterBW;
|
||
FSpecView.AGCTop := FController.FAGCTop;
|
||
FSpecView.WfAGCEnabled := FController.FWfAGCEnabled;
|
||
FSpecView.WfNFEnabled := FController.FWfNFEnabled;
|
||
FSpecView.WfManualHigh := FController.FWfManualHigh;
|
||
FSpecView.WfManualLow := FController.FWfManualLow;
|
||
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
||
FSpecView.SpecRefLevel := FController.FSpecRefLevel;
|
||
FSpecView.SpecRange := FController.FSpecRange;
|
||
FSpecView.SpecGridStep := FController.FSpecGridStep;
|
||
// TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k)
|
||
FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE;
|
||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
||
FSpecView.PAMaxPower := FController.FPAMaxPower;
|
||
FWidebandView := TWidebandView.Create;
|
||
|
||
// Движки создаёт контроллер (владелец lifecycle — нужно для демона); здесь
|
||
// лишь навешиваем колбэки на методы MainForm. FSampleRate уже загружен из
|
||
// startup-preview выше, поэтому FDSPEngine рождается с нужной частотой.
|
||
FController.CreateEngines(FController.FSampleRate);
|
||
FController.FNetwork.OnDeviceFound := FController.OnDeviceFound;
|
||
FController.FNetwork.OnHPStatus := FController.OnHPStatus;
|
||
FController.FNetwork.OnDDCIQ := FController.OnDDCIQ;
|
||
FController.FNetwork.OnMicPacket := FController.OnMicPacket;
|
||
FController.FNetwork.OnWideband := OnWidebandCB;
|
||
// Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано).
|
||
TSettingsManager.DefaultTX(FController.FTXSettings);
|
||
TSettingsManager.DefaultAlex(FController.FAlexSettings);
|
||
TSettingsManager.DefaultXvtr(FController.FXvtrSettings);
|
||
FController.FCurrentXvtr := -1;
|
||
|
||
BuildUI;
|
||
ResizeSMeter;
|
||
ApplyDarkTheme;
|
||
UpdateVfoDisplay;
|
||
UpdateFilterButtons;
|
||
|
||
// DSP Engine.Open (загрузка libwdsp, ~1-2 сек) вызывается при нажатии START.
|
||
FController.FWDSPReady := False;
|
||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
|
||
FController.OnAudioConsume := FWebAdapter.AudioConsume;
|
||
// Сырьё спектра/водопада из контроллера → рендер FSpecView (UI-подписчик).
|
||
FController.OnSpectrumData := OnSpectrumReady;
|
||
FController.OnWaterfallData := OnWaterfallReady;
|
||
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
|
||
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
|
||
FController.FDSPEngine.OnPullMicSamples := FController.PullSoundCardMic;
|
||
|
||
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;
|
||
// Грациозный стоп радио (Run=0) до освобождения; сами движки закрывает и
|
||
// освобождает TRadioController.Destroy (FreeEngines) ниже.
|
||
if FController.FNetwork.Running then
|
||
FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0);
|
||
FController.FNetwork.Disconnect;
|
||
FreeAndNil(FSpecView);
|
||
FreeAndNil(FWidebandView);
|
||
|
||
// Сохраняем настройки при закрытии
|
||
if FController.FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FController.SaveGlobalSettings;
|
||
if FController.FDevConnected then
|
||
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
|
||
end;
|
||
// Размер окна сохраняем всегда (не зависит от подключения)
|
||
FController.FSettings.SaveStartupPreview(FController.FVfoA, FController.FVfoB, FController.FSampleRate);
|
||
FController.FSettings.Save;
|
||
FreeAndNil(FChannelController); // адаптер каналов (не владеет store/контроллером)
|
||
FreeAndNil(FController.FChannelStore);
|
||
FWebServer.Stop;
|
||
FreeAndNil(FWebAdapter); // адаптер не владеет сервером — освобождаем после Stop
|
||
FWebServer.Free;
|
||
FreeAndNil(FCATAdapter); // его Destroy останавливает+освобождает CAT движок/транспорты
|
||
// Ядро освобождаем последним — его Destroy закрывает и освобождает движки
|
||
// (FreeEngines) и FSettings.
|
||
FreeAndNil(FController);
|
||
end;
|
||
|
||
// Маршалинг команд контроллера в UI-поток (фронтенды зовут FController.Invoke).
|
||
// В режиме демона хозяин подставит другой механизм (очередь/прямой вызов).
|
||
procedure TMainForm.DoInvoke(M: TThreadMethod);
|
||
begin
|
||
TThread.Synchronize(nil, M);
|
||
end;
|
||
|
||
// Контроллер изменил состояние — обновляем соответствующие виджеты.
|
||
// Выполняется в UI-потоке (команды контроллера вызываются из UI-потока либо
|
||
// маршалятся адаптерами). FSyncingFromController защищает от рекурсии, когда
|
||
// программное изменение виджета триггерит его OnChange.
|
||
procedure TMainForm.OnControllerState(Sender: TObject; Field: TRadioField);
|
||
var
|
||
i: Integer;
|
||
SavedFilter, SavedFilterBW: Integer;
|
||
begin
|
||
FSyncingFromController := True;
|
||
try
|
||
case Field of
|
||
rfVfoA:
|
||
begin
|
||
FreqDispA.Frequency := Round(FController.FVfoA);
|
||
// Не рисуем синхронно из wheel/drag: частые события забивают UI-поток.
|
||
// Invalidate лишь планирует; DrawSpectrum откладываем (FSpectrumDirty).
|
||
// Линейку дёргаем сразу — её рендер дешёвый, иначе отстаёт от спектра.
|
||
SyncSpecViewFreq;
|
||
FSpectrumDirty := True;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
if FController.FLastVfoScrolled then
|
||
begin
|
||
FWaterfallDirty := True;
|
||
if PbWaterfall <> nil then PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
rfActiveVfo:
|
||
begin
|
||
UpdateVfoDisplay; // дисплеи A/B + подсветка RX/TX-кнопок
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
rfCenterFreq:
|
||
begin
|
||
// CTUN-скролл центра (drag/web): ruler-метки сдвинулись → инвалидируем
|
||
// кэш; спектр откладываем (FSpectrumDirty) — drag шлёт много событий.
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpectrumDirty := True;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
rfVfoB:
|
||
begin
|
||
FreqDispB.Frequency := Round(FController.FVfoB);
|
||
if FController.FActiveVfo = 1 then
|
||
begin
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpectrumDirty := True;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
end;
|
||
rfTransmitting:
|
||
begin
|
||
if FController.FTransmitting then
|
||
begin
|
||
BtnMOX.ClrNorm := TColor($00000044);
|
||
BtnMOX.ClrText := TColor($000000FF);
|
||
BtnMOX.ClrTextAct := TColor($000000FF);
|
||
BtnMOX.Active := True;
|
||
end else
|
||
StyleButton(BtnMOX, False);
|
||
// TX-overlay в SpectrumView: TX-частота/VFO-индекс (по селектору
|
||
// FSplitTxB, независимо от активного RX-VFO) и режим.
|
||
FSpecView.TXFreq := ActiveTXFreqHz;
|
||
if FController.FSplitTxB then FSpecView.TXVfoIndex := 1
|
||
else FSpecView.TXVfoIndex := 0;
|
||
// DUP: данные от RX-анализатора (TXMode=False), TX-полоса рисуется
|
||
// поверх через TXOverlay. Без DUP при TX оба флага True.
|
||
FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex;
|
||
FSpecView.TXOverlay := FController.FTransmitting;
|
||
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
||
end; // (DUC-очередь сбрасывает SetMOX)
|
||
rfTuning:
|
||
StyleButton(BtnTUN, FController.FTuning);
|
||
rfRunning:
|
||
// Run-состояние: кнопка START/STOP, доступность MOX, запрет сна.
|
||
// Текст offline-статуса контекстно-зависим (Disconnected / Connection
|
||
// lost / …) — его ставит вызывающий, не общий рендер.
|
||
if FController.FRunning then
|
||
begin
|
||
InhibitSleep;
|
||
BtnStartStop.Caption := 'STOP';
|
||
StyleButton(BtnStartStop, True);
|
||
BtnMOX.Enabled := True;
|
||
end
|
||
else
|
||
begin
|
||
UninhibitSleep;
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
BtnMOX.Enabled := False;
|
||
end;
|
||
rfADCOverload:
|
||
if FSpecView <> nil then
|
||
begin
|
||
FSpecView.ADCOverloadVisible := FController.FADCOverloadActive;
|
||
if PbSpectrum <> nil then PbSpectrum.Invalidate;
|
||
end;
|
||
rfBand:
|
||
// В XVTR-режиме HF-кнопки гасим (активна XVTR-кнопка, см. rfXvtr).
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
StyleButton(BtnBand[i],
|
||
(FController.FCurrentXvtr < 0) and (i = FController.FCurrentBand));
|
||
rfXvtr:
|
||
begin
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if BtnXvtrBand[i] <> nil then
|
||
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr);
|
||
UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100)
|
||
// BEACON-кнопка — только Pluto в трансвертере (QO-100 lock); для
|
||
// openHPSDR смысла не имеет (нет дрейфующего LNB → прячем).
|
||
if BtnBeacon <> nil then
|
||
begin
|
||
BtnBeacon.Visible := FController.IsPluto and (FController.FCurrentXvtr >= 0);
|
||
UpdateBeaconButton;
|
||
end;
|
||
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
|
||
// rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное.
|
||
UpdateWidebandFrequencyView;
|
||
if PbWideband <> nil then PbWideband.Invalidate;
|
||
if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate;
|
||
PushXvtrToWeb;
|
||
end;
|
||
rfBandRestore:
|
||
// Явная смена диапазона: сброс порогов водопада (не на каждом тюне).
|
||
FSpecView.ResetWfAvgBuf;
|
||
rfMute:
|
||
begin
|
||
StyleButton(BtnMute, FController.FMuted);
|
||
if FController.FMuted then BtnMute.Caption := 'UNMUTE'
|
||
else BtnMute.Caption := 'MUTE';
|
||
BtnMute.Invalidate;
|
||
end;
|
||
rfNR: UpdateNRButton;
|
||
rfNB: UpdateNBButton;
|
||
rfSNB: UpdateSNBButton;
|
||
rfANF: UpdateANFButton;
|
||
rfMode:
|
||
begin
|
||
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FController.FMode);
|
||
// UpdateFilterButtons (для не-FM) мутирует FFilter → сбрасывает на дефолт
|
||
// режима. Это поведение нужно при СМЕНЕ режима (web/channel), но не при
|
||
// пассивном рендере: RestoreBand/ActivateXvtr уже задали фильтр из кэша.
|
||
// На реальной смене режима контроллер выставляет дефолт через
|
||
// ApplyModeDefaults ДО rfMode, так что сохраняется именно он — поведение
|
||
// не меняется. Последующий rfFilter стилит кнопки под восстановленный фильтр.
|
||
SavedFilter := FController.FFilter;
|
||
SavedFilterBW := FController.FFilterBW;
|
||
UpdateFilterButtons; // подписи + FM-панели под режим
|
||
FController.FFilter := SavedFilter;
|
||
FController.FFilterBW := SavedFilterBW;
|
||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||
SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим
|
||
end;
|
||
rfFilter:
|
||
begin
|
||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||
SyncSpecViewFreq;
|
||
end;
|
||
rfAGCMode:
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
|
||
rfVolume:
|
||
TrkVolume.Position := FController.FVolume;
|
||
rfRxGain:
|
||
UpdateRxGainUI;
|
||
rfDrive:
|
||
begin
|
||
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
|
||
UpdateDriveLabel; // % / дБ рядом с ползунком
|
||
FChannelController.OnDriveChanged; // внешнее изменение снимает память канала
|
||
end;
|
||
rfAGCTop:
|
||
begin
|
||
TrkAGC.Position := FController.FAGCTop;
|
||
LblAGCTop.Caption := Format('%ddB', [FController.FAGCTop]);
|
||
FSpecView.AGCTop := FController.FAGCTop;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
rfBeaconLock:
|
||
UpdateBeaconButton; // статус/коррекция QO-100 beacon lock
|
||
rfCTun:
|
||
begin
|
||
StyleButton(BtnCTun, FController.FCTun);
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
FSpecView.DrawWaterfall;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
rfDuplex:
|
||
begin
|
||
// Единая точка рендера DUP — для кнопки И для авто-вкл в QO-100 (оба идут
|
||
// через SetDuplex → rfDuplex). Раньше QO-100 включал DUP, но кнопку не
|
||
// подсвечивал → визуал расходился с состоянием.
|
||
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
|
||
// На передаче переключаем источник пикселей и сбрасываем буферы, чтобы
|
||
// старые TX/RX bin'ы не оставались на следующем кадре.
|
||
if FController.FTransmitting then
|
||
begin
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetDisplaySourceTX(not FController.FDisplayDuplex);
|
||
FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex;
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.ResetWfAvgBuf;
|
||
ApplySpecViewGridFromState;
|
||
FSpecView.DrawSpectrum;
|
||
FSpecView.DrawWaterfall;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
rfWfAGC:
|
||
FSpecView.WfAGCEnabled := FController.FWfAGCEnabled;
|
||
rfWfNF:
|
||
FSpecView.WfNFEnabled := FController.FWfNFEnabled;
|
||
rfFMStepIdx:
|
||
begin
|
||
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx);
|
||
if FWebServer <> nil then FWebServer.FMStepIdx := FController.FFMStepIdx;
|
||
if (FController.FMode = MODE_FM) and (FSpecView <> nil) then
|
||
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx];
|
||
end;
|
||
rfFMStep:
|
||
if BtnFMStep <> nil then
|
||
begin
|
||
StyleButton(BtnFMStep, FController.FFMStepOn);
|
||
BtnFMStep.Repaint;
|
||
end;
|
||
rfFMSQ:
|
||
begin
|
||
StyleButton(BtnFMSQ, FController.FFMSQOn);
|
||
BtnFMSQ.Repaint;
|
||
end;
|
||
rfFMSQLevel:
|
||
begin
|
||
TrkFMSQ.Position := FController.FFMSQLevel;
|
||
if LblFMSQ <> nil then LblFMSQ.Caption := IntToStr(FController.FFMSQLevel);
|
||
end;
|
||
rfFMCTCSS:
|
||
begin
|
||
StyleButton(BtnFMCTCSS, FController.FFMCTCSSOn);
|
||
BtnFMCTCSS.Repaint;
|
||
end;
|
||
rfFMCTCSSTone:
|
||
if FCTCSSDropDown <> nil then FCTCSSDropDown.SetItemIndex(FController.FFMCTCSSToneIdx);
|
||
rfSampleRate:
|
||
begin
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
|
||
// ChangeSampleRate пересоздал WDSP-канал — пересчёт AGC-линий под
|
||
// актуальную ширину спектра (UI-зависимая величина).
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
end;
|
||
rfFMRpt:
|
||
begin
|
||
if BtnFMRptMinus <> nil then
|
||
begin
|
||
StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS);
|
||
BtnFMRptMinus.Repaint;
|
||
end;
|
||
if BtnFMRptPlus <> nil then
|
||
begin
|
||
StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS);
|
||
BtnFMRptPlus.Repaint;
|
||
end;
|
||
// Текст оффсета не трогаем, пока пользователь печатает в поле.
|
||
if (EdFMRptOffset <> nil) and not EdFMRptOffset.Focused then
|
||
EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
|
||
end;
|
||
rfDevice:
|
||
begin
|
||
// Полная синхронизация device-виджетов после ConnectDevice (load-by-MAC).
|
||
// Состояние уже в контроллере; здесь только рендер + UI/CAT/web-побочки.
|
||
FCATLastGlobal := FController.FLoadedGlobal;
|
||
// Тема — app-global (своё хранилище LoadTheme/SaveTheme), не из device-блоба:
|
||
// возвращаем живое значение в persist-буфер, чтобы blob нёс актуальную тему.
|
||
FController.FLoadedGlobal.LightTheme := FLightTheme;
|
||
FCATAdapter.ApplySettings(FController.FLoadedGlobal);
|
||
RelabelBands; // HF ↔ VHF/UHF подписи под тип бэкенда
|
||
ApplyBackendCapsToUI; // пресеты sample rate + гейтинг полей
|
||
RebuildXvtrButtons;
|
||
PushXvtrToWeb;
|
||
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
|
||
UpdateDriveLabel; // переключаем единицу %/дБ под бэкенд
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
|
||
UpdateNRButton; UpdateNBButton; UpdateSNBButton; UpdateANFButton;
|
||
FSpecView.WfAGCEnabled := FController.FWfAGCEnabled;
|
||
FSpecView.WfNFEnabled := FController.FWfNFEnabled;
|
||
FSpecView.WfManualHigh := FController.FWfManualHigh;
|
||
FSpecView.WfManualLow := FController.FWfManualLow;
|
||
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
||
FSpecView.ResetWfAvgBuf;
|
||
if Assigned(FWidebandView) then
|
||
FWidebandView.SetFillSpectrum(FController.FWidebandFill);
|
||
FSpecView.FillSpectrum := FController.FSpectrumFill;
|
||
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
|
||
if FController.FLoadedGlobal.DisplayFPS > 0 then
|
||
ApplyFPS(FController.FLoadedGlobal.DisplayFPS);
|
||
ApplyFreqMhzDigits(EnsureRange(FController.FLoadedGlobal.FreqMhzDigits, 3, 5));
|
||
if FController.FLoadedGlobal.FFTSize > 0 then
|
||
begin
|
||
FSpecView.SpecRefLevel := FController.FSpecRefLevel;
|
||
FSpecView.SpecRange := FController.FSpecRange;
|
||
FSpecView.SpecGridStep := FController.FSpecGridStep;
|
||
InvalidateGridCache;
|
||
end;
|
||
end;
|
||
|
||
rfDeviceList:
|
||
begin
|
||
// Discovery обновил список устройств (или завершился без них).
|
||
if Assigned(FDeviceDialog) then
|
||
begin
|
||
if FController.FDiscoverNoneFound then
|
||
begin
|
||
FDeviceDialog.NoDevicesFound;
|
||
SetStatusText(2, 'No hardware found');
|
||
end
|
||
else
|
||
begin
|
||
FDeviceDialog.RefreshFound;
|
||
if FController.FDeviceStore.DiscoveredCount > 0 then
|
||
SetStatusText(2, Format('Found: %s',
|
||
[FController.FDeviceStore.Discovered(
|
||
FController.FDeviceStore.DiscoveredCount - 1).IPAddress]));
|
||
end;
|
||
end;
|
||
PushDeviceListToWeb; // web-overlay тоже получает обновлённый список
|
||
end;
|
||
end;
|
||
finally
|
||
FSyncingFromController := False;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
|
||
begin
|
||
// Сохраняем позицию здесь — окно ещё полностью видимо и стабильно.
|
||
// В FormDestroy Qt-виджет может быть уже в нестабильном состоянии.
|
||
SaveWindowBounds;
|
||
// Останавливаем трансивер как при нажатии STOP
|
||
if FController.FNetwork.Connected then
|
||
begin
|
||
FController.StopRunning;
|
||
FController.FNetwork.Disconnect;
|
||
end;
|
||
CloseAction := caFree;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Build UI (без анонимных процедур и inline var)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.BuildUI;
|
||
const
|
||
BTN_H = 26;
|
||
BTN_SM = 24;
|
||
LEFT_W = 232;
|
||
PLUTO_AGC_H = 64; // высота отдельного Pluto RF-AGC блока (под AGC)
|
||
TOP_VFO_W = 244;
|
||
TOP_VFO_FREQ_H = 47;
|
||
var
|
||
i, X, Y, W: Integer;
|
||
B: TFlatButton;
|
||
SepLine: TPanel;
|
||
|
||
function MakeBtn(Parent: TWinControl; const Cap: string;
|
||
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
|
||
begin
|
||
Result := TFlatButton.Create(Self);
|
||
Result.Parent := Parent;
|
||
Result.Caption := Cap;
|
||
Result.Left := ALeft;
|
||
Result.Top := ATop;
|
||
Result.Width := AW;
|
||
Result.Height := AH;
|
||
Result.OnClick := Handler;
|
||
StyleButton(Result, False);
|
||
end;
|
||
|
||
procedure MakeLbl(Parent: TWinControl; const Cap: string; ALeft, ATop: Integer);
|
||
var
|
||
L: TLabel;
|
||
begin
|
||
L := TLabel.Create(Self);
|
||
L.Parent := Parent;
|
||
L.Caption := Cap;
|
||
L.Left := ALeft;
|
||
L.Top := ATop;
|
||
L.Font.Color := CLR_TEXTDIM;
|
||
L.Font.Name := 'Courier New';
|
||
L.Font.Size := 7;
|
||
end;
|
||
|
||
begin
|
||
// ---- Toolbar ----
|
||
PanelToolbar := TPanel.Create(Self);
|
||
PanelToolbar.Parent := Self;
|
||
PanelToolbar.Align := alTop;
|
||
PanelToolbar.Height := 72;
|
||
PanelToolbar.BevelOuter := bvNone;
|
||
PanelToolbar.Color := CLR_PANEL;
|
||
|
||
|
||
X := 4;
|
||
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
|
||
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
|
||
BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
|
||
|
||
// DISCOVER — синеватый акцент
|
||
BtnDiscover.ClrNorm := TColor($00101828);
|
||
BtnDiscover.ClrHot := TColor($00182038);
|
||
BtnDiscover.ClrActive := TColor($00182038);
|
||
BtnDiscover.ClrBorder := TColor($00304860);
|
||
BtnDiscover.ClrText := TColor($0090B8D0);
|
||
BtnDiscover.ClrTextAct := TColor($00B0D0F0);
|
||
// START/STOP — зелёный тон в обоих состояниях
|
||
BtnStartStop.ClrNorm := TColor($00101810);
|
||
BtnStartStop.ClrBorder := TColor($00304830);
|
||
BtnStartStop.ClrText := TColor($0090C090);
|
||
|
||
// Линия-разделитель внутри PanelToolbar, прижата к его нижнему краю
|
||
SepLine := TPanel.Create(Self);
|
||
SepLine.Parent := PanelToolbar;
|
||
SepLine.BevelOuter := bvNone;
|
||
SepLine.Color := TColor($00606060);
|
||
SepLine.SetBounds(16, PanelToolbar.Height - 10, PanelToolbar.Width - 32, 2);
|
||
SepLine.Anchors := [akLeft, akRight, akBottom];
|
||
|
||
// ---- Status bar ----
|
||
StatusPanel := TMainStatusBar.Create(Self);
|
||
StatusPanel.Parent := Self;
|
||
|
||
// ---- Left panel ----
|
||
PanelLeft := TPanel.Create(Self);
|
||
PanelLeft.Parent := Self;
|
||
PanelLeft.Align := alLeft;
|
||
PanelLeft.Width := LEFT_W;
|
||
PanelLeft.BevelOuter := bvNone;
|
||
|
||
Y := 0;
|
||
|
||
// Compact top VFO group
|
||
PanelTopVfoGroup := TPanel.Create(Self);
|
||
PanelTopVfoGroup.Parent := PanelToolbar;
|
||
PanelTopVfoGroup.SetBounds(0, 0, TOP_VFO_W * 2 + 64, 60);
|
||
PanelTopVfoGroup.BevelOuter := bvNone;
|
||
PanelTopVfoGroup.Color := TColor($00181818);
|
||
|
||
// VFO A
|
||
PanelTopVfoA := TPanel.Create(Self);
|
||
PanelTopVfoA.Parent := PanelTopVfoGroup;
|
||
PanelTopVfoA.SetBounds(0, 0, TOP_VFO_W, 60);
|
||
PanelTopVfoA.BevelOuter := bvNone;
|
||
PanelTopVfoA.Color := CLR_PANEL;
|
||
|
||
PanelVfoA := TPanel.Create(Self);
|
||
PanelVfoA.Parent := PanelTopVfoA;
|
||
PanelVfoA.SetBounds(0, 0, TOP_VFO_W, TOP_VFO_FREQ_H);
|
||
PanelVfoA.BevelOuter := bvNone;
|
||
|
||
BtnTopVfoASelect := MakeBtn(PanelVfoA, 'A', 0, 5, 20, 18, BtnTopVfoASelectClick);
|
||
BtnTopVfoASelect.Font.Name := 'Courier New';
|
||
BtnTopVfoASelect.Font.Size := 7;
|
||
BtnTopVfoASelect.Font.Bold := True;
|
||
BtnTopVfoASelect.ClrNorm := TColor($00303030);
|
||
BtnTopVfoASelect.ClrHot := TColor($00404040);
|
||
BtnTopVfoASelect.ClrActive := TColor($00404040);
|
||
BtnTopVfoASelect.ClrBorder := TColor($00505050);
|
||
BtnTopVfoASelect.ClrText := CLR_TEXTDIM;
|
||
BtnTopVfoASelect.ClrTextAct := CLR_TEXT;
|
||
|
||
BtnTopVfoATX := MakeBtn(PanelVfoA, 'TX', 0, 25, 20, 18, BtnTopVfoATXClick);
|
||
BtnTopVfoATX.Font.Name := 'Courier New';
|
||
BtnTopVfoATX.Font.Size := 7;
|
||
BtnTopVfoATX.Font.Bold := True;
|
||
BtnTopVfoATX.ClrNorm := TColor($00303030);
|
||
BtnTopVfoATX.ClrHot := TColor($00404040);
|
||
BtnTopVfoATX.ClrActive := TColor($00404040);
|
||
BtnTopVfoATX.ClrBorder := TColor($00505050);
|
||
BtnTopVfoATX.ClrText := CLR_TEXTDIM;
|
||
BtnTopVfoATX.ClrTextAct := CLR_TEXT;
|
||
|
||
LblVfoALabel := TLabel.Create(Self);
|
||
LblVfoALabel.Parent := PanelVfoA;
|
||
LblVfoALabel.Caption := 'VFO-A';
|
||
LblVfoALabel.Left := 0; LblVfoALabel.Top := 0;
|
||
LblVfoALabel.Font.Name := 'Courier New'; LblVfoALabel.Font.Size := 7;
|
||
LblVfoALabel.Font.Color := CLR_TEXTDIM;
|
||
LblVfoALabel.Visible := False;
|
||
|
||
FreqDispA := TFreqDisplay.Create(Self);
|
||
FreqDispA.Parent := PanelVfoA;
|
||
FreqDispA.SetBounds(20, -2, TOP_VFO_W - 20, TOP_VFO_FREQ_H);
|
||
FreqDispA.FontSize := TOP_VFO_FONT;
|
||
FreqDispA.FontName := 'Courier New';
|
||
FreqDispA.Frequency := Round(FController.FVfoA);
|
||
FreqDispA.ColorNormal := CLR_FREQ;
|
||
FreqDispA.ColorHover := TColor($0040DDFF);
|
||
FreqDispA.ColorDim := CLR_TEXTDIM;
|
||
FreqDispA.MinMhzDigits := 3;
|
||
FreqDispA.DimLeadingZeros := True;
|
||
FreqDispA.CenterText := False;
|
||
FreqDispA.OnChange := FreqDispAChanged;
|
||
FreqDispA.OnClick := FreqDispAClick;
|
||
|
||
// VFO B
|
||
PanelTopVfoB := TPanel.Create(Self);
|
||
PanelTopVfoB.Parent := PanelTopVfoGroup;
|
||
PanelTopVfoB.SetBounds(0, 0, TOP_VFO_W, 60);
|
||
PanelTopVfoB.BevelOuter := bvNone;
|
||
PanelTopVfoB.Color := CLR_PANEL;
|
||
|
||
PanelVfoB := TPanel.Create(Self);
|
||
PanelVfoB.Parent := PanelTopVfoB;
|
||
PanelVfoB.SetBounds(0, 0, TOP_VFO_W, TOP_VFO_FREQ_H);
|
||
PanelVfoB.BevelOuter := bvNone;
|
||
|
||
BtnTopVfoBSelect := MakeBtn(PanelVfoB, 'B', 0, 5, 20, 18, BtnTopVfoBSelectClick);
|
||
BtnTopVfoBSelect.Font.Name := 'Courier New';
|
||
BtnTopVfoBSelect.Font.Size := 7;
|
||
BtnTopVfoBSelect.Font.Bold := True;
|
||
BtnTopVfoBSelect.ClrNorm := TColor($00303030);
|
||
BtnTopVfoBSelect.ClrHot := TColor($00404040);
|
||
BtnTopVfoBSelect.ClrActive := TColor($00404040);
|
||
BtnTopVfoBSelect.ClrBorder := TColor($00505050);
|
||
BtnTopVfoBSelect.ClrText := CLR_TEXTDIM;
|
||
BtnTopVfoBSelect.ClrTextAct := CLR_TEXT;
|
||
|
||
BtnTopVfoBTX := MakeBtn(PanelVfoB, 'TX', 0, 25, 20, 18, BtnTopVfoBTXClick);
|
||
BtnTopVfoBTX.Font.Name := 'Courier New';
|
||
BtnTopVfoBTX.Font.Size := 7;
|
||
BtnTopVfoBTX.Font.Bold := True;
|
||
BtnTopVfoBTX.ClrNorm := TColor($00303030);
|
||
BtnTopVfoBTX.ClrHot := TColor($00404040);
|
||
BtnTopVfoBTX.ClrActive := TColor($00404040);
|
||
BtnTopVfoBTX.ClrBorder := TColor($00505050);
|
||
BtnTopVfoBTX.ClrText := CLR_TEXTDIM;
|
||
BtnTopVfoBTX.ClrTextAct := CLR_TEXT;
|
||
|
||
LblVfoBLabel := TLabel.Create(Self);
|
||
LblVfoBLabel.Parent := PanelVfoB;
|
||
LblVfoBLabel.Caption := 'VFO-B';
|
||
LblVfoBLabel.Left := 0; LblVfoBLabel.Top := 0;
|
||
LblVfoBLabel.Font.Name := 'Courier New'; LblVfoBLabel.Font.Size := 7;
|
||
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
|
||
LblVfoBLabel.Visible := False;
|
||
|
||
FreqDispB := TFreqDisplay.Create(Self);
|
||
FreqDispB.Parent := PanelVfoB;
|
||
FreqDispB.SetBounds(20, -2, TOP_VFO_W - 20, TOP_VFO_FREQ_H);
|
||
FreqDispB.FontSize := TOP_VFO_FONT;
|
||
FreqDispB.FontName := 'Courier New';
|
||
FreqDispB.Frequency := Round(FController.FVfoB);
|
||
FreqDispB.ColorNormal := CLR_FREQ_DIM;
|
||
FreqDispB.ColorHover := TColor($0040DDFF);
|
||
FreqDispB.ColorDim := CLR_TEXTDIM;
|
||
FreqDispB.MinMhzDigits := 3;
|
||
FreqDispB.DimLeadingZeros := True;
|
||
FreqDispB.CenterText := False;
|
||
FreqDispB.OnChange := FreqDispBChanged;
|
||
FreqDispB.OnClick := FreqDispBClick;
|
||
|
||
// VFO transfer buttons — горизонтальный ряд по центру
|
||
PanelVfoButtons := TPanel.Create(Self);
|
||
PanelVfoButtons.Parent := PanelTopVfoGroup;
|
||
PanelVfoButtons.SetBounds(0, 0, 140, 20);
|
||
PanelVfoButtons.BevelOuter := bvNone;
|
||
PanelVfoButtons.Color := TColor($00181818);
|
||
|
||
BtnVfoACopyB := MakeBtn(PanelVfoButtons, 'A>B', 0, 0, 44, 20, BtnVfoACopyBClick);
|
||
BtnVfoSwap := MakeBtn(PanelVfoButtons, 'A<>B', 0, 0, 44, 20, BtnVfoSwapClick);
|
||
BtnVfoBCopyA := MakeBtn(PanelVfoButtons, 'B>A', 0, 0, 44, 20, BtnVfoBCopyAClick);
|
||
// Янтарный стиль, шрифт под размер кнопок
|
||
StyleSpanButton(BtnVfoACopyB, False);
|
||
StyleSpanButton(BtnVfoSwap, False);
|
||
StyleSpanButton(BtnVfoBCopyA, False);
|
||
BtnVfoACopyB.Font.Size := 7;
|
||
BtnVfoSwap.Font.Size := 7;
|
||
BtnVfoBCopyA.Font.Size := 7;
|
||
|
||
LayoutTopVfoBlock;
|
||
|
||
// TX controls
|
||
PanelTX := TPanel.Create(Self);
|
||
PanelTX.Parent := PanelLeft;
|
||
PanelTX.SetBounds(0, Y, LEFT_W, 96);
|
||
PanelTX.BevelOuter := bvNone;
|
||
MakeLbl(PanelTX, 'TX', 4, 2);
|
||
|
||
BtnMOX := MakeBtn(PanelTX, 'MOX',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 0), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 4, 0), BTN_SM, BtnMOXClick);
|
||
BtnTUN := MakeBtn(PanelTX, 'TUN',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 1), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 4, 1), BTN_SM, BtnTUNClick);
|
||
|
||
BtnDUP := MakeBtn(PanelTX, 'DUP',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 0), 44,
|
||
LeftPanelButtonWidth(LEFT_W, 4, 0), BTN_SM, BtnDUPClick);
|
||
BtnDUP.Tag := 0;
|
||
StyleButton(BtnDUP, FController.FDisplayDuplex);
|
||
|
||
LblDrv := TLabel.Create(Self);
|
||
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 77;
|
||
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
|
||
LblDrv.Font.Name := 'Courier New'; LblDrv.Font.Size := 7;
|
||
|
||
TrkDrive := TFlatSlider.Create(Self);
|
||
TrkDrive.Parent := PanelTX; TrkDrive.Left := 34; TrkDrive.Top := 74;
|
||
TrkDrive.Width := 120; TrkDrive.Height := 16;
|
||
TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50;
|
||
TrkDrive.OnChange := TrkDriveChange;
|
||
|
||
LblDrvVal := TLabel.Create(Self);
|
||
LblDrvVal.Parent := PanelTX;
|
||
LblDrvVal.Left := 158; LblDrvVal.Top := 77;
|
||
LblDrvVal.Font.Color := CLR_TEXT; LblDrvVal.Font.Name := 'Courier New';
|
||
LblDrvVal.Font.Size := 7;
|
||
UpdateDriveLabel;
|
||
|
||
Inc(Y, 100);
|
||
|
||
// RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE
|
||
PanelRXBlock := TPanel.Create(Self);
|
||
PanelRXBlock.Parent := PanelLeft;
|
||
PanelRXBlock.SetBounds(0, Y, LEFT_W, 96);
|
||
PanelRXBlock.BevelOuter := bvNone;
|
||
MakeLbl(PanelRXBlock, 'RX', 4, 2);
|
||
|
||
BtnCTun := MakeBtn(PanelRXBlock, 'CTUN',
|
||
LeftPanelButtonLeft(LEFT_W, 3, 0), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 3, 0),
|
||
BTN_H, BtnCTunClick);
|
||
BtnCTun.Tag := 0;
|
||
StyleButton(BtnCTun, FController.FCTun);
|
||
|
||
BtnChannels := MakeBtn(PanelRXBlock, 'Channel',
|
||
LeftPanelButtonLeft(LEFT_W, 3, 1), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 3, 1),
|
||
BTN_H, BtnChannelsClick);
|
||
BtnChannels.OnMouseDown := BtnChannelsMouseDown;
|
||
StyleButton(BtnChannels, False);
|
||
|
||
FChannelsDropDown := TFlatDropDown.Create(BtnChannels, PanelLeft, 1, BTN_SM);
|
||
FChannelsDropDown.OnSelect := OnChannelsDropDownSelect;
|
||
RefreshChannelsDropDown;
|
||
|
||
// QO-100 beacon lock — 3-я колонка ряда. Видима только в XVTR-режиме
|
||
// (см. рендер rfXvtr ниже). Подпись/подсветка обновляются по rfBeaconLock.
|
||
BtnBeacon := MakeBtn(PanelRXBlock, 'BEACON',
|
||
LeftPanelButtonLeft(LEFT_W, 3, 2), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 3, 2),
|
||
BTN_H, BtnBeaconClick);
|
||
BtnBeacon.Visible := False;
|
||
BtnBeacon.OnMouseDown := BtnBeaconMouseDown; // ПКМ → декодер + окно констелляции
|
||
StyleButton(BtnBeacon, False);
|
||
|
||
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
||
TrkVolume := TFlatSlider.Create(Self);
|
||
TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46;
|
||
TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 16;
|
||
TrkVolume.Min := 0; TrkVolume.Max := 100;
|
||
TrkVolume.Position := FController.FVolume;
|
||
TrkVolume.OnChange := TrkVolumeChange;
|
||
|
||
BtnNR := MakeBtn(PanelRXBlock, 'NR',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 0), 70,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 0), BTN_SM, BtnNRClick);
|
||
BtnNB := MakeBtn(PanelRXBlock, 'NB',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 1), 70,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 1), BTN_SM, BtnNBClick);
|
||
BtnSNB := MakeBtn(PanelRXBlock, 'SNB',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 2), 70,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 2), BTN_SM, BtnSNBClick);
|
||
BtnANF := MakeBtn(PanelRXBlock, 'ANF',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 3), 70,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 3), BTN_SM, BtnANFClick);
|
||
BtnMute := MakeBtn(PanelRXBlock, 'MUTE',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 4), 70,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnMuteClick);
|
||
|
||
Inc(Y, 100);
|
||
|
||
// AGC controls
|
||
PanelAGC := TPanel.Create(Self);
|
||
PanelAGC.Parent := PanelLeft;
|
||
PanelAGC.SetBounds(0, Y, LEFT_W, 70);
|
||
PanelAGC.BevelOuter := bvNone;
|
||
|
||
// AGC mode — 5 кнопок в ряд
|
||
MakeLbl(PanelAGC, 'AGC', 4, 2);
|
||
BtnAGCMode[0] := MakeBtn(PanelAGC, 'FAST',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 0), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 0),
|
||
BTN_SM, BtnAGCModeClick);
|
||
BtnAGCMode[0].Tag := 0;
|
||
BtnAGCMode[1] := MakeBtn(PanelAGC, 'MED',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 1), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 1),
|
||
BTN_SM, BtnAGCModeClick);
|
||
BtnAGCMode[1].Tag := 1;
|
||
BtnAGCMode[2] := MakeBtn(PanelAGC, 'SLOW',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 2), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 2),
|
||
BTN_SM, BtnAGCModeClick);
|
||
BtnAGCMode[2].Tag := 2;
|
||
BtnAGCMode[3] := MakeBtn(PanelAGC, 'LONG',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 3), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 3),
|
||
BTN_SM, BtnAGCModeClick);
|
||
BtnAGCMode[3].Tag := 3;
|
||
BtnAGCMode[4] := MakeBtn(PanelAGC, 'OFF',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 4), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 4),
|
||
BTN_SM, BtnAGCModeClick);
|
||
BtnAGCMode[4].Tag := 4;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
|
||
|
||
// AGC level slider
|
||
MakeLbl(PanelAGC, 'THRESH', 4, 44);
|
||
TrkAGC := TFlatSlider.Create(Self);
|
||
TrkAGC.Parent := PanelAGC; TrkAGC.Left := 56; TrkAGC.Top := 40;
|
||
TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 16;
|
||
TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → −20..-120 dBm
|
||
TrkAGC.Position := FController.FAGCTop;
|
||
TrkAGC.Reversed := True;
|
||
TrkAGC.OnChange := TrkAGCChange;
|
||
|
||
LblAGCTop := TLabel.Create(Self);
|
||
LblAGCTop.Parent := PanelAGC;
|
||
LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 44;
|
||
LblAGCTop.Caption := Format('-%ddB', [FController.FAGCTop]);
|
||
LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New';
|
||
LblAGCTop.Font.Size := 7;
|
||
|
||
Inc(Y, 74);
|
||
|
||
// --- Pluto RF AGC/gain — отдельный блок ПОД блоком AGC (WDSP). Виден только
|
||
// когда подключён Pluto-бэкенд; раскладку панелей ниже сдвигает LayoutLeftPanel.
|
||
PanelPlutoAGC := TPanel.Create(Self);
|
||
PanelPlutoAGC.Parent := PanelLeft;
|
||
PanelPlutoAGC.SetBounds(0, Y, LEFT_W, PLUTO_AGC_H);
|
||
PanelPlutoAGC.BevelOuter := bvNone;
|
||
PanelPlutoAGC.Visible := False;
|
||
MakeLbl(PanelPlutoAGC, 'RF AGC', 4, 2);
|
||
// Режимы AD936x: FAST/SLOW/HYB/MAN. Tag = индекс режима (1/2/3/0).
|
||
BtnPlutoGain[0] := MakeBtn(PanelPlutoAGC, 'FAST',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 0), 16, LeftPanelButtonWidth(LEFT_W, 4, 0),
|
||
BTN_SM, BtnPlutoGainClick);
|
||
BtnPlutoGain[0].Tag := 1;
|
||
BtnPlutoGain[1] := MakeBtn(PanelPlutoAGC, 'SLOW',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 1), 16, LeftPanelButtonWidth(LEFT_W, 4, 1),
|
||
BTN_SM, BtnPlutoGainClick);
|
||
BtnPlutoGain[1].Tag := 2;
|
||
BtnPlutoGain[2] := MakeBtn(PanelPlutoAGC, 'HYB',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 2), 16, LeftPanelButtonWidth(LEFT_W, 4, 2),
|
||
BTN_SM, BtnPlutoGainClick);
|
||
BtnPlutoGain[2].Tag := 3;
|
||
BtnPlutoGain[3] := MakeBtn(PanelPlutoAGC, 'MAN',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 3), 16, LeftPanelButtonWidth(LEFT_W, 4, 3),
|
||
BTN_SM, BtnPlutoGainClick);
|
||
BtnPlutoGain[3].Tag := 0;
|
||
// Manual gain slider.
|
||
MakeLbl(PanelPlutoAGC, 'GAIN', 4, 44);
|
||
TrkRxGain := TFlatSlider.Create(Self);
|
||
TrkRxGain.Parent := PanelPlutoAGC; TrkRxGain.Left := 56; TrkRxGain.Top := 40;
|
||
TrkRxGain.Width := LEFT_W - 96; TrkRxGain.Height := 16;
|
||
TrkRxGain.Min := 0; TrkRxGain.Max := 73; // AD936x hardwaregain dB
|
||
TrkRxGain.Position := FController.FRxGainDb;
|
||
TrkRxGain.OnChange := TrkRxGainChange;
|
||
LblRxGain := TLabel.Create(Self);
|
||
LblRxGain.Parent := PanelPlutoAGC;
|
||
LblRxGain.Left := LEFT_W - 38; LblRxGain.Top := 44;
|
||
LblRxGain.Caption := Format('%ddB', [FController.FRxGainDb]);
|
||
LblRxGain.Font.Color := CLR_TEXT; LblRxGain.Font.Name := 'Courier New';
|
||
LblRxGain.Font.Size := 7;
|
||
// НЕ Inc(Y): панель скрыта и наложена на позицию Bands; LayoutLeftPanel сдвинет.
|
||
|
||
// Band selector. Высота резервирует 2 дополнительных ряда под XVTR-кнопки
|
||
// (создаются динамически в RebuildXvtrButtons). В каждом ряду до 6 кнопок,
|
||
// итого до 12 XVTR-кнопок (наш CFG_XVTR_COUNT = 8 умещается с запасом).
|
||
PanelBands := TPanel.Create(Self);
|
||
PanelBands.Parent := PanelLeft;
|
||
PanelBands.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelBands.BevelOuter := bvNone;
|
||
MakeLbl(PanelBands, 'BAND', 4, 2);
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelBands, BAND_NAMES[i],
|
||
LeftPanelButtonLeft(LEFT_W, 6, i mod 6),
|
||
16 + (i div 6) * 27,
|
||
LeftPanelButtonWidth(LEFT_W, 6, i mod 6),
|
||
BTN_SM, BtnBandClick);
|
||
B.Tag := i;
|
||
BtnBand[i] := B;
|
||
end;
|
||
// XVTR кнопки создаются позже (в RebuildXvtrButtons), их Tag = -(idx+1)
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
BtnXvtrBand[i] := nil;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// Mode selector
|
||
PanelMode := TPanel.Create(Self);
|
||
PanelMode.Parent := PanelLeft;
|
||
PanelMode.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelMode.BevelOuter := bvNone;
|
||
MakeLbl(PanelMode, 'MODE', 4, 2);
|
||
for i := 0 to MODE_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelMode, MODE_NAMES[i],
|
||
LeftPanelButtonLeft(LEFT_W, 4, i mod 4),
|
||
16 + (i div 4) * 27,
|
||
LeftPanelButtonWidth(LEFT_W, 4, i mod 4),
|
||
BTN_SM, BtnModeClick);
|
||
B.Tag := i;
|
||
BtnMode[i] := B;
|
||
StyleButton(B, i = FController.FMode);
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// Filter selector (10 кнопок в 2 ряда по 5)
|
||
PanelFilter := TPanel.Create(Self);
|
||
PanelFilter.Parent := PanelLeft;
|
||
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelFilter.BevelOuter := bvNone;
|
||
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
||
for i := 0 to FILT_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelFilter, '---',
|
||
LeftPanelButtonLeft(LEFT_W, 5, i mod 5),
|
||
16 + (i div 5) * 27,
|
||
LeftPanelButtonWidth(LEFT_W, 5, i mod 5),
|
||
BTN_SM, BtnFilterClick);
|
||
B.Tag := i;
|
||
BtnFilter[i] := B;
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// FM Squelch panel (visible only in FM mode, above CTCSS)
|
||
PanelFMSQ := TPanel.Create(Self);
|
||
PanelFMSQ.Parent := PanelLeft;
|
||
PanelFMSQ.SetBounds(0, Y, LEFT_W, 50);
|
||
PanelFMSQ.BevelOuter := bvNone;
|
||
PanelFMSQ.Visible := False;
|
||
MakeLbl(PanelFMSQ, 'SQL', 4, 2);
|
||
W := LeftPanelButtonWidth(LEFT_W, 4, 0);
|
||
BtnFMSQ := MakeBtn(PanelFMSQ, 'SQL',
|
||
LeftPanelButtonLeft(LEFT_W, 4, 0), 16,
|
||
W, BTN_SM, BtnFMSQClick);
|
||
StyleButton(BtnFMSQ, False);
|
||
// Slider: vertically centred with button text (button top=16, h=24 → center=28)
|
||
// Reserve 28px on the right for the numeric label
|
||
TrkFMSQ := TFlatSlider.Create(Self);
|
||
TrkFMSQ.Parent := PanelFMSQ;
|
||
TrkFMSQ.Left := BtnFMSQ.Left + BtnFMSQ.Width + 3;
|
||
TrkFMSQ.Top := 21; // 28 - 14/2 = 21
|
||
TrkFMSQ.Width := LEFT_W - TrkFMSQ.Left - 30 - 4; // leave 30px for label + margins
|
||
TrkFMSQ.Height := 14;
|
||
TrkFMSQ.Min := 0; TrkFMSQ.Max := 100;
|
||
TrkFMSQ.Position := FController.FFMSQLevel;
|
||
TrkFMSQ.OnChange := TrkFMSQChange;
|
||
// Numeric value label to the right of the slider
|
||
LblFMSQ := TLabel.Create(Self);
|
||
LblFMSQ.Parent := PanelFMSQ;
|
||
LblFMSQ.Left := TrkFMSQ.Left + TrkFMSQ.Width + 3;
|
||
LblFMSQ.Top := 21;
|
||
LblFMSQ.Width := 26;
|
||
LblFMSQ.Caption := IntToStr(FController.FFMSQLevel);
|
||
LblFMSQ.Font.Color := CLR_TEXT;
|
||
LblFMSQ.Font.Name := 'Courier New';
|
||
LblFMSQ.Font.Size := 8;
|
||
|
||
// FM CTCSS panel (visible only when FM mode is active)
|
||
PanelFMCTCSS := TPanel.Create(Self);
|
||
PanelFMCTCSS.Parent := PanelLeft;
|
||
PanelFMCTCSS.SetBounds(0, Y, LEFT_W, 50);
|
||
PanelFMCTCSS.BevelOuter := bvNone;
|
||
PanelFMCTCSS.Visible := False;
|
||
MakeLbl(PanelFMCTCSS, 'CTCSS', 4, 2);
|
||
BtnFMCTCSS := MakeBtn(PanelFMCTCSS, 'CTCSS',
|
||
LeftPanelButtonLeft(LEFT_W, 3, 0), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 3, 0),
|
||
BTN_H, BtnFMCTCSSClick);
|
||
StyleButton(BtnFMCTCSS, False);
|
||
BtnFMCTCSSTone := MakeBtn(PanelFMCTCSS, CTCSS_NAMES[0],
|
||
LeftPanelButtonLeft(LEFT_W, 3, 1), 16,
|
||
LEFT_W - LEFT_PANEL_BTN_PAD -
|
||
LeftPanelButtonLeft(LEFT_W, 3, 1),
|
||
BTN_H, BtnFMCTCSSToneClick);
|
||
StyleButton(BtnFMCTCSSTone, False);
|
||
|
||
FCTCSSDropDown := TFlatDropDown.Create(BtnFMCTCSSTone, PanelLeft, 4, BTN_SM);
|
||
FCTCSSDropDown.SetItems(CTCSS_NAMES);
|
||
FCTCSSDropDown.SetItemIndex(FController.FFMCTCSSToneIdx);
|
||
FCTCSSDropDown.OnSelect := OnCTCSSDropDownSelect;
|
||
|
||
// FM Step panel (visible only in FM mode, between Filter and SQL)
|
||
PanelFMStep := TPanel.Create(Self);
|
||
PanelFMStep.Parent := PanelLeft;
|
||
PanelFMStep.SetBounds(0, Y, LEFT_W, 50);
|
||
PanelFMStep.BevelOuter := bvNone;
|
||
PanelFMStep.Visible := False;
|
||
MakeLbl(PanelFMStep, 'STEP', 4, 2);
|
||
BtnFMStep := MakeBtn(PanelFMStep, 'STEP',
|
||
LeftPanelButtonLeft(LEFT_W, 3, 0), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 3, 0),
|
||
BTN_H, BtnFMStepClick);
|
||
StyleButton(BtnFMStep, FController.FFMStepOn);
|
||
BtnFMStepSel := MakeBtn(PanelFMStep, FM_STEP_NAMES[FController.FFMStepIdx],
|
||
LeftPanelButtonLeft(LEFT_W, 3, 1), 16,
|
||
LEFT_W - LEFT_PANEL_BTN_PAD -
|
||
LeftPanelButtonLeft(LEFT_W, 3, 1),
|
||
BTN_H, BtnFMStepSelClick);
|
||
StyleButton(BtnFMStepSel, False);
|
||
|
||
FStepDropDown := TFlatDropDown.Create(BtnFMStepSel, PanelLeft, FM_STEP_COUNT, BTN_SM);
|
||
FStepDropDown.SetItems(FM_STEP_NAMES);
|
||
FStepDropDown.SetItemIndex(FController.FFMStepIdx);
|
||
FStepDropDown.OnSelect := OnStepDropDownSelect;
|
||
|
||
// FM Repeater offset panel (visible only in FM mode, below CTCSS)
|
||
PanelFMRpt := TPanel.Create(Self);
|
||
PanelFMRpt.Parent := PanelLeft;
|
||
PanelFMRpt.SetBounds(0, Y, LEFT_W, 50);
|
||
PanelFMRpt.BevelOuter := bvNone;
|
||
PanelFMRpt.Visible := False;
|
||
MakeLbl(PanelFMRpt, 'RPT', 4, 2);
|
||
BtnFMRptMinus := MakeBtn(PanelFMRpt, '-',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 0), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 0), BTN_H, BtnFMRptMinusClick);
|
||
StyleButton(BtnFMRptMinus, False);
|
||
BtnFMRptPlus := MakeBtn(PanelFMRpt, '+',
|
||
LeftPanelButtonLeft(LEFT_W, 5, 4), 16,
|
||
LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_H, BtnFMRptPlusClick);
|
||
StyleButton(BtnFMRptPlus, False);
|
||
W := LeftPanelButtonLeft(LEFT_W, 5, 1);
|
||
EdFMRptOffset := TFlatEdit.Create(Self);
|
||
EdFMRptOffset.Parent := PanelFMRpt;
|
||
EdFMRptOffset.Left := W;
|
||
EdFMRptOffset.Top := 18;
|
||
EdFMRptOffset.Width := LeftPanelButtonLeft(LEFT_W, 5, 4) - W - LEFT_PANEL_BTN_GAP;
|
||
EdFMRptOffset.Height := BTN_H - 4;
|
||
EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
|
||
EdFMRptOffset.OnChange := EdFMRptOffsetChange;
|
||
|
||
// ---- Right panel ----
|
||
PanelRight := TPanel.Create(Self);
|
||
PanelRight.Parent := Self;
|
||
PanelRight.Align := alClient;
|
||
PanelRight.BevelOuter := bvNone;
|
||
PanelRight.DoubleBuffered := True;
|
||
PanelRight.OnResize := RightPanelResize;
|
||
|
||
FSampleRateOverlay := TSampleRateOverlay.Create(Self);
|
||
FSampleRateOverlay.OnSpanSelect := OnSampleRateSelect;
|
||
FSampleRateOverlay.OnHidePanel := OnSampleRateHidePanel;
|
||
FSampleRateOverlay.OnInvalidate := OnSampleRateOverlayInvalidate;
|
||
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
|
||
FSpecView.SampleRateOverlay := FSampleRateOverlay;
|
||
|
||
// S-метр — внутри PanelToolbar, справа
|
||
PanelSMeterRight := TPanel.Create(Self);
|
||
PanelSMeterRight.Parent := PanelToolbar;
|
||
PanelSMeterRight.BevelOuter := bvNone;
|
||
PanelSMeterRight.Color := CLR_PANEL;
|
||
PbSMeterRight := TPaintBox.Create(Self);
|
||
PbSMeterRight.Parent := PanelSMeterRight;
|
||
PbSMeterRight.Align := alClient;
|
||
PbSMeterRight.OnPaint := FSpecView.PaintSMeterRight;
|
||
PbSMeterRight.Color := CLR_PANEL;
|
||
|
||
if FUseOpenGLSpectrum then
|
||
begin
|
||
PbWideband := TOpenGLControl.Create(Self);
|
||
TOpenGLControl(PbWideband).AutoResizeViewport := False;
|
||
TOpenGLControl(PbWideband).DoubleBuffered := True;
|
||
TOpenGLControl(PbWideband).OnPaint := FWidebandView.Paint;
|
||
TOpenGLControl(PbWideband).OnMouseDown := PbWidebandMouseDown;
|
||
end
|
||
else
|
||
begin
|
||
PbWideband := TPaintBox.Create(Self);
|
||
TPaintBox(PbWideband).OnPaint := FWidebandView.Paint;
|
||
TPaintBox(PbWideband).OnMouseDown := PbWidebandMouseDown;
|
||
end;
|
||
PbWideband.Parent := PanelRight;
|
||
PbWideband.Visible := False;
|
||
|
||
PbWidebandRuler := TPaintBox.Create(Self);
|
||
PbWidebandRuler.Parent := PanelRight;
|
||
PbWidebandRuler.OnPaint := FWidebandView.PaintRuler;
|
||
PbWidebandRuler.Cursor := crDefault;
|
||
PbWidebandRuler.Visible := False;
|
||
|
||
if FUseOpenGLSpectrum then
|
||
begin
|
||
PbSpectrum := TOpenGLControl.Create(Self);
|
||
TOpenGLControl(PbSpectrum).AutoResizeViewport := False;
|
||
TOpenGLControl(PbSpectrum).DoubleBuffered := True;
|
||
TOpenGLControl(PbSpectrum).OnPaint := FSpecView.PaintSpectrum;
|
||
TOpenGLControl(PbSpectrum).OnMouseDown := PbSpectrumMouseDown;
|
||
TOpenGLControl(PbSpectrum).OnDblClick := PbSpectrumDblClick;
|
||
TOpenGLControl(PbSpectrum).OnMouseMove := PbSpectrumMouseMove;
|
||
TOpenGLControl(PbSpectrum).OnMouseUp := PbSpectrumMouseUp;
|
||
TOpenGLControl(PbSpectrum).OnMouseLeave := PbSpectrumMouseLeave;
|
||
TSpectrumViewOpenGL(FSpecView).AttachControl(TOpenGLControl(PbSpectrum));
|
||
end
|
||
else
|
||
begin
|
||
PbSpectrum := TPaintBox.Create(Self);
|
||
TPaintBox(PbSpectrum).OnPaint := FSpecView.PaintSpectrum;
|
||
TPaintBox(PbSpectrum).OnMouseDown := PbSpectrumMouseDown;
|
||
TPaintBox(PbSpectrum).OnDblClick := PbSpectrumDblClick;
|
||
TPaintBox(PbSpectrum).OnMouseMove := PbSpectrumMouseMove;
|
||
TPaintBox(PbSpectrum).OnMouseUp := PbSpectrumMouseUp;
|
||
TPaintBox(PbSpectrum).OnMouseLeave := PbSpectrumMouseLeave;
|
||
end;
|
||
PbSpectrum.Parent := PanelRight;
|
||
|
||
PbRuler := TPaintBox.Create(Self);
|
||
PbRuler.Parent := PanelRight;
|
||
PbRuler.OnPaint := FSpecView.PaintRuler;
|
||
PbRuler.Cursor := crDefault;
|
||
FSpecView.PbRuler := PbRuler;
|
||
|
||
// ---- Splitter между спектром+линейкой и водопадом ----
|
||
PanelSplitter := TPanel.Create(Self);
|
||
PanelSplitter.Parent := PanelRight;
|
||
PanelSplitter.BevelOuter := bvNone;
|
||
PanelSplitter.Color := TColor($00303030);
|
||
PanelSplitter.Cursor := crVSplit;
|
||
PanelSplitter.Height := 5;
|
||
PanelSplitter.OnMouseDown := SplitterMouseDown;
|
||
PanelSplitter.OnMouseMove := SplitterMouseMove;
|
||
PanelSplitter.OnMouseUp := SplitterMouseUp;
|
||
|
||
if FUseOpenGLSpectrum then
|
||
begin
|
||
PbWaterfall := TOpenGLControl.Create(Self);
|
||
TOpenGLControl(PbWaterfall).AutoResizeViewport := False;
|
||
TOpenGLControl(PbWaterfall).DoubleBuffered := True;
|
||
TOpenGLControl(PbWaterfall).OnPaint := FSpecView.PaintWaterfall;
|
||
TOpenGLControl(PbWaterfall).OnMouseDown := PbWaterfallMouseDown;
|
||
TOpenGLControl(PbWaterfall).OnMouseMove := PbWaterfallMouseMove;
|
||
TOpenGLControl(PbWaterfall).OnMouseUp := PbWaterfallMouseUp;
|
||
TSpectrumViewOpenGL(FSpecView).AttachWaterfallControl(TOpenGLControl(PbWaterfall));
|
||
end
|
||
else
|
||
begin
|
||
PbWaterfall := TPaintBox.Create(Self);
|
||
TPaintBox(PbWaterfall).OnPaint := FSpecView.PaintWaterfall;
|
||
TPaintBox(PbWaterfall).OnMouseDown := PbWaterfallMouseDown;
|
||
TPaintBox(PbWaterfall).OnMouseMove := PbWaterfallMouseMove;
|
||
TPaintBox(PbWaterfall).OnMouseUp := PbWaterfallMouseUp;
|
||
end;
|
||
PbWaterfall.Parent := PanelRight;
|
||
|
||
// Initial layout
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Splitter — перетаскивание границы спектр/водопад
|
||
// Обновление размеров происходит только при отпускании мыши (MouseUp),
|
||
// чтобы исключить фризы во время перетаскивания.
|
||
// Во время перетаскивания рисуем только призрак-линию на PanelSplitter.
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
begin
|
||
if Button <> mbLeft then Exit;
|
||
FSplitterDrag := True;
|
||
FSplitterSH0 := FSpectrumHeight;
|
||
// Y в координатах PanelRight
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
FSplitterDragY0 := P.Y;
|
||
PanelSplitter.Color := TColor($005050A0); // подсветка при захвате
|
||
{$IFDEF WINDOWS}
|
||
SetCapture(PanelSplitter.Handle);
|
||
{$ENDIF}
|
||
end;
|
||
|
||
procedure TMainForm.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
DeltaY: Integer;
|
||
NewSH: Integer;
|
||
TopOff, AvailH: Integer;
|
||
RULER_H: Integer;
|
||
const
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60;
|
||
MIN_WH = 40;
|
||
begin
|
||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||
if not FSplitterDrag then Exit;
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
DeltaY := P.Y - FSplitterDragY0;
|
||
|
||
TopOff := 0;
|
||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH <= 0 then Exit;
|
||
|
||
NewSH := FSplitterSH0 + DeltaY;
|
||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||
|
||
// Только двигаем сплиттер визуально — без пересчёта битмапов
|
||
PanelSplitter.Top := TopOff + NewSH + RULER_H;
|
||
PbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H;
|
||
PbWaterfall.Height := AvailH - NewSH;
|
||
end;
|
||
|
||
procedure TMainForm.SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
DeltaY: Integer;
|
||
NewSH: Integer;
|
||
TopOff, AvailH: Integer;
|
||
RULER_H: Integer;
|
||
const
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60;
|
||
MIN_WH = 40;
|
||
begin
|
||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||
if not FSplitterDrag then Exit;
|
||
FSplitterDrag := False;
|
||
{$IFDEF WINDOWS}
|
||
ReleaseCapture;
|
||
{$ENDIF}
|
||
PanelSplitter.Color := TColor($00303030); // обычный цвет
|
||
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
DeltaY := P.Y - FSplitterDragY0;
|
||
|
||
TopOff := 0;
|
||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH <= 0 then Exit;
|
||
|
||
NewSH := FSplitterSH0 + DeltaY;
|
||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||
|
||
// Сохраняем новое соотношение и делаем полный пересчёт с перерисовкой
|
||
FSplitterRatio := NewSH / AvailH;
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Resize handler (normal method instead of anonymous procedure)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.RightPanelResize(Sender: TObject);
|
||
begin
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
procedure TMainForm.LayoutTopVfoBlock;
|
||
const
|
||
MARGIN = 4;
|
||
TOP_OFFSET = 2;
|
||
TOP_VFO_FREQ_H = 47;
|
||
VFO_BTN_FREQ_GAP = 4;
|
||
VFO_FREQ_LEFT_PAD = 2;
|
||
VFO_FREQ_RIGHT_PAD = 8;
|
||
XFER_BTN_W = 44;
|
||
XFER_BTN_H = 20;
|
||
XFER_BTN_GAP = 4;
|
||
XFER_PANEL_W = XFER_BTN_W * 3 + XFER_BTN_GAP * 2; // = 140
|
||
XFER_SIDE_GAP = 14; // минимальный зазор между VFO и кнопками трансфера
|
||
SETTINGS_GAP = 8;
|
||
MAX_SPREAD = 100; // максимальный дополнительный отступ с каждой стороны при растяжении
|
||
// VFO-B получает левый отступ = правому паддингу VFO-A для симметрии зазоров
|
||
VFO_B_LEFT_PAD = VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD; // = 10
|
||
var
|
||
BlockH, FreqTop, CenterY, SafeLeft, SafeRight, GroupLeft, GroupTop: Integer;
|
||
RightW, SMW, SMLeft, AvailW, MinGroupW, ExtraPerSide, DynGap: Integer;
|
||
TopVfoAW, TopVfoBW, NeedFreqAW, NeedFreqBW, FreqTextW, GroupW: Integer;
|
||
XferX, XferY: Integer;
|
||
begin
|
||
if (PanelToolbar = nil) or (PanelTopVfoGroup = nil) or
|
||
(PanelTopVfoA = nil) or (PanelTopVfoB = nil) then Exit;
|
||
|
||
Canvas.Font.Name := FreqDispB.FontName;
|
||
Canvas.Font.Size := FreqDispB.FontSize;
|
||
Canvas.Font.Style := [fsBold];
|
||
FreqDispA.Left := BtnTopVfoASelect.Width + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
|
||
FreqDispB.Left := VFO_B_LEFT_PAD + BtnTopVfoBSelect.Width + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
|
||
FreqTextW := Canvas.TextWidth(StringOfChar('0', Max(3, FFreqMhzDigits)) + '.000.000');
|
||
NeedFreqAW := FreqDispA.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD;
|
||
NeedFreqBW := FreqDispB.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD;
|
||
TopVfoAW := NeedFreqAW;
|
||
TopVfoBW := NeedFreqBW;
|
||
|
||
// Минимальная ширина компактной группы
|
||
MinGroupW := TopVfoAW + XFER_SIDE_GAP + XFER_PANEL_W + XFER_SIDE_GAP + TopVfoBW;
|
||
|
||
SafeLeft := MARGIN;
|
||
if BtnSettings <> nil then
|
||
SafeLeft := BtnSettings.Left + BtnSettings.Width + SETTINGS_GAP;
|
||
SafeRight := ClientWidth - MARGIN;
|
||
if PanelLeft <> nil then
|
||
begin
|
||
RightW := ClientWidth - PanelLeft.Width;
|
||
if RightW > 200 then
|
||
begin
|
||
SMW := Round(RightW * SMETER_WIDTH_RATIO);
|
||
SMLeft := ClientWidth - SMW - SMETER_MARGIN;
|
||
SafeRight := Min(SafeRight, SMLeft - TOP_SMETER_GAP);
|
||
end;
|
||
end;
|
||
|
||
AvailW := SafeRight - SafeLeft;
|
||
if AvailW < MinGroupW then
|
||
begin
|
||
PanelTopVfoGroup.Visible := False;
|
||
Exit;
|
||
end;
|
||
|
||
// Динамический зазор: растёт с шириной окна, но не больше MAX_SPREAD
|
||
ExtraPerSide := Min((AvailW - MinGroupW) div 2, MAX_SPREAD);
|
||
DynGap := XFER_SIDE_GAP + ExtraPerSide;
|
||
GroupW := TopVfoAW + DynGap + XFER_PANEL_W + DynGap + TopVfoBW;
|
||
|
||
BlockH := PanelToolbar.Height - (MARGIN * 2) - 8;
|
||
if BlockH < 56 then BlockH := 56;
|
||
|
||
// Центрируем группу в доступном пространстве
|
||
GroupLeft := SafeLeft + (AvailW - GroupW) div 2;
|
||
GroupTop := TOP_OFFSET;
|
||
|
||
CenterY := PanelToolbar.Height div 2 - GroupTop;
|
||
|
||
PanelTopVfoGroup.SetBounds(GroupLeft, GroupTop, GroupW, BlockH);
|
||
PanelTopVfoGroup.Visible := True;
|
||
|
||
// VFO-A: левый край группы
|
||
PanelTopVfoA.SetBounds(0, 0, TopVfoAW, BlockH);
|
||
PanelTopVfoA.Visible := True;
|
||
PanelVfoA.SetBounds(0, 0, TopVfoAW, BlockH);
|
||
// A выше оси, TX ниже; 1px зазор с каждой стороны
|
||
BtnTopVfoASelect.SetBounds(0, CenterY - BtnTopVfoASelect.Height - 1,
|
||
BtnTopVfoASelect.Width, BtnTopVfoASelect.Height);
|
||
BtnTopVfoATX.SetBounds(0, CenterY + 1,
|
||
BtnTopVfoATX.Width, BtnTopVfoATX.Height);
|
||
FreqTop := CenterY - (TOP_VFO_FREQ_H div 2);
|
||
FreqDispA.SetBounds(FreqDispA.Left, FreqTop,
|
||
FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H);
|
||
|
||
// Кнопки трансфера: горизонтальный ряд, вертикально по оси
|
||
XferX := TopVfoAW + DynGap;
|
||
XferY := CenterY - (XFER_BTN_H div 2);
|
||
PanelVfoButtons.SetBounds(XferX, XferY, XFER_PANEL_W, XFER_BTN_H);
|
||
BtnVfoACopyB.SetBounds(0, 0, XFER_BTN_W, XFER_BTN_H);
|
||
BtnVfoSwap.SetBounds(XFER_BTN_W + XFER_BTN_GAP, 0, XFER_BTN_W, XFER_BTN_H);
|
||
BtnVfoBCopyA.SetBounds((XFER_BTN_W + XFER_BTN_GAP) * 2, 0, XFER_BTN_W, XFER_BTN_H);
|
||
|
||
// VFO-B: правый край группы
|
||
PanelTopVfoB.SetBounds(TopVfoAW + DynGap + XFER_PANEL_W + DynGap, 0, TopVfoBW, BlockH);
|
||
PanelTopVfoB.Visible := True;
|
||
PanelVfoB.SetBounds(0, 0, TopVfoBW, BlockH);
|
||
BtnTopVfoBSelect.SetBounds(VFO_B_LEFT_PAD, CenterY - BtnTopVfoBSelect.Height - 1,
|
||
BtnTopVfoBSelect.Width, BtnTopVfoBSelect.Height);
|
||
BtnTopVfoBTX.SetBounds(VFO_B_LEFT_PAD, CenterY + 1,
|
||
BtnTopVfoBTX.Width, BtnTopVfoBTX.Height);
|
||
FreqTop := CenterY - (TOP_VFO_FREQ_H div 2);
|
||
FreqDispB.SetBounds(FreqDispB.Left, FreqTop,
|
||
FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H);
|
||
|
||
end;
|
||
|
||
procedure TMainForm.ResizeSMeter;
|
||
// PanelSMeterRight на главной форме (Parent=Self).
|
||
// По Y: от самого верха формы до низа тулбара.
|
||
var
|
||
FW: Integer;
|
||
RightW: Integer; // ширина PanelRight
|
||
SMW: Integer; // ширина S-метра
|
||
SMLeft: Integer;
|
||
SMTop: Integer;
|
||
SMBot: Integer;
|
||
begin
|
||
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or (PanelLeft = nil) then Exit;
|
||
|
||
LayoutTopVfoBlock;
|
||
|
||
FW := ClientWidth;
|
||
// Ширина зоны спектра (PanelRight)
|
||
RightW := FW - PanelLeft.Width;
|
||
if RightW < 200 then Exit;
|
||
|
||
// Ширина S-метра = 32% от PanelRight
|
||
SMW := Round(RightW * SMETER_WIDTH_RATIO);
|
||
|
||
// X: прижат к правому краю PanelToolbar
|
||
SMLeft := FW - SMW - SMETER_MARGIN;
|
||
|
||
SMTop := SMETER_MARGIN;
|
||
SMBot := PanelToolbar.Height - SMETER_MARGIN - 9;
|
||
|
||
PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
|
||
end;
|
||
|
||
function TMainForm.UpdateWidebandFrequencyView: Boolean;
|
||
var
|
||
E: TXvtrEntry;
|
||
SrcStart, SrcEnd: Double;
|
||
MarkerChanged, ViewChanged: Boolean;
|
||
begin
|
||
Result := False;
|
||
if FWidebandView = nil then Exit;
|
||
if FController.FActiveVfo = 0 then
|
||
MarkerChanged := FWidebandView.SetMarkerHz(FController.FVfoA)
|
||
else
|
||
MarkerChanged := FWidebandView.SetMarkerHz(FController.FVfoB);
|
||
if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then
|
||
begin
|
||
E := FController.FXvtrSettings.Entries[FController.FCurrentXvtr];
|
||
SrcStart := E.FreqBegin - E.LOOffset + E.LOError;
|
||
SrcEnd := E.FreqEnd - E.LOOffset + E.LOError;
|
||
ViewChanged := FWidebandView.SetFrequencyView(E.FreqBegin, E.FreqEnd,
|
||
SrcStart, SrcEnd);
|
||
end
|
||
else
|
||
ViewChanged := FWidebandView.SetFrequencyView(0.0, 61440000.0, 0.0,
|
||
61440000.0);
|
||
Result := MarkerChanged or ViewChanged;
|
||
end;
|
||
|
||
procedure TMainForm.ResizeSpectrumPanels;
|
||
const
|
||
SPLITTER_H = 5;
|
||
WB_H = 118;
|
||
MIN_SH = 60; // минимальная высота спектра
|
||
MIN_WH = 40; // минимальная высота водопада
|
||
var
|
||
RW, RH, SH, WH, TopOff, WideH, WideSpecH: Integer;
|
||
AvailH: Integer;
|
||
RULER_H: Integer;
|
||
begin
|
||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||
if PanelRight = nil then Exit;
|
||
if Assigned(FSpecView) then SyncSpecViewFreq;
|
||
RW := PanelRight.ClientWidth;
|
||
RH := PanelRight.ClientHeight;
|
||
UpdateWidebandFrequencyView;
|
||
WideH := 0;
|
||
WideSpecH := 0;
|
||
if FController.FShowWideband then
|
||
WideH := Min(MulDiv(WB_H, Screen.PixelsPerInch, 96), Max(40, RH div 2));
|
||
if WideH > 0 then
|
||
WideSpecH := Max(1, WideH - RULER_H);
|
||
TopOff := WideH;
|
||
if PbWideband <> nil then
|
||
begin
|
||
PbWideband.SetBounds(0, 0, RW, WideSpecH);
|
||
PbWideband.Visible := FController.FShowWideband and (WideSpecH > 0);
|
||
if FController.FShowWideband and (RW > 0) and (WideSpecH > 0) then
|
||
begin
|
||
FWidebandView.SetBitmapSize(RW, WideSpecH);
|
||
PbWideband.Invalidate;
|
||
end;
|
||
end;
|
||
if PbWidebandRuler <> nil then
|
||
begin
|
||
PbWidebandRuler.SetBounds(0, WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0));
|
||
PbWidebandRuler.Visible := FController.FShowWideband and (WideH > 0);
|
||
if FController.FShowWideband and (RW > 0) and (WideH > 0) then
|
||
begin
|
||
FWidebandView.SetRulerSize(RW, RULER_H);
|
||
PbWidebandRuler.Invalidate;
|
||
end;
|
||
end;
|
||
// S-метр позиционируется отдельно в ResizeSMeter
|
||
ResizeSMeter;
|
||
|
||
// ---- Оба скрыты ----
|
||
if (not FController.FShowSpectrum) and (not FController.FShowWaterfall) then
|
||
begin
|
||
PbSpectrum.Visible := False;
|
||
PbRuler.Visible := False;
|
||
PanelSplitter.Visible := False;
|
||
PbWaterfall.Visible := False;
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := 0;
|
||
FWaterfallHeight := 0;
|
||
PositionSampleRateOverlay;
|
||
Exit;
|
||
end;
|
||
|
||
// ---- Только спектр (водопад скрыт) ----
|
||
if FController.FShowSpectrum and (not FController.FShowWaterfall) then
|
||
begin
|
||
AvailH := RH - TopOff - RULER_H;
|
||
if AvailH < MIN_SH then AvailH := MIN_SH;
|
||
SH := AvailH;
|
||
WH := 0;
|
||
PbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||
PbSpectrum.Visible := True;
|
||
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||
PbRuler.Visible := True;
|
||
PanelSplitter.Visible := False;
|
||
PbWaterfall.Visible := False;
|
||
PositionSampleRateOverlay;
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := SH;
|
||
FWaterfallHeight := 0;
|
||
if RW > 0 then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(RW, SH);
|
||
FSpecView.SetWaterfallBitmapSize(1, 1);
|
||
FSpecView.SetRulerSize(RW, RULER_H);
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
PbRuler.Invalidate;
|
||
end;
|
||
Exit;
|
||
end;
|
||
|
||
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
|
||
if (not FController.FShowSpectrum) and FController.FShowWaterfall then
|
||
begin
|
||
AvailH := RH - TopOff - RULER_H;
|
||
if AvailH < MIN_WH then AvailH := MIN_WH;
|
||
SH := 0;
|
||
WH := AvailH;
|
||
PbSpectrum.Visible := False;
|
||
PanelSplitter.Visible := False;
|
||
PbWaterfall.SetBounds(0, TopOff, RW, WH);
|
||
PbWaterfall.Visible := True;
|
||
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
|
||
PbRuler.Visible := True;
|
||
PositionSampleRateOverlay;
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := 0;
|
||
FWaterfallHeight := WH;
|
||
if RW > 0 then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(1, 1);
|
||
FSpecView.SetWaterfallBitmapSize(RW, WH);
|
||
FSpecView.SetRulerSize(RW, RULER_H);
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
PbRuler.Invalidate;
|
||
end;
|
||
Exit;
|
||
end;
|
||
|
||
// ---- Оба видимы: стандартный режим со сплиттером ----
|
||
PbSpectrum.Visible := True;
|
||
PbRuler.Visible := True;
|
||
PanelSplitter.Visible := True;
|
||
PbWaterfall.Visible := True;
|
||
|
||
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
||
|
||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||
if FSplitterRatio < MIN_SH / AvailH then
|
||
FSplitterRatio := MIN_SH / AvailH;
|
||
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
|
||
FSplitterRatio := 1.0 - MIN_WH / AvailH;
|
||
|
||
SH := Round(AvailH * FSplitterRatio);
|
||
WH := AvailH - SH;
|
||
if WH < MIN_WH then WH := MIN_WH;
|
||
|
||
// Спектр
|
||
PbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||
// Линейка частот — всегда прижата к низу спектра
|
||
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||
// Сплиттер — между линейкой и водопадом
|
||
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
|
||
// Водопад — под сплиттером
|
||
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
|
||
|
||
PositionSampleRateOverlay;
|
||
|
||
// Обновляем переменные размеров
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := SH;
|
||
FWaterfallHeight := WH;
|
||
|
||
// Пересоздаём bitmap точно под новый размер
|
||
if RW > 0 then
|
||
begin
|
||
if SH > 0 then FSpecView.SetSpectrumBitmapSize(RW, SH);
|
||
if WH > 0 then FSpecView.SetWaterfallBitmapSize(RW, WH);
|
||
FSpecView.SetRulerSize(RW, RULER_H);
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Dark Theme
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.ApplyDarkTheme;
|
||
var T: TAppTheme; i: Integer;
|
||
procedure DP(P: TPanel);
|
||
begin P.Color := T.Panel; P.Font.Color := T.Text; end;
|
||
procedure DL(L: TLabel; Dim: Boolean = True);
|
||
begin if Dim then L.Font.Color := T.TextDim else L.Font.Color := T.Text; end;
|
||
procedure DS(S: TFlatSlider);
|
||
begin
|
||
S.SetThemeColors(T.SliderBG, T.SliderTrackEmpty, T.SliderTrackFill,
|
||
T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr);
|
||
end;
|
||
begin
|
||
if FLightTheme then T := LightTheme else T := DarkTheme;
|
||
|
||
Color := T.BG; Font.Color := T.Text;
|
||
DP(PanelToolbar); DP(PanelLeft);
|
||
DP(PanelTopVfoGroup); DP(PanelTopVfoA); DP(PanelTopVfoB);
|
||
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
|
||
DP(PanelBands); DP(PanelMode); DP(PanelFilter);
|
||
if PanelFMSQ <> nil then DP(PanelFMSQ);
|
||
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
|
||
if PanelFMStep <> nil then DP(PanelFMStep);
|
||
if PanelFMRpt <> nil then DP(PanelFMRpt);
|
||
if EdFMRptOffset <> nil then EdFMRptOffset.SetAppTheme(T);
|
||
DP(PanelRXBlock); DP(PanelAGC); DP(PanelTX);
|
||
DP(PanelRight);
|
||
|
||
if PbSpectrum is TPaintBox then
|
||
TPaintBox(PbSpectrum).Color := T.BG;
|
||
if PbWaterfall is TPaintBox then
|
||
TPaintBox(PbWaterfall).Color := T.BG;
|
||
if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel;
|
||
if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel;
|
||
ApplyStatusTheme(T);
|
||
|
||
// Метки
|
||
DL(LblAGCTop, False);
|
||
DL(LblDrv);
|
||
if LblFMSQ <> nil then DL(LblFMSQ, False);
|
||
|
||
// Кнопки тулбара
|
||
BtnDiscover.ClrNorm := T.TbDiscoverNorm;
|
||
BtnDiscover.ClrHot := T.TbDiscoverHot;
|
||
BtnDiscover.ClrActive := T.TbDiscoverHot;
|
||
BtnDiscover.ClrBorder := T.TbDiscoverBorder;
|
||
BtnDiscover.ClrText := T.TbDiscoverText;
|
||
BtnDiscover.ClrTextAct := T.TbDiscoverText;
|
||
BtnDiscover.Invalidate;
|
||
BtnStartStop.ClrNorm := T.TbStartNorm;
|
||
BtnStartStop.ClrBorder := T.TbStartBorder;
|
||
BtnStartStop.ClrText := T.TbStartText;
|
||
BtnStartStop.Invalidate;
|
||
|
||
// Обычные кнопки — все массивы
|
||
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
|
||
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
|
||
for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
|
||
StyleButton(BtnCTun, BtnCTun.Active);
|
||
StyleButton(BtnDUP, BtnDUP.Active);
|
||
if BtnFMSQ <> nil then StyleButton(BtnFMSQ, BtnFMSQ.Active);
|
||
if TrkFMSQ <> nil then DS(TrkFMSQ);
|
||
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, BtnFMCTCSS.Active);
|
||
if BtnFMCTCSSTone <> nil then StyleButton(BtnFMCTCSSTone, False);
|
||
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, BtnFMRptMinus.Active);
|
||
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active);
|
||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
||
if FStepDropDown <> nil then FStepDropDown.ApplyStyle(StyleButton, T.Panel);
|
||
if BtnChannels <> nil then StyleButton(BtnChannels, False);
|
||
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
|
||
StyleButton(BtnNR, BtnNR.Active);
|
||
StyleButton(BtnNB, BtnNB.Active);
|
||
StyleButton(BtnSNB, BtnSNB.Active);
|
||
StyleButton(BtnANF, BtnANF.Active);
|
||
StyleButton(BtnMute, BtnMute.Active);
|
||
StyleButton(BtnMOX, BtnMOX.Active);
|
||
StyleButton(BtnTUN, BtnTUN.Active);
|
||
|
||
// VFO-copy
|
||
StyleSpanButton(BtnVfoACopyB, False);
|
||
StyleSpanButton(BtnVfoSwap, False);
|
||
StyleSpanButton(BtnVfoBCopyA, False);
|
||
|
||
// Ползунки
|
||
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
|
||
|
||
// Спектр
|
||
FSpecView.SetTheme(T);
|
||
if Assigned(FWidebandView) then FWidebandView.SetTheme(T);
|
||
|
||
// VFO display — вызывает UpdateVfoDisplay, который использует тему
|
||
UpdateVfoDisplay;
|
||
end;
|
||
|
||
procedure TMainForm.SetStatusText(Index: Integer; const Text: string);
|
||
begin
|
||
if StatusPanel = nil then Exit;
|
||
StatusPanel.SetStatusText(Index, Text);
|
||
end;
|
||
|
||
procedure TMainForm.SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean);
|
||
begin
|
||
SetStatusText(2, ConnText);
|
||
SetStatusText(3, 'Supply --');
|
||
SetStatusText(4, 'RX idle');
|
||
SetStatusText(5, 'TX idle');
|
||
SetStatusText(6, 'SEQ --');
|
||
SetStatusText(7, 'PLL --');
|
||
if FSpecView <> nil then
|
||
FSpecView.ADCOverloadVisible := False;
|
||
if PbSpectrum <> nil then
|
||
PbSpectrum.Invalidate;
|
||
FillChar(FController.FDDCSeqValid, SizeOf(FController.FDDCSeqValid), 0);
|
||
FController.FSeqErrorCount := 0;
|
||
FController.FLastSeqErrorDDC := -1;
|
||
FController.FLastSeqErrorDelta := 0;
|
||
FController.FSeqOkStreak := 0;
|
||
if ClearDevice then
|
||
begin
|
||
SetStatusText(0, 'Board --');
|
||
SetStatusText(1, 'IP --');
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyStatusTheme(const T: TAppTheme);
|
||
begin
|
||
if StatusPanel = nil then Exit;
|
||
StatusPanel.ApplyTheme(T);
|
||
end;
|
||
|
||
procedure TMainForm.SetLightTheme(V: Boolean);
|
||
begin
|
||
if FLightTheme = V then Exit;
|
||
FLightTheme := V;
|
||
FController.FLoadedGlobal.LightTheme := V; // зеркало в persist-буфер контроллера
|
||
ApplyDarkTheme;
|
||
FController.FSettings.SaveTheme(V);
|
||
FController.FSettings.Save;
|
||
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
|
||
TSettingsForm(FSettingsForm).LoadLightTheme(V);
|
||
end;
|
||
|
||
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
|
||
var T: TAppTheme;
|
||
begin
|
||
if FLightTheme then T := LightTheme else T := DarkTheme;
|
||
B.Active := Active;
|
||
B.ClrNorm := T.BtnNorm;
|
||
B.ClrActive := T.BtnActive;
|
||
B.ClrHot := T.BtnHot;
|
||
if Active then B.ClrBorder := T.BtnBorderActive
|
||
else B.ClrBorder := T.BtnBorderNorm;
|
||
B.ClrText := T.BtnText;
|
||
B.ClrTextAct := T.BtnTextActive;
|
||
B.Font.Name := 'Courier New';
|
||
B.Font.Size := 8;
|
||
end;
|
||
|
||
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
|
||
var T: TAppTheme;
|
||
begin
|
||
if FLightTheme then T := LightTheme else T := DarkTheme;
|
||
B.Active := Active;
|
||
B.ClrNorm := T.SpanNorm;
|
||
B.ClrActive := T.SpanActive;
|
||
B.ClrHot := T.SpanActive;
|
||
B.ClrBorder := T.SpanBorder;
|
||
B.ClrText := T.SpanText;
|
||
B.ClrTextAct := T.SpanText;
|
||
B.Font.Name := 'Courier New';
|
||
B.Font.Size := 7;
|
||
end;
|
||
|
||
procedure TMainForm.PositionSampleRateOverlay;
|
||
const
|
||
MARGIN_X = 34;
|
||
MARGIN_Y = 4;
|
||
begin
|
||
if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit;
|
||
// Ширина = доступная ширина спектра справа от MARGIN_X (контент сам клампится
|
||
// к этому пределу). Раньше фиксировалось 320px → 8 Pluto-кнопок не влезали.
|
||
FSampleRateOverlay.SetBounds(
|
||
MARGIN_X,
|
||
MARGIN_Y,
|
||
Max(320, PbSpectrum.Width - MARGIN_X * 2),
|
||
FSampleRateOverlay.Height);
|
||
end;
|
||
|
||
procedure TMainForm.OnSampleRateOverlayInvalidate(Sender: TObject);
|
||
begin
|
||
FSpectrumDirty := True;
|
||
if Assigned(FSpecView) then
|
||
begin
|
||
FSpecView.SpectrumDirty := True;
|
||
FSpecView.InvalidateOverlayCache;
|
||
end;
|
||
if Assigned(PbSpectrum) then PbSpectrum.Invalidate;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// VFO
|
||
// ===========================================================================
|
||
|
||
function TMainForm.FormatFreq(Hz: Double): string;
|
||
var
|
||
Mhz: Int64;
|
||
KHz, Rest: Integer;
|
||
begin
|
||
Mhz := Trunc(Hz / 1000000);
|
||
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
|
||
Rest := Trunc(Hz) mod 1000;
|
||
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
|
||
end;
|
||
|
||
procedure TMainForm.UpdateVfoDisplay;
|
||
const
|
||
TOP_VFO_FREQ_H = 47;
|
||
var T: TAppTheme;
|
||
begin
|
||
if FLightTheme then T := LightTheme else T := DarkTheme;
|
||
|
||
FreqDispA.Frequency := Round(FController.FVfoA);
|
||
FreqDispB.Frequency := Round(FController.FVfoB);
|
||
FreqDispA.FontSize := TOP_VFO_FONT;
|
||
FreqDispB.FontSize := TOP_VFO_FONT;
|
||
FreqDispA.Height := TOP_VFO_FREQ_H;
|
||
FreqDispB.Height := TOP_VFO_FREQ_H;
|
||
|
||
FreqDispA.ColorHover := T.FreqHover;
|
||
FreqDispA.ColorHoverBG := T.FreqHoverBG;
|
||
FreqDispA.ColorDim := T.FreqDimDot;
|
||
FreqDispB.ColorHover := T.FreqHover;
|
||
FreqDispB.ColorHoverBG := T.FreqHoverBG;
|
||
FreqDispB.ColorDim := T.FreqDimDot;
|
||
|
||
if FController.FActiveVfo = 0 then
|
||
begin
|
||
FreqDispA.ColorNormal := T.FreqActive;
|
||
FreqDispB.ColorNormal := T.FreqDim;
|
||
LblVfoALabel.Font.Color := T.FreqActive;
|
||
LblVfoBLabel.Font.Color := T.TextDim;
|
||
if BtnTopVfoASelect <> nil then begin
|
||
BtnTopVfoASelect.ClrNorm := T.VfoSelActNorm;
|
||
BtnTopVfoASelect.ClrActive := T.VfoSelActNorm;
|
||
BtnTopVfoASelect.ClrHot := T.VfoSelActNorm;
|
||
BtnTopVfoASelect.ClrBorder := T.VfoSelActBorder;
|
||
BtnTopVfoASelect.ClrText := T.VfoSelActText;
|
||
BtnTopVfoASelect.ClrTextAct := T.VfoSelActTextAct;
|
||
BtnTopVfoASelect.Active := True;
|
||
BtnTopVfoASelect.Invalidate;
|
||
end;
|
||
if BtnTopVfoBSelect <> nil then begin
|
||
BtnTopVfoBSelect.ClrNorm := T.VfoSelInaNorm;
|
||
BtnTopVfoBSelect.ClrActive := T.VfoSelInaNorm;
|
||
BtnTopVfoBSelect.ClrHot := T.VfoSelInaNorm;
|
||
BtnTopVfoBSelect.ClrBorder := T.VfoSelInaBorder;
|
||
BtnTopVfoBSelect.ClrText := T.VfoSelInaText;
|
||
BtnTopVfoBSelect.ClrTextAct := T.VfoSelInaTextAct;
|
||
BtnTopVfoBSelect.Active := False;
|
||
BtnTopVfoBSelect.Invalidate;
|
||
end;
|
||
end else begin
|
||
FreqDispA.ColorNormal := T.FreqDim;
|
||
FreqDispB.ColorNormal := T.FreqActive;
|
||
LblVfoALabel.Font.Color := T.TextDim;
|
||
LblVfoBLabel.Font.Color := T.FreqActive;
|
||
if BtnTopVfoASelect <> nil then begin
|
||
BtnTopVfoASelect.ClrNorm := T.VfoSelInaNorm;
|
||
BtnTopVfoASelect.ClrActive := T.VfoSelInaNorm;
|
||
BtnTopVfoASelect.ClrHot := T.VfoSelInaNorm;
|
||
BtnTopVfoASelect.ClrBorder := T.VfoSelInaBorder;
|
||
BtnTopVfoASelect.ClrText := T.VfoSelInaText;
|
||
BtnTopVfoASelect.ClrTextAct := T.VfoSelInaTextAct;
|
||
BtnTopVfoASelect.Active := False;
|
||
BtnTopVfoASelect.Invalidate;
|
||
end;
|
||
if BtnTopVfoBSelect <> nil then begin
|
||
BtnTopVfoBSelect.ClrNorm := T.VfoSelActNorm;
|
||
BtnTopVfoBSelect.ClrActive := T.VfoSelActNorm;
|
||
BtnTopVfoBSelect.ClrHot := T.VfoSelActNorm;
|
||
BtnTopVfoBSelect.ClrBorder := T.VfoSelActBorder;
|
||
BtnTopVfoBSelect.ClrText := T.VfoSelActText;
|
||
BtnTopVfoBSelect.ClrTextAct := T.VfoSelActTextAct;
|
||
BtnTopVfoBSelect.Active := True;
|
||
BtnTopVfoBSelect.Invalidate;
|
||
end;
|
||
end;
|
||
if BtnTopVfoATX <> nil then
|
||
begin
|
||
if not FController.FSplitTxB then
|
||
begin
|
||
BtnTopVfoATX.ClrNorm := T.VfoTxActNorm;
|
||
BtnTopVfoATX.ClrHot := T.VfoTxActNorm;
|
||
BtnTopVfoATX.ClrActive := T.VfoTxActNorm;
|
||
BtnTopVfoATX.ClrBorder := T.VfoTxActBorder;
|
||
BtnTopVfoATX.ClrText := T.VfoTxActText;
|
||
BtnTopVfoATX.ClrTextAct := T.VfoTxActText;
|
||
BtnTopVfoATX.Active := True;
|
||
end
|
||
else
|
||
begin
|
||
BtnTopVfoATX.ClrNorm := T.VfoTxInaNorm;
|
||
BtnTopVfoATX.ClrHot := T.VfoTxInaNorm;
|
||
BtnTopVfoATX.ClrActive := T.VfoTxInaNorm;
|
||
BtnTopVfoATX.ClrBorder := T.VfoTxInaBorder;
|
||
BtnTopVfoATX.ClrText := T.VfoTxInaText;
|
||
BtnTopVfoATX.ClrTextAct := T.VfoTxInaText;
|
||
BtnTopVfoATX.Active := False;
|
||
end;
|
||
BtnTopVfoATX.Invalidate;
|
||
end;
|
||
if BtnTopVfoBTX <> nil then
|
||
begin
|
||
if FController.FSplitTxB then
|
||
begin
|
||
BtnTopVfoBTX.ClrNorm := T.VfoTxActNorm;
|
||
BtnTopVfoBTX.ClrHot := T.VfoTxActNorm;
|
||
BtnTopVfoBTX.ClrActive := T.VfoTxActNorm;
|
||
BtnTopVfoBTX.ClrBorder := T.VfoTxActBorder;
|
||
BtnTopVfoBTX.ClrText := T.VfoTxActText;
|
||
BtnTopVfoBTX.ClrTextAct := T.VfoTxActText;
|
||
BtnTopVfoBTX.Active := True;
|
||
end
|
||
else
|
||
begin
|
||
BtnTopVfoBTX.ClrNorm := T.VfoTxInaNorm;
|
||
BtnTopVfoBTX.ClrHot := T.VfoTxInaNorm;
|
||
BtnTopVfoBTX.ClrActive := T.VfoTxInaNorm;
|
||
BtnTopVfoBTX.ClrBorder := T.VfoTxInaBorder;
|
||
BtnTopVfoBTX.ClrText := T.VfoTxInaText;
|
||
BtnTopVfoBTX.ClrTextAct := T.VfoTxInaText;
|
||
BtnTopVfoBTX.Active := False;
|
||
end;
|
||
BtnTopVfoBTX.Invalidate;
|
||
end;
|
||
FreqDispA.Invalidate;
|
||
FreqDispB.Invalidate;
|
||
LblVfoALabel.Invalidate;
|
||
LblVfoBLabel.Invalidate;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Rendering helpers — delegated to TSpectrumView
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.InvalidateGridCache;
|
||
begin
|
||
FSpecView.InvalidateGridCache;
|
||
end;
|
||
|
||
procedure TMainForm.SyncSpecViewFreq;
|
||
begin
|
||
FSpecView.VfoA := FController.FVfoA;
|
||
FSpecView.VfoB := FController.FVfoB;
|
||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||
FSpecView.CenterFreq := FController.FCenterFreq;
|
||
FSpecView.SpanHz := FController.FSpanHz;
|
||
FSpecView.Mode := FController.FMode;
|
||
FSpecView.FilterBW := FController.FFilterBW;
|
||
if FController.FMode = MODE_FM then
|
||
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]
|
||
else
|
||
FSpecView.FMGridStepHz := 0;
|
||
// TX overlay следует за активной TX-частотой (учитывает split). При CTUN
|
||
// спектр TX будет отрисован на VFO, а не в центре дисплея.
|
||
FSpecView.TXFreq := ActiveTXFreqHz;
|
||
// Какой VFO привязан к TX (для split-полос фильтра): задаётся селектором
|
||
// FSplitTxB (TX-A/TX-B), независимо от активного RX-VFO.
|
||
if FController.FSplitTxB then FSpecView.TXVfoIndex := 1
|
||
else FSpecView.TXVfoIndex := 0;
|
||
if UpdateWidebandFrequencyView and FController.FShowWideband and (PbWideband <> nil) then
|
||
PbWideband.Invalidate;
|
||
end;
|
||
|
||
|
||
// ===========================================================================
|
||
// Timers
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.MeterTimerTick(Sender: TObject);
|
||
const
|
||
AVG_ALPHA = 0.25; // EMA для сглаженного среднего уровня
|
||
ZONE_DB = 6.0; // полуширина зоны вокруг среднего (дБ)
|
||
// Адаптивный alpha: базовый + ускорение при большой разнице
|
||
PEAK_BASE = 0.06; // базовая скорость пика
|
||
MIN_BASE = 0.06; // базовая скорость минимума
|
||
ACCEL_FACTOR = 0.018; // ускорение на каждый dB разницы
|
||
MAX_ALPHA = 0.85; // максимальная скорость (не мгновенно)
|
||
var
|
||
PeakTarget, MinTarget: Double;
|
||
PeakDiff, MinDiff: Double;
|
||
PeakAlpha, MinAlpha: Double;
|
||
begin
|
||
if not FController.FRunning then
|
||
begin
|
||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||
Exit;
|
||
end;
|
||
|
||
// QO-100 beacon lock: одна итерация контура (no-op если лок выключен).
|
||
FController.ServiceBeaconLock;
|
||
// Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR).
|
||
FController.ServicePlutoTelemetry;
|
||
|
||
// Сглаженное среднее (EMA)
|
||
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA;
|
||
|
||
// Цели для Peak и Min
|
||
PeakTarget := Max(FController.FLastSMeter, FSMeterAvg + ZONE_DB);
|
||
MinTarget := Min(FController.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 := FController.FLastSMeter;
|
||
FSpecView.SMeterPeak := FSMeterPeak;
|
||
FSpecView.SMeterMin := FSMeterMin;
|
||
FSpecView.LastFwdW := FController.FLastFwdW;
|
||
FSpecView.LastSWR := FController.FLastSWR;
|
||
FSpecView.Transmitting := FController.FTransmitting;
|
||
|
||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||
// PWR/SWR метки и бары обновляем здесь (10 Гц), а не в DoUpdateStatus —
|
||
// HP Status пакеты идут ~150 раз/с, при таком rate цифры дёргаются.
|
||
UpdateTXMeters;
|
||
|
||
if FController.FWDSPReady then
|
||
begin
|
||
if FController.FTransmitting then
|
||
begin
|
||
case FController.FDSPEngine.TXMicSource of
|
||
txmsSoundCard: SetStatusText(5, 'TX sound card');
|
||
txmsWeb: SetStatusText(5, 'TX web mic');
|
||
else
|
||
SetStatusText(5, 'TX radio mic');
|
||
end;
|
||
end
|
||
else
|
||
SetStatusText(5, 'TX idle');
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||
var
|
||
NowTick: QWord;
|
||
ElapsedTick: QWord;
|
||
begin
|
||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
|
||
// что вызвало бы сброс буфера водопада на каждом тике.
|
||
if (FSpectrumWidth = 0) or
|
||
(FController.FShowWideband and (PbWideband.Width <> FSpectrumWidth)) or
|
||
(FController.FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
|
||
(PbSpectrum.Height <> FSpectrumHeight))) or
|
||
(FController.FShowWaterfall and not FController.FShowSpectrum and
|
||
((PbWaterfall.Width <> FSpectrumWidth) or
|
||
(PbWaterfall.Height <> FWaterfallHeight))) then
|
||
ResizeSpectrumPanels;
|
||
if FSpectrumWidth <= 0 then Exit;
|
||
SyncSpecViewFreq;
|
||
|
||
// Проверка таймаута — трансивер не отвечает
|
||
if FController.FRunning and FController.FNetwork.Running then
|
||
begin
|
||
NowTick := GetTickCount64;
|
||
ElapsedTick := NowTick - FController.FRXStartTime;
|
||
// Ждём первый пакет 5 секунд после старта
|
||
if (FController.FRXLastPktTime = 0) and (ElapsedTick > 5000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FController.FRunning := False;
|
||
UninhibitSleep;
|
||
FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0);
|
||
SetRadioOfflineStatus('Hardware timeout');
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
// После первого пакета — следим чтобы поток не прерывался более 3 сек
|
||
if (FController.FRXLastPktTime > 0) and ((NowTick - FController.FRXLastPktTime) > 3000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FController.FRunning := False;
|
||
UninhibitSleep;
|
||
FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0);
|
||
SetRadioOfflineStatus('Connection lost');
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
if FController.FRunning then
|
||
begin
|
||
// Обновляем статус из таймера — без Synchronize в сетевом потоке.
|
||
SetStatusText(4, 'RX running');
|
||
if FController.FSeqErrorCount > 0 then
|
||
SetStatusText(6, Format('SEQ ERR DDC%d', [FController.FLastSeqErrorDDC]))
|
||
else
|
||
SetStatusText(6, 'SEQ OK');
|
||
|
||
if FController.FWDSPReady then
|
||
begin
|
||
FController.FLastSMeter := FController.FDSPEngine.GetSMeterDBm;
|
||
FController.FDSPEngine.SetSpectrumWidth(FSpectrumWidth);
|
||
Inc(FAgcLineCounter);
|
||
if FAgcLineCounter >= 6 then
|
||
begin
|
||
FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
FSpecView.AGCThresh := FController.FDSPEngine.AGCThresh;
|
||
FSpecView.AGCHangLevel := FController.FDSPEngine.AGCHangLevel;
|
||
FAgcLineCounter := 0;
|
||
end;
|
||
end;
|
||
FSpecView.WDSPReady := FController.FWDSPReady;
|
||
// Обновляем оверлей если видим
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.UpdateSMeter(FController.FLastSMeter);
|
||
FVfoOverlay.UpdateVfo(FController.FVfoA);
|
||
PositionVfoOverlay;
|
||
end;
|
||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||
if FSpectrumDirty then
|
||
begin
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if FSpecView.NeedsRulerRedraw then
|
||
PbRuler.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
if FWaterfallDirty then
|
||
begin
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
FWaterfallDirty := False;
|
||
end;
|
||
if FWidebandDirty and FController.FShowWideband then
|
||
begin
|
||
PbWideband.Invalidate;
|
||
FWidebandDirty := False;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Не подключены — показываем чистый экран (без анимации)
|
||
if not FController.FNetwork.Connected then
|
||
begin
|
||
// При первом старте показываем демо-спектр
|
||
if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight);
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Подключены, но не запущены — статичный спектр (шум)
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
end;
|
||
|
||
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
|
||
if (FSpectrumDirty or FWaterfallDirty) and (not FController.FRunning) then
|
||
begin
|
||
if FSpectrumDirty then
|
||
FSpecView.DrawSpectrum;
|
||
if FWaterfallDirty then
|
||
FSpecView.DrawWaterfall;
|
||
if FSpectrumDirty then
|
||
PbSpectrum.Invalidate;
|
||
if FSpecView.NeedsRulerRedraw then
|
||
PbRuler.Invalidate;
|
||
if FWaterfallDirty then
|
||
PbWaterfall.Invalidate;
|
||
FWaterfallDirty := False;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
|
||
// Исходящее зеркало состояния + спектр/водопад в web — в адаптере (всё из
|
||
// FController, включая последний кадр пикселей). Сервер сам гейтит по FRunning.
|
||
if Assigned(FWebAdapter) then
|
||
FWebAdapter.PushState;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Network callbacks → sync helpers
|
||
// ===========================================================================
|
||
|
||
// OnDeviceFound / discovery теперь в TRadioController (сетевой поток → store +
|
||
// rfDeviceList). UI слушает rfDeviceList в OnControllerState.
|
||
|
||
procedure TMainForm.UpdateTXMeters;
|
||
// Вызывается из MeterTimerTick (10 Гц). FController.FLastFwdW/FLastSWR/
|
||
// FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с)
|
||
// в TRadioController.ApplyHPStatus.
|
||
begin
|
||
UpdateStatusField7; // PLL (openHPSDR) или статус beacon lock (Pluto)
|
||
UpdateStatusField3; // Supply (openHPSDR) или Temp/RSSI (Pluto)
|
||
end;
|
||
|
||
procedure TMainForm.UpdateStatusField3;
|
||
// Поле 3 статус-бара. openHPSDR — supply V/A (HP status). Pluto — температура
|
||
// чипа и RSSI приёмника (нет PA → supply неприменим). Значения берём из
|
||
// контроллера (FLastPlutoTempC/RSSIdB / FLastSupplyV/A) — логика опроса в ядре.
|
||
var
|
||
S: string;
|
||
begin
|
||
// Pluto: нет PA/supply — показываем аппаратную телеметрию AD936x.
|
||
if FController.IsPluto then
|
||
begin
|
||
if not FController.FRunning then begin SetStatusText(3, 'Pluto --'); Exit; end;
|
||
S := '';
|
||
if FController.FLastPlutoTempC > TELEMETRY_NONE then
|
||
S := Format('Temp %.0fC', [FController.FLastPlutoTempC]);
|
||
if FController.FLastPlutoRSSIdB > TELEMETRY_NONE then
|
||
begin
|
||
if S <> '' then S := S + ' ';
|
||
S := S + Format('RSSI %.0fdB', [FController.FLastPlutoRSSIdB]);
|
||
end;
|
||
if S = '' then S := 'Pluto --';
|
||
SetStatusText(3, S);
|
||
Exit;
|
||
end;
|
||
|
||
if not FController.FRunning then
|
||
begin
|
||
SetStatusText(3, 'Supply --');
|
||
Exit;
|
||
end;
|
||
|
||
if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then
|
||
begin
|
||
SetStatusText(3, 'Supply n/a');
|
||
Exit;
|
||
end;
|
||
|
||
if FController.FLastSupplyV >= 0 then
|
||
begin
|
||
if FController.FLastSupplyA >= 0 then
|
||
SetStatusText(3,
|
||
Format('Supply %.1fV %.1fA', [FController.FLastSupplyV, FController.FLastSupplyA]))
|
||
else
|
||
SetStatusText(3,
|
||
Format('Supply %.1fV', [FController.FLastSupplyV]));
|
||
end else
|
||
SetStatusText(3, 'Supply --');
|
||
end;
|
||
|
||
function TMainForm.BeaconStatusText: string;
|
||
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
|
||
// поле ~200px → держим ≤ ~24 символов.
|
||
begin
|
||
if not FController.BeaconLockEnabled then Exit('BCN: off');
|
||
if FController.BeaconDecodeFreqHz <= 0 then Exit('BCN: click beacon');
|
||
case FController.BeaconState of
|
||
bsLock: Result := Format('BCN LOCK %.0fHz /%.0fdB',
|
||
[FController.BeaconCorrectionHz, FController.BeaconSNRdB]);
|
||
bsSearch: Result := Format('BCN sync /%.0fdB', [FController.BeaconSNRdB]);
|
||
else Result := 'BCN --';
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateStatusField7;
|
||
// Поле 7 статус-бара. openHPSDR — PLL-lock (как прежде). Pluto в трансвертере —
|
||
// статус QO-100 beacon lock (у Pluto нет PLL-телеметрии); Pluto без XVTR — пусто.
|
||
begin
|
||
if FController.IsPluto then
|
||
begin
|
||
if FController.FCurrentXvtr >= 0 then SetStatusText(7, BeaconStatusText)
|
||
else SetStatusText(7, '');
|
||
Exit;
|
||
end;
|
||
if not FController.FRunning then SetStatusText(7, 'PLL --')
|
||
else if FController.FLastPLLLock then SetStatusText(7, 'PLL OK')
|
||
else SetStatusText(7, 'PLL?');
|
||
end;
|
||
|
||
procedure TMainForm.OnWidebandCB(ADCIndex: Integer;
|
||
const Samples: array of SmallInt; Count: Integer);
|
||
begin
|
||
if (not FController.FShowWideband) or (FWidebandView = nil) then Exit;
|
||
FWidebandView.SetSamples(Samples, Count);
|
||
FWidebandDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||
begin
|
||
SetStatusText(4, 'RX running');
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX settings — применение к WDSP, сборка DUC Specific, sound-card pull
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// BuildMicLineSelectByte / SendDUCSpecificFromSettings переехали в
|
||
// TRadioController (чистые над FTXSettings/движком; нужны headless StartRunning).
|
||
|
||
function TMainForm.DefaultMicSource: TTXMicSource;
|
||
// Mic source для MOX-кнопки и Web-TX: если настроена звуковая карта — берём с неё,
|
||
// иначе используем HW mic-стрим от трансивера.
|
||
// HW PTT всегда переключает на txmsRadio непосредственно в ApplyMOX.
|
||
begin
|
||
if FController.FAudioInDevName <> '' then Result := txmsSoundCard
|
||
else Result := txmsRadio;
|
||
end;
|
||
|
||
// ApplyTXSettingsToDSP переехал в TRadioController (вся TX-цепь над FTXSettings).
|
||
|
||
// ===========================================================================
|
||
// Button handlers
|
||
// ===========================================================================
|
||
|
||
{ Поток открытия — отдельный класс, без анонимных процедур }
|
||
// Открываем WDSP в фоновом потоке чтобы не блокировать UI при нажатии START
|
||
type
|
||
TWDSPOpenThread = class(TThread)
|
||
private
|
||
FForm: TMainForm;
|
||
FResult: Boolean;
|
||
procedure SyncDone;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AForm: TMainForm);
|
||
end;
|
||
|
||
constructor TWDSPOpenThread.Create(AForm: TMainForm);
|
||
begin
|
||
inherited Create(False);
|
||
FForm := AForm;
|
||
FResult := False;
|
||
FreeOnTerminate := True;
|
||
end;
|
||
|
||
procedure TWDSPOpenThread.Execute;
|
||
var
|
||
M: TThreadMethod;
|
||
begin
|
||
try
|
||
FResult := FForm.FController.FDSPEngine.Open;
|
||
except
|
||
FResult := False;
|
||
end;
|
||
M := SyncDone;
|
||
TThread.Synchronize(nil, M);
|
||
end;
|
||
|
||
procedure TWDSPOpenThread.SyncDone;
|
||
begin
|
||
FForm.OnWDSPOpenDone(FResult);
|
||
end;
|
||
|
||
// TDiscoverThread / no-device-сигнал переехали в TRadioController.Discover.
|
||
|
||
procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer);
|
||
begin
|
||
if ASampleRate <= 0 then Exit;
|
||
if Assigned(FController.FDSPEngine) then
|
||
begin
|
||
FController.FDSPEngine.Close;
|
||
FreeAndNil(FController.FDSPEngine);
|
||
end;
|
||
FController.FWDSPReady := False;
|
||
FController.FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||
// Иначе sound-card mic путь теряется после смены sample rate (колбэк
|
||
// навешивается в FormCreate, но новый движок про него не знал).
|
||
FController.FDSPEngine.OnPullMicSamples := FController.PullSoundCardMic;
|
||
end;
|
||
|
||
function TMainForm.EnsureWDSPWisdom: Boolean;
|
||
var
|
||
WisdomDir: string;
|
||
WisdomPath: string;
|
||
DirA: AnsiString;
|
||
BuildThread: TWisdomBuildThread;
|
||
Dlg: TWisdomProgressDialog;
|
||
begin
|
||
Result := True;
|
||
|
||
if not Assigned(@WDSPwisdom) then Exit;
|
||
|
||
WisdomDir := GetAppCfgDir;
|
||
WisdomPath := WisdomDir + WDSP_WISDOM_FILE;
|
||
|
||
if FileExists(WisdomPath) then
|
||
begin
|
||
// По рекомендации WDSP вызываем функцию на каждом старте:
|
||
// если файл уже есть, библиотека просто импортирует wisdom.
|
||
DirA := AnsiString(WisdomDir);
|
||
WDSPwisdom(PAnsiChar(DirA));
|
||
Exit;
|
||
end;
|
||
|
||
BtnStartStop.Enabled := False;
|
||
SetStatusText(2, 'Creating WDSP wisdom...');
|
||
|
||
BuildThread := TWisdomBuildThread.Create(WisdomDir);
|
||
try
|
||
if FLightTheme then
|
||
Dlg := TWisdomProgressDialog.Create(Self, BuildThread, LightTheme)
|
||
else
|
||
Dlg := TWisdomProgressDialog.Create(Self, BuildThread, DarkTheme);
|
||
try
|
||
Dlg.ShowModal;
|
||
finally
|
||
Dlg.Free;
|
||
end;
|
||
|
||
BuildThread.WaitFor;
|
||
|
||
if BuildThread.ErrorText <> '' then
|
||
begin
|
||
ShowMessage('Failed to create WDSP wisdom.' + LineEnding +
|
||
BuildThread.ErrorText);
|
||
Result := False;
|
||
end
|
||
else if not FileExists(WisdomPath) then
|
||
begin
|
||
ShowMessage('WDSP wisdom was not created.' + LineEnding +
|
||
'Expected file: ' + WisdomPath);
|
||
Result := False;
|
||
end;
|
||
finally
|
||
BuildThread.Free;
|
||
BtnStartStop.Enabled := True;
|
||
end;
|
||
|
||
if Result then
|
||
SetStatusText(2, 'DSP ready')
|
||
else
|
||
SetStatusText(2, 'WDSP wisdom missing');
|
||
end;
|
||
|
||
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
|
||
begin
|
||
// Открываем диалог выбора устройства
|
||
if not Assigned(FDeviceDialog) then
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.Store := FController.FDeviceStore;
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
|
||
else FDeviceDialog.SetTheme(DarkTheme);
|
||
FDeviceDialog.ClearResult; // сбрасываем старый выбор перед открытием
|
||
FDeviceDialog.ShowModal;
|
||
// Если пользователь нажал CONNECT — результат хранится в DialogResult,
|
||
// START его заберёт и использует с приоритетом над autostart
|
||
end;
|
||
|
||
procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
|
||
// Запускается когда пользователь нажимает DISCOVER внутри диалога.
|
||
// Контроллер чистит список и стартует поток; UI слушает rfDeviceList.
|
||
// Сетевые Pluto (без mDNS) не находятся scan'ом — собираем список URI для
|
||
// прямого probe: сохранённые Pluto + ручной ввод IP в поле диалога.
|
||
var
|
||
URIs: array of string;
|
||
i: Integer;
|
||
ip: string;
|
||
|
||
procedure AddURI(const U: string);
|
||
var k: Integer;
|
||
begin
|
||
if U = '' then Exit;
|
||
for k := 0 to High(URIs) do if SameText(URIs[k], U) then Exit;
|
||
SetLength(URIs, Length(URIs) + 1);
|
||
URIs[High(URIs)] := U;
|
||
end;
|
||
|
||
begin
|
||
SetLength(URIs, 0);
|
||
for i := 0 to FController.FDeviceStore.SavedCount - 1 do
|
||
if FController.FDeviceStore.Saved(i).Kind = bkPluto then
|
||
AddURI(FController.FDeviceStore.Saved(i).URI);
|
||
if Assigned(FDeviceDialog) then
|
||
begin
|
||
ip := FDeviceDialog.ManualIP;
|
||
if ip <> '' then
|
||
begin
|
||
if (Pos('ip:', ip) = 1) or (Pos('usb:', ip) = 1) or (Pos('local:', ip) = 1) then
|
||
AddURI(ip)
|
||
else
|
||
AddURI('ip:' + ip);
|
||
end;
|
||
end;
|
||
FController.SetPlutoProbeURIs(URIs);
|
||
|
||
SetStatusText(2, 'Discovering...');
|
||
FController.Discover;
|
||
end;
|
||
|
||
procedure TMainForm.BtnStartStopClick(Sender: TObject);
|
||
var
|
||
Dev: THPSDRDevice;
|
||
AutoIP: string;
|
||
begin
|
||
if FController.FNetwork.Connected then
|
||
begin
|
||
// --- STOP ---
|
||
DoStopAndDisconnect;
|
||
Exit;
|
||
end;
|
||
|
||
// --- START ---
|
||
AutoIP := '';
|
||
FPendingBoardType := 0;
|
||
|
||
// Явный выбор через CONNECT в диалоге имеет приоритет над autostart
|
||
if Assigned(FDeviceDialog) and FDeviceDialog.DialogResult.Accepted then
|
||
begin
|
||
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
|
||
FPendingBoardType := FDeviceDialog.GetSavedBoardType(
|
||
FDeviceDialog.DialogResult.SavedIdx);
|
||
FDeviceDialog.ClearResult; // consumed — следующий START снова спросит
|
||
end
|
||
else
|
||
begin
|
||
if Assigned(FDeviceDialog) then
|
||
AutoIP := FDeviceDialog.GetAutoStartIP;
|
||
|
||
if AutoIP <> '' then
|
||
begin
|
||
FPendingIP := AutoIP;
|
||
FPendingBoardType := FDeviceDialog.GetAutoStartBoardType;
|
||
end
|
||
else
|
||
begin
|
||
if not Assigned(FDeviceDialog) then
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.Store := FController.FDeviceStore;
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
|
||
else FDeviceDialog.SetTheme(DarkTheme);
|
||
if FDeviceDialog.ShowModal <> mrOk then Exit;
|
||
if not FDeviceDialog.DialogResult.Accepted then Exit;
|
||
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
|
||
FPendingBoardType := FDeviceDialog.GetSavedBoardType(
|
||
FDeviceDialog.DialogResult.SavedIdx);
|
||
FDeviceDialog.ClearResult;
|
||
end;
|
||
end;
|
||
|
||
// Создаём запись устройства. Если это известный (discovered/saved) Pluto —
|
||
// восстанавливаем Kind/URI/Serial/MAC из store по IP; иначе HPSDR по IP.
|
||
Dev := ResolveDevice(FPendingIP);
|
||
if Dev.Kind = bkHPSDR then Dev.BoardType := FPendingBoardType;
|
||
StartWithDevice(Dev);
|
||
end;
|
||
|
||
function TMainForm.ResolveDevice(const IP: string): THPSDRDevice;
|
||
// По IP/URI ищет устройство среди discovered, затем saved, чтобы восстановить
|
||
// Kind/URI/Serial/MAC (нужно для Pluto). Логика общая с демоном — в контроллере.
|
||
begin
|
||
Result := FController.ResolveDevice(IP);
|
||
end;
|
||
|
||
procedure TMainForm.DoStopAndDisconnect;
|
||
// STOP-teardown, общий для десктоп-STOP и web-disconnect. Движок (graceful Run=0,
|
||
// сброс TX/телеметрии) + персист настроек + Disconnect — в контроллере
|
||
// (StopAndDisconnect); рендер run-кнопки/MOX/sleep — через rfRunning. Здесь —
|
||
// только очистка спектра и контекстный offline-статус.
|
||
begin
|
||
FController.StopAndDisconnect;
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
SetRadioOfflineStatus('Disconnected', True);
|
||
end;
|
||
|
||
procedure TMainForm.StartWithDevice(const Dev: THPSDRDevice);
|
||
// Запуск с уже выбранным устройством — общий путь для десктоп-диалога и
|
||
// web-оверлея (без модального диалога). Подбор sample rate до WDSP, открытие
|
||
// WDSP (async) и подключение.
|
||
var
|
||
PreloadRate: Integer;
|
||
FoundByIP: Boolean;
|
||
i: Integer;
|
||
PreG: TGlobalSettings;
|
||
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||
begin
|
||
// Подбираем sample rate до открытия WDSP, чтобы не делать двойной Open:
|
||
// 1) Open на 192k, 2) сразу ChangeSampleRate+Open ещё раз.
|
||
// Если устройство уже было обнаружено (есть MAC) и есть сохранённый профиль,
|
||
// создаём WDSPEngine сразу с нужной частотой.
|
||
PreloadRate := FController.FSampleRate;
|
||
FoundByIP := False;
|
||
i := FController.FDeviceStore.FindDiscoveredByIP(Dev.IPAddress);
|
||
if i >= 0 then
|
||
begin
|
||
FoundByIP := True;
|
||
if FController.FSettings.LoadDevice(FController.FDeviceStore.Discovered(i).MAC, PreG, PreBands)
|
||
and (PreG.SampleRate > 0) then
|
||
PreloadRate := PreG.SampleRate;
|
||
end;
|
||
if FoundByIP and (PreloadRate > 0) then
|
||
begin
|
||
FController.FSampleRate := PreloadRate;
|
||
FController.FSpanHz := PreloadRate;
|
||
if (not FController.FWDSPReady) and (Assigned(FController.FDSPEngine)) and (FController.FDSPEngine.SampleRate <> PreloadRate) then
|
||
RecreateDSPEngine(PreloadRate);
|
||
end;
|
||
|
||
// --- Открываем WDSP асинхронно чтобы не блокировать UI ---
|
||
FPendingDev := Dev;
|
||
BtnStartStop.Enabled := False;
|
||
SetStatusText(2, 'Opening DSP...');
|
||
if FController.FWDSPReady then
|
||
// WDSP уже открыт — сразу подключаемся
|
||
DoConnectDevice(Dev)
|
||
else
|
||
// Открываем в фоновом потоке
|
||
TWDSPOpenThread.Create(Self);
|
||
end;
|
||
|
||
procedure TMainForm.OnWDSPOpenDone(Success: Boolean);
|
||
begin
|
||
// Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке
|
||
FController.FWDSPReady := Success;
|
||
if not Success then
|
||
begin
|
||
SetStatusText(2, 'DSP failed');
|
||
BtnStartStop.Enabled := True;
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
ShowMessage('WDSP not loaded.' + LineEnding +
|
||
IfThen(FController.FDSPEngine.LastError <> '', FController.FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding +
|
||
'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).');
|
||
Exit;
|
||
end;
|
||
DoConnectDevice(FPendingDev);
|
||
end;
|
||
|
||
procedure TMainForm.RelabelBands;
|
||
// Переписывает подписи band-кнопок под план активного бэкенда (HF ↔ VHF/UHF).
|
||
var i: Integer; pl: Boolean;
|
||
begin
|
||
pl := FController.IsPluto;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
if BtnBand[i] <> nil then
|
||
BtnBand[i].Caption := BandName(i, pl);
|
||
end;
|
||
|
||
procedure TMainForm.ApplyBackendCapsToUI;
|
||
// Пресеты sample-rate оверлея из Caps (Pluto >560k) + (TODO) гейтинг полей,
|
||
// которых нет у бэкенда (PA/SWR/Alex/Wideband).
|
||
var
|
||
Caps: TBackendCaps;
|
||
begin
|
||
Caps := FController.BackendCaps;
|
||
if Assigned(FSampleRateOverlay) then
|
||
begin
|
||
if (Caps.SampleRateMode = srmContinuous) and (Length(Caps.RatePresets) > 0) then
|
||
FSampleRateOverlay.SetRatePresets(Caps.RatePresets)
|
||
else
|
||
FSampleRateOverlay.SetRatePresetsDefault; // HPSDR 48k..1536k
|
||
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
|
||
end;
|
||
|
||
// Отдельный Pluto RF-AGC блок под AGC: показываем и сдвигаем панели ниже.
|
||
LayoutLeftPanel(Caps.HasHWGain);
|
||
if Caps.HasHWGain then UpdateRxGainUI;
|
||
end;
|
||
|
||
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
||
begin
|
||
BtnStartStop.Enabled := True;
|
||
|
||
// Connect + load-by-MAC + перестройка диапазона/XVTR/активного VFO + go-live —
|
||
// вся оркестрация в контроллере (ConnectAndRun); рендер device/band/run-
|
||
// виджетов идёт через события (rfDevice/rfBandRestore/rfXvtr/rfActiveVfo/
|
||
// rfRunning). Здесь — только UI-extras (статус-строки/AGCTop/resize).
|
||
if not FController.ConnectAndRun(Dev, FSpectrumWidth) then
|
||
begin
|
||
ShowMessage('Failed to connect to ' + Dev.IPAddress +
|
||
IfThen(FController.FNetwork.LastError <> '', ': ' + FController.FNetwork.LastError, ''));
|
||
Exit;
|
||
end;
|
||
|
||
// Активный VFO=A: подсветку (рисовалась в FormCreate при FActiveVfo=0) освежаем
|
||
// здесь — для B это сделал SetActiveVfo(1) внутри ConnectAndRun.
|
||
if FController.FActiveVfo <> 1 then
|
||
UpdateVfoDisplay;
|
||
|
||
SetStatusText(1, 'IP: ' + FController.FNetwork.Device.IPAddress);
|
||
SetStatusText(0, 'Board: ' + FController.BoardDisplayName);
|
||
FSpecView.AGCTop := FController.FAGCTop;
|
||
SetStatusText(6, 'SEQ OK');
|
||
SetStatusText(2, 'Running');
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
|
||
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
|
||
// Перестройка всегда происходит на частоту активного VFO (не на оффсет).
|
||
// CTUN при этом сбрасывается чтобы не было путаницы с оффсетом.
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActiveVfoFreq — частота активного VFO
|
||
// ---------------------------------------------------------------------------
|
||
function TMainForm.ActiveVfoFreq: Int64;
|
||
begin
|
||
if FController.FActiveVfo = 0 then Result := Round(FController.FVfoA)
|
||
else Result := Round(FController.FVfoB);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
|
||
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
|
||
// Перестройка на частоту выбранного VFO. CTUN не трогаем.
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ActivateVfo(Idx: Integer);
|
||
begin
|
||
// Движок (переключение RX-VFO, CTUN-логика, центр/сеть, диапазон) — в
|
||
// контроллере; рендер — OnControllerState(rfActiveVfo/rfBand/rfCTun).
|
||
FController.SetActiveVfo(Idx);
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispAClick(Sender: TObject);
|
||
begin
|
||
// Клик на VFO-A → активируем A (перестройка на FController.FVfoA)
|
||
if FController.FActiveVfo <> 0 then
|
||
ActivateVfo(0);
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispBClick(Sender: TObject);
|
||
begin
|
||
// Клик на VFO-B → активируем B (перестройка на FController.FVfoB)
|
||
if FController.FActiveVfo <> 1 then
|
||
ActivateVfo(1);
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoASelectClick(Sender: TObject);
|
||
begin
|
||
if FController.FActiveVfo <> 0 then
|
||
ActivateVfo(0);
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoATXClick(Sender: TObject);
|
||
begin
|
||
FController.FSplitTxB := False;
|
||
UpdateVfoDisplay;
|
||
|
||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||
begin
|
||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
||
FController.FTransmitting, True, True);
|
||
FController.FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoBSelectClick(Sender: TObject);
|
||
begin
|
||
if FController.FActiveVfo <> 1 then
|
||
ActivateVfo(1);
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoBTXClick(Sender: TObject);
|
||
begin
|
||
FController.FSplitTxB := True;
|
||
if FController.FSplitTxB and (FController.FActiveVfo <> 0) then
|
||
ActivateVfo(0)
|
||
else
|
||
UpdateVfoDisplay;
|
||
|
||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||
begin
|
||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
||
FController.FTransmitting, True, True);
|
||
FController.FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64);
|
||
begin
|
||
if FController.FActiveVfo = 0 then
|
||
ApplyVfoA(NewFreq)
|
||
else
|
||
FController.FVfoA := NewFreq; // просто сохраняем, приёмник не перестраиваем
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||
begin
|
||
// Движок (сохранение B, перестройка при активном B, диапазон, сеть) — в
|
||
// контроллере; рендер — OnControllerState(rfVfoB).
|
||
FController.SetVfoB(NewFreq);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.UpdateFilterButtons;
|
||
var
|
||
i: Integer;
|
||
Names: array[0..FILT_COUNT-1] of string;
|
||
DefIdx: Integer;
|
||
begin
|
||
if FController.FMode = MODE_FM then
|
||
begin
|
||
// FM: show only NFM and WFM buttons, resize them to fill the row
|
||
CloseCTCSSPopup;
|
||
if FController.FFilter > 1 then FController.FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
|
||
FController.FFilterBW := FILT_FM_BW[FController.FFilter];
|
||
FController.FFMDeviation := FILT_FM_DEV[FController.FFilter];
|
||
BtnFilter[0].Caption := FILT_FM_NAMES[0];
|
||
BtnFilter[0].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 0);
|
||
BtnFilter[0].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 0);
|
||
BtnFilter[0].Top := 16;
|
||
BtnFilter[0].Visible := True;
|
||
StyleButton(BtnFilter[0], FController.FFilter = 0);
|
||
BtnFilter[1].Caption := FILT_FM_NAMES[1];
|
||
BtnFilter[1].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 1);
|
||
BtnFilter[1].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 1);
|
||
BtnFilter[1].Top := 16;
|
||
BtnFilter[1].Visible := True;
|
||
StyleButton(BtnFilter[1], FController.FFilter = 1);
|
||
for i := 2 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||
// Shrink panel to fit label + 1 button row so SQL/CTCSS sit right below
|
||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
||
if PanelFMSQ <> nil then
|
||
begin
|
||
PanelFMSQ.Visible := True;
|
||
StyleButton(BtnFMSQ, FController.FFMSQOn);
|
||
TrkFMSQ.Position := FController.FFMSQLevel;
|
||
LblFMSQ.Caption := IntToStr(FController.FFMSQLevel);
|
||
end;
|
||
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := True;
|
||
if PanelFMStep <> nil then
|
||
begin
|
||
StyleButton(BtnFMStep, FController.FFMStepOn);
|
||
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx);
|
||
PanelFMStep.Visible := True;
|
||
end;
|
||
if PanelFMRpt <> nil then
|
||
begin
|
||
StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS);
|
||
StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS);
|
||
EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
|
||
PanelFMRpt.Visible := True;
|
||
end;
|
||
RelayoutBelowBands;
|
||
Exit;
|
||
end;
|
||
|
||
// Non-FM: restore full 2-row height and 5-column layout, hide FM panels
|
||
PanelFilter.Height := 74;
|
||
if PanelFMSQ <> nil then PanelFMSQ.Visible := False;
|
||
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
|
||
if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end;
|
||
if PanelFMRpt <> nil then PanelFMRpt.Visible := False;
|
||
for i := 0 to FILT_COUNT - 1 do
|
||
begin
|
||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
||
BtnFilter[i].Top := 16 + (i div 5) * 27;
|
||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, 5, i mod 5);
|
||
BtnFilter[i].Visible := True;
|
||
end;
|
||
|
||
case FController.FMode of
|
||
0, 1: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
|
||
DefIdx := FILT_SSB_DEF;
|
||
end;
|
||
2: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
|
||
DefIdx := FILT_DSB_DEF;
|
||
end;
|
||
3, 4: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
|
||
DefIdx := FILT_CW_DEF;
|
||
end;
|
||
else begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
|
||
DefIdx := FILT_AM_DEF;
|
||
end;
|
||
end;
|
||
|
||
FController.FFilter := DefIdx;
|
||
case FController.FMode of
|
||
0, 1: FController.FFilterBW := FILT_SSB_BW[DefIdx];
|
||
2: FController.FFilterBW := FILT_DSB_BW[DefIdx];
|
||
3, 4: FController.FFilterBW := FILT_CW_BW[DefIdx];
|
||
else FController.FFilterBW := FILT_AM_BW[DefIdx];
|
||
end;
|
||
|
||
for i := 0 to FILT_COUNT-1 do
|
||
begin
|
||
BtnFilter[i].Caption := Names[i];
|
||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
||
end;
|
||
RelayoutBelowBands;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ApplyVfoA — единая точка смены частоты VFO A
|
||
//
|
||
// CTUN ВЫКЛ (классический режим):
|
||
// FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
||
//
|
||
// CTUN ВКЛ (piHPSDR/Thetis режим):
|
||
// FController.FCenterFreq статичен — спектр/водопад не двигаются
|
||
// FController.FVfoA движется по дисплею (маркер гуляет)
|
||
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
|
||
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
|
||
// hits the edge of the display")
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
||
begin
|
||
// Тонкий делегат: движок (XVTR-клэмп, CTUN, band-detect, FM авто-RPT, сеть) —
|
||
// в SetVfoA; канальная оркестрация — в FController.OnAfterTune (хук в конце
|
||
// SetVfoA → FChannelController.OnVfoTuned); рендер — OnControllerState.
|
||
FController.SetVfoA(NewFreq);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CTUN toggle
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.BtnCTunClick(Sender: TObject);
|
||
begin
|
||
// Логика (band cache, center/shift, сеть) — в контроллере; рендер — в
|
||
// OnControllerState(rfCTun).
|
||
FController.SetCTun(not FController.FCTun);
|
||
end;
|
||
|
||
procedure TMainForm.BtnBeaconClick(Sender: TObject);
|
||
// ЛКМ по BEACON: вкл/выкл лок маяка (= декодер + трим LOError). При включении
|
||
// «вооружаем» клик (следующий ЛКМ по спектру наведёт декодер на маяк) и открываем
|
||
// окно констелляции/бюллетеня. Декодер ведёт несущую, контур стекает дрейф в LOError.
|
||
begin
|
||
FController.SetBeaconLock(not FController.BeaconLockEnabled);
|
||
FBeaconArming := FController.BeaconLockEnabled;
|
||
if FController.BeaconLockEnabled then
|
||
begin
|
||
if FBeaconScopeForm = nil then
|
||
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
|
||
FBeaconScopeForm.ApplyTheme(DarkTheme);
|
||
FBeaconScopeForm.Show;
|
||
end;
|
||
UpdateBeaconButton;
|
||
end;
|
||
|
||
procedure TMainForm.BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
// ПКМ по BEACON: показать/скрыть окно констелляции+бюллетеня (диагностика декодера).
|
||
// Лок не трогает — он на ЛКМ.
|
||
begin
|
||
if Button <> mbRight then Exit;
|
||
if not FController.BeaconLockEnabled then Exit;
|
||
if FBeaconScopeForm = nil then
|
||
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
|
||
FBeaconScopeForm.ApplyTheme(DarkTheme);
|
||
if FBeaconScopeForm.Visible then FBeaconScopeForm.Hide
|
||
else FBeaconScopeForm.Show;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateBeaconButton;
|
||
// Beacon-маркеры + кнопка BEACON. Кнопка видима только на Pluto в трансвертере
|
||
// (см. rfXvtr) — это ПРОСТО кнопка: подпись 'BEACON', подсветка = лок включён.
|
||
// Вся инфа (lock/коррекция/поиск/«click») — в статус-баре, поле 7 (вместо PLL),
|
||
// см. UpdateStatusField7. На openHPSDR кнопка скрыта, beacon-маркеры неактивны.
|
||
begin
|
||
// Маркеры на спектре: опорная частота (зелёная) + отслеживаемый центроид (оранж).
|
||
if FSpecView <> nil then
|
||
begin
|
||
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
||
FController.BeaconRefHz,
|
||
FController.BeaconTrackedFreqHz);
|
||
// Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка).
|
||
FSpecView.SetBeaconDecMarker(
|
||
FController.BeaconLockEnabled and (FController.BeaconDecodeFreqHz > 0),
|
||
FController.BeaconDecodeFreqHz, 450.0);
|
||
end;
|
||
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
|
||
if BtnBeacon = nil then Exit;
|
||
BtnBeacon.Caption := 'BEACON';
|
||
StyleButton(BtnBeacon, FController.BeaconLockEnabled);
|
||
BtnBeacon.Repaint;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyDUP(Active: Boolean);
|
||
begin
|
||
// Единый путь: контроллер ставит FDisplayDuplex + KeepRXDuringTX (живьём) и шлёт
|
||
// rfDuplex → там рендер кнопки/дисплея. Так тумблер и QO-100-авто согласованы
|
||
// (раньше кнопка писала FDisplayDuplex напрямую, минуя KeepRXDuringTX и rfDuplex).
|
||
FController.SetDuplex(Active);
|
||
end;
|
||
|
||
procedure TMainForm.BtnDUPClick(Sender: TObject);
|
||
begin
|
||
ApplyDUP(not FController.FDisplayDuplex);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyTUN(Active: Boolean);
|
||
begin
|
||
// FWebClientActive держит web-слой актуальным (OnClientActiveChanged).
|
||
FController.SetTune(Active); // ядро TUN (safety/тон/drive/MOX) + рендер через rfTuning
|
||
end;
|
||
|
||
procedure TMainForm.BtnTUNClick(Sender: TObject);
|
||
begin
|
||
ApplyTUN(not FController.FTuning);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Клик/драг по спектру/водопаду
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Перевод X пикселя в частоту
|
||
function PixelToFreq(PixelX, PanelWidth: Integer;
|
||
CenterFreq, SpanHz: Double): Double;
|
||
begin
|
||
Result := CenterFreq + (PixelX / PanelWidth - 0.5) * SpanHz;
|
||
end;
|
||
|
||
procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
ClickFreq: Int64;
|
||
StepHz: Int64;
|
||
begin
|
||
if PanelWidth <= 0 then Exit;
|
||
if (FController.FMode = MODE_FM) and FController.FFMStepOn then
|
||
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
|
||
else
|
||
StepHz := 100;
|
||
ClickFreq := Round(FController.FCenterFreq + (PixelX / PanelWidth - 0.5) * FController.FSpanHz);
|
||
ClickFreq := ((ClickFreq + StepHz div 2) div StepHz) * StepHz;
|
||
if FController.FActiveVfo = 0 then
|
||
ApplyVfoA(ClickFreq)
|
||
else
|
||
FController.SetVfoB(ClickFreq); // движок + рендер через OnControllerState(rfVfoB)
|
||
end;
|
||
|
||
procedure TMainForm.DoWidebandClick(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
FreqHz: Double;
|
||
ClickFreq: Int64;
|
||
StepHz: Int64;
|
||
BandIdx: Integer;
|
||
WasCTun: Boolean;
|
||
begin
|
||
if (FWidebandView = nil) or
|
||
(not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit;
|
||
|
||
if (FController.FMode = MODE_FM) and FController.FFMStepOn then
|
||
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
|
||
else
|
||
StepHz := 100;
|
||
ClickFreq := Round(FreqHz);
|
||
ClickFreq := ((ClickFreq + StepHz div 2) div StepHz) * StepHz;
|
||
|
||
if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then
|
||
begin
|
||
if ClickFreq < FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin then
|
||
ClickFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin);
|
||
if ClickFreq > FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd then
|
||
ClickFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd);
|
||
end;
|
||
|
||
if FController.FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(ClickFreq, FController.IsPluto);
|
||
if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then
|
||
begin
|
||
if FController.FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FController.FSettings.Save;
|
||
end;
|
||
FController.FCurrentBand := BandIdx;
|
||
RestoreBand(FController.FCurrentBand);
|
||
end;
|
||
end;
|
||
|
||
if FController.FActiveVfo = 0 then
|
||
begin
|
||
WasCTun := FController.FCTun;
|
||
FController.FCTun := False;
|
||
ApplyVfoA(ClickFreq);
|
||
FController.FCTun := WasCTun;
|
||
if WasCTun and FController.FWDSPReady then
|
||
FController.FDSPEngine.SetShift(0.0);
|
||
end
|
||
else
|
||
begin
|
||
FController.FVfoB := ClickFreq;
|
||
FreqDispB.Frequency := Round(FController.FVfoB);
|
||
FController.FCenterFreq := ClickFreq;
|
||
if FController.FWDSPReady then FController.FDSPEngine.SetShift(0.0);
|
||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||
begin
|
||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq),
|
||
XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True);
|
||
FController.FNetwork.SendFullHP;
|
||
end;
|
||
if FController.FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FController.FVfoB, FController.IsPluto);
|
||
if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then
|
||
begin
|
||
if FController.FCurrentBand >= 0 then
|
||
StyleButton(BtnBand[FController.FCurrentBand], False);
|
||
FController.FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FController.FCurrentBand], True);
|
||
end;
|
||
end;
|
||
UpdateVfoDisplay;
|
||
SyncSpecViewFreq;
|
||
FSpectrumDirty := True;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
|
||
FSpecView.InvalidateRulerCache;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
FWaterfallDirty := True;
|
||
if PbWaterfall <> nil then PbWaterfall.Invalidate;
|
||
if UpdateWidebandFrequencyView and (PbWideband <> nil) then
|
||
PbWideband.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
dPix: Integer;
|
||
dFreq: Double;
|
||
begin
|
||
if PanelWidth <= 0 then Exit;
|
||
dPix := PixelX - FSpecDragX0;
|
||
dFreq := dPix / PanelWidth * FController.FSpanHz;
|
||
if FController.FCTun then
|
||
// CTUN ON: drag двигает окно просмотра (DDC-центр) без смены VFO; shift,
|
||
// сеть и рендер — в контроллере SetCenter / OnControllerState(rfCenterFreq).
|
||
FController.SetCenter(FSpecDragFreq - dFreq)
|
||
else
|
||
begin
|
||
// CTUN OFF: drag двигает VFO, FController.FCenterFreq следует через Apply
|
||
if FController.FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FSpecDragFreq - dFreq))
|
||
else
|
||
FController.SetVfoB(FSpecDragFreq - dFreq); // движок + рендер (rfVfoB)
|
||
end;
|
||
end;
|
||
|
||
// Спектр
|
||
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Assigned(FVfoOverlay) and
|
||
FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||
if Assigned(FSampleRateOverlay) and
|
||
FSampleRateOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||
|
||
if Button = mbLeft then
|
||
begin
|
||
// Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
|
||
// либо Shift+ЛКМ в любой момент при включённом локе для перенаведения). Декодер
|
||
// наводится на эту частоту, дальше сам ведёт несущую. Не трогает VFO.
|
||
if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
|
||
(FBeaconArming or (ssShift in Shift)) then
|
||
begin
|
||
FController.BeaconSeedAtHz(
|
||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
||
FBeaconArming := False;
|
||
UpdateBeaconButton;
|
||
Exit;
|
||
end;
|
||
// Левая кнопка: сбрасываем маркер если активен
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerActive := False;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
FSpecDrag := True;
|
||
FSpecDragX0 := X;
|
||
if FController.FCTun then FSpecDragFreq := FController.FCenterFreq
|
||
else begin
|
||
if FController.FActiveVfo = 0 then FSpecDragFreq := FController.FVfoA
|
||
else FSpecDragFreq := FController.FVfoB;
|
||
end;
|
||
PbSpectrum.Cursor := crSizeWE;
|
||
end
|
||
else if Button = mbRight then
|
||
begin
|
||
// Правая кнопка: toggle маркера
|
||
if FSpecView.MarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FSpecView.MarkerX) < 8) then
|
||
begin
|
||
// Клик рядом с текущим маркером — убираем
|
||
FSpecView.MarkerActive := False;
|
||
end
|
||
else
|
||
begin
|
||
// Ставим маркер в новое место
|
||
FSpecView.MarkerActive := True;
|
||
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
|
||
end;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWidebandMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
DoWidebandClick(X, TControl(Sender).Width);
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
begin
|
||
if Assigned(FSampleRateOverlay) then
|
||
begin
|
||
FSampleRateOverlay.HandleMouseMove(X, Y);
|
||
end;
|
||
if Assigned(FVfoOverlay) then
|
||
FVfoOverlay.HandleMouseMove(X, Y);
|
||
if not FSpecDrag then
|
||
begin
|
||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||
PbSpectrum.Cursor := crHandPoint
|
||
else
|
||
PbSpectrum.Cursor := crDefault;
|
||
end;
|
||
|
||
if FSpecDrag and (ssLeft in Shift) then
|
||
DoSpectrumDrag(X, PbSpectrum.Width);
|
||
// Маркер следует за курсором — обновление произойдёт в ближайшем тике таймера (50ms)
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if (Abs(X - FSpecDragX0) < 4) then
|
||
DoSpectrumClick(X, PbSpectrum.Width); // это был клик, не драг
|
||
FSpecDrag := False;
|
||
PbSpectrum.Cursor := crDefault;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumMouseLeave(Sender: TObject);
|
||
begin
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.HandleMouseLeave;
|
||
if Assigned(FVfoOverlay) then
|
||
FVfoOverlay.HandleMouseLeave;
|
||
if not FSpecDrag then PbSpectrum.Cursor := crDefault;
|
||
end;
|
||
|
||
// Водопад (аналогично)
|
||
procedure TMainForm.PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerActive := False;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
FSpecDrag := True;
|
||
FSpecDragX0 := X;
|
||
if FController.FCTun then FSpecDragFreq := FController.FCenterFreq
|
||
else begin
|
||
if FController.FActiveVfo = 0 then FSpecDragFreq := FController.FVfoA
|
||
else FSpecDragFreq := FController.FVfoB;
|
||
end;
|
||
PbWaterfall.Cursor := crSizeWE;
|
||
end
|
||
else if Button = mbRight then
|
||
begin
|
||
if FSpecView.MarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FSpecView.MarkerX) < 8) then
|
||
FSpecView.MarkerActive := False
|
||
else
|
||
begin
|
||
FSpecView.MarkerActive := True;
|
||
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
end;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
begin
|
||
if FSpecDrag and (ssLeft in Shift) then
|
||
DoSpectrumDrag(X, PbWaterfall.Width);
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if (Abs(X - FSpecDragX0) < 4) then
|
||
DoSpectrumClick(X, PbWaterfall.Width);
|
||
FSpecDrag := False;
|
||
PbWaterfall.Cursor := crDefault;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnBandClick(Sender: TObject);
|
||
var
|
||
NewBand: Integer;
|
||
begin
|
||
NewBand := (Sender as TFlatButton).Tag;
|
||
if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit;
|
||
// Если в XVTR-режиме — DeactivateXvtr вернёт HF-состояние (RestoreBand
|
||
// на сохранённом FController.FCurrentBand). Если NewBand совпадает с восстановленным
|
||
// FController.FCurrentBand, дальше делать ничего не нужно.
|
||
if FController.FCurrentXvtr >= 0 then
|
||
begin
|
||
DeactivateXvtr;
|
||
if NewBand = FController.FCurrentBand then Exit;
|
||
end
|
||
else if NewBand = FController.FCurrentBand then
|
||
Exit;
|
||
|
||
if FController.FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FController.FSettings.Save;
|
||
end;
|
||
FController.FCurrentBand := NewBand;
|
||
RestoreBand(FController.FCurrentBand);
|
||
end;
|
||
|
||
procedure TMainForm.BtnModeClick(Sender: TObject);
|
||
begin
|
||
// Логика (FMode, дефолтный фильтр, DSP SetMode/TUN/ApplyModeFilter, band cache)
|
||
// — в контроллере; рендер кнопок/фильтра/спектра — в OnControllerState(rfMode).
|
||
FController.SetMode((Sender as TFlatButton).Tag);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||
begin
|
||
// Логика (FFilter/FFilterBW по таблицам режима, ApplyModeFilter, band cache)
|
||
// — в контроллере; рендер — в OnControllerState(rfFilter).
|
||
FController.SetFilterIdx((Sender as TFlatButton).Tag);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// FM CTCSS helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TMainForm.ApplyFMSquelch;
|
||
begin
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetFMSquelch(FController.FFMSQOn and (FController.FMode = MODE_FM), FController.FFMSQLevel);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMSQClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetFMSquelch(not FController.FFMSQOn); // DSP + OnControllerState
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.TrkFMSQChange(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetFMSquelchLevel(TrkFMSQ.Position);
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.CloseCTCSSPopup;
|
||
begin
|
||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
|
||
CloseFMStepPopup;
|
||
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMRptMinusClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
// Toggle решает UI; команда несёт сетевой сдвиг + рендер кнопок (OnControllerState).
|
||
if FController.FFMRptDir = RPT_MINUS then
|
||
FController.SetFMRpt(RPT_NONE)
|
||
else
|
||
FController.SetFMRpt(RPT_MINUS);
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMRptPlusClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
if FController.FFMRptDir = RPT_PLUS then
|
||
FController.SetFMRpt(RPT_NONE)
|
||
else
|
||
FController.SetFMRpt(RPT_PLUS);
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.EdFMRptOffsetChange(Sender: TObject);
|
||
var Hz: Double;
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
Hz := RptParseOffset(EdFMRptOffset.Text);
|
||
if Hz > 0.0 then
|
||
begin
|
||
FController.SetFMRptOffset(Hz); // применяет, если RPT вкл; OnControllerState
|
||
if FController.FFMRptDir <> RPT_NONE then SaveCurrentBand;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateRptAutoState;
|
||
begin
|
||
// Логика авто-MINUS — в контроллере; рендер кнопок — OnControllerState(rfFMRpt).
|
||
FController.UpdateRptAutoState;
|
||
end;
|
||
|
||
procedure TMainForm.CloseFMStepPopup;
|
||
begin
|
||
if FStepDropDown <> nil then FStepDropDown.ClosePopup;
|
||
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
|
||
end;
|
||
|
||
procedure TMainForm.SetFMStep(Idx: Integer);
|
||
begin
|
||
// Тонкая обёртка: команда контроллера обновит UI/web/спецвью через
|
||
// OnControllerState(rfFMStepIdx). Сохранена для существующих вызовов
|
||
// (RestoreBand, dropdown-select, web-sync, XVTR-restore).
|
||
FController.SetFMStep(Idx);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMStepClick(Sender: TObject);
|
||
begin
|
||
FController.FFMStepOn := not FController.FFMStepOn;
|
||
StyleButton(BtnFMStep, FController.FFMStepOn);
|
||
BtnFMStep.Repaint;
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMStepSelClick(Sender: TObject);
|
||
begin
|
||
FCTCSSDropDown.ClosePopup;
|
||
FStepDropDown.Toggle;
|
||
end;
|
||
|
||
procedure TMainForm.OnStepDropDownSelect(Sender: TObject; Idx: Integer);
|
||
begin
|
||
SetFMStep(Idx);
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Channels dropdown / store
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TMainForm.RefreshChannelsDropDown;
|
||
var
|
||
names: array of string;
|
||
i: Integer;
|
||
begin
|
||
if (FController.FChannelStore = nil) or (FChannelsDropDown = nil) then Exit;
|
||
SetLength(names, FController.FChannelStore.Count);
|
||
for i := 0 to FController.FChannelStore.Count - 1 do
|
||
names[i] := FController.FChannelStore.GetChannel(i).Name;
|
||
FChannelsDropDown.SetItems(names);
|
||
if (FChannelController.ActiveIdx >= 0) and (FChannelController.ActiveIdx < FController.FChannelStore.Count) then
|
||
FChannelsDropDown.SetItemIndex(FChannelController.ActiveIdx)
|
||
else if BtnChannels <> nil then
|
||
BtnChannels.Caption := 'Channel';
|
||
end;
|
||
|
||
procedure TMainForm.OnChannelStoreChanged(Sender: TObject);
|
||
begin
|
||
RefreshChannelsDropDown;
|
||
end;
|
||
|
||
procedure TMainForm.BtnChannelsClick(Sender: TObject);
|
||
begin
|
||
if FController.FChannelStore.Count = 0 then Exit;
|
||
FCTCSSDropDown.ClosePopup;
|
||
FStepDropDown.ClosePopup;
|
||
FChannelsDropDown.Toggle;
|
||
end;
|
||
|
||
procedure TMainForm.BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
Def: TChannel;
|
||
begin
|
||
if Button <> mbRight then Exit;
|
||
FChannelsDropDown.ClosePopup;
|
||
if FChannelsForm = nil then
|
||
FChannelsForm := TChannelsForm.Create(Self, FController.FChannelStore, OnChannelStoreChanged);
|
||
// Передаём текущее состояние радио как значения по умолчанию для нового канала
|
||
TChannelStore.DefaultChannel(Def);
|
||
Def.RXFreq := FController.FVfoA;
|
||
Def.TXFreq := ActiveTXFreqHz;
|
||
Def.Mode := FController.FMode;
|
||
Def.RptDir := FController.FFMRptDir;
|
||
Def.RptOffsetHz := FController.FFMRptOffsetHz;
|
||
Def.CTCSSOn := FController.FFMCTCSSOn;
|
||
Def.CTCSSToneIdx := FController.FFMCTCSSToneIdx;
|
||
Def.Power := 100;
|
||
Def.Name := '';
|
||
TChannelsForm(FChannelsForm).SetCurrentDefaults(Def);
|
||
TChannelsForm(FChannelsForm).ApplyTheme(DarkTheme);
|
||
TChannelsForm(FChannelsForm).Show;
|
||
end;
|
||
|
||
procedure TMainForm.OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
|
||
begin
|
||
// Логика применения канала (band/XVTR/mode/FM/drive/freq) — в адаптере каналов;
|
||
// рендер кнопки Channel — в OnChannelActiveChanged.
|
||
FChannelController.Select(Idx);
|
||
end;
|
||
|
||
procedure TMainForm.OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
|
||
begin
|
||
// Рендер кнопки Channel по событию адаптера каналов (Active=True → подсветка+имя).
|
||
if BtnChannels = nil then Exit;
|
||
StyleButton(BtnChannels, Active);
|
||
if Active then BtnChannels.Caption := Name
|
||
else BtnChannels.Caption := 'Channel';
|
||
BtnChannels.Repaint;
|
||
end;
|
||
|
||
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
|
||
begin
|
||
// Тонкая обёртка: команда применяет к DSP, OnControllerState(rfFMCTCSSTone)
|
||
// обновит выпадающий список. Сохранена для вызовов из restore/channel/XVTR.
|
||
FController.SetFMCTCSSTone(Idx);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMCTCSSClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.FFMCTCSSAutoActive := False; // ручное действие снимает авто-флаг
|
||
FController.SetFMCTCSS(not FController.FFMCTCSSOn);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMCTCSSToneClick(Sender: TObject);
|
||
begin
|
||
FStepDropDown.ClosePopup;
|
||
FCTCSSDropDown.Toggle;
|
||
end;
|
||
|
||
procedure TMainForm.OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer);
|
||
begin
|
||
SetFMCTCSSTone(Idx);
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
|
||
begin
|
||
// Своп + перестройка — в контроллере; рендер дисплеев/спектра/band — через
|
||
// OnControllerState(rfVfoA/rfVfoB/rfBand). Кэш линейки сбрасываем здесь
|
||
// (большой скачок частоты — позиции тиков меняются).
|
||
FController.VfoSwap;
|
||
FSpecView.InvalidateRulerCache;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoACopyBClick(Sender: TObject);
|
||
begin
|
||
// A>B: B получает частоту A. Команда сохраняет B, перестраивает если B
|
||
// активен, рендерит через OnControllerState(rfVfoB).
|
||
FController.VfoACopyB;
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoBCopyAClick(Sender: TObject);
|
||
begin
|
||
// B>A: A получает частоту B. При активном A идём через ApplyVfoA (сохраняем
|
||
// канальную оркестрацию); иначе только сохраняем + рендер дисплея.
|
||
FController.FVfoA := FController.FVfoB;
|
||
if FController.FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FController.FVfoA))
|
||
else
|
||
FreqDispA.Frequency := Round(FController.FVfoA);
|
||
end;
|
||
|
||
function TMainForm.ActiveTXFreqHz: Double;
|
||
begin
|
||
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
|
||
Result := FController.ActiveTXFreqHz;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyMOX(Active: Boolean);
|
||
begin
|
||
// FWebClientActive держит web-слой актуальным (OnClientActiveChanged) —
|
||
// определяет mic-source при TX-on.
|
||
FController.SetMOX(Active); // ядро TX (safety/PTT/DSP) + рендер через rfTransmitting
|
||
end;
|
||
|
||
procedure TMainForm.BtnMOXClick(Sender: TObject);
|
||
begin
|
||
// Если идёт tune — клик MOX просто выходит из tune (без перехода на голос).
|
||
if FController.FTuning then
|
||
begin
|
||
ApplyTUN(False);
|
||
Exit;
|
||
end;
|
||
ApplyMOX(not FController.FTransmitting);
|
||
end;
|
||
|
||
procedure TMainForm.BtnMuteClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.ToggleMute; // логика (движок) + OnControllerState обновит кнопку
|
||
end;
|
||
|
||
procedure TMainForm.UpdateNRButton;
|
||
begin
|
||
case FController.FNRMode of
|
||
1: BtnNR.Caption := 'NR';
|
||
2: BtnNR.Caption := 'NR2';
|
||
3: BtnNR.Caption := 'NR3';
|
||
4: BtnNR.Caption := 'NR4';
|
||
else
|
||
BtnNR.Caption := 'NR';
|
||
end;
|
||
StyleButton(BtnNR, FController.FNRMode <> 0);
|
||
BtnNR.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateNBButton;
|
||
begin
|
||
case FController.FNBMode of
|
||
1: BtnNB.Caption := 'NB';
|
||
2: BtnNB.Caption := 'NB2';
|
||
else
|
||
BtnNB.Caption := 'NB';
|
||
end;
|
||
StyleButton(BtnNB, FController.FNBMode <> 0);
|
||
BtnNB.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateSNBButton;
|
||
begin
|
||
BtnSNB.Caption := 'SNB';
|
||
StyleButton(BtnSNB, FController.FSNB);
|
||
BtnSNB.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateANFButton;
|
||
begin
|
||
BtnANF.Caption := 'ANF';
|
||
StyleButton(BtnANF, FController.FANF);
|
||
BtnANF.Invalidate;
|
||
end;
|
||
|
||
// ApplyNoiseFilterButtonsToDSP переехал в TRadioController (NR/NB/SNB/ANF → WDSP).
|
||
|
||
procedure TMainForm.BtnNRClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetNR((FController.FNRMode + 1) mod 5);
|
||
end;
|
||
|
||
procedure TMainForm.BtnNBClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetNB((FController.FNBMode + 1) mod 3);
|
||
end;
|
||
|
||
procedure TMainForm.BtnSNBClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetSNB(not FController.FSNB);
|
||
end;
|
||
|
||
procedure TMainForm.BtnANFClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetANF(not FController.FANF);
|
||
end;
|
||
|
||
|
||
procedure TMainForm.ApplyWfAGCNF(WfAGC, WfNF: Boolean);
|
||
begin
|
||
FController.SetWfAGC(WfAGC);
|
||
FController.SetWfNF(WfNF);
|
||
end;
|
||
|
||
procedure TMainForm.ApplyADCSettings(Dither, Random: Boolean);
|
||
begin
|
||
FController.FDitherEnabled := Dither;
|
||
FController.FRandomEnabled := Random;
|
||
if FController.FNetwork.Connected and FController.FRunning then
|
||
FController.FNetwork.ConfigureDDCs(1, FController.FSampleRate div 1000, 0, FController.FDitherEnabled, FController.FRandomEnabled);
|
||
if FController.FDevConnected then
|
||
FController.SaveGlobalSettings;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyWebSettings(Enabled: Boolean; Port: Integer;
|
||
const BindAddr, User, Pass: string);
|
||
var W: TWebSettings;
|
||
begin
|
||
FWebEnabled := Enabled;
|
||
FWebPort := Port;
|
||
FWebBindAddr := BindAddr;
|
||
FWebUser := User;
|
||
FWebPass := Pass;
|
||
FWebServer.Reconfigure(User, Pass, BindAddr, Word(Port));
|
||
if Enabled then FWebServer.Start;
|
||
W.Enabled := Enabled;
|
||
W.Port := Port;
|
||
W.BindAddr := BindAddr;
|
||
W.User := User;
|
||
W.Pass := Pass;
|
||
FController.FSettings.SaveWebSettings(W);
|
||
end;
|
||
|
||
procedure TMainForm.OnSampleRateHidePanel;
|
||
begin
|
||
FPanelHidden := not FPanelHidden;
|
||
if FPanelHidden then
|
||
begin
|
||
FLeftPanelW := PanelLeft.Width; // сохраняем реальную ширину (учитывает DPI на Win32)
|
||
PanelLeft.Width := 0;
|
||
FVfoOverlay.Width := 260;
|
||
FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера
|
||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||
FVfoOverlay.Visible := True;
|
||
PositionVfoOverlay;
|
||
end
|
||
else
|
||
begin
|
||
PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину
|
||
FVfoOverlay.HandleMouseLeave;
|
||
FVfoOverlay.Visible := False;
|
||
end;
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
|
||
ResizeSpectrumPanels;
|
||
ResizeSMeter;
|
||
end;
|
||
|
||
procedure TMainForm.PositionVfoOverlay;
|
||
var
|
||
VfoX, FilterEndX, OW, NewLeft, NewTop: Integer;
|
||
HiHz: Double;
|
||
begin
|
||
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
||
OW := FVfoOverlay.Width;
|
||
|
||
if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then
|
||
begin
|
||
VfoX := Round((FController.FVfoA - FController.FCenterFreq + FController.FSpanHz / 2) / FController.FSpanHz * PbSpectrum.Width)
|
||
end
|
||
else
|
||
VfoX := PbSpectrum.Width div 2;
|
||
|
||
case FController.FMode of
|
||
MODE_LSB: HiHz := -100;
|
||
MODE_USB: HiHz := FController.FFilterBW;
|
||
else
|
||
HiHz := FController.FFilterBW / 2;
|
||
end;
|
||
if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then
|
||
FilterEndX := VfoX + Round(HiHz / FController.FSpanHz * PbSpectrum.Width)
|
||
else
|
||
FilterEndX := VfoX;
|
||
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
||
|
||
// Всегда справа от правого края полосы фильтра.
|
||
NewLeft := Max(4, Min(PbSpectrum.Width - OW - 4, FilterEndX + 6));
|
||
NewTop := 16;
|
||
if (FVfoOverlay.Left <> NewLeft) or (FVfoOverlay.Top <> NewTop) then
|
||
begin
|
||
FVfoOverlay.Left := NewLeft;
|
||
FVfoOverlay.Top := NewTop;
|
||
OnVfoOverlayInvalidate(FVfoOverlay);
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
|
||
begin
|
||
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
|
||
end;
|
||
|
||
procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
FController.FMode := Mode;
|
||
FController.FFilterBW := FilterBW;
|
||
if Mode = MODE_FM then
|
||
begin
|
||
FController.FFilter := FILT_FM_DEF;
|
||
for Idx := 0 to FILT_FM_COUNT - 1 do
|
||
if FILT_FM_BW[Idx] = FilterBW then
|
||
begin
|
||
FController.FFilter := Idx;
|
||
FController.FFMDeviation := FILT_FM_DEV[Idx];
|
||
Break;
|
||
end;
|
||
end;
|
||
if FController.FWDSPReady then
|
||
begin
|
||
FController.FDSPEngine.SetMode(FController.FMode);
|
||
FController.ApplyModeFilter;
|
||
end;
|
||
SyncSpecViewFreq;
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||
begin
|
||
// Команды контроллера применяют к DSP и через OnControllerState обновляют кнопки.
|
||
FController.SetNR(NRMode);
|
||
FController.SetNB(NBMode);
|
||
FController.SetSNB(SNBOn);
|
||
FController.SetANF(ANFOn);
|
||
end;
|
||
|
||
procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer);
|
||
begin
|
||
FController.SetAGCMode(AGCMode);
|
||
end;
|
||
|
||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||
begin
|
||
FSpectrumDirty := True;
|
||
if Assigned(FSpecView) then
|
||
begin
|
||
FSpecView.SpectrumDirty := True;
|
||
FSpecView.InvalidateOverlayCache;
|
||
end;
|
||
if Assigned(PbSpectrum) then PbSpectrum.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
||
begin
|
||
// Тонкая обёртка: движок (пересоздание WDSP-канала, DDC-rate, DSP-restore) +
|
||
// персист — в команде SetSampleRate; рендер overlay/AGC — OnControllerState.
|
||
FController.SetSampleRate(SampleRate);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока)
|
||
// Все изменения состояния выполняем через TThread.Synchronize чтобы
|
||
// не трогать UI и WDSP из чужого потока.
|
||
// ===========================================================================
|
||
|
||
// Mode/Filter/AGC/AGCTop/Volume/Drive/Span/Band/XvtrBand/Freq/FreqA/FreqB/Center/
|
||
// WfAGC/WfNF/Mute/NR/NB/SNB/ANF/Attn/ClientActiveChanged перенесены в TWebAdapter
|
||
// (чистые команды контроллера).
|
||
|
||
// ── Run ────────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnRun(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebRun;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebRun;
|
||
begin
|
||
// set_run отвязан от десктопного модального диалога: web стартует через connect
|
||
// (overlay), а set_run:true без выбранного устройства бессмысленно. Здесь только
|
||
// STOP-путь (set_run:false), симметрично web-disconnect.
|
||
if (not FWebSyncBool) and FController.FNetwork.Connected then
|
||
DoStopAndDisconnect;
|
||
end;
|
||
|
||
// ── Web discovery / connect / disconnect / saved-CRUD ───────────────────────
|
||
|
||
procedure TMainForm.WebOnConnect(const IP: string);
|
||
begin
|
||
FWebSyncStr1 := IP;
|
||
FWebSyncM := SyncWebConnect;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebConnect;
|
||
var
|
||
Dev: THPSDRDevice;
|
||
i: Integer;
|
||
begin
|
||
if FController.FNetwork.Connected then Exit; // уже подключены
|
||
FillChar(Dev, SizeOf(Dev), 0);
|
||
Dev.IPAddress := FWebSyncStr1;
|
||
// BoardType (тип платы не определяется при Connect — только из discovery/saved):
|
||
// сначала найденное устройство (есть и MAC для preload), иначе сохранённое по IP
|
||
// — как десктоп берёт FPendingBoardType из GetSavedBoardType.
|
||
i := FController.FDeviceStore.FindDiscoveredByIP(FWebSyncStr1);
|
||
if i >= 0 then
|
||
Dev.BoardType := FController.FDeviceStore.Discovered(i).BoardType
|
||
else
|
||
for i := 0 to FController.FDeviceStore.SavedCount - 1 do
|
||
if FController.FDeviceStore.Saved(i).IPAddress = FWebSyncStr1 then
|
||
begin
|
||
Dev.BoardType := FController.FDeviceStore.Saved(i).BoardType;
|
||
Break;
|
||
end;
|
||
StartWithDevice(Dev);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnDiscover;
|
||
begin
|
||
FWebSyncM := SyncWebDiscover;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebDiscover;
|
||
begin
|
||
SetStatusText(2, 'Discovering...');
|
||
FController.Discover; // rfDeviceList-рендер запушит обновлённый список в web
|
||
end;
|
||
|
||
procedure TMainForm.WebOnDisconnect;
|
||
begin
|
||
FWebSyncM := SyncWebDisconnect;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebDisconnect;
|
||
begin
|
||
if FController.FNetwork.Connected then
|
||
DoStopAndDisconnect;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnDevAdd(const AName, IP: string);
|
||
begin
|
||
FWebSyncStr1 := AName;
|
||
FWebSyncStr2 := IP;
|
||
FWebSyncM := SyncWebDevAdd;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebDevAdd;
|
||
begin
|
||
FController.AddSavedDevice(FWebSyncStr1, FWebSyncStr2); // Pluto по схеме URI
|
||
PushDeviceListToWeb;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnDevRemove(Idx: Integer);
|
||
begin
|
||
FWebSyncInt := Idx;
|
||
FWebSyncM := SyncWebDevRemove;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebDevRemove;
|
||
begin
|
||
FController.FDeviceStore.RemoveSaved(FWebSyncInt);
|
||
PushDeviceListToWeb;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnDevAutoStart(Idx: Integer);
|
||
begin
|
||
FWebSyncInt := Idx;
|
||
FWebSyncM := SyncWebDevAutoStart;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebDevAutoStart;
|
||
begin
|
||
FController.FDeviceStore.SetAutoStart(FWebSyncInt);
|
||
PushDeviceListToWeb;
|
||
end;
|
||
|
||
procedure TMainForm.PushDeviceListToWeb;
|
||
// store (saved+discovered) → FWebServer.SetDeviceList. Saved идут первыми, поэтому
|
||
// их позиция в массиве == saved-индекс (web шлёт его в dev_remove/dev_autostart).
|
||
var
|
||
Arr: TWebDeviceArray;
|
||
i, k: Integer;
|
||
D: TDiscoveredDevice;
|
||
Sv: TSavedDevice;
|
||
begin
|
||
if FWebServer = nil then Exit;
|
||
SetLength(Arr, FController.FDeviceStore.SavedCount +
|
||
FController.FDeviceStore.DiscoveredCount);
|
||
k := 0;
|
||
for i := 0 to FController.FDeviceStore.SavedCount - 1 do
|
||
begin
|
||
Sv := FController.FDeviceStore.Saved(i);
|
||
Arr[k].Name := Sv.Name;
|
||
Arr[k].IP := Sv.IPAddress;
|
||
Arr[k].BoardType := Sv.BoardType;
|
||
Arr[k].IsSaved := True;
|
||
Arr[k].AutoStart := Sv.AutoStart;
|
||
Inc(k);
|
||
end;
|
||
for i := 0 to FController.FDeviceStore.DiscoveredCount - 1 do
|
||
begin
|
||
D := FController.FDeviceStore.Discovered(i);
|
||
Arr[k].Name := D.DisplayName;
|
||
Arr[k].IP := D.IPAddress;
|
||
Arr[k].BoardType := D.BoardType;
|
||
Arr[k].IsSaved := False;
|
||
Arr[k].AutoStart := False;
|
||
Inc(k);
|
||
end;
|
||
FWebServer.SetDeviceList(Arr);
|
||
end;
|
||
|
||
// ── CTUN ──────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.TrkDriveChange(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit; // программная установка из OnControllerState — не команда
|
||
FController.SetDrive(TrkDrive.Position); // rfDrive-рендер снимет память канала (OnDriveChanged)
|
||
end;
|
||
|
||
procedure TMainForm.UpdateDriveLabel;
|
||
// Значение рядом с ползунком drive: openHPSDR — проценты, Pluto — TX-аттенюация
|
||
// в дБ (берём из контроллера; маппинг drive%→дБ — там же).
|
||
begin
|
||
if LblDrvVal = nil then Exit;
|
||
if FController.IsPluto then
|
||
LblDrvVal.Caption := Format('%.0fdB', [FController.PlutoTxAttForDrive])
|
||
else
|
||
LblDrvVal.Caption := Format('%d%%', [FController.FDrivePercent]);
|
||
end;
|
||
|
||
function TMainForm.CalcDriveByte: Byte;
|
||
begin
|
||
// Формула — в контроллере (чистая над состоянием). Делегат для существующих
|
||
// вызовов MainForm.
|
||
Result := FController.CalcDriveByte;
|
||
end;
|
||
|
||
procedure TMainForm.OnPASettingsChange(MaxPower: Double;
|
||
const BandCal: array of Double);
|
||
var i: Integer;
|
||
begin
|
||
FController.FPAMaxPower := MaxPower;
|
||
if FSpecView <> nil then FSpecView.PAMaxPower := MaxPower;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
begin
|
||
if i <= High(BandCal) then
|
||
FController.FPABandCal[i] := BandCal[i]
|
||
else
|
||
FController.FPABandCal[i] := 100.0;
|
||
end;
|
||
FController.FDriveLevel := CalcDriveByte;
|
||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True);
|
||
end;
|
||
|
||
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
||
begin
|
||
// Логика (clamp/apply/persist) — в контроллере; UI лишь передаёт значение.
|
||
FController.SetPlutoTxMaxAtt(MaxAttDb);
|
||
UpdateDriveLabel; // изменился потолок → пересчитать дБ-подпись drive
|
||
end;
|
||
|
||
procedure TMainForm.OnVHFCalSettingsChange(const VHFCal: array of Double);
|
||
var i: Integer;
|
||
begin
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
if i <= High(VHFCal) then
|
||
FController.FVHFBandCal[i] := VHFCal[i]
|
||
else
|
||
FController.FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
|
||
end;
|
||
FController.FDriveLevel := CalcDriveByte;
|
||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True);
|
||
end;
|
||
|
||
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
||
// Любая правка в Settings→Transmit летит сюда: обновляем FController.FTXSettings,
|
||
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
|
||
var
|
||
OldMicByte, NewMicByte: Byte;
|
||
OldAtt, OldTUNLevel, OldTUNFreq: Integer;
|
||
GridChanged: Boolean;
|
||
begin
|
||
OldMicByte := FController.BuildMicLineSelectByte;
|
||
OldAtt := FController.FTXSettings.AttOnTX;
|
||
OldTUNLevel := FController.FTXSettings.TUNLevel;
|
||
OldTUNFreq := FController.FTXSettings.TUNFreq;
|
||
GridChanged := (FController.FTXSpecRefLevel <> T.TXSpecRefLevel)
|
||
or (FController.FTXSpecRange <> T.TXSpecRange)
|
||
or (FController.FTXSpecGridStep <> T.TXSpecGridStep);
|
||
FController.FTXSettings := T;
|
||
NewMicByte := FController.BuildMicLineSelectByte;
|
||
// TX-grid в момент правки — обновляем рабочие поля и (если на передаче) пушим в FSpecView
|
||
FController.FTXSpecRefLevel := FController.FTXSettings.TXSpecRefLevel;
|
||
FController.FTXSpecRange := FController.FTXSettings.TXSpecRange;
|
||
if FController.FTXSettings.TXSpecGridStep > 0 then
|
||
FController.FTXSpecGridStep := FController.FTXSettings.TXSpecGridStep;
|
||
if GridChanged and FController.FTransmitting then
|
||
ApplySpecViewGridFromState;
|
||
FController.ApplyTXSettingsToDSP;
|
||
if (OldMicByte <> NewMicByte) or (OldAtt <> FController.FTXSettings.AttOnTX) then
|
||
FController.SendDUCSpecificFromSettings;
|
||
// TUN-параметры live: если идёт tune — обновить тон/drive, иначе просто запомнить.
|
||
if FController.FTuning then
|
||
begin
|
||
if (OldTUNFreq <> FController.FTXSettings.TUNFreq) and FController.FWDSPReady then
|
||
FController.FDSPEngine.SetTXTone(True, FController.FTXSettings.TUNFreq, 1.0);
|
||
if OldTUNLevel <> FController.FTXSettings.TUNLevel then
|
||
begin
|
||
FController.FDriveLevel := CalcDriveByte;
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||
begin
|
||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
||
FController.FTransmitting, True, True);
|
||
FController.FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
end;
|
||
// Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json
|
||
if FController.FDevConnected then
|
||
begin
|
||
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
|
||
FController.FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings);
|
||
begin
|
||
FController.FAlexSettings := A;
|
||
if FController.FNetwork.Connected then
|
||
FController.FNetwork.SetAlexConfig(A);
|
||
if FController.FDevConnected then
|
||
begin
|
||
FController.FSettings.SaveAlex(FController.FDevMAC, A);
|
||
FController.FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// XVTR (Transverter) integration
|
||
// ===========================================================================
|
||
|
||
// Ищет XVTR слот, в чьё [FreqBegin..FreqEnd] попадает данная видимая частота.
|
||
// Возвращает индекс 0..CFG_XVTR_COUNT-1 или -1.
|
||
function TMainForm.FindXvtrIdxForFreq(FreqHz: Double): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := -1;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
with FController.FXvtrSettings.Entries[i] do
|
||
if Enabled and (FreqHz >= FreqBegin) and (FreqHz <= FreqEnd) then
|
||
Exit(i);
|
||
end;
|
||
|
||
// Транслирует видимую частоту в IF (то, что слышит/передаёт трансивер).
|
||
// Если активный XVTR не задан — возвращает входную частоту без изменений.
|
||
function TMainForm.XvtrTranslate(VisibleHz: Double): Double;
|
||
begin
|
||
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
|
||
Result := FController.XvtrTranslate(VisibleHz);
|
||
end;
|
||
|
||
function TMainForm.XvtrTranslateTX(VisibleHz: Double): Double;
|
||
begin
|
||
// TX-трансляция (QO-100 split-LO); обёртка над контроллером.
|
||
Result := FController.XvtrTranslateTX(VisibleHz);
|
||
end;
|
||
|
||
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
||
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
||
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
||
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis.
|
||
procedure TMainForm.ApplyXvtrToNetwork;
|
||
begin
|
||
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
|
||
FController.ApplyXvtrToNetwork;
|
||
end;
|
||
|
||
// Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые,
|
||
// создаёт новые для enabled слотов в 3-м и 4-м ряду PanelBands.
|
||
procedure TMainForm.RelayoutBelowBands;
|
||
var
|
||
Y: Integer;
|
||
begin
|
||
if PanelBands = nil then Exit;
|
||
Y := PanelBands.Top + PanelBands.Height + 2;
|
||
PanelMode.Top := Y; Y := Y + PanelMode.Height + 2;
|
||
PanelFilter.Top := Y; Y := Y + PanelFilter.Height + 2;
|
||
if (PanelFMStep <> nil) and PanelFMStep.Visible then
|
||
begin
|
||
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
|
||
end;
|
||
if (PanelFMSQ <> nil) and PanelFMSQ.Visible then
|
||
begin
|
||
PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2;
|
||
end;
|
||
if (PanelFMCTCSS <> nil) and PanelFMCTCSS.Visible then
|
||
begin
|
||
PanelFMCTCSS.Top := Y; Y := Y + PanelFMCTCSS.Height + 2;
|
||
end;
|
||
if (PanelFMRpt <> nil) and PanelFMRpt.Visible then
|
||
begin
|
||
PanelFMRpt.Top := Y; Y := Y + PanelFMRpt.Height + 2;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.RebuildXvtrButtons;
|
||
var
|
||
i, Pos, XvtrRows: Integer;
|
||
BtnH, RowH, MaxBottom: Integer;
|
||
B: TFlatButton;
|
||
Cap: string;
|
||
begin
|
||
// Уничтожаем все старые
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if BtnXvtrBand[i] <> nil then
|
||
begin
|
||
BtnXvtrBand[i].Free;
|
||
BtnXvtrBand[i] := nil;
|
||
end;
|
||
if PanelBands = nil then Exit;
|
||
|
||
// Измеряем фактический bottom последнего ряда обычных кнопок BAND
|
||
// (уже отмасштабированы LCL при показе формы)
|
||
MaxBottom := 0;
|
||
BtnH := 0;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
if BtnBand[i] <> nil then
|
||
begin
|
||
if BtnBand[i].Top + BtnBand[i].Height > MaxBottom then
|
||
MaxBottom := BtnBand[i].Top + BtnBand[i].Height;
|
||
BtnH := BtnBand[i].Height;
|
||
end;
|
||
if BtnH = 0 then BtnH := MulDiv(24, Screen.PixelsPerInch, 96);
|
||
RowH := BtnH + 3; // шаг ряда: высота кнопки + 3px зазор (как 27=24+3 при 1x)
|
||
|
||
Pos := 0; // позиция среди enabled слотов
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if FController.FXvtrSettings.Entries[i].Enabled then
|
||
begin
|
||
if Pos >= 12 then Break; // 2 ряда по 6 кнопок
|
||
Cap := FController.FXvtrSettings.Entries[i].ButtonText;
|
||
if Cap = '' then Cap := 'X' + IntToStr(i + 1);
|
||
B := TFlatButton.Create(Self);
|
||
B.Parent := PanelBands;
|
||
B.Caption := Cap;
|
||
B.Left := LeftPanelButtonLeft(PanelBands.Width, 6, Pos mod 6);
|
||
B.Top := MaxBottom + 4 + (Pos div 6) * RowH;
|
||
B.Width := LeftPanelButtonWidth(PanelBands.Width, 6, Pos mod 6);
|
||
B.Height := BtnH;
|
||
B.OnClick := BtnXvtrBandClick;
|
||
B.Tag := -(i + 1); // отрицательное значение = XVTR индекс
|
||
BtnXvtrBand[i] := B;
|
||
StyleButton(B, i = FController.FCurrentXvtr);
|
||
Inc(Pos);
|
||
end;
|
||
// Динамически подгоняем высоту PanelBands под реальное содержимое
|
||
if Pos = 0 then XvtrRows := 0
|
||
else if Pos <= 6 then XvtrRows := 1
|
||
else XvtrRows := 2;
|
||
if XvtrRows = 0 then
|
||
PanelBands.Height := MaxBottom + 6
|
||
else
|
||
PanelBands.Height := MaxBottom + 4 + XvtrRows * RowH + 6;
|
||
RelayoutBelowBands;
|
||
end;
|
||
|
||
// Обработчик клика по XVTR-кнопке. Tag = -(idx+1).
|
||
procedure TMainForm.BtnXvtrBandClick(Sender: TObject);
|
||
var
|
||
TagVal, Idx: Integer;
|
||
begin
|
||
TagVal := (Sender as TFlatButton).Tag;
|
||
Idx := -TagVal - 1;
|
||
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
|
||
if not FController.FXvtrSettings.Entries[Idx].Enabled then Exit;
|
||
if FController.FCurrentXvtr = Idx then Exit; // уже на этом XVTR
|
||
// Сохраняем текущий band перед сменой (как BtnBandClick)
|
||
if FController.FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FController.FSettings.Save;
|
||
end;
|
||
ActivateXvtrBand(Idx);
|
||
end;
|
||
|
||
// Активирует XVTR-band: устанавливает видимую частоту по LastFreq или
|
||
// середине [FreqBegin..FreqEnd], применяет network XVTR-режим.
|
||
procedure TMainForm.ActivateXvtrBand(Idx: Integer);
|
||
begin
|
||
// Тонкий делегат: вход в XVTR-слот + рендер кнопок/частот/спектра/wideband/web —
|
||
// всё в команде/событиях (rfXvtr/rfBand/rfActiveVfo).
|
||
FController.ActivateXvtr(Idx);
|
||
end;
|
||
|
||
// Деактивирует XVTR — возвращает к HF-режиму. FController.FCurrentBand указывает
|
||
// на HF-диапазон, с которого пользователь зашёл в трансвертер. RestoreBand вернёт
|
||
// VFO/Mode/Filter/AGC/CTUN/Span этого диапазона и обновит подсветку HF band-кнопки.
|
||
procedure TMainForm.DeactivateXvtr;
|
||
begin
|
||
// Тонкий делегат: выход в HF (SaveCurrentBand + HF-возврат RestoreBand + рендер
|
||
// кнопок/wideband/web) — всё в команде/событиях.
|
||
FController.DeactivateXvtr;
|
||
end;
|
||
|
||
procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings);
|
||
begin
|
||
FController.FXvtrSettings := X;
|
||
RebuildXvtrButtons;
|
||
// Если активный XVTR стал disabled — выйти из XVTR режима
|
||
if (FController.FCurrentXvtr >= 0) and
|
||
((FController.FCurrentXvtr >= CFG_XVTR_COUNT) or
|
||
(not FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled)) then
|
||
DeactivateXvtr;
|
||
// Перепрошить XVTR в сеть (если параметры текущего изменились)
|
||
ApplyXvtrToNetwork;
|
||
UpdateWidebandFrequencyView;
|
||
if PbWideband <> nil then PbWideband.Invalidate;
|
||
if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate;
|
||
PushXvtrToWeb;
|
||
if FController.FDevConnected then
|
||
begin
|
||
FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings);
|
||
FController.FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PushXvtrToWeb;
|
||
var
|
||
Arr: TWebXvtrArray;
|
||
i, Cnt: Integer;
|
||
begin
|
||
if FWebServer = nil then Exit;
|
||
Cnt := 0;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if FController.FXvtrSettings.Entries[i].Enabled then Inc(Cnt);
|
||
SetLength(Arr, Cnt);
|
||
Cnt := 0;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if FController.FXvtrSettings.Entries[i].Enabled then
|
||
begin
|
||
Arr[Cnt].Idx := i;
|
||
Arr[Cnt].Name := FController.FXvtrSettings.Entries[i].ButtonText;
|
||
Inc(Cnt);
|
||
end;
|
||
FWebServer.SetXvtrBands(Arr, FController.FCurrentXvtr);
|
||
end;
|
||
|
||
procedure TMainForm.TrkVolumeChange(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetVolume(TrkVolume.Position);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Mouse wheel — перестройка VFO
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
||
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
|
||
// Колёсико на форме/спектре/водопаде — меняет активный VFO с квантованием
|
||
// Шаг зависит от модификаторов: Ctrl+Shift=1МГц, Ctrl=1кГц, Shift=10Гц, иначе 100Гц
|
||
// Квантование: результат выровнен до кратного шагу (как в FreqDisplay)
|
||
// CTUN логика: через ApplyVfoA
|
||
function OverFreqDisp(C: TControl): Boolean;
|
||
var P: TPoint;
|
||
begin
|
||
Result := False;
|
||
if (C = nil) or (not C.Visible) then Exit;
|
||
// Mouse.CursorPos — надёжные экранные координаты (переданный MousePos на
|
||
// Cocoa может быть не в экранной системе).
|
||
P := C.ScreenToClient(Mouse.CursorPos);
|
||
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
||
end;
|
||
var
|
||
Step: Int64;
|
||
Delta: Int64;
|
||
Base: Int64;
|
||
NewFreq: Int64;
|
||
begin
|
||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||
// виджет крутил бы B, а этот хендлер — активный A.
|
||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
||
if (FController.FMode = MODE_FM) and FController.FFMStepOn and
|
||
not ((ssCtrl in Shift) or (ssShift in Shift)) then
|
||
Step := FM_STEP_HZ[FController.FFMStepIdx]
|
||
else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||
else if ssCtrl in Shift then Step := 1000
|
||
else if ssShift in Shift then Step := 10
|
||
else Step := 100;
|
||
|
||
Delta := 1;
|
||
if WheelDelta < 0 then Delta := -1;
|
||
|
||
// Квантование (та же логика что в FreqDisplay.ChangeByDigit) — от частоты
|
||
// АКТИВНОГО VFO, а не всегда VFO A.
|
||
Base := (ActiveVfoFreq div Step) * Step;
|
||
if Delta > 0 then
|
||
NewFreq := Base + Step
|
||
else
|
||
begin
|
||
if ActiveVfoFreq = Base then
|
||
NewFreq := Base - Step
|
||
else
|
||
NewFreq := Base; // снэп к нижней границе
|
||
end;
|
||
|
||
// Клэмп диапазона перестройки. XVTR — ApplyVfoA сам ограничит по FreqBegin/End.
|
||
// Pluto — по диапазону бэкенда (Caps). Иначе HF (30 кГц..60 МГц).
|
||
if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then
|
||
NewFreq := Max(0, NewFreq)
|
||
else if FController.IsPluto then
|
||
NewFreq := Max(Round(FController.BackendCaps.MinFreqHz),
|
||
Min(Round(FController.BackendCaps.MaxFreqHz), NewFreq))
|
||
else
|
||
NewFreq := Max(30000, Min(60000000, NewFreq));
|
||
if FController.FActiveVfo = 0 then
|
||
ApplyVfoA(NewFreq)
|
||
else
|
||
FController.SetVfoB(NewFreq); // крутим активный VFO B
|
||
|
||
Handled := True;
|
||
end;
|
||
|
||
|
||
procedure TMainForm.BtnAGCModeClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetAGCMode((Sender as TFlatButton).Tag);
|
||
end;
|
||
|
||
procedure TMainForm.TrkAGCChange(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetAGCTop(TrkAGC.Position);
|
||
end;
|
||
|
||
procedure TMainForm.TrkRxGainChange(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetRxGain(TrkRxGain.Position); // переводит в manual
|
||
end;
|
||
|
||
procedure TMainForm.BtnPlutoGainClick(Sender: TObject);
|
||
begin
|
||
if FSyncingFromController then Exit;
|
||
FController.SetRxGainMode((Sender as TFlatButton).Tag); // Tag = индекс режима
|
||
end;
|
||
|
||
procedure TMainForm.UpdateRxGainUI;
|
||
// Рендер Pluto RF-AGC блока: подсветка режима + слайдер/значение manual gain.
|
||
var i: Integer;
|
||
begin
|
||
if PanelPlutoAGC = nil then Exit;
|
||
FSyncingFromController := True;
|
||
try
|
||
for i := 0 to 3 do
|
||
if BtnPlutoGain[i] <> nil then
|
||
StyleButton(BtnPlutoGain[i], BtnPlutoGain[i].Tag = FController.FRxGainMode);
|
||
TrkRxGain.Position := FController.FRxGainDb;
|
||
TrkRxGain.Enabled := (FController.FRxGainMode = 0); // активен только в manual
|
||
if FController.FRxGainMode = 0 then
|
||
LblRxGain.Caption := Format('%ddB', [FController.FRxGainDb])
|
||
else
|
||
LblRxGain.Caption := 'AGC';
|
||
finally
|
||
FSyncingFromController := False;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.LayoutLeftPanel(Pluto: Boolean);
|
||
// Показывает/скрывает Pluto RF-AGC блок под AGC и сдвигает панели ниже
|
||
// (Bands и далее через RelayoutBelowBands).
|
||
var baseTop: Integer;
|
||
begin
|
||
if (PanelPlutoAGC = nil) or (PanelAGC = nil) or (PanelBands = nil) then Exit;
|
||
baseTop := PanelAGC.Top + PanelAGC.Height + 4;
|
||
PanelPlutoAGC.Top := baseTop;
|
||
PanelPlutoAGC.Visible := Pluto;
|
||
if Pluto then
|
||
PanelBands.Top := baseTop + PanelPlutoAGC.Height + 2
|
||
else
|
||
PanelBands.Top := baseTop;
|
||
RelayoutBelowBands;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// DSP/Audio callbacks — вызываются из рабочих потоков
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
// Подписчик на FController.OnSpectrumData (сырьё из DSP-потока) — только рендер;
|
||
// web-снимок держит контроллер (FSpectrumBuf).
|
||
begin
|
||
FSpecView.SetSpectrumData(Pixels, Count);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
// Подписчик на FController.OnWaterfallData — рендер с децимацией скорости
|
||
// прокрутки (FWaterfallFrameInterval). Web получает каждый кадр (в контроллере).
|
||
begin
|
||
Inc(FWaterfallFrameCounter);
|
||
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
|
||
begin
|
||
FWaterfallFrameCounter := 0;
|
||
FSpecView.SetWaterfallData(Pixels, Count);
|
||
FWaterfallDirty := True;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Settings apply methods (вызываются из TSettingsForm)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||
SpecAvgTimeMS: Double);
|
||
begin
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetFFTParams(FFTSize, WinType);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||
begin
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS);
|
||
FController.FWfManualHigh := WfHigh;
|
||
FController.FWfManualLow := WfLow;
|
||
FController.FWfAGCOffset := WfAGCOffset;
|
||
FSpecView.WfManualHigh := FController.FWfManualHigh;
|
||
FSpecView.WfManualLow := FController.FWfManualLow;
|
||
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
||
FSpecView.ResetWfAvgBuf;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||
begin
|
||
FController.FSpecRefLevel := RefLevel;
|
||
FController.FSpecRange := Range;
|
||
if GridStep > 0 then FController.FSpecGridStep := GridStep;
|
||
// На передаче FSpecView показывает TX-сетку — RX-правка не должна её перетирать.
|
||
if not FController.FTransmitting then ApplySpecViewGridFromState;
|
||
end;
|
||
|
||
procedure TMainForm.ApplySpecViewGridFromState;
|
||
begin
|
||
// При DUP во время TX данные приходят от RX-анализатора → используем RX-сетку.
|
||
if FController.FTransmitting and not FController.FDisplayDuplex then
|
||
begin
|
||
FSpecView.SpecRefLevel := FController.FTXSpecRefLevel;
|
||
FSpecView.SpecRange := FController.FTXSpecRange;
|
||
FSpecView.SpecGridStep := FController.FTXSpecGridStep;
|
||
end else
|
||
begin
|
||
FSpecView.SpecRefLevel := FController.FSpecRefLevel;
|
||
FSpecView.SpecRange := FController.FSpecRange;
|
||
FSpecView.SpecGridStep := FController.FSpecGridStep;
|
||
end;
|
||
InvalidateGridCache;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||
begin
|
||
if FController.FAudioOut.IsOpen then
|
||
FController.FAudioOut.Close;
|
||
FController.FAudioOut.DeviceIndex := DevIndex;
|
||
FController.FAudioOutDevName := DevName;
|
||
try
|
||
FController.FAudioOut.Open;
|
||
except
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||
begin
|
||
if FController.FAudioIn.IsOpen then
|
||
FController.FAudioIn.Close;
|
||
FController.FAudioIn.DeviceIndex := DevIndex;
|
||
FController.FAudioInDevName := DevName;
|
||
if DevIndex >= 0 then
|
||
begin
|
||
try
|
||
FController.FAudioIn.Open;
|
||
except
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyAudioBufferSize(BufferSize: Integer);
|
||
var
|
||
WasOpen: Boolean;
|
||
begin
|
||
if BufferSize <= 128 then BufferSize := 128
|
||
else if BufferSize <= 256 then BufferSize := 256
|
||
else BufferSize := 512;
|
||
if FController.FAudioOut.OutputBufferSize = BufferSize then Exit;
|
||
|
||
WasOpen := FController.FAudioOut.IsOpen;
|
||
if WasOpen then FController.FAudioOut.Close;
|
||
FController.FAudioOut.OutputBufferSize := BufferSize;
|
||
if WasOpen then
|
||
begin
|
||
try
|
||
FController.FAudioOut.Open;
|
||
except
|
||
end;
|
||
end;
|
||
FController.FSettings.SaveAudioBufferSize(BufferSize);
|
||
FController.FSettings.Save;
|
||
end;
|
||
|
||
procedure TMainForm.ApplySendAudioToRadio(Enabled: Boolean);
|
||
begin
|
||
FController.FSendAudioToRadio := Enabled;
|
||
// Один переключатель: вкл = слать поток + снять мьют динамика,
|
||
// выкл = замьютить динамик (byte 1400 bit1) и не слать поток.
|
||
FController.FNetwork.SetSpeakerAudio(Enabled);
|
||
if FController.FDevConnected then
|
||
FController.SaveGlobalSettings;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall,
|
||
ShowWideband, WidebandFill, SpectrumFill: Boolean);
|
||
begin
|
||
FController.FShowSpectrum := ShowSpectrum;
|
||
FController.FShowWaterfall := ShowWaterfall;
|
||
FController.FShowWideband := ShowWideband;
|
||
FController.FWidebandFill := WidebandFill;
|
||
FController.FSpectrumFill := SpectrumFill;
|
||
if Assigned(FWidebandView) then
|
||
FWidebandView.SetFillSpectrum(FController.FWidebandFill);
|
||
FSpecView.FillSpectrum := FController.FSpectrumFill;
|
||
if Assigned(FController.FNetwork) and FController.FNetwork.Connected then
|
||
FController.FNetwork.ConfigureWideband(0, FController.FShowWideband, 512, 16, 70, 32);
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyFPS(FPS: Integer);
|
||
begin
|
||
if FPS < 1 then FPS := 1;
|
||
if FPS > 100 then FPS := 100;
|
||
FDisplayFPS := FPS;
|
||
FController.FLoadedGlobal.DisplayFPS := FPS; // зеркало в persist-буфер контроллера
|
||
FSpectrumTimer.Interval := 1000 div FPS;
|
||
// CPU path keeps the historical every-other-frame waterfall update.
|
||
// OpenGL uploads only one texture row per frame, so it can match display FPS.
|
||
if FUseOpenGLSpectrum then
|
||
FWaterfallFrameInterval := 1
|
||
else
|
||
FWaterfallFrameInterval := 2;
|
||
FWaterfallFrameCounter := 0;
|
||
FWaterfallDirty := True;
|
||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
||
if FController.FWDSPReady then
|
||
FController.FDSPEngine.SetDisplayFPS(FPS);
|
||
end;
|
||
|
||
procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer);
|
||
begin
|
||
Digits := EnsureRange(Digits, 3, 5);
|
||
FFreqMhzDigits := Digits;
|
||
FController.FLoadedGlobal.FreqMhzDigits := Digits; // зеркало в persist-буфер контроллера
|
||
FreqDispA.MinMhzDigits := Digits;
|
||
FreqDispB.MinMhzDigits := Digits;
|
||
UpdateFreqDisplayMax; // верхняя граница: активный XVTR / Pluto Caps / HF по цифрам
|
||
if Assigned(FWebServer) then
|
||
FWebServer.FreqMhzDigits := Digits;
|
||
LayoutTopVfoBlock;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateFreqDisplayMax;
|
||
// Верхняя граница FreqDisplay. Активный трансвертер (XVTR/QO-100) может показывать
|
||
// частоты выше диапазона железа (напр. QO-100 — 10.5 ГГц) → берём его FreqEnd.
|
||
// Иначе Pluto — из Caps (до 6 ГГц), HPSDR — по числу MHz-цифр.
|
||
const
|
||
MaxFreqs: array[3..5] of Int64 = (999999999, 9999999999, 99999999999);
|
||
var
|
||
hi: Int64; xi: Integer;
|
||
begin
|
||
xi := FController.FCurrentXvtr;
|
||
if (xi >= 0) and (xi < CFG_XVTR_COUNT) then
|
||
hi := Round(FController.FXvtrSettings.Entries[xi].FreqEnd) + 1000000 // +1 МГц запас
|
||
else if FController.IsPluto then
|
||
hi := Round(FController.BackendCaps.MaxFreqHz)
|
||
else
|
||
hi := MaxFreqs[EnsureRange(FFreqMhzDigits, 3, 5)];
|
||
if hi < 1000000 then hi := MaxFreqs[3];
|
||
if FreqDispA <> nil then FreqDispA.MaxFreq := hi;
|
||
if FreqDispB <> nil then FreqDispB.MaxFreq := hi;
|
||
end;
|
||
|
||
procedure TMainForm.BtnSettingsClick(Sender: TObject);
|
||
var
|
||
SF: TSettingsForm;
|
||
begin
|
||
if not Assigned(FSettingsForm) then
|
||
begin
|
||
SF := TSettingsForm.Create(Self);
|
||
FSettingsForm := SF;
|
||
SF.OnDisplayChange := ApplyDisplayParams;
|
||
SF.OnWaterfallChange := ApplyWaterfallParams;
|
||
SF.OnGridChange := ApplyGridParams;
|
||
SF.OnAudioDevChange := ApplyAudioDevice;
|
||
SF.OnAudioInDevChange := ApplyAudioInputDevice;
|
||
SF.OnAudioBufferChange := ApplyAudioBufferSize;
|
||
SF.OnSendAudioToRadioChange := ApplySendAudioToRadio;
|
||
SF.OnVisibilityChange := ApplyVisibility;
|
||
SF.OnFPSChange := ApplyFPS;
|
||
SF.OnPAChange := OnPASettingsChange;
|
||
SF.OnPlutoTxChange := OnPlutoTxMaxAttChange;
|
||
SF.OnVHFCalChange := OnVHFCalSettingsChange;
|
||
SF.OnCATChange := OnCATSettingsChange;
|
||
SF.OnThemeChange := SetLightTheme;
|
||
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
|
||
SF.OnTXChange := OnTXSettingsChange;
|
||
SF.OnAlexChange := OnAlexSettingsChange;
|
||
SF.OnXvtrChange := OnXvtrSettingsChange;
|
||
SF.OnWfAGCNFChange := ApplyWfAGCNF;
|
||
SF.OnADCChange := ApplyADCSettings;
|
||
SF.OnWebSettingsChange := ApplyWebSettings;
|
||
end;
|
||
SF := TSettingsForm(FSettingsForm);
|
||
SF.LoadTXSettings(FController.FTXSettings,
|
||
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
|
||
SF.LoadAlexSettings(FController.FAlexSettings,
|
||
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
|
||
SF.LoadXvtrSettings(FController.FXvtrSettings);
|
||
SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal);
|
||
SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb);
|
||
SF.LoadVHFCalSettings(FController.FVHFBandCal);
|
||
SF.LoadCATSettings(
|
||
FCATLastGlobal.CATSerialEnabled,
|
||
FCATLastGlobal.CATSerialPort,
|
||
FCATLastGlobal.CATSerialBaud,
|
||
FCATLastGlobal.CATSerialDataBits,
|
||
FCATLastGlobal.CATSerialStopBits,
|
||
FCATLastGlobal.CATSerialParity,
|
||
FCATLastGlobal.CATTcpEnabled,
|
||
FCATLastGlobal.CATTcpPort);
|
||
// Перечисляем PA устройства
|
||
SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn);
|
||
SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill,
|
||
FController.FSpectrumFill);
|
||
SF.LoadWfAGCNF(FController.FWfAGCEnabled, FController.FWfNFEnabled);
|
||
SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled);
|
||
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass);
|
||
SF.LoadFPS(FDisplayFPS);
|
||
SF.LoadLightTheme(FLightTheme);
|
||
SF.LoadFreqMhzDigits(FFreqMhzDigits);
|
||
SF.LoadAudioBufferSize(FController.FAudioOut.OutputBufferSize);
|
||
SF.LoadSendAudioToRadio(FController.FSendAudioToRadio);
|
||
// Загружаем текущие значения
|
||
if FController.FWDSPReady then
|
||
SF.LoadValues(
|
||
FController.FDSPEngine.FFTSize,
|
||
FController.FDSPEngine.WindowType,
|
||
FController.FDSPEngine.SpecDetector,
|
||
FController.FDSPEngine.SpecAvgMode,
|
||
FController.FDSPEngine.SpecAvgTimeMS,
|
||
FController.FDSPEngine.WfDetector,
|
||
FController.FDSPEngine.WfAvgMode,
|
||
FController.FDSPEngine.WfAvgTimeMS,
|
||
FController.FWfManualHigh,
|
||
FController.FWfManualLow,
|
||
FController.FWfAGCOffset,
|
||
FController.FSpecRefLevel, FController.FSpecRange, FController.FSpecGridStep,
|
||
FController.FAudioOutDevName, FController.FAudioInDevName)
|
||
else
|
||
SF.LoadValues(
|
||
131072, 2, 0, 3, 30.0,
|
||
0, 3, 120.0, FController.FWfManualHigh, FController.FWfManualLow, FController.FWfAGCOffset,
|
||
FController.FSpecRefLevel, FController.FSpecRange, FController.FSpecGridStep,
|
||
FController.FAudioOutDevName, FController.FAudioInDevName);
|
||
SF.Show;
|
||
end;
|
||
|
||
procedure TMainForm.AfterShowTick(Sender: TObject);
|
||
begin
|
||
// Вызывается один раз через 200 мс после старта формы
|
||
FAfterShowTimer.Enabled := False;
|
||
|
||
// Повторно применяем сохранённую позицию: к этому моменту WM уже декорировал
|
||
// окно и Qt знает frame extents, поэтому move() ставит рамку точно.
|
||
if FHasPendingRestore then
|
||
begin
|
||
SetBounds(FRestoreL, FRestoreT, Width, Height);
|
||
FHasPendingRestore := False;
|
||
end;
|
||
|
||
try
|
||
FController.FAudioOut.Open;
|
||
except
|
||
end;
|
||
|
||
EnsureWDSPWisdom;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// CAT subsystem
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.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;
|
||
// Зеркалим CAT-поля в persist-буфер контроллера (BuildGlobalSettings берёт их
|
||
// сквозь из FLoadedGlobal; на connect они грузятся в FLoadedGlobal → FCATLastGlobal).
|
||
FController.FLoadedGlobal.CATSerialEnabled := FCATLastGlobal.CATSerialEnabled;
|
||
FController.FLoadedGlobal.CATSerialPort := FCATLastGlobal.CATSerialPort;
|
||
FController.FLoadedGlobal.CATSerialBaud := FCATLastGlobal.CATSerialBaud;
|
||
FController.FLoadedGlobal.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits;
|
||
FController.FLoadedGlobal.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits;
|
||
FController.FLoadedGlobal.CATSerialParity := FCATLastGlobal.CATSerialParity;
|
||
FController.FLoadedGlobal.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled;
|
||
FController.FLoadedGlobal.CATTcpPort := FCATLastGlobal.CATTcpPort;
|
||
FCATAdapter.ApplySettings(FCATLastGlobal);
|
||
FController.FSettings.SaveCATSettings(FCATLastGlobal);
|
||
FController.SaveGlobalSettings;
|
||
FController.FSettings.Save;
|
||
end;
|
||
|
||
end.
|