Files
ewsdr/MainForm.pas
T
Uladzimir KarpenkaandClaude Opus 4.8 3d2d76452c Phase 5 (batch 23): move TX mic callbacks into the controller
OnMicPacket (HW mic packet -> WDSP, only when TX mic source is Radio) and
PullSoundCardMic (sound-card input -> WDSP, when source is SoundCard) are
pure data-path handlers with no UI/MainForm state, so they move verbatim
into TRadioController. FormCreate now wires FNetwork.OnMicPacket and
FDSPEngine.OnPullMicSamples to the controller's own methods. These are
backend-agnostic consumers (they operate on the common interchange
format), so they belong in the core regardless of HPSDR vs future Pluto
backends. Behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:33:11 +03:00

6963 lines
264 KiB
ObjectPascal
Raw Blame History

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