mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
- Move PanelTX above PanelBands; DRV slider now below MOX/TUN buttons - MOX/TUN resized to match standard button style (54×24, BTN_SM) - PanelBands default height reduced to 74 (no reserved XVTR space) - RebuildXvtrButtons dynamically resizes PanelBands (+27px per XVTR row) and calls RelayoutBelowBands to reposition Mode/Filter/CTUN/RX panels Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6431 lines
226 KiB
ObjectPascal
6431 lines
226 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, FlatButton, FlatSlider, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
|
||
LCLIntf, LCLType, GraphType,
|
||
HPSDRProtocol, HPSDRNetwork,
|
||
WDSP, WDSPEngine, AudioOutput, AudioInput,
|
||
Settings,
|
||
WebServer,
|
||
CATEngine, CATSerial, CATTcp,
|
||
SpectrumView,
|
||
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','750','600','500','400','250','100','50','25');
|
||
FILT_CW_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(1000, 800, 750, 600, 500, 400, 250, 100, 50, 25);
|
||
FILT_CW_DEF = 4; // 500
|
||
|
||
// AM/SAM/DSB
|
||
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('16k','12k','10k','8k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
|
||
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400);
|
||
FILT_AM_DEF = 3; // 8k
|
||
|
||
// FM
|
||
FILT_FM_NAMES: array[0..FILT_COUNT-1] of string =
|
||
('20k','15k','12k','10k','8k','6k','5k','4k','3k','2k');
|
||
FILT_FM_BW: array[0..FILT_COUNT-1] of Integer =
|
||
(20000, 15000, 12000, 10000, 8000, 6000, 5000, 4000, 3000, 2000);
|
||
FILT_FM_DEF = 0; // 20k
|
||
|
||
BAND_NAMES: array[0..BAND_COUNT-1] of string = (
|
||
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
|
||
BAND_FREQ: array[0..BAND_COUNT-1] of Double = (
|
||
1900000, 3750000, 5357000, 7100000, 10125000,
|
||
14200000, 18120000, 21200000, 24940000, 28500000, 50150000);
|
||
|
||
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
|
||
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
|
||
|
||
SMETER_WIDTH_RATIO = 0.32;
|
||
SMETER_MARGIN = 3;
|
||
TOP_SMETER_GAP = 72;
|
||
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;
|
||
FPLLLock: Boolean;
|
||
FHWPTT: Boolean;
|
||
public
|
||
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean);
|
||
procedure Execute;
|
||
end;
|
||
|
||
// Синхронизирующий объект для DDC IQ
|
||
TDDCSeqSync = class
|
||
private
|
||
FForm: TObject;
|
||
FDDCIdx: Integer;
|
||
FSeq: LongWord;
|
||
public
|
||
constructor Create(AForm: TObject; Idx: Integer; Seq: LongWord);
|
||
procedure Execute;
|
||
end;
|
||
|
||
{ TMainForm }
|
||
TMainForm = class(TForm)
|
||
private
|
||
// ---- Network ----
|
||
FNetwork: THPSDRNetwork;
|
||
FDevices: array of TDeviceItem;
|
||
FDeviceCount: Integer;
|
||
FDeviceDialog: TDeviceDialog;
|
||
FPendingIP: string; // IP устройства для подключения
|
||
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
|
||
FPanelHidden: Boolean; // True = левая панель скрыта
|
||
FPendingBoardType: Integer; // BoardType устройства для подключения
|
||
|
||
// ---- DSP + Audio ----
|
||
FDSPEngine: TWDSPEngine;
|
||
FAudioOut: TAudioOutput;
|
||
FAudioIn: TAudioInput;
|
||
FAudioOutDevName: string; // текущее имя выходного устройства
|
||
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
|
||
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 на диапазон
|
||
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 нажата
|
||
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
|
||
FLastFwdW: Double;
|
||
FLastSWR: Double;
|
||
// Эти значения приходят с HP Status пакетами (50..200 раз/с). UI обновляется
|
||
// из MeterTimerTick (10 Гц), чтобы цифры/полоски не дёргались.
|
||
FLastSupplyV: 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;
|
||
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;
|
||
// ---- Splitter ----
|
||
FSplitterDrag: Boolean;
|
||
FSplitterDragY0: Integer;
|
||
FSplitterSH0: Integer;
|
||
FSplitterRatio: Double;
|
||
// --- Settings ---
|
||
FSettings: TSettingsManager;
|
||
FWebServer: TWebServer; // веб-интерфейс (порт 8080)
|
||
FSavedMicSource: TTXMicSource; // сохраняется при переключении на txmsWeb
|
||
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; // однократный таймер для пост-инициализации
|
||
|
||
// ---- Toolbar ----
|
||
PanelToolbar: TPanel;
|
||
BtnDiscover: TFlatButton;
|
||
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
|
||
BtnSettings: TFlatButton; // открыть диалог настроек
|
||
PanelSep: TPanel; // 1px разделитель под тулбаром
|
||
|
||
// ---- 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;
|
||
|
||
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;
|
||
PanelSpanButtons: TPanel;
|
||
LblSpan: TLabel;
|
||
BtnSpan48k: TFlatButton;
|
||
BtnSpan96k: TFlatButton;
|
||
BtnSpan192k: TFlatButton;
|
||
BtnSpan384k: TFlatButton;
|
||
BtnSpan768k: TFlatButton;
|
||
BtnSpan1536k: TFlatButton;
|
||
BtnHidePanel: TFlatButton; // скрыть/показать левую панель
|
||
PbSpectrum: TPaintBox;
|
||
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
||
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
||
PbWaterfall: TPaintBox;
|
||
|
||
// ---- Status bar ----
|
||
StatusBar1: TStatusBar;
|
||
|
||
// ---- Helpers ----
|
||
procedure BuildUI;
|
||
procedure ApplyDarkTheme;
|
||
procedure SetLightTheme(V: Boolean);
|
||
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
|
||
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
|
||
procedure PositionSpanOverlayButtons;
|
||
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: Double; PLLLock, HWPTT: Boolean);
|
||
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;
|
||
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 PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||
procedure BtnVfoSwapClick(Sender: TObject);
|
||
procedure BtnVfoACopyBClick(Sender: TObject);
|
||
procedure BtnVfoBCopyAClick(Sender: TObject);
|
||
procedure BtnMOXClick(Sender: TObject);
|
||
procedure BtnMuteClick(Sender: TObject);
|
||
procedure UpdateNRButton;
|
||
procedure UpdateNBButton;
|
||
procedure UpdateSNBButton;
|
||
procedure UpdateANFButton;
|
||
procedure ApplyNoiseFilterButtonsToDSP;
|
||
procedure BtnNRClick(Sender: TObject);
|
||
procedure BtnNBClick(Sender: TObject);
|
||
procedure BtnSNBClick(Sender: TObject);
|
||
procedure BtnANFClick(Sender: TObject);
|
||
procedure BtnSpanClick(Sender: TObject);
|
||
// Веб-интерфейс: callbacks от TWebServer
|
||
procedure WebOnFreq(Hz: Double);
|
||
procedure WebOnMode(Mode: Integer);
|
||
procedure WebOnFilter(BW: Integer);
|
||
procedure WebOnAGC(Mode: Integer);
|
||
procedure WebOnAGCTop(DB: Integer);
|
||
procedure WebOnBand(Idx: Integer);
|
||
procedure WebOnSpan(Hz: Integer);
|
||
procedure WebOnVolume(V: Integer);
|
||
procedure WebOnWfAGC(On_: Boolean);
|
||
procedure WebOnWfNF(On_: Boolean);
|
||
procedure WebOnRun(On_: Boolean);
|
||
procedure WebOnMute(On_: Boolean);
|
||
procedure WebOnCtun(On_: Boolean);
|
||
procedure WebOnNR(Mode: Integer);
|
||
procedure WebOnNB(Mode: Integer);
|
||
procedure WebOnSNB(On_: Boolean);
|
||
procedure WebOnANF(On_: Boolean);
|
||
procedure WebOnFreqB(Hz: Double);
|
||
procedure WebOnActiveVfo(Idx: Integer);
|
||
procedure WebOnCenter(Hz: Double);
|
||
procedure WebOnMOX(On_: Boolean);
|
||
procedure WebOnDrive(V: Integer);
|
||
procedure WebOnFreqA(Hz: Double);
|
||
procedure WebOnAttn(Idx: Integer);
|
||
procedure WebOnTun(On_: Boolean);
|
||
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;
|
||
// --- 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 BtnHidePanelClick(Sender: TObject);
|
||
procedure BtnSettingsClick(Sender: TObject);
|
||
procedure PositionVfoOverlay;
|
||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||
procedure PbSpectrumDblClick(Sender: TObject);
|
||
procedure TrkDriveChange(Sender: TObject);
|
||
procedure TrkVolumeChange(Sender: TObject);
|
||
function CalcDriveByte: Byte;
|
||
function ActiveTXFreqHz: Double;
|
||
procedure ApplyMOX(Active: Boolean);
|
||
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
|
||
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
|
||
procedure 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 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);
|
||
// 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: TProgressBar;
|
||
FTimer: TTimer;
|
||
FThread: TWisdomBuildThread;
|
||
FPhase: Integer;
|
||
procedure TimerTick(Sender: TObject);
|
||
public
|
||
constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread); reintroduce;
|
||
end;
|
||
|
||
var
|
||
MainForm: TMainForm;
|
||
|
||
implementation
|
||
|
||
uses SettingsForm;
|
||
|
||
{$R *.lfm}
|
||
|
||
constructor TWisdomBuildThread.Create(const ADirectory: string);
|
||
begin
|
||
inherited Create(False);
|
||
FreeOnTerminate := False;
|
||
FDirectory := ADirectory;
|
||
FError := '';
|
||
end;
|
||
|
||
procedure TWisdomBuildThread.Execute;
|
||
var
|
||
DirA: AnsiString;
|
||
begin
|
||
try
|
||
if not Assigned(@WDSPwisdom) then Exit;
|
||
DirA := AnsiString(FDirectory);
|
||
WDSPwisdom(PAnsiChar(DirA));
|
||
except
|
||
on E: Exception do
|
||
FError := E.ClassName + ': ' + E.Message;
|
||
end;
|
||
end;
|
||
|
||
constructor TWisdomProgressDialog.Create(AOwner: TComponent;
|
||
AThread: TWisdomBuildThread);
|
||
begin
|
||
inherited CreateNew(AOwner);
|
||
FThread := AThread;
|
||
FPhase := 0;
|
||
|
||
Caption := 'WDSP Wisdom';
|
||
Width := 520;
|
||
Height := 140;
|
||
Position := poScreenCenter;
|
||
BorderStyle := bsDialog;
|
||
BorderIcons := [];
|
||
Color := CLR_BG;
|
||
Font.Name := 'Courier New';
|
||
Font.Size := 9;
|
||
Font.Color := CLR_TEXT;
|
||
|
||
FInfoLabel := TLabel.Create(Self);
|
||
FInfoLabel.Parent := Self;
|
||
FInfoLabel.SetBounds(16, 16, 480, 40);
|
||
FInfoLabel.AutoSize := False;
|
||
FInfoLabel.WordWrap := True;
|
||
FInfoLabel.Font.Color := CLR_TEXT;
|
||
FInfoLabel.Caption :=
|
||
'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.';
|
||
|
||
FProgress := TProgressBar.Create(Self);
|
||
FProgress.Parent := Self;
|
||
FProgress.SetBounds(16, 72, 480, 22);
|
||
FProgress.Min := 0;
|
||
FProgress.Max := 100;
|
||
FProgress.Position := 0;
|
||
|
||
FTimer := TTimer.Create(Self);
|
||
FTimer.Interval := 250;
|
||
FTimer.OnTimer := TimerTick;
|
||
FTimer.Enabled := True;
|
||
end;
|
||
|
||
procedure TWisdomProgressDialog.TimerTick(Sender: TObject);
|
||
var
|
||
P: PAnsiChar;
|
||
S: string;
|
||
begin
|
||
FPhase := (FPhase + 7) mod 101;
|
||
FProgress.Position := FPhase;
|
||
|
||
if Assigned(@wisdom_get_status) then
|
||
begin
|
||
P := wisdom_get_status;
|
||
if P <> nil then
|
||
begin
|
||
S := Trim(string(AnsiString(P)));
|
||
if S <> '' then
|
||
FInfoLabel.Caption := S;
|
||
end;
|
||
end;
|
||
|
||
if Assigned(FThread) and FThread.Finished then
|
||
begin
|
||
FTimer.Enabled := False;
|
||
ModalResult := mrOk;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Общая функция декодирования типа платы — используется везде
|
||
// ===========================================================================
|
||
|
||
function BoardTypeName(BoardType: Integer): string;
|
||
begin
|
||
case BoardType of
|
||
1: Result := 'HERMES (ANAN-10/100)';
|
||
2: Result := 'HERMES-E (ANAN-10E/100B)';
|
||
3: Result := 'ANGELIA (ANAN-100D)';
|
||
4: Result := 'ORION (ANAN-200D)';
|
||
5: Result := 'ORION MkII (ANAN-7000/8000)';
|
||
6: Result := 'HERMES-LITE 2';
|
||
10: Result := 'SATURN (G2)';
|
||
else Result := Format('Unknown Board #%d', [BoardType]);
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Sync helpers
|
||
// ===========================================================================
|
||
|
||
constructor TDeviceFoundSync.Create(AForm: TObject;
|
||
const D: THPSDRDevice; const E: string);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FDev := D;
|
||
FEntry := E;
|
||
end;
|
||
|
||
procedure TDeviceFoundSync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoAddDevice(FDev, FEntry);
|
||
end;
|
||
|
||
constructor TStatusUISync.Create(AForm: TObject;
|
||
FW, SW, SV: Double; PLL, HWPTT: Boolean);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FFwdW := FW;
|
||
FSWRV := SW;
|
||
FSupplyV := SV;
|
||
FPLLLock := PLL;
|
||
FHWPTT := HWPTT;
|
||
end;
|
||
|
||
procedure TStatusUISync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT);
|
||
end;
|
||
|
||
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FDDCIdx := Idx;
|
||
FSeq := Seq;
|
||
end;
|
||
|
||
procedure TDDCSeqSync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoUpdateDDCSeqOrNoDevice(FDDCIdx, FSeq);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// FormCreate / FormDestroy
|
||
// ===========================================================================
|
||
|
||
|
||
// ===========================================================================
|
||
// Settings helpers
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.RestoreWindowBounds;
|
||
var
|
||
L, T, Wd, Ht: Integer;
|
||
begin
|
||
FSettings.LoadWindowBounds(L, T, Wd, Ht);
|
||
if (Wd > 400) and (Ht > 300) then
|
||
begin
|
||
Left := L;
|
||
Top := T;
|
||
Width := Wd;
|
||
Height := Ht;
|
||
end;
|
||
end;
|
||
|
||
function TMainForm.MakeBandSettings: TBandSettings;
|
||
begin
|
||
Result.VfoA := FVfoA;
|
||
Result.VfoB := FVfoB;
|
||
Result.Mode := FMode;
|
||
Result.FilterIdx := FFilter;
|
||
Result.FilterBW := FFilterBW;
|
||
Result.AGCMode := FAGCMode;
|
||
Result.AGCTop := FAGCTop;
|
||
Result.CTun := FCTun;
|
||
Result.SpanHz := FSpanHz;
|
||
// Waterfall AGC/NF — глобальные (не диапазонные)
|
||
end;
|
||
|
||
function TMainForm.MakeGlobalSettings: TGlobalSettings;
|
||
var i: Integer;
|
||
begin
|
||
Result.Volume := FVolume;
|
||
Result.DriveLevel := TrkDrive.Position;
|
||
Result.PAMaxPower := FPAMaxPower;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
Result.PABandCal[i] := FPABandCal[i];
|
||
Result.ActiveVfo := FActiveVfo;
|
||
Result.NRMode := BtnNR.Tag;
|
||
Result.NBMode := BtnNB.Tag;
|
||
Result.SNBEnabled := BtnSNB.Tag <> 0;
|
||
Result.ANFEnabled := BtnANF.Tag <> 0;
|
||
Result.AGCSlope := 0;
|
||
Result.AGCHangThreshold := 100;
|
||
Result.WfAGCEnabled := FWfAGCEnabled;
|
||
Result.WfNFEnabled := FWfNFEnabled;
|
||
Result.LastBand := FCurrentBand;
|
||
Result.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
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
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;
|
||
|
||
// --- CTUN ---
|
||
FCTun := B.CTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
if FWDSPReady and not FCTun then
|
||
FDSPEngine.SetShift(0.0);
|
||
|
||
// --- Sample Rate / Span ---
|
||
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
|
||
// Обновляем только FSpanHz для отображения (берём текущий FSampleRate)
|
||
FSpanHz := FSampleRate;
|
||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
|
||
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
|
||
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
|
||
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
|
||
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
|
||
|
||
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
|
||
FCenterFreq := B.VfoA; // DDC = центр диапазона
|
||
FVfoB := B.VfoB;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
|
||
// ApplyVfoA обновит сеть, FreqDisp, shift, перерисует
|
||
ApplyVfoA(Round(B.VfoA));
|
||
end;
|
||
|
||
procedure TMainForm.SaveAllAndExit;
|
||
begin
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
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;
|
||
StartupVfoA, StartupVfoB: Double;
|
||
begin
|
||
FVfoA := 14200000;
|
||
FVfoB := 7100000;
|
||
FActiveVfo := 0;
|
||
FMode := 1;
|
||
FFilter := 5;
|
||
FAGCMode := 1; // MEDIUM
|
||
FAGCTop := 90; // −90 dBm
|
||
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
||
FCTun := False;
|
||
FFilterBW := 2700;
|
||
FSpecDrag := False;
|
||
FDriveLevel := 0;
|
||
FPAMaxPower := 100.0;
|
||
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
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;
|
||
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;
|
||
|
||
// Создаём веб-сервер (логин: admin, пароль: hpsdr — менять здесь)
|
||
FWebServer := TWebServer.Create('admin', 'hpsdr');
|
||
FWebServer.OnFreq := WebOnFreq;
|
||
FWebServer.OnMode := WebOnMode;
|
||
FWebServer.OnFilter := WebOnFilter;
|
||
FWebServer.OnAGC := WebOnAGC;
|
||
FWebServer.OnAGCTop := WebOnAGCTop;
|
||
FWebServer.OnBand := WebOnBand;
|
||
FWebServer.OnSpan := WebOnSpan;
|
||
FWebServer.OnVolume := WebOnVolume;
|
||
FWebServer.OnWfAGC := WebOnWfAGC;
|
||
FWebServer.OnWfNF := WebOnWfNF;
|
||
FWebServer.OnRun := WebOnRun;
|
||
FWebServer.OnMute := WebOnMute;
|
||
FWebServer.OnCtun := WebOnCtun;
|
||
FWebServer.OnNR := WebOnNR;
|
||
FWebServer.OnNB := WebOnNB;
|
||
FWebServer.OnSNB := WebOnSNB;
|
||
FWebServer.OnANF := WebOnANF;
|
||
FWebServer.OnFreqB := WebOnFreqB;
|
||
FWebServer.OnActiveVfo := WebOnActiveVfo;
|
||
FWebServer.OnCenter := WebOnCenter;
|
||
FWebServer.OnMOX := WebOnMOX;
|
||
FWebServer.OnDrive := WebOnDrive;
|
||
FWebServer.OnFreqA := WebOnFreqA;
|
||
FWebServer.OnAttn := WebOnAttn;
|
||
FWebServer.OnTun := WebOnTun;
|
||
FWebServer.OnWebMic := WebOnMic;
|
||
FWebServer.OnXvtrBand := WebOnXvtrBand;
|
||
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;
|
||
FLastFwdW := 0;
|
||
FLastSWR := 1;
|
||
FLastSupplyV:= -1;
|
||
FLastPLLLock:= False;
|
||
FDeviceCount := 0;
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
FVfoOverlay := TVfoOverlay.Create(Self);
|
||
FVfoOverlay.OnSelect := OnModeFilterSelect;
|
||
FVfoOverlay.Visible := False;
|
||
FPanelHidden := False;
|
||
FShowSpectrum := True;
|
||
FShowWaterfall := True;
|
||
FDisplayFPS := 60;
|
||
FFreqMhzDigits := 3;
|
||
FSplitterRatio := 0.40;
|
||
FSplitterDrag := False;
|
||
|
||
FSpecView := TSpectrumView.Create;
|
||
FSpecView.VfoA := FVfoA;
|
||
FSpecView.VfoB := FVfoB;
|
||
FSpecView.ActiveVfo := FActiveVfo;
|
||
FSpecView.CenterFreq := FCenterFreq;
|
||
FSpecView.SpanHz := FSpanHz;
|
||
FSpecView.Mode := FMode;
|
||
FSpecView.FilterBW := FFilterBW;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
FSpecView.WfAGCEnabled := FWfAGCEnabled;
|
||
FSpecView.WfNFEnabled := FWfNFEnabled;
|
||
FSpecView.WfManualHigh := FWfManualHigh;
|
||
FSpecView.WfManualLow := FWfManualLow;
|
||
FSpecView.WfAGCOffset := FWfAGCOffset;
|
||
FSpecView.SpecRefLevel := FSpecRefLevel;
|
||
FSpecView.SpecRange := FSpecRange;
|
||
FSpecView.SpecGridStep := FSpecGridStep;
|
||
// 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;
|
||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
|
||
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
|
||
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
|
||
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
|
||
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
|
||
ResizeSMeter;
|
||
ApplyDarkTheme;
|
||
UpdateVfoDisplay;
|
||
UpdateFilterButtons;
|
||
|
||
// DSP Engine — создаём объект, Open вызовется при нажатии START
|
||
// (FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек)
|
||
FWDSPReady := False;
|
||
// BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz.
|
||
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
|
||
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
|
||
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
|
||
FDSPEngine.OnAudio := OnAudioReady;
|
||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
|
||
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
|
||
FDSPEngine.OnPullMicSamples := PullSoundCardMic;
|
||
|
||
// Audio output/input — создаём объекты сейчас, открываем после показа формы
|
||
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
|
||
FAudioOut := TAudioOutput.Create(48000);
|
||
FAudioIn := TAudioInput.Create(48000);
|
||
|
||
FMeterTimer := TTimer.Create(Self);
|
||
FMeterTimer.Interval := 100;
|
||
FMeterTimer.OnTimer := MeterTimerTick;
|
||
FMeterTimer.Enabled := True;
|
||
|
||
|
||
FSpectrumTimer := TTimer.Create(Self);
|
||
FSpectrumTimer.Interval := 50;
|
||
FSpectrumTimer.OnTimer := SpectrumTimerTick;
|
||
FSpectrumTimer.Enabled := True;
|
||
|
||
// Однократный таймер — инициализация аудио после показа формы
|
||
FAfterShowTimer := TTimer.Create(Self);
|
||
FAfterShowTimer.Interval := 200;
|
||
FAfterShowTimer.OnTimer := AfterShowTick;
|
||
FAfterShowTimer.Enabled := True;
|
||
|
||
OnMouseWheel := FormMouseWheel;
|
||
|
||
// Проверяем и при необходимости добавляем исключение в Windows Firewall
|
||
// (UDP входящий трафик для HPSDR Protocol 2)
|
||
{$IFDEF WINDOWS}
|
||
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
|
||
{$ENDIF}
|
||
end;
|
||
|
||
procedure TMainForm.FormDestroy(Sender: TObject);
|
||
begin
|
||
FMeterTimer.Enabled := False;
|
||
FSpectrumTimer.Enabled := False;
|
||
if FNetwork.Running then
|
||
FNetwork.SetRunAndFreq(False, 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.SaveWindowBounds(Left, Top, Width, Height);
|
||
FSettings.Save;
|
||
FSettings.Free;
|
||
FWebServer.Stop;
|
||
FWebServer.Free;
|
||
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
|
||
if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end;
|
||
FreeAndNil(FCATEngine);
|
||
end;
|
||
|
||
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
|
||
begin
|
||
// Останавливаем трансивер как при нажатии STOP
|
||
if FNetwork.Connected then
|
||
begin
|
||
if FRunning then
|
||
begin
|
||
FNetwork.UpdateState(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;
|
||
|
||
function MakeBtn(Parent: TWinControl; const Cap: string;
|
||
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
|
||
begin
|
||
Result := TFlatButton.Create(Self);
|
||
Result.Parent := Parent;
|
||
Result.Caption := Cap;
|
||
Result.Left := ALeft;
|
||
Result.Top := ATop;
|
||
Result.Width := AW;
|
||
Result.Height := AH;
|
||
Result.OnClick := Handler;
|
||
StyleButton(Result, False);
|
||
end;
|
||
|
||
procedure MakeLbl(Parent: TWinControl; const Cap: string; ALeft, ATop: Integer);
|
||
var
|
||
L: TLabel;
|
||
begin
|
||
L := TLabel.Create(Self);
|
||
L.Parent := Parent;
|
||
L.Caption := Cap;
|
||
L.Left := ALeft;
|
||
L.Top := ATop;
|
||
L.Font.Color := CLR_TEXTDIM;
|
||
L.Font.Name := 'Courier New';
|
||
L.Font.Size := 7;
|
||
end;
|
||
|
||
begin
|
||
// ---- Toolbar ----
|
||
PanelToolbar := TPanel.Create(Self);
|
||
PanelToolbar.Parent := Self;
|
||
PanelToolbar.Align := alTop;
|
||
PanelToolbar.Height := 36;
|
||
PanelToolbar.BevelOuter := bvNone;
|
||
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);
|
||
|
||
// ---- Separator line ----
|
||
|
||
// ---- Status bar ----
|
||
StatusBar1 := TStatusBar.Create(Self);
|
||
StatusBar1.Parent := Self;
|
||
StatusBar1.Align := alBottom;
|
||
StatusBar1.SimplePanel := False;
|
||
with StatusBar1.Panels do
|
||
begin
|
||
Add; Items[0].Width := 150; Items[0].Text := 'IP: --';
|
||
Add; Items[1].Width := 120; Items[1].Text := 'Board: --';
|
||
Add; Items[2].Width := 200; Items[2].Text := 'Supply: --';
|
||
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
|
||
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
|
||
Add; Items[5].Width := 280; Items[5].Text := 'TX: --';
|
||
end;
|
||
|
||
// ---- Left panel ----
|
||
PanelLeft := TPanel.Create(Self);
|
||
PanelLeft.Parent := Self;
|
||
PanelLeft.Align := alLeft;
|
||
PanelLeft.Width := LEFT_W;
|
||
PanelLeft.BevelOuter := bvNone;
|
||
|
||
Y := 38; // gap below separator: separator at y=66, content starts at y=74
|
||
|
||
// Compact top VFO group
|
||
PanelTopVfoGroup := TPanel.Create(Self);
|
||
PanelTopVfoGroup.Parent := Self;
|
||
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);
|
||
|
||
// 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.OnResize := RightPanelResize;
|
||
|
||
PanelSpanButtons := TPanel.Create(Self);
|
||
PanelSpanButtons.Parent := PanelRight;
|
||
PanelSpanButtons.Align := alTop;
|
||
PanelSpanButtons.Height := 38;
|
||
PanelSpanButtons.BevelOuter := bvNone;
|
||
|
||
LblSpan := TLabel.Create(Self);
|
||
LblSpan.Parent := PanelSpanButtons; LblSpan.Left := 4; LblSpan.Top := 8;
|
||
LblSpan.Caption := 'SPAN:'; LblSpan.Font.Color := CLR_TEXTDIM;
|
||
LblSpan.Font.Name := 'Courier New'; LblSpan.Font.Size := 7;
|
||
|
||
BtnSpan48k := MakeBtn(PanelSpanButtons, '48k', 50, 3, 46, BTN_SM, BtnSpanClick);
|
||
BtnSpan96k := MakeBtn(PanelSpanButtons, '96k', 99, 3, 46, BTN_SM, BtnSpanClick);
|
||
BtnSpan192k := MakeBtn(PanelSpanButtons, '192k', 148, 3, 52, BTN_SM, BtnSpanClick);
|
||
BtnSpan384k := MakeBtn(PanelSpanButtons, '384k', 203, 3, 52, BTN_SM, BtnSpanClick);
|
||
BtnSpan768k := MakeBtn(PanelSpanButtons, '768k', 258, 3, 52, BTN_SM, BtnSpanClick);
|
||
BtnSpan1536k := MakeBtn(PanelSpanButtons, '1536k', 313, 3, 58, BTN_SM, BtnSpanClick);
|
||
BtnSpan48k.Tag := 48000;
|
||
BtnSpan96k.Tag := 96000;
|
||
BtnSpan192k.Tag := 192000;
|
||
BtnSpan384k.Tag := 384000;
|
||
BtnSpan768k.Tag := 768000;
|
||
BtnSpan1536k.Tag := 1536000;
|
||
StyleSpanButton(BtnSpan48k, False);
|
||
StyleSpanButton(BtnSpan96k, False);
|
||
StyleSpanButton(BtnSpan192k, True);
|
||
StyleSpanButton(BtnSpan384k, False);
|
||
StyleSpanButton(BtnSpan768k, False);
|
||
StyleSpanButton(BtnSpan1536k, False);
|
||
|
||
BtnHidePanel := MakeBtn(PanelRight, '◀', 0, 0, 28, 28, BtnHidePanelClick);
|
||
BtnHidePanel.Font.Size := 10;
|
||
BtnHidePanel.ClrNorm := TColor($00543F18);
|
||
BtnHidePanel.ClrHot := TColor($00624B1D);
|
||
BtnHidePanel.ClrActive := TColor($00624B1D);
|
||
BtnHidePanel.ClrBorder := TColor($00685624);
|
||
BtnHidePanel.ClrText := TColor($00D8E6EE);
|
||
BtnHidePanel.ClrTextAct := TColor($00F0FAFF);
|
||
BtnHidePanel.BringToFront;
|
||
|
||
// Разделитель: на уровне формы, под всеми верхними элементами
|
||
PanelSep := TPanel.Create(Self);
|
||
PanelSep.Parent := Self;
|
||
PanelSep.Align := alNone;
|
||
PanelSep.BevelOuter := bvNone;
|
||
PanelSep.ParentColor := False;
|
||
PanelSep.Color := TColor($00606060);
|
||
PanelSep.SetBounds(16, PanelToolbar.Height + 30, ClientWidth - 32, 2);
|
||
|
||
// Большой S-метр — на главной форме поверх двух панелей
|
||
// S-метр: Parent=Self, перекрывает Toolbar+SpanButtons вертикально
|
||
PanelSMeterRight := TPanel.Create(Self);
|
||
PanelSMeterRight.Parent := Self;
|
||
PanelSMeterRight.BevelOuter := bvNone;
|
||
PanelSMeterRight.Color := CLR_PANEL;
|
||
PbSMeterRight := TPaintBox.Create(Self);
|
||
PbSMeterRight.Parent := PanelSMeterRight;
|
||
PbSMeterRight.Align := alClient;
|
||
PbSMeterRight.OnPaint := FSpecView.PaintSMeterRight;
|
||
PbSMeterRight.Color := CLR_PANEL;
|
||
|
||
PbSpectrum := TPaintBox.Create(Self);
|
||
PbSpectrum.Parent := PanelRight;
|
||
PbSpectrum.OnPaint := FSpecView.PaintSpectrum;
|
||
PbSpectrum.OnMouseDown := PbSpectrumMouseDown;
|
||
PbSpectrum.OnDblClick := PbSpectrumDblClick;
|
||
PbSpectrum.OnMouseMove := PbSpectrumMouseMove;
|
||
PbSpectrum.OnMouseUp := PbSpectrumMouseUp;
|
||
|
||
PbRuler := TPaintBox.Create(Self);
|
||
PbRuler.Parent := PanelRight;
|
||
PbRuler.OnPaint := FSpecView.PaintRuler;
|
||
PbRuler.Cursor := crDefault;
|
||
FSpecView.PbRuler := PbRuler;
|
||
|
||
// ---- Splitter между спектром+линейкой и водопадом ----
|
||
PanelSplitter := TPanel.Create(Self);
|
||
PanelSplitter.Parent := PanelRight;
|
||
PanelSplitter.BevelOuter := bvNone;
|
||
PanelSplitter.Color := TColor($00303030);
|
||
PanelSplitter.Cursor := crVSplit;
|
||
PanelSplitter.Height := 5;
|
||
PanelSplitter.OnMouseDown := SplitterMouseDown;
|
||
PanelSplitter.OnMouseMove := SplitterMouseMove;
|
||
PanelSplitter.OnMouseUp := SplitterMouseUp;
|
||
|
||
PbWaterfall := TPaintBox.Create(Self);
|
||
PbWaterfall.Parent := PanelRight;
|
||
PbWaterfall.OnPaint := FSpecView.PaintWaterfall;
|
||
PbWaterfall.OnMouseDown := PbWaterfallMouseDown;
|
||
PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
|
||
PbWaterfall.OnMouseUp := PbWaterfallMouseUp;
|
||
|
||
// SPAN кнопки как оверлей на спектре (в стиле web)
|
||
BtnSpan48k.Parent := PanelRight;
|
||
BtnSpan96k.Parent := PanelRight;
|
||
BtnSpan192k.Parent := PanelRight;
|
||
BtnSpan384k.Parent := PanelRight;
|
||
BtnSpan768k.Parent := PanelRight;
|
||
BtnSpan1536k.Parent := PanelRight;
|
||
LblSpan.Visible := False;
|
||
PositionSpanOverlayButtons;
|
||
|
||
// Initial layout
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Splitter — перетаскивание границы спектр/водопад
|
||
// Обновление размеров происходит только при отпускании мыши (MouseUp),
|
||
// чтобы исключить фризы во время перетаскивания.
|
||
// Во время перетаскивания рисуем только призрак-линию на PanelSplitter.
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
begin
|
||
if Button <> mbLeft then Exit;
|
||
FSplitterDrag := True;
|
||
FSplitterSH0 := FSpectrumHeight;
|
||
// Y в координатах PanelRight
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
FSplitterDragY0 := P.Y;
|
||
PanelSplitter.Color := TColor($005050A0); // подсветка при захвате
|
||
{$IFDEF WINDOWS}
|
||
SetCapture(PanelSplitter.Handle);
|
||
{$ENDIF}
|
||
end;
|
||
|
||
procedure TMainForm.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
DeltaY: Integer;
|
||
NewSH: Integer;
|
||
TopOff, AvailH: Integer;
|
||
const
|
||
RULER_H = 18;
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60;
|
||
MIN_WH = 40;
|
||
begin
|
||
if not FSplitterDrag then Exit;
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
DeltaY := P.Y - FSplitterDragY0;
|
||
|
||
TopOff := PanelSpanButtons.Height;
|
||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH <= 0 then Exit;
|
||
|
||
NewSH := FSplitterSH0 + DeltaY;
|
||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||
|
||
// Только двигаем сплиттер визуально — без пересчёта битмапов
|
||
PanelSplitter.Top := TopOff + NewSH + RULER_H;
|
||
PbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H;
|
||
PbWaterfall.Height := AvailH - NewSH;
|
||
end;
|
||
|
||
procedure TMainForm.SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
DeltaY: Integer;
|
||
NewSH: Integer;
|
||
TopOff, AvailH: Integer;
|
||
const
|
||
RULER_H = 18;
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60;
|
||
MIN_WH = 40;
|
||
begin
|
||
if not FSplitterDrag then Exit;
|
||
FSplitterDrag := False;
|
||
{$IFDEF WINDOWS}
|
||
ReleaseCapture;
|
||
{$ENDIF}
|
||
PanelSplitter.Color := TColor($00303030); // обычный цвет
|
||
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
DeltaY := P.Y - FSplitterDragY0;
|
||
|
||
TopOff := PanelSpanButtons.Height;
|
||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH <= 0 then Exit;
|
||
|
||
NewSH := FSplitterSH0 + DeltaY;
|
||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||
|
||
// Сохраняем новое соотношение и делаем полный пересчёт с перерисовкой
|
||
FSplitterRatio := NewSH / AvailH;
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Resize handler (normal method instead of anonymous procedure)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.RightPanelResize(Sender: TObject);
|
||
begin
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
procedure TMainForm.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 (PanelSpanButtons = 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 := VFO_BTN_W + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
|
||
FreqDispB.Left := VFO_B_LEFT_PAD + VFO_BTN_W + 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 + 30 - (MARGIN * 2); // 30 = span row content, not full PanelSpanButtons
|
||
if BlockH < 56 then BlockH := 56;
|
||
|
||
// Центрируем группу в доступном пространстве
|
||
GroupLeft := SafeLeft + (AvailW - GroupW) div 2;
|
||
GroupTop := TOP_OFFSET;
|
||
|
||
// Ось выравнивания: центр S-метра на форме (SMTop=3, SMBot=63) → y=33
|
||
// Относительно PanelTopVfoGroup (GroupTop=2): CenterY = 33 - 2 = 31
|
||
CenterY := (PanelToolbar.Height + 30) 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);
|
||
|
||
if PanelSep <> nil then
|
||
PanelSep.SetBounds(16, PanelToolbar.Height + 30, ClientWidth - 32, 2);
|
||
|
||
PanelTopVfoGroup.BringToFront;
|
||
end;
|
||
|
||
procedure TMainForm.ResizeSMeter;
|
||
// PanelSMeterRight на главной форме (Parent=Self).
|
||
// По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px).
|
||
var
|
||
FW: Integer;
|
||
RightW: Integer; // ширина PanelRight
|
||
SMW: Integer; // ширина S-метра
|
||
SMLeft: Integer;
|
||
SMTop: Integer;
|
||
SMBot: Integer;
|
||
begin
|
||
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or
|
||
(PanelSpanButtons = nil) or (PanelLeft = nil) then Exit;
|
||
|
||
LayoutTopVfoBlock;
|
||
|
||
FW := ClientWidth;
|
||
// Ширина зоны спектра (PanelRight)
|
||
RightW := FW - PanelLeft.Width;
|
||
if RightW < 200 then Exit;
|
||
|
||
// Ширина S-метра = 32% от PanelRight
|
||
SMW := Round(RightW * SMETER_WIDTH_RATIO);
|
||
|
||
// X: прижат к правому краю формы
|
||
SMLeft := FW - SMW - SMETER_MARGIN;
|
||
|
||
// Y: от верха формы+margin до низа PanelSpanButtons-margin
|
||
SMTop := SMETER_MARGIN;
|
||
SMBot := PanelToolbar.Height + 30 - SMETER_MARGIN; // 30 = span row content, ends before separator
|
||
|
||
PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
|
||
PanelSMeterRight.BringToFront;
|
||
end;
|
||
|
||
procedure TMainForm.ResizeSpectrumPanels;
|
||
const
|
||
RULER_H = 18;
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60; // минимальная высота спектра
|
||
MIN_WH = 40; // минимальная высота водопада
|
||
var
|
||
RW, RH, SH, WH, TopOff: Integer;
|
||
AvailH: Integer;
|
||
begin
|
||
if PanelRight = nil then Exit;
|
||
if Assigned(FSpecView) then SyncSpecViewFreq;
|
||
RW := PanelRight.ClientWidth;
|
||
RH := PanelRight.ClientHeight;
|
||
TopOff := PanelSpanButtons.Height;
|
||
// S-метр позиционируется отдельно в ResizeSMeter
|
||
ResizeSMeter;
|
||
|
||
// ---- Оба скрыты ----
|
||
if (not FShowSpectrum) and (not FShowWaterfall) then
|
||
begin
|
||
PbSpectrum.Visible := False;
|
||
PbRuler.Visible := False;
|
||
PanelSplitter.Visible := False;
|
||
PbWaterfall.Visible := False;
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := 0;
|
||
FWaterfallHeight := 0;
|
||
PositionSpanOverlayButtons;
|
||
Exit;
|
||
end;
|
||
|
||
// ---- Только спектр (водопад скрыт) ----
|
||
if FShowSpectrum and (not FShowWaterfall) then
|
||
begin
|
||
AvailH := RH - TopOff - RULER_H;
|
||
if AvailH < MIN_SH then AvailH := MIN_SH;
|
||
SH := AvailH;
|
||
WH := 0;
|
||
PbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||
PbSpectrum.Visible := True;
|
||
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||
PbRuler.Visible := True;
|
||
PanelSplitter.Visible := False;
|
||
PbWaterfall.Visible := False;
|
||
PositionSpanOverlayButtons;
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := SH;
|
||
FWaterfallHeight := 0;
|
||
if RW > 0 then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(RW, SH);
|
||
FSpecView.SetWaterfallBitmapSize(1, 1);
|
||
FSpecView.SetRulerSize(RW, RULER_H);
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
PbRuler.Invalidate;
|
||
end;
|
||
Exit;
|
||
end;
|
||
|
||
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
|
||
if (not FShowSpectrum) and FShowWaterfall then
|
||
begin
|
||
AvailH := RH - TopOff - RULER_H;
|
||
if AvailH < MIN_WH then AvailH := MIN_WH;
|
||
SH := 0;
|
||
WH := AvailH;
|
||
PbSpectrum.Visible := False;
|
||
PanelSplitter.Visible := False;
|
||
PbWaterfall.SetBounds(0, TopOff, RW, WH);
|
||
PbWaterfall.Visible := True;
|
||
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
|
||
PbRuler.Visible := True;
|
||
PositionSpanOverlayButtons;
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := 0;
|
||
FWaterfallHeight := WH;
|
||
if RW > 0 then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(1, 1);
|
||
FSpecView.SetWaterfallBitmapSize(RW, WH);
|
||
FSpecView.SetRulerSize(RW, RULER_H);
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
PbRuler.Invalidate;
|
||
end;
|
||
Exit;
|
||
end;
|
||
|
||
// ---- Оба видимы: стандартный режим со сплиттером ----
|
||
PbSpectrum.Visible := True;
|
||
PbRuler.Visible := True;
|
||
PanelSplitter.Visible := True;
|
||
PbWaterfall.Visible := True;
|
||
|
||
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
||
|
||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||
if FSplitterRatio < MIN_SH / AvailH then
|
||
FSplitterRatio := MIN_SH / AvailH;
|
||
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
|
||
FSplitterRatio := 1.0 - MIN_WH / AvailH;
|
||
|
||
SH := Round(AvailH * FSplitterRatio);
|
||
WH := AvailH - SH;
|
||
if WH < MIN_WH then WH := MIN_WH;
|
||
|
||
// Спектр
|
||
PbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||
// Линейка частот — всегда прижата к низу спектра
|
||
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||
// Сплиттер — между линейкой и водопадом
|
||
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
|
||
// Водопад — под сплиттером
|
||
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
|
||
|
||
PositionSpanOverlayButtons;
|
||
|
||
// Обновляем переменные размеров
|
||
FSpectrumWidth := RW;
|
||
FSpectrumHeight := SH;
|
||
FWaterfallHeight := WH;
|
||
|
||
// Пересоздаём bitmap точно под новый размер
|
||
if RW > 0 then
|
||
begin
|
||
if SH > 0 then FSpecView.SetSpectrumBitmapSize(RW, SH);
|
||
if WH > 0 then FSpecView.SetWaterfallBitmapSize(RW, WH);
|
||
FSpecView.SetRulerSize(RW, RULER_H);
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Dark Theme
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.ApplyDarkTheme;
|
||
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);
|
||
DP(PanelRX); DP(PanelTX);
|
||
DP(PanelRight); DP(PanelSpanButtons);
|
||
|
||
PbSpectrum.Color := T.BG;
|
||
PbWaterfall.Color := T.BG;
|
||
if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel;
|
||
if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel;
|
||
|
||
// Метки
|
||
DL(LblAGCTop, False);
|
||
DL(LblDrv); DL(LblSpan);
|
||
|
||
// Кнопки тулбара
|
||
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);
|
||
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);
|
||
|
||
// Span / VFO-copy / HidePanel
|
||
StyleSpanButton(BtnSpan48k, BtnSpan48k.Active);
|
||
StyleSpanButton(BtnSpan96k, BtnSpan96k.Active);
|
||
StyleSpanButton(BtnSpan192k, BtnSpan192k.Active);
|
||
StyleSpanButton(BtnSpan384k, BtnSpan384k.Active);
|
||
StyleSpanButton(BtnSpan768k, BtnSpan768k.Active);
|
||
StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active);
|
||
StyleSpanButton(BtnVfoACopyB, False);
|
||
StyleSpanButton(BtnVfoSwap, False);
|
||
StyleSpanButton(BtnVfoBCopyA, False);
|
||
if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False);
|
||
|
||
// Ползунки
|
||
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
|
||
|
||
// Спектр
|
||
FSpecView.SetTheme(T);
|
||
|
||
// VFO display — вызывает UpdateVfoDisplay, который использует тему
|
||
UpdateVfoDisplay;
|
||
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.PositionSpanOverlayButtons;
|
||
const
|
||
BTN_TOP = 6;
|
||
GAP = 3;
|
||
W48 = 40;
|
||
W96 = 40;
|
||
W192 = 44;
|
||
W384 = 44;
|
||
W768 = 44;
|
||
W1536 = 50;
|
||
HBTN = 18;
|
||
var
|
||
X, NeedW, BaseX, BaseY: Integer;
|
||
begin
|
||
if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit;
|
||
NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8;
|
||
if PbSpectrum.Width < NeedW then
|
||
begin
|
||
BtnSpan48k.Visible := False;
|
||
BtnSpan96k.Visible := False;
|
||
BtnSpan192k.Visible := False;
|
||
BtnSpan384k.Visible := False;
|
||
BtnSpan768k.Visible := False;
|
||
BtnSpan1536k.Visible := False;
|
||
Exit;
|
||
end;
|
||
|
||
BaseX := PbSpectrum.Left;
|
||
BaseY := PbSpectrum.Top;
|
||
X := BaseX + 56;
|
||
BtnSpan48k.SetBounds(X, BaseY + BTN_TOP, W48, HBTN); Inc(X, W48 + GAP);
|
||
BtnSpan96k.SetBounds(X, BaseY + BTN_TOP, W96, HBTN); Inc(X, W96 + GAP);
|
||
BtnSpan192k.SetBounds(X, BaseY + BTN_TOP, W192, HBTN); Inc(X, W192 + GAP);
|
||
BtnSpan384k.SetBounds(X, BaseY + BTN_TOP, W384, HBTN); Inc(X, W384 + GAP);
|
||
BtnSpan768k.SetBounds(X, BaseY + BTN_TOP, W768, HBTN); Inc(X, W768 + GAP);
|
||
BtnSpan1536k.SetBounds(X, BaseY + BTN_TOP, W1536, HBTN);
|
||
if BtnHidePanel <> nil then
|
||
BtnHidePanel.SetBounds(BaseX, BaseY + BTN_TOP, 28, HBTN);
|
||
|
||
BtnSpan48k.Visible := True;
|
||
BtnSpan96k.Visible := True;
|
||
BtnSpan192k.Visible := True;
|
||
BtnSpan384k.Visible := True;
|
||
BtnSpan768k.Visible := True;
|
||
BtnSpan1536k.Visible := True;
|
||
|
||
BtnSpan48k.BringToFront;
|
||
BtnSpan96k.BringToFront;
|
||
BtnSpan192k.BringToFront;
|
||
BtnSpan384k.BringToFront;
|
||
BtnSpan768k.BringToFront;
|
||
BtnSpan1536k.BringToFront;
|
||
if BtnHidePanel <> nil then BtnHidePanel.BringToFront;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// VFO
|
||
// ===========================================================================
|
||
|
||
function TMainForm.FormatFreq(Hz: Double): string;
|
||
var
|
||
Mhz: 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;
|
||
// 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;
|
||
|
||
// Диагностика TX mic-пути — в отдельной панели [5], чтобы её не перетирали
|
||
// другие источники (DDC seq, audio-логи и т.п.).
|
||
if FWDSPReady then
|
||
begin
|
||
if FTransmitting then
|
||
begin
|
||
if FTXSettings.MicSource = 1 then
|
||
StatusBar1.Panels[5].Text :=
|
||
Format('TX sc: avail=%d ring=%d',
|
||
[FAudioIn.Available, FDSPEngine.TXMicRingAvail])
|
||
else
|
||
StatusBar1.Panels[5].Text :=
|
||
Format('TX hw: ring=%d', [FDSPEngine.TXMicRingAvail]);
|
||
end
|
||
else
|
||
StatusBar1.Panels[5].Text := 'TX: idle';
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||
var
|
||
NowTick: QWord;
|
||
ElapsedTick: QWord;
|
||
begin
|
||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
|
||
// что вызвало бы сброс буфера водопада на каждом тике.
|
||
if (FSpectrumWidth = 0) or
|
||
(FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
|
||
(PbSpectrum.Height <> FSpectrumHeight))) or
|
||
(FShowWaterfall and not FShowSpectrum and
|
||
((PbWaterfall.Width <> FSpectrumWidth) or
|
||
(PbWaterfall.Height <> FWaterfallHeight))) then
|
||
ResizeSpectrumPanels;
|
||
ResizeSMeter;
|
||
if FSpectrumWidth <= 0 then Exit;
|
||
SyncSpecViewFreq;
|
||
|
||
// Проверка таймаута — трансивер не отвечает
|
||
if FRunning and FNetwork.Running then
|
||
begin
|
||
NowTick := GetTickCount64;
|
||
ElapsedTick := NowTick - FRXStartTime;
|
||
// Ждём первый пакет 5 секунд после старта
|
||
if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FRunning := False;
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН';
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
// После первого пакета — следим чтобы поток не прерывался более 3 сек
|
||
if (FRXLastPktTime > 0) and ((NowTick - FRXLastPktTime) > 3000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FRunning := False;
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||
StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ';
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
if FRunning then
|
||
begin
|
||
// Обновляем StatusBar из таймера — без Synchronize в сетевом потоке
|
||
StatusBar1.Panels[3].Text :=
|
||
Format('RX DDC%d | seq=%d | pkts=%d',
|
||
[FLastDDCIndex, FLastDDCSeq, FRXPacketCount]);
|
||
|
||
if FWDSPReady then
|
||
begin
|
||
FLastSMeter := FDSPEngine.GetSMeterDBm;
|
||
FDSPEngine.SetSpectrumWidth(FSpectrumWidth);
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
FSpecView.AGCThresh := FDSPEngine.AGCThresh;
|
||
FSpecView.AGCHangLevel := FDSPEngine.AGCHangLevel;
|
||
end;
|
||
FSpecView.WDSPReady := FWDSPReady;
|
||
// Обновляем оверлей если видим
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.UpdateSMeter(FLastSMeter);
|
||
FVfoOverlay.UpdateVfo(FVfoA);
|
||
PositionVfoOverlay;
|
||
end;
|
||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||
if FSpectrumDirty then
|
||
begin
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if FSpecView.NeedsRulerRedraw then
|
||
PbRuler.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
if FWaterfallDirty then
|
||
begin
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
FWaterfallDirty := False;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Не подключены — показываем чистый экран (без анимации)
|
||
if not FNetwork.Connected then
|
||
begin
|
||
// При первом старте показываем демо-спектр
|
||
if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then
|
||
begin
|
||
FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight);
|
||
FSpecView.ResetSpectrumBuf;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Подключены, но не запущены — статичный спектр (шум)
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
end;
|
||
|
||
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
|
||
if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then
|
||
begin
|
||
if FSpectrumDirty then
|
||
FSpecView.DrawSpectrum;
|
||
if FWaterfallDirty then
|
||
FSpecView.DrawWaterfall;
|
||
if FSpectrumDirty then
|
||
PbSpectrum.Invalidate;
|
||
if FSpecView.NeedsRulerRedraw then
|
||
PbRuler.Invalidate;
|
||
if FWaterfallDirty then
|
||
PbWaterfall.Invalidate;
|
||
FWaterfallDirty := False;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
|
||
// Пушим текущее состояние в веб-клиенты (если есть)
|
||
if Assigned(FWebServer) then
|
||
FWebServer.PushSpectrum(
|
||
FSpectrumBuf, 1024,
|
||
FWaterfallBuf,
|
||
FLastSMeter,
|
||
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
|
||
FSpanHz, FVolume,
|
||
FWfAGCEnabled, FWfNFEnabled,
|
||
FCurrentBand,
|
||
FRunning and FNetwork.Connected,
|
||
FRunning, FMuted, FCTun,
|
||
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
|
||
FCenterFreq, FFilter,
|
||
FVfoB, FActiveVfo,
|
||
FTransmitting, TrkDrive.Position,
|
||
FAtten, FTuning, FDisplayDuplex,
|
||
FLastFwdW, FLastSWR, FPAMaxPower);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Network callbacks → sync helpers
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.OnDeviceFound(const Dev: THPSDRDevice);
|
||
var
|
||
BoardName, Entry: string;
|
||
Sync: TDeviceFoundSync;
|
||
M: TThreadMethod;
|
||
begin
|
||
BoardName := BoardTypeName(Dev.BoardType);
|
||
Entry := Format('%s %s FW:%d DDC:%d',
|
||
[Dev.IPAddress, BoardName,
|
||
Dev.FirmwareVersion, Dev.NumDDCs]);
|
||
Sync := TDeviceFoundSync.Create(Self, Dev, Entry);
|
||
try
|
||
M := Sync.Execute;
|
||
TThread.Synchronize(nil, M);
|
||
finally
|
||
Sync.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
Idx := Length(FDevices);
|
||
SetLength(FDevices, Idx + 1);
|
||
FDevices[Idx].Dev := Dev;
|
||
FDevices[Idx].Display := Entry;
|
||
Inc(FDeviceCount);
|
||
|
||
if Assigned(FDeviceDialog) then
|
||
FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType);
|
||
|
||
StatusBar1.Panels[4].Text := Format('Found: %s', [Dev.IPAddress]);
|
||
end;
|
||
|
||
procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus);
|
||
const
|
||
// Порог fwd-мощности (W), ниже которого SWR не вычисляем — там всё ADC-шум.
|
||
SWR_MIN_FWD_W = 1.0;
|
||
// Верхняя граница SWR (выше — это всё равно «беда»).
|
||
SWR_CAP = 9.9;
|
||
var
|
||
ExcPwr, FwdPwr, RevPwr: Word;
|
||
SupplyV, 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;
|
||
// Board 5 (Orion MkII) measures supply voltage via UserADC0 (bytes 57-58),
|
||
// not the supply_volts field (bytes 49-50) — matching Thetis behaviour.
|
||
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);
|
||
// 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,
|
||
(Status.StatusBits and HPS_PLL_LOCKED) <> 0,
|
||
(Status.StatusBits and HPS_PTT) <> 0);
|
||
try
|
||
M := Sync.Execute;
|
||
TThread.Synchronize(nil, M);
|
||
finally
|
||
Sync.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
|
||
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;
|
||
FLastPLLLock := PLLLock;
|
||
|
||
// Hardware PTT (foot switch / mic PTT) — обнаружение фронта (нужно делать
|
||
// на rate HP Status, а не таймера, чтобы не пропустить короткое нажатие).
|
||
if HWPTT <> FHWPTTActive then
|
||
begin
|
||
FHWPTTActive := HWPTT;
|
||
// Hardware PTT игнорируется во время TUN; иначе включает/выключает передачу
|
||
// только если кнопка MOX не нажата (приоритет за UI).
|
||
if (not FTuning) and (not BtnMOX.Active) then
|
||
ApplyMOX(HWPTT);
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateTXMeters;
|
||
// Вызывается из MeterTimerTick (10 Гц). Использует FLastFwdW/FLastSWR/
|
||
// FLastSupplyV/FLastPLLLock — они обновляются на rate HP Status (~150/с)
|
||
// в DoUpdateStatus.
|
||
var
|
||
PLLStr: string;
|
||
begin
|
||
if FLastPLLLock then PLLStr := 'PLL OK' else PLLStr := 'PLL?';
|
||
if FLastSupplyV >= 0 then
|
||
StatusBar1.Panels[2].Text := Format('Supply: %.1fV | %s', [FLastSupplyV, PLLStr])
|
||
else
|
||
StatusBar1.Panels[2].Text := PLLStr;
|
||
end;
|
||
|
||
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
|
||
var
|
||
SamplesPerFrame: Integer;
|
||
begin
|
||
// Вызывается из сетевого потока — UI не трогаем напрямую
|
||
|
||
// Счётчик принятых пакетов
|
||
Inc(FRXPacketCount);
|
||
FRXLastPktTime := GetTickCount64; // фиксируем время последнего пакета
|
||
|
||
// 1. Подаём IQ данные в DSP — только с активного DDC
|
||
if (DDCIndex = FActiveDDC) and FWDSPReady then
|
||
begin
|
||
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
|
||
Integer(Data.SamplesPerFrame[1]);
|
||
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
|
||
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
|
||
end;
|
||
|
||
// 2. Запоминаем последний seq — StatusBar обновит таймер (без Synchronize в горячем пути)
|
||
FLastDDCSeq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
|
||
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
|
||
FLastDDCIndex := DDCIndex;
|
||
end;
|
||
|
||
procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||
begin
|
||
StatusBar1.Panels[3].Text :=
|
||
Format('RX DDC%d | seq=%d | pkts=%d', [DDCIdx, Seq, FRXPacketCount]);
|
||
end;
|
||
|
||
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
|
||
begin
|
||
// HW mic-стрим уходит в WDSP только если выбран источник Radio.
|
||
// Иначе сэмплы игнорируются (mic берётся со звуковой карты).
|
||
if FTransmitting and FWDSPReady and
|
||
(FTXSettings.MicSource = Ord(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)
|
||
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
|
||
// bit 3 — PTT/bias polarity (не используем)
|
||
// bit 4 — Orion mic bias (1 = enable +5V bias)
|
||
// bit 5 — Saturn 3.5mm/XLR (не используем)
|
||
begin
|
||
Result := 0;
|
||
if FTXSettings.MicLineIn then Result := Result or $01;
|
||
if FTXSettings.MicBoost then Result := Result or $02;
|
||
if not FTXSettings.MicPTTEnabled then Result := Result or $04;
|
||
if FTXSettings.MicBias then Result := Result or $10;
|
||
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;
|
||
// 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;
|
||
|
||
procedure TMainForm.ApplyTXSettingsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
// Mic source — сначала, чтобы PullSoundCardMic уже видел правильное значение
|
||
FDSPEngine.SetTXMicSource(TTXMicSource(FTXSettings.MicSource));
|
||
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 зовёт при MicSource=SoundCard.
|
||
// Тянем только реально доступные сэмплы из 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;
|
||
StatusBar1.Panels[4].Text := 'No hardware found';
|
||
end
|
||
else
|
||
StatusBar1.Panels[3].Text := Format('DDC%d seq=%d', [DDCIdx, Seq]);
|
||
end;
|
||
|
||
procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer);
|
||
begin
|
||
if ASampleRate <= 0 then Exit;
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.Close;
|
||
FreeAndNil(FDSPEngine);
|
||
end;
|
||
FWDSPReady := False;
|
||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||
FDSPEngine.OnAudio := OnAudioReady;
|
||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||
end;
|
||
|
||
function TMainForm.EnsureWDSPWisdom: Boolean;
|
||
var
|
||
WisdomDir: string;
|
||
WisdomPath: string;
|
||
DirA: AnsiString;
|
||
BuildThread: TWisdomBuildThread;
|
||
Dlg: TWisdomProgressDialog;
|
||
begin
|
||
Result := True;
|
||
|
||
if not Assigned(@WDSPwisdom) then Exit;
|
||
|
||
WisdomDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
|
||
WisdomPath := WisdomDir + WDSP_WISDOM_FILE;
|
||
|
||
if FileExists(WisdomPath) then
|
||
begin
|
||
// По рекомендации WDSP вызываем функцию на каждом старте:
|
||
// если файл уже есть, библиотека просто импортирует wisdom.
|
||
DirA := AnsiString(WisdomDir);
|
||
WDSPwisdom(PAnsiChar(DirA));
|
||
Exit;
|
||
end;
|
||
|
||
BtnStartStop.Enabled := False;
|
||
StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...';
|
||
|
||
BuildThread := TWisdomBuildThread.Create(WisdomDir);
|
||
try
|
||
Dlg := TWisdomProgressDialog.Create(Self, BuildThread);
|
||
try
|
||
Dlg.ShowModal;
|
||
finally
|
||
Dlg.Free;
|
||
end;
|
||
|
||
BuildThread.WaitFor;
|
||
|
||
if BuildThread.ErrorText <> '' then
|
||
begin
|
||
ShowMessage('Failed to create WDSP wisdom.' + LineEnding +
|
||
BuildThread.ErrorText);
|
||
Result := False;
|
||
end
|
||
else if not FileExists(WisdomPath) then
|
||
begin
|
||
ShowMessage('WDSP wisdom was not created.' + LineEnding +
|
||
'Expected file: ' + WisdomPath);
|
||
Result := False;
|
||
end;
|
||
finally
|
||
BuildThread.Free;
|
||
BtnStartStop.Enabled := True;
|
||
end;
|
||
|
||
if Result then
|
||
StatusBar1.Panels[3].Text := 'WDSP wisdom ready'
|
||
else
|
||
StatusBar1.Panels[3].Text := 'WDSP wisdom missing';
|
||
end;
|
||
|
||
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
|
||
begin
|
||
// Открываем диалог выбора устройства
|
||
if not Assigned(FDeviceDialog) then
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
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;
|
||
StatusBar1.Panels[4].Text := 'Discovering...';
|
||
FNetwork.DirectIP := '';
|
||
TDiscoverThread.Create(FNetwork, Self);
|
||
end;
|
||
|
||
procedure TMainForm.BtnStartStopClick(Sender: TObject);
|
||
var
|
||
Idx, i: Integer;
|
||
Dev: THPSDRDevice;
|
||
GenPkt: TGeneralPacket;
|
||
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;
|
||
StatusBar1.Panels[3].Text := 'RX: waiting...';
|
||
// Сохраняем текущую частоту/настройки чтобы следующий 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);
|
||
StatusBar1.Panels[4].Text := 'Not connected';
|
||
StatusBar1.Panels[0].Text := 'IP: --';
|
||
StatusBar1.Panels[1].Text := 'Board: --';
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
|
||
// --- START ---
|
||
AutoIP := '';
|
||
FPendingBoardType := 0;
|
||
|
||
// Явный выбор через 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;
|
||
StatusBar1.Panels[3].Text := 'Opening WDSP...';
|
||
if FWDSPReady then
|
||
// WDSP уже открыт — сразу подключаемся
|
||
DoConnectDevice(Dev)
|
||
else
|
||
// Открываем в фоновом потоке
|
||
TWDSPOpenThread.Create(Self);
|
||
end;
|
||
|
||
procedure TMainForm.OnWDSPOpenDone(Success: Boolean);
|
||
begin
|
||
// Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке
|
||
FWDSPReady := Success;
|
||
if not Success then
|
||
begin
|
||
StatusBar1.Panels[3].Text := 'WDSP open failed';
|
||
BtnStartStop.Enabled := True;
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
ShowMessage('WDSP not loaded.' + LineEnding +
|
||
IfThen(FDSPEngine.LastError <> '', FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding +
|
||
'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).');
|
||
Exit;
|
||
end;
|
||
DoConnectDevice(FPendingDev);
|
||
end;
|
||
|
||
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
||
var
|
||
GenPkt: TGeneralPacket;
|
||
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];
|
||
// Slider position (0..100) and calibrated drive byte
|
||
TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
|
||
FDriveLevel := CalcDriveByte;
|
||
FCurrentBand := G_Settings.LastBand;
|
||
// SampleRate — глобальный, загружаем до RestoreBand
|
||
if G_Settings.SampleRate > 0 then
|
||
begin
|
||
FSampleRate := G_Settings.SampleRate;
|
||
FSpanHz := FSampleRate;
|
||
end;
|
||
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
|
||
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
|
||
BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton;
|
||
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
|
||
FWfAGCEnabled := G_Settings.WfAGCEnabled;
|
||
FWfNFEnabled := G_Settings.WfNFEnabled;
|
||
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). Если в настройках выбрана конкретная
|
||
// soundcard — открываем её и принудительно переключаем MicSource на SoundCard,
|
||
// иначе пользователь нажмёт MOX и не поймёт почему его микрофон молчит.
|
||
if G_Settings.AudioInDevice <> '' then
|
||
begin
|
||
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
|
||
FAudioInDevName := G_Settings.AudioInDevice;
|
||
FAudioIn.Open;
|
||
FTXSettings.MicSource := 1; // SoundCard
|
||
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);
|
||
|
||
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
|
||
StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType);
|
||
|
||
// General packet
|
||
FillChar(GenPkt, SizeOf(GenPkt), 0);
|
||
GenPkt.Command := CMD_GENERAL;
|
||
GenPkt.Flags37 := $08;
|
||
GenPkt.Flags38 := $01;
|
||
GenPkt.PAConfig := $01;
|
||
if FNetwork.Device.BoardType = 5 then
|
||
GenPkt.AlexEnable := $03
|
||
else
|
||
GenPkt.AlexEnable := $01;
|
||
FNetwork.SendGeneralPacket(GenPkt);
|
||
|
||
if FNetwork.Device.BoardType in [3, 4, 5] then
|
||
FActiveDDC := 2
|
||
else
|
||
FActiveDDC := 0;
|
||
|
||
// Синхронизируем 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;
|
||
|
||
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
|
||
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0);
|
||
SendDUCSpecificFromSettings;
|
||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||
ApplyTXSettingsToDSP;
|
||
ApplyModeFilter;
|
||
|
||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||
if FWDSPReady then
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета
|
||
|
||
BtnStartStop.Caption := 'STOP';
|
||
StyleButton(BtnStartStop, True);
|
||
StatusBar1.Panels[4].Text := '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);
|
||
end;
|
||
FSpecView.InvalidateRulerCache;
|
||
SyncSpecViewFreq;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.UpdateFilterButtons;
|
||
var
|
||
i: Integer;
|
||
Names: array[0..FILT_COUNT-1] of string;
|
||
DefIdx: Integer;
|
||
begin
|
||
case FMode of
|
||
0, 1: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
|
||
DefIdx := FILT_SSB_DEF;
|
||
end;
|
||
3, 4: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
|
||
DefIdx := FILT_CW_DEF;
|
||
end;
|
||
5: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_FM_NAMES[i];
|
||
DefIdx := FILT_FM_DEF;
|
||
end;
|
||
else begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
|
||
DefIdx := FILT_AM_DEF;
|
||
end;
|
||
end;
|
||
|
||
FFilter := DefIdx;
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[DefIdx];
|
||
3, 4: FFilterBW := FILT_CW_BW[DefIdx];
|
||
5: FFilterBW := FILT_FM_BW[DefIdx];
|
||
else FFilterBW := FILT_AM_BW[DefIdx];
|
||
end;
|
||
|
||
for i := 0 to FILT_COUNT-1 do
|
||
begin
|
||
BtnFilter[i].Caption := Names[i];
|
||
StyleButton(BtnFilter[i], i = FFilter);
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ApplyVfoA — единая точка смены частоты VFO A
|
||
//
|
||
// CTUN ВЫКЛ (классический режим):
|
||
// FCenterFreq = FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
||
//
|
||
// CTUN ВКЛ (piHPSDR/Thetis режим):
|
||
// FCenterFreq статичен — спектр/водопад не двигаются
|
||
// FVfoA движется по дисплею (маркер гуляет)
|
||
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
|
||
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
|
||
// hits the edge of the display")
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
||
var
|
||
Offset: Double;
|
||
FiltLo: Double;
|
||
FiltHi: Double;
|
||
HalfSpan: Double;
|
||
Scrolled: Boolean;
|
||
BandIdx: Integer;
|
||
begin
|
||
// Клэмп в пределах 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);
|
||
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;
|
||
|
||
// Перерисовываем спектр всегда (маркер и полоса двигаются)
|
||
// Если идёт drag — не вызываем Draw напрямую, таймер подхватит
|
||
SyncSpecViewFreq;
|
||
if FSpecDrag then
|
||
FSpectrumDirty := True
|
||
else
|
||
begin
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if Scrolled then
|
||
begin
|
||
FSpecView.DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CTUN toggle
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.BtnCTunClick(Sender: TObject);
|
||
begin
|
||
FCTun := not FCTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
FBandCache[FCurrentBand].CTun := FCTun;
|
||
if not FCTun then
|
||
begin
|
||
// Выключили CTUN: центрируемся на VFO, shift=0
|
||
FCenterFreq := FVfoA;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(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;
|
||
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 Button = mbLeft then
|
||
begin
|
||
// Левая кнопка: сбрасываем маркер если активен
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerActive := False;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
FSpecDrag := True;
|
||
FSpecDragX0 := X;
|
||
if FCTun then FSpecDragFreq := FCenterFreq
|
||
else begin
|
||
if FActiveVfo = 0 then FSpecDragFreq := FVfoA
|
||
else FSpecDragFreq := FVfoB;
|
||
end;
|
||
PbSpectrum.Cursor := crSizeWE;
|
||
end
|
||
else if Button = mbRight then
|
||
begin
|
||
// Правая кнопка: toggle маркера
|
||
if FSpecView.MarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FSpecView.MarkerX) < 8) then
|
||
begin
|
||
// Клик рядом с текущим маркером — убираем
|
||
FSpecView.MarkerActive := False;
|
||
end
|
||
else
|
||
begin
|
||
// Ставим маркер в новое место
|
||
FSpecView.MarkerActive := True;
|
||
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
|
||
end;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
begin
|
||
if FSpecDrag and (ssLeft in Shift) then
|
||
DoSpectrumDrag(X, PbSpectrum.Width);
|
||
// Маркер следует за курсором — обновление произойдёт в ближайшем тике таймера (50ms)
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if (Abs(X - FSpecDragX0) < 4) then
|
||
DoSpectrumClick(X, PbSpectrum.Width); // это был клик, не драг
|
||
FSpecDrag := False;
|
||
PbSpectrum.Cursor := crDefault;
|
||
end;
|
||
end;
|
||
|
||
// Водопад (аналогично)
|
||
procedure TMainForm.PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerActive := False;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
FSpecDrag := True;
|
||
FSpecDragX0 := X;
|
||
if FCTun then FSpecDragFreq := FCenterFreq
|
||
else begin
|
||
if FActiveVfo = 0 then FSpecDragFreq := FVfoA
|
||
else FSpecDragFreq := FVfoB;
|
||
end;
|
||
PbWaterfall.Cursor := crSizeWE;
|
||
end
|
||
else if Button = mbRight then
|
||
begin
|
||
if FSpecView.MarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FSpecView.MarkerX) < 8) then
|
||
FSpecView.MarkerActive := False
|
||
else
|
||
begin
|
||
FSpecView.MarkerActive := True;
|
||
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
end;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
begin
|
||
if FSpecDrag and (ssLeft in Shift) then
|
||
DoSpectrumDrag(X, PbWaterfall.Width);
|
||
if FSpecView.MarkerActive then
|
||
begin
|
||
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if (Abs(X - FSpecDragX0) < 4) then
|
||
DoSpectrumClick(X, PbWaterfall.Width);
|
||
FSpecDrag := False;
|
||
PbWaterfall.Cursor := crDefault;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnBandClick(Sender: TObject);
|
||
var
|
||
NewBand: Integer;
|
||
begin
|
||
NewBand := (Sender as TFlatButton).Tag;
|
||
if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit;
|
||
// Если в 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: верхний срез -100 Гц
|
||
1: begin Lo := 100; Hi := FFilterBW; end; // USB
|
||
2: begin Lo := -Half; Hi := Half; end; // DSB
|
||
3: begin Lo := -Half; Hi := Half; end; // CWL
|
||
4: begin Lo := -Half; Hi := Half; end; // CWU
|
||
5: begin Lo := -Half; Hi := Half; end; // FM
|
||
6: begin Lo := -Half; Hi := Half; end; // AM
|
||
7: begin Lo := -Half; Hi := Half; end; // SAM
|
||
else Lo := -Half; Hi := Half;
|
||
end;
|
||
if FWDSPReady then begin
|
||
FDSPEngine.SetFilter(Lo, Hi);
|
||
FDSPEngine.SetTXFilter(Lo, Hi);
|
||
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];
|
||
3, 4: FFilterBW := FILT_CW_BW[N];
|
||
5: FFilterBW := FILT_FM_BW[N];
|
||
else FFilterBW := FILT_AM_BW[N]; // DSB, AM, SAM
|
||
end;
|
||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq;
|
||
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;
|
||
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;
|
||
end;
|
||
if FWDSPReady then
|
||
begin
|
||
// Переключаем источник микрофона: при TX из браузера — txmsWeb,
|
||
// при TX с кнопки без веб-клиента — штатный источник из настроек.
|
||
if FTransmitting and Assigned(FWebServer) and FWebServer.WebClientActive then
|
||
begin
|
||
FSavedMicSource := TTXMicSource(FTXSettings.MicSource);
|
||
FWebMicActive := True;
|
||
FDSPEngine.SetTXMicSource(txmsWeb);
|
||
end
|
||
else if not FTransmitting and FWebMicActive then
|
||
begin
|
||
FWebMicActive := False;
|
||
FDSPEngine.SetTXMicSource(FSavedMicSource);
|
||
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.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
|
||
FTransmitting, True, True);
|
||
FNetwork.ClearDUCIQQueue;
|
||
FNetwork.SendFullHP;
|
||
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.BtnHidePanelClick(Sender: TObject);
|
||
begin
|
||
FPanelHidden := not FPanelHidden;
|
||
if FPanelHidden then
|
||
begin
|
||
PanelLeft.Width := 0;
|
||
BtnHidePanel.Caption := '▶';
|
||
FVfoOverlay.Parent := PanelRight;
|
||
FVfoOverlay.Width := 260;
|
||
FVfoOverlay.Height := 130;
|
||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||
FVfoOverlay.Visible := True;
|
||
FVfoOverlay.BringToFront;
|
||
PositionVfoOverlay;
|
||
end
|
||
else
|
||
begin
|
||
PanelLeft.Width := 232;
|
||
BtnHidePanel.Caption := '◀';
|
||
FVfoOverlay.Visible := False;
|
||
end;
|
||
ResizeSpectrumPanels;
|
||
ResizeSMeter;
|
||
end;
|
||
|
||
procedure TMainForm.PositionVfoOverlay;
|
||
var
|
||
VfoX, OW, SpTop: Integer;
|
||
begin
|
||
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
||
OW := FVfoOverlay.Width;
|
||
SpTop := PbSpectrum.Top; // смещение PbSpectrum внутри PanelRight
|
||
|
||
if PbSpectrum.Width > 0 then
|
||
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * PbSpectrum.Width)
|
||
else
|
||
VfoX := PbSpectrum.Width div 2;
|
||
|
||
// Всегда справа от VFO-линии
|
||
FVfoOverlay.Left := Min(PanelRight.Width - OW - 4, VfoX + 6);
|
||
FVfoOverlay.Top := SpTop + 6;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
|
||
begin
|
||
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
|
||
end;
|
||
|
||
procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||
begin
|
||
FMode := Mode;
|
||
FFilterBW := FilterBW;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
ApplyModeFilter;
|
||
end;
|
||
SyncSpecViewFreq;
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnSpanClick(Sender: TObject);
|
||
var
|
||
NewRate: Integer;
|
||
DDCRate: Word;
|
||
begin
|
||
NewRate := (Sender as TFlatButton).Tag;
|
||
if NewRate = FSampleRate then Exit;
|
||
|
||
FSampleRate := NewRate;
|
||
FSpanHz := NewRate;
|
||
DDCRate := NewRate div 1000;
|
||
|
||
// Обновляем кнопки сразу — UI отзывчив
|
||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
|
||
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
|
||
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
|
||
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
|
||
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
|
||
Application.ProcessMessages;
|
||
|
||
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
|
||
// SampleRate — глобальный: сохраняем в global settings
|
||
if FDevConnected then
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
|
||
// 1. Останавливаем WDSP и сбрасываем очередь старых пакетов
|
||
if FWDSPReady then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open
|
||
end;
|
||
|
||
// 2. Отправляем новый rate трансиверу
|
||
// (после остановки DSP — новые пакеты сразу идут с правильным rate)
|
||
if FNetwork.Connected then
|
||
FNetwork.ConfigureDDCs(1, DDCRate, 0);
|
||
|
||
// 3. Восстанавливаем настройки DSP
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
if FNetwork.Connected then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока)
|
||
// Все изменения состояния выполняем через TThread.Synchronize чтобы
|
||
// не трогать UI и WDSP из чужого потока.
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.WebOnFreq(Hz: Double);
|
||
begin
|
||
FWebSyncFreq := Hz;
|
||
FWebSyncM := SyncWebFreq;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebFreq;
|
||
var BandIdx: Integer;
|
||
begin
|
||
// Веб шлёт "freq" для активного VFO
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FWebSyncFreq))
|
||
else
|
||
begin
|
||
FVfoB := FWebSyncFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FRunning then
|
||
FNetwork.SetRunAndFreq(True, 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];
|
||
3,4: FFilterBW := FILT_CW_BW[Idx];
|
||
5: FFilterBW := FILT_FM_BW[Idx];
|
||
else FFilterBW := FILT_AM_BW[Idx];
|
||
end;
|
||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||
ApplyModeFilter;
|
||
SyncSpecViewFreq;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
end;
|
||
Exit;
|
||
end;
|
||
// Positive value = bandwidth in Hz, find closest filter
|
||
FFilterBW := FWebSyncInt;
|
||
case FMode of
|
||
0,1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
||
3,4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
||
5: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
|
||
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
||
end;
|
||
best := 0; dist := MaxInt;
|
||
for i := 0 to FILT_COUNT-1 do
|
||
if Abs(BW_arr[i] - FWebSyncInt) < dist then
|
||
begin
|
||
dist := Abs(BW_arr[i] - FWebSyncInt);
|
||
best := i;
|
||
end;
|
||
FFilter := best;
|
||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||
ApplyModeFilter;
|
||
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;
|
||
var
|
||
ValidSpans: array[0..5] of Integer;
|
||
Found: Boolean;
|
||
i: Integer;
|
||
Btn: TFlatButton;
|
||
begin
|
||
ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000;
|
||
ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000;
|
||
Found := False;
|
||
for i := 0 to 5 do
|
||
if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end;
|
||
if not Found then Exit;
|
||
if FWebSyncInt = FSampleRate then Exit;
|
||
Btn := nil;
|
||
if FWebSyncInt = 48000 then Btn := BtnSpan48k
|
||
else if FWebSyncInt = 96000 then Btn := BtnSpan96k
|
||
else if FWebSyncInt = 192000 then Btn := BtnSpan192k
|
||
else if FWebSyncInt = 384000 then Btn := BtnSpan384k
|
||
else if FWebSyncInt = 768000 then Btn := BtnSpan768k
|
||
else if FWebSyncInt = 1536000 then Btn := BtnSpan1536k;
|
||
if Assigned(Btn) then BtnSpanClick(Btn);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnVolume(V: Integer);
|
||
begin
|
||
FWebSyncInt := V;
|
||
FWebSyncM := SyncWebVolume;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebVolume;
|
||
begin
|
||
FVolume := Max(0, Min(100, FWebSyncInt));
|
||
TrkVolume.Position := FVolume;
|
||
if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnWfAGC(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebWfAGC;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebWfAGC;
|
||
begin
|
||
FWfAGCEnabled := FWebSyncBool;
|
||
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.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
|
||
Singles: array[0..5759] of Single;
|
||
Buf: array[0..5759] of Double;
|
||
i, n: Integer;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
n := Count;
|
||
if n > Length(Buf) then n := Length(Buf);
|
||
Move(Samples^, Singles[0], n * SizeOf(Single));
|
||
for i := 0 to n - 1 do
|
||
Buf[i] := Singles[i];
|
||
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;
|
||
var
|
||
Cal: Double;
|
||
Pos: Integer;
|
||
XvtrActive: Boolean;
|
||
begin
|
||
if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then
|
||
Cal := FPABandCal[FCurrentBand]
|
||
else
|
||
Cal := 100.0;
|
||
|
||
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
|
||
|
||
// Pos — итоговый процент драйва 0..100 ДО per-band калибровки.
|
||
//
|
||
// Логика (как в Thetis):
|
||
// • Регулярный TX:
|
||
// HF → Pos = TrkDrive.Position
|
||
// XVTR → Pos = TrkDrive.Position × (XVTR.TXPower / 100)
|
||
// (TXPower работает как верхний предел драйва на трансверторе)
|
||
// • TUN (кнопка):
|
||
// HF → Pos = TUNLevel (TTXSettings.TUNLevel)
|
||
// XVTR, UseXVTRTunePower=False → Pos = TUNLevel (поведение как HF)
|
||
// XVTR, UseXVTRTunePower=True → Pos = XVTR.TXPower
|
||
// (галка ЗАМЕНЯЕТ TUNLevel на XVTR power — Thetis-семантика)
|
||
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;
|
||
|
||
Result := Round(Pos / 100.0 * Cal / 100.0 * 255.0);
|
||
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.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, DDC IN routing, T/R relay suppression.
|
||
procedure TMainForm.ApplyXvtrToNetwork;
|
||
var
|
||
En: Boolean;
|
||
UseDDCIn: Boolean;
|
||
DisablePA: Boolean;
|
||
RxAnt: Byte;
|
||
begin
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
En := True;
|
||
// XVTRRF=True означает что трансвертер использует HF тракт — не нужно
|
||
// переключаться на отдельный XVTR DDC IN на Alex. Default False →
|
||
// подключаем XVTR-IF к выделенному пину XVTR DDC IN.
|
||
UseDDCIn := not FXvtrSettings.Entries[FCurrentXvtr].XVTRRF;
|
||
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
|
||
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
|
||
end
|
||
else
|
||
begin
|
||
En := False; UseDDCIn := False; DisablePA := False; RxAnt := 0;
|
||
end;
|
||
if FNetwork.Connected then
|
||
FNetwork.SetXvtrMode(En, UseDDCIn, 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; Inc(Y, 76);
|
||
PanelFilter.Top := Y; Inc(Y, 76);
|
||
BtnCTun.Top := Y; BtnDUP.Top := Y; Inc(Y, 30); // BTN_H(26) + 4
|
||
PanelRX.Top := Y;
|
||
end;
|
||
|
||
procedure TMainForm.RebuildXvtrButtons;
|
||
const
|
||
BTN_H_LOC = 24;
|
||
var
|
||
i, Pos, XvtrRows: Integer;
|
||
W: 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;
|
||
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 := 16 + (2 + (Pos div 6)) * 27;
|
||
B.Width := W - 2;
|
||
B.Height := BTN_H_LOC;
|
||
B.OnClick := BtnXvtrBandClick;
|
||
B.Tag := -(i + 1); // отрицательное значение = XVTR индекс
|
||
BtnXvtrBand[i] := B;
|
||
StyleButton(B, i = FCurrentXvtr);
|
||
Inc(Pos);
|
||
end;
|
||
// Динамически подгоняем высоту PanelBands под реальное число XVTR-рядов
|
||
if Pos = 0 then XvtrRows := 0
|
||
else if Pos <= 6 then XvtrRows := 1
|
||
else XvtrRows := 2;
|
||
PanelBands.Height := 74 + XvtrRows * 27;
|
||
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;
|
||
begin
|
||
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
|
||
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
|
||
FCurrentXvtr := Idx;
|
||
// Выбор начальной частоты: LastFreq если в диапазоне, иначе середина
|
||
Vis := FXvtrSettings.Entries[Idx].LastFreq;
|
||
if (Vis < FXvtrSettings.Entries[Idx].FreqBegin) or
|
||
(Vis > FXvtrSettings.Entries[Idx].FreqEnd) then
|
||
Vis := (FXvtrSettings.Entries[Idx].FreqBegin +
|
||
FXvtrSettings.Entries[Idx].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;
|
||
// VFO на видимую частоту; FCenterFreq тоже visible (как и для HF — DDC переход
|
||
// в IF происходит только в момент UpdateState/SetRunAndFreq через XvtrTranslate).
|
||
FVfoA := Vis;
|
||
FCenterFreq := Vis;
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
// Сбросить CTUN — не имеет смысла в начальном состоянии XVTR-band
|
||
if FCTun then
|
||
begin
|
||
FCTun := False;
|
||
StyleButton(BtnCTun, False);
|
||
end;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
// Применяем драйв (если есть кастомный TX power для этого XVTR)
|
||
// Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей.
|
||
// Отправляем в сеть с трансляцией
|
||
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 (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||
else if ssCtrl in Shift then Step := 1000
|
||
else if ssShift in Shift then Step := 10
|
||
else Step := 100;
|
||
|
||
Delta := 1;
|
||
if WheelDelta < 0 then Delta := -1;
|
||
|
||
// Квантование (та же логика что в FreqDisplay.ChangeByDigit)
|
||
Base := (Round(FVfoA) div Step) * Step;
|
||
if Delta > 0 then
|
||
NewFreq := Base + Step
|
||
else
|
||
begin
|
||
if Round(FVfoA) = Base then
|
||
NewFreq := Base - Step
|
||
else
|
||
NewFreq := Base; // снэп к нижней границе
|
||
end;
|
||
|
||
NewFreq := Max(30000, Min(60000000, NewFreq));
|
||
ApplyVfoA(NewFreq);
|
||
|
||
Handled := True;
|
||
end;
|
||
|
||
|
||
procedure TMainForm.BtnAGCModeClick(Sender: TObject);
|
||
const
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (
|
||
agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := (Sender as TFlatButton).Tag;
|
||
FAGCMode := N;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = N);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetAGC(AGCModes[N], 50.0);
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
end;
|
||
|
||
procedure TMainForm.TrkAGCChange(Sender: TObject);
|
||
begin
|
||
FAGCTop := TrkAGC.Position;
|
||
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
FSpecView.AGCTop := FAGCTop;
|
||
FSpecView.DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// DSP/Audio callbacks — вызываются из рабочих потоков
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.OnAudioReady(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
var
|
||
MonoBuf: array[0..1023] of Single;
|
||
i: Integer;
|
||
begin
|
||
if Assigned(FWebServer) and FWebServer.WebClientActive then
|
||
begin
|
||
for i := 0 to Count - 1 do
|
||
MonoBuf[i] := (Left[i] + Right[i]) * 0.5;
|
||
FWebServer.PushAudio(@MonoBuf[0], Count);
|
||
Exit;
|
||
end;
|
||
FAudioOut.Write(Left, Right, Count);
|
||
end;
|
||
|
||
procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := Min(Count, 1024);
|
||
for i := 0 to N - 1 do
|
||
FSpectrumBuf[i] := Pixels[i]; // local copy for WebServer
|
||
FSpectrumBufCount := N;
|
||
FSpecView.SetSpectrumData(Pixels, Count);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := Min(Count, 1024);
|
||
for i := 0 to N - 1 do
|
||
FWaterfallBuf[i] := Pixels[i]; // local copy for WebServer
|
||
FWaterfallBufCount := N;
|
||
Inc(FWaterfallFrameCounter);
|
||
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
|
||
begin
|
||
FWaterfallFrameCounter := 0;
|
||
FSpecView.SetWaterfallData(Pixels, Count);
|
||
FWaterfallDirty := True;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Settings apply methods (вызываются из TSettingsForm)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||
SpecAvgTimeMS: Double);
|
||
begin
|
||
if FWDSPReady then
|
||
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetFFTParams(FFTSize, WinType);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||
begin
|
||
if FWDSPReady then
|
||
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS);
|
||
FWfManualHigh := WfHigh;
|
||
FWfManualLow := WfLow;
|
||
FWfAGCOffset := WfAGCOffset;
|
||
FSpecView.WfManualHigh := FWfManualHigh;
|
||
FSpecView.WfManualLow := FWfManualLow;
|
||
FSpecView.WfAGCOffset := FWfAGCOffset;
|
||
FSpecView.ResetWfAvgBuf;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||
begin
|
||
FSpecRefLevel := RefLevel;
|
||
FSpecRange := Range;
|
||
if GridStep > 0 then FSpecGridStep := GridStep;
|
||
// На передаче FSpecView показывает 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
|
||
if not FAudioOut.Open then
|
||
StatusBar1.Panels[3].Text := 'Audio out: ' + FAudioOut.LastError
|
||
else
|
||
StatusBar1.Panels[3].Text := 'Audio out: ' + IfThen(DevName = '', 'Default', DevName);
|
||
except
|
||
on E: Exception do
|
||
StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||
begin
|
||
if FAudioIn.IsOpen then
|
||
FAudioIn.Close;
|
||
FAudioIn.DeviceIndex := DevIndex;
|
||
FAudioInDevName := DevName;
|
||
if DevIndex >= 0 then
|
||
begin
|
||
try
|
||
if not FAudioIn.Open then
|
||
StatusBar1.Panels[3].Text := 'Mic: ' + FAudioIn.LastError
|
||
else
|
||
StatusBar1.Panels[3].Text := 'Mic: ' + DevName;
|
||
except
|
||
on E: Exception do
|
||
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
|
||
end;
|
||
// Автопереключение mic-источника: явный выбор PortAudio-устройства
|
||
// → берём mic со звуковой карты, иначе HW mic-стрим бы перебивал его.
|
||
FTXSettings.MicSource := 1; // SoundCard
|
||
end
|
||
else
|
||
begin
|
||
StatusBar1.Panels[3].Text := 'Mic: none';
|
||
// Нет выбранного soundcard mic → возвращаемся на HW mic-стрим.
|
||
FTXSettings.MicSource := 0; // Radio
|
||
end;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetTXMicSource(TTXMicSource(FTXSettings.MicSource));
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||
FSettings.Save;
|
||
end;
|
||
// Если открыт диалог настроек — синхронизировать UI
|
||
if Assigned(FSettingsForm) then
|
||
TSettingsForm(FSettingsForm).LoadTXSettings(FTXSettings);
|
||
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.OnVisibilityChange := ApplyVisibility;
|
||
SF.OnFPSChange := ApplyFPS;
|
||
SF.OnPAChange := OnPASettingsChange;
|
||
SF.OnCATChange := OnCATSettingsChange;
|
||
SF.OnThemeChange := SetLightTheme;
|
||
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
|
||
SF.OnTXChange := OnTXSettingsChange;
|
||
SF.OnAlexChange := OnAlexSettingsChange;
|
||
SF.OnXvtrChange := OnXvtrSettingsChange;
|
||
SF.OnWfAGCNFChange := ApplyWfAGCNF;
|
||
end;
|
||
SF := TSettingsForm(FSettingsForm);
|
||
SF.LoadTXSettings(FTXSettings);
|
||
SF.LoadAlexSettings(FAlexSettings,
|
||
IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType));
|
||
SF.LoadXvtrSettings(FXvtrSettings);
|
||
SF.LoadPASettings(FPAMaxPower, FPABandCal);
|
||
SF.LoadCATSettings(
|
||
FCATLastGlobal.CATSerialEnabled,
|
||
FCATLastGlobal.CATSerialPort,
|
||
FCATLastGlobal.CATSerialBaud,
|
||
FCATLastGlobal.CATSerialDataBits,
|
||
FCATLastGlobal.CATSerialStopBits,
|
||
FCATLastGlobal.CATSerialParity,
|
||
FCATLastGlobal.CATTcpEnabled,
|
||
FCATLastGlobal.CATTcpPort);
|
||
// Перечисляем PA устройства
|
||
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
|
||
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
|
||
SF.LoadWfAGCNF(FWfAGCEnabled, FWfNFEnabled);
|
||
SF.LoadFPS(FDisplayFPS);
|
||
SF.LoadLightTheme(FLightTheme);
|
||
SF.LoadFreqMhzDigits(FFreqMhzDigits);
|
||
// Загружаем текущие значения
|
||
if FWDSPReady then
|
||
SF.LoadValues(
|
||
FDSPEngine.FFTSize,
|
||
FDSPEngine.WindowType,
|
||
FDSPEngine.SpecDetector,
|
||
FDSPEngine.SpecAvgMode,
|
||
FDSPEngine.SpecAvgTimeMS,
|
||
FDSPEngine.WfDetector,
|
||
FDSPEngine.WfAvgMode,
|
||
FDSPEngine.WfAvgTimeMS,
|
||
FWfManualHigh,
|
||
FWfManualLow,
|
||
FWfAGCOffset,
|
||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||
FAudioOutDevName, FAudioInDevName)
|
||
else
|
||
SF.LoadValues(
|
||
131072, 2, 0, 3, 30.0,
|
||
0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset,
|
||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||
FAudioOutDevName, FAudioInDevName);
|
||
SF.Show;
|
||
end;
|
||
|
||
procedure TMainForm.AfterShowTick(Sender: TObject);
|
||
begin
|
||
// Вызывается один раз через 200 мс после старта формы
|
||
FAfterShowTimer.Enabled := False;
|
||
|
||
// Открываем PortAudio — теперь FPC уже перехватывает сигналы
|
||
try
|
||
if not FAudioOut.Open then
|
||
StatusBar1.Panels[3].Text := 'Audio: ' + FAudioOut.LastError
|
||
else
|
||
StatusBar1.Panels[3].Text := 'Audio OK (PortAudio)';
|
||
except
|
||
on E: Exception do
|
||
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
|
||
end;
|
||
|
||
EnsureWDSPWisdom;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// CAT subsystem
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.InitCATEngine;
|
||
var
|
||
Ctx: TCATContext;
|
||
begin
|
||
FillChar(Ctx, SizeOf(Ctx), 0);
|
||
Ctx.GetVfoA := CATGetVfoA;
|
||
Ctx.GetVfoB := CATGetVfoB;
|
||
Ctx.GetMode := CATGetMode;
|
||
Ctx.GetActiveVfo := CATGetActiveVfo;
|
||
Ctx.GetAGCMode := CATGetAGCMode;
|
||
Ctx.GetVolume := CATGetVolume;
|
||
Ctx.GetDriveLevel := CATGetDriveLevel;
|
||
Ctx.GetFilterIdx := CATGetFilterIdx;
|
||
Ctx.GetFilterBW := CATGetFilterBW;
|
||
Ctx.GetNRMode := CATGetNRMode;
|
||
Ctx.GetNBMode := CATGetNBMode;
|
||
Ctx.GetSNBEnabled := CATGetSNB;
|
||
Ctx.GetANFEnabled := CATGetANF;
|
||
Ctx.GetTransmitting := CATGetTX;
|
||
Ctx.GetRunning := CATGetRunning;
|
||
Ctx.GetSMeter := CATGetSMeter;
|
||
Ctx.GetCurrentBand := CATGetBand;
|
||
Ctx.SetVfoA := CATSetVfoA;
|
||
Ctx.SetVfoB := CATSetVfoB;
|
||
Ctx.SetMode := WebOnMode;
|
||
Ctx.SetActiveVfo := WebOnActiveVfo;
|
||
Ctx.SetAGCMode := WebOnAGC;
|
||
Ctx.SetVolume := WebOnVolume;
|
||
Ctx.SetDriveLevel := WebOnDrive;
|
||
Ctx.SetFilterIdx := CATSetFilterIdx;
|
||
Ctx.SetNRMode := WebOnNR;
|
||
Ctx.SetNBMode := WebOnNB;
|
||
Ctx.SetSNBEnabled := WebOnSNB;
|
||
Ctx.SetANFEnabled := WebOnANF;
|
||
Ctx.SetTransmitting := WebOnMOX;
|
||
Ctx.DoBandUp := CATDoBandUp;
|
||
Ctx.DoBandDown := CATDoBandDown;
|
||
Ctx.DoTuneUp := CATDoTuneUp;
|
||
Ctx.DoTuneDown := CATDoTuneDown;
|
||
Ctx.DoBandByIndex := WebOnBand;
|
||
FCATEngine := TCATEngine.Create(Ctx);
|
||
FCATSerial := TCATSerialManager.Create(FCATEngine);
|
||
FCATTcp := TCATTcpServer.Create(FCATEngine);
|
||
end;
|
||
|
||
procedure TMainForm.CATApplySettings(const G: TGlobalSettings);
|
||
var
|
||
Cfgs: array[0..3] of TCATSerialConfig;
|
||
i: Integer;
|
||
begin
|
||
for i := 0 to 3 do
|
||
begin
|
||
Cfgs[i].Enabled := G.CATSerialEnabled[i];
|
||
Cfgs[i].PortName := G.CATSerialPort[i];
|
||
Cfgs[i].BaudRate := G.CATSerialBaud[i];
|
||
Cfgs[i].DataBits := G.CATSerialDataBits[i];
|
||
Cfgs[i].StopBits := G.CATSerialStopBits[i];
|
||
case G.CATSerialParity[i] of
|
||
1: Cfgs[i].Parity := cspOdd;
|
||
2: Cfgs[i].Parity := cspEven;
|
||
else Cfgs[i].Parity := cspNone;
|
||
end;
|
||
end;
|
||
FCATSerial.ApplyConfig(Cfgs);
|
||
FCATTcp.Stop;
|
||
if G.CATTcpEnabled then
|
||
begin
|
||
FCATTcp.Port := G.CATTcpPort;
|
||
FCATTcp.Start;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.OnCATSettingsChange(
|
||
const SerEnabled: array of Boolean;
|
||
const SerPort: array of string;
|
||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||
TcpEnabled: Boolean; TcpPort: Integer);
|
||
var i: Integer;
|
||
begin
|
||
for i := 0 to 3 do
|
||
begin
|
||
if i <= High(SerEnabled) then FCATLastGlobal.CATSerialEnabled[i] := SerEnabled[i];
|
||
if i <= High(SerPort) then FCATLastGlobal.CATSerialPort[i] := SerPort[i];
|
||
if i <= High(SerBaud) then FCATLastGlobal.CATSerialBaud[i] := SerBaud[i];
|
||
if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i];
|
||
if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i];
|
||
if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i];
|
||
end;
|
||
FCATLastGlobal.CATTcpEnabled := TcpEnabled;
|
||
FCATLastGlobal.CATTcpPort := TcpPort;
|
||
CATApplySettings(FCATLastGlobal);
|
||
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.
|