mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Replaces TProgressBar in TWisdomProgressDialog with TFlatProgressBar, drawn entirely via Canvas (plain rectangle, no native widget styling). Dialog now receives the active TAppTheme so both dark and light themes are applied consistently to background, label, and progress bar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7272 lines
252 KiB
ObjectPascal
7272 lines
252 KiB
ObjectPascal
unit MainForm;
|
||
|
||
{
|
||
OpenHPSDR Transceiver - Main Form
|
||
Lazarus / FPC 3.2+ (нет inline var, нет анонимных процедур)
|
||
|
||
ИСПРАВЛЕНИЯ:
|
||
- (Web NR/NB/ANF fix) BtnNRClick, BtnNBClick, BtnANFClick переписаны
|
||
без использования «Sender as TFlatButton» — теперь обращаются напрямую
|
||
к BtnNR / BtnNB / BtnANF. Это устраняет EAccessViolation / EInvalidCast
|
||
при вызове из SyncWebNR/SyncWebNB/SyncWebANF через TThread.Synchronize,
|
||
где Sender передавался как nil. Исключение в UI-потоке приводило к
|
||
разрыву WebSocket-соединения и «отключению» веб-клиента.
|
||
- SyncWebNR / SyncWebNB / SyncWebANF: передают реальную кнопку (BtnNR /
|
||
BtnNB / BtnANF) вместо nil — защита на случай изменения обработчиков.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay,
|
||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types,
|
||
FlatProgressBar,
|
||
LCLIntf, LCLType, GraphType,
|
||
HPSDRProtocol, HPSDRNetwork,
|
||
WDSP, WDSPEngine, AudioOutput, AudioInput,
|
||
Settings,
|
||
WebServer,
|
||
CATEngine, CATSerial, CATTcp,
|
||
SpectrumView,
|
||
StatusBar,
|
||
WinFirewall;
|
||
|
||
const
|
||
CLR_BG = TColor($00101010);
|
||
CLR_PANEL = TColor($00181818);
|
||
CLR_BORDER = TColor($00303030);
|
||
CLR_FREQ = TColor($0000FF00);
|
||
CLR_FREQ_DIM = TColor($00007800);
|
||
CLR_AMBER = TColor($0000AAFF);
|
||
CLR_ACTIVE = TColor($000C3010);
|
||
CLR_ACTIVE_BDR = TColor($00387838);
|
||
CLR_INACTIVE = TColor($001A1A1A);
|
||
CLR_TEXT = TColor($00CCCCCC);
|
||
CLR_TEXTDIM = TColor($00666666);
|
||
CLR_METER_ON = TColor($0000CC44);
|
||
CLR_METER_OVR = TColor($000000CC);
|
||
CLR_SPECTRUM = TColor($0044FF44);
|
||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||
|
||
BAND_COUNT = 11;
|
||
MODE_COUNT = 8;
|
||
FILT_COUNT = 10;
|
||
|
||
// Группы фильтров по типу модуляции
|
||
// SSB: LSB/USB
|
||
FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k');
|
||
FILT_SSB_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(5000, 4400, 3800, 3300, 2900, 2700, 2400, 2100, 1800, 1000);
|
||
FILT_SSB_DEF = 5; // 2.7k
|
||
|
||
// CW: CWL/CWU
|
||
FILT_CW_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('1.0k','800','600','500','400','250','150','100','50','25');
|
||
FILT_CW_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(1000, 800, 600, 500, 400, 250, 150, 100, 50, 25);
|
||
FILT_CW_DEF = 3; // 500
|
||
|
||
// DSB
|
||
FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
|
||
FILT_DSB_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400);
|
||
FILT_DSB_DEF = 3; // 8.0k
|
||
|
||
// AM/SAM
|
||
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
|
||
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000);
|
||
FILT_AM_DEF = 4; // 10k
|
||
|
||
// FM: NFM (2.5 kHz deviation / 11 kHz BW) and FM (5.0 kHz deviation / 16 kHz BW)
|
||
FILT_FM_COUNT = 2;
|
||
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
|
||
FILT_FM_BW: array[0..1] of Integer = (11000, 16000);
|
||
FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0);
|
||
FILT_FM_DEF = 0; // NFM
|
||
|
||
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
|
||
CTCSS_COUNT = 38;
|
||
CTCSS_TONES: array[0..37] of Double = (
|
||
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);
|
||
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;
|
||
|
||
type
|
||
TDeviceItem = record
|
||
Dev: THPSDRDevice;
|
||
Display: string;
|
||
end;
|
||
|
||
// Синхронизирующий объект для OnDeviceFound
|
||
TDeviceFoundSync = class
|
||
private
|
||
FForm: TObject; // TMainForm, через forward ref
|
||
FDev: THPSDRDevice;
|
||
FEntry: string;
|
||
public
|
||
constructor Create(AForm: TObject; const D: THPSDRDevice; const E: string);
|
||
procedure Execute;
|
||
end;
|
||
|
||
// Синхронизирующий объект для HP Status
|
||
TStatusUISync = class
|
||
private
|
||
FForm: TObject;
|
||
FFwdW: Double;
|
||
FSWRV: Double;
|
||
FSupplyV: Double;
|
||
FSupplyA: Double;
|
||
FPLLLock: Boolean;
|
||
FHWPTT: Boolean;
|
||
FADCOverload: Byte;
|
||
public
|
||
constructor Create(AForm: TObject; FW, SW, SV, SA: Double; PLL, HWPTT: Boolean;
|
||
ADCOverload: Byte);
|
||
procedure Execute;
|
||
end;
|
||
|
||
// Синхронизирующий объект для DDC IQ
|
||
TDDCSeqSync = class
|
||
private
|
||
FForm: TObject;
|
||
FDDCIdx: Integer;
|
||
FSeq: LongWord;
|
||
public
|
||
constructor Create(AForm: TObject; Idx: Integer; Seq: LongWord);
|
||
procedure Execute;
|
||
end;
|
||
|
||
{ TMainForm }
|
||
TMainForm = class(TForm)
|
||
private
|
||
// ---- Network ----
|
||
FNetwork: THPSDRNetwork;
|
||
FDevices: array of TDeviceItem;
|
||
FDeviceCount: Integer;
|
||
FDeviceDialog: TDeviceDialog;
|
||
FPendingIP: string; // IP устройства для подключения
|
||
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
|
||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||
FPanelHidden: Boolean; // True = левая панель скрыта
|
||
FPendingBoardType: Integer; // BoardType устройства для подключения
|
||
|
||
// ---- DSP + Audio ----
|
||
FDSPEngine: TWDSPEngine;
|
||
FAudioOut: TAudioOutput;
|
||
FAudioIn: TAudioInput;
|
||
FAudioOutDevName: string; // текущее имя выходного устройства
|
||
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
|
||
FTXSettings: TTXSettings; // полный TX-конфиг (загружается per-MAC)
|
||
FAlexSettings: TAlexSettings; // per-band antenna/routing config (Alex board)
|
||
FXvtrSettings: TXvtrSettings; // transverter slots (per-device)
|
||
FCurrentXvtr: Integer; // -1 = HF, 0..CFG_XVTR_COUNT-1 = active XVTR
|
||
FWDSPReady: Boolean; // True когда WDSP открыт и работает
|
||
|
||
// ---- State ----
|
||
FVfoA: Double;
|
||
FVfoB: Double;
|
||
FActiveVfo: Integer;
|
||
FMode: Integer;
|
||
FFilter: Integer;
|
||
FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF
|
||
FAGCTop: Integer; // AGC level dBm, −20..−120
|
||
FCTun: Boolean; // Center Tune: True=спектр стоит, маркер двигается
|
||
FFilterBW: Integer; // текущая полоса фильтра в Гц
|
||
// мышь на спектре/водопаде
|
||
FSpecDrag: Boolean;
|
||
FSpecDragX0: Integer; // X при нажатии
|
||
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
||
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
||
FDriveLevel: Byte;
|
||
FPAMaxPower: Double;
|
||
FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
|
||
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка VHF/XVTR слотов
|
||
FRunning: Boolean;
|
||
FTransmitting: Boolean;
|
||
FSplitTxB: 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;
|
||
// DUP: при TX продолжаем показывать RX1-водопад/панораму, а TX-фильтр
|
||
// и TX-линии рисуем поверх. Аппаратной командой не является.
|
||
FDisplayDuplex: Boolean;
|
||
// TUN: режим тоновой подстройки. WDSP TXA PostGen генерирует чистый
|
||
// тон, Drive подменяется на FTXSettings.TUNLevel (через Cal[band]).
|
||
FTuning: Boolean;
|
||
FHWPTTActive: Boolean; // True = hardware PTT нажата
|
||
FHWPTTStartedTX: Boolean; // True = TX был включён именно через HWPTT
|
||
FMuted: Boolean;
|
||
FVolume: Integer;
|
||
FAtten: Integer; // RX step attenuator index: 0=0dB 1=10dB 2=20dB
|
||
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;
|
||
FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора
|
||
FSpectrumHeight: Integer;
|
||
FWaterfallHeight:Integer;
|
||
FShowSpectrum: Boolean;
|
||
FShowWaterfall: Boolean;
|
||
FDisplayFPS: Integer;
|
||
FWaterfallDirty: Boolean;
|
||
// Waterfall settings (kept here for MakeGlobalSettings, buttons, settings dialog)
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FDitherEnabled: Boolean;
|
||
FRandomEnabled: Boolean;
|
||
FFMDeviation: Double;
|
||
FFMCTCSSOn: Boolean;
|
||
FFMCTCSSToneIdx: Integer;
|
||
FFMSQOn: Boolean;
|
||
FFMSQLevel: Integer;
|
||
FFMStepOn: Boolean;
|
||
FFMStepIdx: Integer;
|
||
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 ---
|
||
FSettings: TSettingsManager;
|
||
FWebServer: TWebServer;
|
||
FWebEnabled: Boolean;
|
||
FWebPort: Integer;
|
||
FWebBindAddr: string;
|
||
FWebUser: string;
|
||
FWebPass: string;
|
||
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
|
||
// --- CAT ---
|
||
FCATEngine: TCATEngine;
|
||
FCATSerial: TCATSerialManager;
|
||
FCATTcp: TCATTcpServer;
|
||
FCATSyncFreq: Double;
|
||
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (для сохранения)
|
||
// Временные поля для передачи параметров в Synchronize-методы
|
||
FWebSyncFreq: Double;
|
||
FWebSyncInt: Integer;
|
||
FWebSyncBool: Boolean;
|
||
FWebSyncM: TThreadMethod;
|
||
FCurrentBand: Integer; // текущий активный диапазон 0..10
|
||
FSettingsForm: TObject; // TSettingsForm (cast при использовании)
|
||
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
|
||
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
|
||
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
|
||
FDevConnected: Boolean; // True после первого подключения
|
||
FLightTheme: Boolean; // True = светлая тема
|
||
FFreqMhzDigits: Integer; // 3=999MHz, 4=9.999GHz, 5=99.999GHz
|
||
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
|
||
FSpanHz: Double;
|
||
FSampleRate: Integer; // текущий DDC sample rate (Hz)
|
||
|
||
// ---- 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;
|
||
|
||
PanelRX: 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;
|
||
|
||
// ---- Right panel ----
|
||
PanelRight: TPanel;
|
||
PbSpectrum: TPaintBox;
|
||
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
||
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
||
PbWaterfall: TPaintBox;
|
||
|
||
// ---- Status bar ----
|
||
StatusPanel: TMainStatusBar;
|
||
|
||
// ---- Helpers ----
|
||
procedure BuildUI;
|
||
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;
|
||
procedure InvalidateGridCache;
|
||
procedure SyncSpecViewFreq;
|
||
procedure RecreateDSPEngine(ASampleRate: Integer);
|
||
function EnsureWDSPWisdom: Boolean;
|
||
|
||
// Network callbacks
|
||
procedure OnDeviceFound(const Dev: THPSDRDevice);
|
||
procedure OnHPStatusCB(const Status: THighPriorityStatus);
|
||
procedure OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
|
||
procedure OnWDSPOpenDone(Success: Boolean);
|
||
procedure DoConnectDevice(const Dev: THPSDRDevice);
|
||
procedure OnMicPacketCB(const Data: TMicDataPacket);
|
||
|
||
// DSP/Audio callbacks (вызываются из рабочих потоков)
|
||
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
||
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
|
||
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||
|
||
// Public UI update (called from sync objects)
|
||
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
|
||
procedure DoUpdateStatus(FwdW, SWRV, SupplyV, SupplyA: Double; PLLLock, HWPTT: Boolean;
|
||
ADCOverload: Byte);
|
||
procedure UpdateTXMeters;
|
||
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
|
||
|
||
// Event handlers
|
||
procedure BtnDiscoverClick(Sender: TObject);
|
||
procedure BtnDiscoverFromDialog(Sender: TObject);
|
||
procedure BtnStartStopClick(Sender: TObject);
|
||
procedure FreqDispAChanged(Sender: TObject; NewFreq: Int64);
|
||
procedure ApplyVfoA(NewFreq: Int64);
|
||
procedure FreqDispAClick(Sender: TObject);
|
||
procedure FreqDispBClick(Sender: TObject);
|
||
procedure BtnTopVfoASelectClick(Sender: TObject);
|
||
procedure BtnTopVfoATXClick(Sender: TObject);
|
||
procedure BtnTopVfoBSelectClick(Sender: TObject);
|
||
procedure BtnTopVfoBTXClick(Sender: TObject);
|
||
procedure ActivateVfo(Idx: Integer);
|
||
function ActiveVfoFreq: Int64;
|
||
// Настройки
|
||
procedure SaveCurrentBand;
|
||
procedure RestoreBand(BandIdx: Integer);
|
||
procedure SaveAllAndExit;
|
||
procedure RestoreWindowBounds;
|
||
procedure SaveWindowBounds;
|
||
function MakeGlobalSettings: TGlobalSettings;
|
||
function MakeBandSettings: TBandSettings;
|
||
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||
procedure BtnBandClick(Sender: TObject);
|
||
procedure ApplyModeFilter;
|
||
procedure BtnModeClick(Sender: TObject);
|
||
procedure BtnFilterClick(Sender: TObject);
|
||
procedure BtnCTunClick(Sender: TObject);
|
||
procedure BtnDUPClick(Sender: TObject);
|
||
procedure ApplyDUP(Active: Boolean);
|
||
procedure BtnTUNClick(Sender: TObject);
|
||
procedure ApplyTUN(Active: Boolean);
|
||
procedure UpdateFilterButtons;
|
||
procedure BtnFMSQClick(Sender: TObject);
|
||
procedure TrkFMSQChange(Sender: TObject);
|
||
procedure ApplyFMSquelch;
|
||
procedure BtnFMCTCSSClick(Sender: TObject);
|
||
procedure BtnFMCTCSSToneClick(Sender: TObject);
|
||
procedure SetFMCTCSSTone(Idx: Integer);
|
||
procedure CloseCTCSSPopup;
|
||
procedure 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 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 PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||
procedure BtnVfoSwapClick(Sender: TObject);
|
||
procedure BtnVfoACopyBClick(Sender: TObject);
|
||
procedure BtnVfoBCopyAClick(Sender: TObject);
|
||
procedure BtnMOXClick(Sender: TObject);
|
||
procedure BtnMuteClick(Sender: TObject);
|
||
procedure UpdateNRButton;
|
||
procedure UpdateNBButton;
|
||
procedure UpdateSNBButton;
|
||
procedure UpdateANFButton;
|
||
procedure ApplyNoiseFilterButtonsToDSP;
|
||
procedure BtnNRClick(Sender: TObject);
|
||
procedure BtnNBClick(Sender: TObject);
|
||
procedure BtnSNBClick(Sender: TObject);
|
||
procedure BtnANFClick(Sender: TObject);
|
||
// Веб-интерфейс: 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);
|
||
// TX settings: применить FTXSettings к WDSP и переслать DUC Specific
|
||
// (mic-биты Boost/Bias/LineIn/PTT попадают в byte 50 DUCSpecific).
|
||
procedure ApplyTXSettingsToDSP;
|
||
procedure SendDUCSpecificFromSettings;
|
||
function PullSoundCardMic(MaxN: Integer): Integer;
|
||
function BuildMicLineSelectByte: Byte;
|
||
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: 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;
|
||
|
||
TWisdomBuildThread = class(TThread)
|
||
private
|
||
FDirectory: string;
|
||
FError: string;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(const ADirectory: string);
|
||
property ErrorText: string read FError;
|
||
end;
|
||
|
||
TWisdomProgressDialog = class(TForm)
|
||
private
|
||
FInfoLabel: TLabel;
|
||
FProgress: TFlatProgressBar;
|
||
FTimer: TTimer;
|
||
FThread: TWisdomBuildThread;
|
||
FPhase: Integer;
|
||
procedure TimerTick(Sender: TObject);
|
||
public
|
||
constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread;
|
||
const ATheme: TAppTheme); reintroduce;
|
||
end;
|
||
|
||
var
|
||
MainForm: TMainForm;
|
||
|
||
implementation
|
||
|
||
uses SettingsForm;
|
||
|
||
{$R *.lfm}
|
||
|
||
constructor TWisdomBuildThread.Create(const ADirectory: string);
|
||
begin
|
||
inherited Create(False);
|
||
FreeOnTerminate := False;
|
||
FDirectory := ADirectory;
|
||
FError := '';
|
||
end;
|
||
|
||
procedure TWisdomBuildThread.Execute;
|
||
var
|
||
DirA: AnsiString;
|
||
begin
|
||
try
|
||
if not Assigned(@WDSPwisdom) then Exit;
|
||
DirA := AnsiString(FDirectory);
|
||
WDSPwisdom(PAnsiChar(DirA));
|
||
except
|
||
on E: Exception do
|
||
FError := E.ClassName + ': ' + E.Message;
|
||
end;
|
||
end;
|
||
|
||
constructor TWisdomProgressDialog.Create(AOwner: TComponent;
|
||
AThread: TWisdomBuildThread; const ATheme: TAppTheme);
|
||
begin
|
||
inherited CreateNew(AOwner);
|
||
FThread := AThread;
|
||
FPhase := 0;
|
||
|
||
Caption := 'WDSP Wisdom';
|
||
Width := 520;
|
||
Height := 140;
|
||
Position := poScreenCenter;
|
||
BorderStyle := bsDialog;
|
||
BorderIcons := [];
|
||
Color := ATheme.BG;
|
||
Font.Name := 'Courier New';
|
||
Font.Size := 9;
|
||
Font.Color := ATheme.Text;
|
||
|
||
FInfoLabel := TLabel.Create(Self);
|
||
FInfoLabel.Parent := Self;
|
||
FInfoLabel.SetBounds(16, 16, 480, 40);
|
||
FInfoLabel.AutoSize := False;
|
||
FInfoLabel.WordWrap := True;
|
||
FInfoLabel.Font.Color := ATheme.Text;
|
||
FInfoLabel.Caption :=
|
||
'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.';
|
||
|
||
FProgress := TFlatProgressBar.Create(Self);
|
||
FProgress.Parent := Self;
|
||
FProgress.SetBounds(16, 72, 480, 22);
|
||
FProgress.Min := 0;
|
||
FProgress.Max := 100;
|
||
FProgress.Position := 0;
|
||
FProgress.SetAppTheme(ATheme);
|
||
|
||
FTimer := TTimer.Create(Self);
|
||
FTimer.Interval := 250;
|
||
FTimer.OnTimer := TimerTick;
|
||
FTimer.Enabled := True;
|
||
end;
|
||
|
||
procedure TWisdomProgressDialog.TimerTick(Sender: TObject);
|
||
var
|
||
P: PAnsiChar;
|
||
S: string;
|
||
begin
|
||
FPhase := (FPhase + 7) mod 101;
|
||
FProgress.Position := FPhase;
|
||
|
||
if Assigned(@wisdom_get_status) then
|
||
begin
|
||
P := wisdom_get_status;
|
||
if P <> nil then
|
||
begin
|
||
S := Trim(string(AnsiString(P)));
|
||
if S <> '' then
|
||
FInfoLabel.Caption := S;
|
||
end;
|
||
end;
|
||
|
||
if Assigned(FThread) and FThread.Finished then
|
||
begin
|
||
FTimer.Enabled := False;
|
||
ModalResult := mrOk;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Общая функция декодирования типа платы — используется везде
|
||
// ===========================================================================
|
||
|
||
function BoardTypeName(BoardType: Integer): string;
|
||
begin
|
||
case BoardType of
|
||
1: Result := 'HERMES (ANAN-10/100)';
|
||
2: Result := 'HERMES-E (ANAN-10E/100B)';
|
||
3: Result := 'ANGELIA (ANAN-100D)';
|
||
4: Result := 'ORION (ANAN-200D)';
|
||
5: Result := 'ORION MkII (ANAN-7000/8000)';
|
||
6: Result := 'HERMES-LITE 2';
|
||
10: Result := 'SATURN (G2)';
|
||
else Result := Format('Unknown Board #%d', [BoardType]);
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Sync helpers
|
||
// ===========================================================================
|
||
|
||
constructor TDeviceFoundSync.Create(AForm: TObject;
|
||
const D: THPSDRDevice; const E: string);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FDev := D;
|
||
FEntry := E;
|
||
end;
|
||
|
||
procedure TDeviceFoundSync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoAddDevice(FDev, FEntry);
|
||
end;
|
||
|
||
constructor TStatusUISync.Create(AForm: TObject;
|
||
FW, SW, SV, SA: Double; PLL, HWPTT: Boolean; ADCOverload: Byte);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FFwdW := FW;
|
||
FSWRV := SW;
|
||
FSupplyV := SV;
|
||
FSupplyA := SA;
|
||
FPLLLock := PLL;
|
||
FHWPTT := HWPTT;
|
||
FADCOverload := ADCOverload;
|
||
end;
|
||
|
||
procedure TStatusUISync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FSupplyA, FPLLLock, FHWPTT,
|
||
FADCOverload);
|
||
end;
|
||
|
||
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FDDCIdx := Idx;
|
||
FSeq := Seq;
|
||
end;
|
||
|
||
procedure TDDCSeqSync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoUpdateDDCSeqOrNoDevice(FDDCIdx, FSeq);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// FormCreate / FormDestroy
|
||
// ===========================================================================
|
||
|
||
|
||
// ===========================================================================
|
||
// Settings helpers
|
||
// ===========================================================================
|
||
|
||
function CurrentScreenDPI: Integer;
|
||
begin
|
||
Result := Screen.PixelsPerInch;
|
||
if Result <= 0 then Result := 96;
|
||
end;
|
||
|
||
procedure TMainForm.RestoreWindowBounds;
|
||
var
|
||
L, T, Wd, Ht, SavedDPI, CurDPI: Integer;
|
||
Maximized: Boolean;
|
||
R, WA: TRect;
|
||
Mon: TMonitor;
|
||
begin
|
||
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;
|
||
FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized);
|
||
end;
|
||
|
||
function TMainForm.MakeBandSettings: TBandSettings;
|
||
begin
|
||
Result.VfoA := FVfoA;
|
||
Result.VfoB := FVfoB;
|
||
Result.Mode := FMode;
|
||
Result.FilterIdx := FFilter;
|
||
Result.FilterBW := FFilterBW;
|
||
Result.AGCMode := FAGCMode;
|
||
Result.AGCTop := FAGCTop;
|
||
Result.CTun := FCTun;
|
||
Result.SpanHz := FSpanHz;
|
||
Result.FMSQOn := FFMSQOn;
|
||
Result.FMSQLevel := FFMSQLevel;
|
||
Result.CTCSSOn := FFMCTCSSOn;
|
||
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
|
||
Result.FMStepOn := FFMStepOn;
|
||
Result.FMStepIdx := FFMStepIdx;
|
||
// Waterfall AGC/NF — глобальные (не диапазонные)
|
||
end;
|
||
|
||
function TMainForm.MakeGlobalSettings: TGlobalSettings;
|
||
var i: Integer;
|
||
begin
|
||
Result.Volume := FVolume;
|
||
Result.DriveLevel := TrkDrive.Position;
|
||
Result.PAMaxPower := FPAMaxPower;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
Result.PABandCal[i] := FPABandCal[i];
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
Result.VHFBandCal[i] := FVHFBandCal[i];
|
||
Result.ActiveVfo := FActiveVfo;
|
||
Result.NRMode := BtnNR.Tag;
|
||
Result.NBMode := BtnNB.Tag;
|
||
Result.SNBEnabled := BtnSNB.Tag <> 0;
|
||
Result.ANFEnabled := BtnANF.Tag <> 0;
|
||
Result.AGCSlope := 0;
|
||
Result.AGCHangThreshold := 100;
|
||
Result.WfAGCEnabled := FWfAGCEnabled;
|
||
Result.WfNFEnabled := FWfNFEnabled;
|
||
Result.DitherEnabled := FDitherEnabled;
|
||
Result.RandomEnabled := FRandomEnabled;
|
||
Result.LastBand := FCurrentBand;
|
||
Result.LastXvtr := FCurrentXvtr;
|
||
Result.SampleRate := FSampleRate;
|
||
// Display settings
|
||
if FWDSPReady then
|
||
begin
|
||
Result.FFTSize := FDSPEngine.FFTSize;
|
||
Result.WindowType := FDSPEngine.WindowType;
|
||
Result.SpecDetector := FDSPEngine.SpecDetector;
|
||
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
|
||
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
|
||
Result.WfDetector := FDSPEngine.WfDetector;
|
||
Result.WfAvgMode := FDSPEngine.WfAvgMode;
|
||
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
|
||
end
|
||
else
|
||
begin
|
||
Result.FFTSize := 131072;
|
||
Result.WindowType := 2;
|
||
Result.SpecDetector := 0;
|
||
Result.SpecAvgMode := 3;
|
||
Result.SpecAvgTimeMS := 30.0;
|
||
Result.WfDetector := 0;
|
||
Result.WfAvgMode := 3;
|
||
Result.WfAvgTimeMS := 120.0;
|
||
end;
|
||
Result.WfManualHigh := FWfManualHigh;
|
||
Result.WfManualLow := FWfManualLow;
|
||
Result.WfAGCOffset := FWfAGCOffset;
|
||
Result.SpecRefLevel := FSpecRefLevel;
|
||
Result.SpecRange := FSpecRange;
|
||
Result.SpecGridStep := FSpecGridStep;
|
||
Result.AudioSampleRate := FAudioOut.SampleRate;
|
||
Result.ShowSpectrum := FShowSpectrum;
|
||
Result.ShowWaterfall := FShowWaterfall;
|
||
Result.DisplayDuplex := FDisplayDuplex;
|
||
Result.DisplayFPS := FDisplayFPS;
|
||
Result.LightTheme := FLightTheme;
|
||
Result.FreqMhzDigits := FFreqMhzDigits;
|
||
// Audio device names
|
||
Result.AudioOutDevice := FAudioOutDevName;
|
||
Result.AudioInDevice := FAudioInDevName;
|
||
// CAT settings — carried from the last loaded/saved device config
|
||
Result.CATSerialEnabled := FCATLastGlobal.CATSerialEnabled;
|
||
Result.CATSerialPort := FCATLastGlobal.CATSerialPort;
|
||
Result.CATSerialBaud := FCATLastGlobal.CATSerialBaud;
|
||
Result.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits;
|
||
Result.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits;
|
||
Result.CATSerialParity := FCATLastGlobal.CATSerialParity;
|
||
Result.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled;
|
||
Result.CATTcpPort := FCATLastGlobal.CATTcpPort;
|
||
end;
|
||
|
||
procedure TMainForm.SaveCurrentBand;
|
||
begin
|
||
if not FDevConnected then Exit;
|
||
// В XVTR-режиме НЕ трогаем HF band-cache: FBandCache[FCurrentBand]
|
||
// относится к HF-диапазону, на котором пользователь был ДО активации
|
||
// трансвертера, и FVfoA сейчас на XVTR-частоте (144.x, 432.x и т.д.).
|
||
// Запись в FBandCache[FCurrentBand] стерла бы корректное HF-состояние.
|
||
// Вместо этого сохраняем LastFreq в XVTR-настройках.
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
if (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
begin
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastMode := FMode;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFilterIdx := FFilter;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQOn := FFMSQOn;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQLevel := FFMSQLevel;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSOn := FFMCTCSSOn;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop;
|
||
end;
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
Exit;
|
||
end;
|
||
FBandCache[FCurrentBand] := MakeBandSettings;
|
||
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
|
||
end;
|
||
|
||
procedure TMainForm.RestoreBand(BandIdx: Integer);
|
||
var
|
||
B: TBandSettings;
|
||
i: Integer;
|
||
begin
|
||
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
|
||
B := FBandCache[BandIdx];
|
||
|
||
// --- Кнопки диапазонов ---
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
StyleButton(BtnBand[i], i = BandIdx);
|
||
// Сбрасываем waterfall thresholds при смене диапазона
|
||
FSpecView.ResetWfAvgBuf;
|
||
|
||
// --- Режим: сначала устанавливаем FMode, потом кнопки и WDSP ---
|
||
FMode := B.Mode;
|
||
for i := 0 to MODE_COUNT - 1 do
|
||
StyleButton(BtnMode[i], i = FMode);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetMode(FMode);
|
||
|
||
// --- Фильтр ---
|
||
FFilter := B.FilterIdx;
|
||
FFilterBW := B.FilterBW;
|
||
UpdateFilterButtons; // обновляет кнопки фильтра
|
||
ApplyModeFilter; // применяет Lo/Hi в WDSP с учётом нового FMode
|
||
|
||
// --- AGC ---
|
||
FAGCMode := B.AGCMode;
|
||
FAGCTop := B.AGCTop;
|
||
for i := 0 to 4 do
|
||
StyleButton(BtnAGCMode[i], i = FAGCMode);
|
||
TrkAGC.Position := FAGCTop;
|
||
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
|
||
// --- FM Squelch + CTCSS ---
|
||
FFMSQOn := B.FMSQOn;
|
||
FFMSQLevel := B.FMSQLevel;
|
||
// UI update happens inside UpdateFilterButtons (called above) when mode=FM
|
||
FFMCTCSSOn := B.CTCSSOn;
|
||
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
||
SetFMCTCSSTone(B.CTCSSToneIdx);
|
||
ApplyFMSquelch;
|
||
FFMStepOn := B.FMStepOn;
|
||
SetFMStep(B.FMStepIdx);
|
||
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
|
||
|
||
// --- CTUN ---
|
||
FCTun := B.CTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
if FWDSPReady and not FCTun then
|
||
FDSPEngine.SetShift(0.0);
|
||
|
||
// --- Sample Rate / Span ---
|
||
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
|
||
FSpanHz := FSampleRate;
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.SetCurrentRate(FSampleRate);
|
||
|
||
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
|
||
FCenterFreq := B.VfoA; // DDC = центр диапазона
|
||
FVfoB := B.VfoB;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
|
||
// Пересчитываем drive byte для нового диапазона перед отправкой в сеть
|
||
FDriveLevel := CalcDriveByte;
|
||
// ApplyVfoA обновит сеть, FreqDisp, shift, перерисует
|
||
ApplyVfoA(Round(B.VfoA));
|
||
end;
|
||
|
||
procedure TMainForm.SaveAllAndExit;
|
||
begin
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
if FDevConnected then
|
||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||
// Запомнить LastFreq XVTR для следующего запуска
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.FormCreate(Sender: TObject);
|
||
var
|
||
bi_, StartupRate, i: Integer;
|
||
WebCfg: TWebSettings;
|
||
StartupVfoA, StartupVfoB: Double;
|
||
begin
|
||
SetExceptionMask(GetExceptionMask + [exZeroDivide, exInvalidOp]);
|
||
FVfoA := 14200000;
|
||
FVfoB := 7100000;
|
||
FActiveVfo := 0;
|
||
FMode := 1;
|
||
FFilter := 5;
|
||
FAGCMode := 1; // MEDIUM
|
||
FAGCTop := 90; // −90 dBm
|
||
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
||
FCTun := False;
|
||
FFilterBW := 2700;
|
||
FSpecDrag := False;
|
||
FDriveLevel := 0;
|
||
FPAMaxPower := 100.0;
|
||
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i];
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
FSplitTxB := False;
|
||
FDUCPendingCount := 0;
|
||
FDisplayDuplex := False;
|
||
FTuning := False;
|
||
FMuted := False;
|
||
FVolume := 70;
|
||
FCenterFreq := FVfoA;
|
||
FSpanHz := 192000;
|
||
FSampleRate := 192000;
|
||
FCurrentBand := 5; // 20m по умолчанию
|
||
// Waterfall normalization
|
||
FWfManualHigh := -80.0;
|
||
FWfManualLow := -130.0;
|
||
FWfAGCOffset := 0.0;
|
||
FWfAGCEnabled := True;
|
||
FWfNFEnabled := False;
|
||
FDitherEnabled := True;
|
||
FRandomEnabled := True;
|
||
FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
|
||
FFMCTCSSOn := False;
|
||
FFMCTCSSToneIdx := 0;
|
||
FFMSQOn := False;
|
||
FFMSQLevel := 30;
|
||
FFMStepOn := True;
|
||
FFMStepIdx := FM_STEP_DEF;
|
||
FSpectrumBufCount := 1024;
|
||
FWaterfallBufCount := 1024;
|
||
FWaterfallFrameInterval := 2;
|
||
FWaterfallFrameCounter := 0;
|
||
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;
|
||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||
|
||
// Инициализируем кэш диапазонов умолчаниями
|
||
for bi_ := 0 to CFG_BAND_COUNT - 1 do
|
||
TSettingsManager.DefaultBand(bi_, FBandCache[bi_]);
|
||
FCurrentBand := 5;
|
||
|
||
// Загружаем JSON настройки
|
||
FSettings := TSettingsManager.Create;
|
||
FSettings.Load;
|
||
if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
|
||
begin
|
||
FVfoA := StartupVfoA;
|
||
FVfoB := StartupVfoB;
|
||
FCenterFreq := FVfoA;
|
||
if StartupRate > 0 then
|
||
begin
|
||
FSampleRate := StartupRate;
|
||
FSpanHz := StartupRate;
|
||
end;
|
||
end;
|
||
// Восстанавливаем размер/позицию окна при старте (до подключения устройства)
|
||
RestoreWindowBounds;
|
||
// S-метр позиционируем после рестора размера окна
|
||
// (будет пересчитан в первом тике SpectrumTimerTick)
|
||
FLastSMeter := -130;
|
||
|
||
// Загружаем настройки веб-сервера из JSON и создаём сервер
|
||
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;
|
||
FSettings.LoadCATSettings(FCATLastGlobal);
|
||
FLightTheme := 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;
|
||
FDisplayFPS := 60;
|
||
FFreqMhzDigits := 3;
|
||
FSplitterRatio := 0.40;
|
||
FSplitterDrag := False;
|
||
|
||
FSpecView := TSpectrumView.Create;
|
||
FSpecView.VfoOverlay := FVfoOverlay;
|
||
FSpecView.VfoA := FVfoA;
|
||
FSpecView.VfoB := FVfoB;
|
||
FSpecView.ActiveVfo := FActiveVfo;
|
||
FSpecView.CenterFreq := FCenterFreq;
|
||
FSpecView.SpanHz := FSpanHz;
|
||
FSpecView.Mode := FMode;
|
||
FSpecView.FilterBW := FFilterBW;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
FSpecView.WfAGCEnabled := FWfAGCEnabled;
|
||
FSpecView.WfNFEnabled := FWfNFEnabled;
|
||
FSpecView.WfManualHigh := FWfManualHigh;
|
||
FSpecView.WfManualLow := FWfManualLow;
|
||
FSpecView.WfAGCOffset := FWfAGCOffset;
|
||
FSpecView.SpecRefLevel := FSpecRefLevel;
|
||
FSpecView.SpecRange := FSpecRange;
|
||
FSpecView.SpecGridStep := FSpecGridStep;
|
||
// TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k)
|
||
FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE;
|
||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
||
FSpecView.PAMaxPower := FPAMaxPower;
|
||
|
||
FNetwork := THPSDRNetwork.Create;
|
||
FNetwork.OnDeviceFound := OnDeviceFound;
|
||
FNetwork.OnHPStatus := OnHPStatusCB;
|
||
FNetwork.OnDDCIQ := OnDDCIQCB;
|
||
FNetwork.OnMicPacket := OnMicPacketCB;
|
||
// Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано).
|
||
TSettingsManager.DefaultTX(FTXSettings);
|
||
TSettingsManager.DefaultAlex(FAlexSettings);
|
||
TSettingsManager.DefaultXvtr(FXvtrSettings);
|
||
FCurrentXvtr := -1;
|
||
|
||
BuildUI;
|
||
ResizeSMeter;
|
||
ApplyDarkTheme;
|
||
UpdateVfoDisplay;
|
||
UpdateFilterButtons;
|
||
|
||
// DSP Engine — создаём объект, Open вызовется при нажатии START
|
||
// (FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек)
|
||
FWDSPReady := False;
|
||
// BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz.
|
||
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
|
||
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
|
||
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
|
||
FDSPEngine.OnAudio := OnAudioReady;
|
||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
|
||
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
|
||
FDSPEngine.OnPullMicSamples := PullSoundCardMic;
|
||
|
||
// Audio output/input — создаём объекты сейчас, открываем после показа формы
|
||
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
|
||
FAudioOut := TAudioOutput.Create(48000);
|
||
FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize;
|
||
FAudioIn := TAudioInput.Create(48000);
|
||
|
||
FMeterTimer := TTimer.Create(Self);
|
||
FMeterTimer.Interval := 100;
|
||
FMeterTimer.OnTimer := MeterTimerTick;
|
||
FMeterTimer.Enabled := True;
|
||
|
||
|
||
FSpectrumTimer := TTimer.Create(Self);
|
||
FSpectrumTimer.Interval := 50;
|
||
FSpectrumTimer.OnTimer := SpectrumTimerTick;
|
||
FSpectrumTimer.Enabled := True;
|
||
|
||
// Однократный таймер — инициализация аудио после показа формы
|
||
FAfterShowTimer := TTimer.Create(Self);
|
||
FAfterShowTimer.Interval := 200;
|
||
FAfterShowTimer.OnTimer := AfterShowTick;
|
||
FAfterShowTimer.Enabled := True;
|
||
|
||
OnMouseWheel := FormMouseWheel;
|
||
|
||
// Проверяем и при необходимости добавляем исключение в Windows Firewall
|
||
// (UDP входящий трафик для HPSDR Protocol 2)
|
||
{$IFDEF WINDOWS}
|
||
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
|
||
{$ENDIF}
|
||
end;
|
||
|
||
procedure TMainForm.FormDestroy(Sender: TObject);
|
||
begin
|
||
FMeterTimer.Enabled := False;
|
||
FSpectrumTimer.Enabled := False;
|
||
if FNetwork.Running then
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
FNetwork.Disconnect;
|
||
FNetwork.Free;
|
||
FAudioOut.Close;
|
||
FAudioOut.Free;
|
||
FAudioIn.Close;
|
||
FAudioIn.Free;
|
||
FDSPEngine.Close;
|
||
FDSPEngine.Free;
|
||
FreeAndNil(FSpecView);
|
||
|
||
// Сохраняем настройки при закрытии
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
if FDevConnected then
|
||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||
end;
|
||
// Размер окна сохраняем всегда (не зависит от подключения)
|
||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||
FSettings.Save;
|
||
FSettings.Free;
|
||
FWebServer.Stop;
|
||
FWebServer.Free;
|
||
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
|
||
if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end;
|
||
FreeAndNil(FCATEngine);
|
||
end;
|
||
|
||
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
|
||
begin
|
||
// Сохраняем позицию здесь — окно ещё полностью видимо и стабильно.
|
||
// В FormDestroy Qt-виджет может быть уже в нестабильном состоянии.
|
||
SaveWindowBounds;
|
||
// Останавливаем трансивер как при нажатии STOP
|
||
if FNetwork.Connected then
|
||
begin
|
||
if FRunning then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
if FWDSPReady then FDSPEngine.SetTXRun(False);
|
||
end;
|
||
FNetwork.Disconnect;
|
||
end;
|
||
CloseAction := caFree;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Build UI (без анонимных процедур и inline var)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.BuildUI;
|
||
const
|
||
BTN_H = 26;
|
||
BTN_SM = 24;
|
||
LEFT_W = 232;
|
||
TOP_VFO_W = 244;
|
||
TOP_VFO_FREQ_H = 47;
|
||
TOP_VFO_BTN_H = 14;
|
||
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(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(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, 72);
|
||
PanelTX.BevelOuter := bvNone;
|
||
MakeLbl(PanelTX, 'TX', 4, 2);
|
||
|
||
W := (LEFT_W - 6) div 4;
|
||
BtnMOX := MakeBtn(PanelTX, 'MOX', 2, 16, W - 2, BTN_SM, BtnMOXClick);
|
||
BtnTUN := MakeBtn(PanelTX, 'TUN', W + 2, 16, W - 2, BTN_SM, BtnTUNClick);
|
||
|
||
LblDrv := TLabel.Create(Self);
|
||
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 53;
|
||
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 := 50;
|
||
TrkDrive.Width := 150; TrkDrive.Height := 16;
|
||
TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50;
|
||
TrkDrive.OnChange := TrkDriveChange;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// 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);
|
||
W := (LEFT_W - 6) div 6;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelBands, BAND_NAMES[i],
|
||
2 + (i mod 6) * W,
|
||
16 + (i div 6) * 27,
|
||
W - 2, BTN_SM, BtnBandClick);
|
||
B.Tag := i;
|
||
BtnBand[i] := B;
|
||
end;
|
||
// 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);
|
||
W := (LEFT_W - 6) div 4;
|
||
for i := 0 to MODE_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelMode, MODE_NAMES[i],
|
||
2 + (i mod 4) * W,
|
||
16 + (i div 4) * 27,
|
||
W - 2, BTN_SM, BtnModeClick);
|
||
B.Tag := i;
|
||
BtnMode[i] := B;
|
||
StyleButton(B, i = FMode);
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// Filter selector (10 кнопок в 2 ряда по 5)
|
||
PanelFilter := TPanel.Create(Self);
|
||
PanelFilter.Parent := PanelLeft;
|
||
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelFilter.BevelOuter := bvNone;
|
||
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
||
W := (LEFT_W - 6) div 5;
|
||
for i := 0 to FILT_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelFilter, '---',
|
||
2 + (i mod 5) * W,
|
||
16 + (i div 5) * 27,
|
||
W - 2, BTN_SM, BtnFilterClick);
|
||
B.Tag := i;
|
||
BtnFilter[i] := B;
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// 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, 4);
|
||
W := LEFT_W div 4 - 2; // button width ~56px
|
||
BtnFMSQ := MakeBtn(PanelFMSQ, 'SQL', 2, 20, W, BTN_SM, BtnFMSQClick);
|
||
StyleButton(BtnFMSQ, False);
|
||
// Slider: vertically centred with button text (button top=20, h=24 → center=32)
|
||
// Reserve 28px on the right for the numeric label
|
||
TrkFMSQ := TFlatSlider.Create(Self);
|
||
TrkFMSQ.Parent := PanelFMSQ;
|
||
TrkFMSQ.Left := W + 6;
|
||
TrkFMSQ.Top := 25; // 32 - 14/2 = 25
|
||
TrkFMSQ.Width := LEFT_W - W - 6 - 30 - 4; // leave 30px for label + margins
|
||
TrkFMSQ.Height := 14;
|
||
TrkFMSQ.Min := 0; TrkFMSQ.Max := 100;
|
||
TrkFMSQ.Position := 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 := 25;
|
||
LblFMSQ.Width := 26;
|
||
LblFMSQ.Caption := IntToStr(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, 4);
|
||
W := LEFT_W div 3 - 2;
|
||
BtnFMCTCSS := MakeBtn(PanelFMCTCSS, 'CTCSS', 2, 20, W, BTN_H, BtnFMCTCSSClick);
|
||
StyleButton(BtnFMCTCSS, False);
|
||
BtnFMCTCSSTone := MakeBtn(PanelFMCTCSS, CTCSS_NAMES[0], W + 4, 20, LEFT_W - W - 8, BTN_H, BtnFMCTCSSToneClick);
|
||
StyleButton(BtnFMCTCSSTone, False);
|
||
|
||
FCTCSSDropDown := TFlatDropDown.Create(BtnFMCTCSSTone, PanelLeft, 4, BTN_SM);
|
||
FCTCSSDropDown.SetItems(CTCSS_NAMES);
|
||
FCTCSSDropDown.SetItemIndex(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, 4);
|
||
W := LEFT_W div 3 - 2;
|
||
BtnFMStep := MakeBtn(PanelFMStep, 'STEP', 2, 20, W, BTN_H, BtnFMStepClick);
|
||
StyleButton(BtnFMStep, FFMStepOn);
|
||
BtnFMStepSel := MakeBtn(PanelFMStep, FM_STEP_NAMES[FFMStepIdx],
|
||
W + 4, 20, LEFT_W - W - 8, BTN_H, BtnFMStepSelClick);
|
||
StyleButton(BtnFMStepSel, False);
|
||
|
||
FStepDropDown := TFlatDropDown.Create(BtnFMStepSel, PanelLeft, FM_STEP_COUNT, BTN_SM);
|
||
FStepDropDown.SetItems(FM_STEP_NAMES);
|
||
FStepDropDown.SetItemIndex(FFMStepIdx);
|
||
FStepDropDown.OnSelect := OnStepDropDownSelect;
|
||
|
||
// CTUN / DUP buttons (display-related)
|
||
BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick);
|
||
BtnCTun.Tag := 0;
|
||
StyleButton(BtnCTun, FCTun);
|
||
|
||
BtnDUP := MakeBtn(PanelLeft, 'DUP',
|
||
2 + (LEFT_W div 3) + 0, Y, LEFT_W div 3 - 2, BTN_H, BtnDUPClick);
|
||
BtnDUP.Tag := 0;
|
||
StyleButton(BtnDUP, FDisplayDuplex);
|
||
|
||
Inc(Y, BTN_H + 4);
|
||
|
||
// RX controls
|
||
PanelRX := TPanel.Create(Self);
|
||
PanelRX.Parent := PanelLeft;
|
||
PanelRX.SetBounds(0, Y, LEFT_W, 122);
|
||
PanelRX.BevelOuter := bvNone;
|
||
|
||
// AGC mode — 5 кнопок в ряд
|
||
MakeLbl(PanelRX, 'AGC', 4, 4);
|
||
W := (LEFT_W - 10) div 5;
|
||
BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 2, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag:=0;
|
||
BtnAGCMode[1] := MakeBtn(PanelRX,'MED', W+4, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag:=1;
|
||
BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 2*W+6, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag:=2;
|
||
BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 3*W+8, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag:=3;
|
||
BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 4*W+10, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag:=4;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
|
||
|
||
// AGC level slider
|
||
MakeLbl(PanelRX, 'THRESH', 4, 50);
|
||
TrkAGC := TFlatSlider.Create(Self);
|
||
TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 46;
|
||
TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 16;
|
||
TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → −20..-120 dBm
|
||
TrkAGC.Position := FAGCTop;
|
||
TrkAGC.Reversed := True;
|
||
TrkAGC.OnChange := TrkAGCChange;
|
||
|
||
LblAGCTop := TLabel.Create(Self);
|
||
LblAGCTop.Parent := PanelRX;
|
||
LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 50;
|
||
LblAGCTop.Caption := Format('-%ddB', [FAGCTop]);
|
||
LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New';
|
||
LblAGCTop.Font.Size := 7;
|
||
|
||
// VOL
|
||
MakeLbl(PanelRX, 'VOL', 4, 74);
|
||
TrkVolume := TFlatSlider.Create(Self);
|
||
TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 70;
|
||
TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 16;
|
||
TrkVolume.Min := 0; TrkVolume.Max := 100;
|
||
TrkVolume.Position := FVolume;
|
||
TrkVolume.OnChange := TrkVolumeChange;
|
||
|
||
// NR NB SNB ANF MUTE
|
||
W := (LEFT_W - 10) div 5;
|
||
BtnNR := MakeBtn(PanelRX, 'NR', 2, 96, W, BTN_SM, BtnNRClick);
|
||
BtnNB := MakeBtn(PanelRX, 'NB', W+4, 96, W, BTN_SM, BtnNBClick);
|
||
BtnSNB := MakeBtn(PanelRX, 'SNB', 2*W+6, 96, W, BTN_SM, BtnSNBClick);
|
||
BtnANF := MakeBtn(PanelRX, 'ANF', 3*W+8, 96, W, BTN_SM, BtnANFClick);
|
||
BtnMute:= MakeBtn(PanelRX, 'MUTE', 4*W+10, 96, W, BTN_SM, BtnMuteClick);
|
||
|
||
Inc(Y, 126);
|
||
|
||
// ---- 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(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;
|
||
|
||
PbSpectrum := TPaintBox.Create(Self);
|
||
PbSpectrum.Parent := PanelRight;
|
||
PbSpectrum.OnPaint := FSpecView.PaintSpectrum;
|
||
PbSpectrum.OnMouseDown := PbSpectrumMouseDown;
|
||
PbSpectrum.OnDblClick := PbSpectrumDblClick;
|
||
PbSpectrum.OnMouseMove := PbSpectrumMouseMove;
|
||
PbSpectrum.OnMouseUp := PbSpectrumMouseUp;
|
||
PbSpectrum.OnMouseLeave := PbSpectrumMouseLeave;
|
||
|
||
PbRuler := TPaintBox.Create(Self);
|
||
PbRuler.Parent := PanelRight;
|
||
PbRuler.OnPaint := FSpecView.PaintRuler;
|
||
PbRuler.Cursor := crDefault;
|
||
FSpecView.PbRuler := PbRuler;
|
||
|
||
// ---- Splitter между спектром+линейкой и водопадом ----
|
||
PanelSplitter := TPanel.Create(Self);
|
||
PanelSplitter.Parent := PanelRight;
|
||
PanelSplitter.BevelOuter := bvNone;
|
||
PanelSplitter.Color := TColor($00303030);
|
||
PanelSplitter.Cursor := crVSplit;
|
||
PanelSplitter.Height := 5;
|
||
PanelSplitter.OnMouseDown := SplitterMouseDown;
|
||
PanelSplitter.OnMouseMove := SplitterMouseMove;
|
||
PanelSplitter.OnMouseUp := SplitterMouseUp;
|
||
|
||
PbWaterfall := TPaintBox.Create(Self);
|
||
PbWaterfall.Parent := PanelRight;
|
||
PbWaterfall.OnPaint := FSpecView.PaintWaterfall;
|
||
PbWaterfall.OnMouseDown := PbWaterfallMouseDown;
|
||
PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
|
||
PbWaterfall.OnMouseUp := PbWaterfallMouseUp;
|
||
|
||
// 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_W = 20;
|
||
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;
|
||
|
||
procedure TMainForm.ResizeSpectrumPanels;
|
||
const
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60; // минимальная высота спектра
|
||
MIN_WH = 40; // минимальная высота водопада
|
||
var
|
||
RW, RH, SH, WH, TopOff: 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;
|
||
TopOff := 0;
|
||
// 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);
|
||
DP(PanelRX); DP(PanelTX);
|
||
DP(PanelRight);
|
||
|
||
PbSpectrum.Color := T.BG;
|
||
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 FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
||
if FStepDropDown <> nil then FStepDropDown.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);
|
||
|
||
// 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;
|
||
FSettings.SaveTheme(V);
|
||
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 FSpecView.SpectrumDirty := True;
|
||
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(FVfoA);
|
||
FreqDispB.Frequency := Round(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 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 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 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 := FVfoA;
|
||
FSpecView.VfoB := FVfoB;
|
||
FSpecView.ActiveVfo := FActiveVfo;
|
||
FSpecView.CenterFreq := FCenterFreq;
|
||
FSpecView.SpanHz := FSpanHz;
|
||
FSpecView.Mode := FMode;
|
||
FSpecView.FilterBW := FFilterBW;
|
||
if FMode = MODE_FM then
|
||
FSpecView.FMGridStepHz := FM_STEP_HZ[FFMStepIdx]
|
||
else
|
||
FSpecView.FMGridStepHz := 0;
|
||
// TX overlay следует за активной TX-частотой (учитывает split). При CTUN
|
||
// спектр TX будет отрисован на VFO, а не в центре дисплея.
|
||
FSpecView.TXFreq := ActiveTXFreqHz;
|
||
// Какой VFO привязан к TX (для split-полос фильтра): 1=B при FSplitTxB, иначе FActiveVfo.
|
||
if FSplitTxB then FSpecView.TXVfoIndex := 1
|
||
else FSpecView.TXVfoIndex := FActiveVfo;
|
||
end;
|
||
|
||
|
||
// ===========================================================================
|
||
// Timers
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.MeterTimerTick(Sender: TObject);
|
||
const
|
||
AVG_ALPHA = 0.25; // EMA для сглаженного среднего уровня
|
||
ZONE_DB = 6.0; // полуширина зоны вокруг среднего (дБ)
|
||
// Адаптивный alpha: базовый + ускорение при большой разнице
|
||
PEAK_BASE = 0.06; // базовая скорость пика
|
||
MIN_BASE = 0.06; // базовая скорость минимума
|
||
ACCEL_FACTOR = 0.018; // ускорение на каждый dB разницы
|
||
MAX_ALPHA = 0.85; // максимальная скорость (не мгновенно)
|
||
var
|
||
PeakTarget, MinTarget: Double;
|
||
PeakDiff, MinDiff: Double;
|
||
PeakAlpha, MinAlpha: Double;
|
||
begin
|
||
if not FRunning then
|
||
begin
|
||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||
Exit;
|
||
end;
|
||
|
||
|
||
// Сглаженное среднее (EMA)
|
||
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FLastSMeter * AVG_ALPHA;
|
||
|
||
// Цели для Peak и Min
|
||
PeakTarget := Max(FLastSMeter, FSMeterAvg + ZONE_DB);
|
||
MinTarget := Min(FLastSMeter, FSMeterAvg - ZONE_DB);
|
||
|
||
// Адаптивный alpha: чем дальше от цели — тем быстрее догоняем
|
||
PeakDiff := Abs(PeakTarget - FSMeterPeak);
|
||
MinDiff := Abs(MinTarget - FSMeterMin);
|
||
PeakAlpha := Min(MAX_ALPHA, PEAK_BASE + PeakDiff * ACCEL_FACTOR);
|
||
MinAlpha := Min(MAX_ALPHA, MIN_BASE + MinDiff * ACCEL_FACTOR);
|
||
|
||
FSMeterPeak := FSMeterPeak + PeakAlpha * (PeakTarget - FSMeterPeak);
|
||
FSMeterMin := FSMeterMin + MinAlpha * (MinTarget - FSMeterMin);
|
||
|
||
FSpecView.LastSMeter := FLastSMeter;
|
||
FSpecView.SMeterPeak := FSMeterPeak;
|
||
FSpecView.SMeterMin := FSMeterMin;
|
||
FSpecView.LastFwdW := FLastFwdW;
|
||
FSpecView.LastSWR := FLastSWR;
|
||
FSpecView.Transmitting := FTransmitting;
|
||
|
||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||
// PWR/SWR метки и бары обновляем здесь (10 Гц), а не в DoUpdateStatus —
|
||
// HP Status пакеты идут ~150 раз/с, при таком rate цифры дёргаются.
|
||
UpdateTXMeters;
|
||
|
||
if FWDSPReady then
|
||
begin
|
||
if FTransmitting then
|
||
begin
|
||
if FDSPEngine.TXMicSource = txmsSoundCard then
|
||
SetStatusText(5, 'TX sound card')
|
||
else
|
||
SetStatusText(5, 'TX radio mic');
|
||
end
|
||
else
|
||
SetStatusText(5, 'TX idle');
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||
var
|
||
NowTick: QWord;
|
||
ElapsedTick: QWord;
|
||
WebStatusText, WebBoardText, WebIPText, WebSupplyText: string;
|
||
WebPLLText, WebRXText, WebTXText, WebSeqText: string;
|
||
begin
|
||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
|
||
// что вызвало бы сброс буфера водопада на каждом тике.
|
||
if (FSpectrumWidth = 0) or
|
||
(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 FRunning and FNetwork.Running then
|
||
begin
|
||
NowTick := GetTickCount64;
|
||
ElapsedTick := NowTick - FRXStartTime;
|
||
// Ждём первый пакет 5 секунд после старта
|
||
if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FRunning := False;
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(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);
|
||
FRunning := False;
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
SetRadioOfflineStatus('Connection lost');
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
if 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 FWDSPReady then
|
||
begin
|
||
FLastSMeter := FDSPEngine.GetSMeterDBm;
|
||
FDSPEngine.SetSpectrumWidth(FSpectrumWidth);
|
||
Inc(FAgcLineCounter);
|
||
if FAgcLineCounter >= 6 then
|
||
begin
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
FSpecView.AGCThresh := FDSPEngine.AGCThresh;
|
||
FSpecView.AGCHangLevel := FDSPEngine.AGCHangLevel;
|
||
FAgcLineCounter := 0;
|
||
end;
|
||
end;
|
||
FSpecView.WDSPReady := FWDSPReady;
|
||
// Обновляем оверлей если видим
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.UpdateSMeter(FLastSMeter);
|
||
FVfoOverlay.UpdateVfo(FVfoA);
|
||
PositionVfoOverlay;
|
||
end;
|
||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||
if FSpectrumDirty then
|
||
begin
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if FSpecView.NeedsRulerRedraw then
|
||
PbRuler.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
if FWaterfallDirty then
|
||
begin
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
FWaterfallDirty := False;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Не подключены — показываем чистый экран (без анимации)
|
||
if not FNetwork.Connected then
|
||
begin
|
||
// При первом старте показываем демо-спектр
|
||
if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight);
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Подключены, но не запущены — статичный спектр (шум)
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
end;
|
||
|
||
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
|
||
if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then
|
||
begin
|
||
if FSpectrumDirty then
|
||
FSpecView.DrawSpectrum;
|
||
if FWaterfallDirty then
|
||
FSpecView.DrawWaterfall;
|
||
if FSpectrumDirty then
|
||
PbSpectrum.Invalidate;
|
||
if FSpecView.NeedsRulerRedraw then
|
||
PbRuler.Invalidate;
|
||
if FWaterfallDirty then
|
||
PbWaterfall.Invalidate;
|
||
FWaterfallDirty := False;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
|
||
// Пушим текущее состояние в веб-клиенты (если есть)
|
||
if Assigned(FWebServer) then
|
||
begin
|
||
if FNetwork.Connected then
|
||
begin
|
||
if FRunning then
|
||
WebStatusText := 'Running'
|
||
else
|
||
WebStatusText := 'Connected';
|
||
WebBoardText := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType);
|
||
WebIPText := 'IP: ' + FNetwork.Device.IPAddress;
|
||
end
|
||
else
|
||
begin
|
||
WebStatusText := 'Disconnected';
|
||
WebBoardText := 'Board --';
|
||
WebIPText := 'IP --';
|
||
end;
|
||
|
||
if not FRunning then
|
||
begin
|
||
WebSupplyText := 'Supply --';
|
||
WebPLLText := 'PLL --';
|
||
end
|
||
else
|
||
begin
|
||
if FLastPLLLock then
|
||
WebPLLText := 'PLL OK'
|
||
else
|
||
WebPLLText := 'PLL?';
|
||
|
||
if not BoardSupportsSupplyVoltage(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 FRunning then
|
||
WebRXText := 'RX running'
|
||
else
|
||
WebRXText := 'RX idle';
|
||
|
||
if FTuning then
|
||
WebTXText := 'TX tune'
|
||
else if FTransmitting then
|
||
WebTXText := 'TX active'
|
||
else
|
||
WebTXText := 'TX idle';
|
||
|
||
if FSeqErrorCount > 0 then
|
||
WebSeqText := Format('SEQ ERR DDC%d', [FLastSeqErrorDDC])
|
||
else if FRunning then
|
||
WebSeqText := 'SEQ OK'
|
||
else
|
||
WebSeqText := 'SEQ --';
|
||
|
||
FWebServer.PushSpectrum(
|
||
FSpectrumBuf, 1024,
|
||
FWaterfallBuf,
|
||
FLastSMeter,
|
||
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
|
||
FSpanHz, FVolume,
|
||
FWfAGCEnabled, FWfNFEnabled,
|
||
FCurrentBand,
|
||
FRunning and FNetwork.Connected,
|
||
FRunning, FMuted, FCTun,
|
||
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
|
||
FCenterFreq, FFilter,
|
||
FVfoB, FActiveVfo,
|
||
FTransmitting, TrkDrive.Position,
|
||
FAtten, FTuning, FDisplayDuplex,
|
||
FLastFwdW, FLastSWR, 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(Self, Dev, Entry);
|
||
try
|
||
M := Sync.Execute;
|
||
TThread.Synchronize(nil, M);
|
||
finally
|
||
Sync.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
Idx := Length(FDevices);
|
||
SetLength(FDevices, Idx + 1);
|
||
FDevices[Idx].Dev := Dev;
|
||
FDevices[Idx].Display := Entry;
|
||
Inc(FDeviceCount);
|
||
|
||
if Assigned(FDeviceDialog) then
|
||
FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType);
|
||
|
||
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(FNetwork.Device.BoardType) then
|
||
begin
|
||
if FNetwork.Device.BoardType = 5 then
|
||
SupplyV := ADCToSupplyVolts(
|
||
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
|
||
FNetwork.Device.BoardType)
|
||
else
|
||
SupplyV := ADCToSupplyVolts(
|
||
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
|
||
FNetwork.Device.BoardType);
|
||
|
||
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
|
||
SupplyA := ADCToSupplyCurrent(
|
||
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
|
||
FNetwork.Device.BoardType);
|
||
end;
|
||
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
|
||
// ExciterPwr это мощность TX, не уровень принятого сигнала.
|
||
FwdW := ADCToWatts100(FwdPwr);
|
||
// FTransmitting — наш программный флаг (MOX/TUN/HW-PTT через ApplyMOX).
|
||
// HPS_PTT-бит в StatusBits — это вход аппаратного PTT (footswitch/mic),
|
||
// а не «трансивер сейчас передаёт», поэтому только на него полагаться нельзя.
|
||
IsTx := 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(Self, 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 <> FHWPTTActive then
|
||
begin
|
||
FHWPTTActive := HWPTT;
|
||
if not FTuning then
|
||
begin
|
||
if HWPTT then
|
||
begin
|
||
// Press: не перекрываем ручной MOX; запоминаем что мы включили TX.
|
||
if not BtnMOX.Active then
|
||
begin
|
||
ApplyMOX(True);
|
||
FHWPTTStartedTX := True;
|
||
end;
|
||
end else
|
||
begin
|
||
// Release: выключаем TX только если он был включён через HWPTT.
|
||
// Нельзя проверять BtnMOX.Active — ApplyMOX(True) сам его выставляет.
|
||
if FHWPTTStartedTX then
|
||
begin
|
||
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 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(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 FWDSPReady then
|
||
begin
|
||
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
|
||
Integer(Data.SamplesPerFrame[1]);
|
||
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
|
||
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
|
||
end;
|
||
|
||
// 2. Запоминаем последний seq — статус обновит таймер (без Synchronize в горячем пути)
|
||
FLastDDCSeq := Seq;
|
||
FLastDDCIndex := DDCIndex;
|
||
end;
|
||
|
||
procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||
begin
|
||
SetStatusText(4, 'RX running');
|
||
end;
|
||
|
||
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
|
||
begin
|
||
// HW mic-стрим уходит в WDSP только если выбран источник Radio.
|
||
// Иначе сэмплы игнорируются (mic берётся со звуковой карты).
|
||
if FTransmitting and FWDSPReady and
|
||
(FDSPEngine.TXMicSource = txmsRadio) then
|
||
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX settings — применение к WDSP, сборка DUC Specific, sound-card pull
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TMainForm.BuildMicLineSelectByte: Byte;
|
||
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
|
||
// bit 0 — Line In (1 = line-in вместо mic-in)
|
||
// bit 1 — Mic Boost (+20 dB); не ставим когда Line In
|
||
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
|
||
// bit 3 — Tip/Ring (0=Tip, 1=Ring; Orion/OrionMkII)
|
||
// bit 4 — Orion mic bias (1 = enable +5V bias)
|
||
// bit 5 — Saturn XLR/balanced (не используем)
|
||
begin
|
||
Result := 0;
|
||
if FTXSettings.MicLineIn then
|
||
Result := Result or $01 // bit 0: Line In
|
||
else if FTXSettings.MicBoost then
|
||
Result := Result or $02; // bit 1: Boost только при Mic In
|
||
if not FTXSettings.MicPTTEnabled then Result := Result or $04; // bit 2: инверсия
|
||
if FTXSettings.MicTipRing then Result := Result or $08; // bit 3: Ring
|
||
if FTXSettings.MicBias then Result := Result or $10; // bit 4: bias
|
||
end;
|
||
|
||
procedure TMainForm.SendDUCSpecificFromSettings;
|
||
var
|
||
DUCPkt: TDUCSpecificPacket;
|
||
RateKsps: Word;
|
||
begin
|
||
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
||
RateKsps := 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((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(FTXSettings.AttOnTX, 0, 31));
|
||
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
|
||
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
|
||
FNetwork.SendDUCSpecific(DUCPkt);
|
||
end;
|
||
|
||
function TMainForm.DefaultMicSource: TTXMicSource;
|
||
// Mic source для MOX-кнопки и Web-TX: если настроена звуковая карта — берём с неё,
|
||
// иначе используем HW mic-стрим от трансивера.
|
||
// HW PTT всегда переключает на txmsRadio непосредственно в ApplyMOX.
|
||
begin
|
||
if FAudioInDevName <> '' then Result := txmsSoundCard
|
||
else Result := txmsRadio;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyTXSettingsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
// Mic source — выставляем значение по умолчанию для MOX-кнопки;
|
||
// при реальном PTT ApplyMOX скорректирует источник.
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
FDSPEngine.SetMicGain(FTXSettings.MicGainDB);
|
||
FDSPEngine.SetTXFilterFull(FTXSettings.FilterLow, FTXSettings.FilterHigh,
|
||
FTXSettings.FilterNC, FTXSettings.FilterMP,
|
||
FTXSettings.FilterWindow);
|
||
FDSPEngine.SetTXCompressor(FTXSettings.CompressorOn, FTXSettings.CompressorGain);
|
||
FDSPEngine.SetTXLeveler(FTXSettings.LevelerOn, FTXSettings.LevelerTop,
|
||
FTXSettings.LevelerDecay);
|
||
FDSPEngine.SetTXALC(FTXSettings.ALCOn, FTXSettings.ALCMaxGain,
|
||
FTXSettings.ALCDecay);
|
||
FDSPEngine.SetTXPhaseRot(FTXSettings.PhaseRotOn, FTXSettings.PhaseRotStages,
|
||
FTXSettings.PhaseRotFreq);
|
||
FDSPEngine.SetTXEQ(FTXSettings.EQOn, FTXSettings.EQNumBands,
|
||
FTXSettings.EQGains, FTXSettings.EQFreqs);
|
||
FDSPEngine.SetTXAMCarrierLevel(FTXSettings.AMCarrierLevel);
|
||
FDSPEngine.SetTXFMParams(FTXSettings.FMDeviation, FTXSettings.FMLowCut,
|
||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||
FDSPEngine.SetTXCTCSS(FTXSettings.CTCSSOn, FTXSettings.CTCSSFreq);
|
||
// TX display
|
||
FDSPEngine.SetTXFFTParams(FTXSettings.TXFFTSize, FTXSettings.TXWindowType);
|
||
FDSPEngine.SetTXSpectrumDisplay(FTXSettings.TXSpecDetector,
|
||
FTXSettings.TXSpecAvgMode,
|
||
FTXSettings.TXSpecAvgTimeMS);
|
||
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
|
||
FTXSettings.TXWfAvgMode,
|
||
FTXSettings.TXWfAvgTimeMS);
|
||
end;
|
||
|
||
function TMainForm.PullSoundCardMic(MaxN: Integer): Integer;
|
||
// Колбэк, который TTXDSPThread зовёт при TXMicSource=txmsSoundCard.
|
||
// Тянем только реально доступные сэмплы из FAudioIn — иначе мы пушили бы
|
||
// нули и mic-цепь в WDSP получала бы тишину вместо реального сигнала.
|
||
// Читаем пачкой в локальный буфер и пушим одним вызовом — экономим
|
||
// будилки семафора и предотвращаем подмену TX-блока тишиной.
|
||
var
|
||
i, n: Integer;
|
||
Buf: array[0..2047] of Double;
|
||
begin
|
||
Result := 0;
|
||
if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit;
|
||
n := FAudioIn.Available;
|
||
if n > MaxN then n := MaxN;
|
||
if n > Length(Buf) then n := Length(Buf);
|
||
for i := 0 to n - 1 do
|
||
Buf[i] := FAudioIn.ReadSample;
|
||
if n > 0 then
|
||
FDSPEngine.PushTXMicSamplesD(Buf, n);
|
||
Result := n;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Button handlers
|
||
// ===========================================================================
|
||
|
||
{ Поток открытия — отдельный класс, без анонимных процедур }
|
||
// Открываем WDSP в фоновом потоке чтобы не блокировать UI при нажатии START
|
||
type
|
||
TWDSPOpenThread = class(TThread)
|
||
private
|
||
FForm: TMainForm;
|
||
FResult: Boolean;
|
||
procedure SyncDone;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AForm: TMainForm);
|
||
end;
|
||
|
||
constructor TWDSPOpenThread.Create(AForm: TMainForm);
|
||
begin
|
||
inherited Create(False);
|
||
FForm := AForm;
|
||
FResult := False;
|
||
FreeOnTerminate := True;
|
||
end;
|
||
|
||
procedure TWDSPOpenThread.Execute;
|
||
var
|
||
M: TThreadMethod;
|
||
begin
|
||
try
|
||
FResult := FForm.FDSPEngine.Open;
|
||
except
|
||
FResult := False;
|
||
end;
|
||
M := SyncDone;
|
||
TThread.Synchronize(nil, M);
|
||
end;
|
||
|
||
procedure TWDSPOpenThread.SyncDone;
|
||
begin
|
||
FForm.OnWDSPOpenDone(FResult);
|
||
end;
|
||
|
||
type
|
||
TDiscoverThread = class(TThread)
|
||
private
|
||
FNet: THPSDRNetwork;
|
||
FForm: TMainForm;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(ANet: THPSDRNetwork; AForm: TMainForm);
|
||
end;
|
||
|
||
constructor TDiscoverThread.Create(ANet: THPSDRNetwork; AForm: TMainForm);
|
||
begin
|
||
FNet := ANet;
|
||
FForm := AForm;
|
||
FreeOnTerminate := True;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TDiscoverThread.Execute;
|
||
var
|
||
Devs: THPSDRDeviceArray;
|
||
Sync: TDDCSeqSync;
|
||
M: TThreadMethod;
|
||
begin
|
||
Devs := FNet.Discover(2000);
|
||
if Length(Devs) = 0 then
|
||
begin
|
||
// DDCIdx = -1 is the sentinel for "no device found"
|
||
Sync := TDDCSeqSync.Create(FForm, -1, 0);
|
||
try
|
||
M := Sync.Execute;
|
||
TThread.Synchronize(nil, M);
|
||
finally
|
||
Sync.Free;
|
||
end;
|
||
end;
|
||
// Devices found are signalled via OnDeviceFound callback during Discover()
|
||
end;
|
||
|
||
{ Перегруженный DoUpdateDDCSeq принимает -1 как «нет устройств» }
|
||
procedure TMainForm.DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
|
||
begin
|
||
if DDCIdx = -1 then
|
||
begin
|
||
if Assigned(FDeviceDialog) then
|
||
begin
|
||
FDeviceDialog.ClearDiscovered;
|
||
FDeviceDialog.AddDiscovered('', '-- no device found --');
|
||
end;
|
||
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(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.Close;
|
||
FreeAndNil(FDSPEngine);
|
||
end;
|
||
FWDSPReady := False;
|
||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||
FDSPEngine.OnAudio := OnAudioReady;
|
||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||
end;
|
||
|
||
function TMainForm.EnsureWDSPWisdom: Boolean;
|
||
var
|
||
WisdomDir: string;
|
||
WisdomPath: string;
|
||
DirA: AnsiString;
|
||
BuildThread: TWisdomBuildThread;
|
||
Dlg: TWisdomProgressDialog;
|
||
begin
|
||
Result := True;
|
||
|
||
if not Assigned(@WDSPwisdom) then Exit;
|
||
|
||
WisdomDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
|
||
WisdomPath := WisdomDir + WDSP_WISDOM_FILE;
|
||
|
||
if FileExists(WisdomPath) then
|
||
begin
|
||
// По рекомендации WDSP вызываем функцию на каждом старте:
|
||
// если файл уже есть, библиотека просто импортирует wisdom.
|
||
DirA := AnsiString(WisdomDir);
|
||
WDSPwisdom(PAnsiChar(DirA));
|
||
Exit;
|
||
end;
|
||
|
||
BtnStartStop.Enabled := False;
|
||
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...');
|
||
FNetwork.DirectIP := '';
|
||
TDiscoverThread.Create(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 FNetwork.Connected then
|
||
begin
|
||
// --- STOP ---
|
||
if FRunning then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
if FWDSPReady then FDSPEngine.SetTXRun(False);
|
||
StyleButton(BtnMOX, False);
|
||
end;
|
||
FRXPacketCount := 0;
|
||
FActiveDDC := 0;
|
||
// Сохраняем текущую частоту/настройки чтобы следующий START восстановил их
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
FSettings.SaveAlex(FDevMAC, FAlexSettings);
|
||
// XVTR LastFreq → актуальная видимая частота в момент остановки
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
end;
|
||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||
FSettings.Save;
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
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 := FSampleRate;
|
||
FoundByIP := False;
|
||
for i := 0 to High(FDevices) do
|
||
if SameText(FDevices[i].Dev.IPAddress, Dev.IPAddress) then
|
||
begin
|
||
FoundByIP := True;
|
||
if FSettings.LoadDevice(FDevices[i].Dev.MAC, PreG, PreBands) and (PreG.SampleRate > 0) then
|
||
PreloadRate := PreG.SampleRate;
|
||
Break;
|
||
end;
|
||
if FoundByIP and (PreloadRate > 0) then
|
||
begin
|
||
FSampleRate := PreloadRate;
|
||
FSpanHz := PreloadRate;
|
||
if (not FWDSPReady) and (Assigned(FDSPEngine)) and (FDSPEngine.SampleRate <> PreloadRate) then
|
||
RecreateDSPEngine(PreloadRate);
|
||
end;
|
||
|
||
// --- Открываем WDSP асинхронно чтобы не блокировать UI ---
|
||
FPendingDev := Dev;
|
||
BtnStartStop.Enabled := False;
|
||
SetStatusText(2, 'Opening DSP...');
|
||
if FWDSPReady then
|
||
// WDSP уже открыт — сразу подключаемся
|
||
DoConnectDevice(Dev)
|
||
else
|
||
// Открываем в фоновом потоке
|
||
TWDSPOpenThread.Create(Self);
|
||
end;
|
||
|
||
procedure TMainForm.OnWDSPOpenDone(Success: Boolean);
|
||
begin
|
||
// Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке
|
||
FWDSPReady := Success;
|
||
if not Success then
|
||
begin
|
||
SetStatusText(2, 'DSP failed');
|
||
BtnStartStop.Enabled := True;
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
ShowMessage('WDSP not loaded.' + LineEnding +
|
||
IfThen(FDSPEngine.LastError <> '', FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding +
|
||
'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).');
|
||
Exit;
|
||
end;
|
||
DoConnectDevice(FPendingDev);
|
||
end;
|
||
|
||
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
||
var
|
||
GenPkt: TGeneralPacket;
|
||
G_Settings: TGlobalSettings;
|
||
i: Integer;
|
||
begin
|
||
BtnStartStop.Enabled := True;
|
||
|
||
if not FNetwork.Connect(Dev) then
|
||
begin
|
||
ShowMessage('Failed to connect to ' + Dev.IPAddress +
|
||
IfThen(FNetwork.LastError <> '', ': ' + FNetwork.LastError, ''));
|
||
Exit;
|
||
end;
|
||
|
||
// --- Загружаем настройки устройства по MAC ---
|
||
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
|
||
FDevConnected := True;
|
||
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
|
||
FCATLastGlobal := G_Settings;
|
||
CATApplySettings(G_Settings);
|
||
// TX-настройки per-device — отдельная JSON-секция "tx"
|
||
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||
// Alex-настройки per-device (антенны/маршрутизация)
|
||
FSettings.LoadAlex(FDevMAC, FAlexSettings);
|
||
FNetwork.SetAlexConfig(FAlexSettings);
|
||
// XVTR-настройки per-device (трансвертеры)
|
||
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
|
||
FCurrentXvtr := -1;
|
||
RebuildXvtrButtons;
|
||
ApplyXvtrToNetwork;
|
||
PushXvtrToWeb;
|
||
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
|
||
FTXSpecRange := FTXSettings.TXSpecRange;
|
||
if FTXSettings.TXSpecGridStep > 0 then
|
||
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
|
||
FVolume := G_Settings.Volume;
|
||
FActiveVfo := G_Settings.ActiveVfo;
|
||
// PA settings
|
||
FPAMaxPower := G_Settings.PAMaxPower;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
FPABandCal[i] := G_Settings.PABandCal[i];
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
FVHFBandCal[i] := G_Settings.VHFBandCal[i];
|
||
// Slider position (0..100) and calibrated drive byte
|
||
TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
|
||
FCurrentBand := G_Settings.LastBand;
|
||
FDriveLevel := CalcDriveByte;
|
||
// SampleRate — глобальный, загружаем до RestoreBand
|
||
if G_Settings.SampleRate > 0 then
|
||
begin
|
||
FSampleRate := G_Settings.SampleRate;
|
||
FSpanHz := FSampleRate;
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.SetCurrentRate(FSampleRate);
|
||
end;
|
||
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
|
||
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
|
||
BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton;
|
||
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
|
||
FWfAGCEnabled := G_Settings.WfAGCEnabled;
|
||
FWfNFEnabled := G_Settings.WfNFEnabled;
|
||
FDitherEnabled := G_Settings.DitherEnabled;
|
||
FRandomEnabled := G_Settings.RandomEnabled;
|
||
FWfManualHigh := G_Settings.WfManualHigh;
|
||
FWfManualLow := G_Settings.WfManualLow;
|
||
FWfAGCOffset := G_Settings.WfAGCOffset;
|
||
FSpecView.WfAGCEnabled := FWfAGCEnabled;
|
||
FSpecView.WfNFEnabled := FWfNFEnabled;
|
||
FSpecView.WfManualHigh := FWfManualHigh;
|
||
FSpecView.WfManualLow := FWfManualLow;
|
||
FSpecView.WfAGCOffset := FWfAGCOffset;
|
||
FSpecView.ResetWfAvgBuf;
|
||
FShowSpectrum := G_Settings.ShowSpectrum;
|
||
FShowWaterfall := G_Settings.ShowWaterfall;
|
||
FDisplayDuplex := G_Settings.DisplayDuplex;
|
||
if BtnDUP <> nil then StyleButton(BtnDUP, 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 FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
|
||
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
|
||
G_Settings.SpecAvgTimeMS);
|
||
FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
|
||
G_Settings.WfAvgTimeMS);
|
||
end;
|
||
// Аудио устройство вывода
|
||
FAudioOut.Close;
|
||
if G_Settings.AudioSampleRate > 0 then
|
||
FAudioOut.SampleRate := G_Settings.AudioSampleRate;
|
||
if G_Settings.AudioOutDevice <> '' then
|
||
begin
|
||
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice);
|
||
FAudioOutDevName := G_Settings.AudioOutDevice;
|
||
end
|
||
else
|
||
begin
|
||
FAudioOut.DeviceIndex := -1;
|
||
FAudioOutDevName := '';
|
||
end;
|
||
FAudioOut.Open;
|
||
// Аудио устройство ввода (TX mic). MicSource (Radio/SoundCard) определяется
|
||
// автоматически в ApplyMOX по источнику PTT — здесь только открываем устройство.
|
||
if G_Settings.AudioInDevice <> '' then
|
||
begin
|
||
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
|
||
FAudioInDevName := G_Settings.AudioInDevice;
|
||
FAudioIn.Open;
|
||
end
|
||
else
|
||
begin
|
||
FAudioIn.DeviceIndex := -1;
|
||
FAudioInDevName := '';
|
||
end;
|
||
end;
|
||
RestoreBand(FCurrentBand);
|
||
|
||
// Восстанавливаем XVTR-режим, если он был активен при выходе.
|
||
// FCurrentBand уже загружен (HF-диапазон, на который пользователь зашёл
|
||
// до XVTR), его FBandCache не пострадает — ActivateXvtrBand только
|
||
// выставит FVfoA на XVTR-частоту и FCurrentXvtr.
|
||
if (G_Settings.LastXvtr >= 0) and (G_Settings.LastXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[G_Settings.LastXvtr].Enabled then
|
||
ActivateXvtrBand(G_Settings.LastXvtr);
|
||
|
||
SetStatusText(1, 'IP: ' + FNetwork.Device.IPAddress);
|
||
SetStatusText(0, 'Board: ' + BoardTypeName(FNetwork.Device.BoardType));
|
||
|
||
// General packet
|
||
FillChar(GenPkt, SizeOf(GenPkt), 0);
|
||
GenPkt.Command := CMD_GENERAL;
|
||
GenPkt.Flags37 := $08;
|
||
GenPkt.Flags38 := $01;
|
||
GenPkt.PAConfig := $01;
|
||
if FNetwork.Device.BoardType = 5 then
|
||
GenPkt.AlexEnable := $03
|
||
else
|
||
GenPkt.AlexEnable := $01;
|
||
FNetwork.SendGeneralPacket(GenPkt);
|
||
|
||
if FNetwork.Device.BoardType in [3, 4, 5] then
|
||
FActiveDDC := 2
|
||
else
|
||
FActiveDDC := 0;
|
||
|
||
// Синхронизируем samplerate WDSP если нужно
|
||
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(FSampleRate);
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
end;
|
||
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
|
||
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
|
||
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
||
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True);
|
||
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
|
||
FRunning := True;
|
||
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) из FTXSettings.
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
|
||
SendDUCSpecificFromSettings;
|
||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||
ApplyTXSettingsToDSP;
|
||
ApplyModeFilter;
|
||
|
||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||
if FWDSPReady then
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета
|
||
|
||
BtnStartStop.Caption := 'STOP';
|
||
StyleButton(BtnStartStop, True);
|
||
SetStatusText(2, 'Running');
|
||
BtnMOX.Enabled := True;
|
||
end;
|
||
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
|
||
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
|
||
// Перестройка всегда происходит на частоту активного VFO (не на оффсет).
|
||
// CTUN при этом сбрасывается чтобы не было путаницы с оффсетом.
|
||
// ---------------------------------------------------------------------------
|
||
// ---------------------------------------------------------------------------
|
||
// FreqToBandIdx — возвращает индекс диапазона для частоты Hz, или -1
|
||
// ---------------------------------------------------------------------------
|
||
function FreqToBandIdx(Hz: Double): Integer;
|
||
const
|
||
// Границы диапазонов [Low, High] в Гц (приблизительные)
|
||
BAND_LO: array[0..10] of Double = (
|
||
1800000, 3500000, 5330000, 7000000, 10100000,
|
||
14000000, 18068000, 21000000, 24890000, 28000000, 50000000);
|
||
BAND_HI: array[0..10] of Double = (
|
||
2000000, 4000000, 5410000, 7300000, 10150000,
|
||
14350000, 18168000, 21450000, 24990000, 29700000, 54000000);
|
||
var
|
||
i: Integer;
|
||
begin
|
||
Result := -1;
|
||
for i := 0 to 10 do
|
||
if (Hz >= BAND_LO[i]) and (Hz <= BAND_HI[i]) then
|
||
begin
|
||
Result := i;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActiveVfoFreq — частота активного VFO
|
||
// ---------------------------------------------------------------------------
|
||
function TMainForm.ActiveVfoFreq: Int64;
|
||
begin
|
||
if FActiveVfo = 0 then Result := Round(FVfoA)
|
||
else Result := Round(FVfoB);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
|
||
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
|
||
// Перестройка на частоту выбранного VFO. CTUN не трогаем.
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ActivateVfo(Idx: Integer);
|
||
var
|
||
ActiveFreq: Int64;
|
||
NewBand: Integer;
|
||
Offset: Double;
|
||
HalfSpan: Double;
|
||
FreqVisible: Boolean;
|
||
begin
|
||
FActiveVfo := Idx;
|
||
|
||
// Частота нового активного VFO
|
||
if FActiveVfo = 0 then
|
||
ActiveFreq := Round(FVfoA)
|
||
else
|
||
ActiveFreq := Round(FVfoB);
|
||
|
||
// Обновляем визуальный дисплей
|
||
UpdateVfoDisplay;
|
||
|
||
if FCTun then
|
||
begin
|
||
// CTUN включён: проверяем, видна ли новая частота на текущем спектре
|
||
HalfSpan := FSpanHz / 2;
|
||
Offset := ActiveFreq - FCenterFreq;
|
||
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
|
||
|
||
if FreqVisible then
|
||
begin
|
||
// Частота видна в окне — просто меняем shift, DDC не трогаем
|
||
if FWDSPReady then FDSPEngine.SetShift(Offset);
|
||
// Сеть не трогаем — DDC (FCenterFreq) остаётся прежним
|
||
end
|
||
else
|
||
begin
|
||
// Частота за пределами спектра — центрируем окно и сбрасываем CTUN
|
||
FCTun := False;
|
||
StyleButton(BtnCTun, False);
|
||
FBandCache[FCurrentBand].CTun := False;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// CTUN выключен: перестраиваем DDC на новую частоту
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
// Переключаем кнопку диапазона по новой частоте
|
||
NewBand := FreqToBandIdx(ActiveFreq);
|
||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := NewBand;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispAClick(Sender: TObject);
|
||
begin
|
||
// Клик на VFO-A → активируем A (перестройка на FVfoA)
|
||
if FActiveVfo <> 0 then
|
||
ActivateVfo(0);
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispBClick(Sender: TObject);
|
||
begin
|
||
// Клик на VFO-B → активируем B (перестройка на FVfoB)
|
||
if FActiveVfo <> 1 then
|
||
ActivateVfo(1);
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoASelectClick(Sender: TObject);
|
||
begin
|
||
if FActiveVfo <> 0 then
|
||
ActivateVfo(0);
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoATXClick(Sender: TObject);
|
||
begin
|
||
FSplitTxB := False;
|
||
UpdateVfoDisplay;
|
||
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoBSelectClick(Sender: TObject);
|
||
begin
|
||
if FActiveVfo <> 1 then
|
||
ActivateVfo(1);
|
||
end;
|
||
|
||
procedure TMainForm.BtnTopVfoBTXClick(Sender: TObject);
|
||
begin
|
||
FSplitTxB := True;
|
||
if FSplitTxB and (FActiveVfo <> 0) then
|
||
ActivateVfo(0)
|
||
else
|
||
UpdateVfoDisplay;
|
||
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64);
|
||
begin
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(NewFreq)
|
||
else
|
||
FVfoA := NewFreq; // просто сохраняем, приёмник не перестраиваем
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||
var
|
||
BandIdx: Integer;
|
||
begin
|
||
FVfoB := NewFreq;
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
// VFO-B активен: перестраиваем приёмник
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
// Обновляем диапазон
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
FDriveLevel := CalcDriveByte;
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.UpdateFilterButtons;
|
||
var
|
||
i: Integer;
|
||
Names: array[0..FILT_COUNT-1] of string;
|
||
DefIdx: Integer;
|
||
W: Integer;
|
||
begin
|
||
if FMode = MODE_FM then
|
||
begin
|
||
// FM: show only NFM and WFM buttons, resize them to fill the row
|
||
CloseCTCSSPopup;
|
||
if FFilter > 1 then FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
|
||
FFilterBW := FILT_FM_BW[FFilter];
|
||
FFMDeviation := FILT_FM_DEV[FFilter];
|
||
W := (PanelFilter.Width - 6) div 2;
|
||
BtnFilter[0].Caption := FILT_FM_NAMES[0];
|
||
BtnFilter[0].Left := 2;
|
||
BtnFilter[0].Width := W - 2;
|
||
BtnFilter[0].Top := 16;
|
||
BtnFilter[0].Visible := True;
|
||
StyleButton(BtnFilter[0], FFilter = 0);
|
||
BtnFilter[1].Caption := FILT_FM_NAMES[1];
|
||
BtnFilter[1].Left := W + 2;
|
||
BtnFilter[1].Width := W - 2;
|
||
BtnFilter[1].Top := 16;
|
||
BtnFilter[1].Visible := True;
|
||
StyleButton(BtnFilter[1], 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, FFMSQOn);
|
||
TrkFMSQ.Position := FFMSQLevel;
|
||
LblFMSQ.Caption := IntToStr(FFMSQLevel);
|
||
end;
|
||
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := True;
|
||
if PanelFMStep <> nil then
|
||
begin
|
||
StyleButton(BtnFMStep, FFMStepOn);
|
||
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FFMStepIdx);
|
||
PanelFMStep.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;
|
||
W := (PanelFilter.Width - 6) div 5;
|
||
for i := 0 to FILT_COUNT - 1 do
|
||
begin
|
||
BtnFilter[i].Left := 2 + (i mod 5) * W;
|
||
BtnFilter[i].Top := 16 + (i div 5) * 27;
|
||
BtnFilter[i].Width := W - 2;
|
||
BtnFilter[i].Visible := True;
|
||
end;
|
||
|
||
case FMode of
|
||
0, 1: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
|
||
DefIdx := FILT_SSB_DEF;
|
||
end;
|
||
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;
|
||
|
||
FFilter := DefIdx;
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[DefIdx];
|
||
2: FFilterBW := FILT_DSB_BW[DefIdx];
|
||
3, 4: FFilterBW := FILT_CW_BW[DefIdx];
|
||
else FFilterBW := FILT_AM_BW[DefIdx];
|
||
end;
|
||
|
||
for i := 0 to FILT_COUNT-1 do
|
||
begin
|
||
BtnFilter[i].Caption := Names[i];
|
||
StyleButton(BtnFilter[i], i = FFilter);
|
||
end;
|
||
RelayoutBelowBands;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ApplyVfoA — единая точка смены частоты VFO A
|
||
//
|
||
// CTUN ВЫКЛ (классический режим):
|
||
// FCenterFreq = FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
||
//
|
||
// CTUN ВКЛ (piHPSDR/Thetis режим):
|
||
// FCenterFreq статичен — спектр/водопад не двигаются
|
||
// FVfoA движется по дисплею (маркер гуляет)
|
||
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
|
||
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
|
||
// hits the edge of the display")
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
||
var
|
||
Offset: Double;
|
||
FiltLo: Double;
|
||
FiltHi: Double;
|
||
HalfSpan: Double;
|
||
Scrolled: Boolean;
|
||
BandIdx: Integer;
|
||
begin
|
||
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
|
||
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
|
||
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
|
||
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
|
||
end;
|
||
FVfoA := NewFreq;
|
||
Scrolled := False;
|
||
|
||
if not FCTun then
|
||
begin
|
||
// ---- CTUN OFF ----
|
||
// DDC = VFO, сдвига нет
|
||
FCenterFreq := FVfoA;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(0.0);
|
||
end
|
||
else
|
||
begin
|
||
// ---- CTUN ON ----
|
||
// DDC (FCenterFreq) стоит на месте.
|
||
// SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы
|
||
// демодулятор принимал сигнал на FVfoA, а не на FCenterFreq.
|
||
// Shift = FVfoA - FCenterFreq (в Гц)
|
||
Offset := FVfoA - FCenterFreq;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(Offset);
|
||
|
||
// Проверяем не вышла ли полоса фильтра за край дисплея
|
||
HalfSpan := FSpanHz / 2;
|
||
case FMode of
|
||
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
|
||
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
|
||
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
|
||
end;
|
||
|
||
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
|
||
begin
|
||
// Полоса вышла за край → прокручиваем центр
|
||
if FiltHi > HalfSpan then
|
||
FCenterFreq := FVfoA - HalfSpan * 0.5
|
||
else
|
||
FCenterFreq := FVfoA + HalfSpan * 0.5;
|
||
// Пересчитываем сдвиг после прокрутки
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
// DDC перестраивается на новый FCenterFreq
|
||
Scrolled := True;
|
||
end;
|
||
end;
|
||
|
||
// Обновляем VFO дисплей
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
|
||
// Обновляем кнопку диапазона если частота перешла в другой диапазон.
|
||
// В XVTR-режиме HF band-кнопки не подсвечиваем (мы клэмпим VFO внутри
|
||
// XVTR-диапазона — FreqToBandIdx всегда вернёт -1 для VHF/UHF).
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoA);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
FDriveLevel := CalcDriveByte;
|
||
end;
|
||
end;
|
||
|
||
// DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован)
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
|
||
// Не рисуем синхронно из wheel/drag path: частые события мыши иначе
|
||
// забивают UI-поток и мешают аудио. Таймер подхватит ближайший кадр.
|
||
SyncSpecViewFreq;
|
||
FSpectrumDirty := True;
|
||
PbSpectrum.Invalidate;
|
||
if Scrolled then
|
||
begin
|
||
FWaterfallDirty := True;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CTUN toggle
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.BtnCTunClick(Sender: TObject);
|
||
begin
|
||
FCTun := not FCTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
FBandCache[FCurrentBand].CTun := FCTun;
|
||
if not FCTun then
|
||
begin
|
||
// Выключили CTUN: центрируемся на VFO, shift=0
|
||
FCenterFreq := FVfoA;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Включили CTUN: фиксируем текущее положение дисплея
|
||
// FCenterFreq остаётся как есть — спектр не прыгает
|
||
end;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
FSpecView.DrawWaterfall;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyDUP(Active: Boolean);
|
||
begin
|
||
FDisplayDuplex := Active;
|
||
StyleButton(BtnDUP, FDisplayDuplex);
|
||
// Если сейчас идёт передача — переключаем источник пикселей и сбрасываем
|
||
// буферы, чтобы старые TX/RX bin'ы не оставались на следующем кадре.
|
||
if FTransmitting then
|
||
begin
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDisplaySourceTX(not FDisplayDuplex);
|
||
FSpecView.TXMode := FTransmitting and not FDisplayDuplex;
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.ResetWfAvgBuf;
|
||
ApplySpecViewGridFromState;
|
||
FSpecView.DrawSpectrum;
|
||
FSpecView.DrawWaterfall;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnDUPClick(Sender: TObject);
|
||
begin
|
||
ApplyDUP(not FDisplayDuplex);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyTUN(Active: Boolean);
|
||
begin
|
||
if Active = FTuning then Exit;
|
||
// Safety: те же блокировки что в ApplyMOX. TUN — это TX через WDSP PostGen,
|
||
// он тоже должен молчать на бэндах с DoNotTx или на RX-only XVTR.
|
||
if Active and FDevConnected and (FCurrentXvtr < 0)
|
||
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then
|
||
Exit;
|
||
if Active and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
|
||
Exit;
|
||
if Active then
|
||
begin
|
||
// Включаем тон ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном
|
||
if FWDSPReady then
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||
FTuning := True;
|
||
StyleButton(BtnTUN, True);
|
||
// Drive байт пересчитываем (CalcDriveByte увидит FTuning=True)
|
||
FDriveLevel := CalcDriveByte;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0);
|
||
// Активируем TX (то же что MOX — PTT, run TXA-канал)
|
||
if not FTransmitting then ApplyMOX(True);
|
||
end else
|
||
begin
|
||
// Выключаем TX, потом тон, потом восстанавливаем drive
|
||
if FTransmitting then ApplyMOX(False);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetTXTone(False, 0, 0);
|
||
FTuning := False;
|
||
StyleButton(BtnTUN, False);
|
||
FDriveLevel := CalcDriveByte;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnTUNClick(Sender: TObject);
|
||
begin
|
||
ApplyTUN(not FTuning);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Клик/драг по спектру/водопаду
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Перевод X пикселя в частоту
|
||
function PixelToFreq(PixelX, PanelWidth: Integer;
|
||
CenterFreq, SpanHz: Double): Double;
|
||
begin
|
||
Result := CenterFreq + (PixelX / PanelWidth - 0.5) * SpanHz;
|
||
end;
|
||
|
||
procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
ClickFreq: Int64;
|
||
StepHz: Int64;
|
||
BandIdx: Integer;
|
||
begin
|
||
if PanelWidth <= 0 then Exit;
|
||
if (FMode = MODE_FM) and FFMStepOn then
|
||
StepHz := FM_STEP_HZ[FFMStepIdx]
|
||
else
|
||
StepHz := 100;
|
||
ClickFreq := Round(FCenterFreq + (PixelX / PanelWidth - 0.5) * FSpanHz);
|
||
ClickFreq := (ClickFreq div StepHz) * StepHz;
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(ClickFreq)
|
||
else
|
||
begin
|
||
FVfoB := ClickFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FCenterFreq := ClickFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
dPix: Integer;
|
||
dFreq: Double;
|
||
begin
|
||
if PanelWidth <= 0 then Exit;
|
||
dPix := PixelX - FSpecDragX0;
|
||
dFreq := dPix / PanelWidth * FSpanHz;
|
||
if FCTun then
|
||
begin
|
||
// CTUN ON: drag двигает окно просмотра (FCenterFreq/DDC)
|
||
// VFO остаётся, shift обновляется = VFO - новый FCenterFreq
|
||
FCenterFreq := FSpecDragFreq - dFreq;
|
||
if FWDSPReady then
|
||
begin
|
||
if FActiveVfo = 0 then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq)
|
||
else
|
||
FDSPEngine.SetShift(FVfoB - FCenterFreq);
|
||
end;
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
FSpectrumDirty := True;
|
||
end
|
||
else
|
||
begin
|
||
// CTUN OFF: drag двигает VFO, FCenterFreq следует через Apply
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FSpecDragFreq - dFreq))
|
||
else
|
||
begin
|
||
FVfoB := FSpecDragFreq - dFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// Спектр
|
||
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if 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 FCTun then FSpecDragFreq := FCenterFreq
|
||
else begin
|
||
if FActiveVfo = 0 then FSpecDragFreq := FVfoA
|
||
else FSpecDragFreq := FVfoB;
|
||
end;
|
||
PbSpectrum.Cursor := crSizeWE;
|
||
end
|
||
else if Button = mbRight then
|
||
begin
|
||
// Правая кнопка: toggle маркера
|
||
if FSpecView.MarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FSpecView.MarkerX) < 8) then
|
||
begin
|
||
// Клик рядом с текущим маркером — убираем
|
||
FSpecView.MarkerActive := False;
|
||
end
|
||
else
|
||
begin
|
||
// Ставим маркер в новое место
|
||
FSpecView.MarkerActive := True;
|
||
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
|
||
end;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
begin
|
||
if 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 FCTun then FSpecDragFreq := FCenterFreq
|
||
else begin
|
||
if FActiveVfo = 0 then FSpecDragFreq := FVfoA
|
||
else FSpecDragFreq := FVfoB;
|
||
end;
|
||
PbWaterfall.Cursor := crSizeWE;
|
||
end
|
||
else if Button = mbRight then
|
||
begin
|
||
if FSpecView.MarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FSpecView.MarkerX) < 8) then
|
||
FSpecView.MarkerActive := False
|
||
else
|
||
begin
|
||
FSpecView.MarkerActive := True;
|
||
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
end;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
begin
|
||
if FSpecDrag and (ssLeft in Shift) then
|
||
DoSpectrumDrag(X, PbWaterfall.Width);
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if (Abs(X - FSpecDragX0) < 4) then
|
||
DoSpectrumClick(X, PbWaterfall.Width);
|
||
FSpecDrag := False;
|
||
PbWaterfall.Cursor := crDefault;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnBandClick(Sender: TObject);
|
||
var
|
||
NewBand: Integer;
|
||
begin
|
||
NewBand := (Sender as TFlatButton).Tag;
|
||
if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit;
|
||
// Если в XVTR-режиме — DeactivateXvtr вернёт HF-состояние (RestoreBand
|
||
// на сохранённом FCurrentBand). Если NewBand совпадает с восстановленным
|
||
// FCurrentBand, дальше делать ничего не нужно.
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
DeactivateXvtr;
|
||
if NewBand = FCurrentBand then Exit;
|
||
end
|
||
else if NewBand = FCurrentBand then
|
||
Exit;
|
||
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.Save;
|
||
end;
|
||
FCurrentBand := NewBand;
|
||
RestoreBand(FCurrentBand);
|
||
end;
|
||
|
||
procedure TMainForm.ApplyModeFilter;
|
||
var
|
||
Lo, Hi, Half: Integer;
|
||
begin
|
||
Half := FFilterBW div 2;
|
||
case FMode of
|
||
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
|
||
1: begin Lo := 100; Hi := FFilterBW; end; // USB
|
||
2: begin Lo := -Half; Hi := Half; end; // DSB
|
||
3: begin Lo := -Half; Hi := Half; end; // CWL
|
||
4: begin Lo := -Half; Hi := Half; end; // CWU
|
||
5: begin Lo := -Half; Hi := Half; end; // FM
|
||
6: begin Lo := -Half; Hi := Half; end; // AM
|
||
7: begin Lo := -Half; Hi := Half; end; // SAM
|
||
else Lo := -Half; Hi := Half;
|
||
end;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetFilter(Lo, Hi);
|
||
FDSPEngine.SetTXFilter(Lo, Hi);
|
||
if FMode = MODE_FM then
|
||
begin
|
||
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
||
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
|
||
end
|
||
else
|
||
FDSPEngine.SetFMSquelch(False, 0);
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnModeClick(Sender: TObject);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := (Sender as TFlatButton).Tag;
|
||
FMode := N;
|
||
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(N);
|
||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||
if FTuning then
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||
end;
|
||
UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq; // FSpecView.Mode/FilterBW — иначе полоса рисуется под старый режим
|
||
end;
|
||
|
||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := (Sender as TFlatButton).Tag;
|
||
FFilter := N;
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[N];
|
||
2: FFilterBW := FILT_DSB_BW[N];
|
||
3, 4: FFilterBW := FILT_CW_BW[N];
|
||
5: begin
|
||
FFilterBW := FILT_FM_BW[N];
|
||
FFMDeviation := FILT_FM_DEV[N];
|
||
end;
|
||
else FFilterBW := FILT_AM_BW[N];
|
||
end;
|
||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// FM CTCSS helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TMainForm.ApplyFMSquelch;
|
||
begin
|
||
if FWDSPReady then
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMSQClick(Sender: TObject);
|
||
begin
|
||
FFMSQOn := not FFMSQOn;
|
||
StyleButton(BtnFMSQ, FFMSQOn);
|
||
BtnFMSQ.Repaint;
|
||
ApplyFMSquelch;
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.TrkFMSQChange(Sender: TObject);
|
||
begin
|
||
FFMSQLevel := TrkFMSQ.Position;
|
||
if LblFMSQ <> nil then LblFMSQ.Caption := IntToStr(FFMSQLevel);
|
||
ApplyFMSquelch;
|
||
SaveCurrentBand;
|
||
end;
|
||
|
||
procedure TMainForm.CloseCTCSSPopup;
|
||
begin
|
||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
|
||
CloseFMStepPopup;
|
||
end;
|
||
|
||
procedure TMainForm.CloseFMStepPopup;
|
||
begin
|
||
if FStepDropDown <> nil then FStepDropDown.ClosePopup;
|
||
end;
|
||
|
||
procedure TMainForm.SetFMStep(Idx: Integer);
|
||
begin
|
||
FFMStepIdx := Idx;
|
||
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(Idx);
|
||
if FWebServer <> nil then FWebServer.FMStepIdx := Idx;
|
||
if (FMode = MODE_FM) and (FSpecView <> nil) then
|
||
FSpecView.FMGridStepHz := FM_STEP_HZ[Idx];
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMStepClick(Sender: TObject);
|
||
begin
|
||
FFMStepOn := not FFMStepOn;
|
||
StyleButton(BtnFMStep, 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;
|
||
|
||
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
|
||
begin
|
||
FFMCTCSSToneIdx := Idx;
|
||
if FCTCSSDropDown <> nil then FCTCSSDropDown.SetItemIndex(Idx);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[Idx]);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMCTCSSClick(Sender: TObject);
|
||
begin
|
||
FFMCTCSSOn := not FFMCTCSSOn;
|
||
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
||
BtnFMCTCSS.Repaint; // Flush visual state before WDSP call
|
||
if FWDSPReady then
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
end;
|
||
|
||
procedure TMainForm.BtnFMCTCSSToneClick(Sender: TObject);
|
||
begin
|
||
FStepDropDown.ClosePopup;
|
||
FCTCSSDropDown.Toggle;
|
||
end;
|
||
|
||
procedure TMainForm.OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer);
|
||
begin
|
||
SetFMCTCSSTone(Idx);
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
|
||
var
|
||
Tmp: Double;
|
||
BandIdx: Integer;
|
||
ActiveFreq: Int64;
|
||
begin
|
||
// Меняем частоты местами
|
||
Tmp := FVfoA; FVfoA := FVfoB; FVfoB := Tmp;
|
||
|
||
// Определяем новую частоту активного VFO
|
||
if FActiveVfo = 0 then ActiveFreq := Round(FVfoA)
|
||
else ActiveFreq := Round(FVfoB);
|
||
|
||
// Перестраиваем приёмник
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
|
||
// Обновляем кнопку диапазона
|
||
BandIdx := FreqToBandIdx(ActiveFreq);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
|
||
FSpecView.InvalidateRulerCache;
|
||
UpdateVfoDisplay;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoACopyBClick(Sender: TObject);
|
||
begin
|
||
// A>B: копируем частоту A в B (VFO-B получает частоту VFO-A)
|
||
FVfoB := FVfoA;
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
// B активен — перестраиваем приёмник на новую частоту B
|
||
FCenterFreq := Round(FVfoB);
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
UpdateVfoDisplay;
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoBCopyAClick(Sender: TObject);
|
||
begin
|
||
// B>A: копируем частоту B в A (VFO-A получает частоту VFO-B)
|
||
FVfoA := FVfoB;
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FVfoA)) // A активен — перестраиваем приёмник
|
||
else
|
||
begin
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
UpdateVfoDisplay;
|
||
end;
|
||
end;
|
||
|
||
function TMainForm.ActiveTXFreqHz: Double;
|
||
begin
|
||
if FSplitTxB then Result := FVfoB
|
||
else if FActiveVfo = 0 then Result := FVfoA
|
||
else Result := FVfoB;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyMOX(Active: Boolean);
|
||
var
|
||
WasTransmitting: Boolean;
|
||
begin
|
||
// Safety: block TX on bands marked DoNotTx in Alex config
|
||
if Active and FDevConnected and (FCurrentXvtr < 0)
|
||
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then
|
||
Exit;
|
||
// Safety: block TX on RX-only transverter
|
||
if Active and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
|
||
Exit;
|
||
WasTransmitting := FTransmitting;
|
||
FTransmitting := Active;
|
||
if Active then FDriveLevel := CalcDriveByte;
|
||
FDUCPendingCount := 0;
|
||
if FTransmitting then
|
||
begin
|
||
BtnMOX.ClrNorm := TColor($00000044);
|
||
BtnMOX.ClrText := TColor($000000FF);
|
||
BtnMOX.ClrTextAct := TColor($000000FF);
|
||
BtnMOX.Active := True;
|
||
end else
|
||
StyleButton(BtnMOX, False);
|
||
// TX-overlay в SpectrumView: фиксируем актуальную TX-частоту/VFO-индекс и режим
|
||
FSpecView.TXFreq := ActiveTXFreqHz;
|
||
if FSplitTxB then FSpecView.TXVfoIndex := 1
|
||
else FSpecView.TXVfoIndex := FActiveVfo;
|
||
// DUP: данные остаются от RX-анализатора (TXMode=False), но TX-полоса
|
||
// фильтра рисуется поверх через TXOverlay. Без DUP при TX оба флага True.
|
||
FSpecView.TXMode := FTransmitting and not FDisplayDuplex;
|
||
FSpecView.TXOverlay := FTransmitting;
|
||
// Перерисовываем сетку спектра под активный тракт (Thetis-style).
|
||
ApplySpecViewGridFromState;
|
||
if FTransmitting and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.ClearDUCIQQueue;
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
True, True, True);
|
||
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
|
||
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
|
||
end;
|
||
if FWDSPReady then
|
||
begin
|
||
// Mic source — определяем автоматически по источнику TX:
|
||
// Web TX → txmsWeb
|
||
// HW PTT (FHWPTTActive при входе в ApplyMOX уже True) → txmsRadio
|
||
// MOX-кнопка/CAT → sound card если настроена, иначе txmsRadio
|
||
if FTransmitting then
|
||
begin
|
||
if Assigned(FWebServer) and FWebServer.WebClientActive then
|
||
begin
|
||
FWebMicActive := True;
|
||
FDSPEngine.SetTXMicSource(txmsWeb);
|
||
end
|
||
else if FHWPTTActive then
|
||
FDSPEngine.SetTXMicSource(txmsRadio)
|
||
else
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end
|
||
else if FWebMicActive then
|
||
begin
|
||
FWebMicActive := False;
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end;
|
||
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX,
|
||
// чтобы TX leakage не накапливался в RXA pipeline и FFT-истории
|
||
// RX-анализатора. В DUP RX-тракт работает как обычно.
|
||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
||
FDSPEngine.SetTXRun(FTransmitting);
|
||
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
||
begin
|
||
FDSPEngine.FlushRX;
|
||
if Assigned(FAudioOut) then
|
||
FAudioOut.Clear;
|
||
{$IFDEF WINDOWS}
|
||
// Windows: на TX→RX дропаем И RX-IQ пакеты, И аудио-выход на это окно.
|
||
// Источник «хвоста» — radio FPGA TX-buffer + PA slew-down: после MOX-off
|
||
// радио дотравливает буфер передатчика, RX1 ловит это как leak, и WDSP
|
||
// отдаёт это и в водопад, и в звук. На Linux наблюдается окно <30мс,
|
||
// на Windows может тянуться 200–500мс. Подобрать значение можно здесь
|
||
// без перекомпиляции engine. На Linux код-путь не активируется.
|
||
FDSPEngine.BeginPostTXMute(250);
|
||
{$ENDIF}
|
||
end;
|
||
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
|
||
// источник на RX, чтобы видеть приём во время передачи.
|
||
if FTransmitting and FDisplayDuplex then
|
||
FDSPEngine.SetDisplaySourceTX(False);
|
||
end;
|
||
if (not FTransmitting) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
False, True, True);
|
||
FNetwork.ClearDUCIQQueue;
|
||
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX), drive=0
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnMOXClick(Sender: TObject);
|
||
begin
|
||
// Если идёт tune — клик MOX просто выходит из tune (без перехода на голос).
|
||
if FTuning then
|
||
begin
|
||
ApplyTUN(False);
|
||
Exit;
|
||
end;
|
||
ApplyMOX(not FTransmitting);
|
||
end;
|
||
|
||
procedure TMainForm.BtnMuteClick(Sender: TObject);
|
||
begin
|
||
FMuted := not FMuted;
|
||
StyleButton(BtnMute, FMuted);
|
||
if FMuted then BtnMute.Caption := 'UNMUTE'
|
||
else BtnMute.Caption := 'MUTE';
|
||
if FWDSPReady then FDSPEngine.SetMute(FMuted);
|
||
end;
|
||
|
||
procedure TMainForm.UpdateNRButton;
|
||
begin
|
||
case BtnNR.Tag of
|
||
1: BtnNR.Caption := 'NR';
|
||
2: BtnNR.Caption := 'NR2';
|
||
3: BtnNR.Caption := 'NR3';
|
||
4: BtnNR.Caption := 'NR4';
|
||
else
|
||
BtnNR.Caption := 'NR';
|
||
end;
|
||
StyleButton(BtnNR, BtnNR.Tag <> 0);
|
||
BtnNR.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateNBButton;
|
||
begin
|
||
case BtnNB.Tag of
|
||
1: BtnNB.Caption := 'NB';
|
||
2: BtnNB.Caption := 'NB2';
|
||
else
|
||
BtnNB.Caption := 'NB';
|
||
end;
|
||
StyleButton(BtnNB, BtnNB.Tag <> 0);
|
||
BtnNB.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateSNBButton;
|
||
begin
|
||
BtnSNB.Caption := 'SNB';
|
||
StyleButton(BtnSNB, BtnSNB.Tag <> 0);
|
||
BtnSNB.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateANFButton;
|
||
begin
|
||
BtnANF.Caption := 'ANF';
|
||
StyleButton(BtnANF, BtnANF.Tag <> 0);
|
||
BtnANF.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyNoiseFilterButtonsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
FDSPEngine.SetNRMode(BtnNR.Tag);
|
||
FDSPEngine.SetNBMode(BtnNB.Tag);
|
||
FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
|
||
FDSPEngine.SetANF(BtnANF.Tag <> 0);
|
||
end;
|
||
|
||
procedure TMainForm.BtnNRClick(Sender: TObject);
|
||
begin
|
||
BtnNR.Tag := (BtnNR.Tag + 1) mod 5;
|
||
UpdateNRButton;
|
||
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
|
||
end;
|
||
|
||
procedure TMainForm.BtnNBClick(Sender: TObject);
|
||
begin
|
||
BtnNB.Tag := (BtnNB.Tag + 1) mod 3;
|
||
UpdateNBButton;
|
||
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
|
||
end;
|
||
|
||
procedure TMainForm.BtnSNBClick(Sender: TObject);
|
||
begin
|
||
BtnSNB.Tag := 1 - BtnSNB.Tag;
|
||
UpdateSNBButton;
|
||
if FWDSPReady then FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
|
||
end;
|
||
|
||
procedure TMainForm.BtnANFClick(Sender: TObject);
|
||
begin
|
||
BtnANF.Tag := 1 - BtnANF.Tag;
|
||
UpdateANFButton;
|
||
if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag <> 0);
|
||
end;
|
||
|
||
|
||
procedure TMainForm.ApplyWfAGCNF(WfAGC, WfNF: Boolean);
|
||
begin
|
||
FWfAGCEnabled := WfAGC;
|
||
FWfNFEnabled := WfNF;
|
||
FSpecView.WfAGCEnabled := FWfAGCEnabled;
|
||
FSpecView.WfNFEnabled := FWfNFEnabled;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyADCSettings(Dither, Random: Boolean);
|
||
begin
|
||
FDitherEnabled := Dither;
|
||
FRandomEnabled := Random;
|
||
if FNetwork.Connected and FRunning then
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
|
||
if FDevConnected then
|
||
FSettings.SaveGlobal(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;
|
||
FSettings.SaveWebSettings(W);
|
||
end;
|
||
|
||
procedure TMainForm.OnSampleRateHidePanel;
|
||
begin
|
||
FPanelHidden := not FPanelHidden;
|
||
if FPanelHidden then
|
||
begin
|
||
PanelLeft.Width := 0;
|
||
FVfoOverlay.Width := 260;
|
||
FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера
|
||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||
FVfoOverlay.SetDSPState(BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, FAGCMode);
|
||
FVfoOverlay.Visible := True;
|
||
PositionVfoOverlay;
|
||
end
|
||
else
|
||
begin
|
||
PanelLeft.Width := 232;
|
||
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 (FSpanHz > 0) then
|
||
begin
|
||
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * PbSpectrum.Width)
|
||
end
|
||
else
|
||
VfoX := PbSpectrum.Width div 2;
|
||
|
||
case FMode of
|
||
MODE_LSB: HiHz := -100;
|
||
MODE_USB: HiHz := FFilterBW;
|
||
else
|
||
HiHz := FFilterBW / 2;
|
||
end;
|
||
if (PbSpectrum.Width > 0) and (FSpanHz > 0) then
|
||
FilterEndX := VfoX + Round(HiHz / 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
|
||
FMode := Mode;
|
||
FFilterBW := FilterBW;
|
||
if Mode = MODE_FM then
|
||
begin
|
||
FFilter := FILT_FM_DEF;
|
||
for Idx := 0 to FILT_FM_COUNT - 1 do
|
||
if FILT_FM_BW[Idx] = FilterBW then
|
||
begin
|
||
FFilter := Idx;
|
||
FFMDeviation := FILT_FM_DEV[Idx];
|
||
Break;
|
||
end;
|
||
end;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
ApplyModeFilter;
|
||
end;
|
||
SyncSpecViewFreq;
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||
FVfoOverlay.SetDSPState(BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, FAGCMode);
|
||
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||
begin
|
||
BtnNR.Tag := NRMode; UpdateNRButton;
|
||
BtnNB.Tag := NBMode; UpdateNBButton;
|
||
BtnSNB.Tag := Ord(SNBOn); UpdateSNBButton;
|
||
BtnANF.Tag := Ord(ANFOn); UpdateANFButton;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
end;
|
||
|
||
procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer);
|
||
const
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
var
|
||
I: Integer;
|
||
begin
|
||
FAGCMode := AGCMode;
|
||
for I := 0 to 4 do StyleButton(BtnAGCMode[I], I = AGCMode);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetAGC(AGCModes[AGCMode], 50.0);
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
end;
|
||
|
||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||
begin
|
||
FSpectrumDirty := True;
|
||
if Assigned(FSpecView) then FSpecView.SpectrumDirty := True;
|
||
if Assigned(PbSpectrum) then PbSpectrum.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
||
var
|
||
NewRate: Integer;
|
||
DDCRate: Word;
|
||
begin
|
||
NewRate := SampleRate;
|
||
if NewRate = FSampleRate then Exit;
|
||
|
||
FSampleRate := NewRate;
|
||
FSpanHz := NewRate;
|
||
DDCRate := NewRate div 1000;
|
||
|
||
if Assigned(FSampleRateOverlay) then
|
||
FSampleRateOverlay.SetCurrentRate(NewRate);
|
||
Application.ProcessMessages;
|
||
|
||
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
|
||
// SampleRate — глобальный: сохраняем в global settings
|
||
if FDevConnected then
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
|
||
// 1. Останавливаем WDSP и сбрасываем очередь старых пакетов
|
||
if FWDSPReady then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open
|
||
end;
|
||
|
||
// 2. Отправляем новый rate трансиверу
|
||
// (после остановки DSP — новые пакеты сразу идут с правильным rate)
|
||
if FNetwork.Connected then
|
||
FNetwork.ConfigureDDCs(1, DDCRate, 0, FDitherEnabled, FRandomEnabled);
|
||
|
||
// 3. Восстанавливаем настройки DSP
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
if FNetwork.Connected then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока)
|
||
// Все изменения состояния выполняем через TThread.Synchronize чтобы
|
||
// не трогать UI и WDSP из чужого потока.
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.WebOnFreq(Hz: Double);
|
||
begin
|
||
FWebSyncFreq := Hz;
|
||
FWebSyncM := SyncWebFreq;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebFreq;
|
||
var BandIdx: Integer;
|
||
begin
|
||
// Веб шлёт "freq" для активного VFO
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FWebSyncFreq))
|
||
else
|
||
begin
|
||
FVfoB := FWebSyncFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FRunning then
|
||
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
UpdateVfoDisplay;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnMode(Mode: Integer);
|
||
begin
|
||
FWebSyncInt := Mode;
|
||
FWebSyncM := SyncWebMode;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebMode;
|
||
var i: Integer;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt >= MODE_COUNT) then Exit;
|
||
FMode := FWebSyncInt;
|
||
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
|
||
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
||
UpdateFilterButtons;
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq;
|
||
FBandCache[FCurrentBand].Mode := FMode;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnFilter(BW: Integer);
|
||
begin
|
||
FWebSyncInt := BW;
|
||
FWebSyncM := SyncWebFilter;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebFilter;
|
||
var
|
||
best, dist, i, j: Integer;
|
||
BW_arr: array[0..FILT_COUNT-1] of Integer;
|
||
Idx: Integer;
|
||
begin
|
||
// Negative value = -(idx+1) means filter index was passed directly
|
||
if FWebSyncInt < 0 then
|
||
begin
|
||
Idx := (-FWebSyncInt) - 1;
|
||
if (Idx >= 0) and (Idx < FILT_COUNT) then
|
||
begin
|
||
FFilter := Idx;
|
||
case FMode of
|
||
0,1: FFilterBW := FILT_SSB_BW[Idx];
|
||
2: FFilterBW := FILT_DSB_BW[Idx];
|
||
3,4: FFilterBW := FILT_CW_BW[Idx];
|
||
5: if Idx < FILT_FM_COUNT then
|
||
begin
|
||
FFilterBW := FILT_FM_BW[Idx];
|
||
FFMDeviation := FILT_FM_DEV[Idx];
|
||
end;
|
||
else FFilterBW := FILT_AM_BW[Idx];
|
||
end;
|
||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
end;
|
||
Exit;
|
||
end;
|
||
// Positive value = bandwidth in Hz, find closest filter
|
||
FFilterBW := FWebSyncInt;
|
||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
|
||
case FMode of
|
||
0,1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
||
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
|
||
3,4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
||
5: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
|
||
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
||
end;
|
||
best := 0; dist := MaxInt;
|
||
for i := 0 to FILT_COUNT-1 do
|
||
if Abs(BW_arr[i] - FWebSyncInt) < dist then
|
||
begin
|
||
dist := Abs(BW_arr[i] - FWebSyncInt);
|
||
best := i;
|
||
end;
|
||
FFilter := best;
|
||
if FMode = 5 then FFMDeviation := FILT_FM_DEV[best];
|
||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnAGC(Mode: Integer);
|
||
begin
|
||
FWebSyncInt := Mode;
|
||
FWebSyncM := SyncWebAGC;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebAGC;
|
||
var i: Integer;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt > 4) then Exit;
|
||
FAGCMode := FWebSyncInt;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnAGCTop(DB: Integer);
|
||
begin
|
||
FWebSyncInt := DB;
|
||
FWebSyncM := SyncWebAGCTop;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebAGCTop;
|
||
begin
|
||
FAGCTop := Max(20, Min(120, FWebSyncInt));
|
||
TrkAGC.Position := FAGCTop;
|
||
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnBand(Idx: Integer);
|
||
begin
|
||
FWebSyncInt := Idx;
|
||
FWebSyncM := SyncWebBand;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebBand;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt >= BAND_COUNT) then Exit;
|
||
// Выйти из XVTR при переходе на HF из web. DeactivateXvtr вернёт HF-состояние.
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
DeactivateXvtr;
|
||
if FWebSyncInt = FCurrentBand then Exit;
|
||
end
|
||
else if FWebSyncInt = FCurrentBand then
|
||
Exit;
|
||
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
|
||
FCurrentBand := FWebSyncInt;
|
||
RestoreBand(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 FXvtrSettings.Entries[FWebSyncInt].Enabled then Exit;
|
||
if FWebSyncInt = FCurrentXvtr then Exit;
|
||
if FDevConnected then begin SaveCurrentBand; 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
|
||
FVolume := Max(0, Min(100, FWebSyncInt));
|
||
TrkVolume.Position := FVolume;
|
||
if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnWfAGC(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebWfAGC;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebWfAGC;
|
||
begin
|
||
FWfAGCEnabled := FWebSyncBool;
|
||
FSpecView.WfAGCEnabled := FWfAGCEnabled;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnWfNF(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebWfNF;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebWfNF;
|
||
begin
|
||
FWfNFEnabled := FWebSyncBool;
|
||
FSpecView.WfNFEnabled := FWfNFEnabled;
|
||
end;
|
||
|
||
// ── Run ────────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnRun(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebRun;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebRun;
|
||
begin
|
||
// Toggle only if state differs
|
||
if FWebSyncBool <> FRunning then
|
||
BtnStartStopClick(nil);
|
||
end;
|
||
|
||
// ── Mute ───────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnMute(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebMute;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebMute;
|
||
begin
|
||
if FWebSyncBool <> FMuted then
|
||
BtnMuteClick(nil);
|
||
end;
|
||
|
||
// ── CTUN ──────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnCtun(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebCtun;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebCtun;
|
||
begin
|
||
if FWebSyncBool <> FCTun then
|
||
BtnCTunClick(nil);
|
||
end;
|
||
|
||
// ── NR ────────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnNR(Mode: Integer);
|
||
begin
|
||
FWebSyncInt := Mode;
|
||
FWebSyncM := SyncWebNR;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebNR;
|
||
begin
|
||
if FWebSyncInt < 0 then FWebSyncInt := 0;
|
||
if FWebSyncInt > 4 then FWebSyncInt := 4;
|
||
if BtnNR.Tag <> FWebSyncInt then
|
||
begin
|
||
BtnNR.Tag := FWebSyncInt;
|
||
UpdateNRButton;
|
||
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
|
||
end;
|
||
end;
|
||
|
||
// ── NB ────────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnNB(Mode: Integer);
|
||
begin
|
||
FWebSyncInt := Mode;
|
||
FWebSyncM := SyncWebNB;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebNB;
|
||
begin
|
||
if FWebSyncInt < 0 then FWebSyncInt := 0;
|
||
if FWebSyncInt > 2 then FWebSyncInt := 2;
|
||
if BtnNB.Tag <> FWebSyncInt then
|
||
begin
|
||
BtnNB.Tag := FWebSyncInt;
|
||
UpdateNBButton;
|
||
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnSNB(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebSNB;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebSNB;
|
||
begin
|
||
if FWebSyncBool <> (BtnSNB.Tag <> 0) then
|
||
BtnSNBClick(BtnSNB);
|
||
end;
|
||
|
||
// ── ANF ───────────────────────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnANF(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebANF;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebANF;
|
||
begin
|
||
if FWebSyncBool <> (BtnANF.Tag <> 0) then
|
||
BtnANFClick(BtnANF);
|
||
end;
|
||
|
||
// ── VFO-B freq (из веба) ─────────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnFreqB(Hz: Double);
|
||
begin
|
||
FWebSyncFreq := Hz;
|
||
FWebSyncM := SyncWebFreqB;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebFreqB;
|
||
var BandIdx: Integer;
|
||
begin
|
||
FVfoB := FWebSyncFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
FCenterFreq := FVfoB;
|
||
if not FCTun then
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FRunning then
|
||
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
UpdateVfoDisplay;
|
||
end;
|
||
|
||
// ── Активный VFO (из веба) ───────────────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnActiveVfo(Idx: Integer);
|
||
begin
|
||
FWebSyncInt := Idx;
|
||
FWebSyncM := SyncWebActiveVfo;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebActiveVfo;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt > 1) then Exit;
|
||
if FWebSyncInt = FActiveVfo then Exit;
|
||
ActivateVfo(FWebSyncInt);
|
||
end;
|
||
|
||
// ── CTUN drag center scroll (из веба) ────────────────────────────────────
|
||
|
||
procedure TMainForm.WebOnCenter(Hz: Double);
|
||
begin
|
||
FWebSyncFreq := Hz;
|
||
FWebSyncM := SyncWebCenter;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebCenter;
|
||
begin
|
||
// Сдвигаем DDC-центр без смены VFO (аналог CTUN-drag в десктопе)
|
||
FCenterFreq := FWebSyncFreq;
|
||
if FWDSPReady then
|
||
begin
|
||
if FActiveVfo = 0 then FDSPEngine.SetShift(FVfoA - FCenterFreq)
|
||
else FDSPEngine.SetShift(FVfoB - FCenterFreq);
|
||
end;
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FSpecView.InvalidateRulerCache;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnMOX(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebMOX;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebMOX;
|
||
begin
|
||
ApplyMOX(FWebSyncBool);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnDrive(V: Integer);
|
||
begin
|
||
FWebSyncInt := V;
|
||
FWebSyncM := SyncWebDrive;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebDrive;
|
||
begin
|
||
// Установка Position вызывает TrkDriveChange через OnChange автоматически
|
||
TrkDrive.Position := FWebSyncInt;
|
||
end;
|
||
|
||
procedure TMainForm.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
|
||
FAtten := Max(0, Min(2, FWebSyncInt));
|
||
FNetwork.SetStepAtten(FAtten * 10);
|
||
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 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;
|
||
FDSPEngine.PushTXMicSamplesD(Buf, n);
|
||
end;
|
||
|
||
procedure TMainForm.TrkDriveChange(Sender: TObject);
|
||
begin
|
||
FDriveLevel := CalcDriveByte;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
function TMainForm.CalcDriveByte: Byte;
|
||
// Thetis-compatible formula:
|
||
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
|
||
// where:
|
||
// target_volts = sqrt( 10^((target_dBm)/10) * 0.05 ) [E=sqrt(P*R), R=50, P in mW]
|
||
// target_dBm = 10*log10(pwr_W * 1000) - Cal
|
||
// pwr_W = Pos/100 * FPAMaxPower
|
||
// Cal = per-band PA gain in dB (HF: ~38.8-41.3, VHF default: ~56.2)
|
||
var
|
||
Cal: Double;
|
||
Pos: Integer;
|
||
XvtrActive: Boolean;
|
||
pwr_W: Double;
|
||
target_dBm: Double;
|
||
target_volts: Double;
|
||
audio_vol: Double;
|
||
begin
|
||
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
|
||
|
||
if XvtrActive then
|
||
Cal := FVHFBandCal[FCurrentXvtr]
|
||
else if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) then
|
||
Cal := FPABandCal[FCurrentBand]
|
||
else
|
||
Cal := 38.8;
|
||
|
||
// Pos — итоговый процент драйва 0..100
|
||
if FTuning then
|
||
begin
|
||
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
|
||
Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100)
|
||
else
|
||
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
|
||
end
|
||
else
|
||
begin
|
||
if XvtrActive then
|
||
Pos := Round(TrkDrive.Position * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
|
||
else
|
||
Pos := TrkDrive.Position;
|
||
end;
|
||
|
||
if Pos <= 0 then begin Result := 0; Exit; end;
|
||
|
||
pwr_W := Pos / 100.0 * FPAMaxPower;
|
||
target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal;
|
||
target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05);
|
||
audio_vol := Min(target_volts / 0.8, 1.0);
|
||
Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
|
||
end;
|
||
|
||
procedure 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 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
|
||
FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ);
|
||
FDUCPendingCount := 0;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.OnPASettingsChange(MaxPower: Double;
|
||
const BandCal: array of Double);
|
||
var i: Integer;
|
||
begin
|
||
FPAMaxPower := MaxPower;
|
||
if FSpecView <> nil then FSpecView.PAMaxPower := MaxPower;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
begin
|
||
if i <= High(BandCal) then
|
||
FPABandCal[i] := BandCal[i]
|
||
else
|
||
FPABandCal[i] := 100.0;
|
||
end;
|
||
FDriveLevel := CalcDriveByte;
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, 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
|
||
FVHFBandCal[i] := VHFCal[i]
|
||
else
|
||
FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
|
||
end;
|
||
FDriveLevel := CalcDriveByte;
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
|
||
end;
|
||
|
||
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
||
// Любая правка в Settings→Transmit летит сюда: обновляем FTXSettings,
|
||
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
|
||
var
|
||
OldMicByte, NewMicByte: Byte;
|
||
OldAtt, OldTUNLevel, OldTUNFreq: Integer;
|
||
GridChanged: Boolean;
|
||
begin
|
||
OldMicByte := BuildMicLineSelectByte;
|
||
OldAtt := FTXSettings.AttOnTX;
|
||
OldTUNLevel := FTXSettings.TUNLevel;
|
||
OldTUNFreq := FTXSettings.TUNFreq;
|
||
GridChanged := (FTXSpecRefLevel <> T.TXSpecRefLevel)
|
||
or (FTXSpecRange <> T.TXSpecRange)
|
||
or (FTXSpecGridStep <> T.TXSpecGridStep);
|
||
FTXSettings := T;
|
||
NewMicByte := BuildMicLineSelectByte;
|
||
// TX-grid в момент правки — обновляем рабочие поля и (если на передаче) пушим в FSpecView
|
||
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
|
||
FTXSpecRange := FTXSettings.TXSpecRange;
|
||
if FTXSettings.TXSpecGridStep > 0 then
|
||
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
|
||
if GridChanged and FTransmitting then
|
||
ApplySpecViewGridFromState;
|
||
ApplyTXSettingsToDSP;
|
||
if (OldMicByte <> NewMicByte) or (OldAtt <> FTXSettings.AttOnTX) then
|
||
SendDUCSpecificFromSettings;
|
||
// TUN-параметры live: если идёт tune — обновить тон/drive, иначе просто запомнить.
|
||
if FTuning then
|
||
begin
|
||
if (OldTUNFreq <> FTXSettings.TUNFreq) and FWDSPReady then
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||
if OldTUNLevel <> FTXSettings.TUNLevel then
|
||
begin
|
||
FDriveLevel := CalcDriveByte;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
end;
|
||
// Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||
FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings);
|
||
begin
|
||
FAlexSettings := A;
|
||
if FNetwork.Connected then
|
||
FNetwork.SetAlexConfig(A);
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveAlex(FDevMAC, A);
|
||
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 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
|
||
Result := VisibleHz;
|
||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
|
||
// f_IF = f_visible - LOOffset + LOError
|
||
Result := VisibleHz
|
||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||
end;
|
||
|
||
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
||
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
||
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
||
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis.
|
||
procedure TMainForm.ApplyXvtrToNetwork;
|
||
var
|
||
En: Boolean;
|
||
DisablePA: Boolean;
|
||
RxAnt: Byte;
|
||
begin
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
En := True;
|
||
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
|
||
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
|
||
end
|
||
else
|
||
begin
|
||
En := False; DisablePA := False; RxAnt := 0;
|
||
end;
|
||
if FNetwork.Connected then
|
||
FNetwork.SetXvtrMode(En, DisablePA, RxAnt);
|
||
end;
|
||
|
||
// Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые,
|
||
// создаёт новые для enabled слотов в 3-м и 4-м ряду PanelBands.
|
||
procedure TMainForm.RelayoutBelowBands;
|
||
var
|
||
Y: Integer;
|
||
begin
|
||
if PanelBands = nil then Exit;
|
||
Y := PanelBands.Top + PanelBands.Height + 2;
|
||
PanelMode.Top := Y; Y := Y + PanelMode.Height + 2;
|
||
PanelFilter.Top := Y; Y := Y + PanelFilter.Height + 2;
|
||
if (PanelFMStep <> nil) and PanelFMStep.Visible then
|
||
begin
|
||
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
|
||
end;
|
||
if (PanelFMSQ <> nil) and PanelFMSQ.Visible then
|
||
begin
|
||
PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2;
|
||
end;
|
||
if (PanelFMCTCSS <> nil) and PanelFMCTCSS.Visible then
|
||
begin
|
||
PanelFMCTCSS.Top := Y; Y := Y + PanelFMCTCSS.Height + 2;
|
||
end;
|
||
BtnCTun.Top := Y; BtnDUP.Top := Y; Y := Y + BtnCTun.Height + 4;
|
||
PanelRX.Top := Y;
|
||
end;
|
||
|
||
procedure TMainForm.RebuildXvtrButtons;
|
||
var
|
||
i, Pos, XvtrRows: Integer;
|
||
W, 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)
|
||
|
||
W := (PanelBands.Width - 6) div 6;
|
||
Pos := 0; // позиция среди enabled слотов
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if FXvtrSettings.Entries[i].Enabled then
|
||
begin
|
||
if Pos >= 12 then Break; // 2 ряда по 6 кнопок
|
||
Cap := FXvtrSettings.Entries[i].ButtonText;
|
||
if Cap = '' then Cap := 'X' + IntToStr(i + 1);
|
||
B := TFlatButton.Create(Self);
|
||
B.Parent := PanelBands;
|
||
B.Caption := Cap;
|
||
B.Left := 2 + (Pos mod 6) * W;
|
||
B.Top := MaxBottom + 4 + (Pos div 6) * RowH;
|
||
B.Width := W - 2;
|
||
B.Height := BtnH;
|
||
B.OnClick := BtnXvtrBandClick;
|
||
B.Tag := -(i + 1); // отрицательное значение = XVTR индекс
|
||
BtnXvtrBand[i] := B;
|
||
StyleButton(B, i = 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 FXvtrSettings.Entries[Idx].Enabled then Exit;
|
||
if FCurrentXvtr = Idx then Exit; // уже на этом XVTR
|
||
// Сохраняем текущий band перед сменой (как BtnBandClick)
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.Save;
|
||
end;
|
||
ActivateXvtrBand(Idx);
|
||
end;
|
||
|
||
// Активирует XVTR-band: устанавливает видимую частоту по LastFreq или
|
||
// середине [FreqBegin..FreqEnd], применяет network XVTR-режим.
|
||
procedure TMainForm.ActivateXvtrBand(Idx: Integer);
|
||
var
|
||
Vis: Double;
|
||
i: Integer;
|
||
E: TXvtrEntry;
|
||
begin
|
||
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
|
||
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
|
||
FCurrentXvtr := Idx;
|
||
E := FXvtrSettings.Entries[Idx];
|
||
// Выбор начальной частоты: LastFreq если в диапазоне, иначе середина
|
||
Vis := E.LastFreq;
|
||
if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
|
||
Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
|
||
// Отключаем кнопки HF band
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
StyleButton(BtnBand[i], False);
|
||
// Подсвечиваем активную XVTR-кнопку
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if BtnXvtrBand[i] <> nil then
|
||
StyleButton(BtnXvtrBand[i], i = Idx);
|
||
// Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant)
|
||
ApplyXvtrToNetwork;
|
||
// Восстанавливаем Mode + FM Squelch + CTCSS для этого XVTR-слота
|
||
FFMSQOn := E.LastFMSQOn;
|
||
FFMSQLevel := E.LastFMSQLevel;
|
||
FMode := E.LastMode;
|
||
FFilter := E.LastFilterIdx;
|
||
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
|
||
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
||
UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI
|
||
ApplyModeFilter;
|
||
FFMCTCSSOn := E.LastCTCSSOn;
|
||
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
||
SetFMCTCSSTone(E.LastCTCSSToneIdx);
|
||
ApplyFMSquelch;
|
||
FFMStepOn := E.LastFMStepOn;
|
||
SetFMStep(E.LastFMStepIdx);
|
||
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
|
||
// AGC
|
||
FAGCMode := E.LastAGCMode;
|
||
FAGCTop := E.LastAGCTop;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
|
||
TrkAGC.Position := FAGCTop;
|
||
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
// CTUN
|
||
FCTun := E.LastCTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
// VFO на видимую частоту; FCenterFreq тоже visible
|
||
FVfoA := Vis;
|
||
FCenterFreq := Vis;
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
// Пересчитываем drive byte для нового XVTR слота и отправляем в сеть
|
||
FDriveLevel := CalcDriveByte;
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
|
||
FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
PushXvtrToWeb;
|
||
end;
|
||
|
||
// Деактивирует XVTR — возвращает к HF-режиму.
|
||
// FCurrentBand был сохранён неизменным во время XVTR (см. ActivateXvtrBand,
|
||
// ApplyVfoA, SaveCurrentBand) — он указывает на HF-диапазон, с которого
|
||
// пользователь зашёл в трансвертер. RestoreBand вернёт VFO/Mode/Filter/AGC
|
||
// /CTUN/Span этого диапазона и обновит подсветку HF band-кнопки.
|
||
procedure TMainForm.DeactivateXvtr;
|
||
var i: Integer;
|
||
begin
|
||
if FCurrentXvtr < 0 then Exit;
|
||
// Запоминаем LastFreq для bandstack
|
||
if FCurrentXvtr < CFG_XVTR_COUNT then
|
||
begin
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
FSettings.Save;
|
||
end;
|
||
end;
|
||
FCurrentXvtr := -1;
|
||
// Сбрасываем подсветку XVTR-кнопок
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if BtnXvtrBand[i] <> nil then
|
||
StyleButton(BtnXvtrBand[i], False);
|
||
ApplyXvtrToNetwork;
|
||
// Восстанавливаем HF band: VFO/Mode/Filter/AGC/CTUN/Span из кэша.
|
||
// RestoreBand сам обновит подсветку HF band-кнопки и сетевое состояние.
|
||
if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||
RestoreBand(FCurrentBand);
|
||
PushXvtrToWeb;
|
||
end;
|
||
|
||
procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings);
|
||
begin
|
||
FXvtrSettings := X;
|
||
RebuildXvtrButtons;
|
||
// Если активный XVTR стал disabled — выйти из XVTR режима
|
||
if (FCurrentXvtr >= 0) and
|
||
((FCurrentXvtr >= CFG_XVTR_COUNT) or
|
||
(not FXvtrSettings.Entries[FCurrentXvtr].Enabled)) then
|
||
DeactivateXvtr;
|
||
// Перепрошить XVTR в сеть (если параметры текущего изменились)
|
||
ApplyXvtrToNetwork;
|
||
PushXvtrToWeb;
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
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 FXvtrSettings.Entries[i].Enabled then Inc(Cnt);
|
||
SetLength(Arr, Cnt);
|
||
Cnt := 0;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if FXvtrSettings.Entries[i].Enabled then
|
||
begin
|
||
Arr[Cnt].Idx := i;
|
||
Arr[Cnt].Name := FXvtrSettings.Entries[i].ButtonText;
|
||
Inc(Cnt);
|
||
end;
|
||
FWebServer.SetXvtrBands(Arr, FCurrentXvtr);
|
||
end;
|
||
|
||
procedure TMainForm.TrkVolumeChange(Sender: TObject);
|
||
begin
|
||
FVolume := TrkVolume.Position;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Mouse wheel — перестройка VFO
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
||
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
|
||
// Колёсико на форме/спектре/водопаде — меняет VFO с квантованием
|
||
// Шаг зависит от модификаторов: Ctrl+Shift=1МГц, Ctrl=1кГц, Shift=10Гц, иначе 100Гц
|
||
// Квантование: результат выровнен до кратного шагу (как в FreqDisplay)
|
||
// CTUN логика: через ApplyVfoA
|
||
var
|
||
Step: Int64;
|
||
Delta: Int64;
|
||
Base: Int64;
|
||
NewFreq: Int64;
|
||
begin
|
||
if (FMode = MODE_FM) and FFMStepOn and
|
||
not ((ssCtrl in Shift) or (ssShift in Shift)) then
|
||
Step := FM_STEP_HZ[FFMStepIdx]
|
||
else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||
else if ssCtrl in Shift then Step := 1000
|
||
else if ssShift in Shift then Step := 10
|
||
else Step := 100;
|
||
|
||
Delta := 1;
|
||
if WheelDelta < 0 then Delta := -1;
|
||
|
||
// Квантование (та же логика что в FreqDisplay.ChangeByDigit)
|
||
Base := (Round(FVfoA) div Step) * Step;
|
||
if Delta > 0 then
|
||
NewFreq := Base + Step
|
||
else
|
||
begin
|
||
if Round(FVfoA) = Base then
|
||
NewFreq := Base - Step
|
||
else
|
||
NewFreq := Base; // снэп к нижней границе
|
||
end;
|
||
|
||
// В XVTR-режиме HF-клэмп не применяем: ApplyVfoA сам ограничит по FreqBegin/FreqEnd
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
NewFreq := Max(0, NewFreq)
|
||
else
|
||
NewFreq := Max(30000, Min(60000000, NewFreq));
|
||
ApplyVfoA(NewFreq);
|
||
|
||
Handled := True;
|
||
end;
|
||
|
||
|
||
procedure TMainForm.BtnAGCModeClick(Sender: TObject);
|
||
const
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (
|
||
agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := (Sender as TFlatButton).Tag;
|
||
FAGCMode := N;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = N);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetAGC(AGCModes[N], 50.0);
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
end;
|
||
|
||
procedure TMainForm.TrkAGCChange(Sender: TObject);
|
||
begin
|
||
FAGCTop := TrkAGC.Position;
|
||
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// DSP/Audio callbacks — вызываются из рабочих потоков
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.OnAudioReady(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
var
|
||
MonoBuf: array[0..1023] of Single;
|
||
i: Integer;
|
||
begin
|
||
if Assigned(FWebServer) and FWebServer.WebClientActive then
|
||
begin
|
||
for i := 0 to Count - 1 do
|
||
MonoBuf[i] := (Left[i] + Right[i]) * 0.5;
|
||
FWebServer.PushAudio(@MonoBuf[0], Count);
|
||
Exit;
|
||
end;
|
||
FAudioOut.Write(Left, Right, Count);
|
||
end;
|
||
|
||
procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := Min(Count, 1024);
|
||
for i := 0 to N - 1 do
|
||
FSpectrumBuf[i] := Pixels[i]; // local copy for WebServer
|
||
FSpectrumBufCount := N;
|
||
FSpecView.SetSpectrumData(Pixels, Count);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := Min(Count, 1024);
|
||
for i := 0 to N - 1 do
|
||
FWaterfallBuf[i] := Pixels[i]; // local copy for WebServer
|
||
FWaterfallBufCount := N;
|
||
Inc(FWaterfallFrameCounter);
|
||
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
|
||
begin
|
||
FWaterfallFrameCounter := 0;
|
||
FSpecView.SetWaterfallData(Pixels, Count);
|
||
FWaterfallDirty := True;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Settings apply methods (вызываются из TSettingsForm)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||
SpecAvgTimeMS: Double);
|
||
begin
|
||
if FWDSPReady then
|
||
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetFFTParams(FFTSize, WinType);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||
begin
|
||
if FWDSPReady then
|
||
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS);
|
||
FWfManualHigh := WfHigh;
|
||
FWfManualLow := WfLow;
|
||
FWfAGCOffset := WfAGCOffset;
|
||
FSpecView.WfManualHigh := FWfManualHigh;
|
||
FSpecView.WfManualLow := FWfManualLow;
|
||
FSpecView.WfAGCOffset := FWfAGCOffset;
|
||
FSpecView.ResetWfAvgBuf;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||
begin
|
||
FSpecRefLevel := RefLevel;
|
||
FSpecRange := Range;
|
||
if GridStep > 0 then FSpecGridStep := GridStep;
|
||
// На передаче FSpecView показывает TX-сетку — RX-правка не должна её перетирать.
|
||
if not FTransmitting then ApplySpecViewGridFromState;
|
||
end;
|
||
|
||
procedure TMainForm.ApplySpecViewGridFromState;
|
||
begin
|
||
// При DUP во время TX данные приходят от RX-анализатора → используем RX-сетку.
|
||
if FTransmitting and not 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 FAudioOut.IsOpen then
|
||
FAudioOut.Close;
|
||
FAudioOut.DeviceIndex := DevIndex;
|
||
FAudioOutDevName := DevName;
|
||
try
|
||
FAudioOut.Open;
|
||
except
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||
begin
|
||
if FAudioIn.IsOpen then
|
||
FAudioIn.Close;
|
||
FAudioIn.DeviceIndex := DevIndex;
|
||
FAudioInDevName := DevName;
|
||
if DevIndex >= 0 then
|
||
begin
|
||
try
|
||
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 FAudioOut.OutputBufferSize = BufferSize then Exit;
|
||
|
||
WasOpen := FAudioOut.IsOpen;
|
||
if WasOpen then FAudioOut.Close;
|
||
FAudioOut.OutputBufferSize := BufferSize;
|
||
if WasOpen then
|
||
begin
|
||
try
|
||
FAudioOut.Open;
|
||
except
|
||
end;
|
||
end;
|
||
FSettings.SaveAudioBufferSize(BufferSize);
|
||
FSettings.Save;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
|
||
begin
|
||
FShowSpectrum := ShowSpectrum;
|
||
FShowWaterfall := ShowWaterfall;
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyFPS(FPS: Integer);
|
||
begin
|
||
if FPS < 1 then FPS := 1;
|
||
if FPS > 100 then FPS := 100;
|
||
FDisplayFPS := FPS;
|
||
FSpectrumTimer.Interval := 1000 div FPS;
|
||
// Thetis по умолчанию обновляет водопад через один display frame.
|
||
FWaterfallFrameInterval := 2;
|
||
FWaterfallFrameCounter := 0;
|
||
FWaterfallDirty := True;
|
||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDisplayFPS(FPS);
|
||
end;
|
||
|
||
procedure TMainForm.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.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(FTXSettings,
|
||
IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType));
|
||
SF.LoadAlexSettings(FAlexSettings,
|
||
IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType));
|
||
SF.LoadXvtrSettings(FXvtrSettings);
|
||
SF.LoadPASettings(FPAMaxPower, FPABandCal);
|
||
SF.LoadVHFCalSettings(FVHFBandCal);
|
||
SF.LoadCATSettings(
|
||
FCATLastGlobal.CATSerialEnabled,
|
||
FCATLastGlobal.CATSerialPort,
|
||
FCATLastGlobal.CATSerialBaud,
|
||
FCATLastGlobal.CATSerialDataBits,
|
||
FCATLastGlobal.CATSerialStopBits,
|
||
FCATLastGlobal.CATSerialParity,
|
||
FCATLastGlobal.CATTcpEnabled,
|
||
FCATLastGlobal.CATTcpPort);
|
||
// Перечисляем PA устройства
|
||
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
|
||
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
|
||
SF.LoadWfAGCNF(FWfAGCEnabled, FWfNFEnabled);
|
||
SF.LoadADCSettings(FDitherEnabled, FRandomEnabled);
|
||
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass);
|
||
SF.LoadFPS(FDisplayFPS);
|
||
SF.LoadLightTheme(FLightTheme);
|
||
SF.LoadFreqMhzDigits(FFreqMhzDigits);
|
||
SF.LoadAudioBufferSize(FAudioOut.OutputBufferSize);
|
||
// Загружаем текущие значения
|
||
if FWDSPReady then
|
||
SF.LoadValues(
|
||
FDSPEngine.FFTSize,
|
||
FDSPEngine.WindowType,
|
||
FDSPEngine.SpecDetector,
|
||
FDSPEngine.SpecAvgMode,
|
||
FDSPEngine.SpecAvgTimeMS,
|
||
FDSPEngine.WfDetector,
|
||
FDSPEngine.WfAvgMode,
|
||
FDSPEngine.WfAvgTimeMS,
|
||
FWfManualHigh,
|
||
FWfManualLow,
|
||
FWfAGCOffset,
|
||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||
FAudioOutDevName, FAudioInDevName)
|
||
else
|
||
SF.LoadValues(
|
||
131072, 2, 0, 3, 30.0,
|
||
0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset,
|
||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||
FAudioOutDevName, FAudioInDevName);
|
||
SF.Show;
|
||
end;
|
||
|
||
procedure TMainForm.AfterShowTick(Sender: TObject);
|
||
begin
|
||
// Вызывается один раз через 200 мс после старта формы
|
||
FAfterShowTimer.Enabled := False;
|
||
|
||
// Повторно применяем сохранённую позицию: к этому моменту WM уже декорировал
|
||
// окно и Qt знает frame extents, поэтому move() ставит рамку точно.
|
||
if FHasPendingRestore then
|
||
begin
|
||
SetBounds(FRestoreL, FRestoreT, Width, Height);
|
||
FHasPendingRestore := False;
|
||
end;
|
||
|
||
try
|
||
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);
|
||
FSettings.SaveCATSettings(FCATLastGlobal);
|
||
if FDevConnected then
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
FSettings.Save;
|
||
end;
|
||
|
||
// --- Getters (called from CAT thread — read only, no sync needed) -----------
|
||
|
||
function TMainForm.CATGetVfoA: Double; begin Result := FVfoA; end;
|
||
function TMainForm.CATGetVfoB: Double; begin Result := FVfoB; end;
|
||
function TMainForm.CATGetMode: Integer; begin Result := FMode; end;
|
||
function TMainForm.CATGetActiveVfo: Integer; begin Result := FActiveVfo; end;
|
||
function TMainForm.CATGetAGCMode: Integer; begin Result := FAGCMode; end;
|
||
function TMainForm.CATGetVolume: Integer; begin Result := FVolume; end;
|
||
function TMainForm.CATGetDriveLevel: Integer;begin Result := TrkDrive.Position; end;
|
||
function TMainForm.CATGetFilterIdx: Integer; begin Result := FFilter; end;
|
||
function TMainForm.CATGetFilterBW: Integer; begin Result := FFilterBW; end;
|
||
function TMainForm.CATGetNRMode: Integer; begin Result := BtnNR.Tag; end;
|
||
function TMainForm.CATGetNBMode: Integer; begin Result := BtnNB.Tag; end;
|
||
function TMainForm.CATGetSNB: Boolean; begin Result := BtnSNB.Tag <> 0; end;
|
||
function TMainForm.CATGetANF: Boolean; begin Result := BtnANF.Tag <> 0; end;
|
||
function TMainForm.CATGetTX: Boolean; begin Result := FTransmitting; end;
|
||
function TMainForm.CATGetRunning: Boolean; begin Result := FRunning; end;
|
||
function TMainForm.CATGetSMeter: Double; begin Result := FLastSMeter; end;
|
||
function TMainForm.CATGetBand: Integer; begin Result := FCurrentBand; end;
|
||
|
||
// --- Setters ----------------------------------------------------------------
|
||
|
||
procedure TMainForm.CATSetVfoA(V: Double);
|
||
begin
|
||
FCATSyncFreq := V;
|
||
TThread.Synchronize(nil, SyncCATVfoA);
|
||
end;
|
||
|
||
procedure TMainForm.CATSetVfoB(V: Double);
|
||
begin
|
||
FCATSyncFreq := V;
|
||
TThread.Synchronize(nil, SyncCATVfoB);
|
||
end;
|
||
|
||
procedure TMainForm.CATSetFilterIdx(V: Integer);
|
||
begin
|
||
// WebOnFilter: negative value encodes 0-based index as -(idx+1)
|
||
WebOnFilter(-(V + 1));
|
||
end;
|
||
|
||
procedure TMainForm.CATDoBandUp;
|
||
begin TThread.Synchronize(nil, SyncCATBandUp); end;
|
||
|
||
procedure TMainForm.CATDoBandDown;
|
||
begin TThread.Synchronize(nil, SyncCATBandDown); end;
|
||
|
||
procedure TMainForm.CATDoTuneUp;
|
||
begin TThread.Synchronize(nil, SyncCATTuneUp); end;
|
||
|
||
procedure TMainForm.CATDoTuneDown;
|
||
begin TThread.Synchronize(nil, SyncCATTuneDown); end;
|
||
|
||
// --- Sync methods (run in main thread) --------------------------------------
|
||
|
||
procedure TMainForm.SyncCATVfoA;
|
||
var BandIdx: Integer;
|
||
begin
|
||
FVfoA := FCATSyncFreq;
|
||
if FActiveVfo = 0 then
|
||
begin
|
||
ApplyVfoA(Round(FVfoA));
|
||
end
|
||
else
|
||
begin
|
||
// VFO A is not active — just update display and band indicator
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
BandIdx := FreqToBandIdx(FVfoA);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SyncCATVfoB;
|
||
var BandIdx: Integer;
|
||
begin
|
||
FVfoB := FCATSyncFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FRunning then
|
||
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
UpdateVfoDisplay;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SyncCATBandUp;
|
||
begin
|
||
if FCurrentBand < BAND_COUNT - 1 then
|
||
WebOnBand(FCurrentBand + 1);
|
||
end;
|
||
|
||
procedure TMainForm.SyncCATBandDown;
|
||
begin
|
||
if FCurrentBand > 0 then
|
||
WebOnBand(FCurrentBand - 1);
|
||
end;
|
||
|
||
procedure TMainForm.SyncCATTuneUp;
|
||
begin
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FVfoA) + 10)
|
||
else
|
||
begin
|
||
FCATSyncFreq := FVfoB + 10;
|
||
SyncCATVfoB;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SyncCATTuneDown;
|
||
begin
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FVfoA) - 10)
|
||
else
|
||
begin
|
||
FCATSyncFreq := FVfoB - 10;
|
||
SyncCATVfoB;
|
||
end;
|
||
end;
|
||
|
||
end.
|